spark_model/weight_map/
expert.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/// FP8 expert weight: gate/up/down projections as FP8 block-scaled weights.
14///
15/// Used for native FP8 expert dispatch (no NVFP4 dequant overhead).
16/// Each projection stores [N, K] FP8 E4M3 weights + block scales.
17#[derive(Debug, Clone, Copy)]
18pub struct Fp8ExpertWeight {
19    pub gate_proj: Fp8Weight,
20    pub up_proj: Fp8Weight,
21    pub down_proj: Fp8Weight,
22}
23
24/// Full attention layer weights (12 layers in Qwen3-Next).
25#[derive(Debug, Clone, Copy)]
26pub struct AttentionWeights {
27    /// Q projection: [hidden_size, num_heads * head_dim] BF16.
28    pub q_proj: DenseWeight,
29    /// K projection: [hidden_size, num_kv_heads * head_dim] BF16.
30    pub k_proj: DenseWeight,
31    /// V projection: [hidden_size, num_kv_heads * head_dim] BF16.
32    pub v_proj: DenseWeight,
33    /// O projection: [num_heads * head_dim, hidden_size] NVFP4.
34    pub o_proj: QuantizedWeight,
35    /// Q RMS norm weight: `[head_dim]` BF16 (per-head Qwen3-family convention).
36    pub q_norm: DenseWeight,
37    /// K RMS norm weight: `[head_dim]` BF16 (per-head Qwen3-family convention).
38    pub k_norm: DenseWeight,
39    /// MiniMax-style full-hidden Q RMSNorm weight: [num_heads * head_dim] BF16.
40    ///
41    /// Set to `Some(..)` for models that apply RMSNorm over the concatenated
42    /// Q projection output (MiniMax M2) before the view-into-heads and before
43    /// RoPE. Mathematically different from the per-head `q_norm` above
44    /// (MiniMax normalizes by the global hidden-dim RMS; Qwen3 normalizes
45    /// per-head). Attention forward branches on `.is_some()` to pick which
46    /// pre-RoPE norm to apply. Default `None` keeps all existing models on
47    /// the per-head `q_norm` path — behavior-preserving for every non-
48    /// MiniMax loader.
49    pub q_norm_full: Option<DenseWeight>,
50    /// MiniMax-style full-hidden K RMSNorm weight: [num_kv_heads * head_dim] BF16.
51    pub k_norm_full: Option<DenseWeight>,
52    /// K scale for FP8 KV cache.
53    pub k_scale: f32,
54    /// V scale for FP8 KV cache.
55    pub v_scale: f32,
56}
57
58/// Linear attention (SSM / Gated Delta Net) layer weights (36 layers).
59#[derive(Debug, Clone, Copy)]
60pub struct SsmWeights {
61    /// QKVZ projection: [hidden_size, qkvz_size] BF16.
62    pub in_proj_qkvz: DenseWeight,
63    /// Beta-Alpha projection: [hidden_size, ba_size] BF16.
64    pub in_proj_ba: DenseWeight,
65    /// Conv1d weight: [d_inner, 1, d_conv] BF16.
66    pub conv1d: DenseWeight,
67    /// A_log parameter: `[num_v_heads]` FP32.
68    pub a_log: DenseWeight,
69    /// dt_bias parameter: `[num_v_heads]` FP32.
70    pub dt_bias: DenseWeight,
71    /// Gate norm weight: `[hidden_size]` BF16.
72    pub norm: DenseWeight,
73    /// Output projection: [hidden_size, hidden_size] NVFP4.
74    pub out_proj: QuantizedWeight,
75}
76
77/// MoE expert weights (shared across all 512 experts per layer).
78#[derive(Debug, Clone, Copy)]
79pub struct ExpertWeight {
80    pub gate_proj: QuantizedWeight,
81    pub up_proj: QuantizedWeight,
82    pub down_proj: QuantizedWeight,
83}
84
85impl ExpertWeight {
86    /// Null expert (all pointers NULL). Used for remote experts under EP.
87    /// Kernels detect NULL pointers and write zero output for these experts.
88    pub fn null() -> Self {
89        Self {
90            gate_proj: QuantizedWeight::null(),
91            up_proj: QuantizedWeight::null(),
92            down_proj: QuantizedWeight::null(),
93        }
94    }
95}
96
97// ── Unified quantization types ──────────────────────────────────────
98//
99// These enums abstract over different quantization formats (NVFP4, FP8,
100// BF16 dense) so that layer dispatch code uses a single type instead of
101// cascading if/else chains checking multiple Optional fields.
102//
103// Adding a new quantization format requires:
104//   1. Add a variant to QuantWeight
105//   2. Add match arms in quant_gemv/quant_gemm (ops.rs)
106//   3. Implement the weight loader for the new format
107
108/// Quantized weight for any supported format.
109///
110/// Encapsulates all data a GEMV/GEMM kernel needs to dequantize and compute.
111/// The forward path matches on this enum to select the correct kernel.
112/// Enum branch compiles to ~1 cycle vs GPU kernel launch at ~5000 cycles.
113#[derive(Debug, Clone, Copy)]
114pub enum QuantWeight {
115    /// NVFP4 E2M1: packed nibbles + FP8 group scales + f32 global scale.
116    /// Kernel: w4a16_gemv (decode) / w4a16_gemm (prefill)
117    Nvfp4(QuantizedWeight),
118
119    /// FP8 E4M3: byte-packed weights + per-block BF16 scales.
120    /// Kernel: w8a16_gemv (decode) / w8a16_gemm (prefill)
121    Fp8(Fp8Weight),
122
123    /// BF16 dense (unquantized). Kernel: dense_gemv / dense_gemm
124    Dense(DenseWeight),
125
126    /// Keep-packed ternary Q2_0 (`ATLAS_GGUF_NATIVE_Q2`): raw `block_q2_0` bytes,
127    /// 2-bit resident. Decode dispatches `q2_0_gemv_vec`; prefill transient-
128    /// dequants to BF16 then runs `dense_gemm`. Tier-1c attention path.
129    PackedQ2(PackedQ2Weight),
130}
131
132impl QuantWeight {
133    /// Null weight (for remote experts under EP or unused projections).
134    pub fn null() -> Self {
135        Self::Nvfp4(QuantizedWeight::null())
136    }
137
138    /// Whether this weight points to NULL (placeholder).
139    pub fn is_null(&self) -> bool {
140        match self {
141            Self::Nvfp4(w) => w.is_null(),
142            Self::Fp8(w) => w.weight.is_null(),
143            Self::Dense(w) => w.weight.is_null(),
144            Self::PackedQ2(w) => w.is_null(),
145        }
146    }
147
148    /// Extract as keep-packed Q2_0, if this weight is that variant.
149    pub fn as_packed_q2(&self) -> Option<&PackedQ2Weight> {
150        match self {
151            Self::PackedQ2(w) => Some(w),
152            _ => None,
153        }
154    }
155
156    /// Extract as NVFP4, if this weight is that variant.
157    pub fn as_nvfp4(&self) -> Option<&QuantizedWeight> {
158        match self {
159            Self::Nvfp4(w) => Some(w),
160            _ => None,
161        }
162    }
163
164    /// Extract as FP8, if this weight is that variant.
165    pub fn as_fp8(&self) -> Option<&Fp8Weight> {
166        match self {
167            Self::Fp8(w) => Some(w),
168            _ => None,
169        }
170    }
171
172    /// Extract as Dense, if this weight is that variant.
173    pub fn as_dense(&self) -> Option<&DenseWeight> {
174        match self {
175            Self::Dense(w) => Some(w),
176            _ => None,
177        }
178    }
179}
180
181impl From<QuantizedWeight> for QuantWeight {
182    fn from(w: QuantizedWeight) -> Self {
183        Self::Nvfp4(w)
184    }
185}
186
187impl From<Fp8Weight> for QuantWeight {
188    fn from(w: Fp8Weight) -> Self {
189        Self::Fp8(w)
190    }
191}
192
193impl From<DenseWeight> for QuantWeight {
194    fn from(w: DenseWeight) -> Self {
195        Self::Dense(w)
196    }
197}
198
199impl From<PackedQ2Weight> for QuantWeight {
200    fn from(w: PackedQ2Weight) -> Self {
201        Self::PackedQ2(w)
202    }
203}
204
205/// Per-expert weights in any supported quant format.
206///
207/// Replaces the separate `ExpertWeight` (NVFP4) and `Fp8ExpertWeight` (FP8)
208/// types with a single unified type.
209#[derive(Debug, Clone, Copy)]
210pub struct QuantExpertWeight {
211    pub gate_proj: QuantWeight,
212    pub up_proj: QuantWeight,
213    pub down_proj: QuantWeight,
214}
215
216impl QuantExpertWeight {
217    pub fn null() -> Self {
218        Self {
219            gate_proj: QuantWeight::null(),
220            up_proj: QuantWeight::null(),
221            down_proj: QuantWeight::null(),
222        }
223    }
224}
225
226impl From<ExpertWeight> for QuantExpertWeight {
227    fn from(w: ExpertWeight) -> Self {
228        Self {
229            gate_proj: QuantWeight::Nvfp4(w.gate_proj),
230            up_proj: QuantWeight::Nvfp4(w.up_proj),
231            down_proj: QuantWeight::Nvfp4(w.down_proj),
232        }
233    }
234}
235
236impl From<Fp8ExpertWeight> for QuantExpertWeight {
237    fn from(w: Fp8ExpertWeight) -> Self {
238        Self {
239            gate_proj: QuantWeight::Fp8(w.gate_proj),
240            up_proj: QuantWeight::Fp8(w.up_proj),
241            down_proj: QuantWeight::Fp8(w.down_proj),
242        }
243    }
244}