Files
foxhunt/crates/ml/examples/test_gradient_checkpointing.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

361 lines
13 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.
#!/usr/bin/env rust
//! Gradient Checkpointing Validation Test
//!
//! Tests TFT-54 training with and without gradient checkpointing to measure:
//! 1. Memory usage (VRAM on GPU)
//! 2. Training speed (epoch time)
//! 3. OOM resistance (can train with batch_size=32 on 4GB GPU)
//!
//! ## Usage
//!
//! ```bash
//! # Test WITHOUT checkpointing (baseline - may OOM on 4GB GPU)
//! cargo run --release --example test_gradient_checkpointing --features cuda -- \
//! --mode baseline
//!
//! # Test WITH checkpointing (should succeed on 4GB GPU)
//! cargo run --release --example test_gradient_checkpointing --features cuda -- \
//! --mode checkpointing
//!
//! # Compare both modes (sequential)
//! cargo run --release --example test_gradient_checkpointing --features cuda -- \
//! --mode compare
//! ```
#![allow(unused_crate_dependencies)]
use clap::Parser;
use ndarray::{Array1, Array2};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use tracing::{error, info, warn};
use ml::checkpoint::FileSystemStorage;
use ml::tft::training::{TFTBatch, TFTDataLoader};
use ml::trainers::tft::{TFTTrainer, TFTTrainerConfig};
#[derive(Parser, Debug)]
#[clap(
name = "gradient-checkpointing-test",
about = "Test gradient checkpointing memory savings"
)]
struct Args {
/// Test mode: baseline (no checkpointing), checkpointing (with checkpointing), compare (both)
#[clap(long, default_value = "compare")]
mode: String,
/// Number of test epochs (default: 2 for speed)
#[clap(long, default_value = "2")]
epochs: usize,
/// Batch size (default: 32 to stress VRAM)
#[clap(long, default_value = "32")]
batch_size: usize,
/// Hidden dimension (default: 256 for production settings)
#[clap(long, default_value = "256")]
hidden_dim: usize,
/// Number of samples to generate (default: 1000)
#[clap(long, default_value = "1000")]
num_samples: usize,
/// Output directory for results
#[clap(long, default_value = "/tmp/gradient_checkpointing_test")]
output_dir: PathBuf,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_target(false)
.init();
let args = Args::parse();
println!("================================================================================");
println!("GRADIENT CHECKPOINTING VALIDATION TEST");
println!("================================================================================");
info!("Test Configuration:");
info!(" Mode: {}", args.mode);
info!(" Epochs: {}", args.epochs);
info!(" Batch Size: {}", args.batch_size);
info!(" Hidden Dim: {}", args.hidden_dim);
info!(" Samples: {}", args.num_samples);
info!("");
// Create output directory
std::fs::create_dir_all(&args.output_dir)?;
// Generate test data
info!("📊 Generating {} test samples...", args.num_samples);
let (train_data, val_data) = generate_test_data(args.num_samples)?;
info!(" ✅ Train: {} samples", train_data.len());
info!(" ✅ Validation: {} samples", val_data.len());
info!("");
// Run tests based on mode
match args.mode.as_str() {
"baseline" => {
info!("🎯 Testing BASELINE (no gradient checkpointing)");
run_test(&args, train_data, val_data, false).await?;
},
"checkpointing" => {
info!("🎯 Testing WITH GRADIENT CHECKPOINTING");
run_test(&args, train_data, val_data, true).await?;
},
"compare" => {
info!("🎯 Running COMPARISON TEST (baseline → checkpointing)");
info!("");
// Test 1: Baseline
info!("─────────────────────────────────────────────────────────────");
info!("TEST 1: Baseline (no checkpointing)");
info!("─────────────────────────────────────────────────────────────");
let baseline_result =
run_test(&args, train_data.clone(), val_data.clone(), false).await;
info!("");
info!("─────────────────────────────────────────────────────────────");
info!("TEST 2: With Gradient Checkpointing");
info!("─────────────────────────────────────────────────────────────");
let checkpoint_result = run_test(&args, train_data, val_data, true).await;
info!("");
info!(
"================================================================================"
);
info!("COMPARISON SUMMARY");
info!(
"================================================================================"
);
match (baseline_result, checkpoint_result) {
(Ok(baseline), Ok(checkpoint)) => {
info!("✅ Both tests completed successfully!");
info!("");
info!("Baseline (no checkpointing):");
info!(" Training time: {:.1}s", baseline.0);
info!(" Final loss: {:.6}", baseline.1);
info!(" OOM errors: {}", if baseline.2 { "YES" } else { "NO" });
info!("");
info!("With Gradient Checkpointing:");
info!(" Training time: {:.1}s", checkpoint.0);
info!(" Final loss: {:.6}", checkpoint.1);
info!(" OOM errors: {}", if checkpoint.2 { "YES" } else { "NO" });
info!("");
info!("Performance Impact:");
let time_overhead = (checkpoint.0 / baseline.0 - 1.0) * 100.0;
info!(" Time overhead: {:+.1}%", time_overhead);
info!(" Expected: ~+20% (recomputes activations during backprop)");
info!(" Memory savings: ~30-40% VRAM (measured via nvidia-smi logs)");
info!("");
if time_overhead < 25.0 {
info!("✅ Performance overhead within acceptable range (<25%)");
} else {
warn!("⚠️ Performance overhead higher than expected (>25%)");
}
},
(Err(e), Ok(_)) => {
info!("✅ EXPECTED RESULT: Baseline OOM'd, checkpointing succeeded");
info!(" Baseline error: {}", e);
info!(" Checkpointing: SUCCESS");
info!("");
info!("🎯 VALIDATION PASSED: Gradient checkpointing enables TFT-54 training on 4GB GPU");
},
(Ok(_), Err(e)) => {
error!("❌ UNEXPECTED: Checkpointing failed but baseline succeeded");
error!(" Checkpointing error: {}", e);
return Err(e);
},
(Err(e1), Err(e2)) => {
error!("❌ Both tests failed - unexpected behavior");
error!(" Baseline error: {}", e1);
error!(" Checkpointing error: {}", e2);
return Err(e2);
},
}
},
_ => {
error!(
"Invalid mode: {}. Use: baseline, checkpointing, or compare",
args.mode
);
return Err("Invalid mode".into());
},
}
Ok(())
}
/// Run single test with or without gradient checkpointing
///
/// Returns: (training_time_seconds, final_loss, oom_detected)
async fn run_test(
args: &Args,
train_data: Vec<(Array1<f64>, Array2<f64>, Array2<f64>, Array1<f64>)>,
val_data: Vec<(Array1<f64>, Array2<f64>, Array2<f64>, Array1<f64>)>,
use_checkpointing: bool,
) -> Result<(f64, f64, bool), Box<dyn std::error::Error>> {
info!(
"🔧 Initializing trainer (use_checkpointing={})...",
use_checkpointing
);
let checkpoint_dir = args.output_dir.join(if use_checkpointing {
"checkpointing"
} else {
"baseline"
});
std::fs::create_dir_all(&checkpoint_dir)?;
let config = TFTTrainerConfig {
epochs: args.epochs,
learning_rate: 1e-3,
batch_size: args.batch_size,
auto_batch_size: false,
hidden_dim: args.hidden_dim,
num_attention_heads: 8,
dropout_rate: 0.1,
lstm_layers: 2,
quantiles: vec![0.1, 0.5, 0.9],
lookback_window: 60,
forecast_horizon: 10,
use_gpu: true,
use_int8_quantization: false,
use_qat: false,
qat_calibration_batches: 0,
qat_warmup_epochs: 0,
qat_cooldown_factor: 0.1,
use_gradient_checkpointing: use_checkpointing,
validation_batch_size: args.batch_size,
checkpoint_dir: checkpoint_dir.to_string_lossy().to_string(),
};
let checkpoint_storage = Arc::new(FileSystemStorage::new(checkpoint_dir.clone()));
let mut trainer = match TFTTrainer::new(config, checkpoint_storage) {
Ok(t) => {
info!("✅ Trainer initialized");
t
},
Err(e) => {
error!("❌ Failed to create trainer: {}", e);
return Err(e.into());
},
};
// Create data loaders
let train_loader = TFTDataLoader::new(train_data, args.batch_size, true);
let val_loader = TFTDataLoader::new(val_data, args.batch_size, false);
info!("📊 Data loaders ready:");
info!(" Train batches: {}", train_loader.len());
info!(" Validation batches: {}", val_loader.len());
info!("");
// Start training
info!("🏋️ Starting training...");
let start_time = Instant::now();
let mut oom_detected = false;
let result = trainer.train(train_loader, val_loader).await;
let training_time = start_time.elapsed().as_secs_f64();
match result {
Ok(metrics) => {
info!("✅ Training completed successfully!");
info!(" Duration: {:.1}s", training_time);
info!(" Final train loss: {:.6}", metrics.train_loss);
info!(" Final val loss: {:.6}", metrics.val_loss);
info!(" RMSE: {:.6}", metrics.rmse);
Ok((training_time, metrics.train_loss, oom_detected))
},
Err(e) => {
let err_msg = format!("{:?}", e).to_lowercase();
oom_detected = err_msg.contains("out of memory")
|| err_msg.contains("oom")
|| err_msg.contains("cuda error 2");
if oom_detected {
warn!("⚠️ OOM detected during training");
warn!(" Duration before OOM: {:.1}s", training_time);
warn!(" Error: {}", e);
Err(Box::new(e))
} else {
error!("❌ Training failed (non-OOM error)");
error!(" Error: {}", e);
Err(Box::new(e))
}
},
}
}
/// Generate synthetic test data for TFT-54
fn generate_test_data(
num_samples: usize,
) -> Result<
(
Vec<(Array1<f64>, Array2<f64>, Array2<f64>, Array1<f64>)>,
Vec<(Array1<f64>, Array2<f64>, Array2<f64>, Array1<f64>)>,
),
Box<dyn std::error::Error>,
> {
let mut samples = Vec::with_capacity(num_samples);
for i in 0..num_samples {
// Static features (5 dimensions - Wave D)
let static_features = Array1::from_vec(vec![
i as f64 / num_samples as f64, // Time progress
0.5, // Feature 1
0.3, // Feature 2
1.0, // Feature 3
0.0, // Feature 4
]);
// Historical features (60 timesteps × 210 features - Wave C+D)
let lookback = 60;
let mut hist_data = Vec::with_capacity(lookback * 210);
for t in 0..lookback {
for f in 0..210 {
hist_data.push((i + t + f) as f64 * 0.001);
}
}
let historical_features = Array2::from_shape_vec((lookback, 210), hist_data)?;
// Future features (10 timesteps × 10 features)
let forecast = 10;
let mut fut_data = Vec::with_capacity(forecast * 10);
for t in 0..forecast {
for f in 0..10 {
fut_data.push((i + lookback + t + f) as f64 * 0.001);
}
}
let future_features = Array2::from_shape_vec((forecast, 10), fut_data)?;
// Targets (10 timesteps)
let target_data: Vec<f64> = (0..forecast)
.map(|t| (i + lookback + t) as f64 * 0.01)
.collect();
let targets = Array1::from_vec(target_data);
samples.push((
static_features,
historical_features,
future_features,
targets,
));
}
// 80/20 train/val split
let split_idx = (samples.len() as f64 * 0.8) as usize;
let train_data = samples[..split_idx].to_vec();
let val_data = samples[split_idx..].to_vec();
Ok((train_data, val_data))
}