ggml-meta : split a host-resident KV cache by head - #66
Open
Piggidragon wants to merge 9 commits into
Open
Conversation
This was referenced Sep 3, 2026
Owner
|
Automated preliminary review by Codex; the repository owner plans a separate manual review. Verdict: CONDITIONAL PASS. Blocking
Will slow review
Nits
Developmental progress
Reviewed head: 9db019e |
A copy of an input is named "<backend>#<source>#<copy>" in a name field of fixed size. With many devices the backend label of the meta backend lists all of them and the source name is what gets cut, so a consumer can no longer tell which tensor the copy was made from. Cut the label instead. Assisted-by: Claude Opus 5
A KV cache in host memory reaches attention as a scheduler copy, which is a leaf in the compute buffer. Such a leaf never reached the device split-state callback and fell through to MIRRORED, while the queries stayed split by head: each device then attended heads whose keys live on the other device. With more than one KV head that aborts in the FlashAttention kernel, or returns wrong output where the query split happens to stay a multiple of the KV head count. Offer a copied-in leaf to the callback under the name the graph gave it, and let the callback recognise the cache there. The cache folds its heads into one flat axis but the copy arrives permuted, with the heads on an axis of their own, so its segments and granularity are rescaled to whole heads. Assisted-by: Claude Opus 5
The KV granularity was derived from the query granularity through n_gqa, which assumes the V side has the head size of the K side. Count whole KV heads and scale each side by its own head size. Assisted-by: Claude Opus 5
A fused QKV puts Kcur and Vcur in a strided view, which a host-resident cache reads back through the meta buffer. The rows split, so the chunk splice could not express the read. Assisted-by: Claude Opus 5
A linear-attention op packs the state it writes back together with its output, so that split does not line up with the one a host-resident state expects. On device the two orders agree; in host memory they disagree, and the split state of the fused op no longer resolves. The state is small next to the attention cache that -nkvo exists to move, so keep it device-resident. Assisted-by: Claude Opus 5
Run the tensor-split architecture matrix a second time with the cache in host memory, and add an 8-device CI run, where the scheduler copy name is long enough to be truncated. Assisted-by: Claude Opus 5
The strided head split path ran only for a single stream, so a host-resident cache built with --parallel N fell through to the chunk splice, which cannot express that write. Loop over ne[3] and offset each side by its own stride. Assisted-by: Claude Opus 5
A split that ends with a view of a host tensor left the subgraph bookkeeping short of the node count and aborted. That happens with more than one cache stream, which the test matrix now covers. Also make the scheduler copy name a stated contract instead of a grammar that two files reconstruct on their own, note the 2d transfer fallback at both strided cache paths, say out loud that split mode tensor overrides the recurrent state placement, and record the Gemma 4 host cache accuracy gap where it is skipped. Assisted-by: Claude Opus 5
Piggidragon
force-pushed
the
mgpu/host-kv-split-by-head
branch
from
September 6, 2026 06:32
5c917a8 to
8206130
Compare
The scheduler flags a copy as an input when it keeps more than one, which hid a host-resident cache from the split state callback. A cut source name now asserts instead of naming another tensor, and memset writes a head split like set_tensor does. Assisted-by: Claude Opus 5
This was referenced Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
--split-mode tensortogether with--no-kv-offloadeither aborts or silently returns wrongoutput on any model with more than one KV head. This fixes that.
This is the correctness part of #57, which was too large to review as one change. #57 can be closed
in favour of this set:
-sm tensor--attn-splitCLAUDE.mdsymlinkThe defect
With
-sm tensor,ggml-backend-metaasks the model's split-state callback how each tensor isdivided across devices, but only for tensors outside a compute buffer:
With
-nkvothe cache lives in host memory, so it is not a tensor of the meta backend at all. Thescheduler copies it in, and that copy is a leaf in the compute buffer. It never reaches the callback
and falls through to
MIRROREDwhile the queries stay split by head, so both devices hold every KVhead but only half the query heads.
handle_flash_attn_extthen takes itskv_mirroredbranch,which is correct only for MLA with one KV head, and each device's renumbered queries attend the
other device's keys.
Whether that aborts or only produces garbage depends on whether the per-device query head count
stays a multiple of the KV head count.
The commits
ggml : keep the source name of a scheduler copy- a copy is named<backend>#<source>#<copy>in a name field of fixed size. The meta backend's label lists every device, so with enough devices
the source name is what gets cut and the consumer can no longer tell which tensor the copy came
from. Cut the label instead.
ggml-meta : split a host-resident KV cache by head- offer a copied-in compute-buffer leafto the split-state callback under the name it was copied from, and split it on the head axis. The
copy arrives permuted as
[head_dim, n_kv, n_head_kv, n_stream], so the cache's segments andgranularity are rescaled to whole heads. Every other copied-in leaf keeps the mirrored fallback.
llama : split a V tensor on its own head size- the KV granularity was derived from the querygranularity through
n_gqa, which assumes the V side has the head size of the K side.ggml : read back a strided row split from a meta buffer- a fused QKV puts Kcur and Vcur in astrided view that a host-resident cache reads back through the meta buffer.
llama : keep the recurrent state on device under split mode tensor- a linear-attention oppacks the state it writes back together with its output, so that split does not line up with the
one a host-resident state expects. Needed for any hybrid or recurrent model to run at all here.
tests : cover a host-resident KV cache split by tensor- see below.Testing
Two GPUs, RTX 4070 (gen4 x16) + RTX 3060 (gen3 x4), CUDA, NCCL, stock clocks. Greedy,
--temp 0 --top-k 1 -s 1, 48 tokens,-c 4096,-fit off,-ts 50,50for-sm tensor, sha256prefix of the output. "before" is
llama/devat9dc93fd62.-sm tensor -nkvo, before and after:defcc467489b9fe9e07cb42d45be171fggml-backend-meta.cpp:82692e1787dfbff1354ggml-backend-meta.cpp:826361ca91089116566a80adbb9cefb12799b50b14b17935032ggml-backend-meta.cpp:82658f6e8d41f1747f0-ts 55,45 -nkvofattn.cu:506 GGML_ASSERT(Q->ne[2] % K->ne[2] == 0)1bc54856ec2b7c82Each "after" hash equals that model's
-sm tensorhash, so the host-resident cache now agrees withthe device-resident one.
Nothing changes where it already worked. All 15 configurations below are bit-identical before and
after:
-sm layer-sm layer -nkvo-sm tensor1243fdca3e993373adbdcb93dcd4bf77e07cb42d45be171f1872d08d9c8156111872d08d9c81561192e1787dfbff1354361ca91089116566361ca91089116566361ca91089116566287db7134353eb01287db7134353eb019b50b14b1793503258f6e8d41f1747f058f6e8d41f1747f058f6e8d41f1747f0gemma-4 is the one model where
-sm layerand-sm layer -nkvodiffer, before and after alike. Theperplexity above puts that at 0.10%, i.e. one token flipping on float noise rather than a defect.
Uneven splits
gemma-4,
-sm tensor, with and without-nkvo. Identical in every case, and none of them abort:-ts1bc54856ec2b7c821bc54856ec2b7c82bff10e2e7c2eddfebff10e2e7c2eddfebe56a72280d7ff0ebe56a72280d7ff0efa424d57e41011a2fa424d57e41011a2Perplexity
-c 2048over a 15216-token slice of this repository's docs,llama-perplexity, 7 chunks.-sm layer-sm tensor-sm tensor -nkvo-sm layer-sm tensor-sm tensor -nkvoEvery device-resident configuration is unchanged to four decimals. Qwen3.8 host-resident now matches
its device-resident value exactly.
gemma-4 is iSWA and does not reach that identity: 235.0273 against 227.2279, a 3.4% gap, from
5.96e10 before. The output is coherent and the greedy hashes above match, so this is not the
mirrored-head defect. It is listed under "not fixed here" rather than claimed as solved.
The gap is specific to
-sm tensor. Under-sm layerthe same model and cache reach parity:-sm layer-sm tensorso a host-resident iSWA cache is not generally at fault; something in the tensor-split path is. That
0.10% is also what makes the two
-sm layerhashes in the table below differ - a single greedy tokenflipping on float noise, not a defect.
Throughput
llama-bench -ngl 99 -ts 50/50 -r 3. Onllama/devthe-sm tensor -nkvorow aborts the wholellama-benchprocess, which is why there is no before column for it.Nothing that worked before moves outside run-to-run noise. On Qwen3.8 a host-resident cache under
-sm tensorreaches 25.61 tg against 8.68 under-sm layer, which is what makes the option usable.Regression test
tests/test-llama-archs.cppran the tensor-split matrix with the default context parameters, whichleave KV offload on, so it could not reach this path at all. It now runs the meta configuration a
second time with
offload_kqv = false, andci/run.shadds an 8-device run, where the schedulercopy name is long enough to be truncated.
Every model in the matrix aborts or mismatches on
llama/devin theMeta -nkvocolumn and passeswith this PR.
test-llama-archs -s 1at 1, 2, 3 and 4 virtual CUDA devices and the 8-device-a llamarun all pass. Built with-DLLAMA_FATAL_WARNINGS=ON.Not fixed here
cache and lists the strided head-split write as the one piece missing for
-sm tensor; commit 2supplies the synchronous form it needs.
-sm tensor(commit 5 sidesteps it).-sm tensordoes not reach parity with a device-resident cache, as measuredabove (3.43%, against 0.10% under
-sm layer). The same combination has a second symptom: a mix ofdevice- and host-resident iSWA layers degrades, which is why kv-cache : resolve the partial KV residency set once for the model #67 refuses
--kv-gpu-layersfor iSWAunder
-sm tensor. Both are probably the one root cause, and the likely place to look is how themeta backend maps externally created views onto its per-buffer compute containers, which carries
its own FIXME already. Not root-caused here.
Requirements
Assisted-by:commit trailers.