spark_model/weight_map/
ssm_qwen35.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#[path = "ssm_qwen35/dequant_fp8.rs"]
14mod dequant_fp8;
15use dequant_fp8::dequant_fp8_block_slice_bf16;
16
17/// Qwen3.5 SSM weights with separate projections.
18pub struct SsmWeightsQwen35 {
19    /// QKV projection: [qkv_size, hidden_size] BF16 (Q+K+V, no Z).
20    pub in_proj_qkv: DenseWeight,
21    /// Z gate projection: [z_size, hidden_size] BF16.
22    pub in_proj_z: DenseWeight,
23    /// Alpha projection: [num_value_heads, hidden_size] BF16.
24    pub in_proj_a: DenseWeight,
25    /// Beta projection: [num_value_heads, hidden_size] BF16.
26    pub in_proj_b: DenseWeight,
27    /// Conv1d weight: [d_inner, 1, d_conv] BF16.
28    pub conv1d: DenseWeight,
29    /// A_log parameter: `[num_v_heads]` FP32.
30    pub a_log: DenseWeight,
31    /// dt_bias parameter: `[num_v_heads]` FP32.
32    pub dt_bias: DenseWeight,
33    /// Gate norm weight: `[value_dim]` BF16.
34    pub norm: DenseWeight,
35    /// Output projection: [value_dim, hidden_size] BF16 (NOT NVFP4 — quantizer skipped these).
36    pub out_proj: DenseWeight,
37}
38
39/// Load SSM weights for Qwen3.5 (separate projections, BF16 out_proj).
40pub(crate) fn load_ssm_qwen35(
41    store: &WeightStore,
42    layer_prefix: &str,
43    gpu: &dyn GpuBackend,
44    // Kept for loader-dispatch signature parity; `dense_auto` routes by the
45    // projection's actual on-disk dtype rather than the model-wide variant.
46    _variant: Nvfp4Variant,
47) -> Result<SsmWeightsQwen35> {
48    let p = format!("{layer_prefix}.linear_attn");
49
50    // Per-projection load by on-disk dtype. FP8 (Holo, block-scaled) and BF16
51    // (AEON, in modules_to_not_convert) ship plain `.weight` → `dense_auto`.
52    // The sakamakismile AgentWorld re-quant instead quantizes the GDN/SSM
53    // projections to NVFP4 (`weight_packed`); dequant those to BF16, with dims
54    // inferred from the packed shape (rows = packed[0], cols = packed[1] * 2,
55    // since E2M1 packs 2 values/byte). Mirrors the dense loader's `load_ssm_proj`
56    // (qwen35_dense.rs) — without this the NVFP4 SSM projections fail with
57    // "linear_attn.in_proj_qkv.weight not found in store".
58    let load_proj = |prefix: &str| -> Result<DenseWeight> {
59        if store.contains(&format!("{prefix}.weight_packed")) {
60            let shape = store.get(&format!("{prefix}.weight_packed"))?.shape.clone();
61            dequant_nvfp4_to_bf16(store, prefix, shape[0], shape[1] * 2, gpu)
62        } else {
63            dense_auto(store, &format!("{prefix}.weight"), gpu)
64        }
65    };
66
67    Ok(SsmWeightsQwen35 {
68        in_proj_qkv: load_proj(&format!("{p}.in_proj_qkv"))?,
69        in_proj_z: load_proj(&format!("{p}.in_proj_z"))?,
70        in_proj_a: load_proj(&format!("{p}.in_proj_a"))?,
71        in_proj_b: load_proj(&format!("{p}.in_proj_b"))?,
72        conv1d: dense_auto(store, &format!("{p}.conv1d.weight"), gpu)?,
73        // A_log and dt_bias MUST be FP32 — BF16 precision causes exponential
74        // error amplification in the GDR decay gate at 8k+ tokens.
75        a_log: dense_keep_f32(store, &format!("{p}.A_log"), gpu)?,
76        dt_bias: dense_keep_f32(store, &format!("{p}.dt_bias"), gpu)?,
77        // norm.weight is safe as BF16 (no recurrent amplification)
78        norm: dense_f32_safe(store, &format!("{p}.norm.weight"), gpu)?,
79        out_proj: load_proj(&format!("{p}.out_proj"))?,
80    })
81}
82
83/// Load MoE weights for Qwen3.5, auto-selecting NVFP4 naming convention.
84///
85/// Under EP (ep_world_size > 1), only local experts are loaded from the store.
86/// Remote experts get NULL pointers — kernels detect NULL and write zero output.
87/// `skip_routed_experts`: when true, routed experts get NULL weights (saves memory
88/// when native FP8 MoE dispatch handles them). Shared expert is always loaded.
89pub(crate) fn load_moe_qwen35(
90    store: &WeightStore,
91    layer_prefix: &str,
92    num_experts: usize,
93    gpu: &dyn GpuBackend,
94    config: &atlas_core::config::ModelConfig,
95    variant: Nvfp4Variant,
96    absmax_k: spark_runtime::gpu::KernelHandle,
97    quantize_k: spark_runtime::gpu::KernelHandle,
98    stream: u64,
99    skip_routed_experts: bool,
100) -> Result<MoeWeights> {
101    let p = format!("{layer_prefix}.mlp");
102
103    let gate = dense_auto(store, &format!("{p}.gate.weight"), gpu)?;
104    let shared_expert_gate = dense_auto(store, &format!("{p}.shared_expert_gate.weight"), gpu)?;
105
106    let inter = config.moe_intermediate_size;
107    let h = config.hidden_size;
108
109    let qctx = QuantizeCtx {
110        absmax_k,
111        quantize_k,
112        stream,
113    };
114
115    // Qwen3.6-35B-A3B BF16 release ships a FUSED MoE layout: one
116    // `experts.gate_up_proj: [num_experts, 2*inter, hidden]` and one
117    // `experts.down_proj: [num_experts, hidden, inter]` per layer. Slice
118    // each expert at load time and runtime-quantize to NVFP4.
119    let fused_gate_up_key = format!("{p}.experts.gate_up_proj");
120    let fused_down_key = format!("{p}.experts.down_proj");
121    // FUSED expert layout: one `experts.gate_up_proj [E, 2*inter, h]` + one
122    // `experts.down_proj [E, h, inter]` per layer, sliced per expert at load and
123    // runtime-quantized to NVFP4. Two on-disk dtypes occur in the wild:
124    //   - BF16 (Qwen3.6-35B-A3B BF16 release) → slice and quantize directly.
125    //   - FP8E4M3 block-scaled (lovedheart AgentWorld-35B FP8: routed experts
126    //     fused-FP8 with `*_scale_inv`, while attention/SSM/shared are BF16) →
127    //     dequant each slice FP8→BF16 (reusing dequant_fp8_blockscaled_bf16)
128    //     then quantize to NVFP4. Equivalent to the proven NVFP4 expert decode
129    //     path (cf. ATLAS_FORCE_NVFP4_MOE), so no native-FP8 fused-shared kernel
130    //     contract is involved. Detection is dtype-based, not variant-based, so
131    //     it also covers a fused-BF16 layer inside a globally-FP8 checkpoint.
132    let is_fused = store.contains(&fused_gate_up_key) && store.contains(&fused_down_key);
133    let fused_is_fp8 = is_fused
134        && store
135            .get(&fused_gate_up_key)
136            .map(|w| w.dtype == WeightDtype::FP8E4M3)
137            .unwrap_or(false);
138
139    let load_expert_fused = |expert_idx: usize| -> Result<ExpertWeight> {
140        let fused_gu = store.get(&fused_gate_up_key)?;
141        let fused_d = store.get(&fused_down_key)?;
142        if fused_is_fp8 {
143            // gate_up: [E, 2*inter, h] FP8 + gate_up_proj_scale_inv [E, sn, sk]
144            // down:    [E, h, inter] FP8   + down_proj_scale_inv    [E, sn, sk]
145            let gu_s = store.get(&format!("{fused_gate_up_key}_scale_inv"))?;
146            let d_s = store.get(&format!("{fused_down_key}_scale_inv"))?;
147            let (gu_sn, gu_sk) = (gu_s.shape[1], gu_s.shape[2]);
148            let (d_sn, d_sk) = (d_s.shape[1], d_s.shape[2]);
149            let gu_s_f32 = gu_s.dtype == WeightDtype::FP32;
150            let d_s_f32 = d_s.dtype == WeightDtype::FP32;
151            let gu_w_stride = 2 * inter * h; // FP8 = 1 byte/element
152            let d_w_stride = h * inter;
153            let gu_s_elem = if gu_s_f32 { 4 } else { 2 };
154            let d_s_elem = if d_s_f32 { 4 } else { 2 };
155            let gu_s_stride = gu_sn * gu_sk * gu_s_elem;
156            let d_s_stride = d_sn * d_sk * d_s_elem;
157            // Dequant the whole gate_up[e] [2*inter, h] FP8 → BF16, then slice
158            // gate (rows 0..inter) and up (rows inter..2*inter).
159            let gu_bf16 = dequant_fp8_block_slice_bf16(
160                gpu,
161                fused_gu.ptr.offset(expert_idx * gu_w_stride),
162                gu_s.ptr.offset(expert_idx * gu_s_stride),
163                2 * inter,
164                h,
165                gu_sn,
166                gu_sk,
167                gu_s_f32,
168            )?;
169            let down_bf16 = dequant_fp8_block_slice_bf16(
170                gpu,
171                fused_d.ptr.offset(expert_idx * d_w_stride),
172                d_s.ptr.offset(expert_idx * d_s_stride),
173                h,
174                inter,
175                d_sn,
176                d_sk,
177                d_s_f32,
178            )?;
179            let gate_dw = DenseWeight { weight: gu_bf16 };
180            let up_dw = DenseWeight {
181                weight: gu_bf16.offset(inter * h * 2), // BF16 = 2 bytes
182            };
183            let down_dw = DenseWeight { weight: down_bf16 };
184            let out = ExpertWeight {
185                gate_proj: quantize_to_nvfp4(
186                    &gate_dw, inter, h, gpu, absmax_k, quantize_k, stream,
187                )?,
188                up_proj: quantize_to_nvfp4(&up_dw, inter, h, gpu, absmax_k, quantize_k, stream)?,
189                down_proj: quantize_to_nvfp4(
190                    &down_dw, h, inter, gpu, absmax_k, quantize_k, stream,
191                )?,
192            };
193            gpu.free(gu_bf16)?;
194            gpu.free(down_bf16)?;
195            Ok(out)
196        } else {
197            // BF16 fused: slice and quantize directly.
198            let bf16 = 2usize;
199            let gu_per_expert_bytes = 2 * inter * h * bf16;
200            let d_per_expert_bytes = h * inter * bf16;
201            let gate_off = expert_idx * gu_per_expert_bytes;
202            let up_off = gate_off + inter * h * bf16;
203            let down_off = expert_idx * d_per_expert_bytes;
204            let gate_dw = DenseWeight {
205                weight: fused_gu.ptr.offset(gate_off),
206            };
207            let up_dw = DenseWeight {
208                weight: fused_gu.ptr.offset(up_off),
209            };
210            let down_dw = DenseWeight {
211                weight: fused_d.ptr.offset(down_off),
212            };
213            Ok(ExpertWeight {
214                gate_proj: quantize_to_nvfp4(
215                    &gate_dw, inter, h, gpu, absmax_k, quantize_k, stream,
216                )?,
217                up_proj: quantize_to_nvfp4(&up_dw, inter, h, gpu, absmax_k, quantize_k, stream)?,
218                down_proj: quantize_to_nvfp4(
219                    &down_dw, h, inter, gpu, absmax_k, quantize_k, stream,
220                )?,
221            })
222        }
223    };
224
225    // Route every projection through `quantized_any` so the per-tensor BF16
226    // fallback applies uniformly to shared and routed experts. Hybrid MoE
227    // checkpoints (AgentWorld-35B, Qwen3.5-397B) ship the shared expert — and
228    // occasionally individual routed experts — as unquantized BF16 even when
229    // the model is globally FP8/NVFP4. Dispatching on the global `variant`
230    // alone sent those tensors down the FP8/NVFP4 arm and failed with
231    // "weight_scale_inv not found" before the fallback could catch them.
232    let load_expert = |prefix: &str| -> Result<ExpertWeight> {
233        Ok(ExpertWeight {
234            gate_proj: quantized_any(
235                store,
236                &format!("{prefix}.gate_proj"),
237                inter,
238                h,
239                gpu,
240                variant,
241                qctx,
242            )?,
243            up_proj: quantized_any(
244                store,
245                &format!("{prefix}.up_proj"),
246                inter,
247                h,
248                gpu,
249                variant,
250                qctx,
251            )?,
252            down_proj: quantized_any(
253                store,
254                &format!("{prefix}.down_proj"),
255                h,
256                inter,
257                gpu,
258                variant,
259                qctx,
260            )?,
261        })
262    };
263
264    let shared_expert = load_expert(&format!("{p}.shared_expert"))?;
265
266    let mut experts = Vec::with_capacity(num_experts);
267    for e in 0..num_experts {
268        if skip_routed_experts || !config.is_local_expert(e) {
269            experts.push(ExpertWeight::null());
270        } else if is_fused {
271            experts.push(load_expert_fused(e)?);
272        } else {
273            experts.push(load_expert(&format!("{p}.experts.{e}"))?);
274        }
275    }
276
277    // Fused layout (BF16 or FP8 source) shares ONE `experts.gate_up_proj` +
278    // `experts.down_proj` tensor across all experts (sliced by offset), so it
279    // can't be freed per-expert like the separate path. Free the shared source
280    // here now that every expert has been quantized to NVFP4 — #200 only frees
281    // the per-slice dequant intermediates, not these originals, so this is
282    // additive (no double-free). Drops the redundant ~60GB so only the NVFP4
283    // copies remain resident.
284    if is_fused {
285        if let Ok(w) = store.get(&fused_gate_up_key) {
286            let _ = gpu.free(w.ptr);
287        }
288        if let Ok(w) = store.get(&fused_down_key) {
289            let _ = gpu.free(w.ptr);
290        }
291    }
292
293    Ok(MoeWeights {
294        gate,
295        shared_expert,
296        shared_expert_gate,
297        experts,
298        router_pre_norm: None,
299        correction_bias: None,
300    })
301}
302
303/// Load MoE experts as native FP8 weights (no NVFP4 conversion).
304///
305/// Returns the standard MoeWeights (with NVFP4 gate/shared for compatibility)
306/// PLUS a Vec of Fp8ExpertWeight for native FP8 dispatch.
307pub(crate) fn load_moe_qwen35_fp8_experts(
308    store: &WeightStore,
309    layer_prefix: &str,
310    num_experts: usize,
311    gpu: &dyn GpuBackend,
312    config: &atlas_core::config::ModelConfig,
313) -> Result<Vec<Fp8ExpertWeight>> {
314    let p = format!("{layer_prefix}.mlp");
315    let mut fp8_experts = Vec::with_capacity(num_experts);
316
317    for e in 0..num_experts {
318        if config.is_local_expert(e) {
319            let ep = format!("{p}.experts.{e}");
320            fp8_experts.push(Fp8ExpertWeight {
321                gate_proj: load_fp8_block_scaled_as_fp8weight(
322                    store,
323                    &format!("{ep}.gate_proj"),
324                    gpu,
325                )?,
326                up_proj: load_fp8_block_scaled_as_fp8weight(store, &format!("{ep}.up_proj"), gpu)?,
327                down_proj: load_fp8_block_scaled_as_fp8weight(
328                    store,
329                    &format!("{ep}.down_proj"),
330                    gpu,
331                )?,
332            });
333        } else {
334            // Remote-expert placeholder: NULL pointers never dereferenced.
335            // `Fp8BlockScaled` chosen as the format tag because that's the
336            // dominant disk format for Qwen FP8 checkpoints — keeps the
337            // tag consistent with what the routed expert would carry if
338            // it weren't remote.
339            let null_block = Fp8Weight {
340                weight: DevicePtr::NULL,
341                row_scale: DevicePtr::NULL,
342                n: 0,
343                k: 0,
344                scale_format: WeightQuantFormat::Fp8BlockScaled,
345            };
346            fp8_experts.push(Fp8ExpertWeight {
347                gate_proj: null_block,
348                up_proj: null_block,
349                down_proj: null_block,
350            });
351        }
352    }
353
354    // Also load shared expert as FP8
355    let shared_prefix = format!("{p}.shared_expert");
356    let _shared_fp8 = Fp8ExpertWeight {
357        gate_proj: load_fp8_block_scaled_as_fp8weight(
358            store,
359            &format!("{shared_prefix}.gate_proj"),
360            gpu,
361        )?,
362        up_proj: load_fp8_block_scaled_as_fp8weight(
363            store,
364            &format!("{shared_prefix}.up_proj"),
365            gpu,
366        )?,
367        down_proj: load_fp8_block_scaled_as_fp8weight(
368            store,
369            &format!("{shared_prefix}.down_proj"),
370            gpu,
371        )?,
372    };
373
374    Ok(fp8_experts)
375}
376
377/// Load MoE weights for models without shared experts (e.g. Qwen3-VL).
378///
379/// Creates zero-filled dummy shared expert weights so the fused MoE kernels
380/// (which always launch top_k+1 blocks) produce zero contribution from the
381/// shared expert slot. `weight_scale_2 = 0.0` ensures dequant → 0.
382pub(crate) fn load_moe_no_shared(
383    store: &WeightStore,
384    layer_prefix: &str,
385    num_experts: usize,
386    gpu: &dyn GpuBackend,
387    config: &atlas_core::config::ModelConfig,
388    variant: Nvfp4Variant,
389) -> Result<MoeWeights> {
390    let p = format!("{layer_prefix}.mlp");
391
392    let gate = dense(store, &format!("{p}.gate.weight"))?;
393
394    // Allocate correctly-sized zero-filled GPU buffers for dummy shared expert.
395    // The fused kernel always runs a shared expert block (blockIdx.y == top_k),
396    // which reads full expert-sized weight matrices. Buffers must match real
397    // expert dimensions or the kernel will read out of bounds (CUDA error 900).
398    // weight_scale_2 = 0.0 ensures dequant → 0 regardless of packed contents.
399    let h = config.hidden_size;
400    let inter = config.moe_intermediate_size;
401    let group_size = 16usize; // NVFP4 quantization group size (matches kernel GROUP_SIZE)
402
403    // gate_proj/up_proj: [inter, h] → packed = inter * h / 2, scale = inter * (h / group_size)
404    let gu_packed_bytes = inter * h / 2;
405    let gu_scale_bytes = inter * (h / group_size);
406    // down_proj: [h, inter] → packed = h * inter / 2, scale = h * (inter / group_size)
407    let d_packed_bytes = h * inter / 2;
408    let d_scale_bytes = h * (inter / group_size);
409
410    let alloc_zero = |size: usize| -> Result<DevicePtr> {
411        let ptr = gpu.alloc(size)?;
412        gpu.memset(ptr, 0, size)?;
413        Ok(ptr)
414    };
415
416    let mk_zero_quant = |packed_sz: usize, scale_sz: usize| -> Result<QuantizedWeight> {
417        Ok(QuantizedWeight {
418            weight: alloc_zero(packed_sz)?,
419            weight_scale: alloc_zero(scale_sz)?,
420            weight_scale_2: 0.0,
421            input_scale: DevicePtr::NULL,
422            weight_scale_2_vec: DevicePtr::NULL,
423        })
424    };
425
426    let shared_expert = ExpertWeight {
427        gate_proj: mk_zero_quant(gu_packed_bytes, gu_scale_bytes)?,
428        up_proj: mk_zero_quant(gu_packed_bytes, gu_scale_bytes)?,
429        down_proj: mk_zero_quant(d_packed_bytes, d_scale_bytes)?,
430    };
431    // Gate weight for shared expert: zero BF16 [hidden_size] → sigmoid(0)=0.5.
432    // Doesn't matter since shared_out is all zeros (0.5 * 0 = 0).
433    let shared_expert_gate = DenseWeight {
434        weight: alloc_zero(h * 2)?,
435    };
436
437    let mut experts = Vec::with_capacity(num_experts);
438    for e in 0..num_experts {
439        if config.is_local_expert(e) {
440            experts.push(ExpertWeight {
441                gate_proj: quantized_auto(
442                    store,
443                    &format!("{p}.experts.{e}.gate_proj"),
444                    gpu,
445                    variant,
446                )?,
447                up_proj: quantized_auto(store, &format!("{p}.experts.{e}.up_proj"), gpu, variant)?,
448                down_proj: quantized_auto(
449                    store,
450                    &format!("{p}.experts.{e}.down_proj"),
451                    gpu,
452                    variant,
453                )?,
454            });
455        } else {
456            experts.push(ExpertWeight::null());
457        }
458    }
459
460    Ok(MoeWeights {
461        gate,
462        shared_expert,
463        shared_expert_gate,
464        experts,
465        router_pre_norm: None,
466        correction_bias: None,
467    })
468}