spark_model/weight_map/
loaders_mtp.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/// Slice a stacked + fused MTP MoE expert layout into per-expert
14/// `DenseExpertWeight`s via DevicePtr offsets (zero-copy).
15///
16/// Expects two BF16 tensors in `store`:
17///   `{mlp}.experts.gate_up_proj` shape `[E, 2*I, H]`
18///       — first `I` rows of axis 1 are gate, next `I` rows are up
19///   `{mlp}.experts.down_proj`    shape `[E, H, I]`
20///
21/// Each expert's `gate`, `up`, `down` are contiguous sub-tensors of the
22/// stacked allocations, so we hand back DenseWeights pointing into the
23/// same underlying GPU memory. The WeightStore retains ownership of the
24/// stacked allocations; the offset pointers are aliases and must NEVER
25/// be passed to `gpu.free()` (the loader doesn't, and ModelWeights drops
26/// the WeightStore last in any case).
27pub(super) fn load_mtp_experts_stacked(
28    store: &WeightStore,
29    mlp: &str,
30    num_experts: usize,
31) -> Result<Vec<DenseExpertWeight>> {
32    let gate_up = store.get(&format!("{mlp}.experts.gate_up_proj"))?;
33    let down = store.get(&format!("{mlp}.experts.down_proj"))?;
34
35    ensure!(
36        gate_up.shape.len() == 3,
37        "MTP stacked experts.gate_up_proj: expected 3D [E,2I,H], got {:?}",
38        gate_up.shape
39    );
40    ensure!(
41        down.shape.len() == 3,
42        "MTP stacked experts.down_proj: expected 3D [E,H,I], got {:?}",
43        down.shape
44    );
45    ensure!(
46        gate_up.shape[0] == num_experts,
47        "MTP stacked experts.gate_up_proj: expert dim {} != num_experts {num_experts}",
48        gate_up.shape[0]
49    );
50    ensure!(
51        down.shape[0] == num_experts,
52        "MTP stacked experts.down_proj: expert dim {} != num_experts {num_experts}",
53        down.shape[0]
54    );
55
56    let two_inter = gate_up.shape[1];
57    let hidden = gate_up.shape[2];
58    ensure!(
59        two_inter % 2 == 0,
60        "MTP stacked experts.gate_up_proj: 2nd dim must be even (gate+up fused), got {two_inter}"
61    );
62    let intermediate = two_inter / 2;
63
64    ensure!(
65        down.shape[1] == hidden,
66        "MTP stacked: gate_up_proj.hidden ({hidden}) != down_proj.hidden ({})",
67        down.shape[1]
68    );
69    ensure!(
70        down.shape[2] == intermediate,
71        "MTP stacked: down_proj.intermediate ({}) != gate_up_proj/2 ({intermediate})",
72        down.shape[2]
73    );
74
75    // Stacked tensors must be BF16 — the per-expert split path also returns
76    // BF16 (norm/gate dense or dequanted projections), so we keep the
77    // contract uniform downstream.
78    ensure!(
79        matches!(gate_up.dtype, WeightDtype::BF16),
80        "MTP stacked experts.gate_up_proj: expected BF16, got {:?}",
81        gate_up.dtype
82    );
83    ensure!(
84        matches!(down.dtype, WeightDtype::BF16),
85        "MTP stacked experts.down_proj: expected BF16, got {:?}",
86        down.dtype
87    );
88
89    let elt = WeightDtype::BF16.byte_size();
90    let half_bytes = intermediate * hidden * elt;
91    let gate_up_stride = two_inter * hidden * elt;
92    let down_stride = hidden * intermediate * elt;
93
94    let mut experts = Vec::with_capacity(num_experts);
95    for e in 0..num_experts {
96        let base_gu = gate_up.ptr.offset(e * gate_up_stride);
97        experts.push(DenseExpertWeight {
98            gate_proj: DenseWeight { weight: base_gu },
99            up_proj: DenseWeight {
100                weight: base_gu.offset(half_bytes),
101            },
102            down_proj: DenseWeight {
103                weight: down.ptr.offset(e * down_stride),
104            },
105        });
106    }
107    Ok(experts)
108}
109
110// ── Qwen3.5-MoE weight loaders ──
111// Two NVFP4 naming conventions exist:
112//   Standard (nvidia/txn545):  weight, weight_scale, weight_scale_2, input_scale
113//   Sehyo (compressed-tensors): weight_packed, weight_scale, weight_global_scale, input_global_scale
114// Additionally, Sehyo quantizes attention/SSM projections; standard keeps them BF16.
115
116/// Weight quantization variant (on-disk format).
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum Nvfp4Variant {
119    /// Standard ModelOpt: weight, weight_scale, weight_scale_2, input_scale.
120    /// Attention/SSM projections are BF16 dense.
121    Standard,
122    /// Sehyo/compressed-tensors: weight_packed, weight_global_scale, input_global_scale.
123    /// Attention/SSM projections are NVFP4 quantized.
124    CompressedTensors,
125    /// FP8 block-scaled (e.g. Qwen/Qwen3.5-35B-A3B-FP8, Qwen/Qwen3.6-35B-A3B-FP8):
126    /// weight (float8_e4m3fn) + weight_scale_inv (BF16 per-`[128,128]`-block).
127    ///
128    /// Loaded **NATIVELY as FP8** in Qwen3 and Qwen3.5/3.6 model families.
129    /// Attention uses `w8a16_gemv` (decode) + `w8a16_gemm` (prefill).
130    /// MoE uses the FP8 fused grouped-GEMM batch1/2/3 path.
131    /// SSM uses `w8a16_gemv` decode + `fp8_gemm_n128` prefill (single-scale).
132    /// No silent FP8→BF16→NVFP4 triple-conversion.
133    ///
134    /// Historical note: the variant name retains "Dequanted" because the
135    /// `Bf16Raw` cousin and the pre-2026-05-24 NVFP4 detour did dequant on
136    /// load. The dispatch tables in `qwen35/load_layers.rs` (`LayerType::
137    /// FullAttention if native_fp8`) and `qwen3.rs` (line 176) now branch
138    /// to native FP8 paths when `quant_format == QuantFormat::Fp8`.
139    Fp8Dequanted,
140    /// Raw BF16/FP16 fine-tunes (e.g. samuelcardillo/Carnice-MoE-35B-A3B):
141    /// only `.weight` tensors exist (no quantization metadata). Runtime-quantize
142    /// from BF16 to NVFP4 at load time. Quality is suboptimal vs. a
143    /// pre-calibrated NVFP4 release — the user gets a warning at startup.
144    Bf16Raw,
145}