mod calibration;
mod dump;
mod metrics;
mod quant;
mod tokenize;
use anyhow::Context;
use calibration::{Band, WrongLine, KL_WRONG};
use quant::DominantQuant;
use std::path::{Path, PathBuf};
use std::process::Command;
const DUMPER_CANDIDATES: &[&str] = &["target/llama_logits", ".local-scripts/llama_logits"];
const DUMPER_ENV: &str = "FRINK_LLAMA_LOGITS";
const PROMPT: &str = "The capital of France is";
const KL_NOISE: f64 = 1e-3;
const _: () = {
assert!(
KL_NOISE < KL_WRONG,
"MATCH must be tighter than the tightest WRONG line"
);
};
pub struct ParityArgs {
pub model: String,
pub prompt: Option<String>,
pub prompt_tokens: Option<usize>,
pub top_k: usize,
pub dumper: Vec<String>,
pub dump_logits: Option<String>,
}
pub fn run(args: ParityArgs) -> anyhow::Result<()> {
let path = crate::pull::resolve_model_path(&args.model)?;
let dumpers = dumper_paths(&args.dumper)?;
let tokens_report = tokenize::run(&dumpers[0], Path::new(&path))?;
match &tokens_report {
Some(r) => tokenize::print_report(r),
None => println!(
"tokenizer: SKIPPED — the installed libllama cannot load this checkpoint, so it has \
no tokenization to compare against. The logit comparison below will fail for the \
same reason."
),
}
println!();
let prompt = args.prompt.clone().unwrap_or_else(|| PROMPT.to_string());
let (tokens, frink_logits) = crate::verify_engine::prefill_logits(
Path::new(&path),
&prompt,
frink_models::tokenizer::SpecialTokens::Parse,
args.prompt_tokens,
)
.context("frink prefill")?;
let backend = frink_core::weight_matrix::active_backend();
if backend != frink_core::kernel_registry::Backend::Cpu {
eprintln!(
"parity: frink ran on {}, the reference on llama.cpp CPU — a non-MATCH verdict \
here could be either engine or the backend difference. Run with FRINK_METAL=0 \
FRINK_CUDA=0 to isolate the engines.",
backend.as_str()
);
}
let references = collect_references(&dumpers, &path, &tokens, frink_logits.len())?;
let ref_logits: Vec<&[f32]> = references.iter().map(|r| r.logits.as_slice()).collect();
if let Some(prefix) = args.dump_logits.as_deref() {
for p in dump::write(prefix, &tokens, &ref_logits, &frink_logits)? {
println!("wrote {}", p.display());
}
println!();
}
let gguf = frink_gguf::GgufFile::open(&path).ok();
let quant = DominantQuant::of(gguf.as_ref().map_or(&[][..], |f| &f.tensors));
let band = Band::measure(&ref_logits, &frink_logits, &quant);
let report = compare(ref_logits[0], &frink_logits, args.top_k, &band);
print_report(
&Run {
model: &args.model,
n_tokens: tokens.len(),
top_k: args.top_k,
backend: backend.as_str(),
},
&quant,
&references,
&band,
&report,
);
let mut failures: Vec<String> = Vec::new();
if tokens_report.as_ref().is_some_and(|r| r.diverged()) {
failures.push("frink and llama.cpp tokenize the same text differently".to_string());
}
if report.verdict == Verdict::Wrong {
failures.push(format!(
"frink disagrees with llama.cpp beyond numeric noise (KL {:.3e} nats to its \
nearest reference)",
band.nearest()
));
}
if !failures.is_empty() {
anyhow::bail!("{}", failures.join("; "));
}
Ok(())
}
fn collect_references(
dumpers: &[PathBuf],
model: &str,
tokens: &[u32],
n_vocab: usize,
) -> anyhow::Result<Vec<Reference>> {
let mut out: Vec<Reference> = Vec::new();
for (i, dumper) in dumpers.iter().enumerate() {
let primary = i == 0;
let reference = match reference_logits(dumper, model, tokens, i) {
Ok(r) => r,
Err(e) if primary => return Err(e),
Err(e) => {
eprintln!(
"parity: reference [{i}] {} produced nothing ({e:#}); the WRONG line will be \
measured without it",
dumper.display()
);
continue;
}
};
if reference.logits.len() != n_vocab {
if primary {
anyhow::bail!(
"vocab size disagrees: llama.cpp {} vs frink {n_vocab} — the logit vectors \
are not comparable, fix the loader before reading any metric",
reference.logits.len()
);
}
eprintln!(
"parity: reference [{i}] reports {} vocabulary entries against frink's \
{n_vocab}; dropped from the WRONG line rather than compared to a different \
distribution",
reference.logits.len()
);
continue;
}
out.push(reference);
}
Ok(out)
}
#[derive(Debug, PartialEq, Eq)]
enum Verdict {
Match,
Drift,
TieFlip,
Wrong,
}
struct Report {
verdict: Verdict,
kl_ref_frink: f64,
kl_frink_ref: f64,
total_variation: f64,
max_prob_delta: f64,
top1_ref: usize,
top1_frink: usize,
ref_top1_rank_in_frink: usize,
ref_top2_margin: f64,
ref_top2_logit_gap: f32,
ref_top2_logit_gap_ulps: i64,
frink_gap_on_ref_pair: f32,
topk_overlap: usize,
}
fn compare(ref_logits: &[f32], frink_logits: &[f32], k: usize, band: &Band) -> Report {
let p = metrics::softmax(ref_logits);
let q = metrics::softmax(frink_logits);
let kl_pq = band.kl_to_frink(0);
let kl_qp = metrics::kl(&q, &p);
let (tv, max_delta) = metrics::total_variation(&p, &q);
let ref_order = metrics::order_desc(&p);
let fx_order = metrics::order_desc(&q);
let top1_ref = ref_order[0];
let top1_frink = fx_order[0];
let ref_top1_rank_in_frink = fx_order.iter().position(|&i| i == top1_ref).unwrap_or(0);
let (ref_top2_margin, ref_top2_logit_gap, ref_top2_logit_gap_ulps, frink_gap_on_ref_pair) =
if ref_order.len() > 1 {
let (i1, i2) = (ref_order[0], ref_order[1]);
(
p[i1] - p[i2],
ref_logits[i1] - ref_logits[i2],
metrics::ulps_between(ref_logits[i1], ref_logits[i2]),
frink_logits[i1] - frink_logits[i2],
)
} else {
(1.0, 0.0, 0, 0.0)
};
let k = k.min(ref_order.len());
let ref_top: std::collections::HashSet<usize> = ref_order[..k].iter().copied().collect();
let topk_overlap = fx_order[..k].iter().filter(|i| ref_top.contains(i)).count();
let outside = band.frink_is_outside();
let verdict = if top1_ref == top1_frink {
if kl_pq < KL_NOISE {
Verdict::Match
} else if !outside {
Verdict::Drift
} else {
Verdict::Wrong
}
} else if !outside && ref_top2_margin <= max_delta {
Verdict::TieFlip
} else {
Verdict::Wrong
};
Report {
verdict,
kl_ref_frink: kl_pq,
kl_frink_ref: kl_qp,
total_variation: tv,
max_prob_delta: max_delta,
top1_ref,
top1_frink,
ref_top1_rank_in_frink,
ref_top2_margin,
ref_top2_logit_gap,
ref_top2_logit_gap_ulps,
frink_gap_on_ref_pair,
topk_overlap,
}
}
struct Run<'a> {
model: &'a str,
n_tokens: usize,
top_k: usize,
backend: &'a str,
}
fn print_report(
run: &Run<'_>,
quant: &DominantQuant,
references: &[Reference],
band: &Band,
r: &Report,
) {
let Run {
model,
n_tokens,
top_k: k,
backend,
} = *run;
let name = Path::new(model)
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| model.to_string());
let verdict = match r.verdict {
Verdict::Match => "MATCH",
Verdict::Drift => "DRIFT",
Verdict::TieFlip => "TIE-FLIP",
Verdict::Wrong => "WRONG",
};
println!(
"parity {name}: {verdict} ({n_tokens}-token prompt, first-token distribution, \
frink {backend} vs llama.cpp cpu)"
);
for (i, reference) in references.iter().enumerate() {
let who = match reference.libllama.as_deref() {
Some(p) => p.to_string(),
None => "(this dumper predates the `libllama` line — rebuild with \
tools/build_llama_logits.sh)"
.to_string(),
};
println!(
" reference [{i}] {who} KL(llama||frink) {:.3e}",
band.kl_to_frink(i)
);
}
print_wrong_line(quant, references, band);
println!(
" KL(llama||frink) {:.3e} nats KL(frink||llama) {:.3e} nats [reference 0]",
r.kl_ref_frink, r.kl_frink_ref
);
println!(
" total variation {:.3e} max |delta p| {:.3e}",
r.total_variation, r.max_prob_delta
);
println!(
" top-1 llama {} / frink {} (llama's top-1 is rank {} for frink)",
r.top1_ref, r.top1_frink, r.ref_top1_rank_in_frink
);
println!(
" top-{k} overlap {}/{k} llama top-2 margin {:.3e}",
r.topk_overlap, r.ref_top2_margin
);
println!(
" top-1/top-2 gap {:+.3e} logits ({} ulps) frink on the same pair {:+.3e}",
r.ref_top2_logit_gap, r.ref_top2_logit_gap_ulps, r.frink_gap_on_ref_pair
);
match r.verdict {
Verdict::Match => println!(" same distribution to within f32 accumulation-order noise."),
Verdict::Drift => match quant.label().filter(|_| quant.q8k_dotted()) {
Some(q) => println!(
" same token. The distributions differ by more than summation order, and for \
{q} that is EXPECTED: llama.cpp declares `vec_dot_type = Q8_K` for it, so it \
quantizes the ACTIVATION to 8 bits and accumulates in integers, while frink \
keeps activations in f32. Different arithmetic, with frink on the more \
precise side. See docs/plans/llama-cpp-gap-inventory.md §10."
),
None => println!(
" same token, but the distributions differ by more than accumulation order \
explains — worth a per-layer divergence run before trusting this row."
),
},
Verdict::TieFlip => {
println!(
" top-1 differs, but llama's own top-2 margin ({:.3e}) is under the observed \
per-token noise ({:.3e}): a tie swapped, not a wrong graph.",
r.ref_top2_margin, r.max_prob_delta
);
println!(
" the two candidates are {} ulps apart in llama's logits ({:+.3e} absolute); \
frink puts the same pair {:+.3e} apart, so the flip is a sign change of a gap \
this size — not a redistribution.",
r.ref_top2_logit_gap_ulps, r.ref_top2_logit_gap, r.frink_gap_on_ref_pair
);
}
Verdict::Wrong => println!(
" the graphs disagree. This is not sampling noise and not a tie: something in \
the forward pass differs."
),
}
}
fn print_wrong_line(quant: &DominantQuant, references: &[Reference], band: &Band) {
match band.line() {
WrongLine::Calibrated { spread, line } => {
let (a, b) = spread.between;
println!(
" WRONG line {line:.3e} measured: references [{a}] and [{b}] disagree \
with EACH OTHER by {:.3e} on this checkpoint",
spread.kl
);
println!(
" frink's nearest reference is {:.3e}, {:.0}% of the line",
band.nearest(),
100.0 * band.nearest() / line
);
}
WrongLine::Absolute(line) => println!(
" WRONG line {line:.3e} absolute: llama.cpp's own build-to-build spread on \
{} is 0 to 4.6e-4, two orders under it",
quant.label().unwrap_or("this checkpoint")
),
WrongLine::Uncalibrated => {
println!(
" WRONG line NONE — {} is dotted against Q8_K activations, and two \
builds of llama.cpp disagree with EACH OTHER by up to 3.5e-2 there (#111), so \
no constant can mean `the graphs disagree`.",
quant.label().unwrap_or("this checkpoint")
);
println!(
" Pass --dumper a second time, built against another \
libllama, to measure this checkpoint's own line. ({} reference in this run.)",
references.len()
);
}
}
}
fn dumper_paths(explicit: &[String]) -> anyhow::Result<Vec<PathBuf>> {
if !explicit.is_empty() {
return explicit
.iter()
.map(|e| {
let p = PathBuf::from(e);
if p.exists() {
Ok(p)
} else {
anyhow::bail!("--dumper {} does not exist", p.display())
}
})
.collect();
}
if let Some(e) = std::env::var_os(DUMPER_ENV) {
let p = PathBuf::from(e);
if p.exists() {
return Ok(vec![p]);
}
anyhow::bail!(
"{DUMPER_ENV} points at {}, which does not exist",
p.display()
);
}
for c in DUMPER_CANDIDATES {
let p = PathBuf::from(c);
if p.exists() {
return Ok(vec![p]);
}
}
anyhow::bail!(
"reference dumper not built. It links llama.cpp's own library, so it is built \
separately from the cargo workspace:\n\n ./tools/build_llama_logits.sh\n\n\
(set LLAMA_CPP_PREFIX if llama.cpp is not a Homebrew install, or --dumper / \
{DUMPER_ENV} to point at a binary elsewhere)"
)
}
struct Reference {
logits: Vec<f32>,
libllama: Option<String>,
}
const LIBLLAMA_LINE: &str = "libllama ";
fn reference_logits(
dumper: &Path,
model: &str,
tokens: &[u32],
slot: usize,
) -> anyhow::Result<Reference> {
let out_path =
std::env::temp_dir().join(format!("frink-parity-{}-{slot}.bin", std::process::id()));
let mut cmd = Command::new(dumper);
cmd.arg(model).arg(&out_path);
for t in tokens {
cmd.arg(t.to_string());
}
let out = cmd.output().context("running the reference dumper")?;
if !out.status.success() {
anyhow::bail!(
"reference dumper failed: {}",
String::from_utf8_lossy(&out.stderr)
.lines()
.last()
.unwrap_or("(no stderr)")
);
}
let bytes = std::fs::read(&out_path)
.with_context(|| format!("reading reference logits from {}", out_path.display()))?;
let _ = std::fs::remove_file(&out_path);
if bytes.len() % 4 != 0 {
anyhow::bail!("reference logits file is not a whole number of f32");
}
Ok(Reference {
logits: bytes
.as_chunks::<4>()
.0
.iter()
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect(),
libllama: parse_libllama(&String::from_utf8_lossy(&out.stdout)),
})
}
fn parse_libllama(stdout: &str) -> Option<String> {
stdout
.lines()
.find_map(|l| l.trim().strip_prefix(LIBLLAMA_LINE))
.map(str::trim)
.filter(|p| !p.is_empty() && *p != "unknown")
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
fn one_reference(reference: &[f32], frink: &[f32]) -> Band {
Band::measure(
&[reference],
frink,
&DominantQuant::weigh(Some("Q8_0"), Some("Q8_0")),
)
}
#[test]
fn identical_logits_are_a_match() {
let l = vec![0.1f32, 5.0, -2.0, 3.3];
let r = compare(&l, &l, 3, &one_reference(&l, &l));
assert_eq!(r.verdict, Verdict::Match);
assert!(r.kl_ref_frink < 1e-12, "KL was {}", r.kl_ref_frink);
assert_eq!(r.top1_ref, r.top1_frink);
assert_eq!(r.topk_overlap, 3);
assert_eq!(r.ref_top1_rank_in_frink, 0);
}
#[test]
fn a_different_graph_is_reported_wrong() {
let a = vec![0.0f32, 8.0, 0.0, 0.0];
let b = vec![0.0f32, 0.0, 8.0, 0.0];
let r = compare(&a, &b, 2, &one_reference(&a, &b));
assert_eq!(r.verdict, Verdict::Wrong);
assert_ne!(r.top1_ref, r.top1_frink);
}
#[test]
fn a_near_tie_that_swaps_is_a_tie_flip_not_a_failure() {
let a = vec![-10.0f32, 2.000_01, 2.0];
let b = vec![-10.0f32, 2.0, 2.000_01];
let r = compare(&a, &b, 2, &one_reference(&a, &b));
assert_eq!(r.verdict, Verdict::TieFlip);
assert_ne!(r.top1_ref, r.top1_frink);
assert_eq!(r.topk_overlap, 2);
}
#[test]
fn same_top1_with_a_shifted_tail_is_drift_not_a_match() {
let a = vec![6.0f32, 1.0, 1.0, 1.0];
let b = vec![6.0f32, 1.0, 1.0, 2.0];
let r = compare(&a, &b, 4, &one_reference(&a, &b));
assert_eq!(r.verdict, Verdict::Drift);
assert_eq!(r.top1_ref, r.top1_frink);
assert!(r.kl_ref_frink >= KL_NOISE && r.kl_ref_frink < KL_WRONG);
}
#[test]
fn a_uniform_logit_shift_is_invisible_because_softmax_is_shift_invariant() {
let a = vec![0.5f32, 1.5, -3.0];
let b: Vec<f32> = a.iter().map(|v| v + 7.25).collect();
let r = compare(&a, &b, 3, &one_reference(&a, &b));
assert_eq!(r.verdict, Verdict::Match);
}
#[test]
fn the_reported_gap_is_the_same_pair_on_both_sides_and_flips_sign() {
let a = vec![-10.0f32, 2.000_01, 2.0];
let b = vec![-10.0f32, 2.0, 2.000_01];
let r = compare(&a, &b, 2, &one_reference(&a, &b));
assert_eq!(r.verdict, Verdict::TieFlip);
assert!(
r.ref_top2_logit_gap > 0.0,
"the reference ranks its own pair the right way round"
);
assert!(
r.frink_gap_on_ref_pair < 0.0,
"frink ranks the SAME pair the other way: gap {}",
r.frink_gap_on_ref_pair
);
assert!(
r.ref_top2_logit_gap_ulps > 0 && r.ref_top2_logit_gap_ulps < 1_000,
"a tie must be a handful of ulps, got {}",
r.ref_top2_logit_gap_ulps
);
}
#[test]
fn the_verdict_is_the_same_whichever_reference_the_report_names() {
let a = vec![0.0f32, 4.0, 1.0, 0.5];
let b = vec![0.0f32, 2.5, 1.0, 0.5];
let frink = vec![0.0f32, 3.6, 1.0, 0.5];
let quant = DominantQuant::weigh(Some("Q4K"), Some("Q4K"));
let ab = Band::measure(&[&a, &b], &frink, &quant);
let ba = Band::measure(&[&b, &a], &frink, &quant);
let first = compare(&a, &frink, 4, &ab);
let second = compare(&b, &frink, 4, &ba);
assert_eq!(first.verdict, Verdict::Drift);
assert_eq!(second.verdict, Verdict::Drift);
assert!(first.kl_ref_frink > KL_NOISE && second.kl_ref_frink > KL_NOISE);
assert_ne!(first.kl_ref_frink, second.kl_ref_frink);
let alone = Band::measure(&[&b], &frink, &quant);
let solo = compare(&b, &frink, 4, &alone);
assert!(solo.kl_ref_frink > 3.008e-2, "{}", solo.kl_ref_frink);
assert_eq!(solo.verdict, Verdict::Drift);
}
#[test]
fn the_wrong_rung_is_decided_by_the_nearest_reference_not_the_one_being_reported() {
let a = vec![0.0f32, 4.0, 1.0, 0.5];
let b = vec![0.0f32, 2.0, 1.0, 0.5];
let frink = vec![0.0f32, 3.8, 0.2, 0.5];
let quant = DominantQuant::weigh(Some("Q4K"), Some("Q4K"));
let band = Band::measure(&[&b, &a], &frink, &quant);
let line = band.line().value().expect("two references give a line");
assert!(
band.kl_to_frink(0) > line,
"the fixture must put the PRIMARY over the line, else this proves nothing: {:.4e} \
against {line:.4e}",
band.kl_to_frink(0)
);
assert!(band.nearest() < line);
let r = compare(&b, &frink, 4, &band);
assert_eq!(r.verdict, Verdict::Drift);
assert!(
r.kl_ref_frink > line,
"the printed KL is still the named reference's"
);
}
#[test]
fn a_frink_further_from_every_reference_than_they_are_from_each_other_is_wrong() {
let a = vec![0.0f32, 4.00, 1.0, 0.5];
let b = vec![0.0f32, 3.99, 1.0, 0.5];
let frink = vec![0.0f32, 1.00, 1.0, 0.5];
let quant = DominantQuant::weigh(Some("Q4K"), Some("Q4K"));
let band = Band::measure(&[&a, &b], &frink, &quant);
assert!(band.frink_is_outside());
assert_eq!(compare(&a, &frink, 4, &band).verdict, Verdict::Wrong);
}
#[test]
fn the_reference_identity_is_parsed_and_its_absence_is_not_invented() {
assert_eq!(
parse_libllama("libllama /opt/homebrew/lib/libllama.dylib\nn_vocab 32000\n").as_deref(),
Some("/opt/homebrew/lib/libllama.dylib")
);
assert_eq!(parse_libllama("n_vocab 32000\n"), None);
assert_eq!(parse_libllama("libllama unknown\nn_vocab 32000\n"), None);
assert_eq!(parse_libllama("libllama \n"), None);
}
#[test]
fn every_dumper_is_resolved_and_a_missing_one_is_never_substituted() {
let me = std::env::current_exe().unwrap();
let me = me.to_string_lossy().into_owned();
let resolved = dumper_paths(&[me.clone(), me.clone()]).unwrap();
assert_eq!(resolved.len(), 2, "both references must survive");
let err = dumper_paths(&[me, "/nonexistent/llama_logits".into()])
.unwrap_err()
.to_string();
assert!(
err.contains("/nonexistent/llama_logits"),
"a bad second dumper must name itself, got: {err}"
);
}
}