Files
foxhunt/docs/superpowers/specs/2026-05-21-crt-train-isv-driven-lambda-controller.md
jgrusewski ed34d356a8 docs(per-horizon-cfc): kickoff — spec + plan + historical record
Spec: docs/superpowers/specs/2026-05-21-per-horizon-cfc-inference-design.md
Plan: docs/superpowers/plans/2026-05-21-per-horizon-cfc-inference-plan.md

Spec went through 2 critical-review passes (32 total findings, all resolved).
Bucketing source: CfC.tau (per-channel, trained, log-uniform init at HIDDEN_DIM=128).
Atomic refactor reverts MTER scaffolding. 5 ISV-driven controllers. All-on-device
transition (no bulk DtoH). Single fused per-branch kernel. Compact ragged heads_w_skip.
Validated via 3 Argo smokes (training stability, CRT.diag inference differentiation,
fxt-backtest end-to-end).

Also committing historical record: superseded MTER spec/plan, intervention A
plan, ISV λ controller spec/plan, GPU log ring spec/plan. Per spec §5.3 these
stay as audit trail; not in build path.

Plan has 18 tasks, full TDD with bite-sized steps, ready for
subagent-driven-development execution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:05:28 +02:00

7.8 KiB
Raw Blame History

CRT.train Output-Smoothness — ISV-Driven λ Controller (Spec Extension)

Date: 2026-05-21 Branch context: ml-alpha-phase-a (HEAD 70ecfc0fe; static-λ implementation shipped in 10 commits) Predecessor spec: 2026-05-20-crt-train-output-smoothness-design.md Status: Design — extends the predecessor by replacing the static SMOOTHNESS_LAMBDA_RATIO with a signal-driven controller anchored on observed h30 jitter.


1. Motivation

The predecessor spec ships λ[h] = smoothness_base_lambda × SMOOTHNESS_LAMBDA_RATIO[h] where the ratio array [1, 3.33, 10, 33.33, 200] is a tuned constant. This violates two memory rules:

The K-ratio assumption (h6000 should change 200× more slowly than h30) is probably right but the model's intrinsic noise (the observed h30 jitter) is what we should anchor on, not the abstract ratio of horizon lengths.

2. Hypothesis

Anchor target per-horizon on the OBSERVED h30 jitter EMA, scale down per horizon by K_h30 / K_h. Drive λ[h] to push observed jitter toward target. Use a permanent floor so the controller never fully disables.

3. Architectural Intervention

3.1 State (new buffers on PerceptionTrainer)

  • jitter_ema_d: CudaSlice<f32> shape [5] — running EMA of raw_per_h.
  • jitter_first_obs_d: CudaSlice<i32> shape [1] — sentinel 0 → 1 on first observation (per pearl_first_observation_bootstrap).

smoothness_lambda_d keeps its name but its semantics change: it is no longer uploaded once at construction; it is updated each step inside the captured graph.

3.2 New CUDA kernel cuda/smoothness_lambda_controller.cu

extern "C" __global__ void smoothness_lambda_controller(
    const float* raw_per_h,        // [5]  emitted by output_smoothness_loss_and_grad
    float* jitter_ema,              // [5]  in/out — EMA state
    int*   first_obs,               // [1]  in/out — sentinel
    float  base_lambda,             // scalar — amplitude knob from cfg.smoothness_base_lambda
    float* lambda_out               // [5]  new λ values for the kernel's NEXT step
);

Math (per thread h ∈ {0..4}, single block of 5 threads):

// 1. EMA update with sentinel bootstrap.
if (first_obs[0] == 0):
    jitter_ema[h] = raw_per_h[h]
    if (h == 0): first_obs[0] = 1
else:
    // Fixed-α floor at 0.5 (Wiener-α floor for non-stationary loops
    // per pearl_wiener_alpha_floor_for_nonstationary). The target
    // drifts as the policy co-adapts, so MSE-optimal α isn't right.
    jitter_ema[h] = 0.5 * jitter_ema[h] + 0.5 * raw_per_h[h]

__syncthreads()    // all threads need jitter_ema[0] for target derivation

// 2. Target = ISV-anchored on observed h30 jitter.
//    target[0] = jitter_ema[0]                              ← h30 self-targets
//    target[h] = jitter_ema[0] * (HORIZONS[0] / HORIZONS[h]) for h ≥ 1
float target_h
if (h == 0):
    target_h = jitter_ema[0]
else:
    target_h = jitter_ema[0] * (HORIZONS[0] / HORIZONS[h])

// 3. λ controller: push UP when observed > target; relax to floor otherwise.
float excess_ratio = max(0.0, jitter_ema[h] / max(target_h, 1e-9) - 1.0)
float lambda_new = base_lambda * (1.0 + excess_ratio)
lambda_out[h] = max(LAMBDA_FLOOR, lambda_new)

