Files
foxhunt/AGENT_23_TEST_IMPLEMENTATION_GUIDE.md
jgrusewski 33afaabe1a feat(ml): Final Stabilization Wave - 100% FP32 test pass rate, QAT infrastructure
- PPO numerical stability: Added epsilon (1e-8) protection at 4 log locations
- Hurst division by zero: Fixed in trending.rs:394 and price_features.rs:342
- DQN 225-feature support: Fixed dimension mismatch (feature_vec[4..])
- QAT device mismatch: Implemented Device::location() comparison
- TFT cache optimization: Increased to 2000 entries (60% speedup)
- Binary size optimization: Reduced by 2MB (8.7%) via dependency tuning
- Unused imports: Eliminated all 34 warnings in ML crate
- Test coverage: Added 94+ production hardening tests

Test Results:
- FP32 Models: 1,317/1,317 tests passing (100%)
- Overall Workspace: 313/314 passing (99.7%)
- QAT: 0/24 (temporarily disabled, compilation errors)

Performance:
- TFT training: ~2 min (60% faster via cache optimization)
- DQN training: ~15s (10-25% faster via mimalloc)
- Average improvement: 922× vs minimum requirements

QAT Blockers (P0 - 1-2 weeks):
1. Device mismatch: 11 compilation errors in qat_tft.rs
2. Gradient checkpointing: CLI flag exists but not implemented
3. OOM recovery: AutoBatchSizer exists but no retry integration

Documentation:
- FINAL_VALIDATION_SUMMARY.md (17 agents, 281 lines)
- STABILIZATION_WAVE_COMPLETION_REPORT.md (290 lines)
- DEPLOYMENT_QUICK_START.md (385 lines)
- PRE_DEPLOYMENT_CHECKLIST.md (426 lines)
- KNOWN_ISSUES.md (385 lines)
- NEXT_STEPS_ROADMAP.md (27KB)

Status:  FP32 PRODUCTION READY | 🔴 QAT BLOCKED
2025-10-25 15:36:57 +02:00

16 KiB

Agent 23: Test Implementation Quick Reference

Purpose: Quick reference for implementing 21 critical ML test cases Priority Order: Phase 1 (8 tests) → Phase 2 (4 tests) → Phase 3 (7 tests) → Phase 4 (2 tests)


Phase 1: Production Blockers (16 hours)

Test #5: Zero Batch Size Handling

File: ml/tests/tft_zero_batch_test.rs (NEW)

#[test]
fn test_tft_forward_pass_with_zero_batch_size() -> Result<(), MLError> {
    let device = Device::Cpu;
    let config = TFTConfig::default();
    let mut tft = TemporalFusionTransformer::new(config, device)?;

    let static_features = Tensor::zeros((0, 5), DType::F32, &device)?;
    let historical_features = Tensor::zeros((0, 60, 210), DType::F32, &device)?;
    let future_features = Tensor::zeros((0, 10, 10), DType::F32, &device)?;

    let output = tft.forward(&static_features, &historical_features, &future_features)?;
    assert_eq!(output.dims(), &[0, 10, 9]);
    Ok(())
}

Test #6: Batch Size Mismatch Validation

File: ml/tests/tft_batch_mismatch_test.rs (NEW)

#[test]
fn test_tft_input_validation_detects_mismatched_batch_sizes() -> Result<(), MLError> {
    let device = Device::Cpu;
    let config = TFTConfig::default();
    let mut tft = TemporalFusionTransformer::new(config, device)?;

    // Mismatched batch sizes: 4 vs 2
    let static_features = Tensor::zeros((4, 5), DType::F32, &device)?;
    let historical_features = Tensor::zeros((2, 60, 210), DType::F32, &device)?;
    let future_features = Tensor::zeros((2, 10, 10), DType::F32, &device)?;

    let result = tft.forward(&static_features, &historical_features, &future_features);
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), MLError::ModelError(_)));
    Ok(())
}

