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;