use std::collections::{HashMap, VecDeque};
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
#[cfg(feature = "simd")]
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
#[cfg(feature = "gpu")]
use torsh_core::sync::RwLockExt;
use torsh_core::{
dtype::TensorElement,
error::{Result, TorshError},
};
use crate::memory_pool::global_acquire_uninit;
#[cfg(feature = "simd")]
use scirs2_core::simd_aligned::AlignedVec;
#[cfg(unix)]
use std::os::unix::fs::FileExt;
#[cfg(windows)]
use std::os::windows::fs::FileExt;
const MEMORY_MAPPING_THRESHOLD: usize = 1024 * 1024 * 1024;
#[cfg(feature = "simd")]
const ALIGNED_STORAGE_THRESHOLD: usize = 1024;
#[cfg(feature = "simd")]
const SIMD_OPTIMIZED_THRESHOLD: usize = 10240;
#[cfg(feature = "simd")]
pub struct SimdStorage<T> {
original: AlignedVec<T>,
cow: RwLock<Option<AlignedVec<T>>>,
mutated: AtomicBool,
shared: AtomicBool,
}
#[cfg(feature = "simd")]
impl<T> SimdStorage<T> {
pub fn new(data: AlignedVec<T>) -> Self {
Self {
original: data,
cow: RwLock::new(None),
mutated: AtomicBool::new(false),
shared: AtomicBool::new(false),
}
}
pub fn len(&self) -> usize {
self.original.len()
}
pub fn is_empty(&self) -> bool {
self.original.is_empty()
}
pub fn capacity(&self) -> usize {
self.original.capacity()
}
pub fn is_mutated(&self) -> bool {
self.mutated.load(Ordering::Acquire)
}
pub fn try_as_slice(&self) -> Option<&[T]> {
if self.is_mutated() {
None
} else {
Some(self.original.as_slice())
}
}
pub fn mark_shared(&self) {
self.shared.store(true, Ordering::SeqCst);
}
pub fn is_shared(&self) -> bool {
self.shared.load(Ordering::SeqCst)
}
}
#[cfg(feature = "simd")]
impl<T: Copy> SimdStorage<T> {
pub fn with_slice<R>(&self, f: impl FnOnce(&[T]) -> R) -> R {
if !self.is_mutated() {
return f(self.original.as_slice());
}
let guard = self.cow.read().unwrap_or_else(|e| e.into_inner());
match guard.as_ref() {
Some(buffer) => f(buffer.as_slice()),
None => f(self.original.as_slice()),
}
}
pub fn with_slice_mut<R>(&self, f: impl FnOnce(&mut [T]) -> R) -> Result<R> {
let mut guard = self.cow.write().unwrap_or_else(|e| e.into_inner());
if guard.is_none() {
let source = self.original.as_slice();
let mut buffer = AlignedVec::with_capacity(source.len()).map_err(|e| {
TorshError::InvalidArgument(format!("Failed to create SIMD COW buffer: {e}"))
})?;
if !source.is_empty() {
unsafe {
std::ptr::copy_nonoverlapping(
source.as_ptr(),
buffer.as_mut_ptr(),
source.len(),
);
buffer.set_len(source.len());
}
}
*guard = Some(buffer);
self.mutated.store(true, Ordering::Release);
}
match guard.as_mut() {
Some(buffer) => Ok(f(buffer.as_mut_slice())),
None => Err(TorshError::SynchronizationError(
"SIMD copy-on-write buffer disappeared".to_string(),
)),
}
}
pub fn to_vec(&self) -> Vec<T> {
self.with_slice(|slice| slice.to_vec())
}
}
#[cfg(feature = "simd")]
impl<T> std::fmt::Debug for SimdStorage<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SimdStorage")
.field("len", &self.original.len())
.field("mutated", &self.mutated.load(Ordering::Relaxed))
.field("shared", &self.shared.load(Ordering::Relaxed))
.finish()
}
}
#[cfg(feature = "gpu")]
pub struct DeviceBuffer {
ptr: u64,
bytes: usize,
dtype: torsh_core::dtype::DType,
backend: Arc<dyn oxicuda_backend::ComputeBackend>,
}
#[cfg(feature = "gpu")]
impl DeviceBuffer {
pub(crate) fn adopt(
ptr: u64,
bytes: usize,
dtype: torsh_core::dtype::DType,
backend: Arc<dyn oxicuda_backend::ComputeBackend>,
) -> Self {
Self {
ptr,
bytes,
dtype,
backend,
}
}
pub fn ptr(&self) -> u64 {
self.ptr
}
pub fn bytes(&self) -> usize {
self.bytes
}
pub fn dtype(&self) -> torsh_core::dtype::DType {
self.dtype
}
pub fn backend(&self) -> &Arc<dyn oxicuda_backend::ComputeBackend> {
&self.backend
}
}
#[cfg(feature = "gpu")]
impl Drop for DeviceBuffer {
fn drop(&mut self) {
let _ = self.backend.free(self.ptr);
}
}
#[cfg(feature = "gpu")]
impl std::fmt::Debug for DeviceBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeviceBuffer")
.field("ptr", &format_args!("{:#x}", self.ptr))
.field("bytes", &self.bytes)
.field("dtype", &self.dtype)
.field("backend", &self.backend.name())
.finish()
}
}
pub enum TensorStorage<T: TensorElement> {
InMemory(Arc<RwLock<Vec<T>>>),
MemoryMapped(Arc<RwLock<MemoryMappedStorage<T>>>),
#[cfg(feature = "simd")]
Aligned(Arc<RwLock<AlignedVec<T>>>),
#[cfg(feature = "simd")]
SimdOptimized(Arc<SimdStorage<T>>),
#[cfg(feature = "gpu")]
Device {
buffer: Arc<DeviceBuffer>,
host_cache: Arc<RwLock<Option<Vec<T>>>>,
},
}
impl<T: TensorElement> std::fmt::Debug for TensorStorage<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InMemory(data) => f.debug_tuple("InMemory").field(data).finish(),
Self::MemoryMapped(storage) => f.debug_tuple("MemoryMapped").field(storage).finish(),
#[cfg(feature = "simd")]
Self::Aligned(_) => f.debug_tuple("Aligned").field(&"<AlignedVec>").finish(),
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => f.debug_tuple("SimdOptimized").field(storage).finish(),
#[cfg(feature = "gpu")]
Self::Device { buffer, .. } => f.debug_tuple("Device").field(buffer).finish(),
}
}
}
#[derive(Debug)]
pub struct MemoryMappedStorage<T: TensorElement> {
file: File,
file_path: PathBuf,
num_elements: usize,
cache: HashMap<usize, T>,
max_cache_size: usize,
access_pattern: VecDeque<usize>,
is_temporary: bool,
}
impl<T: TensorElement + Copy> TensorStorage<T> {
pub fn in_memory(data: Vec<T>) -> Self {
Self::InMemory(Arc::new(RwLock::new(data)))
}
pub fn memory_mapped(data: Vec<T>, file_path: Option<PathBuf>) -> Result<Self> {
let storage = MemoryMappedStorage::new(data, file_path)?;
Ok(Self::MemoryMapped(Arc::new(RwLock::new(storage))))
}
pub fn memory_mapped_filled(
num_elements: usize,
value: T,
file_path: Option<PathBuf>,
) -> Result<Self> {
let storage = MemoryMappedStorage::new_filled(num_elements, value, file_path)?;
Ok(Self::MemoryMapped(Arc::new(RwLock::new(storage))))
}
#[cfg(feature = "simd")]
pub fn aligned(data: Vec<T>) -> Result<Self> {
Ok(Self::Aligned(Arc::new(RwLock::new(Self::to_aligned_vec(
&data,
)?))))
}
#[cfg(feature = "simd")]
pub(crate) fn aligned_from_slice(data: &[T]) -> Result<Self> {
Ok(Self::Aligned(Arc::new(RwLock::new(Self::to_aligned_vec(
data,
)?))))
}
#[cfg(feature = "simd")]
fn to_aligned_vec(data: &[T]) -> Result<AlignedVec<T>> {
let mut aligned_vec = AlignedVec::with_capacity(data.len()).map_err(|e| {
TorshError::InvalidArgument(format!("Failed to create aligned storage: {e}"))
})?;
if !data.is_empty() {
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), aligned_vec.as_mut_ptr(), data.len());
aligned_vec.set_len(data.len());
}
}
Ok(aligned_vec)
}
pub fn fast_result(data: Vec<T>) -> Self {
Self::InMemory(Arc::new(RwLock::new(data)))
}
#[cfg(feature = "simd")]
pub fn simd_optimized(data: Vec<T>) -> Result<Self> {
let aligned_vec = Self::to_aligned_vec(&data)?;
let simd_storage = SimdStorage::new(aligned_vec);
Ok(Self::SimdOptimized(Arc::new(simd_storage)))
}
pub fn create_optimal(data: Vec<T>) -> Result<Self> {
let size_bytes = data.len() * std::mem::size_of::<T>();
if size_bytes >= MEMORY_MAPPING_THRESHOLD {
Self::memory_mapped(data, None)
} else {
#[cfg(feature = "simd")]
{
if size_bytes >= SIMD_OPTIMIZED_THRESHOLD {
return Self::simd_optimized(data);
} else if size_bytes >= ALIGNED_STORAGE_THRESHOLD {
return Self::aligned(data);
}
}
Ok(Self::in_memory(data))
}
}
#[cfg(feature = "gpu")]
pub(crate) fn device(buffer: Arc<DeviceBuffer>) -> Self {
Self::Device {
buffer,
host_cache: Arc::new(RwLock::new(None)),
}
}
pub fn is_device(&self) -> bool {
#[cfg(feature = "gpu")]
{
matches!(self, Self::Device { .. })
}
#[cfg(not(feature = "gpu"))]
{
false
}
}
#[cfg(feature = "gpu")]
pub(crate) fn device_buffer(&self) -> Option<&Arc<DeviceBuffer>> {
match self {
Self::Device { buffer, .. } => Some(buffer),
_ => None,
}
}
#[cfg(feature = "gpu")]
fn with_host_cache<R, F>(
buffer: &Arc<DeviceBuffer>,
host_cache: &RwLock<Option<Vec<T>>>,
f: F,
) -> Result<R>
where
F: FnOnce(&[T]) -> Result<R>,
T: Copy,
{
{
let guard = host_cache.read_or_recover();
if let Some(cached) = guard.as_ref() {
return f(cached);
}
}
let downloaded = Self::download(buffer)?;
{
let mut guard = host_cache.write_or_recover();
if guard.is_none() {
*guard = Some(downloaded);
}
}
let guard = host_cache.read_or_recover();
match guard.as_ref() {
Some(cached) => f(cached),
None => Err(TorshError::SynchronizationError(
"device host cache disappeared".to_string(),
)),
}
}
#[cfg(feature = "gpu")]
fn download(buffer: &Arc<DeviceBuffer>) -> Result<Vec<T>>
where
T: Copy,
{
let element_size = std::mem::size_of::<T>();
if element_size == 0 || buffer.bytes() % element_size != 0 {
return Err(TorshError::InvalidOperation(format!(
"device buffer of {} bytes does not hold whole {}-byte elements",
buffer.bytes(),
element_size
)));
}
if buffer.dtype() != T::dtype() {
return Err(TorshError::InvalidOperation(format!(
"device buffer holds {} but the tensor element type is {}",
buffer.dtype(),
T::dtype()
)));
}
let count = buffer.bytes() / element_size;
let mut raw = vec![0u8; buffer.bytes()];
buffer
.backend()
.copy_dtoh(&mut raw, buffer.ptr())
.map_err(|e| TorshError::InvalidOperation(format!("device download failed: {e}")))?;
let mut out: Vec<T> = Vec::with_capacity(count);
if count > 0 {
unsafe {
std::ptr::copy_nonoverlapping(
raw.as_ptr(),
out.as_mut_ptr().cast::<u8>(),
buffer.bytes(),
);
out.set_len(count);
}
}
Ok(out)
}
pub fn len(&self) -> usize {
match self {
Self::InMemory(data) => {
data.read().map(|guard| guard.len()).unwrap_or(0) }
Self::MemoryMapped(storage) => {
storage.read().map(|guard| guard.num_elements).unwrap_or(0) }
#[cfg(feature = "simd")]
Self::Aligned(data) => {
data.read().map(|guard| guard.len()).unwrap_or(0) }
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => storage.len(), #[cfg(feature = "gpu")]
Self::Device { buffer, .. } => {
let element_size = std::mem::size_of::<T>();
if element_size == 0 {
0
} else {
buffer.bytes() / element_size
}
}
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self, index: usize) -> Result<T>
where
T: Copy,
{
match self {
Self::InMemory(data) => {
let data_guard = data.read().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during read".to_string())
})?;
data_guard
.get(index)
.copied()
.ok_or_else(|| TorshError::IndexOutOfBounds {
index,
size: data_guard.len(),
})
}
Self::MemoryMapped(storage) => storage
.write()
.map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?
.get(index),
#[cfg(feature = "simd")]
Self::Aligned(data) => {
let data_guard = data.read().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during read".to_string())
})?;
if index >= data_guard.len() {
Err(TorshError::IndexOutOfBounds {
index,
size: data_guard.len(),
})
} else {
Ok(data_guard.as_slice()[index])
}
}
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => {
storage.with_slice(|slice| {
slice
.get(index)
.copied()
.ok_or_else(|| TorshError::IndexOutOfBounds {
index,
size: slice.len(),
})
})
}
#[cfg(feature = "gpu")]
Self::Device { buffer, host_cache } => {
Self::with_host_cache(buffer, host_cache, |slice| {
slice
.get(index)
.copied()
.ok_or_else(|| TorshError::IndexOutOfBounds {
index,
size: slice.len(),
})
})
}
}
}
pub fn set(&self, index: usize, value: T) -> Result<()>
where
T: Copy,
{
match self {
Self::InMemory(data) => {
let mut data_guard = data.write().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?;
if index >= data_guard.len() {
return Err(TorshError::IndexOutOfBounds {
index,
size: data_guard.len(),
});
}
data_guard[index] = value;
Ok(())
}
Self::MemoryMapped(storage) => storage
.write()
.map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?
.set(index, value),
#[cfg(feature = "simd")]
Self::Aligned(data) => {
let mut data_guard = data.write().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?;
if index >= data_guard.len() {
return Err(TorshError::IndexOutOfBounds {
index,
size: data_guard.len(),
});
}
(*data_guard).set(index, value);
Ok(())
}
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => {
storage.with_slice_mut(|slice| {
let size = slice.len();
match slice.get_mut(index) {
Some(slot) => {
*slot = value;
Ok(())
}
None => Err(TorshError::IndexOutOfBounds { index, size }),
}
})?
}
#[cfg(feature = "gpu")]
Self::Device { .. } => Err(Self::device_is_immutable()),
}
}
#[cfg(feature = "gpu")]
fn device_is_immutable() -> TorshError {
TorshError::InvalidOperation(
"device-resident storage is immutable; call make_unique() or to_device(DeviceType::Cpu) first"
.to_string(),
)
}
pub fn get_slice(&self, start: usize, len: usize) -> Result<Vec<T>>
where
T: Copy,
{
match self {
Self::InMemory(data) => {
let data_guard = data.read().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during read".to_string())
})?;
if start + len > data_guard.len() {
return Err(TorshError::IndexOutOfBounds {
index: start + len - 1,
size: data_guard.len(),
});
}
Ok(data_guard[start..start + len].to_vec())
}
Self::MemoryMapped(storage) => storage
.write()
.map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?
.get_slice(start, len),
#[cfg(feature = "simd")]
Self::Aligned(data) => {
let data_guard = data.read().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during read".to_string())
})?;
if start + len > data_guard.len() {
return Err(TorshError::IndexOutOfBounds {
index: start + len - 1,
size: data_guard.len(),
});
}
let slice = data_guard.as_slice();
Ok(slice[start..start + len].to_vec())
}
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => storage.with_slice(|slice| {
if start + len > slice.len() {
return Err(TorshError::IndexOutOfBounds {
index: start + len - 1,
size: slice.len(),
});
}
Ok(slice[start..start + len].to_vec())
}),
#[cfg(feature = "gpu")]
Self::Device { buffer, host_cache } => {
Self::with_host_cache(buffer, host_cache, |slice| {
if start + len > slice.len() {
return Err(TorshError::IndexOutOfBounds {
index: start + len - 1,
size: slice.len(),
});
}
Ok(slice[start..start + len].to_vec())
})
}
}
}
pub fn set_slice(&self, start: usize, values: &[T]) -> Result<()>
where
T: Copy,
{
match self {
Self::InMemory(data) => {
let mut data_guard = data.write().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?;
if start + values.len() > data_guard.len() {
return Err(TorshError::IndexOutOfBounds {
index: start + values.len() - 1,
size: data_guard.len(),
});
}
data_guard[start..start + values.len()].copy_from_slice(values);
Ok(())
}
Self::MemoryMapped(storage) => storage
.write()
.map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?
.set_slice(start, values),
#[cfg(feature = "simd")]
Self::Aligned(data) => {
let mut data_guard = data.write().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?;
if start + values.len() > data_guard.len() {
return Err(TorshError::IndexOutOfBounds {
index: start + values.len() - 1,
size: data_guard.len(),
});
}
let slice = data_guard.as_mut_slice();
slice[start..start + values.len()].copy_from_slice(values);
Ok(())
}
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => storage.with_slice_mut(|slice| {
let size = slice.len();
if start + values.len() > size {
return Err(TorshError::IndexOutOfBounds {
index: start + values.len() - 1,
size,
});
}
slice[start..start + values.len()].copy_from_slice(values);
Ok(())
})?,
#[cfg(feature = "gpu")]
Self::Device { .. } => Err(Self::device_is_immutable()),
}
}
pub fn to_vec(&self) -> Result<Vec<T>>
where
T: Copy,
{
match self {
Self::InMemory(data) => Ok(data
.read()
.map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during read".to_string())
})?
.clone()),
Self::MemoryMapped(storage) => storage
.write()
.map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?
.to_vec(),
#[cfg(feature = "simd")]
Self::Aligned(data) => {
let data_guard = data.read().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during read".to_string())
})?;
Ok(data_guard.as_slice().to_vec())
}
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => Ok(storage.to_vec()),
#[cfg(feature = "gpu")]
Self::Device { buffer, host_cache } => {
Self::with_host_cache(buffer, host_cache, |slice| Ok(slice.to_vec()))
}
}
}
pub fn storage_type(&self) -> &'static str {
match self {
Self::InMemory(_) => "in_memory",
Self::MemoryMapped(_) => "memory_mapped",
#[cfg(feature = "simd")]
Self::Aligned(_) => "aligned_simd",
#[cfg(feature = "simd")]
Self::SimdOptimized(_) => "simd_optimized",
#[cfg(feature = "gpu")]
Self::Device { .. } => "device",
}
}
pub fn memory_usage(&self) -> usize {
match self {
Self::InMemory(data) => {
data.read()
.map(|guard| guard.len() * std::mem::size_of::<T>())
.unwrap_or(0) }
Self::MemoryMapped(storage) => {
storage
.read()
.map(|storage_guard| {
storage_guard.cache.len() * std::mem::size_of::<T>()
+ std::mem::size_of::<MemoryMappedStorage<T>>()
})
.unwrap_or(std::mem::size_of::<MemoryMappedStorage<T>>()) }
#[cfg(feature = "simd")]
Self::Aligned(data) => {
data.read()
.map(|data_guard| {
data_guard.capacity() * std::mem::size_of::<T>()
})
.unwrap_or(0) }
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => {
let buffers = if storage.is_mutated() { 2 } else { 1 };
storage.capacity() * std::mem::size_of::<T>() * buffers
}
#[cfg(feature = "gpu")]
Self::Device { buffer, host_cache } => {
let cached = host_cache
.read_or_recover()
.as_ref()
.map_or(0, |cache| cache.len() * std::mem::size_of::<T>());
buffer.bytes() + cached
}
}
}
pub fn with_slice<R, F>(&self, f: F) -> Result<R>
where
F: FnOnce(&[T]) -> Result<R>,
T: Copy,
{
match self {
Self::InMemory(data) => {
let data_guard = data.read().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during read".to_string())
})?;
f(data_guard.as_slice())
}
Self::MemoryMapped(storage) => {
let vec = storage
.write()
.map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?
.to_vec()?;
f(&vec)
}
#[cfg(feature = "simd")]
Self::Aligned(data) => {
let data_guard = data.read().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during read".to_string())
})?;
f(data_guard.as_slice())
}
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => {
storage.with_slice(f)
}
#[cfg(feature = "gpu")]
Self::Device { buffer, host_cache } => Self::with_host_cache(buffer, host_cache, f),
}
}
#[cfg(feature = "simd")]
pub fn try_as_slice_direct(&self) -> Option<&[T]> {
match self {
Self::SimdOptimized(storage) => storage.try_as_slice(),
_ => None,
}
}
pub fn with_slice_mut<R, F>(&self, f: F) -> Result<R>
where
F: FnOnce(&mut [T]) -> Result<R>,
T: Copy,
{
match self {
Self::InMemory(data) => {
let mut data_guard = data.write().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?;
f(data_guard.as_mut_slice())
}
Self::MemoryMapped(_) => {
Err(TorshError::InvalidArgument(
"Memory-mapped storage does not support mutable slice access".to_string(),
))
}
#[cfg(feature = "simd")]
Self::Aligned(data) => {
let mut data_guard = data.write().map_err(|_| {
TorshError::SynchronizationError("Lock poisoned during write".to_string())
})?;
f(data_guard.as_mut_slice())
}
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => {
storage.with_slice_mut(f)?
}
#[cfg(feature = "gpu")]
Self::Device { .. } => Err(Self::device_is_immutable()),
}
}
}
fn unique_backing_path() -> PathBuf {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
std::env::temp_dir().join(format!(
"torsh_tensor_{pid}_{nanos}_{seq}.mmap",
pid = std::process::id()
))
}
impl<T: TensorElement> MemoryMappedStorage<T> {
fn open_backing_file(file_path: Option<PathBuf>) -> Result<(File, PathBuf, bool)> {
let (file_path, is_temporary) = match file_path {
Some(path) => (path, false),
None => (unique_backing_path(), true),
};
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(true)
.open(&file_path)
.map_err(|e| {
TorshError::IoError(format!("Failed to create memory-mapped file: {e}"))
})?;
Ok((file, file_path, is_temporary))
}
pub fn new(data: Vec<T>, file_path: Option<PathBuf>) -> Result<Self> {
let (mut file, file_path, is_temporary) = Self::open_backing_file(file_path)?;
let data_bytes = unsafe {
std::slice::from_raw_parts(
data.as_ptr() as *const u8,
std::mem::size_of_val(data.as_slice()),
)
};
file.write_all(data_bytes).map_err(|e| {
TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
})?;
file.flush()
.map_err(|e| TorshError::IoError(format!("Failed to flush memory-mapped file: {e}")))?;
Ok(Self {
file,
file_path,
num_elements: data.len(),
cache: HashMap::new(),
max_cache_size: 10000, access_pattern: VecDeque::new(),
is_temporary,
})
}
pub fn new_filled(num_elements: usize, value: T, file_path: Option<PathBuf>) -> Result<Self>
where
T: Copy,
{
let (mut file, file_path, is_temporary) = Self::open_backing_file(file_path)?;
let element_size = std::mem::size_of::<T>();
if element_size > 0 && num_elements > 0 {
const TARGET_CHUNK_BYTES: usize = 1024 * 1024;
let chunk_elements = (TARGET_CHUNK_BYTES / element_size).clamp(1, num_elements);
let chunk = vec![value; chunk_elements];
let chunk_bytes = unsafe {
std::slice::from_raw_parts(
chunk.as_ptr() as *const u8,
std::mem::size_of_val(chunk.as_slice()),
)
};
let mut written = 0usize;
while written < num_elements {
let remaining = num_elements - written;
let this_chunk = remaining.min(chunk_elements);
file.write_all(&chunk_bytes[..this_chunk * element_size])
.map_err(|e| {
TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
})?;
written += this_chunk;
}
}
file.flush()
.map_err(|e| TorshError::IoError(format!("Failed to flush memory-mapped file: {e}")))?;
Ok(Self {
file,
file_path,
num_elements,
cache: HashMap::new(),
max_cache_size: 10000,
access_pattern: VecDeque::new(),
is_temporary,
})
}
pub fn file_path(&self) -> &std::path::Path {
&self.file_path
}
pub fn get(&mut self, index: usize) -> Result<T>
where
T: Copy,
{
if index >= self.num_elements {
return Err(TorshError::IndexOutOfBounds {
index,
size: self.num_elements,
});
}
if let Some(&value) = self.cache.get(&index) {
self.update_access_pattern(index);
return Ok(value);
}
let value = self.read_element_from_file(index)?;
if self.cache.len() < self.max_cache_size {
self.cache.insert(index, value);
} else {
self.evict_lru();
self.cache.insert(index, value);
}
self.update_access_pattern(index);
Ok(value)
}
pub fn set(&mut self, index: usize, value: T) -> Result<()>
where
T: Copy,
{
if index >= self.num_elements {
return Err(TorshError::IndexOutOfBounds {
index,
size: self.num_elements,
});
}
self.cache.insert(index, value);
self.write_element_to_file(index, value)?;
self.update_access_pattern(index);
Ok(())
}
pub fn get_slice(&mut self, start: usize, len: usize) -> Result<Vec<T>>
where
T: Copy,
{
if start + len > self.num_elements {
return Err(TorshError::IndexOutOfBounds {
index: start + len - 1,
size: self.num_elements,
});
}
if len == 0 {
return Ok(Vec::new());
}
let element_size = std::mem::size_of::<T>();
let mut buf = global_acquire_uninit::<T>(len);
if element_size == 0 {
let uninit = buf.as_uninit_slice_mut();
for slot in uninit.iter_mut().take(len) {
slot.write(unsafe { std::mem::zeroed() });
}
return Ok(buf.into_vec(len));
}
{
let byte_len = len * element_size;
let ptr = buf.as_uninit_slice_mut().as_mut_ptr() as *mut u8;
let byte_buf = unsafe {
std::ptr::write_bytes(ptr, 0, byte_len);
std::slice::from_raw_parts_mut(ptr, byte_len)
};
self.read_bytes_at(byte_buf, (start * element_size) as u64)?;
}
Ok(buf.into_vec(len))
}
fn read_bytes_at(&mut self, buffer: &mut [u8], offset: u64) -> Result<()> {
#[cfg(unix)]
{
self.file.read_exact_at(buffer, offset).map_err(|e| {
TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
})?;
}
#[cfg(windows)]
{
let mut read_total = 0usize;
while read_total < buffer.len() {
let n = self
.file
.seek_read(&mut buffer[read_total..], offset + read_total as u64)
.map_err(|e| {
TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
})?;
if n == 0 {
return Err(TorshError::IoError(
"Unexpected end of memory-mapped file".to_string(),
));
}
read_total += n;
}
}
#[cfg(not(any(unix, windows)))]
{
use std::io::{Read, Seek, SeekFrom};
self.file.seek(SeekFrom::Start(offset)).map_err(|e| {
TorshError::IoError(format!("Failed to seek in memory-mapped file: {e}"))
})?;
self.file.read_exact(buffer).map_err(|e| {
TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
})?;
}
Ok(())
}
pub fn set_slice(&mut self, start: usize, values: &[T]) -> Result<()>
where
T: Copy,
{
if start + values.len() > self.num_elements {
return Err(TorshError::IndexOutOfBounds {
index: start + values.len() - 1,
size: self.num_elements,
});
}
for (i, &value) in values.iter().enumerate() {
self.set(start + i, value)?;
}
Ok(())
}
pub fn to_vec(&mut self) -> Result<Vec<T>>
where
T: Copy,
{
self.get_slice(0, self.num_elements)
}
fn read_element_from_file(&mut self, index: usize) -> Result<T>
where
T: Copy,
{
let offset = index * std::mem::size_of::<T>();
let mut buffer = vec![0u8; std::mem::size_of::<T>()];
#[cfg(unix)]
{
self.file
.read_exact_at(&mut buffer, offset as u64)
.map_err(|e| {
TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
})?;
}
#[cfg(windows)]
{
self.file
.seek_read(&mut buffer, offset as u64)
.map_err(|e| {
TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
})?;
}
#[cfg(not(any(unix, windows)))]
{
self.file
.seek(SeekFrom::Start(offset as u64))
.map_err(|e| {
TorshError::IoError(format!("Failed to seek in memory-mapped file: {e}"))
})?;
self.file.read_exact(&mut buffer).map_err(|e| {
TorshError::IoError(format!("Failed to read from memory-mapped file: {e}"))
})?;
}
let value = unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const T) };
Ok(value)
}
fn write_element_to_file(&mut self, index: usize, value: T) -> Result<()>
where
T: Copy,
{
let offset = index * std::mem::size_of::<T>();
let buffer = unsafe {
std::slice::from_raw_parts(&value as *const T as *const u8, std::mem::size_of::<T>())
};
#[cfg(unix)]
{
self.file.write_all_at(buffer, offset as u64).map_err(|e| {
TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
})?;
}
#[cfg(windows)]
{
self.file.seek_write(buffer, offset as u64).map_err(|e| {
TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
})?;
}
#[cfg(not(any(unix, windows)))]
{
self.file
.seek(SeekFrom::Start(offset as u64))
.map_err(|e| {
TorshError::IoError(format!("Failed to seek in memory-mapped file: {e}"))
})?;
self.file.write_all(buffer).map_err(|e| {
TorshError::IoError(format!("Failed to write to memory-mapped file: {e}"))
})?;
}
Ok(())
}
fn update_access_pattern(&mut self, index: usize) {
self.access_pattern.push_back(index);
if self.access_pattern.len() > self.max_cache_size {
self.access_pattern.pop_front();
}
}
fn evict_lru(&mut self) {
if let Some(lru_index) = self.access_pattern.front().copied() {
self.cache.remove(&lru_index);
}
}
}
impl<T: TensorElement> Drop for MemoryMappedStorage<T> {
fn drop(&mut self) {
if self.is_temporary {
let _ = std::fs::remove_file(&self.file_path);
}
}
}
impl<T: TensorElement> Clone for TensorStorage<T> {
fn clone(&self) -> Self {
match self {
Self::InMemory(data) => Self::InMemory(Arc::clone(data)),
Self::MemoryMapped(storage) => Self::MemoryMapped(Arc::clone(storage)),
#[cfg(feature = "simd")]
Self::Aligned(data) => Self::Aligned(Arc::clone(data)),
#[cfg(feature = "simd")]
Self::SimdOptimized(storage) => {
storage.mark_shared();
Self::SimdOptimized(Arc::clone(storage))
}
#[cfg(feature = "gpu")]
Self::Device { buffer, host_cache } => Self::Device {
buffer: Arc::clone(buffer),
host_cache: Arc::clone(host_cache),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_in_memory_storage() {
let data = vec![1.0f32, 2.0, 3.0, 4.0];
let storage = TensorStorage::in_memory(data.clone());
assert_eq!(storage.len(), 4);
assert!(!storage.is_empty());
assert_eq!(storage.storage_type(), "in_memory");
assert_eq!(storage.get(0).expect("get(0) failed"), 1.0);
assert_eq!(storage.get(3).expect("get(3) failed"), 4.0);
let slice = storage.get_slice(1, 2).expect("get_slice failed");
assert_eq!(slice, vec![2.0, 3.0]);
}
#[test]
fn test_optimal_storage_selection() {
let small_data = vec![1.0f32; 200];
let small_storage =
TensorStorage::create_optimal(small_data).expect("create_optimal failed");
#[cfg(feature = "simd")]
{
assert_eq!(small_storage.storage_type(), "in_memory");
}
#[cfg(not(feature = "simd"))]
{
assert_eq!(small_storage.storage_type(), "in_memory");
}
}
#[test]
fn test_memory_usage_calculation() {
let data = vec![1.0f32; 1000];
let storage = TensorStorage::in_memory(data);
let expected_size = 1000 * std::mem::size_of::<f32>();
assert_eq!(storage.memory_usage(), expected_size);
}
#[test]
#[cfg(feature = "simd")]
fn test_aligned_storage() {
let data = vec![1.0f32, 2.0, 3.0, 4.0];
let storage =
TensorStorage::aligned(data.clone()).expect("aligned storage creation failed");
assert_eq!(storage.len(), 4);
assert!(!storage.is_empty());
assert_eq!(storage.storage_type(), "aligned_simd");
assert_eq!(storage.get(0).expect("get(0) failed"), 1.0);
assert_eq!(storage.get(3).expect("get(3) failed"), 4.0);
let slice = storage.get_slice(1, 2).expect("get_slice failed");
assert_eq!(slice, vec![2.0, 3.0]);
let vec = storage.to_vec().expect("to_vec failed");
assert_eq!(vec, data);
}
#[test]
#[cfg(feature = "simd")]
fn test_optimal_storage_selection_with_aligned() {
let medium_data = vec![1.0f32; 2000]; let medium_storage = TensorStorage::create_optimal(medium_data)
.expect("create_optimal for medium data failed");
assert_eq!(medium_storage.storage_type(), "aligned_simd");
let small_data = vec![1.0f32; 100]; let small_storage = TensorStorage::create_optimal(small_data)
.expect("create_optimal for small data failed");
assert_eq!(small_storage.storage_type(), "in_memory");
}
}