diff --git a/crates/ml/examples/alpha_compose_backtest.rs b/crates/ml/examples/alpha_baseline.rs similarity index 72% rename from crates/ml/examples/alpha_compose_backtest.rs rename to crates/ml/examples/alpha_baseline.rs index a131d85cb..09aeb3ad8 100644 --- a/crates/ml/examples/alpha_compose_backtest.rs +++ b/crates/ml/examples/alpha_baseline.rs @@ -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 = 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 = (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::(cli.horizon * STATE_DIM)?; - let mut next_states_dev = stream.alloc_zeros::(cli.horizon * STATE_DIM)?; - let mut actions_dev = stream.alloc_zeros::(cli.horizon)?; - let mut rewards_dev = stream.alloc_zeros::(cli.horizon)?; - let mut dones_dev = stream.alloc_zeros::(cli.horizon)?; - let mut q_current_dev = stream.alloc_zeros::(cli.horizon * N_ACTIONS)?; - let mut q_next_dev = stream.alloc_zeros::(cli.horizon * N_ACTIONS)?; - let mut target_dev = stream.alloc_zeros::(cli.horizon)?; - let mut single_state_dev = stream.alloc_zeros::(STATE_DIM)?; - let mut single_q_dev = stream.alloc_zeros::(N_ACTIONS)?; - // C51 device buffers (always allocated; cheap). - let mut probs_current_dev = stream.alloc_zeros::(cli.horizon * N_ACTIONS * c51_n_atoms)?; - let mut probs_next_dev = stream.alloc_zeros::(cli.horizon * N_ACTIONS * c51_n_atoms)?; - let mut m_dev = stream.alloc_zeros::(cli.horizon * c51_n_atoms)?; - let mut single_probs_dev = stream.alloc_zeros::(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::(h_enriched_buf_capacity) - .context("alloc h_enriched buffer")?; - let mut mamba2_block: Option = 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 = 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 = 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 = vec![0.0; train_batch * STATE_DIM]; let mut next_states_host_train: Vec = 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 = 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, ®ime_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::() / (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, diff --git a/crates/ml/src/env/loaders.rs b/crates/ml/src/env/loaders.rs index 5f6b022b1..6230e06cc 100644 --- a/crates/ml/src/env/loaders.rs +++ b/crates/ml/src/env/loaders.rs @@ -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: //! diff --git a/scripts/alpha_pipeline.sh b/scripts/alpha_pipeline.sh index e47cb14b9..16ef185e0 100755 --- a/scripts/alpha_pipeline.sh +++ b/scripts/alpha_pipeline.sh @@ -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 diff --git a/scripts/walk_forward_cv.sh b/scripts/walk_forward_cv.sh index bc0c59d22..bb676c40b 100755 --- a/scripts/walk_forward_cv.sh +++ b/scripts/walk_forward_cv.sh @@ -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