Skip to main content

fuel_tensor/
device.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Device abstraction for CPU, CUDA, and Metal backends.
3//!
4//! ```rust
5//! use fuel_tensor::Device;
6//! let dev = Device::cpu();
7//! assert!(dev.is_cpu());
8//! assert_eq!(dev.location(), fuel_tensor::DeviceLocation::Cpu);
9//! ```
10use crate::{DType, HostBuffer, Result, Shape, WithDType};
11use fuel_backend_contract::dyn_backend::DynBackendDevice;
12use fuel_cpu_backend::dyn_impl::CpuBackendDevice;
13use std::sync::Arc;
14
15pub use fuel_ir::DeviceLocation;
16
17/// A device on which tensors can be created and computations performed.
18///
19/// Internally this is an `Arc<dyn DynBackendDevice>` — a trait-object handle
20/// to the concrete backend device.  CPU, CUDA, Metal, and arbitrary third-party
21/// backends are all accessed through this single type.
22///
23/// # Example
24///
25/// ```rust
26/// use fuel_tensor::{Device, DType};
27/// use fuel_tensor::lazy::Tensor;
28/// let dev = Device::cpu();
29/// let t = Tensor::zeros((2, 3), DType::F32, &dev)?;
30/// assert_eq!(t.shape().dims(), &[2, 3]);
31/// # Ok::<(), fuel_tensor::Error>(())
32/// ```
33#[derive(Clone, Debug)]
34pub struct Device {
35    pub(crate) inner: Arc<dyn DynBackendDevice>,
36}
37
38/// Trait for host array-like values that describe their shape and yield CPU storage.
39///
40/// Implemented for scalars, arrays, slices, and nested vecs up to 4 dimensions.
41/// It lets array-like Rust values report the [`Shape`] their data describes and
42/// materialize into a [`HostBuffer`] — the raw material a lazy tensor is built from.
43///
44/// # Example
45///
46/// ```rust
47/// use fuel_tensor::NdArray;
48/// // Scalars, arrays, and nested arrays all implement `NdArray` and report
49/// // the shape their data describes.
50/// assert_eq!(3.14f32.shape()?.dims(), &[] as &[usize]);
51/// assert_eq!((&[1f32, 2., 3.]).shape()?.dims(), &[3]);
52/// assert_eq!((&[[1f32, 2.], [3., 4.]]).shape()?.dims(), &[2, 2]);
53/// # Ok::<(), fuel_tensor::Error>(())
54/// ```
55pub trait NdArray {
56    /// Returns the shape determined by this array-like value.
57    fn shape(&self) -> Result<Shape>;
58
59    /// Converts this value into CPU storage.
60    fn to_cpu_storage(&self) -> HostBuffer;
61}
62
63impl<S: WithDType> NdArray for S {
64    fn shape(&self) -> Result<Shape> {
65        Ok(Shape::from(()))
66    }
67
68    fn to_cpu_storage(&self) -> HostBuffer {
69        S::to_cpu_storage(&[*self])
70    }
71}
72
73impl<S: WithDType, const N: usize> NdArray for &[S; N] {
74    fn shape(&self) -> Result<Shape> {
75        Ok(Shape::from(self.len()))
76    }
77
78    fn to_cpu_storage(&self) -> HostBuffer {
79        S::to_cpu_storage(self.as_slice())
80    }
81}
82
83impl<S: WithDType> NdArray for &[S] {
84    fn shape(&self) -> Result<Shape> {
85        Ok(Shape::from(self.len()))
86    }
87
88    fn to_cpu_storage(&self) -> HostBuffer {
89        S::to_cpu_storage(self)
90    }
91}
92
93impl<S: WithDType, const N: usize, const M: usize> NdArray for &[[S; N]; M] {
94    fn shape(&self) -> Result<Shape> {
95        Ok(Shape::from((M, N)))
96    }
97
98    fn to_cpu_storage(&self) -> HostBuffer {
99        S::to_cpu_storage_owned(self.concat())
100    }
101}
102
103impl<S: WithDType, const N1: usize, const N2: usize, const N3: usize> NdArray
104    for &[[[S; N3]; N2]; N1]
105{
106    fn shape(&self) -> Result<Shape> {
107        Ok(Shape::from((N1, N2, N3)))
108    }
109
110    fn to_cpu_storage(&self) -> HostBuffer {
111        let mut vec = Vec::with_capacity(N1 * N2 * N3);
112        for i1 in 0..N1 {
113            for i2 in 0..N2 {
114                vec.extend(self[i1][i2])
115            }
116        }
117        S::to_cpu_storage_owned(vec)
118    }
119}
120
121impl<S: WithDType, const N1: usize, const N2: usize, const N3: usize, const N4: usize> NdArray
122    for &[[[[S; N4]; N3]; N2]; N1]
123{
124    fn shape(&self) -> Result<Shape> {
125        Ok(Shape::from((N1, N2, N3, N4)))
126    }
127
128    fn to_cpu_storage(&self) -> HostBuffer {
129        let mut vec = Vec::with_capacity(N1 * N2 * N3 * N4);
130        for i1 in 0..N1 {
131            for i2 in 0..N2 {
132                for i3 in 0..N3 {
133                    vec.extend(self[i1][i2][i3])
134                }
135            }
136        }
137        S::to_cpu_storage_owned(vec)
138    }
139}
140
141impl<S: WithDType> NdArray for Vec<S> {
142    fn shape(&self) -> Result<Shape> {
143        Ok(Shape::from(self.len()))
144    }
145
146    fn to_cpu_storage(&self) -> HostBuffer {
147        S::to_cpu_storage(self.as_slice())
148    }
149}
150
151impl<S: WithDType> NdArray for Vec<&[S]> {
152    fn shape(&self) -> Result<Shape> {
153        if self.is_empty() {
154            crate::bail!("empty array")
155        }
156        let n = self.len();
157        let m = self[0].len();
158        for v in self.iter() {
159            if v.len() != m {
160                crate::bail!("two elements have different len {m} {}", v.len())
161            }
162        }
163        Ok(Shape::from((n, m)))
164    }
165
166    fn to_cpu_storage(&self) -> HostBuffer {
167        let data = self.iter().copied().flatten().copied().collect::<Vec<_>>();
168        S::to_cpu_storage_owned(data)
169    }
170}
171
172impl<S: WithDType> NdArray for Vec<Vec<S>> {
173    fn shape(&self) -> Result<Shape> {
174        if self.is_empty() {
175            crate::bail!("empty array")
176        }
177        let n = self.len();
178        let m = self[0].len();
179        for v in self.iter() {
180            if v.len() != m {
181                crate::bail!("two elements have different len {m} {}", v.len())
182            }
183        }
184        Ok(Shape::from((n, m)))
185    }
186
187    fn to_cpu_storage(&self) -> HostBuffer {
188        let len: usize = self.iter().map(|v| v.len()).sum();
189        let mut dst = Vec::with_capacity(len);
190        for v in self.iter() {
191            dst.extend(v.iter().copied());
192        }
193        S::to_cpu_storage_owned(dst)
194    }
195}
196
197impl<S: WithDType> NdArray for Vec<Vec<Vec<S>>> {
198    fn shape(&self) -> Result<Shape> {
199        if self.is_empty() {
200            crate::bail!("empty array")
201        }
202        let shape0 = self[0].shape()?;
203        let n = self.len();
204        for v in self.iter() {
205            let shape = v.shape()?;
206            if shape != shape0 {
207                crate::bail!("two elements have different shapes {shape:?} {shape0:?}")
208            }
209        }
210        Ok(Shape::from([[n].as_slice(), shape0.dims()].concat()))
211    }
212
213    fn to_cpu_storage(&self) -> HostBuffer {
214        if self.is_empty() {
215            return S::to_cpu_storage_owned(vec![]);
216        }
217        let len: usize = self
218            .iter()
219            .map(|v| v.iter().map(|v| v.len()).sum::<usize>())
220            .sum();
221        let mut dst = Vec::with_capacity(len);
222        for v1 in self.iter() {
223            for v2 in v1.iter() {
224                dst.extend(v2.iter().copied());
225            }
226        }
227        S::to_cpu_storage_owned(dst)
228    }
229}
230
231impl<S: WithDType> NdArray for Vec<Vec<Vec<Vec<S>>>> {
232    fn shape(&self) -> Result<Shape> {
233        if self.is_empty() {
234            crate::bail!("empty array")
235        }
236        let shape0 = self[0].shape()?;
237        let n = self.len();
238        for v in self.iter() {
239            let shape = v.shape()?;
240            if shape != shape0 {
241                crate::bail!("two elements have different shapes {shape:?} {shape0:?}")
242            }
243        }
244        Ok(Shape::from([[n].as_slice(), shape0.dims()].concat()))
245    }
246
247    fn to_cpu_storage(&self) -> HostBuffer {
248        let len: usize = self
249            .iter()
250            .map(|v| {
251                v.iter()
252                    .map(|v| v.iter().map(|v| v.len()).sum::<usize>())
253                    .sum::<usize>()
254            })
255            .sum();
256        let mut dst = Vec::with_capacity(len);
257        for v1 in self.iter() {
258            for v2 in v1.iter() {
259                for v3 in v2.iter() {
260                    dst.extend(v3.iter().copied());
261                }
262            }
263        }
264        S::to_cpu_storage_owned(dst)
265    }
266}
267
268impl Device {
269    /// Returns a CPU device.
270    ///
271    /// # Example
272    ///
273    /// ```rust
274    /// use fuel_tensor::Device;
275    /// let dev = Device::cpu();
276    /// assert!(dev.is_cpu());
277    /// ```
278    pub fn cpu() -> Self {
279        Device {
280            inner: Arc::new(CpuBackendDevice),
281        }
282    }
283
284    /// Creates a device backed by a custom [`DynBackendDevice`].
285    ///
286    /// The device uses dynamic dispatch for all operations, enabling
287    /// third-party backends without modifying `fuel-core`.
288    pub fn custom(device: Arc<dyn DynBackendDevice>) -> Self {
289        Device { inner: device }
290    }
291
292    /// Borrow the underlying `Arc<dyn DynBackendDevice>`.
293    ///
294    /// Phase 7.5 work item G2: fuel-graph's slot-populating
295    /// constructors (`Tensor::from_f32`, `const_f32_like`, etc.) take
296    /// a `&Arc<dyn DynBackendDevice>` so they can allocate Storage on
297    /// the right device without depending on fuel-core. Callers that
298    /// hold a `&fuel_tensor::Device` thread it through with
299    /// `dev.as_dyn()`.
300    pub fn as_dyn(&self) -> &Arc<dyn DynBackendDevice> {
301        &self.inner
302    }
303
304    /// Returns `true` if this is a custom (third-party) device.
305    pub fn is_custom(&self) -> bool {
306        !self.is_cpu() && !self.is_cuda() && !self.is_metal()
307    }
308
309    /// Sets the random seed for this device's random number generator.
310    pub fn set_seed(&self, seed: u64) -> Result<()> {
311        self.inner.set_seed_dyn(seed)
312    }
313
314    /// Returns the current random seed for this device.
315    pub fn get_current_seed(&self) -> Result<u64> {
316        self.inner.get_current_seed_dyn()
317    }
318
319    /// Returns `true` if both devices refer to the same physical device.
320    ///
321    /// # Example
322    ///
323    /// ```rust
324    /// use fuel_tensor::Device;
325    /// assert!(Device::cpu().same_device(&Device::cpu()));
326    /// ```
327    pub fn same_device(&self, rhs: &Self) -> bool {
328        self.inner.same_device_dyn(&*rhs.inner)
329    }
330
331    /// Returns the physical [`DeviceLocation`] for this device.
332    ///
333    /// # Example
334    ///
335    /// ```rust
336    /// use fuel_tensor::{Device, DeviceLocation};
337    /// assert_eq!(Device::cpu().location(), DeviceLocation::Cpu);
338    /// ```
339    pub fn location(&self) -> DeviceLocation {
340        self.inner.location_dyn()
341    }
342
343    /// Returns `true` if this is the CPU device.
344    ///
345    /// # Example
346    ///
347    /// ```rust
348    /// use fuel_tensor::Device;
349    /// assert!(Device::cpu().is_cpu());
350    /// ```
351    pub fn is_cpu(&self) -> bool {
352        matches!(self.location(), DeviceLocation::Cpu)
353    }
354
355    /// Returns `true` if this is a CUDA device.
356    ///
357    /// # Example
358    ///
359    /// ```rust
360    /// use fuel_tensor::Device;
361    /// assert!(!Device::cpu().is_cuda());
362    /// ```
363    pub fn is_cuda(&self) -> bool {
364        matches!(self.location(), DeviceLocation::Cuda { .. })
365    }
366
367    /// Returns `true` if this is a Metal device.
368    ///
369    /// # Example
370    ///
371    /// ```rust
372    /// use fuel_tensor::Device;
373    /// assert!(!Device::cpu().is_metal());
374    /// ```
375    pub fn is_metal(&self) -> bool {
376        matches!(self.location(), DeviceLocation::Metal { .. })
377    }
378
379    /// Returns `true` if this device has native BF16 support.
380    ///
381    /// # Example
382    ///
383    /// ```rust
384    /// use fuel_tensor::Device;
385    /// // CPU does not have native BF16 support
386    /// assert!(!Device::cpu().supports_bf16());
387    /// ```
388    pub fn supports_bf16(&self) -> bool {
389        self.inner.supports_bf16()
390    }
391
392    /// Returns [`DType::BF16`] if supported, otherwise [`DType::F32`].
393    ///
394    /// # Example
395    ///
396    /// ```rust
397    /// use fuel_tensor::{Device, DType};
398    /// assert_eq!(Device::cpu().bf16_default_to_f32(), DType::F32);
399    /// ```
400    pub fn bf16_default_to_f32(&self) -> DType {
401        if self.supports_bf16() {
402            DType::BF16
403        } else {
404            DType::F32
405        }
406    }
407
408    /// Synchronizes the device, waiting for all pending operations to complete.
409    ///
410    /// This is a no-op on CPU.
411    ///
412    /// ```rust
413    /// use fuel_tensor::Device;
414    /// Device::cpu().synchronize()?;
415    /// # Ok::<(), fuel_tensor::Error>(())
416    /// ```
417    pub fn synchronize(&self) -> Result<()> {
418        self.inner.synchronize_dyn()
419    }
420}