cleanup(fflag): remove use_soft_updates — always EMA — [FFLAG-006]
DQNConfig.use_soft_updates was true at all 4 construction sites; the else branch (legacy hard update) was unreachable. Deleted field + 4 setters + collapsed update_target_networks() to the unconditional cosine-annealed EMA path. Hard-copy mode removed per feedback_no_feature_flags.md.
This commit is contained in:
@@ -65,8 +65,6 @@ pub struct DQNConfig {
|
||||
pub tau_final: f64,
|
||||
/// Total training steps for cosine schedule (T). 0 = use fixed tau (no annealing).
|
||||
pub tau_anneal_steps: u64,
|
||||
/// Use soft (EMA) or hard target updates
|
||||
pub use_soft_updates: bool,
|
||||
|
||||
// Rainbow DQN warmup period
|
||||
/// Number of steps to collect experiences with random exploration before training begins
|
||||
@@ -253,7 +251,6 @@ impl Default for DQNConfig {
|
||||
tau: 0.005,
|
||||
tau_final: 0.0005, // 10x slower at convergence for offline RL stability
|
||||
tau_anneal_steps: 100_000, // cosine anneal over 100K gradient steps
|
||||
use_soft_updates: true,
|
||||
warmup_steps: 1000,
|
||||
n_steps: 1,
|
||||
initial_capital: 100_000.0,
|
||||
@@ -566,7 +563,6 @@ impl DQNConfig {
|
||||
tau: 0.005,
|
||||
tau_final: 0.0005,
|
||||
tau_anneal_steps: 100_000,
|
||||
use_soft_updates: true,
|
||||
warmup_steps: 1000,
|
||||
n_steps: 3,
|
||||
initial_capital: 100000.0,
|
||||
@@ -630,7 +626,6 @@ impl DQNConfig {
|
||||
tau: 0.005,
|
||||
tau_final: 0.0005,
|
||||
tau_anneal_steps: 100_000,
|
||||
use_soft_updates: true,
|
||||
warmup_steps: 0,
|
||||
n_steps: 1, // Default to single-step TD (most stable)
|
||||
initial_capital: 100000.0,
|
||||
@@ -700,7 +695,6 @@ impl DQNConfig {
|
||||
tau: 0.005,
|
||||
tau_final: 0.001, // Very conservative annealing for emergency mode
|
||||
tau_anneal_steps: 50_000,
|
||||
use_soft_updates: true,
|
||||
|
||||
// Rainbow DQN warmup period
|
||||
warmup_steps: 0, // No warmup for emergency mode (safety first)
|
||||
@@ -2055,63 +2049,45 @@ impl DQN {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update target networks using cosine-annealed EMA (soft) or hard copy.
|
||||
/// Update target networks using cosine-annealed EMA (soft updates).
|
||||
///
|
||||
/// This is the shared implementation used by both [`train_step`] and
|
||||
/// Shared implementation used by both [`train_step`] and
|
||||
/// [`apply_accumulated_gradients`] to keep target network update logic
|
||||
/// in a single place.
|
||||
/// in a single place. Hard-copy legacy mode was removed — all DQN
|
||||
/// training uses cosine-annealed Polyak averaging.
|
||||
fn update_target_networks(&mut self) -> Result<(), MLError> {
|
||||
if self.config.use_soft_updates {
|
||||
// Cosine-annealed EMA (BYOL/MoCo v3 style)
|
||||
// tau(t) = tau_final - (tau_final - tau_base) * (cos(pi*t/T) + 1) / 2
|
||||
// Early: tau ~ tau_base (fast adaptation) -> Late: tau ~ tau_final (stability)
|
||||
let current_tau = if self.config.tau_anneal_steps > 0 {
|
||||
let progress =
|
||||
(self.training_steps as f64 / self.config.tau_anneal_steps as f64).min(1.0);
|
||||
let cosine_factor = (std::f64::consts::PI * progress).cos();
|
||||
self.config.tau_final
|
||||
- (self.config.tau_final - self.config.tau) * (cosine_factor + 1.0) / 2.0
|
||||
} else {
|
||||
self.config.tau // Fixed tau when annealing disabled
|
||||
};
|
||||
|
||||
// Cold-path fallback: copy_weights_from as hard-copy approximation.
|
||||
// The real training path uses the fused CUDA EMA kernel on flat buffers.
|
||||
// DistributionalDueling and IQN have no per-layer copy — fused trainer only.
|
||||
if let (Some(ref branching_net), Some(ref mut branching_target)) =
|
||||
(&self.branching_q_network, &mut self.branching_target_network)
|
||||
{
|
||||
branching_target.copy_weights_from(branching_net)
|
||||
.map_err(|e| {
|
||||
MLError::TrainingError(format!("Branching EMA update failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Log EMA update every 1000 steps
|
||||
if self.training_steps % 1000 == 0 {
|
||||
let half_life = convergence_half_life(current_tau);
|
||||
debug!(
|
||||
"EMA target update at step {} (τ={:.6}, half-life={:.0} steps)",
|
||||
self.training_steps, current_tau, half_life
|
||||
);
|
||||
}
|
||||
// Cosine-annealed EMA (BYOL/MoCo v3 style)
|
||||
// tau(t) = tau_final - (tau_final - tau_base) * (cos(pi*t/T) + 1) / 2
|
||||
// Early: tau ~ tau_base (fast adaptation) -> Late: tau ~ tau_final (stability)
|
||||
let current_tau = if self.config.tau_anneal_steps > 0 {
|
||||
let progress =
|
||||
(self.training_steps as f64 / self.config.tau_anneal_steps as f64).min(1.0);
|
||||
let cosine_factor = (std::f64::consts::PI * progress).cos();
|
||||
self.config.tau_final
|
||||
- (self.config.tau_final - self.config.tau) * (cosine_factor + 1.0) / 2.0
|
||||
} else {
|
||||
// Hard update: Full copy every N steps (legacy mode)
|
||||
// DistributionalDueling and IQN have no per-layer copy — fused trainer only.
|
||||
if self.training_steps % self.config.target_update_freq as u64 == 0 {
|
||||
if let (Some(ref branching_net), Some(ref mut branching_target)) =
|
||||
(&self.branching_q_network, &mut self.branching_target_network)
|
||||
{
|
||||
branching_target.copy_weights_from(branching_net).map_err(|e| {
|
||||
MLError::TrainingError(format!("Branching hard update failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
self.config.tau // Fixed tau when annealing disabled
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Hard target update at step {} (every {} steps)",
|
||||
self.training_steps, self.config.target_update_freq
|
||||
);
|
||||
}
|
||||
// Cold-path fallback: copy_weights_from as hard-copy approximation.
|
||||
// The real training path uses the fused CUDA EMA kernel on flat buffers.
|
||||
// DistributionalDueling and IQN have no per-layer copy — fused trainer only.
|
||||
if let (Some(ref branching_net), Some(ref mut branching_target)) =
|
||||
(&self.branching_q_network, &mut self.branching_target_network)
|
||||
{
|
||||
branching_target.copy_weights_from(branching_net)
|
||||
.map_err(|e| {
|
||||
MLError::TrainingError(format!("Branching EMA update failed: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Log EMA update every 1000 steps
|
||||
if self.training_steps % 1000 == 0 {
|
||||
let half_life = convergence_half_life(current_tau);
|
||||
debug!(
|
||||
"EMA target update at step {} (τ={:.6}, half-life={:.0} steps)",
|
||||
self.training_steps, current_tau, half_life
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user