1use std::collections::HashMap;
16use std::fs::File;
17use std::io::{Read, Seek, SeekFrom};
18use std::path::{Path, PathBuf};
19use std::sync::{Arc, Mutex, RwLock};
20use std::time::{Duration, Instant};
21use torsh_core::sync::{MutexExt, RwLockExt};
22
23use torsh_core::{
24 dtype::TensorElement,
25 error::{Result, TorshError},
26 shape::Shape,
27};
28
29#[derive(Debug, Clone)]
31pub struct LazyLoadConfig {
32 pub chunk_size: usize,
34 pub max_cached_chunks: usize,
36 pub cache_ttl: Duration,
38 pub memory_pressure_threshold: usize,
40}
41
42impl Default for LazyLoadConfig {
43 fn default() -> Self {
44 Self {
45 chunk_size: 1024 * 1024, max_cached_chunks: 16,
47 cache_ttl: Duration::from_secs(300), memory_pressure_threshold: 1024 * 1024 * 1024, }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct LazyTensorMetadata {
56 pub shape: Shape,
58 pub dtype: String,
60 pub total_elements: usize,
62 pub element_size: usize,
64 pub data_offset: u64,
66}
67
68#[derive(Debug, Clone)]
70struct CachedChunk<T: TensorElement> {
71 data: Vec<T>,
73 range: (usize, usize),
75 last_accessed: Instant,
77}
78
79pub struct LazyTensor<T: TensorElement> {
81 metadata: LazyTensorMetadata,
83 file: Arc<Mutex<File>>,
85 #[allow(dead_code)]
87 file_path: PathBuf,
88 chunk_cache: Arc<RwLock<HashMap<usize, CachedChunk<T>>>>,
90 config: LazyLoadConfig,
92 _phantom: std::marker::PhantomData<T>,
94}
95
96impl<T: TensorElement> LazyTensor<T> {
97 pub fn new<P: AsRef<Path>>(
107 file_path: P,
108 metadata: LazyTensorMetadata,
109 config: LazyLoadConfig,
110 ) -> Result<Self> {
111 let file_path = file_path.as_ref().to_path_buf();
112 let file = File::open(&file_path)
113 .map_err(|e| TorshError::IoError(format!("Failed to open file: {}", e)))?;
114
115 Ok(Self {
116 metadata,
117 file: Arc::new(Mutex::new(file)),
118 file_path,
119 chunk_cache: Arc::new(RwLock::new(HashMap::new())),
120 config,
121 _phantom: std::marker::PhantomData,
122 })
123 }
124
125 pub fn shape(&self) -> &Shape {
127 &self.metadata.shape
128 }
129
130 pub fn len(&self) -> usize {
132 self.metadata.total_elements
133 }
134
135 pub fn is_empty(&self) -> bool {
137 self.metadata.total_elements == 0
138 }
139}
140
141impl<T: TensorElement + bytemuck::Pod> LazyTensor<T> {
151 pub fn get_element(&self, index: usize) -> Result<T> {
159 if index >= self.metadata.total_elements {
160 return Err(TorshError::InvalidArgument(format!(
161 "Index {} out of bounds for tensor with {} elements",
162 index, self.metadata.total_elements
163 )));
164 }
165
166 let chunk_index = index / self.config.chunk_size;
167 let chunk_offset = index % self.config.chunk_size;
168
169 let chunk = self.load_chunk(chunk_index)?;
170 Ok(chunk.data[chunk_offset])
171 }
172
173 pub fn get_range(&self, start: usize, end: usize) -> Result<Vec<T>> {
182 if start > end || end > self.metadata.total_elements {
183 return Err(TorshError::InvalidArgument(format!(
184 "Invalid range [{}..{}] for tensor with {} elements",
185 start, end, self.metadata.total_elements
186 )));
187 }
188
189 let mut result = Vec::with_capacity(end - start);
190 let start_chunk = start / self.config.chunk_size;
191 let end_chunk = (end - 1) / self.config.chunk_size;
192
193 for chunk_idx in start_chunk..=end_chunk {
194 let chunk = self.load_chunk(chunk_idx)?;
195
196 let chunk_start = chunk_idx * self.config.chunk_size;
197 let chunk_end = std::cmp::min(
198 (chunk_idx + 1) * self.config.chunk_size,
199 self.metadata.total_elements,
200 );
201
202 let range_start = std::cmp::max(start, chunk_start) - chunk_start;
203 let range_end = std::cmp::min(end, chunk_end) - chunk_start;
204
205 result.extend_from_slice(&chunk.data[range_start..range_end]);
206 }
207
208 Ok(result)
209 }
210
211 pub fn load_all(&self) -> Result<Vec<T>> {
216 self.get_range(0, self.metadata.total_elements)
217 }
218
219 fn load_chunk(&self, chunk_index: usize) -> Result<Arc<CachedChunk<T>>> {
221 {
223 let cache = self.chunk_cache.read_or_recover();
224 if let Some(cached) = cache.get(&chunk_index) {
225 return Ok(Arc::new(CachedChunk {
227 data: cached.data.clone(),
228 range: cached.range,
229 last_accessed: Instant::now(),
230 }));
231 }
232 }
233
234 let start_element = chunk_index * self.config.chunk_size;
236 let end_element = std::cmp::min(
237 (chunk_index + 1) * self.config.chunk_size,
238 self.metadata.total_elements,
239 );
240 let chunk_size = end_element - start_element;
241
242 let data = self.load_chunk_from_file(start_element, chunk_size)?;
244
245 let chunk = Arc::new(CachedChunk {
246 data,
247 range: (start_element, end_element),
248 last_accessed: Instant::now(),
249 });
250
251 {
253 let mut cache = self.chunk_cache.write_or_recover();
254
255 self.cleanup_cache(&mut cache);
257
258 cache.insert(chunk_index, (*chunk).clone());
259 }
260
261 Ok(chunk)
262 }
263
264 fn load_chunk_from_file(&self, start_element: usize, chunk_size: usize) -> Result<Vec<T>> {
288 let mut file = self.file.lock_or_recover();
289
290 let file_offset =
291 self.metadata.data_offset + (start_element as u64 * self.metadata.element_size as u64);
292
293 file.seek(SeekFrom::Start(file_offset))
294 .map_err(|e| TorshError::IoError(format!("Failed to seek: {}", e)))?;
295
296 let element_size = std::mem::size_of::<T>();
302 if self.metadata.element_size != element_size {
303 return Err(TorshError::InvalidArgument(format!(
304 "Tensor element size mismatch: file metadata declares {} byte(s) per \
305 element but `{}` is {} byte(s)",
306 self.metadata.element_size,
307 std::any::type_name::<T>(),
308 element_size
309 )));
310 }
311
312 let mut buffer = vec![0u8; chunk_size * element_size];
313 file.read_exact(&mut buffer)
314 .map_err(|e| TorshError::IoError(format!("Failed to read chunk: {}", e)))?;
315
316 let data: Vec<T> = bytemuck::pod_collect_to_vec(&buffer);
320
321 Ok(data)
322 }
323}
324
325impl<T: TensorElement> LazyTensor<T> {
326 fn cleanup_cache(&self, cache: &mut HashMap<usize, CachedChunk<T>>) {
328 let now = Instant::now();
329
330 cache.retain(|_, chunk| now.duration_since(chunk.last_accessed) < self.config.cache_ttl);
332
333 if cache.len() > self.config.max_cached_chunks {
335 let mut chunks_to_remove: Vec<_> = cache
336 .iter()
337 .map(|(idx, chunk)| (*idx, chunk.last_accessed))
338 .collect();
339 chunks_to_remove.sort_by_key(|(_, accessed)| *accessed);
340
341 let to_remove = cache.len() - self.config.max_cached_chunks;
342 for (idx, _) in chunks_to_remove.iter().take(to_remove) {
343 cache.remove(idx);
344 }
345 }
346 }
347
348 pub fn cache_stats(&self) -> CacheStats {
350 let cache = self.chunk_cache.read_or_recover();
351
352 let total_cached_elements: usize = cache.values().map(|chunk| chunk.data.len()).sum();
353
354 CacheStats {
355 cached_chunks: cache.len(),
356 total_cached_elements,
357 estimated_memory_usage: total_cached_elements * std::mem::size_of::<T>(),
358 }
359 }
360
361 pub fn clear_cache(&self) {
363 let mut cache = self.chunk_cache.write_or_recover();
364 cache.clear();
365 }
366
367 pub fn check_memory_pressure(&self) -> Result<()> {
369 let stats = self.cache_stats();
370
371 if stats.estimated_memory_usage > self.config.memory_pressure_threshold {
372 let mut cache = self.chunk_cache.write_or_recover();
373
374 let recent_threshold = Duration::from_secs(60);
376 let now = Instant::now();
377
378 cache.retain(|_, chunk| now.duration_since(chunk.last_accessed) < recent_threshold);
379 }
380
381 Ok(())
382 }
383}
384
385#[derive(Debug, Clone)]
387pub struct CacheStats {
388 pub cached_chunks: usize,
390 pub total_cached_elements: usize,
392 pub estimated_memory_usage: usize,
394}
395
396pub struct LazyTensorBuilder {
398 config: LazyLoadConfig,
399}
400
401impl LazyTensorBuilder {
402 pub fn new() -> Self {
404 Self {
405 config: LazyLoadConfig::default(),
406 }
407 }
408
409 pub fn chunk_size(mut self, size: usize) -> Self {
411 self.config.chunk_size = size;
412 self
413 }
414
415 pub fn max_cached_chunks(mut self, max: usize) -> Self {
417 self.config.max_cached_chunks = max;
418 self
419 }
420
421 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
423 self.config.cache_ttl = ttl;
424 self
425 }
426
427 pub fn memory_pressure_threshold(mut self, threshold: usize) -> Self {
429 self.config.memory_pressure_threshold = threshold;
430 self
431 }
432
433 pub fn build<T: TensorElement, P: AsRef<Path>>(
435 self,
436 file_path: P,
437 metadata: LazyTensorMetadata,
438 ) -> Result<LazyTensor<T>> {
439 LazyTensor::new(file_path, metadata, self.config)
440 }
441}
442
443impl Default for LazyTensorBuilder {
444 fn default() -> Self {
445 Self::new()
446 }
447}
448
449pub mod utils {
451 use super::*;
452 use std::fs::File;
453 use std::io::{BufReader, Read};
454 use torsh_core::dtype::DType;
455
456 pub fn create_metadata_from_header<P: AsRef<Path>>(file_path: P) -> Result<LazyTensorMetadata> {
461 let file = File::open(file_path)?;
462 let mut reader = BufReader::new(file);
463
464 let mut header_size_bytes = [0u8; 4];
467 reader
468 .read_exact(&mut header_size_bytes)
469 .map_err(|e| TorshError::IoError(format!("Failed to read header size: {}", e)))?;
470
471 let header_size = u32::from_le_bytes(header_size_bytes) as usize;
472
473 let mut header_data = vec![0u8; header_size];
474 reader
475 .read_exact(&mut header_data)
476 .map_err(|e| TorshError::IoError(format!("Failed to read header: {}", e)))?;
477
478 let header_str = String::from_utf8(header_data)
485 .map_err(|e| TorshError::SerializationError(format!("Invalid header: {}", e)))?;
486
487 let dims = parse_json_usize_array(&header_str, "shape").ok_or_else(|| {
488 TorshError::SerializationError(format!(
489 "Header is missing a valid 'shape' field: {}",
490 header_str
491 ))
492 })?;
493
494 let dtype = parse_json_string(&header_str, "dtype").ok_or_else(|| {
495 TorshError::SerializationError(format!(
496 "Header is missing a valid 'dtype' field: {}",
497 header_str
498 ))
499 })?;
500
501 let element_size = dtype
504 .parse::<DType>()
505 .map_err(|e| {
506 TorshError::SerializationError(format!("Unsupported dtype '{}': {}", dtype, e))
507 })?
508 .size();
509
510 let shape_elements: usize = dims.iter().product();
513 let total_elements = match parse_json_usize(&header_str, "total_elements") {
514 Some(declared) if declared != shape_elements => {
515 return Err(TorshError::SerializationError(format!(
516 "Header total_elements ({}) does not match shape product ({})",
517 declared, shape_elements
518 )));
519 }
520 Some(declared) => declared,
521 None => shape_elements,
522 };
523
524 Ok(LazyTensorMetadata {
525 shape: Shape::new(dims),
526 dtype,
527 total_elements,
528 element_size,
529 data_offset: 4 + header_size as u64,
530 })
531 }
532
533 fn json_value_after_key<'a>(header: &'a str, key: &str) -> Option<&'a str> {
538 let quoted_key = format!("\"{}\"", key);
539 let key_pos = header.find("ed_key)?;
540 let after_key = &header[key_pos + quoted_key.len()..];
541 let colon_pos = after_key.find(':')?;
542 Some(after_key[colon_pos + 1..].trim_start())
543 }
544
545 fn parse_json_string(header: &str, key: &str) -> Option<String> {
547 let value = json_value_after_key(header, key)?;
548 let inner = value.strip_prefix('"')?;
549 let end = inner.find('"')?;
550 Some(inner[..end].to_string())
551 }
552
553 fn parse_json_usize(header: &str, key: &str) -> Option<usize> {
555 let value = json_value_after_key(header, key)?;
556 let digits: String = value.chars().take_while(|c| c.is_ascii_digit()).collect();
557 if digits.is_empty() {
558 return None;
559 }
560 digits.parse::<usize>().ok()
561 }
562
563 fn parse_json_usize_array(header: &str, key: &str) -> Option<Vec<usize>> {
565 let value = json_value_after_key(header, key)?;
566 let inner = value.strip_prefix('[')?;
567 let end = inner.find(']')?;
568 let mut dims = Vec::new();
569 for part in inner[..end].split(',') {
570 let trimmed = part.trim();
571 if trimmed.is_empty() {
572 continue;
573 }
574 dims.push(trimmed.parse::<usize>().ok()?);
575 }
576 Some(dims)
577 }
578
579 pub fn lazy_tensor_from_file<T: TensorElement, P: AsRef<Path>>(
581 file_path: P,
582 ) -> Result<LazyTensor<T>> {
583 let metadata = create_metadata_from_header(&file_path)?;
584 LazyTensor::new(file_path, metadata, LazyLoadConfig::default())
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use std::io::Write;
592 use tempfile::NamedTempFile;
593 use torsh_core::shape::Shape;
594
595 fn create_test_file() -> (NamedTempFile, LazyTensorMetadata) {
596 let mut temp_file = NamedTempFile::new().expect("temp file creation should succeed");
597
598 let header = r#"{"shape":[10,10],"dtype":"f32","total_elements":100}"#;
600 let header_size = header.len() as u32;
601 temp_file
602 .write_all(&header_size.to_le_bytes())
603 .expect("write should succeed");
604 temp_file
605 .write_all(header.as_bytes())
606 .expect("write should succeed");
607
608 for i in 0..100 {
610 temp_file
611 .write_all(&(i as f32).to_le_bytes())
612 .expect("write should succeed");
613 }
614 temp_file.flush().expect("flush should succeed");
615
616 let metadata = LazyTensorMetadata {
617 shape: Shape::new(vec![10, 10]),
618 dtype: "f32".to_string(),
619 total_elements: 100,
620 element_size: 4,
621 data_offset: 4 + header.len() as u64,
622 };
623
624 (temp_file, metadata)
625 }
626
627 #[test]
628 fn test_lazy_tensor_creation() {
629 let (temp_file, metadata) = create_test_file();
630
631 let lazy_tensor: LazyTensor<f32> =
632 LazyTensor::new(temp_file.path(), metadata, LazyLoadConfig::default())
633 .expect("lazy tensor creation should succeed");
634
635 assert_eq!(lazy_tensor.len(), 100);
636 assert!(!lazy_tensor.is_empty());
637 assert_eq!(lazy_tensor.shape().dims(), &[10, 10]);
638 }
639
640 #[test]
641 fn test_lazy_loading_element_access() {
642 let (temp_file, metadata) = create_test_file();
643
644 let lazy_tensor: LazyTensor<f32> = LazyTensor::new(
645 temp_file.path(),
646 metadata,
647 LazyLoadConfig {
648 chunk_size: 10,
649 ..LazyLoadConfig::default()
650 },
651 )
652 .expect("lazy tensor creation should succeed");
653
654 let element = lazy_tensor
656 .get_element(5)
657 .expect("get_element should succeed");
658 assert!((element - 5.0).abs() < f32::EPSILON);
659
660 let element = lazy_tensor
661 .get_element(50)
662 .expect("get_element should succeed");
663 assert!((element - 50.0).abs() < f32::EPSILON);
664 }
665
666 #[test]
667 fn test_lazy_loading_range_access() {
668 let (temp_file, metadata) = create_test_file();
669
670 let lazy_tensor: LazyTensor<f32> = LazyTensor::new(
671 temp_file.path(),
672 metadata,
673 LazyLoadConfig {
674 chunk_size: 10,
675 ..LazyLoadConfig::default()
676 },
677 )
678 .expect("lazy tensor creation should succeed");
679
680 let range = lazy_tensor
682 .get_range(10, 20)
683 .expect("get_range should succeed");
684 assert_eq!(range.len(), 10);
685
686 for (i, &value) in range.iter().enumerate() {
687 let expected = (10 + i) as f32;
688 assert!((value - expected).abs() < f32::EPSILON);
689 }
690 }
691
692 #[test]
693 fn test_cache_management() {
694 let (temp_file, metadata) = create_test_file();
695
696 let lazy_tensor: LazyTensor<f32> = LazyTensor::new(
697 temp_file.path(),
698 metadata,
699 LazyLoadConfig {
700 chunk_size: 10,
701 max_cached_chunks: 2,
702 ..LazyLoadConfig::default()
703 },
704 )
705 .expect("lazy tensor creation should succeed");
706
707 lazy_tensor
709 .get_element(5)
710 .expect("get_element should succeed"); lazy_tensor
712 .get_element(15)
713 .expect("get_element should succeed"); let stats = lazy_tensor.cache_stats();
716 assert!(stats.cached_chunks <= 2); lazy_tensor
719 .get_element(25)
720 .expect("get_element should succeed"); let stats_after = lazy_tensor.cache_stats();
723 assert!(stats_after.cached_chunks <= 3);
726 assert!(stats_after.total_cached_elements > 0);
727 }
728
729 #[test]
730 fn test_lazy_tensor_builder() {
731 let (temp_file, metadata) = create_test_file();
732
733 let lazy_tensor: LazyTensor<f32> = LazyTensorBuilder::new()
734 .chunk_size(5)
735 .max_cached_chunks(3)
736 .build(temp_file.path(), metadata)
737 .expect("lazy operation should succeed");
738
739 assert_eq!(lazy_tensor.config.chunk_size, 5);
740 assert_eq!(lazy_tensor.config.max_cached_chunks, 3);
741 }
742
743 #[test]
744 fn test_out_of_bounds_access() {
745 let (temp_file, metadata) = create_test_file();
746
747 let lazy_tensor: LazyTensor<f32> =
748 LazyTensor::new(temp_file.path(), metadata, LazyLoadConfig::default())
749 .expect("lazy tensor creation should succeed");
750
751 let result = lazy_tensor.get_element(1000);
753 assert!(result.is_err());
754
755 let result = lazy_tensor.get_range(90, 110);
756 assert!(result.is_err());
757 }
758
759 fn unique_temp_path(tag: &str) -> std::path::PathBuf {
761 use std::sync::atomic::{AtomicU64, Ordering};
762 static COUNTER: AtomicU64 = AtomicU64::new(0);
763 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
764 let pid = std::process::id();
765 let nanos = std::time::SystemTime::now()
766 .duration_since(std::time::UNIX_EPOCH)
767 .map(|d| d.as_nanos())
768 .unwrap_or(0);
769 std::env::temp_dir().join(format!("torsh_{}_{}_{}_{}.bin", tag, pid, n, nanos))
770 }
771
772 fn write_header_file(path: &std::path::Path, header: &str, data: &[f32]) {
775 let mut file = std::fs::File::create(path).expect("temp file creation should succeed");
776 let header_size = header.len() as u32;
777 file.write_all(&header_size.to_le_bytes())
778 .expect("write should succeed");
779 file.write_all(header.as_bytes())
780 .expect("write should succeed");
781 for &value in data {
782 file.write_all(&value.to_le_bytes())
783 .expect("write should succeed");
784 }
785 file.flush().expect("flush should succeed");
786 }
787
788 #[test]
789 fn test_create_metadata_from_header_populates_all_fields() {
790 let header = r#"{"shape":[3,4,5],"dtype":"f32","total_elements":60}"#;
791 let path = unique_temp_path("lazy_header_f32");
792 write_header_file(&path, header, &[]);
793
794 let metadata =
795 utils::create_metadata_from_header(&path).expect("metadata parsing should succeed");
796
797 assert_eq!(metadata.shape.dims(), &[3, 4, 5]);
799 assert_eq!(metadata.dtype, "f32");
800 assert_eq!(metadata.total_elements, 60);
801 assert_eq!(metadata.element_size, 4);
802 assert_eq!(metadata.data_offset, 4 + header.len() as u64);
803
804 let _ = std::fs::remove_file(&path);
805 }
806
807 #[test]
808 fn test_create_metadata_from_header_derives_element_size_and_total() {
809 let header = r#"{"shape":[2,8],"dtype":"f64"}"#;
812 let path = unique_temp_path("lazy_header_f64");
813 write_header_file(&path, header, &[]);
814
815 let metadata =
816 utils::create_metadata_from_header(&path).expect("metadata parsing should succeed");
817
818 assert_eq!(metadata.shape.dims(), &[2, 8]);
819 assert_eq!(metadata.dtype, "f64");
820 assert_eq!(metadata.total_elements, 16);
821 assert_eq!(metadata.element_size, 8);
822 assert_eq!(metadata.data_offset, 4 + header.len() as u64);
823
824 let _ = std::fs::remove_file(&path);
825 }
826
827 #[test]
828 fn test_create_metadata_from_header_roundtrip_load() {
829 let header = r#"{"shape":[2,3],"dtype":"f32","total_elements":6}"#;
832 let path = unique_temp_path("lazy_header_roundtrip");
833 let data: Vec<f32> = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
834 write_header_file(&path, header, &data);
835
836 let metadata =
837 utils::create_metadata_from_header(&path).expect("metadata parsing should succeed");
838 assert_eq!(metadata.total_elements, 6);
839 assert_eq!(metadata.data_offset, 4 + header.len() as u64);
840
841 let lazy: LazyTensor<f32> = LazyTensor::new(
842 &path,
843 metadata,
844 LazyLoadConfig {
845 chunk_size: 4,
846 ..LazyLoadConfig::default()
847 },
848 )
849 .expect("lazy tensor creation should succeed");
850
851 let loaded = lazy.load_all().expect("load_all should succeed");
852 assert_eq!(loaded, data);
853
854 let _ = std::fs::remove_file(&path);
855 }
856
857 #[test]
858 fn test_create_metadata_from_header_rejects_missing_shape() {
859 let header = r#"{"dtype":"f32","total_elements":6}"#;
860 let path = unique_temp_path("lazy_header_missing_shape");
861 write_header_file(&path, header, &[]);
862
863 let result = utils::create_metadata_from_header(&path);
864 assert!(result.is_err());
865
866 let _ = std::fs::remove_file(&path);
867 }
868
869 #[test]
870 fn test_create_metadata_from_header_rejects_inconsistent_total() {
871 let header = r#"{"shape":[2,3],"dtype":"f32","total_elements":99}"#;
873 let path = unique_temp_path("lazy_header_inconsistent");
874 write_header_file(&path, header, &[]);
875
876 let result = utils::create_metadata_from_header(&path);
877 assert!(result.is_err());
878
879 let _ = std::fs::remove_file(&path);
880 }
881}