FuseCreateAssembleToSlice Pass¶
Fuses tensor.create + tensor.assemble pairs into a single tensor.slice view, eliminating the intermediate buffer.
Overview¶
Orchestration code commonly allocates a small staging tensor with tensor.create, fills it via an InCore call, and then writes the result back into a sub-region of an existing target with tensor.assemble(target, source, offsets). When the staging tensor is consumed by exactly one assemble, the stage-then-copy pattern is equivalent to writing directly into a tensor.slice(target, shape, offsets) view of the target — no intermediate buffer is needed.
This pass detects that pattern and rewrites it: the tensor.create becomes a tensor.slice of the assemble target, and the assemble itself is dropped. Downstream codegen then writes through the slice view directly, avoiding both the temporary allocation and the explicit copy.
Requirements:
- The pass requires
IRProperty::SplitIncoreOrch— Orchestration functions must already be split out from InCore code (OutlineHierarchyScopes/OutlineIncoreScopes/OutlineClusterScopeshave run). - Only Orchestration functions are scanned; InCore, AIC, AIV, and Opaque functions are returned unchanged.
When to use: 27th pass in the Default strategy, after AllocateMemoryAddr (so memory addresses are already assigned for any tensors that survive) and before DeriveCallDirections and the trailing Simplify. It is the last tensor-shape rewrite before call-direction inference.
API¶
| C++ | Python | Level |
|---|---|---|
pass::FuseCreateAssembleToSlice() |
passes.fuse_create_assemble_to_slice() |
Program-level |
Python usage:
from pypto.pypto_core import passes
fuse_pass = passes.fuse_create_assemble_to_slice()
program_fused = fuse_pass(program)
Algorithm¶
For each Orchestration function (others are returned unchanged) the pass runs three phases:
-
Buffer-root analysis —
BufferRootCollectorwalks the function body and builds avar → rootmap. Function parameters are their own roots;tensor.createandtensor.sliceresults define new roots; var-aliasing assignments inherit the root of the aliased value; and the result oftensor.assemble(target, source, offsets)inherits the root oftarget(arg0), notsource(arg1). The collector also threads roots throughForStmt/WhileStmtiter args (linkingiter_arg, the correspondingreturn_var, and the loop-body uses), tracks tuple roots for tuple-returning calls viatuple_output_roots_and resolvesTupleGetItemExprfrom those call results, and propagates roots through call output parameters whose direction isOut/InOut. The result is a single buffer identity per var across loop-carried state, supported tuple-returning call outputs, and cross-function aliasing. -
Pattern detection —
AssemblePatternCollectorscans for the eligible pairs: - Each
tensor.createwhose root resolves to itself (i.e. the create is the buffer's origin) is recorded increate_vars. - Each
tensor.assemble(target, source, offsets)whosesourceresolves back to a recorded create root is recorded as a candidate fuse withFuseInfo{target_expr, offset_tuple}. -
If a single create root is observed in two or more assembles it is moved from
fusible_rootstonon_fusible_rootsand excluded from rewriting. -
Rewrite —
FuseCreateAssembleMutatorperforms the IR mutation: tensor.create(shape, dtype)→tensor.slice(target, shape_tuple, offset_tuple). When the assemble target rank exceeds the created tile rank (e.g. a 2D tile assembled into a 3D tensor at[b, p, q]), the slice's shape tuple is padded with leading singleton1dims so that shape and offset ranks match.- The matched
tensor.assembleAssignStmt is replaced with an emptySeqStmts, and the var it bound is remapped to the slice target so downstream uses still see the same identity. - When eliminating an assemble inside a
ForStmtorWhileStmtbody causes a yielded iter arg to become pass-through (yield(iter_arg)instead ofyield(new_value)),StripPassThroughIterArgs/StripPassThroughWhileIterArgsremoves that iter arg, drops the correspondingreturn_var, and substitutes the iter arg'sinit_valueinto the body. Other iter args carrying real loop-carried state are preserved.
| Source pattern | Action |
|---|---|
create assembled exactly once into a target |
Replace create with slice; drop assemble; remap aliased var |
create assembled ≥ 2 times |
Marked non-fusible; IR unchanged |
create not followed by any assemble |
IR unchanged |
assemble whose source is a tensor.slice (not a create) |
IR unchanged |
| Function is InCore / AIC / AIV / Opaque | Function returned unchanged |
Example¶
Basic fusion¶
Before:
@pl.program
class Before:
@pl.function(type=pl.FunctionType.InCore)
def fill_row(
self,
x: pl.Tensor[[4, 8], pl.FP32],
r: pl.Scalar[pl.INDEX],
out: pl.Out[pl.Tensor[[1, 8], pl.FP32]],
) -> pl.Tensor[[1, 8], pl.FP32]:
row_tile: pl.Tile[[1, 8], pl.FP32] = pl.load(x, [r, 0], [1, 8])
out_1: pl.Tensor[[1, 8], pl.FP32] = pl.store(row_tile, [0, 0], out)
return out_1
@pl.function(type=pl.FunctionType.Orchestration)
def orch(
self,
x: pl.Tensor[[4, 8], pl.FP32],
out: pl.Out[pl.Tensor[[4, 8], pl.FP32]],
) -> pl.Tensor[[4, 8], pl.FP32]:
for r in pl.range(4):
row: pl.Tensor[[1, 8], pl.FP32] = pl.create_tensor([1, 8], dtype=pl.FP32)
row = self.fill_row(x, r, row)
out = pl.assemble(out, row, [r, 0])
return out
After:
@pl.program
class After:
@pl.function(type=pl.FunctionType.Orchestration)
def orch(
self,
x: pl.Tensor[[4, 8], pl.FP32],
out: pl.Out[pl.Tensor[[4, 8], pl.FP32]],
) -> pl.Tensor[[4, 8], pl.FP32]:
for r in pl.range(4):
row: pl.Tensor[[1, 8], pl.FP32] = pl.slice(out, [1, 8], [r, 0])
row = self.fill_row(x, r, row)
return out
The pl.create_tensor is replaced with a pl.slice view of out; pl.assemble is removed; the trailing out = pl.assemble(...) iter arg is stripped because it has become pass-through.
Rank-padded shape (2D tile into 3D target)¶
When the assemble target has higher rank than the create, the slice shape is padded with leading singleton 1 dims so shape and offsets align:
Before:
@pl.function(type=pl.FunctionType.Orchestration)
def orch(
self,
x: pl.Tensor[[4, 8], pl.FP32],
out: pl.Out[pl.Tensor[[2, 4, 8], pl.FP32]],
) -> pl.Tensor[[2, 4, 8], pl.FP32]:
for b in pl.range(2):
for c in pl.range(2):
col = c * 4
chunk: pl.Tensor[[2, 4], pl.FP32] = pl.create_tensor([2, 4], dtype=pl.FP32)
chunk = self.compute(x, chunk)
out = pl.assemble(out, chunk, [b, 0, col])
return out
After:
@pl.function(type=pl.FunctionType.Orchestration)
def orch(
self,
x: pl.Tensor[[4, 8], pl.FP32],
out: pl.Out[pl.Tensor[[2, 4, 8], pl.FP32]],
) -> pl.Tensor[[2, 4, 8], pl.FP32]:
for b in pl.range(2):
for c in pl.range(2):
col = c * 4
chunk: pl.Tensor[[1, 2, 4], pl.FP32] = pl.slice(out, [1, 2, 4], [b, 0, col])
chunk = self.compute(x, chunk)
return out
Implementation¶
Header: include/pypto/ir/transforms/passes.h
Implementation: src/ir/transforms/fuse_create_assemble_to_slice_pass.cpp
BufferRootCollector(IRVisitor) — buffer-identity analysis through assignments, loop iter args, tuples, and call-output aliasing.AssemblePatternCollector(IRVisitor) — detects creates assembled exactly once; multi-assemble roots are excluded.FuseCreateAssembleMutator(IRMutator) — rewritescreate→slice, drops the matchedassemble, and strips pass-through iter args fromfor/whileloops.
Python binding: python/bindings/modules/passes.cpp
Tests: tests/ut/ir/transforms/test_fuse_create_assemble_to_slice.py
test_basic_create_assemble_fused_to_slice— basic fusion + iter-arg strippingtest_duplicate_assemble_not_fused— multi-assemble negative casetest_slice_source_not_fused— assemble whose source is a slice is not fusedtest_multi_iter_arg_partial_fuse— only the assembled iter arg is stripped; other state-carrying iter args survivetest_3d_target_2d_tile_offset_padded— leading-singleton padding when target rank > tile ranktest_no_orchestration_function_noop— pass is a no-op without an Orchestration function
Pass Properties¶
| Property | Value |
|---|---|
| Required | SplitIncoreOrch |
| Produced | — |
| Invalidated | — |
The pass preserves all input properties: it rewrites Orchestration body statements only, does not introduce new IR forms outside tensor.slice, and is idempotent — re-running it on already-fused IR finds no create + single assemble pattern and is a no-op.
Scope¶
| Function type | Action |
|---|---|
| Orchestration | Scanned; eligible create + single assemble pairs fused to slice |
| InCore (InCore, AIC, AIV) | Returned unchanged |
| Opaque | Returned unchanged |
The pass is a no-op when no Orchestration function contains a fusable create + assemble pair.