hk-dsl — HipKittens kernels in Python, over an MLIR dialect
Write Radeon kernels in Python, get HipKittens C++, get a .so, call it from
torch. The point is not convenience: it is that three classes of silently
wrong kernel on RDNA3 stop being expressible.
import hk
@hk.kernel(grid=lambda p: (hk.cdiv(p.o.cols, 64), hk.cdiv(p.o.rows, 16), p.o.batch))
def add(a: hk.Tensor[hk.bf16], b: hk.Tensor[hk.bf16], o: hk.Tensor[hk.bf16]):
idx = hk.tile_coord(hk.arch.block_idx.z, 0, hk.arch.block_idx.y,
hk.arch.block_idx.x)
t = hk.RT(hk.bf16, 16, 64)
o[idx] = a[idx].load(t) + b[idx].load(t)
add(a, b, out) # traces, verifies, translates, compiles (once), launches
The three traps
Each of these cost days to find by hand, and none of them produces a diagnostic:
s_waitcntcarries no register dependence, so the compiler will hoist a WMMA above the wait that was supposed to guard it. The translation binds the fragment with a volatile asm after every wait (IR).- A VGPR spill is silent corruption, not slowness, because the library
hand-places
s_waitcntand scratch traffic reorders against it.k.build()refuses to return a spilling kernel (runtime). - Only row-layout 16-bit operands reach
ds_read_b128; column layout degrades to eight times the instructions. The passes flag it while tracing, not at benchmark time (IR).
The whole argument is in why this exists.
Install
From a wheel. One per Python interpreter, because it carries the MLIR
bindings for one CPython; it includes the HipKittens headers, so at run time
only hipcc is needed. CPython 3.10 and 3.12 wheels are attached to each
GitHub release, or
build one with scripts/build_wheel.sh (building):
pip install hk_dsl-0.3.1-cp310-cp310-manylinux_2_35_x86_64.whl
From source. The backend is the third_party/HipKittens submodule, so
clone with --recursive:
git clone --recursive https://github.com/vivienfanghuagood/hk-dsl.git && cd hk-dsl
scripts/build_mlir.sh # MLIR 23.1.3 only, X86 target, Python bindings (~20 min on 64 cores)
scripts/build_hk_mlir.sh # the dialect, passes, translation and hk._mlir (~1 min)
torch is not a dependency: tracing and compiling do not need it, launching does. Details, memory limits and the variables the scripts take: getting started.
First kernel
With add above, each step needs more than the one before it:
print(add.trace().dump()) # the hk MLIR module, after the passes -- needs nothing
print(add.source()) # the HipKittens C++ it translates to -- needs nothing
print(add.build().report()) # hipcc + the resource gate -- no GPU
add(a, b, out) # launch -- a Radeon and torch
build() needing no GPU is deliberate: the failure most likely to bite —
generated code that compiles but spills — is caught on a laptop. The full
walk-through, with tensors and a check against torch, is
getting started.
Results
On one W7900D (gfx1100). Each number is from the development record, linked; performance has the full tables, the methods, and the losses.
| kernel | result | against | source |
|---|---|---|---|
| GEMM, bf16, Qwen3-8B's linear shapes | 82.1–85.7 TFLOP/s: 1.03–1.08x | the faster of Triton (autotuned) and FlyDSL; hipBLASLt 1.19–1.34x | the scoreboard |
| RMSNorm, fused add + RMSNorm, softmax | 0.37–0.97x, slower | the faster of Triton, FlyDSL and Inductor | the scoreboard |
| dropout, KV cache write, LayerNorm backward | 1.02x; 1.18–3.00x; 1.12x at 1024 wide, 0.66–0.92x (slower) at 2048–8192 | the Triton tutorial or a Triton row kernel | rows by index |
| attention forward | 61.0–64.9 TFLOPs, 0.999–1.002x (1.008x causal); 2.9–3.2x torch SDPA, 4.0x causal | the handwritten C++; torch SDPA | Phase 4 |
| norms, softmax, RoPE, SiLU-mul, quantize | 13 of 15 cases faster, the other two a tie | torch.compile |
Phases 1 and 2 |
| decode GEMV, 1–32 tokens | 1.93–2.03x per step | hipBLASLt | cross-workgroup reduction |
| paged decode attention | slower: 0.83–0.95x per layer at 16–64 requests on vLLM 0.31 (it won from 16 up on 0.16.1); up to 8 requests the backend runs Triton's kernel on hk's cache, and routing more to it slowed the captured step | vLLM 0.31's Triton attention, captured | vLLM 0.31 |
| paged prefill, cached prefixes and chunks included | 3.6–7.9x | vLLM's Triton unified_attention |
captured whole |
| Qwen3-8B in vLLM, linear-layer patch | 1.52–1.84x at batch 1, 1.10–1.35x at batch 16 | stock vLLM 0.16.1.dev0 | cross-workgroup reduction |
| Qwen3-8B in vLLM under its defaults, both patches | 1.48–1.85x per request at every shape; attention alone 1.007–1.48x | stock vLLM 0.16.1.dev0, CUDA graphs, prefix caching, chunked prefill | captured whole |
| Qwen3-8B in vLLM 0.31 under its defaults, both patches | 1.20–1.75x per request; attention alone 1.05–1.48x (1.01–1.39x the TRITON_ATTN backend it replaces) | stock vLLM 0.31.0 | vLLM 0.31 |
| MoE GEMM, bf16 | 1.00–2.77x at 1–256 tokens; slower above, where the layer stays on Triton | vLLM 0.31's Triton fused_moe |
MoE |
| OLMoE-1B-7B in vLLM 0.31, MoE patch / with hk's attention | 1.02–1.41x / 1.17–2.22x per request | stock vLLM 0.31.0 | MoE |
| W4A16 decode GEMM (AWQ), fp16 | 1.40–2.61x | vLLM 0.31's Triton AWQ kernel; slower (0.69–0.90x) than gptq_gemm_rdna3 at 1–4 tokens |
4-bit weights |
| Qwen3-8B-AWQ / Qwen3-14B-AWQ in vLLM 0.31, 4-bit patch | 1.07–3.80x / 1.16–4.43x per request; decode step to 4.46x / 5.52x | stock vLLM 0.31.0, fp16 | 4-bit weights |
Qwen3-8B served by SGLang 0.5.18, linear patch (HK_SGLANG_LINEAR=1) |
1.49–1.81x output throughput at concurrency 1–32 | stock SGLang with sglang-radeon, eager | SGLang |
shipped ops at 7-8B shapes (validate.py) |
GEMM 1.12x, decode GEMV 3.90–4.38x, attention 2.76x (3.94x causal), quantize 2.78x; norms 0.88–0.96x | torch eager, PyTorch 2.10 | validation |
Repository layout
hk/dsl/ the language: @kernel / @func, values and operators, the AST
rewrite, control flow, hk.arch / lds / math / group / collective
hk/types.py dtypes and the tile / vector / tensor types a kernel is written in
hk/compiler/ the MLIR context, the pass pipeline, the translation entry point
hk/_mlir/ the built bindings (hk dialect + MLIR core), not checked in
hk/target/ gfx1100 / gfx1201 facts, as data
hk/codegen/ the module boundary around a translated kernel (scaffold.py)
hk/runtime/ arch detection, hipcc + content-hash cache, the resource gate,
and the torch.ops registration (torch_ext.py)
hk/autotune/ schedule search: build the space, keep what passes the gate, time
hk/tuned/ the schedules that search picked, and the measured routing
tables, as shipped data
hk/ops/ kernels written in the DSL, plus the SDPA drop-in (sdpa.py)
hk/integration/ patching a running vLLM or SGLang; engine.py is what every
patch shares (op, routes, counters, CUDA-graph contract)
mlir/ the hk dialect (ODS), its passes, the C++ translation,
hk-opt / hk-translate, lit tests
include/ hk's own headers (pyutils/hk_bind.cuh, the fast pybind entry)
third_party/HipKittens/
the backend: kittens.cuh and include/rdna3, include/rdna4;
also the C++ kernels hk is measured against (kernels/rdna3)
scripts/ building MLIR, the dialect and the wheels
tools/hk-bench/ the benchmarks and engine harnesses behind every number here
examples/ complete kernels, each checked against torch and timed
docs/ the documentation, and the development record (docs/history)
The hk/ paths are under python/; the tests are in tests/hk/.
Documentation
- Documentation index, and why this exists
- Getting started — install, a first kernel, running the tests and examples
- The language — parameters, types, operators, memory, control flow, limits
- The IR — the hk dialect, its passes, the C++ translation,
hk-opt/hk-translate - The runtime — the cache, the resource gates, warming, autotune,
torch.ops - Integration — vLLM and SGLang: the patches, their switches, routing, CUDA graphs
- Performance — every result, with its hardware, baseline and source
- Building — MLIR, the dialect, the wheels
- Testing — the four tiers and the gates
- Development record — how it got here, phase by phase, with the negative results
Examples
Each runs on the GPU (checks against torch, then times), and with
--dump ir|cpp or --build on a machine without one. See
examples/README.md.
| file | what it is |
|---|---|
examples/01_vector_add.py |
elementwise add, the smallest complete kernel |
examples/02_rmsnorm.py |
RMSNorm: runtime loop, row reduction, cross-warp combine, broadcast |
examples/03_softmax.py |
row softmax: max, exp, sum, normalise |
examples/04_gemm.py |
C = A·Bᵀ with LDS staging, WMMA and a derived LDS wait |
examples/05_attention.py |
flash-attention forward with an online softmax |
examples/06_control_flow.py |
for / if / while on device values, carried scalars, the generated scf and C++ |
examples/07_dropout.py |
Triton's dropout tutorial: hk.rand, the same draws as tl.rand, the same elements kept |
examples/08_embedding.py |
an embedding lookup: rows by index with gather |
examples/09_kv_scatter.py |
a KV cache write: rows to slots with scatter, padding slots dropped |
examples/10_layernorm_backward.py |
Triton's LayerNorm backward: row and column reductions, partial sums without a lock |
examples/11_vllm_integration.py |
your kernel in vLLM: register, check, patch, and an end-to-end A/B in one command |
examples/validate.py |
the shipped hk.ops kernels at typical shapes against torch: correctness and speed |
License
MIT; see LICENSE. hk-dsl began as part of a fork of HipKittens by HazyResearch, and it generates C++ against the HipKittens headers, which remain HipKittens', under its MIT License. See NOTICE.
Metadata
Release files for hk-dsl 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hk_dsl-0.3.1-cp312-cp312-manylinux_2_38_x86_64.whl | CPython 3.12 | CPython 3.12 | Linux glibc 2.38+ x86-64 | Details |
| hk_dsl-0.3.1-cp310-cp310-manylinux_2_35_x86_64.whl | CPython 3.10 | CPython 3.10 | Linux glibc 2.35+ x86-64 | Details |
Total release size: 22.7 MB
Release files / hk_dsl-0.3.1-cp312-cp312-manylinux_2_38_x86_64.whl
| Download URL | hk_dsl-0.3.1-cp312-cp312-manylinux_2_38_x86_64.whl |
|---|---|
| Size | 11.4 MB |
| Tags | CPython 3.12 Linux glibc 2.38+ x86-64 |
|
SHA-256 checksum How to use checksums |
c5052a1e94137e6c9825d50c522291ba81fe2ac42e2620c6b1ed3cc9a049f322
|
|
BLAKE2b-256 checksum How to use checksums |
77006c314c455bb2357bbb9b951e508289aa341742f5e7b3f2beacd21e98265e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / hk_dsl-0.3.1-cp310-cp310-manylinux_2_35_x86_64.whl
| Download URL | hk_dsl-0.3.1-cp310-cp310-manylinux_2_35_x86_64.whl |
|---|---|
| Size | 11.3 MB |
| Tags | CPython 3.10 Linux glibc 2.35+ x86-64 |
|
SHA-256 checksum How to use checksums |
eed35c18deeb5070f1a70e3b802e13d24e8b4a1f617a5052f5282084c8234743
|
|
BLAKE2b-256 checksum How to use checksums |
3f83bf89cea053867c3b1072855a576e07d36eb85155f7f88434d75163b9f162
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|