feat(ml-alpha): PerceptionTrainer.save_checkpoint + alpha_train wiring (X13+X14)

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<String>) 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).
This commit is contained in:
jgrusewski
2026-05-19 08:41:12 +02:00
parent 47a605e4c5
commit 7545651bce
2 changed files with 26 additions and 2 deletions

View File

@@ -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<String>,
}
/// 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<String> = 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")?)

View File

@@ -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],