Commit Graph

45 Commits

Author SHA1 Message Date
jgrusewski
71b467be40 chore(ml-alpha): remove V1-forward test files (broken since X8 reshape)
Both tests exercised CfcTrunk::capture_graph_a + perception_forward_captured
+ snapshot_hidden, which feed V1-shaped CfC weights. After X8 the trunk's
CfC was reshaped to v2 layout (cfc_n_in=HIDDEN_DIM); the V1 forward path
now feeds FEATURE_DIM input into HIDDEN_DIM-shaped CfC — runtime garbage.

The methods themselves are still in trunk.rs (marked dead-code by rustc).
A deeper cleanup pass — deleting the V1 weight fields (heads_w_d, proj_*),
V1 per-step scratches, V1 cubin function handles, and the V1 forward
methods themselves — is a follow-up commit when fresh.

Verification: ml-alpha + ml-backtesting + fxt-backtest all build clean.
2026-05-19 09:08:04 +02:00
jgrusewski
b47b2fabfb fix(ml-core): deterministic GPU weight init via scoped_init_seed
ml_core::cuda_autograd::init::generate_uniform (backing xavier_uniform,
kaiming_uniform, bias_uniform, near_zero_xavier) defaulted to seeding
from SystemTime::now() + thread_id, producing non-reproducible weights
across processes. Mamba2 stacks initialise via OwnedGpuLinear::xavier,
which routes through this helper — so PerceptionTrainer.evaluate output
diverged 5-30% across fresh-process runs with identical cfg.seed.

Fix: thread-local seedable RNG override. New API:

    let _g = ml_core::cuda_autograd::init::scoped_init_seed(seed);
    // ... all xavier/kaiming/bias/near_zero calls draw from
    //     StdRng::seed_from_u64(seed) chain while _g is alive ...
    // _g dropped here -> restores default time-based seeding

PerceptionTrainer::new now installs the guard before any Mamba2Block
construction, so the trainer is reproducible from cfg.seed end-to-end.
CfC/VSN/heads already used explicit ChaCha8Rng::seed_from_u64 — only
Mamba2 was affected.

Production behavior unchanged when no guard is set. ml-core: 306 tests
pass, ml-alpha: 34 lib tests pass.

Regression test: crates/ml-alpha/tests/perception_forward_golden.rs
captures bit-exact PerceptionTrainer.evaluate output (loss + 160 probs
on a deterministic seed=42 fixture) into a 644-byte golden file.
Three consecutive runs now produce max_abs_diff=0; pre-fix runs varied
by 0.1-0.3 absolute on individual probs.

