use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use rlx_core::flow_util::compile_graph_gemma_prefill_with_params;
use rlx_gemma::config::GemmaConfig;
use rlx_gemma::qat_loader::GemmaQatLoader;
use rlx_runtime::Device;
const PROMPT_IDS: &[u32] = &[818, 5279, 529, 7001, 563, 7001];
const BUCKET: usize = 16;
fn ckpt_dir() -> Result<PathBuf> {
if let Some(d) = std::env::var_os("RLX_GEMMA4_E2B_DIR") {
let p = PathBuf::from(d);
if p.join("config.json").is_file() {
return Ok(p);
}
bail!("RLX_GEMMA4_E2B_DIR={p:?} has no config.json");
}
let home = std::env::var_os("HOME").context("HOME not set")?;
let base = std::path::Path::new(&home).join(
".cache/huggingface/hub/\
models--google--gemma-4-E2B-it-qat-mobile-transformers/snapshots",
);
let snap = std::fs::read_dir(&base)?
.flatten()
.next()
.with_context(|| format!("{base:?} empty"))?
.path();
if !snap.join("config.json").is_file() {
bail!("{snap:?} has no config.json");
}
Ok(snap)
}
#[derive(Clone)]
struct TapStats {
len: usize,
nan: usize,
min: f32,
max: f32,
mean: f64,
first4: [f32; 4],
}
fn stats(v: &[f32]) -> TapStats {
let mut nan = 0;
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
let mut sum = 0f64;
let mut nf = 0u64;
for &x in v {
if x.is_nan() {
nan += 1;
} else {
if x < min {
min = x;
}
if x > max {
max = x;
}
sum += x as f64;
nf += 1;
}
}
let mean = if nf > 0 { sum / nf as f64 } else { 0.0 };
let mut first4 = [0f32; 4];
for (i, &x) in v.iter().take(4).enumerate() {
first4[i] = x;
}
TapStats {
len: v.len(),
nan,
min,
max,
mean,
first4,
}
}
fn run_on(device: Device, ple: &[f32], ids_f32: &[f32]) -> Result<Vec<Vec<f32>>> {
let dir = ckpt_dir()?;
let cfg = GemmaConfig::from_file(&dir.join("config.json"))?;
let mut bld = GemmaQatLoader::open(&dir)?;
let mut packed = HashMap::new();
let (graph, params) = rlx_gemma::builder::build_gemma_graph_sized_packed_ext(
&cfg,
&mut bld,
1,
BUCKET,
true,
false,
false,
&mut packed,
None,
None,
)?;
let mut compiled = compile_graph_gemma_prefill_with_params(device, graph, params)?;
let outs = compiled.run(&[("input_ids", ids_f32), ("per_layer_inputs", ple)]);
Ok(outs)
}
const TAP_NAMES: &[&str] = &[
"tap01_embed_scale",
"tap02_input_layernorm",
"tapA_Q_post_proj",
"tapB_K_post_proj",
"tapC_V_post_proj",
"tapD_Q_4d_pre_norm",
"tapE_Q_4d_post_norm",
"tap03_Q_post_norm_3d",
"tap04_K_post_norm_3d",
"tap05_V_post_norm_3d",
"tap06_Q_post_rope",
"tap07_K_post_rope",
"tapF_K_rep",
"tapG_V_rep",
"tap08_attn_out",
"tap09_attn_out_post_norm",
"tap10_residual_post_attn",
"tap11_layer0_final_h",
];
fn main() -> Result<()> {
if std::env::var("RLX_TAP_L0").ok().is_none() {
bail!("set RLX_TAP_L0=1 to enable the layer-0 taps");
}
let dir = ckpt_dir()?;
let cfg = GemmaConfig::from_file(&dir.join("config.json"))?;
let loader = GemmaQatLoader::open(&dir)?;
let mut ids = vec![0u32; BUCKET];
for (i, &t) in PROMPT_IDS.iter().enumerate() {
ids[i] = t;
}
let ple = loader.compute_per_layer_inputs(&cfg, &ids)?;
let ids_f32: Vec<f32> = ids.iter().map(|&i| i as f32).collect();
println!("→ tap diff on prompt {PROMPT_IDS:?} (bucket={BUCKET})");
println!(
"→ PLE len={}, expected={}",
ple.len(),
BUCKET * cfg.num_hidden_layers * cfg.ple_width()
);
let devices: Vec<(Device, &str)> = [
(Device::Cpu, "Cpu"),
(Device::Metal, "Metal"),
(Device::Mlx, "Mlx"),
]
.into_iter()
.filter(|(d, _)| rlx_runtime::is_available(*d))
.collect();
let mut all_outs: Vec<(String, Vec<Vec<f32>>)> = Vec::new();
for &(d, label) in &devices {
let t = std::time::Instant::now();
match run_on(d, &ple, &ids_f32) {
Ok(outs) => {
println!(
" [{label}] ran in {:.2}s, {} outputs",
t.elapsed().as_secs_f32(),
outs.len()
);
all_outs.push((label.into(), outs));
}
Err(e) => {
println!(" [{label}] FAILED: {e}");
}
}
}
if all_outs.is_empty() {
bail!("no successful runs");
}
let n_outs = all_outs[0].1.len();
println!(
"→ {} outputs total (1 logits + {} taps)",
n_outs,
n_outs - 1
);
{
let vocab = cfg.vocab_size;
println!("\n── Per-position argmax (all {BUCKET} positions) ──");
let amx = |row: &[f32]| {
row.iter()
.enumerate()
.filter(|(_, v)| v.is_finite())
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(i, _)| i)
.unwrap_or(0)
};
for pos in 0..BUCKET {
let mut line = format!(" pos {pos:>2}:");
let mut cpu_ax = None;
for (label, outs) in &all_outs {
let row = &outs[0][pos * vocab..(pos + 1) * vocab];
let ax = amx(row);
if label == "Cpu" {
cpu_ax = Some(ax);
}
let flag = if cpu_ax.is_some() && Some(ax) != cpu_ax {
" ✗"
} else {
""
};
line.push_str(&format!(" {label}={ax:>6}{flag}"));
}
println!("{line}");
}
}
println!(
"\n── Last-token logits (tok#{}, argmax) ──",
PROMPT_IDS.len() - 1
);
let vocab = cfg.vocab_size;
for (label, outs) in &all_outs {
let logits = &outs[0];
let last_pos = PROMPT_IDS.len() - 1;
let row = &logits[last_pos * vocab..(last_pos + 1) * vocab];
let s = stats(row);
let ax = row
.iter()
.enumerate()
.filter(|(_, v)| v.is_finite())
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(i, _)| i)
.unwrap_or(0);
println!(
" [{label:>5}] argmax={ax:>6} min={:.3e} mean={:.3e} max={:.3e} nan={} first4={:?}",
s.min, s.mean, s.max, s.nan, s.first4
);
}
println!("\n── Tap-by-tap stats (length, NaN, min, mean, max, first 4 vals) ──");
let n_taps = n_outs.saturating_sub(1);
for i in 0..n_taps {
let name = TAP_NAMES.get(i).copied().unwrap_or("<extra>");
println!("\n {name} (output[{}])", i + 1);
for (label, outs) in &all_outs {
if let Some(t) = outs.get(i + 1) {
let s = stats(t);
println!(
" [{label:>5}] len={:>7} nan={:>3} min={:+.3e} mean={:+.3e} max={:+.3e} first4={:?}",
s.len, s.nan, s.min, s.mean, s.max, s.first4
);
}
}
}
println!("\n── Tap full-tensor L2 vs CPU ──");
if let Some(ci) = all_outs.iter().position(|(l, _)| l == "Cpu") {
let cpu_outs = &all_outs[ci].1;
for tap_i in 0..n_taps {
let name = TAP_NAMES.get(tap_i).copied().unwrap_or("<extra>");
let Some(c) = cpu_outs.get(tap_i + 1) else {
continue;
};
for (label, outs) in &all_outs {
if label == "Cpu" {
continue;
}
let Some(g) = outs.get(tap_i + 1) else {
continue;
};
let n = c.len().min(g.len());
let l2: f64 = c[..n]
.iter()
.zip(&g[..n])
.map(|(a, b)| (*a as f64 - *b as f64).powi(2))
.sum::<f64>()
.sqrt();
println!(" {name:>24} [{label:>5}] L2={l2:.3e}");
}
}
}
println!("\n── First divergent tap (where Metal or Mlx first differs from Cpu) ──");
let cpu_idx = all_outs.iter().position(|(l, _)| l == "Cpu");
if let Some(ci) = cpu_idx {
let cpu_outs = &all_outs[ci].1;
for (label, outs) in &all_outs {
if label == "Cpu" {
continue;
}
let mut first_diverge: Option<(usize, &str, f32, f32)> = None;
for tap_i in 0..n_taps {
let Some(c) = cpu_outs.get(tap_i + 1) else {
continue;
};
let Some(g) = outs.get(tap_i + 1) else {
continue;
};
let n = c.len().min(g.len());
for j in 0..n.min(4096) {
let a = c[j];
let b = g[j];
let diff = (a - b).abs();
let scale = a.abs().max(b.abs()).max(1e-6);
if diff / scale > 0.01 && diff > 1e-3 {
first_diverge =
Some((tap_i, TAP_NAMES.get(tap_i).copied().unwrap_or("?"), a, b));
break;
}
}
if first_diverge.is_some() {
break;
}
}
match first_diverge {
Some((i, name, cv, gv)) => {
println!(
" [{label}] first diverges at tap #{i} ({name}): cpu={cv} vs {label}={gv}"
);
}
None => {
println!(" [{label}] matches Cpu within tolerance on all checked taps");
}
}
}
}
Ok(())
}