Skip to content

qwen4exp: compute QSA cache-layout inputs once per ubatch, not per layer - #150

Open
talexeyft wants to merge 2 commits into
unslothai:qwen4exp/qwen3.8-flash-nextfrom
talexeyft:qwen4exp-qsa-perf
Open

qwen4exp: compute QSA cache-layout inputs once per ubatch, not per layer#150
talexeyft wants to merge 2 commits into
unslothai:qwen4exp/qwen3.8-flash-nextfrom
talexeyft:qwen4exp-qsa-perf

Conversation

@talexeyft

@talexeyft talexeyft commented Aug 30, 2026

Copy link
Copy Markdown

qwen4exp: compute QSA cache-layout inputs once per ubatch, not per layer

Problem

set_input_qsa results depend only on the indexer cells and the ubatch, yet the graph
asked for them once per attention layer (48x), each time running O(n_kv) serial host
loops and filling a separate set of input tensors. At long context this dominated
decode: GPU util sat at ~31% while host code burned tens of ms per token.

Changes

  1. Share one set of QSA input tensors (k_idxs, cell_blk, blk_cells, blk_pos, bias)
    across all layers with the same compress ratio (graph::qsa_shared). One fill +
    one H2D copy per ubatch instead of 48.
  2. Memoize set_input_qsa on llama_memory_hybrid_idx, keyed by a fingerprint of
    per-stream cell generations (new llama_kv_cells::get_generation(), bumped by every
    mutator) and the ubatch tokens. Decode-step repeats hit the memo; prefill batches
    above a 256MB bias cap bypass it.

Measured

Qwen3.8-Flash-Next UD-IQ4_XS, 2x RTX 3090 (NVLink), f16 KV cache:

32k 131k 176k 229k
decode tps before 29.1 16.2 OOM -
decode tps after 32.1 21.1 18.2 15.5
prefill tps before 336 120 - -
prefill tps after 492 315 260 220

Full write-up (sweeps, nsys profiles, remaining bottlenecks):
https://github.com/talexeyft/llama.cpp/tree/qwen4exp-qsa-perf (branch commit message),
local: admin_gpu/ai_experiments/qwen38_flash_next_speed/REPORT.md

set_input_qsa results depend only on the indexer cells and the ubatch,
yet the graph asked for them once per attention layer (48x), each time
running O(n_kv) serial host loops and filling a separate set of input
tensors. At long context this dominated decode: GPU util sat at ~31%
while host code burned tens of ms per token.

Two changes:

1. Share one set of QSA input tensors (k_idxs, cell_blk, blk_cells,
   blk_pos, bias) across all layers with the same compress ratio
   (graph::qsa_shared). One fill + one H2D copy per ubatch instead of 48.

2. Memoize set_input_qsa on llama_memory_hybrid_idx, keyed by a
   fingerprint of per-stream cell generations (new
   llama_kv_cells::get_generation(), bumped by every mutator) and the
   ubatch tokens. Decode-step repeats hit the memo; prefill batches
   above a 256MB bias cap bypass it.

Measured on Qwen3.8-Flash-Next UD-IQ4_XS, 2x RTX 3090 (NVLink), f16 KV:

  decode tps   32k    131k   176k   229k
  before       29.1   16.2   OOM    -
  after        32.1   21.1   18.2   15.5

  prefill tps  32k    131k   176k   229k
  before       336    120    -      -
  after        492    315    260    220
@talexeyft
talexeyft requested a review from CISC as a code owner August 30, 2026 16:29
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.

2 participants