1use std::collections::BTreeMap;
18
19use metrale_config::DeclaredPrecisionPlan;
20use serde_json::Value;
21
22use crate::circuit_toml::CircuitError;
23use crate::config_map::{ConfigMap, ConfigMapError, MappedConfig, map_config};
24use crate::declared_precision::DeclaredPrecision;
25use crate::format::{Format, Scale};
26use crate::instantiate::instantiate;
27use crate::ir::{ArchShape, Circuit};
28use crate::precision::{LinearFormats, PrecisionError};
29use crate::precision_policy::{PolicyPrecision, caps_named, tier_named};
30
31#[derive(Debug, Clone, Copy, Default)]
33pub struct QuantMetadata<'a> {
34 pub hf_quant_config: Option<&'a str>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ServePrecision {
42 Declared,
44 Policy {
48 tier: String,
50 caps: Vec<String>,
52 engine: Vec<(String, LinearFormats)>,
54 },
55}
56
57#[derive(Debug, Clone)]
59pub struct ResolvedCheckpoint {
60 pub arch: String,
62 pub model_type: String,
64 pub shape: ArchShape,
66 pub params: BTreeMap<String, String>,
69 pub kv_cache: Option<Format>,
71 pub circuit: Circuit,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
77pub enum CheckpointError {
78 #[error("{file}: {detail}")]
80 Json {
81 file: &'static str,
83 detail: String,
85 },
86 #[error("no circuit serves model_type `{model_type}` (served: {served})")]
88 UnknownModelType {
89 model_type: String,
91 served: String,
93 },
94 #[error(transparent)]
96 Map(#[from] ConfigMapError),
97 #[error("quantization: {0}")]
100 Quant(String),
101 #[error(transparent)]
103 Precision(#[from] PrecisionError),
104 #[error(transparent)]
106 Circuit(#[from] CircuitError),
107}
108
109mod states;
110pub use states::recurrent_states;
111
112struct Arch {
114 circuit: &'static str,
115 config_map: &'static str,
116}
117
118macro_rules! circuits_file {
119 ($f:literal) => {
120 include_str!(concat!("../../../kernels/circuits/", $f))
121 };
122}
123
124const ARCHES: [Arch; 4] = [
125 Arch {
126 circuit: circuits_file!("qwen3_5.toml"),
127 config_map: circuits_file!("qwen3_5.config.toml"),
128 },
129 Arch {
130 circuit: circuits_file!("qwen3_6_moe.toml"),
131 config_map: circuits_file!("qwen3_6_moe.config.toml"),
132 },
133 Arch {
134 circuit: circuits_file!("nemotron_h.toml"),
135 config_map: circuits_file!("nemotron_h.config.toml"),
136 },
137 Arch {
138 circuit: circuits_file!("dense_gqa.toml"),
139 config_map: circuits_file!("dense_gqa.config.toml"),
140 },
141];
142
143const BLOCKS: [(&str, &str); 1] = [("qwen3_hybrid", circuits_file!("blocks/qwen3_hybrid.toml"))];
145
146pub fn config_maps() -> Result<Vec<ConfigMap>, ConfigMapError> {
148 ARCHES
149 .iter()
150 .map(|a| ConfigMap::parse(a.config_map))
151 .collect()
152}
153
154pub fn instantiate_from_checkpoint(
157 config_json: &str,
158 quant: QuantMetadata<'_>,
159) -> Result<Circuit, CheckpointError> {
160 Ok(resolve_checkpoint(config_json, quant, &ServePrecision::Declared)?.circuit)
161}
162
163pub fn resolve_checkpoint(
166 config_json: &str,
167 quant: QuantMetadata<'_>,
168 serve: &ServePrecision,
169) -> Result<ResolvedCheckpoint, CheckpointError> {
170 let config = parse_config(config_json)?;
171 let (i, mapped) = map_checkpoint_value(&config)?;
172 let qc = quantization_config(&config, quant)?;
173 let plan = match &qc {
174 Some(qc) => DeclaredPrecisionPlan::from_quantization_config(qc)
175 .map_err(|e| CheckpointError::Quant(format!("{e:#}")))?,
176 None => DeclaredPrecisionPlan::default(),
177 };
178 if let Some(qc) = &qc
179 && plan.rules.is_empty()
180 {
181 return Err(CheckpointError::Quant(format!(
182 "the quantization block declares no scheme for any layer (a missing quant group): {}",
183 serde_json::to_string(qc)
184 .unwrap_or_default()
185 .chars()
186 .take(200)
187 .collect::<String>()
188 )));
189 }
190 let kv_cache = kv_cache_format(qc.as_ref())?;
191 let text = ARCHES[i].circuit;
192 let circuit = match serve {
193 ServePrecision::Declared => {
194 let precision = DeclaredPrecision::new(&plan);
195 let circuit = instantiate(text, &BLOCKS, &mapped.shape, &precision)?;
196 let refused = precision.refusals();
197 if !refused.is_empty() {
198 return Err(CheckpointError::Quant(refused.join("; ")));
199 }
200 circuit
201 }
202 ServePrecision::Policy { tier, caps, engine } => {
203 let policy =
204 metrale_config::WeightQuantPolicy::new(tier_named(tier)?, &plan, caps_named(caps)?);
205 instantiate(
206 text,
207 &BLOCKS,
208 &mapped.shape,
209 &PolicyPrecision::new(policy, engine),
210 )?
211 }
212 };
213 Ok(ResolvedCheckpoint {
214 arch: mapped.arch,
215 model_type: mapped.model_type,
216 shape: mapped.shape,
217 params: mapped.params,
218 kv_cache,
219 circuit,
220 })
221}
222
223pub fn map_checkpoint(config_json: &str) -> Result<MappedConfig, CheckpointError> {
226 Ok(map_checkpoint_value(&parse_config(config_json)?)?.1)
227}
228
229fn parse_config(config_json: &str) -> Result<Value, CheckpointError> {
230 serde_json::from_str(config_json).map_err(|e| CheckpointError::Json {
231 file: "config.json",
232 detail: e.to_string(),
233 })
234}
235
236fn map_checkpoint_value(config: &Value) -> Result<(usize, MappedConfig), CheckpointError> {
238 let model_type = config
239 .get("model_type")
240 .and_then(Value::as_str)
241 .unwrap_or_default()
242 .to_string();
243 let maps = config_maps()?;
244 let Some((i, map)) = maps
245 .iter()
246 .enumerate()
247 .find(|(_, m)| m.model_types().contains(&model_type))
248 else {
249 let mut served: Vec<&str> = maps
250 .iter()
251 .flat_map(|m| m.model_types().iter().map(String::as_str))
252 .collect();
253 served.sort_unstable();
254 return Err(CheckpointError::UnknownModelType {
255 model_type,
256 served: served.join(", "),
257 });
258 };
259 Ok((i, map_config(map, config)?))
260}
261
262fn quantization_config(
264 config: &Value,
265 quant: QuantMetadata<'_>,
266) -> Result<Option<Value>, CheckpointError> {
267 if let Some(qc) = config.get("quantization_config").filter(|v| !v.is_null()) {
268 return Ok(Some(qc.clone()));
269 }
270 quant
271 .hf_quant_config
272 .map(|text| {
273 serde_json::from_str(text).map_err(|e| CheckpointError::Json {
274 file: "hf_quant_config.json",
275 detail: e.to_string(),
276 })
277 })
278 .transpose()
279}
280
281fn kv_cache_format(qc: Option<&Value>) -> Result<Option<Format>, CheckpointError> {
285 let Some(qc) = qc else {
286 return Ok(None);
287 };
288 let fp8 = Format::Fp8E4m3 {
289 scale: Scale::PerTensor,
290 };
291 if let Some(s) = qc.get("kv_cache_scheme").filter(|v| !v.is_null()) {
292 let bits = s.get("num_bits").and_then(Value::as_u64);
293 let ty = s.get("type").and_then(Value::as_str);
294 return match (bits, ty) {
295 (Some(8), Some("float")) => Ok(Some(fp8)),
296 _ => Err(CheckpointError::Quant(format!(
297 "kv_cache_scheme {s} has no KV-cache format in the circuit"
298 ))),
299 };
300 }
301 let algo = qc
302 .get("kv_cache_quant_algo")
303 .or_else(|| qc.get("quantization")?.get("kv_cache_quant_algo"))
304 .filter(|v| !v.is_null());
305 match algo.map(|a| a.as_str()) {
306 None => Ok(None),
307 Some(Some("FP8")) => Ok(Some(fp8)),
308 Some(other) => Err(CheckpointError::Quant(format!(
309 "kv_cache_quant_algo {other:?} has no KV-cache format in the circuit"
310 ))),
311 }
312}