Skip to content

IR by example

Prerequisite: compiler introduction. This walkthrough starts with one weighted-neighbor computation and follows the same compiler fixture through Domain, Iter, Kernel and Task IR. No GPU is needed to inspect these transformations.

This is a compiler-development exercise, not a dump of the native CPU quickstart. Native Tensor programs can instead use gf_tensor; see execution paths.

1. Start with the computation

The message has the same meaning as this Python definition:

import tiga as tg

class WeightedAggregation(tg.MessagePassing):
    reducer = tg.sum()

    def edge(self, src, dst, edge):
        return src.x * edge.weight

Source values are multiplied by edge weights, then summed per destination. The homepage supplies a complete Python call. Here the input is the checked distributed-plan.mlir fixture: it also describes 1,024 entities, degree bounds 4–16 and partition metadata. That extra placement information makes task planning visible.

The fixture is hand-authored MLIR. The four expandable outputs below are generated from it by the real compiler, not manually invented diagrams or claims that a Python call always produces these stages.

2. Read the notation

MLIR is a textual way to represent typed operations. The fragment below needs only a few conventions:

Notation Meaning
%x, %message A named value; names can change when the compiler prints IR
@sum A reference to a symbol defined elsewhere in the module
f32, i64 A 32-bit float and a 64-bit integer
tensor<?xf32> A one-dimensional float Tensor with a runtime length
^bb0(...) A block with arguments, here the inputs to one edge's computation
arith.mulf Floating-point multiplication
"gf.yield" Return the message from this region, not Python's generator yield
{...} / <{...}> Attribute/property dictionaries; the printer may canonicalize their placement

A region contains operations; the edge function becomes such a region. A pass analyzes or transforms IR.

3. Domain IR: preserve the message

Two edges produce 8 and 15; sum combines them into 23 at one destination.

The numbers illustrate one row; they are not constants in the compiler fixture. The key is one message per edge, reduced per destination. Traversal and thread assignment are not specified yet.

This is the fixture's gf.apply operation, with line wrapping adjusted. The graph relation and @sum definition are in the full module below:

%out = "gf.apply"(%relation, %x, %weight) ({
^bb0(%source: f32, %edge: f32):
  %message = arith.mulf %source, %edge : f32
  "gf.yield"(%message) : (f32) -> ()
}) {reducers = [@sum], region_kinds = array<i64: 0>,
    input_segment_sizes = array<i64: 2>,
    snapshot_versions = array<i64: 3, 3>,
    effects = ["read", "read"], deterministic = false,
    input_roles = ["src", "edge"], input_names = ["x", "weight"]
} : (!gf.relation, tensor<?xf32>, tensor<?xf32>) -> tensor<?xf32>

Read the useful parts first:

  • %relation is the graph; %x and %weight are input arrays.
  • input_roles = ["src", "edge"] binds the first array to source nodes and the second to edges. %source and %edge are scalar block arguments.
  • arith.mulf preserves the Python multiplication.
  • reducers = [@sum] identifies the sum reducer.
  • The version/effect attributes preserve validity and data-access information. They do not yet choose a GPU block size.

The actual dialect prefix here is gf, not gf.domain.

Full compiler output: domain
// Generated by tools/render_ir_docs.py; do not edit.
// Source: tests/mlir/distributed-plan.mlir
module {
  "gf.reducer"() <{associative = true, commutative = true, kind = "algebraic", message_types = [f32], result_types = [f32], state_types = [f32], sym_name = "sum"}> ({
    %cst = arith.constant 0.000000e+00 : f32
    "gf.reducer_yield"(%cst) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32):
    "gf.reducer_yield"(%arg0) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32, %arg1: f32):
    %0 = arith.addf %arg0, %arg1 : f32
    "gf.reducer_yield"(%0) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32):
    "gf.reducer_yield"(%arg0) : (f32) -> ()
  }) : () -> ()
  func.func @distributed(%arg0: tensor<?xi64>, %arg1: tensor<?xi64>, %arg2: tensor<?xf32>, %arg3: tensor<?xf32>) -> tensor<?xf32> {
    %0 = "gf.relation"(%arg0, %arg1) <{degree_max = 16 : i64, degree_min = 4 : i64, halo_depth = -1 : i64, lifecycle = "frozen", mesh_axis = 1 : i64, mesh_shape = array<i64: 2, 4>, num_dst = 1024 : i64, num_src = 1024 : i64, origin = "external", partition_balance = "edges", realization = "materialized", relation_id = "mesh", version = 3 : i64}> : (tensor<?xi64>, tensor<?xi64>) -> !gf.relation
    %1 = "gf.apply"(%0, %arg2, %arg3) <{deterministic = false, effects = ["read", "read"], input_names = ["x", "weight"], input_roles = ["src", "edge"], input_segment_sizes = array<i64: 2>, reducers = [@sum], region_kinds = array<i64: 0>, snapshot_versions = array<i64: 3, 3>}> ({
    ^bb0(%arg4: f32, %arg5: f32):
      %2 = arith.mulf %arg4, %arg5 : f32
      "gf.yield"(%2) : (f32) -> ()
    }) : (!gf.relation, tensor<?xf32>, tensor<?xf32>) -> tensor<?xf32>
    return %1 : tensor<?xf32>
  }
}

