Repository navigation
[Skills] Add review-pr: advisory PR review adapted from aiter and ATOM - #1279
Merged
Merged
Conversation
ATOM and aiter both ship a `.claude/skills/review-pr` skill. ATOM's is one readable file; aiter's adds a deterministic fetch, per-diff rule derivation, gates that refuse a card whose steps left no trace, and an independent refuter. FlyDSL has neither. This ports aiter's structure, keeps ATOM's single readable entry file, and fills it with FlyDSL's own rules. - fetch.sh pins the head and the merge target, reads the PR's CI checks and the CI benchmark rows (compare_benchmark.py always exits 0, so nothing else reads them), and runs the triage. - triage.py derives the rules a diff can trigger, scans added and deleted lines for candidate sites, collects buffer-bound, CI-coverage and benchmark-coverage evidence, and runs the seven Step 9 gates. - rules.md holds 76 rules; each cites a FlyDSL PR where the defect shipped, was caught in review, or was fixed. - run_one.sh runs worker and refuter headless, optionally in a Docker sandbox that mounts the checkout read-only and holds no GitHub token. The review is advisory and never posts to GitHub.
The independent refuter may uphold a finding but correct a detail, such as a misfiled line number; on the PR 1227 sample it corrected three of five. Those corrections lived only in independent.txt, so a published card still carried the wrong citation. apply-refutation now lists them under the card, numbered by the surviving findings, and rerunning it does not duplicate them.
PRs 1106 and 1157 built a multi-agent review engine for FlyDSL. Several of its rules are independent of that architecture and close gaps here: - fetch.sh pins the head by SHA and computes the diff from the two pinned commits, recording its sha256, instead of rereading the PR's moving refs and `gh pr diff`, so a push during a review cannot mix revisions. - A finding stays [inferred] until its arithmetic is run rather than narrated, and until its chain reaches an observable that masks or the buffer descriptor do not drop. - The refuter kills a finding only when the code shows it is wrong, not because it depends on realistic GPU state. - The blind-spot step names where a first pass misses; correctness findings outrank convention ones under the five-finding cap; deep procedures defer to the skills that own them. - The scan adopts the legacy-spelling patterns (raw ir.*, scf builders, buffer_ops.*, make_ptr) and flags deleted barriers, NaN guards, FileCheck lines and tests. - run_one.sh records each agent's wall time and token use in usage.txt.
This was referenced Oct 13, 2026
jhinpan
marked this pull request as ready for review
October 13, 2026 04:29
Collaborator
Author
|
cc @zhiding512 This is ready for review and test. |
coderfeli
approved these changes
Oct 14, 2026
This was referenced Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
.claude/skills/review-pr, the FlyDSL PR reviewer. It follows thereview-prskills in ROCm/ATOM and ROCm/aiter, so the three repos review in one format. It replaces the engine in #1106 and #1157, which are closed, and absorbs the parts of that work that do not depend on its architecture./review-pr <N>in Claude Code, orrun_one.sh <N>headless, ends in one card per PR: a one-sentence summary, an advisory verdict, what CI at the head actually ran, the CI benchmark rows for the changed kernels, at most five findings, each tagged[verified]or[inferred]and ending in an action, and any corrections from the independent refuter. The skill never posts to GitHub.What comes from where
fetch.shartifacts[verified]run_one.sh; agents optionally in a Docker sandbox with the checkout read-only and no GitHub token;usage.txtper agentEvidence specific to FlyDSL:
scripts/compare_benchmark.pyreturns 0, so the CI benchmark step cannot fail and nothing reads it.fetch.shpulls theratio=rows out of the PR's own test-job logs, andtriage.py benchcovnames the changed kernels no benchmark reaches.make_buffer_tensordefaults tomax_size=True, i.e. no byte bound;triage.py boundslists added descriptors without one.Relation to earlier work
The rule set reuses the review-history mining in #1083 (closed), checked against today's tree (for example
SMEM_CAPACITY_MAPhas moved topython/flydsl/runtime/device.py) and extended with later cases (PRs 1130, 1136, 1153, 1177, 1199, 1200, 1220).From #1106 and #1157, this PR absorbed: the head pinned by SHA and the diff computed from the pinned commits; "run the arithmetic, do not narrate it" and "walk the chain to an observable" as the bar for
[verified]; a refuter that does not kill findings for depending on realistic GPU state; the sweep's list of where a first pass misses; correctness outranking convention under the cap; deferring deep procedures to the owning skills; the legacy-spelling scan patterns; deleted barriers, NaN guards, FileCheck lines and tests as removed-guard leads; and a per-agent usage record. Their publisher, untrusted-container profile, watcher and unreachable-test scanner stay on their branches for when reviews are posted automatically.Sample runs
Both ran
run_one.shwithFLYDSL_REVIEW_SANDBOX_IMAGE=rocm/sgl-dev:v0.5.19-rocm720-mi35x-20260915on an MI355X node (no GPU used). All seven gates were green and the refuter killed no finding. Nothing was posted to either PR.Replay of PR 950 (merged Aug 3; the change whose bf16 cost PR 1009 later bisected), at this PR's head. Worker 783 s, refuter 193 s; $5.50 + $1.46 as priced by the CLI.
FlyDSL PR 950 (replay) — [Perf]optimize the flash attention fp8 performance for gfx950
Rewrites the gfx950 fp8 dual-wave flash-attention main loop into a barrier-fenced phase-pipelined schedule selected by new
FA_*environment variables, and puts the XCD block remap behind a new default-off trait that only the fp8 builder turns on, for sequences of at least 64 q blocks.Review (advisory): 🔴 HIGH RISK
CI (head d618179): NONE — no checks are reported at this head, so no GPU test job (mi325-1 / mi35x-1 / navi-2) ran on this code; nothing is classified PR, baseline or infra because nothing ran.
Perf (CI benchmark, advisory): NOT MEASURED — no completed
test (linux-flydsl-*)job exists at this head, the PR body carries no numbers, and the only benchmark that reaches either changed file istests/kernels/test_flash_attn_fwd.py.🔴 [verified]
kernels/attention/flash_attn_utils.py:927turns the previously unconditional XCD block remap intoif const_expr(traits.XCD_SWIZZLE and not traits.SPLITK and not traits.CAUSAL and traits.NUM_HEADS_Q % 8 == 0), andXCD_SWIZZLEdefaults toFalseon both traits classes (:1482,:1766). Onlykernels/attention/flash_attn_fp8_gfx950.pyever passesxcd_swizzle=True; the bf16/f16 builder atkernels/attention/flash_attn_gfx950.py:102— whichflash_attn_interface._build_denseand_build_varlencall for dense, varlen and paged attention — still calls_make_dualwave_swp_traitswithout it, and_init_dualwave_thread_mappingis shared by both contexts (:3290bf16,:4377fp8). Impact: every non-causal, non-split-K bf16/f16 config withnum_heads % 8 == 0is compiled with the plain grid mapping instead of the XCD-local one. The remap is a bijection and the code comment says output is bit-identical, so no correctness test can see it and no benchmark ran here — the loss is silent and will only show up as lower long-context throughput. The same gate also disables the remap for fp8 below 64 q blocks (seq_len <= 16128atBLOCK_M=256), which was previously on at every length. Author must threadxcd_swizzlethrough the bf16 builder (or keep the old unconditional behaviour there) and post base-vs-head numbers for one long-context bf16 shape showing the mapping change is intended.kernels/attention/flash_attn_fp8_gfx950.py:182-184readsFA_PP,FA_PP_PRIOandFA_TPVfromos.environat trace time inside the@flyc.kernelbody and feeds them toconst_expr, so they pick between three materially different loop schedules. None of them is inDualwaveSwpFp8Traits.cache_tag(kernels/attention/flash_attn_utils.py:1772-1800) and none is in_CACHE_INVALIDATING_ENV_VARS(python/flydsl/compiler/jit_function.py:128, fiveFLYDSL_*names), and the kernel source text is identical for every value, so the key cannot separate them. Impact: two runs sharingFLYDSL_RUNTIME_CACHE_DIRthat differ only inFA_PPcompute the same key, and the second is served the first's binary — a stale artifact and a mis-attributed measurement, by construction silent. The defaults also leave code dead in every shipped build: withFA_PP=1theif const_expr(not PP)arm never compiles, and that arm no longer carries thesched_group_barrierpairs this PR deleted, soFA_PP=0is a third never-benchmarked schedule rather than the pre-PR fallback. Author must either hard-code the chosen schedule and delete the dead arms, or move the knobs intopython/flydsl/utils/env.pywithFLYDSL_*names and add them to the fp8cache_tag.kernels/attention/flash_attn_fp8_gfx950.py:252opens a wave-group barrier shift with_stagger_extra_barrier_if_one(ctx.stagger_i32)(waves 4-7 take one extras_barrier, sincestagger_i32iswave_id // 4), gated only onFA_PPand never closed. The bf16 sibling does both halves of this and gates both on the trait:kernels/attention/flash_attn_gfx950.py:244opens,:680closes with_stagger_extra_barrier_if_zerobefore the store, each underif const_expr(traits.DUALWAVE_SWP_ENABLE_STAGGER). Impact:dualwave_swp_enable_stagger=Falseis now inert on the fp8 path while still sitting in the fp8cache_tag, so the public kwarg silently does nothing; and the two wave halves stay one barrier phase apart through the epilogue. I could not reach a wrong value — the 6-deep K/V ring with aj+4/j+5lookahead leaves one buffer of slack andstore_final_ousespermlane32_swaprather than LDS — so this is a question, not a defect. Reviewer should ask why the fp8 path needs no closing barrier when the bf16 path does, and whether the stagger should honourtraits.DUALWAVE_SWP_ENABLE_STAGGER.kernels/attention/flash_attn_fp8_gfx950.pyand changes two softmax helpers plus the grid mapping inkernels/attention/flash_attn_utils.py. No CI check ran at this head andscripts/compare_benchmark.pyreturns 0 unconditionally, so even a green run would not have caught a regression. Impact: nothing on either side of this merge can tell a reviewer whether the new schedule is faster, or by how much, on any shape — exactly the gap that let two earlier gfx950 attention regressions sit unnoticed. Author must attach base-vs-head timings for at least one short and one long-context shape (the XCD threshold changes behaviour acrossseq_len16128 vs 16129), taken withFLYDSL_RUNTIME_ENABLE_CACHE=0, and state GPU, ROCm version and sample count.📝 [verified] Two changes in
kernels/attention/flash_attn_utils.pyreach past the fp8 kernel the title names._score_pair_sum(:413) is rewritten from a 32-step scalarfaddchain tolo + hifollowed byVec.reduce("add", ...), and its call site at:3726is insideDualwaveSoftmaxHelper— the bf16/f16 dual-wave kernel, whose emitted reduction therefore changes too (whether the f32 result changes depends on how fastmath was already reassociating it). AndMIN_Q_BLOCKS_XCD_SWIZZLE = 64(:36) arrives with no derivation: atBLOCK_M=256it moves the XCD crossover toseq_len = 16129, and nothing says on which shapes that crossover was measured. Reviewer should ask whether the bf16 path was re-benchmarked after the helper rewrite, and for the measurement behind the 64-block threshold.Independent reader's corrections (not yet applied to the findings above):
flash_attn_interface._build_dense(:88) builds the generic module (flash_attn_generic.build_flash_attn_func_module), not the dual-wave bf16 one; the affected callers are_build_dense_dualwave(:122/:140),_build_varlen(:186/:204) and the paged builders at :273 and :322.dualwave_swp_enable_stagger=Falseis "now inert" on the fp8 path is wrong — grep forstaggerin merge-target/kernels/attention/flash_attn_fp8_gfx950.py finds no stagger call at all, andDUALWAVE_SWP_ENABLE_STAGGERon the fp8 traits is referenced only by the dataclass field (:1702) and the cache_tag (:1769). The kwarg was already inert there; what this PR adds is an unconditional open that ignores the trait rather than honouring it.Caveats: the replay ran offline from git, with no PR description and no CI, so the empty-description finding is an artifact of the replay. The first finding's rule was written from PR 1009's fix, so it shows the mechanics, not unbiased recall. The second finding appears in no rule's examples and is still on main:
kernels/attention/flash_attn_fp8_gfx950.py:183-185readsFA_PP,FA_PP_PRIOandFA_TPVat trace time, and none of them reaches the JIT cache key.#1227 (open, RMSNorm tuning for gfx1250), run with this PR's first commit. About 15 min end to end.
FlyDSL PR #1227 — [Kernel][Perf] Tune RMSNorm for gfx1250: BF16 hardware conversion, early loads, small-N kernel choice
Adds five gfx1250-only tuning switches to the three RMSNorm forward builders — hardware bf16 conversion, gamma loaded in pass 1 behind a scheduling barrier, bounds checks dropped on fully-interior tiles, a single-barrier block reduce, and a reciprocal mean — plus a new rule for choosing between the one-row and multi-row small-N kernels.
Review (advisory):⚠️ NEEDS WORK
CI (head bb146e1): all green — test (linux-flydsl-mi325-1 / gfx942), test (linux-flydsl-mi35x-1 / gfx950), test (linux-flydsl-navi-2), plus Python/C++ style, CodeQL and docs; Multi-GPU SKIPPED (no
multi-gpulabel). No red checks, so nothing to classify as PR / baseline / infra. Every behavioral change in this diff is gated onarch == "gfx1250", which none of the three GPU runners provides.Perf (CI benchmark, advisory):
rmsnorm 32768x8192 bf16ratio 0.995–1.000x vs main@1bd7ebd5, 3 rows, one run each on a shared runner — this confirms the "no change off gfx1250" claim and nothing else. NOT MEASURED for the changed paths: no CI benchmark row reaches gfx1250, fused-add RMSNorm, or any N ≤ 2048 shape where the new kernel-choice rule decides.kernels/norm/rmsnorm_kernel.py:137and:875turn on gamma preloading for every gfx1250 row width, butpreload_weight_vec(kernels/norm/rmsnorm_common.py:95) only halves the cached vector for 16-bit gamma — with FP32 gamma it falls through toload_weight_vec, caching 8 VGPRs per tile instead of 4. BF16 activations with FP32 weights are a supported combination (rmsnorm_common.py:151) and the PR's own new test uses it. At N = 32768 on gfx1250,default_block_threadsreturns 256 (its 512 branch is gfx95-only), so NUM_VEC_ITERS = 16 andgamma_localholds 128 VGPRs rather than the 64 behind the quoted 252-VGPR budget; the fused kernel hoists x, residual and gamma together, so it is worse. Impact: spills to scratch or thes_set_vgpr_msbbank switching this PR exists to avoid — a silent slowdown against main on exactly the wide rows it claims 2.4x on, with no wrong values. The body's "F32 rows" do not cover this: f32 activations set elem_bits = 32 and never enter the vector path at all. Reviewer should ask forrmsnorm(bf16 x, fp32 weight)at N = 16384 and 32768 on gfx1250, base vs head, with the VGPR counts — or forPRELOAD_GAMMA/HOIST_LOADSto be conditioned on the cached gamma width.MULTI_ROW_MIN_M = Nrule atkernels/norm/rmsnorm_kernel.py:625is still contested and the thread is unresolved: zhiding512 measured 4096×1536 and 4096×2048 at 0.88x and 0.78x against main on a second gfx1250 part, jhinpan's bb146e1 answered the ISA cause and asked for a re-run, and no re-run has been posted (the review state is DISMISSED, not approved). The body itself says the 131-row sweep behind the rule "was not re-tuned" after the two changes that followed it. Impact: on a higher-latency gfx1250 part, M between N and 8192 at N = 1024–2048 silently takes the slower kernel — a regression against main on shapes main handles well. Reviewer should ask zhiding512 to re-measure the two rows and the M = N / M = N+1 pairs at bb146e1 before merge, and hold the fallback (one workgroup per row for N > 768 at M ≤ 8192) ready.tests/kernels/test_rmsnorm.py:1926skips the new NaN test unlesshas_hw_cvt_pk_bf16_f32(get_rocm_arch()), which admits gfx95x and gfx1250. On gfx950 — the only PR runner it admits — the predicate this PR replaced already returned True on main, so the test passes identically on the parent commit there; on gfx1250, where the author reports all 8 cases failing on main, no CI job exists. Impact: the three green GPU ticks are a skip or an unchanged pass for every line this PR adds, and the regression test pins nothing that CI can check. It still has value as a forward guard on gfx950. Author must attach the gfx1250 run showing the 8 cases failing on the parent and passing at bb146e1, since no runner can reproduce it.📝 [inferred]
kernels/norm/rmsnorm_kernel.py:1005hoists x, residual and gamma in the fused kernel but, unlike its two siblings at:410and:706, emits nofx.rocdl.sched_barrier(0). The PR's own comment at:99says hoisting alone is not enough because the scheduler sinks the loads back below the first wait; x and residual are anchored by tile 0's add, but gamma is not consumed until the pass-2 loop at:1055, which is exactly that case. Impact: possible speed left on the table in the fused path, not a correctness issue. Reviewer should ask whether the barrier was tried in the fused kernel and measured worse, or simply not tried.📝 [verified] The new capability predicate at
kernels/norm/rmsnorm_common.py:118is not adopted bykernels/norm/layernorm_kernel.py, which imports the same module yet keeps three hand-copied(arch == "gfx950") or str(arch).startswith("gfx95")expressions (lines 102, 506, 1074) and its own_to_elem_vecat line 68. The PR body declares this out of scope. Impact: after merge, gfx1250 RMSNorm preserves FP32-weight NaN payloads in bf16 output while gfx1250 LayerNorm still flushes payloads ≥ 0x7FFF8000 to ±0 — two norms on one chip with different NaN semantics. Reviewer should ask for a follow-up issue, and whether the predicate belongs next toget_warp_sizeinpython/flydsl/runtime/device.pyrather than in a kernel helper.Independent reader's corrections (not yet applied to the findings above):
This run predates the later commits, so it shows the refuter's corrections as they are now listed, but not the stricter prompts.
Trying it
Not included
python3 -m pytest .claude/skills/review-pr/test_triage.pyruns locally only:.claude/is outsidescripts/run_tests.sh, and a selftest workflow would trigger the GPU matrix on every skill edit.