Skip to main content

Structured SDFGs

docc represents programs as stateful dataflow multigraphs (SDFGs), a data-centric intermediate representation introduced by DaCe. An SDFG makes data movement explicit: every read and write of memory is an edge in a graph. This is what makes data dependencies, parallelism, and transfers between host and device easy to analyze.

docc works on Structured SDFGs, in which control flow is a tree of loops, branches, and sequences instead of an arbitrary graph. A Structured SDFG consists of three parts:

  1. Data: named containers and symbols,
  2. Control flow: a tree of control flow nodes,
  3. Dataflow: a small graph inside each leaf of the tree (a block) that computes something.

SDFG and Structured SDFG​

The original SDFG describes control flow as a state machine: states, each with a dataflow graph, connected by edges with conditions and symbol assignments (sdfg/include/sdfg/control_flow/). This can represent any control flow, including goto, which is why the C/C++ frontend lifts LLVM IR into this form first. It then structurizes the state machine into a Structured SDFG.

The Python and PyTorch frontends build Structured SDFGs directly. All analyses, passes, transformations, and the code generator work on Structured SDFGs.

Data​

Containers are named pieces of data. A container is either an argument of the function or transient, that is, local to the SDFG. Its type is one of:

TypeExampleDescription
ScalarScalar(PrimitiveType.Float)A single value of a primitive type: Float, Double, Half, Int64, Bool, …
PointerPointer(Scalar(...))A pointer to memory, for example a C array or a NumPy buffer
ArrayArray(Scalar(...), "N")A fixed-size array, possibly with a symbolic size
TensorA multi-dimensional array with shape and strides
StructureA C struct with named members

Symbols are integer scalar containers that appear in index expressions, loop bounds, and conditions, for example N and i. docc keeps expressions over symbols in symbolic form (using SymEngine), so it can reason about them. For example, it can prove that x[i] and x[i + N] never overlap if 0 <= i < N.

Control flow​

The control flow of a Structured SDFG is a tree. Its root is a Sequence, and its leaves are blocks that compute.

NodeDescription
SequenceExecutes its children in order
BlockA leaf that contains a dataflow graph
ForA loop with an induction variable, initial value, condition, and update. Iterations run in order.
MapA loop whose iterations are independent. It carries a schedule type that says how the iterations run in parallel, for example omp (OpenMP) or cuda_offload.
ReduceA loop that combines values across iterations with an associative and commutative operator such as + or max, which makes it parallelizable
WhileA loop with an arbitrary exit condition, ended by Break
IfElseOne or more cases, each a condition and a Sequence
Break, Continue, ReturnLeave a loop or the function

Frontends create For loops. Turning a For into a Map is the core of parallelization: it is only legal if the iterations are independent, which analyses determine and transformations check.

Dataflow​

Inside each Block, a dataflow graph describes the computation. Its nodes are connected by edges called memlets:

ElementDescription
AccessNodeReads or writes a container
ConstantNodeA literal value with a type, for example 2.0 as Float. Constants are only read, so they have no incoming edges.
TaskletA single operation, for example fp_add, fp_mul, fp_fma, or a comparison, with named inputs and outputs (connectors) such as _in1 and _out
LibraryNodeA higher-level operation, for example matrix multiplication (gemm), dot, einsum, malloc, memcpy, or a neural-network layer. Passes later expand library nodes into loops, or targets map them to optimized libraries.
MemletAn edge that moves data between an access node and a tasklet or library node. Its subset says which element is accessed, for example i for x[i].

Because every memory access is a memlet with a symbolic subset, docc knows exactly which elements each block reads and writes.

Literals do not need a container. A constant node feeds a value directly into a tasklet:

two = b.add_constant(block, "2.0", F) # ConstantNode
b.add_memlet(block, two, "", mul, "_in1", type=F)

Example: WAXPBY​

The following script builds WAXPBY (w[i] = alpha * x[i] + beta * y[i]) by hand. The frontends use the same builder.

waxpby_sdfg.py
from docc.sdfg import StructuredSDFGBuilder, Scalar, Pointer, PrimitiveType, TaskletCode

F = Scalar(PrimitiveType.Float)
I = Scalar(PrimitiveType.Int64)
vec = Pointer(F)


def build_waxpby():
b = StructuredSDFGBuilder("waxpby")

# Data: arguments, the loop symbol, and two temporaries
for name, ty in [("N", I), ("alpha", F), ("x", vec), ("beta", F), ("y", vec), ("w", vec)]:
b.add_container(name, ty, is_argument=True)
b.add_container("i", I)
b.add_container("t1", F)
b.add_container("t2", F)

# Control flow: for (i = 0; i < N; i = i + 1)
b.begin_for("i", "0", "N", "1")

# Dataflow: one block with three tasklets
block = b.add_block()
mul1 = b.add_tasklet(block, TaskletCode.fp_mul, ["_in1", "_in2"], ["_out"])
mul2 = b.add_tasklet(block, TaskletCode.fp_mul, ["_in1", "_in2"], ["_out"])
add = b.add_tasklet(block, TaskletCode.fp_add, ["_in1", "_in2"], ["_out"])
t1, t2 = b.add_access(block, "t1"), b.add_access(block, "t2")

b.add_memlet(block, b.add_access(block, "alpha"), "", mul1, "_in1", type=F)
b.add_memlet(block, b.add_access(block, "x"), "", mul1, "_in2", subset="i", type=vec)
b.add_memlet(block, mul1, "_out", t1, "", type=F)
b.add_memlet(block, b.add_access(block, "beta"), "", mul2, "_in1", type=F)
b.add_memlet(block, b.add_access(block, "y"), "", mul2, "_in2", subset="i", type=vec)
b.add_memlet(block, mul2, "_out", t2, "", type=F)
b.add_memlet(block, t1, "", add, "_in1", type=F)
b.add_memlet(block, t2, "", add, "_in2", type=F)
b.add_memlet(block, add, "_out", b.add_access(block, "w"), "", subset="i", type=vec)

b.end_for()
return b


if __name__ == "__main__":
sdfg = build_waxpby().move()
sdfg.validate()
print(sdfg.to_cpp())

sdfg.to_dot() renders the result with Graphviz:

The WAXPBY SDFG: a for loop containing one block with the dataflow graph

The outer frame is the For loop (#1), and the inner frame is the block (#3) with its dataflow graph. Ellipses are access nodes, dashed for transient containers such as t1 and t2. Octagons are tasklets. Edges are memlets, labeled with the element they move, for example _in2 = x[i].

To render an SDFG yourself:

python -c "from waxpby_sdfg import build_waxpby; print(build_waxpby().move().to_dot())" > waxpby.dot
dot -Tsvg waxpby.dot -o waxpby.svg

Inspect an SDFG​

MethodResult
sdfg.to_cpp()The C++ code that docc generates for the SDFG
sdfg.to_dot()The SDFG as a Graphviz graph
sdfg.to_json()The SDFG as JSON, the same format as the dumps described in Debug Options
sdfg.validate()Checks that the SDFG is well-formed and raises an error if not
sdfg.rootThe root Sequence, to walk the control flow tree

Continue with Analyses to see what docc can find out about this loop.