fix(ml-alpha): asymmetric lambda clamp [1.0, 2.0] — boost-only ISV

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 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-17 19:16:37 +02:00
parent 5d42ab0e98
commit 0171c8c0ea
2 changed files with 25 additions and 12 deletions

View File

@@ -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(

View File

@@ -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::<f32>() / 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)?