Graph transforms¶
Whole-tensor rewrites, before any loop exists. In Hexir these belong on the
hexir dialect, between shape inference and hexir-lower-to-tir.
This is where the largest wins for static models are, because it is where memory traffic is decided. A kernel that reads its input from device memory instead of from a register has already lost, and no amount of scheduling recovers it.
Fusion¶
Hexir has none. LowerToTIRPass emits exactly one hextir.prim_func per
hexir op, so linear → add → relu writes and re-reads two full intermediate
tensors.
Transform |
Prior art |
What it buys |
|---|---|---|
Elementwise into producer |
Relay |
The bias-and-activation epilogue stops touching memory. On an MLP this is most of the traffic. |
Op-kind fusion algebra |
Relay’s pattern kinds — |
A rule for what may fuse with what, instead of a list of special cases. The cheapest way to get fusion that generalizes. |
Multi-output fusion |
XLA |
One kernel producing two live results, e.g. a normalization emitting both output and saved statistics. |
Horizontal fusion |
XLA |
Batches many small independent kernels into one launch. Directly targets decode, where launch count dominates. |
Fuse into the dispatch region |
IREE |
Cheap producers get cloned into every consumer rather than materialized. Recompute beats a round trip to DRAM. |
Fusion with a cost model |
XLA |
Stops fusion that increases work more than it saves bandwidth. Needed once fusion is aggressive. |
Fuse the kernels after fusing the graph |
Relax |
Two-stage: decide at the graph level, then actually merge the loop nests. This is the shape Hexir would want, given |
Relax’s FuseOps/FuseTIR split is worth singling out. Hexir already has
hexir.call_tir and module-scope prim funcs — the same structure Relax uses —
so graph-level fusion can be expressed as “rewrite the calls”, with a separate
pass that merges the callee loop nests.
Algebraic and simplifying rewrites¶
HexirCombine.td exists and every pattern in it is commented out. Only
ConstantOp::fold is live.
Transform |
Prior art |
What it buys |
|---|---|---|
Algebraic simplification |
XLA |
The long tail: add-of-zero, multiply-by-one, |
Constant folding |
Relay |
Already present for single ops. |
Constant evaluation |
IREE |
Any subgraph reachable only from weights runs once, at compile time, and the result is baked into RODATA. Folds weight transposes, dequantization setup, and layout changes to nothing. |
CSE and DCE |
Relay |
MLIR’s |
Batchnorm and inference-only folding |
Relay |
Folds normalization into the preceding matmul’s weights. Free at inference. |
Scale-axis folding |
Relay |
Pushes a per-channel scale into adjacent weights, removing an elementwise pass. |
Combining parallel branches |
Relay |
Three projections sharing one input become one wider matmul. This is exactly QKV fusion. |
Expanding composite ops |
XLA |
Lets the frontend keep high-level ops without every backend needing a kernel for each. |
Layout and data movement¶
Hexir has no transpose and no reshape — both are commented out in
HexirOps.td. That is a gap with unusual leverage: layout is often worth more
than scheduling, and for constant weights it is free.
Transform |
Prior art |
What it buys |
|---|---|---|
Global layout assignment |
XLA |
Chooses a physical layout per value with constraints, instead of hardcoding row-major everywhere. |
Layout conversion and alteration |
Relay |
Inserts the minimum number of transposes to give every kernel the layout it prefers. |
Transpose propagation |
IREE |
Sinks transposes toward the edges of the graph, where they cancel or land on a constant. |
Reshape as metadata |
Relax |
A reshape becomes a view, never a copy. |
Weight pre-packing |
Relax |
Weights are stored in the exact tiled layout the kernel reads. Costs compile time and file size, buys coalescing at every inference. |
Layout-free buffer annotation |
Relax |
Marks which buffers the compiler is free to re-lay-out, so pre-packing is safe by construction. |
Precision and quantization¶
Hexir supports f32 and f64. Every checked-in example is f64, which for a GPU is the wrong default by a factor of thirty-two on consumer silicon.
Transform |
Prior art |
What it buys |
|---|---|---|
Automatic mixed precision |
Relay/Relax |
Demotes what is safe to f16/bf16 and keeps accumulation wide. |
Contraction input demotion |
IREE |
Narrower operands, f32 accumulate — the standard inference tradeoff. |
Integer quantization |
TVM QNN ( |
int8 weights and activations, with the requantize arithmetic expressed in the IR rather than hidden in a kernel. |
Fuse dequantize into matmul |
IREE |
Weights stay compressed in memory and are expanded in registers. The single biggest decode win — see LLM serving. |
Narrow index and accumulator types |
TIR |
Smaller address arithmetic, more registers for real work. |
Placement and partitioning¶
Hexir’s hexir-partition is the same idea as TVM’s BYOC flow and IREE’s device
affinity analysis, but with a static registry instead of a cost model, and with
no transfer insertion at all.
Transform |
Prior art |
What it buys |
|---|---|---|
Annotate, merge, partition |
Relay |
Merging is the missing middle step: adjacent same-device ops become one region, so placement is decided for subgraphs rather than one op at a time. |
Composite pattern matching |
Relay |
Recognize a multi-op pattern (softmax, attention, GELU written out longhand) and replace it with one op that has a good kernel. |
Transfer insertion |
IREE |
Required before heterogeneous placement can ever be profitable. Hexir currently errors instead: a dispatch whose device disagrees with the active device is rejected in the VM. |
Transfer cost in the objective |
IREE’s affinity/topology analysis |
Placement that accounts for PCIe. Without it, a “faster” op can lose to the copy that feeds it. |
Dynamic-to-static |
Relay |
Freeze shapes wherever they are actually known, so the static path stays fast even in a dynamic model. |
Op coverage as an optimization¶
Not a transform, but it gates several: a pattern that cannot be expressed cannot be fused.
MulOp is commented out in HexirOps.td, so there is no way to write a gated
activation. softmax, gelu, tanh, sigmoid and friends are declared with
no lowering. Missing entirely: batched matmul, concat, slice, reductions,
embedding/gather, argmax. TVM and IREE both treat a rich op set plus a
generic lowering as cheaper than a large kernel library, and both provide a
generic path (te.compute / linalg.generic) so a new elementwise op costs
one pattern rather than one kernel.