training.h is the transformer training kit: RMSnorm, LayerNorm, RoPE,
SwiGLU, softmax, AdamW, and the fused-RMSnorm-GEMV inference primitive.
Apple builds use kernels/metal/training_kernels.metal (and
fused_norm_gemv.metal for the fusion). Portable CPU builds use
lib/ops/training_cpu.cpp. CUDA builds with TC_ENABLE_CUDA=ON route
managed-memory RMSNorm forward/backward, LayerNorm forward/backward, RoPE
forward/backward, SwiGLU forward/backward, softmax forward/backward, and
fp32/fp16-gradient AdamW through lib/cuda/training.cu after CUDA
initialization; host-only buffers fall back to the portable CPU path. Set
TC_DISABLE_CUDA_GEMM=1,
TC_CUDA_GEMM=0, or TC_USE_CUDA_GEMM=0 to force CPU fallback.
This page describes shapes, the kernel design, and the conventions you need to plug them into a training step.
tc_status_t tc_rmsnorm_forward (ctx, X, gamma, Y, rstd_out, N, D, eps);
tc_status_t tc_rmsnorm_backward(ctx, X, gamma, dY, rstd, dX, dgamma, N, D);| Buffer | Shape | dtype |
|---|---|---|
X |
[N, D] |
fp16 |
gamma |
[D] |
fp16 |
Y |
[N, D] |
fp16 |
rstd_out |
[N] |
fp32 (saved for backward) |
dY |
[N, D] |
fp16 |
dX |
[N, D] |
fp16 |
dgamma |
[D] |
fp32 (accumulator dtype — feeds directly into tc_adamw_step with grad_dtype=TC_DTYPE_F32) |
Math:
rms(x) = sqrt(mean(x^2) + eps)
y = (x / rms) * gamma
Compared with LayerNorm, RMSnorm drops the mean-subtraction and the bias. This is the normalization used by Llama, Mistral, Qwen, and basically every modern open LLM since 2023.
Kernel design: one threadgroup per row. Per row we compute mean(x^2)
via a simdgroup-level sum reduction, then broadcast rstd back, then
apply y[i] = x[i] * rstd * gamma[i] in fp32 internally, cast to fp16 on
write.
tc_status_t tc_layernorm_forward (ctx, X, gamma, beta, Y, mean_out, rstd_out,
N, D, eps);
tc_status_t tc_layernorm_backward(ctx, X, gamma, dY, mean, rstd, dX, N, D);| Buffer | Shape | dtype |
|---|---|---|
X |
[N, D] |
fp16 |
gamma, beta |
[D] |
fp16 |
Y |
[N, D] |
fp16 |
mean_out, rstd_out |
[N] |
fp32 |
Same per-row threadgroup layout as RMSnorm, with both the mean and the inverse standard deviation saved for backward.
tc_status_t tc_rope_forward(ctx, X, cos_t, sin_t, batch, heads, seq, head_dim);
tc_status_t tc_rope_backward(ctx, dX, cos_t, sin_t, batch, heads, seq, head_dim);| Buffer | Shape | dtype | Notes |
|---|---|---|---|
X / dX |
[B, H, S, D] |
fp16 | in-place |
cos_t |
[S, D/2] |
fp32 | precomputed by host |
sin_t |
[S, D/2] |
fp32 | precomputed by host |
For each (b, h, s, k) where k < D/2, the kernel rotates the pair
(X[b, h, s, k], X[b, h, s, k + D/2]) by (cos_t[s, k], sin_t[s, k]).
The backward pass applies the inverse rotation to dX:
dx0 = dy0 * cos + dy1 * sin, dx1 = -dy0 * sin + dy1 * cos.
This matches Llama / Mistral's RoPE convention (half-rotation grouping,
not the interleaved variant some PyTorch implementations use).
Compute cos_t and sin_t on the host once per sequence length:
for (int s = 0; s < S; ++s) {
for (int k = 0; k < D/2; ++k) {
float freq = powf(rope_base, -2.f * k / D); /* rope_base = 10000 */
float angle = (float)s * freq;
cos_t[s][k] = cosf(angle);
sin_t[s][k] = sinf(angle);
}
}tc_gguf_get_llama_config returns rope_freq_base and rope_freq_scale
in case you're loading a model whose RoPE was scaled.
tc_status_t tc_swiglu_forward (ctx, gate, up, out, n);
tc_status_t tc_swiglu_backward(ctx, gate, up, dout, dgate, dup, n);| Buffer | Shape | dtype |
|---|---|---|
gate, up, out, dout, dgate, dup |
[n] |
fp16 |
Math:
silu(x) = x / (1 + exp(-x))
out = silu(gate) * up
Pointwise; one thread per element. Used in the MLP of every modern LLM
(o = down(silu(gate(x)) * up(x))). The corresponding GEMMs are
tc_gemm calls; SwiGLU is the elementwise glue.
tc_status_t tc_softmax_forward (ctx, X, Y, N, D);
tc_status_t tc_softmax_backward(ctx, Y, dY, dX, N, D);| Buffer | Shape | dtype |
|---|---|---|
X, Y, dY, dX |
[N, D] |
fp16 |
Numerically stable row-wise softmax: per-row max subtraction, exp, sum, divide. Used outside attention (e.g. final logits, mixture-of-experts routing).
The attention kernel has its own softmax inlined (the FlashAttention online-softmax scheme); this standalone variant is for everything else.
tc_status_t tc_adamw_step(ctx,
params_fp32, m_fp32, v_fp32,
grads, grad_dtype,
n,
lr, beta1, beta2, eps, wd, bc1, bc2);| Buffer | Shape | dtype | Read/Write |
|---|---|---|---|
params_fp32 |
[n] |
fp32 | RW (master weights) |
m_fp32 |
[n] |
fp32 | RW (1st moment) |
v_fp32 |
[n] |
fp32 | RW (2nd moment) |
grads |
[n] |
fp16 or fp32 | R |
Math:
m = β1 * m + (1 - β1) * g
v = β2 * v + (1 - β2) * g²
m̂ = m / bc1 (bc1 = 1 - β1^t passed by host)
v̂ = v / bc2 (bc2 = 1 - β2^t passed by host)
θ = θ - lr * (m̂ / (√v̂ + eps) + wd * θ)
Two notes:
- fp32 master weights, fp16 grads. This is the standard mixed-precision training recipe; you keep the fp32 copy of the parameters, and the forward/backward produce fp16 grads which the optimizer reads.
- Bias corrections passed by host. The kernel doesn't know what step
it is. Compute
bc1 = 1 - powf(beta1, t),bc2 = 1 - powf(beta2, t)on the host and pass them in. This is one fp32 division saved per element per step.
One thread per element; pointwise.
tc_status_t tc_fused_rmsnorm_gemv(ctx, X, gamma, W, Y, M, N, K, eps);
tc_status_t tc_fused_layernorm_gemv(ctx, X, gamma, beta, W, Y, M, N, K, eps);| Buffer | Shape | dtype | Notes |
|---|---|---|---|
X |
[M, K] |
fp16 | typically M ≤ 4 (inference batch) |
gamma |
[K] |
fp16 | norm scale |
beta |
[K] |
fp16 | LayerNorm shift; only used by tc_fused_layernorm_gemv |
W |
[K, N] |
fp16 | projection weight |
Y |
[M, N] |
fp16 | Y = Norm(X, γ[, β]) @ W |
The hot path inside an LLM decode step is:
x_norm = RMSnorm(x) # write [hidden]
q = x_norm @ Wq # read [hidden], write [hidden]
k = x_norm @ Wk
v = x_norm @ Wv
That x_norm write/read round trip is pure memory traffic -- the normalized
vector is consumed three times immediately after being produced. The
fused kernels compute rstd inline, apply the normalization in-register
during the matmul accumulation, and skip the write-back entirely. The
LayerNorm variant also computes row mean and applies beta[k].
Two-pass intra-threadgroup design:
- Pass 1: each simdgroup computes
sum(x_i²)for a slice ofK; warp-reduce tomean(x²), thenrstd = rsqrt(mean + eps). - Pass 2: same threadgroup iterates over
K, multiplyingx[k] * rstd * gamma[k]while accumulating againstW[k, :]for the row ofYit owns.
For tc_fused_layernorm_gemv, pass 1 computes sum(x_i) and
sum(x_i²) so the kernel can derive mean and variance; pass 2 uses
((x[k] - mean) * rstd * gamma[k] + beta[k]) before multiplying by
W[k, :].
Caveats:
- Tuned for M ≤ 4. For training (M ≥ 32), use the separate norm-forward +
tc_gemmpath -- the per-row statistic recompute would dominate at larger M. - The fused kernels don't expose
rstd_outormean_out. If you need them for backward, use the separate path. The fused path is inference-only.
Validated by tests/test_fused_norm_gemv.c against the separate
tc_rmsnorm_forward + tc_gemm and tc_layernorm_forward + tc_gemm
paths at rms_scaled ≤ 5e-3.
Putting it together, one forward step of a Llama-style transformer block:
/* RMSnorm */
tc_rmsnorm_forward(ctx, X, gamma_attn, X_norm, rstd_attn, N, D, eps);
/* QKV projection */
tc_gemm(ctx, &qkv_desc, X_norm, W_qkv, QKV);
/* RoPE on Q and K, in-place */
tc_rope_forward(ctx, Q, cos_t, sin_t, B, H, S, head_dim);
tc_rope_forward(ctx, K, cos_t, sin_t, B, H_kv, S, head_dim);
/* Attention */
tc_attention_desc adesc = { ... return_lse=true ... };
tc_attention_forward(ctx, &adesc, Q, K, V, O, LSE);
/* O projection */
tc_gemm(ctx, &o_desc, O, W_o, X_after_attn);
/* Residual */
/* y = x + x_after_attn done with a small elementwise kernel or
tc_gemm with alpha=1, beta=1 trick */
/* Second RMSnorm */
tc_rmsnorm_forward(ctx, Y, gamma_mlp, Y_norm, rstd_mlp, N, D, eps);
/* MLP gate, up */
tc_gemm(ctx, &gate_desc, Y_norm, W_gate, GATE);
tc_gemm(ctx, &up_desc, Y_norm, W_up, UP);
/* SwiGLU */
tc_swiglu_forward(ctx, GATE, UP, GU, B * S * mlp_dim);
/* MLP down */
tc_gemm(ctx, &down_desc, GU, W_down, MLP_OUT);
/* Final residual */The backward mirrors with _backward variants; the optimizer step is one
tc_adamw_step per parameter group.
tests/test_transformer_block.c and tests/test_e2e_training.c build
exactly this pattern at small sizes; they're worth reading as a reference
for shape and buffer lifecycle.
- Fused-adamw for fp16 grads + bf16 master weight option
- Bias-add fused into
tc_gemmfor FFN