Files
foxhunt/ml/tests/quantized_checkpoint_test.rs
jgrusewski e166a4fc02 Wave 3: Update LOW RISK test files (225→54 features)
- Updated 73 test files across 10 categories
- Total 557 replacements (225 → 54)
- DQN tests: 252/262 passing (9 failures - slice index blocker)
- TFT tests: 98/98 passing
- MAMBA-2 tests: 11/11 passing
- Hyperopt tests: 98/98 passing

Critical findings:
- Blocker: ml/src/trainers/dqn.rs:3444 hardcoded slice indices
- Architecture mismatch: extract_current_features() vs extract_current_features_v2()

Wave 3 Agent breakdown:
- Agent 1: DQN test files (12 files)
- Agent 2: PPO test files (2 files)
- Agent 3: TFT test files (6 files)
- Agent 4: MAMBA-2 test files (2 files)
- Agent 5: Feature extraction tests (3 files)
- Agent 6: Integration test files (9 files)
- Agent 7: Data loader test files (3 files)
- Agent 8: Hyperopt test files (1 file)
- Agent 9: Benchmark test files (9 files)
- Agent 10: Utility & misc test files (73 files)

Next: Fix slice index blocker, then Wave 4 (OFI integration 46→54)
2025-11-23 01:22:32 +01:00

