Skip to content

feat(qwen4exp): 262K context for four requests in split mode, KV of some layers on the Strix - #833

Merged
davide221 merged 2 commits into
mainfrom
feat/qwen4exp-kv-offload
Oct 9, 2026
Merged

davide221 merged 2 commits into
mainfrom
feat/qwen4exp-kv-offload

Conversation

@davide221

@davide221 davide221 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

In split mode, Qwen3.8-Flash-Next now keeps its full 262,144-token context with four requests at once. When the R9700 runs out of memory, some attention layers keep their KV cache on the Strix Halo instead of the server cutting the context. Before this, --max-ctx auto stopped at 131,072 tokens for four slots.

Changes

  1. fix(ggml-cuda): copy between two devices on the source's stream.

    • The problem: ggml_backend_cuda_buffer_cpy_tensor queued its peer copy on the per-thread stream of whichever device was current. On ROCm, a copy from the R9700 to the Strix Halo iGPU queued on the destination's stream completes without writing anything.
    • The effect: the scheduler's copies of graph inputs to the other device arrived as zeros. Those are the K/V row indices, positions and masks. With MTP, a verify step then wrote its four K/V rows into row 0, so output changed from run to run.
    • Before this PR, split serving never sent a graph input to the expert device, so nothing on main hit it.
    • The fix sets the source device first, as ggml_backend_cuda_cpy_tensor_async already does.
    • Regression test: test_cuda_copy_batch gains cross_device_tensor_copy, which needs two GPUs and skips otherwise. On an R9700 + Strix Halo it reads 0 0 0 0 without the fix and 19 20 21 22 with it.
  2. feat(qwen4exp): attention layers' KV on the Strix when the R9700 is full.

    • When the fit misses, the last full-attention layers keep their K/V and indexer rows on the expert device. They run their attention there, from the input projections through the output projection, and only the layer's input and output cross between the GPUs.
    • It moves as few layers as cover the shortfall, and only while the Strix has room for them. The context shrinks only once no more layers can move.
    • Qwen4ExpCache::remote_layers keeps those rows in a second buffer on the expert device. Prefix snapshots save and restore them with the rest.
    • move_qwen4exp_attention moves those layers' attention and indexer weights.
    • Qwen4ExpLayerBuilder::on_cache_device pins the nodes a moved layer's attention adds. It covers the single-sequence, verify, pipelined-prompt and multi-slot builders.
    • The chunk plan counts only the rows left on the target. It also reports one layer's rows across all slots (layer_bytes), so the fit knows how many layers to move.
    • The QSA prefix bootstrap pools each layer's keys on the GPU that holds its rows.
    • docs/QWEN4EXP.md and the profile description cover it.

Measured

Setup: R9700 + Strix Halo (192 GB), UD-Q4_K_XL, Manage's split launcher, four slots, adaptive MTP, greedy.

262,144 tokens, 7 of 12 layers on the Strix 131,072 tokens, every row on the R9700
Decode, a request alone (mean of 4 prompts) 45.9 tok/s 59.3 tok/s
Decode, four together 68.6 tok/s 78.3 tok/s
Outputs alone vs together 4 of 4 identical 4 of 4 identical
Recall of 3 facts hidden in a long prompt 3 of 3 at 158,927 tokens 3 of 3 at 96,307 tokens
Free on the R9700, lowest 3.9 GiB 2.6 GiB

At 262,144 tokens:

  • The same prompt gives the same reply in repeated runs: short ×3 and 6K ×2, the second 6K run restored from the prefix cache.
  • A short reply is the same alone and beside another request.
  • A 16K prompt resent prefills in 0.25 s instead of 11.0 s, with the same reply.
  • A 158,848-token prompt completes beside three short requests.

The two layouts give the same reply on 2 of 4 prompts. The other 2 diverge at a near tie, one at the first token, because those layers' attention runs on a different GPU. Both sets of replies are coherent.

The context costs speed: about 23% for a request alone and 12% for four together, from the extra GPU-to-GPU hops of the moved layers.

Tests

Suite Result
test_server_unit 650 passed
test_cuda_copy_batch passes, including the new test
test_qwen4exp_prefix_cache passes
test_qwen4exp_seq_engine (exact distinct slots) passes

Built for gfx1151 + gfx1201.

🤖 Generated with Claude Code

View guided diff

mrciffa and others added 2 commits October 9, 2026 20:58
ggml_backend_cuda_buffer_cpy_tensor queued its peer copy on the per-thread
stream of whichever device was current. On ROCm a copy from an R9700 to a
Strix Halo iGPU queued on the destination's stream completes without
writing, so the scheduler's copies of graph inputs to the other device
(row indices, positions, masks) arrived as zeros. It now sets the source
device first, as ggml_backend_cuda_cpy_tensor_async queues its copy on the
source's stream.

Split serving never sent a graph input to the expert device until the
next change, which runs some attention layers there.

test_cuda_copy_batch gains cross_device_tensor_copy (skipped below two
devices). On an R9700 + Strix Halo it reads 0 0 0 0 without the fix and
19 20 21 22 with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… when the R9700 is full

`--max-ctx auto` lowered the context once the R9700 could not hold every
slot's cache beside the dense weights: four requests at once got 131,072
tokens. A miss now first moves the last full-attention layers' K/V and
indexer rows to the expert device, as few layers as cover the shortfall
while that device holds them, and runs those layers' attention there
(input projections through the output projection). The context shrinks
only once no more layers can move.

- Qwen4ExpCache::remote_layers: those rows live in a second buffer on the
  expert device; snapshots save and restore them with the rest.
- move_qwen4exp_attention moves the layers' attention and indexer weights.
- Qwen4ExpLayerBuilder::on_cache_device pins the nodes a layer's attention
  adds to the expert device (single-sequence, verify, pipelined prompt and
  multi-slot builders); qwen4exp_pin_split places them.
- The chunk plan counts the rows on the target only and reports one
  layer's rows across slots (Qwen4ExpChunkPlan::layer_bytes).
- The QSA prefix bootstrap pools each layer on the device holding its rows.

R9700 + Strix Halo, Manage's split launcher, four slots: 262,144 tokens
with 7 of 12 layers moved, 3.9 GiB left on the R9700. Outputs repeat run
to run and match alone and together (4 of 4); a resent 16K prompt
prefills in 0.25 s instead of 11.0 s with the same reply; three facts
hidden in a 158,927-token prompt are all recalled; a 158,848-token prompt
completes beside three short requests. A request alone decodes at 45.9
tok/s and four together at 68.6, against 59.3 and 78.3 at 131,072 tokens
with every row on the R9700.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@davide221 davide221 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

No issues found. Checked for bugs and logic errors in the changed code.

Reviewed cb6116c6 · 1 candidate issue checked, kept at ≥70% confidence · add the code-review label to review again

@davide221
davide221 merged commit b8b3cf5 into main Oct 9, 2026
11 checks passed
@davide221
davide221 deleted the feat/qwen4exp-kv-offload branch October 12, 2026 07:37
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