Skip to content

llama : attention split on main, for the KV cache in VRAM - #15

Merged
Piggidragon merged 5 commits into
mainfrom
mgpu/attn-split-v2
Oct 16, 2026
Merged

Piggidragon merged 5 commits into
mainfrom
mgpu/attn-split-v2

Conversation

@Piggidragon

@Piggidragon Piggidragon commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Rebases --attn-split (#7) onto main, without the host-KV stack it depended on, so it works with the KV cache in VRAM. #7 stays unchanged.

With -sm tensor on GPUs of different speed, the faster GPU can now get more of the weights. The slower one takes more KV heads in exchange, so both still fit in memory.

What changed against #7

  • Rebased: the commits of llama : add an attention split separate from the tensor split #7 (option, validation, picking the split by the reference tensor) are carried over unchanged. Its tests depended on the host-KV structure of ggml-meta : split a host-resident KV cache by head #5 and are replaced.
  • Rounding spread over the layers: each layer gets whole KV heads. The remainder is spread over the layers by error diffusion, so the share over all layers follows the ratio within one head: -as 0.3,0.7 gives 20 of 68 KV heads (29.4%). Before, the rounding alternated between the GPUs and gave 25 heads (36.8%).
  • Log line: model load reports what the split came to, for example attention split, KV heads of all layers per device: device 0: 17 (25.0%), device 1: 51 (75.0%).
  • llama-bench -as: works like -ts and is shown in the results table. llama : add an attention split separate from the tensor split GenerelSchwerz/llama.cpp#69 listed it as missing.
  • Help text: says that the heads take their weights and KV cache along, and gives the full-VRAM use case.
  • Tests: test-llama-archs adds Meta -as 1/0 (all heads on the first GPU) and Meta -as 1/3, with the KV cache in VRAM. 214 OK on 4070 + 3060. The only failures are maion-coder MoE in the "Mixed batch" column, which also fails on a single GPU and on main.

Review

A review (reviewer-xhigh) found 5 issues; all are fixed in ff1a44e:

  • Crash (medium): the split boundaries were computed in float. A share that is inexact in binary, such as -as 0,0.9, gave Q/O and K/V/KV cache of one layer different head counts, and the server aborted at the first decode. -ts 0.45,0.45 did the same; that bug is older than this PR. The boundaries are now computed in double, with a small margin below whole units.
  • Overflow: shares whose sum overflows (-as 3e38,3e38) gave NaN. The shares are now stored as fractions of their sum, computed in double.
  • Doc: the overall share did not follow the ratio. It now does, see the error diffusion above.
  • Log: layers in host memory (-ngl) and layers without their own KV cache are no longer counted.
  • Silent no-op: a warning now appears when the shares of the devices in use add up to 0, or when no layer of the model splits its heads (e.g. DeepSeek V4).

Checked after the fixes on the 27B: -as 0,0.9, -ts 0.45,0.45, -as 0.3,0.7, -as 3e38,3e38, -as 0,0,1 and the production config all load and answer. test-llama-archs gives 214 OK for the -as configs.

Memory

Qwen3.8-27B UD-Q4_K_S. What follows which split:

part MiB follows
FFN 9023 -ts
linear attention (GDN) 3026 -ts
output 995 -ts
recurrent states (2 slots) 898 -ts
attention weights (17 layers incl. MTP) 1029 -as
KV cache, 262144 tokens q5_1 + MTP draft 6432 -as

Configurations that load with 2 slots, 262144 tokens and MTP on RTX 4070 + RTX 3060 (VRAM in use, MiB):

-ts -as KV heads 4070 4070 3060
0.5,0.5 — 50% 11754 11480
0.6,0.4 — 50% out of memory
0.55 – 0.6 1,3 25% out of memory on the 3060
0.63,0.37 1,3 25% 11880 11362
0.76,0.24 0,1 0% 11822 11410
0.56,0.44 0.375,0.625 36.8% 11672 11568

These are the sizes at load. While running, the 4070 needs about 150 MiB more (CUDA graphs, pools). -ts 0.63,0.37 -as 1,3 loads, but it runs out of memory at the first long request (cudaGraphInstantiate). -ts 0.625,0.375 -as 1,3 runs at full context with 10k and 95k-token prompts and keeps about 550 MiB free on both GPUs at peak. The linear-attention layers split in 16 head groups, so -ts moves their share in coarse steps (0.625 is exactly 10 of 16). That is also why 0.61 already overfills the 3060.

Speed

llama-bench, q5_1 cache, native FA, -ub 512 -b 1024, NCCL, t/s:

config pp512 pp3 tg32 pp512 @32k pp3 @32k tg32 @32k
50/50 652 74.8 29.8 570 61.5 28.7
-ts 0.63 -as 1/3 727 (+12%) 86.0 (+15%) 33.1 (+11%) 596 (+4%) 68.4 (+11%) 31.3 (+9%)
-ts 0.76 -as 0/1 817 (+25%) 80.3 (+7%) 29.4 (−1%) 619 (+9%) 63.9 (+4%) 27.5 (−4%)
-ts 0.56 -as 0.375 677 (+4%) 80.0 (+7%) 32.0 (+7%) 576 (+1%) 65.0 (+6%) 30.6 (+7%)

Decode is bound by memory bandwidth (4070: 504 GB/s, 3060: 360 GB/s), prefill by compute (about 2.2:1). 0.63 sits between the two ratios and is the best point overall. All KV heads on the 3060 helps only prefill, because it moves the long-context attention to the slower card.

Together with #14 (small all-reduces through the internal pipeline), -ts 0.63 -as 1/3 against production 50/50:

test t/s change
tg32 36.7 +23%
pp3 99.1 +33%
pp512 726 +11%
tg32 @32k 34.4 +20%
pp3 @32k 76.4 +24%
pp512 @32k 596 +5%

🤖 Generated with Claude Code

Piggidragon and others added 5 commits October 16, 2026 10:58
The share of the attention heads a device gets decides both how much of a host-
resident cache it receives and how much attention work it does, and neither has
to follow the memory split. Add --attn-split (-as) to set it; unset it follows
--tensor-split and nothing changes. The share is rounded to whole heads by the
granularity rules that already keep GQA consistent.

Assisted-by: Claude Opus 5
(cherry picked from commit 88c3a20)
std::stof took a partial parse, a negative, a NaN and an all-zero set without a
word, and those reach a cumulative division and a conversion to whole heads. Reject
them at the command line and fall back to the tensor split at the API boundary.

Also say what the rounding does instead of promising the nearest share.

Assisted-by: Claude Opus 5
(cherry picked from commit aef345a)
A substring of the name also caught tensors whose split does not count heads.

Assisted-by: Claude Opus 5
(cherry picked from commit 6fe4274)
… log the result

The attention split now applies to a device-resident KV cache on main, without
the host KV changes it was stacked on.

- each layer gets whole KV heads and the rounding goes to a different device
  from layer to layer, so the share over all layers follows the ratio; the help
  said the shares are rounded down
- the model load logs the KV heads each device got over all layers
- llama-bench takes -as like -ts
- test-llama-archs runs the tensor split with all heads on the first device and
  with heads split 1/3

Qwen3.8-27B UD-Q4_K_S on an RTX 4070 + RTX 3060, 262144 tokens of q5_1 cache in
2 slots and MTP: -ts 0.6,0.4 alone does not fit, -ts 0.63,0.37 -as 1,3 does
(11880 / 11362 MiB) and is 11-15% faster than 50/50 at short context and 4-11%
at 32k (pp512 652 -> 727, pp3 74.8 -> 86.0, tg32 29.8 -> 33.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the split boundaries are computed in double and with a margin below whole
  units: a share that is inexact in binary rounded the tensors of a layer to
  different head counts (-as 0,0.9 or -ts 0.45,0.45 aborted at the first decode
  on Qwen3.8-27B)
- the attention split spreads the rounding over the layers of a kind, so the
  heads over all layers follow the ratio within one head: -as 0.3,0.7 gives
  20 of 68 KV heads (29.4%) instead of 25 (36.8%)
- the shares are kept as fractions of their sum, computed in double, so large
  shares cannot overflow into NaN
- the load log skips layers in host memory and layers without their own KV, and
  warns when the shares of the devices in use add up to 0 or no layer of the
  model splits its heads by attn_output.weight

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant