metrale_model_layers/layers/moe/
forward_token_major.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Token-major N-row MoE decode (`forward_token_major_decode`).
4//!
5//! Owner: model-layers (MoE).
6//! Invariants: none beyond the types.
7
8use super::*;
9
10impl MoeLayer {
11    /// 2026-09-25: MoE for `num_tokens` decode rows with the token-major kernels of
12    /// the `moe_prefill` module, without sorting by expert: batched gate GEMM and
13    /// top-K, then gate+up, silu+down and weighted-sum+blend over every
14    /// (row, expert-slot) pair.
15    ///
16    /// Serves untransposed NVFP4 experts with an NVFP4 shared expert. LoRA, BF16
17    /// or FP8 experts, a mixed BF16 shared expert, the unified layout, or no
18    /// shared expert go to `forward_batched`.
19    pub fn forward_token_major_decode(
20        &self,
21        input: DevicePtr,
22        num_tokens: usize,
23        ctx: &ForwardContext,
24        stream: u64,
25    ) -> Result<()> {
26        // 2026-09-25: These kernels do not handle zero-computation experts (router
27        // width above num_experts), so refuse instead of mis-routing.
28        anyhow::ensure!(
29            self.router_logits_n as usize == ctx.config.num_experts,
30            "zero-expert MoE routing is not wired on this dispatch variant yet (forward_token_major)"
31        );
32        self.refuse_lean_layout("MoeLayer::forward_token_major_decode")?;
33
34        // 2026-09-25: The token-major kernels have no LoRA fold. With a MoE
35        // adapter installed, `forward_batched` folds the router and expert deltas
36        // per row. The test is on adapter presence, not on the per-step route,
37        // so a captured graph holds no host branch on the route.
38        if self.lora.is_some() {
39            return self.forward_batched(input, num_tokens, ctx, stream);
40        }
41        let has_shared = self.weights.shared_expert.gate_proj.weight.0 != 0
42            && self.weights.shared_expert.up_proj.weight.0 != 0
43            && self.weights.shared_expert.down_proj.weight.0 != 0;
44        let nvfp4_supported = self.bf16_gate_weight_ptrs.is_none()
45            && !self.has_mixed_bf16_shared_expert()
46            && self.fp8_gate_weight_ptrs.is_none()
47            && !self.use_t_layout_for_decode()
48            && has_shared;
49        if !nvfp4_supported {
50            return self.forward_batched(input, num_tokens, ctx, stream);
51        }
52
53        let h = ctx.config.hidden_size as u32;
54        let inter = ctx.config.moe_intermediate_size as u32;
55        let num_experts = ctx.config.num_experts as u32;
56        let top_k = ctx.config.num_experts_per_tok as u32;
57        let n = num_tokens as u32;
58
59        let router_in = self.router_input(input, n, h, ctx, stream)?;
60        let gate_logits = ctx.buffers.gate_logits();
61        if let Some(ref nvfp4) = self.gate_nvfp4 {
62            ops::w4a16_gemm(
63                ctx.gpu,
64                self.w4a16_gemm,
65                router_in,
66                nvfp4,
67                gate_logits,
68                n,
69                num_experts,
70                h,
71                stream,
72            )?;
73        } else {
74            ops::dense_gemm(
75                ctx.gpu,
76                self.dense_gemm,
77                router_in,
78                &self.weights.gate,
79                gate_logits,
80                n,
81                num_experts,
82                h,
83                stream,
84            )?;
85        }
86
87        let scratch = ctx.buffers.scratch();
88        let indices_dev = scratch;
89        let weights_dev = scratch.offset(num_tokens * top_k as usize * 4);
90        if let Some(bias) = self.correction_bias_dev {
91            ops::moe_topk_sigmoid_batched(
92                ctx.gpu,
93                self.moe_topk_sigmoid_batched_k,
94                gate_logits,
95                bias,
96                indices_dev,
97                weights_dev,
98                num_experts,
99                top_k,
100                ctx.config.norm_topk_prob,
101                ctx.config.routed_scaling_factor as f32,
102                n,
103                stream,
104            )?;
105        } else {
106            ops::moe_topk_softmax_batched(
107                ctx.gpu,
108                self.moe_topk_batched,
109                gate_logits,
110                indices_dev,
111                weights_dev,
112                num_experts,
113                top_k,
114                ctx.config.norm_topk_prob,
115                n,
116                stream,
117            )?;
118        }
119
120        let expert_gate_out = ctx.buffers.expert_gate_out();
121        let expert_up_out = ctx.buffers.expert_up_out();
122        let expert_down_out = ctx.buffers.expert_down_out();
123        let shared_gate_scratch = ctx.buffers.logits();
124        let shared_up_scratch = ctx.buffers.ssm_qkvz();
125        let shared_down_out = ctx.buffers.attn_output();
126        let output = ctx.buffers.moe_output();
127
128        ops::moe_expert_gate_up_shared_prefill(
129            ctx.gpu,
130            self.moe_expert_gate_up_shared_token_major,
131            input,
132            self.gate_ptrs.packed_ptrs,
133            self.gate_ptrs.scale_ptrs,
134            self.gate_ptrs.scale2_vals,
135            expert_gate_out,
136            self.up_ptrs.packed_ptrs,
137            self.up_ptrs.scale_ptrs,
138            self.up_ptrs.scale2_vals,
139            expert_up_out,
140            indices_dev,
141            &self.weights.shared_expert.gate_proj,
142            shared_gate_scratch,
143            &self.weights.shared_expert.up_proj,
144            shared_up_scratch,
145            inter,
146            h,
147            top_k,
148            n,
149            stream,
150        )?;
151        ops::moe_expert_silu_down_shared_prefill(
152            ctx.gpu,
153            self.moe_expert_silu_down_shared_token_major,
154            expert_gate_out,
155            expert_up_out,
156            self.down_ptrs.packed_ptrs,
157            self.down_ptrs.scale_ptrs,
158            self.down_ptrs.scale2_vals,
159            expert_down_out,
160            indices_dev,
161            shared_gate_scratch,
162            shared_up_scratch,
163            &self.weights.shared_expert.down_proj,
164            shared_down_out,
165            h,
166            inter,
167            top_k,
168            n,
169            stream,
170        )?;
171
172        let is_ep = ctx.comm.is_some() && ctx.config.ep_world_size > 1;
173        let shared_for_blend = if is_ep {
174            ctx.gpu
175                .memset_async(expert_gate_out, 0, num_tokens * h as usize * 2, stream)?;
176            expert_gate_out
177        } else {
178            shared_down_out
179        };
180        ops::moe_weighted_sum_blend_prefill(
181            ctx.gpu,
182            self.moe_weighted_sum_blend_token_major,
183            output,
184            expert_down_out,
185            weights_dev,
186            shared_for_blend,
187            input,
188            self.weights.shared_expert_gate.weight,
189            h,
190            top_k,
191            h,
192            n,
193            stream,
194        )?;
195
196        if let Some(comm) = ctx.comm
197            && ctx.config.ep_world_size > 1
198        {
199            if ctx.graph_capture {
200                comm.all_reduce(output.0, num_tokens * h as usize * 2)?;
201            } else {
202                comm.all_reduce_async(output.0, num_tokens * h as usize * 2, stream)?;
203            }
204            if self.weights.shared_expert_gate.weight.0 == 0 {
205                ops::residual_add(
206                    ctx.gpu,
207                    self.residual_add,
208                    output,
209                    shared_down_out,
210                    n * h,
211                    stream,
212                )?;
213            } else {
214                ops::moe_batched_blend(
215                    ctx.gpu,
216                    self.moe_batched_blend,
217                    output,
218                    shared_down_out,
219                    input,
220                    self.weights.shared_expert_gate.weight,
221                    h,
222                    n,
223                    stream,
224                )?;
225            }
226        }
227
228        Ok(())
229    }
230}