feat: CV-based readiness gate for IQL features — self-calibrating warmup
Single GPU-resident scalar: CV = sigma_adv / |mean_adv|. When CV > 1 (noisy advantages), readiness < 1 → features suppressed. When CV ≤ 1 (stable advantages), readiness = 1 → features fully active. All 5 IQL downstream kernels blend toward neutral defaults at readiness=0: - Per-sample C51 support: blends to [-1, 1] default - PER modulation: blends to td_errors unmodified - Branch scales: blends to uniform 0.25 - Advantage weights: blends to neutral 1.0 - Expectile gap epsilon: gap scaled by readiness Zero new hyperparameters. Computed from existing adv_stats_buf. Best Sharpe improved 11.39 → 14.23 on 10-epoch stability test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,7 @@ pub struct GpuIqlTrainer {
|
||||
// ── New integration buffers ─────────────────────────────────────
|
||||
adv_stats_buf: CudaSlice<f32>, // [2] mean, variance
|
||||
adv_sigma_ema_buf: CudaSlice<f32>, // [1] GPU-side EMA of advantage std
|
||||
readiness_buf: CudaSlice<f32>, // [1] CV-based readiness scalar (0=suppress, 1=active)
|
||||
adv_sigma_ema_kernel: CudaFunction,
|
||||
per_sample_support_buf: CudaSlice<f32>, // [B, 3]
|
||||
branch_scales_buf: CudaSlice<f32>, // [B, 4]
|
||||
@@ -229,6 +230,7 @@ impl GpuIqlTrainer {
|
||||
// New integration buffers
|
||||
let adv_stats_buf = alloc_f32(&stream, 2, "iql_adv_stats")?;
|
||||
let adv_sigma_ema_buf = alloc_f32(&stream, 1, "iql_adv_sigma_ema")?;
|
||||
let readiness_buf = alloc_f32(&stream, 1, "iql_readiness")?;
|
||||
let mut per_sample_support_buf = alloc_f32(&stream, b * 3, "iql_per_sample_support")?;
|
||||
let branch_scales_buf = alloc_f32(&stream, b * 4, "iql_branch_scales")?;
|
||||
let expectile_gap_buf = alloc_f32(&stream, b, "iql_expectile_gap")?;
|
||||
@@ -304,6 +306,7 @@ impl GpuIqlTrainer {
|
||||
advantage_weights_buf,
|
||||
adv_stats_buf,
|
||||
adv_sigma_ema_buf,
|
||||
readiness_buf,
|
||||
adv_sigma_ema_kernel: kernels.adv_sigma_ema_update,
|
||||
per_sample_support_buf,
|
||||
branch_scales_buf,
|
||||
@@ -560,6 +563,7 @@ impl GpuIqlTrainer {
|
||||
.arg(q_out_buf)
|
||||
.arg(actions_buf)
|
||||
.arg(&self.v_out_buf)
|
||||
.arg(&self.readiness_buf)
|
||||
.arg(&mut self.advantage_weights_buf)
|
||||
.arg(&beta)
|
||||
.arg(&batch_size_i32)
|
||||
@@ -625,6 +629,7 @@ impl GpuIqlTrainer {
|
||||
.arg(&self.advantage_weights_buf)
|
||||
.arg(&indices_ptr)
|
||||
.arg(&self.adv_sigma_ema_buf)
|
||||
.arg(&self.readiness_buf)
|
||||
.arg(&beta)
|
||||
.arg(&lambda)
|
||||
.arg(&tau)
|
||||
@@ -667,6 +672,7 @@ impl GpuIqlTrainer {
|
||||
.launch_builder(&self.adv_sigma_ema_kernel)
|
||||
.arg(&self.adv_stats_buf)
|
||||
.arg(&mut self.adv_sigma_ema_buf)
|
||||
.arg(&mut self.readiness_buf)
|
||||
.arg(&ema_beta)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (1, 1, 1),
|
||||
@@ -696,6 +702,7 @@ impl GpuIqlTrainer {
|
||||
.arg(&self.v_out_buf)
|
||||
.arg(q_out_buf)
|
||||
.arg(&mut self.per_sample_support_buf)
|
||||
.arg(&self.readiness_buf)
|
||||
.arg(&gamma)
|
||||
.arg(&batch_i32)
|
||||
.arg(&total_actions_i32)
|
||||
@@ -733,6 +740,7 @@ impl GpuIqlTrainer {
|
||||
.arg(&self.v_out_buf)
|
||||
.arg(actions_buf)
|
||||
.arg(&mut self.branch_scales_buf)
|
||||
.arg(&self.readiness_buf)
|
||||
.arg(&batch_i32)
|
||||
.arg(&ta)
|
||||
.arg(&b0)
|
||||
@@ -803,6 +811,7 @@ impl GpuIqlTrainer {
|
||||
.launch_builder(&self.per_sample_epsilon_kernel)
|
||||
.arg(&self.expectile_gap_buf)
|
||||
.arg(&self.gap_mean_buf)
|
||||
.arg(&self.readiness_buf)
|
||||
.arg(&mut self.per_sample_epsilon_buf)
|
||||
.arg(&base_epsilon)
|
||||
.arg(&batch_i32)
|
||||
|
||||
@@ -576,6 +576,7 @@ void iql_compute_advantage_weights(
|
||||
const float* __restrict__ q_out, /* [B, total_actions] */
|
||||
const int* __restrict__ actions, /* [B] taken action indices */
|
||||
const float* __restrict__ v_out, /* [B] V(s) from IQL */
|
||||
const float* __restrict__ readiness_buf,/* [1] CV-based readiness */
|
||||
float* __restrict__ adv_weights, /* [B] output weights */
|
||||
float beta,
|
||||
int batch_size,
|
||||
@@ -585,11 +586,13 @@ void iql_compute_advantage_weights(
|
||||
int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= batch_size) return;
|
||||
|
||||
float r = readiness_buf[0];
|
||||
float q_taken = q_out[b * total_actions + actions[b]];
|
||||
float adv = q_taken - v_out[b];
|
||||
float w = expf(beta * adv);
|
||||
/* Clamp for numerical stability */
|
||||
adv_weights[b] = fminf(fmaxf(w, 0.01f), 100.0f);
|
||||
float raw_w = expf(beta * adv);
|
||||
float clamped_w = fminf(fmaxf(raw_w, 0.01f), 100.0f);
|
||||
/* Blend: readiness=0 → neutral weight 1.0, readiness=1 → IQL weight */
|
||||
adv_weights[b] = r * clamped_w + (1.0f - r) * 1.0f;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -612,6 +615,7 @@ void iql_modulate_td_errors(
|
||||
const float* __restrict__ adv_weights, /* [B] advantage weights */
|
||||
const int* __restrict__ indices, /* [B] buffer indices for staleness */
|
||||
const float* __restrict__ sigma_adv_buf, /* [1] EMA of advantage std (device-side) */
|
||||
const float* __restrict__ readiness_buf, /* [1] CV-based readiness */
|
||||
float beta,
|
||||
float staleness_lambda,
|
||||
float staleness_tau,
|
||||
@@ -623,6 +627,8 @@ void iql_modulate_td_errors(
|
||||
int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= batch_size) return;
|
||||
|
||||
float r = readiness_buf[0];
|
||||
|
||||
float sigma_adv = sigma_adv_buf[0];
|
||||
float K = expf(beta * 3.0f * fmaxf(sigma_adv, 1e-6f));
|
||||
K = fmaxf(K, 1.1f);
|
||||
@@ -633,7 +639,9 @@ void iql_modulate_td_errors(
|
||||
int age = (write_pos - indices[b] + capacity) % capacity;
|
||||
float decay = expf(-staleness_lambda * (float)age / fmaxf(staleness_tau, 1.0f));
|
||||
|
||||
td_errors[b] *= w * decay;
|
||||
/* Blend: readiness=0 → td_errors unchanged, readiness=1 → full modulation */
|
||||
float modulation = r * (w * decay) + (1.0f - r) * 1.0f;
|
||||
td_errors[b] *= modulation;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -652,17 +660,25 @@ extern "C" __global__
|
||||
void iql_adv_sigma_ema_update(
|
||||
const float* __restrict__ adv_stats, /* [2]: mean, variance */
|
||||
float* __restrict__ sigma_ema, /* [1] running EMA (device-side) */
|
||||
float* __restrict__ readiness_buf, /* [1] CV-based readiness scalar */
|
||||
float ema_beta /* 0.99 */
|
||||
)
|
||||
{
|
||||
float mean = adv_stats[0];
|
||||
float var = adv_stats[1];
|
||||
float sigma = sqrtf(fmaxf(var, 0.0f));
|
||||
float prev = sigma_ema[0];
|
||||
if (prev < 1e-8f) {
|
||||
sigma_ema[0] = sigma; /* first update: no EMA */
|
||||
sigma_ema[0] = sigma;
|
||||
} else {
|
||||
sigma_ema[0] = ema_beta * prev + (1.0f - ema_beta) * sigma;
|
||||
}
|
||||
|
||||
/* CV-based readiness: CV = sigma / |mean|.
|
||||
* CV > 1 → advantages are noise → suppress IQL features.
|
||||
* CV < 1 → advantages are stable → features fully active. */
|
||||
float cv = sigma_ema[0] / fmaxf(fabsf(mean), 1e-6f);
|
||||
readiness_buf[0] = fminf(1.0f / fmaxf(cv, 1.0f), 1.0f);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -717,6 +733,7 @@ void iql_compute_per_sample_support(
|
||||
const float* __restrict__ v_out, /* [B] */
|
||||
const float* __restrict__ q_out, /* [B, total_actions] */
|
||||
float* __restrict__ per_sample_support, /* [B*3] */
|
||||
const float* __restrict__ readiness_buf, /* [1] CV-based readiness */
|
||||
float gamma,
|
||||
int batch_size,
|
||||
int total_actions,
|
||||
@@ -726,6 +743,7 @@ void iql_compute_per_sample_support(
|
||||
int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= batch_size) return;
|
||||
|
||||
float r = readiness_buf[0];
|
||||
float v = v_out[b];
|
||||
const float* q = q_out + b * total_actions;
|
||||
|
||||
@@ -736,9 +754,14 @@ void iql_compute_per_sample_support(
|
||||
}
|
||||
|
||||
float half_w = spread * (1.0f + gamma);
|
||||
float v_min = v - half_w;
|
||||
float v_max = v + half_w;
|
||||
float delta_z = (v_max - v_min) / (float)(num_atoms - 1);
|
||||
float iql_vmin = v - half_w;
|
||||
float iql_vmax = v + half_w;
|
||||
float iql_dz = (iql_vmax - iql_vmin) / (float)(num_atoms - 1);
|
||||
|
||||
/* Blend: readiness=0 → default [-1,1], readiness=1 → V(s)-centered */
|
||||
float v_min = r * iql_vmin + (1.0f - r) * (-1.0f);
|
||||
float v_max = r * iql_vmax + (1.0f - r) * (1.0f);
|
||||
float delta_z = r * iql_dz + (1.0f - r) * (2.0f / (float)(num_atoms - 1));
|
||||
|
||||
per_sample_support[b * 3 + 0] = v_min;
|
||||
per_sample_support[b * 3 + 1] = v_max;
|
||||
@@ -762,6 +785,7 @@ void iql_per_branch_advantage(
|
||||
const float* __restrict__ v_out, /* [B] */
|
||||
const int* __restrict__ actions, /* [B] factored action indices */
|
||||
float* __restrict__ branch_scales, /* [B*4] */
|
||||
const float* __restrict__ readiness_buf, /* [1] CV-based readiness */
|
||||
int batch_size,
|
||||
int total_actions,
|
||||
int b0_size, int b1_size, int b2_size, int b3_size
|
||||
@@ -809,9 +833,12 @@ void iql_per_branch_advantage(
|
||||
max_a = fmaxf(max_a, a_branch[d]);
|
||||
}
|
||||
|
||||
float r = readiness_buf[0];
|
||||
float inv_max = (max_a > 1e-8f) ? (1.0f / max_a) : 1.0f;
|
||||
for (int d = 0; d < 4; d++) {
|
||||
branch_scales[b * 4 + d] = a_branch[d] * inv_max;
|
||||
/* Blend: readiness=0 → uniform 0.25, readiness=1 → IQL-derived scale */
|
||||
float iql_scale = a_branch[d] * inv_max;
|
||||
branch_scales[b * 4 + d] = r * iql_scale + (1.0f - r) * 0.25f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -874,6 +901,7 @@ extern "C" __global__
|
||||
void iql_compute_per_sample_epsilon(
|
||||
const float* __restrict__ gap, /* [B] per-sample expectile gaps */
|
||||
const float* __restrict__ gap_mean, /* [1] scalar mean gap */
|
||||
const float* __restrict__ readiness_buf, /* [1] CV-based readiness */
|
||||
float* __restrict__ per_sample_eps, /* [B] output epsilon values */
|
||||
float base_epsilon,
|
||||
int batch_size
|
||||
@@ -882,8 +910,11 @@ void iql_compute_per_sample_epsilon(
|
||||
int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= batch_size) return;
|
||||
|
||||
float r = readiness_buf[0];
|
||||
float gm = fmaxf(gap_mean[0], 1e-8f);
|
||||
float x = gap[b] / gm - 1.0f;
|
||||
/* Scale gap by readiness — at r=0, gap=0 → sigmoid(-1)=0.27 → uniform ~0.27*base_eps */
|
||||
float scaled_gap = r * gap[b];
|
||||
float x = scaled_gap / gm - 1.0f;
|
||||
float sig = 1.0f / (1.0f + expf(-x));
|
||||
per_sample_eps[b] = base_epsilon * sig;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user