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¶
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:
%relationis the graph;%xand%weightare input arrays.input_roles = ["src", "edge"]binds the first array to source nodes and the second to edges.%sourceand%edgeare scalar block arguments.arith.mulfpreserves 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 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¶
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¶
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.joinjoins their completion events.gf_kernel.launchstill 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:
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.