Skip to main content

torsh_tensor/
storage.rs

1//! Storage management for tensor data
2//!
3//! This module provides storage abstractions for tensor data, including both
4//! in-memory and memory-mapped storage options with automatic optimization
5//! based on data size.
6//!
7//! # Features
8//!
9//! - **In-memory storage**: Fast access for smaller tensors
10//! - **Memory-mapped storage**: Efficient for large tensors with caching
11//! - **Automatic optimization**: Chooses optimal storage based on size
12//! - **Cross-platform support**: Works on Unix, Windows, and other platforms
13//! - **LRU cache management**: Optimizes memory usage for memory-mapped storage
14
15use std::collections::{HashMap, VecDeque};
16use std::fs::{File, OpenOptions};
17use std::io::Write;
18use std::path::PathBuf;
19#[cfg(feature = "simd")]
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::{Arc, RwLock};
22
23#[cfg(feature = "gpu")]
24use torsh_core::sync::RwLockExt;
25use torsh_core::{
26    dtype::TensorElement,
27    error::{Result, TorshError},
28};
29
30use crate::memory_pool::global_acquire_uninit;
31
32// šŸš€ SciRS2 AlignedVec integration for SIMD-optimized storage
33#[cfg(feature = "simd")]
34use scirs2_core::simd_aligned::AlignedVec;
35
36#[cfg(unix)]
37use std::os::unix::fs::FileExt;
38#[cfg(windows)]
39use std::os::windows::fs::FileExt;
40
41/// Threshold for switching to memory-mapped storage (1 GB)
42const MEMORY_MAPPING_THRESHOLD: usize = 1024 * 1024 * 1024;
43
44/// Threshold for using aligned storage for SIMD optimization (1 KB)
45/// Arrays larger than this benefit from cache-line aligned memory for SIMD operations
46#[cfg(feature = "simd")]
47const ALIGNED_STORAGE_THRESHOLD: usize = 1024;
48
49/// Threshold for using lock-free SIMD storage (10 KB)
50/// Arrays larger than this benefit from lock-free access patterns
51#[cfg(feature = "simd")]
52const SIMD_OPTIMIZED_THRESHOLD: usize = 10240;
53
54// ============================================================================
55// PHASE 5: SIMD-OPTIMIZED LOCK-FREE STORAGE
56// ============================================================================
57// Reads are lock-free (a single atomic flag check) for as long as the storage
58// has never been written to; the first write copies the buffer once into a
59// guarded copy-on-write buffer.
60// Benefits:
61// - No lock acquisition overhead for reads of read-only tensors (~20ns savings)
62// - Direct slice access for SIMD operations
63// - Mutation support at every tensor size, without ever mutating memory a
64//   previously handed-out `&[T]` still points into
65// ============================================================================
66
67/// SIMD-optimized storage with Copy-on-Write semantics (Phase 5)
68///
69/// This storage variant eliminates lock overhead for read operations *while the
70/// tensor has never been written to*, which is the dominant case for SIMD
71/// workloads:
72///
73/// - The buffer handed to [`SimdStorage::new`] is **never mutated in place**, so
74///   [`SimdStorage::try_as_slice`] can hand out a plain `&[T]` with no guard at
75///   all (that is the "lock-free read" this variant exists for).
76/// - The first write copies that buffer once into a private copy-on-write buffer
77///   guarded by an `RwLock`; every later write mutates it in place, so a
78///   `for i in 0..n { t.set(i, v) }` loop is O(n), not O(n²).
79/// - Once the copy exists, readers go through the same `RwLock`, exactly like
80///   [`TensorStorage::Aligned`]. Slices handed out earlier stay valid because the
81///   original buffer is kept alive and untouched for the storage's lifetime.
82///
83/// This is what makes `set`/`set_slice`/`with_slice_mut` work on tensors of every
84/// size instead of failing above the 10 KB `SimdOptimized` threshold.
85#[cfg(feature = "simd")]
86pub struct SimdStorage<T> {
87    /// The buffer published at construction. Immutable for the whole lifetime of
88    /// this storage, which is what makes lock-free slice hand-out sound.
89    original: AlignedVec<T>,
90    /// Copy-on-write buffer holding the authoritative data once a write happened.
91    cow: RwLock<Option<AlignedVec<T>>>,
92    /// Lock-free flag: `true` once `cow` holds the authoritative data.
93    mutated: AtomicBool,
94    /// Whether this storage is shared with another `TensorStorage` handle
95    shared: AtomicBool,
96}
97
98#[cfg(feature = "simd")]
99impl<T> SimdStorage<T> {
100    /// Create new SIMD storage from data
101    pub fn new(data: AlignedVec<T>) -> Self {
102        Self {
103            original: data,
104            cow: RwLock::new(None),
105            mutated: AtomicBool::new(false),
106            shared: AtomicBool::new(false),
107        }
108    }
109
110    /// Get the length of the storage
111    ///
112    /// Mutation never changes the element count, so this stays lock-free.
113    pub fn len(&self) -> usize {
114        self.original.len()
115    }
116
117    /// Check if storage is empty
118    pub fn is_empty(&self) -> bool {
119        self.original.is_empty()
120    }
121
122    /// Get the capacity
123    pub fn capacity(&self) -> usize {
124        self.original.capacity()
125    }
126
127    /// Whether this storage has been written to (and therefore keeps its
128    /// authoritative data in the guarded copy-on-write buffer).
129    pub fn is_mutated(&self) -> bool {
130        self.mutated.load(Ordering::Acquire)
131    }
132
133    /// Get the immutable, lock-free slice.
134    ///
135    /// Returns `None` once the storage has been written to: after that the
136    /// authoritative data lives behind a lock and cannot be exposed as an
137    /// unguarded borrow. Callers should fall back to
138    /// [`SimdStorage::with_slice`].
139    pub fn try_as_slice(&self) -> Option<&[T]> {
140        if self.is_mutated() {
141            None
142        } else {
143            Some(self.original.as_slice())
144        }
145    }
146
147    /// Mark as shared (for Clone)
148    pub fn mark_shared(&self) {
149        self.shared.store(true, Ordering::SeqCst);
150    }
151
152    /// Check if shared
153    pub fn is_shared(&self) -> bool {
154        self.shared.load(Ordering::SeqCst)
155    }
156}
157
158#[cfg(feature = "simd")]
159impl<T: Copy> SimdStorage<T> {
160    /// Read the storage contents, lock-free while it has never been written to.
161    pub fn with_slice<R>(&self, f: impl FnOnce(&[T]) -> R) -> R {
162        if !self.is_mutated() {
163            return f(self.original.as_slice());
164        }
165        // A poisoned lock still holds structurally valid data (the only panic
166        // path while it is held is an allocation failure before publication),
167        // so recover rather than fail every subsequent read.
168        let guard = self.cow.read().unwrap_or_else(|e| e.into_inner());
169        match guard.as_ref() {
170            Some(buffer) => f(buffer.as_slice()),
171            // `mutated` is only set after `cow` is populated, so this is
172            // unreachable; fall back to the original rather than panic.
173            None => f(self.original.as_slice()),
174        }
175    }
176
177    /// Mutate the storage contents, promoting to the copy-on-write buffer on the
178    /// first call.
179    pub fn with_slice_mut<R>(&self, f: impl FnOnce(&mut [T]) -> R) -> Result<R> {
180        let mut guard = self.cow.write().unwrap_or_else(|e| e.into_inner());
181        if guard.is_none() {
182            let source = self.original.as_slice();
183            let mut buffer = AlignedVec::with_capacity(source.len()).map_err(|e| {
184                TorshError::InvalidArgument(format!("Failed to create SIMD COW buffer: {e}"))
185            })?;
186            if !source.is_empty() {
187                // SAFETY: `buffer` has capacity for `source.len()` elements of
188                // `T` and the regions do not overlap; `T: Copy` so a bitwise
189                // copy is a valid initialization.
190                unsafe {
191                    std::ptr::copy_nonoverlapping(
192                        source.as_ptr(),
193                        buffer.as_mut_ptr(),
194                        source.len(),
195                    );
196                    buffer.set_len(source.len());
197                }
198            }
199            *guard = Some(buffer);
200            // Publish only after the buffer is populated: a reader that observes
201            // `mutated == true` is guaranteed to find `cow` initialized.
202            self.mutated.store(true, Ordering::Release);
203        }
204
205        match guard.as_mut() {
206            Some(buffer) => Ok(f(buffer.as_mut_slice())),
207            None => Err(TorshError::SynchronizationError(
208                "SIMD copy-on-write buffer disappeared".to_string(),
209            )),
210        }
211    }
212
213    /// Convert to Vec (copying data)
214    pub fn to_vec(&self) -> Vec<T> {
215        self.with_slice(|slice| slice.to_vec())
216    }
217}
218
219#[cfg(feature = "simd")]
220impl<T> std::fmt::Debug for SimdStorage<T> {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        f.debug_struct("SimdStorage")
223            .field("len", &self.original.len())
224            .field("mutated", &self.mutated.load(Ordering::Relaxed))
225            .field("shared", &self.shared.load(Ordering::Relaxed))
226            .finish()
227    }
228}
229
230// ============================================================================
231// DEVICE-RESIDENT STORAGE
232// ============================================================================
233// A tensor whose data lives in device memory keeps only a pointer here; the
234// host copy is materialised lazily and at most once. That is what turns a chain
235// of GPU ops into "one upload, one download" instead of a host round trip per
236// operation.
237// ============================================================================
238
239/// An owned device allocation.
240///
241/// **A device buffer is immutable for its whole life.** ToRSh writes it exactly
242/// once, as the output of a backend op, and every mutation path on a
243/// device-resident tensor demotes the tensor to host storage first (see
244/// [`crate::Tensor::make_unique`]). Two properties follow, and the rest of the
245/// design rests on them:
246///
247/// - the lazily downloaded host copy in [`TensorStorage::Device`] never needs
248///   invalidating, and
249/// - no device-to-device copy is required — which matters, because
250///   [`oxicuda_backend::ComputeBackend`] does not provide one.
251///
252/// The buffer owns its pointer: `adopt` is the only constructor,
253/// and `Drop` releases the allocation. It also owns a handle on the backend that
254/// allocated it, so freeing consults no registry, takes no ToRSh lock, and stays
255/// correct even after a different backend has been installed.
256#[cfg(feature = "gpu")]
257pub struct DeviceBuffer {
258    /// Device pointer owned by this buffer.
259    ptr: u64,
260    /// Size of the allocation in bytes.
261    bytes: usize,
262    /// Element type the bytes encode.
263    dtype: torsh_core::dtype::DType,
264    /// The backend that allocated `ptr`, and the only one that may free it.
265    backend: Arc<dyn oxicuda_backend::ComputeBackend>,
266}
267
268#[cfg(feature = "gpu")]
269impl DeviceBuffer {
270    /// Take ownership of `ptr`, which `backend` allocated with `bytes` bytes.
271    ///
272    /// The caller must not free `ptr` afterwards and must never hand the same
273    /// pointer to a second `DeviceBuffer`: `Drop` frees it exactly once.
274    pub(crate) fn adopt(
275        ptr: u64,
276        bytes: usize,
277        dtype: torsh_core::dtype::DType,
278        backend: Arc<dyn oxicuda_backend::ComputeBackend>,
279    ) -> Self {
280        Self {
281            ptr,
282            bytes,
283            dtype,
284            backend,
285        }
286    }
287
288    /// The device pointer this buffer owns.
289    pub fn ptr(&self) -> u64 {
290        self.ptr
291    }
292
293    /// Size of the allocation in bytes.
294    pub fn bytes(&self) -> usize {
295        self.bytes
296    }
297
298    /// Element type the buffer's bytes encode.
299    pub fn dtype(&self) -> torsh_core::dtype::DType {
300        self.dtype
301    }
302
303    /// The backend that owns this allocation.
304    pub fn backend(&self) -> &Arc<dyn oxicuda_backend::ComputeBackend> {
305        &self.backend
306    }
307}
308
309#[cfg(feature = "gpu")]
310impl Drop for DeviceBuffer {
311    fn drop(&mut self) {
312        // Deliberately infallible: `drop` may run while unwinding, so a failed
313        // free must never panic, and no ToRSh lock is taken here.
314        let _ = self.backend.free(self.ptr);
315    }
316}
317
318#[cfg(feature = "gpu")]
319impl std::fmt::Debug for DeviceBuffer {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        f.debug_struct("DeviceBuffer")
322            .field("ptr", &format_args!("{:#x}", self.ptr))
323            .field("bytes", &self.bytes)
324            .field("dtype", &self.dtype)
325            .field("backend", &self.backend.name())
326            .finish()
327    }
328}
329
330/// Storage abstraction for tensor data
331pub enum TensorStorage<T: TensorElement> {
332    /// In-memory storage for smaller tensors
333    InMemory(Arc<RwLock<Vec<T>>>),
334    /// Memory-mapped storage for large tensors
335    MemoryMapped(Arc<RwLock<MemoryMappedStorage<T>>>),
336    /// Cache-line aligned storage for SIMD-optimized operations (14.17x speedup)
337    #[cfg(feature = "simd")]
338    Aligned(Arc<RwLock<AlignedVec<T>>>),
339    /// šŸš€ Lock-free SIMD storage with Copy-on-Write semantics (Phase 5)
340    ///
341    /// Benefits:
342    /// - Lock-free read access (~20ns savings per operation)
343    /// - Direct slice access for SIMD
344    /// - Thread-safe through atomic COW
345    #[cfg(feature = "simd")]
346    SimdOptimized(Arc<SimdStorage<T>>),
347    /// Device-resident storage: the data lives in GPU memory.
348    ///
349    /// Produced by the residency path in [`crate::gpu_dispatch`], so a chain of
350    /// device ops never returns to the host between operations.
351    #[cfg(feature = "gpu")]
352    Device {
353        /// The device allocation holding this tensor's data.
354        buffer: Arc<DeviceBuffer>,
355        /// Host copy, downloaded on the first host-side read and kept
356        /// afterwards.
357        ///
358        /// It never needs invalidating: the device buffer is immutable, because
359        /// every mutation path demotes the tensor to host storage first.
360        host_cache: Arc<RwLock<Option<Vec<T>>>>,
361    },
362}
363
364impl<T: TensorElement> std::fmt::Debug for TensorStorage<T> {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        match self {
367            Self::InMemory(data) => f.debug_tuple("InMemory").field(data).finish(),
368            Self::MemoryMapped(storage) => f.debug_tuple("MemoryMapped").field(storage).finish(),
369            #[cfg(feature = "simd")]
370            Self::Aligned(_) => f.debug_tuple("Aligned").field(&"<AlignedVec>").finish(),
371            #[cfg(feature = "simd")]
372            Self::SimdOptimized(storage) => f.debug_tuple("SimdOptimized").field(storage).finish(),
373            #[cfg(feature = "gpu")]
374            Self::Device { buffer, .. } => f.debug_tuple("Device").field(buffer).finish(),
375        }
376    }
377}
378
379/// Memory-mapped storage implementation
380#[derive(Debug)]
381pub struct MemoryMappedStorage<T: TensorElement> {
382    /// File backing the memory mapping
383    file: File,
384    /// Path to the backing file
385    file_path: PathBuf,
386    /// Number of elements stored
387    num_elements: usize,
388    /// Cache for frequently accessed elements
389    cache: HashMap<usize, T>,
390    /// Maximum cache size
391    max_cache_size: usize,
392    /// Access pattern tracking for cache optimization
393    access_pattern: VecDeque<usize>,
394    /// Whether the storage is temporary (should be deleted on drop)
395    is_temporary: bool,
396}
397
398impl<T: TensorElement + Copy> TensorStorage<T> {
399    /// Create in-memory storage
400    pub fn in_memory(data: Vec<T>) -> Self {
401        Self::InMemory(Arc::new(RwLock::new(data)))
402    }
403
404    /// Create memory-mapped storage
405    pub fn memory_mapped(data: Vec<T>, file_path: Option<PathBuf>) -> Result<Self> {
406        let storage = MemoryMappedStorage::new(data, file_path)?;
407        Ok(Self::MemoryMapped(Arc::new(RwLock::new(storage))))
408    }
409
410    /// Create disk-backed storage of `num_elements` copies of `value` without
411    /// ever holding the whole tensor in RAM.
412    ///
413    /// This is the storage behind [`crate::Tensor::disk_backed`]: the backing
414    /// file is filled in bounded chunks, so datasets larger than available
415    /// memory can be created.
416    pub fn memory_mapped_filled(
417        num_elements: usize,
418        value: T,
419        file_path: Option<PathBuf>,
420    ) -> Result<Self> {
421        let storage = MemoryMappedStorage::new_filled(num_elements, value, file_path)?;
422        Ok(Self::MemoryMapped(Arc::new(RwLock::new(storage))))
423    }
424
425    /// Create cache-line aligned storage for SIMD operations (14.17x speedup potential)
426    #[cfg(feature = "simd")]
427    pub fn aligned(data: Vec<T>) -> Result<Self> {
428        Ok(Self::Aligned(Arc::new(RwLock::new(Self::to_aligned_vec(
429            &data,
430        )?))))
431    }
432
433    /// Create cache-line aligned storage directly from a borrowed slice.
434    ///
435    /// Same result as [`TensorStorage::aligned`] without the intermediate `Vec`:
436    /// callers that already hold (or can borrow) the source data pay one
437    /// allocation and one bulk copy instead of two of each.
438    #[cfg(feature = "simd")]
439    pub(crate) fn aligned_from_slice(data: &[T]) -> Result<Self> {
440        Ok(Self::Aligned(Arc::new(RwLock::new(Self::to_aligned_vec(
441            data,
442        )?))))
443    }
444
445    /// Bulk-copy `data` into a freshly allocated [`AlignedVec`].
446    ///
447    /// This is one `copy_nonoverlapping` rather than a per-element `push` with a
448    /// capacity check per iteration, which is what every tensor ≄ 1 KB pays on
449    /// construction.
450    #[cfg(feature = "simd")]
451    fn to_aligned_vec(data: &[T]) -> Result<AlignedVec<T>> {
452        let mut aligned_vec = AlignedVec::with_capacity(data.len()).map_err(|e| {
453            TorshError::InvalidArgument(format!("Failed to create aligned storage: {e}"))
454        })?;
455
456        if !data.is_empty() {
457            // SAFETY: `aligned_vec` was allocated with capacity for `data.len()`
458            // elements of `T`, the two regions cannot overlap (the destination
459            // was just allocated), and `T: Copy` so a bitwise copy is a valid
460            // initialization of the destination elements.
461            unsafe {
462                std::ptr::copy_nonoverlapping(data.as_ptr(), aligned_vec.as_mut_ptr(), data.len());
463                aligned_vec.set_len(data.len());
464            }
465        }
466
467        Ok(aligned_vec)
468    }
469
470    /// šŸš€ **Phase 7**: Create fast result storage (skips alignment copy)
471    ///
472    /// For SIMD operation results where we already have the data in a Vec,
473    /// uses InMemory storage to avoid the ~10µs alignment copy overhead.
474    ///
475    /// # Performance
476    /// - Skips AlignedVec copy (saves ~10µs for 50K elements)
477    /// - Uses InMemory storage (has RwLock but we just created it)
478    /// - Optimal for result tensors that won't be immediately used in SIMD ops
479    pub fn fast_result(data: Vec<T>) -> Self {
480        Self::InMemory(Arc::new(RwLock::new(data)))
481    }
482
483    /// šŸš€ **Phase 5**: Create lock-free SIMD-optimized storage
484    ///
485    /// This storage variant eliminates RwLock overhead for reads:
486    /// - Lock-free read access (~20ns savings per operation)
487    /// - Direct slice access for SIMD operations
488    /// - Thread-safe through Copy-on-Write semantics
489    ///
490    /// # Performance
491    /// - Best for medium-to-large tensors (> 10KB)
492    /// - Optimal for SIMD operations that read but rarely write
493    /// - **Note**: Has ~10µs alignment copy overhead for 50K elements
494    #[cfg(feature = "simd")]
495    pub fn simd_optimized(data: Vec<T>) -> Result<Self> {
496        let aligned_vec = Self::to_aligned_vec(&data)?;
497        let simd_storage = SimdStorage::new(aligned_vec);
498        Ok(Self::SimdOptimized(Arc::new(simd_storage)))
499    }
500
501    /// Create storage automatically based on size and performance characteristics
502    ///
503    /// **Storage Selection Strategy**:
504    /// - Very large (>1GB): Memory-mapped for virtual memory efficiency
505    /// - Large (>10KB, SIMD enabled): SimdOptimized (lock-free reads)
506    /// - Medium (>1KB, SIMD enabled): Aligned storage for SIMD alignment
507    /// - Small (<1KB): In-memory with RwLock
508    pub fn create_optimal(data: Vec<T>) -> Result<Self> {
509        let size_bytes = data.len() * std::mem::size_of::<T>();
510
511        if size_bytes >= MEMORY_MAPPING_THRESHOLD {
512            // Very large data: use memory mapping
513            Self::memory_mapped(data, None)
514        } else {
515            #[cfg(feature = "simd")]
516            {
517                if size_bytes >= SIMD_OPTIMIZED_THRESHOLD {
518                    // Large data: use lock-free SimdOptimized storage
519                    // This eliminates RwLock overhead for read operations
520                    return Self::simd_optimized(data);
521                } else if size_bytes >= ALIGNED_STORAGE_THRESHOLD {
522                    // Medium data: use aligned storage for SIMD alignment
523                    return Self::aligned(data);
524                }
525            }
526            // Small data: use regular in-memory storage
527            Ok(Self::in_memory(data))
528        }
529    }
530
531    /// Wrap an owned device allocation as device-resident storage.
532    ///
533    /// The host cache starts empty and is filled by the first host-side read.
534    #[cfg(feature = "gpu")]
535    pub(crate) fn device(buffer: Arc<DeviceBuffer>) -> Self {
536        Self::Device {
537            buffer,
538            host_cache: Arc::new(RwLock::new(None)),
539        }
540    }
541
542    /// Whether this storage's data lives in device memory.
543    ///
544    /// Always `false` without the `gpu` feature, so callers that must demote
545    /// before a write can test it unconditionally.
546    pub fn is_device(&self) -> bool {
547        #[cfg(feature = "gpu")]
548        {
549            matches!(self, Self::Device { .. })
550        }
551        #[cfg(not(feature = "gpu"))]
552        {
553            false
554        }
555    }
556
557    /// The device allocation backing this storage, if it is device-resident.
558    #[cfg(feature = "gpu")]
559    pub(crate) fn device_buffer(&self) -> Option<&Arc<DeviceBuffer>> {
560        match self {
561            Self::Device { buffer, .. } => Some(buffer),
562            _ => None,
563        }
564    }
565
566    /// Run `f` against the host copy of a device buffer, downloading it first if
567    /// this is the first host-side read.
568    ///
569    /// This is the **single** download point for device-resident storage: every
570    /// later read is served from the cache, which is what keeps a materialised
571    /// view or a per-element `get` loop from re-transferring the whole tensor.
572    #[cfg(feature = "gpu")]
573    fn with_host_cache<R, F>(
574        buffer: &Arc<DeviceBuffer>,
575        host_cache: &RwLock<Option<Vec<T>>>,
576        f: F,
577    ) -> Result<R>
578    where
579        F: FnOnce(&[T]) -> Result<R>,
580        T: Copy,
581    {
582        // Fast path: serve from the cache under a read guard, exactly like the
583        // `InMemory` arm does.
584        {
585            let guard = host_cache.read_or_recover();
586            if let Some(cached) = guard.as_ref() {
587                return f(cached);
588            }
589        }
590
591        // Cold path: download while holding no guard at all, then publish. The
592        // write guard is never held across user code, so a closure that reads
593        // this storage again cannot dead-lock against the download.
594        let downloaded = Self::download(buffer)?;
595        {
596            let mut guard = host_cache.write_or_recover();
597            if guard.is_none() {
598                *guard = Some(downloaded);
599            }
600        }
601
602        let guard = host_cache.read_or_recover();
603        match guard.as_ref() {
604            Some(cached) => f(cached),
605            None => Err(TorshError::SynchronizationError(
606                "device host cache disappeared".to_string(),
607            )),
608        }
609    }
610
611    /// Copy a device buffer's contents into a freshly allocated host `Vec<T>`.
612    #[cfg(feature = "gpu")]
613    fn download(buffer: &Arc<DeviceBuffer>) -> Result<Vec<T>>
614    where
615        T: Copy,
616    {
617        let element_size = std::mem::size_of::<T>();
618        if element_size == 0 || buffer.bytes() % element_size != 0 {
619            return Err(TorshError::InvalidOperation(format!(
620                "device buffer of {} bytes does not hold whole {}-byte elements",
621                buffer.bytes(),
622                element_size
623            )));
624        }
625        // Checked rather than assumed: this is what makes the reinterpret below
626        // sound without relying on a guard in another module.
627        if buffer.dtype() != T::dtype() {
628            return Err(TorshError::InvalidOperation(format!(
629                "device buffer holds {} but the tensor element type is {}",
630                buffer.dtype(),
631                T::dtype()
632            )));
633        }
634
635        let count = buffer.bytes() / element_size;
636        let mut raw = vec![0u8; buffer.bytes()];
637        buffer
638            .backend()
639            .copy_dtoh(&mut raw, buffer.ptr())
640            .map_err(|e| TorshError::InvalidOperation(format!("device download failed: {e}")))?;
641
642        let mut out: Vec<T> = Vec::with_capacity(count);
643        if count > 0 {
644            // SAFETY: `out` was allocated by `Vec<T>` with capacity for `count`
645            // elements, so it is `T`-aligned and spans exactly `buffer.bytes()`
646            // bytes; `raw` was just allocated, so the regions cannot overlap.
647            // The dtype check above establishes that the downloaded bytes are a
648            // valid representation of `T`, and `T: Copy` makes a bitwise copy a
649            // valid initialization.
650            unsafe {
651                std::ptr::copy_nonoverlapping(
652                    raw.as_ptr(),
653                    out.as_mut_ptr().cast::<u8>(),
654                    buffer.bytes(),
655                );
656                out.set_len(count);
657            }
658        }
659        Ok(out)
660    }
661
662    /// Get the number of elements
663    pub fn len(&self) -> usize {
664        match self {
665            Self::InMemory(data) => {
666                data.read().map(|guard| guard.len()).unwrap_or(0) // If lock is poisoned, return 0 (safe fallback)
667            }
668            Self::MemoryMapped(storage) => {
669                storage.read().map(|guard| guard.num_elements).unwrap_or(0) // If lock is poisoned, return 0 (safe fallback)
670            }
671            #[cfg(feature = "simd")]
672            Self::Aligned(data) => {
673                data.read().map(|guard| guard.len()).unwrap_or(0) // If lock is poisoned, return 0 (safe fallback)
674            }
675            #[cfg(feature = "simd")]
676            Self::SimdOptimized(storage) => storage.len(), // Lock-free!
677            #[cfg(feature = "gpu")]
678            Self::Device { buffer, .. } => {
679                // Derived from the allocation size: no download, no lock.
680                let element_size = std::mem::size_of::<T>();
681                if element_size == 0 {
682                    0
683                } else {
684                    buffer.bytes() / element_size
685                }
686            }
687        }
688    }
689
690    /// Check if storage is empty
691    pub fn is_empty(&self) -> bool {
692        self.len() == 0
693    }
694
695    /// Get element at index
696    pub fn get(&self, index: usize) -> Result<T>
697    where
698        T: Copy,
699    {
700        match self {
701            Self::InMemory(data) => {
702                let data_guard = data.read().map_err(|_| {
703                    TorshError::SynchronizationError("Lock poisoned during read".to_string())
704                })?;
705                data_guard
706                    .get(index)
707                    .copied()
708                    .ok_or_else(|| TorshError::IndexOutOfBounds {
709                        index,
710                        size: data_guard.len(),
711                    })
712            }
713            Self::MemoryMapped(storage) => storage
714                .write()
715                .map_err(|_| {
716                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
717                })?
718                .get(index),
719            #[cfg(feature = "simd")]
720            Self::Aligned(data) => {
721                let data_guard = data.read().map_err(|_| {
722                    TorshError::SynchronizationError("Lock poisoned during read".to_string())
723                })?;
724                if index >= data_guard.len() {
725                    Err(TorshError::IndexOutOfBounds {
726                        index,
727                        size: data_guard.len(),
728                    })
729                } else {
730                    Ok(data_guard.as_slice()[index])
731                }
732            }
733            #[cfg(feature = "simd")]
734            Self::SimdOptimized(storage) => {
735                // Lock-free while the storage has never been written to.
736                storage.with_slice(|slice| {
737                    slice
738                        .get(index)
739                        .copied()
740                        .ok_or_else(|| TorshError::IndexOutOfBounds {
741                            index,
742                            size: slice.len(),
743                        })
744                })
745            }
746            #[cfg(feature = "gpu")]
747            Self::Device { buffer, host_cache } => {
748                Self::with_host_cache(buffer, host_cache, |slice| {
749                    slice
750                        .get(index)
751                        .copied()
752                        .ok_or_else(|| TorshError::IndexOutOfBounds {
753                            index,
754                            size: slice.len(),
755                        })
756                })
757            }
758        }
759    }
760
761    /// Set element at index
762    pub fn set(&self, index: usize, value: T) -> Result<()>
763    where
764        T: Copy,
765    {
766        match self {
767            Self::InMemory(data) => {
768                let mut data_guard = data.write().map_err(|_| {
769                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
770                })?;
771                if index >= data_guard.len() {
772                    return Err(TorshError::IndexOutOfBounds {
773                        index,
774                        size: data_guard.len(),
775                    });
776                }
777                data_guard[index] = value;
778                Ok(())
779            }
780            Self::MemoryMapped(storage) => storage
781                .write()
782                .map_err(|_| {
783                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
784                })?
785                .set(index, value),
786            #[cfg(feature = "simd")]
787            Self::Aligned(data) => {
788                let mut data_guard = data.write().map_err(|_| {
789                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
790                })?;
791                if index >= data_guard.len() {
792                    return Err(TorshError::IndexOutOfBounds {
793                        index,
794                        size: data_guard.len(),
795                    });
796                }
797                // Use the new set() method from AlignedVec
798                (*data_guard).set(index, value);
799                Ok(())
800            }
801            #[cfg(feature = "simd")]
802            Self::SimdOptimized(storage) => {
803                // Copy-on-write: the first write promotes the immutable buffer
804                // into a guarded mutable copy, later writes go straight in.
805                storage.with_slice_mut(|slice| {
806                    let size = slice.len();
807                    match slice.get_mut(index) {
808                        Some(slot) => {
809                            *slot = value;
810                            Ok(())
811                        }
812                        None => Err(TorshError::IndexOutOfBounds { index, size }),
813                    }
814                })?
815            }
816            #[cfg(feature = "gpu")]
817            Self::Device { .. } => Err(Self::device_is_immutable()),
818        }
819    }
820
821    /// The error every in-place write on device-resident storage returns.
822    ///
823    /// Device buffers are immutable by construction (see [`DeviceBuffer`]), so
824    /// the tensor must be demoted to host storage before it can be written.
825    #[cfg(feature = "gpu")]
826    fn device_is_immutable() -> TorshError {
827        TorshError::InvalidOperation(
828            "device-resident storage is immutable; call make_unique() or to_device(DeviceType::Cpu) first"
829                .to_string(),
830        )
831    }
832
833    /// Get multiple elements
834    pub fn get_slice(&self, start: usize, len: usize) -> Result<Vec<T>>
835    where
836        T: Copy,
837    {
838        match self {
839            Self::InMemory(data) => {
840                let data_guard = data.read().map_err(|_| {
841                    TorshError::SynchronizationError("Lock poisoned during read".to_string())
842                })?;
843                if start + len > data_guard.len() {
844                    return Err(TorshError::IndexOutOfBounds {
845                        index: start + len - 1,
846                        size: data_guard.len(),
847                    });
848                }
849                Ok(data_guard[start..start + len].to_vec())
850            }
851            Self::MemoryMapped(storage) => storage
852                .write()
853                .map_err(|_| {
854                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
855                })?
856                .get_slice(start, len),
857            #[cfg(feature = "simd")]
858            Self::Aligned(data) => {
859                let data_guard = data.read().map_err(|_| {
860                    TorshError::SynchronizationError("Lock poisoned during read".to_string())
861                })?;
862                if start + len > data_guard.len() {
863                    return Err(TorshError::IndexOutOfBounds {
864                        index: start + len - 1,
865                        size: data_guard.len(),
866                    });
867                }
868                let slice = data_guard.as_slice();
869                Ok(slice[start..start + len].to_vec())
870            }
871            #[cfg(feature = "simd")]
872            Self::SimdOptimized(storage) => storage.with_slice(|slice| {
873                if start + len > slice.len() {
874                    return Err(TorshError::IndexOutOfBounds {
875                        index: start + len - 1,
876                        size: slice.len(),
877                    });
878                }
879                Ok(slice[start..start + len].to_vec())
880            }),
881            #[cfg(feature = "gpu")]
882            Self::Device { buffer, host_cache } => {
883                Self::with_host_cache(buffer, host_cache, |slice| {
884                    if start + len > slice.len() {
885                        return Err(TorshError::IndexOutOfBounds {
886                            index: start + len - 1,
887                            size: slice.len(),
888                        });
889                    }
890                    Ok(slice[start..start + len].to_vec())
891                })
892            }
893        }
894    }
895
896    /// Set multiple elements
897    pub fn set_slice(&self, start: usize, values: &[T]) -> Result<()>
898    where
899        T: Copy,
900    {
901        match self {
902            Self::InMemory(data) => {
903                let mut data_guard = data.write().map_err(|_| {
904                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
905                })?;
906                if start + values.len() > data_guard.len() {
907                    return Err(TorshError::IndexOutOfBounds {
908                        index: start + values.len() - 1,
909                        size: data_guard.len(),
910                    });
911                }
912                data_guard[start..start + values.len()].copy_from_slice(values);
913                Ok(())
914            }
915            Self::MemoryMapped(storage) => storage
916                .write()
917                .map_err(|_| {
918                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
919                })?
920                .set_slice(start, values),
921            #[cfg(feature = "simd")]
922            Self::Aligned(data) => {
923                let mut data_guard = data.write().map_err(|_| {
924                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
925                })?;
926                if start + values.len() > data_guard.len() {
927                    return Err(TorshError::IndexOutOfBounds {
928                        index: start + values.len() - 1,
929                        size: data_guard.len(),
930                    });
931                }
932                // Use as_mut_slice() and copy
933                let slice = data_guard.as_mut_slice();
934                slice[start..start + values.len()].copy_from_slice(values);
935                Ok(())
936            }
937            #[cfg(feature = "simd")]
938            Self::SimdOptimized(storage) => storage.with_slice_mut(|slice| {
939                let size = slice.len();
940                if start + values.len() > size {
941                    return Err(TorshError::IndexOutOfBounds {
942                        index: start + values.len() - 1,
943                        size,
944                    });
945                }
946                slice[start..start + values.len()].copy_from_slice(values);
947                Ok(())
948            })?,
949            #[cfg(feature = "gpu")]
950            Self::Device { .. } => Err(Self::device_is_immutable()),
951        }
952    }
953
954    /// Convert to vector (useful for small tensors or debugging)
955    pub fn to_vec(&self) -> Result<Vec<T>>
956    where
957        T: Copy,
958    {
959        match self {
960            Self::InMemory(data) => Ok(data
961                .read()
962                .map_err(|_| {
963                    TorshError::SynchronizationError("Lock poisoned during read".to_string())
964                })?
965                .clone()),
966            Self::MemoryMapped(storage) => storage
967                .write()
968                .map_err(|_| {
969                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
970                })?
971                .to_vec(),
972            #[cfg(feature = "simd")]
973            Self::Aligned(data) => {
974                let data_guard = data.read().map_err(|_| {
975                    TorshError::SynchronizationError("Lock poisoned during read".to_string())
976                })?;
977                Ok(data_guard.as_slice().to_vec())
978            }
979            #[cfg(feature = "simd")]
980            Self::SimdOptimized(storage) => Ok(storage.to_vec()),
981            #[cfg(feature = "gpu")]
982            Self::Device { buffer, host_cache } => {
983                Self::with_host_cache(buffer, host_cache, |slice| Ok(slice.to_vec()))
984            }
985        }
986    }
987
988    /// Get storage type information
989    pub fn storage_type(&self) -> &'static str {
990        match self {
991            Self::InMemory(_) => "in_memory",
992            Self::MemoryMapped(_) => "memory_mapped",
993            #[cfg(feature = "simd")]
994            Self::Aligned(_) => "aligned_simd",
995            #[cfg(feature = "simd")]
996            Self::SimdOptimized(_) => "simd_optimized",
997            #[cfg(feature = "gpu")]
998            Self::Device { .. } => "device",
999        }
1000    }
1001
1002    /// Get estimated memory usage in bytes
1003    pub fn memory_usage(&self) -> usize {
1004        match self {
1005            Self::InMemory(data) => {
1006                data.read()
1007                    .map(|guard| guard.len() * std::mem::size_of::<T>())
1008                    .unwrap_or(0) // If lock is poisoned, return 0 (safe fallback)
1009            }
1010            Self::MemoryMapped(storage) => {
1011                storage
1012                    .read()
1013                    .map(|storage_guard| {
1014                        // Memory usage is just the cache size plus metadata
1015                        storage_guard.cache.len() * std::mem::size_of::<T>()
1016                            + std::mem::size_of::<MemoryMappedStorage<T>>()
1017                    })
1018                    .unwrap_or(std::mem::size_of::<MemoryMappedStorage<T>>()) // Fallback to metadata size
1019            }
1020            #[cfg(feature = "simd")]
1021            Self::Aligned(data) => {
1022                data.read()
1023                    .map(|data_guard| {
1024                        // AlignedVec uses more memory due to alignment padding
1025                        data_guard.capacity() * std::mem::size_of::<T>()
1026                    })
1027                    .unwrap_or(0) // If lock is poisoned, return 0 (safe fallback)
1028            }
1029            #[cfg(feature = "simd")]
1030            Self::SimdOptimized(storage) => {
1031                // After the first write the copy-on-write buffer is held
1032                // alongside the (retained) original one.
1033                let buffers = if storage.is_mutated() { 2 } else { 1 };
1034                storage.capacity() * std::mem::size_of::<T>() * buffers
1035            }
1036            #[cfg(feature = "gpu")]
1037            Self::Device { buffer, host_cache } => {
1038                // The device allocation, plus the host copy once one exists.
1039                let cached = host_cache
1040                    .read_or_recover()
1041                    .as_ref()
1042                    .map_or(0, |cache| cache.len() * std::mem::size_of::<T>());
1043                buffer.bytes() + cached
1044            }
1045        }
1046    }
1047
1048    /// Execute a function with immutable access to data slice (zero-copy within scope)
1049    ///
1050    /// This enables zero-copy SIMD operations by providing direct `&[T]` access
1051    /// within the closure scope while the lock is held.
1052    ///
1053    /// # Arguments
1054    /// * `f` - Closure that receives `&[T]` and returns `Result<R>`
1055    ///
1056    /// # Returns
1057    /// Result from the closure
1058    ///
1059    /// # Performance
1060    /// - Zero allocations for in-memory and aligned storage
1061    /// - Converts memory-mapped storage to Vec (one allocation)
1062    ///
1063    /// # Examples
1064    /// ```ignore
1065    /// storage.with_slice(|data| {
1066    ///     // Direct SIMD access to data
1067    ///     f32::simd_add(&data, &other_data)
1068    /// })?;
1069    /// ```
1070    pub fn with_slice<R, F>(&self, f: F) -> Result<R>
1071    where
1072        F: FnOnce(&[T]) -> Result<R>,
1073        T: Copy,
1074    {
1075        match self {
1076            Self::InMemory(data) => {
1077                let data_guard = data.read().map_err(|_| {
1078                    TorshError::SynchronizationError("Lock poisoned during read".to_string())
1079                })?;
1080                f(data_guard.as_slice())
1081            }
1082            Self::MemoryMapped(storage) => {
1083                // Memory-mapped storage requires conversion to Vec
1084                let vec = storage
1085                    .write()
1086                    .map_err(|_| {
1087                        TorshError::SynchronizationError("Lock poisoned during write".to_string())
1088                    })?
1089                    .to_vec()?;
1090                f(&vec)
1091            }
1092            #[cfg(feature = "simd")]
1093            Self::Aligned(data) => {
1094                let data_guard = data.read().map_err(|_| {
1095                    TorshError::SynchronizationError("Lock poisoned during read".to_string())
1096                })?;
1097                f(data_guard.as_slice())
1098            }
1099            #[cfg(feature = "simd")]
1100            Self::SimdOptimized(storage) => {
1101                // šŸš€ Lock-free access while the storage has never been written to.
1102                storage.with_slice(f)
1103            }
1104            #[cfg(feature = "gpu")]
1105            Self::Device { buffer, host_cache } => Self::with_host_cache(buffer, host_cache, f),
1106        }
1107    }
1108
1109    /// Try to get direct slice access without closures (only works for SimdOptimized)
1110    ///
1111    /// Returns `Some(&[T])` if storage is SimdOptimized **and** has never been
1112    /// written to (in which case its buffer is immutable and an unguarded borrow
1113    /// is sound). Returns `None` for every other storage type, and for a
1114    /// SimdOptimized storage that has been mutated — callers must fall back to
1115    /// [`TensorStorage::with_slice`].
1116    ///
1117    /// # Performance
1118    /// - SimdOptimized (unmutated): Direct slice access, zero overhead
1119    /// - Others: Returns None (use with_slice instead)
1120    #[cfg(feature = "simd")]
1121    pub fn try_as_slice_direct(&self) -> Option<&[T]> {
1122        match self {
1123            Self::SimdOptimized(storage) => storage.try_as_slice(),
1124            _ => None,
1125        }
1126    }
1127
1128    /// Execute a function with mutable access to data slice (zero-copy within scope)
1129    ///
1130    /// This enables zero-copy in-place operations by providing direct `&mut [T]` access
1131    /// within the closure scope while the lock is held.
1132    ///
1133    /// # Arguments
1134    /// * `f` - Closure that receives `&mut [T]` and returns `Result<R>`
1135    ///
1136    /// # Returns
1137    /// Result from the closure
1138    ///
1139    /// # Performance
1140    /// - Zero allocations for in-memory storage
1141    /// - Aligned storage not yet supported (returns error)
1142    /// - Memory-mapped storage not supported for mutable access (returns error)
1143    ///
1144    /// # Examples
1145    /// ```ignore
1146    /// storage.with_slice_mut(|data| {
1147    ///     // In-place SIMD operation
1148    ///     f32::simd_add_inplace(data, &other_data)
1149    /// })?;
1150    /// ```
1151    pub fn with_slice_mut<R, F>(&self, f: F) -> Result<R>
1152    where
1153        F: FnOnce(&mut [T]) -> Result<R>,
1154        T: Copy,
1155    {
1156        match self {
1157            Self::InMemory(data) => {
1158                let mut data_guard = data.write().map_err(|_| {
1159                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
1160                })?;
1161                f(data_guard.as_mut_slice())
1162            }
1163            Self::MemoryMapped(_) => {
1164                // Memory-mapped storage doesn't support mutable slice access
1165                Err(TorshError::InvalidArgument(
1166                    "Memory-mapped storage does not support mutable slice access".to_string(),
1167                ))
1168            }
1169            #[cfg(feature = "simd")]
1170            Self::Aligned(data) => {
1171                let mut data_guard = data.write().map_err(|_| {
1172                    TorshError::SynchronizationError("Lock poisoned during write".to_string())
1173                })?;
1174                f(data_guard.as_mut_slice())
1175            }
1176            #[cfg(feature = "simd")]
1177            Self::SimdOptimized(storage) => {
1178                // Copy-on-write promotion, then a direct mutable slice.
1179                storage.with_slice_mut(f)?
1180            }
1181            #[cfg(feature = "gpu")]
1182            Self::Device { .. } => Err(Self::device_is_immutable()),
1183        }
1184    }
1185}
1186
1187/// Build a backing-file path that is unique per storage instance.
1188///
1189/// A single per-process name would make two temporary memory-mapped tensors
1190/// share (and truncate) one file, so the name carries the pid, a monotonic
1191/// counter and a nanosecond timestamp.
1192fn unique_backing_path() -> PathBuf {
1193    static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1194    let seq = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1195    let nanos = std::time::SystemTime::now()
1196        .duration_since(std::time::UNIX_EPOCH)
1197        .unwrap_or_default()
1198        .as_nanos();
1199    std::env::temp_dir().join(format!(
1200        "torsh_tensor_{pid}_{nanos}_{seq}.mmap",
1201        pid = std::process::id()
1202    ))
1203}
1204
1205impl<T: TensorElement> MemoryMappedStorage<T> {
1206    /// Open (or create) the backing file for a storage instance.
1207    fn open_backing_file(file_path: Option<PathBuf>) -> Result<(File, PathBuf, bool)> {
1208        let (file_path, is_temporary) = match file_path {
1209            Some(path) => (path, false),
1210            // Unique per storage instance: sharing one name per process made
1211            // every new temporary tensor truncate the previous one's data.
1212            None => (unique_backing_path(), true),
1213        };
1214
1215        let file = OpenOptions::new()
1216            .create(true)
1217            .read(true)
1218            .write(true)
1219            .truncate(true)
1220            .open(&file_path)
1221            .map_err(|e| {
1222                TorshError::IoError(format!("Failed to create memory-mapped file: {e}"))
1223            })?;
1224
1225        Ok((file, file_path, is_temporary))
1226    }
1227
1228    /// Create new memory-mapped storage
1229    pub fn new(data: Vec<T>, file_path: Option<PathBuf>) -> Result<Self> {
1230        let (mut file, file_path, is_temporary) = Self::open_backing_file(file_path)?;
1231
1232        // Write data to file
1233        let data_bytes = unsafe {
1234            std::slice::from_raw_parts(
1235                data.as_ptr() as *const u8,
1236                std::mem::size_of_val(data.as_slice()),
1237            )
1238        };
1239        file.write_all(data_bytes).map_err(|e| {
1240            TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
1241        })?;
1242        file.flush()
1243            .map_err(|e| TorshError::IoError(format!("Failed to flush memory-mapped file: {e}")))?;
1244
1245        Ok(Self {
1246            file,
1247            file_path,
1248            num_elements: data.len(),
1249            cache: HashMap::new(),
1250            max_cache_size: 10000, // Cache up to 10k elements
1251            access_pattern: VecDeque::new(),
1252            is_temporary,
1253        })
1254    }
1255
1256    /// Create memory-mapped storage of `num_elements` copies of `value` **without
1257    /// materialising the tensor in RAM**.
1258    ///
1259    /// The backing file is written in bounded chunks, so a tensor larger than
1260    /// available memory can be created: peak resident memory is the chunk size,
1261    /// not the tensor size.
1262    pub fn new_filled(num_elements: usize, value: T, file_path: Option<PathBuf>) -> Result<Self>
1263    where
1264        T: Copy,
1265    {
1266        let (mut file, file_path, is_temporary) = Self::open_backing_file(file_path)?;
1267
1268        let element_size = std::mem::size_of::<T>();
1269        if element_size > 0 && num_elements > 0 {
1270            // Write in ~1 MiB chunks so peak RAM stays bounded.
1271            const TARGET_CHUNK_BYTES: usize = 1024 * 1024;
1272            let chunk_elements = (TARGET_CHUNK_BYTES / element_size).clamp(1, num_elements);
1273            let chunk = vec![value; chunk_elements];
1274            let chunk_bytes = unsafe {
1275                std::slice::from_raw_parts(
1276                    chunk.as_ptr() as *const u8,
1277                    std::mem::size_of_val(chunk.as_slice()),
1278                )
1279            };
1280
1281            let mut written = 0usize;
1282            while written < num_elements {
1283                let remaining = num_elements - written;
1284                let this_chunk = remaining.min(chunk_elements);
1285                file.write_all(&chunk_bytes[..this_chunk * element_size])
1286                    .map_err(|e| {
1287                        TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
1288                    })?;
1289                written += this_chunk;
1290            }
1291        }
1292
1293        file.flush()
1294            .map_err(|e| TorshError::IoError(format!("Failed to flush memory-mapped file: {e}")))?;
1295
1296        Ok(Self {
1297            file,
1298            file_path,
1299            num_elements,
1300            cache: HashMap::new(),
1301            max_cache_size: 10000,
1302            access_pattern: VecDeque::new(),
1303            is_temporary,
1304        })
1305    }
1306
1307    /// Path of the file backing this storage.
1308    pub fn file_path(&self) -> &std::path::Path {
1309        &self.file_path
1310    }
1311
1312    /// Get element at index with caching
1313    pub fn get(&mut self, index: usize) -> Result<T>
1314    where
1315        T: Copy,
1316    {
1317        if index >= self.num_elements {
1318            return Err(TorshError::IndexOutOfBounds {
1319                index,
1320                size: self.num_elements,
1321            });
1322        }
1323
1324        // Check cache first
1325        if let Some(&value) = self.cache.get(&index) {
1326            self.update_access_pattern(index);
1327            return Ok(value);
1328        }
1329
1330        // Read from file
1331        let value = self.read_element_from_file(index)?;
1332
1333        // Add to cache if there's space
1334        if self.cache.len() < self.max_cache_size {
1335            self.cache.insert(index, value);
1336        } else {
1337            // Evict least recently used element
1338            self.evict_lru();
1339            self.cache.insert(index, value);
1340        }
1341
1342        self.update_access_pattern(index);
1343        Ok(value)
1344    }
1345
1346    /// Set element at index
1347    pub fn set(&mut self, index: usize, value: T) -> Result<()>
1348    where
1349        T: Copy,
1350    {
1351        if index >= self.num_elements {
1352            return Err(TorshError::IndexOutOfBounds {
1353                index,
1354                size: self.num_elements,
1355            });
1356        }
1357
1358        // Update cache
1359        self.cache.insert(index, value);
1360
1361        // Write to file
1362        self.write_element_to_file(index, value)?;
1363        self.update_access_pattern(index);
1364        Ok(())
1365    }
1366
1367    /// Get slice of elements
1368    ///
1369    /// The whole range is fetched with a **single** positional read into the
1370    /// destination buffer, instead of one syscall (and one heap allocation) per
1371    /// element via the LRU cache. The cache is bypassed on purpose: every write
1372    /// goes through to the file, so the file is authoritative.
1373    pub fn get_slice(&mut self, start: usize, len: usize) -> Result<Vec<T>>
1374    where
1375        T: Copy,
1376    {
1377        if start + len > self.num_elements {
1378            return Err(TorshError::IndexOutOfBounds {
1379                index: start + len - 1,
1380                size: self.num_elements,
1381            });
1382        }
1383
1384        if len == 0 {
1385            return Ok(Vec::new());
1386        }
1387
1388        let element_size = std::mem::size_of::<T>();
1389        let mut buf = global_acquire_uninit::<T>(len);
1390
1391        if element_size == 0 {
1392            // Zero-sized elements carry no bytes: nothing to read.
1393            let uninit = buf.as_uninit_slice_mut();
1394            for slot in uninit.iter_mut().take(len) {
1395                // SAFETY: a zero-sized type has exactly one value; reading the
1396                // (empty) representation back is a no-op.
1397                slot.write(unsafe { std::mem::zeroed() });
1398            }
1399            return Ok(buf.into_vec(len));
1400        }
1401
1402        {
1403            let byte_len = len * element_size;
1404            let ptr = buf.as_uninit_slice_mut().as_mut_ptr() as *mut u8;
1405            // SAFETY: the buffer is allocated for `len` elements of `T`, so it
1406            // spans `byte_len` bytes and is correctly aligned for `T`. The bytes
1407            // are zeroed before a `&mut [u8]` is formed so no uninitialized
1408            // memory is ever exposed as an initialized reference; the read then
1409            // overwrites exactly that range, which is what `into_vec(len)`
1410            // claims as initialized.
1411            let byte_buf = unsafe {
1412                std::ptr::write_bytes(ptr, 0, byte_len);
1413                std::slice::from_raw_parts_mut(ptr, byte_len)
1414            };
1415            self.read_bytes_at(byte_buf, (start * element_size) as u64)?;
1416        }
1417
1418        Ok(buf.into_vec(len))
1419    }
1420
1421    /// Read exactly `buffer.len()` bytes at `offset` from the backing file.
1422    fn read_bytes_at(&mut self, buffer: &mut [u8], offset: u64) -> Result<()> {
1423        #[cfg(unix)]
1424        {
1425            self.file.read_exact_at(buffer, offset).map_err(|e| {
1426                TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
1427            })?;
1428        }
1429
1430        #[cfg(windows)]
1431        {
1432            let mut read_total = 0usize;
1433            while read_total < buffer.len() {
1434                let n = self
1435                    .file
1436                    .seek_read(&mut buffer[read_total..], offset + read_total as u64)
1437                    .map_err(|e| {
1438                        TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
1439                    })?;
1440                if n == 0 {
1441                    return Err(TorshError::IoError(
1442                        "Unexpected end of memory-mapped file".to_string(),
1443                    ));
1444                }
1445                read_total += n;
1446            }
1447        }
1448
1449        #[cfg(not(any(unix, windows)))]
1450        {
1451            use std::io::{Read, Seek, SeekFrom};
1452            self.file.seek(SeekFrom::Start(offset)).map_err(|e| {
1453                TorshError::IoError(format!("Failed to seek in memory-mapped file: {e}"))
1454            })?;
1455            self.file.read_exact(buffer).map_err(|e| {
1456                TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
1457            })?;
1458        }
1459
1460        Ok(())
1461    }
1462
1463    /// Set slice of elements
1464    pub fn set_slice(&mut self, start: usize, values: &[T]) -> Result<()>
1465    where
1466        T: Copy,
1467    {
1468        if start + values.len() > self.num_elements {
1469            return Err(TorshError::IndexOutOfBounds {
1470                index: start + values.len() - 1,
1471                size: self.num_elements,
1472            });
1473        }
1474
1475        for (i, &value) in values.iter().enumerate() {
1476            self.set(start + i, value)?;
1477        }
1478        Ok(())
1479    }
1480
1481    /// Convert entire storage to vector
1482    pub fn to_vec(&mut self) -> Result<Vec<T>>
1483    where
1484        T: Copy,
1485    {
1486        self.get_slice(0, self.num_elements)
1487    }
1488
1489    /// Read element from file
1490    fn read_element_from_file(&mut self, index: usize) -> Result<T>
1491    where
1492        T: Copy,
1493    {
1494        let offset = index * std::mem::size_of::<T>();
1495        let mut buffer = vec![0u8; std::mem::size_of::<T>()];
1496
1497        #[cfg(unix)]
1498        {
1499            self.file
1500                .read_exact_at(&mut buffer, offset as u64)
1501                .map_err(|e| {
1502                    TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
1503                })?;
1504        }
1505
1506        #[cfg(windows)]
1507        {
1508            self.file
1509                .seek_read(&mut buffer, offset as u64)
1510                .map_err(|e| {
1511                    TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
1512                })?;
1513        }
1514
1515        #[cfg(not(any(unix, windows)))]
1516        {
1517            self.file
1518                .seek(SeekFrom::Start(offset as u64))
1519                .map_err(|e| {
1520                    TorshError::IoError(format!("Failed to seek in memory-mapped file: {e}"))
1521                })?;
1522            self.file.read_exact(&mut buffer).map_err(|e| {
1523                TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
1524            })?;
1525        }
1526
1527        // Convert bytes to T. The byte buffer is a `Vec<u8>` (alignment 1), so
1528        // the read must be an unaligned one.
1529        let value = unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const T) };
1530        Ok(value)
1531    }
1532
1533    /// Write element to file
1534    fn write_element_to_file(&mut self, index: usize, value: T) -> Result<()>
1535    where
1536        T: Copy,
1537    {
1538        let offset = index * std::mem::size_of::<T>();
1539        let buffer = unsafe {
1540            std::slice::from_raw_parts(&value as *const T as *const u8, std::mem::size_of::<T>())
1541        };
1542
1543        #[cfg(unix)]
1544        {
1545            self.file.write_all_at(buffer, offset as u64).map_err(|e| {
1546                TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
1547            })?;
1548        }
1549
1550        #[cfg(windows)]
1551        {
1552            self.file.seek_write(buffer, offset as u64).map_err(|e| {
1553                TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
1554            })?;
1555        }
1556
1557        #[cfg(not(any(unix, windows)))]
1558        {
1559            self.file
1560                .seek(SeekFrom::Start(offset as u64))
1561                .map_err(|e| {
1562                    TorshError::IoError(format!("Failed to seek in memory-mapped file: {e}"))
1563                })?;
1564            self.file.write_all(buffer).map_err(|e| {
1565                TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
1566            })?;
1567        }
1568
1569        Ok(())
1570    }
1571
1572    /// Update access pattern for cache management
1573    fn update_access_pattern(&mut self, index: usize) {
1574        self.access_pattern.push_back(index);
1575        if self.access_pattern.len() > self.max_cache_size {
1576            self.access_pattern.pop_front();
1577        }
1578    }
1579
1580    /// Evict least recently used element from cache
1581    fn evict_lru(&mut self) {
1582        if let Some(lru_index) = self.access_pattern.front().copied() {
1583            self.cache.remove(&lru_index);
1584        }
1585    }
1586}
1587
1588impl<T: TensorElement> Drop for MemoryMappedStorage<T> {
1589    fn drop(&mut self) {
1590        if self.is_temporary {
1591            // Clean up temporary file
1592            let _ = std::fs::remove_file(&self.file_path);
1593        }
1594    }
1595}
1596
1597impl<T: TensorElement> Clone for TensorStorage<T> {
1598    fn clone(&self) -> Self {
1599        match self {
1600            Self::InMemory(data) => Self::InMemory(Arc::clone(data)),
1601            Self::MemoryMapped(storage) => Self::MemoryMapped(Arc::clone(storage)),
1602            #[cfg(feature = "simd")]
1603            Self::Aligned(data) => Self::Aligned(Arc::clone(data)),
1604            #[cfg(feature = "simd")]
1605            Self::SimdOptimized(storage) => {
1606                // Mark the storage as shared for COW semantics
1607                storage.mark_shared();
1608                Self::SimdOptimized(Arc::clone(storage))
1609            }
1610            #[cfg(feature = "gpu")]
1611            Self::Device { buffer, host_cache } => Self::Device {
1612                // Both handles are shared: the device allocation is immutable,
1613                // and sharing the cache means a clone never re-downloads.
1614                buffer: Arc::clone(buffer),
1615                host_cache: Arc::clone(host_cache),
1616            },
1617        }
1618    }
1619}
1620
1621#[cfg(test)]
1622mod tests {
1623    use super::*;
1624
1625    #[test]
1626    fn test_in_memory_storage() {
1627        let data = vec![1.0f32, 2.0, 3.0, 4.0];
1628        let storage = TensorStorage::in_memory(data.clone());
1629
1630        assert_eq!(storage.len(), 4);
1631        assert!(!storage.is_empty());
1632        assert_eq!(storage.storage_type(), "in_memory");
1633
1634        assert_eq!(storage.get(0).expect("get(0) failed"), 1.0);
1635        assert_eq!(storage.get(3).expect("get(3) failed"), 4.0);
1636
1637        let slice = storage.get_slice(1, 2).expect("get_slice failed");
1638        assert_eq!(slice, vec![2.0, 3.0]);
1639    }
1640
1641    #[test]
1642    fn test_optimal_storage_selection() {
1643        // Small data should use in-memory storage (200 f32 = 800 bytes < 1024 threshold)
1644        let small_data = vec![1.0f32; 200];
1645        let small_storage =
1646            TensorStorage::create_optimal(small_data).expect("create_optimal failed");
1647
1648        #[cfg(feature = "simd")]
1649        {
1650            // With SIMD enabled, small data below threshold should use in-memory
1651            assert_eq!(small_storage.storage_type(), "in_memory");
1652        }
1653        #[cfg(not(feature = "simd"))]
1654        {
1655            // Without SIMD, all data uses in-memory storage
1656            assert_eq!(small_storage.storage_type(), "in_memory");
1657        }
1658    }
1659
1660    #[test]
1661    fn test_memory_usage_calculation() {
1662        let data = vec![1.0f32; 1000];
1663        let storage = TensorStorage::in_memory(data);
1664        let expected_size = 1000 * std::mem::size_of::<f32>();
1665        assert_eq!(storage.memory_usage(), expected_size);
1666    }
1667
1668    #[test]
1669    #[cfg(feature = "simd")]
1670    fn test_aligned_storage() {
1671        let data = vec![1.0f32, 2.0, 3.0, 4.0];
1672        let storage =
1673            TensorStorage::aligned(data.clone()).expect("aligned storage creation failed");
1674
1675        assert_eq!(storage.len(), 4);
1676        assert!(!storage.is_empty());
1677        assert_eq!(storage.storage_type(), "aligned_simd");
1678
1679        // Test basic element access
1680        assert_eq!(storage.get(0).expect("get(0) failed"), 1.0);
1681        assert_eq!(storage.get(3).expect("get(3) failed"), 4.0);
1682
1683        // Test slice access
1684        let slice = storage.get_slice(1, 2).expect("get_slice failed");
1685        assert_eq!(slice, vec![2.0, 3.0]);
1686
1687        // Test conversion to vec
1688        let vec = storage.to_vec().expect("to_vec failed");
1689        assert_eq!(vec, data);
1690    }
1691
1692    #[test]
1693    #[cfg(feature = "simd")]
1694    fn test_optimal_storage_selection_with_aligned() {
1695        // Medium-size data should use aligned storage when SIMD is enabled
1696        let medium_data = vec![1.0f32; 2000]; // Above ALIGNED_STORAGE_THRESHOLD
1697        let medium_storage = TensorStorage::create_optimal(medium_data)
1698            .expect("create_optimal for medium data failed");
1699        assert_eq!(medium_storage.storage_type(), "aligned_simd");
1700
1701        // Small data should still use in-memory storage
1702        let small_data = vec![1.0f32; 100]; // Below ALIGNED_STORAGE_THRESHOLD
1703        let small_storage = TensorStorage::create_optimal(small_data)
1704            .expect("create_optimal for small data failed");
1705        assert_eq!(small_storage.storage_type(), "in_memory");
1706    }
1707}