use std::path::{Path, PathBuf};
use std::process::Command;
use ferrox_core::cache::KvCache;
use ferrox_gguf::ShardedGguf;
use ferrox_models::config::ModelConfig;
use ferrox_models::decoder::Decoder;
use ferrox_models::tokenizer::GgufBpeTokenizer;
const PROMPT: &str = "The capital of France is";
const MAX_NEW_TOKENS: usize = 12;
const CHILD: &str = "FERROX_TEST_CPU_POOL_CHILD";
const CHILD_GGUF: &str = "FERROX_TEST_CPU_POOL_GGUF";
const MARKER: &str = "FERROX_TOKENS";
fn models_dir() -> PathBuf {
if let Some(dir) = std::env::var_os("FERROX_TEST_MODELS_DIR") {
return PathBuf::from(dir);
}
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../models")
}
fn candidates() -> Vec<PathBuf> {
let dir = models_dir();
[
"hf_test/SmolLM2-135M-Instruct-Q8_0.gguf",
"hf_test/SmolLM2-135M-Instruct-Q4_K_M.gguf",
]
.iter()
.map(|name| dir.join(name))
.filter(|path| path.exists())
.collect()
}
fn argmax(logits: &[f32]) -> usize {
logits
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are finite"))
.map(|(i, _)| i)
.expect("non-empty logits")
}
fn greedy_tokens(path: &Path) -> Vec<usize> {
let file = ShardedGguf::open(path).expect("open GGUF");
let config = ModelConfig::from_gguf(&file).expect("model config");
let tok = GgufBpeTokenizer::from_gguf(&file).expect("tokenizer");
let decoder = Decoder::from_gguf(path, config.clone()).expect("load decoder");
let prompt: Vec<usize> = tok.encode(PROMPT).into_iter().map(|t| t as usize).collect();
let mut caches: Vec<KvCache> = (0..config.n_layers)
.map(|_| KvCache::new(config.n_kv_heads, config.head_dim))
.collect();
let logits = decoder.forward_batch(&prompt, 0, &mut caches);
let mut last = logits.last().expect("non-empty prompt").clone();
let mut generated = Vec::with_capacity(MAX_NEW_TOKENS);
for step in 0..MAX_NEW_TOKENS {
let next = argmax(&last);
generated.push(next);
last = decoder.forward_token(next, prompt.len() + step, &mut caches);
}
generated
}
fn tokens_from_child(path: &Path, backend: &str) -> Vec<usize> {
let exe = std::env::current_exe().expect("test binary path");
let out = Command::new(exe)
.args(["--exact", "generates_tokens_for_the_parent", "--nocapture"])
.env(CHILD, "1")
.env(CHILD_GGUF, path)
.env("FERROX_CPU_POOL", backend)
.env("FERROX_CPU_INT_DOT", "1")
.env("FERROX_TEST_MODELS_DIR", models_dir())
.output()
.expect("spawn child test process");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
out.status.success(),
"child ({backend}) failed: {}\n{}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let line = stdout
.lines()
.find_map(|l| l.strip_prefix(MARKER))
.unwrap_or_else(|| panic!("child ({backend}) printed no {MARKER} line:\n{stdout}"));
line.split_whitespace()
.map(|t| t.parse::<usize>().expect("token id"))
.collect()
}
#[test]
fn generates_tokens_for_the_parent() {
if std::env::var_os(CHILD).is_none() {
return;
}
let path = PathBuf::from(std::env::var_os(CHILD_GGUF).expect("child needs a GGUF path"));
let tokens = greedy_tokens(&path);
let ids: Vec<String> = tokens.iter().map(|t| t.to_string()).collect();
println!("{MARKER} {}", ids.join(" "));
}
#[test]
fn the_two_cpu_schedulers_produce_token_identical_output() {
let models = candidates();
if models.is_empty() {
eprintln!(
"skip: no checkpoint under {} -- this suite needs a real GGUF",
models_dir().display()
);
return;
}
for path in models {
let rayon = tokens_from_child(&path, "rayon");
let spin = tokens_from_child(&path, "spin");
assert_eq!(
rayon.len(),
MAX_NEW_TOKENS,
"{}: the rayon arm generated nothing to compare",
path.display()
);
assert_eq!(
rayon,
spin,
"{}: the persistent pool and rayon disagree about the greedy continuation",
path.display()
);
}
}