refactor(alpha_baseline): rename, drop conditionals, strip dead paths

Rename binary alpha_compose_backtest → alpha_baseline and remove the
boolean flags whose features are now mandatory:

  --c51            (always C51 distributional Q)
  --temporal       (always Mamba2 temporal encoder)
  --isv-continual  (controller always fires per eval episode)
  --regime-scale   (vol-EMA regime defense always on)
  --pruned-actions (FALSIFIED 2026-05-15 per pearl_action_pruning_falsified)

Every dependent code path was stripped, not just gated:

- Linear-Q kernels (lq_fwd, lq_grad, munch_kernel) and their cubin loads
  are gone — C51 is the only Q-network.
- Single-env push_kernel / h_store_kernel loads removed; the backtest
  has been batched-parallel-env since T14 and only the _batched
  variants are called here. (The smoke binary still uses single-env
  variants because one env per episode is its job.)
- Dead transition buffers removed: states_dev, next_states_dev,
  actions_dev, rewards_dev, dones_dev, q_current_dev, q_next_dev,
  target_dev, single_state_dev, single_q_dev, probs_current_dev,
  probs_next_dev, m_dev, single_probs_dev, single-env state_pinned,
  action_pinned, window_tensor, h_enriched_buf_dev.
- Dead constants and helpers: PRUNED_ACTIONS, N_WEIGHTS, N_BIASES,
  epsilon_greedy, epsilon_greedy_gated.

End-to-end verification on the existing Q1 fxcache (rebuild was OOM
locally; full multi-quarter validation is the next phase):

  cost=0.0000  best τ=0.250  Sharpe_ann=+36.83  win=0.984  trades/ep=83.3
  cost=0.0625  best τ=0.250  Sharpe_ann=+38.53  win=0.996  trades/ep=83.2
  cost=0.1250  best τ=0.250  Sharpe_ann=+38.37  win=0.994  trades/ep=83.3
  cost=0.2500  best τ=0.250  Sharpe_ann=+34.24  win=0.990  trades/ep=85.6
  cost=0.5000  best τ=0.250  Sharpe_ann=+31.83  win=0.946  trades/ep=84.8

Numbers track the prior T16-flag config (within stochastic noise),
confirming the conditional-stripping was a pure simplification — no
behavioral change, just a smaller, honester binary.

Also updated:
- scripts/alpha_pipeline.sh — A/B conditions collapse to fixed-cost
  vs cost-randomized training (the only opt-in left).
- scripts/walk_forward_cv.sh — drop legacy flags, pass --window-k only.
- crates/ml/src/env/loaders.rs — module doc-comment updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-16 01:02:44 +02:00
parent 55ffe2b26f
commit 34586dad68
4 changed files with 157 additions and 337 deletions

View File

