Skip to main content

Transformations

A transformation rewrites one specific place in a Structured SDFG, for example one loop. Unlike a pass, it does not search the SDFG itself: you, a pass, or a scheduler choose where to apply it. Every transformation first checks whether it is legal there, so applying it never changes what the program computes.

The Transformation interface​

MethodDescription
can_be_applied(builder, analysis_manager)Checks the preconditions, using analyses, without changing the SDFG
apply(builder, analysis_manager)Performs the rewrite
try_apply(builder, analysis_manager)can_be_applied followed by apply; returns whether it was applied
to_json(j), from_json(builder, j)Serialize the transformation and where it was applied, for record and replay

A transformation is constructed with the node it applies to, for example LoopTiling(loop, 256).

Example: tile and parallelize WAXPBY​

Using build_waxpby() from Structured SDFGs, this script tiles the loop, parallelizes the tiles, runs them with OpenMP, and compiles the result:

import tempfile
import numpy as np
from waxpby_sdfg import build_waxpby
from docc.sdfg import AnalysisManager, LoopTiling, LoopParallelization, OMPTransform
from docc.compiler.compiled_sdfg import CompiledSDFG

builder = build_waxpby()

def apply(transformation):
analysis_manager = AnalysisManager(builder)
assert transformation.can_be_applied(builder, analysis_manager)
transformation.apply(builder, analysis_manager)

def find_loop(indvar):
return AnalysisManager(builder).loop_analysis().find_loop_by_indvar(indvar)

apply(LoopTiling(find_loop("i"), 256)) # chunks of 256 iterations
apply(LoopParallelization(find_loop("i_tile0"))) # outer For -> Map
apply(OMPTransform(find_loop("i_tile0"))) # run the Map with OpenMP

sdfg = builder.move()
compiled = CompiledSDFG(sdfg._compile(tempfile.mkdtemp(), "openmp"), sdfg)

N = 100_000
x, y, w = np.ones(N, np.float32), np.ones(N, np.float32), np.zeros(N, np.float32)
compiled(N, np.float32(2.0), x, np.float32(3.0), y, w)
assert np.allclose(w, 5.0)

The generated loop nest:

#pragma omp parallel for schedule(static) private(t2, t1, i)
for(i_tile0 = 0;i_tile0 < N;i_tile0 = 256 + i_tile0)
for(i = i_tile0;(i < N && i < 256 + i_tile0);i = 1 + i)
...

Each step builds on the previous one. OMPTransform only accepts a Map, so the loop must be parallelized first. LoopParallelization in turn only applies if the iterations are independent. For a prefix sum (w[i] = w[i - 1] + w[i]), can_be_applied returns False, because each iteration reads what the previous one wrote.

Create a new AnalysisManager after each change, or reuse one and call invalidate_all(), because transformations change the SDFG that the analyses describe. After builder.move(), the builder is empty; do not use it again.

Available transformations​

GroupTransformationsLocation
Loop structureLoopTiling, MultiLevelTiling, LoopInterchange, LoopSkewing, LoopDistribute, LoopSplit, LoopPeeling, LoopShift, LoopRotate, LoopUnitStride, TrivialLoopElimination, UnrollTransformopt/include/sdfg/transformations/
MapsMapCollapse, MapFusion, MapDelinearizeopt/include/sdfg/transformations/
ParallelizationLoopParallelization (For → Map), OMPTransform (OpenMP schedule), StreamKopt/include/sdfg/parallelization/, opt/include/sdfg/transformations/
VectorizationVectorizeTransform, TileVectorizeropt/include/sdfg/transformations/, opt/include/sdfg/tiles/
TilesTileFusion, LocalStorage, SoftwarePipeliningopt/include/sdfg/tiles/transformations/
GPUCUDATransform, CUDAOffloadTransform, ROCMOffloadTransform, GpuMmaTransformopt/include/sdfg/transformations/offloading/, opt/include/sdfg/targets/

All of them except MultiLevelTiling, LoopSplit, LoopShift, LoopRotate, LoopUnitStride, and MapDelinearize are available in Python under the same names in docc.sdfg. The target schedulers are built from these transformations: the OpenMP scheduler, for example, applies OMPTransform.

Record and replay​

A Recorder applies transformations and records each one, with its parameters and the loop it was applied to, as JSON. A Replayer applies a recorded sequence to another SDFG, for example the same function in a later run:

import json
from docc.sdfg import Recorder, Replayer

builder = build_waxpby() # a fresh SDFG; the previous builder was moved
recorder = Recorder()
recorder.apply(LoopTiling(find_loop("i"), 256), builder, AnalysisManager(builder))
# ... further transformations

history = recorder.history # JSON string
print([step["transformation_type"] for step in json.loads(history)])

other = build_waxpby()
Replayer().apply(other, AnalysisManager(other), history)

Recording is how optimizations are exchanged with the Daisytuner service: transfer tuning looks up recorded transformation sequences for similar loops and replays them.

Write a transformation​

A transformation derives from transformations::Transformation:

#include "sdfg/transformations/transformation.h"

class MyTransformation : public sdfg::transformations::Transformation {
sdfg::structured_control_flow::StructuredLoop& loop_;

public:
explicit MyTransformation(sdfg::structured_control_flow::StructuredLoop& loop) : loop_(loop) {}

std::string name() const override { return "MyTransformation"; }

bool can_be_applied(sdfg::builder::StructuredSDFGBuilder& builder,
sdfg::analysis::AnalysisManager& analysis_manager) override {
// Check preconditions with analyses; do not modify the SDFG here.
return true;
}

void apply(sdfg::builder::StructuredSDFGBuilder& builder,
sdfg::analysis::AnalysisManager& analysis_manager) override {
// Rewrite the SDFG through the builder.
}

void to_json(nlohmann::json& j) const override {
j["transformation_type"] = name();
j["subgraph"]["0"]["element_id"] = loop_.element_id();
}
};

To make it replayable, add a static from_json(builder, j) that finds the node with builder.find_element_by_id(...) and returns the transformation, and register the type in dispatch_transformation in opt/include/sdfg/transformations/transformation_registry.h. Registration checks at compile time that from_json and to_json have the required form. LoopInterchange shows the complete pattern.