Commit Graph

4 Commits

Author SHA1 Message Date
jgrusewski
20c361a300 feat(ml-alpha): Phase 1d.4 GPU-native backtest — proper SSM-stacker Sharpe verdict
Four new kernels in mamba2_alpha_kernel.cu:
  - backtest_per_trade_pnl       : [T, N] per-trade PnL with threshold filter
  - backtest_sum_reduce_f32      : block tree-reduce returns per threshold (T scalars)
  - backtest_sum_squared_reduce  : block tree-reduce returns² per threshold (T scalars)
  - backtest_sum_reduce_i32      : block tree-reduce trade counts per threshold

All atomicAdd-free via block tree-reduce in shared memory (per
feedback_no_atomicadd). Single kernel launch handles the full
threshold sweep across all sequences via grid_x=T, grid_y=ceil(N/256).

New module crates/ml-alpha/src/backtest.rs:
  - GpuBacktest::from_block(&Mamba2Block) — reuses cubin already loaded
  - GpuBacktest::run(probs, prices_t, prices_kt, thresholds, cost) → Vec<BacktestStats>
  - Returns: n_trades, mean_ret, std_ret, Sharpe (per-trade unannualised),
    hit_rate, total_pnl per threshold

Wired into phase1d_long_horizon.rs after the stacker eval:
  - Convert stacker_logits → probs via sigmoid
  - Upload probs + end-bar prices + (end-bar + horizon) prices to GPU
  - Sweep thresholds [0.00, 0.02, 0.05, 0.10, 0.15, 0.20, 0.25]
  - Print per-threshold table + best Sharpe operating point
  - GATE: per-trade Sharpe > 1.5 = deployable, 0.5-1.5 = marginal, < 0.5 = fail

Cost model: 0.25 price units round-trip = 1 ES.FUT tick = $12.50/contract.
Tunable via --cost-per-trade. Realistic for retail flow; brokers can
trade at half-tick or better.

GPU-pure on the hot path: kernels do per-trade math + reductions;
host only receives T (= 7 here) scalars per metric for final Sharpe
arithmetic. No GPU↔CPU roundtrip per trade.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 09:37:57 +02:00
jgrusewski
eb8c251afb feat(ml-alpha): Mamba2AdamW optimizer + end-to-end training-loop validation (Phase 1d.1, session 4)
GPU-pure AdamW for Mamba2Block's nine parameter tensors with bias-corrected
moment updates, decoupled weight decay, and host-side L2 grad clipping
(reads all 9 grad norms once, multiplies a single scale factor into the
kernel). Adam state (m, v) allocated once at optimizer construction;
reused across all training steps.

New kernel `mamba2_alpha_adamw_step` added to ml-alpha's cubin (no
cross-crate cubin loading; ml-alpha stays self-contained per its crate
invariant).

Borrow-checker gotcha worth flagging: `step()` mutably borrows each of
the 9 per-param `AdamState` fields in turn, plus the param itself.
Tried `apply()` as a method on `&self` — conflicts with `&mut self.s_*`.
Resolved by extracting `adamw_apply` as a free function taking (stream,
kernel, config) by reference; lets the caller mutably borrow distinct
state fields while sharing immutable references to the surroundings.

**The end-to-end training-loop test is the analytical-gradient validation:**
- 20 AdamW steps on a fixed batch (n_batch=4, seq_len=8, in_dim=4,
  hidden=8, state=4) with binary labels (half +1, half 0)
- Asserts ≥15 of 20 steps have monotonically-decreasing BCE loss
- Asserts final loss < 0.65 (below the chance baseline ln(2) ≈ 0.693)

If backward had a sign flip, scale error, or wrong reduction axis
anywhere across:
  - BCE-with-logits derivative (sigmoid(z) - y) / N
  - Output projection cuBLAS sgemm (dY^T @ X for dw_out; dY @ W for dx)
  - Scan backward kernel (per-channel scratch d_a/d_b/d_w_c + d_h_s2
    identity passthrough)
  - Reduction kernels (sum over j for d_a/d_b, sum over i for d_w_c)
  - A/B projection backwards + branch-sum to recover d_x
  - Input projection backward
  - AdamW with bias correction + decoupled weight decay

…loss would NOT decrease monotonically. It does. The full backward
chain is correct.

