TOML-based per-model training profiles replacing hardcoded hyperparameters. 8 config files (DQN/PPO/supervised × production/smoketest + hyperopt + walk-forward). 3-tier loading: env var > filesystem > embedded defaults. Full CLI coverage for all profile fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
376 lines
11 KiB
Markdown
376 lines
11 KiB
Markdown
# Training Profile Configuration System
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace hardcoded training hyperparameters with TOML configuration profiles, extending the existing GPU profile pattern to cover all trainers, hyperopt, and walk-forward settings.
|
|
|
|
**Architecture:** Per-model-type TOML profiles loaded via 3-tier resolution (env var > filesystem > embedded defaults). All TOML fields are also exposed as CLI args. A `TrainingProfile` loader in `ml` crate applies profile values to existing `DQNHyperparameters` / PPO structs with `Option<T>` fields so sparse profiles work.
|
|
|
|
**Tech Stack:** Rust, serde + toml, clap (existing CLI framework), include_str! for embedded defaults
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-03-21-training-profiles-design.md`
|
|
|
|
---
|
|
|
|
## Files to Create
|
|
|
|
| File | Responsibility |
|
|
|------|---------------|
|
|
| `config/training/dqn-production.toml` | Full production DQN config (all Rainbow features) |
|
|
| `config/training/dqn-smoketest.toml` | CI smoke test DQN (small network, 1 epoch) |
|
|
| `config/training/dqn-hyperopt.toml` | Hyperopt search space + fixed flags |
|
|
| `config/training/ppo-production.toml` | PPO production config |
|
|
| `config/training/ppo-smoketest.toml` | PPO smoke test |
|
|
| `config/training/supervised-production.toml` | Shared supervised model config |
|
|
| `config/training/supervised-smoketest.toml` | Supervised smoke test |
|
|
| `config/training/walk-forward.toml` | Walk-forward window defaults |
|
|
| `crates/ml/src/training_profile.rs` | Profile loader + section structs + apply_to() |
|
|
|
|
## Files to Delete
|
|
|
|
| File | Reason |
|
|
|------|--------|
|
|
| `config/ml/training.toml` | Replaced by per-model profiles in `config/training/` |
|
|
|
|
## Files to Modify
|
|
|
|
| File | Changes |
|
|
|------|---------|
|
|
| `crates/ml/src/lib.rs` | Add `pub mod training_profile;` |
|
|
| `crates/ml/examples/train_baseline_rl.rs` | Add `--training-profile` CLI arg, load + apply profile |
|
|
| `crates/ml/examples/train_baseline_supervised.rs` | Add `--training-profile` CLI arg |
|
|
| `crates/ml/src/trainers/dqn/smoke_tests/helpers.rs` | Load `dqn-smoketest.toml` instead of hardcoding |
|
|
| `infra/k8s/training/populate-test-data-job.yaml` | Copy `config/training/*.toml` to PVC |
|
|
| `infra/k8s/training/job-template.yaml` | Add `TRAINING_PROFILE` env var |
|
|
|
|
---
|
|
|
|
### Task 1: Create the profile loader module
|
|
|
|
**Files:**
|
|
- Create: `crates/ml/src/training_profile.rs`
|
|
|
|
- [ ] **Step 1: Define the TOML section structs**
|
|
|
|
All fields `Option<T>` — unset means "keep default":
|
|
|
|
```rust
|
|
use serde::Deserialize;
|
|
use std::sync::Arc;
|
|
use std::path::Path;
|
|
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
pub struct DqnTrainingProfile {
|
|
pub training: Option<TrainingSection>,
|
|
pub exploration: Option<ExplorationSection>,
|
|
pub replay_buffer: Option<ReplayBufferSection>,
|
|
pub distributional: Option<DistributionalSection>,
|
|
pub branching: Option<BranchingSection>,
|
|
pub advanced: Option<AdvancedSection>,
|
|
pub risk: Option<RiskSection>,
|
|
pub early_stopping: Option<EarlyStoppingSection>,
|
|
pub experience: Option<ExperienceSection>,
|
|
pub walk_forward: Option<WalkForwardSection>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
pub struct TrainingSection {
|
|
pub epochs: Option<usize>,
|
|
pub batch_size: Option<usize>,
|
|
pub learning_rate: Option<f64>,
|
|
pub gamma: Option<f64>,
|
|
pub tau: Option<f64>,
|
|
pub warmup_steps: Option<usize>,
|
|
pub gradient_clip_norm: Option<f64>,
|
|
pub weight_decay: Option<f64>,
|
|
pub max_steps_per_epoch: Option<usize>,
|
|
pub hidden_dim_base: Option<usize>,
|
|
}
|
|
|
|
// ... all other sections with Option<T> fields
|
|
```
|
|
|
|
- [ ] **Step 2: Implement the 3-tier loader**
|
|
|
|
Follow the same pattern as `crates/ml-core/src/gpu/profile.rs`:
|
|
```rust
|
|
const DQN_PRODUCTION_TOML: &str = include_str!("../../config/training/dqn-production.toml");
|
|
const DQN_SMOKETEST_TOML: &str = include_str!("../../config/training/dqn-smoketest.toml");
|
|
// ... etc
|
|
|
|
impl DqnTrainingProfile {
|
|
pub fn load(profile_name: &str) -> Self {
|
|
// 1. $FOXHUNT_TRAINING_PROFILE env var
|
|
// 2. config/training/<profile>.toml filesystem
|
|
// 3. include_str! embedded defaults
|
|
}
|
|
|
|
pub fn apply_to(&self, hp: &mut crate::trainers::dqn::DQNHyperparameters) {
|
|
// For each Some(v) field, set the corresponding hp field
|
|
if let Some(ref t) = self.training {
|
|
if let Some(v) = t.epochs { hp.epochs = v; }
|
|
if let Some(v) = t.batch_size { hp.batch_size = v; }
|
|
// ... etc
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Add unit tests**
|
|
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn test_load_dqn_production() {
|
|
let p = DqnTrainingProfile::load("dqn-production");
|
|
assert!(p.training.is_some());
|
|
let t = p.training.unwrap();
|
|
assert_eq!(t.epochs, Some(100));
|
|
}
|
|
|
|
#[test]
|
|
fn test_sparse_profile_keeps_defaults() {
|
|
// smoketest only sets a few fields
|
|
let p = DqnTrainingProfile::load("dqn-smoketest");
|
|
assert!(p.distributional.is_none()); // not set in smoketest
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_to_overwrites_only_some_fields() {
|
|
let mut hp = DQNHyperparameters::default();
|
|
let original_gamma = hp.gamma;
|
|
let p = DqnTrainingProfile::load("dqn-smoketest");
|
|
p.apply_to(&mut hp);
|
|
assert_eq!(hp.epochs, 1); // overridden by smoketest
|
|
assert_eq!(hp.gamma, original_gamma); // not in smoketest, kept default
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Register module**
|
|
|
|
Add `pub mod training_profile;` to `crates/ml/src/lib.rs`.
|
|
|
|
- [ ] **Step 5: Verify**
|
|
|
|
Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- training_profile`
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add crates/ml/src/training_profile.rs crates/ml/src/lib.rs
|
|
git commit -m "feat: training profile loader with 3-tier TOML resolution"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Create the TOML profile files
|
|
|
|
**Files:**
|
|
- Create: `config/training/dqn-production.toml`
|
|
- Create: `config/training/dqn-smoketest.toml`
|
|
- Create: `config/training/dqn-hyperopt.toml`
|
|
- Create: `config/training/ppo-production.toml`
|
|
- Create: `config/training/ppo-smoketest.toml`
|
|
- Create: `config/training/supervised-production.toml`
|
|
- Create: `config/training/supervised-smoketest.toml`
|
|
- Create: `config/training/walk-forward.toml`
|
|
|
|
- [ ] **Step 1: Create `dqn-production.toml`**
|
|
|
|
Extract current hardcoded defaults from `DQNHyperparameters::conservative()` and `smoke_params()` in helpers.rs. Every field that exists in DQNHyperparameters gets a TOML entry.
|
|
|
|
- [ ] **Step 2: Create `dqn-smoketest.toml`**
|
|
|
|
Sparse profile — only the overrides currently in `smoke_params()`:
|
|
```toml
|
|
[training]
|
|
epochs = 1
|
|
batch_size = 16
|
|
warmup_steps = 0
|
|
hidden_dim_base = 32
|
|
max_steps_per_epoch = 50
|
|
|
|
[replay_buffer]
|
|
buffer_size = 1024
|
|
min_replay_size = 32
|
|
|
|
[early_stopping]
|
|
enabled = false
|
|
|
|
[experience]
|
|
gpu_n_episodes = 2
|
|
gpu_timesteps_per_episode = 50
|
|
```
|
|
|
|
- [ ] **Step 3: Create remaining profiles**
|
|
|
|
PPO production/smoketest, supervised production/smoketest, walk-forward, hyperopt — all following the same pattern. Extract values from existing code defaults.
|
|
|
|
- [ ] **Step 4: Verify profiles parse**
|
|
|
|
Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- training_profile`
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add config/training/
|
|
git commit -m "feat: training profile TOML files for DQN, PPO, supervised, walk-forward"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Wire profiles into train_baseline_rl CLI
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/examples/train_baseline_rl.rs`
|
|
|
|
- [ ] **Step 1: Add `--training-profile` CLI arg**
|
|
|
|
```rust
|
|
#[arg(long, default_value = "dqn-production")]
|
|
training_profile: String,
|
|
```
|
|
|
|
- [ ] **Step 2: Load profile and apply to hyperparams**
|
|
|
|
After CLI arg parsing, before trainer creation:
|
|
```rust
|
|
let profile = ml::training_profile::DqnTrainingProfile::load(&args.training_profile);
|
|
profile.apply_to(&mut hyperparams);
|
|
// CLI args override profile (existing hp_f64/hp_usize/hp_bool logic stays)
|
|
```
|
|
|
|
- [ ] **Step 3: Add CLI args for ALL profile fields not yet exposed**
|
|
|
|
Check every field in `DqnTrainingProfile` sections. For any field that doesn't already have a `#[arg]` in the Args struct, add one. This ensures full CLI coverage.
|
|
|
|
- [ ] **Step 4: Verify**
|
|
|
|
Run: `SQLX_OFFLINE=true cargo check -p ml --example train_baseline_rl`
|
|
Run: `SQLX_OFFLINE=true cargo run --release --example train_baseline_rl -p ml -- --help` (verify all args appear)
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add crates/ml/examples/train_baseline_rl.rs
|
|
git commit -m "feat: --training-profile CLI arg for train_baseline_rl"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Wire profiles into train_baseline_supervised CLI
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/examples/train_baseline_supervised.rs`
|
|
|
|
- [ ] **Step 1: Add `--training-profile` CLI arg**
|
|
|
|
Same pattern as Task 3. Default to `supervised-production`.
|
|
|
|
- [ ] **Step 2: Load and apply profile**
|
|
|
|
- [ ] **Step 3: Verify + commit**
|
|
|
|
---
|
|
|
|
### Task 5: Migrate smoke test helpers to use TOML profiles
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/src/trainers/dqn/smoke_tests/helpers.rs`
|
|
|
|
- [ ] **Step 1: Replace hardcoded `smoke_params()` with profile load**
|
|
|
|
```rust
|
|
pub(super) fn smoke_params() -> DQNHyperparameters {
|
|
let mut p = DQNHyperparameters::conservative();
|
|
let profile = crate::training_profile::DqnTrainingProfile::load("dqn-smoketest");
|
|
profile.apply_to(&mut p);
|
|
// Keep production features that aren't in the TOML
|
|
p.enable_gpu_experience_collector = true;
|
|
p
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify all smoke tests still pass**
|
|
|
|
Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- smoke_tests`
|
|
Run: `SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml --lib -- smoke_tests --ignored --test-threads=1`
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add crates/ml/src/trainers/dqn/smoke_tests/helpers.rs
|
|
git commit -m "refactor: smoke test params loaded from dqn-smoketest.toml profile"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Update K8s job template + PVC populate
|
|
|
|
**Files:**
|
|
- Modify: `infra/k8s/training/job-template.yaml`
|
|
- Modify: `infra/k8s/training/populate-test-data-job.yaml`
|
|
|
|
- [ ] **Step 1: Add TRAINING_PROFILE env var to job template**
|
|
|
|
```yaml
|
|
- name: TRAINING_PROFILE
|
|
value: "dqn-production"
|
|
```
|
|
|
|
And pass as `--training-profile=$TRAINING_PROFILE` in the args.
|
|
|
|
- [ ] **Step 2: Copy training profiles to PVC in populate job**
|
|
|
|
Add to the populate job script:
|
|
```bash
|
|
# Training profiles
|
|
mkdir -p /test-data/config/training
|
|
cp /cargo-target/src/config/training/*.toml /test-data/config/training/
|
|
echo "Training profiles: $(ls /test-data/config/training/ | wc -l) files copied"
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add infra/k8s/training/
|
|
git commit -m "infra: add TRAINING_PROFILE to K8s job + copy profiles to PVC"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Profile and validate
|
|
|
|
- [ ] **Step 1: Run tests**
|
|
|
|
```bash
|
|
SQLX_OFFLINE=true cargo test -p ml --lib
|
|
SQLX_OFFLINE=true cargo test -p ml --lib -- training_profile
|
|
```
|
|
|
|
- [ ] **Step 2: Run train_baseline_rl with profile**
|
|
|
|
```bash
|
|
SQLX_OFFLINE=true cargo run --release --example train_baseline_rl -p ml -- \
|
|
--training-profile dqn-smoketest \
|
|
--data-dir test_data/futures-baseline --symbol ES.FUT \
|
|
--epochs 2 --max-steps-per-epoch 20 \
|
|
--train-months 3 --val-months 1 --test-months 1 --step-months 3
|
|
```
|
|
|
|
- [ ] **Step 3: Verify --help shows all args**
|
|
|
|
```bash
|
|
SQLX_OFFLINE=true cargo run --release --example train_baseline_rl -p ml -- --help
|
|
```
|
|
|
|
- [ ] **Step 4: Commit + push**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat: training profile system — TOML configs for all trainers"
|
|
git push origin main
|
|
```
|