Skip to content

hybrid-decode-pad

Source: fixes/hybrid-decode-pad/

Keep decode padding out of live KV blocks on hybrid models whose attention KV block is split into smaller kernel blocks.

Affects hybrid models (GDN, linear attention or Mamba next to attention), e.g. Qwen3.5 / Qwen3-Next class
Releases v0.26.0, v0.30.0
Patches vllm_gaudi/v1/worker/hpu_model_runner.py
Upstream vllm-gaudi#1912 (open, same fix in the runner)
Test test_hybrid_decode_pad.py (torch; the source tests need a runner source)

Problem

On a hybrid model vLLM sizes the attention KV block so that one attention page holds one recurrent state, for example 512, 640, 896 or 1152 tokens. The HPU attention kernel works in smaller kernel blocks (128 tokens), so initialize_kv_cache splits every KV block into k kernel blocks, allocates num_blocks * k + 1 kernel blocks (the last one is a spare), and the attention block tables hold kernel-block ids. The pad ids are still set from the KV-block count (_PAD_BLOCK_ID = num_blocks). Kernel block num_blocks is not the spare: it lies inside KV block num_blocks // k, a live block of the pool.

Symptom

Decode batches are padded up to bucket sizes, so every decode batch whose size is not a bucket size writes the padding rows' K/V over a few context tokens of whichever request owns that block, in every attention layer. That request silently loses part of its context (for example one digit of a long number read back wrong), and with prefix caching a cached copy of the block is corrupted for later hits too. Nothing is logged.

Who is affected

Hybrid models whose attention KV block size is not a supported kernel block size. Models whose KV block equals the kernel block (every non-hybrid model, and hybrids whose block size happens to be a kernel size) are not affected: there num_blocks is the spare block.

The fix

The fix wraps initialize_kv_cache. When an attention group's KV block is split into kernel blocks, the pad ids move to the last kernel block of the attention tensors: the spare block after the pool with block splitting, or the last kernel block of the spare KV block with the shared layout. Reads of padding rows then hit the spare block as well. Unsplit layouts are left alone (one log line). KV-cache initialisation fails with RuntimeError if the layout is split but the attention tensors do not end in a spare kernel block outside the pool. The Mamba pad is not touched. The upstream pull request makes the same change inside initialize_kv_cache and also fixes the speculative decoding pad slots; when it is present, apply.py reports "not needed".

Verify

python3 fixes/hybrid-decode-pad/apply.py --check   # 0 = applied or not needed, 1 = not applied, 2 = source drifted
GAUDI_FIXES_ROOT=/path/to/site-packages pytest fixes/hybrid-decode-pad

At startup the server logs where the decode pad moved. End to end: on a hybrid model, keep three (or five, seven) requests decoding together while one of them holds a long context with a distinctive string, then ask for the string back. Without the fix the answer is occasionally wrong in a few characters.