Skip to content

spec(BACKEND-ROCM): the ROCm head_dim=128 decode arm, the ROCm half of #382 - #564

Merged
localai-bot merged 4 commits into
mudler:mainfrom
joral:row/ROCM-DECODE-ATTN-D128-SPEC
Aug 14, 2026
Merged

spec(BACKEND-ROCM): the ROCm head_dim=128 decode arm, the ROCm half of #382#564
localai-bot merged 4 commits into
mudler:mainfrom
joral:row/ROCM-DECODE-ATTN-D128-SPEC

Conversation

@joral

@joral joral commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What this is

Spec and records only — no source changes. One spec, its claim, and one row
in the roadmap issue table. The implementation follows in its own PR, against
this spec.

Issue: #382 — "decode-opt attention
kernel is head_dim-256 only; head_dim 128 (Qwen3-dense, Llama, Mistral) falls to
the block kernel." This is the ROCm half. The CUDA half already merged as
#425 (66399617); #382 stays open because only CUDA was covered and the flip to
default-ON is still owed on both backends.

Three-place agreement: .agents/roadmap_v1.md:92 (#382KERNEL-ATTN-PAGED),
the spec's header block, and this PR body.

 .agents/specs/rocm-decode-attn-d128.md        | 269 +
 .agents/claims/CLAIM-ROCM-DECODE-ATTN-D128.md |   5 +
 .agents/roadmap_v1.md                         |   1 +

The gap it specs

The ROCm dispatch gated every fast decode kernel it has to
d == 256 || d == 512:

const bool bf16_decode_opt =
    decode_opt && (d == 256 || d == 512) && query.dtype == DType::kBF16 && ...

head_dim=128 is what Qwen3-dense, Llama and Mistral use, so every one of those
models fell straight through to PagedAttnOnline. Not a gfx1200-specific gap
and not a WMMA-specific one — the file had no fast decode path at all for the
head size the models actually use.

Cross-checked against the pinned oracle (555967922): vLLM's own
CALL_CUSTOM_LAUNCHER_BLK_HEAD switches on head_size with cases 64 and
128
(csrc/rocm/attention.cu:3607-3618), including on RDNA4 via the
is_navi_gpu() launcher variant. Upstream's fast kernel covers d=128; ours
didn't.

The change it specs is correspondingly small: PagedAttnDecodeOptBf16T<EPL> and
PagedAttnDecodeGqaBf16<QG,EPL,NWARPS> are already generic over head_dim via
EPL. The only hardcoded constraint is in the vectorized load/store helpers
(static_assert(EPL == 8 || EPL == 16)), and EPL=4 needs an 8-byte uint2
load instead of a 16-byte uint4 — the same pattern, one size down. No new
algorithm, tiling or synchronization.

It is a mirror of merged work, not new design

§1 is a reconciliation section, written because the re-verify-before-claiming
rule changed the shape of the change. #382 — filed by an outside contributor —
already names this defect exactly, on the CUDA file, and proposes the same fix.
PR #425 already landed it for CUDA.

So this adopts the merged arm's flag (VT_ATTN_DECODE_D128), default (OFF)
and stated reason verbatim rather than inventing new ones: the arm is
correctness-complete but not byte-exact, because warp-strided online softmax
reduces the KV sequence in a different order than the kernel it replaces, so
a greedy anchor can move at an exact bf16 tie. Shipping OFF keeps every existing
golden byte-identical.

The part worth reviewing closely: §5's cross-architecture reversal

#382 measured this same EPL=4 arm on sm_110 / Jetson AGX Thor as 1.6x
slower
than the kernel it replaces (81.6 → 131.0 ms at c=1, worse at c=8).
gfx1200 measures it 3.53x faster.

Both can be true — different kernels, different fallbacks (CUDA's generic block
kernel vs ROCm's PagedAttnOnline), different occupancy and LDS budgets,
different memory systems. The spec records the disagreement rather than
averaging it away, and draws the conclusion that follows: a shared default is
forbidden.
Any future flip to default-ON must be argued per backend with
per-backend measurement. The ROCm win is not evidence for the CUDA arm, and
#382's sm_110 regression is not evidence against this one.

That is what keeps #382 open, and it is the single most important claim in this
spec to agree or disagree with.

One deviation from the original commit, and one reviewer's call

  1. §7's link to the rocWMMA spec was de-linked. The rocWMMA d=128 arm is a
    separate spike with its own claim and its own (unfiled) issue, and it is
    deliberately not in this PR. That left §7 pointing a markdown link at a file
    this branch does not contain — which check-agent-record catches as
    dangling link rocm-decode-attn-d128-wmma.md, a hard gate failure, not a
    cosmetic one. §7 now names the file in backticks as "landing separately"
    instead. That is the only change from the originally committed spec; the
    rest of the file and the claim are byte-identical. §4's decode_wmma
    disjunct and §6's "the WMMA spec" are prose forward-references to the same
    thing and needed no change.

  2. The spec already carries an ## Outcome section. AGENTS.md §"Spec before
    code" says the spec is committed before implementation, never written up
    afterwards. The commit order satisfies that, but the timestamps show spec and
    implementation were authored 37 seconds apart, so it was not a real sequence —
    the Outcome was written from work already done. Recording it here as a
    reviewer's call rather than deciding it in my own favour.

What is deliberately not here

  • The implementation. src/vt/rocm/rocm_paged_attn.hip (+474), a new bf16
    Qwen3-geometry cross-device test (+93), two flag-on ctest registrations, and
    one scripts/env-doc-allowlist.txt entry. Follows in its own PR against this
    spec.
  • The rocWMMA d=128 spike. Separate spec, separate claim, separate issue.
  • The flip to default-ON, on either backend. Owes the near-tie razor, a
    distributional gate and golden regen under the ratified-tie rule — and per §5
    argued per backend, not once.
  • qg=4/qg=8 GQA fusion at any d (PagedAttnDecodeGqaBf16's fused
    condition only ever covered qg==2 and qg==8 && d==512; real, but
    head-dim-independent), and prefill at d=128.

Gate status

scripts/agent-preflight.sh on this branch: 11 gates fail, and the set is
identical to main's own.
Confirmed rather than asserted — the same script was
run in a clean detached worktree at mudler/vllm.cpp main (fafa16f0, this
PR's base), which fails exactly these: check-release-workflow,
check-test-registration, audit-live-rows (stale ACTIVE row
ENG-FORGE-COAUTHOR, unrelated), test_release_{archive,pipeline,metadata,accelerator_metadata,macos_metadata},
test_agent_onboard (asserts the default branch is master, gets main),
test_check_test_registration, test_audit_live_rows. This PR adds no gate
failure.

A twelfth was ours and is fixed: check-commit-trailers — a gate that landed
on main after this branch's original base — rejected
Assisted-by: Claude Sonnet 5 [Claude Code] as malformed against
ASSISTED_BY = <identity>:<model> [tool]. Now
Assisted-by: Claude:claude-sonnet-5 [Claude Code], and the checker reports
OK: commit trailer contract.

The commit SHA was rebuilt. The branch was originally cut at bbc482a2, 167
commits back, and carried that malformed trailer. It was rebased onto current
main (fafa16f0) rather than merged, so there is no merge commit and the
trailer fix reaches the commit. Apart from the one de-linked line above, the
spec and claim are byte-identical to the original commit, and the roadmap
issue-table edit is exactly the one #488 row with every other key untouched —
a keyed record reapplied scope-wise, not three-way merged.

Implementation re-gated on this base, gfx1200, GPU lock held

The spec's evidence was taken at the old base. Since this PR carries no source,
this is context for the follow-on rather than this PR's own gate — but the
implementation branch was rebuilt and re-gated on fafa16f0 so the spec's
Outcome is not quoting a tree that no longer exists:

result
cmake --build build-hip (gfx1200, ROCm 7.2.3, Release) 783/783, clean
ctest -R 'rocm|cross_device' 6/6, incl. both flag-on registrations
new case under VT_ATTN_DECODE_D128=1 1 case, 6 assertions, NMSE 2.97e-11 vs tol 5e-4
full ctest 8 failures, all pre-existing (below)

Those 8 are attributed, not waved past. Every one throws the same root cause
vt: no kernel for op 63 on device type 5 (src/vt/op_provider.cpp:254).
OpId(63) is kSharedExpertGate, a MoE shared-expert gate op with no ROCm
kernel registered; the ROCm implementation of it is open and unmerged as PR
#509
("ROCm MoE combine/gate ops — SharedExpertGate, MoeCombine,
MoeCombineGate", issue #41). Nothing to do with paged attention.

Note the spec's own figures say ctest 385/393; the tree is now 399 tests with
the same 8 failures. Same failures, more tests on main. The spec was left as
the record of what was measured when, rather than silently retconned.

The performance figures in §5 — 3.53x TPOT, the +42.7%/+25.0%/+17.8% decode
throughput table — were taken at the original base and have not been
re-measured. They belong to the implementation PR. §5 already carries the caveat
that the throughput table is single-run on a board that may also drive a
display: indicative, not the 2-3x-idle-reproduced standard.

…alf of mudler#382

FOLLOWING_AGENTS_PROTOCOL

Spec and claim only -- no source changes. The implementation follows in its
own PR, against this spec.

Reconciles the record before implementing, per the re-verify-before-claiming
rule. Issue mudler#382 already names this exact defect ("decode-opt attention kernel
is head_dim-256 only; head_dim 128 falls to the block kernel") and PR mudler#425
already merged the CUDA half of it. This spec is therefore the ROCm MIRROR of
landed work, not new design, and it adopts that arm's flag
(VT_ATTN_DECODE_D128), default (OFF) and stated reason verbatim: the arm is
correctness-complete but not byte-exact, because warp-strided online softmax
reduces the KV sequence in a different order than the kernel it replaces, so a
greedy anchor can move at an exact bf16 tie.

mudler#488 -- the ROCm 8.1x per-call decode-attention measurement -- is recorded as
the motivating observation, not the issue: it explicitly asserts no cause, and
this change supplies one without closing it. Adds mudler#488 to the roadmap issue
table, where it was missing.

The finding worth carrying: mudler#382 measured this same EPL=4 arm 1.6x SLOWER on
sm_110, where gfx1200 measures it 3.53x faster. Recorded, not reconciled. It is
why the flip to default-ON must be argued per backend rather than once, and it
is what keeps mudler#382 open.

Row: BACKEND-ROCM (the ACTIVE row whose code this edits; mudler#382 itself is filed
against the cross-backend kernel row, which check-agent-record will not accept
as an active claim's row -- the two differ deliberately and the claim says so).
Issue: mudler#382

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-sonnet-5 [Claude Code]
@joral
joral marked this pull request as ready for review August 13, 2026 01:27
…OWER than the CUDA arm it mirrors (mudler#382)

FOLLOWING_AGENTS_PROTOCOL

Spec only -- no source changes.

Found by reading the merged CUDA arm (6639961) hunk by hunk against this one.
CUDA added TWO EPL=4 loaders, not one: LoadRowN<4, __nv_bfloat16> AND
LoadRowN<4, float>. The second exists because CUDA's d == 32*4 launch branch
carries no dtype gate at all, and cuda_paged_attn.cu:2796 dispatches
LaunchDecode<TQ, TKV, float> for out.dtype == kF32 -- so f32 reaches the
decode-opt kernel on CUDA.

ROCm's bf16_decode_opt requires query, k_cache, v_cache and out to ALL be
kBF16, and LoadRowEplBf16/StoreRowEplBf16 are bf16 by construction. The
fallback dispatch supports FIVE dtype combinations, so four of the five still
fall to PagedAttnOnline at d=128 -- the exact fallback this arm exists to get
off. f32 is live, exercised surface on ROCm, not dead code.

The limitation is PRE-EXISTING, not introduced: ROCm's decode-opt has been
bf16-only at every head_dim, so the d=128 arm inherits the gap rather than
creating it. Recording it anyway, because §1 calls this change a mirror of
merged work and on dtype coverage it is NOT one -- and because an arm that is
not implemented is owed a record, not left to be discovered later. Not waived:
an f32 d=128 decode is silently slow rather than refused, which is the weaker
of the two failure modes.

§1 gains the same qualifier so a reader of the reconciliation section is not
told "mirror" without the exception.

Row: BACKEND-ROCM
Issue: mudler#382

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]
@localai-org-maint-bot

Copy link
Copy Markdown
Collaborator

@mudler @richiejp This spec-only PR looks good to merge from my review. It cleanly separates the ROCm implementation follow-up, preserves the CUDA arm’s default-OFF correctness policy, records the gfx1200/sm_110 performance reversal instead of generalizing across backends, and its agent-record gates pass. The current Linux/Windows reds are documented as baseline failures rather than changes introduced by these three record files.

…, and lacked Risks and Stop conditions (mudler#382)

Review repairs on top of joral's spec, pushed to the PR branch so the authorship
of the work itself stays with them. Spec-only; no source file is touched.

The blocking one: `## Outcome (2026-08-12)` opened with "Landed the ROCm `d=128`
decode arm". Nothing landed. On main, `git log -S'VT_ATTN_DECODE_D128' --
src/vt/rocm/` is EMPTY and `rocm_paged_attn.hip:1684` still reads
`d == 256 || d == 512`. AGENTS.md scopes `## Outcome` to a row reaching DONE,
and `BACKEND-ROCM` is ACTIVE with the code unmerged. Merging that text would put
a false "landed" on main, where the next agent greps for the flag, finds
nothing, and cannot tell "never merged" from "reverted" from "renamed" -- the
exact ambiguity this project has already lost time to.

Renamed to `## Result on the implementation branch`, with a banner stating
outright that it is not landed, quoting the two commands that show it, and
naming what has to be true before it becomes `## Outcome`. Section 4 moves from
"What changed" to "What will change" for the same reason: it enumerates five
edits to `rocm_paged_attn.hip` that are not in the tree.

Added the two sections AGENTS.md requires and this spec did not have:

- `## 8. Risks and decisions` -- the bf16-tie reduction-order risk that is the
  reason the arm ships default OFF, the single-board provenance of the 3.53x,
  the sm_110 1.6x-SLOWER reversal recorded but deliberately unreconciled, the
  spec-before-code consequence, the dangling WMMA flag, and the unquantified
  residual mudler#488 gap.
- `## 9. Stop conditions` -- stop if the gate moves with the flag OFF, stop if
  the flag-ON arm cannot be shown to REACH the new kernel (a green gate that
  never entered the kernel proves nothing), stop before flipping the default,
  stop if a second board contradicts the first, and do not extend this row to
  the rocWMMA arm.

`## 6. What this does not claim` was left where it is; it is a non-claims list,
not a stop condition, and it reads correctly beside the new sections.

Three smaller corrections, each verified rather than assumed:

- `**Base:**` said `bbc482a2`; the branch was rebased and its own claim file
  records `fafa16f0`. The spec and the claim now agree.
- Section 4 specifies a gate referencing `VT_ATTN_DECODE_WMMA`. That flag exists
  nowhere in `src/` or `tests/`, and `rocm-decode-attn-d128-wmma.md` is neither
  in this PR nor on main, so an implementer following section 4 verbatim writes
  a reference to an undefined symbol. Marked as a forward reference.
- The upstream anchor `attention.cu:3607-3618` is `3609-3620` at the pin. The
  content it names is exact; the line numbers had drifted by two.

Verified on this tree: check-doc-checkpoint OK, check-agent-record OK
(ENGINE=152 MODEL=362 QUANT=82 KERNEL=51 BACKEND=81).

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]
@localai-bot

Copy link
Copy Markdown
Collaborator

I've pushed a commit to this branch (de49041) rather than sending you round again — the fixes were all in the spec text and mechanical enough that a round trip would have cost more than it was worth. Your two commits are untouched and still yours; mine sits on top.

The one that was blocking. ## Outcome (2026-08-12) opened with "Landed the ROCm d=128 decode arm". It hasn't landed — on main, git log -S'VT_ATTN_DECODE_D128' -- src/vt/rocm/ is empty and rocm_paged_attn.hip:1684 still reads d == 256 || d == 512. Merging that text would put a false "landed" on main, and the next person greps for the flag, finds nothing, and can't tell "never merged" from "reverted" from "renamed".

Renamed to ## Result on the implementation branch, with a banner saying outright that it is not landed, the two commands that show it, and what has to be true before it becomes ## Outcome. ## 4. What changed## 4. What will change for the same reason — it lists five edits to rocm_paged_attn.hip that aren't in the tree.

Added the two sections AGENTS.md requires and the spec didn't have: ## 8. Risks and decisions and ## 9. Stop conditions. I drew both from material already in your spec rather than inventing positions — the bf16-tie reduction-order risk that's the reason for default OFF, the single-board provenance of the 3.53x, the sm_110 reversal recorded-but-unreconciled, the dangling WMMA flag, and the unquantified residual #488 gap. Read them and push back if I've put words in your mouth anywhere. I left ## 6. What this does not claim alone; it's a non-claims list rather than stop conditions, and it reads fine beside them.

Three smaller ones: **Base:** said bbc482a2 where your own claim file records fafa16f0 after the rebase, so they now agree; the VT_ATTN_DECODE_WMMA gate in §4 is marked as a forward reference, since that flag exists nowhere in src/ or tests/ and the sibling spec is neither here nor on main; and the upstream anchor attention.cu:3607-3618 is 3609-3620 at the pin — content exact, line numbers drifted by two.

Worth saying about the review itself: the technical content here checked out better than most specs I've reviewed in this repo. Every source anchor you cite resolves exactly — bf16_decode_opt at :1684 matches your quoted snippet verbatim, the static_assert(EPL == 8 || EPL == 16) at 268 and 289, the launch switches at 1875-1904 with only 8/16 instantiations, the five-combination fallback dispatch at 1937-1947, and the CUDA counter-claim at cuda_paged_attn.cu:321/329/2796. The upstream anchor is right at the pinned oracle too. And §5 labels its own throughput table "indicative, not the 2-3x-idle-reproduced standard" and states plainly that no post-change per-call oracle re-measure was run — that's the spec being honest about a gate it couldn't reach, which is exactly right.

On CI: the build-test-cpu red is a flake, not yours — test_cpu_threadpool asserts a wall-clock ratio on a shared runner and measured 111 against a 100 threshold. Your PR changes only Markdown, so it can't cause a C++ timing failure. Tracked as #631. Windows is the known-broken arm (#584). And pr-size/agent-record red on fork PRs because the checker demands the base SHA be an ancestor of head, which it never is once main moves — I hit it verifying your trailers and it's now filed as a real bug on our side; from the merge base your trailers check OK.

Merging once CI settles. Thanks for this one — it was the least work of the twelve to get right.

@joral

joral commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

I still need to figure out my process a little. When I completed the graph capture changes with almost no performance impact, I felt the need to find a reason. Went down the rabbit hole here and then into the wmma changes. Then tried to figure out how best to untangle the two separate issues into the requested spec and implementation changes, and how to apply them in the right order.

Will make sure the implementation is cleaned up and ready for PR tomorrow, then the WMMA changes.

Brings the branch current so the checkers can compute a range: they require the
base SHA to be an ancestor of head, which stops being true once main moves.
Merging rather than rebasing keeps joral's commits byte-identical.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]

# Conflicts:
#	.agents/roadmap_v1.md
@localai-bot
localai-bot merged commit 373aa12 into mudler:main Aug 14, 2026
20 of 23 checks passed
localai-bot pushed a commit to joral/vllm.cpp that referenced this pull request Aug 15, 2026
…mudler#382)

Review repair on top of joral's commits, which are untouched.

The spec merged as mudler#564 with a banner reading "**Not landed.** ... No
`VT_ATTN_DECODE_D128` exists in `src/vt/rocm/` on `main` --
`git log -S'VT_ATTN_DECODE_D128' -- src/vt/rocm/` is empty". Landing this PR
makes all three clauses false and hands a reader a git command that returns the
opposite of what the spec claims.

That is the MIRROR IMAGE of the finding that blocked mudler#564: that review stopped a
spec claiming work had landed when it had not. This one would have landed the
code and left the spec claiming it had not. Same defect, opposite sign, and the
same consequence -- the record disagreeing with the tree, which AGENTS.md treats
as the thing git exists to prevent.

Four corrections, each verified against the landed diff rather than assumed:

1. The banner now says the arm lands here, and states why the section stays
   `## Result` rather than becoming `## Outcome`: BACKEND-ROCM remains ACTIVE,
   and `## Outcome` is scoped to a row reaching DONE.

2. Section 4's gate was specified as
   `(d == 128 && (decode_d128 || decode_wmma)) || 256 || 512`. As landed the
   `decode_wmma` disjunct is ABSENT, because that flag does not exist anywhere
   in `src/` or `tests/`. Recorded as the deliberate deviation it is, next to
   the forward-reference paragraph that already anticipates it.

3. "Two extra ctest registrations" -> ONE. The planned second,
   `VT_ATTN_DECODE_WMMA=1`, could not land for the same reason.

4. "6/6 pass, including both new flag-on registrations" -> 5/5 with the one
   that exists, with the superseded figure named rather than silently swapped.

WHAT THIS DOES NOT FIX, and now says so in two places. The flag-ON arm has no
proof it REACHES the new kernel. `OpProviderStats` counts at provider
granularity, so the case's `declines == 0` is identical with the flag set and
unset; and because `RegisteredDevices()` excludes kCPU, the case runs 1 test
case and 0 assertions on every non-ROCm machine while exiting 0. So the
registration added to make the arm "actually gated rather than silently
skipped" is green on nothing everywhere this project owns hardware.

Section 9's stop condition 2 -- "stop if the flag-ON arm cannot be shown to
reach the new kernel; confirm selection counts, not just tokens" -- is
therefore still OPEN, and the spec now says that instead of implying the ctest
registrations discharged it. Closing it needs a kernel-selection counter in
`rocm_paged_attn.hip` asserted to differ between the two registrations.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]
localai-bot pushed a commit to joral/vllm.cpp that referenced this pull request Aug 17, 2026
…mudler#382)

Review repair on top of joral's commits, which are untouched.

The spec merged as mudler#564 with a banner reading "**Not landed.** ... No
`VT_ATTN_DECODE_D128` exists in `src/vt/rocm/` on `main` --
`git log -S'VT_ATTN_DECODE_D128' -- src/vt/rocm/` is empty". Landing this PR
makes all three clauses false and hands a reader a git command that returns the
opposite of what the spec claims.

That is the MIRROR IMAGE of the finding that blocked mudler#564: that review stopped a
spec claiming work had landed when it had not. This one would have landed the
code and left the spec claiming it had not. Same defect, opposite sign, and the
same consequence -- the record disagreeing with the tree, which AGENTS.md treats
as the thing git exists to prevent.

Four corrections, each verified against the landed diff rather than assumed:

1. The banner now says the arm lands here, and states why the section stays
   `## Result` rather than becoming `## Outcome`: BACKEND-ROCM remains ACTIVE,
   and `## Outcome` is scoped to a row reaching DONE.

2. Section 4's gate was specified as
   `(d == 128 && (decode_d128 || decode_wmma)) || 256 || 512`. As landed the
   `decode_wmma` disjunct is ABSENT, because that flag does not exist anywhere
   in `src/` or `tests/`. Recorded as the deliberate deviation it is, next to
   the forward-reference paragraph that already anticipates it.

3. "Two extra ctest registrations" -> ONE. The planned second,
   `VT_ATTN_DECODE_WMMA=1`, could not land for the same reason.

4. "6/6 pass, including both new flag-on registrations" -> 5/5 with the one
   that exists, with the superseded figure named rather than silently swapped.

WHAT THIS DOES NOT FIX, and now says so in two places. The flag-ON arm has no
proof it REACHES the new kernel. `OpProviderStats` counts at provider
granularity, so the case's `declines == 0` is identical with the flag set and
unset; and because `RegisteredDevices()` excludes kCPU, the case runs 1 test
case and 0 assertions on every non-ROCm machine while exiting 0. So the
registration added to make the arm "actually gated rather than silently
skipped" is green on nothing everywhere this project owns hardware.

Section 9's stop condition 2 -- "stop if the flag-ON arm cannot be shown to
reach the new kernel; confirm selection counts, not just tokens" -- is
therefore still OPEN, and the spec now says that instead of implying the ctest
registrations discharged it. Closing it needs a kernel-selection counter in
`rocm_paged_attn.hip` asserted to differ between the two registrations.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]
localai-bot pushed a commit that referenced this pull request Aug 17, 2026
… OFF

Implements [`.agents/specs/rocm-decode-attn-d128.md`](https://github.com/mudler/vllm.cpp/blob/main/.agents/specs/rocm-decode-attn-d128.md), which landed ahead of this change as #564.

The ROCm half of #382. The CUDA half merged as #425 (`66399617`); this mirrors it, adopting that arm's flag, default and stated reason rather than inventing new ones.

## What changes

`d == 128` — the Qwen3-dense / Llama / Mistral head size — reaches the fast decode kernels instead of falling through to the generic `PagedAttnOnline`. `LoadRowEplBf16`/`StoreRowEplBf16` gain an `EPL=4` (`uint2`) case beside the existing `EPL=8`/`16`; the dispatch gates and the two launch switches gain a `d == 128` arm. No new kernel and no new algorithm — the kernel bodies were already generic over `EPL`.

**Default OFF, opt in with `VT_ATTN_DECODE_D128=1`** — the same env var, default and reason as the merged CUDA arm. The arm is correctness-complete but not byte-exact against the kernel it replaces: warp-strided online softmax reduces the KV sequence in a different **order** than `PagedAttnOnline`'s per-tile loop, so a greedy anchor can move at an exact bf16 tie. Shipping OFF keeps every existing golden byte-identical. The flip owes the near-tie razor, a distributional gate and regen under the ratified-tie rule, and per the spec must be argued **per backend** — see the reversal below. That is what keeps #382 open.

## Reviewer note

Spec §4 item 3 writes the gate as `(d == 128 && (decode_d128 || decode_wmma))`. This commit implements it **without** the `decode_wmma` disjunct, which is what the same item's "Forward reference" paragraph instructs: `VT_ATTN_DECODE_WMMA` does not exist in the tree, and the flag lands with the rocWMMA arm on its own branch. The difference is intentional; it is visible in the diff before the note explaining it is.

## Evidence

gfx1200 (RX 9060 XT, RDNA4, 32 CU), ROCm 7.2.3, `$GPU_LOCK` held. All figures are a same-binary flag A/B — no rebuild between arms — at 1024-token synthetic prompt, 128 generated, greedy, seed 0, 2 reps per cell agreeing within ~1%.

| Model | head_dim | decode path | TPOT OFF | TPOT ON | speedup |
|---|---|---|---|---|---|
| Qwen3-0.6B | 128 | `qg=2` fused | 42.53 ms | 11.78 ms | **3.61x** |
| Qwen3-1.7B | 128 | `qg=2` fused | 52.85 ms | 21.93 ms | **2.41x** |
| Qwen3-4B | 128 | `qg=4` per-head | 81.89 ms | 39.22 ms | **2.09x** |
| Qwen3.5-0.8B | 256 | — (control) | 23.76 ms | 23.55 ms | 1.01x |

Qwen3-4B has no GQA fusion at any head_dim, so its 2.09x isolates the `EPL` widening from the fusion.

**Qwen3.5-0.8B is the negative control and it earned its keep.** Its `head_dim` is 256, so the `d == 128` gate provably cannot reach it. Its first OFF rep came in a 33% outlier at 31.14 ms, which a blind 2-rep average would have reported as a ~1.2x "win" for a model the flag cannot affect. Re-run three times: 23.86 / 23.75 / 23.68 against ON's 23.52 / 23.57.

End-to-end output throughput rises less than TPOT on the same runs (0.6B 2.48x, 1.7B 2.05x, 4B 2.02x) because they carry a 1024-token prefill the flag does not touch. TPOT isolates decode; throughput dilutes it.

### Concurrency — the advantage grows, it does not compress

Qwen3-1.7B, `--num-prompts` = 2x concurrency:

| Conc | tok/s OFF | tok/s ON | ratio | TPOT ratio |
|---|---|---|---|---|
| 1 | 12.89 | 24.66 | 1.91x | 2.40x |
| 2 | 23.27 | 47.45 | 2.04x | 2.45x |
| 4 | 39.10 | 86.86 | 2.22x | 2.46x |
| 8 | 58.97 | 147.35 | **2.50x** | **2.77x** |
| 16 | 78.43 | 227.08 | **2.90x** | **3.18x** |

This refuted the prediction made before the run, which reasoned that a tiny grid at concurrency 1 flatters the fast kernel. The dominant effect is the reverse: from c8 to c16 the fallback scales only **1.33x** against the arm's **1.54x**, and scaling efficiency at c16 relative to perfect-linear-from-c1 is **38% OFF against 58% ON**. `PagedAttnOnline` is the batch-scaling bottleneck, not merely slow per call, so the win is largest in the regime a server actually runs in.

The c1 row reproduces an independently-run four-model sweep to within ~1% (52.85/21.93 there vs 53.40/22.26 here).

### Correctness

- `ctest -R 'rocm|cross_device'` **5/5**, chained directly to the exact-SHA push.
- New case: "paged attention at Qwen3 geometry (bf16, GQA 2, head_dim 128) matches the CPU oracle", looped over `RegisteredDevices()`, NMSE <= 5e-4 vs the CPU oracle plus `OpProviderStats::declines == 0`. Genuinely new coverage — the existing generic cross-device test runs at `d=8, f32` and never reached any bf16 `EPL`-templated kernel, so none of them had bf16 coverage in this suite. (The merged CUDA arm shipped with no test at all.)
- Because the arm ships OFF **and** its flag is read into a `static const bool` — once per process — the default registration can only ever gate the fallback. `tests/CMakeLists.txt` adds a second invocation with the flag set, same shape as the existing `test_dense_gateup_fused_marlin_off_*` pair. Verified non-vacuous against the #463 trap: 1 case, 6 assertions, not zero.
- Full `ctest` 448/455. The 7 failures are **proven** pre-existing, not asserted: a clean `main` `2784dd7b` worktree built from source with none of this code fails the identical set (only `test_op_parity`'s index shifts 403 -> 404, from the added registration). They are a missing `shellcheck`, an mmap-RSS assertion, a JSON type error, and the `SharedExpertGate` ROCm registration gap owed to unmerged #509.
- `agent-preflight` fails 9, a strict **subset** of that same baseline's 10 (differing only by `role-undeclared`). `check-commit-trailers` and `check-doc-checkpoint` both pass against this base.

## Carried finding

#382 measured this same `EPL=4` arm **1.6x slower** on sm_110 / Jetson AGX Thor, where gfx1200 measures it 2-3.6x faster. Recorded, not reconciled — different kernels, different fallbacks, different memory systems. It is why the default-ON flip must be argued per backend rather than once, and it is preserved in the spec rather than averaged away.

## Against the pinned oracle: 6.35x to 1.75x slower on per-token decode

Measured after the tables above, with **both sides in the same container**, oracle = vLLM `555967922` in its production configuration via `vllm bench serve`. Qwen3-0.6B, 1024 in / 128 out, concurrency 1, **8 prompts**, warmup discarded, **3 reps**:

| | TPOT reps | mean | vs oracle |
|---|---|---|---|
| ours, flag unset | 42.54 / 42.46 / 42.19 | 42.40 ms | 6.35x slower |
| ours, `VT_ATTN_DECODE_D128=1` | 11.97 / 11.38 / 11.66 | **11.67 ms** | **1.75x slower** |
| vLLM `555967922` | 6.57 / 6.90 / 6.58 | 6.68 ms | — |

Running our binary against the container's ROCm rather than the host's is a substitution, so it was proved inert first: in-container matches native at 42.79 vs 42.53 ms unset, and 12.03 vs 11.78 ms with the flag.

The prompt count is load-bearing. At `--num-prompts 2` the oracle returned TPOT **6.96 ms and 13.45 ms on consecutive reps**, a ~2x spread averaging to a plausible-looking and entirely fictional number. At 8 prompts with a discarded warmup both sides hold to ~±0.3 ms.

This number lived only in a PR comment, which a squash merge does not carry into the tree. It is now in the spec's §5 and appended to `.agents/benchmark-record.md`, with its caveats attached rather than trailing.

## The container/glibc blocker was RETRACTED

An earlier revision of this body, and the spec's §6, said the oracle re-measure was blocked on a Nix-glibc vs container-glibc ABI mismatch. **That diagnosis was wrong and is retracted.** Our binary runs inside the pinned oracle container; the earlier failures were self-inflicted (`LD_LIBRARY_PATH` exported container-wide, which breaks the container's own tools, plus a bind mount that silently yielded nothing and looked exactly like a missing ELF interpreter). §6 now reads "not run — **not blocked**", and the WMMA-spec cross-reference is gone. A false blocker in the record is worse than no record, because it stops the next person from trying.

## Not claimed

**This does not close #488.** That issue asks for a **per-call** kernel comparison and explicitly asserts no cause. The number above is **per-token latency** with asymmetric harnesses — the oracle over HTTP via `vllm bench serve`, ours in-process — so TPOT is the only comparable axis, and TTFT, E2EL and end-to-end throughput carry the oracle's HTTP and tokenizer overhead and are directional only. It is not the same-tool per-call trace `AGENTS.md` wants before a throughput claim. `rocprofv3` is present in the container and our binary traces under it; what is still owed is decode-phase windowing on the oracle side, or the trace compares our decode against vLLM's model load and graph capture. One board, one shape. **`docs/BENCHMARKS.md`'s ROCm axis stays PENDING**, and the row this PR adds is marked DIRECTIONAL and sits beside the existing row rather than overwriting it.

**The flag-ON arm still has no proof it REACHES the new kernel, now filed as #1134.** `RegisteredDevices()` (`tests/vt/test_backend_cross_device.cpp:84-96`) enumerates `{kCUDA, kMETAL, kVULKAN, kXPU, kROCM}` and excludes `kCPU`, so on a CPU-only runner — which is what CI has — the new case reports 1 test case, 0 assertions, exit 0, for **both** registrations. On ROCm the case's only backend assertion is `OpProviderStats::declines == 0`, counted at **provider** granularity, so it is identical with the flag set and unset. §9 stop condition 2 is left OPEN. The spec disclosed this honestly; what was missing is the issue `AGENTS.md` requires for a known gap not fixed in flow. Searched before filing: not a duplicate of #463 (the unset-weights-env-var shape, which does not describe the `declines` half), #785 (a kernel that never LAUNCHES behind a dead `#if`) or #900 (same family, LTX-2.5 subject).

Also out of scope and named in the spec's new `## Owed` section: the dtype gap (ROCm's decode-opt is bf16-only at every head_dim, so 4 of 5 dtype combinations still fall to `PagedAttnOnline` at `d=128` — pre-existing, inherited, not introduced), `qg=4`/`qg=8` fusion, `d=128` prefill, and the rocWMMA arm.

## Record repairs carried in the final commit

`docs(BACKEND-ROCM): retract the blocker, keep the oracle number, and file the gap`, on top of joral's commits, which are untouched. It carries the retraction above; the oracle number into §5 and `.agents/benchmark-record.md`; `docs/BENCHMARKS.md` and `docs/STATUS.md` reconciled; §7's stale "**two** flag-on ctest registrations" corrected to one, matching what `8aedd780` already fixed in §4 item 3 and the Test-coverage section; a literal `## Owed` heading over the owed list; and #1134 filed and appended to `.agents/issue-index.md`. Two comment-only edits at `rocm_paged_attn.hip:330` and `:455`, which still enumerated "EPL=8 → d=256, EPL=16 → d=512" without the new `EPL=4` case although the top-of-file comment at `:264` had been updated.

The branch is **rebased** onto `origin/main` `d1e5e9bc` — it was 39 commits behind, and the rebase drops the earlier `merge: upstream/main` commit. Gates rerun from the worktree with explicit SHAs: `check-commit-trailers`, `check-commit-style`, `check-doc-checkpoint`, `check-public-doc-tables`, `check-agent-record` and `check-pr-size`, all OK. Not rebuilt and not re-run on hardware: every gfx1200 figure here is joral's, unchanged.

Row: BACKEND-ROCM
Issue: #382
Issue: #1134
Spec: #564

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants