Skip to main content

oxmera_tensor/
storage.rs

1//! Storage: the owned buffer behind one or more tensors.
2
3use std::any::Any;
4use std::sync::Arc;
5
6use oxmera_core::{DType, Device, Error, Result};
7
8/// Typed host memory for the CPU backend.
9///
10/// One variant per supported dtype, so element access never reinterprets
11/// bytes and the crate stays free of `unsafe` on the CPU path.
12#[derive(Debug, Clone)]
13pub enum CpuStorage {
14    /// 32-bit floats.
15    F32(Vec<f32>),
16    /// 64-bit signed integers (indices, argmax results, class targets).
17    I64(Vec<i64>),
18    /// Raw bytes (`U8`/`Bool`).
19    U8(Vec<u8>),
20}
21
22impl CpuStorage {
23    /// The dtype this buffer holds.
24    pub fn dtype(&self) -> DType {
25        match self {
26            CpuStorage::F32(_) => DType::F32,
27            CpuStorage::I64(_) => DType::I64,
28            CpuStorage::U8(_) => DType::U8,
29        }
30    }
31
32    /// Number of elements in the buffer.
33    pub fn len(&self) -> usize {
34        match self {
35            CpuStorage::F32(v) => v.len(),
36            CpuStorage::I64(v) => v.len(),
37            CpuStorage::U8(v) => v.len(),
38        }
39    }
40
41    /// Whether the buffer holds no elements.
42    pub fn is_empty(&self) -> bool {
43        self.len() == 0
44    }
45
46    /// The `f32` elements, or a typed error for other dtypes.
47    pub fn f32s(&self) -> Result<&[f32]> {
48        match self {
49            CpuStorage::F32(v) => Ok(v),
50            other => Err(Error::DTypeMismatch {
51                expected: DType::F32,
52                got: other.dtype(),
53                op: "CpuStorage::f32s",
54            }),
55        }
56    }
57
58    /// The `i64` elements, or a typed error for other dtypes.
59    pub fn i64s(&self) -> Result<&[i64]> {
60        match self {
61            CpuStorage::I64(v) => Ok(v),
62            other => Err(Error::DTypeMismatch {
63                expected: DType::I64,
64                got: other.dtype(),
65                op: "CpuStorage::i64s",
66            }),
67        }
68    }
69}
70
71/// A Metal buffer that is safe to share across threads.
72///
73/// `metal::Buffer` is a smart pointer to an `MTLBuffer`.
74#[cfg(target_os = "macos")]
75#[derive(Debug)]
76pub struct MetalBuffer {
77    buffer: metal::Buffer,
78    /// The Metal device index the buffer was allocated on.
79    pub device_index: usize,
80}
81
82#[cfg(target_os = "macos")]
83impl MetalBuffer {
84    /// Wrap a Metal buffer allocated on device `device_index`.
85    pub fn new(buffer: metal::Buffer, device_index: usize) -> Self {
86        Self {
87            buffer,
88            device_index,
89        }
90    }
91
92    /// The underlying Metal buffer.
93    pub fn buffer(&self) -> &metal::Buffer {
94        &self.buffer
95    }
96}
97
98// SAFETY: MTLBuffer is documented by Apple as thread-safe ("Most Metal
99// objects can be used from multiple threads"; command *encoders* are the
100// exception and are never stored here). The wrapper only hands out shared
101// references; all mutation happens through Metal command buffers, which
102// serialize on the command queue.
103#[cfg(target_os = "macos")]
104#[allow(unsafe_code)]
105unsafe impl Send for MetalBuffer {}
106// SAFETY: see the `Send` justification above; `&MetalBuffer` exposes no
107// interior mutability outside Metal's own synchronized command path.
108#[cfg(target_os = "macos")]
109#[allow(unsafe_code)]
110unsafe impl Sync for MetalBuffer {}
111
112/// A device buffer owned by a backend crate the tensor hub does not depend
113/// on (the CUDA backend keeps its `cudarc` allocation here). The hub sees
114/// only its element count and device; the owning backend downcasts
115/// `inner` back to its concrete buffer type.
116pub struct OpaqueBuffer {
117    inner: Arc<dyn Any + Send + Sync>,
118    len: usize,
119}
120
121impl OpaqueBuffer {
122    /// Wrap a backend allocation holding `len` elements.
123    pub fn new(inner: Arc<dyn Any + Send + Sync>, len: usize) -> Self {
124        Self { inner, len }
125    }
126
127    /// The backend's allocation, for it to downcast.
128    pub fn inner(&self) -> &Arc<dyn Any + Send + Sync> {
129        &self.inner
130    }
131
132    /// Elements the allocation holds.
133    pub fn len(&self) -> usize {
134        self.len
135    }
136
137    /// Whether the allocation holds no elements.
138    pub fn is_empty(&self) -> bool {
139        self.len == 0
140    }
141}
142
143impl std::fmt::Debug for OpaqueBuffer {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.debug_struct("OpaqueBuffer")
146            .field("len", &self.len)
147            .finish_non_exhaustive()
148    }
149}
150
151/// Where the bytes actually live.
152#[derive(Debug)]
153pub enum StorageData {
154    /// Host memory for the CPU backend.
155    Cpu(CpuStorage),
156    /// A GPU buffer for the Apple Metal backend.
157    #[cfg(target_os = "macos")]
158    Metal(MetalBuffer),
159    /// A buffer owned by an out-of-tree backend (see [`OpaqueBuffer`]).
160    Opaque(OpaqueBuffer),
161}
162
163/// An owned, reference-counted buffer of elements on one device.
164///
165/// Multiple tensors (views) may share one storage; storage never knows how
166/// many.
167#[derive(Debug)]
168pub struct Storage {
169    data: StorageData,
170    dtype: DType,
171    device: Device,
172}
173
174impl Storage {
175    /// Zero-initialized CPU storage for `numel` `f32` elements.
176    pub fn cpu_f32_zeros(numel: usize) -> Self {
177        Self::from_f32_vec(vec![0.0; numel])
178    }
179
180    /// CPU storage owning `data` as `f32` elements.
181    pub fn from_f32_vec(data: Vec<f32>) -> Self {
182        Self {
183            data: StorageData::Cpu(CpuStorage::F32(data)),
184            dtype: DType::F32,
185            device: Device::Cpu,
186        }
187    }
188
189    /// CPU storage owning `data` as `i64` elements.
190    pub fn from_i64_vec(data: Vec<i64>) -> Self {
191        Self {
192            data: StorageData::Cpu(CpuStorage::I64(data)),
193            dtype: DType::I64,
194            device: Device::Cpu,
195        }
196    }
197
198    /// Storage backed by a Metal buffer holding `dtype` elements.
199    #[cfg(target_os = "macos")]
200    pub fn from_metal(buffer: MetalBuffer, dtype: DType) -> Self {
201        let device = Device::Metal {
202            index: buffer.device_index,
203        };
204        Self {
205            data: StorageData::Metal(buffer),
206            dtype,
207            device,
208        }
209    }
210
211    /// The element type of this buffer.
212    pub fn dtype(&self) -> DType {
213        self.dtype
214    }
215
216    /// The device this buffer lives on.
217    pub fn device(&self) -> Device {
218        self.device
219    }
220
221    /// The raw storage data.
222    pub fn data(&self) -> &StorageData {
223        &self.data
224    }
225
226    /// The typed CPU buffer, or a typed error when the storage is on a
227    /// different device.
228    pub fn cpu(&self) -> Result<&CpuStorage> {
229        match &self.data {
230            StorageData::Cpu(c) => Ok(c),
231            _ => Err(Error::DeviceMismatch {
232                lhs: self.device,
233                rhs: Device::Cpu,
234                op: "Storage::cpu",
235            }),
236        }
237    }
238
239    /// Storage backed by an out-of-tree backend's allocation.
240    pub fn from_opaque(buffer: OpaqueBuffer, dtype: DType, device: Device) -> Self {
241        Self {
242            data: StorageData::Opaque(buffer),
243            dtype,
244            device,
245        }
246    }
247
248    /// The opaque buffer, when this storage lives on an out-of-tree backend.
249    pub fn opaque(&self) -> Option<&OpaqueBuffer> {
250        match &self.data {
251            StorageData::Opaque(b) => Some(b),
252            _ => None,
253        }
254    }
255}