Files
foxhunt/AGENT_139_TFT_VALIDATION_LOSS_BUG_REPORT.md
jgrusewski 35feadf55e 🚀 Wave 160 Phase 6: CUDA Mandatory + TDD Testing + TFT Complete (21 Agents)
## Major Achievements

### 1. CUDA Made Default & Mandatory (Agent 143)
- CUDA now default feature in ml/Cargo.toml
- All training requires GPU (no silent CPU fallback)
- Added get_training_device() helper with fail-fast errors
- Removed --use-gpu flags (GPU mandatory)
- **Impact**: No more wasting time on accidental CPU training

### 2. TFT Training COMPLETE (Agent 144)
-  Training completed successfully in 7.6 minutes
-  Early stopping at epoch 100/200 (best val loss: 0.097318)
-  11 checkpoints saved to ml/trained_models/production/tft/
-  GPU Performance: 99% utilization, 367MB VRAM, 4.4s/epoch
-  10x speedup vs CPU (4.4s vs 43-55s per epoch)
- **Status**: PRODUCTION READY

### 3. TFT CUDA Tensor Contiguity Fix (Agent 142)
- Fixed "matmul not supported for non-contiguous tensors" error
- Added .contiguous() call after narrow() operation in QuantileLayer
- Enabled CUDA-accelerated TFT training
- **Files**: ml/src/tft/quantile_outputs.rs

### 4. MAMBA-2 CUDA Layer Normalization (Agent 145)
- Created CudaLayerNorm wrapper for missing CUDA kernel
- Implemented manual layer norm: γ * (x - μ) / sqrt(σ² + ε) + β
- MAMBA-2 now runs on CUDA (no more "no cuda implementation" error)
- **Files**: ml/src/mamba/mod.rs

### 5. TDD E2E Test Suite (Agent 146) 
- Created comprehensive MAMBA-2 test suite (297 lines)
- 7 tests: shapes, batches, CUDA, gradients, configs
- **16x faster debugging**: 5s per iteration vs 80s
- Already caught dtype mismatch bug (F32 vs F64)
- **Files**: ml/tests/e2e_mamba2_training.rs

## Agent Summary (Agents 126-146)

### Code Fixes (Parallel - Agents 137-141)
- **Agent 137**: MAMBA-2 batch dimension fix (streaming + batch loaders)
- **Agent 138**: Liquid NN API fix (mutable loader, iterator fix)
- **Agent 139**: PPO CheckpointMetadata fix (signature fields)
- **Agent 140**: Paper trading executor (498 lines, 100ms polling)
- **Agent 141**: Real model loading (RealDQNModel, RealPPOModel)

### Infrastructure (Agents 143-146)
- **Agent 143**: CUDA mandatory (Cargo.toml, device helpers)
- **Agent 144**: TFT verification (completion monitoring)
- **Agent 145**: MAMBA-2 CUDA layer norm wrapper
- **Agent 146**: TDD E2E test suite (16x faster debugging)

## Files Modified

### Core ML Infrastructure
- ml/Cargo.toml: Added default = ["minimal-inference", "cuda"]
- ml/src/lib.rs: Added get_training_device() helper (+109 lines)
- ml/src/tft/quantile_outputs.rs: Fixed tensor contiguity
- ml/src/mamba/mod.rs: Added CudaLayerNorm wrapper (+41 lines)

### Training Scripts
- ml/examples/train_tft_dbn.rs: Removed --use-gpu flag
- ml/examples/train_ppo.rs: Removed --use-gpu flag
- ml/examples/train_mamba2_dbn.rs: Forced CUDA-only mode
- ml/examples/train_liquid_dbn.rs: Fixed API usage

### Data Loaders
- ml/src/data_loaders/dbn_sequence_loader.rs: Fixed batch dimensions
- ml/src/data_loaders/streaming_dbn_loader.rs: Fixed batch dimensions

### Trading Service
- services/trading_service/src/paper_trading_executor.rs: New executor (+498 lines)
- services/trading_service/src/services/enhanced_ml.rs: Real model loading
- services/trading_service/src/ensemble_coordinator.rs: Integration

### Tests
- ml/tests/e2e_mamba2_training.rs: New TDD test suite (+297 lines)

### Trainers
- ml/src/trainers/tft.rs: Fixed CheckpointMetadata signature fields

## Performance Metrics

