Files
foxhunt/crates/ml-alpha/cuda/bucket_transition_kernels.cu
jgrusewski afb6d73396 refactor(per-horizon): N_HORIZONS 5→3 — bucket-coupled CUDA kernels
Five CUDA kernels + their Rust caller migrated atomically to prevent
silent memory layout corruption between Rust-side bucket geometry
(MAX_BUCKET_DIM=96, BUCKET_DIM_K=[43,43,42], BUCKET_CHANNEL_OFFSET=
[0,43,86,128]) and kernel-side hardcoded constants.

Kernels:
- bucket_transition_kernels.cu: N_HORIZONS 5→3, MAX_BUCKET_DIM 28→96,
  BUCKET_DIM_LAST 28→42, BUCKET_OFFSETS {0,25,50,75,100,128}→{0,43,86,128};
  bucket_assign_kernel quintile→tercile rewire.
- cfc_step_per_branch.cu: defines + doc.
- heads_block_diagonal_fwd.cu: same; REDUCE_PAD 32→128 (next pow2 ≥ 96).
- multi_horizon_heads.cu: N_HORIZONS_H 5→3 covers fwd, bwd, batched, and
  the GRN/2-layer variants via the single define + shared mem [N_HORIZONS_H]
  + loop bounds.
- output_smoothness.cu: OS_N_HORIZONS 5→3.

Rust caller (silent-corruption fix found during audit):
- crates/ml-alpha/src/cfc/step.rs:135,192 had local hardcoded
  N_HORIZONS=5 / MAX_BUCKET_DIM=28 constants — replaced with
  crate::heads::N_HORIZONS / crate::cfc::bucket_routing::MAX_BUCKET_DIM
  (single source of truth via the Rust SoT constants).

cargo build -p ml-alpha: all 5 cubins rebuild PASS.
GPU oracle tests on RTX 3050 sm_86: 5/19 pass; 14 failures are test-side
fixtures hardcoding old 5×28 layout — owned by SDD Task 8 (not regression).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:26:37 +02:00

