Conversation
|
Hi @Rhonstin, thanks for your contribution! Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:
Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below. |
The stock upstream README does not describe what this fork carries. Replace it with a branch map and a summary of the qwen4exp/llmcmp work: the QSA pooled-key cache (upstream PR ggml-org#28699), the E06 phase-prefill port, and the production patch stack. Upstream link, build recipe and license notes kept.
|
Tried this on gfx1151 (Ryzen AI Max+ 395 / Radeon 8060S, ROCm 10.0.0) and hit a context-init failure. Reporting it because the isolation is fairly precise, with the caveat up front that I have not confirmed it reproduces on clean master — see the last paragraph. The patch applies cleanly (only line offsets) and builds without warnings. At runtime:
if (mem_idx && mem_idx->get_n_stream() == 1) { // -> if (false && ...)makes the same build pass again ( The throw appears to come from for (auto & [buft, mbuf] : mbufs_new) {
const auto & mbuf_cur = mbufs.at(buft);which would fit the one-buffer-per-buffer-type deviation described in the PR body: the set of buffer types seen on restore no longer matches the set recorded on save. Caveat, and the reason I am not filing this as a confirmed bug: the tree I tested on is a fork of master that carries local Qwen3.8-Flash-Next MTP work and speculative-checkpoint machinery, so The idea itself is very welcome here — the analogous pooled-key cache for GLM-5.3-Flash was the largest prefill win we measured on this hardware, and the per-buffer-type allocation is exactly right for a layer-split setup, which is what we run (one local GPU plus one RPC worker). |
The block-sparse attention indexer scored queries against block summary keys (mean of each block's compress_ratio raw keys, normalized, roped) that were regathered and recomputed over the whole cached context every token, in every QSA layer - the dominant remaining depth cost. Only complete blocks are ever scored (incomplete tails ride the bias term), and a complete block's members never change, so the summaries are cached: one f32 row per position block per layer, written by the graph via set_rows. Per ubatch only the freshly completed blocks (at most one per decode step) are pooled/normed/roped; the score reads the cache. Validity is a per-sequence block watermark clamped by seq_rm/seq_cp/ seq_add/seq_div/clear and reset by full state loads; a PARTIAL_ONLY restore (speculative checkpoint replay) leaves the indexer cells untouched and its rollback arrives through seq_rm, so rejection costs nothing. Stale rows are finite and masked by the -inf bias, exactly like the previous garbage partial-block pools. Single-stream memories only; multi-stream and LLAMA_QSA_NO_POOLED_CACHE=1 fall back to the full recompute (whose blk_cells/blk_pos inputs are then not created at all - an unreferenced graph input is never allocated, and filling it would write through a null pointer). Ported from @apepojken's qwen4exp-spec-mtp fork (commits 472b758 and the pooled-cache parts of the 843d575 review pass), with one deviation: the rows are allocated one buffer per indexer-cache buffer type. The reference allocates all layers in a single buffer, which on a layer-split setup makes every layer but the first read and write its rows over the inter-GPU links - measured on an 8-GPU box that costs 2x decode throughput; one buffer per device recovers it. Rows exist only for layers that own a QSA cache: the indexer cache is filtered to the dense-attention layers, while a recurrent layer can still carry a nonzero ratio in the metadata. Both the ratio pick and the row allocation therefore skip recurrent layers - calling get_k_storage() on one otherwise throws std::out_of_range during context construction, as reproduced with test-llama-archs --arch qwen4exp, whose synthetic model stores a ratio for every layer. The test passes with the cache active after the guard. A/B on 8 GPUs (Qwen3.8-Flash-Next UD-Q3_K_XL, MTP n-max 2, q8_0 KV, ctx 131072): decode 24.33 vs 22.25 t/s at 63k-token depth (+9.3%), 27.80 vs 25.41 at 114k (+9.4%); prefill par; greedy output bit-identical at both depths; prefix-reuse rollback replays match fresh runs.
85121fb to
141f3f5
Compare
|
Thanks @Patt92 — this is a real defect in the PR, and not your fork or ROCm-specific: I reproduced it on a clean checkout with the repo's own test (CPU backend): The throw is in the pooled-cache constructor, not in state I/O. It walks every layer with Fixed in the branch (commit amended): both the ratio pick and the row allocation now skip recurrent layers. Thanks for testing on gfx1151 — reports from outside the NVIDIA box are exactly what this change needed. |
|
Thank you for the improvement! I am testing it and I realized vision is broken (5070 Ti + Ryzen 9950X 128G DDR5). These are from an agent, I hope it helps:
Filling ( Under ranked mode, blocks are assigned by dense rank over live cells, so this works out to ~ Ranked mode is switched on by the author's own code for exactly this case: An image chunk adds ~1024 cells while advancing Any single image request to a qwen4exp server with an mmproj loaded. Aborts with: Stack: The kill switch doesn't save this one either. |
Adapted from ggml-org#28699 (141f3f5) while preserving the branch's multimodal QSA fallback.
…y cache)
Eight conflicting hunks in src/llama-memory-hybrid-idx.{h,cpp}, all resolved to
the PR. The fork's side of these files was an earlier revision of this same PR:
it had kept the pooled-cache storage and its accessors but nothing filled or
read the cache any more (the graph re-pooled every block each ubatch, and the
header itself said the dirty_* wiring was gone), and git had merged the two
revisions' matching declarations as duplicates. Both files are now identical
to the PR's; the qwen4exp.cpp graph side merged clean.
The PR's per-device buffer vector is kept as is. On one GPU it degenerates to a
single buffer, so there is nothing to gain from re-expressing the fill against
the fork's single-buffer form, and carrying a stale revision of a live PR would
conflict again on its next update.
|
Heads-up from a downstream user of this branch on Symptom. With two live sequences sharing one unified KV cache (llama-server serves every slot as its own sequence from position 0, and Hermes-style agents fire small auxiliary requests concurrently), the model intermittently answers as if the user's last messages were empty - it literally cannot see them, and invents a name for the empty input. Probabilistic: re-sending the same prompt succeeds (~2/4 failures with concurrency, 0/4 without). Root cause. Pooled rows are keyed by position block only, with no sequence identity. Two sequences holding different cells at the same positions therefore write and score the same rows, and whichever wrote last wins: the loser reads the other sequence's summaries as its own tail blocks. The guard instrumentation on my tree logged Fixes in the patch below (built and run on this branch @
This sizing change also covers @sodre90's vision report: Cost: the pool grows to Verified: same concurrent shape as the repro (8.8k-token main request + small concurrent request): 2/4 wrong before, 4/4 correct after, zero guard violations. Happy to open a PR against this branch if that's useful. |
|
Some numbers from Vulkan on AMD in case they're useful. On this machine, the re-pooling this PR removes seems to be most of the long-context decode cost, so the gain here should be much Setup: Ryzen AI Max+ 395 (Radeon 8060S, gfx1151, 128 GB), Ubuntu 24.04, Mesa 25.2.8 RADV, llama.cpp b11111 (4ceb171), Vulkan, a Qwen3.8-Flash-Next finetune in UD-Q4_K_XL, -fa on, K/V cache llama-server decode: 22.1 t/s at 4K context, 17.5 at 32K, 10.7 at 99K, 7.4 at 188K (45 → 57 → 94 → 135 ms per token). I haven't run this branch. Instead I rebuilt the QSA part of one decode step as a standalone ggml graph and timed it on the same GPU. It uses the same calls in the same order as ┌────────┬────────────────┬─────────────────┬────────────┬───────────┐ This accounts for ~43 of the ~48 ms that decode slows down between 4K and 99K, and ~85 of ~90 ms at 188K. GGML_VK_PERF_LOGGER at 98304 cells (12 layers, one token, 44.4 ms total):
So roughly 35 of the 44 ms goes to pooling, normalizing and roping blocks that are already finished. If the pooled cache removes that, a rough estimate for this machine is ~17 t/s instead Happy to share the benchmark source if it helps. |
Overview
The QSA indexer of the qwen4exp architecture (Qwen3.8-Flash-Next) scored queries against block summary keys that were regathered and recomputed over the whole cached context every token, in every QSA layer - the dominant remaining decode cost at depth. This PR caches those summaries incrementally.
set_rows; per ubatch only the freshly completed blocks are pooled/normed/roped, and the score reads the cacheseq_rm/seq_cp/seq_add/seq_div/clearand reset by full state loads; aPARTIAL_ONLYrestore (speculative checkpoint replay) leaves the indexer cells untouched and its rollback arrives throughseq_rmLLAMA_QSA_NO_POOLED_CACHE=1fall back to the full recomputePorted from @apepojken's
qwen4exp-spec-mtpfork (commits 472b758 and the pooled-cache parts of the 843d575 review pass), with one deviation: the pooled rows are allocated one buffer per indexer-cache buffer type. The reference allocates all layers in a single buffer, which on a layer-split setup makes every layer but the first read and write its rows over the inter-GPU links; measured on an 8-GPU box that costs 2x decode throughput, and one buffer per device recovers it.Additional information
A/B on 8 GPUs (1x RTX 3090 + 7x CMP 90HX; Qwen3.8-Flash-Next UD-Q3_K_XL; MTP n-max 2; q8_0 KV; ctx 131072), same binary toggled via the kill switch:
Prefill unchanged within noise; greedy output bit-identical at both depths (64 tokens); prefix-reuse rollback (seq_rm replay) matches fresh runs.
Also examined from the same fork: 9b09d26 (graph reuse + shared QSA input) is already on master; 51c0c10, b6d995d and 6634bfd measured as Vulkan-specific on our CUDA box and are not included.
Requirements