Repository navigation
feat(rocke) : Ask the compiler which intrinsics each GPU can actually run, and commit the answer - #12269
Conversation
✅ All Checks Passed — Ready for Review
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
|
🎉 All checks passed! This PR is ready for review. |
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds toolchain-probed intrinsic availability artifacts and gates for LLVM/GPU compatibility, while removing invalid declarations from both Python and C++ tables.
Changes:
- Adds arch-domain generation, validation, and committed LLVM columns.
- Adds emitted-IR compile/link validity checking.
- Documents the multi-toolchain and architecture-axis strategy.
File summaries
| File | Description |
|---|---|
| dnn-providers/hip-kernel-provider/rocke/TESTING.md | Updated as part of this pull request. |
| dnn-providers/hip-kernel-provider/rocke/platform/tools/gen_arch_domain.py | Updated as part of this pull request. |
| dnn-providers/hip-kernel-provider/rocke/platform/tools/check_ir_validity.py | Updated as part of this pull request. |
| dnn-providers/hip-kernel-provider/rocke/platform/tests/run_all.py | Updated as part of this pull request. |
| dnn-providers/hip-kernel-provider/rocke/platform/tests/core/test_arch_domain_artifact.py | Updated as part of this pull request. |
| dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/core/lower_llvm.py | Updated as part of this pull request. |
| dnn-providers/hip-kernel-provider/rocke/platform/dsl_docs/development/multi_toolchain_strategy.md | Updated as part of this pull request. |
| dnn-providers/hip-kernel-provider/rocke/platform/dsl_docs/development/arch_axis_proposal.md | Updated as part of this pull request. |
| dnn-providers/hip-kernel-provider/rocke/platform/cpp/core/lower_llvm/data.cpp | Updated as part of this pull request. |
Review details
Suppressed comments (6)
dnn-providers/hip-kernel-provider/rocke/platform/dsl_docs/development/arch_axis_proposal.md:380
- This status table says
KNOWN_BADremains empty, but the new validity gate defines two committedKNOWN_BADentries inplatform/tools/check_ir_validity.py:81-90. The document therefore misstates the current state of the gating policy; either update this row to acknowledge the allowlist or change the gate and its acceptance criteria consistently.
| `KNOWN_BAD` | stays empty; arch-unavailable keys become build errors, not allowlist entries |
dnn-providers/hip-kernel-provider/rocke/platform/dsl_docs/development/arch_axis_proposal.md:305
- The stated sweep size is inconsistent with the committed data: the llvm22 artifact has 1,169 cells over seven wired arches, which is 167 keys, not 148. Please update this design record or remove the fixed count so it cannot drift again.
Cost, measured: 148 keys × 7 arches on `llvm22`, ~15 s wall across cores. Cheap
enough to run in CI, not just on demand — which is why the gate is a test rather
than a nightly.
dnn-providers/hip-kernel-provider/rocke/platform/tests/run_all.py:192
run_all.py --onlyis documented as a byte-identity family filter, but this forwards the same substring tocheck_ir_validity.py, which matches case IDs instead. A valid filter such as--only mfmacan match the identity gate while matching no corpus cases, causing the new gate'sFATAL: selection matched no casesexit and making the documented partial runner fail unexpectedly. Use a separate filter contract or do not forward this option to the IR gate.
if args.only:
ir_gate += ["--only", args.only]
dnn-providers/hip-kernel-provider/rocke/platform/tools/check_ir_validity.py:278
- The tool lookup deliberately falls back to
PATH, but this gate never checks the selected clang's identity against the native flavor used bycurrent_flavor()/comgr. WithROCKE_LLVM_BINorPATHpointing at another LLVM, the gate can compile with a different intrinsic set and report a result for the wrong toolchain; the generator already rejects this mismatch, so apply the same provenance check here or fail closed.
clang = _llvm_tool("clang")
opt = None if args.no_verify else _llvm_tool("opt")
dnn-providers/hip-kernel-provider/rocke/platform/tools/gen_arch_domain.py:634
- If every literal retry returns the verifier's
immarg operand has non-immediate parameterdiagnostic,settledremainsNoneand this falls back to the originalarch_absentresult. That records a probe/module defect as a target negative, contrary to the comment above and to the six-status contract; preserve the last retry result (typicallyprobe_error) when no settled result exists.
if settled is None and last and last[0] == STATUS_ARCH_ABSENT:
settled = last
status, evidence = settled or (status, evidence)
dnn-providers/hip-kernel-provider/rocke/platform/tools/gen_arch_domain.py:411
- The generator documentation says there are 149 keys, but the committed llvm22 column has 1,169
(key, arch)cells across seven wired arches, i.e. 167 keys. This is exactly the kind of fixed-count drift the PR says the artifact should prevent; update the statement or describe the count dynamically.
reported ok for a target where the instruction does not exist. Nine of the
149 keys are foldable this way. -O0 keeps the call alive to ISel, which is
- Files reviewed: 9/11 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Review of head
Validation: 9 structural/CPU tests passed, including 2,352 subtests, and 11 AMDGPU lowering tests passed. A focused LLVM 20 compile/link probe also passed. Reviewed the GitHub diff and isolated exact-head sources. Full regeneration, C++ byte-identity builds, and GPU numerical tests were not run. Assisted-by: codex / gpt-6-astra / medium |
cgmillette
left a comment
There was a problem hiding this comment.
PR #12269 — AICK-2272 · head b2966e72
Verdict:
| # | Location | Finding | Sev |
|---|---|---|---|
| N1 | CMakeLists.txt + test_arch_domain_artifact.py |
test staged in rocke_pytest but import gen_arch_domain (tools/ not installed) + checkout-relative _DATA → errors/0 columns in provider artifact |
MED |
| N2 | check_ir_validity._compile |
omits -nogpulib the probe generator deems required (foreign-ROCm device bitcode) |
LOW |
Legend — Sev: MED/LOW. Net-new only (existing reviewer comments excluded).
…s case)
Byte-identity proves the two engines emit the same bytes; the golden proves
those bytes do not change. Neither asks the AMDGPU toolchain whether the IR is
legal, so an illegal construct emitted stably and by both engines is green in
both. This gate asks that question: lower every case in the representative
corpus and push each module through clang to a linked hsaco.
Three design points, all load-bearing:
- The oracle is a LINK, not a verify and not a codegen. A declare for an
intrinsic that does not exist passes `opt -passes=verify` AND passes
`clang -S` -- the backend silently treats the unknown llvm.* name as an
ordinary external function and emits a GOT-relative call to it. The undefined
symbol surfaces only at link. Stopping earlier is blind to the whole
fictional-intrinsic bug class (the decl table has several).
- Every compile runs in a subprocess. A backend failure is a report_fatal_error,
not an exception: compiling in-process via comgr means one bad module kills
the run and emits no report. The isolation is a correctness requirement, not a
performance choice, and it makes the per-module diagnostic free.
- Only the host's own LLVM flavor can be validated. Modules lower at any flavor
but there is no local compiler for the others; those report UNVALIDATED, never
green. With no LLVM tools at all the gate self-skips loudly (--strict makes
that a failure). Tools are located via the ROCm install rocke already resolves,
with PATH checked last -- a ROCm ships clang/opt under <root>/llvm/bin without
putting them on PATH, and the tool that is on PATH may be a different vintage
than the comgr we lower against.
Reuses rocke_ir_parity_harness.cases() as the corpus (128 cases, 14 families,
7 arches), so there is no second corpus to maintain and it grows with the
golden. 27 s for the full sweep.
Known failures live in a KNOWN_BAD allowlist, same convention and same rule as
KNOWN_VIOLATIONS in test_library_layering.py: it only shrinks. An entry that
starts compiling is itself reported as a failure, so a fix cannot leave dead
weight behind. It lands with two entries -- attention/gfx942/3d_{bf16,fp16}
_d128_b64 hit an LLVM 23 backend fatal error ("Do not know how to expand this
operator's operand"), found by this sweep. The other 126 cases compile and link.
Wired into run_all.py after the byte-identity gate (--no-ir-validity opts out)
and documented as TESTING.md section 4.4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit af980fb)
All three have zero _need() call sites in either engine, so nothing can ever
emit them -- they are table weight that outlived its caller, and two of the
three are also wrong.
global.atomic.fadd.v2bf16 no such intrinsic in shipping ROCm; the
pk-bf16 atomic was already rewritten to a
generic atomicrmw fadd <2 x bfloat> with
the fine/remote-memory metadata, and its
docstring says so. The entry is what that
fix left behind.
amdgcn.cvt.scalef32.pk.fp8.f32 declared i32 f(i32, <2 x float>, float,
amdgcn.cvt.scalef32.pk.bf8.f32 i1); LLVM wants a <2 x i16> return and 5
arguments. No lowering handler exists in
either engine, so no kernel ever hit the
mismatch. Deleting them means whoever
wires the op up writes a correct declare
rather than inheriting a broken one.
Deliberately NOT deleted, against the working notes:
global.atomic.fadd.v2f16 the notes claimed this was dead too. It is
not -- _op_memref_global_atomic_add_pk_f16
needs it, as does lower_llvm/mem.cpp. LLVM
auto-upgrades the declare away, which is
why it still compiles; making rocke emit
the atomicrmw directly is an emission
change needing a golden re-bless, so it is
recorded as a follow-up, not folded in.
arith.cvt_scalef32_pk_{fp8,bf8}_f32 the now-handler-less op names stay
registered; they live in index-parallel
arrays the serializer keys off.
Zero emitted-byte change by construction, and confirmed: golden IR anchors green
(839 passed, 154 skipped, 1711 subtests), emitted-IR validity gate GREEN at
126 OK / 2 KNOWN_BAD / 0 new failures, decl-table parity green at 169 subtests
(172 minus the three deleted). The byte-identity gate could not run on this host
-- no cmake -- but the parity check covers exactly the table edited here, and
nothing reachable changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit bcd2cf6)
…toolchain
rocke resolves an intrinsic `declare` on one axis only -- the LLVM flavor. The
target arch picks an ISA backend and never reaches the decl table, so "is this
intrinsic available on this GPU" is asked nowhere at build time. S1 of
dsl_docs/development/arch_axis_proposal.md measures that missing axis instead of
hand-maintaining it, and lands the result as data. Nothing consumes the artifact
yet; that is deliberate -- it cannot break anything, and it surfaces the defects
that justify the rest.
Two stages, because the two axes have different oracles:
* flavor, arch-free, once per key: `opt -S` resolves a recognised `llvm.*`
declare on parse (attaching attributes, remangling overloads) and leaves an
unrecognised name verbatim as an ordinary external function.
* arch, once per (key, arch) that survived: compile AND LINK a probe module.
A link, because a declare for a nonexistent intrinsic is accepted by both
`opt -passes=verify` and `clang -S`; a subprocess, because an unsupported
intrinsic reaches report_fatal_error.
Answering the flavor axis first is not just a speedup. It is the only stage that
can answer a key whose codegen crashes: an unknown `llvm.*` name with a metadata
operand is lowered as an ordinary call, and taking the alignment of a metadata
argument segfaults the backend.
Probe synthesis passes real SSA values, never poison. Poison is a valid constant
of every first-class type and so looks like the generic choice, but it makes the
probe lie -- a poison i32 operand sends raw.ptr.buffer.load.lds into a
legalisation failure on gfx942, where concrete integers lower cleanly. That one
diagnostic also gets a literal-operand re-probe, since some operands must be
immediates without the declare saying `immarg`; scoped to that diagnostic
because literals could otherwise constant-fold an unsupported intrinsic away and
manufacture an `ok`.
Non-answers are never recorded as negatives. `target_unsupported` (this clang
cannot target this arch), `toolchain_crash` (asking killed the compiler) and
`probe_error` (our module was malformed) are distinct from `arch_absent`.
A result is only meaningful against the toolchain that produced it, so the
artifact records the clang identity and the resolved flavor, and the generator
refuses to run when those two disagree.
Measured on ROCm 7.1 / LLVM 20 (flavor llvm20), 148 keys x 7 wired arches:
ok=487 arch_absent=206 name_absent=224 target_unsupported=116 toolchain_crash=3,
with zero unclassified cells. Regeneration is byte-identical, so `--check` is a
no-op gate. gfx1250 is unprobeable on this vintage by construction; every other
flavor is unvalidated here for the same reason -- one host can only ever
validate its own column.
Findings already visible in the data, all pending separate fixes: 32 declares
name-absent in llvm20 (including ds.read.tr16.b128, which no arch and no flavor
resolves), 15 whose name exists but which no wired arch can lower, and
amdgcn.permlane64, a GFX11+ instruction whose declare segfaults instruction
selection on every wave64 target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 9b88bad)
Re-validating the generator on llvm22 found three real defects.
One artifact per flavor. A host can only ever measure its own LLVM, so a
single shared file meant whichever flavor ran last silently overwrote the
others and --check failed on every machine whose toolchain differed from
the one that blessed the file -- which is exactly what happened when this
host moved from ROCm 7.1 to 7.2. The file is now named after the flavor,
and --check on a flavor with no committed column reports UNVALIDATED and
exits 0 rather than failing.
Widen the literal-operand rescue. llvm20 only ever complained that it
could not legalise a variable operand; llvm22's verifier rejects it up
front with "immarg operand has non-immediate parameter". Both diagnostics
now trigger the rescue, and it tries several literal values because a
value out of range for one operand kind is in range for another.
Take the literal sweep's answer as final. The rescue used to fall back to
the original variable-operand failure when no literal variant produced a
clean answer, which left four raw.ptr.buffer.load.lds cells unclassified.
With no variable integers left there is nothing our probe can still fix,
so a surviving legalisation failure is the target speaking: those cells
are arch_absent, matching the llvm20 column where the same key is CDNA-only.
The verifier's immarg complaint is deliberately excluded from that rule --
surviving the sweep means the non-immediate operand is not an integer,
which really is our module's fault.
llvm22 column: ok=627 arch_absent=275 name_absent=126 toolchain_crash=8,
zero unclassified, regeneration byte-identical. The eight crashes are
LLVM 22 segfaulting on cvt.scalef32.pk.f32.{bf8,fp8} for RDNA targets;
keeping them a distinct status rather than folding them into arch_absent
is what let permlane64 -- which crashed llvm20 everywhere and is ok on
llvm22 -- avoid being recorded as a wrong arch answer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 9d2d067)
…g them Regenerating the llvm20 column on ROCm 7.1 produced a different table on every run: llvm.fabs.f32 on gfx11-generic came back "Aborted" roughly one run in three and was recorded as a permanent toolchain_crash. It is not -- under -j the probes fork enough linkers to occasionally hit the process limit, and the abort is the host running out of resources, not the compiler failing on the intrinsic. The serial second pass already existed for exactly this reason and already carried the right principle in its comment -- a real result is stable under retry, a resource failure is not -- it just only applied it to probe_error. Extend it to toolchain_crash. A genuine crash reproduces serially (permlane64 on the wave64 targets still does, all three of them), so the retry separates the two without having to guess which is which. Three consecutive runs are now byte-identical, and identical to the committed llvm20 column: ok=487 arch_absent=206 name_absent=224 target_unsupported=116 toolchain_crash=3. That closes the open question from the previous commit -- the llvm20 column was blessed by the older generator, and regenerating it with the current one changes nothing, so the widened literal rescue and the final-answer rule affect llvm22 only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 95d0d01)
The probe compiled at -O3, which let the IR pipeline delete the very call
it was asking about. A module with nothing left to select links happily,
so the probe reported ok for an intrinsic the target does not have.
llvm.amdgcn.permlane64 on gfx942 is the case that found it. A kernel
argument is wave-uniform, permlane64 of a uniform value is the identity,
and LLVM 22's InstCombine folds it away; the object held no permlane64 at
all and the link succeeded. The instruction is GFX11-only and LLVM in fact
crashes on it for GFX9 -- llc, which runs no IR pipeline, segfaults in
SIInstrInfo::getInstSizeInBytes. LLVM 20 lacked the fold, so the same cell
was correctly recorded as toolchain_crash there; the llvm22 column
disagreeing with the llvm20 column is what prompted the check.
Nine of the 149 keys are foldable this way -- the cross-lane and uniform
ones (permlane64, ds.bpermute, readfirstlane, readlane) plus a few others.
Only three cells actually changed, all permlane64 on the wave64 CDNA
targets, ok -> toolchain_crash, which now matches llvm20 exactly. The
other six kept their verdicts and two of them report a clearer diagnostic
at -O0 ("Cannot select" instead of a legalisation failure).
A false ok is the dangerous direction here: it silently admits a kernel
that links but cannot run, where a false arch_absent merely breaks a
working build loudly.
llvm22: ok=624 arch_absent=275 name_absent=126 toolchain_crash=11,
regeneration byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 1818746)
The arch-domain columns are generated but nothing checked them, and a generated artifact that is not gated drifts from its generator. A stale availability table is worse than none, because it is trusted. Two halves, because they fail for different reasons and are available in different places. The structural half needs no LLVM, no GPU and no built C++ engine: it checks each committed column against the decl table it claims to describe. That is the realistic drift -- add or rename an intrinsic key and the artifact does not grow a row, it just quietly has no opinion on the one key nobody has measured, while still parsing and still looking authoritative. The regeneration half re-probes the toolchain and requires this host's column back byte-identical; it is the only check that can catch a wrong measurement rather than a missing one. A full sweep is 148 keys x 7 targets in ~15 s, so it is a test rather than a nightly. A host can only ever measure its own LLVM, so a flavor with no matching toolchain skips and never fails -- "we did not get an answer" must not be recorded as "the answer is no", which is the same rule the artifact itself follows in separating arch_absent from target_unsupported and toolchain_crash. probe_error is asserted absent rather than merely valid: it is the one status that means our own probe module was malformed, so it is a defect in the generator and must never survive into a committed column. Verified both directions: green on a clean tree, red on a fabricated cell (regeneration half) and red on a dropped key (structural half, in 0.19 s with no toolchain). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit d6144e8)
…o stories The two arch-axis documents presented a 15-cell hand-run probe table as data and described a schema that is not what shipped. Both were written on an llvm23 host, and llvm23 is the one flavor with no generated column behind it -- so nothing in them could be reproduced end to end. Two of the five hand-probed rows did not survive a second flavor. On llvm20 the ds.load.tr16.b128.* rows are name_absent rather than "Cannot select": the name does not exist in LLVM 20 at all, and ds.read.tr16.b64 on gfx1250 is target_unsupported because LLVM 20 cannot target it. That matters for the factorization argument those rows were quoted to support -- arch_domain is not independent of flavor, it is undefined wherever the name is absent, and the factorization holds only because the flavor axis is evaluated first and short-circuits. A table storing one arch set per key with no flavor column would have had to invent those cells. Corrected in both documents, with the hand table kept and labelled as the argument that motivated the generator rather than as the data. Also recorded the three ways the shipped schema departs from the proposed one (one file per flavor instead of an unvalidated marker in a shared file; no wildcard row; evidence on every non-ok cell), the two no-data statuses the three-outcome taxonomy had no bucket for, the -O0 requirement without which the optimiser deletes the call and the probe reports ok for a target that cannot run it, and the serial retry that keeps a resource failure from being committed as a compiler crash. The new epic document is the delivery breakdown: five stories, motivation first, each ending with something that was not true before. These four documents were untracked. An uncommitted document cannot be the record, which is most of why the stale claims went unnoticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 50386f9)
…asure Rebasing the table onto current develop surfaced a design flaw in the gate added alongside it. Requiring every committed column to cover every decl key means one newly wired intrinsic reds the build on every machine until someone has run all three toolchains -- recording "we did not get an answer" as "the answer is no", which is the one thing the six-status design exists to prevent. Split the check by who can act on it. A stale key -- present in the artifact, gone from the decl table -- needs no toolchain to fix, so it stays a hard failure on every column from any host. An unmeasured key can only be answered on a host running that flavor's LLVM, so it fails for this host's flavor and is a named skip elsewhere, naming the keys and the toolchain needed. Re-measure the llvm22 column against develop's decl table, which has grown 19 keys since the branch point: 167 keys, arch_absent=332 name_absent=147 ok=649 toolchain_crash=41. The llvm20 column is now those 19 keys short and reports as a skip until a ROCm 7.1 host runs the generator.
The decl table has grown from 148 keys to 167 since the llvm20 column was blessed, so the errand outstanding on Story 1 is no longer just the nine `-O0`-sensitive keys -- it is one generator run on a ROCm 7.1 host that fixes both. State the key count as a moving number rather than a constant, since quoting it as fixed is what let the gap go unnoticed.
…s `ok` `llvm.amdgcn.raw.ptr.buffer.load.async.lds` does not terminate under the llvm20 clang at `-O0` on any of the six targetable arches. The sweep had no timeout, so the probe was simply waited on; the committed llvm20 column nonetheless recorded `ok` for all six, because that column predates `-O0` becoming mandatory and at `-O3` the IR pipeline deletes the call being probed. Six cells therefore claimed an intrinsic is available on hardware where the toolchain cannot even finish compiling it -- a false `ok`, the error direction that silently admits a kernel that cannot run. Three changes, one per link in that chain: * A per-probe timeout with its own status. `toolchain_timeout` joins `target_unsupported` and `toolchain_crash` as "no data" -- never `arch_absent`, which is a claim the target cannot lower the intrinsic. A timeout retries on the same budget during the serial pass: the retry exists to remove contention, not to grant more time. * Provenance in every column. `clang` says which compiler answered, not which question it was asked, and those differ -- the same build reports `ok` or hangs for the same key depending on the optimisation level. A column now carries `generator`, `probe_cflags` and `probe_timeout_s`, and a test asserts them, so a column measured the wrong way can be told from one measured right. That is the check that would have caught this. * Host-independent crash evidence. ` (core dumped)` is a property of the host's core-dump settings, not of the crash, so committed evidence differed between machines observing the identical fault and the drift gate reported drift where there was none. Normalised away. Both columns regenerated. llvm20: the six cells above are the only status change across the 148 pre-existing keys. llvm22: zero cell changes, across both the generator change and a toolchain bump, so it needed only the new provenance fields. Two fixes to how the sweep runs, both found while regenerating: * Size the worker pool by this process's share, not the host's. The cap was `min(8, os.cpu_count())`, and `os.cpu_count()` reports the machine -- on a scheduler-pinned host that read 384 against an affinity of 2, so the sweep ran 8 workers, each spawning clang and ld.lld, against two cores. That is the load the serial-retry pass exists to undo. Now the minimum of affinity, a cgroup CPU quota (v2 then v1) and `os.cpu_count()`. The quota path takes its root as a parameter because no host here imposes a quota, and an untested fallback is not a fallback. * Run probes from the scratch directory. Probing is *expected* to crash clang, and a crash on a host with core dumps enabled dropped a multi-megabyte `core` into the worktree the sweep was run from.
… the host's `check_byte_identity.py` passed `os.cpu_count()` to `cmake --build -j`. That reports the machine, which on a scheduler-pinned node here is 384 against an affinity of 2 -- so the gate every engine change has to pass was asking ninja for 384 compile jobs it had two cores to run. The arch-domain generator had already grown the fix. Lift it into a shared `tools/_hostcaps.py` rather than copy it: affinity, then a cgroup v2/v1 CFS quota, then the host count, narrowest wins. A quota is a rate, so a fractional share rounds up to one whole worker -- rounding down would be a hang rather than a slow build. The helper keeps its existing tests, which now cover both callers. Its docstring drops the probe-specific rationale, which stays at the generator's call site where the RLIMIT_NPROC argument for the extra `min(8, ...)` cap lives; that cap is about clang's own process fan-out and does not apply here. Leading underscore because every other module under `tools/` is a CLI entry point and this one is not.
…alone `--check` compared the committed column byte for byte against a fresh run. `toolchain.clang` records the exact ROCm build that measured it, so the check red on every host whose patch level differed from the one that blessed the artifact -- which is most of them, for a reason nobody can act on. It was already red here: the llvm22 column was blessed by roc-7.2.4 and this host runs roc-7.2.0, with all 1169 cells in agreement. Compare the parsed documents instead, via `_drift`, and forgive a difference in `toolchain.clang` alone. Two independent builds of the same flavor reproducing a column cell for cell is not staleness; it is better evidence than a byte match, because it shows the column describes the flavor rather than one machine. Nothing else is forgiven, including the rest of `toolchain`. Those fields say how the question was asked rather than who answered it, and they are exactly where a wrong measurement hides -- the same clang reports `ok` or hangs for the same key depending on whether `probe_cflags` carries `-O0`, which is the defect that field exists to expose. A `generator` or `probe_timeout_s` change means the committed answers came from a method we no longer use, so those still demand a re-bless even when every cell happens to agree. Drift now also reports which cells moved instead of only that something did. Tests pin both directions: a different build is not drift; a changed status, a changed probe method with identical cells, and an unparseable column all are.
…ruction The arch-domain probe passed the literal 0 for every `immarg`. Where 0 is not a legal value of that operand, ISel has no pattern and reports `Cannot select` -- which is also what a genuinely unsupported target reports. The generator could not tell the two apart and recorded the target as incapable. `llvm.amdgcn.global.load.lds` is the case. Its third parameter is the per-lane transfer size in bytes; 0 is not one. Both committed columns said `arch_absent` on all seven targets for an intrinsic with a builder entry point, a lowering handler in both engines, and a production instance whose own comments record the resulting instructions in the emitted ISA. Three of those seven cells were wrong, on a live path. That is the dangerous direction. A false `arch_absent` breaks a build that works; the permissive failure only costs a diagnostic the link would have produced anyway. It is also the direction the first consumer of this table will promote into a hard error. The existing literal rescue cannot reach it. `literal_ints` substitutes into non-`immarg` integer arguments -- an operand that is already an immediate has nothing to substitute -- and `_wants_literals` triggers only on the two "that operand had to be an immediate" diagnostics, which an illegal *value* does not produce. There is no diagnostic that says "your operand value was out of range", so the only discriminator is to ask again with a different value. So: on `arch_absent`, and only there, re-probe with each of 1/2/4/16 in every `immarg` and take the first that links. Any legal value wins, because the question the table answers is whether the target can lower the intrinsic, not whether it can lower it with the operand we happened to pick. The sweep is built by comparing module text against the base probe, so a declare with no integer `immarg` produces no variants and costs nothing -- the common case is still one probe. The winning value lands in the cell as `probe_imm`. Without it a first-try `ok` and a fourth-candidate `ok` are indistinguishable, and re-deriving that by hand is precisely what nobody did for two columns. Both columns regenerated, on ROCm 7.1.1 and 7.2.0 respectively. Three cells move in each -- gfx90a/gfx942/gfx950 on that one key, at size 1 -- and the four RDNA targets still say `arch_absent` after the full sweep, which is the correct answer and matches the builder's own documented size domain. Everything else in both columns reproduced against a compiler build different from the one that blessed it. Generator bumped to 3: the probe semantics changed, so an older column must not be mistaken for one measured this way. Tests pin both directions. A sweep that only manufactures more `ok` cells has broken the table the other way round, and that is the failure nobody notices. No emitted bytes change -- tools, tests and data only. Byte-identity GREEN at llvm20 and llvm22, 40 families each.
`llvm23` was the one flavor with no machine-checked column behind it. A ROCm 10.0 container has the toolchain, and the generator needs clang and `opt`, not a GPU, so there was no reason for the gap other than nobody having a vehicle. The first run came back with 27 `probe_error` cells -- the status that means the generator is broken, not the toolchain. Both causes were the same thing seen from different angles: llvm23 words two diagnostics differently, and we had taught the classifier only the older wordings. `intrinsic not supported on subtarget` is a target saying outright what llvm20 and llvm22 only imply by failing to select. Twelve cells across `permlane16`, `permlanex16` and `mov.dpp8` had a clear answer and it was being thrown away. Now classified `arch_absent`, like the three failure shapes that mean the same thing. `invalid data size for load-to-LDS intrinsic; must be 1, 2, 4, 12, or 16` is the `immarg` defect, caught one stage earlier: llvm23 checks the transfer size in the verifier, before codegen. It is a complaint about our module rather than about the target, so it routes to the operand rescue. It is also the only diagnostic of the three that says what would have been acceptable, so the sweep's candidate list now matches the domain llvm23 enumerates rather than the one we inferred -- 12 added. Two consequences for the sweep. It now runs on `probe_error` as well as `arch_absent`: "our module was malformed" is exactly what it repairs. And when no candidate links, a cell that started as `probe_error` takes the first real classification the sweep produced instead of keeping its original evidence. The four RDNA targets are the case -- the default probe was malformed, but a legal size fails there too, so the honest answer is `arch_absent`, and committing `probe_error` would have reported the generator broken for a target that simply cannot lower the call. The llvm23 column: 722 `ok`, 374 `arch_absent`, 28 `name_absent`, 45 `toolchain_crash`, no `probe_error`. `global.load.lds` resolves the same way it does on the other two vintages -- CDNA at size 1, RDNA not at any size. All three columns regenerated at generator 4. llvm20 and llvm22 differ only in that number: neither had a `probe_error` cell, and the added candidate never wins ahead of 1, so nothing llvm23 taught us changes what the older vintages say. That is the result worth having -- the fix is in what we can read, not in what we measure. Tests pin both new wordings and the candidate list. Two design docs stopped claiming llvm23 has no column. No emitted bytes change. Byte-identity GREEN at llvm20 and llvm22, 40 families each.
b2966e7 to
e148bb1
Compare
|
note: when you stack PRs it affects mergeability |
Review of #12269 found the same defect in several places: a gate that could not actually ask its question reported a verdict anyway. Every fix below draws the one line the arch-domain design already draws -- "we could not ask" is never recorded as "the answer is no". Generator (tools/gen_arch_domain.py): * A missing `opt` no longer substitutes "the name exists" for the flavor-axis check. Every key would have been recorded as present without being checked, so the run now exits 1 having written nothing. No complete LLVM install ships clang without `opt`, so the pair means a partial toolchain rather than a probing host with less to offer. * Any cell still `probe_error` after the serial retry fails the run with its diagnostic printed, rather than being committed. This covers signal-killed clang and every future unrecognised wording, without misfiling an OOM kill -- a property of the host -- as a crash of the compiler. Validity gate (tools/check_ir_validity.py): * Probe at -O0. At -O3 the optimizer can delete the construct under test and leave a linkable module, which is the false `ok` this gate exists to prevent. Pinned to the generator's flag by test. * Pass -nogpulib, so the answer depends on the module under test rather than on whichever ROCm's device bitcode clang happened to find. * A target this clang cannot build (`invalid target ID`) is UNVALIDATED, not an emission failure. rocke wires arches ahead of the toolchains that can build for them, and this gate now runs by default. * A clang whose version disagrees with the flavor the corpus was lowered at is UNVALIDATED. The former `flavor != native` test compared two rocke-side values and ran before clang was even resolved, so the binary was never asked what it was. * ROCKE_LLVM_BIN is authoritative when set. Falling through on a miss let clang come from the named directory while `opt` came from the installed ROCm -- two vintages recorded as one toolchain. Runner (tests/run_all.py): * New `--only-ir`. One substring was forwarded to two tools with different matching contracts, and they read an empty selection in opposite directions -- run_diff runs zero families and reports green, check_ir_validity calls it FATAL -- so an ordinary typo produced one vacuous green and one red. Artifact and packaging: * Regenerated all three columns on their matching ROCm images. Each had drifted from the decl table: 3 stale keys and 2 unmeasured ones. All three now match exactly at 166 keys. * test_arch_domain_artifact.py resolves its data through the installed package and is excluded from the installed test tree. It imports the generator out of tools/, which is not installed, and an unresolvable import there is a collection error that aborts the whole session. clang_identity/flavor_of_clang are shared via tools/_llvm_identity.py: the two tools act on a mismatch differently on purpose, but must not disagree about what a mismatch is. Gates: artifact test 27 passed / 3548 subtests on llvm20, llvm22 and llvm23; `--check` up to date on all three; check_ir_validity GREEN on llvm20 (8 unvalidated) and llvm22 (4 unvalidated). It is RED on llvm23 for 3 gfx950 attention cases that fail only at -O0 there and pass at -O0 on both other flavors; the rejected construct is backend-selected MIR, not IR rocke emits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks all — every comment on this PR is addressed below, grouped by reviewer. Fixed 14, rebutted 1, left open with reasoning 1, plus 2 acknowledgements. All changes are in Verification for every claim below: the byte-identity gate and the emitted-IR validity gate were run at all three LLVM flavors ( @cgmilletteN1 — the artifact test breaks an installed tree. Fixed both ways you suggested. N2 — @tenpercentP1 — packaging / installed-tree failure. Same fix as N1 above. On the JSON data files specifically: the existing rules already cover them, and that is now verified rather than assumed. P2 — the default validity gate fails on targets the host compiler cannot support. Fixed. Two notes. First, on the current tip your repro is not reachable as written — an llvm23-gated instance raises FATAL before the compile stage ever runs; that is fixed in this same round, and only then does this finding become observable again. Second, the llvm20 gate is now GREEN with 8 unvalidated cases — 4 for an untargetable arch, 4 requiring a newer LLVM — which is the shape this finding asked for. P2 — missing Your repro is what made the severity clear: a On stacking this PR. Understood and noted for how this lands. Copilot
This found more than it claimed. No verdicts changed at
Clang installs its own So instead, any cell still
It is non-obvious enough to have misled a careful reader once, so the loop now carries a comment explaining why the skip does not abort it. Happy to reopen if you are seeing different behaviour on another unittest version.
One correction to the finding: the count at the time was 166, not 167. The committed columns had drifted from the decl table in both directions — 3 stale keys and 2 unmeasured ones — which is precisely the failure the artifact gate exists to catch. All three columns are regenerated and now match the decl table exactly.
Investigating it turned up a sharper version than stated.
The gate now prints The two helpers moved to a shared
The path exists exactly as described. But after regenerating all three columns, zero cells carry an |
cgmillette
left a comment
There was a problem hiding this comment.
PR #12269 — measure per-GPU intrinsic legality + commit it · Verdict: 🟢 high-quality, 🔴 blocked on conflicts
(link gate already caught 3 dead declares + 2 broken corpus cases). ~16 prior findings addressed — not duplicated.
AICK-2272.
| # | Sev | Location | Net-new finding |
|---|---|---|---|
| 1 | 🔴 | PR state | mergeable_state: dirty — conflicts; base of stacked #12441, mergeability coupled |
| 2 | 🟡 | drift gate / columns | Structural gate catches missing/stale keys but not wrong cells; only a host with that exact LLVM vintage can re-measure → ~17k committed "facts" are trust-based in normal CI |
| 3 | 🟡 | arch-domain artifact | 17k data lines + drift gate are inert until consumer #12441 (stacked, unmerged) lands; only the link gate delivers value now |
| 4 | 🟢 | gen_arch_domain.py classifier |
Diagnostic-wording list → each new LLVM vintage needs a code change or the refusal gate hard-fails that column (recurring tax) |
| 5 | 🟢 | immarg fallback (left-open item) |
Concur with author's deferral: zero cells reach it; post-retry refusal gate backstops — not a blocker |
Legend: 🔴 blocking · 🟠 should-fix · 🟡 minor-nit · 🟢 info/confirmed.
Conflicts, both resolved by keeping both sides: - rocke/platform/CMakeLists.txt: the installed-tests EXCLUDE list keeps develop's test_lds_analyzer.py and this branch's test_arch_domain_artifact.py, and the comment above it names both. - rocke/platform/tests/run_all.py: keep this branch's emitted-IR validity gate (--no-ir-validity, --only-ir) alongside develop's native-parity and ctest rework (--config); the usage header lists both sets of flags. Not a textual conflict, so not resolved here: develop added the xf32 MFMA decls (mfma.f32.16x16x8.xf32, mfma.f32.32x32x4.xf32) to both engines, and the committed intrinsic_arch_domain.<flavor>.json columns do not cover them yet, so test_arch_domain_artifact.py fails on a host that can measure its own flavor until the artifact is regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
develop added mfma.f32.16x16x8.xf32 and mfma.f32.32x32x4.xf32 to both
engines' decl tables. The committed intrinsic_arch_domain.<flavor>.json
columns predate them, so test_arch_domain_artifact.py failed on any host
that can measure its own flavor ("has no answer for 2 decl key(s)").
Regenerated each column with tools/gen_arch_domain.py on the toolchain
that column is stamped with:
llvm20 ROCm 7.1.1 clang 20.0.0git roc-7.1.1 25444
llvm22 ROCm 7.2.0 clang 22.0.0git roc-7.2.0 26014
llvm23 ROCm 10.0.0 clang 23.0.0git llvm-project 8f497e0992fb
Every regeneration reproduced every existing row unchanged, including
the toolchain_crash and toolchain_timeout rows, so the diff is purely
additive: two rows and two canonical entries per column. Both keys
measure ok on gfx942 only (and target_unsupported on gfx1250 for llvm20,
whose clang cannot target it), which agrees with the emitter's
gfx942-only guard.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tests/tools/test_run_all.py, which came from develop, drives
run_all.main() against a fake subprocess.run and isolates the native
pytest/ctest plumbing with --no-guard --no-gate. This branch added a
default-on emitted-IR validity gate to run_all.py, so after the merge
that step ran inside every one of those tests. Ten of them failed on
the merged tree; all of them passed on develop, and the file did not
exist on the branch:
- the gate's Python child carries no env, so the tests' kw["env"]
lookup over every sys.executable child raised KeyError (4 tests);
- the fake's return code leaked into main()'s status, so ctest being
correctly skipped still returned 8 (1 test);
- a fake that raises was hit before ctest discovery (1 test);
- an assertion that no Python child ran after a native-setup failure
saw the gate's child (4 tests).
Opt each argv out of the step with --no-ir-validity, as they already do
for the guard and the byte-identity gate, and say so in the module
docstring so the next default-on step does not recreate the trap.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… glue The gfx942 tiled attention kernel names grew a "fnuz" part on the Python side in #12304 (dispatcher refactor), but the C++ glue that reimplements spec.kernel_name() was never updated, so for any spec with a kv_storage_dtype the two engines named the same kernel differently -- Python emitted ..._kvfp8e4m3_fnuz_..., C++ emitted ..._kvfp8e4m3_.... That is a one-sided emission change, which the byte-identity invariant exists to catch: the gate has been reporting 70 GREEN + 1 DRIFT on gfx942_attention_tiled_3d ever since. The 2D builder carries the same token and the same omission; it is latent only because no 2D config in the corpus sets kv_storage_dtype, so fix it in the same change rather than leave a known trap behind. Python is the source of truth here, so the C++ side moves. Both engines emit "fnuz" under the *same* condition as the kv part -- presence of a kv_storage_dtype, not a test of the fp8 format -- so the tag is attached to the kv branch rather than computed separately; a comment on each site records that this is deliberate mirroring, not an inferred format check. Verified in the three ck_ub24.04 images (one toolchain per flavor): byte-identity is now GREEN 71/71 at llvm20, llvm22 and llvm23 (was 70 + 1 DRIFT at every flavor). Emitted-IR validity stays GREEN, and the platform pytest failure set is byte-identical to develop's own (8 failed / 1683 passed / 340 skipped; all 8 are environmental -- the images ship no ml_dtypes or pybind11). Co-Authored-By: Claude Code <noreply@anthropic.com>
…cke-arch-domain-table # Conflicts: # dnn-providers/hip-kernel-provider/rocke/TESTING.md # dnn-providers/hip-kernel-provider/rocke/platform/CMakeLists.txt
develop carries #12269, the squash of the arch-domain table branch this lane is stacked on. A plain merge sees the table's files as added independently on both sides, which is ten conflicts, six of them add/add. Merged with origin/users/congma/ck/rocke-arch-domain-table as the common base for that stacked content instead, which leaves two real conflicts: - TESTING.md: keep develop's reworded "Two entrypoints with different environments and selection" opener and this lane's "arch domain" step in the developer runner's chain. - platform/tests/test_rocke.py: keep develop's backend-aware rejection assertions (#12816, #12718) on test_av_b128_rejects_lds_pointer and test_s_prefetch_inst_rejects_lds_pointer, with this lane's explicit arch="gfx950" on the lowering call. develop also gained tests in files this lane never touched, so git cannot see them as conflicts, but they assume things this lane changed: - Callers that rely on the default target this lane removes raise "lowering requires an explicit gfx target" once merged. Name the target they were verified against, gfx950, which was the old default: - platform/tests/core/test_tf32.py: eight _lower_kernel_to_llvm_python calls and eight rocke_engine.lower_serialized_ir calls. Several sit inside pytest.raises(match=...) blocks, so left alone they fail on the wrong error; the ones behind importorskip("rocke_engine") only surface where the engine is built. - library/tests/test_attention_builds.py: TestAttentionDenseGfx950CodegenKnobs._lower. - platform/tests/tools/test_run_all.py drives run_all.py with every other step switched off. This lane's arch-domain gate is a new default-on step, so it ran under the test's fake subprocess.run and returned a child with no env. Add --no-arch-domain beside --no-ir-validity in the four argv lists, as the file's docstring asks. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
TheRock Submodule Bump ActivityNewest first
|
…12441) ISSUE ID : AICK-2273 # summary ## What this branch is about rocke's emitter had no way to know whether an intrinsic it declares actually exists on the architecture it is lowering for. An unknown `llvm.amdgcn.*` declare is not a parse error, not a verifier error, and not a `clang -S` error — it survives every check rocke ran and fails much later, in the backend, as `Cannot select`. Worse, the same name can be present at one LLVM flavor and absent at another, and present on one arch and absent on its neighbour. #12269 put the measurement on develop: a generator that probes the toolchain, an emitted-IR validity gate that compiles *and links* every corpus case, and three committed columns of answers (one per LLVM flavor). Nothing read them. This branch is their first consumer, plus the fixes that building a consumer forced: 1. **A reader and a warning lane** — the lowerer asks the table whenever a kernel demands an intrinsic, on both backends. 2. **Removal of the implicit `gfx950` target default** everywhere it had spread: both engines, the per-family bindings and the C wrappers, so every lowering names the target it is actually for. This is what lets the lane mean anything. 3. **A corrected table and sturdier gates**: reading the columns as a consumer showed that one of them was wrong, and that the generator and the corpus gates each measured less than they reported. Plus a long series of methodology fixes, all of which draw one line: > **A non-answer must never be recorded as a negative.** > "We could not ask" is not "the answer is no". The lane is warn-only. Making it an error is a separate change (AICK-2274). The commit list includes commits that #12269 already merged by squash; the diff against `develop` is what this PR changes. --- ## 1. The warning lane **Reader** `python/rocke/core/arch/domain.py` — validates the envelope and raises on a malformed or wrong-schema column rather than defaulting every field (which would have presented a corrupt artifact as a complete absence of data and switched the lane off in silence — the one failure the lane exists to prevent). A column that exists but cannot be read raises too; only a *missing* file means "no data". **What it reads** — `python/rocke/core/arch/data/intrinsic_arch_domain.<flavor>.json`, schema `rocke.intrinsic_arch_domain/v1`, **171 keys × 7 wired arches = 1197 cells per column**: | Flavor | `ok` | `name_absent` | `arch_absent` | no-data | |---|---|---|---|---| | llvm20 | 535 | 294 | 194 | 174 | | llvm22 | 669 | 147 | 340 | 41 | | llvm23 | 740 | 21 | 391 | 45 | One column per flavor because **a host can only measure its own flavor** — flavor is a runtime choice (`ROCKE_LLVM_FLAVOR`), so the generator can never produce all three in one run, and a gate must skip the flavors its host cannot measure rather than mark them failing. The lowerer's declaration table has 27 more keys than the columns: the 26 declarations develop added for data prefetch, workgroup clusters and TDM (#12857), and `raw.ptr.buffer.load.f16` (#12710), have not been measured yet. An unmeasured key is no data, so the lane stays silent for those intrinsics until the columns are regenerated. That is a measurement on each of the three toolchains, and it is left to a follow-up. **Status vocabulary** splits answers from non-answers: - Answers: `ok`, `name_absent`, `arch_absent` - No data: `target_unsupported`, `toolchain_crash`, `toolchain_timeout`, `probe_error` Only `arch_absent` is negative (`is_negative`). Everything else is silence. `name_absent` is silent on purpose: it is a flavor-axis fact that holds for every target, and the validity gate already reports a reachable one with the compiler's own diagnostic. **Warning lane** — `_Lowerer._need()` consults the table and emits `ArchDomainWarning`. The default backend is the C++ engine, which returns finished IR without the Python lowerer running, so the same check also runs over its emitted `declare` lines; the declare → key mapping is exact because both engines emit the table's declares verbatim. Controlled by `ROCKE_ARCH_DOMAIN`: `warn` (default) / `off`; any other value **raises**, because every way of getting this variable wrong is somebody trying to switch the lane off, and silently defaulting to `warn` hands them the opposite of what they asked for. The value is read in `lower_kernel_via_backend`, which every lowering passes through (`lower_kernel_to_llvm`, `compile_kernel` and the per-family wrappers), so it raises for every kernel, entry point and backend rather than only for a kernel that happens to demand an intrinsic. The LLVM flavor is likewise resolved once per lowering and handed to each stage. `error` mode is planned (AICK-2274). **Gate** `tools/check_arch_domain.py` — wired into `run_all.py`. It lowers the 194-case corpus at all three committed flavors (pure Python; it needs no toolchain, so it rules on every flavor on any host) and compares the warnings with `EXPECTED_WARNINGS`, which only ever shrinks. The list holds nine entries: three intrinsics (`ds.read.tr16.b64`, `mfma.f32.16x16x32.bf16`, `mfma.f32.16x16x32.f16`) on gfx942 at each flavor. All nine are one defect in the parity harness rather than in a kernel: it builds the gfx950 3D attention kernel for two gfx942 cases. The validity gate lists the same two cases as `KNOWN_BAD`. Fixing the harness is left to the change that makes the lane binding, which needs the list empty. Each entry names the corpus cases allowed to demand it, so the list cannot widen in silence: a case an entry does not name is unexpected even when its intrinsic is already allowlisted, and an entry whose case stops demanding it, or no longer exists, is stale. A case that did not run (`--only`, `--arch`, `--flavor`, or an instance that declines the flavor) is judged neither way. The comparison is a pure function with a test per rule. ## 2. No implicit target default Two commits, in order: first a pure refactor making all 158 implicit lowering call sites name their arch explicitly; then removal of the default itself from both engines. Omitting the target now raises. This is what lets the warning lane mean anything — a kernel lowered at a default arch is a kernel whose arch nobody chose. The default had also spread beyond the lowerer, and the same series removes it from those places: the per-family pybind entry points (one shared `require_arch` replaces their per-family "usual target" fallbacks), the Python wrappers in `core/backend.py` that mirrored them, and the scalar-attention C wrappers below. ## 3. A corrected table and sturdier gates - **Generator 4 → 6, and one column corrected.** The generator built its probes from rocke's hand-written declaration table, which records `immarg` only where an author happened to add it, and passed the unmarked operands as variables where the compiler demands constants. llvm22 and llvm23 reject that in the verifier, so the generator's rescue recorded the true answer; llvm20 has no such check, died at ISel with `Cannot select` — which is also what a genuine negative looks like — and recorded `arch_absent`. Probes are now built from the declare `opt` resolves, so the signature comes from the LLVM being measured. 33 llvm20 cells move: 27 `arch_absent` → `ok` and 6 `arch_absent` → `toolchain_crash` (a crash is no data, so the lane stays silent). llvm22 and llvm23 move no status. Two more ways to bank a non-answer as an answer are closed: a failed name probe (`opt` crashed or timed out) was recorded as `name_absent` for every arch, and a partial `--only`/`--arch` sweep could be filed as a whole column. - **One meaning of "this flavor refuses the instance".** The arch-domain gate counted four gfx1250 instances that refuse an older LLVM as lowering errors — eight failures on every run of a clean tree — while the validity gate already treated them as unmeasurable. Both now import one predicate from `tools/_flavor_gate.py`, and a test asserts they hold the same object. - **Provenance.** The validity gate no longer trusts an `opt` from another install: an older `opt` rejecting a construct its parser predates reads as "rocke emitted invalid IR" when the IR is fine, so that lane is skipped on a mismatch. The generator's identity check covers `opt` as well as `clang`. - **The arch-domain gate lowers through the native lowerer.** Through the dispatching entry point, any host with the C++ engine built would lower every case without consulting the table, harvest nothing, and call every allowlist entry stale. ## 4. What testing this turned up **The scalar-attention C wrappers.** The three public `_lower_to_llvm` wrappers in `attention_unified_entry.cpp` are the only lowering entries whose signature carries no target, and nothing in the tree called them. They passed `NULL`, which the lowerer now rejects, so all three had turned into `ROCKE_ERR_VALUE`. They now name `gfx950` explicitly — a fixed target, not a fallback, and the header says so; a caller who wants another target builds with the `_new` entry and lowers through `rocke_lower_kernel_to_llvm_ex`. **A crash in the C++ scalar reduce builder.** Testing the wrappers found that `rocke_build_unified_attention_reduce_scalar` died with SIGSEGV on every input: its loops read three tensor descriptors that nothing had built. It now builds them, in the order the Python builder does, and emits the kernel the Python builder emits (byte-identical). A new ctest lowers each wrapper and requires text identical to lowering the same kernel through the target-taking entry. **Bindings that ignored their target.** A sweep over every per-family binding that takes a `(spec, arch)` found six that discarded `arch` (layernorm2d, elementwise and the scalar attention family's serialize/verify entry points), two fused-MoE e2e entry points that hard-coded gfx950 into the spec they built, and a ninth that kept a pybind default. All now call `require_arch` first. The sweep is a test now: an extension-gated check reads every `(spec, arch)` binding off the module and requires each to reject an empty target, so a binding added later is held to the same rule. **Bindings that arrived with develop.** Three stream-K wgrad host-plan bindings (`conv_wgrad_is_valid_spec`, `conv_wgrad_streamk_default_pool` and `conv_wgrad_streamk_host`, from #13151) resolved their target with `arch_or_default`, the helper this branch removed, and declared `arch` with a gfx950 default. `rocke_engine.cpp` stopped compiling, so the pybind extension could not be built; CI builds no extension, so nothing turned red. They now take the target like every other binding, and the sweep covers the two that take a spec (pybind prints `arch: str = 'gfx950'` for a defaulted argument, which is why the sweep could not see a default before). One decision to look at: develop's stream-K tests pinned "without an arch both engines default to gfx950" for the host planner. The Python planning helpers keep their default; the C++ binding now refuses an empty target, and the test asserts that. The same thing happened a second time with the gfx950 block-scaled GEMM bindings that arrived with #13450 (`gfx950_scaled_serialize_ir`, `gfx950_scaled_verify` and `gfx950_scaled_lower_llvm`: four uses of `arch_or_default`), and `rocke_engine.cpp` again stopped compiling without anything turning red. They call `require_arch` first now. Because the extension-gated sweep cannot run where no extension is built, a second test reads `cpp/bindings` and fails on either way a binding guesses its target: the removed `arch_or_default` helper, or a gfx target as the default of an `arch` argument. It lists every offending line, and it fails on the previous merge without the fix. It reads the source, so it runs from a source checkout and skips in an installed layout. ## 5. Review Every review thread has a fix, one commit per concern: - **Tracker IDs out of code** (Rule 1): four places cited IDs; each now names the defect. The allowlist's reason string was also wrong about the defect — it blamed two declaration-table items, but the nine warnings come from the parity harness — and now says so. - **Trust boundaries** (Rule 4): a malformed or wrong-schema column, an unreadable column and an unknown `ROCKE_ARCH_DOMAIN` value each raise, instead of reading as "no data" or defaulting to `warn`. - **One flavor per lowering**: with no flavor given, the C++ path and the arch-domain post-pass each resolved their own, and compiler discovery is retryable, so the IR could be checked against the wrong flavor's column. It is now resolved once in the dispatcher, including for both engines under `ROCKE_BACKEND=both`. - **Tests lower at the arch they are about**: three substitutions of an explicit `arch=` had put the wrong arch in, and `test_legacy_equals_gfx950`, which compared a call with itself, is replaced by `test_omitting_the_target_raises`. - **`develop` merged four times** since the first review (111 commits, then 99, 29 and 14). Only the first conflicted textually (four binding translation units and `test_compiler_detection.py`). The trouble in all four was semantic: develop's new code was written against the default target this PR removes, so it called the lowerer, or a binding, without naming one. Each occurrence is fixed here: the compiler-detection tests (three of the calls sit inside a subprocess script string, and they had turned the gfx94X CI shards red) and a binding-selection probe; the flag-attribute test that turned the hipkernelprovider jobs red (twelve subtests), a library gate test that CI does not run, and the FP64 elementwise and reduce tests; and the stream-K and block-scaled GEMM bindings in section 4. A scan of the Python and C++ that the merges brought in finds no other call that names no target. - **Formatting after the merge**: this branch's parenthesized `with` statements make black treat `test_rocke.py` as Python 3.9+, so develop's three multi-line `with a, b, c:` blocks in that file now need reformatting. Each version of the file is black-clean alone; the merge is not, and the pre-commit job checks every file a PR changes. - **The allowlist names its cases** (see the gate in section 1): it was keyed by intrinsic, target and flavor alone, so a second corpus case demanding an allowlisted intrinsic left the gate green, and fixing one of the two cases that demand it could not make the entry stale. - **Wording**: the status vocabulary in `TESTING.md` lists `probe_error`, durations are out of the generator's comments, and the documentation examples name the target. --- ## Verification Run in the three toolchain images (one per LLVM flavor) against an export of the branch tip; no GPU. The pybind extension is built in each image and put on `PYTHONPATH`, so the tests that need it run (CI builds no extension and skips them). | Check | llvm20 | llvm22 | llvm23 | |---|---|---|---| | byte identity, every family | GREEN | GREEN | GREEN | | ctest | 20/20 | 20/20 | 20/20 | | emitted-IR validity (194 cases) | GREEN (2 documented `KNOWN_BAD`, 8 unvalidated) | GREEN (2 documented `KNOWN_BAD`, 4 unvalidated) | GREEN (2 documented `KNOWN_BAD`) | | arch-domain corpus gate (194 cases, 3 flavors) | GREEN (exactly the 9 documented warnings) | same | same | | platform pytest, default and under `ROCKE_BACKEND=both` | 2685 passed, 59 failed (both runs) | 2689 passed, 59 failed (both runs) | 2690 passed, 59 failed (both runs) | | library pytest | 2598 passed, 1 failed | 2599 passed, 1 failed | 2599 passed, 1 failed | Every failure is also a failure of `develop` itself, checked on an export of its tip (59 in the platform suite at every flavor, one in the library suite). The failing set is the same at the three flavors: - 56 tests import `ml_dtypes`, which none of the images has: six in `core/test_gfx1250_fp4_scaled_wmma.py` and fifty in `instances/test_gfx950_mxfp_examples.py` (new with #13450); - two tests in `core/test_arch_domain_artifact.py` (every declaration is covered; regenerating the host's column is a no-op) fail because of the 27 unmeasured declarations in section 1. That file is excluded from the installed test tree, so CI does not run it; - `tools/test_run_all.py::test_existing_engine_archive_is_reused_by_pytest` fails whenever pytest runs under `tests/run_all.py`, which exports the variable the test expects to be unset; - `library/tests/test_kda_hostpack.py::test_runner_geometry_without_gpu` builds a stub kernel without the `cluster_dims` attribute that #12857 made the manifest code read. The key #13450 added to the three columns (`mfma.scale.f32.32x32x64.f8f6f4.v8i32.v8i32`) was re-measured in each image with this branch's generator: every cell, status and evidence, matches the committed one. A note on the llvm20 run. At llvm20, `raw.ptr.buffer.load.async.lds` runs past the 60 s probe budget on six targets (the committed column records `toolchain_timeout` for them), and the generator's timeout kills the `clang` driver but not the `clang -cc1` it started, so each regeneration (including the regeneration test above) leaves twelve runaway compiler processes behind. In a two-CPU container they starve the rest of the suite: an earlier full run timed out `test_binding_compiler_selection.py::test_resolver_failure_releases_binding_builders` (valgrind, a fixed 120 s limit), which passes in about 15 s alone, on this branch and on `develop`. The numbers above are from a run that killed the leftover probes as they appeared. The leak is in the generator already on `develop`, not in this PR, and is left to a follow-up. Not run: a GPU, Windows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
feat(rocke): ask the compiler which intrinsics each GPU can actually run, and commit the answer
Branch:
users/congma/ck/rocke-arch-domain-tableBase:
developISSUE ID : AICK-2272
Summary — For human reviewers
The short version
rocke has two gates on what it emits. Byte-identity proves the two engines emit the
same bytes; the golden proves those bytes do not change. Neither asks the AMDGPU
toolchain whether the IR is legal, so an illegal construct emitted stably and by
both engines is green in both.
And when rocke emits a
declarefor anllvm.amdgcn.*intrinsic, it decides whatto declare based on the LLVM version it is building against. It never asks the other
half of the question: can the GPU we are compiling for actually run this? That
question is asked nowhere at build time — the target architecture picks an ISA
backend and never reaches the declaration table at all.
This PR closes both gaps, in that order.
First it adds a gate that pushes every kernel in the representative corpus through
clang to a linked binary, which is the only oracle that catches an intrinsic
that does not exist. That gate immediately found three dead declarations in the
table, two of them with signatures LLVM would have rejected, and they are deleted.
Then it answers the architecture question by measurement: probe the toolchain once
for every (intrinsic, target) pair in the table, and commit the result as a data
file — one per LLVM vintage, three of them, every cell machine-measured. Nothing
reads those files yet. That is deliberate: a table nobody consumes cannot break
anything, and it surfaces the defects that justify the rest of the work.
Why a link, and not a verify
This is the part that looks like it should work and does not. A
declarefor anintrinsic that does not exist passes
opt -passes=verify, and passesclang -Stoo. The backend assumes the unknown
llvm.*name is an ordinary function definedsomewhere else and emits a GOT-relative call to it. Nothing objects until the
linker cannot find the symbol.
flowchart LR A[emit IR] --> B[opt -passes=verify] B -->|accepts| C[clang -S] C -->|accepts| D[link] D -->|undefined symbol| E((caught only here)) style E fill:#fddA check that stops at compile is blind to the entire fictional-intrinsic bug class,
and the declaration table had several. The same fact is why the architecture probes
later in this PR also run through to a link rather than to an object.
Why the architecture table has to be measured rather than written down
The table is roughly 167 intrinsics across 7 wired targets — about 1,200 cells per
vintage. Nobody can fill that in by hand and be right. An earlier attempt did fill
in a small part of it by hand, and when those rows were re-checked on a second LLVM
version, two of five did not survive. That is the argument for generating it, in
miniature.
Generating it once is also not enough. A generated file that nothing checks drifts
away from its generator, and a stale availability table is worse than no table at
all, because people trust it. So the gate ships with the generator, not after it.
The gate has already earned its place several times over: every defect in the list
below was found because something re-ran the generator and compared.
The parts that are easy to get wrong
Most of the work here is not the probing. It is making the probe tell the truth. The
same error keeps reappearing in different disguises, and it is always the same shape
— the probe failed for a reason that has nothing to do with the target, and the
failure got recorded as a fact about the target.
flowchart TD P[probe fails] --> W{whose fault?} W -->|target cannot lower it| A[arch_absent — a real answer] W -->|our module was malformed| E[probe_error — fix the generator] W -->|compiler crashed or hung| N[no data — stay silent] W -->|clang cannot target this| U[no data — stay silent] A --> D[the only status allowed<br/>to mean 'no'] style D fill:#dfd style E fill:#fddSeven things had to be right:
The oracle has to be a link — for the reason above.
The LLVM-version question has to be asked first. Not for speed — it is the only
stage that can answer a key whose code generation crashes. An unrecognised
llvm.*name taking a metadata operand gets lowered as an ordinary call, and taking the
alignment of a metadata argument segfaults the backend. Asking "does this name
exist" with a text round-trip avoids the crash entirely.
Probes must be compiled at
-O0. At-O3the optimiser can delete the verycall being asked about. A module with nothing left links happily, and the probe
records "available" for an intrinsic the target does not have. This is not
hypothetical: one cross-lane intrinsic is the identity when its input is uniform, so
the optimiser folded it away, the object contained no such instruction, and the link
succeeded. Nine keys are foldable this way.
A hang is not an answer either. One buffer intrinsic never terminates under the
oldest vintage at
-O0, on any targetable arch. With no timeout the sweep simplywaited — and the column recorded
okfor all six cells, because it had been blessedbefore
-O0became mandatory and at-O3the call was being deleted. Six cellsclaimed an intrinsic was available on hardware where the compiler cannot even finish
compiling it. Timeouts now have their own status, and columns now record how the
question was asked, so a column measured the wrong way can be told from one measured
right. That provenance is the check that would have caught it.
A non-answer must never be recorded as a "no". Seven statuses, not two. "This
target cannot lower it" is a real negative. "This clang cannot target this
architecture", "asking killed the compiler", "asking hung the compiler", and "our
probe module was malformed" are absence of data, and collapsing them into the
negative is the main way this becomes worse than having no table.
An operand we chose badly is our fault, not the target's. The probe passed the
literal 0 for every constant-required operand. Where 0 is not a legal value, the
backend reports "Cannot select" — which is exactly what a genuinely unsupported
target reports. One load-to-LDS intrinsic takes a transfer size in bytes, and 0 is
not one, so every column said "no target can lower this" about an intrinsic that has
a builder entry point, a lowering handler in both engines, and a production instance
whose own comments record the resulting instructions. Three of those seven cells
were wrong, on a live path. There is no diagnostic that says "your operand value was
out of range", so the only way to tell the two apart is to ask again with a
different value — and the winning value is now recorded in the cell, because
otherwise a first-try answer and a fourth-try answer look identical.
Transient failures must not be committed as facts. Regenerating one column gave
a different answer roughly one run in three: under parallel execution the probes
fork enough linkers to occasionally hit the host's process limit, and that abort was
being recorded as a permanent compiler crash. The root cause was sizing the worker
pool from the host's CPU count — which on a scheduler-pinned node reported 384
against an affinity of 2. A real result is stable under retry and a resource failure
is not, so a serial second pass separates them without anyone having to guess; and
the pool is now sized from this process's actual share. The same fix is lifted into
a shared helper, because the byte-identity gate was asking ninja for 384 compile
jobs on the same two cores.
The newest vintage taught us words we did not know
The third column was the one with no machine-checked data behind it, for no better
reason than nobody having a vehicle to run it in. Its first run came back with 27
cells of "the generator is broken" — and both causes were the same thing from
different angles: that LLVM words two diagnostics differently, and the classifier
had only been taught the older wordings.
One is a target saying outright that an intrinsic is not supported on this
subtarget, where the older vintages only imply it by failing to select. Twelve cells
had a clear answer and it was being thrown away.
The other is the operand defect above, caught one stage earlier — in the verifier,
before codegen — and it is the only one of the three that says what would have been
acceptable. The candidate list the sweep tries now matches the domain that
diagnostic enumerates, rather than the one we had inferred.
The result worth having: after all three columns were regenerated with what the
newest vintage taught us, the two older columns changed in nothing but the
generator stamp. The fix was in what we could read, not in what we measure.
What this found
Three dead declarations, deleted. None had a call site in either engine, so
nothing could ever emit them — table weight that outlived its caller. Two were also
simply wrong: declared with a return type and argument count LLVM does not accept.
No kernel had hit the mismatch because no lowering handler existed. Deleting them
means whoever wires the op up writes a correct declaration rather than inheriting a
broken one.
One that looked dead and is not, deliberately kept. The working notes said a
packed-f16 atomic declaration was dead too. It is not — both engines need it. LLVM
auto-upgrades the declaration away, which is why it still compiles. Making rocke
emit the modern form directly is an emission change needing a golden re-bless, so it
is recorded as a follow-up rather than folded in.
Two corpus cases that do not compile at all, found by the new link gate, on an
attention path at one target. They land in an allowlist with the defect named.
And from the measurement alone, with nothing consuming the table yet:
intrinsics whose names exist in no shipped LLVM; intrinsics whose names exist but
which no wired target can lower; one case where the compiler segfaults rather than
reporting that a target cannot select an instruction; and one where it hangs
indefinitely. Fixes for these are separate changes — this PR is the measurement,
deliberately kept apart from acting on it.
Honesty about coverage
Two different limits, and they are not the same.
The link gate can only validate the host's own LLVM vintage. Modules lower at
any vintage, but there is no local compiler for the others; those report
UNVALIDATED, never green. With no LLVM tools at all the gate self-skips loudly.
The architecture table can only be measured on the LLVM the machine has. All
three vintages now have a complete, machine-measured column, which was not true when
this branch started — one column lagged the declaration table and reported itself as
incomplete rather than pretending. That machinery stays, because it will be true
again the next time an intrinsic is wired: a key that is in the artifact but gone
from the table is a hard failure anywhere, because anyone can fix it; a key that has
never been measured fails only on a host running that LLVM, and is a named skip
elsewhere that says which toolchain is needed.
No column is ever reported as green on a machine that could not measure it.
One thing the drift gate deliberately forgives: the exact compiler build. Two
independent builds of the same vintage reproducing a column cell for cell is not
staleness — it is better evidence than a byte match, because it shows the column
describes the vintage rather than one machine. Nothing else is forgiven, including
the rest of the toolchain record, because those fields say how the question was
asked rather than who answered it, and that is exactly where a wrong measurement
hides.
How this was tested
Regeneration is byte-identical run to run, and all three columns were regenerated
against compiler builds different from the ones that blessed them. The drift gate
was verified in both directions: green on a clean tree; red on a fabricated cell, a
dropped key, a changed probe method with identical cells, and an unparseable column.
Its structural half runs in a fraction of a second with no toolchain present, which
is why this is a test rather than a nightly job.
For the declaration deletions: the golden IR anchors and the declaration-table
parity check both green, and the link gate green with no new failures. Note that the
commit message for that change records "the byte-identity gate could not run on this
host -- no cmake"; the gate does run, in containers, and has since been green across
this branch and the one stacked on it. The claim in that message is stale, not the
result.
No emitted bytes change. The only emitter-adjacent edit is the removal of three
declarations that nothing could reach.
Reviewing this
16 commits. The first two are the link gate and the dead-declaration deletion it
justified. The next two build the probe generator and the multi-column schema. The
remaining twelve are each a defect in the measurement found by re-running it, which
is the intended reading order — the commit messages are where the evidence for each
lives.
14 files. Three are the data columns and account for ~17,000 of the ~20,100 added
lines; the two engine-side files are deletions only.
Summary — For AI reviewers
Change surface
The two engine files change by deletion only, symmetrically, and every deleted
key has zero
_need()call sites in either engine — so no reachable path changesand no emitted byte can move. Nothing reads the arch-domain artifact. Byte-identity
and the golden are untouched by construction; confirm the symmetry, because a
one-sided deletion is exactly what byte-identity exists to catch.
The gaps being closed
check_ir_validity.py(new)Availability factors as:
rocke resolves declarations on the flavor axis only. The arch axis is asked
nowhere. This PR measures the missing factor.
Note the factorization is not clean, and the code depends on that:
arch_domainisundefined wherever the name is absent, so the product only holds because the
flavor axis is evaluated first and short-circuits. A schema storing one arch set per
key with no flavor dimension would have to invent those cells. Check that no code
path evaluates the arch axis for a key the flavor axis already rejected.
Properties to verify
Each is falsifiable against the code.
Both oracles are a link, not a verify and not a codegen. A
declarefor anonexistent intrinsic passes both
opt -passes=verifyandclang -S; thebackend lowers it to a GOT-relative external call. Confirm neither the corpus
gate nor the arch probe treats codegen success as an answer.
Every compile runs in a subprocess. A backend failure is a
report_fatal_error, which terminates the process rather than raising — compilingin-process means one bad module kills the run and emits no report. The isolation
is a correctness requirement, not a performance choice. Confirm a killed child is
classified as a crash rather than retried into success.
Tool resolution prefers the ROCm install over
PATH. A ROCm ships clang andoptunder its own tree without putting them onPATH, and the tool that is onPATHmay be a different vintage than the comgr the modules were lowered against.Confirm
PATHis checked last.Two stages, flavor first, and the ordering is load-bearing. Stage one is an
opt -Sround-trip: LLVM resolves a recognisedllvm.*declare on parse(attaching attribute groups, remangling overloads, or letting AutoUpgrade consume
it entirely), while an unrecognised name round-trips verbatim. It must run first
because it is the only stage that can answer a key whose codegen crashes — an
unknown
llvm.*with ametadataoperand is lowered throughTargetLowering::LowerCallTo, and taking a metadata argument's alignment faults.Confirm no path reaches stage two for a
name_absentkey.-O0is mandatory for the probes. At-O3the IR pipeline can delete theprobed call, leaving a module that links with nothing selected — a false
ok.Nine keys are foldable this way. Confirm the optimisation level cannot be raised
by configuration or inherited from the environment, and that it is recorded in the
artifact rather than only applied.
Every probe has a timeout, and a timeout is not a negative. One key does not
terminate at
-O0on any targetable arch.toolchain_timeoutmust jointarget_unsupported,toolchain_crashandprobe_erroras no-data. Confirm theserial retry re-runs a timeout on the same budget — the retry exists to remove
contention, not to grant more time — and that no path lets an eventually-finishing
probe upgrade a timeout into
ok.Seven statuses, and only one of them is a negative.
ok,arch_absent,name_absent,target_unsupported,toolchain_crash,toolchain_timeout,probe_error. Onlyarch_absentmeans "this target cannot lower it". Verify noaggregation, serialization, or summary path collapses a no-data status into a
negative. The asymmetry is the reason: a false
arch_absentbreaks a workingbuild loudly and safely; a false
oksilently admits a kernel that cannot run.probe_errormust be absent from every committed column, not merely valid.It means our own probe module was malformed — a generator defect, not a
measurement. Confirm the gate asserts absence rather than membership in the status
enum. All three committed columns currently hold zero.
Probe operands are real SSA values, never
poison.poisonis a validconstant of every first-class type and therefore looks like the generic choice,
but it makes the probe lie: a poison
i32sends one buffer intrinsic into alegalisation failure on a target where concrete integers lower cleanly — a false
arch_absent.The literal-operand rescue is scoped, and its verdict is final. Some operands
must be immediates even where the declaration carries no
immarg; one vintagefails to legalise a variable operand while newer ones reject it in the verifier,
so all those diagnostics trigger the retry, and several literal values are tried
because a value out of range for one operand kind is in range for another. Two
constraints to check: the rescue must be scoped to those diagnostics (literals
could otherwise constant-fold an unsupported intrinsic away and manufacture an
ok), and once every integer operand is a constant a surviving legalisationfailure is the target's answer, recorded as
arch_absentrather than falling backto the original failure. The verifier's
immargcomplaint is deliberatelyexcluded from that final-answer rule — surviving the sweep there means the
non-immediate operand is not an integer, which is our module's fault.
The immediate-value sweep fires on
arch_absentand onprobe_error, andrecords its winner. An illegal value in a constant-required operand produces
the same "Cannot select" as an unsupported target, and no diagnostic
distinguishes them, so the only discriminator is asking again with a different
value. Confirm: the sweep is built by comparing module text against the base
probe, so a declare with no integer constant operand produces no variants and
costs nothing; the winning value lands in the cell so a first-try
okand alater-candidate
okare distinguishable; and a cell that started asprobe_errortakes the first real classification the sweep produced ratherthan keeping its original evidence — committing
probe_errorthere would reportthe generator broken for a target that simply cannot lower the call.
Serial retry separates resource failures from real ones. A real result is
stable under retry; a host process-limit abort is not. Confirm the retry covers
the crash, timeout and
probe_errorstatuses, and that a crash reproducingserially is still recorded as a crash.
Worker count comes from this process's share, not the host's.
os.cpu_count()reports the machine; on a scheduler-pinned node that read 384against an affinity of 2, which is what produced the process-limit aborts in the
first place.
_hostcapstakes the narrowest of affinity, a cgroup v2 then v1 CFSquota, and the host count. Confirm a fractional quota share rounds up to one
whole worker — rounding down is a hang, not a slow build — that the quota path
takes its root as a parameter so it is testable on a host imposing none, and that
the extra
min(8, ...)cap stays at the probe call site, since it is aboutclang's own process fan-out and does not apply to the build caller.
Toolchain identity is recorded, cross-checked, and partially forgiven. The
artifact records the clang identity, the resolved flavor, the generator version,
the probe cflags and the probe timeout; the generator refuses to run when the
recorded flavor and the compiler disagree. The drift check compares parsed
documents and forgives a difference in the clang build alone — two builds of one
vintage agreeing cell for cell is evidence, not staleness. Confirm nothing else
in the toolchain record is forgiven: those fields say how the question was asked,
and a generator or cflags or timeout change means the committed answers came from
a method no longer in use, which demands a re-bless even when every cell agrees.
Crash evidence is host-independent.
(core dumped)is a property of thehost's core-dump settings, not of the fault, so it is normalised away before
being committed. Without this the drift gate reports drift between two machines
observing the identical crash. Relatedly, probes run from the scratch directory,
because probing is expected to crash clang and a core file must not land in the
worktree.
The gates split by who can act on the failure. For the artifact: a stale key
— in the artifact, gone from the declaration table — needs no toolchain to fix
and is a hard failure on every column from any host; a key that has never been
measured fails only for the host's own vintage and is a named skip elsewhere,
naming both the keys and the toolchain required. For the link gate: only the
host's vintage can go green, the rest report UNVALIDATED. Verify no aggregate
exit path turns a skip or an UNVALIDATED column into a pass, and equally that one
newly wired intrinsic cannot red the build on every machine until all toolchains
have been run.
KNOWN_BADonly shrinks. Same convention and same rule asKNOWN_VIOLATIONSin
test_library_layering.py. An entry that starts compiling is itself reportedas a failure, so a fix cannot leave dead weight behind. It lands with two
entries.
The three deleted declarations are unreachable, and the fourth was kept for a
reason. Confirm each deleted key has zero
_need()call sites in both engines,and that the packed-f16 atomic was not deleted — both engines need it, and LLVM
auto-upgrading the declaration away is why it still compiles. Its replacement is
an emission change requiring a golden re-bless, correctly deferred. The
now-handler-less op names stay registered because they live in index-parallel
arrays the serializer keys off; removing them is not a no-op.
Regeneration is byte-identical, which is what makes the check a usable gate
rather than a source of noise. Confirm nothing non-deterministic reaches the
serialized output — ordering, timestamps, paths, or parallelism-dependent
iteration.
Repo rules that apply
engines of keys with no call sites; confirm the symmetry and that no reachable
path changed.
platform/may hardcode an absolute path or escape the tree; anchoron
__file__orrocke.assets.tests/run_all.pyenforces this and both newtools must satisfy it.
tempfile,os.cpu_count(),pathlib,shutil.which. No/tmp,nproc, orsudo. The generator spawns parallelsubprocesses and writes intermediate modules — check temp-dir handling and the
worker-count derivation.
ruff check --fix.lower_llvm.pyisemitter code and its edit here is a hand-written deletion.
_hostcapscarries the tests it was liftedwith, now covering both callers; the artifact gate is new with the generator; the
link gate is itself a test wired into
run_all.py.Known-weak points, stated so review can focus
tools/._hostcaps.pyis the onlynon-CLI module there; the naming is the only thing marking that distinction.
target_unsupportedrather than as a negative.document cannot be the record, which is most of why stale claims in them went
unnoticed. They now keep the earlier hand-run probe table, but labelled as the
argument that motivated the generator rather than as data. Check that labelling
survived, and that no numeric claim in either document lacks a generated artifact
behind it.
could not run on this host for want of cmake. It does run, in containers, and is
green. The message is wrong; the result is not.
not. Only the former can catch a wrong measurement rather than a missing one,
so on hosts without LLVM the effective coverage is materially weaker than a green
result suggests.
contributed some; a fourth will contribute more, and the failure mode is
probe_errorcells rather than wrong answers — loud, but it means a new toolchaincannot be measured without a code change.
Scope calibration
A finding of the form "the emitter still does not consult this table" is out of
scope and expected — consumption is deliberately a later change, so that the table
can be validated against real kernels before it can block anyone. A finding that the
intrinsics shown to be missing or unlowerable are not fixed here is likewise out
of scope: this PR is the measurement, and acting on it is separate work. A finding
that the two allowlisted corpus cases still fail is out of scope — they are named,
owned, and the allowlist is the mechanism that keeps them visible.
What review cannot establish from the diff
Whether the measurements are correct depends on the toolchain that produced them.
A static review can confirm the oracles are links, the ordering short-circuits, the
optimisation level is pinned, the statuses do not collapse, and the gates fail in
both directions. It cannot confirm that any particular cell reflects what the GPU
can really run.
Test plan
columns
that blessed them, agreeing cell for cell
with identical cells, an unparseable column
python -m pytest tests/core/test_arch_domain_artifact.py -qdeletions
otherwise is stale)
pre-commit run --from-ref origin/develop --to-ref HEADfrom the repo root —not yet run; per-commit hooks passed on each commit
report UNVALIDATED by design, never green