feat(ml-alpha): CfcTrunk save/load checkpoint + --checkpoint CLI (C14)

CfcTrunk::save_checkpoint(path) reads each device weight tensor back
via memcpy_dtoh and bincode-serialises into a CheckpointV1 envelope:
  { version, n_in, n_hid,
    w_in, w_rec, b, tau,
    heads_w, heads_b,
    proj_w, proj_b, proj_g, proj_n }
Total ~22k-25k f32 = ~90 KB per trunk. Tiny.

CfcTrunk::load_checkpoint(dev, cfg, path) deserialises + validates
(version == 1, n_in/n_hid match the supplied CfcConfig — a model
trained for one arch can't silently load against another). Constructs
a fresh trunk via new_random (for kernel bindings + scratch buffers)
then overwrites every weight tensor via memcpy_htod. The random init
values are thrown away — marginally wasteful, but keeps the
construction code paths unified.

Roundtrip test (--ignored, CUDA-required): save trunk_A → load → read
back every device tensor and assert bit-equality between trunk_A and
the loaded trunk_B. Passed locally. Dim-mismatch rejection test runs
without CUDA (verifies bincode envelope serialise/deserialise).

bin/fxt-backtest --checkpoint <path>: when set, overrides --seed and
loads from disk. When absent, warns loudly that the trunk is
random-initialised and backtest results are noise. This makes the
binary genuinely useful as a deployment tool — point it at a trained
checkpoint and run real backtests.

Adds bincode workspace dep to ml-alpha (was already in workspace
dependencies, just not in ml-alpha's [dependencies] block). serde
features bumped to ["derive"] (was using workspace default which
omits derive macros).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-18 09:31:50 +02:00
parent ed19985c95
commit 3fa215ad2e
3 changed files with 232 additions and 5 deletions

View File

@@ -75,10 +75,15 @@ struct RunArgs {
/// Cap on events processed; 0 = exhaust the input stream.
#[arg(long, default_value_t = 0)]
max_events: u64,
/// Random seed for v1 trunk initialisation (a checkpoint loader is
/// follow-up work — see harness.rs::new doc).
/// Random seed for trunk initialisation when --checkpoint is not given.
#[arg(long, default_value_t = 0xC0FFEE)]
seed: u64,
/// Path to a CfcTrunk bincode checkpoint (see ml-alpha
/// CfcTrunk::save_checkpoint). When set, overrides --seed and the
/// trunk runs with trained weights. Without it the trunk is
/// random-initialised — useful for plumbing tests, not real backtests.
#[arg(long)]
checkpoint: Option<PathBuf>,
/// Output directory; per-cell artifacts written to <out>/cell_NNNN/.
#[arg(long)]
out: PathBuf,
@@ -140,8 +145,18 @@ fn run(args: RunArgs) -> Result<()> {
};
let dev = MlDevice::cuda(0).map_err(|e| anyhow::anyhow!("MlDevice::cuda(0): {e}"))?;
let trunk = CfcTrunk::new_random(&dev, &CfcConfig::default(), args.seed)
.context("CfcTrunk::new_random")?;
let trunk = if let Some(ckpt_path) = &args.checkpoint {
tracing::info!(checkpoint = %ckpt_path.display(), "loading CfcTrunk checkpoint");
CfcTrunk::load_checkpoint(&dev, &CfcConfig::default(), ckpt_path)
.with_context(|| format!("load checkpoint {}", ckpt_path.display()))?
} else {
tracing::warn!(
seed = args.seed,
"no --checkpoint provided; using new_random trunk (random init — backtest results are NOISE)"
);
CfcTrunk::new_random(&dev, &CfcConfig::default(), args.seed)
.context("CfcTrunk::new_random")?
};
let cfg = BacktestHarnessConfig {
data_root: args.data.clone(),