[Tools] Add architecture-general ISA resource diff tool - #1015
Conversation
81873df to
3b3e9e8
Compare
3b3e9e8 to
ea7fe17
Compare
| vgpr_count=285, | ||
| ), | ||
| # An RDNA kernel: no `.agpr_count` anywhere, and LDS spelled ds_load/ds_store. | ||
| "rdna": dict( |
There was a problem hiding this comment.
gfx1250 is based on cdna5 arch.
There was a problem hiding this comment.
Done
You're right, and thanks — the label was wrong. Fixed in 8ef12c8.
Rather than relabel it cdna5, I dropped the microarchitecture names from this code entirely, because the parser never branches on the product family. It branches on two ISA properties.
8118d2c to
b0f3f3d
Compare
jhinpan
left a comment
There was a problem hiding this comment.
Requesting changes because the new fail-closed exit-code contract has two paths that still return RESULT: OK for inputs the tool cannot safely compare. I reproduced both on head b0f3f3dd: LLVM's normal *-unknown-gfx* target spelling bypasses architecture comparison, and an unparseable *final_isa.s is omitted from a multi-kernel tree as only a warning. The focused unit test, repository checks, Python style check, and legacy-spelling scan otherwise pass.
| """Split ``amdgcn-amd-amdhsa--gfx950:sramecc+:xnack+`` into its parts.""" | ||
| if not target_id: | ||
| return Arch() | ||
| tail = target_id.rsplit("--", 1)[-1] |
There was a problem hiding this comment.
[blocking] This split only recognizes the abbreviated amdgcn-amd-amdhsa--gfx950 form used by the synthetic test. LLVM also emits target IDs such as amdgcn-amd-amdhsa-unknown-gfx950; for those, parse_target_id() returns processor=None. Because compare_snapshots() rejects an architecture mismatch only when both processors are known, an unknown-gfx942 snapshot compared with unknown-gfx950 currently returns exit 0 / RESULT: OK. Please extract the trailing concrete gfx... processor from both LLVM spellings (and fail closed when it is genuinely unavailable), with a regression test proving a 942/950 mismatch exits 2.
There was a problem hiding this comment.
Done in b491815e. Processor now comes from the last - separated field, so both spellings parse.
The guard needed fixing too, otherwise the parse fix alone would not have closed it: with both processors None, processor != processor is false, so it never reached the known check. It now blocks whenever a processor is unidentified and the target IDs differ.
Tests: both spellings, 942 vs 950 → exit 2, gfx11-generic vs gfx12-generic → exit 2.
|
|
||
| records = parse_isa(chosen) | ||
| if not records: | ||
| warnings.append(f"{chosen}: not an LLVM AMDHSA dump (no {METADATA_BEGIN}); skipped") |
There was a problem hiding this comment.
[blocking] A discovered *final_isa.s that yields no records is omitted as a warning, so the comparison can become silently partial. I reproduced this with one valid dump plus one empty/truncated final_isa.s on each side: both bad files are skipped and diff returns exit 0 / RESULT: OK for the remaining kernel. That contradicts the stated fail-closed contract. Please record this as a problem (or retain a blocked sentinel entry) so any final-ISA file the tool cannot parse forces exit 2, and cover the mixed valid/invalid tree case.
There was a problem hiding this comment.
Done in b491815e. Recorded as a problem instead of a warning, so it reaches exit 2 — and summarize/snapshot fail closed on it too, via Snapshot.trustworthy.
Took the problem over a sentinel entry: a sentinel on both sides yields one blocked record per metric, and on one side it degrades into ONLY IN AFTER.
Test covers the mixed valid/invalid tree, with the healthy half asserted to exit 0 on its own first.
| always a count line and a `RESULT:` verdict that matches the exit code exactly. | ||
|
|
||
| **Columns marked `*` are regression triggers**; the rest are context. The full | ||
| column reference is in `docs/testing_benchmarking_guide.md` §"Compare per-kernel |
There was a problem hiding this comment.
This points readers to a Compare per-kernel ISA resources section that is not present in docs/testing_benchmarking_guide.md; this PR only adds the script to that guide's source-file table. Please either add the promised column reference or link to the documentation that actually defines these columns.
There was a problem hiding this comment.
Done in b491815e. Dropped the dangling reference and put the column table in the skill itself.
docs/kernel_tuning_guide.md:462 already pointed here for the full column reference, so the two files pointed at each other while it existed in neither. Defining it here makes that pointer true, rather than adding an ISA column reference to a testing guide.
b0f3f3d to
b491815
Compare
|
@jhinpan @jli-melchior |
jhinpan
left a comment
There was a problem hiding this comment.
The three findings from b0f3f3d are fixed, but the fail-closed contract still has five independently reproduced paths that report trustworthy data or RESULT: OK after losing information. The focused unit tests and repository checks pass on exact head b491815; the inline findings are additional trustworthiness gaps in the new tool.
| f"architecture differs ({before_arch.processor} vs " | ||
| f"{after_arch.processor}); resource counts are not comparable" | ||
| ) | ||
| elif before_arch.target_id != after_arch.target_id: |
There was a problem hiding this comment.
[blocking] Processor equality does not prove the target modes are comparable. I changed only the suffixes to gfx942:xnack+ and gfx942:xnack-; this branch returned exit 0 / RESULT: OK. Removing both target declarations also returns exit 0. These modes can change code generation and resource use, so please normalize the empty-vs-unknown environment spelling but require a nonempty processor and identical feature sets, otherwise exit 2. Add regressions for differing features and two missing targets.
There was a problem hiding this comment.
Done in aa9b7307. Comparability is now decided on the parsed processor and feature set instead of the raw target ID string, so the environment field normalizes away and xnack/sramecc no longer slip through. Features are sorted on parse, since LLVM's ordering is not a contract.
Both halves confirmed: gfx942:xnack+ vs gfx942:xnack- → exit 2, xnack+ vs sramecc+:xnack+ → exit 2, no target directive on either side → exit 2, and amdgcn-amd-amdhsa--gfx942 vs amdgcn-amd-amdhsa-unknown-gfx942 stays exit 0.
One consequence worth naming: requiring a nonempty processor also refuses gfx11-generic against itself, since RE_PROC does not recognize it. That is the right default — without a processor the scratch/spill applicability is undecidable — and if generic targets ever matter here the fix is to teach RE_PROC about them, not to loosen the verdict. Pinned in the tests either way.
| records = {} | ||
| for entry in scan.entries: | ||
| name, name_problems = _kernel_name(entry) | ||
| if not name: |
There was a problem hiding this comment.
[blocking] This silently drops a metadata entry with neither .name nor .symbol; the resulting snapshot still has trustworthy=True. records[name] = ... also lets a later duplicate name overwrite an earlier record, so a stale duplicate can hide a changed counter. Treat missing and duplicate kernel identities as snapshot problems that force exit 2, and cover both mutations.
There was a problem hiding this comment.
Done in aa9b7307. Both are now snapshot problems.
The identity-less case was a plain bug: _kernel_name() already returned "metadata entry has neither .name nor .symbol" and the caller discarded it before continue. parse_isa() now returns (records, file problems) so there is a place for a fault that belongs to the file rather than to a record; collect() folds them into Snapshot.problems.
Duplicates keep the first entry rather than the last and record the collision, so a stale duplicate can no longer overwrite a changed counter silently.
Both regressions splice a second metadata entry into an otherwise healthy file — a single-entry file already raises on losing its only kernel, which hides the case you reported.
| if raw is None: | ||
| return Cell.unparsed(f"metadata field .{metric.field} is absent") | ||
| try: | ||
| return Cell.of(int(raw)) |
There was a problem hiding this comment.
[blocking] Resource counters are non-negative, but int(raw) accepts impossible values. Changing .vgpr_count from 285 to -1 was classified as an improvement and returned exit 0. Validate the metric domain and make any negative resource count untrustworthy instead of comparable.
There was a problem hiding this comment.
Done in aa9b7307. Negative values are rejected in both _metadata_cell() and _symbol_cell(), which makes the metric unparsed and forces exit 2.
Agreed this was the worst of the five: every metric here is a count or a byte size, so a negative one means the dump is malformed, and rendering the drop toward it as an improvement is the one verdict the tool must never invent. Regression covers .vgpr_count: -1 and a negative .set symbol.
| path = Path(path) | ||
| # Assembly is ASCII in practice. Replacing a stray byte degrades one instruction | ||
| # count instead of aborting the run with a traceback and a misleading exit code. | ||
| text = path.read_text(encoding="utf-8", errors="replace") |
There was a problem hiding this comment.
[blocking] Replacement decoding can silently erase an instruction mnemonic. Corrupting one byte in one ds_read changed the reported count from 3 to 2 while the snapshot remained trustworthy with no problems. Decode strictly, or detect replacement characters and force exit 2, rather than publishing partial instruction counts.
There was a problem hiding this comment.
Done in aa9b7307. Kept the lenient decode — a traceback reports worse than exit 2 does — but the file is now decoded strictly first, and a UnicodeDecodeError is recorded as a file problem before falling back to errors="replace". So the replacement is admitted rather than counted.
Went with strict-then-fall-back over scanning for U+FFFD, which would also fire on a file that legitimately contains that character. Regression corrupts one byte inside a ds_read mnemonic and asserts both halves: the count really does drop to 2, and the snapshot is no longer trustworthy.
| if not isinstance(value, int) or isinstance(value, bool): | ||
| raise SnapshotError(f"{where}.value must be an integer") | ||
| return Cell.of(value) | ||
| if state in (NA, UNPARSED): |
There was a problem hiding this comment.
[blocking] Schema v2 accepts n/a for every metric. Setting mandatory vgpr to n/a on both JSON inputs bypasses comparison and returns exit 0. Validate state applicability by metric/architecture: mandatory register, spill, scratch-byte, and static-LDS counters cannot be n/a; reserve it only for quantities that genuinely do not exist on that target.
There was a problem hiding this comment.
Declining this one, unlike the other four.
n/a is only ever produced by _instruction_cells(), for scratch_store/scratch_load on a target that spills through buffer_*. No path in this tool — or in LLVM — emits n/a for vgpr, and Cell.from_json() already validates the state enum, the value type and the reason type. Reaching the case you describe means hand-editing the tool's own serialized output, which is not a trustworthiness gap in the data path the way the other four are: those all start from a file LLVM or the filesystem produced.
The prescribed fix also has a cost I would rather not pay. "Validate state applicability by metric/architecture" puts a second copy of the applicability rules in the loader, so spills_via_scratch() and the JSON validator would have to be kept in agreement forever, and the first time they drift the tool refuses a snapshot it wrote itself.
Happy to reconsider if you can show a snapshot the tool actually produces that carries n/a on a mandatory metric, or a second producer of this schema — either would move it into the same class as the rest.
b491815 to
aa9b730
Compare
|
@jhinpan |
aa9b730 to
a3e74b4
Compare
jhinpan
left a comment
There was a problem hiding this comment.
Reviewed current head a3e74b4 in three passes (parser/data integrity; CLI/schema/adversarial inputs; tests/docs/CI). The focused unit suite passes 9/9, and check_repo plus Python style both pass. The Navi CI failure is an unrelated runner OOM (only 122 MiB free). However, the reusable JSON path still violates the fail-closed contract: the previously reported case where both snapshots mark a mandatory metric as n/a still returns RESULT: OK, and Cell.from_json() also accepts negative resource values, so a 10 -> -1 VGPR change is reported as an improvement with exit 0. Please make snapshot loading validate metric state/domain and add CLI-level JSON regressions for both cases. No GPU run was needed because this PR is compile-only tooling and both failures reproduce deterministically through its CLI.
| value = raw.get("value") | ||
| if not isinstance(value, int) or isinstance(value, bool): | ||
| raise SnapshotError(f"{where}.value must be an integer") | ||
| return Cell.of(value) |
There was a problem hiding this comment.
[blocking] The assembly parser now rejects negative counters, but the reusable JSON decoder still accepts them. I reproduced a schema-v2 snapshot with vgpr=-1: load_snapshot() marked it trustworthy and diffing 10 -> -1 returned exit 0 / RESULT: OK. Please reject negative VALUE cells during snapshot loading (and add a CLI-level JSON regression). This is the same fail-closed boundary as the still-unresolved mandatory-n/a case, where two snapshots both declaring vgpr as n/a also return OK.
There was a problem hiding this comment.
Split verdict on the two halves.
Negative VALUE cells on load — accepted, fixed in 7eff378a. You are right that this was an asymmetry I introduced: I put the domain check in _metadata_cell()/_symbol_cell() and left Cell.from_json() without it, for the same Cell type and the same invariant. Non-negativity belongs to the metric, not to the parser that happened to read it. Cell.from_json() now raises SnapshotError on a negative value, consistent with how it already rejects a wrong type there.
CLI-level regression added as asked: diff ten.json negative.json → exit 2, with a healthy JSON pair asserted at exit 0 first so the 2 is attributable. Mutation-checked — disabling the new branch fails exactly that test.
Mandatory n/a — still declining, for the reason given last round, which this review does not address. The distinction is domain versus applicability. Non-negativity is intrinsic to the value and is one comparison in the place the value is constructed, which is why I took it. Which metrics may be n/a is a property of the target, derived by _instruction_cells() from spills_via_scratch(); asserting it in the loader means a second copy of that rule, and the first time the two drift the tool refuses a snapshot it wrote itself.
I asked for one of two things to move it into the same class as the rest: a snapshot the tool actually produces that carries n/a on a mandatory metric, or a second producer of this schema. Re-running the same hand-edited input is not either of those — Cell.na() is called in exactly one place, for scratch_store/scratch_load, and no path in this tool or in LLVM writes n/a for vgpr. If you have a concrete producer in mind I will take it.
Unrelated but worth flagging: this review was against a3e74b4, which was a force-push of the same commit onto a newer main from another machine. I rebased onto it rather than over it, so nothing from that push was dropped — the only delta from a3e74b4 is the fix above.
Compare per-kernel register, spill, scratch, and LDS usage between two FLYDSL_DUMP_IR dump directories or JSON snapshots, exposing resource regressions that functional tests do not surface. To work outside CDNA, read register counts from the per-kernel `.set <kernel>.num_vgpr`/`.num_agpr` symbols that LLVM emits on every AMDGPU target, rather than the CDNA-only `.agpr_count` metadata field, and count LDS traffic under both the `ds_read` and the gfx11+ `ds_load` spelling. Take the processor and the feature set from the target ID, normalizing the triple's environment field, which is spelled either empty or `unknown` for one and the same target. Report each metric as a value, as not applicable, or as unparsed, and exit 0, 1, or 2 for no regression, a regression, or an untrustworthy result. Fail closed on anything that would otherwise answer from a partial comparison: an unparsed or impossible metric, a dump file that does not parse or decode, a kernel entry with no identity or a duplicated one, and two sides whose targets are not provably the same. Covered by a backend-agnostic test over all three parser axes and exposed to agents as the `isa-resource-diff` skill.
a3e74b4 to
7eff378
Compare
Compare per-kernel register, spill, scratch, and LDS usage between two
FLYDSL_DUMP_IR dump directories or JSON snapshots, to expose resource
changes that functional tests do not surface.
Read register counts from the per-kernel
.set <kernel>.num_vgprand.num_agprsymbols, which LLVM emits on every AMDGPU target, ratherthan the CDNA-only
.agpr_countmetadata field, and count LDS trafficunder either spelling, since gfx11 renamed ds_read to ds_load. Both are
required for the tool to work outside CDNA.
Report each metric as a value, as not applicable, or as unparsed, and
block a comparison only on the last. Make
vgprthe single VGPR-familytrigger, since the metadata total already includes the accumulators on
gfx90a and later. Exit 0, 1, or 2 for no regression, a regression, or an
untrustworthy result; a crash never reports as 1.
Covered by a backend-agnostic test that generates a CDNA and an RDNA dump
shape, and exposed to agents as the
isa-resource-diffskill.