Files
rustytorch/examples/pinn_mre_helmholtz/tests/validate_training.rs
T
2026-03-04 00:08:42 +00:00

301 lines
9.6 KiB
Rust

//! Validation Tests for PINN Training
//!
//! These tests verify that the training loop actually updates weights
//! and that loss decreases over training.
use pinn_mre_helmholtz::{GpuAdam, GradientWorkspace};
use rtx_tensor::{Tensor, Device};
/// Test that GpuAdam actually modifies parameters
#[test]
fn test_gpu_adam_modifies_weights() {
let device = Device::Cpu;
// Create simple parameter and gradient
let original_val = 1.0f32;
let grad_val = 0.5f32;
let shapes = vec![vec![1]];
let mut opt = GpuAdam::with_defaults(&shapes, &device).unwrap();
let mut params = vec![
Tensor::from_vec(vec![original_val], &[1], &device).unwrap()
];
let grads = vec![
Tensor::from_vec(vec![grad_val], &[1], &device).unwrap()
];
// Copy original value
let original = params[0].to_cpu().unwrap()[0];
// Perform optimization step
opt.step(&mut params, &grads).unwrap();
// Check that parameter changed
let updated = params[0].to_cpu().unwrap()[0];
assert!(
(updated - original).abs() > 1e-8,
"Parameter should have changed: original={}, updated={}",
original,
updated
);
// With positive gradient, parameter should decrease (gradient descent)
assert!(
updated < original,
"With positive gradient, parameter should decrease: original={}, updated={}",
original,
updated
);
}
/// Test that multiple Adam steps continue to modify parameters
#[test]
fn test_gpu_adam_multiple_steps() {
let device = Device::Cpu;
let shapes = vec![vec![2, 2]];
// Use larger learning rate for faster convergence in test
let mut opt = GpuAdam::new(&shapes, &device, 0.1, 0.9, 0.999, 1e-8).unwrap();
let mut params = vec![
Tensor::from_vec(vec![1.0f32, 2.0, 3.0, 4.0], &[2, 2], &device).unwrap()
];
// Track parameter norm over steps
let mut norms = Vec::new();
let initial_norm: f32 = params[0].to_cpu().unwrap().iter().map(|x| x * x).sum::<f32>().sqrt();
norms.push(initial_norm);
// Perform 100 steps with constant gradient pointing toward zero
for _ in 0..100 {
let param_data = params[0].to_cpu().unwrap();
// Gradient = parameter (so we're doing gradient descent on ||params||^2)
let grads = vec![
Tensor::from_vec(param_data, &[2, 2], &device).unwrap()
];
opt.step(&mut params, &grads).unwrap();
let norm: f32 = params[0].to_cpu().unwrap().iter().map(|x| x * x).sum::<f32>().sqrt();
norms.push(norm);
}
// Check that the first 10 steps show decreasing norm
for i in 1..10.min(norms.len()) {
assert!(
norms[i] < norms[i - 1],
"Norm should decrease: step {} norm = {}, step {} norm = {}",
i - 1,
norms[i - 1],
i,
norms[i]
);
}
// Final norm should be significantly smaller than initial
assert!(
*norms.last().unwrap() < *norms.first().unwrap() * 0.5,
"Final norm should be at least 50% smaller than initial: initial={}, final={}",
norms.first().unwrap(),
norms.last().unwrap()
);
}
/// Test GradientWorkspace creation
#[test]
fn test_gradient_workspace_creation() {
let device = Device::Cpu;
let batch_size = 32;
let ff_dim = 16;
let hidden_dim = 64;
let num_hidden_layers = 3;
let ws = GradientWorkspace::new(batch_size, ff_dim, hidden_dim, num_hidden_layers, &device).unwrap();
// Check dimensions
assert_eq!(ws.num_layers(), 4); // 3 hidden + 1 output
// Check z/h shapes
assert_eq!(ws.z.len(), 4);
assert_eq!(ws.h.len(), 4);
// Check dW/db shapes
assert_eq!(ws.dW.len(), 4);
assert_eq!(ws.db.len(), 4);
// First layer: [hidden_dim, ff_dim * 2]
assert_eq!(ws.dW[0].shape(), &[64, 32]);
assert_eq!(ws.db[0].shape(), &[64]);
// Output layer: [2, hidden_dim]
assert_eq!(ws.dW[3].shape(), &[2, 64]);
assert_eq!(ws.db[3].shape(), &[2]);
}
/// Test MSE backward gradient
#[test]
fn test_mse_backward_gradient() {
use pinn_mre_helmholtz::mse_backward;
let device = Device::Cpu;
// Predictions and targets
let pred = Tensor::from_vec(vec![1.0f32, 2.0, 3.0, 4.0], &[2, 2], &device).unwrap();
let target = Tensor::from_vec(vec![0.0f32, 0.0, 0.0, 0.0], &[2, 2], &device).unwrap();
let grad = mse_backward(&pred, &target).unwrap();
// dL/dpred = (2/N) * (pred - target) = (2/2) * pred = pred
let grad_cpu = grad.to_cpu().unwrap();
assert!((grad_cpu[0] - 1.0).abs() < 1e-5, "Expected 1.0, got {}", grad_cpu[0]);
assert!((grad_cpu[1] - 2.0).abs() < 1e-5, "Expected 2.0, got {}", grad_cpu[1]);
assert!((grad_cpu[2] - 3.0).abs() < 1e-5, "Expected 3.0, got {}", grad_cpu[2]);
assert!((grad_cpu[3] - 4.0).abs() < 1e-5, "Expected 4.0, got {}", grad_cpu[3]);
}
/// Test layer backward computation
#[test]
fn test_layer_backward() {
use pinn_mre_helmholtz::layer_backward;
let device = Device::Cpu;
// Simple case: 2 samples, 2 output units, 2 input units
let d_l_dh = Tensor::from_vec(vec![1.0f32, 1.0, 1.0, 1.0], &[2, 2], &device).unwrap();
let h = Tensor::from_vec(vec![0.5f32, 0.5, 0.5, 0.5], &[2, 2], &device).unwrap();
let h_prev = Tensor::from_vec(vec![1.0f32, 0.5, 1.0, 0.5], &[2, 2], &device).unwrap();
let w = Tensor::from_vec(vec![1.0f32, 0.0, 0.0, 1.0], &[2, 2], &device).unwrap();
let (d_w, d_b, dh_prev) = layer_backward(&d_l_dh, &h, &h_prev, &w, true).unwrap();
// Check shapes
assert_eq!(d_w.shape(), &[2, 2]);
assert_eq!(d_b.shape(), &[2]);
assert_eq!(dh_prev.shape(), &[2, 2]);
// With h=0.5, tanh'(z) = 1 - 0.5^2 = 0.75
// dL_dz = dL_dh * 0.75 = 0.75 for all elements
// db = sum(dL_dz, axis=0) = [1.5, 1.5]
let db_cpu = d_b.to_cpu().unwrap();
assert!((db_cpu[0] - 1.5).abs() < 1e-5, "db[0] should be 1.5, got {}", db_cpu[0]);
assert!((db_cpu[1] - 1.5).abs() < 1e-5, "db[1] should be 1.5, got {}", db_cpu[1]);
}
/// Test that PINN solver training actually reduces loss
#[test]
fn test_pinn_training_reduces_loss() {
use pinn_mre_helmholtz::{Config, Mre1DPinnSolver};
// Create solver with small network for quick test
let mut cfg = Config::default();
cfg.n_data = 50; // Smaller dataset for faster test
cfg.u_layers = 2; // Fewer layers
cfg.u_hidden = 32;
cfg.u_ff_dim = 16;
cfg.epochs = 100; // We'll control steps manually
let mut solver = Mre1DPinnSolver::new(cfg).expect("Failed to create solver");
// Run forward pass to get initial loss
solver.train_step_data_only().expect("First training step failed");
let initial_loss = solver.compute_data_loss_for_logging()
.expect("Failed to compute initial loss");
println!("Initial loss: {:.6e}", initial_loss);
// Run 500 training steps
for step in 1..=500 {
solver.train_step_data_only().expect("Training step failed");
// Log progress every 100 steps
if step % 100 == 0 {
let loss = solver.compute_data_loss_for_logging().unwrap();
println!("Step {}: loss = {:.6e}", step, loss);
}
}
// Compute final loss
let final_loss = solver.compute_data_loss_for_logging()
.expect("Failed to compute final loss");
println!("Final loss: {:.6e}", final_loss);
println!("Improvement: {:.1}x", initial_loss / final_loss);
// The loss should decrease over training
// We expect at least some improvement (2% reduction - conservative threshold)
// Note: The network may converge quickly on this simple problem
assert!(
final_loss < initial_loss,
"Training should reduce loss: initial={:.6e}, final={:.6e}",
initial_loss,
final_loss
);
}
/// Profile training step to identify bottlenecks
#[test]
#[cfg(feature = "cuda")]
fn test_profile_training_step() {
use pinn_mre_helmholtz::{Config, Mre1DPinnSolver};
use std::time::Instant;
let mut cfg = Config::default();
cfg.n_data = 200; // Same as benchmark
cfg.u_layers = 3;
cfg.u_hidden = 64;
cfg.u_ff_dim = 64;
let mut solver = Mre1DPinnSolver::new(cfg).expect("Failed to create solver");
// Warm up
for _ in 0..5 {
solver.train_step_data_only().expect("Training step failed");
}
// Get CUDA context for sync
let ctx = rtx_tensor::storage::cuda_manager::get_or_create_context(0).unwrap();
ctx.synchronize().unwrap();
// Time 10 complete training steps
let start = Instant::now();
for _ in 0..10 {
solver.train_step_data_only().expect("Training step failed");
}
ctx.synchronize().unwrap();
let elapsed = start.elapsed();
let per_step = elapsed / 10;
println!("\n=== TRAINING STEP PROFILING ===");
println!("Total time for 10 steps: {:?}", elapsed);
println!("Per step: {:?}", per_step);
println!("Target: < 100µs");
println!("Status: {}", if per_step.as_micros() < 100 { "✅ PASS" } else { "❌ FAIL - too slow!" });
}
/// Test the train_analytical convenience method
#[test]
fn test_train_analytical_method() {
use pinn_mre_helmholtz::{Config, Mre1DPinnSolver};
let mut cfg = Config::default();
cfg.n_data = 30;
cfg.u_layers = 2;
cfg.u_hidden = 16;
cfg.u_ff_dim = 8;
let mut solver = Mre1DPinnSolver::new(cfg).expect("Failed to create solver");
// Run 100 steps with logging
let best_loss = solver.train_analytical(100, 50)
.expect("train_analytical failed");
println!("Best loss from train_analytical: {:.6e}", best_loss);
// Should produce a finite, reasonable loss
assert!(best_loss.is_finite(), "Loss should be finite");
assert!(best_loss < 1.0, "Loss should be reasonable (< 1.0)");
}