fix(per-horizon-cfc): sync trunk bucket metadata from transition output
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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::<u32>(),
|
||||
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::<u32>(),
|
||||
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::<u8>(),
|
||||
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::<u32>(),
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user