Repository navigation
[ROCm] Octave KV (ROCM_OCTAVE): native 3-bit KV cache backend for AMD GPUs - #59774
Conversation
…native HIP kernels Adds the ROCM_OCTAVE attention backend and the octave_k3v3_compact, octave_k3v3 and octave_k3v4 cache dtypes: random signs + Hadamard rotation, Lloyd-Max codebooks applied with v_perm_b32 tables, norm-preserving scales. Store, portable decode, reduce and rotation run on every AMD target; RDNA3 (gfx1100-1103) gets WMMA decode and prefill. No Triton fallback: an architecture without a native prefill kernel is rejected at startup. Design and architecture contract: docs/design/rocm_octave.md. Signed-off-by: JartX <sagformas@epdcenter.es>
|
Documentation preview: https://vllm--59774.org.readthedocs.build/en/59774/ |
ROCM_OCTAVE): native 3-bit KV cache backend for AMD GPUsROCM_OCTAVE): native 3-bit KV cache backend for AMD GPUs
…M8K evals The decode split-KV partials were allocated per attention layer at the cudagraph capture size. With vLLM defaults on Qwen3.5-4B that is ~4.3 GB across 8 layers, which left 2.8 GiB for the KV cache and aborted startup (not enough Mamba blocks for max_num_seqs). Layers run sequentially, so the partials now come from the shared WorkspaceManager, reserved once by the metadata builder, as TURBOQUANT does: 6.9 GiB and 220k KV tokens. Tests: - store kernel leaves padding slots (-1) untouched - Python slot size matches octave::Format - octave_* cache dtypes map to their KV quant modes and uint8 storage - GSM8K configs for Qwen3.5-4B (head 256, 64 rotary dims), models-octave.txt GSM8K, Qwen/Qwen3.5-4B, 1319 questions, 5-shot (gfx1100): bf16 KV 77.94 | octave_k3v4 78.01 | octave_k3v3 76.35 | compact 75.66 Signed-off-by: JartX <sagformas@epdcenter.es>
Match the names of the upstream PR vllm-project#59774: the ROCM_OCTAVE backend and the octave_k3v4, octave_k3v3 and octave_k3v3_compact cache dtypes replace ROCM_SPLITQ and splitq_*. The kernels, format, reference, tests and GSM8K configs are now identical to the PR's, except for 5f0ce9e (query maps sized for one KV block), which the PR lacks. The WMMA kernels are compiled for gfx1100-1103 only and are empty stubs elsewhere, so _ARCH_TIERS keys them on "gfx110" instead of "gfx11", which also matched gfx1150/1151. Launch commands using --kv-cache-dtype splitq_* or ROCM_SPLITQ must switch to the new names. Signed-off-by: JartX <sagformas@epdcenter.es>
|
This pull request has merge conflicts that must be resolved before it can be |
… qwen4_exp QSA Add octave_decode_sparse: the WMMA split-KV decode in a mode where query row i attends to the logical positions indices[i, :] (negative entries skipped) instead of its first q_to_klen[i] tokens. Rows do not share positions, so each is its own segment; K, V and scale loads take a per-token slot pointer. Everything else (dequantization, WMMA, split-KV, reduce with the V un-rotation) is shared with the dense decode. Let the qwen4_exp QSA owner keep its main K/V cache in Octave format (octave_k3v4, octave_k3v3, octave_k3v3_compact): the cache is written with octave_cache_store and the sparse attention over the indexer's top-k runs octave_decode_sparse. The split-KV partials are reserved in the shared workspace before it is locked. Kernel tested against the PyTorch reference (k3v3/k3v4, GQA groups 6, 8 and 6x2, random position subsets with holes: rel. err. 3.3e-4) and, with indices 0..L-1, bit-identical to the dense decode. Qwen3.8-Flash-Next TP4 fp16: KV cache 87k -> 238k tokens. Signed-off-by: JartX <sagformas@epdcenter.es>
Quantizing K/V to 3 bits costs qwen4_exp most where its attention is sharpest: the last 64 positions and the first 4 (attention sinks). Keep an fp16 shadow copy of those tokens in small direct-mapped tables keyed by cache slot (tagged, so a lost entry falls back to the quantized path). The sparse WMMA decode masks window hits, and a small exact kernel adds them back as one more split before the reduce. End-to-end NLL vs fp16 (12 x ~8k-token texts / 24 short texts), k3v3: pos>=2048 +3.6% -> +1.5% pos<2048 +5.1% -> noise short +6.0% -> +0.7% KV capacity is unchanged (155,930 vs 47,409 tokens in fp16). Signed-off-by: JartX <sagformas@epdcenter.es>
Signed-off-by: JartX <sagformas@epdcenter.es>
…parse decode tests Drop the temporary VLLM_QSA_OCTAVE_WINDOW switch: the fp16 sink and recent window is always on for an Octave QSA cache. Test the sparse decode against the PyTorch reference with the window off, on, and with evicted entries falling back to the packed cache. Signed-off-by: JartX <sagformas@epdcenter.es>
|
This pull request has merge conflicts that must be resolved before it can be |
Qwen3.8-27B on Dual RX 7900 XTX: Benchmarking KV Cache Quantization in vLLMI wanted to see how well Qwen3.8-27B would run on two AMD Radeon RX 7900 XTX GPUs (24 GB each), especially during long conversations. The goal was straightforward: reduce GPU memory usage by quantizing the KV cache, while keeping generation speed and answer quality as close as possible to the unquantized setup. I compared five KV-cache configurations in vLLM, all using Main results
The three Octave variants sustained roughly 44–45 tokens/s even with ~180K tokens of context. The capacity figures above describe the aggregate KV-cache token capacity reported by vLLM, not the maximum length of a single conversation. For these tests, One important correction came out of the testing. Our first measurements made long-context generation look extremely slow, but the issue was in our benchmark script rather than the quantized cache. We estimated how long prompt processing (prefill) would take and accidentally counted much of that time as generation. We fixed the measurement by using streaming and timing the interval between the first and last received output tokens. That revealed the real bottleneck: processing a very long prompt before generation begins. At around 180K context tokens, the quantized modes took roughly two minutes to produce the first token. Once generation started, however, they still ran at around 44 tokens/s. This distinction matters when judging how responsive the model feels in everyday use. Did quantization affect quality?We did not observe a clear quality difference in these tests. All five configurations answered our 10 reasoning questions correctly and passed the needle-in-a-haystack retrieval checks at the tested context lengths. We then used vLLM's GSM8K evaluation ( We also measured server throughput using the official
Which one would I use?For this setup, I would choose These findings apply to two RX 7900 XTX GPUs and this particular vLLM build. Other hardware or software configurations may produce a different ranking. Tests carried out October 10–13, 2026. Backend: Octave, vLLM PR #59774. |
|
Thanks a lot for taking the time to run this on 2× 7900 XTX with a third-party AWQ checkpoint. An independent setup like this is exactly the kind of data that's hard to get on our own boxes. Two small suggestions, if you have time for another pass:
On the ~2 min TTFT at 180K: that's mostly prefill and expected at that length on two cards. If you happen to have the |
|
@JartX @vidriagon Given that we can reach a longer context length now, can you please evaluate on long context dataset e.g. NIAH ? |
|
@tjtanaa Of course Whatever you ask for, I will do. |
|
Thanks for the detailed feedback, really helpful. Reran both points: GSM8K via chat endpoint. You were right — with
Relative ranking is basically unchanged (all within noise), but now the absolute numbers should be comparable to other reports. Thanks for catching that.
TTFT is basically identical, so that confirms Octave isn't adding any prefill overhead — the ~2 min figure at 180K is just prefill scaling with context, same as you'd expect. But the decode gap is already there at 8K, not something that only shows up at long context — |
| from vllm.platforms import current_platform | ||
| from vllm.v1.attention.ops import rocm_octave as sq | ||
|
|
||
| pytestmark = pytest.mark.skipif( |
There was a problem hiding this comment.
@JartX Given that this is only validated on gfx1100, please make sure that the tests are gated to only run on this hardware. We will relax the condition once validation has been done on other platform.
| return vllm_config.compilation_config.max_cudagraph_capture_size or 4 | ||
|
|
||
|
|
||
| class RocmOctaveAttentionBackend(AttentionBackend): |
There was a problem hiding this comment.
we should also override this supports_compute_capability to only validated architecture.
Add get_rdna_version() next to get_cdna_version() and use both in the Octave backend and tests instead of the on_gfx* helpers. The kernels are still built and dispatched only for gfx1100 and gfx942. Signed-off-by: JartX <sagformas@epdcenter.es>
|
@tjtanaa done, pushed in 884e0fb: I added The kernels are still only built and dispatched for gfx1100 and gfx942, the chips I validated, so another RDNA3 chip passes selection but gets a clear "not validated" error from the kernel dispatch instead of running untested code. Happy to open it up to the whole gfx11 family if you prefer. Tested on gfx1100: |
| return attn_type == AttentionType.DECODER | ||
|
|
||
| @classmethod | ||
| def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]: |
There was a problem hiding this comment.
@JartX This is not tested in the unit test. Please add a unit test for this.
There was a problem hiding this comment.
Added test_kernels_follow_declared_cache_layouts in test_rocm_octave.py (5da887e): for every layout in supported_kv_cache_layouts() it builds the cache view vLLM allocates (customize_spec + dense_kv_cache_views, two KV heads so LBHNC and LBNHC really differ) and checks that store, decode (native and portable tier) and prefill match a contiguous cache bit for bit. I also checked it catches a kernel that ignores the strides (LBNHC then fails). gfx1100: 107 passed.
Store, decode and prefill on the cache view allocated for each layout in supported_kv_cache_layouts() must match a contiguous cache bit for bit. Signed-off-by: JartX <sagformas@epdcenter.es>
|
/ci run |
|
✅ Triggered Buildkite CI #97126 for commit ℹ️ This PR is 358 commits behind upstream |
models-octave.txt and its three Qwen3.5-4B configs were not referenced by any Buildkite step, so the Octave end-to-end accuracy check never ran in CI. Add an MI300 step for it to the vllm/ci pipeline (lm_eval.yaml) and to test-amd.yaml. MI300 because the Octave ops are built only for gfx942 and gfx1100; MI355 (gfx950) would fail backend selection. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: tjtanaa <tunjian.tan@embeddedllm.com>
The MFMA decode needs a head's tokens contiguous (it loads 16 of them as one tile); with LBNHC it fell back to the portable kernel without notice. Signed-off-by: JartX <sagformas@epdcenter.es>
|
/ci run |
|
✅ Triggered Buildkite CI #97134 for commit ℹ️ This PR is 4 commits behind upstream |
Octave KV is upstream now (vllm-project#59774): take upstream's version and reapply the per-layer exact window of the sparse decode (eb8b358). - RDNA3 per-token-head INT8/INT4 ops move to their own legacy _C binding file (csrc/attention/rdna3_torch_bindings.cpp), since upstream turned csrc/torch_bindings.cpp into the stable-ABI library. - NF4 PLE ported to upstream's PLE embedding-method API (lookup_width hook; packed rows are still reduced across ranks as raw bytes). - gfx11 skinny-GEMM routing moved into _gfx11_skinny_gemm, gfx11 only. - AMD QSA fused pre-indexer/prepare kept to BF16 models (it rounds through BF16); fp16 Flash-Next keeps the unfused path. - Prefill lane follows the BlockTables API without kernel_block_sizes. - KVQuantMode.CUSTOM = 15 (upstream aliased it to OCTAVE_K3V4, vllm-project#63691). - Qwen3 parser: a <tool_call> opener ends an unclosed parameter only when a <function= follows (upstream vllm-project#59928 keeps a literal one in a value). - qwen4_exp experiment hooks kept as they were. Built for gfx1100 on torch 2.11; our kernels at scratch 0; 540 tests pass (octave, moe octw3, moe w4a16, ple, unquantized gemm, qwen3 parser, kv dtype). Signed-off-by: JartX <sagformas@epdcenter.es>
FlashAttentionImpl.__init__ rejects quantized caches FlashAttention cannot read, and octave_* is quantized since vllm-project#59774. QSA reads Octave with its own kernel, so Qwen4Exp with --kv-cache-dtype octave_* failed at startup. Pass "auto" to the parent for Octave and keep the real dtype. Signed-off-by: JartX <sagformas@epdcenter.es>
…MD GPUs (vllm-project#59774) Signed-off-by: JartX <sagformas@epdcenter.es> Signed-off-by: tjtanaa <tunjian.tan@embeddedllm.com> Co-authored-by: tjtanaa <tunjian.tan@embeddedllm.com> Signed-off-by: Christopher Mackenzie <christopher.mackenzie.software@gmail.com>
…MD GPUs (vllm-project#59774) Signed-off-by: JartX <sagformas@epdcenter.es> Signed-off-by: tjtanaa <tunjian.tan@embeddedllm.com> Co-authored-by: tjtanaa <tunjian.tan@embeddedllm.com> Signed-off-by: Christopher Mackenzie <christopher.mackenzie.software@gmail.com>
Brings in upstream main through vllm-project#61529, including Octave KV (vllm-project#59774). mxfp4_qsa now sits alongside the Octave dtypes in _QSA_CACHE_DTYPES, and the two cache-update overrides are merged into one method. Signed-off-by: Limpus, David <dlimpus@amd.com>
Brings in upstream main through vllm-project#61529, including Octave KV (vllm-project#59774). mxfp4_qsa now sits alongside the Octave dtypes in _QSA_CACHE_DTYPES, and the two cache-update overrides are merged into one method. Signed-off-by: Limpus, David <dlimpus@amd.com>
Overview
Adds
ROCM_OCTAVE, an attention backend that keeps the KV cache offull-attention layers at 3-4 bits per value, with native HIP kernels for
RDNA3 (gfx1100) and MI300X (gfx942). Select it with
--kv-cache-dtype octave_k3v3,octave_k3v4oroctave_k3v3_compact(216, 248 and 196 bytes per slot at head size 256, against 1024 for fp16).
RFC: #59773.
docs/design/rocm_octave.mddescribes the format and theper-architecture contract.
Claims
53 vs 27 concurrent 128k requests).
on both architectures. Next-token NLL at 380k is +0.0009 to +0.0015 nats
over fp16.
faster from 32k on (65 vs 103 µs at 128k).
MTP verification 11× faster.
We have served it in production on RDNA3 since October 2, 2026.
supports_compute_capabilityrejects other devices.Validation
Models. Qwen3.8-27B W4A16 (compressed-tensors, int4, group 32). To
reproduce,
cyankiwi/Qwen3.8-27B-AWQ-INT4is a public checkpoint with the same scheme and architecture; KV token
counts will differ slightly.
Tests (
pytest tests/kernels/attention/test_rocm_octave.py), on thisbranch at 28aa54e (merged with
main3897f95):test_rocm_octave.py.soall three cache dtypes. That covers:
times: a wrong codebook, a least-squares scale, a wrong
log2(e), a missingquery debias. Each time the relevant tests failed.
VLLM_GPU_SYNC_CHECK=erroron gfx942: servedcyankiwi/Qwen3.8-27B-AWQ-INT4with
octave_k3v3and MTP k=3 through 40 GSM8K questions with no syncerror (97.5%).
update-dockerfile-graph,which needs
/bin/bashand cannot run on my machine. mypy (CI stage)passes.
kernels/attentionstep runstest_rocm_octave.pyon itsMI300 runners (gfx942). gfx1100 is not in CI; it was validated locally as
above.
MI300X end to end. Setup:
--max-model-len 131072; only the cachedtype changes between rows.
tests/evals/gsm8k/gsm8k_eval.py, chat, thinking off, 200questions, temperature 0.6.
vllm bench serve.--attention-backend ROCM_AITER_FA. With onlyVLLM_ROCM_USE_AITER=1, vLLM picksROCM_ATTN.octave_k3v3octave_k3v4octave_k3v3_compactROCM_AITER_FAROCM_ATTNAITER is ahead with 16 users and in time to first token at 100k, which points
at our prefill over a cached prefix; that is the next thing to work on.
MI300X: NIAH in detail
The haystack is vLLM's own Python source, cut to 126k tokens. 16 keys are
inserted every ~7.7k tokens, each a line such as
# PROJECT KEY Aldebaran: WK3DEGZD, with star names as look-alike labels andrandom 8-character values. The same document is cut at 32k, 64k and 126k,
which hold 4, 8 and 16 keys. For each key present the model is asked, thinking
off, for only that key's value; an answer counts if it contains the exact
value. Keys sit between 3% and 95% of the context.
octave_k3v3octave_k3v4octave_k3v3_compactROCM_AITER_FAROCM_ATTNThe model saturates this task at 128k. So it shows that the 3-bit cache loses
no retrieval there; it does not rank the formats. The harder test is the 800k
key curve in the RFC, where the keys start to blur only past 640k, because of
the model and not the cache.
MI300X:
vllm bench servein detailSetup:
--dataset-name random), 512 output tokens with--ignore-eos,--seed 0, no prefix reuse.No request failed. Each cell is median TTFT / median TPOT / output tok/s.
Random tokens give MTP a low acceptance, the same for every row.
octave_k3v3octave_k3v4octave_k3v3_compactROCM_AITER_FAROCM_ATTNWithout MTP, to compare with UltraQuant (#57057), which needs bf16 here:
octave_k3v3(fp16)ultraquant_4bit(bf16)On gfx942 UltraQuant's FlyDSL decode is not used, because it targets gfx950
and does not cover 6 query heads per KV head, so this is its Triton fallback.
With MTP k=3 its output was not usable on this setup (GSM8K 1%, NIAH 0/28);
without MTP it is correct (GSM8K 98.0, NIAH 28/28). This says nothing about
UltraQuant on gfx950.
MI300X: attention per layer
benchmarks/attention_benchmarks, the whole model on one GPU: 24 query heads, 4 KV heads.octave_k3v3Prefill, against
ROCM_AITER_FAwith an fp16 cache:RDNA3 (4× RX 7900 XTX, TP4) evaluation
Setup:
TurboQuant is correct.
turboquant_k3v4_ncoctave_k3v4octave_k3v3octave_k3v3_compactAttention per layer at 380k (one TP4 shard):
turboquant_k3v4_ncoctave_k3v3_compactoctave_k3v3Time to first token at 380k, end to end with MTP and a cold prefix:
octave_k3v3_compact: 297 s;At 100k all three are within 8%, because the GEMMs and the all-reduce
dominate.
When I replaced the metadata builder the backend first inherited with its
own, I checked that nothing moved (TP4, MTP, GSM8K 500 at concurrency 6):
Sparse attention (qwen4_exp QSA) evaluation
Setup:
--max-num-batched-tokens 512, no prefix caching.prompt_logprobsover 12 documents of ~8k tokens, split atposition 2048, and over 24 short documents.
octave_k3v3, no windowoctave_k3v3That run used our fork for the fp16 port of QSA and two RDNA3 MoE fixes,
which go in separate PRs. On this branch alone the QSA path is bf16, like
upstream.
Details
What changes. Most of the PR is new files:
vllm/v1/attention/ops/rocm_octave.py: the format and its PyTorchreference, which defines the byte layout.
vllm/v1/attention/backends/rocm_octave_attn.py: the backend and itsmetadata builder.
csrc/attention/octave_*, the kernels:architectures;
octave_torch_bindings.cpp.--modelplus the new cache dtypes inbenchmarks/attention_benchmarks.Changes to existing files are small:
vllm/config/cache.py,vllm/utils/torch_utils.pyandvllm/v1/kv_cache_interface.py;CMakeLists.txt;customize_specnow runs under the current vLLM config, because thebackend needs the model's rotary dims to size the slot;
qwen4_exp(QSA), Octave as the main cache through a sparse decode,with the new fused QSA prepare turned off for an Octave cache, since it
writes raw BF16 K/V.
Nothing changes for other backends or cache dtypes.
Design choices a reviewer may want to know about:
added once it has its own kernels and passes the tests and an end-to-end
run. The RFC lists what each one needs.
maps on the device without host syncs, supports
UNIFORM_BATCHCUDAgraphs, and refreshes the maps between fused MTP draft steps.
v_mfma_i32_16x16x32_i8, with the query split into high andlow int8 halves.
v_perm: each codebook byte becomes an exactfp16 denormal, which the MI300 MFMA keeps.
-amdgpu-mfma-vgpr-form, so theaccumulators do not halve the occupancy.
Qwen3.5 / 3.6 / 3.8.
gfx1100-only for now.
the RDNA3 ones. Each architecture lives in its own file behind the same
octave_prefill/octave_decodeentry points, so it can be reviewed andlater removed on its own.
Not a duplicate. I searched open PRs and issues for
octave,turboquant rocm,kv cache 3-bit,kv cache quantization hipandlloyd-max kv. The closest are:None targets RDNA with native kernels, and none runs below 8 bits with MTP on
ROCm.
AI assistance. I used AI assistance to write this PR: Claude, and
Qwen3.8-27B running locally on the boxes it was measured on. I have reviewed
every changed line, ran the tests and evaluations above, and can answer for
the change.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.