metrale_model_arch/moe_v41/
single.rs

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