4. Iter IR: choose a traversal

The gf-lower-domain-to-iter pass replaces gf.apply with gf_iter.traverse.

Domain specifies the messages and per-destination reduction. Iter specifies how to enumerate the relation coordinates, not GPU threads or explicit loop instructions.

Walk three destination rows

Use a smaller CSR example with the same message formula. These numbers are not the actual data of the 1,024-node compiler fixture. The complete program uses ordinary Torch tensors: python examples/ir_csr_walkthrough.py.

import torch

row_ptr = torch.tensor([0, 2, 3, 3], dtype=torch.int64)
col_idx = torch.tensor([0, 1, 1], dtype=torch.int64)
x = torch.tensor([2., 3.], requires_grad=True)
weight = torch.tensor([4., 5., 2.])

row_ptr has one more element than the destination count. Each position in col_idx identifies an edge; the stored value identifies its source node. There are three destinations, two sources and three edges. Edge IDs are not source IDs.

Destination zero selects edges zero and one; source values two and three times weights four and five sum to twenty-three.

Destination i Edge range [row_ptr[i], row_ptr[i+1]) Sources col_idx[e] Messages and result
0 [0, 2): edges 0, 1 0, 1 2*4 + 3*5 = 23
1 [2, 3): edge 2 1 3*2 = 6
2 [3, 3): empty row None sum identity: 0

This explanatory Python loop shows the meaning; it is neither compiler output nor code that a Tiga application must write:

row_ptr, col_idx = [0, 2, 3, 3], [0, 1, 1]
x, weight = [2., 3.], [4., 5., 2.]
out = []
for i in range(len(row_ptr) - 1):
    accumulator = 0.0  # @sum.identity()
    for e in range(row_ptr[i], row_ptr[i + 1]):
        j = col_idx[e]
        message = x[j] * weight[e]  # edge region
        accumulator += message    # @sum.lift / combine
    out.append(accumulator)        # @sum.finalize
assert out == [23., 6., 0.]

destination-major groups traversal by destination, then visits that row's neighbors. compressed-row locates those neighbors through CSR offsets and column indices. Neither property sorts source IDs, rebuilds topology or assigns GPU blocks.

Complete Torch program

This CPU program outputs [23, 6, 0], with source gradient [4, 7].

Full source: examples/ir_csr_walkthrough.py
"""Read destination-major CSR traversal using ordinary Torch tensors."""

import torch
import tiga as tg


class WeightedAggregation(tg.MessagePassing):
    reducer = tg.sum()

    def edge(self, src, dst, edge):
        return src.x * edge.weight


def main():
    row_ptr = torch.tensor([0, 2, 3, 3], dtype=torch.int64)
    col_idx = torch.tensor([0, 1, 1], dtype=torch.int64)
    x = torch.tensor([2., 3.], requires_grad=True)
    weight = torch.tensor([4., 5., 2.])
    graph = tg.Graph.from_csr(row_ptr, col_idx, num_src=2, validate="full")
    kernel = WeightedAggregation()
    output = kernel(graph=graph, src={"x": x}, dst={}, edge={"weight": weight})
    (gradient,) = torch.autograd.grad(output.sum(), x)
    torch.testing.assert_close(output, torch.tensor([23., 6., 0.]))
    torch.testing.assert_close(gradient, torch.tensor([4., 7.]))
    print("output:", output.tolist())
    print("d(sum(output))/dx:", gradient.tolist())
    return output, gradient


