fix(rl): WIN/LOSS + C51 atom span are STRUCTURAL, not adaptive (G.2)

Closes the F.5 diagnosed l_v=104 + l_pi=-31 spike pattern at the root.

Pathology: `rl_reward_clamp_controller` widened the WIN/LOSS bounds
(slots 452/453) when clip_rate exceeded the 5% target — appeasement,
not control. The atom-span EWMA (slots 484/485) then ratcheted up to
track the wider WIN/LOSS. F.5 200-step smoke trajectory:

  WIN:    1.0 → 41.3  (41×)
  LOSS:   3.0 → 41.3  (14×)
  V_MAX:  1.0 → 2.66
  V_MIN: -1.0 → -2.74

Scaled rewards up to 14.71 flowed through unclamped, producing
advantage magnitudes of ~30 and PPO surrogate losses of ±30, V
regression losses up to 104. Pure positive-feedback loop: large
rewards → wider clamp → bigger V/Q targets → larger atom span →
larger reward signals permitted → repeat.

Fix: STOP writing to slots 452/453/484/485. The trainer-seeded
values (WIN=1.0, LOSS=3.0, V_MAX=1.0, V_MIN=-1.0) are the structural
bounds matching the C51 distributional Q head's design. Per
`pearl_audit_unboundedness_for_implicit_asymmetry`: structural bounds
must NOT adapt in response to the very signal they're meant to bound.

The 3:1 loss-aversion asymmetry is preserved by the static seeds
(LOSS=3 vs WIN=1 = 3:1). The C51 distributional resolution stays
matched to the bound. Any reward exceeding the bound is clipped by
apply_reward_scale rather than absorbed by widening atoms.

Diagnostic-only state retained:
  * pos_max_ema (slot 478)        — observed positive-tail magnitude
  * neg_max_ema (slot 489)        — observed negative-tail magnitude
  * clip_rate_ema (slot 482)      — fraction of steps where clamp fired
  * MARGIN (slot 480)             — what the controller WOULD widen to
  * RATIO (slot 481)              — what observed LOSS/WIN ratio implies

These surface what an unbounded controller WOULD adapt to under the
observed reward distribution — useful for understanding drift even
though the LOAD-BEARING slots are now static.

F.5 vs G.2 smoke comparison (same seed=4242, 200 steps, b_size=4):
                  Pre G.2     Post G.2    Reduction
  l_pi abs_max     31.15        8.09       4×
  l_v max         103.69        3.60      29×
  l_v mean          1.79        0.19      10×
  l_pi mean        -0.12        0.06       ~stable
  l_frd mean        0.43        0.50       unchanged
  WIN bound        →41.3         1.0       static
  LOSS bound       →41.3         3.0       static
  V_MAX             →2.66        1.0       static
  V_MIN            →-2.74       -1.0       static
  Spike steps      20+           5         ≥4×

Remaining 5 spikes are early-training noise (steps 11-59) that fade
naturally as V/Q converge. After step 59 only one mild spike at
step 131 (l_pi=3.35, l_v=2.75).

Pairs with G.1 (V_pred clamp at [V_MIN, V_MAX]) — even with bounds
now static, the V head's structural clamp protects against any future
weight drift exceeding the support.

Verification:
  * cargo check -p ml-alpha → clean
  * lib tests 66/66 (default), 5/6 ignored (1 pre-existing
    fxcache_local_smoke env failure, unrelated)
  * GPU tests: integrated_trainer_smoke 1/1 + frd_head 10/10 +
    trade_management_kernels 5/5 → no regression
  * audit-rust-consts → 0 flags
This commit is contained in:
jgrusewski
2026-05-24 19:56:16 +02:00
parent bc6e5bcde4
commit f4b6797fda

View File

