Files
foxhunt/crates/ml-alpha/cuda/horizon_lambda.cu
jgrusewski 410ab6b0ea arch(ml-alpha): ISV-driven σ + adaptive Z_SCALE — both controllers anchor on loss_ema
Per pearl_controller_anchors_isv_driven, every controller anchor/target/
cap derives from a tracked signal, not hardcoded constants. The σ-only
revert kept Kendall σ as a free Adam-learned scalar — that violated ISV
discipline and fought Adam's m/√v normalization
(pearl_adam_normalizes_loss_weights).

Single source of truth for both per-horizon controllers:

  log_sigma_h[h]  ← max(log(0.5), 0.5 * log(loss_ema[h]))
                    Kendall equilibrium (∂L/∂log σ = 0 ⟹ σ_h² = mean_bce_h)
                    in closed form. Asymmetric floor at log(0.5) prevents
                    collapse. No Adam state, no gradient delay.

  lambda[h]       ← clamp(1.0, 2.0, 1.0 + Z_SCALE_ISV * z_h)
                    Z_SCALE_ISV = (LAMBDA_CEILING - LAMBDA_FLOOR) / z_max_ema
                    Adaptive scale auto-uses the full clamp envelope:
                    the historical-max-z horizon maps exactly to
                    LAMBDA_CEILING. Replaces hardcoded Z_SCALE=0.5 which
                    rarely engaged on real data (max observed λ ~1.04).

Both anchor on the same ISV (loss_ema). z_max_ema is a new single-scalar
EMA state tracking max |z| across horizons, with first-obs bootstrap.

Removes:
  - opt_log_sigma AdamW optimizer (σ no longer learned)
  - grad_log_sigma_h_d memset (BCE kernel writes; output ignored — kept
    only to preserve BCE kernel signature)

Kernel signature change (horizon_ema_and_lambda):
  +z_max_ema [1]       (read+write EMA state)
  +log_sigma_h [5]     (closed-form output, overwrite)

Discipline:
  - First-obs bootstrap (sentinel <= 0) per pearl_first_observation_bootstrap
  - Permanent floor (max(real, floor)) per pearl_blend_formulas_must_have_permanent_floor
  - Asymmetric clamp per pearl_audit_unboundedness_for_implicit_asymmetry
  - Z-score normalisation per pearl_zscore_normalization_for_magnitude_asymmetric_signals
  - No nvrtc, no atomicAdd, no host branches in graph capture

All 9 perception_overfit tests pass — including
horizon_ema_and_lambda_track_after_training which validates the kernel
end-to-end through 64 K-loop iterations of capture/replay.

Submit local smoke; cluster A/B vs σ-only baseline (0.7506/0.7519) and
vs Phase 1+2+3 (0.7749/0.7591) follows once the perf-only 3-fold A/B
confirms no regression at b23f8f2ef.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 19:32:03 +02:00

