use std::ffi::c_int;
#[cfg(target_os = "linux")]
use std::ffi::c_void;
use std::io::Write as _;
use std::sync::Mutex;
use std::time::Duration;
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());
const EXIT_TEARDOWN_LIMIT: Duration = Duration::from_secs(10);
unsafe extern "C" {
#[cfg(not(target_os = "linux"))]
fn atexit(handler: extern "C" fn()) -> c_int;
#[cfg(target_os = "linux")]
fn on_exit(handler: extern "C" fn(c_int, *mut c_void), arg: *mut c_void) -> c_int;
fn _exit(status: c_int) -> !;
}
pub fn exit_now(status: i32) -> ! {
let _ = std::io::stdout().flush();
let _ = std::io::stderr().flush();
unsafe { _exit(status) }
}
fn teardown_within(limit: Duration, status: c_int, teardown: impl FnOnce()) {
let watchdog = std::thread::Builder::new().name("dspk-exit-limit".into()).spawn(move || {
std::thread::sleep(limit);
unsafe { _exit(status) }
});
drop(watchdog);
teardown();
}
fn shutdown_at_exit(status: c_int) {
let is_wgpu = |device: &Device| matches!(device, Device::Wgpu(_));
if OPENED.lock().unwrap_or_else(|e| e.into_inner()).iter().any(is_wgpu) {
teardown_within(EXIT_TEARDOWN_LIMIT, status, || shutdown_devices_where(is_wgpu));
}
}
#[cfg(target_os = "linux")]
extern "C" fn exit_handler(status: c_int, _arg: *mut c_void) {
shutdown_at_exit(status);
}
#[cfg(not(target_os = "linux"))]
extern "C" fn exit_handler() {
shutdown_at_exit(0);
}
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 {
#[cfg(target_os = "linux")]
on_exit(exit_handler, std::ptr::null_mut());
#[cfg(not(target_os = "linux"))]
atexit(exit_handler);
}
}
pub fn open_device(device: Device) -> Client {
configure_runtime();
let client = device.client();
opened(&device);
client
}
pub fn shutdown_devices() {
shutdown_devices_where(|_| true);
}
fn shutdown_devices_where(select: impl Fn(&Device) -> bool) {
let devices: Vec<Device> = {
let mut opened = OPENED.lock().unwrap_or_else(|e| e.into_inner());
let (chosen, kept) = std::mem::take(&mut *opened).into_iter().partition(|d| select(d));
*opened = kept;
chosen
};
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 exit_child() {
match std::env::var("DSPK_EXIT_TEST").as_deref() {
Ok("now") => {
print!("flushed");
exit_now(7);
}
Ok("limit") => teardown_within(Duration::from_millis(100), 3, || std::thread::sleep(Duration::from_secs(600))),
_ => {}
}
}
fn in_child(mode: &str) -> std::process::Output {
std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", "compute::target::tests::exit_child", "--nocapture"])
.env("DSPK_EXIT_TEST", mode)
.output()
.unwrap()
}
#[test]
fn exit_now_flushes_and_keeps_the_status() {
let out = in_child("now");
assert_eq!(out.status.code(), Some(7));
assert!(String::from_utf8_lossy(&out.stdout).contains("flushed"));
}
#[test]
fn a_stuck_teardown_ends_the_process_with_its_status() {
let start = std::time::Instant::now();
assert_eq!(in_child("limit").status.code(), Some(3));
assert!(start.elapsed() < Duration::from_secs(60), "took {:?}", start.elapsed());
}
#[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(..))));
}
}
}