From 6d8a38f0b4e35550f73941bfcbdd97b5a786d7d2 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 15 Apr 2026 15:06:07 +0200 Subject: [PATCH] fix: backtracking checkpoint save + branch Adam reset bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Checkpoint save: improvement_rate used sharpe_history.last() which is the CURRENT epoch (just pushed). Always compared current to itself → 0. Fix: use sharpe_history[len-2] (previous epoch). 2. Branch Adam reset: required !checkpoints.is_empty() but branch reset doesn't USE checkpoints — it just zeros momentum in-place. Removed the checkpoint guard so the lightweight fix fires even without saved checkpoints. Both bugs prevented backtracking from ever acting in train-l7g9t despite detecting 6 consecutive frozen epochs. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/ml/src/trainers/dqn/trainer/training_loop.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index 3de566dfb..a5d43089f 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -2457,7 +2457,12 @@ impl DQNTrainer { } // 2. Save checkpoint when improving - let prev_sharpe = self.sharpe_history.last().copied().unwrap_or(0.0) as f32; + // Use second-to-last sharpe (last is current epoch, already pushed before this runs) + let prev_sharpe = if self.sharpe_history.len() >= 2 { + self.sharpe_history[self.sharpe_history.len() - 2] as f32 + } else { + 0.0 + }; let improvement_rate = val_sharpe_f32 - prev_sharpe; // per epoch if improvement_rate > 0.01 && !self.backtracking.route_active { self.save_backtracking_checkpoint(epoch, improvement_rate, val_sharpe_f32)?; @@ -2522,10 +2527,10 @@ impl DQNTrainer { // reset (zeroes m/v for indices 8-41, preserves trunk momentum). This may break // Adam fixed point without changing weights. Give it 3 epochs to take effect. // Full rewind triggers at plateau_threshold + 3. + // Branch Adam reset doesn't need checkpoints — it's a lightweight in-place fix. if self.backtracking.plateau_epochs == self.backtracking.plateau_threshold && !self.backtracking.route_active && epoch.saturating_sub(self.backtracking.last_rewind_epoch) >= self.backtracking.min_rewind_interval - && !self.backtracking.checkpoints.is_empty() { if let Some(ref mut fused) = self.fused_ctx { if let Err(e) = fused.reset_branch_adam_momentum() {