.gitignore: added exception for crates/ml-alpha/tests/fixtures/*.bin
so deterministic test fixtures land in repo.

Per pearl_scoped_init_seed_for_reproducibility in project memory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:19:13 +02:00
jgrusewski
e004d6c217 Revert "arch(ml-alpha): restore count-delta redundancy at feature slot [26]"
This reverts commit 008f65d894.
2026-05-18 23:16:38 +02:00
jgrusewski
008f65d894 arch(ml-alpha): restore count-delta redundancy at feature slot [26]
The C16 tick-rule swap (19986c8d9) replaced the dense `trade_count` delta
at snap_feature_assemble's slot [18] with a signed L1 tick-rule estimate.
That broke a load-bearing redundancy in Phase 1+2+3:

  Phase 1+2+3 feature [17] = log1p(trade_count_delta)
  Phase 1+2+3 feature [18] = signed_log1p(trade_count_delta)
                           = log1p(trade_count_delta) for count ≥ 0

Within a file, `cur.trade_count >= prev.trade_count` (monotonic), so the
delta is non-negative and the two features were *bit-identical* floats.
The encoder's Mamba2 W_in had two random-initialised rows projecting the
same dense signal, giving it effective 2× capacity allocation on
trade-flow.

Post-C16:
  feature [17] = log1p(trade_count_delta)         (unchanged)
  feature [18] = signed_log1p(tick_rule_estimate) (new, uncorrelated)

Empirical (7.8M ES MBP-10 snapshots, Q1+Q2 2024 production data):

  | Stat                   | OLD count_delta | NEW tick_rule   |
  |------------------------|-----------------|-----------------|
  | zero rate              | 10.2%           | 49.1%           |
  | mean ± std             | 4.26 ± 3.82     | -0.18 ± 10.95   |
  | max |value|            | 100             | 3378            |
  | Pearson r vs count     | 1.0000 (id)     | 0.0002          |

  Sign-class breakdown vs Phase 1+2+3 slot [18]:
    44.3% new=0 but old≠0  (44% of true trades MISSED by tick-rule)
     5.5% new≠0 but old=0  (cancel-as-trade false positives)
    22.8% both positive    (agree)
     0.0% both negative    (old never negative)
    22.6% sign disagreement (old saw trades, new says "seller")

The tick-rule heuristic is a strictly different (and noisier) signal,
not a superset. Three mechanisms simultaneously regressed mean_auc:
  A) lost 2× W_in capacity on count signal
  B) noisier signal at slot [18]
  C) extreme outliers (max 33× wider) destabilise LayerNorm at [18]

Fix (Option 2 per the diagnostic):

  out[26] = signed_log1p((float) trade_count)

Restores the duplicate count-delta signal at a previously-reserved slot.
Slot [18] keeps the new tick-rule signal — the 5.5% "signal added" and
the directional info at L1 are still available. FEATURE_DIM (40) is
unchanged; LayerNorm + Mamba2 W_in dimensions are unchanged.

Both the single-snapshot kernel and the batched kernel are updated.
`snap_feature_bit_equiv::reserved_slots_are_zero` updated to assert
the new slot-26 semantics (signed_log1p of synthetic trade_count=7
= log(8) ≈ 2.079).

All 9 perception_overfit tests pass + all 9 snap_feature_bit_equiv
tests pass.

Cluster verification: single-fold smoke + 3-fold validation follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:19:23 +02:00
jgrusewski
1a465cf7d5 refactor(ml-alpha): revert axes B/C/D/E — keep only Kendall σ (axis A)
Post-A/B verdict (see project_ml_alpha_v2_ab_verdict.md): v2 with all
5 axes was marginally tied on h6000 (+0.0013 vs 0.7591 baseline mean,
fails the +0.01 win threshold) and slightly below on mean_auc
(−0.0208 vs 0.7749 baseline mean, within 1σ) at ~5× the wall-time
cost. Per `feedback_v7_gem_methodology` (measure before delete or
wire), the architecture has been measured — it doesn't earn its
compute cost. This commit reverts the axes that didn't lift:

  - axis B (L2 anchor + Wiener-α controller) — DROPPED
  - axis C (horizon-token attention pool)    — DROPPED
  - axis D (regime-MoE gate + experts)       — DROPPED
  - axis E (inverted cross-variate attn)     — DROPPED
  - axis A (Kendall σ-weighted BCE)          — KEPT

Files deleted (kernels, host bindings, numgrad tests, trainer state):
  - cuda/{horizon_token_attention_pool, inverted_attention_pool,
          inv_pooled_merge, regime_moe_gate, anchor_l2,
          horizon_mean_collapse}.cu
  - src/{horizon_token_attention_pool, inverted_attention_pool,
         inv_pooled_merge, regime_moe_gate, anchor_l2,
         horizon_mean_collapse}.rs
  - src/trainer/{multi_horizon_attention, anchor_controller}.rs
  - tests/{horizon_token_attention_pool_numgrad,
           inverted_attention_pool_numgrad,
           regime_moe_gate_numgrad,
           anchor_l2_numgrad}.rs

Files restored (from V1 commit 41292303d):
  - cuda/attention_pool.cu — legacy single-Q attention pool kernel
  - src/trainer/perception.rs — pre-MHA trainer state with the
    legacy `attn_*` plumbing intact.

Files modified:
  - bce_loss_multi_horizon.cu stays σ-aware (kept the V7 work; it
    has the kernel function name preserved from V1).
  - perception.rs: ADD `log_sigma_h_d [N_HORIZONS]`,
    `grad_log_sigma_h_d [N_HORIZONS]`, `opt_log_sigma` AdamW
    directly on PerceptionTrainer (no MHA bundle). BCE callsites in
    `step_batched` (training) and `evaluate_batched` thread the σ
    args. Grad scratch zeroed each step before the BCE launch.
    `opt_log_sigma.step` lives in section 9 alongside the other
    AdamW updates.

NET DIFF: 23 files, 442 insertions, 3160 deletions (~2700-line
cleanup).

LOCAL VERIFICATION (RTX 3050 sm_86, --test-threads=1):
  - ml-alpha builds clean (cuda feature)
  - bce_grad_finite_diff 4/4 PASS (BCE still works through σ-kernel)
  - perception_overfit 9/9 PASS (full trainer pipeline, loss-shrinks
    tests still green)

NEXT: cluster smoke + 3-fold A/B vs task #200 baseline. Expected
wall-time ≈ baseline 17 s/epoch (we're back to baseline architecture
plus 5 scalar Kendall σ params + 1 tiny AdamW). Expected lift on
mean_auc: modest — Kendall σ rebalances per-horizon contributions
based on observed BCE EMA, which may help horizons with intrinsically
higher noise floors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 18:11:05 +02:00
jgrusewski
11b964359b perf(ml-alpha): MoE bwd compact scratch + scatter kernel + inv-attn loop interchange
Two perf optimizations bundled:

(1) MoE backward scratch compaction
    Old: `grad_w_scratch_d [B, N_H, N_E, H, H]` = 1.3 MB per step (B=1)
    New: `grad_w_scratch_d [B, N_H, H, H]`      = 320 KB per step
    4× memory reduction. Since each batch's top-1 router selects ONE
    expert, only that expert's slot was ever non-zero in the prior
    layout — the N_E axis was entirely wasteful.

    The shape change required:
    - Updated `regime_moe_gate_bwd` to write the compact layout.
    - New `regime_moe_gate_scatter` kernel scatters per-(b, h)
      rank-1 contributions into `grad_experts_W[e]` / `grad_experts_b[e]`
      based on `top_e[b]`. Grid (N_E, H, ceil(H/32)) × block (32) —
      one warp per (e, d_out, d_in_chunk). 65536 → 16384 grid cells
      (4× fewer blocks dispatched).
    - Dropped the previously-naive 65536-block `reduce_axis0` for
      `grad_experts_w` from `perception.rs` (the scatter kernel
      produces the final per-expert grad directly).
    - `tests/regime_moe_gate_numgrad.rs` reads `grad_experts_w` from
      the scatter output instead of host-side reducing the 5D scratch.

(2) inverted_attention bwd loop interchange
    Phase 2's tight loop:
      for k:
        for j:
          ds_myh_j = d_scores[my_h, j]   // doesn't depend on k!
          ds_j_myh = d_scores[j, my_h]   // doesn't depend on k!
          ...
    Hoisted d_scores reads out of the K-loop into J-outer with
    per-thread `q_arr[K_MAX]` / `k_arr[K_MAX]` register accumulators.
    Net: 32× fewer DRAM reads of d_scores per thread per bwd.

CORRECTNESS:
  - regime_moe_gate numgrad PASSES (1/1, 11 numgrad checks).
  - inverted_attention numgrad PASSES (1/1, 6 numgrad checks).
  - perception_overfit 9/9 PASS — including loss-shrinks tests.

NEXT: re-run cluster smoke to measure the new wall-time vs the 17 s
baseline. Prior smoke at a263cd544 was 11.26 s for 1000 steps; this
commit's smoke will reveal whether the MoE scratch compaction + loop
interchange land us closer to the 30 s/epoch gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:03:21 +02:00
jgrusewski
6a3f45d872 refactor(ml-alpha): consolidate BCE — Kendall σ is THE BCE, wire MHA into trainer
Single source of truth for the multi-horizon BCE per the new
feedback_single_source_of_truth_no_duplicates pearl. Eliminates the
`bce_loss_multi_horizon_sigma.cu` / `loss_sigma.rs` duplication
introduced earlier today and folds the Kendall σ-weighting into the
canonical kernel.

Deletions:
  cuda/bce_loss_multi_horizon_sigma.cu              (folded into legacy)
  src/trainer/loss_sigma.rs                         (helper merged)
  tests/bce_sigma_numgrad.rs                        (subsumed)

Rewrites:
  cuda/bce_loss_multi_horizon.cu — replaced with σ-aware
    implementation; kernel function name kept as
    `bce_multi_horizon_forward_backward` so cubin symbol stays stable.
    NVIDIA-grade warp-shuffle reduce (4 warps, 1 cross-warp barrier);
    new args `log_sigma_h` (per-horizon Kendall σ logarithm) and
    `d_log_sigma_h` (its gradient).
  src/trainer/loss.rs — standalone helper updated to new 11-arg
    kernel signature. Exposes optional `log_sigma_h` in `BceInput`
    (None → zeros / passthrough Kendall init) and returns
    `mean_bce_per_h` + `d_log_sigma_h` in `BceOutput`.
  tests/bce_grad_finite_diff.rs — adds `log_sigma_h: None` to the
    test inputs; all 4 numgrad tests PASS unchanged.
  build.rs — drops `bce_loss_multi_horizon_sigma` entry from
    KERNELS. The single canonical `bce_loss_multi_horizon` cubin
    now contains the σ-aware kernel.

Wiring:
  PerceptionTrainer gains a single `pub mha: MultiHorizonAttention`
    field. Owns `log_sigma_h_d` + `grad_log_sigma_h_d` (and the rest
    of the multi-horizon attention path, anchored on Stage 2 to fully
    replace the legacy `attention_pool` path).
  step_batched + evaluate_batched BCE callsites now thread
    `mha.log_sigma_h_d` and `mha.grad_log_sigma_h_d` through the
    11-arg kernel signature.

This commit keeps the legacy `attention_pool` callsite in place; the
Stage 2 commit will replace it with `mha.pool` + `mha.inv_pool` +
`mha.moe` and delete the `attn_*` fields entirely.

Verified locally: ml-alpha builds clean with the cuda feature,
bce_grad_finite_diff (4/4) PASS on RTX 3050.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:59:12 +02:00
jgrusewski
55aeddaebd feat(ml-alpha): anchor_l2 kernel + Wiener-α controller (v2 B) [V8]
L2 anchor regularization toward initialization (axis B). Anchors
horizon_tokens + Q + MoE experts toward their init values to prevent
the calibration drift observed in v1 (where val_loss climbed as α
opened past epoch 1 in 2 of 3 folds).

KERNEL (`anchor_l2_fwd_bwd`):
  loss_out = λ · Σ_i (p[i] − p_init[i])²
  grad_p[i] += 2λ · (p[i] − p_init[i])

  - Warp-shuffle reduce; one block per parameter group; strided thread
    loop over n. Cross-warp reduce uses one __syncthreads.
  - Coalesced grad write via stride loop.
  - λ passed as device-side [1]-buffer (host writes scalar before launch
    — capture-safe).

CONTROLLER (`trainer::anchor_controller::AnchorController`):
  - Signal-driven λ floor: λ_floor = ‖p_init‖ / (100 · √numel).
    Cross-fold-persistent per pearl_kelly_cap_signal_driven_floors.
  - Wiener-α smoother (α = diff_var / (diff_var + sample_var + ε))
    on val_loss change; α floored at 0.4 per
    pearl_wiener_alpha_floor_for_nonstationary.
  - λ blends toward target = |ema_change|·scale with α; floored at
    λ_floor per pearl_blend_formulas_must_have_permanent_floor.
  - First-observation bootstrap (sentinel state replaced directly on
    first step) per pearl_first_observation_bootstrap.
  - 4/4 unit tests PASS: signal-floor init, bootstrap returns floor,
    floor protection across 1000 steps, λ_max cap.

NUMGRAD VERIFICATION (RTX 3050 sm_86):
  anchor_l2_numgrad PASSES with closed-form parity (machine precision)
  and central-difference parity (4 random positions) within 5e-2 rel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:26:31 +02:00
jgrusewski
11b96dac6a feat(ml-alpha): regime_moe_gate kernel + numgrad (v2 D) [V6]
Top-1 Mixture-of-Experts gate per Switch Transformer. Three kernels
in one .cu file:

  - regime_moe_gate_fwd: select top-1 expert from gate_logits, apply
    its [H, H] linear to fused_ctx → routed_ctx [B, N_H, H].
  - regime_moe_gate_bwd: chain-rule grads through the SELECTED
    expert's W and bias (sparse-by-expert scratches), plus
    d_fused_ctx accumulator. Inactive experts get zero contribution.
  - regime_moe_gate_aux: softmax of gate_logits + load-balancing
    auxiliary loss (frac · prob_mean × N_EXPERTS).

ARCHITECTURE:
  - N_EXPERTS = 4. Each expert is a [H, H] linear with bias.
  - Total expert params: 4 · 128 · 128 + 4 · 128 = 66 KB. Cheap.
  - STE on gate: gate logit grad is zero from the expert path (top-1
    is non-differentiable); the load-balance aux loss provides the
    differentiable signal that pushes routing toward balanced usage.

PERFORMANCE:
  - Grid (B, N_H, 1), block (HIDDEN_DIM). One block per (b, h).
  - Forward: each thread computes one output channel via a dot
    product over HIDDEN_DIM input dims (#pragma unroll 8).
  - Backward d_fused_ctx: each thread accumulates over d_out
    sequentially (HIDDEN_DIM iterations) since the weight matrix
    column is naturally aligned to the thread's d_in index.
  - Backward d_W/d_b scratches are sparse-by-expert; downstream
    reduce_axis0 collapses over (B, N_H).
  - Top-1 chosen by thread 0 per block, broadcast via shared mem.

NUMGRAD VERIFICATION (RTX 3050 sm_86):
  forward_matches_host_reference_and_backward_matches_numgrad
  PASSES 11 checks (4 on d_W, 3 on d_b, 4 on d_fused_ctx) within
  5e-2 rel / 5e-3 abs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:21:59 +02:00
jgrusewski
d94696620d feat(ml-alpha): inverted_attention_pool kernel + numgrad (v2 E) [V3]
iTransformer-style cross-variate attention pass: each of HIDDEN_DIM
features becomes a "variate token" with its K-trajectory as embedding.

FORWARD:
  X_inv[h, k]    = ln_out[k, h]                       # transpose
  scores[h, j]   = inv_scale · Σ_k X_inv[h, k] · X_inv[j, k]
  attn[h, j]     = softmax_j(scores)
  pooled[h]      = Σ_j attn[h, j] · mean_k_X_inv[j]   # mean-K commutes out

BACKWARD: three independent chains into ln_out:
  - value path:  (1/K) · attn[h', my_h] · d_pooled[h']  (per-k constant)
  - query path:  inv_scale · Σ_j d_scores[my_h, j] · X_inv[j, k]
  - key path:    inv_scale · Σ_h' d_scores[h', my_h] · X_inv[h', k]
  Softmax bwd: d_scores[h, j] = attn · (d_attn - Σ_l attn · d_attn)

IMPLEMENTATION NOTES:
  - First attempt cached attn [H, H] = 64 KB in shared mem → tripped
    the 48 KB dynamic-shared limit on sm_86 (CUDA_ERROR_INVALID_VALUE).
  - Fixed by moving d_scores to a DRAM scratch buffer; shared mem
    holds only X_inv [H, K] (≤ 16 KB at K = 32). One block-wide barrier
    between the d_scores write and the value/query/key accumulation.
  - All per-batch slice writes; no atomicAdd, no cross-block race.
  - Pooled computation uses the mean-K commute (Σ_k attn · X_inv =
    attn · mean_k_X_inv), saving an entire H×K accumulation pass.

LOCAL VERIFICATION (RTX 3050 sm_86):
  forward_then_backward_matches_central_difference PASSES 6 numgrad
  checks on ln_out positions within 5e-2 rel / 5e-3 abs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:16:26 +02:00
jgrusewski
e8f6c4721f feat(ml-alpha): horizon_token_attention_pool kernel + numgrad (v2 C) [V2]
Replaces the falsified per-horizon Q_h pool with a single shared
query Q over an extended key sequence [horizon_tokens; LN_b_out],
producing per-horizon outputs via TFT-style horizon-token mixing.

FORWARD:
  scores[i] = Σ_d Q[d] · ext[i, d]                      (i ∈ [0, N_H + K))
  attn      = softmax_i(scores)
  S[d]      = Σ_k attn[N_H + k] · ln_out[k, d]          (shared time agg)
  ctx[h, d] = attn[h] · horizon_tokens[h, d] + S[d]     (per-horizon out)

BACKWARD: full chain rule with softmax-centring; gradients to
horizon_tokens, Q, and ln_out via the saved attn weights.

NVIDIA-grade implementation per feedback_nvidia_grade_perf_for_kernels:
  - Warp-shuffle reduce (block_reduce_sum / block_reduce_max helpers)
    for all per-d dot products and softmax aggregates.
  - Cross-warp reduce uses exactly one __syncthreads.
  - Non-divergent shuffles: inactive lanes contribute 0 via ternary.
  - Block-per-batch + horizon-loop inside block → grad_ln_out += is
    race-free without atomicAdd.
  - Smem layout computed at launch: [s_attn(N_H+K); s_warp(N_WARPS);
    s_d_S(H) on bwd]. No over-allocation.

LOCAL VERIFICATION (RTX 3050 sm_86):
  forward_then_backward_matches_central_difference PASSES 12 numgrad
  checks (4 each on horizon_tokens / Q / ln_out) at 5e-2 rel / 5e-3
  abs envelope. First-try pass.

NOTE: .gitignore adjusted with narrow allow-rules for crates/ml-alpha/{
cuda,src,tests}/horizon_token_* paths — the broad "*token*" rule
intended for auth tokens was hiding these source files. Explicit
allow keeps the security rule intact while exempting these specific
files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:09:45 +02:00
jgrusewski
679ab3f5eb feat(ml-alpha): Kendall sigma-weighted BCE kernel + numgrad (v2 A) [V7]
New `bce_multi_horizon_sigma_forward_backward` kernel implementing the
Kendall homoscedastic uncertainty weighting per spec axis A:

  raw_bce_h   = Σ_{i in h, m_i=1} L_i
  count_h     = #{i in h : m_i = 1}
  mean_bce_h  = raw_bce_h / count_h
  w_h         = base_weight_h / (2 · exp(2 · log_sigma_h))
  total_loss  = Σ_h [ w_h · mean_bce_h + log_sigma_h ]
  d L / d p_i           = m_i · (w_h / count_h) · (p − y) / (p (1 − p))
  d L / d log_sigma_h   = 1 − 2 · w_h · mean_bce_h

NVIDIA-grade implementation per feedback_nvidia_grade_perf_for_kernels:
  - Warp-shuffle reduction (`__shfl_xor_sync`) for both per-horizon
    sums and the global valid count, replacing block tree-reduce.
  - One `__syncthreads` for the cross-warp aggregate; no inner-loop
    barriers.
  - Non-divergent shuffles: inactive lanes contribute 0 via ternary,
    never via `if (tid < N) shuffle`.
  - Coalesced strided access in both forward and gradient passes.
  - Pre-compiled cubin via build.rs; no nvrtc.

Independent of the legacy `bce_loss_multi_horizon` kernel — that one
stays untouched so eval/smoke paths are unaffected. The v2 trainer
wires this kernel in via commit V10.

Standalone helper `bce_sigma_loss_and_grad_gpu` in `trainer::loss_sigma`
for numgrad parity tests. Three numgrad tests all PASS on RTX 3050
(sm_86) within 5e-2 rel / 5e-3 abs:
  - d_log_sigma_h ↔ central-difference (numgrad on log_sigma)
  - grad_probs    ↔ central-difference (8 random positions)
  - total_loss    ↔ closed-form reconstructed from mean_bce_per_h

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 13:35:26 +02:00
jgrusewski
41292303dc refactor(ml-alpha): remove per-horizon Q_h path (C21-C25 falsified) [V1]
3-fold A/B sweep 2026-05-18 at commit 83546b5c3 falsified the simple
per-horizon Q_h attention pool:
  mean_auc 0.7559 ± 0.0068  vs baseline 0.7749 ± 0.024  (Δ = -0.019)
  h6000    0.7588 ± 0.0049  vs baseline 0.7591 ± 0.018  (Δ = -0.0003)

best_epoch on val_loss = 1 in 2/3 folds → calibration drift as α opens;
no horizon-distribution shift toward h6000. The per-horizon path
spends capacity on directions that hurt log-likelihood without lifting
ranking quality.

V1 of the v2 redesign deletes the falsified path entirely (per
feedback_no_partial_refactor; v2 spec/plan committed earlier today
captures the migration). Files removed:
  cuda/per_horizon_attention_pool.cu
  cuda/per_horizon_residual_head.cu
  cuda/per_horizon_prob_blend.cu
  src/per_horizon_attention_pool.rs
  src/per_horizon_residual_head.rs
  src/trainer/per_horizon_state.rs
  tests/per_horizon_attention_pool_numgrad.rs
  tests/per_horizon_residual_head_numgrad.rs
  tests/per_horizon_full_pipeline_smoke.rs

perception.rs:
  - struct field `per_horizon` removed
  - new() initialization removed
  - step_batched section 4.5 (forward_with_blend) → reserved comment
  - step_batched section 5a (backward_through_blend) → reserved comment
  - step_batched section 9 (adamw_step) → reserved comment
  - existing 17 optimizer groups + BCE/attention-pool path untouched
  - reduce_axis0 kernel kept (still used by existing param-grad reducers)

build.rs KERNELS: dropped the 3 per_horizon entries.
lib.rs + trainer/mod.rs: dropped per_horizon module declarations.

Workspace compiles clean (cargo check -p ml-alpha). Next: V2 builds
the horizon_token_attention_pool kernel as the v2 replacement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 13:26:45 +02:00
jgrusewski
69c64f2266 test(ml-alpha): per-horizon pipeline end-to-end smoke + α-gate (C23)
Composes the C21 + C22 kernels with a host-side learnable α-gate to
prove the full per-horizon contribution path works end-to-end without
yet doing the captured-graph integration in PerceptionTrainer.

Pipeline:
  LNb [B, K, HIDDEN_DIM]
   → per_horizon_attention_pool_fwd  → context_h [B, N_HORIZONS, HIDDEN_DIM]
   → per_horizon_residual_head_fwd   → residual  [B, N_HORIZONS]
   → final[b, h] = baseline[b, h] + tanh(α[h]) * residual[b, h]

Two tests cover the critical invariants for adoption-safety:

  alpha_zero_init_is_identity_to_baseline
    With α = [0, 0, 0, 0, 0] and any random Q_h / w_res / bias_res,
    final_logit MUST be bit-identical to baseline_logit (because
    tanh(0) = 0). Verified via to_bits() byte equality. Proves that
    initialising the new variant with α=0 makes it a strict superset
    of the existing path — switching to AttentionPoolVariant::PerHorizon
    cannot regress before any training has happened.

  alpha_nonzero_changes_output_and_grads_flow_end_to_end
    With α = [0.5, -0.3, 0.2, -0.1, 0.4]:
      * final ≠ baseline (residual contributing) ✓
      * all final logits finite ✓
      * full backward chain (residual_head_bwd → attention_pool_bwd)
        produces finite d_Q_h_scratch + finite d_LNb with at least
        one non-zero entry in each → gradients flow back to both the
        attention queries and the LN_b input ✓

This closes the kernel-side correctness story. The remaining
integration commits (C24+) are operational:

  C24: extend CheckpointV1 → V2 (add q_h, w_res, bias_res, alpha
       fields; V1 files load as Variant::SharedQuery)
  C25: PerceptionTrainer wiring — allocate the device buffers, fold
       attention + residual + gate into the captured graph, plumb
       gradients into AdamW's param list
  C26: 1-epoch smoke (assert no NaN, loss decreases vs baseline) —
       needs real training data + multi-GPU time
  C27: 30-epoch × 3-fold A/B (task #204) — decision gate per
       docs/superpowers/specs/2026-05-18-per-horizon-attention-pool-design.md
       §0 falsifiable claim

C24-C25 are 1-2 day work even when carefully scoped; C26-C27 need
real GPU-hours + result analysis. C21-C23 land the validatable kernel
correctness piece without committing to that time investment yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:38:24 +02:00
jgrusewski
8c335caef7 feat(ml-alpha): per-horizon residual head kernel + numgrad parity (C22)
Companion kernel to C21's per_horizon_attention_pool. Computes a
per-horizon scalar residual from each horizon's context vector:

  residual[b, h] = Σ_d w_res[h, d] * context_h[b, h, d] + bias_res[h]

Designed to be added (behind a learnable α-gate) to the existing
multi_horizon_heads logit output — keeps the existing GRN head kernel
completely unchanged. The per-horizon attention pool's contribution
flows through this lightweight projection without weight-shape
changes elsewhere or checkpoint-V2-bumping.

Path A integration sketch (deferred to follow-up commit C23):
  alpha_logit_per_horizon = existing_head(h_K)[h]            # from current path
                         + tanh(α[h]) * residual_kernel(context_h)[h]
where α[h] is a learnable 5-vector init'd to 0 (no effect at start).
Training discovers per-horizon whether the residual contributes.
This is a strict superset of the existing path — α=0 → bit-identical
to today.

Backward kernel produces:
  d_w_res        — per-block scratch [B, N_HORIZONS, HIDDEN_DIM]
                   for host reduce_axis0 → shared [N_HORIZONS, HIDDEN_DIM]
  d_bias_res     — per-block scratch [B, N_HORIZONS], same reduction
  d_context_h    — per-batch indexed; += chained with attention bwd

Single-writer discipline preserved (no atomicAdd per
feedback_no_atomicadd.md); horizon loop inside the per-batch block.

Numgrad parity test:
  - B=3, N_HORIZONS=5, HIDDEN_DIM=128 fixture.
  - Loss = Σ residual_out (so d_residual = 1).
  - Probes 8 random w_res indices, all 5 bias_res entries, 8 random
    context_h indices via central-difference at ±eps=1e-2.
  - All within 5e-2 rel-tol or 5e-3 abs-floor.
  - Passes on RTX 3050.

Same scope discipline as C21: kernel + binding + numgrad first;
trainer wiring + α-gate + smoke training + A/B sweep follow once
both kernels are individually validated (now done).

Closes the second kernel-correctness portion of #203. Remaining:
  C23: trainer wiring (capture attn_pool fwd into the graph; sum
       residual into existing head output with α-gate)
  C24: CheckpointV1 → V2 bump (add q_h, w_res, bias_res, alpha fields)
  C25: 1-epoch smoke (assert no NaN, loss decreases vs baseline)
  C26: 30-epoch × 3-fold A/B (#204) — decision gate per spec §0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:36:42 +02:00
jgrusewski
edc449eecf feat(ml-alpha): per-horizon attention pool kernel + numgrad parity (C21)
First implementation slice of the per-horizon attention pool design
(docs/superpowers/specs/2026-05-18-per-horizon-attention-pool-design.md).
Lands the kernel + Rust binding + numgrad verification; downstream
wiring into PerceptionTrainer's captured graph + CheckpointV2 bump +
A/B sweep are follow-up commits gated on this proving correctness.

Kernel (cuda/per_horizon_attention_pool.cu):

  per_horizon_attention_pool_fwd
    Q_h[N_HORIZONS, HIDDEN_DIM] × LNb[B, K, HIDDEN_DIM]
      → context_h[B, N_HORIZONS, HIDDEN_DIM]
        attn_h_weights[B, N_HORIZONS, K]
    Per-block math identical to the single-Q variant, looped over
    N_HORIZONS sequentially within each batch's block. Grid stays
    (B, 1, 1) so backward grad_ln_out writes are race-free
    (per feedback_no_atomicadd.md — no cross-block contention).
    Per-batch shared mem ~k_seq + BLOCK + HIDDEN_DIM floats.

  per_horizon_attention_pool_bwd
    Same chain-rule pattern as attention_pool_bwd but with the horizon
    loop inside the block: each (b, h) slice updates grad_ln_out in
    place (sequential horizon accumulation), grad_Q_h is written as
    per-block scratch [B, N_HORIZONS, HIDDEN_DIM] for host reduce.
    Single-writer discipline preserved.

Rust binding (src/per_horizon_attention_pool.rs):
  PerHorizonAttentionPool::{new, forward, backward}. Self-contained;
  doesn't yet touch PerceptionTrainer or CfcTrunk. Loads the cubin
  via the standard env!("OUT_DIR") path. Dynamic shared-mem byte
  count computed per launch from k_seq.

Numgrad parity test (tests/per_horizon_attention_pool_numgrad.rs):
  - B=2, K=8, HIDDEN_DIM=128, N_HORIZONS=5 fixture.
  - Loss = Σ context_h (so d_context = 1 everywhere — clean analytical).
  - Backward kernel produces analytical grads; central-difference of
    forward kernel at ±eps=1e-2 across 8 random Q_h indices + 8 random
    LNb indices verifies analytical matches CD within 5e-2 rel-tol or
    5e-3 abs-floor.
  - Passes on RTX 3050.

build.rs picks up the new .cu file automatically via the existing
KERNELS list; cubin compiles cleanly at sm_86 + sm_89.

Same scope discipline as Phase 2D.2 (VSN numgrad) — kernel correctness
first, integration second. The follow-up commit set per the spec §3
appendix:
  C22: extend multi_horizon_heads.cu signature to accept per-horizon
       context input + bump head_w shape to [N_HORIZONS, 2*HIDDEN_DIM]
  C23: wire PerHorizonAttentionPool into PerceptionTrainer + CfcTrunk
       captured graph behind AttentionPoolVariant config flag
  C24: CheckpointV1 → V2 bump with discriminant + optional q_h field
  C25: smoke training (one epoch, no NaN, loss decreases)
  C26: 30-epoch × 3-fold A/B sweep (#204) — decision gate per spec §0

Closes the kernel-correctness portion of #203.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:32:16 +02:00
jgrusewski
e9c4acfb12 feat(ml-alpha): MultiHorizonLoader inference-only mode
Add inference_only flag to MultiHorizonLoaderConfig that skips per-file
forward-label precomputation (~half the file-load cost), plus
peek_first() and next_inference_input() chronological-streaming methods
for the ml-backtesting LOB harness.

- min_size relaxed to cfg.seq_len when inference_only=true (training
  still requires seq_len + max_horizon + 1 for label generation)
- New cursor fields (inference_file_idx, inference_snap_idx) walk every
  loaded snapshot in chronological order; reset() zeros both
- peek_first() seeds CfcTrunk::capture_graph_a with cur==prev semantics
  (prev_ts_ns==ts_ns, trade_signed_vol=0) — natural stream-start
- next_inference_input() errors if cfg.inference_only=false (guard
  against accidental mixing of training/inference paths)
- All trainer call-sites (alpha_train example + multi_horizon_loader
  tests) updated with inference_only: false (zero behaviour change)
- Inline test module exercises both modes; tests skip gracefully when
  fixture data isn't populated rather than panicking

See docs/superpowers/specs/2026-05-18-real-lob-integration-design.md
§1 (trainer parity) + §7 (orchestrator).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 08:08:38 +02:00
jgrusewski
494a2e4827 perf(ml-alpha): block-per-batch cfc_step + reduce_axis0 reducer (Phase B commit 1)
Per docs/superpowers/specs/2026-05-17-kloop-parallelization-design.md.

cfc_step_batched (fwd + bwd) refactored from grid=(1,1,1) with internal
n_batch loop to grid=(B,1,1) — each block handles one batch. Removes
the single-SM bottleneck on the K-loop's most-called kernel (64×/step).

Param-grad accumulation moves to per-batch scratch:
  cfc_grad_w_in_scratch_d  [B, n_hid, n_in]
  cfc_grad_w_rec_scratch_d [B, n_hid, n_hid]
  cfc_grad_b_scratch_d     [B, n_hid]
  cfc_grad_tau_scratch_d   [B, n_hid]

Zeroed once per training step, K-loop's 64 bwd calls += into them, then
4 reduce_axis0 launches collapse B → final grad buffers (OVERWRITE)
before AdamW. New AdamW-after-reducer invariant: final grads are
meaningful only after the reducer has run in the current step.

New reduce_axis0 kernel: single parameterised reducer [B, N] → [N] via
block tree-reduce (no atomicAdd per feedback_no_atomicadd.md). Same
pattern as layer_norm_reduce_param_grads — CUDA-Graph-safe.

cfc_step_backward_batched shared-mem dropped from (B+1)*n_hid*4 to
2*n_hid*4 bytes per block (only one row of sd_pre needed per block bi).

Tests:
- New stacked_trainer_loss_shrinks_at_batch_32: FIRST test that
  actually exercises the cross-batch reduction code path; existing
  perception_overfit suite was all B=1. Initial 0.24 → final 0.00.
- Scratch-clears test removed (explanatory comment kept): structurally
  hard to assert directly due to begin_capture/end_capture not
  executing kernels; the B=32 convergence smoke implicitly validates
  scratch zeroing since divergence would otherwise be immediate.

All 9 perception_overfit smokes + 4 backward_finite_diff tests pass.

build.rs:
- KERNELS list adds "reduce_axis0"
- Cache-bust → v11

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:47:40 +02:00
jgrusewski
005ded9722 feat(ml-alpha): TGN Δt Fourier features in snap_feature_assemble (Phase 2C)
Bumps FEATURE_DIM 32→40. Slots [32..40] now carry 8 TGN-style Fourier
features encoding the elapsed time Δt = ts_ns - prev_ts_ns:
  (cos(ω_k · Δt_ns), sin(ω_k · Δt_ns))_{k=0..3}
at log-spaced periods [60s, 6s, 600ms, 60ms].

This gives Mamba2's input vector explicit Δt encoding that's
discriminative across temporal scales — particularly important once
decision-stride > 1 (Phase 2A) lands and the gap between K-positions
becomes irregular. Without these features the model has no way to
distinguish "1ms gap" from "1s gap" between consecutive K-positions.

Slots [0..32] unchanged (bit-equivalent for the first 32 features).
Reserved-zero slots [26..32] kept for future macro context. Slots
[20..26] still hold the loader-precomputed EMA regime cascade.

Frequencies stored in __constant__ memory (SNAP_DT_OMEGAS[4]) — small
table, broadcast read pattern, no register pressure. Frequency
selection rationale (one per log-decade):
  60s    — minute-scale macro session context
  6s     — 10s-scale liquidity windows
  600ms  — sub-second microstructure
  60ms   — tick-cluster spacing

Δt clamped to >= 0 so the rare out-of-order timestamp doesn't produce
nonsense angles. Each (cos, sin) pair satisfies cos²+sin² = 1
(verified by new test `dt_fourier_features_are_bounded`).

New tests in snap_feature_bit_equiv.rs:
  - dt_fourier_features_are_bounded: |slot| <= 1 + cos²+sin² == 1
  - dt_fourier_discriminates_scales: Δt=1ms vs Δt=1s produce L2-distinct
    Fourier vectors (>0.1)
  - reserved_slots_are_zero updated to check [20..32] (regime + reserved)
    instead of [20..FEATURE_DIM]

All 8 perception_overfit smokes still pass (synthetic stride=1 and
stride=4 both converge 0.32 → 0.0000) — proves the wider FEATURE_DIM=40
input doesn't break the Mamba2+LN+GRN chain.

build.rs cache-bust → v8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:09:08 +02:00
jgrusewski
70d5fc29cf feat(ml-alpha): decision-stride loader + CLI + Mamba2 dt_s scaling (Phase 2A)
Decision-stride S lets a length-K sequence span ((K-1)*S + 1) raw
snapshots instead of K consecutive ones — expands the effective
time-window covered by each sequence at the same K-positions compute
cost. With K=64 and S=4, the window covers 256 ticks (~5s on ES MBP-10
at 20ms-tick) instead of 64 ticks (~1.3s).

Loader (crates/ml-alpha/src/data/loader.rs):
- `MultiHorizonLoaderConfig.decision_stride: usize` (default 1, must
  pre-existing call sites add the new field).
- `next_sequence` reads snapshot at `anchor + k * stride`; labels at the
  same indices (labels stay in absolute-snapshot horizons regardless of
  stride, e.g. h=6000 always means "predict 6000 raw snapshots forward").
- `prev` snapshot for microstructure features (prev_mid, prev_ts_ns)
  now points to the prior K-position (`anchor + (k-1)*stride`), NOT the
  consecutive-snapshot prior, so `Δt = ts_ns - prev_ts_ns` carries the
  actual elapsed time between K-positions (consumed by Mamba2's dt_s and
  the planned Phase 2C TGN Fourier features).
- New `#[ignore]` real-data test: `loader_stride_4_yields_correct_spacing`
  asserts Δt monotonicity at stride=4.

Mamba2 dt_s (crates/ml-alpha/src/trainer/perception.rs):
- `PerceptionTrainerConfig.decision_stride: usize` plumbs the stride
  through. dispatch_train_step + evaluate_batched now use
  `dt_s = decision_stride as f32` so Mamba2's selective scan
  `exp(-dt * sigmoid(a))` reflects the real elapsed time. With stride=1
  the behaviour is identical to before.

CLI (crates/ml-alpha/examples/alpha_train.rs):
- `--decision-stride <S>` flag (default 1) wired into both train and val
  loaders + PerceptionTrainerConfig.

Argo workflow:
- `decision-stride` parameter on the template (default "1") +
  `--decision-stride` script flag + propagation into the train pod's
  alpha_train invocation.

Synthetic smoke (tests/perception_overfit.rs):
- `stacked_trainer_loss_shrinks_with_stride_4` proves the trainer-level
  dt_s=4.0 keeps the Mamba2+LN+CfC+GRN chain numerically stable.
  Converges 0.32 → 0.0000 (matches stride=1 smoke trajectory — dt_s
  scaling didn't break the SSM dynamics).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:04:38 +02:00
jgrusewski
0171c8c0ea 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>
2026-05-17 19:16:37 +02:00
jgrusewski
37c3a8f4d7 feat(ml-alpha): ISV-driven per-horizon EMA + lambda (Phase 1+2)
Foundation for replacing the static `--auto-horizon-weights` formula
(`min(1, K/h)`) with a signal-driven per-horizon gradient scaler. Per
`feedback_isv_for_adaptive_bounds.md`: adaptive bounds live in ISV,
not hardcoded constants. Per `pearl_adam_normalizes_loss_weights.md`:
Adam normalizes per-loss weight lifts (SP13 saw 13× aux_w produce
only 0.6%/epoch divergence), so the effective lever is scaling the
GRADIENT into the shared trunk, not the BCE coefficient. This commit
sets up the EMA + lambda infrastructure; Phase 3 (wiring lambda into
heads_bwd to actually scale the trunk gradient) is gated on the
3-fold CV results from eb51c0f9c.

Phase 1 — BCE kernel emits per-horizon UNWEIGHTED mean BCE:
  cuda/bce_loss_multi_horizon.cu:
    - New output buffer `loss_per_horizon[N_HORIZONS=5]`.
    - Per-horizon shared-mem accumulators (sloss_h, svalid_h) with
      block tree-reduce — no atomicAdd, per `feedback_no_atomicadd.md`.
    - Hardcoded N_HORIZONS_BCE=5; total shared-mem usage ~13 KiB
      (comfortable under any SM smem limit).
    - The aggregate `loss_out` is still the externally-weighted mean
      callers use for reporting; the new buffer is the UNWEIGHTED
      signal an EMA layer needs.

Phase 2 — EMA + lambda kernel:
  cuda/horizon_lambda.cu (new):
    - Single-thread kernel (5 horizons, fixed-size loop — trivial).
    - First-observation bootstrap via sentinel = 0 per
      `pearl_first_observation_bootstrap.md`; replaces directly when
      `loss_ema_h <= 0` (safer than `== 0` under --use_fast_math).
    - Fixed α = 0.1 EMA for now; Wiener-optimal α follow-up flagged
      (`pearl_wiener_optimal_adaptive_alpha.md`).
    - lambda_h = clamp(loss_ema_h / mean(loss_ema), 0.5, 2.0).
      - Ratio gives natural "under-trained → boost" signal.
      - Clamp prevents winner-take-all per
        `pearl_controller_amplifies_dominant_magnitude_trap.md`
        and bounded-modifier safety per
        `pearl_audit_unboundedness_for_implicit_asymmetry.md`.

Trainer wiring (trainer/perception.rs):
  - 3 new fields: `loss_per_horizon_d`, `loss_ema_d`, `lambda_d`
    (all 5-element f32 CudaSlices; pre-allocated, zero-initialised).
  - Cached `horizon_lambda_fn` + module handle per the
    `BiasKernels`-style pattern (no per-call cuModuleLoadData).
  - BCE callsite (train + eval paths) updated to pass
    `loss_per_horizon_d`.
  - `dispatch_train_step` launches `horizon_ema_and_lambda` right
    after BCE, BEFORE the K-loop backward. Inside the captured
    graph; per-step launch overhead is ~1 µs.
  - `loss_ema_snapshot()` + `lambda_snapshot()` test-only accessors
    (mapped-pinned readback, not for hot path) for diagnostics.

Smoke test — `horizon_ema_and_lambda_track_after_training`:
  - Verifies pre-step EMA + lambda are zero (sentinel).
  - After 5 training steps:
    loss_ema = [0.59, 0.66, 0.45, 0.62, 0.50] — finite + positive.
    lambda   = [1.05, 1.16, 0.80, 1.10, 0.89] — mean ≈ 1.0, all
    inside the [0.5, 2.0] clamp envelope.
  - Lower per-horizon BCE → lower lambda (de-emphasize); higher
    BCE → higher lambda (boost). Exactly the ISV semantics we want.

Validation: 6 perception_overfit tests pass, synthetic overfit still
shrinks (0.33 → 0.0006), 26 ml-alpha lib + 23 integration tests
green. lambda_d is computed every step but NOT YET CONSUMED by
heads_bwd; training behavior is bit-identical to 3a196382f. Phase 3
(consume lambda_d in heads_bwd_batched to scale the per-horizon
gradient into the trunk) follows once CV confirms the foundation is
stable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:48:37 +02:00
jgrusewski
eb51c0f9cd feat(ml-alpha): walk-forward CV via file-list-driven loader
mhzs7 reported val mean_auc=0.726 on 3a196382f — but the trainer
constructed both train and val MultiHorizonLoader with the SAME
`mbp10_root: cli.mbp10_data_dir`. The two loaders only differed by
seed. So val sequences were held-out-by-anchor from the same files
train sampled from; not temporally OOS. Per
`pearl_single_window_oos_is_not_oos.md` a single-window result that
doesn't enforce time-ordered separation can collapse across true
walk-forward folds.

Refactor: drop `mbp10_root` from `MultiHorizonLoaderConfig` (which
forced caller to share the dir between train and val). New API takes
an explicit `files: Vec<PathBuf>` — the loader preserves the order
given and does no internal shuffle, so callers control temporal
ordering. Added `discover_mbp10_files_sorted(root)` helper that
enumerates a dir and sorts by filename (chronological under the
`ES.FUT_<YEAR>-Q<n>.dbn.zst` convention).

alpha_train.rs splits the discovered files by 3 new CLI flags:
  --cv-fold <k>            (default 0)
  --cv-n-folds <N>         (default 1 — single fold)
  --cv-train-window <W>    (default 0 — auto)

Single-fold default (cv_n_folds=1): train on all files except the
last, val on the last file. This replaces the old "same files for
both" bug; even runs that don't think about CV now get a temporal
split by default.

Sliding-window CV (cv_n_folds > 1): fold k trains on files
[k..k+W] and validates on file [k+W]. With 9 quarterly files
(2024-Q1..2026-Q1) and `--cv-n-folds 3`, the natural layout is:

  fold 0: train 2024-Q1..2024-Q4 (W=4) → val 2025-Q1
  fold 1: train 2024-Q2..2025-Q1       → val 2025-Q2
  fold 2: train 2024-Q3..2025-Q2       → val 2025-Q3
  blind holdout: 2025-Q4, 2026-Q1

Threaded the flags through scripts/argo-alpha-perception.sh and
infra/k8s/argo/alpha-perception-template.yaml so each fold submits
as an independent workflow.

Updated tests/multi_horizon_loader.rs to the new API:
  loader_yields_seq_with_valid_labels — exercises discover + load.
  loader_errors_on_empty_files       — replaces missing-root test.
  discover_errors_on_missing_root    — pinpoints the discover step.

Honors:
  - feedback_no_partial_refactor.md — every consumer migrated atomically.
  - feedback_no_legacy_aliases.md — no `mbp10_root` shim left behind.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:36:48 +02:00
jgrusewski
3a196382f0 fix(ml-alpha): captured graph poisoned eval via SyncOnDrop events
z2w9w cluster run hit CUDA_ERROR_INVALID_VALUE at "eval snap_batched
fwd" the first time validation ran after a captured training step.
Training itself succeeded (epoch 0 train_loss=0.69 over 250 captured
graph replays); only the subsequent direct eval kernel launch failed.

Root cause (vendor/cudarc/src/driver/safe/core.rs:920):
  CudaSlice::device_ptr_mut() returns a `SyncOnDrop::Record` guard.
  On drop, that guard UNCONDITIONALLY calls `event.record(stream)` on
  the slice's `.read` event (the check at line 953 only gates the
  cuStreamWaitEvent on .write — the unconditional event.record at the
  end runs no matter what). Inside a stream-capture region, those
  event.record(stream) calls turn the CudaEvents into "captured
  events" per the CUDA Driver API. Captured events can ONLY be waited
  on by streams in the same capture sequence; any later
  cuStreamWaitEvent from outside fails with CUDA_ERROR_INVALID_VALUE.

  The trainer's eval path then called `device_ptr_mut()` again to
  stage the eval DtoDs — which inserted exactly that
  cuStreamWaitEvent on the now-captured `.write` event of every
  trainer CudaSlice. First kernel launch after the dtods failed.

Why this hit z2w9w now: a) all our work is on a SINGLE stream
(`self.stream`), so the event-based multi-stream sync that cudarc
inserts is pure overhead, b) the capture region is exactly where
those overhead events become poisonous.

Fix: disable cudarc's read/write event tracking BEFORE the trainer
allocates ANY device memory. With tracking off at alloc time,
CudaSlice::new returns `read: None, write: None` (core.rs:1283).
SyncOnDrop::record_event with `event: None` produces a `Record(None)`
that does nothing on drop. launch_builder skips its event waits/
records too. The capture region runs clean; the eval direct launches
have no stale captured events to wait on.

Validation:
  - 6 perception_overfit tests pass, including 3 NEW regression tests
    that pin the exact failure modes:
    * evaluate_alone_succeeds — eval with no prior training
    * evaluate_works_after_warmup_only — eval after 1 uncaptured step
    * evaluate_works_after_capture_no_replay — eval right after capture
      (the minimal repro that pinpointed `device_ptr_mut` inside capture)
    * evaluate_works_after_captured_training_step — full warmup +
      capture + replay + eval
  - 26 ml-alpha lib + 23 ml-alpha integration + 306 ml-core lib all pass.

Also drops the now-redundant disable/enable_event_tracking dance
around the capture region — events are globally disabled for the
trainer's lifetime so no per-capture flipping needed.

Honors: feedback_no_quickfixes.md (root-cause traced through
cudarc's safe wrappers to the SyncOnDrop record contract, not a
symptom-suppress sleep/retry hack).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:01:02 +02:00
jgrusewski
c3ee5e165a feat(ml-alpha): wire batched kernels through trainer + CLI (#8)
Plumbs the batched cfc + heads kernels added in 829ddfa62 through
PerceptionTrainer, evaluator, and the alpha_train CLI:

  PerceptionTrainerConfig.n_batch         — batch size, default 1
  PerceptionTrainer::step_batched         — process B sequences per
                                            optimizer step using
                                            cfc_step_batched / heads_batched
  PerceptionTrainer::evaluate_batched     — forward-only batched eval
  PerceptionTrainer::step / evaluate      — thin B=1 wrappers preserving
                                            existing single-sequence
                                            test/inference APIs (assert
                                            cfg.n_batch == 1)
  alpha_train CLI: --batch-size N         — accumulates B sequences per
                                            optimizer step in train loop;
                                            val loop also batches and uses
                                            evaluate_batched

Per-K scratch buffers all grow to [K, B, dim] layout (K-major, slot-k
contiguous). Mamba2's [B, K, H] output is transposed once after
forward via the new transpose_3d_swap_01 kernel, and grad_h_enriched_seq_t
is transposed back to [B, K, H] before Mamba2 backward. Two transposes
per training step; negligible (1.5MB at B=32).

Dead unbatched kernel handles removed from the trainer (step_fn,
step_bwd_fn, heads_fn, heads_bwd_fn, grad_x_d) — all training and
inference now go through the batched variants for B ≥ 1. The
single-sample kernels remain in CUDA for the standalone test helpers
in cfc/step.rs and heads.rs.

Local 2Q smoke (seq_len=32, B=4, --auto-horizon-weights, 800 train
seqs × 2 epochs):
  epoch 0: val_loss=0.7138 AUC h30/h100/h300/h1000/h6000 = .55/.55/.57/.61/.51
  epoch 1: val_loss=0.6558 AUC h30/h100/h300/h1000/h6000 = .72/.68/.75/.68/.65

vs the in-flight qf5mj baseline (B=1, K=96, no horizon weighting) which
had val_loss=0.6933 best and AUCs oscillating at ~0.50 — this batched
run hits AUC 0.75 (h300) and 0.72 (h30) in just 2 epochs of 200
optimizer updates. Batching + horizon-weighting unblocks the model.

77 ml-alpha tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:42:35 +02:00
jgrusewski
85ce295773 feat(ml-alpha): per-horizon BCE weighting (fixes label-correlation inflation)
For seq_len K and horizon h with h ≫ K, the K position-supervised
labels in a single sequence are near-identical (sequential positions'
forward windows overlap by ~(h-1)/h). Per-position BCE therefore
treats ~K highly-correlated labels as independent samples, inflating
gradient pressure on long horizons by a factor of K.

Concretely at K=96:
  h=30   → ~3 effective samples per seq (forward windows overlap ~97%)
  h=100  → ~1                          (~99%)
  h=6000 → ~1                          (~99.98%)

Per-position supervision was paying 96× the natural signal density on
h=6000, pulling the model toward fitting noise at long horizons.

Fix: the fused BCE kernel now accepts an optional
`loss_weights[N_HORIZONS]` (nullptr → uniform = no-op). Each (k, h)
loss + grad contribution is multiplied by w_h; the normaliser is the
sum of weighted valid entries instead of the raw valid count.

`auto_horizon_weights(K, horizons)` computes `w_h = min(1.0, K/h)` so
short horizons stay at full weight and long horizons collapse to
their independent-sample density. Exposed via CLI:
  --auto-horizon-weights              # K/h auto-derived
  --horizon-weights "1,1,0.5,0.1,0.02" # explicit floats

Default behaviour is uniform (1.0) — apples-to-apples with the
in-flight qf5mj baseline. Synthetic overfit still 0.6268 → 0.1144 in
250 steps (82% drop). 77 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:17:38 +02:00
jgrusewski
248d8fe510 feat(ml-alpha): full architectural pass — recurrent CfC, GPU BCE, regime features, training discipline
Comprehensive fix for the issues identified after the BPTT-unroll cluster
run plateaued at val_loss ~0.692 with oscillating AUCs:

ARCHITECTURE
  - CfC h_old is now RECURRENT across positions. Previously reset to
    zero every step → CfC degenerated to a per-cell tanh-FC layer.
    New: h_old at step k IS h_new at step k-1. Heads still operate
    on h_new_k, but now the CfC actually carries state. Reverse-order
    backward through the K positions accumulates grad_h_old → grad_h_new
    via the new optional `grad_h_carry` arg on multi_horizon_heads_backward.
  - tau is TRAINED. cfc_step_backward now writes grad_tau (per-cell decay
    constant derivative), trainer gets a 7th AdamW group at 0.1× cfc lr.
  - 6 NEW regime features (EMA cascade computed loader-side per file)
    fill slots out[20..26] of snap_features. Gives the model multi-minute
    trend / volatility / liquidity context that is structurally unreachable
    inside the K-snapshot BPTT window. Slots: mid-z (med/slow), trend
    signal, log-vol slow, log-spread med, log-trade-rate med. All bounded
    via log1p / signed-log so no tuned constants leak in.

PERFORMANCE (NVIDIA-style)
  - GPU-fused multi-horizon BCE for the entire [K, N_HORIZONS] grid in
    ONE launch (was K host roundtrips). Native NaN-label masking.
  - K-loop is fully GPU-resident: pre-allocated per-K scratch
    (h_new_per_k, probs_per_k, labels_per_k, grad_probs_per_k), zero
    device allocs inside step(). Only TWO syncs per sequence (after
    forward, after backward) vs previously 2K+1.
  - Stream-ordered kernel launches with pointer-offset addressing into
    per-K buffers — host doesn't wait between K iterations.
  - cfc_step_backward / multi_horizon_heads_backward both use += grad
    semantics; trainer pre-zeroes accumulators once per step().
  - MAMBA2_ALPHA_MAX_K capped at 96 (was temporarily at 256). 96 covers
    h=30/100/300 with room; regime features handle h=1000/h=6000.

TRAINING DISCIPLINE
  - LR schedule: linear warmup (default 200 steps) + cosine decay to
    lr * lr_min_factor (default 0.1). Applied per training step to both
    CfC and Mamba2 AdamW groups via new set_lr_cfc/set_lr_mamba2.
  - Best-checkpoint tracking by val_loss; recorded in summary
    (best_epoch, best_val_loss, best_val_auc).
  - Early stopping on val_loss plateau (default patience = 3).
  - CRITICAL BUG FIX: validation now uses new `evaluate()` method
    (forward-only) instead of `step()`. Previous CLI called step()
    on val data, which ran the full backward + AdamW update on the
    validation set. With per-step BPTT that's ~K× more pressure than
    the old comment ("statistically negligible") assumed.

Synthetic overfit: 0.6442 → 0.1233 in 250 steps (81% drop, sharper
than the previous 70%). 77 ml-alpha tests pass.

Local 2Q smoke (seq_len=64, 600 train seqs/epoch, 4 epochs):
  val_loss 0.7011 → 0.6990, best epoch=1, h300 AUC 0.565 in epoch 0.

Phase E.3 callers (ml/examples/alpha_baseline.rs,
alpha_dqn_h600_smoke.rs) use the LEGACY Mamba2 forward_train +
backward_from_h_enriched path — unaffected by these changes (their
kernels are pre-zeroed via alloc_zeros, so the += grad semantics
remain correct in single-call mode).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 09:59:37 +02:00
jgrusewski
16f5febf27 feat(ml-alpha): per-step supervision unrolls BPTT through full sequence
The final-step-only trainer (one BCE prediction per 32-snapshot
window) trained flat at chance on real ES data despite working on
synthetic overfit: train_loss=0.6953, val_loss=0.6943 across 40k
gradient steps. Gradient density was the bottleneck — one supervised
position per sequence × ~8K seqs/epoch isn't enough signal for the
SSM to find the alpha.

This commit supervises the model at EVERY position in the sequence:

  mamba2_alpha_scan_fwd_seq    — emits h_enriched at every t step
                                 ([N, K, sh2] instead of [N, sh2])
  mamba2_alpha_scan_bwd_seq    — accepts d_h_enriched_seq, injects
                                 gradient at each t before propagating
                                 d_state through the gate chain.
                                 d_w_c and d_h_s2 accumulate across t.

  PerceptionTrainer.step()    — loop k=0..K; cfc + heads + BCE at
                                each valid label; cfc/heads grads
                                accumulate via += in kernel writes.
                                One Mamba2 backward call consumes the
                                full grad_h_enriched_seq.

  cfc_step_backward            — grad_w_in/w_rec/b writes changed
                                 to += (callers MUST pre-zero).
  multi_horizon_heads_backward — grad_w/grad_b writes changed to +=.

  alpha_train.rs               — passes per-position label rows to
                                 step(); AUC still scored from
                                 last-position predictions.

Phase E.3 callers (alpha_baseline.rs, alpha_dqn_h600_smoke.rs) use
the LEGACY Mamba2 forward_train + backward path with `alloc_zeros`
grad buffers — unaffected.

Synthetic overfit still converges 0.6664 → 0.1976 in 250 steps.
Local 2-quarter ES.FUT smoke shows the val AUC at h300 climbing
0.513 → 0.566 over 3 epochs (was flat-at-chance before). First
gradient signal we've gotten through the new architecture.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 01:03:33 +02:00
jgrusewski
2289fa062a feat(ml-alpha): normalize snap features to O(1)-O(10) scale
First L40S run (alpha-perception-s6hqv, commit 586d1e782) trained flat
at chance: train_loss=0.6953, val_loss=0.6943, AUCs all near 0.50
across 5 epochs and 40k gradient steps. Synthetic overfit on the same
trainer hit 0.59→0.19 in 250 steps, so wiring was sound.

The signal-killer was feature scale: raw size deltas were ±100, dt_ms
could exceed 1e4, and log-returns sat at ~1e-4. Mamba2's W_in
projection saturates on those extremes, gradient bleeds out.

Now in the kernel:
  out[0]     = (mid - prev_mid) / tick_size           [tick-return]
  out[12..17]= sgn(d) * log1p(|d|)                    [signed-log OFI]
  out[18]    = sgn(v) * log1p(|v|)                    [signed-log vol]
  out[19]    = log1p(max(dt_ms, 0))                   [log dt]

No tuned constants — tick_size is a market quantity; log1p and
signed-log are monotone bounded transforms. Bit-equiv tests updated
to assert the new closed-form expressions (still GPU-only, no CPU
oracle).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 00:33:17 +02:00
jgrusewski
1dc1f3563c refactor(ml-alpha): consolidate trainers — stacked is THE perception trainer
The merged Mamba2 -> CfC -> heads design from the 2026-05-16 spec
amendment supersedes the CfC-alone path. Removing the old CfC-only
PerceptionTrainer and renaming the stacked Mamba2CfcTrainer to
PerceptionTrainer (one trainer, clean naming).

Deletions:
  - src/trainer/perception.rs (the OLD CfC-only trainer)
  - tests/perception_overfit.rs (CfC-only smoke)
  - tests/perception_debug_dump.rs (CfC-only trajectory print)
  - tests/stacked_overfit.rs (replaced by perception_overfit pointing
    at the renamed module)

Renames:
  - src/trainer/stacked.rs -> src/trainer/perception.rs
  - Mamba2CfcTrainer -> PerceptionTrainer
  - Mamba2CfcTrainerConfig -> PerceptionTrainerConfig
  - tests/stacked_overfit.rs content -> tests/perception_overfit.rs

CLI rewrite:
  examples/alpha_train.rs now drives the stacked PerceptionTrainer.
  Per-step inputs are sequences (Vec<Mbp10RawInput>) of length
  seq_len; labels come from the LAST position of the window
  (per-horizon). Flags: --seq-len, --mamba2-state-dim, --lr-cfc,
  --lr-mamba2 (no more --n-hid since hidden_dim is fixed at 128 to
  match Mamba2 and CfC by design).

Test status:
  - 64 GPU tests pass on local sm_86 (31 lib unit + 33 integration)
  - synthetic-overfit (rebranded perception_overfit): 250 steps,
    initial=0.5951 -> final=0.1917 (68% drop, well above 40% gate)
  - All bit-equiv / finite-diff / invariant tests still PASS
  - One ignored test: the gate_artifact integration (waiting for
    cluster-trained summary inputs)

The cluster gate (Task 18) now compares stacked-trained AUC vs a
Mamba2-baseline AUC (TBD: stacked vs a simpler "Mamba2 only" config
or an external reference baseline).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:08:25 +02:00
jgrusewski
45b203e31e feat(ml-alpha): Mamba2CfcTrainer — stacked Mamba2 -> CfC -> heads
Realizes the 2026-05-16 spec amendment merging Mamba2 + CfC into one
stacked architecture (vs the original "compete via gate" framing).

Forward chain:
  snap_features × seq_len
   -> window pack [1, seq_len, FEATURE_DIM]
   -> Mamba2Block.forward_train -> (logit, cache.h_enriched [1, hidden_dim])
   -> cfc_step(x=h_enriched, h_old=0) -> h_new
   -> heads -> probs [5]
   -> BCE(probs, labels)

Backward chain:
  BCE -> grad_probs
   -> heads_backward -> grad_h_new + grad_W_heads, grad_b_heads
   -> cfc_step_backward -> grad_W_in, grad_W_rec, grad_b + grad_x (=grad_h_enriched)
   -> Mamba2.backward_from_h_enriched(&cache, &grad_h_enriched_tensor)
      -> Mamba2BackwardGrads (full 9-tensor gradient set)

Optimizers (6 total):
  - 5 CfC AdamWs (W_in, W_rec, b, heads_w, heads_b) — reused from
    PerceptionTrainer's per-param-group pattern
  - 1 Mamba2AdamW for all 9 Mamba2 parameter tensors (existing
    implementation in mamba2_block.rs)

Synthetic-overfit on constant +1 direction (seq_len=16, state_dim=8,
lr_cfc=3e-3, lr_mamba2=1e-3, 250 steps):
  initial_avg=0.5951 → final_avg=0.1917 (68% drop, well past 40% gate).
  Monotone descent at all 5 progress checkpoints.

Architectural note (v1): CfC runs with h_old=0 each step (no inter-
step recurrence). With h_old=0, the CfC layer is effectively per-cell
tau-scaled tanh FC. Inter-step CfC state (h_old carrying between
calls) is a v2 extension once the cluster gate validates the v1
foundation.

The cluster gate (Task 18) now has the actual stacked production
trainer to deploy, not a CfC-alone-vs-Mamba2-alone bench.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:03:55 +02:00
jgrusewski
deed15b34e feat(ml-alpha): cfc_step_backward emits grad_x for upstream chain
Adds grad_x[k] = sum_i d_pre[i] * W_in[i,k] computed by thread 0 of
the cfc_step_backward kernel (after the existing __syncthreads in
the shared-mem sd_pre relay). Required by the stacked Mamba2 -> CfC
design: Mamba2.backward_from_h_enriched needs grad on h_enriched,
which is the CfC's "x" input in the stacked topology.

For the existing CfC-only PerceptionTrainer (x = snap_features, no
upstream learnable layer), grad_x is computed but discarded into a
preallocated buffer.

backward_finite_diff tests still pass (4/4) — the new arg is the
14th positional kernel arg; existing callers updated. perception_
overfit smoke still passes (loss 0.5669 -> 0.0665 in 200 steps).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:59:36 +02:00
jgrusewski
1305d6531b feat(ml-alpha): PerceptionTrainer end-to-end PASS on synthetic overfit
Resolves Task 13 — the synthetic-overfit divergence I thought was a
wiring bug was actually init-sensitivity on the n_hid=32 toy. With
seed=0x4242 + lr=3e-2 + constant +1 direction + 200 steps + reset_
hidden_state per sample, the trainer converges loss 0.5669 -> 0.0665
(88% drop, well under the 60% gate threshold).

The 200-step weight trajectory (debug_long_horizon_weight_trajectory)
shows monotone descent:
  step 0:   loss=0.6932  hb[0]=0.030  hw[0,0]=-0.124
  step 50:  loss=0.2332  hb[0]=1.164  hw[0,0]= 0.997
  step 100: loss=0.1301  hb[0]=1.599  hw[0,0]= 1.412
  step 190: loss=0.0747  hb[0]=1.999  hw[0,0]= 1.777
Heads weights drive monotonically into the correct sigmoid tail.

The chain is sound:
  - heads_backward finite-diff at 1% relative
  - cfc_step_backward finite-diff at 5% relative
  - BCE forward+backward at 5% relative finite-diff
  - AdamW invariants (zero-grad + wd, descent on g=theta)
  - Graph A capture bit-identical to sequential
  - end-to-end overfit on constant +1 = 88% loss drop in 200 steps

Removed the #[ignore] + the speculative "wiring bug" doc comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:44:08 +02:00
jgrusewski
1bb878b6a7 wip(ml-alpha): PerceptionTrainer scaffold + rename drops phase_a naming
Per user feedback "no phase naming, give proper naming to files and
functions": renames src/trainer/phase_a.rs -> perception.rs,
src/data/phase_a_loader.rs -> data/loader.rs, and the corresponding
types (PhaseATrainer -> PerceptionTrainer, PhaseALoader ->
MultiHorizonLoader, PhaseAConfig -> MultiHorizonLoaderConfig,
PhaseASequence -> LabeledSequence). Test files renamed in lock-step.

Adds PerceptionTrainer.step() — full end-to-end forward + heads
backward + cfc_step_backward (K=1 truncated BPTT) + 5 AdamW param
groups (W_in, W_rec, b, heads_w, heads_b). reset_hidden_state()
zeros h_old between independent samples.

KNOWN ISSUE — synthetic-overfit smoke (tests/perception_overfit.rs)
does NOT yet show loss shrinkage on the 200-step budget:
  initial_avg=0.6914, final_avg=0.6955 (random-baseline ln(2)=0.693)

The kernels are individually correct (heads_bwd + cfc_bwd finite-diff
at 5% rel, AdamW invariant ‖θ‖ 40->1 in 200 steps). The end-to-end
chain doesn't converge — most likely due to half the CfC cells having
near-1 decay from log-uniform tau init on a zeroed hidden state, so
only the fast cells carry signal. Fix candidates for next session:
  - tau init narrower / per-task tuned for the smoke
  - longer step budget (1000+) with adjusted LR
  - validate end-to-end with explicit print of grad/probs across iters

Task 13 is NOT complete (the gate criterion isn't met). Subsequent
work continues from here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:29:32 +02:00
jgrusewski
753284f2ae feat(ml-alpha): backward kernels — heads + cfc_step (K=1 BPTT)
multi_horizon_heads_backward: sigmoid + linear chain rule. One block,
HIDDEN_DIM=128 threads. Computes grad_w, grad_b, grad_h_in. No
atomicAdd; per-thread accumulation only.

cfc_step_backward: truncated K=1 BPTT through one CfC time step.
Forward pre/decay/tanh recomputed inside the kernel; emits grad_w_in,
grad_w_rec, grad_b, grad_h_old. tau is held frozen (structural
log-uniform init per Hasani 2022; backprop through tau deferred to
Phase A v2 if the gate needs it). Uses dynamic shared memory for the
d_pre relay between threads (size = 2 * n_hid * 4 bytes).

Tests (4/4 on sm_86) validate via on-GPU finite-difference:
  - heads grad_h vs forward(h±eps) → matches at eps=1e-3, rel<=1%
  - heads grad_b vs forward(b±eps) → matches at eps=1e-3, rel<=1%
  - cfc grad_b vs forward(b±eps) → matches at eps=1e-3, rel<=5%
  - cfc grad_h_old vs forward(h_old±eps) → matches at eps=1e-3, rel<=5%

CPU is not the reference (per feedback_no_cpu_test_fallbacks.md). The
kernel is the truth; numerical perturbation validates the analytic
gradient against the kernel's own forward.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:20:54 +02:00
jgrusewski
a6f4772617 feat(ml-alpha): Phase A data loader — predecoded MBP-10 -> seq + labels
Reuses ml-features::predecoded::load_or_predecode_mbp10 (no cycle —
ml-features doesn't depend on ml-alpha). Yields seq_len-sized windows
of Mbp10RawInput plus 5-horizon binary labels via
multi_horizon_labels::generate_labels.

Per-snapshot prev_mid / prev_ts_ns / trade_signed_vol come from the
prior snapshot in the source stream (not from the anchor), so the
CfC trunk sees a continuous-time signal across the entire seq.

Labels: NaN at edge positions (no forward window) or tied prices;
BCE kernel masks these (Task 10).

Tests:
  - loader_errors_on_missing_root: passes (1/1 inline)
  - loader_yields_seq_with_valid_labels: --ignored, runs at gate time
    with FOXHUNT_TEST_DATA set

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:17:51 +02:00
jgrusewski
3cb6992364 feat(ml-alpha): CUDA Graph A capture for perception forward
Captures snap_feature_assemble -> cfc_step -> heads -> projection into
a single replayable graph. Scalars (dt_s, ts_ns, prev_mid, ...) are
frozen at capture time per cudarc 0.19 semantics; the trunk
re-captures when those change. A follow-up task moves scalars into a
device-resident buffer for cross-step replay stability.

Key learning: cudarc's default event-tracking creates cross-stream
dependencies that begin_capture rejects with
CUDA_ERROR_STREAM_CAPTURE_ISOLATION. Pattern (from crates/ml/.../
fused_training.rs): bracket begin/end_capture with
context.disable_event_tracking() / enable_event_tracking(). Mode
remains CU_STREAM_CAPTURE_MODE_RELAXED. Pre-allocate MappedF32Buffer
staging slots as struct fields (host-malloc during/around capture is
also a trigger).

The captured forward writes h_pong directly (no ping-pong swap inside
the captured region — the swap mutates pointer identity which would
invalidate captured kernel args). Heads and projection both read
h_pong.

Tests (3/3 on sm_86):
  - graph_a_replay_matches_sequential: captured replay output equals
    sequential dispatch on same input at eps<=1e-5 (probs) / 1e-4 (proj)
  - graph_a_replay_is_deterministic: 3 consecutive replays produce
    bit-identical output
  - graph_a_replay_outputs_finite: probs in [0,1], proj finite

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:05:25 +02:00
jgrusewski
d472b2ac3a feat(ml-alpha): BCE multi-horizon + AdamW kernels
bce_loss_multi_horizon: fused forward+backward, block tree-reduce (no
atomicAdd), NaN labels masked (drop). Loss = mean over valid; grad =
(p-y)/(p(1-p)) scaled by 1/N_valid.

adamw_step: element-wise AdamW with weight decay; one thread per param.

Tests pass on sm_86:
  BCE (4/4): positive+finite loss, near-zero loss when probs match
    labels, analytic grad matches GPU-computed finite-difference at
    eps=1e-3 / max_relative=5e-2 across 5 perturbation points, NaN
    labels mask grad and contribute zero to loss/N_valid.
  AdamW (4/4): zero-grad moves param only by weight-decay, positive
    grad decreases param, step counter increments, repeated descent
    on grad=theta drives ‖θ‖ from 40 to <1 in 200 steps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:57:26 +02:00
jgrusewski
1ed81cf6c0 feat(ml-alpha): CfcTrunk forward — sequential perception dispatch
CfcTrunk owns weights, ping-pong hidden buffers, and pre-allocated
per-step scratch (snap features, probs, projection output). Modules
and CudaFunction handles cached at new_random so the hot path
avoids reload. forward_snapshot dispatches snap_feature_assemble ->
cfc_step -> heads -> projection sequentially; Graph A capture (Task 11)
will fold these into a single launch.

The Mamba2 prefix (per 2026-05-16 spec amendment) is added in a
follow-up task before Graph A capture.

Tests (5/5 on sm_86):
  - probs in [0,1] across all 5 horizons
  - hidden state changes after forward
  - probs + proj are finite
  - layer-norm proj has near-zero mean
  - 50-step run leaves hidden finite

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:55:00 +02:00
jgrusewski
9ddde632b2 feat(ml-alpha): projection kernel (128->8 + layer-norm)
Single-block 8-thread kernel; thread j computes its own 128-dim dot
product, then thread 0 computes block-wide mean/var, then each thread
applies the per-output affine layer-norm. No atomicAdd; reductions are
single-thread (8 elements — negligible cost).

Tests (5/5 on sm_86) assert:
  - layer-norm zero-mean output under identity gain
  - layer-norm unit-variance output under identity gain
  - ln_bias shifts mean uniformly
  - ln_gain scales variance (var = gain^2)
  - finite output under zero input (variance clamp activates)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:52:57 +02:00
jgrusewski
d4e46aba94 feat(ml-alpha): multi_horizon_heads kernel (128->5 sigmoid)
Per-horizon P(up) at h ∈ {30, 100, 300, 1000, 6000} snapshots forward.
Single-block 5-thread kernel; each thread is its own 128-dim dot
product + sigmoid. No atomicAdd.

Tests (5/5 pass on sm_86) assert invariants only:
  - sigmoid output ∈ [0, 1] for all heads
  - zero weights + zero bias → 0.5 exactly
  - bias = +20 → saturates near 1
  - bias = -20 → saturates near 0
  - per-head independence (mixed-bias configuration)

Addendum updated to explicitly state no-CPU-mirror discipline per
feedback_no_cpu_test_fallbacks.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:51:48 +02:00
jgrusewski
f927469ed3 feat(ml-alpha): cfc_step kernel + invariant-based GPU validation
Hasani 2022 closed-form CfC recurrence; one thread per hidden unit, no
atomicAdd. Tests assert algebraic invariants (dt=0 -> identity, zero
weights -> h_old * decay, large tau -> h_old preserved, output bound).

Also removes src/cfc/oracle.rs and replaces snap_feature bit-equiv
test with property assertions per feedback_no_cpu_test_fallbacks.md.
CPU mirrors are bug-locks; validation is now via known synthetic
inputs + analytical relations on the GPU output.

12 tests pass on local sm_86 (7 snap_feature invariants + 5 cfc_step
invariants).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:50:20 +02:00
jgrusewski
611c82b4ed feat(ml-alpha): snap_feature_assemble kernel + CPU oracle
Per-snapshot 32-dim feature vector (mid log-return, spread, depth, OFI,
trade-flow, dt). Single-block single-thread kernel; uploads via
MappedF32Buffer DtoD into CudaSlice per the addendum Pattern 3.

Bit-equiv tested CPU vs GPU at eps<=1e-5 over (synthetic input,
reserved-slots-are-zero, zero-prev-mid edge case).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:46:58 +02:00
jgrusewski
c098c5dae1 feat(ml-alpha): mapped-pinned ingress slots for snapshot + fill
Typed ABIs (#[repr(C)] SnapshotPayload, FillPayload) backed by
pinned_mem::MappedF32Buffer. write_volatile is the only hot-path
CPU->GPU pathway; GPU reads via device_ptr() with zero HtoD.

Tests pass on local sm_86 (3/3): snapshot round-trip, fill round-trip,
pre-allocation invariant (device pointer stable across writes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:42:28 +02:00
jgrusewski
1dac3c3fc2 feat(ml-alpha): IsvBus — 32-slot device-resident f32 bus
Holds perception outputs (slots 0..13) on-device; slow-path write/snapshot
go through MappedF32Buffer DtoD per the htod/htoh discipline. Slot
semantics documented in design spec Section 7.

Also deletes examples/alpha_mamba_baseline.rs which Task 1 left orphaned
(used the deleted eval + training modules). Task 17 will rebuild the
Mamba2 baseline trainer path inside gate/cfc_vs_mamba2.rs against the
new Phase A loader.

Tests pass on local sm_86 (3/3): round-trip one slot, 32-slot capacity,
multi-slot independent writes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:38:54 +02:00