ResolveBackendOpLayouts Pass¶
Repairs backend-required tile layouts for elementwise ops. [N, 1] col-major vectors are reshaped into [1, N] row-major views, while general non-row-major tiles are coerced through tile.move(..., blayout=row_major). Runs in the tile-PTO stage between InferTileMemorySpace and ExpandMixedKernel; the pass re-normalizes statement structure internally before returning, so NormalizedStmtStructure is preserved across the pass.
Overview¶
After FlattenTileNdTo2D and InferTileMemorySpace, every tile op is in 2-D form with a known layout. Several PTO elementwise ops (registered in src/backend/common/pto_ops_common.cpp) require their tile operands and result to be row_major. This pass repairs those local violations at the consumer:
- For each
AssignStmt/EvalStmtwhose RHS is aCall, queryBackend::GetTileLayoutSpec(op_name). - Skip if no spec is registered, or if all constrained tile inputs and output already use
row_major. - For
[N, 1]col-major inputs, inserttile.reshape(arg, [1, N])before the call. This is a metadata-only view repair because[N, 1]col-major and[1, N]row-major have the same flat memory order. - For other non-row-major tile inputs, insert
tile.move(arg, target_memory=<same>, blayout=row_major, slayout=none_box)before the call. - For
AssignStmtresults whose original result type is not row-major, assign the repaired call to a row-major temporary, then restore the original result layout with eithertile.reshapefor column vectors ortile.movefor general matrix tiles.
The pass is backend-driven: the set of constrained ops and their per-input requirements come from each op's BackendOpRegistryEntry (see set_input_layout / set_output_layout in pto_ops_common.cpp). The pass code itself stays backend-agnostic — adding a new constrained op only requires registering its layout spec, not editing this pass.
Requirements:
- Run after
FlattenTileNdTo2D(assumes 2-D tile ops). - Function must be
InCore— Orchestration / Group functions are skipped. - A backend must be configured via
BackendConfig::Set(...). Otherwise the pass is a no-op.
When to use: As part of the Default tile-PTO pipeline, after layout-altering passes (FlattenTileNdTo2D, InferTileMemorySpace) and before ExpandMixedKernel. The pass manager already places it in the correct slot.
API¶
| C++ | Python | Level |
|---|---|---|
pass::ResolveBackendOpLayouts() |
passes.resolve_backend_op_layouts() |
Function-level |
Python usage:
from pypto.pypto_core import passes
repair = passes.resolve_backend_op_layouts()
program = repair(program)
Algorithm¶
For each function in the program:
Skip if function is not InCore.
Skip if no backend is configured.
Walk the body with IRMutator. For each AssignStmt / EvalStmt whose
RHS is a Call:
spec = backend.GetTileLayoutSpec(call.op.name)
if spec is None: continue
if no constrained input/output needs row_major repair: continue
For each input i targeting a row_major slot:
Skip if the input is non-tile or already row-major.
reshape_var = fresh temp
(AssignStmt: name derived from the result variable.
EvalStmt: name derived from the literal "layout_fix".
Both forms add "row_major" + "arg<i>" qualifiers.)
If input is [N, 1] col-major:
emit reshape_var = tile.reshape(arg_i, [1, N])
Else:
emit reshape_var = tile.move(arg_i, target_memory=<same>,
blayout=row_major, slayout=none_box)
substitute reshape_var into the call
repaired = OpRegistry.Create(call.op.name, new_args, call.kwargs)
If statement is AssignStmt and result_type is constrained but not row-major:
tmp = fresh row-major temp ("row_major" qualifier on the result name)
emit tmp = repaired
If original result is a column vector:
emit result_var = tile.reshape(tmp, original_result_shape)
Else:
emit result_var = tile.move(tmp, target_memory=<same>,
blayout=<original>, slayout=<original>)
Else:
emit result_var = repaired (or EvalStmt with repaired)
Non-tile inputs (scalars, shapes) and inputs whose required layout is nullopt are left untouched. The repair is local to the constrained op: downstream code still sees the original variable type and layout because AssignStmt results are restored after the row-major operation.
Example¶
(adapted from tests/ut/ir/transforms/test_resolve_backend_op_layouts_pass.py::test_rewrites_column_vector_add_through_row_major_reshape, with the Ascend910B backend active)
Before:
@pl.program
class Before:
@pl.function(type=pl.FunctionType.InCore)
def repro(
self,
data: pl.Tensor[[16, 256], pl.FP32],
out: pl.Out[pl.Tensor[[16, 1], pl.FP32]],
) -> pl.Tensor[[16, 1], pl.FP32]:
acc_0: pl.Tile[[16, 1], pl.FP32, pl.MemorySpace.Vec] = pl.tile.create(
[16, 1], dtype=pl.FP32, target_memory=pl.MemorySpace.Vec
)
acc_1: pl.Tile[[16, 1], pl.FP32, pl.MemorySpace.Vec] = pl.tile.muls(acc_0, 0.0)
chunk: pl.Tile[[16, 256], pl.FP32, pl.MemorySpace.Vec] = pl.load(data, [0, 0], [16, 256])
tmp: pl.Tile[[16, 256], pl.FP32, pl.MemorySpace.Vec] = pl.tile.create(
[16, 256], dtype=pl.FP32, target_memory=pl.MemorySpace.Vec
)
partial: pl.Tile[[16, 1], pl.FP32, pl.MemorySpace.Vec] = pl.tile.row_sum(chunk, tmp)
updated: pl.Tile[[16, 1], pl.FP32, pl.MemorySpace.Vec] = pl.tile.add(acc_1, partial)
stored: pl.Tensor[[16, 1], pl.FP32] = pl.store(updated, [0, 0], out)
return stored
After:
@pl.program
class After:
@pl.function(type=pl.FunctionType.InCore)
def repro(
self,
data: pl.Tensor[[16, 256], pl.FP32],
out: pl.Out[pl.Tensor[[16, 1], pl.FP32]],
) -> pl.Tensor[[16, 1], pl.FP32]:
acc_0: pl.Tile[[16, 1], pl.FP32, pl.MemorySpace.Vec] = pl.tile.create(
[16, 1], dtype=pl.FP32, target_memory=pl.MemorySpace.Vec
)
acc_0_rm: pl.Tile[[1, 16], pl.FP32, pl.MemorySpace.Vec] = pl.tile.reshape(acc_0, [1, 16])
acc_1_rm: pl.Tile[[1, 16], pl.FP32, pl.MemorySpace.Vec] = pl.tile.muls(acc_0_rm, 0.0)
acc_1: pl.Tile[[16, 1], pl.FP32, pl.MemorySpace.Vec] = pl.tile.reshape(acc_1_rm, [16, 1])
chunk: pl.Tile[[16, 256], pl.FP32, pl.MemorySpace.Vec] = pl.load(data, [0, 0], [16, 256])
tmp: pl.Tile[[16, 256], pl.FP32, pl.MemorySpace.Vec] = pl.tile.create(
[16, 256], dtype=pl.FP32, target_memory=pl.MemorySpace.Vec
)
partial: pl.Tile[[16, 1], pl.FP32, pl.MemorySpace.Vec] = pl.tile.row_sum(chunk, tmp)
acc_1_rm2: pl.Tile[[1, 16], pl.FP32, pl.MemorySpace.Vec] = pl.tile.reshape(acc_1, [1, 16])
partial_rm: pl.Tile[[1, 16], pl.FP32, pl.MemorySpace.Vec] = pl.tile.reshape(partial, [1, 16])
updated_rm: pl.Tile[[1, 16], pl.FP32, pl.MemorySpace.Vec] = pl.tile.add(acc_1_rm2, partial_rm)
updated: pl.Tile[[16, 1], pl.FP32, pl.MemorySpace.Vec] = pl.tile.reshape(updated_rm, [16, 1])
stored: pl.Tensor[[16, 1], pl.FP32] = pl.store(updated, [0, 0], out)
return stored
tile.muls, tile.add, and similar elementwise PTO ops require row_major inputs and output. Each constrained call is wrapped: the [16, 1] operand is reshaped to [1, 16] immediately before the call, the call runs in row-major form, and the result is reshaped back to [16, 1] so downstream code (tile.store, return type) keeps the user-visible shape. tile.row_sum is unconstrained, so its inputs and output are left as-is.
Implementation¶
| File | Role |
|---|---|
include/pypto/ir/transforms/passes.h (ResolveBackendOpLayouts) |
Public C++ factory |
src/ir/transforms/resolve_backend_op_layouts_pass.cpp |
Mutator and pass body |
include/pypto/ir/transforms/pass_properties.h (kResolveBackendOpLayoutsProperties) |
Pass properties |
python/bindings/modules/passes.cpp (resolve_backend_op_layouts) |
Python binding |
python/pypto/pypto_core/passes.pyi (resolve_backend_op_layouts) |
Type stub |
tests/ut/ir/transforms/test_resolve_backend_op_layouts_pass.py |
Unit tests (binary, unary, scalar-binary on [N, 1] vectors, plus matrix layout coercion through tile.move) |
Layout constraints are registered per op via BackendOpRegistryEntry::set_input_layout / set_output_layout in src/backend/common/pto_ops_common.cpp (e.g. row-major elementwise ops listed in RequiresRowMajorLayout, tile.cast, tile.rsqrt, tile.cmps, tile.sort32, tile.mscatter, ...).
Key helpers in the pass source:
NeedsInputRepair/NeedsOutputRepair— detect constrainedrow_majorslots whose current tile layout is not row-major.CreateLayoutMoveCall— emits thetile.moveused for general matrix layout coercion and result restoration.BackendLayoutRepairMutator::VisitStmt_(const AssignStmtPtr&)/VisitStmt_(const EvalStmtPtr&)— emit the pre-call reshape/move repairs, rebuild the call, and (forAssignStmt) emit the post-call reshape/move restoration when needed.RewriteFunction— bypasses non-InCorefunctions and the unconfigured-backend case before invoking the mutator.
Pass Properties¶
| Property | Value |
|---|---|
| Required | SSAForm, IncoreTileOps, SplitIncoreOrch, TileOps2D |
| Produced | SSAForm, IncoreTileOps, SplitIncoreOrch, TileOps2D, NormalizedStmtStructure |
| Invalidated | — |
Each repair sequence temporarily wraps a previously single-statement op with extra tile.reshape assignments, which would break the canonical statement layout. To keep NormalizedStmtStructure intact across the pass, ResolveBackendOpLayouts invokes NormalizeStmtStructure on its own output before returning, so the property is produced rather than invalidated.
Design Decisions¶
| Decision | Rationale |
|---|---|
Drive layout requirements from Backend::GetTileLayoutSpec rather than hard-coded op lists in the pass |
Each backend declares its own constraints next to its codegen registration. The pass stays backend-agnostic per pass-context-config.md; new constrained ops cost one set_input_layout call, not a pass edit. |
Prefer tile.reshape for [N, 1] vectors |
[N, 1] col-major and [1, N] row-major share the same flat memory, so reshape is cheaper and preserves the existing vector repair behavior. |
Use tile.move for general matrix layout coercion |
Full tiles such as [16, 256] cannot be repaired by a shape-only vector reshape. A same-memory tile.move materializes a row-major view before row-major PTO ops such as tile.exp, then restores the original layout when needed. |
| Bypass when no backend is configured | Many tests build IR without selecting a backend. A no-op fast path keeps those green and avoids spurious mutations. |
Skip non-InCore functions |
Layout constraints apply to per-core elementwise execution; Orchestration and Group functions only contain calls to lower-level kernels and have nothing to repair. |