use crate::common;
use cutile::api::{copy_host_vec_to_device, meta, ones, zeros};
use cutile::jit_cache::{self, jit_backend_compile_count, jit_disk_hit_count, FileSystemJitStore};
use cutile::prelude::*;
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[cutile::module]
mod jit_disk_cache_test_module {
use cutile::core::*;
#[cutile::entry()]
fn add<const S: [i32; 1]>(
z: &mut Tensor<f32, S>,
x: &Tensor<f32, { [-1] }>,
y: &Tensor<f32, { [-1] }>,
) {
let tile_x = load_tile_like(x, z);
let tile_y = load_tile_like(y, z);
z.store(tile_x + tile_y);
}
}
fn launch_and_check(tile: usize) {
let x: Arc<Tensor<f32>> = ones(&[256]).sync().expect("ones").into();
let y: Arc<Tensor<f32>> = ones(&[256]).sync().expect("ones").into();
let z = zeros(&[256]).sync().expect("zeros").partition([tile]);
let z_host = jit_disk_cache_test_module::add(z, x, y)
.unzip()
.0
.unpartition()
.to_host_vec()
.sync()
.expect("add kernel");
assert!(z_host.iter().all(|&v| (v - 2.0f32).abs() < 1e-6));
}
fn fresh_dir(label: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let dir = std::env::temp_dir().join(format!(
"cutile_disk_cache_gpu_{label}_{}_{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed),
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn enable_at(dir: &Path) {
jit_cache::enable(Arc::new(FileSystemJitStore::new(dir).expect("open store")));
}
const CHILD_DIR_ENV: &str = "CUTILE_TEST_JIT_DISK_CACHE_CHILD_DIR";
const CHILD_ROLE_ENV: &str = "CUTILE_TEST_JIT_DISK_CACHE_ROLE";
#[test]
fn disk_cache_cross_process_hit() {
if let (Some(dir), Some(role)) = (
std::env::var_os(CHILD_DIR_ENV),
std::env::var_os(CHILD_ROLE_ENV),
) {
let is_reader = role.to_str() == Some("reader");
common::with_test_stack(move || {
enable_at(Path::new(&dir));
launch_and_check(64);
jit_cache::disable();
if is_reader {
assert_eq!(
jit_backend_compile_count(),
0,
"reader must not spawn tileiras: the writer stored every kernel it launches"
);
assert!(
jit_disk_hit_count() >= 1,
"reader must serve kernels from the disk cache, got {}",
jit_disk_hit_count(),
);
} else {
assert_eq!(
jit_disk_hit_count(),
0,
"writer starts cold: nothing is on disk yet"
);
assert!(
jit_backend_compile_count() >= 1,
"writer must compile and store at least one kernel"
);
}
});
return;
}
common::with_test_stack(|| {
let _guard = common::cache_test_lock();
let dir = fresh_dir("xproc");
let exe = std::env::current_exe().expect("current_exe");
let run = |role: &str| {
std::process::Command::new(&exe)
.args([
"jit_disk_cache::disk_cache_cross_process_hit",
"--exact",
"--nocapture",
"--test-threads=1",
])
.env(CHILD_DIR_ENV, &dir)
.env(CHILD_ROLE_ENV, role)
.output()
.expect("spawn child test process")
};
for role in ["writer", "reader"] {
let out = run(role);
assert!(
out.status.success(),
"{role} process failed.\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
if role == "writer" {
assert!(
std::fs::read_dir(&dir)
.expect("read writer cache directory")
.next()
.transpose()
.expect("read writer cache entry")
.is_some(),
"writer process succeeded without populating the cache; the child test filter may have matched zero tests"
);
}
}
let _ = std::fs::remove_dir_all(&dir);
});
}
#[test]
fn disk_cache_default_off_writes_nothing() {
common::with_test_stack(|| {
let _guard = common::cache_test_lock();
jit_cache::disable();
assert!(!jit_cache::is_enabled());
let puts_before = jit_cache::stats().puts;
launch_and_check(32);
assert_eq!(
jit_cache::stats().puts,
puts_before,
"no store is installed, so nothing may be written"
);
});
}
#[test]
fn disk_cache_degrades_softly_on_io_errors() {
common::with_test_stack(|| {
let _guard = common::cache_test_lock();
let dir = fresh_dir("degraded");
let root = dir.join("store");
enable_at(&root);
std::fs::remove_dir_all(&root).unwrap();
std::fs::write(&root, b"not a directory").unwrap();
let io_errors_before = jit_cache::stats().io_errors;
launch_and_check(128); jit_cache::disable();
assert!(
jit_cache::stats().io_errors > io_errors_before,
"the broken store must surface as counted soft errors"
);
let _ = std::fs::remove_dir_all(&dir);
});
}
fn corrupt_entry_payload(path: &Path) {
let mut bytes = std::fs::read(path).expect("read cached cubin");
assert_eq!(
&bytes[0..12],
b"CUTILECUBIN\0",
"unexpected cache entry magic"
);
let gpu_len = u16::from_le_bytes(bytes[80..82].try_into().unwrap()) as usize;
let fp_len = u16::from_le_bytes(bytes[82..84].try_into().unwrap()) as usize;
let payload_len = u64::from_le_bytes(bytes[88..96].try_into().unwrap()) as usize;
let payload_start = 96 + gpu_len + fp_len;
assert_eq!(
bytes.len(),
payload_start + payload_len,
"cache entry size does not match header"
);
bytes[payload_start] ^= 0xFF;
let new_digest = Sha256::digest(&bytes[payload_start..payload_start + payload_len]);
bytes[16..48].copy_from_slice(&new_digest);
std::fs::write(path, bytes).expect("write corrupted cubin");
}
#[test]
fn disk_cache_recompiles_after_driver_rejection() {
common::with_test_stack(|| {
let _guard = common::cache_test_lock();
let dir = fresh_dir("driver_reject");
enable_at(&dir);
let tile = 16;
let z_meta = meta::<f32>(&[256]).partition([tile]);
let x_meta = meta::<f32>(&[256]);
let y_meta = meta::<f32>(&[256]);
jit_disk_cache_test_module::add(z_meta, x_meta, y_meta)
.compile()
.expect("meta compile should write the add kernel to disk");
let mut files = vec![];
for shard in std::fs::read_dir(&dir).unwrap() {
let shard = shard.unwrap().path();
for entry in std::fs::read_dir(&shard).unwrap() {
files.push(entry.unwrap().path());
}
}
assert_eq!(
files.len(),
1,
"expected one cached cubin from meta compile"
);
corrupt_entry_payload(&files[0]);
cutile::tile_kernel::get_kernel_cache().clear();
let backend_before = jit_backend_compile_count();
let hits_before = jit_disk_hit_count();
let x_vec = Arc::new(vec![1.0f32; 256]);
let y_vec = Arc::new(vec![1.0f32; 256]);
let z_vec = Arc::new(vec![0.0f32; 256]);
let x = Arc::new(copy_host_vec_to_device(&x_vec).sync().expect("copy x"));
let y = Arc::new(copy_host_vec_to_device(&y_vec).sync().expect("copy y"));
let z = copy_host_vec_to_device(&z_vec).sync().expect("copy z");
let z_host = jit_disk_cache_test_module::add(z.partition([tile]), x, y)
.unzip()
.0
.unpartition()
.to_host_vec()
.sync()
.expect("real launch after driver rejection should succeed");
jit_cache::disable();
assert!(z_host.iter().all(|&v| (v - 2.0f32).abs() < 1e-6));
assert_eq!(
jit_disk_hit_count() - hits_before,
1,
"the corrupted entry must be read from disk once"
);
assert_eq!(
jit_backend_compile_count() - backend_before,
1,
"driver rejection must trigger exactly one tileiras recompile"
);
let _ = std::fs::remove_dir_all(&dir);
});
}