56 affine Dialect
56.1 Beginner Summary
The affine dialect represents loops, conditionals, and memory accesses whose indexing can be described with affine expressions.
An affine expression is a restricted integer expression built from loop induction variables, symbols, constants, addition, subtraction, multiplication by constants, floor division by constants, ceiling division by constants, and modulo by constants.
For a beginner, the practical idea is:
affine.foris a loop with analyzable bounds.affine.ifis a branch with analyzable integer constraints.affine.loadandaffine.storeare memory accesses with analyzable subscripts.affine.apply,affine.min, andaffine.maxcompute analyzable index expressions.- Affine loop passes can tile, fuse, unroll, vectorize, parallelize, and simplify loop nests because the compiler can reason about their bounds and memory accesses.
Think of affine as MLIR’s “structured loop math” dialect. It is more restricted than scf, but those restrictions are exactly what make stronger loop and memory optimizations possible.
56.2 Why This Dialect Exists
Many compiler optimizations need to answer questions such as:
- How many times does this loop run?
- Which memory element does this iteration access?
- Do two loop nests access the same element?
- Can these loops be fused?
- Can this loop be tiled?
- Is this loop parallel?
- What are the exact bounds of this slice?
General-purpose control flow makes those questions difficult. A loop whose bound is computed by arbitrary code may be impossible to analyze precisely.
The affine dialect solves this by using a restricted but useful model:
loop bounds and memory indices are affine functions of loop IVs and symbols
That restriction gives MLIR a strong middle-level representation for polyhedral-style loop transformations, dependence analysis, locality improvement, and exact index simplification.
The cost is that not every program fits. Data-dependent loops, arbitrary branches, indirect indexing, and pointer-heavy control flow often belong in other dialects such as scf, cf, memref, or llvm.
56.3 When It Matters
The affine dialect matters when a compiler still wants to transform loop nests aggressively.
It commonly appears in pipelines for:
- Dense linear algebra kernels.
- Stencil computations.
- Tensor-to-loop lowering.
- Locality optimization.
- Loop tiling.
- Loop fusion.
- Loop unrolling and unroll-and-jam.
- Scalar replacement of temporary memref values.
- Raising simple
memref.loadandmemref.storeoperations into analyzable affine accesses. - Generating explicit copies into faster memory spaces.
- Super-vectorization from loop nests to
vectoroperations. - Mapping simple top-level affine loops to GPU kernels.
A typical flow looks like this:
linalg / tensor / structured computation
-> affine.for, affine.load, affine.store, affine.apply
-> affine tiling, fusion, simplification, scalar replacement
-> affine vectorization or parallelization
-> lower-affine
-> scf / arith / memref / vector
-> target-specific lowering
The key point is that affine is usually a transformation dialect. You use it while the compiler still benefits from exact loop and index structure, then you lower it away before final code generation.
56.4 When To Use It
Use the affine dialect when loop bounds and memory indices are affine functions of known induction variables and symbols.
Use it for:
- Counted loops with statically analyzable bounds.
- Loop nests over dense arrays.
- Index computations that are linear plus constants, constant divisions, or constant modulo.
- Memref accesses where each subscript is analyzable.
- Tiling and fusion pipelines.
- Dependence-sensitive loop optimization.
- Generating local buffers or explicit DMA-like transfers.
- Expressing structured parallel loop bands with affine bounds.
Do not force code into affine when the computation is not affine.
Use scf instead for:
- Data-dependent
whileloops. - Bounds computed by arbitrary operations.
- Branches that depend on arbitrary boolean values.
- Irregular algorithms.
- Control flow where analysis precision is less important than generality.
Use memref.load and memref.store instead of affine.load and affine.store when the access indices are not valid affine expressions.
56.5 Core Concepts
56.5.1 Affine Expressions
An affine expression is an integer index expression that MLIR can analyze.
Examples:
#m0 = affine_map<(d0) -> (d0 + 4)>
#m1 = affine_map<(d0, d1)[s0] -> (d0 * 16 + d1 + s0)>
#m2 = affine_map<(d0) -> (d0 floordiv 4)>
#m3 = affine_map<(d0) -> (d0 mod 8)>
In these examples:
d0andd1are dimensions.s0is a symbol.- Constants can appear directly.
- Multiplication is allowed by constants, not by arbitrary runtime values.
The restrictions are intentional. They let MLIR compose, simplify, compare, and bound the expressions.
56.5.2 Dimensions And Symbols
Affine maps use dimensions and symbols:
#map = affine_map<(d0, d1)[s0] -> (d0 + 2 * d1 + s0)>
Dimensions are usually loop induction variables or values that vary inside the current affine scope.
Symbols are values that are treated as invariant for the affine expression, such as function arguments, constants, or values defined outside the relevant affine scope.
The difference matters because Affine verification depends on whether each operand is a valid dimension or a valid symbol at the operation’s location.
In custom syntax, dimension operands are written in parentheses and symbol operands are written in square brackets:
%j = affine.apply #map(%i, %k)[%offset]
Here %i and %k are dimension operands and %offset is a symbol operand.
56.5.3 Affine Maps And Integer Sets
An affine map computes one or more index results:
#map = affine_map<(d0)[s0] -> (d0 + s0, d0 + 4)>
An integer set describes constraints:
#set = affine_set<(d0)[s0] : (d0 >= 0, -d0 + s0 - 1 >= 0)>
affine.apply, affine.min, affine.max, loop bounds, and memory accesses use affine maps. affine.if uses integer sets.
56.5.4 Half-Open Loops
affine.for uses half-open loop ranges:
for i = lower_bound; i < upper_bound; i += step
The step is a positive constant. Lower and upper bounds are affine maps. If a bound map has multiple results, the lower bound is interpreted as a maximum and the upper bound is interpreted as a minimum.
This is why custom syntax can use:
affine.for %i = max #lb(%n) to min #ub(%n) {
...
}
56.5.5 Analyzable Memory Access
affine.load and affine.store are like memref.load and memref.store, but their subscripts must be affine expressions.
%v = affine.load %A[%i, %j + 1] : memref<64x64xf32>
affine.store %v, %B[%i, %j] : memref<64x64xf32>
This gives MLIR enough information for dependence analysis and transformations such as fusion, scalar replacement, and local buffer generation.
56.5.6 Affine Is Not The Final Form
Backends generally do not consume Affine operations directly.
After Affine-specific transformations are done, lower-affine converts the dialect to more primitive dialects:
affine.for -> scf.for
affine.if -> scf.if
affine.apply -> arith index arithmetic
affine.load -> memref.load
affine.store -> memref.store
affine.parallel -> scf.parallel
affine.vector_* -> vector.load / vector.store
This is a common MLIR pattern: use a structured dialect while it helps optimization, then lower it to more general dialects.
56.6 Operations
56.6.1 Index Operations
affine.apply-
Applies a single-result affine map to
indexoperands and returns anindex. It is the basic operation for naming an affine index computation in SSA form.
#map = affine_map<(d0)[s0] -> (d0 + s0)>
%j = affine.apply #map(%i)[%offset]
Use affine.apply when an index expression needs to be reused or passed to another operation.
affine.min- Computes the minimum of all results of an affine map.
#ub = affine_map<(d0) -> (d0, 64)>
%limit = affine.min #ub(%n)
This is useful for upper bounds, edge tiles, and clamping computations.
affine.max- Computes the maximum of all results of an affine map.
#lb = affine_map<(d0) -> (0, d0 - 8)>
%start = affine.max #lb(%n)
This is useful for lower bounds and guarded slices.
affine.linearize_index- Converts a multi-dimensional index into one linear index using a basis.
%linear = affine.linearize_index [%i, %j] by (4, 8) : index
With a basis like (4, 8), the first basis element is an outer bound. It can help analysis but is not used in the arithmetic. The operation can also carry the disjoint property, which asserts that the component indices fit within their basis ranges.
affine.delinearize_index- Converts one linear index into multiple indices using a basis.
%i, %j = affine.delinearize_index %linear into (4, 8) : index, index
The basis elements must be strictly positive. Dynamic basis values that are zero or negative cause undefined behavior. Lowerings may also assume the index arithmetic does not overflow in the signed sense.
56.6.2 Control-Flow Operations
affine.for- A counted loop with affine lower and upper bounds and a positive constant step.
affine.for %i = 0 to 64 {
...
}
affine.for can carry loop-carried values through iter_args, similar to scf.for. If the loop produces results, the body must end with affine.yield.
affine.if- A structured conditional controlled by an affine integer set.
#set = affine_set<(d0)[s0] : (d0 >= 0, -d0 + s0 - 1 >= 0)>
affine.if #set(%i)[%n] {
...
}
Like scf.if, it can produce values from then and else regions.
affine.parallel- A parallel loop band with affine bounds.
affine.parallel (%i) = (0) to (16) {
...
}
It can also express reductions with reduction kinds such as "addf". The execution order is unspecified, so the body must be valid for parallel execution.
affine.yield-
Terminates the region of
affine.for,affine.if, oraffine.parallel. It yields zero or more values to the parent operation.
When the parent operation has no results, custom syntax can omit the affine.yield.
56.6.3 Memory Operations
affine.load- Loads one element from a memref using affine indices.
%v = affine.load %A[%i, %j + 1] : memref<64x64xf32>
The result type is the memref element type.
affine.store- Stores one element to a memref using affine indices.
affine.store %v, %A[%i, %j + 1] : memref<64x64xf32>
The stored value type must match the memref element type.
affine.prefetch- Describes a prefetch of a memref element or region using affine indices. It carries whether the prefetch is for a read or write, a locality hint, and whether the target is the data cache or instruction cache.
Use it when a pipeline wants to preserve prefetch intent before lowering to target-specific code.
affine.vector_load- Loads a contiguous slice from a memref into a non-zero-rank vector using affine indices.
%v = affine.vector_load %A[%i] : memref<64xf32>, vector<4xf32>
The memref element type and vector element type must match.
affine.vector_store- Stores a non-zero-rank vector into a contiguous memref slice using affine indices.
affine.vector_store %v, %A[%i] : memref<64xf32>, vector<4xf32>
These vector operations are still affine memory accesses. The vector type describes the slice shape.
56.6.4 DMA Operations
affine.dma_start- Starts a non-blocking DMA transfer from a source memref to a destination memref. Source, destination, and tag indices all use affine maps.
The operation records:
- Source memref and source indices.
- Destination memref and destination indices.
- Tag memref and tag indices.
- Number of elements.
- Optional stride and elements-per-stride values.
affine.dma_wait- Waits for the DMA transfer associated with a tag memref element to complete.
The DMA operations are useful for explicitly managed memory hierarchies, such as moving data between slower and faster memory spaces. They are also used by Affine data-copy and pipelining transformations.
56.7 Transformations
56.7.1 Canonicalization And Simplification
Most Affine ops have verifiers, folders, or canonicalization patterns.
Useful simplification passes include:
affine-simplify-structures: simplifies affine expressions in maps and sets and normalizes memrefs.affine-simplify-min-max: simplifiesaffine.min,affine.max, andaffine.applyusing canonicalization and value-bounds reasoning.affine-simplify-with-bounds: simplifiesaffine.delinearize_index,affine.linearize_index, andaffine.forusing value bounds analysis.affine-expand-index-ops: lowers affine index operations such asaffine.linearize_indexandaffine.delinearize_indexinto more fundamental operations.affine-expand-index-ops-as-affine: lowers those index operations intoaffine.applyoperations when affine form should be preserved longer.
Use these passes after transformations introduce complex maps, min/max expressions, or linearized index calculations.
56.7.2 Loop Transformations
The Affine dialect has a large loop-optimization surface:
affine-loop-fusion: fuses affine loop nests, including producer-consumer and sibling fusion cases.affine-loop-tile: tiles affine loop nests.affine-loop-unroll: unrolls affine loops.affine-loop-unroll-jam: unrolls an outer loop and jams the copies of the inner loop body together.affine-loop-invariant-code-motion: hoists loop-invariant operations out of affine loops.affine-loop-normalize: normalizes affine loop-like ops, including promotion of single-iteration loops when enabled.affine-loop-coalescing: coalesces nested loops with independent bounds into one loop.
These are the main reason to use Affine in the first place. They are much more powerful when the compiler can see exact loop bounds and memory subscripts.
56.7.3 Memory And Locality Transformations
Affine also has memory-specific transformations:
affine-data-copy-generate: creates explicit copies for affine memory operations, optionally using DMA and faster memory spaces.affine-scalrep: performs scalar replacement by forwarding stores to loads and eliminating redundant affine loads.affine-raise-from-memref: turns supportedmemref.loadandmemref.storeoperations intoaffine.loadandaffine.store.affine-fold-memref-alias-ops: foldsmemref.subview,memref.expand_shape, andmemref.collapse_shapeinto affine loads and stores when possible.affine-pipeline-data-transfer: overlaps non-blocking DMA transfers with computation through double buffering.
These passes are especially useful when a compiler is trying to reduce memory traffic, improve locality, or target a memory hierarchy with explicit fast and slow spaces.
56.7.4 Parallelization And Vectorization
Affine can expose parallelism and vector structure:
affine-parallelize: converts suitableaffine.forloops into one-dimensionalaffine.paralleloperations.affine-super-vectorize: vectorizes affine loops to target-independentvectoroperations.
affine.parallel still belongs to the Affine dialect, so it can remain analyzable before later lowering to scf.parallel or GPU-oriented forms.
56.7.5 Transform Dialect Controls
Affine also exposes some transformations through the Transform dialect:
transform.affine.simplify_bounded_affine_opstransform.affine.simplify_min_max_affine_opstransform.affine.super_vectorize
These are not Affine dialect operations. They are Transform dialect operations that target Affine operations from a transform script.
Use them when a pipeline wants to control Affine simplification or vectorization declaratively through Transform IR.
56.8 Conversions And Lowering Paths
56.8.1 lower-affine
The main lowering pass is lower-affine.
It lowers Affine operations to a combination of:
arithscfmemrefvector
Important rewrites include:
affine.fortoscf.foraffine.iftoscf.ifaffine.paralleltoscf.parallelaffine.yieldtoscf.yieldaffine.apply,affine.min, andaffine.maxto primitivearithoperations onindexaffine.loadtomemref.loadaffine.storetomemref.storeaffine.prefetchtomemref.prefetchaffine.dma_starttomemref.dma_startaffine.dma_waittomemref.dma_waitaffine.vector_loadtovector.loadaffine.vector_storetovector.storeaffine.linearize_indexandaffine.delinearize_indexto lower-level index arithmetic
This is usually the point where Affine’s high-level restrictions are no longer needed.
56.8.2 Affine To GPU
convert-affine-for-to-gpu converts top-level affine.for operations to GPU kernels.
The pass has options for the number of GPU block dimensions and thread dimensions:
gpu-block-dimsgpu-thread-dims
Use this when affine loop nests are simple enough to map directly to GPU launch structure. More complex GPU pipelines may first go through scf, gpu, linalg, or vector transformations.
56.8.3 Raising From MemRef
affine-raise-from-memref goes in the opposite direction from lowering.
It tries to turn supported:
memref.load
memref.store
into:
affine.load
affine.store
This is useful when earlier passes produced ordinary memref operations but the indices are still affine and later Affine passes can optimize them.
56.9 Example IR
56.9.1 Shifted Copy
This example uses affine.for, affine.apply, affine.load, and affine.store.
#shift = affine_map<(d0)[s0] -> (d0 + s0)>
func.func @shifted_copy(%src: memref<128xf32>,
%dst: memref<128xf32>,
%offset: index) {
affine.for %i = 0 to 64 {
%j = affine.apply #shift(%i)[%offset]
%v = affine.load %src[%j] : memref<128xf32>
affine.store %v, %dst[%i] : memref<128xf32>
}
return
}
The loop is analyzable because the bounds are constants and the memory subscripts are affine expressions of %i and %offset.
56.9.2 Guarded Read
This example uses affine.if with an integer set.
#set = affine_set<(d0)[s0] : (d0 >= 0, -d0 + s0 - 1 >= 0)>
func.func @guarded_read(%A: memref<?xf32>, %i: index, %n: index) -> f32 {
%zero = arith.constant 0.0 : f32
%v = affine.if #set(%i)[%n] -> f32 {
%x = affine.load %A[%i] : memref<?xf32>
affine.yield %x : f32
} else {
affine.yield %zero : f32
}
return %v : f32
}
The integer set means:
0 <= i && i <= n - 1
Because that condition is affine, MLIR can reason about it together with other affine constraints.
56.9.3 Parallel Reduction
This example uses affine.parallel and affine.yield.
func.func @sum16(%A: memref<16xf32>) -> f32 {
%sum = affine.parallel (%i) = (0) to (16) reduce ("addf") -> f32 {
%v = affine.load %A[%i] : memref<16xf32>
affine.yield %v : f32
}
return %sum : f32
}
The operation says the iterations can run in parallel and their yielded values are combined with a floating-point add reduction.
56.9.4 Linearize And Delinearize
func.func @linearize(%i: index, %j: index) -> (index, index, index) {
%lin = affine.linearize_index [%i, %j] by (4, 8) : index
%d0, %d1 = affine.delinearize_index %lin into (4, 8) : index, index
return %lin, %d0, %d1 : index, index, index
}
Linearization converts a multi-index into one index. Delinearization converts one index back into coordinates. These operations are common around flattening, tiling, and mapping multi-dimensional loops to one-dimensional work.
56.9.5 Vector Access
func.func @vector_access(%A: memref<64xf32>,
%v: vector<4xf32>) -> vector<4xf32> {
%c0 = arith.constant 0 : index
%loaded = affine.vector_load %A[%c0] : memref<64xf32>, vector<4xf32>
affine.vector_store %v, %A[%c0] : memref<64xf32>, vector<4xf32>
return %loaded : vector<4xf32>
}
The access starts at an affine index, while the vector type describes the shape of the slice.
56.10 Mental Model
Treat the affine dialect as a contract with the optimizer.
When you write:
affine.for %i = 0 to 64 {
%v = affine.load %A[%i] : memref<64xf32>
}
you are not just saying “run a loop and load memory.” You are also saying:
the loop range and memory access are structured enough for MLIR to analyze
That contract enables stronger transformations.
The tradeoff is that Affine is less general than scf. If the compiler cannot prove that a value is a valid dimension or symbol, or if the expression is not affine, the IR should use a more general dialect.
Good beginner rule:
Use affine when the loop math is simple and important.
Use scf when the control flow is general.
56.11 Gotchas
- Affine expressions are not arbitrary arithmetic. Multiplication by a runtime value is not affine.
- Dimension and symbol placement matters. A value that is valid as a symbol in one location may not be valid as a symbol somewhere else.
affine.forhas a positive constant step.affine.forranges are half-open. The upper bound is not included.- Multi-result lower-bound maps are interpreted as
max; multi-result upper-bound maps are interpreted asmin. affine.loadandaffine.storerequire affine subscripts. If indexing is indirect or data-dependent, usememref.loadandmemref.store.affine.parallelhas unspecified iteration order. Only use it when the body is safe for parallel execution or expresses reductions correctly.- Dynamic basis elements for
affine.delinearize_indexmust be strictly positive. affine.linearize_indexandaffine.delinearize_indexlowerings may assume signedindexarithmetic does not overflow.- Affine is usually not a final backend dialect. Most pipelines eventually run
lower-affine.
56.12 Source Map
Primary definitions:
mlir/include/mlir/Dialect/Affine/IR/AffineOps.tdmlir/include/mlir/Dialect/Affine/IR/AffineOps.hmlir/lib/Dialect/Affine/IR/AffineOps.cppmlir/include/mlir/Dialect/Affine/Transforms/Passes.tdmlir/lib/Dialect/Affine/Transforms/mlir/include/mlir/Dialect/Affine/TransformOps/AffineTransformOps.tdmlir/lib/Conversion/AffineToStandard/AffineToStandard.cppmlir/include/mlir/Conversion/Passes.td
All Affine dialect operations covered in this chapter:
affine.applyaffine.delinearize_indexaffine.dma_startaffine.dma_waitaffine.foraffine.ifaffine.linearize_indexaffine.loadaffine.maxaffine.minaffine.parallelaffine.prefetchaffine.storeaffine.vector_loadaffine.vector_storeaffine.yield
Affine-specific passes covered in this chapter:
affine-data-copy-generateaffine-loop-fusionaffine-loop-invariant-code-motionaffine-loop-tileaffine-loop-unrollaffine-loop-unroll-jamaffine-pipeline-data-transferaffine-scalrepaffine-super-vectorizeaffine-parallelizeaffine-loop-normalizeaffine-loop-coalescingaffine-raise-from-memrefaffine-simplify-structuresaffine-simplify-min-maxaffine-simplify-with-boundsaffine-expand-index-opsaffine-expand-index-ops-as-affineaffine-fold-memref-alias-ops
Conversion passes and paths covered in this chapter:
lower-affineconvert-affine-for-to-gpu- Affine to
arith - Affine to
scf - Affine to
memref - Affine to
vector - Raising supported
memref.loadandmemref.storeoperations back to Affine withaffine-raise-from-memref