ck_tile: fix 2:4-sparse SWMMAC correctness on gfx1201/RDNA4 (3 bugs) + fail→pass repro - #3759
ck_tile: fix 2:4-sparse SWMMAC correctness on gfx1201/RDNA4 (3 bugs) + fail→pass repro#3759The-Monk wants to merge 5 commits into
Conversation
…dormant, Stage 27) All new paths gated behind GGML_HIP_* env vars -> OFF by default. Zero change to default dispatch. Backs the AMD PR ROCm/composable_kernel#3759 §3-4 journey. WINS: - mul_mat_2of4_fp8_mmq: MMQ-grade 2:4-sparse fp8 GEMM (cooperative tiles, LDS staging, ILP>=4). At/ahead of our native-fp8 WMMA MMQ on the 3 dominant GEMM shapes (+1.8% gate-up / +3.9% q-o / +32% down-proj). - mul_mat_dense_fp8_mmq: dense-fp8 MMQ twin. - shuffle-based activation-quantize in mmvq.cu (128-thread, float4 loads, warp-shuffle max-reduction, zero LDS/__syncthreads) -- replaces the 32-thread LDS+sync reducer; ~36us->18us, closed ~40% of the remaining 8% gap. Both kernels now high-90s% of native-fp8 (0.989x same-session). - k/v adaptive tile in mul_mat_2of4_fp8.cu. CORRECTIONS / MEASUREMENT CONTROLS (the honest half): - mul_mat_dense_fp8_v3: dense-fp8 twin used as the sparsity-isolation control. Proves the ISA 2x/4x sparsity ceiling WASHES to ~1% at library grade (2x ISA -> 1.25x crude -> ~1.01x well-tiled). This is the finding sent to AMD. - swmmac24_iu4_fixed: NEGATIVE test. Applying CK's idx swap+XOR-1 to our OWN correct native SWMMAC encoder BREAKS it (err 0 -> 385). Proves the CK bug is compress/packing-convention-specific, NOT universal hardware behavior -- folded into PR #3759 Bug 3 precision note. - int4_24_probe: int4-2:4 probe (FAILING, gated off). Source of the "4x" decomposition: 2x(int4 packing, already in dense wmma_i32_16x16x32_iu4) x 2x(2:4 sparsity, washes) -> net ~1.2x weight-bandwidth only.
|
Thanks for digging this out and writing it up in this much detail — this had been sitting without a Short version: bug 1's fix holds up — I reproduced a failure on the base tree and, for what this This is not a formal approval: I can only speak for gfx1201 and the int8 path. Environment. AMD Radeon RX 9070 XT, Two environment-side notes so the commands reproduce, neither of which is a problem with this PR: 1. The repro can't be built from the PR alone
#include "real_a_tile.h" // REAL_A_16x128: Quark int8 2:4 weight tileoutside the I generated a substitute tile to get something running: same 2. base fails, head passes
The harness wraps 3. The whole effect is bug 1Two more variants — base plus only the
The 4. The test never reaches iu4, so bugs 2 and 3 get no numerical coverage from itI disassembled the code objects of the kernels that actually launch, rather than grepping the That follows from the instantiation: the test uses So the description reasons about all three bugs from the iu4 side, but the committed test only What I could check is the shape side, with a probe that instantiates
Both trees compile, both emit 5. The buildable configuration passes on the unfixed tree — and the reason is the one you identifiedBuilt without
I expected that to be because the synthetic input has no under-filled groups, but that isn't it. I
So under-filled groups are plentiful — the distribution includes 0 — and it still passes on the buggy The practical consequence: once the missing header is supplied in whatever form, the configuration 6. The change isn't scoped to gfx1201, and bug 1's fix demonstrably reaches CDNAThis is my main question before merge, and I don't think it's answerable from gfx1201 alone. Neither changed hunk has an architecture predicate; both branch on struct MmaTransformsDefaultSelector<MmaOp, CompilerTarget,
std::enable_if_t<MmaOp::OpFamily == MmaOpFamily::SPARSE>>
{ using SelectedTransforms = MmaDefaultTransformsSparse<MmaOp::kCompressionRatio>; };with no I checked that rather than inferring it. A compile-only probe that instantiates static_assert(std::is_same_v<
typename MmaTransformsDefaultSelector<MmaOp942, Gfx942Target>::SelectedTransforms,
MmaDefaultTransformsSparse<MmaOp942::kCompressionRatio>>);compiles on both trees at That makes bug 1's fix a behaviour change for gfx942 as well, for inputs whose survivors don't sit The narrower half of this: I found no On CI: the GitHub API reports no check suites and no commit statuses for either 7. Minor, non-blocking
What would let me say more
Happy to re-run any of this on gfx1201. If it would help, I can also try to pin bug 3's mapping |
|
Thanks for this — running it on your own gfx1201 and bisecting the line both directions is exactly the review this needed, and the §5 finding (the canonical pattern structurally can't detect the bug) plus §1 (missing header) are both fair hits. Fixes pushed in ad79359: §1/§5 — repro now self-contained, and the default config fails-before/passes-after. §4 — iu4 coverage. Agreed this is the real gap, and I'll take you up on the one-hot §6 — intended scope. Bug 1's fix is intentionally architecture-generic: the old §7 — housekeeping. Internal stage labels reworded to bug-number references throughout. Noted on the Your env notes ( |
|
Since you have gfx1201 and clearly care about this corner of the stack — some context on where this PR came from, and the wider map it belongs to. While building a 2:4-sparse fp8/int4 inference path for RDNA4 we kept hitting the same pattern: the capability is in the ISA, but no library exercises it, so the first real user finds the bugs (or the absence). The list of gaps we personally hit and had to fill with hand-written kernels, in case any of it is useful to you or worth more upstream issues: Matrix/dot datapaths:
Verification gaps that compound it — validating new datapath kernels on gfx1201 is disproportionately hard because: device ASAN is unavailable (no Happy to compare notes on any of these, or split out issues where they belong in other repos — the sparse/iu4 items are the CK-relevant ones, which is why they became this PR. |
|
Re-ran everything on All artifacts, logs, compile commands and disassembly for both rounds: 1. The bare iu4 intrinsic route you asked about — it existsThe builtins are on this toolchain. The name needs a __builtin_amdgcn_swmmac_i32_16x16x32_iu4_w32(bool, int, bool, int2, int8, int, bool);
__builtin_amdgcn_swmmac_i32_16x16x64_iu4_w32(bool, int2, bool, int4, int8, int, bool);Seven arguments. I left the parameters unnamed above because I measured what
So arg1 sets
Per-lane operand sizes for wave32: K=32 takes A = 1 dword, B = 2 dwords, Toolchain: AMD clang 23.0.0git, What this is and is not. It's a numeric route to the instruction that To be precise about one thing, since you may want to reuse it: 2.
|
| build | tree | K=32 | K=64 | K=128 | |
|---|---|---|---|---|---|
| default (adversarial) | base 8fc1ac2 |
112 | 127 | 272 | FAIL |
| default | head ad79359d0 |
0 | 0 | 0 | PASS |
-DUSE_CANONICAL_PATTERN |
base 8fc1ac2 |
0 | 0 | 0 | PASS |
-DUSE_CANONICAL_PATTERN |
head | 0 | 0 | 0 | PASS |
| default | head minus bug 1 | 112 | 127 | 272 | FAIL |
| default | base plus bug 1 only | 0 | 0 | 0 | PASS |
All six are run1 == run2 == run3 byte-for-byte. Your 112/127/272 match to the
digit. §1 and §5 are closed from my side: the test is self-contained, the
default configuration is the one that fails on the unfixed tree, and the
canonical control passes on base — preserving the evidence that the old pattern
could not detect this.
Two further things from the same table. The head minus bug 1 build produces
stdout byte-identical to base, and bug 1 only passes — so for this test's
inputs, bug 1's one-line change is both necessary and sufficient for the
observed difference. That's a statement about this test, not about the fix's
full effect. And I disassembled all six builds: each contains 7
v_swmmac_i32_16x16x32_iu8 and zero iu4 instructions, so this table is a
bug-1 regression result and I'm not counting any of it as evidence for bugs 2
or 3.
I also checked that your header delta from ac24ac28d is comment-only: with
comments stripped, both sparse_transforms.hpp (163 lines) and
sparse_mma_pipeline.hpp (206 lines) are byte-identical across the two commits.
So my round-1 four-way bisect carries over to the current head without
re-running it. Hashes are in EVIDENCE_round2.md.
3. Nothing in the tree references the test
test/ck_tile/gfx1201_sparse_swmmac/ contains the .cpp and nothing else — no
CMakeLists.txt — and test/ck_tile/CMakeLists.txt has no add_subdirectory
for it. I grepped the whole head tree: the string gfx1201_sparse_swmmac
appears nowhere outside that directory. No CMake file, no script, no
Jenkinsfile stanza refers to it. So as the PR stands, nothing in the project's
own tooling can find this test — every result in this thread comes from
hand-driven builds, including mine.
I raised add_subdirectory last time as a footnote for the eventual gtest port.
That was too soft: whatever form the test finally takes, something in the tree
has to point at it, or the fix has no standing guard.
I want to be careful about the CI half of this, because I was about to overstate
it. The repository has no .github/ directory at all, so it isn't using GitHub
Actions and the empty check-suites on all three commits are the expected
consequence of that, not evidence of anything. CI here runs from the top-level
Jenkinsfile, which doesn't report into GitHub Checks — meaning I can't see from
outside whether it ran on this PR, what it covered, or whether it has gfx1201
hardware. That's worth asking a maintainer directly, and it's a different
question from the registration gap, which is visible in the tree and cheap to
close.
4. A thought on splitting bug 1 out
Entirely the maintainers' call and yours — I'm a drive-by reviewer with one GPU.
Bug 1 is in good shape: the test's default configuration fails before and passes
after on real hardware, independently reproduced, and the change is a plain
out-of-spec default. I find your §6 argument for keeping it
architecture-generic convincing. My probe result there is narrower than the
claim, though, and I should have said so last round: it shows that a gfx942
compile of the sparse path succeeds and emits v_smfmac_i32_16x16x64_i8. That
establishes the selector reaches gfx942 at compile time. It is not a runtime
result and doesn't generalize across gfx9 by itself.
Bugs 2 and 3 sit differently: the pk4 case is compiled out by default behind
-DENABLE_PK4_CASE, and the executing kernels are iu8-only, so as far as
anything in this thread shows, no number yet distinguishes bug 2 or bug 3 being
present from being absent.
Merging bug 1 now and keeping bugs 2 and 3 in a follow-up until the sweep and a
CK-level pk4 test land would get the confirmed fix into the tree without waiting
on the part that still needs measurement. Splitting inside this PR works just as
well if you'd rather not open another one — the point is only that the
verified part shouldn't have to wait on the unverified part.
5. What's next, and what I can't do
In order: the one-hot metadata sweep proper — full raw idx enumeration against
individually one-hot compressed slots, low and high nibble stimulated
separately, B basis-swept so I can tell which physical B element each result came
from, all 32 lanes and all 8 accumulator dwords collected, K=32 and K=64 swept
independently rather than one extrapolated from the other, with the lane/element
map established by calibration rather than borrowed from CK. Then the same
question end to end through CK's pk4 path, with bug-2-only and bug-3-only revert
mutants, since if those two can mask each other a head-vs-base comparison won't
separate them. Everything lands in the repo above with the raw tables.
Two things I can't close from here. I have no CDNA hardware, so my gfx942 result
stays compile-level; someone with a gfx942 part running the now-self-contained
repro would turn that into a runtime answer. And I can't tell whether this
repository's maintainers want a hardware-specific test registered in the normal
test graph, gated behind an option, or kept standalone — that decision, and what
evidence they'll accept for a target they may not have in CI, has to come from
them. Worth asking explicitly rather than either of us guessing.
On your verification-tooling questions (device ASAN, stochastic PC sampling, the
GL2C EA size-split counters reading zero): I have the same card and would rather
give you measurements than guesses, so I'll try all three and report back —
after the sweep, purely because I don't want to leave that half-finished. I've
subscribed to ROCm/legacy-rocm-build#6613. I can see why you grouped them with this work; the
absence of device ASAN is precisely why bug 2's class had to be found the hard
way.
|
Sweep done, plus an end-to-end pk4 test. Headline: your bug-3 mapping is Everything I measured is reproducible from 1. The one-hot iu4 metadata sweep — SWAP + XOR-1 confirmedMethod, as offered: bare I pre-registered your rule and its predicted table, hashed and committed, before Result: your transform reproduces the hardware pairing exactly. 384 cells The raw law underneath:
Plain identity. No swap, no XOR. Both halves of your rule are the two places
Substituting both into the identity law reproduces your code line for line, and Adversarial checks, each breaking one way the first pass could have looked like
A correction against my own case: my pre-registered transcription of your On the ARCH SCOPE commentKeep the instruction to re-measure — I was going to argue against it and I was What I'd add is that the constants aren't encoding a gfx1201 oddity — the 2.
|
| include tree | result |
|---|---|
base 8fc1ac2 |
0 failing of 18 |
head ad79359d0 |
1 failing of 18 — SparseTransformsTest.SingleNonZeroPerGroup |
EXPECT_EQ failed at test_amdgcn_sparse_mma.cpp:202 (x9, the compressed values)
compressed out, base: 5 5 6 6 <- what the test expects
compressed out, head: 5 0 6 0 <- what your fix produces
idx unchanged (0x000000bb) in both
MixedSparsityPattern, NonZerosAtSlots{1And3,0And3} and all eight
FullMatrixVerify_* cases pass on both trees; this is the only one that moves.
Caveat on method: googletest isn't installed in my container, so I supplied a
minimal gtest.h shim (TEST, EXPECT_*, ASSERT_*, GTEST_SKIP) rather than
building the CMake target. The test source itself is byte-for-byte upstream, and
the shim clearly does detect failures since it found this one — but I haven't run
the registered target through CMake, so treat this as "the upstream test's own
assertions fail on head", not "the CI job goes red" (which I can't observe
anyway).
Two things follow. The test needs updating as part of this PR, and that diff is
some of the better evidence in the change, since it shows the old behaviour
written down and corrected. And it explains why the two-survivor cases don't
move: bug 1 only bites when a group has fewer than two survivors, which
SingleNonZeroPerGroup is the only case in that file to supply. That's also the
shape of the gap new cases would fill.
It makes the registration point from my last comment concrete too: there's
already a registered home with the right idiom, so the standalone repro could
become extra cases in that file rather than a new directory — no new CMake entry
needed at all if it goes there.
3. End-to-end pk_int4 through CK
ENABLE_PK4_CASE expands to a printf and a TODO; it doesn't instantiate the
packed pipeline and asserts nothing. Grepping test/ and example/, the only
two files referencing the sparse pipeline are your standalone repro and
test_amdgcn_sparse_mma.cpp, and neither instantiates it with pk_int4_t. So as
far as the tree shows, bugs 2 and 3 have no numerical coverage in it, and the
numbers below are the first I can find evidence of for the packed path.
The fill convention you flagged as unproven, resolved. Asked CK's own types
rather than guessed:
- for
pk_int4_tthe register map's vector index enumerates logical 4-bit
elements, not physical bytes — at K=32,num_vector_itemsis 16 while
sizeof(AWarpTensor)is 8, so porting the int8 fill verbatim overruns the
tensor by 2×; - logical element
vgoes to bytev/2, high nibble for evenv; - the coordinate order is pinned without copying it from the code under test:
K ≠ N, so the two components have different ranges. Measured, A and B both
returncoord[0]in[0,15]andcoord[1]in[0,K-1], so B is
B[coord[1] * N + coord[0]]. I had this backwards at first and it produced a
plausible-looking wrong answer, which is why I went back and pinned it from the
ranges.
Logical k landing at raw nibble offset k XOR 1 is the same XOR the sweep
found from the hardware side. Not fully independent evidence — both involve the
same nibble convention — but they're arrived at from opposite ends.
Kill matrix. One independent build and run per shape, so a compile failure at
one shape can't mask another. max_abs_err against a dense int4 CPU reference
over the same logical values; int64 accumulation, exact comparison.
Every mutant row sits on top of the §4 fix, since without it nothing builds at
K≥128 and the rows would not be comparable. The first two rows show that fix's
effect on its own.
| tree | K=32 | K=64 | K=128 | K=256 |
|---|---|---|---|---|
head ad79359d0, unmodified |
0 PASS | 0 PASS | compile fail | compile fail |
| head + §4 fix — the baseline for the rows below | 0 PASS | 0 PASS | 0 PASS | 0 PASS |
| … + bug 2 reverted | 0 PASS | 0 PASS | compile fail | compile fail |
| … + bug 3 fully reverted | 311 FAIL | 451 FAIL | 635 FAIL | 904 FAIL |
| … + bug 3, swap kept, XOR removed | 460 FAIL | 810 FAIL | 1142 FAIL | 2290 FAIL |
| … + bug 3, XOR kept, swap removed | 351 FAIL | 588 FAIL | 887 FAIL | 1397 FAIL |
| … + bug 1's true-zero default reverted, packed path | 64 FAIL | 68 FAIL | 89 FAIL | 132 FAIL |
- Bug 3 is load-bearing, and in these configurations neither half alone
suffices — the CK-side counterpart to §1. (It doesn't rule out some other
equivalent formulation; only that these two partial forms are wrong.) - Bug 1's true-zero default is load-bearing in the packed path too, not only the
scalar path — the same defect §2's existing test records. - Bug 2's mutant survives at K=32 and K=64 and is killed only at K=128 and
K=256, as a compile error. The condition isn't "multi-fragment" as such but
"shapes where the idx word count diverges": at K=32/64 both accountings round
to a single word, so the difference is unobservable there. Whatever form a
regression test for bug 2 takes — end-to-end or a type-level unit test — it
needs a case where those counts differ, or it won't exercise that fix.
The test carries guard bands around every per-lane buffer, verified intact after
the fill so an off-by-a-factor is loud rather than silent; asserts stimulus
coverage rather than assuming it (all eleven at-most-two-survivor group patterns
present — the six two-survivor pairs, four single-survivor, one all-zero — and
the signed end point −8 present in both operands); and uses a B spanning the full
signed range, asymmetric in k and n so a transposed reading can't cancel out.
4. Multi-fragment packed A doesn't compile
At K=128 and K=256 the pk4 pipeline fails to build on head:
sparse_mma_pipeline.hpp:311: 'ATransformResult must match the return type of
ATransform::exec' SparseIdxPack<2> vs SparseIdxPack<1>
checkATransformResult() re-derives the expected type as
decltype(ATransform::execExtVec(std::declval<ExternalAvecRef>()))leaving LogicalADataType at its default. ext_vector_t<pk_int4_t, N> has
signed char as its scalar type, so the default resolves PackedSize to 1 —
while the real call path, exec(), passes ADataType explicitly and gets 2. The
check disagrees with the call it validates. It's hidden at K=32/64 because 8 and
16 two-bit fields both round to one idx word; the counts first diverge at K=128.
static_assert(
std::is_same_v<ATransformResult,
- decltype(ATransform::execExtVec(std::declval<ExternalAvecRef>()))>,
+ decltype(ATransform::template execExtVec<AVecType, ADataType>(
+ std::declval<ExternalAvecRef>()))>,
"ATransformResult must match the return type of ATransform::exec");With that, all four shapes compile and pass, and your int8 tests are unaffected
(adversarial ALL PASS, canonical control ALL PASS). Whether K≥128 is a shape the
packed path is meant to support is your and the maintainers' call — the argument
that it is comes only from your int8 test covering K=128 and the pipeline being a
template over K, which isn't conclusive for a different data type. What is
measured is that the check and the call disagree. I haven't run a compile matrix
over every type/shape combination, so read the diff as "makes the check agree
with the call", not as "verified side-effect-free".
5. What this doesn't show
Mutant kills show the test is sensitive to the code under test; they aren't proof
head is correct. If head and my harness shared a wrong layout assumption, mutants
could still die while a correlated error passed — the range-based determination
of the coordinate order and the CK-free sweep are what reduce that risk, not the
kill matrix. Everything is FragsM = FragsN = 1, gfx1201, one toolchain, wave32,
full EXEC. I still have no CDNA hardware, so bug 1's reach there stays a
compile-level result on my side.
Happy to open a PR against your branch with any of this rather than leave you to
lift it — the pk4 case and the SingleNonZeroPerGroup update being the two that
matter most for someone reviewing without a gfx1201 part.
942eb11 to
8c60a12
Compare
|
This is a remarkable verification round — the pre-registration discipline, the adversarial follow-ups on your own first-pass results, and the coordinate-system self-correction you flagged against your own case are all above and beyond. Everything actionable from your three comments is now in the branch as 1. 2. Repro folded into the registered file. 3. pk4 numerical coverage — ported from your harness, with attribution. Your 4. §4 ( 5. ARCH SCOPE comment extended with your convention-scope point. It now states that the measured hardware idx law is a plain identity in raw nibble coordinates, that SWAP + XOR-1 are the coordinate change from On the Results on gfx1201 (Radeon AI PRO R9700, ROCm 7.14), full registered suite: One honest caveat on portability: the new E2E cases |
|
Re-verified 1. On the pk4 portPlease keep it as it is. I wrote that harness and published it under MIT; I am happy for it 2. What I tested
The ROCm version differs from yours (7.14.0), so this is a cross-version reproduction rather With
3. Mutation matrixTest code held at head, one production edit per mutant, the target's objects deleted and
No mutant survived. The corrected Bug 2 is guarded at compile time, not numerically. Even with the The packed-path rows are synthetic mutants — that path does not exist at base — so they show I also checked whether more shapes were worth adding: 4. The
|
GPU_TARGETS |
base f7982554 |
head 8c60a1249 |
|---|---|---|
| gfx942 | builds | fails |
| gfx950 | builds | fails |
| gfx942;gfx1201 | builds | fails |
| gfx12-generic;gfx1201 | builds | builds |
| gfx1201 | builds | builds |
include/ck_tile/core/arch/mma/sparse/wmma/sparse_gfx12.hpp:153:17: error:
'__builtin_amdgcn_swmmac_i32_16x16x32_iu8_w32' needs target feature gfx12-insts,wavefrontsize32
... also :287 (iu4 16x16x32) and :313 (iu4 16x16x64)
The gfx942 and gfx950 stages are both ON by default in Jenkinsfile.
I think the gfx950 precedent did not transfer because it is a different kind of test.
SparseMMATrait.SparseMfmaGfx950Specialization is a pure type-trait check — it names
CompilerTargetGfx950 but never instantiates a pipeline or emits a builtin. The existing
pipeline tests (FullMatrixVerify_*) go through mma_pipeline_test::run_pipeline_matrix_test,
which dispatches over the configured CMake targets. The new E2E cases name Gfx1201Target
directly and bypass both, so the gfx12 SWMMAC wrappers are instantiated in every device pass,
including gfx9 ones. At base nothing instantiated them, which is why base builds.
I first tried guarding this in the test with a compile-time counterpart to device_is_gfx12()
driven by CK_CMAKE_GPU_TARGET_IDS. That fixes single-target gfx9 builds but not
gfx942;gfx1201, because that macro lists all configured targets and is identical in every
device pass. Guarding the wrappers themselves is what actually works, and it matches the
existing #if defined(__gfx950__) idiom in unary_element_wise_operation.hpp:
exec(AVecType const& aVec, BVecType const& bVec, CVecType const& cVec, int32_t idx)
{
+ #if defined(__GFX12__)
using P = WarpGemmParamsParser<Params...>;
return {__builtin_amdgcn_swmmac_i32_16x16x32_iu8_w32(true, // A signedness
aVec,
@@
P::clamp)};
+ #else
+ // Not a gfx12 device pass: this specialization can be instantiated in a
+ // multi-target build (e.g. GPU_TARGETS="gfx942;gfx1201"), where the
+ // builtin is unavailable. Unreachable as long as dispatch only calls
+ // this on a gfx12 device; a mis-dispatch traps rather than miscomputes.
+ (void)aVec;
+ (void)bVec;
+ (void)cVec;
+ (void)idx;
+ __builtin_trap();
+ __builtin_unreachable();
+ #endif
}applied to the three wrappers the new tests instantiate (iu8 16x16x32, iu4 16x16x32, iu4
16x16x64). Two details -Werror cares about: using P has to move inside the #if
(-Wunused-local-typedef), and the parameters need consuming in the #else
(-Wunused-parameter). The other eight wrappers in that header would need the same treatment if anything
instantiates them from a multi-target build; nothing in the configurations I built does, but
I have not checked the whole tree. If the intent is to make the header generally
mixed-target safe rather than just fix this regression, all eleven should be guarded
consistently.
With that patch:
GPU_TARGETS |
build | run |
|---|---|---|
| gfx942 | ok | — |
| gfx950 | ok | — |
| gfx942;gfx1201 | ok | 21/21 pass |
| gfx12-generic;gfx1201 | ok | 21/21 pass |
| gfx1201 | ok | 21/21 pass |
Note this keeps gfx12 coverage in mixed builds, which the CK_CMAKE_GPU_TARGET_IDS approach
I tried would have thrown away. I did not try a test-side guard driven by __GFX12__. I can open this as a PR against your branch if that is easier, or you can take the diff.
5. A CMake gate that may exclude this file from the gfx1201 CI job
Smaller, and I can only verify half of it.
The target is guarded by test/ck_tile/core/arch/mma/CMakeLists.txt:16:
if(GPU_TARGETS MATCHES "gfx9|gfx120")Measured with ninja -t targets all:
GPU_TARGETS |
configure | test_amdgcn_sparse_mma |
|---|---|---|
| gfx1201 | ok | generated |
| gfx12-generic | ok | not generated |
| gfx942 / gfx950 | ok | generated |
| gfx1250 | ok | not generated |
gfx12-generic matches neither alternative. What makes me think this matters is that
pipeline_tests_helper.hpp already anticipates exactly that configuration:
// gfx12-generic and gfx11-generic make no difference with the specialized archs.
// Some CI pipelines make use of that and configure the project with the generic
// flags besides compiling for (f.e.) gfx1201.So the runtime side expects a gfx12-generic build running on a gfx1201 device, while the
CMake gate excludes that configuration outright. A job configured that way would configure
successfully and quietly build no sparse-MMA test target at all — a silent omission rather
than a failure. Jenkinsfile:843 passes "gfx12-generic" to ck.runBuildCKAndTests on a
rocmnode("gfx1201") agent, but that helper lives in the external ck shared library, so I
cannot see what reaches -DGPU_TARGETS.
Could someone with access check that job's CMakeCache.txt or configure log for the effective
GPU_TARGETS, and whether test_amdgcn_sparse_mma appears in its targets? If it does need
fixing, I would add the entry rather than broaden the regex to gfx12, since gfx12 as a
regex also matches gfx1250:
if(GPU_TARGETS MATCHES "gfx9|gfx120" OR "gfx12-generic" IN_LIST GPU_TARGETS)I verified that this builds and lists all three new tests under GPU_TARGETS=gfx12-generic
and changes nothing for gfx1201, gfx942 or gfx1250.
One related observation: device_is_gfx12() uses strstr(gcnArchName, "gfx12"), which also
matches gfx1250, and amdgcn_target_id::GFX1250 is 0x1250, which falls inside the
GFX1200 .. GFX12_GENERIC (0x1200 .. 0x12FF) range that pipeline_tests_helper.hpp uses
for its generic-target handling. Neither matters today, but both would if gfx1250 ever built
this file.
6. Limits
- gfx942 and gfx950 are compile-only here; I have no CDNA hardware, so nothing about
runtime behaviour on those parts is verified. - Partial EXEC is not tested. I could not find anything in
sparse_mma_pipeline.hpp
stating that a full wave is a precondition. Since these fixes touch survivor selection and
nibble shuffling, could you confirm whether partial EXEC is simply unsupported for this
primitive? If so, a one-line comment saying that would close it. - gfx1250 is unevaluated: it does not build in my ROCm at all, for an unrelated reason
(hipLaunchAttributeClusterDimensionis absent in 7.2.4). - None of this substitutes for a CI run.
I can provide the exact configure/build commands, the per-mutant diffs, and full logs for any
row above.
|
@doplxyz — thank you, this is an exceptional review. The cross-version reproduction on 7.2.4 and the full mutation matrix are more than I could have asked for, and you're right that the mutant is the evidence, not the base-vs-head pass. I've pushed Build regression (gfx9 / multi-target) — fixedApplied your On the other eight wrappers: I scoped the guard to the three actually instantiated in a device pass today, to keep the change to what's tested. I agree the header should be mixed-target-safe as a property, not just for these three — if you'd prefer I extend the same guard to all eleven in this PR, say the word and I will (it's mechanical; I just didn't want to ship guards on wrappers neither of us has build-exercised in a mixed target). Partial EXEC — unsupported, now documentedConfirmed: this primitive requires a full active wavefront. The A/B/C fragments are distributed per-lane across all 32 lanes ( Bug-2 framing — agreedYou're correct that bug 2 is guarded at compile time (the CMake / CI gate (
|
|
Unrelated to this PR's review — just closing the loop on the offer I made earlier. I ran the three Posted as a comment on your ROCm/legacy-rocm-build#6613 rather than as new issues, since it is your ticket and Measurements, scripts and raw logs: Headline for each: device ASAN — every RDNA target from gfx1030 on rejects The ball on this PR is still yours; nothing here needs a reply from you. |
|
@doplxyz — noted, and thank you. You said this needs no reply, so just to close it out rather than leave it hanging: I've read the write-up on #6613 and responded there. On the one item still open between us on this PR — I offered to extend the The CMake fix is in at |
…+ repro Fixes three bugs in the sparse MmaOpFamily compress path that produce wrong results on gfx1201 (v_swmmac_*_iu4): phantom fill on <2-nonzero 2:4 groups, pk_int4_t group-of-4 byte miscount (OOB), and idx-metadata ordering mismatch. Adds a standalone repro that fails on unpatched CK (max_abs_err=352) and passes with the fix (max_abs_err=0). Refs ROCm#3753. (cherry picked from commit cd34d2f)
Address review feedback on the sparse SWMMAC fixes: - generate the adversarial A tile in-source (all six two-survivor position pairs, four single-survivor positions, zero-group; values keyed to (row, group, position)) and make it the DEFAULT build -- fails on the unfixed tree (max_abs_err 112/127/272 at K=32/64/128), passes after the fix; removes the uncommitted real_a_tile.h dependency - demote the slots-0,2 canonical pattern to -DUSE_CANONICAL_PATTERN control (it cannot detect the default bug) - scaffold a pk_int4 case behind -DENABLE_PK4_CASE, explicitly marked unvalidated (host fill layout unproven) pending an independent iu4 metadata-mapping cross-check - reword internal process labels to bug-number references; add ARCH SCOPE note at the packed path (the SWAP+XOR-1 mapping is gfx1201-measured) CCA (cherry picked from commit ad79359)
…e2e; fix pk4 K>=128 type check
Review-round changes, all four from doplxyz's independent verification
(github.com/doplxyz/ck3759-gfx1201-verification):
- SparseTransformsTest.SingleNonZeroPerGroup expected {V, V}: the second
compressed slot leaking a_vec[slot3] through the pre-fix {a[2], a[3]}
default -- bug 1 recorded as the expected result. Corrected to {V, 0}
with a HISTORY note; this was the only registered case supplying a
group with fewer than two survivors, which is exactly where bug 1
bites.
- The standalone repro moves from test/ck_tile/gfx1201_sparse_swmmac/
(unregistered, no CMakeLists) into test_amdgcn_sparse_mma.cpp as
SparseSwmmacE2E.{AdversarialGeneratedTile,CanonicalPatternControl};
the compile-time USE_CANONICAL_PATTERN toggle becomes a runtime
parameter so the control runs in the same binary. Runtime-skipped on
non-gfx12 devices.
- New SparsePk4E2E.AdversarialInt4AllShapes (K=32/64/128/256): first
in-tree numerical coverage for the packed-nibble path (bugs 2/3).
Ported with attribution from doplxyz's MIT-licensed pk4_e2e harness:
dense int4 CPU oracle over logical values (independent of the
transform under test), guard-banded host fill via CK's own register
maps. K=128/256 also lock bug 2 and the type-check fix below at
compile time -- idx word counts only diverge at FragsK > 1.
- checkATransformResult (sparse_mma_pipeline.hpp) re-derived the
expected transform type via execExtVec with the defaulted
LogicalADataType, resolving PackedSize to 1 while the real exec call
passes ADataType and gets 2 -- so the pk4 pipeline failed to compile
at K>=128. Fix (explicit template arguments) by doplxyz, applied
verbatim.
- ARCH SCOPE comment extended: one-hot sweeps with bare
v_swmmac_*_iu4_w32 builtins show the gfx1201 idx law is a plain
identity in raw nibble coordinates; SWAP + XOR-1 encode
CK_TILE_USE_PK4_LAYOUT_SHUFFLE's high-nibble-first convention, not a
hardware quirk, and config.hpp pins that macro on (the pk_int4.hpp
#else branches are unreachable). Convention dependency documented
alongside the existing per-architecture re-measurement instruction.
Verified on gfx1201 (Radeon AI PRO R9700, ROCm 7.14): all 21 tests in
test_amdgcn_sparse_mma pass, including the four pk4 shapes (max_abs_err
= 0 exact), the adversarial e2e that fails on the unfixed tree, and the
corrected single-survivor case.
CCA
(cherry picked from commit 8c60a12)
…l-wave requirement The gfx12 SWMMAC builtin wrappers (iu8 16x16x32, iu4 16x16x32/64) are instantiated by the new E2E tests via Gfx1201Target directly, so in a multi-target build (e.g. GPU_TARGETS=gfx942;gfx1201, both ON by default in Jenkinsfile) the gfx12 builtins are emitted in gfx9 device passes where they do not exist, breaking the test_amdgcn_sparse_mma build. Guard the three instantiated wrappers with #if defined(__GFX12__), matching the existing __gfx950__ idiom; the non-gfx12 pass traps rather than miscomputes on any mis-dispatch. Also document that this primitive requires a full active wavefront (per-lane fragment distribution), so partial EXEC is unsupported. Reported and patch-tested by @doplxyz on RX 9070 XT / ROCm 7.2.4. (cherry picked from commit ddb8567)
test/.../mma/CMakeLists.txt gated test_amdgcn_sparse_mma on GPU_TARGETS MATCHES gfx9|gfx120, which gfx12-generic does not match, so a CI job configured with gfx12-generic on a gfx1201 device (as pipeline_tests_helper.hpp anticipates) would silently build no sparse-MMA test target. Add an explicit gfx12-generic term rather than broadening to gfx12 (which also matches gfx1250). Reported by @doplxyz. (cherry picked from commit 84fbf0d)
84fbf0d to
b5ffb45
Compare
0. TL;DR for the maintainer (schung-amd)
Three correctness bugs in
ck_tile's SPARSEMmaOpFamilypath produce wrong results on gfx1201/RDNA4 (v_swmmac_*_iu4). Root-caused, fixed (two headers), and reproduced with a committed standalone test that fails on current CK and passes with the fix. We found these while building a production-grade 2:4-sparse fp8/int4 GEMM for LLM inference on RDNA4 — §3-4 give that context, which doubles as validation that the fixed path works end-to-end at scale.1. The three bugs (files:
sparse_mma_pipeline.hpp,sparse_transforms.hpp)Bug 1 —
compress_a_implwrites phantom values on <2-nonzero 2:4 groups.The fallback
ADataType nonzero_elems[2] = {a_vec[i*4+2], a_vec[i*4+3]}seeds the compressed pair from fixed input positions rather than true zero. A 2:4 group with 0 or 1 real nonzeros then reconstructs with a phantom value from a wrong lane (observed as register aliasing / UB). Fix: defaultnonzero_elemsto true{0,0}and fill only real survivors — 0/1/2-nonzero groups all reconstruct exactly.Bug 2 — packed sub-byte (
pk_int4_t) group-of-4 scan treats a byte as one element.The compaction's "nonzero per group of 4" scan operates on bytes, but a
pk_int4_tbyte holds two 4-bit values; a byte is "nonzero" if either nibble is, so the group-of-4 assumption over-counts and writes out of bounds intononzero_elems[2]/[3]. Fix: packed-sub-byte-aware nibble counting (guarded with astatic_assertthat the packed path currently implements onlypk_int4_t).Bug 3 —
compress_a_implemits the two per-group idx metadata fields in an order that mismatches what the hardware reads, requiring a SWAP + XOR-1 to correct.Within CK's own
pk_int4_tpacking, the metadata CK writes has the two idx fields swapped relative to which compressed survivor they govern, plus a XOR-1, versus whatv_swmmac_i32_iu4consumes on gfx1201 — so the sparse result comes out wrong (pos(HIGH-nibble survivor, found first) = idx1_field XOR 1,pos(LOW-nibble survivor, found second) = idx0_field XOR 1). Denseiu4was confirmed correct first (11 seeds) to isolate this to the sparse metadata path. Fix: emit the idx fields with the swap+XOR so the hardware reconstructs the correct positions (Idx0 < Idx1, the documented 2:4 contract, is naturally satisfied).Precision note (so this isn't over-stated): we verified this is specific to CK's compress/packing convention, not a universal "the hardware swaps." An independently-derived encoder in our own driver produces correct metadata without the swap — and applying the swap to it breaks a working encoding. So the fix corrects CK's own ordering to match the hardware; it is not a claim about
v_swmmac_iu4behavior for all encoders.2. The test (committed; fails on current CK, passes with the fix)
test/ck_tile/gfx1201_sparse_swmmac/sparse_swmmac_correctness_repro.cppdrives CK's own machinery for the SPARSEMmaOpFamily— it buildsPipeline::AWarpDstrEncodinginternally (no manual compression math), generates an adversarial 2:4 A tile in-source (each 4-group cycles through all six two-survivor position pairs, all four single-survivor positions, and the zero-survivor group — survivors at every position, values keyed to (row, group, position) so misplacement shows numerically rather than cancelling), runs the SWMMAC GEMM (K=32/64/128, three FragsK tile sizes), and checks against a CPU int64-accumulate reference. The legacy slots-0,2 pattern is retained as an opt-in control (-DUSE_CANONICAL_PATTERN); it cannot detect Bug 1 and passes on both trees.Verification (2026-08-15, head
ad79359d0vs merge-base8fc1ac24e9, identical test source on both trees, 1x R9700 gfx1201, ROCm 7.14):max_abs_err = 112 / 127 / 272at K=32/64/128max_abs_err = 0on all three shapesReproduction (self-contained — no external files):
Historical note: earlier revisions of this description cited
max_abs_err=352from a real Quark-quantized weight tile (REAL_A_16x128,-DUSE_REAL_TILE); that input is superseded by the in-source generator (same root cause, now reproducible from the PR alone) — the 352 figures apply only to that tile.(We compiled with the ROCm-devel clang toolchain directly —
clang++ -x hip --offload-arch=gfx1201; the distrohipccon this box is a stale 5.7-era wrapper unrelated to CK's target toolchain and will not build ck_tile headers. Any current ROCm/HIP clang works the same way.)ASAN attempted, not obtained (honest note, not a blocker): we tried to get a device-sanitizer stack trace on the Bug-1/Bug-2 OOB write for extra evidence (
-fsanitize=address -fgpu-sanitize). Device ASAN needs anxnack+target-ID variant; RDNA4 (gfx1201) does not accept anxnack+/xnack-feature suffix at all (clang++: error: invalid target ID 'gfx1201:xnack+') — HIP device-side ASAN is a gfx9/CDNA-class feature (gfx90a/gfx94x with xnack+), not available on RDNA4 consumer/workstation targets in this ROCm toolchain. We did not chase this further since it's not required to reproduce the bug — the max_abs_err numbers above are a complete, deterministic fail→pass.We'll port this into CK's gtest format for the PR; the standalone repro is included so it's runnable without the full CK test build.
3. Context — why these bugs mattered (the "whole thing")
We hit these building a library-grade 2:4-sparse GEMM for RDNA4 LLM inference (llama.cpp/ggml fork, gfx1201, 2× R9700). The journey, honestly:
v_swmmacmicrobench hits 765 TOP/s fp8-2:4 (2× dense) and 1531 TOP/s int4-2:4 (4×) at ILP≥4 — 88-100% of the R9700 spec.wmma_i32_16x16x32_iu4— gfx1201's dense int4 engine is already K=32-wide. The second 2× washes:swmmac_i32_16x16x32_iu4(749,761 GOP/s) ≈wmma_i32_16x16x32_iu4(765,061) = 0.98×, instruction-for-instruction, because dense int4 already occupies the K=32 slot. The only sparse tensor edge is the K=64 form (1.77×) — the same "2×-K" mechanism fp8-2:4 already showed washing at library grade. So on high-arithmetic-intensity LLM GEMM shapes (compute-bound), structured 2:4 is a ~1.2× weight-bandwidth win (memory-bound regime only), not the tensor 2×/4×. (Correctness first regardless — the perf question is separate, and now answered.)4. Closing the gap — result
The 8% was pinned (by direct rocprofv3 head-to-head) to two things, NOT the big GEMMs (we're at/ahead of our own fp8 MMQ there: +1.8% / +3.9% / +32% on gate-up / q-o / down-proj). Fixing them:
__syncthreads()reduction with the upstream mmq.cuh quantize shape — 128 threads,float4loads, warp-shuffle max-reduction, zero LDS/sync (halved it, 36µs→~18µs). Closed ~40% of the remaining gap; both kernels now high-90s% of our native-fp8 kernel (0.989× same-session).Net: the hand-written 2:4 kernel now reaches ~96–99% of our native-fp8 WMMA MMQ (which we built — none exists upstream), is ahead on the FLOP-dominant GEMMs, and carries the measured ~1% sparsity edge over a comparable dense kernel — a library-competitive RDNA4 2:4 GEMM on top of the correctness fixes.
5. A related RDNA4 iu4 quirk (same class, different instruction — FYI, not part of the fix)
The Bug-3 element-ordering quirk on
swmmac_iu4is not isolated: an independent kernel-authoring effort on this box hit the same class on the densev_dot8_i32_iu4/sudot8 path — "dots mismatched element pairs." RDNA4's iu4 instruction family appears to carry undocumented element-ordering conventions in both the sparse (SWMMAC metadata) and dense (dot8 operand pairing) paths. Documenting these in the ISA/CK would save the next implementer the multi-day reverse-engineering we did. Happy to write up the dense one too if useful.6. Method (for reproducibility)
All numbers: gfx1201 (R9700), ROCm 7.14, GPU-isolated, warm, medians ≥3. Correctness gated by execution (CPU-reference compare), never inspection. The full capability-optimizer method (measure → grade vs the published peak → drive the lever) is what surfaced both the bugs and the microbench-vs-model boundary.