spark_model/layers/moe/
helpers_c.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Shared-expert precision setup, predequantization, and router input.
4
5use super::*;
6
7impl MoeLayer {
8    /// Pre-dequant dense (non-expert) NVFP4 weights to FP8 for zero-overhead prefill.
9    ///
10    /// Only affects gate GEMM and shared expert GEMMs.  Expert weights stay NVFP4
11    /// (they're bandwidth-bound so FP8 wouldn't help).
12    pub fn predequant_for_prefill(
13        &mut self,
14        gpu: &dyn GpuBackend,
15        config: &atlas_core::config::ModelConfig,
16        stream: u64,
17    ) -> Result<()> {
18        let h = config.hidden_size;
19        let shared_inter = config.shared_expert_intermediate_size;
20        let num_experts = config.num_experts;
21        let predequant_k = gpu.kernel("w4a16", "predequant_nvfp4_to_fp8")?;
22
23        // Pre-dequant gate weight: [num_experts, H] → FP8 [num_experts, H]
24        if let Some(ref nvfp4) = self.gate_nvfp4 {
25            self.gate_fp8 =
26                Some(nvfp4.predequant_to_fp8(gpu, predequant_k, num_experts, h, stream)?);
27        }
28
29        // A checkpoint-native BF16 shared expert is the authoritative copy.
30        // Do not manufacture an FP8 prefill variant with different numerics.
31        if self.bf16_shared_expert.is_none()
32            && !self.weights.shared_expert.gate_proj.is_null()
33            && shared_inter > 0
34        {
35            self.shared_gate_fp8 = Some(self.weights.shared_expert.gate_proj.predequant_to_fp8(
36                gpu,
37                predequant_k,
38                shared_inter,
39                h,
40                stream,
41            )?);
42            self.shared_up_fp8 = Some(self.weights.shared_expert.up_proj.predequant_to_fp8(
43                gpu,
44                predequant_k,
45                shared_inter,
46                h,
47                stream,
48            )?);
49            self.shared_down_fp8 = Some(self.weights.shared_expert.down_proj.predequant_to_fp8(
50                gpu,
51                predequant_k,
52                h,
53                shared_inter,
54                stream,
55            )?);
56        }
57
58        Ok(())
59    }
60
61    /// Set FP8 expert weights for native FP8 dispatch.
62    ///
63    /// Builds device-side pointer tables from FP8 expert weights so the
64    /// fused FP8 MoE kernel can index by expert_id at dispatch time.
65    /// Also stores the shared expert FP8 weights for direct pointer passing.
66    pub fn set_fp8_experts(
67        &mut self,
68        experts: &[Fp8ExpertWeight],
69        shared_expert: Fp8ExpertWeight,
70        gpu: &dyn GpuBackend,
71    ) -> Result<()> {
72        self.fp8_gate_weight_ptrs = Some(build_fp8_ptr_table(experts, |e| &e.gate_proj, gpu)?);
73        self.fp8_up_weight_ptrs = Some(build_fp8_ptr_table(experts, |e| &e.up_proj, gpu)?);
74        self.fp8_down_weight_ptrs = Some(build_fp8_ptr_table(experts, |e| &e.down_proj, gpu)?);
75        self.fp8_shared_expert = Some(shared_expert);
76        Ok(())
77    }
78
79    /// Set BF16 expert weights for the FP8-dequant-on-load MoE path.
80    ///
81    /// Activated by `ATLAS_FP8_DEQUANT_MOE_TO_BF16=1`. Eliminates the per-layer
82    /// 0.989 FP8 cosine ceiling (measured in bench/fp8_dgx2_drift/cosine_run.py)
83    /// by serving experts as BF16 throughout, matching vLLM-BF16 reference
84    /// numerics. Memory cost: 2× expert weights vs native FP8.
85    ///
86    /// `shared_*` are the shared expert's BF16 gate/up/down DevicePtrs (or
87    /// `DevicePtr::NULL` when the model has no shared expert).
88    pub fn set_bf16_experts(
89        &mut self,
90        gate_experts: &[crate::weight_map::DenseWeight],
91        up_experts: &[crate::weight_map::DenseWeight],
92        down_experts: &[crate::weight_map::DenseWeight],
93        shared_gate: DevicePtr,
94        shared_up: DevicePtr,
95        shared_down: DevicePtr,
96        gpu: &dyn GpuBackend,
97    ) -> Result<()> {
98        use super::build_bf16_ptr_table;
99        self.bf16_gate_weight_ptrs = Some(build_bf16_ptr_table(gate_experts, gpu)?);
100        self.bf16_up_weight_ptrs = Some(build_bf16_ptr_table(up_experts, gpu)?);
101        self.bf16_down_weight_ptrs = Some(build_bf16_ptr_table(down_experts, gpu)?);
102        if shared_gate.is_null() && shared_up.is_null() && shared_down.is_null() {
103            self.bf16_shared_expert = None;
104        } else {
105            self.set_bf16_shared_expert(
106                DenseWeight {
107                    weight: shared_gate,
108                },
109                DenseWeight { weight: shared_up },
110                DenseWeight {
111                    weight: shared_down,
112                },
113            )?;
114        }
115        Ok(())
116    }
117
118    /// Install checkpoint-native BF16 shared-expert weights independently of
119    /// routed-expert precision.
120    pub fn set_bf16_shared_expert(
121        &mut self,
122        gate_proj: DenseWeight,
123        up_proj: DenseWeight,
124        down_proj: DenseWeight,
125    ) -> Result<()> {
126        self.bf16_shared_expert = Some(Bf16SharedExpert::new(gate_proj, up_proj, down_proj)?);
127        Ok(())
128    }
129
130    /// Whether a BF16 shared expert must overwrite the contribution produced
131    /// by a quantized fused routed-expert kernel.
132    pub(super) fn has_mixed_bf16_shared_expert(&self) -> bool {
133        self.bf16_shared_expert.is_some() && self.bf16_gate_weight_ptrs.is_none()
134    }
135
136    /// Evaluate a checkpoint-native BF16 shared expert into `down_out`.
137    ///
138    /// Callers supply scratch buffers because the safe aliases differ between
139    /// decode and prefill. Returns `true` when BF16 weights were installed.
140    #[allow(clippy::too_many_arguments)]
141    pub(super) fn run_bf16_shared_expert(
142        &self,
143        input: DevicePtr,
144        num_tokens: u32,
145        hidden_size: u32,
146        shared_intermediate: u32,
147        gate_out: DevicePtr,
148        up_out: DevicePtr,
149        down_out: DevicePtr,
150        ctx: &ForwardContext,
151        stream: u64,
152    ) -> Result<bool> {
153        let Some(shared) = self.bf16_shared_expert else {
154            return Ok(false);
155        };
156        anyhow::ensure!(
157            num_tokens > 0 && shared_intermediate > 0,
158            "BF16 shared expert requires non-zero token and intermediate dimensions"
159        );
160
161        let project = |activation: DevicePtr,
162                       weight: &DenseWeight,
163                       output: DevicePtr,
164                       n: u32,
165                       k: u32|
166         -> Result<()> {
167            if num_tokens == 1 {
168                ops::dense_gemv(
169                    ctx.gpu,
170                    self.dense_gemv,
171                    activation,
172                    weight,
173                    output,
174                    n,
175                    k,
176                    stream,
177                )
178            } else if ctx.dispatch.cublas_gemm {
179                // Multi-token shared expert: cuBLASLt BF16 beats the hand-written
180                // mma.sync GEMM. The single-token arm above stays on the GEMV —
181                // decode-sized shapes do not repay cuBLAS heuristic overhead.
182                ops::cublas_bf16_proj_dense(
183                    activation,
184                    weight.weight,
185                    output,
186                    num_tokens,
187                    n,
188                    k,
189                    stream,
190                )
191            } else {
192                ops::dense_gemm_prefill(
193                    ctx.gpu,
194                    self.dense_gemm,
195                    self.dense_gemm_pipelined,
196                    activation,
197                    weight,
198                    output,
199                    num_tokens,
200                    n,
201                    k,
202                    stream,
203                )
204            }
205        };
206
207        project(
208            input,
209            &shared.gate_proj,
210            gate_out,
211            shared_intermediate,
212            hidden_size,
213        )?;
214        project(
215            input,
216            &shared.up_proj,
217            up_out,
218            shared_intermediate,
219            hidden_size,
220        )?;
221        ops::silu_mul(
222            ctx.gpu,
223            self.moe_act_mul,
224            gate_out,
225            up_out,
226            gate_out,
227            num_tokens * shared_intermediate,
228            stream,
229        )?;
230        project(
231            gate_out,
232            &shared.down_proj,
233            down_out,
234            hidden_size,
235            shared_intermediate,
236        )?;
237        Ok(true)
238    }
239
240    /// Router gate GEMM for a BF16 (dense) gate weight at prefill:
241    /// `gate_logits[n, num_experts] = router_in @ gate^T`.
242    ///
243    /// PINNED to scalar-ORDER numerics, never a reassociating fast GEMM.
244    /// Router logits are selection inputs, not data: top-k reads them after a
245    /// BF16 store, where near-tied experts sit 1 ulp apart, so ANY change in
246    /// accumulation order flips selections on borderline tokens and the flip
247    /// compounds through every downstream layer. Rerouting this one GEMM to
248    /// `dense_gemm_bf16_pipelined` (mma.sync accumulation, "cosine=1.0" but
249    /// not bit-identical) moved BFCL on the FP8 MoE flagship from 86.55 to
250    /// 84.76 overall — deterministic, reproduced to the hundredth on two
251    /// trees (2026-08-12, 03a74eac19 / cb9f8ecab4) — while every dense-model
252    /// gate stayed inside noise. The routed-expert GEMMs tolerate order
253    /// changes; the router does not.
254    ///
255    /// `dense_gemm_bf16_router` satisfies the pin: it keeps the scalar
256    /// kernel's exact per-output FP32 fma chain in strict k = 0..K-1 order
257    /// (only blocking/vectorization differ) and is verified BIT-IDENTICAL to
258    /// `dense_gemm_bf16` under the production `--fmad=false` build (0
259    /// differing elements over [4510,2048]x[2048,256] and
260    /// [2255,2048]x[2048,256]) at ~2x the speed — the router GEMM is 40
261    /// layers x ~3.2 ms of cold prefill (the real payload of the mprof
262    /// "sort_by_expert" bucket), so the 2x is ~65 ms per 4.5k-token prefill.
263    /// Falls back to the scalar kernel when the variant is absent from a
264    /// model's gemm module.
265    #[allow(clippy::too_many_arguments)]
266    pub(super) fn router_gate_gemm_dense(
267        &self,
268        router_in: DevicePtr,
269        gate_logits: DevicePtr,
270        num_tokens: u32,
271        num_experts: u32,
272        hidden_size: u32,
273        ctx: &ForwardContext,
274        stream: u64,
275    ) -> Result<()> {
276        if self.dense_gemm_router.0 != 0 {
277            return ops::dense_gemm_router(
278                ctx.gpu,
279                self.dense_gemm_router,
280                router_in,
281                &self.weights.gate,
282                gate_logits,
283                num_tokens,
284                num_experts,
285                hidden_size,
286                stream,
287            );
288        }
289        ops::dense_gemm(
290            ctx.gpu,
291            self.dense_gemm,
292            router_in,
293            &self.weights.gate,
294            gate_logits,
295            num_tokens,
296            num_experts,
297            hidden_size,
298            stream,
299        )
300    }
301
302    /// Apply the router pre-normalization (Gemma-4 only) and return the
303    /// pointer that should be fed into the gate GEMV. If the MoE has no
304    /// router_pre_norm weight, this is a no-op and returns `input` unchanged.
305    ///
306    /// HF Gemma4TextRouter computes:
307    ///   router_input = rms_norm(x) * scale * hidden_size^(-0.5)
308    /// We fused `scale * root_size` into a single BF16 weight at load time
309    /// so the existing rms_norm kernel applies both steps in one pass.
310    ///
311    /// The normed output is written to `ctx.buffers.qkv_output()` which is
312    /// free at MoE time (the attention block already consumed qkv_output).
313    pub(super) fn router_input(
314        &self,
315        input: DevicePtr,
316        num_tokens: u32,
317        h: u32,
318        ctx: &ForwardContext,
319        stream: u64,
320    ) -> Result<DevicePtr> {
321        let Some(ref weight) = self.weights.router_pre_norm else {
322            return Ok(input);
323        };
324        let eps = ctx.config.rms_norm_eps as f32;
325        let normed = ctx.buffers.qkv_output();
326        ops::rms_norm(
327            ctx.gpu,
328            self.pre_expert_norm_k,
329            input,
330            weight,
331            normed,
332            num_tokens,
333            h,
334            eps,
335            stream,
336        )?;
337        Ok(normed)
338    }
339}