metrale_config/
precision_plan.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: The declared precision plan: what a checkpoint's `quantization_config`
4//! declares, per linear layer, for the weight AND the input activations.
5//!
6//! This is the one place that reads the precision half of `quantization_config`
7//! (compressed-tensors `config_groups`, ModelOpt `quantized_layers` / `quant_algo`, and the
8//! HF `fp8` block scheme). Loaders and kernel dispatch ask [`DeclaredPrecisionPlan::resolve`]
9//! for a module and run what it says by default. A lever may only go BELOW what the plan
10//! declares; running above it is not a silent default either.
11//!
12//! Owner: config (quantization).
13//! Invariants:
14//! - Pure: no environment, no I/O. A malformed block is an error, never a guess.
15//! - A module the plan does not cover (ignored, unmatched, or no quantization at all)
16//!   resolves to [`LayerPrecision::UNQUANTIZED`]: 16-bit weights and activations.
17
18use anyhow::Result;
19
20#[path = "precision_plan/parse.rs"]
21mod parse;
22#[path = "precision_plan/target.rs"]
23mod target;
24
25pub use target::{IgnoreDialect, Target};
26
27/// 2026-09-28: Element type of a quantized operand.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum NumKind {
30    /// 2026-09-28: A float format (E4M3 at 8 bits, E2M1 at 4 bits).
31    Float,
32    /// 2026-09-28: An integer format.
33    Int,
34}
35
36/// 2026-09-28: How many values share one scale.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Granularity {
39    /// 2026-09-28: One scale for the whole tensor.
40    Tensor,
41    /// 2026-09-28: One scale per output channel (weights).
42    Channel,
43    /// 2026-09-28: One scale per token (activations).
44    Token,
45    /// 2026-09-28: One scale per `n` consecutive values along K.
46    Group(u32),
47    /// 2026-09-28: One scale per `n` values plus a per-tensor global scale
48    /// (compressed-tensors `tensor_group`, ModelOpt NVFP4).
49    TensorGroup(u32),
50    /// 2026-09-28: One scale per `[rows, cols]` block (weights).
51    Block(u32, u32),
52    /// 2026-09-28: Not stated by the checkpoint.
53    Unstated,
54}
55
56/// 2026-09-28: When an operand's scales are computed.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ScaleTiming {
59    /// 2026-09-28: Calibrated and stored in the checkpoint.
60    Static,
61    /// 2026-09-28: Computed at run time.
62    Dynamic,
63    /// 2026-09-28: Local scales at run time under a stored global scale
64    /// (compressed-tensors `dynamic: "local"`).
65    Local,
66}
67
68/// 2026-09-28: One quantized operand as declared.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct Operand {
71    /// 2026-09-28: Float or int.
72    pub kind: NumKind,
73    /// 2026-09-28: Bits per element.
74    pub bits: u8,
75    /// 2026-09-28: Scale sharing.
76    pub granularity: Granularity,
77    /// 2026-09-28: Scale timing.
78    pub timing: ScaleTiming,
79}
80
81impl Operand {
82    /// 2026-09-28: NVFP4 as ModelOpt and compressed-tensors declare it: E2M1, group 16
83    /// under a per-tensor scale.
84    pub const NVFP4: Operand = Operand {
85        kind: NumKind::Float,
86        bits: 4,
87        granularity: Granularity::TensorGroup(16),
88        timing: ScaleTiming::Static,
89    };
90
91    /// 2026-09-28: A 4-bit float (NVFP4 or MXFP4).
92    pub fn is_fp4(&self) -> bool {
93        self.kind == NumKind::Float && self.bits == 4
94    }
95
96    /// 2026-09-28: An 8-bit float (E4M3).
97    pub fn is_fp8(&self) -> bool {
98        self.kind == NumKind::Float && self.bits == 8
99    }
100}
101
102/// 2026-09-28: What one linear layer declares. `None` means that operand is not quantized
103/// (16-bit).
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct LayerPrecision {
106    /// 2026-09-28: The weight's declared format.
107    pub weight: Option<Operand>,
108    /// 2026-09-28: The input activations' declared format.
109    pub activation: Option<Operand>,
110}
111
112impl LayerPrecision {
113    /// 2026-09-28: 16-bit weights and activations.
114    pub const UNQUANTIZED: LayerPrecision = LayerPrecision {
115        weight: None,
116        activation: None,
117    };
118
119    /// 2026-09-28: The activations are declared 4-bit float (W4A4 on an FP4 weight).
120    pub fn activation_is_fp4(&self) -> bool {
121        self.activation.is_some_and(|a| a.is_fp4())
122    }
123
124    /// 2026-09-28: The activations are declared 8-bit float.
125    pub fn activation_is_fp8(&self) -> bool {
126        self.activation.is_some_and(|a| a.is_fp8())
127    }
128
129    /// 2026-09-28: A short label, e.g. `W4A4`, `W8A8`, `W4A16`, `W16A16`.
130    pub fn label(&self) -> String {
131        let bits = |o: Option<Operand>| o.map_or(16, |o| o.bits);
132        format!("W{}A{}", bits(self.weight), bits(self.activation))
133    }
134}
135
136/// 2026-09-28: Which `quantization_config` dialect the plan came from.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138pub enum PlanSource {
139    /// 2026-09-28: No precision declared (no block, or a block with no scheme).
140    #[default]
141    Undeclared,
142    /// 2026-09-28: compressed-tensors `config_groups`.
143    CompressedTensors,
144    /// 2026-09-28: ModelOpt (`quantized_layers`, `config_groups` or a global `quant_algo`).
145    ModelOpt,
146    /// 2026-09-28: The HF `fp8` method (`weight_block_size`, `activation_scheme`).
147    Fp8,
148}
149
150/// 2026-09-28: One scheme and the modules it targets.
151#[derive(Debug, Clone)]
152pub struct Rule {
153    /// 2026-09-28: Targets, in the order the checkpoint lists them.
154    pub targets: Vec<Target>,
155    /// 2026-09-28: What the targeted modules declare.
156    pub precision: LayerPrecision,
157}
158
159/// 2026-09-28: The per-layer precision a checkpoint declares.
160#[derive(Debug, Clone, Default)]
161pub struct DeclaredPrecisionPlan {
162    /// 2026-09-28: Dialect.
163    pub source: PlanSource,
164    /// 2026-09-28: Schemes.
165    pub rules: Vec<Rule>,
166    /// 2026-09-28: Modules left unquantized.
167    pub ignore: Vec<Target>,
168}
169
170impl DeclaredPrecisionPlan {
171    /// 2026-09-28: A plan that declares nothing (no `quantization_config`).
172    pub const UNDECLARED: DeclaredPrecisionPlan = DeclaredPrecisionPlan {
173        source: PlanSource::Undeclared,
174        rules: Vec::new(),
175        ignore: Vec::new(),
176    };
177
178    /// 2026-09-28: Parse the `quantization_config` object (the value, not the whole
179    /// config.json). A ModelOpt `hf_quant_config.json` nested under `"quantization"` is
180    /// accepted as is.
181    pub fn from_quantization_config(qc: &serde_json::Value) -> Result<Self> {
182        parse::plan(qc)
183    }
184
185    /// 2026-09-28: What `module` (a checkpoint module path such as
186    /// `model.language_model.layers.3.mlp.gate_proj`, no `.weight` suffix) declares.
187    ///
188    /// Ignored modules are unquantized. Otherwise the compressed-tensors precedence
189    /// applies: exact names first, then patterns ordered by their text, then class
190    /// targets (`Linear`), and the first match wins.
191    pub fn resolve(&self, module: &str) -> LayerPrecision {
192        if self.ignore.iter().any(|t| t.matches_name(module)) {
193            return LayerPrecision::UNQUANTIZED;
194        }
195        let mut best: Option<(u8, &str, LayerPrecision)> = None;
196        for rule in &self.rules {
197            for t in &rule.targets {
198                let Some(tier) = t.tier_for(module) else {
199                    continue;
200                };
201                let key = (tier, t.text());
202                if best.is_none_or(|(bt, bs, _)| key < (bt, bs)) {
203                    best = Some((tier, t.text(), rule.precision));
204                }
205            }
206        }
207        best.map_or(LayerPrecision::UNQUANTIZED, |(_, _, p)| p)
208    }
209
210    /// 2026-09-28: True when no module declares anything.
211    pub fn is_undeclared(&self) -> bool {
212        self.source == PlanSource::Undeclared
213    }
214}
215
216#[cfg(test)]
217#[path = "precision_plan_tests.rs"]
218mod precision_plan_tests;