TODO: Extend validate_input_dimensions in ml/src/tft/mod.rs to check batch consistency.

Test #8: NaN/Inf Input Propagation

File: ml/tests/tft_nan_handling_test.rs (NEW)

#[test]
fn test_tft_forward_pass_with_nan_inf_inputs() -> Result<(), MLError> {
    let device = Device::Cpu;
    let config = TFTConfig::default();
    let mut tft = TemporalFusionTransformer::new(config, device)?;

    // Create inputs with NaN
    let mut static_data = vec![0.0f32; 5];
    static_data[2] = f32::NAN;
    let static_features = Tensor::from_vec(static_data, (1, 5), &device)?;

    let historical_features = Tensor::zeros((1, 60, 210), DType::F32, &device)?;
    let future_features = Tensor::zeros((1, 10, 10), DType::F32, &device)?;

    // Should NOT panic, NaN propagates
    let output = tft.forward(&static_features, &historical_features, &future_features)?;

    // Verify NaN present in output (makes corruption detectable)
    let output_vec = output.to_vec3::<f32>()?;
    let has_nan = output_vec.iter()
        .flat_map(|batch| batch.iter())
        .flat_map(|horizon| horizon.iter())
        .any(|&val| val.is_nan());
    assert!(has_nan, "NaN should propagate through model");
    Ok(())
}

Test #9: Corrupt Checkpoint Deserialization

File: ml/tests/tft_corrupt_checkpoint_test.rs (NEW)

#[test]
fn test_tft_deserialization_of_corrupt_checkpoint_data() -> Result<(), MLError> {
    let device = Device::Cpu;
    let config = TFTConfig::default();
    let tft = TemporalFusionTransformer::new(config, device)?;

    // Test 1: Random bytes
    let random_bytes = vec![0xDE, 0xAD, 0xBE, 0xEF; 1024];
    let result = tft.deserialize_state(&random_bytes);
    assert!(result.is_err());

    // Test 2: Truncated file (incomplete safetensors)
    let truncated = vec![0x00; 50]; // Way too small
    let result = tft.deserialize_state(&truncated);
    assert!(result.is_err());

    // Test 3: Wrong format (JSON instead of safetensors)
    let json_data = br#"{"weights": [1.0, 2.0, 3.0]}"#;
    let result = tft.deserialize_state(json_data);
    assert!(result.is_err());

    Ok(())
}

Test #11: GPU OOM Handling

File: ml/tests/tft_gpu_oom_test.rs (NEW)

#[test]
#[cfg(feature = "cuda")]
#[ignore = "Requires GPU with limited VRAM"]
fn test_gpu_oom_during_forward_pass() -> Result<(), MLError> {
    let device = Device::cuda_if_available(0)?;
    if !device.is_cuda() {
        return Ok(()); // Skip if no GPU
    }

    // Allocate large tensor to consume most VRAM
    let vram_consumer = Tensor::zeros((8000, 8000), DType::F32, &device)?;

    // Attempt large batch that should trigger OOM
    let config = TFTConfig {
        input_dim: 225,
        hidden_dim: 512, // Larger than default
        ..Default::default()
    };
    let mut tft = TemporalFusionTransformer::new(config, device.clone())?;

    let static_features = Tensor::zeros((256, 5), DType::F32, &device)?; // Large batch
    let historical_features = Tensor::zeros((256, 60, 210), DType::F32, &device)?;
    let future_features = Tensor::zeros((256, 10, 10), DType::F32, &device)?;

    // Should catch OOM and return error, NOT panic
    let result = tft.forward(&static_features, &historical_features, &future_features);

    if result.is_err() {
        // Verify error is Hardware category
        let err = result.unwrap_err();
        assert!(matches!(err, MLError::ModelError(_)));
    }

    drop(vram_consumer); // Clean up
    Ok(())
}

Test #13: Out-of-Distribution Inputs (Quantized)

File: ml/tests/tft_quantized_ood_test.rs (NEW)

