Skip to main content

gguf_rs_lib/reader/
tensor_reader.rs

1//! Tensor-specific reading utilities
2
3use crate::error::{GGUFError, Result};
4use crate::tensor::{TensorData, TensorInfo};
5use std::io::{Read, Seek, SeekFrom};
6
7const MAX_READ_CHUNK_SIZE: usize = 1024 * 1024;
8
9/// Specialized reader for tensor data with format-specific handling
10#[derive(Debug)]
11pub struct TensorReader<R> {
12    /// Underlying reader
13    reader: R,
14    /// Current position
15    position: u64,
16}
17
18/// Options for tensor reading
19#[derive(Debug, Clone)]
20pub struct TensorReadOptions {
21    /// Whether to validate the descriptor offset against GGUF's minimum 8-byte alignment
22    pub validate_alignment: bool,
23    /// Whether to compute the crate's non-cryptographic payload checksum.
24    ///
25    /// GGUF carries no reference checksum to verify, so this records a value
26    /// for caller comparison; it does not establish integrity or authenticity.
27    pub compute_checksum: bool,
28    /// Whether to decompress quantized data
29    pub decompress_quantized: bool,
30    /// Maximum tensor size to read (0 = no limit)
31    pub max_tensor_size: usize,
32    /// Preferred buffer size for reading; temporary chunks are capped at 1 MiB.
33    pub buffer_size: usize,
34}
35
36impl Default for TensorReadOptions {
37    fn default() -> Self {
38        Self {
39            validate_alignment: true,
40            compute_checksum: false,
41            decompress_quantized: false, // Keep raw quantized data by default
42            max_tensor_size: 0,
43            buffer_size: 1024 * 1024, // 1MB buffer
44        }
45    }
46}
47
48/// Result of reading tensor data
49#[derive(Debug, Clone)]
50pub struct TensorReadResult {
51    /// The tensor data
52    pub data: TensorData,
53    /// Actual bytes read
54    pub bytes_read: usize,
55    /// Whether the data was decompressed
56    pub was_decompressed: bool,
57    /// Checksum of the data (if computed)
58    pub checksum: Option<u32>,
59}
60
61impl<R: Read> TensorReader<R> {
62    /// Create a new tensor reader
63    pub fn new(reader: R) -> Self {
64        Self { reader, position: 0 }
65    }
66
67    /// Read tensor data with default options
68    pub fn read_tensor_data(&mut self, tensor_info: &TensorInfo) -> Result<TensorReadResult> {
69        self.read_tensor_data_with_options(tensor_info, &TensorReadOptions::default())
70    }
71
72    /// Read tensor data with custom options
73    pub fn read_tensor_data_with_options(
74        &mut self,
75        tensor_info: &TensorInfo,
76        options: &TensorReadOptions,
77    ) -> Result<TensorReadResult> {
78        let expected_size =
79            usize::try_from(tensor_info.checked_expected_data_size()?).map_err(|_| {
80                GGUFError::InvalidTensorData(format!(
81                    "Tensor '{}' size does not fit this platform",
82                    tensor_info.name()
83                ))
84            })?;
85
86        // Check size limits
87        if options.max_tensor_size > 0 && expected_size > options.max_tensor_size {
88            return Err(GGUFError::InvalidTensorData(format!(
89                "Tensor '{}' size {} exceeds maximum {}",
90                tensor_info.name(),
91                expected_size,
92                options.max_tensor_size
93            )));
94        }
95
96        // Reject descriptor/configuration errors before consuming input or
97        // allocating a payload-sized buffer.
98        if options.validate_alignment {
99            self.validate_tensor_alignment(tensor_info)?;
100        }
101        if options.decompress_quantized && tensor_info.tensor_type().is_quantized() {
102            return Err(GGUFError::FeatureUnavailable(format!(
103                "decompression for {} tensors",
104                tensor_info.tensor_type()
105            )));
106        }
107
108        // Grow only after each bounded read so a truncated hostile input cannot force a
109        // descriptor-sized allocation before any tensor bytes have arrived.
110        let data = self.read_exact_owned_tracking(expected_size, options.buffer_size)?;
111
112        let tensor_data = TensorData::new_owned(data);
113        let checksum = options.compute_checksum.then(|| tensor_data.checksum());
114
115        Ok(TensorReadResult {
116            data: tensor_data,
117            bytes_read: expected_size,
118            was_decompressed: false,
119            checksum,
120        })
121    }
122
123    /// Read multiple tensors efficiently
124    pub fn read_multiple_tensors(
125        &mut self,
126        tensor_infos: &[&TensorInfo],
127        options: &TensorReadOptions,
128    ) -> Result<Vec<TensorReadResult>> {
129        let mut results = Vec::new();
130        results.try_reserve_exact(tensor_infos.len()).map_err(|_| {
131            GGUFError::InvalidTensorData("Unable to allocate tensor read results".to_string())
132        })?;
133
134        for tensor_info in tensor_infos {
135            let result = self.read_tensor_data_with_options(tensor_info, options)?;
136            results.push(result);
137        }
138
139        Ok(results)
140    }
141
142    /// Read tensor data in chunks for large tensors
143    pub fn read_tensor_chunked(
144        &mut self,
145        tensor_info: &TensorInfo,
146        chunk_size: usize,
147        mut callback: impl FnMut(&[u8]) -> Result<()>,
148    ) -> Result<()> {
149        let total_size =
150            usize::try_from(tensor_info.checked_expected_data_size()?).map_err(|_| {
151                GGUFError::InvalidTensorData(format!(
152                    "Tensor '{}' size does not fit this platform",
153                    tensor_info.name()
154                ))
155            })?;
156        if total_size > 0 && chunk_size == 0 {
157            return Err(GGUFError::InvalidTensorData(
158                "Tensor read chunk size must be greater than zero".to_string(),
159            ));
160        }
161        let mut remaining = total_size;
162        let buffer_size = chunk_size.min(remaining).min(MAX_READ_CHUNK_SIZE);
163        let mut buffer = Vec::new();
164        buffer.try_reserve_exact(buffer_size).map_err(|_| {
165            GGUFError::InvalidTensorData("Unable to allocate tensor read chunk".to_string())
166        })?;
167        buffer.resize(buffer_size, 0);
168
169        while remaining > 0 {
170            let to_read = buffer.len().min(remaining);
171            self.read_exact_tracking(&mut buffer[..to_read])?;
172
173            callback(&buffer[..to_read])?;
174            remaining -= to_read;
175        }
176
177        Ok(())
178    }
179
180    /// Validate tensor data alignment
181    fn validate_tensor_alignment(&self, tensor_info: &TensorInfo) -> Result<()> {
182        // TensorReader does not have access to `general.alignment`; the full file and
183        // stream readers enforce that exact value. Every valid declared alignment is
184        // a multiple of eight, so this is the strongest deterministic check available.
185        const MIN_GGUF_ALIGNMENT: u64 = 8;
186        if !tensor_info.data_offset().is_multiple_of(MIN_GGUF_ALIGNMENT) {
187            return Err(GGUFError::InvalidTensorData(format!(
188                "Tensor '{}' offset is not aligned to at least {} bytes",
189                tensor_info.name(),
190                MIN_GGUF_ALIGNMENT
191            )));
192        }
193
194        Ok(())
195    }
196
197    /// Get current position
198    pub fn position(&self) -> u64 {
199        self.position
200    }
201
202    /// Reset position counter
203    pub fn reset_position(&mut self) {
204        self.position = 0;
205    }
206
207    /// Skip bytes in the stream
208    pub fn skip_bytes(&mut self, count: usize) -> Result<()> {
209        let mut buffer = [0u8; 8192];
210        let mut remaining = count;
211
212        while remaining > 0 {
213            let to_skip = remaining.min(buffer.len());
214            self.read_exact_tracking(&mut buffer[..to_skip])?;
215            remaining -= to_skip;
216        }
217
218        Ok(())
219    }
220
221    fn read_exact_owned_tracking(&mut self, size: usize, buffer_size: usize) -> Result<Vec<u8>> {
222        if size == 0 {
223            return Ok(Vec::new());
224        }
225        if buffer_size == 0 {
226            return Err(GGUFError::InvalidTensorData(
227                "Tensor read buffer size must be greater than zero".to_string(),
228            ));
229        }
230
231        let chunk_size = buffer_size.min(size).min(MAX_READ_CHUNK_SIZE);
232        let mut chunk = Vec::new();
233        chunk.try_reserve_exact(chunk_size).map_err(|_| {
234            GGUFError::InvalidTensorData("Unable to allocate tensor read chunk".to_string())
235        })?;
236        chunk.resize(chunk_size, 0);
237        let mut data = Vec::new();
238        let mut remaining = size;
239        while remaining > 0 {
240            let to_read = remaining.min(chunk.len());
241            self.read_exact_tracking(&mut chunk[..to_read])?;
242            data.try_reserve(to_read).map_err(|_| {
243                GGUFError::InvalidTensorData("Unable to allocate tensor data buffer".to_string())
244            })?;
245            data.extend_from_slice(&chunk[..to_read]);
246            remaining -= to_read;
247        }
248        Ok(data)
249    }
250
251    fn read_exact_tracking(&mut self, mut buffer: &mut [u8]) -> Result<()> {
252        while !buffer.is_empty() {
253            match self.reader.read(buffer) {
254                Ok(0) => return Err(GGUFError::UnexpectedEof),
255                Ok(bytes_read) => {
256                    self.advance(bytes_read)?;
257                    let (_, remaining) = buffer.split_at_mut(bytes_read);
258                    buffer = remaining;
259                }
260                Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
261                Err(error) => return Err(error.into()),
262            }
263        }
264        Ok(())
265    }
266
267    fn advance(&mut self, bytes: usize) -> Result<()> {
268        let bytes = u64::try_from(bytes)
269            .map_err(|_| GGUFError::InvalidTensorData("Read size does not fit u64".to_string()))?;
270        self.position = self.position.checked_add(bytes).ok_or_else(|| {
271            GGUFError::InvalidTensorData("Tensor reader position overflows u64".to_string())
272        })?;
273        Ok(())
274    }
275}
276
277impl<R: Read + Seek> TensorReader<R> {
278    /// Create a tensor reader with seeking support
279    pub fn with_seek(reader: R) -> Self {
280        Self { reader, position: 0 }
281    }
282
283    /// Seek to a specific position
284    pub fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
285        let new_pos = self.reader.seek(pos)?;
286        self.position = new_pos;
287        Ok(new_pos)
288    }
289
290    /// Read tensor data at a specific offset
291    pub fn read_tensor_at_offset(
292        &mut self,
293        tensor_info: &TensorInfo,
294        offset: u64,
295        options: &TensorReadOptions,
296    ) -> Result<TensorReadResult> {
297        self.seek(SeekFrom::Start(offset))?;
298        self.read_tensor_data_with_options(tensor_info, options)
299    }
300
301    /// Read multiple tensors by seeking to their offsets
302    pub fn read_tensors_by_offset(
303        &mut self,
304        tensors: &[(u64, &TensorInfo)], // (offset, tensor_info) pairs
305        options: &TensorReadOptions,
306    ) -> Result<Vec<TensorReadResult>> {
307        let mut results = Vec::new();
308        results.try_reserve_exact(tensors.len()).map_err(|_| {
309            GGUFError::InvalidTensorData("Unable to allocate tensor read results".to_string())
310        })?;
311
312        for &(offset, tensor_info) in tensors {
313            let result = self.read_tensor_at_offset(tensor_info, offset, options)?;
314            results.push(result);
315        }
316
317        Ok(results)
318    }
319}
320
321/// Utility functions for tensor reading
322pub struct TensorReadUtils;
323
324impl TensorReadUtils {
325    /// Calculate optimal buffer size for reading a tensor
326    pub fn optimal_buffer_size(tensor_info: &TensorInfo) -> usize {
327        let tensor_size = tensor_info
328            .checked_expected_data_size()
329            .ok()
330            .and_then(|size| usize::try_from(size).ok())
331            .unwrap_or(usize::MAX);
332
333        // Use powers of 2 for better memory alignment
334        let base_size = match tensor_size {
335            0..=4096 => 4096,             // 4KB for small tensors
336            4097..=65_536 => 16_384,      // 16KB for medium tensors
337            65_537..=1_048_576 => 65_536, // 64KB for large tensors
338            _ => 262_144,                 // 256KB for very large tensors
339        };
340
341        base_size.min(tensor_size)
342    }
343
344    /// Check if a tensor should be read in chunks
345    pub fn should_read_chunked(tensor_info: &TensorInfo, chunk_threshold: usize) -> bool {
346        tensor_info
347            .checked_expected_data_size()
348            .ok()
349            .and_then(|size| usize::try_from(size).ok())
350            .is_none_or(|size| size > chunk_threshold)
351    }
352
353    /// Calculate memory requirements for reading tensors
354    pub fn calculate_memory_requirements(tensor_infos: &[&TensorInfo]) -> TensorMemoryRequirements {
355        let mut total_size = 0u64;
356        let mut max_tensor_size = 0u64;
357        let mut quantized_count = 0;
358        let mut non_quantized_count = 0;
359
360        for tensor_info in tensor_infos {
361            let size = tensor_info.expected_data_size();
362            total_size = total_size.saturating_add(size);
363            max_tensor_size = max_tensor_size.max(size);
364
365            if tensor_info.tensor_type().is_quantized() {
366                quantized_count += 1;
367            } else {
368                non_quantized_count += 1;
369            }
370        }
371
372        TensorMemoryRequirements {
373            total_size: usize::try_from(total_size).unwrap_or(usize::MAX),
374            max_tensor_size: usize::try_from(max_tensor_size).unwrap_or(usize::MAX),
375            tensor_count: tensor_infos.len(),
376            quantized_tensor_count: quantized_count,
377            non_quantized_tensor_count: non_quantized_count,
378            recommended_buffer_size: tensor_infos
379                .iter()
380                .max_by_key(|tensor| tensor.expected_data_size())
381                .map_or(0, |tensor| Self::optimal_buffer_size(tensor)),
382        }
383    }
384}
385
386/// Memory requirements for reading tensors
387#[derive(Debug, Clone, PartialEq, Eq)]
388pub struct TensorMemoryRequirements {
389    /// Total size of all tensors
390    pub total_size: usize,
391    /// Size of the largest tensor
392    pub max_tensor_size: usize,
393    /// Total number of tensors
394    pub tensor_count: usize,
395    /// Number of quantized tensors
396    pub quantized_tensor_count: usize,
397    /// Number of non-quantized tensors
398    pub non_quantized_tensor_count: usize,
399    /// Recommended buffer size for reading
400    pub recommended_buffer_size: usize,
401}
402
403impl TensorMemoryRequirements {
404    /// Get the average tensor size
405    pub fn average_tensor_size(&self) -> usize {
406        self.total_size.checked_div(self.tensor_count).unwrap_or(0)
407    }
408
409    /// Check if memory requirements are reasonable
410    pub fn is_reasonable(&self, available_memory: usize) -> bool {
411        // Should use less than or equal to 80% of available memory
412        self.total_size <= available_memory.saturating_mul(4) / 5
413    }
414}
415
416impl std::fmt::Display for TensorReadResult {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        write!(
419            f,
420            "TensorReadResult {{ bytes: {}, decompressed: {}{}}}",
421            self.bytes_read,
422            self.was_decompressed,
423            if let Some(checksum) = self.checksum {
424                format!(", checksum: 0x{:08x}", checksum)
425            } else {
426                String::new()
427            }
428        )
429    }
430}
431
432impl std::fmt::Display for TensorMemoryRequirements {
433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434        write!(
435            f,
436            "TensorMemoryRequirements {{ total: {} bytes, max: {} bytes, count: {} ({} quantized, {} non-quantized), avg: {} bytes }}",
437            self.total_size,
438            self.max_tensor_size,
439            self.tensor_count,
440            self.quantized_tensor_count,
441            self.non_quantized_tensor_count,
442            self.average_tensor_size()
443        )
444    }
445}
446
447#[cfg(all(test, feature = "std"))]
448mod tests {
449    use super::*;
450    use crate::tensor::{TensorShape, TensorType};
451    use std::io::Cursor;
452
453    fn create_test_tensor_info(name: &str, shape: Vec<u64>, tensor_type: TensorType) -> TensorInfo {
454        let shape = TensorShape::new(shape).unwrap();
455        TensorInfo::new(name.to_string(), shape, tensor_type, 0)
456    }
457
458    #[test]
459    fn test_tensor_reader_creation() {
460        let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
461        let cursor = Cursor::new(data);
462        let reader = TensorReader::new(cursor);
463
464        assert_eq!(reader.position(), 0);
465    }
466
467    #[test]
468    fn test_read_tensor_data() {
469        let data = vec![0u8; 16]; // 4 F32 values
470        let cursor = Cursor::new(data);
471        let mut reader = TensorReader::new(cursor);
472
473        let tensor_info = create_test_tensor_info("test", vec![4], TensorType::F32);
474        let options = TensorReadOptions {
475            validate_alignment: false, // Skip alignment check for test data
476            ..Default::default()
477        };
478        let result = reader.read_tensor_data_with_options(&tensor_info, &options).unwrap();
479
480        assert_eq!(result.bytes_read, 16);
481        assert_eq!(result.data.len(), 16);
482        assert!(!result.was_decompressed);
483    }
484
485    #[test]
486    fn test_read_tensor_with_options() {
487        let data = vec![0u8; 8]; // 2 F32 values
488        let cursor = Cursor::new(data);
489        let mut reader = TensorReader::new(cursor);
490
491        let tensor_info = create_test_tensor_info("test", vec![2], TensorType::F32);
492        let options = TensorReadOptions {
493            compute_checksum: true,
494            validate_alignment: false, // Skip alignment check for test
495            ..Default::default()
496        };
497
498        let result = reader.read_tensor_data_with_options(&tensor_info, &options).unwrap();
499        assert!(result.checksum.is_some());
500    }
501
502    #[test]
503    fn test_read_multiple_tensors() {
504        let data = vec![0u8; 24]; // Two tensors: 4 F32 + 2 F32
505        let cursor = Cursor::new(data);
506        let mut reader = TensorReader::new(cursor);
507
508        let tensor1 = create_test_tensor_info("tensor1", vec![4], TensorType::F32);
509        let tensor2 = create_test_tensor_info("tensor2", vec![2], TensorType::F32);
510        let tensor_infos = vec![&tensor1, &tensor2];
511
512        let options = TensorReadOptions {
513            validate_alignment: false, // Skip alignment check for test data
514            ..Default::default()
515        };
516        let results = reader.read_multiple_tensors(&tensor_infos, &options).unwrap();
517        assert_eq!(results.len(), 2);
518        assert_eq!(results[0].bytes_read, 16);
519        assert_eq!(results[1].bytes_read, 8);
520    }
521
522    #[test]
523    fn test_read_tensor_chunked() {
524        let data = vec![0u8; 16];
525        let cursor = Cursor::new(data);
526        let mut reader = TensorReader::new(cursor);
527
528        let tensor_info = create_test_tensor_info("test", vec![4], TensorType::F32);
529        let mut chunks = Vec::new();
530
531        reader
532            .read_tensor_chunked(&tensor_info, 8, |chunk| {
533                chunks.push(chunk.to_vec());
534                Ok(())
535            })
536            .unwrap();
537
538        assert_eq!(chunks.len(), 2);
539        assert_eq!(chunks[0].len(), 8);
540        assert_eq!(chunks[1].len(), 8);
541    }
542
543    #[test]
544    fn test_zero_sized_read_buffers_are_rejected() {
545        let tensor_info = create_test_tensor_info("test", vec![1], TensorType::F32);
546        let mut reader = TensorReader::new(Cursor::new(vec![0u8; 4]));
547        let options =
548            TensorReadOptions { validate_alignment: false, buffer_size: 0, ..Default::default() };
549        assert!(reader.read_tensor_data_with_options(&tensor_info, &options).is_err());
550
551        let mut reader = TensorReader::new(Cursor::new(vec![0u8; 4]));
552        assert!(reader.read_tensor_chunked(&tensor_info, 0, |_| Ok(())).is_err());
553    }
554
555    #[test]
556    fn test_unimplemented_decompression_is_reported() {
557        let tensor_info = create_test_tensor_info("test", vec![32], TensorType::Q4_0);
558        let mut reader = TensorReader::new(Cursor::new(vec![0u8; 18]));
559        let options = TensorReadOptions {
560            validate_alignment: false,
561            decompress_quantized: true,
562            ..Default::default()
563        };
564        assert!(matches!(
565            reader.read_tensor_data_with_options(&tensor_info, &options),
566            Err(GGUFError::FeatureUnavailable(_))
567        ));
568        assert_eq!(reader.position(), 0);
569    }
570
571    #[test]
572    fn invalid_alignment_is_rejected_before_reading() {
573        let mut tensor_info = create_test_tensor_info("test", vec![1], TensorType::F32);
574        tensor_info.data_offset = 1;
575        let mut reader = TensorReader::new(Cursor::new(vec![0u8; 4]));
576
577        assert!(reader.read_tensor_data(&tensor_info).is_err());
578        assert_eq!(reader.position(), 0);
579    }
580
581    #[test]
582    fn test_size_limit() {
583        let data = vec![0u8; 16];
584        let cursor = Cursor::new(data);
585        let mut reader = TensorReader::new(cursor);
586
587        let tensor_info = create_test_tensor_info("test", vec![4], TensorType::F32);
588        let options = TensorReadOptions {
589            max_tensor_size: 8, // Smaller than tensor size (16)
590            ..Default::default()
591        };
592
593        let result = reader.read_tensor_data_with_options(&tensor_info, &options);
594        assert!(result.is_err());
595    }
596
597    #[test]
598    fn test_tensor_read_utils() {
599        let small_tensor = create_test_tensor_info("small", vec![10], TensorType::F32);
600        let large_tensor = create_test_tensor_info("large", vec![100000], TensorType::F32);
601
602        let small_buffer = TensorReadUtils::optimal_buffer_size(&small_tensor);
603        let large_buffer = TensorReadUtils::optimal_buffer_size(&large_tensor);
604
605        assert!(small_buffer <= large_buffer);
606
607        assert!(!TensorReadUtils::should_read_chunked(&small_tensor, 1000));
608        assert!(TensorReadUtils::should_read_chunked(&large_tensor, 1000));
609    }
610
611    #[test]
612    fn test_memory_requirements() {
613        let tensor1 = create_test_tensor_info("t1", vec![100], TensorType::F32);
614        let tensor2 = create_test_tensor_info("t2", vec![200], TensorType::Q4_0);
615        let tensors = vec![&tensor1, &tensor2];
616
617        let req = TensorReadUtils::calculate_memory_requirements(&tensors);
618        assert_eq!(req.tensor_count, 2);
619        assert_eq!(req.quantized_tensor_count, 1);
620        assert_eq!(req.non_quantized_tensor_count, 1);
621        assert!(req.total_size > 0);
622        assert!(req.max_tensor_size >= req.average_tensor_size());
623    }
624
625    #[test]
626    fn test_seeking_reader() {
627        let data = vec![0u8; 32];
628        let cursor = Cursor::new(data);
629        let mut reader = TensorReader::with_seek(cursor);
630
631        // Seek to position 16
632        let pos = reader.seek(SeekFrom::Start(16)).unwrap();
633        assert_eq!(pos, 16);
634        assert_eq!(reader.position(), 16);
635    }
636
637    #[test]
638    fn test_skip_bytes() {
639        let data = vec![0u8; 32];
640        let cursor = Cursor::new(data);
641        let mut reader = TensorReader::new(cursor);
642
643        reader.skip_bytes(10).unwrap();
644        assert_eq!(reader.position(), 10);
645
646        reader.skip_bytes(5).unwrap();
647        assert_eq!(reader.position(), 15);
648    }
649
650    #[test]
651    fn test_display_implementations() {
652        let result = TensorReadResult {
653            data: TensorData::new_owned(vec![1, 2, 3, 4]),
654            bytes_read: 4,
655            was_decompressed: false,
656            checksum: Some(0x12345678),
657        };
658
659        let display_str = format!("{}", result);
660        assert!(display_str.contains("4"));
661        assert!(display_str.contains("0x12345678"));
662
663        let req = TensorMemoryRequirements {
664            total_size: 1000,
665            max_tensor_size: 500,
666            tensor_count: 2,
667            quantized_tensor_count: 1,
668            non_quantized_tensor_count: 1,
669            recommended_buffer_size: 256,
670        };
671
672        let req_str = format!("{}", req);
673        assert!(req_str.contains("1000"));
674        assert!(req_str.contains("500"));
675    }
676
677    #[test]
678    fn test_memory_requirements_reasonable() {
679        let req = TensorMemoryRequirements {
680            total_size: 800,
681            max_tensor_size: 400,
682            tensor_count: 2,
683            quantized_tensor_count: 1,
684            non_quantized_tensor_count: 1,
685            recommended_buffer_size: 256,
686        };
687
688        assert!(req.is_reasonable(1000)); // 800 < 80% of 1000
689        assert!(!req.is_reasonable(900)); // 800 >= 80% of 900
690        assert_eq!(req.average_tensor_size(), 400); // 800 / 2
691    }
692}