feat(rl): adaptive reward clamp from positive-tail EMA

alpha-rl-rmgm5 (commit a776fab31) deep-diag finding:
  - static `[-3, +1]` clamp fired on 85% of steps
  - pre-clamp max: p95=15.5  p99=45.2  max=2830 (in WIN-bound units)
  - win distribution avg=+$2.06 max=+$11 squished to +1.0
  - loss distribution avg=-$3.84 routinely exceeded -3.0
  - per-trade EV = 0.357 * 2.06 + 0.643 * (-3.84) = -$1.74

The static clamp was crushing the gradient differential between
profitable and unprofitable trades, leaving Q with no signal to
distinguish good actions from bad. Adaptive bounds let the actual
winning-trade distribution reach the C51 atom support.

Implementation:
  - apply_reward_scale.cu: dual reduction (max|scaled| + max(positive
    scaled, 0)); positive-tail published to new ISV slot 478
  - rl_reward_clamp_controller.cu: maintains EMA of slot 478 in slot
    479 via Wiener-α blend (floor 0.4 per pearl_wiener_alpha_floor);
    writes WIN_eff = clamp(MARGIN * EMA, [1.0, 20.0]) to slot 452
    and LOSS_eff = RATIO * WIN to slot 453
  - 4 new ISV slots (478-481): raw + EMA + margin + ratio
  - Trainer per-step launch added at both apply_reward_scale sites
    (helper method + step_with_lobsim inline path)
  - Shared-mem bytes doubled at both apply_reward_scale launches
  - Static-default seeds added to with_controllers_bootstrapped
    (MARGIN=1.5, RATIO=3.0) — controller's bootstrap-on-sentinel
    path takes over from these once first positive reward observed
  - Diag JSONL exposes pos_scaled_max, pos_scaled_max_ema, and the
    margin/ratio config

Preserves 3:1 loss-aversion asymmetry per
pearl_audit_unboundedness_for_implicit_asymmetry — RATIO is itself
ISV-tunable. WIN floor 1.0 / ceiling 20.0 are hardcoded per the
user-stated "floors and clamp bounds" exemption (2026-05-24).

Adds #![recursion_limit = "256"] to alpha_rl_train.rs — the diag
json! block crossed serde_json's default 128-arg expansion budget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-24 12:36:12 +02:00
parent a776fab31f
commit 51b9f46364
6 changed files with 296 additions and 23 deletions

View File

@@ -68,6 +68,7 @@ const KERNELS: &[&str] = &[
"rl_isv_write", // RL R9: generic single-slot ISV seeder for tunable design constants — no HtoD
"rl_q_pi_agree_b", // RL R9 audit: per-batch fraction (argmax Q == argmax π) → ISV[407] EMA; wires previously-dead slot
"rl_pi_action_kernel", // audit Option B: π drives action selection via multinomial sampling from softmax(pi_logits); Q becomes pure critic
"rl_reward_clamp_controller", // audit 2026-05-24: adaptive [-LOSS, +WIN] clamp from positive-tail EMA; replaces static [-3, +1] that crushed winning-trade signal in rmgm5
];
// Cache bust v31 — five new reduce / derive kernels populate the input

View File

