diff --git a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs index 2a20ca7e0..653fbdfd4 100644 --- a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs +++ b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs @@ -5203,20 +5203,42 @@ impl GpuDqnTrainer { let d_adv_ptr = self.cql_d_adv_logits.raw_ptr(); // B1/G2: Adaptive cql_alpha. - // - health from ISV[LEARNING_HEALTH_INDEX] (0.5 fallback if pinned ptr null) + // - health from ISV[LEARNING_HEALTH_INDEX] // - regime_stability from ISV[11] // - cql_alpha_eff = base × (1 − regime_stability) × health // Collapse → 0 (CQL off); volatile+healthy → full; stable+healthy → 0. - let (health, regime_stability) = if self.isv_signals_pinned.is_null() { - (0.5_f32, 0.5_f32) + // + // Task 2.5 Bug #7 — when `isv_signals_pinned` is null (smoke scale + // without ISV warmup), the previous silent fallback of (0.5, 0.5) + // gave `cql_alpha_eff = base × 0.5 × 0.5 = 0.25 × base` by degenerate + // math, not by design — violating feedback_no_hiding.md. Fix: emit a + // tracing::warn! once and gate the regime multiplier OFF (health=1.0, + // regime=0.0 → cql_alpha = base × 1.0 × 1.0 = base, the scheduled + // value). Full ISV warmup at smoke scale is a follow-up task. + let cql_alpha = if self.isv_signals_pinned.is_null() { + // Log once per-ctor lifetime so spam is bounded (this function + // runs every step). The std::sync::Once pattern here is local + // to this formula — constructors still populate isv_signals_pinned + // at full scale so the warn only fires under smoke-scoped runs + // that legitimately skip ISV warmup. + static WARN_ONCE: std::sync::Once = std::sync::Once::new(); + WARN_ONCE.call_once(|| { + tracing::warn!( + "cql_alpha regime gate: isv_signals_pinned is null — formula \ + disabled, using scheduled cql_alpha (base × 1.0 × 1.0). \ + Smoke-scale runs without ISV warmup expected; full-scale \ + should never hit this path." + ); + }); + self.config.cql_alpha } else { - unsafe { + let (health, regime_stability) = unsafe { let h = (*self.isv_signals_pinned.add(LEARNING_HEALTH_INDEX)).clamp(0.0, 1.0); let s = (*self.isv_signals_pinned.add(11)).clamp(0.0, 1.0); (h, s) - } + }; + self.config.cql_alpha * (1.0 - regime_stability) * health }; - let cql_alpha = self.config.cql_alpha * (1.0 - regime_stability) * health; // Cache for logging via FusedTrainingCtx / DQNTrainer. self.last_cql_alpha_eff = cql_alpha;