use std::sync::Mutex;
use cubecl::device::{AmdDevice, CpuDevice, CudaDevice, WgpuDevice};
use cubecl::device_handle::DeviceHandle;
use cubecl::prelude::*;
use cubecl::Device;
use crate::device::{ComputeError, ComputeTarget};
pub const KERNEL_CACHE_ENV: &str = "DSPK_KERNEL_CACHE";
pub const CACHE_DIR_ENV: &str = "DSPK_CACHE_DIR";
pub fn configure_runtime() {
use cubecl::config::cache::CacheConfig;
use cubecl::config::{CubeClRuntimeConfig, RuntimeConfig};
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
let mut config = CubeClRuntimeConfig::from_current_dir().override_from_env();
config.compilation.cache = std::env::var(KERNEL_CACHE_ENV).map_or(true, |v| v.trim() != "0");
match std::env::var_os(CACHE_DIR_ENV) {
Some(dir) if !dir.is_empty() => config.environment.path = CacheConfig::Directory(dir.into()),
_ if matches!(config.environment.path, CacheConfig::Target) => config.environment.path = CacheConfig::Global,
_ => {}
}
let _ = CubeClRuntimeConfig::try_set(config);
});
}
pub trait ComputeTask {
type Output;
fn run(self, client: Client) -> Self::Output;
}
impl ComputeTarget {
pub fn device(self) -> Result<Device, ComputeError> {
configure_runtime();
Ok(match self.checked()? {
Self::Wgpu => Device::Wgpu(WgpuDevice::default()),
Self::Cpu => Device::Cpu(CpuDevice),
Self::Cuda => Device::Cuda(CudaDevice::default()),
Self::Hip => Device::Hip(AmdDevice::default()),
})
}
pub fn client(self) -> Result<Client, ComputeError> {
Ok(open_device(self.device()?))
}
pub fn run<T: ComputeTask>(self, task: T) -> Result<T::Output, ComputeError> {
Ok(task.run(self.client()?))
}
}
static OPENED: Mutex<Vec<Device>> = Mutex::new(Vec::new());
unsafe extern "C" {
fn atexit(handler: extern "C" fn()) -> std::ffi::c_int;
}
extern "C" fn shutdown_at_exit() {
shutdown_devices();
}
fn opened(device: &Device) {
let mut devices = OPENED.lock().unwrap_or_else(|e| e.into_inner());
if devices.contains(device) {
return;
}
devices.push(device.clone());
drop(devices);
unsafe {
atexit(shutdown_at_exit);
}
}
pub fn open_device(device: Device) -> Client {
configure_runtime();
let client = device.client();
opened(&device);
client
}
pub fn shutdown_devices() {
let devices = std::mem::take(&mut *OPENED.lock().unwrap_or_else(|e| e.into_inner()));
for device in devices {
DeviceHandle::<()>::shutdown(cubecl::RuntimeId::strip(device.to_id()));
}
}
#[cfg(test)]
mod tests {
use super::*;
struct RuntimeName;
impl ComputeTask for RuntimeName {
type Output = String;
fn run(self, client: Client) -> String {
client.name().to_string()
}
}
fn threads() -> usize {
std::fs::read_dir("/proc/self/task").map(|d| d.count()).unwrap_or(0)
}
#[test]
#[cfg(target_os = "linux")]
fn shutdown_stops_the_device_threads() {
let Some(target) = ComputeTarget::for_tests().into_iter().find(|t| *t != ComputeTarget::Cpu) else { return };
let client = target.client().unwrap();
let data: Vec<u8> = (0..64u8).collect();
let handle = client.create(cubecl::bytes::Bytes::from_bytes_vec(data.clone()));
assert_eq!(client.read_one_unchecked(handle).to_vec(), data);
drop(client);
let open = threads();
shutdown_devices();
let closed = threads();
assert!(closed < open, "{open} threads with the device open, {closed} after shutdown");
let again = target.client().unwrap();
assert!(!again.name().is_empty());
}
#[test]
fn runs_on_available_targets_and_refuses_missing_ones() {
for target in ComputeTarget::for_tests() {
assert!(!target.run(RuntimeName).unwrap().is_empty());
}
for target in ComputeTarget::ALL.into_iter().filter(|t| !t.is_available()) {
assert!(matches!(target.run(RuntimeName), Err(ComputeError::Unavailable(..))));
}
}
}