Summary
Under --pto-level=level3, pto.alloc_multi_tile addr = %base fans the base out into a
multi-address pto.pointer_cast, but slot k's address is emitted as an unfolded
arith.addi %base, %c<k*slotBytes> instead of a constant. Per the multi-buffer design
(docs/designs/ptoas-multi-buffer-explicit-design.md §5.4.1), a pointer_cast whose
address list contains a non-constant operand falls back to the conservative path
(rootBuffer = addr0, baseAddresses = {0}), so all slot narrowing is lost.
Consequence: at level3 the whole multi-buffer feature degrades to single-buffer sync.
- Dynamic slot (
%k = arith.remui %i, %c2): no dyn event ids at all — one static
EVENT_ID0, load and consume fully serialized. level2 on the same input emits two
primed event ids + set_flag_dyn / wait_flag_dyn.
- Constant slots (
%mb[0] / %mb[1]): worse than not using multi-buffer — the two
slots are treated as one buffer, so an extra false MTE3→MTE2 WAR pair appears that two
plain pto.alloc_tiles with constant addresses do not produce.
This blocks PyPTO: when PyPTO owns local-memory planning it always invokes ptoas at
--pto-level=level3, so today adopting alloc_multi_tile / multi_tile_get there is a
net regression, and the feature is only usable via the ptoas memory planner (level2).
Pass / pipeline name
pto-view-to-memref (level3 explicit-addr fan-out); observable in pto-insert-sync and
the graph sync solver.
Command line
# dynamic slot
ptoas mb_l3.pto -o mb_l3.cpp --enable-insert-sync --pto-level=level3 --pto-arch=a3
ptoas mb_l2.pto -o mb_l2.cpp --enable-insert-sync --pto-level=level2 --pto-arch=a3 # reference
# constant slots
ptoas cs_multi_l3.pto -o cs_multi_l3.cpp --enable-insert-sync --pto-level=level3 --pto-arch=a3
ptoas cs_plain_l3.pto -o cs_plain_l3.cpp --enable-insert-sync --pto-level=level3 --pto-arch=a3 # reference
--enable-graph-sync-solver instead of --enable-insert-sync behaves the same.
Before IR (input)
A. Dynamic slot, level3 (mb_l3.pto)
module {
func.func @prefetch_dyn_eid_l3(
%gm : memref<16x16xf16, #pto.address_space<gm>>,
%dst : memref<16x16xf16, #pto.address_space<gm>>,
%n : index) attributes {pto.entry} {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%c2 = arith.constant 2 : index
%base = arith.constant 32768 : i64
%mb = pto.alloc_multi_tile addr = %base
: !pto.multi_tile_buf<vec, 16x16xf16, count=2>
scf.for %i = %c0 to %n step %c1 {
%idx = arith.remui %i, %c2 : index
%prod = pto.multi_tile_get %mb[%idx]
: !pto.multi_tile_buf<vec, 16x16xf16, count=2>
-> !pto.tile_buf<vec, 16x16xf16>
pto.tload ins(%gm : memref<16x16xf16, #pto.address_space<gm>>)
outs(%prod : !pto.tile_buf<vec, 16x16xf16>)
pto.tstore ins(%prod : !pto.tile_buf<vec, 16x16xf16>)
outs(%dst : memref<16x16xf16, #pto.address_space<gm>>)
}
return
}
}
The level2 reference (mb_l2.pto) is the same module with addr = %base and the %base
constant removed.
B. Constant slots, level3 (cs_multi_l3.pto)
module {
func.func @cs_multi(%gm0 : memref<16x16xf16, #pto.address_space<gm>>,
%gm1 : memref<16x16xf16, #pto.address_space<gm>>,
%dst : memref<16x16xf16, #pto.address_space<gm>>) attributes {pto.entry} {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%base = arith.constant 32768 : i64
%mb = pto.alloc_multi_tile addr = %base : !pto.multi_tile_buf<vec, 16x16xf16, count=2>
%s0 = pto.multi_tile_get %mb[%c0] : !pto.multi_tile_buf<vec, 16x16xf16, count=2> -> !pto.tile_buf<vec, 16x16xf16>
%s1 = pto.multi_tile_get %mb[%c1] : !pto.multi_tile_buf<vec, 16x16xf16, count=2> -> !pto.tile_buf<vec, 16x16xf16>
pto.tload ins(%gm0 : memref<16x16xf16, #pto.address_space<gm>>) outs(%s0 : !pto.tile_buf<vec, 16x16xf16>)
pto.tstore ins(%s0 : !pto.tile_buf<vec, 16x16xf16>) outs(%dst : memref<16x16xf16, #pto.address_space<gm>>)
pto.tload ins(%gm1 : memref<16x16xf16, #pto.address_space<gm>>) outs(%s1 : !pto.tile_buf<vec, 16x16xf16>)
pto.tstore ins(%s1 : !pto.tile_buf<vec, 16x16xf16>) outs(%dst : memref<16x16xf16, #pto.address_space<gm>>)
return
}
}
The reference (cs_plain_l3.pto) replaces the multi-buffer with two plain allocs:
%a0 = arith.constant 32768 : i64
%a1 = arith.constant 33280 : i64
%s0 = pto.alloc_tile addr = %a0 : !pto.tile_buf<vec, 16x16xf16>
%s1 = pto.alloc_tile addr = %a1 : !pto.tile_buf<vec, 16x16xf16>
Expected behavior
level3 should reach the same sync quality as level2 for the same slot expressions — the
slot addresses are compile-time constants in both cases, the only difference is who
computed them.
- Case A: two primed event ids +
set_flag_dyn / wait_flag_dyn keyed off the slot SSA
(as test/lit/pto/multi_tile_prefetch_dyn_event_id.pto checks at the default level).
- Case B: slots 0 and 1 have disjoint
baseAddresses, so no sync between them — the same
sequence the two-plain-alloc_tile reference produces.
Actual IR or error
IR after pto-view-to-memref at level3 (case A) — slot 1's address is an unfolded addi:
%c32768_i64 = arith.constant 32768 : i64
%c512_i64 = arith.constant 512 : i64
%0 = arith.addi %c32768_i64, %c512_i64 : i64
%1 = pto.pointer_cast(%c32768_i64, %0) %c16, %c16_0 {config = ...} : memref<16x16xf16, ...>
%2 = pto.bind_tile %1, ...
scf.for %arg3 = %c0 to %arg2 step %c1 {
%3 = arith.remui %arg3, %c2 : index
%4 = pto.slot_marker %2[%3] : ...
...
}
Emitted C++, case A at level3 (single static event id, serialized):
set_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID0);
wait_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID0);
set_flag(PIPE_MTE2, PIPE_MTE3, EVENT_ID0);
wait_flag(PIPE_MTE2, PIPE_MTE3, EVENT_ID0);
set_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID0);
wait_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID0);
Same module at level2 (correct — two event ids, dynamic ids v14 / v21):
set_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID0);
set_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID1);
wait_flag(PIPE_MTE3, PIPE_MTE2, v14);
set_flag(PIPE_MTE2, PIPE_MTE3, EVENT_ID0);
wait_flag(PIPE_MTE2, PIPE_MTE3, EVENT_ID0);
set_flag(PIPE_MTE3, PIPE_MTE2, v21);
wait_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID0);
wait_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID1);
Case B at level3 — diff of the sync/op sequence, cs_plain_l3.cpp → cs_multi_l3.cpp.
The multi-buffer version has one extra false WAR pair between slot 0's store and
slot 1's load:
> set_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID0);
> wait_flag(PIPE_MTE3, PIPE_MTE2, EVENT_ID0);
Suggested fix
In the level3 explicit-addr path of PTOViewToMemref, fold base + k * slotBytes when
base is a constant (both operands are constants by construction), so the fan-out
produces N literal arith.constant addresses — the shape PTOPlanMemory already produces
at level1/level2. Alternatively, run a canonicalize/fold before the InsertSync and GSS IR
translators so UpdatePointerCastOpMemInfo sees constants.
A regression test could extend test/lit/pto/multi_tile_level3_explicit_addr.pto with a
CHECK that the fan-out addresses are constants, plus a level3 variant of
multi_tile_prefetch_dyn_event_id.pto.
Version
ptoas 0.54 (installed binary /usr/local/ptoas/0.54/bin/ptoas); local source checkout
at 4259f31e915af45ac7365aea514d2cfd6aff0103. Reproduced identically with
--enable-insert-sync and --enable-graph-sync-solver.
- Arch: a3
- PTOAS build level: level3
Summary
Under
--pto-level=level3,pto.alloc_multi_tile addr = %basefans the base out into amulti-address
pto.pointer_cast, but slot k's address is emitted as an unfoldedarith.addi %base, %c<k*slotBytes>instead of a constant. Per the multi-buffer design(
docs/designs/ptoas-multi-buffer-explicit-design.md§5.4.1), apointer_castwhoseaddress list contains a non-constant operand falls back to the conservative path
(
rootBuffer = addr0,baseAddresses = {0}), so all slot narrowing is lost.Consequence: at level3 the whole multi-buffer feature degrades to single-buffer sync.
%k = arith.remui %i, %c2): no dyn event ids at all — one staticEVENT_ID0, load and consume fully serialized. level2 on the same input emits twoprimed event ids +
set_flag_dyn/wait_flag_dyn.%mb[0]/%mb[1]): worse than not using multi-buffer — the twoslots are treated as one buffer, so an extra false MTE3→MTE2 WAR pair appears that two
plain
pto.alloc_tiles with constant addresses do not produce.This blocks PyPTO: when PyPTO owns local-memory planning it always invokes ptoas at
--pto-level=level3, so today adoptingalloc_multi_tile/multi_tile_getthere is anet regression, and the feature is only usable via the ptoas memory planner (level2).
Pass / pipeline name
pto-view-to-memref(level3 explicit-addr fan-out); observable inpto-insert-syncandthe graph sync solver.
Command line
--enable-graph-sync-solverinstead of--enable-insert-syncbehaves the same.Before IR (input)
A. Dynamic slot, level3 (
mb_l3.pto)The level2 reference (
mb_l2.pto) is the same module withaddr = %baseand the%baseconstant removed.
B. Constant slots, level3 (
cs_multi_l3.pto)The reference (
cs_plain_l3.pto) replaces the multi-buffer with two plain allocs:Expected behavior
level3 should reach the same sync quality as level2 for the same slot expressions — the
slot addresses are compile-time constants in both cases, the only difference is who
computed them.
set_flag_dyn/wait_flag_dynkeyed off the slot SSA(as
test/lit/pto/multi_tile_prefetch_dyn_event_id.ptochecks at the default level).baseAddresses, so no sync between them — the samesequence the two-plain-
alloc_tilereference produces.Actual IR or error
IR after
pto-view-to-memrefat level3 (case A) — slot 1's address is an unfoldedaddi:Emitted C++, case A at level3 (single static event id, serialized):
Same module at level2 (correct — two event ids, dynamic ids
v14/v21):Case B at level3 — diff of the sync/op sequence,
cs_plain_l3.cpp→cs_multi_l3.cpp.The multi-buffer version has one extra false WAR pair between slot 0's store and
slot 1's load:
Suggested fix
In the level3 explicit-addr path of
PTOViewToMemref, foldbase + k * slotByteswhenbaseis a constant (both operands are constants by construction), so the fan-outproduces N literal
arith.constantaddresses — the shapePTOPlanMemoryalready producesat level1/level2. Alternatively, run a canonicalize/fold before the InsertSync and GSS IR
translators so
UpdatePointerCastOpMemInfosees constants.A regression test could extend
test/lit/pto/multi_tile_level3_explicit_addr.ptowith aCHECKthat the fan-out addresses are constants, plus a level3 variant ofmulti_tile_prefetch_dyn_event_id.pto.Version
ptoas 0.54(installed binary/usr/local/ptoas/0.54/bin/ptoas); local source checkoutat
4259f31e915af45ac7365aea514d2cfd6aff0103. Reproduced identically with--enable-insert-syncand--enable-graph-sync-solver.