spark_model/weight_map/
moe.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Auto-extracted from `weight_map.rs` during refactor wave 4a.
4
5#![allow(unused_imports)]
6
7use anyhow::{Context, Result, bail, ensure};
8use spark_runtime::gpu::{DevicePtr, GpuBackend};
9use spark_runtime::weights::{WeightDtype, WeightStore};
10
11use super::*;
12
13/// MoE layer weights.
14pub struct MoeWeights {
15    /// Router gate: [hidden_size, num_experts] BF16.
16    pub gate: DenseWeight,
17    /// Shared expert (always active).
18    pub shared_expert: ExpertWeight,
19    /// Shared expert gate sigmoid weight: `[1]` BF16.
20    pub shared_expert_gate: DenseWeight,
21    /// Per-expert weights: 512 experts.
22    pub experts: Vec<ExpertWeight>,
23    /// Optional router pre-normalization weight.
24    /// Set for Gemma-4 MoE where the HF reference applies a pure RMSNorm to
25    /// the router input followed by a per-dim scale multiplication:
26    ///   `router_input = rms_norm(x) * scale * hidden_size^(-0.5)`
27    /// Stored as a BF16 `[hidden_size]` vector containing `scale * root_size`
28    /// so the existing rms_norm kernel (`output = x/rms(x) * weight`) applies
29    /// both steps in one pass. `None` for models that feed the router from
30    /// the raw post-attention residual.
31    pub router_pre_norm: Option<DenseWeight>,
32    /// Optional expert correction bias: `[num_experts]` F32.
33    ///
34    /// Set for models using the DeepSeek-V3 / MiniMax-M2 loss-free-balancing
35    /// routing trick: the bias is added to sigmoid(gate_logits) *only* for
36    /// top-k selection; gathered dispatch weights come from the unbiased
37    /// sigmoid scores. Consumed by `moe_topk_sigmoid` kernel via its `bias`
38    /// argument.
39    ///
40    /// `None` for softmax-routed Qwen/Gemma MoE. Nemotron-H carries its own
41    /// bias in `NemotronMoeWeights::e_score_correction_bias` because its
42    /// MoE is a separate layer type (Mamba-2 interleaved) — those paths
43    /// don't touch this struct.
44    pub correction_bias: Option<DenseWeight>,
45}
46
47impl MoeWeights {
48    /// Create empty MoeWeights for testing (all null pointers).
49    #[cfg(test)]
50    pub fn empty(num_experts: usize) -> Self {
51        let null_dense = DenseWeight {
52            weight: DevicePtr::NULL,
53        };
54        let null_quant = QuantizedWeight {
55            weight: DevicePtr::NULL,
56            weight_scale: DevicePtr::NULL,
57            weight_scale_2: 1.0,
58            input_scale: DevicePtr::NULL,
59            weight_scale_2_vec: DevicePtr::NULL,
60        };
61        let null_expert = ExpertWeight {
62            gate_proj: null_quant,
63            up_proj: null_quant,
64            down_proj: null_quant,
65        };
66        Self {
67            gate: null_dense,
68            shared_expert: null_expert,
69            shared_expert_gate: null_dense,
70            experts: vec![null_expert; num_experts],
71            router_pre_norm: None,
72            correction_bias: None,
73        }
74    }
75}
76
77/// All weights for one transformer layer.
78pub enum LayerWeights {
79    FullAttention {
80        input_norm: DenseWeight,
81        attn: AttentionWeights,
82        post_attn_norm: DenseWeight,
83        moe: MoeWeights,
84    },
85    LinearAttention {
86        input_norm: DenseWeight,
87        ssm: SsmWeights,
88        post_attn_norm: DenseWeight,
89        moe: MoeWeights,
90    },
91}
92
93/// BF16 expert weight (before NVFP4 quantization).
94#[derive(Debug, Clone, Copy)]
95pub struct DenseExpertWeight {
96    pub gate_proj: DenseWeight,
97    pub up_proj: DenseWeight,
98    pub down_proj: DenseWeight,
99}
100
101/// MTP (Multi-Token Prediction) head weights (all BF16 from safetensors).
102///
103/// Single decoder layer + concat projection. All projection weights are BF16
104/// and get quantized to NVFP4 at load time by the weight loader.
105pub struct MtpWeights {
106    /// RMSNorm on token embedding before concat: `[hidden_size]` BF16.
107    pub pre_fc_norm_embedding: DenseWeight,
108    /// RMSNorm on target hidden state before concat: `[hidden_size]` BF16.
109    pub pre_fc_norm_hidden: DenseWeight,
110    /// Concat projection: `[hidden_size, 2*hidden_size]` BF16.
111    pub fc: DenseWeight,
112    /// Input layernorm for the attention layer: `[hidden_size]` BF16.
113    pub input_layernorm: DenseWeight,
114    /// Attention projections (all BF16).
115    pub q_proj: DenseWeight,
116    pub k_proj: DenseWeight,
117    pub v_proj: DenseWeight,
118    pub o_proj: DenseWeight,
119    pub q_norm: DenseWeight,
120    pub k_norm: DenseWeight,
121    /// Post-attention layernorm: `[hidden_size]` BF16.
122    pub post_attn_layernorm: DenseWeight,
123    /// MoE router gate: [num_experts, hidden_size] BF16.
124    /// NULL when `dense_ffn` is `Some` (dense FFN MTP head).
125    pub moe_gate: DenseWeight,
126    /// Shared expert (BF16). NULL fields when `dense_ffn` is `Some`.
127    pub shared_expert: DenseExpertWeight,
128    /// Shared expert gate: [1, hidden_size] BF16.
129    /// NULL when `dense_ffn` is `Some`.
130    pub shared_expert_gate: DenseWeight,
131    /// Per-expert weights (512 experts, BF16).
132    /// Empty when `dense_ffn` is `Some`.
133    pub experts: Vec<DenseExpertWeight>,
134    /// Dense FFN triple (`gate_proj`, `up_proj`, `down_proj`) — used by MTP
135    /// heads bundled with dense (non-MoE) FP8 checkpoints, e.g.
136    /// `Qwen/Qwen3.6-27B-FP8`. When `Some`, the MoE fields above are unused
137    /// and the forward path takes the dense MLP shortcut.
138    pub dense_ffn: Option<DenseExpertWeight>,
139    /// Final output RMSNorm: `[hidden_size]` BF16.
140    pub norm: DenseWeight,
141}