metrale_model_layers/layers/moe/
forward_k3.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `MoeLayer::forward_k3`, the MoE block for three rows at once (K=3 verify).
4//!
5//! Owner: model-layers (MoE).
6//! Invariants: none beyond the types.
7
8use super::*;
9
10impl MoeLayer {
11    /// 2026-09-25: MoE for three rows: `input` is the normed [3, H] BF16 MoE input.
12    ///
13    /// Gate projection for all rows, top-K, then the fused expert gate+up,
14    /// silu+down and weighted-sum+blend kernels. Output: `moe_output()` [3, H].
15    /// LoRA, BF16 experts, E8M0 experts and an unsupported mixed shared expert
16    /// take `forward_batched` instead.
17    pub fn forward_k3(
18        &self,
19        input: DevicePtr, // 2026-09-25: [3, H] BF16
20        ctx: &ForwardContext,
21        stream: u64,
22    ) -> Result<()> {
23        // 2026-09-27: An NVFP4 `--expert-quantization` tier: the grouped decode, whose rows do
24        // not depend on how many share the call.
25        if self.nvfp4_grouped_decode_ok(3, ctx) {
26            return self.forward_nvfp4_grouped_decode(input, 3, ctx, stream);
27        }
28        self.refuse_lean_layout("MoeLayer::forward_k3")?;
29        // 2026-09-25: The batch3 kernels do not handle zero-computation experts
30        // (router width above num_experts), so refuse instead of mis-routing.
31        anyhow::ensure!(
32            self.router_logits_n as usize == ctx.config.num_experts,
33            "zero-expert MoE routing is not wired on this dispatch variant yet (forward_k3)"
34        );
35
36        // 2026-09-25: The fused batch3 kernels have no LoRA fold. With a MoE
37        // adapter installed, `forward_batched` applies the router and expert
38        // deltas per row and writes the same moe_output [3, H].
39        if self.lora.is_some() {
40            return self.forward_batched(input, 3, ctx, stream);
41        }
42        // 2026-09-25: BF16 (FP8-dequant-on-load) experts have no batch3 kernel,
43        // and the loader frees their FP8 sources, so they always take
44        // `forward_batched`.
45        if self.bf16_gate_weight_ptrs.is_some() {
46            return self.forward_batched(input, 3, ctx, stream);
47        }
48        // 2026-09-25: Mixed NVFP4-routed / BF16-shared experts: only the
49        // unified-T branch (both batch3_t kernels present, no EP) serves them,
50        // with NULL shared weights and a BF16 shared pass afterwards.
51        let mixed_bf16_shared = self.has_mixed_bf16_shared_expert();
52        if mixed_bf16_shared
53            && !(self.use_t_layout_for_decode()
54                && self.moe_expert_gate_up_shared_batch3_t_k.0 != 0
55                && self.moe_expert_silu_down_shared_batch3_t_k.0 != 0
56                && !(ctx.comm.is_some() && ctx.config.ep_world_size > 1))
57        {
58            return self.forward_batched(input, 3, ctx, stream);
59        }
60        // 2026-09-25: E8M0 (native MXFP4, per-32 scale) routed experts must not
61        // reach `moe_expert_gate_up_shared_batch3_t`: that NVFP4 kernel has
62        // GROUP_SIZE 16, so it reads K/16 scale rows from a K/32-row E8M0 buffer
63        // and decodes them as E4M3. `forward_batched` selects the `_e8m0`
64        // kernels through `e8m0_or` on its `use_t_layout_for_prefill` branch.
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        let router_in = self.router_input(input, 3, h, ctx, stream)?;
75        let gate_logits = ctx.buffers.gate_logits();
76        if let Some(ref nvfp4) = self.gate_nvfp4 {
77            ops::w4a16_gemv_batch3(
78                ctx.gpu,
79                self.w4a16_gemv_batch3,
80                router_in,
81                nvfp4,
82                gate_logits,
83                num_experts,
84                h,
85                stream,
86            )?;
87        } else {
88            self.router_gemm_bf16(router_in, gate_logits, 3, num_experts, h, ctx, stream)?;
89        }
90
91        // 2026-09-25: scratch holds indices [3, top_k] u32, then weights [3, top_k]
92        // f32. With a correction bias this path always uses sigmoid scoring; it
93        // has no sqrtsoftplus branch.
94        let scratch = ctx.buffers.scratch();
95        let indices_dev = scratch;
96        let weights_dev = scratch.offset(3 * top_k as usize * 4);
97        if let Some(bias) = self.correction_bias_dev {
98            ops::moe_topk_sigmoid_batched(
99                ctx.gpu,
100                self.moe_topk_sigmoid_batched_k,
101                gate_logits,
102                bias,
103                indices_dev,
104                weights_dev,
105                num_experts,
106                top_k,
107                ctx.config.norm_topk_prob,
108                ctx.config.routed_scaling_factor as f32,
109                3,
110                stream,
111            )?;
112        } else {
113            ops::moe_topk_softmax_batched(
114                ctx.gpu,
115                self.moe_topk_batched,
116                gate_logits,
117                indices_dev,
118                weights_dev,
119                num_experts,
120                top_k,
121                ctx.config.norm_topk_prob,
122                3,
123                stream,
124            )?;
125        }
126
127        super::union_stats::maybe_sample_expert_union(ctx, indices_dev, 3, top_k as usize, stream);
128
129        let expert_gate_out = ctx.buffers.expert_gate_out();
130        let expert_up_out = ctx.buffers.expert_up_out();
131        let shared_gate_scratch = ctx.buffers.logits();
132        let shared_up_scratch = ctx.buffers.ssm_qkvz();
133        let expert_down_out = ctx.buffers.expert_down_out();
134        let shared_down_out = ctx.buffers.attn_output();
135        let output = ctx.buffers.moe_output();
136
137        let is_ep = ctx.comm.is_some() && ctx.config.ep_world_size > 1;
138
139        if let (Some(gp), Some(up), Some(dp), Some(sh)) = (
140            &self.fp8_gate_weight_ptrs,
141            &self.fp8_up_weight_ptrs,
142            &self.fp8_down_weight_ptrs,
143            &self.fp8_shared_expert,
144        ) {
145            ops::moe_expert_gate_up_shared_fp8_batch3(
146                ctx.gpu,
147                self.moe_expert_gate_up_shared_fp8_batch3,
148                input,
149                gp.weight_ptrs,
150                gp.scale_ptrs,
151                expert_gate_out,
152                up.weight_ptrs,
153                up.scale_ptrs,
154                expert_up_out,
155                indices_dev,
156                &sh.gate_proj,
157                shared_gate_scratch,
158                &sh.up_proj,
159                shared_up_scratch,
160                inter,
161                h,
162                top_k,
163                stream,
164            )?;
165            ops::moe_expert_silu_down_shared_fp8_batch3(
166                ctx.gpu,
167                self.moe_expert_silu_down_shared_fp8_batch3,
168                expert_gate_out,
169                expert_up_out,
170                dp.weight_ptrs,
171                dp.scale_ptrs,
172                expert_down_out,
173                indices_dev,
174                shared_gate_scratch,
175                shared_up_scratch,
176                &sh.down_proj,
177                shared_down_out,
178                h,
179                inter,
180                top_k,
181                stream,
182            )?;
183            // 2026-09-25: With EP, blend a zeroed shared term (expert_gate_out is
184            // not read after silu_down); the shared expert is added once after
185            // the all-reduce below.
186            let shared_for_blend = if is_ep && !shared_down_out.is_null() {
187                ctx.gpu
188                    .memset_async(expert_gate_out, 0, 3 * h as usize * 2, stream)?;
189                expert_gate_out
190            } else {
191                shared_down_out
192            };
193            ops::moe_weighted_sum_blend_batch3(
194                ctx.gpu,
195                self.moe_weighted_sum_blend_fp8_batch3,
196                output,
197                expert_down_out,
198                weights_dev,
199                shared_for_blend,
200                input,
201                self.weights.shared_expert_gate.weight,
202                h,
203                top_k,
204                h,
205                stream,
206            )?;
207        } else if self.use_t_layout_for_decode() {
208            // 2026-09-25: Transposed `[K/2, N]` tables; `use_t_layout_for_decode()`
209            // is false in hybrid mode.
210            let gate_t = self
211                .gate_ptrs_t
212                .as_ref()
213                .expect("gate_ptrs_t under unified_t");
214            let up_t = self.up_ptrs_t.as_ref().expect("up_ptrs_t under unified_t");
215            let down_t = self
216                .down_ptrs_t
217                .as_ref()
218                .expect("down_ptrs_t under unified_t");
219            let null_qw = QuantizedWeight::null();
220            // 2026-09-25: Mixed config: NULL shared weights make the kernels write
221            // zeros to the shared outputs; the BF16 shared pass after silu_down_t
222            // replaces them.
223            let (sh_gate_t, sh_up_t, sh_down_t) = if mixed_bf16_shared {
224                (&null_qw, &null_qw, &null_qw)
225            } else {
226                (
227                    self.shared_gate_t.as_ref().unwrap_or(&null_qw),
228                    self.shared_up_t.as_ref().unwrap_or(&null_qw),
229                    self.shared_down_t.as_ref().unwrap_or(&null_qw),
230                )
231            };
232            ops::moe_expert_gate_up_shared_batch3_t(
233                ctx.gpu,
234                self.moe_expert_gate_up_shared_batch3_t_k,
235                input,
236                gate_t.packed_ptrs,
237                gate_t.scale_ptrs,
238                gate_t.scale2_vals,
239                expert_gate_out,
240                up_t.packed_ptrs,
241                up_t.scale_ptrs,
242                up_t.scale2_vals,
243                expert_up_out,
244                indices_dev,
245                sh_gate_t,
246                shared_gate_scratch,
247                sh_up_t,
248                shared_up_scratch,
249                inter,
250                h,
251                top_k,
252                stream,
253            )?;
254            ops::moe_expert_silu_down_shared_batch3_t(
255                ctx.gpu,
256                self.moe_expert_silu_down_shared_batch3_t_k,
257                expert_gate_out,
258                expert_up_out,
259                down_t.packed_ptrs,
260                down_t.scale_ptrs,
261                down_t.scale2_vals,
262                expert_down_out,
263                indices_dev,
264                shared_gate_scratch,
265                shared_up_scratch,
266                sh_down_t,
267                shared_down_out,
268                h,
269                inter,
270                top_k,
271                stream,
272            )?;
273            if mixed_bf16_shared {
274                let shared_inter = ctx.config.shared_expert_intermediate_size as u32;
275                self.run_bf16_shared_expert(
276                    input,
277                    3,
278                    h,
279                    shared_inter,
280                    shared_gate_scratch,
281                    shared_up_scratch,
282                    shared_down_out,
283                    ctx,
284                    stream,
285                )?;
286            }
287            // 2026-09-25: With EP, blend a zeroed shared term; the shared expert
288            // is added after the all-reduce below.
289            let shared_for_blend = if is_ep && !shared_down_out.is_null() {
290                ctx.gpu
291                    .memset_async(expert_gate_out, 0, 3 * h as usize * 2, stream)?;
292                expert_gate_out
293            } else {
294                shared_down_out
295            };
296            ops::moe_weighted_sum_blend_batch3(
297                ctx.gpu,
298                self.moe_weighted_sum_blend_batch3,
299                output,
300                expert_down_out,
301                weights_dev,
302                shared_for_blend,
303                input,
304                self.weights.shared_expert_gate.weight,
305                h,
306                top_k,
307                h,
308                stream,
309            )?;
310        } else {
311            ops::moe_expert_gate_up_shared_batch3(
312                ctx.gpu,
313                self.moe_expert_gate_up_shared_batch3,
314                input,
315                self.gate_ptrs.packed_ptrs,
316                self.gate_ptrs.scale_ptrs,
317                self.gate_ptrs.scale2_vals,
318                expert_gate_out,
319                self.up_ptrs.packed_ptrs,
320                self.up_ptrs.scale_ptrs,
321                self.up_ptrs.scale2_vals,
322                expert_up_out,
323                indices_dev,
324                &self.weights.shared_expert.gate_proj,
325                shared_gate_scratch,
326                &self.weights.shared_expert.up_proj,
327                shared_up_scratch,
328                inter,
329                h,
330                top_k,
331                stream,
332            )?;
333            ops::moe_expert_silu_down_shared_batch3(
334                ctx.gpu,
335                self.moe_expert_silu_down_shared_batch3,
336                expert_gate_out,
337                expert_up_out,
338                self.down_ptrs.packed_ptrs,
339                self.down_ptrs.scale_ptrs,
340                self.down_ptrs.scale2_vals,
341                expert_down_out,
342                indices_dev,
343                shared_gate_scratch,
344                shared_up_scratch,
345                &self.weights.shared_expert.down_proj,
346                shared_down_out,
347                h,
348                inter,
349                top_k,
350                stream,
351            )?;
352            // 2026-09-25: With EP, blend a zeroed shared term; the shared expert
353            // is added after the all-reduce below.
354            let shared_for_blend = if is_ep && !shared_down_out.is_null() {
355                ctx.gpu
356                    .memset_async(expert_gate_out, 0, 3 * h as usize * 2, stream)?;
357                expert_gate_out
358            } else {
359                shared_down_out
360            };
361            ops::moe_weighted_sum_blend_batch3(
362                ctx.gpu,
363                self.moe_weighted_sum_blend_batch3,
364                output,
365                expert_down_out,
366                weights_dev,
367                shared_for_blend,
368                input,
369                self.weights.shared_expert_gate.weight,
370                h,
371                top_k,
372                h,
373                stream,
374            )?;
375        }
376
377        if let Some(comm) = ctx.comm
378            && ctx.config.ep_world_size > 1
379        {
380            if ctx.graph_capture {
381                comm.all_reduce(output.0, 3 * h as usize * 2)?;
382            } else {
383                comm.all_reduce_async(output.0, 3 * h as usize * 2, stream)?;
384            }
385            // 2026-09-25: Add the shared expert once, after the reduce; gated
386            // when the layer has a shared_expert_gate weight.
387            if !shared_down_out.is_null() {
388                if self.weights.shared_expert_gate.weight.0 == 0 {
389                    ops::residual_add(
390                        ctx.gpu,
391                        self.residual_add,
392                        output,
393                        shared_down_out,
394                        3 * h,
395                        stream,
396                    )?;
397                } else {
398                    ops::moe_batched_blend(
399                        ctx.gpu,
400                        self.moe_batched_blend,
401                        output,
402                        shared_down_out,
403                        input,
404                        self.weights.shared_expert_gate.weight,
405                        h,
406                        3,
407                        stream,
408                    )?;
409                }
410            }
411        }
412
413        Ok(())
414    }
415}
416
417/// 2026-09-25: True when `forward_k3` must send the rows to `forward_batched`:
418/// E8M0 routed experts cannot use the GROUP_SIZE-16 batch3 kernel.
419pub(crate) fn k3_e8m0_needs_per_token(scale_kind: crate::weight_map::WeightQuantFormat) -> bool {
420    matches!(scale_kind, crate::weight_map::WeightQuantFormat::Mxfp4E8m0)
421}
422
423#[cfg(test)]
424#[path = "forward_k3_dispatch_tests.rs"]
425mod k3_dispatch_tests;