Skip to main content

mesh_sieve/accelerator/
backend.rs

1//! Backend-neutral buffers and transfers.
2
3use bytemuck::{Pod, Zeroable};
4
5use super::AcceleratorError;
6
7/// Values that can be copied byte-for-byte between host and accelerator.
8#[cfg(not(feature = "cuda"))]
9pub trait DeviceValue: Pod + Zeroable + Send + Sync + 'static {}
10
11#[cfg(not(feature = "cuda"))]
12impl<T> DeviceValue for T where T: Pod + Zeroable + Send + Sync + 'static {}
13
14/// Values that can be copied byte-for-byte between host and CUDA memory.
15#[cfg(feature = "cuda")]
16pub trait DeviceValue:
17    Pod
18    + Zeroable
19    + Send
20    + Sync
21    + 'static
22    + cudarc::driver::DeviceRepr
23    + cudarc::driver::ValidAsZeroBits
24{
25}
26
27#[cfg(feature = "cuda")]
28impl<T> DeviceValue for T where
29    T: Pod
30        + Zeroable
31        + Send
32        + Sync
33        + 'static
34        + cudarc::driver::DeviceRepr
35        + cudarc::driver::ValidAsZeroBits
36{
37}
38
39/// Minimal metadata exposed by an opaque device allocation.
40pub trait DeviceBuffer<T: DeviceValue>: Send + Sync {
41    /// Number of elements in the allocation.
42    fn len(&self) -> usize;
43
44    /// Whether this allocation is empty.
45    fn is_empty(&self) -> bool {
46        self.len() == 0
47    }
48}
49
50/// Explicit memory and synchronization boundary implemented by accelerators.
51pub trait AcceleratorBackend: Send + Sync {
52    /// Backend-owned typed allocation.
53    type Buffer<T: DeviceValue>: DeviceBuffer<T>;
54    /// Backend event/fence type.
55    type Event: Send + Sync;
56    /// Native backend error.
57    type Error: std::error::Error + Send + Sync + 'static;
58
59    /// Process-local identity used to prevent mixing resources from different
60    /// backend instances. CPU resources all use identity zero.
61    fn identity(&self) -> u64;
62
63    /// Copy host values into a new device allocation.
64    fn upload<T: DeviceValue>(&self, values: &[T]) -> Result<Self::Buffer<T>, Self::Error>;
65    /// Allocate a zero-initialized device buffer.
66    fn allocate<T: DeviceValue>(&self, len: usize) -> Result<Self::Buffer<T>, Self::Error>;
67    /// Replace all values in an existing allocation.
68    fn upload_into<T: DeviceValue>(
69        &self,
70        values: &[T],
71        buffer: &mut Self::Buffer<T>,
72    ) -> Result<(), Self::Error>;
73    /// Copy a complete device allocation into a host slice.
74    fn download<T: DeviceValue>(
75        &self,
76        buffer: &Self::Buffer<T>,
77        values: &mut [T],
78    ) -> Result<(), Self::Error>;
79    /// Wait for all work submitted through this backend.
80    fn synchronize(&self) -> Result<(), Self::Error>;
81}
82
83/// Host buffer used by the reference backend.
84#[derive(Clone, Debug, PartialEq)]
85pub struct CpuBuffer<T>(pub(crate) Vec<T>);
86
87impl<T: DeviceValue> DeviceBuffer<T> for CpuBuffer<T> {
88    fn len(&self) -> usize {
89        self.0.len()
90    }
91}
92
93impl<T> CpuBuffer<T> {
94    /// Borrow values for CPU reference execution and tests.
95    pub fn as_slice(&self) -> &[T] {
96        &self.0
97    }
98
99    /// Mutably borrow values for CPU reference execution and tests.
100    pub fn as_mut_slice(&mut self) -> &mut [T] {
101        &mut self.0
102    }
103}
104
105/// Reference backend that exercises exactly the packed accelerator layouts.
106#[derive(Clone, Copy, Debug, Default)]
107pub struct CpuBackend;
108
109impl AcceleratorBackend for CpuBackend {
110    type Buffer<T: DeviceValue> = CpuBuffer<T>;
111    type Event = ();
112    type Error = AcceleratorError;
113
114    fn identity(&self) -> u64 {
115        0
116    }
117
118    fn upload<T: DeviceValue>(&self, values: &[T]) -> Result<Self::Buffer<T>, Self::Error> {
119        Ok(CpuBuffer(values.to_vec()))
120    }
121
122    fn allocate<T: DeviceValue>(&self, len: usize) -> Result<Self::Buffer<T>, Self::Error> {
123        Ok(CpuBuffer(vec![T::zeroed(); len]))
124    }
125
126    fn upload_into<T: DeviceValue>(
127        &self,
128        values: &[T],
129        buffer: &mut Self::Buffer<T>,
130    ) -> Result<(), Self::Error> {
131        if values.len() != buffer.0.len() {
132            return Err(AcceleratorError::LengthMismatch {
133                expected: buffer.0.len(),
134                found: values.len(),
135            });
136        }
137        buffer.0.copy_from_slice(values);
138        Ok(())
139    }
140
141    fn download<T: DeviceValue>(
142        &self,
143        buffer: &Self::Buffer<T>,
144        values: &mut [T],
145    ) -> Result<(), Self::Error> {
146        if values.len() != buffer.0.len() {
147            return Err(AcceleratorError::LengthMismatch {
148                expected: buffer.0.len(),
149                found: values.len(),
150            });
151        }
152        values.copy_from_slice(&buffer.0);
153        Ok(())
154    }
155
156    fn synchronize(&self) -> Result<(), Self::Error> {
157        Ok(())
158    }
159}
160
161/// User-facing execution backend preference.
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub enum ComputeBackend {
164    /// Always execute on the host reference backend.
165    Cpu,
166    /// Use the portable WGPU path where supported by the operation.
167    Wgpu,
168    /// Use the native CUDA path on this device ordinal.
169    Cuda { device_ordinal: usize },
170    /// Select the best backend that initializes successfully.
171    Auto,
172}
173
174impl ComputeBackend {
175    /// Resolve `Auto` and report both the selected backend and why.
176    ///
177    /// CUDA fallback is attempted only here, during initialization. Execution
178    /// errors are intentionally never converted into a silent CPU fallback.
179    pub fn resolve(&self) -> Result<(Self, String), AcceleratorError> {
180        match self {
181            Self::Cpu => Ok((Self::Cpu, "CPU backend explicitly requested".into())),
182            Self::Wgpu => {
183                #[cfg(feature = "wgpu")]
184                return Ok((Self::Wgpu, "WGPU backend explicitly requested".into()));
185                #[cfg(not(feature = "wgpu"))]
186                Err(AcceleratorError::BackendUnavailable(
187                    "mesh-sieve was built without the `wgpu` feature".into(),
188                ))
189            }
190            Self::Cuda { device_ordinal } => {
191                #[cfg(feature = "cuda")]
192                {
193                    super::cuda::CudaBackend::new(super::cuda::CudaOptions {
194                        device_ordinal: *device_ordinal,
195                        ..Default::default()
196                    })?;
197                    Ok((
198                        self.clone(),
199                        format!("CUDA 13.0.3 ABI device {device_ordinal} and NVRTC initialized"),
200                    ))
201                }
202                #[cfg(not(feature = "cuda"))]
203                {
204                    let _ = device_ordinal;
205                    Err(AcceleratorError::BackendUnavailable(
206                        "mesh-sieve was built without the `cuda` feature".into(),
207                    ))
208                }
209            }
210            Self::Auto => {
211                #[cfg(feature = "cuda")]
212                {
213                    return match super::cuda::CudaBackend::new(super::cuda::CudaOptions::default())
214                    {
215                        Ok(_) => Ok((
216                            Self::Cuda { device_ordinal: 0 },
217                            "CUDA 13.0.3 ABI device 0 and NVRTC initialized successfully".into(),
218                        )),
219                        Err(error) => Ok((
220                            Self::Cpu,
221                            format!("CUDA initialization failed ({error}); using CPU"),
222                        )),
223                    };
224                }
225                #[cfg(not(feature = "cuda"))]
226                Ok((
227                    Self::Cpu,
228                    "CUDA support is not compiled in; using CPU".into(),
229                ))
230            }
231        }
232    }
233}