HBM: off-chip memoryOne row in flightOn-chip SRAM: registers and shared memory239.7 GB/s effective bandwidth
Pass
Implementation
3072

Forward pass, Fused Triton, N = 3072. 239.7 GB/s effective bandwidth, 75% of the T4's nominal 320 GB/s. One round trip through HBM per row.

One trip in, one trip out: the whole reduction stays on chip.

No. 02

Fused RMSNorm

May–Jun 2026
Specification
Hardware NVIDIA T4 (Google Colab), nominal 320 GB/s
Benchmark 4,096 rows × N columns, fp16, N from 256 to 4096
Forward, fused ~240 GB/s peak (239.7 at N = 3,072), about 75% of nominal bandwidth
Forward, eager PyTorch ~62 GB/s
Forward, torch.compile ~240 GB/s; the fused kernel is at parity
Backward, fused ~54 GB/s, 2.7× eager
Backward, eager PyTorch ~20 GB/s
Backward, torch.compile 62 to 75 GB/s; the fused kernel is 28% behind at peak
Numerics fp32 accumulation; tested against a PyTorch reference in fp32, fp16, and bf16
Tuning triton.autotune over block sizes 512 to 8,192 and 4 to 16 warps

What it is

RMSNorm normalizes a row by its own root-mean-square and then scales it by a learned vector γ. Arithmetically that is almost nothing: a square, a mean, an add, a reciprocal square root, a multiply, and another multiply. The cost is entirely in moving the row. Every one of those steps, run as its own PyTorch kernel, reads the row out of HBM and writes it back — so the operation runs at a fraction of what the card can move, and the arithmetic is never the limit.

Fusing means writing all of it as one kernel: read the row once, keep everything in registers and on-chip memory, write the result once. That is the whole idea, and the measurement that matters is not FLOPs but effective bandwidth — how close to the card’s memory ceiling the kernel gets.

How it works

One Triton program per row, so the entire reduction fits in a single block and no cross-block communication is needed. The row is loaded once in fp16 or bf16 and accumulated in fp32, because summing thousands of squared fp16 values in fp16 drifts. Block size and warp count are left to triton.autotune, which sweeps block sizes from 512 to 8,192 and 4 to 16 warps and picks per shape; a pruning hook drops any configuration whose block is narrower than the row.

The backward pass computes dx the same way, and accumulates dγ across blocks with tl.atomic_add straight into the shared gradient vector.

Results

On a T4 with a nominal 320 GB/s, the fused forward kernel peaks at about 240 GB/s — roughly three quarters of the card’s rated bandwidth, and level with torch.compile across the sweep. Eager PyTorch sits near 62 GB/s, which is the cost of the round trips rather than the maths.

The backward pass reaches about 54 GB/s against eager’s 20. That is a real speedup and still the weakest part of the project, for a reason worth stating plainly.

What didn’t work

The backward pass does not beat torch.compile, and the reason is the dγ accumulation. Every block atomically adds its partial weight gradient into one shared N-element vector. Under a high block count those atomics contend on the same addresses and serialize, and the kernel spends its time waiting rather than moving bytes. TorchInductor avoids this entirely: it writes each block’s partial into a workspace buffer and then launches a second, fully vectorized kernel to reduce them. No atomics, no contention.

Selecting Backward and Fused in the scene above shows this as a queue of carts at a single loading dock. It is not a subtle effect — it is the entire gap.

The forward parity is also worth reading precisely. The fused kernel matches torch.compile on the forward pass. It does not match it on the backward pass, and describing the project as reaching parity without saying which direction would overstate it.

Next

Implement the two-stage dγ reduction — block partials to a workspace, then a separate reduction kernel — and re-benchmark. The interactive above is built to take a fourth implementation in the backward mode when that exists, so the comparison can be made in the same place rather than in a new chart.

Data from rmsnorm@ca31f9f