feat(phase-e-4-a): mirror --temporal in backtest binary + T11 ISV-continual

Phase E.4.A Tasks 11+12: backtest binary gains the same
--temporal Mamba2 forward chain as the smoke binary, plus the
--isv-continual flag that fires the stacker-threshold controller
at the end of each eval episode (Pillar B).

Changes:
- Imports: ml_alpha::mamba2_block::{Mamba2Block, Mamba2BlockConfig},
  ml_core::cuda_autograd::gpu_tensor::GpuTensor
- CLI flags: --temporal, --window-k, --mamba2-hidden-dim,
  --mamba2-state-dim, --isv-continual
- Cubin loading: alpha_window_push + stacker_threshold_controller
- Q-net sizing: c51_input_dim = mamba2_hidden_dim when --c51 --temporal
- Buffers: window_tensor GpuTensor, h_enriched_buf_dev, isv_dev,
  ctl_wiener_dev
- Training inference path: push + Mamba2 forward + h_enriched →
  C51 forward (mirrors smoke binary)
- Training batched compute: terminal Mamba2 forward, h_enriched_buf
  for current/next, c51_input_dim threading
- Eval inference path: push + Mamba2 forward + h_enriched → C51
  forward + Thompson select (with scoped borrow guard)
- T11 ISV-continual: stacker-threshold controller fires at end of
  each eval episode; ISV slot 543 (threshold), 545 (observed-rate),
  546 (Kelly atten) update with realized rollout stats. Co-exists
  with the τ-grid sweep (τ-grid still gates; ISV updates parallel
  observation of "live deployment" behaviour).
- JSON output: new fields temporal, window_k, mamba2_hidden_dim,
  mamba2_state_dim, isv_continual.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-15 21:28:40 +02:00
parent 75d67bd203
commit 4d65ace625

View File

