1use std::collections::HashSet;
39use std::path::Path;
40
41use anyhow::{Context, Result, anyhow, bail};
42use rlx_core::safetensors_checkpoint::SafetensorsCheckpoint;
43use rlx_core::weight_loader::WeightLoader;
44use safetensors::Dtype;
45
46use crate::qat::{GemmaQuantBits, GemmaQuantPlan, dequantize_matrix, unpack_row};
47
48pub struct GemmaQatLoader {
50 ckpt: SafetensorsCheckpoint,
51 plan: GemmaQuantPlan,
52 taken: HashSet<String>,
53}
54
55impl GemmaQatLoader {
56 pub fn open(dir: &Path) -> Result<Self> {
59 let ckpt = SafetensorsCheckpoint::open(dir)?;
60 let cfg_path = dir.join("config.json");
61 let raw = std::fs::read(&cfg_path)
62 .with_context(|| format!("reading {cfg_path:?} for quantization_config"))?;
63 let json: serde_json::Value =
64 serde_json::from_slice(&raw).with_context(|| format!("parsing {cfg_path:?}"))?;
65 let quant = json.get("quantization_config").ok_or_else(|| {
66 anyhow!("{cfg_path:?}: no quantization_config (not a QAT checkpoint?)")
67 })?;
68 let plan = GemmaQuantPlan::from_json(quant);
69 Ok(Self {
70 ckpt,
71 plan,
72 taken: HashSet::new(),
73 })
74 }
75
76 fn remap(key: &str) -> String {
82 if key.starts_with("lm_head") {
83 return key.to_string();
84 }
85 const VERBATIM: [&str; 4] = [
86 "model.vision_tower",
87 "model.audio_tower",
88 "model.embed_vision",
89 "model.embed_audio",
90 ];
91 if VERBATIM.iter().any(|p| key.starts_with(p)) {
92 return key.to_string();
93 }
94 match key.strip_prefix("model.") {
95 Some(rest) => format!("model.language_model.{rest}"),
96 None => key.to_string(),
97 }
98 }
99
100 fn unpacked_cols(packed_cols: usize, bits: GemmaQuantBits) -> usize {
102 packed_cols * bits.values_per_byte()
103 }
104
105 fn read_scale(&self, name: &str) -> Result<Vec<f32>> {
106 let (bytes, dt, _shape) = self.ckpt.tensor_raw(name)?;
107 anyhow::ensure!(dt == Dtype::F32, "{name}: scale must be F32, got {dt:?}");
108 Ok(bytes
109 .chunks_exact(4)
110 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
111 .collect())
112 }
113
114 fn dequant_linear(&self, st_weight: &str) -> Result<(Vec<f32>, Vec<usize>)> {
117 let module = st_weight.strip_suffix(".weight").unwrap_or(st_weight);
118 let bits = self
119 .plan
120 .resolve_bits(module)
121 .ok_or_else(|| anyhow!("{module}: no quant bits (in modules_to_not_convert?)"))?;
122 let (qbytes, qdt, qshape) = self.ckpt.tensor_raw(st_weight)?;
123 anyhow::ensure!(
124 matches!(qdt, Dtype::U8 | Dtype::I8),
125 "{st_weight}: expected U8/I8 packed weight, got {qdt:?}"
126 );
127 anyhow::ensure!(
128 qshape.len() == 2,
129 "{st_weight}: expected rank-2, got {qshape:?}"
130 );
131 let out = qshape[0];
132 let inn = Self::unpacked_cols(qshape[1], bits);
133 let scale = self.read_scale(&format!("{module}.weight_scale"))?;
134 let w = dequantize_matrix(&qbytes, &scale, out, inn, bits)?;
135 Ok((w, vec![out, inn]))
136 }
137
138 fn dequant_embedding_full(&self, base: &str) -> Result<(Vec<f32>, Vec<usize>)> {
143 let bits = self
144 .plan
145 .resolve_bits(base)
146 .ok_or_else(|| anyhow!("{base}: no quant bits for embedding"))?;
147 let (qbytes, qdt, qshape) = self
148 .ckpt
149 .tensor_raw(&format!("{base}.embedding_quantized"))?;
150 anyhow::ensure!(
151 matches!(qdt, Dtype::U8 | Dtype::I8),
152 "{base}.embedding_quantized: expected U8/I8, got {qdt:?}"
153 );
154 let vocab = qshape[0];
155 let dim = Self::unpacked_cols(qshape[1], bits);
156 let (sbytes, _sdt, sshape) = self.ckpt.tensor_raw(&format!("{base}.embedding_scale"))?;
157 let scale: Vec<f32> = sbytes
158 .chunks_exact(4)
159 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
160 .collect();
161 anyhow::ensure!(
162 sshape == [vocab, 1],
163 "{base}.embedding_scale: expected per-row [vocab,1], got {sshape:?} \
164 (grouped scale → use dequant_embedding_rows)"
165 );
166 let w = dequantize_matrix(&qbytes, &scale, vocab, dim, bits)?;
167 Ok((w, vec![vocab, dim]))
168 }
169
170 pub fn dequant_embedding_rows(
177 &self,
178 builder_key: &str,
179 rows: &[u32],
180 ) -> Result<(Vec<f32>, usize)> {
181 let base = Self::remap(builder_key);
182 let base = base.strip_suffix(".weight").unwrap_or(&base);
183 let bits = self
184 .plan
185 .resolve_bits(base)
186 .ok_or_else(|| anyhow!("{base}: no quant bits for embedding"))?;
187 let (qbytes, qdt, qshape) = self
188 .ckpt
189 .tensor_raw(&format!("{base}.embedding_quantized"))?;
190 anyhow::ensure!(
191 matches!(qdt, Dtype::U8 | Dtype::I8),
192 "{base}.embedding_quantized: expected U8/I8, got {qdt:?}"
193 );
194 let vocab = qshape[0];
195 let packed_cols = qshape[1];
196 let dim = Self::unpacked_cols(packed_cols, bits);
197 let (sbytes, _sdt, sshape) = self.ckpt.tensor_raw(&format!("{base}.embedding_scale"))?;
198 let scale: Vec<f32> = sbytes
199 .chunks_exact(4)
200 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
201 .collect();
202 let groups = if sshape == [vocab, 1] {
203 1
204 } else {
205 anyhow::ensure!(
206 sshape.len() == 2 && sshape[0] == vocab,
207 "{base}.embedding_scale: unexpected shape {sshape:?}"
208 );
209 sshape[1]
210 };
211 anyhow::ensure!(
212 dim % groups == 0,
213 "{base}: dim {dim} not divisible by scale groups {groups}"
214 );
215 let block = dim / groups;
216 let mut out = Vec::with_capacity(rows.len() * dim);
217 for &r in rows {
218 let r = r as usize;
219 anyhow::ensure!(r < vocab, "{base}: row {r} >= vocab {vocab}");
220 let row_bytes = &qbytes[r * packed_cols..(r + 1) * packed_cols];
221 let q = unpack_row(row_bytes, dim, bits);
222 for (j, qv) in q.iter().enumerate() {
223 let g = j / block;
224 out.push(*qv as f32 * scale[r * groups + g]);
225 }
226 }
227 Ok((out, dim))
228 }
229
230 pub fn float_tensor(&self, builder_key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
232 self.take_float(&Self::remap(builder_key))
233 }
234
235 pub fn inputs_embeds(&self, cfg: &crate::config::GemmaConfig, ids: &[u32]) -> Result<Vec<f32>> {
238 let (rows, dim) = self.dequant_embedding_rows("model.embed_tokens.weight", ids)?;
239 anyhow::ensure!(
240 dim == cfg.hidden_size,
241 "embed dim {dim} != hidden {}",
242 cfg.hidden_size
243 );
244 let scale = (cfg.hidden_size as f32).sqrt();
245 Ok(rows.iter().map(|v| v * scale).collect())
246 }
247
248 pub fn compute_per_layer_inputs(
258 &self,
259 cfg: &crate::config::GemmaConfig,
260 ids: &[u32],
261 ) -> Result<Vec<f32>> {
262 let seq = ids.len();
263 let h = cfg.hidden_size;
264 let nl = cfg.num_hidden_layers;
265 let pw = cfg.ple_width();
266 let eps = cfg.rms_norm_eps as f32;
267
268 let (tok_rows, tdim) =
269 self.dequant_embedding_rows("model.embed_tokens_per_layer.weight", ids)?;
270 anyhow::ensure!(tdim == nl * pw, "per-layer embed dim {tdim} != {nl}*{pw}");
271 let tok_scale = (pw as f32).sqrt();
272
273 let ie = self.inputs_embeds(cfg, ids)?; let (w, wshape) = self.float_tensor("model.per_layer_model_projection.weight")?;
275 anyhow::ensure!(
276 wshape == [nl * pw, h],
277 "per_layer_model_projection shape {wshape:?} != [{}, {h}]",
278 nl * pw
279 );
280 let proj_scale = (h as f32).powf(-0.5);
281 let (gnorm, gshape) = self.float_tensor("model.per_layer_projection_norm.weight")?;
282 anyhow::ensure!(gshape == [pw], "projection_norm shape {gshape:?} != [{pw}]");
283 let inv_sqrt2 = 0.5f32.sqrt();
284
285 let mut out = vec![0f32; seq * nl * pw];
286 for s in 0..seq {
287 let emb = &ie[s * h..(s + 1) * h];
288 for layer in 0..nl {
289 let mut block = vec![0f32; pw];
291 for (d, b) in block.iter_mut().enumerate() {
292 let wrow = &w[(layer * pw + d) * h..(layer * pw + d + 1) * h];
293 let acc: f32 = emb.iter().zip(wrow).map(|(a, b)| a * b).sum();
294 *b = acc * proj_scale;
295 }
296 let ss: f32 = block.iter().map(|x| x * x).sum();
298 let rms = 1.0 / (ss / pw as f32 + eps).sqrt();
299 for d in 0..pw {
300 let normed = block[d] * rms * (1.0 + gnorm[d]);
301 let tok = tok_rows[s * tdim + layer * pw + d] * tok_scale;
302 out[s * nl * pw + layer * pw + d] = (normed + tok) * inv_sqrt2;
303 }
304 }
305 }
306 Ok(out)
307 }
308
309 fn take_float(&self, st: &str) -> Result<(Vec<f32>, Vec<usize>)> {
318 let (bytes, dt, shape) = self.ckpt.tensor_raw(st)?;
319 let mut data = match dt {
320 Dtype::F32 | Dtype::F16 | Dtype::BF16 => bytes_view_to_f32(&bytes, dt)?,
321 other => bail!("{st}: take_float on non-float dtype {other:?}"),
322 };
323 if st.ends_with("norm.weight") {
324 for v in &mut data {
325 *v -= 1.0;
326 }
327 }
328 Ok((data, shape))
329 }
330
331 fn take_impl(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
332 self.taken.insert(key.to_string());
333 let st = Self::remap(key);
334 if self.ckpt.contains(&st) {
336 let (_b, dt, _s) = self.ckpt.tensor_raw(&st)?;
337 return match dt {
338 Dtype::U8 | Dtype::I8 => self.dequant_linear(&st),
339 _ => self.take_float(&st),
340 };
341 }
342 let base = st.strip_suffix(".weight").unwrap_or(&st);
345 if self.ckpt.contains(&format!("{base}.embedding_quantized")) {
346 return self.dequant_embedding_full(base);
347 }
348 bail!("GemmaQatLoader: tensor {key} (→ {st}) not found in checkpoint")
349 }
350}
351
352fn bytes_view_to_f32(bytes: &[u8], dt: Dtype) -> Result<Vec<f32>> {
353 Ok(match dt {
354 Dtype::F32 => bytes
355 .chunks_exact(4)
356 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
357 .collect(),
358 Dtype::F16 => bytes
359 .chunks_exact(2)
360 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f32())
361 .collect(),
362 Dtype::BF16 => bytes
363 .chunks_exact(2)
364 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f32())
365 .collect(),
366 other => bail!("unsupported float dtype {other:?}"),
367 })
368}
369
370fn transpose_2d(data: &[f32], rows: usize, cols: usize) -> Vec<f32> {
371 let mut t = vec![0f32; data.len()];
372 for r in 0..rows {
373 for c in 0..cols {
374 t[c * rows + r] = data[r * cols + c];
375 }
376 }
377 t
378}
379
380impl WeightLoader for GemmaQatLoader {
381 fn format_id(&self) -> &'static str {
382 "gemma-qat-safetensors"
383 }
384
385 fn len(&self) -> usize {
386 self.ckpt.keys().count()
387 }
388
389 fn take(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
390 self.take_impl(key)
391 }
392
393 fn take_transposed(&mut self, key: &str) -> Result<(Vec<f32>, Vec<usize>)> {
394 let (data, shape) = self.take_impl(key)?;
395 anyhow::ensure!(
396 shape.len() == 2,
397 "take_transposed on non-2D tensor {key}: {shape:?}"
398 );
399 let (rows, cols) = (shape[0], shape[1]);
400 Ok((transpose_2d(&data, rows, cols), vec![cols, rows]))
401 }
402
403 fn remaining_keys(&self) -> Vec<String> {
404 self.ckpt
405 .keys()
406 .filter(|k| !self.taken.contains(*k))
407 .map(|s| s.to_string())
408 .collect()
409 }
410
411 fn arch_hint(&self) -> Option<&str> {
412 Some("gemma4")
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 fn fixture_dir() -> Option<std::path::PathBuf> {
422 if let Some(d) = std::env::var_os("RLX_GEMMA4_E2B_DIR") {
423 let p = std::path::PathBuf::from(d);
424 return p.join("config.json").is_file().then_some(p);
425 }
426 let home = std::env::var_os("HOME")?;
428 let base = std::path::Path::new(&home).join(
429 ".cache/huggingface/hub/\
430 models--google--gemma-4-E2B-it-qat-mobile-transformers/snapshots",
431 );
432 let snap = std::fs::read_dir(&base).ok()?.flatten().next()?.path();
433 snap.join("config.json").is_file().then_some(snap)
434 }
435
436 #[test]
437 fn loads_and_dequants_real_checkpoint() {
438 let Some(dir) = fixture_dir() else {
439 eprintln!("[qat_loader] checkpoint not found — skipping");
440 return;
441 };
442 let mut ld = GemmaQatLoader::open(&dir).expect("open ckpt");
443
444 let (q, qs) = ld
446 .take("model.layers.0.self_attn.q_proj.weight")
447 .expect("take q_proj");
448 assert_eq!(qs, vec![2048, 1536]);
449 assert!(q.iter().all(|v| v.is_finite()));
450 assert!(q.iter().any(|&v| v != 0.0));
451
452 let (n, ns) = ld.take("model.norm.weight").expect("take norm");
454 assert_eq!(ns, vec![1536]);
455 assert!(n.iter().all(|v| v.is_finite()));
456
457 let (_h, hs) = ld.take("lm_head.weight").expect("take lm_head");
459 assert_eq!(hs, vec![262144, 1536]);
460
461 let (_qt, qts) = ld
463 .take_transposed("model.layers.0.self_attn.k_proj.weight")
464 .expect("take_t k_proj");
465 assert_eq!(qts, vec![1536, 256]); let (ple, dim) = ld
469 .dequant_embedding_rows("model.embed_tokens_per_layer.weight", &[818, 5279])
470 .expect("ple rows");
471 assert_eq!(dim, 35 * 256);
472 assert_eq!(ple.len(), 2 * 35 * 256);
473 assert!(ple.iter().all(|v| v.is_finite()));
474
475 let fx_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
479 .join("../../fixtures/gemma4_e2b/loader_check.json");
480 if let Ok(raw) = std::fs::read(&fx_path) {
481 let fx: serde_json::Value = serde_json::from_slice(&raw).unwrap();
482 let arr = |k: &str| -> Vec<f32> {
483 fx[k]
484 .as_array()
485 .unwrap()
486 .iter()
487 .map(|v| v.as_f64().unwrap() as f32)
488 .collect()
489 };
490 let approx = |a: &[f32], b: &[f32], tag: &str| {
491 assert_eq!(a.len(), b.len(), "{tag} len");
492 for (i, (x, y)) in a.iter().zip(b).enumerate() {
493 assert!(
494 (x - y).abs() <= 1e-6 * (1.0 + y.abs()),
495 "{tag}[{i}] {x} != {y}"
496 );
497 }
498 };
499 approx(&q[..8], &arr("q_proj_l0_row0_head8"), "q_proj");
501 let n_raw: Vec<f32> = n[..8].iter().map(|x| x + 1.0).collect();
505 approx(&n_raw, &arr("norm_head8"), "norm");
506 approx(&ple[..8], &arr("ple_tok818_first8"), "ple_b0");
508 approx(&ple[256..260], &arr("ple_tok818_block1_first4"), "ple_b1");
509 eprintln!("[qat_loader] exact cross-check vs HF: PASS");
510 } else {
511 eprintln!("[qat_loader] loader_check.json absent — structural checks only");
512 }
513 }
514
515 #[test]
516 fn per_layer_inputs_match_hf() {
517 let Some(dir) = fixture_dir() else {
518 eprintln!("[qat_loader] checkpoint not found — skipping");
519 return;
520 };
521 let ld = GemmaQatLoader::open(&dir).expect("open ckpt");
522 let cfg = crate::config::GemmaConfig::from_file(&dir.join("config.json")).expect("cfg");
523 let ids = [818u32, 5279, 529, 7001, 563];
524 let pli = ld.compute_per_layer_inputs(&cfg, &ids).expect("pli");
525 assert_eq!(pli.len(), 5 * 35 * 256);
526 assert!(pli.iter().all(|v| v.is_finite()));
527
528 let bin = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
530 .join("../../fixtures/gemma4_e2b/per_layer_inputs.bin");
531 if let Ok(raw) = std::fs::read(&bin) {
532 let hf: Vec<f32> = raw
533 .chunks_exact(4)
534 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
535 .collect();
536 assert_eq!(hf.len(), pli.len(), "pli len mismatch");
537 let mut maxd = 0f32;
538 for (a, b) in pli.iter().zip(&hf) {
539 maxd = maxd.max((a - b).abs());
540 }
541 eprintln!("[qat_loader] per_layer_inputs maxdiff vs HF = {maxd:.3e}");
542 assert!(maxd < 5e-3, "per_layer_inputs maxdiff {maxd} too large");
544 } else {
545 eprintln!("[qat_loader] per_layer_inputs.bin absent — structural check only");
546 }
547 }
548}