metrale_config/
weight_quantization.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: `--weight-quantization`: the precision each linear layer runs at, and the one
4//! policy that loaders and kernel dispatch ask for it.
5//!
6//! - `declared` (the default) follows the checkpoint's `quantization_config`
7//!   ([`DeclaredPrecisionPlan`]) per layer, for the weights and for the input activations.
8//! - `nvfp4` is the engine as it ran before the plan existed. FP8-declared projections are
9//!   requantized to NVFP4 at load. Decode runs 16-bit activations, and FP4 activations only
10//!   under `--w4a4-downcast` ([`W4a4Downcast`]). The dense-FFN MMQ prefill runs FP4
11//!   activations on every NVFP4 FFN.
12//!
13//! The serve publishes a [`WeightQuantTier`] once. Each loader builds a [`WeightQuantPolicy`]
14//! from it and the model's plan, and stamps what the policy answers on the weights it builds
15//! ([`Nvfp4Act`]). Kernel dispatch reads those stamps and the published tier, and nothing
16//! else.
17//!
18//! Owner: config (quantization).
19//! Invariants:
20//! - Pure: no environment, no I/O, no process state.
21//! - Under `nvfp4` no answer depends on the plan.
22//! - A checkpoint that declares nothing ([`DeclaredPrecisionPlan::is_undeclared`]) gets the
23//!   `nvfp4` answers under both tiers. Load-time quantization of a 16-bit checkpoint is
24//!   governed by its own flags, not this one.
25
26use anyhow::{Result, bail};
27
28use crate::precision_plan::{DeclaredPrecisionPlan, LayerPrecision};
29
30/// 2026-09-28: The `--weight-quantization` tiers.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
32pub enum WeightQuantization {
33    /// 2026-09-28: Each layer at the precision its checkpoint declares.
34    #[default]
35    Declared,
36    /// 2026-09-28: FP8-declared projections requantized to NVFP4 at load, as before the plan
37    /// existed.
38    Nvfp4,
39}
40
41impl WeightQuantization {
42    /// 2026-09-28: Every tier, in the order the flag lists them.
43    pub const ALL: [Self; 2] = [Self::Declared, Self::Nvfp4];
44
45    /// 2026-09-28: The flag value, recipe value and gate-record value.
46    pub fn name(self) -> &'static str {
47        match self {
48            Self::Declared => "declared",
49            Self::Nvfp4 => "nvfp4",
50        }
51    }
52}
53
54/// 2026-09-28: `--w4a4-downcast` and `--w4a4-downcast-wide`, which exist only under the
55/// `nvfp4` tier. They run every NVFP4 small-M projection's activations as NVFP4 (W4A4),
56/// whatever the checkpoint declares.
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
58pub enum W4a4Downcast {
59    /// 2026-09-28: 16-bit activations at decode.
60    #[default]
61    Off,
62    /// 2026-09-28: `--w4a4-downcast`: up to 32 rows.
63    Narrow,
64    /// 2026-09-28: `--w4a4-downcast --w4a4-downcast-wide`: up to 64 rows.
65    Wide,
66}
67
68impl W4a4Downcast {
69    /// 2026-09-28: From the two presence flags; `-wide` alone does nothing, as it always has.
70    pub fn from_flags(downcast: bool, wide: bool) -> Self {
71        match (downcast, wide) {
72            (false, _) => Self::Off,
73            (true, false) => Self::Narrow,
74            (true, true) => Self::Wide,
75        }
76    }
77}
78
79/// 2026-09-28: What the serve publishes: the tier and, under `nvfp4`, its W4A4 lever.
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
81pub struct WeightQuantTier {
82    tier: WeightQuantization,
83    downcast: W4a4Downcast,
84}
85
86impl WeightQuantTier {
87    /// 2026-09-28: The `nvfp4` tier with its W4A4 lever. A `declared` tier refuses a lever:
88    /// the checkpoint already decides every layer's activations there.
89    pub fn new(tier: WeightQuantization, downcast: W4a4Downcast) -> Result<Self> {
90        if tier == WeightQuantization::Declared && downcast != W4a4Downcast::Off {
91            bail!(
92                "--w4a4-downcast applies only with --weight-quantization nvfp4; under \
93                 `declared` each layer's activations follow the checkpoint's \
94                 quantization_config"
95            );
96        }
97        Ok(Self { tier, downcast })
98    }
99
100    /// 2026-09-28: The tier.
101    pub fn tier(self) -> WeightQuantization {
102        self.tier
103    }
104
105    /// 2026-09-28: The `nvfp4` tier's W4A4 lever; always `Off` under `declared`.
106    pub fn downcast(self) -> W4a4Downcast {
107        self.downcast
108    }
109
110    /// 2026-09-28: Whether decode may take the FP4 block-scale projection path at all, so
111    /// its kernels and activation scratch are prepared.
112    pub fn uses_w4a4_decode(self) -> bool {
113        match self.tier {
114            WeightQuantization::Declared => true,
115            WeightQuantization::Nvfp4 => self.downcast != W4a4Downcast::Off,
116        }
117    }
118
119    /// 2026-09-28: The widest row count the W4A4 path serves for a weight stamped `act`,
120    /// given `kernel_rows`, the widest the prepared kernels serve (0 when absent). 0 means
121    /// the weight's decode activations stay 16-bit.
122    ///
123    /// Under `nvfp4` the lever decides for every weight alike: 32 rows, or 64 with `-wide`.
124    /// Under `declared` a weight stamped [`Nvfp4Act::A4`] runs W4A4 at every row count the
125    /// kernels serve, and any other weight runs 16-bit activations.
126    pub fn w4a4_rows(
127        self,
128        act: Nvfp4Act,
129        narrow_rows: u32,
130        wide_rows: u32,
131        kernel_rows: u32,
132    ) -> u32 {
133        match self.tier {
134            WeightQuantization::Nvfp4 => match self.downcast {
135                W4a4Downcast::Off => 0,
136                W4a4Downcast::Narrow => narrow_rows,
137                W4a4Downcast::Wide => wide_rows,
138            },
139            WeightQuantization::Declared => {
140                if act == Nvfp4Act::A4 {
141                    kernel_rows
142                } else {
143                    0
144                }
145            }
146        }
147    }
148
149    /// 2026-09-28: Whether a dense FFN whose gate, up or down is stamped `act` sends its 2-
150    /// and 3-row decode steps through the W4A4 projections. Under `declared` the stamps
151    /// decide; under `nvfp4` the lever does, as `--w4a4-downcast` always has.
152    pub fn ffn_small_batch_w4a4(self, any_a4: bool) -> bool {
153        match self.tier {
154            WeightQuantization::Nvfp4 => self.downcast != W4a4Downcast::Off,
155            WeightQuantization::Declared => any_a4,
156        }
157    }
158
159    /// 2026-09-28: Whether a dense FFN runs its single decode row W4A4. Only `declared` does,
160    /// and only for an FFN that declares FP4 activations; `--w4a4-downcast` never reached
161    /// the single row.
162    pub fn ffn_single_row_w4a4(self, any_a4: bool) -> bool {
163        self.tier == WeightQuantization::Declared && any_a4
164    }
165}
166
167/// 2026-09-28: The activation format the policy set for one NVFP4 weight, which the loader
168/// stamps on it (`QuantizedWeight::act`).
169#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
170pub enum Nvfp4Act {
171    /// 2026-09-28: No stamp: the loader does not consult the policy, or the tier is `nvfp4`.
172    /// Decode follows the published tier's lever, and prefill may run FP4 activations (the
173    /// dense-FFN MMQ arm), as before the plan existed.
174    #[default]
175    Unstamped,
176    /// 2026-09-28: The checkpoint declares FP4 input activations (W4A4): decode and prefill
177    /// run them FP4.
178    A4,
179    /// 2026-09-28: The checkpoint declares wider activations (A8 or A16): no FP4 activations
180    /// in decode or prefill.
181    Wide,
182}
183
184impl Nvfp4Act {
185    /// 2026-09-28: The stamp of a weight built from `parts` (a fused concatenation): FP4 only
186    /// when every part is, `Wide` when any part is, else unstamped.
187    pub fn combine(parts: impl IntoIterator<Item = Nvfp4Act>) -> Nvfp4Act {
188        let mut all_a4 = true;
189        let mut any = false;
190        for p in parts {
191            any = true;
192            match p {
193                Nvfp4Act::Wide => return Nvfp4Act::Wide,
194                Nvfp4Act::Unstamped => all_a4 = false,
195                Nvfp4Act::A4 => {}
196            }
197        }
198        if any && all_a4 {
199            Nvfp4Act::A4
200        } else {
201            Nvfp4Act::Unstamped
202        }
203    }
204
205    /// 2026-09-28: Whether prefill may quantize this weight's activations to FP4.
206    pub fn allows_fp4_prefill(self) -> bool {
207        self != Nvfp4Act::Wide
208    }
209}
210
211/// 2026-09-28: Which kernels the policy may rely on. The W8A8 decode family reports its
212/// presence here (`metrale_model_layers::layers::kernel_caps`).
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
214pub struct KernelCaps {
215    /// 2026-09-28: A per-token E4M3 activation quantizer plus E4M3xE4M3 small-M GEMMs for the
216    /// dense and non-expert projections. Until they land, an FP8-declared layer served at its
217    /// FP8 weights runs the W8A16 kernels: its activations are above the declared precision.
218    pub w8a8_decode: bool,
219    /// 2026-09-28: The same for MoE experts (routed and shared), whose grouped kernels land
220    /// separately ([`is_expert_module`]).
221    pub w8a8_moe_decode: bool,
222    /// 2026-09-28: The W8A8 decode of non-expert projections whose FP8 weights are 128x128
223    /// block-scaled (HF `fp8`, e.g. the attention and GDN of Qwen3.6-35B-A3B-FP8), validated
224    /// separately from the per-channel dense family
225    /// ([`WeightQuantPolicy::fp8_block_scaled_decode_act`]).
226    pub w8a8_block_scaled_decode: bool,
227    /// 2026-09-28: An FP8 lm_head that serves every decode row count in one pass. The FP8
228    /// head today launches once per row, which collapses throughput at width (C16 88.9 tok/s
229    /// against 200.0 on the NVFP4 head, C128 not finishing), so `declared` takes a declared
230    /// FP8 head only once this is set ([`LmHeadChoice::PendingFp8Kernel`]).
231    pub fp8_lm_head_batched: bool,
232}
233
234/// 2026-09-28: Whether `module` is a MoE expert projection (routed or shared), which the
235/// [`KernelCaps::w8a8_moe_decode`] kernels serve.
236pub fn is_expert_module(module: &str) -> bool {
237    module.contains(".experts.")
238        || module.ends_with(".experts")
239        || module.contains(".shared_expert.")
240}
241
242/// 2026-09-28: The activation format a layer runs at in decode.
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub enum ActFormat {
245    /// 2026-09-28: BF16 activations.
246    Bf16,
247    /// 2026-09-28: E4M3 activations with a per-token (or per-group) scale.
248    Fp8,
249    /// 2026-09-28: NVFP4 activations.
250    Fp4,
251}
252
253/// 2026-09-28: The lm_head format `--lm-head-dtype default` resolves to under `declared`.
254#[derive(Clone, Copy, Debug, PartialEq, Eq)]
255pub enum LmHeadFormat {
256    /// 2026-09-28: The head stays 16-bit (the checkpoint does not quantize it).
257    Bf16,
258    /// 2026-09-28: The checkpoint's FP8 head, decoded W8A16 (W8A8 once the kernels land).
259    Fp8,
260    /// 2026-09-28: NVFP4.
261    Nvfp4,
262}
263
264/// 2026-09-28: What `--lm-head-dtype default` resolves to ([`WeightQuantPolicy::lm_head`]).
265#[derive(Clone, Copy, Debug, PartialEq, Eq)]
266pub enum LmHeadChoice {
267    /// 2026-09-28: The engine's per-model default head: under `nvfp4`, or with nothing
268    /// declared.
269    EngineDefault,
270    /// 2026-09-28: The head format the checkpoint declares.
271    Declared(LmHeadFormat),
272    /// 2026-09-28: The checkpoint declares an FP8 head but the batched FP8 head kernel
273    /// ([`KernelCaps::fp8_lm_head_batched`]) is absent: the engine's default head, which is
274    /// above or below the declared precision depending on the model, until it lands.
275    PendingFp8Kernel,
276}
277
278/// 2026-09-28: The per-layer answers for one model: its declared plan under the published
279/// tier.
280#[derive(Clone, Copy, Debug)]
281pub struct WeightQuantPolicy<'a> {
282    tier: WeightQuantTier,
283    plan: &'a DeclaredPrecisionPlan,
284    caps: KernelCaps,
285}
286
287impl<'a> WeightQuantPolicy<'a> {
288    /// 2026-09-28: The policy for a model whose `quantization_config` parsed to `plan`.
289    pub fn new(tier: WeightQuantTier, plan: &'a DeclaredPrecisionPlan, caps: KernelCaps) -> Self {
290        Self { tier, plan, caps }
291    }
292
293    /// 2026-09-28: The policy for a checkpoint whose parsed `quantization_config` is `qc`;
294    /// `None` (no block) declares nothing.
295    pub fn for_checkpoint(
296        tier: WeightQuantTier,
297        qc: Option<&'a crate::QuantizationConfig>,
298        caps: KernelCaps,
299    ) -> Self {
300        static UNDECLARED: DeclaredPrecisionPlan = DeclaredPrecisionPlan::UNDECLARED;
301        Self::new(tier, qc.map_or(&UNDECLARED, |q| &q.precision), caps)
302    }
303
304    /// 2026-09-28: The tier in force.
305    pub fn tier(&self) -> WeightQuantTier {
306        self.tier
307    }
308
309    /// 2026-09-28: Whether answers follow the checkpoint: the `declared` tier on a checkpoint
310    /// that declares something.
311    pub fn follows_plan(&self) -> bool {
312        self.tier.tier == WeightQuantization::Declared && !self.plan.is_undeclared()
313    }
314
315    /// 2026-09-28: What `module` declares.
316    pub fn declared(&self, module: &str) -> LayerPrecision {
317        self.plan.resolve(module)
318    }
319
320    /// 2026-09-28: The stamp for `module`'s NVFP4 weight (`QuantizedWeight::act`): `A4` where
321    /// the checkpoint declares FP4 activations, `Wide` where it declares any other (so no
322    /// FP4 activations run below it), unstamped under `nvfp4` or with no declaration.
323    pub fn nvfp4_act(&self, module: &str) -> Nvfp4Act {
324        if !self.follows_plan() {
325            return Nvfp4Act::Unstamped;
326        }
327        if self.plan.resolve(module).activation_is_fp4() {
328            Nvfp4Act::A4
329        } else {
330            Nvfp4Act::Wide
331        }
332    }
333
334    /// 2026-09-28: Whether the policy asks for `module`'s FP8 weights to be served as FP8,
335    /// not requantized to NVFP4 at load: under `declared`, every weight the checkpoint
336    /// declares FP8. A load site without an FP8 decode arm for the declared scale layout
337    /// still requantizes, logs it, and stamps the NVFP4 copy `Wide`.
338    pub fn wants_fp8_weights(&self, module: &str) -> bool {
339        self.follows_plan() && self.plan.resolve(module).weight.is_some_and(|w| w.is_fp8())
340    }
341
342    /// 2026-09-28: The decode activations of `module` when it is served at its FP8 weights
343    /// (`None` when the policy does not ask for them): FP8 where the checkpoint declares FP8
344    /// activations and the W8A8 decode kernels are present, BF16 otherwise (above declared,
345    /// until those kernels land). This is WHEN; the W8A8 family says whether it CAN serve a
346    /// given weight and row count.
347    pub fn fp8_decode_act(&self, module: &str) -> Option<ActFormat> {
348        if !self.wants_fp8_weights(module) {
349            return None;
350        }
351        let a8 = self
352            .plan
353            .resolve(module)
354            .activation
355            .is_some_and(|a| a.is_fp8());
356        let kernels = if is_expert_module(module) {
357            self.caps.w8a8_moe_decode
358        } else {
359            self.caps.w8a8_decode
360        };
361        Some(if kernels && a8 {
362            ActFormat::Fp8
363        } else {
364            ActFormat::Bf16
365        })
366    }
367
368    /// 2026-09-28: [`Self::fp8_decode_act`] for a non-expert `module` whose FP8 weight is
369    /// 128x128 block-scaled: FP8 only when [`KernelCaps::w8a8_block_scaled_decode`] is set as
370    /// well, BF16 (W8A16) otherwise. An expert module answers as `fp8_decode_act` does.
371    pub fn fp8_block_scaled_decode_act(&self, module: &str) -> Option<ActFormat> {
372        let act = self.fp8_decode_act(module)?;
373        Some(
374            if act == ActFormat::Fp8
375                && !is_expert_module(module)
376                && !self.caps.w8a8_block_scaled_decode
377            {
378                ActFormat::Bf16
379            } else {
380                act
381            },
382        )
383    }
384
385    /// 2026-09-28: Whether the policy serves `module` at its FP8 weights and the checkpoint
386    /// declares FP8 activations for it, whatever the kernels. Where the W8A8 answer is BF16,
387    /// the layer runs above its declared activations until its W8A8 path is validated, which
388    /// the load log states.
389    pub fn declares_fp8_activations(&self, module: &str) -> bool {
390        self.wants_fp8_weights(module)
391            && self
392                .plan
393                .resolve(module)
394                .activation
395                .is_some_and(|a| a.is_fp8())
396    }
397
398    /// 2026-10-03: Whether the block-scaled attention/GDN W8A8 may run although
399    /// [`KernelCaps::w8a8_block_scaled_decode`] is off. That cap is held because, stacked with
400    /// the expert W8A8 on Qwen/Qwen3.6-35B-A3B-FP8, it flipped a greedy tie. Only a MoE
401    /// checkpoint that declares FP8 activations on none of its `experts` (the NVFP4 35B,
402    /// whose experts are NVFP4) has no such stack. A dense checkpoint, or the FP8 35B with its
403    /// experts held at BF16 activations, keeps W8A16, as the combination is unvalidated there.
404    pub fn lifts_block_scaled_w8a8(&self, experts: &[String], experts_decode_fp8: bool) -> bool {
405        self.follows_plan()
406            && !experts_decode_fp8
407            && !experts.is_empty()
408            && experts.iter().all(|m| !self.declares_fp8_activations(m))
409    }
410
411    /// 2026-09-28: The head `--lm-head-dtype default` takes: under `declared`, the declared
412    /// format, except a declared FP8 head while [`KernelCaps::fp8_lm_head_batched`] is false;
413    /// the engine's per-model default otherwise, or for a format with no head kernel.
414    pub fn lm_head(&self) -> LmHeadChoice {
415        if !self.follows_plan() {
416            return LmHeadChoice::EngineDefault;
417        }
418        match self.plan.resolve("lm_head").weight {
419            None => LmHeadChoice::Declared(LmHeadFormat::Bf16),
420            Some(w) if w.is_fp8() && self.caps.fp8_lm_head_batched => {
421                LmHeadChoice::Declared(LmHeadFormat::Fp8)
422            }
423            Some(w) if w.is_fp8() => LmHeadChoice::PendingFp8Kernel,
424            Some(w) if w.is_fp4() => LmHeadChoice::Declared(LmHeadFormat::Nvfp4),
425            Some(_) => LmHeadChoice::EngineDefault,
426        }
427    }
428}
429
430#[cfg(test)]
431#[path = "weight_quantization_tests.rs"]
432mod weight_quantization_tests;