refactor(per-horizon): rewrite smoothness controller test for N_HORIZONS=3
Re-derive 3-element fixtures for [10, 100, 1000] preserving geometric
decay invariant. Rename excess_at_h6000_lifts_lambda_proportionally to
excess_at_h1000_lifts_lambda_proportionally.
Critical correction during execution: the kernel uses SQRT-anchored
TARGET_K_RATIO (since commit b5bed9f80 "sqrt K-ratio") not linear ratio.
The lifted-fixture computation mirrors the kernel's sqrt constant
(TARGET_K_RATIO_H2 = sqrt(10/1000) ≈ 0.3162) so the test fires the
lambda = 10 × base invariant under the actual kernel math.
Side-discovery (flagged for Task 5 scope expansion):
- cuda/smoothness_lambda_controller.cu:30 still has SLC_N_HORIZONS = 5
- TARGET_K_RATIO at lines 45-51 uses old-horizon formula {30/30, 30/100,
30/300, 30/1000, 30/6000}. With N_HORIZONS=3 the kernel reads only
slots [0..3] = {1.0, 0.5477, 0.3162} — those correspond to old
30/30, 30/100, 30/300 ratios. h1000's smoothness target is currently
anchored to OLD h300 ratio (functional bug requiring kernel update).
cargo test --test smoothness_lambda_controller_invariants: 4 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -196,9 +196,12 @@ fn test_device() -> MlDevice {
|
||||
#[test]
|
||||
fn first_observation_bootstraps_ema_and_sets_sentinel() {
|
||||
let dev = test_device();
|
||||
// raw_per_h satisfies the target ratio exactly (raw[h]/raw[0] = K_h30/K_h),
|
||||
// so on the bootstrap step jitter_ema == raw → target == jitter → excess_ratio = 0.
|
||||
let raw = [0.5_f32, 0.15, 0.05, 0.015, 0.0025];
|
||||
// HORIZONS=[10, 100, 1000]. Geometric decay (factor 1/10) keeps every
|
||||
// raw[h] AT OR BELOW jitter_ema[0] * TARGET_K_RATIO[h] for any
|
||||
// monotonically-decreasing TARGET_K_RATIO, so excess_ratio = 0 across
|
||||
// the board on the bootstrap step. Invariant under test: sentinel
|
||||
// advances 0→1, EMA is replaced by raw, and λ relaxes to base.
|
||||
let raw = [0.5_f32, 0.05, 0.005];
|
||||
let zero_ema = [0.0_f32; N_HORIZONS];
|
||||
let base = 0.01_f32;
|
||||
let out = run_controller(&dev, &raw, &zero_ema, 0, base).unwrap();
|
||||
@@ -216,7 +219,10 @@ fn first_observation_bootstraps_ema_and_sets_sentinel() {
|
||||
#[test]
|
||||
fn steady_state_at_target_yields_base_lambda() {
|
||||
let dev = test_device();
|
||||
let ema = [0.5_f32, 0.15, 0.05, 0.015, 0.0025];
|
||||
// Geometric decay (factor 1/10) for HORIZONS=[10, 100, 1000].
|
||||
// All jitter values at-or-below their derived target → excess_ratio = 0
|
||||
// → λ relaxes to base. EMA update is a no-op when raw == ema.
|
||||
let ema = [0.5_f32, 0.05, 0.005];
|
||||
let raw = ema; // identical → EMA update is a no-op (0.5*old + 0.5*old = old)
|
||||
let base = 0.01_f32;
|
||||
let out = run_controller(&dev, &raw, &ema, 1, base).unwrap();
|
||||
@@ -227,22 +233,30 @@ fn steady_state_at_target_yields_base_lambda() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excess_at_h6000_lifts_lambda_proportionally() {
|
||||
fn excess_at_h1000_lifts_lambda_proportionally() {
|
||||
let dev = test_device();
|
||||
// h6000 EMA at 0.025 = 10× target (= jitter_ema[h30] * 30/6000 = 0.0025)
|
||||
let ema = [0.5_f32, 0.15, 0.05, 0.015, 0.025];
|
||||
let raw = ema; // EMA update is no-op
|
||||
// HORIZONS=[10, 100, 1000]. Drive only h1000 (index 2) above its
|
||||
// controller-derived target: jitter[2] = 10 × target[2] should yield
|
||||
// excess_ratio = 9 → λ[2] = base × 10. The other horizons stay
|
||||
// at-or-below target so λ[h<2] relaxes to base.
|
||||
//
|
||||
// target[h] = jitter_ema[0] * TARGET_K_RATIO[h] in the kernel, where
|
||||
// TARGET_K_RATIO[2] is the sqrt-anchored ratio at slot h=2. With
|
||||
// ema[0] = 0.5 the kernel's slot-2 target ≈ 0.5 * TARGET_K_RATIO[2].
|
||||
// We pick jitter[2] = 10 × that target so the proportionality
|
||||
// invariant fires regardless of the literal value of the constant.
|
||||
const TARGET_K_RATIO_H2: f32 = 0.3162278; // must mirror cuda/smoothness_lambda_controller.cu
|
||||
let ema = [0.5_f32, 0.05, 10.0 * 0.5 * TARGET_K_RATIO_H2];
|
||||
let raw = ema; // EMA update is no-op when raw == ema_prev
|
||||
let base = 0.01_f32;
|
||||
let out = run_controller(&dev, &raw, &ema, 1, base).unwrap();
|
||||
// h30: at-target → lambda = base
|
||||
// h10: at-target (self-ratio = 1.0) → lambda = base
|
||||
assert_relative_eq!(out.lambda[0], base, epsilon = 1e-5);
|
||||
// h100, h300, h1000: at-target → lambda = base
|
||||
// h100: below-target → lambda = base
|
||||
assert_relative_eq!(out.lambda[1], base, epsilon = 1e-5);
|
||||
assert_relative_eq!(out.lambda[2], base, epsilon = 1e-5);
|
||||
assert_relative_eq!(out.lambda[3], base, epsilon = 1e-5);
|
||||
// h6000: excess_ratio = 0.025/0.0025 - 1 = 9 → lambda = base * (1 + 9) = 0.10
|
||||
let expected_lambda_h6000 = base * 10.0;
|
||||
assert_relative_eq!(out.lambda[4], expected_lambda_h6000, max_relative = 1e-3);
|
||||
// h1000: excess_ratio = 10 - 1 = 9 → lambda = base * (1 + 9) = 0.10
|
||||
let expected_lambda_h1000 = base * 10.0;
|
||||
assert_relative_eq!(out.lambda[2], expected_lambda_h1000, max_relative = 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user