@@ -41,6 +41,10 @@ use clap::Parser;
use cudarc::driver::{CudaContext, DevicePtr, DevicePtrMut}; use cudarc::driver::{CudaContext, DevicePtr, DevicePtrMut};
use tracing::info; use tracing::info;
// Phase E.4.A.8/T12: Mamba2 temporal encoder (ml-alpha Phase 1d.1).
use ml_alpha::mamba2_block::{Mamba2Block, Mamba2BlockConfig};
use ml_core::cuda_autograd::gpu_tensor::GpuTensor;
// ── Mapped-pinned helpers (mirror gpu_training_guard.rs MappedBuffer) ── // ── Mapped-pinned helpers (mirror gpu_training_guard.rs MappedBuffer) ──
struct MappedI32 { struct MappedI32 {
@@ -234,6 +238,22 @@ struct Cli {
/// ±0.125-tick. Phase E.3 Path 3 follow-up. /// ±0.125-tick. Phase E.3 Path 3 follow-up.
#[arg(long, default_value_t = false)] #[arg(long, default_value_t = false)]
real_spread: bool, real_spread: bool,
/// Phase E.4.A: enable the temporal-encoder stack (sliding window +
/// Mamba2 + C51).
#[arg(long, default_value_t = false)]
temporal: bool,
#[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,
#[arg(long, default_value = "config/ml/alpha_compose_backtest.json")] #[arg(long, default_value = "config/ml/alpha_compose_backtest.json")]
out_path: PathBuf, out_path: PathBuf,
} }
@@ -353,6 +373,17 @@ fn main() -> Result<()> {
let c51_project_kernel = c51_module.load_function("alpha_c51_project_kernel")?; 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_grad_kernel = c51_module.load_function("alpha_c51_grad_kernel")?;
let c51_thompson_kernel = c51_module.load_function("alpha_c51_thompson_select_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")?;
// Phase E.4.A.7 controller (used in --isv-continual eval path).
let ctl_module = ctx
.load_cubin(ml::cuda_pipeline::alpha_kernels::STACKER_THRESHOLD_CONTROLLER_CUBIN.to_vec())
.context("stacker controller cubin")?;
let ctl_kernel = ctl_module
.load_function("stacker_threshold_controller_update")?;
let c51_n_atoms = cli.c51_n_atoms; let c51_n_atoms = cli.c51_n_atoms;
let c51_v_min = cli.c51_vmin; let c51_v_min = cli.c51_vmin;
let c51_v_max = cli.c51_vmax; let c51_v_max = cli.c51_vmax;
@@ -414,14 +445,19 @@ fn main() -> Result<()> {
); );
// --- Initialize Q-network --- // --- 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 { let n_weights_eff: usize = if cli.c51 {
N_ACTIONS * c51_n_atoms * STATE_DIM N_ACTIONS * c51_n_atoms * c51_input_dim
} else { } else {
N_WEIGHTS N_WEIGHTS
}; };
let n_biases_eff: usize = if cli.c51 { N_ACTIONS * c51_n_atoms } else { N_BIASES }; let n_biases_eff: usize = if cli.c51 { N_ACTIONS * c51_n_atoms } else { N_BIASES };
let mut rng = SmokeRng::new(cli.seed.wrapping_add(0xDEAD_BEEF)); let mut rng = SmokeRng::new(cli.seed.wrapping_add(0xDEAD_BEEF));
let xavier_scale = (2.0_f32 / STATE_DIM as f32).sqrt(); let xavier_scale = (2.0_f32 / c51_input_dim as f32).sqrt();
let w_init: Vec<f32> = (0..n_weights_eff) let w_init: Vec<f32> = (0..n_weights_eff)
.map(|_| xavier_scale * 2.0 * (rng.next_f32() - 0.5)) .map(|_| xavier_scale * 2.0 * (rng.next_f32() - 0.5))
.collect(); .collect();
@@ -455,6 +491,37 @@ fn main() -> Result<()> {
let state_pinned = unsafe { MappedF32::new(STATE_DIM)? }; let state_pinned = unsafe { MappedF32::new(STATE_DIM)? };
let action_pinned = unsafe { MappedI32::new()? }; let action_pinned = unsafe { MappedI32::new()? };
// 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 mamba2_block: Option<Mamba2Block> = if cli.temporal {
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={}",
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
};
let n_atoms_i = c51_n_atoms as i32;
// ISV buffer + Wiener state for the eval-time controller path.
let mut isv_host: Vec<f32> = vec![0.0; 552];
isv_host[ml::cuda_pipeline::alpha_isv_slots::RANDOM_BASELINE_MEAN_INDEX] = -5191.53;
isv_host[ml::cuda_pipeline::alpha_isv_slots::RANDOM_BASELINE_STD_INDEX] = 4963.62;
isv_host[ml::cuda_pipeline::alpha_isv_slots::TRADE_RATE_TARGET_INDEX] = 0.08;
let mut isv_dev = stream.clone_htod(&isv_host).context("upload isv")?;
let mut ctl_wiener_dev = stream.alloc_zeros::<f32>(3).context("alloc ctl wiener")?;
// --------------------------------------------------------------- // ---------------------------------------------------------------
// Phase 1: TRAIN DQN on train segment. // Phase 1: TRAIN DQN on train segment.
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -475,11 +542,51 @@ fn main() -> Result<()> {
let mut actions_host: Vec<i32> = Vec::with_capacity(cli.horizon); let mut actions_host: Vec<i32> = Vec::with_capacity(cli.horizon);
let mut rewards_host: Vec<f32> = Vec::with_capacity(cli.horizon); let mut rewards_host: Vec<f32> = Vec::with_capacity(cli.horizon);
let mut dones_host: Vec<f32> = Vec::with_capacity(cli.horizon); let mut dones_host: Vec<f32> = Vec::with_capacity(cli.horizon);
// Phase E.4.A.7 (T12): zero window at episode start.
if cli.temporal {
stream.memset_zeros(window_tensor.data_mut())
.context("zero window on episode reset (train)")?;
}
loop { loop {
let s_vec = env.state(&state).to_vec(); let s_vec = env.state(&state).to_vec();
let action: u8 = if cli.c51 { let action: u8 = if cli.c51 {
state_pinned.write(&s_vec); state_pinned.write(&s_vec);
{ // Phase E.4.A.7 (T12): push and run Mamba2.
if cli.temporal {
let (w_ptr, _g1) = window_tensor.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_window_push(
&stream, &push_kernel,
state_pinned.dev_u64(), w_ptr,
cli.window_k as i32, state_dim_i,
)?;
}
}
if cli.temporal {
let block = mamba2_block.as_ref().expect("Mamba2Block missing");
let (_logit, cache) = block.forward_train(&window_tensor)
.map_err(|e| anyhow::anyhow!("mamba2 forward: {e}"))?;
// Store h_enriched for batched training.
let h_host = stream.clone_dtoh(cache.h_enriched.cuda_data())?;
let slot_offset = state.step * cli.mamba2_hidden_dim;
let mut buf_host = stream.clone_dtoh(&h_enriched_buf_dev)?;
for j in 0..cli.mamba2_hidden_dim {
buf_host[slot_offset + j] = h_host[j];
}
stream.memcpy_htod(&buf_host, &mut h_enriched_buf_dev)?;
// C51 forward on h_enriched.
let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream);
let (p_ptr, _g2) = single_probs_dev.device_ptr_mut(&stream);
let (h_ptr, _g3) = cache.h_enriched.cuda_data().device_ptr(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward(
&stream, &c51_fwd_kernel,
w_ptr, b_ptr, h_ptr, p_ptr,
1, c51_input_dim as i32, n_act_i, n_atoms_i,
)?;
}
} else {
let (w_ptr, _g0) = w_dev.device_ptr(&stream); let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream); let (b_ptr, _g1) = b_dev.device_ptr(&stream);
let (p_ptr, _g2) = single_probs_dev.device_ptr_mut(&stream); let (p_ptr, _g2) = single_probs_dev.device_ptr_mut(&stream);
@@ -487,7 +594,7 @@ fn main() -> Result<()> {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward( ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward(
&stream, &c51_fwd_kernel, &stream, &c51_fwd_kernel,
w_ptr, b_ptr, state_pinned.dev_u64(), p_ptr, w_ptr, b_ptr, state_pinned.dev_u64(), p_ptr,
1, state_dim_i, n_act_i, n_atoms_i, 1, c51_input_dim as i32, n_act_i, n_atoms_i,
)?; )?;
} }
} }
@@ -564,30 +671,64 @@ fn main() -> Result<()> {
stream.memcpy_htod(&rewards_norm, &mut rewards_dev)?; stream.memcpy_htod(&rewards_norm, &mut rewards_dev)?;
stream.memcpy_htod(&dones_host, &mut dones_dev)?; stream.memcpy_htod(&dones_host, &mut dones_dev)?;
if cli.c51 { if cli.c51 {
// C51 forward/project/grad // Phase E.4.A.8 (T12): in temporal mode, the batched C51
// forwards read h_enriched_buf_dev populated during the
// per-step inference. Run one extra Mamba2 forward on the
// terminal window to fill slot ep_len.
if cli.temporal {
let block = mamba2_block.as_ref().expect("Mamba2Block missing");
let (_logit, cache) = block.forward_train(&window_tensor)
.map_err(|e| anyhow::anyhow!("mamba2 terminal forward: {e}"))?;
let h_host_term = stream.clone_dtoh(cache.h_enriched.cuda_data())?;
let term_offset = (ep_len as usize) * cli.mamba2_hidden_dim;
let mut buf_host = stream.clone_dtoh(&h_enriched_buf_dev)?;
for j in 0..cli.mamba2_hidden_dim {
buf_host[term_offset + j] = h_host_term[j];
}
stream.memcpy_htod(&buf_host, &mut h_enriched_buf_dev)?;
}
let (curr_input_ptr_guard, next_input_ptr_guard);
let (curr_input_ptr, next_input_ptr): (u64, u64) = if cli.temporal {
let (h_ptr_curr, g_curr) = h_enriched_buf_dev.device_ptr(&stream);
let (h_ptr_next_base, g_next) = h_enriched_buf_dev.device_ptr(&stream);
curr_input_ptr_guard = g_curr;
next_input_ptr_guard = g_next;
let _ = (&curr_input_ptr_guard, &next_input_ptr_guard);
(
h_ptr_curr,
h_ptr_next_base + (cli.mamba2_hidden_dim as u64) * 4u64,
)
} else {
let (s_ptr, g_curr) = states_dev.device_ptr(&stream);
let (n_ptr, g_next) = next_states_dev.device_ptr(&stream);
curr_input_ptr_guard = g_curr;
next_input_ptr_guard = g_next;
let _ = (&curr_input_ptr_guard, &next_input_ptr_guard);
(s_ptr, n_ptr)
};
// C51 forward online → probs_current
{ {
let (w_ptr, _g0) = w_dev.device_ptr(&stream); let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream); let (b_ptr, _g1) = b_dev.device_ptr(&stream);
let (s_ptr, _g2) = states_dev.device_ptr(&stream);
let (p_ptr, _g3) = probs_current_dev.device_ptr_mut(&stream); let (p_ptr, _g3) = probs_current_dev.device_ptr_mut(&stream);
unsafe { unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward( ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward(
&stream, &c51_fwd_kernel, &stream, &c51_fwd_kernel,
w_ptr, b_ptr, s_ptr, p_ptr, w_ptr, b_ptr, curr_input_ptr, p_ptr,
ep_len, state_dim_i, n_act_i, n_atoms_i, ep_len, c51_input_dim as i32, n_act_i, n_atoms_i,
)?; )?;
} }
} }
// C51 forward target → probs_next
{ {
let (w_ptr, _g0) = w_target_dev.device_ptr(&stream); let (w_ptr, _g0) = w_target_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_target_dev.device_ptr(&stream); let (b_ptr, _g1) = b_target_dev.device_ptr(&stream);
let (s_ptr, _g2) = next_states_dev.device_ptr(&stream);
let (p_ptr, _g3) = probs_next_dev.device_ptr_mut(&stream); let (p_ptr, _g3) = probs_next_dev.device_ptr_mut(&stream);
unsafe { unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward( ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward(
&stream, &c51_fwd_kernel, &stream, &c51_fwd_kernel,
w_ptr, b_ptr, s_ptr, p_ptr, w_ptr, b_ptr, next_input_ptr, p_ptr,
ep_len, state_dim_i, n_act_i, n_atoms_i, ep_len, c51_input_dim as i32, n_act_i, n_atoms_i,
)?; )?;
} }
} }
@@ -612,11 +753,12 @@ fn main() -> Result<()> {
let (s_ptr, _g3) = states_dev.device_ptr(&stream); let (s_ptr, _g3) = states_dev.device_ptr(&stream);
let (dw_ptr, _g4) = dw_dev.device_ptr_mut(&stream); let (dw_ptr, _g4) = dw_dev.device_ptr_mut(&stream);
let (db_ptr, _g5) = db_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 { unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_grad( ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_grad(
&stream, &c51_grad_kernel, &stream, &c51_grad_kernel,
p_ptr, m_ptr, a_ptr, s_ptr, dw_ptr, db_ptr, p_ptr, m_ptr, a_ptr, grad_input_ptr, dw_ptr, db_ptr,
ep_len, state_dim_i, n_act_i, n_atoms_i, ep_len, c51_input_dim as i32, n_act_i, n_atoms_i,
1.0 / ep_len as f32, 1.0 / ep_len as f32,
)?; )?;
} }
@@ -752,11 +894,44 @@ fn main() -> Result<()> {
let mut state = EpisodeState::new(); let mut state = EpisodeState::new();
let mut terminal_r = 0.0_f32; let mut terminal_r = 0.0_f32;
let mut ep_n_trades = 0_u32; let mut ep_n_trades = 0_u32;
// Phase E.4.A.7 (T12): zero window at eval episode start.
if cli.temporal {
stream.memset_zeros(window_tensor.data_mut())
.context("zero window on eval episode reset")?;
}
loop { loop {
let s_vec = env.state(&state).to_vec(); let s_vec = env.state(&state).to_vec();
let action: u8 = if cli.c51 { let action: u8 = if cli.c51 {
state_pinned.write(&s_vec); state_pinned.write(&s_vec);
{ // Phase E.4.A (T12): push + Mamba2 + C51. The push
// is scoped in its own block so the mutable borrow
// guard drops before the &window_tensor read.
if cli.temporal {
{
let (w_ptr, _g1) = window_tensor.data_mut().device_ptr_mut(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_window_push(
&stream, &push_kernel,
state_pinned.dev_u64(), w_ptr,
cli.window_k as i32, state_dim_i,
)?;
}
}
let block = mamba2_block.as_ref().expect("Mamba2Block missing");
let (_logit, cache) = block.forward_train(&window_tensor)
.map_err(|e| anyhow::anyhow!("mamba2 forward (eval): {e}"))?;
let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream);
let (p_ptr, _g2) = single_probs_dev.device_ptr_mut(&stream);
let (h_ptr, _g3) = cache.h_enriched.cuda_data().device_ptr(&stream);
unsafe {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward(
&stream, &c51_fwd_kernel,
w_ptr, b_ptr, h_ptr, p_ptr,
1, c51_input_dim as i32, n_act_i, n_atoms_i,
)?;
}
} else {
let (w_ptr, _g0) = w_dev.device_ptr(&stream); let (w_ptr, _g0) = w_dev.device_ptr(&stream);
let (b_ptr, _g1) = b_dev.device_ptr(&stream); let (b_ptr, _g1) = b_dev.device_ptr(&stream);
let (p_ptr, _g2) = single_probs_dev.device_ptr_mut(&stream); let (p_ptr, _g2) = single_probs_dev.device_ptr_mut(&stream);
@@ -764,7 +939,7 @@ fn main() -> Result<()> {
ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward( ml::cuda_pipeline::alpha_kernels::launch_alpha_c51_forward(
&stream, &c51_fwd_kernel, &stream, &c51_fwd_kernel,
w_ptr, b_ptr, state_pinned.dev_u64(), p_ptr, w_ptr, b_ptr, state_pinned.dev_u64(), p_ptr,
1, state_dim_i, n_act_i, n_atoms_i, 1, c51_input_dim as i32, n_act_i, n_atoms_i,
)?; )?;
} }
} }
@@ -832,6 +1007,36 @@ fn main() -> Result<()> {
if terminal_r > 0.0 { if terminal_r > 0.0 {
win_count += 1; win_count += 1;
} }
// Phase E.4.A Pillar B (T11): fire the stacker-threshold
// controller at end of each eval episode when
// --isv-continual. Adapts ISV slot 543/545/546 based on
// observed trade-rate and Sharpe; the τ-grid still
// overrides via `threshold` for the actual gate, but the
// ISV evolution is the "live deployment" simulation.
if cli.isv_continual {
let trade_count_ep = ep_n_trades as f32;
let decisions_ep = (state.step as f32).max(1.0);
let baseline_std = isv_host[ml::cuda_pipeline::alpha_isv_slots::RANDOM_BASELINE_STD_INDEX].max(1e-6);
let rollout_sharpe = terminal_r / baseline_std;
unsafe {
let (isv_ptr, _gi) = isv_dev.device_ptr_mut(&stream);
let (wv_ptr, _gw) = ctl_wiener_dev.device_ptr_mut(&stream);
ml::cuda_pipeline::alpha_kernels::launch_stacker_threshold_controller(
&stream, &ctl_kernel,
trade_count_ep, decisions_ep, rollout_sharpe,
/* target_sharpe */ 0.5,
/* k_threshold */ 0.01,
/* k_atten */ 0.005,
/* wiener_alpha_floor */ 0.4,
/* ctl_alpha_meta */ 0.1,
ml::cuda_pipeline::alpha_isv_slots::STACKER_THRESHOLD_INDEX as i32,
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,
isv_ptr, wv_ptr,
)?;
}
}
} }
let n = rewards.len() as f64; let n = rewards.len() as f64;
let mean = rewards.iter().map(|r| *r as f64).sum::<f64>() / n; let mean = rewards.iter().map(|r| *r as f64).sum::<f64>() / n;
@@ -919,6 +1124,11 @@ fn main() -> Result<()> {
"c51_n_atoms": if cli.c51 { c51_n_atoms } else { 0 }, "c51_n_atoms": if cli.c51 { c51_n_atoms } else { 0 },
"c51_vmin": if cli.c51 { c51_v_min } else { 0.0 }, "c51_vmin": if cli.c51 { c51_v_min } else { 0.0 },
"c51_vmax": if cli.c51 { c51_v_max } 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,
"horizon": cli.horizon, "horizon": cli.horizon,
"train_frac": cli.train_frac, "train_frac": cli.train_frac,
"n_train_episodes": cli.n_train_episodes, "n_train_episodes": cli.n_train_episodes,