diff --git a/crates/ml-alpha/src/trainer/perception.rs b/crates/ml-alpha/src/trainer/perception.rs index 458f50854..1d0f8c446 100644 --- a/crates/ml-alpha/src/trainer/perception.rs +++ b/crates/ml-alpha/src/trainer/perception.rs @@ -2316,6 +2316,56 @@ impl PerceptionTrainer { self.trunk.save_checkpoint(path).context("trunk save_checkpoint") } + /// X11 inference entry point: forward-only pass over `snapshots` + /// (length `cfg.n_batch * cfg.seq_len` per the trainer's batching). + /// Returns per-horizon probabilities flattened as `[K, B, N_HORIZONS]` + /// in row-major layout — same shape as `evaluate_batched`'s second + /// return value but with no loss computed and no labels required. + /// + /// The backtester calls this per decision: load `PerceptionTrainer` + /// from a Checkpoint via `from_checkpoint`, maintain a sliding K-window + /// of recent snapshots, take the last K position's probs as the + /// decision signal. + pub fn forward_only(&mut self, snapshots: &[Mbp10RawInput]) -> Result> { + // Mamba2 + the full perception chain process the K snapshots + // together; BCE consumes the labels. For inference we pass + // zero-valued dummy labels and discard the resulting loss. + let dummy_labels: Vec<[f32; N_HORIZONS]> = + vec![[0.0_f32; N_HORIZONS]; snapshots.len()]; + let (_loss, probs) = self.evaluate(snapshots, &dummy_labels)?; + Ok(probs) + } + + /// X11 checkpoint-loaded constructor: instantiates a PerceptionTrainer + /// from a Checkpoint file, ready for `forward_only` inference. The + /// optimizer state + gradient buffers ARE allocated (training-only + /// state); they're unused at inference but allocating them keeps + /// the struct invariant simple. For pure-inference deployments where + /// the memory matters, a leaner "InferenceOnly" variant can be added + /// later. + pub fn from_checkpoint( + dev: &MlDevice, + cfg: &PerceptionTrainerConfig, + path: &std::path::Path, + ) -> Result { + // Construct a fresh trainer (with random init) to wire up all + // grad buffers + kernel handles + scratches, then overwrite the + // trunk's weights from the checkpoint via load_checkpoint. + let mut trainer = Self::new(dev, cfg)?; + let trunk_cfg = crate::cfc::trunk::CfcConfig { + n_in: FEATURE_DIM, + n_hid: HIDDEN_DIM, + cfc_n_in: HIDDEN_DIM, + mamba2_state_dim: cfg.mamba2_state_dim, + n_batch: cfg.n_batch, + seq_len: cfg.seq_len, + }; + let new_trunk = crate::cfc::trunk::CfcTrunk::load_checkpoint(dev, &trunk_cfg, path) + .context("PerceptionTrainer::from_checkpoint: trunk load")?; + trainer.trunk = new_trunk; + Ok(trainer) + } + pub fn evaluate( &mut self, snapshots: &[Mbp10RawInput],