spark_model/layers/moe/
forward_token_major.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Token-major N-token MoE decode experiment.
4
5use super::*;
6
7impl MoeLayer {
8    /// Token-major fused decode for small N>=4.
9    ///
10    /// This reuses the generic `moe_prefill` kernels without the sorted/grouped
11    /// GEMM path. It batches gate/top-k and processes all `(token, expert-slot)`
12    /// routes in three token-major kernels:
13    ///
14    /// gate GEMM -> batched topK -> gate+up -> silu+down -> wsum/blend.
15    ///
16    /// First pass is NVFP4 + shared-expert only, matching Holo's current decode
17    /// path. FP8/BF16/unified-layout variants deliberately fall back to the
18    /// existing implementation until they have equivalent generic kernels.
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        // LongCat zero-experts are wired only on the single-token decode
27        // + prefill paths (v1); this variant would silently mis-route the
28        // 384-wide router. Named refusal, not silent wrongness.
29        anyhow::ensure!(
30            self.router_logits_n as usize == ctx.config.num_experts,
31            "zero-expert MoE routing is not wired on this dispatch variant yet (forward_token_major)"
32        );
33
34        // SOLID Incr-4: the token-major fast path has no fold hooks. When a
35        // MoE adapter is RESIDENT, delegate to the per-row batched fallback,
36        // which folds router + gate/up/down route-agnostically (base rows
37        // no-op device-side via the `moe_row_adapter` map; a homogeneous
38        // batch without the map uses the request-granularity
39        // `moe_route_gate`). PRESENCE-gated exactly like forward_k2/k3 —
40        // install-time-fixed, so graph-safe (decode graphs drain on adapter
41        // rotate/swap); gating on the per-step ROUTE here would bake a host
42        // branch into a captured `padded_n` graph. A `Refuse` batch was
43        // already bailed host-side before the ladder
44        // (`ensure_decode_route_servable` in `decode_batch_compute_main`).
45        if self.lora.is_some() {
46            return self.forward_batched(input, num_tokens, ctx, stream);
47        }
48        let has_shared = self.weights.shared_expert.gate_proj.weight.0 != 0
49            && self.weights.shared_expert.up_proj.weight.0 != 0
50            && self.weights.shared_expert.down_proj.weight.0 != 0;
51        let nvfp4_supported = self.bf16_gate_weight_ptrs.is_none()
52            && !self.has_mixed_bf16_shared_expert()
53            && self.fp8_gate_weight_ptrs.is_none()
54            && !self.use_t_layout_for_decode()
55            && has_shared;
56        if !nvfp4_supported {
57            return self.forward_batched(input, num_tokens, ctx, stream);
58        }
59
60        let h = ctx.config.hidden_size as u32;
61        let inter = ctx.config.moe_intermediate_size as u32;
62        let num_experts = ctx.config.num_experts as u32;
63        let top_k = ctx.config.num_experts_per_tok as u32;
64        let n = num_tokens as u32;
65
66        let router_in = self.router_input(input, n, h, ctx, stream)?;
67        let gate_logits = ctx.buffers.gate_logits();
68        if let Some(ref nvfp4) = self.gate_nvfp4 {
69            ops::w4a16_gemm(
70                ctx.gpu,
71                self.w4a16_gemm,
72                router_in,
73                nvfp4,
74                gate_logits,
75                n,
76                num_experts,
77                h,
78                stream,
79            )?;
80        } else {
81            ops::dense_gemm(
82                ctx.gpu,
83                self.dense_gemm,
84                router_in,
85                &self.weights.gate,
86                gate_logits,
87                n,
88                num_experts,
89                h,
90                stream,
91            )?;
92        }
93
94        let scratch = ctx.buffers.scratch();
95        let indices_dev = scratch;
96        let weights_dev = scratch.offset(num_tokens * 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                n,
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                n,
123                stream,
124            )?;
125        }
126
127        let expert_gate_out = ctx.buffers.expert_gate_out();
128        let expert_up_out = ctx.buffers.expert_up_out();
129        let expert_down_out = ctx.buffers.expert_down_out();
130        let shared_gate_scratch = ctx.buffers.logits();
131        let shared_up_scratch = ctx.buffers.ssm_qkvz();
132        let shared_down_out = ctx.buffers.attn_output();
133        let output = ctx.buffers.moe_output();
134
135        ops::moe_expert_gate_up_shared_prefill(
136            ctx.gpu,
137            self.moe_expert_gate_up_shared_token_major,
138            input,
139            self.gate_ptrs.packed_ptrs,
140            self.gate_ptrs.scale_ptrs,
141            self.gate_ptrs.scale2_vals,
142            expert_gate_out,
143            self.up_ptrs.packed_ptrs,
144            self.up_ptrs.scale_ptrs,
145            self.up_ptrs.scale2_vals,
146            expert_up_out,
147            indices_dev,
148            &self.weights.shared_expert.gate_proj,
149            shared_gate_scratch,
150            &self.weights.shared_expert.up_proj,
151            shared_up_scratch,
152            inter,
153            h,
154            top_k,
155            n,
156            stream,
157        )?;
158        ops::moe_expert_silu_down_shared_prefill(
159            ctx.gpu,
160            self.moe_expert_silu_down_shared_token_major,
161            expert_gate_out,
162            expert_up_out,
163            self.down_ptrs.packed_ptrs,
164            self.down_ptrs.scale_ptrs,
165            self.down_ptrs.scale2_vals,
166            expert_down_out,
167            indices_dev,
168            shared_gate_scratch,
169            shared_up_scratch,
170            &self.weights.shared_expert.down_proj,
171            shared_down_out,
172            h,
173            inter,
174            top_k,
175            n,
176            stream,
177        )?;
178
179        let is_ep = ctx.comm.is_some() && ctx.config.ep_world_size > 1;
180        let shared_for_blend = if is_ep {
181            ctx.gpu
182                .memset_async(expert_gate_out, 0, num_tokens * h as usize * 2, stream)?;
183            expert_gate_out
184        } else {
185            shared_down_out
186        };
187        ops::moe_weighted_sum_blend_prefill(
188            ctx.gpu,
189            self.moe_weighted_sum_blend_token_major,
190            output,
191            expert_down_out,
192            weights_dev,
193            shared_for_blend,
194            input,
195            self.weights.shared_expert_gate.weight,
196            h,
197            top_k,
198            h,
199            n,
200            stream,
201        )?;
202
203        if let Some(comm) = ctx.comm
204            && ctx.config.ep_world_size > 1
205        {
206            if ctx.graph_capture {
207                comm.all_reduce(output.0, num_tokens * h as usize * 2)?;
208            } else {
209                comm.all_reduce_async(output.0, num_tokens * h as usize * 2, stream)?;
210            }
211            if self.weights.shared_expert_gate.weight.0 == 0 {
212                ops::residual_add(
213                    ctx.gpu,
214                    self.residual_add,
215                    output,
216                    shared_down_out,
217                    n * h,
218                    stream,
219                )?;
220            } else {
221                ops::moe_batched_blend(
222                    ctx.gpu,
223                    self.moe_batched_blend,
224                    output,
225                    shared_down_out,
226                    input,
227                    self.weights.shared_expert_gate.weight,
228                    h,
229                    n,
230                    stream,
231                )?;
232            }
233        }
234
235        Ok(())
236    }
237}