Skip to content

[Pass Bug] level3 alloc_multi_tile fan-out addresses are not constant-folded, so multi-buffer sync degrades to single-buffer #1106

Description

@lyfne123

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions