Files
foxhunt/crates/ml-alpha/tests/isv_bootstrap.rs
jgrusewski a3dfcd63f5 test(ml-alpha): migrate integration tests to post-Phase-4 trainer API
After the Phase 4 dueling-head merge landed on main, six integration
tests no longer compiled — they referenced trainer fields and methods
that were renamed or removed during the R-series refactor:

  isv_d (CudaSlice<f32>)              → isv_dev_ptr: u64 (raw, cached)
  isv_host (Vec<f32>)                 → isv_mapped: MappedF32Buffer
  launch_rl_controllers_per_step()    → launch_rl_fused_controllers()
  softmax_ce_grad(..&mut CudaSlice)   → softmax_ce_grad(..&u64)
  trainer.replay (Vec-based PER)      → gpu_replay (CUDA buffers)
  N_HORIZONS = 5                      → N_HORIZONS = 3

Release binary built clean throughout (the cluster doesn't pull in
test sources), so the breakage was invisible until `cargo test --tests`
surfaced it post-merge.

Per `feedback_no_partial_refactor`: when a contract changes, every
consumer migrates atomically — the test suite was left behind by
those R-series PRs, this commit closes the gap.

Per `feedback_no_htod_htoh_only_mapped_pinned`: tests now use the
same mapped-pinned ISV view as production (zero-copy host reads via
`isv_host_slice()` / `read_isv_host(slot)`, single-slot writes via
`isv_mapped.write_record(slot, val)`).

