Files
foxhunt/ml/tests/wave_d_memory_stress_test.rs
jgrusewski 83629f9ca8 feat(deployment): Complete Runpod GPU deployment infrastructure
Implement comprehensive Runpod deployment with S3 volume mount architecture for
FP32 ML model training on Tesla V100 GPUs.

## Infrastructure Components

### Deployment Scripts (scripts/)
- runpod_deploy.sh: Master deployment orchestrator (8-step workflow)
- runpod_upload.sh: S3 upload for binaries and test data
- upload_env_to_runpod.sh: Secure .env credentials upload
- runpod_deploy_test.sh: Prerequisites validation

### Docker Configuration
- Dockerfile.runpod: Multi-stage CUDA 12.1 runtime (~2GB, no binaries)
- entrypoint.sh: Volume verification and training execution
- Architecture: Volume mount (NO S3 downloads in pods)

### S3 Configuration
- Bucket: se3zdnb5o4 (Iceland region: eur-is-1)
- Endpoint: https://s3api-eur-is-1.runpod.io
- Structure: binaries/, test_data/, models/, .env

### OpenTofu Infrastructure (terraform/runpod/)
- main.tf: Pod and volume resources
- variables.tf: Configuration variables
- outputs.tf: Pod connection info
- Security: NO credentials in state (uses volume .env)

## Deployment Assets Uploaded

### Training Binaries (77MB)
- train_tft_parquet (23M) - TFT-225 features
- train_mamba2_parquet (22M) - MAMBA-2 state space
- train_dqn (22M) - Deep Q-Network
- train_ppo (13M) - Proximal Policy Optimization

### Test Data (13.8 MB)
- 9 Parquet files: ES.FUT, NQ.FUT, 6E.FUT, ZN.FUT (180-day datasets)

### Credentials
- .env file (1.5 KB, private access, chmod 600)

## Documentation

### Deployment Guides
- RUNPOD_DEPLOYMENT_READY_SUMMARY.md: Complete deployment status
- RUNPOD_VOLUME_DEPLOYMENT_GUIDE.md: Step-by-step guide (42KB)
- RUNPOD_DEPLOYMENT_QUICK_START.md: Quick reference
- RUNPOD_UPLOAD_GUIDE.md: S3 upload instructions
- RUNPOD_VOLUME_CONFIGURATION_COMPLETE.md: S3 setup report
- RUNPOD_S3_PARQUET_UPLOAD_REPORT.md: Data upload verification

### Architecture Documentation
- RUNPOD_VOLUME_MOUNT_ARCHITECTURE.md: Volume mount design
- RUNPOD_S3_ARCHITECTURE_DIAGRAM.txt: S3 API vs filesystem access
- DOCKERFILE_RUNPOD_FINAL_SUMMARY.md: Docker image specification

### Decision Documentation
- RUNPOD_DEPLOYMENT_CHECKLIST.md: Go/no-go decision matrix (27KB)
- RUNPOD_DEPLOYMENT_DECISION_TREE.md: Decision workflow
- FP32_RUNPOD_DEPLOYMENT_READY.md: FP32 deployment readiness

## QAT Enhancements

### Core QAT Infrastructure
- ml/src/memory_optimization/qat.rs: Enhanced QAT observer (+226 lines)
- ml/src/memory_optimization/auto_batch_size.rs: OOM recovery (+84 lines)
- ml/src/tft/qat_tft.rs: QAT TFT wrapper (+154 lines)
- ml/src/trainers/tft.rs: QAT training integration (+433 lines)
- ml/src/qat_metrics_exporter.rs: NEW - QAT metrics export

### QAT Testing
- ml/tests/qat_integration_tests.rs: NEW - Integration test suite
- ml/tests/qat_gradient_clipping_test.rs: NEW - Gradient clipping tests
- ml/tests/qat_device_consistency_test.rs: Device mismatch tests (+205 lines)
- ml/tests/qat_accuracy_validation_test.rs: Accuracy validation
- ml/tests/qat_tft_integration_test.rs: TFT QAT integration

