metrale_model_layers/layers/moe/
forward_bf16_grouped_decode.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-10-02: `MoeLayer::forward_bf16_grouped_decode`: the MoE of `m` rows over BF16 experts
4//! (`set_bf16_experts`) in one routed+shared dispatch, on the BF16 point of the tensor-core grouped
5//! family (`moe_bf16_grouped_tc.cu`). The steps are those of the grouped NVFP4 decode
6//! (`forward_nvfp4_grouped_decode.rs`): the per-row router, `moe_fp8_grouped_sort`, gate+up and
7//! SiLU, down, the grouped blend. Its user is the MTP drafter of a checkpoint whose MTP head is
8//! BF16 (nvidia/Qwen3.6-35B-A3B-NVFP4), so a batched propose runs the drafter's MoE once for all
9//! its sequences instead of per-expert GEMVs per sequence.
10//!
11//! Owner: model-layers (MoE).
12//! Invariants: `forward_bf16_grouped_decode` launches nothing unless
13//! `bf16_grouped_decode_ok(m, ctx)` holds; a row's bits do not depend on `m`.
14
15use super::forward_fp8_grouped_decode::grouped_decode_buffer_need;
16use super::forward_nvfp4_grouped_decode::NVFP4_GROUPED_DECODE_TC_MAX_ROWS;
17use super::*;
18
19impl MoeLayer {
20    /// 2026-10-02: The weight, kernel, shape and arena terms of
21    /// [`Self::bf16_grouped_decode_ok`], decidable without a `ForwardContext` (the MTP batch cap
22    /// sizes a batch with it).
23    pub fn bf16_grouped_decode_arena_ok(
24        &self,
25        m: usize,
26        cfg: &metrale_config::ModelConfig,
27        b: &metrale_gpu_runtime::buffers::BufferArena,
28    ) -> bool {
29        let (h, inter) = (cfg.hidden_size, cfg.moe_intermediate_size);
30        let need =
31            grouped_decode_buffer_need(m, h, inter, cfg.num_experts, cfg.num_experts_per_tok);
32        let k = &self.nvfp4_grouped;
33        (1..=NVFP4_GROUPED_DECODE_TC_MAX_ROWS).contains(&m)
34            && ops::bf16_grouped_tc_shape_ok(inter as u32, h as u32, ops::BF16_GROUPED_GATE_UP_TC)
35            && ops::bf16_grouped_tc_shape_ok(h as u32, inter as u32, ops::BF16_GROUPED_DOWN_TC)
36            && k.bf16_gate_up_tc.0 != 0
37            && k.bf16_down_tc.0 != 0
38            && self.bf16_gate_weight_ptrs.is_some()
39            && self.bf16_up_weight_ptrs.is_some()
40            && self.bf16_down_weight_ptrs.is_some()
41            && self.bf16_shared_expert.is_some()
42            && self.moe_weighted_sum_blend_fp8_grouped_k.0 != 0
43            && self.moe_fp8_grouped_sort_k.0 != 0
44            && self.moe_topk_softmax_rows_k.0 != 0
45            && self.router_gemv_batchm_k.0 != 0
46            && cfg.num_experts <= ops::FP8_GROUPED_SORT_MAX_EXPERTS as usize
47            && self.router_logits_n as usize == cfg.num_experts
48            && self.gate_nvfp4.is_none()
49            && self.correction_bias_dev.is_none()
50            && self.weights.router_pre_norm.is_none()
51            && self.lora.is_none()
52            && self.pre_expert_norm.is_none()
53            && self.tid2eid_dev.is_none()
54            && h.is_multiple_of(8)
55            && arena_fits(&need, b)
56    }
57
58    /// 2026-10-02: Whether `forward_bf16_grouped_decode` serves `m` rows under `ctx`: the arena
59    /// terms, the per-row router's context terms and no expert parallelism.
60    pub fn bf16_grouped_decode_ok(&self, m: usize, ctx: &ForwardContext) -> bool {
61        let cfg = ctx.config;
62        let refused = ctx.levers.fp32_gate
63            || self.fp32_routing_active(ctx.levers)
64            || (self.is_dflash_capture_layer && ctx.levers.frankenstein_decode_via_prefill)
65            || (ctx.comm.is_some() && cfg.ep_world_size > 1);
66        self.bf16_grouped_decode_arena_ok(m, cfg, ctx.buffers) && !refused
67    }
68
69    /// 2026-10-02: The BF16 MoE of `m` rows: `input` is `[m, H]` BF16, the output lands in rows
70    /// `0..m` of `moe_output()`. Returns an error, launching nothing, when
71    /// `bf16_grouped_decode_ok(m, ctx)` is false.
72    pub fn forward_bf16_grouped_decode(
73        &self,
74        input: DevicePtr,
75        m: usize,
76        ctx: &ForwardContext,
77        stream: u64,
78    ) -> Result<()> {
79        anyhow::ensure!(
80            self.bf16_grouped_decode_ok(m, ctx),
81            "forward_bf16_grouped_decode: predicate false for m={m} (caller must gate on it)"
82        );
83        let (Some(gp), Some(up), Some(dp), Some(sh)) = (
84            self.bf16_gate_weight_ptrs,
85            self.bf16_up_weight_ptrs,
86            self.bf16_down_weight_ptrs,
87            self.bf16_shared_expert.as_ref(),
88        ) else {
89            anyhow::bail!("forward_bf16_grouped_decode: BF16 experts missing");
90        };
91        let h = ctx.config.hidden_size as u32;
92        let inter = ctx.config.moe_intermediate_size as u32;
93        let num_experts = ctx.config.num_experts as u32;
94        let top_k = ctx.config.num_experts_per_tok as u32;
95        let n = m as u32;
96        let te = m * top_k as usize;
97        if ctx.stats.once("log:moe_bf16_grouped_decode") {
98            tracing::info!(
99                "MoE BF16 grouped decode active (first use M={m}, top_k={top_k}, \
100                 experts={num_experts}; one-time log)"
101            );
102        }
103        let scratch = ctx.buffers.scratch();
104        let indices_dev = scratch;
105        let weights_dev = scratch.offset(te * 4);
106        self.grouped_route(
107            input,
108            m,
109            GroupedRouting::PerRow,
110            indices_dev,
111            weights_dev,
112            ctx,
113            stream,
114        )?;
115        // 2026-10-02: The sort scratch in `gate_logits`, as the grouped NVFP4 decode lays it out.
116        let ne = num_experts as usize;
117        let gate_logits = ctx.buffers.gate_logits();
118        let sorted_token_ids = gate_logits;
119        let sorted_expert_ids = gate_logits.offset(te * 4);
120        let expert_offsets = gate_logits.offset(te * 4 * 2);
121        let token_to_perm = gate_logits.offset(te * 4 * 2 + (ne + 1) * 4);
122        let cap = ops::fp8_grouped_active_cap(n, top_k, num_experts);
123        let active_experts = token_to_perm.offset(te * 4);
124        let active_count = active_experts.offset(cap as usize * 4);
125        ops::moe_fp8_grouped_sort(
126            ctx.gpu,
127            self.moe_fp8_grouped_sort_k,
128            ops::Fp8GroupedSortOut {
129                sorted_token_ids,
130                sorted_expert_ids,
131                expert_offsets,
132                token_to_perm,
133                active_experts,
134                active_count,
135            },
136            indices_dev,
137            te as u32,
138            num_experts,
139            top_k,
140            stream,
141        )?;
142        let act = ctx.buffers.expert_gate_out();
143        let expert_down_out = ctx.buffers.expert_down_out();
144        let shared_act = ctx.buffers.logits();
145        let shared_out = ctx.buffers.attn_output();
146        let k = &self.nvfp4_grouped;
147        ops::moe_expert_gate_up_act_bf16_grouped(
148            ctx.gpu,
149            k.bf16_gate_up_tc,
150            input,
151            gp,
152            up,
153            act,
154            expert_offsets,
155            sorted_token_ids,
156            active_experts,
157            active_count,
158            sh.gate_proj.weight,
159            sh.up_proj.weight,
160            shared_act,
161            inter,
162            h,
163            cap,
164            n,
165            stream,
166        )?;
167        ops::moe_expert_down_act_bf16_grouped(
168            ctx.gpu,
169            k.bf16_down_tc,
170            act,
171            dp,
172            expert_down_out,
173            expert_offsets,
174            active_experts,
175            active_count,
176            shared_act,
177            sh.down_proj.weight,
178            shared_out,
179            h,
180            inter,
181            cap,
182            n,
183            stream,
184        )?;
185        ops::moe_weighted_sum_blend_fp8_grouped(
186            ctx.gpu,
187            self.moe_weighted_sum_blend_fp8_grouped_k,
188            ctx.buffers.moe_output(),
189            expert_down_out,
190            weights_dev,
191            token_to_perm,
192            shared_out,
193            input,
194            self.weights.shared_expert_gate.weight,
195            h,
196            top_k,
197            h,
198            n,
199            stream,
200        )
201    }
202
203    /// 2026-10-02: The grouped decode of `m` rows on whichever expert tables this layer holds for
204    /// it: FP8 (`forward_fp8_grouped_decode`) or BF16 (`forward_bf16_grouped_decode`).
205    pub fn any_grouped_decode_ok(&self, m: usize, ctx: &ForwardContext) -> bool {
206        if self.nvfp4_grouped.declared_experts {
207            self.nvfp4_grouped_decode_ok(m, ctx)
208        } else if self.fp8_gate_weight_ptrs.is_some() {
209            self.fp8_grouped_decode_ok(m, ctx)
210        } else {
211            self.bf16_grouped_decode_ok(m, ctx)
212        }
213    }
214
215    /// 2026-10-02: [`Self::any_grouped_decode_ok`]'s arena terms.
216    pub fn any_grouped_decode_arena_ok(
217        &self,
218        m: usize,
219        cfg: &metrale_config::ModelConfig,
220        b: &metrale_gpu_runtime::buffers::BufferArena,
221    ) -> bool {
222        if self.nvfp4_grouped.declared_experts {
223            // 2026-10-02: The NVFP4 grouped decode's arena needs are the FP8 grouped decode's.
224            (1..=NVFP4_GROUPED_DECODE_TC_MAX_ROWS).contains(&m)
225                && arena_fits(
226                    &grouped_decode_buffer_need(
227                        m,
228                        cfg.hidden_size,
229                        cfg.moe_intermediate_size,
230                        cfg.num_experts,
231                        cfg.num_experts_per_tok,
232                    ),
233                    b,
234                )
235        } else if self.fp8_gate_weight_ptrs.is_some() {
236            self.fp8_grouped_decode_arena_ok(m, cfg, b)
237        } else {
238            self.bf16_grouped_decode_arena_ok(m, cfg, b)
239        }
240    }
241
242    /// 2026-10-02: Run [`Self::any_grouped_decode_ok`]'s path.
243    pub fn forward_any_grouped_decode(
244        &self,
245        input: DevicePtr,
246        m: usize,
247        ctx: &ForwardContext,
248        stream: u64,
249    ) -> Result<()> {
250        if self.nvfp4_grouped.declared_experts {
251            self.forward_nvfp4_grouped_decode(input, m, ctx, stream)
252        } else if self.fp8_gate_weight_ptrs.is_some() {
253            self.forward_fp8_grouped_decode(input, m, ctx, stream)
254        } else {
255            self.forward_bf16_grouped_decode(input, m, ctx, stream)
256        }
257    }
258}
259
260/// 2026-10-02: Whether the arena holds a grouped decode's buffers (`grouped_decode_buffer_need`).
261fn arena_fits(
262    need: &super::forward_fp8_grouped_decode::GroupedDecodeBufferNeed,
263    b: &metrale_gpu_runtime::buffers::BufferArena,
264) -> bool {
265    b.scratch_bytes() >= need.scratch
266        && b.gate_logits_bytes() >= need.gate_logits
267        && b.expert_gate_out_bytes() >= need.expert_gate_out
268        && b.expert_down_out_bytes() >= need.expert_down_out
269        && b.logits_bytes() >= need.shared_act
270        && b.attn_output_bytes() >= need.row_hidden
271        && b.moe_output_bytes() >= need.row_hidden
272}