Kernels
relu / DTYPE_THROUGHPUT.md
superexpai's picture
Optimized relu: cpu/cuda/xpu, 1.2-1.85x faster on RTX 4090, benchmarked vs upstream and torch.relu
e873e70 verified
|
Raw
History Blame Contribute Delete
3.36 kB
# ReLU throughput vs dtype on RTX 4090 (sm_89)
ReLU is **memory-bound** (read N + write N, trivial compute). So at the GPU's
byte-bandwidth wall, **element throughput scales as 1/sizeof(dtype)** β€” precision
choice trades only bytes moved, not compute. Measured two ways (custom CUDA
kernel + `torch.relu`), all correctness-verified.
Harness: `bench/relu_dtype.cu` (custom, covers fp8/int8 that PyTorch has no
ReLU for) and a `torch.relu` cross-check. 128-bit (int4) vectorized loads so every
dtype gets an identical memory access pattern.
## Result 1 β€” intrinsic precision effect (DRAM-bound, N=64M, all > L2)
Byte-bandwidth is **dtype-invariant** (~943 GB/s = the wall); element rate = 1/sizeof.
| dtype | bytes | GB/s | Gelem/s | vs fp32 |
|-------|:---:|:---:|:---:|:---:|
| fp64 / int64 | 8 | ~943 | 59 | **0.5Γ—** |
| fp32 / int32 | 4 | ~944 | 118 | 1.0Γ— |
| **fp16 / bf16** / int16 | 2 | ~944 | 236 | **2.0Γ—** |
| **fp8 e4m3 / e5m2** / int8 / uint8 | 1 | ~941 | 471 | **4.0Γ—** |
Cross-checked in `torch.relu` @ 64M: fp64 0.50Γ—, bf16 2.01Γ—, fp16 2.01Γ—, int8 4.02Γ—
β€” identical to the custom kernel. fp16 and bf16 are exactly equal (both 2 bytes);
the format (5e10m vs 8e7m) is irrelevant to a memory-bound op. fp8 only measurable
via the custom kernel (`torch.relu` raises NotImplementedError for float8).
## Result 2 β€” L2-residency bonus (fixed element count near the 72 MB L2)
At a *fixed element count* where the low-precision working set fits in the 72 MB L2
but fp32 spills to DRAM, low precision gets an EXTRA super-linear win. `torch.relu`
@ N=16.7M (4096Β²):
| dtype | working set | GB/s | Gelem/s | vs fp32 | regime |
|-------|:---:|:---:|:---:|:---:|:---|
| fp32 | 134 MB | 935 | 117 | 1.0Γ— | DRAM (spills) |
| bf16 | 67 MB | 3753 | 938 | **8.0Γ—** | **L2-resident** |
| fp16 | 67 MB | 3598 | 900 | 7.7Γ— | L2-resident |
| int8 | 34 MB | 3631 | 1816 | **15.5Γ—** | L2-resident |
This is two effects stacked: 2Γ— (bf16 bytes) Γ— ~4Γ— (L2 vs DRAM BW). Real activation
tensors that hover around L2 size see exactly this β€” a strong reason to keep
activations in bf16/fp16.
## L2-resident pure (custom kernel, N=2M, all fit in L2)
Absolute L2 bandwidth is higher and rises with larger dtypes (more in-flight bytes
per int4 chunk): fp64 ~3.4 TB/s β†’ fp8 ~1.6 TB/s. Element rate still favors small
dtypes (fp8 ~808 vs fp32 ~395 Gelem/s) but sub-linearly, because tiny byte working
sets underutilize the memory system.
## Takeaways
1. **bf16/fp16 ReLU = 2Γ— fp32 throughput** intrinsically (memory-bound, fewer
bytes), and up to ~8Γ— at L2-boundary sizes. fp16 == bf16 for this op.
2. **fp8/int8 = 4Γ— fp32** intrinsically, up to ~15Γ— at L2-boundary sizes.
3. **fp64/int64 = 0.5Γ— fp32** β€” avoid 64-bit for elementwise.
4. The byte-bandwidth wall (~943 GB/s) is the same for every dtype; you cannot beat
it within one dtype β€” only move fewer bytes (lower precision) or skip the
round-trip (fusion).
5. The shipped `kernels-community/relu` kernel is **fp32-only** (`TORCH_CHECK`).
Adding bf16/fp16 (and int8/fp8) would unlock the 2–4Γ— shown here for the dtypes
real models actually use; the vectorized + streaming kernel design generalizes
directly (template on T, 16-byte loads, per-element relu).
## Reproduce
```
cd bench && nvcc -arch=sm_89 -O3 -o relu_dtype relu_dtype.cu && ./relu_dtype
```