Skip to main content

oxidelake_core/
hardware.rs

1//! Hardware backend and device identifiers.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8use crate::EngineError;
9
10/// The execution backends OxideLake knows about.
11///
12/// `CpuSimd` is always available and is the correctness reference; the GPU
13/// backends are opt-in cargo features and are selected at runtime by the
14/// hardware detector.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[non_exhaustive]
17pub enum BackendKind {
18    /// Vectorized CPU execution through Arrow/DataFusion kernels and rayon.
19    CpuSimd,
20    /// NVIDIA CUDA (feature `cuda`).
21    Cuda,
22    /// Apple Metal on unified memory (feature `metal`, macOS only).
23    Metal,
24}
25
26impl BackendKind {
27    /// Every backend kind, in detection priority order (GPU first).
28    pub const ALL: [BackendKind; 3] = [BackendKind::Cuda, BackendKind::Metal, BackendKind::CpuSimd];
29
30    /// The canonical lowercase name used in environment variables, `EXPLAIN`
31    /// tags and logs.
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            BackendKind::CpuSimd => "cpu",
35            BackendKind::Cuda => "cuda",
36            BackendKind::Metal => "metal",
37        }
38    }
39
40    /// `true` for backends that execute on a device rather than the host CPU.
41    pub const fn is_gpu(self) -> bool {
42        !matches!(self, BackendKind::CpuSimd)
43    }
44}
45
46impl fmt::Display for BackendKind {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(self.as_str())
49    }
50}
51
52impl FromStr for BackendKind {
53    type Err = EngineError;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        match s.trim().to_ascii_lowercase().as_str() {
57            "cpu" | "cpusimd" | "cpu-simd" | "cpu_simd" => Ok(BackendKind::CpuSimd),
58            "cuda" => Ok(BackendKind::Cuda),
59            "metal" => Ok(BackendKind::Metal),
60            other => Err(EngineError::plan(format!(
61                "unknown backend '{other}'; expected one of cpu, cuda, metal"
62            ))),
63        }
64    }
65}
66
67/// Identifies one device of a backend, e.g. `cuda:0`.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub struct DeviceId {
70    /// The backend the device belongs to.
71    pub backend: BackendKind,
72    /// The device ordinal within that backend (always `0` for the CPU).
73    pub ordinal: u32,
74}
75
76impl DeviceId {
77    /// The host CPU.
78    pub const CPU: DeviceId = DeviceId {
79        backend: BackendKind::CpuSimd,
80        ordinal: 0,
81    };
82
83    /// Builds a device identifier.
84    pub const fn new(backend: BackendKind, ordinal: u32) -> Self {
85        Self { backend, ordinal }
86    }
87}
88
89impl fmt::Display for DeviceId {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "{}:{}", self.backend, self.ordinal)
92    }
93}
94
95#[cfg(test)]
96#[allow(clippy::unwrap_used, clippy::expect_used)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn parses_all_names_case_insensitively() {
102        assert_eq!("cpu".parse::<BackendKind>().unwrap(), BackendKind::CpuSimd);
103        assert_eq!(" CUDA ".parse::<BackendKind>().unwrap(), BackendKind::Cuda);
104        assert_eq!("Metal".parse::<BackendKind>().unwrap(), BackendKind::Metal);
105        assert!("tpu".parse::<BackendKind>().is_err());
106    }
107
108    #[test]
109    fn display_round_trips_through_parse() {
110        for kind in BackendKind::ALL {
111            assert_eq!(kind.as_str().parse::<BackendKind>().unwrap(), kind);
112        }
113    }
114
115    #[test]
116    fn device_id_display() {
117        assert_eq!(DeviceId::new(BackendKind::Cuda, 1).to_string(), "cuda:1");
118        assert_eq!(DeviceId::CPU.to_string(), "cpu:0");
119        assert!(!BackendKind::CpuSimd.is_gpu());
120        assert!(BackendKind::Metal.is_gpu());
121    }
122}