### QAT Documentation
- ml/docs/QAT_GUIDE.md: Comprehensive QAT guide (+616 lines)
- ml/docs/QAT_GRADIENT_CHECKPOINTING_WORKAROUND.md: NEW - Workaround guide
- QAT_BLOCKERS_ROOT_CAUSE_ANALYSIS.md: P0 blocker analysis (44KB)
- QAT_ACCURACY_VALIDATION_REPORT.md: Accuracy comparison
- QAT_GRADIENT_CLIPPING_VALIDATION_REPORT.md: Clipping validation

### QAT Monitoring
- config/grafana/dashboards/qat-training-metrics.json: NEW - Grafana dashboard

## AWS CLI Configuration

### Credentials Setup
- ~/.aws/credentials: Runpod profile configured
  - Access Key: user_2xxA3XcIFj16yfL3aBon9niiSpr
  - Secret Key: (from RUNPOD_S3_SECRET)
- ~/.aws/config: Iceland region (eur-is-1)

## Production Readiness

### FP32 Models:  READY FOR DEPLOYMENT
- DQN: 15-20s training, ~6MB GPU memory
- PPO: 7-10s training, ~145MB GPU memory
- MAMBA-2: 2-3 min training, ~164MB GPU memory
- TFT-225: 3-5 min training, ~500MB GPU memory
- Total GPU Budget: 815MB (fits on 4GB+ Tesla V100)

### QAT Models: 🔴 BLOCKED
- 24 tests implemented but DO NOT COMPILE (11 errors)
- 3 P0 blockers: device mismatch, gradient checkpointing, OOM recovery
- Timeline: 1-2 weeks to fix (13h P0 fixes + validation)

### Wave D Features:  OPERATIONAL
- 225 features fully integrated
- Feature extraction: 5.10μs/bar (196x faster than target)
- Wave D backtest: Sharpe 2.00, Win Rate 60%, Drawdown 15%
- Database migration 045: Applied cleanly, zero conflicts

## Cost Analysis

### One-Time Setup
- Network Volume: $4/month (50GB SSD)
- Upload costs: FREE (S3 API included)

### Per Training Run (TFT-225)
- GPU: Tesla V100-PCIE-16GB @ $0.29/hr
- Training Time: ~4 hours
- Cost per run: $1.16

### Monthly (20 Training Runs)
- Storage: $4.00/month
- Training: $23.20/month (20 runs × $1.16)
- Total: $27.20/month

## Security

### Credentials Management
-  NO credentials in Docker image
-  NO credentials in Terraform state
-  .env gitignored and not committed
-  .env file private on S3 (HTTP 401 on public access)
-  Docker Hub repository PRIVATE (jgrusewski/foxhunt)

### Access Control
- S3 API: Local client uploads only
- Volume mount: Pod filesystem access only
- Authentication: AWS CLI with Runpod profile required

## Next Steps

1.  COMPLETE: Build Docker image
2.  PENDING: Push to Docker Hub
3.  PENDING: Deploy pod via Runpod console
4.  PENDING: Validate training on Tesla V100

## Performance Targets

- Build time: 5-10 min
- Upload time: ~20 sec (90MB total)
- Pod startup: ~30 sec
- Training time: 3-5 min (TFT-225)
- Total deployment: ~40 min from start to first training run

## Test Status

- FP32 tests: 597/608 passing (98.2%)
- QAT tests: 0/24 passing (compilation errors)
- Overall: 2,062/2,086 passing (98.8% excluding QAT)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 01:11:43 +02:00

