Skip to content

feat(passes): opt-in dbC=2 L0C double-buffer under the PyPTO memory planner (subsumes #1991)#2002

Open
tonibohnlein wants to merge 7 commits into
hw-native-sys:mainfrom
tonibohnlein:dbc2-pypto-planner
Open

feat(passes): opt-in dbC=2 L0C double-buffer under the PyPTO memory planner (subsumes #1991)#2002
tonibohnlein wants to merge 7 commits into
hw-native-sys:mainfrom
tonibohnlein:dbc2-pypto-planner

Conversation

@tonibohnlein

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in dbC=2 (double-buffered L0C) path under the PyPTO memory planner, and the
tagger fix it depends on. dbC=2 keeps two co-live L0C accumulators ping-ponged so tile i's
FIXPIPE drain overlaps tile i+1's MAD. The PTOAS-planner dbC=2 path shipped in #1959; this
adds the PyPTO-planner path behind a flag that is default OFF (experimental).

This PR subsumes #1991 (its tagger fix is the first commit here) — see "Why coupled".

Part 1 — don't tag pipelined cube accumulators (was #1991)

LowerPipelineLoops's PipelineMembershipTagger tagged every tile in a pipeline-stage
body with pipeline_membership, including cube-matmul accumulators. An accumulator is written
by the single serialized cube (drain-before-next) and is never co-live with the next stage's
accumulator, so tagging it made MemoryReuse's #1949 capacity gate request one L0C buffer per
stage and then shed it back — a spurious PH-MR-001, and for an accumulator nested N loops
deep, 2^N requested buffers. Fix: skip cube-matmul accumulators (tile.matmul* writing
Mem.Acc); data-movement ops that target Acc (e.g. tile.extract(target_memory=Acc)) stay
tagged. This also removes a spurious PH-MR-001 on the plain non-dbC path.

Part 2 — opt-in dbC=2 under the PyPTO planner

  • The one exception to Part 1: the moving loop of a dbC=2 emit (carries
    kPipelineDoubleBufferCAttr) does tag its cube accumulator → flat depth-2 membership
    0:0,0:1,0:0,0:1 → the MemoryReuse: capacity-gated packing fixes the pipelined-operand collapse (#1475) #1949 capacity gate allocates exactly two co-live L0C buffers
    (the ping-pong), reused across the grid.
  • The flag: PassContext(enable_pypto_l0c_double_buffer=…) (default False), threaded
    through ir.compile / runtime.compile_program / the s-test harness, and — importantly —
    through PassManager's two nested dump/profile PassContexts (the same class of silent-reset
    bug previously fixed for memory_planner). The chooser gate becomes
    allow_double_buffer_c = ptoas_planner || (pypto_planner && flag); the PTOAS path is unchanged.

Why coupled

Part 1 is a hard prerequisite for Part 2: without the cube-accumulator skip, the dbC
accumulator gets nested 2×2 membership → the capacity gate sheds it to one buffer → no
ping-pong. Shipping them together keeps the change coherent.

Validation

  • UTs: test_dbc2_pypto_flag_allocates_ping_pong pins flag OFF → 1 acc buffer / ON → 2
    co-live; test_pipelined_accumulator_is_not_tagged pins the skip vs. carve-out. Full
    transforms + pass suites green.
  • Device (a2a3): PyPTO-planner dbC=2 direct-store PASS on 4/8/16-tile grids — numerics
    match the torch golden, and the two accumulators land in distinct L0C buffers at {0, 32 KB}.
  • Independent review: ship-with-nits — no correctness or cross-layer defects; the flag
    threads through both nested contexts with no silent-reset regression. The one code nit (a
    loop-invariant read) is fixed here; the two remaining are test-coverage follow-ups on the
    default-off path (a dbC matmul nested in a user pl.pipeline; the profiling-path forwarding).

Notes

…ership

LowerPipelineLoops stamps a (group, stage) `pipeline_membership` on every
tile a pipeline stage defines so MemoryReuse keeps the stages' clones in
separate buffers. That is correct for the operands a stage *loads* — their
loads overlap the previous stage's compute, so two stages' operand buffers
are genuinely co-live and must stay apart — but wrong for the cube accumulator.

A cube accumulator is not loaded; it is written by the single serialized cube
(a `tile.matmul*` MAD), which retires one tile's MAD before starting the next
regardless of how many stages the scheduler overlaps. So a stage's accumulator
is never co-live with the next stage's accumulator. Tagging it anyway makes
MemoryReuse's capacity-gate request one L0C buffer per stage and then shed it
back onto one — a redundant separation that emits a spurious PH-MR-001, and for
an accumulator nested N pipeline loops deep balloons to 2^N requested buffers
before shedding.

Skip cube-accumulator tiles in the membership tagger, gated on the producer
being a `tile.matmul*` MAD (not on `Mem.Acc` alone — a data-movement op that
also targets Acc, e.g. `tile.extract(..., target_memory=Acc)`, is a genuine
per-stage buffer that overlaps across stages and must keep its membership).
The drain-before-next accumulator now coalesces by lifetime alone onto the
single L0C buffer it needs, and the spurious perf hint is gone. Operand tagging
is unchanged, so the pipelined-operand double-buffer (hw-native-sys#1475) is unaffected.
Coalescing accumulators is correctness-safe: a shared L0C buffer forces the
same drain-before-next serialization the single-accumulator schedule already
relies on. Split-K's in-place `matmul_acc` chain (a RAW dependency, lifetimes
touch but don't overlap) also coalesces correctly and no longer gets spuriously
separated.

Add a regression test asserting a pipelined `tile.matmul` accumulator stays
untagged while its operands — and a non-cube Acc producer (`tile.extract` into
Acc) — carry distinct-stage membership, and update the pass docs (en + zh-cn)
to record the cube-accumulator exception.
…s its ping-pong

The cube-accumulator skip added in the previous commit leaves every pipelined
accumulator untagged. That is correct for the single-accumulator schedule, but
the dbC=2 double-buffered-L0C emit genuinely co-lives two accumulators — tile i's
FIXPIPE drain overlaps tile i+1's MAD — so its accumulator DOES need a
pipeline_membership to double-buffer under the PyPTO memory planner.

Carve out the dbC case: the moving loop that carries kPipelineDoubleBufferCAttr
tags its cube accumulator with its own (group, stage); every enclosing pipeline
loop still skips it (the outer loop double-buffers operands, but the MADs writing
successive outer-stage accumulators still serialize on the one cube). The result
is a flat depth-2 membership from the dbC loop alone, so MemoryReuse's
capacity-gate (hw-native-sys#1475) allocates exactly the two co-live L0C buffers instead of the
per-loop cross-product it previously sheds back to a single serialized buffer.

Verified under the PyPTO planner: the four unrolled accumulators carry a flat
"0:0"/"0:1" membership and bind to two co-live 64KB L0C buffers (the MM,MM,ST,ST
ping-pong), with no spurious PH-MR-001 — matching what the PTOAS planner produces
via InitMemRef. dbC=2 activation under PyPTO remains gated off (a later step,
pending the opt-in flag and device validation); this only wires the emit so the
capacity-gate can honor it.
…t flag)

The previous two commits made the PyPTO capacity gate able to honor the dbC=2
accumulator (flat depth-2 membership). This wires the activation: a PassContext
flag `enable_pypto_l0c_double_buffer` (default off, experimental) that opens the
chooser's dbC=2 design point under the PyPTO planner.

  cfg.allow_double_buffer_c = ptoas_planner || (pypto_planner && flag);

PTOAS keeps dbC=2 unconditionally (device-validated). Under PyPTO the flag is off
by default, so default codegen is byte-identical (the "PyPTO stays dbC=1" golden
still holds). When on, AutoTileMatmulL0 emits the two co-live accumulators, the
tagger gives them a flat depth-2 membership, and MemoryReuse's capacity gate
(hw-native-sys#1475) allocates exactly the two co-live L0C buffers — verified end to end
(flag off -> 1 Acc buffer; flag on -> 2, the MM,MM,ST,ST ping-pong).

Cross-layer: PassContext ctor arg + getter (C++ header/impl), nanobind binding,
type stub; threaded through `ir.compile` + `runtime.compile_program`. New unit
test asserts the flag flips the PyPTO L0C buffer count 1<->2. Docs (14-auto_tile,
en + zh-cn) updated to describe the opt-in and how the pair survives allocation
under each planner.

Also propagate the flag through PassManager's nested dump/profile PassContexts
(the same fix 55301c1 applied to memory_planner) — the chooser reads it *during*
pass execution, so a nested context that resets it would silently disable dbC=2
whenever the pipeline dumps IR (i.e. exactly the device-compile path).

Kept default-off pending device validation of the numerics + drain-hidden win
before any default flip.
Adds `test_direct_store_dbc_pypto` (4/8/16-tile grids) alongside the existing
PTOAS cases in test_dbc2_double_buffer.py, run under `memory_planner=PYPTO` with
`enable_pypto_l0c_double_buffer=True`. Same shapes and torch golden as the PTOAS
sweep, so it is the numerics correctness gate for the new allocation path: under
PyPTO the two co-live L0C accumulators are kept apart by MemoryReuse's capacity
gate (not skipped as under PTOAS) and the reuse-WAR is enforced by PyPTO codegen
sync, so a value check on a >=4-tile grid catches a wrong-order drain/MAD.

Threads the opt-in through the harness: `PTOTestCase` gains a
`get_enable_pypto_l0c_double_buffer()` hook (ctor override, default off) that the
test runner passes to `compile_program`.
…m noqa

Rebase cleanup after replaying the PyPTO-planner dbC=2 commits onto main
(post-hw-native-sys#1959 merge):

- The mat-scratch `xfail(run=False)` had drifted onto `test_direct_store_dbc_pypto`
  (the PyPTO dbC=2 direct-store test, which passes) instead of `test_mat_scratch_dbc`.
  Move it back and correct the reason: the mat-scratch failure under PTOAS is the
  chained-matmul consumer's K-reduction accumulator if-phi handle being split across
  two L0C buffers by PTO codegen (not the Acc->Mat assemble drain, not dbC-specific),
  fixed upstream by hw-native-sys#1961.
- `compile_program` grew an 11th arg (enable_pypto_l0c_double_buffer); add
  `# noqa: PLR0913`, matching the sibling `compile()` entry point.
… loop

Review nit: kPipelineDoubleBufferCAttr depends only on the loop op, not the
clone index, so read it once above the replication loop instead of per clone.
Behaviour-identical.
@coderabbitai

coderabbitai Bot commented Jul 10, 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

Run ID: b1f083f6-ade8-48ae-9215-136aaab3a185

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

Adds an experimental PyPTO L0C double-buffer flag, propagates it through compilation and test execution, updates pipeline membership and allocation behavior, and adds unit/system coverage plus English and Chinese documentation.

Changes

L0C double-buffering

Layer / File(s) Summary
Configuration and propagation
include/pypto/ir/transforms/pass_context.h, src/ir/transforms/pass_context.cpp, python/bindings/modules/passes.cpp, python/pypto/ir/..., python/pypto/runtime/runner.py, tests/st/harness/core/...
Adds enable_pypto_l0c_double_buffer, exposes it through PassContext, forwards it through compilation, profiling, dump mode, and test execution.
Pipeline tagging and dbC gating
src/ir/transforms/auto_tile_matmul_l0_pass.cpp, src/ir/transforms/lower_pipeline_loops_pass.cpp
Allows dbC=2 for PTOAS or opted-in PyPTO and conditionally omits cube-accumulator pipeline_membership tags unless accumulator double-buffering is enabled.
Behavioral coverage and documentation
tests/ut/ir/transforms/..., tests/st/runtime/ops/test_dbc2_double_buffer.py, docs/en/dev/passes/..., docs/zh-cn/dev/passes/...
Tests membership tagging, one-versus-two PyPTO L0C buffers, and direct-store execution under both planners; updates planner-specific documentation.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Compile
  participant PassContext
  participant AutoTile
  participant LowerPipelineLoops
  participant MemoryReuse
  Caller->>Compile: enable PyPTO dbC flag
  Compile->>PassContext: resolve and store flag
  PassContext->>AutoTile: query dbC enablement
  AutoTile->>LowerPipelineLoops: emit pipeline double-buffer attribute
  LowerPipelineLoops->>MemoryReuse: provide accumulator membership metadata
  MemoryReuse->>MemoryReuse: allocate one or two L0C buffers
Loading

Possibly related PRs

Suggested labels: enhancement

Poem

A bunny toggles dbC with care,
Two L0C buffers wait in pairs.
Tags hop lightly through the flow,
PyPTO now knows when ping-pongs go.
Tests thump their paws: “It’s there!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% 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 summarizes the main change: an opt-in dbC=2 L0C double-buffer path under PyPTO.
Description check ✅ Passed The description directly matches the changeset and explains the opt-in PyPTO dbC=2 path and tagger fix.
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.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces an experimental opt-in flag, enable_pypto_l0c_double_buffer, to enable L0C double-buffering (dbC=2) under the PyPTO memory planner. It updates PassContext in C++ and its Python bindings, propagates the flag through the compilation pipeline, and modifies LowerPipelineLoops to avoid tagging cube accumulators unless they belong to a dbC=2 moving loop. Documentation and comprehensive unit/integration tests are also updated to validate this new allocation path. There are no review comments to address, and I have no additional feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@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: 9cb71692d8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +206 to +210
dbc_flag = (
enable_pypto_l0c_double_buffer
if enable_pypto_l0c_double_buffer is not None
else outer.get_enable_pypto_l0c_double_buffer()
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include dbC opt-in in the JIT cache key

When this branch inherits enable_pypto_l0c_double_buffer from an outer PassContext, the flag changes the generated AutoTileMatmulL0/MemoryReuse output but the JIT cache does not distinguish it: python/pypto/jit/decorator.py builds the key from platform/strategy/analyze-auto-scopes/memory_planner and has no effective dbC flag. A @pl.jit kernel first called with the PyPTO flag off and later inside PassContext(..., enable_pypto_l0c_double_buffer=True) will reuse the dbC=1 artifact (or keep dbC=2 stuck on in the reverse order), so the opt-in is silently ignored for cached JIT calls; add the effective flag to the JIT compile kwargs/cache key.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9002ecf — good catch. Added _resolve_enable_pypto_l0c_double_buffer() (reads the active PassContext, mirroring _resolve_memory_planner) and included it in make_cache_key at both call sites (decorator.py). So a JIT kernel compiled with the flag off no longer reuses that dbC=1 artifact when later called under PassContext(enable_pypto_l0c_double_buffer=True) (or vice versa). New UTs pin it: test_dbc_double_buffer_flag_splits_key (the flag splits the key) + test_reads_the_active_pass_context (the resolver reads the ambient context).

@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: 1

🧹 Nitpick comments (1)
tests/st/harness/core/harness.py (1)

191-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring missing new param.

enable_pypto_l0c_double_buffer is added to the constructor (Line 199) and stored (Line 225), but the docstring's Args: section doesn't document it, unlike memory_planner.

📝 Proposed docstring addition
             memory_planner: Override the on-chip memory planner (PYPTO/PTOAS).
                 If None, falls back to ``get_memory_planner()`` (which returns
                 None, deferring to ir.compile's PYPTO default).
+            enable_pypto_l0c_double_buffer: Override the experimental PyPTO
+                dbC=2 opt-in. If None, falls back to
+                ``get_enable_pypto_l0c_double_buffer()`` (which returns None,
+                deferring to ir.compile's default of off).
         """
🤖 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/st/harness/core/harness.py` around lines 191 - 226, Document the
enable_pypto_l0c_double_buffer constructor parameter in the __init__ Args
section, describing that it overrides whether PyPTO L0C double buffering is
enabled and that None preserves the default behavior.
🤖 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 `@python/pypto/ir/compile.py`:
- Line 67: Document the enable_pypto_l0c_double_buffer parameter in the
compile() docstring’s Args section, alongside memory_planner. Describe that it
is experimental and that None inherits the outer context setting or defaults to
disabled.

---

Nitpick comments:
In `@tests/st/harness/core/harness.py`:
- Around line 191-226: Document the enable_pypto_l0c_double_buffer constructor
parameter in the __init__ Args section, describing that it overrides whether
PyPTO L0C double buffering is enabled and that None preserves the default
behavior.
🪄 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

Run ID: 3a018a0a-be33-4645-a44b-0260729309be

📥 Commits

Reviewing files that changed from the base of the PR and between 8ebddcb and 9cb7169.

📒 Files selected for processing (18)
  • docs/en/dev/passes/14-auto_tile_matmul_l0.md
  • docs/en/dev/passes/25-lower_pipeline_loops.md
  • docs/zh-cn/dev/passes/14-auto_tile_matmul_l0.md
  • docs/zh-cn/dev/passes/25-lower_pipeline_loops.md
  • include/pypto/ir/transforms/pass_context.h
  • python/bindings/modules/passes.cpp
  • python/pypto/ir/compile.py
  • python/pypto/ir/pass_manager.py
  • python/pypto/pypto_core/passes.pyi
  • python/pypto/runtime/runner.py
  • src/ir/transforms/auto_tile_matmul_l0_pass.cpp
  • src/ir/transforms/lower_pipeline_loops_pass.cpp
  • src/ir/transforms/pass_context.cpp
  • tests/st/harness/core/harness.py
  • tests/st/harness/core/test_runner.py
  • tests/st/runtime/ops/test_dbc2_double_buffer.py
  • tests/ut/ir/transforms/test_auto_tile_matmul_l0.py
  • tests/ut/ir/transforms/test_lower_pipeline_loops.py

Comment thread python/pypto/ir/compile.py
…aram

Review (hw-native-sys#2002):

- codex (P2): `enable_pypto_l0c_double_buffer` changes the AutoTileMatmulL0 /
  MemoryReuse artifact, but the @pl.jit cache key did not distinguish it, so a
  kernel first compiled with the flag off (then called under
  `PassContext(enable_pypto_l0c_double_buffer=True)`) silently reused the dbC=1
  artifact. Add `_resolve_enable_pypto_l0c_double_buffer()` (reads the active
  PassContext, like `_resolve_memory_planner`) and include it in `make_cache_key`
  at both call sites. New UTs: the flag splits the key + the resolver reads the
  ambient context; updated the pinned key-structure test.
- coderabbit: document `enable_pypto_l0c_double_buffer` in the `compile()` docstring.
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