From 7545651bce3c05d45d237892835a59f0d9f5a274 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Tue, 19 May 2026 08:41:12 +0200 Subject: [PATCH] feat(ml-alpha): PerceptionTrainer.save_checkpoint + alpha_train wiring (X13+X14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X13: Adds PerceptionTrainer::save_checkpoint as a thin delegate to self.trunk.save_checkpoint. Inference-only serialization โ€” grads + AdamW state aren't included. X14: Inside the existing auc_h6000_improved block in alpha_train.rs, calls trainer.save_checkpoint(out_dir / 'trunk_best_h6000.bin') so the trained trunk lands alongside alpha_train_summary.json. Extends AlphaTrainSummary with best_h6000_ckpt_path (Option) so downstream tooling (fxt-backtest --checkpoint) can locate the file without re-deriving the path. After this commit, every alpha-perception Argo workflow run produces a CheckpointV2 file at every new-best-h6000 epoch, ready for backtest consumption. Verification: - ml-alpha lib tests: 34 pass - alpha_train example builds clean (release) Per spec ยง1.1 (X13+X14). --- crates/ml-alpha/examples/alpha_train.rs | 20 ++++++++++++++++++-- crates/ml-alpha/src/trainer/perception.rs | 8 ++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/ml-alpha/examples/alpha_train.rs b/crates/ml-alpha/examples/alpha_train.rs index ea594aa1e..060aead41 100644 --- a/crates/ml-alpha/examples/alpha_train.rs +++ b/crates/ml-alpha/examples/alpha_train.rs @@ -164,7 +164,7 @@ struct Cli { decision_stride: usize, } -#[derive(Serialize)] +#[derive(Serialize, serde::Deserialize, Default)] struct AlphaTrainSummary { epochs: usize, seq_len: usize, @@ -200,6 +200,11 @@ struct AlphaTrainSummary { best_auc_h6000_per_horizon: [f32; N_HORIZONS], /// True if training stopped early (patience exceeded). early_stopped: bool, + /// Relative path (within `out_dir`) to the best-h6000 trunk + /// checkpoint file, or None if no h6000 improvement was ever recorded. + /// Written by the X14 wiring inside the `auc_h6000_improved` block. + #[serde(default, skip_serializing_if = "Option::is_none")] + best_h6000_ckpt_path: Option, } /// Linear warmup then cosine decay to `lr_min`. `step_idx` is the @@ -310,6 +315,7 @@ fn main() -> Result<()> { let mut best_auc_h6000 = f32::NEG_INFINITY; let mut best_auc_h6000_epoch = 0usize; let mut best_auc_h6000_per_horizon = [0.5_f32; N_HORIZONS]; + let mut best_h6000_ckpt_path: Option = None; let mut auc_h6000_no_improvement = 0usize; let lr_min_cfc = cli.lr_cfc * cli.lr_min_factor; @@ -606,7 +612,16 @@ fn main() -> Result<()> { best_auc_h6000_epoch = epoch; best_auc_h6000_per_horizon = per_horizon_auc; auc_h6000_no_improvement = 0; - tracing::info!(epoch, auc_h6000, "new best auc_h6000"); + // X14: emit a CheckpointV2 file alongside summary.json so + // fxt-backtest --checkpoint can load the trained trunk. + let ckpt_path = cli.out.join("trunk_best_h6000.bin"); + trainer.save_checkpoint(&ckpt_path) + .context("save_checkpoint(trunk_best_h6000.bin)")?; + best_h6000_ckpt_path = Some("trunk_best_h6000.bin".to_string()); + tracing::info!( + epoch, auc_h6000, path = %ckpt_path.display(), + "new best auc_h6000 (saved checkpoint)" + ); } else { auc_h6000_no_improvement += 1; } @@ -650,6 +665,7 @@ fn main() -> Result<()> { best_auc_h6000, best_auc_h6000_per_horizon, early_stopped, + best_h6000_ckpt_path, }; let summary_path = cli.out.join("alpha_train_summary.json"); std::fs::write(&summary_path, serde_json::to_vec_pretty(&summary).context("serialize summary")?) diff --git a/crates/ml-alpha/src/trainer/perception.rs b/crates/ml-alpha/src/trainer/perception.rs index 69c6ae2ac..5e338b612 100644 --- a/crates/ml-alpha/src/trainer/perception.rs +++ b/crates/ml-alpha/src/trainer/perception.rs @@ -2306,6 +2306,14 @@ impl PerceptionTrainer { /// Single-sequence forward-only eval โ€” thin wrapper around /// [`evaluate_batched`] for cfg.n_batch == 1. + /// X13: Persist the trunk's v2 weights to a CheckpointV2 bincode file. + /// Delegates to `self.trunk.save_checkpoint` โ€” gradient buffers and + /// AdamW state are NOT serialized (training-only, not needed for + /// inference). + pub fn save_checkpoint(&self, path: &std::path::Path) -> Result<()> { + self.trunk.save_checkpoint(path).context("trunk save_checkpoint") + } + pub fn evaluate( &mut self, snapshots: &[Mbp10RawInput],