115 lines
5.2 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.
// horizon_lambda.cu — ISV-driven per-horizon gradient scaler AND closed-form Kendall σ.
//
// Single source of truth for the two per-horizon controllers:
//
// 1) lambda[h] ∈ [LAMBDA_FLOOR, LAMBDA_CEILING] — multiplier on the
// trunk grad_h contribution in heads_grn_bwd. Boosts horizons
// that the model is currently failing to learn (per their
// unweighted BCE EMA).
//
// 2) log_sigma_h[h] — Kendall σ for the BCE forward kernel. Computed
// in closed form from the same loss_ema signal (Kendall's
// gradient equilibrium ⟹ σ_h = sqrt(mean_bce_h)). Replaces
// Adam-learned σ, eliminating the m/√v normalization conflict
// (pearl_adam_normalizes_loss_weights).
//
// ISV anchors per `pearl_controller_anchors_isv_driven`: every anchor/
// target/cap derives from a tracked signal, never from hardcoded
// constants outside the bootstrap epsilons:
//
// loss_ema[h] — EMA(unweighted BCE per horizon)
// z_max_ema — EMA(max |z_h|) across horizons → drives
// adaptive Z_SCALE so the OBSERVED-max-z
// horizon maps exactly to LAMBDA_CEILING.
// With the prior hardcoded Z_SCALE=0.5 the
// controller rarely engaged on real data
// (max observed lambda ~1.04); adaptive
// Z_SCALE uses the full envelope.
//
// Sentinel bootstrap (`prev <= 0`) per
// `pearl_first_observation_bootstrap.md`; permanent floor
// (`max(real, floor)`) per `pearl_blend_formulas_must_have_permanent_floor.md`;
// asymmetric clamp (floor-only on σ, floor + ceiling on λ) per
// `pearl_audit_unboundedness_for_implicit_asymmetry.md`. Z-score
// normalization per `pearl_zscore_normalization_for_magnitude_asymmetric_signals.md`.
#define N_HORIZONS_LAMBDA 5
#define ALPHA_FIXED 0.1f
#define LAMBDA_FLOOR 1.0f
#define LAMBDA_CEILING 2.0f
#define LOG_SIGMA_FLOOR (-0.6931472f) // log(0.5) — σ never collapses below 0.5
#define Z_MAX_FLOOR 0.1f // guards Z_SCALE_ISV when z_max_ema is tiny
extern "C" __global__ void horizon_ema_and_lambda(
const float* __restrict__ loss_per_horizon, // [5] — current step UNWEIGHTED BCE
float* __restrict__ loss_ema, // [5] — EMA state (read + write)
float* __restrict__ z_max_ema, // [1] — EMA(max|z|) state (read + write)
float* __restrict__ lambda, // [5] — output: λ_h
float* __restrict__ log_sigma_h // [5] — output: closed-form Kendall σ
) {
if (threadIdx.x != 0 || blockIdx.x != 0) return;
// -------- 1) EMA update on loss_per_horizon, with first-obs bootstrap.
float new_ema[N_HORIZONS_LAMBDA];
float sum = 0.0f;
#pragma unroll
for (int h = 0; h < N_HORIZONS_LAMBDA; ++h) {
const float cur = loss_per_horizon[h];
const float prev = loss_ema[h];
new_ema[h] = (prev <= 0.0f) ? cur : (prev + ALPHA_FIXED * (cur - prev));
loss_ema[h] = new_ema[h];
sum += new_ema[h];
}
const float mean = sum / (float)N_HORIZONS_LAMBDA;
// -------- 2) Z-score normalize across horizons.
float ssq = 0.0f;
#pragma unroll
for (int h = 0; h < N_HORIZONS_LAMBDA; ++h) {
const float diff = new_ema[h] - mean;
ssq += diff * diff;
}
const float std = sqrtf(ssq / (float)N_HORIZONS_LAMBDA + 1e-12f);
const float inv_std = (std > 1e-6f) ? (1.0f / std) : 0.0f;
float z[N_HORIZONS_LAMBDA];
float z_max = 0.0f;
#pragma unroll
for (int h = 0; h < N_HORIZONS_LAMBDA; ++h) {
z[h] = (new_ema[h] - mean) * inv_std;
const float az = fabsf(z[h]);
if (az > z_max) z_max = az;
}
// -------- 3) z_max_ema: tracks the typical spread, drives adaptive Z_SCALE.
// Sentinel = 0 ⇒ first-obs bootstrap. Fixed α matches loss_ema's
// smoothing horizon.
const float prev_zmax = z_max_ema[0];
const float new_zmax = (prev_zmax <= 0.0f)
? z_max
: (prev_zmax + ALPHA_FIXED * (z_max - prev_zmax));
z_max_ema[0] = new_zmax;
// -------- 4) Adaptive Z_SCALE: maps the historical-max-z to LAMBDA_CEILING.
// When z_max_ema is large (spread regime), Z_SCALE_ISV shrinks
// to avoid saturating early; when small (tight regime), it
// grows to amplify the controller into engagement.
const float z_anchor = fmaxf(new_zmax, Z_MAX_FLOOR);
const float z_scale_isv = (LAMBDA_CEILING - LAMBDA_FLOOR) / z_anchor;
// -------- 5) Per-horizon outputs.
#pragma unroll
for (int h = 0; h < N_HORIZONS_LAMBDA; ++h) {
// λ_h: boost-only, asymmetric clamped.
const float raw_lambda = 1.0f + z_scale_isv * z[h];
lambda[h] = fmaxf(LAMBDA_FLOOR, fminf(LAMBDA_CEILING, raw_lambda));
// log σ_h: Kendall equilibrium from the same loss_ema signal.
// ∂L/∂log σ = 0 ⟹ σ_h² = mean_bce_h ⟹ log σ_h = ½·log(loss_ema_h).
// Floor at log(0.5) so σ can't collapse to 0 (which would explode w_h).
const float ema_h = fmaxf(new_ema[h], 1e-12f);
const float raw_log_sigma = 0.5f * logf(ema_h);
log_sigma_h[h] = fmaxf(LOG_SIGMA_FLOOR, raw_log_sigma);
}
}