if __name__ == "__main__":
    main()

Read the actual Iter operation

The following excerpt preserves the compiler's SSA names; line wrapping and attribute order are adjusted:

%1 = "gf_iter.traverse"(%0, %arg2, %arg3) <{
  coordinate_hierarchy = "compressed-row", ordering = "destination-major",
  reducers = [@sum], region_kinds = array<i64: 0>,
  input_segment_sizes = array<i64: 2>, snapshot_versions = array<i64: 3, 3>,
  input_roles = ["src", "edge"], input_names = ["x", "weight"],
  effects = ["read", "read"], deterministic = false
}> ({
^bb0(%arg4: f32, %arg5: f32):
  %2 = arith.mulf %arg4, %arg5 : f32
  "gf_iter.yield"(%2) : (f32) -> ()
}) : (!gf.relation, tensor<?xf32>, tensor<?xf32>) -> tensor<?xf32>
IR part Meaning in this example
%0 Result of gf.relation, wrapping topology; not node zero
%arg2 / %arg3 Entire source field x / edge field weight
%arg4 / %arg5 Scalars for the current edge: x[col_idx[e]] / weight[e], not whole rows
arith.mulf → gf_iter.yield Compute and return one message, not the destination's final reduction
reducers = [@sum] Traverse invokes the module's sum algebra; its addition is not inside the edge region
%1 Output array for all destinations, not one edge
region_kinds = [0], input_segment_sizes = [2] One edge region, two field inputs, no additional node region
snapshot_versions = [3,3] / effects Logical input versions and read effects, not iteration counts or array lengths
deterministic = false No fixed reduction order requested; destination-major does not imply bitwise floating-point determinism

There are still no block_rows, warp counts or streams; those belong to Kernel / Task planning. Zero is the empty-row result of this sum algebra, not every reducer.

How other relations traverse

The pass chooses coordinates from the relation kind; not every graph becomes CSR:

Relation coordinate_hierarchy Enumeration
Materialized CSR compressed-row Read existing edges from row ranges
Dense / triangular cartesian-product Source/destination combinations, retaining relation boundary conditions
Generated radius generated-neighborhood Select candidates using neighborhood generation rules
Ranked kNN ranked-pairs Select relation coordinates by ranking

This is a pass classification, not universal provider coverage. Paged relations remain in Domain IR in this pass rather than being forced into compressed-row traversal. See LowerDomainToIter.cpp and the Iter verifier.

Full compiler output: iter
// Generated by tools/render_ir_docs.py; do not edit.
// Source: tests/mlir/distributed-plan.mlir
module {
  "gf.reducer"() <{associative = true, commutative = true, kind = "algebraic", message_types = [f32], result_types = [f32], state_types = [f32], sym_name = "sum"}> ({
    %cst = arith.constant 0.000000e+00 : f32
    "gf.reducer_yield"(%cst) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32):
    "gf.reducer_yield"(%arg0) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32, %arg1: f32):
    %0 = arith.addf %arg0, %arg1 : f32
    "gf.reducer_yield"(%0) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32):
    "gf.reducer_yield"(%arg0) : (f32) -> ()
  }) : () -> ()
  func.func @distributed(%arg0: tensor<?xi64>, %arg1: tensor<?xi64>, %arg2: tensor<?xf32>, %arg3: tensor<?xf32>) -> tensor<?xf32> {
    %0 = "gf.relation"(%arg0, %arg1) <{degree_max = 16 : i64, degree_min = 4 : i64, halo_depth = -1 : i64, lifecycle = "frozen", mesh_axis = 1 : i64, mesh_shape = array<i64: 2, 4>, num_dst = 1024 : i64, num_src = 1024 : i64, origin = "external", partition_balance = "edges", realization = "materialized", relation_id = "mesh", version = 3 : i64}> : (tensor<?xi64>, tensor<?xi64>) -> !gf.relation
    %1 = "gf_iter.traverse"(%0, %arg2, %arg3) <{coordinate_hierarchy = "compressed-row", deterministic = false, effects = ["read", "read"], input_names = ["x", "weight"], input_roles = ["src", "edge"], input_segment_sizes = array<i64: 2>, ordering = "destination-major", reducers = [@sum], region_kinds = array<i64: 0>, snapshot_versions = array<i64: 3, 3>}> ({
    ^bb0(%arg4: f32, %arg5: f32):
      %2 = arith.mulf %arg4, %arg5 : f32
      "gf_iter.yield"(%2) : (f32) -> ()
    }) : (!gf.relation, tensor<?xf32>, tensor<?xf32>) -> tensor<?xf32>
    return %1 : tensor<?xf32>
  }
}

