refactor(ml-alpha): move Mamba2 stack 1 from PerceptionTrainer to CfcTrunk (X3)

PerceptionTrainer no longer owns the Mamba2 stack-1 block; the trunk's
mamba2_stack_1 Option is filled in after Mamba2Block::new (which still
runs at the same point in PerceptionTrainer::new so the Mamba2 weight
init order is unchanged). Mamba2AdamW was already constructed against
&mamba2 before the move, so optimizer state is preserved.

CfcTrunk gains mamba2_l1_mut() / mamba2_l1() / mamba2_l2_mut() / mamba2_l2()
accessors that unwrap the Options. Forward, backward, and AdamW step
sites in evaluate_batched / step_batched / evaluate redirect through the
new accessors.

Gradient buffers (mamba2_grads_buffers) and AdamW state (mamba2_adamw)
remain at trainer level — training-only state stays with the trainer.

Verification:
- perception_forward_golden: PASS (max_diff = 0.000000)
- ml-alpha lib tests: 34 pass

Per spec 2026-05-19-ml-alpha-v2-trunk-grows-and-deployability-design.md §1.1, §2.2 (X3).
EOF
)
This commit is contained in:
jgrusewski
2026-05-19 01:39:47 +02:00
parent fdef6efe98
commit 2d849dd5e3
2 changed files with 34 additions and 7 deletions

View File

@@ -526,6 +526,25 @@ impl CfcTrunk {
/// Reads each device tensor back via memcpy_dtoh and packs into a
/// CheckpointV1 envelope. Currently writes ~22-25k f32s = ~90KB
/// per CfcTrunk.
/// Accessor for Mamba2 stack 1. Panics if X3 migration hasn't run.
pub fn mamba2_l1_mut(&mut self) -> &mut crate::mamba2_block::Mamba2Block {
self.mamba2_stack_1.as_mut()
.expect("Mamba2 stack 1 not populated (X3 migration must have run)")
}
pub fn mamba2_l1(&self) -> &crate::mamba2_block::Mamba2Block {
self.mamba2_stack_1.as_ref()
.expect("Mamba2 stack 1 not populated (X3 migration must have run)")
}
/// Accessor for Mamba2 stack 2. Panics if X5 migration hasn't run.
pub fn mamba2_l2_mut(&mut self) -> &mut crate::mamba2_block::Mamba2Block {
self.mamba2_stack_2.as_mut()
.expect("Mamba2 stack 2 not populated (X5 migration must have run)")
}
pub fn mamba2_l2(&self) -> &crate::mamba2_block::Mamba2Block {
self.mamba2_stack_2.as_ref()
.expect("Mamba2 stack 2 not populated (X5 migration must have run)")
}
pub fn save_checkpoint(&self, path: &std::path::Path) -> Result<()> {
use std::io::Write;
let w_in = {

View File

@@ -153,8 +153,9 @@ pub struct PerceptionTrainer {
heads_grn_bwd_fn: CudaFunction,
transpose_3d_fn: CudaFunction,
// Mamba2 encoder block + its optimizer
pub mamba2: Mamba2Block,
// X3: Mamba2 stack 1 weights moved to `self.trunk.mamba2_stack_1`.
// Access via `self.trunk.mamba2_l1()` / `mamba2_l1_mut()`. Gradient
// buffers + AdamW state remain at trainer level.
pub mamba2_adamw: Mamba2AdamW,
/// Phase 2B: SECOND Mamba2 stack. Sits between the (first-stack-output
/// + LN_a) and the existing LN_b → CfC. Same hidden_dim as stack 1
@@ -635,6 +636,11 @@ impl PerceptionTrainer {
let mamba2_grads_buffers = Mamba2BackwardGradsBuffers::new(
&stream, cfg.n_batch, cfg.seq_len, FEATURE_DIM, HIDDEN_DIM, cfg.mamba2_state_dim,
).context("Mamba2BackwardGradsBuffers::new")?;
// X3: move Mamba2 stack 1 into the trunk; subsequent access via
// self.trunk.mamba2_l1() / mamba2_l1_mut(). Mamba2AdamW was built
// above against &mamba2 (read-only borrow that ended before this
// move), so the optimizer state is fully constructed.
trunk.mamba2_stack_1 = Some(mamba2);
// ── Phase 2B: SECOND Mamba2 stack ──
// Same hidden_dim + state_dim as stack 1. in_dim = HIDDEN_DIM
@@ -1057,7 +1063,6 @@ impl PerceptionTrainer {
heads_grn_fwd_fn,
heads_grn_bwd_fn,
transpose_3d_fn,
mamba2,
mamba2_adamw,
mamba2_fwd_scratch,
mamba2_bwd_scratch,
@@ -1432,7 +1437,8 @@ impl PerceptionTrainer {
// ── 2. Mamba2 stack-1 forward — writes into mamba2_fwd_scratch.
// Consumes vsn_out_d (gated features). in_dim = FEATURE_DIM (40).
self.mamba2
self.trunk
.mamba2_l1_mut()
.forward_train_seq_into(&self.vsn_out_d, &mut self.mamba2_fwd_scratch)
.context("mamba2 (l1) forward_train_seq_into")?;
@@ -2115,7 +2121,8 @@ impl PerceptionTrainer {
// Writes:
// mamba2_grads_buffers.d_x_from_in = grad on VSN output
// (consumed by VSN bwd below).
self.mamba2
self.trunk
.mamba2_l1_mut()
.backward_from_h_enriched_seq_full_into(
&self.vsn_out_d,
&self.mamba2_fwd_scratch,
@@ -2293,7 +2300,7 @@ impl PerceptionTrainer {
// GPU-resident grad-clip + AdamW (zero host roundtrips).
// Replaces step_from_buffers which did 9× memcpy_dtoh per step.
self.mamba2_adamw
.step_from_buffers_gpu_clip(&mut self.mamba2, &self.mamba2_grads_buffers)
.step_from_buffers_gpu_clip(self.trunk.mamba2_l1_mut(), &self.mamba2_grads_buffers)
.context("mamba2 (l1) AdamW step_from_buffers_gpu_clip")?;
self.mamba2_l2_adamw
.step_from_buffers_gpu_clip(&mut self.mamba2_l2, &self.mamba2_l2_grads_buffers)
@@ -2470,7 +2477,8 @@ impl PerceptionTrainer {
}
// Mamba2 stack-1 fwd → m1.h_enriched_seq.
self.mamba2
self.trunk
.mamba2_l1_mut()
.forward_train_seq_into(&self.vsn_out_d, &mut self.mamba2_fwd_scratch)
.context("eval mamba2 (l1) fwd_into")?;