Skip to content

feat(codegen): Lower a multi-slot MemRef to a ptoas multi-buffer region - #2257

Open
lyfne123 wants to merge 1 commit into
hw-native-sys:mainfrom
lyfne123:feat/ptoas-multi-buffer-region
Open

feat(codegen): Lower a multi-slot MemRef to a ptoas multi-buffer region#2257
lyfne123 wants to merge 1 commit into
hw-native-sys:mainfrom
lyfne123:feat/ptoas-multi-buffer-region

Conversation

@lyfne123

@lyfne123 lyfne123 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

pl.MemRef(slots=N) (#2210) says "one allocation, N uniform slots, this use takes slot k" — which is exactly what ptoas's pto.alloc_multi_tile / pto.multi_tile_get describe. Under memory_planner=PTOAS codegen now hands ptoas the declaration whole instead of emitting N unrelated alloc_tiles.

That is what buys the double-buffering: ptoas plans the N slots as one region it is forbidden to merge, and derives per-slot (dynamic event id) synchronization from the slot expression, overlapping iteration i's load with iteration i-1's compute.

No new IR op, no new pass, no new user-facing switch — the declaration already carries the intent, and the memory planner already decides who owns addresses.

Generated .pto (PTOAS planner)

%ROTATING_mb = pto.alloc_multi_tile valid_row = %c64_index valid_col = %c64_index
             : !pto.multi_tile_buf<!pto.tile_buf<loc=vec, dtype=f32, rows=64, cols=64, ...>, count=2>
scf.for %i = %c0_index to %c4_index step %c1_index {
  %0 = arith.remsi %i, %c2_index : index
  %t = pto.multi_tile_get %ROTATING_mb[%0] : !pto.multi_tile_buf<..., count=2> -> !pto.tile_buf<...>
  pto.tload ins(...) outs(%t : ...)
  ...
}

The operand is the slot index, not the byte offset InitMemRef resolved it into — ptoas matches the index's affine form to decide which accesses can share a slot, and that is what earns the dynamic event ids. Handing it i % 2 * 16384 would defeat the analysis.

What changed

Layer Change
InitMemRef Keeps slot_count_ / slot_index_ on the resolved MemRef. Resolving the index into byte_offset_ says where the slot lands; it does not stop the MemRef from being slot k of an N-slot allocation
InitMemRef The blanket "declared allocation rejected under PTOAS" is narrowed to single-slot declarations (see below)
PTO codegen PlanMultiBufferRegions decides eligibility per allocation; one pto.alloc_multi_tile in the function head, one pto.multi_tile_get per use. Gated on emit_tile_addr_ == false
Printer / parser / bindings / stub pl.MemRef(base, offset, size, slots=N)[k] round-trips, so a post-InitMemRef dump reparses as the same slot
DeepClone Remaps slot_index_ as it does byte_offset_ — the index outlives InitMemRef and names SSA values, so a clone must follow it

Why the PTOAS rejection could be narrowed

InitMemRef rejected every declared allocation under memory_planner=PTOAS, because the isolation is enforced by MemoryReuse, which ptoas replaces wholesale — "honoring the allocation but not its isolation would hand back exactly the coalescing the author declared it to prevent".

A multi-slot declaration is the exception: it becomes a ptoas region whose slots ptoas plans into disjoint physical segments it is explicitly forbidden to alias-merge. The separation the author asked for is carried into ptoas rather than lost. A single-slot declaration still has no counterpart and stays rejected — now pointing at slots=N as the supported spelling.

Unsupported shapes are loud, not silent

A slotted allocation ptoas cannot describe — non-uniform slot types, a space other than Vec/Mat/Acc, a runtime valid shape, a slot carried out of an if/loop as a phi, a count outside [2, 16] — raises a ValueError naming the shape. Falling back to per-slot alloc_tile would silently undo the separation, which is the one thing the declaration exists to state.

Why PTOAS-planner only

Measured with ptoas 0.54 (--enable-insert-sync, a3) on the same double-buffer loop:

alloc_tile (today) alloc_multi_tile + multi_tile_get
level2 (PTOAS planner) ✅ primes 2 event ids; wait_flag(..., v24) / set_flag(..., v25) in-loop → real overlap
level3 + dynamic slot single EVENT_ID0, serialized single EVENT_ID0 — no difference
level3 + constant slots no sync between the slots one extra false MTE3→MTE2 WAR pair

Root cause of the level3 rows: its address fan-out emits an unfolded arith.addi %base, %c512, and a pto.pointer_cast with a non-constant address falls back to conservative aliasing, so slot narrowing is lost. Filed as hw-native-sys/PTOAS#1106; --enable-graph-sync-solver behaves the same. When that lands, widening the gate is one condition in PlanMultiBufferRegions with no API change.

So the PyPTO planner path is untouched — it keeps baking addresses at level3.

Testing

  • New tests/ut/codegen/test_multi_buffer_codegen.py (15): region emission, slot operand is the index (not a byte offset), constant slots share one region, every blocker branch errors with its reason, PyPTO planner unaffected, slot geometry + print/parse round-trip survive InitMemRef.
  • End-to-end: the generated .pto compiles under ptoas 0.54 --pto-level=level2 --enable-insert-sync, emitting two primed event ids plus in-loop dynamic set_flag_dyn / wait_flag_dyn.
  • Regression: tests/ut + tests/lint8584 passed, 2 skipped. clang-tidy clean on the 13 changed sources.
  • Docs updated (language/00-python_syntax, passes/29-init_memref, codegen/00-pto_codegen) in both en and zh.

Follow-ups (not in this PR)

  • On-device numeric validation (this PR is structural + ptoas static compile).
  • Two tiles selecting the same slot expression each emit their own multi_tile_get. Measured cost: one extra Tile object + TASSIGN; ptoas CSEs the duplicate index and the sync output is byte-identical to a shared handle — so this is cleanup, not a correctness or performance issue.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 03607e5e-9a00-4d39-8a3b-b25648beaa0e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds PTOAS support for declarative multi-slot MemRef allocations. Slot metadata survives parsing, initialization, cloning, and printing. Eligible declarations emit shared pto.alloc_multi_tile regions with indexed pto.multi_tile_get access, while unsupported forms raise errors.

Changes

PTOAS multi-slot MemRef support

Layer / File(s) Summary
Preserve MemRef slot metadata
python/bindings/modules/ir.cpp, python/pypto/..., src/ir/transforms/..., include/pypto/ir/memref.h, docs/.../29-init_memref.md
MemRef constructors, parsing, initialization, cloning, and printing preserve slot counts, slot indices, offsets, and round-trip geometry.
Plan and emit multi-buffer regions
include/pypto/codegen/pto/..., src/codegen/pto/..., docs/en/dev/codegen/..., docs/zh/dev/codegen/..., docs/.../language/...
PTOAS validates eligible declarations and emits shared pto.alloc_multi_tile regions with indexed pto.multi_tile_get operations.
Validate generated behavior
tests/ut/codegen/test_multi_buffer_codegen.py
Tests cover PTOAS emission, unsupported declarations, PyPTO behavior, and metadata preservation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels: enhancement

Poem

A rabbit maps each slot in line,
One shared buffer, neatly aligned.
Indices hop where offsets stood,
PTOAS emits them as it should.
Tests guard each path with care—
Multi-slot buffers bloom in air.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes lowering multi-slot MemRefs to a PTOAS multi-buffer region.
Description check ✅ Passed The description explains the multi-slot MemRef lowering, planner behavior, validation, testing, and documentation changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dea0372885

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1347 to +1348
} else if (type_str != candidate.slot_type_str) {
candidate.blocker = "its slots hold differently shaped tiles, and ptoas slots are uniform";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject per-slot valid-shape mismatches

When slots have the same physical tile type but different TileView.valid_shape values—or a later slot has a runtime valid shape—this comparison still considers them uniform because GetTileBufTypeStringFromTileType() deliberately renders v_row=? and v_col=?. TryComputeStaticValidShape() is then applied only to the first tile, and its extents are shared by the entire alloc_multi_tile region, so other slots silently receive the wrong valid extent and can truncate or overrun computation. Compare every slot's effective valid shape and reject mismatches or dynamic extents before creating the region.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
include/pypto/codegen/pto/pto_codegen.h (1)

873-878: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clear the new region state in FunctionState::Reset().

Reset() at Lines 940-990 does not clear multi_buffer_regions or multi_buffer_region_order. Every other member of FunctionState is cleared there. Today this is safe because PlanMultiBufferRegions clears both at its start, and GenerateFunction always calls it. The state is therefore correct only because of the call order between fs_.Reset() (Line 621 of src/codegen/pto/pto_codegen.cpp) and PlanMultiBufferRegions (Line 870). If a later change reads a region before planning runs, the previous function's %mb handle is returned. Add the clears so Reset() matches its documented contract.

♻️ Proposed change in FunctionState::Reset()
       extra_alloc_tiles.clear();
       ssa_to_tile_buf_type.clear();
       subview_materializations.clear();
+
+      multi_buffer_regions.clear();
+      multi_buffer_region_order.clear();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/pypto/codegen/pto/pto_codegen.h` around lines 873 - 878, Update
FunctionState::Reset() to clear both multi_buffer_regions and
multi_buffer_region_order alongside the other per-function state. Ensure reset
removes all multi-buffer region entries and discovery-order data before the next
function is processed.
src/codegen/pto/pto_codegen.cpp (1)

1249-1260: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the shared dims selection.

Lines 1253-1259 repeat the dims selection of ComputeAllocTileFields (Lines 1215-1221) exactly. The comment states that the two must agree. A small file-local helper that returns the const std::vector<ir::ExprPtr>* makes that agreement structural instead of documented, so a later change to one rule cannot skip the other.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codegen/pto/pto_codegen.cpp` around lines 1249 - 1260, Extract the
repeated valid-dimension selection logic from
PTOCodegen::TryComputeStaticValidShape and ComputeAllocTileFields into a shared
file-local helper returning const std::vector<ir::ExprPtr>*. Update both callers
to use it, preserving the priority of non-empty tile_view_->valid_shape over
tile_type->shape_ and returning nullptr when neither is available.
tests/ut/codegen/test_multi_buffer_codegen.py (1)

212-214: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the index into the multi_tile_get lines.

_lines(mlir, "pto.multi_tile_get")[0] raises IndexError when no pto.multi_tile_get is emitted. The reported failure then names the list index, not the missing operation. Assert the list is non-empty first, so a regression in region lowering reports the actual cause with the MLIR attached.

♻️ Proposed change
         mlir = _codegen(RotatingSlot, passes.MemoryPlanner.PTOAS)
-        get = _lines(mlir, "pto.multi_tile_get")[0]
+        gets = _lines(mlir, "pto.multi_tile_get")
+        assert gets, f"expected the slot to be read from the region:\n{mlir}"
+        get = gets[0]
         slot_ssa = get.split("[")[1].split("]")[0]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/codegen/test_multi_buffer_codegen.py` around lines 212 - 214, Update
the test around _codegen(RotatingSlot, passes.MemoryPlanner.PTOAS) to store the
matching pto.multi_tile_get lines, assert that the collection is non-empty with
the generated MLIR included in the failure message, then extract the first line
for slot_ssa. Preserve the existing parsing behavior when the operation is
present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@include/pypto/codegen/pto/pto_type_utils.h`:
- Around line 49-58: Update the documentation around kMinMultiTileBufSlots and
kMaxMultiTileBufSlots to state that PTOAS rejects declarations outside [2, 16].
Clarify that ordinary one-allocation-per-slot lowering applies only within the
PyPTO planner, and do not imply unsupported counts are lowered normally by
PTOAS.

In `@python/bindings/modules/ir.cpp`:
- Around line 823-835: Update CheckSlotCount, used by the MemRef Python
constructors, to throw pypto::ValueError when slots is zero instead of
triggering CHECK. Ensure invalid pl.MemRef(..., slots=0) calls surface as Python
ValueError while valid slot counts retain their current behavior.
- Around line 823-842: Update the resolved MemRef overloads in the DSL typing
wrapper at memref.py to accept both slots and slot, matching the constructor
signatures exposed by the C++ bindings and core stub. Ensure the printed form
MemRef(base, offset, size, slots=N)[k] type-checks and round-trips, and run
pyright for the affected Python typing files.

In `@src/codegen/pto/pto_codegen.cpp`:
- Around line 1308-1324: Update the pass-1 candidate discovery loop to require
both a multi-slot allocation and a valid memref slot index before assigning
geometry and reference_tile, using memref->slot_index_. Keep candidates seeded
for every eligible slotted base so pass 2 still reports unsubscripted-only
bindings, while ensuring slot_type_str, slot_tile_type, and reference_tile come
from the first tile that actually selects a slot.

In `@tests/ut/codegen/test_multi_buffer_codegen.py`:
- Around line 320-321: Update the assertion over slots in the ConstantSlots test
to narrow each MemRef byte_offset_ to ConstInt before accessing value, asserting
the expected constant type for both folded offsets. Preserve the existing offset
set comparison while using the appropriate ConstInt type or established
narrowing mechanism.

---

Nitpick comments:
In `@include/pypto/codegen/pto/pto_codegen.h`:
- Around line 873-878: Update FunctionState::Reset() to clear both
multi_buffer_regions and multi_buffer_region_order alongside the other
per-function state. Ensure reset removes all multi-buffer region entries and
discovery-order data before the next function is processed.

In `@src/codegen/pto/pto_codegen.cpp`:
- Around line 1249-1260: Extract the repeated valid-dimension selection logic
from PTOCodegen::TryComputeStaticValidShape and ComputeAllocTileFields into a
shared file-local helper returning const std::vector<ir::ExprPtr>*. Update both
callers to use it, preserving the priority of non-empty tile_view_->valid_shape
over tile_type->shape_ and returning nullptr when neither is available.

In `@tests/ut/codegen/test_multi_buffer_codegen.py`:
- Around line 212-214: Update the test around _codegen(RotatingSlot,
passes.MemoryPlanner.PTOAS) to store the matching pto.multi_tile_get lines,
assert that the collection is non-empty with the generated MLIR included in the
failure message, then extract the first line for slot_ssa. Preserve the existing
parsing behavior when the operation is present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cf608035-82e6-4912-ad0d-7d0974c4f4da

📥 Commits

Reviewing files that changed from the base of the PR and between 9f8e753 and dea0372.

📒 Files selected for processing (18)
  • docs/en/dev/codegen/00-pto_codegen.md
  • docs/en/dev/language/00-python_syntax.md
  • docs/en/dev/passes/29-init_memref.md
  • docs/zh/dev/codegen/00-pto_codegen.md
  • docs/zh/dev/language/00-python_syntax.md
  • docs/zh/dev/passes/29-init_memref.md
  • include/pypto/codegen/pto/pto_codegen.h
  • include/pypto/codegen/pto/pto_type_utils.h
  • include/pypto/ir/memref.h
  • python/bindings/modules/ir.cpp
  • python/pypto/language/parser/type_resolver.py
  • python/pypto/pypto_core/ir.pyi
  • src/codegen/pto/pto_codegen.cpp
  • src/codegen/pto/pto_type_utils.cpp
  • src/ir/transforms/init_memref.cpp
  • src/ir/transforms/python_printer.cpp
  • src/ir/transforms/utils/deep_clone_utils.cpp
  • tests/ut/codegen/test_multi_buffer_codegen.py

Comment on lines +49 to +58
/// The slot-count bounds ptoas's `!pto.multi_tile_buf` verifier enforces
/// (`MAX_MULTI_BUFFER_NUM = 16`); a declaration outside them keeps the ordinary
/// one-alloc-per-slot lowering.
inline constexpr uint64_t kMinMultiTileBufSlots = 2;
inline constexpr uint64_t kMaxMultiTileBufSlots = 16;

/// Wrap a single-slot `!pto.tile_buf<...>` into ptoas's N-slot container type,
/// `!pto.multi_tile_buf<<slot>, count=N>` — the result of `pto.alloc_multi_tile`
/// and the operand of `pto.multi_tile_get`. `count` must be within ptoas's
/// `[2, 16]`; callers gate on that before reaching here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the unsupported-slot behavior.

The comment says a declaration outside [2, 16] uses ordinary per-slot allocations. PlanMultiBufferRegions instead rejects a PTOAS declaration above kMaxMultiTileBufSlots. State that PTOAS rejects unsupported counts. Keep the ordinary allocation behavior limited to the PyPTO planner.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@include/pypto/codegen/pto/pto_type_utils.h` around lines 49 - 58, Update the
documentation around kMinMultiTileBufSlots and kMaxMultiTileBufSlots to state
that PTOAS rejects declarations outside [2, 16]. Clarify that ordinary
one-allocation-per-slot lowering applies only within the PyPTO planner, and do
not imply unsupported counts are lowered normally by PTOAS.

Comment on lines +823 to +835
CheckSlotCount(slots);
new (self) MemRef(base, byte_offset, size, span, is_pinned, slots, slot);
},
nb::arg("base"), nb::arg("byte_offset"), nb::arg("size"), nb::arg("span") = Span::unknown(),
nb::arg("is_pinned") = false, nb::arg("slots") = 1, nb::arg("slot") = nb::none(),
"Create a memory reference with base Ptr, integer byte_offset, and size. Set is_pinned for an "
"author-declared allocation whose size the parser leaves for InitMemRef to derive; slots/slot "
"select one slot of a multi-slot declaration")
.def(nb::init<VarPtr, ExprPtr, uint64_t, Span>(), nb::arg("base"), nb::arg("byte_offset"),
nb::arg("size"), nb::arg("span") = Span::unknown(),
"Create a memory reference with base Ptr, byte_offset expression, and size")
.def(
"__init__",
[](MemRef* self, const VarPtr& base, const ExprPtr& byte_offset, uint64_t size, const Span& span,
uint64_t slots, const ExprPtr& slot) {
CheckSlotCount(slots);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Raise ValueError for slots=0.

CheckSlotCount uses CHECK for public Python input. Replace it with throw pypto::ValueError(...). An invalid pl.MemRef(..., slots=0) call must raise a Python exception instead of triggering an internal assertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/bindings/modules/ir.cpp` around lines 823 - 835, Update
CheckSlotCount, used by the MemRef Python constructors, to throw
pypto::ValueError when slots is zero instead of triggering CHECK. Ensure invalid
pl.MemRef(..., slots=0) calls surface as Python ValueError while valid slot
counts retain their current behavior.

Comment on lines +823 to +842
CheckSlotCount(slots);
new (self) MemRef(base, byte_offset, size, span, is_pinned, slots, slot);
},
nb::arg("base"), nb::arg("byte_offset"), nb::arg("size"), nb::arg("span") = Span::unknown(),
nb::arg("is_pinned") = false, nb::arg("slots") = 1, nb::arg("slot") = nb::none(),
"Create a memory reference with base Ptr, integer byte_offset, and size. Set is_pinned for an "
"author-declared allocation whose size the parser leaves for InitMemRef to derive; slots/slot "
"select one slot of a multi-slot declaration")
.def(nb::init<VarPtr, ExprPtr, uint64_t, Span>(), nb::arg("base"), nb::arg("byte_offset"),
nb::arg("size"), nb::arg("span") = Span::unknown(),
"Create a memory reference with base Ptr, byte_offset expression, and size")
.def(
"__init__",
[](MemRef* self, const VarPtr& base, const ExprPtr& byte_offset, uint64_t size, const Span& span,
uint64_t slots, const ExprPtr& slot) {
CheckSlotCount(slots);
new (self) MemRef(base, byte_offset, size, span, /*is_pinned=*/false, slots, slot);
},
nb::arg("base"), nb::arg("byte_offset"), nb::arg("size"), nb::arg("span") = Span::unknown(),
nb::arg("slots") = 1, nb::arg("slot") = nb::none(),
"Create a memory reference with base Ptr, byte_offset expression, and size. slots/slot record "
"which slot of a multi-slot allocation this is, which outlives InitMemRef resolving the index "
"into the offset")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Synchronize the DSL MemRef overloads.

