Pass, PassContext, PassPipeline, and PassManager¶
Framework for organizing and executing IR transformation passes on Programs with property tracking, instrumentation, and strategy-based optimization pipelines.
Overview¶
| Component | Description |
|---|---|
| Pass (C++) | Standalone class for Program → Program transformations with property declarations |
| IRProperty / IRPropertySet | Enum + bitset for verifiable IR properties (SSAForm, HasMemRefs, etc.) |
| PassInstrument / PassContext | Instrument callbacks (before/after pass) with thread-local context stack |
| PassPipeline (C++) | Ordered sequence of passes executed in order |
| PassManager (Python) | High-level manager using PassPipeline, with strategy-based optimization |
Key Features¶
- Property Tracking: Passes declare required, produced, and invalidated properties
- Instrumentation: PassContext holds PassInstruments that run before/after each pass
- Runtime Verification: VerificationInstrument checks properties against actual IR
- Strategy-based Pipelines: Pre-configured optimization levels (
Default,DebugTileOptimization) - Immutable Transformations: Return new IR nodes, don't modify in place
IRProperty System¶
IRProperty Enum¶
Header: include/pypto/ir/transforms/ir_property.h
| Property | Description |
|---|---|
SSAForm |
IR is in SSA form |
TypeChecked |
IR has passed type checking |
NoNestedCalls |
No nested call expressions |
NormalizedStmtStructure |
Statement structure normalized |
NoRedundantBlocks |
No single-child or nested SeqStmts |
SplitIncoreOrch |
InCore scopes outlined into separate functions |
ClusterOutlined |
Cluster scopes outlined into Group functions |
HasMemRefs |
MemRef objects initialized on variables |
IncoreTileOps |
InCore functions use tile ops |
MixedKernelExpanded |
Mixed InCore functions split into AIC + AIV + Group |
AllocatedMemoryAddr |
All MemRefs have valid addresses within buffer limits |
TileTypeCoherence |
Every TileType has canonical tile_view (implicit views stored as nullopt) |
OrchestrationReferencesResolved |
Every non-builtin Call inside a FunctionType::Orchestration function targets a Function in the surrounding Program |
IRPropertySet¶
Efficient bitset-backed set with Insert, Remove, Contains, ContainsAll, Union, Difference, ToString.
PassProperties¶
struct PassProperties {
IRPropertySet required; // Preconditions
IRPropertySet produced; // New properties guaranteed after running
IRPropertySet invalidated; // Properties this pass breaks
};
Per-Pass Property Declarations¶
| Pass | Required | Produced | Invalidated |
|---|---|---|---|
| InlineFunctions | — | InlineFunctionsEliminated | — |
| UnrollLoops | TypeChecked | TypeChecked | — |
| CtrlFlowTransform | TypeChecked | TypeChecked, StructuredCtrlFlow | — |
| ConvertToSSA | TypeChecked | TypeChecked, SSAForm | NormalizedStmtStructure |
| FlattenCallExpr | SSAForm | SSAForm, NoNestedCalls | NormalizedStmtStructure |
| NormalizeStmtStructure | TypeChecked | TypeChecked, NormalizedStmtStructure | — |
| OutlineIncoreScopes | TypeChecked, SSAForm | SplitIncoreOrch | — |
| OutlineClusterScopes | TypeChecked, SSAForm | ClusterOutlined | — |
| ConvertTensorToTileOps | SplitIncoreOrch | IncoreTileOps | — |
| LowerCompositeOps | — | — | — |
| FlattenTileNdTo2D | SSAForm, IncoreTileOps | SSAForm, TileOps2D | — |
| LegalizeTileCast | — | — | — |
| AutoTileMatmulL0 | SSAForm, IncoreTileOps, TileOps2D | SSAForm, IncoreTileOps, TileOps2D | — |
| CanonicalizeTileSlice | SSAForm, SplitIncoreOrch, IncoreTileOps, TileOps2D, NormalizedStmtStructure | SSAForm, SplitIncoreOrch, IncoreTileOps, TileOps2D, NormalizedStmtStructure | — |
| ResolveBackendOpLayouts | SSAForm, IncoreTileOps, SplitIncoreOrch, TileOps2D | SSAForm, IncoreTileOps, SplitIncoreOrch, TileOps2D, NormalizedStmtStructure | — |
| LowerAutoVectorSplit | SSAForm, IncoreTileOps, SplitIncoreOrch, TileOps2D, TileMemoryInferred, NormalizedStmtStructure | SSAForm, IncoreTileOps, SplitIncoreOrch, TileOps2D, TileMemoryInferred, NormalizedStmtStructure | — |
| ExpandMixedKernel | SSAForm, IncoreTileOps, SplitIncoreOrch, TileOps2D | SSAForm, MixedKernelExpanded | — |
| NormalizeReturnOrder | SplitIncoreOrch, IncoreTileOps | — | — |
| InitMemRef | TypeChecked, SSAForm, SplitIncoreOrch, IncoreTileOps, TileOps2D | HasMemRefs | SSAForm |
| MaterializeSemanticAliases | SplitIncoreOrch, IncoreTileOps, HasMemRefs, TileOps2D | — | — |
| MemoryReuse | TypeChecked, SplitIncoreOrch, IncoreTileOps, HasMemRefs, TileOps2D | — | — |
| AllocateMemoryAddr | TypeChecked, SplitIncoreOrch, IncoreTileOps, HasMemRefs, TileOps2D | AllocatedMemoryAddr | — |
| FoldNoOpReshape | SplitIncoreOrch, IncoreTileOps, HasMemRefs, TileOps2D | — | — |
| FuseCreateAssembleToSlice | — | — | — |
| DeriveCallDirections | SplitIncoreOrch | CallDirectionsResolved | — |
| AutoDeriveTaskDependencies | SplitIncoreOrch, CallDirectionsResolved | CallDirectionsResolved | — |
| ExpandManualPhaseFence | NoNestedCalls, NormalizedStmtStructure, CallDirectionsResolved | NoNestedCalls, NormalizedStmtStructure, CallDirectionsResolved | — |
| SynthesizeAllReduceSignals | — | — | — |
| MaterializeCommDomainScopes | — | CommDomainScopesMaterialized | — |
| LowerHostTensorCollectives | CommDomainScopesMaterialized | CommDomainScopesMaterialized | — |
| MaterializeDistTensorCtx | CommDomainScopesMaterialized | CommDomainScopesMaterialized | — |
| Simplify | — | — | — |
| MaterializeRuntimeScopes | SplitIncoreOrch, CallDirectionsResolved | RuntimeScopesMaterialized | — |
| ClassifyIterArgCarry | CallDirectionsResolved, RuntimeScopesMaterialized | IterArgCarryClassified, RuntimeScopesMaterialized | — |
Note: VerifySSA and TypeCheck are PropertyVerifiers (verification rules), not Passes. They run via
VerificationInstrumentor therun_verifier()utility — see Verifier.
C++ Pass Infrastructure¶
Pass Class¶
class Pass {
ProgramPtr operator()(const ProgramPtr& program) const; // checks PassContext
std::string GetName() const;
IRPropertySet GetRequiredProperties() const;
IRPropertySet GetProducedProperties() const;
IRPropertySet GetInvalidatedProperties() const;
};
Pass::operator() checks PassContext::Current() and runs instruments before/after the actual transform.
Creating Passes with Properties¶
namespace pass {
Pass YourPass() {
return CreateFunctionPass(TransformFunc, "YourPass",
{.required = {IRProperty::SSAForm},
.produced = {IRProperty::SomeProperty},
.invalidated = {IRProperty::AnotherProperty}});
}
}
PassContext and Instruments¶
Header: include/pypto/ir/transforms/pass_context.h
PassInstrument¶
Abstract base class for pass instrumentation callbacks:
class PassInstrument {
virtual void RunBeforePass(const Pass& pass, const ProgramPtr& program) = 0;
virtual void RunAfterPass(const Pass& pass, const ProgramPtr& program) = 0;
virtual std::string GetName() const = 0;
};
VerificationInstrument¶
Concrete instrument that uses PropertyVerifierRegistry to verify properties:
class VerificationInstrument : public PassInstrument {
explicit VerificationInstrument(VerificationMode mode);
// BEFORE: verify required properties before pass
// AFTER: verify produced properties after pass
// BEFORE_AND_AFTER: both
};
CallbackInstrument¶
Lightweight instrument that invokes user-provided callbacks, useful for ad-hoc instrumentation (IR dumping, logging, profiling) without subclassing PassInstrument:
class CallbackInstrument : public PassInstrument {
using Callback = std::function<void(const Pass&, const ProgramPtr&)>;
explicit CallbackInstrument(Callback before_pass = nullptr,
Callback after_pass = nullptr,
std::string name = "CallbackInstrument");
};
# Python: dump IR after each pass
def after_pass(p, program):
print(f"After {p.get_name()}")
with passes.PassContext([passes.CallbackInstrument(after_pass=after_pass)]):
pipeline.run(program)
run_passes(dump_ir=True) uses CallbackInstrument internally to dump IR after each pass, delegating verification to the C++ pipeline. When invoked inside an existing PassContext, dump mode preserves the outer context's instruments (e.g., user-provided VerificationInstrument) and verification level, appending the dump instrument to the combined list.
Dump verbosity (PassDumpLevel). The dump_passes knob (on ir.compile, RunConfig, and run_passes' dump_ir) accepts a PassDumpLevel enum — or a bool for backwards compatibility (True → CONCISE, False → NONE):
| Level | Meaning |
|---|---|
NONE |
No per-pass dumps. |
CONCISE |
Concise canonical IR (the default); best for diffing passes. |
EXPLICIT |
Fully-resolved dump — self-describing for layouts (issue #2088). |
By default (CONCISE) a dumped pl.Tile annotation omits its blayout/slayout/fractal whenever they equal the memory-space implicit view, and canonical IR stores an implicit view as nullopt — so a tile can print with no TileView at all even though its real layout is non-trivial (e.g. a pl.Mem.Acc tile is really blayout=col_major, slayout=row_major, fractal=1024). EXPLICIT makes every dumped tile print its fully-resolved layout from GetEffectiveTileView, and surfaces the window_buffer back-reference that a pld.DistributedTensor carries but the concise form drops — so a layout/aliasing bug is decidable from the printed IR alone. EXPLICIT dumps still reparse to identical IR: the tile layout canonicalizes back to nullopt (an explicit view matching the implicit one), and the window-buffer marker is an informational trailing string the parser strips on reload (the real reference re-derives from pld.tensor.window). This keeps compiled.validate_ir() — which reloads every dump — working. Programmatically, pass explicit_layout=True to python_print(...).
from pypto.ir import PassDumpLevel
from pypto.runtime import RunConfig
RunConfig(dump_passes=PassDumpLevel.EXPLICIT) # fully-resolved dumps
RunConfig(dump_passes=True) # == PassDumpLevel.CONCISE
ReportInstrument¶
Carries the directory that on-disk pipeline artifacts are written to. It observes no pass itself — DiagnosticInstrument reads its output_dir to decide where to append perf_hints.log:
class ReportInstrument : public PassInstrument {
explicit ReportInstrument(std::string output_dir);
const std::string& GetOutputDir() const;
};
instrument = passes.ReportInstrument("/path/to/report")
with passes.PassContext([instrument]):
pipeline.run(program)
compile() creates one pointing at build_output/<name>/report/.
Memory usage is no longer reported here. It is rendered from a pass dump by python -m pypto.tools.memory_map — see Memory Map.
RoundtripInstrument¶
Print→parse roundtrip verification instrument. After every pass, it:
- Prints the resulting IR to Python DSL text via
python_print() - Parses the text back to an IR
Programviaparse() - Asserts
structural_equal(original, reparsed)— a failure means the printer or parser cannot faithfully represent the IR produced by that pass
from pypto.pypto_core import passes
from pypto.ir.instruments import make_roundtrip_instrument
with passes.PassContext([make_roundtrip_instrument()]):
result = passes.convert_to_ssa()(program)
Known non-fatal cases (instrument skips the check without failing):
| Case | Behaviour | Reason |
|---|---|---|
Printer InternalError (e.g. ForKind::Unroll + SSA iter_args) |
UserWarning, roundtrip skipped |
No valid DSL syntax for this transitional state |
UnknownType in original IR (manually built via ir.Call(ir.Op(...))) |
Silent skip | Parsing infers a concrete type; this is a type improvement, not a bug |
tensor.add(x, scalar) → tensor.adds after roundtrip |
Silent skip | Python API dispatches scalar RHS to tensor.adds; manual construction used wrong op name |
tile.load 3-arg → 4-arg after roundtrip |
Silent skip | C++ requires 4 args; manually constructed IR with 3 args is normalised by the printer |
| Variable pointer mismatch (dynamic-shape Vars in return types) | Silent skip | structural_equal without enable_auto_mapping cannot track Vars outside the function body |
Enabled by default in unit tests via tests/ut/conftest.py (see Test Fixture below). Disable with PYPTO_VERIFY_LEVEL=basic or PYPTO_VERIFY_LEVEL=none.
PassContext¶
Thread-local context stack with with-style nesting. Holds both instruments and pass configuration (e.g., verification level):
class PassContext {
explicit PassContext(std::vector<PassInstrumentPtr> instruments,
VerificationLevel verification_level = VerificationLevel::Basic);
void EnterContext(); // push onto thread-local stack
void ExitContext(); // pop from stack
VerificationLevel GetVerificationLevel() const;
static PassContext* Current(); // get active context
};
All pass-related configuration belongs in PassContext — see .claude/rules/pass-context-config.md.
Python Usage¶
from pypto.pypto_core import passes
# Enable verification for a block of code
with passes.PassContext([passes.VerificationInstrument(passes.VerificationMode.AFTER)]):
result = passes.convert_to_ssa()(program) # instruments fire automatically
# Disable automatic verification for a block
with passes.PassContext([], passes.VerificationLevel.NONE):
result = pipeline.run(program) # no automatic verification
# Nesting: inner context overrides outer
with passes.PassContext([passes.VerificationInstrument(passes.VerificationMode.AFTER)]):
with passes.PassContext([]): # disable instruments for this block
result = some_pass(program) # no verification
Test Fixture¶
All unit tests automatically run with property verification and roundtrip verification via tests/ut/conftest.py. Roundtrip is the default for tests so that printer/parser asymmetries are caught automatically.
@pytest.fixture(autouse=True)
def pass_verification_context():
level_str = os.environ.get("PYPTO_VERIFY_LEVEL", "roundtrip").lower()
instruments = []
if level_str != "none":
instruments.append(passes.VerificationInstrument(passes.VerificationMode.BEFORE_AND_AFTER))
if level_str == "roundtrip":
from pypto.ir.instruments import make_roundtrip_instrument
instruments.append(make_roundtrip_instrument())
with passes.PassContext(instruments):
yield
Override via environment variable:
PYPTO_VERIFY_LEVEL |
Property verification | Roundtrip |
|---|---|---|
roundtrip (default for tests) |
✅ BEFORE_AND_AFTER | ✅ |
basic |
✅ BEFORE_AND_AFTER | ❌ |
none |
❌ | ❌ |
PassPipeline (C++)¶
class PassPipeline {
void AddPass(Pass pass);
ProgramPtr Run(const ProgramPtr& program) const; // executes passes in order
std::vector<std::string> GetPassNames() const;
std::vector<Pass> GetPasses() const;
};
PassPipeline is the single source of truth for ordered pass objects and their names. GetPasses() returns lightweight copies of the pass handles for inspection or composing another pipeline. Each pass's operator() checks the active PassContext for instruments.
Automatic Verification¶
When VerificationLevel is Basic (the default), the pipeline automatically verifies a small set of lightweight properties exactly once each. This catches common IR errors without requiring manual PassContext setup.
Verified properties: {SSAForm, TypeChecked, AllocatedMemoryAddr}
How it works:
- After each pass, check if it produced any verified properties not yet checked
- Verify those properties using
PropertyVerifierRegistry - Throw
VerificationErroron errors - Track verified properties to avoid re-checking
With the Default strategy:
| After Pass | Properties Verified | Cumulative |
|---|---|---|
| ConvertToSSA | SSAForm, TypeChecked | 2 |
| FlattenCallExpr | (TypeChecked already verified — skipped) | 2 |
| AllocateMemoryAddr | AllocatedMemoryAddr | 3 |
Total: 3 property checks (each property verified exactly once).
Control via PassContext:
from pypto import ir
from pypto.pypto_core import passes
# Disable automatic verification via PassContext
with passes.PassContext([], passes.VerificationLevel.NONE):
pipeline.run(program)
# Or per-compilation
ir.compile(program, verification_level=ir.VerificationLevel.NONE)
# Environment variable (default when no PassContext): PYPTO_VERIFY_LEVEL=none|basic|roundtrip
How the level is determined:
- If
PassContextis active → use itsverification_level(default: Basic) - If no
PassContext→ useGetDefaultVerificationLevel()(readsPYPTO_VERIFY_LEVELenv var, default: Basic)
Python PassManager¶
File: python/pypto/ir/pass_manager.py
API¶
| Method | Description |
|---|---|
get_strategy(strategy) |
Get PassManager configured for strategy |
run_passes(program, dump_ir, output_dir, prefix) |
Execute passes via PassPipeline |
get_pass_names() |
Get names of all passes |
passes / pass_names |
Read-only snapshots derived from the underlying PassPipeline |
Usage¶
from pypto import ir
from pypto.pypto_core import passes
# Default usage
pm = ir.PassManager.get_strategy(ir.OptimizationStrategy.Default)
result = pm.run_passes(program)
# With verification via PassContext
with passes.PassContext([passes.VerificationInstrument(passes.VerificationMode.AFTER)]):
result = pm.run_passes(program)
Strategy Notes¶
The PTO-oriented tile stage shared by Default and DebugTileOptimization is:
LowerCompositeOpsFlattenTileNdTo2DLegalizeTileCast(expandstile.castpairs the target ISA cannot emit as onepto.tcvt)AutoTileMatmulL0CanonicalizeTileSliceInferTileMemorySpaceResolveBackendOpLayouts(self-normalizes statement structure internally)LowerAutoVectorSplit(live auto-split lowering path; converts AUTOpl.splitmixed InCore functions into the explicitsplit_aivform before ExpandMixedKernel)ExpandMixedKernelInjectGMPipeBufferSplitVectorKernel(only stamps attrs for split_aiv functions + handles the no-split dual-AIV path)StampTfreeSplit(copies each cross-core tpop's split/pipe-id onto its matching tfree op)NormalizeReturnOrderSkewCrossCorePipeline(cross-core cube/vector software-pipeline skew; runs immediately before LowerPipelineLoops)LowerPipelineLoopsCanonicalizeIOOrderMaterializeTensorStrides— wired into the default pipeline starting from RFC #1300 P6InitMemRefMaterializeSemanticAliases(semantics-required must-alias: loop-carry / in-place; always runs)MemoryReuseAllocateMemoryAddrFoldNoOpReshapeFuseCreateAssembleToSliceDeriveCallDirectionsAutoDeriveTaskDependencies(compiler deps for runtime scopes; AUTO-scope analysis is opt-in)ExpandManualPhaseFence(manual-scope phase-fence TaskId dep compression)SynthesizeAllReduceSignals(distributed: host allreduce optional signal -> explicit internal signal IR)MaterializeCommDomainScopes(distributed: WindowBuffer + CommDomainScopeStmt wrappers in each host_orch body; no-op for comm-less programs)LowerHostTensorCollectives(host-level tensor collectives -> internal builtin chip dispatches)MaterializeDistTensorCtx(explicit CommCtx params/args for DistributedTensor params)SimplifyMaterializeRuntimeScopes(inserts AUTO RuntimeScopeStmt so orchestration codegen emits PTO2_SCOPE 1:1)ClassifyIterArgCarry(stamps each ForStmt iter_arg as trivial alias / rebind carry, and sizes manual-scope TaskId fence arrays)
DebugTileOptimization is a debug-only strategy for inspecting this tile stage
without the tensor-only prefix passes. Use Default for normal compilation and
for non-strategy-specific tests so the maintained pipeline stays covered.
ResolveBackendOpLayouts repairs
backend-constrained elementwise tile ops using registered layout metadata.
For the current PTO row-major elementwise ops, it rewrites [N, 1] vector
operands into [1, N] row_major tile.reshape operations at the
constrained use site, where row-major is inferred from the target shape.
It then reshapes the result back to the original vector shape when
needed.
NormalizeReturnOrder reorders ReturnStmt::value_ in InCore functions so that
return[i] corresponds to the i-th Out/InOut parameter in declaration order,
and updates TupleGetItemExpr indices at call sites accordingly. This lets
orchestration codegen map tuple element indices to output parameters with a
direct out_indices[i] lookup, without tracing through tile.store/yield
chains. The pass is placed before InitMemRef so it runs after all kernel
splitting but before memory allocation.
Simplify folds arithmetic identities (x + 0 → x, x * 1 → x), evaluates
constant-only expressions, runs range-aware rewrites using loop-variable
bounds and if-branch constraints, and propagates scalar constants through
single-assignment bindings. As a final step it runs a conservative scalar
DCE: any AssignStmt whose LHS is a scalar Var and whose RHS contains
no Call anywhere is removed once its LHS has no remaining uses. Any
expression that contains a Call — at the top level or nested inside an
arithmetic tree — is preserved because the IR has no purity annotation yet,
so the call might have observable side effects. The DCE step recurses into
ForStmt/IfStmt/WhileStmt/ScopeStmt bodies so nested dead scalars
are cleaned up as well.
Using PassPipeline Directly¶
from pypto.pypto_core import passes
pipeline = passes.PassPipeline()
pipeline.add_pass(passes.convert_to_ssa())
pipeline.add_pass(passes.init_mem_ref())
pipeline.add_pass(passes.memory_reuse())
# Execute
result = pipeline.run(program)
# Inspect pass properties
p = passes.convert_to_ssa()
print(p.get_name()) # "ConvertToSSA"
print(p.get_produced_properties()) # {SSAForm}
Adding New Passes¶
- Declare in
passes.h:Pass YourNewPass(); - Implement in
src/ir/transforms/withPassProperties - Python binding in
python/bindings/modules/passes.cpp - Property declarations: Set required/produced/invalidated in factory
- Type stub in
python/pypto/pypto_core/passes.pyi - Register in PassManager if part of a strategy
- Test in
tests/ut/ir/transforms/
Testing¶
tests/ut/ir/transforms/test_ir_property.py— IRProperty/IRPropertySet teststests/ut/ir/transforms/test_pass_pipeline.py— Pipeline, PassContext, instruments, and automatic verification teststests/ut/ir/transforms/test_pass_manager.py— PassManager backward compatibilitytests/ut/conftest.py— Autouse fixture enabling AFTER verification for all tests