Replaces hardcoded thresholds AND clamp bounds across 12 RL controllers
with observed-signal-driven ISV-slot bounds. Eliminates the architectural
failure mode that surfaced in walk-forward fold 0 (alpha-rl-m9cx5) and
fold 1 (alpha-rl-jgdh6): under Phase 4.5 advantage normalization, eight
controllers (PPO clip, target_tau, rollout_steps, entropy_coef, per_α,
gamma, reward_scale, q_distill_lambda) saturated at extrema within ~50
steps and stayed pinned for the rest of training — same pattern across
two different data slices, confirming the bug is structural rather than
data-size-dependent.
Mechanism: each controller's noise floor was hardcoded as a small
fraction of its target (`_NOISE_FLOOR_FRAC = 0.01f`), calibrated against
a pre-Phase-4.5 signal regime. Phase 4.5 normalization reduces operating
KL by ~50× — observed signal stays below the 1%-of-target floor, the
Schulman widen path fires continuously (asymmetric in the wrong
direction), and ε hits MAX 0.50 within ~50 training steps. ksll2's full-
data run (n_folds=1) happened to escape via a single above-band KL
observation that triggered tighten; both walk-forward folds (3 and 6
files) did not.
Per `feedback_adaptive_not_tuned`, `feedback_isv_for_adaptive_bounds`,
`pearl_controller_anchors_isv_driven`: every threshold now derives from
observed signal statistics (Welford online variance) rather than
constants calibrated against a prior signal regime. User explicitly
expanded scope mid-implementation: "if all clamps ISV bound should be
added to this spec and tasks" — applying the principle consistently
means hardcoded MIN/MAX clamp bounds count too, not just the saturating
noise floors. 12 controllers, single atomic commit per
`feedback_no_partial_refactor`.
Spec: docs/superpowers/specs/2026-05-30-adaptive-controller-floor-design.md
Plan: docs/superpowers/plans/2026-05-30-adaptive-controller-floor-plan.md
# New kernel
- rl_signal_variance_update.cu (80 LOC) — Welford online variance.
Single-thread single-block. Sentinel-zero skip per pearl. Per-controller
Welford triple (count, mean, M²) drives every adaptive noise floor.
# Per-controller refactors (12 .cu files)
- ppo_clip + target_tau + rollout_steps + entropy_coef + per_α —
adaptive noise floor = max(target × 0.5, sqrt(observed_var) × 2);
asymmetric Schulman (tighten on single observation, widen requires 3
consecutive below-band).
- gamma (Special G) — hardcoded GAMMA_MIN = 0.995 → adaptive via
Welford MEAN of trade duration. Per spec Q6 resolution: the Welford
mean's natural N-smoothing lag breaks the gamma↔trade_duration
feedback loop without explicit step-period gating. 100-observation
warmup falls back to EMA before Welford has enough samples.
- reward_scale (Special R) — asymmetric DECREASE rate cap (5% per
step) on both bootstrap-replace and Wiener-blend paths; bootstrap-
fraction floor (10% of bootstrap) until 100 trades close.
- q_distill_lambda (Special Q) — hardcoded MIN_LAMBDA = 0.05 → adaptive
via Welford on q_distill_kl_ema: max(0.001, std × 0.05).
- v_blend_alpha (Phase 4.4) — 5 hardcoded constants → 5 ISV slots
(DEAD_SIGNAL_FLOOR, TARGET_TRACK_RATIO, SCHULMAN_STEP, EMA_ALPHA,
BOOTSTRAP_ALPHA) + adaptive dead-signal floor from V_scalar magnitude
variance.
- ppo_ratio_clamp — adaptive MIN/MAX from observed log-ratio variance.
Architectural 2.0 absolute floor preserved (don't degenerate to
vanilla policy gradient).
- reward_clamp — V_BOUND_FLOOR, V_BOUND_EWMA_ALPHA, MIN_WIN, MIN_RATIO,
MAX_RATIO, MIN/MAX_MARGIN, MARGIN_TOLERANCE/ADJUST_RATE,
CLIP_RATE_EMA_ALPHA → 10 ISV slots.
- gate_threshold — hardcoded alpha = 0.01 → ISV slot 638.
- Clamp-bound expansion: EPS_MIN/MAX, TAU_MIN, ROLLOUT_MIN/MAX,
COEF_MIN/MAX, PER_ALPHA_MIN/MAX, GAMMA_MAX, MAX_LAMBDA, KL_TOLERANCE,
LAMBDA_RAMP_RATE, LAMBDA_DECAY_RATE → all ISV.
- WIENER_ALPHA_FLOOR shared across 9 controllers → single ISV slot 659.
# Trainer integration (integrated.rs)
- rl_signal_variance_update kernel loaded + helper method
`launch_rl_signal_variance_update`.
- Per-step Welford launches for 9 controller inputs, placed between
EMA producers and rl_fused_controllers in the per-step pipeline.
- Input-slot lookup array for rl_fused_controllers updated: rollout_steps
now consumes RL_ADV_VAR_PRE_NORM_INDEX (emitted by
rl_advantage_normalize before in-place normalize) instead of the
post-norm advantage_var_ratio (definitionally ~0 under Phase 4.5).
- ~30 new ISV bootstrap entries in with_controllers_bootstrapped.
# ISV slot allocation (isv_slots.rs)
- 72 new slots, RL_SLOTS_END 588 → 660. +288 bytes mapped-pinned.
- 9 Welford variance triples + 5 asymmetric Schulman counters +
3 new input signals (var_pre_norm, gamma_min_adaptive, reward_mag) +
v_blend (5 + 3 Welford) + ppo_ratio_clamp (1 + 3 Welford) +
reward_clamp (5) + gate_threshold (1) + q_distill (1) + 20 clamp bounds +
shared wiener floor.
# Bootstrap-clamp consistency fix (caught by Task 16 testing)
The original draft bootstrapped RL_EPS_BOOTSTRAP at 0.01 (= KL target),
but EPS_MIN was 0.05 — bootstrap value below clamp range. First post-
bootstrap step always snapped ε to MIN regardless of signal direction
("snap to MIN" behavior the unit tests surfaced). Fixed by bootstrapping
ε at MIN (0.05) so asymmetric Schulman operates from a valid state.
# Validation
- cargo build --release: clean
- cargo build --tests: clean
- 3 GPU Welford kernel tests (G1 constant→0var, G2 sequence→known var,
G3 sentinel skip): all pass
- 5 GPU adaptive-floor invariant tests (G3 PPO clip holds at bootstrap
when signal below floor, G4 tightens on single above-band, G5 widens
only after 3 consecutive below-band, G6 post-warmup uses Welford mean,
G6 pre-warmup uses EMA): all pass
- integrated_trainer_smoke (full GPU pipeline): passes
- 1k local smoke at b=128: 1000/1000 steps, completed_clean, no NaN,
controllers genuinely adapting (γ 0.90→0.974, per_α 0.40→0.52,
q_distill_λ 0.05→0.21, ε held at 0.05 = MIN per Phase 4.5 small-KL
regime — was previously stuck at MAX 0.50 throughout fold 0/1)
- compute-sanitizer memcheck b=128 5 steps: 0 errors
Cluster validation (G7-G9) submitted as follow-up runs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
200 lines
10 KiB
Plaintext
200 lines
10 KiB
Plaintext
// rl_per_alpha_controller.cu — emits PER (Prioritized Experience Replay)
|
||
// priority exponent to ISV[RL_PER_ALPHA_INDEX=405].
|
||
//
|
||
// Phase E.3b of the integrated RL trainer
|
||
// (docs/superpowers/plans/2026-05-22-integrated-rl-trainer.md).
|
||
//
|
||
// PER's priority exponent α controls how aggressively the replay buffer
|
||
// concentrates sampling on high-TD-error transitions. Per
|
||
// `pearl_controller_anchors_isv_driven` and
|
||
// `feedback_isv_for_adaptive_bounds`, α is NOT a hardcoded constant:
|
||
// it adapts to the tail thickness of the TD-error distribution.
|
||
//
|
||
// Controller logic:
|
||
// * High TD-error kurtosis (heavy tails) → a few transitions
|
||
// dominate the loss → raise α to sharpen the priority distribution
|
||
// and concentrate sampling on the informative tails.
|
||
// * Low kurtosis (light tails, ≈ Gaussian) → TD-errors are uniform →
|
||
// keep α near the standard PER default (0.6) — over-sharpening on a
|
||
// light-tailed distribution wastes samples on noise.
|
||
//
|
||
// Bootstrap discipline (per `pearl_first_observation_bootstrap`): the
|
||
// ISV slot starts at 0.0 sentinel. First emit DERIVES the bootstrap
|
||
// from the current `input_slot` (td_kurtosis EMA) via the same target
|
||
// formula the per-step path uses, instead of writing a hardcoded
|
||
// "canonical PER 0.6 default". The earlier hardcoded-0.6 pattern
|
||
// coincided with `target(kurt=10) = 0.6` — the canonical
|
||
// heavy-tailed-market kurtosis — creating a Wiener-blend fixed point
|
||
// where `prev = target = 0.6` froze the controller in a dead-zone
|
||
// for any input near canonical. The derive-from-input pattern
|
||
// eliminates the coincidence: bootstrap is whatever the target formula
|
||
// emits for the current input (= 0.4 at sentinel-zero input, = 0.6
|
||
// when input has already EMA'd to canonical 10, = 0.886 at kurt=20,
|
||
// etc.) so the controller's per-step Wiener blend always sees a real
|
||
// `prev` vs `target` delta on subsequent inputs.
|
||
//
|
||
// Subsequent emits Wiener-α blend with floor 0.4 (per
|
||
// `pearl_wiener_alpha_floor_for_nonstationary` — the TD-error
|
||
// distribution drifts as Q co-adapts, breaking stationarity).
|
||
//
|
||
// Bounds: α ∈ [0.3, 1.0]. Below 0.3 the priority distribution
|
||
// approaches uniform (PER degenerates into vanilla replay); at 1.0
|
||
// sampling is perfectly proportional to priority.
|
||
|
||
#define RL_PER_ALPHA_INDEX 405
|
||
// PER α MIN/MAX clamp bounds are now ISV-driven per the 2026-05-30
|
||
// clamp-bound extension. Runtime-tunable + visible in diag.
|
||
#define RL_PER_ALPHA_MIN_INDEX 647
|
||
#define RL_PER_ALPHA_MAX_INDEX 648
|
||
// Kurtosis of a standard normal = 3.0 ("excess kurtosis 0" with the
|
||
// alternative convention). Used as the breakpoint above which we start
|
||
// raising α.
|
||
// ISV-driven kurtosis interpretation constants per
|
||
// `feedback_isv_for_adaptive_bounds`.
|
||
#define RL_KURT_GAUSSIAN_INDEX 471
|
||
#define RL_KURT_NOISE_FLOOR_INDEX 472
|
||
#define RL_KURT_LIFT_SCALE_INDEX 459
|
||
// Schulman tolerance from the shared global slot.
|
||
#define RL_SCHULMAN_TOLERANCE_INDEX 468
|
||
// Noise-floor gate: if the streaming kurtosis estimator emits a value
|
||
// below this magnitude, treat it as "no signal" (sentinel-zero proxy)
|
||
// and hold α at the prior value. Without this, on the first few steps
|
||
// after the streaming estimator initialises, a sub-Gaussian kurtosis
|
||
// reading (e.g. 0.5 — near-uniform per-step batch means before tails
|
||
// develop) would yield target = 0.4 = PER_ALPHA_MIN, dragging α toward
|
||
// MIN despite zero real signal. Matches the defensive noise-floor
|
||
// pattern on the other controllers (per
|
||
// pearl_multiplicative_controllers_need_bounded_step_and_noise_floor).
|
||
// (KURT_NOISE_FLOOR — was 1.0f #define — now isv[RL_KURT_NOISE_FLOOR_INDEX])
|
||
// Wiener-α floor — shared across 9 controllers (slot 659).
|
||
#define RL_WIENER_ALPHA_FLOOR_INDEX 659
|
||
|
||
// Adaptive controller floors (spec 2026-05-30-adaptive-controller-floor-design):
|
||
// noise floor augmented with Welford-derived adaptive component (replaces
|
||
// reliance on the ISV-driven RL_KURT_NOISE_FLOOR_INDEX absolute floor alone).
|
||
// Asymmetric Schulman semantics: α RISES on a single above-band kurtosis
|
||
// observation (heavy tails = safety signal, sharpen PER fast); α FALLS only
|
||
// after N consecutive below-band observations (light tails = patient drift).
|
||
// Replaces the regime where the controller stuck at MIN 0.4 because every
|
||
// step's kurtosis read landed just above the absolute floor but well below
|
||
// the band — the Wiener blend dragged α down even on single observations.
|
||
#define RL_TD_KURT_VAR_COUNT_INDEX 604
|
||
#define RL_TD_KURT_VAR_M2_INDEX 606
|
||
#define RL_TD_KURT_BELOW_COUNT_INDEX 607
|
||
#define NOISE_FLOOR_STD_MULTIPLIER 2.0f // floor ≥ 2σ of observed kurtosis
|
||
#define WIDEN_PATIENCE_CONSECUTIVE 3.0f // α descent requires N below-band steps
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// rl_per_alpha_controller:
|
||
// Single-thread controller — writes ONE float to
|
||
// isv[RL_PER_ALPHA_INDEX].
|
||
//
|
||
// Inputs:
|
||
// isv [≥ RL_PER_ALPHA_INDEX+1] — ISV bus
|
||
// alpha_step — Wiener-α from the controller's signal stats;
|
||
// floored at WIENER_ALPHA_FLOOR before the blend.
|
||
// Named `alpha_step` to disambiguate from the
|
||
// output (PER's α).
|
||
// td_kurtosis_ema — caller-computed kurtosis EMA of the per-
|
||
// transition |TD-error| series. Phase F produces
|
||
// this via a 4-moment reduce over the replay
|
||
// priority column.
|
||
//
|
||
// Outputs:
|
||
// isv[RL_PER_ALPHA_INDEX] — PER priority exponent α ∈
|
||
// [PER_ALPHA_MIN, PER_ALPHA_MAX]
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Phase R5: scalar input arg replaced with `input_slot` ISV index so the
|
||
// EMA producer (Phase R3 ema_update_on_done targeting
|
||
// ISV[RL_TD_KURTOSIS_EMA_INDEX=422]) feeds this controller without any
|
||
// host roundtrip per `feedback_cpu_is_read_only`.
|
||
extern "C" __global__ void rl_per_alpha_controller(
|
||
float* __restrict__ isv,
|
||
float alpha_step,
|
||
int input_slot
|
||
) {
|
||
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
||
|
||
const float prev = isv[RL_PER_ALPHA_INDEX];
|
||
|
||
// Noise-floor gate: kurtosis below the adaptive noise floor is
|
||
// dominated by streaming-estimator startup noise (per-step batch-mean
|
||
// differences before tails accumulate). Hold α at prev to avoid
|
||
// dragging toward PER_ALPHA_MIN on cold-start.
|
||
//
|
||
// Adaptive component (spec 2026-05-30): floor scales with observed
|
||
// kurtosis std. Replaces reliance on the absolute ISV floor alone.
|
||
// Welford sample variance = M² / (count − 1) when count > 1.
|
||
const float td_kurtosis_ema = isv[input_slot];
|
||
const float kurt_count = isv[RL_TD_KURT_VAR_COUNT_INDEX];
|
||
const float kurt_var = (kurt_count > 1.0f)
|
||
? isv[RL_TD_KURT_VAR_M2_INDEX] / (kurt_count - 1.0f)
|
||
: 0.0f;
|
||
const float kurt_std = sqrtf(kurt_var);
|
||
const float adaptive_noise_floor = fmaxf(isv[RL_KURT_NOISE_FLOOR_INDEX],
|
||
kurt_std * NOISE_FLOOR_STD_MULTIPLIER);
|
||
if (td_kurtosis_ema > 0.0f && td_kurtosis_ema < adaptive_noise_floor) {
|
||
// Real signal present but below the noise floor — hold prev.
|
||
// (Strict sentinel zero handled by the prev==0 bootstrap path
|
||
// below, which derives target from the current EMA so the
|
||
// first-non-zero observation still seeds correctly.)
|
||
if (prev != 0.0f) return;
|
||
}
|
||
|
||
// Compute target from the current input EMA via a piecewise linear
|
||
// lift. Shared between bootstrap and per-step paths so the dead-zone
|
||
// coincidence with a hardcoded bootstrap value cannot recur.
|
||
// kurt ≤ KURT_GAUSSIAN → target = 0.4 (PER_ALPHA_MIN + 0.1)
|
||
// kurt = KURT_GAUSSIAN + KURT_LIFT_SCALE (≈10) → target = 0.6 (canonical PER)
|
||
// kurt → ∞ → target → PER_ALPHA_MAX
|
||
//
|
||
// The 0.4-0.6 baseline keeps the steady-state output near PER's
|
||
// canonical 0.6 (when input EMA stabilises at kurt=10) while
|
||
// leaving headroom to lift toward 1.0 under heavy tails.
|
||
const float per_alpha_min = isv[RL_PER_ALPHA_MIN_INDEX];
|
||
const float per_alpha_max = isv[RL_PER_ALPHA_MAX_INDEX];
|
||
const float kurt_excess = fmaxf(0.0f, td_kurtosis_ema - isv[RL_KURT_GAUSSIAN_INDEX]);
|
||
const float kurt_lift_scale = isv[RL_KURT_LIFT_SCALE_INDEX];
|
||
float target = 0.4f + 0.2f * (kurt_excess / kurt_lift_scale);
|
||
target = fmaxf(per_alpha_min, fminf(target, per_alpha_max));
|
||
|
||
// Bootstrap on sentinel 0.0 per pearl_first_observation_bootstrap:
|
||
// first emit replaces directly with the computed target. At
|
||
// construction time when input EMA is also sentinel-zero, target =
|
||
// 0.4 (min), which is distinct from EVERY non-sentinel target
|
||
// value the formula can emit (≥ 0.4). The next per-step call with
|
||
// any real input will see prev=0.4 vs target≥0.4 and Wiener-blend
|
||
// away from the sentinel.
|
||
if (prev == 0.0f) {
|
||
isv[RL_PER_ALPHA_INDEX] = target;
|
||
return;
|
||
}
|
||
|
||
// Wiener-α blend with floor per pearl_wiener_alpha_floor_for_nonstationary.
|
||
const float a = fmaxf(alpha_step, isv[RL_WIENER_ALPHA_FLOOR_INDEX]);
|
||
float out = (1.0f - a) * prev + a * target;
|
||
|
||
out = fmaxf(per_alpha_min, fminf(out, per_alpha_max));
|
||
|
||
// Asymmetric Schulman patience on the DESCENT direction (spec 2026-05-30):
|
||
// PER α rises on a single above-band kurtosis observation (heavy tails
|
||
// = act fast to concentrate sampling on informative tails). PER α
|
||
// falls only after N consecutive below-band observations — a single
|
||
// light-tailed step must not drag α toward MIN.
|
||
const float kurt_gaussian = isv[RL_KURT_GAUSSIAN_INDEX];
|
||
const float tolerance = isv[RL_SCHULMAN_TOLERANCE_INDEX];
|
||
if (out < prev && td_kurtosis_ema < kurt_gaussian / tolerance) {
|
||
const float new_count = isv[RL_TD_KURT_BELOW_COUNT_INDEX] + 1.0f;
|
||
isv[RL_TD_KURT_BELOW_COUNT_INDEX] = new_count;
|
||
if (new_count < WIDEN_PATIENCE_CONSECUTIVE) {
|
||
// Hold at prev until patience accumulates.
|
||
isv[RL_PER_ALPHA_INDEX] = prev;
|
||
return;
|
||
}
|
||
} else {
|
||
isv[RL_TD_KURT_BELOW_COUNT_INDEX] = 0.0f;
|
||
}
|
||
|
||
isv[RL_PER_ALPHA_INDEX] = out;
|
||
}
|