450 lines
15 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.
//! Wave D Memory Stress Test - 100K+ Symbol Simulation
//!
//! Agent D27: Validates memory scalability and leak detection for production-scale deployments.
//!
//! ## Test Scenario
//! - Simulate 100,000 concurrent symbols (realistic for multi-exchange production)
//! - Each symbol maintains a FeatureExtractionPipeline instance
//! - Generate synthetic OHLCV data (1000 bars per symbol)
//! - Run for 10,000 update cycles
//!
//! ## Memory Targets
//! - Expected: 100K symbols × 4.6KB = 460MB
//! - Maximum allowed: 500MB
//! - No memory leaks: stable after initial allocation
//!
//! ## Success Criteria
//! - ✅ Memory usage <500MB for 100K symbols
//! - ✅ No memory leaks detected (stable RSS)
//! - ✅ Linear scaling O(n) with symbol count
//! - ✅ GC pressure minimal (<1% CPU)
use chrono::Utc;
use ml::features::extraction::OHLCVBar;
use ml::features::pipeline::{FeatureConfig, FeatureExtractionPipeline};
use std::collections::HashMap;
use std::time::{Duration, Instant};
use sysinfo::System;
/// Memory checkpoint for tracking allocations
#[derive(Debug, Clone)]
struct MemoryCheckpoint {
timestamp: Instant,
symbol_count: usize,
rss_bytes: u64,
virtual_bytes: u64,
available_bytes: u64,
}
impl MemoryCheckpoint {
fn capture(sys: &System, symbol_count: usize, start: Instant) -> Self {
let pid = sysinfo::get_current_pid().expect("Failed to get PID");
let process = sys.process(pid).expect("Process not found");
Self {
timestamp: start,
symbol_count,
rss_bytes: process.memory(),
virtual_bytes: process.virtual_memory(),
available_bytes: sys.available_memory(),
}
}
fn memory_per_symbol(&self) -> f64 {
if self.symbol_count == 0 {
0.0
} else {
self.rss_bytes as f64 / self.symbol_count as f64
}
}
}
/// Generate synthetic OHLCV bar for testing
fn generate_synthetic_bar(base_price: f64, index: usize) -> OHLCVBar {
let open = base_price + (index as f64 * 0.1) % 10.0;
let high = open + 0.5;
let low = open - 0.5;
let close = open + (index as f64 * 0.05) % 1.0;
let volume = 1000.0 + (index as f64 * 10.0) % 500.0;
OHLCVBar {
timestamp: Utc::now(),
open,
high,
low,
close,
volume,
}
}
/// Stress test metrics
#[derive(Debug)]
struct StressTestMetrics {
start_time: Instant,
end_time: Instant,
total_symbols: usize,
total_updates: u64,
checkpoints: Vec<MemoryCheckpoint>,
warmup_duration: Duration,
stress_duration: Duration,
}
impl StressTestMetrics {
fn new() -> Self {
let now = Instant::now();
Self {
start_time: now,
end_time: now,
total_symbols: 0,
total_updates: 0,
checkpoints: Vec::new(),
warmup_duration: Duration::ZERO,
stress_duration: Duration::ZERO,
}
}
fn memory_growth(&self) -> f64 {
if self.checkpoints.len() < 2 {
return 0.0;
}
let first = &self.checkpoints[0];
let last = &self.checkpoints[self.checkpoints.len() - 1];
((last.rss_bytes as f64 - first.rss_bytes as f64) / first.rss_bytes as f64) * 100.0
}
fn memory_leak_detected(&self) -> bool {
// Check if memory grew >5% after initial allocation stabilized
if self.checkpoints.len() < 4 {
return false;
}
// Compare middle checkpoint (after warmup) to final checkpoint
let mid_idx = self.checkpoints.len() / 2;
let mid = &self.checkpoints[mid_idx];
let last = &self.checkpoints[self.checkpoints.len() - 1];
let growth =
((last.rss_bytes as f64 - mid.rss_bytes as f64) / mid.rss_bytes as f64) * 100.0;
growth > 5.0
}
fn print_summary(&self) {
println!("\n{}", "=".repeat(80));
println!("Wave D Memory Stress Test - Summary");
println!("{}", "=".repeat(80));
println!("Total Symbols: {}", self.total_symbols);
println!("Total Updates: {}", self.total_updates);
println!("Warmup Duration: {:?}", self.warmup_duration);
println!("Stress Duration: {:?}", self.stress_duration);
println!("Total Duration: {:?}", self.end_time - self.start_time);
println!("\nMemory Checkpoints:");
println!("{}", "-".repeat(80));
println!(
"{:<15} {:<15} {:<15} {:<15}",
"Symbols", "RSS (MB)", "Virtual (MB)", "Per Symbol (KB)"
);
println!("{}", "-".repeat(80));
for checkpoint in &self.checkpoints {
println!(
"{:<15} {:<15.2} {:<15.2} {:<15.2}",
checkpoint.symbol_count,
checkpoint.rss_bytes as f64 / 1_048_576.0,
checkpoint.virtual_bytes as f64 / 1_048_576.0,
checkpoint.memory_per_symbol() / 1024.0
);
}
println!("{}", "-".repeat(80));
println!("\nMemory Analysis:");
println!(" Memory Growth: {:.2}%", self.memory_growth());
println!(
" Leak Detected: {}",
if self.memory_leak_detected() {
"❌ YES"
} else {
"✅ NO"
}
);
if let Some(last) = self.checkpoints.last() {
let final_mb = last.rss_bytes as f64 / 1_048_576.0;
let target_mb = 500.0;
println!(" Final RSS: {:.2} MB", final_mb);
println!(" Target: {:.2} MB", target_mb);
println!(
" Status: {}",
if final_mb <= target_mb {
"✅ PASS"
} else {
"❌ FAIL"
}
);
}
println!("{}\n", "=".repeat(80));
}
}
#[test]
#[ignore = "Expensive test - run explicitly with: cargo test wave_d_memory_stress_100k_symbols -- --ignored --nocapture"]
fn wave_d_memory_stress_100k_symbols() {
println!("\n🚀 Starting Wave D Memory Stress Test - 100K Symbols");
println!("Target: <500MB memory usage, no leaks, linear scaling\n");
let mut metrics = StressTestMetrics::new();
let mut sys = System::new_all();
sys.refresh_all();
// Capture baseline memory
let baseline = MemoryCheckpoint::capture(&sys, 0, Instant::now());
metrics.checkpoints.push(baseline.clone());
println!(
"📊 Baseline RSS: {:.2} MB",
baseline.rss_bytes as f64 / 1_048_576.0
);
// Phase 1: Allocate 100K pipelines (simulating 100K symbols)
const TOTAL_SYMBOLS: usize = 100_000;
const CHECKPOINT_INTERVALS: [usize; 4] = [1_000, 10_000, 50_000, 100_000];
const WARMUP_BARS: usize = 50;
println!(
"\n🔧 Phase 1: Allocating {} FeatureExtractionPipeline instances...",
TOTAL_SYMBOLS
);
let phase1_start = Instant::now();
let config = FeatureConfig {
enable_price: true,
enable_volume: true,
enable_time: true,
enable_indicators: true,
enable_microstructure: true,
enable_statistical: true,
warmup_bars: WARMUP_BARS,
};
let mut pipelines: HashMap<String, FeatureExtractionPipeline> =
HashMap::with_capacity(TOTAL_SYMBOLS);
for i in 0..TOTAL_SYMBOLS {
let symbol = format!("SYM{:06}", i);
let pipeline = FeatureExtractionPipeline::with_config(config.clone());
pipelines.insert(symbol, pipeline);
// Memory checkpoints
if CHECKPOINT_INTERVALS.contains(&(i + 1)) {
sys.refresh_all();
let checkpoint = MemoryCheckpoint::capture(&sys, i + 1, phase1_start);
metrics.checkpoints.push(checkpoint.clone());
println!(
"{} symbols allocated: RSS {:.2} MB ({:.2} KB/symbol)",
checkpoint.symbol_count,
checkpoint.rss_bytes as f64 / 1_048_576.0,
checkpoint.memory_per_symbol() / 1024.0
);
}
// Progress indicator every 10K
if (i + 1) % 10_000 == 0 && !CHECKPOINT_INTERVALS.contains(&(i + 1)) {
println!(" ... {} symbols allocated", i + 1);
}
}
metrics.total_symbols = TOTAL_SYMBOLS;
metrics.warmup_duration = phase1_start.elapsed();
println!(
"✓ Phase 1 Complete: {} symbols in {:?}",
TOTAL_SYMBOLS, metrics.warmup_duration
);
// Phase 2: Warmup (feed 50 bars to each pipeline)
println!(
"\n🔥 Phase 2: Warming up pipelines ({} bars per symbol)...",
WARMUP_BARS
);
let phase2_start = Instant::now();
for (symbol, pipeline) in pipelines.iter_mut() {
let base_price = 100.0 + (symbol.chars().last().unwrap() as u32 as f64);
for bar_idx in 0..WARMUP_BARS {
let bar = generate_synthetic_bar(base_price, bar_idx);
pipeline.update(&bar);
}
}
let warmup_elapsed = phase2_start.elapsed();
println!(
"✓ Phase 2 Complete: Warmup finished in {:?}",
warmup_elapsed
);
// Capture post-warmup memory
sys.refresh_all();
let post_warmup = MemoryCheckpoint::capture(&sys, TOTAL_SYMBOLS, phase2_start);
metrics.checkpoints.push(post_warmup.clone());
println!(
" RSS after warmup: {:.2} MB ({:.2} KB/symbol)",
post_warmup.rss_bytes as f64 / 1_048_576.0,
post_warmup.memory_per_symbol() / 1024.0
);
// Phase 3: Stress test (10K update cycles on all symbols)
const UPDATE_CYCLES: usize = 10_000;
const CYCLE_CHECKPOINTS: [usize; 5] = [1_000, 2_500, 5_000, 7_500, 10_000];
println!(
"\n💪 Phase 3: Stress testing with {} update cycles...",
UPDATE_CYCLES
);
let phase3_start = Instant::now();
for cycle in 0..UPDATE_CYCLES {
// Update all pipelines in this cycle
for (symbol, pipeline) in pipelines.iter_mut() {
let base_price = 100.0 + (symbol.chars().last().unwrap() as u32 as f64);
let bar = generate_synthetic_bar(base_price, WARMUP_BARS + cycle);
pipeline.update(&bar);
metrics.total_updates += 1;
}
// Memory checkpoints
if CYCLE_CHECKPOINTS.contains(&(cycle + 1)) {
sys.refresh_all();
let checkpoint = MemoryCheckpoint::capture(&sys, TOTAL_SYMBOLS, phase3_start);
metrics.checkpoints.push(checkpoint.clone());
println!(
" ✓ Cycle {}/{}: RSS {:.2} MB ({:.2} KB/symbol)",
cycle + 1,
UPDATE_CYCLES,
checkpoint.rss_bytes as f64 / 1_048_576.0,
checkpoint.memory_per_symbol() / 1024.0
);
}
// Progress indicator every 1000 cycles
if (cycle + 1) % 1_000 == 0 && !CYCLE_CHECKPOINTS.contains(&(cycle + 1)) {
println!(" ... Cycle {}/{}", cycle + 1, UPDATE_CYCLES);
}
}
metrics.stress_duration = phase3_start.elapsed();
metrics.end_time = Instant::now();
println!(
"✓ Phase 3 Complete: {} update cycles in {:?}",
UPDATE_CYCLES, metrics.stress_duration
);
// Final memory capture
sys.refresh_all();
let final_checkpoint = MemoryCheckpoint::capture(&sys, TOTAL_SYMBOLS, phase3_start);
metrics.checkpoints.push(final_checkpoint.clone());
// Print comprehensive summary
metrics.print_summary();
// Assertions
let final_mb = final_checkpoint.rss_bytes as f64 / 1_048_576.0;
assert!(
final_mb <= 500.0,
"Memory usage exceeded 500MB target: {:.2} MB",
final_mb
);
assert!(
!metrics.memory_leak_detected(),
"Memory leak detected: RSS grew >5% after warmup stabilization"
);
// Check linear scaling (memory per symbol should be consistent)
if metrics.checkpoints.len() >= 3 {
let first = &metrics.checkpoints[1]; // After 1K symbols
let last = &metrics.checkpoints[metrics.checkpoints.len() - 1];
let first_per_symbol = first.memory_per_symbol();
let last_per_symbol = last.memory_per_symbol();
let variance_pct = ((last_per_symbol - first_per_symbol).abs() / first_per_symbol) * 100.0;
println!(
"Linear Scaling Check: {:.2} KB/symbol (1K) vs {:.2} KB/symbol (100K) = {:.2}% variance",
first_per_symbol / 1024.0,
last_per_symbol / 1024.0,
variance_pct
);
assert!(
variance_pct < 20.0,
"Memory per symbol variance too high: {:.2}% (expected <20%)",
variance_pct
);
}
println!("\n✅ Wave D Memory Stress Test: PASSED");
println!(" - Memory usage: {:.2} MB / 500 MB target", final_mb);
println!(" - No memory leaks detected");
println!(" - Linear scaling confirmed");
}
#[test]
fn wave_d_memory_scaling_small() {
// Quick test with 1K symbols for CI/CD (non-ignored)
println!("\n🧪 Wave D Memory Scaling Test - 1K Symbols (Quick)");
let mut sys = System::new_all();
sys.refresh_all();
let baseline = MemoryCheckpoint::capture(&sys, 0, Instant::now());
println!(
"Baseline RSS: {:.2} MB",
baseline.rss_bytes as f64 / 1_048_576.0
);
const SYMBOLS: usize = 1_000;
let config = FeatureConfig::default();
let mut pipelines: Vec<FeatureExtractionPipeline> = Vec::with_capacity(SYMBOLS);
// Allocate 1K pipelines
for _ in 0..SYMBOLS {
pipelines.push(FeatureExtractionPipeline::with_config(config.clone()));
}
// Warmup each pipeline
for (i, pipeline) in pipelines.iter_mut().enumerate() {
let base_price = 100.0 + i as f64;
for bar_idx in 0..50 {
let bar = generate_synthetic_bar(base_price, bar_idx);
pipeline.update(&bar);
}
}
sys.refresh_all();
let after = MemoryCheckpoint::capture(&sys, SYMBOLS, Instant::now());
// Calculate delta memory (excluding baseline process overhead)
let delta_mb = (after.rss_bytes as i64 - baseline.rss_bytes as i64) as f64 / 1_048_576.0;
let per_symbol_kb = (delta_mb * 1024.0) / SYMBOLS as f64;
println!(
"After 1K symbols: Total {:.2} MB, Delta {:.2} MB ({:.2} KB/symbol)",
after.rss_bytes as f64 / 1_048_576.0,
delta_mb,
per_symbol_kb
);
// For 1K symbols, expect <50MB delta (50KB per symbol including overhead)
assert!(
delta_mb <= 50.0,
"Memory delta too high for 1K symbols: {:.2} MB (expected <50MB)",
delta_mb
);
// Verify each symbol uses reasonable memory (expected ~4.6KB, allow up to 50KB with overhead)
assert!(
per_symbol_kb <= 50.0,
"Memory per symbol too high: {:.2} KB (expected <50KB)",
per_symbol_kb
);
println!("✅ Small-scale memory test: PASSED");
}