AscTile passes
-asctile-cube-transpose-to-load
Remove transpose operations on cube operands and add transpose attributes to load/copy operations
This pass eliminates asctile.transpose operations on cube (matrix multiplication) operands in L0A/L0B
buffers by adding transposeA or transposeB attributes to the underlying load and copy operations.
The transformation applies when:
The transpose result is in L0A or L0B buffer (cube operands)
The transpose input is a
copyoperation with exactly one useThe copy’s source is a
loadoperation with exactly one use
The pass:
Removes the transpose operation
Adds
transposeAattribute to load and copy if result is in L0AAdds
transposeBattribute to load and copy if result is in L0BSwaps the dimensions in the copy result type (transposed shape)
Example transformation:
// Before: separate load, copy, and transpose
%0 = load %tensor[%i, %j], %pad : tensor, tensor<32x128xf16, L1>
%1 = copy %0[%x, %y] : tensor<32x128xf16, L1>, tensor<32x128xf16, L0A>
%2 = transpose %1 : tensor<32x128xf16, L0A> to tensor<128x32xf16, L0A>
// After: transpose attribute propagated, transpose op removed
%0 = load %tensor[%i, %j], %pad {transposeA} : tensor, tensor<32x128xf16, L1>
%1 = copy %0[%x, %y] {transposeA} : tensor<128x32xf16, L1>, tensor<128x32xf16, L0A>
-asctile-densify-unroll-groups
Group operations tagged with the same unroll_group attribute together after loop unrolling
This pass collects operations tagged with asctile.unroll_group attribute (added by TagUnrollGroups pass)
and moves them together within their blocks to improve locality and enable better optimization opportunities.
For each unroll group, operations are densified according to their type:
LoadOps: moved toward the top (earliest valid position respecting dominance)
StoreOps: moved toward the bottom (latest valid position respecting dominance)
The movement respects dominance constraints and memory effects to preserve program semantics.
After densification, the unroll_group attribute is removed from all operations.
Example transformation:
// Before: load, relu, load, store, relu, store (with groups 0 and 1)
%0 = load {unroll_group=0}
%1 = relu %0
%2 = load {unroll_group=0}
store %0 {unroll_group=1}
%3 = relu %2
store %2 {unroll_group=1}
// After: loads grouped, stores grouped
%0 = load
%2 = load
%1 = relu %0
%3 = relu %2
store %0
store %2
-asctile-detect-bias-load
Detect load operation from GM to L1 for bias
This pass identifies load operations that feed bias data for matrix multiplication and marks them
with the isBias attribute. This enables subsequent passes to optimize bias handling in matmul operations.
The pass identifies bias loads by checking if the loaded tensor is copied to TensorLocation::BT (bias table),
which is the dedicated memory location for bias values in matmul operations.
Example transformation:
// Before: load without bias marking
%0 = load %tensor[%offset] : tensor, tensor<128xf16, L1>
%1 = copy %0[%base] : tensor<128xf16, L1> to tensor<128xf16, BT>
// After: load marked as bias
%0 = load %tensor[%offset] {isBias} : tensor, tensor<128xf16, L1>
%1 = copy %0[%base] : tensor<128xf16, L1> to tensor<128xf16, BT>
-asctile-fold-cast
Fold chains of cast operations and verify supported type conversions
This pass folds chains of consecutive asctile.cast operations and validates that all remaining
cast operations perform type conversions that are supported by the hardware.
Folding: When two cast operations are chained (e.g., cast(i8→i16) followed by cast(i16→i32)),
they are folded into a single direct cast (e.g., cast(i8→i32)), if the resulting conversion is
supported.
Validation: After folding, the pass checks that all cast operations use supported type conversions:
Integer to integer: i8→i16, i8→i32, i16→i32, i32→i16, i32→i64, i64→i32
Integer to float: i8/i16/i32→f16, i16/i32/i64→f32
Float to integer: bf16→i32, f16→i8/i16/i32, f32→i16/i32/i64
Float to float: bf16→f16/f32, f16→bf16/f32, f32→bf16/f16/f32
If any cast operation violates these constraints, the pass emits an error and signals failure.
-asctile-legalize-matmul
Verify that matmul accumulator operations use valid accumulator operands
This pass validates that all asctile.matmul_acc operations use an accumulator operand that is
defined by an asctile.accumulator operation. This ensures the matmul accumulator pattern is correct
and the hardware constraints are satisfied.
If any matmul_acc operation violates this constraint, the pass emits an error and signals failure.
This is a verification-only pass that does not transform the IR.
-asctile-promote-pure-ops
Hoist pure operations upward within their blocks to enable code motion optimizations
This pass moves pure (side-effect-free) operations as early as possible within their containing block, respecting dominance constraints. This enables loop-invariant code motion and improves scheduling.
Pure operations include arithmetic ops, constants, and other ops without side effects. Operations with side effects (e.g., memory ops, barriers) remain in their original positions.
Example transformation:
// Before: pure ops interleaved with barriers inside a loop
scf.for {
%0 = addf %a, %b // pure
%1 = addf %a, %0 // pure, depends on %0
gpu.barrier // side effect
%2 = mulf %a, %b // pure, independent
%3 = constant 7.0 // pure, independent
%4 = addf %a, %3 // pure, depends on %3
%5 = subf %4, %2 // pure
gpu.barrier // side effect
}
// After: pure ops hoisted as early as possible
scf.for {
%3 = constant 7.0
%2 = mulf %a, %b
%4 = addf %a, %3
%0 = addf %a, %b
%1 = addf %a, %0
%5 = subf %4, %2
gpu.barrier
gpu.barrier
}
-asctile-split-cube-load
Split direct GM-to-L0 loads into GM-to-L1 followed by L1-to-L0 copies for cube operations
This pass transforms direct loads from global memory (GM) to L0A/L0B buffer locations into a two-step sequence: load to L1 first, then copy from L1 to L0A/L0B. This is required for cube (matrix multiplication) operations where operands must reside in specific L0 buffers.
The pass applies two patterns:
ConvertLoadGMToL0: Splits
loadproducing L0A/L0B tensor into:loadproducing L1 tensor (from GM)copyfrom L1 to L0A/L0B with zero offsets
MarkTileOperandInMmad: Marks L1 tiles used exclusively for matrix multiplication:
If an L1 tensor is copied only to L0A, marks it with
isMatrixAattributeValidates consistency: same L1 tensor should not be copied to both L0A and L0B
Emits error if L1 tensor is used for non-copy operations
Example transformation:
// Before: direct load to L0A
%0 = load %tensor[%offset] : tensor, tensor<16x16xf32, L0A>
// After: split into L1 load + L1-to-L0A copy
%0 = load %tensor[%offset] : tensor, tensor<16x16xf32, L1>
%zero = constant 0 : i32
%1 = copy %0[%zero, %zero] : tensor<16x16xf32, L1> to tensor<16x16xf32, L0A>
-asctile-tag-unroll-groups
Tag Load and Store operations inside unrollable loops with unroll_group attributes
This pass tags asctile.load and asctile.store operations inside scf.for loops that have
the asctile.unroll_factor attribute. The tags are used by DensifyUnrollGroups pass after
loop unrolling to group related operations together.
Tagging behavior depends on the small-groups option:
smallGroups=false (default): All LoadOps in a loop get the same group index, all StoreOps get the next group index. This enables densification to bring all loads together and all stores together.
smallGroups=true: Each LoadOp/StoreOp gets a unique group index. This preserves the original ordering during densification.
Only loops with unroll_factor attribute are processed; other loops are skipped.
Example transformation:
// Before: loop with unroll_factor
scf.for {unroll_factor = 4} {
%0 = load ...
%1 = load ...
store %0, ...
}
// After (smallGroups=false): loads share group 0, stores share group 1
scf.for {unroll_factor = 4} {
%0 = load {unroll_group = 0}
%1 = load {unroll_group = 0}
store %0, ... {unroll_group = 1}
}
// After (smallGroups=true): each op gets unique group
scf.for {unroll_factor = 4} {
%0 = load {unroll_group = 0}
%1 = load {unroll_group = 1}
store %0, ... {unroll_group = 2}
}
Options
-small-groups : Tag each operation with unique group index; otherwise all loads share one group and stores another
-asctile-transform-math-ops
Convert tensor arithmetic operations with splat operands to scalar-splat variants for HW efficiency
This pass specializes arithmetic and comparison operations on tiles when one operand is a splat value
(uniform constant across all elements). The scalar-splat variants (adds, muls, cmps, etc.) are
more efficient on the hardware than tensor-tensor operations.
Transformation patterns (applied greedily with pattern benefits):
MaxWithZeroToRelu (benefit 2):
max(x, 0)ormax(0, x)→relu(x)formaximumf,maxnumf,maxsiScalarizeArithOp (benefit 1, for commutative ops: add, mul, max, min):
If one operand is splat constant → converts to scalar-splat variant
Can handle splat on either lhs or rhs (swaps operands if lhs is splat)
addf(tensor, splat)→adds(tensor, scalar)mulf(splat, tensor)→muls(tensor, scalar)(swapped)
ScalarizeArithRhsOp (benefit 1, for non-commutative ops: sub, div):
Only rhs splat is converted (cannot swap operands)
subf(tensor, splat)→subs(tensor, scalar)subf(splat, tensor)remains unchanged
ScalarizeShL/ScalarizeShR:
shli/shrsi(tensor, splat)→shls/shrs(tensor, scalar)ScalarizeCompare:
cmp(mode, tensor, splat)→cmps(mode, tensor, scalar)If lhs is splat, comparison mode is inverted (LT→GT, LE→GE, etc.)
Example transformations:
// Splat constant converted to scalar
%cst = constant dense<7.0> : tensor<32xf32>
%0 = addf %tensor, %cst : tensor<32xf32>
// →
%scalar = constant 7.0 : f32
%0 = adds %tensor, %scalar : tensor<32xf32>
// Max with zero becomes relu
%zero = constant dense<0.0> : tensor<32xf32>
%0 = maximumf %zero, %tensor : tensor<32xf32>
// →
%0 = relu %tensor : tensor<32xf32>
// Comparison with splat, lhs case (mode inverted)
%cst = constant dense<1.0> : tensor<32xf32>
%0 = cmp LT %cst, %tensor : tensor<32xf32>
// →
%scalar = constant 1.0 : f32
%0 = cmps GT %tensor, %scalar : tensor<32xf32> // LT → GT
-asctile-transform-store-fixpipe
Convert store/copy operations from L0C to fixpipe variants and fold relu/cast into fixpipe flags
This pass handles data movement from L0C (matrix multiplication output buffer) by converting regular store/copy operations to fixpipe variants, which support fused relu and quantization operations.
The “fixpipe” operation is a hardware-specific pipeline that can combine:
Data transfer from L0C to UB or GM
Optional relu activation
Optional quantization (cast operation)
Transformation patterns:
TransformCopyOp:
copywith L0C source →copy_fixpipeTransformStoreOp:
storewith L0C value →store_fixpipeTransformFixpipeReluOp/CopyFixpipeReluOp: If fixpipe input is
reluon L0C, use the relu operand directly and setrelu=trueflag on fixpipeTransformFixpipeCastOp/CopyFixpipeCastOp: If fixpipe input is
caston L0C, use the cast input directly and setquantize=trueflag on fixpipe
Example transformations:
// Before: separate store and relu
%result = matmul_acc %acc, %a, %b : tensor<16x16xf32, L0C>
%activated = relu %result : tensor<16x16xf32, L0C>
store %activated, %tensor[%offset] : tensor<16x16xf32, L0C>, tensor
// After: fused store_fixpipe with relu flag
%result = matmul_acc %acc, %a, %b : tensor<16x16xf32, L0C>
store_fixpipe relu=true %result, %tensor[%offset] : tensor<16x16xf32, L0C>, tensor
// Before: separate copy and cast
%result = matmul_acc ... : tensor<16x16xf32, L0C>
%casted = cast %result : tensor<16x16xf32, L0C> to tensor<16x16xi8, UB>
copy %casted, [%zero] : tensor<16x16xf32, L0C> to tensor<16x16xi8, UB>
// After: fused copy_fixpipe with quantize flag
%result = matmul_acc ... : tensor<16x16xf32, L0C>
copy_fixpipe quantize=true %result, [%zero] : tensor<16x16xf32, L0C> to tensor<16x16xi8, UB>
-asctile-unroll-loop
Unroll scf.for loops by their unroll_factor attribute value
This pass unrolls scf.for loops by the factor specified in their asctile.unroll_factor attribute.
After unrolling, the attribute is removed from the loop.
For loops where the iteration count is statically known and evenly divisible by the unroll factor, the loop step is multiplied by the factor and the body is replicated.
For dynamic loops (unknown upper bound) or when iteration count is not evenly divisible, an epilogue
loop is created to handle the remaining iterations. The unroll_group attributes are removed from
operations in the epilogue loop since they don’t correspond to the original unrolled iterations.
Example transformations:
// Static loop: 32 iterations, unroll factor 2
scf.for %i = 0 to 32 step 1 {unroll_factor = 2} {
%val = index_cast %i
set_value %val, %tensor[0]
}
// After: 16 iterations with step 2, body replicated twice
scf.for %i = 0 to 32 step 2 {
%val = index_cast %i
set_value %val, %tensor[0]
%i1 = addi %i, 1
%val1 = index_cast %i1
set_value %val1, %tensor[0]
}
// Dynamic loop: unknown upper bound, unroll factor 3
scf.for %i = 0 to %n step 1 {unroll_factor = 3} { ... }
// After: main loop with step 3 + epilogue loop for remainder
%remainder = remsi %n, 3
%aligned = subi %n, %remainder
scf.for %i = 0 to %aligned step 3 { /* body replicated 3 times */ }
scf.for %i = %aligned to %n step 1 { /* original body, no unroll_group */ }
Options
-annotate : Add annotation attribute to each inner operation
-asctile-unscalarize-reduction
Convert scalar reduction results to 1-element tiles when used in tensor operations
This pass avoids scalar results from reduce_as_1d operations when all users are scalar-tensor operations
(adds, subs, muls, divs, mins, maxs, splat). Instead, the result is kept as a 1-element tensor, which is
more efficient for the hardware as it avoids scalar-to-tensor conversions.
The pass uses dialect conversion to transform:
reduce_as_1dwith scalar result →reduce_as_1dwith 1-element tensor resultsplat(scalar to tensor) →broadcast(1-element tensor to larger tensor)Scalar-tensor ops (adds, subs, etc.) → tensor-tensor ops (addf, subf, etc.)
The transformation only applies when ALL users of the scalar reduction result are unscalarizable. If any user operates on the scalar directly (e.g., scalar arithmetic), the reduction is left unchanged.
Example transformations:
// Before: scalar reduction used in scalar-tensor ops
%scalar = reduce_as_1d <sum> %tensor : tensor<16xf32>, f32
%result1 = adds %tensor, %scalar : tensor<16xf32>
%result2 = subs %tensor, %scalar : tensor<16xf32>
// After: 1-element tensor reduction used in tensor-tensor ops
%reduced = reduce_as_1d <sum> %tensor : tensor<16xf32>, tensor<1xf32>
%broadcast1 = broadcast %reduced : tensor<1xf32> to tensor<16xf32>
%result1 = addf %tensor, %broadcast1 : tensor<16xf32>
%broadcast2 = broadcast %reduced : tensor<1xf32> to tensor<16xf32>
%result2 = subf %tensor, %broadcast2 : tensor<16xf32>
// Case skipped: scalar has non-unscalarizable user
%scalar = reduce_as_1d <sum> %tensor : tensor<16xf32>, f32
%result = adds %tensor, %scalar : tensor<16xf32> // unscalarizable
%scalar_op = addf %scalar, %scalar : f32 // scalar user -> NOT transformed
-asctile-vector-transpose-to-load
Replace transpose and load to single operation when possible
Replace asctile.load and asctile.transpose into single asctile.load with asctile.transpose_dims attribute.
This pass only handles GM->UB transfers.
Example:
// Before:
%1 = asctile.load : tensor<16x32xf32, UB>
%2 = asctile.transpose %1 : tensor<16x32xf32, UB> to tensor<32x16xf32, UB>
// After:
%2 = asctile.load {asctile.transpose_dims = [1, 0]} : tensor<32x16xf32, UB>