Conversation
|
nice work on your PRs!!! https://github.com/detain/sglang/tree/stack-36644-core combined this PR with #38144 + #38209 + #36644 on 4× RTX PRO 6000 Workstation Edition (sm_120, 96 GB) Running this PR as the base of a combined stack against latest main (post-#37500, incl. #36806/#37068), stacked in dependency order: #36787 → #37275 → #38144 → #38209 → #36644. On top of the five PRs we carry three local additions:
Plus rebase adaptations (no functional change): the NUMA-interleaved PLE pinning now attaches to upstream's allocate_ple_host_table() backend abstraction (pinned path only, file backend untouched), and the environ.py additions were merged against the new SGLANG_QWEN4_PLE_FILE_* block. Notes: all auto-selection env knobs (SGLANG_QSA_DECODE_BACKEND / SGLANG_QSA_MQA_BACKEND / SGLANG_QSA_PREFILL_GEOMETRY) left at auto; the tuned prefill geometry requires the 188-SM Server Edition under auto (used tuned to force otherwise). #38144 was the difference between stable and silently-wrong long-context top-k for us. it runs pretty awesome. my cmd line is (Note that SGLANG_QSA_PREFILL_GEOMETRY=tuned line needs dropped for anyone using the Server Edition of the RTX 6000) |
Use the paged Triton path for sparse decode on SM120. Keep the environment override resolution cached for the process lifetime.
Mask non-owner PLE host loads so ranks that do not own a row no longer issue the clamped row-zero read over PCIe. The old read could not corrupt output, because the load mask already included the in-range predicate, but it was an out-of-range access pattern and wasted host bandwidth on every non-owner rank. Also retain the MQA fallback after a compile failure instead of dropping to torch silently, and prewarm the Qwen4 JIT kernels before graph capture so the first capture does not compile inside the graph.
Add Triton 3.7.1 FP8 blockwise configurations measured for the Qwen3.8 expert-parallel layouts on the RTX PRO 6000 Blackwell Server Edition.
Use split-K work partitioning for low-row sparse decode and register the SM120 benchmark with the kernel benchmark suite.
Use Triton for QSA MQA scoring on SM120 and register the architecture-gated benchmark with the kernel benchmark suite.
Merge split-K partials in the final decode program to remove the separate reduction launch.
Spread the GPU-read host table across online NUMA nodes before registering its pages. Keep node-local pinning as an explicit fallback.
Select the hc_mix Triton geometry from the row count on SM120 while preserving the existing path on other architectures.
Use the grouped W4A4 kernel for small routed-row counts on SM120 and register its architecture-gated benchmark.
Clamp sparse attention indices before lookup, keep KV pool address arithmetic in 64 bits, and guard the degenerate inputs the bounds work exposed: empty rows, negative and past-the-end indices, and prefill rows whose visible extent is shorter than the selected budget.
Choose the PLE prefetch buffer from the active capture state so graph replay uses the buffer that was actually captured.
Select the SM120 launch geometry for the Qwen3.8-Flash-Next QSA sparse prefill kernels at both direct launch sites. Add the manual benchmark and registered tests used to check the selection. Auto dispatch uses the tuned tuples on SM120, excluding SM121, with exactly 188 multiprocessors, Triton 3.7 or newer, top-k 2051, and head dimension 256. Group size 6 accepts one KV head. Group size 12 accepts one or two KV heads. Ordinary prefill starts at 8192 total query rows. Chunk prefill starts at 512 rows per request and requires its rectangular grid to stay within twice the active row count. The table setting selects the device table. The tuned setting bypasses the GPU allowlist while retaining the workload and runtime gates. The group-6 tuple uses an 8-row head tile with a 4-warp, 3-stage schedule. The group-12 tuple keeps a 16-row head tile with a 2-warp, 3-stage schedule. The existing measurements show 16 to 23 percent lower chunk-kernel latency for group 6 and 7 to 8 percent for group 12 on the RTX PRO 6000. The benchmark accepts paired per-request query and KV lengths and checks the output from the last timed iteration against the torch reference. Registered coverage checks the production tuple on an admitted SM120 runner, fallback selection on other architectures, launch wiring, stream ordering, and the selector envelope. Measurements use BF16, query counts from 32 through 8192, KV lengths from 8192 through 131072, and CUDA-event medians over 100 iterations with providers alternating in one timing window.
dc928af to
d825cef
Compare
|
Thanks for the run and the write-up. That is the first Workstation Edition result I have seen for this stack. #36497 merged into main on 09-08, so I have rebased #36787 and this PR onto main; the rebase notes are in the #36787 description. Two of your findings are folded in: the prewarm now reads the graph config and the draft width from the runtime context bags, and the fixture change is not needed any more because the GDN commit is gone. On that commit: main now converts the prefill state to fp32 on sm12x by itself, and FlashInfer 0.6.18 ships the pooled bf16 decode kernel, so the exact-sm120 fp32 contract this stack used to carry is dropped rather than relaxed. Your launch line should work unchanged. On SGLANG_QSA_PREFILL_GEOMETRY=tuned: the device set is exact because every number in this PR comes from the 188-SM Server Edition. If you can run test/manual/kernels/benchmark/attention/bench_qsa_sparse_prefill.py on the Workstation part and post the table, I will add it to the set so the override is not needed there. |
Depends on #36787 and stays a draft until it merges. #36497 merged into
mainthrough #37500 on 2026-09-08, so this branch is now #36787 rebased ontomainplus one commit; that commit is what this PR adds (its GPU test moved totest/registered/kernel/qsa/in the rebase).#36787 gave sm120 its QSA sparse decode, split-K and MQA scoring paths but left the two sparse prefill kernels (
_sparse_gqa_prefilland_sparse_gqa_chunk_prefill) launching with the L20 fallback selection: the table's large-prefill entry suppliesBLOCK_N=16with one warp and two stages, and the head tile comes frommax(16, next_power_of_2(group_size)). This PR selects an sm120 launch family by measurement.Motivation
Prefill for this model routes every full-attention layer through the sparse kernels: the indexer selects
token_topk + compress_ratio - 1 = 2051token slots per query row and the attention kernel streams up to that budget (rows whose visible context is shorter stream less). That makes the cost per call nearly independent of context length once the budget saturates; in the measurements below the chunk kernel moves only from 2.98 ms to 3.05 ms between 32k and 128k context on the previous geometry. The cost sits in launch configuration rather than data volume, so tuning the geometry improves it without touching the arithmetic.The tuned family, held in
_SM120_PREFILL_CONFIGSand selected by measurement:BLOCK_M=8,BLOCK_N=16, 4 warps, 3 stages.BLOCK_M=16,BLOCK_N=16, 2 warps, 3 stages.max_q * num_requests <= 2 * total_q) so a batch of one long request padded by short ones cannot take a geometry measured on uniform batches. Triton older than 3.7 is a policy floor: the family was measured on 3.7.1, and a 3.6.0 spot check compiled and ran the 8-row dot. sm121 (GB10) is excluded the same way as in [Qwen 3.8 Flash Next] Add sm120 (RTX PRO 6000 Blackwell) support #36787.SGLANG_QSA_PREFILL_GEOMETRYofferstable(force the device table) andtuned(skip the device set and row gates while keeping the workload, architecture, and version gates, for measuring an unlisted SM120 part with the included benchmark), in the style of the [Qwen 3.8 Flash Next] Add sm120 (RTX PRO 6000 Blackwell) support #36787 backend overrides. The 170-SM 5090 CI runner exercises correctness through the forced tuned path; its performance was not measured, which is why it is not in the device set.On the mechanism: in both kernels one Triton program handles one query row, and
BLOCK_Mtiles the query heads of one KV group, so the fp32 accumulator is[BLOCK_M, 256]per program and aBLOCK_Mbelow the group size is invalid rather than untried (Triton lowers the 8-row dot onto the 16-row MMA instruction shape). The ablations and the register table below carry the evidence: for group 6 either change alone measured slower, so the gain needs the smaller head tile and the schedule together, and the register data rules out the single-factor stories. For group 12, which cannot shrinkBLOCK_Mbelow its 12 heads, the 2-warp 3-stage schedule accounts for the full gain.Modifications
One commit over the stacked history:
_get_prefill_configinsparse_attn.pywith the named_SM120_PREFILL_CONFIGStable, the workload-envelope conditions (kernel kind, top-k, local KV-head count, row thresholds, and the grid-utilization bound), the exact device set, the Triton 3.7 version floor, theSGLANG_QSA_PREFILL_GEOMETRYoverride, and a bounded cache on the device-table choice.Kernel measurements
Produced by the included benchmark on one GPU of the host below: batch 1, chunk 8192, 2,051 selected token slots per query row, head dimension 256, one KV head, BF16, CUDA events, 10 warmups, median of 100 iterations. Timed providers alternate inside one shared window because sequential per-provider runs measured up to 6.8% higher on the first provider from clock state. A provider's absolute time still depends on which providers share its window, so milliseconds are comparable within one table and the ratios are the result; repeats of one cell within a fixed window agree to about 0.1%. The torch reference is computed on every timing run, every provider is checked against it, and the benchmark exits nonzero past a 0.02 max-abs bound. The torch and synthetic columns are timed in their own windows at three iterations, so they are context rather than part of the interleaved comparison. The selected indices come from a deterministic affine walk over the visible range; adjacent rows therefore share fewer selected tokens than a real top-k output would, and the tables below measure the streaming cost, at most a pessimistic view of inter-row cache reuse. Iterations reuse the same K/V tensors, so at 8k and 32k the working set stays L2-resident across iterations; the 128k rows exceed it. A cold-cache or streaming variant (rotating K/V and index buffers between iterations) was not measured, so the cutoffs are established in the regime described here. The
previousprovider derives the pre-tuning geometry from the shipped table logic, so the delta reproduces on a single checkout, and every cell of every table can be regenerated with a forced-geometry provider from the benchmark's command line.The chunk kernel, which serves prefill with a cached prefix and every chunk after the first (a prefix-free single-chunk prompt takes the non-chunked kernel):
The synthetic column materializes packed K/V and calls FlashAttention on the result; the backend has no such prefill route, so it shows what a packed implementation would cost, and past 32k its gather materializes about 17 GB per iteration and dominates it.
The non-chunked kernel, retuned by the same selector, measured with query rows equal to the KV length (torch-checked run):
The TP1 rank shape (24 query heads over 2 KV heads, group size 12) was measured separately and the tuned tuple wins there as well: 4.2 to 5.0% on the chunk kernel across the three KV lengths and 1.1% on the non-chunked kernel at 8k, at 8,192 query rows. The per-program tile is KV-head independent (the KV-head count only scales the grid), which the measurement confirms.
Query-count sweep and the 512 cutoff
The benchmark takes a query count and a forced geometry, so the same harness measured the fixed sm120 tuples against the geometry the previous selector picks at each
total_q(KV 8,192):The table wins short prefills and the tuned family wins at 512 and above. The 512 cutoff is the lowest measured row count at which the family wins for both shapes; the observed crossover sits in (256, 512]. The sweep points below 8,192 hold KV at 8,192 with a prefix, so every row streams the full selected budget; the 8,192 point is prefix-free with ramping visibility, and it is the only measured ordinary-prefill point, which is why the ordinary path requires 8,192 rows while the sweep-backed cutoff applies to the chunk path. Production
total_qis a batch sum. A measured batch of 16 requests of 64 rows (1,024 rows total) ran 4.2% slower on the group-6 tuple and 6.3% faster on the group-12 tuple than the table; in that benchmark shape the per-request context also drops to about 512 tokens, so the batching effect and the shorter-context effect are not separated by this one point. The gate requires 512 rows per request on average, which keeps every measured win and sends that mixed regime to the table, and the added grid-utilization bound keeps batches whose shape the sweep did not cover off the tuned path as well; the benchmark now takes per-request KV lengths so the two effects can be measured separately later. The gap is not monotonic above the cutoff: at 1,024 rows the win shrinks to 0.9% (group 6) and 6.9% (group 12) where the previous selector switches to its large-total_qentry, then widens again at 8,192. The family never regresses at or above 512 in these measurements, and the CPU selector test pins both tuples at every measured query count plus the 511/512 boundary.Geometry ablations
Group 6 (chunk kernel, 8,192 query rows, same tensors and indices across tuples):
Group 12 (
BLOCK_Mcannot go below the 12 heads, so the schedule is the only axis):Compiled-kernel attributes for the ablation tuples:
End-to-end serving
NVFP4 TP2 on two GPUs of the host below, PLE host offload, CUDA graphs, chunked prefill 8,192, MTP off; random inputs at ISL 8,192 / OSL 128, 64 prompts, this branch against its exact base commit. Concurrency 1 was run three times per side with the radix cache flushed before every run:
At ISL 8,192 the within-side spread reaches 9.7 ms (about 2%), larger than the expected effect, which is about 1.0 ms by construction: a prefix-free single-chunk prompt takes the non-chunked kernel, where the group-12 saving is about 0.09 ms per layer across the twelve full-attention layers. So at that shape the serving-level effect is below what the measurement resolves, in either direction. At ISL 131,072 the prompt spans sixteen chunks and every chunk after the first takes the chunk kernel, where the win is largest; every branch run is faster than every base run and the medians differ by 19.6 ms (0.24%). That is smaller than the sum of the kernel-table savings, which is expected where prefill attention overlaps other per-chunk work, and it is the regime where the change shows at serving level. The kernel tables above are the measurement of the change itself. A single concurrency-16 pair at ISL 8,192 measured TTFT 3,529.8 ms (branch) against 3,564.8 ms (base).
Accuracy Tests
The tuned tuples change the online-softmax tile boundaries (
BLOCK_N32 to 16 attotal_q512, and the schedule elsewhere), so prefill outputs are not bitwise identical to the previous geometry. The registered tests bound the kernel against an FP32 torch reference atatol=rtol=2e-2, and end to end thesglang.test.run_evalGSM8K protocol (5-shot; the five prefix examples are held out of the 1,319-item test split, scoring 1,314; chat API, greedy decoding at temperature 0, thinking enabled) ran on an NVFP4 TP2 server built from this branch and from the exact base commit on the same two GPUs:Greedy decoding removes sampling variance; residual differences can still come from request arrival order under 16 client threads, dynamic batching, and kernel reduction order. The 0.38-point difference is in the branch's favor and sits below an approximate standard error for a difference of two runs at this sample count (about 0.55 points, treating the runs as independent; the shared prompt set makes the true uncertainty of the difference smaller than that bound, and one pair of runs cannot settle it), so the right reading is no measured regression. An earlier temperature-1.0 pair of the same protocol moved by the same margin in the other direction, which is consistent with that reading.
Tests
test/registered/kernels/test_qsa_sparse_prefill_triton.py, registered for both the SM120 (RTX 5090) and H100 CI suites so the tuned branch itself runs in CI while the H100 run keeps the fallback path covered:atol=2e-2, rtol=2e-2: both rank geometries, one and two KV heads, ragged batch 1/2/8, non-multiple-of-four prefixes, negative and out-of-range selected indices, deterministic replay, and side streams.BLOCK_M=8, and rows past index 2,050 stream the full 2,051-slot budget. The case also asserts the selector's returned tuple under the production predicate._get_table_prefill_config.test/registered/unit/layers/attention/test_qsa_sparse_prefill_config.py(CPU): imports_SM120_PREFILL_CONFIGSand pins both tuples at every measured query count plus the 511/512 boundary, covers theSGLANG_QSA_PREFILL_GEOMETRYvalues, the Triton version gate, and the fallback path with the architecture predicates mocked, and drives the realis_sm120_supportedandis_sm121predicates through mocked CUDA facts.The existing QSA suites (
test_qsa.py,test_qsa_mqa_triton.py,test_qsa_sparse_index_bounds.py,test_qsa_mtp_shared_indexer.py, compressed addressing) pass unchanged.Compute Sanitizer memcheck, initcheck, racecheck and synccheck are clean on the benchmark's sanitizer driver: batch 1, tuned geometry, both kernels at both 8k and 128k for both rank shapes (eight cases per tool), guard-banded allocations.
Environment
Not covered
The indexer scoring and selection chain is unchanged. The chunk wrapper still derives its launch bound with a device read; removing the host reads on that path is a separate change that needs its own measurement.
Checklist
CI States
Latest PR Test (Base): ❌ Run #35072149282
Latest PR Test (Extra): ❌ Run #35072148938
Latest PR Test (AMD ROCm 10): ❌ Run #35072149349