cvf86 controller_branch diag (commit 708c121f2) revealed:
rollout_steps: 99.99% WIDEN, 0% HOLD, 0% SHRINK
The bounded-step + noise-floor fix was correctly applied, but the
controller WIDENED 99.99% of steps because the hardcoded
ADV_VAR_RATIO_TARGET = 0.1 (`#define` in the kernel) was a b_size>1
design choice. The streaming-EMA regime at b_size=1 has var/|mean|
naturally living in [1, 10] (median 3.9), so input is ALWAYS >> 0.1
and the controller correctly says "noisy advantages → widen". Result:
n_rollout pegs at MAX=8192 within ~5 steps and stays for 50k steps,
PPO update frequency drops 4×, KL stays in numerical noise (median
1.7e-8), Q can't learn (l_q stuck at ~2.7 vs uniform 3.04).
## Fix: ISV-driven target
Per `feedback_isv_for_adaptive_bounds`: ADV_VAR_RATIO_TARGET now
lives in ISV slot 449 (`RL_ADV_VAR_RATIO_TARGET_INDEX`), seeded at
trainer init to 5.0 (matches streaming-regime median 3.9). The
controller reads `isv[RL_ADV_VAR_RATIO_TARGET_INDEX]` each step
instead of a `#define`.
Expected behavior at TARGET=5.0:
* Median input 3.9 lands in-band [3.33, 7.5] → HOLD
* n_rollout stays near BOOTSTRAP=2048 instead of MAX
* 4× more PPO updates per step → policy actually moves
* KL leaves noise floor → ε controller activates
* Q has gradient signal → can learn
Noise floor is now derived multiplicatively from the ISV target
(`target × ADV_VAR_RATIO_NOISE_FLOOR_FRAC = 0.01`) so adjusting
the target proportionally adjusts the floor — no separate slot
needed.
## Wiring
`rl_streaming_clamp_init.cu` extended to seed all three ISV-resident
design constants (adv_var clamp ceiling, td_kurt clamp ceiling, AND
adv_var regression target). Single kernel call at trainer init —
still no HtoD per `feedback_no_htod_htoh_only_mapped_pinned`.
## Diag bake-in
`controller_branch.rollout_steps_target` now reads from
`isv[RL_ADV_VAR_RATIO_TARGET_INDEX]` instead of the prior hardcoded
`0.1f32` literal. The diag shows the current ISV-resident target
so post-hoc branch analysis uses the actual value the controller
saw, and lets us track whether a future adaptive controller (one
that maintains target from observed-input percentile EMA) is
moving the target correctly.
## Slot allocation
RL_SLOTS_END: 449 → 450 (one new design-constant slot).
## Test updates
G1 (isv_bootstrap) + G3 (r5_controllers) skip slot 449 in the
sentinel-zero loop and assert the seeded value (5.0) separately.
G3's `advantage_var_ratio` input bumped from 5.0 → 20.0 so the
WIDEN branch still fires (input > new target × 1.5 = 7.5) and the
test still validates that the controller moves off bootstrap.
## Verified gates (local sm_86)
G1 isv_bootstrap ✅
G3 controllers ✅ (with updated input)
G4 target_update ✅
integrated_smoke ✅
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
161 lines
7.5 KiB
Plaintext
161 lines
7.5 KiB
Plaintext
// rl_rollout_steps_controller.cu — emits PPO rollout buffer flush size
|
||
// to ISV[RL_N_ROLLOUT_STEPS_INDEX=404].
|
||
//
|
||
// Phase E.3b of the integrated RL trainer
|
||
// (docs/superpowers/plans/2026-05-22-integrated-rl-trainer.md).
|
||
//
|
||
// The rollout length sets how many on-policy transitions PPO accumulates
|
||
// before flushing into an update batch. Per
|
||
// `pearl_controller_anchors_isv_driven` and
|
||
// `feedback_isv_for_adaptive_bounds`, this is NOT a hardcoded constant:
|
||
// it adapts to the noise level of the advantage estimator.
|
||
//
|
||
// Controller logic (Schulman-style bounded step):
|
||
// * input > ADV_VAR_RATIO_TARGET × TOLERANCE → noisy → widen rollout
|
||
// by ADJUST_RATE.
|
||
// * input < ADV_VAR_RATIO_TARGET / TOLERANCE → stable → shrink rollout
|
||
// by ADJUST_RATE.
|
||
// * in-band → hold.
|
||
//
|
||
// CRITICAL: the prior design used `scale = clamp(input/target, 0.5, 2.0)`
|
||
// which saturated to ±2× on the SIGN of (input − target), not the
|
||
// magnitude. With typical streaming input 1–10 and target=0.1, every
|
||
// step doubled until the controller hit ROLLOUT_MAX in ≤4 steps
|
||
// (canonical: alpha-rl-kc2h9 fold0 had n_rollout pegged at MAX 100% of
|
||
// 50000 steps despite no actual signal change). Schulman-style bounded
|
||
// discrete adjustment converges smoothly to MAX/MIN when signal is
|
||
// consistently out-of-band, but doesn't slam there from one
|
||
// observation.
|
||
//
|
||
// Noise-floor gate holds the controller when input is below
|
||
// ADV_VAR_RATIO_NOISE_FLOOR = ADV_VAR_RATIO_TARGET × 0.01 — at that
|
||
// magnitude the streaming kernel's signal is dominated by numerical
|
||
// noise / cold-start, not a real "advantages are clean" indication
|
||
// worth shrinking the rollout for.
|
||
//
|
||
// Bootstrap discipline (per `pearl_first_observation_bootstrap`): the
|
||
// ISV slot starts at 0.0 sentinel. First emit writes
|
||
// `ROLLOUT_BOOTSTRAP = 2048` directly (the canonical PPO default).
|
||
// Subsequent emits Wiener-α blend with floor 0.4 (per
|
||
// `pearl_wiener_alpha_floor_for_nonstationary` — the advantage
|
||
// distribution drifts as π_new co-adapts, breaking stationarity).
|
||
//
|
||
// Bounds: rollout ∈ [256, 8192]. Below 256 PPO has too little data per
|
||
// update and the surrogate is dominated by sampling noise; above 8192
|
||
// updates lag the data so far behind that the importance ratios blow
|
||
// past the clip band.
|
||
|
||
#define RL_N_ROLLOUT_STEPS_INDEX 404
|
||
#define ROLLOUT_MIN 256.0f
|
||
#define ROLLOUT_MAX 8192.0f
|
||
#define ROLLOUT_BOOTSTRAP 2048.0f
|
||
// ISV slot holding the adaptive regression target. Per
|
||
// `feedback_isv_for_adaptive_bounds`: target lives in ISV (seeded at
|
||
// trainer init by rl_streaming_clamp_init, visible in diag,
|
||
// modifiable at runtime) rather than as a kernel-side `#define`.
|
||
// Default 5.0 — matches the b_size=1 streaming regime where var/|mean|
|
||
// naturally lives in [1, 10]. Prior `#define = 0.1` was wrong for the
|
||
// streaming regime, causing 99.99% WIDEN events in alpha-rl-cvf86.
|
||
#define RL_ADV_VAR_RATIO_TARGET_INDEX 449
|
||
// Noise-floor multiplier: input below TARGET × NOISE_FLOOR_FRAC is
|
||
// numerical noise — hold. Derived multiplicatively from the
|
||
// ISV-resident target so adjusting the target proportionally
|
||
// adjusts the floor.
|
||
#define ADV_VAR_RATIO_NOISE_FLOOR_FRAC 0.01f
|
||
// In-band tolerance: input within ±33 % of target → hold.
|
||
#define ADV_VAR_RATIO_TOLERANCE 1.5f
|
||
// Per-step bounded multiplicative adjustment. 1.5× == 50% growth /
|
||
// 33% shrinkage per fire.
|
||
#define ADV_VAR_RATIO_ADJUST_RATE 1.5f
|
||
#define WIENER_ALPHA_FLOOR 0.4f
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// rl_rollout_steps_controller:
|
||
// Single-thread controller — writes ONE float to
|
||
// isv[RL_N_ROLLOUT_STEPS_INDEX].
|
||
//
|
||
// Inputs:
|
||
// isv [≥ RL_N_ROLLOUT_STEPS_INDEX+1] — ISV bus
|
||
// alpha — Wiener-α from controller stats;
|
||
// floored at WIENER_ALPHA_FLOOR before
|
||
// the blend.
|
||
// advantage_var_over_abs_mean — caller-computed
|
||
// `var(A) / max(|mean A|, ε)` EMA;
|
||
// Phase F produces this via a small
|
||
// reduce kernel over the rollout
|
||
// buffer's advantage column.
|
||
//
|
||
// Outputs:
|
||
// isv[RL_N_ROLLOUT_STEPS_INDEX] — rollout length ∈ [ROLLOUT_MIN, ROLLOUT_MAX]
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// Phase R5: scalar input arg replaced with `input_slot` ISV index so the
|
||
// EMA producer (Phase R3 ema_update_per_step targeting
|
||
// ISV[RL_ADVANTAGE_VAR_RATIO_EMA_INDEX=421]) feeds this controller
|
||
// without any host roundtrip per `feedback_cpu_is_read_only`.
|
||
extern "C" __global__ void rl_rollout_steps_controller(
|
||
float* __restrict__ isv,
|
||
float alpha,
|
||
int input_slot
|
||
) {
|
||
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
||
|
||
const float prev = isv[RL_N_ROLLOUT_STEPS_INDEX];
|
||
// Bootstrap on sentinel 0.0 per pearl_first_observation_bootstrap.
|
||
if (prev == 0.0f) {
|
||
isv[RL_N_ROLLOUT_STEPS_INDEX] = ROLLOUT_BOOTSTRAP;
|
||
return;
|
||
}
|
||
|
||
// Cold-start gate: sentinel-zero EMA (streaming kernel hasn't
|
||
// fired yet).
|
||
const float advantage_var_over_abs_mean = isv[input_slot];
|
||
if (advantage_var_over_abs_mean == 0.0f) return;
|
||
|
||
// ISV-driven regression target — read from
|
||
// RL_ADV_VAR_RATIO_TARGET_INDEX (seeded at trainer init).
|
||
const float adv_var_target = isv[RL_ADV_VAR_RATIO_TARGET_INDEX];
|
||
|
||
// Noise-floor gate: input below target × NOISE_FLOOR_FRAC is
|
||
// dominated by numerical noise — hold rollout unchanged.
|
||
// Mirrors the ppo_clip / target_tau controllers' design after the
|
||
// alpha-rl-mjzfk multiplicative-blow-up incident.
|
||
const float adv_var_noise_floor =
|
||
adv_var_target * ADV_VAR_RATIO_NOISE_FLOOR_FRAC;
|
||
if (advantage_var_over_abs_mean < adv_var_noise_floor) return;
|
||
|
||
// Bounded multiplicative adjustment (Schulman-style adaptive).
|
||
// At most ADV_VAR_RATIO_ADJUST_RATE × shift per step regardless of
|
||
// how far input is from target. After several consecutive
|
||
// out-of-band observations the output drifts smoothly toward
|
||
// MIN/MAX, but a single observation can't slam it there.
|
||
float scale;
|
||
if (advantage_var_over_abs_mean
|
||
> adv_var_target * ADV_VAR_RATIO_TOLERANCE) {
|
||
// Noisy → widen.
|
||
scale = ADV_VAR_RATIO_ADJUST_RATE;
|
||
} else if (advantage_var_over_abs_mean
|
||
< adv_var_target / ADV_VAR_RATIO_TOLERANCE) {
|
||
// Clean → shrink.
|
||
scale = 1.0f / ADV_VAR_RATIO_ADJUST_RATE;
|
||
} else {
|
||
// In-band: hold.
|
||
scale = 1.0f;
|
||
}
|
||
float target = prev * scale;
|
||
target = fmaxf(ROLLOUT_MIN, fminf(target, ROLLOUT_MAX));
|
||
|
||
// First-observation replace-directly per
|
||
// `pearl_first_observation_bootstrap`.
|
||
if (prev == ROLLOUT_BOOTSTRAP) {
|
||
isv[RL_N_ROLLOUT_STEPS_INDEX] = target;
|
||
return;
|
||
}
|
||
|
||
// Wiener-α blend with floor per pearl_wiener_alpha_floor_for_nonstationary.
|
||
const float a = fmaxf(alpha, WIENER_ALPHA_FLOOR);
|
||
float out = (1.0f - a) * prev + a * target;
|
||
|
||
out = fmaxf(ROLLOUT_MIN, fminf(out, ROLLOUT_MAX));
|
||
isv[RL_N_ROLLOUT_STEPS_INDEX] = out;
|
||
}
|