spark_model/weight_map/
nvfp4_detect.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/// Step (1) of [`detect_nvfp4_variant`]: the variant the CONFIG declares,
14/// asked WITHOUT a [`WeightStore`].
15///
16/// Factored out (#915, 2026-09-11) so a caller that has only `config.json` —
17/// the pre-load residency prediction in
18/// `weight_loader::predicted_residency`, which runs before the store
19/// exists — asks exactly the question the loader will later answer instead
20/// of keeping a second copy of this precedence. `None` means "the config
21/// does not say"; it is NEVER a guess, and the sniffing half of
22/// [`detect_nvfp4_variant`] is what resolves it once the store is loaded.
23pub fn config_declared_variant(config: &atlas_core::config::ModelConfig) -> Option<Nvfp4Variant> {
24    let qc = config.quantization_config.as_ref()?;
25    match qc.quant_method.as_str() {
26        "modelopt" if qc.quant_algo.eq_ignore_ascii_case("NVFP4") => Some(Nvfp4Variant::Standard),
27        "modelopt" if qc.quant_algo.eq_ignore_ascii_case("FP8") => Some(Nvfp4Variant::Fp8Dequanted),
28        "compressed-tensors" => {
29            // `format` is the sub-selector here. Block-scaled FP8 is tagged
30            // either with a literal "fp8" OR with compressed-tensors'
31            // `"float-quantized"` (8-bit float = FP8 E4M3, e.g.
32            // Hcompany/Holo-3.1-*-FP8); the rest ("nvfp4-pack-quantized",
33            // "pack-quantized") are NVFP4.
34            let fmt = qc.format.to_ascii_lowercase();
35            if fmt.contains("fp8") || fmt.contains("float-quant") {
36                Some(Nvfp4Variant::Fp8Dequanted)
37            } else {
38                Some(Nvfp4Variant::CompressedTensors)
39            }
40        }
41        "fp8" => Some(Nvfp4Variant::Fp8Dequanted),
42        // Unknown method with non-empty ignore list — the caller falls
43        // through to heuristic detection. A warning was already emitted by
44        // `quant_format::detect_quant_format`.
45        _ => None,
46    }
47}
48
49/// Detect the weight quantization variant from the weight store.
50///
51/// Dispatch order matches vLLM / TRT-LLM / SGLang:
52///   1. **Config-declared scheme** (`config.quantization_config.quant_method`)
53///      wins outright. This is the authoritative signal and the only one
54///      that correctly handles checkpoints with an `ignore` list (e.g.
55///      `lukealonso/MiniMax-M2.7-NVFP4`, whose MLP `gate_proj` is
56///      intentionally unquantized and therefore has no `.weight_scale`
57///      tensor — sniffing would mis-detect the whole checkpoint as
58///      `Bf16Raw` and then read uint8-packed FP4 as BF16, which is the
59///      4× byte overrun that surfaces as `CUDA_ERROR_ILLEGAL_ADDRESS`
60///      ten seconds into load).
61///   2. **Tensor-name sniffing** for the many checkpoints in the wild
62///      that ship without a `quantization_config` block.
63pub fn detect_nvfp4_variant(
64    store: &WeightStore,
65    config: &atlas_core::config::ModelConfig,
66) -> Nvfp4Variant {
67    // (1) Config-first dispatch. See module docs on `quant_format` for
68    // the full rationale — this is the fix for the Discord 2026-04-17
69    // `CUDA_ERROR_ILLEGAL_ADDRESS` bug. `None` = the config does not declare
70    // one (or declares a method this engine does not know), which is the only
71    // case that falls through to sniffing.
72    if let Some(declared) = config_declared_variant(config) {
73        return declared;
74    }
75
76    let lp = config.layer_prefix(0);
77
78    // Check MoE expert key first (most models are MoE).
79    let local_expert = config.local_expert_range().0;
80    let moe_sehyo_key = format!("{lp}.mlp.experts.{local_expert}.gate_proj.weight_packed");
81    if store.contains(&moe_sehyo_key) {
82        return Nvfp4Variant::CompressedTensors;
83    }
84
85    // Check dense FFN key (non-MoE models like Qwen3.5-27B).
86    let dense_sehyo_key = format!("{lp}.mlp.gate_proj.weight_packed");
87    if store.contains(&dense_sehyo_key) {
88        return Nvfp4Variant::CompressedTensors;
89    }
90
91    // Mistral uses "layers.{i}.experts.{e}.w1" naming (no "model." prefix, no ".mlp.").
92    let mistral_key = format!("layers.0.experts.{local_expert}.w1.weight_packed");
93    if store.contains(&mistral_key) {
94        return Nvfp4Variant::CompressedTensors;
95    }
96
97    // Fallback: scan any tensor name for `.weight_packed` suffix.
98    // Catches compressed-tensors checkpoints with unexpected naming conventions.
99    if store.names().any(|k| k.ends_with(".weight_packed")) {
100        return Nvfp4Variant::CompressedTensors;
101    }
102
103    // Check for FP8 block-scaled weights (e.g. Qwen/Qwen3.5-35B-A3B-FP8):
104    // FP8 models have `weight_scale_inv` alongside FP8E4M3 weights.
105    //
106    // Two SPELLINGS of the same layer, not two layers. The second entry used to
107    // be indexed by `local_expert_range().0` — an EXPERT index used as a LAYER
108    // index. `local_expert_range` returns global expert ids (see its sibling
109    // `is_local_expert`), so on a single node or EP rank 0 it is 0 and layer 0
110    // is probed by accident; on rank >= 1 it is `ep_rank * num_experts /
111    // ep_world_size` and each rank probes a DIFFERENT layer. With 256 experts
112    // over 2 ranks, rank 1 probed layer 39 — a full-attention layer whose FP8
113    // `q_proj` trips the attention sniff below — so two ranks loading one
114    // checkpoint could disagree about its quantisation variant.
115    //
116    // Detection must not depend on EP rank: every rank sees the same file.
117    const ALT_LAYER0_PREFIX: &str = "model.language_model.layers.0";
118    let prefixes_to_check = [lp.clone(), ALT_LAYER0_PREFIX.to_string()];
119    for pfx in &prefixes_to_check {
120        let fp8_key = format!("{pfx}.mlp.experts.{local_expert}.gate_proj.weight_scale_inv");
121        if store.contains(&fp8_key) {
122            return Nvfp4Variant::Fp8Dequanted;
123        }
124        let fp8_dense_key = format!("{pfx}.mlp.gate_proj.weight_scale_inv");
125        if store.contains(&fp8_dense_key) {
126            return Nvfp4Variant::Fp8Dequanted;
127        }
128        let fp8_attn_key = format!("{pfx}.self_attn.q_proj.weight_scale_inv");
129        if store.contains(&fp8_attn_key) {
130            return Nvfp4Variant::Fp8Dequanted;
131        }
132        // compressed-tensors `float-quantized` FP8 (e.g. Hcompany/Holo-3.1-*-FP8)
133        // ships block-FP8 as an FP8E4M3 `.weight` + 2D `.weight_scale` — NO
134        // `.weight_packed` (that's NVFP4) and NO `.weight_scale_inv` (that's
135        // DeepSeek/Qwen-native FP8). The `.weight_scale` name alias-collides
136        // with compressed-tensors NVFP4, so the `.weight_scale` checks below
137        // would misroute it to an NVFP4 variant. Disambiguate by the
138        // unambiguous FP8E4M3 weight dtype: an FP8E4M3 projection weight is
139        // always block-FP8 (Fp8Dequanted; the FP8→BF16→NVFP4 requant path in
140        // `quantized_from_fp8` reads the 2D `.weight_scale`).
141        for key in [
142            format!("{pfx}.mlp.experts.{local_expert}.gate_proj.weight"),
143            format!("{pfx}.mlp.gate_proj.weight"),
144            format!("{pfx}.self_attn.q_proj.weight"),
145        ] {
146            if store
147                .get(&key)
148                .map(|w| w.dtype == WeightDtype::FP8E4M3)
149                .unwrap_or(false)
150            {
151                return Nvfp4Variant::Fp8Dequanted;
152            }
153        }
154    }
155    // Fallback: scan any tensor name for `.weight_scale_inv` suffix.
156    // Catches FP8 checkpoints where the layer prefix hasn't been resolved yet.
157    if store.names().any(|k| k.ends_with(".weight_scale_inv")) {
158        return Nvfp4Variant::Fp8Dequanted;
159    }
160
161    // BF16/FP16 fine-tune detection: no quantization markers at all.
162    // If even `.weight_scale` is absent (i.e., not a Standard NVFP4 model
163    // either), fall through to runtime quantization from raw BF16/FP16.
164    // Catches third-party fine-tunes like samuelcardillo/Carnice-MoE-35B-A3B
165    // that ship only `.weight` tensors with no per-channel scales.
166    let any_standard_scale = store.names().any(|k| k.ends_with(".weight_scale"));
167    if !any_standard_scale {
168        tracing::warn!(
169            "No NVFP4/FP8 quantization metadata found (no .weight_packed / .weight_scale_inv / .weight_scale). \
170             Falling back to runtime BF16→NVFP4 quantization. Quality will be inferior to a calibrated NVFP4 release."
171        );
172        return Nvfp4Variant::Bf16Raw;
173    }
174
175    // Partial-NVFP4 guard: some upstream checkpoints (notably google/gemma-4-26B-A4B-it)
176    // ship `.weight_scale` on KV-cache scale tensors but NOT on the MLP/MoE
177    // projections Atlas actually consumes. If we claim Standard here the
178    // loader will then fail with a cryptic `Weight '...mlp.gate_proj.weight_scale'
179    // not found in store` half-way through load (logged against #bugs 2026-04-15
180    // by kiiv6565). Sniff the canonical L0 MLP gate_proj — if its `.weight_scale`
181    // is missing, the right answer is BF16 runtime quantization, not Standard.
182    let has_mlp_scale = {
183        let k_dense = format!("{lp}.mlp.gate_proj.weight_scale");
184        let k_moe = format!("{lp}.mlp.experts.{local_expert}.gate_proj.weight_scale");
185        store.contains(&k_dense) || store.contains(&k_moe)
186    };
187    if !has_mlp_scale {
188        tracing::warn!(
189            "Partial NVFP4 metadata: `.weight_scale` exists for some tensors (e.g. KV scales) \
190             but not for MLP/MoE projections. Falling back to runtime BF16→NVFP4 quantization. \
191             For best quality use a fully-quantized NVFP4 release (e.g. Sehyo/*-NVFP4)."
192        );
193        return Nvfp4Variant::Bf16Raw;
194    }
195
196    Nvfp4Variant::Standard
197}
198
199/// Load a quantized weight using the appropriate naming convention.
200///
201/// For `Fp8Dequanted`, requires `quant_ctx` (absmax_k, quantize_k, stream)
202/// to runtime-quantize the dequanted BF16 to NVFP4.
203pub(crate) fn quantized_auto(
204    store: &WeightStore,
205    prefix: &str,
206    gpu: &dyn GpuBackend,
207    variant: Nvfp4Variant,
208) -> Result<QuantizedWeight> {
209    match variant {
210        Nvfp4Variant::Standard => quantized(store, prefix, gpu),
211        Nvfp4Variant::CompressedTensors => quantized_v2(store, prefix, gpu),
212        Nvfp4Variant::Fp8Dequanted => {
213            unreachable!("Fp8Dequanted must use quantized_auto_fp8 with quant context")
214        }
215        Nvfp4Variant::Bf16Raw => {
216            unreachable!("Bf16Raw must use quantized_any with quant context")
217        }
218    }
219}
220
221/// Quantize context for FP8→BF16→NVFP4 runtime conversion.
222#[derive(Clone, Copy)]
223pub(crate) struct QuantizeCtx {
224    pub absmax_k: spark_runtime::gpu::KernelHandle,
225    pub quantize_k: spark_runtime::gpu::KernelHandle,
226    pub stream: u64,
227}
228
229/// Load a quantized weight, dispatching by variant. Handles all three on-disk formats
230/// including FP8 block-scaled (requires dimensions for FP8→BF16→NVFP4 conversion).
231pub(crate) fn quantized_any(
232    store: &WeightStore,
233    prefix: &str,
234    n: usize,
235    k: usize,
236    gpu: &dyn GpuBackend,
237    variant: Nvfp4Variant,
238    qctx: QuantizeCtx,
239) -> Result<QuantizedWeight> {
240    let _t_detect = std::time::Instant::now();
241    // Per-key fallback (B8 #bugs RedHatAI/Qwen3-Coder-Next-NVFP4): some
242    // models that are CompressedTensors overall keep certain projections
243    // (e.g. `linear_attn.out_proj`) as raw BF16 with no quantization
244    // metadata. Detect that case here and runtime-quantize, instead of
245    // failing the whole load with "weight_global_scale not found".
246    let has_packed = store.contains(&format!("{prefix}.weight_packed"));
247    let has_scale = store.contains(&format!("{prefix}.weight_scale"));
248    let has_scale_inv = store.contains(&format!("{prefix}.weight_scale_inv"));
249    let has_only_dense =
250        !has_packed && !has_scale && !has_scale_inv && store.contains(&format!("{prefix}.weight"));
251
252    // Per-key fallback #2 (unsloth/Qwen3.6-{27B,35B-A3B}-NVFP4, re-quantized
253    // 2026-07-10): mixed-precision checkpoints that are NVFP4 for most of the
254    // net but leave a tail of layers — and, in the MoE, the shared experts —
255    // as FP8 E4M3 with a per-row `weight_scale` ([N,1] BF16). Those keys carry
256    // no NVFP4 metadata at all (no `weight_packed`, no `weight_global_scale`,
257    // no `weight_scale_2`), so the declared NVFP4 variant cannot load them and
258    // the whole model dies on `weight_global_scale not found in store`.
259    // Detect the FP8 layout per key and dequant→runtime-quantize instead.
260    //
261    // The three NVFP4 layouts are all excluded by construction, so this can
262    // never steal a key that IS NVFP4:
263    //   Standard (ModelOpt/nvidia) -> has `weight_scale_2`
264    //   CompressedTensors (Sehyo)  -> has `weight_packed` + `weight_global_scale`
265    //   this FP8 case              -> has neither, and `.weight` is FP8E4M3
266    let has_fp8_dense = !has_packed
267        && !store.contains(&format!("{prefix}.weight_global_scale"))
268        && !store.contains(&format!("{prefix}.weight_scale_2"))
269        && (has_scale || has_scale_inv)
270        && store
271            .get(&format!("{prefix}.weight"))
272            .map(|w| w.dtype == WeightDtype::FP8E4M3)
273            .unwrap_or(false);
274
275    let effective_variant = if has_only_dense && !matches!(variant, Nvfp4Variant::Bf16Raw) {
276        tracing::debug!("{prefix}: no quantization metadata; falling back to runtime BF16→NVFP4");
277        Nvfp4Variant::Bf16Raw
278    } else if has_fp8_dense
279        && !matches!(variant, Nvfp4Variant::Fp8Dequanted | Nvfp4Variant::Bf16Raw)
280    {
281        tracing::debug!("{prefix}: FP8 key in an NVFP4 checkpoint; dequant FP8→BF16→NVFP4");
282        Nvfp4Variant::Fp8Dequanted
283    } else {
284        variant
285    };
286
287    let _t_detect_ns = _t_detect.elapsed().as_nanos() as u64;
288    match effective_variant {
289        Nvfp4Variant::Standard => quantized(store, prefix, gpu),
290        Nvfp4Variant::CompressedTensors => quantized_v2(store, prefix, gpu),
291        Nvfp4Variant::Fp8Dequanted => quantized_from_fp8(
292            store,
293            prefix,
294            n,
295            k,
296            gpu,
297            qctx.absmax_k,
298            qctx.quantize_k,
299            qctx.stream,
300        ),
301        Nvfp4Variant::Bf16Raw => {
302            use std::sync::atomic::{AtomicU64, Ordering};
303            static T_DETECT: AtomicU64 = AtomicU64::new(0);
304            static T_GET: AtomicU64 = AtomicU64::new(0);
305            static T_QUANT: AtomicU64 = AtomicU64::new(0);
306            static T_FREE: AtomicU64 = AtomicU64::new(0);
307            static N: AtomicU64 = AtomicU64::new(0);
308            T_DETECT.fetch_add(_t_detect_ns, Ordering::Relaxed);
309            // Raw BF16/FP16 fine-tune: load the dense weight then runtime-quantize.
310            let _t = std::time::Instant::now();
311            let w = store.get(&format!("{prefix}.weight"))?;
312            let bf16 = DenseWeight { weight: w.ptr };
313            T_GET.fetch_add(_t.elapsed().as_nanos() as u64, Ordering::Relaxed);
314            let _t = std::time::Instant::now();
315            let q = quantize_to_nvfp4(
316                &bf16,
317                n,
318                k,
319                gpu,
320                qctx.absmax_k,
321                qctx.quantize_k,
322                qctx.stream,
323            )?;
324            T_QUANT.fetch_add(_t.elapsed().as_nanos() as u64, Ordering::Relaxed);
325            let _t = std::time::Instant::now();
326            // Free the BF16 source: the NVFP4 buffer is a fresh allocation, so the
327            // on-disk BF16 weight is now redundant. Without this a 35B BF16 MoE
328            // (Bf16Raw, SEPARATE per-expert layout routed through here by #200's
329            // `quantized_any`) holds BOTH the ~60GB BF16 experts AND the ~22GB
330            // NVFP4 copies → ~109GB pre-KV, no room for KV. Safe + mirrors
331            // `quantized_from_fp8` which frees its BF16 intermediate the same way.
332            gpu.free(w.ptr)?;
333            T_FREE.fetch_add(_t.elapsed().as_nanos() as u64, Ordering::Relaxed);
334            let c = N.fetch_add(1, Ordering::Relaxed) + 1;
335            if c.is_multiple_of(512) {
336                let ms = |a: &AtomicU64| a.load(Ordering::Relaxed) as f64 / 1.0e6;
337                tracing::info!(
338                    "quantized_any(Bf16Raw) PROFILE after {c} calls (ms total): detect={:.1} \
339                     store_get={:.1} quantize={:.1} free={:.1} | sum={:.1} per_call={:.3}ms",
340                    ms(&T_DETECT),
341                    ms(&T_GET),
342                    ms(&T_QUANT),
343                    ms(&T_FREE),
344                    ms(&T_DETECT) + ms(&T_GET) + ms(&T_QUANT) + ms(&T_FREE),
345                    (ms(&T_DETECT) + ms(&T_GET) + ms(&T_QUANT) + ms(&T_FREE)) / c as f64,
346                );
347            }
348            Ok(q)
349        }
350    }
351}
352
353/// Load a quantized weight from FP8 block-scaled data: FP8→BF16→NVFP4.
354///
355/// `n` and `k` are the logical weight dimensions (e.g. [inter, hidden] for gate_proj).
356pub(crate) fn quantized_from_fp8(
357    store: &WeightStore,
358    prefix: &str,
359    n: usize,
360    k: usize,
361    gpu: &dyn GpuBackend,
362    absmax_k: spark_runtime::gpu::KernelHandle,
363    quantize_k: spark_runtime::gpu::KernelHandle,
364    stream: u64,
365) -> Result<QuantizedWeight> {
366    let bf16 = dequant_fp8_blockscaled_to_bf16(store, prefix, gpu)?;
367    let result = quantize_to_nvfp4(&bf16, n, k, gpu, absmax_k, quantize_k, stream)?;
368    // Free the BF16 intermediate — only the NVFP4 result is needed.
369    gpu.free(bf16.weight)?;
370    Ok(result)
371}
372
373/// Load FP8 block-scaled weight as BF16 dense (no NVFP4 re-quantization).
374///
375/// Use this when the runtime NVFP4 quantization produces degenerate weights
376/// (e.g., FP8 checkpoints where double-quantization degrades quality).
377/// The weight stays in BF16 and uses `dense_gemv`/`dense_gemm` kernels.
378#[allow(dead_code)]
379pub(crate) fn dense_from_fp8(
380    store: &WeightStore,
381    prefix: &str,
382    gpu: &dyn GpuBackend,
383) -> Result<DenseWeight> {
384    dequant_fp8_blockscaled_to_bf16(store, prefix, gpu)
385}
386
387/// Load full attention weights for Qwen3.5 (all Q/K/V/O are NVFP4 on disk).
388#[allow(dead_code)]
389pub(crate) fn load_attention_qwen35(
390    store: &WeightStore,
391    layer_prefix: &str,
392    gpu: &dyn GpuBackend,
393) -> Result<AttentionWeights> {
394    let p = format!("{layer_prefix}.self_attn");
395    let (k_scale, v_scale) = load_kv_scales(store, &p, gpu);
396    Ok(AttentionWeights {
397        // Q/K/V are NVFP4 quantized — load packed, return as dense (the weight_packed data)
398        // The weight_loader will handle creating QuantizedWeight from these
399        q_proj: dense(store, &format!("{p}.q_proj.weight_packed"))?,
400        k_proj: dense(store, &format!("{p}.k_proj.weight_packed"))?,
401        v_proj: dense(store, &format!("{p}.v_proj.weight_packed"))?,
402        o_proj: quantized_v2(store, &format!("{p}.o_proj"), gpu)?,
403        q_norm: dense(store, &format!("{p}.q_norm.weight"))?,
404        k_norm: dense(store, &format!("{p}.k_norm.weight"))?,
405        q_norm_full: None,
406        k_norm_full: None,
407        k_scale,
408        v_scale,
409    })
410}
411
412/// Load NVFP4 quantized projection for Qwen3.5 full attention layer.
413#[allow(dead_code)]
414pub(crate) fn load_quantized_proj_qwen35(
415    store: &WeightStore,
416    prefix: &str,
417    gpu: &dyn GpuBackend,
418) -> Result<QuantizedWeight> {
419    quantized_v2(store, prefix, gpu)
420}
421
422#[cfg(test)]
423mod ep_detection_tests {
424    use super::*;
425    use atlas_core::config::ModelConfig;
426    use spark_runtime::weights::WeightStore;
427
428    /// A store holding only the FP8 attention marker at a given layer, which is
429    /// what the detector sniffs for. Names are all the detector reads.
430    fn store_with(names: &[String]) -> WeightStore {
431        use std::collections::HashMap;
432        let map: HashMap<String, spark_runtime::weights::WeightTensor> = names
433            .iter()
434            .map(|n| {
435                (
436                    n.clone(),
437                    spark_runtime::weights::WeightTensor {
438                        ptr: spark_runtime::gpu::DevicePtr::NULL,
439                        shape: vec![1],
440                        dtype: spark_runtime::weights::WeightDtype::FP8E4M3,
441                    },
442                )
443            })
444            .collect();
445        WeightStore::from_map(map)
446    }
447
448    #[test]
449    fn alternate_layer0_fp8_dtype_is_detected_on_every_ep_rank() {
450        let mut cfg = ModelConfig::qwen3_next_80b_nvfp4();
451        cfg.quantization_config = None;
452        let store =
453            store_with(&["model.language_model.layers.0.self_attn.q_proj.weight".to_string()]);
454
455        cfg.ep_world_size = 2;
456        for ep_rank in 0..2 {
457            cfg.ep_rank = ep_rank;
458            assert_eq!(
459                detect_nvfp4_variant(&store, &cfg),
460                Nvfp4Variant::Fp8Dequanted,
461                "EP rank {ep_rank} must inspect the same layer-zero checkpoint marker"
462            );
463        }
464    }
465
466    #[test]
467    fn scale_inv_suffix_fallback_detects_an_unexpected_prefix() {
468        let mut cfg = ModelConfig::qwen3_next_80b_nvfp4();
469        cfg.quantization_config = None;
470        let store =
471            store_with(&["third_party.transformer.blocks.17.attn.q.weight_scale_inv".to_string()]);
472        assert_eq!(
473            detect_nvfp4_variant(&store, &cfg),
474            Nvfp4Variant::Fp8Dequanted
475        );
476    }
477}