LLM serving¶
An autoregressive model is not just a bigger static model. It runs the same weights thousands of times with a changing shape, it carries state between invocations, and its two phases have opposite bottlenecks. Several entries here are prerequisites rather than optimizations: without them the workload cannot be expressed at all.
The prior art is TVM’s MLC-LLM stack, IREE’s sharktank/shortfin path, XLA as used by JAX inference servers, and the serving systems — vLLM, SGLang, TensorRT-LLM — which are not compilers but decided most of these questions first.
Two phases, two bottlenecks¶
Prefill |
Decode |
|
|---|---|---|
Shape |
|
|
Bound by |
compute |
memory bandwidth on weights |
Wants |
tensor cores, tiling, large tiles |
split-K reductions, quantized weights, no launch overhead |
Kernel |
GEMM |
GEMV |
Hexir’s matmul maps the M axis to blockIdx.y, so at M = 1 the grid is one
block deep and a single row of threads each walks the whole K dimension
serially. Decode would run at a few percent of memory bandwidth. Prefill and
decode genuinely want different kernels, selected on shape — this is what
TVM’s Dlight does with separate gpu.Matmul and gpu.GEMV rules, and what
TensorRT-LLM does with separate GEMM plugins.
Blockers¶
None of these is an optimization. Each is a capability the current design excludes.
Blocker |
Where it bites |
Prior art for the fix |
|---|---|---|
Shapes are compile-time constants |
|
Symbolic shapes in Relax; IREE’s dynamic dispatch dimensions and workgroup-count regions; XLA |
No control flow in the VM |
The command list is flat by design. A decode loop cannot be expressed. |
Relax VM bytecode; IREE’s VM. |
No persistent state across invocations |
|
XLA donated buffers; IREE |
No shared memory or cross-thread reduction in |
Flash attention and split-K both need them. |
TIR |
No narrow or quantized types |
|
TVM QNN and MLC’s group quantization; IREE’s quantized matmul path. |
The op set cannot express a transformer |
No |
Any of the three. |
Attention¶
Technique |
Prior art |
Note |
|---|---|---|
Fused attention |
IREE |
Tiled scores with a running softmax. Avoids materializing the S×S matrix, which for long context is the difference between running and running out of memory. |
Flash decoding / split-KV |
vLLM and FlashInfer decode kernels; |
At |
Paged KV cache |
vLLM PagedAttention; MLC’s |
Fixed-size blocks with a block table, instead of one contiguous buffer per sequence. Removes the fragmentation that otherwise caps batch size. |
Prefix / prompt caching |
SGLang RadixAttention; vLLM prefix caching |
Shared prefixes computed once across requests. A serving concern, but it constrains the cache layout. |
Sliding window and sink attention |
Serving systems |
Bounded cache for long conversations. |
Grouped-query attention |
Model architectures, but it changes kernel shapes |
Fewer KV heads than Q heads — the reduction and the cache layout both change. |
Quantized KV cache |
fp8 and int8 KV in TensorRT-LLM and vLLM |
The cache eventually outgrows the weights; halving it doubles the context or the batch. |
Chunked prefill |
vLLM, SGLang |
Splits a long prompt so it interleaves with decode work instead of blocking it. Needs control flow. |
Weights¶
Decode reads every weight once per token and does almost no arithmetic per byte. That makes weight bandwidth the whole problem, and compression the whole answer.
Technique |
Prior art |
What it buys |
|---|---|---|
Weight-only int4 / int8 group quantization |
MLC’s |
Roughly 4× less weight traffic per token, with f16 arithmetic. The largest single decode speedup available. |
Dequantize fused into the GEMV |
IREE |
Weights stay packed in memory and expand in registers. Unfused dequantization writes the full f16 tensor back to memory and loses the entire benefit. |
Weight pre-packing at compile time |
Relax |
The packed, tiled, possibly interleaved layout the kernel wants, computed once during compilation. |
Weight residency |
XLA donated buffers; IREE parameter archives |
Upload once per process. Hexir re-uploads every constant on every |
fp8 weights and activations |
TensorRT-LLM, XLA on Hopper |
Where the hardware supports it, another halving. |
QKV and gate/up fusion |
Relay |
Three or two projections over the same input become one wider matmul: one launch, one pass over the input. |
Fusing the glue¶
Everything between the matmuls is bandwidth-bound, and there is a lot of it: two normalizations, two residual adds, a RoPE, and a gated activation per layer. Unfused, each is a full read and write of the activation tensor.
Fusion |
Prior art |
|---|---|
RMSNorm with the residual add |
Every serving stack; Dlight has an RMSNorm rule |
RoPE into the QKV projection epilogue |
TensorRT-LLM, vLLM |
SwiGLU — |
IREE |
Residual add into the following normalization |
XLA fusion, automatically |
Sampling on device — argmax, top-k, top-p |
IREE |
The last one is worth its own note: sampling on the host means a device-to-host copy and a full synchronization per token, which on a small model can rival the model itself.
Reducing launches¶
A 32-layer model with seven kernels per layer is 224 launches per token before
any fusion. At M = 1 each kernel may take less time than its launch.
Technique |
Prior art |
Note |
|---|---|---|
CUDA graph capture of the decode step |
XLA |
The standard fix, and it is worth capturing per batch size. |
Horizontal fusion of small kernels |
XLA |
Independent small ops merged into one launch. |
Persistent / mega-kernel decode |
Research and recent serving work |
One launch per token for the whole model. The endgame; only meaningful after everything else. |
Layer code shared across layers |
Relax |
Also a compile-time and artifact-size argument: emitting 32 copies of identical kernels is what Hexir would do today. |
Multi-GPU¶
Beyond the current scope, but it is the same partitioning problem Hexir already models per op — so worth knowing where it leads.
Technique |
Prior art |
|---|---|
Tensor parallelism |
TVM Disco; XLA GSPMD sharding annotations |
Collective fusion and overlap |
XLA |
Pipeline parallelism |
XLA and serving systems |
Sharding propagation |
XLA |
The shortest useful path¶
If the goal is an LLM running acceptably rather than a complete compiler, the dependency chain is short and strictly ordered:
Narrow types — f16/bf16 activations. Everything else assumes them.
Dynamic shapes in the artifact format, with launch geometry computed at dispatch. Unblocks all of prefill.
Control flow in the VM plus persistent buffers. Unblocks decode and the KV cache.
Weight residency and an allocation plan — see Memory.
A GEMV kernel with split-K, and shape-based selection between it and the GEMM path.
Weight-only int4 with fused dequantization.
Fused attention, once shared memory and cross-thread reduction exist.
Graph capture of the decode step.
Steps 1–4 are format and runtime work; 5–8 are the kernel work from Kernel transforms. Neither half is useful without the other.