Repository navigation
Conversation
….512 Turing ran block-FP8 QPN8 linears through the Volta m8n8k4 packed GEMM for M <= 8 (half tensor rate on SM75) and above M=8 reconstructed the whole FP16 weight on every call before cuBLAS. A new m16n8k8 GEMM decodes and block-scales the packed codes once per CTA while staging them in shared memory (bit-identical to the dense reconstruction), reads both operands through ldmatrix, rasterises row tiles in groups for L2 reuse and splits K across CTAs with a fixed-order FP32 reduction. Tile and split come from a wave model over the kernel instances' occupancy. No second weight layout. Turing runs M=9..512 through it; M <= 8 keeps the packed GEMM and larger M the dense prefill. Timed without Python dispatch (20 calls per CUDA graph, replays interleaved, Quadro RTX 8000), the tensor-core GEMM took 1.01-1.28x the time of the packed GEMM at M=1..8 on 10 of 13 DeepSeek-V4 and Qwen3.8-27B projection shapes and 0.78-1.02x on the other three; at M=16..64 it took 0.16-0.60x the time of the previous path. A DeepSeek-V4 A/B (PP5, DSpark K=5, decode M=6 on the RTX stages) with the GEMM also at M <= 8 had 1 ms slower decode steps and 0.5 % faster prefill. The FP8 decoder and lane map move into fp8_qpn8_codes_sm70.cuh, shared by both kernels. The Turing source ships sm_75 code even in a 7.0-only build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172gtUiymppDzziDqKfy78Q Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
The scale of a K step was read from global memory in store_shared, after the MMAs of the previous step, so its latency sat on the path to the next __syncthreads. It is now loaded in load_global together with the codes and carried in registers. Same arithmetic, bitwise equal outputs (840 tile and split configurations on the 27B TP2 shapes); the BM128 variant also drops its 32-byte stack frame. RTX 8000, CUDA graphs against the previous kernel: M 128-512 with BM128 7-12 % faster; BM16, BM32 and BM64 within 2 %. Prefetching two K steps was measured as well and rejected: more registers (BM16 54 -> 64, BM32 72 -> 82), fewer resident blocks, slower for every tile height. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172gtUiymppDzziDqKfy78Q Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
A CTA of the M <= 8 QPN8 GEMM covers 32 output columns with split-K warps along K, so it holds 32 * split_k threads. On Turing (1024 threads per SM) a split-16 CTA fills half an SM, so N = 5120 (160 CTAs) spills 16 CTAs past the 144 slots into a second, almost empty wave. Split-K 8 fits the grid into one wave: o_proj/out_proj (K 3072) 0.78-0.90 and down_proj (K 8704) 0.73-0.87 of the split-16 time (CUDA graphs, M = 1..8, RTX 8000). The V100 holds 320 split-16 CTAs per wave and loses with split-K 8 on spills (1.18-1.24), so Volta keeps its table. The fitted split applies to the M <= 8 launch only, where it was measured: the per-tensor path keeps the table split for its M16 and M32 bodies (the native M=17..32 candidate takes split-K 12 or 16), and the block path reaches the packed GEMM only up to M 8. Split-K 32 for grids with no more CTAs than SMs was faster in isolation (DeepSeek-V4 wq_a/wkv 0.79-0.90 of split 16) but changed no DeepSeek-V4 step time in an A/B/A/B (62/65/70 ms both, prefill 250k 222.25-222.28 vs 222.16-222.37 s) and lowered the sampled draft acceptance in all eight comparisons (0.1-1.5 points), so it is not used. Qwen3.8-27B NVFP4 (FP8 attention projections, TP2 on two RTX 8000) gains 1.4-1.8 % decode tok/s in an A/B/A/B, texts byte-identical in all boots. Output error against FP64 is unchanged (per-tensor 4.1-4.9e-4 at M 1-100). Tests: tests/quantization/test_turing_qpn_kernel_selection.py 33 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172gtUiymppDzziDqKfy78Q Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
The grouped QPN kernel launched one grid row per routing slot (tokens * top_k). A batch never has more groups than experts, so in prefill most rows were padding groups that only read goff and exit: DSv4 at 512 tokens launched 3072 rows for at most 256 groups, Flash-Next at 748 tokens 7480 for 512. The grid now spans min(slots, experts) rows, still a static bound for CUDA graphs; decode (slots < experts) launches as before. Bit-identical output. CUDA-graph microbench of the kernel source with only this line changed against the previous build (sm_70 SASS on both cards, synthetic weights, uniform routing; us per call, lower is better): - DSv4 T512 (E256 top-6): RTX 8000 w13 7070 -> 6506, w2 3546 -> 3250; V100 w13 5524 -> 5004, w2 3004 -> 2442 - Flash-Next PP T748 (E512 top-10): RTX w13 3800 -> 3297, w2 2682 -> 2002; V100 3127 -> 2567, 2557 -> 1634 - Flash-Next TP2 T748: RTX 1961 -> 1653, 2113 -> 1329; V100 1663 -> 1275, 1977 -> 1052 - T6 decode: unchanged Tests: tests/kernels/moe/test_skinny_sm70_moe.py 77 passed on V100 and RTX 8000 with the rebuilt _C; pre-commit passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3uEMYxiUMDKrA6E8o8sP Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
Turing executes Volta's mma.m8n8k4 at half its tensor rate. Expert weights are now packed at load in the fragment order of the card's MMA: m8n8k4 on SM70, m16n8k8 from SM75 on (a pure permutation, same size, no second copy; each layer lives on one card). The new kernel uses the weights as the 16-row A operand and the expert's rows as the 8-column B operand, reads each activation slice once per warp (no shared-memory stage), shares each activation load across two 32-column tiles and each weight decode across four 8-row tiles in prefill. The decoded weights equal the m8n8k4 path bit for bit; only the tensor core's summation order differs. Microbenchmark on a Quadro RTX 8000 (CUDA graphs, per call, same checkpoint weights in both orders): DSv4 512 tokens w13 6555 -> 4303 us, w2 3271 -> 2238 us GLM-5.3 512 w13 8566 -> 5316 us, w2 4205 -> 2760 us decode (4-6 tokens) at parity (-1.3 % .. +1.0 %) The m8n8k4 order of the new source is bit-identical to the previous kernel; the m16n8k8 order stays within 6.3e-4 of the output maximum and matches the fp32 reference as closely. The shared header mma_fragments_sm70.cuh holds the e2m1 decoder (derived from TurboMind's cvt_f16x8_e2m1), the m16n8k8 MMA and ldmatrix, replacing the copies in nvfp4_qpn4_sm70.cu, skinny_moe_qpn_sm70.cu and fp8_qpn8_mma_sm75.cu; below SM75 the MMA and ldmatrix helpers trap. The split-K choice per fragment order stays in grouped_splitk. skinny_moe_qpn_sm70.cu ships sm_75 code next to its sm_70 code. The rest of the SM70 family stays sm_70 and runs on Turing through binary compatibility: a Qwen3.8-27B A/B/A/B of the whole family built for 7.0 and 7.5 against this split showed no gain from the extra images. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3uEMYxiUMDKrA6E8o8sP Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
The Turing sweep's structure carries over to the m8n8k4 kernel: a CTA takes two 32-column tiles that share each staged activation fragment, and both GEMMs prefer 8 split-K warps (16 or 10 only where 8 does not divide K). Prefill keeps two 8-row blocks per weight load (four helped DSv4 by 1 % and slowed GLM-5.3 by 6 %). V100, CUDA graphs, us per call against the previous kernel: DSv4 512 tokens w13 4979 -> 4374 (0.88), w2 2441 -> 2395 (0.98) GLM-5.3 512 w13 6878 -> 5954 (0.87), w2 3567 -> 3218 (0.90) decode (4-6 tok) 0.80 .. 0.97 With the same split-K the tile counts change no bit; the new split regroups the reduction. The two-accumulator variant (nacc 2) had no caller and leaves the kernel and the op signature; the N % 64 check and the grid are shared by both fragment orders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3uEMYxiUMDKrA6E8o8sP Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
Real routers skew: in a DeepSeek-V4 prefill chunk a few experts take hundreds of rows while others take a handful. The large MXFP4 dispatch test only drew uniform ids (about 256 rows per expert); it now also runs a skewed draw that checks its own shape (one expert over four 32-row passes, one under 16 rows), so the hand kernels' multi-pass groups and single partial blocks are compared against the reference in one call. Tests: tests/kernels/moe/test_skinny_sm70_moe.py -k large_mxfp4: 2 passed on V100 (m8n8k4), 4 passed on RTX 8000 (both orders). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141pGKkAa2PjDMhrUwfX3ox Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
… without P2P For two-rank tensor-parallel groups whose GPUs NCCL cannot connect peer to peer (P2P disabled or unavailable), one kernel exchanges the FP16 payload through a pinned host buffer both ranks map: each block writes its chunk, fences, raises a flag, waits for the peer's flag and adds the peer's chunk. One FP16 rounding and commutative, so the result has NCCL's bits. No CPU work, capturable in CUDA graphs. It sits after the SM70 ring in the all-reduce chain and takes FP16 payloads up to kernel_config.host_reduce.max_bytes (256 KiB); larger ones stay on NCCL. kernel_config.host_reduce.enabled turns it off. The exchange kernel is adapted from kernels/skinny_ar.cu of mzen17/v100-skinny-unify (MIT, notice kept in the source), turned into handle-based channels so a process can own more than one two-rank group. Co-authored-by: Mike Zeng <92705460+mzen17@users.noreply.github.com> Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ZLMmGB8mjc43VpwPxLQNn (cherry picked from commit f53a668)
FA2's Turing build runs causal head-dim-256 prefills over an fp16 paged cache slowly: its split-KV kernel keeps Q in shared memory next to K and V, a CTA fills the SM's 64 KB, and without cp.async every tile's loads stall the math. A 2048-token chunk after 150k tokens of context takes 165.8 ms there, about 23 TFLOPS. The new kernel holds 128 packed query rows per CTA in registers (eight warps of 16; the query heads of a KV head share each loaded key), double-buffers K/V tiles of 32 keys in shared memory, and fetches the next tile in two register-staged halves while the current one computes. 32-bit cache offsets and a compile-time page size keep the MMA loop free of spills; the O rescale is skipped while every factor of the warp is 1. The source is built for sm_75 only. Quadro RTX 8000, Qwen3.8-27B per-rank shapes (12 query heads, 2 KV heads), causal chunk at the end of the context, ms per call: 2048 tokens 32k ctx 35.3 -> 13.1 150k ctx 165.8 -> 61.5 (61 TFLOPS) 512 tokens 150k ctx 52.5 -> 20.0 96 tokens 150k ctx 24.6 -> 19.8 64 tokens 150k ctx 5.3 vs 19.9 (FA2 splits the context there) Errors against an fp32 reference equal FA2's on every case. The FLASH_ATTN backend takes the kernel on SM75, by the capability of the query's own device, once a request in the batch brings at least attention_config.flash_attn_turing_prefill_min_query_len (65) query tokens: causal FA2 attention with an fp16 query over a non-quantized cache, without sliding window, ALiBi, sinks, softcap or batch invariance. The launcher takes pages of 16, 32 or 64 tokens and forms 32-bit cache offsets, so the selection keeps other page sizes and cache views of 2^31 elements or more on FA2. Tests: tests/kernels/attention/test_paged_prefill_attention_sm75.py (fp32 reference cases on an SM75 GPU; the selection keeps pages of 128 and 800 tokens and a cache beyond 32-bit offsets on FA2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3uEMYxiUMDKrA6E8o8sP Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
MLA prefill backend selection asks flash_v100_dense_prefill_lse_available() while the model is built, before the engine binds its Flash-V100 runtime. The check went through get_flash_dense_forward(), which binds the operation and raised "Flash-V100 resources must be bound before execution": the first GLM-5.3-Flash boot failed with it. The loader is now separate from the bound accessor; availability only loads, execution still binds. Tests: tests/v1/attention/test_flash_v100_dense_prefill_availability.py, 2 passed (availability without a bound runtime; execution still requires it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019x3uEMYxiUMDKrA6E8o8sP Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
Without peer access, a TP2 all-reduce goes through host memory: on two Quadro RTX 8000, NCCL's all-reduce kernels took 96 of 208 s of a 150k-token Qwen3.8-27B prefill that already ran the Turing prefill attention. kernel_config.tp_prefill_overlap_min_tokens (0 = off) now runs eager prefill batches of at least that many tokens, without draft tokens or padding, as two interleaved halves, after the DBO micro-batch mechanics: both halves run the model on one compute stream in two threads, and each all-reduce (GroupCoordinator's _all_reduce_out_place and all_reduce_sum2) runs on a comm stream while the other half computes its layer. The field stays out of the compile hash: the compiled graph is the same, only called twice. A half is a token range of the batch (prefill_overlap.token_range_batch): a request cut at the split ends there in the first half and continues in the second, as a following chunk would, so the second half's attention reads the first half's KV writes of the same layer. Each half builds its attention metadata with its own builder (builders 1 and 2; builder 0 keeps the whole batch's metadata for the speculator). In mamba align mode both halves must use the recurrent state slot of the chunk end: align_ubatch_split moves the split so that the first half also ends inside that state block (801 + 799 tokens for a 1600-token chunk of 800-token blocks), and the second half continues the state the first half wrote in place. The output concatenation moved to ubatching (cat_ubatch_outputs) for both runners. Needs TP without PP, DP or decode context parallelism. Errors at startup reject LoRA, KV transfer, PLE offload, mamba cache mode "all", encoder-decoder models, models with FusedMoE layers (their modular kernel asserts outside DBO when prepare/finalize is not async), and models with deepstack visual features (Qwen3-VL's deepstack_num_level, Granite 4 Vision's _ds_layer_indices), which sit in a model buffer that each half would read from token 0. Inside the halves the SM70 fused all-reduce + RMSNorm ops raise, since they would run a collective on the compute stream beside the other half's all-reduce on the comm stream, and model inputs other than input_ids, inputs_embeds and positions raise. A failing micro-batch now sets a shared abort flag before it wakes the next one, which raises MicroBatchAborted at its next yield instead of waiting forever for the failed thread; PrefillOverlap re-raises the original error. The existing DBO micro-batches use the same contexts. Tests: tests/v1/worker/test_prefill_overlap.py, 14 passed on an RTX 8000 (token ranges, M-RoPE input split, align-mode split, rejection of DCP, encoder-decoder, MoE and deepstack models, and on a GPU: two halves with a slow stand-in all-reduce on the comm stream equal the whole batch bit for bit, and a failure in either half stops both and reports its error); tests/v1/worker/test_gpu_model_runner_v2.py, 5 passed (its two failing test_sm70_v2_mtp_profile_reports_from_last_pp_stage cases fail the same on main). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141pGKkAa2PjDMhrUwfX3ox Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
Without peer access, NCCL moves TP all-reduces through host memory with SM copies (LL protocol), and so does the host reduce's kernel mode. Their polling of host memory slows the kernels running beside them. In an nsys profile of a 100k-token Qwen3.8-27B prefill with the TP prefill overlap on two Quadro RTX 8000, the NVFP4 weight dequantization kernels ran 31 and 7 % longer per call, the FP8 ones 18 to 37 %, and the largest cuBLAS GEMM 22 % longer per tile row than in the baseline profile without overlap, so overlapping gained little. host_reduce.copy_engine_max_bytes (0 = off; otherwise above max_bytes) adds a mode for payloads above max_bytes: the copy engines move this rank's tensor into its host slot and the peer's back in up to 16 pieces, ordered by stream memory operations (cuStreamWriteValue32 once a piece landed, cuStreamWaitValue32 before reading the peer's), then one FP16 add gives the same bits as the kernel mode and NCCL's two-rank sum. Sending runs on the channel's own copy stream, receiving and the add on the caller's stream; slots alternate by call parity and are reused only after the peer acknowledged reading them. The mode pins four slots of copy_engine_max_bytes in shared host memory. The call sequence lives on the host, so the mode cannot be captured: while a stream is capturing, payloads above max_bytes are rejected (copy_engine_mode_not_capturable) and the next all-reduce in the chain serves them, as before. Decode payloads stay on the kernel mode. RTX 8000 pair, 8,192,000 B per call, eager: 3.15 ms (2.60 GB/s) against NCCL's 3.93 ms (2.08 GB/s); a GEMM loop beside it changed its time by -2.8 % against +18.2 % beside NCCL, a memory-bound loop by +1.8 % against +6.0 %. In the 100k-token profile with this mode the prefill took 95.85 s instead of 118.22 s, the dequantization kernels ran 4 to 11 % and the largest GEMM tile 0.3 % longer than without overlap. Tests: tests/distributed/test_host_reduce.py on the RTX pair with NCCL_P2P_DISABLE=1, 3 passed (new: copy-engine mode bit-identical to NCCL for 0.3 to 21 MB, back to back on a side stream beside GEMMs and mixed with the kernel mode; a captured payload above max_bytes is rejected without failing the capture; the config rejects a copy_engine_max_bytes at or below max_bytes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141pGKkAa2PjDMhrUwfX3ox Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
One CTA of paged_prefill_attention_sm75 fills a Turing SM (64 KB of shared memory), so its grid loses up to a whole wave to the last one: a 1600-token chunk of Qwen3.8-27B per TP rank is 150 CTAs on the RTX 8000's 72 SMs, three waves for 2.1 waves of work, and an 800-token half of the TP prefill overlap is 76 CTAs, two waves for 1.06. Each CTA's key range can now be split into whole-tile ranges; the splits write unnormalized fp32 partial outputs with their row maxima and sums, and merge_splits_kernel combines them by log-sum-exp. The launcher picks the split count (up to 4) that finishes the CTAs in the fewest full-CTA times on this device (SMs times the kernel's occupancy); the op takes num_kv_splits, 0 for that choice, for tests and benchmarks. RTX 8000, 12 query and 2 KV heads, head dim 256, CUDA graph, ms per call (without split-KV / automatic): 1600 tokens after 30k 12.40 / 9.71, after 100k 40.42 / 31.75, after 184k 75.59 / 58.47; 801 tokens after 100k 27.08 / 17.14, after 184k 49.65 / 32.07. One forced split keeps the time without split-KV (40.41 at 100k); 1600 after 1600 is 1.28 / 1.08. Tests: tests/kernels/attention/test_paged_prefill_attention_sm75.py, 21 passed on an RTX 8000 (the fp32 reference cases with 0, 1, 2 and 4 splits, including splits without visible keys, and the selection cases). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141pGKkAa2PjDMhrUwfX3ox Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
The prefill path of the Turing NVFP4 linear dequantizes each weight into an fp16 buffer for cuBLAS. Its Triton kernel took 64 groups per program, the fastest tile on Volta, which holds 128 values per thread on Turing, past Triton's register budget there: about 200 GB/s on the RTX 8000. Sweep at the Qwen3.8-27B TP2 shapes per rank (GROUPS_PER_BLOCK 4..64 x 1..8 warps, every configuration bit-identical): Turing runs fastest at 16 groups and 8 warps, ms per call today -> new: 17408 x 5120 1.090 -> 0.496 (460 GB/s), 5120 x 8704 0.580 -> 0.250, 5120 x 2560 0.166 -> 0.075, 8192 x 5120 0.525 -> 0.230. On the V100 64 groups stay the fastest for the large shape (17408 x 5120 0.930 ms), so Volta keeps them; the tile is chosen per device. Tests: tests/kernels/quantization/test_nvfp4_qpn2_dequant.py on an RTX 8000 and a V100, 11 passed each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141pGKkAa2PjDMhrUwfX3ox Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
This was referenced Oct 28, 2026
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.
Update 2026-10-28: rebased onto main c59b3c4, which contains #1227; its two commits are no longer part of this branch, which now holds 14 commits (7 of #1278, 1 of #1216, 6 new). The six are unchanged apart from the regenerated reference (git range-diff). Test Plan files on this head, Quadro RTX 8000 / Tesla V100: test_prefill_overlap.py 14 / 14, test_paged_prefill_attention_sm75.py 21 / 5 passed (16 skipped), test_nvfp4_qpn2_dequant.py 11 / 11 and test_flash_v100_dense_prefill_availability.py 2 / 2 passed; test_gpu_model_runner_v2.py 5 passed with the two failures described below on both cards, which fail the same way on main c59b3c4; test_flash_attn_packed_gqa_verify.py skips as a module as before; tests/distributed/test_host_reduce.py 3 passed on the RTX 8000 pair with NCCL_P2P_DISABLE=1. pre-commit run --from-ref origin/main --to-ref HEAD on this head: every hook passes except check-layering, which now reports vllm/config/speculative.py (model coupling 142 -> 144) next to vllm/model_executor/layers/fused_moe/runner/moe_runner.py (platform coupling 37 -> 39); main c59b3c4 reports the same two counts, so this PR adds none. pre-commit run mypy-3.10 --hook-stage manual: passed.
Purpose
Tensor-parallel prefill on GPUs without peer access is bound by attention and by all-reduces. On two Quadro RTX 8000 (Turing, PCIe, no P2P), an nsys profile of a 150,033-token Qwen3.8-27B NVFP4 prefill with TP=2 (fork build 4da0bcf) spent, per card, 108 s in FA2's attention kernels and 96 s in NCCL all-reduce kernels out of 268 s. NCCL moves the data through host memory; a two-rank FP16 all-reduce of 8,192,000 B reaches 2.08 GB/s there. This PR gives Turing its own prefill attention, which brings that prefill to 208 s (attention 47 s, all-reduce still 96 s). It then hides the all-reduces behind compute: the batch runs as two interleaved halves, the all-reduces go through the copy engines instead of SMs, the attention splits its key range so that the halves fill the GPU, and the NVFP4 weight dequantization gets a Turing tile.
Stacked PR. The branch sits on main c59b3c4 plus two open PRs: #1278 (Turing tensor-core paths, 7 commits) and #1216 (two-rank host all-reduce, 1 commit, which the copy-engine mode extends); #1227 (packed GQA verify for FA2, same attention backend file), on which it was built, is merged. Only the last six commits are new here, so please review those (git log e4127aa..3f48315, where e4127aa is the copy of #1216). The copy of #1278 is identical to the open PR; the copy of #1216 differs from it only in the anchors of the generated docs/design/architecture/runtime_reference.md.
The six commits:
Not a duplicate. I checked against main 899e9ed and all 81 open PRs in four ways:
Findings:
AI assistance was used for this change. I reviewed every line and ran the tests and measurements below.
Test Plan
Native build of this branch in place (paged_prefill_attention_sm75.cu builds for sm_75 only, like fp8_qpn8_mma_sm75.cu of #1278).
One pytest process per file on a Quadro RTX 8000 (SM75) and on a Tesla V100 (SM70):
python -m pytest tests/v1/worker/test_prefill_overlap.py
python -m pytest tests/v1/worker/test_gpu_model_runner_v2.py
python -m pytest tests/kernels/attention/test_paged_prefill_attention_sm75.py
python -m pytest tests/kernels/quantization/test_nvfp4_qpn2_dequant.py
python -m pytest tests/v1/attention/test_flash_v100_dense_prefill_availability.py
python -m pytest tests/kernels/attention/test_flash_attn_packed_gqa_verify.py (#1227's test, same backend file)
On the two Quadro RTX 8000 with peer access off, as on the measured machine:
NCCL_P2P_DISABLE=1 python -m pytest tests/distributed/test_host_reduce.py
pre-commit run --from-ref origin/main --to-ref HEAD; pre-commit run mypy-3.10 --hook-stage manual --from-ref origin/main --to-ref HEAD
Path coverage
Acceleration and benchmark contract (required for performance changes)
Test Result
Model A/B/A/B, commits 3 to 6. Fork builds: reference a1ac0c8, changed f21d1c5 (Purpose). Qwen3.8-27B NVFP4, TP=2 on two Quadro RTX 8000 (PCIe, no P2P).
Contribution of the steps, on the same entry; not A/B. The middle three columns are single boots, each the first boot of its fork build; the outer columns are boot a of the A/B arms. Prefill time in s, lower is better. Columns: reference arm boot a / + overlap (fork build 88d25b8, kernel config without copy_engine_max_bytes) / + copy-engine mode (c181629) / + split-KV (afe87ab) / + NVFP4 tile (changed arm boot a, f21d1c5):
Commit 1 (Turing prefill attention). The model A/B has it in both arms, so its own evidence is:
nsys profiles behind commit 4: single boots, kernel time per call on one RTX 8000, lower is better.
The dequantization work per call does not depend on the prompt length.
That is +7 to +37 % beside NCCL and +4 to +11 % beside the copy engines.
Kernel microbenchmarks, from the commit messages. Time per call, lower is better.
Errors against an fp32 reference equal FA2's in every case.
Every swept configuration is bit-identical. On a V100, 64 groups stay the fastest (17408 x 5120 0.930 ms).
Unit tests on this branch, native build in place (TORCH_CUDA_ARCH_LIST=7.0; _C carries 73 sm_70 and 3 sm_75 cubins), one process per file:
Known failures and skips to read these results against:
pre-commit run --from-ref origin/main --to-ref HEAD: every hook passes except check-layering, which reports vllm/model_executor/layers/fused_moe/runner/moe_runner.py (platform coupling 37 -> 39); this PR does not touch that file and main reports the same. pre-commit run mypy-3.10 --hook-stage manual: passed. The pre-commit CI job fails on main's own run at 899e9ed (run 40532180681) with mypy errors in vllm/v1/kv_cache_interface.py:156-158 and the same check-layering count; none of these files is touched here.
🤖 Generated with Claude Code
https://claude.ai/code/session_0172gtUiymppDzziDqKfy78Q