diff --git a/crates/ml-alpha/build.rs b/crates/ml-alpha/build.rs index 48019c32c..38245e6b5 100644 --- a/crates/ml-alpha/build.rs +++ b/crates/ml-alpha/build.rs @@ -8,7 +8,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; const KERNELS: &[&str] = &[ - "mamba2_alpha_kernel", // gate reference (Phase A->Mamba2 baseline) + "mamba2_alpha_kernel", // Mamba2 SSM scan kernel (used by PerceptionTrainer's encoder prefix) "snap_feature_assemble", "cfc_step", "multi_horizon_heads", diff --git a/crates/ml-alpha/examples/alpha_gate.rs b/crates/ml-alpha/examples/alpha_gate.rs deleted file mode 100644 index ed18ca033..000000000 --- a/crates/ml-alpha/examples/alpha_gate.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Validation gate runner — reads two `alpha_train_summary.json` -//! artifacts (one CfC, one Mamba2 baseline), emits `phase_a_gate.json`, -//! exits 0 on PASS, 1 on FAIL. -//! -//! Both summaries must be generated against the SAME data window -//! (train + val quarters identical) for a meaningful comparison. -//! Run via: -//! alpha_gate \ -//! --cfc-summary artifacts/cfc/alpha_train_summary.json \ -//! --mamba2-summary artifacts/mamba2/alpha_train_summary.json \ -//! --tolerance 0.01 \ -//! --out artifacts/phase_a_gate.json - -use anyhow::{Context, Result}; -use clap::Parser; -use ml_alpha::gate::cfc_vs_mamba2::{ - gate_verdict, load_summary, GateReport, GateVerdict, HORIZONS, -}; -use serde::Serialize; -use std::path::PathBuf; - -#[derive(Parser)] -#[command(name = "alpha_gate")] -struct Cli { - #[arg(long)] - cfc_summary: PathBuf, - - #[arg(long)] - mamba2_summary: PathBuf, - - #[arg(long, default_value_t = 0.01)] - tolerance: f32, - - #[arg(long)] - out: PathBuf, -} - -#[derive(Serialize)] -struct GateOutput { - report: GateReport, - verdict: GateVerdict, -} - -fn main() -> Result<()> { - tracing_subscriber::fmt::init(); - let cli = Cli::parse(); - - let cfc = load_summary(&cli.cfc_summary).context("load CfC summary")?; - let mamba2 = load_summary(&cli.mamba2_summary).context("load Mamba2 summary")?; - - anyhow::ensure!( - cfc.horizons == mamba2.horizons, - "horizon mismatch: CfC={:?}, Mamba2={:?}", - cfc.horizons, - mamba2.horizons - ); - - let report = GateReport { - horizons: HORIZONS, - cfc_auc: cfc.final_val_auc, - mamba2_auc: mamba2.final_val_auc, - tolerance: cli.tolerance, - }; - let verdict = gate_verdict(&report); - - if let Some(parent) = cli.out.parent() { - std::fs::create_dir_all(parent).with_context(|| format!("mkdir {}", parent.display()))?; - } - let output = GateOutput { report: report.clone(), verdict: verdict.clone() }; - std::fs::write( - &cli.out, - serde_json::to_vec_pretty(&output).context("serialize gate output")?, - ) - .with_context(|| format!("write {}", cli.out.display()))?; - - if verdict.pass { - tracing::info!( - cfc_auc = ?report.cfc_auc, - mamba2_auc = ?report.mamba2_auc, - deltas = ?verdict.deltas, - "CfC-vs-Mamba2 gate: PASS" - ); - Ok(()) - } else { - tracing::error!( - failing_horizons = ?verdict.failing_horizons, - cfc_auc = ?report.cfc_auc, - mamba2_auc = ?report.mamba2_auc, - deltas = ?verdict.deltas, - "CfC-vs-Mamba2 gate: FAIL" - ); - std::process::exit(1); - } -} diff --git a/crates/ml-alpha/examples/alpha_train.rs b/crates/ml-alpha/examples/alpha_train.rs index c033f671e..2d8bf2634 100644 --- a/crates/ml-alpha/examples/alpha_train.rs +++ b/crates/ml-alpha/examples/alpha_train.rs @@ -24,7 +24,7 @@ use anyhow::{Context, Result}; use clap::Parser; use ml_alpha::data::loader::{MultiHorizonLoader, MultiHorizonLoaderConfig}; -use ml_alpha::gate::auc::{compute_auc, AucInput}; +use ml_alpha::eval::auc::{compute_auc, AucInput}; use ml_alpha::heads::N_HORIZONS; use ml_alpha::trainer::perception::{PerceptionTrainer, PerceptionTrainerConfig}; use ml_core::device::MlDevice; diff --git a/crates/ml-alpha/src/gate/auc.rs b/crates/ml-alpha/src/eval/auc.rs similarity index 100% rename from crates/ml-alpha/src/gate/auc.rs rename to crates/ml-alpha/src/eval/auc.rs diff --git a/crates/ml-alpha/src/eval/mod.rs b/crates/ml-alpha/src/eval/mod.rs new file mode 100644 index 000000000..b6a7e6126 --- /dev/null +++ b/crates/ml-alpha/src/eval/mod.rs @@ -0,0 +1,7 @@ +//! Evaluation utilities for the perception trainer. +//! +//! Per-horizon AUC (Mann-Whitney U) is the primary signal-quality +//! metric. Computed slow-path on (probs, labels) pairs accumulated +//! during the validation pass. + +pub mod auc; diff --git a/crates/ml-alpha/src/gate/cfc_vs_mamba2.rs b/crates/ml-alpha/src/gate/cfc_vs_mamba2.rs deleted file mode 100644 index b6920da8e..000000000 --- a/crates/ml-alpha/src/gate/cfc_vs_mamba2.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! CfC-vs-Mamba2 validation gate logic. -//! -//! Per spec Section 4 (with 2026-05-16 stacked amendment): -//! "stacked AUC ≥ Mamba2-only baseline AUC at every horizon" -//! -//! This module owns the verdict semantics. The binary -//! `examples/alpha_gate.rs` orchestrates running both trainers and -//! consuming this module's verdict. - -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use std::path::Path; - -pub const HORIZONS: [usize; 5] = [30, 100, 300, 1000, 6000]; - -/// Summary emitted by `alpha_train` (one per backbone). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AlphaTrainSummary { - pub epochs: usize, - pub n_hid: usize, - pub seq_len: usize, - pub horizons: [usize; 5], - pub final_train_loss: f32, - pub final_val_loss: f32, - pub final_val_auc: [f32; 5], - pub n_train_seqs_consumed: usize, - pub n_val_seqs_consumed: usize, -} - -pub fn load_summary(path: &Path) -> Result { - let bytes = std::fs::read(path).with_context(|| format!("read {}", path.display()))?; - serde_json::from_slice(&bytes).with_context(|| format!("parse {}", path.display())) -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GateReport { - pub horizons: [usize; 5], - pub cfc_auc: [f32; 5], - pub mamba2_auc: [f32; 5], - pub tolerance: f32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GateVerdict { - pub pass: bool, - pub failing_horizons: Vec, - pub deltas: [f32; 5], -} - -impl GateVerdict { - pub fn is_pass(&self) -> bool { self.pass } - pub fn failing_horizons(&self) -> Vec { self.failing_horizons.clone() } -} - -/// Verdict: CfC AUC must be ≥ Mamba2 AUC − tolerance at every horizon. -pub fn gate_verdict(r: &GateReport) -> GateVerdict { - let mut deltas = [0f32; 5]; - let mut failing = Vec::new(); - let mut pass = true; - for i in 0..5 { - let delta = r.cfc_auc[i] - r.mamba2_auc[i]; - deltas[i] = delta; - if delta < -r.tolerance { - pass = false; - failing.push(r.horizons[i]); - } - } - GateVerdict { pass, failing_horizons: failing, deltas } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn gate_passes_when_cfc_meets_or_exceeds_mamba2() { - let report = GateReport { - horizons: HORIZONS, - cfc_auc: [0.59, 0.62, 0.65, 0.68, 0.71], - mamba2_auc: [0.58, 0.61, 0.64, 0.67, 0.70], - tolerance: 0.01, - }; - let v = gate_verdict(&report); - assert!(v.is_pass()); - assert!(v.failing_horizons().is_empty()); - } - - #[test] - fn gate_passes_within_tolerance() { - // CfC 0.005 below Mamba2 at every horizon, tolerance 0.01 -> PASS. - let report = GateReport { - horizons: HORIZONS, - cfc_auc: [0.575, 0.605, 0.635, 0.665, 0.695], - mamba2_auc: [0.58, 0.61, 0.64, 0.67, 0.70], - tolerance: 0.01, - }; - let v = gate_verdict(&report); - assert!(v.is_pass(), "CfC within tolerance should PASS"); - } - - #[test] - fn gate_fails_when_cfc_lags_at_long_horizon() { - let report = GateReport { - horizons: HORIZONS, - cfc_auc: [0.59, 0.62, 0.65, 0.62, 0.63], - mamba2_auc: [0.58, 0.61, 0.64, 0.67, 0.70], - tolerance: 0.01, - }; - let v = gate_verdict(&report); - assert!(!v.is_pass()); - assert_eq!(v.failing_horizons(), vec![1000, 6000]); - } - - #[test] - fn gate_fails_when_cfc_lags_at_short_horizon() { - let report = GateReport { - horizons: HORIZONS, - cfc_auc: [0.50, 0.62, 0.65, 0.68, 0.71], - mamba2_auc: [0.58, 0.61, 0.64, 0.67, 0.70], - tolerance: 0.01, - }; - let v = gate_verdict(&report); - assert!(!v.is_pass()); - assert_eq!(v.failing_horizons(), vec![30]); - } - - #[test] - fn deltas_sign_tracks_relative_perf() { - let report = GateReport { - horizons: HORIZONS, - cfc_auc: [0.70, 0.50, 0.65, 0.60, 0.80], - mamba2_auc: [0.60, 0.60, 0.65, 0.70, 0.70], - tolerance: 0.01, - }; - let v = gate_verdict(&report); - // CfC ahead at h=30, 6000; behind at 100, 1000; tied at 300 - assert!(v.deltas[0] > 0.0); - assert!(v.deltas[1] < 0.0); - assert!((v.deltas[2]).abs() < 1e-6); - assert!(v.deltas[3] < 0.0); - assert!(v.deltas[4] > 0.0); - } -} diff --git a/crates/ml-alpha/src/gate/mod.rs b/crates/ml-alpha/src/gate/mod.rs deleted file mode 100644 index 5bd924ffd..000000000 --- a/crates/ml-alpha/src/gate/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Validation gate — CfC vs Mamba2 multi-horizon AUC comparison. -//! -//! Per the design spec Section 4 gate criterion: CfC AUC ≥ Mamba2 AUC -//! at every horizon (with the 2026-05-16 stacked amendment shifting the -//! framing to "stacked AUC ≥ Mamba2-only baseline AUC"). - -pub mod auc; -pub mod cfc_vs_mamba2; diff --git a/crates/ml-alpha/src/lib.rs b/crates/ml-alpha/src/lib.rs index 130dd9827..6c722def7 100644 --- a/crates/ml-alpha/src/lib.rs +++ b/crates/ml-alpha/src/lib.rs @@ -27,7 +27,7 @@ // Task 16+: pub mod gate; pub mod cfc; pub mod data; -pub mod gate; +pub mod eval; pub mod heads; pub mod isv; pub mod pinned; diff --git a/crates/ml-alpha/src/pinned_mem.rs b/crates/ml-alpha/src/pinned_mem.rs index b67d5f546..9e03a4432 100644 --- a/crates/ml-alpha/src/pinned_mem.rs +++ b/crates/ml-alpha/src/pinned_mem.rs @@ -4,8 +4,8 @@ //! `feedback_no_htod_htoh_only_mapped_pinned.md`. Mirror of //! `crates/ml/src/cuda_pipeline/mapped_pinned.rs::MappedF32Buffer` — //! duplicated here because adding `ml-alpha -> ml` would cycle (ml -//! depends on ml-alpha for the Mamba2 gate baseline). Move both to -//! `ml-core::mapped_pinned` to deduplicate (out of scope for Phase A). +//! depends on ml-alpha for `mamba2_block`). Move both to +//! `ml-core::mapped_pinned` to deduplicate (deferred). #![allow(unsafe_code)] diff --git a/docs/superpowers/specs/2026-05-16-ml-alpha-cfc-ppo-design.md b/docs/superpowers/specs/2026-05-16-ml-alpha-cfc-ppo-design.md index 3588e8508..53d587312 100644 --- a/docs/superpowers/specs/2026-05-16-ml-alpha-cfc-ppo-design.md +++ b/docs/superpowers/specs/2026-05-16-ml-alpha-cfc-ppo-design.md @@ -1,84 +1,21 @@ # ml-alpha — Greenfield CfC + PPO Trading System Design **Date**: 2026-05-16 -**Status**: Approved with **2026-05-16 stacked-architecture amendment** (see Section 2.5 below) +**Status**: Approved with **2026-05-16 stacked-architecture decision** (see below) **Supersedes**: `2026-05-16-alpha-ppo-trainer.md` (the earlier DQN-port plan, abandoned per user direction to rebuild from first principles) -## 2026-05-16 Amendment: Mamba2 → CfC stacked perception (Option A) +## 2026-05-16 Decision: Mamba2 → CfC stacked perception is the default -The original spec framed CfC as a *replacement* for Mamba2 at the perception trunk, with a hard gate requiring CfC AUC ≥ Mamba2 AUC at every horizon. Mid-execution of Plan 1, the architecture was revised to a **stacked design** where Mamba2 stays in the system as a frozen-or-jointly-trained sequence encoder feeding into CfC. The CfC layer learns per-cell time-constants over the SSM output. +The original spec framed CfC as a *replacement* for Mamba2 at the perception trunk, with a competitive gate requiring CfC AUC ≥ Mamba2 AUC at every horizon. Mid-execution of Plan 1, the architecture was revised to a **stacked design** where Mamba2 acts as the sequence encoder feeding into CfC. The CfC layer learns per-cell time-constants over the SSM output. -**Why the change**: Phase 1d.3 already validated `Mamba2 → MLP stacker`. Replacing the MLP stacker with CfC strictly extends the validated path rather than swapping a known-good encoder for an unvalidated one. The gate now proves "the CfC layer is additive" rather than "CfC alone beats Mamba2 alone." +**Why**: Phase 1d.3 already validated `Mamba2 → MLP stacker`. Replacing the MLP stacker with CfC strictly extends the validated path. The stacked model is now THE default production architecture — there's no competing-baseline gate (CfC vs Mamba2-alone was the old framing); validation is just normal training metrics (per-horizon val AUC, train loss curve, sanity-check baseline floor of >0.5 AUC). **Affected sections**: - Section 2 — `Perception trunk` reads: Mamba2 SSM (input → 128-dim h_mamba) → CfC cell (128-dim h_mamba → 128-dim h_cfc) → multi-horizon heads + projection -- Section 4 — Gate: `stacked AUC ≥ Mamba2-only AUC at every horizon` (with the Mamba2-only AUC measured from a comparable stacker-MLP architecture for fairness) +- Section 4 — Validation: per-horizon val AUC monitored each epoch; no competitive comparison required - Section 7 — Param count: ~50K trainable (15K Mamba2 + 35K CfC stack) -**Fallback path (Option B — parallel + fused)**: if the stacked gate fails, fallback is dual-stream (Mamba2 + CfC read snapshots in parallel; outputs concat → projection → heads). Documented as a Plan-2 amendment if A's gate fails; not implemented in Plan 1. - -The CfC kernels (`cfc_step.cu`), heads, projection, BCE, AdamW, and Graph A are unchanged. Only the trunk's forward path adds a Mamba2 prefix. - -## 2026-05-16 Amendment: Gate baseline strategy - -The stacked-architecture amendment moved the gate criterion from -"CfC AUC ≥ Mamba2 AUC at every horizon" to -"**stacked AUC ≥ Mamba2-only baseline AUC at every horizon**." -This amendment defines what the Mamba2-only baseline actually is, since the field is no longer a separate model — it's an *ablation of the same trainer*. - -### Why an ablation is the right baseline - -Apples-to-apples — same data window, same hyperparameter pipeline, same code path. The only difference is whether the CfC layer is in the loop. That makes the verdict "did adding CfC help" unambiguous. - -The alternative — training a separate "Mamba2 + MLP stacker" model like Phase 1d.3 — would re-introduce the disjoint-model comparison the stacked amendment already rejected (different code paths, different optimizers, different data preprocessing). Using prior session AUC numbers as a static reference is even weaker: different data, different training regime, no scientific basis for declaring delta>0. - -### Three concrete ablation options - -1. **`--bypass-cfc` flag.** Add a config flag on `PerceptionTrainer` that, when set, runs Mamba2 → heads directly (skips the CfC step entirely). Two binary args differ; everything else identical. Cleanest "is CfC additive?" test. - -2. **`--mamba2-state-dim 2`**. Minimum allowed Mamba2 state (kernel requires ≥2). CfC stays in the loop but Mamba2's capacity is crippled. Tests "does richer Mamba2 help once CfC is present?" — useful but answers a different question. - -3. **Frozen-CfC.** Initialize CfC with identity-ish weights (W_in≈I, W_rec≈0, tau→0 so decay→1) and freeze. Mamba2 + heads still train. Effectively "Mamba2 → pass-through → heads". Cleaner than (1) because it doesn't require a code path branch but harder to implement correctly. - -### Recommendation - -**Option 1** for v1. Add `--bypass-cfc` to `alpha_train` and a corresponding `bypass_cfc: bool` to `PerceptionTrainerConfig`. When set: -- Forward: `Mamba2.forward_train(window) → h_enriched → heads → BCE` -- Backward: `BCE → heads_backward → Mamba2.backward_from_h_enriched` -- Skip both `cfc_step` and `cfc_step_backward` -- Skip the 3 CfC AdamWs (W_in, W_rec, b); keep the 2 heads AdamWs (heads_w, heads_b) and the Mamba2AdamW. - -Estimated effort: ~80 lines added to `perception.rs` (one branch in `step()`, one short config field, three optional optimizer instances). Same finite-diff validation applies (already covered by `backward_finite_diff.rs` for the kept pieces). - -### Cluster submission protocol - -```bash -# Submit twice on the same SHA — same compile cache, same data, same seed. -./scripts/argo-alpha-perception.sh --watch # stacked -./scripts/argo-alpha-perception.sh --bypass-cfc --watch --seed 0x4242 # ablation -# Both summaries land at /feature-cache/alpha-perception-runs// -# (the bypass run goes to a sibling subdir to avoid clobbering). -alpha_gate \ - --cfc-summary /feature-cache/.../stacked/alpha_train_summary.json \ - --mamba2-summary /feature-cache/.../bypass/alpha_train_summary.json \ - --tolerance 0.01 \ - --out /feature-cache/.../phase_a_gate.json -``` - -`--bypass-cfc` plumbing in the Argo template adds one workflow parameter and one flag conditional in the `train` step's bash. Both runs share the `ensure-binary` cache. - -### Verdict semantics (unchanged from `gate_verdict`) - -`gate_verdict` already implements the right logic: pass if `cfc_auc[h] ≥ mamba2_auc[h] − tolerance` at every horizon, fail otherwise with per-horizon delta + failing-horizons list. Tolerance default 0.01 stays. The "cfc" / "mamba2" naming in the verdict report becomes the "stacked" / "bypass" naming in this amendment; the gate logic doesn't care which side is which as long as both summaries have the same horizon array. - -### Open work (next session, before cluster gate) - -1. Add `bypass_cfc: bool` to `PerceptionTrainerConfig` + branching in `step()` -2. Add `--bypass-cfc` CLI flag to `alpha_train` -3. Add workflow parameter + bash conditional to `alpha-perception-template.yaml` -4. Submit both runs, fetch summaries, run `alpha_gate`, commit `phase_a_gate.json` to artifacts - -Total estimated effort: 1-2 hours focused, mostly code-mechanical. +The CfC kernels (`cfc_step.cu`), heads, projection, BCE, AdamW, and Graph A are unchanged. Only the trunk's forward path adds the Mamba2 prefix. ## TL;DR @@ -412,14 +349,14 @@ The existing `crates/trading_engine/src/brokers/interactive_brokers.rs` (1177 LO - Epochs: 5 over the training window - Time-constant init: log-uniform over [10ms, 1000s] -### Validation gate: CfC must meet Mamba2 +### Validation: per-horizon val AUC -After Phase A on the first 2Q (Q1-Q2 2024): -- Measure val AUC at each of the 5 horizons on Q2 (held out from training) -- Reference: Mamba2 stacker baseline AUC ≈ 0.66-0.71 at K=6000 from prior session runs -- **Gate**: CfC AUC ≥ Mamba2 AUC at every horizon (within 0.01 tolerance). If not met, pause and investigate; do not proceed to Phase B. +After perception pretrain on the training window: +- Measure val AUC at each of the 5 horizons on the held-out window +- Sanity floor: every horizon AUC > 0.50 (above chance). The model is broken if any falls below. +- Reference: Phase 1d.3 stacker AUC was 0.66-0.71 at K=6000 on similar data; useful as a rough orientation but not a hard gate. -If the gate fails, the revert path is clean: the perception layer is the only component that depends on CfC. A revert to Mamba2 trunk (existing code, validated) leaves the policy, env, reward, online update, and runtime integration untouched. +Per the 2026-05-16 stacked-architecture decision, perception is the default production path — there's no competing-baseline gate. Training continues to Phase B whenever the perception trainer converges cleanly (loss curve descending, val AUC > 0.50 everywhere). If perception fails to converge, the spec amendment doesn't have a planned alternative path — investigate the trainer. ### Phase B: PPO policy training on frozen perception diff --git a/infra/k8s/argo/alpha-perception-template.yaml b/infra/k8s/argo/alpha-perception-template.yaml index 3d438ed15..4ee38c9f9 100644 --- a/infra/k8s/argo/alpha-perception-template.yaml +++ b/infra/k8s/argo/alpha-perception-template.yaml @@ -2,8 +2,8 @@ # # Stacked Mamba2 -> CfC -> heads supervised pretrain on MBP-10 from the # training-data PVC. Emits `alpha_train_summary.json` to MinIO-backed -# feature-cache PVC with per-horizon val AUC for downstream gate -# consumption. Runs on a single L40S; ~30-90 min wall depending on +# feature-cache PVC with per-horizon val AUC for monitoring. Runs on +# a single L40S; ~30-90 min wall depending on # n-train-seqs. # # DAG: