Files
foxhunt/crates/ml-alpha/tests/isv_bootstrap.rs
jgrusewski 705d6c156b audit: ISV-ify 10 more design constants — Schulman + bootstraps + streaming α
Per `feedback_isv_for_adaptive_bounds` + user "do all except floors
and clamp bounds": 10 more constants moved from kernel-side `#define`s
into ISV slots (78 slots total now).

## Slot additions (468-477)

  RL_SCHULMAN_TOLERANCE_INDEX        (468, =1.5)   — shared by 4 controllers
  RL_SCHULMAN_ADJUST_RATE_INDEX      (469, =1.5)   — shared by 4 controllers
  RL_STREAM_ALPHA_INDEX              (470, =0.05)  — shared by var + kurt streaming
  RL_KURT_GAUSSIAN_INDEX             (471, =3.0)
  RL_KURT_NOISE_FLOOR_INDEX          (472, =1.0)
  RL_TAU_BOOTSTRAP_INDEX             (473, =0.005)
  RL_EPS_BOOTSTRAP_INDEX             (474, =0.2)
  RL_ROLLOUT_BOOTSTRAP_INDEX         (475, =2048)
  RL_REWARD_SCALE_BOOTSTRAP_INDEX    (476, =1.0)
  RL_PPO_RATIO_CLAMP_BOOTSTRAP_INDEX (477, =10.0)

## Skipped (per user "do all except floors and clamp bounds")

  * `*_MIN`/`*_MAX` clamp bounds (algebraic domain — risk γ=1.5 nonsense)
  * Numerical floors: ABS_MEAN_FLOOR=1e-6, M2_SQ_FLOOR=1e-12, EPS_PNL=1e-3
    (risk div-by-zero if mis-tuned)
  * C51 atom layout (V_MIN/V_MAX) — architecture, not config

## Wiring

  * Shared Schulman pattern: 4 controllers (ppo_clip, target_tau,
    rollout_steps, plus per_α independent KURT slots) now read TOLERANCE
    + ADJUST_RATE from the same 2 ISV slots. Single source of truth.
  * Each controller's bootstrap (1st-emit on sentinel-zero) reads
    isv[*_BOOTSTRAP_INDEX] instead of #define value. The `prev ==
    BOOTSTRAP` first-observation replace-direct check also reads from
    ISV.
  * 2 streaming kernels (var + kurt) share RL_STREAM_ALPHA_INDEX.

## Diag bake-in

JSONL `isv_config` block grows by 10 new fields: schulman_tolerance,
schulman_adjust_rate, stream_alpha, kurt_gaussian, kurt_noise_floor,
tau_bootstrap, eps_bootstrap, rollout_bootstrap,
reward_scale_bootstrap, ppo_ratio_clamp_bootstrap. Total isv_config
fields: 26.

Also includes windowed action_entropy fix (was structurally 0 at
b_size=1) — accumulates EMA-smoothed action distribution over
~1k-step window, computes entropy on the windowed dist. Makes the
exploration metric meaningful at b_size=1.

## Slot total

RL_SLOTS_END: 468 → 478. **78 total ISV slots.**

## Verified gates (local sm_86)

  G1 isv_bootstrap    (with 10 new assertions)
  G3 controllers     
  G4 target_update   
  integrated_smoke   

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 01:47:18 +02:00

276 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 full ISV slice to host. Uses the same pattern as the
// trainer's own per-step ISV mirror refresh.
let mut isv = vec![0.0_f32; RL_SLOTS_END];
let stream = dev.cuda_stream().expect("cuda_stream");
stream
.memcpy_dtoh(&trainer.isv_d, isv.as_mut_slice())
.expect("isv dtoh");
// 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],
);
}