#[cfg(feature = "cuda")]
use std::cell::Cell;
#[cfg(feature = "cuda")]
use std::collections::HashMap;
#[cfg(feature = "cuda")]
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
#[cfg(feature = "cuda")]
use crate::driver::{CudaContext, CudaModule, CudaStream, LaunchConfig};
#[cfg(feature = "cuda")]
use crate::error::Result;
#[cfg(feature = "cuda")]
static KERNEL_CACHE: OnceLock<Mutex<HashMap<String, Arc<Mutex<CudaModule>>>>> = OnceLock::new();
#[cfg(feature = "cuda")]
thread_local! {
static KERNEL_CACHE_HITS: Cell<u64> = const { Cell::new(0) };
static KERNEL_CACHE_MISSES: Cell<u64> = const { Cell::new(0) };
}
#[cfg(feature = "cuda")]
static CACHE_CLEAR_LOCK: Mutex<()> = Mutex::new(());
#[cfg(feature = "cuda")]
pub(crate) fn get_kernel_cache() -> &'static Mutex<HashMap<String, Arc<Mutex<CudaModule>>>> {
KERNEL_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
#[cfg(feature = "cuda")]
fn lock_cache(
cache: &Mutex<HashMap<String, Arc<Mutex<CudaModule>>>>,
) -> Result<std::sync::MutexGuard<'_, HashMap<String, Arc<Mutex<CudaModule>>>>> {
cache
.lock()
.map_err(|e| crate::GpuError::KernelLaunch(format!("Cache lock poisoned: {e}")))
}
#[cfg(feature = "cuda")]
fn lock_module(module: &Mutex<CudaModule>) -> Result<std::sync::MutexGuard<'_, CudaModule>> {
module
.lock()
.map_err(|e| crate::GpuError::KernelLaunch(format!("Module lock poisoned: {e}")))
}
#[cfg(feature = "cuda")]
pub(crate) fn get_or_compile_kernel(
ctx: &CudaContext,
key: &str,
ptx: &str,
) -> Result<Arc<Mutex<CudaModule>>> {
let cache = get_kernel_cache();
{
let cache_guard = lock_cache(cache)?;
if let Some(module) = cache_guard.get(key) {
KERNEL_CACHE_HITS.with(|c| c.set(c.get() + 1));
return Ok(Arc::clone(module));
}
}
KERNEL_CACHE_MISSES.with(|c| c.set(c.get() + 1));
eprintln!("[KERNEL-CACHE] Compiling: {key}");
let module = CudaModule::from_ptx(ctx, ptx)?;
let module_arc = Arc::new(Mutex::new(module));
lock_cache(cache)?.insert(key.to_string(), Arc::clone(&module_arc));
Ok(module_arc)
}
#[cfg(feature = "cuda")]
pub(crate) fn compile_lock_launch(
ctx: &CudaContext,
stream: &CudaStream,
cache_key: &str,
ptx: &str,
kernel_name: &str,
config: &LaunchConfig,
args: &mut [*mut std::ffi::c_void],
) -> Result<()> {
let module_arc = get_or_compile_kernel(ctx, cache_key, ptx)?;
let mut module = lock_module(&module_arc)?;
unsafe {
stream.launch_kernel(&mut module, kernel_name, config, args)?;
}
Ok(())
}
#[cfg(feature = "cuda")]
#[must_use]
pub fn kernel_cache_hits() -> u64 {
KERNEL_CACHE_HITS.with(Cell::get)
}
#[cfg(feature = "cuda")]
#[must_use]
pub fn kernel_cache_misses() -> u64 {
KERNEL_CACHE_MISSES.with(Cell::get)
}
#[cfg(feature = "cuda")]
pub fn reset_kernel_cache_stats() {
KERNEL_CACHE_HITS.with(|c| c.set(0));
KERNEL_CACHE_MISSES.with(|c| c.set(0));
}
#[cfg(feature = "cuda")]
fn clear_kernel_cache_locked() {
if let Some(cache) = KERNEL_CACHE.get() {
if let Ok(mut guard) = lock_cache(cache) {
guard.clear();
}
}
reset_kernel_cache_stats();
}
#[cfg(feature = "cuda")]
pub struct KernelCacheExclusive(MutexGuard<'static, ()>);
#[cfg(feature = "cuda")]
impl KernelCacheExclusive {
pub fn clear(&self) {
clear_kernel_cache_locked();
}
}
#[cfg(feature = "cuda")]
#[must_use]
pub fn kernel_cache_exclusive() -> KernelCacheExclusive {
KernelCacheExclusive(
CACHE_CLEAR_LOCK
.lock()
.unwrap_or_else(PoisonError::into_inner),
)
}
#[cfg(feature = "cuda")]
pub fn clear_kernel_cache() {
let _exclusive = kernel_cache_exclusive();
clear_kernel_cache_locked();
}
#[cfg(not(feature = "cuda"))]
#[must_use]
pub fn kernel_cache_hits() -> u64 {
0
}
#[cfg(not(feature = "cuda"))]
#[must_use]
pub fn kernel_cache_misses() -> u64 {
0
}
#[cfg(not(feature = "cuda"))]
pub fn reset_kernel_cache_stats() {}
#[cfg(not(feature = "cuda"))]
pub fn clear_kernel_cache() {}
#[cfg(not(feature = "cuda"))]
pub struct KernelCacheExclusive;
#[cfg(not(feature = "cuda"))]
impl KernelCacheExclusive {
pub fn clear(&self) {}
}
#[cfg(not(feature = "cuda"))]
#[must_use]
pub fn kernel_cache_exclusive() -> KernelCacheExclusive {
KernelCacheExclusive
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_clean_stats() {
reset_kernel_cache_stats();
assert_eq!(kernel_cache_hits(), 0);
assert_eq!(kernel_cache_misses(), 0);
}
#[test]
fn test_kernel_cache_stats_initial() {
assert_clean_stats();
}
#[test]
fn test_clear_kernel_cache() {
clear_kernel_cache();
assert_clean_stats();
}
#[test]
fn test_idempotent_operations() {
for _ in 0..3 {
reset_kernel_cache_stats();
clear_kernel_cache();
}
assert_clean_stats();
}
}
#[cfg(all(test, feature = "cuda"))]
mod cuda_tests {
use super::*;
fn assert_clean_stats() {
reset_kernel_cache_stats();
assert_eq!(kernel_cache_hits(), 0);
assert_eq!(kernel_cache_misses(), 0);
}
fn assert_counter_round_trip(hits: u64, misses: u64) {
assert_clean_stats();
for _ in 0..hits {
KERNEL_CACHE_HITS.with(|c| c.set(c.get() + 1));
}
for _ in 0..misses {
KERNEL_CACHE_MISSES.with(|c| c.set(c.get() + 1));
}
assert_eq!(kernel_cache_hits(), hits);
assert_eq!(kernel_cache_misses(), misses);
assert_clean_stats();
}
#[test]
fn test_cache_stats_are_not_polluted_by_other_threads() {
reset_kernel_cache_stats();
KERNEL_CACHE_HITS.with(|c| c.set(c.get() + 1));
std::thread::spawn(|| {
reset_kernel_cache_stats();
for _ in 0..500 {
KERNEL_CACHE_HITS.with(|c| c.set(c.get() + 1));
KERNEL_CACHE_MISSES.with(|c| c.set(c.get() + 1));
}
})
.join()
.expect("neighbour thread must not panic");
assert_eq!(
(kernel_cache_hits(), kernel_cache_misses()),
(1, 0),
"another thread's cache activity was attributed to this thread"
);
}
#[test]
fn test_clear_cannot_interleave_with_exclusive_holder() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc as StdArc;
let cleared = StdArc::new(AtomicBool::new(false));
let cleared_bg = StdArc::clone(&cleared);
let exclusive = kernel_cache_exclusive();
let bg = std::thread::spawn(move || {
clear_kernel_cache();
cleared_bg.store(true, Ordering::SeqCst);
});
for _ in 0..50 {
assert!(
!cleared.load(Ordering::SeqCst),
"clear_kernel_cache() completed while another caller held \
exclusivity — a clear can still land inside someone else's \
compile/hit observation"
);
std::thread::sleep(std::time::Duration::from_millis(2));
}
drop(exclusive);
bg.join().expect("clearing thread must not panic");
assert!(
cleared.load(Ordering::SeqCst),
"clear_kernel_cache() never completed after exclusivity was released"
);
}
fn clear_and_assert_empty() {
let exclusive = kernel_cache_exclusive();
exclusive.clear();
let guard = lock_cache(get_kernel_cache()).expect("Cache lock should not be poisoned");
assert!(guard.is_empty(), "Cache should be empty");
}
#[test]
fn test_get_kernel_cache_static_and_reentrant() {
let cache1 = get_kernel_cache();
let cache2 = get_kernel_cache();
assert!(std::ptr::eq(cache1, cache2));
for _ in 0..3 {
let _guard = lock_cache(cache1).expect("lock");
}
}
#[test]
fn test_clear_kernel_cache_clears_hashmap() {
clear_and_assert_empty();
}
#[test]
fn test_atomic_counter_operations() {
assert_counter_round_trip(5, 3);
assert_counter_round_trip(100, 50);
}
#[test]
fn test_clear_uninitialized_cache() {
clear_kernel_cache();
assert_clean_stats();
}
}