[dsv4.1] Exact bf16 consumer top-k adapted from DeepSelect - #39305
Merged
Merged
Conversation
Replace the approximate 13-bit-key `topk_bf16_small` kernel with an exact selection adapted from DeepSelect's register-resident init-window select, rewritten in plain SIMT and specialised for 16384-wide rows at k = 512: one CTA of 512 threads per row, each thread holding a contiguous slice of up to 32 bf16 scores in registers, a histogram of the raw high byte and then of the raw low byte within the pivot's bucket to find the k-th largest value exactly, a packed gt/eq census per thread, and the equal quota spread by a block prefix. 40 registers per thread (3 CTAs per SM, no spills); the staging buffer shares its smem with the histograms. The host API is unchanged: per-row `seq_lens` (prefetched ahead of the PDL wait, as the streaming top-k does), the fused page-table transform, `-1` past `min(k, seq_len)`. `k` is now at most 2048. NaN scores remain unsupported. Tests: the existing cases plus odd row widths (partial last vector), heavy ties / constant / all -inf rows, signed zeros around the pivot, and denormal / full-range bit patterns, all checked as value multisets against torch.topk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DarkSharpness
requested review from
BBuf,
HaiShaw,
HydraQYH,
celve and
yuan-luo
as code owners
September 13, 2026 14:47
The histogram counts NaN scores but no ordered compare ever selects them, so a row with n positive NaNs stages only its top (k - n) real scores and the remaining slots would echo whatever shared memory held before, which after the page transform could be any block. Derive the staged count from the census totals already in shared memory and write -1 past it. The normal path stages exactly k and pays one broadcast smem read plus a select per output; measured within noise from 1 to 8192 rows. Negative NaNs order below -inf and were never selected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Motivation
The DeepSeek-V4.1 two-level sparse indexer's consumer step picks the top-512 of up to 16384 bf16 sparse logits per row and writes them through the per-row physical block table (
topk_transform_bf16_small). The kernel behind it was a placeholder: a single 13-bit fp16-derived histogram (approximate at the selection boundary), 1024 threads x 16 elements, and untuned.Modifications
Replace it with an exact selection adapted from DeepSelect. DeepSelect's kernel starts by selecting the top-k of a 32 KiB "init window" that lives entirely in registers; our consumer rows (<= 16384 bf16) are exactly that window, so our kernel agent took that part of the algorithm, dropped the TMA/streaming machinery around it, rewrote it in plain SIMT and specialised it for 16384-wide rows at k = 512. The result was then reviewed and wired into the existing host API here.
What is kept from DeepSelect:
x ^ (x < 0 ? 0xFFFF : 0x8000): negatives reversed and below positives, -0 below +0), applied once per lane while walking the histogram instead of once per element;__hgt2_mask/__heq2_maskon bf16x2,prmt+dp4ato turn the flags into one bit per element, one warp prefix plus one shared atomic per warp for the block-wide (gt, eq) prefix, and an equal quota so exactly k elements are taken.What is specific to this port:
The host API is unchanged: per-row
seq_lens(prefetched ahead of the PDL wait, like the streaming top-k), the fused page-table transform,-1pastmin(k, seq_len).kis now at most 2048 (was 4096). NaN scores are never selected (no ordered compare matches them): a row with n positive NaNs yields its top (k - n) real scores and-1in the remaining slots, derived from the census totals so nothing reads stale shared memory; a negative NaN orders below-infand is simply never picked.Accuracy
test/registered/kernels/ops/attention/test_topk_bf16.py(100 cases): the existing configurations plus odd row widths (partial last vector), heavy ties / constant / all-infrows, signed zeros around the pivot, denormals and full-range bf16 bit patterns, and rows with positive / negative NaNs below and above k. Every case checks the selected values as a multiset againsttorch.topk(elements of equal value may swap) and the page transform through the inverse table.test/registered/attention/unittests/dsv4/test_dsv41_sparse_indexer.pypasses.Performance
B200,
marker.do_bench(CUDA graph, L2-rotated inputs), k = 512, page transform included, random bf16 scores, permuted block table. Old kernel -> this PR, in us:Beyond ~450 rows the GPU is saturated (3 CTAs/SM) and time is linear in rows: 36 us at 2048 rows and 134 us at 8192 rows for full-width rows.
k = 2048 stays supported but is not the target shape: it is 10-15% slower than the old kernel at small batch (the epilogue runs on 512 threads instead of 1024) and 10% faster at 512 rows.
Checklist
🤖 Generated with Claude Code
CI States
Latest PR Test (Base): ❌ Run #34764516423
Latest PR Test (Extra): ❌ Run #34764516294
Latest PR Test (AMD ROCm 10): ❌ Run #34764516371