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