use std::path::{Path, PathBuf};
use half::f16;
use objc2::AnyThread;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, ProtocolObject};
use objc2_core_ml::{
MLComputeUnits, MLDictionaryFeatureProvider, MLFeatureProvider, MLFeatureValue, MLModel,
MLModelConfiguration, MLMultiArray, MLMultiArrayDataType,
};
use objc2_foundation::{NSArray, NSDictionary, NSNumber, NSString, NSURL};
use crate::runtime::error::RuntimeError;
#[allow(deprecated)]
pub fn compile_and_load(
package: &Path,
cpu_and_ne: bool,
) -> Result<Retained<MLModel>, RuntimeError> {
let config: Retained<MLModelConfiguration> = unsafe { MLModelConfiguration::new() };
let units = if cpu_and_ne {
MLComputeUnits::CPUAndNeuralEngine
} else {
MLComputeUnits::CPUOnly
};
unsafe { config.setComputeUnits(units) };
let cached = cached_model_path(package);
if cached.is_dir() {
match current_source_key(package) {
Ok(key) if meta_matches(&cached_meta_path(package), &key) => {
match load_model_from_dir(&cached, &config) {
Ok(model) => {
tracing::info!(
cache = %cached.display(),
"loaded compiled ANE model from cache"
);
return Ok(model);
}
Err(e) => {
tracing::warn!(
cache = %cached.display(),
error = %e,
"cached ANE model failed to load; recompiling"
);
}
}
}
Ok(_) => {
tracing::info!(
cache = %cached.display(),
"ANE compiled-model cache stale (source or OS changed); recompiling"
);
}
Err(e) => {
tracing::warn!(error = %e, "could not compute ANE cache key; recompiling");
}
}
}
tracing::info!(
package = %package.display(),
cache = %cached.display(),
"compiling ANE model (cold-start ~20s), caching for fast restarts"
);
let compiled_url = compile_package(package)?;
if let Some(temp_dir) = url_to_path(&compiled_url) {
match populate_cache(package, &temp_dir) {
Ok(()) => {
match load_model_from_dir(&cached, &config) {
Ok(model) => return Ok(model),
Err(e) => tracing::warn!(
cache = %cached.display(),
error = %e,
"freshly cached ANE model failed to load; loading from temp"
),
}
}
Err(e) => tracing::warn!(
cache = %cached.display(),
error = %e,
"failed to populate ANE compiled-model cache; loading from temp"
),
}
} else {
tracing::warn!("compiled ANE model URL is not a local path; cache skipped");
}
load_model_from_url(package, &compiled_url, &config)
}
#[allow(deprecated)]
fn compile_package(package: &Path) -> Result<Retained<NSURL>, RuntimeError> {
let path_str = package.to_str().ok_or_else(|| RuntimeError::LoadFailed {
path: package.to_path_buf(),
message: "package path is not valid UTF-8".to_string(),
})?;
let ns_path = NSString::from_str(path_str);
let pkg_url: Retained<NSURL> = NSURL::fileURLWithPath(&ns_path);
unsafe { MLModel::compileModelAtURL_error(&pkg_url) }.map_err(|err| RuntimeError::LoadFailed {
path: package.to_path_buf(),
message: format!("compileModelAtURL failed: {}", ns_error_message(&err)),
})
}
fn load_model_from_dir(
compiled_dir: &Path,
config: &MLModelConfiguration,
) -> Result<Retained<MLModel>, RuntimeError> {
let path_str = compiled_dir
.to_str()
.ok_or_else(|| RuntimeError::LoadFailed {
path: compiled_dir.to_path_buf(),
message: "compiled model path is not valid UTF-8".to_string(),
})?;
let ns_path = NSString::from_str(path_str);
let url: Retained<NSURL> = NSURL::fileURLWithPath(&ns_path);
load_model_from_url(compiled_dir, &url, config)
}
fn load_model_from_url(
package: &Path,
compiled_url: &NSURL,
config: &MLModelConfiguration,
) -> Result<Retained<MLModel>, RuntimeError> {
unsafe { MLModel::modelWithContentsOfURL_configuration_error(compiled_url, config) }.map_err(
|err| RuntimeError::LoadFailed {
path: package.to_path_buf(),
message: format!("modelWithContentsOfURL failed: {}", ns_error_message(&err)),
},
)
}
const COMPILED_CACHE_DIR: &str = "compiled_cache";
fn compiled_cache_dir(package: &Path) -> PathBuf {
package
.parent()
.unwrap_or_else(|| Path::new("."))
.join(COMPILED_CACHE_DIR)
}
fn cached_model_path(package: &Path) -> PathBuf {
let stem = package
.file_stem()
.map(|s| s.to_os_string())
.unwrap_or_else(|| std::ffi::OsString::from("model"));
let mut name = stem;
name.push(".mlmodelc");
compiled_cache_dir(package).join(name)
}
fn cached_meta_path(package: &Path) -> PathBuf {
let mut p = cached_model_path(package).into_os_string();
p.push(".meta");
PathBuf::from(p)
}
fn macos_product_version() -> Result<String, RuntimeError> {
let out = std::process::Command::new("sw_vers")
.arg("-productVersion")
.output()
.map_err(|e| RuntimeError::LoadFailed {
path: PathBuf::from("sw_vers"),
message: format!("failed to run sw_vers: {e}"),
})?;
if !out.status.success() {
return Err(RuntimeError::LoadFailed {
path: PathBuf::from("sw_vers"),
message: format!("sw_vers exited with {}", out.status),
});
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn dir_size_and_newest_mtime(root: &Path) -> std::io::Result<(u64, u128)> {
let mut total: u64 = 0;
let mut newest: u128 = 0;
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let ft = entry.file_type()?;
if ft.is_dir() {
stack.push(entry.path());
} else if ft.is_file() {
let meta = entry.metadata()?;
total = total.saturating_add(meta.len());
if let Ok(mtime) = meta.modified()
&& let Ok(dur) = mtime.duration_since(std::time::UNIX_EPOCH)
{
newest = newest.max(dur.as_nanos());
}
}
}
}
Ok((total, newest))
}
fn build_source_key(package: &Path, os_version: &str) -> Result<String, RuntimeError> {
let (size, mtime) =
dir_size_and_newest_mtime(package).map_err(|e| RuntimeError::LoadFailed {
path: package.to_path_buf(),
message: format!("failed to stat package for cache key: {e}"),
})?;
Ok(format!("size={size} mtime_ns={mtime} os={os_version}"))
}
fn current_source_key(package: &Path) -> Result<String, RuntimeError> {
build_source_key(package, &macos_product_version()?)
}
fn meta_matches(meta_path: &Path, key: &str) -> bool {
match std::fs::read_to_string(meta_path) {
Ok(content) => content.trim() == key.trim(),
Err(_) => false,
}
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ft = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if ft.is_dir() {
copy_dir_recursive(&from, &to)?;
} else {
std::fs::copy(&from, &to)?;
}
}
Ok(())
}
fn populate_cache(package: &Path, temp_dir: &Path) -> std::io::Result<()> {
let cache_dir = compiled_cache_dir(package);
std::fs::create_dir_all(&cache_dir)?;
sweep_stale_temp_dirs(&cache_dir);
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = std::process::id();
let staging = cache_dir.join(format!(".staging.{pid}.{stamp}"));
let cleanup = || {
let _ = std::fs::remove_dir_all(&staging);
};
if let Err(e) = copy_dir_recursive(temp_dir, &staging) {
cleanup();
return Err(e);
}
let final_dir = cached_model_path(package);
let mut trash: Option<PathBuf> = None;
if final_dir.exists() {
let aside = cache_dir.join(format!(".trash.{pid}.{stamp}"));
if let Err(e) = std::fs::rename(&final_dir, &aside) {
cleanup();
return Err(e);
}
trash = Some(aside);
}
if let Err(e) = std::fs::rename(&staging, &final_dir) {
cleanup();
if let Some(aside) = trash {
let _ = std::fs::remove_dir_all(&aside);
}
return Err(e);
}
if let Some(aside) = trash {
let _ = std::fs::remove_dir_all(&aside);
}
let key = current_source_key(package).map_err(std::io::Error::other)?;
std::fs::write(cached_meta_path(package), key)?;
Ok(())
}
fn sweep_stale_temp_dirs(cache_dir: &Path) {
let Ok(entries) = std::fs::read_dir(cache_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with(".staging.") || name.starts_with(".trash.") {
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
fn url_to_path(url: &NSURL) -> Option<PathBuf> {
let ns = url.path()?;
Some(PathBuf::from(ns.to_string()))
}
#[allow(deprecated)]
pub fn predict_f32(
model: &MLModel,
input_name: &str,
mel: &[f32],
shape: &[usize],
output_name: &str,
) -> Result<(Vec<f32>, Vec<usize>), RuntimeError> {
let expected_len: usize = shape.iter().product();
if mel.len() != expected_len {
return Err(RuntimeError::DataLengthMismatch {
expected: expected_len,
got: mel.len(),
});
}
let dims: Vec<Retained<NSNumber>> = shape.iter().map(|&d| NSNumber::new_usize(d)).collect();
let ns_shape: Retained<NSArray<NSNumber>> = NSArray::from_retained_slice(&dims);
let input: Retained<MLMultiArray> = unsafe {
MLMultiArray::initWithShape_dataType_error(
MLMultiArray::alloc(),
&ns_shape,
MLMultiArrayDataType::Float16,
)
}
.map_err(|err| {
RuntimeError::InferenceFailed(format!(
"MLMultiArray init failed: {}",
ns_error_message(&err)
))
})?;
let in_strides = strides_of(&input)?;
{
let base = unsafe { input.dataPointer() }.as_ptr() as *mut f16;
write_strided(base, mel, shape, &in_strides);
}
let feat: Retained<MLFeatureValue> =
unsafe { MLFeatureValue::featureValueWithMultiArray(&input) };
let key = NSString::from_str(input_name);
let value: &AnyObject = &feat;
let dict: Retained<NSDictionary<NSString, AnyObject>> =
NSDictionary::from_slices(&[&*key], &[value]);
let provider: Retained<MLDictionaryFeatureProvider> = unsafe {
MLDictionaryFeatureProvider::initWithDictionary_error(
MLDictionaryFeatureProvider::alloc(),
&dict,
)
}
.map_err(|err| {
RuntimeError::InferenceFailed(format!(
"feature provider init failed: {}",
ns_error_message(&err)
))
})?;
let provider_obj: &ProtocolObject<dyn MLFeatureProvider> = ProtocolObject::from_ref(&*provider);
let result: Retained<ProtocolObject<dyn MLFeatureProvider>> =
unsafe { model.predictionFromFeatures_error(provider_obj) }.map_err(|err| {
RuntimeError::InferenceFailed(format!("prediction failed: {}", ns_error_message(&err)))
})?;
let out_key = NSString::from_str(output_name);
let out_feat: Retained<MLFeatureValue> = unsafe { result.featureValueForName(&out_key) }
.ok_or_else(|| {
RuntimeError::InferenceFailed(format!("output '{output_name}' missing from result"))
})?;
let out_arr: Retained<MLMultiArray> =
unsafe { out_feat.multiArrayValue() }.ok_or_else(|| {
RuntimeError::InferenceFailed(format!("output '{output_name}' is not a multi-array"))
})?;
let out_shape = shape_of(&out_arr)?;
let out_strides = strides_of(&out_arr)?;
let out_len: usize = out_shape.iter().product();
let out_dtype = unsafe { out_arr.dataType() };
let raw = unsafe { out_arr.dataPointer() }.as_ptr();
let data = match out_dtype {
MLMultiArrayDataType::Float16 => {
read_strided_f16(raw as *const f16, &out_shape, &out_strides)
}
MLMultiArrayDataType::Float32 => {
read_strided_f32(raw as *const f32, &out_shape, &out_strides)
}
other => {
return Err(RuntimeError::InferenceFailed(format!(
"unsupported output dataType {other:?}"
)));
}
};
debug_assert_eq!(data.len(), out_len);
Ok((data, out_shape))
}
fn shape_of(arr: &MLMultiArray) -> Result<Vec<usize>, RuntimeError> {
let ns: Retained<NSArray<NSNumber>> = unsafe { arr.shape() };
Ok(nsarray_usize(&ns))
}
fn strides_of(arr: &MLMultiArray) -> Result<Vec<usize>, RuntimeError> {
let ns: Retained<NSArray<NSNumber>> = unsafe { arr.strides() };
Ok(nsarray_usize(&ns))
}
fn nsarray_usize(ns: &NSArray<NSNumber>) -> Vec<usize> {
let n = ns.count();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let num = ns.objectAtIndex(i);
out.push(num.as_usize());
}
out
}
fn write_strided(base: *mut f16, data: &[f32], shape: &[usize], strides: &[usize]) {
let rank = shape.len();
let total = data.len();
let mut idx = vec![0usize; rank];
for &v in data.iter().take(total) {
let mut off = 0usize;
for d in 0..rank {
off += idx[d] * strides[d];
}
unsafe { *base.add(off) = f16::from_f32(v) };
for d in (0..rank).rev() {
idx[d] += 1;
if idx[d] < shape[d] {
break;
}
idx[d] = 0;
}
}
}
fn read_strided_f16(base: *const f16, shape: &[usize], strides: &[usize]) -> Vec<f32> {
read_strided_with(shape, strides, |off| unsafe { (*base.add(off)).to_f32() })
}
fn read_strided_f32(base: *const f32, shape: &[usize], strides: &[usize]) -> Vec<f32> {
read_strided_with(shape, strides, |off| unsafe { *base.add(off) })
}
fn read_strided_with(
shape: &[usize],
strides: &[usize],
mut read: impl FnMut(usize) -> f32,
) -> Vec<f32> {
let rank = shape.len();
let total: usize = shape.iter().product();
let mut out = Vec::with_capacity(total);
let mut idx = vec![0usize; rank];
for _ in 0..total {
let mut off = 0usize;
for d in 0..rank {
off += idx[d] * strides[d];
}
out.push(read(off));
for d in (0..rank).rev() {
idx[d] += 1;
if idx[d] < shape[d] {
break;
}
idx[d] = 0;
}
}
out
}
fn ns_error_message(err: &objc2_foundation::NSError) -> String {
err.localizedDescription().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use std::time::Instant;
fn package_path() -> PathBuf {
let home = std::env::var("HOME").expect("HOME set");
PathBuf::from(home).join(".gigastt/models/ane/gigaam_v3_encoder_768.mlpackage")
}
#[test]
fn cache_paths_are_sibling_compiled_cache_dir() {
let pkg = Path::new("/models/ane/gigaam_v3_encoder_768.mlpackage");
assert_eq!(
compiled_cache_dir(pkg),
PathBuf::from("/models/ane/compiled_cache")
);
assert_eq!(
cached_model_path(pkg),
PathBuf::from("/models/ane/compiled_cache/gigaam_v3_encoder_768.mlmodelc")
);
assert_eq!(
cached_meta_path(pkg),
PathBuf::from("/models/ane/compiled_cache/gigaam_v3_encoder_768.mlmodelc.meta")
);
}
#[test]
fn build_source_key_changes_with_size_and_os() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg = tmp.path().join("pkg.mlpackage");
fs::create_dir_all(pkg.join("Data")).unwrap();
fs::write(pkg.join("Data").join("weight.bin"), b"abc").unwrap();
let key_a = build_source_key(&pkg, "26.1").expect("key a");
let key_a2 = build_source_key(&pkg, "26.1").expect("key a2");
assert_eq!(key_a, key_a2, "same source+OS must produce the same key");
let key_os = build_source_key(&pkg, "27.0").expect("key os");
assert_ne!(key_a, key_os, "OS version must be part of the key");
fs::write(pkg.join("Data").join("weight.bin"), b"abcdef").unwrap();
let key_size = build_source_key(&pkg, "26.1").expect("key size");
assert_ne!(key_a, key_size, "changed source size must change the key");
}
#[test]
fn meta_matches_only_on_exact_key() {
let tmp = tempfile::tempdir().expect("tempdir");
let meta = tmp.path().join("model.mlmodelc.meta");
assert!(!meta_matches(&meta, "size=10 mtime_ns=5 os=26.1"));
fs::write(&meta, "size=10 mtime_ns=5 os=26.1\n").unwrap();
assert!(meta_matches(&meta, "size=10 mtime_ns=5 os=26.1"));
assert!(!meta_matches(&meta, "size=11 mtime_ns=5 os=26.1"));
assert!(!meta_matches(&meta, "size=10 mtime_ns=5 os=27.0"));
}
#[test]
fn copy_dir_recursive_reproduces_tree() {
let tmp = tempfile::tempdir().expect("tempdir");
let src = tmp.path().join("src");
let dst = tmp.path().join("dst");
fs::create_dir_all(src.join("nested")).unwrap();
fs::write(src.join("top.bin"), b"top").unwrap();
fs::write(src.join("nested").join("inner.bin"), b"inner").unwrap();
copy_dir_recursive(&src, &dst).expect("copy");
assert_eq!(fs::read(dst.join("top.bin")).unwrap(), b"top");
assert_eq!(
fs::read(dst.join("nested").join("inner.bin")).unwrap(),
b"inner"
);
}
#[test]
fn populate_cache_atomically_places_model_and_sidecar() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg = tmp.path().join("gigaam_v3_encoder_768.mlpackage");
fs::create_dir_all(&pkg).unwrap();
fs::write(pkg.join("Manifest.json"), b"{}").unwrap();
let temp_compiled = tmp.path().join("temp.mlmodelc");
fs::create_dir_all(temp_compiled.join("model")).unwrap();
fs::write(temp_compiled.join("coremldata.bin"), b"compiled").unwrap();
fs::write(temp_compiled.join("model").join("net.bin"), b"net").unwrap();
populate_cache(&pkg, &temp_compiled).expect("populate");
let cached = cached_model_path(&pkg);
assert!(cached.is_dir(), "cached model dir must exist");
assert_eq!(
fs::read(cached.join("coremldata.bin")).unwrap(),
b"compiled"
);
assert_eq!(
fs::read(cached.join("model").join("net.bin")).unwrap(),
b"net"
);
let key = current_source_key(&pkg).expect("source key");
assert!(
meta_matches(&cached_meta_path(&pkg), &key),
"sidecar key must match the current source key after populate_cache"
);
let leftover: Vec<_> = fs::read_dir(compiled_cache_dir(&pkg))
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with(".staging"))
.collect();
assert!(leftover.is_empty(), "no .staging dirs must remain");
}
fn ref_dir() -> PathBuf {
PathBuf::from("/tmp/gigaam-ane-spike/bridge_ref")
}
fn read_f32(path: &Path) -> Vec<f32> {
let bytes = fs::read(path).expect("read f32 file");
assert_eq!(bytes.len() % 4, 0, "f32 file length not a multiple of 4");
bytes
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
fn read_shapes(path: &Path) -> (Vec<usize>, Vec<usize>) {
let txt = fs::read_to_string(path).expect("read shapes.txt");
let mut in_shape = Vec::new();
let mut out_shape = Vec::new();
for line in txt.lines() {
let mut it = line.split_whitespace();
match it.next() {
Some("in") => in_shape = it.map(|s| s.parse().unwrap()).collect(),
Some("out") => out_shape = it.map(|s| s.parse().unwrap()).collect(),
_ => {}
}
}
(in_shape, out_shape)
}
fn cosine(a: &[f32], b: &[f32]) -> f64 {
let mut dot = 0.0f64;
let mut na = 0.0f64;
let mut nb = 0.0f64;
for (&x, &y) in a.iter().zip(b.iter()) {
dot += x as f64 * y as f64;
na += x as f64 * x as f64;
nb += y as f64 * y as f64;
}
dot / (na.sqrt() * nb.sqrt())
}
#[test]
#[ignore = "requires the 768 bucket .mlpackage + Python bridge_ref/; runs on ANE"]
fn bridge_loads_predicts_matches_python_reference() {
let pkg = package_path();
let refd = ref_dir();
if !pkg.exists() {
eprintln!("SKIP: missing package {pkg:?} (run convert_gigaam_ane.py --buckets 768)");
return;
}
if !refd.join("shapes.txt").exists() {
eprintln!("SKIP: missing {refd:?}/shapes.txt (run dump_bridge_ref.py)");
return;
}
let (in_shape, ref_out_shape) = read_shapes(&refd.join("shapes.txt"));
let mel = read_f32(&refd.join("mel_in.f32"));
let ref_out = read_f32(&refd.join("encoded_ref.f32"));
assert_eq!(
in_shape,
vec![1, 64, 768],
"unexpected reference input shape"
);
let model = compile_and_load(&pkg, true).expect("compile_and_load");
let (out, out_shape) =
predict_f32(&model, "mel", &mel, &in_shape, "encoded").expect("predict_f32");
println!("out_shape={out_shape:?} ref_out_shape={ref_out_shape:?}");
assert_eq!(
out_shape, ref_out_shape,
"output shape mismatch vs Python ref"
);
assert_eq!(
out.len(),
ref_out.len(),
"output length mismatch vs Python ref"
);
assert!(
out.iter().all(|v| v.is_finite()),
"output has non-finite values"
);
let cos = cosine(&out, &ref_out);
let max_abs = out
.iter()
.zip(ref_out.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, f32::max);
println!("cosine={cos:.6} max_abs={max_abs:.6}");
assert!(cos > 0.999, "cosine {cos:.6} <= 0.999 vs Python reference");
for _ in 0..4 {
let _ = predict_f32(&model, "mel", &mel, &in_shape, "encoded").expect("warm predict");
}
let iters = 12;
let mut times_ms = Vec::with_capacity(iters);
for _ in 0..iters {
let t = Instant::now();
let _ = predict_f32(&model, "mel", &mel, &in_shape, "encoded").expect("timed predict");
times_ms.push(t.elapsed().as_secs_f64() * 1000.0);
}
times_ms.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median_ms = times_ms[times_ms.len() / 2];
let audio_secs = in_shape[2] as f64 / 100.0;
let rtfx = audio_secs / (median_ms / 1000.0);
println!("median_ms={median_ms:.3} audio_secs={audio_secs:.3} RTFx={rtfx:.1}");
}
#[test]
#[ignore = "requires the 768 bucket .mlpackage; compiles on ANE (~20s first load)"]
fn bridge_disk_cache_skips_recompile_and_preserves_output() {
let real_pkg = package_path();
if !real_pkg.exists() {
eprintln!(
"SKIP: missing package {real_pkg:?} (run convert_gigaam_ane.py --buckets 768)"
);
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let pkg = tmp.path().join("gigaam_v3_encoder_768.mlpackage");
std::os::unix::fs::symlink(&real_pkg, &pkg).expect("symlink package into tempdir");
let real_cache = compiled_cache_dir(&real_pkg);
let real_cache_existed = real_cache.exists();
let cache_dir = compiled_cache_dir(&pkg);
assert!(!cached_model_path(&pkg).exists(), "cache must start empty");
assert_eq!(
cache_dir,
tmp.path().join("compiled_cache"),
"cache must derive inside the tempdir, not the real cache"
);
let in_shape = vec![1usize, 64, 768];
let n: usize = in_shape.iter().product();
let mel: Vec<f32> = (0..n).map(|i| (i as f32 % 17.0) * 0.01).collect();
let t0 = Instant::now();
let model1 = compile_and_load(&pkg, true).expect("first compile_and_load");
let first_ms = t0.elapsed().as_secs_f64() * 1000.0;
let (out1, shape1) =
predict_f32(&model1, "mel", &mel, &in_shape, "encoded").expect("predict 1");
assert!(
cached_model_path(&pkg).exists(),
"first load must populate the disk cache"
);
let key = current_source_key(&pkg).expect("source key");
assert!(
meta_matches(&cached_meta_path(&pkg), &key),
"sidecar must match the current source key after first load"
);
let t1 = Instant::now();
let model2 = compile_and_load(&pkg, true).expect("second compile_and_load");
let second_ms = t1.elapsed().as_secs_f64() * 1000.0;
let (out2, shape2) =
predict_f32(&model2, "mel", &mel, &in_shape, "encoded").expect("predict 2");
println!("cold_start_first_ms={first_ms:.1} cache_hit_second_ms={second_ms:.1}");
assert!(
first_ms > 5_000.0,
"expected cold compile > 5s, got {first_ms:.1} ms"
);
assert!(
second_ms < 2_000.0,
"expected cache-hit load < 2s, got {second_ms:.1} ms"
);
assert!(
second_ms < first_ms / 2.0,
"cache hit ({second_ms:.1} ms) must be much faster than cold ({first_ms:.1} ms)"
);
assert_eq!(shape1, shape2, "output shape changed across cache hit");
assert_eq!(
out1.len(),
out2.len(),
"output length changed across cache hit"
);
assert_eq!(
out1.iter().map(|f| f.to_bits()).collect::<Vec<_>>(),
out2.iter().map(|f| f.to_bits()).collect::<Vec<_>>(),
"cache hit must produce byte-identical output"
);
assert_eq!(
real_cache.exists(),
real_cache_existed,
"the real cache dir must be untouched by this test"
);
}
}