Skip to content

Rewrite memory planning#913

Open
FangRui0 wants to merge 10 commits into
hw-native-sys:mainfrom
FangRui0:feature_memplan
Open

Rewrite memory planning#913
FangRui0 wants to merge 10 commits into
hw-native-sys:mainfrom
FangRui0:feature_memplan

Conversation

@FangRui0

@FangRui0 FangRui0 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

概述
这个 PR 恢复并保留旧版 PTOAS local memory planner,同时新增一套可选的 modern memplan 实现。默认情况下,level1/level2 仍走 legacy memplan,以保持既有行为和兼容性;开发中的新规划器通过 --plan-memory-impl=modern 显式启用。
本次改动继续保留 tile-native pto.alloc_tile lowering 路径:level1/level2 下,pto.alloc_tile(no addr) 不再先降成 memref.alloc,而是透传到 memplan,由 memplan 直接补充常量 addr,后续 pto.t* tile op 继续消费 !pto.tile_buf。
modern memplan 在引入 Largest-First-Fit 和四道禁止复用闸门,用于更系统地约束地址复用安全性。

主要改动

  • 双 memplan 实现

保留旧版 memplan,文件仍为 lib/PTO/Transforms/PTOPlanMemory.cpp / PTOPlanMemory.h。
新增 modern memplan,文件为 lib/PTO/Transforms/PTOPlanMemoryModern.cpp。
pto-plan-memory 仍是 ModuleOp 级别 pass,内部遍历 func::FuncOp 做函数级 local memory planning。
tools/ptoas/ptoas.cpp 负责选择实现:

--plan-memory-impl=legacy:默认,走旧版 memplan。
--plan-memory-impl=modern:显式启用新 memplan。

当显式启用 modern memplan 且用户未指定 --plan-memory-order-by-size 时,modern 默认开启 largest-first 排序;legacy 仍保持原默认行为。

  • pto.alloc_tile 校验和 lowering

level1/level2:禁止用户显式写 pto.alloc_tile addr。
level1/level2:保留 pto.alloc_tile(no addr) 到 pto-plan-memory,规划完成后直接填充 addr。
level3:要求用户显式提供 local 地址。
pto.alloc_tile(no addr) 不再走 memref.alloc -> pto.pointer_cast -> pto.bind_tile 中间路径。
legacy 和 modern memplan 都支持直接规划 tile-native pto.alloc_tile root。
lowering 路径变化:

// 当前pr的实现
pto.alloc_tile(no addr)
  -> PTOViewToMemref 透传
  -> pto-plan-memory 收集为 local allocation root
  -> legacy/modern memplan 规划 local addr
  -> 直接回写 pto.alloc_tile addr
  -> 后续 pto.t* tile op 继续使用 !pto.tile_buf

// 之前的实现
memref.alloc -> pto.pointer_cast -> pto.bind_tile

非 alloc_tile 的 memref/address root 仍可在 materialize 阶段使用 pto.pointer_cast + pto.bind_tile 表达规划后的地址与 tile metadata。

  • legacy memplan 兼容 tile-native 路径

legacy memplan 增加对 pto.alloc_tile(no addr) 的 root 收集。
BufferInfo 支持从 TileBufType 计算 shape、element type 和 size。
materialize 阶段支持把规划 offset 回写成 pto.alloc_tile addr = ...。
pto.tile_buf_addr 在 legacy liveness 中建模为读取 tile source,避免被未知 local-buffer op 误判。

  • modern memplan

modern memplan 当前聚焦基础 SPEC_LEVEL_0 复用策略:
modern 的复用判定由四道禁止复用闸门约束:
生命周期闸门:生命周期重叠则禁止复用,touching 场景继续进入后续闸门。
phi family 闸门:为 branch/yield/iter_arg alias 场景预留互斥控制流建模。
target-specific load/tpop hazard:用于 A3 等 target 上 load-derived buffer 与特定 writer touching 复用的硬件/后端风险。
op semantic no-alias:建模 PyPTO 风格 inplace 约束,以及 legacy 中已有的 scratch-output conflict。

  • reserve_buffer 规则
    level1/level2:
    支持 auto = true。
    拒绝用户显式指定 base。
    auto = true 的 reserve buffer 在普通 local buffer 规划后,通过 aligned first-fit 在剩余空洞中分配 base。
    level3:
    要求 auto = false。
    要求显式提供 base。