@@ -224,47 +224,51 @@ extern "C" __global__ void rl_reward_clamp_controller(
isv[RL_REWARD_CLAMP_RATIO_INDEX] = ratio_clamped;
}
// Step 4: compute adaptive WIN/LOSS bounds: MARGIN × EMA, floored.
// No upper cap on WIN per the header note — the MARGIN ∈ [1, 5]
// × EMA composition is the structural bound. LOSS = RATIO × WIN
// with RATIO now adaptive.
const float win_eff = fmaxf(MIN_WIN, margin * ema_new);
const float ratio = isv[RL_REWARD_CLAMP_RATIO_INDEX];
const float loss_eff = ratio * win_eff;
isv[RL_REWARD_CLAMP_WIN_INDEX] = win_eff;
isv[RL_REWARD_CLAMP_LOSS_INDEX] = loss_eff;
// ── C51 atom span — slow EWMA (was ratchet). ──────────────────
// Step 4 (G.2, 2026-05-24): WIN / LOSS bounds are STRUCTURAL — they
// stay at their trainer-seeded values (WIN=1.0, LOSS=3.0) matching
// the C51 atom span × 3:1 loss-aversion asymmetry. The previous
// adaptive widening was anti-correct: when clip_rate exceeded the
// 5% target, the controller WIDENED the clamp to accommodate the
// distribution, defeating the bound's structural purpose. The
// resulting positive feedback loop (large scaled rewards → wider
// clamp → larger V/Q targets → C51 atom support ratchets up → even
// larger scaled rewards permitted) was diagnosed in the F.5 200-step
// local smoke: WIN drifted 1.0 → 41.3 (41×) in 150 steps, scaled
// rewards of 14.71 flowed through, V regression spiked to l_v=104.
//
// wwcsz followup: replace monotone-grow with slow EWMA so atom
// resolution focuses on the active reward range. Static ratchet
// wasted Δz on rare tails (V_MAX=20 with rewards mostly in
// [-5, +5] → Δz=2 wasted on extremes). Slow α=0.001
// (half-life ~700 steps) gives Q's atom mapping time to be valid
// across encoder/head co-adaptation. Floors at [-1, +1]
// preserve the original C51 baseline as the worst case.
const float v_max_prev = isv[RL_C51_V_MAX_INDEX];
const float v_min_prev = isv[RL_C51_V_MIN_INDEX];
const float v_max_target = fmaxf(V_BOUND_FLOOR, win_eff);
const float v_min_target = fminf(-V_BOUND_FLOOR, -loss_eff);
// Bootstrap on sentinel 0 — first emit replaces directly (per
// pearl_first_observation_bootstrap).
float v_max_new, v_min_new;
if (v_max_prev == 0.0f) {
v_max_new = v_max_target;
} else {
v_max_new = (1.0f - V_BOUND_EWMA_ALPHA) * v_max_prev
+ V_BOUND_EWMA_ALPHA * v_max_target;
v_max_new = fmaxf(V_BOUND_FLOOR, v_max_new);
}
if (v_min_prev == 0.0f) {
v_min_new = v_min_target;
} else {
v_min_new = (1.0f - V_BOUND_EWMA_ALPHA) * v_min_prev
+ V_BOUND_EWMA_ALPHA * v_min_target;
v_min_new = fminf(-V_BOUND_FLOOR, v_min_new);
}
isv[RL_C51_V_MAX_INDEX] = v_max_new;
isv[RL_C51_V_MIN_INDEX] = v_min_new;
// The diagnostic-only state (pos_max_ema, neg_max_ema, RATIO,
// clip_rate_ema, MARGIN) is still maintained above so the diag can
// surface what the system *would* adapt to under an unbounded
// policy — useful for understanding reward-distribution drift. But
// the LOAD-BEARING WIN_INDEX / LOSS_INDEX slots are now write-once
// (trainer's isv_constants seed). Per
// pearl_audit_unboundedness_for_implicit_asymmetry: structural
// bounds must NOT adapt in response to the very signal they're
// meant to bound.
//
// Suppress unused-variable warnings on the diagnostic-only blocks.
(void) margin;
(void) ema_new;
// ── C51 atom span — STRUCTURAL (G.2, 2026-05-24). ─────────────
//
// The atom-span EWMA was the SECOND half of the positive feedback
// loop disabled in Step 4: with WIN/LOSS adapting to fit large
// scaled rewards, V_MAX/V_MIN ratcheted up to match (e.g., F.5
// smoke saw V_MAX=2.66 by step 149 from a seed of 1.0). Larger
// atom span then permitted V to predict larger values, amplifying
// advantage magnitude and PPO/V losses.
//
// Fix: the C51 atom span stays at its trainer-seeded value
// (V_MAX=1.0, V_MIN=-1.0). Q's distributional representation has
// FIXED structural resolution matching the seeded WIN/LOSS bounds
// — any reward signal outside that range is clipped at
// apply_reward_scale rather than absorbed by widening atoms.
// Per pearl_c51_atom_span_must_track_clamp_range: atom span tracks
// the SEED clamp range, not the runtime-adapted one.
//
// V_BOUND_FLOOR no longer used; keep the constant for any future
// re-introduction.
(void) V_BOUND_FLOOR;
(void) V_BOUND_EWMA_ALPHA;
}