metrale_config/precision_plan/target.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: Module targets of a declared precision plan and how they match.
4//!
5//! Owner: config (quantization).
6//! Invariants: none beyond the types.
7
8use anyhow::{Context, Result};
9
10/// 2026-09-28: One target or ignore entry.
11#[derive(Debug, Clone)]
12pub enum Target {
13 /// 2026-09-28: An exact module path.
14 Exact(String),
15 /// 2026-09-28: A compressed-tensors `re:` pattern, anchored at both ends as Python's
16 /// `re.match` plus the checkpoint's `$` do.
17 Regex(String, regex::Regex),
18 /// 2026-09-28: A ModelOpt glob (`*`, `?`).
19 Glob(String),
20 /// 2026-09-30: An HF `fp8` `modules_to_not_convert` / `ignored_layers` entry: a module
21 /// path matched on whole dotted segments. It names the module itself, a parent of it
22 /// (`model.visual` excludes `model.visual.blocks.0.attn.proj`), or its trailing segments
23 /// (`lm_head`); `mlp.gate` names the MoE router `...mlp.gate`, never the dense
24 /// `...mlp.gate_proj`. These are the three rules of transformers `should_convert_module`
25 /// (quantizers/quantizers_utils.py: `re.match(f"{key}\\.")`, `re.match(key)`,
26 /// `name.endswith(key)`) restricted to segment boundaries, and include vLLM's exact
27 /// `prefix in ignored_layers` (`is_layer_skipped`, quantization/utils/quant_utils.py). An
28 /// entry with regex syntax beyond `.` is that regex, anchored at the start (`re.match`) and
29 /// ending on a segment boundary.
30 HfModule(String, Option<regex::Regex>),
31 /// 2026-09-28: A module class (`Linear`). Every module the plan is asked about is a
32 /// linear layer, so `Linear` matches all of them and any other class none.
33 Class(String),
34}
35
36/// 2026-09-30: Which format's matching rules an ignore / exclude entry follows.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum IgnoreDialect {
39 /// 2026-09-30: compressed-tensors `ignore` ([`Target::compressed_tensors`]).
40 CompressedTensors,
41 /// 2026-09-30: ModelOpt `exclude_modules` ([`Target::modelopt`]).
42 ModelOpt,
43 /// 2026-09-30: HF `fp8` `modules_to_not_convert` / `ignored_layers` ([`Target::hf_module`]).
44 HfFp8,
45}
46
47impl Target {
48 /// 2026-09-30: An ignore entry of `dialect`, the one matcher the precision plan and the
49 /// weight loaders share. A malformed entry is refused.
50 pub fn ignore_entry(dialect: IgnoreDialect, s: &str) -> Result<Self> {
51 match dialect {
52 IgnoreDialect::CompressedTensors => Self::compressed_tensors(s),
53 IgnoreDialect::ModelOpt => Ok(Self::modelopt(s)),
54 IgnoreDialect::HfFp8 => Self::hf_module(s),
55 }
56 }
57
58 /// 2026-09-28: A compressed-tensors target or ignore entry: `re:<pattern>`, a class
59 /// name (`Linear`), or an exact path.
60 /// 2026-09-30: As compressed-tensors matches them (0.12.2, utils/match.py `_match_name`):
61 /// a `re:` pattern by `re.match` (anchored at the start only), anything else by equality.
62 pub fn compressed_tensors(s: &str) -> Result<Self> {
63 if let Some(p) = s.strip_prefix("re:") {
64 let re = regex::Regex::new(&format!("^(?:{p})"))
65 .with_context(|| format!("quantization_config: bad target pattern {s:?}"))?;
66 return Ok(Target::Regex(s.to_string(), re));
67 }
68 if !s.contains('.') && s.chars().next().is_some_and(char::is_uppercase) {
69 return Ok(Target::Class(s.to_string()));
70 }
71 Ok(Target::Exact(s.to_string()))
72 }
73
74 /// 2026-09-30: An HF `fp8` `modules_to_not_convert` / `ignored_layers` entry
75 /// ([`Target::HfModule`]). An empty entry, or one whose regex does not compile, is refused.
76 pub fn hf_module(s: &str) -> Result<Self> {
77 anyhow::ensure!(
78 !s.trim().is_empty(),
79 "quantization_config: an empty modules_to_not_convert entry"
80 );
81 let regex_syntax =
82 s.contains(['*', '?', '[', ']', '(', ')', '+', '^', '$', '|', '\\', '{']);
83 let re = if regex_syntax {
84 Some(
85 regex::Regex::new(&format!("^(?:{s})(?:\\.|$)"))
86 .with_context(|| format!("quantization_config: bad module pattern {s:?}"))?,
87 )
88 } else {
89 None
90 };
91 Ok(Target::HfModule(s.to_string(), re))
92 }
93
94 /// 2026-09-28: A ModelOpt entry: a glob when it has a wildcard, else exact.
95 /// 2026-09-30: ModelOpt writes exact module names or fnmatch globs; vLLM's
96 /// `ModelOptFp8Config.is_layer_excluded` (quantization/modelopt.py) adds a substring
97 /// fallback, which is not followed here: it has the `mlp.gate` / `mlp.gate_proj` hazard.
98 pub fn modelopt(s: &str) -> Self {
99 if s.contains(['*', '?']) {
100 Target::Glob(s.to_string())
101 } else if s == "Linear" {
102 Target::Class(s.to_string())
103 } else {
104 Target::Exact(s.to_string())
105 }
106 }
107
108 /// 2026-09-28: The entry as the checkpoint wrote it.
109 pub fn text(&self) -> &str {
110 match self {
111 Target::Exact(s)
112 | Target::Regex(s, _)
113 | Target::Glob(s)
114 | Target::HfModule(s, _)
115 | Target::Class(s) => s,
116 }
117 }
118
119 /// 2026-09-28: Does this entry name `module` (classes excluded)?
120 pub fn matches_name(&self, module: &str) -> bool {
121 match self {
122 Target::Exact(s) => s == module,
123 Target::Regex(_, re) => re.is_match(module),
124 Target::Glob(g) => glob_match(g.as_bytes(), module.as_bytes()),
125 Target::HfModule(_, Some(re)) => re.is_match(module),
126 Target::HfModule(s, None) => {
127 module == s
128 || module
129 .strip_prefix(s.as_str())
130 .is_some_and(|rest| rest.starts_with('.'))
131 || module
132 .strip_suffix(s.as_str())
133 .is_some_and(|head| head.ends_with('.'))
134 }
135 Target::Class(c) => c == "Linear",
136 }
137 }
138
139 /// 2026-09-28: Precedence tier when this entry matches `module`: 0 exact name, 1
140 /// pattern, 2 class. `None` when it does not match.
141 pub fn tier_for(&self, module: &str) -> Option<u8> {
142 if !self.matches_name(module) {
143 return None;
144 }
145 Some(match self {
146 Target::Exact(_) => 0,
147 Target::HfModule(s, None) if s == module => 0,
148 Target::Regex(..) | Target::Glob(_) | Target::HfModule(..) => 1,
149 Target::Class(_) => 2,
150 })
151 }
152}
153
154/// 2026-09-28: `*` matches any run, `?` one byte; everything else literally.
155fn glob_match(p: &[u8], s: &[u8]) -> bool {
156 let (mut pi, mut si) = (0usize, 0usize);
157 let (mut star, mut mark) = (None, 0usize);
158 while si < s.len() {
159 if pi < p.len() && (p[pi] == b'?' || p[pi] == s[si]) {
160 pi += 1;
161 si += 1;
162 } else if pi < p.len() && p[pi] == b'*' {
163 star = Some(pi);
164 mark = si;
165 pi += 1;
166 } else if let Some(sp) = star {
167 pi = sp + 1;
168 mark += 1;
169 si = mark;
170 } else {
171 return false;
172 }
173 }
174 p[pi..].iter().all(|&c| c == b'*')
175}