测试
恢复之前删除的 test/samples/planmemory 相关 sample。
现有 test/lit/pto/plan_memory_*.pto 增加 modern RUN,使同一批用例同时覆盖 legacy 和 modern。
新增/更新覆盖:
pto.alloc_tile addr level 校验。
tile-native pto.alloc_tile(no addr) 不经过 memref.alloc。
reserve_buffer level 校验与 auto base 规划。
modern Largest-First-Fit。
modern 四道闸门相关用例。
DPS output writer liveness。
target hazard / semantic no-alias 场景。
same-address UB overlap sync 相关回归用例。

限制
默认仍使用 legacy memplan;modern 需要显式通过 --plan-memory-impl=modern 启用。
modern 仍不作为默认生产路径。
modern 没有实现旧版 StorageEntry 合并框架。
modern 没有实现完整 SPEC_LEVEL_1 / SPEC_LEVEL_2。
modern 没有实现旧版回滚式投机规划。
modern 没有实现 bank-conflict-aware planning。
pipeline membership 相关闸门当前不实现;PTOAS 显式 multibuffer 已覆盖 ping-pong slot 分离语义。
aggressive 地址复用后的跨 pipe hazard 需要同步 pass 配合保证正确性。

@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 restores the mainline local memory planner (PlanMemory pass) and introduces a fallback address resolution pass (AllocToPointerCast) to handle local allocation handles when automatic planning is absent. It also updates the PTOViewToMemref pass to preserve pto.alloc_tile as a tile-native allocation root, avoiding the intermediate memref.alloc generation. The review feedback highlights critical issues in the fallback address calculation within AllocToPointerCast.cpp that could lead to address collisions, and a missing case in getReserveBufferMemSpec within tools/ptoas/ptoas.cpp that would cause validation to fail for LEFT, RIGHT, and ACC address spaces.

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.

Comment on lines +198 to +212
explicit UnresolvedPointerCastToPointerCastPattern(
MLIRContext *context,
DenseMap<Value, SmallVector<uint64_t>> buffer2Offsets)
: OpRewritePattern<pto::PointerCastOp>(context),
buffer2Offsets(std::move(buffer2Offsets)) {
uint64_t maxOff = 0;
for (const auto &kv : this->buffer2Offsets) {
for (uint64_t off : kv.second)
maxOff = std::max(maxOff, off);
}
fallbackNextOffset =
((maxOff + kDefaultAllocAlignmentBytes - 1) /
kDefaultAllocAlignmentBytes) *
kDefaultAllocAlignmentBytes;
}

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.

high

Recalculating fallbackNextOffset by finding the maximum offset in buffer2Offsets and aligning it up is incorrect and can lead to address collisions. If maxOff is the start offset of the last buffer, setting fallbackNextOffset to alignUp(maxOff) will cause any subsequent fallback allocations to start at the same offset, resulting in overlapping memory ranges. Since the correct fallbackNextOffset is already computed and advanced in runOnOperation(), it should be passed directly to the pattern constructor.

  explicit UnresolvedPointerCastToPointerCastPattern(
      MLIRContext *context,
      DenseMap<Value, SmallVector<uint64_t>> buffer2Offsets,
      uint64_t fallbackNextOffset)
      : OpRewritePattern<pto::PointerCastOp>(context),
        buffer2Offsets(std::move(buffer2Offsets)),
        fallbackNextOffset(fallbackNextOffset) {}

Comment on lines +268 to +269
patterns.add<UnresolvedPointerCastToPointerCastPattern>(
patterns.getContext(), std::move(buffer2Offsets));

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.

high

Pass the advanced fallbackNextOffset from runOnOperation() to the pattern constructor to avoid incorrect recalculation.

Suggested change
patterns.add<UnresolvedPointerCastToPointerCastPattern>(
patterns.getContext(), std::move(buffer2Offsets));
patterns.add<UnresolvedPointerCastToPointerCastPattern>(
patterns.getContext(), std::move(buffer2Offsets), fallbackNextOffset);

Comment thread tools/ptoas/ptoas.cpp
Comment on lines +567 to +574
case AddressSpace::LEFT:
case AddressSpace::RIGHT:
case AddressSpace::ACC:
case AddressSpace::BIAS:
case AddressSpace::SCALING:
case AddressSpace::GM:
case AddressSpace::Zero:
break;

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.

high

