# Bug Fixes Implementation Plan (Spec B) > **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:** Fix all 9 CRITICAL bugs and top-priority IMPORTANT issues found by the 9-agent audit, eliminating wrong training results and silent error suppression. **Architecture:** Config-only fixes first (lowest risk), then Rust refactors, then CUDA kernel changes last. Each task is independently testable and committable. Order follows the spec's Phase 1 → Phase 2 risk-based sequencing. **Tech Stack:** Rust, CUDA (.cu kernels compiled by build.rs), cudarc 0.19.3, serde **Spec:** `docs/superpowers/specs/2026-03-27-bug-fixes-design.md` --- ## File Map | File | Bugs | Action | |---|---|---| | `crates/ml/src/trainers/dqn/config.rs` | #4, #5 | Make gradient_clip_norm and entropy_coefficient non-optional | | `crates/ml/src/trainers/dqn/fused_training.rs` | #4 | Remove unwrap_or on gradient_clip_norm | | `crates/ml/src/trainers/dqn/trainer/constructor.rs` | #4, #5 | Remove unwrap_or fallbacks | | `crates/ml/src/trainers/dqn/data_loading.rs` | #3 | Replace unwrap_or(1.0) with direct indexing | | `crates/ml-dqn/src/network.rs` | #8 | Conditional step_count increment | | `crates/ml-dqn/src/quantile_regression.rs` | #6, #7 | Single CUDA context, remove silent fallbacks | | `crates/ml/src/cuda_pipeline/backtest_env_kernel.cu` | #1, #2 | Complete episode reset, update max_equity | | `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` | #10 | Export budget fraction constants | | `crates/ml-dqn/src/branching.rs` | #16 | Remove MaybeNoisyLinear enum | | `crates/ml/src/trainers/dqn/trainer/action.rs` | #15 | Replace unwrap_or(2) with expect | | `crates/ml/src/trainers/dqn/trainer/state.rs` | #15 | Replace unwrap_or(0.0) with expect | | `crates/ml/src/cuda_pipeline/gpu_action_selector.rs` | #9 | Add NoisyNet architecture comment | --- ### Task 1: Unify gradient_clip_norm — eliminate inconsistent defaults Bug #4: Main trainer clips at 10.0, IQN/IQL clip at 1.0. The fix: resolve `gradient_clip_norm` once at the top of `FusedTrainingCtx::new()` and pass the resolved value to ALL sub-configs. **Files:** - Modify: `crates/ml/src/trainers/dqn/fused_training.rs` - [ ] **Step 1: Resolve gradient_clip_norm once at top of constructor** At the top of `FusedTrainingCtx::new()` (before any config construction), add: ```rust let resolved_grad_norm = hyperparams.gradient_clip_norm.unwrap_or(10.0); ``` Then replace all 3 sites: - Line 194: `max_grad_norm: hyperparams.gradient_clip_norm.unwrap_or(10.0) as f32,` → `max_grad_norm: resolved_grad_norm as f32,` - Line 263: `max_grad_norm: hyperparams.gradient_clip_norm.unwrap_or(1.0) as f32,` → `max_grad_norm: resolved_grad_norm as f32,` - Line 298: `max_grad_norm: hyperparams.gradient_clip_norm.unwrap_or(1.0) as f32,` → `max_grad_norm: resolved_grad_norm as f32,` - [ ] **Step 2: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo check -p ml && SQLX_OFFLINE=true cargo test -p ml --lib -- dqn 2>&1 | tail -5` Expected: Compiles, all tests pass. - [ ] **Step 3: Commit** ```bash git add crates/ml/src/trainers/dqn/fused_training.rs git commit -m "fix: unify gradient_clip_norm — resolve once, pass to all sub-trainers (was 10.0 vs 1.0)" ``` --- ### Task 2: Make entropy_coefficient non-optional Bug #5: `Option` with 3 different `unwrap_or` defaults (0.001, 0.01, 0.02). Fix: make it non-optional with a single canonical default. **Files:** - Modify: `crates/ml/src/trainers/dqn/config.rs` - Modify: `crates/ml/src/trainers/dqn/trainer/constructor.rs` - [ ] **Step 1: Change type in DQNHyperparameters** In `config.rs` (~line 960), change: ```rust pub entropy_coefficient: Option, ``` To: ```rust #[serde(default = "default_entropy_coefficient")] pub entropy_coefficient: f64, ``` Add the default function nearby: ```rust fn default_entropy_coefficient() -> f64 { 0.001 } ``` In the `conservative()` and `default()` impl, change: ```rust entropy_coefficient: Some(0.001), ``` To: ```rust entropy_coefficient: 0.001, ``` - [ ] **Step 2: Remove unwrap_or in constructor.rs** Line 314: `entropy_coefficient: hyperparams.entropy_coefficient.unwrap_or(0.01),` → `entropy_coefficient: hyperparams.entropy_coefficient,` Line 399: `let coeff = hyperparams.entropy_coefficient.unwrap_or(0.01);` → `let coeff = hyperparams.entropy_coefficient;` - [ ] **Step 3: Fix all compilation errors from type change** Run `SQLX_OFFLINE=true cargo check --workspace 2>&1 | head -40` — fix any sites that still use `Some(...)` or `.unwrap()` on entropy_coefficient. Common patterns: - `Some(0.001)` → `0.001` - `.unwrap_or(x)` → remove - Pattern match on `Some(v)` → use directly - [ ] **Step 4: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo check --workspace && SQLX_OFFLINE=true cargo test -p ml --lib -- dqn && SQLX_OFFLINE=true cargo test -p ml-dqn --lib 2>&1 | tail -5` Expected: All pass. - [ ] **Step 5: Commit** ```bash git add crates/ml/src/trainers/dqn/config.rs crates/ml/src/trainers/dqn/trainer/constructor.rs git commit -m "fix: make entropy_coefficient non-optional — single canonical default 0.001" ``` --- ### Task 3: Replace close price unwrap_or(1.0) with direct indexing Bug #3: Silent wrong rewards when price parse fails. The indices are always in-bounds (loop from 1..n), so direct indexing is safe and surfaces any future bugs. **Files:** - Modify: `crates/ml/src/trainers/dqn/data_loading.rs` - [ ] **Step 1: Find and replace the unwrap_or(1.0)** At line 306, change: ```rust let prev = close_prices_f32.get(i - 1).copied().unwrap_or(1.0); let curr = close_prices_f32.get(i).copied().unwrap_or(prev); ``` To: ```rust let prev = close_prices_f32[i - 1]; let curr = close_prices_f32[i]; ``` - [ ] **Step 2: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo check -p ml && SQLX_OFFLINE=true cargo test -p ml --lib -- dqn 2>&1 | tail -5` - [ ] **Step 3: Commit** ```bash git add crates/ml/src/trainers/dqn/data_loading.rs git commit -m "fix: replace close price unwrap_or(1.0) with direct indexing — no silent wrong rewards" ``` --- ### Task 4: Fix step_count incrementing during inference Bug #8: `forward()` unconditionally increments step_count, advancing dropout schedule during eval. **Files:** - Modify: `crates/ml-dqn/src/network.rs` - [ ] **Step 1: Read current forward() to understand full context** Read `network.rs` around line 245-260 to see how `step_count` and `dropout_scheduler` are used. - [ ] **Step 2: Add `training: bool` parameter to forward()** Change the forward method to accept a training flag. Only increment step_count and advance dropout scheduler when training=true. Update the method signature and all call sites. If there's a trait constraint, add a `forward_eval` convenience method that calls `forward(state, false)`. - [ ] **Step 3: Update all call sites** Search for `.forward(` across the codebase. Inference sites (backtest, action selection) pass `false`. Training sites pass `true`. - [ ] **Step 4: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo check --workspace && SQLX_OFFLINE=true cargo test -p ml-dqn --lib 2>&1 | tail -5` - [ ] **Step 5: Commit** ```bash git add crates/ml-dqn/src/network.rs git commit -m "fix: step_count only increments during training — no inference side effects" ``` --- ### Task 5: Single CUDA context in quantile loss Bug #6: Creates CudaContext 3× per call. Refactor to create once and pass through. **Files:** - Modify: `crates/ml-dqn/src/quantile_regression.rs` - [ ] **Step 1: Read the full quantile_huber_loss function** Understand the 3 context creation sites and the inner `_per_sample` function. - [ ] **Step 2: Refactor to single context** Create context + stream once at the top. Pass the stream to all operations that need it. If `to_host()` creates its own context internally, check if there's a `to_host_with_stream()` variant or use manual `memcpy_dtoh`. - [ ] **Step 3: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo check -p ml-dqn && SQLX_OFFLINE=true cargo test -p ml-dqn --lib 2>&1 | tail -5` - [ ] **Step 4: Commit** ```bash git add crates/ml-dqn/src/quantile_regression.rs git commit -m "fix: single CUDA context per quantile loss call — was 3× context creation" ``` --- ### Task 6: Remove silent fallbacks in quantile loss Bug #7: `unwrap_or(0.0)` / `unwrap_or(0.5)` on tensor indexing hides bugs. **Files:** - Modify: `crates/ml-dqn/src/quantile_regression.rs` - [ ] **Step 1: Replace unwrap_or with direct indexing** At line ~385-387, change: ```rust let pred_val = pred_host.get(idx).copied().unwrap_or(0.0); let tgt_val = tgt_host.get(idx).copied().unwrap_or(0.0); let tau_val = tau_host.get(idx).copied().unwrap_or(0.5); ``` To: ```rust let pred_val = pred_host[idx]; let tgt_val = tgt_host[idx]; let tau_val = tau_host[idx]; ``` - [ ] **Step 2: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo test -p ml-dqn --lib 2>&1 | tail -5` - [ ] **Step 3: Commit** ```bash git add crates/ml-dqn/src/quantile_regression.rs git commit -m "fix: remove silent 0.0/0.5 fallbacks in quantile loss — panic on OOB" ``` --- ### Task 7: Complete backtest episode reset + fix stale max_equity Bugs #1 + #2: The most impactful CUDA kernel changes. Reset all 8 portfolio fields, update max_equity before floor check. **Files:** - Modify: `crates/ml/src/cuda_pipeline/backtest_env_kernel.cu` - [ ] **Step 1: Fix pre-trade floor reset (lines 105-112)** Replace: ```cuda portfolio_state[ps + 0] = liq_value; portfolio_state[ps + 1] = 0.0f; portfolio_state[ps + 2] = liq_value; portfolio_state[ps + 3] = 0.0f; portfolio_state[ps + 4] = max_equity; portfolio_state[ps + 5] = 0.0f; portfolio_state[ps + 6] = cum_return + liq_ret; portfolio_state[ps + 7] += 1.0f; ``` With: ```cuda portfolio_state[ps + 0] = liq_value; // value = new initial capital portfolio_state[ps + 1] = 0.0f; // position = flat portfolio_state[ps + 2] = liq_value; // cash = new initial capital portfolio_state[ps + 3] = 0.0f; // entry_price = none portfolio_state[ps + 4] = liq_value; // max_equity = RESET (was stale) portfolio_state[ps + 5] = 0.0f; // hold_time = 0 portfolio_state[ps + 6] = 0.0f; // cum_return = RESET (was accumulating) portfolio_state[ps + 7] = 0.0f; // step_count = RESET (was incrementing) ``` - [ ] **Step 2: Fix max_equity update before post-trade floor check (line ~176-184)** After `float new_value = cash + position * close;` (line 176), insert BEFORE the floor check: ```cuda // Update max_equity BEFORE floor check — prevents stale peak max_equity = fmaxf(max_equity, new_value); ``` - [ ] **Step 3: Fix post-trade floor reset (lines 199-206)** Replace with complete reset (same pattern as pre-trade): ```cuda portfolio_state[ps + 0] = new_value; portfolio_state[ps + 1] = 0.0f; portfolio_state[ps + 2] = new_value; portfolio_state[ps + 3] = 0.0f; portfolio_state[ps + 4] = new_value; // max_equity = RESET portfolio_state[ps + 5] = 0.0f; portfolio_state[ps + 6] = 0.0f; // cum_return = RESET portfolio_state[ps + 7] = 0.0f; // step_count = RESET ``` - [ ] **Step 4: Verify build (cubins recompile via build.rs)** Run: `SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5` Expected: build.rs recompiles the .cu file, check passes. - [ ] **Step 5: Run tests** Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- dqn 2>&1 | tail -5` - [ ] **Step 6: Commit** ```bash git add crates/ml/src/cuda_pipeline/backtest_env_kernel.cu git commit -m "fix: complete backtest episode reset (all 8 fields) + update max_equity before floor check" ``` --- ### Task 8: Export budget fraction constants + fix hardcoded values Bug #10: `0.10` and `0.05` hardcoded in gpu_dqn_trainer.rs should reference the constants from fused_training.rs. **Files:** - Modify: `crates/ml/src/trainers/dqn/fused_training.rs` - Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` - [ ] **Step 1: Make budget constants public** In `fused_training.rs`, change: ```rust const CQL_GRAD_BUDGET: f32 = 0.15; const IQN_GRAD_BUDGET: f32 = 0.10; const ENS_GRAD_BUDGET: f32 = 0.05; ``` To: ```rust pub(crate) const CQL_GRAD_BUDGET: f32 = 0.15; pub(crate) const IQN_GRAD_BUDGET: f32 = 0.10; pub(crate) const ENS_GRAD_BUDGET: f32 = 0.05; ``` - [ ] **Step 2: Reference constants in gpu_dqn_trainer.rs** At line ~831 in `apply_iqn_trunk_gradient`, change: ```rust let max_component_norm = self.config.max_grad_norm * 0.10; // IQN_GRAD_BUDGET ``` To: ```rust let max_component_norm = self.config.max_grad_norm * crate::trainers::dqn::fused_training::IQN_GRAD_BUDGET; ``` At line ~1072 in `apply_ensemble_trunk_gradient`, change: ```rust let max_component_norm = self.config.max_grad_norm * 0.05; // ENS_GRAD_BUDGET ``` To: ```rust let max_component_norm = self.config.max_grad_norm * crate::trainers::dqn::fused_training::ENS_GRAD_BUDGET; ``` - [ ] **Step 3: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo check -p ml && SQLX_OFFLINE=true cargo test -p ml --lib -- dqn 2>&1 | tail -5` - [ ] **Step 4: Commit** ```bash git add crates/ml/src/trainers/dqn/fused_training.rs crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs git commit -m "fix: reference budget fraction constants — eliminate hardcoded 0.10/0.05 in gpu_dqn_trainer" ``` --- ### Task 9: Remove MaybeNoisyLinear single-variant enum Bug #16: Enum with only `Noisy` variant — unnecessary indirection. **Files:** - Modify: `crates/ml-dqn/src/branching.rs` - [ ] **Step 1: Read current MaybeNoisyLinear usage** Search for all `MaybeNoisyLinear` references in branching.rs to understand the scope of changes. - [ ] **Step 2: Replace enum with direct NoisyLinear** - Remove the `MaybeNoisyLinear` enum definition - Change all struct fields from `MaybeNoisyLinear` to `NoisyLinear` - Remove all `let MaybeNoisyLinear::Noisy(n) = self;` destructuring — use the field directly - Inline the thin wrapper methods (forward, reset_noise, etc.) or call directly on NoisyLinear - [ ] **Step 3: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo check --workspace && SQLX_OFFLINE=true cargo test -p ml-dqn --lib 2>&1 | tail -5` - [ ] **Step 4: Commit** ```bash git add crates/ml-dqn/src/branching.rs git commit -m "fix: remove MaybeNoisyLinear single-variant enum — use NoisyLinear directly" ``` --- ### Task 10: Fix Tier 1 silent unwrap_or fallbacks Bug #15: Critical silent fallbacks in action selection and state processing. **Files:** - Modify: `crates/ml/src/trainers/dqn/trainer/action.rs` - Modify: `crates/ml/src/trainers/dqn/trainer/state.rs` - [ ] **Step 1: Fix action.rs:161 — argmax fallback** Change `unwrap_or(2)` (silent Flat action) to `.expect("argmax on non-empty Q-values")`. - [ ] **Step 2: Fix state.rs:73 — price to_f32 fallback** Change `unwrap_or(0.0)` to `.expect("trading price fits f32")`. - [ ] **Step 3: Verify compilation + tests** Run: `SQLX_OFFLINE=true cargo check -p ml && SQLX_OFFLINE=true cargo test -p ml --lib -- dqn 2>&1 | tail -5` - [ ] **Step 4: Commit** ```bash git add crates/ml/src/trainers/dqn/trainer/action.rs crates/ml/src/trainers/dqn/trainer/state.rs git commit -m "fix: replace silent unwrap_or fallbacks in action selection and state processing" ``` --- ### Task 11: Add NoisyNet architecture comment to action selector Bug #9 (NOT A BUG): Document the architectural choice so future audits don't re-flag this. **Files:** - Modify: `crates/ml/src/cuda_pipeline/gpu_action_selector.rs` - [ ] **Step 1: Add comment at top of file** After the module doc comment, add: ```rust // NoisyNet exploration note: Q-values received by the action selector already // include factorized Gaussian noise from NoisyLinear layers in the network // forward pass. Epsilon-greedy acts as a secondary fallback, not the primary // exploration mechanism when NoisyNet is enabled. Explicit noise injection // here would double-count exploration. ``` - [ ] **Step 2: Commit** ```bash git add crates/ml/src/cuda_pipeline/gpu_action_selector.rs git commit -m "docs: clarify NoisyNet exploration is in network forward, not action selector" ``` --- ## Execution Dependencies ``` Task 1 (gradient_clip_norm) — independent Task 2 (entropy_coefficient) — independent Task 3 (close price) — independent Task 4 (step_count) — independent Task 5 (CUDA context) — independent Task 6 (quantile fallbacks) — after Task 5 (same file) Task 7 (backtest reset) — independent Task 8 (budget constants) — independent Task 9 (MaybeNoisyLinear) — independent Task 10 (unwrap_or fixes) — independent Task 11 (NoisyNet comment) — independent ``` Tasks 5-6 must be sequential (same file). All others are independent and can run in parallel. ## Verification After ALL tasks: ```bash SQLX_OFFLINE=true cargo check --workspace SQLX_OFFLINE=true cargo test -p ml --lib -- dqn SQLX_OFFLINE=true cargo test -p ml-dqn --lib ``` All must pass with zero failures.