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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Cargo.toml: drops gbdt; adds memmap2 + approx; keeps ml-core only
(cannot depend on ml: would cycle since ml depends on ml-alpha for
the Mamba2 gate baseline).
build.rs: compiles 7 cubins (mamba2_alpha + 6 new placeholders)
with -O3 --use_fast_math --ftz --fmad. Skips kernels whose source
isn't present yet so partial check-ins work. Every env::var paired
with rerun-if-env-changed per the canonical build pearl.
src/pinned_mem.rs: local copy of MappedF32Buffer (mirrors
ml::cuda_pipeline::mapped_pinned::MappedF32Buffer). Drives the only
permitted CPU<->GPU path per feedback_no_htod_htoh_only_mapped_pinned.
Eventually the move-to-ml-core refactor will deduplicate; out of
scope for the Phase A branch.
Addendum: updates the import path to ml_alpha::pinned_mem.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removes mlp/training/eval/backtest/metrics_detail/calibration and the
old example trainers. Preserves multi_horizon_labels, purged_split,
fxcache_reader (Phase A data path), mamba2_block (gate reference).
Subsequent commits populate cfc/heads/isv/pinned/trainer/data/gate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two complementary additions to validate the minute-horizon alpha
hypothesis at IBKR-realistic costs:
1. `alpha_baseline --decision-stride N`: emits a new action every N
steps; between decisions force action=0 (wait) so an open position
is held rather than re-decided per bar. Cuts per-bar trade counts
~stride× and removes the coin-flip overtrading. Local 2Q sweep
showed stride=200 + scaled training (8K episodes × 25 envs × H=1200)
flipped Sharpe at ¼-tick from -4.29 (per-bar, 3-fold mean) to +1.78,
with std collapsing from ±8.8 to ±1.15. Break-even cost moved from
<¼-tick to ~1-tick — for the first time positive at IBKR-realistic
passive-execution frictions.
2. `alpha_train_stacker --max-rows N`: optional cap on bars consumed
from the fxcache. Used during local 2Q smoke (--max-rows 4M against
the 17.8M-row 9Q fxcache) to fit Mamba2 training on a 4 GB consumer
GPU; on the cluster (--no-cap) it sees all 9Q.
3. New Argo workflow `alpha-cv`: standalone template that compiles
alpha_train_stacker + alpha_baseline + alpha_fill_coeffs.json,
trains the stacker on the 9Q fxcache, then runs 9 sequential
walk-forward folds of alpha_baseline on disjoint 1.9M-bar windows
(one per quarter). Launcher script `scripts/argo-alpha-cv.sh`
mirrors argo-train.sh conventions.
The local 2Q test that motivated this commit is summarised inline in
the alpha-cv template comments; the verdict was "framing was the bug —
once decision cadence matches the multi-minute alpha horizon, the
strategy is positive at IBKR commission".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
T10 wires the C51 → Mamba2 backward chain in both binaries:
- New alpha_train_window_store_batched_kernel captures per-step windows
for end-of-epoch re-forward (Mamba2 cache required for backward).
- Mamba2Block::backward_from_h_enriched lets the C51 grad-input feed
directly into Mamba2 backward, skipping the unused W_out projection.
- alpha_c51_grad_input → backward_from_h_enriched → Mamba2AdamW.step
closes the loop in alpha_dqn_h600_smoke and alpha_compose_backtest.
Smoke (--temporal --c51): all 4 KCs PASS. R_mean -6.3 → +4.2 vs
Phase E.3 close R_mean -4.7 (no-temporal). EARLY_Q_MOVEMENT
calibration (mamba2_snapshot + mamba2_weight_distance) lifts the
diagnostic from 0.0023 (head-only) to 0.0590 (head + encoder),
giving an honest learning signal when the encoder absorbs gradient.
Backtest (--c51 --temporal --window-k 16 --isv-continual):
cost=0.0000 best τ=0.250 Sharpe_ann=+34.56 (was +10.41 head-only,
-22.54 frozen-Mamba2)
cost=0.0625 best τ=0.250 Sharpe_ann=+33.22
cost=0.1250 best τ=0.250 Sharpe_ann=+30.85 (Phase 1d.4 baseline: -4.0)
cost=0.2500 best τ=0.250 Sharpe_ann=+27.73
cost=0.5000 best τ=0.250 Sharpe_ann=+15.68
Caveat: in-sample results; OOS gate next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
System-scoped naming for the alpha trading system's training binaries —
same rationale as the earlier phase_e_* → alpha_* rename. These binaries
produce / validate the durable alpha-system components (Mamba2 + stacker
+ calibration); they're tooling, not milestone artifacts.
phase1a.rs → alpha_bar_baseline.rs
phase1a_detailed.rs → alpha_bar_detailed.rs
phase1d_calibrate.rs → alpha_calibrate.rs
phase1d_mamba.rs → alpha_mamba_baseline.rs
phase1d_long_horizon.rs → alpha_train_stacker.rs
clap `name = "..."` strings updated to match new filenames; cross-refs
in docstrings (alpha_calibrate.rs, gbm_baseline.rs) fixed.
Plus: NEW `--alpha-cache-out <PATH>` flag on alpha_train_stacker.rs
(Phase E.1 Task 12b). After the existing Mamba2 + stacker training
completes, runs stacker inference on ALL bars (not just val/test) and
dumps the resulting alpha_logits as a little-endian binary file:
[u32 n_bars] [f32 logits[n_bars]]
Bars `< seq_len − 1` are written as 0.0 (Pearl A sentinel — no history).
Inference uses the same Block-S column normalisation (col_mean/col_std)
computed during stacker training, applied to all bars consistently.
This cache is consumed by alpha_dqn_h600_smoke.rs (next commit) which
loads it and populates SnapshotRow.alpha_logit — replacing the current
hardcoded 0.0 placeholder with the real Phase 1d.3 stacker output.
Build verified: `cargo build -p ml-alpha --release --example alpha_train_stacker`
completes clean in 40s.
Initial single-cost backtest at τ=0.25 cost=0.25 revealed the binding
constraint: mean_ret = -0.176, pre-cost EV ≈ +0.074, so the model has
a real directional edge but K=6000 price moves are too small to clear
1-tick round-trip cost. The cost-vs-edge balance is the real Phase 1d.4
verdict question, not whether the model has signal.
Two enhancements per the insight block in the previous run:
1. **Cost sweep**: GpuBacktest::run now takes `costs: &[f32]` and
produces (C × T) rows instead of T. The smoke runs at five costs:
- 0.0 frictionless upper bound (theoretical max Sharpe)
- 0.0625 quarter-tick (very aggressive execution)
- 0.125 half-tick (professional desk)
- 0.25 1 tick = $12.50/contract (retail / pessimistic)
- 0.50 2 ticks (very pessimistic)
Tells us the break-even cost where Sharpe crosses zero.
2. **Annualised Sharpe**: per-trade Sharpe × sqrt(trades_per_year).
trades_per_year = n_trades × (seconds_per_year / test_time_span_seconds).
test_time_span_seconds derived from first/last test sequence end-bar
timestamps via FxCacheReader::record_timestamp. Standard Sharpe-time-
scaling assumption (trades roughly i.i.d.); imperfect when signals
cluster in correlated regimes, but the right ballpark for comparison
with industry benchmarks.
Output adds per-cost-band "best operating point" tables plus a clear
"REALISTIC VERDICT" line at cost=0.125 (half-tick — what a professional
desk would actually pay) with three gates:
- Sharpe_ann > 2.0 → deployable
- 0.5 < Sharpe_ann ≤ 2.0 → marginal
- Sharpe_ann ≤ 0.5 → fail at realistic cost
The "FRICTIONLESS UPPER BOUND" line reports the intrinsic edge — what
the model could theoretically achieve at zero cost. Even if realistic
Sharpe fails, this number tells us whether the model has anything to
optimise toward at deployment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Bool flag with `default_value_t = true` doesn't accept `--stacker true`
on the command line in clap — it expects either the flag alone (which
inverts) or a custom action. Cleanest fix: drop the flag entirely;
the stacker block always runs when cal_frac is in (0, 1).
Phase 1d.3 stacker delivered the key result:
- Stacker test AUC: 0.7078 (raw Mamba: 0.6619, +4.6pts)
- Stacker test accuracy: 0.6683 (raw Mamba: 0.6187, +5.0pts)
- Stacker test Brier: 0.2088 (raw Mamba: 0.2299, well below chance 0.25)
- Stacker spread-Q4 accuracy: 0.8164 (raw Mamba spread-Q4: 0.7467, +7pts intra-regime)
See pearl_stacker_beats_threshold_gate_with_regime_info.md for full
write-up and design implications for Phase 1d.4 backtest.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Builds a second-stage MLP that takes [mamba_logit, 6 Block-S features]
as input (7 dims) and learns the joint alpha-and-regime score in one
calibrated output. Trains on the cal half of val (same 50/50 split as
Platt/isotonic so all comparisons are on the same held-out test bars).
Architecture: 7 → hidden_dim → 1 sigmoid, GELU activation, BCE-with-logits
loss, AdamW. Uses the existing GPU-native `MlpModel` from
crates/ml-alpha/src/mlp.rs — same primitives used for the Phase 1c MLP
baseline. No CPU compute on the hot path (per feedback_cpu_is_read_only);
all weights, activations, gradients, optimizer state on GPU; host writes
the input matrix to a pinned buffer once per batch via GpuTensor::from_host.
The Block-S columns are z-score normalised using cal-half statistics
(then applied to the full val matrix) before training; mamba_logit is
left raw since it's already close to standard-normal scale via the
Mamba's natural calibration (see pearl_mamba_sss_state_yields_native_calibration).
After training, reports stacker held-out accuracy + AUC + Brier +
log-loss, plus stratified accuracy by Block-S feature so we can see
whether the stacker absorbed the regime conditioning (uniform accuracy
across quintiles) or just sharpened the Q4-gate (still elevated in Q4).
Why this matters for production deployment per pearl_mamba_inherits_regime_structure:
- Single calibrated score for conformal coverage gating downstream
- Retrainable when market regimes drift
- Captures interactions between regime features that a static threshold
AND can't (e.g., spread-Q4 only when book is balanced)
- Replaces the planned Phase 1d.3 dual-head architecture with a smaller
stacked-generalisation approach (no separate regime classifier)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After calibration, also stratify val accuracy across the 6 Block-S
features (time_since_trade, time_since_snap, book_event_rate, spread_bps,
L1_imbalance, micro_mid_drift) by sampling each val sequence's END BAR
feature value, then running `metrics_detail::stratified_accuracy` per
column with 5 quintile bins.
Tells us whether the K=6000 Mamba alpha concentrates in specific book
regimes (justifying an explicit regime head per Phase 1d.3) or is
uniform across regimes (allowing direct backtest in Phase 1d.4). The
Phase 1c stateless MLP showed strong stratification (spread-Q4 hit
0.752 acc on 76K samples while middle quintiles fell below 0.50);
this run tests whether the Mamba inherits or transcends that pattern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends phase1d_long_horizon with a 50/50 val split (cal/test halves):
fit Platt and Isotonic on cal, evaluate on held-out test. Reports both
uncalibrated AND calibrated metrics (accuracy, AUC, Brier, log-loss).
Hypothesis: the K=6000 Mamba result (AUC=0.66 / acc=0.62 from the
uncalibrated 4cf9499b5 smoke) has a 4-point AUC-accuracy gap which
mirrors the Phase 1c pattern; Platt should compress that gap and lift
accuracy further without retraining.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The DECISIVE gate for FoxhuntQ-Δ's two-head architecture. The K-sweep
(commit db874b184) showed stateless single-snapshot alpha decays from
K=50 peak to gone by K=500. The two-head design exists to amplify
short-horizon evidence into long-horizon prediction via SSM state
accumulation; this smoke is the actual test of that hypothesis.
Gate per the implementation plan:
- AUC > 0.55 at K=6000 → multi-minute alpha confirmed, design validated
- AUC < 0.52 → decisive FAIL, design dead in current form
- 0.52 ≤ AUC ≤ 0.55 → marginal, tune or extend seq_len
New module `multi_horizon_labels.rs` generates labels at arbitrary K
with tie-drops + NaN guards (mirror of `purged_split::binary_direction_label`
semantics but bypassing Phase1aConfig's hardcoded K=100). 5 unit tests
covering: strict-ramp all-ones, constant-series all-tied, K-too-large
edge case, index alignment with mixed up/down/tied, non-finite drops.
New example `phase1d_long_horizon.rs` loads snapshot fxcache, generates
K=6000 labels, splits 80/20 with horizon-sized embargo (so train sequences
ending near boundary don't share forward-window prices with val), trains
Mamba2 (seq_len=32, hidden=64, state=16), reports val AUC.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Trains the from-scratch GPU-pure Mamba2 block against the snapshot fxcache,
gathers sequence batches via end-bar lookup into train/val labels, runs
AdamW for N epochs, computes val AUC.
First-shot result (epochs=3, stride=8, lr=1e-3, hidden=64, state=16, seq_len=32):
- Train BCE: 2.338 → 1.164 → 0.957 (monotone, still dropping)
- Val accuracy: 0.5645 (beats MLP 0.5241)
- Val AUC: 0.5684 (below MLP 0.6849)
Interpretation: undertrained (loss curve still descending steeply; stride=8
sees only 1/8 of data; lr=1e-3 conservative given the training-loop unit
test converged at lr=1e-2). Not yet a clean GATE FAIL — needs a retry with
stride=2, lr=3e-3, epochs=10-20 before declaring the model class has a
ceiling below the stateless MLP baseline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
GPU-pure stateful encoder skeleton for the snapshot-stream falsification.
This session lands the build infrastructure + weight allocation + kernel
loading; forward/backward + training loop in follow-up sessions.
- build.rs compiles `../ml/src/cuda_pipeline/mamba2_temporal_kernel.cu` to
`mamba2_temporal_kernel.cubin` in OUT_DIR (rerun-if-env-changed=CUDA_COMPUTE_CAP
per the L40S/H100 cubin-staleness pattern). Zero header dependencies → single
nvcc invocation; no NVRTC.
- `Mamba2Block` holds all parameters on GPU (`OwnedGpuLinear` from ml-core
for the projection layers, raw `CudaSlice<f32>` for `W_c` which the kernel
reads directly). Xavier init via ml-core, which uses pinned host buffers
for the seed transfer.
- Both `mamba2_scan_projected_fwd` and `mamba2_scan_projected_bwd` kernel
symbols resolve at construction; forward and backward paths in follow-up.
- State dim hardcoded at ≤16 in the kernel; config validation rejects >16.
Tests (3 passing on real GPU):
- Reject state_dim > 16
- Reject zero dims
- Constructs + loads both kernels + correct param count (8417 for 81×64×16×1)
Aligns with project memories:
- feedback_no_nvrtc: pre-compiled cubin via build.rs
- feedback_no_htod_htoh_only_mapped_pinned: pinned via ml-core init helpers
- ml-alpha invariant: no `ml`/`ml-supervised` dep (only the .cu source file)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Platt scaling drops held-out Brier from 0.346 → 0.221 (chance=0.250);
log-loss 1.096 → 0.632. Both Platt and isotonic land below chance baseline.
AUC-accuracy gap was pure miscalibration, not fundamental misexpression.
Learned: Platt a=0.32 (raw logits too extreme), b=0.89 (positive offset
needed). Trained MLP underconfidence on positives compounds with negative
prior. Proceed to 1d.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three things landing atomically because they're load-bearing for each other:
1. **Trend-scanning leakage fix** — trend_scanning.rs was emitting OLS slope+t-stat
over a *forward* window [t, t+L]. With the Phase 1a label = sign(price[t+60]
− price[t]), the forward feature window overlaps the label window, contaminating
it. Purged walk-forward only sterilizes forward-looking *labels* that cross
the train/val split, not forward-looking *features* that peek inside the same
horizon the label measures. The leak inflated MLP accuracy from 0.49
(legacy 74-dim baseline) to 0.75 — vanished to 0.50 after switching to a
trailing window. Bounded the perfect-fit t-stat sentinel from ±1e6 → ±20
(p<1e-30 is already meaningless); eliminated the 16k corruption-cap drops.
2. **Variable-dim alpha column** — fxcache schema now carries the alpha-feature
width via metadata (`alpha_feature_dim`), not a compile-time constant. Same
on-disk format hosts the 134-dim bar-level stack OR the 81-dim snapshot stack.
Reader + auto-detect honor the metadata-declared dim; downstream MLP auto-sizes
`in_dim`. Single schema, no forks.
3. **Snapshot pipeline (Phase 1c falsification)** — `snapshot_pipeline.rs`: 81-dim
per-MBP10-snapshot extractor reusing 10 snapshot-native alpha blocks + 6 new
snapshot-specific features (time-since-trade, time-since-snap, event-rate,
spread-bps, L1-imbalance, microprice-mid drift). `precompute_features` gets
`--row-unit snapshot` flag; emits one fxcache row per LOB update (1.97M rows
from MBP-10 data vs 206K for bar mode).
**Smoke verdict on real data** (ES.FUT, 1.97M snapshots, 384K val):
- Bar-level honest alpha: accuracy=0.5005, AUC=0.5043 (no signal)
- **Snapshot-level alpha**: accuracy=0.5241, AUC=0.6849 (real signal, 384K val)
- GBM corroboration: accuracy=0.5401 (non-linear partitioning sees more)
- Horizon decay: alpha peaks at K=20-50 snapshots (~5-25ms), gone by K=500
- Regime-conditional: spread-Q4 quintile hits 0.752 accuracy on 76k samples
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>