use std::path::PathBuf;
use crate::models::lazy_quantized_qwen3::QuantizedQwen3Model;
use crate::models::lazy_qwen3::Qwen3Config;
use fuel_loaders::quantized::gguf_file::Value as GgufValue;
use fuel_loaders::quantized::gguf_mmap::MmapedContent;
use serde::{Deserialize, Serialize};
pub const DECODE_LEN: usize = 32;
pub const TOP_K: usize = 16;
pub const PROMPTS: &[&str] = &[
"Write a Rust function to calculate the factorial of a given number.",
"The capital of France is",
];
pub const MODEL_REPO: &str = "unsloth/Qwen3-0.6B-GGUF";
pub const MODEL_FILE: &str = "Qwen3-0.6B-Q4_K_M.gguf";
pub const MODEL_REVISION: &str = "main";
pub const TOKENIZER_REPO: &str = "Qwen/Qwen3-0.6B";
pub const FIXTURE_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/qwen3_0_6b_cpu_reference.json"
);
#[derive(Clone, Copy, Debug)]
pub enum DtypeTier {
F32F32,
F16OrQuantized,
}
impl DtypeTier {
pub fn relative_bound(self) -> f32 {
match self {
DtypeTier::F32F32 => 1e-3,
DtypeTier::F16OrQuantized => 2e-2,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct StepRecord {
pub token: u32,
pub max_abs_logit: f32,
pub top_logits: Vec<(u32, f32)>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PromptRun {
pub prompt: String,
pub steps: Vec<StepRecord>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ReferenceFixture {
pub model_repo: String,
pub model_file: String,
pub decode_len: usize,
pub top_k: usize,
pub rerun_spread_max_abs_delta: f32,
pub runs: Vec<PromptRun>,
}
pub fn qwen3_cfg_from_gguf(mc: &MmapedContent) -> anyhow::Result<Qwen3Config> {
let md = mc.metadata();
let get = |k: &str| -> anyhow::Result<&GgufValue> {
md.get(k)
.ok_or_else(|| anyhow::anyhow!("gguf metadata: missing key {k:?}"))
};
let num_attention_heads = get("qwen3.attention.head_count")?.to_u32()? as usize;
let num_key_value_heads = get("qwen3.attention.head_count_kv")?.to_u32()? as usize;
let head_dim = get("qwen3.attention.key_length")?.to_u32()? as usize;
let num_hidden_layers = get("qwen3.block_count")?.to_u32()? as usize;
let hidden_size = get("qwen3.embedding_length")?.to_u32()? as usize;
let intermediate_size = get("qwen3.feed_forward_length")?.to_u32()? as usize;
let max_position_embeddings = get("qwen3.context_length")?.to_u32()? as usize;
let rms_norm_eps = get("qwen3.attention.layer_norm_rms_epsilon")?.to_f32()? as f64;
let rope_theta = get("qwen3.rope.freq_base")?.to_f32()? as f64;
let vocab_size = match md.get("qwen3.vocab_size") {
Some(v) => v.to_u32()? as usize,
None => {
let info = mc
.content()
.tensor_infos
.get("token_embd.weight")
.ok_or_else(|| anyhow::anyhow!("gguf: missing token_embd.weight"))?;
let dims = info.shape.dims();
if dims.is_empty() {
anyhow::bail!("gguf: token_embd.weight has empty shape");
}
dims[0]
}
};
let tie_word_embeddings = !mc.content().tensor_infos.contains_key("output.weight");
Ok(Qwen3Config {
vocab_size,
hidden_size,
intermediate_size,
num_hidden_layers,
num_attention_heads,
num_key_value_heads,
head_dim,
max_position_embeddings,
sliding_window: None,
max_window_layers: 0,
use_sliding_window: false,
rope_theta,
rms_norm_eps,
attention_bias: false,
tie_word_embeddings,
})
}
pub fn load_model() -> anyhow::Result<(QuantizedQwen3Model, tokenizers::Tokenizer, Qwen3Config)> {
let api = hf_hub::api::sync::Api::new()?;
let model_path: PathBuf = api
.repo(hf_hub::Repo::with_revision(
MODEL_REPO.to_string(),
hf_hub::RepoType::Model,
MODEL_REVISION.to_string(),
))
.get(MODEL_FILE)?;
let tokenizer_path = api
.model(TOKENIZER_REPO.to_string())
.get("tokenizer.json")?;
let tokenizer = tokenizers::Tokenizer::from_file(tokenizer_path).map_err(anyhow::Error::msg)?;
let mmaped = MmapedContent::from_path(&model_path)?;
let cfg = qwen3_cfg_from_gguf(&mmaped)?;
drop(mmaped);
let model = QuantizedQwen3Model::from_gguf(&model_path, &cfg)
.map_err(|e| anyhow::anyhow!("from_gguf: {e}"))?;
Ok((model, tokenizer, cfg))
}
pub fn run_greedy(
model: &QuantizedQwen3Model,
tokenizer: &tokenizers::Tokenizer,
cfg: &Qwen3Config,
prompt: &str,
) -> anyhow::Result<PromptRun> {
let formatted = format!("<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n");
let encoding = tokenizer
.encode(formatted, true)
.map_err(anyhow::Error::msg)?;
let prompt_tokens = encoding.get_ids();
let vocab_size = cfg.vocab_size;
let slice_last = |flat: Vec<f32>, seq: usize| -> Vec<f32> {
let off = (seq - 1) * vocab_size;
flat[off..off + vocab_size].to_vec()
};
let record_step = |logits: &[f32]| -> StepRecord {
let token = logits
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits must not be NaN"))
.map(|(idx, _)| idx as u32)
.expect("logits must be non-empty");
let max_abs_logit = logits.iter().fold(0f32, |m, &v| m.max(v.abs()));
let mut indexed: Vec<(u32, f32)> = logits
.iter()
.enumerate()
.map(|(i, &v)| (i as u32, v))
.collect();
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("logits must not be NaN"));
indexed.truncate(TOP_K);
StepRecord {
token,
max_abs_logit,
top_logits: indexed,
}
};
let mut all_tokens: Vec<u32> = prompt_tokens.to_vec();
let logits_flat = model
.forward(&all_tokens, 0)
.map_err(|e| anyhow::anyhow!("forward: {e}"))?
.realize_f32();
let mut logits = slice_last(logits_flat, all_tokens.len());
let mut steps = Vec::with_capacity(DECODE_LEN);
let first = record_step(&logits);
let mut next_token = first.token;
steps.push(first);
for _ in 1..DECODE_LEN {
all_tokens.push(next_token);
let logits_flat = model
.forward(&all_tokens, 0)
.map_err(|e| anyhow::anyhow!("forward: {e}"))?
.realize_f32();
logits = slice_last(logits_flat, all_tokens.len());
let step = record_step(&logits);
next_token = step.token;
steps.push(step);
}
Ok(PromptRun {
prompt: prompt.to_string(),
steps,
})
}
pub enum CandidateLogits<'a> {
Full(&'a [f32]),
TopK(&'a [(u32, f32)]),
}
impl CandidateLogits<'_> {
pub fn get(&self, vocab_id: u32) -> f32 {
match self {
Self::Full(row) => row
.get(vocab_id as usize)
.copied()
.unwrap_or(f32::NEG_INFINITY),
Self::TopK(pairs) => pairs
.iter()
.find(|&&(id, _)| id == vocab_id)
.map(|&(_, logit)| logit)
.unwrap_or(f32::NEG_INFINITY),
}
}
}
pub fn compare_step(
i: usize,
candidate_token: u32,
candidate_logits: &CandidateLogits<'_>,
reference: &StepRecord,
rel_bound: f32,
excused_near_ties: &mut usize,
) -> Result<(), String> {
let bound = rel_bound * reference.max_abs_logit.max(f32::MIN_POSITIVE);
for &(vocab_id, r_logit) in &reference.top_logits {
let c_logit = candidate_logits.get(vocab_id);
let diff = (c_logit - r_logit).abs();
if diff > bound {
return Err(format!(
"step {i}: vocab {vocab_id} logit diff {diff} exceeds bound {bound} \
(candidate={c_logit} reference={r_logit})"
));
}
}
if candidate_token != reference.token {
let margin = if reference.top_logits.len() >= 2 {
reference.top_logits[0].1 - reference.top_logits[1].1
} else {
f32::INFINITY
};
if margin < bound && *excused_near_ties == 0 {
*excused_near_ties += 1;
return Ok(());
}
return Err(format!(
"step {i}: greedy token mismatch: candidate={} reference={} \
(top1/top2 margin={margin}, bound={bound}, already excused={excused_near_ties})",
candidate_token, reference.token
));
}
Ok(())
}
pub fn compare_runs(
candidate: &PromptRun,
reference: &PromptRun,
tier: DtypeTier,
) -> Result<(), String> {
if candidate.steps.len() != reference.steps.len() {
return Err(format!(
"step count mismatch: candidate={} reference={}",
candidate.steps.len(),
reference.steps.len()
));
}
let rel_bound = tier.relative_bound();
let mut excused_near_ties = 0usize;
for (i, (c, r)) in candidate
.steps
.iter()
.zip(reference.steps.iter())
.enumerate()
{
compare_step(
i,
c.token,
&CandidateLogits::TopK(&c.top_logits),
r,
rel_bound,
&mut excused_near_ties,
)?;
}
Ok(())
}
pub fn load_fixture() -> ReferenceFixture {
let bytes = std::fs::read(FIXTURE_PATH).unwrap_or_else(|e| {
panic!(
"{FIXTURE_PATH} is missing or unreadable ({e}) — run \
`cargo run -p fuel-transformers --features dev-tools --example \
qwen3_yardstick_tool -- regenerate` once (needs network) to produce it"
)
});
serde_json::from_slice(&bytes).expect("fixture JSON must deserialize as ReferenceFixture")
}
pub fn regenerate_qwen3_cpu_reference_fixture() {
let (model, tokenizer, cfg) = load_model().expect("load_model");
unsafe {
std::env::set_var("RAYON_NUM_THREADS", "1");
}
let single_threaded =
run_greedy(&model, &tokenizer, &cfg, PROMPTS[0]).expect("run_greedy (RAYON_NUM_THREADS=1)");
unsafe {
std::env::remove_var("RAYON_NUM_THREADS");
}
let runs: Vec<PromptRun> = PROMPTS
.iter()
.map(|p| run_greedy(&model, &tokenizer, &cfg, p).expect("run_greedy (default threads)"))
.collect();
let mut spread = 0f32;
for (a, b) in runs[0].steps.iter().zip(single_threaded.steps.iter()) {
for &(vid, b_logit) in &b.top_logits {
let a_logit = CandidateLogits::TopK(&a.top_logits).get(vid);
if a_logit.is_finite() {
spread = spread.max((a_logit - b_logit).abs());
}
}
}
let bound = DtypeTier::F16OrQuantized.relative_bound() * runs[0].steps[0].max_abs_logit;
assert!(
spread * 4.0 <= bound,
"reduction-order spread (RAYON_NUM_THREADS=1 vs default) {spread} is not ≤ 1/4 of \
the tolerance bound {bound} — the tolerance table in \
docs/joint-gpu-milestone-plan.md §5.1 needs re-measuring before this fixture can \
be trusted, per the plan's own calibration rule"
);
let fixture = ReferenceFixture {
model_repo: MODEL_REPO.to_string(),
model_file: MODEL_FILE.to_string(),
decode_len: DECODE_LEN,
top_k: TOP_K,
rerun_spread_max_abs_delta: spread,
runs,
};
let json = serde_json::to_string_pretty(&fixture).expect("serialize fixture");
std::fs::write(FIXTURE_PATH, json).expect("write fixture");
println!(
"wrote {FIXTURE_PATH}: {} prompts x {DECODE_LEN} steps, rerun spread {spread}",
fixture.runs.len()
);
}
pub fn qwen3_cpu_speed_harness() {
let (model, tokenizer, cfg) = load_model().expect("load_model");
let prompt = PROMPTS[0];
let _ = run_greedy(&model, &tokenizer, &cfg, prompt).expect("warm-up run_greedy");
let formatted = format!("<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n");
let encoding = tokenizer.encode(formatted, true).expect("encode");
let prompt_tokens = encoding.get_ids().to_vec();
let prefill_start = std::time::Instant::now();
let logits_flat = model
.forward(&prompt_tokens, 0)
.expect("forward")
.realize_f32();
let prefill_dt = prefill_start.elapsed();
let vocab_size = cfg.vocab_size;
let off = (prompt_tokens.len() - 1) * vocab_size;
let mut logits = logits_flat[off..off + vocab_size].to_vec();
let mut next_token = logits
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(i, _)| i as u32)
.unwrap();
let decode_start = std::time::Instant::now();
for index in 1..DECODE_LEN {
let logits_flat = model
.forward(&[next_token], prompt_tokens.len() + index - 1)
.expect("forward")
.realize_f32();
logits = logits_flat;
next_token = logits
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(i, _)| i as u32)
.unwrap();
}
let decode_dt = decode_start.elapsed();
let decode_steps = (DECODE_LEN - 1) as f64;
println!(
"device=cpu model={MODEL_REPO}/{MODEL_FILE} \
prefill: {} tokens in {:.3}s ({:.2} tok/s) \
decode: {decode_steps} tokens in {:.3}s ({:.2} tok/s)",
prompt_tokens.len(),
prefill_dt.as_secs_f64(),
prompt_tokens.len() as f64 / prefill_dt.as_secs_f64(),
decode_dt.as_secs_f64(),
decode_steps / decode_dt.as_secs_f64(),
);
}