where LAMBDA_FLOOR = 1e-4 — the permanent floor (per pearl_blend_formulas_must_have_permanent_floor: max(real, floor), never blend).

3.3 Launch order in step_batched

Replace the current sequence:

... → BCE → output_smoothness → backward K-loop

with:

... → BCE → output_smoothness → smoothness_lambda_controller → backward K-loop

The controller runs AFTER output_smoothness (which emits raw_per_h) and BEFORE the backward K-loop. The updated smoothness_lambda_d is used by the NEXT step's output_smoothness launch — one-step delay, standard closed-loop pattern.

3.4 Removals

  • pub const SMOOTHNESS_LAMBDA_RATIO: [f32; N_HORIZONS] in heads.rs — DELETED (no longer load-bearing; ratio is now derived from HORIZONS).
  • The constructor's λ pre-multiplication block in perception.rs — REPLACED with λ_init = [LAMBDA_FLOOR; 5] upload.

The cfg.smoothness_base_lambda knob stays — it's the amplitude (controller equilibrium value when excess_ratio == 0).

4. Implementation Surface

File Change
crates/ml-alpha/cuda/smoothness_lambda_controller.cu NEW
crates/ml-alpha/build.rs Add "smoothness_lambda_controller" to KERNELS, bump cache-bust v12 → v13
crates/ml-alpha/src/heads.rs DELETE SMOOTHNESS_LAMBDA_RATIO const
crates/ml-alpha/src/trainer/perception.rs Add jitter_ema_d, jitter_first_obs_d, controller fn handle + module; init λ to LAMBDA_FLOOR; launch controller in step_batched
crates/ml-alpha/tests/output_smoothness_grad_finite_diff.rs NO change — existing tests still valid (they test the smoothness kernel; the controller is separate)
crates/ml-alpha/tests/smoothness_lambda_controller_invariants.rs NEW — invariant tests for the controller

The CLI flag --smoothness-base-lambda and the Argo plumbing stay unchanged.

5. Validation Gate Update

Spec §5 MUST/WIN/STRETCH gates unchanged. ADD a controller-specific invariant test:

Controller MUST gates (unit tests, no training run needed):

  1. First-observation sentinel: first_obs == 0 → kernel sets jitter_ema = raw_per_h, first_obs = 1, λ = [LAMBDA_FLOOR; 5].
  2. Steady-state symmetry: if raw_per_h[h] / raw_per_h[0] == HORIZONS[0] / HORIZONS[h] for all h, the controller emits λ[h] = base_lambda exactly (excess_ratio = 0 everywhere).
  3. Excess response: if jitter_ema[h6000] is 10× its target, λ[h6000] = base_lambda * 10.
  4. Floor invariant: when base_lambda = 0, λ[h] = LAMBDA_FLOOR for all h. The kernel does NOT collapse to producing zero loss + zero grad (because LAMBDA_FLOOR > 0); that's an intentional change vs the static-λ spec — the floor is a safety net.

If you want the "static λ=0 is a no-op" property back (kernel literally produces nothing), set LAMBDA_FLOOR = 0.0 — but then the controller can self-disable and that violates the user's "with floor" requirement. Resolution: LAMBDA_FLOOR = 1e-4 chosen so the residual smoothing gradient is empirically negligible (~2 × 1e-4 × (Δp)² / N_pairs1e-7 per gradient entry — well below f32 grad noise).

6. Open Questions

  1. First-observation sentinel race? With 5 threads writing first_obs after __syncthreads(), only thread h == 0 writes the sentinel — single writer, no race. Verify in code.

  2. Should target_h for h=0 always equal jitter_ema[0] (self-target, controller passive)? Yes per design; h30 has no slower horizon to anchor on, so it tracks the amplitude knob directly. excess_ratio[0] = 0 always.

  3. max(target_h, 1e-9) epsilon — is this a tuned constant? Technically yes, but it's a denominator-safety epsilon, not a behavioral knob. Per existing codebase convention (e.g., REGIME_EPS = 1e-6 in data/loader.rs), epsilon constants are exempt from the no-tuned-constants rule.

7. Backward Compatibility

The CLI flag --smoothness-base-lambda stays — operator can still sweep it for amplitude. Default 0.0 used to be "kernel produces zero loss/grad"; now it means "controller maintains λ = LAMBDA_FLOOR for all h." Behaviorally near-equivalent (LAMBDA_FLOOR is tiny) but not literally zero. If literally-zero behavior is required, set LAMBDA_FLOOR = 0.0 — but this conflicts with the user's "with floor" requirement.


End of spec extension. Awaiting plan and execution.