Files
foxhunt/ml/src/gradient_utils.rs
jgrusewski c645e6222d Wave 11: Rainbow DQN integration + 23/23 tests passing
CRITICAL FINDINGS from 3-trial validation:
- 85,120 gradient clipping warnings (81.6% of logs) - REGRESSION
- Rainbow features DISABLED: use_dueling=false, use_distributional=false, use_noisy_nets=false
- Negative Q-values confirmed: HOLD -1000 to -3250
- Performance: Sharpe 0.29 (target 0.77)

Changes:
- Fixed N-Step compilation (7/7 tests passing)
- Fixed Distributional compilation (6/6 tests passing)
- Fixed Dueling CUDA errors (10/10 tests passing)
- Added TDD validation for state_dim=225
- Total: 23/23 Wave 11 tests passing (100%)

Issues requiring investigation:
1. Why are Dueling/Distributional/Noisy disabled in hyperopt?
2. Why gradient explosion despite previous fixes?
3. Test coverage gaps - unit tests pass but integration fails

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 13:53:59 +01:00

64 lines
2.3 KiB
Rust

//! Gradient utilities for Candle framework
//!
//! Provides gradient clipping and other gradient-related operations
//! that are missing from candle_nn::optim
use candle_core::{Error, backprop::GradStore, Var};
/// Clip gradients by global L2 norm (similar to PyTorch's clip_grad_norm_)
///
/// This function computes the L2 norm of all gradients and scales them
/// proportionally if the total norm exceeds max_norm.
///
/// # Arguments
/// * `vars` - Slice of Var containing model parameters
/// * `grads` - Mutable reference to GradStore from loss.backward()
/// * `max_norm` - Maximum allowed gradient norm
///
/// # Returns
/// A tuple of (actual_norm, clipped_norm) where:
/// - actual_norm: The total gradient norm before clipping
/// - clipped_norm: The gradient norm after clipping (≤ max_norm)
///
/// # Example
/// ```no_run
/// use ml::gradient_utils::clip_grad_norm;
/// use candle_core::Var;
///
/// let vars = vec![]; // Your model variables
/// // ... compute loss ...
/// let mut grads = loss.backward()?;
/// let (actual_norm, clipped_norm) = clip_grad_norm(&vars, &mut grads, 10.0)?;
/// println!("Gradient norm: {} -> {}", actual_norm, clipped_norm);
/// # Ok::<(), candle_core::Error>(())
/// ```
pub fn clip_grad_norm(vars: &[Var], grads: &mut GradStore, max_norm: f64) -> Result<(f64, f64), Error> {
let mut total_norm_sq = 0.0f64;
// First pass: Calculate the total L2 norm of all gradients
for var in vars.iter() {
if let Some(grad) = grads.get(var) {
let norm_sq = grad.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
total_norm_sq += norm_sq;
}
}
let total_norm = total_norm_sq.sqrt();
// Second pass: Scale gradients if the norm exceeds the maximum
if total_norm > max_norm {
let scale_factor = max_norm / (total_norm + 1e-6); // Add epsilon for numerical stability
for var in vars.iter() {
// Remove gradient, scale it, and insert back
if let Some(grad) = grads.remove(var) {
let scaled_grad = (grad * scale_factor)?;
grads.insert(var, scaled_grad);
}
}
// Return both actual and clipped norms
Ok((total_norm, max_norm))
} else {
// Norm is already below max_norm, no clipping needed
Ok((total_norm, total_norm))
}
}