From 60fd7de96d7182299c95b5f707d9247fc26ff736 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Tue, 28 Apr 2026 23:33:05 +0200 Subject: [PATCH] =?UTF-8?q?feat(diag):=20Phase=201=20=E2=80=94=20HealthDia?= =?UTF-8?q?gSnapshot=20struct=20+=20mapped-pinned=20wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/ml/src/cuda_pipeline/health_diag.rs | 411 +++++++++++++++++++++ crates/ml/src/cuda_pipeline/mod.rs | 9 + docs/dqn-wire-up-audit.md | 2 + 3 files changed, 422 insertions(+) create mode 100644 crates/ml/src/cuda_pipeline/health_diag.rs diff --git a/crates/ml/src/cuda_pipeline/health_diag.rs b/crates/ml/src/cuda_pipeline/health_diag.rs new file mode 100644 index 000000000..0eea37eff --- /dev/null +++ b/crates/ml/src/cuda_pipeline/health_diag.rs @@ -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::::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 { + let num_bytes = std::mem::size_of::(); + 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::() + } +} + +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::(), + 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::(), 4); + } +} diff --git a/crates/ml/src/cuda_pipeline/mod.rs b/crates/ml/src/cuda_pipeline/mod.rs index c922173ba..b33ef3695 100644 --- a/crates/ml/src/cuda_pipeline/mod.rs +++ b/crates/ml/src/cuda_pipeline/mod.rs @@ -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; diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index 8e0a7e230..37baad247 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -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 `/../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.