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;