ggml-meta : split a host-resident KV cache by head - #57
Conversation
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
A linear-attention op packs the state it writes back together with its output, so the split of that state does not line up with the one the cache expects. On device that stays consistent, since each device reads back its own slice, but a host cache is laid out globally and the two orders disagree. Keep the recurrent state device-resident when the model is split by tensor. Next to the attention cache it is small, and --no-kv-offload keeps working on hybrid models instead of aborting. Assisted-by: Claude Opus 5
--kv-gpu-layers keeps the first N attention KV layers device-resident. Taking them in layer order fills the device that owns the first layers and leaves the free memory of the others unused, so on two GPUs only half the budget is reachable. Take one layer per owning device in turn instead. A single device, and a model split by tensor, see one device group and are unaffected. Assisted-by: Claude Opus 5
--kv-gpu-layers was ignored by every cache made of several sub-caches - iSWA and the DSA, DSV4 and MSA variants among them - because each sub-cache would have taken the full budget and kept up to N layers of its own. Resolve the device-resident layers once for the whole model instead. Each cache then honours a set of layer indices, so one budget covers all of its sub-caches and the per-layout opt-in is no longer needed. An iSWA cache split by tensor still keeps the whole cache in host memory: a mix of device- and host-resident layers stays coherent there but costs about 5% perplexity, which is not understood yet. Assisted-by: Claude Opus 5
A hybrid model counts every layer, so most of the budget went to layers the attention cache does not own. Qwen3.5 spent 3 of 4 requested layers that way. Assisted-by: Claude Opus 5
A device-resident layer saves the transfer it would cost, which is worth the most where the link is slowest. Two cards in different slots differ by several times, so measure the host-to-device bandwidth once and take the layers of the slowest device before the others. Devices of similar speed stay one group and keep the previous round-robin. Assisted-by: Claude Opus 5
Tensor parallelism splits attention by head, and the share a device gets decides both how much of a host-resident cache it receives and how much attention work it does. Neither has to follow the memory split, so give the heads a ratio of their own. Unset, --attn-split follows --tensor-split. Assisted-by: Claude Opus 5
…sor-split # Conflicts: # src/llama-model.cpp
An out of memory in a simple buft aborted the process instead of returning nullptr, so the caller could not fall back. Assisted-by: Claude Opus 5
A model without output.weight reuses the embedding table as the output projection, under the name of the table. Only that copy reaches the meta device, so the name is unambiguous there, but it fell through to MIRRORED and every device held the whole table and ran the whole projection. Assisted-by: Claude Opus 5
The scheduler builds "<backend>#<name>#<copy>" into GGML_MAX_NAME, and the meta backend name grows with the device count, so from five devices on the name is cut and the trailing part is gone. The old parse then returned the whole mangled string, the cache copy was not recognised and fell back to MIRRORED - each device attended the wrong heads. Assisted-by: Claude Opus 5
The tensors of the context already point at the meta buffer when a simple buffer fails, so freeing it leaves them dangling. Assisted-by: Claude Opus 5
The KV granularity came from n_embd_head_k, so a model with a value length of its own put the device boundaries between heads of the V cache. Under split mode tensor with a host-resident cache it aborted on the head assert. Assisted-by: Claude Opus 5
The set was resolved even with the cache fully offloaded, which measured every host link for nothing. The attention compute then moved off the CPU on the request alone, so a layout that keeps the whole cache in host memory streamed all of it to the device each ubatch. Report back what the cache got, and let the compute follow that. Assisted-by: Claude Opus 5
Archive: rejected optimizations and open workMoved out of the description to keep it to what this PR actually changes. Nothing here is part Same two GPUs, Rejected: even FFN split boundaries
Both lose prefill and neither gains generation. Rejected: size the compute buffer per device
Not implemented. It is memory only, never speed; the fix has to change what Rejected: pipeline parallelism with a host-resident cache
The control settles it. On
The scheduler's Rejected: backend sampling under
|
output.weight |
backend sampling | tg | VRAM CUDA0 + CUDA1 |
|---|---|---|---|
| axis 1 (this PR) | off | 25.23 | 9772 + 9522 MiB |
MIRRORED |
off | 24.36 | 10270 + 10020 MiB |
Mirroring the head costs 3.4% generation and 996 MiB before backend sampling wins anything back.
That is commit 8 run backwards. What it wins, on Qwen3-0.6B-Q8_0, 512 tokens:
-sm |
output.weight |
backend sampling | tg |
|---|---|---|---|
| layer | (n/a) | off | 318.8 |
| layer | (n/a) | on | 349.0 (+9.5%) |
| tensor | axis 1 | off | 214.9 |
| tensor | MIRRORED |
off | 207.7 |
| tensor | MIRRORED |
on | 211.8 (+2.0%) |
+9.5% under -sm layer is the ceiling, on a 0.6B model. The saving is a fixed ~0.27 ms per
token; on a 27B at 41 ms per token it is below 1%, against the 3.4% the mirrored head costs. It
is also not clean: with the head mirrored, the first sampled request aborts at
ggml-backend-meta.cpp:901, because build_sampling pads t_logits and a ubatch with no output
rows leaves result_output with zero elements, which has no defined split state.
Rejected: per (layer, device) residency for --kv-gpu-layers
Letting a layer be device-resident on one device and host-resident on the other. --attn-split
already reaches that point and pays no memory for it. Qwen3.8-27B-UD-Q5_K_M, -c 65536,
58780-token prompt, q8_0, -nkvo --kv-cpu-pinned -rso -sm tensor -ts 50,50 -n 128:
--attn-split |
--kv-gpu-layers |
VRAM CUDA0 + CUDA1 | pp | tg |
|---|---|---|---|---|
(follows -ts) |
0 | 10106 + 9852 | 426.94 | 2.40 |
1,0 |
0 | 10724 + 9230 | 495.54 | 7.35 |
(follows -ts) |
4 | 10376 + 10122 | 444.37 | 3.07 |
1,0 |
4 | 11266 + 9228 | 502.47 | 8.74 |
(follows -ts) |
8 | 10646 + 10392 | 464.56 | 4.26 |
1,0 |
8 | 11808 + 9228 | 507.82 | 10.71 |
(follows -ts) |
12 | 10916 + 10662 | 484.93 | 6.98 |
1,0 |
12 | cudaMalloc failed on CUDA0 |
||
(follows -ts) |
16 | 11152 + 10898 | 507.56 | 19.71 |
The pairs match on total VRAM to within 4 MiB, so those rows are equal-budget comparisons.
-as 1,0 at 8 layers is exactly what the finer knob would deliver at that budget. Below it,
-as 1,0 wins outright because it drops the slow link for free while a residency budget has to
buy its way out.
One narrow band is left: between half and full residency -as 1,0 runs out of room on the fast
device while the even split still pays the slow link. Per-device residency would reach about
14 t/s there against the even split's 6.98 - modelled, not measurable today. Against that stands
mixing buffer types inside a meta buffer or staging a slice per subgraph, both new machinery in
ggml-backend-meta.cpp. Rejected.
Rejected: overlapping the AllReduce with compute
Built and measured. Worth +3.0% prefill, regresses the one MoE tested, and takes an intermittent
illegal memory access.
Skipping the AllReduce entirely (wrong output, timing only) on Qwen3.8-27B-UD-Q5_K_M, a
13500-token prompt, -sm tensor -ts 50/50 -c 20480: prefill goes from 23443 ms to 14573 ms. The
collective is 38% of prefill and it is link bound, about 21 MiB per direction per boundary
over the gen3 x4 link, 10.5 ms measured against 12.8 ms of pure transfer. Hiding all of it would
be +61%.
Only the tail projection of a subgraph can go in that shadow; everything else is upstream of the
collective and the next subgraph is downstream. A longer suffix is not safe to chunk, because
ggml-alloc stacks the intermediates inside a suffix on top of each other. The tail is 3150 ms
of the 14573 ms of compute, so hiding all of it caps the change at +11%.
393 lines were built: a split collective in ggml-backend.h
(comm_allreduce_tensor_begin/end), a NCCL implementation on its own stream with an event pair
per slot, and chunked tail MUL_MAT in ggml-backend-meta.cpp, software pipelined at depth 1.
| model | prefill off | prefill on | delta | tg |
|---|---|---|---|---|
| Qwen3.8-27B-UD-Q5_K_M | 575.4 | 592.7 | +3.0% | 24.66 to 24.67 |
| Qwen3.8-27B-UD-IQ2_M | 578.3 | 596.2 | +3.1% | 29.42 to 29.55 |
| gemma-4-26B-A4B | 1522.6 | 1500.6 | -1.4% | 80.2 to 80.1 |
The pipelining is what pays, not the chunking: baseline 569.6, chunked tail with the one-shot
collective 572.5, chunked tail with the split collective 592.1. The split collective is
bit-identical to the one-shot one.
gemma-4 takes an illegal memory access in 2 to 4 runs out of 12, always at the end of prefill.
It needs both the chunked tail and a collective (AllReduce disabled: 0/14; tail computed whole:
0/10). Not the split collective, not NCCL, not CUDA graphs, not the view node.
compute-sanitizer memcheck reports nothing. That is the shape of a latent synchronization
hazard in the existing AllReduce path that the chunking exposes rather than causes. Not
localized.
Rejected. The measurement does point somewhere: hiding the collective properly means
micro-batching the ubatch one level up, in llama, where a whole layer of compute sits between
one boundary and the next. That is a different and much larger change.
Still open
Everything under "Not fixed here" in the description still stands. These come out of the
measurements above, cheapest first.
-
-asand-kvglforllama-bench. Both exist incommon/arg.cppbutllama-bench
only takes-sm,-ts,-ub,-ngland-fa, so the two sweeps that produced the
largest wins had to be run throughllama-completion. sched: pipeline the delivery of a host-resident KV cache #39 already adds-kvcpand-rso
tollama-bench; these belong next to them. - Sweep
-ubunder-sm tensor. Never measured. It is also the only lever a user has
against the per-device compute buffer above.
From the moe-cache-drafting branch: --moe-expert-cache-size, --moe-expert-cache-l2-pinned-mb, --experimental-logs, the automatic grouped decode / prefetch / bias residency behaviour, and the layer-split-only and speculative interactions. Marked as coming from an unmerged branch, like the PR GenerelSchwerz#57 and GenerelSchwerz#39 material. Assisted-by: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013Bs926e6SdsxnkrqqKq5HP
|
Review of frozen head Blocking
The documented upstream issue #27757 concerns host-resident KV under tensor parallelism, but this 395-line diff also adds a public attention-split API, a runtime bandwidth benchmark and new residency policy, tied-output handling, recurrent-state policy changes, meta-buffer OOM semantics, and an unrelated Keep the host-cache correctness fix and its regression test focused here. Move the attention-split API, bandwidth/residency policy, tied-output optimization, allocation behavior, and documentation cleanup into separately justified PRs.
Scheduler copy names are stored in a 64-byte fixed field and formatted as The new prefix parser returns This is not theoretical topology: upstream has reports involving 8-10 CUDA devices. The design should preserve source-tensor identity in scheduler metadata instead of recovering it from a lossy display name. Requiring
The option explicitly represents independently owned draft KV layers. Speculative setup disables full KV offload and copies the requested count here, then creates an MTP context here. The new global picker only considers IDs below The declared base selected the first N layers after applying the cache filter (base lines 242-251, base placement), so this is a regression. Selection must use
Will slow the review
Existing This area has strong upstream precedent: an earlier meta tensor-split change was reverted after a crash, with a maintainer explicitly requesting tensor-split CI coverage (comment. The corrected implementation #27574 added MQA, GQA, per-head fixtures, and 1-4 virtual-device CI. Reuse
Examples include the four-line copy-name explanation here and host-cache explanation here. Reduce these to short why-only comments after the design is simplified. Validation
|
The copy name is "<backend>#<source>#<copy>" in a field of GGML_MAX_NAME, and the meta backend name grows with the device count, so at eight devices the source name is the part that is cut. Nothing can recover the identity of the copy after that. Cut the backend label instead - it is only a label, the source name is what identifies the copy. Assisted-by: Claude Opus 5
The patterns accepted a name with no digits, so a name that lost its layer index still matched and the layer parse threw on the empty string. Assisted-by: Claude Opus 5
An MTP cache holds only the nextn layers, which sit above the layers of the main context. The picker looked at the trunk alone, so it never named a layer the MTP cache owns while still reporting the requested count as delivered - spec_draft_kv_gpu_layers kept the whole draft cache in host memory. Assisted-by: Claude Opus 5
The aggregate left the residency set out, which fails a build with -Werror=missing-field-initializers. Assisted-by: Claude Opus 5
The bound rejected exactly as many entries as the public array holds. Assisted-by: Claude Opus 5
Assisted-by: Claude Opus 5
A fused QKV makes Kcur and Vcur strided views, so a host-resident cache reads them back from a tensor that is not contiguous. Only the write side handled that; the read hit the contiguity assert. On falcon, split mode tensor with no_kv_offload aborted. Assisted-by: Claude Opus 5
The matrix built every context with the cache offloaded, so it never ran the path this branch fixes. Add two columns to the meta config - the cache in host memory, and the same with all heads on the first device and part of the cache kept resident - and a check that an MTP context spends its residency budget on the layers it owns. The copy name only fills the name field from eight devices on, so run one arch at that count in the CI. Assisted-by: Claude Opus 5
Resolve test-llama-archs.cpp: keep test_mtp_kv_residency, adopt the log_level -> verbosity rename from llama/dev. Assisted-by: Claude Sonnet 5
|
Splitting this up as requested. Closing in favour of seven focused PRs, all against
#66, #65, #64, #67 and #63 are independent of each other and can be reviewed in any order. On the review points from this PR:
One item from this PR's description did not survive re-measurement and has been dropped rather than Still open, and stated as a limitation in #66 rather than claimed fixed: an iSWA cache under |
Fixes
--split-mode tensortogether with--no-kv-offload, which on any model with more thanone KV head either aborts or silently returns wrong output. Adds
--attn-splitand makes--kv-gpu-layersusable, both of which turn a host-resident cache from unusable into thefastest option for a long context on an asymmetric pair of GPUs.
Replaces #48, which fixed one model shape and carried unrelated history.
The 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.The scheduler 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 deviceshold every KV head 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'srenumbered 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-meta : split a host-resident KV cache by head- offer a copied-in compute-bufferleaf to the split-state callback under the name it was copied from, and split it on the head
axis. The scheduler names such a copy
<backend>#cache_k_l0 (view) (permuted)#0; undoingthat decoration is the scheduler's own convention, so it is undone in the meta backend. 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 : keep the recurrent state on device under split mode tensor- a linear-attentionop packs the state it writes back together with its output, so that split does not line up
with the one a host cache expects. On device it stays consistent; host-side the two orders
disagree and produce plausible but wrong tokens. The state is small (~150 MiB on
Qwen3.8-27B) and it is the attention cache that
-nkvoexists to move.kv-cache : spread partial GPU residency across devices---kv-gpu-layers Ntook thefirst N layers in layer order, filling one device and leaving the others' free memory unused.
It now takes one layer per owning device in turn.
kv-cache : apply partial GPU residency to every cache layout---kv-gpu-layerswasignored by every cache made of several sub-caches (iSWA, DSA, DSV4, MSA), because each
sub-cache would have taken the full budget. Resolved once for the whole model instead.
kv-cache : keep a recurrent layer out of the partial residency budget- a hybrid modelcounted every layer, so most of the budget went to layers the attention cache does not own.
On Qwen3.5,
--kv-gpu-layers 4delivered one layer.kv-cache : spend the partial residency budget on the slowest link first- a residentlayer saves the transfer it would cost, which is worth most where the link is slowest. The
host-to-device bandwidth is measured once through the same path the cache is delivered on,
and the slowest device's layers are taken first. Devices within 15% stay one group and keep
the round-robin of commit 3.
llama : add an attention split separate from the tensor split---attn-split(-as).The share a device gets of the attention heads decides both how much of a host cache it
receives and how much attention work it does, and neither has to follow the memory split.
Unset, it follows
--tensor-splitand nothing changes.llama : split a tied output projection under split mode tensor- a model withoutoutput.weightreuses the embedding table under the table's name. That copy landed in a metabuffer, missed
pattern_output_weightand fell through toMIRRORED, so every device heldthe whole table and ran the whole projection.
Testing
Two GPUs, RTX 4070 (gen4 x16) + RTX 3060 (gen3 x4), CUDA, NCCL. Greedy,
--temp 0 --top-k 1 --seed 1, 48 tokens,-c 4096,-fit off,-ts 50,50for-sm tensor, sha256 prefix of theoutput. "before" is the branch point,
43aaa59b6.All numbers below were measured at stock clocks. An earlier revision of this description was
measured with the 4070 overclocked, which is why the throughput figures have moved by a few
percent.
-sm tensor -nkvo, before and after:3fdf2d9f40ad5d8cggml-backend-meta.cpp:826ggml-backend-meta.cpp:826f89017c318699020ggml-backend-meta.cpp:826-ts 55,45 -nkvofattn.cu:373 GGML_ASSERT(Q->ne[2] % K->ne[2] == 0)"fixed" means the hash equals that model's
-sm tensorhash:-sm layer-sm layer -nkvo-sm tensor-sm tensor -nkvobdda82c6574fb090bdda82c6574fb090ed21fa6254900f95ed21fa6254900f95dc9c9b89a57e8ac2dc9c9b89a57e8ac274bc62ba2d48fd7a74bc62ba2d48fd7a0ba93868baa7ddda0ba93868baa7ddda0ba93868baa7ddda0ba93868baa7ddda1b0c0da6c4bf450c1b0c0da6c4bf450c1b0c0da6c4bf450c1b0c0da6c4bf450c8372a5cb22e6f1c18372a5cb22e6f1c1eab8b94ee9cb4376eab8b94ee9cb4376Nothing changes where it already worked: all ten
-sm layerand-sm tensorhashes above areidentical on
43aaa59b6. Uneven splits agree too - on gemma-4,-ts 55,45,60,40,45,55and
65,35each give the same hash with and without-nkvo.The head split works in bytes off
nb[2], so it does not depend on the cache type. OnQwen3.8-27B-UD-Q5_K_M,
-sm tensor,-sm tensor -nkvoand-sm tensor -nkvo --kv-gpu-layers 4all give
8dfc3fab3519a246with-ctk q8_0 -ctv q8_0, and the same again withq4_0.Perplexity,
-c 2048over a 15216-token slice of this repository's docs. Unmodified build of43aaa59b6against this branch, identical to four decimals:-sm layer-sm tensorand moving the cache to host memory, pinning it, or keeping part of it on the device leaves the
result unchanged:
-sm tensor-sm tensor -nkvo-sm tensor -nkvo --kv-cpu-pinned-sm tensor -nkvo --kv-gpu-layers 4-sm layer -nkvo-sm layer -nkvo --kv-gpu-layers 8Performance
llama-bench,-ngl 99 -ts 50/50 -r 3.-sm tensor -nkvodid not run before this PR.Which split mode wins is model-dependent, not a property of MoE.
Where the residency budget goes
The case this is for: a dense model that has to stay on the GPUs, and a context large enough
that the cache no longer fits beside it. Qwen3.8-27B-UD-Q5_K_M (19.8 GB),
-ngl 99 -n 128,prompt filled to the context. The two links are not alike and the startup measurement reports
what they do:
A factor of 7.3, so a layer kept off the 3060 is worth 7.3 kept off the 4070.
-c 20480,15216-token prompt, f16 cache,
-sm layer -nkvo --kv-cpu-pinned --recurrent-state-offload. Themodel has 16 owned attention layers, 8 per GPU; the round-robin column is this branch with
commit 6 reverted:
--kv-gpu-layersAt 8 layers, the point where the 3060 owns none of the host-resident cache, the two orders cost
the same memory and differ by +85%. Prefill moves with it, 645.77 to 959.89 t/s. Both orders
converge at 16, where each takes every layer. A device-resident cache reaches 17.78.
-c 65536, 58466-token prompt, where the cache cannot be device-resident at all:--kv-gpu-layers8 layers cost 1088 MiB, all of it on the 3060. Against the f16 baseline that is 6.5x decode;
against the same budget spread round-robin, +120%.
A quantized cache is a transport saving here, not a kernel cost: device-resident, f16 is
slightly faster than q8_0 (17.78 against 17.41); host-resident at
-c 65536it is +72% (1.39against 2.39). The link is the limit. Halving the bytes also halves what a resident layer costs.
Under
-sm tensorthe budget only pays near-completeSame model and context,
-sm tensor -ts 50,50 -nkvo, q8_0:--kv-gpu-layersTensor parallelism splits every layer's cache across both devices, so a partly resident cache
still sends the remaining layers to the slow device; only the last layer removes the transport.
Commit 6 changes nothing here - all layers report the same meta device, so there is one group.
--kv-gpu-layersunder-sm tensoris worth setting to the full attention layer count or notat all.
At full residency
-sm tensorbeats-sm layeron generation (19.82 against 14.45) and loseson prefill (507.38 against 804.09).
Giving the heads a ratio of their own
The other way out is to stop sending the slow device half the cache. Same prompt,
-sm tensor -ts 50,50 -nkvo, q8_0:--attn-split--kv-gpu-layers-ts)1,01,03.1x from the head split alone, and 4.4x combined with the residency budget. Perplexity is
unmoved: on Qwen3.8-27B-UD-Q5_K_M at
-c 2048,-sm layergives 3.7843,-sm tensor3.7860and
-sm tensor -as 1,03.7824, against an error bar of +/- 0.096.The share is rounded to whole heads, and the rounding is what the granularity rules already
enforce so that GQA stays consistent. The cache copy and the query tensor do not always round to
the same share: Qwen3.8-27B has
n_head_kv = 4and a query tensor fused with a gate, and-as 0.75,0.25there lands on3,1, not on2,2. It is a distinct working share, identical toan explicit
-as 3,1(3.7831 perplexity for both) and measurably between the two neighbours -3.47 t/s against 2.41 for
2,2and 7.36 for4,0on the run above.Setting
--attn-splitwhen it is not used costs nothing. Against this branch with commit 7reverted, same configs:
-sm tensor -nkvo-sm tensor -nkvo --kv-gpu-layers 16--tensor-splitwants the fast deviceWith a device-resident cache both devices run concurrently inside a layer and the collective is
a barrier, so the layer costs
max(share_i / rate_i)and the optimum sits near the computeratio, not at the memory ratio. The default splits by free memory, which on two 12 GiB cards is
50/50 regardless of speed.
llama-bench -r 3,-sm tensor:gemma-4-26B-A4B:
-tsQwen3.8-27B-UD-Q5_K_M, which sits much closer to the memory limit:
-ts+11% prefill and +11 to +13% generation from an argument. On both models the optimum is a real
compute optimum below the memory limit and lands near the 4070:3060 ratio, not at the limit
itself: gemma-4 flattens after 65/35 and stops fitting at 75/25, Qwen3.8 peaks at 60/40, is past
it at 62/38 and stops fitting at 64/36.
With a host-resident cache the answer differs, because
-as 1,0already puts the cache on thefast card and there is no room left to move weight there as well.
-c 65536,-as 1,0 --kv-gpu-layers 8:-tsUnder
-sm layerthe answer is the opposite again, because there the devices run one after theother and
-tsis the layer count. Same model and prompt,--kv-gpu-layers 16:-ts+17% prefill for nothing but an argument. 60,40 no longer fits.
Most of Qwen3.8's
-sm layer -nkvodeficit is the recurrent state, not the attention cache:-c 4096 -n 128, 7.62 t/s against 17.24 with--recurrent-state-offload.-sm tensorgetsthat for free after commit 2.
Not fixed here
delivery of the cache and lists the strided head-split write as the one piece missing for
-sm tensor; commit 1 supplies the synchronous form it needs.--attn-splitautomatically from the measured bandwidth. The measurement isalready there and the rounding would land on
1,0for this pair, but it changes the reductionorder for every multi-GPU user, so it stays opt-in until it has run on more than one machine.
llama_params_fitforSPLIT_MODE_TENSOR, so-tshas to be set by hand. The sweepabove gives it a second motive: the optimum is worth +11% and it cannot be made a default
without a feasibility check against free memory. The margin is narrow - on Qwen3.8-27B-Q5_K_M
the optimum is 60/40 and 64/36 no longer fits.
-sm tensor(commit 2 sidesteps it).device- and host-resident layers degrades there. On gemma-4,
-sm tensor -nkvo, one, two orfour resident layers all give bit-identical and worse perplexity while 0 and "all" agree, so
something switches as soon as the cache is mixed. It is not
offload_attn_computeand not anunpinned host buffer; layer split and a plain non-iSWA cache under tensor split are both
unaffected. Root-causing it needs a look at how the meta backend maps externally created views
onto its per-buffer compute containers, which carries its own FIXME already.
Earlier revisions of this description carried four rejected optimizations and their
measurements. They are preserved in a comment below rather than here.
AI usage disclosure: yes, see the commit trailers.