Files
foxhunt/crates/ml-alpha/cuda/cfc_step.cu
jgrusewski 753284f2ae feat(ml-alpha): backward kernels — heads + cfc_step (K=1 BPTT)
multi_horizon_heads_backward: sigmoid + linear chain rule. One block,
HIDDEN_DIM=128 threads. Computes grad_w, grad_b, grad_h_in. No
atomicAdd; per-thread accumulation only.

cfc_step_backward: truncated K=1 BPTT through one CfC time step.
Forward pre/decay/tanh recomputed inside the kernel; emits grad_w_in,
grad_w_rec, grad_b, grad_h_old. tau is held frozen (structural
log-uniform init per Hasani 2022; backprop through tau deferred to
Phase A v2 if the gate needs it). Uses dynamic shared memory for the
d_pre relay between threads (size = 2 * n_hid * 4 bytes).

Tests (4/4 on sm_86) validate via on-GPU finite-difference:
  - heads grad_h vs forward(h±eps) → matches at eps=1e-3, rel<=1%
  - heads grad_b vs forward(b±eps) → matches at eps=1e-3, rel<=1%
  - cfc grad_b vs forward(b±eps) → matches at eps=1e-3, rel<=5%
  - cfc grad_h_old vs forward(h_old±eps) → matches at eps=1e-3, rel<=5%

CPU is not the reference (per feedback_no_cpu_test_fallbacks.md). The
kernel is the truth; numerical perturbation validates the analytic
gradient against the kernel's own forward.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:20:54 +02:00

111 lines
4.0 KiB
Plaintext

// cfc_step.cu — one CfC time step (Hasani 2022 closed-form recurrence).
//
// 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]
// decay[i] = exp(-dt / max(tau[i], 1e-6))
// 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_pre[i] = grad_h_new[i] * (1 - decay) * (1 - s^2)
// 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 held frozen (structurally log-uniform initialised per spec
// Section 2); no grad_tau path in this version.
//
// 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_h_old // [n_hid]
) {
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 decay = expf(-dt_s / fmaxf(tau[i], 1e-6f));
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();
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_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;
}