352 lines
13 KiB
Markdown
352 lines
13 KiB
Markdown
# Config Unification Plan — Single Source of Truth
|
||
|
||
> **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:** Eliminate all hardcoded config values and `use_` boolean bloat. Every training parameter flows from TOML → DQNHyperparameters → downstream configs. No Default impl should diverge from the TOML-driven path.
|
||
|
||
**Architecture:** Single config flow: `TOML file → DqnTrainingProfile::load() → DQNHyperparameters → GpuDqnTrainConfig + GpuExperienceCollectorConfig + DQNConfig`. Default impls match conservative() exactly. All `use_` booleans that are always true are removed — the features are always on.
|
||
|
||
**Tech Stack:** Rust config structs, TOML deserialization, training_profile.rs.
|
||
|
||
---
|
||
|
||
## Problem Statement
|
||
|
||
Three config structs have independent `Default` impls with conflicting values:
|
||
|
||
| Parameter | DQNConfig | GpuDqnTrainConfig | ExperienceCollectorConfig |
|
||
|-----------|-----------|-------------------|--------------------------|
|
||
| state_dim | 48 | 72 | — |
|
||
| v_min/v_max | -25/25 | -2/2 | -2/2 |
|
||
| learning_rate | 1e-4 | 3e-4 | — |
|
||
| weight_decay | 1e-4 | 1e-5 | — |
|
||
| batch_size | 64 | 256 | — |
|
||
| num_atoms | 51 | 51 | 1 |
|
||
| use_noisy_nets | true | — | false |
|
||
| use_distributional | true | — | false |
|
||
|
||
Plus 26 ExperienceCollectorConfig parameters are hardcoded with no TOML exposure.
|
||
|
||
## Design Principles
|
||
|
||
1. **TOML is the source of truth** — every tunable value has a TOML field
|
||
2. **conservative() reads TOML** — no hardcoded literals in the function
|
||
3. **Default impls match conservative()** — tests behave like production
|
||
4. **No `use_` booleans for always-on features** — if it's always true, remove the flag
|
||
5. **One config flow** — TOML → DQNHyperparameters → everything else
|
||
|
||
---
|
||
|
||
## Task 1: Clean up `use_` booleans (config.rs + dqn.rs)
|
||
|
||
**Files:**
|
||
- Modify: `crates/ml/src/trainers/dqn/config.rs` — DQNHyperparameters
|
||
- Modify: `crates/ml-dqn/src/dqn.rs` — DQNConfig
|
||
|
||
**Always-on features (remove the `use_` field, hardwire to true internally):**
|
||
- `use_double_dqn` — always true since Wave 6.4
|
||
- `use_dueling` — always true (Rainbow DQN standard)
|
||
- `use_per` — always true (memory says "PER always enabled, non-PER paths are dead code")
|
||
- `use_branching` — always true (9×3×3 action space)
|
||
- `use_distributional` — always true (C51 is the primary loss)
|
||
- `use_noisy_nets` — always true (replaces epsilon-greedy)
|
||
- `use_huber_loss` — always true (more robust than MSE)
|
||
- `use_cql` — always true (activated this session)
|
||
|
||
For each:
|
||
- [ ] **Step 1:** Remove the field from DQNHyperparameters
|
||
- [ ] **Step 2:** Remove the field from DQNConfig
|
||
- [ ] **Step 3:** In constructors that read these fields, replace `if self.use_X { ... }` with just `{ ... }`
|
||
- [ ] **Step 4:** In training_profile.rs, remove the TOML field if it exists (or ignore it)
|
||
- [ ] **Step 5:** Search for ALL references with `grep -rn "use_double_dqn\|use_dueling\|use_per\b\|use_branching\|use_distributional\|use_noisy_nets\|use_huber_loss\|use_cql\b"` and update each site
|
||
- [ ] **Step 6:** Build + test
|
||
|
||
**Keep as legitimate config flags:**
|
||
- `use_cvar_action_selection` — experimental, toggleable
|
||
- `enable_kelly_sizing` — can be disabled for ablation
|
||
- `enable_circuit_breaker` — safety feature, must be toggleable
|
||
- `enable_action_masking` — experimental
|
||
- `enable_regime_qnetwork` — experimental
|
||
- `enable_early_stopping` — must be toggleable
|
||
|
||
- [ ] **Step 7:** Commit: "refactor: remove always-on use_ booleans — 8 features are mandatory"
|
||
|
||
---
|
||
|
||
## Task 2: Unify Default impls to match conservative()
|
||
|
||
**Files:**
|
||
- Modify: `crates/ml-dqn/src/dqn.rs` — DQNConfig::default()
|
||
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` — GpuDqnTrainConfig::default()
|
||
- Modify: `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` — GpuExperienceCollectorConfig default
|
||
|
||
Every `Default` impl must produce values identical to what `conservative()` → constructor flow would produce. This ensures tests behave like production.
|
||
|
||
- [ ] **Step 1:** In DQNConfig::default():
|
||
- `v_min: -50.0, v_max: 50.0` (match conservative/reward v6)
|
||
- `learning_rate: 3e-5` (match conservative)
|
||
- `weight_decay: 1e-4` (match conservative)
|
||
- `batch_size: 64` (match smoketest, conservative)
|
||
- `num_atoms: 51`
|
||
- `state_dim: 48` (match actual feature dim with padding)
|
||
- `num_actions: 9` (match 9-action exposure space)
|
||
|
||
- [ ] **Step 2:** In GpuDqnTrainConfig::default():
|
||
- `state_dim: 72` → `state_dim: 48` (or remove default entirely — always set by constructor)
|
||
- `v_min: -50.0, v_max: 50.0`
|
||
- `lr: 3e-5` → match conservative
|
||
- `weight_decay: 1e-4`
|
||
- `batch_size: 64`
|
||
- `branch_0_size: 9` (correct — 9 exposure levels)
|
||
|
||
- [ ] **Step 3:** In GpuExperienceCollectorConfig default:
|
||
- `use_noisy_nets: true` (was false — mismatch)
|
||
- `use_distributional: true` (was false — mismatch)
|
||
- `num_atoms: 51` (was 1 — mismatch)
|
||
- `v_min: -50.0, v_max: 50.0`
|
||
- `q_clip_min: -200.0, q_clip_max: 200.0` (match reward v6 scale)
|
||
|
||
- [ ] **Step 4:** Build + all tests pass
|
||
- [ ] **Step 5:** Commit: "fix: unify Default impls to match conservative() — no more config mismatches"
|
||
|
||
---
|
||
|
||
## Task 3: Expose ExperienceCollectorConfig params in TOML
|
||
|
||
**Files:**
|
||
- Modify: `crates/ml/src/training_profile.rs` — add ExperienceSection
|
||
- Modify: `config/training/dqn-smoketest.toml` — add [experience] section
|
||
- Create: `config/training/dqn-production.toml` — add [experience] section (if not exists)
|
||
- Modify: `crates/ml/src/trainers/dqn/trainer/training_loop.rs` — read from profile
|
||
|
||
26 parameters need TOML exposure. Group them:
|
||
|
||
```toml
|
||
[experience]
|
||
gpu_n_episodes = 32 # episodes per experience collection round
|
||
gpu_timesteps_per_episode = 100 # bars per episode
|
||
initial_capital = 100000.0 # starting equity
|
||
|
||
[experience.fill_simulation]
|
||
ioc_fill_prob = 0.85
|
||
limit_fill_min = 0.30
|
||
limit_fill_max = 0.80
|
||
spread_cost_frac = 0.50
|
||
spread_capture_frac = 0.50
|
||
|
||
[experience.barriers]
|
||
profit_mult = 1.02
|
||
loss_mult = 0.98
|
||
max_bars = 500.0
|
||
scale = 0.5
|
||
|
||
[risk]
|
||
max_position = 2.0
|
||
loss_aversion = 1.5
|
||
q_clip_min = -200.0
|
||
q_clip_max = 200.0
|
||
```
|
||
|
||
- [ ] **Step 1:** Add `ExperienceSection`, `FillSimulationSection`, `BarrierSection`, `RiskSection` to training_profile.rs
|
||
- [ ] **Step 2:** Implement `apply_to()` for each section
|
||
- [ ] **Step 3:** Update dqn-smoketest.toml with [experience] section
|
||
- [ ] **Step 4:** In training_loop.rs, pass TOML values to ExperienceCollectorConfig instead of hardcoded defaults
|
||
- [ ] **Step 5:** Build + test
|
||
- [ ] **Step 6:** Commit: "feat: expose 26 experience collector params in TOML training profiles"
|
||
|
||
---
|
||
|
||
## Task 4: Wire v_range computation from reward scale
|
||
|
||
**Files:**
|
||
- Modify: `crates/ml/src/trainers/dqn/config.rs` — compute v_min/v_max from reward_scale + gamma
|
||
- Modify: `crates/ml/src/hyperopt/adapters/dqn.rs` — same computation in from_continuous()
|
||
|
||
v_min/v_max should NEVER be hardcoded. They should be computed:
|
||
```rust
|
||
let reward_scale = 10.0; // reward v6 REWARD_SCALE
|
||
let v_range = (reward_scale / (1.0 - gamma) * 1.2).clamp(20.0, 300.0);
|
||
let v_min = -v_range;
|
||
let v_max = v_range;
|
||
```
|
||
|
||
- [ ] **Step 1:** Add `reward_scale: f64` to DQNHyperparameters (default 10.0)
|
||
- [ ] **Step 2:** In conservative(), compute v_min/v_max from reward_scale + gamma
|
||
- [ ] **Step 3:** Remove hardcoded v_min/v_max from conservative()
|
||
- [ ] **Step 4:** In hyperopt adapter from_continuous(), use same formula
|
||
- [ ] **Step 5:** Add reward_scale to TOML and hyperopt search space
|
||
- [ ] **Step 6:** Build + test
|
||
- [ ] **Step 7:** Commit: "feat: v_range computed from reward_scale + gamma — no more hardcoded C51 bounds"
|
||
|
||
---
|
||
|
||
## Task 5: Consolidate DQNConfig → derive from DQNHyperparameters
|
||
|
||
**Files:**
|
||
- Modify: `crates/ml-dqn/src/dqn.rs` — DQNConfig construction
|
||
- Modify: `crates/ml/src/trainers/dqn/trainer/constructor.rs` — DQNConfig from hyperparams
|
||
|
||
DQNConfig should NOT have its own defaults. It should be constructed FROM DQNHyperparameters:
|
||
|
||
```rust
|
||
impl From<&DQNHyperparameters> for DQNConfig {
|
||
fn from(hp: &DQNHyperparameters) -> Self {
|
||
Self {
|
||
state_dim: hp.state_dim(),
|
||
num_actions: hp.num_actions(),
|
||
learning_rate: hp.learning_rate,
|
||
gamma: hp.gamma,
|
||
batch_size: hp.batch_size,
|
||
v_min: hp.v_min(),
|
||
v_max: hp.v_max(),
|
||
num_atoms: hp.num_atoms,
|
||
// ... all fields derived
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 1:** Add `From<&DQNHyperparameters> for DQNConfig`
|
||
- [ ] **Step 2:** Use this conversion in constructor.rs instead of field-by-field copy
|
||
- [ ] **Step 3:** Remove DQNConfig::default() (or make it delegate to From<conservative()>)
|
||
- [ ] **Step 4:** Build + test
|
||
- [ ] **Step 5:** Commit: "refactor: DQNConfig derived from DQNHyperparameters — single source of truth"
|
||
|
||
---
|
||
|
||
## Task 6: Consolidate GpuDqnTrainConfig → derive from DQNHyperparameters
|
||
|
||
**Files:**
|
||
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
|
||
- Modify: `crates/ml/src/trainers/dqn/fused_training.rs`
|
||
|
||
Same pattern: GpuDqnTrainConfig should be constructed FROM DQNHyperparameters, not hardcoded:
|
||
|
||
```rust
|
||
impl GpuDqnTrainConfig {
|
||
pub fn from_hyperparams(hp: &DQNHyperparameters, dqn_config: &DQNConfig) -> Self {
|
||
Self {
|
||
state_dim: dqn_config.state_dim,
|
||
num_atoms: hp.num_atoms,
|
||
v_min: hp.v_min() as f32,
|
||
v_max: hp.v_max() as f32,
|
||
lr: hp.learning_rate as f32,
|
||
weight_decay: hp.weight_decay as f32,
|
||
// ... all derived
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 1:** Add `from_hyperparams()` constructor
|
||
- [ ] **Step 2:** Use it in fused_training.rs instead of field-by-field construction
|
||
- [ ] **Step 3:** Remove or deprecate GpuDqnTrainConfig::default()
|
||
- [ ] **Step 4:** Build + test
|
||
- [ ] **Step 5:** Commit: "refactor: GpuDqnTrainConfig derived from hyperparams — no independent defaults"
|
||
|
||
---
|
||
|
||
## Task 7: Update TOML files with all parameters
|
||
|
||
**Files:**
|
||
- Modify: `config/training/dqn-smoketest.toml`
|
||
- Modify: `config/training/dqn-hyperopt.toml`
|
||
- Create or modify: `config/training/dqn-production.toml`
|
||
|
||
Each TOML file should have ALL sections:
|
||
```toml
|
||
[training]
|
||
epochs = 50
|
||
batch_size = 64
|
||
learning_rate = 0.00003
|
||
gamma = 0.95
|
||
weight_decay = 0.0001
|
||
reward_scale = 10.0
|
||
|
||
[distributional]
|
||
num_atoms = 51
|
||
# v_min/v_max computed from reward_scale + gamma
|
||
|
||
[exploration]
|
||
epsilon_start = 1.0
|
||
epsilon_end = 0.02
|
||
epsilon_decay = 0.95
|
||
noisy_sigma_init = 0.5
|
||
entropy_coefficient = 0.001
|
||
count_bonus_coefficient = 0.1
|
||
q_gap_threshold = 0.05
|
||
|
||
[experience]
|
||
gpu_n_episodes = 32
|
||
gpu_timesteps_per_episode = 100
|
||
initial_capital = 100000.0
|
||
|
||
[risk]
|
||
max_position = 2.0
|
||
loss_aversion = 1.5
|
||
kelly_fractional = 0.5
|
||
kelly_max_fraction = 0.25
|
||
|
||
[advanced]
|
||
n_steps = 3
|
||
tau = 0.005
|
||
c51_warmup_epochs = 5
|
||
her_ratio = 0.2
|
||
cql_alpha = 0.1
|
||
iqn_lambda = 0.25
|
||
spectral_norm_sigma_max = 3.0
|
||
gradient_clip_norm = 10.0
|
||
|
||
[early_stopping]
|
||
enabled = false
|
||
patience = 10
|
||
```
|
||
|
||
- [ ] **Step 1:** Create comprehensive dqn-smoketest.toml
|
||
- [ ] **Step 2:** Create comprehensive dqn-production.toml
|
||
- [ ] **Step 3:** Update dqn-hyperopt.toml with new sections
|
||
- [ ] **Step 4:** Verify all TOML fields are read by training_profile.rs
|
||
- [ ] **Step 5:** Build + test
|
||
- [ ] **Step 6:** Commit: "feat: comprehensive TOML training profiles — every parameter configurable"
|
||
|
||
---
|
||
|
||
## Task 8: Integration test — config consistency
|
||
|
||
**Files:**
|
||
- Create: `crates/ml/src/trainers/dqn/smoke_tests/config_consistency.rs`
|
||
|
||
Write a test that verifies all three config structs produce identical values when constructed through the proper flow:
|
||
|
||
```rust
|
||
#[test]
|
||
fn test_config_consistency() {
|
||
let hp = DQNHyperparameters::conservative();
|
||
let dqn_config = DQNConfig::from(&hp);
|
||
let gpu_config = GpuDqnTrainConfig::from_hyperparams(&hp, &dqn_config);
|
||
|
||
// All must agree
|
||
assert_eq!(dqn_config.num_atoms as usize, hp.num_atoms);
|
||
assert!((dqn_config.v_min - hp.v_min()).abs() < 1e-6);
|
||
assert!((gpu_config.v_min as f64 - hp.v_min()).abs() < 0.01);
|
||
assert!((gpu_config.lr as f64 - hp.learning_rate).abs() < 1e-8);
|
||
// ... all shared fields
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 1:** Write config consistency test
|
||
- [ ] **Step 2:** Test passes
|
||
- [ ] **Step 3:** Commit: "test: config consistency — all 3 structs agree on shared parameters"
|
||
|
||
---
|
||
|
||
## Execution Order
|
||
|
||
```
|
||
Task 1 (remove use_ booleans) → Task 2 (unify defaults) → Task 3 (TOML exposure)
|
||
→ Task 4 (v_range computation) → Task 5 (DQNConfig from HP)
|
||
→ Task 6 (GpuConfig from HP) → Task 7 (TOML files) → Task 8 (consistency test)
|
||
```
|
||
|
||
Each task produces a working, testable state. Total: ~8 commits.
|