diff --git a/crates/ml-alpha/src/trainer/integrated.rs b/crates/ml-alpha/src/trainer/integrated.rs index a83fd4da2..b70491f41 100644 --- a/crates/ml-alpha/src/trainer/integrated.rs +++ b/crates/ml-alpha/src/trainer/integrated.rs @@ -3941,7 +3941,11 @@ impl IntegratedTrainer { self.last_pi_loss = l_pi_host; let l_q_host = unsafe { std::ptr::read_volatile(self.ss_q_loss_mapped.host_ptr) }; - self.last_q_loss = l_q_host / (b_size as f32); + // Kernel already divides by B in atomicAdd (dqn_distributional_q.cu:285). + // The previous host divide was a double-divide bug — true mean CE + // appeared as mean/B ≈ 0.003 instead of ≈ 3.0. Matches l_pi convention + // (ppo_clipped_surrogate.cu:269-271 also kernel-side divides). + self.last_q_loss = l_q_host; let l_v_sum_host = unsafe { std::ptr::read_volatile(self.ss_v_loss_sum_mapped.host_ptr) }; let l_v_host = l_v_sum_host / (b_size as f32); @@ -5193,7 +5197,8 @@ impl IntegratedTrainer { // is zero = sentinel, and the per-branch LR controller's cold- // start gate handles that. let l_q_host = unsafe { std::ptr::read_volatile(self.ss_q_loss_mapped.host_ptr) }; - let l_q = l_q_host / (b_size as f32).max(1.0); + // Kernel divides by B (see step_synthetic_body comment). No host divide. + let l_q = l_q_host; self.dqn_head .backward_gemm(