The helper getReserveBufferMemSpec is missing cases for the local memory spaces LEFT (L0A), RIGHT (L0B), and ACC (L0C). As a result, any reserve_buffer declared in these spaces will return an empty ReserveBufferMemSpec (with capacity 0), causing validateReserveBufferBase to always fail with a misleading capacity overflow error. Adding the correct capacities and alignments for these spaces prevents false validation failures.

  case AddressSpace::LEFT:
    return {65536ull, 4096};
  case AddressSpace::RIGHT:
    return {65536ull, 4096};
  case AddressSpace::ACC:
    return {arch == PTOArch::A5 ? 262144ull : 131072ull, 4096};
  case AddressSpace::BIAS:
  case AddressSpace::SCALING:
  case AddressSpace::GM:
  case AddressSpace::Zero:
    break;

@reedhecre

reedhecre commented Jul 8, 2026

Copy link
Copy Markdown

Codex Review

该评论由 review 机器人自动更新。

  • PR: Rewrite memory planning #913 Rewrite memory planning
  • Author: FangRui0
  • Base/Head: main / feature_memplan
  • Head SHA: 5298ca878c81
  • Trigger: PR 有新提交
  • Generated At: 2026-07-14T12:50:57Z
  • Previous Head SHA: 48f2e2fdbdef
  • Status: failed at codex-review (exit=1)

Summary

Review failed at stage codex-review: exit=1

Findings

未生成结构化 findings,因为 review 过程提前失败。

Log Tail

 test/lit/pto/subview_tmatmul_acc_slices_a5.pto     |    2 +-
 test/lit/pto/subview_validshape_guard.pto          |    6 +-
 .../tile_assemble_level3_subview_tmov_emitc.pto    |   43 +
 test/lit/pto/tile_compact_mode_emitc.pto           |    6 +-
 test/lit/pto/tinsert_level3_tile_native_sync.pto   |   38 +
 test/lit/pto/tpow_emitc.pto                        |    4 +-
 test/lit/pto/tpow_int_no_tmp.pto                   |    4 +-
 test/lit/pto/tpows_emitc.pto                       |    4 +-
 test/lit/pto/tpows_int_no_tmp.pto                  |    2 +-
 test/lit/pto/tquant_dynamic_tmp_valid_a3.pto       |    8 +-
 test/lit/pto/trem_emitc.pto                        |    8 +-
 test/lit/pto/trems_emitc.pto                       |    4 +-
 ...hape_explicit_dynamic_valid_shape_preserved.pto |    3 +-
 test/lit/pto/trsqrt_emitc.pto                      |    8 +-
 test/lit/pto/tsort32_emitc.pto                     |    8 +-
 .../op_fusion_adapter_placement_level2_tadd.pto    |    9 +-
 .../op_fusion_adapter_placement_level3_tadd.pto    |   14 +-
 .../op_fusion_region_pipeline_level2.pto           |    4 +-
 .../normalize_uncovered_tile_sections_vector.pto   |    2 +-
 tools/ptoas/ptoas.cpp                              |  133 ++-
 92 files changed, 3410 insertions(+), 439 deletions(-)
===== END STAGE clone rc=0 @ 2026-07-14 20:50:48 =====

===== STAGE codex-review @ 2026-07-14 20:50:48 =====
set -euo pipefail
cd '/tmp/ptoas-pr-review-monitor/runs/20260714_205037_pr913/repo'
'codex' exec -C '/tmp/ptoas-pr-review-monitor/runs/20260714_205037_pr913/repo' -s read-only -c 'model_provider="codereview"' -c 'model="gpt-5.4"' -c 'model_reasoning_effort="xhigh"' --output-schema '/tmp/ptoas-pr-review-monitor/runs/20260714_205037_pr913/review_schema.json' -o '/tmp/ptoas-pr-review-monitor/runs/20260714_205037_pr913/codex_last_message.json' --color never - < '/tmp/ptoas-pr-review-monitor/runs/20260714_205037_pr913/review_prompt.txt'
[monitor] stage timeout: 1800s
OpenAI Codex v0.115.0 (research preview)
--------
workdir: /tmp/ptoas-pr-review-monitor/runs/20260714_205037_pr913/repo
model: gpt-5.4
provider: codereview
approval: never
sandbox: read-only
reasoning effort: xhigh
reasoning summaries: none
session id: 019f60ae-13b4-7ac3-8fd8-7cbd8ec756f2
--------
user
你现在在审查 GitHub PR。

