//! 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], ); }