Skip to content

feat(pto): auto-promote persistent fragment loops to full unroll - #1341

Open
jimmychou0 wants to merge 4 commits into
hw-native-sys:mainfrom
jimmychou0:feat-persistent-unroll-promotion
Open

feat(pto): auto-promote persistent fragment loops to full unroll#1341
jimmychou0 wants to merge 4 commits into
hw-native-sys:mainfrom
jimmychou0:feat-persistent-unroll-promotion

Conversation

@jimmychou0

@jimmychou0 jimmychou0 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Stacked on PR #1339:本分支基于 feat-unroll-enable-hint(#1339 的最新单提交 5acad2f4c,已含其全部 review 修复,包括 Pass B 更名 pto-convert-scf-to-cf-with-loop-hints 与完整 SCF→CF 转换),本 PR 自身只有 1 个提交。#1339 合入 main 后,本 PR 的 diff 会自动收窄为 promotion 部分。

概述

新增 pto-promote-persistent-fragment-loops pass:自动识别访问 persistent SIMT fragment buffer 的循环并提升为强制 full unroll,落实 persistent fragment 方案unroll-enable 与 persistent promotion 设计 中的 promotion 部分。

解决的问题:persistent fragment materialization 要求访问 persistent buffer 的循环被完全展开(否则访问无法归一到稳定的 resident slot)。此前该前置条件依赖调用方手写 {pto.unroll = "full"}——漏标时 materialization 在缺少静态访问集合的情况下失败,且没有清晰诊断。本 pass 把"必须手写"变成"自动识别 + 自动提升 + fail-fast"。

实现

识别(discovery):

  • 以显式 llvm.alloca {pto.persistent} 为入口(不做结构推断),沿 use graph(getelementptr → load/store 及其他 user)收集每个相关 op 的所有外层 scf.for:section 内直接包裹 access 的循环、包裹整个 pto.section.simt 的 kernel 级循环、多层嵌套的所有层级;
  • pto.persistent 属性名从 SIMTPersistentFragmentAnalysis.cpp 的私有常量提升为 PTO.h 共享常量。

提升(promotion):

原始状态 结果
无 hint pto.unroll = "full"
pto.unroll = "enable" 覆盖为 "full"(刻意:保持 enable 会把 loop 留到 metadata 阶段,materialization 失去静态访问集合)
pto.unroll = "full" 保持不变
pto.unroll_factor = N 硬错误

命中的循环附加内部 marker pto.persistent_unroll:marker 让 pto-unroll-loops每一条"丢 hint + remark"的兜底都升级为 persistent 专属硬错误——动态 trip、空 body、非 index 归纳变量、展开失败(这些在 promotion 阶段无法预先检查);marker 随循环展开一同消失,不残留 IR。

Fail-fast(硬错误,绝不静默降级——persistent alloca 不是可安全回退的普通内存):

  • persistent 循环上的 unroll_factor;
  • scf.while 内的 persistent access(该结构无法承载 full-unroll hint);
  • 静态 trip 超过 max-persistent-unroll-trip-count(pass option,默认 128,报错含函数名、trip 值与每个 persistent allocation 的位置)。

诊断在整个函数内收集完毕后统一失败,保证并行 pass 调度下发出的诊断集合确定。

管线位置:prepareVPTOForEmission 内、pto-unroll-loops 正前方(promotion → native unroll → SCCP/canonicalize/CSE → persistent fragment analysis → materialization)。

职责边界:只做识别与标注;slot 分配、keep/resume 生成、SIMT outline、LLVM metadata lowering 仍归各自 pass。

测试与验证

  • lit:promote_persistent_loops.pto(无 hint/enable/full 三种提升、无关循环不动、嵌套全层级、kernel 级循环、iter_args 穿线、promotion+unroll 端到端无残留 attr、被覆盖的 enable 不产生任何 unroll metadata);promote_persistent_loops_invalid.pto(factor / scf.while / guardrail 报错);promote_persistent_dynamic_unroll.pto(三种 unroll 侧硬错误,每个用 -split-input-file 独立 module 以保证诊断可观测)。
  • 144:全量 lit 通过,仅 3 个 element/predicate/vsts offset-index 用例失败——不含本改动时同样失败,属于 main Enable VPTO soft post-update by default #1330(Enable VPTO soft post-update by default)与该验证树基线不一致,与本 PR 无关;全部既有 persistent fragment 测试通过;CANN 模拟器 dsl-st 全套无 FAIL;合规检查 errors=0。

提交列表

  1. bf705b632 feat(pto): auto-promote persistent fragment loops to full unroll

