From 4f888abbf09ad4ed02c4be1815fde8d424822e9e Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Tue, 19 May 2026 09:03:09 +0200 Subject: [PATCH] feat(ml-alpha): PerceptionTrainer::forward_only + from_checkpoint (X11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X11 inference interface for the deployability backtester. Two new public methods on PerceptionTrainer: forward_only(snapshots) -> Vec Forward-only pass over a K-snapshot window. Wraps evaluate() with dummy zero-valued labels and discards the loss. Returns the same [K, B, N_HORIZONS]-shaped probability output as evaluate_batched. from_checkpoint(dev, cfg, path) -> Result Build a PerceptionTrainer ready for inference: constructs a fresh trainer (with random init) to wire up kernel handles + grad buffers + scratches, then overwrites the trunk's weights from the Checkpoint file via CfcTrunk::load_checkpoint. The optimizer + grad buffers stay allocated — unused at inference, but allocating them keeps the struct invariant uniform. A leaner inference-only struct can be added later if memory matters. Architectural note: the trunk is the source of truth for weights (X1-X9 established that). PerceptionTrainer is the kernel-launch adapter that drives forward + backward over those weights. Inference doesn't need a separate forward path on the trunk — the trainer's evaluate_batched already does the full chain correctly. fxt-backtest can now construct a PerceptionTrainer in inference role via from_checkpoint + call forward_only per decision. Verification: ml-alpha + ml-backtesting + fxt-backtest build clean. ml-alpha lib tests: 33 pass. --- crates/ml-alpha/src/trainer/perception.rs | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) 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],