Repository navigation
[Engine] Decode past 64K cells: a multi-CTA top-k and a shuffle-free block scorer in their own window graphs (STRATA_SELECT_LONG=1, opt-in, GP100 builds) - #3820
Closed
ruibeikaa wants to merge 3 commits into
Conversation
… scores without shuffles, for the long-context select (SM60 build; not called yet)
A decode window's QSA select runs the one-CTA top-k (block_topk_wide_kernel past ~135K
cells: 1-5 SMs busy for 0.39 ms per call at 300K cells, 1.15 ms at 1M on a GP100 die) and
block_scores_multi_kernel (5 shuffles per head per block and query), x12 QSA layers per
window: about 7.4 ms of a 300K-cell window on 4x GP100.
New entry points, the existing qsa_block_scores / qsa_block_topk declarations untouched
(the header is shared with SYCL):
- qsa_block_topk_long: with caller scratch (qsa_topk_multi_scratch_ints), no active count
and <= 16 queries, block_topk_multi_*: n CTAs per query (one per 2,048 blocks of
capacity, at most 32), six kernels in a row (radix passes 0-3, the count, the emit)
exchanging through the scratch - no clusters, no spin waits, capturable, nothing carried
between calls. thr and `above` are functions of the integer histograms, the digit rule
is block_topk_kernel's as a scan, and the emit is block_topk_wide_kernel's row emit
offset by the earlier warps' and CTAs' counts: the same ids. STRATA_TOPK_MULTI=0 off,
=1 on any card; default compute capability 6.x past the register kernel's reach.
Otherwise it is qsa_block_topk.
- qsa_block_scores_long: for <= 8 queries and no active count, block_scores_dx_kernel:
lane i of warp h builds block i's head-h sum as the multi kernel's xor butterfly tree
(32 leaves, bit-reversed depth-first order) for every query of the call, keys staged once
per tile (next tile prefetched), relu over the heads in the warp kernel's order; the tail
block stays block_scores_tail_kernel's. STRATA_SCORES_DX=0 off, =1 on any card; default
6.x. Otherwise it is qsa_block_scores.
block_scores_multi_kernel's per-(block, query) arithmetic and the top-k kernels' digit and
emit rules are those of the tree these were checked on (the multi kernel's body moved into
block_scores_multi_body with the same products and shuffles for one set of keys; the
register kernel's digit is now a scan with the same result).
The kernels and the fast paths are built only with STRATA_EXPERIMENTAL_SM60 and not on
HIP; elsewhere the _long functions are the plain calls and the scratch size is 0. Metal
gets stubs. Nothing calls them yet (the verifier's STRATA_SELECT_LONG follows), so every
path is unchanged.
Per call on one GP100 die (capacity 1M cells), top-k / scores, us, measured on the
0.1.41-based tree:
T=1 T=2 T=5
8K 63->39/9->12 65->46/13->15 78->67/24->17
300K 392->60/131->103 390->71/217->154 411->117/487->203
1M 1144->101/403->303 1147->127/692->476 1296->245/1594->638
Checked there against qsa_block_topk_ref and block_scores_kernel byte for byte: contexts
1K-1M, T=1-5, 1/3/7/32 CTAs per query, capacities 131K-1M, and tie-heavy scores (7
values, mostly zeros, -0.0/0.0/negatives, NaN).
…K cells (STRATA_SELECT_LONG=1, default off) The multi-CTA top-k and the shuffle-free decode scorer (qsa_block_topk_long, qsa_block_scores_long) are six and one kernels more per QSA call. On 4x GP100 they cost about 0.25 ms a window each at short contexts, broke even at 30K and saved 4.0-4.7 ms a window at 300K (measured on the 0.1.41-based tree; replies the same). So with STRATA_SELECT_LONG=1 the verifier keeps two window graphs per size, exec_[sel_long_][T]: sel_long_ 1 (the _long calls, topk_scratch_ passed) once a window reaches select_long_at() cells (STRATA_SELECT_LONG_AT, default 65536; 0: always), the one-CTA select below it. - run() picks the variant before it captures; on an overlapped layer split it hands the same choice to the next stage before capturing its window there, and prelaunch() takes it from the window's position, so a stage never launches the other variant (the Niko1221#871 doorbell variant keeps its own [sel_long_] row). - Batch rows (capture_batch) and --pipeline-windows (capture_all, pl_launch) keep the one-CTA select and run exec_[0]. - The scratch (qsa_topk_multi_scratch_ints(T) ints) is carved from the arena only with STRATA_SELECT_LONG=1 on a build that has the kernels. Without STRATA_SELECT_LONG=1 sel_long_ stays 0: the arena layout, the graphs captured and the kernels in them are the ones before this change. STRATA_TOPK_MULTI=0 and STRATA_SCORES_DX=0 still turn either half off inside the long graphs.
…A_SELECT_LONG=1: both variants captured up front, pl_launch picks by the window's context) --pipeline-windows captures every window graph before any window is in flight (capture_all), so with STRATA_SELECT_LONG=1 it now captures both select variants, and pl_launch launches exec_[1][T] once the window reaches select_long_at() cells, exec_[0][T] below it. This is what lets a split of three or more stages keep the long-context select on the pipelined path. Without STRATA_SELECT_LONG=1 capture_all captures only [0] and pl_launch runs it, as before.
Niko1221
pushed a commit
that referenced
this pull request
Oct 27, 2026
…opt-in STRATA_SELECT_LONG=1 (#3820) Problem: a decode window's QSA select runs one CTA per query for the top-k (block_topk_wide_kernel past ~135K cells) and the warp-shuffle block scorer (block_scores_multi_kernel), in each QSA layer. Both grow linearly with the context: on one GP100 die at 300K cells a top-k call takes 0.39 ms with 1-5 SMs busy and a scores call 0.22 ms (two queries). At short contexts the same work is small and an extra kernel is pure overhead. Change: qsa_select.cu (inside !__HIPCC__ && STRATA_EXPERIMENTAL_SM60) gets block_topk_multi_* (six kernels over several CTAs per query, one per 2,048 blocks of capacity, at most 32, exchanging through caller scratch: no clusters, no spin waits, capturable) and block_scores_dx_kernel (the multi scorer's per-head sums built in registers as the same 32-leaf xor-butterfly tree, no __shfl_xor_sync, the tail block stays block_scores_tail_kernel's). New public qsa_topk_multi_scratch_ints, qsa_block_topk_long and qsa_block_scores_long forward to qsa_block_topk / qsa_block_scores outside the build gate or their conditions. verify.cpp keeps a second set of window graphs (exec_[sel_long_][T]) for windows that reach STRATA_SELECT_LONG_AT cells (default 65536); the choice is made before a capture and handed to the next stage of an overlapped layer split, capture_all captures both, pl_launch picks by the window's position. The ids and the score bits are the replaced kernels'. STRATA_TOPK_MULTI=0 and STRATA_SCORES_DX=0 turn either half off inside the long graphs. Review changes against the PR: in a build without the multi-CTA kernels STRATA_SELECT_LONG=1 now says so and is ignored (the PR would have kept two sets of graphs for nothing there); Metal gets stubs for the three new calls (the SYCL tree has its own verify.cpp and qsa_select copy). Default path: unset, select_long_on() is a cached false, topk_scratch_ is not allocated, sel_long_ stays 0, and every window uses exec_[0][T], the graph it used before; the index change is the only difference in verify.cpp. Merge note for f-027-gh-eng (#3702 evict_window_graph, not in this branch's base): its exec_ / exec_nr_ / launched_ accesses need the same [sel_long_] first index. Tests: src/kernels/qsa_select_long_parity.cpp (new; registered with STRATA_SCORES_DX=1 STRATA_TOPK_MULTI=1 in the SM60 test block, and a second case with STRATA_TOPK_MULTI_N=3; skips with 77 without the kernels) compares qsa_block_scores_long with qsa_block_scores (nq 1-8, n_bid 8 .. 131072, tail blocks of 0-3 cells, every block 0..n_bid memcmp) and qsa_block_topk_long with qsa_block_topk (nq 1-16, contexts 1,000 .. 524,288, random scores, all-equal scores and 7 distinct values for ties across CTAs, selected ids of every query; the multi scratch is junk-filled and must have been written). z440 (RTX A4000, sm_86, CUDA 13.0, gpu lock, build with -DSTRATA_EXPERIMENTAL_SM60=ON): "172 cases, 0 differ -> PASS" with one CTA per 2,048 blocks and with N=3. sm_60 compile of the whole tree (CUDA 12.9, P1004): exit 0. Not covered: no GP100 run here (the kernels' last word is the P100, and the speed claim). The contributor's numbers (four GP100 dies, every expert resident, --pipeline-windows 3: 400 tokens at 115K context 51.0 -> 52.5 tok/s, +2.9%; prompts below 64K unchanged; replies and state hashes equal on 15 requests) were not re-measured. The model gate is phaseA-tests/gh027/gates/3820-select-long.sh.
Owner
|
Thank you. Fixed in 0.2.7: Multi-CTA decode top-k and a shuffle-free decode scorer, opt-in STRATA_SELECT_LONG=1. Your name is on the commit. Release: https://github.com/Niko1221/Strata/releases/tag/v0.2.7 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem and result
Everything below was measured on four GP100 dies (two PH402 SKU 200 boards, 32 GB HBM2 per die, 128 GB in all) with every expert resident in VRAM: no CPU share, no PCIe share, no adaptive tier. A rig that keeps experts in RAM or streams them over PCIe was not measured. HIP, SYCL and Metal were not built.
A decode window's QSA select runs one CTA per query for the top-k (
block_topk_wide_kernelpast ~135K cells) and the warp-shuffle block scorer (block_scores_multi_kernel), in each of the 12 QSA layers. Both grow linearly with the context: on one GP100 die at 300K cells a top-k call takes 0.39 ms with 1-5 SMs busy and a scores call 0.22 ms (two queries), about 7.4 ms of a window over the four dies. At short contexts the same work is small, and an extra kernel is pure overhead.This adds a multi-CTA decode top-k and a shuffle-free decode scorer, which return the same ids and the same score bits as the kernels they stand for. With
STRATA_SELECT_LONG=1the verifier keeps a second set of window graphs that uses them once a window reachesSTRATA_SELECT_LONG_ATcells (default 65536); below that the graphs are today's. On four GP100 dies with--pipeline-windows 3, the 400 tokens decoded at 115K context went 51.0 -> 52.5 tok/s (+2.9%; 30.75 -> 29.86 ms a window), the prompts below 64K stayed where they were, and every reply and engine state hash stayed the same on 15 requests. Unset (the default), nothing is allocated or captured and every graph is the one before.Related discussion
None (opt-in kernels in the
STRATA_EXPERIMENTAL_SM60build, no default change). This branch is main plus three commits and needs none of my other PRs. The main e88ee39 run below was on a branch that also carried #3455, #3760, #3819 and #3751, all on in the arm before this one.Approach
qsa_select.cu, inside!__HIPCC__ && STRATA_EXPERIMENTAL_SM60:block_topk_multi_*: six kernels over several CTAs per query (one per 2,048 blocks of capacity, at most 32), exchanging through caller scratch - no clusters, no spin waits, capturable, nothing carried between calls. The digit rule isblock_topk_kernel's as a scan and the emit isblock_topk_wide_kernel's row emit offset by the earlier CTAs' counts, so the ids are the same.block_scores_dx_kernel<1..8>: the multi scorer's per-head sums built in registers as the same 32-leaf xor-butterfly tree, without__shfl_xor_sync, then relu over the heads in the warp kernel's order; the tail block staysblock_scores_tail_kernel's. The score bits are the same.qsa_topk_multi_scratch_ints,qsa_block_topk_long,qsa_block_scores_long. Outside the build gate, or outside their conditions (scratch given, no active-block count, at most 16 / 8 queries), they forward toqsa_block_topk/qsa_block_scores. The existing declarations and bodies are unchanged.STRATA_TOPK_MULTI=0/STRATA_SCORES_DX=0turn either half off inside the long graphs (both default on at compute capability 6.x).verify.cpp:exec_[2][9], the Fresh prompts over --short-read decode !!!! (token 0) when the GPU holds 100% of the experts (48 GB card) #871 doorbell set and the launched flags get the variant as first index;sel_long_picks it before a capture.run()hands the choice to the next stage before capturing that stage's window, andprelaunch()derives it from the window's position, so a stage never launches the other variant.capture_allcaptures both variants with the switch on, andpl_launchpicks by the window's context, so--pipeline-windowstakes the long select too.Validation
Rig: four GP100 dies (compute capability 6.0, application clocks 1050 MHz graphics / 715 MHz memory, 140 W cap), Windows 11, driver 581.80 (TCC), CUDA 12.9, VS 2022. Build
-DSTRATA_EXPERIMENTAL_SM60=ON -DCMAKE_CUDA_ARCHITECTURES=60, ggml at the pinned 3cf03257f, unpatched. Model: Swift 1.5 IQ3_S (abliterated GSQ-RCO GGUF, dense matrices in Q8_0).Passed:
--pipeline-windows 3, opt-in): a host-side scheduler, a slot pool, per-stage undo, an 11-output draft chain #3760, [Engine] Layer split: a stage's commit without the host wait, as on one GPU (STRATA_SPLIT_COMMIT_ASYNC=1, opt-in) #3819 and [Engine] sm_60: the MTP drafter's draft head and projections on the Pascal Q8_0 GEMV too (STRATA_MTP_Q8_SM60=1, opt-in) #3751 on; arm S5 = S4 +STRATA_SELECT_LONG=1(threshold at its default).--serve --expert-cache auto --prefill auto --spec 4 --spec-min-p 0.85 --mtp <drafter> --max-context 1048576 --kv int8 --layer-split 12,24,36 --mtp-window 8192 --pipeline-windows 3 --conversation-cache-mib 32768 --conversation-cache-slots 8 --rope-scaling yarn --rope-scale 4. Environment, the same in every arm:STRATA_Q8_SM60=1 STRATA_STAGE_TRIM=1 STRATA_PREFILL_PIPE=70 STRATA_MMQ_RESIDENT_SORT_NE=1 STRATA_DF_BRANCH=1 STRATA_FUSE_DECODE=1 STRATA_FUSE_WAIT=2 STRATA_SIDE_MIXER=2 STRATA_GR_UP75=1 STRATA_B6_MMVQ_ROWS=1 STRATA_ADAPT_LAG=2 STRATA_ADAPT_NOWAIT=1 STRATA_DECODE_TIMING=1. Each start's switches were checked in the engine process's environment.STRATA_STATE_HASH=1and a 250K-token read added (15 requests), and a timing pass without debug switches (14 requests).STRATA_STATE_HASHitem equal on all 15 requests, including the two that decode past 64K cells, where the long graphs run (400 tokens at 115K, 16 tokens after the 250K read).STRATA_SM60_TABLE=0in both arms, addingSTRATA_SELECT_LONG=1together withSTRATA_IQ_STAGE_TABLES=1took the 115K request 52.0 -> 53.6 tok/s and the five short prompts +0.15%, the same replies; the two switches were not separated there.qsa_block_topk_refandblock_scores_kernel: contexts 1K-1M, 1-5 queries, 1/3/7/32 CTAs per query, capacities 131K-1M, tie-heavy scores (7 values, mostly zeros, -0.0 / 0.0 / negatives, NaN).STRATA_EXPERIMENTAL_SM60=ON(strata) and sm_86 without it (strata), both built without errors. The sm_60 exe containsblock_scores_dx_kernelandblock_topk_multi_*; the sm_86 exe contains neither.Failed: none observed.
Not run:
--pipeline-windows 3, where decode windows go throughpl_launch. Serial decode with the long select was measured only on the older tree, which predates Fresh prompts over --short-read decode !!!! (token 0) when the GPU holds 100% of the experts (48 GB card) #871.Limits
STRATA_EXPERIMENTAL_SM60only. In another build the switch still keeps the second graph set, with the plain kernels in it: no effect but the graphs' memory.CI note: the red
metal-linkcheck is not caused by this PR. The job fails while compiling upstream'smetal/src/mtl_rt.mm(lines 69 and 92:MTLGPUFamilyMetal4,MTLCompileOptions.mathMode,MTLMathModeFast/MTLMathModeSafe), which themacos-14runner's SDK does not provide, so it stops before linking. The only file this PR changes undermetal/ismetal/src/engine/stubs_0145.cpp(a link stub for the new entry point).