feat(sp4): Tasks A14+A15 — Pearl C engagement counter in 5 Adam kernels
Layer A scaffolding for post-clamp feedback-loop self-correction. A14: All 5 Adam kernels (dqn/iqn/iql/attn/curiosity) extended with register-then-tree-reduce engagement counter. Per-thread `local_engage` set on clamp engagement; block tree-reduce (no atomicAdd, mirrors dqn_grad_norm_kernel pattern); single block-leader writes per-block count to clamp_engage_per_block_buf[engage_buf_offset + blockIdx.x]. `engage_buf_offset == -1 (SP4_ENGAGE_OFFSET_DISABLED)` skips writeback. A15: Host-side rate-deficit check in pearl_c_post_adam_engagement_check. Sums per-block counts via mapped-pinned zero-copy reads, computes engagement_rate, rate_deficit = rate - 0.01. Applies Pearls A+D via pearl_c_rate_deficit_ema (host-only [f32;8]) + pearl_c_rate_deficit_state_buf (MappedF32Buffer[24] = 8 groups × 3 Wiener floats). Wired post-graph in FusedTrainingCtx::run_full_step for groups 0/3/4/5/6 and post-curiosity_train in training_loop for group 7. 3 design issues resolved per Layer A scope: 1. Curiosity sub-launches: SP4_ENGAGE_BUF_LEN extended 2048->2816 (= 11 x 256). Curiosity sub-launches use offsets 1792/2048/2304/2560 (W1/b1/W2/b2). Host-side check sums all 4. Allocation + reset-registry updated. 2. TLOB/Attn sharing attn_adam_kernel: TLOB passes SP4_ENGAGE_OFFSET_DISABLED=-1 (silently skips Pearl C). Attn writes to its slot (offset 6x256=1536). Layer B can refactor if needed. 3. DQN main Adam covers groups 0/1/2 in single launch: Layer A accounts only under group 0 (DqnTrunk). Groups 1/2 Pearl C tracking deferred to Layer B's per-group sub-launch decision. Wiring path: GpuDqnTrainer exposes nan_flags_buf_ptr() + clamp_engage_per_block_buf_dev_ptr(). FusedTrainingCtx adds wire_aux_trainer_pearl_c_buffers() for IQN/IQL hi+lo/Attn. GpuExperienceCollector adds set_curiosity_pearl_c_buffers() for the curiosity trainer. Wiring fires once in init_gpu_experience_collector. State reset registry (Task A12 follow-up): - sp4_clamp_engage_counters description updated to reflect 2816 buffer length and 4 distinct curiosity sub-launch offsets - New entries: sp4_pearl_c_rate_deficit_state (24 mapped-pinned floats, Pearls A+D Wiener state) and sp4_pearl_c_rate_deficit_ema (host-only [f32; 8] EMA surrogate). Both reset to zero at fold boundary so Pearl A's first-observation sentinel fires on the new fold's first engagement-rate-deficit observation. Dispatch arms wired in reset_named_state alongside existing sp4_* entries. Layer A: Pearl C is observability scaffolding. Mech 9's clamp still uses hardcoded 100xQ_ABS_REF. Engagement counters fire correctly, rate_deficit EMA tracks. Force-bump branch logs via tracing::debug only; Layer B's atomic flip activates the actual ISV mutation. cargo check --lib --tests clean. state_reset_registry tests pass. sp4_isv_slots::tests::pearl_c_engage_buf_layout pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,52 +81,92 @@ extern "C" __global__ void attn_adam_kernel(
|
||||
* — same pattern wired into all five Adam kernels. Used by both
|
||||
* GpuAttention and GpuTlob (TLOB shares this kernel via the same cubin).
|
||||
* 0=disabled (debug only). */
|
||||
float weight_clamp_max_abs
|
||||
float weight_clamp_max_abs,
|
||||
/* SP4 Task A14 (2026-04-30): Pearl C engagement counter via warp+
|
||||
* block tree-reduce (no atomicAdd). GpuAttention + GpuTlob both pass
|
||||
* `engage_buf_offset = ParamGroup::Attn × MAX_BLOCKS_PER_ADAM` so
|
||||
* Pearl C tracks the kernel-level engagement rate (TLOB and core
|
||||
* attention share the same SP4 group). `engage_buf_offset < 0`
|
||||
* skips the writeback. */
|
||||
int* __restrict__ nan_flags_buf,
|
||||
int diag_slot,
|
||||
int* __restrict__ clamp_engage_per_block_buf,
|
||||
int engage_buf_offset
|
||||
) {
|
||||
int tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (tid >= total_params) return;
|
||||
bool active = (tid < total_params);
|
||||
int local_engage = 0;
|
||||
|
||||
/* Skip NaN/Inf gradients — leave param, momentum, variance unchanged. */
|
||||
{
|
||||
if (active) {
|
||||
/* Skip NaN/Inf gradients — leave param, momentum, variance unchanged. */
|
||||
float g_check = (float)grads[tid];
|
||||
if (!isfinite(g_check)) { grads[tid] = 0.0f; return; }
|
||||
if (!isfinite(g_check)) {
|
||||
grads[tid] = 0.0f;
|
||||
} else {
|
||||
/* Gradient clipping */
|
||||
float grad_norm = norm[0];
|
||||
float clip_scale = (grad_norm > max_grad_norm && grad_norm > 0.0f)
|
||||
? max_grad_norm / grad_norm : 1.0f;
|
||||
float g = grads[tid] * clip_scale;
|
||||
|
||||
/* AdamW: weight decay applied to params directly */
|
||||
params[tid] = params[tid] * (1.0f - lr * weight_decay);
|
||||
|
||||
/* Moment updates */
|
||||
float m_val = beta1 * m[tid] + (1.0f - beta1) * g;
|
||||
float v_val = beta2 * v[tid] + (1.0f - beta2) * g * g;
|
||||
m[tid] = m_val;
|
||||
v[tid] = v_val;
|
||||
|
||||
/* Bias correction */
|
||||
int adam_t = adam_t_buf[0];
|
||||
float f_t = (float)adam_t;
|
||||
float bc1 = 1.0f - powf(beta1, f_t);
|
||||
float bc2 = 1.0f - powf(beta2, f_t);
|
||||
float m_hat = m_val / (bc1 + 1e-12f);
|
||||
float v_hat = v_val / (bc2 + 1e-12f);
|
||||
|
||||
/* Parameter update */
|
||||
float p = params[tid] - lr * m_hat / (sqrtf(v_hat) + eps);
|
||||
|
||||
/* SP3 Mech 9 + SP4 Task A14: post-Adam |p| clamp +
|
||||
* engagement counter — last step before writeback. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
if (fabsf(p) > weight_clamp_max_abs) {
|
||||
if (nan_flags_buf != nullptr && diag_slot >= 0) {
|
||||
nan_flags_buf[diag_slot] = 1;
|
||||
}
|
||||
local_engage = 1;
|
||||
}
|
||||
p = fminf(fmaxf(p, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
}
|
||||
|
||||
params[tid] = p;
|
||||
|
||||
/* Zero gradient for next step */
|
||||
grads[tid] = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gradient clipping */
|
||||
float grad_norm = norm[0];
|
||||
float clip_scale = (grad_norm > max_grad_norm && grad_norm > 0.0f)
|
||||
? max_grad_norm / grad_norm : 1.0f;
|
||||
float g = grads[tid] * clip_scale;
|
||||
/* SP4 Task A14: warp + block tree-reduce of `local_engage`. */
|
||||
int e = local_engage;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
e += __shfl_xor_sync(0xFFFFFFFF, e, offset);
|
||||
|
||||
/* AdamW: weight decay applied to params directly */
|
||||
params[tid] = params[tid] * (1.0f - lr * weight_decay);
|
||||
__shared__ int warp_engage[8]; /* blockDim.x = 256 ⇒ 8 warps */
|
||||
int warp_id = threadIdx.x / 32;
|
||||
int warp_lane = threadIdx.x % 32;
|
||||
if (warp_lane == 0) warp_engage[warp_id] = e;
|
||||
__syncthreads();
|
||||
|
||||
/* Moment updates */
|
||||
float m_val = beta1 * m[tid] + (1.0f - beta1) * g;
|
||||
float v_val = beta2 * v[tid] + (1.0f - beta2) * g * g;
|
||||
m[tid] = m_val;
|
||||
v[tid] = v_val;
|
||||
|
||||
/* Bias correction */
|
||||
int adam_t = adam_t_buf[0];
|
||||
float f_t = (float)adam_t;
|
||||
float bc1 = 1.0f - powf(beta1, f_t);
|
||||
float bc2 = 1.0f - powf(beta2, f_t);
|
||||
float m_hat = m_val / (bc1 + 1e-12f);
|
||||
float v_hat = v_val / (bc2 + 1e-12f);
|
||||
|
||||
/* Parameter update */
|
||||
float p = params[tid] - lr * m_hat / (sqrtf(v_hat) + eps);
|
||||
|
||||
/* SP3 Mech 9: post-Adam |p| clamp — last step before writeback. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
p = fminf(fmaxf(p, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
if (warp_id == 0) {
|
||||
int val = (warp_lane < blockDim.x / 32) ? warp_engage[warp_lane] : 0;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
val += __shfl_xor_sync(0xFFFFFFFF, val, offset);
|
||||
if (warp_lane == 0 && engage_buf_offset >= 0 && clamp_engage_per_block_buf != nullptr) {
|
||||
clamp_engage_per_block_buf[engage_buf_offset + blockIdx.x] = val;
|
||||
}
|
||||
}
|
||||
|
||||
params[tid] = p;
|
||||
|
||||
/* Zero gradient for next step */
|
||||
grads[tid] = 0.0f;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
|
||||
@@ -323,34 +323,80 @@ extern "C" __global__ void curiosity_adam_step(
|
||||
int step, /* 1-based step counter */
|
||||
/* SP3 Mech 9 (close-out v2): ISV-driven |p| bound (100 × Q_ABS_REF.max(1.0))
|
||||
* — same pattern wired into all five Adam kernels. 0=disabled (debug only). */
|
||||
float weight_clamp_max_abs
|
||||
float weight_clamp_max_abs,
|
||||
/* SP4 Task A14 (2026-04-30): Pearl C engagement counter via warp+
|
||||
* block tree-reduce. Curiosity has 4 sub-buffer launches (W1, b1,
|
||||
* W2, b2) per step. Each sub-launch's grid is sized for its own
|
||||
* param count, so distinct engage_buf_offsets are required to
|
||||
* avoid overwriting peers' counts:
|
||||
* W1 → SP4_ENGAGE_OFFSET_CURIOSITY_W1 (= 7 × MAX_BLOCKS = 1792)
|
||||
* b1 → SP4_ENGAGE_OFFSET_CURIOSITY_B1 (= 8 × MAX_BLOCKS = 2048)
|
||||
* W2 → SP4_ENGAGE_OFFSET_CURIOSITY_W2 (= 9 × MAX_BLOCKS = 2304)
|
||||
* b2 → SP4_ENGAGE_OFFSET_CURIOSITY_B2 (= 10 × MAX_BLOCKS = 2560)
|
||||
* Host-side `pearl_c_post_adam_engagement_check(Curiosity, N)` sums
|
||||
* all four ranges. `engage_buf_offset < 0` skips the writeback. */
|
||||
int* __restrict__ nan_flags_buf,
|
||||
int diag_slot,
|
||||
int* __restrict__ clamp_engage_per_block_buf,
|
||||
int engage_buf_offset
|
||||
) {
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= num_params) return;
|
||||
bool active = (i < num_params);
|
||||
int local_engage = 0;
|
||||
|
||||
float lr_bf = lr;
|
||||
float beta1_bf = beta1;
|
||||
float beta2_bf = beta2;
|
||||
float eps_bf = eps;
|
||||
float one_bf = 1.0f;
|
||||
if (active) {
|
||||
float lr_bf = lr;
|
||||
float beta1_bf = beta1;
|
||||
float beta2_bf = beta2;
|
||||
float eps_bf = eps;
|
||||
float one_bf = 1.0f;
|
||||
|
||||
float g = grads[i] / (float)batch_size;
|
||||
float g = grads[i] / (float)batch_size;
|
||||
|
||||
/* Skip NaN/Inf gradients */
|
||||
if (!isfinite((float)g)) return;
|
||||
/* Skip NaN/Inf gradients — fall through to engagement reduce so
|
||||
* the per-block count stays correct. */
|
||||
if (isfinite((float)g)) {
|
||||
m[i] = beta1_bf * m[i] + (one_bf - beta1_bf) * g;
|
||||
v[i] = beta2_bf * v[i] + (one_bf - beta2_bf) * g * g;
|
||||
float m_hat = m[i] / (1.0f - powf(beta1, (float)step));
|
||||
float v_hat = v[i] / (1.0f - powf(beta2, (float)step));
|
||||
float p = params[i] - lr_bf * m_hat / (sqrtf(v_hat) + eps_bf);
|
||||
|
||||
m[i] = beta1_bf * m[i] + (one_bf - beta1_bf) * g;
|
||||
v[i] = beta2_bf * v[i] + (one_bf - beta2_bf) * g * g;
|
||||
float m_hat = m[i] / (1.0f - powf(beta1, (float)step));
|
||||
float v_hat = v[i] / (1.0f - powf(beta2, (float)step));
|
||||
float p = params[i] - lr_bf * m_hat / (sqrtf(v_hat) + eps_bf);
|
||||
/* SP3 Mech 9 + SP4 Task A14: post-Adam |p| clamp +
|
||||
* engagement counter — last step before writeback. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
if (fabsf(p) > weight_clamp_max_abs) {
|
||||
if (nan_flags_buf != nullptr && diag_slot >= 0) {
|
||||
nan_flags_buf[diag_slot] = 1;
|
||||
}
|
||||
local_engage = 1;
|
||||
}
|
||||
p = fminf(fmaxf(p, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
}
|
||||
|
||||
/* SP3 Mech 9: post-Adam |p| clamp — last step before writeback. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
p = fminf(fmaxf(p, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
params[i] = p;
|
||||
}
|
||||
}
|
||||
|
||||
params[i] = p;
|
||||
/* SP4 Task A14: warp + block tree-reduce of `local_engage`. */
|
||||
int e = local_engage;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
e += __shfl_xor_sync(0xFFFFFFFF, e, offset);
|
||||
|
||||
__shared__ int warp_engage[8]; /* blockDim.x = 256 ⇒ 8 warps */
|
||||
int warp_id = threadIdx.x / 32;
|
||||
int warp_lane = threadIdx.x % 32;
|
||||
if (warp_lane == 0) warp_engage[warp_id] = e;
|
||||
__syncthreads();
|
||||
|
||||
if (warp_id == 0) {
|
||||
int val = (warp_lane < blockDim.x / 32) ? warp_engage[warp_lane] : 0;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
val += __shfl_xor_sync(0xFFFFFFFF, val, offset);
|
||||
if (warp_lane == 0 && engage_buf_offset >= 0 && clamp_engage_per_block_buf != nullptr) {
|
||||
clamp_engage_per_block_buf[engage_buf_offset + blockIdx.x] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
@@ -984,6 +984,13 @@ impl DecisionTransformer {
|
||||
// the clamp here — the kernel branch is `if (bound > 0.0f)`,
|
||||
// so Adam behaves exactly as before for DT.
|
||||
let weight_clamp_max_abs_dt: f32 = 0.0_f32;
|
||||
// SP4 Task A14: DT is a separate offline-RL trainer outside the
|
||||
// SP4 8-group taxonomy — Pearl C engagement counter disabled.
|
||||
let dt_nan_flags_null: u64 = 0;
|
||||
let dt_diag_slot_disabled: i32 = -1;
|
||||
let dt_engage_buf_null: u64 = 0;
|
||||
let dt_engage_off_disabled: i32 =
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_DISABLED;
|
||||
unsafe {
|
||||
stream.launch_builder(&adam.adam_update)
|
||||
.arg(¶ms_ptr_mut)
|
||||
@@ -999,6 +1006,11 @@ impl DecisionTransformer {
|
||||
.arg(&l1_end_dt)
|
||||
.arg(&l1_lambda_dt)
|
||||
.arg(&weight_clamp_max_abs_dt) // SP3 Mech 9: disabled (no ISV in DT scope)
|
||||
// SP4 Task A14: disabled (DT outside SP4 group taxonomy).
|
||||
.arg(&dt_nan_flags_null)
|
||||
.arg(&dt_diag_slot_disabled)
|
||||
.arg(&dt_engage_buf_null)
|
||||
.arg(&dt_engage_off_disabled)
|
||||
.launch(lc)
|
||||
.map_err(|e| MLError::ModelError(format!("DT adam: {e}")))?;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,19 @@ extern "C" __global__ void dqn_grad_norm_finalize(
|
||||
* headroom. Disabled when `weight_clamp_max_abs == 0.0` (debug
|
||||
* fast-path; production never sets 0).
|
||||
*
|
||||
* SP4 Task A14 (2026-04-30): Pearl C engagement counter. Each thread
|
||||
* sets `local_engage = 1` when its parameter overshoots the bound;
|
||||
* block-wide warp+block tree-reduce (mirrors `dqn_grad_norm_kernel`
|
||||
* pattern, no atomicAdd per `feedback_no_atomicadd`); single block-leader
|
||||
* writes per-block count to
|
||||
* `clamp_engage_per_block_buf[engage_buf_offset + blockIdx.x]`. Host
|
||||
* sums across blocks → engagement_rate; rate-deficit Wiener-EMA detects
|
||||
* post-clamp feedback-loop saturation. `engage_buf_offset < 0` skips
|
||||
* the counter write (used by aux trainers outside the SP4 8-group
|
||||
* taxonomy — DT, OFI embed, denoise, recursive_conf, sel).
|
||||
* `nan_flags_buf[diag_slot] = 1` is a sticky idempotent write capturing
|
||||
* "any thread overshot the bound this step" for HEALTH_DIAG.
|
||||
*
|
||||
* Launch config: grid=(ceil(TOTAL_PARAMS/256), 1, 1), block=(256, 1, 1).
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
@@ -129,62 +142,105 @@ extern "C" __global__ void dqn_adam_update_kernel(
|
||||
const float* __restrict__ weight_decay_mask, /* [TOTAL_PARAMS] 1.0=decay, 0.0=no decay */
|
||||
int l1_end, /* param index where L1 stops (end of w_s1) */
|
||||
float l1_lambda, /* L1 penalty (1e-3). 0=disabled */
|
||||
float weight_clamp_max_abs /* SP3 Mech 9: ISV-driven |p_val| bound (100 * Q_ABS_REF.max(1.0)). 0=disabled. */
|
||||
float weight_clamp_max_abs, /* SP3 Mech 9: ISV-driven |p_val| bound (100 * Q_ABS_REF.max(1.0)). 0=disabled. */
|
||||
/* SP4 Task A14: Pearl C engagement counter. */
|
||||
int* __restrict__ nan_flags_buf, /* sticky overflow flag store; nan_flags_buf[diag_slot]=1 on first thread that overshoots */
|
||||
int diag_slot, /* index into nan_flags_buf (one per Adam kernel) */
|
||||
int* __restrict__ clamp_engage_per_block_buf, /* [SP4_PARAM_GROUP_COUNT × MAX_BLOCKS_PER_ADAM] per-block engagement counts */
|
||||
int engage_buf_offset /* per-group offset (group*MAX_BLOCKS); -1 skips writeback (aux trainers) */
|
||||
) {
|
||||
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (idx >= total_params) return;
|
||||
/* Out-of-range threads still participate in the block reduce (with
|
||||
* local_engage=0) so the block-wide tree-reduce stays correct. */
|
||||
bool active = (idx < total_params);
|
||||
|
||||
float lr = *lr_ptr;
|
||||
int t = *t_ptr;
|
||||
float g_f = grads[idx];
|
||||
int local_engage = 0;
|
||||
float lr = active ? *lr_ptr : 0.0f;
|
||||
int t = active ? *t_ptr : 1;
|
||||
float g_f = active ? grads[idx] : 0.0f;
|
||||
|
||||
/* Skip NaN/Inf gradients — leave param, momentum, variance unchanged.
|
||||
* Non-deterministic NaN can arise from IQN/CQL auxiliary gradient
|
||||
* injection during early training with extreme quantile estimates. */
|
||||
if (!isfinite(g_f)) return;
|
||||
bool grad_finite = active && isfinite(g_f);
|
||||
|
||||
/* Clip using the COMPLETED L2 norm (single buffer, written by grad_norm_finalize).
|
||||
* max_grad_norm read from pinned device-mapped buffer (EMA-based adaptive clip). */
|
||||
float max_grad_norm = *max_grad_norm_buf;
|
||||
float norm_f = *grad_norm_f32; /* Already L2 norm (sqrt applied by finalize) */
|
||||
float clip_scale_f = (norm_f > max_grad_norm) ? (max_grad_norm / norm_f) : 1.0f;
|
||||
float clipped_g_f = g_f * clip_scale_f;
|
||||
if (grad_finite) {
|
||||
/* Clip using the COMPLETED L2 norm (single buffer, written by grad_norm_finalize).
|
||||
* max_grad_norm read from pinned device-mapped buffer (EMA-based adaptive clip). */
|
||||
float max_grad_norm = *max_grad_norm_buf;
|
||||
float norm_f = *grad_norm_f32; /* Already L2 norm (sqrt applied by finalize) */
|
||||
float clip_scale_f = (norm_f > max_grad_norm) ? (max_grad_norm / norm_f) : 1.0f;
|
||||
float clipped_g_f = g_f * clip_scale_f;
|
||||
|
||||
/* Adam bias correction: native float — no bf16 rounding issues. */
|
||||
float beta1_t = fmaxf(1.0f - powf(beta1, (float)t), 1e-8f);
|
||||
float beta2_t = fmaxf(1.0f - powf(beta2, (float)t), 1e-8f);
|
||||
/* Adam bias correction: native float — no bf16 rounding issues. */
|
||||
float beta1_t = fmaxf(1.0f - powf(beta1, (float)t), 1e-8f);
|
||||
float beta2_t = fmaxf(1.0f - powf(beta2, (float)t), 1e-8f);
|
||||
|
||||
/* Adam moment updates in native float — full precision, no bf16 conversion. */
|
||||
float m_i = beta1 * m[idx] + (1.0f - beta1) * clipped_g_f;
|
||||
float v_i = beta2 * v[idx] + (1.0f - beta2) * clipped_g_f * clipped_g_f;
|
||||
m[idx] = m_i;
|
||||
v[idx] = v_i;
|
||||
/* Adam moment updates in native float — full precision, no bf16 conversion. */
|
||||
float m_i = beta1 * m[idx] + (1.0f - beta1) * clipped_g_f;
|
||||
float v_i = beta2 * v[idx] + (1.0f - beta2) * clipped_g_f * clipped_g_f;
|
||||
m[idx] = m_i;
|
||||
v[idx] = v_i;
|
||||
|
||||
float m_hat = m_i / beta1_t;
|
||||
float v_hat = v_i / beta2_t;
|
||||
float m_hat = m_i / beta1_t;
|
||||
float v_hat = v_i / beta2_t;
|
||||
|
||||
/* AdamW weight decay (decoupled) — masked: trunk+value=1.0, branches=0.0. */
|
||||
float p_val = params[idx];
|
||||
float wd = weight_decay * weight_decay_mask[idx];
|
||||
p_val = p_val - lr * (m_hat / (sqrtf(v_hat) + epsilon) + wd * p_val);
|
||||
/* AdamW weight decay (decoupled) — masked: trunk+value=1.0, branches=0.0. */
|
||||
float p_val = params[idx];
|
||||
float wd = weight_decay * weight_decay_mask[idx];
|
||||
p_val = p_val - lr * (m_hat / (sqrtf(v_hat) + epsilon) + wd * p_val);
|
||||
|
||||
/* G8: L1 proximal step on w_s1 — automatic feature selection */
|
||||
if (idx < l1_end && l1_lambda > 0.0f) {
|
||||
float sign_val = (p_val > 0.0f) ? 1.0f : -1.0f;
|
||||
p_val = sign_val * fmaxf(fabsf(p_val) - lr * l1_lambda, 0.0f);
|
||||
/* G8: L1 proximal step on w_s1 — automatic feature selection */
|
||||
if (idx < l1_end && l1_lambda > 0.0f) {
|
||||
float sign_val = (p_val > 0.0f) ? 1.0f : -1.0f;
|
||||
p_val = sign_val * fmaxf(fabsf(p_val) - lr * l1_lambda, 0.0f);
|
||||
}
|
||||
|
||||
/* SP3 Mech 9 + SP4 Task A14: post-Adam weight clamp + Pearl C
|
||||
* engagement counter. ISV-driven bound (100 × Q_ABS_REF.max(1.0))
|
||||
* computed host-side and passed in. Targets cuBLAS sgemm f32
|
||||
* accumulator overflow at slots 26+32 — bounds weights 1 OOM below
|
||||
* slot 44-45 diagnostic threshold (1e3 × Q_ABS_REF) so the
|
||||
* diagnostic retains regression-sentinel headroom. Disabled when
|
||||
* bound==0 (debug fast-path; production never sets 0).
|
||||
*
|
||||
* `local_engage = 1` when |p_val| would overshoot — captured into
|
||||
* the per-block tree-reduce below. The sticky `nan_flags_buf` write
|
||||
* is idempotent (always writes 1, never 0) so no race exists. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
if (fabsf(p_val) > weight_clamp_max_abs) {
|
||||
if (nan_flags_buf != nullptr && diag_slot >= 0) {
|
||||
nan_flags_buf[diag_slot] = 1;
|
||||
}
|
||||
local_engage = 1;
|
||||
}
|
||||
p_val = fminf(fmaxf(p_val, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
}
|
||||
|
||||
params[idx] = p_val;
|
||||
}
|
||||
|
||||
/* SP3 Mech 9: post-Adam weight clamp. ISV-driven bound
|
||||
* (100 × Q_ABS_REF.max(1.0)) computed host-side and passed in. Targets
|
||||
* cuBLAS sgemm f32 accumulator overflow at slots 26+32 — bounds weights
|
||||
* 1 OOM below slot 44-45 diagnostic threshold (1e3 × Q_ABS_REF) so the
|
||||
* diagnostic retains regression-sentinel headroom. Disabled when
|
||||
* bound==0 (debug fast-path; production never sets 0). */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
p_val = fminf(fmaxf(p_val, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
}
|
||||
/* SP4 Task A14: warp + block tree-reduce of `local_engage`. Mirrors
|
||||
* `dqn_grad_norm_kernel`'s pattern — no atomicAdd. blockDim.x=256 ⇒ 8
|
||||
* warps; warp_sums fits in __shared__[8]. */
|
||||
int e = local_engage;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
e += __shfl_xor_sync(0xFFFFFFFF, e, offset);
|
||||
|
||||
params[idx] = p_val;
|
||||
__shared__ int warp_engage[8];
|
||||
int warp_id = threadIdx.x / 32;
|
||||
int warp_lane = threadIdx.x % 32;
|
||||
if (warp_lane == 0) warp_engage[warp_id] = e;
|
||||
__syncthreads();
|
||||
|
||||
if (warp_id == 0) {
|
||||
int val = (warp_lane < blockDim.x / 32) ? warp_engage[warp_lane] : 0;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
val += __shfl_xor_sync(0xFFFFFFFF, val, offset);
|
||||
if (warp_lane == 0 && engage_buf_offset >= 0 && clamp_engage_per_block_buf != nullptr) {
|
||||
clamp_engage_per_block_buf[engage_buf_offset + blockIdx.x] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -230,6 +230,13 @@ pub struct GpuAttention {
|
||||
t_dev_ptr: u64,
|
||||
/// Total attention parameters count.
|
||||
total_params: usize,
|
||||
|
||||
// ── SP4 Task A14 Pearl C engagement-counter buffer pointers ────────
|
||||
/// Mapped-pinned `nan_flags_buf[55]` device pointer (owned by
|
||||
/// `GpuDqnTrainer`). 0 = unset → kernel skips per-block writeback.
|
||||
pearl_c_nan_flags_ptr: u64,
|
||||
/// Mapped-pinned `clamp_engage_per_block_buf` device pointer.
|
||||
pearl_c_engage_buf_ptr: u64,
|
||||
}
|
||||
|
||||
impl GpuAttention {
|
||||
@@ -534,9 +541,32 @@ impl GpuAttention {
|
||||
t_pinned,
|
||||
t_dev_ptr,
|
||||
total_params,
|
||||
// SP4 Task A14: Pearl C buffers wired post-construction by
|
||||
// FusedTrainingCtx via `set_pearl_c_buffers`. Until then,
|
||||
// null pointer + disabled offset signals the kernel to skip
|
||||
// per-block engagement-counter writeback.
|
||||
pearl_c_nan_flags_ptr: 0,
|
||||
pearl_c_engage_buf_ptr: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// SP4 Task A14: wire the trainer's mapped-pinned Pearl C buffers
|
||||
/// (owned by `GpuDqnTrainer`) into this attention head. Called once
|
||||
/// post-construction by `FusedTrainingCtx`. After wiring, every
|
||||
/// `adam_step` records per-block clamp-engagement counts at offset
|
||||
/// `ParamGroup::Attn × MAX_BLOCKS_PER_ADAM` (= 6 × 256 = 1536).
|
||||
pub fn set_pearl_c_buffers(&mut self, nan_flags: u64, engage_buf: u64) {
|
||||
self.pearl_c_nan_flags_ptr = nan_flags;
|
||||
self.pearl_c_engage_buf_ptr = engage_buf;
|
||||
}
|
||||
|
||||
/// SP4 Task A15: parameter count accessor for the host-side Pearl C
|
||||
/// engagement-rate check. Returns `config.total_params()` cached at
|
||||
/// construction.
|
||||
pub fn total_params(&self) -> usize {
|
||||
self.total_params
|
||||
}
|
||||
|
||||
/// Apply multi-head attention to batch of states with OFI embedding.
|
||||
///
|
||||
/// Input is constructed by concatenating states[D,B] + ofi_embed[10,B] into
|
||||
@@ -1084,6 +1114,30 @@ impl GpuAttention {
|
||||
let wd = 1e-5_f32;
|
||||
let t_ptr = self.t_dev_ptr;
|
||||
|
||||
// SP4 Task A14: Pearl C engagement-counter args. Both
|
||||
// `GpuAttention` (this module) and `GpuTlob` share the same
|
||||
// `attn_adam_kernel` cubin. For Layer A, GpuAttention writes to
|
||||
// its slot at offset 6×256=1536 (ParamGroup::Attn). GpuTlob
|
||||
// passes SP4_ENGAGE_OFFSET_DISABLED (= -1) so it silently
|
||||
// skips Pearl C bookkeeping until Layer B refactors them apart.
|
||||
// If `set_pearl_c_buffers` has not been called (FusedTrainingCtx
|
||||
// wiring), pass null + disabled offset so the kernel skips.
|
||||
let pearl_c_active = self.pearl_c_engage_buf_ptr != 0
|
||||
&& self.pearl_c_nan_flags_ptr != 0;
|
||||
let nan_flags_ptr: u64 = self.pearl_c_nan_flags_ptr;
|
||||
let diag_slot: i32 = if pearl_c_active {
|
||||
crate::cuda_pipeline::SP4_ATTN_ADAM_DIAG_SLOT
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
let engage_buf_ptr: u64 = self.pearl_c_engage_buf_ptr;
|
||||
let engage_buf_offset: i32 = if pearl_c_active {
|
||||
(crate::cuda_pipeline::ParamGroup::Attn as i32)
|
||||
* (crate::cuda_pipeline::MAX_BLOCKS_PER_ADAM as i32)
|
||||
} else {
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_DISABLED
|
||||
};
|
||||
|
||||
unsafe {
|
||||
stream
|
||||
.launch_builder(&self.adam_kernel)
|
||||
@@ -1101,6 +1155,10 @@ impl GpuAttention {
|
||||
.arg(&t_ptr)
|
||||
.arg(&tp_i32)
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9 (close-out v2): post-Adam |p| clamp
|
||||
.arg(&nan_flags_ptr) // SP4 A14: sticky engage flag store
|
||||
.arg(&diag_slot) // SP4 A14: diag slot (53 = Attn)
|
||||
.arg(&engage_buf_ptr) // SP4 A14: per-block engage buf
|
||||
.arg(&engage_buf_offset) // SP4 A14: group offset (6 = Attn)
|
||||
.launch(adam_cfg)
|
||||
.map_err(|e| MLError::ModelError(format!("attention adam kernel: {e}")))?;
|
||||
}
|
||||
|
||||
@@ -494,6 +494,13 @@ pub struct GpuCuriosityTrainer {
|
||||
|
||||
// Maximum number of samples the buffers were allocated for
|
||||
buf_capacity: usize,
|
||||
|
||||
// ── SP4 Task A14 Pearl C engagement-counter buffer pointers ────────
|
||||
/// Mapped-pinned `nan_flags_buf[55]` device pointer (owned by
|
||||
/// `GpuDqnTrainer`). 0 = unset → kernel skips per-block writeback.
|
||||
pearl_c_nan_flags_ptr: u64,
|
||||
/// Mapped-pinned `clamp_engage_per_block_buf` device pointer.
|
||||
pearl_c_engage_buf_ptr: u64,
|
||||
}
|
||||
|
||||
/// Launch Adam optimizer step for one parameter group.
|
||||
@@ -502,6 +509,7 @@ pub struct GpuCuriosityTrainer {
|
||||
/// bound for `curiosity_adam_step`. Caller threads `100 × Q_ABS_REF.max(1.0)`
|
||||
/// from training_loop down through `train_on_collector_buffers`. 0=disabled
|
||||
/// (debug only).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn launch_adam_step(
|
||||
stream: &CudaStream,
|
||||
adam_func: &CudaFunction,
|
||||
@@ -513,9 +521,25 @@ fn launch_adam_step(
|
||||
batch_size: usize,
|
||||
step: i32,
|
||||
weight_clamp_max_abs: f32,
|
||||
/* SP4 Task A14: per-sub-launch Pearl C engagement-counter args.
|
||||
* `engage_buf_offset` is unique per sub-buffer (W1/b1/W2/b2) so
|
||||
* the 4 sub-launches don't overwrite each other's per-block counts.
|
||||
* Pass `nan_flags_ptr=0` + `engage_buf_ptr=0` +
|
||||
* `engage_buf_offset=SP4_ENGAGE_OFFSET_DISABLED` to skip writeback. */
|
||||
nan_flags_ptr: u64,
|
||||
diag_slot: i32,
|
||||
engage_buf_ptr: u64,
|
||||
engage_buf_offset: i32,
|
||||
) -> Result<(), MLError> {
|
||||
let blocks = ((num_params as u32) + 255) / 256;
|
||||
debug_assert!(
|
||||
(blocks as usize) <= crate::cuda_pipeline::MAX_BLOCKS_PER_ADAM,
|
||||
"curiosity_adam blocks={} exceeds MAX_BLOCKS_PER_ADAM={} — Pearl C \
|
||||
clamp_engage_per_block_buf cannot accommodate.",
|
||||
blocks, crate::cuda_pipeline::MAX_BLOCKS_PER_ADAM
|
||||
);
|
||||
let cfg = LaunchConfig {
|
||||
grid_dim: (((num_params as u32) + 255) / 256, 1, 1),
|
||||
grid_dim: (blocks, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
};
|
||||
@@ -536,6 +560,10 @@ fn launch_adam_step(
|
||||
.arg(&ADAM_EPS)
|
||||
.arg(&step)
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9 (close-out v2): post-Adam |p| clamp
|
||||
.arg(&nan_flags_ptr) // SP4 A14: sticky engage flag store
|
||||
.arg(&diag_slot) // SP4 A14: diag slot (54 = Curiosity)
|
||||
.arg(&engage_buf_ptr) // SP4 A14: per-block engage buf
|
||||
.arg(&engage_buf_offset) // SP4 A14: per-sub-launch offset
|
||||
.launch(cfg)
|
||||
.map_err(|e| {
|
||||
MLError::ModelError(format!("curiosity_adam_step launch: {e}"))
|
||||
@@ -681,9 +709,34 @@ impl GpuCuriosityTrainer {
|
||||
step: 0,
|
||||
state_dim,
|
||||
buf_capacity: max_samples,
|
||||
// SP4 Task A14: Pearl C buffers wired post-construction by
|
||||
// FusedTrainingCtx via `set_pearl_c_buffers`. Until then,
|
||||
// null pointer + disabled offset signals each curiosity
|
||||
// sub-launch to skip per-block engagement writeback.
|
||||
pearl_c_nan_flags_ptr: 0,
|
||||
pearl_c_engage_buf_ptr: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// SP4 Task A14: thread the main DQN trainer's mapped-pinned Pearl C
|
||||
/// buffers into this curiosity trainer. Called once post-construction
|
||||
/// by `FusedTrainingCtx`. After wiring, the 4 sub-launches (W1, b1,
|
||||
/// W2, b2) record per-block clamp-engagement counts at distinct
|
||||
/// offsets `SP4_ENGAGE_OFFSET_CURIOSITY_{W1,B1,W2,B2}` so they don't
|
||||
/// overwrite each other. Pass 0 for either pointer to disable.
|
||||
pub fn set_pearl_c_buffers(&mut self, nan_flags: u64, engage_buf: u64) {
|
||||
self.pearl_c_nan_flags_ptr = nan_flags;
|
||||
self.pearl_c_engage_buf_ptr = engage_buf;
|
||||
}
|
||||
|
||||
/// SP4 Task A15: total parameter count summed across the 4 sub-buffers
|
||||
/// (W1 + b1 + W2 + b2). Used by the host-side Pearl C engagement-rate
|
||||
/// check, which sums per-block counts across all 4 sub-launch ranges
|
||||
/// before dividing by total params.
|
||||
pub const fn total_params() -> usize {
|
||||
CUR_W1_LEN + CUR_B1_LEN + CUR_W2_LEN + CUR_B2_LEN
|
||||
}
|
||||
|
||||
/// Train the curiosity forward model on GPU-resident experience data.
|
||||
///
|
||||
/// Performs one training step via cuBLAS GEMM pipeline (forward + backward + Adam).
|
||||
@@ -920,10 +973,55 @@ impl GpuCuriosityTrainer {
|
||||
self.step += 1;
|
||||
let step = self.step;
|
||||
|
||||
launch_adam_step(stream, &self.adam_func, &mut weights.w1, &self.grad_w1, CUR_W1_LEN, &mut self.adam_m_w1, &mut self.adam_v_w1, n_train, step, weight_clamp_max_abs)?;
|
||||
launch_adam_step(stream, &self.adam_func, &mut weights.b1, &self.grad_b1, CUR_B1_LEN, &mut self.adam_m_b1, &mut self.adam_v_b1, n_train, step, weight_clamp_max_abs)?;
|
||||
launch_adam_step(stream, &self.adam_func, &mut weights.w2, &self.grad_w2, CUR_W2_LEN, &mut self.adam_m_w2, &mut self.adam_v_w2, n_train, step, weight_clamp_max_abs)?;
|
||||
launch_adam_step(stream, &self.adam_func, &mut weights.b2, &self.grad_b2, CUR_B2_LEN, &mut self.adam_m_b2, &mut self.adam_v_b2, n_train, step, weight_clamp_max_abs)?;
|
||||
// SP4 Task A14: each sub-launch gets its own engage_buf_offset so
|
||||
// the 4 launches' per-block counts don't overlap. Pearl C is
|
||||
// active only when both buffer pointers are wired by
|
||||
// FusedTrainingCtx via `set_pearl_c_buffers`.
|
||||
let pearl_c_active = self.pearl_c_engage_buf_ptr != 0
|
||||
&& self.pearl_c_nan_flags_ptr != 0;
|
||||
let nan_flags_ptr = self.pearl_c_nan_flags_ptr;
|
||||
let engage_buf_ptr = self.pearl_c_engage_buf_ptr;
|
||||
let diag_slot = if pearl_c_active {
|
||||
crate::cuda_pipeline::SP4_CURIOSITY_ADAM_DIAG_SLOT
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
let (off_w1, off_b1, off_w2, off_b2) = if pearl_c_active {
|
||||
(
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_CURIOSITY_W1,
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_CURIOSITY_B1,
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_CURIOSITY_W2,
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_CURIOSITY_B2,
|
||||
)
|
||||
} else {
|
||||
let off = crate::cuda_pipeline::SP4_ENGAGE_OFFSET_DISABLED;
|
||||
(off, off, off, off)
|
||||
};
|
||||
|
||||
launch_adam_step(
|
||||
stream, &self.adam_func, &mut weights.w1, &self.grad_w1,
|
||||
CUR_W1_LEN, &mut self.adam_m_w1, &mut self.adam_v_w1,
|
||||
n_train, step, weight_clamp_max_abs,
|
||||
nan_flags_ptr, diag_slot, engage_buf_ptr, off_w1,
|
||||
)?;
|
||||
launch_adam_step(
|
||||
stream, &self.adam_func, &mut weights.b1, &self.grad_b1,
|
||||
CUR_B1_LEN, &mut self.adam_m_b1, &mut self.adam_v_b1,
|
||||
n_train, step, weight_clamp_max_abs,
|
||||
nan_flags_ptr, diag_slot, engage_buf_ptr, off_b1,
|
||||
)?;
|
||||
launch_adam_step(
|
||||
stream, &self.adam_func, &mut weights.w2, &self.grad_w2,
|
||||
CUR_W2_LEN, &mut self.adam_m_w2, &mut self.adam_v_w2,
|
||||
n_train, step, weight_clamp_max_abs,
|
||||
nan_flags_ptr, diag_slot, engage_buf_ptr, off_w2,
|
||||
)?;
|
||||
launch_adam_step(
|
||||
stream, &self.adam_func, &mut weights.b2, &self.grad_b2,
|
||||
CUR_B2_LEN, &mut self.adam_m_b2, &mut self.adam_v_b2,
|
||||
n_train, step, weight_clamp_max_abs,
|
||||
nan_flags_ptr, diag_slot, engage_buf_ptr, off_b2,
|
||||
)?;
|
||||
|
||||
debug!(step, n_train, "curiosity GPU training step complete (cuBLAS GEMM pipeline)");
|
||||
|
||||
|
||||
@@ -66,6 +66,14 @@ use super::gpu_aux_heads::{
|
||||
use super::gpu_health_diag::GpuHealthDiag;
|
||||
use super::gpu_moe_head::GpuMoeHead;
|
||||
use super::mapped_pinned::{MappedF32Buffer, MappedI32Buffer, MappedU32Buffer, MappedU64Buffer};
|
||||
// SP4 Task A14: shared constants (engagement-counter buffer layout +
|
||||
// per-Adam-kernel diag-slot allocations) used by `new()` constructor +
|
||||
// the 5 Adam launch sites + the host-side Pearl C rate-deficit check.
|
||||
use super::sp4_isv_slots::{
|
||||
SP4_PARAM_GROUP_COUNT, MAX_BLOCKS_PER_ADAM, SP4_ENGAGE_BUF_LEN,
|
||||
SP4_ENGAGE_OFFSET_DISABLED, SP4_NAN_FLAGS_END,
|
||||
SP4_DQN_ADAM_DIAG_SLOT, ParamGroup,
|
||||
};
|
||||
|
||||
// ── Precompiled cubins (build.rs → include_bytes! → ZERO runtime nvcc) ──────
|
||||
pub(crate) static DQN_UTILITY_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/dqn_utility_kernels.cubin"));
|
||||
@@ -2907,6 +2915,27 @@ pub struct GpuDqnTrainer {
|
||||
/// = 2048 ints. Mapped-pinned i32.
|
||||
pub(crate) clamp_engage_per_block_buf: MappedI32Buffer,
|
||||
|
||||
/// SP4 Task A15: Pearl C rate-deficit Wiener-state buffer.
|
||||
/// `SP4_PARAM_GROUP_COUNT × 3 = 24` floats laid out as
|
||||
/// `[sample_var, diff_var, x_lag]` per param group. Tracks the
|
||||
/// engagement-rate-deficit (= engagement_rate - 0.01 theoretical p99
|
||||
/// baseline) via Pearls A+D over time. Mapped-pinned f32 — host-side
|
||||
/// `pearl_c_post_adam_engagement_check` reads/writes via host_ptr.
|
||||
/// Reset to all-zeros at fold boundary via the state-reset registry
|
||||
/// (Task A12). Layer A: this state is computed and tracked but not
|
||||
/// yet consumed — Layer B will read sustained-positive rate-deficit
|
||||
/// to force-bump `ISV[WEIGHT_BOUND[group]]`.
|
||||
pub(crate) pearl_c_rate_deficit_state_buf: MappedF32Buffer,
|
||||
|
||||
/// SP4 Task A15: Pearl C rate-deficit EMA (host-only, one f32 per
|
||||
/// param group). Acts as the `prev_x_mean` for `pearls_ad_update`
|
||||
/// applied to engagement-rate deficits each Adam step. Pearls A+D
|
||||
/// state for those EMAs lives in `pearl_c_rate_deficit_state_buf`
|
||||
/// (mapped pinned). Host-only because rate-deficit reduce + EMA is
|
||||
/// post-Adam diagnostic, never read on device. Reset to all-zeros
|
||||
/// at fold boundary via the state-reset registry (Task A12).
|
||||
pub(crate) pearl_c_rate_deficit_ema: [f32; SP4_PARAM_GROUP_COUNT],
|
||||
|
||||
/// SP4 producer step-observation scratch — each of the 47 producers
|
||||
/// (40 SP4 + 7 retrofit) writes its per-step `step_observation` to
|
||||
/// its dedicated slot here. Host then applies Pearls A+D
|
||||
@@ -6047,6 +6076,12 @@ impl GpuDqnTrainer {
|
||||
// SP3 Mech 9: ISV-driven post-Adam weight clamp (see `launch_adam_update`).
|
||||
let q_abs_ref_eff = self.read_isv_signal_at(Q_ABS_REF_INDEX).max(1.0_f32);
|
||||
let weight_clamp_max_abs: f32 = 100.0_f32 * q_abs_ref_eff;
|
||||
// SP4 Task A14: aux trainer (OFI embed) is outside the SP4 8-group
|
||||
// taxonomy — disable Pearl C engagement counter for this launch.
|
||||
let _aux_nan_flags_null: u64 = 0;
|
||||
let _aux_diag_slot_disabled: i32 = -1;
|
||||
let _aux_engage_buf_null: u64 = 0;
|
||||
let _aux_engage_off_disabled: i32 = SP4_ENGAGE_OFFSET_DISABLED;
|
||||
let blocks = ((OFI_EMBED_TOTAL_PARAMS as u32 + 255) / 256).max(1);
|
||||
unsafe {
|
||||
self.stream
|
||||
@@ -6068,6 +6103,13 @@ impl GpuDqnTrainer {
|
||||
.arg(&l1_end_aux)
|
||||
.arg(&l1_lambda_aux)
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9: post-Adam |p_val| clamp
|
||||
// SP4 Task A14: aux trainers (OFI embed) live outside the
|
||||
// SP4 8-group taxonomy — pass null nan_flags + disabled
|
||||
// engage_buf_offset so the kernel skips per-block writeback.
|
||||
.arg(&_aux_nan_flags_null)
|
||||
.arg(&_aux_diag_slot_disabled)
|
||||
.arg(&_aux_engage_buf_null)
|
||||
.arg(&_aux_engage_off_disabled)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (blocks, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
@@ -12723,9 +12765,12 @@ impl GpuDqnTrainer {
|
||||
// the F1 step-3540 NaN root). SP3 Mech 10 (slot 49): one additional
|
||||
// slot for post-trunk h_s2 activation max-abs sentinel — fires at
|
||||
// 1e3 × H_S2_RMS_EMA.max(1.0) and stays quiet under normal Mech 10
|
||||
// clamp operation (clamp at 100×, diagnostic at 1000×). Buffer size
|
||||
// reviewable by SP2 framework codification.
|
||||
let nan_flags_buf = stream.alloc_zeros::<i32>(50)
|
||||
// clamp operation (clamp at 100×, diagnostic at 1000×). SP4 Task
|
||||
// A14 (slots 50-54): per-Adam-kernel sticky engagement-overflow
|
||||
// flags (one per of dqn/iqn/iql/attn/curiosity Adam kernels).
|
||||
// Sized via SP4_NAN_FLAGS_END = 55. Buffer size reviewable by SP2
|
||||
// framework codification.
|
||||
let nan_flags_buf = stream.alloc_zeros::<i32>(SP4_NAN_FLAGS_END)
|
||||
.map_err(|e| MLError::ModelError(format!("nan_flags alloc: {e}")))?;
|
||||
|
||||
// SP2 + SP3 Mech 5 + Mech 10: fused NaN/threshold-check (ptr, len) tables.
|
||||
@@ -12763,12 +12808,12 @@ impl GpuDqnTrainer {
|
||||
// Per `feedback_no_htod_htoh_only_mapped_pinned`: mapped pinned
|
||||
// (`cuMemHostAlloc(DEVICEMAP|PORTABLE)`) is the only allowed CPU↔GPU
|
||||
// path for these state buffers.
|
||||
const SP4_PARAM_GROUP_COUNT: usize = 8;
|
||||
const SP4_PRODUCER_COUNT: usize = 47; // 40 SP4 + 7 retrofit existing producers
|
||||
const SP4_WIENER_FLOATS_PER_SLOT: usize = 3; // sample_var, diff_var, x_lag
|
||||
const SP4_WIENER_TOTAL_FLOATS: usize = SP4_PRODUCER_COUNT * SP4_WIENER_FLOATS_PER_SLOT; // 141
|
||||
const MAX_BLOCKS_PER_ADAM: usize = 256;
|
||||
const SP4_ENGAGE_BUF_LEN: usize = SP4_PARAM_GROUP_COUNT * MAX_BLOCKS_PER_ADAM; // 2048
|
||||
// SP4 Task A14: SP4_PARAM_GROUP_COUNT, MAX_BLOCKS_PER_ADAM, and
|
||||
// SP4_ENGAGE_BUF_LEN are now public constants in `sp4_isv_slots`
|
||||
// so launch sites + helpers can reference them without re-declaring.
|
||||
|
||||
let wiener_state_buf = unsafe { MappedF32Buffer::new(SP4_WIENER_TOTAL_FLOATS) }
|
||||
.map_err(|e| MLError::ModelError(format!("SP4 wiener_state_buf alloc: {e}")))?;
|
||||
@@ -12779,6 +12824,18 @@ impl GpuDqnTrainer {
|
||||
let producer_step_scratch_buf = unsafe { MappedF32Buffer::new(SP4_PRODUCER_COUNT) }
|
||||
.map_err(|e| MLError::ModelError(format!("SP4 producer_step_scratch_buf alloc: {e}")))?;
|
||||
|
||||
// SP4 Task A15: dedicated Pearl C rate-deficit Wiener-state
|
||||
// buffer (8 groups × 3 floats: sample_var, diff_var, x_lag).
|
||||
// Separate from `wiener_state_buf` to keep SP4-bound producer
|
||||
// state isolated from Pearl C diagnostic state — clean reset
|
||||
// semantics + independent fold-boundary handling.
|
||||
const PEARL_C_RATE_DEFICIT_FLOATS: usize = SP4_PARAM_GROUP_COUNT * SP4_WIENER_FLOATS_PER_SLOT;
|
||||
let pearl_c_rate_deficit_state_buf =
|
||||
unsafe { MappedF32Buffer::new(PEARL_C_RATE_DEFICIT_FLOATS) }
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
"SP4 pearl_c_rate_deficit_state_buf alloc: {e}"
|
||||
)))?;
|
||||
|
||||
// SP4 Task A7 fix-up #2: oracle sub-buffer descriptor tables.
|
||||
// `SP4_ORACLE_TABLE_MAX_SUB = 4` covers Curiosity's 4 sub-buffers
|
||||
// (groups 0-6 use only the first slot). Tables are reused across
|
||||
@@ -14188,6 +14245,8 @@ impl GpuDqnTrainer {
|
||||
nan_flags_buf,
|
||||
wiener_state_buf,
|
||||
clamp_engage_per_block_buf,
|
||||
pearl_c_rate_deficit_state_buf,
|
||||
pearl_c_rate_deficit_ema: [0.0_f32; SP4_PARAM_GROUP_COUNT],
|
||||
producer_step_scratch_buf,
|
||||
oracle_subbuf_table_buf,
|
||||
oracle_subbuf_counts_buf,
|
||||
@@ -18353,6 +18412,12 @@ impl GpuDqnTrainer {
|
||||
// SP3 Mech 9: ISV-driven post-Adam weight clamp (see `launch_adam_update`).
|
||||
let q_abs_ref_eff = self.read_isv_signal_at(Q_ABS_REF_INDEX).max(1.0_f32);
|
||||
let weight_clamp_max_abs: f32 = 100.0_f32 * q_abs_ref_eff;
|
||||
// SP4 Task A14: aux trainer (denoise) is outside the SP4 8-group
|
||||
// taxonomy — Pearl C engagement counter disabled for this launch.
|
||||
let _aux_nan_flags_null: u64 = 0;
|
||||
let _aux_diag_slot_disabled: i32 = -1;
|
||||
let _aux_engage_buf_null: u64 = 0;
|
||||
let _aux_engage_off_disabled: i32 = SP4_ENGAGE_OFFSET_DISABLED;
|
||||
let blocks = ((DENOISE_TOTAL_PARAMS as u32 + 255) / 256).max(1);
|
||||
unsafe {
|
||||
self.stream
|
||||
@@ -18374,6 +18439,11 @@ impl GpuDqnTrainer {
|
||||
.arg(&l1_end_aux)
|
||||
.arg(&l1_lambda_aux)
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9: post-Adam |p_val| clamp
|
||||
// SP4 Task A14: disabled — denoise outside SP4 group taxonomy.
|
||||
.arg(&_aux_nan_flags_null)
|
||||
.arg(&_aux_diag_slot_disabled)
|
||||
.arg(&_aux_engage_buf_null)
|
||||
.arg(&_aux_engage_off_disabled)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (blocks, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
@@ -20895,7 +20965,20 @@ impl GpuDqnTrainer {
|
||||
let weight_clamp_max_abs: f32 = 100.0_f32 * q_abs_ref_eff;
|
||||
|
||||
let count = self.total_params as i32;
|
||||
// SP4 Task A14: cap blocks at MAX_BLOCKS_PER_ADAM (256). Per-block
|
||||
// engagement-counter buffer is sized for that ceiling; running with
|
||||
// more blocks would have the kernel write past the per-group slice.
|
||||
// For DQN main-trainer total_params can exceed 256×256=65 536, so we
|
||||
// assert the bound here. If it ever fires, the buffer needs sizing
|
||||
// up before this Adam launch can grow.
|
||||
let blocks = ((self.total_params + 255) / 256) as u32;
|
||||
debug_assert!(
|
||||
(blocks as usize) <= MAX_BLOCKS_PER_ADAM,
|
||||
"dqn_adam blocks={} exceeds MAX_BLOCKS_PER_ADAM={} — Pearl C \
|
||||
clamp_engage_per_block_buf cannot accommodate; resize buffer \
|
||||
or chunk the launch.",
|
||||
blocks, MAX_BLOCKS_PER_ADAM
|
||||
);
|
||||
let cfg = LaunchConfig {
|
||||
grid_dim: (blocks, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
@@ -20906,6 +20989,17 @@ impl GpuDqnTrainer {
|
||||
let m_ptr = self.ptrs.m_buf;
|
||||
let v_ptr = self.ptrs.v_buf;
|
||||
|
||||
// SP4 Task A14: Pearl C engagement-counter args. The single main
|
||||
// DQN Adam launch covers param-groups 0/1/2 (trunk/value/branches);
|
||||
// we account engagement under group 0 (DqnTrunk) for Layer A — see
|
||||
// `pearl_c_post_adam_engagement_check` doc-comment for the
|
||||
// sub-launch deferral rationale.
|
||||
let nan_flags_ptr = self.nan_flags_buf.raw_ptr();
|
||||
let diag_slot: i32 = SP4_DQN_ADAM_DIAG_SLOT;
|
||||
let engage_buf_ptr: u64 = self.clamp_engage_per_block_buf.dev_ptr;
|
||||
let engage_buf_offset: i32 =
|
||||
(ParamGroup::DqnTrunk.idx() * MAX_BLOCKS_PER_ADAM) as i32;
|
||||
|
||||
// Safety: argument order matches the extern "C" kernel signature exactly.
|
||||
// grad_norm_buf contains L2 norm from launch_grad_norm_finalize().
|
||||
unsafe {
|
||||
@@ -20928,6 +21022,10 @@ impl GpuDqnTrainer {
|
||||
.arg(&l1_end) // G8: L1 boundary (end of w_s1)
|
||||
.arg(&l1_lambda) // G8: L1 penalty strength
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9: post-Adam |p_val| clamp
|
||||
.arg(&nan_flags_ptr) // SP4 A14: sticky engage flag store
|
||||
.arg(&diag_slot) // SP4 A14: diag slot (50 for dqn_adam)
|
||||
.arg(&engage_buf_ptr) // SP4 A14: per-block engagement buf
|
||||
.arg(&engage_buf_offset) // SP4 A14: group offset (0 = DqnTrunk)
|
||||
.launch(cfg)
|
||||
.map_err(|e| {
|
||||
MLError::ModelError(format!("dqn_adam_update_kernel launch: {e}"))
|
||||
@@ -20937,6 +21035,141 @@ impl GpuDqnTrainer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SP4 Task A15: host-side Pearl C engagement-rate-deficit check.
|
||||
///
|
||||
/// Reads per-block engagement counts from
|
||||
/// `clamp_engage_per_block_buf` (mapped pinned, zero-copy host
|
||||
/// access), reduces to a single rate, computes the deficit relative
|
||||
/// to the theoretical p99 baseline (1%), then applies Pearls A + D
|
||||
/// (`pearls_ad_update`) to maintain the rate-deficit EMA per param
|
||||
/// group. For Layer A this method only updates the diagnostic EMA
|
||||
/// and emits a `tracing::debug!` line when the EMA exceeds 0.005
|
||||
/// (sustained over-engagement). Layer B will read the EMA and
|
||||
/// force-bump `ISV[WEIGHT_BOUND[group]]`.
|
||||
///
|
||||
/// Curiosity reads four sub-launch ranges
|
||||
/// (`SP4_ENGAGE_OFFSET_CURIOSITY_{W1,B1,W2,B2}`) so the four
|
||||
/// internal Adam updates' counts are summed into a single group
|
||||
/// engagement rate. All other groups read the single contiguous
|
||||
/// `[group_idx × MAX_BLOCKS_PER_ADAM, +MAX_BLOCKS_PER_ADAM)` range.
|
||||
///
|
||||
/// **Layer A limitation #3** (groups 1/2 accounting): the main DQN
|
||||
/// Adam covers param groups 0 (Trunk), 1 (Value), and 2 (Branches)
|
||||
/// in a single fused launch — engagement is recorded under group 0
|
||||
/// only. Layer B will split per-group sub-launches.
|
||||
pub(crate) fn pearl_c_post_adam_engagement_check(
|
||||
&mut self,
|
||||
group: ParamGroup,
|
||||
param_count: usize,
|
||||
) -> Result<(), MLError> {
|
||||
use super::sp4_wiener_ema::{pearls_ad_update, WienerState};
|
||||
|
||||
if param_count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let group_idx = group.idx();
|
||||
// Curiosity sums 4 sub-launch ranges (see launch_adam_step in
|
||||
// gpu_curiosity_trainer.rs). All other groups read a single
|
||||
// contiguous range starting at `group_idx × MAX_BLOCKS_PER_ADAM`.
|
||||
let block_offsets: &[usize] = match group {
|
||||
ParamGroup::Curiosity => &[
|
||||
7 * MAX_BLOCKS_PER_ADAM, // W1
|
||||
8 * MAX_BLOCKS_PER_ADAM, // b1
|
||||
9 * MAX_BLOCKS_PER_ADAM, // W2
|
||||
10 * MAX_BLOCKS_PER_ADAM, // b2
|
||||
],
|
||||
_ => &[
|
||||
/* group_idx * MAX_BLOCKS_PER_ADAM — fall back to
|
||||
* runtime computation in the loop body. */
|
||||
0
|
||||
],
|
||||
};
|
||||
|
||||
// Reduce per-block counts → total engagement count.
|
||||
let mut total_engage: i64 = 0;
|
||||
// Safety: `clamp_engage_per_block_buf.host_ptr` is a mapped-pinned
|
||||
// i32 buffer of `SP4_ENGAGE_BUF_LEN` elements — valid for the
|
||||
// lifetime of the trainer. Reads are zero-copy via the device-
|
||||
// mapped page; the kernel writes were ordered by Adam-launch
|
||||
// synchronization upstream of this call.
|
||||
unsafe {
|
||||
let p = self.clamp_engage_per_block_buf.host_ptr as *const i32;
|
||||
if matches!(group, ParamGroup::Curiosity) {
|
||||
for &base in block_offsets {
|
||||
for b in 0..MAX_BLOCKS_PER_ADAM {
|
||||
total_engage += *p.add(base + b) as i64;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let base = group_idx * MAX_BLOCKS_PER_ADAM;
|
||||
for b in 0..MAX_BLOCKS_PER_ADAM {
|
||||
total_engage += *p.add(base + b) as i64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let engagement_rate = (total_engage as f32) / (param_count as f32);
|
||||
let rate_deficit = engagement_rate - 0.01_f32;
|
||||
|
||||
// Apply Pearls A + D to the rate-deficit time series. The EMA
|
||||
// surrogate (`prev_x_mean`) lives in `pearl_c_rate_deficit_ema`
|
||||
// (host-only), and the (sample_var, diff_var, x_lag) Wiener
|
||||
// triple lives in `pearl_c_rate_deficit_state_buf` at offset
|
||||
// `group_idx * 3`.
|
||||
let prev_x_mean = self.pearl_c_rate_deficit_ema[group_idx];
|
||||
let mut state = unsafe {
|
||||
let p = self.pearl_c_rate_deficit_state_buf.host_ptr as *const f32;
|
||||
let off = group_idx * 3;
|
||||
WienerState {
|
||||
sample_var: *p.add(off),
|
||||
diff_var: *p.add(off + 1),
|
||||
x_lag: *p.add(off + 2),
|
||||
}
|
||||
};
|
||||
let new_x_mean = pearls_ad_update(prev_x_mean, &mut state, rate_deficit);
|
||||
self.pearl_c_rate_deficit_ema[group_idx] = new_x_mean;
|
||||
unsafe {
|
||||
let p = self.pearl_c_rate_deficit_state_buf.host_ptr as *mut f32;
|
||||
let off = group_idx * 3;
|
||||
*p.add(off) = state.sample_var;
|
||||
*p.add(off + 1) = state.diff_var;
|
||||
*p.add(off + 2) = state.x_lag;
|
||||
}
|
||||
|
||||
// Layer A: log only on sustained over-engagement. Layer B will
|
||||
// mutate `ISV[WEIGHT_BOUND[group]]` instead.
|
||||
if new_x_mean > 0.005_f32 {
|
||||
tracing::debug!(
|
||||
target: "sp4.pearl_c",
|
||||
group = ?group,
|
||||
param_count,
|
||||
engagement_rate,
|
||||
rate_deficit,
|
||||
rate_deficit_ema = new_x_mean,
|
||||
"SP4 Pearl C: sustained engagement-rate deficit \
|
||||
(>0.005 EMA) — Layer A diagnostic; Layer B will \
|
||||
force-bump WEIGHT_BOUND."
|
||||
);
|
||||
}
|
||||
|
||||
// Zero per-block counts post-read so next step starts clean.
|
||||
// Curiosity must zero all 4 sub-ranges; others zero one range.
|
||||
unsafe {
|
||||
let p = self.clamp_engage_per_block_buf.host_ptr as *mut i32;
|
||||
if matches!(group, ParamGroup::Curiosity) {
|
||||
for &base in block_offsets {
|
||||
std::ptr::write_bytes(p.add(base), 0, MAX_BLOCKS_PER_ADAM);
|
||||
}
|
||||
} else {
|
||||
let base = group_idx * MAX_BLOCKS_PER_ADAM;
|
||||
std::ptr::write_bytes(p.add(base), 0, MAX_BLOCKS_PER_ADAM);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Weight flattening / unflattening (GPU d2d — zero host roundtrip)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -21941,14 +22174,18 @@ impl GpuDqnTrainer {
|
||||
/// SP4 Layer A Task A12 (2026-04-30): bulk-zero the Pearl C
|
||||
/// engagement-counter buffer at fold boundary.
|
||||
///
|
||||
/// `clamp_engage_per_block_buf` is a 2048-int mapped-pinned buffer
|
||||
/// (`SP4_PARAM_GROUP_COUNT × MAX_BLOCKS_PER_ADAM = 8 × 256`) counting
|
||||
/// post-Adam clamp triggers per (param-group, Adam-launch-block). Host
|
||||
/// reduces across blocks → engagement_rate; rate-deficit Wiener EMA
|
||||
/// detects post-clamp feedback-loop saturation. At fold boundary all
|
||||
/// counters reset to 0 so the new fold's engagement-rate tracking
|
||||
/// restarts from a clean baseline (no inherited fold-N saturation
|
||||
/// state biasing the new fold's first per-step rate readings).
|
||||
/// `clamp_engage_per_block_buf` is a 2816-int mapped-pinned buffer
|
||||
/// (`(SP4_PARAM_GROUP_COUNT + 3 curiosity sub-launches) ×
|
||||
/// MAX_BLOCKS_PER_ADAM = 11 × 256`) counting post-Adam clamp
|
||||
/// triggers per (param-group, Adam-launch-block). Host reduces
|
||||
/// across blocks → engagement_rate; rate-deficit Wiener EMA detects
|
||||
/// post-clamp feedback-loop saturation. The buffer length grew
|
||||
/// 2048→2816 in Task A14/A15 to give Curiosity's 4 sub-launches
|
||||
/// (W1/b1/W2/b2) distinct offset slots avoiding per-block index
|
||||
/// collisions. At fold boundary all counters reset to 0 so the new
|
||||
/// fold's engagement-rate tracking restarts from a clean baseline
|
||||
/// (no inherited fold-N saturation state biasing the new fold's
|
||||
/// first per-step rate readings).
|
||||
///
|
||||
/// Constructor zero-init via `MappedI32Buffer::new` is the cold-start;
|
||||
/// this method re-applies the same byte-zero at every subsequent fold
|
||||
@@ -21957,7 +22194,7 @@ impl GpuDqnTrainer {
|
||||
// Safety: `host_ptr` is a mapped-pinned `*mut i32` of length
|
||||
// `self.clamp_engage_per_block_buf.len` returned by `cuMemHostAlloc`.
|
||||
// i32 zero == bytewise-zero, so `write_bytes(.., 0, ..)` produces
|
||||
// 2048 valid i32 zeros.
|
||||
// SP4_ENGAGE_BUF_LEN=2816 valid i32 zeros.
|
||||
unsafe {
|
||||
std::ptr::write_bytes(
|
||||
self.clamp_engage_per_block_buf.host_ptr as *mut u8,
|
||||
@@ -21967,6 +22204,50 @@ impl GpuDqnTrainer {
|
||||
}
|
||||
}
|
||||
|
||||
/// SP4 Task A15: bulk-zero the Pearl C rate-deficit Wiener-state
|
||||
/// buffer at fold boundary.
|
||||
///
|
||||
/// `pearl_c_rate_deficit_state_buf` is a 24-float mapped-pinned
|
||||
/// buffer = `SP4_PARAM_GROUP_COUNT × {sample_var, diff_var, x_lag}`.
|
||||
/// Reset to all-zeros so Pearl A's first-observation sentinel fires
|
||||
/// on the new fold's first engagement-rate-deficit observation
|
||||
/// (`prev_x_mean == 0 AND state.x_lag == 0` → first-observation
|
||||
/// replacement). Companion to `reset_sp4_pearl_c_rate_deficit_ema`
|
||||
/// — both halves of Pearl A's sentinel contract reset in lockstep.
|
||||
pub fn reset_sp4_pearl_c_rate_deficit_state(&mut self) {
|
||||
unsafe {
|
||||
std::ptr::write_bytes(
|
||||
self.pearl_c_rate_deficit_state_buf.host_ptr as *mut u8,
|
||||
0u8,
|
||||
self.pearl_c_rate_deficit_state_buf.len * std::mem::size_of::<f32>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// SP4 Task A15: zero the host-only Pearl C rate-deficit EMA at
|
||||
/// fold boundary. Companion to
|
||||
/// `reset_sp4_pearl_c_rate_deficit_state` — both halves of Pearl
|
||||
/// A's sentinel contract reset together.
|
||||
pub fn reset_sp4_pearl_c_rate_deficit_ema(&mut self) {
|
||||
self.pearl_c_rate_deficit_ema = [0.0_f32; SP4_PARAM_GROUP_COUNT];
|
||||
}
|
||||
|
||||
/// SP4 Task A14: device pointer to the trainer's `nan_flags_buf`
|
||||
/// (mapped via `CudaSlice::raw_ptr()`). Used by aux trainers (IQN,
|
||||
/// IQL hi/lo, Attn, Curiosity) to share the Pearl C sticky-flag
|
||||
/// store via `set_pearl_c_buffers`.
|
||||
pub fn nan_flags_buf_ptr(&self) -> u64 {
|
||||
self.nan_flags_buf.raw_ptr()
|
||||
}
|
||||
|
||||
/// SP4 Task A14: device pointer to the trainer's
|
||||
/// `clamp_engage_per_block_buf` (mapped pinned). Used by aux
|
||||
/// trainers to share the Pearl C per-block engagement counter
|
||||
/// buffer via `set_pearl_c_buffers`.
|
||||
pub fn clamp_engage_per_block_buf_dev_ptr(&self) -> u64 {
|
||||
self.clamp_engage_per_block_buf.dev_ptr
|
||||
}
|
||||
|
||||
/// Read-only accessor for diagnostics / HEALTH_DIAG mirror.
|
||||
pub fn grad_norm_fast_ema_value(&self) -> f32 { unsafe { *self.grad_norm_fast_ema_pinned } }
|
||||
/// Read-only accessor for diagnostics / HEALTH_DIAG mirror.
|
||||
@@ -22309,6 +22590,12 @@ impl GpuDqnTrainer {
|
||||
// SP3 Mech 9: ISV-driven post-Adam weight clamp (see `launch_adam_update`).
|
||||
let q_abs_ref_eff = self.read_isv_signal_at(Q_ABS_REF_INDEX).max(1.0_f32);
|
||||
let weight_clamp_max_abs: f32 = 100.0_f32 * q_abs_ref_eff;
|
||||
// SP4 Task A14: aux trainer (sel/recursive_conf) is outside the SP4
|
||||
// 8-group taxonomy — Pearl C engagement counter disabled.
|
||||
let _aux_nan_flags_null: u64 = 0;
|
||||
let _aux_diag_slot_disabled: i32 = -1;
|
||||
let _aux_engage_buf_null: u64 = 0;
|
||||
let _aux_engage_off_disabled: i32 = SP4_ENGAGE_OFFSET_DISABLED;
|
||||
let blocks = ((sel_dim as u32 + 255) / 256).max(1);
|
||||
unsafe {
|
||||
self.stream
|
||||
@@ -22330,6 +22617,11 @@ impl GpuDqnTrainer {
|
||||
.arg(&l1_end_aux)
|
||||
.arg(&l1_lambda_aux)
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9: post-Adam |p_val| clamp
|
||||
// SP4 Task A14: disabled — sel/recursive_conf outside SP4 group taxonomy.
|
||||
.arg(&_aux_nan_flags_null)
|
||||
.arg(&_aux_diag_slot_disabled)
|
||||
.arg(&_aux_engage_buf_null)
|
||||
.arg(&_aux_engage_off_disabled)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (blocks, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
|
||||
@@ -4281,6 +4281,16 @@ impl GpuExperienceCollector {
|
||||
self.curiosity_trainer.as_ref()
|
||||
}
|
||||
|
||||
/// SP4 Task A14: thread the main DQN trainer's mapped-pinned Pearl C
|
||||
/// buffers (`nan_flags_buf`, `clamp_engage_per_block_buf`) into the
|
||||
/// curiosity trainer. Called once post-construction by
|
||||
/// `FusedTrainingCtx`. No-op when curiosity is disabled.
|
||||
pub fn set_curiosity_pearl_c_buffers(&mut self, nan_flags: u64, engage_buf: u64) {
|
||||
if let Some(ref mut trainer) = self.curiosity_trainer {
|
||||
trainer.set_pearl_c_buffers(nan_flags, engage_buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable curiosity training (e.g. after an async kernel crash).
|
||||
pub fn disable_curiosity(&mut self) {
|
||||
self.curiosity_trainer = None;
|
||||
|
||||
@@ -257,6 +257,26 @@ pub struct GpuIqlTrainer {
|
||||
grad_norm_blocks: usize,
|
||||
#[allow(dead_code)]
|
||||
state_dim_padded: usize,
|
||||
|
||||
/// SP4 Task A14: Pearl C engagement-counter buffer pointers, shared
|
||||
/// from the main DQN trainer's mapped-pinned buffers via
|
||||
/// `set_pearl_c_buffers`. The IQL Adam writes
|
||||
/// `clamp_engage_per_block_buf[engage_buf_offset + blockIdx.x]` and
|
||||
/// `nan_flags_buf[52]` (SP4_IQL_ADAM_DIAG_SLOT). High- and low-tau
|
||||
/// IQL trainers share the same diag slot but use distinct
|
||||
/// `engage_buf_offset` values (high=4 × MAX_BLOCKS, low=5 × MAX_BLOCKS)
|
||||
/// — the offset is supplied as a constructor arg via `param_group`.
|
||||
/// Both pointers default to 0 → kernel skips the write.
|
||||
pearl_c_nan_flags_ptr: u64,
|
||||
pearl_c_engage_buf_ptr: u64,
|
||||
|
||||
/// SP4 Task A14: which `ParamGroup` this IQL trainer's params live in.
|
||||
/// `IqlHigh` (= group 4) for the standard high-tau trainer,
|
||||
/// `IqlLow` (= group 5) for the low-tau companion. Used to compute
|
||||
/// `engage_buf_offset = group.idx() * MAX_BLOCKS_PER_ADAM` at launch.
|
||||
/// Defaults to `IqlHigh` (smoke tests / standalone runs); set via
|
||||
/// `set_pearl_c_param_group` after construction.
|
||||
pearl_c_param_group: crate::cuda_pipeline::ParamGroup,
|
||||
}
|
||||
|
||||
impl GpuIqlTrainer {
|
||||
@@ -508,9 +528,45 @@ impl GpuIqlTrainer {
|
||||
total_params,
|
||||
grad_norm_blocks,
|
||||
state_dim_padded,
|
||||
// SP4 Task A14: Pearl C disabled until `set_pearl_c_buffers` is
|
||||
// called by FusedTrainingCtx. Default group `IqlHigh`; the low-
|
||||
// tau trainer overrides via `set_pearl_c_param_group`.
|
||||
pearl_c_nan_flags_ptr: 0,
|
||||
pearl_c_engage_buf_ptr: 0,
|
||||
pearl_c_param_group: crate::cuda_pipeline::ParamGroup::IqlHigh,
|
||||
})
|
||||
}
|
||||
|
||||
/// SP4 Task A14: thread the main DQN trainer's mapped-pinned Pearl C
|
||||
/// buffers down to the IQL Adam launch. Pass 0 for either pointer to
|
||||
/// disable Pearl C (smoke tests / standalone runs).
|
||||
pub fn set_pearl_c_buffers(&mut self, nan_flags: u64, engage_buf: u64) {
|
||||
self.pearl_c_nan_flags_ptr = nan_flags;
|
||||
self.pearl_c_engage_buf_ptr = engage_buf;
|
||||
}
|
||||
|
||||
/// SP4 Task A15: parameter count accessor for the host-side Pearl C
|
||||
/// engagement-rate check. Returns the total V(s) MLP parameter count.
|
||||
pub fn total_params(&self) -> usize {
|
||||
self.total_params
|
||||
}
|
||||
|
||||
/// SP4 Task A14: declare which `ParamGroup` this IQL trainer's params
|
||||
/// live in. `IqlHigh` (= 4) for the standard high-tau trainer, `IqlLow`
|
||||
/// (= 5) for the companion low-tau trainer. Drives `engage_buf_offset
|
||||
/// = group.idx() × MAX_BLOCKS_PER_ADAM` at launch.
|
||||
pub fn set_pearl_c_param_group(&mut self, group: crate::cuda_pipeline::ParamGroup) {
|
||||
debug_assert!(
|
||||
matches!(
|
||||
group,
|
||||
crate::cuda_pipeline::ParamGroup::IqlHigh
|
||||
| crate::cuda_pipeline::ParamGroup::IqlLow
|
||||
),
|
||||
"IQL Pearl C param-group must be IqlHigh or IqlLow"
|
||||
);
|
||||
self.pearl_c_param_group = group;
|
||||
}
|
||||
|
||||
/// Run one IQL value network training step — fully deterministic.
|
||||
///
|
||||
/// Pipeline: 3 forward GEMMs + SiLU + bias_add + expectile_loss
|
||||
@@ -983,6 +1039,29 @@ impl GpuIqlTrainer {
|
||||
let mgn = self.config.max_grad_norm;
|
||||
let t_ptr = self.t_dev_ptr;
|
||||
|
||||
// SP4 Task A14: Pearl C engagement-counter args. If
|
||||
// `set_pearl_c_buffers` was called by FusedTrainingCtx wiring,
|
||||
// the kernel records per-block engagement counts at offset
|
||||
// `pearl_c_param_group * MAX_BLOCKS_PER_ADAM` (IqlHigh=4 →
|
||||
// offset 1024, IqlLow=5 → offset 1280). When the host pointer
|
||||
// is unset (pre-wire-up or aux test paths), pass null +
|
||||
// SP4_ENGAGE_OFFSET_DISABLED so the kernel skips writeback.
|
||||
let pearl_c_active = self.pearl_c_engage_buf_ptr != 0
|
||||
&& self.pearl_c_nan_flags_ptr != 0;
|
||||
let nan_flags_ptr: u64 = self.pearl_c_nan_flags_ptr;
|
||||
let diag_slot: i32 = if pearl_c_active {
|
||||
crate::cuda_pipeline::SP4_IQL_ADAM_DIAG_SLOT
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
let engage_buf_ptr: u64 = self.pearl_c_engage_buf_ptr;
|
||||
let engage_buf_offset: i32 = if pearl_c_active {
|
||||
(self.pearl_c_param_group.idx() as i32)
|
||||
* (crate::cuda_pipeline::MAX_BLOCKS_PER_ADAM as i32)
|
||||
} else {
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_DISABLED
|
||||
};
|
||||
|
||||
unsafe {
|
||||
self.stream
|
||||
.launch_builder(&self.adam_kernel)
|
||||
@@ -1000,6 +1079,10 @@ impl GpuIqlTrainer {
|
||||
.arg(&t_ptr)
|
||||
.arg(&total_params_i32)
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9 (close-out v2): post-Adam |p| clamp
|
||||
.arg(&nan_flags_ptr) // SP4 A14: sticky engage flag store
|
||||
.arg(&diag_slot) // SP4 A14: diag slot (52 = IQL)
|
||||
.arg(&engage_buf_ptr) // SP4 A14: per-block engage buf
|
||||
.arg(&engage_buf_offset) // SP4 A14: group offset (4 or 5)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (adam_blocks as u32, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
|
||||
@@ -331,6 +331,17 @@ pub struct GpuIqnHead {
|
||||
/// Pre-computed target h_s2 from DQN Pass 2 (avoids redundant trunk forward).
|
||||
/// Set via `set_cached_target_h_s2()`, consumed (taken) by `execute_training_pipeline`.
|
||||
cached_target_h_s2_ptr: Option<u64>,
|
||||
|
||||
/// SP4 Task A14: Pearl C engagement-counter buffer pointers, shared
|
||||
/// from the main DQN trainer's mapped-pinned buffers via
|
||||
/// `set_pearl_c_buffers`. `nan_flags_buf` is the shared `[i32; 55]`
|
||||
/// sticky-flag store; the IQN Adam writes `[51]=1` on overflow.
|
||||
/// `clamp_engage_per_block_buf` is the shared `[i32; 2048]` per-block
|
||||
/// counter buffer indexed by `engage_buf_offset = ParamGroup::Iqn.idx()
|
||||
/// × MAX_BLOCKS_PER_ADAM`. Both default to 0 → kernel skips the write
|
||||
/// (compatible with smoke tests / standalone `train_iqn_step_gpu`).
|
||||
pearl_c_nan_flags_ptr: u64,
|
||||
pearl_c_engage_buf_ptr: u64,
|
||||
/// Persistent rewards buffer [B] f32
|
||||
rewards_buf: CudaSlice<f32>,
|
||||
/// Persistent dones buffer [B] f32
|
||||
@@ -674,6 +685,8 @@ impl GpuIqnHead {
|
||||
branch_actions,
|
||||
target_h_s2,
|
||||
cached_target_h_s2_ptr: None,
|
||||
pearl_c_nan_flags_ptr: 0,
|
||||
pearl_c_engage_buf_ptr: 0,
|
||||
rewards_buf,
|
||||
dones_buf,
|
||||
save_q_online,
|
||||
@@ -830,6 +843,17 @@ impl GpuIqnHead {
|
||||
self.cached_target_h_s2_ptr = Some(ptr);
|
||||
}
|
||||
|
||||
/// SP4 Task A14: thread the main DQN trainer's mapped-pinned Pearl C
|
||||
/// buffers down to the IQN Adam launch. `nan_flags` is the shared
|
||||
/// `[i32; SP4_NAN_FLAGS_END]` sticky-flag buffer; `engage_buf` is the
|
||||
/// shared `[i32; SP4_ENGAGE_BUF_LEN]` per-block engagement-count buffer.
|
||||
/// Pass 0 for either pointer to disable Pearl C for this IQN trainer
|
||||
/// (smoke tests / standalone runs).
|
||||
pub fn set_pearl_c_buffers(&mut self, nan_flags: u64, engage_buf: u64) {
|
||||
self.pearl_c_nan_flags_ptr = nan_flags;
|
||||
self.pearl_c_engage_buf_ptr = engage_buf;
|
||||
}
|
||||
|
||||
/// Core IQN training pipeline: trunk forward → cuBLAS forward → loss → backward → Adam.
|
||||
///
|
||||
/// Assumes `branch_actions`, `rewards_buf`, `dones_buf` are already populated.
|
||||
@@ -1475,6 +1499,21 @@ impl GpuIqnHead {
|
||||
let mgn = self.config.max_grad_norm;
|
||||
let t_ptr = self.t_dev_ptr;
|
||||
|
||||
// SP4 Task A14: Pearl C engagement-counter args. IQN params live in
|
||||
// ParamGroup::Iqn (= group 3); offset = 3 × MAX_BLOCKS_PER_ADAM. The
|
||||
// caller (`FusedTrainingCtx`) plumbs through `nan_flags_dev_ptr` +
|
||||
// `clamp_engage_dev_ptr` from the main DQN trainer's mapped-pinned
|
||||
// buffers via `set_pearl_c_buffers`.
|
||||
let nan_flags_ptr = self.pearl_c_nan_flags_ptr;
|
||||
let diag_slot: i32 = crate::cuda_pipeline::SP4_IQN_ADAM_DIAG_SLOT;
|
||||
let engage_buf_ptr = self.pearl_c_engage_buf_ptr;
|
||||
let engage_buf_offset: i32 = if engage_buf_ptr == 0 {
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_DISABLED
|
||||
} else {
|
||||
(crate::cuda_pipeline::ParamGroup::Iqn.idx()
|
||||
* crate::cuda_pipeline::MAX_BLOCKS_PER_ADAM) as i32
|
||||
};
|
||||
|
||||
unsafe {
|
||||
effective_stream
|
||||
.launch_builder(&self.adam_kernel)
|
||||
@@ -1499,6 +1538,10 @@ impl GpuIqnHead {
|
||||
.arg(&b2_i32)
|
||||
.arg(&b3_i32)
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9 (close-out v2): post-Adam |p| clamp
|
||||
.arg(&nan_flags_ptr) // SP4 A14: Pearl C sticky engage flag
|
||||
.arg(&diag_slot) // SP4 A14: nan_flags_buf slot
|
||||
.arg(&engage_buf_ptr) // SP4 A14: per-block engage buffer
|
||||
.arg(&engage_buf_offset) // SP4 A14: group offset (Iqn=3)
|
||||
.launch(adam_config)
|
||||
.map_err(|e| MLError::ModelError(format!("IQN adam kernel: {e}")))?;
|
||||
}
|
||||
@@ -1555,6 +1598,13 @@ impl GpuIqnHead {
|
||||
self.total_params
|
||||
}
|
||||
|
||||
/// SP4 Task A15: parameter count accessor for the host-side Pearl C
|
||||
/// engagement-rate check. Returns `config.total_params()` cached at
|
||||
/// construction.
|
||||
pub fn total_params(&self) -> usize {
|
||||
self.total_params
|
||||
}
|
||||
|
||||
/// Test-only borrow of the owning CUDA stream — the same stream every
|
||||
/// async DtoD copy on this head is queued against. The test issues its
|
||||
/// fills and read-backs on this stream, then synchronises before
|
||||
|
||||
@@ -697,6 +697,17 @@ impl GpuTlob {
|
||||
let eps = 1e-8_f32;
|
||||
let wd = 1e-5_f32;
|
||||
let t_ptr = self.t_dev_ptr;
|
||||
// SP4 Task A14: Pearl C engagement-counter args. TLOB shares
|
||||
// the `attn_adam_kernel` cubin with `GpuAttention`. For Layer A
|
||||
// we silently disable Pearl C bookkeeping on this trainer:
|
||||
// null pointer + SP4_ENGAGE_OFFSET_DISABLED. Layer B can split
|
||||
// the kernel ownership and add per-trainer engagement slots.
|
||||
let nan_flags_ptr_null: u64 = 0;
|
||||
let diag_slot_disabled: i32 = -1;
|
||||
let engage_buf_ptr_null: u64 = 0;
|
||||
let engage_off_disabled: i32 =
|
||||
crate::cuda_pipeline::SP4_ENGAGE_OFFSET_DISABLED;
|
||||
|
||||
unsafe {
|
||||
self.stream
|
||||
.launch_builder(&self.adam_kernel)
|
||||
@@ -714,6 +725,13 @@ impl GpuTlob {
|
||||
.arg(&t_ptr)
|
||||
.arg(&tp_i32)
|
||||
.arg(&weight_clamp_max_abs) // SP3 Mech 9 (close-out v2): post-Adam |p| clamp
|
||||
// SP4 A14: TLOB shares attn_adam_kernel with GpuAttention;
|
||||
// pass null/disabled to skip per-block engagement writeback
|
||||
// (Layer A — Layer B can split the kernel if needed).
|
||||
.arg(&nan_flags_ptr_null)
|
||||
.arg(&diag_slot_disabled)
|
||||
.arg(&engage_buf_ptr_null)
|
||||
.arg(&engage_off_disabled)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (adam_blocks as u32, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
|
||||
@@ -203,44 +203,83 @@ void iql_adam_kernel(
|
||||
int total_params,
|
||||
/* SP3 Mech 9 (close-out v2): ISV-driven |p| bound (100 × Q_ABS_REF.max(1.0))
|
||||
* — same pattern wired into all five Adam kernels. 0=disabled (debug only). */
|
||||
float weight_clamp_max_abs
|
||||
float weight_clamp_max_abs,
|
||||
/* SP4 Task A14 (2026-04-30): Pearl C engagement counter. Same warp+
|
||||
* block tree-reduce pattern as `dqn_adam_update_kernel`. Caller
|
||||
* passes `engage_buf_offset = group_idx × MAX_BLOCKS_PER_ADAM` (group
|
||||
* = ParamGroup::IqlHigh or IqlLow depending on which IQL trainer
|
||||
* launched). `engage_buf_offset < 0` skips the writeback. */
|
||||
int* __restrict__ nan_flags_buf,
|
||||
int diag_slot,
|
||||
int* __restrict__ clamp_engage_per_block_buf,
|
||||
int engage_buf_offset
|
||||
)
|
||||
{
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= total_params) return;
|
||||
bool active = (i < total_params);
|
||||
int local_engage = 0;
|
||||
|
||||
/* Gradient clipping from sum-of-squares */
|
||||
float gn = sqrtf(grad_norm_sq_buf[0]);
|
||||
float clip_scale = (gn > max_grad_norm && gn > 0.0f) ? (max_grad_norm / gn) : 1.0f;
|
||||
float g = grads[i] * clip_scale;
|
||||
if (active) {
|
||||
/* Gradient clipping from sum-of-squares */
|
||||
float gn = sqrtf(grad_norm_sq_buf[0]);
|
||||
float clip_scale = (gn > max_grad_norm && gn > 0.0f) ? (max_grad_norm / gn) : 1.0f;
|
||||
float g = grads[i] * clip_scale;
|
||||
|
||||
/* AdamW: decoupled weight decay */
|
||||
float p = params[i];
|
||||
p -= lr * weight_decay * p;
|
||||
/* AdamW: decoupled weight decay */
|
||||
float p = params[i];
|
||||
p -= lr * weight_decay * p;
|
||||
|
||||
/* Adam moment updates */
|
||||
float m_i = beta1 * m[i] + (1.0f - beta1) * g;
|
||||
float v_i = beta2 * v[i] + (1.0f - beta2) * g * g;
|
||||
/* Adam moment updates */
|
||||
float m_i = beta1 * m[i] + (1.0f - beta1) * g;
|
||||
float v_i = beta2 * v[i] + (1.0f - beta2) * g * g;
|
||||
|
||||
/* Bias correction */
|
||||
int t = t_buf[0];
|
||||
float m_hat = m_i / (1.0f - powf(beta1, (float)t));
|
||||
float v_hat = v_i / (1.0f - powf(beta2, (float)t));
|
||||
/* Bias correction */
|
||||
int t = t_buf[0];
|
||||
float m_hat = m_i / (1.0f - powf(beta1, (float)t));
|
||||
float v_hat = v_i / (1.0f - powf(beta2, (float)t));
|
||||
|
||||
/* Parameter update */
|
||||
p -= lr * m_hat / (sqrtf(v_hat) + eps);
|
||||
/* Parameter update */
|
||||
p -= lr * m_hat / (sqrtf(v_hat) + eps);
|
||||
|
||||
/* SP3 Mech 9: post-Adam |p| clamp. Last step before writeback —
|
||||
* applies after all decay/decoupled updates so the bound holds for
|
||||
* the value the next forward sees. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
p = fminf(fmaxf(p, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
/* SP3 Mech 9 + SP4 Task A14: post-Adam |p| clamp + engagement
|
||||
* counter. Last step before writeback — applies after all
|
||||
* decay/decoupled updates so the bound holds for the value the
|
||||
* next forward sees. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
if (fabsf(p) > weight_clamp_max_abs) {
|
||||
if (nan_flags_buf != nullptr && diag_slot >= 0) {
|
||||
nan_flags_buf[diag_slot] = 1;
|
||||
}
|
||||
local_engage = 1;
|
||||
}
|
||||
p = fminf(fmaxf(p, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
}
|
||||
|
||||
params[i] = p;
|
||||
m[i] = m_i;
|
||||
v[i] = v_i;
|
||||
grads[i] = 0.0f; /* zero gradient for next step */
|
||||
}
|
||||
|
||||
params[i] = p;
|
||||
m[i] = m_i;
|
||||
v[i] = v_i;
|
||||
grads[i] = 0.0f; /* zero gradient for next step */
|
||||
/* SP4 Task A14: warp + block tree-reduce of `local_engage`. */
|
||||
int e = local_engage;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
e += __shfl_xor_sync(0xFFFFFFFF, e, offset);
|
||||
|
||||
__shared__ int warp_engage[8]; /* blockDim.x = 256 ⇒ 8 warps */
|
||||
int warp_id = threadIdx.x / 32;
|
||||
int warp_lane = threadIdx.x % 32;
|
||||
if (warp_lane == 0) warp_engage[warp_id] = e;
|
||||
__syncthreads();
|
||||
|
||||
if (warp_id == 0) {
|
||||
int val = (warp_lane < blockDim.x / 32) ? warp_engage[warp_lane] : 0;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
val += __shfl_xor_sync(0xFFFFFFFF, val, offset);
|
||||
if (warp_lane == 0 && engage_buf_offset >= 0 && clamp_engage_per_block_buf != nullptr) {
|
||||
clamp_engage_per_block_buf[engage_buf_offset + blockIdx.x] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
@@ -851,6 +851,12 @@ void iqn_grad_norm_phase2(
|
||||
* `apply_iqn_trunk_gradient` saw NaN at slot 26 with finite inputs because
|
||||
* `online_params` here drifted via this Adam, never reached by the
|
||||
* single-kernel Mech 9 in `dqn_adam_update_kernel`. Disabled when bound==0.
|
||||
*
|
||||
* SP4 Task A14 (2026-04-30): Pearl C engagement counter via warp+block
|
||||
* tree-reduce mirroring `dqn_grad_norm_kernel` (no atomicAdd). Single
|
||||
* block-leader writes `clamp_engage_per_block_buf[engage_buf_offset +
|
||||
* blockIdx.x]`. `engage_buf_offset < 0` skips the writeback (defensive
|
||||
* default for callers that disable Pearl C).
|
||||
*/
|
||||
extern "C" __global__
|
||||
void iqn_adam_kernel(
|
||||
@@ -870,50 +876,89 @@ void iqn_adam_kernel(
|
||||
int b1_size, /* runtime: unused, for consistent interface */
|
||||
int b2_size, /* runtime: unused, for consistent interface */
|
||||
int b3_size, /* runtime: unused, for consistent interface */
|
||||
float weight_clamp_max_abs /* SP3 Mech 9: ISV-driven |p| bound (100 × Q_ABS_REF.max(1.0)). 0=disabled. */
|
||||
float weight_clamp_max_abs, /* SP3 Mech 9: ISV-driven |p| bound (100 × Q_ABS_REF.max(1.0)). 0=disabled. */
|
||||
/* SP4 Task A14: Pearl C engagement counter. */
|
||||
int* __restrict__ nan_flags_buf, /* sticky overflow flag */
|
||||
int diag_slot, /* nan_flags_buf index for this Adam kernel */
|
||||
int* __restrict__ clamp_engage_per_block_buf, /* [SP4_PARAM_GROUP_COUNT × MAX_BLOCKS_PER_ADAM] */
|
||||
int engage_buf_offset /* group*MAX_BLOCKS; -1 to skip */
|
||||
)
|
||||
{
|
||||
int tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (tid >= total_params) return;
|
||||
bool active = (tid < total_params);
|
||||
int local_engage = 0;
|
||||
|
||||
float g = grads[tid];
|
||||
if (active) {
|
||||
float g = grads[tid];
|
||||
|
||||
/* Skip NaN/Inf gradients — leave param, momentum, variance unchanged. */
|
||||
if (!isfinite(g)) { grads[tid] = 0.0f; return; }
|
||||
/* Skip NaN/Inf gradients — leave param, momentum, variance unchanged.
|
||||
* Still fall through to the block reduce so the engagement counter
|
||||
* sees this thread's local_engage=0. */
|
||||
if (!isfinite(g)) {
|
||||
grads[tid] = 0.0f;
|
||||
} else {
|
||||
/* Gradient clipping: norm_sq[0] is raw sum-of-squares from grad_norm kernel */
|
||||
float grad_norm_val = sqrtf(fmaxf(norm_sq[0], 0.0f));
|
||||
float clip_scale = (grad_norm_val > max_grad_norm && grad_norm_val > 0.0f)
|
||||
? max_grad_norm / grad_norm_val : 1.0f;
|
||||
g *= clip_scale;
|
||||
|
||||
/* Gradient clipping: norm_sq[0] is raw sum-of-squares from grad_norm kernel */
|
||||
float grad_norm_val = sqrtf(fmaxf(norm_sq[0], 0.0f));
|
||||
float clip_scale = (grad_norm_val > max_grad_norm && grad_norm_val > 0.0f)
|
||||
? max_grad_norm / grad_norm_val : 1.0f;
|
||||
g *= clip_scale;
|
||||
/* AdamW update */
|
||||
float mi = beta1 * m[tid] + (1.0f - beta1) * g;
|
||||
float vi = beta2 * v[tid] + (1.0f - beta2) * g * g;
|
||||
m[tid] = mi;
|
||||
v[tid] = vi;
|
||||
|
||||
/* AdamW update */
|
||||
float mi = beta1 * m[tid] + (1.0f - beta1) * g;
|
||||
float vi = beta2 * v[tid] + (1.0f - beta2) * g * g;
|
||||
m[tid] = mi;
|
||||
v[tid] = vi;
|
||||
/* Bias correction */
|
||||
int adam_t = adam_t_buf[0];
|
||||
float m_hat = mi / (1.0f - powf(beta1, (float)adam_t));
|
||||
float v_hat = vi / (1.0f - powf(beta2, (float)adam_t));
|
||||
|
||||
/* Bias correction */
|
||||
int adam_t = adam_t_buf[0];
|
||||
float m_hat = mi / (1.0f - powf(beta1, (float)adam_t));
|
||||
float v_hat = vi / (1.0f - powf(beta2, (float)adam_t));
|
||||
/* Parameter update with decoupled weight decay */
|
||||
float p = params[tid];
|
||||
p = p - lr * (m_hat / (sqrtf(v_hat) + eps) + weight_decay * p);
|
||||
|
||||
/* Parameter update with decoupled weight decay */
|
||||
float p = params[tid];
|
||||
p = p - lr * (m_hat / (sqrtf(v_hat) + eps) + weight_decay * p);
|
||||
/* SP3 Mech 9 + SP4 Task A14: post-Adam |p| clamp +
|
||||
* engagement-counter increment. Bound is host-supplied
|
||||
* (100 × Q_ABS_REF.max(1.0)). One OOM below slot 48
|
||||
* diagnostic threshold (1e3 × Q_ABS_REF) so the regression
|
||||
* sentinel keeps 10× firing headroom. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
if (fabsf(p) > weight_clamp_max_abs) {
|
||||
if (nan_flags_buf != nullptr && diag_slot >= 0) {
|
||||
nan_flags_buf[diag_slot] = 1;
|
||||
}
|
||||
local_engage = 1;
|
||||
}
|
||||
p = fminf(fmaxf(p, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
}
|
||||
|
||||
/* SP3 Mech 9: post-Adam |p| clamp. Bound is host-supplied
|
||||
* (100 × Q_ABS_REF.max(1.0)). One OOM below slot 48 diagnostic
|
||||
* threshold (1e3 × Q_ABS_REF) so the regression sentinel keeps
|
||||
* 10× firing headroom. */
|
||||
if (weight_clamp_max_abs > 0.0f) {
|
||||
p = fminf(fmaxf(p, -weight_clamp_max_abs), weight_clamp_max_abs);
|
||||
params[tid] = p;
|
||||
|
||||
/* Zero gradient for next step */
|
||||
grads[tid] = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
params[tid] = p;
|
||||
/* SP4 Task A14: warp + block tree-reduce of `local_engage`. */
|
||||
int e = local_engage;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
e += __shfl_xor_sync(0xFFFFFFFF, e, offset);
|
||||
|
||||
/* Zero gradient for next step */
|
||||
grads[tid] = 0.0f;
|
||||
__shared__ int warp_engage[8]; /* blockDim.x = 256 ⇒ 8 warps */
|
||||
int warp_id = threadIdx.x / 32;
|
||||
int warp_lane = threadIdx.x % 32;
|
||||
if (warp_lane == 0) warp_engage[warp_id] = e;
|
||||
__syncthreads();
|
||||
|
||||
if (warp_id == 0) {
|
||||
int val = (warp_lane < blockDim.x / 32) ? warp_engage[warp_lane] : 0;
|
||||
for (int offset = 16; offset > 0; offset >>= 1)
|
||||
val += __shfl_xor_sync(0xFFFFFFFF, val, offset);
|
||||
if (warp_lane == 0 && engage_buf_offset >= 0 && clamp_engage_per_block_buf != nullptr) {
|
||||
clamp_engage_per_block_buf[engage_buf_offset + blockIdx.x] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Forward-Only Kernel (Inference) ────────────────────────────────── */
|
||||
|
||||
@@ -65,6 +65,13 @@ pub use sp4_isv_slots::{
|
||||
GRAD_CLIP_BOUND_INDEX, H_S2_BOUND_INDEX, L1_LAMBDA_TRUNK_INDEX,
|
||||
SP4_PARAM_GROUP_COUNT, SP4_BRANCH_COUNT, SP4_SLOT_BASE, SP4_SLOT_END, ParamGroup,
|
||||
weight_bound, adam_m_bound, adam_v_bound, wd_rate, atom_pos_bound,
|
||||
// SP4 Task A14: Pearl C engagement-counter buffer constants.
|
||||
MAX_BLOCKS_PER_ADAM, SP4_ENGAGE_BUF_LEN, SP4_ENGAGE_OFFSET_DISABLED,
|
||||
SP4_DQN_ADAM_DIAG_SLOT, SP4_IQN_ADAM_DIAG_SLOT, SP4_IQL_ADAM_DIAG_SLOT,
|
||||
SP4_ATTN_ADAM_DIAG_SLOT, SP4_CURIOSITY_ADAM_DIAG_SLOT, SP4_NAN_FLAGS_END,
|
||||
SP4_ENGAGE_OFFSET_CURIOSITY_W1, SP4_ENGAGE_OFFSET_CURIOSITY_B1,
|
||||
SP4_ENGAGE_OFFSET_CURIOSITY_W2, SP4_ENGAGE_OFFSET_CURIOSITY_B2,
|
||||
SP4_ENGAGE_EXTRA_CURIOSITY_SUBLAUNCHES,
|
||||
};
|
||||
pub mod sp4_wiener_ema;
|
||||
pub use sp4_wiener_ema::{
|
||||
|
||||
@@ -48,6 +48,73 @@ pub const SP4_SLOT_BASE: usize = 131;
|
||||
/// One past last slot used by SP4. Must equal `ISV_TOTAL_DIM` post-bump.
|
||||
pub const SP4_SLOT_END: usize = 171;
|
||||
|
||||
// ── SP4 Task A14: Pearl C engagement-counter buffer layout ───────────
|
||||
|
||||
/// Maximum block grid size per Adam-kernel launch — the engagement
|
||||
/// counter buffer reserves `MAX_BLOCKS_PER_ADAM` slots per param group,
|
||||
/// indexed by `blockIdx.x`. 256 covers up to ≈65k params per group at
|
||||
/// blockDim=256 threads, comfortably above all current production
|
||||
/// param-group sizes (DQN trunk≈30k, DQN value≈4k, branches≈80k —
|
||||
/// branch group is the only one exceeding 65k, but the launcher caps
|
||||
/// `blocks` at `MAX_BLOCKS_PER_ADAM` defensively in launch code).
|
||||
pub const MAX_BLOCKS_PER_ADAM: usize = 256;
|
||||
|
||||
/// Total length of the Pearl C engagement-counter mapped-pinned buffer
|
||||
/// (`clamp_engage_per_block_buf`). The 8 main param groups occupy
|
||||
/// offsets [0..2048) (8 × 256). Curiosity has 4 sub-launches (W1/b1/
|
||||
/// W2/b2) sharing the same kernel; to avoid block-index collisions
|
||||
/// between sub-launches, each gets its own offset slot. Sub-launch 0
|
||||
/// reuses Curiosity's main group offset (group_idx=7 → 1792). Sub-
|
||||
/// launches 1/2/3 occupy extra slots at offsets 2048/2304/2560 — i.e.
|
||||
/// 3 extra logical "groups" beyond the canonical 8. Total length:
|
||||
/// (8 + 3) × 256 = 11 × 256 = 2816 i32s. Host-side
|
||||
/// `pearl_c_post_adam_engagement_check` for `ParamGroup::Curiosity`
|
||||
/// sums all 4 sub-launch ranges.
|
||||
pub const SP4_ENGAGE_EXTRA_CURIOSITY_SUBLAUNCHES: usize = 3;
|
||||
pub const SP4_ENGAGE_BUF_LEN: usize =
|
||||
(SP4_PARAM_GROUP_COUNT + SP4_ENGAGE_EXTRA_CURIOSITY_SUBLAUNCHES) * MAX_BLOCKS_PER_ADAM;
|
||||
|
||||
/// Curiosity sub-launch offsets in `clamp_engage_per_block_buf`.
|
||||
/// Sub-launch 0 (W1) reuses the Curiosity main-group offset (7×256=1792).
|
||||
/// Sub-launches 1/2/3 (b1/W2/b2) get extra slots 8/9/10 × 256
|
||||
/// (= 2048/2304/2560).
|
||||
pub const SP4_ENGAGE_OFFSET_CURIOSITY_W1: i32 = 7 * (MAX_BLOCKS_PER_ADAM as i32);
|
||||
pub const SP4_ENGAGE_OFFSET_CURIOSITY_B1: i32 = 8 * (MAX_BLOCKS_PER_ADAM as i32);
|
||||
pub const SP4_ENGAGE_OFFSET_CURIOSITY_W2: i32 = 9 * (MAX_BLOCKS_PER_ADAM as i32);
|
||||
pub const SP4_ENGAGE_OFFSET_CURIOSITY_B2: i32 = 10 * (MAX_BLOCKS_PER_ADAM as i32);
|
||||
|
||||
/// Sentinel `engage_buf_offset` value passed by aux trainers outside
|
||||
/// the SP4 8-group taxonomy (DT, OFI embed, denoise, recursive_conf,
|
||||
/// sel). Causes the kernel's per-block writeback to be skipped — Pearl
|
||||
/// C only tracks the canonical 8 param groups.
|
||||
pub const SP4_ENGAGE_OFFSET_DISABLED: i32 = -1;
|
||||
|
||||
// ── SP4 Task A14: per-Adam-kernel diag_slot allocations ──────────────
|
||||
// `nan_flags_buf` slots 50-54 record per-Adam-kernel sticky-engagement
|
||||
// flags (idempotent writes — no race). The buffer is sized 50 today
|
||||
// (see gpu_dqn_trainer.rs `nan_flags_buf` alloc); A14 grows it to 55.
|
||||
|
||||
/// `dqn_adam_update_kernel` (main DQN trainer + 3 post_aux helpers).
|
||||
pub const SP4_DQN_ADAM_DIAG_SLOT: i32 = 50;
|
||||
|
||||
/// `iqn_adam_kernel` (IQN dual-head trainer).
|
||||
pub const SP4_IQN_ADAM_DIAG_SLOT: i32 = 51;
|
||||
|
||||
/// `iql_adam_kernel` (both high- and low-tau IQL trainers share the
|
||||
/// slot — identical kernel code; per-group separation is via
|
||||
/// `engage_buf_offset`, not the sticky flag).
|
||||
pub const SP4_IQL_ADAM_DIAG_SLOT: i32 = 52;
|
||||
|
||||
/// `attn_adam_kernel` (GpuAttention + GpuTlob — TLOB shares the cubin).
|
||||
pub const SP4_ATTN_ADAM_DIAG_SLOT: i32 = 53;
|
||||
|
||||
/// `curiosity_adam_step` (4 sub-buffer launches per step).
|
||||
pub const SP4_CURIOSITY_ADAM_DIAG_SLOT: i32 = 54;
|
||||
|
||||
/// One past the last `nan_flags_buf` index used by SP4 — drives the
|
||||
/// buffer allocation size in `GpuDqnTrainer::new`.
|
||||
pub const SP4_NAN_FLAGS_END: usize = 55;
|
||||
|
||||
/// Convenience: slot index for `WEIGHT_BOUND[group]`.
|
||||
#[inline]
|
||||
pub const fn weight_bound(group: usize) -> usize {
|
||||
@@ -141,4 +208,36 @@ mod tests {
|
||||
assert!(atom_pos_bound(SP4_BRANCH_COUNT - 1) < WEIGHT_BOUND_BASE,
|
||||
"atom_pos_bound max must not alias into WEIGHT_BOUND family");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pearl_c_engage_buf_layout() {
|
||||
// 8 main groups + 3 extra curiosity sub-launch slots.
|
||||
assert_eq!(MAX_BLOCKS_PER_ADAM, 256);
|
||||
assert_eq!(SP4_PARAM_GROUP_COUNT, 8);
|
||||
assert_eq!(SP4_ENGAGE_EXTRA_CURIOSITY_SUBLAUNCHES, 3);
|
||||
assert_eq!(SP4_ENGAGE_BUF_LEN, 11 * 256);
|
||||
assert_eq!(SP4_ENGAGE_BUF_LEN, 2816);
|
||||
|
||||
// Curiosity sub-launch offsets are distinct + contiguous starting
|
||||
// from the canonical Curiosity group offset.
|
||||
assert_eq!(SP4_ENGAGE_OFFSET_CURIOSITY_W1, 7 * 256);
|
||||
assert_eq!(SP4_ENGAGE_OFFSET_CURIOSITY_B1, 8 * 256);
|
||||
assert_eq!(SP4_ENGAGE_OFFSET_CURIOSITY_W2, 9 * 256);
|
||||
assert_eq!(SP4_ENGAGE_OFFSET_CURIOSITY_B2, 10 * 256);
|
||||
|
||||
// Highest sub-launch end must fit within the buffer.
|
||||
let highest_end = (SP4_ENGAGE_OFFSET_CURIOSITY_B2 as usize) + MAX_BLOCKS_PER_ADAM;
|
||||
assert_eq!(highest_end, SP4_ENGAGE_BUF_LEN);
|
||||
|
||||
// Sentinel disabled value.
|
||||
assert_eq!(SP4_ENGAGE_OFFSET_DISABLED, -1);
|
||||
|
||||
// Per-Adam-kernel diag slot indices land within the
|
||||
// sticky-flag buffer (size SP4_NAN_FLAGS_END=55).
|
||||
assert!((SP4_DQN_ADAM_DIAG_SLOT as usize) < SP4_NAN_FLAGS_END);
|
||||
assert!((SP4_IQN_ADAM_DIAG_SLOT as usize) < SP4_NAN_FLAGS_END);
|
||||
assert!((SP4_IQL_ADAM_DIAG_SLOT as usize) < SP4_NAN_FLAGS_END);
|
||||
assert!((SP4_ATTN_ADAM_DIAG_SLOT as usize) < SP4_NAN_FLAGS_END);
|
||||
assert!((SP4_CURIOSITY_ADAM_DIAG_SLOT as usize) < SP4_NAN_FLAGS_END);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1687,6 +1687,65 @@ impl FusedTrainingCtx {
|
||||
// ── Outside graph: kernel-free host ops only ─────────────────
|
||||
self.pending_vaccine_batch = None;
|
||||
|
||||
// SP4 Task A15: Pearl C post-Adam engagement-rate check (Layer A).
|
||||
// Reads per-block engagement counts from the mapped-pinned buffer
|
||||
// (zero-copy host access), reduces → engagement_rate, applies
|
||||
// Pearls A+D to the rate-deficit time series. Layer A logs only;
|
||||
// Layer B will mutate ISV[WEIGHT_BOUND[group]] on sustained
|
||||
// over-engagement.
|
||||
//
|
||||
// The 5 main Adam launches captured in this step's graph-replay
|
||||
// each contribute to a distinct param-group offset:
|
||||
// group 0 (DqnTrunk) — covers DQN Trunk/Value/Branches
|
||||
// (Layer A simplification: counted under
|
||||
// group 0 only — Layer B will split).
|
||||
// group 3 (Iqn) — IQN dual-head trainer.
|
||||
// group 4 (IqlHigh) — IQL high-tau trainer.
|
||||
// group 5 (IqlLow) — IQL low-tau trainer.
|
||||
// group 6 (Attn) — 4-head attention (TLOB shares cubin
|
||||
// but disables Pearl C).
|
||||
// group 7 (Curiosity) — handled in `train_curiosity_gpu`,
|
||||
// not here (separate caller path).
|
||||
let total_params_dqn = self.trainer.total_params();
|
||||
if let Err(e) = self.trainer.pearl_c_post_adam_engagement_check(
|
||||
crate::cuda_pipeline::ParamGroup::DqnTrunk,
|
||||
total_params_dqn,
|
||||
) {
|
||||
tracing::warn!("SP4 Pearl C check (DQN trunk) failed (non-fatal): {e}");
|
||||
}
|
||||
if let Some(ref iqn) = self.gpu_iqn {
|
||||
let n = iqn.total_params();
|
||||
if let Err(e) = self.trainer.pearl_c_post_adam_engagement_check(
|
||||
crate::cuda_pipeline::ParamGroup::Iqn,
|
||||
n,
|
||||
) {
|
||||
tracing::warn!("SP4 Pearl C check (IQN) failed (non-fatal): {e}");
|
||||
}
|
||||
}
|
||||
let iql_high_n = self.gpu_iql.total_params();
|
||||
if let Err(e) = self.trainer.pearl_c_post_adam_engagement_check(
|
||||
crate::cuda_pipeline::ParamGroup::IqlHigh,
|
||||
iql_high_n,
|
||||
) {
|
||||
tracing::warn!("SP4 Pearl C check (IQL high) failed (non-fatal): {e}");
|
||||
}
|
||||
let iql_low_n = self.gpu_iql_low.total_params();
|
||||
if let Err(e) = self.trainer.pearl_c_post_adam_engagement_check(
|
||||
crate::cuda_pipeline::ParamGroup::IqlLow,
|
||||
iql_low_n,
|
||||
) {
|
||||
tracing::warn!("SP4 Pearl C check (IQL low) failed (non-fatal): {e}");
|
||||
}
|
||||
if let Some(ref attn) = self.gpu_attention {
|
||||
let n = attn.total_params();
|
||||
if let Err(e) = self.trainer.pearl_c_post_adam_engagement_check(
|
||||
crate::cuda_pipeline::ParamGroup::Attn,
|
||||
n,
|
||||
) {
|
||||
tracing::warn!("SP4 Pearl C check (Attn) failed (non-fatal): {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Adaptive IQN lambda: read loss OUTSIDE graph (pinned read, no kernel).
|
||||
if let Some(ref iqn) = self.gpu_iqn {
|
||||
if let Ok(loss) = iqn.read_total_loss() {
|
||||
@@ -3229,6 +3288,53 @@ impl FusedTrainingCtx {
|
||||
self.trainer.set_curiosity_weights(&weights.w1, &weights.b1, &weights.w2, &weights.b2);
|
||||
}
|
||||
|
||||
/// SP4 Task A14: device pointer to the trainer's `nan_flags_buf`.
|
||||
/// Used by external aux paths (`GpuExperienceCollector` for the
|
||||
/// curiosity trainer) to share the Pearl C sticky-flag store.
|
||||
pub(crate) fn nan_flags_buf_ptr(&self) -> u64 {
|
||||
self.trainer.nan_flags_buf_ptr()
|
||||
}
|
||||
|
||||
/// SP4 Task A14: device pointer to the trainer's
|
||||
/// `clamp_engage_per_block_buf` (mapped pinned). Used by external
|
||||
/// aux paths to share the Pearl C per-block engagement-counter buf.
|
||||
pub(crate) fn clamp_engage_per_block_buf_dev_ptr(&self) -> u64 {
|
||||
self.trainer.clamp_engage_per_block_buf_dev_ptr()
|
||||
}
|
||||
|
||||
/// SP4 Task A14: wire the main DQN trainer's mapped-pinned Pearl C
|
||||
/// buffers into all aux trainers owned by this `FusedTrainingCtx`.
|
||||
/// Idempotent — safe to call multiple times. Curiosity is wired
|
||||
/// separately because it lives in `GpuExperienceCollector`, not
|
||||
/// here; that path goes through
|
||||
/// `GpuExperienceCollector::set_curiosity_pearl_c_buffers`.
|
||||
pub(crate) fn wire_aux_trainer_pearl_c_buffers(&mut self) {
|
||||
let nan_flags_ptr = self.trainer.nan_flags_buf_ptr();
|
||||
let engage_buf_ptr = self.trainer.clamp_engage_per_block_buf_dev_ptr();
|
||||
|
||||
// IQN head (group 3 = ParamGroup::Iqn).
|
||||
if let Some(ref mut iqn) = self.gpu_iqn {
|
||||
iqn.set_pearl_c_buffers(nan_flags_ptr, engage_buf_ptr);
|
||||
}
|
||||
// IQL high-tau trainer (group 4 = ParamGroup::IqlHigh) — default
|
||||
// group on construction; explicitly re-set for clarity.
|
||||
self.gpu_iql.set_pearl_c_buffers(nan_flags_ptr, engage_buf_ptr);
|
||||
self.gpu_iql.set_pearl_c_param_group(
|
||||
crate::cuda_pipeline::ParamGroup::IqlHigh,
|
||||
);
|
||||
// IQL low-tau trainer (group 5 = ParamGroup::IqlLow).
|
||||
self.gpu_iql_low.set_pearl_c_buffers(nan_flags_ptr, engage_buf_ptr);
|
||||
self.gpu_iql_low.set_pearl_c_param_group(
|
||||
crate::cuda_pipeline::ParamGroup::IqlLow,
|
||||
);
|
||||
// Attention head (group 6 = ParamGroup::Attn). TLOB shares the
|
||||
// attn_adam_kernel cubin but disables Pearl C at the launch site
|
||||
// (Layer A); we do not wire it here.
|
||||
if let Some(ref mut attn) = self.gpu_attention {
|
||||
attn.set_pearl_c_buffers(nan_flags_ptr, engage_buf_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute Q-value statistics from raw f32 states pointer (cold path).
|
||||
pub(crate) fn compute_q_stats_from_ptr(
|
||||
&mut self,
|
||||
|
||||
@@ -527,7 +527,17 @@ impl StateResetRegistry {
|
||||
RegistryEntry {
|
||||
name: "sp4_clamp_engage_counters",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "GpuDqnTrainer.clamp_engage_per_block_buf [2048 ints = SP4_PARAM_GROUP_COUNT × MAX_BLOCKS_PER_ADAM] — SP4 Pearl C engagement counter (per-(group, Adam-launch-block) clamp-trigger count). Mapped-pinned i32; bulk write_bytes(0) at fold boundary so the new fold's engagement-rate tracking restarts from a clean 0 baseline (Task A12)",
|
||||
description: "GpuDqnTrainer.clamp_engage_per_block_buf [2816 ints = (SP4_PARAM_GROUP_COUNT + 3 curiosity sub-launches) × MAX_BLOCKS_PER_ADAM = 11 × 256] — SP4 Pearl C engagement counter (per-(group, Adam-launch-block) clamp-trigger count). Mapped-pinned i32; bulk write_bytes(0) at fold boundary so the new fold's engagement-rate tracking restarts from a clean 0 baseline. Buffer length grew 2048→2816 in Task A14/A15 to give Curiosity's 4 sub-launches (W1/b1/W2/b2) distinct offset slots avoiding per-block index collisions (Task A12 + A14/A15)",
|
||||
},
|
||||
RegistryEntry {
|
||||
name: "sp4_pearl_c_rate_deficit_state",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "GpuDqnTrainer.pearl_c_rate_deficit_state_buf [24 floats = SP4_PARAM_GROUP_COUNT × {sample_var, diff_var, x_lag}] — SP4 Pearl C rate-deficit Wiener-state (per param group). Mapped-pinned f32; bulk write_bytes(0) at fold boundary so the new fold's engagement-rate-deficit EMA restarts from Pearl A sentinel (`prev_x_mean == 0 AND state.x_lag == 0` → first-observation replacement). Paired with sp4_pearl_c_rate_deficit_ema — both reset together (Task A15)",
|
||||
},
|
||||
RegistryEntry {
|
||||
name: "sp4_pearl_c_rate_deficit_ema",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "GpuDqnTrainer.pearl_c_rate_deficit_ema [8 floats, host-only [f32; SP4_PARAM_GROUP_COUNT]] — SP4 Pearl C rate-deficit EMA per param group (the `prev_x_mean` surrogate read by `pearls_ad_update`). Reset to all-zeros at fold boundary so the new fold's deficit tracking restarts from the Pearl A sentinel. Companion to sp4_pearl_c_rate_deficit_state (Task A15)",
|
||||
},
|
||||
];
|
||||
Self { entries }
|
||||
|
||||
@@ -1430,6 +1430,14 @@ impl DQNTrainer {
|
||||
info!("Curiosity Q-penalty: weights wired to fused trainer (lambda={:.2})",
|
||||
self.hyperparams.curiosity_q_penalty_lambda);
|
||||
}
|
||||
// SP4 Task A14: wire the main DQN trainer's mapped-
|
||||
// pinned Pearl C buffers into all aux trainers
|
||||
// (IQN, IQL hi/lo, Attn) and the curiosity trainer.
|
||||
fused_ctx.wire_aux_trainer_pearl_c_buffers();
|
||||
let nan_flags_ptr = fused_ctx.nan_flags_buf_ptr();
|
||||
let engage_buf_ptr = fused_ctx.clamp_engage_per_block_buf_dev_ptr();
|
||||
collector.set_curiosity_pearl_c_buffers(nan_flags_ptr, engage_buf_ptr);
|
||||
info!("SP4 Pearl C: aux trainer engagement-counter buffers wired");
|
||||
}
|
||||
self.gpu_experience_collector = Some(collector);
|
||||
}
|
||||
@@ -1774,6 +1782,20 @@ impl DQNTrainer {
|
||||
) {
|
||||
debug!("GPU curiosity training failed (non-fatal): {e}");
|
||||
}
|
||||
// SP4 Task A15: Pearl C post-Adam engagement-rate check for
|
||||
// curiosity trainer (Layer A). Curiosity has 4 sub-launches
|
||||
// (W1/b1/W2/b2) and `pearl_c_post_adam_engagement_check`
|
||||
// sums all 4 sub-launch ranges before computing the rate.
|
||||
if let Some(ref mut fused) = self.fused_ctx {
|
||||
let curiosity_total = crate::cuda_pipeline::gpu_curiosity_trainer
|
||||
::GpuCuriosityTrainer::total_params();
|
||||
if let Err(e) = fused.trainer_mut().pearl_c_post_adam_engagement_check(
|
||||
crate::cuda_pipeline::ParamGroup::Curiosity,
|
||||
curiosity_total,
|
||||
) {
|
||||
tracing::warn!("SP4 Pearl C check (Curiosity) failed (non-fatal): {e}");
|
||||
}
|
||||
}
|
||||
if let Some(ref stream) = self.cuda_stream {
|
||||
match stream.record_event(None) {
|
||||
Ok(event) => {
|
||||
@@ -5841,12 +5863,34 @@ impl DQNTrainer {
|
||||
}
|
||||
"sp4_clamp_engage_counters" => {
|
||||
// Bulk write_bytes(0) on `clamp_engage_per_block_buf`
|
||||
// (2048 mapped-pinned i32). Each fold restarts Pearl C's
|
||||
// (2816 mapped-pinned i32 — Task A14/A15 grew it from
|
||||
// 2048 to accommodate Curiosity's 4 distinct sub-launch
|
||||
// offset slots). Each fold restarts Pearl C's
|
||||
// engagement-rate tracking from a clean 0 baseline.
|
||||
if let Some(ref mut fused) = self.fused_ctx {
|
||||
fused.trainer_mut().reset_sp4_clamp_engage_counters();
|
||||
}
|
||||
}
|
||||
"sp4_pearl_c_rate_deficit_state" => {
|
||||
// Bulk write_bytes(0) on `pearl_c_rate_deficit_state_buf`
|
||||
// (24 mapped-pinned f32 = 8 groups × {sample_var, diff_var,
|
||||
// x_lag}). Pearl A's first-observation sentinel requires
|
||||
// `state.x_lag == 0` on the new fold's first engagement-
|
||||
// rate-deficit observation; reset paired with the host-
|
||||
// only `pearl_c_rate_deficit_ema` below.
|
||||
if let Some(ref mut fused) = self.fused_ctx {
|
||||
fused.trainer_mut().reset_sp4_pearl_c_rate_deficit_state();
|
||||
}
|
||||
}
|
||||
"sp4_pearl_c_rate_deficit_ema" => {
|
||||
// Reset the host-only `pearl_c_rate_deficit_ema [f32; 8]`
|
||||
// EMA surrogate to all zeros. Companion to
|
||||
// sp4_pearl_c_rate_deficit_state — both halves of Pearl A's
|
||||
// sentinel contract reset together.
|
||||
if let Some(ref mut fused) = self.fused_ctx {
|
||||
fused.trainer_mut().reset_sp4_pearl_c_rate_deficit_ema();
|
||||
}
|
||||
}
|
||||
"isv_gamma_dir_eff" | "isv_gamma_mag_eff" | "isv_gamma_ord_eff" | "isv_gamma_urg_eff" => {
|
||||
// D.2 per-branch gamma slots. Reset to 0.0 at fold boundary;
|
||||
// per_branch_gamma_update GPU kernel re-populates on next epoch-boundary launch.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user