metrale_model_weights/weights/kv_scale_keys.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: The one resolver for a checkpoint's per-layer FP8 KV-cache scales.
4//!
5//! Exporters spell the pair three ways, relative to the attention prefix `p`
6//! (`model.layers.N.self_attn` and the like):
7//!
8//! | spelling | K key | V key | written by |
9//! |----------------|--------------------|--------------------|------------------------------------|
10//! | `KProjOutput` | `p.k_proj.k_scale` | `p.v_proj.v_scale` | ModelOpt, older llm-compressor |
11//! | `AttnModule` | `p.attn.k_scale` | `p.attn.v_scale` | vLLM's own `Attention` module name |
12//! | `Bare` | `p.k_scale` | `p.v_scale` | llm-compressor `kv_cache_scheme` |
13//!
14//! Every spelling carries the same quantity: the dequant multiplier
15//! `amax / 448`, which `reshape_and_cache_fp8` divides by on the write and the
16//! FP8 attention kernels multiply by on the read.
17//!
18//! Owner: model-weights (checkpoint key naming).
19//! Invariants:
20//! - [`resolve_kv_scale_keys`] is the only place a scale key is spelled. The
21//! per-layer loader (`weight_map::load_kv_scales`) and the serve-time census
22//! ([`kv_scale_census`]) both go through it, so the count serve logs is the
23//! number of layers that load a checkpoint scale.
24//! - A layer with two spellings, or with K but not V (or V but not K), is an
25//! error, never a silent 1.0.
26
27use std::collections::BTreeMap;
28
29use anyhow::{Result, bail};
30
31/// 2026-09-28: How a checkpoint names one layer's K/V scale pair.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
33pub enum KvScaleSpelling {
34 KProjOutput,
35 AttnModule,
36 Bare,
37}
38
39impl KvScaleSpelling {
40 /// 2026-09-28: Longest suffix first, so a name parses to the most specific
41 /// spelling: `p.k_proj.k_scale` is `KProjOutput` of `p`, not `Bare` of
42 /// `p.k_proj`.
43 pub const PRECEDENCE: [Self; 3] = [Self::KProjOutput, Self::AttnModule, Self::Bare];
44
45 /// 2026-09-28: `(K suffix, V suffix)`, each appended to `"{attn_prefix}."`.
46 pub fn suffixes(self) -> (&'static str, &'static str) {
47 match self {
48 Self::KProjOutput => ("k_proj.k_scale", "v_proj.v_scale"),
49 Self::AttnModule => ("attn.k_scale", "attn.v_scale"),
50 Self::Bare => ("k_scale", "v_scale"),
51 }
52 }
53
54 fn keys(self, attn_prefix: &str) -> (String, String) {
55 let (k, v) = self.suffixes();
56 (format!("{attn_prefix}.{k}"), format!("{attn_prefix}.{v}"))
57 }
58
59 /// 2026-09-28: The attention prefix `name` is a K or V scale of, in this
60 /// spelling.
61 fn prefix_of(self, name: &str) -> Option<&str> {
62 let (k, v) = self.suffixes();
63 [k, v].into_iter().find_map(|suffix| {
64 name.strip_suffix(suffix)
65 .and_then(|rest| rest.strip_suffix('.'))
66 .filter(|p| !p.is_empty())
67 })
68 }
69}
70
71/// 2026-09-28: The two store keys one attention layer's scales are read from.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct KvScaleKeys {
74 pub spelling: KvScaleSpelling,
75 pub k: String,
76 pub v: String,
77}
78
79/// 2026-09-28: The scale keys of the attention layer at `attn_prefix`.
80///
81/// `Ok(None)` when no spelling has either key. An error when the layer has a
82/// K or V key in more than one spelling (which one the exporter meant is not
83/// ours to guess), or has K without V or V without K in its one spelling.
84pub fn resolve_kv_scale_keys(
85 has: impl Fn(&str) -> bool,
86 attn_prefix: &str,
87) -> Result<Option<KvScaleKeys>> {
88 let mut found: Option<KvScaleKeys> = None;
89 for spelling in KvScaleSpelling::PRECEDENCE {
90 let (k, v) = spelling.keys(attn_prefix);
91 let (has_k, has_v) = (has(&k), has(&v));
92 if !has_k && !has_v {
93 continue;
94 }
95 if has_k != has_v {
96 let (present, missing) = if has_k { (&k, &v) } else { (&v, &k) };
97 bail!(
98 "FP8 KV scales for `{attn_prefix}` are incomplete: the checkpoint has \
99 `{present}` but not `{missing}`"
100 );
101 }
102 if let Some(first) = &found {
103 bail!(
104 "FP8 KV scales for `{attn_prefix}` are ambiguous: the checkpoint has both \
105 `{}`/`{}` and `{k}`/`{v}`",
106 first.k,
107 first.v,
108 );
109 }
110 found = Some(KvScaleKeys { spelling, k, v });
111 }
112 Ok(found)
113}
114
115/// 2026-09-28: Every attention layer whose scales [`resolve_kv_scale_keys`]
116/// finds, keyed by attention prefix.
117#[derive(Debug, Default, Clone, PartialEq, Eq)]
118pub struct KvScaleCensus {
119 pub layers: BTreeMap<String, KvScaleKeys>,
120}
121
122impl KvScaleCensus {
123 /// 2026-09-28: Layers that load their scales from the checkpoint.
124 pub fn len(&self) -> usize {
125 self.layers.len()
126 }
127
128 pub fn is_empty(&self) -> bool {
129 self.layers.is_empty()
130 }
131
132 /// 2026-09-28: The distinct spellings in use, as their K suffixes, for the
133 /// serve log.
134 pub fn spellings(&self) -> Vec<&'static str> {
135 let mut s: Vec<KvScaleSpelling> = self.layers.values().map(|k| k.spelling).collect();
136 s.sort();
137 s.dedup();
138 s.into_iter().map(|s| s.suffixes().0).collect()
139 }
140}
141
142/// 2026-09-28: Parse every name that ends in a known scale suffix to its
143/// attention prefix (most specific spelling first), then resolve each prefix
144/// with [`resolve_kv_scale_keys`]. Any layer that resolver rejects fails the
145/// census.
146pub fn kv_scale_census<'a>(
147 names: impl IntoIterator<Item = &'a str>,
148 has: impl Fn(&str) -> bool,
149) -> Result<KvScaleCensus> {
150 let mut prefixes: Vec<&str> = names
151 .into_iter()
152 .filter_map(|n| {
153 KvScaleSpelling::PRECEDENCE
154 .into_iter()
155 .find_map(|s| s.prefix_of(n))
156 })
157 .collect();
158 prefixes.sort_unstable();
159 prefixes.dedup();
160 let mut layers = BTreeMap::new();
161 for p in prefixes {
162 if let Some(keys) = resolve_kv_scale_keys(&has, p)? {
163 layers.insert(p.to_string(), keys);
164 }
165 }
166 Ok(KvScaleCensus { layers })
167}
168
169#[cfg(test)]
170#[path = "kv_scale_keys_tests.rs"]
171mod tests;