fa36d5538404ca76c7f9f14f6e59705c5a877f2c
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a0e81fbdfc |
arch(crt-a): forward_step incremental SSM state — enables event-rate trunk forward
Per A0 investigation memo (commit
|
||
|
|
a45fd85986 |
feat(ml-alpha): raise Mamba2 state cap 16→32 + add auc_h6000 early-stop
Three correlated changes for the next CV round: 1. Mamba2 state_dim cap: 16 → 32 cuda/mamba2_alpha_kernel.cu: MAMBA2_ALPHA_MAX_STATE_D 16 → 32. Per-thread state register `float x[32]` (128 B/thread) and per-thread x_hist replay cache `float x_hist[K*32]` (up to 12 KiB/thread of local memory at K=96). L40S/H100 register file (256 KiB/SM) absorbs this without occupancy collapse for our block dims (32-128 threads). Update Rust-side MAMBA2_KERNEL_STATE_MAX + validation message + test name. Kernel header doc updated. 2. New early-stop option: auc_h6000 examples/alpha_train.rs: add the long-horizon AUC as a third early-stop metric. The ISV CV ( |
||
|
|
ab94ce2a49 |
perf(ml-alpha): device-resident AdamW step counter (capture prep)
Stage 1+2 of #162 (CUDA Graph capture of training step). The AdamW kernels previously took the step counter as a host scalar arg, which gets baked into kernel args at CUDA Graph capture time — replays would freeze the counter and produce wrong bias-correction values. Both AdamW variants now read the step from a device pointer, advanced by a tiny 1-thread `increment_counter` kernel that goes inside the captured region. Each replay correctly increments and observes the new step value. Kernel changes: adamw_step.cu: - adamw_step: int step → const int* step_ptr - adamw_increment_counter: new, +=1 on step_ptr[0] mamba2_alpha_kernel.cu: - mamba2_alpha_adamw_step_devscale: int t → const int* step_ptr - mamba2_alpha_increment_step_counter: new Rust changes: trainer/optim.rs (AdamW): - host `step: i32` → device `step_count_d: CudaSlice<i32>` - step(): launch increment kernel BEFORE adamw kernel; both read device counter via pointer arg. - step_count(): test-only accessor, mapped-pinned readback (sync). mamba2_block.rs (Mamba2AdamW): - kept host `step_count: i32` for legacy paths (`step`, `step_from_buffers`) which aren't capture-compatible anyway (host grad-norm dtoh, host scalar grad_scale). - added device `step_count_d: CudaSlice<i32>` for the production gpu_clip path; advances via `kernel_increment_step` kernel inside the captured region. - adamw_apply_devscale: `t: i32` → `step_d: &CudaSlice<i32>`. Validation: - 4 adamw_invariants tests pass (step_count_increments specifically exercises the device counter). - 10 mamba2_block lib tests pass (training_loop_decreases_loss exercises legacy host-counter path). - Synthetic overfit smoke: initial=0.25 → final=0.0006 (matches pre-refactor trajectory bit-for-bit-equivalent). Stage 3+4 (capture brackets + first-call-capture / subsequent-replay state machine in step_batched) follows in the next commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b6fb720acd |
perf(ml-alpha): eliminate all dtoh from training hot path
GPU-resident grad-norm + clip-scale; mapped-pinned loss readback.
Replaces 9× memcpy_dtoh per Mamba2 AdamW step (grad-norm host roundtrip)
+ 1× per-step download() (loss). Saves ~10 stream-sync barriers/step.
New kernels (cuda/grad_norm.cu):
- grad_norm_sq_phase1: per-block tree-reduce of x[i]^2 (no atomicAdd)
- grad_norm_sq_phase2: cross-tensor accumulator (sequential stream-ordered)
- grad_clip_scale: writes min(1, max_norm/sqrt(norm_sq)) to device ptr
- mamba2_alpha_adamw_step_devscale: reads grad_scale from device pointer
instead of host scalar, allowing AdamW kernels to launch async without
waiting for a CPU-side norm computation.
Trainer changes (perception.rs):
- loss_d kept device-side; mapped-pinned MappedF32Buffer shadow.
- Single stream.synchronize() at end of step (was 2: post-bwd + download).
- DtoD copy loss_d → loss_host_d queued, then sync flushes both kernels
+ copy in one barrier. Loss read via host_ptr (no dtoh).
Mamba2 AdamW (mamba2_block.rs):
- step_from_buffers_gpu_clip(): all grad-norm tensors processed via
phase1+phase2 chain, scale computed on-device, AdamW launches with
devscale variant. Zero host roundtrips.
- Pre-allocated block_partials_d, grad_norm_sq_d, grad_scale_d.
Optimizer (optim.rs): removed redundant stream.synchronize() per AdamW step.
Each per-tensor AdamW kernel is stream-ordered; sync only needed before
host reads, which the trainer handles centrally.
Synthetic overfit smoke: initial=0.30 → final=0.0006 (matches pre-refactor
trajectory). Full ml-alpha test suite passes (45 tests across lib +
integration).
Honors:
- feedback_no_htod_htoh_only_mapped_pinned.md (MappedF32Buffer only)
- feedback_no_atomicadd.md (block tree-reduce only)
- feedback_no_legacy_aliases.md (step_from_buffers replaced, not aliased)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
485150c7b7 |
feat(ml-alpha): lift Mamba2 kernel seq_len cap from 32 → 256
Previous BPTT-unroll run had val_loss trending (0.6941→0.6922 over 5
epochs) but AUCs oscillating around 0.50 — the architecture lacked
context for medium/long horizons (h300, h1000, h6000 ≫ seq_len=32).
Phase 1d.2 validated the SSM at seq_len=6000; this is a step toward
restoring useful sequence depth.
Bumps:
- MAMBA2_ALPHA_MAX_K constant: 32 → 256
- x_hist per-thread replay buffer: 2KB → 16KB (spills to
DRAM-backed per-thread local memory; L2-cached, acceptable
perf cost vs the 8x context gain)
- Mamba2BlockConfig::validate updates the cap
Backward compat: legacy Phase E.3 callers (alpha_baseline,
alpha_dqn_h600_smoke) only use K=12 / K=32 — unaffected by the
larger compile-time max.
Synthetic overfit still converges 0.7135 → 0.2079 in 250 steps.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|