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}