spark_model/layers/moe/
forward_k3.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! MoeLayer::forward_k3 (verify K=3).
4
5use super::*;
6
7impl MoeLayer {
8    /// Fused K=3 forward: process 3 tokens through MoE in 5 kernel launches.
9    ///
10    /// Gate GEMV batch3 → batched topK → fused expert gate+up → fused silu+down → fused wsum+blend.
11    /// Expert buffers sized for 3*top_k slots. Output at moe_output() [3, H].
12    pub fn forward_k3(
13        &self,
14        input: DevicePtr, // [3, H] BF16 — normed MoE input for 3 tokens
15        ctx: &ForwardContext,
16        stream: u64,
17    ) -> Result<()> {
18        // LongCat zero-experts are wired only on the single-token decode
19        // + prefill paths (v1); this variant would silently mis-route the
20        // 384-wide router. Named refusal, not silent wrongness.
21        anyhow::ensure!(
22            self.router_logits_n as usize == ctx.config.num_experts,
23            "zero-expert MoE routing is not wired on this dispatch variant yet (forward_k3)"
24        );
25
26        // Feature-1: a resident MoE adapter forces the per-row batched fallback
27        // (folds gate/up/down route-agnostically; base rows no-op; same
28        // moe_output[3,H]), skipping any no-fold fast path. Install-time gate →
29        // graph-safe (graphs drain on rotate/swap). Router adapter refused inside.
30        if self.lora.is_some() {
31            return self.forward_batched(input, 3, ctx, stream);
32        }
33        // BF16 (FP8-dequant-on-load) experts have no fused batch3 kernel.
34        // The FP8 batch3 branch below would read expert weights that were
35        // FREED at dequant-load → garbage MTP-verify logits → degenerate
36        // repetition. Route the 3-token verify through the per-token BF16
37        // batched path, which produces the same moe_output()[3,H]. (SSOT:
38        // reuses the decode BF16 kernels via forward_batched.)
39        if self.bf16_gate_weight_ptrs.is_some() {
40            return self.forward_batched(input, 3, ctx, stream);
41        }
42        // Mixed NVFP4-routed / BF16-shared (Laguna): batch the routed half
43        // through the _t kernels and run the shared expert as one batched BF16
44        // pass afterwards. See forward_k2 for the rationale.
45        let mixed_bf16_shared = self.has_mixed_bf16_shared_expert();
46        if mixed_bf16_shared
47            && !(self.use_t_layout_for_decode()
48                && self.moe_expert_gate_up_shared_batch3_t_k.0 != 0
49                && self.moe_expert_silu_down_shared_batch3_t_k.0 != 0
50                && !(ctx.comm.is_some() && ctx.config.ep_world_size > 1))
51        {
52            return self.forward_batched(input, 3, ctx, stream);
53        }
54        // E8M0 (native MXFP4, per-32 E8M0 scale) routed experts MUST NOT reach
55        // the unified-T batch3 kernel `moe_expert_gate_up_shared_batch3_t`: like
56        // its K=2 twin it is an NVFP4 kernel that hardcodes GROUP_SIZE=16 and
57        // would read `inter·h/16` scale bytes from the correctly-sized
58        // `inter·h/32` E8M0 scale buffer — a 2× over-read →
59        // CUDA_ERROR_ILLEGAL_ADDRESS (it also E4M3-decodes E8M0 scale bytes →
60        // garbage even in-bounds). No E8M0 batch3 kernel exists, so route all 3
61        // verify tokens through the per-token unified-T path (`forward_batched`),
62        // whose `use_t_layout_for_prefill` branch selects the GS32 `_e8m0`
63        // kernel via `e8m0_or` — the same correct path ordinary decode already
64        // uses. Mirrors the K=2 guard at the top of `forward_k2`.
65        if k3_e8m0_needs_per_token(self.experts_scale_kind) {
66            return self.forward_batched(input, 3, ctx, stream);
67        }
68
69        let h = ctx.config.hidden_size as u32;
70        let inter = ctx.config.moe_intermediate_size as u32;
71        let num_experts = ctx.config.num_experts as u32;
72        let top_k = ctx.config.num_experts_per_tok as u32;
73
74        // Gemma-4 router pre-norm (no-op for other models).
75        let router_in = self.router_input(input, 3, h, ctx, stream)?;
76        // 1. Gate GEMV batch3: reads gate weight once for 3 tokens
77        let gate_logits = ctx.buffers.gate_logits();
78        if let Some(ref nvfp4) = self.gate_nvfp4 {
79            ops::w4a16_gemv_batch3(
80                ctx.gpu,
81                self.w4a16_gemv_batch3,
82                router_in,
83                nvfp4,
84                gate_logits,
85                num_experts,
86                h,
87                stream,
88            )?;
89        } else {
90            ops::dense_gemm(
91                ctx.gpu,
92                self.dense_gemm,
93                router_in,
94                &self.weights.gate,
95                gate_logits,
96                3,
97                num_experts,
98                h,
99                stream,
100            )?;
101        }
102
103        // 2. Batched topK for 3 tokens. Sigmoid+bias for MiniMax/DeepSeek-V3,
104        //    softmax otherwise.
105        let scratch = ctx.buffers.scratch();
106        let indices_dev = scratch;
107        let weights_dev = scratch.offset(3 * top_k as usize * 4);
108        if let Some(bias) = self.correction_bias_dev {
109            ops::moe_topk_sigmoid_batched(
110                ctx.gpu,
111                self.moe_topk_sigmoid_batched_k,
112                gate_logits,
113                bias,
114                indices_dev,
115                weights_dev,
116                num_experts,
117                top_k,
118                ctx.config.norm_topk_prob,
119                ctx.config.routed_scaling_factor as f32,
120                3,
121                stream,
122            )?;
123        } else {
124            ops::moe_topk_softmax_batched(
125                ctx.gpu,
126                self.moe_topk_batched,
127                gate_logits,
128                indices_dev,
129                weights_dev,
130                num_experts,
131                top_k,
132                ctx.config.norm_topk_prob,
133                3,
134                stream,
135            )?;
136        }
137
138        super::union_stats::maybe_sample_expert_union(ctx, indices_dev, 3, top_k as usize, stream);
139
140        // 3-5. Fused expert dispatch for 3 tokens
141        let expert_gate_out = ctx.buffers.expert_gate_out();
142        let expert_up_out = ctx.buffers.expert_up_out();
143        let shared_gate_scratch = ctx.buffers.logits();
144        let shared_up_scratch = ctx.buffers.ssm_qkvz();
145        let expert_down_out = ctx.buffers.expert_down_out();
146        let shared_down_out = ctx.buffers.attn_output();
147        let output = ctx.buffers.moe_output();
148
149        let is_ep = ctx.comm.is_some() && ctx.config.ep_world_size > 1;
150
151        if let (Some(gp), Some(up), Some(dp), Some(sh)) = (
152            &self.fp8_gate_weight_ptrs,
153            &self.fp8_up_weight_ptrs,
154            &self.fp8_down_weight_ptrs,
155            &self.fp8_shared_expert,
156        ) {
157            // FP8 batch3 path
158            ops::moe_expert_gate_up_shared_fp8_batch3(
159                ctx.gpu,
160                self.moe_expert_gate_up_shared_fp8_batch3,
161                input,
162                gp.weight_ptrs,
163                gp.scale_ptrs,
164                expert_gate_out,
165                up.weight_ptrs,
166                up.scale_ptrs,
167                expert_up_out,
168                indices_dev,
169                &sh.gate_proj,
170                shared_gate_scratch,
171                &sh.up_proj,
172                shared_up_scratch,
173                inter,
174                h,
175                top_k,
176                stream,
177            )?;
178            ops::moe_expert_silu_down_shared_fp8_batch3(
179                ctx.gpu,
180                self.moe_expert_silu_down_shared_fp8_batch3,
181                expert_gate_out,
182                expert_up_out,
183                dp.weight_ptrs,
184                dp.scale_ptrs,
185                expert_down_out,
186                indices_dev,
187                shared_gate_scratch,
188                shared_up_scratch,
189                &sh.down_proj,
190                shared_down_out,
191                h,
192                inter,
193                top_k,
194                stream,
195            )?;
196            // EP fix: after silu_down, expert_gate_out is free — use as zero buffer
197            let shared_for_blend = if is_ep && !shared_down_out.is_null() {
198                ctx.gpu
199                    .memset_async(expert_gate_out, 0, 3 * h as usize * 2, stream)?;
200                expert_gate_out
201            } else {
202                shared_down_out
203            };
204            ops::moe_weighted_sum_blend_batch3(
205                ctx.gpu,
206                self.moe_weighted_sum_blend_fp8_batch3,
207                output,
208                expert_down_out,
209                weights_dev,
210                shared_for_blend,
211                input,
212                self.weights.shared_expert_gate.weight,
213                h,
214                top_k,
215                h,
216                stream,
217            )?;
218        } else if self.use_t_layout_for_decode() {
219            // Phase 8a unified-layout NVFP4 batch=3 verify (MTP K=3). Hybrid
220            // mode skips this branch — small-N MTP verify wins on warp-
221            // reduction originals.
222            let gate_t = self
223                .gate_ptrs_t
224                .as_ref()
225                .expect("gate_ptrs_t under unified_t");
226            let up_t = self.up_ptrs_t.as_ref().expect("up_ptrs_t under unified_t");
227            let down_t = self
228                .down_ptrs_t
229                .as_ref()
230                .expect("down_ptrs_t under unified_t");
231            let null_qw = QuantizedWeight::null();
232            // Mixed config: in-kernel shared expert off (NULL), computed in
233            // BF16 below instead — the NVFP4 shared_*_t tables are load-time
234            // placeholders and numerically wrong for this checkpoint.
235            let (sh_gate_t, sh_up_t, sh_down_t) = if mixed_bf16_shared {
236                (&null_qw, &null_qw, &null_qw)
237            } else {
238                (
239                    self.shared_gate_t.as_ref().unwrap_or(&null_qw),
240                    self.shared_up_t.as_ref().unwrap_or(&null_qw),
241                    self.shared_down_t.as_ref().unwrap_or(&null_qw),
242                )
243            };
244            ops::moe_expert_gate_up_shared_batch3_t(
245                ctx.gpu,
246                self.moe_expert_gate_up_shared_batch3_t_k,
247                input,
248                gate_t.packed_ptrs,
249                gate_t.scale_ptrs,
250                gate_t.scale2_vals,
251                expert_gate_out,
252                up_t.packed_ptrs,
253                up_t.scale_ptrs,
254                up_t.scale2_vals,
255                expert_up_out,
256                indices_dev,
257                sh_gate_t,
258                shared_gate_scratch,
259                sh_up_t,
260                shared_up_scratch,
261                inter,
262                h,
263                top_k,
264                stream,
265            )?;
266            ops::moe_expert_silu_down_shared_batch3_t(
267                ctx.gpu,
268                self.moe_expert_silu_down_shared_batch3_t_k,
269                expert_gate_out,
270                expert_up_out,
271                down_t.packed_ptrs,
272                down_t.scale_ptrs,
273                down_t.scale2_vals,
274                expert_down_out,
275                indices_dev,
276                shared_gate_scratch,
277                shared_up_scratch,
278                sh_down_t,
279                shared_down_out,
280                h,
281                inter,
282                top_k,
283                stream,
284            )?;
285            if mixed_bf16_shared {
286                let shared_inter = ctx.config.shared_expert_intermediate_size as u32;
287                self.run_bf16_shared_expert(
288                    input,
289                    3,
290                    h,
291                    shared_inter,
292                    shared_gate_scratch,
293                    shared_up_scratch,
294                    shared_down_out,
295                    ctx,
296                    stream,
297                )?;
298            }
299            // The _t branch previously returned without writing moe_output at
300            // all — every sibling branch ends in this blend.
301            let shared_for_blend = if is_ep && !shared_down_out.is_null() {
302                ctx.gpu
303                    .memset_async(expert_gate_out, 0, 3 * h as usize * 2, stream)?;
304                expert_gate_out
305            } else {
306                shared_down_out
307            };
308            ops::moe_weighted_sum_blend_batch3(
309                ctx.gpu,
310                self.moe_weighted_sum_blend_batch3,
311                output,
312                expert_down_out,
313                weights_dev,
314                shared_for_blend,
315                input,
316                self.weights.shared_expert_gate.weight,
317                h,
318                top_k,
319                h,
320                stream,
321            )?;
322        } else {
323            // NVFP4 batch3 path
324            ops::moe_expert_gate_up_shared_batch3(
325                ctx.gpu,
326                self.moe_expert_gate_up_shared_batch3,
327                input,
328                self.gate_ptrs.packed_ptrs,
329                self.gate_ptrs.scale_ptrs,
330                self.gate_ptrs.scale2_vals,
331                expert_gate_out,
332                self.up_ptrs.packed_ptrs,
333                self.up_ptrs.scale_ptrs,
334                self.up_ptrs.scale2_vals,
335                expert_up_out,
336                indices_dev,
337                &self.weights.shared_expert.gate_proj,
338                shared_gate_scratch,
339                &self.weights.shared_expert.up_proj,
340                shared_up_scratch,
341                inter,
342                h,
343                top_k,
344                stream,
345            )?;
346            ops::moe_expert_silu_down_shared_batch3(
347                ctx.gpu,
348                self.moe_expert_silu_down_shared_batch3,
349                expert_gate_out,
350                expert_up_out,
351                self.down_ptrs.packed_ptrs,
352                self.down_ptrs.scale_ptrs,
353                self.down_ptrs.scale2_vals,
354                expert_down_out,
355                indices_dev,
356                shared_gate_scratch,
357                shared_up_scratch,
358                &self.weights.shared_expert.down_proj,
359                shared_down_out,
360                h,
361                inter,
362                top_k,
363                stream,
364            )?;
365            // EP fix: after silu_down, expert_gate_out is free — use as zero buffer
366            let shared_for_blend = if is_ep && !shared_down_out.is_null() {
367                ctx.gpu
368                    .memset_async(expert_gate_out, 0, 3 * h as usize * 2, stream)?;
369                expert_gate_out
370            } else {
371                shared_down_out
372            };
373            ops::moe_weighted_sum_blend_batch3(
374                ctx.gpu,
375                self.moe_weighted_sum_blend_batch3,
376                output,
377                expert_down_out,
378                weights_dev,
379                shared_for_blend,
380                input,
381                self.weights.shared_expert_gate.weight,
382                h,
383                top_k,
384                h,
385                stream,
386            )?;
387        }
388
389        // EP all-reduce: sum partial outputs for 3 tokens
390        if let Some(comm) = ctx.comm
391            && ctx.config.ep_world_size > 1
392        {
393            if ctx.graph_capture {
394                comm.all_reduce(output.0, 3 * h as usize * 2)?;
395            } else {
396                comm.all_reduce_async(output.0, 3 * h as usize * 2, stream)?;
397            }
398            // Add shared expert with sigmoid gate (BUG #41 fix)
399            if !shared_down_out.is_null() {
400                if self.weights.shared_expert_gate.weight.0 == 0 {
401                    ops::residual_add(
402                        ctx.gpu,
403                        self.residual_add,
404                        output,
405                        shared_down_out,
406                        3 * h,
407                        stream,
408                    )?;
409                } else {
410                    ops::moe_batched_blend(
411                        ctx.gpu,
412                        self.moe_batched_blend,
413                        output,
414                        shared_down_out,
415                        input,
416                        self.weights.shared_expert_gate.weight,
417                        h,
418                        3,
419                        stream,
420                    )?;
421                }
422            }
423        }
424
425        Ok(())
426    }
427}
428
429/// K=3-verify MoE dispatch guard — the K=3 twin of `k2_e8m0_needs_per_token`
430/// (`forward_k2.rs`). E8M0 (native MXFP4, per-32 E8M0 scale) routed experts
431/// MUST take the per-token unified-T path (GS32 `_e8m0` kernel via `e8m0_or`),
432/// NOT the GS16 NVFP4 `moe_expert_gate_up_shared_batch3_t` batch3 kernel: that
433/// kernel reads `inter·h/16` scale bytes from the correctly-sized `inter·h/32`
434/// E8M0 scale buffer — a 2× over-read → CUDA_ERROR_ILLEGAL_ADDRESS.
435/// Pure decision, unit-tested and wired at the top of `forward_k3`.
436pub(crate) fn k3_e8m0_needs_per_token(scale_kind: crate::weight_map::WeightQuantFormat) -> bool {
437    matches!(scale_kind, crate::weight_map::WeightQuantFormat::Mxfp4E8m0)
438}
439
440// Focused dispatch tests live in a sibling file (same pattern as forward_k2).
441#[cfg(test)]
442#[path = "forward_k3_dispatch_tests.rs"]
443mod k3_dispatch_tests;