feat(ml-alpha): PerceptionTrainer::forward_only + from_checkpoint (X11)

X11 inference interface for the deployability backtester. Two new
public methods on PerceptionTrainer:

  forward_only(snapshots) -> Vec<f32>
      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<Self>
      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.
This commit is contained in:
jgrusewski
2026-05-19 09:03:09 +02:00
parent 87b8303950
commit 4f888abbf0

View File

@@ -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<Vec<f32>> {
// 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<Self> {
// 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],