仓库:hw-native-sys/PTOAS
PR:#913 Rewrite memory planning
作者:FangRui0
base branch:origin/main
head branch:HEAD(当前已 checkout 到 PR head)

要求:
1. 只审查这个 PR 相对 origin/main 的改动,必要时可以看上下文文件。
2. 重点找真实的 correctness / regression / contract mismatch / CI / runtime / compatibility 问题。
3. 不要提纯风格建议,不要提低价值猜测。
4. 严格按优先级输出:
   - P1:高概率会导致错误结果、编译/运行失败、严重回归、发布阻断
   - P2:重要缺陷、行为回归、遗漏校验/测试、较大兼容性问题
   - P3:次要但明确可改的问题
5. 如果没有问题,summary 直接写:未检查到 PR #913 存在问题,并返回 findings=[]。
6. 如果有问题,summary 简洁概括,findings 里每条都要给出:
   - severity
   - title
   - body(说明为什么是问题,尽量具体)
   - file(尽量给相对路径)
   - line(能确定就填整数,否则 null)

建议先查看:
- git status --short
- git diff --stat origin/main...HEAD
- git diff --unified=80 origin/main...HEAD

最终输出必须严格匹配 JSON schema。

mcp startup: no servers
Reconnecting... 1/5 (unexpected status 403 Forbidden: {"code":"INSUFFICIENT_BALANCE","message":"Insufficient account balance"}, url: https://codex.0u0o.com/responses, cf-ray: a1b0a7c3be678900-LAX, request id: 42508868-7465-4bb0-987f-9ffd53be93b4)
Reconnecting... 2/5 (unexpected status 403 Forbidden: {"code":"INSUFFICIENT_BALANCE","message":"Insufficient account balance"}, url: https://codex.0u0o.com/responses, cf-ray: a1b0a7c78f3cad59-LAX, request id: a957964f-efd1-48d4-9656-6d41cdebcf02)
Reconnecting... 3/5 (unexpected status 403 Forbidden: {"code":"INSUFFICIENT_BALANCE","message":"Insufficient account balance"}, url: https://codex.0u0o.com/responses, cf-ray: a1b0a7cc0bdb29a5-LAX, request id: 965d1073-239c-4741-86cb-a79b4ec4312c)
Reconnecting... 4/5 (unexpected status 403 Forbidden: {"code":"INSUFFICIENT_BALANCE","message":"Insufficient account balance"}, url: https://codex.0u0o.com/responses, cf-ray: a1b0a7d28a4f0900-LAX, request id: 30fb8974-2ad1-4bb2-b163-70700ff607e7)
Reconnecting... 5/5 (unexpected status 403 Forbidden: {"code":"INSUFFICIENT_BALANCE","message":"Insufficient account balance"}, url: https://codex.0u0o.com/responses, cf-ray: a1b0a7df2fc49d07-LAS, request id: bf412816-493b-4cf5-bd8f-e2a37379998b)
ERROR: unexpected status 403 Forbidden: {"code":"INSUFFICIENT_BALANCE","message":"Insufficient account balance"}, url: https://codex.0u0o.com/responses, cf-ray: a1b0a7f42a24b26d-LAX, request id: 6a83b31c-29f2-49ff-ae80-18026ec082d4
Warning: no last agent message; wrote empty content to /tmp/ptoas-pr-review-monitor/runs/20260714_205037_pr913/codex_last_message.json
===== END STAGE codex-review rc=1 @ 2026-07-14 20:50:57 =====

@FangRui0
FangRui0 force-pushed the feature_memplan branch 5 times, most recently from 2809e7f to 72a7f11 Compare July 13, 2026 12:10

@zhangstevenunity zhangstevenunity left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Focused correctness review (built this PR head locally, ptoas 0.51, and ran targeted repros). CI is green and the default path stays on legacy; modern is opt-in via --plan-memory-impl=modern.

Findings

  • P2 (confirmed miscompile, modern only): the modern planner reuses a loop-invariant buffer's address for a scratch buffer allocated later in the same loop iteration. See inline on PTOPlanMemoryModern.cpp. Reproduced against this exact build.
  • P3: redundant double-diagnostic for level1/2 reserve_buffer with explicit base (inline on ptoas.cpp). Same comment notes that the Gemini bot's HIGH finding about getReserveBufferMemSpec missing LEFT/RIGHT/ACC is actually unreachable (the op verifier restricts reserve_buffer location to vec/mat), so no fix is needed there.

Looks sound

  • Legacy liveness changes (GEN = all operands, tile_buf_addr/TPrint as reads, tile-native alloc_tile root collection) broaden liveness in the conservative/safe direction; legacy keeps using MLIR dataflow Liveness, so it is correct on the loop case that breaks modern.
  • InsertSync cross-root overlap addition (isLocalBufferOverlapCrossRoot) only ever adds hazard edges when both sides have known absolute addresses and returns the historical false for unknown addresses, so it cannot introduce a missed-sync.
  • The two earlier Gemini HIGH findings are stale/non-issues: UnresolvedPointerCastToPointerCastPattern was deleted with the standalone pass, and the getReserveBufferMemSpec gap is unreachable (see above).

Minor consideration (not blocking): broadening legacy GEN to all operands keeps input buffers live longer, which slightly reduces reuse. Worth a sanity check that capacity-tight default kernels don't newly overflow VEC, but the sample/vpto-sim CI passing suggests no widespread regression.

}
};

static bool lifetimesStrictlyOverlap(const RootInfo &lhs, const RootInfo &rhs) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 (confirmed silent miscompile): a loop-invariant buffer read inside a loop can be given the same address as a scratch buffer allocated later in the loop.

Liveness here is a single linear pre-order index: markUse/markWrite set allocIndex..freeIndex, and lifetimesStrictlyOverlap compares those integers. A root defined outside a loop but read inside it gets freeIndex = its last static use index in the body; nothing extends its live range across the loop back-edge to cover the whole scf.for. So a scratch allocated later in the same iteration looks non-overlapping, and canShare() places it at the same offset. The scratch's per-iteration write then clobbers the loop-invariant value, and the next iteration reads garbage.

InsertSync cannot rescue this: it orders ops within one iteration but cannot prevent cross-iteration clobbering of an aliased address; the two buffers are logically distinct yet simultaneously live.

Reproduced against this PR head (ptoas 0.51):

%inv = memref.alloc() : memref<16x16x16xf16, #pto.address_space<vec>>
pto.tload ins(%arg0) outs(%inv)          // loaded once, must survive all iterations
scf.for %i = %c0 to %c4 step %c1 {
  pto.tstore ins(%inv) outs(%arg1)       // loop-invariant read
  %tmp = memref.alloc() : memref<16x16x16xf16, #pto.address_space<vec>>
  pto.tload ins(%arg2) outs(%tmp)        // scratch written every iteration
  pto.tstore ins(%tmp) outs(%arg1)
}

--mlir-print-ir-after=pto-plan-memory:

  • legacy: %inv -> pointer_cast(0), %tmp -> pointer_cast(8192) (distinct, correct)
  • --plan-memory-impl=modern: %inv -> pointer_cast(0), %tmp -> pointer_cast(0) (same address -> miscompile)

Loop-invariant tiles read inside a loop (weights, scales, KV, ...) are extremely common, so this will hit real kernels once modern is enabled. Existing tests only cover buffers used after the loop (plan_memory_loop_no_reuse_outer_live) and iter_args (seedForIterArgAliases), not loop-invariant reads inside the loop.

Fix direction: when walking a loop region, extend freeIndex of any root defined outside the loop but used inside it to at least the loop op's index (symmetrically, pull allocIndex back for a root written inside a loop but read after it). Legacy is correct precisely because MLIR's dataflow Liveness already models the back-edge.

Comment thread tools/ptoas/ptoas.cpp
}

if (op.getBaseAttr())
(void)validateReserveBufferBase(op, arch);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P3: redundant/confusing double diagnostic. For a level1/level2 reserve_buffer with auto = false and a base, this first calls validateReserveBufferBase (which emits its own error, including a bogus reserved range exceeds ... capacity: base N + size M > 0 bytes whenever getReserveBufferMemSpec returns capacity 0), then unconditionally emits the not supported when --pto-level=level1 or level2 error below. That stacks two errors for one condition. Since the op is rejected here regardless, drop the validateReserveBufferBase call and emit only the not supported message.

Related: the Gemini bot flagged (HIGH) that getReserveBufferMemSpec is missing LEFT/RIGHT/ACC/BIAS/SCALING (capacity 0). That path is actually unreachable -- pto.reserve_buffer's verifier restricts location to vec/mat (confirmed: location = #pto.address_space<acc> fails with "expects 'location' to be #pto.address_space or #pto.address_space"), and both vec/mat have correct capacities. So no fix is needed for the missing cases; only this stray call is worth cleaning up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants