mod common;
use std::path::PathBuf;
use common::Rng;
use kime_cpu::{CpuBackend, executor, par};
use kime_model::Model;
use kime_tensor::{BatchBuf, Executor, Outputs, Rows};
use serde_json::Value;
type Q = (Vec<u32>, Vec<u32>, u8);
fn questions() -> Vec<Q> {
let path = format!("{}/../kime-eval/fixtures/parity/laya.jsonl", env!("CARGO_MANIFEST_DIR"));
let mut out = Vec::new();
for line in std::fs::read_to_string(path).unwrap().lines() {
let case: Value = serde_json::from_str(line).unwrap();
for q in case["questions"].as_array().into_iter().flatten() {
let ids = |k: &str| -> Vec<u32> {
q[k].as_array().unwrap().iter().map(|x| x.as_u64().unwrap() as u32).collect()
};
out.push((ids("ids"), ids("markers"), q["qtype"].as_u64().unwrap() as u8));
}
}
out
}
fn run(exec: &mut Executor<CpuBackend>, qs: &[&Q]) -> Vec<(Vec<u32>, [u32; 2])> {
let (mut buf, mut out) = (BatchBuf::default(), Outputs::default());
for q in qs {
buf.push(&q.0, &q.1, q.2);
}
exec.run(&buf.batch(), &mut out).unwrap();
let mut at = 0;
qs.iter()
.zip(&out.act)
.map(|(q, a)| {
let l = out.logits[at..at + q.1.len()].iter().map(|x| x.to_bits()).collect();
at += q.1.len();
(l, a.map(f32::to_bits))
})
.collect()
}
fn steps(exec: &mut Executor<CpuBackend>, qs: &[&Q], at: usize) -> Vec<(&'static str, Vec<f32>)> {
for p in exec.plans_mut() {
p.dump();
}
let (mut buf, mut out) = (BatchBuf::default(), Outputs::default());
for q in qs {
buf.push(&q.0, &q.1, q.2);
}
let bucket = exec.run(&buf.batch(), &mut out).unwrap();
let plan = exec.plans_mut().find(|p| p.bucket() == bucket).unwrap();
let tok: usize = qs[..at].iter().map(|q| q.0.len()).sum();
let mark: usize = qs[..at].iter().map(|q| q.1.len()).sum();
let (q, mut kept) = (qs[at], Vec::new());
for d in plan.dumps() {
let (lo, n) = match d.rows {
Rows::Tokens => (tok, q.0.len()),
Rows::Seqs => (at, 1),
Rows::Markers => (mark, q.1.len()),
};
kept.push((d.name, d.data[lo * d.width..(lo + n) * d.width].to_vec()));
}
kept
}
fn explain(exec: &mut Executor<CpuBackend>, batch: &[&Q], at: usize) -> String {
let alone = steps(exec, &batch[at..=at], 0);
let inside = steps(exec, batch, at);
for (i, (a, b)) in alone.iter().zip(&inside).enumerate() {
let diff = a.1.iter().zip(&b.1).position(|(x, y)| x.to_bits() != y.to_bits());
if let Some(j) = diff {
return format!(
"step {i} ({}) first differs at value {j}: {} alone, {} in the batch",
a.0, a.1[j], b.1[j]
);
}
}
"no step differs when run again, so the difference is not repeatable".into()
}
#[test]
fn same_bits_alone_in_batches_and_after_other_work() {
let dir = std::env::var_os("KIME_MODELS").map(|m| PathBuf::from(m).join("laya"));
let Some(dir) = dir.filter(|d| d.join("model.safetensors").is_file()) else {
assert!(std::env::var_os("KIME_REQUIRE_WEIGHTS").is_none(), "no Laya weights");
eprintln!("skipping: set KIME_MODELS to a folder holding laya/ with its weights");
return;
};
let model = Model::open(&dir).unwrap();
let mut exec = executor(&model, par::available()).unwrap();
let qs = questions();
let refs: Vec<&Q> = qs.iter().collect();
let alone: Vec<_> = refs.iter().map(|q| run(&mut exec, &[q]).remove(0)).collect();
let mut rng = Rng(16);
let mut order: Vec<usize> = (0..qs.len()).collect();
for round in 0..20 {
for i in (1..order.len()).rev() {
order.swap(i, rng.below(i + 1));
}
let mut start = 0;
while start < order.len() {
let end = (start + 1 + rng.below(32)).min(order.len());
let batch: Vec<&Q> = order[start..end].iter().map(|&i| refs[i]).collect();
for (k, got) in run(&mut exec, &batch).iter().enumerate() {
let i = order[start + k];
if *got != alone[i] {
let why = explain(&mut exec, &batch, k);
panic!(
"round {round}: question {i} differs in a batch of {}: {why}",
batch.len()
);
}
}
start = end;
}
}
for i in 0..qs.len() {
let other: Vec<&Q> = (0..1 + rng.below(16)).map(|_| refs[rng.below(qs.len())]).collect();
run(&mut exec, &other);
let got = run(&mut exec, &[refs[i]]).remove(0);
assert!(got == alone[i], "question {i} differs after other work");
}
}