Files
rustytorch/crates/training/rtx-preprocessing/src/loaders/memory_mapped.rs
T
2026-03-04 00:08:42 +00:00

456 lines
15 KiB
Rust

//! Memory-mapped file loader with lazy loading and prefetching support
use crate::{PreprocessingError, Result};
use dashmap::DashMap;
use memmap2::{Mmap, MmapOptions};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use rtx_tensor::{Device, Tensor};
/// Configuration for memory-mapped file loading
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LoaderConfig {
/// Page size for memory mapping (in bytes)
pub page_size: usize,
/// Prefetching strategy to use
pub prefetch_strategy: PrefetchStrategy,
/// Whether to use shared memory segments
pub use_shared_memory: bool,
/// Whether to enable lazy loading with page faults
pub enable_lazy_loading: bool,
}
impl Default for LoaderConfig {
fn default() -> Self {
Self {
page_size: 4096,
prefetch_strategy: PrefetchStrategy::None,
use_shared_memory: false,
enable_lazy_loading: false,
}
}
}
/// Prefetching strategies for optimizing memory access patterns
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PrefetchStrategy {
/// No prefetching
None,
/// Sequential prefetching with configurable window size
Sequential { window_size: usize },
/// Random access optimized prefetching
Random { cache_size: usize },
/// Adaptive prefetching based on access patterns
Adaptive { max_window_size: usize },
}
/// Statistics for memory-mapped file loader performance
#[derive(Debug, Clone, Default)]
pub struct LoaderStatistics {
pub total_reads: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub bytes_read: u64,
pub pages_loaded: usize,
}
/// Page information for lazy loading
#[derive(Debug, Clone)]
struct PageInfo {
page_id: usize,
offset: usize,
size: usize,
loaded_at: Option<Instant>,
access_count: u64,
}
/// Global shared memory registry for cross-process sharing
static SHARED_MEMORY_REGISTRY: std::sync::LazyLock<DashMap<PathBuf, Arc<SharedMemorySegment>>> =
std::sync::LazyLock::new(DashMap::new);
/// Shared memory segment that can be accessed across processes
#[derive(Debug)]
struct SharedMemorySegment {
mmap: Mmap,
file_size: usize,
path: PathBuf,
reference_count: Arc<Mutex<usize>>,
}
/// Memory-mapped file loader with advanced features
pub struct MemoryMappedFileLoader {
config: LoaderConfig,
mmap: Option<Mmap>,
shared_segment: Option<Arc<SharedMemorySegment>>,
file_path: Option<PathBuf>,
file_size: usize,
pages: RwLock<HashMap<usize, PageInfo>>,
statistics: Arc<RwLock<LoaderStatistics>>,
access_pattern_history: Arc<RwLock<Vec<(usize, Instant)>>>,
}
impl MemoryMappedFileLoader {
/// Create a new memory-mapped file loader with the given configuration
pub fn new(config: LoaderConfig) -> Self {
Self {
config,
mmap: None,
shared_segment: None,
file_path: None,
file_size: 0,
pages: RwLock::new(HashMap::new()),
statistics: Arc::new(RwLock::new(LoaderStatistics::default())),
access_pattern_history: Arc::new(RwLock::new(Vec::new())),
}
}
/// Get the loader configuration
pub fn config(&self) -> &LoaderConfig {
&self.config
}
/// Check if a file is currently loaded
pub fn is_loaded(&self) -> bool {
self.mmap.is_some() || self.shared_segment.is_some()
}
/// Open and memory-map a file
pub fn open(&mut self, path: &Path) -> Result<()> {
// Check if file exists
if !path.exists() {
return Err(PreprocessingError::invalid_input(format!(
"File does not exist: {}",
path.display()
)));
}
let file = File::open(path)
.map_err(|e| PreprocessingError::invalid_input(format!("Failed to open file: {e}")))?;
let file_size = file
.metadata()
.map_err(|e| {
PreprocessingError::invalid_input(format!("Failed to get file metadata: {e}"))
})?
.len() as usize;
if self.config.use_shared_memory {
// Check if this file is already in shared memory
let path_buf = path.to_path_buf();
if let Some(shared_segment) = SHARED_MEMORY_REGISTRY.get(&path_buf) {
// Increment reference count
let mut ref_count = shared_segment.reference_count.lock().unwrap();
*ref_count += 1;
self.shared_segment = Some(shared_segment.clone());
self.file_size = shared_segment.file_size;
self.file_path = Some(path_buf);
return Ok(());
}
// Create new shared segment
let mmap = unsafe {
MmapOptions::new().map(&file).map_err(|e| {
PreprocessingError::invalid_input(format!("Failed to memory-map file: {e}"))
})?
};
let shared_segment = Arc::new(SharedMemorySegment {
mmap,
file_size,
path: path_buf.clone(),
reference_count: Arc::new(Mutex::new(1)),
});
SHARED_MEMORY_REGISTRY.insert(path_buf.clone(), shared_segment.clone());
self.shared_segment = Some(shared_segment);
self.file_path = Some(path_buf);
} else {
// Create private memory mapping
let mmap = unsafe {
MmapOptions::new().map(&file).map_err(|e| {
PreprocessingError::invalid_input(format!("Failed to memory-map file: {e}"))
})?
};
self.mmap = Some(mmap);
self.file_path = Some(path.to_path_buf());
}
self.file_size = file_size;
// Initialize lazy loading pages if enabled
if self.config.enable_lazy_loading {
self.initialize_pages();
}
Ok(())
}
/// Initialize page structure for lazy loading
fn initialize_pages(&self) {
if self.file_size == 0 {
return;
}
let mut pages = self.pages.write();
let page_count = self.file_size.div_ceil(self.config.page_size);
for page_id in 0..page_count {
let offset = page_id * self.config.page_size;
let size = std::cmp::min(self.config.page_size, self.file_size - offset);
pages.insert(
page_id,
PageInfo {
page_id,
offset,
size,
loaded_at: None,
access_count: 0,
},
);
}
}
/// Get the size of the currently loaded file
pub fn file_size(&self) -> usize {
self.file_size
}
/// Get the number of loaded pages
pub fn loaded_pages_count(&self) -> usize {
let pages = self.pages.read();
pages
.values()
.filter(|page| page.loaded_at.is_some())
.count()
}
/// Read a chunk of data from the file
pub fn read_chunk(&self, offset: usize, size: usize, device: &Device) -> Result<Tensor> {
if !self.is_loaded() {
return Err(PreprocessingError::invalid_input("File not loaded"));
}
if offset + size > self.file_size {
return Err(PreprocessingError::invalid_input(format!(
"Read beyond file size: {} + {} > {}",
offset, size, self.file_size
)));
}
// Update statistics
{
let mut stats = self.statistics.write();
stats.total_reads += 1;
stats.bytes_read += size as u64;
}
// Record access pattern for adaptive prefetching
{
let mut history = self.access_pattern_history.write();
history.push((offset, Instant::now()));
// Keep history bounded to prevent memory growth
if history.len() > 1000 {
history.remove(0);
}
}
// Handle lazy loading if enabled
if self.config.enable_lazy_loading {
self.handle_lazy_loading(offset, size)?;
}
// Handle prefetching
self.handle_prefetching(offset, size);
// Get the actual data
let data = self.get_raw_data(offset, size)?;
// Convert to f32 and create tensor
let float_data: Vec<f32> = data
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
let tensor_size = float_data.len();
Tensor::from_slice(&float_data, &[tensor_size], device).map_err(|e| {
PreprocessingError::invalid_input(format!("Failed to create tensor: {e:?}"))
})
}
/// Handle lazy loading for the requested range
fn handle_lazy_loading(&self, offset: usize, size: usize) -> Result<()> {
let start_page = offset / self.config.page_size;
let end_page = (offset + size).div_ceil(self.config.page_size);
let mut pages = self.pages.write();
let now = Instant::now();
for page_id in start_page..end_page {
if let Some(page) = pages.get_mut(&page_id) {
if page.loaded_at.is_none() {
// Simulate page fault and loading
page.loaded_at = Some(now);
page.access_count += 1;
// Update statistics
let mut stats = self.statistics.write();
stats.cache_misses += 1;
stats.pages_loaded += 1;
} else {
page.access_count += 1;
// Update statistics
let mut stats = self.statistics.write();
stats.cache_hits += 1;
}
}
}
Ok(())
}
/// Handle prefetching based on the configured strategy
fn handle_prefetching(&self, offset: usize, size: usize) {
match &self.config.prefetch_strategy {
PrefetchStrategy::None => {
// No prefetching
}
PrefetchStrategy::Sequential { window_size } => {
self.prefetch_sequential(offset, size, *window_size);
}
PrefetchStrategy::Random { cache_size } => {
self.prefetch_random(offset, size, *cache_size);
}
PrefetchStrategy::Adaptive { max_window_size } => {
self.prefetch_adaptive(offset, size, *max_window_size);
}
}
}
/// Sequential prefetching implementation
fn prefetch_sequential(&self, offset: usize, _size: usize, window_size: usize) {
if !self.config.enable_lazy_loading {
return;
}
let start_page = offset / self.config.page_size;
let prefetch_start = start_page + 1;
let prefetch_end = std::cmp::min(
prefetch_start + window_size,
self.file_size.div_ceil(self.config.page_size),
);
let mut pages = self.pages.write();
let now = Instant::now();
for page_id in prefetch_start..prefetch_end {
if let Some(page) = pages.get_mut(&page_id)
&& page.loaded_at.is_none()
{
page.loaded_at = Some(now);
}
}
}
/// Random access prefetching implementation
fn prefetch_random(&self, _offset: usize, _size: usize, _cache_size: usize) {
// Implement random access prefetching based on cache size
// This would involve keeping frequently accessed pages in memory
}
/// Adaptive prefetching implementation
fn prefetch_adaptive(&self, offset: usize, size: usize, max_window_size: usize) {
// Analyze access pattern history to predict next accesses
let history = self.access_pattern_history.read();
if history.len() < 3 {
// Fall back to sequential with small window
self.prefetch_sequential(offset, size, 1);
return;
}
// Simple pattern detection: check if accesses are sequential
let recent_accesses: Vec<usize> = history
.iter()
.rev()
.take(5)
.map(|(offset, _)| *offset)
.collect();
let mut is_sequential = true;
let mut stride = 0;
if recent_accesses.len() >= 2 {
stride = recent_accesses[0].saturating_sub(recent_accesses[1]);
for i in 1..recent_accesses.len() - 1 {
let current_stride = recent_accesses[i].saturating_sub(recent_accesses[i + 1]);
if current_stride != stride {
is_sequential = false;
break;
}
}
}
if is_sequential && stride > 0 {
// Predict next accesses based on detected stride
let predicted_window =
std::cmp::min(stride / self.config.page_size + 1, max_window_size);
self.prefetch_sequential(offset, size, predicted_window);
} else {
// Fall back to small sequential window
self.prefetch_sequential(offset, size, 1);
}
}
/// Get raw data from the memory-mapped file
fn get_raw_data(&self, offset: usize, size: usize) -> Result<&[u8]> {
if let Some(ref mmap) = self.mmap {
Ok(&mmap[offset..offset + size])
} else if let Some(ref shared_segment) = self.shared_segment {
Ok(&shared_segment.mmap[offset..offset + size])
} else {
Err(PreprocessingError::invalid_input(
"No memory mapping available",
))
}
}
/// Check if this loader shares memory with another loader
pub fn shares_memory_with(&self, other: &Self) -> bool {
match (&self.shared_segment, &other.shared_segment) {
(Some(self_segment), Some(other_segment)) => Arc::ptr_eq(self_segment, other_segment),
_ => false,
}
}
/// Get performance statistics
pub fn statistics(&self) -> LoaderStatistics {
self.statistics.read().clone()
}
}
impl Drop for MemoryMappedFileLoader {
fn drop(&mut self) {
if let Some(ref shared_segment) = self.shared_segment
&& let Some(path) = &self.file_path
{
let mut ref_count = shared_segment.reference_count.lock().unwrap();
*ref_count -= 1;
if *ref_count == 0 {
SHARED_MEMORY_REGISTRY.remove(path);
}
}
}
}
// Add Send and Sync bounds for thread safety
unsafe impl Send for MemoryMappedFileLoader {}
unsafe impl Sync for MemoryMappedFileLoader {}