#[test]
fn test_quantized_model_handles_out_of_distribution_inputs() -> Result<(), MLError> {
    let device = Device::Cpu;
    let config = TFTConfig::default();

    // Create quantized TFT
    let fp32_tft = TemporalFusionTransformer::new(config, device.clone())?;
    let quantized_tft = QuantizedTemporalFusionTransformer::from_fp32(&fp32_tft)?;

    // Create input with extreme value (calibration range typically -2.0 to 2.0)
    let mut static_data = vec![1.0f32; 5];
    static_data[2] = 50.0; // WAY outside calibration range
    let static_features = Tensor::from_vec(static_data, (1, 5), &device)?;

    let historical_features = Tensor::zeros((1, 60, 210), DType::F32, &device)?;
    let future_features = Tensor::zeros((1, 10, 10), DType::F32, &device)?;

    // Should NOT panic, should clamp to INT8 range
    let output = quantized_tft.forward(&static_features, &historical_features, &future_features)?;

    // Verify output is valid (no NaN)
    let output_vec = output.to_vec3::<f32>()?;
    let has_nan = output_vec.iter()
        .flat_map(|batch| batch.iter())
        .flat_map(|horizon| horizon.iter())
        .any(|&val| val.is_nan());
    assert!(!has_nan, "Quantized model should produce valid output for OOD inputs");
    Ok(())
}

Test #15: CUDA Initialization Failure Fallback

File: ml/tests/tft_cuda_fallback_test.rs (NEW)

#[test]
fn test_graceful_fallback_on_cuda_initialization_failure() -> Result<(), MLError> {
    // Simulate CUDA unavailable by using CPU device
    let device = Device::Cpu;
    let config = TFTConfig::default();

    // Should succeed on CPU
    let tft = TemporalFusionTransformer::new(config, device.clone())?;

    let static_features = Tensor::zeros((2, 5), DType::F32, &device)?;
    let historical_features = Tensor::zeros((2, 60, 210), DType::F32, &device)?;
    let future_features = Tensor::zeros((2, 10, 10), DType::F32, &device)?;

    let output = tft.forward(&static_features, &historical_features, &future_features)?;
    assert_eq!(output.dims(), &[2, 10, 9]);
    Ok(())
}

#[test]
#[ignore = "Requires CUDA_VISIBLE_DEVICES=-1 environment variable"]
fn test_cuda_fallback_with_env_variable() -> Result<(), MLError> {
    // Run with: CUDA_VISIBLE_DEVICES=-1 cargo test test_cuda_fallback_with_env_variable
    std::env::set_var("CUDA_VISIBLE_DEVICES", "-1");

    // Device::cuda_if_available should fall back to CPU
    let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
    assert!(!device.is_cuda());

    let config = TFTConfig::default();
    let tft = TemporalFusionTransformer::new(config, device)?;

    // Should work fine
    assert!(true);
    Ok(())
}

Test #21: Feature Extraction NaN Handling

File: ml/tests/feature_extraction_nan_test.rs (NEW)

#[test]
fn test_feature_extraction_with_nan_ohlcv() -> Result<(), MLError> {
    use crate::features::extraction::extract_features;

    // Create OHLCV data with NaN
    let mut ohlcv = vec![
        (100.0, 105.0, 98.0, 103.0, 1000.0), // Valid
        (f32::NAN, 106.0, 99.0, 104.0, 1100.0), // NaN open
        (104.0, 107.0, 100.0, 105.0, 1200.0), // Valid
    ];

    // Should detect NaN and return error
    let result = extract_features(&ohlcv);
    assert!(result.is_err());

    match result.unwrap_err() {
        MLError::DataPreprocessing { stage, message } => {
            assert!(message.contains("NaN") || message.contains("invalid"));
        },
        _ => panic!("Expected DataPreprocessing error"),
    }
    Ok(())
}

Phase 2: High-Impact Edge Cases (8 hours)

Test #1: Zero Variance Normalization

