plan: alpha-rl perf + checkpoint + walk-forward implementation

7 tasks: TF32 on DQN + Mamba2 handles, AdamW save/load, trainer
checkpoint/resume, training loop wiring, local smoke, H100 walk-forward.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-27 20:16:48 +02:00
parent 6e641b934c
commit e730b5cbd1

View File

@@ -0,0 +1,333 @@
# Alpha-RL Performance + Checkpoint + Walk-Forward Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 2-3× training throughput via TF32, crash recovery via checkpointing, OOS validation via 3-fold walk-forward on H100.
**Architecture:** Enable TF32 Tensor Core math on all cuBLAS handles (2 sites). Add checkpoint/resume serialization for model weights + Adam state + ISV to the training binary. Validate with 3-fold walk-forward at 25k steps/fold on H100.
**Tech Stack:** Rust, CUDA cuBLAS, cudarc, mapped-pinned memory, Argo Workflows
---
### Task 1: Enable TF32 on DQN cuBLAS handle
**Files:**
- Modify: `crates/ml-alpha/src/rl/dqn.rs:314` (after `cublasSetWorkspace_v2`)
- [ ] **Step 1: Add TF32 math mode after workspace setup**
In `crates/ml-alpha/src/rl/dqn.rs`, after line 313 (the closing of the `cublasSetWorkspace_v2` block), add inside the same `unsafe` block:
```rust
cudarc::cublas::sys::cublasSetMathMode(
*cublas.handle(),
cudarc::cublas::sys::cublasMath_t::CUBLAS_TF32_TENSOR_OP_MATH,
)
.result()
.map_err(|e| anyhow::anyhow!("DqnHead: cublasSetMathMode TF32: {e:?}"))?;
```
- [ ] **Step 2: Build check**
Run: `SQLX_OFFLINE=true cargo check -p ml-alpha`
Expected: compiles cleanly
- [ ] **Step 3: Commit**
```bash
git add crates/ml-alpha/src/rl/dqn.rs
git commit -m "perf(cuda): enable TF32 Tensor Core math on DQN cuBLAS handle"
```
### Task 2: Enable TF32 on Mamba2 cuBLAS handle
**Files:**
- Modify: `crates/ml-alpha/src/mamba2_block.rs:549` (after `cublasSetWorkspace_v2`)
- [ ] **Step 1: Add TF32 math mode after workspace setup**
In `crates/ml-alpha/src/mamba2_block.rs`, after line 549 (the closing of the `cublasSetWorkspace_v2` block), add inside the same `unsafe` block:
```rust
cudarc::cublas::sys::cublasSetMathMode(
*cublas.handle(),
cudarc::cublas::sys::cublasMath_t::CUBLAS_TF32_TENSOR_OP_MATH,
)
.result()
.map_err(|e| anyhow!("Mamba2Block: cublasSetMathMode TF32: {e:?}"))?;
```
- [ ] **Step 2: Build check**
Run: `SQLX_OFFLINE=true cargo check -p ml-alpha`
Expected: compiles cleanly
- [ ] **Step 3: Local smoke test (RTX 3050 Ti, sm_86 supports TF32)**
Run a 100-step local smoke and verify l_q is within 1% of baseline:
```bash
FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --lib -- smoke_tests --ignored --nocapture 2>&1 | tail -5
```
Expected: smoke passes, no NaN
- [ ] **Step 4: Commit**
```bash
git add crates/ml-alpha/src/mamba2_block.rs
git commit -m "perf(cuda): enable TF32 Tensor Core math on Mamba2 cuBLAS handle"
```
### Task 3: Add AdamW save/load methods
**Files:**
- Modify: `crates/ml-alpha/src/trainer/optim.rs`
- [ ] **Step 1: Add save method to AdamW**
Add after the existing `impl AdamW` block (or at the end of the impl):
```rust
/// Serialize Adam state (m, v, step_count, hyperparams) to writer.
/// Reads device buffers via mapped-pinned DtoH (one-shot, not hot path).
pub fn save(&self, w: &mut impl std::io::Write) -> Result<()> {
let m_host = self._stream.clone().read_sync(&self.m)
.map_err(|e| anyhow::anyhow!("AdamW save m: {e}"))?;
let v_host = self._stream.clone().read_sync(&self.v)
.map_err(|e| anyhow::anyhow!("AdamW save v: {e}"))?;
let n = m_host.len() as u64;
w.write_all(&n.to_le_bytes())?;
w.write_all(&self.step_count_host.to_le_bytes())?;
w.write_all(&self.lr.to_le_bytes())?;
w.write_all(&self.beta1.to_le_bytes())?;
w.write_all(&self.beta2.to_le_bytes())?;
w.write_all(&self.eps.to_le_bytes())?;
w.write_all(&self.wd.to_le_bytes())?;
let m_bytes: &[u8] = bytemuck::cast_slice(&m_host);
w.write_all(m_bytes)?;
let v_bytes: &[u8] = bytemuck::cast_slice(&v_host);
w.write_all(v_bytes)?;
Ok(())
}
/// Restore Adam state from reader. Sizes must match.
pub fn load(&mut self, r: &mut impl std::io::Read) -> Result<()> {
let mut buf8 = [0u8; 8];
let mut buf4 = [0u8; 4];
r.read_exact(&mut buf8)?;
let n = u64::from_le_bytes(buf8) as usize;
anyhow::ensure!(n == self.m.len(), "AdamW load: m size mismatch {n} vs {}", self.m.len());
r.read_exact(&mut buf4)?;
self.step_count_host = i32::from_le_bytes(buf4);
r.read_exact(&mut buf4)?; self.lr = f32::from_le_bytes(buf4);
r.read_exact(&mut buf4)?; self.beta1 = f32::from_le_bytes(buf4);
r.read_exact(&mut buf4)?; self.beta2 = f32::from_le_bytes(buf4);
r.read_exact(&mut buf4)?; self.eps = f32::from_le_bytes(buf4);
r.read_exact(&mut buf4)?; self.wd = f32::from_le_bytes(buf4);
let mut m_host = vec![0f32; n];
r.read_exact(bytemuck::cast_slice_mut(&mut m_host))?;
self._stream.clone().write_sync(&m_host, &mut self.m)
.map_err(|e| anyhow::anyhow!("AdamW load m: {e}"))?;
let mut v_host = vec![0f32; n];
r.read_exact(bytemuck::cast_slice_mut(&mut v_host))?;
self._stream.clone().write_sync(&v_host, &mut self.v)
.map_err(|e| anyhow::anyhow!("AdamW load v: {e}"))?;
Ok(())
}
```
- [ ] **Step 2: Verify bytemuck is in dependencies**
Run: `grep bytemuck crates/ml-alpha/Cargo.toml`
If missing, add `bytemuck = { version = "1", features = ["derive"] }`.
- [ ] **Step 3: Build check**
Run: `SQLX_OFFLINE=true cargo check -p ml-alpha`
- [ ] **Step 4: Commit**
```bash
git add crates/ml-alpha/src/trainer/optim.rs crates/ml-alpha/Cargo.toml
git commit -m "feat(rl): AdamW save/load for checkpoint persistence"
```
### Task 4: Add IntegratedTrainer checkpoint save/load
**Files:**
- Modify: `crates/ml-alpha/src/trainer/integrated.rs`
- [ ] **Step 1: Add save_checkpoint method**
Add a `save_checkpoint` method to `IntegratedTrainer` that serializes:
- DQN weights (online + target): `dqn_head.w_d`, `dqn_head.b_d`, `dqn_head.w_target_d`, `dqn_head.b_target_d`
- Policy/V head weights
- ISV bus (585 f32 from mapped-pinned `isv_mapped.host_ptr`)
- All 20 AdamW states via `adam.save()`
- Step counter
- Encoder via `perception.save_checkpoint()`
Format: sequential sections with `[name_len: u16][name: bytes][data_len: u64][data: bytes]`.
File header: `[magic: u32 = 0x464F5843][version: u32 = 1][step: u64]`.
Write to `<out_dir>/checkpoint-<step>.bin`. Keep last 2, delete older.
- [ ] **Step 2: Add load_checkpoint method**
Inverse of save: read header, validate magic/version, restore each section by name lookup. Skip unknown sections for forward compatibility.
- [ ] **Step 3: Build check**
Run: `SQLX_OFFLINE=true cargo check -p ml-alpha`
- [ ] **Step 4: Commit**
```bash
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): IntegratedTrainer checkpoint save/load"
```
### Task 5: Wire checkpoint into training loop
**Files:**
- Modify: `crates/ml-alpha/examples/alpha_rl_train.rs`
- [ ] **Step 1: Add CLI flags**
Add to the CLI struct (clap):
```rust
/// Resume from checkpoint file path. Restores model weights, Adam
/// state, ISV, and step counter. Training continues from saved step.
#[arg(long)]
resume_from: Option<PathBuf>,
/// Checkpoint interval in steps (default: 5000). Set to 0 to disable.
#[arg(long, default_value = "5000")]
checkpoint_every: usize,
```
- [ ] **Step 2: Add resume logic before training loop**
After trainer init, before `for step in 0..cli.n_steps`:
```rust
let start_step = if let Some(ref ckpt_path) = cli.resume_from {
trainer.load_checkpoint(ckpt_path)
.with_context(|| format!("resume from {}", ckpt_path.display()))?
} else {
0
};
```
Change loop to `for step in start_step..cli.n_steps`.
- [ ] **Step 3: Add checkpoint save inside training loop**
After the per-step JSONL write, before the next iteration:
```rust
if cli.checkpoint_every > 0 && step > 0 && step % cli.checkpoint_every == 0 {
let ckpt_path = cli.out.join(format!("checkpoint-{step}.bin"));
trainer.save_checkpoint(&ckpt_path, step as u64)
.with_context(|| format!("checkpoint at step {step}"))?;
eprintln!("checkpoint saved: {}", ckpt_path.display());
// Rolling: keep last 2, delete older
let old_step = step.saturating_sub(cli.checkpoint_every * 2);
if old_step > 0 {
let old_path = cli.out.join(format!("checkpoint-{old_step}.bin"));
let _ = std::fs::remove_file(&old_path);
}
}
```
- [ ] **Step 4: Build check**
Run: `SQLX_OFFLINE=true cargo check -p ml-alpha`
- [ ] **Step 5: Commit**
```bash
git add crates/ml-alpha/examples/alpha_rl_train.rs
git commit -m "feat(rl): wire checkpoint save/resume into training loop"
```
### Task 6: Local validation smoke
**Files:** None (test only)
- [ ] **Step 1: Run 200-step local smoke with TF32**
```bash
SQLX_OFFLINE=true cargo build --release -p ml-alpha --example alpha_rl_train
# Run 200 steps with checkpoint at 100
./target/release/examples/alpha_rl_train \
--n-backtests 16 --n-steps 200 --checkpoint-every 100 \
--out /tmp/smoke-tf32 \
--mbp10-data-dir test_data/futures-baseline \
--trades-data-dir test_data/futures-baseline 2>&1 | tail -5
```
Expected: completes without NaN, checkpoint files at `/tmp/smoke-tf32/checkpoint-100.bin`
- [ ] **Step 2: Test resume**
```bash
./target/release/examples/alpha_rl_train \
--n-backtests 16 --n-steps 200 --checkpoint-every 100 \
--resume-from /tmp/smoke-tf32/checkpoint-100.bin \
--out /tmp/smoke-tf32-resume \
--mbp10-data-dir test_data/futures-baseline \
--trades-data-dir test_data/futures-baseline 2>&1 | tail -5
```
Expected: starts from step 100, runs to 200, no crash
- [ ] **Step 3: Commit all together**
```bash
git add -A
git commit -m "test: validate TF32 + checkpoint save/resume local smoke"
```
### Task 7: Deploy walk-forward on H100
**Files:** None (deployment only)
- [ ] **Step 1: Push branch**
```bash
git push origin ml-alpha-phase-a
```
- [ ] **Step 2: Submit 3-fold walk-forward on H100**
```bash
./scripts/argo-train.sh \
--model alpha-rl \
--branch ml-alpha-phase-a \
--gpu-pool ci-training-h100 \
--folds 3
```
If `argo-train.sh` doesn't support `--folds` with step override, submit directly:
```bash
argo submit --from=wftmpl/alpha-rl -n foxhunt \
-p git-branch=ml-alpha-phase-a \
-p n-steps=25000 \
-p n-backtests=1024 \
-p per-capacity=65536 \
-p gpu-pool=ci-training-h100
```
Run 3 times with fold-idx 0, 1, 2.
- [ ] **Step 3: Monitor with log-only (no extra pods on GPU node)**
```bash
kubectl logs <pod-name> -n foxhunt --tail=1 -f | grep "step.*wr="
```
- [ ] **Step 4: Validate success criteria**
All 3 folds must show:
- wr > 0.55
- No fold with wr < 0.50
- Entropy stable (no collapse)
- hold% between 30-70%