Local L40S profile (perception_overfit smoke) GPU kernel time:
1589ms → 739ms (53.5% reduction, 2.15× speedup).
Wall-clock smoke: 9.6s → 4.94s (1.94× faster).
Per-kernel deltas (nsys --cuda-graph-trace=node):
reduce_axis0: 362ms → 10ms (36× faster)
Block layout: per-column (1 block / output) → 32-wide column tile
(block_dim = 32 × 8). Cross-thread reads were strided by n_tail
(~40K floats = 160KB stride) — one cache line per thread, 8× HBM
bandwidth wasted. New tile gives coalesced 128B transactions per
warp. Block tree-reduce kept (no atomicAdd, per feedback_no_atomicadd).
+1 shared-mem pad to eliminate 32-way bank conflict on the ty reduce.
multi_horizon_heads_grn_bwd_batched: 540ms → 113ms (4.8× faster)
1. Stage h_row[HIDDEN] and a1[5,HEAD_MID] in shared at block entry.
Eliminates ~28K redundant DRAM reads/block across Pass 3 + Pass 5.
2. Pass 5 reorder: k outer / i inner with d_z1[k,m] pinned in
register; writes to grad_w1_scratch are sequential per-thread.
3. Block size 64 → 128 threads. Pass 5/6 now partition over i
(output column): cross-thread writes become COALESCED 128B/warp
(was stride-128 = 512B). Passes 2/3/4 gate on (tid < HEAD_MID).
4. Pass 3 thread role: m_out → m_in/n. Same coalescing fix on
grad_w2 writes AND w2 reads in the d_eta_2 sum.
cfc_step_backward_batched: 351ms → 271ms (1.3× faster)
1. Stage x_b[n_in] and h_old_b[n_hid] in shared (was 128× redundant
DRAM reads per block; now 1× cooperative load).
2. Pass 1 thread role: i (output row) → k (output col). For each
i loop iteration, the warp writes grad_w_in[..., tid] /
grad_w_rec[..., tid] — COALESCED 128B/warp (was stride-128
non-coalesced).
multi_horizon_heads_grn_fwd_batched: 211ms → 204ms
Stage h_row[HIDDEN] in shared — Pass 1 and Pass 3 both consume.
cfc_step_batched (fwd): 95ms → 94ms
Stage x_b and h_old_b in shared.
Shared-mem budgets fit comfortably under the 48KB SM cap (~6KB / ~2KB
respectively). All 9 perception_overfit tests pass — gradient
correctness validated end-to-end (constant-signal overfit, K-loop
capture/replay, stride-4 path, evaluate-only paths).
Discipline:
- Block tree-reduce only, never atomicAdd
- No nvrtc; pre-compiled cubins via build.rs
- Mapped-pinned-only is unaffected (CPU↔GPU contract untouched)
- Single source of truth: replaced kernels in place, no v2 suffixes
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
365 lines
15 KiB
Plaintext
365 lines
15 KiB
Plaintext
// cfc_step.cu — one CfC time step (Hasani 2022 closed-form recurrence)
|
||
// + batched variants + 3D transpose helper.
|
||
|
||
// Transpose a [N1, N2, N3] row-major tensor to [N2, N1, N3]. Used by
|
||
// the batched PerceptionTrainer to convert Mamba2's [B, K, hidden]
|
||
// per-step output into the [K, B, hidden] layout that makes per-K
|
||
// slot access contiguous in the trainer's K loop (and back again for
|
||
// grad_h_enriched_seq feeding Mamba2's backward).
|
||
//
|
||
// Launches one block per (i, j) pair, blockDim.x threads cover N3.
|
||
extern "C" __global__ void transpose_3d_swap_01(
|
||
const float* __restrict__ in, // [N1, N2, N3]
|
||
float* __restrict__ out, // [N2, N1, N3]
|
||
int N1,
|
||
int N2,
|
||
int N3
|
||
) {
|
||
int i = blockIdx.x;
|
||
int j = blockIdx.y;
|
||
int k = blockIdx.z * blockDim.x + threadIdx.x;
|
||
if (i >= N1 || j >= N2 || k >= N3) return;
|
||
out[((long long)j * N1 + i) * N3 + k] = in[((long long)i * N2 + j) * N3 + k];
|
||
}
|
||
|
||
//
|
||
// pre[i] = sum_k w_in[i,k] * x[k] + sum_k w_rec[i,k] * h_old[k] + b[i]
|
||
// decay[i] = exp(-dt_s / max(tau[i], 1e-6))
|
||
// h_new[i] = h_old[i] * decay[i] + (1 - decay[i]) * tanh(pre[i])
|
||
//
|
||
// Launched as one block of n_hid threads (or grid of blocks if n_hid > 128).
|
||
// Each thread computes one hidden unit; reductions are intra-thread (each
|
||
// thread does its own dot product). No atomicAdd. Per
|
||
// `feedback_no_atomicadd.md` and the GPU/CPU contract.
|
||
|
||
extern "C" __global__ void cfc_step(
|
||
const float* __restrict__ w_in, // [n_hid, n_in], row-major
|
||
const float* __restrict__ w_rec, // [n_hid, n_hid], row-major
|
||
const float* __restrict__ b, // [n_hid]
|
||
const float* __restrict__ tau, // [n_hid]
|
||
const float* __restrict__ x, // [n_in]
|
||
const float* __restrict__ h_old, // [n_hid]
|
||
float dt_s,
|
||
int n_in,
|
||
int n_hid,
|
||
float* __restrict__ h_new // [n_hid]
|
||
) {
|
||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||
if (i >= n_hid) return;
|
||
|
||
float pre = b[i];
|
||
for (int k = 0; k < n_in; ++k) {
|
||
pre += w_in[i * n_in + k] * x[k];
|
||
}
|
||
for (int k = 0; k < n_hid; ++k) {
|
||
pre += w_rec[i * n_hid + k] * h_old[k];
|
||
}
|
||
const float decay = expf(-dt_s / fmaxf(tau[i], 1e-6f));
|
||
h_new[i] = h_old[i] * decay + (1.0f - decay) * tanhf(pre);
|
||
}
|
||
|
||
// Backward through ONE cfc_step (truncated BPTT, K=1).
|
||
//
|
||
// Forward (for reference):
|
||
// pre[i] = b[i] + sum_k W_in[i,k] x[k] + sum_k W_rec[i,k] h_old[k]
|
||
// τ_eff = max(tau[i], 1e-6)
|
||
// decay[i] = exp(-dt / τ_eff)
|
||
// s[i] = tanh(pre[i])
|
||
// h_new[i] = h_old[i] * decay + (1 - decay) * s
|
||
//
|
||
// Given grad_h_new[i] = dL/dh_new[i], compute:
|
||
// d_decay[i] = grad_h_new[i] * (h_old[i] - s[i])
|
||
// d_pre[i] = grad_h_new[i] * (1 - decay) * (1 - s^2)
|
||
// grad_tau[i] += d_decay[i] * decay[i] * dt / τ_eff² (when tau > eps)
|
||
// grad_b[i] += d_pre[i]
|
||
// grad_W_in[i,k] += d_pre[i] * x[k]
|
||
// grad_W_rec[i,k] += d_pre[i] * h_old[k]
|
||
// grad_h_old[i] += grad_h_new[i] * decay + sum_j d_pre[j] * W_rec[j, i]
|
||
//
|
||
// tau IS trained — grad_tau receives a `+=` accumulation (callers must
|
||
// pre-zero across the per-position K loop).
|
||
//
|
||
// One thread per hidden unit (128 threads in one block). Block tree-
|
||
// reduce not needed — each thread owns its own grad_W rows and grad_h
|
||
// component; grad_h_old accumulates via shared `d_pre` reads.
|
||
|
||
extern "C" __global__ void cfc_step_backward(
|
||
const float* __restrict__ w_in, // [n_hid, n_in]
|
||
const float* __restrict__ w_rec, // [n_hid, n_hid]
|
||
const float* __restrict__ b,
|
||
const float* __restrict__ tau,
|
||
const float* __restrict__ x, // [n_in]
|
||
const float* __restrict__ h_old, // [n_hid]
|
||
const float* __restrict__ grad_h_new, // [n_hid]
|
||
float dt_s,
|
||
int n_in,
|
||
int n_hid,
|
||
float* __restrict__ grad_w_in, // [n_hid, n_in]
|
||
float* __restrict__ grad_w_rec, // [n_hid, n_hid]
|
||
float* __restrict__ grad_b, // [n_hid]
|
||
float* __restrict__ grad_tau, // [n_hid] — accumulated via +=
|
||
float* __restrict__ grad_h_old, // [n_hid]
|
||
float* __restrict__ grad_x // [n_in] — set to nullptr to skip
|
||
) {
|
||
extern __shared__ float smem[];
|
||
float* sd_pre = smem; // [n_hid]
|
||
float* sdecay = sd_pre + n_hid; // [n_hid]
|
||
|
||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||
if (i >= n_hid) return;
|
||
|
||
// Recompute forward pre + tanh to derive d_pre.
|
||
float pre = b[i];
|
||
for (int k = 0; k < n_in; ++k) pre += w_in[i * n_in + k] * x[k];
|
||
for (int k = 0; k < n_hid; ++k) pre += w_rec[i * n_hid + k] * h_old[k];
|
||
const float s = tanhf(pre);
|
||
const float tau_eps = 1e-6f;
|
||
const float tau_eff = fmaxf(tau[i], tau_eps);
|
||
const float decay = expf(-dt_s / tau_eff);
|
||
const float dh = grad_h_new[i];
|
||
const float d_pre = dh * (1.0f - decay) * (1.0f - s * s);
|
||
|
||
sd_pre[i] = d_pre;
|
||
sdecay[i] = decay;
|
||
__syncthreads();
|
||
|
||
// Parameter-grad writes use `+=`: callers may invoke this kernel
|
||
// multiple times per training step (per-position supervision) and
|
||
// need the gradients summed across calls. Callers MUST pre-zero
|
||
// grad_b / grad_w_in / grad_w_rec / grad_tau at the start of each step.
|
||
grad_b[i] += d_pre;
|
||
for (int k = 0; k < n_in; ++k) {
|
||
grad_w_in[i * n_in + k] += d_pre * x[k];
|
||
}
|
||
for (int k = 0; k < n_hid; ++k) {
|
||
grad_w_rec[i * n_hid + k] += d_pre * h_old[k];
|
||
}
|
||
// grad_tau accumulation. Zero contribution if tau is clamped to eps
|
||
// (max() inactive on this side) — strict d(max)/d(tau) = 0 there.
|
||
if (tau[i] > tau_eps) {
|
||
const float d_decay = dh * (h_old[i] - s);
|
||
grad_tau[i] += d_decay * decay * dt_s / (tau_eff * tau_eff);
|
||
}
|
||
|
||
// grad_h_old[i] = dh * decay + sum_j d_pre[j] * W_rec[j, i]
|
||
float gh = dh * decay;
|
||
for (int j = 0; j < n_hid; ++j) {
|
||
gh += sd_pre[j] * w_rec[j * n_hid + i];
|
||
}
|
||
grad_h_old[i] = gh;
|
||
|
||
// grad_x[k] = sum_i d_pre[i] * W_in[i, k]. Each thread i contributes to
|
||
// n_in different output positions — different threads write to different
|
||
// grad_x slots if we partition by i, but multiple i's contribute to each k.
|
||
// Avoid atomicAdd: rely on thread 0 to do the n_in × n_hid sum
|
||
// sequentially (n_in is small — typically 32 or 128, this is one-time per
|
||
// backward call). Discriminated by nullptr → skip path for back-compat.
|
||
if (grad_x != nullptr && i == 0) {
|
||
for (int k = 0; k < n_in; ++k) {
|
||
float gx = 0.0f;
|
||
for (int j = 0; j < n_hid; ++j) {
|
||
gx += sd_pre[j] * w_in[j * n_in + k];
|
||
}
|
||
grad_x[k] = gx;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// ─── Batched variants ────────────────────────────────────────────────
|
||
//
|
||
// Phase B (2026-05-17): block-per-batch refactor. Launch contract:
|
||
// grid=(n_batch, 1, 1), block=(n_hid, 1, 1).
|
||
// Each block handles ONE batch's n_hid channels in parallel.
|
||
//
|
||
// Param-grad writes go through per-batch scratch buffers
|
||
// (grad_w_in_scratch [B, n_hid, n_in] etc.). Thread (bi, i) is the
|
||
// sole writer to its slice of each scratch tensor; the K-loop's 64
|
||
// invocations accumulate via += within the same (bi, i, *) slice.
|
||
// After the K-loop the caller runs `reduce_axis0` to sum B → final
|
||
// grad buffer (OVERWRITE semantics). No atomicAdd per
|
||
// feedback_no_atomicadd.md.
|
||
//
|
||
// Shared memory budget per block: sd_pre[n_hid] + sdecay[n_hid] =
|
||
// 2 * n_hid * 4 bytes. At n_hid=128 → 1 KiB. Far below 48 KiB limit,
|
||
// no cudaFuncSetAttribute needed.
|
||
|
||
// Block-per-batch (Phase B): grid=(n_batch, 1, 1), block=(n_hid, 1, 1).
|
||
// Each block handles ONE batch's n_hid channels in parallel. Removes
|
||
// the internal batch loop — frees ~31 SMs per launch at B=32.
|
||
extern "C" __global__ void cfc_step_batched(
|
||
const float* __restrict__ w_in, // [n_hid, n_in], row-major
|
||
const float* __restrict__ w_rec, // [n_hid, n_hid], row-major
|
||
const float* __restrict__ b, // [n_hid]
|
||
const float* __restrict__ tau, // [n_hid]
|
||
const float* __restrict__ x, // [n_batch, n_in]
|
||
const float* __restrict__ h_old, // [n_batch, n_hid]
|
||
float dt_s,
|
||
int n_in,
|
||
int n_hid,
|
||
int n_batch,
|
||
float* __restrict__ h_new // [n_batch, n_hid]
|
||
) {
|
||
// Dynamic shared layout (caller supplies (n_in + n_hid) * 4 bytes):
|
||
// s_x [n_in] — staged x_b row (read by every channel thread)
|
||
// s_h_old [n_hid] — staged h_old_b row (same)
|
||
extern __shared__ float smem_fwd[];
|
||
float* s_x = smem_fwd;
|
||
float* s_h_old = s_x + n_in;
|
||
|
||
int bi = blockIdx.x;
|
||
int tid = threadIdx.x;
|
||
if (bi >= n_batch) return;
|
||
|
||
// Cooperative staging: 128× redundant DRAM reads per row → 1× per row
|
||
// shared across all channel threads. Both rows fit since n_in = n_hid
|
||
// = HIDDEN.
|
||
if (tid < n_in) s_x[tid] = x[(long long)bi * n_in + tid];
|
||
if (tid < n_hid) s_h_old[tid] = h_old[(long long)bi * n_hid + tid];
|
||
__syncthreads();
|
||
|
||
if (tid >= n_hid) return;
|
||
const int i = tid;
|
||
const float bias_i = b[i];
|
||
const float decay = expf(-dt_s / fmaxf(tau[i], 1e-6f));
|
||
const float one_minus_decay = 1.0f - decay;
|
||
|
||
float pre = bias_i;
|
||
for (int k = 0; k < n_in; ++k) pre += w_in[i * n_in + k] * s_x[k];
|
||
for (int k = 0; k < n_hid; ++k) pre += w_rec[i * n_hid + k] * s_h_old[k];
|
||
h_new[(long long)bi * n_hid + i] =
|
||
s_h_old[i] * decay + one_minus_decay * tanhf(pre);
|
||
}
|
||
|
||
|
||
// Block-per-batch backward.
|
||
// grid=(n_batch, 1, 1) block=(n_hid, 1, 1)
|
||
// shared mem: 2 * n_hid * 4 bytes (sd_pre + sdecay; one row each)
|
||
//
|
||
// Param-grad scratch tensors hold per-batch slices. Thread (bi, i) is
|
||
// sole writer to its slice of each scratch tensor for every K-iteration
|
||
// the K-loop calls this kernel — no race even with B parallel blocks.
|
||
// Caller zeroes the scratch buffers once per training step; this kernel
|
||
// uses += to accumulate across the K-loop's 64 invocations.
|
||
//
|
||
// Outputs grad_h_old and grad_x are per-batch indexed; each block bi is
|
||
// sole writer to its [bi, :] slice (overwrite, single launch).
|
||
extern "C" __global__ void cfc_step_backward_batched(
|
||
const float* __restrict__ w_in, // [n_hid, n_in]
|
||
const float* __restrict__ w_rec, // [n_hid, n_hid]
|
||
const float* __restrict__ b, // [n_hid]
|
||
const float* __restrict__ tau, // [n_hid]
|
||
const float* __restrict__ x, // [n_batch, n_in]
|
||
const float* __restrict__ h_old, // [n_batch, n_hid]
|
||
const float* __restrict__ grad_h_new, // [n_batch, n_hid]
|
||
float dt_s,
|
||
int n_in,
|
||
int n_hid,
|
||
int n_batch,
|
||
float* __restrict__ grad_w_in_scratch, // [n_batch, n_hid, n_in] accum +=
|
||
float* __restrict__ grad_w_rec_scratch, // [n_batch, n_hid, n_hid] accum +=
|
||
float* __restrict__ grad_b_scratch, // [n_batch, n_hid] accum +=
|
||
float* __restrict__ grad_tau_scratch, // [n_batch, n_hid] accum +=
|
||
float* __restrict__ grad_h_old, // [n_batch, n_hid] overwrite
|
||
float* __restrict__ grad_x // [n_batch, n_in] overwrite (nullptr OK)
|
||
) {
|
||
extern __shared__ float smem[];
|
||
// Shared layout — caller passes (2*n_hid + n_in + n_hid) * 4 bytes:
|
||
// sd_pre [n_hid] — d_pre_b per channel (set in Pass 0, consumed Pass 1..3)
|
||
// sdecay [n_hid] — reserved for future passes / kept for ABI symmetry
|
||
// s_x [n_in] — staged x_b row (eliminates 128× redundant DRAM reads)
|
||
// s_h_old [n_hid] — staged h_old_b row (same)
|
||
float* sd_pre = smem;
|
||
float* sdecay = sd_pre + n_hid;
|
||
float* s_x = sdecay + n_hid;
|
||
float* s_h_old = s_x + n_in;
|
||
|
||
int bi = blockIdx.x;
|
||
int tid = threadIdx.x;
|
||
// block_dim is >= max(n_hid, n_in); each thread serves both roles
|
||
// across passes. Most paths gate on whether (tid < n_hid) / (tid < n_in).
|
||
if (bi >= n_batch) return;
|
||
|
||
// Cooperative staging — coalesced loads, single sync.
|
||
if (tid < n_in) s_x[tid] = x[(long long)bi * n_in + tid];
|
||
if (tid < n_hid) s_h_old[tid] = h_old[(long long)bi * n_hid + tid];
|
||
__syncthreads();
|
||
|
||
// Pass 0: compute pre[i], d_pre[i], decay[i]; write per-channel scratch.
|
||
// Thread tid serves the channel role i = tid. Reads from shared.
|
||
float dh_b = 0.0f;
|
||
float decay_i = 0.0f;
|
||
float s_i = 0.0f;
|
||
if (tid < n_hid) {
|
||
const int i = tid;
|
||
const float bias_i = b[i];
|
||
const float tau_eps = 1e-6f;
|
||
const float tau_i_raw = tau[i];
|
||
const float tau_eff = fmaxf(tau_i_raw, tau_eps);
|
||
decay_i = expf(-dt_s / tau_eff);
|
||
sdecay[i] = decay_i;
|
||
dh_b = grad_h_new[(long long)bi * n_hid + i];
|
||
|
||
float pre = bias_i;
|
||
for (int k = 0; k < n_in; ++k) pre += w_in[i * n_in + k] * s_x[k];
|
||
for (int k = 0; k < n_hid; ++k) pre += w_rec[i * n_hid + k] * s_h_old[k];
|
||
s_i = tanhf(pre);
|
||
const float d_pre_b = dh_b * (1.0f - decay_i) * (1.0f - s_i * s_i);
|
||
sd_pre[i] = d_pre_b;
|
||
|
||
// Per-channel scratch (one float per thread, no coalescing concern).
|
||
grad_b_scratch[(long long)bi * n_hid + i] += d_pre_b;
|
||
|
||
// Branchless tau-grad — gates contribution by (tau > eps) without
|
||
// warp divergence.
|
||
const float d_decay_b = dh_b * (s_h_old[i] - s_i);
|
||
const float gate = (tau_i_raw > tau_eps) ? 1.0f : 0.0f;
|
||
const float contrib = d_decay_b * decay_i * dt_s / (tau_eff * tau_eff);
|
||
grad_tau_scratch[(long long)bi * n_hid + i] += gate * contrib;
|
||
}
|
||
__syncthreads();
|
||
|
||
// Pass 1: grad_w_in[bi, i, k] += d_pre[i] * x[k]
|
||
// grad_w_rec[bi, i, k] += d_pre[i] * h_old[k]
|
||
// Thread tid serves the column role k = tid. For each i, every warp
|
||
// thread writes to grad_w_*[i, tid] → COALESCED 128B/transaction.
|
||
// Prior layout (thread = i) strided cross-thread writes by n_in/n_hid
|
||
// = 512B, paying 32 separate transactions per warp instruction.
|
||
if (tid < n_in) {
|
||
const int k = tid;
|
||
const float x_k = s_x[k];
|
||
const float h_old_k = s_h_old[k]; // valid because n_in == n_hid here
|
||
for (int i = 0; i < n_hid; ++i) {
|
||
const float d_pre_i = sd_pre[i];
|
||
grad_w_in_scratch[((long long)bi * n_hid + i) * n_in + k]
|
||
+= d_pre_i * x_k;
|
||
grad_w_rec_scratch[((long long)bi * n_hid + i) * n_hid + k]
|
||
+= d_pre_i * h_old_k;
|
||
}
|
||
}
|
||
|
||
// Pass 2: grad_h_old[bi, i] = sum_j sd_pre[j] * W_rec[j, i] + dh_b * decay.
|
||
// Thread tid is back to channel role i = tid; dh_b and decay_i
|
||
// stayed in registers from Pass 0.
|
||
if (tid < n_hid) {
|
||
const int i = tid;
|
||
float gh = dh_b * decay_i;
|
||
for (int j = 0; j < n_hid; ++j) {
|
||
gh += sd_pre[j] * w_rec[j * n_hid + i];
|
||
}
|
||
grad_h_old[(long long)bi * n_hid + i] = gh;
|
||
}
|
||
|
||
// Pass 3: grad_x[bi, k] = sum_j sd_pre[j] * W_in[j, k]
|
||
// Thread tid serves column role k = tid. Sequential along j, sd_pre
|
||
// in shared. One coalesced write per thread (grad_x layout matches).
|
||
if (grad_x != nullptr && tid < n_in) {
|
||
const int k = tid;
|
||
float gx = 0.0f;
|
||
for (int j = 0; j < n_hid; ++j) {
|
||
gx += sd_pre[j] * w_in[j * n_in + k];
|
||
}
|
||
grad_x[(long long)bi * n_in + k] = gx;
|
||
}
|
||
}
|