#![allow(dead_code)]
use ferrox_core::cache::KvCache;
use ferrox_models::{Decoder, ModelConfig};
use std::path::{Path, PathBuf};
pub fn assert_close(got: &[f32], want: &[f32], tol: f32, what: &str) {
assert_eq!(got.len(), want.len(), "{what}: logit count");
let worst = got
.iter()
.zip(want.iter())
.map(|(a, b)| (a - b).abs())
.fold(0f32, f32::max);
assert!(
worst <= tol,
"{what}: max |ferrox - llama.cpp| = {worst} > {tol}\n ferrox: {:?}\n llama: {:?}",
&got[..8.min(got.len())],
&want[..8.min(want.len())]
);
}
pub fn collect_gguf(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
collect_gguf(&p, out);
} else if p.extension().and_then(|x| x.to_str()) == Some("gguf") {
out.push(p);
}
}
}
pub const GRAPH_PROMPT: [usize; 6] = [3, 7, 11, 19, 23, 5];
pub const GRAPH_TOL: f32 = 1e-5;
pub const GELU_TABLE_TOL: f32 = 2e-4;
pub fn graph_fixture_path(name: &str) -> String {
format!(
"{}/tests/fixtures/{name}_tiny.gguf",
env!("CARGO_MANIFEST_DIR")
)
}
pub fn load_graph_fixture(name: &str) -> Decoder {
let path = graph_fixture_path(name);
let file = ferrox_gguf::GgufFile::open(&path).expect("fixture opens");
let config = ModelConfig::from_gguf(&file).expect("fixture config parses");
Decoder::from_gguf(&path, config).expect("fixture loads")
}
pub fn graph_caches(decoder: &Decoder) -> Vec<KvCache> {
decoder
.layers
.iter()
.map(|_| KvCache::new(decoder.config.n_kv_heads, decoder.config.head_dim))
.collect()
}
pub fn assert_all_three_paths_match(name: &str, golden: &[f32]) {
assert_all_three_paths_match_within(name, golden, GRAPH_TOL);
}
pub fn assert_all_three_paths_match_within(name: &str, golden: &[f32], tol: f32) {
let decoder = load_graph_fixture(name);
let mut kv = graph_caches(&decoder);
assert_close(
&decoder.forward_batch_last(&GRAPH_PROMPT, 0, &mut kv),
golden,
tol,
&format!("{name}: prefill (forward_batch_last)"),
);
let mut kv = graph_caches(&decoder);
let mut out = Vec::new();
for (pos, &tok) in GRAPH_PROMPT.iter().enumerate() {
out = decoder.forward_token(tok, pos, &mut kv);
}
assert_close(&out, golden, tol, &format!("{name}: decode"));
let mut kv = vec![graph_caches(&decoder)];
let mut out = Vec::new();
for (pos, &tok) in GRAPH_PROMPT.iter().enumerate() {
let batch = decoder.forward_multi_seq(&[tok], &[pos], &mut kv);
out = batch.into_iter().next().unwrap();
}
assert_close(&out, golden, tol, &format!("{name}: multi-seq"));
}
pub fn worst_vs(got: &[f32], golden: &[f32]) -> f32 {
got.iter()
.zip(golden.iter())
.map(|(a, b)| (a - b).abs())
.fold(0f32, f32::max)
}
pub fn model_dir() -> PathBuf {
if let Ok(d) = std::env::var("FERROX_TEST_MODELS_DIR") {
return PathBuf::from(d);
}
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../models")
}
pub fn argmax(logits: &[f32]) -> usize {
logits
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(i, _)| i)
.unwrap()
}
fn stretch(mut tokens: Vec<usize>, want: usize, bos: Option<usize>) -> Vec<usize> {
if tokens.len() >= want {
tokens.truncate(want);
return tokens;
}
let leading_bos = (bos.is_some() && tokens.first().copied() == bos).then(|| tokens[0]);
let body: Vec<usize> = tokens[leading_bos.iter().count()..].to_vec();
tokens.truncate(leading_bos.iter().count());
while tokens.len() < want {
let take = (want - tokens.len()).min(body.len());
tokens.extend_from_slice(&body[..take]);
}
tokens
}
pub fn prompt_tokens(file: &ferrox_gguf::ShardedGguf, prompt: &str, want: usize) -> Vec<usize> {
use ferrox_models::tokenizer::{GgufBpeTokenizer, GgufSpmTokenizer, SpecialTokens};
let raw: Vec<usize> = match file.metadata_str("tokenizer.ggml.model") {
Some("gpt2" | "gemma4") => GgufBpeTokenizer::from_gguf(file)
.expect("bpe tokenizer")
.encode(prompt, SpecialTokens::Parse)
.into_iter()
.map(|i| i as usize)
.collect(),
Some("llama") => GgufSpmTokenizer::from_gguf(file)
.expect("spm tokenizer")
.encode(prompt, SpecialTokens::Parse)
.into_iter()
.map(|i| i as usize)
.collect(),
other => panic!("this suite does not cover tokenizer {other:?}"),
};
let bos = file
.metadata_u64("tokenizer.ggml.bos_token_id")
.map(|v| v as usize);
let mut tokens = raw;
if ferrox_models::tokenizer::should_add_bos_token(file) {
if let Some(b) = bos {
if tokens.first() != Some(&b) {
tokens.insert(0, b);
}
}
}
stretch(tokens, want, bos)
}