415 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Integration tests for quantized checkpoint save/load functionality
//!
//! Tests:
//! 1. Round-trip save/load preserves weights and metadata
//! 2. Compression reduces file size without data loss
//! 3. Backward compatibility with FP32 checkpoints
//! 4. File size validation (<100MB target)
//! 5. Multi-layer models with different shapes
use candle_core::{Device, Tensor};
use ml::checkpoint::{
calculate_compression_ratio, load_quantized_checkpoint, save_quantized_checkpoint,
QuantizedCheckpointMetadata, QuantizedWeight,
};
use ml::MLError;
use std::collections::HashMap;
use tempfile::{NamedTempFile, TempDir};
/// Test basic save/load round-trip
#[test]
fn test_quantized_checkpoint_round_trip() -> Result<(), MLError> {
let device = Device::Cpu;
let mut weights = HashMap::new();
// Create test quantized weight
let data = Tensor::from_vec(vec![0u8, 64, 128, 192, 255], 5, &device)
.map_err(|e| MLError::ModelError(format!("Tensor creation failed: {}", e)))?;
weights.insert(
"fc1.weight".to_string(),
QuantizedWeight {
data,
scale: 0.01,
zero_point: 127,
shape: vec![5],
},
);
// Save checkpoint
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
let metadata = QuantizedCheckpointMetadata {
model_type: "DQN".to_string(),
version: "1.0.0".to_string(),
quantization_method: "symmetric".to_string(),
..Default::default()
};
save_quantized_checkpoint(path, &weights, Some(metadata.clone()), false)?;
// Load checkpoint
let (loaded_weights, loaded_metadata) = load_quantized_checkpoint(path)?;
// Validate metadata
assert_eq!(loaded_metadata.model_type, "DQN");
assert_eq!(loaded_metadata.version, "1.0.0");
assert_eq!(loaded_metadata.num_layers, 1);
// Validate weights
assert_eq!(loaded_weights.len(), 1);
let loaded_weight = loaded_weights.get("fc1.weight").unwrap();
assert_eq!(loaded_weight.scale, 0.01);
assert_eq!(loaded_weight.zero_point, 127);
assert_eq!(loaded_weight.shape, vec![5]);
// Validate tensor data
let loaded_data = loaded_weight
.data
.flatten_all()
.unwrap()
.to_vec1::<u8>()
.unwrap();
assert_eq!(loaded_data, vec![0u8, 64, 128, 192, 255]);
Ok(())
}
/// Test multi-layer model checkpoint
#[test]
fn test_multi_layer_checkpoint() -> Result<(), MLError> {
let device = Device::Cpu;
let mut weights = HashMap::new();
// Layer 1: 1D weight vector
let fc1_data = Tensor::from_vec(vec![100u8; 128], &[128], &device).unwrap();
weights.insert(
"fc1.weight".to_string(),
QuantizedWeight {
data: fc1_data,
scale: 0.02,
zero_point: 127,
shape: vec![128],
},
);
// Layer 2: 2D weight matrix
let fc2_data = Tensor::from_vec(vec![150u8; 256], &[64, 4], &device).unwrap();
weights.insert(
"fc2.weight".to_string(),
QuantizedWeight {
data: fc2_data,
scale: 0.015,
zero_point: 100,
shape: vec![64, 4],
},
);
// Layer 3: Bias vector
let bias_data = Tensor::from_vec(vec![127u8; 64], &[64], &device).unwrap();
weights.insert(
"fc2.bias".to_string(),
QuantizedWeight {
data: bias_data,
scale: 0.001,
zero_point: 127,
shape: vec![64],
},
);
// Save and load
let temp_file = NamedTempFile::new().unwrap();
save_quantized_checkpoint(temp_file.path(), &weights, None, false)?;
let (loaded_weights, loaded_metadata) = load_quantized_checkpoint(temp_file.path())?;
// Validate all layers loaded
assert_eq!(loaded_weights.len(), 3);
assert_eq!(loaded_metadata.num_layers, 3);
// Validate individual layers
assert!(loaded_weights.contains_key("fc1.weight"));
assert!(loaded_weights.contains_key("fc2.weight"));
assert!(loaded_weights.contains_key("fc2.bias"));
// Validate shapes
assert_eq!(loaded_weights.get("fc1.weight").unwrap().shape, vec![128]);
assert_eq!(loaded_weights.get("fc2.weight").unwrap().shape, vec![64, 4]);
assert_eq!(loaded_weights.get("fc2.bias").unwrap().shape, vec![64]);
Ok(())
}
/// Test gzip compression
#[test]
fn test_gzip_compression() -> Result<(), MLError> {
let device = Device::Cpu;
let mut weights = HashMap::new();
// Create large repetitive data (compresses well)
let data = Tensor::from_vec(vec![42u8; 50_000], &[50_000], &device).unwrap();
weights.insert(
"large_layer".to_string(),
QuantizedWeight {
data,
scale: 1.0,
zero_point: 127,
shape: vec![50_000],
},
);
let temp_dir = TempDir::new().unwrap();
let compressed_path = temp_dir.path().join("compressed.safetensors");
let uncompressed_path = temp_dir.path().join("uncompressed.safetensors");
// Save with compression
let compressed_size = save_quantized_checkpoint(&compressed_path, &weights, None, true)?;
// Save without compression
let uncompressed_size = save_quantized_checkpoint(&uncompressed_path, &weights, None, false)?;
// Compressed should be significantly smaller for repetitive data
println!(
"Compression: {}{} bytes ({:.1}% reduction)",
uncompressed_size,
compressed_size,
100.0 * (1.0 - compressed_size as f64 / uncompressed_size as f64)
);
assert!(compressed_size < uncompressed_size);
// Load compressed and verify data integrity
let (loaded_weights, _) = load_quantized_checkpoint(&compressed_path)?;
assert_eq!(loaded_weights.len(), 1);
let loaded_data = loaded_weights
.get("large_layer")
.unwrap()
.data
.flatten_all()
.unwrap()
.to_vec1::<u8>()
.unwrap();
assert_eq!(loaded_data.len(), 50_000);
assert!(loaded_data.iter().all(|&x| x == 42));
Ok(())
}
/// Test file size comparison (INT8 vs FP32)
#[test]
fn test_file_size_comparison() -> Result<(), MLError> {
let device = Device::Cpu;
let mut weights = HashMap::new();
// Simulate DQN Q-network weights (approximate sizes)
// Input layer: 54 features × 128 hidden = 28,800 params
let fc1_data = Tensor::from_vec(vec![127u8; 28_800], &[54, 128], &device).unwrap();
weights.insert(
"fc1.weight".to_string(),
QuantizedWeight {
data: fc1_data,
scale: 0.01,
zero_point: 127,
shape: vec![54, 128],
},
);
// Hidden layer: 128 × 64 = 8,192 params
let fc2_data = Tensor::from_vec(vec![127u8; 8_192], &[128, 64], &device).unwrap();
weights.insert(
"fc2.weight".to_string(),
QuantizedWeight {
data: fc2_data,
scale: 0.01,
zero_point: 127,
shape: vec![128, 64],
},
);
// Output layer: 64 × 3 = 192 params (BUY/SELL/HOLD)
let fc3_data = Tensor::from_vec(vec![127u8; 192], &[64, 3], &device).unwrap();
weights.insert(
"fc3.weight".to_string(),
QuantizedWeight {
data: fc3_data,
scale: 0.01,
zero_point: 127,
shape: vec![64, 3],
},
);
// Calculate sizes
let int8_size: usize = weights.values().map(|w| w.memory_bytes()).sum();
let fp32_size = int8_size * 4;
let compression_ratio = calculate_compression_ratio(&weights);
println!("Model size comparison:");
println!(
" FP32: {} bytes ({:.2} MB)",
fp32_size,
fp32_size as f64 / 1_048_576.0
);
println!(
" INT8: {} bytes ({:.2} MB)",
int8_size,
int8_size as f64 / 1_048_576.0
);
println!(" Ratio: {:.2}x", compression_ratio);
// Save checkpoint and verify file size
let temp_file = NamedTempFile::new().unwrap();
let file_size = save_quantized_checkpoint(temp_file.path(), &weights, None, false)?;
println!(
" File: {} bytes ({:.2} MB)",
file_size,
file_size as f64 / 1_048_576.0
);
// Validate size reduction
assert_eq!(compression_ratio, 4.0); // FP32/INT8 = 4x
assert!(file_size < fp32_size);
assert!(file_size < 1_000_000); // Should be well under 1MB for this small model
Ok(())
}
/// Test custom metadata fields
#[test]
fn test_custom_metadata() -> Result<(), MLError> {
let device = Device::Cpu;
let mut weights = HashMap::new();
let data = Tensor::from_vec(vec![127u8; 100], &[100], &device).unwrap();
weights.insert(
"test.weight".to_string(),
QuantizedWeight {
data,
scale: 1.0,
zero_point: 127,
shape: vec![100],
},
);
// Add custom metadata
let mut metadata = QuantizedCheckpointMetadata::default();
metadata.model_type = "PPO".to_string();
metadata.version = "2.1.0".to_string();
metadata
.custom
.insert("training_epochs".to_string(), serde_json::json!(50));
metadata
.custom
.insert("dataset".to_string(), serde_json::json!("ES.FUT_2024_Q4"));
metadata
.custom
.insert("accuracy".to_string(), serde_json::json!(0.95));
// Save and load
let temp_file = NamedTempFile::new().unwrap();
save_quantized_checkpoint(temp_file.path(), &weights, Some(metadata.clone()), false)?;
let (_, loaded_metadata) = load_quantized_checkpoint(temp_file.path())?;
// Validate custom fields
assert_eq!(loaded_metadata.model_type, "PPO");
assert_eq!(loaded_metadata.version, "2.1.0");
assert_eq!(
loaded_metadata.custom.get("training_epochs").unwrap(),
&serde_json::json!(50)
);
assert_eq!(
loaded_metadata.custom.get("dataset").unwrap(),
&serde_json::json!("ES.FUT_2024_Q4")
);
Ok(())
}
/// Test large model checkpoint (simulating MAMBA-2 or TFT)
#[test]
fn test_large_model_checkpoint() -> Result<(), MLError> {
let device = Device::Cpu;
let mut weights = HashMap::new();
// Simulate multiple large layers (total ~10MB INT8 → ~40MB FP32)
for i in 0..10 {
let data =
Tensor::from_vec(vec![(i * 25) as u8; 1_000_000], &[1_000_000], &device).unwrap();
weights.insert(
format!("layer{}.weight", i),
QuantizedWeight {
data,
scale: 0.01,
zero_point: 127,
shape: vec![1_000_000],
},
);
}
// Save checkpoint
let temp_file = NamedTempFile::new().unwrap();
let file_size = save_quantized_checkpoint(temp_file.path(), &weights, None, false)?;
// Validate file size is reasonable
println!(
"Large model checkpoint: {} bytes ({:.2} MB)",
file_size,
file_size as f64 / 1_048_576.0
);
assert!(file_size < 50_000_000); // Should be under 50MB
// Load and validate
let (loaded_weights, loaded_metadata) = load_quantized_checkpoint(temp_file.path())?;
assert_eq!(loaded_weights.len(), 10);
assert_eq!(loaded_metadata.num_layers, 10);
// Verify total size
let total_int8_size: usize = loaded_weights.values().map(|w| w.memory_bytes()).sum();
assert_eq!(total_int8_size, 10_000_000); // 10 × 1M = 10MB
Ok(())
}
/// Benchmark save/load performance
#[test]
fn test_performance_benchmark() -> Result<(), MLError> {
let device = Device::Cpu;
let mut weights = HashMap::new();
// Medium-sized model (~1MB INT8)
let data = Tensor::from_vec(vec![127u8; 1_000_000], &[1_000_000], &device).unwrap();
weights.insert(
"model.weight".to_string(),
QuantizedWeight {
data,
scale: 1.0,
zero_point: 127,
shape: vec![1_000_000],
},
);
let temp_file = NamedTempFile::new().unwrap();
// Benchmark save
let save_start = std::time::Instant::now();
save_quantized_checkpoint(temp_file.path(), &weights, None, false)?;
let save_duration = save_start.elapsed();
// Benchmark load
let load_start = std::time::Instant::now();
let _ = load_quantized_checkpoint(temp_file.path())?;
let load_duration = load_start.elapsed();
println!("Performance benchmark (1MB INT8 model):");
println!(" Save: {:?}", save_duration);
println!(" Load: {:?}", load_duration);
// Sanity checks (should be fast for 1MB)
assert!(save_duration.as_millis() < 1000); // < 1 second
assert!(load_duration.as_millis() < 1000); // < 1 second
Ok(())
}