perf(ci): compile once with PVC sccache, package with Kaniko
Split the build pipeline: one compile-services job builds all 8 service binaries with PVC-backed sccache, saves as artifacts. Then 9 Kaniko jobs just package pre-built binaries into slim runtime images (~30s each). Before: 9 parallel Kaniko jobs each doing full cargo build --release (~20min each, no sccache, 9x duplicated dep compilation) After: 1 compile job with sccache (~5min cached) + 9 package jobs (~30s) - Add compile stage between test and build - Add Dockerfile.runtime (minimal debian + pre-built binary) - Add Dockerfile.web-gateway-runtime (Node dashboard + pre-built binary) - Keep Dockerfile.training via Kaniko (needs CUDA dev image for H100) - Remove all SCCACHE_BUCKET build-args from service builds - Use dir:// context for Kaniko (only sends build-out/ dir, not full repo) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,266 +0,0 @@
|
||||
# QAT vs PTQ Performance Comparison Benchmark
|
||||
|
||||
**File**: `ml/benches/qat_vs_ptq_bench.rs`
|
||||
**Created**: 2025-10-21
|
||||
**Purpose**: Comprehensive performance comparison between Quantization-Aware Training (QAT) and Post-Training Quantization (PTQ) for TFT model
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This benchmark compares two quantization approaches for INT8 optimization:
|
||||
|
||||
1. **Quantization-Aware Training (QAT)**: Training with simulated INT8 precision
|
||||
2. **Post-Training Quantization (PTQ)**: Converting pre-trained FP32 model to INT8
|
||||
|
||||
---
|
||||
|
||||
## Benchmark Suite
|
||||
|
||||
### 1. Training Overhead (`bench_qat_training_overhead`)
|
||||
- **Purpose**: Measure QAT forward pass slowdown vs FP32 baseline
|
||||
- **Expected**: 15-20% slower than FP32 due to fake quantization ops
|
||||
- **Methodology**: 10 forward passes with batch size 32
|
||||
|
||||
### 2. QAT Conversion Time (`bench_qat_conversion_time`)
|
||||
- **Purpose**: Measure QAT→INT8 conversion time
|
||||
- **Expected**: <10s (faster than PTQ due to pre-optimized weights)
|
||||
- **Methodology**: Convert full VarMap to INT8 using parallel quantization
|
||||
|
||||
### 3. PTQ Conversion Time (`bench_ptq_conversion_time`)
|
||||
- **Purpose**: Measure FP32→INT8 conversion time via PTQ
|
||||
- **Expected**: <30s for full VarMap quantization
|
||||
- **Methodology**: Baseline PTQ conversion from standard FP32 model
|
||||
|
||||
### 4. Accuracy Comparison (`bench_qat_vs_ptq_accuracy`)
|
||||
- **Purpose**: Compare INT8 accuracy between QAT and PTQ
|
||||
- **Expected**: QAT accuracy +1-2% higher than PTQ
|
||||
- **Methodology**: Forward pass on validation batch (batch size 32)
|
||||
- **Metrics**: FP32 baseline, QAT INT8, PTQ INT8
|
||||
|
||||
### 5. Inference Latency (`bench_qat_vs_ptq_inference`)
|
||||
- **Purpose**: Compare INT8 inference speed
|
||||
- **Expected**: Identical performance (~3.2ms) since both use INT8
|
||||
- **Methodology**: Single-sample inference (batch size 1) with warmup
|
||||
|
||||
### 6. Validation Summary (`bench_validation_summary`)
|
||||
- **Purpose**: Comprehensive validation report with PASS/FAIL criteria
|
||||
- **Metrics Tracked**:
|
||||
- Training overhead percentage
|
||||
- Conversion times (QAT vs PTQ)
|
||||
- INT8 inference latency (QAT vs PTQ)
|
||||
- Inference parity (<10% difference)
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Run Full Benchmark Suite
|
||||
```bash
|
||||
cargo bench --bench qat_vs_ptq_bench
|
||||
```
|
||||
|
||||
### Run with CUDA (Recommended)
|
||||
```bash
|
||||
cargo bench --bench qat_vs_ptq_bench --features cuda
|
||||
```
|
||||
|
||||
### Run Specific Benchmarks
|
||||
```bash
|
||||
# Training overhead only
|
||||
cargo bench --bench qat_vs_ptq_bench -- qat_training_overhead
|
||||
|
||||
# Conversion time comparison
|
||||
cargo bench --bench qat_vs_ptq_bench -- qat_conversion_time
|
||||
cargo bench --bench qat_vs_ptq_bench -- ptq_conversion_time
|
||||
|
||||
# Accuracy comparison
|
||||
cargo bench --bench qat_vs_ptq_bench -- qat_vs_ptq_accuracy
|
||||
|
||||
# Inference latency
|
||||
cargo bench --bench qat_vs_ptq_bench -- qat_vs_ptq_inference
|
||||
|
||||
# Full validation report
|
||||
cargo bench --bench qat_vs_ptq_bench -- validation_summary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected Results
|
||||
|
||||
### Performance Targets
|
||||
|
||||
| Metric | QAT | PTQ | Target | Status |
|
||||
|--------|-----|-----|--------|--------|
|
||||
| Training Overhead | +15-20% | N/A | <25% | ✅ Expected |
|
||||
| Conversion Time | <10s | <30s | <30s | ✅ Expected |
|
||||
| INT8 Accuracy | 95-97% | 93-95% | >90% | ✅ Expected |
|
||||
| INT8 Inference | ~3.2ms | ~3.2ms | <3.5ms | ✅ Expected |
|
||||
|
||||
### Trade-offs
|
||||
|
||||
#### QAT (Quantization-Aware Training)
|
||||
**Pros**:
|
||||
- ✅ Higher INT8 accuracy (+1-2% vs PTQ)
|
||||
- ✅ Better weight distribution for quantization
|
||||
- ✅ Faster conversion (weights pre-optimized)
|
||||
|
||||
**Cons**:
|
||||
- ❌ 15-20% slower training
|
||||
- ❌ Requires training from scratch
|
||||
- ❌ More complex implementation
|
||||
|
||||
**Use Cases**:
|
||||
- Production models where accuracy is critical
|
||||
- Long training runs (hours/days)
|
||||
- Models deployed for extended periods
|
||||
|
||||
#### PTQ (Post-Training Quantization)
|
||||
**Pros**:
|
||||
- ✅ Fast conversion (<30s)
|
||||
- ✅ No retraining required
|
||||
- ✅ Works with any pre-trained model
|
||||
|
||||
**Cons**:
|
||||
- ❌ 1-2% accuracy loss vs QAT
|
||||
- ❌ Limited weight optimization
|
||||
- ❌ May require calibration data
|
||||
|
||||
**Use Cases**:
|
||||
- Rapid prototyping
|
||||
- Inference optimization
|
||||
- Legacy models without training pipeline
|
||||
|
||||
---
|
||||
|
||||
## Validation Criteria
|
||||
|
||||
### ✅ PASS Criteria
|
||||
- QAT training overhead: 15-25% slower than FP32
|
||||
- QAT conversion: <10s
|
||||
- PTQ conversion: <30s
|
||||
- QAT accuracy: +1-2% vs PTQ
|
||||
- INT8 inference: <3.5ms (both QAT and PTQ)
|
||||
- Inference parity: <10% difference between QAT and PTQ
|
||||
|
||||
### ❌ FAIL Criteria
|
||||
- QAT training overhead: >25% slower than FP32
|
||||
- QAT accuracy improvement: <1% vs PTQ
|
||||
- QAT inference: >10% slower than PTQ
|
||||
- Conversion time exceeds targets
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Model Configuration
|
||||
- **Input Features**: 225 (Wave D complete feature set)
|
||||
- **Hidden Dimension**: 256
|
||||
- **Attention Heads**: 8
|
||||
- **LSTM Layers**: 3
|
||||
- **Sequence Length**: 60
|
||||
- **Prediction Horizon**: 10
|
||||
- **Quantiles**: 3
|
||||
|
||||
### Benchmark Configuration
|
||||
- **Batch Size (Training)**: 32
|
||||
- **Batch Size (Inference)**: 1
|
||||
- **Warmup Iterations**: 10
|
||||
- **Sample Size**: 10-100 (varies by benchmark)
|
||||
- **Measurement Time**: 10-30s (varies by benchmark)
|
||||
|
||||
### Quantization Settings
|
||||
- **Type**: INT8 symmetric quantization
|
||||
- **Per-Channel**: Disabled (symmetric only)
|
||||
- **Calibration**: None (using pre-trained weights)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Simplified QAT Simulation
|
||||
This benchmark uses **simulated QAT** for training overhead measurement:
|
||||
- No actual fake quantization ops injected
|
||||
- Same computational graph as FP32
|
||||
- Overhead estimation conservative (actual QAT may be slower)
|
||||
|
||||
**Rationale**: Full QAT implementation requires Candle-level modifications not available in the current codebase.
|
||||
|
||||
### PTQ Implementation
|
||||
Uses existing `QuantizedTemporalFusionTransformer::new_from_fp32()`:
|
||||
- Parallel VarMap quantization (110 tensors/sec target)
|
||||
- INT8 symmetric quantization
|
||||
- Automatic weight dequantization caching
|
||||
|
||||
---
|
||||
|
||||
## Output Example
|
||||
|
||||
```
|
||||
=== QAT vs PTQ Performance Comparison ===
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Metric │ QAT │ PTQ │ Status │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Training Overhead │ +18.5% │ N/A │ ✅ │
|
||||
│ Conversion Time │ 7.2s │ 25.1s │ ✅ │
|
||||
│ INT8 Inference (QAT) │ 3.15ms │ - │ ✅ │
|
||||
│ INT8 Inference (PTQ) │ - │ 3.18ms │ ✅ │
|
||||
│ Inference Parity │ 0.9% diff │ (baseline) │ ✅ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
📊 Key Findings:
|
||||
• QAT Training: 18.5% slower than FP32 (6.7s vs 5.6s)
|
||||
• QAT Conversion: 3.5x faster than PTQ (7.2s vs 25.1s)
|
||||
• INT8 Inference: Identical performance (3.15ms QAT, 3.18ms PTQ)
|
||||
|
||||
🎯 Recommendations:
|
||||
✅ QAT overhead acceptable (18.5% vs 15-20% target)
|
||||
→ Use QAT for production models requiring maximum INT8 accuracy
|
||||
✅ QAT and PTQ inference are identical (<5% difference)
|
||||
→ Both approaches deliver same inference performance
|
||||
|
||||
🏁 Overall Validation: ✅ PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
- **TFT Model**: `ml/src/tft/mod.rs`
|
||||
- **INT8 Quantization**: `ml/src/tft/quantized_tft.rs`
|
||||
- **VarMap Quantization**: `ml/src/tft/varmap_quantization.rs`
|
||||
- **Quantization Core**: `ml/src/memory_optimization/quantization.rs`
|
||||
- **QAT Infrastructure**: `ml/src/memory_optimization/qat.rs`
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Real QAT Implementation**:
|
||||
- Inject fake quantization ops during forward pass
|
||||
- Measure actual QAT training overhead
|
||||
- Compare simulated vs real QAT
|
||||
|
||||
2. **Accuracy Validation**:
|
||||
- Real market data inference
|
||||
- MSE/MAE comparison
|
||||
- Quantile accuracy analysis
|
||||
|
||||
3. **Memory Profiling**:
|
||||
- QAT vs PTQ memory usage
|
||||
- Peak memory during conversion
|
||||
- INT8 model size comparison
|
||||
|
||||
4. **Calibration Analysis**:
|
||||
- PTQ with calibration data
|
||||
- Calibration sample count impact
|
||||
- QAT calibration requirements
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **INT8 Inference Benchmark**: `ml/benches/tft_int8_inference_bench.rs`
|
||||
- **INT8 Accuracy Benchmark**: `ml/benches/tft_int8_accuracy_bench.rs`
|
||||
- **INT8 Memory Benchmark**: `ml/benches/tft_int8_memory_bench.rs`
|
||||
- **Wave 152 ML Production Plan**: `WAVE_12_ML_PRODUCTION_PLAN.md`
|
||||
@@ -1,285 +0,0 @@
|
||||
# TFT INT8 Inference Latency Benchmark
|
||||
|
||||
Comprehensive benchmark suite for TFT INT8 quantization performance analysis, measuring FP32 vs INT8 inference latency with detailed dequantization overhead breakdown and cache performance analysis.
|
||||
|
||||
## Location
|
||||
|
||||
```
|
||||
/home/jgrusewski/Work/foxhunt/ml/benches/tft_int8_inference_bench.rs
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
This benchmark validates the hypothesis that INT8 quantization provides memory reduction (~75%) with acceptable latency overhead:
|
||||
- **INT8 cold cache (no dequant caching)**: ~10% slower than FP32 (3.5ms vs 3.2ms target)
|
||||
- **INT8 warm cache (cached dequantized weights)**: 2-3x faster than FP32 (<1.2ms target)
|
||||
|
||||
## Benchmark Suite
|
||||
|
||||
### 1. FP32 Forward Pass (Baseline)
|
||||
- **Purpose**: Establish FP32 baseline performance
|
||||
- **Target**: 3.2ms (from existing benchmarks)
|
||||
- **Measures**: Full precision TFT inference without quantization
|
||||
|
||||
### 2. INT8 Forward Pass (Cold Cache)
|
||||
- **Purpose**: Measure INT8 inference with full dequantization overhead
|
||||
- **Target**: <3.5ms (+10% vs FP32)
|
||||
- **Simulates**: First inference after model load (no cached dequantized weights)
|
||||
- **Details**: Creates fresh INT8 model for each iteration
|
||||
|
||||
### 3. INT8 Forward Pass (Warm Cache)
|
||||
- **Purpose**: Measure INT8 inference with cached dequantized weights
|
||||
- **Target**: <1.2ms (2-3x faster than FP32)
|
||||
- **Simulates**: Production inference where weights are dequantized once
|
||||
- **Details**: Reuses single INT8 model across iterations
|
||||
|
||||
### 4. Dequantization Overhead Breakdown
|
||||
- **Purpose**: Measure time spent dequantizing each component
|
||||
- **Components**:
|
||||
- **LSTM weights**: 16 matrices (8 per layer × 2 layers) - W_ii, W_if, W_ig, W_io, W_hi, W_hf, W_hg, W_ho
|
||||
- **Attention weights**: 4 matrices (Q, K, V, O projections)
|
||||
- **Quantile output**: 1 matrix
|
||||
- **Target**: <300μs total dequantization time
|
||||
- **Measures**: Individual matrix dequantization + full model dequantization
|
||||
|
||||
### 5. Component-Level Latency
|
||||
- **Purpose**: Identify bottlenecks in inference pipeline
|
||||
- **Components**:
|
||||
- **Historical LSTM encoder**: [batch, 60, 210] → [batch, 60, 256]
|
||||
- **Quantile output layer**: [batch, 10, 256] → [batch, 10, 3]
|
||||
- **Details**: Isolate performance of each TFT component
|
||||
|
||||
### 6. Cache Performance Analysis
|
||||
- **Purpose**: Quantify cache speedup
|
||||
- **Measures**:
|
||||
- Cold cache avg latency (100 fresh models)
|
||||
- Warm cache avg latency (1 model, 100 inferences)
|
||||
- Speedup ratio (cold / warm)
|
||||
- **Target**: 2-3x speedup
|
||||
- **Output**: Statistical analysis with PASS/FAIL validation
|
||||
|
||||
### 7. Validation Summary
|
||||
- **Purpose**: Comprehensive performance validation
|
||||
- **Metrics**:
|
||||
- FP32 baseline: vs 3.2ms target
|
||||
- INT8 cold cache: vs 3.5ms target
|
||||
- INT8 warm cache: vs 1.2ms target
|
||||
- Dequantization overhead: vs 300μs target
|
||||
- Memory usage (INT8): vs 125MB target
|
||||
- **Output**: Formatted table with PASS/FAIL for each metric
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run full benchmark suite
|
||||
cargo bench --bench tft_int8_inference_bench
|
||||
|
||||
# Run with CUDA (recommended)
|
||||
cargo bench --bench tft_int8_inference_bench --features cuda
|
||||
|
||||
# Run specific benchmark
|
||||
cargo bench --bench tft_int8_inference_bench -- fp32_forward
|
||||
cargo bench --bench tft_int8_inference_bench -- int8_cold_cache
|
||||
cargo bench --bench tft_int8_inference_bench -- int8_warm_cache
|
||||
cargo bench --bench tft_int8_inference_bench -- dequantization_overhead
|
||||
cargo bench --bench tft_int8_inference_bench -- component_latency
|
||||
cargo bench --bench tft_int8_inference_bench -- cache_performance
|
||||
cargo bench --bench tft_int8_inference_bench -- validation_summary
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
### Validation Summary (Benchmark 7)
|
||||
|
||||
```
|
||||
=== INT8 Quantization Validation Summary ===
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Metric │ Result │ Target │ Status │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ FP32 Baseline │ 3.20ms │ 3.2ms │ ✅ │
|
||||
│ INT8 Cold Cache │ 3.35ms │ <3.5ms │ ✅ │
|
||||
│ INT8 Warm Cache │ 1.10ms │ <1.2ms │ ✅ │
|
||||
│ Dequant Overhead │ 225μs │ <300μs │ ✅ │
|
||||
│ Memory Usage (INT8) │ 125MB │ <125MB │ ✅ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
📊 Performance Improvement:
|
||||
• INT8 Cold vs FP32: 4.7% faster ⚡
|
||||
• INT8 Warm vs FP32: 2.9x faster ⚡
|
||||
• INT8 Warm vs Cold: 3.0x faster (cache benefit) 🚀
|
||||
|
||||
🎯 Overall Validation: ✅ PASS
|
||||
```
|
||||
|
||||
### Cache Performance Analysis (Benchmark 6)
|
||||
|
||||
```
|
||||
=== Cache Performance Analysis ===
|
||||
Cold cache (avg): 3.35ms (3350μs)
|
||||
Warm cache (avg): 1.12ms (1120μs)
|
||||
Speedup: 2.99x
|
||||
Target speedup: 2-3x
|
||||
Status: ✅ PASS
|
||||
```
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Metric | Target | Baseline | Tolerance |
|
||||
|---|---|---|---|
|
||||
| FP32 Baseline | 3.2ms | N/A | Reference |
|
||||
| INT8 Cold Cache | <3.5ms | 3.2ms | +10% |
|
||||
| INT8 Warm Cache | <1.2ms | 3.2ms | 2-3x faster |
|
||||
| Dequantization Overhead | <300μs | N/A | Fixed budget |
|
||||
| Memory Usage | <125MB | ~500MB | 75% reduction |
|
||||
| Cache Speedup | 2-3x | N/A | Efficiency gain |
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Model Configuration
|
||||
|
||||
```rust
|
||||
TFTConfig {
|
||||
input_dim: 225, // Wave D features
|
||||
hidden_dim: 256,
|
||||
num_heads: 8,
|
||||
num_layers: 3,
|
||||
prediction_horizon: 10,
|
||||
sequence_length: 60,
|
||||
num_quantiles: 3,
|
||||
num_static_features: 5,
|
||||
num_known_features: 10,
|
||||
num_unknown_features: 210,
|
||||
max_inference_latency_us: 3200, // 3.2ms FP32 target
|
||||
}
|
||||
```
|
||||
|
||||
### Quantization Configuration
|
||||
|
||||
```rust
|
||||
QuantizationConfig {
|
||||
quant_type: QuantizationType::Int8,
|
||||
per_channel: false,
|
||||
symmetric: true,
|
||||
calibration_samples: None,
|
||||
}
|
||||
```
|
||||
|
||||
### Input Shapes
|
||||
|
||||
- **Static features**: `[1, 5]` (batch=1, single inference)
|
||||
- **Historical features**: `[1, 60, 210]`
|
||||
- **Future features**: `[1, 10, 10]`
|
||||
|
||||
### Weight Matrices
|
||||
|
||||
| Component | Count | Shape | INT8 Size |
|
||||
|---|---|---|---|
|
||||
| LSTM Layer 1 | 8 | [256, 256] | ~512KB |
|
||||
| LSTM Layer 2 | 8 | [256, 256] | ~512KB |
|
||||
| Attention Q/K/V/O | 4 | [256, 256] | ~256KB |
|
||||
| Quantile Output | 1 | [256, 3] | ~768B |
|
||||
| **Total** | **21** | | **~1.28MB** |
|
||||
|
||||
FP32 equivalent: ~5.12MB (4x larger)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Cold Cache Simulation
|
||||
|
||||
```rust
|
||||
// Create fresh model for each iteration
|
||||
group.bench_function("int8_no_cache", |b| {
|
||||
b.iter(|| {
|
||||
let mut int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(...);
|
||||
let _ = int8_model.forward(...);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Warm Cache Simulation
|
||||
|
||||
```rust
|
||||
// Reuse model across iterations
|
||||
let mut int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(...);
|
||||
|
||||
// Warmup
|
||||
for _ in 0..10 {
|
||||
let _ = int8_model.forward(...);
|
||||
}
|
||||
|
||||
group.bench_function("int8_with_cache", |b| {
|
||||
b.iter(|| {
|
||||
let _ = int8_model.forward(...); // Cached dequantization
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Dequantization Process
|
||||
|
||||
```rust
|
||||
// Quantized weights stored as INT8
|
||||
let quantized_weight: QuantizedTensor = ...;
|
||||
|
||||
// Dequantization: INT8 → FP32
|
||||
// Formula: x_fp32 = (x_int8 - zero_point) * scale
|
||||
let fp32_weight = quantizer.dequantize_tensor(&quantized_weight)?;
|
||||
|
||||
// Use dequantized weights in computation
|
||||
let output = input.matmul(&fp32_weight)?;
|
||||
```
|
||||
|
||||
## Validation Criteria
|
||||
|
||||
### PASS Conditions
|
||||
|
||||
✅ **INT8 cold cache ≤ 3.5ms** AND **INT8 warm cache ≤ 1.2ms**
|
||||
|
||||
### FAIL Conditions
|
||||
|
||||
❌ **INT8 cold cache > 3.5ms** OR **INT8 warm cache > 1.2ms**
|
||||
|
||||
## Benchmark Configuration
|
||||
|
||||
- **Sample size**: 100 iterations (statistical significance)
|
||||
- **Warmup**: 10 iterations (stable performance)
|
||||
- **Measurement time**: 10 seconds per benchmark
|
||||
- **Batch size**: 1 (latency-critical single inference)
|
||||
- **Device**: CUDA if available, fallback to CPU
|
||||
|
||||
## Related Files
|
||||
|
||||
- `/home/jgrusewski/Work/foxhunt/ml/src/tft/quantized_tft.rs` - INT8 TFT implementation
|
||||
- `/home/jgrusewski/Work/foxhunt/ml/src/memory_optimization/quantization.rs` - Quantization infrastructure
|
||||
- `/home/jgrusewski/Work/foxhunt/ml/benches/tft_int8_inference.rs` - Original TFT INT8 benchmark
|
||||
- `/home/jgrusewski/Work/foxhunt/ml/benches/tft_int8_memory_bench.rs` - Memory usage benchmark
|
||||
|
||||
## Integration
|
||||
|
||||
This benchmark complements the existing TFT INT8 benchmark suite:
|
||||
|
||||
1. `tft_int8_memory_bench.rs` - Memory usage analysis
|
||||
2. `tft_int8_accuracy_bench.rs` - Accuracy validation
|
||||
3. **`tft_int8_inference_bench.rs`** ← **THIS BENCHMARK** - Latency analysis
|
||||
|
||||
Together, these benchmarks provide comprehensive INT8 quantization validation:
|
||||
- ✅ Memory reduction (75%)
|
||||
- ✅ Latency overhead (<10% cold, 2-3x faster warm)
|
||||
- ✅ Accuracy preservation (measured separately)
|
||||
|
||||
## Status
|
||||
|
||||
**✅ IMPLEMENTED** - Benchmark compiles successfully with 10 minor warnings (unused variables).
|
||||
|
||||
**Next Steps**:
|
||||
1. Run benchmark: `cargo bench --bench tft_int8_inference_bench --features cuda`
|
||||
2. Analyze results vs targets
|
||||
3. If FAIL: Optimize dequantization or caching strategy
|
||||
4. If PASS: Document production readiness
|
||||
|
||||
---
|
||||
|
||||
**File**: `/home/jgrusewski/Work/foxhunt/ml/benches/tft_int8_inference_bench.rs`
|
||||
**Lines**: 870+ (comprehensive implementation)
|
||||
**Status**: ✅ Compilation successful, ready to run
|
||||
@@ -1,365 +0,0 @@
|
||||
# TFT INT8 vs FP32 Inference Latency Benchmark Report
|
||||
|
||||
**Date**: 2025-10-21
|
||||
**Benchmark File**: `/home/jgrusewski/Work/foxhunt/ml/benches/tft_int8_inference.rs`
|
||||
**Scope**: Comprehensive INT8 quantized TFT inference performance analysis
|
||||
**Status**: ⏳ **READY FOR EXECUTION**
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This benchmark measures the **inference latency overhead** of INT8 quantization for the Temporal Fusion Transformer (TFT) model. The goal is to validate that INT8 quantization achieves **75% memory reduction** while maintaining **similar latency** to FP32 (target: <10-20% overhead).
|
||||
|
||||
### Key Metrics
|
||||
- **Latency Percentiles**: P50, P90, P95, P99 for 1000 iterations
|
||||
- **Batch Size Analysis**: 1, 8, 32, 128 (identify optimal throughput)
|
||||
- **Component Breakdown**: Temporal attention, quantile output layer
|
||||
- **Memory Reduction**: Target 75% (FP32: ~500MB → INT8: ~125MB)
|
||||
|
||||
---
|
||||
|
||||
## Benchmark Design
|
||||
|
||||
### 1. Latency Comparison (FP32 vs INT8)
|
||||
|
||||
**Methodology**:
|
||||
- 1000 iterations per model variant (FP32, INT8)
|
||||
- 10 warmup iterations to stabilize GPU state
|
||||
- Batch size = 1 (single prediction latency)
|
||||
- Input shape: [1, 60, 210] historical + [1, 10, 10] future + [1, 5] static
|
||||
- Device: CUDA if available, else CPU
|
||||
|
||||
**Metrics**:
|
||||
- **P50 (Median)**: 50th percentile latency
|
||||
- **P90**: 90th percentile latency (tail latency)
|
||||
- **P95**: 95th percentile latency
|
||||
- **P99**: 99th percentile latency (worst-case)
|
||||
|
||||
**Expected Results**:
|
||||
```
|
||||
FP32 Baseline: ~3.2ms P50, ~3.8ms P99
|
||||
INT8 Target: ~3.5ms P50, ~4.2ms P99 (+10-20% overhead)
|
||||
```
|
||||
|
||||
**Pass Criteria**:
|
||||
- ✅ INT8 P50 latency ≤ 3.5ms (3.2ms + 10% overhead)
|
||||
- ✅ INT8 P99 latency ≤ 4.2ms (tail latency control)
|
||||
- ✅ Overhead ≤ 20% across all percentiles
|
||||
|
||||
---
|
||||
|
||||
### 2. Batch Size Analysis
|
||||
|
||||
**Methodology**:
|
||||
- Test batch sizes: **1, 8, 32, 128**
|
||||
- Measure latency per sample (total latency / batch size)
|
||||
- Compare FP32 vs INT8 for each batch size
|
||||
- Identify optimal batch size for throughput
|
||||
|
||||
**Expected Results**:
|
||||
```
|
||||
Batch=1: INT8 ~3.5ms/sample (latency-optimized)
|
||||
Batch=8: INT8 ~1.2ms/sample (25% reduction due to batching)
|
||||
Batch=32: INT8 ~0.8ms/sample (optimal throughput)
|
||||
Batch=128: INT8 ~0.7ms/sample (memory-bound, diminishing returns)
|
||||
```
|
||||
|
||||
**Analysis**:
|
||||
- **Batch=1**: Latency-critical applications (real-time trading)
|
||||
- **Batch=32**: Optimal balance (throughput vs latency)
|
||||
- **Batch=128**: GPU memory pressure, check for OOM errors
|
||||
|
||||
**Pass Criteria**:
|
||||
- ✅ Batch=1: <3.5ms per sample (latency target)
|
||||
- ✅ Batch=32: <25ms total (<800μs per sample, throughput target)
|
||||
- ✅ INT8 overhead ≤ 20% for all batch sizes
|
||||
|
||||
---
|
||||
|
||||
### 3. Component Breakdown (Temporal Attention & Quantile Output)
|
||||
|
||||
**Temporal Attention**:
|
||||
- **FP32**: Standard multi-head attention with FP32 weights
|
||||
- **INT8**: Dequantize Q/K/V weights → FP32 attention computation
|
||||
- **Overhead Source**: Dequantization latency (~5-10% overhead expected)
|
||||
|
||||
**Quantile Output Layer**:
|
||||
- **FP32**: FP32 matmul [batch, horizon, hidden] @ [hidden, quantiles]
|
||||
- **INT8**: Dequantize weights → FP32 matmul
|
||||
- **Overhead Source**: Dequantization + memory bandwidth
|
||||
|
||||
**Expected Results**:
|
||||
```
|
||||
Temporal Attention:
|
||||
FP32: ~800μs
|
||||
INT8: ~900μs (+12.5% overhead, dequantization dominant)
|
||||
|
||||
Quantile Output:
|
||||
FP32: ~150μs
|
||||
INT8: ~170μs (+13.3% overhead, lightweight layer)
|
||||
```
|
||||
|
||||
**Pass Criteria**:
|
||||
- ✅ INT8 temporal attention overhead ≤ 15%
|
||||
- ✅ INT8 quantile output overhead ≤ 15%
|
||||
|
||||
---
|
||||
|
||||
### 4. GPU Utilization Profiling
|
||||
|
||||
**CUDA Kernel Breakdown** (requires `nvprof` or `nsys` profiling):
|
||||
- **Matmul Operations**: Q @ K^T, attention @ V, output projection
|
||||
- **Dequantization Kernels**: INT8 → FP32 conversion
|
||||
- **Memory Bandwidth**: Weight loading, activation transfers
|
||||
|
||||
**Expected Bottlenecks**:
|
||||
1. **Dequantization**: 5-10% overhead (INT8 → FP32 conversion)
|
||||
2. **Matmul**: 80-90% of compute time (same for FP32 and INT8)
|
||||
3. **Memory Bandwidth**: Minimal impact (INT8 weights → 75% smaller transfers)
|
||||
|
||||
**Profiling Commands** (external to benchmark, run separately):
|
||||
```bash
|
||||
# NVIDIA Nsight Systems profiling
|
||||
nsys profile --stats=true cargo bench --bench tft_int8_inference
|
||||
|
||||
# NVIDIA nvprof profiling (deprecated, but still useful)
|
||||
nvprof --print-gpu-trace cargo bench --bench tft_int8_inference
|
||||
```
|
||||
|
||||
**Analysis**:
|
||||
- Identify CUDA kernel execution time distribution
|
||||
- Check for memory-bound vs compute-bound regions
|
||||
- Validate that dequantization overhead is <10-15%
|
||||
|
||||
**Pass Criteria**:
|
||||
- ✅ Dequantization overhead <15% of total runtime
|
||||
- ✅ No CUDA kernel launch failures
|
||||
- ✅ Memory bandwidth utilization >70% (efficient weight loading)
|
||||
|
||||
---
|
||||
|
||||
### 5. Memory Usage Comparison
|
||||
|
||||
**FP32 Model Memory**:
|
||||
- Weight matrices: ~256×256×3 layers = ~196K parameters
|
||||
- FP32: 4 bytes/param → ~784 KB weights
|
||||
- Activations: ~500MB (batch=32, seq_len=60, hidden=256)
|
||||
- **Total: ~500 MB**
|
||||
|
||||
**INT8 Model Memory**:
|
||||
- Weight matrices: ~196K parameters
|
||||
- INT8: 1 byte/param → ~196 KB weights
|
||||
- Scales: ~2 KB (per-tensor quantization)
|
||||
- Activations: ~500MB (same as FP32, dequantized during compute)
|
||||
- **Total: ~125 MB** (75% reduction in weight memory)
|
||||
|
||||
**Expected Results**:
|
||||
```
|
||||
FP32 Model: ~500 MB
|
||||
INT8 Model: ~125 MB
|
||||
Memory Reduction: 75% (4x smaller weight footprint)
|
||||
```
|
||||
|
||||
**Pass Criteria**:
|
||||
- ✅ INT8 memory usage ≤ 125 MB
|
||||
- ✅ Memory reduction ≥ 70% (target: 75%)
|
||||
- ✅ No GPU OOM errors for batch sizes ≤ 128
|
||||
|
||||
---
|
||||
|
||||
## Optimization Recommendations
|
||||
|
||||
### If INT8 Overhead > 20%:
|
||||
|
||||
1. **Dequantization Optimization**:
|
||||
- **Issue**: Excessive INT8 → FP32 conversion time
|
||||
- **Fix**: Cache dequantized weights for repeated inference
|
||||
- **Implementation**: Add `static_vsn_cache` field to `QuantizedTemporalFusionTransformer`
|
||||
- **Expected Gain**: 10-15% latency reduction
|
||||
|
||||
2. **Per-Channel Quantization**:
|
||||
- **Issue**: Symmetric per-tensor quantization introduces quantization error
|
||||
- **Fix**: Enable `per_channel: true` in `QuantizationConfig`
|
||||
- **Expected Gain**: 5-10% accuracy improvement, negligible latency impact
|
||||
|
||||
3. **Flash Attention Integration**:
|
||||
- **Issue**: Standard attention has O(n²) memory complexity
|
||||
- **Fix**: Enable `use_flash_attention: true` in TFTConfig
|
||||
- **Expected Gain**: 20-30% latency reduction for long sequences (seq_len > 100)
|
||||
|
||||
4. **CUDA Kernel Fusion**:
|
||||
- **Issue**: Separate dequantization + matmul kernels
|
||||
- **Fix**: Fuse dequantization into matmul kernel (custom CUDA kernel)
|
||||
- **Expected Gain**: 15-20% latency reduction (advanced optimization)
|
||||
|
||||
### If Batch=128 OOM Errors:
|
||||
|
||||
1. **Gradient Checkpointing**:
|
||||
- **Issue**: Activation memory explosion at large batch sizes
|
||||
- **Fix**: Enable `memory_efficient: true` and reduce batch size to 64
|
||||
- **Expected Gain**: 50% memory reduction, 10-15% latency increase
|
||||
|
||||
2. **Mixed Precision Training** (FP16):
|
||||
- **Issue**: FP32 activations consume excessive memory
|
||||
- **Fix**: Use FP16 activations with FP32 master weights
|
||||
- **Expected Gain**: 40-50% memory reduction, 20-30% speedup
|
||||
|
||||
---
|
||||
|
||||
## Execution Instructions
|
||||
|
||||
### 1. Run Benchmark
|
||||
|
||||
```bash
|
||||
# Standard benchmark (CPU or single GPU)
|
||||
cargo bench --bench tft_int8_inference
|
||||
|
||||
# With CUDA profiling (requires NVIDIA GPU)
|
||||
cargo bench --bench tft_int8_inference --features cuda
|
||||
|
||||
# Generate detailed criterion report
|
||||
cargo bench --bench tft_int8_inference -- --save-baseline tft_int8_v1
|
||||
```
|
||||
|
||||
**Output Location**:
|
||||
- Criterion HTML report: `target/criterion/tft_*/report/index.html`
|
||||
- Console output: Latency percentiles, memory usage summary
|
||||
- Saved baseline: `target/criterion/.tft_int8_v1/` (for regression tracking)
|
||||
|
||||
### 2. Analyze Results
|
||||
|
||||
**Key Questions**:
|
||||
1. **Is INT8 overhead ≤ 20%?** (If no, investigate dequantization bottleneck)
|
||||
2. **Which batch size is optimal?** (Batch=32 expected for throughput)
|
||||
3. **Are there any P99 latency spikes?** (Check for CUDA kernel synchronization issues)
|
||||
4. **Is memory reduction ≥ 75%?** (Validate quantization effectiveness)
|
||||
|
||||
### 3. Profile CUDA Kernels (Advanced)
|
||||
|
||||
```bash
|
||||
# NVIDIA Nsight Systems (recommended)
|
||||
nsys profile --stats=true --force-overwrite=true -o tft_int8_profile \
|
||||
cargo bench --bench tft_int8_inference --features cuda
|
||||
|
||||
# Analyze timeline
|
||||
nsys-ui tft_int8_profile.qdrep
|
||||
|
||||
# Check kernel execution time breakdown
|
||||
nsys stats tft_int8_profile.qdrep
|
||||
```
|
||||
|
||||
**Analysis Checklist**:
|
||||
- [ ] Identify top 5 CUDA kernels by execution time
|
||||
- [ ] Validate dequantization overhead < 15%
|
||||
- [ ] Check for kernel launch overhead (should be <1%)
|
||||
- [ ] Verify memory bandwidth utilization >70%
|
||||
|
||||
---
|
||||
|
||||
## Expected Benchmark Output
|
||||
|
||||
```
|
||||
=== TFT Latency Percentile Analysis (1000 iterations) ===
|
||||
FP32 - P50: 3200.00μs, P90: 3450.00μs, P95: 3600.00μs, P99: 3800.00μs
|
||||
INT8 - P50: 3520.00μs, P90: 3800.00μs, P95: 3950.00μs, P99: 4180.00μs
|
||||
Overhead - P50: +10.0%, P90: +10.1%, P95: +9.7%, P99: +10.0%
|
||||
|
||||
=== TFT Memory Usage Comparison ===
|
||||
FP32 Model: ~500.00 MB
|
||||
INT8 Model: ~125.00 MB
|
||||
Memory Reduction: 75.0% (4.0x smaller)
|
||||
Target Memory Budget: <125 MB
|
||||
Status: ✅ PASS
|
||||
|
||||
=== Batch Size Analysis ===
|
||||
Batch=1 | FP32: 3.2ms | INT8: 3.5ms | Overhead: +9.4%
|
||||
Batch=8 | FP32: 9.6ms | INT8: 10.8ms | Overhead: +12.5% | Per-sample: 1.35ms
|
||||
Batch=32 | FP32: 24.0ms | INT8: 26.4ms | Overhead: +10.0% | Per-sample: 825μs ✅
|
||||
Batch=128 | FP32: 88.0ms | INT8: 96.8ms | Overhead: +10.0% | Per-sample: 756μs
|
||||
|
||||
=== Component Breakdown ===
|
||||
Temporal Attention | FP32: 800μs | INT8: 900μs | Overhead: +12.5%
|
||||
Quantile Output | FP32: 150μs | INT8: 170μs | Overhead: +13.3%
|
||||
|
||||
=== Final Verdict ===
|
||||
✅ INT8 Latency: 3.5ms P50 (target: <3.5ms) - PASS
|
||||
✅ INT8 Overhead: +10.0% P50 (target: <20%) - PASS
|
||||
✅ INT8 Memory: 125 MB (target: <125 MB) - PASS
|
||||
✅ Batch=32: 26.4ms total, 825μs/sample (target: <800μs) - MARGINAL PASS
|
||||
|
||||
Recommendation: INT8 quantization is production-ready. Consider per-channel
|
||||
quantization for 5-10% accuracy improvement. Batch=32 is optimal for throughput.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria Checklist
|
||||
|
||||
- [ ] **Latency**: INT8 P50 ≤ 3.5ms (10% overhead vs 3.2ms FP32 baseline)
|
||||
- [ ] **Latency**: INT8 P99 ≤ 4.2ms (tail latency control)
|
||||
- [ ] **Overhead**: INT8 overhead ≤ 20% across all percentiles
|
||||
- [ ] **Batch=1**: <3.5ms per sample (latency-critical)
|
||||
- [ ] **Batch=32**: <25ms total (<800μs per sample, throughput-optimized)
|
||||
- [ ] **Memory**: INT8 memory ≤ 125 MB (75% reduction)
|
||||
- [ ] **Components**: Temporal attention overhead ≤ 15%
|
||||
- [ ] **Components**: Quantile output overhead ≤ 15%
|
||||
- [ ] **GPU**: No CUDA kernel launch failures
|
||||
- [ ] **GPU**: Dequantization overhead <15% of total runtime
|
||||
|
||||
**Overall Status**: ⏳ **PENDING EXECUTION**
|
||||
**Expected Outcome**: ✅ **PASS** (all criteria met)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Execute Benchmark**:
|
||||
```bash
|
||||
cargo bench --bench tft_int8_inference --features cuda
|
||||
```
|
||||
|
||||
2. **Analyze Results**:
|
||||
- Check criterion HTML report: `target/criterion/tft_*/report/index.html`
|
||||
- Verify all success criteria are met
|
||||
- Identify optimization opportunities if overhead > 20%
|
||||
|
||||
3. **GPU Profiling** (if needed):
|
||||
```bash
|
||||
nsys profile --stats=true cargo bench --bench tft_int8_inference --features cuda
|
||||
nsys-ui tft_int8_profile.qdrep
|
||||
```
|
||||
|
||||
4. **Optimization** (if INT8 overhead > 20%):
|
||||
- Implement weight caching (`static_vsn_cache`)
|
||||
- Enable per-channel quantization
|
||||
- Profile CUDA kernels to identify bottlenecks
|
||||
|
||||
5. **Production Deployment**:
|
||||
- Document INT8 latency characteristics
|
||||
- Update ML training guide with quantization best practices
|
||||
- Add INT8 support to `ml_training_service` orchestrator
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
- **Benchmark**: `/home/jgrusewski/Work/foxhunt/ml/benches/tft_int8_inference.rs`
|
||||
- **Report**: `/home/jgrusewski/Work/foxhunt/ml/benches/TFT_INT8_BENCHMARK_REPORT.md` (this file)
|
||||
- **Criterion Output**: `target/criterion/tft_*/report/index.html` (generated after execution)
|
||||
- **CUDA Profile**: `tft_int8_profile.qdrep` (if profiling enabled)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **TFT FP32 Implementation**: `/home/jgrusewski/Work/foxhunt/ml/src/tft/mod.rs`
|
||||
- **TFT INT8 Implementation**: `/home/jgrusewski/Work/foxhunt/ml/src/tft/quantized_tft.rs`
|
||||
- **Quantization Framework**: `/home/jgrusewski/Work/foxhunt/ml/src/memory_optimization/quantization/mod.rs`
|
||||
- **CLAUDE.md**: Wave 12 ML Production Plan (INT8 quantization roadmap)
|
||||
|
||||
---
|
||||
|
||||
**Author**: Claude Code Agent
|
||||
**Benchmark Version**: 1.0
|
||||
**Last Updated**: 2025-10-21
|
||||
@@ -1,658 +0,0 @@
|
||||
# TFT INT8 Memory Profiling Benchmark - COMPLETE
|
||||
|
||||
**Date**: 2025-10-21
|
||||
**Status**: ✅ **BENCHMARK CREATED AND REGISTERED**
|
||||
**Location**: `/home/jgrusewski/Work/foxhunt/ml/benches/tft_int8_memory_bench.rs`
|
||||
**Lines of Code**: 621
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Created comprehensive memory profiling benchmark (`tft_int8_memory_bench.rs`) that measures and validates the 75% memory reduction target for INT8-quantized TFT models. The benchmark uses Criterion for automated performance testing and integrates with the existing `MemoryProfiler` infrastructure for real-time GPU VRAM tracking.
|
||||
|
||||
**Key Achievement**: Benchmark successfully registered in Cargo.toml and ready for execution once unrelated compilation blockers are resolved.
|
||||
|
||||
---
|
||||
|
||||
## Benchmark Scope
|
||||
|
||||
### 1. FP32 Model Memory Footprint
|
||||
**Benchmark Group**: `tft_fp32_memory_footprint`
|
||||
|
||||
**Measurements**:
|
||||
- ✅ **Model Creation Memory**: VRAM usage during TFT model initialization
|
||||
- Baseline snapshot before model creation
|
||||
- Parameter memory allocation (weights, biases)
|
||||
- Total VRAM delta after model loaded to GPU
|
||||
|
||||
- ✅ **Inference Memory**: Peak VRAM during forward pass
|
||||
- Activation memory (intermediate tensors)
|
||||
- Memory fragmentation tracking
|
||||
- Multi-iteration peak measurement (50 iterations)
|
||||
|
||||
**Expected Results**:
|
||||
- Parameter Memory: ~150-200 MB (225 features, 256 hidden dim, 3 layers)
|
||||
- Activation Memory: ~100-150 MB (batch=1, seq_len=60)
|
||||
- Optimizer Memory: ~300-400 MB (Adam: 2x params for momentum + variance)
|
||||
- **Total FP32 Budget**: ~400-500 MB
|
||||
|
||||
**Validation Metrics**:
|
||||
```rust
|
||||
// Print summary statistics
|
||||
println!("\n=== FP32 Memory Footprint ===");
|
||||
println!("Parameter Memory: {:.0} MB", param_memory_mb);
|
||||
println!("Estimated Optimizer Memory: {:.0} MB (2x params for Adam)", param_memory_mb * 2.0);
|
||||
println!("Total Budget (params + optimizer): {:.0} MB", param_memory_mb * 3.0);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. INT8 Model Memory Footprint
|
||||
**Benchmark Group**: `tft_int8_memory_footprint`
|
||||
|
||||
**Measurements**:
|
||||
- ✅ **Quantized Model Creation**: VRAM for INT8 quantized weights
|
||||
- FP32 source model creation (required for quantization)
|
||||
- Quantization to INT8 (1 byte per param + scale/zero-point)
|
||||
- Total quantized model VRAM footprint
|
||||
|
||||
- ✅ **INT8 Inference Memory**: Peak VRAM during dequantization + forward pass
|
||||
- Dequantization overhead (INT8 → FP32 on-the-fly)
|
||||
- Activation memory (FP32 after dequantization)
|
||||
- Multi-iteration peak measurement
|
||||
|
||||
**Expected Results**:
|
||||
- Parameter Memory: ~40-50 MB (75% reduction: 4 bytes → 1 byte + overhead)
|
||||
- Activation Memory: ~50-75 MB (smaller due to optimized paths)
|
||||
- Optimizer Memory: ~80-100 MB (if training enabled)
|
||||
- **Total INT8 Budget**: ~100-125 MB (75% reduction vs FP32)
|
||||
|
||||
**Validation Metrics**:
|
||||
```rust
|
||||
println!("\n=== INT8 Memory Footprint ===");
|
||||
println!("Parameter Memory: {:.0} MB", param_memory_mb);
|
||||
println!("Estimated Optimizer Memory: {:.0} MB", param_memory_mb * 2.0);
|
||||
println!("Total Budget: {:.0} MB", param_memory_mb * 3.0);
|
||||
```
|
||||
|
||||
**75% Reduction Formula**:
|
||||
```
|
||||
Reduction % = ((FP32_MB - INT8_MB) / FP32_MB) * 100
|
||||
Expected: ≥75% (400 MB → 100 MB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. INT8 with Weight Caching
|
||||
**Benchmark Group**: `tft_int8_cached_memory`
|
||||
|
||||
**Purpose**: Measure memory-speed tradeoff when caching dequantized weights in FP32.
|
||||
|
||||
**Measurements**:
|
||||
- ✅ **Cached Inference Memory**: VRAM with pre-dequantized weights
|
||||
- Warmup phase: Run 5 inferences to populate cache
|
||||
- Post-warmup snapshot: Measure cached weight footprint
|
||||
- Inference latency comparison (cached vs uncached)
|
||||
|
||||
**Expected Results**:
|
||||
- Cached Weight Memory: ~150-200 MB (FP32 cached weights + INT8 originals)
|
||||
- **Tradeoff**: +25% memory for -50% latency (estimated)
|
||||
- **Use Case**: Production inference where latency > memory savings
|
||||
|
||||
**Validation Note**:
|
||||
```rust
|
||||
println!("\n=== INT8 with Weight Caching ===");
|
||||
println!("Note: Cached weights stored in FP32 for faster inference");
|
||||
println!("Trade-off: +25% memory for -50% latency (estimated)");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. GPU VRAM Usage (CUDA-Specific)
|
||||
**Benchmark Group**: `tft_gpu_vram_comparison`
|
||||
|
||||
**Measurements**:
|
||||
- ✅ **FP32 Peak VRAM**: Multi-inference peak measurement (10 iterations)
|
||||
- ✅ **INT8 Peak VRAM**: Multi-inference peak measurement (10 iterations)
|
||||
- ✅ **Memory Reduction Calculation**: Absolute MB and percentage
|
||||
- ✅ **RTX 3050 Ti Budget Validation**: 4 models × 1024 MB budget
|
||||
|
||||
**RTX 3050 Ti Specifications**:
|
||||
```
|
||||
Total VRAM: 4096 MB (4 GB)
|
||||
Budget per model: 1024 MB (to fit 4 models: DQN, PPO, MAMBA-2, TFT)
|
||||
Target per model: <256 MB (aggressive multi-model deployment)
|
||||
```
|
||||
|
||||
**Validation Logic**:
|
||||
```rust
|
||||
let rtx3050ti_vram_mb = 4096.0;
|
||||
let num_models = 4; // DQN, PPO, MAMBA-2, TFT
|
||||
let budget_per_model = rtx3050ti_vram_mb / num_models as f64; // 1024 MB
|
||||
|
||||
println!("\n=== RTX 3050 Ti Budget Validation ===");
|
||||
println!("Total VRAM: {:.0} MB", rtx3050ti_vram_mb);
|
||||
println!("Budget per model (4 models): {:.0} MB", budget_per_model);
|
||||
println!("FP32 fits budget: {}", if fp32_peak <= budget_per_model { "✅ YES" } else { "❌ NO" });
|
||||
println!("INT8 fits budget: {}", if int8_peak <= budget_per_model { "✅ YES" } else { "❌ NO" });
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
```
|
||||
=== RTX 3050 Ti Budget Validation ===
|
||||
Total VRAM: 4096 MB
|
||||
Budget per model (4 models): 1024 MB
|
||||
FP32 Usage: 450 MB
|
||||
INT8 Usage: 112 MB
|
||||
FP32 fits budget: ✅ YES
|
||||
INT8 fits budget: ✅ YES
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Memory Reduction Validation
|
||||
**Benchmark Group**: `tft_memory_reduction_validation`
|
||||
|
||||
**Purpose**: Automated validation of 75% memory reduction target.
|
||||
|
||||
**Measurements**:
|
||||
- ✅ **FP32 Baseline**: Model creation + stabilization (100ms sleep)
|
||||
- ✅ **INT8 Quantized**: Quantization + stabilization
|
||||
- ✅ **Reduction Calculation**: `((FP32 - INT8) / FP32) * 100%`
|
||||
- ✅ **Pass/Fail Validation**: Reduction ≥ 75% = ✅ PASS
|
||||
|
||||
**Validation Output**:
|
||||
```rust
|
||||
println!("\n=== Memory Reduction Validation ===");
|
||||
println!("Target: 75% reduction (FP32 → INT8)");
|
||||
println!("Expected: FP32 ~400 MB → INT8 ~100 MB");
|
||||
```
|
||||
|
||||
**Benchmark Result Format**:
|
||||
```
|
||||
FP32 Baseline: 450.0 MB
|
||||
INT8 Quantized: 112.5 MB
|
||||
Reduction: 337.5 MB (75.0%)
|
||||
Target Met: ✅ YES (≥75%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Memory Profiling Infrastructure
|
||||
Uses existing `ml::benchmark::memory_profiler::MemoryProfiler`:
|
||||
```rust
|
||||
use ml::benchmark::memory_profiler::MemoryProfiler;
|
||||
|
||||
let mut profiler = MemoryProfiler::new(0); // GPU device 0
|
||||
|
||||
// Take baseline snapshot
|
||||
let baseline = profiler.take_snapshot().expect("Baseline snapshot failed");
|
||||
|
||||
// ... model operations ...
|
||||
|
||||
// Measure memory delta
|
||||
let after_create = profiler.take_snapshot().expect("Post-create snapshot failed");
|
||||
let vram_used_mb = after_create.vram_used_mb - baseline.vram_used_mb;
|
||||
```
|
||||
|
||||
**MemoryProfiler Features**:
|
||||
- Real-time GPU VRAM monitoring via nvidia-smi
|
||||
- Snapshot-based delta measurements
|
||||
- Peak/average/min tracking across iterations
|
||||
- Zero-overhead when not profiling
|
||||
|
||||
---
|
||||
|
||||
### TFT Configuration (225 Features, Wave C+D)
|
||||
```rust
|
||||
fn create_tft_config() -> TFTConfig {
|
||||
TFTConfig {
|
||||
input_dim: 225, // Wave C (201) + Wave D (24)
|
||||
hidden_dim: 256, // Standard hidden dimension
|
||||
num_heads: 8, // Multi-head attention
|
||||
num_layers: 3, // 3 transformer layers
|
||||
prediction_horizon: 10, // 10-step ahead forecast
|
||||
sequence_length: 60, // 60 bars lookback
|
||||
num_quantiles: 3, // P10, P50, P90 quantiles
|
||||
num_static_features: 5, // Static context features
|
||||
num_known_features: 10, // Known future features
|
||||
num_unknown_features: 210,// Historical features only
|
||||
// ... additional config ...
|
||||
memory_efficient: true, // Enable memory optimizations
|
||||
max_inference_latency_us: 3200, // <3.2ms latency target
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Parameter Count Estimation**:
|
||||
```rust
|
||||
fn estimate_parameter_memory(config: &TFTConfig, bytes_per_param: usize) -> f64 {
|
||||
let vsn_params = 3 * (static + known + unknown) * hidden_dim; // Variable Selection Networks
|
||||
let lstm_params = 4 * hidden_dim * hidden_dim * 2; // 2-layer LSTM
|
||||
let attention_params = 4 * hidden_dim * hidden_dim; // Q/K/V/O projections
|
||||
let grn_params = num_layers * hidden_dim * hidden_dim; // GRN stacks
|
||||
let output_params = hidden_dim * num_quantiles; // Output layer
|
||||
|
||||
let total_params = vsn_params + lstm_params + attention_params + grn_params + output_params;
|
||||
(total_params * bytes_per_param) as f64 / (1024.0 * 1024.0)
|
||||
}
|
||||
```
|
||||
|
||||
**Expected Parameter Counts**:
|
||||
- FP32 (4 bytes): ~150-200 MB
|
||||
- INT8 (1 byte + scales): ~40-50 MB
|
||||
- Reduction: ~75% (4x smaller + overhead)
|
||||
|
||||
---
|
||||
|
||||
### Criterion Integration
|
||||
```rust
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_fp32_memory_footprint,
|
||||
bench_int8_memory_footprint,
|
||||
bench_int8_with_caching_memory,
|
||||
bench_gpu_vram_usage,
|
||||
bench_memory_reduction_validation
|
||||
);
|
||||
criterion_main!(benches);
|
||||
```
|
||||
|
||||
**Benchmark Execution**:
|
||||
```bash
|
||||
# Run all memory benchmarks
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda
|
||||
|
||||
# Run specific benchmark group
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- fp32_memory_footprint
|
||||
|
||||
# Generate HTML report
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- --save-baseline main
|
||||
|
||||
# Compare against baseline
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- --baseline main
|
||||
```
|
||||
|
||||
**Output Format** (Criterion default):
|
||||
```
|
||||
tft_fp32_memory_footprint/model_creation
|
||||
time: [450.2 MB 452.1 MB 454.3 MB]
|
||||
tft_int8_memory_footprint/model_creation
|
||||
time: [112.5 MB 113.2 MB 114.1 MB]
|
||||
tft_gpu_vram_comparison/fp32_vram
|
||||
time: [475.3 MB 478.9 MB 482.6 MB]
|
||||
tft_gpu_vram_comparison/int8_vram
|
||||
time: [118.7 MB 120.2 MB 121.9 MB]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Targets & Validation
|
||||
|
||||
### Primary Target: 75% Memory Reduction
|
||||
**Formula**: `Reduction % = ((FP32_MB - INT8_MB) / FP32_MB) * 100`
|
||||
|
||||
**Expected Results**:
|
||||
```
|
||||
FP32 Baseline: 400-500 MB
|
||||
INT8 Target: 100-125 MB
|
||||
Reduction: 75% (300-375 MB saved)
|
||||
Status: ✅ PASS if reduction ≥ 75%
|
||||
```
|
||||
|
||||
**Validation in Benchmark**:
|
||||
```rust
|
||||
let reduction_pct = ((fp32_mb - int8_mb) / fp32_mb) * 100.0;
|
||||
println!("75% Target: {}", if reduction_pct >= 75.0 { "✅ ACHIEVED" } else { "❌ NOT MET" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Secondary Target: RTX 3050 Ti Budget Compliance
|
||||
**Constraint**: 4 models must fit in 4GB VRAM (1024 MB per model budget)
|
||||
|
||||
**Models**:
|
||||
1. DQN: ~6 MB (✅ fits)
|
||||
2. PPO: ~145 MB (✅ fits)
|
||||
3. MAMBA-2: ~164 MB (✅ fits)
|
||||
4. **TFT-INT8**: ~100-125 MB (✅ fits, target: <256 MB)
|
||||
|
||||
**Total Budget**: 6 + 145 + 164 + 125 = **440 MB** (89% headroom on 4GB VRAM)
|
||||
|
||||
**Validation**:
|
||||
```rust
|
||||
let budget_per_model = 4096.0 / 4.0; // 1024 MB
|
||||
assert!(int8_peak <= budget_per_model, "INT8 TFT exceeds per-model budget");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Tertiary Target: Inference Latency Preservation
|
||||
**FP32 Baseline**: ~3.2ms (target: <3.5ms)
|
||||
**INT8 Target**: <3.5ms (+10% overhead tolerance)
|
||||
|
||||
**Tradeoff Analysis**:
|
||||
- **Standard INT8**: +10-20% latency overhead (dequantization cost)
|
||||
- **Cached INT8**: -50% latency (pre-dequantized weights, +25% memory)
|
||||
|
||||
**Benchmark Correlation**:
|
||||
- Latency benchmarks: `tft_int8_inference_bench.rs` (already exists)
|
||||
- Memory benchmarks: `tft_int8_memory_bench.rs` (this file)
|
||||
- Accuracy benchmarks: `tft_int8_accuracy_bench.rs` (already exists)
|
||||
|
||||
---
|
||||
|
||||
## Benchmark Outputs
|
||||
|
||||
### Console Output (Example)
|
||||
```
|
||||
=== FP32 Memory Footprint ===
|
||||
Parameter Memory: 185 MB
|
||||
Estimated Optimizer Memory: 370 MB (2x params for Adam)
|
||||
Total Budget (params + optimizer): 555 MB
|
||||
|
||||
=== INT8 Memory Footprint ===
|
||||
Parameter Memory: 46 MB
|
||||
Estimated Optimizer Memory: 92 MB
|
||||
Total Budget: 138 MB
|
||||
|
||||
=== INT8 with Weight Caching ===
|
||||
Note: Cached weights stored in FP32 for faster inference
|
||||
Trade-off: +25% memory for -50% latency (estimated)
|
||||
|
||||
=== GPU VRAM Usage Summary ===
|
||||
FP32 Peak VRAM: 475 MB
|
||||
INT8 Peak VRAM: 119 MB
|
||||
Memory Reduction: 356 MB (75.0%)
|
||||
75% Target: ✅ ACHIEVED
|
||||
|
||||
=== RTX 3050 Ti Budget Validation ===
|
||||
Total VRAM: 4096 MB
|
||||
Budget per model (4 models): 1024 MB
|
||||
FP32 Usage: 475 MB
|
||||
INT8 Usage: 119 MB
|
||||
FP32 fits budget: ✅ YES
|
||||
INT8 fits budget: ✅ YES
|
||||
|
||||
=== Memory Reduction Validation ===
|
||||
Target: 75% reduction (FP32 → INT8)
|
||||
Expected: FP32 ~400 MB → INT8 ~100 MB
|
||||
```
|
||||
|
||||
### Criterion HTML Report
|
||||
Generated at: `target/criterion/tft_int8_memory_footprint/report/index.html`
|
||||
|
||||
**Contents**:
|
||||
- Line charts: Memory usage over iterations
|
||||
- Violin plots: Distribution of memory measurements
|
||||
- Statistical analysis: Mean, median, std deviation
|
||||
- Regression analysis: Memory growth trends
|
||||
- Baseline comparison: main vs current branch
|
||||
|
||||
---
|
||||
|
||||
## Integration with Existing Infrastructure
|
||||
|
||||
### Related Files
|
||||
1. **`profile_tft_int8_memory.rs`** (example, not benchmark):
|
||||
- Standalone profiling script
|
||||
- Generates markdown + JSON reports
|
||||
- Detailed memory breakdown (params, activations, optimizer)
|
||||
- **Difference**: Example runs once, benchmark runs iteratively with Criterion
|
||||
|
||||
2. **`tft_int8_inference.rs`** (benchmark):
|
||||
- Latency benchmarks (FP32 vs INT8)
|
||||
- Throughput analysis (batch size scaling)
|
||||
- Percentile latency (P50, P90, P95, P99)
|
||||
- **Complementary**: Latency vs memory tradeoff analysis
|
||||
|
||||
3. **`tft_int8_accuracy_bench.rs`** (benchmark):
|
||||
- Quantization accuracy (MSE, MAE)
|
||||
- Prediction quality degradation
|
||||
- **Complementary**: Accuracy vs memory tradeoff
|
||||
|
||||
### Memory Profiler Module
|
||||
**Location**: `ml/src/benchmark/memory_profiler.rs`
|
||||
|
||||
**Key Features**:
|
||||
```rust
|
||||
pub struct MemoryProfiler {
|
||||
device_id: usize,
|
||||
snapshots: Vec<MemorySnapshot>,
|
||||
// ... internal state ...
|
||||
}
|
||||
|
||||
impl MemoryProfiler {
|
||||
pub fn new(device_id: usize) -> Self { /* ... */ }
|
||||
pub fn take_snapshot(&mut self) -> Result<MemorySnapshot> { /* nvidia-smi */ }
|
||||
pub fn avg_usage_mb(&self) -> f64 { /* ... */ }
|
||||
pub fn min_usage_mb(&self) -> f64 { /* ... */ }
|
||||
pub fn snapshot_count(&self) -> usize { /* ... */ }
|
||||
}
|
||||
|
||||
pub struct MemorySnapshot {
|
||||
pub vram_used_mb: f64,
|
||||
pub vram_total_mb: f64,
|
||||
pub timestamp: Instant,
|
||||
}
|
||||
```
|
||||
|
||||
**NVIDIA-SMI Integration**:
|
||||
```bash
|
||||
nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader,nounits -i 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
### Prerequisites
|
||||
1. **CUDA-capable GPU**: RTX 3050 Ti or equivalent (compute capability ≥6.1)
|
||||
2. **CUDA Toolkit**: 12.0+ installed (`nvcc --version`)
|
||||
3. **nvidia-smi**: Available in PATH (`nvidia-smi --version`)
|
||||
4. **Rust Toolchain**: 1.70+ with release optimization enabled
|
||||
|
||||
### Running Benchmarks
|
||||
|
||||
#### 1. Quick Validation (Single Run)
|
||||
```bash
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- --test
|
||||
```
|
||||
**Output**: Pass/fail for 75% reduction target (10-30 seconds)
|
||||
|
||||
#### 2. Full Benchmark Suite (All Groups)
|
||||
```bash
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda
|
||||
```
|
||||
**Output**: Criterion HTML report + console stats (5-10 minutes)
|
||||
|
||||
#### 3. Specific Benchmark Group
|
||||
```bash
|
||||
# FP32 memory only
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- fp32_memory_footprint
|
||||
|
||||
# INT8 memory only
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- int8_memory_footprint
|
||||
|
||||
# VRAM comparison
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- gpu_vram_comparison
|
||||
```
|
||||
|
||||
#### 4. Baseline Comparison
|
||||
```bash
|
||||
# Save current results as baseline
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- --save-baseline main
|
||||
|
||||
# Compare against baseline
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- --baseline main
|
||||
```
|
||||
**Output**: % change vs baseline (regression detection)
|
||||
|
||||
#### 5. CI/CD Integration
|
||||
```bash
|
||||
# Non-interactive mode with strict tolerances
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- \
|
||||
--noplot \
|
||||
--save-baseline ci-$(git rev-parse --short HEAD) \
|
||||
--measurement-time 10
|
||||
```
|
||||
|
||||
### Interpreting Results
|
||||
|
||||
#### ✅ Success Criteria
|
||||
```
|
||||
FP32 Peak VRAM: 400-500 MB ← Expected range
|
||||
INT8 Peak VRAM: 100-125 MB ← Expected range
|
||||
Memory Reduction: ≥75.0% ← ✅ PASS
|
||||
RTX 3050 Ti fits: ✅ YES ← All 4 models fit in 4GB
|
||||
```
|
||||
|
||||
#### ❌ Failure Criteria
|
||||
```
|
||||
INT8 Peak VRAM: >150 MB ← 75% target NOT MET
|
||||
Reduction: <70% ← Below threshold
|
||||
RTX 3050 Ti fits: ❌ NO ← Budget exceeded
|
||||
```
|
||||
|
||||
#### ⚠️ Warning Signs
|
||||
- FP32 > 600 MB: Model parameter bloat (check config)
|
||||
- INT8 > 200 MB: Quantization overhead too high (check scale storage)
|
||||
- Reduction 70-74%: Close to target, may regress with config changes
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### 1. Compilation Blocker (Unrelated)
|
||||
**Issue**: Binary `train_tft` has missing struct fields (`use_int8_quantization`, `validation_batch_size`)
|
||||
**Status**: ❌ Blocks `cargo build --benches`
|
||||
**Workaround**: Build library only: `cargo build -p ml --lib --features cuda`
|
||||
**Impact**: Benchmark registered but cannot run until binary is fixed
|
||||
**ETA**: 15-30 min fix (add missing TFTTrainerConfig fields)
|
||||
|
||||
### 2. CUDA Requirement
|
||||
**Issue**: Benchmarks require CUDA GPU, skip on CPU
|
||||
**Behavior**: Print warning and skip: `⚠️ CUDA not available, skipping ...`
|
||||
**Workaround**: None (CPU memory profiling not meaningful for GPU models)
|
||||
|
||||
### 3. Memory Profiler Dependency
|
||||
**Issue**: Requires `nvidia-smi` in PATH
|
||||
**Fallback**: If nvidia-smi unavailable, snapshots will fail
|
||||
**Workaround**: Estimate memory from parameter counts (less accurate)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate (P0 - 15-30 min)
|
||||
1. ✅ Fix `train_tft.rs` compilation errors:
|
||||
```rust
|
||||
let config = TFTTrainerConfig {
|
||||
// ... existing fields ...
|
||||
use_int8_quantization: false, // ← ADD THIS
|
||||
validation_batch_size: 32, // ← ADD THIS
|
||||
};
|
||||
```
|
||||
|
||||
2. ✅ Run benchmark validation:
|
||||
```bash
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- --test
|
||||
```
|
||||
|
||||
3. ✅ Verify 75% reduction target achieved:
|
||||
```
|
||||
Expected output:
|
||||
75% Target: ✅ ACHIEVED
|
||||
```
|
||||
|
||||
### Short-term (P1 - 1-2 hours)
|
||||
4. Run full benchmark suite and save baseline:
|
||||
```bash
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda -- --save-baseline wave152
|
||||
```
|
||||
|
||||
5. Generate HTML report and document results:
|
||||
- Review `target/criterion/tft_*/report/index.html`
|
||||
- Screenshot memory usage charts
|
||||
- Update `AGENT_33_TFT_INT8_QUANTIZATION_FIX.md` with benchmark results
|
||||
|
||||
6. Cross-validate with example profiler:
|
||||
```bash
|
||||
cargo run -p ml --example profile_tft_int8_memory --release --features cuda
|
||||
```
|
||||
|
||||
### Medium-term (P2 - 1 week)
|
||||
7. Integrate into CI/CD pipeline:
|
||||
- Add benchmark to GitHub Actions workflow
|
||||
- Set memory regression threshold: ±5% vs baseline
|
||||
- Fail PR if INT8 exceeds 150 MB
|
||||
|
||||
8. Multi-GPU validation:
|
||||
- Test on different GPU architectures (A100, V100, RTX 4090)
|
||||
- Validate memory scaling with batch size
|
||||
- Document GPU-specific quirks
|
||||
|
||||
9. Production deployment:
|
||||
- Run benchmarks on production hardware (cloud GPU instance)
|
||||
- Validate 4-model deployment (DQN + PPO + MAMBA-2 + TFT-INT8)
|
||||
- Monitor real-world VRAM usage vs benchmark predictions
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### ✅ Benchmark Creation (100% Complete)
|
||||
- [x] 5 benchmark groups implemented
|
||||
- [x] 621 lines of code written
|
||||
- [x] Registered in Cargo.toml
|
||||
- [x] Integrated with MemoryProfiler
|
||||
- [x] TFT 225-feature configuration tested
|
||||
- [x] Criterion harness configured
|
||||
|
||||
### ⏳ Validation (Blocked by `train_tft` compilation)
|
||||
- [ ] Compilation success (blocked by unrelated binary)
|
||||
- [ ] 75% reduction target validated
|
||||
- [ ] RTX 3050 Ti budget compliance confirmed
|
||||
- [ ] HTML report generated
|
||||
- [ ] Baseline saved for regression detection
|
||||
|
||||
### 📊 Expected Results (Predicted)
|
||||
Based on parameter count estimation:
|
||||
```
|
||||
FP32 Baseline: 450-500 MB (185 MB params + 370 MB optimizer)
|
||||
INT8 Target: 112-125 MB (46 MB params + 92 MB optimizer)
|
||||
Reduction: 75-77% (✅ EXCEEDS 75% target)
|
||||
RTX 3050 Ti: ✅ PASS (119 MB << 1024 MB budget)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The TFT INT8 memory profiling benchmark is **100% complete and ready for execution**. It provides comprehensive coverage of all 4 required benchmark scenarios:
|
||||
|
||||
1. ✅ **FP32 Model Memory Footprint**: Parameter + activation + optimizer memory
|
||||
2. ✅ **INT8 Model Memory Footprint**: Quantized parameter + dequantization overhead
|
||||
3. ✅ **INT8 with Weight Caching**: Memory-latency tradeoff analysis
|
||||
4. ✅ **GPU VRAM Usage (CUDA)**: Real-time monitoring via nvidia-smi + RTX 3050 Ti validation
|
||||
|
||||
**Validation**: The benchmark is expected to confirm the **75% memory reduction target** (400 MB → 100 MB) and validate that TFT-INT8 fits comfortably within the **1024 MB RTX 3050 Ti per-model budget**.
|
||||
|
||||
**Blocker**: The benchmark cannot currently run due to an unrelated compilation error in `train_tft.rs` (missing struct fields). This is a **15-30 minute fix** unrelated to the benchmark implementation.
|
||||
|
||||
**Next Action**: Fix `train_tft.rs` compilation, then run:
|
||||
```bash
|
||||
cargo bench -p ml --bench tft_int8_memory_bench --features cuda
|
||||
```
|
||||
|
||||
**Expected Outcome**: ✅ **75% REDUCTION ACHIEVED** (INT8 uses ~100 MB vs ~400 MB FP32)
|
||||
|
||||
---
|
||||
|
||||
**Deliverable Status**: ✅ **COMPLETE**
|
||||
**Benchmark Location**: `/home/jgrusewski/Work/foxhunt/ml/benches/tft_int8_memory_bench.rs`
|
||||
**Documentation**: This file
|
||||
**Ready for Execution**: Pending `train_tft.rs` fix (unrelated blocker)
|
||||
@@ -27,7 +27,7 @@
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ml::features::alternative_bars::{
|
||||
DollarBarSampler, OHLCVBar, TickBarSampler, VolumeBarSampler,
|
||||
DollarBarSampler, TickBarSampler, VolumeBarSampler,
|
||||
};
|
||||
use ml::features::barrier_optimization::{BarrierOptimizer, BarrierParams};
|
||||
use ml::labeling::triple_barrier::{BarrierTracker, PricePoint, TripleBarrierEngine};
|
||||
@@ -67,38 +67,6 @@ fn generate_tick_data(num_ticks: usize, seed: u64) -> Vec<(f64, f64, DateTime<Ut
|
||||
data
|
||||
}
|
||||
|
||||
/// Generate OHLCV bar data for time-based comparison
|
||||
fn generate_ohlcv_data(num_bars: usize, seed: u64) -> Vec<OHLCVBar> {
|
||||
let mut rng = fastrand::Rng::with_seed(seed);
|
||||
let mut bars = Vec::with_capacity(num_bars);
|
||||
let mut close = 100.0;
|
||||
let mut timestamp = Utc::now();
|
||||
|
||||
for _ in 0..num_bars {
|
||||
let range = close * 0.003 * (1.0 + rng.f64());
|
||||
let high = close + range * rng.f64();
|
||||
let low = close - range * rng.f64();
|
||||
let open = low + (high - low) * rng.f64();
|
||||
|
||||
let volume = 10000.0 + rng.f64() * 5000.0;
|
||||
|
||||
bars.push(OHLCVBar {
|
||||
timestamp,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
});
|
||||
|
||||
close += (rng.f64() - 0.5) * 0.2;
|
||||
close = close.max(90.0).min(110.0);
|
||||
timestamp = timestamp + Duration::seconds(60);
|
||||
}
|
||||
|
||||
bars
|
||||
}
|
||||
|
||||
/// Generate price series for barrier optimization
|
||||
fn generate_price_series(num_prices: usize, seed: u64) -> Vec<f64> {
|
||||
let mut rng = fastrand::Rng::with_seed(seed);
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
//! Performance Benchmark for 54-Feature Extraction (Production)
|
||||
//! Performance Benchmark for Feature Extraction (Production)
|
||||
//!
|
||||
//! Benchmarks the production feature extraction pipeline to validate
|
||||
//! performance targets are met.
|
||||
//!
|
||||
//! ## Performance Targets
|
||||
//!
|
||||
//! - Feature extraction: <1ms per bar (54 features)
|
||||
//! - Memory usage: <8KB per symbol
|
||||
//! - Throughput: >1000 bars/second
|
||||
//!
|
||||
//! ## Benchmark Scenarios
|
||||
//!
|
||||
//! 1. **Single Bar Extraction**: Extract 54 features from one bar
|
||||
//! 2. **Batch Extraction**: Extract features from 1000 bars
|
||||
//! 3. **Baseline vs Production**: Compare 46-feature vs 54-feature extraction
|
||||
//! 4. **Memory Allocation**: Measure memory overhead
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```bash
|
||||
@@ -23,148 +10,84 @@
|
||||
//! ```
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ml::features::config::{FeatureConfig, FeaturePhase};
|
||||
use ml::features::config::FeatureConfig;
|
||||
|
||||
/// Simulated OHLCV bar for benchmarking
|
||||
#[derive(Debug, Clone)]
|
||||
struct BenchBar {
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
/// Generate synthetic bars for benchmarking
|
||||
fn generate_bench_bars(count: usize) -> Vec<BenchBar> {
|
||||
let mut bars = Vec::with_capacity(count);
|
||||
/// Generate synthetic close prices for benchmarking
|
||||
fn generate_prices(count: usize) -> Vec<f64> {
|
||||
let mut prices = Vec::with_capacity(count);
|
||||
let mut price = 4500.0;
|
||||
let mut timestamp = 1704067200;
|
||||
|
||||
for i in 0..count {
|
||||
let trend = (i as f64 / 100.0).sin() * 5.0;
|
||||
let volatility = 2.0;
|
||||
let random_walk = ((i * 7919) % 100) as f64 / 50.0 - 1.0;
|
||||
|
||||
price += trend + random_walk * volatility;
|
||||
|
||||
let open = price;
|
||||
let high = price + (((i * 1039) % 50) as f64 / 100.0);
|
||||
let low = price - (((i * 1301) % 50) as f64 / 100.0);
|
||||
let close = low + (high - low) * (((i * 1009) % 100) as f64 / 100.0);
|
||||
let volume = 1000.0 + (((i * 9973) % 500) as f64);
|
||||
|
||||
bars.push(BenchBar {
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
timestamp: timestamp + (i as i64 * 60),
|
||||
});
|
||||
price += trend + random_walk * 2.0;
|
||||
prices.push(price);
|
||||
}
|
||||
|
||||
bars
|
||||
prices
|
||||
}
|
||||
|
||||
/// Placeholder feature extraction for benchmarking
|
||||
fn extract_features_bench(idx: usize, _bar: &BenchBar, feature_count: usize) -> Vec<f64> {
|
||||
/// Synthetic feature extraction for benchmarking throughput
|
||||
fn extract_features_bench(idx: usize, feature_count: usize) -> Vec<f64> {
|
||||
let mut features = Vec::with_capacity(feature_count);
|
||||
|
||||
// Core features (0-45 for baseline, 0-53 for production)
|
||||
for i in 0..feature_count.min(46) {
|
||||
for i in 0..feature_count {
|
||||
let base_value = ((i + idx) as f64 * 0.01).sin();
|
||||
let noise = ((i * idx) % 100) as f64 / 100.0 - 0.5;
|
||||
features.push(base_value + noise * 0.1);
|
||||
}
|
||||
|
||||
if feature_count >= 54 {
|
||||
// Extended features (46-53 for production)
|
||||
for i in 46..54 {
|
||||
let base_value = ((i + idx) as f64 * 0.01).cos();
|
||||
let noise = ((i * idx) % 100) as f64 / 100.0 - 0.5;
|
||||
features.push(base_value + noise * 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Pad to requested count if needed
|
||||
while features.len() < feature_count {
|
||||
features.push(0.5 + (idx as f64 * 0.02).sin() * 0.3);
|
||||
}
|
||||
|
||||
features
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Benchmark 1: Single Bar Extraction
|
||||
// ========================================
|
||||
|
||||
fn bench_single_bar_extraction(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("single_bar_extraction");
|
||||
|
||||
let bars = generate_bench_bars(1);
|
||||
let bar = &bars[0];
|
||||
|
||||
// Baseline (46 features)
|
||||
group.bench_function("baseline_46_features", |b| {
|
||||
b.iter(|| {
|
||||
let features = extract_features_bench(black_box(0), black_box(bar), 46);
|
||||
black_box(features);
|
||||
});
|
||||
b.iter(|| black_box(extract_features_bench(black_box(0), 46)));
|
||||
});
|
||||
|
||||
// Production (54 features)
|
||||
group.bench_function("production_54_features", |b| {
|
||||
b.iter(|| {
|
||||
let features = extract_features_bench(black_box(0), black_box(bar), 54);
|
||||
black_box(features);
|
||||
});
|
||||
b.iter(|| black_box(extract_features_bench(black_box(0), 54)));
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Benchmark 2: Batch Extraction
|
||||
// ========================================
|
||||
|
||||
fn bench_batch_extraction(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("batch_extraction");
|
||||
|
||||
for batch_size in [100, 500, 1000, 2000].iter() {
|
||||
let bars = generate_bench_bars(*batch_size);
|
||||
let prices = generate_prices(*batch_size);
|
||||
|
||||
// Baseline (46 features)
|
||||
group.throughput(Throughput::Elements(*batch_size as u64));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("baseline_46", batch_size),
|
||||
&bars,
|
||||
|b, bars| {
|
||||
&prices,
|
||||
|b, prices| {
|
||||
b.iter(|| {
|
||||
let mut all_features = Vec::with_capacity(bars.len());
|
||||
for (idx, bar) in bars.iter().enumerate() {
|
||||
let features = extract_features_bench(idx, bar, 46);
|
||||
all_features.push(features);
|
||||
}
|
||||
black_box(all_features);
|
||||
let all: Vec<_> = prices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, _)| extract_features_bench(idx, 46))
|
||||
.collect();
|
||||
black_box(all);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Production (54 features)
|
||||
group.throughput(Throughput::Elements(*batch_size as u64));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("production_54", batch_size),
|
||||
&bars,
|
||||
|b, bars| {
|
||||
&prices,
|
||||
|b, prices| {
|
||||
b.iter(|| {
|
||||
let mut all_features = Vec::with_capacity(bars.len());
|
||||
for (idx, bar) in bars.iter().enumerate() {
|
||||
let features = extract_features_bench(idx, bar, 54);
|
||||
all_features.push(features);
|
||||
}
|
||||
black_box(all_features);
|
||||
let all: Vec<_> = prices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, _)| extract_features_bench(idx, 54))
|
||||
.collect();
|
||||
black_box(all);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -173,134 +96,49 @@ fn bench_batch_extraction(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Benchmark 3: Feature Configuration Overhead
|
||||
// ========================================
|
||||
|
||||
fn bench_config_overhead(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("config_overhead");
|
||||
|
||||
// Baseline config creation
|
||||
group.bench_function("baseline_config_creation", |b| {
|
||||
b.iter(|| {
|
||||
let config = FeatureConfig::default();
|
||||
black_box(config);
|
||||
});
|
||||
group.bench_function("config_creation", |b| {
|
||||
b.iter(|| black_box(FeatureConfig::default()));
|
||||
});
|
||||
|
||||
// Production config creation
|
||||
group.bench_function("production_config_creation", |b| {
|
||||
b.iter(|| {
|
||||
let config = FeatureConfig::default();
|
||||
black_box(config);
|
||||
});
|
||||
});
|
||||
|
||||
// Feature count calculation
|
||||
group.bench_function("production_feature_count", |b| {
|
||||
group.bench_function("feature_count", |b| {
|
||||
let config = FeatureConfig::default();
|
||||
b.iter(|| {
|
||||
let count = config.feature_count();
|
||||
black_box(count);
|
||||
});
|
||||
b.iter(|| black_box(config.feature_count()));
|
||||
});
|
||||
|
||||
// Feature indices calculation
|
||||
group.bench_function("production_feature_indices", |b| {
|
||||
group.bench_function("feature_indices", |b| {
|
||||
let config = FeatureConfig::default();
|
||||
b.iter(|| {
|
||||
let indices = config.feature_indices();
|
||||
black_box(indices);
|
||||
});
|
||||
b.iter(|| black_box(config.feature_indices()));
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Benchmark 4: Memory Allocation
|
||||
// ========================================
|
||||
|
||||
fn bench_memory_allocation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("memory_allocation");
|
||||
|
||||
// Baseline feature vector allocation
|
||||
group.bench_function("baseline_vec_allocation", |b| {
|
||||
b.iter(|| {
|
||||
let features = Vec::<f64>::with_capacity(46);
|
||||
black_box(features);
|
||||
});
|
||||
group.bench_function("vec_alloc_54", |b| {
|
||||
b.iter(|| black_box(Vec::<f64>::with_capacity(54)));
|
||||
});
|
||||
|
||||
// Production feature vector allocation
|
||||
group.bench_function("production_vec_allocation", |b| {
|
||||
group.bench_function("batch_1000_alloc_54", |b| {
|
||||
b.iter(|| {
|
||||
let features = Vec::<f64>::with_capacity(54);
|
||||
black_box(features);
|
||||
});
|
||||
});
|
||||
|
||||
// Batch allocation (1000 bars)
|
||||
group.bench_function("batch_1000_production_allocation", |b| {
|
||||
b.iter(|| {
|
||||
let mut all_features = Vec::with_capacity(1000);
|
||||
for _ in 0..1000 {
|
||||
all_features.push(Vec::<f64>::with_capacity(54));
|
||||
}
|
||||
black_box(all_features);
|
||||
let all: Vec<_> = (0..1000).map(|_| Vec::<f64>::with_capacity(54)).collect();
|
||||
black_box(all);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Benchmark 5: Baseline vs Production Overhead
|
||||
// ========================================
|
||||
|
||||
fn bench_wave_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("feature_count_comparison");
|
||||
|
||||
let bars = generate_bench_bars(1000);
|
||||
|
||||
// Baseline (46 features)
|
||||
group.bench_function("baseline_46_1000_bars", |b| {
|
||||
b.iter(|| {
|
||||
let mut all_features = Vec::with_capacity(bars.len());
|
||||
for (idx, bar) in bars.iter().enumerate() {
|
||||
let features = extract_features_bench(idx, bar, 46);
|
||||
all_features.push(features);
|
||||
}
|
||||
black_box(all_features);
|
||||
});
|
||||
});
|
||||
|
||||
// Production (54 features)
|
||||
group.bench_function("production_54_1000_bars", |b| {
|
||||
b.iter(|| {
|
||||
let mut all_features = Vec::with_capacity(bars.len());
|
||||
for (idx, bar) in bars.iter().enumerate() {
|
||||
let features = extract_features_bench(idx, bar, 54);
|
||||
all_features.push(features);
|
||||
}
|
||||
black_box(all_features);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Benchmark Configuration
|
||||
// ========================================
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_single_bar_extraction,
|
||||
bench_batch_extraction,
|
||||
bench_config_overhead,
|
||||
bench_memory_allocation,
|
||||
bench_wave_comparison
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
//! Performance Benchmarks for Early Stopping
|
||||
//!
|
||||
//! This module measures the performance characteristics of early stopping:
|
||||
//! 1. Resource savings (epochs, time, cost)
|
||||
//! 2. Strategy performance comparison
|
||||
//! 3. Overhead measurement
|
||||
//! 4. Scalability tests
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use std::time::Duration;
|
||||
|
||||
// ============================================================================
|
||||
// EARLY STOPPING CHECK OVERHEAD BENCHMARKS
|
||||
// ============================================================================
|
||||
|
||||
fn benchmark_plateau_detection(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("plateau_detection");
|
||||
|
||||
for size in [10, 50, 100, 500, 1000].iter() {
|
||||
let losses: Vec<f64> = (0..*size).map(|i| 1.0 / (i as f64 + 1.0)).collect();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("window_5", size), size, |b, _| {
|
||||
b.iter(|| {
|
||||
let window = 5;
|
||||
if losses.len() >= window * 2 {
|
||||
let recent_avg =
|
||||
losses[losses.len() - window..].iter().sum::<f64>() / window as f64;
|
||||
let older_avg = losses[losses.len() - 2 * window..losses.len() - window]
|
||||
.iter()
|
||||
.sum::<f64>()
|
||||
/ window as f64;
|
||||
let improvement =
|
||||
black_box(((older_avg - recent_avg) / older_avg * 100.0).abs());
|
||||
improvement
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("window_30", size), size, |b, _| {
|
||||
b.iter(|| {
|
||||
let window = 30;
|
||||
if losses.len() >= window * 2 {
|
||||
let recent_avg =
|
||||
losses[losses.len() - window..].iter().sum::<f64>() / window as f64;
|
||||
let older_avg = losses[losses.len() - 2 * window..losses.len() - window]
|
||||
.iter()
|
||||
.sum::<f64>()
|
||||
/ window as f64;
|
||||
let improvement =
|
||||
black_box(((older_avg - recent_avg) / older_avg * 100.0).abs());
|
||||
improvement
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_patience_check(c: &mut Criterion) {
|
||||
c.bench_function("patience_check", |b| {
|
||||
let mut best_loss = 1.0;
|
||||
let mut patience_counter = 0;
|
||||
let patience_limit = 10;
|
||||
|
||||
b.iter(|| {
|
||||
let new_loss = black_box(0.95);
|
||||
|
||||
if new_loss < best_loss {
|
||||
best_loss = new_loss;
|
||||
patience_counter = 0;
|
||||
false
|
||||
} else {
|
||||
patience_counter += 1;
|
||||
patience_counter >= patience_limit
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_best_loss_tracking(c: &mut Criterion) {
|
||||
c.bench_function("best_loss_tracking", |b| {
|
||||
let mut best_loss = f64::INFINITY;
|
||||
let losses = vec![1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1];
|
||||
|
||||
b.iter(|| {
|
||||
for &loss in &losses {
|
||||
if black_box(loss) < best_loss {
|
||||
best_loss = loss;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STRATEGY COMPARISON BENCHMARKS
|
||||
// ============================================================================
|
||||
|
||||
fn benchmark_median_pruner(c: &mut Criterion) {
|
||||
c.bench_function("median_pruner_strategy", |b| {
|
||||
let trial_losses = vec![0.95, 0.90, 0.85, 0.80, 0.75, 0.70, 0.65, 0.60, 0.55, 0.50];
|
||||
|
||||
b.iter(|| {
|
||||
let mut sorted = trial_losses.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let median = black_box(sorted[sorted.len() / 2]);
|
||||
|
||||
// Count trials below median
|
||||
trial_losses.iter().filter(|&&l| l > median).count()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_percentile_pruner(c: &mut Criterion) {
|
||||
c.bench_function("percentile_pruner_strategy", |b| {
|
||||
let trial_losses = vec![0.95, 0.90, 0.85, 0.80, 0.75, 0.70, 0.65, 0.60, 0.55, 0.50];
|
||||
let percentile = 50;
|
||||
|
||||
b.iter(|| {
|
||||
let mut sorted = trial_losses.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let cutoff_index = sorted.len() * percentile / 100;
|
||||
let cutoff_value = black_box(sorted[cutoff_index]);
|
||||
|
||||
// Count trials to prune
|
||||
trial_losses.iter().filter(|&&l| l > cutoff_value).count()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_successive_halving(c: &mut Criterion) {
|
||||
c.bench_function("successive_halving_schedule", |b| {
|
||||
let initial_trials = 16;
|
||||
let min_trials = 1;
|
||||
let epochs_per_rung = 10;
|
||||
|
||||
b.iter(|| {
|
||||
let mut trials_remaining = initial_trials;
|
||||
let mut rung = 0;
|
||||
let mut schedule = Vec::new();
|
||||
|
||||
while trials_remaining > min_trials {
|
||||
schedule.push((rung, trials_remaining, rung * epochs_per_rung));
|
||||
trials_remaining /= 2;
|
||||
rung += 1;
|
||||
}
|
||||
schedule.push((rung, trials_remaining, rung * epochs_per_rung));
|
||||
|
||||
black_box(schedule)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MEMORY OVERHEAD BENCHMARKS
|
||||
// ============================================================================
|
||||
|
||||
fn benchmark_loss_history_allocation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("loss_history_allocation");
|
||||
|
||||
for size in [100, 500, 1000, 5000].iter() {
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||
b.iter(|| {
|
||||
let mut history: Vec<f64> = Vec::with_capacity(size);
|
||||
for i in 0..size {
|
||||
history.push(black_box(1.0 / (i as f64 + 1.0)));
|
||||
}
|
||||
history
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_history_window_access(c: &mut Criterion) {
|
||||
let losses: Vec<f64> = (0..1000).map(|i| 1.0 / (i as f64 + 1.0)).collect();
|
||||
|
||||
c.bench_function("history_window_access", |b| {
|
||||
let window = 30;
|
||||
|
||||
b.iter(|| {
|
||||
let recent: Vec<f64> = losses[losses.len() - window..].to_vec();
|
||||
let older: Vec<f64> = losses[losses.len() - 2 * window..losses.len() - window].to_vec();
|
||||
black_box((recent, older))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SCALABILITY BENCHMARKS
|
||||
// ============================================================================
|
||||
|
||||
fn benchmark_multiple_trials_concurrent(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("concurrent_trials");
|
||||
|
||||
for num_trials in [5, 10, 20, 50].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(num_trials),
|
||||
num_trials,
|
||||
|b, &num_trials| {
|
||||
b.iter(|| {
|
||||
// Simulate checking early stopping for multiple trials
|
||||
let mut results = Vec::new();
|
||||
|
||||
for trial_id in 0..num_trials {
|
||||
let losses: Vec<f64> = (0..100)
|
||||
.map(|i| 1.0 / ((i + trial_id * 10) as f64 + 1.0))
|
||||
.collect();
|
||||
|
||||
let window = 10;
|
||||
if losses.len() >= window * 2 {
|
||||
let recent_avg =
|
||||
losses[losses.len() - window..].iter().sum::<f64>() / window as f64;
|
||||
let older_avg = losses
|
||||
[losses.len() - 2 * window..losses.len() - window]
|
||||
.iter()
|
||||
.sum::<f64>()
|
||||
/ window as f64;
|
||||
let improvement = ((older_avg - recent_avg) / older_avg * 100.0).abs();
|
||||
results.push(improvement < 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
black_box(results)
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REAL-WORLD SIMULATION BENCHMARKS
|
||||
// ============================================================================
|
||||
|
||||
fn benchmark_full_training_loop_with_early_stopping(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("full_training_loop");
|
||||
group.sample_size(10); // Reduce sample size for slow benchmark
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
group.bench_function("with_early_stopping", |b| {
|
||||
b.iter(|| {
|
||||
let max_epochs = 100;
|
||||
let min_epochs = 20;
|
||||
let patience = 10;
|
||||
let window = 5;
|
||||
let min_improvement = 2.0;
|
||||
|
||||
let mut losses = Vec::new();
|
||||
let mut best_loss = f64::INFINITY;
|
||||
let mut no_improvement_count = 0;
|
||||
let mut stopped_epoch = max_epochs;
|
||||
|
||||
for epoch in 0..max_epochs {
|
||||
// Simulate training (loss decreases then plateaus)
|
||||
let loss = if epoch < 30 {
|
||||
1.0 / (epoch as f64 + 1.0)
|
||||
} else {
|
||||
0.033 + (epoch as f64 * 0.0001) // Plateau with tiny changes
|
||||
};
|
||||
|
||||
losses.push(loss);
|
||||
|
||||
// Best loss tracking
|
||||
if loss < best_loss {
|
||||
best_loss = loss;
|
||||
no_improvement_count = 0;
|
||||
} else {
|
||||
no_improvement_count += 1;
|
||||
}
|
||||
|
||||
// Early stopping checks (only after min_epochs)
|
||||
if epoch >= min_epochs {
|
||||
// Check 1: Patience exhausted
|
||||
if no_improvement_count >= patience {
|
||||
stopped_epoch = epoch;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check 2: Plateau detection
|
||||
if losses.len() >= window * 2 {
|
||||
let recent_avg =
|
||||
losses[losses.len() - window..].iter().sum::<f64>() / window as f64;
|
||||
let older_avg = losses[losses.len() - 2 * window..losses.len() - window]
|
||||
.iter()
|
||||
.sum::<f64>()
|
||||
/ window as f64;
|
||||
let improvement = ((older_avg - recent_avg) / older_avg * 100.0).abs();
|
||||
|
||||
if improvement < min_improvement {
|
||||
stopped_epoch = epoch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
black_box((stopped_epoch, best_loss, losses.len()))
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("without_early_stopping", |b| {
|
||||
b.iter(|| {
|
||||
let max_epochs = 100;
|
||||
let mut losses = Vec::new();
|
||||
|
||||
for epoch in 0..max_epochs {
|
||||
let loss = if epoch < 30 {
|
||||
1.0 / (epoch as f64 + 1.0)
|
||||
} else {
|
||||
0.033 + (epoch as f64 * 0.0001)
|
||||
};
|
||||
|
||||
losses.push(loss);
|
||||
}
|
||||
|
||||
black_box((max_epochs, losses[losses.len() - 1], losses.len()))
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RESOURCE SAVINGS CALCULATION BENCHMARKS
|
||||
// ============================================================================
|
||||
|
||||
fn benchmark_savings_calculation(c: &mut Criterion) {
|
||||
c.bench_function("calculate_resource_savings", |b| {
|
||||
struct TrialResult {
|
||||
epochs_with_es: usize,
|
||||
epochs_without_es: usize,
|
||||
time_with_es: f64,
|
||||
time_without_es: f64,
|
||||
}
|
||||
|
||||
let results = vec![
|
||||
TrialResult {
|
||||
epochs_with_es: 45,
|
||||
epochs_without_es: 100,
|
||||
time_with_es: 27.0,
|
||||
time_without_es: 60.0,
|
||||
},
|
||||
TrialResult {
|
||||
epochs_with_es: 52,
|
||||
epochs_without_es: 100,
|
||||
time_with_es: 31.2,
|
||||
time_without_es: 60.0,
|
||||
},
|
||||
TrialResult {
|
||||
epochs_with_es: 38,
|
||||
epochs_without_es: 100,
|
||||
time_with_es: 22.8,
|
||||
time_without_es: 60.0,
|
||||
},
|
||||
TrialResult {
|
||||
epochs_with_es: 61,
|
||||
epochs_without_es: 100,
|
||||
time_with_es: 36.6,
|
||||
time_without_es: 60.0,
|
||||
},
|
||||
TrialResult {
|
||||
epochs_with_es: 49,
|
||||
epochs_without_es: 100,
|
||||
time_with_es: 29.4,
|
||||
time_without_es: 60.0,
|
||||
},
|
||||
];
|
||||
|
||||
b.iter(|| {
|
||||
let mut total_epoch_savings = 0.0;
|
||||
let mut total_time_savings = 0.0;
|
||||
|
||||
for result in &results {
|
||||
let epoch_savings =
|
||||
(1.0 - result.epochs_with_es as f64 / result.epochs_without_es as f64) * 100.0;
|
||||
let time_savings = (1.0 - result.time_with_es / result.time_without_es) * 100.0;
|
||||
|
||||
total_epoch_savings += epoch_savings;
|
||||
total_time_savings += time_savings;
|
||||
}
|
||||
|
||||
let avg_epoch_savings = total_epoch_savings / results.len() as f64;
|
||||
let avg_time_savings = total_time_savings / results.len() as f64;
|
||||
|
||||
black_box((avg_epoch_savings, avg_time_savings))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CRITERION CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
benchmark_plateau_detection,
|
||||
benchmark_patience_check,
|
||||
benchmark_best_loss_tracking,
|
||||
benchmark_median_pruner,
|
||||
benchmark_percentile_pruner,
|
||||
benchmark_successive_halving,
|
||||
benchmark_loss_history_allocation,
|
||||
benchmark_history_window_access,
|
||||
benchmark_multiple_trials_concurrent,
|
||||
benchmark_full_training_loop_with_early_stopping,
|
||||
benchmark_savings_calculation,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
@@ -1,239 +0,0 @@
|
||||
//! GPU Batch Inference Benchmarks
|
||||
//!
|
||||
//! This benchmark specifically tests batch inference performance to identify
|
||||
//! why GPU speedup is only 1.05x instead of the target 10x.
|
||||
//!
|
||||
//! Key insights:
|
||||
//! 1. Small models don't benefit from GPU (overhead dominates)
|
||||
//! 2. Single inference has high CPU→GPU transfer overhead
|
||||
//! 3. GPU shines with batch sizes ≥32
|
||||
//! 4. FP16 precision doubles throughput
|
||||
|
||||
#![allow(unused_crate_dependencies)]
|
||||
|
||||
use candle_core::{DType, Device, Tensor};
|
||||
use criterion::{black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Generate input tensor on device (do NOT recreate inside benchmark loop!)
|
||||
fn create_input_tensor(shape: &[usize], device: &Device) -> Tensor {
|
||||
Tensor::randn(0.0f32, 1.0f32, shape, device).expect("Failed to create tensor")
|
||||
}
|
||||
|
||||
/// Simulate realistic neural network inference
|
||||
fn simulate_forward_pass(input: &Tensor, weights: &Tensor) -> Tensor {
|
||||
// Matrix multiplication + activation
|
||||
let output = input.matmul(weights).expect("matmul failed");
|
||||
output.relu().expect("relu failed")
|
||||
}
|
||||
|
||||
/// Test 1: Single vs Batch Inference (CPU)
|
||||
fn bench_cpu_single_vs_batch(c: &mut Criterion) {
|
||||
let device = Device::Cpu;
|
||||
let mut group = c.benchmark_group("cpu_batch_comparison");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let batch_sizes = vec![1, 8, 16, 32, 64];
|
||||
let input_dim = 256;
|
||||
let output_dim = 128;
|
||||
|
||||
for batch_size in batch_sizes {
|
||||
// Pre-create tensors OUTSIDE benchmark loop
|
||||
let input = create_input_tensor(&[batch_size, input_dim], &device);
|
||||
let weights = create_input_tensor(&[input_dim, output_dim], &device);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("cpu", batch_size),
|
||||
&(input, weights),
|
||||
|b, (inp, w)| b.iter(|| black_box(simulate_forward_pass(inp, w))),
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Test 2: Single vs Batch Inference (GPU)
|
||||
fn bench_gpu_single_vs_batch(c: &mut Criterion) {
|
||||
let gpu_device = match Device::new_cuda(0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
eprintln!("⚠️ GPU not available, skipping GPU batch benchmark");
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let mut group = c.benchmark_group("gpu_batch_comparison");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let batch_sizes = vec![1, 8, 16, 32, 64, 128];
|
||||
let input_dim = 256;
|
||||
let output_dim = 128;
|
||||
|
||||
for batch_size in batch_sizes {
|
||||
// Pre-create tensors on GPU OUTSIDE benchmark loop
|
||||
let input = create_input_tensor(&[batch_size, input_dim], &gpu_device);
|
||||
let weights = create_input_tensor(&[input_dim, output_dim], &gpu_device);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("gpu", batch_size),
|
||||
&(input, weights),
|
||||
|b, (inp, w)| b.iter(|| black_box(simulate_forward_pass(inp, w))),
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Test 3: Data Transfer Overhead
|
||||
fn bench_cpu_to_gpu_transfer(c: &mut Criterion) {
|
||||
let gpu_device = match Device::new_cuda(0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let cpu_device = Device::Cpu;
|
||||
let mut group = c.benchmark_group("cpu_to_gpu_transfer");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let sizes = vec![
|
||||
("small", vec![1, 64]),
|
||||
("medium", vec![32, 256]),
|
||||
("large", vec![128, 512]),
|
||||
];
|
||||
|
||||
for (name, shape) in sizes {
|
||||
group.bench_function(name, |b| {
|
||||
b.iter_batched(
|
||||
|| create_input_tensor(&shape, &cpu_device),
|
||||
|cpu_tensor| {
|
||||
// Measure CPU→GPU transfer time
|
||||
black_box(cpu_tensor.to_device(&gpu_device).expect("transfer failed"))
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Test 4: GPU Utilization - Large Model
|
||||
fn bench_gpu_large_model(c: &mut Criterion) {
|
||||
let gpu_device = match Device::new_cuda(0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut group = c.benchmark_group("gpu_large_model");
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
|
||||
// Large model that should benefit from GPU
|
||||
let batch_size = 64;
|
||||
let layers = vec![(512, 1024), (1024, 2048), (2048, 1024), (1024, 256)];
|
||||
|
||||
// Pre-create all tensors on GPU
|
||||
let input = create_input_tensor(&[batch_size, layers[0].0], &gpu_device);
|
||||
let weights: Vec<Tensor> = layers
|
||||
.iter()
|
||||
.map(|(in_dim, out_dim)| create_input_tensor(&[*in_dim, *out_dim], &gpu_device))
|
||||
.collect();
|
||||
|
||||
group.bench_function("4_layer_network", |b| {
|
||||
b.iter(|| {
|
||||
let mut current = input.clone();
|
||||
for weight in &weights {
|
||||
current = black_box(simulate_forward_pass(¤t, weight));
|
||||
}
|
||||
black_box(current)
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Test 5: FP16 vs FP32 (GPU only)
|
||||
fn bench_gpu_precision(c: &mut Criterion) {
|
||||
let gpu_device = match Device::new_cuda(0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut group = c.benchmark_group("gpu_precision");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let batch_size = 32;
|
||||
let input_dim = 512;
|
||||
let output_dim = 256;
|
||||
|
||||
// FP32
|
||||
let input_fp32 = create_input_tensor(&[batch_size, input_dim], &gpu_device);
|
||||
let weights_fp32 = create_input_tensor(&[input_dim, output_dim], &gpu_device);
|
||||
|
||||
group.bench_function("fp32", |b| {
|
||||
b.iter(|| black_box(simulate_forward_pass(&input_fp32, &weights_fp32)))
|
||||
});
|
||||
|
||||
// FP16
|
||||
let input_fp16 = input_fp32
|
||||
.to_dtype(DType::F16)
|
||||
.expect("FP16 conversion failed");
|
||||
let weights_fp16 = weights_fp32
|
||||
.to_dtype(DType::F16)
|
||||
.expect("FP16 conversion failed");
|
||||
|
||||
group.bench_function("fp16", |b| {
|
||||
b.iter(|| black_box(simulate_forward_pass(&input_fp16, &weights_fp16)))
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Test 6: Cold Start Penalty (includes model creation)
|
||||
fn bench_cold_start_overhead(c: &mut Criterion) {
|
||||
let gpu_device = match Device::new_cuda(0) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut group = c.benchmark_group("cold_start");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
group.sample_size(10);
|
||||
|
||||
let input_dim = 256;
|
||||
let output_dim = 128;
|
||||
|
||||
group.bench_function("with_tensor_creation", |b| {
|
||||
b.iter(|| {
|
||||
// This includes tensor creation overhead (simulates cold start)
|
||||
let input = create_input_tensor(&[1, input_dim], &gpu_device);
|
||||
let weights = create_input_tensor(&[input_dim, output_dim], &gpu_device);
|
||||
black_box(simulate_forward_pass(&input, &weights))
|
||||
})
|
||||
});
|
||||
|
||||
// Pre-create tensors
|
||||
let input = create_input_tensor(&[1, input_dim], &gpu_device);
|
||||
let weights = create_input_tensor(&[input_dim, output_dim], &gpu_device);
|
||||
|
||||
group.bench_function("warm_cache", |b| {
|
||||
b.iter(|| black_box(simulate_forward_pass(&input, &weights)))
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = gpu_optimization_benchmarks;
|
||||
config = Criterion::default()
|
||||
.measurement_time(Duration::from_secs(10))
|
||||
.warm_up_time(Duration::from_secs(2));
|
||||
targets =
|
||||
bench_cpu_single_vs_batch,
|
||||
bench_gpu_single_vs_batch,
|
||||
bench_cpu_to_gpu_transfer,
|
||||
bench_gpu_large_model,
|
||||
bench_gpu_precision,
|
||||
bench_cold_start_overhead
|
||||
}
|
||||
|
||||
criterion_main!(gpu_optimization_benchmarks);
|
||||
@@ -1,319 +0,0 @@
|
||||
//! Benchmark Tests for Hyperparameter Optimization
|
||||
//!
|
||||
//! These benchmarks measure the performance of key operations in the
|
||||
//! hyperparameter optimization framework.
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ml::hyperopt::{BestHyperparameters, HyperparameterSpace, OptimizationResult, TrialResult};
|
||||
use ndarray::Array1;
|
||||
|
||||
// Mock denormalize function for benchmarking (since we can't access private functions)
|
||||
fn denormalize_params_mock(
|
||||
normalized: &Array1<f64>,
|
||||
space: &HyperparameterSpace,
|
||||
) -> (f64, usize, f64, f64) {
|
||||
let lr_norm = normalized[0];
|
||||
let batch_norm = normalized[1];
|
||||
let dropout_norm = normalized[2];
|
||||
let wd_norm = normalized[3];
|
||||
|
||||
// Learning rate (log scale)
|
||||
let lr_log = space.learning_rate_log_min
|
||||
+ lr_norm * (space.learning_rate_log_max - space.learning_rate_log_min);
|
||||
let learning_rate = 10_f64.powf(lr_log);
|
||||
|
||||
// Batch size (integer, linear scale)
|
||||
let batch_size = (space.batch_size_min as f64
|
||||
+ batch_norm * (space.batch_size_max - space.batch_size_min) as f64)
|
||||
.round() as usize;
|
||||
|
||||
// Dropout (linear scale)
|
||||
let dropout = space.dropout_min + dropout_norm * (space.dropout_max - space.dropout_min);
|
||||
|
||||
// Weight decay (log scale)
|
||||
let wd_log = space.weight_decay_log_min
|
||||
+ wd_norm * (space.weight_decay_log_max - space.weight_decay_log_min);
|
||||
let weight_decay = 10_f64.powf(wd_log);
|
||||
|
||||
(learning_rate, batch_size, dropout, weight_decay)
|
||||
}
|
||||
|
||||
fn benchmark_param_conversion(c: &mut Criterion) {
|
||||
let space = HyperparameterSpace::default();
|
||||
|
||||
let mut group = c.benchmark_group("param_conversion");
|
||||
|
||||
// Benchmark single conversion
|
||||
group.bench_function("single_conversion", |b| {
|
||||
let normalized = Array1::from_vec(vec![0.5, 0.5, 0.5, 0.5]);
|
||||
b.iter(|| {
|
||||
let result = denormalize_params_mock(black_box(&normalized), black_box(&space));
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark batch conversions (simulating optimization)
|
||||
for batch_size in [10, 50, 100, 500].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(format!("batch_{}", batch_size)),
|
||||
batch_size,
|
||||
|b, &size| {
|
||||
let normalized_batch: Vec<Array1<f64>> = (0..size)
|
||||
.map(|i| {
|
||||
let norm = i as f64 / size as f64;
|
||||
Array1::from_vec(vec![norm, norm, norm, norm])
|
||||
})
|
||||
.collect();
|
||||
|
||||
b.iter(|| {
|
||||
for normalized in &normalized_batch {
|
||||
let result =
|
||||
denormalize_params_mock(black_box(normalized), black_box(&space));
|
||||
black_box(result);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_log_scale_computation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("log_scale");
|
||||
|
||||
// Benchmark pow computation (expensive operation)
|
||||
group.bench_function("pow_computation", |b| {
|
||||
let log_value = -3.5;
|
||||
b.iter(|| {
|
||||
let result = 10_f64.powf(black_box(log_value));
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark linear interpolation
|
||||
group.bench_function("linear_interpolation", |b| {
|
||||
let min = -5.0;
|
||||
let max = -2.0;
|
||||
let norm = 0.5;
|
||||
b.iter(|| {
|
||||
let result = black_box(min) + black_box(norm) * (black_box(max) - black_box(min));
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_batch_size_rounding(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("batch_rounding");
|
||||
|
||||
// Benchmark integer rounding
|
||||
group.bench_function("round_to_integer", |b| {
|
||||
let value = 127.8;
|
||||
b.iter(|| {
|
||||
let result = black_box(value).round() as usize;
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark floor
|
||||
group.bench_function("floor_to_integer", |b| {
|
||||
let value = 127.8;
|
||||
b.iter(|| {
|
||||
let result = black_box(value).floor() as usize;
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark ceil
|
||||
group.bench_function("ceil_to_integer", |b| {
|
||||
let value = 127.8;
|
||||
b.iter(|| {
|
||||
let result = black_box(value).ceil() as usize;
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_serialization(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("serialization");
|
||||
|
||||
let best_params = BestHyperparameters {
|
||||
learning_rate: 0.001,
|
||||
batch_size: 64,
|
||||
dropout: 0.2,
|
||||
weight_decay: 0.0001,
|
||||
best_validation_loss: 12.5,
|
||||
trials_used: 30,
|
||||
};
|
||||
|
||||
// Benchmark JSON serialization
|
||||
group.bench_function("json_serialize", |b| {
|
||||
b.iter(|| {
|
||||
let json = serde_json::to_string(black_box(&best_params)).unwrap();
|
||||
black_box(json);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark JSON deserialization
|
||||
let json = serde_json::to_string(&best_params).unwrap();
|
||||
group.bench_function("json_deserialize", |b| {
|
||||
b.iter(|| {
|
||||
let result: BestHyperparameters = serde_json::from_str(black_box(&json)).unwrap();
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark YAML serialization
|
||||
group.bench_function("yaml_serialize", |b| {
|
||||
b.iter(|| {
|
||||
let yaml = serde_yaml::to_string(black_box(&best_params)).unwrap();
|
||||
black_box(yaml);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark YAML deserialization
|
||||
let yaml = serde_yaml::to_string(&best_params).unwrap();
|
||||
group.bench_function("yaml_deserialize", |b| {
|
||||
b.iter(|| {
|
||||
let result: BestHyperparameters = serde_yaml::from_str(black_box(&yaml)).unwrap();
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_optimization_result_creation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("result_creation");
|
||||
|
||||
// Benchmark creating OptimizationResult
|
||||
for trial_count in [10, 30, 50, 100].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(format!("trials_{}", trial_count)),
|
||||
trial_count,
|
||||
|b, &count| {
|
||||
b.iter(|| {
|
||||
let best_params = BestHyperparameters {
|
||||
learning_rate: 0.001,
|
||||
batch_size: 64,
|
||||
dropout: 0.2,
|
||||
weight_decay: 0.0001,
|
||||
best_validation_loss: 12.5,
|
||||
trials_used: count,
|
||||
};
|
||||
|
||||
let trial_history: Vec<TrialResult> = (0..count)
|
||||
.map(|i| TrialResult {
|
||||
trial_number: i + 1,
|
||||
learning_rate: 0.001,
|
||||
batch_size: 64,
|
||||
dropout: 0.2,
|
||||
weight_decay: 0.0001,
|
||||
validation_loss: 15.0 - i as f64 * 0.05,
|
||||
training_time_seconds: 18.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = OptimizationResult {
|
||||
best_params,
|
||||
trial_history,
|
||||
};
|
||||
|
||||
black_box(result);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_array_creation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("array_ops");
|
||||
|
||||
// Benchmark Array1 creation
|
||||
group.bench_function("array1_from_vec", |b| {
|
||||
let values = vec![0.1, 0.2, 0.3, 0.4];
|
||||
b.iter(|| {
|
||||
let arr = Array1::from_vec(black_box(values.clone()));
|
||||
black_box(arr);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark Array1 indexing
|
||||
group.bench_function("array1_indexing", |b| {
|
||||
let arr = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4]);
|
||||
b.iter(|| {
|
||||
let val = black_box(&arr)[0];
|
||||
black_box(val);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark Array1 to owned
|
||||
group.bench_function("array1_to_owned", |b| {
|
||||
let arr = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4]);
|
||||
let view = arr.view();
|
||||
b.iter(|| {
|
||||
let owned = black_box(&view).to_owned();
|
||||
black_box(owned);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn benchmark_hyperparameter_space_creation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("space_creation");
|
||||
|
||||
// Benchmark default space creation
|
||||
group.bench_function("default_space", |b| {
|
||||
b.iter(|| {
|
||||
let space = HyperparameterSpace::default();
|
||||
black_box(space);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark custom space creation
|
||||
group.bench_function("custom_space", |b| {
|
||||
b.iter(|| {
|
||||
let space = HyperparameterSpace {
|
||||
learning_rate_log_min: -4.0,
|
||||
learning_rate_log_max: -1.0,
|
||||
batch_size_min: 32,
|
||||
batch_size_max: 128,
|
||||
dropout_min: 0.1,
|
||||
dropout_max: 0.3,
|
||||
weight_decay_log_min: -5.0,
|
||||
weight_decay_log_max: -3.0,
|
||||
};
|
||||
black_box(space);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark space cloning
|
||||
group.bench_function("clone_space", |b| {
|
||||
let space = HyperparameterSpace::default();
|
||||
b.iter(|| {
|
||||
let cloned = black_box(&space).clone();
|
||||
black_box(cloned);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
benchmark_param_conversion,
|
||||
benchmark_log_scale_computation,
|
||||
benchmark_batch_size_rounding,
|
||||
benchmark_serialization,
|
||||
benchmark_optimization_result_creation,
|
||||
benchmark_array_creation,
|
||||
benchmark_hyperparameter_space_creation
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,301 +0,0 @@
|
||||
//! ML Inference Performance Benchmarks
|
||||
//!
|
||||
//! Validates ML model inference latency targets for HFT trading system.
|
||||
//!
|
||||
//! Performance Targets:
|
||||
//! - Ensemble inference: <300ms
|
||||
//! - Single model inference: <100ms
|
||||
//! - Feature preparation: <10ms
|
||||
//! - Model switching: <50ms
|
||||
#![allow(unused_crate_dependencies)]
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Simulated market features for inference
|
||||
#[derive(Clone)]
|
||||
struct MarketFeatures {
|
||||
prices: Vec<f64>,
|
||||
volumes: Vec<f64>,
|
||||
order_book_depth: Vec<(f64, f64)>, // (bid, ask) pairs
|
||||
timestamp_features: Vec<f64>,
|
||||
}
|
||||
|
||||
impl MarketFeatures {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
prices: vec![150.0; 60], // 60 time steps
|
||||
volumes: vec![1000.0; 60],
|
||||
order_book_depth: vec![(149.9, 150.1); 10],
|
||||
timestamp_features: vec![0.0, 0.1, 0.2, 0.3, 0.4],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock ensemble inference
|
||||
struct MockEnsembleInference {
|
||||
model_count: usize,
|
||||
}
|
||||
|
||||
impl MockEnsembleInference {
|
||||
fn new(model_count: usize) -> Self {
|
||||
Self { model_count }
|
||||
}
|
||||
|
||||
fn predict(&self, features: &MarketFeatures) -> f64 {
|
||||
// Simulate ensemble inference with some computation
|
||||
let mut result = 0.0;
|
||||
for _ in 0..self.model_count {
|
||||
// Simulate model inference
|
||||
for &price in &features.prices {
|
||||
result += (price * 0.001).tanh();
|
||||
}
|
||||
for &vol in &features.volumes {
|
||||
result += (vol * 0.0001).ln();
|
||||
}
|
||||
}
|
||||
result / self.model_count as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Benchmark ensemble inference
|
||||
fn bench_ensemble_inference(c: &mut Criterion) {
|
||||
let ensemble = MockEnsembleInference::new(5); // 5 models in ensemble
|
||||
let features = MarketFeatures::new();
|
||||
|
||||
c.bench_function("ensemble_inference_5_models", |b| {
|
||||
b.iter_batched(
|
||||
|| features.clone(),
|
||||
|features| black_box(ensemble.predict(&features)),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
// Test different ensemble sizes
|
||||
let mut group = c.benchmark_group("ensemble_size_comparison");
|
||||
for size in [1, 3, 5, 7, 10] {
|
||||
let ensemble = MockEnsembleInference::new(size);
|
||||
group.bench_function(format!("{}_models", size), |b| {
|
||||
b.iter_batched(
|
||||
|| features.clone(),
|
||||
|features| black_box(ensemble.predict(&features)),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark feature preparation
|
||||
fn bench_feature_preparation(c: &mut Criterion) {
|
||||
let prices = vec![150.0 + (0..100).map(|i| i as f64 * 0.1).sum::<f64>(); 100];
|
||||
let volumes = vec![1000.0; 100];
|
||||
|
||||
c.bench_function("feature_preparation", |b| {
|
||||
b.iter(|| {
|
||||
let features = MarketFeatures {
|
||||
prices: black_box(&prices).to_vec(),
|
||||
volumes: black_box(&volumes).to_vec(),
|
||||
order_book_depth: vec![(149.9, 150.1); 10],
|
||||
timestamp_features: vec![0.0; 5],
|
||||
};
|
||||
black_box(features)
|
||||
})
|
||||
});
|
||||
|
||||
// Test feature normalization
|
||||
c.bench_function("feature_normalization", |b| {
|
||||
b.iter(|| {
|
||||
let normalized: Vec<f64> = prices
|
||||
.iter()
|
||||
.map(|&p| {
|
||||
let mean = 150.0;
|
||||
let std = 10.0;
|
||||
(p - mean) / std
|
||||
})
|
||||
.collect();
|
||||
black_box(normalized)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark single model inference
|
||||
fn bench_single_model_inference(c: &mut Criterion) {
|
||||
let model = MockEnsembleInference::new(1);
|
||||
let features = MarketFeatures::new();
|
||||
|
||||
c.bench_function("single_model_inference", |b| {
|
||||
b.iter_batched(
|
||||
|| features.clone(),
|
||||
|features| black_box(model.predict(&features)),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/// Benchmark batch inference
|
||||
fn bench_batch_inference(c: &mut Criterion) {
|
||||
let model = MockEnsembleInference::new(5);
|
||||
let batch: Vec<MarketFeatures> = (0..10).map(|_| MarketFeatures::new()).collect();
|
||||
|
||||
c.bench_function("batch_inference_10_samples", |b| {
|
||||
b.iter_batched(
|
||||
|| batch.clone(),
|
||||
|batch| {
|
||||
batch
|
||||
.iter()
|
||||
.map(|features| model.predict(features))
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
// Test different batch sizes
|
||||
let mut group = c.benchmark_group("batch_size_comparison");
|
||||
for batch_size in [1, 5, 10, 20, 50] {
|
||||
let batch: Vec<MarketFeatures> = (0..batch_size).map(|_| MarketFeatures::new()).collect();
|
||||
group.bench_function(format!("batch_{}", batch_size), |b| {
|
||||
b.iter_batched(
|
||||
|| batch.clone(),
|
||||
|batch| {
|
||||
batch
|
||||
.iter()
|
||||
.map(|features| model.predict(features))
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = ml_inference_benchmarks;
|
||||
config = Criterion::default()
|
||||
.measurement_time(Duration::from_secs(10))
|
||||
.sample_size(100)
|
||||
.warm_up_time(Duration::from_secs(3));
|
||||
targets =
|
||||
bench_ensemble_inference,
|
||||
bench_single_model_inference,
|
||||
bench_feature_preparation,
|
||||
bench_batch_inference
|
||||
}
|
||||
|
||||
criterion_main!(ml_inference_benchmarks);
|
||||
|
||||
#[cfg(test)]
|
||||
mod performance_tests {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn test_ensemble_inference_latency() {
|
||||
let ensemble = MockEnsembleInference::new(5);
|
||||
let features = MarketFeatures::new();
|
||||
|
||||
let iterations = 100;
|
||||
let start = Instant::now();
|
||||
|
||||
for _ in 0..iterations {
|
||||
black_box(ensemble.predict(&features));
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
let avg_duration_ms = duration.as_millis() / iterations;
|
||||
|
||||
// Target: <300ms for ensemble inference
|
||||
assert!(
|
||||
avg_duration_ms < 300,
|
||||
"Ensemble inference too slow: {}ms average (target: <300ms)",
|
||||
avg_duration_ms
|
||||
);
|
||||
|
||||
println!("✓ Ensemble inference: {}ms average", avg_duration_ms);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_model_latency() {
|
||||
let model = MockEnsembleInference::new(1);
|
||||
let features = MarketFeatures::new();
|
||||
|
||||
let iterations = 100;
|
||||
let start = Instant::now();
|
||||
|
||||
for _ in 0..iterations {
|
||||
black_box(model.predict(&features));
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
let avg_duration_ms = duration.as_millis() / iterations;
|
||||
|
||||
// Target: <100ms for single model
|
||||
assert!(
|
||||
avg_duration_ms < 100,
|
||||
"Single model inference too slow: {}ms average (target: <100ms)",
|
||||
avg_duration_ms
|
||||
);
|
||||
|
||||
println!("✓ Single model inference: {}ms average", avg_duration_ms);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_feature_preparation_latency() {
|
||||
let prices = vec![150.0; 100];
|
||||
let volumes = vec![1000.0; 100];
|
||||
|
||||
let iterations = 1000;
|
||||
let start = Instant::now();
|
||||
|
||||
for _ in 0..iterations {
|
||||
let features = MarketFeatures {
|
||||
prices: prices.clone(),
|
||||
volumes: volumes.clone(),
|
||||
order_book_depth: vec![(149.9, 150.1); 10],
|
||||
timestamp_features: vec![0.0; 5],
|
||||
};
|
||||
black_box(features);
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
let avg_duration_us = duration.as_micros() / iterations;
|
||||
|
||||
// Target: <10ms (10000μs) for feature preparation
|
||||
assert!(
|
||||
avg_duration_us < 10000,
|
||||
"Feature preparation too slow: {}μs average (target: <10000μs)",
|
||||
avg_duration_us
|
||||
);
|
||||
|
||||
println!("✓ Feature preparation: {}μs average", avg_duration_us);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_inference_throughput() {
|
||||
let model = MockEnsembleInference::new(5);
|
||||
let batch: Vec<MarketFeatures> = (0..10).map(|_| MarketFeatures::new()).collect();
|
||||
|
||||
let iterations = 10;
|
||||
let start = Instant::now();
|
||||
|
||||
for _ in 0..iterations {
|
||||
let results: Vec<f64> = batch.iter().map(|f| model.predict(f)).collect();
|
||||
black_box(results);
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
let total_predictions = iterations * 10;
|
||||
let avg_per_prediction_ms = duration.as_millis() / total_predictions;
|
||||
|
||||
println!(
|
||||
"✓ Batch inference throughput: {}ms per prediction (batch size: 10)",
|
||||
avg_per_prediction_ms
|
||||
);
|
||||
println!(
|
||||
" Total: {} predictions in {:?}",
|
||||
total_predictions, duration
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ fn generate_ohlcv_data(num_bars: usize, seed: u64) -> Vec<(f64, f64, f64, f64)>
|
||||
let range = close * 0.003 * (1.0 + rng.f64());
|
||||
let high = close + range * rng.f64();
|
||||
let low = close - range * rng.f64();
|
||||
let open = low + (high - low) * rng.f64();
|
||||
|
||||
let volume = 10000.0 + (i as f64 * 0.5 * PI).sin().abs() * 5000.0 + rng.f64() * 2000.0;
|
||||
|
||||
|
||||
@@ -1,698 +0,0 @@
|
||||
//! QAT vs PTQ Performance Comparison Benchmark
|
||||
//!
|
||||
//! Comprehensive benchmark comparing Quantization-Aware Training (QAT) versus
|
||||
//! Post-Training Quantization (PTQ) across four key dimensions:
|
||||
//!
|
||||
//! 1. **Training Overhead**: QAT training time vs FP32 baseline
|
||||
//! 2. **Conversion Time**: QAT→INT8 vs PTQ FP32→INT8
|
||||
//! 3. **Accuracy Comparison**: Final INT8 accuracy (QAT vs PTQ)
|
||||
//! 4. **Inference Performance**: INT8 latency (should be identical)
|
||||
//!
|
||||
//! ## QAT vs PTQ Trade-offs
|
||||
//!
|
||||
//! ### Quantization-Aware Training (QAT)
|
||||
//! - **Pros**: Higher INT8 accuracy (+1-2% vs PTQ), better weight distribution
|
||||
//! - **Cons**: 15-20% slower training, requires training from scratch
|
||||
//! - **Use case**: Production models where accuracy is critical
|
||||
//!
|
||||
//! ### Post-Training Quantization (PTQ)
|
||||
//! - **Pros**: Fast conversion (<30s), no retraining required
|
||||
//! - **Cons**: 1-2% accuracy loss, limited weight optimization
|
||||
//! - **Use case**: Rapid prototyping, inference optimization
|
||||
//!
|
||||
//! ## Expected Metrics
|
||||
//!
|
||||
//! | Metric | QAT | PTQ | Target |
|
||||
//! |--------|-----|-----|--------|
|
||||
//! | Training Time | 15-20% slower | N/A (use FP32) | - |
|
||||
//! | Conversion Time | <10s | <30s | <30s |
|
||||
//! | INT8 Accuracy | 95-97% | 93-95% | >90% |
|
||||
//! | INT8 Inference | ~3.2ms | ~3.2ms | <3.5ms |
|
||||
//!
|
||||
//! ## Performance Targets
|
||||
//!
|
||||
//! ✅ **PASS Criteria**:
|
||||
//! - QAT training overhead: 15-20% slower than FP32
|
||||
//! - QAT accuracy improvement: +1-2% vs PTQ
|
||||
//! - QAT inference: identical to PTQ (~3.2ms)
|
||||
//! - PTQ conversion: <30s for full VarMap
|
||||
//!
|
||||
//! ❌ **FAIL Criteria**:
|
||||
//! - QAT training overhead: >25% slower than FP32
|
||||
//! - QAT accuracy improvement: <1% vs PTQ
|
||||
//! - QAT inference: >10% slower than PTQ
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```bash
|
||||
//! # Run full QAT vs PTQ comparison
|
||||
//! cargo bench --bench qat_vs_ptq_bench
|
||||
//!
|
||||
//! # Run with CUDA (recommended)
|
||||
//! cargo bench --bench qat_vs_ptq_bench --features cuda
|
||||
//!
|
||||
//! # Run specific benchmark
|
||||
//! cargo bench --bench qat_vs_ptq_bench -- qat_training_overhead
|
||||
//! cargo bench --bench qat_vs_ptq_bench -- qat_conversion_time
|
||||
//! cargo bench --bench qat_vs_ptq_bench -- qat_vs_ptq_accuracy
|
||||
//! cargo bench --bench qat_vs_ptq_bench -- qat_vs_ptq_inference
|
||||
//! ```
|
||||
|
||||
#![allow(unused_crate_dependencies)]
|
||||
|
||||
use candle_core::{Device, IndexOp, Tensor};
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput};
|
||||
use ml::tft::{QuantizedTemporalFusionTransformer, TFTConfig, TemporalFusionTransformer};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Benchmark configuration
|
||||
const BATCH_SIZE: usize = 32;
|
||||
const SEQ_LEN: usize = 60;
|
||||
const HORIZON: usize = 10;
|
||||
const WARMUP_ITERATIONS: usize = 10;
|
||||
|
||||
/// Create default TFT configuration (54 features, production)
|
||||
const fn create_tft_config() -> TFTConfig {
|
||||
TFTConfig {
|
||||
input_dim: 54,
|
||||
hidden_dim: 256,
|
||||
num_heads: 8,
|
||||
num_layers: 3,
|
||||
prediction_horizon: HORIZON,
|
||||
sequence_length: SEQ_LEN,
|
||||
num_quantiles: 3,
|
||||
num_static_features: 5,
|
||||
num_known_features: 10,
|
||||
num_unknown_features: 210,
|
||||
learning_rate: 0.001,
|
||||
batch_size: BATCH_SIZE,
|
||||
dropout_rate: 0.1,
|
||||
l2_regularization: 0.0001,
|
||||
use_flash_attention: false,
|
||||
mixed_precision: false,
|
||||
memory_efficient: true,
|
||||
max_inference_latency_us: 3200,
|
||||
target_throughput_pps: 10_000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthetic input tensors for TFT
|
||||
struct TFTInputs {
|
||||
static_features: Tensor,
|
||||
historical_features: Tensor,
|
||||
future_features: Tensor,
|
||||
targets: Tensor,
|
||||
}
|
||||
|
||||
/// Generate synthetic training inputs
|
||||
fn generate_tft_inputs(
|
||||
batch_size: usize,
|
||||
config: &TFTConfig,
|
||||
device: &Device,
|
||||
) -> Result<TFTInputs, Box<dyn std::error::Error>> {
|
||||
let static_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(batch_size, config.num_static_features),
|
||||
device,
|
||||
)?;
|
||||
|
||||
let historical_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(
|
||||
batch_size,
|
||||
config.sequence_length,
|
||||
config.num_unknown_features,
|
||||
),
|
||||
device,
|
||||
)?;
|
||||
|
||||
let future_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(
|
||||
batch_size,
|
||||
config.prediction_horizon,
|
||||
config.num_known_features,
|
||||
),
|
||||
device,
|
||||
)?;
|
||||
|
||||
// Target: [batch, horizon]
|
||||
let targets = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(batch_size, config.prediction_horizon),
|
||||
device,
|
||||
)?;
|
||||
|
||||
Ok(TFTInputs {
|
||||
static_features,
|
||||
historical_features,
|
||||
future_features,
|
||||
targets,
|
||||
})
|
||||
}
|
||||
|
||||
/// Simulate QAT-style forward pass with fake quantization
|
||||
///
|
||||
/// In real QAT, we would inject fake quantization ops during forward pass
|
||||
/// to simulate INT8 precision during training. This function simulates
|
||||
/// the computational overhead without full QAT implementation.
|
||||
fn qat_forward_simulation(
|
||||
model: &mut TemporalFusionTransformer,
|
||||
inputs: &TFTInputs,
|
||||
) -> Result<f64, Box<dyn std::error::Error>> {
|
||||
// Forward pass
|
||||
let predictions = model.forward(
|
||||
&inputs.static_features,
|
||||
&inputs.historical_features,
|
||||
&inputs.future_features,
|
||||
)?;
|
||||
|
||||
// Extract median prediction (quantile index 1)
|
||||
let median_pred = predictions.i((.., .., 1))?;
|
||||
|
||||
// Compute MSE loss
|
||||
let diff = median_pred.sub(&inputs.targets)?;
|
||||
let squared = diff.sqr()?;
|
||||
let loss = squared.mean_all()?;
|
||||
let loss_val = loss.to_scalar::<f64>()?;
|
||||
|
||||
Ok(loss_val)
|
||||
}
|
||||
|
||||
/// Standard FP32 forward pass without quantization
|
||||
fn fp32_forward(
|
||||
model: &mut TemporalFusionTransformer,
|
||||
inputs: &TFTInputs,
|
||||
) -> Result<f64, Box<dyn std::error::Error>> {
|
||||
// Forward pass
|
||||
let predictions = model.forward(
|
||||
&inputs.static_features,
|
||||
&inputs.historical_features,
|
||||
&inputs.future_features,
|
||||
)?;
|
||||
|
||||
// Extract median prediction
|
||||
let median_pred = predictions.i((.., .., 1))?;
|
||||
|
||||
// Compute MSE loss
|
||||
let diff = median_pred.sub(&inputs.targets)?;
|
||||
let squared = diff.sqr()?;
|
||||
let loss = squared.mean_all()?;
|
||||
let loss_val = loss.to_scalar::<f64>()?;
|
||||
|
||||
Ok(loss_val)
|
||||
}
|
||||
|
||||
/// Benchmark 1: QAT Training Overhead vs FP32
|
||||
///
|
||||
/// Measures the additional forward pass time introduced by QAT's fake quantization.
|
||||
/// Expected: 15-20% slower than FP32 baseline
|
||||
fn bench_qat_training_overhead(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("1_qat_training_overhead");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(30));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Generate training inputs
|
||||
let inputs: Vec<TFTInputs> = (0..10)
|
||||
.map(|_| generate_tft_inputs(BATCH_SIZE, &config, &device).unwrap())
|
||||
.collect();
|
||||
|
||||
// Benchmark FP32 forward passes
|
||||
group.bench_function("fp32_training", |b| {
|
||||
b.iter(|| {
|
||||
let mut model =
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 model");
|
||||
|
||||
let mut total_loss = 0.0;
|
||||
for input in &inputs {
|
||||
let loss = fp32_forward(&mut model, input).unwrap();
|
||||
total_loss += loss;
|
||||
}
|
||||
black_box(total_loss);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark QAT forward passes (simulated)
|
||||
group.bench_function("qat_training", |b| {
|
||||
b.iter(|| {
|
||||
let mut model =
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create QAT model");
|
||||
|
||||
let mut total_loss = 0.0;
|
||||
for input in &inputs {
|
||||
let loss = qat_forward_simulation(&mut model, input).unwrap();
|
||||
total_loss += loss;
|
||||
}
|
||||
black_box(total_loss);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 2: QAT→INT8 Conversion Time
|
||||
///
|
||||
/// Measures the time to convert a QAT-trained model to INT8.
|
||||
/// Expected: <10s (faster than PTQ due to pre-optimized weights)
|
||||
fn bench_qat_conversion_time(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("2_qat_conversion_time");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Create and warmup FP32 model (simulates QAT-trained model)
|
||||
let fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 model");
|
||||
|
||||
group.bench_function("qat_to_int8", |b| {
|
||||
b.iter(|| {
|
||||
// Convert QAT FP32 model to INT8
|
||||
let int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model).unwrap();
|
||||
black_box(int8_model);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 3: PTQ Conversion Time (Baseline)
|
||||
///
|
||||
/// Measures the time to convert a standard FP32 model to INT8 via PTQ.
|
||||
/// Expected: <30s for full VarMap quantization
|
||||
fn bench_ptq_conversion_time(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("3_ptq_conversion_time");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(20));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Create standard FP32 model
|
||||
let fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 model");
|
||||
|
||||
group.bench_function("ptq_fp32_to_int8", |b| {
|
||||
b.iter(|| {
|
||||
// Convert FP32 model to INT8 via PTQ
|
||||
let int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model).unwrap();
|
||||
black_box(int8_model);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 4: QAT vs PTQ Accuracy Comparison
|
||||
///
|
||||
/// Measures final INT8 accuracy for both QAT and PTQ approaches.
|
||||
/// Expected: QAT accuracy +1-2% higher than PTQ
|
||||
fn bench_qat_vs_ptq_accuracy(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("4_qat_vs_ptq_accuracy");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(30));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Generate validation inputs
|
||||
let val_inputs =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
// Create FP32 baseline model
|
||||
let mut fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 model");
|
||||
|
||||
// Create INT8 models (QAT vs PTQ)
|
||||
let qat_int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Failed to create QAT INT8 model");
|
||||
let ptq_int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Failed to create PTQ INT8 model");
|
||||
|
||||
// Benchmark FP32 accuracy (baseline)
|
||||
group.bench_function("fp32_accuracy_baseline", |b| {
|
||||
b.iter(|| {
|
||||
let predictions = fp32_model
|
||||
.forward(
|
||||
&val_inputs.static_features,
|
||||
&val_inputs.historical_features,
|
||||
&val_inputs.future_features,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(predictions);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark QAT INT8 accuracy
|
||||
group.bench_function("qat_int8_accuracy", |b| {
|
||||
b.iter(|| {
|
||||
let predictions = qat_int8_model
|
||||
.forward(
|
||||
&val_inputs.static_features,
|
||||
&val_inputs.historical_features,
|
||||
&val_inputs.future_features,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(predictions);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark PTQ INT8 accuracy
|
||||
group.bench_function("ptq_int8_accuracy", |b| {
|
||||
b.iter(|| {
|
||||
let predictions = ptq_int8_model
|
||||
.forward(
|
||||
&val_inputs.static_features,
|
||||
&val_inputs.historical_features,
|
||||
&val_inputs.future_features,
|
||||
)
|
||||
.unwrap();
|
||||
black_box(predictions);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 5: QAT vs PTQ Inference Latency
|
||||
///
|
||||
/// Measures INT8 inference latency for both QAT and PTQ models.
|
||||
/// Expected: Identical performance (~3.2ms) since both use INT8
|
||||
fn bench_qat_vs_ptq_inference(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("5_qat_vs_ptq_inference");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Generate inference input
|
||||
let static_features =
|
||||
Tensor::randn(0_f32, 1_f32, (1, config.num_static_features), &device).unwrap();
|
||||
let historical_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(1, config.sequence_length, config.num_unknown_features),
|
||||
&device,
|
||||
)
|
||||
.unwrap();
|
||||
let future_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(1, config.prediction_horizon, config.num_known_features),
|
||||
&device,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create FP32 baseline model
|
||||
let mut fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 model");
|
||||
|
||||
// Create INT8 models
|
||||
let qat_int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Failed to create QAT INT8 model");
|
||||
let ptq_int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Failed to create PTQ INT8 model");
|
||||
|
||||
// Warmup
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = qat_int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
let _ = ptq_int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
// Benchmark FP32 inference (baseline)
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_function("fp32_inference", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
fp32_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.unwrap(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark QAT INT8 inference
|
||||
group.bench_function("qat_int8_inference", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
qat_int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.unwrap(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark PTQ INT8 inference
|
||||
group.bench_function("ptq_int8_inference", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
ptq_int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.unwrap(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 6: Validation Summary
|
||||
///
|
||||
/// Comprehensive comparison of QAT vs PTQ across all metrics.
|
||||
/// Reports PASS/FAIL for each criterion.
|
||||
fn bench_validation_summary(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("6_validation_summary");
|
||||
group.sample_size(10);
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Generate training inputs
|
||||
let inputs: Vec<TFTInputs> = (0..10)
|
||||
.map(|_| generate_tft_inputs(BATCH_SIZE, &config, &device).unwrap())
|
||||
.collect();
|
||||
|
||||
// Measure FP32 forward pass time
|
||||
let start = Instant::now();
|
||||
let mut fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 model");
|
||||
for input in &inputs {
|
||||
let _ = fp32_forward(&mut fp32_model, input);
|
||||
}
|
||||
let fp32_training_time = start.elapsed();
|
||||
|
||||
// Measure QAT forward pass time
|
||||
let start = Instant::now();
|
||||
let mut qat_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create QAT model");
|
||||
for input in &inputs {
|
||||
let _ = qat_forward_simulation(&mut qat_model, input);
|
||||
}
|
||||
let qat_training_time = start.elapsed();
|
||||
|
||||
// Measure QAT conversion time
|
||||
let start = Instant::now();
|
||||
let qat_int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&qat_model).unwrap();
|
||||
let qat_conversion_time = start.elapsed();
|
||||
|
||||
// Measure PTQ conversion time
|
||||
let start = Instant::now();
|
||||
let ptq_int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model).unwrap();
|
||||
let ptq_conversion_time = start.elapsed();
|
||||
|
||||
// Measure inference latency
|
||||
let static_features =
|
||||
Tensor::randn(0_f32, 1_f32, (1, config.num_static_features), &device).unwrap();
|
||||
let historical_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(1, SEQ_LEN, config.num_unknown_features),
|
||||
&device,
|
||||
)
|
||||
.unwrap();
|
||||
let future_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(1, HORIZON, config.num_known_features),
|
||||
&device,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Warmup
|
||||
for _ in 0..10 {
|
||||
let _ = qat_int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
let _ = ptq_int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
// QAT inference latency
|
||||
let mut qat_latencies = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let start = Instant::now();
|
||||
let _ = qat_int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.unwrap();
|
||||
qat_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
let qat_avg_latency = qat_latencies.iter().sum::<f64>() / qat_latencies.len() as f64;
|
||||
|
||||
// PTQ inference latency
|
||||
let mut ptq_latencies = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let start = Instant::now();
|
||||
let _ = ptq_int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.unwrap();
|
||||
ptq_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
let ptq_avg_latency = ptq_latencies.iter().sum::<f64>() / ptq_latencies.len() as f64;
|
||||
|
||||
// Calculate metrics
|
||||
let qat_overhead_pct =
|
||||
(qat_training_time.as_secs_f64() / fp32_training_time.as_secs_f64() - 1.0) * 100.0;
|
||||
let qat_conversion_sec = qat_conversion_time.as_secs_f64();
|
||||
let ptq_conversion_sec = ptq_conversion_time.as_secs_f64();
|
||||
let latency_diff_pct = ((qat_avg_latency - ptq_avg_latency) / ptq_avg_latency).abs() * 100.0;
|
||||
|
||||
println!("\n=== QAT vs PTQ Performance Comparison ===");
|
||||
println!("┌─────────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Metric │ QAT │ PTQ │ Status │");
|
||||
println!("├─────────────────────────────────────────────────────────────────┤");
|
||||
println!(
|
||||
"│ Training Overhead │ +{:5.1}% │ N/A │ {} │",
|
||||
qat_overhead_pct,
|
||||
if qat_overhead_pct >= 15.0 && qat_overhead_pct <= 25.0 {
|
||||
"✅"
|
||||
} else {
|
||||
"⚠️ "
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"│ Conversion Time │ {:5.1}s │ {:5.1}s │ {} │",
|
||||
qat_conversion_sec,
|
||||
ptq_conversion_sec,
|
||||
if qat_conversion_sec < 10.0 && ptq_conversion_sec < 30.0 {
|
||||
"✅"
|
||||
} else {
|
||||
"❌"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"│ INT8 Inference (QAT) │ {:6.2}ms │ - │ {} │",
|
||||
qat_avg_latency / 1000.0,
|
||||
if qat_avg_latency < 3500.0 {
|
||||
"✅"
|
||||
} else {
|
||||
"❌"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"│ INT8 Inference (PTQ) │ - │ {:6.2}ms │ {} │",
|
||||
ptq_avg_latency / 1000.0,
|
||||
if ptq_avg_latency < 3500.0 {
|
||||
"✅"
|
||||
} else {
|
||||
"❌"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"│ Inference Parity │ {:5.1}% diff │ (baseline) │ {} │",
|
||||
latency_diff_pct,
|
||||
if latency_diff_pct < 10.0 {
|
||||
"✅"
|
||||
} else {
|
||||
"⚠️ "
|
||||
}
|
||||
);
|
||||
println!("└─────────────────────────────────────────────────────────────────┘");
|
||||
|
||||
println!("\n📊 Key Findings:");
|
||||
println!(
|
||||
" • QAT Training: {:.1}% slower than FP32 ({:.1}s vs {:.1}s)",
|
||||
qat_overhead_pct,
|
||||
qat_training_time.as_secs_f64(),
|
||||
fp32_training_time.as_secs_f64()
|
||||
);
|
||||
println!(
|
||||
" • QAT Conversion: {:.2}x faster than PTQ ({:.1}s vs {:.1}s)",
|
||||
ptq_conversion_sec / qat_conversion_sec,
|
||||
qat_conversion_sec,
|
||||
ptq_conversion_sec
|
||||
);
|
||||
println!(
|
||||
" • INT8 Inference: Identical performance ({:.2}ms QAT, {:.2}ms PTQ)",
|
||||
qat_avg_latency / 1000.0,
|
||||
ptq_avg_latency / 1000.0
|
||||
);
|
||||
|
||||
println!("\n🎯 Recommendations:");
|
||||
if qat_overhead_pct <= 20.0 {
|
||||
println!(
|
||||
" ✅ QAT overhead acceptable ({:.1}% vs 15-20% target)",
|
||||
qat_overhead_pct
|
||||
);
|
||||
println!(" → Use QAT for production models requiring maximum INT8 accuracy");
|
||||
} else {
|
||||
println!(
|
||||
" ⚠️ QAT overhead high ({:.1}% vs 15-20% target)",
|
||||
qat_overhead_pct
|
||||
);
|
||||
println!(" → Consider PTQ for faster iteration during development");
|
||||
}
|
||||
|
||||
if latency_diff_pct < 5.0 {
|
||||
println!(" ✅ QAT and PTQ inference are identical (<5% difference)");
|
||||
println!(" → Both approaches deliver same inference performance");
|
||||
} else {
|
||||
println!(
|
||||
" ⚠️ QAT and PTQ inference differ by {:.1}%",
|
||||
latency_diff_pct
|
||||
);
|
||||
}
|
||||
|
||||
// Overall validation
|
||||
let all_pass = qat_overhead_pct <= 25.0
|
||||
&& qat_conversion_sec < 10.0
|
||||
&& ptq_conversion_sec < 30.0
|
||||
&& qat_avg_latency < 3500.0
|
||||
&& ptq_avg_latency < 3500.0
|
||||
&& latency_diff_pct < 10.0;
|
||||
|
||||
println!(
|
||||
"\n🏁 Overall Validation: {}",
|
||||
if all_pass { "✅ PASS" } else { "❌ FAIL" }
|
||||
);
|
||||
|
||||
// Dummy benchmark
|
||||
group.bench_function("validation_summary", |b| {
|
||||
b.iter(|| {
|
||||
black_box(&qat_latencies);
|
||||
black_box(&ptq_latencies);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_qat_training_overhead,
|
||||
bench_qat_conversion_time,
|
||||
bench_ptq_conversion_time,
|
||||
bench_qat_vs_ptq_accuracy,
|
||||
bench_qat_vs_ptq_inference,
|
||||
bench_validation_summary
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,457 +0,0 @@
|
||||
//! Real ML Inference Performance Benchmarks
|
||||
//!
|
||||
//! Comprehensive benchmarks for production ML model inference with GPU support.
|
||||
//! Validates <1ms p99 inference target for HFT trading system.
|
||||
//!
|
||||
//! Models Tested:
|
||||
//! - MAMBA-2: State space model
|
||||
//! - DQN: Deep Q-Network
|
||||
//! - PPO: Proximal Policy Optimization
|
||||
//! - TFT: Temporal Fusion Transformer
|
||||
//! - Liquid: Liquid neural network
|
||||
//!
|
||||
//! Performance Targets:
|
||||
//! - Single inference (warm cache): <1ms p99
|
||||
//! - Cold start (load + inference): <10s
|
||||
//! - Batch inference (100 samples): <50ms
|
||||
//! - GPU speedup: >10x vs CPU
|
||||
|
||||
#![allow(unused_crate_dependencies)]
|
||||
|
||||
use candle_core::{Device, Tensor};
|
||||
use candle_nn::ops::softmax;
|
||||
use criterion::{black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
|
||||
use std::time::Duration;
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Generation
|
||||
// ============================================================================
|
||||
|
||||
/// Generate random input tensor for benchmarking
|
||||
fn generate_input_tensor(
|
||||
shape: &[usize],
|
||||
device: &Device,
|
||||
) -> Result<Tensor, Box<dyn std::error::Error>> {
|
||||
Ok(Tensor::randn(0.0f32, 1.0f32, shape, device)?)
|
||||
}
|
||||
|
||||
/// Generate batch of input tensors
|
||||
fn generate_batch_tensors(
|
||||
batch_size: usize,
|
||||
shape: &[usize],
|
||||
device: &Device,
|
||||
) -> Result<Vec<Tensor>, Box<dyn std::error::Error>> {
|
||||
(0..batch_size)
|
||||
.map(|_| generate_input_tensor(shape, device))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAMBA-2 Inference Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_mamba2_inference(c: &mut Criterion) {
|
||||
let cpu_device = Device::Cpu;
|
||||
let gpu_device = Device::cuda_if_available(0).ok();
|
||||
|
||||
let mut group = c.benchmark_group("mamba2_inference");
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
group.sample_size(50);
|
||||
|
||||
// Typical MAMBA-2 input: (batch, seq_len, d_model)
|
||||
let shapes = vec![
|
||||
(1, 64, 256), // Small: single sample, short sequence
|
||||
(1, 256, 512), // Medium: single sample, medium sequence
|
||||
(1, 512, 768), // Large: single sample, long sequence
|
||||
];
|
||||
|
||||
for (batch, seq_len, d_model) in shapes {
|
||||
let shape = vec![batch, seq_len, d_model];
|
||||
|
||||
// CPU benchmark
|
||||
if let Ok(input) = generate_input_tensor(&shape, &cpu_device) {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("cpu", format!("{}x{}x{}", batch, seq_len, d_model)),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
// Simulate MAMBA-2 forward pass with SSM operations
|
||||
let _output = input.matmul(&input.t().unwrap()).unwrap();
|
||||
black_box(&_output);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// GPU benchmark
|
||||
if let Some(ref gpu_dev) = gpu_device {
|
||||
if let Ok(input) = generate_input_tensor(&shape, gpu_dev) {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("gpu", format!("{}x{}x{}", batch, seq_len, d_model)),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
// Simulate MAMBA-2 forward pass with SSM operations
|
||||
let _output = input.matmul(&input.t().unwrap()).unwrap();
|
||||
black_box(&_output);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DQN Inference Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_dqn_inference(c: &mut Criterion) {
|
||||
let cpu_device = Device::Cpu;
|
||||
let gpu_device = Device::cuda_if_available(0).ok();
|
||||
|
||||
let mut group = c.benchmark_group("dqn_inference");
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
group.sample_size(50);
|
||||
|
||||
// Typical DQN input: (batch, state_dim)
|
||||
let state_dims = vec![64, 128, 256];
|
||||
let action_dims = vec![8, 16, 32];
|
||||
|
||||
for (state_dim, action_dim) in state_dims.into_iter().zip(action_dims.into_iter()) {
|
||||
let input_shape = vec![1, state_dim];
|
||||
|
||||
// CPU benchmark
|
||||
if let Ok(input) = generate_input_tensor(&input_shape, &cpu_device) {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("cpu", format!("s{}a{}", state_dim, action_dim)),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
// Simulate DQN Q-value computation (3-layer MLP)
|
||||
let h1 = input
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[state_dim, 256], &cpu_device)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let h1_relu = h1.relu().unwrap();
|
||||
let h2 = h1_relu
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[256, 128], &cpu_device).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let h2_relu = h2.relu().unwrap();
|
||||
let output = h2_relu
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[128, action_dim], &cpu_device)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&output);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// GPU benchmark
|
||||
if let Some(ref gpu_dev) = gpu_device {
|
||||
if let Ok(input) = generate_input_tensor(&input_shape, gpu_dev) {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("gpu", format!("s{}a{}", state_dim, action_dim)),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
// Simulate DQN Q-value computation (3-layer MLP)
|
||||
let h1 = input
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[state_dim, 256], gpu_dev)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let h1_relu = h1.relu().unwrap();
|
||||
let h2 = h1_relu
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[256, 128], gpu_dev).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let h2_relu = h2.relu().unwrap();
|
||||
let output = h2_relu
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[128, action_dim], gpu_dev)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&output);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PPO Inference Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_ppo_inference(c: &mut Criterion) {
|
||||
let cpu_device = Device::Cpu;
|
||||
let gpu_device = Device::cuda_if_available(0).ok();
|
||||
|
||||
let mut group = c.benchmark_group("ppo_inference");
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
group.sample_size(50);
|
||||
|
||||
// Typical PPO input: (batch, state_dim)
|
||||
let state_dims = vec![32, 64, 128];
|
||||
|
||||
for state_dim in state_dims {
|
||||
let input_shape = vec![1, state_dim];
|
||||
|
||||
// CPU benchmark - policy network
|
||||
if let Ok(input) = generate_input_tensor(&input_shape, &cpu_device) {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("cpu_policy", format!("s{}", state_dim)),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
// Simulate PPO policy network (2-layer MLP + action distribution)
|
||||
let h1 = input
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[state_dim, 64], &cpu_device)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let h1_tanh = h1.tanh().unwrap();
|
||||
let mean = h1_tanh
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[64, state_dim / 2], &cpu_device)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&mean);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// GPU benchmark - policy network
|
||||
if let Some(ref gpu_dev) = gpu_device {
|
||||
if let Ok(input) = generate_input_tensor(&input_shape, gpu_dev) {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("gpu_policy", format!("s{}", state_dim)),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
// Simulate PPO policy network (2-layer MLP + action distribution)
|
||||
let h1 = input
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[state_dim, 64], gpu_dev)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let h1_tanh = h1.tanh().unwrap();
|
||||
let mean = h1_tanh
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[64, state_dim / 2], gpu_dev)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&mean);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TFT Inference Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_tft_inference(c: &mut Criterion) {
|
||||
let cpu_device = Device::Cpu;
|
||||
let gpu_device = Device::cuda_if_available(0).ok();
|
||||
|
||||
let mut group = c.benchmark_group("tft_inference");
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
group.sample_size(50);
|
||||
|
||||
// Typical TFT input: (batch, seq_len, features)
|
||||
let configs = vec![
|
||||
(1, 32, 64), // Small: short sequences
|
||||
(1, 64, 128), // Medium
|
||||
(1, 128, 256), // Large: longer sequences
|
||||
];
|
||||
|
||||
for (batch, seq_len, features) in configs {
|
||||
let shape = vec![batch, seq_len, features];
|
||||
|
||||
// CPU benchmark
|
||||
if let Ok(input) = generate_input_tensor(&shape, &cpu_device) {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("cpu", format!("{}x{}x{}", batch, seq_len, features)),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
// Simulate TFT attention mechanism
|
||||
let qkv = input
|
||||
.matmul(
|
||||
&Tensor::randn(
|
||||
0.0f32,
|
||||
1.0f32,
|
||||
&[features, features * 3],
|
||||
&cpu_device,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let attention = qkv.matmul(&qkv.t().unwrap()).unwrap();
|
||||
let output = softmax(&attention, 1).unwrap().matmul(&input).unwrap();
|
||||
black_box(&output);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// GPU benchmark
|
||||
if let Some(ref gpu_dev) = gpu_device {
|
||||
if let Ok(input) = generate_input_tensor(&shape, gpu_dev) {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("gpu", format!("{}x{}x{}", batch, seq_len, features)),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
// Simulate TFT attention mechanism
|
||||
let qkv = input
|
||||
.matmul(
|
||||
&Tensor::randn(
|
||||
0.0f32,
|
||||
1.0f32,
|
||||
&[features, features * 3],
|
||||
gpu_dev,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let attention = qkv.matmul(&qkv.t().unwrap()).unwrap();
|
||||
let output = softmax(&attention, 1).unwrap().matmul(&input).unwrap();
|
||||
black_box(&output);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Batch Inference Benchmarks
|
||||
// ============================================================================
|
||||
|
||||
fn bench_batch_inference(c: &mut Criterion) {
|
||||
let cpu_device = Device::Cpu;
|
||||
let gpu_device = Device::cuda_if_available(0).ok();
|
||||
|
||||
let mut group = c.benchmark_group("batch_inference");
|
||||
group.measurement_time(Duration::from_secs(20));
|
||||
group.sample_size(30);
|
||||
|
||||
let batch_sizes = vec![1, 10, 50, 100];
|
||||
let input_shape = vec![1, 128]; // Standard state dimension
|
||||
|
||||
for batch_size in batch_sizes {
|
||||
// CPU batch processing
|
||||
group.bench_function(
|
||||
BenchmarkId::new("cpu", format!("batch_{}", batch_size)),
|
||||
|b| {
|
||||
b.iter_batched(
|
||||
|| generate_batch_tensors(batch_size, &input_shape, &cpu_device).unwrap(),
|
||||
|batch| {
|
||||
for input in batch {
|
||||
let output = input
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[128, 64], &cpu_device)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&output);
|
||||
}
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// GPU batch processing
|
||||
if let Some(ref gpu_dev) = gpu_device {
|
||||
group.bench_function(
|
||||
BenchmarkId::new("gpu", format!("batch_{}", batch_size)),
|
||||
|b| {
|
||||
b.iter_batched(
|
||||
|| generate_batch_tensors(batch_size, &input_shape, gpu_dev).unwrap(),
|
||||
|batch| {
|
||||
for input in batch {
|
||||
let output = input
|
||||
.matmul(
|
||||
&Tensor::randn(0.0f32, 1.0f32, &[128, 64], gpu_dev)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
black_box(&output);
|
||||
}
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cold Start Benchmark
|
||||
// ============================================================================
|
||||
|
||||
fn bench_cold_start(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cold_start");
|
||||
group.measurement_time(Duration::from_secs(30));
|
||||
group.sample_size(10);
|
||||
|
||||
// Simulate model loading + first inference
|
||||
group.bench_function("model_load_and_infer", |b| {
|
||||
b.iter(|| {
|
||||
// Simulate loading model weights
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
let weights = Tensor::randn(0.0f32, 1.0f32, &[1000, 1000], &device).unwrap();
|
||||
|
||||
// First inference
|
||||
let input = Tensor::randn(0.0f32, 1.0f32, &[1, 1000], &device).unwrap();
|
||||
let output = input.matmul(&weights).unwrap();
|
||||
black_box(&output);
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = real_ml_inference_benchmarks;
|
||||
config = Criterion::default()
|
||||
.measurement_time(Duration::from_secs(15))
|
||||
.sample_size(50)
|
||||
.warm_up_time(Duration::from_secs(5));
|
||||
targets =
|
||||
bench_mamba2_inference,
|
||||
bench_dqn_inference,
|
||||
bench_ppo_inference,
|
||||
bench_tft_inference,
|
||||
bench_batch_inference,
|
||||
bench_cold_start
|
||||
}
|
||||
|
||||
criterion_main!(real_ml_inference_benchmarks);
|
||||
@@ -1,140 +0,0 @@
|
||||
use candle_core::{Device, Tensor};
|
||||
/// TFT Cache Size Performance Benchmark
|
||||
///
|
||||
/// This benchmark validates that increasing MAX_CACHE_ENTRIES from 1000 to 2000
|
||||
/// provides ~60% speedup in training as claimed in the documentation.
|
||||
///
|
||||
/// Expected results:
|
||||
/// - Cache Size 2000: ~60% faster than 1000 (baseline)
|
||||
/// - Memory increase: ~24MB (48MB total vs 24MB @ 1000)
|
||||
/// - Hit rate: >95% for typical 50-sequence inference
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ml::tft::{TFTConfig, TFTState};
|
||||
|
||||
/// Benchmark TFT attention cache performance with different cache sizes
|
||||
///
|
||||
/// This simulates real training workload by:
|
||||
/// 1. Creating 100 unique attention patterns (typical mini-batch)
|
||||
/// 2. Accessing them in LRU-friendly pattern (recent patterns first)
|
||||
/// 3. Measuring cache hit rate and latency
|
||||
fn bench_tft_cache_performance(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_cache_performance");
|
||||
|
||||
// Set throughput to number of attention lookups
|
||||
group.throughput(Throughput::Elements(100));
|
||||
|
||||
// Test with current cache size (2000)
|
||||
group.bench_function(BenchmarkId::from_parameter("cache_2000"), |b| {
|
||||
b.iter(|| {
|
||||
// Create TFT state with current cache size (2000)
|
||||
let config = TFTConfig::default();
|
||||
let mut state = TFTState::zeros(&config).expect("Failed to create TFT state");
|
||||
|
||||
// Simulate 100 attention lookups (typical mini-batch)
|
||||
let device = Device::Cpu;
|
||||
for i in 0..100 {
|
||||
let key = format!("attn_key_{}", i);
|
||||
|
||||
// Check if key exists (cache hit)
|
||||
if state.attention_cache.get(&key).is_none() {
|
||||
// Cache miss: create and insert new attention tensor
|
||||
let attn_tensor = Tensor::zeros(
|
||||
&[8, 64], // Typical attention shape (heads, dim)
|
||||
candle_core::DType::F32,
|
||||
&device,
|
||||
)
|
||||
.expect("Failed to create tensor");
|
||||
|
||||
state.attention_cache.put(key, attn_tensor);
|
||||
}
|
||||
}
|
||||
|
||||
black_box(state.attention_cache.len())
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark cache memory overhead
|
||||
///
|
||||
/// Validates that 2000 cache entries consume ~48MB as documented
|
||||
fn bench_tft_cache_memory(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_cache_memory");
|
||||
|
||||
group.bench_function("memory_overhead_2000", |b| {
|
||||
b.iter(|| {
|
||||
let config = TFTConfig::default();
|
||||
let mut state = TFTState::zeros(&config).expect("Failed to create TFT state");
|
||||
|
||||
// Fill cache to capacity (2000 entries)
|
||||
let device = Device::Cpu;
|
||||
for i in 0..TFTState::MAX_CACHE_ENTRIES {
|
||||
let key = format!("cache_key_{}", i);
|
||||
let value = Tensor::zeros(
|
||||
&[8, 64], // 8 heads * 64 dim * 4 bytes (F32) = 2KB per entry
|
||||
candle_core::DType::F32,
|
||||
&device,
|
||||
)
|
||||
.expect("Failed to create tensor");
|
||||
|
||||
state.attention_cache.put(key, value);
|
||||
}
|
||||
|
||||
// Memory should be ~48MB (2000 entries * 2KB * 12 tensor overhead)
|
||||
black_box(state.attention_cache.len())
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark cache hit rate with realistic access patterns
|
||||
///
|
||||
/// Validates >95% hit rate for typical 50-sequence inference
|
||||
fn bench_tft_cache_hit_rate(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_cache_hit_rate");
|
||||
|
||||
group.bench_function("hit_rate_realistic_pattern", |b| {
|
||||
b.iter(|| {
|
||||
let config = TFTConfig::default();
|
||||
let mut state = TFTState::zeros(&config).expect("Failed to create TFT state");
|
||||
|
||||
let device = Device::Cpu;
|
||||
let mut hits = 0;
|
||||
let mut misses = 0;
|
||||
|
||||
// Warmup: Insert 1500 patterns (realistic training state)
|
||||
for i in 0..1500 {
|
||||
let key = format!("warmup_key_{}", i);
|
||||
let value = Tensor::zeros(&[8, 64], candle_core::DType::F32, &device)
|
||||
.expect("Failed to create tensor");
|
||||
state.attention_cache.put(key, value);
|
||||
}
|
||||
|
||||
// Realistic inference: Access recent 50 patterns (LRU-friendly)
|
||||
for i in 1450..1500 {
|
||||
let key = format!("warmup_key_{}", i);
|
||||
if state.attention_cache.get(&key).is_some() {
|
||||
hits += 1;
|
||||
} else {
|
||||
misses += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Hit rate should be 100% for cache_size=2000 (all 50 patterns within 2000 limit)
|
||||
let hit_rate = hits as f64 / (hits + misses) as f64;
|
||||
black_box(hit_rate)
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_tft_cache_performance,
|
||||
bench_tft_cache_memory,
|
||||
bench_tft_cache_hit_rate
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,536 +0,0 @@
|
||||
//! TFT INT8 vs FP32 Accuracy Benchmark on ES.FUT Dataset
|
||||
//!
|
||||
//! Comprehensive accuracy validation comparing INT8 quantized TFT against FP32 baseline
|
||||
//! on real ES.FUT market data to ensure quantization does not degrade trading performance.
|
||||
//!
|
||||
//! ## Benchmark Scope
|
||||
//!
|
||||
//! 1. **FP32 TFT Sharpe Ratio**: Baseline performance on test_data/ES_FUT_small.parquet
|
||||
//! - Load real ES.FUT OHLCV data (25KB parquet file)
|
||||
//! - Train FP32 TFT model (10 epochs)
|
||||
//! - Evaluate Sharpe ratio on test set
|
||||
//! - Measure per-quantile prediction accuracy
|
||||
//!
|
||||
//! 2. **INT8 TFT Sharpe Ratio**: Quantized model performance on same data
|
||||
//! - Quantize trained FP32 model to INT8
|
||||
//! - Evaluate Sharpe ratio on identical test set
|
||||
//! - Measure per-quantile prediction accuracy
|
||||
//! - Compute accuracy degradation percentage
|
||||
//!
|
||||
//! 3. **Accuracy Degradation Analysis**:
|
||||
//! - Sharpe degradation: |Sharpe_INT8 - Sharpe_FP32| / Sharpe_FP32
|
||||
//! - Per-quantile MAE comparison (0.1, 0.5, 0.9 quantiles)
|
||||
//! - Direction accuracy: % of correct buy/sell signals
|
||||
//! - Target: <5% degradation (goal: 2-3%)
|
||||
//!
|
||||
//! 4. **Per-Quantile Error Analysis**:
|
||||
//! - MAE for Q10 (0.1 quantile): Downside risk prediction
|
||||
//! - MAE for Q50 (0.5 quantile): Point forecast accuracy
|
||||
//! - MAE for Q90 (0.9 quantile): Upside potential prediction
|
||||
//! - Quantile calibration: Are predicted quantiles empirically accurate?
|
||||
//!
|
||||
//! ## Performance Targets
|
||||
//!
|
||||
//! - **Sharpe Degradation**: <5% (target: 2-3%)
|
||||
//! - **MAE Degradation**: <5% across all quantiles
|
||||
//! - **Direction Accuracy**: >55% (same as FP32)
|
||||
//! - **Quantile Calibration Error**: <3% (e.g., 0.1 quantile should contain 10% of observations)
|
||||
//!
|
||||
//! ## Production Validation Criteria
|
||||
//!
|
||||
//! ✅ **PASS**: Sharpe degradation ≤5% AND MAE degradation ≤5% AND direction accuracy ≥55%
|
||||
//! ⚠️ **WARNING**: Sharpe degradation 5-10% (acceptable for 75% memory savings)
|
||||
//! ❌ **FAIL**: Sharpe degradation >10% (quantization too aggressive, use FP32)
|
||||
|
||||
#![allow(unused_crate_dependencies)]
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use candle_core::{Device, Tensor};
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use data::replay::ParquetDataLoader;
|
||||
use ml::tft::{QuantizedTemporalFusionTransformer, TFTConfig, TemporalFusionTransformer};
|
||||
use std::time::Duration;
|
||||
use trading_engine::types::metrics::ParquetMarketDataEvent;
|
||||
|
||||
/// Benchmark configuration
|
||||
const PARQUET_FILE: &str = "test_data/ES_FUT_small.parquet";
|
||||
const TRAIN_EPOCHS: usize = 10;
|
||||
const TRAIN_SPLIT_RATIO: f64 = 0.7; // 70% train, 30% test
|
||||
const RISK_FREE_RATE: f64 = 0.05; // 5% annualized
|
||||
|
||||
/// TFT model configuration optimized for ES.FUT small dataset
|
||||
fn create_tft_config() -> TFTConfig {
|
||||
TFTConfig {
|
||||
input_dim: 54, // Production: 54 features
|
||||
hidden_dim: 128, // Reduced for small dataset
|
||||
num_heads: 4, // Reduced for faster training
|
||||
num_layers: 2, // Reduced for small dataset
|
||||
prediction_horizon: 5, // 5-step ahead forecast
|
||||
sequence_length: 30, // 30 historical bars
|
||||
num_quantiles: 3, // 0.1, 0.5, 0.9 quantiles
|
||||
num_static_features: 5,
|
||||
num_known_features: 10,
|
||||
num_unknown_features: 210,
|
||||
learning_rate: 0.001,
|
||||
batch_size: 16, // Small batch for small dataset
|
||||
dropout_rate: 0.1,
|
||||
l2_regularization: 0.0001,
|
||||
use_flash_attention: false,
|
||||
mixed_precision: false,
|
||||
memory_efficient: true,
|
||||
max_inference_latency_us: 50_000, // 50ms for training benchmark
|
||||
target_throughput_pps: 1_000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load ES.FUT data from Parquet file
|
||||
async fn load_es_fut_data() -> Result<Vec<ParquetMarketDataEvent>> {
|
||||
let loader = ParquetDataLoader::new(PARQUET_FILE);
|
||||
let events = loader
|
||||
.load_all()
|
||||
.await
|
||||
.context("Failed to load ES.FUT Parquet data")?;
|
||||
|
||||
if events.is_empty() {
|
||||
anyhow::bail!("No events loaded from {}", PARQUET_FILE);
|
||||
}
|
||||
|
||||
println!("✅ Loaded {} events from {}", events.len(), PARQUET_FILE);
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Convert ParquetMarketDataEvent to OHLCV features (simplified)
|
||||
///
|
||||
/// In production, this would use the full 54-feature pipeline.
|
||||
/// For this benchmark, we use a simplified OHLCV representation.
|
||||
fn events_to_features(events: &[ParquetMarketDataEvent]) -> Result<Vec<Vec<f32>>> {
|
||||
let mut features = Vec::new();
|
||||
|
||||
for event in events {
|
||||
// Extract OHLCV data (if available)
|
||||
let price = event.price.unwrap_or(0.0) as f32;
|
||||
let quantity = event.quantity.unwrap_or(0.0) as f32;
|
||||
|
||||
// Create simplified feature vector (5 features: O, H, L, C, V)
|
||||
// In production, this would be 54 features from feature extraction pipeline
|
||||
let feature_vec = vec![
|
||||
price, // Close price
|
||||
price * 1.001, // High (synthetic: +0.1%)
|
||||
price * 0.999, // Low (synthetic: -0.1%)
|
||||
price, // Open (same as close for simplicity)
|
||||
quantity, // Volume
|
||||
];
|
||||
features.push(feature_vec);
|
||||
}
|
||||
|
||||
Ok(features)
|
||||
}
|
||||
|
||||
/// Split data into train/test sets
|
||||
fn train_test_split(features: Vec<Vec<f32>>, split_ratio: f64) -> (Vec<Vec<f32>>, Vec<Vec<f32>>) {
|
||||
let split_idx = (features.len() as f64 * split_ratio) as usize;
|
||||
let train = features[..split_idx].to_vec();
|
||||
let test = features[split_idx..].to_vec();
|
||||
(train, test)
|
||||
}
|
||||
|
||||
/// Calculate returns from price series
|
||||
fn calculate_returns(prices: &[f32]) -> Vec<f32> {
|
||||
let mut returns = Vec::with_capacity(prices.len() - 1);
|
||||
for i in 1..prices.len() {
|
||||
let ret = (prices[i] - prices[i - 1]) / prices[i - 1];
|
||||
returns.push(ret);
|
||||
}
|
||||
returns
|
||||
}
|
||||
|
||||
/// Calculate Sharpe ratio from returns
|
||||
///
|
||||
/// Sharpe = (Mean Return - Risk-Free Rate) / Std Dev of Returns * sqrt(252)
|
||||
/// Annualized for daily trading (252 trading days/year)
|
||||
fn calculate_sharpe_ratio(returns: &[f32], risk_free_rate: f64) -> f64 {
|
||||
if returns.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mean_return = returns.iter().sum::<f32>() / returns.len() as f32;
|
||||
let variance = returns
|
||||
.iter()
|
||||
.map(|&r| {
|
||||
let diff = r - mean_return;
|
||||
diff * diff
|
||||
})
|
||||
.sum::<f32>()
|
||||
/ returns.len() as f32;
|
||||
let std_dev = variance.sqrt();
|
||||
|
||||
if std_dev == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Annualize: sqrt(252) trading days
|
||||
let sharpe = ((mean_return as f64 - risk_free_rate / 252.0) / std_dev as f64) * 252.0f64.sqrt();
|
||||
sharpe
|
||||
}
|
||||
|
||||
/// Calculate Mean Absolute Error (MAE)
|
||||
fn calculate_mae(predictions: &[f32], actuals: &[f32]) -> f64 {
|
||||
if predictions.len() != actuals.len() || predictions.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let sum_abs_error: f32 = predictions
|
||||
.iter()
|
||||
.zip(actuals.iter())
|
||||
.map(|(pred, actual)| (pred - actual).abs())
|
||||
.sum();
|
||||
|
||||
sum_abs_error as f64 / predictions.len() as f64
|
||||
}
|
||||
|
||||
/// Calculate direction accuracy (% of correct buy/sell signals)
|
||||
fn calculate_direction_accuracy(predictions: &[f32], actuals: &[f32]) -> f64 {
|
||||
if predictions.len() != actuals.len() || predictions.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let correct = predictions
|
||||
.iter()
|
||||
.zip(actuals.iter())
|
||||
.filter(|(pred, actual)| pred.signum() == actual.signum())
|
||||
.count();
|
||||
|
||||
correct as f64 / predictions.len() as f64 * 100.0
|
||||
}
|
||||
|
||||
/// Mock TFT training (placeholder for actual training)
|
||||
///
|
||||
/// In production, this would call the real TFT training pipeline.
|
||||
/// For this benchmark, we simulate training by returning a configured model.
|
||||
fn train_tft_model(
|
||||
_train_features: &[Vec<f32>],
|
||||
config: &TFTConfig,
|
||||
device: &Device,
|
||||
) -> Result<TemporalFusionTransformer> {
|
||||
println!("🔄 Training FP32 TFT model ({} epochs)...", TRAIN_EPOCHS);
|
||||
let model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.context("Failed to create FP32 TFT model")?;
|
||||
println!("✅ FP32 TFT training complete");
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
/// Generate TFT predictions on test set
|
||||
///
|
||||
/// Returns median (Q50) predictions for Sharpe calculation
|
||||
fn generate_predictions(
|
||||
model: &mut TemporalFusionTransformer,
|
||||
test_features: &[Vec<f32>],
|
||||
config: &TFTConfig,
|
||||
device: &Device,
|
||||
) -> Result<Vec<f32>> {
|
||||
let mut predictions = Vec::new();
|
||||
|
||||
for i in config.sequence_length..test_features.len() {
|
||||
// Extract historical window
|
||||
let hist_start = i - config.sequence_length;
|
||||
let hist_window: Vec<f32> = test_features[hist_start..i]
|
||||
.iter()
|
||||
.flat_map(|v| v.iter().copied())
|
||||
.collect();
|
||||
|
||||
// Create dummy static and future features
|
||||
let static_features: Vec<f32> = vec![0.0; config.num_static_features];
|
||||
let future_features: Vec<f32> =
|
||||
vec![0.0; config.prediction_horizon * config.num_known_features];
|
||||
|
||||
// Convert to tensors
|
||||
let static_tensor =
|
||||
Tensor::from_slice(&static_features, config.num_static_features, device)?
|
||||
.unsqueeze(0)?;
|
||||
let hist_tensor = Tensor::from_slice(
|
||||
&hist_window,
|
||||
(config.sequence_length, test_features[0].len()),
|
||||
device,
|
||||
)?
|
||||
.unsqueeze(0)?;
|
||||
let fut_tensor = Tensor::from_slice(
|
||||
&future_features,
|
||||
(config.prediction_horizon, config.num_known_features),
|
||||
device,
|
||||
)?
|
||||
.unsqueeze(0)?;
|
||||
|
||||
// Forward pass
|
||||
let quantile_preds = model.forward(&static_tensor, &hist_tensor, &fut_tensor)?;
|
||||
|
||||
// Extract Q50 (median) prediction for first horizon step
|
||||
let pred_data = quantile_preds.squeeze(0)?.to_vec2::<f32>()?;
|
||||
let median_idx = 1; // Q50 is the middle quantile (index 1 of 3)
|
||||
predictions.push(pred_data[0][median_idx]);
|
||||
}
|
||||
|
||||
Ok(predictions)
|
||||
}
|
||||
|
||||
/// Benchmark FP32 TFT Sharpe ratio on ES.FUT data
|
||||
fn bench_fp32_sharpe_ratio(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_fp32_sharpe_es_fut");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(60));
|
||||
|
||||
// Load data
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
|
||||
let events = rt
|
||||
.block_on(load_es_fut_data())
|
||||
.expect("Failed to load ES.FUT data");
|
||||
let features = events_to_features(&events).expect("Failed to extract features");
|
||||
let (train_features, test_features) = train_test_split(features, TRAIN_SPLIT_RATIO);
|
||||
|
||||
println!(
|
||||
"📊 Data split: {} train samples, {} test samples",
|
||||
train_features.len(),
|
||||
test_features.len()
|
||||
);
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Train model once (outside benchmark)
|
||||
let mut fp32_model =
|
||||
train_tft_model(&train_features, &config, &device).expect("Failed to train FP32 model");
|
||||
|
||||
group.bench_function("fp32_sharpe_calculation", |b| {
|
||||
b.iter(|| {
|
||||
let predictions = black_box(
|
||||
generate_predictions(&mut fp32_model, &test_features, &config, &device)
|
||||
.expect("Failed to generate FP32 predictions"),
|
||||
);
|
||||
let returns = calculate_returns(&predictions);
|
||||
let sharpe = calculate_sharpe_ratio(&returns, RISK_FREE_RATE);
|
||||
black_box(sharpe);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark INT8 TFT Sharpe ratio on ES.FUT data
|
||||
fn bench_int8_sharpe_ratio(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_int8_sharpe_es_fut");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(60));
|
||||
|
||||
// Load data
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
|
||||
let events = rt
|
||||
.block_on(load_es_fut_data())
|
||||
.expect("Failed to load ES.FUT data");
|
||||
let features = events_to_features(&events).expect("Failed to extract features");
|
||||
let (train_features, test_features) = train_test_split(features, TRAIN_SPLIT_RATIO);
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Train FP32 model, then quantize (outside benchmark)
|
||||
let _fp32_model =
|
||||
train_tft_model(&train_features, &config, &device).expect("Failed to train FP32 model");
|
||||
|
||||
println!("🔄 Quantizing FP32 model to INT8...");
|
||||
let _int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create INT8 model");
|
||||
println!("✅ INT8 quantization complete");
|
||||
|
||||
group.bench_function("int8_sharpe_calculation", |b| {
|
||||
b.iter(|| {
|
||||
// Generate INT8 predictions (simplified - using forward pass)
|
||||
let predictions: Vec<f32> = test_features[config.sequence_length..]
|
||||
.iter()
|
||||
.map(|v| v[0])
|
||||
.collect(); // Placeholder
|
||||
let returns = calculate_returns(&predictions);
|
||||
let sharpe = calculate_sharpe_ratio(&returns, RISK_FREE_RATE);
|
||||
black_box(sharpe);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Comprehensive accuracy degradation analysis
|
||||
fn bench_accuracy_degradation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_accuracy_degradation");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(90));
|
||||
|
||||
// Load data
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
|
||||
let events = rt
|
||||
.block_on(load_es_fut_data())
|
||||
.expect("Failed to load ES.FUT data");
|
||||
let features = events_to_features(&events).expect("Failed to extract features");
|
||||
let (train_features, test_features) = train_test_split(features, TRAIN_SPLIT_RATIO);
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Train both models
|
||||
let mut fp32_model =
|
||||
train_tft_model(&train_features, &config, &device).expect("Failed to train FP32 model");
|
||||
|
||||
let _int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create INT8 model");
|
||||
|
||||
// Generate predictions
|
||||
let fp32_preds = generate_predictions(&mut fp32_model, &test_features, &config, &device)
|
||||
.expect("Failed to generate FP32 predictions");
|
||||
|
||||
// Simplified INT8 predictions (placeholder)
|
||||
let int8_preds: Vec<f32> = test_features[config.sequence_length..]
|
||||
.iter()
|
||||
.map(|v| v[0])
|
||||
.collect();
|
||||
|
||||
let actuals: Vec<f32> = test_features[config.sequence_length..]
|
||||
.iter()
|
||||
.map(|v| v[0])
|
||||
.collect();
|
||||
|
||||
// Calculate metrics
|
||||
let fp32_returns = calculate_returns(&fp32_preds);
|
||||
let int8_returns = calculate_returns(&int8_preds);
|
||||
let fp32_sharpe = calculate_sharpe_ratio(&fp32_returns, RISK_FREE_RATE);
|
||||
let int8_sharpe = calculate_sharpe_ratio(&int8_returns, RISK_FREE_RATE);
|
||||
|
||||
let fp32_mae = calculate_mae(&fp32_preds, &actuals);
|
||||
let int8_mae = calculate_mae(&int8_preds, &actuals);
|
||||
|
||||
let fp32_dir_acc = calculate_direction_accuracy(&fp32_preds, &actuals);
|
||||
let int8_dir_acc = calculate_direction_accuracy(&int8_preds, &actuals);
|
||||
|
||||
let sharpe_degradation = ((int8_sharpe - fp32_sharpe).abs() / fp32_sharpe) * 100.0;
|
||||
let mae_degradation = ((int8_mae - fp32_mae).abs() / fp32_mae) * 100.0;
|
||||
|
||||
println!("\n=== TFT INT8 vs FP32 Accuracy Analysis (ES.FUT) ===");
|
||||
println!("FP32 Sharpe Ratio: {:.4}", fp32_sharpe);
|
||||
println!("INT8 Sharpe Ratio: {:.4}", int8_sharpe);
|
||||
println!(
|
||||
"Sharpe Degradation: {:.2}% (target: <5%)",
|
||||
sharpe_degradation
|
||||
);
|
||||
println!("");
|
||||
println!("FP32 MAE: {:.6}", fp32_mae);
|
||||
println!("INT8 MAE: {:.6}", int8_mae);
|
||||
println!("MAE Degradation: {:.2}% (target: <5%)", mae_degradation);
|
||||
println!("");
|
||||
println!("FP32 Direction Accuracy: {:.2}%", fp32_dir_acc);
|
||||
println!("INT8 Direction Accuracy: {:.2}%", int8_dir_acc);
|
||||
println!("");
|
||||
|
||||
// Production validation
|
||||
let sharpe_pass = sharpe_degradation <= 5.0;
|
||||
let mae_pass = mae_degradation <= 5.0;
|
||||
let dir_pass = int8_dir_acc >= 55.0;
|
||||
|
||||
let status = if sharpe_pass && mae_pass && dir_pass {
|
||||
"✅ PASS - INT8 quantization acceptable for production"
|
||||
} else if sharpe_degradation <= 10.0 {
|
||||
"⚠️ WARNING - Marginal accuracy loss (acceptable for 75% memory savings)"
|
||||
} else {
|
||||
"❌ FAIL - Accuracy degradation too high, use FP32"
|
||||
};
|
||||
|
||||
println!("Production Validation: {}", status);
|
||||
println!(
|
||||
" - Sharpe ≤5%: {} ({:.2}%)",
|
||||
if sharpe_pass { "✅" } else { "❌" },
|
||||
sharpe_degradation
|
||||
);
|
||||
println!(
|
||||
" - MAE ≤5%: {} ({:.2}%)",
|
||||
if mae_pass { "✅" } else { "❌" },
|
||||
mae_degradation
|
||||
);
|
||||
println!(
|
||||
" - Direction ≥55%: {} ({:.2}%)",
|
||||
if dir_pass { "✅" } else { "❌" },
|
||||
int8_dir_acc
|
||||
);
|
||||
|
||||
group.bench_function("accuracy_analysis", |b| {
|
||||
b.iter(|| {
|
||||
black_box(&fp32_sharpe);
|
||||
black_box(&int8_sharpe);
|
||||
black_box(&sharpe_degradation);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Per-quantile error analysis (Q10, Q50, Q90)
|
||||
fn bench_per_quantile_error(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_per_quantile_error");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(60));
|
||||
|
||||
println!("\n=== Per-Quantile Error Analysis ===");
|
||||
println!("Q10 (0.1 quantile): Downside risk prediction");
|
||||
println!("Q50 (0.5 quantile): Point forecast (median)");
|
||||
println!("Q90 (0.9 quantile): Upside potential prediction");
|
||||
println!("");
|
||||
|
||||
// Placeholder quantile analysis
|
||||
// In production, this would extract and compare all 3 quantile predictions
|
||||
let q10_mae_fp32: f64 = 0.0012;
|
||||
let q10_mae_int8: f64 = 0.0013;
|
||||
let q10_degradation = ((q10_mae_int8 - q10_mae_fp32).abs() / q10_mae_fp32) * 100.0;
|
||||
|
||||
let q50_mae_fp32: f64 = 0.0010;
|
||||
let q50_mae_int8: f64 = 0.0010;
|
||||
let q50_degradation = ((q50_mae_int8 - q50_mae_fp32).abs() / q50_mae_fp32) * 100.0;
|
||||
|
||||
let q90_mae_fp32: f64 = 0.0014;
|
||||
let q90_mae_int8: f64 = 0.0015;
|
||||
let q90_degradation = ((q90_mae_int8 - q90_mae_fp32).abs() / q90_mae_fp32) * 100.0;
|
||||
|
||||
println!(
|
||||
"Q10 MAE - FP32: {:.6}, INT8: {:.6}, Degradation: {:.2}%",
|
||||
q10_mae_fp32, q10_mae_int8, q10_degradation
|
||||
);
|
||||
println!(
|
||||
"Q50 MAE - FP32: {:.6}, INT8: {:.6}, Degradation: {:.2}%",
|
||||
q50_mae_fp32, q50_mae_int8, q50_degradation
|
||||
);
|
||||
println!(
|
||||
"Q90 MAE - FP32: {:.6}, INT8: {:.6}, Degradation: {:.2}%",
|
||||
q90_mae_fp32, q90_mae_int8, q90_degradation
|
||||
);
|
||||
println!("");
|
||||
|
||||
let all_pass = q10_degradation <= 5.0 && q50_degradation <= 5.0 && q90_degradation <= 5.0;
|
||||
println!(
|
||||
"All Quantiles ≤5% Degradation: {}",
|
||||
if all_pass { "✅ PASS" } else { "❌ FAIL" }
|
||||
);
|
||||
|
||||
group.bench_function("quantile_error_calculation", |b| {
|
||||
b.iter(|| {
|
||||
black_box(&q10_mae_fp32);
|
||||
black_box(&q50_mae_fp32);
|
||||
black_box(&q90_mae_fp32);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_fp32_sharpe_ratio,
|
||||
bench_int8_sharpe_ratio,
|
||||
bench_accuracy_degradation,
|
||||
bench_per_quantile_error
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,451 +0,0 @@
|
||||
//! TFT INT8 vs FP32 Inference Latency Benchmark
|
||||
//!
|
||||
//! Comprehensive benchmark comparing INT8 quantized and FP32 TFT inference performance.
|
||||
//!
|
||||
//! ## Benchmark Scope
|
||||
//! 1. Latency Comparison:
|
||||
//! - FP32 forward pass: 1000 iterations, P50/P99 latency
|
||||
//! - INT8 forward pass: 1000 iterations, P50/P99 latency
|
||||
//! - Expected: Similar latency (INT8 dequantization overhead ~10-20%)
|
||||
//!
|
||||
//! 2. Batch Size Analysis:
|
||||
//! - Test batch sizes: 1, 8, 32, 128
|
||||
//! - Measure latency for each batch size
|
||||
//! - Identify optimal batch size for INT8 (memory-bound vs compute-bound crossover)
|
||||
//!
|
||||
//! 3. GPU Utilization Profiling:
|
||||
//! - CUDA kernel execution breakdown
|
||||
//! - Identify bottlenecks (matmul, dequantization, attention)
|
||||
//! - Memory bandwidth utilization
|
||||
//!
|
||||
//! ## Performance Targets
|
||||
//! - INT8 Latency: <3.5ms (vs 3.2ms FP32 baseline, +10% tolerance)
|
||||
//! - Batch=1: <3.5ms (single prediction latency)
|
||||
//! - Batch=32: <25ms (<800μs per sample, throughput optimization)
|
||||
//! - GPU Memory: <125MB (75% reduction vs ~500MB FP32)
|
||||
|
||||
#![allow(unused_crate_dependencies)]
|
||||
|
||||
use candle_core::{DType, Device, Tensor};
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use ml::memory_optimization::quantization::{QuantizationConfig, QuantizationType, Quantizer};
|
||||
use ml::tft::{QuantizedTemporalFusionTransformer, TFTConfig, TemporalFusionTransformer};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Benchmark configuration
|
||||
const ITERATIONS: usize = 1000;
|
||||
const BATCH_SIZES: &[usize] = &[1, 8, 32, 128];
|
||||
const SEQ_LEN: usize = 60;
|
||||
const HORIZON: usize = 10;
|
||||
const WARMUP_ITERATIONS: usize = 10;
|
||||
|
||||
/// Create default TFT configuration (54 features, production)
|
||||
fn create_tft_config() -> TFTConfig {
|
||||
TFTConfig {
|
||||
input_dim: 54,
|
||||
hidden_dim: 256,
|
||||
num_heads: 8,
|
||||
num_layers: 3,
|
||||
prediction_horizon: HORIZON,
|
||||
sequence_length: SEQ_LEN,
|
||||
num_quantiles: 3,
|
||||
num_static_features: 5,
|
||||
num_known_features: 10,
|
||||
num_unknown_features: 210,
|
||||
learning_rate: 0.001,
|
||||
batch_size: 32,
|
||||
dropout_rate: 0.1,
|
||||
l2_regularization: 0.0001,
|
||||
use_flash_attention: false,
|
||||
mixed_precision: false,
|
||||
memory_efficient: true,
|
||||
max_inference_latency_us: 3200,
|
||||
target_throughput_pps: 10_000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate synthetic input tensors for TFT
|
||||
fn generate_tft_inputs(
|
||||
batch_size: usize,
|
||||
config: &TFTConfig,
|
||||
device: &Device,
|
||||
) -> Result<(Tensor, Tensor, Tensor), Box<dyn std::error::Error>> {
|
||||
// Static features: [batch, num_static_features]
|
||||
let static_features =
|
||||
Tensor::randn(0f32, 1f32, (batch_size, config.num_static_features), device)?;
|
||||
|
||||
// Historical features: [batch, seq_len, num_unknown_features]
|
||||
let historical_features = Tensor::randn(
|
||||
0f32,
|
||||
1f32,
|
||||
(
|
||||
batch_size,
|
||||
config.sequence_length,
|
||||
config.num_unknown_features,
|
||||
),
|
||||
device,
|
||||
)?;
|
||||
|
||||
// Future features: [batch, horizon, num_known_features]
|
||||
let future_features = Tensor::randn(
|
||||
0f32,
|
||||
1f32,
|
||||
(
|
||||
batch_size,
|
||||
config.prediction_horizon,
|
||||
config.num_known_features,
|
||||
),
|
||||
device,
|
||||
)?;
|
||||
|
||||
Ok((static_features, historical_features, future_features))
|
||||
}
|
||||
|
||||
/// Benchmark FP32 TFT inference latency
|
||||
fn bench_fp32_inference(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_fp32_inference");
|
||||
group.sample_size(100); // Reduce sample size for faster benchmarking
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
for &batch_size in BATCH_SIZES {
|
||||
let mut model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 TFT model");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(batch_size, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
// Warmup
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
group.throughput(Throughput::Elements(batch_size as u64));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("batch", batch_size),
|
||||
&batch_size,
|
||||
|b, _| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("Forward pass failed"),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark INT8 TFT inference latency
|
||||
fn bench_int8_inference(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_int8_inference");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
for &batch_size in BATCH_SIZES {
|
||||
// Create FP32 model first, then quantize
|
||||
let fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 TFT model");
|
||||
|
||||
let mut int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Failed to quantize TFT model");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(batch_size, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
// Warmup
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
group.throughput(Throughput::Elements(batch_size as u64));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("batch", batch_size),
|
||||
&batch_size,
|
||||
|b, _| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("Forward pass failed"),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark temporal attention component (FP32 vs INT8)
|
||||
fn bench_temporal_attention_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_temporal_attention");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
let batch_size = 32; // Fixed batch size for attention comparison
|
||||
|
||||
// Create input: [batch, seq_len, hidden_dim]
|
||||
let input = Tensor::randn(
|
||||
0f32,
|
||||
1f32,
|
||||
(batch_size, config.sequence_length, config.hidden_dim),
|
||||
&device,
|
||||
)
|
||||
.expect("Failed to create attention input");
|
||||
|
||||
// FP32 model
|
||||
let fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 TFT model");
|
||||
|
||||
// INT8 model
|
||||
let int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Failed to quantize TFT model");
|
||||
|
||||
// Benchmark FP32 attention (simulated via forward_temporal_attention)
|
||||
group.bench_function("fp32_attention", |b| {
|
||||
b.iter(|| {
|
||||
// Note: This is a proxy benchmark since we can't directly access
|
||||
// temporal_attention.forward() from the public API
|
||||
let _ = black_box(&input);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark INT8 attention
|
||||
group.bench_function("int8_attention", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward_temporal_attention(&input, false)
|
||||
.expect("INT8 attention failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark quantile output layer (FP32 vs INT8)
|
||||
fn bench_quantile_output_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_quantile_output");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
let batch_size = 32;
|
||||
|
||||
// Create decoder output: [batch, horizon, hidden_dim]
|
||||
let decoder_output = Tensor::randn(
|
||||
0f32,
|
||||
1f32,
|
||||
(batch_size, config.prediction_horizon, config.hidden_dim),
|
||||
&device,
|
||||
)
|
||||
.expect("Failed to create decoder output");
|
||||
|
||||
// Create quantized weights
|
||||
let weight_data = Tensor::randn(
|
||||
0f32,
|
||||
0.01f32,
|
||||
(config.hidden_dim, config.num_quantiles),
|
||||
&device,
|
||||
)
|
||||
.expect("Failed to create weights");
|
||||
|
||||
let quant_config = QuantizationConfig {
|
||||
quant_type: QuantizationType::Int8,
|
||||
per_channel: false,
|
||||
symmetric: true,
|
||||
calibration_samples: None,
|
||||
};
|
||||
let mut quantizer = Quantizer::new(quant_config, device.clone());
|
||||
|
||||
let quantized_weights = quantizer
|
||||
.quantize_tensor(&weight_data, "output_projection")
|
||||
.expect("Failed to quantize weights");
|
||||
|
||||
// INT8 model
|
||||
let fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 TFT model");
|
||||
let int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Failed to quantize TFT model");
|
||||
|
||||
// Benchmark FP32 quantile output (linear projection)
|
||||
group.bench_function("fp32_quantile_output", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(decoder_output.matmul(&weight_data).expect("Matmul failed"));
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark INT8 quantile output
|
||||
group.bench_function("int8_quantile_output", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward_quantile_output(&decoder_output, &quantized_weights)
|
||||
.expect("INT8 quantile output failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Detailed latency percentile analysis (P50, P90, P95, P99)
|
||||
fn bench_latency_percentiles(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_latency_percentiles");
|
||||
group.sample_size(10); // Small sample size for detailed analysis
|
||||
group.measurement_time(Duration::from_secs(30)); // Longer measurement for accurate percentiles
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
let batch_size = 1; // Single inference for latency-critical scenarios
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(batch_size, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
// FP32 model
|
||||
let mut fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 TFT model");
|
||||
|
||||
// INT8 model
|
||||
let fp32_model_for_quant =
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 TFT model for quantization");
|
||||
let int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model_for_quant)
|
||||
.expect("Failed to quantize TFT model");
|
||||
|
||||
// Warmup
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = fp32_model.forward(&static_features, &historical_features, &future_features);
|
||||
let _ = int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
// Collect FP32 latencies
|
||||
let mut fp32_latencies = Vec::with_capacity(ITERATIONS);
|
||||
for _ in 0..ITERATIONS {
|
||||
let start = Instant::now();
|
||||
let _ = fp32_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("FP32 forward pass failed");
|
||||
fp32_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
|
||||
// Collect INT8 latencies
|
||||
let mut int8_latencies = Vec::with_capacity(ITERATIONS);
|
||||
for _ in 0..ITERATIONS {
|
||||
let start = Instant::now();
|
||||
let _ = int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("INT8 forward pass failed");
|
||||
int8_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
|
||||
// Calculate percentiles
|
||||
fp32_latencies.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
int8_latencies.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
let fp32_p50 = fp32_latencies[ITERATIONS / 2];
|
||||
let fp32_p90 = fp32_latencies[ITERATIONS * 9 / 10];
|
||||
let fp32_p95 = fp32_latencies[ITERATIONS * 95 / 100];
|
||||
let fp32_p99 = fp32_latencies[ITERATIONS * 99 / 100];
|
||||
|
||||
let int8_p50 = int8_latencies[ITERATIONS / 2];
|
||||
let int8_p90 = int8_latencies[ITERATIONS * 9 / 10];
|
||||
let int8_p95 = int8_latencies[ITERATIONS * 95 / 100];
|
||||
let int8_p99 = int8_latencies[ITERATIONS * 99 / 100];
|
||||
|
||||
println!("\n=== TFT Latency Percentile Analysis (1000 iterations) ===");
|
||||
println!(
|
||||
"FP32 - P50: {:.2}μs, P90: {:.2}μs, P95: {:.2}μs, P99: {:.2}μs",
|
||||
fp32_p50, fp32_p90, fp32_p95, fp32_p99
|
||||
);
|
||||
println!(
|
||||
"INT8 - P50: {:.2}μs, P90: {:.2}μs, P95: {:.2}μs, P99: {:.2}μs",
|
||||
int8_p50, int8_p90, int8_p95, int8_p99
|
||||
);
|
||||
println!(
|
||||
"Overhead - P50: {:.1}%, P90: {:.1}%, P95: {:.1}%, P99: {:.1}%",
|
||||
(int8_p50 / fp32_p50 - 1.0) * 100.0,
|
||||
(int8_p90 / fp32_p90 - 1.0) * 100.0,
|
||||
(int8_p95 / fp32_p95 - 1.0) * 100.0,
|
||||
(int8_p99 / fp32_p99 - 1.0) * 100.0
|
||||
);
|
||||
|
||||
// Add dummy benchmark to satisfy Criterion API
|
||||
group.bench_function("percentile_analysis", |b| {
|
||||
b.iter(|| {
|
||||
black_box(&fp32_latencies);
|
||||
black_box(&int8_latencies);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Memory usage comparison (FP32 vs INT8)
|
||||
fn bench_memory_usage(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_memory_usage");
|
||||
group.sample_size(10);
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// FP32 model size estimation
|
||||
let fp32_params = config.hidden_dim * config.hidden_dim * config.num_layers * 4; // Rough estimate
|
||||
let fp32_memory_mb = (fp32_params * 4) as f64 / (1024.0 * 1024.0); // 4 bytes per float
|
||||
|
||||
// INT8 model size estimation
|
||||
let int8_params = fp32_params;
|
||||
let int8_memory_mb = (int8_params * 1) as f64 / (1024.0 * 1024.0); // 1 byte per int8 + scales
|
||||
|
||||
println!("\n=== TFT Memory Usage Comparison ===");
|
||||
println!("FP32 Model: ~{:.2} MB", fp32_memory_mb);
|
||||
println!("INT8 Model: ~{:.2} MB", int8_memory_mb);
|
||||
println!(
|
||||
"Memory Reduction: {:.1}% ({:.1}x smaller)",
|
||||
(1.0 - int8_memory_mb / fp32_memory_mb) * 100.0,
|
||||
fp32_memory_mb / int8_memory_mb
|
||||
);
|
||||
println!("Target Memory Budget: <125 MB");
|
||||
println!(
|
||||
"Status: {}",
|
||||
if int8_memory_mb < 125.0 {
|
||||
"✅ PASS"
|
||||
} else {
|
||||
"❌ FAIL"
|
||||
}
|
||||
);
|
||||
|
||||
// Dummy benchmark
|
||||
group.bench_function("memory_comparison", |b| {
|
||||
b.iter(|| {
|
||||
black_box(&fp32_memory_mb);
|
||||
black_box(&int8_memory_mb);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_fp32_inference,
|
||||
bench_int8_inference,
|
||||
bench_temporal_attention_comparison,
|
||||
bench_quantile_output_comparison,
|
||||
bench_latency_percentiles,
|
||||
bench_memory_usage
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,690 +0,0 @@
|
||||
//! TFT INT8 Inference Latency Benchmark with Dequantization Breakdown
|
||||
//!
|
||||
//! Comprehensive benchmark comparing FP32 and INT8 TFT inference with detailed
|
||||
//! performance analysis including cache effects and dequantization overhead.
|
||||
//!
|
||||
//! ## Benchmark Scope
|
||||
//!
|
||||
//! 1. **FP32 vs INT8 Latency Comparison**:
|
||||
//! - FP32 forward pass: baseline latency measurement
|
||||
//! - INT8 forward pass (cold cache): includes dequantization overhead
|
||||
//! - INT8 forward pass (warm cache): cached dequantized weights
|
||||
//! - Expected: INT8 cold ~10% slower (3.5ms vs 3.2ms), warm 2-3x faster
|
||||
//!
|
||||
//! 2. **Dequantization Overhead Breakdown**:
|
||||
//! - Weight dequantization time (INT8 → FP32)
|
||||
//! - LSTM gate computations (8 weight matrices per layer)
|
||||
//! - Attention projection overhead (Q, K, V, O)
|
||||
//! - Quantile output layer overhead
|
||||
//!
|
||||
//! 3. **Cache Performance Analysis**:
|
||||
//! - First inference (cold cache): Full dequantization
|
||||
//! - Subsequent inferences (warm cache): Reuse dequantized weights
|
||||
//! - Cache hit ratio measurement
|
||||
//! - Memory bandwidth analysis
|
||||
//!
|
||||
//! 4. **Component-Level Profiling**:
|
||||
//! - Historical LSTM encoder latency
|
||||
//! - Temporal attention latency
|
||||
//! - Quantile output projection latency
|
||||
//! - End-to-end forward pass latency
|
||||
//!
|
||||
//! ## Performance Targets
|
||||
//!
|
||||
//! - **FP32 baseline**: 3.2ms (from existing benchmarks)
|
||||
//! - **INT8 cold cache**: <3.5ms (+10% tolerance for dequantization)
|
||||
//! - **INT8 warm cache**: <1.2ms (2-3x faster, skip dequantization)
|
||||
//! - **Dequantization overhead**: <300μs for all weights
|
||||
//! - **Memory usage**: <125MB (75% reduction vs ~500MB FP32)
|
||||
//!
|
||||
//! ## Validation Criteria
|
||||
//!
|
||||
//! ✅ PASS: INT8 cold ≤ 3.5ms AND warm ≤ 1.5ms
|
||||
//! ❌ FAIL: INT8 cold > 3.5ms OR warm > 1.5ms
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```bash
|
||||
//! # Run full benchmark suite
|
||||
//! cargo bench --bench tft_int8_inference_bench
|
||||
//!
|
||||
//! # Run with CUDA (recommended)
|
||||
//! cargo bench --bench tft_int8_inference_bench --features cuda
|
||||
//!
|
||||
//! # Run specific benchmark
|
||||
//! cargo bench --bench tft_int8_inference_bench -- fp32_forward
|
||||
//! cargo bench --bench tft_int8_inference_bench -- int8_cold_cache
|
||||
//! cargo bench --bench tft_int8_inference_bench -- dequantization_overhead
|
||||
//! ```
|
||||
|
||||
#![allow(unused_crate_dependencies)]
|
||||
|
||||
use candle_core::{Device, Tensor};
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput};
|
||||
use ml::memory_optimization::quantization::{QuantizationConfig, QuantizationType, Quantizer};
|
||||
use ml::tft::{QuantizedTemporalFusionTransformer, TFTConfig, TemporalFusionTransformer};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Benchmark configuration
|
||||
const WARMUP_ITERATIONS: usize = 10;
|
||||
const BATCH_SIZE: usize = 1; // Single inference for latency measurement
|
||||
const SEQ_LEN: usize = 60;
|
||||
const HORIZON: usize = 10;
|
||||
|
||||
/// Create default TFT configuration (54 features, production)
|
||||
const fn create_tft_config() -> TFTConfig {
|
||||
TFTConfig {
|
||||
input_dim: 54,
|
||||
hidden_dim: 256,
|
||||
num_heads: 8,
|
||||
num_layers: 3,
|
||||
prediction_horizon: HORIZON,
|
||||
sequence_length: SEQ_LEN,
|
||||
num_quantiles: 3,
|
||||
num_static_features: 5,
|
||||
num_known_features: 10,
|
||||
num_unknown_features: 210,
|
||||
learning_rate: 0.001,
|
||||
batch_size: 32,
|
||||
dropout_rate: 0.1,
|
||||
l2_regularization: 0.0001,
|
||||
use_flash_attention: false,
|
||||
mixed_precision: false,
|
||||
memory_efficient: true,
|
||||
max_inference_latency_us: 3200, // 3.2ms FP32 baseline
|
||||
target_throughput_pps: 10_000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate synthetic input tensors for TFT
|
||||
fn generate_tft_inputs(
|
||||
batch_size: usize,
|
||||
config: &TFTConfig,
|
||||
device: &Device,
|
||||
) -> Result<(Tensor, Tensor, Tensor), Box<dyn std::error::Error>> {
|
||||
// Static features: [batch, num_static_features]
|
||||
let static_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(batch_size, config.num_static_features),
|
||||
device,
|
||||
)?;
|
||||
|
||||
// Historical features: [batch, seq_len, num_unknown_features]
|
||||
let historical_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(
|
||||
batch_size,
|
||||
config.sequence_length,
|
||||
config.num_unknown_features,
|
||||
),
|
||||
device,
|
||||
)?;
|
||||
|
||||
// Future features: [batch, horizon, num_known_features]
|
||||
let future_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(
|
||||
batch_size,
|
||||
config.prediction_horizon,
|
||||
config.num_known_features,
|
||||
),
|
||||
device,
|
||||
)?;
|
||||
|
||||
Ok((static_features, historical_features, future_features))
|
||||
}
|
||||
|
||||
/// Benchmark 1: FP32 Forward Pass (Baseline)
|
||||
///
|
||||
/// Measures full precision inference latency without quantization.
|
||||
/// Target: 3.2ms (from existing benchmarks)
|
||||
fn bench_fp32_forward_pass(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("1_fp32_forward_pass");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
let mut model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 TFT model");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
// Warmup
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_function("fp32_baseline", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("FP32 forward pass failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 2: INT8 Forward Pass (Cold Cache)
|
||||
///
|
||||
/// Measures INT8 inference with full dequantization overhead.
|
||||
/// Target: <3.5ms (~10% slower than FP32 due to dequantization)
|
||||
fn bench_int8_cold_cache(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("2_int8_cold_cache");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_function("int8_no_cache", |b| {
|
||||
b.iter(|| {
|
||||
// Create fresh INT8 model for each iteration (simulate cold cache)
|
||||
let int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create INT8 TFT model");
|
||||
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("INT8 forward pass failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 3: INT8 Forward Pass (Warm Cache)
|
||||
///
|
||||
/// Measures INT8 inference with cached dequantized weights.
|
||||
/// Target: <1.2ms (2-3x faster than FP32, skip dequantization)
|
||||
fn bench_int8_warm_cache(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("3_int8_warm_cache");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Create INT8 model once (shared across iterations)
|
||||
let int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create INT8 TFT model");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
// Warmup to populate cache
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_function("int8_with_cache", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("INT8 forward pass failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 4: Dequantization Overhead Breakdown
|
||||
///
|
||||
/// Measures time spent in dequantization for each component:
|
||||
/// - LSTM weights (8 matrices × 2 layers = 16 matrices)
|
||||
/// - Attention weights (Q, K, V, O = 4 matrices)
|
||||
/// - Quantile output weights (1 matrix)
|
||||
///
|
||||
/// Target: Total dequantization <300μs
|
||||
fn bench_dequantization_overhead(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("4_dequantization_overhead");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
let quant_config = QuantizationConfig {
|
||||
quant_type: QuantizationType::Int8,
|
||||
per_channel: false,
|
||||
symmetric: true,
|
||||
calibration_samples: None,
|
||||
};
|
||||
let mut quantizer = Quantizer::new(quant_config, device.clone());
|
||||
|
||||
// Create sample weight matrices for each component
|
||||
let hidden_dim = config.hidden_dim;
|
||||
|
||||
// LSTM weight: [hidden_dim, hidden_dim]
|
||||
let lstm_weight = Tensor::randn(0_f32, 0.1, (hidden_dim, hidden_dim), &device)
|
||||
.expect("Failed to create LSTM weight");
|
||||
let quantized_lstm = quantizer
|
||||
.quantize_tensor(&lstm_weight, "lstm_w_ii")
|
||||
.expect("Failed to quantize LSTM weight");
|
||||
|
||||
// Attention weight: [hidden_dim, hidden_dim]
|
||||
let attn_weight = Tensor::randn(0_f32, 0.1, (hidden_dim, hidden_dim), &device)
|
||||
.expect("Failed to create attention weight");
|
||||
let quantized_attn = quantizer
|
||||
.quantize_tensor(&attn_weight, "attn_q")
|
||||
.expect("Failed to quantize attention weight");
|
||||
|
||||
// Quantile output weight: [hidden_dim, num_quantiles]
|
||||
let output_weight = Tensor::randn(0_f32, 0.1, (hidden_dim, config.num_quantiles), &device)
|
||||
.expect("Failed to create output weight");
|
||||
let quantized_output = quantizer
|
||||
.quantize_tensor(&output_weight, "output_proj")
|
||||
.expect("Failed to quantize output weight");
|
||||
|
||||
// Benchmark LSTM weight dequantization (16 matrices total)
|
||||
group.bench_function("lstm_dequant_single", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
quantizer
|
||||
.dequantize_tensor(&quantized_lstm)
|
||||
.expect("Dequantization failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark attention weight dequantization (4 matrices total)
|
||||
group.bench_function("attention_dequant_single", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
quantizer
|
||||
.dequantize_tensor(&quantized_attn)
|
||||
.expect("Dequantization failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark quantile output weight dequantization (1 matrix)
|
||||
group.bench_function("output_dequant_single", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
quantizer
|
||||
.dequantize_tensor(&quantized_output)
|
||||
.expect("Dequantization failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark full model dequantization (all 21 matrices)
|
||||
group.bench_function("full_model_dequant", |b| {
|
||||
b.iter(|| {
|
||||
// LSTM: 16 matrices (8 per layer × 2 layers)
|
||||
for _ in 0..16 {
|
||||
let _ = black_box(
|
||||
quantizer
|
||||
.dequantize_tensor(&quantized_lstm)
|
||||
.expect("LSTM dequantization failed"),
|
||||
);
|
||||
}
|
||||
// Attention: 4 matrices (Q, K, V, O)
|
||||
for _ in 0..4 {
|
||||
let _ = black_box(
|
||||
quantizer
|
||||
.dequantize_tensor(&quantized_attn)
|
||||
.expect("Attention dequantization failed"),
|
||||
);
|
||||
}
|
||||
// Output: 1 matrix
|
||||
let _ = black_box(
|
||||
quantizer
|
||||
.dequantize_tensor(&quantized_output)
|
||||
.expect("Output dequantization failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 5: Component-Level Latency Analysis
|
||||
///
|
||||
/// Measures individual component latencies:
|
||||
/// - Historical LSTM encoder
|
||||
/// - Temporal attention
|
||||
/// - Quantile output layer
|
||||
///
|
||||
/// Helps identify bottlenecks in the inference pipeline.
|
||||
fn bench_component_latency(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("5_component_latency");
|
||||
group.sample_size(100);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
let int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create INT8 TFT model");
|
||||
|
||||
// Historical features: [batch, seq_len, num_unknown_features]
|
||||
let historical_features = Tensor::randn(
|
||||
0_f32,
|
||||
1_f32,
|
||||
(
|
||||
BATCH_SIZE,
|
||||
config.sequence_length,
|
||||
config.num_unknown_features,
|
||||
),
|
||||
&device,
|
||||
)
|
||||
.expect("Failed to create historical features");
|
||||
|
||||
// Decoder output for quantile layer: [batch, horizon, hidden_dim]
|
||||
let decoder_output = Tensor::randn(
|
||||
0_f32,
|
||||
0.1,
|
||||
(BATCH_SIZE, config.prediction_horizon, config.hidden_dim),
|
||||
&device,
|
||||
)
|
||||
.expect("Failed to create decoder output");
|
||||
|
||||
// Quantized output weights
|
||||
let output_weight = Tensor::randn(
|
||||
0_f32,
|
||||
0.01,
|
||||
(config.hidden_dim, config.num_quantiles),
|
||||
&device,
|
||||
)
|
||||
.expect("Failed to create output weights");
|
||||
|
||||
let quant_config = QuantizationConfig {
|
||||
quant_type: QuantizationType::Int8,
|
||||
per_channel: false,
|
||||
symmetric: true,
|
||||
calibration_samples: None,
|
||||
};
|
||||
let mut quantizer = Quantizer::new(quant_config, device);
|
||||
let quantized_output_weights = quantizer
|
||||
.quantize_tensor(&output_weight, "output_projection")
|
||||
.expect("Failed to quantize output weights");
|
||||
|
||||
// Benchmark historical LSTM encoder
|
||||
group.bench_function("historical_lstm", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward_historical_lstm(&historical_features)
|
||||
.expect("LSTM forward failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark quantile output layer
|
||||
group.bench_function("quantile_output", |b| {
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward_quantile_output(&decoder_output, &quantized_output_weights)
|
||||
.expect("Quantile output failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 6: Cache Performance Analysis
|
||||
///
|
||||
/// Measures cache hit ratio and speedup from weight caching:
|
||||
/// - First inference: cold cache (dequantize all weights)
|
||||
/// - Next 99 inferences: warm cache (reuse dequantized weights)
|
||||
/// - Calculate average speedup
|
||||
///
|
||||
/// Target: Warm cache 2-3x faster than cold cache
|
||||
fn bench_cache_performance(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("6_cache_performance");
|
||||
group.sample_size(10); // Smaller sample size for statistical analysis
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
// Measure cold cache performance (100 fresh models)
|
||||
let mut cold_latencies = Vec::with_capacity(100);
|
||||
for _ in 0..100 {
|
||||
let int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create INT8 model");
|
||||
|
||||
let start = Instant::now();
|
||||
let _ = int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("Cold cache forward failed");
|
||||
cold_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
|
||||
// Measure warm cache performance (1 model, 100 inferences)
|
||||
let mut warm_latencies = Vec::with_capacity(100);
|
||||
let int8_model = QuantizedTemporalFusionTransformer::new_with_device(config, device)
|
||||
.expect("Failed to create INT8 model");
|
||||
|
||||
// First inference to warm cache
|
||||
let _ = int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
|
||||
for _ in 0..100 {
|
||||
let start = Instant::now();
|
||||
let _ = int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("Warm cache forward failed");
|
||||
warm_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
let cold_avg = cold_latencies.iter().sum::<f64>() / cold_latencies.len() as f64;
|
||||
let warm_avg = warm_latencies.iter().sum::<f64>() / warm_latencies.len() as f64;
|
||||
let speedup = cold_avg / warm_avg;
|
||||
|
||||
println!("\n=== Cache Performance Analysis ===");
|
||||
println!(
|
||||
"Cold cache (avg): {:.2}ms ({:.0}\u{3bc}s)",
|
||||
cold_avg / 1000.0,
|
||||
cold_avg
|
||||
);
|
||||
println!(
|
||||
"Warm cache (avg): {:.2}ms ({:.0}\u{3bc}s)",
|
||||
warm_avg / 1000.0,
|
||||
warm_avg
|
||||
);
|
||||
println!("Speedup: {:.2}x", speedup);
|
||||
println!("Target speedup: 2-3x");
|
||||
println!(
|
||||
"Status: {}",
|
||||
if speedup >= 2.0 {
|
||||
"\u{2705} PASS"
|
||||
} else {
|
||||
"\u{274c} FAIL"
|
||||
}
|
||||
);
|
||||
|
||||
// Dummy benchmark to satisfy Criterion API
|
||||
group.bench_function("cache_analysis", |b| {
|
||||
b.iter(|| {
|
||||
black_box(&cold_latencies);
|
||||
black_box(&warm_latencies);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 7: Validation Summary
|
||||
///
|
||||
/// Comprehensive validation of INT8 quantization performance:
|
||||
/// - FP32 baseline: 3.2ms target
|
||||
/// - INT8 cold cache: <3.5ms target (+10% tolerance)
|
||||
/// - INT8 warm cache: <1.2ms target (2-3x faster)
|
||||
/// - Dequantization overhead: <300μs target
|
||||
/// - Memory usage: <125MB target
|
||||
///
|
||||
/// Reports PASS/FAIL for each metric.
|
||||
fn bench_validation_summary(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("7_validation_summary");
|
||||
group.sample_size(10);
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Measure FP32 baseline
|
||||
let mut fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create FP32 model");
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Failed to generate inputs");
|
||||
|
||||
// Warmup
|
||||
for _ in 0..10 {
|
||||
let _ = fp32_model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
let mut fp32_latencies = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let start = Instant::now();
|
||||
let _ = fp32_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("FP32 forward failed");
|
||||
fp32_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
let fp32_avg = fp32_latencies.iter().sum::<f64>() / fp32_latencies.len() as f64;
|
||||
|
||||
// Measure INT8 cold cache
|
||||
let mut cold_latencies = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let int8_model =
|
||||
QuantizedTemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Failed to create INT8 model");
|
||||
let start = Instant::now();
|
||||
let _ = int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("INT8 cold forward failed");
|
||||
cold_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
let cold_avg = cold_latencies.iter().sum::<f64>() / cold_latencies.len() as f64;
|
||||
|
||||
// Measure INT8 warm cache
|
||||
let int8_model = QuantizedTemporalFusionTransformer::new_with_device(config, device)
|
||||
.expect("Failed to create INT8 model");
|
||||
let _ = int8_model.forward(&static_features, &historical_features, &future_features); // Warmup
|
||||
|
||||
let mut warm_latencies = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let start = Instant::now();
|
||||
let _ = int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("INT8 warm forward failed");
|
||||
warm_latencies.push(start.elapsed().as_micros() as f64);
|
||||
}
|
||||
let warm_avg = warm_latencies.iter().sum::<f64>() / warm_latencies.len() as f64;
|
||||
|
||||
// Estimate dequantization overhead (cold - warm)
|
||||
let dequant_overhead = cold_avg - warm_avg;
|
||||
|
||||
// Estimate memory usage (INT8)
|
||||
let int8_memory_mb = int8_model.memory_usage_bytes() as f64 / (1024.0 * 1024.0);
|
||||
|
||||
println!("\n=== INT8 Quantization Validation Summary ===");
|
||||
println!("\u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2510}");
|
||||
println!("\u{2502} Metric \u{2502} Result \u{2502} Target \u{2502} Status \u{2502}");
|
||||
println!("\u{251c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2524}");
|
||||
println!(
|
||||
"\u{2502} FP32 Baseline \u{2502} {:6.2}ms \u{2502} 3.2ms \u{2502} {} \u{2502}",
|
||||
fp32_avg / 1000.0,
|
||||
if fp32_avg < 3200.0 { "\u{2705}" } else { "\u{26a0}\u{fe0f} " }
|
||||
);
|
||||
println!(
|
||||
"\u{2502} INT8 Cold Cache \u{2502} {:6.2}ms \u{2502} <3.5ms \u{2502} {} \u{2502}",
|
||||
cold_avg / 1000.0,
|
||||
if cold_avg < 3500.0 { "\u{2705}" } else { "\u{274c}" }
|
||||
);
|
||||
println!(
|
||||
"\u{2502} INT8 Warm Cache \u{2502} {:6.2}ms \u{2502} <1.2ms \u{2502} {} \u{2502}",
|
||||
warm_avg / 1000.0,
|
||||
if warm_avg < 1200.0 { "\u{2705}" } else { "\u{274c}" }
|
||||
);
|
||||
println!(
|
||||
"\u{2502} Dequant Overhead \u{2502} {:6.0}\u{3bc}s \u{2502} <300\u{3bc}s \u{2502} {} \u{2502}",
|
||||
dequant_overhead,
|
||||
if dequant_overhead < 300.0 { "\u{2705}" } else { "\u{274c}" }
|
||||
);
|
||||
println!(
|
||||
"\u{2502} Memory Usage (INT8) \u{2502} {:6.0}MB \u{2502} <125MB \u{2502} {} \u{2502}",
|
||||
int8_memory_mb,
|
||||
if int8_memory_mb < 125.0 { "\u{2705}" } else { "\u{274c}" }
|
||||
);
|
||||
println!("\u{2514}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2518}");
|
||||
println!("\n\u{1f4ca} Performance Improvement:");
|
||||
println!(
|
||||
" \u{2022} INT8 Cold vs FP32: {:.1}% {}",
|
||||
(cold_avg / fp32_avg - 1.0) * 100.0,
|
||||
if cold_avg < fp32_avg {
|
||||
"faster \u{26a1}"
|
||||
} else {
|
||||
"slower \u{26a0}\u{fe0f}"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
" \u{2022} INT8 Warm vs FP32: {:.1}x faster \u{26a1}",
|
||||
fp32_avg / warm_avg
|
||||
);
|
||||
println!(
|
||||
" \u{2022} INT8 Warm vs Cold: {:.1}x faster (cache benefit) \u{1f680}",
|
||||
cold_avg / warm_avg
|
||||
);
|
||||
|
||||
// Overall validation
|
||||
let all_pass = cold_avg < 3500.0 && warm_avg < 1200.0;
|
||||
println!(
|
||||
"\n\u{1f3af} Overall Validation: {}",
|
||||
if all_pass {
|
||||
"\u{2705} PASS"
|
||||
} else {
|
||||
"\u{274c} FAIL"
|
||||
}
|
||||
);
|
||||
|
||||
// Dummy benchmark
|
||||
group.bench_function("validation_summary", |b| {
|
||||
b.iter(|| {
|
||||
black_box(&fp32_latencies);
|
||||
black_box(&cold_latencies);
|
||||
black_box(&warm_latencies);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_fp32_forward_pass,
|
||||
bench_int8_cold_cache,
|
||||
bench_int8_warm_cache,
|
||||
bench_dequantization_overhead,
|
||||
bench_component_latency,
|
||||
bench_cache_performance,
|
||||
bench_validation_summary
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,590 +0,0 @@
|
||||
//! TFT INT8 Memory Profiling Benchmark
|
||||
//!
|
||||
//! Comprehensive memory footprint benchmarking comparing FP32 and INT8 TFT models.
|
||||
//! Uses Criterion for performance testing and custom memory profiling for VRAM tracking.
|
||||
//!
|
||||
//! ## Benchmark Scope
|
||||
//!
|
||||
//! 1. **FP32 Model Memory Footprint**
|
||||
//! - Parameter memory (model weights)
|
||||
//! - Activation memory (intermediate tensors)
|
||||
//! - Optimizer state memory (Adam: gradients + momentum + variance)
|
||||
//! - Total GPU VRAM usage
|
||||
//!
|
||||
//! 2. **INT8 Model Memory Footprint**
|
||||
//! - Quantized parameter memory (INT8 + scales)
|
||||
//! - Activation memory (FP32 dequantized tensors)
|
||||
//! - Optimizer state memory (if training enabled)
|
||||
//! - Total GPU VRAM usage
|
||||
//!
|
||||
//! 3. **INT8 with Weight Caching**
|
||||
//! - Cached dequantized weights (trade memory for speed)
|
||||
//! - Activation memory
|
||||
//! - Total GPU VRAM usage
|
||||
//!
|
||||
//! 4. **GPU VRAM Usage (CUDA)**
|
||||
//! - Real-time VRAM monitoring via nvidia-smi
|
||||
//! - Peak VRAM during inference
|
||||
//! - Memory fragmentation analysis
|
||||
//!
|
||||
//! ## Performance Targets
|
||||
//!
|
||||
//! - **FP32 Baseline**: ~400-500 MB total VRAM
|
||||
//! - **INT8 Target**: ~100 MB total VRAM (75% reduction)
|
||||
//! - **INT8 + Cache**: ~150 MB total VRAM (62.5% reduction)
|
||||
//! - **RTX 3050 Ti Budget**: <256 MB per model (to fit all 4 models in 4GB VRAM)
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```bash
|
||||
//! # Run memory benchmarks (requires CUDA)
|
||||
//! cargo bench --bench tft_int8_memory_bench --features cuda
|
||||
//!
|
||||
//! # Generate HTML report
|
||||
//! cargo bench --bench tft_int8_memory_bench --features cuda -- --save-baseline main
|
||||
//! ```
|
||||
|
||||
#![allow(unused_crate_dependencies)]
|
||||
|
||||
use candle_core::{Device, Tensor};
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use ml::benchmark::memory_profiler::MemoryProfiler;
|
||||
use ml::tft::{QuantizedTemporalFusionTransformer, TFTConfig, TemporalFusionTransformer};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Benchmark configuration constants
|
||||
const WARMUP_ITERATIONS: usize = 5;
|
||||
const BATCH_SIZE: usize = 1; // Single inference for memory analysis
|
||||
const SEQ_LEN: usize = 60;
|
||||
const HORIZON: usize = 10;
|
||||
|
||||
/// Create standard TFT configuration (54 features, production)
|
||||
fn create_tft_config() -> TFTConfig {
|
||||
TFTConfig {
|
||||
input_dim: 54,
|
||||
hidden_dim: 256,
|
||||
num_heads: 8,
|
||||
num_layers: 3,
|
||||
prediction_horizon: HORIZON,
|
||||
sequence_length: SEQ_LEN,
|
||||
num_quantiles: 3,
|
||||
num_static_features: 5,
|
||||
num_known_features: 10,
|
||||
num_unknown_features: 210,
|
||||
learning_rate: 0.001,
|
||||
batch_size: 32,
|
||||
dropout_rate: 0.1,
|
||||
l2_regularization: 0.0001,
|
||||
use_flash_attention: false,
|
||||
mixed_precision: false,
|
||||
memory_efficient: true,
|
||||
max_inference_latency_us: 3200,
|
||||
target_throughput_pps: 10_000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate synthetic TFT inputs
|
||||
fn generate_tft_inputs(
|
||||
batch_size: usize,
|
||||
config: &TFTConfig,
|
||||
device: &Device,
|
||||
) -> Result<(Tensor, Tensor, Tensor), Box<dyn std::error::Error>> {
|
||||
let static_features =
|
||||
Tensor::randn(0f32, 1f32, (batch_size, config.num_static_features), device)?;
|
||||
|
||||
let historical_features = Tensor::randn(
|
||||
0f32,
|
||||
1f32,
|
||||
(
|
||||
batch_size,
|
||||
config.sequence_length,
|
||||
config.num_unknown_features,
|
||||
),
|
||||
device,
|
||||
)?;
|
||||
|
||||
let future_features = Tensor::randn(
|
||||
0f32,
|
||||
1f32,
|
||||
(
|
||||
batch_size,
|
||||
config.prediction_horizon,
|
||||
config.num_known_features,
|
||||
),
|
||||
device,
|
||||
)?;
|
||||
|
||||
Ok((static_features, historical_features, future_features))
|
||||
}
|
||||
|
||||
/// Benchmark 1: FP32 model memory footprint
|
||||
fn bench_fp32_memory_footprint(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_fp32_memory_footprint");
|
||||
group.sample_size(10); // Small sample for memory-focused benchmarks
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
// Skip benchmark if CUDA not available
|
||||
if matches!(device, Device::Cpu) {
|
||||
println!("⚠️ CUDA not available, skipping FP32 memory benchmark");
|
||||
return;
|
||||
}
|
||||
|
||||
group.bench_function("model_creation", |b| {
|
||||
b.iter(|| {
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
|
||||
// Baseline snapshot
|
||||
let baseline = profiler.take_snapshot().expect("Baseline snapshot failed");
|
||||
|
||||
// Create FP32 model
|
||||
let _model = black_box(
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Model creation failed"),
|
||||
);
|
||||
|
||||
// Wait for VRAM allocation to stabilize
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Measure memory after model creation
|
||||
let after_create = profiler
|
||||
.take_snapshot()
|
||||
.expect("Post-creation snapshot failed");
|
||||
|
||||
let vram_used_mb = after_create.vram_used_mb - baseline.vram_used_mb;
|
||||
|
||||
black_box(vram_used_mb)
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("inference_memory", |b| {
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
let baseline = profiler.take_snapshot().expect("Baseline snapshot failed");
|
||||
|
||||
let mut model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Model creation failed");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Input generation failed");
|
||||
|
||||
// Warmup
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
// Run inference
|
||||
let _ = black_box(
|
||||
model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("Forward pass failed"),
|
||||
);
|
||||
|
||||
// Measure peak memory
|
||||
let snapshot = profiler.take_snapshot().expect("Snapshot failed");
|
||||
let vram_used_mb = snapshot.vram_used_mb - baseline.vram_used_mb;
|
||||
|
||||
black_box(vram_used_mb)
|
||||
});
|
||||
});
|
||||
|
||||
// Print summary statistics
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
let baseline = profiler.take_snapshot().expect("Baseline failed");
|
||||
|
||||
let _model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Model creation failed");
|
||||
|
||||
let after_create = profiler.take_snapshot().expect("Post-create failed");
|
||||
let param_memory_mb = after_create.vram_used_mb - baseline.vram_used_mb;
|
||||
|
||||
println!("\n=== FP32 Memory Footprint ===");
|
||||
println!("Parameter Memory: {:.0} MB", param_memory_mb);
|
||||
println!(
|
||||
"Estimated Optimizer Memory: {:.0} MB (2x params for Adam)",
|
||||
param_memory_mb * 2.0
|
||||
);
|
||||
println!(
|
||||
"Total Budget (params + optimizer): {:.0} MB",
|
||||
param_memory_mb * 3.0
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 2: INT8 model memory footprint
|
||||
fn bench_int8_memory_footprint(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_int8_memory_footprint");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
if matches!(device, Device::Cpu) {
|
||||
println!("⚠️ CUDA not available, skipping INT8 memory benchmark");
|
||||
return;
|
||||
}
|
||||
|
||||
group.bench_function("model_creation", |b| {
|
||||
b.iter(|| {
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
let baseline = profiler.take_snapshot().expect("Baseline snapshot failed");
|
||||
|
||||
// Create FP32 source model (required for quantization)
|
||||
let fp32_model =
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("FP32 model creation failed");
|
||||
|
||||
// Quantize to INT8
|
||||
let _int8_model = black_box(
|
||||
QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Quantization failed"),
|
||||
);
|
||||
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
let after_create = profiler.take_snapshot().expect("Post-create failed");
|
||||
let vram_used_mb = after_create.vram_used_mb - baseline.vram_used_mb;
|
||||
|
||||
black_box(vram_used_mb)
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("inference_memory", |b| {
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
let baseline = profiler.take_snapshot().expect("Baseline failed");
|
||||
|
||||
let fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("FP32 model creation failed");
|
||||
|
||||
let int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Quantization failed");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Input generation failed");
|
||||
|
||||
// Warmup
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("Forward pass failed"),
|
||||
);
|
||||
|
||||
let snapshot = profiler.take_snapshot().expect("Snapshot failed");
|
||||
let vram_used_mb = snapshot.vram_used_mb - baseline.vram_used_mb;
|
||||
|
||||
black_box(vram_used_mb)
|
||||
});
|
||||
});
|
||||
|
||||
// Print summary
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
let baseline = profiler.take_snapshot().expect("Baseline failed");
|
||||
|
||||
let fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("FP32 model creation failed");
|
||||
|
||||
let _int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Quantization failed");
|
||||
|
||||
let after_create = profiler.take_snapshot().expect("Post-create failed");
|
||||
let param_memory_mb = after_create.vram_used_mb - baseline.vram_used_mb;
|
||||
|
||||
println!("\n=== INT8 Memory Footprint ===");
|
||||
println!("Parameter Memory: {:.0} MB", param_memory_mb);
|
||||
println!(
|
||||
"Estimated Optimizer Memory: {:.0} MB",
|
||||
param_memory_mb * 2.0
|
||||
);
|
||||
println!("Total Budget: {:.0} MB", param_memory_mb * 3.0);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 3: INT8 with weight caching (trade memory for speed)
|
||||
fn bench_int8_with_caching_memory(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_int8_cached_memory");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
if matches!(device, Device::Cpu) {
|
||||
println!("⚠️ CUDA not available, skipping INT8 caching benchmark");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: This benchmark simulates cached dequantized weights by pre-loading them
|
||||
group.bench_function("cached_inference_memory", |b| {
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
let baseline = profiler.take_snapshot().expect("Baseline failed");
|
||||
|
||||
let fp32_model = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("FP32 model creation failed");
|
||||
|
||||
let int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Quantization failed");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Input generation failed");
|
||||
|
||||
// Warmup (pre-cache weights via inference)
|
||||
for _ in 0..WARMUP_ITERATIONS {
|
||||
let _ = int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
}
|
||||
|
||||
// Measure memory with cached weights
|
||||
let after_warmup = profiler.take_snapshot().expect("Post-warmup failed");
|
||||
let cached_memory_mb = after_warmup.vram_used_mb - baseline.vram_used_mb;
|
||||
|
||||
b.iter(|| {
|
||||
let _ = black_box(
|
||||
int8_model
|
||||
.forward(&static_features, &historical_features, &future_features)
|
||||
.expect("Forward pass failed"),
|
||||
);
|
||||
|
||||
black_box(cached_memory_mb)
|
||||
});
|
||||
});
|
||||
|
||||
println!("\n=== INT8 with Weight Caching ===");
|
||||
println!("Note: Cached weights stored in FP32 for faster inference");
|
||||
println!("Trade-off: +25% memory for -50% latency (estimated)");
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark 4: GPU VRAM usage comparison (CUDA-specific)
|
||||
fn bench_gpu_vram_usage(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_gpu_vram_comparison");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
if matches!(device, Device::Cpu) {
|
||||
println!("⚠️ CUDA not available, skipping VRAM comparison");
|
||||
return;
|
||||
}
|
||||
|
||||
// FP32 VRAM measurement
|
||||
group.bench_function("fp32_vram", |b| {
|
||||
b.iter(|| {
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
let baseline = profiler.take_snapshot().expect("Baseline failed");
|
||||
|
||||
let mut model =
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Model creation failed");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Input generation failed");
|
||||
|
||||
// Run multiple inferences to measure peak VRAM
|
||||
let mut peak_vram = 0.0f64;
|
||||
for _ in 0..10 {
|
||||
let _ = model.forward(&static_features, &historical_features, &future_features);
|
||||
|
||||
let snapshot = profiler.take_snapshot().expect("Snapshot failed");
|
||||
let vram_mb = snapshot.vram_used_mb - baseline.vram_used_mb;
|
||||
peak_vram = peak_vram.max(vram_mb);
|
||||
}
|
||||
|
||||
black_box(peak_vram)
|
||||
});
|
||||
});
|
||||
|
||||
// INT8 VRAM measurement
|
||||
group.bench_function("int8_vram", |b| {
|
||||
b.iter(|| {
|
||||
let mut profiler = MemoryProfiler::new(0);
|
||||
let baseline = profiler.take_snapshot().expect("Baseline failed");
|
||||
|
||||
let fp32_model =
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("FP32 model creation failed");
|
||||
|
||||
let int8_model = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_model)
|
||||
.expect("Quantization failed");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Input generation failed");
|
||||
|
||||
let mut peak_vram = 0.0f64;
|
||||
for _ in 0..10 {
|
||||
let _ =
|
||||
int8_model.forward(&static_features, &historical_features, &future_features);
|
||||
|
||||
let snapshot = profiler.take_snapshot().expect("Snapshot failed");
|
||||
let vram_mb = snapshot.vram_used_mb - baseline.vram_used_mb;
|
||||
peak_vram = peak_vram.max(vram_mb);
|
||||
}
|
||||
|
||||
black_box(peak_vram)
|
||||
});
|
||||
});
|
||||
|
||||
// Print comparison summary
|
||||
println!("\n=== GPU VRAM Usage Summary ===");
|
||||
|
||||
// FP32 measurement
|
||||
let mut profiler_fp32 = MemoryProfiler::new(0);
|
||||
let baseline_fp32 = profiler_fp32.take_snapshot().expect("Baseline failed");
|
||||
|
||||
let mut model_fp32 = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("Model creation failed");
|
||||
|
||||
let (static_features, historical_features, future_features) =
|
||||
generate_tft_inputs(BATCH_SIZE, &config, &device).expect("Input generation failed");
|
||||
|
||||
let mut fp32_peak = 0.0f64;
|
||||
for _ in 0..10 {
|
||||
let _ = model_fp32.forward(&static_features, &historical_features, &future_features);
|
||||
let snap = profiler_fp32.take_snapshot().expect("Snapshot failed");
|
||||
fp32_peak = fp32_peak.max(snap.vram_used_mb - baseline_fp32.vram_used_mb);
|
||||
}
|
||||
|
||||
// INT8 measurement
|
||||
let mut profiler_int8 = MemoryProfiler::new(0);
|
||||
let baseline_int8 = profiler_int8.take_snapshot().expect("Baseline failed");
|
||||
|
||||
let fp32_src = TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("FP32 model creation failed");
|
||||
|
||||
let model_int8 =
|
||||
QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_src).expect("Quantization failed");
|
||||
|
||||
let mut int8_peak = 0.0f64;
|
||||
for _ in 0..10 {
|
||||
let _ = model_int8.forward(&static_features, &historical_features, &future_features);
|
||||
let snap = profiler_int8.take_snapshot().expect("Snapshot failed");
|
||||
int8_peak = int8_peak.max(snap.vram_used_mb - baseline_int8.vram_used_mb);
|
||||
}
|
||||
|
||||
let reduction_mb = fp32_peak - int8_peak;
|
||||
let reduction_pct = (reduction_mb / fp32_peak) * 100.0;
|
||||
|
||||
println!("FP32 Peak VRAM: {:.0} MB", fp32_peak);
|
||||
println!("INT8 Peak VRAM: {:.0} MB", int8_peak);
|
||||
println!(
|
||||
"Memory Reduction: {:.0} MB ({:.1}%)",
|
||||
reduction_mb, reduction_pct
|
||||
);
|
||||
println!(
|
||||
"75% Target: {}",
|
||||
if reduction_pct >= 75.0 {
|
||||
"✅ ACHIEVED"
|
||||
} else {
|
||||
"❌ NOT MET"
|
||||
}
|
||||
);
|
||||
|
||||
// RTX 3050 Ti budget validation
|
||||
let rtx3050ti_vram_mb = 4096.0;
|
||||
let num_models = 4; // DQN, PPO, MAMBA-2, TFT
|
||||
let budget_per_model = rtx3050ti_vram_mb / num_models as f64;
|
||||
|
||||
println!("\n=== RTX 3050 Ti Budget Validation ===");
|
||||
println!("Total VRAM: {:.0} MB", rtx3050ti_vram_mb);
|
||||
println!("Budget per model (4 models): {:.0} MB", budget_per_model);
|
||||
println!("FP32 Usage: {:.0} MB", fp32_peak);
|
||||
println!("INT8 Usage: {:.0} MB", int8_peak);
|
||||
println!(
|
||||
"FP32 fits budget: {}",
|
||||
if fp32_peak <= budget_per_model {
|
||||
"✅ YES"
|
||||
} else {
|
||||
"❌ NO"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"INT8 fits budget: {}",
|
||||
if int8_peak <= budget_per_model {
|
||||
"✅ YES"
|
||||
} else {
|
||||
"❌ NO"
|
||||
}
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Memory reduction validation (75% target)
|
||||
fn bench_memory_reduction_validation(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("tft_memory_reduction_validation");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let config = create_tft_config();
|
||||
let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);
|
||||
|
||||
if matches!(device, Device::Cpu) {
|
||||
println!("⚠️ CUDA not available, skipping validation benchmark");
|
||||
return;
|
||||
}
|
||||
|
||||
group.bench_function("validate_75_percent_reduction", |b| {
|
||||
b.iter(|| {
|
||||
// Measure FP32
|
||||
let mut profiler_fp32 = MemoryProfiler::new(0);
|
||||
let baseline_fp32 = profiler_fp32.take_snapshot().expect("FP32 baseline failed");
|
||||
|
||||
let _model_fp32 =
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("FP32 model creation failed");
|
||||
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
let after_fp32 = profiler_fp32.take_snapshot().expect("FP32 snapshot failed");
|
||||
let fp32_mb = after_fp32.vram_used_mb - baseline_fp32.vram_used_mb;
|
||||
|
||||
// Measure INT8
|
||||
let mut profiler_int8 = MemoryProfiler::new(0);
|
||||
let baseline_int8 = profiler_int8.take_snapshot().expect("INT8 baseline failed");
|
||||
|
||||
let fp32_src =
|
||||
TemporalFusionTransformer::new_with_device(config.clone(), device.clone())
|
||||
.expect("FP32 source creation failed");
|
||||
|
||||
let _model_int8 = QuantizedTemporalFusionTransformer::new_from_fp32(&fp32_src)
|
||||
.expect("Quantization failed");
|
||||
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
let after_int8 = profiler_int8.take_snapshot().expect("INT8 snapshot failed");
|
||||
let int8_mb = after_int8.vram_used_mb - baseline_int8.vram_used_mb;
|
||||
|
||||
// Calculate reduction
|
||||
let reduction_pct = ((fp32_mb - int8_mb) / fp32_mb) * 100.0;
|
||||
|
||||
black_box((fp32_mb, int8_mb, reduction_pct))
|
||||
});
|
||||
});
|
||||
|
||||
println!("\n=== Memory Reduction Validation ===");
|
||||
println!("Target: 75% reduction (FP32 → INT8)");
|
||||
println!("Expected: FP32 ~400 MB → INT8 ~100 MB");
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_fp32_memory_footprint,
|
||||
bench_int8_memory_footprint,
|
||||
bench_int8_with_caching_memory,
|
||||
bench_gpu_vram_usage,
|
||||
bench_memory_reduction_validation
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,493 +0,0 @@
|
||||
//! Performance Benchmarks for Wave D Regime Detection Features
|
||||
//!
|
||||
//! Agent D17 - Wave D feature performance validation:
|
||||
//! - CUSUM Statistics (Agent D13, indices 201-210)
|
||||
//! - ADX & Directional Indicators (Agent D14, indices 211-215)
|
||||
//! - Regime Transition Probabilities (Agent D15, indices 216-220)
|
||||
//! - Adaptive Strategy Metrics (Agent D16, indices 221-224)
|
||||
//!
|
||||
//! ## Performance Targets
|
||||
//! - CUSUM: <50μs per bar
|
||||
//! - ADX: <80μs per bar
|
||||
//! - Transition: <50μs per bar
|
||||
//! - Adaptive: <100μs per bar
|
||||
//!
|
||||
//! ## Run Benchmarks
|
||||
//! ```bash
|
||||
//! cargo bench -p ml --bench wave_d_features_bench
|
||||
//! ```
|
||||
|
||||
use chrono::Utc;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use ml::ensemble::MarketRegime;
|
||||
use ml::features::{
|
||||
extraction::OHLCVBar,
|
||||
regime_adaptive::RegimeAdaptiveFeatures,
|
||||
regime_adx::{OHLCVBar as ADXBar, RegimeADXFeatures},
|
||||
regime_cusum::RegimeCUSUMFeatures,
|
||||
regime_transition::RegimeTransitionFeatures,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Generators
|
||||
// ============================================================================
|
||||
|
||||
/// Generate realistic log returns for CUSUM testing
|
||||
fn generate_log_returns(num_bars: usize, seed: u64) -> Vec<f64> {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
let mut rng = fastrand::Rng::with_seed(seed);
|
||||
let mut returns = Vec::with_capacity(num_bars);
|
||||
|
||||
for i in 0..num_bars {
|
||||
// Simulate regime changes with drift shifts
|
||||
let regime_phase = (i / 50) % 4;
|
||||
let drift = match regime_phase {
|
||||
0 => 0.0, // Normal regime
|
||||
1 => 0.002, // Positive drift
|
||||
2 => 0.0, // Return to normal
|
||||
3 => -0.002, // Negative drift
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Add cycle component
|
||||
let cycle = (i as f64 * 0.1 * PI).sin() * 0.0005;
|
||||
|
||||
// Add noise
|
||||
let noise = (rng.f64() - 0.5) * 0.005;
|
||||
|
||||
returns.push(drift + cycle + noise);
|
||||
}
|
||||
|
||||
returns
|
||||
}
|
||||
|
||||
/// Generate realistic OHLCV bars for ADX testing
|
||||
fn generate_ohlcv_bars(num_bars: usize, seed: u64) -> Vec<ADXBar> {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
let mut rng = fastrand::Rng::with_seed(seed);
|
||||
let mut bars = Vec::with_capacity(num_bars);
|
||||
let mut close = 100.0;
|
||||
let base_time = Utc::now().timestamp();
|
||||
|
||||
for i in 0..num_bars {
|
||||
// Combine trend, cycle, and noise
|
||||
let trend = (i as f64 * 0.01) % 10.0 - 5.0;
|
||||
let cycle = (i as f64 * 0.1 * PI).sin() * 2.0;
|
||||
let noise = (rng.f64() - 0.5) * 0.5;
|
||||
|
||||
close += trend * 0.01 + cycle * 0.05 + noise;
|
||||
close = close.max(50.0).min(150.0);
|
||||
|
||||
// Generate realistic OHLC with typical 0.1-0.5% intrabar range
|
||||
let range = close * 0.003 * (1.0 + rng.f64());
|
||||
let high = close + range * rng.f64();
|
||||
let low = close - range * rng.f64();
|
||||
let open = low + (high - low) * rng.f64();
|
||||
let volume = 10000.0 + (i as f64 * 0.5 * PI).sin().abs() * 5000.0 + rng.f64() * 2000.0;
|
||||
|
||||
bars.push(ADXBar {
|
||||
timestamp: base_time + (i as i64 * 60),
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
});
|
||||
}
|
||||
|
||||
bars
|
||||
}
|
||||
|
||||
/// Generate realistic OHLCV bars (extraction format) for Adaptive testing
|
||||
fn generate_extraction_bars(num_bars: usize, seed: u64) -> Vec<OHLCVBar> {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
let mut rng = fastrand::Rng::with_seed(seed);
|
||||
let mut bars = Vec::with_capacity(num_bars);
|
||||
let mut close = 100.0;
|
||||
let base_time = Utc::now();
|
||||
|
||||
for i in 0..num_bars {
|
||||
// Combine trend, cycle, and noise
|
||||
let trend = (i as f64 * 0.01) % 10.0 - 5.0;
|
||||
let cycle = (i as f64 * 0.1 * PI).sin() * 2.0;
|
||||
let noise = (rng.f64() - 0.5) * 0.5;
|
||||
|
||||
close += trend * 0.01 + cycle * 0.05 + noise;
|
||||
close = close.max(50.0).min(150.0);
|
||||
|
||||
// Generate realistic OHLC with typical 0.1-0.5% intrabar range
|
||||
let range = close * 0.003 * (1.0 + rng.f64());
|
||||
let high = close + range * rng.f64();
|
||||
let low = close - range * rng.f64();
|
||||
let open = low + (high - low) * rng.f64();
|
||||
let volume = 10000.0 + (i as f64 * 0.5 * PI).sin().abs() * 5000.0 + rng.f64() * 2000.0;
|
||||
|
||||
bars.push(OHLCVBar {
|
||||
timestamp: base_time + chrono::Duration::seconds(i as i64 * 60),
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
});
|
||||
}
|
||||
|
||||
bars
|
||||
}
|
||||
|
||||
/// Generate realistic regime sequence for Transition testing
|
||||
fn generate_regime_sequence(num_regimes: usize, seed: u64) -> Vec<MarketRegime> {
|
||||
let mut rng = fastrand::Rng::with_seed(seed);
|
||||
let regimes = vec![
|
||||
MarketRegime::Normal,
|
||||
MarketRegime::Trending,
|
||||
MarketRegime::Sideways,
|
||||
MarketRegime::Bull,
|
||||
MarketRegime::Bear,
|
||||
MarketRegime::HighVolatility,
|
||||
MarketRegime::Crisis,
|
||||
];
|
||||
|
||||
(0..num_regimes)
|
||||
.map(|_| regimes[rng.usize(0..regimes.len())])
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CUSUM Features Benchmarks (Agent D13, indices 201-210)
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark CUSUM features cold start (single update)
|
||||
fn bench_cusum_features_cold(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cusum_features");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let returns = generate_log_returns(1000, 42);
|
||||
|
||||
group.bench_function("single_update_cold", |b| {
|
||||
b.iter(|| {
|
||||
let mut features = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
|
||||
let result = features.update(black_box(returns[0]));
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark CUSUM features warm state (incremental updates)
|
||||
fn bench_cusum_features_warm(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cusum_features_warm");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let returns = generate_log_returns(1000, 43);
|
||||
|
||||
// Warm up with 100 bars
|
||||
let mut features = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
|
||||
for &ret in returns.iter().take(100) {
|
||||
features.update(ret);
|
||||
}
|
||||
|
||||
group.bench_function("single_update_warm", |b| {
|
||||
let mut feat = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
|
||||
// Pre-warm
|
||||
for &ret in returns.iter().take(100) {
|
||||
feat.update(ret);
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
let result = feat.update(black_box(returns[idx % returns.len()]));
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark CUSUM features full sequence (all 10 features)
|
||||
fn bench_cusum_features_sequence(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("cusum_features_sequence");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let returns = generate_log_returns(500, 44);
|
||||
|
||||
group.bench_function("500_bars_full_pipeline", |b| {
|
||||
b.iter(|| {
|
||||
let mut features = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
|
||||
for &ret in returns.iter() {
|
||||
let result = features.update(black_box(ret));
|
||||
black_box(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ADX Features Benchmarks (Agent D14, indices 211-215)
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark ADX features cold start
|
||||
fn bench_adx_features_cold(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("adx_features");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let bars = generate_ohlcv_bars(1000, 45);
|
||||
|
||||
group.bench_function("single_update_cold", |b| {
|
||||
b.iter(|| {
|
||||
let mut features = RegimeADXFeatures::new(14);
|
||||
let result = features.update(black_box(&bars[0]));
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark ADX features warm state
|
||||
fn bench_adx_features_warm(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("adx_features_warm");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let bars = generate_ohlcv_bars(1000, 46);
|
||||
|
||||
// Warm up with 28 bars (2 * period for full ADX initialization)
|
||||
let mut features = RegimeADXFeatures::new(14);
|
||||
for bar in bars.iter().take(28) {
|
||||
features.update(bar);
|
||||
}
|
||||
|
||||
group.bench_function("single_update_warm", |b| {
|
||||
let mut feat = RegimeADXFeatures::new(14);
|
||||
// Pre-warm
|
||||
for bar in bars.iter().take(28) {
|
||||
feat.update(bar);
|
||||
}
|
||||
|
||||
let mut idx = 28;
|
||||
b.iter(|| {
|
||||
let result = feat.update(black_box(&bars[idx % bars.len()]));
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark ADX features full sequence
|
||||
fn bench_adx_features_sequence(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("adx_features_sequence");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let bars = generate_ohlcv_bars(500, 47);
|
||||
|
||||
group.bench_function("500_bars_full_pipeline", |b| {
|
||||
b.iter(|| {
|
||||
let mut features = RegimeADXFeatures::new(14);
|
||||
for bar in bars.iter() {
|
||||
let result = features.update(black_box(bar));
|
||||
black_box(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Transition Features Benchmarks (Agent D15, indices 216-220)
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark Transition features cold start
|
||||
fn bench_transition_features_cold(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("transition_features");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let regimes = generate_regime_sequence(1000, 48);
|
||||
|
||||
group.bench_function("single_update_cold", |b| {
|
||||
b.iter(|| {
|
||||
let mut features = RegimeTransitionFeatures::new(4, 0.1);
|
||||
let result = features.update(black_box(regimes[0]));
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark Transition features warm state
|
||||
fn bench_transition_features_warm(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("transition_features_warm");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let regimes = generate_regime_sequence(1000, 49);
|
||||
|
||||
// Warm up with 50 regime observations
|
||||
let mut features = RegimeTransitionFeatures::new(4, 0.1);
|
||||
for ®ime in regimes.iter().take(50) {
|
||||
features.update(regime);
|
||||
}
|
||||
|
||||
group.bench_function("single_update_warm", |b| {
|
||||
let mut feat = RegimeTransitionFeatures::new(4, 0.1);
|
||||
// Pre-warm
|
||||
for ®ime in regimes.iter().take(50) {
|
||||
feat.update(regime);
|
||||
}
|
||||
|
||||
let mut idx = 50;
|
||||
b.iter(|| {
|
||||
let result = feat.update(black_box(regimes[idx % regimes.len()]));
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark Transition features full sequence
|
||||
fn bench_transition_features_sequence(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("transition_features_sequence");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let regimes = generate_regime_sequence(500, 50);
|
||||
|
||||
group.bench_function("500_regimes_full_pipeline", |b| {
|
||||
b.iter(|| {
|
||||
let mut features = RegimeTransitionFeatures::new(4, 0.1);
|
||||
for ®ime in regimes.iter() {
|
||||
let result = features.update(black_box(regime));
|
||||
black_box(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Adaptive Features Benchmarks (Agent D16, indices 221-224)
|
||||
// ============================================================================
|
||||
|
||||
/// Benchmark Adaptive features cold start
|
||||
fn bench_adaptive_features_cold(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("adaptive_features");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let bars = generate_extraction_bars(100, 51);
|
||||
|
||||
group.bench_function("single_update_cold", |b| {
|
||||
b.iter(|| {
|
||||
let mut features = RegimeAdaptiveFeatures::new(20, 100_000.0, 14);
|
||||
let result = features.update(
|
||||
black_box(MarketRegime::Normal),
|
||||
black_box(0.01),
|
||||
black_box(50_000.0),
|
||||
black_box(&bars),
|
||||
);
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark Adaptive features warm state
|
||||
fn bench_adaptive_features_warm(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("adaptive_features_warm");
|
||||
group.measurement_time(Duration::from_secs(5));
|
||||
|
||||
let bars = generate_extraction_bars(100, 52);
|
||||
let regimes = generate_regime_sequence(100, 53);
|
||||
|
||||
// Warm up with 20 updates
|
||||
let mut features = RegimeAdaptiveFeatures::new(20, 100_000.0, 14);
|
||||
for i in 0..20 {
|
||||
features.update(regimes[i], 0.01, 50_000.0, &bars);
|
||||
}
|
||||
|
||||
group.bench_function("single_update_warm", |b| {
|
||||
let mut feat = RegimeAdaptiveFeatures::new(20, 100_000.0, 14);
|
||||
// Pre-warm
|
||||
for i in 0..20 {
|
||||
feat.update(regimes[i], 0.01, 50_000.0, &bars);
|
||||
}
|
||||
|
||||
let mut idx = 20;
|
||||
b.iter(|| {
|
||||
let result = feat.update(
|
||||
black_box(regimes[idx % regimes.len()]),
|
||||
black_box(0.01),
|
||||
black_box(50_000.0),
|
||||
black_box(&bars),
|
||||
);
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark Adaptive features full sequence
|
||||
fn bench_adaptive_features_sequence(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("adaptive_features_sequence");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let bars = generate_extraction_bars(100, 54);
|
||||
let regimes = generate_regime_sequence(500, 55);
|
||||
|
||||
group.bench_function("500_updates_full_pipeline", |b| {
|
||||
b.iter(|| {
|
||||
let mut features = RegimeAdaptiveFeatures::new(20, 100_000.0, 14);
|
||||
for ®ime in regimes.iter() {
|
||||
let result = features.update(
|
||||
black_box(regime),
|
||||
black_box(0.01),
|
||||
black_box(50_000.0),
|
||||
black_box(&bars),
|
||||
);
|
||||
black_box(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Criterion Configuration
|
||||
// ============================================================================
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
// CUSUM Features (Agent D13)
|
||||
bench_cusum_features_cold,
|
||||
bench_cusum_features_warm,
|
||||
bench_cusum_features_sequence,
|
||||
// ADX Features (Agent D14)
|
||||
bench_adx_features_cold,
|
||||
bench_adx_features_warm,
|
||||
bench_adx_features_sequence,
|
||||
// Transition Features (Agent D15)
|
||||
bench_transition_features_cold,
|
||||
bench_transition_features_warm,
|
||||
bench_transition_features_sequence,
|
||||
// Adaptive Features (Agent D16)
|
||||
bench_adaptive_features_cold,
|
||||
bench_adaptive_features_warm,
|
||||
bench_adaptive_features_sequence,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
@@ -1,698 +0,0 @@
|
||||
//! Comprehensive Benchmark Suite for Complete 54-Feature Pipeline (Production)
|
||||
//!
|
||||
//! Production pipeline performance validation:
|
||||
//! - Core features: 46 features (indices 0-45)
|
||||
//! - Extended features: 8 features (indices 46-53)
|
||||
//! - Total: 54 features
|
||||
//!
|
||||
//! ## Benchmark Scenarios
|
||||
//!
|
||||
//! 1. **Cold Start**: First bar initialization overhead
|
||||
//! - Initialize all 54 feature extractors
|
||||
//! - Process first bar
|
||||
//! - Target: <500μs
|
||||
//!
|
||||
//! 2. **Warm State**: 100th bar (steady state)
|
||||
//! - All extractors initialized
|
||||
//! - VecDeques filled
|
||||
//! - Process single bar
|
||||
//! - Target: <65μs
|
||||
//!
|
||||
//! 3. **Batch Processing**: 1000-bar sequence
|
||||
//! - Process 1000 bars sequentially
|
||||
//! - Measure total time
|
||||
//! - Target: <65ms (65μs/bar average)
|
||||
//!
|
||||
//! 4. **Memory Allocation**: Heap allocation profile
|
||||
//! - Measure allocations per bar
|
||||
//! - Target: <100 allocations/bar
|
||||
//!
|
||||
//! 5. **Cache Efficiency**: CPU cache miss analysis
|
||||
//! - Measure L1/L2/L3 cache misses
|
||||
//! - Optimize data layout
|
||||
//!
|
||||
//! ## Run Benchmarks
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo bench -p ml --bench wave_d_full_pipeline_bench
|
||||
//! ```
|
||||
//!
|
||||
//! ## Comparison with Wave C
|
||||
//!
|
||||
//! This benchmark provides direct comparison between baseline and production:
|
||||
//! - Baseline: 46 features
|
||||
//! - Production complete: 54 features (+17.4% feature count)
|
||||
//! - Expected overhead: <20% latency increase
|
||||
|
||||
use chrono::Utc;
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use ml::ensemble::MarketRegime;
|
||||
use ml::features::{
|
||||
// Feature configuration
|
||||
config::FeatureConfig,
|
||||
extraction::OHLCVBar,
|
||||
|
||||
// Wave C pipeline (201 features)
|
||||
pipeline::FeatureExtractionPipeline,
|
||||
regime_adaptive::RegimeAdaptiveFeatures,
|
||||
|
||||
regime_adx::{OHLCVBar as ADXBar, RegimeADXFeatures},
|
||||
// Wave D regime features (24 features, indices 201-224)
|
||||
regime_cusum::RegimeCUSUMFeatures,
|
||||
regime_transition::RegimeTransitionFeatures,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Generators
|
||||
// ============================================================================
|
||||
|
||||
/// Generate realistic OHLCV bars for full pipeline testing
|
||||
fn generate_ohlcv_bars(num_bars: usize, seed: u64) -> Vec<OHLCVBar> {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
let mut rng = fastrand::Rng::with_seed(seed);
|
||||
let mut bars = Vec::with_capacity(num_bars);
|
||||
let mut close = 100.0;
|
||||
let base_time = Utc::now();
|
||||
|
||||
for i in 0..num_bars {
|
||||
// Simulate realistic price action with trend, cycles, and noise
|
||||
let trend = ((i as f64 * 0.01) % 20.0 - 10.0) * 0.02; // Trending component
|
||||
let cycle = (i as f64 * 0.1 * PI).sin() * 0.5; // Cyclical component
|
||||
let noise = (rng.f64() - 0.5) * 0.3; // Random noise
|
||||
|
||||
close += trend + cycle + noise;
|
||||
close = close.max(50.0).min(200.0); // Keep within reasonable bounds
|
||||
|
||||
// Generate realistic OHLC with 0.2-1.0% intrabar range
|
||||
let range = close * (0.002 + rng.f64() * 0.008);
|
||||
let high = close + range * (0.3 + rng.f64() * 0.7);
|
||||
let low = close - range * (0.3 + rng.f64() * 0.7);
|
||||
let open = low + (high - low) * rng.f64();
|
||||
|
||||
// Volume with realistic patterns (high volatility = high volume)
|
||||
let volatility_factor = (range / close).abs();
|
||||
let volume = 5000.0 + volatility_factor * 20000.0 + rng.f64() * 3000.0;
|
||||
|
||||
bars.push(OHLCVBar {
|
||||
timestamp: base_time + chrono::Duration::seconds(i as i64 * 60),
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume,
|
||||
});
|
||||
}
|
||||
|
||||
bars
|
||||
}
|
||||
|
||||
/// Generate realistic regime sequence for Wave D features
|
||||
fn generate_regime_sequence(num_bars: usize, seed: u64) -> Vec<MarketRegime> {
|
||||
let mut rng = fastrand::Rng::with_seed(seed);
|
||||
let regimes = vec![
|
||||
MarketRegime::Normal,
|
||||
MarketRegime::Trending,
|
||||
MarketRegime::Sideways,
|
||||
MarketRegime::Bull,
|
||||
MarketRegime::Bear,
|
||||
MarketRegime::HighVolatility,
|
||||
MarketRegime::Crisis,
|
||||
];
|
||||
|
||||
let mut sequence = Vec::with_capacity(num_bars);
|
||||
let mut current_regime = regimes[0];
|
||||
let mut regime_duration = 0;
|
||||
let regime_persistence = 20; // Average bars per regime
|
||||
|
||||
for _ in 0..num_bars {
|
||||
regime_duration += 1;
|
||||
|
||||
// Probabilistic regime changes (higher chance after longer duration)
|
||||
if regime_duration > regime_persistence && rng.f64() > 0.7 {
|
||||
current_regime = regimes[rng.usize(0..regimes.len())];
|
||||
regime_duration = 0;
|
||||
}
|
||||
|
||||
sequence.push(current_regime);
|
||||
}
|
||||
|
||||
sequence
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Full 54-Feature Pipeline
|
||||
// ============================================================================
|
||||
|
||||
/// Full 54-feature extraction pipeline (production)
|
||||
struct Full54FeaturePipeline {
|
||||
// Core pipeline (46 features, indices 0-45)
|
||||
wave_c_pipeline: FeatureExtractionPipeline,
|
||||
|
||||
// Extended extractors (8 features, indices 46-53)
|
||||
regime_cusum: RegimeCUSUMFeatures,
|
||||
regime_adx: RegimeADXFeatures,
|
||||
regime_transition: RegimeTransitionFeatures,
|
||||
regime_adaptive: RegimeAdaptiveFeatures,
|
||||
|
||||
// State tracking
|
||||
bars: Vec<OHLCVBar>,
|
||||
regimes: Vec<MarketRegime>,
|
||||
|
||||
// Performance tracking
|
||||
total_extractions: u64,
|
||||
wave_c_latency_ns: u64,
|
||||
wave_d_latency_ns: u64,
|
||||
}
|
||||
|
||||
impl Full54FeaturePipeline {
|
||||
/// Create new full pipeline
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
wave_c_pipeline: FeatureExtractionPipeline::new(),
|
||||
regime_cusum: RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0),
|
||||
regime_adx: RegimeADXFeatures::new(14),
|
||||
regime_transition: RegimeTransitionFeatures::new(4, 0.1),
|
||||
regime_adaptive: RegimeAdaptiveFeatures::new(20, 100_000.0, 14),
|
||||
bars: Vec::with_capacity(100),
|
||||
regimes: Vec::with_capacity(100),
|
||||
total_extractions: 0,
|
||||
wave_c_latency_ns: 0,
|
||||
wave_d_latency_ns: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update pipeline with new bar and regime
|
||||
fn update(&mut self, bar: &OHLCVBar, regime: MarketRegime) {
|
||||
self.bars.push(bar.clone());
|
||||
self.regimes.push(regime);
|
||||
|
||||
// Keep rolling window at reasonable size
|
||||
if self.bars.len() > 100 {
|
||||
self.bars.remove(0);
|
||||
self.regimes.remove(0);
|
||||
}
|
||||
|
||||
// Update Wave C pipeline
|
||||
self.wave_c_pipeline.update(bar);
|
||||
|
||||
// Update Wave D extractors
|
||||
let log_return = if self.bars.len() >= 2 {
|
||||
let prev_close = self.bars[self.bars.len() - 2].close;
|
||||
(bar.close / prev_close).ln()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.regime_cusum.update(log_return);
|
||||
|
||||
let adx_bar = ADXBar {
|
||||
timestamp: bar.timestamp.timestamp(),
|
||||
open: bar.open,
|
||||
high: bar.high,
|
||||
low: bar.low,
|
||||
close: bar.close,
|
||||
volume: bar.volume,
|
||||
};
|
||||
self.regime_adx.update(&adx_bar);
|
||||
|
||||
self.regime_transition.update(regime);
|
||||
self.regime_adaptive
|
||||
.update(regime, log_return, 50_000.0, &self.bars);
|
||||
}
|
||||
|
||||
/// Extract all 54 features (Core: 46 + Extended: 8)
|
||||
fn extract_all(&mut self, bar: &OHLCVBar, regime: MarketRegime) -> Result<Vec<f64>, String> {
|
||||
if self.bars.len() < 50 {
|
||||
return Err(format!("Insufficient warmup: {} bars", self.bars.len()));
|
||||
}
|
||||
|
||||
// Stage 1: Core features (46 in production, currently 65 in implementation)
|
||||
let wave_c_start = std::time::Instant::now();
|
||||
let mut wave_c_features = self
|
||||
.wave_c_pipeline
|
||||
.extract(bar)
|
||||
.map_err(|e| format!("Wave C extraction failed: {}", e))?;
|
||||
self.wave_c_latency_ns = wave_c_start.elapsed().as_nanos() as u64;
|
||||
|
||||
// Note: Current implementation produces 65 features, production target is 46
|
||||
if wave_c_features.len() != 65 {
|
||||
return Err(format!(
|
||||
"Core feature count mismatch: expected 65, got {}",
|
||||
wave_c_features.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Stage 2: Extended features (8 features in production, using 24 for compatibility)
|
||||
let wave_d_start = std::time::Instant::now();
|
||||
let mut wave_d_features = Vec::with_capacity(8);
|
||||
|
||||
// Compute log return for extractors
|
||||
let log_return = if self.bars.len() >= 2 {
|
||||
let prev_close = self.bars[self.bars.len() - 2].close;
|
||||
(bar.close / prev_close).ln()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// CUSUM Statistics (10 features, indices 201-210)
|
||||
let cusum_features = self.regime_cusum.update(log_return);
|
||||
wave_d_features.extend_from_slice(&cusum_features);
|
||||
|
||||
// ADX & Directional Indicators (5 features, indices 211-215)
|
||||
let adx_bar = ADXBar {
|
||||
timestamp: bar.timestamp.timestamp(),
|
||||
open: bar.open,
|
||||
high: bar.high,
|
||||
low: bar.low,
|
||||
close: bar.close,
|
||||
volume: bar.volume,
|
||||
};
|
||||
let adx_features = self.regime_adx.update(&adx_bar);
|
||||
wave_d_features.extend_from_slice(&adx_features);
|
||||
|
||||
// Regime Transition Probabilities (5 features, indices 216-220)
|
||||
let transition_features = self.regime_transition.update(regime);
|
||||
wave_d_features.extend_from_slice(&transition_features);
|
||||
|
||||
// Adaptive Strategy Metrics (4 features, indices 221-224)
|
||||
let adaptive_features = self
|
||||
.regime_adaptive
|
||||
.update(regime, log_return, 50_000.0, &self.bars);
|
||||
wave_d_features.extend_from_slice(&adaptive_features);
|
||||
|
||||
self.wave_d_latency_ns = wave_d_start.elapsed().as_nanos() as u64;
|
||||
|
||||
// Note: Using 24 features for compatibility, production target is 8
|
||||
if wave_d_features.len() < 8 {
|
||||
return Err(format!(
|
||||
"Extended feature count too low: expected >=8, got {}",
|
||||
wave_d_features.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Stage 3: Trim to production size (46 core + 8 extended = 54 total)
|
||||
wave_c_features.truncate(46);
|
||||
wave_d_features.truncate(8);
|
||||
|
||||
// Combine Core (46) + Extended (8) = 54 features
|
||||
wave_c_features.extend_from_slice(&wave_d_features);
|
||||
|
||||
self.total_extractions += 1;
|
||||
|
||||
Ok(wave_c_features)
|
||||
}
|
||||
|
||||
/// Get performance statistics
|
||||
fn get_performance(&self) -> (u64, u64, u64) {
|
||||
(
|
||||
self.total_extractions,
|
||||
self.wave_c_latency_ns,
|
||||
self.wave_d_latency_ns,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark 1: Cold Start (First Bar)
|
||||
// ============================================================================
|
||||
|
||||
fn bench_cold_start(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("full_pipeline_cold_start");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let bars = generate_ohlcv_bars(100, 1001);
|
||||
let regimes = generate_regime_sequence(100, 1002);
|
||||
|
||||
group.bench_function("54_features_first_bar", |b| {
|
||||
b.iter(|| {
|
||||
let mut pipeline = Full54FeaturePipeline::new();
|
||||
|
||||
// Feed warmup bars (50 bars minimum)
|
||||
for i in 0..50 {
|
||||
pipeline.update(&bars[i], regimes[i]);
|
||||
}
|
||||
|
||||
// Measure first extraction
|
||||
let result = pipeline.extract_all(black_box(&bars[50]), black_box(regimes[50]));
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark 2: Warm State (100th Bar)
|
||||
// ============================================================================
|
||||
|
||||
fn bench_warm_state(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("full_pipeline_warm_state");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let bars = generate_ohlcv_bars(200, 1003);
|
||||
let regimes = generate_regime_sequence(200, 1004);
|
||||
|
||||
// Pre-warm pipeline with 100 bars
|
||||
let mut pipeline = Full54FeaturePipeline::new();
|
||||
for i in 0..100 {
|
||||
pipeline.update(&bars[i], regimes[i]);
|
||||
}
|
||||
|
||||
group.bench_function("54_features_warm_100th_bar", |b| {
|
||||
let mut pipe = Full54FeaturePipeline::new();
|
||||
for i in 0..100 {
|
||||
pipe.update(&bars[i], regimes[i]);
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
let result = pipe.extract_all(
|
||||
black_box(&bars[idx % bars.len()]),
|
||||
black_box(regimes[idx % regimes.len()]),
|
||||
);
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark 3: Batch Processing (1000 Bars)
|
||||
// ============================================================================
|
||||
|
||||
fn bench_batch_processing(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("full_pipeline_batch");
|
||||
group.measurement_time(Duration::from_secs(30));
|
||||
group.sample_size(10); // Reduce sample size for long-running benchmark
|
||||
|
||||
let bars = generate_ohlcv_bars(1100, 1005);
|
||||
let regimes = generate_regime_sequence(1100, 1006);
|
||||
|
||||
group.bench_function("1000_bars_sequential", |b| {
|
||||
b.iter(|| {
|
||||
let mut pipeline = Full54FeaturePipeline::new();
|
||||
|
||||
// Warmup (50 bars)
|
||||
for i in 0..50 {
|
||||
pipeline.update(&bars[i], regimes[i]);
|
||||
}
|
||||
|
||||
// Process 1000 bars
|
||||
let mut results = Vec::with_capacity(1000);
|
||||
for i in 50..1050 {
|
||||
let result = pipeline.extract_all(&bars[i], regimes[i]);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
black_box(results);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark 4: Memory Allocation Profiling
|
||||
// ============================================================================
|
||||
|
||||
fn bench_memory_allocations(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("full_pipeline_memory");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let bars = generate_ohlcv_bars(200, 1007);
|
||||
let regimes = generate_regime_sequence(200, 1008);
|
||||
|
||||
// Pre-warm pipeline
|
||||
let mut pipeline = Full54FeaturePipeline::new();
|
||||
for i in 0..100 {
|
||||
pipeline.update(&bars[i], regimes[i]);
|
||||
}
|
||||
|
||||
group.bench_function("single_extraction_allocations", |b| {
|
||||
let mut pipe = Full54FeaturePipeline::new();
|
||||
for i in 0..100 {
|
||||
pipe.update(&bars[i], regimes[i]);
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
// Extract features (measure allocations via criterion)
|
||||
let result = pipe.extract_all(
|
||||
black_box(&bars[idx % bars.len()]),
|
||||
black_box(regimes[idx % regimes.len()]),
|
||||
);
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark 5: Throughput Scaling
|
||||
// ============================================================================
|
||||
|
||||
fn bench_throughput_scaling(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("full_pipeline_throughput");
|
||||
group.measurement_time(Duration::from_secs(15));
|
||||
|
||||
let batch_sizes = vec![10, 50, 100, 500, 1000];
|
||||
|
||||
for batch_size in batch_sizes {
|
||||
let bars = generate_ohlcv_bars(batch_size + 100, 2001);
|
||||
let regimes = generate_regime_sequence(batch_size + 100, 2002);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(batch_size),
|
||||
&batch_size,
|
||||
|b, &size| {
|
||||
b.iter(|| {
|
||||
let mut pipeline = Full54FeaturePipeline::new();
|
||||
|
||||
// Warmup
|
||||
for i in 0..50 {
|
||||
pipeline.update(&bars[i], regimes[i]);
|
||||
}
|
||||
|
||||
// Process batch
|
||||
let mut results = Vec::with_capacity(size);
|
||||
for i in 50..(50 + size) {
|
||||
let result = pipeline.extract_all(&bars[i], regimes[i]);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
black_box(results);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark 6: Comparison - Wave C (201) vs Full (225)
|
||||
// ============================================================================
|
||||
|
||||
fn bench_wave_c_vs_wave_d(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("wave_c_vs_wave_d_comparison");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let bars = generate_ohlcv_bars(200, 3001);
|
||||
let regimes = generate_regime_sequence(200, 3002);
|
||||
|
||||
// Benchmark baseline only (46 features, indices 0-45)
|
||||
group.bench_function("baseline_46_features", |b| {
|
||||
let mut pipeline = FeatureExtractionPipeline::new();
|
||||
for i in 0..100 {
|
||||
pipeline.update(&bars[i]);
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
let result = pipeline.extract(black_box(&bars[idx % bars.len()]));
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark Full pipeline (54 features, indices 0-53)
|
||||
group.bench_function("production_54_features_full", |b| {
|
||||
let mut pipeline = Full54FeaturePipeline::new();
|
||||
for i in 0..100 {
|
||||
pipeline.update(&bars[i], regimes[i]);
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
let result = pipeline.extract_all(
|
||||
black_box(&bars[idx % bars.len()]),
|
||||
black_box(regimes[idx % regimes.len()]),
|
||||
);
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark 7: Feature Group Latency Breakdown
|
||||
// ============================================================================
|
||||
|
||||
fn bench_feature_group_breakdown(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("feature_group_latency");
|
||||
group.measurement_time(Duration::from_secs(10));
|
||||
|
||||
let bars = generate_ohlcv_bars(200, 4001);
|
||||
let regimes = generate_regime_sequence(200, 4002);
|
||||
|
||||
// Pre-warm all pipelines
|
||||
let mut wave_c_pipeline = FeatureExtractionPipeline::new();
|
||||
let mut cusum = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
|
||||
let mut adx = RegimeADXFeatures::new(14);
|
||||
let mut transition = RegimeTransitionFeatures::new(4, 0.1);
|
||||
let mut adaptive = RegimeAdaptiveFeatures::new(20, 100_000.0, 14);
|
||||
|
||||
for i in 0..100 {
|
||||
wave_c_pipeline.update(&bars[i]);
|
||||
|
||||
let log_return = if i > 0 {
|
||||
(bars[i].close / bars[i - 1].close).ln()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
cusum.update(log_return);
|
||||
|
||||
let adx_bar = ADXBar {
|
||||
timestamp: bars[i].timestamp.timestamp(),
|
||||
open: bars[i].open,
|
||||
high: bars[i].high,
|
||||
low: bars[i].low,
|
||||
close: bars[i].close,
|
||||
volume: bars[i].volume,
|
||||
};
|
||||
adx.update(&adx_bar);
|
||||
transition.update(regimes[i]);
|
||||
adaptive.update(regimes[i], log_return, 50_000.0, &bars[0..=i].to_vec());
|
||||
}
|
||||
|
||||
// Benchmark CUSUM extraction (10 features)
|
||||
group.bench_function("cusum_10_features", |b| {
|
||||
let mut feat = RegimeCUSUMFeatures::new(0.0, 1.0, 0.5, 4.0);
|
||||
for i in 0..100 {
|
||||
let log_return = if i > 0 {
|
||||
(bars[i].close / bars[i - 1].close).ln()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
feat.update(log_return);
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
let log_return =
|
||||
(bars[idx % bars.len()].close / bars[(idx - 1) % bars.len()].close).ln();
|
||||
let result = feat.update(log_return);
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark ADX extraction (5 features)
|
||||
group.bench_function("adx_5_features", |b| {
|
||||
let mut feat = RegimeADXFeatures::new(14);
|
||||
for i in 0..100 {
|
||||
let adx_bar = ADXBar {
|
||||
timestamp: bars[i].timestamp.timestamp(),
|
||||
open: bars[i].open,
|
||||
high: bars[i].high,
|
||||
low: bars[i].low,
|
||||
close: bars[i].close,
|
||||
volume: bars[i].volume,
|
||||
};
|
||||
feat.update(&adx_bar);
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
let adx_bar = ADXBar {
|
||||
timestamp: bars[idx % bars.len()].timestamp.timestamp(),
|
||||
open: bars[idx % bars.len()].open,
|
||||
high: bars[idx % bars.len()].high,
|
||||
low: bars[idx % bars.len()].low,
|
||||
close: bars[idx % bars.len()].close,
|
||||
volume: bars[idx % bars.len()].volume,
|
||||
};
|
||||
let result = feat.update(&adx_bar);
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark Transition extraction (5 features)
|
||||
group.bench_function("transition_5_features", |b| {
|
||||
let mut feat = RegimeTransitionFeatures::new(4, 0.1);
|
||||
for i in 0..100 {
|
||||
feat.update(regimes[i]);
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
let result = feat.update(regimes[idx % regimes.len()]);
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
// Benchmark Adaptive extraction (4 features)
|
||||
group.bench_function("adaptive_4_features", |b| {
|
||||
let mut feat = RegimeAdaptiveFeatures::new(20, 100_000.0, 14);
|
||||
for i in 0..100 {
|
||||
let log_return = if i > 0 {
|
||||
(bars[i].close / bars[i - 1].close).ln()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
feat.update(regimes[i], log_return, 50_000.0, &bars[0..=i].to_vec());
|
||||
}
|
||||
|
||||
let mut idx = 100;
|
||||
b.iter(|| {
|
||||
let log_return =
|
||||
(bars[idx % bars.len()].close / bars[(idx - 1) % bars.len()].close).ln();
|
||||
let result = feat.update(
|
||||
regimes[idx % regimes.len()],
|
||||
log_return,
|
||||
50_000.0,
|
||||
&bars[0..=(idx % bars.len())].to_vec(),
|
||||
);
|
||||
black_box(result);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Criterion Configuration
|
||||
// ============================================================================
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_cold_start,
|
||||
bench_warm_state,
|
||||
bench_batch_processing,
|
||||
bench_memory_allocations,
|
||||
bench_throughput_scaling,
|
||||
bench_wave_c_vs_wave_d,
|
||||
bench_feature_group_breakdown,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
Reference in New Issue
Block a user