541 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// bucket_transition_kernels.cu — Phase 1→2 transition kernels.
//
// All-on-device per feedback_no_htod_htoh_only_mapped_pinned. No atomicAdd
// per feedback_no_atomicadd; reductions use warp-shuffle. Static tercile
// boundaries for HIDDEN_DIM=128 with N_HORIZONS=3: [0, 43, 86, 128]
// (post-2026-05-22 horizon-rebase to 3 buckets, matches Rust-side
// `crates/ml-alpha/src/cfc/bucket_routing.rs::BUCKET_CHANNEL_OFFSET`).
//
// MAX_BUCKET_DIM=96 matches the Rust-side `MAX_BUCKET_DIM` and is sized
// for the upper bound of ISV-driven τ-assignment concentration (~75% of
// channels into one bucket under extreme τ distributions).
#define HIDDEN_DIM 128
#define N_HORIZONS 3
#define BUCKET_DIM_LAST 42 // last bucket absorbs HIDDEN_DIM - 2*43 = 42
#define MAX_BUCKET_DIM 96
// Static bucket boundaries (could also be __constant__ memory).
// Tercile cut points: 43, 86, 128 (sum = HIDDEN_DIM = 128).
__device__ const unsigned int BUCKET_OFFSETS[N_HORIZONS + 1] = {0, 43, 86, 128};
// ─────────────────────────────────────────────────────────────────────
// tau_sort_kernel: bitonic-merge sort of cfc.tau values.
//
// Launch: 1 block × 32 threads, shared_mem_bytes >= HIDDEN_DIM * 8 bytes
// (4 for tau key, 4 for original index value).
//
// Sorts ascending. Output: sorted_indices_d[p] = original channel index
// whose tau is the p-th smallest.
//
// Algorithm: bitonic merge sort over 128 elements, single block, 4 passes
// of 32 threads cooperating. Each thread handles 4 elements per pass.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void tau_sort_kernel(
const float* __restrict__ tau, // [HIDDEN_DIM]
unsigned int* __restrict__ sorted_indices // [HIDDEN_DIM]
) {
extern __shared__ float smem[];
float* keys = smem; // [HIDDEN_DIM]
unsigned int* vals = (unsigned int*)(smem + HIDDEN_DIM); // [HIDDEN_DIM]
int tid = threadIdx.x;
// Each thread loads 4 elements (128 / 32 = 4)
for (int i = 0; i < 4; ++i) {
int idx = tid + i * 32;
keys[idx] = tau[idx];
vals[idx] = (unsigned int)idx;
}
__syncthreads();
// Bitonic sort over 128 elements.
for (int k = 2; k <= HIDDEN_DIM; k <<= 1) {
for (int j = k >> 1; j > 0; j >>= 1) {
for (int i = 0; i < 4; ++i) {
int idx = tid + i * 32;
int partner = idx ^ j;
if (partner > idx) {
bool ascending = ((idx & k) == 0);
bool swap = ascending ? (keys[idx] > keys[partner]) : (keys[idx] < keys[partner]);
if (swap) {
float tk = keys[idx]; keys[idx] = keys[partner]; keys[partner] = tk;
unsigned int tv = vals[idx]; vals[idx] = vals[partner]; vals[partner] = tv;
}
}
}
__syncthreads();
}
}
// Write sorted_indices.
for (int i = 0; i < 4; ++i) {
int idx = tid + i * 32;
sorted_indices[idx] = vals[idx];
}
}
// ─────────────────────────────────────────────────────────────────────
// bucket_assign_kernel: from sorted_indices, write bucket_id per channel.
//
// Launch: 1 block × HIDDEN_DIM threads.
// Each thread handles one channel. Looks up its rank (position in sorted)
// and assigns bucket id via static tercile boundaries (post-2026-05-22
// N_HORIZONS=3 rebase: cuts at rank 43 and 86 for [43, 43, 42]).
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void bucket_assign_kernel(
const unsigned int* __restrict__ sorted_indices, // [HIDDEN_DIM]
unsigned char* __restrict__ bucket_id_per_channel // [HIDDEN_DIM]
) {
int tid = threadIdx.x;
if (tid >= HIDDEN_DIM) return;
// tid is a sorted-rank position; sorted_indices[tid] is the original channel
// at this rank. Assign bucket = tercile of rank.
unsigned char bucket = 0;
if (tid >= 86) bucket = 2;
else if (tid >= 43) bucket = 1;
bucket_id_per_channel[sorted_indices[tid]] = bucket;
}
// ─────────────────────────────────────────────────────────────────────
// bucket_iqr_kernel: per-bucket Q1 and Q3 of tau values.
//
// Launch: N_HORIZONS blocks × 32 threads. Each block computes Q1, Q3 for
// its bucket via warp-shuffle reduction.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void bucket_iqr_kernel(
const float* __restrict__ tau, // [HIDDEN_DIM]
const unsigned int* __restrict__ sorted_indices, // [HIDDEN_DIM]
float* __restrict__ q1_per_bucket, // [N_HORIZONS]
float* __restrict__ q3_per_bucket // [N_HORIZONS]
) {
int bucket = blockIdx.x;
int bucket_start = BUCKET_OFFSETS[bucket];
int bucket_end = BUCKET_OFFSETS[bucket + 1];
int bucket_size = bucket_end - bucket_start;
int q1_idx = bucket_start + bucket_size / 4;
int q3_idx = bucket_start + (3 * bucket_size) / 4;
if (threadIdx.x == 0) {
// tau values are already in sorted-rank order via sorted_indices.
q1_per_bucket[bucket] = tau[sorted_indices[q1_idx]];
q3_per_bucket[bucket] = tau[sorted_indices[q3_idx]];
}
}
// ─────────────────────────────────────────────────────────────────────
// channels_in_bucket_kernel: build the bucket → channel lookup table.
//
// ALPHA fix (2026-05-21): replaces `tau_reorder_kernel`. Per spec §2.3
// the original reorder approach grouped τ values by bucket position, but
// downstream kernels (`cfc_step_per_branch_fwd`, `tau_clamp_kernel`,
// `heads_w_skip_*_kernel`) all read using ORIGINAL channel indices —
// the reorder caused systematic layout mismatches.
//
// New approach: keep tau_all_d, W_in, W_rec, b, heads_w_skip in their
// ORIGINAL channel layout. At transition, materialise a lookup table
// `channels_in_bucket[k][i]` = original channel index of the i-th
// channel assigned to bucket k. Per-branch kernels iterate
// channels_in_bucket[branch][0..bucket_dim_k[branch]] to find the
// original channels in each bucket.
//
// Layout: `channels_in_bucket` is [N_HORIZONS × MAX_BUCKET_DIM] flat
// row-major. Sentinel value `0xFFFFFFFF` (UINT32_MAX) marks positions
// beyond `bucket_dim_k[k]` (the last bucket has 28 of 28 positions
// occupied; earlier buckets have 25 of 28).
//
// Launch: 1 block × HIDDEN_DIM threads, shared_mem_bytes = N_HORIZONS × 4
// for the per-bucket cursors. Single-thread sequential write (tid==0)
// keeps the cursor increments race-free without atomicAdd per
// feedback_no_atomicadd.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void channels_in_bucket_kernel(
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
unsigned int* __restrict__ channels_in_bucket // [N_HORIZONS × MAX_BUCKET_DIM] flat
) {
__shared__ unsigned int cursor[N_HORIZONS];
int tid = threadIdx.x;
// Init per-bucket cursors to 0.
if (tid < N_HORIZONS) {
cursor[tid] = 0;
}
__syncthreads();
// Fill all output slots with the sentinel value first (parallel across
// tid). For HIDDEN_DIM=128 ≥ N_HORIZONS*MAX_BUCKET_DIM=140 we use a
// simple stride loop: each thread covers a few output positions.
const int total_slots = N_HORIZONS * MAX_BUCKET_DIM;
for (int idx = tid; idx < total_slots; idx += blockDim.x) {
channels_in_bucket[idx] = 0xFFFFFFFFu;
}
__syncthreads();
// Single-thread sequential write: thread 0 walks channels in original
// order and appends each to the cursor position of its bucket. This
// gives deterministic per-bucket ordering (channels listed in
// ascending original-channel-index order). No atomicAdd, no race —
// a single writer per feedback_no_atomicadd.
if (tid == 0) {
for (int c = 0; c < HIDDEN_DIM; ++c) {
unsigned char k = bucket_id_per_channel[c];
// Safety: bucket_id_per_channel must be in [0, N_HORIZONS).
// bucket_assign_kernel guarantees this; the bound check below
// is defensive only.
if (k < (unsigned char)N_HORIZONS) {
unsigned int pos = cursor[k];
unsigned int bdim = bucket_dim_k[k];
if (pos < bdim && pos < (unsigned int)MAX_BUCKET_DIM) {
channels_in_bucket[(unsigned int)k * MAX_BUCKET_DIM + pos] = (unsigned int)c;
cursor[k] = pos + 1;
}
}
}
}
}
// ─────────────────────────────────────────────────────────────────────
// zero_off_bucket_kernel — block-diagonal projection on
// [N_HORIZONS × HIDDEN_DIM] tensors.
//
// ALPHA fix (2026-05-21): the grad mask alone is insufficient to keep
// `heads_w_skip` block-diagonal because Adam's `m` and `v` momentum
// buffers carry pre-transition gradient signal across the boundary. The
// grad mask zeros NEW gradients at off-bucket positions; historical
// momentum continues to push them away from zero each Adam step.
//
// This kernel projects a generic `[N_HORIZONS × HIDDEN_DIM]` tensor
// onto the block-diagonal subspace by writing 0.0 at every (h, c) where
// `bucket_id_per_channel[c] != h`. It is launched on:
// 1. `heads_w_skip_d` at transition (one-shot, zeros off-bucket weights)
// 2. `heads_w_skip` Adam `m` and `v` buffers at transition (one-shot,
// zeros off-bucket momentum so post-transition Adam steps cannot
// revive off-bucket positions)
// 3. `heads_w_skip_d` after every Phase 2 Adam step (belt-and-suspenders;
// guarantees off-bucket positions stay exactly zero even under
// eps-driven Adam step from zero moments)
//
// Launch: grid = (N_HORIZONS, 1, 1), block = (HIDDEN_DIM, 1, 1). Same
// layout convention as `heads_w_skip_mask_init_kernel`.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void zero_off_bucket_kernel(
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
float* __restrict__ tensor // [N_HORIZONS × HIDDEN_DIM]
) {
int h = blockIdx.x;
int c = threadIdx.x;
if (h >= N_HORIZONS || c >= HIDDEN_DIM) return;
int idx = h * HIDDEN_DIM + c;
if (bucket_id_per_channel[c] != (unsigned char)h) {
tensor[idx] = 0.0f;
}
}
// ─────────────────────────────────────────────────────────────────────
// heads_compact_kernel: reorder heads_w_skip into compact ragged layout.
//
// Original heads_w_skip is [N_HORIZONS × HIDDEN_DIM]. Output is compact
// [HIDDEN_DIM] where each horizon h owns BUCKET_DIM[h] contiguous floats
// at position bucket_channel_offset[h].
//
// For each compact-position c in bucket k, read original
// heads_w_skip[k * HIDDEN_DIM + channel_that_landed_at_c].
//
// Launch: 1 block × HIDDEN_DIM threads.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void heads_compact_kernel(
const float* __restrict__ w_skip_orig, // [N_HORIZONS × HIDDEN_DIM]
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
const unsigned int* __restrict__ bucket_channel_offset, // [N_HORIZONS + 1]
float* __restrict__ w_skip_compact // [HIDDEN_DIM]
) {
extern __shared__ unsigned int per_bucket_cursor[]; // [N_HORIZONS + 1]
int tid = threadIdx.x;
if (tid <= N_HORIZONS) {
per_bucket_cursor[tid] = bucket_channel_offset[tid];
}
__syncthreads();
for (int c = 0; c < HIDDEN_DIM; ++c) {
if (tid == 0) {
unsigned char k = bucket_id_per_channel[c];
unsigned int pos = per_bucket_cursor[k];
w_skip_compact[pos] = w_skip_orig[(unsigned int)k * HIDDEN_DIM + c];
per_bucket_cursor[k] = pos + 1;
}
__syncthreads();
}
}
// ─────────────────────────────────────────────────────────────────────
// tau_change_frobenius_kernel: ||tau_t - tau_{t-1}||_F² (scalar output).
//
// Single block × HIDDEN_DIM threads. Block-tree reduction (no atomicAdd
// per feedback_no_atomicadd).
//
// Per pearl_first_observation_bootstrap: caller's ControllerA bootstraps
// the EMA from this scalar on step 1; on subsequent steps the scalar
// feeds the Wiener-α update.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void tau_change_frobenius_kernel(
const float* __restrict__ tau_t, // [HIDDEN_DIM]
const float* __restrict__ tau_t_minus_1, // [HIDDEN_DIM]
float* __restrict__ tau_change_out // [1]
) {
__shared__ float sdata[HIDDEN_DIM];
int tid = threadIdx.x;
if (tid >= HIDDEN_DIM) return;
float diff = tau_t[tid] - tau_t_minus_1[tid];
sdata[tid] = diff * diff;
__syncthreads();
// Block-tree reduction (HIDDEN_DIM=128 is power-of-two; no padding needed).
for (int s = HIDDEN_DIM / 2; s > 0; s >>= 1) {
if (tid < s) sdata[tid] += sdata[tid + s];
__syncthreads();
}
if (tid == 0) *tau_change_out = sdata[0];
}
// ─────────────────────────────────────────────────────────────────────
// slack_factor_apply_kernel: convert (Q1, Q3) → IQR_widened in place.
//
// Per spec §3.2: slack_factor_k = sqrt(Q3_k / Q1_k), ISV-derived from
// observed bucket IQR (no hardcoded 1.5× factor).
//
// Launch: 1 block × N_HORIZONS threads (5 threads, well within a warp).
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void slack_factor_apply_kernel(
float* __restrict__ iqr_lo, // [N_HORIZONS] in: Q1, out: Q1/slack
float* __restrict__ iqr_hi // [N_HORIZONS] in: Q3, out: Q3*slack
) {
int k = threadIdx.x;
if (k >= N_HORIZONS) return;
float q1 = iqr_lo[k];
float q3 = iqr_hi[k];
// Numerical defense: Q1 must be > 0 (taus are log-uniform [0.01, 1000],
// all positive by construction).
float slack = sqrtf(q3 / fmaxf(q1, 1e-6f));
iqr_lo[k] = q1 / slack;
iqr_hi[k] = q3 * slack;
}
// ─────────────────────────────────────────────────────────────────────
// tau_clamp_kernel — Controller B post-Adam projection.
//
// Per spec §3.2 and pearl_adam_normalizes_loss_weights: hard projection
// AFTER each Adam step on the τ slice, bypassing Adam's m/sqrt(v)
// normalization that would no-op a loss-weight modulation.
//
// Per pearl_isv_for_adaptive_bounds: the (lo, hi) inputs are the IQR-
// widened bounds from `slack_factor_apply_kernel` (ISV-derived from each
// bucket's observed Q3/Q1 ratio at the Phase 1→2 transition).
//
// Launch: 1 block × HIDDEN_DIM threads (single warp on Ampere+; tiny).
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void tau_clamp_kernel(
float* __restrict__ tau_all, // [HIDDEN_DIM]
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
const float* __restrict__ iqr_lo, // [N_HORIZONS]
const float* __restrict__ iqr_hi // [N_HORIZONS]
) {
int c = threadIdx.x;
if (c >= HIDDEN_DIM) return;
unsigned char k = bucket_id_per_channel[c];
float v = tau_all[c];
float lo = iqr_lo[k];
float hi = iqr_hi[k];
tau_all[c] = fminf(fmaxf(v, lo), hi);
}
// ─────────────────────────────────────────────────────────────────────
// heads_lr_multiplier_scale_kernel — Controller C per-bucket LR multiplier.
//
// Per spec §3.3 and pearl_adam_normalizes_loss_weights: rescales the
// per-step parameter delta (post-Adam pre-Adam) by a per-horizon LR
// multiplier. Bypasses Adam's m/sqrt(v) normalization because the
// rescaling is applied to the parameter delta itself, not the loss
// weight or the Adam moments.
//
// Layout: `param` is row-major [N_HORIZONS × per_horizon_stride]. Each
// horizon owns a contiguous block of `per_horizon_stride` floats. Element
// `i` belongs to horizon `i / per_horizon_stride`.
//
// Launch: enough threads to cover `total_elements` (one per element).
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void heads_lr_multiplier_scale_kernel(
float* __restrict__ param, // [N_HORIZONS * per_horizon_stride]
const float* __restrict__ param_pre, // pre-Adam snapshot, same shape
const float* __restrict__ lr_mult_per_horizon, // [N_HORIZONS]
int per_horizon_stride,
int total_elements
) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= total_elements) return;
int horizon = i / per_horizon_stride;
if (horizon >= N_HORIZONS) return; // safety bound for stride/total mismatch
float delta = param[i] - param_pre[i];
float scaled_delta = delta * lr_mult_per_horizon[horizon];
param[i] = param_pre[i] + scaled_delta;
}
// ─────────────────────────────────────────────────────────────────────
// h_mag_per_bucket_kernel — per-bucket mean(|h_state|) for Controller D.
//
// Per spec §3.4: Controller D's dead-bucket detector compares per-bucket
// `h_mag_k = mean(|h_state_branch_k|)` (across the batch × bucket_dim_k
// channels) against the first-observation floor with a 1/e decay
// threshold. This kernel computes the per-bucket mean-abs reduction on
// device; the host reads the [N_HORIZONS] result via a mapped-pinned
// shadow each Phase 2 step.
//
// ALPHA fix (2026-05-21): h_state stays in ORIGINAL channel layout (no
// reorder). The bucket → channel lookup goes through
// `channels_in_bucket[bucket][i]` populated at transition by
// `channels_in_bucket_kernel`.
//
// Launch: grid = (N_HORIZONS, 1, 1), block = (32, 1, 1). Each block
// reduces one bucket. Per `feedback_no_atomicadd`, reduction is
// block-tree on shared memory (no atomicAdd).
//
// Input `h_state` is `[B × HIDDEN_DIM]` (ORIGINAL channel layout). Each
// block reads its bucket's `bucket_dim_k[bucket]` channels (via
// `channels_in_bucket[bucket][0..bdim]`) per sample (across all B
// samples), accumulates |value|, and writes the per-bucket mean.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void h_mag_per_bucket_kernel(
const float* __restrict__ h_state, // [B × HIDDEN_DIM]
const unsigned int* __restrict__ channels_in_bucket, // [N_HORIZONS × MAX_BUCKET_DIM]
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
int B,
float* __restrict__ h_mag_per_bucket // [N_HORIZONS]
) {
int bucket = blockIdx.x;
if (bucket >= N_HORIZONS) return;
int bdim = (int)bucket_dim_k[bucket];
// sdata sized for a single-warp reduction (32 lanes).
__shared__ float sdata[32];
int tid = threadIdx.x;
// Each thread sums |h| over its bucket-local index (tid), looking up
// the ORIGINAL channel via channels_in_bucket. Threads with tid >= bdim
// idle — uniform predicate, no warp divergence inside [0, 32).
float local_sum = 0.0f;
if (tid < bdim) {
unsigned int c = channels_in_bucket[bucket * MAX_BUCKET_DIM + tid];
// Defensive: sentinel slot OR out-of-range channel index should
// not contribute. Predicate is uniform across the warp because all
// active threads (tid < bdim) hold valid entries by construction
// of channels_in_bucket_kernel.
if (c < (unsigned int)HIDDEN_DIM) {
for (int b = 0; b < B; ++b) {
float v = h_state[b * HIDDEN_DIM + c];
local_sum += fabsf(v);
}
}
}
sdata[tid] = (tid < 32) ? local_sum : 0.0f;
__syncthreads();
// Block-tree reduction over 32 lanes (single warp). No atomicAdd.
for (int s = 16; s > 0; s >>= 1) {
if (tid < s) sdata[tid] += sdata[tid + s];
__syncthreads();
}
// Lane 0 writes the per-bucket mean. Total elements summed = bdim * B.
if (tid == 0) {
float denom = (float)(bdim * B);
h_mag_per_bucket[bucket] = sdata[0] / fmaxf(denom, 1.0f);
}
}
// ─────────────────────────────────────────────────────────────────────
// heads_w_skip_mask_init_kernel — Phase 1→2 transition one-shot.
//
// Per spec §3.3 (block-diagonal heads) and follow-up to Task 10 Option B:
// build the per-channel routing mask AND zero-out off-bucket entries of
// `heads_w_skip` in place so that subsequent forward/backward never sees
// non-zero cross-bucket weights. Combined with the grad-mask apply kernel
// below, this is mathematically equivalent to a compact-only read of
// `heads_w_skip` at zero implementation cost for the GRN kernel.
//
// Layout: `heads_w_skip` is `[N_HORIZONS × HIDDEN_DIM]` row-major
// (horizon-major). `bucket_id_per_channel[c]` records which horizon owns
// column `c` in the block-diagonal layout. Entry `(h, c)` is in-bucket
// iff `bucket_id_per_channel[c] == h`.
//
// Launch: grid = (N_HORIZONS, 1, 1), block = (HIDDEN_DIM, 1, 1).
// One block per horizon × one thread per channel. No reductions, no
// shared memory, no atomicAdd — fully data-parallel.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void heads_w_skip_mask_init_kernel(
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
float* __restrict__ heads_w_skip, // [N_HORIZONS × HIDDEN_DIM]
float* __restrict__ mask // [N_HORIZONS × HIDDEN_DIM]
) {
int h = blockIdx.x;
int c = threadIdx.x;
if (h >= N_HORIZONS || c >= HIDDEN_DIM) return;
int idx = h * HIDDEN_DIM + c;
float m = (bucket_id_per_channel[c] == (unsigned char)h) ? 1.0f : 0.0f;
mask[idx] = m;
heads_w_skip[idx] *= m; // zero off-bucket entries in place
}
// ─────────────────────────────────────────────────────────────────────
// heads_w_skip_grad_mask_apply_kernel — per-step Phase 2 gradient mask.
//
// Per spec §3.3 + follow-up Option B closure: multiply the post-reduction
// `grad_heads_w_skip` element-wise by the routing mask BEFORE the Adam
// step. Off-bucket positions receive zero gradient, never update, and
// (since they started at zero after the init kernel) remain at zero
// throughout training. This delivers block-diagonal `heads_w_skip`
// behaviour without touching the GRN forward/backward kernels.
//
// Launch: grid = (N_HORIZONS, 1, 1), block = (HIDDEN_DIM, 1, 1). Same
// layout as `heads_w_skip_mask_init_kernel`.
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void heads_w_skip_grad_mask_apply_kernel(
const float* __restrict__ mask, // [N_HORIZONS × HIDDEN_DIM]
float* __restrict__ grad_heads_w_skip // [N_HORIZONS × HIDDEN_DIM]
) {
int h = blockIdx.x;
int c = threadIdx.x;
if (h >= N_HORIZONS || c >= HIDDEN_DIM) return;
int idx = h * HIDDEN_DIM + c;
grad_heads_w_skip[idx] *= mask[idx];
}
// ─────────────────────────────────────────────────────────────────────
// bucket_iqr_double_widening_kernel — Controller D Recovery Attempt 1.
//
// Per spec §3.4: when a bucket's h_mag EMA falls below the first-
// observation × 1/e threshold for `dead_window_k` consecutive Phase 2
// steps, Recovery Attempt 1 doubles the bucket's slack envelope by
// halving its IQR lower bound and doubling the IQR upper bound. This
// releases τ values for that bucket from Controller B's hard projection
// clip, letting them drift to the wider range that may re-engage the
// channels.
//
// Per `feedback_isv_for_adaptive_bounds`: the factor 2 is the natural
// log-doubling step — one full log-spread expansion beyond the bucket's
// natural Q3/Q1 ratio (the IQR already encoded one log-step via the
// transition's `sqrt(Q3/Q1)` slack widening; doubling captures one more).
//
// Launch: grid = (1, 1, 1), block = (1, 1, 1). Single-thread, off the
// hot path (fires at most a handful of times per training run when a
// bucket actually goes dead).
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void bucket_iqr_double_widening_kernel(
float* __restrict__ iqr_lo, // [N_HORIZONS]
float* __restrict__ iqr_hi, // [N_HORIZONS]
int bucket_idx
) {
if (threadIdx.x == 0 && blockIdx.x == 0) {
if (bucket_idx >= 0 && bucket_idx < N_HORIZONS) {
iqr_lo[bucket_idx] *= 0.5f;
iqr_hi[bucket_idx] *= 2.0f;
}
}
}