42 scf Dialect
42.1 Beginner Summary
The scf dialect is MLIR’s structured control flow dialect. It represents loops, conditionals, switches, and structured parallel regions as nested MLIR regions instead of raw blocks and branches.
For a beginner, the main idea is this: scf is where a compiler can still see that a program has a loop or an if statement. That structure makes optimization easier. Later, when the compiler needs a lower-level form, scf can be converted to branch-based cf, GPU launches, OpenMP, SPIR-V structured control flow, or EmitC.
42.2 Why This Dialect Exists
MLIR has both structured and unstructured control flow.
cf models low-level control flow with branches between blocks. That is close to a traditional control-flow graph. It is flexible, but the original source structure can be hard to recover.
scf keeps common control-flow shapes explicit:
scf.foris a counted loop.scf.ifis a conditional with then and optional else regions.scf.whileis a loop with explicit condition and body regions.scf.parallelandscf.forallexpress parallel iteration spaces.scf.index_switchexpresses a structured switch over anindexvalue.
This is useful in the middle of a compiler pipeline. Dialects such as affine, linalg, vector, tosa, and hardware-specific dialects often create or use SCF before lowering to target-specific IR.
42.3 When It Matters
SCF matters whenever the compiler needs to preserve loop and branch structure.
Use it when:
- You need readable loop and if constructs in MLIR.
- You are lowering from higher-level tensor, affine, vector, or model dialects.
- You want to apply loop transformations such as peeling, tiling, fusion, specialization, unrolling, or software pipelining.
- You need a target-independent representation before choosing CPU, GPU, OpenMP, SPIR-V, EmitC, or LLVM lowering.
- You want parallel loops that are not yet committed to a concrete runtime or hardware mapping.
Avoid using it when:
- You are already at low-level CFG form and do not need structured analysis.
- You need arbitrary branches that do not fit structured regions.
- You are encoding target intrinsics or ABI details. Those belong in target dialects such as
llvm,gpu,spirv,omp, oremitc.
42.4 What It Means
An SCF operation usually owns one or more regions. A region is a nested body of IR with its own block arguments and terminator. This is how SCF represents structured control flow without exposing arbitrary branch edges.
SCF also uses SSA values to model values carried through loops and conditionals:
- Loop-carried values enter
scf.forthroughiter_args. scf.yieldpasses values from a region back to the parent op.scf.conditiondecides whether anscf.whilecontinues.scf.reduceandscf.reduce.returndescribe reductions forscf.parallel.scf.forall.in_paralleldescribes how parallelshared_outsare combined.
The important implication is that control-flow structure and value flow are part of the IR, not just comments around blocks.
42.5 Core Concepts
42.5.1 Structured Regions
Most SCF operations contain nested regions. For example, an scf.if contains a then region and may contain an else region. An scf.for contains the loop body. An scf.while contains a before region and an after region.
The region shape is part of the operation verifier. This is why invalid loop or branch structure is usually caught early.
42.5.2 Terminators
SCF region terminators are also SCF operations.
scf.yieldterminates regions ofscf.for,scf.if,scf.index_switch,scf.while, andscf.execute_region.scf.conditionterminates the before region ofscf.while.scf.reduceterminatesscf.parallel.scf.reduce.returnterminates a reduction combiner region.scf.forall.in_parallelterminatesscf.forall.
Some terminators can be omitted in custom syntax when no values are returned. The parser or builder inserts them implicitly.
42.5.3 Loop-Carried Values
scf.for and scf.while can carry values from one iteration to the next. This is the structured equivalent of updating variables in a source-language loop.
The types must line up:
- Initial values define the loop-carried value types.
- Region arguments receive the current iteration values.
scf.yieldprovides the next iteration values.- The parent operation results provide the final values.
42.5.4 Parallel Semantics
scf.parallel means the iteration space may execute in any order and in parallel. If there are data races, behavior is undefined. Reductions are explicit.
scf.forall is a newer target-independent parallel region construct. It models a set of virtual threads and can carry mapping attributes that later lowerings interpret for a concrete target such as GPU.
42.6 Operations
SCF defines 12 operations.
42.6.1 Basic Structured Control Flow
scf.if: A structured if-then-else operation. It takes ani1condition and has a then region plus an optional else region. If it returns values, both branches must yield matching values.scf.index_switch: A structured switch on anindexvalue. It contains one default region and zero or more case regions.scf.execute_region: Executes a region exactly once. It is useful when an operation normally allows only a single block but a temporary multi-block region is needed. The optionalno_inlineunit attribute asks canonicalizers to preserve the op until an explicit lowering step.
42.6.2 Sequential Loops
scf.for: A counted loop with lower bound, upper bound, and positive step. Bounds are half-open: the lower bound is included and the upper bound is excluded. It may have loop-carried values throughiter_args. TheunsignedCmpunit attribute makes integer loop comparisons unsigned.scf.while: A general structured while or do-while loop. It has a before region terminated byscf.conditionand an after region terminated byscf.yield.scf.condition: The terminator of the before region ofscf.while. Its first operand is the continuation condition. If true, execution enters the after region; otherwise thescf.whileexits.scf.yield: The common terminator for many SCF regions. It returns values from the region to the parent SCF operation.
42.6.3 Parallel Loops
scf.parallel: A multi-dimensional parallel loop. Its iteration order is unspecified, and the body may execute in parallel. It can return reduction results throughscf.reduce.scf.reduce: The terminator ofscf.parallel. It lists values to reduce and owns one combiner region per reduced value.scf.reduce.return: The terminator inside eachscf.reducecombiner region. It returns the combined value.scf.forall: A target-independent multi-dimensional parallel region. It represents virtual threads and optional shared tensor outputs. It can carry amappingattribute array whose elements implement the device mapping interface.scf.forall.in_parallel: The terminator ofscf.forall. Its nested region contains aggregation actions, commonly tensor parallel insert operations for shared outputs. For aforallwithout outputs, the region may be empty.
42.7 Attributes And Types
SCF does not define custom types. It uses standard MLIR types such as index, i1, integer and floating-point types, tensors, memrefs, and vectors.
SCF has operation-specific attributes:
scf.forhasunsignedCmp.scf.forallhasstaticLowerBound,staticUpperBound,staticStep, andmapping.scf.index_switchhascases.scf.execute_regionhasno_inline.
The mapping attribute on scf.forall is not an SCF-specific mapping format. It accepts attributes that implement the device mapping interface, such as GPU mapping attributes.
42.8 Transformations
SCF has a large set of loop and structure transformations. Some are normal passes; others are transform dialect ops for scriptable transformations.
42.8.1 SCF Passes
scf-for-loop-canonicalization: Canonicalizes operations insidescf.forloop bodies. It currently focuses on affine min/max computations involving induction variables, loop bounds, and loop steps.scf-for-loop-peeling: Peelsscf.forloops at their bounds. Options:peel-frontandskip-partial.scf-for-loop-range-folding: Folds add/mul operations into loop ranges.scf-for-loop-specialization: Specializesscf.forloops for vectorization.scf-for-to-while: Rewritesscf.forloops asscf.whileloops.scf-forall-to-for: Convertsscf.forallloops to nestedscf.forloops.scf-forall-to-parallel: Convertsscf.forallloops toscf.parallelloops.scf-parallel-for-to-nested-fors: Convertsscf.parallelloops to nestedscf.forloops.scf-parallel-loop-fusion: Fuses adjacentscf.parallelloops.scf-parallel-loop-specialization: Specializesscf.parallelloops for vectorization.scf-parallel-loop-tiling: Tilesscf.parallelloops. Options:parallel-loop-tile-sizesandno-min-max-bounds.test-scf-parallel-loop-collapsing: A test pass for thescf::collapseParallelLoopsutility. It is not a production pipeline pass, but it documents the collapse utility used by SCF tests.
42.8.2 Transform Dialect Hooks
The SCF transform extension exposes these transform ops:
transform.apply_patterns.scf.for_loop_canonicalization: Collects SCF for-loop canonicalization patterns.transform.apply_conversion_patterns.scf.scf_to_control_flow: Collects patterns that lower SCF to branch-based control flow.transform.apply_conversion_patterns.scf.structural_conversions: Collects SCF structural type conversion patterns.transform.loop.coalesce: Coalesces a perfect loop nest.transform.loop.coalesce_nested: Coalesces nested loops, including imperfectly nested loops.transform.loop.forall_to_for: Converts onescf.forallto nestedscf.forloops.transform.loop.forall_to_parallel: Converts onescf.foralltoscf.parallel.transform.loop.fuse_sibling: Fuses siblingscf.fororscf.forallloops when the caller guarantees independence and the loop shapes match.transform.loop.outline: Outlines a loop-like payload operation to a new function and replaces it with a call.transform.loop.parallel_for_to_nested_fors: Converts onescf.parallelto nestedscf.forloops.transform.loop.peel: Peels the first or last iteration ofscf.for.transform.loop.pipeline: Applies software pipelining toscf.for.transform.loop.promote_if_one_iteration: Removes a loop when it has a single iteration.transform.loop.unroll: Unrollsscf.fororaffine.for.transform.loop.unroll_and_jam: Applies unroll-and-jam toscf.fororaffine.for.transform.scf.take_assumed_branch: Replaces anscf.ifwith a selected branch when the transform script asserts that branch is safe to take.
42.8.3 Library-Level Utilities And Patterns
SCF also exposes C++ utilities and pattern population APIs used by other passes:
forallToForLoopforallToParallelLoopparallelForToNestedForspeelForLoopAndSimplifyBoundspeelForLoopFirstIterationtileParallelLooppipelineForLooppopulateSCFStructuralTypeConversionspopulateSCFStructuralTypeConversionTargetpopulateSCFLoopPipeliningPatternspopulateSCFForLoopCanonicalizationPatternspopulateSCFRotateWhileLoopPatterns
These names matter when you read pass implementations: many “dialect passes” are thin wrappers around these reusable utilities.
42.9 Conversions And Lowering Paths
SCF sits in the middle of many MLIR pipelines.
42.9.1 Common Inputs To SCF
lower-affine: Lowersaffine.fortoscf.for,affine.iftoscf.if, and affine expressions to arithmetic.lift-cf-to-scf: Lifts some branch-basedcfcontrol flow back to SCF. It is named “lift” because it is not guaranteed to eliminate allcfops.convert-vector-to-scf: Lowers selected vector operations, especially higher-rank vector transfers, to loops and conditionals using SCF.tosa-to-scf: Converts TOSA control-flow operations to equivalent SCF operations.convert-arm-sme-to-scf: Lowers selected ArmSME operations into SCF plus supporting dialects.convert-openacc-to-scf: Converts some OpenACC constructs to OpenACC plus SCF.- Linalg tiling and fusion utilities commonly generate
scf.fororscf.forallloops as the loop structure around tiled computations.
42.9.2 SCF To Other Dialects
convert-scf-to-cf: Lowers structured SCF operations to thecfdialect. This is the usual path before lowering to LLVM-style low-level control flow.convert-scf-to-emitc: Lowers SCF to EmitC while preserving structured control flow for C/C++ emission.convert-scf-to-openmp: Convertsscf.parallelto OpenMP parallel and workshare constructs. Option:num-threads.convert-scf-to-spirv: Converts SCF to SPIR-V structured control flow. When SCF ops yield values, the pass uses SPIR-V variables and loads/stores because SPIR-V structured control-flow ops do not yield SSA values directly.convert-parallel-loops-to-gpu: Converts mappedscf.paralleloperations togpu.launchoperations.convert-affine-for-to-gpu: An adjacent conversion in the same conversion area. It converts top-levelaffine.forloops to GPU kernels, not SCF ops.
42.9.3 Internal SCF Normalization Paths
scf-forall-to-for: Turn target-independent virtual threads into sequential loops.scf-forall-to-parallel: Turnscf.forallintoscf.parallel.scf-parallel-for-to-nested-fors: Turn parallel loops into sequential nested loops.scf-for-to-while: Express counted loops asscf.while.
These are useful when a downstream target cannot handle the richer SCF form directly.
42.10 Example IR
42.10.1 Counted Loop With Loop-Carried State
This example uses scf.for, scf.if, and scf.yield.
module {
func.func @sum_if_enabled(%n: index, %enabled: i1) -> f32 {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%zero = arith.constant 0.0 : f32
%one = arith.constant 1.0 : f32
%sum = scf.for %i = %c0 to %n step %c1
iter_args(%acc = %zero) -> (f32) {
%candidate = arith.addf %acc, %one : f32
%next = scf.if %enabled -> (f32) {
scf.yield %candidate : f32
} else {
scf.yield %acc : f32
}
scf.yield %next : f32
}
return %sum : f32
}
}
Read it as: start with %zero; for each iteration, either add one or keep the current value; yield the next accumulator value.
42.10.2 While Loop, Switch, And Execute Region
This example uses scf.while, scf.condition, scf.index_switch, scf.execute_region, and scf.yield.
module {
func.func @choose_after_counting(%n: index, %which: index) -> i32 {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%count = scf.while (%i = %c0) : (index) -> index {
%keep_going = arith.cmpi slt, %i, %n : index
scf.condition(%keep_going) %i : index
} do {
^bb0(%j: index):
%next = arith.addi %j, %c1 : index
scf.yield %next : index
}
%chosen = scf.index_switch %which -> i32
case 0 {
%ten = arith.constant 10 : i32
scf.yield %ten : i32
}
case 1 {
%twenty = arith.constant 20 : i32
scf.yield %twenty : i32
}
default {
%thirty = arith.constant 30 : i32
scf.yield %thirty : i32
}
%result = scf.execute_region -> i32 {
%unused = arith.index_cast %count : index to i32
scf.yield %chosen : i32
}
return %result : i32
}
}
scf.execute_region is not changing the value here. It shows how a nested single-execution region can return values to its parent.
42.10.3 Parallel Reduction
This example uses scf.parallel, scf.reduce, and scf.reduce.return.
module {
func.func @parallel_count(%n: index) -> f32 {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%zero = arith.constant 0.0 : f32
%one = arith.constant 1.0 : f32
%sum = scf.parallel (%i) = (%c0) to (%n) step (%c1)
init (%zero) -> f32 {
scf.reduce(%one : f32) {
^bb0(%lhs: f32, %rhs: f32):
%next = arith.addf %lhs, %rhs : f32
scf.reduce.return %next : f32
}
}
return %sum : f32
}
}
The reduction combiner receives two values and returns one combined value. For parallel execution, reduction functions should be associative and commutative if you need deterministic results.
42.10.4 Forall Virtual Threads
This example uses scf.forall and scf.forall.in_parallel.
module {
func.func @virtual_threads(%num_threads: index) {
scf.forall (%thread_id) in (%num_threads) {
scf.forall.in_parallel {
}
}
return
}
}
This is a minimal forall with no shared outputs. Real forall loops often use shared_outs and terminator actions such as tensor parallel insert operations to combine per-thread tensor slices.
42.11 How To Use It
For hand-written or generated MLIR, start with the simplest SCF operation that matches the source construct:
- Use
scf.iffor structured conditional values. - Use
scf.forfor counted loops with known lower bound, upper bound, and positive step. - Use
scf.whilewhen the loop condition is not naturally a counted range. - Use
scf.parallelfor unordered parallel iteration with explicit reductions. - Use
scf.forallfor target-independent virtual-thread distribution. - Use
scf.index_switchfor structured dispatch on anindexvalue.
For compiler pipelines, a common path is:
higher-level dialects
-> affine/linalg/vector/tosa transformations
-> scf
-> loop optimizations and normalization
-> cf, gpu, omp, spirv, emitc, or target-specific lowering
For CPU lowering to LLVM, SCF usually does not go directly to LLVM. A typical route is scf to cf, then cf to llvm, with arithmetic, memref, func, and other dialects lowered along the way.
42.12 Gotchas
scf.foruses a half-open range: it includes the lower bound and excludes the upper bound.- The
scf.forstep must be strictly positive. scf.forhas a no-overflow semantic requirement for the final induction value computation. Do not rely on wraparound.- If an SCF op returns values, the region terminator must yield the same number and types of values.
scf.paralleldoes not make side effects ordered. Data races are undefined.scf.reduceorder is unspecified. Use associative and commutative reductions when result stability matters.scf.forallrepresents virtual threads, not a concrete hardware launch by itself. Mapping attributes need a lowering that understands them.- Lowering to
cfloses high-level structure. Run loop transformations beforeconvert-scf-to-cfwhen those transformations need structured loops. lift-cf-to-scfis useful but not magic. Some CFG shapes cannot be fully lifted back into SCF.scf.execute_regionis a bridge construct. If it remains late in a pipeline, check whether a lowering or inlining step is missing.
42.13 What It Implies In A Compiler Pipeline
Seeing SCF in IR usually means the compiler is still in a structured middle-end phase. Analyses can reason about loops and branches through operation interfaces such as LoopLikeOpInterface and RegionBranchOpInterface.
As the pipeline moves toward code generation, SCF must either be normalized to a form the target supports or lowered away:
- Sequential CPU-style code usually lowers through
cf. - C/C++ emission can preserve structure through EmitC.
- OpenMP lowering consumes
scf.parallel. - GPU lowering can consume mapped
scf.parallel. - SPIR-V lowering maps SCF to SPIR-V structured control-flow operations.
42.14 Source Map
mlir/include/mlir/Dialect/SCF/IR/SCFOps.td: Dialect definition and native operation definitions.mlir/lib/Dialect/SCF/IR/SCF.cpp: Parser, printer, verifier, folding, and canonicalization implementation.mlir/include/mlir/Dialect/SCF/Transforms/Passes.td: SCF pass definitions.mlir/include/mlir/Dialect/SCF/Transforms/Transforms.h: Reusable SCF transformation utilities.mlir/include/mlir/Dialect/SCF/Transforms/Patterns.h: SCF pattern population APIs.mlir/include/mlir/Dialect/SCF/TransformOps/SCFTransformOps.td: Transform dialect extension for SCF and loop transformations.mlir/include/mlir/Conversion/Passes.td: SCF-related conversion pass definitions.mlir/include/mlir/Conversion/SCFToControlFlow/SCFToControlFlow.h: SCF to branch-basedcfconversion patterns.mlir/include/mlir/Conversion/ControlFlowToSCF/ControlFlowToSCF.h: CFG to SCF lifting interface and pass declaration.mlir/include/mlir/Conversion/VectorToSCF/VectorToSCF.h: Vector transfer to SCF conversion patterns and options.mlir/include/mlir/Conversion/SCFToGPU/SCFToGPUPass.h: SCF parallel loop to GPU launch conversion pass declarations.mlir/include/mlir/Conversion/SCFToSPIRV/SCFToSPIRVPass.h: SCF to SPIR-V pass declaration.mlir/include/mlir/Conversion/SCFToEmitC/SCFToEmitC.h: SCF to EmitC conversion patterns.mlir/include/mlir/Conversion/SCFToOpenMP/SCFToOpenMP.h: SCF parallel loop to OpenMP conversion patterns.