@jimmychou0
jimmychou0 force-pushed the feat-persistent-unroll-promotion branch 5 times, most recently from 85b6fc6 to cadfca2 Compare August 26, 2026 04:10
@@ -0,0 +1,62 @@
// Copyright (c) 2026 Huawei Technologies Co., Ltd.

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.

整体实现OK,建议做一些端到端验证测试。可以包括:初版寄存器驻留优化上传了几个使用dsl开发的驻留test/vpto/cases/kernels/rmsnorm-fp32-4096-persistent-fragment-ptodsl/kernel.py等,可以尝试将前端语法改为unroll hint进行端到端测试,再进行tilelang codegen修改测试。

@jimmychou0

Copy link
Copy Markdown
Contributor Author

补充了嵌套 section-wrapping 循环的 lit 用例(Case 6c/6d),固化依赖感知规则在嵌套形态下的判定(422212f58):

  • 6c 外层 iv 索引 fragment:外层循环提升,内层循环保持不动(每个克隆保留内层设备循环,不再按内层 trip 克隆 section)——UNRUN 输出断言 4 个克隆各含一个 section 且保留一个 scf.for
  • 6d 内层 iv 索引 fragment:内层循环提升(尽管它包裹 section),外层保持运行时循环调用展开后的克隆——断言 2 个 section 克隆且外层 scf.for 保留

两者均有端到端数值佐证(123 克隆真实 kernel,逐 slot 校验:外层依赖形态每个克隆正确读取各自驻留 slot;内层依赖形态克隆内静态化、外层循环调用正确)。全部 promotion lit(RUN1/UNROLL/NOMETA/invalid/dynamic)与 runtime 回归(rmsnorm persistent 2/2、intrinsics 全量)通过。

Persistent fragment materialization requires every loop touching a
persistent buffer to be fully unrolled so each access resolves to a
stable resident slot.  Until now this precondition relied on authors
manually writing {pto.unroll = "full"} on those loops; a missing
annotation made materialization fail without a clear cause.  The new
pto-promote-persistent-fragment-loops pass (wired into
prepareVPTOForEmission immediately before pto-unroll-loops) automates
the discovery and marking, per the persistent-fragment plan:

- Discovery: every llvm.alloca carrying {pto.persistent} is an explicit
  entry point (no structural re-inference).  The pass walks the alloca's
  use graph and collects every enclosing scf.for of every related op -
  loops directly wrapping an access inside a SIMT section, kernel-level
  loops wrapping whole pto.section.simt regions, and every nesting
  layer.
- Promotion: no-hint and "enable" loops are rewritten to "full"
  (overriding enable is deliberate: keeping enable would leave the loop
  to the metadata stage and materialization would lose the static access
  set it depends on); an existing "full" is kept.  Every promoted loop
  is additionally marked {pto.persistent_unroll}.
- Fail-fast (hard errors, never a silent fallback): a fixed
  pto.unroll_factor on a persistent loop; a persistent access nested
  under scf.while; a statically known trip count above the new
  max-persistent-unroll-trip-count option (default 128, error names the
  function, the trip count and every persistent allocation).  Diagnostics
  are collected across the whole function before failing once, so the
  emitted set stays deterministic under the parallel pass manager.
- The marker makes pto-unroll-loops turn each of its drop-with-remark
  fallbacks into a persistent-specific hard error: a dynamic trip count,
  an empty body, a non-index induction variable, or a failed unroll.
  Silently keeping a persistent loop would break materialization
  downstream.  The marker disappears together with the unrolled loop.
- The {pto.persistent} attribute name is promoted into PTO.h and shared
  with SIMTPersistentFragmentAnalysis instead of being file-private.

Tests: promote_persistent_loops.pto covers no-hint/enable/full
promotion, unrelated loops left untouched, nested loops, kernel-level
loops wrapping sections, iter_args threading, a promotion + unroll end to
end run leaving no attributes behind, and (via a third RUN line) that an
overridden enable hint produces no llvm.loop.unroll metadata;
promote_persistent_loops_invalid.pto covers the fixed-factor, scf.while
and guardrail errors; promote_persistent_dynamic_unroll.pto covers the
three unroll-side hard errors, one module per -split-input-file section
so every diagnostic is observable regardless of scheduling.

