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}