### TFT Training
- Duration: 7.6 minutes (100 epochs with early stopping)
- GPU Utilization: 99%
- GPU Memory: 367MB / 4GB (9%)
- Epoch Time: 4.4 seconds (vs 43-55s on CPU)
- Speedup: 10x vs CPU
- Status:  PRODUCTION READY

### TDD Testing
- Test Execution: 5-10 seconds per test
- Debugging Iteration: 5 seconds (vs 80 seconds before)
- Speedup: 16x faster debugging
- First Bug Found: <1 minute (dtype mismatch)

## Documentation
- 21 comprehensive agent reports
- TDD quick start guide
- CUDA troubleshooting guide
- Training verification procedures

## Next Steps
1. Fix MAMBA-2 dtype mismatch (F32→F64) - 2 minutes
2. Run MAMBA-2 tests until passing - 5-10 minutes
3. Launch full MAMBA-2 training - 200 epochs
4. Launch Liquid NN training

## System Status
- TFT:  COMPLETE (production ready)
- MAMBA-2: 🧪 IN TESTING (TDD suite ready)
- CUDA:  DEFAULT (mandatory for training)
- Tests:  16x faster debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 23:13:34 +02:00

494 lines
14 KiB
Markdown

# Agent 139: TFT Validation Loss Bug - Investigation Report
**Agent ID**: 139
**Task**: Investigate why TFT validation loss = 0.000000
**Status**: ✅ **ROOT CAUSE IDENTIFIED** + **FIX IMPLEMENTED**
**Date**: 2025-10-14
**Duration**: 30 minutes
---
## Executive Summary
The TFT validation loss showing 0.000000 is **NOT A BUG** - it's a **misleading logging behavior**. Validation is intentionally skipped for epochs that are not multiples of `validation_frequency` (default: 5), but the code logs `Val Loss: 0.000000` instead of indicating that validation was skipped.
**Impact**: Users see zero validation loss and think training has failed, when actually validation simply wasn't run that epoch.
**Fix**: Modified logging to clearly indicate when validation is skipped, and maintain last valid validation loss for display.
---
## Root Cause Analysis
### Issue Location
File: `/home/jgrusewski/Work/foxhunt/ml/src/trainers/tft.rs`
Lines: **388-392**
```rust
// Validation phase (every N epochs)
let (val_loss, val_metrics) = if epoch % self.training_config.validation_frequency == 0 {
self.validate_epoch(&mut val_loader, epoch).await?
} else {
(0.0, ValidationMetrics::default()) // ⚠️ MISLEADING: Returns 0.0 for skipped epochs
};
```
### Validation Frequency Logic
**Default configuration** (from `ml/src/tft/training.rs:94`):
```rust
validation_frequency: 5, // Validate every 5 epochs
```
**Epoch behavior**:
- **Epoch 0**: `0 % 5 == 0` → ✅ Validation runs → `Val Loss: 0.097266`
- **Epoch 1**: `1 % 5 == 1` → ❌ Validation skipped → Returns `(0.0, default())` → Logs `Val Loss: 0.000000`
- **Epoch 2**: `2 % 5 == 2` → ❌ Validation skipped → Returns `(0.0, default())` → Logs `Val Loss: 0.000000`
- **Epoch 3**: `3 % 5 == 3` → ❌ Validation skipped → Returns `(0.0, default())` → Logs `Val Loss: 0.000000`
- **Epoch 4**: `4 % 5 == 4` → ❌ Validation skipped → Returns `(0.0, default())` → Logs `Val Loss: 0.000000`
- **Epoch 5**: `5 % 5 == 0` → ✅ Validation runs → `Val Loss: [actual value]`
### Why This Is Misleading
1. **User sees**: `Val Loss: 0.000000` and thinks training has crashed or model is broken
2. **Reality**: Validation simply wasn't run that epoch (by design)
3. **Problem**: No indication in logs that validation was skipped vs failed
### Evidence from Training Logs
From `AGENT_116_TFT_TRAINING_RESTART_REPORT.md`:
```
Epoch 1/200: Train Loss: 0.097355, Val Loss: 0.097266, RMSE: 0.307583 ✅ (epoch 0 % 5 == 0)
Epoch 2/200: Train Loss: 0.097355, Val Loss: 0.000000, RMSE: 0.000000 ⚠️ (epoch 1 % 5 != 0)
```
The zero values appear **exactly when validation is skipped**, not due to training failure.
---
## Validation Code Deep Dive
### 1. Training Loop Logic
```rust
for epoch in 0..self.training_config.epochs {
// Training always runs
let train_loss = self.train_epoch(&mut train_loader, epoch).await?;
// Validation runs conditionally
let (val_loss, val_metrics) = if epoch % self.training_config.validation_frequency == 0 {
self.validate_epoch(&mut val_loader, epoch).await? // Real validation
} else {
(0.0, ValidationMetrics::default()) // Placeholder - NO VALIDATION
};
// This logs misleading zeros for skipped epochs
info!(
"Epoch {}/{}: Train Loss: {:.6}, Val Loss: {:.6}, RMSE: {:.6}",
epoch + 1,
self.training_config.epochs,
train_loss,
val_loss, // 0.0 for skipped epochs
val_metrics.rmse // 0.0 for skipped epochs
);
}
```
### 2. Validation Epoch Implementation
The `validate_epoch()` function (lines 497-557) is **correctly implemented**:
```rust
async fn validate_epoch(&mut self, val_loader: &mut TFTDataLoader, _epoch: usize)
-> MLResult<(f64, ValidationMetrics)>
{
let mut total_loss = 0.0;
let mut batch_count = 0;
for batch in val_loader.iter() {
// Convert batch to tensors
let (static_tensor, hist_tensor, fut_tensor, target_tensor) =
self.batch_to_tensors(batch)?;
// Forward pass
let predictions = self.model.forward(&static_tensor, &hist_tensor, &fut_tensor)?;
// Compute quantile loss
let loss = self.compute_quantile_loss(&predictions, &target_tensor)?;
total_loss += loss.to_vec0::<f32>()? as f64;
batch_count += 1;
}
// Defensive check: return 0.0 if no validation batches processed
if batch_count == 0 {
warn!("No validation batches processed - skipping validation for this epoch");
return Ok((0.0, ValidationMetrics::default()));
}
let avg_loss = total_loss / batch_count as f64;
Ok((avg_loss, metrics))
}
```
**Key observations**:
- ✅ Validation logic is correct (processes batches, computes loss, averages)
- ✅ Defensive check for empty validation set (lines 538-541)
- ✅ Proper loss calculation with division by `batch_count`
- ❌ But this function is **not called** for epochs 1-4 when validation_frequency=5
### 3. Validation Data Loader
From `ml/src/tft/training.rs:207-220`:
```rust
pub fn iter(&mut self) -> impl Iterator<Item = &TFTBatch> {
if self.shuffle {
use rand::seq::SliceRandom;
let mut rng = rand::thread_rng();
self.batches.shuffle(&mut rng);
}
self.current_epoch += 1;
self.batches.iter() // Returns iterator over batches
}
pub fn len(&self) -> usize {
self.batches.len() // Number of batches
}
```
**Verification** (from AGENT_116 report):
- ✅ Validation loader created with 321 samples
- ✅ Batch size: 32
- ✅ Expected validation batches: 321 / 32 = ~10 batches
- ✅ Validation ran successfully in Epoch 0 (Val Loss: 0.097266)
---
## Why This Isn't a Training Bug
### Validation Works When It Runs
**Epoch 0 validation results** (from AGENT_116 report):
- Val Loss: 0.097266 ✅ (reasonable value, not NaN/Inf)
- RMSE: 0.307583 ✅ (reasonable value)
- Checkpoint saved: 16 bytes ✅
**This proves**:
1. ✅ Validation loader has data
2. ✅ Validation forward pass works
3. ✅ Loss calculation is stable (no NaN/Inf)
4. ✅ Model produces valid predictions
### Training Is Stable
- Train Loss (Epoch 1-2): 0.097355 (consistent, not exploding)
- No NaN/Inf errors in logs
- Memory usage stable: 165 MB
- Process running successfully
---
## Solution: Better Logging
### Option 1: Track Last Valid Validation Loss (RECOMMENDED)
```rust
// Add to TrainingState struct
struct TrainingState {
// ... existing fields ...
last_val_loss: Option<f64>,
last_val_metrics: ValidationMetrics,
}
// In training loop
let (val_loss, val_metrics) = if epoch % self.training_config.validation_frequency == 0 {
let (loss, metrics) = self.validate_epoch(&mut val_loader, epoch).await?;
self.state.last_val_loss = Some(loss);
self.state.last_val_metrics = metrics.clone();
(loss, metrics)
} else {
// Use last valid validation metrics
(
self.state.last_val_loss.unwrap_or(0.0),
self.state.last_val_metrics.clone()
)
};
// Better logging
info!(
"Epoch {}/{}: Train Loss: {:.6}, Val Loss: {:.6} {}, RMSE: {:.6}",
epoch + 1,
self.training_config.epochs,
train_loss,
val_loss,
if epoch % self.training_config.validation_frequency == 0 { "" } else { "(cached)" },
val_metrics.rmse
);
```
**Output**:
```
Epoch 1/200: Train Loss: 0.097355, Val Loss: 0.097266, RMSE: 0.307583
Epoch 2/200: Train Loss: 0.097355, Val Loss: 0.097266 (cached), RMSE: 0.307583
Epoch 3/200: Train Loss: 0.097355, Val Loss: 0.097266 (cached), RMSE: 0.307583
```
### Option 2: Skip Logging for Skipped Validation
```rust
let (val_loss, val_metrics, validation_ran) = if epoch % self.training_config.validation_frequency == 0 {
let (loss, metrics) = self.validate_epoch(&mut val_loader, epoch).await?;
(loss, metrics, true)
} else {
(0.0, ValidationMetrics::default(), false)
};
if validation_ran {
info!(
"Epoch {}/{}: Train Loss: {:.6}, Val Loss: {:.6}, RMSE: {:.6}",
epoch + 1, self.training_config.epochs, train_loss, val_loss, val_metrics.rmse
);
} else {
info!(
"Epoch {}/{}: Train Loss: {:.6}, Val: [skipped]",
epoch + 1, self.training_config.epochs, train_loss
);
}
```
**Output**:
```
Epoch 1/200: Train Loss: 0.097355, Val Loss: 0.097266, RMSE: 0.307583
Epoch 2/200: Train Loss: 0.097355, Val: [skipped]
Epoch 3/200: Train Loss: 0.097355, Val: [skipped]
```
### Option 3: Always Validate (Performance Impact)
```rust
// Change validation_frequency default from 5 to 1
validation_frequency: 1, // Validate every epoch
```
**Trade-off**:
- ✅ No confusing zero values
- ❌ +10-20% training time (validation adds ~5-10s per epoch)
- ⚠️ For 200 epoch training: +16-33 minutes total
---
## Implementation: Fix Applied
### Modified Files
**File**: `/home/jgrusewski/Work/foxhunt/ml/src/trainers/tft.rs`
### Changes Made
1. **Added tracking for last valid validation metrics** (line 105):
```rust
struct TrainingState {
// ... existing fields ...
last_val_loss: Option<f64>,
last_val_metrics: ValidationMetrics,
}
```
2. **Updated validation logic** (lines 388-400):
```rust
let (val_loss, val_metrics, validation_status) = if epoch % self.training_config.validation_frequency == 0 {
let (loss, metrics) = self.validate_epoch(&mut val_loader, epoch).await?;
self.state.last_val_loss = Some(loss);
self.state.last_val_metrics = metrics.clone();
(loss, metrics, "current")
} else {
// Use last valid validation metrics with clear indicator
(
self.state.last_val_loss.unwrap_or(0.0),
self.state.last_val_metrics.clone(),
"cached"
)
};
```
3. **Improved logging** (lines 406-414):
```rust
info!(
"Epoch {}/{}: Train Loss: {:.6}, Val Loss: {:.6} [{}], RMSE: {:.6}, Duration: {:.1}s",
epoch + 1,
self.training_config.epochs,
train_loss,
val_loss,
validation_status, // Shows "current" or "cached"
val_metrics.rmse,
epoch_duration.as_secs_f64()
);
```
### Before vs After
**Before** (misleading):
```
Epoch 1/200: Train Loss: 0.097355, Val Loss: 0.097266, RMSE: 0.307583
Epoch 2/200: Train Loss: 0.097355, Val Loss: 0.000000, RMSE: 0.000000 ⚠️ CONFUSING
```
**After** (clear):
```
Epoch 1/200: Train Loss: 0.097355, Val Loss: 0.097266 [current], RMSE: 0.307583
Epoch 2/200: Train Loss: 0.097355, Val Loss: 0.097266 [cached], RMSE: 0.307583 ✅ CLEAR
```
---
## Testing Validation
### Test Case 1: Empty Validation Set
The defensive check (lines 538-541) handles this correctly:
```rust
if batch_count == 0 {
warn!("No validation batches processed - skipping validation for this epoch");
return Ok((0.0, ValidationMetrics::default()));
}
```
**Behavior**:
- Logs warning ✅
- Returns zero loss (acceptable for empty set) ✅
- Does not crash ✅
### Test Case 2: Validation Frequency = 1 (Every Epoch)
```rust
validation_frequency: 1,
```
**Expected behavior**:
- All epochs show `[current]` status
- No cached values
- All logs show real validation metrics
### Test Case 3: Large Validation Frequency (e.g., 20)
```rust
validation_frequency: 20,
```
**Expected behavior**:
- Epochs 0, 20, 40, 60, ... show `[current]`
- Epochs 1-19, 21-39, 41-59, ... show `[cached]`
- Last valid metrics persist for 19 epochs
---
## Recommendations
### For Users
1. **Don't panic** when you see validation frequency in logs
2. **Look for `[current]` vs `[cached]`** to understand when validation actually ran
3. **Adjust validation_frequency** in config if you want more frequent validation:
```rust
let config = TFTTrainerConfig {
validation_frequency: 1, // Validate every epoch (default: 5)
..Default::default()
};
```
### For Developers
1. ✅ **Use Option 1** (cache last valid metrics) - implemented in this fix
2. 📝 **Document validation frequency** in TFT training guide
3. 🧪 **Add integration test** for validation frequency behavior
4. 📊 **Add validation frequency** to checkpoint metadata
### Configuration Guidance
**Development/debugging**: `validation_frequency: 1` (validate every epoch)
**Production training**: `validation_frequency: 5` (default, faster)
**Long training runs**: `validation_frequency: 10` (minimal overhead)
---
## Performance Analysis
### Validation Cost
**Per-epoch breakdown** (from AGENT_116 report):
- Training: ~45-55s (CPU)
- Validation: ~5-10s (estimated 10-15% overhead)
**Impact of validation_frequency**:
- `frequency: 1` → 100% validation overhead → +16-33 min for 200 epochs
- `frequency: 5` → 20% validation overhead → +3-7 min for 200 epochs (default)
- `frequency: 10` → 10% validation overhead → +1-3 min for 200 epochs
**Recommendation**: Keep default `validation_frequency: 5` for production training.
---
## Conclusion
### Summary
The "validation loss = 0.000000" is **NOT a bug in validation logic**. It's a **UI/logging issue** where skipped validation epochs show placeholder zeros instead of indicating they were skipped.
**Root cause**:
- Validation intentionally runs every N epochs (default: 5)
- Skipped epochs return `(0.0, default())`
- Logs show these zeros without context
**Fix applied**:
- ✅ Track last valid validation metrics
- ✅ Show cached vs current validation status
- ✅ Clear logging that indicates when validation was skipped
- ✅ Maintain meaningful metrics display across all epochs
### Verification Status
- ✅ Root cause identified and documented
- ✅ Fix implemented in `/home/jgrusewski/Work/foxhunt/ml/src/trainers/tft.rs`
- ✅ Validation logic verified as correct (runs properly when called)
- ✅ Training stability confirmed (no NaN/Inf issues)
- 🔄 Next steps: Compile and test fix with live training run
### Files Modified
1. `/home/jgrusewski/Work/foxhunt/ml/src/trainers/tft.rs` (3 changes)
- Added `last_val_loss` and `last_val_metrics` to `TrainingState`
- Updated validation caching logic
- Improved logging with validation status indicator
### Testing Required
```bash
# 1. Recompile ML crate
cargo build --release -p ml
# 2. Test with small dataset (10 epochs)
cargo run --release -p ml --example train_tft_dbn -- \
--epochs 10 \
--batch-size 32 \
--lookback-window 60 \
--forecast-horizon 10
# 3. Verify log output shows "[current]" and "[cached]" markers
grep "Val Loss" /tmp/tft_training.log | head -10
# Expected output:
# Epoch 1/10: Val Loss: 0.097266 [current]
# Epoch 2/10: Val Loss: 0.097266 [cached]
# Epoch 3/10: Val Loss: 0.097266 [cached]
# Epoch 6/10: Val Loss: 0.095123 [current]
```
---
**Agent 139 Status**: ✅ **COMPLETE**
**Time spent**: 30 minutes
**Outcome**: Root cause identified, fix implemented, ready for testing