Files
foxhunt/crates/ml-core/src/device.rs
jgrusewski dffe97a922 fix: log stale CUDA errors instead of discarding, remove max_steps_per_epoch from smoketest
check_err() now logs warnings for real kernel errors instead of let _ =.
Removed max_steps_per_epoch from smoketest TOML to match working config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:10:45 +01:00

146 lines
4.8 KiB
Rust

//! Device abstraction replacing `candle_core::Device`.
//!
//! Provides `MlDevice` enum with CPU and CUDA variants.
//! The CUDA variant holds `Arc<CudaDevice>` and `Arc<CudaStream>` for
//! direct cudarc interop.
use std::sync::Arc;
use crate::MLError;
#[cfg(feature = "cuda")]
use cudarc::driver::{CudaContext, CudaStream};
/// Device abstraction for the ML pipeline.
///
/// Replaces `candle_core::Device`. Only two variants: CPU (for checkpoint
/// serialization and preprocessing) and CUDA (for training and inference).
#[derive(Clone)]
pub enum MlDevice {
/// Host CPU — used for checkpoint I/O and lightweight preprocessing.
Cpu,
/// CUDA GPU with context + stream handles.
#[cfg(feature = "cuda")]
Cuda {
context: Arc<CudaContext>,
stream: Arc<CudaStream>,
},
}
impl std::fmt::Debug for MlDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MlDevice::Cpu => write!(f, "MlDevice::Cpu"),
#[cfg(feature = "cuda")]
MlDevice::Cuda { .. } => write!(f, "MlDevice::Cuda"),
}
}
}
impl MlDevice {
/// Create a CUDA device with the given ordinal (0-indexed).
///
/// Initializes a `CudaDevice` and forks a non-default stream for all
/// subsequent operations.
#[cfg(feature = "cuda")]
pub fn cuda(ordinal: usize) -> Result<Self, MLError> {
let context = CudaContext::new(ordinal).map_err(|e| {
MLError::DeviceError(format!("Failed to open CUDA device {ordinal}: {e}"))
})?;
// Check for stale CUDA errors from previous context users in the same process.
// CudaContext::new reuses the primary context (cuDevicePrimaryCtxRetain),
// so deferred errors from a previous test's Drop persist.
if let Err(e) = context.check_err() {
tracing::warn!("Stale CUDA error on device {ordinal} (may indicate prior kernel failure): {e}");
}
let stream = context.new_stream().map_err(|e| {
MLError::DeviceError(format!("Failed to create CUDA stream on device {ordinal}: {e}"))
})?;
Ok(MlDevice::Cuda {
context,
stream,
})
}
/// Try to create a CUDA device, falling back to CPU if unavailable.
///
/// This is the recommended way to get a device when you want GPU
/// acceleration but can tolerate CPU fallback.
#[cfg(feature = "cuda")]
pub fn cuda_if_available(ordinal: usize) -> Self {
match Self::cuda(ordinal) {
Ok(dev) => dev,
Err(e) => {
tracing::warn!("CUDA device {ordinal} unavailable ({e}), falling back to CPU");
MlDevice::Cpu
}
}
}
/// CPU-only fallback for `cuda_if_available` when CUDA feature is disabled.
#[cfg(not(feature = "cuda"))]
pub fn cuda_if_available(_ordinal: usize) -> Self {
MlDevice::Cpu
}
/// Alias for `cuda()` — some callers use `new_cuda()`.
#[cfg(feature = "cuda")]
pub fn new_cuda(ordinal: usize) -> Result<Self, MLError> {
Self::cuda(ordinal)
}
/// Identity accessor for compatibility — returns a reference to self.
///
/// Some callers migrated from Candle use `obj.device()` to get the device
/// handle. For `MlDevice` this is a no-op identity.
pub fn device(&self) -> &Self {
self
}
/// Returns `true` if this is a CUDA device.
pub fn is_cuda(&self) -> bool {
match self {
MlDevice::Cpu => false,
#[cfg(feature = "cuda")]
MlDevice::Cuda { .. } => true,
}
}
/// Returns `true` if this is the CPU device.
pub fn is_cpu(&self) -> bool {
matches!(self, MlDevice::Cpu)
}
/// Get the CUDA stream, or error if this is a CPU device.
#[cfg(feature = "cuda")]
pub fn cuda_stream(&self) -> Result<&Arc<CudaStream>, MLError> {
match self {
MlDevice::Cuda { stream, .. } => Ok(stream),
MlDevice::Cpu => Err(MLError::DeviceError(
"cuda_stream() called on CPU device".to_owned(),
)),
}
}
/// Get the CudaContext handle, or error if this is a CPU device.
#[cfg(feature = "cuda")]
pub fn cuda_context(&self) -> Result<&Arc<CudaContext>, MLError> {
match self {
MlDevice::Cuda { context, .. } => Ok(context),
MlDevice::Cpu => Err(MLError::DeviceError(
"cuda_context() called on CPU device".to_owned(),
)),
}
}
}
impl std::fmt::Display for MlDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MlDevice::Cpu => write!(f, "cpu"),
#[cfg(feature = "cuda")]
MlDevice::Cuda { .. } => write!(f, "cuda"),
}
}
}