diff --git a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs index 4452861fa..c34cb5156 100644 --- a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs +++ b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs @@ -4573,6 +4573,26 @@ impl GpuDqnTrainer { .arg(&adv_ptr).arg(&adv_mse_ptr) .arg(&scale_mse).arg(&n_adv) .launch(cfg_adv).map_err(|e| MLError::ModelError(format!("blend mse adv: {e}")))?; + + // ── Restore full MSE gradient for magnitude branch (d==1) ──── + // The alpha blend above gave magnitude: (1-alpha)*MSE_grad + // (because C51_grad is zeroed for d==1). At alpha=1.0, this is ZERO. + // Magnitude needs full MSE gradient always — it's the ONLY training + // signal for the magnitude branch head (C51 zeroed, IQN trunk-only). + // Fix: SAXPY d_adv[d==1] += alpha * d_adv_mse[d==1] to restore + // the alpha*MSE portion that was removed by the (1-alpha) scale. + if alpha > 1e-6 { + let mag_offset = (b * b0 * na) as u64 * 4; // bytes: b0 branch comes first + let n_mag = (b * b1 * na) as i32; + let mag_blocks = ((b * b1 * na + 255) / 256) as u32; + let cfg_mag = LaunchConfig { grid_dim: (mag_blocks, 1, 1), block_dim: (256, 1, 1), shared_mem_bytes: 0 }; + let mag_adv_ptr = adv_ptr + mag_offset; + let mag_mse_ptr = adv_mse_ptr + mag_offset; + self.stream.launch_builder(&self.saxpy_f32_kernel) + .arg(&mag_adv_ptr).arg(&mag_mse_ptr) + .arg(&alpha).arg(&n_mag) + .launch(cfg_mag).map_err(|e| MLError::ModelError(format!("restore mag mse: {e}")))?; + } } }