Strix Halo Kernels (gfx1151)
Fused Triton kernels for AMD RDNA3.5 / gfx1151 β Strix Halo, the Radeon 8050S/8060S iGPU in Ryzen AI Max parts.
Why these kernels, on this hardware
Strix Halo inverts the usual trade-off: up to 64 GiB addressable as VRAM out of 128 GB unified, but far lower memory bandwidth than a discrete card. I measured a ~21Γ gap between prefill and decode throughput on this part (benchmarks), which is the signature of a bandwidth-bound machine.
That changes which optimizations matter. Fusing away memory round-trips is worth more here than on hardware where compute is the constraint β so these kernels target the ops that are pure memory traffic in eager PyTorch.
Also, practically: the mainstream Hub kernels are CUDA-only. kernels-community/activation
offers 41 build variants and none of them resolve on ROCm β every one is rejected with
backend (cu128) does not match selected backend (rocm713). Triton sidesteps this
entirely by JIT-compiling for whatever GPU is present.
Measured on gfx1151
fp16, versus eager PyTorch, via triton.testing.do_bench:
| kernel | shape | eager | this kernel | speedup |
|---|---|---|---|---|
| RMSNorm | 512Γ4096 | 0.205 ms | 0.072 ms | 2.84Γ |
| RMSNorm | 2048Γ4096 | 1.592 ms | 0.278 ms | 5.73Γ |
| RMSNorm | 4096Γ5120 | 3.703 ms | 0.508 ms | 7.29Γ |
| RMSNorm | 8192Γ2048 | 3.028 ms | 0.387 ms | 7.82Γ |
| GEGLU | 512Γ8192 | 0.233 ms | 0.197 ms | 1.18Γ |
| GEGLU | 2048Γ8192 | 0.843 ms | 0.554 ms | 1.52Γ |
| GEGLU | 4096Γ4096 | 0.838 ms | 0.550 ms | 1.52Γ |
RMSNorm gains the most, and the gain grows with size β eager does several full passes over the activation, this does one. GEGLU's ceiling is lower because even the fused form must read two inputs and write one output; the win is only the eliminated intermediate.
Flash attention β read the baseline note carefully
PyTorch on gfx1151 reports no flash or memory-efficient SDPA backend by default, so
scaled_dot_product_attention falls back to math, which materializes the full SΓS score
matrix. But there is an AOTriton path β it is gated behind an environment variable:
export TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1
If you run Strix Halo and change one thing after reading this, change that. Enabling it takes attention at (1,32,4096,128) from 204 ms to 12 ms β a ~17Γ win, for free, before any custom kernel is involved.
Against that properly enabled baseline, this kernel is still faster (fp16, causal):
| shape | default (math) | AOTriton flash | this kernel | vs AOTriton |
|---|---|---|---|---|
| (1,32,512,128) | 3.95 ms | 0.530 ms | 0.162 ms | 3.27Γ |
| (1,32,4096,128) | 204.3 ms | 12.14 ms | 5.84 ms | 2.08Γ |
| (2,24,4096,64) | 304.2 ms | 11.97 ms | 4.00 ms | 3.00Γ |
| (1,16,8192,64) | 342.9 ms | 15.43 ms | 6.06 ms | 2.55Γ |
Peak allocation at (1,32,4096,128): 5056 MiB β 160 MiB, because the SΓS matrix is never written.
The honest summary: ~2β3Γ over the vendor path once it is enabled, and ~25β68Γ over what you get if you do not know the env var exists. Quote the first number.
Block sizes are chosen by triton.autotune per (seq_len, head_dim, causal); on gfx1151 it
picks BLOCK_M=32, BLOCK_N=64, num_warps=4, num_stages=2.
Using it
import torch.nn.functional as F
k = get_kernel("axjns/strix-halo-kernels", version=1)
F.scaled_dot_product_attention = k.sdpa # safe: falls back for unsupported inputs
sdpa defers to torch for explicit attn_mask, nonzero dropout_p, GQA/MQA, non-4-D
inputs, non-power-of-two head_dim, fp32, and CPU tensors β it never silently returns
wrong numbers for a case the kernel cannot handle.
Correctness: 24 checks against an fp32 reference (not the fp16 baseline) across causal and non-causal, fp16 and bf16, head_dim 64 and 128, and sequence lengths including non-multiples of the block size (77, 333). Max error 9.8e-4 (fp16), 8.4e-3 (bf16).
Attention is forward-only too β no backward pass.
Usage
import torch
from kernels import get_kernel
k = get_kernel("axjns/strix-halo-kernels", version=1)
# fused RMSNorm over the last dim
y = k.rms_norm(x, weight, eps=1e-6)
# gated activations
h = k.swiglu(gate, up) # silu(gate) * up β LLaMA/Qwen/Mistral MLPs
h = k.geglu(gate, up) # gelu(gate) * up β Flux / SD3 / T5 MLPs
h = k.geglu_chunked(proj_out) # splits (..., 2*d) then gates
With an existing model
layers provides nn.Module drop-ins that keep the original parameter names, so no
weight surgery or state-dict changes are needed:
from kernels import kernelize # swaps compatible layers in-place
layers.RMSNormβ readsself.weightand accepts eithervariance_epsilonoreps, since transformers has used both names across versions.layers.SwiGLUβ expectsgate_proj/up_proj/down_proj(LLaMA-family MLP).layers.GEGLUβ expects aprojemitting2 * inner_dim, then chunks and gates.
Correctness
Validated against eager PyTorch references (also exported, as rms_norm_ref,
swiglu_ref, geglu_ref) across fp32 / fp16 / bf16, 2-D and 3-D inputs,
non-contiguous inputs, and shapes from 4Γ512 to 4096Γ5120. Sum-of-squares is
accumulated in fp32 regardless of input dtype β fp16 accumulation over a
4096-wide row loses precision badly.
Max observed absolute error: 1.9e-6 (fp32), 7.8e-3 (fp16), 6.3e-2 (bf16) β consistent with the dtype's own rounding.
Limitations β read before using
- Forward only. There is no backward pass. These are
torch.autograd-opaque, so gradients will not flow through them. Inference and eval only; do not put these in a training graph expecting it to work. - Measured on one part. Developed and benchmarked on gfx1151 only. The source is not gfx1151-specific and should run anywhere Triton runs, but the numbers above apply to this GPU, at ROCm 7.13 / torch 2.11 / Triton 3.6.
- Tuning is uneven. Attention uses
triton.autotuneand will re-tune on other GPUs. RMSNorm and the gated activations use hand-picked block sizes andnum_warps, chosen for RDNA's 32-wide wavefront and an APU's low CU count β a discrete GPU would likely want different values, and those two are not yet autotuned. - Requires Triton, which ships with PyTorch ROCm builds.
- RMSNorm assumes the normalized dimension is last and contiguous; non-contiguous input is copied, which costs a pass.
Reproducing
python test_bench.py # RMSNorm + gated activations
python test_flash.py # attention correctness + headroom
python sdpa_probe.py # which SDPA backends your build actually has
sdpa_probe.py is worth running on any ROCm box before you optimize anything β it tells
you whether you are silently on the math fallback.