From bf4d0c899f3da92f15a45d52128836a56da614b9 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Thu, 21 May 2026 19:16:27 +0200 Subject: [PATCH] fix(per-horizon-cfc): sync trunk bucket metadata from transition output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit URGENT correctness fix surfaced by Task 13 implementer. Bug: training-time Phase 2 dispatch sites (cfc_step_per_branch_fwd at ~3139, h_mag_per_bucket for Controller D at ~3211-3212) read self.trunk.bucket_*_d which are zero-initialized at trunk construction and only populated via load_checkpoint. The Phase 1→2 transition populates a SEPARATE BucketRoutingMetadata owned by the trainer (self.bucket_routing_metadata) but never syncs it into the trunk fields. With zero-init bucket_dim_k, the per-branch kernel's uniform predicate `tid >= bucket_dim_k[branch]` early-returns ALL threads -> Phase 2 silently no-ops during training, Smoke 1 fails before producing signal. Fix: at transition, DtoD-copy metadata buffers into trunk fields BEFORE the `Some(metadata)` move (so `metadata.*` borrows remain valid): - metadata.bucket_channel_offset_d -> trunk.bucket_channel_offset_d - metadata.bucket_dim_k_d -> trunk.bucket_dim_k_d - metadata.bucket_id_per_channel_d -> trunk.bucket_id_per_channel_d - metadata.heads_w_skip_offset_d -> trunk.heads_w_skip_offset_d Trunk fields remain the single source of truth for both training-time dispatch AND checkpoint serialization. Total sync cost: 4 DtoD memcpys, ~256 bytes total at the one-shot transition (off the hot path). Verification: - `cargo check -p ml-alpha --all-targets`: clean - `cargo test -p ml-alpha --lib`: 33 passed, 0 failed - 3 read sites (perception.rs:3139, 3211-3212, 4725-4726) verified unchanged; all 4 new memcpy_dtod_async calls bracketed in the transition block (perception.rs:2224, 2234, 2244, 2254). - Block-diagonal heads grad-mask init (step 7) continues to read `metadata.bucket_id_per_channel_d` via `as_ref()` after the move; ordering preserved per pearl_canary_input_freshness_launch_order. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/ml-alpha/src/trainer/perception.rs | 70 ++++++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/crates/ml-alpha/src/trainer/perception.rs b/crates/ml-alpha/src/trainer/perception.rs index b0f9bd2a4..b83a4aeb6 100644 --- a/crates/ml-alpha/src/trainer/perception.rs +++ b/crates/ml-alpha/src/trainer/perception.rs @@ -2196,12 +2196,76 @@ impl PerceptionTrainer { } } - // 5. Persist routing metadata in trainer state. Tasks + // 5. Sync trunk's bucket metadata fields from the trainer-owned + // BucketRoutingMetadata. Without this, training-time Phase 2 + // dispatch sites that read `self.trunk.bucket_*_d` + // (cfc_step_per_branch_fwd at the per-branch dispatch and + // h_mag_per_bucket_kernel for Controller D) would see the + // zero-initialized trunk buffers — the per-branch kernel's + // uniform predicate `tid >= bucket_dim_k[branch]` would + // early-return ALL threads → silent Phase 2 no-op. + // + // Trunk fields are the single source of truth for BOTH + // training-time dispatch AND checkpoint serialization. Cost: + // 4 DtoD memcpys, ~256 bytes total at the one-shot + // transition (off the hot path). Sync MUST happen BEFORE + // the `Some(metadata)` move below (borrow of `metadata.*_d` + // requires `metadata` to still be live), and BEFORE the + // heads_w_skip_mask_init kernel at step 5.5 (which still + // reads `metadata.bucket_id_per_channel_d` directly through + // `self.bucket_routing_metadata.as_ref()` after the move). + // Per `pearl_canary_input_freshness_launch_order`: producer + // (sync) precedes every consumer. + unsafe { + let s = self.stream.cu_stream(); + // bucket_channel_offset_d: [N_HORIZONS + 1] × u32 + let (src, _gs) = metadata.bucket_channel_offset_d.device_ptr(&self.stream); + let (dst, _gd) = self.trunk.bucket_channel_offset_d.device_ptr_mut(&self.stream); + cudarc::driver::result::memcpy_dtod_async( + dst, + src, + (N_HORIZONS + 1) * std::mem::size_of::(), + s, + ) + .context("sync bucket_channel_offset to trunk")?; + // bucket_dim_k_d: [N_HORIZONS] × u32 + let (src, _gs) = metadata.bucket_dim_k_d.device_ptr(&self.stream); + let (dst, _gd) = self.trunk.bucket_dim_k_d.device_ptr_mut(&self.stream); + cudarc::driver::result::memcpy_dtod_async( + dst, + src, + N_HORIZONS * std::mem::size_of::(), + s, + ) + .context("sync bucket_dim_k to trunk")?; + // bucket_id_per_channel_d: [HIDDEN_DIM] × u8 + let (src, _gs) = metadata.bucket_id_per_channel_d.device_ptr(&self.stream); + let (dst, _gd) = self.trunk.bucket_id_per_channel_d.device_ptr_mut(&self.stream); + cudarc::driver::result::memcpy_dtod_async( + dst, + src, + HIDDEN_DIM * std::mem::size_of::(), + s, + ) + .context("sync bucket_id_per_channel to trunk")?; + // heads_w_skip_offset_d: [N_HORIZONS + 1] × u32 + let (src, _gs) = metadata.heads_w_skip_offset_d.device_ptr(&self.stream); + let (dst, _gd) = self.trunk.heads_w_skip_offset_d.device_ptr_mut(&self.stream); + cudarc::driver::result::memcpy_dtod_async( + dst, + src, + (N_HORIZONS + 1) * std::mem::size_of::(), + s, + ) + .context("sync heads_w_skip_offset to trunk")?; + } + + // 6. Persist routing metadata in trainer state. Tasks // 10–12 read its slices to drive Phase 2 dispatch + // Controllers B / C / D. self.bucket_routing_metadata = Some(metadata); - // 5.5. Block-diagonal heads grad-mask init (follow-up to + // 7. Block-diagonal heads grad-mask init (follow-up to // Task 10 Option B). One-shot: build the routing // mask AND zero-multiply off-bucket entries of // `heads_w_skip_d` in place. From here on, the per- @@ -2236,7 +2300,7 @@ impl PerceptionTrainer { } } - // 6. Latch trainer into Phase 2. + // 8. Latch trainer into Phase 2. // // NOTE: Task 9 leaves the per-step dispatch path // unchanged (Phase 1 single-CfC, 640-float heads_w_skip).