Files
foxhunt/crates/ml-alpha/cuda/rl_kurtosis_streaming.cu
jgrusewski 39f90f3723 fix(rl): EMA-streaming variance + kurtosis kernels fix b_size=1 dead inputs
mjzfk + pdgxn diags showed `advantage_var_ratio` and `td_kurtosis`
identically 0 for 100% of every 50k-step smoke. Root cause: the
per-batch `rl_var_over_abs_mean_b` and `rl_kurtosis_b` kernels are
mathematically undefined at b_size=1 (variance of a single sample is
zero; kurtosis of a single sample is 0/0). The kernels correctly
returned 0 in that case but the downstream `rl_rollout_steps` and
`rl_per_alpha` controllers then never saw signal and pegged at MIN
(2048 / 0.4) for the entire run.

## Fix: time-axis Welford-EMA streaming

Replace per-batch reduction with per-step EMA-streaming moments
maintained in ISV slots:

  rl_var_over_abs_mean_streaming.cu — maintains streaming mean + M2,
  emits var/|mean| each step. Welford-EMA on the batch-mean of
  advantages_d (one value at b_size=1, or a single batch reduction
  at b_size>1) folded into the time-axis estimator.

  rl_kurtosis_streaming.cu — maintains streaming mean + M2 + M4,
  emits M4/M2² (Pearson kurtosis) each step. Same Welford-EMA shape
  applied to td_per_sample_d batch mean.

Both kernels use STREAM_ALPHA = 0.05 (matches LR_LOSS_EMA_ALPHA —
half-life ≈ 14 steps) so the time estimator smooths over noisy
per-step batch-mean observations. The kernel writes the smoothed
estimate DIRECTLY to the controller-input ISV slot
(RL_ADVANTAGE_VAR_RATIO_EMA_INDEX = 421,
 RL_TD_KURTOSIS_EMA_INDEX = 422); the prior downstream
ema_update_per_step calls for these two signals are REMOVED — the
streaming kernel IS the EMA.

## ISV slot allocation

5 new state slots holding the streaming-mean / M2 / M4 per-stream
state. RL_SLOTS_END: 442 → 447.

  RL_ADV_VAR_STREAM_MEAN_INDEX        = 442  (streaming mean of advantages)
  RL_ADV_VAR_STREAM_M2_INDEX          = 443  (streaming M2 of advantages)
  RL_TD_KURT_STREAM_MEAN_INDEX        = 444  (streaming mean of TD-CE)
  RL_TD_KURT_STREAM_M2_INDEX          = 445
  RL_TD_KURT_STREAM_M4_INDEX          = 446

Per `pearl_first_observation_bootstrap`: sentinel-zero state
triggers replace-direct first-observation bootstrap (the first
step seeds μ = batch_mean, M2 = 0, M4 = 0 — subsequent steps blend).

Per `pearl_blend_formulas_must_have_permanent_floor`: var/|mean|
denominator floored at 1e-6, M2² denominator floored at 1e-12 —
prevents div-by-zero blow-up when streaming mean / variance is
genuinely zero (cold-start or quiet regime).

## Files

  * crates/ml-alpha/cuda/rl_var_over_abs_mean_streaming.cu  — new
  * crates/ml-alpha/cuda/rl_kurtosis_streaming.cu           — new
  * crates/ml-alpha/cuda/rl_var_over_abs_mean_b.cu          — deleted
  * crates/ml-alpha/cuda/rl_kurtosis_b.cu                   — deleted
  * crates/ml-alpha/src/rl/isv_slots.rs                     — +5 slots
  * crates/ml-alpha/src/trainer/integrated.rs               — rewired
                                                              launchers,
                                                              dropped
                                                              redundant
                                                              ema_update
                                                              calls
  * crates/ml-alpha/build.rs                                — swapped
                                                              cubin
                                                              entries

## Verified gates (local sm_86)

  G1 isv_bootstrap   
  G3 controllers     
  G4 target_update   
  integrated_smoke   

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 21:53:56 +02:00