5. Kernel IR: choose an execution schedule

Iter visits neighbors by destination; Kernel groups rows and neighbor slots into a tile.

The left specifies traversal order; the right groups work into a tile. The grid is schematic, not the full 16 × 16 tile or a one-cell-per-thread mapping.

The next passes replace traversal with gf_kernel.launch and select a schedule. This fixture produces these attributes:

traversal = "csr-row"
schedule_kind = "bounded-ragged-row-neighbor"
block_rows = 16 : i64
block_neighbors = 16 : i64
num_warps = 4 : i64
pipeline_stages = 1 : i64

The kernel now carries work grouping and resource contracts. The row/neighbor tile bounds come from the selected plan for this fixture; they are not universal defaults or user-written parameters. Target code still has to be generated.

Full compiler output: kernel
// Generated by tools/render_ir_docs.py; do not edit.
// Source: tests/mlir/distributed-plan.mlir
module {
  "gf.reducer"() <{associative = true, commutative = true, kind = "algebraic", message_types = [f32], result_types = [f32], state_types = [f32], sym_name = "sum"}> ({
    %cst = arith.constant 0.000000e+00 : f32
    "gf.reducer_yield"(%cst) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32):
    "gf.reducer_yield"(%arg0) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32, %arg1: f32):
    %0 = arith.addf %arg0, %arg1 : f32
    "gf.reducer_yield"(%0) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32):
    "gf.reducer_yield"(%arg0) : (f32) -> ()
  }) : () -> ()
  func.func @distributed(%arg0: tensor<?xi64>, %arg1: tensor<?xi64>, %arg2: tensor<?xf32>, %arg3: tensor<?xf32>) -> tensor<?xf32> {
    %0 = "gf.relation"(%arg0, %arg1) <{degree_max = 16 : i64, degree_min = 4 : i64, halo_depth = -1 : i64, lifecycle = "frozen", mesh_axis = 1 : i64, mesh_shape = array<i64: 2, 4>, num_dst = 1024 : i64, num_src = 1024 : i64, origin = "external", partition_balance = "edges", realization = "materialized", relation_id = "mesh", version = 3 : i64}> : (tensor<?xi64>, tensor<?xi64>) -> !gf.relation
    %1 = "gf_kernel.launch"(%arg0, %arg1, %arg2, %arg3) <{block_neighbors = 16 : i64, block_rows = 16 : i64, degree_max = 16 : i64, degree_min = 4 : i64, deterministic = false, effects = ["read", "read"], execution_roles = ["workgroup.destination-rows", "subgroup.neighbor-reduction", "lane.scalar"], input_names = ["x", "weight"], input_roles = ["src", "edge"], input_segment_sizes = array<i64: 2>, instruction_contracts = ["masked-memory", "associative-reduce"], num_rows = 1024 : i64, num_warps = 4 : i64, pipeline_stages = 1 : i64, reducers = [@sum], region_kinds = array<i64: 0>, schedule_handoffs = ["load.global-to-register", "reduce.subgroup", "store.register-to-global"], schedule_kind = "bounded-ragged-row-neighbor", schedule_resources = ["relation.global.read", "input.global.read", "state.register.private", "output.global.write"], snapshot_versions = array<i64: 3, 3>, target_contract = "provider-neutral-v1", traversal = "csr-row"}> ({
    ^bb0(%arg4: f32, %arg5: f32):
      %2 = arith.mulf %arg4, %arg5 : f32
      "gf_kernel.yield"(%2) : (f32) -> ()
    }) : (tensor<?xi64>, tensor<?xi64>, tensor<?xf32>, tensor<?xf32>) -> tensor<?xf32>
    return %1 : tensor<?xf32>
  }
}

6. Task IR: express dependencies

Interior does not wait for halo; boundary waits for unpack event %6; completion events join.

