Skip to main content

oxicuda_backend/
null.rs

1//! A backend that refuses every operation.
2//!
3//! [`NullBackend`] implements [`ComputeBackend`](crate::ComputeBackend) by
4//! returning [`BackendError::Unsupported`]
5//! from every compute/memory method. It is a **test scaffold**: a consumer
6//! crate can compile and exercise its dispatch logic against
7//! `Box<dyn ComputeBackend>` without pulling in any real GPU backend, and
8//! can assert that the "no backend available" path is handled gracefully.
9
10use crate::ComputeBackend;
11use crate::capabilities::{Capabilities, DeviceInfo};
12use crate::error::{BackendError, BackendResult};
13use crate::ops::{BackendTranspose, BinaryOp, ReduceOp, UnaryOp};
14
15/// A no-op backend whose operations all return
16/// [`BackendError::Unsupported`].
17#[derive(Debug, Default, Clone, Copy)]
18pub struct NullBackend;
19
20impl NullBackend {
21    /// Create a new null backend.
22    #[must_use]
23    pub const fn new() -> Self {
24        Self
25    }
26
27    /// Build the standard `Unsupported` error naming the offending op.
28    fn deny(op: &str) -> BackendError {
29        BackendError::Unsupported(format!("null backend cannot perform `{op}`"))
30    }
31}
32
33impl ComputeBackend for NullBackend {
34    fn name(&self) -> &str {
35        "null"
36    }
37
38    fn init(&mut self) -> BackendResult<()> {
39        // Initialization itself is allowed (so the type is constructible in
40        // a pipeline); only actual work is refused.
41        Ok(())
42    }
43
44    fn is_initialized(&self) -> bool {
45        true
46    }
47
48    fn capabilities(&self) -> Capabilities {
49        // A null backend advertises no capabilities at all.
50        Capabilities {
51            max_threads_per_block: 0,
52            max_shared_mem_per_block: 0,
53            warp_size: 0,
54            unified_memory: false,
55            ..Capabilities::cpu()
56        }
57    }
58
59    fn available_devices(&self) -> BackendResult<Vec<DeviceInfo>> {
60        Ok(Vec::new())
61    }
62
63    fn gemm(
64        &self,
65        _trans_a: BackendTranspose,
66        _trans_b: BackendTranspose,
67        _m: usize,
68        _n: usize,
69        _k: usize,
70        _alpha: f64,
71        _a_ptr: u64,
72        _lda: usize,
73        _b_ptr: u64,
74        _ldb: usize,
75        _beta: f64,
76        _c_ptr: u64,
77        _ldc: usize,
78    ) -> BackendResult<()> {
79        Err(Self::deny("gemm"))
80    }
81
82    fn conv2d_forward(
83        &self,
84        _input_ptr: u64,
85        _input_shape: &[usize],
86        _filter_ptr: u64,
87        _filter_shape: &[usize],
88        _output_ptr: u64,
89        _output_shape: &[usize],
90        _stride: &[usize],
91        _padding: &[usize],
92    ) -> BackendResult<()> {
93        Err(Self::deny("conv2d_forward"))
94    }
95
96    fn attention(
97        &self,
98        _q_ptr: u64,
99        _k_ptr: u64,
100        _v_ptr: u64,
101        _o_ptr: u64,
102        _batch: usize,
103        _heads: usize,
104        _seq_q: usize,
105        _seq_kv: usize,
106        _head_dim: usize,
107        _scale: f64,
108        _causal: bool,
109    ) -> BackendResult<()> {
110        Err(Self::deny("attention"))
111    }
112
113    fn reduce(
114        &self,
115        _op: ReduceOp,
116        _input_ptr: u64,
117        _output_ptr: u64,
118        _shape: &[usize],
119        _axis: usize,
120    ) -> BackendResult<()> {
121        Err(Self::deny("reduce"))
122    }
123
124    fn unary(
125        &self,
126        _op: UnaryOp,
127        _input_ptr: u64,
128        _output_ptr: u64,
129        _n: usize,
130    ) -> BackendResult<()> {
131        Err(Self::deny("unary"))
132    }
133
134    fn binary(
135        &self,
136        _op: BinaryOp,
137        _a_ptr: u64,
138        _b_ptr: u64,
139        _output_ptr: u64,
140        _n: usize,
141    ) -> BackendResult<()> {
142        Err(Self::deny("binary"))
143    }
144
145    fn synchronize(&self) -> BackendResult<()> {
146        Ok(())
147    }
148
149    fn alloc(&self, _bytes: usize) -> BackendResult<u64> {
150        Err(Self::deny("alloc"))
151    }
152
153    fn free(&self, _ptr: u64) -> BackendResult<()> {
154        Err(Self::deny("free"))
155    }
156
157    fn copy_htod(&self, _dst: u64, _src: &[u8]) -> BackendResult<()> {
158        Err(Self::deny("copy_htod"))
159    }
160
161    fn copy_dtoh(&self, _dst: &mut [u8], _src: u64) -> BackendResult<()> {
162        Err(Self::deny("copy_dtoh"))
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn null_backend_name_and_init() {
172        let mut be = NullBackend::new();
173        assert_eq!(be.name(), "null");
174        assert!(be.init().is_ok());
175        assert!(be.is_initialized());
176    }
177
178    #[test]
179    fn every_compute_op_is_unsupported() {
180        let be = NullBackend::new();
181        assert!(matches!(
182            be.gemm(
183                BackendTranspose::NoTrans,
184                BackendTranspose::NoTrans,
185                1,
186                1,
187                1,
188                1.0,
189                0,
190                1,
191                0,
192                1,
193                0.0,
194                0,
195                1
196            ),
197            Err(BackendError::Unsupported(_))
198        ));
199        assert!(matches!(
200            be.unary(UnaryOp::Relu, 0, 0, 1),
201            Err(BackendError::Unsupported(_))
202        ));
203        assert!(matches!(
204            be.binary(BinaryOp::Add, 0, 0, 0, 1),
205            Err(BackendError::Unsupported(_))
206        ));
207        assert!(matches!(
208            be.reduce(ReduceOp::Sum, 0, 0, &[1], 0),
209            Err(BackendError::Unsupported(_))
210        ));
211        assert!(matches!(be.alloc(16), Err(BackendError::Unsupported(_))));
212        assert!(matches!(
213            be.copy_htod(0, &[0u8; 4]),
214            Err(BackendError::Unsupported(_))
215        ));
216    }
217
218    #[test]
219    fn synchronize_is_noop_ok() {
220        assert!(NullBackend::new().synchronize().is_ok());
221    }
222
223    #[test]
224    fn no_devices_and_no_capabilities() {
225        let be = NullBackend::new();
226        assert!(be.available_devices().unwrap().is_empty());
227        let caps = be.capabilities();
228        assert_eq!(caps.max_threads_per_block, 0);
229        assert!(!caps.unified_memory);
230    }
231
232    #[test]
233    fn error_message_names_the_op() {
234        let be = NullBackend::new();
235        let err = be.unary(UnaryOp::Exp, 0, 0, 1).unwrap_err();
236        assert!(err.to_string().contains("unary"));
237    }
238
239    #[test]
240    fn usable_as_boxed_trait_object() {
241        let be: Box<dyn ComputeBackend> = Box::new(NullBackend::new());
242        assert_eq!(be.name(), "null");
243        assert!(be.synchronize().is_ok());
244    }
245}