use std::collections::HashMap;
use candle_core::{Device, Tensor};
use vsa_optim_rs::{
config::{PhaseConfig, PredictionConfig, TernaryConfig, VSAConfig},
phase::{DeterministicPhase, DeterministicPhaseConfig, DeterministicPhaseTrainer, PhaseTrainer},
prediction::GradientPredictor,
ternary::TernaryGradientAccumulator,
vsa::VSAGradientCompressor,
};
fn create_mlp_gradients(device: &Device) -> HashMap<String, Tensor> {
let mut gradients = HashMap::new();
gradients.insert(
"fc1.weight".to_string(),
Tensor::randn(0.0f32, 0.1, (64, 32), device).unwrap(),
);
gradients.insert(
"fc1.bias".to_string(),
Tensor::randn(0.0f32, 0.1, 64, device).unwrap(),
);
gradients.insert(
"fc2.weight".to_string(),
Tensor::randn(0.0f32, 0.1, (32, 64), device).unwrap(),
);
gradients.insert(
"fc2.bias".to_string(),
Tensor::randn(0.0f32, 0.1, 32, device).unwrap(),
);
gradients.insert(
"fc3.weight".to_string(),
Tensor::randn(0.0f32, 0.1, (10, 32), device).unwrap(),
);
gradients.insert(
"fc3.bias".to_string(),
Tensor::randn(0.0f32, 0.1, 10, device).unwrap(),
);
gradients
}
fn extract_shapes(gradients: &HashMap<String, Tensor>) -> Vec<(String, Vec<usize>)> {
gradients
.iter()
.map(|(name, grad)| (name.clone(), grad.dims().to_vec()))
.collect()
}
fn param_count(gradients: &HashMap<String, Tensor>) -> usize {
gradients.values().map(|g| g.elem_count()).sum()
}
#[test]
fn test_vsa_compression_mlp_gradients() {
let device = Device::Cpu;
let gradients = create_mlp_gradients(&device);
let total_params = param_count(&gradients);
let mut compressor = VSAGradientCompressor::new(
total_params,
VSAConfig::default()
.with_dimension(512)
.with_compression_ratio(0.1),
);
let (bundled, metadata) = compressor.compress(&gradients).unwrap();
let stats = compressor.get_compression_stats();
println!(
"Compressed {} params to {} dim ({:.1}% memory saved)",
stats.original_params,
stats.compressed_dim,
stats.memory_saving * 100.0
);
assert!(stats.memory_saving > 0.5);
let reconstructed = compressor.decompress(&bundled, &metadata).unwrap();
assert_eq!(reconstructed.len(), gradients.len());
for (name, orig) in &gradients {
let recon = reconstructed.get(name).unwrap();
assert_eq!(orig.dims(), recon.dims(), "Shape mismatch for {name}");
}
for (name, orig) in &gradients {
if !name.contains("weight") {
continue;
}
let recon = reconstructed.get(name).unwrap();
let orig_flat: Vec<f32> = orig.flatten_all().unwrap().to_vec1().unwrap();
let recon_flat: Vec<f32> = recon.flatten_all().unwrap().to_vec1().unwrap();
let dot: f32 = orig_flat
.iter()
.zip(recon_flat.iter())
.map(|(a, b)| a * b)
.sum();
let norm_orig: f32 = orig_flat.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_recon: f32 = recon_flat.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_orig > 1e-6 && norm_recon > 1e-6 {
let cosine = dot / (norm_orig * norm_recon + 1e-8);
println!("{name}: cosine similarity = {cosine:.3}");
assert!(
cosine > 0.0,
"Gradient direction should be preserved for {name}"
);
}
}
}
#[test]
fn test_ternary_accumulation_flow() {
let device = Device::Cpu;
let gradients = create_mlp_gradients(&device);
let shapes = extract_shapes(&gradients);
let config = TernaryConfig::default()
.with_accumulation_steps(4)
.with_stochastic_rounding(true);
let mut accumulator = TernaryGradientAccumulator::new(&shapes, config, &device).unwrap();
for _ in 0..4 {
accumulator.accumulate(&gradients).unwrap();
}
let accumulated = accumulator.get_accumulated().unwrap();
assert_eq!(accumulated.len(), gradients.len());
for (name, orig) in &gradients {
let acc = accumulated.get(name).unwrap();
assert_eq!(orig.dims(), acc.dims());
}
accumulator.reset().unwrap();
}
#[test]
fn test_gradient_prediction_cycle() {
let device = Device::Cpu;
let gradients = create_mlp_gradients(&device);
let shapes = extract_shapes(&gradients);
let config = PredictionConfig::default()
.with_history_size(3)
.with_prediction_steps(2)
.with_momentum(0.9);
let mut predictor = GradientPredictor::new(&shapes, config, &device).unwrap();
assert!(predictor.should_compute_full());
predictor.record_gradient(&gradients).unwrap();
assert!(predictor.should_compute_full());
for _ in 0..2 {
predictor.record_gradient(&gradients).unwrap();
}
if !predictor.should_compute_full() {
let predicted = predictor.predict_gradient().unwrap();
assert_eq!(predicted.len(), gradients.len());
for (name, orig) in &gradients {
let pred = predicted.get(name).unwrap();
assert_eq!(orig.dims(), pred.dims());
}
}
let stats = predictor.get_stats();
assert!(stats.history_size <= 3);
}
#[test]
fn test_phase_trainer_cycle() {
let device = Device::Cpu;
let gradients = create_mlp_gradients(&device);
let shapes = extract_shapes(&gradients);
let config = PhaseConfig::default()
.with_full_steps(2)
.with_predict_steps(4)
.with_correct_every(2);
let mut trainer = PhaseTrainer::new(&shapes, config, &device).unwrap();
for step in 0..10 {
let _step_info = trainer.begin_step().unwrap();
if trainer.should_compute_full() {
trainer.record_full_gradients(&gradients).unwrap();
} else {
let predicted = trainer.get_predicted_gradients().unwrap();
assert_eq!(predicted.len(), gradients.len());
}
let loss = 1.0 / (step as f32 + 1.0); trainer.end_step(loss).unwrap();
}
let stats = trainer.get_stats();
assert!(stats.total_steps >= 10);
assert!(stats.speedup > 0.0);
}
#[test]
fn test_combined_vsa_and_ternary() {
let device = Device::Cpu;
let gradients = create_mlp_gradients(&device);
let total_params = param_count(&gradients);
let mut compressor = VSAGradientCompressor::new(
total_params,
VSAConfig::default().with_dimension(512),
);
let (bundled, metadata) = compressor.compress(&gradients).unwrap();
let memory_per_element = 2.0 / 8.0; let compressed_bytes = bundled.len() as f32 * memory_per_element;
let original_bytes = total_params as f32 * 4.0; let compression_ratio = compressed_bytes / original_bytes;
println!(
"Combined compression: {:.1}% of original ({:.0} bytes -> {:.0} bytes)",
compression_ratio * 100.0,
original_bytes,
compressed_bytes
);
assert!(
compression_ratio < 0.3,
"Should achieve >70% compression with VSA+ternary for test sizes"
);
let reconstructed = compressor.decompress(&bundled, &metadata).unwrap();
assert_eq!(reconstructed.len(), gradients.len());
}
#[test]
fn test_deterministic_phase_trainer_full_cycle() {
let device = Device::Cpu;
let gradients = create_mlp_gradients(&device);
let shapes = extract_shapes(&gradients);
let config = DeterministicPhaseConfig::default()
.with_warmup_steps(5)
.with_full_steps(3)
.with_predict_steps(6)
.with_correct_every(3);
let mut trainer = DeterministicPhaseTrainer::new(&shapes, config, &device).unwrap();
let mut phases_seen = std::collections::HashSet::new();
let mut backward_count = 0;
let mut forward_count = 0;
for step in 0..30 {
let info = trainer.begin_step().unwrap();
phases_seen.insert(format!("{}", info.phase));
forward_count += 1;
if info.needs_backward {
backward_count += 1;
let step_grads = create_mlp_gradients(&device);
trainer.record_full_gradients(&step_grads).unwrap();
} else {
let predicted = trainer.get_predicted_gradients().unwrap();
assert_eq!(predicted.len(), gradients.len());
for (name, pred) in &predicted {
let orig_shape = gradients.get(name).unwrap().dims();
assert_eq!(pred.dims(), orig_shape, "Shape mismatch for {name}");
}
}
let loss = 1.0 / (step + 1) as f32;
trainer.end_step(loss).unwrap();
}
assert!(phases_seen.contains("WARMUP"), "Should have warmup phase");
assert!(phases_seen.contains("FULL"), "Should have full phase");
assert!(phases_seen.contains("PREDICT"), "Should have predict phase");
let stats = trainer.get_stats();
println!("Deterministic trainer stats: {}", stats);
println!(" Forward passes: {}", forward_count);
println!(" Backward passes: {}", backward_count);
println!(" Speedup: {:.2}x", stats.speedup);
assert!(
backward_count < forward_count,
"Should have fewer backward passes than forward"
);
assert!(
stats.speedup > 1.0,
"Should achieve speedup with prediction"
);
}
#[test]
fn test_deterministic_training_reproducibility() {
let device = Device::Cpu;
let gradients = create_mlp_gradients(&device);
let shapes = extract_shapes(&gradients);
let config = DeterministicPhaseConfig::default()
.with_warmup_steps(3)
.with_full_steps(2)
.with_predict_steps(4);
let mut trainer1 = DeterministicPhaseTrainer::new(&shapes, config.clone(), &device).unwrap();
let mut trainer2 = DeterministicPhaseTrainer::new(&shapes, config, &device).unwrap();
let mut preds1 = Vec::new();
let mut preds2 = Vec::new();
for step in 0..15 {
let info1 = trainer1.begin_step().unwrap();
let info2 = trainer2.begin_step().unwrap();
assert_eq!(
format!("{}", info1.phase),
format!("{}", info2.phase),
"Phase mismatch at step {step}"
);
if info1.needs_backward {
let step_grads = create_deterministic_gradients(&device, step);
trainer1.record_full_gradients(&step_grads).unwrap();
trainer2.record_full_gradients(&step_grads).unwrap();
} else {
let p1 = trainer1.get_predicted_gradients().unwrap();
let p2 = trainer2.get_predicted_gradients().unwrap();
preds1.push(p1);
preds2.push(p2);
}
trainer1.end_step(0.5).unwrap();
trainer2.end_step(0.5).unwrap();
}
assert!(!preds1.is_empty(), "Should have made predictions");
for (i, (p1, p2)) in preds1.iter().zip(preds2.iter()).enumerate() {
for (name, t1) in p1 {
let t2 = p2.get(name).unwrap();
let diff: f32 = t1
.sub(t2)
.unwrap()
.abs()
.unwrap()
.flatten_all()
.unwrap()
.max(0)
.unwrap()
.to_scalar()
.unwrap();
assert!(
diff < 1e-6,
"Prediction {i} for {name} should be deterministic, diff={diff}"
);
}
}
}
fn create_deterministic_gradients(device: &Device, step: usize) -> HashMap<String, Tensor> {
let mut gradients = HashMap::new();
let scale = 1.0 + step as f32 * 0.05;
gradients.insert(
"fc1.weight".to_string(),
Tensor::ones((64, 32), candle_core::DType::F32, device)
.unwrap()
.affine(scale as f64, 0.0)
.unwrap(),
);
gradients.insert(
"fc1.bias".to_string(),
Tensor::ones(64, candle_core::DType::F32, device)
.unwrap()
.affine(scale as f64, 0.0)
.unwrap(),
);
gradients.insert(
"fc2.weight".to_string(),
Tensor::ones((32, 64), candle_core::DType::F32, device)
.unwrap()
.affine(scale as f64, 0.0)
.unwrap(),
);
gradients.insert(
"fc2.bias".to_string(),
Tensor::ones(32, candle_core::DType::F32, device)
.unwrap()
.affine(scale as f64, 0.0)
.unwrap(),
);
gradients.insert(
"fc3.weight".to_string(),
Tensor::ones((10, 32), candle_core::DType::F32, device)
.unwrap()
.affine(scale as f64, 0.0)
.unwrap(),
);
gradients.insert(
"fc3.bias".to_string(),
Tensor::ones(10, candle_core::DType::F32, device)
.unwrap()
.affine(scale as f64, 0.0)
.unwrap(),
);
gradients
}