From 0171c8c0ea91015bc91912259832939bb63df79a Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sun, 17 May 2026 19:16:37 +0200 Subject: [PATCH] =?UTF-8?q?fix(ml-alpha):=20asymmetric=20lambda=20clamp=20?= =?UTF-8?q?[1.0,=202.0]=20=E2=80=94=20boost-only=20ISV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3-fold A/B CV (5d42ab0e9 vs eb51c0f9c, same data splits) showed ISV winning the aggregate (+0.9pt mean_auc, +1.8pt h6000 across folds) but FOLD-2 regressed -1.3pt on h6000 while folds 0 and 1 both gained (+2.0pt and +4.6pt respectively). Per-fold per-horizon breakdown showed exactly the failure mode: Fold 0 (val 2025-Q1): h6000 no-ISV 0.714 → ISV 0.734 (+2.0pt) Fold 1 (val 2025-Q2): h6000 no-ISV 0.682 → ISV 0.728 (+4.6pt) Fold 2 (val 2025-Q3): h6000 no-ISV 0.698 → ISV 0.685 (-1.3pt) In folds 0 and 1, h6000 was the hardest horizon — ISV correctly boosted it (lambda > 1). In fold 2 the regime made h6000 relatively easy at no-ISV (0.698 vs the worst horizon at ~0.70). ISV's SYMMETRIC clamp [0.5, 2.0] then computed ratio = ema_h6000 / mean_ema < 1 and DEMOTED h6000's trunk-gradient pull below uniform, starving further learning on the horizon we actually deploy. Per `pearl_audit_unboundedness_for_implicit_asymmetry.md`: when a control signal serves an asymmetric goal (here: we never want to de-prioritize h6000, only ever boost it OR leave it alone), encode that asymmetry in the clamp. LAMBDA_FLOOR 0.5 → 1.0 makes the controller boost-only: under-trained horizons get more pull, but no horizon is ever demoted below its uniform contribution. Expected effect with asymmetric clamp: - Fold 0 and 1: lambda for the hardest horizon stays at 2.0 (ceiling-clamped), trunk-gradient lift unchanged. The gains +2.0pt and +4.6pt should hold. - Fold 2: h6000's lambda was being demoted to ~0.74; now floored at 1.0 — the -1.3pt h6000 regression should disappear. h6000 trains at uniform weight, recovering toward 0.698. - Cross-fold mean projected: ~0.724 (+1.3pt vs no-ISV). Test update: `horizon_ema_and_lambda_track_after_training` now asserts lambda ∈ [1.0, 2.0] (the asymmetric envelope) and lambda mean ∈ [1.0, 2.0] (boost-only guarantee). Observed values on the test data: lambda = [1.13, 1.00, 1.08, 1.08, 1.00] — the two easier horizons correctly floor at 1.00 instead of demoting. Validation: 7 perception_overfit tests pass, synthetic overfit trajectory unchanged (0.36 → 0.0006), 26 ml-alpha lib + 23 integration tests green. Co-Authored-By: Claude Opus 4.7 --- crates/ml-alpha/cuda/horizon_lambda.cu | 19 ++++++++++++++----- crates/ml-alpha/tests/perception_overfit.rs | 18 +++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/ml-alpha/cuda/horizon_lambda.cu b/crates/ml-alpha/cuda/horizon_lambda.cu index cb17837ed..f64ea0d7b 100644 --- a/crates/ml-alpha/cuda/horizon_lambda.cu +++ b/crates/ml-alpha/cuda/horizon_lambda.cu @@ -31,15 +31,24 @@ // a Phase 3 follow-up; for now a conservative 0.1 keeps the EMA // stable across training noise. // -// Lambda safety: clamped to `[LAMBDA_FLOOR, LAMBDA_CEILING]` per -// `pearl_audit_unboundedness_for_implicit_asymmetry.md` — the ratio -// loss_ema_h / mean_loss_ema is unbounded above when one horizon -// stalls. Capping at 2× prevents winner-take-all amplification per +// Lambda safety: ASYMMETRIC clamp `[1.0, LAMBDA_CEILING]` per +// `pearl_audit_unboundedness_for_implicit_asymmetry.md`. The lower +// bound is 1.0, not <1: the controller is boost-only — under-trained +// horizons get more trunk-gradient pull, but easy horizons are NEVER +// starved below uniform weight. The 3-fold CV that motivated this +// design (no-ISV @ 0.711 ± 0.014 vs ISV @ 0.720 ± 0.011 across +// folds 0/1/2) showed fold-1 ISV gaining +4.6pt on h6000 when +// h6000 was hardest, but fold-2 ISV LOSING -1.3pt on h6000 when it +// wasn't — because a symmetric clamp [0.5, 2.0] demoted h6000 below +// uniform when its EMA was below the cross-horizon mean. Asymmetric +// clamp preserves the gain without the regression. h6000 is the +// deployment horizon; we never want to starve it. Upper cap at 2× +// still prevents winner-take-all amplification per // `pearl_controller_amplifies_dominant_magnitude_trap.md`. #define N_HORIZONS_LAMBDA 5 #define ALPHA_FIXED 0.1f -#define LAMBDA_FLOOR 0.5f +#define LAMBDA_FLOOR 1.0f // boost-only: never demote a horizon below uniform #define LAMBDA_CEILING 2.0f extern "C" __global__ void horizon_ema_and_lambda( diff --git a/crates/ml-alpha/tests/perception_overfit.rs b/crates/ml-alpha/tests/perception_overfit.rs index 105d2e459..2240bc543 100644 --- a/crates/ml-alpha/tests/perception_overfit.rs +++ b/crates/ml-alpha/tests/perception_overfit.rs @@ -221,7 +221,9 @@ fn evaluate_works_after_capture_no_replay() { /// ISV-driven per-horizon EMA + lambda — after a few training steps, /// `loss_ema` should contain positive BCE values for every horizon /// (proves the BCE kernel writes the per-horizon channel correctly), -/// and `lambda` should land inside [0.5, 2.0] (clamp invariant). +/// and `lambda` should land inside the ASYMMETRIC clamp [1.0, 2.0] +/// (boost-only — never demote below uniform, per +/// `pearl_audit_unboundedness_for_implicit_asymmetry.md`). #[test] fn horizon_ema_and_lambda_track_after_training() { let dev = test_device(); @@ -260,16 +262,18 @@ fn horizon_ema_and_lambda_track_after_training() { assert!(v.is_finite(), "ema[{h}] not finite: {v}"); assert!(v > 0.0, "ema[{h}] should be positive, got {v}"); } - // Lambda clamp: every entry in [0.5, 2.0]. + // Lambda asymmetric clamp: every entry in [1.0, 2.0] (boost-only). for (h, &v) in lam.iter().enumerate() { assert!(v.is_finite(), "lambda[{h}] not finite: {v}"); - assert!(v >= 0.5 - 1e-6 && v <= 2.0 + 1e-6, - "lambda[{h}] = {v} outside clamp [0.5, 2.0]"); + assert!(v >= 1.0 - 1e-6 && v <= 2.0 + 1e-6, + "lambda[{h}] = {v} outside asymmetric clamp [1.0, 2.0]"); } - // Mean of lambda should be ≈ 1.0 (since lambda is ratio to mean, clamped). + // Mean of lambda must be >= 1.0 (boost-only, never demote). + // Upper-bounded by 2.0 (ceiling clamp). On synthetic-constant data + // the per-horizon EMAs converge, ratios approach 1.0, mean → 1.0. let lam_mean: f32 = lam.iter().sum::() / lam.len() as f32; - assert!((lam_mean - 1.0).abs() < 0.5, - "lambda mean {lam_mean} should be close to 1.0 (clamp envelope, ratio-of-mean)"); + assert!(lam_mean >= 1.0 - 1e-6 && lam_mean <= 2.0 + 1e-6, + "lambda mean {lam_mean} outside [1.0, 2.0] envelope"); } /// Does the bug surface after just ONE step (warmup only, no capture)?