Skip to content

fix MHA KV-cache allocation and singleton mask broadcast across backends - #6869

Open
futz12 wants to merge 3 commits into
Tencent:masterfrom
futz12:fix/mha-kvcache-mask-broadcast
Open

fix MHA KV-cache allocation and singleton mask broadcast across backends#6869
futz12 wants to merge 3 commits into
Tencent:masterfrom
futz12:fix/mha-kvcache-mask-broadcast

Conversation

@futz12

@futz12 futz12 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes persistent KV-cache allocation and singleton 3D attention-mask broadcasting across the generic, x86, ARM, MIPS,
LoongArch, and Vulkan attention implementations.

It also fixes the equivalent singleton-mask issue in Vulkan SDPA.

Problems

Persistent MHA KV caches used the wrong allocator

The architecture-specific MHA implementations concatenated cached and current K/V tensors with:

  k_affine.create(dst_seqlen, embed_dim, elemsize);
  v_affine.create(dst_seqlen, embed_dim, elemsize);

These tensors are returned through top_blobs[1] and top_blobs[2] and persist across subsequent decoding steps. They must
therefore use the caller-provided blob allocator.

Affected optimized implementations:

  • x86
  • ARM
  • MIPS
  • LoongArch

The concatenated caches are now created with:

k_affine.create(dst_seqlen, embed_dim, elemsize, opt.blob_allocator);
v_affine.create(dst_seqlen, embed_dim, elemsize, opt.blob_allocator);

Singleton 3D masks were treated as per-head masks

An attention mask with shape:

(dst_seqlen, src_seqlen, 1)

is intended to be broadcast across every attention head.

Several implementations treated every 3D mask as a per-head mask and selected:

attn_mask_blob.channel(head)

For head > 0, this accesses a channel that does not exist.

Affected paths included:

  • generic MHA FP32
  • generic MHA INT8
  • generic weight block-quantized MHA
  • x86 MHA
  • ARM MHA
  • MIPS MHA
  • LoongArch MHA
  • Vulkan MHA
  • Vulkan SDPA normal path
  • Vulkan SDPA Flash Attention path
  • Vulkan SDPA cooperative-matrix path

Singleton masks now always select channel zero, while masks with multiple channels retain their per-head behavior.

Vulkan mask indexing

The Vulkan SDPA shaders use the following addressing logic:

mask_head = attn_mask_dims == 3 ? head : 0;
mask_index = mask_head * mask_cstep + row * dst_seqlen + column;

Previously, a dims=3, c=1 mask was still passed to the shader as a 3D per-head mask. Every head greater than zero therefore
read beyond the only allocated mask channel.

This was reproduced on an NVIDIA RTX 4060:

past_seqlen=2560
num_heads=4
mask_channels=1

value not match at c:1 h:0 w:1
expect 0.016805 but got 0.015332

The old implementation failed with exit code 1. After treating a singleton 3D mask as a 2D broadcast mask, the same test passes
with exit code 0.

Vulkan MHA additionally keeps 3D masks unpacked so that head packing cannot be confused with query-position packing.

Regression coverage

The MHA and SDPA tests now cover:

  • single-token decoding
  • multi-token decoding with 3 and 4 tokens
  • long KV caches up to 2560 tokens
  • singleton masks
  • per-head masks
  • INT8 MHA
  • weight block-quantized MHA
  • cross-attention cache reuse
  • 257-step rolling KV-cache decoding

Verification

The following tests pass:

test_multiheadattention_kvcache CPU: PASS
test_sdpa_kvcache CPU: PASS
test_multiheadattention_block_quant CPU: PASS
test_multiheadattention_kvcache Vulkan: PASS
test_sdpa_kvcache Vulkan: PASS

futz12 added 2 commits July 28, 2026 00:43
Keep persistent x86 KV caches on the blob allocator, broadcast single-channel attention masks across heads on CPU and Vulkan paths, and add long multi-token cache regressions.
Use the blob allocator for persistent concatenated K/V caches and broadcast singleton 3D attention masks from channel zero on all architecture-specific MHA paths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant