Skip to content

feat(rocke): add gfx950 scaled MFMA GEMM examples, 32x32x64 tile - #13450

Merged
tenpercent merged 11 commits into
developfrom
users/mpodkory/rocke/aick-2061
Oct 14, 2026
Merged

tenpercent merged 11 commits into
developfrom
users/mpodkory/rocke/aick-2061

Conversation

@tenpercent

@tenpercent tenpercent commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

ISSUE ID : AICK-2061

Add native gfx950 32x32x64 scaled MFMA support and a reusable packed RCR GEMM consumer. All 25 ordered FP8 E4M3, BF8 E5M2, FP6 E2M3, BF6 E3M2 and FP4 E2M1 pairs use independent E8M0 K32 scales, FP32 accumulation and BF16/FP16 output. Four focused F8, F6, F4 and mixed examples mirror the existing gfx1250 structure.

The Python and C++ catalog, lane maps, builders, type contracts, LLVM/HIP lowerers and native bindings are mirrored. F8 payloads interleave 16-value chunks; F6/F4 payloads use contiguous 32-value chunks. The overloaded nine-argument ABI uses eight padded i32 carriers and separate A/B scale VGPRs on LLVM20, LLVM22 and LLVM23. Other targets reject the new family. The spec-driven builder is used directly by four untuned correctness examples, following the gfx1250 structure. They demonstrate instruction support and make no performance claim. Storage/support documentation is included. The shared MMA reference explains catalog selection, IR emission and dispatcher responsibilities, and compares the existing 16x16x128 FP8 fused-MoE consumer with the direct 32x32x64 E8M0 GEMM family. Native lowering preserves diagnostic errors, and native kernel names use arena-owned storage without the previous 255-byte limit. Shared dtype normalization remains unchanged.

The finite numerical fixtures use independent A/B scales of 1/4, 1/2, 2 and 4 across rows, columns, K32 groups, K64 steps and output tiles. Signed and positive mantissas, one-sided controls, isolated groups and a NaN output sentinel are checked against an independent decoded reference. Host sensitivity tests prove that output rounding exposes omitted scaling, collapsed halves and shifted group/row/column ownership. These bounded tests do not establish arbitrary-input rounding, E8M0 NaN/extreme exponent behavior or output zero-sign semantics.

Test plan

  • Documentation-only follow-up: corrected the existing scaled-wrapper docstrings and linked both gfx950 instruction paths from the shared MMA/atom references, architecture reference, taxonomy and example guide. All 15 added local links/anchors checked; documented Python queries executed; the two authoring paths matched native LLVM on LLVM20/22/23. Focused suites passed 232 tests / 54 subtests, and full PR pre-commit passed. Executable Python ASTs are unchanged; no lowering, dispatcher, golden or availability-artifact changes. No fresh GPU run.
  • Scoped artifact cleanup: regenerated the LLVM23 availability data using the exact develop compiler (8f497e0992fb7513f7f78a6f6b6f1056c375e961) and re-probed the new MFMA row using the recorded LLVM20/22 compilers. Each flavor artifact now differs from develop only by the new scaled-MFMA key and its seven measured target cells; all 1,176 existing cells, canonical names and toolchain provenance per flavor are unchanged. The full LLVM23 measurement reproduced every existing row. All 50 complete-kernel LLVM23 golden hashes were regenerated and remain byte-identical, with native serialized IR and LLVM agreement. Removed two unrelated gfx1250 import-order changes. Focused checks passed 516 tests / 26 subtests plus 25 artifact tests / 3,608 subtests; all 77 families / 1,091 configurations per flavor were GREEN on LLVM20/22/23, with output hashes and verdicts identical to the pre-cleanup head. Full PR pre-commit passed.
  • Merge-conflict repair against develop at fad26ec7660c8bad7b77f60eb601101adc5164ef: retained both gfx950 scaled-MFMA and gfx1250 prefetch/cluster declarations in matching Python/C++ order. Added the callable parity-emitter entrypoint required by the new differential batch runner, with a regression covering repeated configs and the terminal record on LLVM20/22/23. Fresh native build and 18/18 CTests passed; all 77 families / 1,091 configurations per flavor were GREEN on LLVM20, LLVM22 and LLVM23, including 50 scaled-MFMA configs per flavor and the extra gfx1250 LLVM23 lanes. Full PR pre-commit and PR diff whitespace checks passed.
  • Final dispatcher removal: 484 host tests and 1,414 subtests passed across the retained gfx950 examples, scaled-MFMA contracts and existing dispatcher host suites. Removed API/module absence and direct-example imports checked; full PR pre-commit checks passed. GPU runtime dispatcher tests were excluded.
  • 50 complete-kernel Python/native serialized IR and LLVM byte identities per flavor (150 total); unchanged committed LLVM23 goldens; target/type/shape rejection, diagnostic propagation, 255/256/512-byte names.
  • Full Python/C++ byte-identity gates: all 76 families / 1,000 configurations per flavor GREEN on LLVM20, LLVM22 and LLVM23. The new family now participates in each flavor using the same nine-argument ABI.
  • Fresh native build and all 15 CTests.
  • Prior-head focused fix validation (438a3e78): 354 passed and 3,617 subtests. This includes availability-artifact checks and legacy scaled-MFMA coverage. The original-head ROCm 7.14/LLVM23 host run had 1,319 passes and 83 dependency/hardware skips.
  • Prior-head compiler validation (438a3e78): all 25 ordered pairs × BF16/FP16 on each of AMD LLVM20, LLVM22 and LLVM23 (150 complete kernels), compiled and linked to gfx950 code objects; disassembly checked instruction, selectors, physical 8/6/4-word tuples and separate VGPR scale sources. LLVM20 and LLVM22 availability rows were measured with the exact compiler builds already recorded in those artifacts.
  • Original-head Slurm gfx950, ROCm 7.14/LLVM23: all 25 pairs × BF16/FP16 × COMGR/HIP × M32/N64/K128 and M64/N96/K256; focused K64 and all-code FP6/BF6 cases. 224/224 entries, 2,376 numerical comparisons, including 2,096 non-unity scale comparisons; 224 disassembly checks for instruction, format selectors, physical 8/6/4-word tuples and VGPR scales.
  • Original-head all 25 focused-example CLI selections with --case all: 225 comparisons.
  • Original-head committed GPU numeric suites on Slurm: 224 passed, with mandatory gfx950 admission and no skipped cases.
  • Pre-commit checks over the full PR file range.

The existing relative-path guard still reports the unchanged cmake/InstallGpuReferences.cmake reference-bundle diagnostic; its emitted-IR validity stage passes. Numerical validation uses NumPy/ml_dtypes and HIP transport without torch. No performance claim is made.

Fresh validation of these fixes is host/compiler validation. No fresh GPU numerical run was performed; LLVM20/LLVM22 numerical behavior remains unvalidated on hardware. The prior GPU evidence above belongs to head 9884ea51ad45d78bee5b3afd3397a33caf49a1b7.

The final dispatcher removal changes no kernel builders, lowering, native bindings or numerical harnesses. Its fresh validation is host-only; the compiler/parity evidence above was collected before this removal.

Current cleanup validation is host, compiler-probe and Python/C++ emitted-IR validation; no fresh GPU numerical run was performed. The inherited availability-artifact coverage gap for 26 new gfx1250 prefetch/cluster declaration keys remains outside this PR: their missing rows were reproduced on unmodified develop at ebcb34e73e5e2723aafc0fe893a997c25c2592a8. The full LLVM23 sweep was retained as validation evidence, while the committed cleanup preserves the base coverage and adds only the scaled-MFMA row. The focused artifact run excludes the full declaration-coverage and full committed-column regeneration checks that encounter that inherited gap; full LLVM20/22 sweeps were not repeated.

@therock-pr-bot

therock-pr-bot Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ✅ Pass —
🔎 pre-commit ✅ Pass —
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —
🤖 therock-pr-bot ✅ Pass —

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@therock-pr-bot

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Dispatch pins are ignored, malformed spec fields are normalized, and Python/C++ behavior diverges for long kernel names.

5 open findings
What changed in this PR

Adds native gfx950 32x32x64 scaled-MFMA GEMM support across Python/C++, dispatch, examples, documentation, and numerical/parity testing.

Changes:

  • Adds all 25 ordered FP8/BF8/FP6/BF6/FP4 combinations with E8M0 scaling.
  • Adds LLVM23/HIP lowering, dispatch/binding APIs, and native bindings.
  • Adds examples, reference fixtures, goldens, parity tests, and GPU numerical coverage.

Paths below are relative to dnn-providers/hip-kernel-provider/rocke/platform/.

