feat: wire temporal pipeline + fix hold + add aux_frequency

1. Temporal pipeline in training forward (submit_forward_ops_main):
   - mamba2_step: temporal scan enriches h_s2 with history
   - compute_predictive_coding_loss: temporal smoothness
   - apply_regime_dropout: regime-conditioned dropout
   - launch_isv_temporal_route: per-feature temporal weights
   - risk_budget_forward: risk budget from h_s2
   These were only in the monitoring path (reduce_current_q_stats),
   never in the actual training forward. The model trained without
   any temporal context.

2. ISV signal update after each adam step:
   update_isv_signals() called after mamba2 backward, reads pinned
   loss/grad_norm/Q-mean and updates the 12-element ISV vector.
   Was never called during training — ISV signals stayed at zero.

3. Backtest min_hold fixed:
   eval_min_hold was hardcoded 0 — no hold enforcement in validation.
   Now uses config.min_hold_bars.

4. Backtest ISV signals:
   Passes frozen ISV signals from last training step to validation
   backtest for adaptive hold enforcement.

5. aux_frequency parameter (default 4):
   IQL, IQN, attention, CQL run every 4th step instead of every step.
   ~4x faster epochs. graph_forward still runs every step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-17 22:15:57 +02:00
parent f9aedb1b05
commit 8499b4ee1b
5 changed files with 66 additions and 11 deletions

View File

@@ -282,6 +282,8 @@ pub struct GpuBacktestEvaluator {
config: GpuBacktestConfig,
/// Precomputed annualization factor: sqrt(bars_per_day * 252).
annualization_factor: f32,
/// ISV signals device pointer for adaptive hold. 0 = NULL (static hold).
isv_signals_ptr: u64,
/// Actions buffer for forward kernel output (reused across steps).
forward_actions_buf: CudaSlice<i32>,
@@ -532,6 +534,7 @@ impl GpuBacktestEvaluator {
portfolio_dim: PORTFOLIO_AND_MTF_DIM,
state_dim,
annualization_factor: (config.bars_per_day * config.trading_days_per_year).sqrt(),
isv_signals_ptr: 0,
config,
forward_actions_buf,
action_select_kernel: None,
@@ -548,6 +551,12 @@ impl GpuBacktestEvaluator {
})
}
/// Set ISV signals pointer for adaptive hold in validation backtest.
/// Pass the frozen ISV signals from the last training step.
pub fn set_isv_signals_ptr(&mut self, ptr: u64) {
self.isv_signals_ptr = ptr;
}
// ── Private helpers ───────────────────────────────────────────────────────
/// Build the initial portfolio state vector.
@@ -955,7 +964,7 @@ impl GpuBacktestEvaluator {
shared_mem_bytes: 0,
};
let null_portfolio: u64 = 0;
let eval_min_hold: i32 = 0;
let eval_min_hold: i32 = self.config.min_hold_bars;
let eval_max_pos: f32 = 0.0;
unsafe {
self.stream
@@ -981,7 +990,7 @@ impl GpuBacktestEvaluator {
.arg(&1.0_f32) // eps_urg_mult (no-op at eps=0)
.arg(&(chunk_start as i32)) // timestep for stateless Philox RNG
.arg(&0u64) // per_sample_epsilon: NULL → use cosine schedule
.arg(&0u64) // isv_signals_ptr: NULL → static hold (backtest)
.arg(&self.isv_signals_ptr) // ISV signals for adaptive hold
.launch(launch_cfg)
.map_err(|e| MLError::ModelError(format!(
"chunked action_select chunk_start={chunk_start}: {e}"

View File

@@ -7874,14 +7874,36 @@ impl GpuDqnTrainer {
// ── 1. Forward (cuBLAS SGEMM — Pass 1 + 2, no Pass 3) ──────────
self.launch_cublas_forward()?;
// ── 1b. ISV + plan heads inside graph capture (same as working 11b1a1ca9) ──
// ── 1b. Temporal + ISV + plan pipeline (full training forward) ──
{
let batch_size = self.config.batch_size;
// Mamba2: temporal scan enriches h_s2 with history
self.mamba2_step(batch_size)?;
// Predictive coding: temporal smoothness loss on enriched trunk
self.compute_predictive_coding_loss(batch_size)?;
// Regime-conditioned dropout on h_s2
self.apply_regime_dropout(batch_size, true)?;
// ISV forward: encoder MLP → branch gate + gamma mod
self.launch_isv_forward()?;
// ISV temporal routing: per-feature temporal_weight for next mamba2
self.launch_isv_temporal_route()?;
// ISV feature gate: modulate h_s2 features by regime embedding
self.launch_isv_feature_gate(batch_size)?;
// Recursive confidence: predict own TD-error from h_s2
self.launch_recursive_confidence_forward(batch_size)?;
// Trade plan: h_s2 → 6 plan parameters
self.launch_trade_plan_forward(batch_size)?;
// plan_noise_inject runs outside graph (pre-replay) — see fused_training.rs
// Risk budget: h_s2 → R ∈ (0,1) before Q-value computation
self.risk_budget_forward(batch_size)?;
}
// ── 2+3. Loss + gradient (blended MSE + C51 via c51_alpha ramp) ─

View File

@@ -565,6 +565,11 @@ pub struct DQNHyperparameters {
/// Default: 5 (about 5 minutes for 1-min bars). Range: [2, 20].
pub min_hold_bars: usize,
/// Auxiliary ops (IQL, IQN, attention, CQL) run every Nth training step.
/// Default: 4 (aux ops every 4th step, ~4× faster epochs).
/// 1 = every step (full quality), higher = faster but less frequent aux updates.
pub aux_frequency: usize,
// P2-B Enhancement: Cash reserve requirement
/// Cash reserve requirement as percentage of portfolio value (0-100)
/// Default: 0.0 (no reserve, backward compatible)
@@ -1344,6 +1349,7 @@ impl DQNHyperparameters {
initial_capital: 100_000.0, // $100K default
bars_per_day: 390.0, // 1-minute bars (6.5h × 60min)
min_hold_bars: 5, // 5-bar minimum prevents coin-flip exits (1-2 bar churn)
aux_frequency: 4, // aux ops every 4th step (~4× faster epochs)
// P2-B Enhancement
cash_reserve_percent: 0.0, // Default: no reserve (backward compatible)

View File

@@ -184,6 +184,8 @@ pub(crate) struct FusedTrainingCtx {
stream: Arc<cudarc::driver::CudaStream>,
/// Batch size at creation time -- must match `current_batch_size` to reuse CUDA Graph.
batch_size: usize,
/// Aux ops run every Nth step (IQL, IQN, attention, CQL).
aux_frequency: usize,
/// Steps since last flat buffer sync (deferred to epoch boundary).
steps_since_varmap_sync: usize,
/// GPU-resident copy of best-epoch params. Pure DtoD — no Candle, no safetensors.
@@ -731,6 +733,7 @@ impl FusedTrainingCtx {
}
},
phase_batch_count: 0,
aux_frequency: hyperparams.aux_frequency.max(1),
graph_aux: None,
graph_spectral: None,
graph_mega: None,
@@ -966,13 +969,16 @@ impl FusedTrainingCtx {
}
}
// ── Step 3: Auxiliary ops (graph_aux via cudarc wrapper) ────────
if self.graph_aux.is_some() {
self.pre_replay_state_update(agent);
self.graph_aux.as_ref().unwrap().0.launch()
.map_err(|e| anyhow::anyhow!("graph_aux replay: {e}"))?;
} else {
self.submit_aux_ops(agent, gpu_batch)?;
// ── Step 3: Auxiliary ops (every Nth step for speed) ────────
let aux_freq = self.aux_frequency;
if step % aux_freq == 0 {
if self.graph_aux.is_some() {
self.pre_replay_state_update(agent);
self.graph_aux.as_ref().unwrap().0.launch()
.map_err(|e| anyhow::anyhow!("graph_aux replay: {e}"))?;
} else {
self.submit_aux_ops(agent, gpu_batch)?;
}
}
if let Some(ref pe) = self.phase_events {
PhaseEvents::record(pe.fwd_bwd_end, cu_stream);
@@ -1031,6 +1037,13 @@ impl FusedTrainingCtx {
self.mamba2_backward(self.batch_size)?;
self.step_mamba2_adam()?;
// ── Step 5a-isv: Update ISV signals from training scalars ────────
// Reads pinned loss/grad_norm/Q-mean (written by GPU during adam),
// updates the 12-element ISV signal vector + history buffer.
// Must run AFTER adam (scalars valid) and BEFORE next forward (ISV used).
self.trainer.update_isv_signals()
.map_err(|e| anyhow::anyhow!("ISV signal update: {e}"))?;
// ── Step 5a-risk: Risk branch gentle weight decay ───────────────
// MVP training signal: decay toward initial values to prevent R
// collapse. Full BPTT backward deferred.

View File

@@ -501,6 +501,11 @@ impl DQNTrainer {
self.gpu_evaluator = Some(evaluator);
}
// Wire ISV signals to evaluator for adaptive hold in validation
if let (Some(ref mut eval), Some(ref fused)) = (&mut self.gpu_evaluator, &self.fused_ctx) {
eval.set_isv_signals_ptr(fused.isv_signals_dev_ptr());
}
// ── Extract weight pointers + v_range, then split borrow for fused_ctx ──
// DuelingWeightSet / BranchingWeightSet contain raw u64 device pointers.
// We extract stable pointers to them, then take &mut fused_ctx for QValueProvider.