Skip to content

[Skills] Add PR review and performance-regression validation skills - #1083

Draft
zhiding512 wants to merge 2 commits into
mainfrom
skills/pr-review-and-perf-validation
Draft

[Skills] Add PR review and performance-regression validation skills#1083
zhiding512 wants to merge 2 commits into
mainfrom
skills/pr-review-and-perf-validation

Conversation

@zhiding512

Copy link
Copy Markdown
Collaborator

Summary

Adds two skills under .claude/skills/:

  • review-pr — a static advisory reviewer whose rules are derived from this repository's review history, not ported from another project.
  • validate-kernel-pr — a deterministic executor that runs base and head and produces a machine-checkable validation_report.json, including a performance stage.

Draft: this is tooling that applies to the whole repo, so I would like agreement on the rule set and on the perf gate's decision rule before it is treated as usable.

Motivation

FlyDSL has no automated gate on performance. scripts/compare_benchmark.py prints ratios and ends with:

    print("\nBenchmark comparison report completed.")
    return 0

return 0, unconditionally. A regression cannot turn a PR red, and nothing else in the pipeline checks speed — the numbers land in a job log nobody is required to read.

#1009 is what that costs: a default-off trait placed in front of the bf16 XCD remap shipped a silent regression that lived in main for two weeks. Restoring the mapping recovered +17.4% at S=180,180 and +19.4% at S=239,580, with bit-identical output.

A second motivation is that the failure modes here are mostly quiet. Of ~30 defects reconstructed from merged bugfix PRs, 11 returned silently wrong numbers, 5 were silent perf regressions, 2 were tests or benchmarks that lied, and only 6 crashed. A checklist for this repo has to be weighted toward what fails without saying anything.

Changes

review-pr