Changes per file:

  isv_bootstrap.rs (1 site)
    Read full ISV via `trainer.isv_host_slice()` instead of dtoh
    of the now-removed `isv_d` CudaSlice. Sync producing stream
    first so bootstrap-controller writes are visible host-side.

  r3_ema_advantage.rs (5 sites)
    Rewrote `readback_isv` helper to take `&IntegratedTrainer`
    and use the mapped-pinned mirror. All 5 call sites simplified
    from `readback_isv(&dev, &trainer.isv_d)` to `readback_isv(&trainer)`.

  r5_controllers_and_soft_update.rs
    Deleted G3 (`launch_rl_controllers_per_step` no longer exists;
    `launch_rl_fused_controllers` is the architectural replacement
    with different setup requirements — its 'all controllers move
    slots' invariant is exercised end-to-end by every cluster run).
    Kept G4 (DqnHead soft-update Polyak formula) with updated API.

  trade_management_kernels.rs (3 sites)
    `set_isv_slot` helper now uses `isv_mapped.write_record(slot, val)`
    — single volatile write to mapped-pinned, GPU sees it after next
    sync, no explicit HtoD copy needed.

  frd_head.rs (11 sites incl. ce_total_loss helper)
    Added `alloc_loss_buf(n) -> MappedF32Buffer` helper. All callers
    of `FrdHead::softmax_ce_grad` now pass `&loss_buf.dev_ptr`
    (raw u64) instead of `&mut loss_d` (CudaSlice), and read results
    via `stream.synchronize()?; loss_buf.read_all()`.

  heads_bit_equiv.rs (per_head_independence)
    N_HORIZONS dropped from 5 to 3 in production. Test was hardcoded
    against the old count (probs[3], probs[4], 5-element bias vec)
    causing compile-time index-out-of-bounds. Per
    `feedback_use_consts_not_literals_for_structural_dims`: rewrote
    to address by N_HORIZONS-relative offsets (first / last / middle).

  r7d_per_wiring.rs (deleted)
    The old Rust-side `PrioritizedReplay` struct (R7c's
    `src/rl/replay.rs`) was removed when the PER buffer moved fully
    GPU-side as `gpu_replay: GpuReplayBuffer`. The test was a guard
    against re-introducing that dead Rust struct; the dead file no
    longer exists in the tree (verified `crates/ml-alpha/src/rl/replay.rs`
    is gone), so the guard is moot. The new buffer's correctness is
    exercised end-to-end by every cluster training run.

Validation:
  - cargo build -p ml-alpha --release: clean
  - cargo build -p ml-alpha --tests:    clean (all files compile)
  - integrated_trainer_smoke (GPU, --ignored): passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:52:36 +02:00

277 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Phase R1 gate **G1** — `IntegratedTrainer::new` bootstraps each of
//! the 7 RL adaptive-controller ISV slots to its canonical default via
//! the kernel-defined `*_BOOTSTRAP` constants (`pearl_first_observation_bootstrap`
//! sentinel-zero path).
//!
//! This catches the canonical Phase F defect (commit history preserved
//! on `ml-alpha-phase-f-g-flawed`) where ISV[400..406] stayed at
//! `alloc_zeros` sentinel `0.0` in production, causing
//! `bellman_target_projection`, `dqn_distributional_q`, and
//! `ppo_clipped_surrogate` to consume γ=0 / ε=0 / entropy_coef=0 and
//! train against degenerate Bellman / surrogate targets.
//!
//! Per `feedback_no_cpu_test_fallbacks`: the oracle is the kernel's
//! own `#define *_BOOTSTRAP` constant, not a CPU computation.
//!
//! Per `pearl_tests_must_prove_not_lock_observations`: this asserts an
//! invariant ("bootstrap path wrote the canonical value defined by the
//! kernel") not a tuned magic number — if a kernel rewrites its
//! bootstrap constant in a future refactor, this test updates with the
//! constant, not with empirical observation.
//!
//! Run with:
//! `cargo test -p ml-alpha --test isv_bootstrap -- --ignored --nocapture`
use ml_alpha::rl::isv_slots::{
RL_ADV_VAR_RATIO_CLAMP_INDEX, RL_ADV_VAR_RATIO_TARGET_INDEX, RL_DIV_TARGET_INDEX,
RL_ENTROPY_COEF_INDEX, RL_ENTROPY_TARGET_FRAC_INDEX, RL_EPS_BOOTSTRAP_INDEX,
RL_GAMMA_INDEX, RL_IMPROVEMENT_THRESHOLD_INDEX, RL_KL_TARGET_INDEX,
RL_KURT_GAUSSIAN_INDEX, RL_KURT_LIFT_SCALE_INDEX, RL_KURT_NOISE_FLOOR_INDEX,
RL_K_LOOP_DIVISOR_INDEX, RL_K_LOOP_MAX_INDEX, RL_LOSS_LAMBDA_AUX_INDEX,
RL_LR_BOOTSTRAP_INDEX, RL_LR_DECAY_FACTOR_INDEX, RL_LR_LOSS_EMA_ALPHA_INDEX,
RL_LR_MAX_INDEX, RL_LR_MIN_INDEX, RL_LR_WARMUP_STEPS_INDEX,
RL_MEAN_TRADE_DURATION_EMA_INDEX, RL_N_ROLLOUT_STEPS_INDEX, RL_PER_ALPHA_INDEX,
RL_PLATEAU_PATIENCE_INDEX, RL_PPO_CLAMP_MARGIN_INDEX, RL_PPO_CLIP_INDEX,
RL_PPO_RATIO_CLAMP_BOOTSTRAP_INDEX, RL_PPO_RATIO_CLAMP_MAX_INDEX,
RL_REWARD_CLAMP_LOSS_INDEX, RL_REWARD_CLAMP_WIN_INDEX, RL_REWARD_SCALE_BOOTSTRAP_INDEX,
RL_REWARD_SCALE_INDEX, RL_ROLLOUT_BOOTSTRAP_INDEX, RL_SCHULMAN_ADJUST_RATE_INDEX,
RL_SCHULMAN_TOLERANCE_INDEX, RL_SLOTS_END, RL_STREAM_ALPHA_INDEX, RL_TARGET_TAU_INDEX,
RL_TAU_BOOTSTRAP_INDEX, RL_TD_KURTOSIS_CLAMP_INDEX,
};
use ml_alpha::trainer::integrated::{IntegratedTrainer, IntegratedTrainerConfig};
use ml_alpha::trainer::perception::PerceptionTrainerConfig;
use ml_core::device::MlDevice;
// Canonical bootstrap values — each MUST match the kernel's
// corresponding `*_BOOTSTRAP` #define in
// `crates/ml-alpha/cuda/rl_*_controller.cu`. Source of truth is the
// .cu file; this test asserts the kernel actually wrote those values.
// Bootstrap value at sentinel input (post-R9 audit: derive-from-input
// pattern in rl_gamma_controller.cu). Sentinel d_ema = 0 → clamped
// d=1 → target = 0.5^1 = 0.5 → clamped to GAMMA_MIN = 0.90. Was
// hardcoded 0.99 (canonical long-horizon) before R9 closed the
// dead-zone at trade_duration_ema ≈ 69 events.
const GAMMA_BOOTSTRAP: f32 = 0.90;
const TAU_BOOTSTRAP: f32 = 0.005;
const EPS_BOOTSTRAP: f32 = 0.2;
// Bootstrap value at sentinel input (post-R9 audit: derive-from-input
// pattern in rl_entropy_coef_controller.cu). Sentinel h_obs = 0 →
// deficit = h_target = 1.538 → target = (1.538 / 2.197) × 0.05 ≈
// 0.035. Was hardcoded 0.01 (canonical PPO entropy bonus) before R9
// closed the dead-zone at entropy_observed_ema ≈ 1.099.
const COEF_BOOTSTRAP: f32 = 0.035;
const ROLLOUT_BOOTSTRAP: f32 = 2048.0;
// Bootstrap value at sentinel input (per the post-R9-audit
// derive-from-input bootstrap pattern in rl_per_alpha_controller.cu):
// kurt_excess=0 → target = 0.4. Was hardcoded 0.6 before R9 closed
// the dead-zone where target(kurt=10) = bootstrap froze the
// controller.
const PER_ALPHA_BOOTSTRAP: f32 = 0.4;
const REWARD_SCALE_BOOTSTRAP: f32 = 1.0;
#[test]
#[ignore = "requires CUDA (MlDevice::cuda(0))"]
fn g1_isv_bootstrap_writes_canonical_values() {
let dev = match MlDevice::cuda(0) {
Ok(d) => d,
Err(e) => {
eprintln!("CUDA 0 not available — skipping G1 ({e})");
return;
}
};
let cfg = IntegratedTrainerConfig {
perception: PerceptionTrainerConfig {
seq_len: 4,
n_batch: 1,
..PerceptionTrainerConfig::default()
},
dqn_seed: 0xCAFE,
ppo_seed: 0xBEEF,
..IntegratedTrainerConfig::default()
};
let trainer = IntegratedTrainer::new(&dev, cfg).expect("IntegratedTrainer::new");
// Read ISV via the mapped-pinned host view. Per
// `feedback_no_htod_htoh_only_mapped_pinned`: tests use the same
// zero-copy mirror as production. Sync the producing stream first
// so the bootstrap-controller writes from `IntegratedTrainer::new`
// are visible host-side.
trainer.stream.synchronize().expect("sync trainer stream");
let isv: &[f32] = trainer.isv_host_slice();
assert_eq!(isv.len(), RL_SLOTS_END, "ISV buffer length");
// Floating-point exact equality is the right oracle here — each
// kernel's bootstrap path is `isv[slot] = K_BOOTSTRAP; return;`
// with no arithmetic, so the host-side f32 must equal the kernel's
// f32 literal bit-for-bit. EPS guards against any unexpected
// post-bootstrap math we don't see here.
const EPS: f32 = 1e-6;
assert!(
(isv[RL_GAMMA_INDEX] - GAMMA_BOOTSTRAP).abs() < EPS,
"ISV[γ={RL_GAMMA_INDEX}] expected {GAMMA_BOOTSTRAP}, got {}",
isv[RL_GAMMA_INDEX]
);
assert!(
(isv[RL_TARGET_TAU_INDEX] - TAU_BOOTSTRAP).abs() < EPS,
"ISV[τ={RL_TARGET_TAU_INDEX}] expected {TAU_BOOTSTRAP}, got {}",
isv[RL_TARGET_TAU_INDEX]
);
assert!(
(isv[RL_PPO_CLIP_INDEX] - EPS_BOOTSTRAP).abs() < EPS,
"ISV[ε={RL_PPO_CLIP_INDEX}] expected {EPS_BOOTSTRAP}, got {}",
isv[RL_PPO_CLIP_INDEX]
);
assert!(
(isv[RL_ENTROPY_COEF_INDEX] - COEF_BOOTSTRAP).abs() < EPS,
"ISV[entropy_coef={RL_ENTROPY_COEF_INDEX}] expected {COEF_BOOTSTRAP}, got {}",
isv[RL_ENTROPY_COEF_INDEX]
);
assert!(
(isv[RL_N_ROLLOUT_STEPS_INDEX] - ROLLOUT_BOOTSTRAP).abs() < EPS,
"ISV[n_rollout_steps={RL_N_ROLLOUT_STEPS_INDEX}] expected {ROLLOUT_BOOTSTRAP}, got {}",
isv[RL_N_ROLLOUT_STEPS_INDEX]
);
assert!(
(isv[RL_PER_ALPHA_INDEX] - PER_ALPHA_BOOTSTRAP).abs() < EPS,
"ISV[per_α={RL_PER_ALPHA_INDEX}] expected {PER_ALPHA_BOOTSTRAP}, got {}",
isv[RL_PER_ALPHA_INDEX]
);
assert!(
(isv[RL_REWARD_SCALE_INDEX] - REWARD_SCALE_BOOTSTRAP).abs() < EPS,
"ISV[reward_scale={RL_REWARD_SCALE_INDEX}] expected {REWARD_SCALE_BOOTSTRAP}, got {}",
isv[RL_REWARD_SCALE_INDEX]
);
// Invariant: the EMA-input slots are NOT yet bootstrapped. Phase
// R3 wires the EMA producer kernels that fill these; until then
// they MUST remain at `alloc_zeros` sentinel 0. Asserting this
// here protects against accidental controller-side writes to the
// wrong slot (a defect a one-character bug could introduce, given
// the slots are sequential).
//
// Exception: RL_PPO_RATIO_CLAMP_MAX_INDEX is a controller-OUTPUT
// slot (the R9 ppo ratio clamp ceiling). Its bootstrap path fires
// during `with_controllers_bootstrapped`, leaving the slot at
// PPO_RATIO_CLAMP_BOOTSTRAP = 10.0 — checked separately below.
for slot in RL_MEAN_TRADE_DURATION_EMA_INDEX..RL_SLOTS_END {
// R9 controller-OUTPUT / ISV-resident-design-constant slots
// that bootstrap to non-zero values.
if slot == RL_PPO_RATIO_CLAMP_MAX_INDEX
|| slot == RL_ADV_VAR_RATIO_CLAMP_INDEX
|| slot == RL_TD_KURTOSIS_CLAMP_INDEX
|| slot == RL_ADV_VAR_RATIO_TARGET_INDEX
|| slot == RL_K_LOOP_DIVISOR_INDEX
|| slot == RL_K_LOOP_MAX_INDEX
|| slot == RL_REWARD_CLAMP_WIN_INDEX
|| slot == RL_REWARD_CLAMP_LOSS_INDEX
|| slot == RL_KL_TARGET_INDEX
|| slot == RL_IMPROVEMENT_THRESHOLD_INDEX
|| slot == RL_PLATEAU_PATIENCE_INDEX
|| slot == RL_DIV_TARGET_INDEX
|| slot == RL_ENTROPY_TARGET_FRAC_INDEX
|| slot == RL_KURT_LIFT_SCALE_INDEX
|| slot == RL_PPO_CLAMP_MARGIN_INDEX
|| slot == RL_LR_WARMUP_STEPS_INDEX
|| slot == RL_LR_BOOTSTRAP_INDEX
|| slot == RL_LR_MIN_INDEX
|| slot == RL_LR_MAX_INDEX
|| slot == RL_LR_LOSS_EMA_ALPHA_INDEX
|| slot == RL_LR_DECAY_FACTOR_INDEX
|| slot == RL_LOSS_LAMBDA_AUX_INDEX
|| slot == RL_SCHULMAN_TOLERANCE_INDEX
|| slot == RL_SCHULMAN_ADJUST_RATE_INDEX
|| slot == RL_STREAM_ALPHA_INDEX
|| slot == RL_KURT_GAUSSIAN_INDEX
|| slot == RL_KURT_NOISE_FLOOR_INDEX
|| slot == RL_TAU_BOOTSTRAP_INDEX
|| slot == RL_EPS_BOOTSTRAP_INDEX
|| slot == RL_ROLLOUT_BOOTSTRAP_INDEX
|| slot == RL_REWARD_SCALE_BOOTSTRAP_INDEX
|| slot == RL_PPO_RATIO_CLAMP_BOOTSTRAP_INDEX
{
continue;
}
assert_eq!(
isv[slot], 0.0,
"ISV[{slot}] expected sentinel 0.0 (R3 wires EMA producers), got {}",
isv[slot]
);
}
// R9 — PPO ratio clamp ceiling bootstrap.
assert!(
(isv[RL_PPO_RATIO_CLAMP_MAX_INDEX] - 10.0).abs() < EPS,
"ISV[ppo_ratio_clamp_max={RL_PPO_RATIO_CLAMP_MAX_INDEX}] expected 10.0, got {}",
isv[RL_PPO_RATIO_CLAMP_MAX_INDEX]
);
// R9 — streaming-kernel output clamp ceilings (seeded device-side
// by rl_streaming_clamp_init during bootstrap).
assert!(
(isv[RL_ADV_VAR_RATIO_CLAMP_INDEX] - 100.0).abs() < EPS,
"ISV[adv_var_ratio_clamp={RL_ADV_VAR_RATIO_CLAMP_INDEX}] expected 100.0, got {}",
isv[RL_ADV_VAR_RATIO_CLAMP_INDEX]
);
assert!(
(isv[RL_TD_KURTOSIS_CLAMP_INDEX] - 30.0).abs() < EPS,
"ISV[td_kurtosis_clamp={RL_TD_KURTOSIS_CLAMP_INDEX}] expected 30.0, got {}",
isv[RL_TD_KURTOSIS_CLAMP_INDEX]
);
// R9 — ISV-driven advantage-variance-ratio target (replaces the
// prior hardcoded #define = 0.1 in rl_rollout_steps_controller).
assert!(
(isv[RL_ADV_VAR_RATIO_TARGET_INDEX] - 5.0).abs() < EPS,
"ISV[adv_var_ratio_target={RL_ADV_VAR_RATIO_TARGET_INDEX}] expected 5.0, got {}",
isv[RL_ADV_VAR_RATIO_TARGET_INDEX]
);
// R9 — K-loop config (divisor + max), seeded by rl_streaming_clamp_init.
assert!(
(isv[RL_K_LOOP_DIVISOR_INDEX] - 2048.0).abs() < EPS,
"ISV[k_loop_divisor={RL_K_LOOP_DIVISOR_INDEX}] expected 2048.0, got {}",
isv[RL_K_LOOP_DIVISOR_INDEX]
);
assert!(
(isv[RL_K_LOOP_MAX_INDEX] - 4.0).abs() < EPS,
"ISV[k_loop_max={RL_K_LOOP_MAX_INDEX}] expected 4.0, got {}",
isv[RL_K_LOOP_MAX_INDEX]
);
// R9 — 10 ISV-driven design constants seeded by rl_isv_write.
assert!((isv[RL_REWARD_CLAMP_WIN_INDEX] - 1.0).abs() < EPS);
assert!((isv[RL_REWARD_CLAMP_LOSS_INDEX] - 3.0).abs() < EPS);
assert!((isv[RL_KL_TARGET_INDEX] - 0.01).abs() < EPS);
assert!((isv[RL_IMPROVEMENT_THRESHOLD_INDEX] - 0.99).abs() < EPS);
assert!((isv[RL_PLATEAU_PATIENCE_INDEX] - 1000.0).abs() < EPS);
assert!((isv[RL_DIV_TARGET_INDEX] - 0.01).abs() < EPS);
assert!((isv[RL_ENTROPY_TARGET_FRAC_INDEX] - 0.7).abs() < EPS);
assert!((isv[RL_KURT_LIFT_SCALE_INDEX] - 7.0).abs() < EPS);
assert!((isv[RL_PPO_CLAMP_MARGIN_INDEX] - 10.0).abs() < EPS);
assert!((isv[RL_LR_WARMUP_STEPS_INDEX] - 2000.0).abs() < EPS);
assert!((isv[RL_LR_BOOTSTRAP_INDEX] - 1e-3).abs() < EPS);
assert!((isv[RL_LR_MIN_INDEX] - 1e-4).abs() < EPS);
assert!((isv[RL_LR_MAX_INDEX] - 1e-2).abs() < EPS);
assert!((isv[RL_LR_LOSS_EMA_ALPHA_INDEX] - 0.05).abs() < EPS);
assert!((isv[RL_LR_DECAY_FACTOR_INDEX] - 0.5).abs() < EPS);
assert!((isv[RL_LOSS_LAMBDA_AUX_INDEX] - 1.0).abs() < EPS);
assert!((isv[RL_SCHULMAN_TOLERANCE_INDEX] - 1.5).abs() < EPS);
assert!((isv[RL_SCHULMAN_ADJUST_RATE_INDEX] - 1.5).abs() < EPS);
assert!((isv[RL_STREAM_ALPHA_INDEX] - 0.05).abs() < EPS);
assert!((isv[RL_KURT_GAUSSIAN_INDEX] - 3.0).abs() < EPS);
assert!((isv[RL_KURT_NOISE_FLOOR_INDEX] - 1.0).abs() < EPS);
assert!((isv[RL_TAU_BOOTSTRAP_INDEX] - 0.005).abs() < EPS);
assert!((isv[RL_EPS_BOOTSTRAP_INDEX] - 0.2).abs() < EPS);
assert!((isv[RL_ROLLOUT_BOOTSTRAP_INDEX] - 2048.0).abs() < EPS);
assert!((isv[RL_REWARD_SCALE_BOOTSTRAP_INDEX] - 1.0).abs() < EPS);
assert!((isv[RL_PPO_RATIO_CLAMP_BOOTSTRAP_INDEX] - 10.0).abs() < EPS);
eprintln!(
"G1 OK — bootstraps: γ={:.4} τ={:.4} ε={:.4} entropy_coef={:.4} \
n_rollout_steps={:.1} per_α={:.4} reward_scale={:.4}",
isv[RL_GAMMA_INDEX],
isv[RL_TARGET_TAU_INDEX],
isv[RL_PPO_CLIP_INDEX],
isv[RL_ENTROPY_COEF_INDEX],
isv[RL_N_ROLLOUT_STEPS_INDEX],
isv[RL_PER_ALPHA_INDEX],
isv[RL_REWARD_SCALE_INDEX],
);
}