MaterializeTensorStrides Pass¶
Fills every view.has_value() && view.stride.empty() slot on every TensorType / DistributedTensorType reachable from the program with the packed canonical stride for the carried layout (per RFC #1300 §2.4). After this pass runs, the codegen-entry contract holds: every TensorView that exists has explicit stride matching its layout / shape, and the strict-mode TensorViewCanonical verifier accepts the IR.
Status: this pass is registered (
passes.materialize_tensor_strides()), covered by unit tests, and wired into the default tile/PTO pipeline betweenCanonicalizeIOOrderandInitMemRefstarting from RFC #1300 P6.
Overview¶
PyPTO's IR allows two equivalent forms for TensorType.tensor_view_:
- Implicit —
view.has_value() && view.stride.empty(): the layout tag is set (e.g.DN) but the per-dimension stride is left blank. Downstream consumers must treat empty stride as "use the packed canonical stride for this layout." - Explicit — every dimension has its
ExprPtrstride spelled out.
Codegen needs one machine-readable contract, so MaterializeTensorStrides walks the program and rewrites every implicit TensorView into its explicit packed canonical form using BuildLogicalStridesFromLayout from tensor_view_semantics.h. Bare TensorTypes (!view.has_value()) are left untouched: the TensorViewCanonical verifier accepts them in both modes as implicitly ND-packed and the bare form is unambiguous. When the input type is a DistributedTensorType, the rebuilt type remains distributed and preserves its memref, non-stride TensorView metadata such as pad, and window_buffer back-reference.
Requirements:
SSAForm,SplitIncoreOrch,IncoreTileOps,TileOps2D,TileMemoryInferred,NormalizedStmtStructure
Produces:
TensorViewCanonical—PassPipelineauto-verifies after the pass using the registry's strict-mode verifier (empty stride on a presentTensorViewis rejected — that is the state this pass is responsible for eliminating)
Position in the default pipeline (active since RFC #1300 P6): between CanonicalizeIOOrder and InitMemRef. This is the codegen-prep boundary — every layout-mutating pass (ResolveBackendOpLayouts, ExpandMixedKernel, SplitVectorKernel) has finished, and InitMemRef is the first consumer that needs explicit stride.
API¶
| C++ | Python | Level |
|---|---|---|
pass::MaterializeTensorStrides() |
passes.materialize_tensor_strides() |
Program-level |
from pypto.pypto_core import passes
mat_pass = passes.materialize_tensor_strides()
program_canon = mat_pass(program)
Algorithm¶
The pass uses an IRMutator with a Var-substitution cache, mirroring the pattern used by InferTileMemorySpace. It walks every TypePtr reachable from the program:
- For each function, rebuild parameters / return types / body:
- Walk parameter types; if a parameter's
TensorTypematerializes to a different type, build a freshVarwith the samename_hint/ span and register the substitution. - Walk return types similarly.
-
Walk the body via
IRMutator::VisitStmt. Inside:VisitExpr_(VarPtr): if the Var's type changes afterMaterializeType, build a fresh Var with the new type (consultingvar_cache_so every reference to a rebuilt Var resolves to the same new Var).VisitExpr_(IterArgPtr): same as Var, plus theinit_value_is recursed.VisitExpr_(CallPtr): rebuild viaOpRegistrywhen registered, falling back to a directCallconstructor forGlobalVarcalls / unregistered ops.VisitStmt_(AssignStmtPtr): rebuild RHS first; if the RHS Call's return type is more specific than the current LHS Var type, sync the Var.
-
Type rewriting —
MaterializeType(type): TensorType/DistributedTensorTypewithview.has_value() && view.stride.empty() && layout != NZ: rebuild withBuildLogicalStridesFromLayout(shape, layout)filled in. The distributed wrapper and optional metadata (memref,TensorView.pad,window_buffer) are preserved. Other tensor shapes pass through unchanged (identity preserved).TensorTypewithlayout == NZ: untouched (NZ onTensorTypeis invalid IR; the verifier flags it instead ofBuildLogicalStridesFromLayoutCHECK-failing).TupleType: recurse into element types; rebuild only if any sub-type changed.- Anything else: pass through.
The pass is idempotent: re-running on already-materialized IR is a no-op, since every type comparison short-circuits on identity and MutableCopy is skipped when nothing changed.
| Behavior | Trigger |
|---|---|
| Fill stride with packed canonical | view.has_value() && view.stride.empty() and layout in {ND, DN} |
| Identity pass-through | !view.has_value() (bare tensor) |
| Identity pass-through | view.has_value() && !view.stride.empty() (already explicit) |
| Identity pass-through | view.layout == NZ on TensorType (verifier rejects this separately) |
Example¶
Before — InCore param with empty-stride DN view (user-written pl.Tensor[..., pl.DN] without an explicit stride hint):
@pl.function(type=pl.FunctionType.InCore)
def kernel(b: pl.Tensor[[2, 4, 8], pl.FP32, pl.TensorView(stride=[], layout=pl.TensorLayout.DN)],
out: pl.Out[pl.Tensor[[2, 4, 8], pl.FP32]]) -> pl.Tensor[[2, 4, 8], pl.FP32]:
...
After:
@pl.function(type=pl.FunctionType.InCore)
def kernel(b: pl.Tensor[[2, 4, 8], pl.FP32, pl.TensorView(stride=[32, 1, 4], layout=pl.TensorLayout.DN)],
out: pl.Out[pl.Tensor[[2, 4, 8], pl.FP32]]) -> pl.Tensor[[2, 4, 8], pl.FP32]:
...
The DN packed canonical stride for shape [2, 4, 8] is computed as:
stride[1] = 1(DN trailing-pair innermost)stride[2] = shape[1] = 4stride[0] = shape[1] * shape[2] = 32
For ND, the formula reduces to the standard row-major packed strides.
Stride Formulas¶
See BuildLogicalStridesFromLayout in tensor_view_semantics.h.
| Layout | Formula |
|---|---|
ND |
stride[n-1] = 1; stride[k] = stride[k+1] * shape[k+1] for k = n-2 .. 0 |
DN (n ≥ 2) |
stride[n-2] = 1; stride[n-1] = shape[n-2]; stride[n-3] = shape[n-2] * shape[n-1]; stride[k] = stride[k+1] * shape[k+1] for k = n-4 .. 0 |
NZ |
not representable as flat strides (fractal, tile-only) — BuildLogicalStridesFromLayout CHECK-fails |
MakeIndexMul folds ConstInt * ConstInt (with __builtin_mul_overflow guard so an overflow falls back to a symbolic Mul rather than silently wrapping) and the multiplicative identity, so symbolic dims are preserved as Mul expressions while static chains collapse to a single ConstInt.
Verifier interaction¶
Because the pass declares produced = {... ∪ TensorViewCanonical}, PassPipeline automatically runs the registry's TensorViewCanonical verifier after the pass. The registry default is the strict-mode verifier (RFC #1300 §2.4 codegen-entry contract): it rejects view.has_value() && stride.empty() since this pass is responsible for materializing those slots. Bare TensorType (!view.has_value()) is still accepted — implicit ND-packed is canonical by construction. The same verifier is callable directly via passes.verify_tensor_view_canonical(program, require_materialized=True); pass require_materialized=False for the weak mode used during the parse-time / early-pass window before materialization runs.
Related¶
CanonicalizeIOOrder— runs immediately before; produces the program state the materialization consumesInitMemRef— first downstream consumer that depends on explicit stridetensor_view_semantics.h— the helpers (BuildLogicalStridesFromLayout,CheckCanonicalView,CanonicalizeView)- RFC #1300 — Self-consistent IR TensorType layout representation