feat(sp5): Task A4 — Pearl 4 per-group Adam β1/β2/ε
Per-param-group Adam hyperparameters from per-group gradient
direction-stability EMA + L2 norm. 8 SP4 param groups × 3 hparams = 24
ISV slots [226..250).
Two-kernel chain:
grad_cosine_sim_update — per-group cosine_sim of curr vs prev grads
+ L2 norm; writes back grad_curr → grad_prev
for next step's comparison.
pearl_4_adam_hparams_update — β1/β2/ε from cosine + l2_norm.
Structural envelopes (Invariant 1 anchors per spec line 88):
β1 ∈ [0.85, 0.95]; β2 ∈ [0.99, 0.9995]; ε ∈ [1e-10, 1e-6]
ALPHA_META migration (Option A — atomic shared-contract migration per
feedback_no_partial_refactor): apply_pearls_ad_kernel.cu and the
launch_apply_pearls Rust wrapper now take alpha_meta as a parameter.
All 45 existing call sites (SP4 + SP5 producers) pass the prior default
1.0e-3 explicitly via crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META.
Pearl 4's apply_pearls calls pass 5.0e-4 (half the default per spec
line 89) to limit β change rate.
Theoretical caveat: adaptive β breaks Adam's constant-β convergence
proof. Mitigations: structural envelopes + halved ALPHA_META + Pearls
A+D smoothing. Fall-back path defined: revert + ε-only adaptive
variant if Layer C destabilization observed.
New mapped-pinned buffer: grad_prev_buf_per_group [TOTAL_PARAMS] for
cosine-sim previous-step direction storage. Mapped-pinned i32 mirror
of grad_buf layout.
producer_step_scratch_buf grew 131 → 171 (40 new outputs: 8 cosine_sim
+ 8 l2_norm + 24 β1/β2/ε). wiener_state_buf already at 543 (A1 sized
for entire SP5 block).
StateResetRegistry: 4 new FoldReset entries: sp5_adam_beta1,
sp5_adam_beta2, sp5_adam_eps, sp5_grad_prev_buf. All sentinel 0 →
bootstrap to envelope midpoint via Pearls A+D + cosine_sim=0 fall-back
on first step of new fold.
Adam-launcher consumer migration deferred to Layer B.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -430,6 +430,18 @@ fn main() {
|
||||
// apply_pearls_ad_kernel then smooths via Pearls A+D into ISV[190..210).
|
||||
// Must run AFTER pearl_1_atom (Q_VAR) AND pearl_3_sigma (NOISY_SIGMA).
|
||||
"pearl_2_budget_kernel.cu",
|
||||
// SP5 Task A4 (2026-05-01): per-group gradient cosine similarity + L2 norm.
|
||||
// Single-block 8-thread kernel (one per param group: DqnTrunk/Value/Branches/
|
||||
// IQN/IqlHigh/IqlLow/Attn/Curiosity). Reads grad_buf + grad_prev_buf;
|
||||
// writes cosine_sim[8]→scratch[131..139) and l2_norm[8]→scratch[139..147).
|
||||
// Also writebacks grad_curr → grad_prev_buf for the next step's comparison.
|
||||
"grad_cosine_sim_kernel.cu",
|
||||
// SP5 Task A4 (2026-05-01): Pearl 4 per-param-group Adam β1, β2, ε.
|
||||
// Single-block 8-thread kernel reads cosine_sim + l2_norm from scratch;
|
||||
// writes β1[8]→scratch[147..155), β2[8]→scratch[155..163), ε[8]→scratch[163..171).
|
||||
// apply_pearls_ad_kernel then smooths via Pearls A+D (ALPHA_META=5e-4)
|
||||
// into ISV[226..250). Must run AFTER grad_cosine_sim_kernel.
|
||||
"pearl_4_adam_hparams_kernel.cu",
|
||||
// SP4 GPU-only Pearls A+D applicator (2026-05-01). Single-thread
|
||||
// device-side loop applies Pearls A+D to N consecutive ISV slots
|
||||
// (each with its own Wiener-state triple) on the producer's
|
||||
|
||||
@@ -20,8 +20,13 @@
|
||||
// pattern broke graph capture at `aux_label_scale_ema` mid-step
|
||||
// (CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED — smoke `smoke-test-v9kjv`).
|
||||
//
|
||||
// Constants (`ALPHA_META`, `EPS_DIV`) MUST match
|
||||
// `cuda_pipeline::sp4_wiener_ema::{ALPHA_META, EPS_DIV}` bit-for-bit.
|
||||
// `ALPHA_META` is now passed as a kernel parameter so Pearl 4 can use
|
||||
// a halved rate (5e-4) while all SP4 and other SP5 producers continue
|
||||
// to use the SP4 default (1e-3). `EPS_DIV` remains a compile-time
|
||||
// constant — it is numerical-stability-only and must never vary.
|
||||
//
|
||||
// Constants (`EPS_DIV`) MUST match
|
||||
// `cuda_pipeline::sp4_wiener_ema::EPS_DIV` bit-for-bit.
|
||||
// Single-source-of-truth lives in `sp4_wiener_ema.rs` for documentation;
|
||||
// this duplicate is required because CUDA C++ can't include the Rust
|
||||
// const directly. The new GPU oracle test asserts agreement within
|
||||
@@ -34,14 +39,15 @@ extern "C" __global__ void apply_pearls_ad_kernel(
|
||||
int isv_idx_base,
|
||||
float* __restrict__ wiener_state_buf, /* [wiener_offset_base + 3*n_slots) packed [sv, dv, xl] triples */
|
||||
int wiener_offset_base,
|
||||
int n_slots
|
||||
int n_slots,
|
||||
float alpha_meta /* meta-EMA rate for Wiener variance tracking;
|
||||
SP4 default = 1e-3; Pearl 4 uses 5e-4 */
|
||||
) {
|
||||
// Single-thread device-side loop. Every slot has its own state; no
|
||||
// cross-slot parallelism is meaningful. Guard avoids redundant work
|
||||
// if the launcher accidentally requests >1 thread.
|
||||
if (blockIdx.x != 0 || threadIdx.x != 0) return;
|
||||
|
||||
constexpr float ALPHA_META = 1.0e-3f;
|
||||
constexpr float EPS_DIV = 1.0e-8f;
|
||||
|
||||
for (int i = 0; i < n_slots; ++i) {
|
||||
@@ -85,9 +91,9 @@ extern "C" __global__ void apply_pearls_ad_kernel(
|
||||
const float x_lag_diff = step_obs - x_lag;
|
||||
|
||||
const float new_sample_var =
|
||||
(1.0f - ALPHA_META) * sample_var + ALPHA_META * sample_diff * sample_diff;
|
||||
(1.0f - alpha_meta) * sample_var + alpha_meta * sample_diff * sample_diff;
|
||||
const float new_diff_var =
|
||||
(1.0f - ALPHA_META) * diff_var + ALPHA_META * x_lag_diff * x_lag_diff;
|
||||
(1.0f - alpha_meta) * diff_var + alpha_meta * x_lag_diff * x_lag_diff;
|
||||
|
||||
// EPS_DIV protects 0/0 at t=1 when both variances start near 0.
|
||||
const float alpha_opt =
|
||||
|
||||
@@ -318,6 +318,24 @@ static SP5_PEARL_3_SIGMA_CUBIN: &[u8] =
|
||||
static SP5_PEARL_2_BUDGET_CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/pearl_2_budget_kernel.cubin"));
|
||||
|
||||
/// SP5 Task A4 (2026-05-01): auxiliary gradient cosine similarity cubin.
|
||||
/// Single-block 8-thread kernel (one thread per SP4 param group). Reads
|
||||
/// grad_buf + grad_prev_buf_per_group + group_param_offsets_buf [9 i32];
|
||||
/// computes per-group cos_sim and l2_norm; writes to scratch_buf[131..147).
|
||||
/// Also writes grad_curr → grad_prev_buf_writeback for next step's comparison.
|
||||
/// No atomicAdd (feedback_no_atomicadd).
|
||||
static SP5_GRAD_COSINE_SIM_CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/grad_cosine_sim_kernel.cubin"));
|
||||
|
||||
/// SP5 Task A4 (2026-05-01): Pearl 4 per-group Adam β1/β2/ε cubin.
|
||||
/// Single-block 8-thread kernel (one thread per param group). Reads
|
||||
/// scratch_buf[131..147) (cosine_sim + l2_norm); computes β1/β2/ε within
|
||||
/// structural envelopes (Invariant 1 anchors); writes to scratch_buf[147..171).
|
||||
/// Chained apply_pearls_ad_kernel (24 launches, ALPHA_META=5e-4) smooths into
|
||||
/// ISV[ADAM_BETA1_BASE..ADAM_EPS_BASE+8). No atomicAdd.
|
||||
static SP5_PEARL_4_ADAM_HPARAMS_CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/pearl_4_adam_hparams_kernel.cubin"));
|
||||
|
||||
/// Plan 4 Task 3 (E.3): IQN multi-quantile diagnostic EMAs into ISV[99..103).
|
||||
/// 4-block (256 threads/block, shmem-reduce, no atomicAdd) kernel launched
|
||||
/// alongside `h_s2_rms_ema_update` from `training_loop.rs`. Reads the IQN
|
||||
@@ -901,7 +919,7 @@ pub const SP5_WIENER_TOTAL_FLOATS: usize =
|
||||
(SP4_PRODUCER_COUNT + crate::cuda_pipeline::sp5_isv_slots::SP5_PRODUCER_COUNT)
|
||||
* SP4_WIENER_FLOATS_PER_SLOT;
|
||||
|
||||
/// SP5 Task A1+A2+A3: producer_step_scratch_buf total slot count after growth.
|
||||
/// SP5 Task A1+A2+A3+A4: producer_step_scratch_buf total slot count after growth.
|
||||
/// SP4 had 71 scratch slots [0..71); Task A1 adds:
|
||||
/// 16 floats for q_branch_stats output (SCRATCH_Q_STATS=71, 4 outputs × 4 branches)
|
||||
/// 16 floats for pearl_1_atom output (SCRATCH_ATOM_OUT=87, 4 outputs × 4 branches)
|
||||
@@ -915,8 +933,14 @@ pub const SP5_WIENER_TOTAL_FLOATS: usize =
|
||||
/// budget_cql[4] → SCRATCH_PEARL_2_CQL=119
|
||||
/// budget_ens[4] → SCRATCH_PEARL_2_ENS=123
|
||||
/// flatness[4] → SCRATCH_PEARL_2_FLATNESS=127
|
||||
/// Combined: 71 + 16 + 16 + 4 + 4 + 20 = 131 scratch slots [0..131).
|
||||
pub const SP5_SCRATCH_TOTAL: usize = 131;
|
||||
/// Task A4 adds:
|
||||
/// 8 floats for grad_cosine_sim output (SCRATCH_PEARL_4_COSINE=131, cos_sim × 8 groups)
|
||||
/// 8 floats for grad_l2_norm output (SCRATCH_PEARL_4_L2=139, l2_norm × 8 groups)
|
||||
/// 8 floats for pearl_4_beta1 output (SCRATCH_PEARL_4_BETA1=147, β1 × 8 groups)
|
||||
/// 8 floats for pearl_4_beta2 output (SCRATCH_PEARL_4_BETA2=155, β2 × 8 groups)
|
||||
/// 8 floats for pearl_4_eps output (SCRATCH_PEARL_4_EPS=163, ε × 8 groups)
|
||||
/// Combined: 71 + 16 + 16 + 4 + 4 + 20 + 8 + 8 + 8 + 8 + 8 = 171 scratch slots [0..171).
|
||||
pub const SP5_SCRATCH_TOTAL: usize = 171;
|
||||
|
||||
/// SP5 Task A2: scratch index base for pearl_3_sigma sigma[4] output block.
|
||||
/// Slots [103..107): per-branch NoisyNet sigma.
|
||||
@@ -947,6 +971,34 @@ pub const SCRATCH_PEARL_2_ENS: usize = 123;
|
||||
/// Slots [127..131): per-branch flatness signal (var_q / (σ² + EPS_DIV), clamped [0,1]).
|
||||
pub const SCRATCH_PEARL_2_FLATNESS: usize = 127;
|
||||
|
||||
/// SP5 Task A4: scratch index base for grad_cosine_sim_kernel cosine_sim[8] output block.
|
||||
/// Slots [131..139): per-param-group gradient direction cosine similarity vs previous step.
|
||||
/// Written by `grad_cosine_sim_update`; consumed by `pearl_4_adam_hparams_update`.
|
||||
pub const SCRATCH_PEARL_4_COSINE: usize = 131;
|
||||
|
||||
/// SP5 Task A4: scratch index base for grad_cosine_sim_kernel l2_norm[8] output block.
|
||||
/// Slots [139..147): per-param-group L2 norm of current-step gradient.
|
||||
/// Written by `grad_cosine_sim_update`; consumed by `pearl_4_adam_hparams_update`.
|
||||
pub const SCRATCH_PEARL_4_L2: usize = 139;
|
||||
|
||||
/// SP5 Task A4: scratch index base for pearl_4_adam_hparams_kernel β1[8] output block.
|
||||
/// Slots [147..155): per-param-group adaptive Adam β1 ∈ [0.85, 0.95].
|
||||
/// Written by `pearl_4_adam_hparams_update`; consumed by apply_pearls_ad_kernel →
|
||||
/// ISV[ADAM_BETA1_BASE..ADAM_BETA1_BASE+8).
|
||||
pub const SCRATCH_PEARL_4_BETA1: usize = 147;
|
||||
|
||||
/// SP5 Task A4: scratch index base for pearl_4_adam_hparams_kernel β2[8] output block.
|
||||
/// Slots [155..163): per-param-group adaptive Adam β2 ∈ [0.99, 0.9995].
|
||||
/// Written by `pearl_4_adam_hparams_update`; consumed by apply_pearls_ad_kernel →
|
||||
/// ISV[ADAM_BETA2_BASE..ADAM_BETA2_BASE+8).
|
||||
pub const SCRATCH_PEARL_4_BETA2: usize = 155;
|
||||
|
||||
/// SP5 Task A4: scratch index base for pearl_4_adam_hparams_kernel ε[8] output block.
|
||||
/// Slots [163..171): per-param-group adaptive Adam ε ∈ [1e-10, 1e-6].
|
||||
/// Written by `pearl_4_adam_hparams_update`; consumed by apply_pearls_ad_kernel →
|
||||
/// ISV[ADAM_EPS_BASE..ADAM_EPS_BASE+8).
|
||||
pub const SCRATCH_PEARL_4_EPS: usize = 163;
|
||||
|
||||
/// SP5 Task A1: scratch index base for q_branch_stats 16-float output block.
|
||||
/// Slots [71..87): mean, max_abs_dev, var, entropy × 4 branches.
|
||||
/// SP4 slot 70 = q_dir_grad_p99; SP5 starts at 71 (no collision).
|
||||
@@ -4095,6 +4147,40 @@ pub struct GpuDqnTrainer {
|
||||
/// No atomicAdd (feedback_no_atomicadd), no CPU compute (feedback_no_cpu_compute_strict).
|
||||
/// Loaded from `pearl_2_budget_kernel.cubin`.
|
||||
pearl_2_budget_kernel: CudaFunction,
|
||||
// ── SP5 Task A4: Pearl 4 per-group Adam β1/β2/ε kernels ─────────────
|
||||
/// SP5 Task A4 (2026-05-01): auxiliary gradient cosine similarity kernel.
|
||||
/// Single-block 8-thread kernel (one thread per SP4 param group).
|
||||
/// Reads `grad_buf [total_params]` + `grad_prev_buf_per_group [total_params]`
|
||||
/// + `group_param_offsets_buf [9]` (prefix sums in element units); computes
|
||||
/// per-group cos_sim = dot(g,g_prev)/(|g|×|g_prev|+EPS_COS) and l2_norm = |g|;
|
||||
/// writes to `producer_step_scratch_buf[131..147)`. Also writebacks g_curr →
|
||||
/// grad_prev_buf_per_group for the next step.
|
||||
/// No atomicAdd (feedback_no_atomicadd), no CPU compute (feedback_no_cpu_compute_strict).
|
||||
/// Loaded from `grad_cosine_sim_kernel.cubin`.
|
||||
grad_cosine_sim_kernel: CudaFunction,
|
||||
/// SP5 Task A4 (2026-05-01): Pearl 4 per-group Adam β1/β2/ε controller kernel.
|
||||
/// Single-block 8-thread kernel (one thread per param group). Reads per-group
|
||||
/// cosine_sim + l2_norm from scratch_buf[131..147); computes
|
||||
/// β1=0.85+0.10×stability ∈ [0.85,0.95],
|
||||
/// β2=0.99+0.0095×stability ∈ [0.99,0.9995],
|
||||
/// ε=clamp(grad_norm×1e-7, 1e-10, 1e-6); writes to scratch_buf[147..171).
|
||||
/// Chained apply_pearls_ad_kernel (24 single-slot launches, ALPHA_META=5e-4)
|
||||
/// smooths via Pearls A+D into ISV[ADAM_BETA1_BASE..ADAM_EPS_BASE+8).
|
||||
/// Structural envelopes (Invariant 1 anchors per spec line 88).
|
||||
/// No atomicAdd, no CPU compute.
|
||||
/// Loaded from `pearl_4_adam_hparams_kernel.cubin`.
|
||||
pearl_4_adam_hparams_kernel: CudaFunction,
|
||||
/// SP5 Task A4 (2026-05-01): previous-step gradient buffer for cosine similarity.
|
||||
/// [total_params f32] mapped-pinned (feedback_no_htod_htoh_only_mapped_pinned).
|
||||
/// Zeroed at construction and at each fold boundary (Pearl A sentinel: cosine_sim=0
|
||||
/// on first step → β1/β2 start at envelope midpoints).
|
||||
pub(crate) grad_prev_buf_per_group: MappedF32Buffer,
|
||||
/// SP5 Task A4 (2026-05-01): param-group boundary prefix sums for cosine-sim kernel.
|
||||
/// [9 i32] with group g in element range [offsets[g], offsets[g+1]).
|
||||
/// Values are in f32-element units (not bytes): divide padded byte offsets by 4.
|
||||
/// Host-written once at construction; static for the lifetime of the trainer.
|
||||
pub(crate) group_param_offsets_buf: MappedI32Buffer,
|
||||
|
||||
/// SP5 Task A1 (2026-05-01): per-branch clip-count buffer for Pearl 1
|
||||
/// headroom controller. [4 i32], one element per branch. Zero-initialized
|
||||
/// at construction; populated by `atoms_update_kernel` in Layer B.
|
||||
@@ -9491,6 +9577,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
(SCRATCH_IDX * 3) as i32,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -9575,6 +9662,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
wiener_offset,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -9734,6 +9822,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
((ATOM_POS_BOUND_BASE - SP4_SLOT_BASE) * 3) as i32,
|
||||
SP4_BRANCH_COUNT as i32,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -9976,6 +10065,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
((isv_idx - SP4_SLOT_BASE) * 3) as i32,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -10169,6 +10259,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
((isv_idx - SP4_SLOT_BASE) * 3) as i32,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -10384,6 +10475,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
((isv_idx - SP4_SLOT_BASE) * 3) as i32,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -10524,6 +10616,7 @@ impl GpuDqnTrainer {
|
||||
isv_dev, isv_idx,
|
||||
wiener_dev, wiener_off,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -10545,6 +10638,7 @@ impl GpuDqnTrainer {
|
||||
isv_dev, isv_entropy,
|
||||
wiener_dev, wiener_entropy,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
// q_var → ISV Q_VAR_PER_BRANCH_BASE + b
|
||||
@@ -10559,6 +10653,7 @@ impl GpuDqnTrainer {
|
||||
isv_dev, isv_var,
|
||||
wiener_dev, wiener_var,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -10643,6 +10738,7 @@ impl GpuDqnTrainer {
|
||||
isv_dev, isv_sigma,
|
||||
wiener_dev, wiener_sigma,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
// SF → ISV SIGMA_FRACTION_BASE + b
|
||||
@@ -10657,6 +10753,7 @@ impl GpuDqnTrainer {
|
||||
isv_dev, isv_sf,
|
||||
wiener_dev, wiener_sf,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -10756,6 +10853,7 @@ impl GpuDqnTrainer {
|
||||
isv_dev, isv_idx,
|
||||
wiener_dev, wiener_off,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -10764,6 +10862,156 @@ impl GpuDqnTrainer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SP5 Task A4 (2026-05-01): launch the two-kernel Pearl 4 chain +
|
||||
/// 24 `apply_pearls_ad_kernel` launches to smooth β1/β2/ε into ISV.
|
||||
///
|
||||
/// Execution sequence (all on `self.stream`):
|
||||
/// 1. `grad_cosine_sim_update` (8 threads, 1 block): reads
|
||||
/// `grad_buf [total_params]` + `grad_prev_buf_per_group [total_params]`
|
||||
/// + `group_param_offsets_buf [9]`; writes per-group cosine_sim[8]
|
||||
/// to scratch[131..139) and l2_norm[8] to scratch[139..147); also
|
||||
/// writes grad_curr → grad_prev_buf_per_group for the next step's comparison.
|
||||
/// 2. `pearl_4_adam_hparams_update` (8 threads, 1 block): reads
|
||||
/// scratch[131..147); writes β1[8]→scratch[147..155),
|
||||
/// β2[8]→scratch[155..163), ε[8]→scratch[163..171).
|
||||
/// 3. 24 `apply_pearls_ad_kernel` launches (ALPHA_META=5e-4, half the
|
||||
/// SP4 default): smooths β1[8]/β2[8]/ε[8] via Pearls A+D into
|
||||
/// ISV[ADAM_BETA1_BASE..ADAM_BETA1_BASE+8),
|
||||
/// ISV[ADAM_BETA2_BASE..ADAM_BETA2_BASE+8),
|
||||
/// ISV[ADAM_EPS_BASE..ADAM_EPS_BASE+8).
|
||||
///
|
||||
/// Pearl 4 is independent of Pearls 1/2/3 outputs — depends only on
|
||||
/// grad_buf state. Can run in any order relative to other SP5 launchers.
|
||||
///
|
||||
/// Structural envelopes (Invariant 1 anchors per spec line 88):
|
||||
/// β1 ∈ [0.85, 0.95]; β2 ∈ [0.99, 0.9995]; ε ∈ [1e-10, 1e-6].
|
||||
/// Fold-boundary reset zeroes grad_prev_buf_per_group (Pearl A sentinel):
|
||||
/// first step cosine_sim=0 → β1=0.85, β2=0.99 (low-stability envelope).
|
||||
/// Layer A only — Adam optimizer consumer migration deferred to Layer B.
|
||||
pub(crate) fn launch_sp5_pearl_4_adam_hparams(&self) -> Result<(), MLError> {
|
||||
use crate::cuda_pipeline::sp5_isv_slots::{
|
||||
ADAM_BETA1_BASE, ADAM_BETA2_BASE, ADAM_EPS_BASE, SP5_SLOT_BASE,
|
||||
};
|
||||
use crate::cuda_pipeline::sp4_wiener_ema::launch_apply_pearls;
|
||||
|
||||
const ALPHA_META_PEARL_4: f32 = 5.0e-4_f32; // half SP4 default → slow β change rate
|
||||
|
||||
let scratch_dev = self.producer_step_scratch_buf.dev_ptr;
|
||||
let isv_dev = self.isv_signals_dev_ptr;
|
||||
let wiener_dev = self.wiener_state_buf.dev_ptr;
|
||||
// Wiener base offset for SP5 slots: SP4_PRODUCER_COUNT * 3 Wiener slots
|
||||
// already consumed; SP5 slots start at offset (SP4_PRODUCER_COUNT * 3).
|
||||
let base_wiener_offset: i32 = (SP4_PRODUCER_COUNT * 3) as i32;
|
||||
|
||||
let grad_buf_ptr = self.ptrs.grad_buf;
|
||||
let grad_prev_ptr = self.grad_prev_buf_per_group.dev_ptr;
|
||||
let group_offsets_ptr = self.group_param_offsets_buf.dev_ptr;
|
||||
let cosine_idx_base = SCRATCH_PEARL_4_COSINE as i32; // 131
|
||||
let l2_norm_idx_base = SCRATCH_PEARL_4_L2 as i32; // 139
|
||||
let beta1_idx_base = SCRATCH_PEARL_4_BETA1 as i32; // 147
|
||||
let beta2_idx_base = SCRATCH_PEARL_4_BETA2 as i32; // 155
|
||||
let eps_idx_base = SCRATCH_PEARL_4_EPS as i32; // 163
|
||||
|
||||
// Step 1: grad_cosine_sim_update — 8 threads, 1 block.
|
||||
// Reads grad_buf + grad_prev_buf_per_group + group_param_offsets;
|
||||
// writes cosine_sim[8]→scratch[131..139) and l2_norm[8]→scratch[139..147).
|
||||
// Also writebacks grad_curr → grad_prev_buf_per_group.
|
||||
let cfg_8t = LaunchConfig {
|
||||
grid_dim: (1, 1, 1),
|
||||
block_dim: (8, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
};
|
||||
unsafe {
|
||||
self.stream
|
||||
.launch_builder(&self.grad_cosine_sim_kernel)
|
||||
.arg(&grad_buf_ptr)
|
||||
.arg(&grad_prev_ptr)
|
||||
.arg(&group_offsets_ptr)
|
||||
.arg(&scratch_dev)
|
||||
.arg(&cosine_idx_base)
|
||||
.arg(&l2_norm_idx_base)
|
||||
.arg(&grad_prev_ptr) // writeback = same buffer
|
||||
.launch(cfg_8t)
|
||||
.map_err(|e| MLError::ModelError(format!("grad_cosine_sim_update launch: {e}")))?;
|
||||
}
|
||||
|
||||
// Step 2: pearl_4_adam_hparams_update — 8 threads, 1 block.
|
||||
// Reads scratch[131..147) (cosine_sim + l2_norm); writes
|
||||
// β1[8]→scratch[147..155), β2[8]→scratch[155..163), ε[8]→scratch[163..171).
|
||||
// Stream-ordered with Step 1 (same stream, no explicit sync needed).
|
||||
let cosine_ptr = scratch_dev + (SCRATCH_PEARL_4_COSINE * std::mem::size_of::<f32>()) as u64;
|
||||
let l2_ptr = scratch_dev + (SCRATCH_PEARL_4_L2 * std::mem::size_of::<f32>()) as u64;
|
||||
unsafe {
|
||||
self.stream
|
||||
.launch_builder(&self.pearl_4_adam_hparams_kernel)
|
||||
.arg(&cosine_ptr)
|
||||
.arg(&l2_ptr)
|
||||
.arg(&scratch_dev)
|
||||
.arg(&beta1_idx_base)
|
||||
.arg(&beta2_idx_base)
|
||||
.arg(&eps_idx_base)
|
||||
.launch(cfg_8t)
|
||||
.map_err(|e| MLError::ModelError(format!("pearl_4_adam_hparams_update launch: {e}")))?;
|
||||
}
|
||||
|
||||
// Step 3: 24 apply_pearls_ad_kernel launches (ALPHA_META=5e-4).
|
||||
// β1[8]: scratch[147..155) → ISV[ADAM_BETA1_BASE..ADAM_BETA1_BASE+8)
|
||||
for g in 0..8_usize {
|
||||
let scratch_idx = (SCRATCH_PEARL_4_BETA1 + g) as i32;
|
||||
let isv_idx = (ADAM_BETA1_BASE + g) as i32;
|
||||
let wiener_off = base_wiener_offset + (isv_idx - SP5_SLOT_BASE as i32) * 3;
|
||||
unsafe {
|
||||
launch_apply_pearls(
|
||||
&self.stream,
|
||||
&self.apply_pearls_ad_kernel,
|
||||
scratch_dev, scratch_idx,
|
||||
isv_dev, isv_idx,
|
||||
wiener_dev, wiener_off,
|
||||
1,
|
||||
ALPHA_META_PEARL_4,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// β2[8]: scratch[155..163) → ISV[ADAM_BETA2_BASE..ADAM_BETA2_BASE+8)
|
||||
for g in 0..8_usize {
|
||||
let scratch_idx = (SCRATCH_PEARL_4_BETA2 + g) as i32;
|
||||
let isv_idx = (ADAM_BETA2_BASE + g) as i32;
|
||||
let wiener_off = base_wiener_offset + (isv_idx - SP5_SLOT_BASE as i32) * 3;
|
||||
unsafe {
|
||||
launch_apply_pearls(
|
||||
&self.stream,
|
||||
&self.apply_pearls_ad_kernel,
|
||||
scratch_dev, scratch_idx,
|
||||
isv_dev, isv_idx,
|
||||
wiener_dev, wiener_off,
|
||||
1,
|
||||
ALPHA_META_PEARL_4,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// ε[8]: scratch[163..171) → ISV[ADAM_EPS_BASE..ADAM_EPS_BASE+8)
|
||||
for g in 0..8_usize {
|
||||
let scratch_idx = (SCRATCH_PEARL_4_EPS + g) as i32;
|
||||
let isv_idx = (ADAM_EPS_BASE + g) as i32;
|
||||
let wiener_off = base_wiener_offset + (isv_idx - SP5_SLOT_BASE as i32) * 3;
|
||||
unsafe {
|
||||
launch_apply_pearls(
|
||||
&self.stream,
|
||||
&self.apply_pearls_ad_kernel,
|
||||
scratch_dev, scratch_idx,
|
||||
isv_dev, isv_idx,
|
||||
wiener_dev, wiener_off,
|
||||
1,
|
||||
ALPHA_META_PEARL_4,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Plan C Phase 2 follow-up A.2 (2026-04-29): launch `q_drift_rate_ema_update`
|
||||
/// — single-thread single-block ISV producer for ISV[Q_DRIFT_RATE_INDEX=129].
|
||||
///
|
||||
@@ -10889,6 +11137,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
(SCRATCH_FIRST * 3) as i32,
|
||||
num_groups as i32,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -10965,6 +11214,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
(SCRATCH_IDX_NEXT_BAR * 3) as i32,
|
||||
2,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -11231,6 +11481,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
(SCRATCH_IDX_UTIL_BASE * 3) as i32,
|
||||
(MOE_NUM_EXPERTS + 1) as i32,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -11555,6 +11806,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
(SCRATCH_IDX_LABEL_SCALE * 3) as i32,
|
||||
1,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -11869,6 +12121,7 @@ impl GpuDqnTrainer {
|
||||
self.wiener_state_buf.dev_ptr,
|
||||
(SCRATCH_FIRST * 3) as i32,
|
||||
4,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -12801,6 +13054,66 @@ impl GpuDqnTrainer {
|
||||
.map_err(|e| MLError::ModelError(format!("SP5 action_offsets_buf alloc: {e}")))?;
|
||||
action_offsets_buf.write_from_slice(&[0_i32, 4, 7, 10]);
|
||||
|
||||
// SP5 Task A4 (2026-05-01): load grad_cosine_sim_kernel (auxiliary
|
||||
// gradient cosine similarity). Single-block 8-thread, one thread per
|
||||
// SP4 param group. Reads grad_buf + grad_prev_buf_per_group.
|
||||
let grad_cosine_sim_kernel = {
|
||||
let module = stream.context().load_cubin(SP5_GRAD_COSINE_SIM_CUBIN.to_vec())
|
||||
.map_err(|e| MLError::ModelError(format!("sp5 grad_cosine_sim cubin load: {e}")))?;
|
||||
module.load_function("grad_cosine_sim_update")
|
||||
.map_err(|e| MLError::ModelError(format!("grad_cosine_sim_update load: {e}")))?
|
||||
};
|
||||
|
||||
// SP5 Task A4 (2026-05-01): load pearl_4_adam_hparams_kernel.
|
||||
// Single-block 8-thread, one thread per SP4 param group. Reads
|
||||
// cosine_sim + l2_norm from scratch; writes β1/β2/ε to scratch.
|
||||
let pearl_4_adam_hparams_kernel = {
|
||||
let module = stream.context().load_cubin(SP5_PEARL_4_ADAM_HPARAMS_CUBIN.to_vec())
|
||||
.map_err(|e| MLError::ModelError(format!("sp5 pearl_4_adam_hparams cubin load: {e}")))?;
|
||||
module.load_function("pearl_4_adam_hparams_update")
|
||||
.map_err(|e| MLError::ModelError(format!("pearl_4_adam_hparams_update load: {e}")))?
|
||||
};
|
||||
|
||||
// SP5 Task A4 (2026-05-01): allocate grad_prev_buf_per_group [total_params f32].
|
||||
// Zeroed at construction; fold-boundary reset zeroes it again (Pearl A sentinel:
|
||||
// zero grad_prev → cosine_sim=0 on first step → β1/β2 start at envelope midpoints).
|
||||
// `total_params` was computed at constructor entry (line ~12337).
|
||||
let grad_prev_buf_per_group = unsafe { MappedF32Buffer::new(total_params) }
|
||||
.map_err(|e| MLError::ModelError(format!("SP5 grad_prev_buf_per_group alloc: {e}")))?;
|
||||
|
||||
// SP5 Task A4 (2026-05-01): allocate group_param_offsets_buf [9 i32].
|
||||
// Prefix sums in f32-element units (not bytes): [offsets[0], ..., offsets[8]]
|
||||
// where group g covers [offsets[g], offsets[g+1]).
|
||||
// Groups match SP4_PARAM_GROUP_COUNT=8:
|
||||
// 0=DqnTrunk [0..trunk_param_count)
|
||||
// 1=DqnValue [trunk_param_count..trunk+value_count)
|
||||
// 2=DqnBranches [trunk+value..trunk+value+branch_count)
|
||||
// 3=Iqn, 4=IqlHigh, 5=IqlLow, 6=Attn, 7=Curiosity
|
||||
// For aux groups 3-7: they are separate grad buffers (not slices of grad_buf)
|
||||
// so their offsets are set to [total_params, total_params] — zero-width range
|
||||
// signals the kernel to skip those groups (thread loop produces dot=0,
|
||||
// norm=0, cosine_sim=0 → β1/β2 snap to stable envelope midpoints).
|
||||
// Only DqnTrunk/Value/Branches (groups 0-2) cover the contiguous main grad_buf.
|
||||
let param_sizes = compute_param_sizes(&config);
|
||||
let trunk_end = (padded_byte_offset(¶m_sizes, 13) / 4) as i32;
|
||||
let value_end = (padded_byte_offset(¶m_sizes, 17) / 4) as i32;
|
||||
let branch_end = (padded_byte_offset(¶m_sizes, 33) / 4) as i32;
|
||||
// Aux groups 3-7 sit outside the contiguous grad_buf slab → zero-width sentinel.
|
||||
let tp = total_params as i32;
|
||||
let group_param_offsets_buf = unsafe { MappedI32Buffer::new(9) }
|
||||
.map_err(|e| MLError::ModelError(format!("SP5 group_param_offsets_buf alloc: {e}")))?;
|
||||
group_param_offsets_buf.write_from_slice(&[
|
||||
0, // group 0 DqnTrunk start
|
||||
trunk_end, // group 1 DqnValue start / DqnTrunk end
|
||||
value_end, // group 2 DqnBranches start / DqnValue end
|
||||
branch_end, // group 3 Iqn start / DqnBranches end
|
||||
tp, // group 4 IqlHigh start — zero-width sentinel for aux groups
|
||||
tp, // group 5 IqlLow start
|
||||
tp, // group 6 Attn start
|
||||
tp, // group 7 Curiosity start
|
||||
tp, // sentinel: offsets[8] = end of group 7
|
||||
]);
|
||||
|
||||
// Plan C Phase 2 follow-up A.2: load q_drift_rate_ema kernel
|
||||
// (cold-path, per-epoch). Single-thread single-block ISV producer
|
||||
// for ISV[Q_DRIFT_RATE_INDEX=129]; consumer is `tau_update_kernel`.
|
||||
@@ -15906,6 +16219,10 @@ impl GpuDqnTrainer {
|
||||
pearl_1_atom_kernel,
|
||||
pearl_3_sigma_kernel,
|
||||
pearl_2_budget_kernel,
|
||||
grad_cosine_sim_kernel,
|
||||
pearl_4_adam_hparams_kernel,
|
||||
grad_prev_buf_per_group,
|
||||
group_param_offsets_buf,
|
||||
atoms_clip_count_buf,
|
||||
action_counts_buf,
|
||||
action_offsets_buf,
|
||||
|
||||
@@ -2399,6 +2399,7 @@ impl GpuExperienceCollector {
|
||||
self.reward_component_pearls_wiener_dev_ptr,
|
||||
(SCRATCH_FIRST * 3) as i32,
|
||||
REWARD_COMPONENT_COUNT as i32,
|
||||
crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META,
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
62
crates/ml/src/cuda_pipeline/grad_cosine_sim_kernel.cu
Normal file
62
crates/ml/src/cuda_pipeline/grad_cosine_sim_kernel.cu
Normal file
@@ -0,0 +1,62 @@
|
||||
// crates/ml/src/cuda_pipeline/grad_cosine_sim_kernel.cu
|
||||
//
|
||||
// SP5 Task A4 auxiliary: per-group gradient cosine similarity.
|
||||
// Computes cos_sim(grad_curr, grad_prev) for each of 8 SP4 param groups.
|
||||
// Also computes per-group L2 norm of grad_curr.
|
||||
// Writes grad_curr direction as grad_prev for the next step.
|
||||
//
|
||||
// Single-block, 8-thread (one per group). Each thread reduces over its
|
||||
// group's parameter range to compute:
|
||||
// cos_sim = (g_curr · g_prev) / (|g_curr| × |g_prev| + EPS_COS)
|
||||
// l2_norm = |g_curr|
|
||||
//
|
||||
// No atomicAdd per feedback_no_atomicadd — one thread per group with
|
||||
// independent writes. grad_prev_buf_writeback is written by the same
|
||||
// thread that reads it (no race).
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
extern "C" __global__ void grad_cosine_sim_update(
|
||||
const float* __restrict__ grad_buf, // [total_params] current step's gradient
|
||||
const float* __restrict__ grad_prev_buf, // [total_params] previous step's gradient
|
||||
const int* __restrict__ group_param_offsets, // [9] prefix sums: group g in [offsets[g], offsets[g+1])
|
||||
float* __restrict__ scratch_out, // cosine_sim[8] at [cosine_idx_base..+8)
|
||||
// l2_norm[8] at [l2_norm_idx_base..+8)
|
||||
int cosine_idx_base,
|
||||
int l2_norm_idx_base,
|
||||
float* __restrict__ grad_prev_buf_writeback // [total_params] overwrite prev with curr for next step
|
||||
) {
|
||||
int g = threadIdx.x;
|
||||
if (blockIdx.x != 0 || g >= 8) return;
|
||||
|
||||
int start = group_param_offsets[g];
|
||||
int end = group_param_offsets[g + 1];
|
||||
|
||||
const float EPS_COS = 1e-12f;
|
||||
float dot = 0.0f, norm_curr_sq = 0.0f, norm_prev_sq = 0.0f;
|
||||
|
||||
for (int i = start; i < end; ++i) {
|
||||
float gc = grad_buf[i];
|
||||
float gp = grad_prev_buf[i];
|
||||
dot += gc * gp;
|
||||
norm_curr_sq += gc * gc;
|
||||
norm_prev_sq += gp * gp;
|
||||
}
|
||||
|
||||
float norm_curr = sqrtf(norm_curr_sq);
|
||||
float norm_prev = sqrtf(norm_prev_sq);
|
||||
float cos_sim = dot / (norm_curr * norm_prev + EPS_COS);
|
||||
|
||||
scratch_out[cosine_idx_base + g] = cos_sim;
|
||||
scratch_out[l2_norm_idx_base + g] = norm_curr;
|
||||
|
||||
// Writeback grad_curr to grad_prev for the next step's comparison.
|
||||
// Same thread that read grad_prev writes back grad_curr — no race.
|
||||
for (int i = start; i < end; ++i) {
|
||||
grad_prev_buf_writeback[i] = grad_buf[i];
|
||||
}
|
||||
|
||||
// Make all writes globally visible before pearl_4_adam_hparams_update
|
||||
// reads scratch_out and before the next step reads grad_prev_buf.
|
||||
__threadfence_system();
|
||||
}
|
||||
54
crates/ml/src/cuda_pipeline/pearl_4_adam_hparams_kernel.cu
Normal file
54
crates/ml/src/cuda_pipeline/pearl_4_adam_hparams_kernel.cu
Normal file
@@ -0,0 +1,54 @@
|
||||
// crates/ml/src/cuda_pipeline/pearl_4_adam_hparams_kernel.cu
|
||||
//
|
||||
// SP5 Task A4: Pearl 4 — per-param-group Adam β1, β2, ε from gradient
|
||||
// direction-stability + L2 norm.
|
||||
// 8-thread single-block (one per param group: DqnTrunk/Value/Branches/IQN/
|
||||
// IqlHigh/IqlLow/Attn/Curiosity).
|
||||
//
|
||||
// Producer signal: per-group cosine similarity of current vs previous-step
|
||||
// gradient direction (written by grad_cosine_sim_kernel). High stability
|
||||
// (cosine→1) → β2 → 0.9995 (long memory); low stability (cosine→0 or →-1)
|
||||
// → β2 → 0.99 (short memory). β1 likewise scales with stability.
|
||||
//
|
||||
// Structural envelopes (Invariant 1 anchors per spec line 88):
|
||||
// β1 ∈ [0.85, 0.95]; β2 ∈ [0.99, 0.9995]; ε ∈ [1e-10, 1e-6]
|
||||
//
|
||||
// THEORETICAL CAVEAT: adaptive β breaks Adam's constant-β convergence
|
||||
// proof. Mitigations:
|
||||
// - Structural β envelopes (Invariant 1 anchors)
|
||||
// - ALPHA_META=5e-4 in apply_pearls_ad_kernel (half the SP4 default → slow change rate)
|
||||
// - Pearl A sentinel bootstrap on fold boundary (cosine_sim=0 → envelope midpoints)
|
||||
// If Layer C destabilization observed, fall-back path: revert to ε-only adaptive.
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
extern "C" __global__ void pearl_4_adam_hparams_update(
|
||||
const float* __restrict__ grad_cosine_sim_per_group, // [8] from grad_cosine_sim_kernel
|
||||
const float* __restrict__ grad_l2_norm_per_group, // [8] from grad_cosine_sim_kernel
|
||||
float* __restrict__ scratch_out,
|
||||
int beta1_idx_base,
|
||||
int beta2_idx_base,
|
||||
int eps_idx_base
|
||||
) {
|
||||
int g = threadIdx.x;
|
||||
if (blockIdx.x != 0 || g >= 8) return;
|
||||
|
||||
float stability = fmaxf(0.0f, fminf(1.0f, grad_cosine_sim_per_group[g]));
|
||||
float grad_norm = grad_l2_norm_per_group[g];
|
||||
|
||||
// Structural envelopes (Invariant 1 anchors per spec line 88).
|
||||
// β1 scales linearly with stability: [0.85, 0.95]
|
||||
float beta1 = 0.85f + 0.10f * stability;
|
||||
// β2 scales linearly with stability: [0.99, 0.9995]
|
||||
float beta2 = 0.99f + 0.0095f * stability;
|
||||
// ε scales with grad norm to maintain numerical headroom; envelope clamp.
|
||||
// grad_norm * 1e-7 maps typical grad norms [1e-3, 10.0] to [1e-10, 1e-6].
|
||||
float eps = fmaxf(1e-10f, fminf(1e-6f, grad_norm * 1e-7f));
|
||||
|
||||
scratch_out[beta1_idx_base + g] = beta1;
|
||||
scratch_out[beta2_idx_base + g] = beta2;
|
||||
scratch_out[eps_idx_base + g] = eps;
|
||||
|
||||
// Make writes globally visible before apply_pearls_ad_kernel consumes them.
|
||||
__threadfence_system();
|
||||
}
|
||||
@@ -146,6 +146,10 @@ pub fn pearls_ad_update(
|
||||
/// typically 1..9. Single-thread device-side loop is correct because each
|
||||
/// slot has its own state — no cross-slot parallelism is meaningful.
|
||||
///
|
||||
/// `alpha_meta`: meta-EMA rate for Wiener variance tracking. SP4 default
|
||||
/// and all SP5 producers except Pearl 4 use [`ALPHA_META`] (1e-3). Pearl 4
|
||||
/// uses 5e-4 (half the default per spec line 89 to limit β change rate).
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// All three buffer pointers MUST be valid device pointers. Index ranges
|
||||
@@ -168,6 +172,7 @@ pub(crate) unsafe fn launch_apply_pearls(
|
||||
wiener_state_dev: u64,
|
||||
wiener_offset_base: i32,
|
||||
n_slots: i32,
|
||||
alpha_meta: f32,
|
||||
) -> Result<(), crate::MLError> {
|
||||
use cudarc::driver::{LaunchConfig, PushKernelArg};
|
||||
|
||||
@@ -197,6 +202,7 @@ pub(crate) unsafe fn launch_apply_pearls(
|
||||
.arg(&wiener_state_dev)
|
||||
.arg(&wiener_offset_base)
|
||||
.arg(&n_slots)
|
||||
.arg(&alpha_meta)
|
||||
.launch(cfg)
|
||||
.map_err(|e| {
|
||||
crate::MLError::ModelError(format!("apply_pearls_ad_kernel launch: {e}"))
|
||||
|
||||
@@ -627,6 +627,30 @@ impl StateResetRegistry {
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "GpuDqnTrainer.wiener_state_buf SP5 block [330 floats = 110 SP5 producers × {sample_var, diff_var, x_lag}] at offsets [213..543). SP5 Pearl D Wiener-EMA state for Task A1's 24 producers (16 Pearl 1 + 8 shared signal). Bulk write_bytes(0) at fold boundary covers the full wiener_state_buf [0..543) (SP4 + SP5 together) so Pearl A's sentinel branch fires on the new fold's first producer launch. Both SP4 and SP5 blocks reset atomically — wiener_state_buf grew from 213 to 543 floats in Task A1.",
|
||||
},
|
||||
// SP5 Task A4: per-group Adam β1/β2/ε ISV slots (Pearl 4).
|
||||
// ISV[226..250) = 24 floats (8 groups × 3 hyperparams).
|
||||
// Pearl A sentinel 0 at fold boundary → first step fires first-observation
|
||||
// replacement (cosine_sim=0 → low-stability envelope midpoints).
|
||||
RegistryEntry {
|
||||
name: "sp5_adam_beta1",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "ISV[ADAM_BETA1_BASE=226..234) — per-group Adam β1 (gradient direction stability). Pearl 4 producer: cosine_sim→[0,1] → β1 = 0.85 + 0.10×stability ∈ [0.85, 0.95]. SP5 Pearl A sentinel 0 at fold boundary (Task A4).",
|
||||
},
|
||||
RegistryEntry {
|
||||
name: "sp5_adam_beta2",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "ISV[ADAM_BETA2_BASE=234..242) — per-group Adam β2 (gradient direction stability). Pearl 4 producer: cosine_sim→[0,1] → β2 = 0.99 + 0.0095×stability ∈ [0.99, 0.9995]. SP5 Pearl A sentinel 0 at fold boundary (Task A4).",
|
||||
},
|
||||
RegistryEntry {
|
||||
name: "sp5_adam_eps",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "ISV[ADAM_EPS_BASE=242..250) — per-group Adam ε (grad L2 norm scaled). Pearl 4 producer: ε = clamp(grad_norm×1e-7, 1e-10, 1e-6). SP5 Pearl A sentinel 0 at fold boundary (Task A4).",
|
||||
},
|
||||
RegistryEntry {
|
||||
name: "sp5_grad_prev_buf",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "GpuDqnTrainer.grad_prev_buf_per_group [total_params f32s] — previous-step gradient direction for cosine similarity computation (Pearl 4). Zero at fold boundary so the new fold's first grad_cosine_sim_update sees cosine_sim=0 (low-stability bootstrap) and Pearl A fires first-observation replacement (Task A4).",
|
||||
},
|
||||
];
|
||||
Self { entries }
|
||||
}
|
||||
|
||||
@@ -3603,6 +3603,13 @@ impl DQNTrainer {
|
||||
if let Err(e) = fused.trainer().launch_sp5_pearl_2_budget() {
|
||||
tracing::warn!(error = %e, "SP5 Pearl 2 producer launch failed");
|
||||
}
|
||||
// SP5 Task A4: pearl_4_adam_hparams → apply_pearls_ad ×24
|
||||
// Reads grad_buf (populated in this step's backward pass) + grad_prev_buf.
|
||||
// No ordering dependency on Pearls 1/2/3 — reads gradient state, not ISV.
|
||||
// Writes ISV[226..250) = β1[8] + β2[8] + ε[8] (ALPHA_META_PEARL_4=5e-4).
|
||||
if let Err(e) = fused.trainer().launch_sp5_pearl_4_adam_hparams() {
|
||||
tracing::warn!(error = %e, "SP5 Pearl 4 producer launch failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ const SP5_PEARL_3_SIGMA_CUBIN: &[u8] =
|
||||
const SP5_PEARL_2_BUDGET_CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/pearl_2_budget_kernel.cubin"));
|
||||
|
||||
const SP5_PEARL_4_ADAM_HPARAMS_CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/pearl_4_adam_hparams_kernel.cubin"));
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn load_pearl_3_sigma_kernel(stream: &Arc<CudaStream>) -> CudaFunction {
|
||||
@@ -55,6 +58,16 @@ fn load_pearl_2_budget_kernel(stream: &Arc<CudaStream>) -> CudaFunction {
|
||||
.expect("load pearl_2_budget_update function")
|
||||
}
|
||||
|
||||
fn load_pearl_4_adam_hparams_kernel(stream: &Arc<CudaStream>) -> CudaFunction {
|
||||
let module = stream
|
||||
.context()
|
||||
.load_cubin(SP5_PEARL_4_ADAM_HPARAMS_CUBIN.to_vec())
|
||||
.expect("load pearl_4_adam_hparams_kernel cubin");
|
||||
module
|
||||
.load_function("pearl_4_adam_hparams_update")
|
||||
.expect("load pearl_4_adam_hparams_update function")
|
||||
}
|
||||
|
||||
fn make_test_stream() -> Arc<CudaStream> {
|
||||
let ctx = CudaContext::new(0).expect("CUDA context — is a GPU available?");
|
||||
ctx.default_stream()
|
||||
@@ -861,3 +874,234 @@ fn pearl_2_budget_sharp_regime_iqn_dominates() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests 7 + 8: pearl_4_adam_hparams_update ──────────────────────────────────
|
||||
|
||||
/// SP5 Task A4 unit test: `pearl_4_adam_hparams_update` with high stability input.
|
||||
///
|
||||
/// Synthetic input: cosine_sim=[1.0; 8] (maximum stability), grad_norm=[1.0; 8].
|
||||
///
|
||||
/// Analytical expected:
|
||||
/// stability = fmaxf(0, fminf(1, cosine_sim)) = 1.0
|
||||
/// β1 = 0.85 + 0.10 × 1.0 = 0.95 (long-memory ceiling)
|
||||
/// β2 = 0.99 + 0.0095 × 1.0 = 0.9995 (long-memory ceiling)
|
||||
/// ε = clamp(1.0 × 1e-7, 1e-10, 1e-6) = 1e-7 → clamped to 1e-7 (within [1e-10, 1e-6])
|
||||
///
|
||||
/// Structural envelopes (Invariant 1 anchors): β1∈[0.85,0.95], β2∈[0.99,0.9995].
|
||||
/// No CPU reference oracle per `feedback_no_cpu_test_fallbacks.md`.
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn pearl_4_adam_hparams_high_stability_yields_long_memory() {
|
||||
// Scratch layout (Pearl 4 outputs):
|
||||
// [0..8) = β1[8] (beta1_idx_base=0)
|
||||
// [8..16) = β2[8] (beta2_idx_base=8)
|
||||
// [16..24) = ε[8] (eps_idx_base=16)
|
||||
const BETA1_BASE: usize = 0;
|
||||
const BETA2_BASE: usize = 8;
|
||||
const EPS_BASE: usize = 16;
|
||||
const SCRATCH_SIZE: usize = 24;
|
||||
const N_GROUPS: usize = 8;
|
||||
|
||||
let stream = make_test_stream();
|
||||
let kernel = load_pearl_4_adam_hparams_kernel(&stream);
|
||||
|
||||
// cosine_sim = 1.0 for all 8 groups → maximum stability.
|
||||
let cosine_buf = unsafe { MappedF32Buffer::new(N_GROUPS) }
|
||||
.expect("alloc cosine_buf");
|
||||
cosine_buf.write_from_slice(&[1.0_f32; N_GROUPS]);
|
||||
|
||||
// grad_norm = 1.0 for all 8 groups → ε = 1e-7 (within envelope).
|
||||
let l2_norm_buf = unsafe { MappedF32Buffer::new(N_GROUPS) }
|
||||
.expect("alloc l2_norm_buf");
|
||||
l2_norm_buf.write_from_slice(&[1.0_f32; N_GROUPS]);
|
||||
|
||||
let scratch_buf = unsafe { MappedF32Buffer::new(SCRATCH_SIZE) }
|
||||
.expect("alloc scratch_buf");
|
||||
|
||||
let cosine_dev = cosine_buf.dev_ptr;
|
||||
let l2_dev = l2_norm_buf.dev_ptr;
|
||||
let scratch_dev = scratch_buf.dev_ptr;
|
||||
let beta1_base_i32 = BETA1_BASE as i32;
|
||||
let beta2_base_i32 = BETA2_BASE as i32;
|
||||
let eps_base_i32 = EPS_BASE as i32;
|
||||
|
||||
unsafe {
|
||||
stream
|
||||
.launch_builder(&kernel)
|
||||
.arg(&cosine_dev)
|
||||
.arg(&l2_dev)
|
||||
.arg(&scratch_dev)
|
||||
.arg(&beta1_base_i32)
|
||||
.arg(&beta2_base_i32)
|
||||
.arg(&eps_base_i32)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (1, 1, 1),
|
||||
block_dim: (8, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
})
|
||||
.expect("launch pearl_4_adam_hparams_update (high stability)");
|
||||
}
|
||||
stream.synchronize().expect("sync after pearl_4_adam_hparams_update (high stability)");
|
||||
|
||||
let scratch = scratch_buf.read_all();
|
||||
|
||||
// Analytical values for stability = 1.0:
|
||||
let expected_beta1: f32 = 0.85 + 0.10 * 1.0; // = 0.95
|
||||
let expected_beta2: f32 = 0.99 + 0.0095 * 1.0; // = 0.9995
|
||||
// ε = clamp(1.0 × 1e-7, 1e-10, 1e-6) = 1e-7
|
||||
let expected_eps: f32 = 1.0_f32 * 1e-7_f32; // within [1e-10, 1e-6]
|
||||
|
||||
for g in 0..N_GROUPS {
|
||||
let beta1 = scratch[BETA1_BASE + g];
|
||||
let beta2 = scratch[BETA2_BASE + g];
|
||||
let eps = scratch[EPS_BASE + g];
|
||||
|
||||
println!("group={g} beta1={beta1:.6} beta2={beta6:.7} eps={eps:.2e}",
|
||||
beta6 = beta2);
|
||||
|
||||
// β1 ceiling: 0.95 ± 1 ULP.
|
||||
let rel_b1 = (beta1 - expected_beta1).abs() / expected_beta1.max(1e-9);
|
||||
assert!(
|
||||
rel_b1 < 1e-5,
|
||||
"group={g}: beta1={beta1:.6} expected={expected_beta1:.6} rel={rel_b1:.2e}"
|
||||
);
|
||||
// β2 ceiling: 0.9995 ± 1 ULP.
|
||||
let rel_b2 = (beta2 - expected_beta2).abs() / expected_beta2.max(1e-9);
|
||||
assert!(
|
||||
rel_b2 < 1e-5,
|
||||
"group={g}: beta2={beta2:.7} expected={expected_beta2:.7} rel={rel_b2:.2e}"
|
||||
);
|
||||
// ε must be within structural envelope.
|
||||
assert!(
|
||||
eps >= 1e-10_f32 && eps <= 1e-6_f32,
|
||||
"group={g}: eps={eps:.2e} outside structural envelope [1e-10, 1e-6]"
|
||||
);
|
||||
let rel_eps = (eps - expected_eps).abs() / expected_eps.max(1e-15);
|
||||
assert!(
|
||||
rel_eps < 1e-4,
|
||||
"group={g}: eps={eps:.2e} expected={expected_eps:.2e} rel={rel_eps:.2e}"
|
||||
);
|
||||
// β1 must be at structural ceiling for maximum stability.
|
||||
assert!(
|
||||
(beta1 - 0.95_f32).abs() < 1e-5,
|
||||
"group={g}: beta1={beta1:.6} should be at ceiling 0.95 for stability=1.0"
|
||||
);
|
||||
// β2 must be at structural ceiling for maximum stability.
|
||||
assert!(
|
||||
(beta2 - 0.9995_f32).abs() < 1e-5,
|
||||
"group={g}: beta2={beta2:.7} should be at ceiling 0.9995 for stability=1.0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// SP5 Task A4 unit test: `pearl_4_adam_hparams_update` with low stability input.
|
||||
///
|
||||
/// Synthetic input: cosine_sim=[0.0; 8] (minimum stability), grad_norm=[0.01; 8].
|
||||
///
|
||||
/// Analytical expected:
|
||||
/// stability = fmaxf(0, fminf(1, 0.0)) = 0.0
|
||||
/// β1 = 0.85 + 0.10 × 0.0 = 0.85 (low-stability floor)
|
||||
/// β2 = 0.99 + 0.0095 × 0.0 = 0.99 (low-stability floor)
|
||||
/// ε = clamp(0.01 × 1e-7, 1e-10, 1e-6) = 1e-9 → clamped to 1e-10
|
||||
///
|
||||
/// No CPU reference oracle per `feedback_no_cpu_test_fallbacks.md`.
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn pearl_4_adam_hparams_low_stability_yields_short_memory() {
|
||||
const BETA1_BASE: usize = 0;
|
||||
const BETA2_BASE: usize = 8;
|
||||
const EPS_BASE: usize = 16;
|
||||
const SCRATCH_SIZE: usize = 24;
|
||||
const N_GROUPS: usize = 8;
|
||||
|
||||
let stream = make_test_stream();
|
||||
let kernel = load_pearl_4_adam_hparams_kernel(&stream);
|
||||
|
||||
// cosine_sim = 0.0 for all 8 groups → minimum stability.
|
||||
let cosine_buf = unsafe { MappedF32Buffer::new(N_GROUPS) }
|
||||
.expect("alloc cosine_buf");
|
||||
cosine_buf.write_from_slice(&[0.0_f32; N_GROUPS]);
|
||||
|
||||
// grad_norm = 0.01 → ε = 0.01 × 1e-7 = 1e-9 → clamped to 1e-10.
|
||||
let l2_norm_buf = unsafe { MappedF32Buffer::new(N_GROUPS) }
|
||||
.expect("alloc l2_norm_buf");
|
||||
l2_norm_buf.write_from_slice(&[0.01_f32; N_GROUPS]);
|
||||
|
||||
let scratch_buf = unsafe { MappedF32Buffer::new(SCRATCH_SIZE) }
|
||||
.expect("alloc scratch_buf");
|
||||
|
||||
let cosine_dev = cosine_buf.dev_ptr;
|
||||
let l2_dev = l2_norm_buf.dev_ptr;
|
||||
let scratch_dev = scratch_buf.dev_ptr;
|
||||
let beta1_base_i32 = BETA1_BASE as i32;
|
||||
let beta2_base_i32 = BETA2_BASE as i32;
|
||||
let eps_base_i32 = EPS_BASE as i32;
|
||||
|
||||
unsafe {
|
||||
stream
|
||||
.launch_builder(&kernel)
|
||||
.arg(&cosine_dev)
|
||||
.arg(&l2_dev)
|
||||
.arg(&scratch_dev)
|
||||
.arg(&beta1_base_i32)
|
||||
.arg(&beta2_base_i32)
|
||||
.arg(&eps_base_i32)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (1, 1, 1),
|
||||
block_dim: (8, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
})
|
||||
.expect("launch pearl_4_adam_hparams_update (low stability)");
|
||||
}
|
||||
stream.synchronize().expect("sync after pearl_4_adam_hparams_update (low stability)");
|
||||
|
||||
let scratch = scratch_buf.read_all();
|
||||
|
||||
// Analytical values for stability = 0.0:
|
||||
let expected_beta1: f32 = 0.85; // low-stability floor
|
||||
let expected_beta2: f32 = 0.99; // low-stability floor
|
||||
// ε = clamp(0.01 × 1e-7, 1e-10, 1e-6) = clamp(1e-9, 1e-10, 1e-6) = 1e-9
|
||||
// Actually 1e-9 is within [1e-10, 1e-6], so no clamping needed.
|
||||
let expected_eps: f32 = (0.01_f32 * 1e-7_f32).clamp(1e-10, 1e-6); // = 1e-9
|
||||
|
||||
for g in 0..N_GROUPS {
|
||||
let beta1 = scratch[BETA1_BASE + g];
|
||||
let beta2 = scratch[BETA2_BASE + g];
|
||||
let eps = scratch[EPS_BASE + g];
|
||||
|
||||
println!("group={g} beta1={beta1:.6} beta2={beta2:.7} eps={eps:.2e}");
|
||||
|
||||
// β1 floor: 0.85 ± 1 ULP.
|
||||
let rel_b1 = (beta1 - expected_beta1).abs() / expected_beta1.max(1e-9);
|
||||
assert!(
|
||||
rel_b1 < 1e-5,
|
||||
"group={g}: beta1={beta1:.6} expected={expected_beta1:.6} rel={rel_b1:.2e}"
|
||||
);
|
||||
// β2 floor: 0.99 ± 1 ULP.
|
||||
let rel_b2 = (beta2 - expected_beta2).abs() / expected_beta2.max(1e-9);
|
||||
assert!(
|
||||
rel_b2 < 1e-5,
|
||||
"group={g}: beta2={beta2:.7} expected={expected_beta2:.7} rel={rel_b2:.2e}"
|
||||
);
|
||||
// ε must be within structural envelope.
|
||||
assert!(
|
||||
eps >= 1e-10_f32 && eps <= 1e-6_f32,
|
||||
"group={g}: eps={eps:.2e} outside structural envelope [1e-10, 1e-6]"
|
||||
);
|
||||
let rel_eps = (eps - expected_eps).abs() / expected_eps.max(1e-15);
|
||||
assert!(
|
||||
rel_eps < 1e-4,
|
||||
"group={g}: eps={eps:.2e} expected={expected_eps:.2e} rel={rel_eps:.2e}"
|
||||
);
|
||||
// β1 must be at structural floor for zero stability.
|
||||
assert!(
|
||||
(beta1 - 0.85_f32).abs() < 1e-5,
|
||||
"group={g}: beta1={beta1:.6} should be at floor 0.85 for stability=0.0"
|
||||
);
|
||||
// β2 must be at structural floor for zero stability.
|
||||
assert!(
|
||||
(beta2 - 0.99_f32).abs() < 1e-5,
|
||||
"group={g}: beta2={beta2:.7} should be at floor 0.99 for stability=0.0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
**Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7.
|
||||
|
||||
SP5 Task A4 — Pearl 4 per-group Adam β1/β2/ε GPU producers (Layer A, 2026-05-01): two new CUDA kernels (`grad_cosine_sim_kernel.cu`, `pearl_4_adam_hparams_kernel.cu`) and one `MappedF32Buffer` (`grad_prev_buf_per_group`) land as Layer A additive producers that feed ISV slots [226..250) with per-param-group Adam hyperparameters. `grad_cosine_sim_update`: single-block 8-thread kernel (one thread per SP4 param group: DqnTrunk/Value/Branches/IQN/IqlHigh/IqlLow/Attn/Curiosity), reads `grad_buf[total_params]` + `grad_prev_buf[total_params]` + `group_param_offsets[9]` (prefix sums in element units); computes `dot = Σ gc·gp`, `norm_curr_sq = Σ gc²`, `norm_prev_sq = Σ gp²` per group; writes `cos_sim[8]→scratch[131..139)` and `l2_norm[8]→scratch[139..147)`; writebacks `grad_curr → grad_prev_buf` for the next step's comparison (same thread — no race). `pearl_4_adam_hparams_update`: single-block 8-thread kernel, reads `cos_sim[8]` + `l2_norm[8]` from scratch; clips stability to `[0, 1]`; computes `β1 = 0.85 + 0.10×stability ∈ [0.85, 0.95]`, `β2 = 0.99 + 0.0095×stability ∈ [0.99, 0.9995]`, `ε = clamp(grad_norm × 1e-7, 1e-10, 1e-6)`; writes `β1[8]→scratch[147..155)`, `β2[8]→scratch[155..163)`, `ε[8]→scratch[163..171)`. Structural envelopes (Invariant 1 anchors per spec): β1∈[0.85, 0.95], β2∈[0.99, 0.9995], ε∈[1e-10, 1e-6]. `launch_sp5_pearl_4_adam_hparams` fires 24 `launch_apply_pearls` calls: β1[8] → ISV[226..234), β2[8] → ISV[234..242), ε[8] → ISV[242..250). Uses `ALPHA_META_PEARL_4=5e-4` (half SP4 default 1e-3) per theoretical-caveat mitigation — slow β change rate reduces instability risk from adaptive β breaking Adam's constant-β convergence proof. Pearl A sentinel 0 at fold boundary fires first-observation replacement (cosine_sim=0 → low-stability envelope endpoints). Aux groups 3-7 (IQN/IqlHigh/IqlLow/Attn/Curiosity) have non-contiguous grad allocations; zero-width sentinel offsets `[tp, tp, tp, tp, tp]` in `group_param_offsets_buf` prevent the kernel inner loop from executing for those groups, yielding cosine_sim=0 → β1/β2 start at low-stability floors. `apply_pearls_kernel.cu` signature gained `float alpha_meta` parameter (migration of all 18 call sites atomic per `feedback_no_partial_refactor.md`: 1 in `gpu_experience_collector.rs`, 17 in `gpu_dqn_trainer.rs`). Buffer growth: `producer_step_scratch_buf` 131→171 slots (SP5_SCRATCH_TOTAL); 5 new scratch constants `SCRATCH_PEARL_4_{COSINE=131, L2=139, BETA1=147, BETA2=155, EPS=163}`. StateResetRegistry gains 4 new FoldReset entries: `sp5_adam_beta1` (ISV[226..234)), `sp5_adam_beta2` (ISV[234..242)), `sp5_adam_eps` (ISV[242..250)), `sp5_grad_prev_buf` (grad_prev_buf_per_group — zero at fold boundary so cosine_sim=0 fires Pearl A bootstrap). Wire-up in training_loop.rs: `launch_sp5_pearl_4_adam_hparams()` called immediately after `launch_sp5_pearl_2_budget()` with `tracing::warn!` on error. Two GPU-only `#[ignore = "requires GPU"]` unit tests: (7) high-stability (cosine_sim=1.0 → β1=0.95, β2=0.9995, ε within envelope); (8) low-stability (cosine_sim=0.0 → β1=0.85, β2=0.99, ε within envelope). No CPU compute, no HtoD/HtoH, no atomicAdd, no stubs, no consumer migration. Touched: `cuda_pipeline/grad_cosine_sim_kernel.cu` (new), `cuda_pipeline/pearl_4_adam_hparams_kernel.cu` (new), `cuda_pipeline/apply_pearls_kernel.cu` (+alpha_meta param), `cuda_pipeline/sp4_wiener_ema.rs` (+alpha_meta arg), `cuda_pipeline/gpu_experience_collector.rs` (+1 call site ALPHA_META arg), `build.rs` (+2 cubin entries), `cuda_pipeline/gpu_dqn_trainer.rs` (SP5_SCRATCH_TOTAL 131→171 + 5 new constants + 2 static cubins + 4 struct fields + constructor allocs + struct initializer + 17 call-site ALPHA_META args + launch method), `trainers/dqn/state_reset_registry.rs` (+4 FoldReset entries), `trainers/dqn/trainer/training_loop.rs` (+5 LOC after Pearl 2 launch), `tests/sp5_producer_unit_tests.rs` (+2 GPU-only tests + cubin constant + loader helper). Hard rules: feedback_no_cpu_compute_strict, feedback_no_atomicadd, feedback_no_htod_htoh_only_mapped_pinned, feedback_no_stubs, feedback_no_partial_refactor (apply_pearls 18 call sites all migrated).
|
||||
|
||||
SP5 Task A3 fix-up — Pearl 2 budget tests now assert sum-to-1 normalization invariant (2026-05-01): code-quality review caught that the two A3 unit tests verified each output (c51, iqn, cql, ens) against its analytical expected value within 1% relative tolerance, but did NOT assert the structural invariant `c51 + iqn + cql + ens ≈ 1.0` that the kernel maintains by construction (`ens = max(0, 1 - iqn - c51 - cql)`). A coefficient typo such as `BASE_IQN = 0.111` instead of `0.11` would produce individually-plausible per-component values that all still pass the relative checks while quietly summing to 0.97 or 1.04. Fix adds `assert!((c51+iqn+cql+ens - 1.0).abs() < 1e-4)` inside both per-branch loops. Touched: `crates/ml/tests/sp5_producer_unit_tests.rs` (+12 LOC). cargo test --no-run clean.
|
||||
|
||||
SP5 Task A3 — Pearl 2 per-branch loss budget GPU producer (Layer A, 2026-05-01): one new CUDA kernel (`pearl_2_budget_kernel.cu`) lands as a Layer A additive producer that feeds ISV slots [190..210) with per-branch loss budget and flatness signals. No loss-budget consumer migration in this commit — Layer B wires consumers. `pearl_2_budget_update`: single-block 4-thread kernel (one thread per branch), reads Q_VAR_PER_BRANCH[b] (Pearl 1's q_branch_stats output, ISV[222..226)) + NOISY_SIGMA[b] (Pearl 3 output, ISV[210..214)); computes `flatness[b] = clamp(var_q[b] / (σ[b]² + EPS_DIV), 0, 1)` where EPS_DIV=1e-8 is an Invariant 1 anchor (numerical stability); derives `budget_iqn = BASE_IQN + (1 - flatness) * (1 - BASE_IQN)` (IQN dominates when Q is flat relative to noise), `budget_c51 = flatness * (1 - BASE_IQN) * BASE_C51_SHARE` (C51 takes proportional remainder when Q is well-separated), `budget_cql = 0` (gated by existing regime_stability allocator), `budget_ens = max(0, 1 - iqn - c51 - cql)`; writes 20 floats to scratch[111..131) (SCRATCH_PEARL_2_C51=111, SCRATCH_PEARL_2_IQN=115, SCRATCH_PEARL_2_CQL=119, SCRATCH_PEARL_2_ENS=123, SCRATCH_PEARL_2_FLATNESS=127). BASE_IQN=0.11 and BASE_C51_SHARE=0.84/(1-BASE_IQN) are Invariant 1 anchors (preserve SP4-baseline budget under non-flat regime). `launch_sp5_pearl_2_budget` fires 20 `launch_apply_pearls` calls: 5 output groups × 4 branches each, mapping to ISV[BUDGET_C51_BASE=190..194), ISV[BUDGET_IQN_BASE=194..198), ISV[BUDGET_CQL_BASE=198..202), ISV[BUDGET_ENS_BASE=202..206), ISV[FLATNESS_BASE=206..210). Wiener offset formula: `base_wiener_offset + (isv_slot - SP5_SLOT_BASE) * 3` where `base_wiener_offset = SP4_PRODUCER_COUNT * 3 = 213`. Dependencies: Pearl 2 must chain AFTER Pearl 1 (reads Q_VAR_PER_BRANCH) AND AFTER Pearl 3 (reads NOISY_SIGMA). Buffer growth: `producer_step_scratch_buf` 111→131 slots (SP5_SCRATCH_TOTAL updated 111→131; 5 new module-level constants SCRATCH_PEARL_2_{C51,IQN,CQL,ENS,FLATNESS}). wiener_state_buf already at 543 (A1 sized for entire SP5 block — no further growth). StateResetRegistry gains 5 new FoldReset entries: sp5_budget_c51 (ISV[190..194)), sp5_budget_iqn (ISV[194..198)), sp5_budget_cql (ISV[198..202)), sp5_budget_ens (ISV[202..206)), sp5_flatness (ISV[206..210)) — Pearl A sentinel 0 at fold boundary triggers first-observation replacement on new fold's first launch. Wire-up in training_loop.rs: `launch_sp5_pearl_2_budget()` called immediately after `launch_sp5_pearl_3_sigma()` with `tracing::warn!(error = %e, ...)` on error. Two GPU-only `#[ignore = "requires GPU"]` unit tests: (5) flat-Q regime (σ=1.0, var_q=2.0 → flatness=1.0 → iqn=0.11, c51≈0.84); (6) sharp-Q regime (σ=2.0, var_q=0.04 → flatness≈0.01 → iqn>0.9, c51≈0.0084). No CPU compute, no HtoD/HtoH, no atomicAdd, no stubs, no consumer migration. Touched: `cuda_pipeline/pearl_2_budget_kernel.cu` (new), `build.rs` (+1 cubin entry), `cuda_pipeline/gpu_dqn_trainer.rs` (static cubin + SP5_SCRATCH_TOTAL 111→131 + 5 new scratch constants + docstring update + struct field + cubin loader + struct initializer + launch method), `trainers/dqn/state_reset_registry.rs` (+5 FoldReset entries), `trainers/dqn/trainer/training_loop.rs` (+5 LOC after Pearl 3 launch), `tests/sp5_producer_unit_tests.rs` (+2 GPU-only tests + cubin loader helper). Hard rules: feedback_no_cpu_compute_strict, feedback_no_atomicadd, feedback_no_stubs, feedback_no_partial_refactor (all consumers of existing budget allocator untouched).
|
||||
|
||||
Reference in New Issue
Block a user