metrale_model_layers/layers/moe/
forward_atomic_c4.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `MoeLayer::forward_atomic_c4_decode`, the opt-in 4-row MoE decode
4//! with FP32 atomic accumulation of the routed down projections.
5//!
6//! Owner: model-layers (MoE).
7//! Invariants: none beyond the types.
8
9use super::*;
10
11impl MoeLayer {
12    /// 2026-09-25: 4-row NVFP4 MoE decode, reached from
13    /// `qwen3_ssm/trait_decode_multi_seq.rs` when `METRALE_MOE_ATOMIC_C4_DECODE=1`.
14    ///
15    /// The gate GEMM and top-k run batched. Gate+up uses the token-major
16    /// kernel, then the routed down projections atomic-add their weighted FP32
17    /// results into a `[num_tokens, H]` accumulator in `scratch`. The finalize
18    /// kernel casts it to BF16 and blends the shared expert, except under EP,
19    /// where the shared expert is blended once after the all-reduce.
20    ///
21    /// Falls back to `forward_batched` unless `num_tokens == 4`, both atomic
22    /// kernels are present, the experts are NVFP4 (no BF16 or FP8 expert
23    /// tables, no BF16 shared expert) in the non-transposed decode layout,
24    /// there is no pre-expert norm, and a shared expert exists.
25    pub fn forward_atomic_c4_decode(
26        &self,
27        input: DevicePtr,
28        num_tokens: usize,
29        ctx: &ForwardContext,
30        stream: u64,
31    ) -> Result<()> {
32        anyhow::ensure!(
33            self.router_logits_n as usize == ctx.config.num_experts,
34            "zero-expert MoE routing is not wired on this dispatch variant yet (forward_atomic_c4)"
35        );
36        self.refuse_lean_layout("MoeLayer::forward_atomic_c4_decode")?;
37
38        // 2026-09-25: This path folds no LoRA delta, so `reject_decode_lora`
39        // refuses an adapter-routed batch.
40        self.reject_decode_lora(ctx, "forward_atomic_c4_decode")?;
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 = num_tokens == 4
45            && self.moe_decode_atomic_c4_silu_down_accum_k.0 != 0
46            && self.moe_decode_atomic_c4_finalize_k.0 != 0
47            && self.bf16_gate_weight_ptrs.is_none()
48            && !self.has_mixed_bf16_shared_expert()
49            && self.fp8_gate_weight_ptrs.is_none()
50            && !self.use_t_layout_for_decode()
51            && self.pre_expert_norm.is_none()
52            && has_shared;
53        if !nvfp4_supported {
54            return self.forward_batched(input, num_tokens, ctx, stream);
55        }
56
57        let h = ctx.config.hidden_size as u32;
58        let inter = ctx.config.moe_intermediate_size as u32;
59        let num_experts = ctx.config.num_experts as u32;
60        let top_k = ctx.config.num_experts_per_tok as u32;
61        let n = num_tokens as u32;
62
63        let router_in = self.router_input(input, n, h, ctx, stream)?;
64        let gate_logits = ctx.buffers.gate_logits();
65        if let Some(ref nvfp4) = self.gate_nvfp4 {
66            ops::w4a16_gemm(
67                ctx.gpu,
68                self.w4a16_gemm,
69                router_in,
70                nvfp4,
71                gate_logits,
72                n,
73                num_experts,
74                h,
75                stream,
76            )?;
77        } else {
78            ops::dense_gemm(
79                ctx.gpu,
80                self.dense_gemm,
81                router_in,
82                &self.weights.gate,
83                gate_logits,
84                n,
85                num_experts,
86                h,
87                stream,
88            )?;
89        }
90
91        let scratch = ctx.buffers.scratch();
92        let topk_bytes = num_tokens * top_k as usize * 4;
93        let indices_dev = scratch;
94        let weights_dev = scratch.offset(topk_bytes);
95        let accum_off = (topk_bytes * 2 + 255) & !255;
96        let accum_bytes = num_tokens * h as usize * 4;
97        anyhow::ensure!(
98            ctx.buffers.scratch_bytes() >= accum_off + accum_bytes,
99            "scratch too small for METRALE_MOE_ATOMIC_C4_DECODE: need {} bytes, have {}",
100            accum_off + accum_bytes,
101            ctx.buffers.scratch_bytes()
102        );
103        let routed_accum = scratch.offset(accum_off);
104
105        if let Some(bias) = self.correction_bias_dev {
106            ops::moe_topk_sigmoid_batched(
107                ctx.gpu,
108                self.moe_topk_sigmoid_batched_k,
109                gate_logits,
110                bias,
111                indices_dev,
112                weights_dev,
113                num_experts,
114                top_k,
115                ctx.config.norm_topk_prob,
116                ctx.config.routed_scaling_factor as f32,
117                n,
118                stream,
119            )?;
120        } else {
121            ops::moe_topk_softmax_batched(
122                ctx.gpu,
123                self.moe_topk_batched,
124                gate_logits,
125                indices_dev,
126                weights_dev,
127                num_experts,
128                top_k,
129                ctx.config.norm_topk_prob,
130                n,
131                stream,
132            )?;
133        }
134
135        let expert_gate_out = ctx.buffers.expert_gate_out();
136        let expert_up_out = ctx.buffers.expert_up_out();
137        let shared_gate_scratch = ctx.buffers.logits();
138        let shared_up_scratch = ctx.buffers.ssm_qkvz();
139        let shared_down_out = ctx.buffers.attn_output();
140        let output = ctx.buffers.moe_output();
141
142        ops::moe_expert_gate_up_shared_prefill(
143            ctx.gpu,
144            self.moe_expert_gate_up_shared_token_major,
145            input,
146            self.gate_ptrs.packed_ptrs,
147            self.gate_ptrs.scale_ptrs,
148            self.gate_ptrs.scale2_vals,
149            expert_gate_out,
150            self.up_ptrs.packed_ptrs,
151            self.up_ptrs.scale_ptrs,
152            self.up_ptrs.scale2_vals,
153            expert_up_out,
154            indices_dev,
155            &self.weights.shared_expert.gate_proj,
156            shared_gate_scratch,
157            &self.weights.shared_expert.up_proj,
158            shared_up_scratch,
159            inter,
160            h,
161            top_k,
162            n,
163            stream,
164        )?;
165
166        ctx.gpu.memset_async(routed_accum, 0, accum_bytes, stream)?;
167        ops::moe_decode_atomic_c4_silu_down_accum(
168            ctx.gpu,
169            self.moe_decode_atomic_c4_silu_down_accum_k,
170            expert_gate_out,
171            expert_up_out,
172            self.down_ptrs.packed_ptrs,
173            self.down_ptrs.scale_ptrs,
174            self.down_ptrs.scale2_vals,
175            indices_dev,
176            weights_dev,
177            routed_accum,
178            shared_gate_scratch,
179            shared_up_scratch,
180            &self.weights.shared_expert.down_proj,
181            shared_down_out,
182            h,
183            inter,
184            top_k,
185            n,
186            stream,
187        )?;
188
189        let is_ep = ctx.comm.is_some() && ctx.config.ep_world_size > 1;
190        ops::moe_decode_atomic_c4_finalize(
191            ctx.gpu,
192            self.moe_decode_atomic_c4_finalize_k,
193            output,
194            routed_accum,
195            shared_down_out,
196            input,
197            self.weights.shared_expert_gate.weight,
198            h,
199            n,
200            !is_ep,
201            stream,
202        )?;
203
204        if let Some(comm) = ctx.comm
205            && ctx.config.ep_world_size > 1
206        {
207            if ctx.graph_capture {
208                comm.all_reduce(output.0, num_tokens * h as usize * 2)?;
209            } else {
210                comm.all_reduce_async(output.0, num_tokens * h as usize * 2, stream)?;
211            }
212            ops::moe_batched_blend(
213                ctx.gpu,
214                self.moe_batched_blend,
215                output,
216                shared_down_out,
217                input,
218                self.weights.shared_expert_gate.weight,
219                h,
220                n,
221                stream,
222            )?;
223        }
224
225        Ok(())
226    }
227}