@@ -148,18 +148,12 @@ use ml::env::execution_env::{
};
const STATE_DIM: usize = 10;
const N_WEIGHTS: usize = N_ACTIONS * STATE_DIM;
const N_BIASES: usize = N_ACTIONS;
/// Action allow-list for the pruning experiment. Same shape as the smoke
/// binary: Q-net is unchanged (9 outputs), only the selector restricts.
const PRUNED_ACTIONS: [u8; 4] = [0, 1, 4, 7]; // Wait, BuyMarket, SellMarket, FlatMarket
const FULL_ACTIONS: [u8; 9] = [0, 1, 2, 3, 4, 5, 6, 7, 8];
#[derive(Debug, Parser)]
#[command(
name = "alpha_compose_backtest",
about = "Phase E.3 Task 23 — composition backtest with cost sweep"
name = "alpha_baseline",
about = "Alpha baseline — trained Mamba2 + C51 + ISV-continual + vol-regime defense"
)]
struct Cli {
#[arg(long)]
@@ -247,15 +241,9 @@ struct Cli {
target_update_every: usize,
#[arg(long, default_value_t = 1.0)]
grad_clip: f32,
/// Restrict action selection to {Wait, BuyMarket, SellMarket, FlatMarket}.
/// Applies during BOTH training and eval. **FALSIFIED 2026-05-15** —
/// kept for reproducibility (see `pearl_action_pruning_falsified`).
#[arg(long, default_value_t = false)]
pruned_actions: bool,
/// Use C51 distributional Q-network (Phase E.3 follow-up). Same
/// semantics as the alpha_dqn_h600_smoke `--c51` flag.
#[arg(long, default_value_t = false)]
c51: bool,
// pruned_actions — FALSIFIED 2026-05-15, hardcoded false.
// c51, temporal, isv_continual, regime_scale — always on now,
// removed from CLI surface.
/// C51 atom-support lower bound (normalized reward units).
#[arg(long, default_value_t = -10.0)]
c51_vmin: f32,
@@ -269,35 +257,14 @@ struct Cli {
/// ±0.125-tick. Phase E.3 Path 3 follow-up.
#[arg(long, default_value_t = false)]
real_spread: bool,
/// Phase E.4.A: enable the temporal-encoder stack (sliding window +
/// Mamba2 + C51).
#[arg(long, default_value_t = false)]
temporal: bool,
/// Temporal-encoder window length (number of historical snapshots
/// fed to Mamba2 per step).
#[arg(long, default_value_t = 16)]
window_k: usize,
#[arg(long, default_value_t = 32)]
mamba2_hidden_dim: usize,
#[arg(long, default_value_t = 16)]
mamba2_state_dim: usize,
/// Phase E.4.A Pillar B: fire stacker-threshold controller at the
/// end of EVERY eval episode (not just training). Slot 543
/// (threshold) adapts during the 2D sweep based on observed trade
/// rate / Sharpe.
#[arg(long, default_value_t = false)]
isv_continual: bool,
/// Phase E.4.A T16: enable vol-EMA regime detection + pre-emptive
/// Kelly attenuation. When set:
/// - per inference step, `alpha_regime_vol_update_kernel` reads
/// the parallel envs' current/previous mids and maintains
/// ISV[549]=vol_ema (Wiener-α with 0.4 floor + Pearl A
/// bootstrap) and ISV[550]=vol_ref (slow tracker, β=0.005);
/// - per eval episode (requires `--isv-continual`), the existing
/// stacker controller multiplies its Sharpe-reactive Kelly
/// output by `clamp(vol_ref/vol_ema, 0.25, 1.0)` so vol spikes
/// cut Kelly *before* losses materialise.
/// Off by default to keep the T10 baseline comparable.
#[arg(long, default_value_t = false)]
regime_scale: bool,
/// Phase E.4.A.T15.batched (2026-05-15): parallel envs per training
/// "epoch". n_train_episodes / n_train_par epochs total. Each epoch
/// runs N_par envs in lockstep H steps + ONE batched C51 update at
@@ -330,38 +297,9 @@ impl SmokeRng {
}
}
fn epsilon_greedy(q: &[f32], eps: f32, allowed: &[u8], rng: &mut SmokeRng) -> u8 {
if rng.next_f32() < eps {
allowed[(rng.next_u64() as usize) % allowed.len()]
} else {
let mut best_a: u8 = allowed[0];
let mut best_v: f32 = q[allowed[0] as usize];
for &a in &allowed[1..] {
let v = q[a as usize];
if v > best_v {
best_v = v;
best_a = a;
}
}
best_a
}
}
/// Phase E.3 confidence-gated ε-greedy. If `alpha_confidence < threshold`,
/// force action=0 (Wait). Otherwise standard ε-greedy over `allowed`.
fn epsilon_greedy_gated(
q: &[f32],
alpha_confidence: f32,
threshold: f32,
eps: f32,
allowed: &[u8],
rng: &mut SmokeRng,
) -> u8 {
if alpha_confidence < threshold {
return 0;
}
epsilon_greedy(q, eps, allowed, rng)
}
// The linear-Q ε-greedy + confidence-gated ε-greedy helpers used by the
// legacy scalar Q path were deleted with the rest of that path. All
// action selection now happens on-GPU via `alpha_c51_thompson_select`.
#[derive(Debug, Clone, serde::Serialize)]
struct CostBin {
@@ -389,35 +327,24 @@ fn main() -> Result<()> {
)
.init();
let cli = Cli::parse();
info!("Phase E.3 Task 23 — composition backtest starting");
let allowed_actions: &[u8] = if cli.pruned_actions {
info!(" ACTION SET: pruned ({} actions: Wait, BuyMarket, SellMarket, FlatMarket)", PRUNED_ACTIONS.len());
&PRUNED_ACTIONS
} else {
info!(" ACTION SET: full ({} actions)", FULL_ACTIONS.len());
&FULL_ACTIONS
};
info!("Alpha baseline — trained Mamba2 + C51 + ISV-continual + vol-regime defense");
info!(" ACTION SET: full ({} actions)", FULL_ACTIONS.len());
let allowed_actions: &[u8] = &FULL_ACTIONS;
let ctx = CudaContext::new(0).context("CUDA init")?;
let stream = ctx.default_stream();
// --- Load cubins ---
// alpha_linear_q.cubin provides two kernels we still need: the
// in-place clip and the SGD step (both used on the C51 head's
// dW / dB after gradients). The forward/grad linear-Q kernels are
// unused (C51 is the only Q-network in this binary).
let lq_module = ctx
.load_cubin(ml::cuda_pipeline::alpha_kernels::ALPHA_LINEAR_Q_CUBIN.to_vec())
.context("alpha_linear_q cubin")?;
let lq_fwd = lq_module.load_function("alpha_linear_q_forward_kernel")?;
let lq_grad = lq_module.load_function("alpha_linear_q_grad_kernel")?;
let lq_sgd = lq_module.load_function("alpha_linear_q_sgd_step_kernel")?;
let lq_clip = lq_module.load_function("alpha_clip_inplace_kernel")?;
let munch_cubin: Vec<u8> = std::fs::read(concat!(
env!("OUT_DIR"),
"/alpha_munchausen_target.cubin"
))?;
let munch_module = ctx.load_cubin(munch_cubin)?;
let munch_kernel =
munch_module.load_function("alpha_munchausen_target_kernel")?;
// Phase E.3 follow-up: C51 kernels (always loaded; only used when --c51).
let c51_module = ctx
.load_cubin(ml::cuda_pipeline::alpha_kernels::ALPHA_C51_CUBIN.to_vec())
.context("alpha_c51 cubin")?;
@@ -425,12 +352,9 @@ fn main() -> Result<()> {
let c51_project_kernel = c51_module.load_function("alpha_c51_project_kernel")?;
let c51_grad_kernel = c51_module.load_function("alpha_c51_grad_kernel")?;
let c51_thompson_kernel = c51_module.load_function("alpha_c51_thompson_select_kernel")?;
// Phase E.4.A.6: window push kernel (used in --temporal path).
let push_module = ctx
.load_cubin(ml::cuda_pipeline::alpha_kernels::ALPHA_WINDOW_PUSH_CUBIN.to_vec())
.context("alpha_window_push cubin")?;
let push_kernel = push_module.load_function("alpha_window_push_kernel")?;
let h_store_kernel = push_module.load_function("alpha_h_enriched_store_kernel")?;
let push_batched_kernel = push_module.load_function("alpha_window_push_batched_kernel")?;
let h_store_batched_kernel = push_module.load_function("alpha_h_enriched_store_batched_kernel")?;
// Phase E.4.A.7 controller (used in --isv-continual eval path).
@@ -443,19 +367,15 @@ fn main() -> Result<()> {
let c51_v_min = cli.c51_vmin;
let c51_v_max = cli.c51_vmax;
let c51_delta_z = (c51_v_max - c51_v_min) / (c51_n_atoms.saturating_sub(1).max(1) as f32);
if cli.c51 {
info!(
" Q-network: C51 ({} atoms, [{:.2}, {:.2}], Δz={:.4})",
c51_n_atoms, c51_v_min, c51_v_max, c51_delta_z
);
if c51_n_atoms == 0 || c51_n_atoms > 64 {
anyhow::bail!("--c51-n-atoms must be in (0, 64]");
}
if c51_v_max <= c51_v_min {
anyhow::bail!("--c51-vmax must exceed --c51-vmin");
}
} else {
info!(" Q-network: linear scalar (9 outputs)");
info!(
" Q-network: C51 ({} atoms, [{:.2}, {:.2}], Δz={:.4})",
c51_n_atoms, c51_v_min, c51_v_max, c51_delta_z
);
if c51_n_atoms == 0 || c51_n_atoms > 64 {
anyhow::bail!("--c51-n-atoms must be in (0, 64]");
}
if c51_v_max <= c51_v_min {
anyhow::bail!("--c51-vmax must exceed --c51-vmin");
}
// --- Load env data ---
@@ -500,18 +420,10 @@ fn main() -> Result<()> {
cli.seed,
);
// --- Initialize Q-network ---
let c51_input_dim: usize = if cli.c51 && cli.temporal {
cli.mamba2_hidden_dim
} else {
STATE_DIM
};
let n_weights_eff: usize = if cli.c51 {
N_ACTIONS * c51_n_atoms * c51_input_dim
} else {
N_WEIGHTS
};
let n_biases_eff: usize = if cli.c51 { N_ACTIONS * c51_n_atoms } else { N_BIASES };
// --- Initialize Q-network (C51 over Mamba2 h_enriched) ---
let c51_input_dim: usize = cli.mamba2_hidden_dim;
let n_weights_eff: usize = N_ACTIONS * c51_n_atoms * c51_input_dim;
let n_biases_eff: usize = N_ACTIONS * c51_n_atoms;
let mut rng = SmokeRng::new(cli.seed.wrapping_add(0xDEAD_BEEF));
let xavier_scale = (2.0_f32 / c51_input_dim as f32).sqrt();
let w_init: Vec<f32> = (0..n_weights_eff)
@@ -527,55 +439,27 @@ fn main() -> Result<()> {
let state_dim_i = STATE_DIM as i32;
let n_act_i = N_ACTIONS as i32;
let n_atoms_i = c51_n_atoms as i32;
let mut states_dev = stream.alloc_zeros::<f32>(cli.horizon * STATE_DIM)?;
let mut next_states_dev = stream.alloc_zeros::<f32>(cli.horizon * STATE_DIM)?;
let mut actions_dev = stream.alloc_zeros::<i32>(cli.horizon)?;
let mut rewards_dev = stream.alloc_zeros::<f32>(cli.horizon)?;
let mut dones_dev = stream.alloc_zeros::<f32>(cli.horizon)?;
let mut q_current_dev = stream.alloc_zeros::<f32>(cli.horizon * N_ACTIONS)?;
let mut q_next_dev = stream.alloc_zeros::<f32>(cli.horizon * N_ACTIONS)?;
let mut target_dev = stream.alloc_zeros::<f32>(cli.horizon)?;
let mut single_state_dev = stream.alloc_zeros::<f32>(STATE_DIM)?;
let mut single_q_dev = stream.alloc_zeros::<f32>(N_ACTIONS)?;
// C51 device buffers (always allocated; cheap).
let mut probs_current_dev = stream.alloc_zeros::<f32>(cli.horizon * N_ACTIONS * c51_n_atoms)?;
let mut probs_next_dev = stream.alloc_zeros::<f32>(cli.horizon * N_ACTIONS * c51_n_atoms)?;
let mut m_dev = stream.alloc_zeros::<f32>(cli.horizon * c51_n_atoms)?;
let mut single_probs_dev = stream.alloc_zeros::<f32>(N_ACTIONS * c51_n_atoms)?;
// Mapped-pinned per-step inference buffers (C51 path).
let state_pinned = unsafe { MappedF32::new(STATE_DIM)? };
let action_pinned = unsafe { MappedI32::new(1)? };
// All per-step inference + per-episode transition buffers live in
// the batched parallel-env section below (states_train_dev etc.,
// batched_window_tensor_train, batched_probs_dev, ...). The
// single-env legacy buffers and the scalar linear-Q transition
// buffers were removed when C51 + temporal + batched became the
// only execution path.
// Phase E.4.A.8 (T12): temporal buffers, allocated unconditionally.
let mut window_tensor = GpuTensor::zeros(
&[1, cli.window_k, STATE_DIM], &stream
).map_err(|e| anyhow::anyhow!("alloc window: {e}"))?;
let h_enriched_buf_capacity = (cli.horizon + 1) * cli.mamba2_hidden_dim;
let mut h_enriched_buf_dev = stream
.alloc_zeros::<f32>(h_enriched_buf_capacity)
.context("alloc h_enriched buffer")?;
let mut mamba2_block: Option<Mamba2Block> = if cli.temporal {
let mut mamba2_block: Mamba2Block = {
let cfg = Mamba2BlockConfig {
in_dim: STATE_DIM,
hidden_dim: cli.mamba2_hidden_dim,
state_dim: cli.mamba2_state_dim,
seq_len: cli.window_k,
};
info!("TEMPORAL Mamba2: in={} hidden={} state={} K={}",
info!("Mamba2: in={} hidden={} state={} K={}",
cfg.in_dim, cfg.hidden_dim, cfg.state_dim, cfg.seq_len);
Some(Mamba2Block::new(cfg, stream.clone())
.map_err(|e| anyhow::anyhow!("Mamba2Block init: {e}"))?)
} else {
None
};
// Phase E.4.A.T10: AdamW for Mamba2 weights (only when --temporal).
let mut mamba2_adamw: Option<Mamba2AdamW> = if let Some(block) = mamba2_block.as_ref() {
Some(Mamba2AdamW::new(block, Mamba2AdamWConfig::default())
.map_err(|e| anyhow::anyhow!("Mamba2AdamW init: {e}"))?)
} else {
None
Mamba2Block::new(cfg, stream.clone())
.map_err(|e| anyhow::anyhow!("Mamba2Block init: {e}"))?
};
let mut mamba2_adamw: Mamba2AdamW = Mamba2AdamW::new(&mamba2_block, Mamba2AdamWConfig::default())
.map_err(|e| anyhow::anyhow!("Mamba2AdamW init: {e}"))?;
// Load the C51 grad-input kernel (for backward chain into Mamba2).
let c51_grad_input_kernel = c51_module
.load_function("alpha_c51_grad_input_kernel")
@@ -663,11 +547,9 @@ fn main() -> Result<()> {
}
let mut par_states: Vec<EpisodeState> = vec![EpisodeState::new(); train_n_par];
let mut done_flags = vec![false; train_n_par];
if cli.temporal {
stream.memset_zeros(batched_window_tensor_train.data_mut())?;
stream.memset_zeros(train_windows_tensor_train.data_mut())?;
stream.memset_zeros(d_h_enriched_tensor_train.data_mut())?;
}
stream.memset_zeros(batched_window_tensor_train.data_mut())?;
stream.memset_zeros(train_windows_tensor_train.data_mut())?;
stream.memset_zeros(d_h_enriched_tensor_train.data_mut())?;
stream.memset_zeros(&mut h_enriched_train_dev)?;
let mut states_host_train: Vec<f32> = vec![0.0; train_batch * STATE_DIM];
let mut next_states_host_train: Vec<f32> = vec![0.0; train_batch * STATE_DIM];
@@ -685,45 +567,44 @@ fn main() -> Result<()> {
}
batched_state_pinned_train.write(&batched_states_host);
if cli.temporal {
{
let (w_ptr, _g) = batched_window_tensor_train.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_window_push_batched(
&stream, &push_batched_kernel,
batched_state_pinned_train.dev_u64(), w_ptr,
train_n_par as i32, cli.window_k as i32, state_dim_i,
)?;
}
{
let (w_ptr, _g) = batched_window_tensor_train.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_window_push_batched(
&stream, &push_batched_kernel,
batched_state_pinned_train.dev_u64(), w_ptr,
train_n_par as i32, cli.window_k as i32, state_dim_i,
)?;
}
// T10: capture post-push windows into [H*N_par, K, in_dim] buffer
// at step row offset `step * N_par`.
{
let (src_ptr, _g_src) = batched_window_tensor_train.data().device_ptr(&stream);
let (dst_ptr, _g_dst) = train_windows_tensor_train.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_train_window_store_batched(
&stream, &train_window_store_kernel,
src_ptr, dst_ptr, (step * train_n_par) as i32,
train_n_par as i32, cli.window_k as i32, state_dim_i,
)?;
}
}
// T10: capture post-push windows into [H*N_par, K, in_dim] buffer
// at step row offset `step * N_par`.
{
let (src_ptr, _g_src) = batched_window_tensor_train.data().device_ptr(&stream);
let (dst_ptr, _g_dst) = train_windows_tensor_train.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_train_window_store_batched(
&stream, &train_window_store_kernel,
src_ptr, dst_ptr, (step * train_n_par) as i32,
train_n_par as i32, cli.window_k as i32, state_dim_i,
)?;
}
let block = mamba2_block.as_ref().expect("Mamba2Block missing");
let (_logit, cache) = block.forward_train(&batched_window_tensor_train)
.map_err(|e| anyhow::anyhow!("train mamba2: {e}"))?;
{
let (src_p, _g_src) = cache.h_enriched.cuda_data().device_ptr(&stream);
let (buf_p, _g_buf) = h_enriched_train_dev.device_ptr_mut(&stream);
let step_row_offset = (step * train_n_par) as i32;
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_h_enriched_store_batched(
&stream, &h_store_batched_kernel,
src_p, buf_p, step_row_offset,
train_n_par as i32, cli.mamba2_hidden_dim as i32,
)?;
}
}
let (_logit, cache) = mamba2_block.forward_train(&batched_window_tensor_train)
.map_err(|e| anyhow::anyhow!("train mamba2: {e}"))?;
{
let (src_p, _g_src) = cache.h_enriched.cuda_data().device_ptr(&stream);
let (buf_p, _g_buf) = h_enriched_train_dev.device_ptr_mut(&stream);
let step_row_offset = (step * train_n_par) as i32;
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_h_enriched_store_batched(
&stream, &h_store_batched_kernel,
src_p, buf_p, step_row_offset,
train_n_par as i32, cli.mamba2_hidden_dim as i32,
)?;
}
}
{
let (h_ptr, _g_h) = cache.h_enriched.cuda_data().device_ptr(&stream);
let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream);
@@ -735,17 +616,6 @@ fn main() -> Result<()> {
train_n_par as i32, c51_input_dim as i32, n_act_i, n_atoms_i,
)?;
}
} else {
let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream);
let (p_ptr, _g3) = batched_probs_inference_train.device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward(
&stream, &c51_fwd_kernel,
w_ptr, b_ptr, batched_state_pinned_train.dev_u64(), p_ptr,
train_n_par as i32, c51_input_dim as i32, n_act_i, n_atoms_i,
)?;
}
}
let step_seed = episode_rng.next_u64() as u32;
{
@@ -795,33 +665,15 @@ fn main() -> Result<()> {
// that backward_from_h_enriched can consume. cache.h_enriched
// replaces h_enriched_train_dev curr offset (bit-identical since
// Mamba2 weights haven't been updated yet this epoch).
let cache_train: Option<ml_alpha::mamba2_block::Mamba2ForwardCache> = if cli.temporal {
let block = mamba2_block.as_ref().expect("Mamba2Block missing");
let (_logit, cache) = block.forward_train(&train_windows_tensor_train)
.map_err(|e| anyhow::anyhow!("train mamba2 (epoch forward): {e}"))?;
Some(cache)
} else {
None
};
let (curr_input_ptr_guard, next_input_ptr_guard);
let (curr_input_ptr, next_input_ptr): (u64, u64) = if cli.temporal {
let cache = cache_train.as_ref().expect("cache_train missing");
let (h_curr, g) = cache.h_enriched.cuda_data().device_ptr(&stream);
let (h_base_next, g_next) = h_enriched_train_dev.device_ptr(&stream);
curr_input_ptr_guard = g;
next_input_ptr_guard = g_next;
let _ = (&curr_input_ptr_guard, &next_input_ptr_guard);
let (_logit, cache_train) = mamba2_block.forward_train(&train_windows_tensor_train)
.map_err(|e| anyhow::anyhow!("train mamba2 (epoch forward): {e}"))?;
let (curr_input_ptr, next_input_ptr): (u64, u64) = {
let (h_curr, _g) = cache_train.h_enriched.cuda_data().device_ptr(&stream);
let (h_base_next, _g_next) = h_enriched_train_dev.device_ptr(&stream);
(
h_curr,
h_base_next + (train_n_par as u64) * (cli.mamba2_hidden_dim as u64) * 4u64,
)
} else {
let (s_ptr, g_c) = states_train_dev.device_ptr(&stream);
let (n_ptr, g_n) = next_states_train_dev.device_ptr(&stream);
curr_input_ptr_guard = g_c;
next_input_ptr_guard = g_n;
let _ = (&curr_input_ptr_guard, &next_input_ptr_guard);
(s_ptr, n_ptr)
};
{
@@ -866,48 +718,40 @@ fn main() -> Result<()> {
let (p_ptr, _g0) = probs_curr_train_dev.device_ptr(&stream);
let (m_ptr, _g1) = m_train_dev.device_ptr(&stream);
let (a_ptr, _g2) = actions_train_dev.device_ptr(&stream);
let (s_ptr, _g3) = states_train_dev.device_ptr(&stream);
let (dw_ptr, _g4) = dw_dev.device_ptr_mut(&stream);
let (db_ptr, _g5) = db_dev.device_ptr_mut(&stream);
let grad_input_ptr = if cli.temporal { curr_input_ptr } else { s_ptr };
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_grad(
&stream, &c51_grad_kernel,
p_ptr, m_ptr, a_ptr, grad_input_ptr, dw_ptr, db_ptr,
p_ptr, m_ptr, a_ptr, curr_input_ptr, dw_ptr, db_ptr,
bt, c51_input_dim as i32, n_act_i, n_atoms_i,
1.0 / bt as f32,
)?;
}
}
// T10 backward chain: C51 grad_input → Mamba2 backward → AdamW step.
if cli.temporal {
{
let (p_ptr, _g0) = probs_curr_train_dev.device_ptr(&stream);
let (m_ptr, _g1) = m_train_dev.device_ptr(&stream);
let (a_ptr, _g2) = actions_train_dev.device_ptr(&stream);
let (w_ptr, _g3) = w_dev.device_ptr(&stream);
let (dh_ptr, _g4) = d_h_enriched_tensor_train.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_grad_input(
&stream, &c51_grad_input_kernel,
p_ptr, m_ptr, a_ptr, w_ptr, dh_ptr,
bt, c51_input_dim as i32, n_act_i, n_atoms_i,
1.0 / bt as f32,
)?;
}
{
let (p_ptr, _g0) = probs_curr_train_dev.device_ptr(&stream);
let (m_ptr, _g1) = m_train_dev.device_ptr(&stream);
let (a_ptr, _g2) = actions_train_dev.device_ptr(&stream);
let (w_ptr, _g3) = w_dev.device_ptr(&stream);
let (dh_ptr, _g4) = d_h_enriched_tensor_train.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_grad_input(
&stream, &c51_grad_input_kernel,
p_ptr, m_ptr, a_ptr, w_ptr, dh_ptr,
bt, c51_input_dim as i32, n_act_i, n_atoms_i,
1.0 / bt as f32,
)?;
}
let cache = cache_train.as_ref().expect("cache_train missing");
let block_ref = mamba2_block.as_ref().expect("Mamba2Block missing");
let grads = block_ref
.backward_from_h_enriched(cache, &d_h_enriched_tensor_train)
.map_err(|e| anyhow::anyhow!("Mamba2 backward: {e}"))?;
let _ = cache_train;
let block_mut = mamba2_block.as_mut().expect("Mamba2Block missing");
let adamw = mamba2_adamw.as_mut().expect("Mamba2AdamW missing");
adamw
.step(block_mut, &grads)
.map_err(|e| anyhow::anyhow!("Mamba2AdamW step: {e}"))?;
}
let grads = mamba2_block
.backward_from_h_enriched(&cache_train, &d_h_enriched_tensor_train)
.map_err(|e| anyhow::anyhow!("Mamba2 backward: {e}"))?;
drop(cache_train);
mamba2_adamw
.step(&mut mamba2_block, &grads)
.map_err(|e| anyhow::anyhow!("Mamba2AdamW step: {e}"))?;
{
let (dw_ptr, _g0) = dw_dev.device_ptr_mut(&stream);
unsafe {
@@ -982,15 +826,12 @@ fn main() -> Result<()> {
// memory with no htod copy.
let mids_curr_pinned = unsafe { MappedF32::new(n_par)? };
let mids_prev_pinned = unsafe { MappedF32::new(n_par)? };
let regime_module = if cli.regime_scale {
Some(ctx.load_cubin(
ml::cuda_pipeline::alpha_kernels::ALPHA_REGIME_VOL_UPDATE_CUBIN.to_vec()
).context("alpha_regime_vol_update cubin load")?)
} else { None };
let regime_kernel = regime_module.as_ref().map(|m|
m.load_function("alpha_regime_vol_update_kernel")
.expect("alpha_regime_vol_update_kernel load")
);
let regime_module = ctx.load_cubin(
ml::cuda_pipeline::alpha_kernels::ALPHA_REGIME_VOL_UPDATE_CUBIN.to_vec()
).context("alpha_regime_vol_update cubin load")?;
let regime_kernel = regime_module
.load_function("alpha_regime_vol_update_kernel")
.context("alpha_regime_vol_update_kernel load")?;
let snapshots_arc = env.snapshots_arc();
let fill_model_for_par = env.fill_model.clone();
let env_config_template = env.config.clone();
@@ -1020,13 +861,11 @@ fn main() -> Result<()> {
let mut ep_n_trades_par = vec![0_u32; n_par];
let mut done_flags = vec![false; n_par];
// Zero batched windows.
if cli.temporal {
stream.memset_zeros(batched_window_tensor.data_mut())
.context("zero batched windows")?;
}
stream.memset_zeros(batched_window_tensor.data_mut())
.context("zero batched windows")?;
// T16: reset prev_mids so the first-step Pearl-A bootstrap fires
// cleanly (kernel guards against curr<=0 || prev<=0 internally).
if cli.regime_scale {
{
let zeros = vec![0.0_f32; n_par];
mids_curr_pinned.write(&zeros);
mids_prev_pinned.write(&zeros);
@@ -1046,7 +885,7 @@ fn main() -> Result<()> {
// env step advances. Regime signal therefore reflects vol
// observed up to and including this step, which is what the
// controller's per-episode Kelly update will read.
if let Some(rk) = regime_kernel.as_ref() {
{
let mut mids_host = vec![0.0_f32; n_par];
for i in 0..n_par {
if !done_flags[i] {
@@ -1057,7 +896,7 @@ fn main() -> Result<()> {
unsafe {
let (isv_ptr, _g) = isv_dev.device_ptr_mut(&stream);
ml::cuda_pipeline::alpha_kernels::launch_alpha_regime_vol_update(
&stream, rk,
&stream, &regime_kernel,
mids_curr_pinned.dev_u64(),
mids_prev_pinned.dev_u64(),
isv_ptr,
@@ -1071,23 +910,22 @@ fn main() -> Result<()> {
// is a CPU-side fill, no device-to-device copy needed.
mids_prev_pinned.write(&mids_host);
}
if cli.temporal {
// Batched window push.
{
let (w_ptr, _g) = batched_window_tensor.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_window_push_batched(
&stream, &push_batched_kernel,
batched_state_pinned.dev_u64(), w_ptr,
n_par as i32, cli.window_k as i32, state_dim_i,
)?;
}
// Batched window push.
{
let (w_ptr, _g) = batched_window_tensor.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_window_push_batched(
&stream, &push_batched_kernel,
batched_state_pinned.dev_u64(), w_ptr,
n_par as i32, cli.window_k as i32, state_dim_i,
)?;
}
// Batched Mamba2 forward → cache.h_enriched [N, hidden].
let block = mamba2_block.as_ref().expect("Mamba2Block missing");
let (_logit, cache) = block.forward_train(&batched_window_tensor)
.map_err(|e| anyhow::anyhow!("batched mamba2 forward (eval): {e}"))?;
// Batched C51 forward.
}
// Batched Mamba2 forward → cache.h_enriched [N, hidden].
let (_logit, cache) = mamba2_block.forward_train(&batched_window_tensor)
.map_err(|e| anyhow::anyhow!("batched mamba2 forward (eval): {e}"))?;
// Batched C51 forward.
{
let (h_ptr, _g_h) = cache.h_enriched.cuda_data().device_ptr(&stream);
let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream);
@@ -1099,18 +937,6 @@ fn main() -> Result<()> {
n_par as i32, c51_input_dim as i32, n_act_i, n_atoms_i,
)?;
}
} else {
// C51 reads directly from batched_state_pinned (B=N).
let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream);
let (p_ptr, _g3) = batched_probs_dev.device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward(
&stream, &c51_fwd_kernel,
w_ptr, b_ptr, batched_state_pinned.dev_u64(), p_ptr,
n_par as i32, c51_input_dim as i32, n_act_i, n_atoms_i,
)?;
}
}
// Batched Thompson select.
let step_seed = episode_rng.next_u64() as u32;
@@ -1156,7 +982,10 @@ fn main() -> Result<()> {
#[allow(unused_mut)]
let mut win_count: usize = rewards.iter().filter(|&&r| r > 0.0).count();
// ISV-continual at cell-level (single fire with aggregate stats).
if cli.isv_continual {
// Threshold + Kelly controllers adapt during the 2D sweep; the
// T16 regime kernel feeds the pre-emptive Kelly attenuation
// multiplier via slots 549/550.
{
let mean_terminal_r = rewards.iter().sum::<f32>() / (n_par as f32);
let trade_count_avg = trade_count_total as f32 / (n_par as f32);
let decisions_avg = cli.horizon as f32;
@@ -1173,15 +1002,8 @@ fn main() -> Result<()> {
ml::cuda_pipeline::alpha_isv_slots::TRADE_RATE_TARGET_INDEX as i32,
ml::cuda_pipeline::alpha_isv_slots::TRADE_RATE_OBSERVED_EMA_INDEX as i32,
ml::cuda_pipeline::alpha_isv_slots::STACKER_KELLY_ATTENUATION_INDEX as i32,
// T16 regime hookup. Active when --isv-continual is on;
// the regime kernel writes slots 549/550 per-step during
// inference. Floor 0.25 = max defensive 4× Kelly cut.
if cli.regime_scale {
ml::cuda_pipeline::alpha_isv_slots::REGIME_VOL_EMA_INDEX as i32
} else { -1 },
if cli.regime_scale {
ml::cuda_pipeline::alpha_isv_slots::REGIME_VOL_REF_INDEX as i32
} else { -1 },
ml::cuda_pipeline::alpha_isv_slots::REGIME_VOL_EMA_INDEX as i32,
ml::cuda_pipeline::alpha_isv_slots::REGIME_VOL_REF_INDEX as i32,
0.25,
isv_ptr, wv_ptr,
)?;
@@ -1266,18 +1088,14 @@ fn main() -> Result<()> {
// --- Save JSON ---
let json = serde_json::json!({
"phase": "E.3 Task 23 (2D sweep)",
"pruned_actions": cli.pruned_actions,
"binary": "alpha_baseline",
"n_allowed_actions": allowed_actions.len(),
"c51": cli.c51,
"c51_n_atoms": if cli.c51 { c51_n_atoms } else { 0 },
"c51_vmin": if cli.c51 { c51_v_min } else { 0.0 },
"c51_vmax": if cli.c51 { c51_v_max } else { 0.0 },
"temporal": cli.temporal,
"window_k": if cli.temporal { cli.window_k } else { 0 },
"mamba2_hidden_dim": if cli.temporal { cli.mamba2_hidden_dim } else { 0 },
"mamba2_state_dim": if cli.temporal { cli.mamba2_state_dim } else { 0 },
"isv_continual": cli.isv_continual,
"c51_n_atoms": c51_n_atoms,
"c51_vmin": c51_v_min,
"c51_vmax": c51_v_max,
"window_k": cli.window_k,
"mamba2_hidden_dim": cli.mamba2_hidden_dim,
"mamba2_state_dim": cli.mamba2_state_dim,
"horizon": cli.horizon,
"train_frac": cli.train_frac,
"n_train_episodes": cli.n_train_episodes,

View File

@@ -1,5 +1,5 @@
//! Phase E loader helpers shared by `alpha_dqn_h600_smoke` and
//! `alpha_compose_backtest` (and any future Phase E binary).
//! `alpha_baseline` (and any future Phase E binary).
//!
//! Three loaders:
//!

View File

@@ -18,7 +18,7 @@ set -euo pipefail
REPO=/home/jgrusewski/Work/foxhunt/.worktrees/sp19-20-wr-first
CACHE_DIR=/home/jgrusewski/Work/foxhunt/test_data/feature-cache
FILL=config/ml/alpha_fill_coeffs.json
BIN_BT=./target/release/examples/alpha_compose_backtest
BIN_BT=./target/release/examples/alpha_baseline
BIN_STACK=./target/release/examples/alpha_train_stacker
cd "$REPO"
@@ -55,7 +55,7 @@ fi
echo "Alpha cache: $ALPHA_OUT"
# ----- 2. Build backtest binary (in case of code changes) -----
SQLX_OFFLINE=true cargo build --release --example alpha_compose_backtest -p ml >/dev/null
SQLX_OFFLINE=true cargo build --release --example alpha_baseline -p ml >/dev/null
# ----- 3. Walk-forward CV across 4 conditions × 3 folds -----
# Fold A: offset=0, window=1.2M → train 0..720K, eval 720K..1.2M
@@ -63,11 +63,13 @@ SQLX_OFFLINE=true cargo build --release --example alpha_compose_backtest -p ml >
# Fold C: offset=1.8M, window=1.2M → train 1.8M..2.52M, eval 2.52M..3.0M
# (Assumes Q1+Q2 fxcache has ~3-6M bars total at MBP-10 resolution.)
# Cost randomization is the only opt-in left: `--train-cost-hi 0.5` enables
# uniform[0.0625, 0.5] per-epoch cost sampling. C51 / temporal / ISV-continual
# / regime-scale are now always-on properties of alpha_baseline; the A/B
# split is now just fixed-cost vs cost-randomised training.
declare -a CONDS=(
"A_baseline:--c51 --temporal --window-k 16 --isv-continual"
"B_costrand:--c51 --temporal --window-k 16 --isv-continual --train-cost-hi 0.5"
"C_regime:--c51 --temporal --window-k 16 --isv-continual --regime-scale"
"D_both:--c51 --temporal --window-k 16 --isv-continual --train-cost-hi 0.5 --regime-scale"
"A_fixed:--window-k 16"
"B_costrand:--window-k 16 --train-cost-hi 0.5"
)
declare -a FOLDS=(
@@ -108,7 +110,7 @@ echo "Alpha pipeline cross-quarter walk-forward summary"
echo "===================================================================="
python3 - <<'PY'
import json, os, statistics
conds = ["A_baseline", "B_costrand", "C_regime", "D_both"]
conds = ["A_fixed", "B_costrand"]
folds = ["A", "B", "C"]
# For each (cond, cost): list of best Sharpe across folds

View File

@@ -13,7 +13,7 @@ set -euo pipefail
FXCACHE="${FXCACHE:-/home/jgrusewski/Work/foxhunt/test_data/feature-cache/9297017b6db6795f75e57be8aefb03e45e6427513f42c08e22521e58fa025d5e.fxcache}"
ALPHA="${ALPHA:-config/ml/alpha_logits_cache.bin}"
FILL="${FILL:-config/ml/alpha_fill_coeffs.json}"
BIN="${BIN:-./target/release/examples/alpha_compose_backtest}"
BIN="${BIN:-./target/release/examples/alpha_baseline}"
WINDOW=700000
TRAIN_FRAC=0.6 # 420K train, 280K eval per fold
@@ -38,7 +38,7 @@ for fold_spec in "${FOLDS[@]}"; do
--data-start-offset "$offset" \
--max-snapshots "$WINDOW" \
--train-frac "$TRAIN_FRAC" \
--c51 --temporal --window-k 16 --isv-continual \
--window-k 16 \
--out-path "$out"
done