feat(BACKEND-ROCM): select the attention backend in the runner - #1065
feat(BACKEND-ROCM): select the attention backend in the runner#1065tbrasser wants to merge 5 commits into
Conversation
The runner hardcoded the NHD KV layout while its own comment promised the tensor shape would come from the backend's get_kv_cache_shape. Resolve SelectAttentionBackendName once at KV-cache init, log the selection under VT_ATTN_SELECT_LOG, and validate every full-attention layer's view geometry against the resolved backend so a future backend with a different layout fails loudly instead of silently mis-viewing the cache. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4 [Freebuff]
|
Reviewed alongside #1056. This one is more important than the ROCm framing Why it matters more than you may realise. Order: #1056 must land first. These are not stacked — both branch from Note first that no CI has run on either PR — fork PRs need maintainer 1. There is probably a red test you have not seen. 2. The 3. One backend per runner diverges from upstream, and the anchor does not 4. The 5. Small one. One scope point. Good news on the mechanics: this one is properly reached — |
|
CI has now run on this for the first time, and it confirms both predictions from
Both are green on That second one is the reason I would rather see the Two notes on the CI itself, neither your fault:
Six checks are still running; I will flag anything else that turns up. |
…ape validation, and block-size contract at the entry points Maintainer review of mudler#1065 landed five findings, all addressed here. 1. test_kimi_linear_paged was red: kBlockSize 8 fed GPUModelRunner now hits the reachable %16 contract; fixture moved to 16 (vLLM gate block size). 2. test_bench was red: bench_core.h set block_size = seq_budget (arbitrary), now reachable through the runner validation. Round the synthetic unified block up to a multiple of 16, and validate --block-size at server_main's entry point with a clear error instead of a bare stoi (the %16 contract is now reachable because the runner calls get_kv_cache_shape; this makes it a deliberate, announced change at the two shipped entry points). 3. One backend per runner was wrong for MLA: resolution is now PER GROUP, lazily per kind inside the full-attn region. Dense groups resolve loudly (a platform with no dense backend fails at init, only for models that need one); MLA groups resolve TRITON_MLA on CUDA (whose 3-dim get_kv_cache_shape is exactly the fused cache deepseek_v2.cpp views) and stay op-driven on devices with no registered MLA backend (CPU/ROCm) — a loud throw there would regress working MLA paths. runner.h's "empty only if no full-attention group" comment is now true. 4. The shape check was vacuous (an echo of the engine's own numbers) and untested. It moves to vllm::v1::CheckKvCacheShape (registry.h/cpp) with the per-group expected view (NHD 5-dim / fused MLA 3-dim), and a new registry test registers a deliberately mis-shaped scratch backend (upstream's K/V-outermost shape) and asserts the throw, plus positive controls proving the comparison is real. 5. Resolution moved INSIDE the full_attn_group_id_ >= 0 region (pure-GDN / pooling models pay no selection), fixing the stale runner.h comment and the empty-priority-list trap. Verified in the container: test_attn_backend_registry 17/17 (61), test_runner 19/19 (543), test_kimi_linear_paged 8/8 (206), test_bench 11/11 (80), test_llm_engine 24/24 (493), test_prepare_inputs + test_mla_attention_block green. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:deepseek-v4 [Freebuff]
…TTN is for Two landing repairs on the ROCm attention registration. `docs/FEATURES.md` line 266 was 362 characters in one table cell against `check-public-doc-tables`'s 220 limit, which refuses the change outright. The cell keeps what a reader of that table needs -- the M3 claim, both PR numbers and a spec link -- at 208 characters; the long form already lives in `.agents/backend-matrix.md`, which is where that checker's message points. `docs/USAGE.md` gains the paragraph the change actually owes a user. Until this PR the ROCm kernels were registered (`kPagedAttention`, `kReshapeAndCache`) while `RocmPlatform::get_attn_backend_priority` returned an empty list, so `SelectAttentionBackendName` had nothing to resolve for `kROCM` -- the only platform in that state. It now returns upstream's dense order verbatim. The paragraph also says the thing a user most needs to know: nothing routes to the name until the runner asks for it (mudler#1065), and no flag changes, because this is what the engine picks rather than something anyone selects. EXCEPTION, argued rather than waived: `documentation-checkpoint` still refuses commit `3604c0e06` -- the contributor's -- because it changes `include/vllm/` (`USER_USAGE_PREFIXES`) and `.agents/backend-matrix.md` (`FEATURE_SURFACE_FILES`) without touching `docs/USAGE.md` in that same commit. The checker walks commits individually, so a `docs/USAGE.md` edit in a later commit cannot satisfy an earlier one, and the only way to clear it would be to rewrite a contributor's commit content. The repository squash-merges with `squash_merge_commit_message = PR_BODY`, so what lands is one commit carrying both the code and this documentation -- the state the checker is asking for. The per-commit walk is measuring an intermediate that never reaches `main`. This is the `USER_USAGE_FILES` half of the shape recorded as mudler#515; mudler#1086 narrowed the sibling `feature_surface` trigger to a registration-set change, and the `user_usage` path prefix still keys off the path. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]
Brings the branch onto current main. Merged rather than cherry-picked for the same reason as mudler#1056: the branch predates main by enough that replaying its diff loses merge-base context, while the merge is clean. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]
… contract Three landing repairs on the runner-side attention selection. `VT_ATTN_SELECT_LOG` was read from `src/` and appeared in neither `docs/ENVIRONMENT.md` nor the allowlist, so `check-env-doc` refused the change. It is a diagnostic log switch, not a behaviour-changing knob, so it joins its exact sibling `VT_KV_ALLOC_LOG` in `scripts/env-doc-allowlist.txt` rather than being written up as an operator control. The block-size contract is the part that matters to users, and it was enforced without being stated. Resolving a backend per attention group makes `get_kv_cache_shape` reachable at engine init, and it refuses any block size that is not a multiple of 16. This PR already validates `--block-size` at startup and rounds up the synthetic bench, which is the right shape -- but `docs/USAGE.md` still described the flag as an unconstrained `KV block size`, and `include/vllm.h` still told embedders `<= 0 => 32` with no constraint. An embedder calling `vllm_engine_load` with `block_size = 8` now throws where it used to work, and nothing in the header said so. Both now state the requirement. EXCEPTION, argued rather than waived: `documentation-checkpoint` still refuses commit `7336d4a48` -- the contributor's -- because it changes `include/vllm/` (`USER_USAGE_PREFIXES`) without touching `docs/USAGE.md` in that same commit. The checker walks commits individually, so a `docs/USAGE.md` edit in a later commit cannot satisfy an earlier one, and clearing it would mean rewriting a contributor's commit content. The repository squash-merges with `squash_merge_commit_message = PR_BODY`, so what lands is one commit carrying the code and this documentation together -- exactly the state the checker asks for. The per-commit walk is measuring an intermediate that never reaches `main`. Same shape as mudler#515. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]
`kPagedAttention` and `kReshapeAndCache` have been registered for `kROCM` since
the kernel fan-out, with an 80 KB `src/vt/rocm/rocm_paged_attn.hip` behind them —
but `RocmPlatform::get_attn_backend_priority` returned an empty list, so
`SelectAttentionBackendName` had nothing to resolve. ROCm was the only platform
in that state; Metal, Vulkan and Tenstorrent all return `{"FLASH_ATTN"}`.
This registers `RocmAttentionBackend` under the name `ROCM_ATTN` for `kROCM`,
following the Metal/Vulkan/Tenstorrent registration idiom, and fills in the
dense, MLA and sparse priority lists from upstream `rocm.py` at the pinned
revision `555967922`. The dense list mirrors upstream verbatim —
`{ROCM_ATTN, ROCM_AITER_FA, ROCM_AITER_UNIFIED_ATTN, TRITON_ATTN, TURBOQUANT}` —
because the selection walk skips names that are not registered, so carrying the
AITER entries costs nothing and avoids an inference about which boards gate them.
## The KV-layout deviation, recorded rather than glossed
Upstream's `ROCM_ATTN` *is* the K/V-outermost layout: `rocm.py:521-522` says so
outright. This tree uses NHD, so registering that name against NHD inverts the
name's defining property upstream. The alternative — registering `FLASH_ATTN` for
`kROCM`, as Metal, Vulkan and Tenstorrent do — was considered and rejected,
because it would leave `ROCM_ATTN` permanently unregistered in the priority list,
which is a false claim that ROCm attention is unsupported.
The deviation is therefore carried as one exact tracked exception, recorded in
`include/vllm/v1/attention/backend.h`, in `.agents/specs/rocm-attn-backend.md` §3,
and referenced from `docs/ROCM.md`. It has an explicit expiry: if a real
upstream-layout ROCm kernel lands, the registration changes shape and this stops
being an exception.
Upstream also appends `ROCM_ATTN` only `if not use_kv_connector`
(`rocm.py:429-433`), guarding an asymmetric native K/V cache view. That premise
does not exist here — this registration uses the symmetric NHD layout — and §4 of
the spec records both the reasoning and the condition under which it would stop
holding.
## Reachability, stated plainly
At this commit nothing routes to the registered name: `SelectAttentionBackendName`
has no production caller on `main`. That arrives with #1065, which is why these
were split and why #1056 must land first — #1065 alone calls the selector
unconditionally, and on ROCm's empty list `initialize_kv_cache` would throw.
`.agents/specs/rocm-attn-backend.md` §7 carries this under `## Owed` and §8
records the reachability state.
## Maintainer changes on top
- `docs/FEATURES.md` line 266 was 362 characters in one cell against
`check-public-doc-tables`'s 220 limit, which refuses the change outright. Now
208, keeping the M3 claim, both PR numbers and a spec link; the long form
already lives in `.agents/backend-matrix.md`.
- `docs/USAGE.md` gains the paragraph this change owes a reader — what was
missing, what is registered now, and that no flag changes because the engine
picks this rather than the user.
- Merged onto current `main` rather than cherry-picked: the branch predates main
by enough that replaying its diff conflicts in four C++ files, while the merge
itself is clean.
Issue: #41
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]
Picks up mudler#1056, which this PR requires: the runner resolves a backend name, and before mudler#1056 ROCm had none registered. The only conflict is docs/USAGE.md, where both changes add a paragraph — mudler#1056's on what ROCM_ATTN is, this one's on the block-size contract. Both are kept. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]
|
Thanks — all five findings are addressed in the new commits, and the CI results you posted were the confirmation we needed. 1. 2. The 3. Per-group resolution, not per runner. Resolution now happens per attention group, lazily per kind, inside the 4. The check is now real and tested. It moved to 5. Comment/guard fixed. Resolution moved inside the guarded region; the runner.h "empty only if no full-attention group exists" comment is now true, and a pure-GDN / pooling model pays no selection. Scope — agreed, and it's now called out in the body: the runner work is a different concern from the ROCm row and is proposed to move to its own row/issue/spec ( Verification (CPU tier, clean |
SelectAttentionBackendNamehad zero production callers onmain. Every hitoutside
registry.{h,cpp}was a comment, so the whole engine-level attentionregistry was dead code. This is the change that reaches it.
GPUModelRunner::initialize_kv_cachenow resolves a backend per KV-cache kindinside the view loop, stores the name, logs it under
VT_ATTN_SELECT_LOG, andvalidates each group's view against the resolved backend's
get_kv_cache_shape.Per group, not per runner
An earlier revision resolved one backend for the whole runner from a defaulted —
therefore always dense —
AttnSelectorConfig. That is wrong for MLA models, andthey do reach this code:
runner.cpp:547-548treatskMlaAttentionas thefull-attention group, so DeepSeek-V2/V4, MiniCPM3, GLM4-MoE-Lite and Kimi-K3 would
have resolved
FLASH_ATTNinstead ofTRITON_MLA. Resolution is now lazy andkeyed on the kind, with an
mla_layer_maskparallel tofa_dims; MLA validatesthe fused 3-dim shape and is tolerant, dense is loud.
Upstream resolves per layer (
gpu_model_runner.py:6994-7099, vialayers[layer_name].get_attn_backend()at:7036, with the name chosen inselector.py:185). Per kind is the coarser mirror that this tree's groupstructure supports.
The shape check can now fail
The previous
VT_CHECKcompared the engine's numbers against an echo of the samenumbers, so it could not fail for any reachable backend.
test_attn_backend_registry.cppnow registers a deliberately mis-shaped scratch backend and asserts the throw,
with three positive controls including
FLASH_ATTNchecked against the MLA view.The block-size contract, and why it is a user-visible change
Making
get_kv_cache_shapereachable at engine init also makes itsmultiple-of-16 requirement reachable. Two shipped paths were relying on that
never being checked:
examples/bench/bench_core.hsetblock_sizeto anarbitrary
max_prompt + output_len + 4, andserver_main.cpptook--block-sizethrough a barestd::stoiwith no validation. Both are fixed —the bench rounds up, the server rejects at startup with a clear message rather
than throwing during engine init — and the two fixtures that carried
kBlockSize = 8are now 16.build-test-cpu-arm64confirms the two previously red suites(
test_kimi_linear_paged,test_bench) now pass.Maintainer changes on top
VT_ATTN_SELECT_LOGadded toscripts/env-doc-allowlist.txtbeside itssibling
VT_KV_ALLOC_LOG;check-env-docrefused the change without it.docs/USAGE.mdstilldescribed
--block-sizeas an unconstrainedKV block size, andinclude/vllm.hstill told embedders<= 0 => 32with no constraint — so anembedder calling
vllm_engine_loadwithblock_size = 8would newly throw withnothing in the header to explain it. Both now state the requirement.
mainrather than cherry-picked; the branch predates mainenough that replaying its diff conflicts, while the merge is clean.
Owed
CheckKvCacheShapeis well tested; its production call site is not. Deletingrunner.cpp's install of it leaves the gate green, because no test drives therunner with a non-multiple-of-16 block size and none registers a mis-shaped
backend for the device the runner resolves. A case in
test_runner.cppthatbuilds the runner with
kBlockSize = 8and assertsinitialize_kv_cachethrowswould close it and double as the executable statement of the contract above.
This edits the runner while
.agents/backend-matrix.md:239recordsBACKEND-ROCMas landing with "ZERO selector/model/runner edit". Reaching a dead seam is worth
doing, but it belongs to its own row and issue rather than riding #41.
Issue: #41
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]