Skip to main content

fuel_memory/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! # fuel-storage
3//!
4//! Unified storage abstraction for fuel. Phase 7.5 foundation work item
5//! (see [docs/storage-unification.md](../../docs/storage-unification.md)).
6//!
7//! `Storage` is the single entry point that holds bytes, a dtype tag,
8//! and a backend memory region (closed enum over CPU/CUDA/Vulkan/Metal).
9//! Backends provide *kernels* that operate on these types — backend
10//! storage types live in their own crates and implement the
11//! `fuel_ir::backend::BackendStorage` trait.
12//!
13//! This crate now owns ONLY the closed-enum dispatch wrapper + the
14//! public `Storage` API. Dispatch infrastructure (KernelBindingTable,
15//! registration wrappers, CompiledNode, ExecutionPlan, the picker,
16//! FusedKernelRegistry, cost functions, the PipelinedExecutor) was
17//! extracted to [`fuel-dispatch`](../../fuel-dispatch/) 2026-05-31;
18//! see [docs/session-prompts/dispatch-move-to-fuel-core.md](
19//! ../../docs/session-prompts/dispatch-move-to-fuel-core.md) for the
20//! move's rationale.
21//!
22//! ## Where things live
23//!
24//! - `fuel_ir::backend::BackendStorage` — the abstract trait
25//!   (just `len_bytes()` today; alloc/copy_from land in A4).
26//! - [`fuel_cpu_backend::CpuStorageBytes`] — CPU storage (Phase A3.0).
27//!   Bytes-based, 64-byte aligned, `Arc`-clonable, CoW on mutation.
28//! - `fuel_metal_backend::MetalStorageBytes` — Metal storage (A3.1).
29//! - `fuel_cuda_backend::CudaStorageBytes` — CUDA storage (A3.2).
30//! - `fuel_vulkan_backend::VulkanStorageBytes` — Vulkan storage (A3.3).
31//! - **`fuel_dispatch`** — every binding-table, registration wrapper,
32//!   picker, executor, and cost-fn that used to live here.
33
34/// Vulkan storage variant — re-exported from fuel-vulkan-backend when
35/// the vulkan feature is enabled.
36#[cfg(feature = "vulkan")]
37pub use fuel_vulkan_backend::VulkanStorageBytes as VulkanStorage;
38
39/// CUDA storage variant — re-exported from fuel-cuda-backend when
40/// the cuda feature is enabled.
41#[cfg(feature = "cuda")]
42pub use fuel_cuda_backend::CudaStorageBytes as CudaStorage;
43
44/// Metal storage variant — re-exported from fuel-metal-backend on
45/// Apple platforms when the metal feature is enabled. The metal
46/// feature has no effect on non-Apple platforms (the dep is
47/// target-gated).
48#[cfg(all(feature = "metal", any(target_os = "macos", target_os = "ios")))]
49pub use fuel_metal_backend::MetalStorageBytes as MetalStorage;
50
51use fuel_cpu_backend::CpuStorageBytes;
52use fuel_ir::storage::OutputView;
53use fuel_ir::{DType, Result, SType};
54use std::sync::Arc;
55
56#[cfg(feature = "dlpack")]
57pub mod dlpack_view;
58
59/// Closed enum over backend storage variants. The `Cpu` variant
60/// holds [`CpuStorageBytes`] from `fuel-cpu-backend`. GPU variants
61/// (feature-gated) hold the per-backend `*StorageBytes` types from
62/// each backend crate.
63#[derive(Debug)]
64pub enum BackendStorage {
65    Cpu(CpuStorageBytes),
66    #[cfg(feature = "cuda")]
67    Cuda(CudaStorage),
68    #[cfg(feature = "vulkan")]
69    Vulkan(VulkanStorage),
70    #[cfg(all(feature = "metal", any(target_os = "macos", target_os = "ios")))]
71    Metal(MetalStorage),
72}
73
74/// Top-level storage type: byte-erased payload + runtime dtype tag.
75/// Layout (shape + strides + start_offset) lives separately on the
76/// consuming `Tensor` — `Storage` owns only the bytes and which
77/// device/dtype they represent.
78///
79/// Optionally carries a `bundle` side-table describing how the inner
80/// byte buffer is partitioned into multiple logically independent
81/// outputs. Set by multi-output op authors via
82/// [`Storage::with_bundle`] / [`Storage::new_bundled`]; consumed by
83/// `Op::View` / `Op::ViewOwned` at realize time. See
84/// [`OutputView`](fuel_ir::storage::OutputView).
85#[derive(Debug)]
86pub struct Storage {
87    /// Backend variant + the bytes themselves.
88    pub inner: BackendStorage,
89    /// How to interpret the bytes. Storage's `len_bytes` is the byte
90    /// count; the element count is `len_bytes / dtype.size_in_bytes()`.
91    pub dtype: DType,
92    /// `None` for single-output storage (today's default). `Some(_)`
93    /// for multi-output bundles: a shared Arc'd slice of per-slot
94    /// [`OutputView`] entries, one per logical output. `Op::View`
95    /// nodes share this Arc so the bundle stays alive as long as any
96    /// view holds a reference.
97    pub bundle: Option<Arc<[OutputView]>>,
98    /// How the bytes are ENCODED (orthogonal to `dtype`, which is the
99    /// logical element type). Empty = plain dense `dtype`, byte-identical
100    /// to pre-SType behaviour. v1: PRIMARY storage only — bundle slots
101    /// keep `dtype` only (per-slot SType is a future addition). See
102    /// [`fuel_ir::SType`] and
103    /// `docs/specs/storage-encoding.md`.
104    pub stype: SType,
105}
106
107/// Feature-aware match over `BackendStorage` variants. Used wherever
108/// the dispatch shape `match s { Cpu(...) => ..., Cuda(...) => ... }`
109/// would otherwise need `#[cfg(feature = "...")]` arms inline.
110///
111/// ```
112/// # use fuel_memory::{BackendStorage, dispatch_storage};
113/// fn len_bytes(s: &BackendStorage) -> usize {
114///     dispatch_storage!(s, inner => inner.len_bytes())
115/// }
116/// ```
117#[macro_export]
118macro_rules! dispatch_storage {
119    ($s:expr, $name:ident => $body:expr) => {
120        match $s {
121            $crate::BackendStorage::Cpu($name) => $body,
122            #[cfg(feature = "cuda")]
123            $crate::BackendStorage::Cuda($name) => $body,
124            #[cfg(feature = "vulkan")]
125            $crate::BackendStorage::Vulkan($name) => $body,
126            #[cfg(all(feature = "metal", any(target_os = "macos", target_os = "ios")))]
127            $crate::BackendStorage::Metal($name) => $body,
128        }
129    };
130}
131
132impl BackendStorage {
133    /// Total addressable byte count, regardless of dtype.
134    pub fn len_bytes(&self) -> usize {
135        dispatch_storage!(self, inner => inner.len_bytes())
136    }
137}
138
139impl Storage {
140    /// Build a Storage from an already-allocated backend variant
141    /// plus its dtype tag. Single-output (no bundle metadata); use
142    /// [`Self::with_bundle`] / [`Self::new_bundled`] for the
143    /// multi-output case.
144    pub fn new(inner: BackendStorage, dtype: DType) -> Self {
145        Self {
146            inner,
147            dtype,
148            bundle: None,
149            stype: SType::default(),
150        }
151    }
152
153    /// Build a Storage from a backend variant + dtype tag + bundle
154    /// side-table in one shot. Validates that the bundle is
155    /// non-empty and that slot 0's dtype matches the storage's
156    /// primary dtype (the bundled-storage invariant: slot 0 IS the
157    /// primary).
158    pub fn new_bundled(
159        inner: BackendStorage,
160        dtype: DType,
161        bundle: Arc<[OutputView]>,
162    ) -> Result<Self> {
163        Self::validate_bundle(dtype, &bundle)?;
164        Ok(Self {
165            inner,
166            dtype,
167            bundle: Some(bundle),
168            stype: SType::default(),
169        })
170    }
171
172    /// Attach a bundle side-table to an existing single-output
173    /// Storage. Same validation as [`Self::new_bundled`]; panics in
174    /// debug mode if a bundle is already attached (re-bundling is a
175    /// contract bug).
176    pub fn with_bundle(mut self, bundle: Arc<[OutputView]>) -> Result<Self> {
177        debug_assert!(
178            self.bundle.is_none(),
179            "Storage::with_bundle: bundle already attached",
180        );
181        Self::validate_bundle(self.dtype, &bundle)?;
182        self.bundle = Some(bundle);
183        Ok(self)
184    }
185
186    fn validate_bundle(primary_dtype: DType, bundle: &Arc<[OutputView]>) -> Result<()> {
187        if bundle.is_empty() {
188            return Err(fuel_ir::Error::Msg(
189                "Storage::with_bundle: bundle slice must be non-empty".into(),
190            )
191            .bt());
192        }
193        let slot0 = &bundle[0];
194        if slot0.dtype != primary_dtype {
195            return Err(fuel_ir::Error::Msg(format!(
196                "Storage::with_bundle: slot 0 dtype {:?} must match \
197                 Storage's primary dtype {:?}",
198                slot0.dtype, primary_dtype,
199            ))
200            .bt());
201        }
202        Ok(())
203    }
204
205    /// Whether this storage carries multi-output bundle metadata.
206    pub fn is_bundled(&self) -> bool {
207        self.bundle.is_some()
208    }
209
210    /// Number of logical output slots. `1` for single-output;
211    /// `bundle.len()` for bundled.
212    pub fn slot_count(&self) -> usize {
213        self.bundle.as_ref().map_or(1, |b| b.len())
214    }
215
216    /// Borrow the bundle slice, or `None` for single-output storage.
217    pub fn bundle(&self) -> Option<&[OutputView]> {
218        self.bundle.as_deref()
219    }
220
221    /// Per-slot view; `None` for out-of-range or single-output.
222    pub fn slot_view(&self, idx: usize) -> Option<&OutputView> {
223        self.bundle.as_deref().and_then(|b| b.get(idx))
224    }
225
226    /// The `DType` tag attached to these bytes.
227    pub fn dtype(&self) -> DType {
228        self.dtype
229    }
230
231    /// Total addressable byte count.
232    pub fn len_bytes(&self) -> usize {
233        self.inner.len_bytes()
234    }
235
236    /// Element count = `len_bytes / dtype.size_in_bytes()`.
237    pub fn elem_count(&self) -> usize {
238        let bps = self.dtype.size_in_bytes();
239        self.len_bytes().checked_div(bps).unwrap_or(0)
240    }
241
242    /// Attach an encoding scheme to this storage (consuming builder).
243    /// Does not touch the bytes or the logical dtype — only describes
244    /// HOW the bytes are encoded. See [`fuel_ir::SType`].
245    pub fn with_stype(mut self, stype: SType) -> Self {
246        self.stype = stype;
247        self
248    }
249
250    /// The encoding scheme. Empty = plain dense `dtype`.
251    pub fn stype(&self) -> &SType {
252        &self.stype
253    }
254}
255
256/// Allocate freshly on the CPU backend with the given dtype + element
257/// count. Bytes are zero-initialized and 64-byte aligned (suitable
258/// for AVX-512 SIMD).
259pub fn alloc_cpu_zeroed(dtype: DType, elem_count: usize) -> Result<Storage> {
260    let len_bytes = elem_count.saturating_mul(dtype.size_in_bytes());
261    Ok(Storage::new(
262        BackendStorage::Cpu(CpuStorageBytes::from_zero_bytes(len_bytes)),
263        dtype,
264    ))
265}
266
267/// Build a CPU `Storage` from a typed slice, copying the bytes. The
268/// result has the dtype matching `T` and is 64-byte aligned.
269pub fn from_slice_cpu<T: bytemuck::Pod + fuel_ir::WithDType>(data: &[T]) -> Storage {
270    Storage::new(
271        BackendStorage::Cpu(CpuStorageBytes::from_slice(data)),
272        T::DTYPE,
273    )
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    /// Smoke: building a Storage via the CPU backend and reading back
281    /// dtype + len_bytes + elem_count works.
282    #[test]
283    fn cpu_storage_basic_shape() {
284        let s = alloc_cpu_zeroed(DType::F32, 4).expect("alloc");
285        assert_eq!(s.dtype(), DType::F32);
286        assert_eq!(s.len_bytes(), 16);
287        assert_eq!(s.elem_count(), 4);
288    }
289
290    /// Smoke: dispatch_storage! macro picks the right variant arm.
291    #[test]
292    fn dispatch_macro_routes_to_variant() {
293        let bs = BackendStorage::Cpu(CpuStorageBytes::from_zero_bytes(8));
294        let n = dispatch_storage!(&bs, inner => inner.len_bytes());
295        assert_eq!(n, 8);
296    }
297
298    /// Smoke: BackendStorage::len_bytes goes through dispatch_storage!
299    /// and matches the underlying CpuStorageBytes len_bytes.
300    #[test]
301    fn backend_storage_len_bytes_dispatches() {
302        let bs = BackendStorage::Cpu(CpuStorageBytes::from_zero_bytes(32));
303        assert_eq!(bs.len_bytes(), 32);
304    }
305
306    /// Smoke: zero-element allocations still produce a valid Storage
307    /// with elem_count 0 and dtype intact.
308    #[test]
309    fn zero_element_allocation() {
310        let s = alloc_cpu_zeroed(DType::F64, 0).expect("alloc");
311        assert_eq!(s.dtype(), DType::F64);
312        assert_eq!(s.len_bytes(), 0);
313        assert_eq!(s.elem_count(), 0);
314    }
315
316    /// Smoke: from_slice_cpu preserves dtype + values via Pod cast.
317    #[test]
318    fn from_slice_cpu_round_trip() {
319        let data = vec![1.0_f32, 2.0, 3.0, 4.0];
320        let s = from_slice_cpu(&data);
321        assert_eq!(s.dtype(), DType::F32);
322        assert_eq!(s.elem_count(), 4);
323        assert_eq!(s.len_bytes(), 16);
324    }
325
326    /// Born-red (step 2): a plain Storage has an empty (plain) SType by default.
327    #[test]
328    fn plain_storage_has_empty_stype() {
329        let s = alloc_cpu_zeroed(DType::F32, 4).expect("alloc");
330        assert!(
331            s.stype().is_plain(),
332            "default Storage must carry a plain SType"
333        );
334        assert_eq!(s.stype().layers().len(), 0);
335    }
336
337    /// Born-red (step 2): attaching an SType is preserved and does not disturb
338    /// the logical dtype or the byte count.
339    #[test]
340    fn storage_with_stype_round_trips() {
341        use fuel_ir::{Encoding, GgmlDType, SType};
342        let s = alloc_cpu_zeroed(DType::F32, 8)
343            .expect("alloc")
344            .with_stype(SType::from_layer(Encoding::GgmlBlock {
345                ggml_dtype: GgmlDType::Q4_0,
346            }));
347        assert!(!s.stype().is_plain());
348        assert_eq!(s.dtype(), DType::F32); // dtype unchanged
349        assert_eq!(s.len_bytes(), 32); // bytes unchanged
350    }
351
352    /// A4: alloc symmetry — CpuStorageBytes::alloc and from_zero_bytes
353    /// produce the same shape.
354    #[test]
355    fn cpu_storage_alloc_alias() {
356        let a = fuel_cpu_backend::CpuStorageBytes::alloc(24);
357        let b = fuel_cpu_backend::CpuStorageBytes::from_zero_bytes(24);
358        assert_eq!(a.len_bytes(), b.len_bytes());
359        assert_eq!(a.bytes(), b.bytes());
360    }
361}