metrale_model_layers/layers/moe/
forward_k2.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `MoeLayer::forward_k2`, the MoE block for two rows at once (K=2 verify).
4//!
5//! Owner: model-layers (MoE).
6//! Invariants: none beyond the types.
7
8use anyhow::Context as _;
9
10use super::*;
11
12mod originals;
13mod unified_t;
14
15impl MoeLayer {
16    /// 2026-09-25: MoE for two rows: `input` is the normed [2, H] BF16 MoE input.
17    ///
18    /// Gate projection for both rows, top-K, then the fused expert gate+up,
19    /// silu+down and weighted-sum+blend kernels. The shared expert's scratch
20    /// reuses the `logits` and `ssm_qkvz` buffers. Output: `moe_output()` [2, H].
21    /// LoRA, BF16 experts that cannot use the BF16 batch2 kernels, E8M0 experts
22    /// and an unsupported mixed shared expert take `forward_batched` instead.
23    pub fn forward_k2(
24        &self,
25        input: DevicePtr, // 2026-09-25: [2, H] BF16
26        ctx: &ForwardContext,
27        stream: u64,
28    ) -> Result<()> {
29        // 2026-09-27: An NVFP4 `--expert-quantization` tier: the grouped decode, whose rows do
30        // not depend on how many share the call.
31        if self.nvfp4_grouped_decode_ok(2, ctx) {
32            return self.forward_nvfp4_grouped_decode(input, 2, ctx, stream);
33        }
34        self.refuse_lean_layout("MoeLayer::forward_k2")?;
35        // 2026-09-25: The batch2 kernels do not handle zero-computation experts
36        // (router width above num_experts), so refuse instead of mis-routing.
37        anyhow::ensure!(
38            self.router_logits_n as usize == ctx.config.num_experts,
39            "zero-expert MoE routing is not wired on this dispatch variant yet (forward_k2)"
40        );
41
42        // 2026-09-25: The fused batch2 kernels have no LoRA fold. With a MoE
43        // adapter installed, `forward_batched` applies the router and expert
44        // deltas per row and writes the same moe_output [2, H].
45        if self.lora.is_some() {
46            return self.forward_batched(input, 2, ctx, stream);
47        }
48        // 2026-09-25: BF16 (FP8-dequant-on-load) experts run on the BF16 batch2
49        // kernels when both are present and there is no EP, else on
50        // `forward_batched`. The loader frees their FP8 sources, so the FP8 and
51        // NVFP4 branches below must not see them.
52        let is_ep = ctx.comm.is_some() && ctx.config.ep_world_size > 1;
53        let use_bf16_batch2 = self.bf16_gate_weight_ptrs.is_some()
54            && self.moe_expert_gate_up_shared_bf16_batch2_k.0 != 0
55            && self.moe_expert_silu_down_shared_bf16_batch2_k.0 != 0
56            && !is_ep;
57        if self.bf16_gate_weight_ptrs.is_some() && !use_bf16_batch2 {
58            return self.forward_batched(input, 2, ctx, stream);
59        }
60        // 2026-09-25: E8M0 (native MXFP4, per-32 scale) routed experts must not
61        // reach `moe_expert_gate_up_shared_batch2_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. There is no E8M0 batch2 kernel;
64        // `forward_batched` selects the `_e8m0` kernels through `e8m0_or` on its
65        // `use_t_layout_for_prefill` branch.
66        if k2_e8m0_needs_per_token(self.experts_scale_kind) {
67            return self.forward_batched(input, 2, ctx, stream);
68        }
69        // 2026-09-25: Mixed NVFP4-routed / BF16-shared experts (the Laguna
70        // loader): the batch2 kernels cannot compute a BF16 shared expert. With
71        // either fused layout (unified-T or originals) and no EP, the routed half
72        // runs on those kernels with NULL shared weights and the shared expert
73        // runs afterwards as one batched BF16 pass; otherwise `forward_batched`.
74        let mixed_bf16_shared = self.has_mixed_bf16_shared_expert();
75        let mixed_t_ok = self.use_t_layout_for_decode()
76            && self.moe_expert_gate_up_shared_batch2_t_k.0 != 0
77            && self.moe_expert_silu_down_shared_batch2_t_k.0 != 0;
78        let mixed_orig_ok = !self.use_t_layout_for_decode()
79            && self.moe_expert_gate_up_shared_batch2.0 != 0
80            && self.moe_expert_silu_down_shared_batch2.0 != 0
81            && !self.gate_ptrs.packed_ptrs.is_null();
82        if mixed_bf16_shared && !((mixed_t_ok || mixed_orig_ok) && !is_ep) {
83            return self.forward_batched(input, 2, ctx, stream);
84        }
85
86        let h = ctx.config.hidden_size as u32;
87        let inter = ctx.config.moe_intermediate_size as u32;
88        let num_experts = ctx.config.num_experts as u32;
89        let top_k = ctx.config.num_experts_per_tok as u32;
90
91        // 2026-09-25: With `k2_diag` (METRALE_K2_DIAG=1) the stream is synchronised
92        // after each stage; the context of the first failing sync names the stage.
93        let k2_diag = ctx.levers.k2_diag;
94        if k2_diag {
95            ctx.gpu
96                .synchronize(stream)
97                .context("K2 ENTRY: attention+norm BEFORE forward_k2")?;
98        }
99
100        let router_in = self.router_input(input, 2, h, ctx, stream)?;
101        let gate_logits = ctx.buffers.gate_logits();
102        if let Some(ref nvfp4) = self.gate_nvfp4 {
103            ops::w4a16_gemv_batch2(
104                ctx.gpu,
105                self.w4a16_gemv_batch2,
106                router_in,
107                nvfp4,
108                gate_logits,
109                num_experts,
110                h,
111                stream,
112            )?;
113        } else {
114            self.router_gemm_bf16(router_in, gate_logits, 2, num_experts, h, ctx, stream)?;
115        }
116
117        // 2026-09-25: scratch holds indices [2, top_k] u32, then weights [2, top_k] f32.
118        let scratch = ctx.buffers.scratch();
119        let indices_dev = scratch;
120        let weights_dev = scratch.offset(2 * top_k as usize * 4);
121        if let Some(bias) = self.correction_bias_dev {
122            // 2026-09-25: With a correction bias: sqrt(softplus) scoring for
123            // `scoring_func == "sqrtsoftplus"`, sigmoid otherwise, the same
124            // branch as the single-token and prefill routers.
125            if ctx.config.scoring_func == "sqrtsoftplus" {
126                // 2026-09-25: One non-batched launch per row; gate_logits rows
127                // are num_experts BF16 apart.
128                for t in 0..2usize {
129                    ops::moe_topk_sqrtsoftplus(
130                        ctx.gpu,
131                        self.moe_topk_sqrtsoftplus_k,
132                        gate_logits.offset(t * num_experts as usize * 2),
133                        bias,
134                        indices_dev.offset(t * top_k as usize * 4),
135                        weights_dev.offset(t * top_k as usize * 4),
136                        num_experts,
137                        top_k,
138                        ctx.config.norm_topk_prob,
139                        ctx.config.routed_scaling_factor as f32,
140                        stream,
141                    )?;
142                }
143            } else {
144                ops::moe_topk_sigmoid_batched(
145                    ctx.gpu,
146                    self.moe_topk_sigmoid_batched_k,
147                    gate_logits,
148                    bias,
149                    indices_dev,
150                    weights_dev,
151                    num_experts,
152                    top_k,
153                    ctx.config.norm_topk_prob,
154                    ctx.config.routed_scaling_factor as f32,
155                    2,
156                    stream,
157                )?;
158            }
159        } else {
160            ops::moe_topk_softmax_batched(
161                ctx.gpu,
162                self.moe_topk_batched,
163                gate_logits,
164                indices_dev,
165                weights_dev,
166                num_experts,
167                top_k,
168                ctx.config.norm_topk_prob,
169                2,
170                stream,
171            )?;
172        }
173        super::union_stats::maybe_sample_expert_union(ctx, indices_dev, 2, top_k as usize, stream);
174
175        if k2_diag {
176            ctx.gpu
177                .synchronize(stream)
178                .context("K2: gate-GEMV + topk")?;
179        }
180
181        let expert_gate_out = ctx.buffers.expert_gate_out();
182        let expert_up_out = ctx.buffers.expert_up_out();
183        let shared_gate_scratch = ctx.buffers.logits();
184        let shared_up_scratch = ctx.buffers.ssm_qkvz();
185        let expert_down_out = ctx.buffers.expert_down_out();
186        let shared_down_out = ctx.buffers.attn_output();
187        let output = ctx.buffers.moe_output();
188
189        if use_bf16_batch2
190            && let (Some(gp), Some(up), Some(dp), Some(shared)) = (
191                self.bf16_gate_weight_ptrs,
192                self.bf16_up_weight_ptrs,
193                self.bf16_down_weight_ptrs,
194                self.bf16_shared_expert,
195            )
196        {
197            // 2026-09-25: Non-EP only: `use_bf16_batch2` requires `!is_ep`.
198            ops::moe_expert_gate_up_shared_bf16_batch2(
199                ctx.gpu,
200                self.moe_expert_gate_up_shared_bf16_batch2_k,
201                input,
202                gp,
203                expert_gate_out,
204                up,
205                expert_up_out,
206                indices_dev,
207                shared.gate_proj.weight,
208                shared_gate_scratch,
209                shared.up_proj.weight,
210                shared_up_scratch,
211                inter,
212                h,
213                top_k,
214                stream,
215            )?;
216            ops::moe_expert_silu_down_shared_bf16_batch2(
217                ctx.gpu,
218                self.moe_expert_silu_down_shared_bf16_batch2_k,
219                expert_gate_out,
220                expert_up_out,
221                dp,
222                expert_down_out,
223                indices_dev,
224                shared_gate_scratch,
225                shared_up_scratch,
226                shared.down_proj.weight,
227                shared_down_out,
228                h,
229                inter,
230                top_k,
231                stream,
232            )?;
233            ops::moe_weighted_sum_blend_batch2(
234                ctx.gpu,
235                self.moe_weighted_sum_blend_batch2,
236                output,
237                expert_down_out,
238                weights_dev,
239                shared_down_out,
240                input,
241                self.weights.shared_expert_gate.weight,
242                h,
243                top_k,
244                h,
245                stream,
246            )?;
247        } else if let (Some(gp), Some(up), Some(dp), Some(sh)) = (
248            &self.fp8_gate_weight_ptrs,
249            &self.fp8_up_weight_ptrs,
250            &self.fp8_down_weight_ptrs,
251            &self.fp8_shared_expert,
252        ) {
253            ops::moe_expert_gate_up_shared_fp8_batch2(
254                ctx.gpu,
255                self.moe_expert_gate_up_shared_fp8_batch2,
256                input,
257                gp.weight_ptrs,
258                gp.scale_ptrs,
259                expert_gate_out,
260                up.weight_ptrs,
261                up.scale_ptrs,
262                expert_up_out,
263                indices_dev,
264                &sh.gate_proj,
265                shared_gate_scratch,
266                &sh.up_proj,
267                shared_up_scratch,
268                inter,
269                h,
270                top_k,
271                stream,
272            )?;
273            ops::moe_expert_silu_down_shared_fp8_batch2(
274                ctx.gpu,
275                self.moe_expert_silu_down_shared_fp8_batch2,
276                expert_gate_out,
277                expert_up_out,
278                dp.weight_ptrs,
279                dp.scale_ptrs,
280                expert_down_out,
281                indices_dev,
282                shared_gate_scratch,
283                shared_up_scratch,
284                &sh.down_proj,
285                shared_down_out,
286                h,
287                inter,
288                top_k,
289                stream,
290            )?;
291            // 2026-09-25: With EP, blend a zeroed shared term (expert_gate_out is
292            // not read after silu_down); the shared expert is added once after
293            // the all-reduce below.
294            let shared_for_blend = if is_ep && !shared_down_out.is_null() {
295                ctx.gpu
296                    .memset_async(expert_gate_out, 0, 2 * h as usize * 2, stream)?;
297                expert_gate_out
298            } else {
299                shared_down_out
300            };
301            ops::moe_weighted_sum_blend_batch2(
302                ctx.gpu,
303                self.moe_weighted_sum_blend_fp8_batch2,
304                output,
305                expert_down_out,
306                weights_dev,
307                shared_for_blend,
308                input,
309                self.weights.shared_expert_gate.weight,
310                h,
311                top_k,
312                h,
313                stream,
314            )?;
315        } else if self.use_t_layout_for_decode() {
316            self.forward_k2_unified_t(
317                input,
318                indices_dev,
319                weights_dev,
320                expert_gate_out,
321                expert_up_out,
322                expert_down_out,
323                shared_gate_scratch,
324                shared_up_scratch,
325                shared_down_out,
326                output,
327                inter,
328                h,
329                top_k,
330                is_ep,
331                mixed_bf16_shared,
332                ctx,
333                stream,
334            )?;
335        } else {
336            self.forward_k2_originals(
337                input,
338                indices_dev,
339                weights_dev,
340                expert_gate_out,
341                expert_up_out,
342                expert_down_out,
343                shared_gate_scratch,
344                shared_up_scratch,
345                shared_down_out,
346                output,
347                inter,
348                h,
349                top_k,
350                is_ep,
351                mixed_bf16_shared,
352                ctx,
353                stream,
354            )?;
355        }
356
357        if k2_diag {
358            ctx.gpu
359                .synchronize(stream)
360                .context("K2: expert dispatch (gate_up/silu_down/blend)")?;
361        }
362
363        if let Some(comm) = ctx.comm
364            && ctx.config.ep_world_size > 1
365        {
366            if ctx.graph_capture {
367                comm.all_reduce(output.0, 2 * h as usize * 2)?;
368            } else {
369                comm.all_reduce_async(output.0, 2 * h as usize * 2, stream)?;
370            }
371            // 2026-09-25: Add the shared expert once, after the reduce; gated
372            // when the layer has a shared_expert_gate weight.
373            if !shared_down_out.is_null() {
374                if self.weights.shared_expert_gate.weight.0 == 0 {
375                    ops::residual_add(
376                        ctx.gpu,
377                        self.residual_add,
378                        output,
379                        shared_down_out,
380                        2 * h,
381                        stream,
382                    )?;
383                } else {
384                    ops::moe_batched_blend(
385                        ctx.gpu,
386                        self.moe_batched_blend,
387                        output,
388                        shared_down_out,
389                        input,
390                        self.weights.shared_expert_gate.weight,
391                        h,
392                        2,
393                        stream,
394                    )?;
395                }
396            }
397        }
398
399        Ok(())
400    }
401}
402
403mod forward_k2_helpers;
404pub(crate) use forward_k2_helpers::{batch2_block_width, k2_e8m0_needs_per_token};
405
406#[cfg(test)]
407#[path = "forward_k2_dispatch_tests.rs"]
408mod k2_dispatch_tests;