Docs: the persistent SIMT fragment plan gains an "automatic promotion"
section (discovery rules, enable-override rationale, fail-fast contract,
and the pass's deliberately narrow responsibilities); the loop unroll
hint design doc records it as revision v4.

Validation (144): full lit suite green except the three
element/predicate/vsts offset-index tests that also fail without this
change (they belong to main's hw-native-sys#1330); CANN simulator dsl-st suite passes
with no failures; compliance check reports errors=0.
- discovery walks the pointer flow only (GEP chains; load/store and any
  other consumer are terminal accesses) instead of following every
  result's users, so loops that merely consume a loaded value are no
  longer promoted or hard-errored
- deduplicate the scf.while diagnostic per while op
- keep relatedOps in insertion order via SmallSetVector so diagnostics
  stay stable regardless of the related-op count
- skip statically zero-trip loops instead of promoting them into a
  misleading "no constant trip count" hard error
- PTO.h: move the persistent-attr constants below the unroll contract
  comment they interrupted
- docs: move the v5 revision entry to the top of the revision list and
  sync the pointer-flow wording
- tests: cover load-result consumers and zero-trip loops staying
  untouched, and single emission of the scf.while diagnostic
…d dead loops

Address codex review findings:

- validate the loop-unroll hint before promotion overwrites it, so a
  malformed pto.unroll value or attribute type on a persistent loop
  stays a hard error instead of being silently repaired to "full";
  the validation now lives in a shared LoopUnrollUtils.h used by both
  pto-unroll-loops and the promotion pass
- skip the whole enclosing-loop chain when any member is statically
  zero-trip: the access never executes, so promoting a dynamic outer
  loop would only produce a spurious "no constant trip count" error
- compute the static trip count in uint64_t (ceiling division without
  span+step-1) so extreme bounds like lb=INT64_MIN/ub=INT64_MAX cannot
  overflow signed arithmetic; counts beyond int64_t range report as
  non-constant, which keeps them on the fail-fast paths
- add an end-to-end lit test driving an unannotated persistent loop
  through the full ptoas pipeline into fragment materialization
…apping loops

Promoting a kernel-level loop whose body contains a pto.section.simt
region made VPTO fatobj emission fail on real persistent kernels
(issue hw-native-sys#1379): the full unroll cloned the section once per iteration,
SIMT outlining emitted one single-callsite linkonce_odr entry function
per clone, and BiSheng folded a (run-to-run varying) subset of them, so
their _simt_entry ELF symbols went missing and the VF_SIMT size patch
failed fast.  The emitted module also ballooned (37k vs 811 lines).

The skip is now dependency-aware instead of unconditional.  A
section-wrapping loop is left alone only when its induction variable
never reaches a persistent GEP inside the loop (walked through pure
integer arithmetic): that loop cannot contribute to slot resolution -
materialization only needs the in-section accesses static - while its
unrolling only produces the fragile clone structure.  When the
induction variable does select the fragment slot, unrolling is the only
way to statically resolve the access, so the loop is still promoted.
That dependent shape is verified end to end: with no handwritten hints
the pass promotes it, compilation succeeds, and every clone reads its
own resident slot (per-slot numeric check).  A false negative in the
dependence walk can only leave a loop unpromoted that materialization
then rejects with its own non-constant-index diagnostic, never a wrong
promotion.

Also verified on the way: materialization rewrites dependent persistent
loads into per-clone selections of the resumed resident values, so
same-parity clones become machine-code equivalent - the fold BiSheng
applies is legal whatever the source-level GEP constants were, which is
why keeping clones 'different' cannot avoid the symbol loss.

Validation (A5, CANN 9.1.0-beta.3, LLVM 19.1.7 feature-vpto):
- lit promote_persistent_loops (RUN1/UNROLL/NOMETA incl. new dependent
  case 6b), _invalid, _dynamic_unroll (all hard-error contracts),
  _materialize: PASS
- No-hint dependent kernel (123 clones): auto-promoted, compiled,
  per-slot numerics correct; independent variant untouched as before
- TileLang rmsnorm persistent simtvf (both paths): 2/2 numerics PASS,
  emitted structure back to the two-entry form identical to pre-PR
- Full PTO regression (rmsnorm/mhc/intrinsics incl. unroll hints):
  26 passed
- Review-requested hint variants of the PR#978 case:
  kernel_handwritten_unroll_hint.py (persistent loops written with
  unroll="full" hints) and kernel_auto_promoted_unroll.py (no hints;
  promotion must promote them) both compile through ptoas; for the
  auto variant, promotion promotes exactly the two in-section loops,
  the tile loop is left alone, and materialization emits keep/resume
  (96 sites)
@jimmychou0
jimmychou0 force-pushed the feat-persistent-unroll-promotion branch from 422212f to 211635a Compare August 29, 2026 04:37
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.

2 participants