Arrows show dependencies, not duration. Boundary must wait for halo data; interior has no such dependency. This compiler fixture demonstrates dependency representation. The public distributed runtime uses the communication-then-compute sequence.

Because this fixture includes partition metadata, the gf-plan-distributed-tasks pass adds a partition, a halo exchange plan, and task dependencies. These two actual operations show the distinction (line wrapping adjusted):

%7 = "gf_task.launch"(%1) <{
  callee = @distributed, operandSegmentSizes = array<i32: 1, 0>,
  reads = ["x"], snapshot_version = 3 : i64,
  task_kind = "interior", writes = ["out"]
}> : (!gf_task.partition) -> !gf_storage.event

%8 = "gf_task.launch"(%1, %6) <{
  callee = @distributed, operandSegmentSizes = array<i32: 1, 1>,
  reads = ["x"], snapshot_version = 3 : i64,
  task_kind = "boundary", writes = ["out"]
}> : (!gf_task.partition, !gf_storage.event) -> !gf_storage.event

%1 is the partition. %6 is the event produced by halo unpack:

  • Interior work needs only the partition/local data.
  • Boundary work also consumes %6, so it waits for halo data.
  • gf_storage.join joins their completion events.
  • gf_kernel.launch still carries the message computation; task planning does not replace the multiplication with communication.

This is an IR planning test. It does not launch a real multi-GPU job or prove communication overlap. Without this placement requirement, a simple program need not acquire these tasks.

