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>
241 lines
12 KiB
Plaintext
241 lines
12 KiB
Plaintext
// cfc_step_per_branch.cu — fused per-branch CfC step (forward + backward).
|
||
//
|
||
// Per spec §5.4 point 1 (docs/superpowers/specs/2026-05-21-per-horizon-cfc-inference-design.md):
|
||
// Single fused kernel covering all N_HORIZONS branches × n_batch in one launch.
|
||
// Grid: (B, N_HORIZONS, 1), block: (MAX_BUCKET_DIM=96, 1, 1) uniform predicate.
|
||
//
|
||
// Uniform predicate (`threadIdx.x >= bucket_dim_k[branch]` early-return) handles
|
||
// uneven bucket sizes [43, 43, 42] without warp divergence — the
|
||
// comparison is against a per-block constant, so all threads in a warp take
|
||
// the same branch.
|
||
//
|
||
// Per pearl_cooperative_staging_eliminates_redundant_reads: the per-batch
|
||
// `x` and `h_old` rows are SHARED across all output channels in a block
|
||
// (every thread reads HIDDEN_DIM elements of both in the K-loop). Stage
|
||
// them into shared memory once at block entry. W_in / W_rec rows are
|
||
// per-channel (different per thread c), so they remain direct DRAM reads.
|
||
//
|
||
// Per feedback_no_atomicadd.md: no atomicAdd anywhere. Each thread writes
|
||
// to its own (batch, c) slot in forward, and to its own per-batch grad
|
||
// slice in backward. Cross-batch reduction is the caller's responsibility
|
||
// via the existing `reduce_axis0` infrastructure.
|
||
//
|
||
// Per pearl_no_host_branches_in_captured_graph: no host branching; all
|
||
// control flow uses device-resident metadata read from device tensors.
|
||
//
|
||
// Per feedback_nvidia_grade_perf_for_kernels.md: warp-uniform branches,
|
||
// no atomicAdd, coalesced loads via cooperative staging.
|
||
//
|
||
// ALPHA fix (2026-05-21): the kernel now indexes into ORIGINAL channel
|
||
// layout via `channels_in_bucket[branch][tid]` — abandons the prior
|
||
// bucket-grouped τ layout that mis-aligned with W_in/W_rec/b/tau_all
|
||
// (which all stayed in original-channel layout). See
|
||
// `bucket_transition_kernels.cu::channels_in_bucket_kernel` for the
|
||
// lookup table.
|
||
|
||
#define HIDDEN_DIM 128
|
||
#define N_HORIZONS 3
|
||
#define MAX_BUCKET_DIM 96
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// cfc_step_per_branch_fwd: forward pass.
|
||
//
|
||
// Launch:
|
||
// grid = (B, N_HORIZONS, 1)
|
||
// block = (MAX_BUCKET_DIM = 96, 1, 1)
|
||
// shared_mem_bytes = 2 * HIDDEN_DIM * sizeof(float) = 1024 bytes
|
||
//
|
||
// Per (batch, branch, thread-in-bucket) thread computes:
|
||
// c = channels_in_bucket[branch * MAX_BUCKET_DIM + tid] (original channel index)
|
||
// pre = b[c] + Σ_k W_in[c, k] * x[batch, k] + Σ_k W_rec[c, k] * h_old[batch, k]
|
||
// decay = exp(-dt / max(tau_all[c], 1e-6))
|
||
// h_new[batch, c] = h_old[batch, c] * decay + (1 - decay) * tanh(pre)
|
||
//
|
||
// x_local[HIDDEN_DIM] and h_old_local[HIDDEN_DIM] are cooperative-staged
|
||
// in shared memory; without them every thread re-reads the full row
|
||
// (HIDDEN_DIM × bucket_dim threads → up to 96 × redundant reads per row).
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
extern "C" __global__ void cfc_step_per_branch_fwd(
|
||
const float* __restrict__ w_in, // [HIDDEN_DIM × HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ w_rec, // [HIDDEN_DIM × HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ b, // [HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ tau_all, // [HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ x, // [B × HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ h_old, // [B × HIDDEN_DIM] original channel layout
|
||
float dt,
|
||
int B,
|
||
const unsigned int* __restrict__ channels_in_bucket, // [N_HORIZONS × MAX_BUCKET_DIM]
|
||
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
|
||
float* __restrict__ h_new // [B × HIDDEN_DIM] original channel layout
|
||
) {
|
||
extern __shared__ float smem[];
|
||
float* x_local = smem; // [HIDDEN_DIM]
|
||
float* h_old_local = smem + HIDDEN_DIM; // [HIDDEN_DIM]
|
||
|
||
int batch = blockIdx.x;
|
||
int branch = blockIdx.y;
|
||
int tid = threadIdx.x;
|
||
|
||
if (batch >= B) return;
|
||
|
||
// Cooperative staging of per-batch x[batch, *] and h_old[batch, *] rows.
|
||
// All threads (including those that will early-return on the per-branch
|
||
// predicate) participate in the staging — the row is shared across the
|
||
// block's output channels, so we need every thread to help load.
|
||
//
|
||
// Each thread loads HIDDEN_DIM / blockDim.x = 128 / 96 ≈ 2 elements
|
||
// (rounded up via the stride loop).
|
||
for (int i = tid; i < HIDDEN_DIM; i += blockDim.x) {
|
||
x_local[i] = x[batch * HIDDEN_DIM + i];
|
||
h_old_local[i] = h_old[batch * HIDDEN_DIM + i];
|
||
}
|
||
__syncthreads();
|
||
|
||
// Uniform per-branch predicate: idle threads in shorter buckets early-return.
|
||
// The comparison is against a per-block constant (bucket_dim_k[branch]),
|
||
// so all threads in a warp take the same path → no warp divergence.
|
||
unsigned int bucket_dim = bucket_dim_k[branch];
|
||
if ((unsigned int)tid >= bucket_dim) return;
|
||
|
||
// Look up the ORIGINAL channel index for this (branch, tid) position.
|
||
unsigned int c_u = channels_in_bucket[branch * MAX_BUCKET_DIM + tid];
|
||
// Defensive: sentinel (0xFFFFFFFF) or out-of-range indices must not
|
||
// write h_new. By construction of channels_in_bucket_kernel, all
|
||
// tid < bucket_dim_k[branch] entries hold valid channel indices.
|
||
if (c_u >= (unsigned int)HIDDEN_DIM) return;
|
||
int c = (int)c_u;
|
||
|
||
// pre = b[c] + Σ_k W_in[c, k] * x_local[k] + Σ_k W_rec[c, k] * h_old_local[k]
|
||
float pre = b[c];
|
||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||
pre += w_in[c * HIDDEN_DIM + k] * x_local[k];
|
||
}
|
||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||
pre += w_rec[c * HIDDEN_DIM + k] * h_old_local[k];
|
||
}
|
||
|
||
float decay = expf(-dt / fmaxf(tau_all[c], 1e-6f));
|
||
h_new[batch * HIDDEN_DIM + c] =
|
||
h_old_local[c] * decay + (1.0f - decay) * tanhf(pre);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// cfc_step_per_branch_bwd: backward pass.
|
||
//
|
||
// Launch:
|
||
// grid = (B, N_HORIZONS, 1)
|
||
// block = (MAX_BUCKET_DIM = 96, 1, 1)
|
||
// shared_mem_bytes = 2 * HIDDEN_DIM * sizeof(float) = 1024 bytes
|
||
//
|
||
// Per-batch grad slices are written by this kernel; cross-batch
|
||
// reduction is the caller's responsibility (existing `reduce_axis0`
|
||
// pattern; see crates/ml-alpha/cuda/cfc_step.cu Phase B comment block).
|
||
//
|
||
// grad shapes:
|
||
// grad_w_in : [B × HIDDEN_DIM × HIDDEN_DIM] per-batch scratch (overwrite)
|
||
// grad_w_rec : [B × HIDDEN_DIM × HIDDEN_DIM] per-batch scratch (overwrite)
|
||
// grad_b : [B × HIDDEN_DIM] per-batch scratch (overwrite)
|
||
// grad_tau_all: [B × HIDDEN_DIM] per-batch scratch (overwrite)
|
||
// grad_h_old : [B × HIDDEN_DIM] per-batch (overwrite)
|
||
//
|
||
// Note: this kernel uses OVERWRITE semantics (the caller zeros scratch
|
||
// per training step). The single-step CfC backward in the existing
|
||
// `cfc_step_backward_batched` uses += for K-loop accumulation; for
|
||
// per-branch usage in Phase 2 the K-loop's per-position accumulation is
|
||
// the caller's concern (matching existing scratch + reduce pattern).
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
extern "C" __global__ void cfc_step_per_branch_bwd(
|
||
const float* __restrict__ w_in, // [HIDDEN_DIM × HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ w_rec, // [HIDDEN_DIM × HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ b, // [HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ tau_all, // [HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ x, // [B × HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ h_old, // [B × HIDDEN_DIM] original channel layout
|
||
const float* __restrict__ grad_h_new, // [B × HIDDEN_DIM] original channel layout
|
||
float dt,
|
||
int B,
|
||
const unsigned int* __restrict__ channels_in_bucket, // [N_HORIZONS × MAX_BUCKET_DIM]
|
||
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
|
||
float* __restrict__ grad_w_in, // [B × HIDDEN_DIM × HIDDEN_DIM]
|
||
float* __restrict__ grad_w_rec, // [B × HIDDEN_DIM × HIDDEN_DIM]
|
||
float* __restrict__ grad_b, // [B × HIDDEN_DIM]
|
||
float* __restrict__ grad_tau_all, // [B × HIDDEN_DIM]
|
||
float* __restrict__ grad_h_old // [B × HIDDEN_DIM]
|
||
) {
|
||
extern __shared__ float smem[];
|
||
float* x_local = smem; // [HIDDEN_DIM]
|
||
float* h_old_local = smem + HIDDEN_DIM; // [HIDDEN_DIM]
|
||
|
||
int batch = blockIdx.x;
|
||
int branch = blockIdx.y;
|
||
int tid = threadIdx.x;
|
||
|
||
if (batch >= B) return;
|
||
|
||
// Cooperative staging — same pattern as forward.
|
||
for (int i = tid; i < HIDDEN_DIM; i += blockDim.x) {
|
||
x_local[i] = x[batch * HIDDEN_DIM + i];
|
||
h_old_local[i] = h_old[batch * HIDDEN_DIM + i];
|
||
}
|
||
__syncthreads();
|
||
|
||
unsigned int bucket_dim = bucket_dim_k[branch];
|
||
if ((unsigned int)tid >= bucket_dim) return;
|
||
|
||
// Look up the ORIGINAL channel index for this (branch, tid) position.
|
||
unsigned int c_u = channels_in_bucket[branch * MAX_BUCKET_DIM + tid];
|
||
if (c_u >= (unsigned int)HIDDEN_DIM) return;
|
||
int c = (int)c_u;
|
||
|
||
// Recompute forward pre + tanh to derive d_pre.
|
||
float pre = b[c];
|
||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||
pre += w_in[c * HIDDEN_DIM + k] * x_local[k];
|
||
}
|
||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||
pre += w_rec[c * HIDDEN_DIM + k] * h_old_local[k];
|
||
}
|
||
const float tau_eps = 1e-6f;
|
||
const float tau_raw = tau_all[c];
|
||
const float tau_eff = fmaxf(tau_raw, tau_eps);
|
||
const float decay = expf(-dt / tau_eff);
|
||
const float s = tanhf(pre);
|
||
const float dh = grad_h_new[batch * HIDDEN_DIM + c];
|
||
const float d_pre = dh * (1.0f - decay) * (1.0f - s * s);
|
||
const float d_decay = dh * (h_old_local[c] - s);
|
||
|
||
// Per-batch grad scratch writes — each thread (batch, branch, c) is the
|
||
// sole writer of grad_*[batch, c, ...]. No race; no atomicAdd.
|
||
grad_b[batch * HIDDEN_DIM + c] = d_pre;
|
||
|
||
// grad_tau receives 0 when tau is at the clamp floor (strict d(max)/d(tau) = 0).
|
||
const float gate = (tau_raw > tau_eps) ? 1.0f : 0.0f;
|
||
grad_tau_all[batch * HIDDEN_DIM + c] =
|
||
gate * d_decay * decay * dt / (tau_eff * tau_eff);
|
||
|
||
// grad_w_in[batch, c, k] = d_pre * x_local[k]
|
||
// grad_w_rec[batch, c, k] = d_pre * h_old_local[k]
|
||
//
|
||
// Layout note: thread serves the (batch, c) role, sweeping k in the inner
|
||
// loop. Writes along k are coalesced within a thread but strided across
|
||
// threads (each thread writes its own [c, *] row in a per-(batch) slab).
|
||
// Per pearl_coalesce_via_thread_role_swap: this is single-thread-writes-
|
||
// single-row, the warp-coalescing concern is on cross-thread writes to
|
||
// the SAME row, which we don't do here. No swap needed.
|
||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||
const long long off =
|
||
(long long)batch * HIDDEN_DIM * HIDDEN_DIM
|
||
+ (long long)c * HIDDEN_DIM + k;
|
||
grad_w_in[off] = d_pre * x_local[k];
|
||
grad_w_rec[off] = d_pre * h_old_local[k];
|
||
}
|
||
|
||
// grad_h_old[batch, c] receives the direct decay contribution. The cross-
|
||
// channel term Σ_j d_pre[j] * W_rec[j, c] requires a reduction across
|
||
// threads in the block — the caller may run a second-pass reduction
|
||
// kernel when grad_h_old needs to feed backward through a prior CfC
|
||
// step (currently CfC is K=1 in per-branch Phase 2, so this direct
|
||
// contribution is the only one).
|
||
grad_h_old[batch * HIDDEN_DIM + c] = dh * decay;
|
||
}
|