W1.1 §4.4–4.5: steps 3–5 are premised on a producer shape that does not exist - #509
Conversation
Closes the first of the two gaps W1_1_TYPING_DESIGN.md §4.3 identified.
`GenerateWMMAGemmKernel{via-tile=true}` emits `tile.mma %a, %b, %acc` at the
Tile-IR seam, and its own comment says the op "flows through
rocm-wave-lds-pipeline + lower-tile-to-rocm". Neither runtime pipeline contained
that pass. `tile.mma` therefore survived to LLVM translation and the build died
with
cannot be converted to LLVM IR: missing `LLVMTranslationDialectInterface`
registration for dialect for op: tile.mma
so W1.1's Tile-IR seam was unreachable from the lane that actually executes --
including the canonical pipeline, which already runs `tessera-tile-ir-lowering`
and `rocm-wave-lds-pipeline` and still had no tile -> ROCm lowering after the
generator.
Both pipelines now run `lower-tile-to-rocm{arch=<chip>}` after
`generate-wmma-gemm-kernel`.
── Verified, in both directions ──
Default path unchanged: with via-tile off there is no `tile.mma` to lower and
the emitted hsaco is BYTE-IDENTICAL with and without the pass (diffed at the
tessera-opt level before making the change, since adding a pass to a working
production lane needs that evidence, not an assumption).
via-tile now reachable: injecting only `via-tile=true` -- no hand-added lowering
-- compiles and runs bit-identical to the production `tessera_rocm.wmma` lane on
gfx1151 at 64^3, 256^3 and 128x96x64, matching numpy to ~1e-05. The accumulator
survives the round trip.
Every measurement carried the bogus-option control (inject an invalid pass
option, require ok=False), because an earlier run of this experiment reported
bit-identical output while the injection silently never applied.
`arch=` is mandatory and separately gated: `lower-tile-to-rocm` defaults to a
CDNA part and emits `llvm.amdgcn.mfma.contract`, an MFMA intrinsic wrong for
RDNA 3.5 that does not resolve. The pipeline string stays syntactically valid,
so this fails at link time rather than parse time -- hence a gate, not a comment.
Gates: `test_rocm_pipeline_tile_lowering.py` counts lowerings against generators
(so a third lane added without one fails here), forbids the arch-less spelling,
and compares numerics on hardware. Teeth verified by deleting one pipeline's
pass: the structural and hardware tests both fail.
Remaining from §4.3: `TileToROCM`'s TYPED branch still requires a
`FragmentZeroOp` accumulator.
14442 unit, mypy 0, ruff clean, docs in sync.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…work §4.3 listed two remaining ROCm gaps. The first is closed (PR #508). This records why the second should NOT be picked up next, so it is not mistaken for ready work. `TileToROCM`'s typed fragment branch has no producer and no test. Measured: zero C++ passes emit `fragment_pack`/`fragment_zero` (only the two consumers and the verifier mention them), zero occurrences in `runtime.py`, zero lit fixtures pairing a typed fragment with a ROCm lowering, zero tests asserting its diagnostic, and `materializeFragmentPack` is local to that one file. The fix is also not the obvious one. Relaxing the `FragmentZeroOp` check alone would reproduce the NVIDIA defect exactly -- the typed branch synthesises its own accumulator (`addOperands({*a, *b, zero})`) where the untyped branch threads the real one (`addOperands({mmaData[0], mmaData[1], acc})`). Accepting a non-zero accumulator without threading it would silently discard it. Threading needs an accumulator-side counterpart of `materializeFragmentPack`, which A and B have and the accumulator does not. That work is unverifiable today: with no producer there is no program to run and so no numeric gate. Building it now repeats the mistake §4.3 corrected -- infrastructure for a path nothing executes, sized by reasoning rather than measurement. It belongs with step 3: whichever producer first emits typed fragments for ROCm must carry the accumulator materialisation, and that migration's numeric gate covers both. Docs only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured on starting step 3. `FragmentPackOp::verify` requires exactly one
`!tile.tile` input. What the five `tile.mma` construction sites actually pass:
TileIRLoweringPass x2 tile.async_copy results -- TENSORS
GenerateWMMAGemmKernel lane-level VECTORS (toFrag -> vector::BitCastOp)
GenerateWMMALinearAttnKernel same
GenerateWMMAFlashAttnKernel same
Zero producers pass a `!tile.tile` or a `tile.view` result, so no operand can be
wrapped in `fragment_pack`. The inventory warned to expect per-producer
surprises in step 3; the surprise is not per-producer, it is all of them.
This is a division-of-labour mismatch rather than a syntax gap. The typed form
assumes the COMPILER does the lane mapping (`materializeFragmentPack`); the
hand-written generators do it themselves and hand over finished vectors. Both
are coherent, and they are different models.
So step 3 as written is a rewrite of working, numerically-verified generators --
including the production ROCm GEMM lane -- and step 5 ("delete the permissive
branch") is unreachable, since deleting it breaks every existing producer.
Three options recorded, recommending (c): scope the typed form to synthesized
kernels (the Decision #28 lane) and treat the permissive branch as a DECLARED
compatibility boundary between two legitimate models. (a) rewrites proven
kernels for no measured benefit; (b) widening fragment_pack discards what the
typed contract buys.
Not choosing unilaterally -- it changes W1.1's endpoint. No code written for
steps 3/4, because their premise does not hold.
Docs only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 506f4cf689
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| assert base.get("ok") is True, base.get("reason") | ||
| assert tiled.get("ok") is True, tiled.get("reason") | ||
| assert float(np.max(np.abs(base["output"] - tiled["output"]))) == 0.0, ( |
There was a problem hiding this comment.
Verify that the compiled HSACO actually launched
When the generated module cannot load—for example, because the live AMD chip differs from _rocm_chip()'s default gfx1151—_rocm_compiled_gemm_impl raises _RocmCompiledUnavailable, _execute_rocm_compiled_gemm silently runs the hand-written WMMA fallback, and rt.launch still returns ok=True. Thus both calls can execute the same fallback and satisfy these assertions even though the tile.mma HSACO never ran; TESSERA_STRICT_DISPATCH does not reject these envelope failures. Make the test fail if _execute_rocm_wmma_artifact is reached, or otherwise verify the intended chip and compiled module launch before comparing outputs.
AGENTS.md reference: AGENTS.md:L14-L16
Useful? React with 👍 / 👎.
Docs only. Two findings that stop work rather than start it, both measured while attempting the next steps.
§4.5 — step 3 is not a migration
FragmentPackOp::verifyrequires exactly one!tile.tileinput. What the fivetile.mmaconstruction sites actually pass:TileIRLoweringPass×2tile.async_copyresults — tensorsGenerateWMMAGemmKerneltoFrag→vector::BitCastOp)GenerateWMMALinearAttnKernelGenerateWMMAFlashAttnKernelZero producers pass a
!tile.tile. No operand can be wrapped infragment_pack— the typed contract expectstile.view → fragment_pack → tile.mma, and every producer supplies a tensor or a vector whose lane math it has already done.That's a division-of-labour mismatch, not a syntax gap: the typed form assumes the compiler does the lane mapping (
materializeFragmentPack); the hand-written generators do it themselves. Both are coherent — they're different models.So:
§4.4 — gap 2 is coupled to step 3
TileToROCM's typed branch has no producer and no test: zero C++ emitters offragment_pack/fragment_zero, zero occurrences inruntime.py, zero fixtures pairing a typed fragment with a ROCm lowering. And relaxing itsFragmentZeroOpcheck alone would reproduce the NVIDIA defect — it synthesises its own accumulator (addOperands({*a, *b, zero})) where the untyped branch threads the real one.The choice — yours, not mine
tile.view → fragment_packfragment_packto accept tensors/vectorsUnder (c), step 5 changes from "delete the permissive branch" to "the branch is a declared compatibility envelope, and any path where the compiler owns lane mapping must use the typed form" — a Decision #32-style declared boundary rather than an open TODO.
It also closes W1.1 honestly: steps 1, 2 and 2b's guard are real contract improvements that landed and are gated; steps 3–5 as written were premised on a producer shape that doesn't exist.
No code written for steps 3/4 — their premise doesn't hold.
🤖 Generated with Claude Code