spark_model/quant_format/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Weight-quantization format abstraction.
4//!
5//! Atlas must load quantized checkpoints produced by several toolchains,
6//! each of which serializes the same fundamental numeric format (e.g. NVFP4)
7//! with a different tensor-name convention. Historically we sniffed those
8//! names at load time via `detect_nvfp4_variant` in `weight_map.rs`, but
9//! community re-quants that advertise their scheme in `quantization_config`
10//! (HF standard) and keep some modules unquantized via an `ignore` list
11//! (`lukealonso/MiniMax-M2.7-NVFP4`) broke that heuristic: the detector
12//! saw MLP gates without `.weight_scale` and returned `Bf16Raw`, which
13//! then read uint8-packed FP4 as BF16 — a 4× byte overrun that surfaced
14//! as `CUDA_ERROR_ILLEGAL_ADDRESS` ten seconds into model construction
15//! (reported on Discord 2026-04-17 by `energyburns`, `henryous`).
16//!
17//! The mitigation is to match vLLM / TensorRT-LLM / SGLang: **prefer the
18//! `quantization_config` signal, fall back to tensor-name sniffing only
19//! when it is absent**. This module formalizes that with a trait plus
20//! one implementation per supported serialization layout:
21//!
22//!   * [`CompressedTensorsFormat`] — Neural Magic `llm-compressor`
23//!     (`weight_packed` + `weight_global_scale` + `input_global_scale`)
24//!   * [`ModeloptFormat`] — NVIDIA TensorRT ModelOpt
25//!     (`weight` + `weight_scale` + `weight_scale_2` + `input_scale`)
26//!   * [`Fp8BlockScaledFormat`] — FP8 E4M3 with `weight_scale_inv`
27//!
28//! [`detect_quant_format`] is the single entry point. It inspects
29//! `config.quantization_config` first and only falls back to a heuristic
30//! on the weight store when the config is silent (emitting a warning,
31//! since a silent fallback is precisely what caused the original bug).
32
33use atlas_core::config::ModelConfig;
34use spark_runtime::weights::WeightStore;
35
36use crate::weight_map::Nvfp4Variant;
37
38mod compressed_tensors;
39mod fp8_blockscaled;
40mod modelopt;
41
42pub use compressed_tensors::CompressedTensorsFormat;
43pub use fp8_blockscaled::Fp8BlockScaledFormat;
44pub use modelopt::ModeloptFormat;
45
46/// A serialization layout for quantized weights.
47///
48/// Implementations describe a **loading policy** for a checkpoint: which
49/// tensor names to read, which dtypes to expect, and which module paths
50/// should stay BF16 (the ignore list). The heavy per-linear loading code
51/// remains in `weight_map.rs`; this trait is a thin dispatch wrapper that
52/// selects the right variant while honoring per-module overrides.
53pub trait QuantFormat: Send + Sync + std::fmt::Debug {
54    /// Human-readable name for logs (`"modelopt"`, `"compressed-tensors"`,
55    /// `"fp8-blockscaled"`).
56    fn name(&self) -> &'static str;
57
58    /// The [`Nvfp4Variant`] that this format maps to in the existing
59    /// `weight_map.rs` dispatch. Allows the trait to co-exist with the
60    /// legacy variant-based call sites during incremental migration.
61    fn base_variant(&self) -> Nvfp4Variant;
62
63    /// Is `module_path` in the format's ignore list (should be loaded
64    /// as dense BF16 rather than quantized)? `module_path` is the tensor
65    /// name with the trailing `.weight_scale_2` / `.weight_packed` /
66    /// etc. stripped — i.e. the `prefix` passed to
67    /// `weight_map::quantized_any`.
68    fn is_ignored(&self, module_path: &str) -> bool;
69
70    /// Effective variant for a specific module: the base variant, or
71    /// `Bf16Raw` if the module is in the ignore list. Loaders should
72    /// consult this instead of `base_variant` when loading per-module.
73    fn variant_for(&self, module_path: &str) -> Nvfp4Variant {
74        if self.is_ignored(module_path) {
75            Nvfp4Variant::Bf16Raw
76        } else {
77            self.base_variant()
78        }
79    }
80}
81
82/// Pick the right [`QuantFormat`] for a checkpoint.
83///
84/// Decision order:
85///   1. If `config.quantization_config` is present, use its declared
86///      `quant_method` / `quant_algo` — this is the authoritative signal
87///      every other inference stack (vLLM, TRT-LLM, SGLang) keys off.
88///   2. Otherwise scan the weight store for the convention actually on
89///      disk (legacy path, preserved for the many checkpoints in the
90///      wild that ship without a `quantization_config` block).
91///   3. If neither the config nor the store yields a recognized scheme,
92///      return a `ModeloptFormat` with empty ignore list but emit a
93///      `tracing::warn!`. This preserves existing behavior for the
94///      rarely-used pure-BF16 checkpoints while making the guess loud.
95pub fn detect_quant_format(config: &ModelConfig, store: &WeightStore) -> Box<dyn QuantFormat> {
96    // (1) Config-level dispatch — the path that fixes the
97    // `lukealonso/MiniMax-M2.7-NVFP4` bug.
98    if let Some(qc) = &config.quantization_config {
99        let method = qc.quant_method.as_str();
100        let algo = qc.quant_algo.as_str();
101        let format = qc.format.as_str();
102        let ignore = qc.ignore_modules.clone();
103
104        match method {
105            "modelopt" => {
106                tracing::info!(
107                    "QuantFormat: modelopt (algo={algo:?}), {} ignored module(s)",
108                    ignore.len(),
109                );
110                return Box::new(ModeloptFormat::new(algo.to_string(), ignore));
111            }
112            "compressed-tensors" => {
113                tracing::info!(
114                    "QuantFormat: compressed-tensors (format={format:?}), {} ignored module(s)",
115                    ignore.len(),
116                );
117                return Box::new(CompressedTensorsFormat::new(format.to_string(), ignore));
118            }
119            "fp8" => {
120                tracing::info!(
121                    "QuantFormat: fp8 (block-scaled), {} ignored module(s)",
122                    ignore.len(),
123                );
124                return Box::new(Fp8BlockScaledFormat::new(ignore));
125            }
126            other if !other.is_empty() => {
127                tracing::warn!(
128                    "QuantFormat: config declares unrecognized quant_method={other:?}; \
129                     falling back to tensor-name heuristic. Atlas currently understands \
130                     {{compressed-tensors, modelopt, fp8}}. Checkpoint load may fail."
131                );
132                // fall through
133            }
134            _ => {
135                // Empty method but non-empty ignore list — treat like
136                // heuristic detection (common for older configs).
137            }
138        }
139    }
140
141    // (2) Heuristic fallback. Reuse the existing detector to preserve
142    // every working checkpoint in Atlas's CI matrix; only the partial-
143    // metadata footgun is patched separately in `weight_map.rs`.
144    let variant = crate::weight_map::detect_nvfp4_variant(store, config);
145    let ignore = config
146        .quantization_config
147        .as_ref()
148        .map(|qc| qc.ignore_modules.clone())
149        .unwrap_or_default();
150    match variant {
151        Nvfp4Variant::CompressedTensors => {
152            tracing::info!("QuantFormat: compressed-tensors (detected from tensor names)");
153            Box::new(CompressedTensorsFormat::new(String::new(), ignore))
154        }
155        Nvfp4Variant::Fp8Dequanted => {
156            tracing::info!("QuantFormat: fp8-blockscaled (detected from tensor names)");
157            Box::new(Fp8BlockScaledFormat::new(ignore))
158        }
159        Nvfp4Variant::Standard => {
160            tracing::info!("QuantFormat: modelopt-style NVFP4 (detected from tensor names)");
161            Box::new(ModeloptFormat::new(String::new(), ignore))
162        }
163        Nvfp4Variant::Bf16Raw => {
164            // Pure BF16 / partial-metadata checkpoint. The ModelOpt
165            // impl with empty ignore list will route every call to
166            // `Bf16Raw` via `variant_for` — which is what we want.
167            tracing::warn!(
168                "QuantFormat: no quantization declared and no pre-quantized weights found; \
169                 treating checkpoint as BF16 raw (weights will be runtime-quantized). \
170                 Quality will be inferior to a calibrated NVFP4 release."
171            );
172            Box::new(ModeloptFormat::new(String::new(), ignore)) as Box<dyn QuantFormat>
173        }
174    }
175}
176
177/// HuggingFace-style glob match for module ignore-list entries.
178///
179/// Used by every [`QuantFormat::is_ignored`] impl so all three schemes
180/// share the same semantics. `*` matches any run of characters including
181/// empty; literal `.` matches `.`. Patterns without `*` must match
182/// exactly (prefix match would be too lax — `lm_head` must not match
183/// `lm_head_norm`). Patterns ending in `*` DO act as prefix matches,
184/// which is the dominant HF case (`model.layers.*.self_attn*`).
185pub(crate) fn module_matches_pattern(path: &str, pattern: &str) -> bool {
186    let segments: Vec<&str> = pattern.split('*').collect();
187    if segments.len() == 1 {
188        return path == pattern;
189    }
190    let mut rest = path;
191    // First segment anchors at the start unless the pattern begins with `*`.
192    let first = segments[0];
193    if !first.is_empty() {
194        if !rest.starts_with(first) {
195            return false;
196        }
197        rest = &rest[first.len()..];
198    }
199    // Intermediate segments must appear in order.
200    for seg in &segments[1..segments.len() - 1] {
201        if seg.is_empty() {
202            continue;
203        }
204        match rest.find(seg) {
205            Some(pos) => rest = &rest[pos + seg.len()..],
206            None => return false,
207        }
208    }
209    // Last segment: empty means pattern ended in `*` (any remainder OK);
210    // non-empty means the remainder must END with it.
211    let last = segments[segments.len() - 1];
212    last.is_empty() || rest.ends_with(last)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::{
218        CompressedTensorsFormat, Fp8BlockScaledFormat, ModeloptFormat, QuantFormat,
219        module_matches_pattern as m,
220    };
221    use crate::weight_map::Nvfp4Variant;
222
223    #[test]
224    fn exact_match() {
225        assert!(m("lm_head", "lm_head"));
226        assert!(!m("lm_head_norm", "lm_head"));
227    }
228
229    #[test]
230    fn prefix_star() {
231        assert!(m(
232            "model.layers.5.self_attn.q_proj",
233            "model.layers.*.self_attn*"
234        ));
235        assert!(m(
236            "model.layers.62.self_attn.out",
237            "model.layers.*.self_attn*"
238        ));
239        assert!(!m(
240            "model.layers.5.mlp.gate_proj",
241            "model.layers.*.self_attn*"
242        ));
243    }
244
245    #[test]
246    fn trailing_star_matches_a_prefix() {
247        assert!(m("lm_head.weight", "lm_head*"));
248        assert!(!m("model.lm_head.weight", "lm_head*"));
249    }
250
251    #[test]
252    fn leading_and_all_star_patterns() {
253        assert!(m("model.layers.7.lm_head.weight", "*.lm_head.weight"));
254        assert!(!m("model.layers.7.lm_head.bias", "*.lm_head.weight"));
255        assert!(m("anything.at.all", "*"));
256    }
257
258    #[test]
259    fn middle_star() {
260        assert!(m("model.layers.0.mlp.gate", "model.layers.*.mlp.gate"));
261        assert!(!m("model.layers.0.attn.gate", "model.layers.*.mlp.gate"));
262        assert!(m("a.left.middle.right.z", "a.*.middle.*.z"));
263        assert!(!m("a.right.middle.left.z", "a.*.left.*.z"));
264    }
265
266    #[test]
267    fn ignore_patterns_drive_effective_variants_for_every_format() {
268        let formats: Vec<(Box<dyn QuantFormat>, Nvfp4Variant)> = vec![
269            (
270                Box::new(ModeloptFormat::new(
271                    "NVFP4".into(),
272                    vec!["model.layers.*.self_attn*".into()],
273                )),
274                Nvfp4Variant::Standard,
275            ),
276            (
277                Box::new(CompressedTensorsFormat::new(
278                    "nvfp4-pack-quantized".into(),
279                    vec!["model.layers.*.self_attn*".into()],
280                )),
281                Nvfp4Variant::CompressedTensors,
282            ),
283            (
284                Box::new(Fp8BlockScaledFormat::new(vec![
285                    "model.layers.*.self_attn*".into(),
286                ])),
287                Nvfp4Variant::Fp8Dequanted,
288            ),
289        ];
290        for (format, base) in formats {
291            assert_eq!(format.base_variant(), base);
292            assert_eq!(
293                format.variant_for("model.layers.5.self_attn.q_proj"),
294                Nvfp4Variant::Bf16Raw
295            );
296            assert_eq!(format.variant_for("model.layers.5.mlp.gate_proj"), base);
297        }
298    }
299}