File: ml/tests/cuda_compat_zero_variance_test.rs (NEW)

#[test]
fn test_layer_norm_with_zero_variance_input() -> Result<(), MLError> {
    let device = Device::Cpu;

    // Zero variance: all values identical
    let input = Tensor::new(&[[5.0f32, 5.0, 5.0], [2.0, 2.0, 2.0]], &device)?;
    let weight = Tensor::ones(3, DType::F32, &device)?;
    let bias = Tensor::zeros(3, DType::F32, &device)?;

    // Should NOT panic or produce NaN
    let output = cuda_layer_norm(&input, &[3], Some(&weight), Some(&bias), 1e-5)?;

    // Verify no NaN/Inf
    let output_vec = output.to_vec2::<f32>()?;
    for row in &output_vec {
        for &val in row {
            assert!(!val.is_nan(), "Output should not contain NaN");
            assert!(!val.is_infinite(), "Output should not contain Inf");
        }
    }

    // Output should be all zeros (x - mean = 0)
    for row in &output_vec {
        for &val in row {
            assert!(val.abs() < 1e-5, "Output should be near zero");
        }
    }
    Ok(())
}

Test #2: Device Mismatch Handling

File: Add to ml/tests/cuda_compat_device_test.rs (NEW)

#[test]
#[cfg(feature = "cuda")]
#[ignore = "Requires GPU"]
fn test_layer_norm_handles_device_mismatch() -> Result<(), MLError> {
    let cpu_device = Device::Cpu;
    let gpu_device = Device::cuda_if_available(0)?;

    if !gpu_device.is_cuda() {
        return Ok(()); // Skip if no GPU
    }

    // Input on GPU
    let input = Tensor::new(&[[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]], &gpu_device)?;

    // Weight/bias on CPU (device mismatch)
    let weight = Tensor::ones(3, DType::F32, &cpu_device)?;
    let bias = Tensor::zeros(3, DType::F32, &cpu_device)?;

    // Should auto-move weight/bias to GPU, NOT panic
    let output = cuda_layer_norm(&input, &[3], Some(&weight), Some(&bias), 1e-5)?;

    assert_eq!(output.dims(), &[2, 3]);
    assert!(output.device().is_cuda());
    Ok(())
}

Test #7: LRU Cache Eviction

File: ml/tests/tft_lru_cache_test.rs (NEW)

#[test]
fn test_tft_state_lru_cache_eviction_under_load() -> Result<(), MLError> {
    use lru::LruCache;
    use std::num::NonZeroUsize;

    let capacity = NonZeroUsize::new(10).unwrap(); // Small cache for testing
    let mut cache: LruCache<String, Tensor> = LruCache::new(capacity);

    let device = Device::Cpu;

    // Insert 11 items (exceeds capacity of 10)
    for i in 0..11 {
        let key = format!("key_{}", i);
        let tensor = Tensor::zeros((2, 3), DType::F32, &device)?;
        cache.put(key, tensor);
    }

    // First item (key_0) should be evicted
    assert!(cache.get("key_0").is_none(), "LRU item should be evicted");

    // Last 10 items should remain
    for i in 1..11 {
        let key = format!("key_{}", i);
        assert!(cache.get(&key).is_some(), "Recent item should remain");
    }

    Ok(())
}

Test #18: AutoBatchSizer Zero GPU Memory

File: ml/tests/auto_batch_sizer_zero_gpu_test.rs (NEW)

#[test]
fn test_auto_batch_sizer_handles_zero_gpu_memory() -> Result<(), MLError> {
    use crate::memory_optimization::auto_batch_size::{AutoBatchSizer, BatchSizeConfig, OptimizerType};

    // Mock zero GPU memory scenario
    let config = BatchSizeConfig {
        model_memory_mb: 125.0,
        sequence_length: 60,
        feature_dim: 225,
        gradient_checkpointing: false,
        optimizer_type: OptimizerType::Adam,
        safety_margin: 0.20,
        available_memory_mb: 0.0, // ZERO available memory
    };

    // Should return minimum batch_size=1, NOT panic
    let batch_size = calculate_batch_size(&config)?;
    assert_eq!(batch_size, 1, "Should return minimum batch size");
    Ok(())
}