Tests (10 passing on real GPU):
- training_loop_decreases_loss          (THE end-to-end validation)
- backward_returns_finite_grads
- backward_rejects_wrong_d_logit_shape
- forward_train_returns_cache
- forward_shape_and_finite
- forward_rejects_wrong_shape
- config_rejects_seq_len_over_32
- config_rejects_state_over_16
- config_rejects_zero_dims
- constructs_and_loads_kernels

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 01:56:38 +02:00
jgrusewski
bf6ed42acf fix(ml-alpha): backward kernel concerns — atomicAdd-free per-channel scratch + forward cache (Phase 1d.1)
Addresses four concerns surfaced after the forward-pass commit:

1. Backward kernel was scaffolded with `if (j==0)` to dodge atomicAdd,
   but that drops contributions from j>0 channels. Rewritten so every
   (i, j) thread writes its UNIQUE slot in per-channel scratch:
     d_a_per_channel[N, sh2, K, state_d]
     d_b_per_channel[N, sh2, K, state_d]
   Followed by a unified reduction kernel mamba2_alpha_reduce_d_proj
   that sums over j → d_a_proj / d_b_proj [N, K, state_d]. Same kernel
   handles both call sites (DRY).

2. d_w_c gradient already had the right pattern (d_w_c_per_sample +
   mamba2_alpha_reduce_d_w_c); kept as-is. All three gradient outputs
   now follow the same atomicAdd-free scratch+reduce structure per
   feedback_no_atomicadd.

3. `forward()` was discarding LinearActivations which the backward path
   needs. New `Mamba2ForwardCache` struct carries (input_2d, x, a_proj,
   b_proj, h_enriched) — everything backward needs to recover gradients
   through the four projections + scan. `forward_train()` returns
   `(logit, cache)`; `forward()` thin-wraps and discards the cache for
   inference.

4. `x_hist[32 * 16]` in the backward kernel was hardcoded; configs with
   seq_len > 32 would silently corrupt. Added MAMBA2_KERNEL_SEQ_MAX=32
   constant + config validation. Backward kernel header documents both
   limits explicitly.

Tests (7 passing on real GPU):
- forward_train returns cache with correct shapes for all 5 tensors
- seq_len > 32 rejected at config validation
- state_dim > 16 rejected
- forward output [B, 1] all finite
- forward rejects wrong in_dim / seq_len
- kernel handles all 4 functions resolve (fwd / bwd / reduce_d_proj /
  reduce_d_w_c) + param-count sanity

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 01:45:08 +02:00
jgrusewski
c3e769b4b6 feat(ml-alpha): Mamba2 forward pass — GPU-pure end-to-end (Phase 1d.1, session 2)
Forward inference for the supervised snapshot stream — no ISV, no
temporal_weight, no NULL-pointer dispatch. Clean rewrite of the DQN
mamba2 kernel into a purpose-built alpha kernel.

New kernel `crates/ml-alpha/cuda/mamba2_alpha_kernel.cu` with three
extern "C" symbols:
  - mamba2_alpha_scan_fwd   — selective SSM scan over K timesteps with
                              sigmoid-gated state update; cheaper than
                              the DQN variant (no ISV stability scaling,
                              no per-position temporal_weight)
  - mamba2_alpha_scan_bwd   — analytical backward (scaffolded; full
                              gradient wiring lands in session 3)
  - mamba2_alpha_reduce_d_w_c — block tree-reduce over batch for the
                              W_c gradient (no atomicAdd — per
                              feedback_no_atomicadd)

build.rs swapped from ../ml/src/cuda_pipeline/mamba2_temporal_kernel.cu
to the local cuda/mamba2_alpha_kernel.cu. ml-alpha no longer depends
on ml's CUDA source — fully self-contained alpha-stack.

Forward pipeline:
  1. cuBLAS sgemm: input [B,K,in] @ W_in.T + b_in  → x [B,K,hidden]
  2. cuBLAS sgemm: x @ W_a.T + b_a                  → a_proj [B,K,state]
  3. cuBLAS sgemm: x @ W_b.T + b_b                  → b_proj [B,K,state]
  4. zero-init h_s2, h_enriched [B, hidden]
  5. scan kernel: (a_proj, b_proj, W_c, h_s2) → h_enriched
  6. cuBLAS sgemm: h_enriched @ W_out.T + b_out     → logit [B, 1]

All on GPU; output is a [N] CudaSlice<f32> of raw logits. Caller
sigmoids + thresholds (or feeds directly into BCE-with-logits).

Tests (5 passing on real GPU):
- forward [4, 16, 81] → logit [4, 1], all finite
- reject wrong in_dim
- reject wrong seq_len
- reject state_dim > 16
- reject zero dims
- + parameter-count sanity

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 01:40:18 +02:00