OutlineHierarchyScopes Pass¶
Outlines Hierarchy scopes into separate functions that carry level and role metadata.
Overview¶
This pass transforms ScopeStmt(Hierarchy) nodes — produced by
with pl.at(level=..., role=...) — into separate
Function(Opaque, level, role) definitions and replaces each scope with a
Call to the outlined function.
DSL note:
pl.at(level=pl.Level.CORE_GROUP, ...)is a special case at the parser level — it produces anInCorescope rather than a Hierarchy scope, and is handled later byOutlineIncoreScopes(seepython/pypto/language/dsl_api.py:984). Hierarchy scopes atCORE_GROUPcan still arise from direct IR construction; the C++ name table below covers allLevelvalues for completeness.
Requirements:
- Input IR must be in SSA form (run
ConvertToSSAfirst); SSAForm is preserved (produced) by this pass - Only processes
Opaquefunctions; other function types are left unchanged - Should run before
OutlineIncoreScopesandOutlineClusterScopes
When to use: Run after FlattenCallExpr and before the InCore /
Cluster outlining passes when the IR contains with pl.at(level=..., role=...)
regions that should be lifted into level/role-tagged functions for downstream
hierarchy-aware lowering.
Parent function type is preserved. Unlike OutlineIncoreScopes, this pass
does not promote the parent to Orchestration. Hierarchy is orthogonal to
FunctionType: the outlined child stays Opaque and carries the level/role on
its function metadata.
API¶
| C++ | Python | Level |
|---|---|---|
pass::OutlineHierarchyScopes() |
passes.outline_hierarchy_scopes() |
Program-level |
Factory function:
Python usage:
from pypto.pypto_core import passes
outline_pass = passes.outline_hierarchy_scopes()
program_outlined = outline_pass(program)
Algorithm¶
- Iterate functions: For each function in the program, skip non-
Opaquefunctions and emit them unchanged. - Build symbol table: Collect parameter types/objects and known names via
outline_utils::VarCollectorover the function body. - Outline Hierarchy scopes: Walk the body with
outline_utils::ScopeOutlinerconfigured forScopeKind::Hierarchy. For each scope encountered: - Determine inputs (variables defined outside the scope but used inside).
- Determine outputs (variables defined inside the scope but used after).
- Recurse into nested Hierarchy scopes (inner scopes are outlined first and replaced with calls before the outer scope is lifted).
- Create outlined function: Build a new
Function(FunctionType::Opaque)carrying the scope'slevelandrole, with parameters = inputs and returns = outputs. - Replace scope with Call: Substitute the original
ScopeStmt(Hierarchy)with a Call to the outlined function plusAssignStmts for each output. - Assemble program: Prepend all outlined functions before the originals
and return a new
Program. Parent function types are unchanged.
Naming¶
Outlined functions follow {parent}_{level}[_{role}]_{counter}. The level
component is lowercase; the role suffix is omitted when the scope has no role.
| Level enum | Suffix |
|---|---|
AIV |
aiv |
AIC |
aic |
CORE_GROUP |
core_group |
CHIP_DIE |
chip_die |
CHIP |
chip |
HOST |
host |
CLUSTER_0 |
cluster0 |
CLUSTER_1 |
cluster1 |
CLUSTER_2 |
cluster2 |
GLOBAL |
global |
| Role enum | Suffix |
|---|---|
Orchestrator |
orch |
Worker |
worker |
Examples:
pl.at(level=pl.Level.HOST, role=pl.Role.Orchestrator)→main_host_orch_0pl.at(level=pl.Level.GLOBAL, role=pl.Role.Orchestrator)→main_global_orch_0pl.at(level=pl.Level.CHIP)→main_chip_0
Level aliases (POD = CLUSTER_0, NODE = HOST, UMA = CHIP, etc.)
resolve to the canonical underlying name. For example,
pl.at(pl.Level.POD) produces main_cluster0_0.
Example¶
Before:
@pl.program
class Before:
@pl.function # Opaque function
def main(self, x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
with pl.at(level=pl.Level.HOST, role=pl.Role.Orchestrator):
y: pl.Tensor[[64], pl.FP32] = pl.add(x, x)
return y
After:
@pl.program
class After:
@pl.function(level=pl.Level.HOST, role=pl.Role.Orchestrator)
def main_host_orch_0(self, x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
y: pl.Tensor[[64], pl.FP32] = pl.add(x, x)
return y
@pl.function # Opaque (unchanged — parent type is preserved)
def main(self, x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
y: pl.Tensor[[64], pl.FP32] = self.main_host_orch_0(x)
return y
Note: SubWorker scopes (
role=pl.Role.SubWorker) are not supported as inlinewith pl.at(...)blocks. Declare a SubWorker via@pl.function(level=..., role=pl.Role.SubWorker)as a self-contained function (noselfparameter) inside@pl.program; its body is captured as an :class:InlineStmtand embedded directly in the IR.
Nested Hierarchy Example¶
Nested Hierarchy scopes are outlined recursively. Inner scopes are extracted
first and replaced with calls inside the outer outlined function, producing
chained names like main_global_orch_0_host_orch_0.
Before:
@pl.program
class Before:
@pl.function
def main(self, x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
with pl.at(level=pl.Level.GLOBAL, role=pl.Role.Orchestrator):
y: pl.Tensor[[64], pl.FP32] = pl.add(x, x)
with pl.at(level=pl.Level.HOST, role=pl.Role.Orchestrator):
z: pl.Tensor[[64], pl.FP32] = pl.mul(y, y)
return z
After:
@pl.program
class After:
@pl.function(level=pl.Level.HOST, role=pl.Role.Orchestrator)
def main_global_orch_0_host_orch_0(
self, y: pl.Tensor[[64], pl.FP32]
) -> pl.Tensor[[64], pl.FP32]:
z: pl.Tensor[[64], pl.FP32] = pl.mul(y, y)
return z
@pl.function(level=pl.Level.GLOBAL, role=pl.Role.Orchestrator)
def main_global_orch_0(self, x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
y: pl.Tensor[[64], pl.FP32] = pl.add(x, x)
z: pl.Tensor[[64], pl.FP32] = self.main_global_orch_0_host_orch_0(y)
return z
@pl.function
def main(self, x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
z: pl.Tensor[[64], pl.FP32] = self.main_global_orch_0(x)
return z
InCore scopes nested inside a Hierarchy scope are preserved — they are left
untouched here and lifted later by OutlineIncoreScopes (which runs against
the resulting Opaque hierarchy functions).
Implementation¶
Header: include/pypto/ir/transforms/passes.h
Implementation: src/ir/transforms/outline_hierarchy_scopes_pass.cpp
- Drives
outline_utils::ScopeOutlinerwithScopeKind::HierarchyandFunctionType::Opaque - Builds the symbol table via
outline_utils::VarCollector - Preserves the parent function type via
MutableCopy - Prepends outlined functions to the program's function list
Python binding: python/bindings/modules/passes.cpp
passes.def("outline_hierarchy_scopes", &pass::OutlineHierarchyScopes,
"Create a pass that outlines Hierarchy scopes into separate level/role functions");
Tests: tests/ut/ir/transforms/test_outline_hierarchy_scopes.py
- Basic single-scope outlining (with and without role)
- Multiple scopes per function (independent counter)
- Nested Hierarchy scopes (recursive outlining and chained naming)
- InCore / Cluster scopes are not touched
- Multiple inputs / multiple outputs / no outputs
- Outlining inside control flow
- Parent function type preserved as
Opaque - Level alias resolution (
POD→cluster0) - Independent counters across functions
- Print → re-parse round-trip
OutlineIncoreScopesruns cleanly on the hierarchy-outlined outputHierarchyOutlinedproperty verifier behaviour
Pass Properties¶
| Property | Value |
|---|---|
| Required | SSAForm |
| Produced | SSAForm, HierarchyOutlined, OrchestrationReferencesResolved |
| Invalidated | — |
OrchestrationReferencesResolved is a structural property: every
non-builtin Call inside a FunctionType::Orchestration function targets a
Function present in the Program. Orchestration functions themselves are
declared by the user (or by upstream passes) — this pass does not change
their type. What the pass does add is outlined Opaque callees that
already had Calls in the source program, so it never widens the set of
Orchestration→missing-function edges; once the pass exits, the property
holds (any Call introduced by the pass targets one of the outlined
functions added in the same step). The pass pipeline auto-verifies this
via the registered OrchestrationReferencesResolvedPropertyVerifier;
codegen no longer performs the check.
HierarchyOutlined Property Verifier¶
The HierarchyOutlined IRProperty asserts that no ScopeStmt(Hierarchy)
remains anywhere in the program where this pass is responsible — that is, in
Opaque functions. The verifier (HierarchyOutlinedPropertyVerifierImpl)
walks each Opaque function body and reports any leftover Hierarchy scope
with the message "Hierarchy ScopeStmt found in function (should have been
outlined)".
Hierarchy scopes inside non-Opaque functions are intentionally not
flagged, mirroring the pass's own scope: this pass does not process them, so
the verifier should not require them to be absent.
Relationship to Sibling Outlining Passes¶
| Aspect | OutlineHierarchyScopes | OutlineIncoreScopes | OutlineClusterScopes |
|---|---|---|---|
| Scope kind | ScopeKind::Hierarchy |
ScopeKind::InCore |
ScopeKind::Cluster / standalone ScopeKind::Spmd |
| Output function type | FunctionType::Opaque (with level / role) |
FunctionType::InCore |
FunctionType::Group / FunctionType::Spmd |
| Naming pattern | {func}_{level}[_{role}]_{n} |
{func}_incore_{n} |
{func}_cluster_{n} / {func}_spmd_{n} |
| Promotes parent to | (unchanged) | Orchestration | (unchanged) |
| Processes | Opaque functions only | Opaque functions only | Opaque + Orchestration |
| Pipeline position | 8 (before InCore / Cluster) | 9 | 10 |