Execution¶
Everything between “the kernels are compiled” and “the answer is available”. This is the cheapest section of the catalogue to act on, because none of it requires changing a single kernel — and on a small model, all of it dominates.
Hexir’s runtime replays a flat command list. Each DISPATCH on the CUDA path
loads the CUBIN, looks up the entry point, launches, synchronizes the context,
and unloads the module. That is several milliseconds of driver work wrapped
around a kernel that may take microseconds.
Launch overhead¶
Transform |
Prior art |
What it buys |
|---|---|---|
Cache loaded modules and functions |
Every runtime does this; IREE loads an executable once per device |
Removing |
Command buffers |
IREE HAL |
Record the whole command list once, submit it per invocation. The abstraction Hexir’s HAL is missing. |
CUDA graphs |
XLA |
The concrete version of the above on NVIDIA. Hexir’s command list is static straight-line dataflow by construction, which is exactly the precondition graph capture wants. For decode, this is a large win. |
Deduplicate executables |
IREE |
Identical kernels compiled once, loaded once. |
Elide redundant commands |
IREE |
Removes repeated bindings and barriers from a recorded buffer. |
Indirect / deferred dispatch |
IREE indirect command buffers |
Launch geometry read from a buffer, so a recorded command buffer survives a shape change. The bridge between graph capture and dynamic shapes. |
Fold uniform operands, specialize dispatches |
IREE |
Constant launch parameters become compile-time constants in the kernel. |
Asynchrony¶
Hexir synchronizes twice per dispatch: cuda_launch calls
cuCtxSynchronize before returning, and the CPU path ends with a device wait.
A pipeline that drains after every operation cannot overlap anything.
Transform |
Prior art |
What it buys |
|---|---|---|
Streams and a timeline |
IREE |
Launch everything, wait once at the point a result is actually read. |
Schedule for concurrency |
IREE |
Independent branches of the graph run concurrently instead of in source order. |
Propagate and elide timepoints |
IREE |
Removes waits that are already implied by another dependency. |
Latency-hiding scheduler |
XLA |
Reorders the schedule so long operations overlap with independent work. |
Async copies |
CUDA |
Overlaps the weight upload with the first layers, and the result download with the last. |
Overlap compute with collectives |
XLA |
Multi-GPU only, but the reason tensor parallelism scales at all. |
Two things in the current design make asynchrony easy to add: results are only
observed at PRINT and END, and hexir_buffer_read is the only path back to
the host. Those are natural sync points and there are no others.
The virtual machine¶
program.h documents the choice honestly: a flat command list, because
everything the compiler can produce is straight-line dataflow, and “bytecode
becomes necessary when dynamic shapes or control flow arrive.”
For LLMs, they have arrived.
Capability |
Prior art |
Needed for |
|---|---|---|
Control flow |
TVM’s Relax VM bytecode; IREE’s VM bytecode with |
A decode loop, early exit on end-of-sequence, chunked prefill. |
Dynamic shapes |
IREE |
Sequence length. Hexir bakes |
Shape computation at runtime |
Relax’s symbolic shape expressions; IREE’s workgroup count region on |
Grid size derived from a shape register instead of a constant. |
Function calls and closures |
Relax |
Sharing one layer’s code across thirty-two layers, rather than emitting it thirty-two times. |
Reference counting or explicit lifetimes |
IREE VM refs; Relax |
Bounded memory once control flow means lifetimes are not lexical. |
Loop optimizations |
XLA |
Hoisting anything shape-invariant out of the decode loop. |
Separate load and execute phases |
IREE parameter archives (IRPA), mmapped and bound once; Relax |
Weights uploaded and preprocessed once per process, not once per call. Currently every |
Multi-device and heterogeneous execution¶
Hexir’s premise is per-op placement, so this is closer to the project’s centre than the rest of the catalogue — but the machinery is not there yet: a dispatch whose recorded device disagrees with the active device is rejected rather than bridged.
Transform |
Prior art |
What it buys |
|---|---|---|
Device affinity analysis |
IREE’s affinity attributes and analysis |
Every value knows which device holds it, so transfers are inserted by the compiler rather than assumed by the programmer. |
Transfer insertion and elision |
IREE |
The missing pass. Without it, heterogeneous placement is only expressible when it happens not to need a copy. |
Queue and topology awareness |
IREE device topology; XLA |
Placement that knows the interconnect, not just the op. |
Collectives |
IREE |
Tensor-parallel inference across GPUs. |
Multi-device partitioning |
XLA GSPMD / |
Sharding a model too large for one device. The far end of the roadmap, but it is the same partitioning problem Hexir already models per op. |
Measurement¶
An optimization you cannot see is an optimization you cannot keep.
bench/hexir-bench.c times the first executable in the module against cuBLAS,
which is the right idea for a GEMM and blind to everything else. What it does
not report: per-dispatch breakdown, launch overhead as a share of wall time,
achieved bandwidth for memory-bound kernels, or a decode-shaped case where
M = 1.
For reference, the standard practice in the three projects:
Roofline framing — report achieved bandwidth for memory-bound kernels and achieved FLOPS for compute-bound ones. GFLOPS alone hides the entire elementwise and decode story.
Per-dispatch tracing — IREE’s dispatch instrumentation and XLA’s profiler attribute time to individual kernels, which is what makes fusion decisions arguable rather than aesthetic.
A tracked baseline — cuBLAS for GEMM, and for the CPU path a BLAS or a ukernel, so a regression is visible as a ratio rather than as a number that looks fine in isolation.