use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use candle_core::{DType, Device, Tensor};
use crate::hct::{HctError, HctLoader, HctMetadata};
#[cfg(feature = "haagenti-gpu")]
use haagenti_cuda::decompress::{DecompressConfig, GpuDecompressor};
fn dtype_size(dtype: DType) -> u64 {
match dtype {
DType::F32 | DType::U32 => 4,
DType::F64 | DType::I64 => 8,
DType::F16 | DType::BF16 => 2,
DType::U8 => 1,
_ => 4,
}
}
#[derive(Debug)]
pub struct MemoryBudget {
pub max_bytes: u64,
current_bytes: AtomicU64,
pub warning_threshold: f32,
}
impl Clone for MemoryBudget {
fn clone(&self) -> Self {
Self {
max_bytes: self.max_bytes,
current_bytes: AtomicU64::new(self.current_bytes.load(Ordering::Relaxed)),
warning_threshold: self.warning_threshold,
}
}
}
impl MemoryBudget {
pub fn new(max_bytes: u64) -> Self {
Self {
max_bytes,
current_bytes: AtomicU64::new(0),
warning_threshold: 0.85,
}
}
pub fn unlimited() -> Self {
Self::new(u64::MAX)
}
pub fn current_usage(&self) -> u64 {
self.current_bytes.load(Ordering::Relaxed)
}
pub fn remaining(&self) -> u64 {
self.max_bytes.saturating_sub(self.current_usage())
}
pub fn can_allocate(&self, bytes: u64) -> bool {
self.current_usage() + bytes <= self.max_bytes
}
pub fn allocate(&self, bytes: u64) {
self.current_bytes.fetch_add(bytes, Ordering::Relaxed);
}
pub fn deallocate(&self, bytes: u64) {
self.current_bytes.fetch_sub(bytes, Ordering::Relaxed);
}
pub fn is_warning(&self) -> bool {
let usage_ratio = self.current_usage() as f64 / self.max_bytes as f64;
usage_ratio >= self.warning_threshold as f64
}
}
impl Default for MemoryBudget {
fn default() -> Self {
Self::new(64 * 1024 * 1024 * 1024)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FallbackStrategy {
Skip,
InitializeDefault,
Fail,
}
impl Default for FallbackStrategy {
fn default() -> Self {
Self::InitializeDefault
}
}
#[derive(Debug, Clone)]
pub struct SequentialLoadConfig {
pub memory_budget: MemoryBudget,
pub device: Device,
pub dtype: DType,
pub fallback_strategy: FallbackStrategy,
pub min_quality: f32,
}
impl Default for SequentialLoadConfig {
fn default() -> Self {
Self {
memory_budget: MemoryBudget::default(),
device: Device::Cpu,
dtype: DType::F32,
fallback_strategy: FallbackStrategy::default(),
min_quality: 0.7,
}
}
}
#[derive(Debug)]
pub struct LoadedTensor {
pub name: String,
pub tensor: Tensor,
pub metadata: HctMetadata,
pub recovered: bool,
pub memory_bytes: u64,
}
#[derive(Debug, Clone)]
pub struct LoadProgress {
pub total_files: usize,
pub loaded_files: usize,
pub skipped_files: usize,
pub recovered_files: usize,
pub memory_used: u64,
pub memory_budget: u64,
}
pub struct SequentialHctLoader {
config: SequentialLoadConfig,
files: Vec<PathBuf>,
current_index: usize,
skipped_count: usize,
recovered_count: usize,
}
impl SequentialHctLoader {
pub fn new(dir: impl AsRef<Path>, config: SequentialLoadConfig) -> Result<Self, HctError> {
let dir = dir.as_ref();
let mut files: Vec<_> = std::fs::read_dir(dir)
.map_err(|e| HctError::Io {
path: dir.to_path_buf(),
message: e.to_string(),
})?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "hct"))
.map(|entry| entry.path())
.collect();
files.sort();
tracing::info!(
count = files.len(),
directory = %dir.display(),
memory_budget = %config.memory_budget.max_bytes,
"Sequential HCT loader initialized"
);
Ok(Self {
config,
files,
current_index: 0,
skipped_count: 0,
recovered_count: 0,
})
}
pub fn total_files(&self) -> usize {
self.files.len()
}
pub fn progress(&self) -> LoadProgress {
LoadProgress {
total_files: self.files.len(),
loaded_files: self.current_index,
skipped_files: self.skipped_count,
recovered_files: self.recovered_count,
memory_used: self.config.memory_budget.current_usage(),
memory_budget: self.config.memory_budget.max_bytes,
}
}
pub fn next_tensor(&mut self) -> Option<Result<LoadedTensor, HctError>> {
while self.current_index < self.files.len() {
let path = &self.files[self.current_index];
self.current_index += 1;
match self.load_single_file(path) {
Ok(Some(tensor)) => return Some(Ok(tensor)),
Ok(None) => {
self.skipped_count += 1;
continue;
},
Err(e) => match self.config.fallback_strategy {
FallbackStrategy::Fail => return Some(Err(e)),
FallbackStrategy::Skip => {
tracing::warn!(
path = %path.display(),
error = %e,
"Skipping corrupted file"
);
self.skipped_count += 1;
continue;
},
FallbackStrategy::InitializeDefault => match self.recover_tensor(path, &e) {
Ok(tensor) => {
self.recovered_count += 1;
return Some(Ok(tensor));
},
Err(recover_err) => {
tracing::warn!(
path = %path.display(),
original_error = %e,
recover_error = %recover_err,
"Failed to recover tensor, skipping"
);
self.skipped_count += 1;
continue;
},
},
},
}
}
None
}
pub fn load_all(mut self) -> Result<HashMap<String, Tensor>, HctError> {
let mut tensors = HashMap::new();
while let Some(result) = self.next_tensor() {
let loaded = result?;
tensors.insert(loaded.name, loaded.tensor);
}
Ok(tensors)
}
fn load_single_file(&self, path: &Path) -> Result<Option<LoadedTensor>, HctError> {
let loader = HctLoader::from_file(path)?;
let metadata = loader.metadata().clone();
let estimated_bytes = self.estimate_tensor_bytes(&metadata);
if !self.config.memory_budget.can_allocate(estimated_bytes) {
tracing::warn!(
path = %path.display(),
estimated_bytes = estimated_bytes,
remaining = self.config.memory_budget.remaining(),
"Insufficient memory budget for tensor"
);
return Err(HctError::Tensor {
message: format!(
"Memory budget exceeded: need {} bytes, have {} remaining",
estimated_bytes,
self.config.memory_budget.remaining()
),
});
}
let tensor = loader.to_tensor(&self.config.device, Some(self.config.dtype))?;
let actual_bytes = self.tensor_bytes(&tensor);
self.config.memory_budget.allocate(actual_bytes);
let name = crate::hct::filename_to_tensor_name(&metadata.name);
Ok(Some(LoadedTensor {
name,
tensor,
metadata,
recovered: false,
memory_bytes: actual_bytes,
}))
}
fn recover_tensor(
&self,
path: &Path,
_original_error: &HctError,
) -> Result<LoadedTensor, HctError> {
let loader_result = HctLoader::from_file(path);
let (name, shape, dtype) = match loader_result {
Ok(loader) => {
let metadata = loader.metadata();
let name = crate::hct::filename_to_tensor_name(&metadata.name);
let shape: Vec<usize> = metadata.shape.iter().map(|&d| d as usize).collect();
(name, shape, self.config.dtype)
},
Err(_) => {
let filename = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let name = crate::hct::filename_to_tensor_name(filename);
let shape = self.infer_default_shape(&name);
(name, shape, self.config.dtype)
},
};
let tensor = self.create_default_tensor(&name, &shape, dtype)?;
let actual_bytes = self.tensor_bytes(&tensor);
self.config.memory_budget.allocate(actual_bytes);
tracing::warn!(
name = %name,
shape = ?shape,
"Recovered tensor with default values"
);
Ok(LoadedTensor {
name: name.clone(),
tensor,
metadata: HctMetadata {
name,
original_size: 0,
compressed_size: 0,
compression_ratio: 0.0,
dtype: haagenti::tensor::DType::F32,
shape: shape.iter().map(|&d| d as u64).collect(),
algorithm: haagenti::tensor::CompressionAlgorithm::Lz4,
num_blocks: 0,
flags: 0,
is_holographic: false,
},
recovered: true,
memory_bytes: actual_bytes,
})
}
fn estimate_tensor_bytes(&self, metadata: &HctMetadata) -> u64 {
let elements: u64 = metadata.shape.iter().product();
let bytes_per_element = dtype_size(self.config.dtype);
elements * bytes_per_element
}
fn tensor_bytes(&self, tensor: &Tensor) -> u64 {
let elements = tensor.elem_count() as u64;
let bytes_per_element = dtype_size(tensor.dtype());
elements * bytes_per_element
}
fn infer_default_shape(&self, name: &str) -> Vec<usize> {
if name.contains("layernorm") || name.contains("norm") {
vec![16384]
} else if name.contains("scale") {
vec![1]
} else if name.contains("bias") {
vec![16384]
} else {
vec![1]
}
}
fn create_default_tensor(
&self,
name: &str,
shape: &[usize],
dtype: DType,
) -> Result<Tensor, HctError> {
let tensor =
if name.contains("layernorm") || name.contains("norm") || name.contains("scale") {
Tensor::ones(shape, dtype, &self.config.device)
} else if name.contains("bias") {
Tensor::zeros(shape, dtype, &self.config.device)
} else {
Tensor::zeros(shape, dtype, &self.config.device)
}
.map_err(|e| HctError::Tensor {
message: format!("Failed to create default tensor: {}", e),
})?;
Ok(tensor)
}
}
pub fn load_hct_directory_sequential(
dir: impl AsRef<Path>,
device: &Device,
dtype: DType,
) -> Result<HashMap<String, Tensor>, HctError> {
let config = SequentialLoadConfig {
device: device.clone(),
dtype,
..Default::default()
};
let loader = SequentialHctLoader::new(dir, config)?;
loader.load_all()
}
pub fn load_hct_directory_sequential_budgeted(
dir: impl AsRef<Path>,
device: &Device,
dtype: DType,
max_memory_bytes: u64,
) -> Result<HashMap<String, Tensor>, HctError> {
let config = SequentialLoadConfig {
device: device.clone(),
dtype,
memory_budget: MemoryBudget::new(max_memory_bytes),
..Default::default()
};
let loader = SequentialHctLoader::new(dir, config)?;
loader.load_all()
}
pub fn load_hct_directory_parallel(
dir: impl AsRef<Path>,
device: &Device,
dtype: DType,
) -> Result<HashMap<String, Tensor>, HctError> {
use rayon::prelude::*;
let dir = dir.as_ref();
let files: Vec<PathBuf> = std::fs::read_dir(dir)
.map_err(|e| HctError::Io {
path: dir.to_path_buf(),
message: e.to_string(),
})?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "hct"))
.map(|entry| entry.path())
.collect();
let total = files.len();
tracing::info!(
count = total,
directory = %dir.display(),
"Loading HCT files in parallel"
);
let results: Vec<Result<(String, Tensor), HctError>> = files
.par_iter()
.map(|path| {
let loader = HctLoader::from_file(path)?;
let name = crate::hct::filename_to_tensor_name(&loader.metadata().name);
let tensor = loader.to_tensor(device, Some(dtype))?;
Ok((name, tensor))
})
.collect();
let mut tensors = HashMap::with_capacity(total);
for result in results {
let (name, tensor) = result?;
tensors.insert(name, tensor);
}
tracing::info!(loaded = tensors.len(), "Parallel loading complete");
Ok(tensors)
}
#[cfg(feature = "haagenti-gpu")]
pub fn load_hct_directory_gpu(
dir: impl AsRef<Path>,
device: &Device,
dtype: DType,
gpu_device_id: usize,
) -> Result<HashMap<String, Tensor>, HctError> {
use std::time::Instant;
let dir = dir.as_ref();
let start = Instant::now();
let files: Vec<PathBuf> = std::fs::read_dir(dir)
.map_err(|e| HctError::Io {
path: dir.to_path_buf(),
message: e.to_string(),
})?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "hct"))
.map(|entry| entry.path())
.collect();
let total = files.len();
tracing::info!(
count = total,
directory = %dir.display(),
gpu_device = gpu_device_id,
"Loading HCT files with GPU decompression"
);
let config = DecompressConfig {
device_id: gpu_device_id,
verify_checksums: false, output_f16: dtype == DType::F16,
};
let mut decompressor = GpuDecompressor::with_config(config).map_err(|e| HctError::Format {
message: format!("Failed to create GPU decompressor: {}", e),
})?;
let mut tensors = HashMap::with_capacity(total);
let mut total_input_bytes = 0usize;
let mut total_output_bytes = 0usize;
for path in &files {
let compressed = std::fs::read(path).map_err(|e| HctError::Io {
path: path.clone(),
message: e.to_string(),
})?;
total_input_bytes += compressed.len();
let loader = HctLoader::from_file(path)?;
let metadata = loader.metadata();
let name = crate::hct::filename_to_tensor_name(&metadata.name);
let shape: Vec<usize> = metadata.shape.iter().map(|&d| d as usize).collect();
let decompressed =
decompressor
.decompress(&compressed, &shape)
.map_err(|e| HctError::Format {
message: format!("GPU decompression failed for {}: {}", name, e),
})?;
total_output_bytes += decompressed.len() * 4;
let tensor =
Tensor::from_vec(decompressed, shape.as_slice(), &Device::Cpu).map_err(|e| {
HctError::Tensor {
message: format!("Failed to create tensor from GPU output: {}", e),
}
})?;
let tensor = tensor
.to_dtype(dtype)
.map_err(|e| HctError::Tensor {
message: format!("Failed to convert dtype: {}", e),
})?
.to_device(device)
.map_err(|e| HctError::Tensor {
message: format!("Failed to move to device: {}", e),
})?;
tensors.insert(name, tensor);
}
let elapsed = start.elapsed();
let throughput_mbps = total_output_bytes as f64 / elapsed.as_secs_f64() / 1_000_000.0;
tracing::info!(
loaded = tensors.len(),
input_mb = total_input_bytes as f64 / 1_000_000.0,
output_mb = total_output_bytes as f64 / 1_000_000.0,
ratio = total_output_bytes as f64 / total_input_bytes.max(1) as f64,
elapsed_ms = elapsed.as_millis(),
throughput_mbps = throughput_mbps,
"GPU decompression complete"
);
Ok(tensors)
}
#[cfg(feature = "haagenti-gpu")]
pub fn load_hct_directory_gpu_with_stats(
dir: impl AsRef<Path>,
device: &Device,
dtype: DType,
gpu_device_id: usize,
) -> Result<(HashMap<String, Tensor>, GpuDecompressStats), HctError> {
use std::time::Instant;
let dir = dir.as_ref();
let start = Instant::now();
let files: Vec<PathBuf> = std::fs::read_dir(dir)
.map_err(|e| HctError::Io {
path: dir.to_path_buf(),
message: e.to_string(),
})?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "hct"))
.map(|entry| entry.path())
.collect();
let total = files.len();
let config = DecompressConfig {
device_id: gpu_device_id,
verify_checksums: false,
output_f16: dtype == DType::F16,
};
let mut decompressor = GpuDecompressor::with_config(config).map_err(|e| HctError::Format {
message: format!("Failed to create GPU decompressor: {}", e),
})?;
let mut tensors = HashMap::with_capacity(total);
let mut stats = GpuDecompressStats::default();
for path in &files {
let compressed = std::fs::read(path).map_err(|e| HctError::Io {
path: path.clone(),
message: e.to_string(),
})?;
stats.total_input_bytes += compressed.len();
let loader = HctLoader::from_file(path)?;
let metadata = loader.metadata();
let name = crate::hct::filename_to_tensor_name(&metadata.name);
let shape: Vec<usize> = metadata.shape.iter().map(|&d| d as usize).collect();
let decompress_start = Instant::now();
let decompressed =
decompressor
.decompress(&compressed, &shape)
.map_err(|e| HctError::Format {
message: format!("GPU decompression failed for {}: {}", name, e),
})?;
stats.decompress_time_ms += decompress_start.elapsed().as_secs_f64() * 1000.0;
stats.total_output_bytes += decompressed.len() * 4;
let tensor = Tensor::from_vec(decompressed, shape.as_slice(), &Device::Cpu)
.map_err(|e| HctError::Tensor {
message: format!("Failed to create tensor: {}", e),
})?
.to_dtype(dtype)
.map_err(|e| HctError::Tensor {
message: format!("Failed to convert dtype: {}", e),
})?
.to_device(device)
.map_err(|e| HctError::Tensor {
message: format!("Failed to move to device: {}", e),
})?;
tensors.insert(name, tensor);
stats.num_tensors += 1;
}
stats.total_time_ms = start.elapsed().as_secs_f64() * 1000.0;
stats.throughput_mbps = stats.total_output_bytes as f64 / stats.total_time_ms / 1000.0;
Ok((tensors, stats))
}
#[cfg(feature = "haagenti-gpu")]
#[derive(Debug, Clone, Default)]
pub struct GpuDecompressStats {
pub num_tensors: usize,
pub total_input_bytes: usize,
pub total_output_bytes: usize,
pub decompress_time_ms: f64,
pub total_time_ms: f64,
pub throughput_mbps: f64,
}
#[cfg(feature = "haagenti-gpu")]
impl GpuDecompressStats {
pub fn compression_ratio(&self) -> f32 {
if self.total_input_bytes == 0 {
0.0
} else {
self.total_output_bytes as f32 / self.total_input_bytes as f32
}
}
pub fn summary(&self) -> String {
format!(
"{} tensors, {:.1} MB -> {:.1} MB ({:.1}x), {:.1}ms total ({:.1}ms decompress), {:.1} MB/s",
self.num_tensors,
self.total_input_bytes as f64 / 1_000_000.0,
self.total_output_bytes as f64 / 1_000_000.0,
self.compression_ratio(),
self.total_time_ms,
self.decompress_time_ms,
self.throughput_mbps,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::TempDir;
fn create_test_hct_file(dir: &Path, name: &str, shape: &[u64]) -> PathBuf {
use haagenti::tensor::{CompressionAlgorithm, DType as HctDType, HctWriter};
use haagenti::Lz4Compressor;
let path = dir.join(format!("{}.hct", name));
let file = fs::File::create(&path).expect("create file");
let elements: u64 = shape.iter().product();
let data: Vec<f32> = (0..elements).map(|i| i as f32 * 0.001).collect();
let bytes: Vec<u8> = data.iter().flat_map(|f| f.to_le_bytes()).collect();
let mut writer = HctWriter::new(
file,
CompressionAlgorithm::Lz4,
HctDType::F32,
shape.to_vec(),
)
.with_block_size(64 * 1024);
let compressor = Lz4Compressor::new();
writer
.compress_data(&bytes, &compressor)
.expect("write data");
writer.finish().expect("finish");
path
}
fn create_truncated_hct_file(dir: &Path, name: &str) -> PathBuf {
let path = dir.join(format!("{}.hct", name));
let mut file = fs::File::create(&path).expect("create file");
file.write_all(b"HCTN\x00\x00\x00\x00")
.expect("write truncated file");
path
}
#[test]
fn test_memory_budget_tracking() {
let budget = MemoryBudget::new(1024);
assert_eq!(budget.current_usage(), 0);
assert_eq!(budget.remaining(), 1024);
assert!(budget.can_allocate(512));
assert!(budget.can_allocate(1024));
assert!(!budget.can_allocate(1025));
budget.allocate(512);
assert_eq!(budget.current_usage(), 512);
assert_eq!(budget.remaining(), 512);
assert!(budget.can_allocate(512));
assert!(!budget.can_allocate(513));
budget.deallocate(256);
assert_eq!(budget.current_usage(), 256);
assert_eq!(budget.remaining(), 768);
}
#[test]
fn test_memory_budget_warning_threshold() {
let budget = MemoryBudget::new(1000);
assert!(!budget.is_warning());
budget.allocate(840);
assert!(!budget.is_warning());
budget.allocate(20); assert!(budget.is_warning()); }
#[test]
fn test_sequential_loader_respects_memory_budget() {
let temp_dir = TempDir::new().expect("create temp dir");
create_test_hct_file(temp_dir.path(), "tensor_a", &[100]);
create_test_hct_file(temp_dir.path(), "tensor_b", &[100]);
let config = SequentialLoadConfig {
memory_budget: MemoryBudget::new(100), device: Device::Cpu,
dtype: DType::F32,
fallback_strategy: FallbackStrategy::Fail,
min_quality: 0.7,
};
let mut loader = SequentialHctLoader::new(temp_dir.path(), config).expect("create loader");
let result = loader.next_tensor();
assert!(result.is_some());
let err = result.unwrap();
assert!(err.is_err(), "Should fail due to memory budget");
}
#[test]
fn test_sequential_loader_yields_tensors_one_at_a_time() {
let temp_dir = TempDir::new().expect("create temp dir");
create_test_hct_file(temp_dir.path(), "tensor_a", &[16]);
create_test_hct_file(temp_dir.path(), "tensor_b", &[16]);
create_test_hct_file(temp_dir.path(), "tensor_c", &[16]);
let config = SequentialLoadConfig {
memory_budget: MemoryBudget::unlimited(),
device: Device::Cpu,
dtype: DType::F32,
fallback_strategy: FallbackStrategy::Fail,
min_quality: 0.7,
};
let mut loader = SequentialHctLoader::new(temp_dir.path(), config).expect("create loader");
assert_eq!(loader.total_files(), 3);
let mut count = 0;
while let Some(result) = loader.next_tensor() {
let loaded = result.expect("load tensor");
count += 1;
let progress = loader.progress();
assert_eq!(progress.loaded_files, count);
}
assert_eq!(count, 3);
}
#[test]
fn test_sequential_loader_handles_corrupted_files_skip() {
let temp_dir = TempDir::new().expect("create temp dir");
create_test_hct_file(temp_dir.path(), "tensor_valid", &[16]);
create_truncated_hct_file(temp_dir.path(), "tensor_corrupt");
let config = SequentialLoadConfig {
memory_budget: MemoryBudget::unlimited(),
device: Device::Cpu,
dtype: DType::F32,
fallback_strategy: FallbackStrategy::Skip,
min_quality: 0.7,
};
let mut loader = SequentialHctLoader::new(temp_dir.path(), config).expect("create loader");
assert_eq!(loader.total_files(), 2);
let mut loaded_count = 0;
while let Some(result) = loader.next_tensor() {
if result.is_ok() {
loaded_count += 1;
}
}
let progress = loader.progress();
assert!(progress.loaded_files >= 1);
assert!(progress.skipped_files >= 1);
}
#[test]
fn test_sequential_loader_handles_corrupted_files_recover() {
let temp_dir = TempDir::new().expect("create temp dir");
create_truncated_hct_file(temp_dir.path(), "model_layers_0_input_layernorm_weight");
let config = SequentialLoadConfig {
memory_budget: MemoryBudget::unlimited(),
device: Device::Cpu,
dtype: DType::F32,
fallback_strategy: FallbackStrategy::InitializeDefault,
min_quality: 0.7,
};
let mut loader = SequentialHctLoader::new(temp_dir.path(), config).expect("create loader");
let result = loader.next_tensor();
assert!(result.is_some());
let loaded = result.unwrap().expect("should recover");
assert!(loaded.recovered);
assert!(loaded.name.contains("layernorm"));
let sum = loaded
.tensor
.sum_all()
.expect("sum")
.to_scalar::<f32>()
.expect("scalar");
let count = loaded.tensor.elem_count() as f32;
assert!(
(sum - count).abs() < 0.01,
"Layernorm should be ones, sum={}, count={}",
sum,
count
);
}
#[test]
fn test_sequential_loader_initializes_bias_to_zeros() {
let temp_dir = TempDir::new().expect("create temp dir");
create_truncated_hct_file(temp_dir.path(), "model_layers_0_self_attn_o_proj_bias");
let config = SequentialLoadConfig {
memory_budget: MemoryBudget::unlimited(),
device: Device::Cpu,
dtype: DType::F32,
fallback_strategy: FallbackStrategy::InitializeDefault,
min_quality: 0.7,
};
let mut loader = SequentialHctLoader::new(temp_dir.path(), config).expect("create loader");
let result = loader.next_tensor();
assert!(result.is_some());
let loaded = result.unwrap().expect("should recover");
assert!(loaded.recovered);
assert!(loaded.name.contains("bias"));
let sum = loaded
.tensor
.sum_all()
.expect("sum")
.to_scalar::<f32>()
.expect("scalar");
assert!((sum).abs() < 0.01, "Bias should be zeros, sum={}", sum);
}
#[test]
fn test_load_all_collects_tensors() {
let temp_dir = TempDir::new().expect("create temp dir");
create_test_hct_file(temp_dir.path(), "tensor_a", &[8]);
create_test_hct_file(temp_dir.path(), "tensor_b", &[8]);
let config = SequentialLoadConfig {
memory_budget: MemoryBudget::unlimited(),
device: Device::Cpu,
dtype: DType::F32,
fallback_strategy: FallbackStrategy::Fail,
min_quality: 0.7,
};
let loader = SequentialHctLoader::new(temp_dir.path(), config).expect("create loader");
let tensors = loader.load_all().expect("load all");
assert_eq!(tensors.len(), 2);
assert!(tensors.contains_key("tensor.a") || tensors.contains_key("tensor_a"));
}
#[test]
fn test_progress_tracking() {
let temp_dir = TempDir::new().expect("create temp dir");
create_test_hct_file(temp_dir.path(), "t1", &[4]);
create_test_hct_file(temp_dir.path(), "t2", &[4]);
create_truncated_hct_file(temp_dir.path(), "t3_corrupt");
let config = SequentialLoadConfig {
memory_budget: MemoryBudget::unlimited(),
device: Device::Cpu,
dtype: DType::F32,
fallback_strategy: FallbackStrategy::Skip,
min_quality: 0.7,
};
let mut loader = SequentialHctLoader::new(temp_dir.path(), config).expect("create loader");
let initial = loader.progress();
assert_eq!(initial.total_files, 3);
assert_eq!(initial.loaded_files, 0);
while loader.next_tensor().is_some() {}
let final_progress = loader.progress();
assert_eq!(final_progress.total_files, 3);
assert_eq!(final_progress.loaded_files, 3);
assert!(final_progress.skipped_files >= 1);
}
}