PrintMemRef emits pl.MemRef(base, offset, size, slots=N)[k]. The overloads in python/pypto/language/typing/memref.py accept slots only for declaration forms. Pyright rejects this valid printed form before it can round-trip.

Add slots and slot to the resolved MemRef overloads in the DSL typing wrapper. Match the core stub and these bindings.

As per coding guidelines, **/*.{py,pyi} must use pyright.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/bindings/modules/ir.cpp` around lines 823 - 842, Update the resolved
MemRef overloads in the DSL typing wrapper at memref.py to accept both slots and
slot, matching the constructor signatures exposed by the C++ bindings and core
stub. Ensure the printed form MemRef(base, offset, size, slots=N)[k] type-checks
and round-trips, and run pyright for the affected Python typing files.

Source: Coding guidelines

Comment on lines +1308 to +1324
// Pass 1: find the slotted allocations and take each one's geometry from the
// first tile that actually selects a slot. Reading it from whichever tile came
// first instead would make the outcome depend on discovery order — an unslotted
// binding seen first would fix the count at 1 and skip the checks below, which
// is the silent degradation they exist to prevent.
for (const auto& [tile_var, tile_type] : fs_.tile_var_allocs) {
auto memref = ir::GetDefinedMemRef(tile_type);
if (memref->slot_count_ <= 1) continue;
const ir::Var* base = memref->base_.get();
auto [it, fresh] = candidates.try_emplace(base);
if (!fresh) continue;
discovery_order.push_back(base);
it->second.count = memref->slot_count_;
it->second.slot_type_str = GetTileBufTypeStringFromTileType(tile_type);
it->second.slot_tile_type = tile_type;
it->second.reference_tile = tile_var;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The pass 1 filter does not match its comment.

The comment states that the geometry is taken from "the first tile that actually selects a slot". The loop filters only on memref->slot_count_ <= 1. It does not test memref->slot_index_.

InitMemRef::UserBoundMemRef forwards binding->slot_count_ to every tile bound to the allocation, including a tile that binds it whole with no subscript (the UnsubscriptedBinding case in tests/ut/codegen/test_multi_buffer_codegen.py). Such a tile can therefore become the reference_tile and supply slot_type_str and slot_tile_type.

The allocation is still rejected, because pass 2 sets the "binds it without selecting a slot" blocker. The effect is on the diagnostic: the reported blocker and the anchoring span can name a different tile than intended, and the uniform-type baseline is taken from a tile that selects no slot. Add the slot-index test so the code states what the comment claims.

🐛 Proposed fix
   for (const auto& [tile_var, tile_type] : fs_.tile_var_allocs) {
     auto memref = ir::GetDefinedMemRef(tile_type);
     if (memref->slot_count_ <= 1) continue;
+    if (!memref->slot_index_.has_value() || !*memref->slot_index_) continue;
     const ir::Var* base = memref->base_.get();
     auto [it, fresh] = candidates.try_emplace(base);
     if (!fresh) continue;

Pass 2 then needs a candidate for every slotted base, including one whose only bindings are unsubscripted. Seed the map in a separate walk, or keep the try_emplace for discovery and set the geometry on the first slot-selecting tile only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codegen/pto/pto_codegen.cpp` around lines 1308 - 1324, Update the pass-1
candidate discovery loop to require both a multi-slot allocation and a valid
memref slot index before assigning geometry and reference_tile, using
memref->slot_index_. Keep candidates seeded for every eligible slotted base so
pass 2 still reports unsubscripted-only bindings, while ensuring slot_type_str,
slot_tile_type, and reference_tile come from the first tile that actually
selects a slot.

Comment on lines +320 to +321
# ...and the offset is still resolved, as before.
assert {mr.byte_offset_.value for mr in slots} == {0, 64 * 64 * 4}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix the pyright failure on byte_offset_.value.

The pre-commit pyright hook fails here: Cannot access attribute "value" for class "Expr". MemRef.byte_offset_ is declared as Expr in python/pypto/pypto_core/ir.pyi. value exists only on ConstInt. Narrow the type before reading it.

Both slots of ConstantSlots carry a constant index, so InitMemRef folds each offset to a ConstInt. Asserting that explicitly also documents the folding the test relies on.

🐛 Proposed fix
         # ...and the offset is still resolved, as before.
-        assert {mr.byte_offset_.value for mr in slots} == {0, 64 * 64 * 4}
+        offsets = [mr.byte_offset_ for mr in slots]
+        assert all(isinstance(off, ir.ConstInt) for off in offsets), (
+            "a constant slot index must fold to a constant offset"
+        )
+        assert {off.value for off in offsets} == {0, 64 * 64 * 4}
🧰 Tools
🪛 GitHub Actions: CI / 8_pre-commit.txt

[error] 321-321: Pyright: Cannot access attribute "value" for class "Expr"; attribute "value" is unknown (reportAttributeAccessIssue).

🪛 GitHub Actions: CI / pre-commit

[error] 321-321: Pyright: Cannot access attribute "value" for class "Expr"; attribute "value" is unknown (reportAttributeAccessIssue). The pyright pre-commit hook failed with exit code 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/codegen/test_multi_buffer_codegen.py` around lines 320 - 321, Update
the assertion over slots in the ConstantSlots test to narrow each MemRef
byte_offset_ to ConstInt before accessing value, asserting the expected constant
type for both folded offsets. Preserve the existing offset set comparison while
using the appropriate ConstInt type or established narrowing mechanism.

Sources: Coding guidelines, Pipeline failures

`pl.MemRef(slots=N)` says "one allocation, N uniform slots, this use takes
slot k" — which is exactly what ptoas's `pto.alloc_multi_tile` /
`pto.multi_tile_get` describe. Under `memory_planner=PTOAS` codegen now hands
ptoas the declaration whole instead of emitting N unrelated `alloc_tile`s, so
ptoas plans the slots as one region it may not merge, and derives per-slot
synchronization from the slot expression.

The slot **index** is what reaches ptoas, not the byte offset InitMemRef
resolves it into: ptoas matches the index's affine form to decide which
accesses can share a slot, and that is what earns the rotation dynamic event
ids. Verified with ptoas 0.54 `--pto-level=level2`: the emitted kernel primes
two event ids and uses `wait_flag(..., v24)` / `set_flag(..., v25)` in the loop,
overlapping iteration i's load with iteration i-1's compute.

- InitMemRef keeps `slot_count_` / `slot_index_` on the resolved MemRef.
  Resolving the index into `byte_offset_` says *where* the slot lands; it does
  not stop the MemRef from being slot k of an N-slot allocation. Both fields
  print, so a post-InitMemRef dump round-trips as
  `pl.MemRef(base, offset, size, slots=N)[k]`.
- InitMemRef's blanket rejection of declared allocations under PTOAS is
  narrowed to single-slot ones. That rejection exists because ptoas could
  coalesce what the author separated — which a region provably prevents for its
  own slots. A single slot still has no ptoas counterpart.
- A slotted allocation ptoas cannot describe (non-uniform slot types, a space
  other than Vec/Mat/Acc, a runtime valid shape, a slot carried out as a phi, a
  count outside [2,16]) is a ValueError naming the shape, not a silent
  fallback: falling back to per-slot allocs would undo the separation.
- DeepClone now remaps `slot_index_` as it does `byte_offset_` — the index
  outlives InitMemRef and names SSA values, so a clone must follow it.

The PyPTO planner is untouched: at `--pto-level=level3` ptoas does not fold its
per-slot address fan-out, so the region form there loses the slot analysis it
exists for (and is measurably worse than the baked-address path — an extra
false WAR pair between two constant slots). Filed as hw-native-sys/PTOAS#1106;
when it lands, widening the gate is one condition with no API change.
@lyfne123
lyfne123 force-pushed the feat/ptoas-multi-buffer-region branch from dea0372 to a576434 Compare August 3, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant