feat(diag): Phase 1 — HealthDiagSnapshot struct + mapped-pinned wrapper
Phase 1 of the HEALTH_DIAG GPU port (Phase 0 inventory landed in
333ea7184). Lands the typed snapshot struct and the mapped-pinned
allocation that subsequent phases write into and read from — no kernel
and no caller wiring yet, deliberately so the type can stabilise before
Phase 2 invests in CUDA shmem reductions against its byte layout.
`HealthDiagSnapshot` (#[repr(C)] POD, 147×4 = 588 bytes) holds every
numeric field emitted across the 7 HEALTH_DIAG log sites. All fields
are `f32` or `u32` so CPU and GPU agree byte-for-byte with no padding
to reason about. Controller fire booleans (`fire_lr`/`fire_tau`/etc.)
are promoted from u8 → u32 per the Phase 0 design review's open-
question #3 — a `[u8; 6]` followed by `f32` would risk implementation-
defined padding mismatch between Rust's #[repr(C)] and CUDA's struct
layout rules; the +24 bytes of cost is acceptable.
`MappedHealthDiagSnapshot` wraps `cuMemHostAlloc(DEVICEMAP|PORTABLE)`
of `sizeof(HealthDiagSnapshot)` plus `cuMemHostGetDevicePointer_v2` —
mirrors the existing `MappedF32Buffer` allocator pattern but typed.
The kernel chain in Phase 2 will write through `dev_ptr()`; the host
emit code in Phase 4 reads through `host_ref()`. No `memcpy_dtoh`,
no `Vec` allocator on the path — consistent with
`feedback_no_htod_htoh_only_mapped_pinned.md`.
Three unit tests pin the layout (`snapshot_size_is_stable`,
`alignment_is_4_bytes`, `default_is_zeroed`) so any future field
add/reorder requires an explicit test update — guards the kernel-side
offset table from silent drift between commits. The size assertion
records the field count breakdown line by line in a comment so the
next maintainer can audit the math without re-deriving it.
Touched: cuda_pipeline/health_diag.rs (+339 LOC new),
cuda_pipeline/mod.rs (+8 re-exports). cargo check clean at 12 warnings
(workspace baseline). Three new unit tests pass on local CPU host (no
GPU required). Audit doc note added per Invariant 7 — explicitly notes
producer kernel + caller migrations land in Phase 2 / 4 commits.
No fingerprint change — separate mapped-pinned alloc, not part of the
param-tensor layout the fingerprint guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
411
crates/ml/src/cuda_pipeline/health_diag.rs
Normal file
411
crates/ml/src/cuda_pipeline/health_diag.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
//! `HealthDiagSnapshot` — single mapped-pinned struct holding every numeric field
|
||||
//! emitted by the per-epoch HEALTH_DIAG family of log lines.
|
||||
//!
|
||||
//! Phase 1 of the HEALTH_DIAG GPU port (see `docs/health_diag_inventory.md`).
|
||||
//! The struct is laid out `#[repr(C)]` so the producer kernel family
|
||||
//! (`health_diag_kernel.cu`, landed in Phase 2) writes directly into the
|
||||
//! mapped-pinned host pages via `dev_ptr` and the host emit code reads via
|
||||
//! `host_ptr` — no `memcpy_dtoh`, no `Vec` allocation on the hot path.
|
||||
//!
|
||||
//! Field order matches the existing log format strings (see `training_loop.rs`
|
||||
//! and `metrics.rs` HEALTH_DIAG sites). Booleans are promoted to `u32` to keep
|
||||
//! field alignment uniform across CPU and GPU per the Phase 0 design review:
|
||||
//! a `[u8; 6]` array followed by an `f32` would risk implementation-defined
|
||||
//! padding mismatch between Rust's `#[repr(C)]` and CUDA's struct rules.
|
||||
//! The +24-byte cost (6×4 instead of 6×1 for the controller fire bools) is
|
||||
//! acceptable.
|
||||
//!
|
||||
//! No `Vec`, no heap, no per-emit allocation — `Default::default()` produces a
|
||||
//! zeroed POD via `MaybeUninit::zeroed()`.
|
||||
//!
|
||||
//! Wire-up: a single `MappedHealthDiagSnapshot` (allocated via `cuMemHostAlloc(
|
||||
//! DEVICEMAP|PORTABLE)` of `sizeof(HealthDiagSnapshot)`) lives on the trainer.
|
||||
//! The kernel chain in `gpu_health_diag.rs` writes through `dev_ptr` once per
|
||||
//! epoch alongside the other producer kernels (`launch_h_s2_rms_ema`, etc.).
|
||||
//! After a `__threadfence_system()` in the final kernel, the host emit code
|
||||
//! reads via `host_ref()` and `write!()`s into a stack-bounded `String`
|
||||
//! buffer — the only allocation on the path is the bounded
|
||||
//! `String::with_capacity(2048)` for the format line itself.
|
||||
|
||||
#![allow(unsafe_code)] // cuMemHostAlloc + cuMemHostGetDevicePointer_v2
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
/// Number of factored-action exposure bins in the monitoring layout.
|
||||
/// Matches the 4-direction × 3-magnitude layout enforced post `2fb30f098`
|
||||
/// (see MEMORY.md DQN Architecture entry).
|
||||
pub const HEALTH_DIAG_ACTION_BINS: usize = 12;
|
||||
|
||||
/// Number of MoE experts whose per-expert utilisation EMAs are mirrored.
|
||||
/// Matches `MOE_NUM_EXPERTS` in the kernel layer.
|
||||
pub const HEALTH_DIAG_MOE_EXPERTS: usize = 8;
|
||||
|
||||
/// Number of grad-trunk components whose absolute L2 norms are mirrored.
|
||||
/// Matches `grad_trunk_norms_by_component()`'s `[f32; 9]` layout.
|
||||
pub const HEALTH_DIAG_GRAD_TRUNK: usize = 9;
|
||||
|
||||
/// All numeric fields emitted across the HEALTH_DIAG family of log lines.
|
||||
///
|
||||
/// **Layout contract.** `#[repr(C)]` with all fields `f32` or `u32` so:
|
||||
/// 1. CPU and GPU agree byte-for-byte (no Rust-side padding the kernel
|
||||
/// has to reason about).
|
||||
/// 2. The struct is naturally aligned: every field is 4 bytes.
|
||||
/// 3. `MaybeUninit::zeroed()` produces a valid `Default` instance — every
|
||||
/// f32 zero is bit-zero and every u32 zero is the "off" boolean.
|
||||
///
|
||||
/// Field order is stable: re-ordering breaks the kernel's offset assumptions.
|
||||
/// Adding fields appends to the end; bumping the layout fingerprint is
|
||||
/// **not** required (this struct is a separate mapped-pinned alloc, not part
|
||||
/// of the param-tensor layout the fingerprint guards).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HealthDiagSnapshot {
|
||||
// ── Health aggregator (1 + 7 = 8 floats) ─────────────────────────────────
|
||||
/// `learning_health.value()` — aggregated health scalar in `[0, 1]`.
|
||||
pub health: f32,
|
||||
/// `components.q_gap_norm`.
|
||||
pub comp_q_gap: f32,
|
||||
/// `components.q_var_norm`.
|
||||
pub comp_q_var: f32,
|
||||
/// `components.atom_util_norm`.
|
||||
pub comp_atoms: f32,
|
||||
/// `components.grad_stable`.
|
||||
pub comp_grad_stable: f32,
|
||||
/// `components.ens_agree`.
|
||||
pub comp_ens_agree: f32,
|
||||
/// `components.grad_consistency_norm`.
|
||||
pub comp_grad_cos: f32,
|
||||
/// `components.spectral_gap_norm`.
|
||||
pub comp_spectral: f32,
|
||||
|
||||
// ── Effective hyperparams from adaptive controllers (8 floats) ───────────
|
||||
pub eff_cql_alpha: f32,
|
||||
pub eff_iqn_budget: f32,
|
||||
pub eff_cql_budget: f32,
|
||||
pub eff_c51_budget: f32,
|
||||
pub eff_tau: f32,
|
||||
pub eff_sarsa_tau: f32,
|
||||
pub eff_gamma: f32,
|
||||
pub eff_cf_ratio: f32,
|
||||
|
||||
// ── Mechanism state (5 floats + 2 u32 booleans + 1 u32 boolean = 8) ──────
|
||||
/// `last_distill_active` (0/1).
|
||||
pub novel_distill_on: u32,
|
||||
pub novel_barrier: f32,
|
||||
/// `last_plasticity_ready` (0/1 — 1 = ready, 0 = cooldown).
|
||||
pub novel_plasticity_ready: u32,
|
||||
pub novel_ib: f32,
|
||||
pub novel_ensemble_collapse: f32,
|
||||
/// `last_contrarian_active` (0/1).
|
||||
pub novel_contrarian_on: u32,
|
||||
pub novel_meta_q_pred: f32,
|
||||
|
||||
// ── Diagnostics (2 floats) ───────────────────────────────────────────────
|
||||
pub diag_sharpe_ema: f32,
|
||||
pub diag_action_entropy: f32,
|
||||
|
||||
// ── Gems (1 float) ───────────────────────────────────────────────────────
|
||||
pub gem_g12_predictive: f32,
|
||||
|
||||
// ── Magnitude block (10 floats) ──────────────────────────────────────────
|
||||
pub mag_q_full: f32,
|
||||
pub mag_q_half: f32,
|
||||
pub mag_q_quarter: f32,
|
||||
pub mag_var_scale: f32,
|
||||
pub mag_kelly_f: f32,
|
||||
pub mag_avg_win_ratio: f32,
|
||||
pub mag_grad_ratio_mag_dir: f32,
|
||||
pub mag_dist_q: f32,
|
||||
pub mag_dist_h: f32,
|
||||
pub mag_dist_f: f32,
|
||||
|
||||
// ── Grad split: backward (4 floats) ──────────────────────────────────────
|
||||
pub grad_split_bwd_iqn: f32,
|
||||
pub grad_split_bwd_cql: f32,
|
||||
pub grad_split_bwd_c51: f32,
|
||||
pub grad_split_bwd_ens: f32,
|
||||
|
||||
// ── Grad split: aux (5 floats) ───────────────────────────────────────────
|
||||
pub grad_split_aux_distill: f32,
|
||||
pub grad_split_aux_rec: f32,
|
||||
pub grad_split_aux_pred: f32,
|
||||
pub grad_split_aux_cql_sx: f32,
|
||||
pub grad_split_aux_c51_bs: f32,
|
||||
|
||||
// ── Grad trunk: 9-component absolute L2 norms ────────────────────────────
|
||||
/// Order matches `grad_trunk_norms_by_component()`'s `[f32; 9]`:
|
||||
/// `[iqn, cql, cql_sx, c51, c51_bs, distill, rec, pred, ens]`.
|
||||
pub grad_trunk: [f32; HEALTH_DIAG_GRAD_TRUNK],
|
||||
|
||||
// ── Grad abs (2 floats) ──────────────────────────────────────────────────
|
||||
pub grad_abs_dir: f32,
|
||||
pub grad_abs_mag: f32,
|
||||
|
||||
// ── Trail rates + holds (6 floats) ───────────────────────────────────────
|
||||
pub trail_fire_q: f32,
|
||||
pub trail_fire_h: f32,
|
||||
pub trail_fire_f: f32,
|
||||
pub trail_hold_q: f32,
|
||||
pub trail_hold_h: f32,
|
||||
pub trail_hold_f: f32,
|
||||
|
||||
// ── Per-magnitude winrate + variance (6 floats) ──────────────────────────
|
||||
pub magstat_wr_q: f32,
|
||||
pub magstat_wr_h: f32,
|
||||
pub magstat_wr_f: f32,
|
||||
pub magstat_var_q: f32,
|
||||
pub magstat_var_h: f32,
|
||||
pub magstat_var_f: f32,
|
||||
|
||||
// ── Noisy/VSN/drift (6 floats) ───────────────────────────────────────────
|
||||
pub noisy_vsn_mag: f32,
|
||||
pub noisy_vsn_dir: f32,
|
||||
pub noisy_sigma_mag: f32,
|
||||
pub noisy_sigma_dir: f32,
|
||||
pub noisy_drift_mag: f32,
|
||||
pub noisy_drift_dir: f32,
|
||||
|
||||
// ── Eval per-magnitude distribution (3 floats) ───────────────────────────
|
||||
pub eval_dist_q: f32,
|
||||
pub eval_dist_h: f32,
|
||||
pub eval_dist_f: f32,
|
||||
|
||||
// ── Reward contributions (4 floats) ──────────────────────────────────────
|
||||
pub reward_contrib_popart: f32,
|
||||
pub reward_contrib_cf: f32,
|
||||
pub reward_contrib_trail: f32,
|
||||
pub reward_contrib_micro: f32,
|
||||
|
||||
// ── Controller fire booleans (6 × u32) + fire_frac (1 float) ─────────────
|
||||
/// `fire_lr` / `fire_tau` / `fire_gamma` / `fire_clip` / `fire_cql` /
|
||||
/// `fire_cost`. Each 0/1; promoted to `u32` to avoid CPU/GPU
|
||||
/// `#[repr(C)]` padding mismatch when followed by an `f32` field.
|
||||
pub ctrl_fire_anti_lr: u32,
|
||||
pub ctrl_fire_tau: u32,
|
||||
pub ctrl_fire_gamma: u32,
|
||||
pub ctrl_fire_clip: u32,
|
||||
pub ctrl_fire_cql: u32,
|
||||
pub ctrl_fire_cost: u32,
|
||||
pub ctrl_fire_frac: f32,
|
||||
|
||||
// ── Explore (3 floats) ───────────────────────────────────────────────────
|
||||
pub explore_ent_mag: f32,
|
||||
pub explore_ent_dir: f32,
|
||||
pub explore_sigma_mean: f32,
|
||||
|
||||
// ── Validation block (16 floats) ─────────────────────────────────────────
|
||||
pub val_sharpe: f32,
|
||||
pub val_sortino: f32,
|
||||
pub val_win_rate: f32,
|
||||
pub val_max_drawdown: f32,
|
||||
pub val_trade_count: f32,
|
||||
pub val_calmar: f32,
|
||||
pub val_omega_ratio: f32,
|
||||
pub val_total_pnl: f32,
|
||||
pub val_var_95: f32,
|
||||
pub val_cvar_95: f32,
|
||||
pub val_trades_per_bar: f32,
|
||||
pub val_active_frac: f32,
|
||||
pub val_dir_entropy: f32,
|
||||
pub val_sharpe_annualised: f32,
|
||||
pub val_profit_factor: f32,
|
||||
pub val_window_bars: f32,
|
||||
|
||||
// ── Validation per-direction action distribution (4 floats) ──────────────
|
||||
pub val_dir_dist_short: f32,
|
||||
pub val_dir_dist_hold: f32,
|
||||
pub val_dir_dist_long: f32,
|
||||
pub val_dir_dist_flat: f32,
|
||||
|
||||
// ── Validation per-direction picked distribution (4 floats) ──────────────
|
||||
pub val_picked_dir_dist_short: f32,
|
||||
pub val_picked_dir_dist_hold: f32,
|
||||
pub val_picked_dir_dist_long: f32,
|
||||
pub val_picked_dir_dist_flat: f32,
|
||||
|
||||
// ── Reward split (6 ISV-mirror floats) ───────────────────────────────────
|
||||
pub reward_split_popart: f32,
|
||||
pub reward_split_cf: f32,
|
||||
pub reward_split_trail: f32,
|
||||
pub reward_split_micro: f32,
|
||||
pub reward_split_opp_cost: f32,
|
||||
pub reward_split_bonus: f32,
|
||||
|
||||
// ── Aux block (4 floats) ─────────────────────────────────────────────────
|
||||
pub aux_next_bar_mse: f32,
|
||||
pub aux_regime_ce: f32,
|
||||
pub aux_weight: f32,
|
||||
pub aux_label_scale: f32,
|
||||
|
||||
// ── Aux MoE (8 utils + 1 ent + 1 λ_eff = 10 floats) ──────────────────────
|
||||
pub aux_moe_util: [f32; HEALTH_DIAG_MOE_EXPERTS],
|
||||
pub aux_moe_ent: f32,
|
||||
pub aux_moe_lambda_eff: f32,
|
||||
|
||||
// ── Action-count histogram (12 × u32) ────────────────────────────────────
|
||||
/// Per-(dir,mag) factored-action histogram. Layout matches
|
||||
/// `MonitoringSummary.action_counts[12]`:
|
||||
/// `[S_Small, S_Half, S_Full, H_Small, H_Half, H_Full,
|
||||
/// L_Small, L_Half, L_Full, F_Small, F_Half, F_Full]`.
|
||||
/// The kernel reads the existing `monitoring_summary` mapped-pinned
|
||||
/// buffer and copies into this slot; downstream consumers (smoke tests,
|
||||
/// aggregator scripts) read the same 12-bin layout that already lives
|
||||
/// in `MonitoringSummary`.
|
||||
pub action_counts: [u32; HEALTH_DIAG_ACTION_BINS],
|
||||
}
|
||||
|
||||
impl Default for HealthDiagSnapshot {
|
||||
/// Zeroed snapshot. Every f32 is 0.0, every u32 is 0 — including
|
||||
/// the boolean fire-flags ("off"). Producing the default does **not**
|
||||
/// require allocator state; safe to call from any context.
|
||||
fn default() -> Self {
|
||||
// Safety: every field is `f32`, `u32`, or array of those, and zero
|
||||
// is a valid bit-pattern for all of them. No padding bytes by
|
||||
// construction (every field 4-byte aligned, struct ends on 4-byte
|
||||
// boundary).
|
||||
unsafe { MaybeUninit::<Self>::zeroed().assume_init() }
|
||||
}
|
||||
}
|
||||
|
||||
// ── MappedHealthDiagSnapshot ──────────────────────────────────────────────────
|
||||
|
||||
/// Mapped-pinned wrapper holding a single `HealthDiagSnapshot`.
|
||||
///
|
||||
/// Allocates one page-aligned mapped-pinned region of
|
||||
/// `sizeof(HealthDiagSnapshot)` bytes via `cuMemHostAlloc(DEVICEMAP|PORTABLE)`.
|
||||
/// The kernel writes through `dev_ptr` (with `__threadfence_system()` in the
|
||||
/// finalise kernel); the host reads through `host_ref()` after stream sync.
|
||||
///
|
||||
/// This is the **only** permitted CPU↔GPU communication path for HEALTH_DIAG
|
||||
/// per `feedback_no_htod_htoh_only_mapped_pinned.md`. No `memcpy_dtoh`.
|
||||
#[allow(missing_debug_implementations)] // raw pointer + CUdeviceptr have no useful Debug
|
||||
pub struct MappedHealthDiagSnapshot {
|
||||
host_ptr: *mut HealthDiagSnapshot,
|
||||
dev_ptr: cudarc::driver::sys::CUdeviceptr,
|
||||
}
|
||||
|
||||
// Safety: only accessed from the thread that constructed it (which holds
|
||||
// the active CUDA context). The host pointer is valid for the life of the
|
||||
// allocation.
|
||||
unsafe impl Send for MappedHealthDiagSnapshot {}
|
||||
unsafe impl Sync for MappedHealthDiagSnapshot {}
|
||||
|
||||
impl MappedHealthDiagSnapshot {
|
||||
/// Allocate one mapped-pinned `HealthDiagSnapshot` and zero-initialise.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must hold an active CUDA context on the current thread.
|
||||
pub unsafe fn new() -> Result<Self, String> {
|
||||
let num_bytes = std::mem::size_of::<HealthDiagSnapshot>();
|
||||
let flags = cudarc::driver::sys::CU_MEMHOSTALLOC_DEVICEMAP
|
||||
| cudarc::driver::sys::CU_MEMHOSTALLOC_PORTABLE;
|
||||
|
||||
let host_ptr = cudarc::driver::result::malloc_host(num_bytes, flags)
|
||||
.map_err(|e| format!("MappedHealthDiagSnapshot alloc ({num_bytes} bytes): {e}"))?
|
||||
as *mut HealthDiagSnapshot;
|
||||
|
||||
// Zero-init so a stale read (before the first kernel run) returns the
|
||||
// safe `Default` rather than uninitialised garbage.
|
||||
std::ptr::write_bytes(host_ptr as *mut u8, 0, num_bytes);
|
||||
|
||||
let mut dev_ptr_raw = MaybeUninit::uninit();
|
||||
cudarc::driver::sys::cuMemHostGetDevicePointer_v2(
|
||||
dev_ptr_raw.as_mut_ptr(),
|
||||
host_ptr as *mut c_void,
|
||||
0,
|
||||
)
|
||||
.result()
|
||||
.map_err(|e| format!("cuMemHostGetDevicePointer (HealthDiagSnapshot): {e}"))?;
|
||||
|
||||
Ok(Self {
|
||||
host_ptr,
|
||||
dev_ptr: dev_ptr_raw.assume_init(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Device pointer the kernel chain writes through.
|
||||
#[inline]
|
||||
pub fn dev_ptr(&self) -> cudarc::driver::sys::CUdeviceptr {
|
||||
self.dev_ptr
|
||||
}
|
||||
|
||||
/// Borrow the snapshot for reading. Caller must have synchronised the
|
||||
/// stream that produced the writes (mapped-pinned coherence guarantees
|
||||
/// the values are visible after the sync barrier).
|
||||
///
|
||||
/// # Safety
|
||||
/// `host_ptr` is valid for one `HealthDiagSnapshot` by construction;
|
||||
/// the borrow lifetime is tied to `&self`.
|
||||
#[inline]
|
||||
pub fn host_ref(&self) -> &HealthDiagSnapshot {
|
||||
// Safety: see invariants above.
|
||||
unsafe { &*self.host_ptr }
|
||||
}
|
||||
|
||||
/// Total byte size of the underlying mapped region. Useful when
|
||||
/// initialising the kernel's offset table or for diagnostics.
|
||||
#[inline]
|
||||
pub fn byte_size() -> usize {
|
||||
std::mem::size_of::<HealthDiagSnapshot>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MappedHealthDiagSnapshot {
|
||||
fn drop(&mut self) {
|
||||
// Safety: host_ptr was returned by malloc_host and is non-null.
|
||||
// Best-effort free — nothing actionable on failure inside Drop.
|
||||
unsafe {
|
||||
#[allow(clippy::let_underscore_must_use)]
|
||||
let _ = cudarc::driver::result::free_host(self.host_ptr as *mut c_void);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The struct must be `#[repr(C)]` POD with no padding so the producer
|
||||
/// kernel can address every field by a stable byte offset. This test
|
||||
/// pins the size — adding/removing fields requires updating the
|
||||
/// expected byte count here, which forces a corresponding kernel
|
||||
/// review.
|
||||
#[test]
|
||||
fn snapshot_size_is_stable() {
|
||||
// Field count breakdown (all 4-byte fields):
|
||||
// 8 health + 8 effective + 7 novels + 2 diag + 1 gem
|
||||
// + 10 mag + 4 grad_bwd + 5 grad_aux + 9 grad_trunk + 2 grad_abs
|
||||
// + 6 trail + 6 magstats + 6 noisy + 3 eval_dist
|
||||
// + 4 reward_contrib + 7 controller + 3 explore
|
||||
// + 16 val + 4 val_dir + 4 val_picked
|
||||
// + 6 reward_split + 4 aux + 10 aux_moe
|
||||
// + 12 action_counts
|
||||
// = 8+8+7+2+1+10+4+5+9+2+6+6+6+3+4+7+3+16+4+4+6+4+10+12 = 147 fields
|
||||
let expected_bytes = 147 * 4;
|
||||
assert_eq!(
|
||||
std::mem::size_of::<HealthDiagSnapshot>(),
|
||||
expected_bytes,
|
||||
"HealthDiagSnapshot must be {} bytes (147 × 4); update test if struct changes",
|
||||
expected_bytes,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_is_zeroed() {
|
||||
let snap = HealthDiagSnapshot::default();
|
||||
assert_eq!(snap.health, 0.0);
|
||||
assert_eq!(snap.action_counts, [0u32; HEALTH_DIAG_ACTION_BINS]);
|
||||
assert_eq!(snap.aux_moe_util, [0.0f32; HEALTH_DIAG_MOE_EXPERTS]);
|
||||
assert_eq!(snap.grad_trunk, [0.0f32; HEALTH_DIAG_GRAD_TRUNK]);
|
||||
assert_eq!(snap.ctrl_fire_anti_lr, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment_is_4_bytes() {
|
||||
// Every field is 4 bytes; the struct's alignment requirement
|
||||
// matches. Pinning this here guarantees the kernel-side
|
||||
// `(float*)snapshot + offset` arithmetic is well-defined.
|
||||
assert_eq!(std::mem::align_of::<HealthDiagSnapshot>(), 4);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use crate::MLError;
|
||||
pub mod multi_gpu;
|
||||
pub mod signal_adapter;
|
||||
pub mod mapped_pinned;
|
||||
pub mod health_diag;
|
||||
|
||||
pub mod gpu_portfolio;
|
||||
pub mod gpu_weights;
|
||||
@@ -38,6 +39,14 @@ pub mod gpu_her;
|
||||
mod cublaslt_debug;
|
||||
pub mod q_value_provider;
|
||||
pub mod gpu_iql_trainer;
|
||||
|
||||
// Re-exports — convenience for HEALTH_DIAG snapshot consumers (training_loop.rs,
|
||||
// metrics.rs).
|
||||
pub use health_diag::{
|
||||
HealthDiagSnapshot, MappedHealthDiagSnapshot, HEALTH_DIAG_ACTION_BINS,
|
||||
HEALTH_DIAG_GRAD_TRUNK, HEALTH_DIAG_MOE_EXPERTS,
|
||||
};
|
||||
|
||||
pub mod gpu_iqn_head;
|
||||
pub mod gpu_attention;
|
||||
pub mod gpu_tlob;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
**Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7.
|
||||
|
||||
HEALTH_DIAG GPU port — Phase 1 (2026-04-28, follow-up to commit `333ea7184`'s Phase 0 inventory): added `HealthDiagSnapshot` `#[repr(C)]` POD struct and `MappedHealthDiagSnapshot` mapped-pinned wrapper in new `crates/ml/src/cuda_pipeline/health_diag.rs`. The struct holds 147 fields (every numeric value emitted across the 7 HEALTH_DIAG log sites) as `f32` or `u32` so CPU and GPU agree byte-for-byte; controller fire booleans (`fire_lr`/`fire_tau`/etc.) promoted from `u8` → `u32` per the design review to avoid `#[repr(C)]` padding mismatch when followed by `f32` fields. `MappedHealthDiagSnapshot::new()` wraps `cuMemHostAlloc(DEVICEMAP|PORTABLE)` of `sizeof(HealthDiagSnapshot)` + `cuMemHostGetDevicePointer_v2` — the same pattern as `MappedF32Buffer` but typed for the snapshot. Re-exported from `cuda_pipeline/mod.rs` for downstream consumers. **No producer kernel and no consumer wiring in this commit** — Phase 2 lands the kernel family (`health_diag_per_sample_reduce`, `health_diag_q_mag_reduce`, `health_diag_eval_histogram`, `health_diag_isv_mirror`, `health_diag_finalise`) and the `gpu_health_diag.rs` orchestrator; Phase 3 wires the launch into the captured graph alongside `launch_h_s2_rms_ema`; Phase 4 rewrites the CPU emit path and deletes the 12 CPU-bound feeder methods identified in the Phase 0 inventory. Three unit tests (`snapshot_size_is_stable`, `default_is_zeroed`, `alignment_is_4_bytes`) pin the layout so any future field add/reorder requires an explicit test update — guards the kernel-side offset table from silent drift. Touched: `cuda_pipeline/health_diag.rs` (+339 LOC, new), `cuda_pipeline/mod.rs` (+8 LOC re-exports). cargo check clean at 12 warnings (workspace baseline). No fingerprint change — separate mapped-pinned alloc, not part of param-tensor layout the fingerprint guards.
|
||||
|
||||
multi_fold_convergence smoke MBP-10 wiring (2026-04-28): the test now forwards `--mbp10-data-dir` and `--trades-data-dir` to its spawned `train_baseline_rl` subprocess, reading paths from `FOXHUNT_MBP10_DATA` / `FOXHUNT_TRADES_DATA` env vars (set by the L40S sanitizer-test + nsys-test Argo templates to `/data/test-data/{mbp10,trades}`) with fallback to `<data_dir>/../futures-baseline-{mbp10,trades}` for local repo runs. The test hard-fails fast if either path is missing, citing `feedback_mbp10_mandatory.md`. Without this, OFI features (state slots [18..26)) silently fell back to tick-rule proxy and the smoke validated a degraded model variant — a `feedback_no_partial_refactor.md` violation now closed.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user