spb/forge Public MIT
Forge — LLM training from scratch in pure C++20 + Metal on Apple Silicon.
C++ 61.2%
C 23%
Python 7.6%
TeX 7.2%
CMake 1.1%
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// Unfused-but-GPU-resident causal attention with GQA (M4 correctness path;4// the flash-style fused kernel is M5). Probabilities P [B,H,T,T] are5// materialized in f32 for the backward pass — exactly mirroring the CPU6// reference so parity is bit-for-bit meaningful.7//8// Parallelization: one THREAD per query row (b,h,i) in fwd/dq (grid9// B*H*T), and one thread per KV row (b,h,j) in dkv. dkv iterates the10// q-heads sharing its kv-head, so dk/dv accumulate privately — no atomics,11// deterministic.12//13// Layouts: q/o [B,T,H*hd], k/v [B,T,Hkv*hd], P [B,H,T,T] row-major.14#include <metal_stdlib>15using namespace metal;1617struct AttnParams {18 uint B, T, H, HKV, HD;19 float scale;20 uint causal;21 uint window; // sliding window (keys kept, self incl.); 0 = full22 float softcap; // cap*tanh(s/cap) pre-softmax; 0 = off23};2425kernel void attention_fwd_f32(device const float* Q [[buffer(0)]],26 device const float* K [[buffer(1)]],27 device const float* V [[buffer(2)]],28 device float* O [[buffer(3)]],29 device float* P [[buffer(4)]],30 constant AttnParams& p [[buffer(5)]],31 uint gid [[thread_position_in_grid]]) {32 const uint i = gid % p.T;33 const uint h = (gid / p.T) % p.H;34 const uint b = gid / (p.T * p.H);35 if (b >= p.B) return;3637 const uint rep = p.H / p.HKV;38 const uint hkv = h / rep;39 const uint Cq = p.H * p.HD;40 const uint Ckv = p.HKV * p.HD;4142 device const float* qi = Q + (ulong(b) * p.T + i) * Cq + h * p.HD;43 device float* prow_base = P + ((ulong(b) * p.H + h) * p.T + i) * p.T;44 const uint jmax = p.causal ? i : p.T - 1;45 const uint jmin = (p.window > 0 && i + 1 > p.window) ? i + 1 - p.window : 0;4647 float m = -FLT_MAX;48 for (uint j = jmin; j <= jmax; ++j) {49 device const float* kj = K + (ulong(b) * p.T + j) * Ckv + hkv * p.HD;50 float s = 0.0f;51 for (uint d = 0; d < p.HD; ++d) s = fma(qi[d], kj[d], s);52 s *= p.scale;53 if (p.softcap > 0.0f) s = p.softcap * precise::tanh(s / p.softcap);54 prow_base[j] = s;55 m = max(m, s);56 }57 float sum = 0.0f;58 for (uint j = jmin; j <= jmax; ++j) {59 const float e = exp(prow_base[j] - m);60 prow_base[j] = e;61 sum += e;62 }63 const float inv = 1.0f / sum;64 for (uint j = 0; j < jmin; ++j) prow_base[j] = 0.0f;65 for (uint j = jmin; j <= jmax; ++j) prow_base[j] *= inv;66 for (uint j = jmax + 1; j < p.T; ++j) prow_base[j] = 0.0f;6768 device float* oi = O + (ulong(b) * p.T + i) * Cq + h * p.HD;69 for (uint d = 0; d < p.HD; ++d) oi[d] = 0.0f;70 for (uint j = jmin; j <= jmax; ++j) {71 const float prob = prow_base[j];72 if (prob == 0.0f) continue;73 device const float* vj = V + (ulong(b) * p.T + j) * Ckv + hkv * p.HD;74 for (uint d = 0; d < p.HD; ++d) oi[d] = fma(prob, vj[d], oi[d]);75 }76}7778// D[b,h,i] = dO_i · O_i. This is the FlashAttention-2 preprocessing term and79// it equals rowsum(dP ∘ P) exactly:80// Σ_j P_ij (dO_i · V_j) = dO_i · (Σ_j P_ij V_j) = dO_i · O_i81// Computing it once per query row instead of per (i,j) pair takes the dk/dv82// kernel from O(T³·hd) to O(T²·hd).83kernel void attention_bwd_d_f32(device const float* O [[buffer(0)]],84 device const float* dO [[buffer(1)]],85 device float* D [[buffer(2)]],86 constant AttnParams& p [[buffer(3)]],87 uint gid [[thread_position_in_grid]]) {88 const uint i = gid % p.T;89 const uint h = (gid / p.T) % p.H;90 const uint b = gid / (p.T * p.H);91 if (b >= p.B) return;9293 const uint Cq = p.H * p.HD;94 device const float* oi = O + (ulong(b) * p.T + i) * Cq + h * p.HD;95 device const float* doi = dO + (ulong(b) * p.T + i) * Cq + h * p.HD;96 float acc = 0.0f;97 for (uint d = 0; d < p.HD; ++d) acc = fma(doi[d], oi[d], acc);98 D[(ulong(b) * p.H + h) * p.T + i] = acc;99}100101// dq for one query row (b,h,i): dq_i = Σ_j dS_ij * K_j * scale, where102// dS = P ∘ (dP − D_i) and dP_ij = dO_i · V_j.103kernel void attention_bwd_dq_f32(device const float* Q [[buffer(0)]],104 device const float* K [[buffer(1)]],105 device const float* V [[buffer(2)]],106 device const float* P [[buffer(3)]],107 device const float* dO [[buffer(4)]],108 device const float* D [[buffer(5)]],109 device float* dQ [[buffer(6)]],110 constant AttnParams& p [[buffer(7)]],111 uint gid [[thread_position_in_grid]]) {112 const uint i = gid % p.T;113 const uint h = (gid / p.T) % p.H;114 const uint b = gid / (p.T * p.H);115 if (b >= p.B) return;116117 const uint rep = p.H / p.HKV;118 const uint hkv = h / rep;119 const uint Cq = p.H * p.HD;120 const uint Ckv = p.HKV * p.HD;121122 device const float* prow = P + ((ulong(b) * p.H + h) * p.T + i) * p.T;123 device const float* doi = dO + (ulong(b) * p.T + i) * Cq + h * p.HD;124 device float* dqi = dQ + (ulong(b) * p.T + i) * Cq + h * p.HD;125126 const float row_dot = D[(ulong(b) * p.H + h) * p.T + i];127 for (uint j = 0; j < p.T; ++j) {128 const float prob = prow[j];129 if (prob == 0.0f) continue;130 device const float* vj = V + (ulong(b) * p.T + j) * Ckv + hkv * p.HD;131 device const float* kj = K + (ulong(b) * p.T + j) * Ckv + hkv * p.HD;132 float dp = 0.0f;133 for (uint d = 0; d < p.HD; ++d) dp = fma(doi[d], vj[d], dp);134 float ds = prob * (dp - row_dot) * p.scale;135 if (p.softcap > 0.0f) {136 device const float* qq = Q + (ulong(b) * p.T + i) * Cq + h * p.HD;137 float s = 0.0f;138 for (uint d = 0; d < p.HD; ++d) s = fma(qq[d], kj[d], s);139 const float t = precise::tanh(s * p.scale / p.softcap);140 ds *= 1.0f - t * t;141 }142 for (uint d = 0; d < p.HD; ++d) dqi[d] = fma(ds, kj[d], dqi[d]);143 }144}145146// dk/dv for one KV row (b,hkv,j): sums over the q-heads sharing this147// kv-head and all query rows i (P_ij = 0 above the diagonal already).148kernel void attention_bwd_dkv_f32(device const float* Q [[buffer(0)]],149 device const float* K [[buffer(1)]],150 device const float* V [[buffer(2)]],151 device const float* P [[buffer(3)]],152 device const float* dO [[buffer(4)]],153 device const float* D [[buffer(5)]],154 device float* dK [[buffer(6)]],155 device float* dV [[buffer(7)]],156 constant AttnParams& p [[buffer(8)]],157 uint gid [[thread_position_in_grid]]) {158 const uint j = gid % p.T;159 const uint hkv = (gid / p.T) % p.HKV;160 const uint b = gid / (p.T * p.HKV);161 if (b >= p.B) return;162163 const uint rep = p.H / p.HKV;164 const uint Cq = p.H * p.HD;165 const uint Ckv = p.HKV * p.HD;166167 device const float* vj = V + (ulong(b) * p.T + j) * Ckv + hkv * p.HD;168 device float* dkj = dK + (ulong(b) * p.T + j) * Ckv + hkv * p.HD;169 device float* dvj = dV + (ulong(b) * p.T + j) * Ckv + hkv * p.HD;170171 for (uint r = 0; r < rep; ++r) {172 const uint h = hkv * rep + r;173 for (uint i = 0; i < p.T; ++i) {174 const float prob = P[((ulong(b) * p.H + h) * p.T + i) * p.T + j];175 if (prob == 0.0f) continue;176 device const float* qi = Q + (ulong(b) * p.T + i) * Cq + h * p.HD;177 device const float* doi = dO + (ulong(b) * p.T + i) * Cq + h * p.HD;178179 const float row_dot = D[(ulong(b) * p.H + h) * p.T + i];180 float dp = 0.0f;181 for (uint d = 0; d < p.HD; ++d) dp = fma(doi[d], vj[d], dp);182 float ds = prob * (dp - row_dot) * p.scale;183 if (p.softcap > 0.0f) {184 device const float* kk = K + (ulong(b) * p.T + j) * Ckv + hkv * p.HD;185 float s = 0.0f;186 for (uint d = 0; d < p.HD; ++d) s = fma(qi[d], kk[d], s);187 const float t = precise::tanh(s * p.scale / p.softcap);188 ds *= 1.0f - t * t;189 }190 for (uint d = 0; d < p.HD; ++d) {191 dvj[d] = fma(prob, doi[d], dvj[d]);192 dkj[d] = fma(ds, qi[d], dkj[d]);193 }194 }195 }196}197