Phase 3: Robustness Improvements (10 hours)

Tests #3, #4, #10, #14, #16, #19, #20

(Templates similar to above, focusing on:)

  • F64 CPU fallback
  • Sigmoid extreme values
  • Shared VarMap guard
  • Zero variance quantization
  • Thread-safe metrics
  • Batch > data size
  • Single sample DBN

Phase 4: Validation (2 hours)

Tests #12, #17

(Confirm existing guardrails work)


Test File Organization

ml/tests/
├── tft_zero_batch_test.rs              # Test #5
├── tft_batch_mismatch_test.rs          # Test #6
├── tft_nan_handling_test.rs            # Test #8
├── tft_corrupt_checkpoint_test.rs      # Test #9
├── tft_gpu_oom_test.rs                 # Test #11
├── tft_quantized_ood_test.rs           # Test #13
├── tft_cuda_fallback_test.rs           # Test #15
├── feature_extraction_nan_test.rs      # Test #21
├── cuda_compat_zero_variance_test.rs   # Test #1
├── cuda_compat_device_test.rs          # Test #2
├── tft_lru_cache_test.rs               # Test #7
├── auto_batch_sizer_zero_gpu_test.rs   # Test #18
└── ...

Running Tests

# Run all new tests
cargo test -p ml --test tft_zero_batch_test
cargo test -p ml --test tft_batch_mismatch_test
cargo test -p ml --test tft_nan_handling_test

# Run GPU-specific tests (requires GPU)
cargo test -p ml --test tft_gpu_oom_test --features cuda -- --ignored

# Run all ml tests
cargo test -p ml

# Run with verbose output
cargo test -p ml -- --nocapture

Common Test Utilities

Add to ml/tests/common/mod.rs:

pub fn create_test_tft(device: Device) -> Result<TemporalFusionTransformer, MLError> {
    let config = TFTConfig::default();
    TemporalFusionTransformer::new(config, device)
}

pub fn create_zero_batch_inputs(device: &Device) -> Result<(Tensor, Tensor, Tensor), MLError> {
    let static_features = Tensor::zeros((0, 5), DType::F32, device)?;
    let historical_features = Tensor::zeros((0, 60, 210), DType::F32, device)?;
    let future_features = Tensor::zeros((0, 10, 10), DType::F32, device)?;
    Ok((static_features, historical_features, future_features))
}

pub fn assert_no_nan_inf(tensor: &Tensor) -> Result<(), MLError> {
    let vec = tensor.flatten_all()?.to_vec1::<f32>()?;
    for &val in &vec {
        assert!(!val.is_nan(), "Tensor contains NaN");
        assert!(!val.is_infinite(), "Tensor contains Inf");
    }
    Ok(())
}

Time Estimates (Conservative)

Phase Tests Hours/Test Total
Phase 1 8 2.0 16h
Phase 2 4 2.0 8h
Phase 3 7 1.5 10.5h
Phase 4 2 1.0 2h
TOTAL 21 1.75 avg 36.5h

Realistic Timeline (1 developer, part-time):

  • Week 1: Phase 1 (8 tests, 16h)
  • Week 2: Phase 2 (4 tests, 8h)
  • Week 3: Phase 3 (7 tests, 10.5h)
  • Week 4: Phase 4 (2 tests, 2h)

Success Criteria

  • All 21 tests pass on first run
  • No test introduces new unwrap/expect calls
  • GPU tests pass on CUDA-enabled systems
  • CPU fallback tests pass on non-GPU systems
  • Test execution time < 5 minutes total (excluding GPU OOM test)
  • Code coverage increases by ≥5%

Generated by: Agent 23 Date: 2025-10-25 Next Step: Implement Phase 1 tests before Runpod deployment