spark_model/layers/moe_v41/
single.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! The single-token expert path: the plan that groups a token's selected
5//! experts, the pointer table for them, the routed m=1 compute that sums
6//! the expert rows in plan order, and the host stage / device compute halves
7//! a captured decode step runs. Split from `moe_v41/forward.rs` (500-LoC cap)
8//! when the line was re-cut onto main.
9
10use anyhow::{Result, ensure};
11use spark_runtime::gpu::{DevicePtr, GpuBackend};
12use spark_runtime::weights::expert_stream::{ExpertLru, ExpertSource};
13
14use super::{MoeV41, MoeV41LayerWeights, MoeV41Stage, MoeV41Timing, RouterWeights, prefetch_k};
15use crate::layers::ops::{
16    kquant_mmvq_experts_wn, kquant_q8_1_rows, kquant_q8_1_rows_bytes, kquant_swiglu_q8_1_rows,
17};
18
19impl MoeV41 {
20    /// Group the `(token, k)` assignments by expert (ascending expert id):
21    /// the token rows and routing weights group-major, and the plan
22    /// `(first assignment index, row offset, rows)` per group.
23    #[allow(clippy::type_complexity)]
24    pub(super) fn plan(
25        &self,
26        indices: &[usize],
27        weights: &[f32],
28        m: usize,
29    ) -> (Vec<u8>, Vec<u8>, Vec<(usize, usize, usize)>) {
30        let c = &self.cfg;
31        let mut groups: std::collections::BTreeMap<usize, Vec<(i32, f32, usize)>> =
32            std::collections::BTreeMap::new();
33        for t in 0..m {
34            for kk in 0..c.topk {
35                let a = t * c.topk + kk;
36                groups
37                    .entry(indices[a])
38                    .or_default()
39                    .push((t as i32, weights[a], a));
40            }
41        }
42        let mut rows_host: Vec<u8> = Vec::with_capacity(m * c.topk * 4);
43        let mut w_host: Vec<u8> = Vec::with_capacity(m * c.topk * 4);
44        let mut plan: Vec<(usize, usize, usize)> = Vec::with_capacity(groups.len());
45        for members in groups.values() {
46            let off = rows_host.len() / 4;
47            for &(t, rw, _a) in members {
48                rows_host.extend_from_slice(&t.to_le_bytes());
49                w_host.extend_from_slice(&rw.to_le_bytes());
50            }
51            plan.push((members[0].2, off, members.len()));
52        }
53        (rows_host, w_host, plan)
54    }
55
56    /// The single-token arm's pointer table: gate, up and down block pointers
57    /// of the plan's experts, `3 * ne` entries into `ptrs_dev`. Returns `ne`.
58    pub(super) fn upload_expert_table(
59        &self,
60        gpu: &dyn GpuBackend,
61        plan: &[(usize, usize, usize)],
62        slots: &[spark_runtime::weights::expert_stream::ExpertSlot],
63        stream: u64,
64    ) -> Result<usize> {
65        let ne = plan.len();
66        let mut ptrs: Vec<u8> = Vec::with_capacity(3 * ne * 8);
67        for which in 0..3 {
68            for &(a0, _, _) in plan {
69                let slot = slots[a0];
70                let p = [slot.gate, slot.up, slot.down][which];
71                ptrs.extend_from_slice(&p.0.to_le_bytes());
72            }
73        }
74        gpu.copy_h2d_async(&ptrs, self.ptrs_dev, stream)?;
75        Ok(ne)
76    }
77
78    /// The token's q8_1 rows into `a_q8`, once for the routed experts and the
79    /// shared expert's two input projections (`x_pre` at the readers).
80    pub fn ffn_input_q8_m1(&self, gpu: &dyn GpuBackend, x: DevicePtr, stream: u64) -> Result<()> {
81        kquant_q8_1_rows(
82            gpu,
83            self.k.q8_rows,
84            x,
85            self.a_q8,
86            1,
87            self.cfg.dim as u32,
88            stream,
89        )
90    }
91
92    /// The single-token routed experts from the pointer table: the token
93    /// quantised once (already in `a_q8` when `x_pre`), each projection one
94    /// launch over the `ne` experts, the routing weight folded in at the
95    /// SwiGLU (which writes `h` and its q8_1 rows in one launch), the rows
96    /// summed into `acc` in plan order. Device work only.
97    pub(super) fn routed_m1(
98        &self,
99        gpu: &dyn GpuBackend,
100        x: DevicePtr,
101        ne: usize,
102        x_pre: bool,
103        stream: u64,
104    ) -> Result<()> {
105        let c = &self.cfg;
106        let table = |which: usize| DevicePtr(self.ptrs_dev.0 + (which * ne * 8) as u64);
107        if !x_pre {
108            self.ffn_input_q8_m1(gpu, x, stream)?;
109        }
110        // gate and up in ONE 2 * ne expert batch: the pointer table holds the
111        // ne gate pointers then the ne up pointers contiguously, every entry
112        // reads the same q8_1 token row, and expert e writes row e of
113        // gate_out, so up's rows start at gate_out + ne * inter. Same bytes
114        // per row as the two launches.
115        let up_view = DevicePtr(self.gate_out.0 + (ne * c.inter * 2) as u64);
116        kquant_mmvq_experts_wn(
117            gpu,
118            self.k.mmvq_q2k_experts,
119            table(0),
120            self.a_q8,
121            self.gate_out,
122            c.inter as u32,
123            c.dim as u32,
124            1,
125            2 * ne as u32,
126            0,
127            self.k.experts_warps,
128            stream,
129        )?;
130        kquant_swiglu_q8_1_rows(
131            gpu,
132            self.k.swiglu_q8,
133            self.gate_out,
134            up_view,
135            self.weight_dev,
136            self.h,
137            self.h_q8,
138            ne as u32,
139            c.inter as u32,
140            c.swiglu_limit,
141            stream,
142        )?;
143        kquant_mmvq_experts_wn(
144            gpu,
145            self.k.mmvq_q3k_experts,
146            table(2),
147            self.h_q8,
148            self.down_out,
149            c.dim as u32,
150            c.inter as u32,
151            1,
152            ne as u32,
153            kquant_q8_1_rows_bytes(1, c.inter as u32) as u32,
154            self.k.experts_warps,
155            stream,
156        )?;
157        // All `ne` rows land in the one token row: summed in plan order by a
158        // single kernel. `scatter_add` here (one block per expert row, every
159        // block on the same `acc` row) was an inter-block data race.
160        self.launch_n(gpu, self.k.sum_rows, c.dim, stream, |l| {
161            l.arg_ptr(self.acc)
162                .arg_ptr(self.down_out)
163                .arg_u32(ne as u32)
164                .arg_u32(c.dim as u32)
165        })
166    }
167
168    /// The HOST span of a single-token step between the router GEMV and the
169    /// expert compute: the next layer's router on `x` (prediction, if
170    /// `next`), the selection (drains the stream), the cache fetch with the
171    /// predicted experts started in the background, and the rows / weights /
172    /// pointer-table uploads into fixed buffers. `route_launch` must have
173    /// been issued on `stream` first.
174    pub fn stage_m1<S: ExpertSource + ?Sized>(
175        &self,
176        gpu: &dyn GpuBackend,
177        w: &MoeV41LayerWeights,
178        lru: &mut ExpertLru,
179        src: &S,
180        reader_threads: usize,
181        next: Option<&RouterWeights>,
182        x: DevicePtr,
183        stream: u64,
184    ) -> Result<MoeV41Stage> {
185        let t0 = std::time::Instant::now();
186        let predict = next.filter(|_| prefetch_k() > 0 && lru.has_pool());
187        if let Some(nw) = predict {
188            self.predict_launch(gpu, nw, x, stream)?;
189        }
190        let (weights, indices) = self.route_select(gpu, w, 1, stream)?;
191        let predicted = match predict {
192            Some(nw) => self.predict_select(gpu, nw, prefetch_k(), stream)?,
193            None => Vec::new(),
194        };
195        let t1 = std::time::Instant::now();
196        lru.begin_token();
197        let before = lru.stats();
198        let keys: Vec<(u32, u32)> = indices.iter().map(|&e| (w.layer, e as u32)).collect();
199        let slots = lru.fetch_many_on(gpu, stream, src, &keys, &predicted, reader_threads)?;
200        let after = lru.stats();
201        let t2 = std::time::Instant::now();
202        let (rows_host, w_host, plan) = self.plan(&indices, &weights, 1);
203        gpu.copy_h2d_async(&rows_host, self.rows_dev, stream)?;
204        gpu.copy_h2d_async(&w_host, self.weight_dev, stream)?;
205        let ne = self.upload_expert_table(gpu, &plan, &slots, stream)?;
206        Ok(MoeV41Stage {
207            ne,
208            weights,
209            indices,
210            timing: MoeV41Timing {
211                route_ms: (t1 - t0).as_secs_f64() * 1e3,
212                fetch_ms: (t2 - t1).as_secs_f64() * 1e3,
213                compute_ms: 0.0,
214                hits: after.hits - before.hits,
215                misses: after.misses - before.misses,
216                bytes_read: after.bytes_read - before.bytes_read,
217            },
218        })
219    }
220
221    /// The single-token expert compute after `stage_m1`: the accumulator
222    /// cleared, the routed experts from the pointer table, the shared expert,
223    /// the finish into `out`. Device work only, no per-token scalar
224    /// arguments (`ne` is the top-k: one token's picks are distinct experts),
225    /// so a CUDA graph of it replays for any token once `stage_m1` has
226    /// refilled `rows_dev` / `weight_dev` / `ptrs_dev`.
227    pub fn compute_m1(
228        &self,
229        gpu: &dyn GpuBackend,
230        w: &MoeV41LayerWeights,
231        x: DevicePtr,
232        ne: usize,
233        stream: u64,
234    ) -> Result<DevicePtr> {
235        let c = &self.cfg;
236        ensure!(
237            ne == c.topk,
238            "moe_v41: {ne} distinct experts for one token, expected the top-{}",
239            c.topk
240        );
241        gpu.memset_async(self.acc, 0, c.dim * 4, stream)?;
242        self.routed_m1(gpu, x, ne, false, stream)?;
243        self.shared_expert(gpu, w, x, 1, true, stream)?;
244        Ok(self.out)
245    }
246
247    /// The single-token shared expert up to `sd` on `side`, reading the
248    /// token's q8_1 rows that [`Self::ffn_input_q8_m1`] wrote on the forking
249    /// stream (its own `h` scratch, as the eager step's side stream uses):
250    /// the fork half of [`Self::compute_m1_joined`]. Device work only.
251    pub fn shared_expert_on(
252        &self,
253        gpu: &dyn GpuBackend,
254        w: &MoeV41LayerWeights,
255        x: DevicePtr,
256        side: u64,
257    ) -> Result<()> {
258        self.shared_expert_body(gpu, w, x, 1, self.a_q8, self.sh_q8, true, side)
259    }
260
261    /// [`Self::compute_m1`] with the shared expert forked onto a side stream
262    /// by [`Self::shared_expert_on`]: the routed experts, then the wait on
263    /// `join` (recorded on the side after `sd`), then the same accumulate
264    /// and finish. The eager step's order and bits.
265    pub fn compute_m1_joined(
266        &self,
267        gpu: &dyn GpuBackend,
268        x: DevicePtr,
269        ne: usize,
270        join: u64,
271        stream: u64,
272    ) -> Result<DevicePtr> {
273        let c = &self.cfg;
274        ensure!(
275            ne == c.topk,
276            "moe_v41: {ne} distinct experts for one token, expected the top-{}",
277            c.topk
278        );
279        gpu.memset_async(self.acc, 0, c.dim * 4, stream)?;
280        self.routed_m1(gpu, x, ne, true, stream)?;
281        gpu.stream_wait_event(stream, join)?;
282        self.shared_expert_tail(gpu, 1, stream)?;
283        Ok(self.out)
284    }
285}