diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index b42d4f73e..c6d796328 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -1654,7 +1654,19 @@ extern "C" __global__ void experience_env_step( /* Distributional variance position sizing (Kelly from C51 atoms). * High Var[Q] = uncertain outcome → smaller position. * Low Var[Q] = confident → full position. - * Scale: 1 / (1 + sqrt(Var[Q_taken])). Bounded (0, 1]. */ + * Scale: 1 / (1 + sqrt(Var[Q_taken])). Bounded (0, 1]. + * + * Permanent floor: when the policy's own conviction signal (q_gap) is + * high, var_scale must not silence the magnitude intent. Same pearl as + * the Kelly warmup_floor (`pearl_blend_formulas_must_have_permanent_floor.md`): + * cascading multiplicative shrinks (cvar × q_gap_conviction × kelly × var_scale) + * compose to near-zero at validation when var_q persists high, even + * though every individual term is "well-bounded". The fix is the same: + * floor var_scale at the q_gap-derived conviction already applied + * upstream — it IS the adaptive bound (per feedback_isv_for_adaptive_bounds.md), + * not a tuned constant. With this floor, high conviction overrides + * variance shrinkage so Full intent at eval can produce Full realized + * positions even before var_q has fully decayed. */ if (q_variance != NULL) { /* Taken action's factored index: sum of branch-local indices */ int taken_branch_idx = dir_idx; /* direction is branch 0 */ @@ -1664,6 +1676,11 @@ extern "C" __global__ void experience_env_step( /* Use max variance across the two exposure branches (direction + magnitude) */ float var_q = fmaxf(var_taken, var_mag); float var_scale = 1.0f / (1.0f + sqrtf(var_q)); + /* Permanent floor — same q_gap-derived conviction clamped at line 1628. */ + float var_floor = (q_gaps != NULL) + ? fminf(fmaxf(q_gaps[i] * 0.5f, 0.25f), 1.0f) + : 0.25f; + var_scale = fmaxf(var_scale, var_floor); /* Task 0.3: persist the per-sample Kelly scale for host-side epoch-mean * reduction. Host filters on `> 0.0f` to distinguish measured samples * from unreached/Var[Q]-disabled default-zero slots. */ diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index de0724945..99fe2f6c0 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -2,6 +2,20 @@ **Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7. +var_scale permanent floor for eval magnitude collapse (2026-04-27): the +EVAL_DIST=Quarter 0.59 / Half 0.41 / Full 0.00 pathology persists even with +intent=0.911 for Full because the position-sizing pipeline composes four +multiplicative shrinks (cvar_scale × q_gap_conviction × kelly_f × var_scale) +each well-bounded individually but compounding to ≪ 0.75. Kelly already +fixed via permanent-floor pearl (`pearl_blend_formulas_must_have_permanent_floor.md`); +applied the same pearl to var_scale at `experience_kernels.cu:1666`: +`var_scale = max(var_scale, q_gap_conviction)`. The conviction signal IS +the adaptive bound (per `feedback_isv_for_adaptive_bounds.md`), already +clamped to [0.25, 1.0] at line 1628. With this floor, high conviction +overrides Var[Q] shrinkage so the policy's magnitude intent reaches the +realised position even before var_q decays. No tuned constants — same +adaptive q_gap signal used upstream. + data_source globalization to mbp10 (2026-04-27): completes the alignment started in the earlier "fxcache data_source alignment" entry below. Smoke (`dqn-smoketest.toml`) and localdev (`dqn-localdev.toml`) profiles