1use crate::ComputeBackend;
11use crate::capabilities::{Capabilities, DeviceInfo};
12use crate::error::{BackendError, BackendResult};
13use crate::ops::{BackendTranspose, BinaryOp, ReduceOp, UnaryOp};
14
15#[derive(Debug, Default, Clone, Copy)]
18pub struct NullBackend;
19
20impl NullBackend {
21 #[must_use]
23 pub const fn new() -> Self {
24 Self
25 }
26
27 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 Ok(())
42 }
43
44 fn is_initialized(&self) -> bool {
45 true
46 }
47
48 fn capabilities(&self) -> Capabilities {
49 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}