File Description
tests/​instances/​test_gfx950_mxfp8_numeric.py Adds FP8/BF8 GPU tests.
tests/​instances/​test_gfx950_mxfp6_numeric.py Adds FP6/BF6 and code-space tests.
tests/​instances/​test_gfx950_mxfp4_numeric.py Adds FP4 GPU tests.
tests/​instances/​test_gfx950_mxfp_examples.py Tests examples, fixtures, and dispatch.
tests/​instances/​test_gfx950_mixed_scaled_gemm_numeric.py Tests mixed-format pairs and K64.
tests/​instances/​parity/​gfx950_block_scaled_gemm_emit.py Adds Python parity emitter.
tests/​instances/​parity/​gfx950_block_scaled_gemm_emit.c Adds native parity emitter.
tests/​instances/​gfx950_scaled_numeric.py Adds GPU and ISA validation helpers.
tests/​instances/​conftest.py Exposes the gfx950 numerical fixture.
tests/​core/​test_mma_catalog_lookup.cpp Verifies native catalog layouts.
tests/​core/​test_gfx950_scaled_mfma.py Tests contracts, parity, and rejection.
tests/​core/​gfx950_scaled_mfma_llvm23.json Records LLVM23 golden hashes.
python/​rocke/​instances/​SUPPORT_MATRIX.md Documents gfx950 scaled GEMM support.
python/​rocke/​instances/​gfx950/​block_scaled_gemm.py Implements the Python GEMM builder.
python/​rocke/​instances/​gfx1250/​block_scaled_gemm.py Reorders imports.
python/​rocke/​examples/​gfx950/​gemm/​README.md Documents examples and storage contracts.
python/​rocke/​examples/​gfx950/​gemm/​mxfp8_gemm.py Adds the F8 example.
python/​rocke/​examples/​gfx950/​gemm/​mxfp6_gemm.py Adds the F6 example.
python/​rocke/​examples/​gfx950/​gemm/​mxfp4_gemm.py Adds the F4 example.
python/​rocke/​examples/​gfx950/​gemm/​mixed_scaled_gemm.py Adds the mixed-format example.
python/​rocke/​examples/​gfx950/​gemm/​FP6.md Documents FP6 storage.
python/​rocke/​examples/​gfx950/​gemm/​FP4_SCALE.md Documents FP4 and scale storage.
python/​rocke/​examples/​gfx950/​gemm/​block_scaled_gemm_verify.py Adds numerical execution and verification.
python/​rocke/​examples/​gfx950/​gemm/​_scaled_gemm_example.py Adds shared example CLI handling.
python/​rocke/​examples/​gfx950/​gemm/​__init__.py Initializes the example package.
python/​rocke/​examples/​gfx1250/​gemm/​mixed_scaled_gemm.py Reorders imports.
python/​rocke/​examples/​gfx1250/​gemm/​block_scaled_gemm_verify.py Uses shared reference utilities.
python/​rocke/​examples/​common/​scaled_gemm_reference.py Centralizes low-bit reference fixtures.
python/​rocke/​dispatch/​gemm/​block_scaled.py Adds selection, sweep, and binding APIs.
python/​rocke/​dispatch/​gemm/​__init__.py Exports the new dispatch surface.
python/​rocke/​core/​verify.py Verifies scaled-MFMA contracts.
python/​rocke/​core/​lower_llvm.py Adds LLVM23 intrinsic lowering.
python/​rocke/​core/​lower_hip.py Adds HIP builtin lowering.
python/​rocke/​core/​isa/​backend.py Routes scaled-MFMA operations.
python/​rocke/​core/​ir.py Supports scaled-MFMA IR construction.
python/​rocke/​core/​arch/​target.py Adds scaled-MFMA lane mappings.
python/​rocke/​core/​arch/​mfma_scale.py Defines formats, packing, and contracts.
python/​rocke/​core/​arch/​data/​intrinsic_arch_domain.llvm23.json Updates LLVM23 intrinsic evidence.
python/​rocke/​core/​arch/​data/​arch_specs.json Registers all 25 gfx950 atoms.
dsl_docs/​instances/​index.md Links the new instance documentation.
dsl_docs/​instances/​gfx950_block_scaled_gemm.md Documents the public GEMM contract.
cpp/​instances/​gfx950/​block_scaled_gemm.cpp Implements the native GEMM builder.
cpp/​include/​rocke/​mfma_scale_internal.h Adds native scaled-MFMA helpers.
cpp/​include/​rocke/​instance_gfx950_block_scaled_gemm.h Declares the native instance API.
cpp/​core/​verify.cpp Verifies native scaled-MFMA operations.
cpp/​core/​lower_llvm/​mma.cpp Lowers scaled MFMA to LLVM23.
cpp/​core/​lower_llvm/​data.cpp Registers the intrinsic declaration.
cpp/​core/​lower_llvm/​core.cpp Emits override-only declarations.
cpp/​core/​lower_hip/​lower_hip_mma.cpp Lowers scaled MFMA to HIP.
cpp/​core/​ir/​ir_tile.cpp Adds native IR contract handling.
cpp/​core/​arch/​data.cpp Adds native catalog entries and layouts.
cpp/​bindings/​rocke_engine.cpp Exposes native Python bindings.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@tenpercent
tenpercent marked this pull request as draft October 10, 2026 05:23
@tenpercent tenpercent changed the title feat(hip-kernel-provider): add gfx950 scaled MFMA GEMM examples feat(rocke): add gfx950 scaled MFMA GEMM examples Oct 12, 2026
@tenpercent tenpercent changed the title feat(rocke): add gfx950 scaled MFMA GEMM examples feat(rocke): add gfx950 scaled MFMA GEMM examples, 32x32x64 tile Oct 12, 2026
@tenpercent
tenpercent requested a balanced review from Copilot October 12, 2026 20:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tenpercent

Copy link
Copy Markdown
Contributor Author

Assessed the Copilot comments against head da42ccf9612a6348f05ada27534b8c2b223df7a9.

The gfx950 scaled GEMM builder intentionally remains outside production dispatch. This PR provides native scaled-MFMA instruction support, mirrored Python/C++ builders and lowerers, and untuned correctness examples that invoke the builder directly. The missing dispatch route is expected within that scope; adding a production selector is not required for this PR. Copilot's new comment follows the unconditional wording of Rule 7, which should distinguish production-selection changes from explicitly scoped instruction-support examples.

The remaining comments were also checked:

  • Python and native dtype normalization preserve the existing shared API contract: aliases, case differences and surrounding ASCII whitespace are accepted; unsupported formats still reject. The cited spellings are supported inputs.
  • The native generated-name limit is fixed with arena-owned storage. Regression checks cover 255-, 256- and 512-byte names with Python/native serialized IR and LLVM parity.
  • Lowering diagnostics are preserved through the _ex API; that thread is already resolved.
  • The algorithm/spec-pin comment is no longer applicable after removal of the dispatcher module; that thread is already resolved.

Fresh validation: 36 focused host tests passed, covering direct example builds, CLI case expansion, dtype handling, diagnostics and long native names. Separate raw native-input checks for " FP6 " and " BF8" matched Python IR; unknown and fp32 rejected in both engines. The removed dispatcher module is absent. No fresh GPU validation was performed for this assessment.

A concise documentation clarification would be: "This builder intentionally remains outside production GEMM dispatch. The focused examples invoke it directly to demonstrate native instruction support and validate correctness." Rule 7 should allow this explicit scope, and Rule 4 should exempt documented dtype spelling normalization.

No remaining actionable defect was established in these Copilot comments.

@tenpercent
tenpercent marked this pull request as ready for review October 12, 2026 20:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The kernel lacks a production dispatch route, and malformed or incomplete spec inputs are silently normalized or defaulted.

1 open finding
4 resolved since last review

🧠 Review effort: Balanced

@cgmillette cgmillette left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #13450 — add gfx950 scaled MFMA GEMM examples, 32x32x64 tile (tenpercent, AICK-2061) — 🟢 ACCEPT: solid; one test-portability nit

Prior item Status
Copilot (10-10): lowering wrapper drops diagnostics 🟢 FIXED — _ex path keeps the error; thread resolved
Copilot: selector ignores algorithm/spec_id pins 🟢 N/A — dispatcher module removed
Copilot: Python/C++ name-length mismatch (Rule 5) 🟢 FIXED — arena-owned names; 255/256/512-byte tests
Copilot ×2: dtype whitespace/case normalized (Rule 4) 🟢 Declined, justified — develop's shared normalize_dtype does the same (verified)
Copilot (10-12): unreachable from dispatch (Rule 7) 🟢 Declined — examples-only scope, same as the gfx1250 builder
Copilot (10-14): omitted block_k defaulted (Rule 4) 🟢 Declined, justified — 32 is the only legal value; validator rejects others
Bots / CI 🟢 Green at head (TheRock Win + Linux, superbuild); merge clean, no new failures (ran)
# Sev file:line Finding
N1 🟡 tests/instances/differential/test_gfx950_scaled_mfma_emit.py:44 Hashes raw CLI stdout: fails on Windows (CRLF, ran); differential/ not installed, so CI misses it

Utility: 🟢 Positive — native gfx950 scaled MFMA, mirrored engines, HW-validated examples.


Legend: 🔴 blocking · 🟠 should-fix · 🟡 minor nit · 🟢 info/good

@tenpercent

Copy link
Copy Markdown
Contributor Author

let's address differential enablement in a follow-up

@tenpercent
tenpercent merged commit e2b5b1e into develop Oct 14, 2026
99 checks passed
@tenpercent
tenpercent deleted the users/mpodkory/rocke/aick-2061 branch October 14, 2026 17:03
@assistant-librarian

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

CongMa13 added a commit that referenced this pull request Oct 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants