Files
foxhunt/crates/ml-alpha/cuda/horizon_lambda.cu
jgrusewski 89a5d0b203 refactor(per-horizon): N_HORIZONS 5→3 — horizon_lambda + bce_loss + smoothness kernels
Three CUDA kernels + atomically-coupled Rust consumer:
- horizon_lambda.cu: N_HORIZONS_LAMBDA 5→3
- bce_loss_multi_horizon.cu: BCS_N_HORIZONS 5→3
- smoothness_lambda_controller.cu: SLC_N_HORIZONS 5→3 AND TARGET_K_RATIO
  rebased from old-horizon {30,100,300,1000,6000} sqrt formula to new
  {10,100,1000} → {1.0, 0.3162, 0.1}. Payload size 10 → 6 (2×N_HORIZONS).
- gpu_log.rs: payload_json decoders for RT_INPUT, RT_STATE, RT_OUTPUT
  records updated to 3-horizon field names (h30..h6000 → h10/h100/h1000)
  per feedback_no_partial_refactor.

Bucket-coupled kernels (bucket_transition, cfc_step_per_branch,
heads_block_diagonal_fwd, multi_horizon_heads) STILL HAVE N_HORIZONS=5
and bucket geometry constants. Next commit migrates those — they share
memory layout with Rust-side bucket_routing.rs which is already at
N_HORIZONS=3 / MAX_BUCKET_DIM=96, so kernel-side mismatch would be
silent data corruption at runtime.

decision_policy.cu N_HORIZONS comes from lob_state.cuh — Task 6 scope.

cargo build -p ml-alpha and -p ml-backtesting: cubins rebuild PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:14:09 +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 3
#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, // [3] — current step UNWEIGHTED BCE
float* __restrict__ loss_ema, // [3] — EMA state (read + write)
float* __restrict__ z_max_ema, // [1] — EMA(max|z|) state (read + write)
float* __restrict__ lambda, // [3] — output: λ_h
float* __restrict__ log_sigma_h // [3] — 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);
}
}