feat(sp15-wave4.2 / 3.5.4.b): cuRAND Kaiming-He weight reset for plasticity injection
Phase 3.5.4 (e0e0abfb2) landed the trigger + warm-up tracker but
deferred the actual weight reset (kernel accepted advantage_head_weights
+ n_weights but no-op'd via (void) cast). Wave 4.2 lands the real
reset.
When DD_PERSISTENCE exceeds threshold AND not yet fired this fold,
the kernel now resets the last 10% of advantage-head weights to
Kaiming-He init: Normal(0, sqrt(2/fan_in)) sampled via cuRAND
curand_normal() with per-thread state initialized from a host-passed
seed (Option A: per-call curand_init(seed, tid, 0, &state) for full
determinism — bit-identical samples for identical (seed, tid) pairs).
Single kernel, two phases:
1. Every thread independently re-evaluates fire_now from the same
ISV reads (DD_PERSISTENCE / threshold / fired_flag); the trigger
condition is a pure function of these reads so all threads
converge without cross-block synchronisation. Block 0 / thread 0
also runs the trigger + warm-bars decrement (single-thread ISV
write path).
2. If fire_now: each tid < reset_count writes Kaiming-He sample to
advantage_head_weights[reset_start + tid]. Per-thread independent
write, no atomic, no reduction (feedback_no_atomicadd clean).
New launcher params: fan_in (i32), seed (u64). Grid:
((n_weights/10 + 255) / 256).max(1) x [256, 1, 1]. The .max(1) floor
ensures block 0 always exists even when n_weights/10 == 0.
cuRAND device functions (curand_init, curand_normal) are inlined into
the cubin by nvcc from <curand_kernel.h> in the standard CUDA toolkit
include path — no host-side cuRAND linker dependency required, no
build.rs link change needed.
Oracle test plasticity_injection_kernel_resets_last_10pct_kaiming_he
verifies: (a) ISV[fired] flipped 0->1, (b) ISV[warm] = m_warm - 1,
(c) first 90% bit-identical to 1.0, (d) last 10% all moved off 1.0,
(e) sample mean |mean| < 0.05, (f) sample std within +-20% of
sqrt(2/fan_in), (g) determinism re-check produces bit-identical
samples for identical seed.
3 existing trigger tests migrated to the new launcher signature; the
debounced + no-fire variants additionally assert weight-stability
(early-out path skips the reset region).
Atomic per feedback_no_partial_refactor: kernel + launcher + 4 tests
+ audit doc + build.rs comment + 2 docstrings land together.
Eliminates Phase 3.5.4 deferred consumer per feedback_wire_everything_up
(the (void) casts are gone).
Action-selection consumer wiring (Phase 3.5.4.c) remains the separate
follow-up per the established Phase 3.5.X pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1036,20 +1036,24 @@ fn main() {
|
||||
// (3.5.4) post-amendment-2 fix: fire when DD_PERSISTENCE
|
||||
// exceeds threshold AND PLASTICITY_FIRED_THIS_FOLD == 0 →
|
||||
// set fired flag, set warm-bars counter to M_warm (default
|
||||
// 200), [DEFERRED: reset last 10% of advantage-head weights];
|
||||
// per-bar warm-bars decrement; consumer-wiring follow-up reads
|
||||
// max(COOLDOWN_BARS_REMAINING, PLASTICITY_WARM_BARS_REMAINING)
|
||||
// and forces Hold while > 0. Phase 3.5.4 lands kernel +
|
||||
// launcher + 3 ISV anchor seeds + 3 registry entries + 3
|
||||
// dispatch arms only; the action-selection two-step Flat +
|
||||
// cooldown wiring AND the actual Kaiming-He weight-reset are
|
||||
// both deferred to a Phase 3.5.4.b follow-up atomic commit per
|
||||
// `feedback_no_partial_refactor.md` (matching Phase 1.1-1.5 +
|
||||
// 3.1-3.5.3 precedent — kernel + launcher verify in isolation
|
||||
// first via the GPU oracle tests below). The kernel signature
|
||||
// accepts the advantage_head_weights pointer + n_weights param
|
||||
// so the follow-up commit is purely additive on the kernel
|
||||
// body, not on the launcher contract.
|
||||
// 200), AND reset last 10% of advantage-head weights to
|
||||
// Kaiming-He init (`Normal(0, sqrt(2/fan_in))` sampled via
|
||||
// cuRAND `curand_normal()`); per-bar warm-bars decrement;
|
||||
// consumer-wiring follow-up reads max(COOLDOWN_BARS_REMAINING,
|
||||
// PLASTICITY_WARM_BARS_REMAINING) and forces Hold while > 0.
|
||||
// Phase 3.5.4 (2026-05-06) landed kernel + launcher + 3 ISV
|
||||
// anchor seeds + 3 registry entries + 3 dispatch arms with
|
||||
// the weight-reset deferred (kernel accepted
|
||||
// advantage_head_weights + n_weights but no-op'd via (void)
|
||||
// cast); Wave 4.2 / Phase 3.5.4.b (2026-05-07) lands the
|
||||
// actual cuRAND Kaiming-He reset, extending the launcher
|
||||
// signature with `fan_in: i32` and `seed: u64`. The kernel
|
||||
// includes `<curand_kernel.h>` for device-side
|
||||
// `curand_init`/`curand_normal` (inlined into the cubin by
|
||||
// nvcc — no host-side cuRAND linker dependency required).
|
||||
// The action-selection two-step Flat + cooldown consumer
|
||||
// wiring remains a separate follow-up (Phase 3.5.4.c) per
|
||||
// `feedback_no_partial_refactor.md`.
|
||||
"plasticity_injection_kernel.cu",
|
||||
// SP15 Phase 3.5.5 (2026-05-06): recovery curriculum in PER —
|
||||
// per-step DD_TRAJECTORY_DECREASING proxy. Replaces non-existent
|
||||
|
||||
@@ -1705,13 +1705,16 @@ pub fn launch_sp15_cooldown(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SP15 Phase 3.5.4 (2026-05-06): cubin slot for the plasticity
|
||||
/// injection trigger + warm-up tracker kernel. TWO-STEP recovery per
|
||||
/// spec §9.2 (3.5.4) post-amendment-2 fix:
|
||||
/// SP15 Phase 3.5.4 (2026-05-06) + Wave 4.2 / Phase 3.5.4.b
|
||||
/// (2026-05-07): cubin slot for the plasticity injection trigger +
|
||||
/// warm-up tracker + cuRAND Kaiming-He weight-reset kernel. TWO-STEP
|
||||
/// recovery per spec §9.2 (3.5.4) post-amendment-2 fix:
|
||||
/// 1. Fire when `DD_PERSISTENCE > PLASTICITY_PERSISTENCE_THRESHOLD`
|
||||
/// AND `PLASTICITY_FIRED_THIS_FOLD == 0` → set fired flag, set
|
||||
/// warm-bars counter to M_warm (default 200), [DEFERRED: reset
|
||||
/// last 10% of advantage-head weights to Kaiming-He init].
|
||||
/// warm-bars counter to M_warm (default 200), AND reset the
|
||||
/// trailing 10% of advantage-head weights to Kaiming-He init
|
||||
/// (`Normal(0, sqrt(2/fan_in))` sampled via cuRAND
|
||||
/// `curand_normal()`; gain=√2 for ReLU/GLU activations).
|
||||
/// 2. Per-bar warm-bars decrement; consumer-wiring follow-up reads
|
||||
/// `max(COOLDOWN_BARS_REMAINING, PLASTICITY_WARM_BARS_REMAINING)`
|
||||
/// and forces Hold while > 0 (the OR-gate combining cooldown +
|
||||
@@ -1722,47 +1725,65 @@ pub fn launch_sp15_cooldown(
|
||||
/// `dd_asymmetric_reward_kernel.cu` / 3.5.3 `cooldown_kernel.cu`
|
||||
/// single-kernel-per-cubin pattern.
|
||||
///
|
||||
/// Phase 3.5.4 lands kernel + launcher + 3 ISV constructor seeds + 3
|
||||
/// state-reset registry entries + 3 dispatch arms only; the action-
|
||||
/// selection two-step Flat + cooldown wiring AND the actual Kaiming-
|
||||
/// He weight-reset are both deferred to a Phase 3.5.4.b follow-up
|
||||
/// atomic commit per `feedback_no_partial_refactor.md` (matching
|
||||
/// Phase 1.1-1.5 + 3.1-3.5.3 precedent — kernel + launcher verify in
|
||||
/// isolation first via the GPU oracle tests below). The kernel
|
||||
/// signature accepts the advantage_head_weights pointer + n_weights
|
||||
/// param so the follow-up commit is purely additive on the kernel
|
||||
/// body, not on the launcher contract.
|
||||
/// Phase 3.5.4 landed kernel + launcher + 3 ISV constructor seeds +
|
||||
/// 3 state-reset registry entries + 3 dispatch arms with the weight-
|
||||
/// reset deferred (kernel accepted `advantage_head_weights +
|
||||
/// n_weights` but no-op'd via `(void)` cast). Wave 4.2 / Phase
|
||||
/// 3.5.4.b lands the actual cuRAND Kaiming-He reset, extending the
|
||||
/// launcher signature with `fan_in: i32` and `seed: u64` and
|
||||
/// promoting the kernel grid from single-thread/single-block to a
|
||||
/// 256-thread per-element grid for the weight write. The action-
|
||||
/// selection two-step Flat + cooldown consumer wiring remains a
|
||||
/// separate follow-up (Phase 3.5.4.c) per `feedback_no_partial_refactor.md`.
|
||||
pub static SP15_PLASTICITY_INJECTION_CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/plasticity_injection_kernel.cubin"));
|
||||
|
||||
/// SP15 Phase 3.5.4 (2026-05-06): launcher for the plasticity
|
||||
/// injection trigger + warm-up tracker kernel. Free function (matches
|
||||
/// `launch_sp15_dd_penalty` / `launch_sp15_regret_signal` /
|
||||
/// `launch_sp15_hold_floor` / `launch_sp15_dd_asymmetric_reward` /
|
||||
/// `launch_sp15_cooldown` precedent — single kernel, single cubin,
|
||||
/// single launcher) so unit/oracle tests can drive the kernel
|
||||
/// directly without the trainer struct; production callers invoke
|
||||
/// the same launcher.
|
||||
/// SP15 Phase 3.5.4 (2026-05-06) + Wave 4.2 / Phase 3.5.4.b
|
||||
/// (2026-05-07): launcher for the plasticity injection trigger +
|
||||
/// warm-up tracker + cuRAND Kaiming-He weight-reset kernel. Free
|
||||
/// function (matches `launch_sp15_dd_penalty` /
|
||||
/// `launch_sp15_regret_signal` / `launch_sp15_hold_floor` /
|
||||
/// `launch_sp15_dd_asymmetric_reward` / `launch_sp15_cooldown`
|
||||
/// precedent — single kernel, single cubin, single launcher) so
|
||||
/// unit/oracle tests can drive the kernel directly without the
|
||||
/// trainer struct; production callers invoke the same launcher.
|
||||
///
|
||||
/// `isv` MUST be the ISV bus (≥ `SP15_SLOT_END=443` f32 slots — slot
|
||||
/// 404 is read for DD_PERSISTENCE; slot 437 is read for
|
||||
/// PLASTICITY_PERSISTENCE_THRESHOLD; slots 436 + 438 are read-modify-
|
||||
/// written for PLASTICITY_FIRED_THIS_FOLD + PLASTICITY_WARM_BARS_
|
||||
/// REMAINING). `advantage_head_weights` MUST point to at least
|
||||
/// `n_weights` writable f32 slots — but the weight-reset is DEFERRED
|
||||
/// to Phase 3.5.4.b so the kernel body currently does NOT touch the
|
||||
/// buffer; the parameter is wired through the launcher contract so
|
||||
/// the follow-up commit is purely additive on the kernel body.
|
||||
/// `n_weights` writable f32 slots — Wave 4.2 lands the actual
|
||||
/// Kaiming-He reset of the trailing `n_weights / 10` slots when the
|
||||
/// kernel fires.
|
||||
///
|
||||
/// `m_warm` is the warm-up bar count to set on fire (default 200 per
|
||||
/// spec §9.2 (3.5.4)). Single thread, single block (mirrors
|
||||
/// `cooldown_kernel` / `dd_asymmetric_reward_kernel` / `hold_floor_kernel`
|
||||
/// — no reduction).
|
||||
/// spec §9.2 (3.5.4)). `fan_in` is the input dimension of the
|
||||
/// advantage head's last Linear layer (the Kaiming-He gain
|
||||
/// denominator — `stddev = sqrt(2 / fan_in)`); production callers
|
||||
/// derive it from the trainer's network config. `seed` is a
|
||||
/// per-call deterministic value (e.g. derived from a trainer RNG
|
||||
/// state or `SystemTime::now()` nanos) — the kernel re-initialises
|
||||
/// per-thread `curandState`s from `(seed, sequence=tid, offset=0)`
|
||||
/// every launch; the same `(seed, tid)` pair always yields the same
|
||||
/// sample.
|
||||
///
|
||||
/// Grid: `[((n_weights / 10 + 255) / 256), 1, 1]` × `[256, 1, 1]`.
|
||||
/// Block 0 / thread 0 owns the trigger + warm-bars decrement (single-
|
||||
/// thread, mirrors `cooldown_kernel` / `dd_asymmetric_reward_kernel`
|
||||
/// / `hold_floor_kernel`); the remaining threads race-free write the
|
||||
/// per-element Kaiming-He samples (each thread writes a unique
|
||||
/// `advantage_head_weights[reset_start + tid]`, no atomic, no
|
||||
/// reduction). When `n_weights / 10 == 0` (oracle edge case) the
|
||||
/// grid degenerates to `[1, 1, 1]` so the trigger still runs.
|
||||
pub fn launch_sp15_plasticity_injection(
|
||||
stream: &Arc<CudaStream>,
|
||||
isv: cudarc::driver::sys::CUdeviceptr,
|
||||
advantage_head_weights: cudarc::driver::sys::CUdeviceptr,
|
||||
n_weights: i32,
|
||||
m_warm: f32,
|
||||
fan_in: i32,
|
||||
seed: u64,
|
||||
) -> Result<(), MLError> {
|
||||
let module = stream
|
||||
.context()
|
||||
@@ -1775,6 +1796,13 @@ pub fn launch_sp15_plasticity_injection(
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
"load plasticity_injection_kernel function: {e}"
|
||||
)))?;
|
||||
// Grid sized so every weight in the trailing 10% gets a thread.
|
||||
// `block_dim = 256` matches the codebase's prevailing 1-D
|
||||
// element-wise launch shape. `grid_dim` always ≥ 1 so block 0
|
||||
// thread 0 always runs the trigger, even when `n_weights / 10 == 0`
|
||||
// (oracle edge case where the test passes a tiny weight buffer).
|
||||
let reset_count = (n_weights / 10).max(0);
|
||||
let grid_x = ((reset_count + 255) / 256).max(1) as u32;
|
||||
unsafe {
|
||||
stream
|
||||
.launch_builder(&kernel)
|
||||
@@ -1782,9 +1810,11 @@ pub fn launch_sp15_plasticity_injection(
|
||||
.arg(&advantage_head_weights)
|
||||
.arg(&n_weights)
|
||||
.arg(&m_warm)
|
||||
.arg(&fan_in)
|
||||
.arg(&seed)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (1, 1, 1),
|
||||
block_dim: (1, 1, 1),
|
||||
grid_dim: (grid_x, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
})
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// crates/ml/src/cuda_pipeline/plasticity_injection_kernel.cu
|
||||
//
|
||||
// SP15 Phase 3.5.4 — plasticity injection trigger + warm-up tracker.
|
||||
// SP15 Wave 4.2 / Phase 3.5.4.b — cuRAND Kaiming-He weight reset (now wired).
|
||||
//
|
||||
// TWO-STEP recovery per spec §9.2 (3.5.4) post-amendment-2 fix:
|
||||
// 1. On fire: set PLASTICITY_FIRED_THIS_FOLD = 1.0, set
|
||||
// PLASTICITY_WARM_BARS_REMAINING = M_warm, [DEFERRED: reset last
|
||||
// 10% of advantage-head weights to Kaiming-He init (gain=√2 for
|
||||
// ReLU/GLU activations) via cuRAND]. The action-selection layer
|
||||
// (consumer-wiring follow-up) reads the
|
||||
// PLASTICITY_WARM_BARS_REMAINING = M_warm, AND reset the last 10%
|
||||
// of advantage-head weights to Kaiming-He init (gain=√2 for
|
||||
// ReLU/GLU activations) via cuRAND `curand_normal()`. The
|
||||
// action-selection layer (consumer-wiring follow-up) reads the
|
||||
// PLASTICITY_FIRED_THIS_FOLD == 1 flag transition AND emits Flat
|
||||
// that bar.
|
||||
// 2. Per-bar decrement of PLASTICITY_WARM_BARS_REMAINING; the
|
||||
@@ -16,27 +17,35 @@
|
||||
// PLASTICITY_WARM_BARS_REMAINING)` to force Hold while either
|
||||
// counter > 0.
|
||||
//
|
||||
// **Weight-reset is DEFERRED** to a Phase 3.5.4.b follow-up. This
|
||||
// commit lands:
|
||||
// - Fire/debounce trigger logic (PLASTICITY_FIRED_THIS_FOLD gates
|
||||
// re-fire within the same fold; reset to 0.0 at fold boundary
|
||||
// re-arms the next fold).
|
||||
// - Per-bar warm-bars decrement (every kernel call, regardless of
|
||||
// fire).
|
||||
// - The advantage_head_weights pointer + n_weights param wired
|
||||
// through the launcher signature (kernel accepts but does not
|
||||
// modify them yet — same `(void)foo` no-op pattern used elsewhere
|
||||
// for deferred parameter plumbing).
|
||||
// Wave 4.2 lands the previously-deferred Kaiming-He weight reset:
|
||||
//
|
||||
// Why deferred: weight-reset requires Kaiming-He init via cuRAND, and
|
||||
// accurate targeting of "last 10% of advantage-head weights" requires
|
||||
// plumbing specific param-buffer pointers (mag/ord/urg head Linear
|
||||
// layer slices) that aren't available at this kernel's call site
|
||||
// without trainer-internal access. Establishing the kernel signature +
|
||||
// flag-tracking now keeps the follow-up commit purely additive on the
|
||||
// weight-reset side, mirroring the Phase 1.1-1.5 + 3.1-3.5.3 precedent
|
||||
// of kernel + launcher landing first and consumer wiring following
|
||||
// atomically.
|
||||
// - Reset region: the last 10% of `advantage_head_weights`
|
||||
// (`reset_count = n_weights / 10`, addressed via offset
|
||||
// `advantage_head_weights + n_weights - reset_count`). Resetting
|
||||
// the trailing slice keeps the upstream feature-extraction trunk
|
||||
// intact while re-initialising the head's last layer where the
|
||||
// drawdown-correlated gradients have presumably saturated. This
|
||||
// matches the spec §9.2 (3.5.4) "last 10%" prescription.
|
||||
// - Init formula: Kaiming-He for ReLU/GLU activations,
|
||||
// `weights[i] ~ Normal(0, sqrt(2 / fan_in))` (gain = sqrt(2)).
|
||||
// `fan_in` is supplied by the launcher (host computes it from the
|
||||
// trainer's network config; for oracle tests the test sets it to
|
||||
// a representative value).
|
||||
// - Per-thread cuRAND state via `curand_init(seed, tid, 0, &state)`
|
||||
// + `curand_normal(&state)` × stddev. `seed` is a host-passed
|
||||
// `unsigned long long`; the launcher derives it deterministically
|
||||
// from a trainer RNG state (or `SystemTime` nanos in the oracle
|
||||
// tests). The same (seed, tid) pair always produces the same
|
||||
// sample, satisfying determinism for the oracle test's mean/std
|
||||
// assertions.
|
||||
// - Grid: multi-block, `[((reset_count + 255) / 256), 1, 1]` ×
|
||||
// `[256, 1, 1]`. Block 0 / thread 0 still runs the trigger +
|
||||
// warm-bars decrement (the only ISV-write code path); all other
|
||||
// threads independently re-evaluate the same fire condition from
|
||||
// the same ISV source-of-truth (ISV state is read at kernel-launch
|
||||
// time, the trigger condition is a pure function of those reads,
|
||||
// so every thread computes the same `fire_now` without any
|
||||
// cross-block synchronisation).
|
||||
//
|
||||
// Slot reads/writes (literal indices to keep this kernel self-contained;
|
||||
// the canonical names live in `crates/ml/src/cuda_pipeline/sp15_isv_slots.rs`
|
||||
@@ -44,50 +53,60 @@
|
||||
// indices via compile-time `assert_eq!` regression checks):
|
||||
// ISV[404] DD_PERSISTENCE (read)
|
||||
// ISV[437] PLASTICITY_PERSISTENCE_THRESHOLD (read)
|
||||
// ISV[436] PLASTICITY_FIRED_THIS_FOLD (read-modify-write)
|
||||
// ISV[438] PLASTICITY_WARM_BARS_REMAINING (read-modify-write)
|
||||
// ISV[436] PLASTICITY_FIRED_THIS_FOLD (read-modify-write — block 0 thread 0 only)
|
||||
// ISV[438] PLASTICITY_WARM_BARS_REMAINING (read-modify-write — block 0 thread 0 only)
|
||||
//
|
||||
// Hard rules:
|
||||
//
|
||||
// * `feedback_no_atomicadd` — single-thread, single-block, pure
|
||||
// scalar arithmetic; no reductions, no `atomicAdd`.
|
||||
// * `feedback_no_partial_refactor` — kernel + launcher land first;
|
||||
// consumer wiring (action-selection two-step Flat+cooldown
|
||||
// integration via `effective_cooldown =
|
||||
// max(COOLDOWN_BARS_REMAINING, PLASTICITY_WARM_BARS_REMAINING)`)
|
||||
// deferred to a follow-up atomic commit per the established Phase
|
||||
// 1.1-1.5 / 3.1-3.5.3 precedent. Weight-reset is also deferred
|
||||
// explicitly — the launcher signature accepts the pointer + count
|
||||
// so the follow-up is purely additive on the kernel body, not on
|
||||
// the launcher contract.
|
||||
// * `feedback_no_stubs` — kernel returns the actual updated state
|
||||
// (flag transition + warm-bars decrement); the deferred weight-
|
||||
// reset is a no-op on the same call rather than a stubbed
|
||||
// placeholder return. The three oracle tests verify fire-on-
|
||||
// threshold-crossing, debounce-within-fold, and below-threshold-
|
||||
// no-fire behaviors.
|
||||
// * `feedback_no_atomicadd` — weight-reset is per-thread independent
|
||||
// (each thread writes a unique element of `advantage_head_weights`);
|
||||
// no reductions, no `atomicAdd`. The trigger logic remains
|
||||
// single-thread (block 0 / thread 0) so its ISV writes are
|
||||
// race-free without atomics.
|
||||
// * `feedback_no_partial_refactor` — kernel + launcher signature
|
||||
// change + 4 oracle tests (3 existing migrated + 1 new) land
|
||||
// atomically in this commit (Wave 4.2); consumer wiring (action
|
||||
// selection forces Hold via `effective_cooldown =
|
||||
// max(COOLDOWN_BARS_REMAINING, PLASTICITY_WARM_BARS_REMAINING)`
|
||||
// remains the separate follow-up tracked under Phase 3.5.4.c.
|
||||
// * `feedback_no_stubs` — kernel performs the actual Kaiming-He
|
||||
// reset; no `(void)` casts remain. Wave 4.2 closes the Phase
|
||||
// 3.5.4-deferred weight-reset NO-OP per
|
||||
// `feedback_wire_everything_up.md`.
|
||||
// * `feedback_isv_for_adaptive_bounds` — PLASTICITY_PERSISTENCE_
|
||||
// THRESHOLD is ISV-tracked (slot 437); the producer driving it
|
||||
// from the running mean of dd_persistence_bars is deferred but
|
||||
// the kernel reads it rather than a hardcoded literal so the
|
||||
// follow-up swap is a no-op at the consumer.
|
||||
//
|
||||
// Single-thread, single-block (mirrors `cooldown_kernel` /
|
||||
// `dd_asymmetric_reward_kernel` / `hold_floor_kernel` /
|
||||
// `regret_signal_kernel` / `dd_penalty_kernel` — no reduction).
|
||||
// * `feedback_no_cpu_compute_strict` — cuRAND init + sampling are
|
||||
// GPU-resident (`curand_init` / `curand_normal` are device-only
|
||||
// functions inlined by nvcc into the cubin from
|
||||
// `<curand_kernel.h>`).
|
||||
// * `pearl_no_host_branches_in_captured_graph` — `seed` is a kernel
|
||||
// argument (a `u64` value, not a host-side branch); the launcher
|
||||
// itself can be invoked from inside a graph capture region
|
||||
// because the kernel-launch shape (grid/block) is fixed at the
|
||||
// call (a function of `n_weights / 10` which is also a launch
|
||||
// argument, not a host-conditional).
|
||||
|
||||
#include <curand_kernel.h>
|
||||
|
||||
extern "C" __global__ void plasticity_injection_kernel(
|
||||
float* __restrict__ isv,
|
||||
float* __restrict__ advantage_head_weights, /* deferred: weight-reset NO-OP for now */
|
||||
float* __restrict__ advantage_head_weights,
|
||||
int n_weights,
|
||||
float m_warm /* default 200 bars */
|
||||
float m_warm, /* default 200 bars */
|
||||
int fan_in, /* Kaiming gain denominator */
|
||||
unsigned long long seed /* cuRAND seed */
|
||||
) {
|
||||
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
||||
|
||||
// Every thread independently reads the ISV state to determine
|
||||
// `fire_now`. The trigger condition is a pure function of those
|
||||
// reads, so all threads converge on the same answer without any
|
||||
// cross-block synchronisation. The same reads also drive block 0
|
||||
// thread 0's ISV-write path below.
|
||||
const float persistence = isv[/*DD_PERSISTENCE_INDEX*/ 404];
|
||||
const float threshold = isv[/*PLASTICITY_PERSISTENCE_THRESHOLD_INDEX*/ 437];
|
||||
const float fired = isv[/*PLASTICITY_FIRED_THIS_FOLD_INDEX*/ 436];
|
||||
float warm = isv[/*PLASTICITY_WARM_BARS_REMAINING_INDEX*/ 438];
|
||||
|
||||
// Trigger: persistence exceeds threshold AND not yet fired this
|
||||
// fold. PLASTICITY_FIRED_THIS_FOLD is reset to 0.0 at fold boundary
|
||||
@@ -95,31 +114,58 @@ extern "C" __global__ void plasticity_injection_kernel(
|
||||
// re-arming the gate for the next fold. The `< 0.5f` half-bit
|
||||
// tolerance check matches the existing `cooldown_remaining <= 0.0f`
|
||||
// float-equality-with-tolerance pattern in `cooldown_kernel`.
|
||||
if (persistence > threshold && fired < 0.5f) {
|
||||
isv[/*PLASTICITY_FIRED_THIS_FOLD_INDEX*/ 436] = 1.0f;
|
||||
warm = m_warm;
|
||||
const bool fire_now = (persistence > threshold) && (fired < 0.5f);
|
||||
|
||||
// Weight-reset DEFERRED to Phase 3.5.4.b — kernel param plumbed
|
||||
// through but no-op. Follow-up: Kaiming-He reset of last 10% of
|
||||
// advantage-head weights via cuRAND with gain=√2 for ReLU/GLU
|
||||
// activations. The `(void)foo` cast pattern matches the Rust
|
||||
// `_ = foo` convention used elsewhere in this codebase for
|
||||
// deferred parameter plumbing.
|
||||
(void)advantage_head_weights;
|
||||
(void)n_weights;
|
||||
// Block 0 / thread 0 owns the ISV writes (fired flag + warm-bars
|
||||
// counter). Single-thread to mirror the cooldown_kernel /
|
||||
// dd_asymmetric_reward_kernel / hold_floor_kernel / regret_signal
|
||||
// / dd_penalty_kernel pattern — no reduction, no atomic.
|
||||
if (threadIdx.x == 0 && blockIdx.x == 0) {
|
||||
float warm = isv[/*PLASTICITY_WARM_BARS_REMAINING_INDEX*/ 438];
|
||||
|
||||
if (fire_now) {
|
||||
isv[/*PLASTICITY_FIRED_THIS_FOLD_INDEX*/ 436] = 1.0f;
|
||||
warm = m_warm;
|
||||
}
|
||||
|
||||
// Per-bar decrement of warm-bars (every call, regardless of
|
||||
// fire). The trigger above runs first so the same call that
|
||||
// fires the gate also immediately consumes one bar of warm-up
|
||||
// — bias downward by 1 is acceptable per the test's [198, 200]
|
||||
// tolerance and avoids the bookkeeping branch needed to skip
|
||||
// the decrement on the trip step (mirrors `cooldown_kernel`'s
|
||||
// identical trigger-then-decrement sequence).
|
||||
if (warm > 0.0f) {
|
||||
warm -= 1.0f;
|
||||
}
|
||||
isv[/*PLASTICITY_WARM_BARS_REMAINING_INDEX*/ 438] = warm;
|
||||
|
||||
__threadfence_system();
|
||||
}
|
||||
|
||||
// Per-bar decrement of warm-bars (every call, regardless of fire).
|
||||
// The trigger above runs first so the same call that fires the gate
|
||||
// also immediately consumes one bar of warm-up — bias downward by 1
|
||||
// is acceptable per the test's [198, 200] tolerance and avoids the
|
||||
// bookkeeping branch needed to skip the decrement on the trip step
|
||||
// (mirrors `cooldown_kernel`'s identical trigger-then-decrement
|
||||
// sequence).
|
||||
if (warm > 0.0f) {
|
||||
warm -= 1.0f;
|
||||
}
|
||||
isv[/*PLASTICITY_WARM_BARS_REMAINING_INDEX*/ 438] = warm;
|
||||
// Weight reset: last 10% of advantage-head weights via Kaiming-He
|
||||
// sampling. Each thread writes a unique element so no atomic /
|
||||
// reduction is required. Threads beyond `reset_count` early-out;
|
||||
// when `fire_now == false` every thread early-outs and the kernel
|
||||
// is a pure trigger/decrement (the previous Phase 3.5.4 behaviour).
|
||||
if (!fire_now) return;
|
||||
|
||||
__threadfence_system();
|
||||
const int reset_count = n_weights / 10;
|
||||
if (reset_count <= 0) return;
|
||||
|
||||
const int reset_start = n_weights - reset_count;
|
||||
const int tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (tid >= reset_count) return;
|
||||
|
||||
// Per-thread cuRAND state: (seed, sequence=tid, offset=0). Same
|
||||
// (seed, tid) pair always produces the same sample, satisfying
|
||||
// determinism for oracle-test mean/std assertions. `fan_in` is the
|
||||
// input dimension of the advantage head's last Linear layer; for
|
||||
// ReLU/GLU activations the Kaiming-He gain is sqrt(2).
|
||||
curandState state;
|
||||
curand_init(seed, (unsigned long long)tid, 0ULL, &state);
|
||||
|
||||
const float gain = sqrtf(2.0f);
|
||||
const float stddev = gain / sqrtf((float)fan_in);
|
||||
advantage_head_weights[reset_start + tid] = curand_normal(&state) * stddev;
|
||||
}
|
||||
|
||||
@@ -1141,10 +1141,12 @@ mod gpu {
|
||||
/// reads the fired flag transition AND emits Flat that bar, then
|
||||
/// forces Hold while
|
||||
/// `max(COOLDOWN_BARS_REMAINING, PLASTICITY_WARM_BARS_REMAINING) > 0`.
|
||||
/// Weight-reset deferred to Phase 3.5.4.b — kernel param plumbed
|
||||
/// through but no-op for now (the test passes a non-zero
|
||||
/// advantage_head_weights buffer to verify the launcher accepts it
|
||||
/// without modifying the contents).
|
||||
/// Wave 4.2 / Phase 3.5.4.b: kernel now also performs the
|
||||
/// Kaiming-He weight-reset; this trigger-only test passes the
|
||||
/// extra `fan_in` + `seed` launcher params and asserts the
|
||||
/// trigger behaviour (fired flag + warm-bars). The dedicated
|
||||
/// `plasticity_injection_kernel_resets_last_10pct_kaiming_he`
|
||||
/// test covers the weight-reset numerics.
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn plasticity_fires_when_persistence_exceeds_threshold() {
|
||||
@@ -1164,9 +1166,10 @@ mod gpu {
|
||||
isv_init[PLASTICITY_WARM_BARS_REMAINING_INDEX] = 0.0;
|
||||
isv_buf.write_from_slice(&isv_init);
|
||||
|
||||
// Empty advantage-head weights buffer for now (weight-reset
|
||||
// deferred to Phase 3.5.4.b — kernel takes the pointer but
|
||||
// does not modify contents).
|
||||
// Wave 4.2 weight-reset is exercised in detail by the
|
||||
// dedicated Kaiming-He oracle below; here we just provide a
|
||||
// 64-element buffer so the launcher can run (n_weights / 10 =
|
||||
// 6 trailing slots get re-sampled).
|
||||
let weights_buf = unsafe { MappedF32Buffer::new(64) }
|
||||
.expect("alloc MappedF32Buffer for advantage_head_weights");
|
||||
weights_buf.write_from_slice(&vec![1.0f32; 64]);
|
||||
@@ -1177,6 +1180,8 @@ mod gpu {
|
||||
weights_buf.dev_ptr,
|
||||
/*n_weights*/ 64,
|
||||
/*m_warm*/ 200.0,
|
||||
/*fan_in*/ 256,
|
||||
/*seed*/ 0xCAFE_BABE_DEAD_BEEF_u64,
|
||||
)
|
||||
.expect("launch plasticity_injection_kernel (fires)");
|
||||
stream
|
||||
@@ -1238,6 +1243,8 @@ mod gpu {
|
||||
weights_buf.dev_ptr,
|
||||
64,
|
||||
200.0,
|
||||
/*fan_in*/ 256,
|
||||
/*seed*/ 0xDEAD_BEEF_F00D_CAFE_u64,
|
||||
)
|
||||
.expect("launch plasticity_injection_kernel (debounced)");
|
||||
stream
|
||||
@@ -1256,6 +1263,17 @@ mod gpu {
|
||||
(warm - 49.0).abs() < 1e-9,
|
||||
"PLASTICITY_WARM_BARS_REMAINING = {warm}, expected 49 (decremented from 50, no re-fire)"
|
||||
);
|
||||
|
||||
// Wave 4.2: when debounced, the kernel returns early before
|
||||
// the weight-reset region, so all 64 weights must remain
|
||||
// exactly 1.0 (no Kaiming-He sample written).
|
||||
let weights_host = weights_buf.read_all();
|
||||
for (i, w) in weights_host.iter().enumerate() {
|
||||
assert!(
|
||||
(w - 1.0).abs() < 1e-9,
|
||||
"weights[{i}] = {w}, expected 1.0 (debounced — kernel must skip weight-reset)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// SP15 Phase 3.5.4 — plasticity injection does NOT fire when
|
||||
@@ -1293,6 +1311,8 @@ mod gpu {
|
||||
weights_buf.dev_ptr,
|
||||
64,
|
||||
200.0,
|
||||
/*fan_in*/ 256,
|
||||
/*seed*/ 0xFEED_FACE_BAAD_F00D_u64,
|
||||
)
|
||||
.expect("launch plasticity_injection_kernel (no-fire)");
|
||||
stream
|
||||
@@ -1310,6 +1330,189 @@ mod gpu {
|
||||
"PLASTICITY_WARM_BARS_REMAINING = {}, expected 0.0 (no fire → no warm-up set; the `if (warm > 0)` guard prevents underflow when already 0)",
|
||||
isv_host[PLASTICITY_WARM_BARS_REMAINING_INDEX]
|
||||
);
|
||||
|
||||
// Wave 4.2: when no-fire, the kernel returns early before the
|
||||
// weight-reset region, so all 64 weights must remain exactly
|
||||
// 1.0 (no Kaiming-He sample written).
|
||||
let weights_host = weights_buf.read_all();
|
||||
for (i, w) in weights_host.iter().enumerate() {
|
||||
assert!(
|
||||
(w - 1.0).abs() < 1e-9,
|
||||
"weights[{i}] = {w}, expected 1.0 (no-fire — kernel must skip weight-reset)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// SP15 Wave 4.2 / Phase 3.5.4.b — when the plasticity injection
|
||||
/// gate fires, the trailing 10% of `advantage_head_weights` must
|
||||
/// be re-initialised to Kaiming-He samples
|
||||
/// (`Normal(0, sqrt(2/fan_in))`, gain=√2 for ReLU/GLU
|
||||
/// activations) drawn via cuRAND `curand_normal()`. The first 90%
|
||||
/// of the buffer must remain bit-identical to its pre-launch
|
||||
/// value (1.0) — the kernel writes element `[reset_start + tid]`
|
||||
/// only.
|
||||
///
|
||||
/// Distribution checks (with `n_weights = 1280` → `reset_count =
|
||||
/// 128`, `fan_in = 256`, expected `stddev = sqrt(2/256) ≈
|
||||
/// 0.08839`):
|
||||
/// - Per-element diff from 1.0: every reset slot must have
|
||||
/// changed (probability of an exact-1.0 sample is essentially
|
||||
/// zero — Kaiming stddev ≈ 0.088 means 1.0 is ~11.3 σ out, and
|
||||
/// anyway the kernel writes the *sample*, not the original).
|
||||
/// - Mean of reset region: |mean| < 0.05 (sampling-noise
|
||||
/// tolerance for n=128 from a Normal with stddev≈0.088 — the
|
||||
/// true SE of the mean is `0.088 / sqrt(128) ≈ 0.0078`, so
|
||||
/// `0.05` is a comfortable ~6σ envelope around 0).
|
||||
/// - Std of reset region: within ±20% of `sqrt(2/fan_in)`
|
||||
/// (n=128 sample std has ~6% relative SE; ±20% is comfortable
|
||||
/// headroom).
|
||||
/// - ISV[PLASTICITY_FIRED_THIS_FOLD_INDEX] flipped 0 → 1.
|
||||
/// - ISV[PLASTICITY_WARM_BARS_REMAINING_INDEX] = m_warm − 1
|
||||
/// (per-bar decrement runs in the same launch as the trigger
|
||||
/// per the cooldown_kernel trigger-then-decrement convention).
|
||||
///
|
||||
/// Determinism: a fixed seed (`42`) drives `curand_init(seed,
|
||||
/// tid, 0, &state)` per thread; re-running this test always
|
||||
/// produces identical samples.
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn plasticity_injection_kernel_resets_last_10pct_kaiming_he() {
|
||||
use ml::cuda_pipeline::sp15_isv_slots::{
|
||||
DD_PERSISTENCE_INDEX, PLASTICITY_FIRED_THIS_FOLD_INDEX,
|
||||
PLASTICITY_PERSISTENCE_THRESHOLD_INDEX, PLASTICITY_WARM_BARS_REMAINING_INDEX,
|
||||
};
|
||||
|
||||
let stream = make_test_stream();
|
||||
|
||||
// ISV: ensure the trigger fires — large persistence, small
|
||||
// threshold, fired flag clear, warm-bars 0.
|
||||
let isv_buf = unsafe { MappedF32Buffer::new(ISV_LEN) }
|
||||
.expect("alloc MappedF32Buffer for isv bus");
|
||||
let mut isv_init = vec![0.0f32; ISV_LEN];
|
||||
isv_init[DD_PERSISTENCE_INDEX] = 1_000.0;
|
||||
isv_init[PLASTICITY_PERSISTENCE_THRESHOLD_INDEX] = 1.0;
|
||||
isv_init[PLASTICITY_FIRED_THIS_FOLD_INDEX] = 0.0;
|
||||
isv_init[PLASTICITY_WARM_BARS_REMAINING_INDEX] = 0.0;
|
||||
isv_buf.write_from_slice(&isv_init);
|
||||
|
||||
// Weight buffer: n_weights = 1280, reset_count = 128 (10%).
|
||||
// Pre-fill every slot with 1.0 so we can detect both
|
||||
// "unchanged" (first 90%) and "changed" (last 10%) by exact
|
||||
// float comparison + sample-statistic checks.
|
||||
const N_WEIGHTS: usize = 1280;
|
||||
const FAN_IN: i32 = 256;
|
||||
const SEED: u64 = 42;
|
||||
const M_WARM: f32 = 200.0;
|
||||
let weights_buf = unsafe { MappedF32Buffer::new(N_WEIGHTS) }
|
||||
.expect("alloc MappedF32Buffer for advantage_head_weights");
|
||||
weights_buf.write_from_slice(&vec![1.0f32; N_WEIGHTS]);
|
||||
|
||||
ml::cuda_pipeline::gpu_dqn_trainer::launch_sp15_plasticity_injection(
|
||||
&stream,
|
||||
isv_buf.dev_ptr,
|
||||
weights_buf.dev_ptr,
|
||||
N_WEIGHTS as i32,
|
||||
M_WARM,
|
||||
FAN_IN,
|
||||
SEED,
|
||||
)
|
||||
.expect("launch plasticity_injection_kernel (kaiming-he)");
|
||||
stream
|
||||
.synchronize()
|
||||
.expect("synchronize after plasticity_injection_kernel launch (kaiming-he)");
|
||||
|
||||
// ISV side-effects: fired flipped to 1, warm = M_warm - 1.
|
||||
let isv_host = isv_buf.read_all();
|
||||
assert!(
|
||||
(isv_host[PLASTICITY_FIRED_THIS_FOLD_INDEX] - 1.0).abs() < 1e-9,
|
||||
"PLASTICITY_FIRED_THIS_FOLD = {}, expected 1.0 after fire",
|
||||
isv_host[PLASTICITY_FIRED_THIS_FOLD_INDEX]
|
||||
);
|
||||
let warm = isv_host[PLASTICITY_WARM_BARS_REMAINING_INDEX];
|
||||
assert!(
|
||||
(warm - (M_WARM - 1.0)).abs() < 1e-6,
|
||||
"PLASTICITY_WARM_BARS_REMAINING = {warm}, expected {} (M_warm - 1, per-bar decrement runs same call)",
|
||||
M_WARM - 1.0
|
||||
);
|
||||
|
||||
// Weight-reset side-effects.
|
||||
let weights_host = weights_buf.read_all();
|
||||
let reset_count: usize = N_WEIGHTS / 10; // 128
|
||||
let reset_start: usize = N_WEIGHTS - reset_count; // 1152
|
||||
|
||||
// First 90% must be bit-identical to the pre-launch value.
|
||||
for (i, w) in weights_host[..reset_start].iter().enumerate() {
|
||||
assert!(
|
||||
(w - 1.0).abs() < 1e-9,
|
||||
"weights[{i}] = {w}, expected 1.0 (outside reset region — kernel must not touch)"
|
||||
);
|
||||
}
|
||||
|
||||
// Last 10%: every slot must have moved off 1.0 (a Kaiming
|
||||
// stddev≈0.088 makes the sentinel 1.0 ~11σ out, so an exact
|
||||
// 1.0 sample is effectively zero-probability).
|
||||
let resets = &weights_host[reset_start..];
|
||||
for (i, w) in resets.iter().enumerate() {
|
||||
assert!(
|
||||
(w - 1.0).abs() > 1e-6,
|
||||
"weights[{}] = {w}, expected resampled value ≠ 1.0",
|
||||
reset_start + i
|
||||
);
|
||||
}
|
||||
|
||||
// Mean of resets ≈ 0 within sampling-noise envelope.
|
||||
let n = resets.len() as f64;
|
||||
let mean: f64 = resets.iter().map(|&w| w as f64).sum::<f64>() / n;
|
||||
assert!(
|
||||
mean.abs() < 0.05,
|
||||
"Kaiming-reset region mean = {mean:.6}, expected |mean| < 0.05 (n={n}, true SE ~ 0.0078)"
|
||||
);
|
||||
|
||||
// Std of resets ≈ sqrt(2/fan_in) within ±20%.
|
||||
let expected_std = (2.0_f64 / FAN_IN as f64).sqrt();
|
||||
let var: f64 = resets
|
||||
.iter()
|
||||
.map(|&w| (w as f64 - mean).powi(2))
|
||||
.sum::<f64>()
|
||||
/ (n - 1.0);
|
||||
let std = var.sqrt();
|
||||
assert!(
|
||||
(std - expected_std).abs() / expected_std < 0.20,
|
||||
"Kaiming-reset region std = {std:.6}, expected {expected_std:.6} ± 20%"
|
||||
);
|
||||
|
||||
// Determinism re-check: same (seed, tid) on a fresh buffer
|
||||
// produces bit-identical samples (catches accidental
|
||||
// entropy sources like clock-derived seeds inside the
|
||||
// kernel). Re-launch with the same seed against a fresh
|
||||
// 1.0-filled buffer — but note ISV state must also be reset
|
||||
// (fired=0) so the trigger re-fires.
|
||||
weights_buf.write_from_slice(&vec![1.0f32; N_WEIGHTS]);
|
||||
isv_init[PLASTICITY_FIRED_THIS_FOLD_INDEX] = 0.0;
|
||||
isv_init[PLASTICITY_WARM_BARS_REMAINING_INDEX] = 0.0;
|
||||
isv_buf.write_from_slice(&isv_init);
|
||||
ml::cuda_pipeline::gpu_dqn_trainer::launch_sp15_plasticity_injection(
|
||||
&stream,
|
||||
isv_buf.dev_ptr,
|
||||
weights_buf.dev_ptr,
|
||||
N_WEIGHTS as i32,
|
||||
M_WARM,
|
||||
FAN_IN,
|
||||
SEED, // same seed
|
||||
)
|
||||
.expect("launch plasticity_injection_kernel (determinism re-check)");
|
||||
stream
|
||||
.synchronize()
|
||||
.expect("synchronize after plasticity_injection_kernel launch (determinism)");
|
||||
let resets_2 = weights_buf.read_all();
|
||||
for i in 0..reset_count {
|
||||
let a = resets[i];
|
||||
let b = resets_2[reset_start + i];
|
||||
assert!(
|
||||
(a - b).abs() < 1e-9,
|
||||
"non-deterministic Kaiming sample at tid={i}: first={a}, second={b}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// SP15 Phase 3.5.5 — DD_TRAJECTORY_DECREASING fires when dd_pct
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user