diff --git a/docs/plans/2026-02-25-unified-training-plan.md b/docs/plans/2026-02-25-unified-training-plan.md new file mode 100644 index 000000000..dbc30c8dd --- /dev/null +++ b/docs/plans/2026-02-25-unified-training-plan.md @@ -0,0 +1,953 @@ +# Unified Training Binaries Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Consolidate 21 training example binaries into 2 unified binaries (`train_baseline_rl`, `train_baseline_supervised`), delete the rest, and update infra. + +**Architecture:** Rename existing `train_baseline.rs` to `train_baseline_rl.rs` for DQN/PPO RL training. Create `train_baseline_supervised.rs` that uses a model factory + generic `UnifiedTrainable` training loop for the 8 supervised models. Both share `baseline_common/mod.rs` for data loading. + +**Tech Stack:** Rust, Candle, `UnifiedTrainable` trait, walk-forward windows, DBN data, clap CLI + +--- + +### Task 1: Rename `train_baseline` → `train_baseline_rl` + +**Files:** +- Rename: `crates/ml/examples/train_baseline.rs` → `crates/ml/examples/train_baseline_rl.rs` +- Modify: `crates/ml/Cargo.toml` (line ~208) + +**Step 1: Copy the file to the new name** + +```bash +cp crates/ml/examples/train_baseline.rs crates/ml/examples/train_baseline_rl.rs +``` + +**Step 2: Update the `#[command]` name in the new file** + +In `crates/ml/examples/train_baseline_rl.rs`, change line 49: + +```rust +// Before: +#[command(name = "train_baseline", about = "Train DQN/PPO with walk-forward windows")] +// After: +#[command(name = "train_baseline_rl", about = "Train DQN/PPO with walk-forward RL windows")] +``` + +**Step 3: Update the doc comment** + +Change line 1: + +```rust +// Before: +//! Walk-forward training binary for DQN and PPO models. +// After: +//! Walk-forward RL training binary for DQN and PPO models. +``` + +**Step 4: Update `Cargo.toml` explicit example entry** + +In `crates/ml/Cargo.toml`, change the `[[example]]` entry (lines 207-209): + +```toml +# Before: +[[example]] +name = "train_baseline" +path = "examples/train_baseline.rs" + +# After: +[[example]] +name = "train_baseline_rl" +path = "examples/train_baseline_rl.rs" +``` + +**Step 5: Delete the old file** + +```bash +rm crates/ml/examples/train_baseline.rs +``` + +**Step 6: Verify compilation** + +```bash +SQLX_OFFLINE=true cargo check -p ml --example train_baseline_rl 2>&1 | tail -5 +``` + +Expected: compiles with 0 errors. + +**Step 7: Commit** + +```bash +git add crates/ml/examples/train_baseline_rl.rs crates/ml/examples/train_baseline.rs crates/ml/Cargo.toml +git commit -m "refactor(ml): rename train_baseline → train_baseline_rl" +``` + +--- + +### Task 2: Create `train_baseline_supervised` — CLI + model factory + +**Files:** +- Create: `crates/ml/examples/train_baseline_supervised.rs` + +This is the main new file. It has 4 sections: CLI args, model factory, training loop, and main. + +**Step 1: Create the file with CLI args and model enum** + +Create `crates/ml/examples/train_baseline_supervised.rs` with the following content: + +```rust +//! Walk-forward supervised training binary for all non-RL models. +//! +//! Trains models using the `UnifiedTrainable` trait on real OHLCV data loaded +//! from Databento DBN files. Supports walk-forward windows, early stopping, +//! checkpoint saving, and z-score normalization. +//! +//! # Supported Models +//! +//! tft, mamba2, liquid, tggn, tlob, kan, xlstm, diffusion +//! +//! # Usage +//! +//! ```bash +//! SQLX_OFFLINE=true cargo run -p ml --example train_baseline_supervised --release -- \ +//! --model kan --epochs 50 --batch-size 128 \ +//! --data-dir data/cache/futures-baseline \ +//! --output-dir ml/trained_models +//! ``` + +#![allow(unused_crate_dependencies, clippy::cognitive_complexity)] +#![deny( + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::indexing_slicing +)] + +use std::path::{Path, PathBuf}; +use std::time::Instant; + +use anyhow::{Context, Result}; +use candle_core::{Device, Tensor}; +use clap::Parser; +use tracing::{error, info, warn}; + +use ml::features::extraction::extract_ml_features; +use ml::training::unified_trainer::UnifiedTrainable; +use ml::types::OHLCVBar; +use ml::walk_forward::{generate_walk_forward_windows, NormStats, WalkForwardConfig}; + +#[allow(unreachable_pub)] +mod baseline_common; +use baseline_common::{load_all_bars, spread_cost_bps}; + +// Model adapter imports +use ml::tft::TFTConfig; +use ml::tft::trainable_adapter::TrainableTFT; +use ml::mamba::{Mamba2Config, Mamba2SSM}; +use ml::liquid::candle_cfc::CfCTrainConfig; +use ml::liquid::adapter::LiquidTrainableAdapter; +use ml::tgnn::{TGGNConfig}; +use ml::tgnn::trainable_adapter::TGGNTrainableAdapter; +use ml::tlob::trainable_adapter::{TLOBAdapterConfig, TLOBTrainableAdapter}; +use ml::kan::{KANConfig, KANTrainableAdapter}; +use ml::xlstm::config::XLSTMConfig; +use ml::xlstm::trainable::XLSTMTrainableAdapter; +use ml::diffusion::config::DiffusionConfig; +use ml::diffusion::trainable::DiffusionTrainableAdapter; +use ml::gpu::DeviceConfig; + +// --------------------------------------------------------------------------- +// CLI Arguments +// --------------------------------------------------------------------------- + +/// Walk-forward supervised training binary for non-RL models. +#[derive(Parser, Debug)] +#[command(name = "train_baseline_supervised", about = "Train supervised models with walk-forward windows")] +struct Args { + /// Model to train: tft, mamba2, liquid, tggn, tlob, kan, xlstm, diffusion, all + #[arg(long)] + model: String, + + /// Path to directory containing .dbn.zst files (with symbol subdirectories) + #[arg(long, default_value = "data/cache/futures-baseline")] + data_dir: PathBuf, + + /// Symbol subdirectory to load (e.g. "ES.FUT", "NQ.FUT") + #[arg(long, default_value = "ES.FUT")] + symbol: String, + + /// Maximum training epochs per fold + #[arg(long, default_value_t = 50)] + epochs: usize, + + /// Training batch size + #[arg(long, default_value_t = 128)] + batch_size: usize, + + /// Learning rate + #[arg(long, default_value_t = 1e-3)] + learning_rate: f64, + + /// Feature dimension (must match extract_ml_features output) + #[arg(long, default_value_t = 51)] + feature_dim: usize, + + /// Max training steps per epoch (0 = use all bars) + #[arg(long, default_value_t = 2000)] + max_steps_per_epoch: usize, + + /// Output directory for trained model checkpoints + #[arg(long, default_value = "ml/trained_models")] + output_dir: PathBuf, + + /// Early stopping patience (epochs without improvement) + #[arg(long, default_value_t = 10)] + patience: usize, + + /// Walk-forward: initial training window in months + #[arg(long, default_value_t = 12)] + train_months: u32, + + /// Walk-forward: validation window in months + #[arg(long, default_value_t = 3)] + val_months: u32, + + /// Walk-forward: test window in months + #[arg(long, default_value_t = 3)] + test_months: u32, + + /// Walk-forward: step size in months between folds + #[arg(long, default_value_t = 3)] + step_months: u32, + + /// Maximum absolute per-bar return; larger moves are clamped + #[arg(long, default_value_t = 0.01)] + max_bar_return: f64, + + /// Round-trip commission cost in basis points + #[arg(long, default_value_t = 1.0)] + tx_cost_bps: f64, + + /// Instrument tick size in price units (ES=0.25) + #[arg(long, default_value_t = 0.25)] + tick_size: f64, + + /// Typical bid-ask spread in ticks + #[arg(long, default_value_t = 1.0)] + spread_ticks: f64, +} + +// --------------------------------------------------------------------------- +// Supported models +// --------------------------------------------------------------------------- + +const ALL_MODELS: &[&str] = &["tft", "mamba2", "liquid", "tggn", "tlob", "kan", "xlstm", "diffusion"]; + +fn validate_model(name: &str) -> Result<()> { + if name == "all" || ALL_MODELS.contains(&name) { + Ok(()) + } else { + anyhow::bail!( + "Unknown model '{}'. Valid: {} or 'all'", + name, + ALL_MODELS.join(", ") + ); + } +} + +// --------------------------------------------------------------------------- +// Model Factory +// --------------------------------------------------------------------------- + +/// Create a model adapter by name with production-default configs. +/// +/// All OHLCV models receive `feature_dim` as input dimension. +/// The adapter's internal projection layers handle mapping to model-native dims. +fn create_model( + name: &str, + feature_dim: usize, + learning_rate: f64, + device: &Device, +) -> Result> { + match name { + "tft" => { + let config = TFTConfig { + num_static_features: 0, + num_time_varying_features: feature_dim, + hidden_size: 128, + num_attention_heads: 4, + num_quantiles: 3, + dropout_rate: 0.1, + num_encoder_steps: 60, + ..TFTConfig::default() + }; + let mut adapter = TrainableTFT::new(config) + .map_err(|e| anyhow::anyhow!("Failed to create TFT: {}", e))?; + adapter.set_learning_rate(learning_rate) + .map_err(|e| anyhow::anyhow!("Failed to set TFT learning rate: {}", e))?; + Ok(Box::new(adapter)) + } + "mamba2" => { + let config = Mamba2Config { + d_model: 128, + n_layers: 4, + state_size: 16, + seq_len: 60, + d_inner: 256, + ..Mamba2Config::default() + }; + let mut adapter = Mamba2SSM::new(config, device) + .map_err(|e| anyhow::anyhow!("Failed to create Mamba2: {}", e))?; + adapter.set_learning_rate(learning_rate) + .map_err(|e| anyhow::anyhow!("Failed to set Mamba2 learning rate: {}", e))?; + Ok(Box::new(adapter)) + } + "liquid" => { + let config = CfCTrainConfig { + input_size: feature_dim, + hidden_size: 128, + output_size: 1, + backbone_hidden_sizes: vec![128, 64], + learning_rate, + device: DeviceConfig::from_device(device), + ..CfCTrainConfig::default() + }; + let adapter = LiquidTrainableAdapter::new(config) + .map_err(|e| anyhow::anyhow!("Failed to create Liquid: {}", e))?; + Ok(Box::new(adapter)) + } + "tggn" => { + let config = TGGNConfig { + node_dim: feature_dim, + hidden_dim: 32, + num_layers: 2, + max_nodes: 64, + max_edges: 128, + edge_dim: 4, + temporal_decay: 0.99, + update_frequency_ns: 1_000_000, + use_simd: false, + }; + let mut adapter = TGGNTrainableAdapter::new(config, device) + .map_err(|e| anyhow::anyhow!("Failed to create TGGN: {}", e))?; + adapter.set_learning_rate(learning_rate) + .map_err(|e| anyhow::anyhow!("Failed to set TGGN learning rate: {}", e))?; + Ok(Box::new(adapter)) + } + "tlob" => { + let config = TLOBAdapterConfig { + d_model: 128, + num_heads: 4, + num_layers: 2, + seq_len: 1, + feature_dim, + }; + let mut adapter = TLOBTrainableAdapter::new(config, device) + .map_err(|e| anyhow::anyhow!("Failed to create TLOB: {}", e))?; + adapter.set_learning_rate(learning_rate) + .map_err(|e| anyhow::anyhow!("Failed to set TLOB learning rate: {}", e))?; + Ok(Box::new(adapter)) + } + "kan" => { + let config = KANConfig { + grid_size: 5, + spline_order: 4, + layer_widths: vec![feature_dim, 32, 16, 1], + learning_rate, + weight_decay: 1e-4, + grad_clip: 1.0, + }; + let adapter = KANTrainableAdapter::new(config, device) + .map_err(|e| anyhow::anyhow!("Failed to create KAN: {}", e))?; + Ok(Box::new(adapter)) + } + "xlstm" => { + let config = XLSTMConfig { + input_dim: feature_dim, + hidden_dim: 128, + ..XLSTMConfig::default() + }; + let mut adapter = XLSTMTrainableAdapter::new(config, device) + .map_err(|e| anyhow::anyhow!("Failed to create xLSTM: {}", e))?; + adapter.set_learning_rate(learning_rate) + .map_err(|e| anyhow::anyhow!("Failed to set xLSTM learning rate: {}", e))?; + Ok(Box::new(adapter)) + } + "diffusion" => { + let config = DiffusionConfig { + input_dim: feature_dim, + hidden_dim: 128, + ..DiffusionConfig::default() + }; + let adapter = DiffusionTrainableAdapter::new(config, device.clone()) + .map_err(|e| anyhow::anyhow!("Failed to create Diffusion: {}", e))?; + Ok(Box::new(adapter)) + } + _ => anyhow::bail!("Unknown model: {}", name), + } +} + +// --------------------------------------------------------------------------- +// Data preparation (reused from KAN example pattern) +// --------------------------------------------------------------------------- + +/// Build (input, target) tensor pairs from OHLCV bars for regression training. +/// +/// Features are extracted via `extract_ml_features` (51-dim), z-score normalized, +/// and the target is the next-bar return in basis points, clipped. +fn prepare_fold_data( + train_bars: &[OHLCVBar], + val_bars: &[OHLCVBar], + args: &Args, + device: &Device, +) -> Result<(Vec<(Tensor, Tensor)>, Vec<(Tensor, Tensor)>)> { + let train_features = extract_ml_features(train_bars) + .context("Failed to extract training features")?; + let val_features = extract_ml_features(val_bars) + .context("Failed to extract validation features")?; + + if train_features.len() < 2 { + anyhow::bail!("Insufficient training features: {}", train_features.len()); + } + if val_features.len() < 2 { + anyhow::bail!("Insufficient validation features: {}", val_features.len()); + } + + let norm_stats = NormStats::from_features(&train_features); + let norm_train = norm_stats.normalize_batch(&train_features); + let norm_val = norm_stats.normalize_batch(&val_features); + + let train_bar_offset = train_bars.len().saturating_sub(train_features.len()); + let val_bar_offset = val_bars.len().saturating_sub(val_features.len()); + + let train_pairs = build_tensor_pairs(&norm_train, train_bars, train_bar_offset, args, device)?; + let val_pairs = build_tensor_pairs(&norm_val, val_bars, val_bar_offset, args, device)?; + + Ok((train_pairs, val_pairs)) +} + +/// Convert normalized features + bars into (input, target) tensor pairs. +fn build_tensor_pairs( + norm_features: &[[f64; 51]], + bars: &[OHLCVBar], + bar_offset: usize, + args: &Args, + device: &Device, +) -> Result> { + let mut pairs = Vec::new(); + let n = norm_features.len(); + let limit = n.saturating_sub(1); + let step_limit = if args.max_steps_per_epoch > 0 { + args.max_steps_per_epoch.min(limit) + } else { + limit + }; + + for i in 0..step_limit { + let Some(feat) = norm_features.get(i) else { continue }; + + let bar_idx = i + bar_offset; + let close_cur = bars.get(bar_idx).map(|b| b.close).unwrap_or(0.0); + let close_next = bars.get(bar_idx + 1).map(|b| b.close).unwrap_or(close_cur); + + let return_bps = if close_cur.abs() > 1e-10 { + (close_next - close_cur) / close_cur * 10_000.0 + } else { + 0.0 + }; + let max_bps = args.max_bar_return * 10_000.0; + let clipped = return_bps.clamp(-max_bps, max_bps); + let spread = spread_cost_bps(close_cur, args.tick_size, args.spread_ticks); + let net_return = clipped - (args.tx_cost_bps + spread); + + let input_f32: Vec = feat.iter().map(|&v| v as f32).collect(); + let target_f32 = vec![net_return as f32]; + + let input_tensor = Tensor::from_vec(input_f32, &[1, args.feature_dim], device) + .context("Failed to create input tensor")?; + let target_tensor = Tensor::from_vec(target_f32, &[1, 1], device) + .context("Failed to create target tensor")?; + + pairs.push((input_tensor, target_tensor)); + } + + Ok(pairs) +} + +// --------------------------------------------------------------------------- +// Generic training loop +// --------------------------------------------------------------------------- + +/// Run one training epoch on mini-batches. Returns average loss. +fn run_training_epoch( + adapter: &mut dyn UnifiedTrainable, + train_pairs: &[(Tensor, Tensor)], + batch_size: usize, +) -> Result { + let mut epoch_loss_sum = 0.0_f64; + let mut epoch_steps = 0_usize; + let n_train = train_pairs.len(); + let mut batch_start = 0_usize; + + while batch_start < n_train { + let batch_end = (batch_start + batch_size).min(n_train); + let Some(batch_slice) = train_pairs.get(batch_start..batch_end) else { break }; + + let batch_inputs: Vec<&Tensor> = batch_slice.iter().map(|(inp, _)| inp).collect(); + let batch_targets: Vec<&Tensor> = batch_slice.iter().map(|(_, tgt)| tgt).collect(); + + if batch_inputs.is_empty() { + batch_start = batch_end; + continue; + } + + let input_cat = Tensor::cat(&batch_inputs, 0) + .context("Failed to concatenate batch inputs")?; + let target_cat = Tensor::cat(&batch_targets, 0) + .context("Failed to concatenate batch targets")?; + + adapter.zero_grad() + .map_err(|e| anyhow::anyhow!("zero_grad failed: {}", e))?; + let predictions = adapter.forward(&input_cat) + .map_err(|e| anyhow::anyhow!("forward failed: {}", e))?; + let loss = adapter.compute_loss(&predictions, &target_cat) + .map_err(|e| anyhow::anyhow!("compute_loss failed: {}", e))?; + let loss_val = loss.to_scalar::() + .map_err(|e| anyhow::anyhow!("loss to_scalar failed: {}", e))?; + + adapter.backward(&loss) + .map_err(|e| anyhow::anyhow!("backward failed: {}", e))?; + adapter.optimizer_step() + .map_err(|e| anyhow::anyhow!("optimizer_step failed: {}", e))?; + + epoch_loss_sum += loss_val as f64; + epoch_steps += 1; + batch_start = batch_end; + } + + Ok(if epoch_steps > 0 { epoch_loss_sum / epoch_steps as f64 } else { 0.0 }) +} + +/// Train a model on a single walk-forward fold. Returns best validation loss. +fn train_fold( + model_name: &str, + fold: usize, + train_pairs: &[(Tensor, Tensor)], + val_pairs: &[(Tensor, Tensor)], + args: &Args, + device: &Device, + output_dir: &Path, +) -> Result { + info!("[{}] Fold {} -- {} train, {} val pairs", model_name, fold, train_pairs.len(), val_pairs.len()); + + let mut adapter = create_model(model_name, args.feature_dim, args.learning_rate, device)?; + + let mut best_val_loss = f64::MAX; + let mut epochs_without_improvement = 0_usize; + + for epoch in 0..args.epochs { + let epoch_start = Instant::now(); + + let avg_train_loss = run_training_epoch(adapter.as_mut(), train_pairs, args.batch_size)?; + let val_loss = adapter.validate(val_pairs) + .map_err(|e| anyhow::anyhow!("validation failed: {}", e))?; + let elapsed = epoch_start.elapsed(); + + info!( + " Fold {} Epoch {}/{}: train_loss={:.6}, val_loss={:.6}, lr={:.2e}, time={:.1}s", + fold, epoch + 1, args.epochs, avg_train_loss, val_loss, + adapter.get_learning_rate(), elapsed.as_secs_f64(), + ); + + if val_loss < best_val_loss { + best_val_loss = val_loss; + epochs_without_improvement = 0; + let ckpt_path = output_dir.join(format!("{}_fold{}_best", model_name, fold)); + let ckpt_str = ckpt_path.to_str().unwrap_or("checkpoint"); + if let Err(e) = adapter.save_checkpoint(ckpt_str) { + warn!(" Failed to save checkpoint: {}", e); + } else { + info!(" [{}] New best val_loss={:.6}, checkpoint saved", model_name, val_loss); + } + } else { + epochs_without_improvement += 1; + } + + if epochs_without_improvement >= args.patience { + info!(" [{}] Early stopping at epoch {} (patience {} exhausted)", model_name, epoch + 1, args.patience); + break; + } + } + + // Final checkpoint + let final_path = output_dir.join(format!("{}_fold{}_final", model_name, fold)); + let final_str = final_path.to_str().unwrap_or("final"); + if let Err(e) = adapter.save_checkpoint(final_str) { + warn!(" Failed to save final checkpoint: {}", e); + } + + Ok(best_val_loss) +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let args = Args::parse(); + validate_model(&args.model)?; + + let models_to_train: Vec<&str> = if args.model == "all" { + ALL_MODELS.to_vec() + } else { + vec![args.model.as_str()] + }; + + // Device selection + let device = match Device::cuda_if_available(0) { + Ok(d) => { info!("Using CUDA device"); d } + Err(_) => { info!("CUDA unavailable, using CPU"); Device::Cpu } + }; + + // Load OHLCV bars + info!("Loading OHLCV bars for {} from {}", args.symbol, args.data_dir.display()); + let all_bars = load_all_bars(&args.data_dir, &args.symbol)?; + info!("Loaded {} bars", all_bars.len()); + + if all_bars.len() < 100 { + anyhow::bail!("Insufficient data: {} bars (need >= 100)", all_bars.len()); + } + + // Walk-forward windows + let wf_config = WalkForwardConfig { + initial_train_months: args.train_months, + val_months: args.val_months, + test_months: args.test_months, + step_months: args.step_months, + }; + let windows = generate_walk_forward_windows(&all_bars, &wf_config); + if windows.is_empty() { + anyhow::bail!("No walk-forward windows generated. Data too short for configured window sizes."); + } + info!("Generated {} walk-forward folds", windows.len()); + + // Train each model + for model_name in &models_to_train { + info!("=== Training model: {} ===", model_name); + let model_output = args.output_dir.join(model_name); + std::fs::create_dir_all(&model_output) + .with_context(|| format!("Failed to create {}", model_output.display()))?; + + let mut fold_results: Vec<(usize, f64)> = Vec::new(); + + for window in &windows { + let fold = window.fold; + info!("--- Fold {}: train={}, val={}, test={} bars ---", + fold, window.train.len(), window.val.len(), window.test.len()); + + let (train_pairs, val_pairs) = match prepare_fold_data( + &window.train, &window.val, &args, &device, + ) { + Ok(data) => data, + Err(e) => { + warn!("Skipping fold {} -- {}", fold, e); + continue; + } + }; + + if train_pairs.is_empty() || val_pairs.is_empty() { + warn!("Skipping fold {} -- empty data after feature extraction", fold); + continue; + } + + match train_fold(model_name, fold, &train_pairs, &val_pairs, &args, &device, &model_output) { + Ok(best_val) => fold_results.push((fold, best_val)), + Err(e) => error!("[{}] Fold {} failed: {}", model_name, fold, e), + } + } + + // Summary + info!("=== {} Results ({} folds) ===", model_name, fold_results.len()); + for (fold, loss) in &fold_results { + info!(" Fold {}: best_val_loss = {:.6}", fold, loss); + } + if !fold_results.is_empty() { + let avg: f64 = fold_results.iter().map(|(_, l)| l).sum::() / fold_results.len() as f64; + info!(" Average: {:.6}", avg); + } + info!(" Checkpoints: {}", model_output.display()); + } + + Ok(()) +} +``` + +**Step 2: Verify compilation** + +```bash +SQLX_OFFLINE=true cargo check -p ml --example train_baseline_supervised 2>&1 | tail -20 +``` + +Expected: compilation errors from import paths that need adjustment. This is expected — the import paths above are best-effort based on research. The implementer MUST fix any import errors by checking the actual module paths in `crates/ml/src/lib.rs` and each model's `mod.rs`. + +Common fixes needed: +- `ml::tft::trainable_adapter::TrainableTFT` — may need to be `ml::tft::TrainableTFT` if re-exported +- `ml::liquid::adapter::LiquidTrainableAdapter` — may need to be `ml::liquid::LiquidTrainableAdapter` +- `ml::gpu::DeviceConfig` — check if `from_device()` method exists, may need different construction +- `Device::cuda_if_available(0)` — actually returns `Result`, handle properly +- `device.clone()` for DiffusionTrainableAdapter — Device may not implement Clone, use `Device::Cpu` or re-create + +Fix ALL compilation errors iteratively until `cargo check` passes clean. + +**Step 3: Commit** + +```bash +git add crates/ml/examples/train_baseline_supervised.rs +git commit -m "feat(ml): add train_baseline_supervised for 8 UnifiedTrainable models" +``` + +--- + +### Task 3: Add `[[example]]` entry to Cargo.toml + +**Files:** +- Modify: `crates/ml/Cargo.toml` + +**Step 1: Add the example entry** + +After the `train_baseline_rl` entry, add: + +```toml +[[example]] +name = "train_baseline_supervised" +path = "examples/train_baseline_supervised.rs" +``` + +**Step 2: Verify both examples compile** + +```bash +SQLX_OFFLINE=true cargo check -p ml --example train_baseline_rl --example train_baseline_supervised 2>&1 | tail -5 +``` + +**Step 3: Commit** + +```bash +git add crates/ml/Cargo.toml +git commit -m "build(ml): add train_baseline_supervised example entry" +``` + +--- + +### Task 4: Delete 20 old training examples + +**Files:** +- Delete 10 unreferenced examples +- Delete 10 production examples (now consolidated) + +**Step 1: Delete all 20 files** + +```bash +# 10 unreferenced (dead code) +rm crates/ml/examples/train_ppo_es_fut.rs +rm crates/ml/examples/train_tft.rs +rm crates/ml/examples/train_dqn.rs +rm crates/ml/examples/train_dqn_production.rs +rm crates/ml/examples/train_ppo_extended.rs +rm crates/ml/examples/train_rainbow.rs +rm crates/ml/examples/train_tft_parquet.rs +rm crates/ml/examples/train_tft_qat.rs +rm crates/ml/examples/train_mamba2_parquet.rs +rm crates/ml/examples/train_continuous_ppo_parquet.rs + +# 10 production examples (consolidated into baselines) +rm crates/ml/examples/train_dqn_es_fut.rs +rm crates/ml/examples/train_ppo_parquet.rs +rm crates/ml/examples/train_tft_dbn.rs +rm crates/ml/examples/train_mamba2_dbn.rs +rm crates/ml/examples/train_liquid_dbn.rs +rm crates/ml/examples/train_tggn_dbn.rs +rm crates/ml/examples/train_tlob.rs +rm crates/ml/examples/train_kan_dbn.rs +rm crates/ml/examples/train_xlstm_dbn.rs +rm crates/ml/examples/train_diffusion_dbn.rs +``` + +**Step 2: Verify remaining examples compile** + +```bash +SQLX_OFFLINE=true cargo check -p ml --example train_baseline_rl --example train_baseline_supervised --example evaluate_baseline 2>&1 | tail -5 +``` + +**Step 3: Commit** + +```bash +git add -u crates/ml/examples/ +git commit -m "refactor(ml): delete 20 training examples consolidated into baselines + +Removed 10 unreferenced examples (dead code) and 10 production +examples now handled by train_baseline_rl and train_baseline_supervised." +``` + +--- + +### Task 5: Update `Dockerfile.training` + +**Files:** +- Modify: `infra/docker/Dockerfile.training` (lines 62-83) + +**Step 1: Replace the build command** + +Replace the `cargo build` block (lines 62-83) with: + +```dockerfile +# Build training binaries with CUDA support +RUN cargo build --release -p ml --features ml/cuda \ + --example train_baseline_rl \ + --example train_baseline_supervised \ + --example evaluate_baseline \ + --example hyperopt_dqn_demo \ + --example hyperopt_ppo_demo \ + --example hyperopt_tft_demo \ + --example hyperopt_mamba2_demo \ + && mkdir -p /build/out \ + && for bin in train_baseline_rl train_baseline_supervised evaluate_baseline hyperopt_dqn_demo hyperopt_ppo_demo hyperopt_tft_demo hyperopt_mamba2_demo; do \ + cp target/release/examples/${bin} /build/out/ && strip /build/out/${bin}; \ + done +``` + +**Step 2: Update the usage comment at the top** + +Change line 3: + +```dockerfile +# Run: docker run --gpus all foxhunt-training train_baseline_supervised --model kan [args...] +``` + +**Step 3: Commit** + +```bash +git add infra/docker/Dockerfile.training +git commit -m "build(docker): update training image for unified baselines" +``` + +--- + +### Task 6: Update `train.sh` + +**Files:** +- Modify: `infra/scripts/train.sh` (lines 36-47, 128-142) + +**Step 1: Update `MODEL_BINARY` map** + +Replace lines 36-47: + +```bash +declare -A MODEL_BINARY=( + [dqn]=train_baseline_rl + [ppo]=train_baseline_rl + [tft]=train_baseline_supervised + [mamba2]=train_baseline_supervised + [tggn]=train_baseline_supervised + [tlob]=train_baseline_supervised + [liquid]=train_baseline_supervised + [kan]=train_baseline_supervised + [xlstm]=train_baseline_supervised + [diffusion]=train_baseline_supervised +) +``` + +**Step 2: Update `build_args()` to pass `--model` flag** + +Replace the `build_args()` function (lines ~127-142): + +```bash +build_args() { + local model="$1" + local binary="${MODEL_BINARY[$model]}" + local args=("$binary") + + # Both binaries accept --model + args+=("--model" "$model") + args+=("--symbol" "$SYMBOL") + args+=("--max-steps-per-epoch" "$MAX_STEPS") + args+=("--data-dir" "$DATA_DIR") + args+=("--output-dir" "/output") + + if [[ "$PRESET" == "hyperopt" ]]; then + args+=("--trials" "$TRIALS") + fi + + echo "${args[*]}" +} +``` + +Note: `train_baseline_rl` uses `--model dqn|ppo|both`, so passing `--model dqn` works correctly. + +**Step 3: Commit** + +```bash +git add infra/scripts/train.sh +git commit -m "infra(train.sh): route all models through unified baselines" +``` + +--- + +### Task 7: Update `job-template.yaml` + +**Files:** +- Modify: `infra/k8s/training/job-template.yaml` (lines 33, 46) + +**Step 1: Update the comment and default value** + +Line 33 comment: + +```yaml + # train_baseline_rl (for dqn, ppo) + # train_baseline_supervised (for tft, mamba2, tggn, tlob, liquid, kan, xlstm, diffusion) +``` + +Line 46 default value: + +```yaml + value: train_baseline_supervised +``` + +**Step 2: Commit** + +```bash +git add infra/k8s/training/job-template.yaml +git commit -m "infra(k8s): update job template for unified training binaries" +``` + +--- + +### Task 8: Final verification + +**Step 1: Check that all remaining examples compile** + +```bash +SQLX_OFFLINE=true cargo check -p ml --examples 2>&1 | tail -10 +``` + +Expected: 0 errors. All remaining examples (train_baseline_rl, train_baseline_supervised, evaluate_baseline, cuda_test, gpu_training_benchmark, evaluate_ppo, hyperopt_*) should compile. + +**Step 2: Verify no stale references to deleted binaries** + +```bash +# Search for any remaining references to old binary names +grep -r "train_dqn_es_fut\|train_ppo_parquet\|train_tft_dbn\|train_mamba2_dbn\|train_liquid_dbn\|train_tggn_dbn\|train_kan_dbn\|train_xlstm_dbn\|train_diffusion_dbn" \ + --include="*.rs" --include="*.yaml" --include="*.yml" --include="*.sh" --include="*.toml" --include="Dockerfile*" \ + crates/ services/ infra/ bin/ .gitlab-ci.yml 2>/dev/null || echo "Clean — no stale references" +``` + +Expected: "Clean — no stale references" (docs/plans/ may still reference old names, that's fine — they're historical). + +**Step 3: Commit any remaining fixes** + +If stale references found, fix them and commit.