@@ -54,19 +54,37 @@
// Win cap default 1.0 matches the C51 atom span (V_MAX=1.0); loss cap
// default 3.0 preserves loss-aversion asymmetry (HFT P&L losses are
// typically 2-3× larger than wins per close). Seeded at trainer init
// by `rl_isv_write`; tunable at runtime by re-seeding.
// by `rl_isv_write`; tunable at runtime by re-seeding. Also adaptive
// per-step via `rl_reward_clamp_controller` reading the per-step
// positive-scaled max published below.
#define RL_REWARD_CLAMP_WIN_INDEX 452
#define RL_REWARD_CLAMP_LOSS_INDEX 453
// audit 2026-05-24: per-step max(positive scaled reward, 0) published
// here so `rl_reward_clamp_controller` can adapt WIN/LOSS bounds from
// the actual positive-tail distribution rather than the static 1.0/3.0
// defaults. alpha-rl-rmgm5 evidence: clamp fired on 85% of steps with
// p95 pre_clamp = 15.5× the WIN bound, crushing the gradient signal
// of profitable trades down to 1.0 while losses (avg -3.84) routinely
// exceeded the LOSS bound 3.0. Adaptive clamp lets the winning tail
// reach the C51 atom span properly.
#define RL_POS_SCALED_REWARD_MAX_INDEX 478
// Single block; grid-stride for b_size > BLOCK_DIM. Caller launches
// with block_dim = min(b_size, 256), grid_dim = 1, shared bytes =
// block_dim * sizeof(float).
// Shared-mem layout: caller passes `shared_mem_bytes = 2 * block_dim *
// sizeof(float)`. Lower half [0..block_dim) tracks max(|scaled|) for
// the diag slot; upper half [block_dim..2*block_dim) tracks max(positive
// scaled, 0) for the adaptive clamp controller's input signal.
extern "C" __global__ void apply_reward_scale(
float* __restrict__ isv, // ISV bus IN/OUT
float* __restrict__ rewards, // [b_size] IN/OUT (scaled+clamped)
int b_size
) {
extern __shared__ float s_max[];
extern __shared__ float smem[];
float* s_abs = smem;
float* s_pos = smem + blockDim.x;
const int tid = threadIdx.x;
const float scale = isv[RL_REWARD_SCALE_INDEX];
// ISV-driven clamp bounds (was hardcoded 1.0 / 3.0). Reading per
@@ -75,8 +93,11 @@ extern "C" __global__ void apply_reward_scale(
const float clamp_win = isv[RL_REWARD_CLAMP_WIN_INDEX];
const float clamp_loss = isv[RL_REWARD_CLAMP_LOSS_INDEX];
// Pass 1 — scale, clamp, accumulate per-thread max |scaled_pre_clamp|.
float local_max = 0.0f;
// Pass 1 — scale, clamp, accumulate per-thread max |scaled| AND max
// max(positive scaled, 0). Two independent local maxes tracked in
// a single grid-stride loop so the kernel's HBM read pass is shared.
float local_abs = 0.0f;
float local_pos = 0.0f;
for (int b = tid; b < b_size; b += blockDim.x) {
const float raw = rewards[b];
const float scaled = raw * scale;
@@ -84,7 +105,17 @@ extern "C" __global__ void apply_reward_scale(
// Track magnitude BEFORE clamping so the diag sees what the
// controller failed to bound on its own.
const float a = fabsf(scaled);
if (a > local_max) local_max = a;
if (a > local_abs) local_abs = a;
// Track positive tail BEFORE clamping for the adaptive clamp
// controller — winning-trade signal that the current static
// WIN=1.0 bound clips. Negative-tail (losses) tracked
// implicitly via max(|scaled|) at the diag slot since losses
// dominate the abs-distribution; the controller assumes the
// documented loss-aversion ratio (LOSS = ratio × WIN) so we
// don't need a separate negative-tail tracker.
const float p = fmaxf(scaled, 0.0f);
if (p > local_pos) local_pos = p;
// Asymmetric clamp: [-clamp_loss, +clamp_win] from ISV.
// Defaults preserve loss aversion (loss > win) while bounding
@@ -93,24 +124,29 @@ extern "C" __global__ void apply_reward_scale(
fminf(scaled, clamp_win));
rewards[b] = clamped;
}
s_max[tid] = local_max;
s_abs[tid] = local_abs;
s_pos[tid] = local_pos;
__syncthreads();
// Pass 2 — block tree reduce (no atomics per pearl_no_atomicadd).
// Both reductions advance together to share the __syncthreads barrier.
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
if (tid < stride) {
const float a = s_max[tid];
const float b = s_max[tid + stride];
s_max[tid] = a > b ? a : b;
const float a_a = s_abs[tid];
const float a_b = s_abs[tid + stride];
s_abs[tid] = a_a > a_b ? a_a : a_b;
const float p_a = s_pos[tid];
const float p_b = s_pos[tid + stride];
s_pos[tid] = p_a > p_b ? p_a : p_b;
}
__syncthreads();
}
// Thread 0 publishes the per-step pre-clamp max to the diagnostic
// ISV slot. This is a POINT measurement (not an EMA) — each
// training step overwrites with the current batch's max so the
// diag emitter sees the value for THIS step's reward batch.
// Thread 0 publishes per-step maxes to ISV. Both are POINT
// measurements; the controller maintains the EMA over the positive
// signal in its own slot.
if (tid == 0) {
isv[RL_MAX_ABS_SCALED_REWARD_PRE_CLAMP_INDEX] = s_max[0];
isv[RL_MAX_ABS_SCALED_REWARD_PRE_CLAMP_INDEX] = s_abs[0];
isv[RL_POS_SCALED_REWARD_MAX_INDEX] = s_pos[0];
}
}

View File

@@ -0,0 +1,95 @@
// rl_reward_clamp_controller.cu — emits adaptive reward-clamp bounds to
// ISV[RL_REWARD_CLAMP_WIN_INDEX=452] and ISV[RL_REWARD_CLAMP_LOSS_INDEX=453].
//
// Audit 2026-05-24: the static clamp `[-3.0, +1.0]` set up by
// `rl_isv_write` at trainer init was load-bearing on 85% of steps in
// alpha-rl-rmgm5 — pre-clamp p95 = 15.5× WIN, max = 2830× WIN. The
// adaptive bound below tracks the EMA of the per-step positive-tail
// max (published by `apply_reward_scale` into slot 478) so the C51
// Bellman target receives the actual winning-trade magnitude
// distribution rather than a near-constant +1.0 clipped signal.
//
// Reads:
// isv[RL_POS_SCALED_REWARD_MAX_INDEX = 478] — per-step max(scaled, 0)
// isv[RL_POS_SCALED_REWARD_MAX_EMA_INDEX = 479] — controller's EMA state
// isv[RL_REWARD_CLAMP_MARGIN_INDEX = 480] — k multiplier (default 1.5)
// isv[RL_REWARD_CLAMP_RATIO_INDEX = 481] — loss/win ratio (default 3.0)
//
// Writes:
// isv[RL_POS_SCALED_REWARD_MAX_EMA_INDEX = 479] — updated EMA
// isv[RL_REWARD_CLAMP_WIN_INDEX = 452] — adaptive WIN bound
// isv[RL_REWARD_CLAMP_LOSS_INDEX = 453] — adaptive LOSS = ratio × WIN
//
// EMA discipline: Wiener-α blend with caller-supplied α (host-floored
// at WIENER_ALPHA_FLOOR per `pearl_wiener_alpha_floor_for_nonstationary`).
// Bootstrap on sentinel 0 per `pearl_first_observation_bootstrap` —
// first non-zero raw observation replaces the EMA slot directly so the
// blend always has a real prev value to mix with.
//
// Bounds: WIN ∈ [MIN_WIN=1.0, MAX_WIN=20.0]. MIN_WIN preserves the
// floor from the static design — clamp never tightens below the C51
// V_MAX. MAX_WIN caps runaway during early training before the EMA
// stabilises (atom support × 20 still well within float headroom).
//
// LOSS = RATIO × WIN preserves the loss-aversion asymmetry from the
// `pearl_audit_unboundedness_for_implicit_asymmetry` design. Driven
// from a separate ISV slot so the asymmetry ratio is itself tunable
// without recompile.
//
// Per `feedback_no_atomicadd`: single-thread kernel. Per
// `feedback_cpu_is_read_only`: all state in ISV. Per
// `feedback_isv_for_adaptive_bounds`: hardcoded MIN/MAX retained per
// the user-stated "floors and clamp bounds" exemption (2026-05-24).
#define RL_POS_SCALED_REWARD_MAX_INDEX 478
#define RL_POS_SCALED_REWARD_MAX_EMA_INDEX 479
#define RL_REWARD_CLAMP_MARGIN_INDEX 480
#define RL_REWARD_CLAMP_RATIO_INDEX 481
#define RL_REWARD_CLAMP_WIN_INDEX 452
#define RL_REWARD_CLAMP_LOSS_INDEX 453
#define MIN_WIN 1.0f
#define MAX_WIN 20.0f
#define WIENER_ALPHA_FLOOR 0.4f
extern "C" __global__ void rl_reward_clamp_controller(
float* __restrict__ isv,
float alpha
) {
if (threadIdx.x != 0 || blockIdx.x != 0) return;
const float pos_max = isv[RL_POS_SCALED_REWARD_MAX_INDEX];
const float ema_prev = isv[RL_POS_SCALED_REWARD_MAX_EMA_INDEX];
// EMA maintenance — bootstrap on sentinel 0 with first non-zero
// raw observation; otherwise Wiener-α blend with floor.
float ema_new;
if (ema_prev == 0.0f) {
if (pos_max == 0.0f) {
// Both prev and obs sentinels — defer adaptation, leave the
// statically-seeded WIN(1.0) / LOSS(3.0) bounds untouched.
// Next call where the kernel observes a positive reward
// will bootstrap the EMA and start adapting.
return;
}
ema_new = pos_max;
} else {
const float a = fmaxf(alpha, WIENER_ALPHA_FLOOR);
// If pos_max==0 this step (no positive reward sample), still
// blend toward 0 — captures regimes where wins disappear. The
// EMA decays gracefully toward 0 over ~1/alpha steps.
ema_new = (1.0f - a) * ema_prev + a * pos_max;
}
isv[RL_POS_SCALED_REWARD_MAX_EMA_INDEX] = ema_new;
// Compute adaptive WIN bound: MARGIN × EMA, clamped to [MIN, MAX].
const float margin = isv[RL_REWARD_CLAMP_MARGIN_INDEX];
const float win_eff = fmaxf(MIN_WIN, fminf(MAX_WIN, margin * ema_new));
// LOSS = RATIO × WIN preserves asymmetry. RATIO seeded to 3.0.
const float ratio = isv[RL_REWARD_CLAMP_RATIO_INDEX];
const float loss_eff = ratio * win_eff;
isv[RL_REWARD_CLAMP_WIN_INDEX] = win_eff;
isv[RL_REWARD_CLAMP_LOSS_INDEX] = loss_eff;
}

View File

@@ -1,3 +1,8 @@
// The per-step diag `json!{...}` block now spans ≥ 130 fields after the
// adaptive-clamp additions, exceeding serde_json's default macro recursion
// budget of 128. Bumping at the crate root keeps the expansion within limits.
#![recursion_limit = "256"]
//! Integrated RL trainer CLI (DQN + PPO on shared Mamba2 -> CfC encoder).
//!
//! Drives `IntegratedTrainer::step_with_lobsim` against a real `LobSimCuda`
@@ -59,6 +64,8 @@ use ml_alpha::rl::isv_slots::{
RL_ROLLOUT_BOOTSTRAP_INDEX, RL_SCHULMAN_ADJUST_RATE_INDEX, RL_SCHULMAN_TOLERANCE_INDEX,
RL_STREAM_ALPHA_INDEX, RL_TAU_BOOTSTRAP_INDEX,
RL_MAX_ABS_SCALED_REWARD_PRE_CLAMP_INDEX, RL_MEAN_ABS_PNL_EMA_INDEX,
RL_POS_SCALED_REWARD_MAX_INDEX, RL_POS_SCALED_REWARD_MAX_EMA_INDEX,
RL_REWARD_CLAMP_MARGIN_INDEX, RL_REWARD_CLAMP_RATIO_INDEX,
RL_MEAN_TRADE_DURATION_EMA_INDEX, RL_N_ROLLOUT_STEPS_INDEX, RL_PER_ALPHA_INDEX,
RL_PI_GRAD_NORM_EMA_INDEX, RL_PPO_CLIP_INDEX, RL_PPO_LOG_RATIO_ABS_MAX_INDEX,
RL_PPO_RATIO_CLAMP_MAX_INDEX, RL_Q_DIVERGENCE_EMA_INDEX, RL_Q_GRAD_NORM_EMA_INDEX,
@@ -656,6 +663,13 @@ fn main() -> Result<()> {
"abs_max": reward_abs_max,
"scaled_pre_clamp_max":
isv[RL_MAX_ABS_SCALED_REWARD_PRE_CLAMP_INDEX],
// Adaptive clamp signals — surface the controller's
// POINT input + EMA + emitted bounds so post-hoc
// analysis can verify the adaptation chain is moving.
"pos_scaled_max":
isv[RL_POS_SCALED_REWARD_MAX_INDEX],
"pos_scaled_max_ema":
isv[RL_POS_SCALED_REWARD_MAX_EMA_INDEX],
},
// audit — PPO importance-ratio clamp diagnostics.
//
@@ -764,6 +778,8 @@ fn main() -> Result<()> {
"rollout_bootstrap": isv[RL_ROLLOUT_BOOTSTRAP_INDEX],
"reward_scale_bootstrap":isv[RL_REWARD_SCALE_BOOTSTRAP_INDEX],
"ppo_ratio_clamp_bootstrap": isv[RL_PPO_RATIO_CLAMP_BOOTSTRAP_INDEX],
"reward_clamp_margin": isv[RL_REWARD_CLAMP_MARGIN_INDEX],
"reward_clamp_ratio": isv[RL_REWARD_CLAMP_RATIO_INDEX],
},
// audit — Q vs π action agreement EMA at slot 407
// (previously dead). 1.0 = perfect ranking consistency,

View File

@@ -38,8 +38,9 @@
//! | 452-461 | 10 ISV-driven design constants (first batch) | rl_isv_write (seed loop) |
//! | 462-467 | 6 LR-controller numerics + aux λ | rl_isv_write (seed loop) |
//! | 468-477 | 10 design constants (Schulman + bootstraps + streaming α + kurt refs) | rl_isv_write |
//! | 478-481 | 4 adaptive reward-clamp slots (raw + EMA + margin + ratio) | apply_reward_scale + rl_reward_clamp_controller |
//!
//! Total: 78 slots; `RL_SLOTS_END = 478`.
//! Total: 82 slots; `RL_SLOTS_END = 482`.
/// Discount factor γ. Controller input: mean trade duration / anchor.
/// Bootstrap 0.99.
@@ -548,6 +549,52 @@ pub const RL_REWARD_SCALE_BOOTSTRAP_INDEX: usize = 476;
/// PPO ratio-clamp bootstrap. Default 10.0.
pub const RL_PPO_RATIO_CLAMP_BOOTSTRAP_INDEX: usize = 477;
// ─── Adaptive reward-clamp (audit 2026-05-24) ───────────────────────
//
// alpha-rl-rmgm5 (commit a776fab31) diag showed the static
// `[-RL_REWARD_CLAMP_LOSS, +RL_REWARD_CLAMP_WIN] = [-3.0, +1.0]`
// clamp firing on 85% of steps with p95 pre-clamp = 15.5× the WIN
// bound. Winning trade signal (avg +$2.06, max +$11 in scaled units)
// was squished to +1.0, while losses (avg -$3.84) routinely exceeded
// the LOSS bound. This kills the gradient differential between
// profitable and unprofitable actions in the C51 Bellman target.
//
// Fix: `rl_reward_clamp_controller` reads the per-step positive-tail
// max published by `apply_reward_scale` into slot 478, EMAs it into
// slot 479, and writes
//
// WIN_eff = clamp(MARGIN(480) × EMA(479), MIN_WIN=1.0, MAX_WIN=20.0)
// LOSS_eff = RATIO(481) × WIN_eff
//
// back into slots 452/453. The 3:1 ratio preserves the loss-aversion
// asymmetry per `pearl_audit_unboundedness_for_implicit_asymmetry`.
/// Per-step max(positive scaled reward, 0) — POINT measurement
/// published by `apply_reward_scale` for the adaptive clamp
/// controller. Overwritten every step.
pub const RL_POS_SCALED_REWARD_MAX_INDEX: usize = 478;
/// EMA of `RL_POS_SCALED_REWARD_MAX_INDEX` — maintained by
/// `rl_reward_clamp_controller` via Wiener-α blend (floor 0.4 per
/// `pearl_wiener_alpha_floor_for_nonstationary`; the reward
/// distribution drifts as the policy co-adapts). Sentinel 0 →
/// bootstrap to first non-zero raw observation per
/// `pearl_first_observation_bootstrap`.
pub const RL_POS_SCALED_REWARD_MAX_EMA_INDEX: usize = 479;
/// Adaptive reward-clamp margin multiplier — `WIN_eff = MARGIN × ema`.
/// Default 1.5 — gives 50% headroom above the EMA so occasional
/// fat-tail wins still register without overshooting the C51 atom
/// support. Seeded by `rl_isv_write` at trainer init.
pub const RL_REWARD_CLAMP_MARGIN_INDEX: usize = 480;
/// Adaptive reward-clamp loss/win ratio — `LOSS_eff = RATIO × WIN_eff`.
/// Default 3.0 — preserves the loss-aversion asymmetry from the
/// static `[-3, +1]` design per
/// `pearl_audit_unboundedness_for_implicit_asymmetry`. Seeded by
/// `rl_isv_write`.
pub const RL_REWARD_CLAMP_RATIO_INDEX: usize = 481;
/// Last RL-allocated slot index (exclusive). The integrated trainer
/// extends `ISV_TOTAL_DIM` to at least this value at trainer init time.
pub const RL_SLOTS_END: usize = 478;
pub const RL_SLOTS_END: usize = 482;

View File

@@ -159,6 +159,12 @@ const EXTRACT_REALIZED_PNL_DELTA_CUBIN: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/extract_realized_pnl_delta.cubin"));
const APPLY_REWARD_SCALE_CUBIN: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/apply_reward_scale.cubin"));
// Adaptive reward-clamp controller — audit fix 2026-05-24. Consumes the
// per-step positive-tail max written by apply_reward_scale into slot
// 478 and emits adaptive WIN/LOSS bounds into slots 452/453. See the
// kernel header for the design rationale (rmgm5 clamp-firing audit).
const RL_REWARD_CLAMP_CONTROLLER_CUBIN: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/rl_reward_clamp_controller.cubin"));
const ACTIONS_TO_MARKET_TARGETS_CUBIN: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/actions_to_market_targets.cubin"));
@@ -374,6 +380,9 @@ pub struct IntegratedTrainer {
extract_realized_pnl_delta_fn: CudaFunction,
_apply_reward_scale_module: Arc<CudaModule>,
apply_reward_scale_fn: CudaFunction,
// Adaptive reward-clamp controller (audit 2026-05-24).
_rl_reward_clamp_controller_module: Arc<CudaModule>,
rl_reward_clamp_controller_fn: CudaFunction,
_actions_to_market_targets_module: Arc<CudaModule>,
actions_to_market_targets_fn: CudaFunction,
@@ -759,6 +768,12 @@ impl IntegratedTrainer {
let apply_reward_scale_fn = apply_reward_scale_module
.load_function("apply_reward_scale")
.context("load apply_reward_scale")?;
let rl_reward_clamp_controller_module = ctx
.load_cubin(RL_REWARD_CLAMP_CONTROLLER_CUBIN.to_vec())
.context("load rl_reward_clamp_controller cubin")?;
let rl_reward_clamp_controller_fn = rl_reward_clamp_controller_module
.load_function("rl_reward_clamp_controller")
.context("load rl_reward_clamp_controller")?;
let actions_to_market_targets_module = ctx
.load_cubin(ACTIONS_TO_MARKET_TARGETS_CUBIN.to_vec())
.context("load actions_to_market_targets cubin")?;
@@ -1065,6 +1080,8 @@ impl IntegratedTrainer {
extract_realized_pnl_delta_fn,
_apply_reward_scale_module: apply_reward_scale_module,
apply_reward_scale_fn,
_rl_reward_clamp_controller_module: rl_reward_clamp_controller_module,
rl_reward_clamp_controller_fn,
_actions_to_market_targets_module: actions_to_market_targets_module,
actions_to_market_targets_fn,
_abs_copy_module: abs_copy_module,
@@ -1182,9 +1199,17 @@ impl IntegratedTrainer {
// (slot, value) pair — pure device write, no HtoD per
// `feedback_no_htod_htoh_only_mapped_pinned`.
{
let isv_constants: [(usize, f32); 26] = [
let isv_constants: [(usize, f32); 28] = [
// Static seeds for the adaptive reward-clamp controller —
// these are the initial values that
// `rl_reward_clamp_controller` will replace once it
// observes the first positive reward. Until then, kernels
// reading slots 452/453 see the documented [-3, +1]
// defaults.
(crate::rl::isv_slots::RL_REWARD_CLAMP_WIN_INDEX, 1.0),
(crate::rl::isv_slots::RL_REWARD_CLAMP_LOSS_INDEX, 3.0),
(crate::rl::isv_slots::RL_REWARD_CLAMP_MARGIN_INDEX, 1.5),
(crate::rl::isv_slots::RL_REWARD_CLAMP_RATIO_INDEX, 3.0),
(crate::rl::isv_slots::RL_KL_TARGET_INDEX, 0.01),
(crate::rl::isv_slots::RL_IMPROVEMENT_THRESHOLD_INDEX, 0.99),
(crate::rl::isv_slots::RL_PLATEAU_PATIENCE_INDEX, 1000.0),
@@ -1672,10 +1697,13 @@ impl IntegratedTrainer {
) -> Result<()> {
debug_assert_eq!(rewards_d.len(), b_size);
let block_x: u32 = (b_size as u32).min(256).max(1);
// Shared mem: 2 × block × f32 — kernel runs two parallel
// reductions (max|scaled| + max(positive scaled)) per audit
// 2026-05-24, both stored in `extern __shared__ float smem[]`.
let cfg = LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (block_x, 1, 1),
shared_mem_bytes: block_x * (std::mem::size_of::<f32>() as u32),
shared_mem_bytes: 2 * block_x * (std::mem::size_of::<f32>() as u32),
};
let b_size_i = b_size as i32;
let mut launch = self.stream.launch_builder(&self.apply_reward_scale_fn);
@@ -1683,6 +1711,30 @@ impl IntegratedTrainer {
unsafe {
launch.launch(cfg).context("apply_reward_scale launch")?;
}
// Adaptive reward-clamp controller — reads the per-step
// positive-tail max just published into slot 478, maintains
// an EMA in slot 479, writes adaptive WIN/LOSS bounds into
// slots 452/453 for the NEXT step's apply_reward_scale to
// consume. Single-thread kernel; α driven by the same
// controller-cohort floor as the other R5 EMAs.
{
let cfg_ctrl = LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (1, 1, 1),
shared_mem_bytes: 0,
};
let alpha = RL_LR_CONTROLLER_ALPHA;
let mut launch = self
.stream
.launch_builder(&self.rl_reward_clamp_controller_fn);
launch.arg(&self.isv_d).arg(&alpha);
unsafe {
launch
.launch(cfg_ctrl)
.context("rl_reward_clamp_controller launch")?;
}
}
Ok(())
}
@@ -2962,20 +3014,25 @@ impl IntegratedTrainer {
.context("launch_rl_controllers_per_step")?;
// Apply the reward scale on device (in place on rewards_d) +
// asymmetric clamp [-3, +1] + per-step pre-clamp max diag.
// Reads ISV[406] which the controller just updated; writes
// ISV[439] for the diag emitter.
// adaptive clamp + per-step pre-clamp diag + per-step
// positive-tail max for the adaptive clamp controller.
// Reads ISV[406] (scale) + ISV[452]/ISV[453] (current clamp
// bounds, refreshed by rl_reward_clamp_controller via the
// helper below); writes ISV[439] (diag) + ISV[478] (positive
// tail input for the controller).
//
// Single-block layout: tree reduction needs every thread in
// the same block to participate in __syncthreads, so we cap
// block_x at min(b_size, 256). For b_size > 256 the kernel's
// grid-stride loop walks the rest.
// grid-stride loop walks the rest. Shared mem now 2× block ×
// f32 to fit the parallel max(|scaled|) + max(positive)
// reductions.
{
let block_x: u32 = (b_size as u32).min(256).max(1);
let cfg = LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (block_x, 1, 1),
shared_mem_bytes: block_x * (std::mem::size_of::<f32>() as u32),
shared_mem_bytes: 2 * block_x * (std::mem::size_of::<f32>() as u32),
};
let mut launch = self.stream.launch_builder(&self.apply_reward_scale_fn);
launch
@@ -2987,6 +3044,27 @@ impl IntegratedTrainer {
}
}
// Adaptive reward-clamp controller — refresh slots 452/453 from
// the just-published positive-tail max in slot 478. The new
// bounds take effect on the NEXT step's apply_reward_scale.
{
let cfg_ctrl = LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (1, 1, 1),
shared_mem_bytes: 0,
};
let alpha = RL_LR_CONTROLLER_ALPHA;
let mut launch = self
.stream
.launch_builder(&self.rl_reward_clamp_controller_fn);
launch.arg(&self.isv_d).arg(&alpha);
unsafe {
launch
.launch(cfg_ctrl)
.context("rl_reward_clamp_controller launch (step_with_lobsim path)")?;
}
}
// GPU compute_advantage_return: returns_d, advantages_d
// populated for step_synthetic to consume.
{