#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::as_conversions,
clippy::missing_docs_in_private_items,
clippy::missing_panics_doc,
unsafe_code,
missing_docs
)]
use std::path::{Path, PathBuf};
use candle_core::{DType, Device, IndexOp, Tensor};
use candle_mi::{GenericTransformer, HookSpec, MIBackend, MlpLayout, QkvLayout, TransformerConfig};
use serial_test::serial;
const MODEL_ID: &str = "microsoft/Phi-3-mini-4k-instruct";
const ABS_DIFF_BAR_CPU: f32 = 1e-3;
const ABS_DIFF_BAR_GPU: f32 = 5e-3;
fn reference_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("scripts")
.join("phi3_mini_forward_reference.json")
}
fn hf_cache_dir() -> PathBuf {
if let Ok(cache) = std::env::var("HF_HOME") {
return PathBuf::from(cache).join("hub");
}
if let Ok(home) = std::env::var("USERPROFILE") {
return PathBuf::from(home)
.join(".cache")
.join("huggingface")
.join("hub");
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home)
.join(".cache")
.join("huggingface")
.join("hub");
}
panic!("Cannot find HuggingFace cache directory");
}
fn find_snapshot(model_id: &str) -> Option<PathBuf> {
let model_dir_name = format!("models--{}", model_id.replace('/', "--"));
let snapshots_dir = hf_cache_dir().join(model_dir_name).join("snapshots");
for entry in std::fs::read_dir(snapshots_dir).ok()?.flatten() {
let path = entry.path();
let has_weights = path.join("model.safetensors").exists()
|| path.join("model.safetensors.index.json").exists();
if path.join("config.json").exists() && has_weights {
return Some(path);
}
}
None
}
fn safetensors_paths(snapshot: &Path) -> Vec<PathBuf> {
let single = snapshot.join("model.safetensors");
if single.exists() {
return vec![single];
}
let index_path = snapshot.join("model.safetensors.index.json");
let index_str = std::fs::read_to_string(&index_path).unwrap_or_else(|_| {
panic!(
"no model.safetensors or index.json in {}",
snapshot.display()
)
});
let index: serde_json::Value = serde_json::from_str(&index_str).unwrap();
let weight_map = index["weight_map"].as_object().unwrap();
let mut shard_names: Vec<String> = weight_map
.values()
.map(|v| v.as_str().unwrap().to_string())
.collect();
shard_names.sort();
shard_names.dedup();
shard_names.iter().map(|name| snapshot.join(name)).collect()
}
fn cuda_device() -> Option<Device> {
Device::cuda_if_available(0).ok().filter(Device::is_cuda)
}
#[allow(clippy::too_many_lines)]
fn run_phi3_mini_forward_parity(device: &Device, device_name: &str, abs_diff_bar: f32) {
let reference_str = std::fs::read_to_string(reference_path()).expect(
"failed to read phi3_mini_forward_reference.json — run scripts/phi3_mini_validation.py first",
);
let reference: serde_json::Value = serde_json::from_str(&reference_str).unwrap();
let model_repo = reference["model_repo"].as_str().unwrap();
let ref_hidden = reference["hidden_size"].as_u64().unwrap() as usize;
let ref_layers = reference["num_layers"].as_u64().unwrap() as usize;
let ref_vocab = reference["vocab_size"].as_u64().unwrap() as usize;
let ref_head_dim = reference["head_dim"].as_u64().unwrap() as usize;
let test_cases = reference["test_cases"].as_array().unwrap();
assert_eq!(model_repo, MODEL_ID, "oracle JSON model_repo mismatch");
println!("Validating Phi-3 mini forward parity ({device_name}) against Python oracle:");
println!(" model: {model_repo}");
println!(
" hidden_size={ref_hidden}, num_layers={ref_layers}, \
vocab_size={ref_vocab}, head_dim={ref_head_dim}"
);
println!(
" {} test cases, abs-diff bar = {abs_diff_bar:.0e}",
test_cases.len()
);
let snapshot =
find_snapshot(MODEL_ID).unwrap_or_else(|| panic!("{MODEL_ID} not found in HF cache"));
let config_str = std::fs::read_to_string(snapshot.join("config.json")).unwrap();
let json: serde_json::Value = serde_json::from_str(&config_str).unwrap();
let config = TransformerConfig::from_hf_config(&json).unwrap();
assert_eq!(config.hidden_size, ref_hidden);
assert_eq!(config.num_layers, ref_layers);
assert_eq!(config.vocab_size, ref_vocab);
assert_eq!(config.head_dim, ref_head_dim);
assert_eq!(
config.qkv_layout,
QkvLayout::Fused,
"Phi-3 must use fused QKV"
);
assert_eq!(
config.mlp_layout,
MlpLayout::GatedFused,
"Phi-3 must use fused gate-up MLP"
);
let dtype = DType::F32;
let paths = safetensors_paths(&snapshot);
let vb =
unsafe { candle_nn::VarBuilder::from_mmaped_safetensors(&paths, dtype, device).unwrap() };
let model = GenericTransformer::load(config, device, dtype, vb).unwrap();
assert_eq!(model.num_layers(), ref_layers);
assert_eq!(model.hidden_size(), ref_hidden);
assert_eq!(model.vocab_size(), ref_vocab);
let mut max_abs_diff: f32 = 0.0;
let mut failures: Vec<String> = Vec::new();
for tc in test_cases {
let prompt = tc["prompt"].as_str().unwrap();
let ref_tokens: Vec<u32> = tc["tokens"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as u32)
.collect();
let ref_top10 = tc["top_10"].as_array().unwrap();
let input = Tensor::new(&ref_tokens[..], device)
.unwrap()
.unsqueeze(0)
.unwrap();
let hooks = HookSpec::new();
let result = model.forward(&input, &hooks).unwrap();
let logits = result.output();
let (batch, out_seq, vocab) = logits.dims3().unwrap();
assert_eq!(batch, 1);
assert_eq!(out_seq, ref_tokens.len());
assert_eq!(vocab, ref_vocab);
let last_logits: Vec<f32> = logits
.to_device(&Device::Cpu)
.unwrap()
.to_dtype(DType::F32)
.unwrap()
.i((0, out_seq - 1))
.unwrap()
.to_vec1()
.unwrap();
let mut indexed: Vec<(usize, f32)> = last_logits
.iter()
.enumerate()
.map(|(i, &v)| (i, v))
.collect();
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let ref_top1_idx = ref_top10[0]["index"].as_u64().unwrap() as usize;
let ref_top1_logit = ref_top10[0]["logit"].as_f64().unwrap() as f32;
println!("\nPrompt: {prompt:?} ({} tokens)", ref_tokens.len());
println!(
" Python top-1: ({ref_top1_idx}, {ref_top1_logit:.4}) \
Rust top-1: ({}, {:.4})",
indexed[0].0, indexed[0].1
);
let mut prompt_max_diff: f32 = 0.0;
for (rank, ref_item) in ref_top10.iter().enumerate() {
let ref_idx = ref_item["index"].as_u64().unwrap() as usize;
let ref_logit = ref_item["logit"].as_f64().unwrap() as f32;
let (rust_idx, rust_logit) = indexed[rank];
if rust_idx != ref_idx {
failures.push(format!(
"{prompt:?} rank {rank}: index mismatch (Rust {rust_idx}, Python {ref_idx})"
));
}
let diff = (rust_logit - ref_logit).abs();
if diff >= abs_diff_bar {
failures.push(format!(
"{prompt:?} rank {rank}: logit abs-diff {diff:.3e} >= {abs_diff_bar:.0e} \
(Rust {rust_logit:.4}, Python {ref_logit:.4})"
));
}
prompt_max_diff = prompt_max_diff.max(diff);
max_abs_diff = max_abs_diff.max(diff);
}
println!(" max abs-diff over top-10: {prompt_max_diff:.3e}");
}
println!(
"\n{} test cases on {device_name}; max abs-diff across all top-10 logits = {:.3e} (bar: {:.0e})",
test_cases.len(),
max_abs_diff,
abs_diff_bar
);
assert!(
failures.is_empty(),
"Phi-3 mini forward parity FAILED ({} divergences):\n {}",
failures.len(),
failures.join("\n ")
);
}
#[test]
#[ignore = "requires microsoft/Phi-3-mini-4k-instruct cached; run with --ignored"]
#[serial]
fn phi3_mini_forward_parity_cpu() {
if find_snapshot(MODEL_ID).is_none() {
eprintln!("SKIP: {MODEL_ID} not in HF cache");
return;
}
run_phi3_mini_forward_parity(&Device::Cpu, "CPU", ABS_DIFF_BAR_CPU);
}
#[test]
#[ignore = "requires microsoft/Phi-3-mini-4k-instruct cached and a CUDA device; run with --ignored"]
#[serial]
fn phi3_mini_forward_parity_gpu() {
if find_snapshot(MODEL_ID).is_none() {
eprintln!("SKIP: {MODEL_ID} not in HF cache");
return;
}
let Some(device) = cuda_device() else {
eprintln!("SKIP: no CUDA device available");
return;
};
run_phi3_mini_forward_parity(&device, "CUDA", ABS_DIFF_BAR_GPU);
}