Adding a New ONNXFusedOp Fusion Pattern
July 14, 2026 · View on GitHub
Snapshot of the fusion infrastructure as of the Dialect/ONNX/Transforms
reorg (FusionOpChain → FusionOpKindHelper, FusedOpKindPattern →
FusedPatternForOpKind, NNPA's OpFusionHelper → ZHighFusionOpHelper).
Supersedes FusionOpChain.md in this directory, which describes the
pre-reorg names and layout.
1. What a fusion pattern is
Some IR patterns are a short, linear chain of ops that is cheaper to
recognize once and lower as a single custom code-generation step than to
lower op-by-op. The infrastructure captures such a chain inside an
ONNXFusedOp container between two independent passes:
- Creation (a
Transformpass, e.g.FusionOpStickUnstick): matches an anchor op, walks forward/backward through the chain, and if beneficial, wraps the chain in anONNXFusedOp, storing the chain's structural parameters as named MLIR attributes. - Lowering (a
Conversionpass, e.g.ZHighToZLoworONNXToKrnl): reads those attributes back, re-verifies the body wasn't altered by an intervening optimization, and emits optimized code in one step — instead of re-discovering the pattern from scratch.
Both directions share one non-accelerator base class so that only the per-kind content (what the chain looks like, what parameters it has) is written once, per kind.
2. Data structures
2.1 ONNXFusedOp — the container (generic ONNX op)
Defined in src/Dialect/ONNX/AdditionalONNXOps.td (verifier body in
src/Dialect/ONNX/ONNXOps/Additional/FusedOp.cpp).
| Field | Kind | Notes |
|---|---|---|
kind | StrAttr | Identifies which fusion pattern this instance is (e.g. "zhigh.extended_layout_transform"). Lowering dispatches on this string. |
inputs | Variadic<AnyType> operands | Every external tensor value the body needs, in order. Becomes the body's block arguments, same order. |
outputs | Variadic<AnyType> results | One per value the body's onnx.Yield produces. |
body | SizedRegion<1> | IsolatedFromAbove — nothing inside may reference a value from outside except through inputs/block arguments. Constant-producing ops (ONNXConstantOp, ONNXNoneOp, ConstantLike) are cloned inside instead of threaded as inputs. |
Traits: Pure, IsolatedFromAbove, has a verifier, and delegates shape
inference to the ops inside the body (the greedy rewriter infers each inner
op, then the Yield drives the FusedOp's own result types).
2.2 FusionOpKindHelper — generic builder/consumer base
src/Dialect/ONNX/Transforms/FusionOpHelper.hpp / .cpp. Never
instantiated directly — always subclassed, once per fusion kind.
Fields (populated by the subclass, consumed by the base):
| Field | Type | Ordering requirement |
|---|---|---|
ops | SmallVector<Operation*> | Chain order: ops[i]'s output feeds ops[i+1]'s input; ops.back() is the op whose result(s) become the FusedOp's outputs (or one of several, see finalResults). |
finalResults | SmallVector<Value> | One entry per FusedOp output/Yield operand, in the same order as fusedOp.getOutputs(). |
Non-virtual template methods (the calling sequences — do not override):
fuse(rewriter, loc) -> ONNXFusedOp— creation side. Sets the insertion point toops.back(), builds the isolated body (external inputs collected, constants cloned inside), callsembedAttrs(), then replaces and erases the original chain ops back-to-front.retrieveOpsAndOutputValues(fusedOp)— lowering side. Walks the body, repopulatingops/finalResultsfrom a liveONNXFusedOp. Cannot fail.verifyAndRetrieveAttrs(fusedOp) -> bool— lowering side. Calls the virtualretrieveAttrs()thenverify();falseon either failure (withLLVM_DEBUGoutput). Requiresopsalready populated.static unFuse(rewriter, fusedOp) -> LogicalResult— the generic fallback: inlines the body back into the enclosing function so the constituent ops lower on their own. Static so the catch-all pattern (§2.4) can call it without an instance.
Protected helper for subclasses:
static isInsideFusedOp(op) -> bool—truewhenopis nested inside anONNXFusedOpbody already. Mandatory first check in every subclass'sdetectIfBeneficial(see §4, step 3) — fusion moves ops into the body rather than erasing them, so without this guard the same pattern re-matches its own output and the pass diverges.
Pure-virtual subclass contract (four methods, enforced by the compiler):
| Method | Direction | Contract |
|---|---|---|
getKind() const -> StringRef | both | Returns the kind string constant for this pattern. |
embedAttrs(fusedOp) const | creation | Writes every parameter field to a named attr. Only function that writes attrs. |
retrieveAttrs(fusedOp) -> bool | lowering | Reads every attr back into the fields; false if any required attr is missing. Only function that reads attrs. |
verify() const -> bool | lowering | Cross-checks ops (from retrieveOpsAndOutputValues) against the fields (from retrieveAttrs) — catches a body silently altered by another pass after fusion. |
One additional, non-virtual contract member (documented in
FusionOpHelper.hpp, not enforceable as a real virtual because its
signature varies per subclass):
bool detectIfBeneficial(const DimAnalysis *dimAnalysis, AnchorOpType startOp);
AnchorOpType is whatever op the subclass anchors its match on
(ONNXLayoutTransformOp, ONNXUnsqueezeOp, ...). Virtual dispatch needs a
uniform signature across overrides, so this can't be declared = 0 in the
base — instead it's enforced at compile time wherever the subclass is
plugged into FusedPatternForOpKind<AnchorOpType, FusionT> (§2.3):
omitting it fails to compile there, not in FusionOpHelper.hpp.
2.3 FusedPatternForOpKind<AnchorOpType, FusionT> — creation-side adapter
src/Dialect/ONNX/Transforms/FusionOpBasePattern.hpp. Generic
OpRewritePattern<AnchorOpType> template — one instantiation per
(anchor op, fusion kind) pair:
template <typename AnchorOpType, typename FusionT>
class FusedPatternForOpKind : public mlir::OpRewritePattern<AnchorOpType> {
DimAnalysis *dimAnalysis;
public:
FusedPatternForOpKind(MLIRContext *context, DimAnalysis *dimAnalysis);
LogicalResult matchAndRewrite(AnchorOpType anchorOp,
PatternRewriter &rewriter) const override {
FusionT fusion;
if (!fusion.detectIfBeneficial(dimAnalysis, anchorOp))
return failure();
fusion.fuse(rewriter, anchorOp.getLoc());
return success();
}
};
You never subclass this — you instantiate it with your FusionOpKindHelper
subclass and register it in a RewritePatternSet (§4, step 5).
2.4 Lowering-side adapters (generic, non-accelerator)
src/Conversion/ONNXToKrnl/ONNXToKrnlCommon.hpp / .cpp.
-
FusedOpKindLowering<FusionT>—OpConversionPattern<ONNXFusedOp>template. Subclass it and implement only:FailureOr<Value> lowerVerified(ONNXFusedOp fusedOp, OpAdaptor adaptor, ConversionPatternRewriter &rewriter, FusionT &fusion) const override;The base's
matchAndRewritehandles: bail iffusedOp.getKind() != FusionT::kKind(so unrelated kinds fall through to the next pattern);retrieveOpsAndOutputValues+verifyAndRetrieveAttrs(falls back toFusionOpKindHelper::unFuseon failure); call yourlowerVerified; drop all intra-body def-use edges;replaceOp. You never callreplaceOpor touch body uses yourself. -
FusedOpInlineFallback— a concrete, benefit-0OpConversionPattern<ONNXFusedOp>. Register it once per conversion pass that may see anONNXFusedOp(already done in the genericConvertONNXToKrnl.cpp). Catches any kind with no dedicatedFusedOpKindLoweringsubclass registered in that pass, emits a warning, and inlines the body viaFusionOpKindHelper::unFuseso the constituent ops lower individually through their own patterns.
3. File map
| Piece | Lives in | Accelerator-specific? |
|---|---|---|
ONNXFusedOp op definition | src/Dialect/ONNX/AdditionalONNXOps.td, .../ONNXOps/Additional/FusedOp.cpp | No |
FusionOpKindHelper (base) | src/Dialect/ONNX/Transforms/FusionOpHelper.{hpp,cpp} | No |
FusedPatternForOpKind<A,F> (creation adapter) | src/Dialect/ONNX/Transforms/FusionOpBasePattern.hpp | No |
FusedOpKindLowering<F>, FusedOpInlineFallback (lowering adapters) | src/Conversion/ONNXToKrnl/ONNXToKrnlCommon.{hpp,cpp} | No |
Your FusionOpKindHelper subclass (kind + params + detect/embed/retrieve/verify) | e.g. .../ZHigh/ZHighOps/ZHighFusionOpHelper.{hpp,cpp} for NNPA; a new file under Dialect/ONNX/Transforms/ if truly generic | Depends on the pattern |
Creation-pass registration (patterns.insert<FusedPatternForOpKind<...>>) | The Transform pass that owns the anchor op's dialect, e.g. src/Accelerators/NNPA/Transform/ZHigh/FusionOpStickUnstick.cpp | Follows the anchor op |
Lowering-pass registration (your FusedOpKindLowering<F> subclass) | The Conversion pass that lowers ONNXFusedOp for that kind, e.g. src/Accelerators/NNPA/Conversion/ZHighToZLow/ZHighToZLow.cpp | Follows where the kind is lowered |
Existing worked examples: ExtLayoutTransformFusionHelper and
ExpandMulStickFusionHelper, both in ZHighFusionOpHelper.{hpp,cpp},
registered for creation in FusionOpStickUnstick.cpp and for lowering in
ZHighToZLow.cpp.
4. Step-by-step: capturing a new pattern
Say you're adding a new kind, "zhigh.my_pattern", anchored on
ONNXFooOp.
-
Pick the kind string and anchor op. Kind strings are dialect-prefixed (
"zhigh."for NNPA fusions) so the lowering pass can namespace by dispatch. The anchor op is whichever op in the chain is cheapest/most specific tomatchon (head or tail of the chain — either works, seeExtLayoutTransformFusionHelperanchored on the first op of its chain vs.ExpandMulStickFusionHelperalso anchored on the first op). -
Create the subclass file (or add to an existing one in the same dialect, like
ZHighFusionOpHelper.hpp/.cpp):class MyPatternFusion : public onnx_mlir::FusionOpKindHelper { public: static constexpr llvm::StringLiteral kKind{"zhigh.my_pattern"}; // Parameter fields extracted during detection, needed during lowering. int64_t myAxis = -1; bool myFlag = false; bool detectIfBeneficial( const DimAnalysis *dimAnalysis, mlir::ONNXFooOp startOp); llvm::StringRef getKind() const override { return kKind; } void embedAttrs(mlir::ONNXFusedOp fusedOp) const override; bool retrieveAttrs(mlir::ONNXFusedOp fusedOp) override; bool verify() const override; }; -
Implement
detectIfBeneficial. First line, always:if (isInsideFusedOp(startOp)) return false;Then walk the chain from
startOp(e.g. via asingleUserOfType<T>helper — seeZHighFusionOpHelper.cppfor reusable static helpers), validating each step and populatingops(chain order) andfinalResults(one per eventual FusedOp output) and every parameter field. End with a beneficial threshold check — only returntruewhen fusing is actually worth it, not just legal. -
Implement
embedAttrs/retrieveAttrs/verify.embedAttrswrites each field as a named MLIR attr;retrieveAttrsreads them back (fail if any required one is missing — guards against stale/hand-edited IR);verifyre-derives the expected op count/types from the fields and checksopsstill matches, emittingLLVM_DEBUGon mismatch. -
Register the creation pattern in the
Transformpass that ownsONNXFooOp's matching (inFusionOpStickUnstick.cpp, alongside the existing two):using FusedPatternsForMyPattern = FusedPatternForOpKind<ONNXFooOp, MyPatternFusion>; ... patterns.insert<FusedPatternsForMyPattern>(&getContext(), dimAnalysis);(
FusionOpStickUnstick.cppalso gates its two existing fused patterns behind adisableFusedOpOption/disableFusedOpflag, falling back to a hand-written composite-op pattern when disabled — decide whether your pattern needs the same escape hatch or can always fuse.) -
Implement the lowering. In the
Conversionpass responsible for lowering this kind (ZHighToZLow.cppfor NNPA"zhigh.*"kinds):struct MyPatternLowering : public FusedOpKindLowering<MyPatternFusion> { using Base = FusedOpKindLowering<MyPatternFusion>; using Base::Base; FailureOr<Value> lowerVerified(ONNXFusedOp fusedOp, OpAdaptor adaptor, ConversionPatternRewriter &rewriter, MyPatternFusion &fusion) const override { // fusion.myAxis, fusion.myFlag, adaptor.getInputs(), ... — emit code, // return the single Value that replaces the FusedOp's output(s). } };Register it in that pass's pattern list (see
ZHighToZLow.cpparoundpatterns.insert<ZHighToZLowFusedExtLayoutTransformLowering>(...)). The genericFusedOpInlineFallback(benefit 0, already registered inConvertONNXToKrnl.cpp) covers you automatically if this step is skipped or not yet written — the FusedOp just inlines and lowers op-by-op instead. -
Test. At minimum:
- Unit-test
detectIfBeneficialon positive/negative IR snippets (chain present but not beneficial; chain broken by an extra use; chain present and beneficial). - Round-trip: run the creation pass then the lowering pass on the same module; confirm the final Krnl/output IR matches what direct, non-fused lowering of the original chain would produce.
- Confirm
verify()actually rejects a body you've hand-tampered with between creation and lowering (e.g. delete one op in the body) — it should fall back tounFuserather than crash or mis-lower. - If you added a disable flag, test both settings.
- Unit-test
5. Common pitfalls
- Forgetting the
isInsideFusedOpguard → infinite rewrite loop, since matched ops are moved into the body, not erased. embedAttrs/retrieveAttrstouching attrs outside those two methods → breaks the "only two functions touch attrs" invariant that makes the attr set easy to audit for a given kind.opsnot in chain order →fuse()'s insertion-point choice (ops.back()) andreplaceAndErase's back-to-front erase both assume strict chain order for dominance; violating it can erase a still-used op.- Registering your
FusedOpKindLowering<F>at benefit 0 or below → it can lose toFusedOpInlineFallbackand your kind always inlines instead of using your optimized lowering. Use default/explicit benefit above 0. DimAnalysisnull — required non-null throughout; there's no shape-comparison fallback path.