Skip to content

sm_75: the decode QSA selection at long context, a halving scorer and a split top-k (bitwise, decode +6-12 % at 410K) - #2072

Closed
rafatxf wants to merge 1 commit into
Niko1221:mainfrom
rafatxf:pr/sm75-decode-selection
Closed

rafatxf wants to merge 1 commit into
Niko1221:mainfrom
rafatxf:pr/sm75-decode-selection

Conversation

@rafatxf

@rafatxf rafatxf commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

At long context a verify window's QSA selection is a fixed cost per layer that the rest of the window does not hide: at ~420K cells on an RTX 2080 Ti the 12 layers' scores and top-k took ~6 ms of a ~52 ms window (nsys, --serve, 256 tokens after a 422K conversation). Both kernels here produce the same bits as today's, and both have a switch.

  1. block_scores_halve_kernel (decode scorer, block_scores_multi_kernel's replacement on sm_75). Its time grew with the window's queries: 117 / 265 / 510 µs for 1 / 3 / 6 queries at 420K. The keys are read once, so the cost was the 5 xor shuffles per (query, head) value - 20 per block and query - and, once those were gone, the per-block shared-memory reads of the queries (ncu: mio_throttle). Here the 4 × queries values (padded to a power of 2) are reduced together by recursive halving: at offset o each lane keeps half its values and adds its partner's copy of them. That is the pair (lane l, lane l ^ o) the butterfly adds for that value at that offset, with the same offsets in the same order, so every value's additions are the butterfly's operand for operand (float addition commutes). The lane's query values live in registers; 5-8 queries run as two calls of up to 4. STRATA_SCORES_HALVE=0 keeps the old kernel.
  2. qsa_block_topk_split (decode top-k without clusters). Sm_90+ has block_topk_cluster_kernel; elsewhere a window's 1-8 queries ran block_topk_wide_kernel on 1-8 SMs. Here a query's blocks are cut into chunks of 2,048, and six short launches do four radix passes (each chunk adds its histogram to the query's global one), a count of each chunk's cells above / at the threshold, and the emit. Each pass's CTAs take the previous digit from the complete histogram with block_topk_kernel's rule (a pure function of integer counts), so the threshold, above, the tie budget and the emitted cells are the same: identical ids by construction. STRATA_TOPK_SPLIT=0 keeps the wide kernel; =1 turns it on for any card (tests).

Both default on sm_75 only (measured there); nothing changes for other cards.

Setup

RTX 2080 Ti 22 GB (sm_75, PCIe 3.0 x16), i7-6700, 56 GB DDR4-2133, CUDA 13.0, driver 595, SM clock locked at 1650 MHz, 240 W. v0.1.43 built from source with -DSTRATA_BUILD_TESTS=ON. Model Q2_0, --resident-experts, --spec 4 --spec-min-p 0.7, --max-context 524288 (YaRN ×2), --kv int8 --kv-resident 32768.

Scorer: QSA_BENCH_DECODE=1 qsa_select_bench CTX Q 50 524288 (new mode: a decode call, hash of the scores)

context queries before after
32K 1 / 3 / 4 / 8 12.1 / 24.8 / 31.7 / 59.2 µs 7.7 / 14.5 / 15.0 / 36.0
131K 1 / 3 / 4 / 8 41.1 / 86.8 / 113.6 / 217.0 34.2 / 66.2 / 67.1 / 93.3
420K 1 / 3 / 4 / 6 / 8 116.7 / 264.9 / 346.6 / 509.7 / 773.9 100.5 / 117.9 / 124.5 / 238.0 / 255.1

The same hash before and after in every case (1-8 queries, 32K-524K). At 3-4 queries and 420K it now reads the keys at ~390 GB/s.

Top-k: qsa_topk_parity CTX Q 50 524288 0 (a decode call), dispatched time

context 1 query 3 6
9K 0.034 → 0.032 ms 0.034 → 0.035 0.033 → 0.042
131K 0.095 → 0.033 0.094 → 0.039 0.095 → 0.048
420K 0.203 → 0.033 0.209 → 0.040 0.210 → 0.064
524K 0.243 → 0.034 0.248 → 0.041 0.251 → 0.066

qsa_topk_parity --selftest: PASS (continuous, 64-level and equal scores; 2K-524K under 9K-524K capacities; decode and prompt calls).

Engine: decode after a long conversation

--serve: one 390K request, then two appends of 16K tokens, 256 tokens generated after each (resumed from the conversation, so the decode sits at ~406K and ~422K):

depth before after
~406K 52.7 tok/s 56.0
~422K 53.0 59.3

A one-shot 4K run writes the same tokens before and after (same MD5) at the same speed. The server here runs it since 2026-10-12.

Tests

qsa_parity: 0 failures. qsa_topk_parity --selftest: PASS. qsa_select_bench with QSA_BENCH_DECODE=1: identical hashes with STRATA_SCORES_HALVE 0 and 1.

🤖 Generated with Claude Code

…ing scorer (bitwise)

A verify window's 1-8 queries scored and selected on a handful of SMs; at
420K cells on an RTX 2080 Ti the two took ~6 ms of a ~52 ms window.

- block_scores_halve_kernel: the decode scorer's warp sums as one
  recursive-halving reduction (each value pairs lane l with l ^ o at offsets
  16..1, the butterfly's additions operand for operand), the lane's query
  values in registers (shared-memory reads were the limit, ncu mio_throttle),
  5-8 queries as two calls of up to 4. Same bits (qsa_select_bench
  QSA_BENCH_DECODE=1 hashes, 32K-524K, 1-8 queries). 420K: 1 / 3 / 4 / 6 / 8
  queries 117 / 265 / 347 / 510 / 774 -> 101 / 118 / 125 / 238 / 255 us.
  STRATA_SCORES_HALVE=0 keeps the old kernel.
- qsa_block_topk_split: the decode top-k over chunks of 2,048 blocks in six
  launches (four radix passes into per-query global histograms, a count, the
  emit); every CTA takes each digit from the complete histogram with
  block_topk_kernel's rule, so the ids are identical (qsa_topk_parity
  --selftest: continuous, 64-level and equal scores, 2K-524K). Decode calls
  at 420K: 0.21 -> 0.033-0.064 ms; short contexts unchanged within 0.01 ms.
  STRATA_TOPK_SPLIT=0 keeps the wide kernel (=1: on any card, for tests).

Both on sm_75 only by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xrip added a commit to xrip/Strata that referenced this pull request Oct 12, 2026
… faster

Decode at 172K (the deterministic A/B, live env, one binary): QSA_ROWS alone 72.38 / 72.38 tok/s, QSA_ROWS with Niko1221#2072's
split top-k 74.05 / 73.21, Niko1221#2072 alone (its scorer and split top-k) 74.64 / 74.29. The scorer at 172K, 4 queries:
141.1 -> 57.8 us (Niko1221#2072) against ~100 us (QSA_ROWS). So the cherry-picked QSA_ROWS goes (code, bench, doc).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xrip added a commit to xrip/Strata that referenced this pull request Oct 12, 2026
…itwise), STRATA_QSA_ROWS reverted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Niko1221 pushed a commit that referenced this pull request Oct 17, 2026
…d halving scorer (bitwise)


A verify window's 1-8 queries scored and selected on a handful of SMs; at
420K cells on an RTX 2080 Ti the two took ~6 ms of a ~52 ms window.

- block_scores_halve_kernel: the decode scorer's warp sums as one
  recursive-halving reduction (each value pairs lane l with l ^ o at offsets
  16..1, the butterfly's additions operand for operand), the lane's query
  values in registers (shared-memory reads were the limit, ncu mio_throttle),
  5-8 queries as two calls of up to 4. Same bits (qsa_select_bench
  QSA_BENCH_DECODE=1 hashes, 32K-524K, 1-8 queries). 420K: 1 / 3 / 4 / 6 / 8
  queries 117 / 265 / 347 / 510 / 774 -> 101 / 118 / 125 / 238 / 255 us.
  STRATA_SCORES_HALVE=0 keeps the old kernel.
- qsa_block_topk_split: the decode top-k over chunks of 2,048 blocks in six
  launches (four radix passes into per-query global histograms, a count, the
  emit); every CTA takes each digit from the complete histogram with
  block_topk_kernel's rule, so the ids are identical (qsa_topk_parity
  --selftest: continuous, 64-level and equal scores, 2K-524K). Decode calls
  at 420K: 0.21 -> 0.033-0.064 ms; short contexts unchanged within 0.01 ms.
  STRATA_TOPK_SPLIT=0 keeps the wide kernel (=1: on any card, for tests).

Both on sm_75 only by default.
Niko1221 added a commit that referenced this pull request Oct 17, 2026
#2222) are opt-in with STRATA_SM75_PERF=1


No sm_75 card is available here to confirm the series, so the kernels that were on by default for compute capability 7.5
in the submitted PRs now follow one switch, include/strata/core/sm75_series.hpp.  Each keeps its own variable, which
wins in both directions.  The one-read prompt top-k (#2093) was on for every CUDA card as submitted; it is now Turing only
like the rest.  STRATA_EMULATE_CC=75 counts as a Turing card, so a 75-virtual build on an sm_86 GPU runs the series for tests.
@Niko1221

Copy link
Copy Markdown
Owner

Thanks rafatxf, good work. We ported this with your authorship kept. We have no Turing card, so we could not confirm the speed. What we did check: the default path is byte-identical to before (identity gate on RTX 3060, P100 and A4000), and with the new path forced on, including a build that runs the real sm_75 device code on an sm_86 card, the gate is identical too. Because we cannot measure it on sm_75 it ships as opt-in (STRATA_SM75_PERF=1, plus your own variable STRATA_SCORES_HALVE / STRATA_TOPK_SPLIT), documented in docs/DETAILS.md. If a Turing owner confirms the gain we will consider turning it on by default.

xrip added a commit to xrip/Strata that referenced this pull request Oct 17, 2026
Conflicts: upstream took the Turing kernels this fork had cherry-picked (Niko1221#1821 Niko1221#1825 Niko1221#2069-Niko1221#2072 Niko1221#2081 Niko1221#2091 Niko1221#2093),
now opt-in with STRATA_SM75_PERF=1: their versions kept (native_mmvq, qsa_select, qsa_prompt_attn, prefill, gemm,
kernels). Upstream's sm_75 table replaces ours (STRATA_SM75_TABLE gone; STRATA_MMVQ_IL_ROWS can set rows).
conversation_cache.hpp: the disk tier's on_evict on upstream's serial-aware evict_oldest. generate.cpp: the disk
tier's read-back before Niko1221#1164's borrow (a disk restore never borrows), --conversation-cache-park-gain beside the
disk options. CMakeLists: prefill_mmq_bench kept beside upstream's new tests. Tokenizer: our long-text cache beside
max_special_len.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eddoursul added a commit to eddoursul/Lamina that referenced this pull request Oct 17, 2026
…stream's new trees; the engine stays this branch's

Upstream's 264 commits since 296b59d. Where both sides changed a file of the engine (src/, include/: 39 files) or
CMakeLists.txt, this branch's version stays whole, and weights.cpp too (upstream's adds only STRATA_HC_REQ8's loader,
which needs its header; --hc-q8 does this here). The files only upstream changed are upstream's: the GGUF reader (an
all-zero compress_ratios list is a converter's placeholder, Niko1221#2396), the BF16 dequantization (the wide kernels now on by
default on sm_86 too: the prompt path's dense matrices on the 3090, bitwise; prompt times unchanged), the AVX2 expert
rows' exact one-token form and STRATA_PRIMARY_DEVICE's helpers (neither called here). The engine keeps its version,
0.1.43.1.

Taken:
- serve/: a request without sampling fields gets the model card's set for its thinking state under the config's block
  (Niko1221#2685; this branch's /v1/choose still reads the model's own logits); max_tokens counts the thinking (the answer
  keeps a quarter; "answer_room": false turns it off); Anthropic's thinking.budget_tokens is a budget too; a prompt is
  encoded from the last special token it shares with a recent one (Niko1221#567); a tool body is not rescanned before a closing
  tag; logit_bias lists normalized once; the Prometheus gauges over the window between scrapes (Niko1221#2341); a config's
  joined args split and its JSON errors placed (Niko1221#2267); an explicit reasoning false kept; the batch slots' turn
  checkpoint pick (Niko1221#2518) and STRATA_ADMIT_SHORT_FIRST.
- web/: the page sends no sampling until it is changed.
- setup.py, tools/, docs/, bench/results, the Metal, SYCL, HIP and Qwen3.6 trees and their tests.
  test_parser_call_scan declares a tool: without one this branch's parser passes the text through verbatim.

Not taken (the engine files they change stay this branch's; upstream's new sources are in the tree, not built):
- The cuda86 group (5a6f4ed): STRATA_QSA_ROWS, STRATA_ATTN_DAILY, STRATA_QSA_SPLIT_ATTN, STRATA_SIDE_MIXER (and its
  fix 477ac13), STRATA_HC_REQ8, STRATA_PROMPT_ATTN_IMMA, the grouped short-prompt gather and the cooperative top-k
  digit: this engine's own kernels do these (SPLIT_ATTN and SIDE_MIXER are ports of its fd23d91 and 34a35db);
  STRATA_B6_MMVQ_ROWS (that kernel is 1.3-3.8% of the 3090's time here); STRATA_EXPDEC, STRATA_HC_UPMIX and the
  once-per-token quantization of the MoE input (c675f5b): open items. 7e8485a: every KV format takes the batched
  window here.
- The cpu group: the AVX2 router and Q5_1/Q8_0 row dots (the router runs on the GPU here, STRATA_KQ256 is off),
  STRATA_CPU_IQ_ALL_EXACT1 (CPUs without AVX-512), --host-core sibling and --pool-affinity measure (hybrid CPUs), the
  --lookup-chain cap (no such option here).
- The cache group (the resident RAM mode's options, STRATA_PCIE_BALANCE, the tier's evictions under NOWAIT, the WSL K/V
  host copy) and fix3 (the PLE gather_batch fix Niko1221#2622: no such fast path here; the layer split's rules; the P2P probe
  Niko1221#2666: no peer copies here): code this engine does not have.
- The conversation cache's reserve, park gain, pinned prefixes and borrowing: upstream's conversation cache is not built
  here (this engine has its own prompt cache; --prompt-cache-spread's and the borrowing's ideas are open items).
- havok (Niko1221#2364), batch, batch-1441: batch slots, layer splits, reads beside decoding slots (one request at a time
  here). The pointer-table MMQ (STRATA_MMQ_TAB, STRATA_MMQ_DIRECT) and STRATA_MMQ_GROUP_SORT: open items.
- sm75, rdna2, rdna34, sycl, Metal: other hardware (Niko1221#2072's split top-k and halving scorer and 6befb3d's Q6_K form:
  open items). The Qwen3.6 engine.

Window hashes on fixed text (thinking, 1,200 tokens, --adapt-every 0, the second GPU's tier pinned) equal the previous
build's on both models; the server's 862 unit tests pass (10 skipped); api_cache_test and api_stream_test pass on both
models.
eddoursul added a commit to eddoursul/Lamina that referenced this pull request Oct 18, 2026
…s in registers (a warp a key block, the lanes' sums by recursive halving with this engine's dot4: upstream's Niko1221#2072 scorer, bitwise block_scores_kernel's; 5-8 queries in two groups of warps over the same blocks; one query keeps block_scores_kernel), and the top-k of up to 8 queries in one launch over many 256-thread blocks (a query's blocks in chunks of at least 2,048 that meet at barriers in device memory between the radix passes, each taking every digit from the query's whole histogram by block_topk_kernel's rule, then each chunk's cells emitted at the counts of the chunks before it; the grid from the capacity, capped at what stays resident; a short context takes one block and no barrier). qsa_select_parity (windows of 1-8 queries now): scores bitwise and selections identical in 123 + 76 cases on both GPUs; window hashes after the 16K and 64K prompts identical to the build before (caches fixed). The 3090, us, before -> now: scores 4 queries 16K 6.5 -> 5.0, 131K 29.4 -> 24.7, 200K 44.9 -> ~37, 8 queries 131K 67.2 -> ~39; top-k 4 queries 2K 14.8 -> 7.0, 16K 17.1 -> 14.6, 131K 35.3 -> 18.6, 200K 62.7 -> ~21, 8 queries 131K 37.9 -> 23.8. Fixed text after the long prompts, the old kernels against these in one binary (2 rounds both orders), a round's time: IQ3_S 64K -1.6%, 131K -2.8%; UD-Q4_K_XL 64K -0.6%, 131K -2.1%; 16K within 0.1%

Claude-Session: https://claude.ai/code/session_019XsrN8Zv3y7DHzXC9STsiV
@rafatxf

rafatxf commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Ported in 0.1.45, thanks! Turing numbers for STRATA_SM75_PERF (2x RTX 2080 Ti, Q2_0 and IQ3_S) are in #2069. Closing.

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