Rules were mined from 796 human inline review comments (Copilot's 1134 filtered out) and the diffs of ~40 merged bugfix PRs. Every rule cites a real PR. Categories reflect what actually breaks here:

Category Representative evidence
JIT cache-key direction over-keying breaks AOT reuse (#624); a runtime value leaking into the key (#556); a new trait missing from cache_tag (#1020)
Gate and flag threading validation bypassed by a new branch (#1020, #844); a default-off trait orphaning sibling builders (#1009); a surviving False and (#650)
Architecture capability wave size derived from is_rdna_arch — gfx1250 is wave32 and not matched by RDNA prefixes (#1024); prefix too wide (#544, #943)
Index width / ABI fx.Int64 widening (#1064); flattening before launch, where _LayoutPlan packs shapes as int32 (#1020)
Data-movement math truncating division in a copy decomposition (#1064, #1007); atom width vs register vector width (#650, #564); a mask guarding fewer dims than the address fuses (#969)
Numerics fx.* wrapper missing @dsl_math_wrap_result, costing 11.1–11.9% silently (#1035); ballot body assuming a per-lane predicate (#1033)
Layout algebra adaptor value/attr mismatch (#1052); getValue() without isStatic() (#926); slice not moving the base (#707)
Tests and benchmarks a suite that cannot fail (#1056, #1033, #899); a parser keeping the last match (#654)

Two things are deliberate:

scan_flydsl_diff.py turns the mechanical checks into a deterministic candidate list, so a review works a fixed set of sites rather than whatever the model happened to notice.

validate-kernel-pr

Stages: merge_sim, gpu_claim, runtime_compat, test_policy, correctness, perf, diff_scan.

The perf stage is the point. Design is A/B/A interleaved with the noise floor measured during the run:

  • Sandwiching head between two base runs cancels monotonic drift. Running all of base then all of head charges every clock ramp to the patch.
  • The two base runs are a genuine A/A control; their disagreement is this machine's noise floor for that row, measured now rather than guessed.

A fixed threshold cannot work here, and run_benchmark.sh already says why:

the widest tier at M=64 fills 0.25 workgroups per CU on a 256-CU gfx950 and swings 24% run to run

A 5% gate would flag that row on roughly every other run.

Two FlyDSL-specific hazards are handled explicitly:

  • Warm-cache measurement. When the patch touches lib/, include/, python/flydsl/, kernels/common/ or tools/, the JIT key does not move with the change and a warm cache serves the previous kernel — a perfectly reproducible measurement of the wrong binary. The executor forces FLYDSL_RUNTIME_ENABLE_CACHE=0 and records why. Base and head also get separate cache directories, so base cannot serve head.
  • Labelled metric parsing. fix(run_benchmark): report base norm bandwidth, not the last variant (layernorm 1.69 → 5.6 TB/s) #654 kept the last regex match and reported layernorm at 1.69 TB/s for months against a real 5.6 — and the current-vs-main gate could not catch it, because main was mislabelled identically.

Testing

python3 -m pytest .claude/skills/validate-kernel-pr/tests/test_validator.py -q   # 23 passed
python3 scripts/check_repo.py                                                    # all checks pass
bash scripts/check_python_style.sh --base origin/main --head HEAD                # clean

Every stage has been observed failing on a seeded defect and passing on a matched control. That pairing is the point: a stage only ever observed passing is decoration, not a check.

Property Seeded defect Negative control
Regression detected head 20% / 6% slower identical code, 12 seeds, stays green
Noise not mistaken for signal 24% run-to-run noise, 12 seeds, never blocks
Drift cancelled real regression under 3%/round drift drift alone is not a regression
Unresolvable case reported honestly 5% loss under 24% noise reported unchanged, not a coin flip
Direction correct (#848 shipped inverted columns) throughput and latency both ways
Row labels preserved (#654) two rows, same metric last row must not overwrite the first
Incomplete run cannot claim PASS perf skipped all stages passed

Live A/B/A run through real subprocesses, seeded −20% on one row:

perf stage: fail | 1 row(s) regressed beyond the measured noise floor
verdict   : BLOCK
  gemm|4096,4096,4096|bf16      unchanged      +0.0%  noise_floor= 3.0%  control_dev= 0.0%
  layernorm|32768,8192|bf16     regression    -19.5%  noise_floor= 3.3%  control_dev= 2.4%

Also run end-to-end against a real PR (#1077) on this machine, where it correctly returned INCONCLUSIVE rather than blaming the author: the local prebuilt runtime is 0.3.1 while the checkout is 0.3.2, so every kernel import dies on cannot import name 'get_warp_size' — a symbol #1024 added. runtime_compat classifies that as an environment fact and skips correctness and perf instead of filing a red result.

The agent-docs checker also caught three drifted paths in my own skill text (including a LayoutUtils.cpp that does not exist); fixed in the second commit.

Performance

No runtime code is touched — this adds tooling only, so there is no kernel performance impact.

Dependencies

  • No new third-party dependencies. Uses gh, git, amd-smi and the stdlib.

Breaking Changes

None. Additive; no existing skill, script or workflow is modified.

Known limitations

Stated in the skill rather than implied:

  • No --pr N orchestration. The caller creates the worktree and names --tests / --bench-cmd; an irrelevant target can still produce PASS. The report names both so a reviewer can reject that evidence.
  • No LLVM-pin-change detection. If a patch moves the pin, both sides need rebuilding (cf. [CI] Build the benchmark baseline wheel against the base commit's LLVM #1071) or the comparison is meaningless.
  • No gfx1250 coverage — no runner in the CI matrix reports gfx1250, and this executor cannot cover an architecture it has no device for.

Review asks

  1. Are the rule severities calibrated the way you would want, particularly the cache-key and gate-threading families?
  2. Is the perf decision rule (A/A control as the noise floor, with a 3% floor under it) the right shape, and are the default 5 rounds enough for the benchmark set you care about?
  3. Should the perf stage eventually run in CI, or stay a local/manual gate?

Made with Cursor

FlyDSL has no automated gate on performance. scripts/compare_benchmark.py
prints ratios and ends with an unconditional `return 0`, so a regression
cannot turn a PR red, and nothing else in the pipeline checks speed. PR #1009
is what that costs: a default-off trait placed in front of the bf16 XCD remap
shipped a silent regression that lived in main for two weeks, and restoring
the mapping recovered +17.4% and +19.4% at two production sequence lengths
with bit-identical output.

Add two skills under .claude/skills/.

review-pr is a static advisory reviewer. Its rules are derived from this
repository's own history rather than ported from another project: 796 human
inline review comments (Copilot filtered out) and the diffs of roughly forty
merged bugfix PRs. Every rule cites a real PR. The categories reflect what
actually fails here -- JIT cache-key direction (#624, #556, #1020),
architecture capability derived from a family predicate (#1024), index width
at the ABI boundary (#1064, #1020), truncating division in copy decomposition
(#1064, #1007), fastmath wrappers missing @dsl_math_wrap_result (#1035),
layout algebra invariants (#1052, #926, #707), and tests or benchmarks that
cannot fail (#1056, #1033, #654). Of the defects reconstructed, eleven
returned silently wrong numbers and only six crashed, so the checklist is
weighted toward silent failure.

scan_flydsl_diff.py turns the mechanical checks into a deterministic candidate
list, so a review works a fixed set of sites instead of whatever the model
happened to notice. Each of its categories was confirmed to fire on real
pre-fix code.

validate-kernel-pr is the deterministic executor. Its perf stage runs base and
head A/B/A on one verified-idle GPU: sandwiching head between two base runs
cancels monotonic drift, and the two base runs form an A/A control whose
disagreement is the noise floor, measured during the run rather than guessed.
A fixed threshold cannot work here -- run_benchmark.sh already documents a
softmax-backward tier that swings 24% run to run at low occupancy.

The executor also forces FLYDSL_RUNTIME_ENABLE_CACHE=0 when the patch touches
paths the JIT key does not track, since a warm cache would otherwise serve the
previous kernel and produce a reproducible measurement of the wrong binary,
and it isolates the two sides' cache directories so base cannot serve head.

Every stage has been observed failing on a seeded defect and passing on a
matched control; that pairing is the point, since a stage only ever observed
passing is decoration. 23 regression tests cover regression detection, noise
rejection across twelve seeds, drift cancellation, metric direction (#848
shipped inverted speedup columns), row-label preservation (#654 kept the last
regex match and mislabelled layernorm for months), and the rule that an
incomplete run can never report PASS.

Signed-off-by: zhimding <zhimding@amd.com>
The repository's own agent-docs checker caught three drifted paths in the two
new skills:

- lib/Dialect/Fly/Utils/LayoutUtils.cpp does not exist. The layout utilities
  are IntTupleUtils.cpp and NormalForm.cpp; IntTupleUtils.cpp is where the
  crd2idx fix in #1052 actually landed.
- The brace form lib/Dialect/FlyROCDL/{CDNA3,...}/ is not a resolvable path;
  name the parent directory and list the families in prose instead.
- The validator's regression suite is at
  .claude/skills/validate-kernel-pr/tests/test_validator.py, not tests/.

Also exclude .claude/ from scan_flydsl_diff.py: skill prose and test fixtures
quote the scanner's own patterns literally, so any PR touching the skills lit
up every category. Verified the nine seeded-defect categories still fire.

Signed-off-by: zhimding <zhimding@amd.com>
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.

1 participant