106 lines
4.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// rl_kurtosis_streaming.cu — EMA-streaming kurtosis `M4 / M2²` that
// folds ACROSS STEPS instead of across batch.
//
// Replaces `rl_kurtosis_b` for the b_size=1 hot path of the integrated
// RL trainer. The per-batch reduction was mathematically undefined at
// b_size=1 (kurtosis of a single sample is 0/0), so the td_kurtosis
// EMA stayed identically zero forever and the rl_per_alpha controller
// never received signal. Canonical incident: alpha-rl-mjzfk fold0
// (commit 53aeef099) — 100% of 50k steps had td_kurtosis = 0.
//
// Design: maintain three streaming Welford-EMA moments in ISV slots —
// streaming mean (μ_t), M2 (E[(x-μ_t)²]), M4 (E[(x-μ_t)⁴]). Each step:
//
// 1. Compute the current batch's mean x_t.
// 2. Update streaming mean (sentinel-zero first-obs replace):
// μ_t = (1-α) μ_{t-1} + α x_t
// 3. Re-deviate against the UPDATED mean (per Welford):
// δ = x_t μ_t
// 4. Update streaming M2 and M4:
// M2_t = (1-α) M2_{t-1} + α δ²
// M4_t = (1-α) M4_{t-1} + α δ⁴
// 5. Write kurtosis directly to the controller's input slot:
// out_slot = M4_t / (M2_t² + ε)
//
// For a Gaussian distribution, kurtosis = 3.0 (Pearson). Heavy-tailed
// distributions (e.g. TD errors after Q has started learning) have
// kurtosis > 3 — which is what the rl_per_alpha controller cranks up
// PER's priority exponent α to track. By streaming across STEPS the
// kernel sees the actual per-step variation in td_per_sample's batch
// mean, which is a meaningful kurtosis signal even when each batch
// has only one entry.
//
// α = 0.05 — matches rl_var_over_abs_mean_streaming and
// LR_LOSS_EMA_ALPHA for consistency. The downstream
// `ema_update_per_step` call is REMOVED for this signal — the
// streaming kernel writes the smoothed estimate directly to the
// controller's input ISV slot.
//
// Per `pearl_first_observation_bootstrap` + `pearl_blend_formulas_must_have_permanent_floor`:
// sentinel-zero state triggers replace-direct first-observation
// bootstrap; the M2² denominator is permanent-floored at 1e-12 to
// prevent division blow-up when streaming variance is genuinely zero.
//
// Per `pearl_no_atomicadd`: single-thread kernel.
#define STREAM_ALPHA 0.05f
#define M2_SQ_FLOOR 1e-12f
extern "C" __global__ void rl_kurtosis_streaming(
const float* __restrict__ x,
float* __restrict__ isv,
int b_size,
int out_slot,
int mean_slot,
int m2_slot,
int m4_slot
) {
if (threadIdx.x != 0 || blockIdx.x != 0) return;
// Current batch mean (trivial at b_size=1).
float batch_mean = 0.0f;
for (int b = 0; b < b_size; ++b) batch_mean += x[b];
batch_mean /= (float)b_size;
// Streaming mean with first-observation bootstrap.
const float mean_prev = isv[mean_slot];
float mean_new;
if (mean_prev == 0.0f) {
mean_new = batch_mean;
} else {
mean_new = (1.0f - STREAM_ALPHA) * mean_prev
+ STREAM_ALPHA * batch_mean;
}
isv[mean_slot] = mean_new;
// Welford-EMA on δ² and δ⁴ against the UPDATED mean.
const float dev = batch_mean - mean_new;
const float dev2 = dev * dev;
const float dev4 = dev2 * dev2;
const float m2_prev = isv[m2_slot];
float m2_new;
if (m2_prev == 0.0f && mean_prev == 0.0f) {
m2_new = dev2; // first observation
} else {
m2_new = (1.0f - STREAM_ALPHA) * m2_prev
+ STREAM_ALPHA * dev2;
}
isv[m2_slot] = m2_new;
const float m4_prev = isv[m4_slot];
float m4_new;
if (m4_prev == 0.0f && mean_prev == 0.0f) {
m4_new = dev4; // first observation
} else {
m4_new = (1.0f - STREAM_ALPHA) * m4_prev
+ STREAM_ALPHA * dev4;
}
isv[m4_slot] = m4_new;
// Kurtosis = M4 / M2² (Pearson). Permanent-floor on M2² prevents
// div-by-zero before the streaming variance has accumulated.
const float m2_sq = m2_new * m2_new;
isv[out_slot] = m4_new / (m2_sq + M2_SQ_FLOOR);
}