Repository navigation
[SM100] Add exact BF16 paged MQA histograms for next-n 1–6 - #96
Heisenberg-Yin wants to merge 1 commit into
Conversation
Add a separate SM100 API for DeepSeek-V4.1 LiteTopK decode: MXFP4 paged MQA logits with BF16 head weights and BF16 output, optionally counting every live non-NaN output score once into a 1024-bin coarse histogram. Adding the histogram does not change the logits. Existing MQA APIs and their FP32 scores are unchanged. - fp4_paged_mqa_logits_bf16(q, kv_cache, weights, context_lens, block_table, schedule_meta, max_context_len, indices=None, histogram=None, tokens_per_request=1): Q packed MXFP4 [rows, 1, 32, 64] + UE8M0 scales, KV pages [pages, 128, 1, 68], BF16 weights [rows, 32]. Adjacent equal request IDs share KV loads. - get_paged_mqa_logits_bf16_metadata(context_lens, block_kv=128, num_sms, indices, tokens_per_request): split-384 schedule balanced by estimated split cost, up to 16384 rows; not interchangeable with the existing split-256 metadata. - tokens_per_request is a CPU hint in 1..6: 1..4 select Q4 with three TMEM stages, 5/6 select Q6 with two; 6 also swizzles the histogram's shared memory. - histogram: contiguous int32 [rows, 1024]; counts are added, bins run in descending score order and match the paired SGLang BF16 selector. - Both APIs are exposed through pybind and tvm-ffi (deep_gemm and sgl_deep_gemm). Kernels live in deep_gemm/include/deep_gemm/mqa_bf16/, the contract in docs/mqa_bf16.md, and the test in sgl_deep_gemm/tests/test_mqa_bf16.py (added to run_tests.sh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port sgl-project#96 with split384 metadata and Q4/Q6 routing for one through six query tokens. Signed-off-by: Heisenberg-Yin <ziqi003@e.ntu.edu.sg>
…stograms, next-n 1..6 Port the BF16 paged MQA producer of deepseek-ai#462 (merge 057ca59: MXFP4 Q/KV, BF16 head weights, BF16 head reduction and BF16 logits) as an isolated `mqa_bf16` kernel with its own split-384 cost-balanced schedule, and let it emit the same coarse histogram as the FP32 path. - APIs: `get_paged_mqa_logits_bf16_metadata(context_lens, 128, num_sms, indices=, tokens_per_request=)` and `fp4_paged_mqa_logits_bf16(..., indices=, histogram=, tokens_per_request=)` (pybind, TVM-FFI, deep_gemm / sgl_deep_gemm). - Adjacent equal request IDs share KV loads; the CPU hint selects Q4 / 3 TMEM stages (1..4) or Q6 / 2 stages (5..6; 6 also XOR-swizzles the histogram). Q4 keeps deepseek-ai#462's static TMEM stage mapping (its no-histogram kernel is SASS-identical to deepseek-ai#462); Q6 rotates a two-stage TMEM ring. - Exact histogram: every live non-NaN stored BF16 score counted once per row (16-bit shared counters for a window of rows, packed fast path for 2^-14 <= |x| < 16, published with packed 64-bit global atomics). The histogram kernels' scheduler caps a task's splits so the 16-bit overflow guard runs once per task; Q6 counts a 12-row window with 6 KV stages, so a CTA moving to the next request does not publish mid-kernel. Logits are bit-identical with and without the histogram. - Shared with the FP32 path: the coarse-bin encoding (epilogue/ coarse_histogram.cuh, plus a BF16 overload) and NoHistogram; dev's packing, ring pipeline (new parity-phase option for steps longer than a 2-stage ring), math / PTX / UMMA descriptor helpers. Only the kernel core, its shared-memory layout, its scheduler and the histogram accumulator are BF16-specific. - Test: sgl_deep_gemm/tests/test_mqa_bf16.py (n = 1..6, PDL off / on, exact counts, accumulation, CUDA-graph replay with changing lengths and request IDs). Replaces sgl-project#96.
Closing in favor of #94, which now includes this BF16 DeepSeek-V4.1 producer alongside the FP32 histogram, with the shared bin encoding and helpers deduplicated and a faster histogram path. Please review there. |
…stograms, next-n 1..6 Port the BF16 paged MQA producer of deepseek-ai#462 (merge 057ca59: MXFP4 Q/KV, BF16 head weights, BF16 head reduction and BF16 logits) as an isolated `mqa_bf16` kernel with its own split-384 cost-balanced schedule, and let it emit the same coarse histogram as the FP32 path. - APIs: `get_paged_mqa_logits_bf16_metadata(context_lens, 128, num_sms, indices=, tokens_per_request=)` and `fp4_paged_mqa_logits_bf16(..., indices=, histogram=, tokens_per_request=)`: TORCH_LIBRARY ops in csrc/torch_library_api.cpp (`Tensor(a!)? histogram`, no schema defaults), deep_gemm / sgl_deep_gemm wrappers that own the defaults, and pybind. - Adjacent equal request IDs share KV loads; the CPU hint selects Q4 / 3 TMEM stages (1..4) or Q6 / 2 stages (5..6; 6 also XOR-swizzles the histogram). Q4 keeps deepseek-ai#462's static TMEM stage mapping (its no-histogram kernel is SASS-identical to deepseek-ai#462); Q6 rotates a two-stage TMEM ring. Each stage count is the deepest ring that fits (TMEM columns, SM100 shared memory with the histogram window); split 384 and the 3 x 128 math threads are named `Config` constants. The schedule (one CTA, two int32 per row in shared memory) checks its documented limit of 16384 rows. - Exact histogram: every live non-NaN stored BF16 score counted once per row (16-bit shared counters for a window of rows, packed fast path for 2^-14 <= |x| < 16, published with packed 64-bit global atomics). The histogram kernels' scheduler caps a task's splits so the 16-bit overflow guard runs once per task; Q6 counts a 12-row window with 6 KV stages, so a CTA moving to the next request does not publish mid-kernel. Logits are bit-identical with and without the histogram. - Shared with the FP32 path: the coarse-bin encoding (epilogue/ coarse_histogram.cuh, plus a BF16 overload) and NoHistogram; dev's packing, ring pipeline (new parity-phase option for steps longer than a 2-stage ring), math / PTX / UMMA descriptor helpers. Only the kernel core, its shared-memory layout, its scheduler and the histogram accumulator are BF16-specific. - Test: sgl_deep_gemm/tests/test_mqa_bf16.py, a plain script that run_tests.sh runs on SM100 (n = 1..6, PDL off / on, exact counts, accumulation, CUDA-graph replay with changing lengths and request IDs; the schedule's row limit). Replaces sgl-project#96. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Fridge003 @DarkSharpness could you take a look when you are free?
Thanks to @TKONIY for the collaboration on this work.
Motivation
The companion SGLang#42351 uses a score histogram to select the exact top-512 index-K slots for DeepSeek-V4.1 decode and target verify. This PR ports the upstream BF16 paged MQA producer into sgl-project
devand adds an optional exact histogram of its stored scores, including next-n 5/6 KV reuse.The companion SGLang PR is an incremental change after SGLang #40764. The existing FP32 module stays in #40764; BF16 producer kernels, bindings and producer tests stay here. SGLang adds the BF16 selector and serving integration and requires this PR's BF16 APIs.
Code sources and diff scope
The BF16 scoring arithmetic and device core are adapted from deepseek-ai/DeepGEMM#462, merge commit
057ca5964aae0879ff2e0eb71ee05a3cb0ba3df7. deepseek-ai#462 is both an implementation source and the independent numerical/performance reference. BF16 output and BF16 head reduction originate upstream.mqa_bf16/057ca5964aaekernel.cuhimpls/sm100_mqa_logits.cuhscheduler.cuhscheduler/sm100_paged_mqa_logits.cuhlayout.cuhlayout/mqa_logits.cuhpacking.cuhcommon/packing.cuhring_pipeline.cuhcommon/ring_pipeline.cuhhelpers.cuhThe coarse-bin encoding follows the earlier sgl-project/DeepGEMM#94 histogram design. BF16 counter packing, histogram integration, Q6 routing, swizzle, scheduling changes and dev bindings are the work added here. The existing generic MQA APIs keep their implementation and default FP32 behavior.
This PR targets
devatec226bcf400c11d62755137b916fae9ee359c684. Its 17 files, +1,827 / −1 include 1,372 device-header lines in a separatedeep_gemm/mqa_bf16directory, so adapted upstream code is visible as newly added files. It is a complete isolated port plus histogram extension, rather than a small diff that only adds histogram atomics to the generic dev kernel.Modifications
get_paged_mqa_logits_bf16_metadataandfp4_paged_mqa_logits_bf16through both pybind and TVM-FFI wrappers (deep_gemm/sgl_deep_gemm).[R,1024]histogram. Histogram-enabled and histogram-disabled calls return bit-identical live scores. Counts are per row and per score, with no pooling or block-max substitution.tokens_per_request: 1–4 selects Q4 / 3 TMEM stages; 5–6 selects Q6 / 2 stages, sharing KV loads among adjacent query rows of a request; hint 6 enables shared-memory histogram XOR swizzle.docs/mqa_bf16.mdand GPU tests insgl_deep_gemm/tests/test_mqa_bf16.py.Scope
SM100, H32/D128, MXFP4 Q
[R,1,32,64]with packed int32 UE8M0 scales[R,1,32], paged uint8 KV[pages,128,1,68], BF16 weights[R,32], and BF16 output[R,max_context_len]with row stride aligned to 1536 elements. Context lengths are int32[R,1]; page tables have one row per score row. CPU hint is 1–6, with at most 16,384 rows.IDs identify adjacent request runs; page tables match and causal lengths are nondecreasing within each run. Omitting IDs treats rows independently. Schedule and producer use the same hint/IDs/lengths; this metadata is incompatible with the generic MQA schedule. Only live score columns are defined. Callers zero counts before an independent call, or use the companion selector which consumes and clears them.
BF16 scoring follows upstream deepseek-ai#462 semantics and can yield a different top-k from the default FP32 producer. Histogram counts ignore NaNs; the companion selector's nonfinite behavior is documented in the SGLang PR.
Accuracy Tests
Fresh focused test run on the complete SGLang PR head: 55 passed — 15 dispatch/storage/metadata unit tests, 12 producer tests, and 28 BF16 selector/serving GPU tests. The GPU suite covers n=1–6, PDL off/on, exact counts, repeated accumulation, zero/page/split/top-k boundaries, shared and changing request IDs, dynamic CUDA graphs, FullTopK/SparseTable, storage lifetime, fallback and BF16 candidate maxima. The producer test is an unchanged copy of the repository test run outside the package directory; its source hash is recorded.
Every one of the 90 cold benchmark cells verifies live BF16 bits against the independent upstream deepseek-ai#462 producer, exact histograms against a per-score oracle, selection values against
torch.topk, valid unique physical slots and reusable scratch. LiteTopK additionally matches(BF16 ordered score key descending, physical slot ascending)exactly. The default FP32 chain is checked against top-k of its own FP32 scores. Outputs are checked before and after timing.The separate fresh qualification has 69 passing records: 4 large-batch cases up to 768 rows, 51 graph replays across page/split/top-k boundaries, 8 selector edge cases (including ties, signed zero, nonfinite scores and overflow), graph storage growth, and 5 actual FullTopK hook cases. Both live producer bindings come from the dev-targeted #96 build; the existing sparse APIs are used directly. The 90-cell cold matrix and 69-record qualification apply the live SGLang diff to local main snapshot
a8b54f138e; the complete PR head additionally passes the same 55-test suite. All timed selector/top-k files match the complete head byte for byte. Three unrelated Hopper-path base changes are recorded in the source audit. Earlier measurements are retained separately and are not substituted for these reruns.Speed Tests and Profiling
These fresh measurements use the dev-targeted TVM-FFI build from this PR, with all device headers verified against its live diff. Upstream deepseek-ai#462 is the BF16 reference. The following numbers are CUPTI producer kernel durations from the randomized measurements, not isolated histogram execution costs. Reference BF16 runs in a DeepSelect chain, final no-hist runs alone, and final histogram runs in the LiteTopK chain. Mapping, selection and metadata are not separate summands in these producer durations, but their scheduling and resource effects can affect the durations.
Each timed cold replay contains one call, with cache eviction before the call. The producer-duration change is an observed cold measurement under these chain contexts. It is not an isolated instruction-cost estimate; a matched producer-only histogram/no-hist ablation with reset costs controlled would be needed for that. PDL is enabled in these measurements; its on/off performance contribution was not measured.
Cold-cache measurements use NVIDIA B200 (148 SMs), Torch
2.13.0+cu130, CUDA13.0, and PDL enabled. Comparisons within each cell run on the same GPU; n=1–3 use GPU4 and n=4–6 GPU6. The matrix is n=1–6 × B={1,16,64} × L={4096,16384,65536,262144,1048342}: 90 cold cells. B counts requests, R=B×n score rows, and L compressed KV positions.Inputs are captured DeepSeek-V4.1 layer-20 MXFP4 Q/KV and BF16-rounded head weights, extended to each shape with randomly permuted physical pages and each query row's causal length. The default FP32 producer receives one independent page-table/length row per query, as the serving path does; the paired BF16 producer can reuse KV among adjacent equal request IDs. L is the live context, while score allocation retains the captured ~1M-column maximum window; only live columns are defined.
Each cell uses 80 randomized paired blocks. A 1 GiB buffer is zeroed before every single-call CUDA graph replay to evict the preceding cache contents. The flush is outside the timed span; CUPTI measures each chain from its first kernel start to its last kernel end. Outputs and scratch are reused. Physical-slot mapping, histogram consumption and scratch cleanup are included. Q projection/packing, schedule/plan construction, serving weight conversion and CPU launch overhead are outside this matrix.
Clocks and power settings are unchanged. Idle gates and per-process monitoring reject affected cells with a 10 s / 6 s margin, including their preparation and profiling window. Raw samples, kernel launch signatures, clocks, rejected attempts and source/binary hashes accompany the results.
Geometric means over 15 cold shapes per n. Ratios compare observed producer kernel durations in their respective chains; the last column is the histogram-chain producer duration divided by standalone no-hist duration, minus one. Use the complete cold chain measurements for the latency/speedup conclusion.
cold: all 90 producer cells
The full cold SGLang chain has geometric means of 1.549× against default FP32 SGLang and 1.249× against the faster full/sliced DeepSelect chain. The first comparison includes producer precision/arithmetic and KV-sharing changes. The companion SGLang PR contains every cold chain result and the slower cases.
Cold A/A controls duplicate both the default SGLang and full-width DeepSelect chains with separate buffers/graphs and identical flush treatment. Default SGLang A/A: geometric mean 1.0024, range 0.9502–1.0491; DeepSelect A/A: 0.9988, range 0.9705–1.0348. Near-equal timing ratios are numerical results, not significance claims.
devalready includes the sparse MQA APIs used by SGLang's SparseTable path; this PR adds the BF16 dense producer. Producer and chain timings do not establish full-model serving throughput or model quality.