Full compiler output: task
// Generated by tools/render_ir_docs.py; do not edit.
// Source: tests/mlir/distributed-plan.mlir
module {
  "gf.reducer"() <{associative = true, commutative = true, kind = "algebraic", message_types = [f32], result_types = [f32], state_types = [f32], sym_name = "sum"}> ({
    %cst = arith.constant 0.000000e+00 : f32
    "gf.reducer_yield"(%cst) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32):
    "gf.reducer_yield"(%arg0) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32, %arg1: f32):
    %0 = arith.addf %arg0, %arg1 : f32
    "gf.reducer_yield"(%0) : (f32) -> ()
  }, {
  ^bb0(%arg0: f32):
    "gf.reducer_yield"(%arg0) : (f32) -> ()
  }) : () -> ()
  func.func @distributed(%arg0: tensor<?xi64>, %arg1: tensor<?xi64>, %arg2: tensor<?xf32>, %arg3: tensor<?xf32>) -> tensor<?xf32> {
    %0 = "gf.relation"(%arg0, %arg1) <{degree_max = 16 : i64, degree_min = 4 : i64, halo_depth = -1 : i64, lifecycle = "frozen", mesh_axis = 1 : i64, mesh_shape = array<i64: 2, 4>, num_dst = 1024 : i64, num_src = 1024 : i64, origin = "external", partition_balance = "edges", realization = "materialized", relation_id = "mesh", version = 3 : i64}> : (tensor<?xi64>, tensor<?xi64>) -> !gf.relation
    %1 = "gf_task.partition"(%0) <{ghost_entities = -1 : i64, mesh_axis = 1 : i64, mesh_shape = array<i64: 2, 4>, owned_entities = 256 : i64, scheme = "destination", version = 3 : i64}> {ghost_map_algorithm = "csr-owned-row-remote-src-sort-unique", neighbor_discovery = "alltoall-counts-then-request-ids", owner_map_algorithm = "contiguous-destination-prefix"} : (!gf.relation) -> !gf_task.partition
    %2 = "gf_task.halo"(%1) <{depth = -1 : i64, fields = ["x"], strategy = "auto"}> : (!gf_task.partition) -> !gf_task.halo_plan
    %3 = "gf_storage.region"(%arg2) <{elements = 1024 : i64, logical_id = "x", version = 3 : i64}> : (tensor<?xf32>) -> !gf_storage.region
    %4 = "gf_storage.instance"(%3) <{capacity_bytes = 1024 : i64, device = "local", external = true, layout = "partitioned", memory_space = "device"}> : (!gf_storage.region) -> !gf_storage.instance
    %5 = "gf_storage.instance"(%3) <{capacity_bytes = 3072 : i64, device = "ghost", external = true, layout = "partitioned", memory_space = "device"}> : (!gf_storage.region) -> !gf_storage.instance
    %send, %done = "gf_task.halo_pack"(%2, %4) <{bytes = 3072 : i64, field = "x", snapshot_version = 3 : i64}> : (!gf_task.halo_plan, !gf_storage.instance) -> (!gf_task.halo_buffer, !gf_storage.event)
    %receive, %done_0 = "gf_task.halo_exchange"(%2, %send, %done) <{bytes = 3072 : i64, group = "mesh", snapshot_version = 3 : i64}> : (!gf_task.halo_plan, !gf_task.halo_buffer, !gf_storage.event) -> (!gf_task.halo_buffer, !gf_storage.event)
    %6 = "gf_task.halo_unpack"(%2, %receive, %5, %done_0) <{field = "x", snapshot_version = 3 : i64}> : (!gf_task.halo_plan, !gf_task.halo_buffer, !gf_storage.instance, !gf_storage.event) -> !gf_storage.event
    %7 = "gf_task.launch"(%1) <{callee = @distributed, operandSegmentSizes = array<i32: 1, 0>, reads = ["x"], snapshot_version = 3 : i64, task_kind = "interior", writes = ["out"]}> : (!gf_task.partition) -> !gf_storage.event
    %8 = "gf_task.launch"(%1, %6) <{callee = @distributed, operandSegmentSizes = array<i32: 1, 1>, reads = ["x"], snapshot_version = 3 : i64, task_kind = "boundary", writes = ["out"]}> : (!gf_task.partition, !gf_storage.event) -> !gf_storage.event
    %9 = "gf_storage.join"(%7, %8) <{snapshot_version = 3 : i64}> : (!gf_storage.event, !gf_storage.event) -> !gf_storage.event
    %10 = "gf_kernel.launch"(%arg0, %arg1, %arg2, %arg3) <{block_neighbors = 16 : i64, block_rows = 16 : i64, degree_max = 16 : i64, degree_min = 4 : i64, deterministic = false, effects = ["read", "read"], execution_roles = ["workgroup.destination-rows", "subgroup.neighbor-reduction", "lane.scalar"], input_names = ["x", "weight"], input_roles = ["src", "edge"], input_segment_sizes = array<i64: 2>, instruction_contracts = ["masked-memory", "associative-reduce"], num_rows = 1024 : i64, num_warps = 4 : i64, pipeline_stages = 1 : i64, reducers = [@sum], region_kinds = array<i64: 0>, schedule_handoffs = ["load.global-to-register", "reduce.subgroup", "store.register-to-global"], schedule_kind = "bounded-ragged-row-neighbor", schedule_resources = ["relation.global.read", "input.global.read", "state.register.private", "output.global.write"], snapshot_versions = array<i64: 3, 3>, target_contract = "provider-neutral-v1", traversal = "csr-row"}> ({
    ^bb0(%arg4: f32, %arg5: f32):
      %11 = arith.mulf %arg4, %arg5 : f32
      "gf_kernel.yield"(%11) : (f32) -> ()
    }) {gf_task.planned} : (tensor<?xi64>, tensor<?xi64>, tensor<?xf32>, tensor<?xf32>) -> tensor<?xf32>
    return %10 : tensor<?xf32>
  }
}

7. Reproduce the outputs

First complete the compiler test build. From the repository root:

export TIGA_OPT="$PWD/build/compiler/bin/gf-opt"

"$TIGA_OPT" tests/mlir/distributed-plan.mlir

"$TIGA_OPT" tests/mlir/distributed-plan.mlir \
  -gf-lower-domain-to-iter

"$TIGA_OPT" tests/mlir/distributed-plan.mlir \
  -gf-lower-domain-to-iter -gf-lower-iter-to-kernel \
  -gf-select-kernel-schedule

"$TIGA_OPT" tests/mlir/distributed-plan.mlir \
  -gf-lower-domain-to-iter -gf-lower-iter-to-kernel \
  -gf-select-kernel-schedule -gf-plan-distributed-tasks

The documentation snapshots are checked against compiler output:

python tools/render_ir_docs.py --gf-opt "$TIGA_OPT" --check

After an intentional compiler change, run the same command without --check to regenerate the snapshots, review the changes, then rebuild the docs. The source test also checks the halo/task structure.

Next

Build and contribute maps these layers to implementation files. Memory and distributed execution explains ownership, storage instances and events in more depth.