Conv Canonicalization
All addresses on this page apply to neuronx_cc
2.24.5133.0+58f8de22, binaryhlo2penguin(cp310)..rodataVA−0x200000,.textVA−0x201000. Other versions will differ.
Abstract
CanonicalizeConv is one of the hlo2penguin MLIR conv-canonicalization passes that
normalizes mhlo.convolution before the conv device-lowering selects a kernel
(6.8.1). It is a func-level
OperationPass<func::FuncOp> — its kind is readable from the RTTI on the
getName/clonePass wrapper — registered under the pass argument canonicalize-conv. A byte-identical
twin, StableHLOCanonicalizeConv (stablehlo-canonicalize-conv), runs the same
algorithm over the StableHLO dialect.
The pass does exactly two rewrites — it makes stride and padding explicit so the backend only ever sees zero-padding, unit-stride convolutions:
explicitlyPadfolds the conv's windowpaddinginto a standalonemhlo.padon the LHS input, then rebuilds the conv withpadding = 0.explicitlyStridefolds each non-unitwindow_strideinto a stridedmhlo.sliceof the LHS input, then rebuilds the conv withwindow_strides = 1.
That is the whole transform. The most important thing a reimplementer must know is
what it does not do: there is no dimension-number permute, no mhlo.transpose
insertion, no conv→dot rewrite, and no conv→im2col-matmul rewrite anywhere in this
pass (see §7). Dimension-numbers,
feature_group_count, and batch_group_count are read only to locate the
spatial axes, and are copied verbatim onto the rebuilt conv. The "canonical form"
this pass produces is therefore: same dim-numbers, same layout, same group
counts — but padding and stride hoisted out of the conv into preceding pad/slice
ops. Layout normalization to a fixed order (NHWC etc.) happens elsewhere, not here.
For reimplementation, the contract is:
- The func-level two-phase driver: collect all eligible
ConvolutionOps, then rewrite each (pad-first, stride-second). - The eligibility gate
canCanonicalize: rank-4 (2-D spatial) convs with small spatial parameters only; everything else passes through untouched. - The two rewrites' exact rebuilt-conv signature, including the verbatim copy-through of dim-numbers and both group counts.
- The MHLO ↔ StableHLO split: same algorithm, different dialect ops, different
source file and a different
PadOp::buildsignature.
| MHLO factory | mlir::createCanonicalizeConvPass @0x2077c60 |
| StableHLO factory | mlir::createStableHLOCanonicalizeConvPass @0x2119950 |
| Pass args | canonicalize-conv / stablehlo-canonicalize-conv |
| Pass kind | PassWrapper<…, OperationPass<func::FuncOp>> (func-level) |
| MHLO driver | CanonicalizeConv::runOnOperation @0x207c270 (653 B) |
| StableHLO driver | StableHLOCanonicalizeConv::runOnOperation @0x211dcd0 |
| Source files | hilo/MLIRPasses/Transforms/CanonicalizeConv.cc @0x2f66a0 · …/StableHLOCanonicalizeConv.cc @0x348ed8 |
| IR level | MHLO / StableHLO (pre-Penguin) |
| Error code | NCC_EUET001 @0x226305 (pad-path unsupported element type) |
1. Driver — runOnOperation
Purpose
Walk the function, collect every eligible convolution, then rewrite each. Collect-then-rewrite avoids mutating the IR while the walk is in flight.
Entry Point
CanonicalizeConv::runOnOperation @0x207c270 (653 B)
├─ mlir::detail::walk(funcOp, callback=0x2077710, PreOrder) ── collect
│ └─ canCanonicalize @0x20774d0 ── gate
├─ explicitlyPad @0x207a7a0 ── phase 2: pad
├─ explicitlyStride @0x2079890 ── phase 2: stride
└─ isBackwardConv @0x20777c0 ── phase 2b (VLOG-gated)
Algorithm
function runOnOperation(this): // sub_207C270
funcOp = this->[+0x28] & ~7 // the func::FuncOp under processing
SmallVector<Operation*,6> convs // inline storage, cap 6
// PHASE 1 — walk + collect
walk(funcOp, callback=0x2077710, PreOrder) // collect lambda below
// PHASE 2 — rewrite each collected conv, PAD FIRST then STRIDE
for op in convs:
newOp = explicitlyPad(op) // @0x207c393 — returns rebuilt conv
explicitlyStride(newOp) // @0x207c39b
// PHASE 2b — diagnostic re-pad, gated by VLOG flag qword_9C714B8
if qword_9C714B8: // byte read just before the loop
re-walk; for each remaining ConvolutionOp whose producer chain
hits another ConvolutionOp:
if isBackwardConv(op): explicitlyPad(op) // @0x207c446
function collect_callback(payload, Operation* op): // sub_2077710 (169 B)
if op->[+0x30]->[+0x10] == TypeID<void>: return // term/null
if op->[+0x30]->[+0x10] != TypeID<mhlo::ConvolutionOp>: return // not a conv
if !canCanonicalize(op): return
(*payload).push_back(op) // SmallVector
QUIRK — the order is fixed:
explicitlyPadruns beforeexplicitlyStride. The stride rewrite's slice math (§4b) assumes the conv it rebuilds already has explicit, zeroed padding. Reordering the two silently produces wrong slice bounds when the input padding is non-zero.
NOTE — phase 2b is purely diagnostic — it is gated on the BSS VLOG flag
qword_9C714B8and usesisBackwardConvonly to decide whether to emit a re-pad for logging. It does not select a different lowering and has no effect in a normal (non-verbose) build.
2. Eligibility Gate — canCanonicalize
Purpose
Accept only convolutions that are worth materializing explicitly: rank-4 (2-D spatial) convs with small spatial parameters. Everything else returns to the backend untouched.
Algorithm
function canCanonicalize(conv): // sub_20774D0 (576 B)
if conv op-state byte [op+0x2E] >= 0: ud2 // sanity: op must be a valid conv
cp = buildMhloConvParams(conv, conv.operand0, conv.operand1) // @0x2077510 → §5
accept = false
if cp.rank_lhs == 4 && cp.rank_rhs == 4: // var_248 / var_208 — both rank-4
for v in cp.<window dims>: if v != 2: reject // 2-D spatial expected
d0 = cp.dims[ inputSpatialDim0 ]
if d0 <= 1: reject
elif d0 <= 3: accept = qword_9C71578 // small-kernel feature flag
d1 = cp.dims[ inputSpatialDim1 ]
if d1 <= 1: reject
elif d1 <= 3: accept = qword_9C71578
accept = ( cp.<stride/pad>[idx] <= 31 ) // upper bound 31 on one component
free cp (ConvParams dtor inline)
return accept
Plain-language gate: rank-4 convolutions only, with spatial extents > 1, a
feature-flag escape (qword_9C71578) for size-2/3 kernels, and an upper bound of
31 on one stride/pad component. The rank-4 + small-spatial shape of the gate is
read directly; the mapping from individual var_* stack slots to named
ConvParams fields is reconstructed, because ConvParams is a large struct whose
vectors are witnessed only by destructor order (§5).
GOTCHA — the gate builds a full
ConvParams(every conv attribute + both operand shapes) on every candidate, just to read a handful of fields. A naive reimplementation that skips the build and reads attributes directly is faster but must reproduce the rank-4 and small-spatial cutoffs exactly, or it will canonicalize convs the device-lowering expects to receive un-normalized.
3. Rewrite — explicitlyPad
Purpose
Hoist the conv's window padding out into a standalone mhlo.pad on the LHS
input, then rebuild the conv with zeroed padding.
Algorithm
function explicitlyPad(convOp) -> Operation*: // sub_207A7A0 (6858 B)
dn = convOp.getDimensionNumbers() // @0x8f77bc0
fgc = convOp.getFeatureGroupCount() // @0x8f77be0 — copied through
bgc = convOp.getBatchGroupCount() // @0x8f77cf0 — copied through
elemTy = input.getElementType()
// zero pad-value constant:
zero = OpBuilder::create<mhlo::ConstantOp>(loc, scalarTy(elemTy),
DenseElementsAttr(0)) // @0x2077e00
// explicit pad of the LHS input with the conv's spatial padding:
padded = mhlo::PadOp::build(b, st, paddedTy, input, zero, // @0x8f7f470
edge_padding_low = DenseIntElementsAttr, // conv pad-low on spatial dims
edge_padding_high = DenseIntElementsAttr, // conv pad-high on spatial dims
interior_padding = DenseIntElementsAttr(0)) // always 0 — no dilation here
// rebuild conv over the padded input with ZERO padding:
newConv = OpBuilder::create<mhlo::ConvolutionOp>(loc, convOp.getType(), // @0x2078070
/*lhs*/ padded, /*rhs*/ kernel,
/*window_strides*/ sameAttr,
/*padding*/ DenseIntElementsAttr(0), // << zeroed
/*lhs_dilation*/ sameAttr,
/*rhs_dilation*/ sameAttr,
/*window_reversal*/ DenseElementsAttr,
dn, fgc, bgc, precision_config) // dim-numbers + groups VERBATIM
convOp.erase() // @0x9b59040
return newConv
OpBuilder::create<mhlo::ConvolutionOp> (@0x2078070) carries:
RankedTensorType, 2× TypedValue<RankedTensorType>, 4× DenseIntElementsAttr
(strides, padding, lhs_dilation, rhs_dilation), DenseElementsAttr (window_reversal),
ConvDimensionNumbersAttr, 2× unsigned long (feature_group_count,
batch_group_count), and ArrayAttr (precision_config). The two group counts are
plain integers copied straight through — they are never restructured.
NOTE — the error path runs
hilo::formatErrorMessage<>(ErrorCode)(@0x1ec6fc0) → emitsNCC_EUET001(@0x226305) when the input element type is unsupported on the pad path.llvm::report_fatal_errorguards oneRegisteredOperationNamelookup.
4. Rewrite — explicitlyStride
Purpose
For each non-unit window stride, fold the stride into a strided mhlo.slice of the
(already-padded) LHS input, then rebuild the conv at unit stride.
Algorithm
function explicitlyStride(convOp) -> void: // sub_2079890 (3675 B)
ctx = attr.getContext()
inShape = cast<RankedTensorType>(input).getShape()
dn = convOp.getDimensionNumbers()
i64 = IntegerType::get(ctx, 64, Signless) // stride-attr element type
emit = lambda(spatialDim, strideVal): // sub_20782A0 (5604 B), per spatial dim
sliced = mhlo::SliceOp::build(b, st, slicedTy, curInput, // @0x8f83dd0
start_indices = DenseIntElementsAttr(0…),
limit_indices = DenseIntElementsAttr(full extent),
strides = DenseIntElementsAttr(stride on dim, 1 elsewhere))
newConv = OpBuilder::create<mhlo::ConvolutionOp>(loc, …, // @0x2078070
/*lhs*/ sliced, /*rhs*/ kernel,
/*window_strides*/ DenseIntElementsAttr(1), // << unit stride
/*padding*/ 0, // already zero from explicitlyPad
lhs_dil, rhs_dil,
/*window_reversal*/ BoolAttr::get(ctx, b), // @0x9b4ddb0
dn, fgc, bgc, precision_config)
set inherent attrs; erase old; return newConv
// iterate window_strides (DenseElementsAttr::getRawIntOrFloat @0x9afddd0 / isSplat);
// for any stride != 1, invoke emit(dim, stride).
// mhlo::AddOp::build (@0x8f704b0) composes multi-dim slice-bound index arithmetic.
Net algebra:
conv(x, w, window_strides=s, padding=0)
→ conv( slice(x, strides=s), w, window_strides=1, padding=0 )
The stride is pushed out of the conv into the slice — the classic explicit-stride canonicalization. The rebuilt conv reuses the original result type, so the output shape is preserved.
mhlo::SliceOp::build (@0x8f83dd0) takes 3× DenseIntElementsAttr =
start / limit / stride; the third is where the folded window stride lands.
5. hilo::ConvParams and buildMhloConvParams
buildMhloConvParams(ConvolutionOp&, lhs, rhs) (@0x21cd050, 5455 B) is the single
shared attribute/shape extractor — its only caller is canCanonicalize. It reads
every conv attribute and both operand shapes into a hilo::ConvParams (~0x200 bytes).
Accessors called, each resolved from its demangled symbol in the callee graph:
| Group | Symbols |
|---|---|
| Dim-numbers | getDimensionNumbers (@0x8f77bc0) → all ten dim accessors: getInput{Batch,Feature,Spatial}Dimension(s), getKernel{Input,Output}FeatureDimension, getKernelSpatialDimensions, getOutput{Batch,Feature,Spatial}Dimension(s) |
| Window attrs | getWindowStrides (@0x8f77a30), getPadding (@0x8f77a80), getLhsDilation (@0x8f77ad0), getRhsDilation (@0x8f77b20), getWindowReversal (@0x8f77b70) |
| Group counts | getFeatureGroupCount (@0x8f77be0), getBatchGroupCount (@0x8f77cf0) |
| Shapes | ShapedType::getShape (@0x9b21890) — both operands |
Variadic attrs are materialized into vectors via
llvm::append_range<vector<long>, DenseElementsAttr::ElementIterator<long>>
(@0x21cc870, strides/pad/dilation), the unsigned long and bool variants
(@0x21cc640, @0x21ccaa0), and padding pairs into vector<pair<long,long>>
(_M_realloc_insert @0x1f30f50). A hilo::Permutation(SmallVector<long,6>)
(@0x21e4f70) holding the canonical spatial-dim order is stored in ConvParams.
QUIRK — the
hilo::Permutationis the closest this pass comes to a layout transform — but it is only read by the gate (§2). It is never lowered to anmhlo.transpose. No dimension-number reordering reaches the IR.
Struct layout (witnessed by ~ConvParams @0x21cc510)
The destructor frees 11 owned regions in order; the layout below is reconstructed from that order plus the accessor call sites, so the field → semantic mapping is deduced rather than observed [INFERRED]:
| Offset | Kind | Inferred role |
|---|---|---|
| 0x000 / 0x040 / 0x080 | string / SmallVector | name + first dim vectors |
| 0x0C8 – 0x128 | 5× vector<T> (ptr,size,cap) | stride / pad / dilation / window-reversal arrays |
| 0x170 / 0x1B0 / 0x1F0 | 3× SmallVector<long> | input / kernel / output shape + spatial-dim vectors + Permutation |
Returned ranks land in stack slots var_248 / var_208 (the == 4 gate, §2).
6. MHLO ↔ StableHLO Twin
StableHLOCanonicalizeConv::runOnOperation @0x211dcd0 is the same control flow as
the MHLO driver — same SmallVector collect, same pad-first/stride-second loop,
same qword_9C714B8 VLOG gate, same isBackwardConv diagnostic branch. The deltas
are dialect substitutions:
| Axis | MHLO | StableHLO |
|---|---|---|
| TypeID filter | mhlo::ConvolutionOp | stablehlo::ConvolutionOp |
| Rewrites emit | mhlo.{pad,slice,add,constant,convolution} | stablehlo.{…} |
| Param extractor | buildMhloConvParams @0x21cd050 | buildStableHLOConvParams @0x21cee00 |
| Dim accessors | mhlo::ConvDimensionNumbersAttr | stablehlo::ConvDimensionNumbersAttr |
| Source file | CanonicalizeConv.cc @0x2f66a0 | StableHLOCanonicalizeConv.cc @0x348ed8 |
PadOp::build sig | 3× DenseIntElementsAttr (@0x8f7f470) | 3× llvm::ArrayRef<long> (@0x9159ef0) |
The two passes are separate translation units running the same algorithm, not one
templated body: the StableHLO pass references its own source file
hilo/MLIRPasses/Transforms/StableHLOCanonicalizeConv.cc @0x348ed8, and its
stablehlo::PadOp::build @0x9159ef0 takes llvm::ArrayRef<long> where the MHLO
one takes DenseIntElementsAttr. Everything else matches — the error code
NCC_EUET001 @0x226305 and the gate logic are identical across the two.
StableHLO member set: canCanonicalize @0x21191c0,
isBackwardConv @0x21194b0, explicitlyPad @0x211c270 (6744 B),
explicitlyStride @0x211b600 (2998 B), stride lambda @0x211a140.
7. What This Pass Does Not Do
The name invites three assumptions that the binary does not support. A reimplementer must not expect any of the following from conv canonicalization:
- No conv→dot and no conv→im2col-matmul rewrite. The four bodies
(
runOnOperation,explicitlyPad,explicitlyStride, stride lambda) reference zeroim2col/DotGeneral/TransposeOp/matmulbuilders (rg -icover all four decompiled bodies = 0). The only ops built are{pad, slice, add, constant, convolution}. The actual conv-as-matrix lowering for Trainium lives downstream in the Penguin middle-end / Tensorizer backend, out of scope for hlo2penguin. - No dimension-number permute /
mhlo.transposeinsertion. Dim-numbers are read only to locate spatial axes and copied verbatim onto the rebuilt conv (§3, §4). Thehilo::Permutationis gate-internal (§5). - No layout normalization to a fixed order (NHWC etc.). The rebuilt conv keeps the original layout. Any fixed-layout requirement is enforced by the conv device-lowering (6.8.1), not here.
NOTE — the
tensor.g2s.im2col.*andtensor.g2s.tile.*strings present in the binary are not Neuron extension ops emitted by this pass. They are upstream LLVM NVPTX intrinsics (llvm.nvvm.cp.async.bulk.tensor.g2s.{tile,im2col}.*, Hopper TMA) and NVGPU-dialect verifier strings, statically linked from the XLA-GPU dependency and GPU-targeted — never produced by any hlo2penguin pass.
Function Map
| Function | Addr | Size | Role | Confidence |
|---|---|---|---|---|
CanonicalizeConv::runOnOperation | 0x207c270 | 653 B | two-phase driver | CERTAIN |
| collect callback (lambda) | 0x2077710 | 169 B | filter mhlo::ConvolutionOp + gate → push | CERTAIN |
CanonicalizeConv::canCanonicalize | 0x20774d0 | 576 B | eligibility gate | HIGH |
CanonicalizeConv::isBackwardConv | 0x20777c0 | 1147 B | predicate (VLOG diag only) | HIGH |
CanonicalizeConv::explicitlyPad | 0x207a7a0 | 6858 B | pad-fold → mhlo.pad + rebuilt conv | HIGH |
CanonicalizeConv::explicitlyStride | 0x2079890 | 3675 B | stride-fold driver | HIGH |
| explicitlyStride lambda | 0x20782a0 | 5604 B | per-dim mhlo.slice + rebuilt conv | HIGH |
hilo::buildMhloConvParams | 0x21cd050 | 5455 B | parse all conv attrs+shapes → ConvParams | HIGH |
hilo::ConvParams::~ConvParams | 0x21cc510 | 294 B | struct-layout witness (11 owned buffers) | HIGH |
mlir::createCanonicalizeConvPass | 0x2077c60 | — | MHLO factory | CERTAIN |
StableHLOCanonicalizeConv::runOnOperation | 0x211dcd0 | ~653 B | byte-identical StableHLO driver | CERTAIN |
StableHLOCanonicalizeConv::explicitlyPad | 0x211c270 | 6744 B | → stablehlo.pad | HIGH |
StableHLOCanonicalizeConv::explicitlyStride | 0x211b600 | 2998 B | StableHLO stride driver | HIGH |
hilo::buildStableHLOConvParams | 0x21cee00 | — | StableHLO param parse | HIGH |
mlir::createStableHLOCanonicalizeConvPass | 0x2119950 | — | StableHLO factory | CERTAIN |
BSS flag globals
| Symbol | Role | Confidence |
|---|---|---|
qword_9C714B8 | gates phase-2b isBackwardConv diagnostic re-pad | CERTAIN |
qword_9C71578 | gates acceptance of size-2/3 spatial dims in the gate | HIGH |
Both are unnamed BSS bytes, most likely cl::opt/VLOG statics; their option-argument
names and defaults were not recovered from this binary.
Evidence summary
| Claim | Anchor |
|---|---|
explicitlyPad emits mhlo.pad + a zeroed-padding conv, with group counts copied as 2× unsigned long | callee graph of 0x207a7a0 lists mhlo::PadOp::build (@0x8f7f470, 3× DenseIntElementsAttr) and OpBuilder::create<mhlo::ConvolutionOp> (@0x2078070), whose demangled signature ends …, unsigned long, unsigned long, mlir::ArrayAttr&& |
explicitlyStride folds the stride into the third DenseIntElementsAttr of mhlo.slice | mhlo::SliceOp::build (@0x8f83dd0) demangles to (…, Type, Value, DenseIntElementsAttr ×3) = start/limit/stride; BoolAttr::get (@0x9b4ddb0) and AddOp::build (@0x8f704b0) appear in the stride callee graph |
Func-level pass, source CanonicalizeConv.cc, error NCC_EUET001 | both string literals (0x2f66a0, 0x226305) appear in the explicitlyPad context refs; the OperationPass<func::FuncOp> wrapper carries matching RTTI |
| No im2col / dot / transpose construction anywhere in the pass | a case-insensitive search for im2col, DotGeneral, TransposeOp, and matmul over all four bodies (runOnOperation, explicitlyPad, explicitlyStride, stride lambda) returns nothing |
| MHLO and StableHLO are separate translation units | StableHLO explicitlyPad @0x211c270 references StableHLOCanonicalizeConv.cc @0x348ed8, and stablehlo::PadOp::build @0x9159ef0 takes llvm::ArrayRef<long> |
Limits of this reading
- The per-vector
ConvParamsfield → semantic mapping (§5) is witnessed only by destructor order, not by named accessors. - The
cl::optnames and defaults of the two BSS flags (qword_9C714B8,qword_9C71578) were not recovered. - The full human-readable text of
NCC_EUET001is assembled at runtime and does not exist as a single pool string.
Related Components
| Name | Relationship |
|---|---|
| Conv device-lowering selection (6.8.1) | consumes the zero-pad/unit-stride canonical form this pass produces; selects the kernel |
| Conv kernels (6.8.2 Depthwise / 6.8.3 Dense Conv2D / 6.8.4 conv2d_pbp) | the actual private_nkl conv lowerings the device-lowering dispatches to |
Cross-References
- Conv Device-Lowering Selection — 6.8.1, the next stage; expects zero-padding, unit-stride convs
- private_nkl Depthwise Conv Kernels — 6.8.2, depthwise conv kernel implementations dispatched after lowering selection
- private_nkl Dense Conv2D (NHWC-Replication & Column-Packing) — 6.8.3, dense conv2d kernel implementation
- Experimental conv2d_pbp Kernels — 6.8.4, additional (experimental) conv kernel variants