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>
276 lines
13 KiB
Rust
276 lines
13 KiB
Rust
//! 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],
|
||
);
|
||
}
|