Skip to content

[Perf][SM75] Turing prefill attention; overlap TP prefill all-reduces on GPUs without P2P - #1279

Open
Peuqui wants to merge 14 commits into
1CatAI:mainfrom
Peuqui:pr-tp-prefill
Open

Peuqui wants to merge 14 commits into
1CatAI:mainfrom
Peuqui:pr-tp-prefill

Conversation

@Peuqui

@Peuqui Peuqui commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

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:

  1. Turing paged prefill attention for head dim 256 (40831e9).
    • FA2's Turing split-KV kernel keeps Q in shared memory next to K and V, so one CTA fills the SM's 64 KB, and without cp.async every tile load stalls the math. On a 2048-token chunk after 150k tokens of context it takes 165.8 ms (about 23 TFLOPS).
    • The new kernel holds 128 GQA-packed query rows per CTA in registers, double-buffers K/V tiles of 32 keys and fetches the next tile in two register-staged halves while the current one computes: 61.5 ms (61 TFLOPS) on the same chunk.
    • FLASH_ATTN takes it on SM75, by the capability of the query's own device, when all of these hold: causal, head dim 256, fp16 query, non-quantized paged cache with pages of 16, 32 or 64 tokens, cache offsets below 2^31, no sliding window, ALiBi, sinks or softcap, no batch invariance, and a request in the batch brings at least attention_config.flash_attn_turing_prefill_min_query_len (65) query tokens. Everything else stays on FA2; below 65 tokens FA2 splits the context and stays faster. The kernel builds for sm_75 only.
  2. Flash-V100 dense prefill availability (9db824d). This fix is independent of the rest and is not part of any measurement here; I can move it to its own PR if preferred (see Serve GLM 5.3 Flash and Qwen3.8 Flash Next GGUF across seven heterogeneous GPUs #1260 below).
    • MLA prefill backend selection calls 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" at the first GLM-5.3-Flash boot.
    • Availability now only loads the entry; execution still binds.
  3. Overlap of TP prefill all-reduces with compute (4ce181c), behind kernel_config.tp_prefill_overlap_min_tokens (0 = off, the default).
    • Eager prefill batches of at least that many tokens, without draft tokens or padding, run as two token-range halves on one compute stream in two threads, using the DBO micro-batch contexts. Each all-reduce (GroupCoordinator._all_reduce_out_place and all_reduce_sum2) runs on a comm stream while the other half computes its layer.
    • A request cut at the split continues in the second half as a following chunk would, so the second half's attention reads the first half's KV writes. In mamba align mode the split moves so that both halves use the recurrent state slot of the chunk end (801 + 799 tokens for a 1600-token chunk of 800-token blocks). Each half builds its attention metadata with its own builder; builder 0 keeps the whole batch's metadata for the speculator.
    • Scope is dense models; only Qwen3.8-27B with TP=2 was measured. Rejected at startup with a ValueError:
      • TP below 2, PP or DP above 1, decode context parallelism, encoder-decoder models, LoRA, KV transfer, PLE offload and mamba cache mode "all";
      • models with FusedMoE layers (their modular kernel asserts outside DBO when prepare/finalize is not async);
      • models with deepstack visual features (deepstack_num_level > 0 in the Qwen3-VL family, _ds_layer_indices in Granite 4 Vision), which sit in a model buffer that each half would read from token 0.
    • Raised at the first overlapped batch instead:
      • the SM70 fused all-reduce + RMSNorm ops raise a RuntimeError, because they would run a collective on the compute stream beside the other half's all-reduce on the comm stream;
      • model inputs other than input_ids, inputs_embeds and positions raise NotImplementedError.
    • An exception in one half now stops the other half at its next yield and is re-raised in the caller. Before, the other thread and the caller waited forever; this also applied to the existing DBO micro-batches, which share the contexts.
    • The field stays out of the compile hash: the compiled graph is the same, called twice.
  4. Copy-engine mode of the host all-reduce (1c2d228), behind kernel_config.host_reduce.copy_engine_max_bytes (0 = off, the default; otherwise larger than max_bytes and a multiple of 16).
    • With the overlap alone, NCCL's SM-driven transfers slowed the kernels running beside them. In the nsys profiles (Test Result) the weight dequantization kernels ran 7 to 37 % longer per call and the largest cuBLAS GEMM 22 % longer per tile row than without overlap. In single boots the overlap alone shortened the prefill by 13 % at 29k, 3 % at 184k and 0.5 % at 250k tokens.
    • The new mode serves FP16 payloads above max_bytes up to copy_engine_max_bytes. The copy engines move this rank's tensor into its host slot and the peer's tensor back in up to 16 pieces, ordered by cuStreamWriteValue32 and cuStreamWaitValue32. One FP16 add then gives the same bits as the kernel mode and NCCL's two-rank sum.
    • It pins four slots of copy_engine_max_bytes in shared host memory per channel.
    • 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 (NCCL) serves them, as without this mode. Decode payloads up to max_bytes (256 KiB by default) stay on the kernel mode of [Kernel] Two-rank FP16 all-reduce through pinned host memory for GPUs without P2P #1216.
  5. Split-KV for the Turing prefill attention (e8f5c6e).
    • One CTA fills a Turing SM, so a 1600-token chunk per TP rank (150 CTAs on 72 SMs) takes three waves for 2.1 waves of work, and an 800-token half (76 CTAs) takes two waves for 1.06.
    • Each CTA's key range can now be split into whole-tile ranges, merged by log-sum-exp. The launcher picks the split count (up to 4) that finishes in the fewest full-CTA times on this device (SMs times the kernel's occupancy).
  6. Turing tile for the NVFP4 prefill dequantization (0d83b1b).
    • The Triton kernel _nvfp4_qpn2_dequant_kernel, which dequantizes NVFP4 weights into fp16 for cuBLAS, took 64 groups per program, Volta's fastest tile. On Turing that reaches about 200 GB/s.
    • Turing now takes 16 groups, 2.2 to 2.3x faster and bit-identical; the tile is chosen per device, and Volta keeps 64.

Not a duplicate. I checked against main 899e9ed and all 81 open PRs in four ways:

  • Full file lists (gh api --paginate repos/1CatAI/1Cat-vLLM/pulls/N/files).
  • The added and removed lines of every open PR, searched for this PR's functions (_all_reduce_out_place, all_reduce_sum2, UBatchContext, make_ubatch_contexts, host_reduce, paged_prefill_attention, nvfp4_qpn2_dequant, get_flash_dense_forward, flash_v100_dense_prefill_lse_available, cuStreamWriteValue).
  • A three-way merge of every open PR with this branch.
  • Searches for all-reduce, allreduce, overlap, host memory, copy engine, P2P, prefill attention, Turing, SM75, split-KV, NVFP4 dequant, micro-batch, DBO, ubatch, Flash-V100 and "bound before execution".

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

Path / provider Format Key conditions (shape, TP/EP, graph, capability) Expected selection / fallback Evidence (source / static / executed / measured) Unverified scope
FLASH_ATTN prefill on SM75 fp16 paged KV (non-quantized), head dim 256, causal a request with at least 65 query tokens; pages of 16/32/64 tokens; cache offsets below 2^31; no window, ALiBi, sinks, softcap or batch invariance; capability of the query's device paged_prefill_attention_sm75 with split-KV by waves; otherwise FA2 executed: test_paged_prefill_attention_sm75.py (fp32 reference with 0/1/2/4 splits; selection keeps pages of 128 and 800 and caches beyond 32-bit offsets on FA2); measured: nsys single boots, model A/B (both arms) other head dims, fp8 KV, SM8x+ (never selected there); CUDA graph capture only in the split-KV microbenchmark
TP prefill overlap dense models (measured: NVFP4) TP >= 2, PP = DP = DCP = 1; eager prefill batch without draft tokens or padding, >= tp_prefill_overlap_min_tokens; mamba cache mode none or align two halves, all-reduces on a comm stream; off by default; startup ValueError for the configurations and models listed in Purpose executed: test_prefill_overlap.py (token ranges, M-RoPE positions, align split, rejections incl. DCP, encoder-decoder, MoE and the deepstack features of Qwen3-VL and Granite 4 Vision, halves on a GPU bit-identical to the whole batch, a failing half stops both); measured: Qwen3.8-27B TP=2 A/B TP > 2; other dense models; multimodal models with token-indexed side buffers other than the deepstack features of Qwen3-VL and Granite 4 Vision; collectives other than all-reduce (for example sequence parallelism) are not routed
host all-reduce, copy-engine mode FP16 two ranks without P2P; payload above max_bytes and up to copy_engine_max_bytes; not capturing copy engines + stream memory operations; while capturing, NCCL; off by default executed: test_host_reduce.py on the RTX pair (bit-identical to NCCL for 0.3 to 21 MB, back to back on a side stream beside GEMMs, mixed with the kernel mode; captured 655,360 B payload rejected without failing the capture; copy_engine_max_bytes at or below max_bytes rejected by the config); measured: nsys route hit, model A/B more than two ranks (two-rank only, like #1216); devices without stream memory operations (not probed when the channel opens)
NVFP4 prefill dequantization NVFP4 (QPN2) SM75 16 groups per program; SM70 keeps 64 executed: test_nvfp4_qpn2_dequant.py on RTX 8000 and V100; measured: model A/B none
Flash-V100 dense prefill availability MLA model build before runtime binding availability loads without binding; execution binds executed: test_flash_v100_dense_prefill_availability.py no model run on this branch

Acceleration and benchmark contract (required for performance changes)

  • Default enabled or opt-in:
    • On by default on the matching capability, per device: the Turing prefill attention (attention_config.flash_attn_turing_prefill_min_query_len = 65; 0 disables it), its split-KV choice and the NVFP4 tile.
    • Opt-in, 0 by default: the overlap (kernel_config.tp_prefill_overlap_min_tokens) and the copy-engine mode (kernel_config.host_reduce.copy_engine_max_bytes).
  • Required CLI options and environment switches:
    • Changed arm of the model A/B: --kernel-config '{"triton_sm7x_tensor_cores":true,"tp_prefill_overlap_min_tokens":1024,"host_reduce":{"copy_engine_max_bytes":20971520}}'. Reference arm: --kernel-config '{"triton_sm7x_tensor_cores":true}'.
    • triton_sm7x_tensor_cores belongs to a later PR of this series (Triton tensor-core routes) and does not exist on this branch. Here the switches are '{"tp_prefill_overlap_min_tokens":1024,"host_reduce":{"copy_engine_max_bytes":20971520}}'.
    • Entry: Qwen3.8-27B NVFP4, TP=2 on the two Quadro RTX 8000, --dtype float16, --block-size 16, --max-num-batched-tokens 2048, --max-model-len 262144, --enable-prefix-caching, --disable-custom-all-reduce, MTP with 3 speculative tokens, cudagraph_capture_sizes [1, 2, 4, 8], VLLM_USE_V2_MODEL_RUNNER=1, NCCL_P2P_DISABLE=1.
  • KV cache dtype used for the benchmark: auto (fp16).
  • Wheel SHA or source commit:
  • PYTHONPATH, source overlays, or external native libraries used:
    • Both arms of the model A/B ran with a patched Triton 3.6 build (tensor cores on sm_70 and sm_75, from a later PR of this series) first on PYTHONPATH. The changed arm also had its fork source tree on PYTHONPATH and ran on copies of the reference arm's compile caches; the overlap leaves the compiled graph unchanged.
    • Commit 6 changes the tile of a Triton kernel. Its sweep numbers come from the commit message, and which Triton build the sweep ran on was not recorded.
    • The commit 1 profiles ran the environment's stock Triton 3.6.0. Unit tests: in-place build, stock Triton 3.6.0, no overlays.
  • User-entry route-hit, speed, and output-quality evidence:
    • The changed boots logged "TP prefill overlap: prefill batches of at least 1024 tokens run as two interleaved halves." Their engine config showed host_reduce copy_engine_max_bytes=20971520 and tp_prefill_overlap_min_tokens=1024.
    • In the nsys profiles the Turing attention replaced FA2's prefill kernels, and the copy-engine mode replaced almost all NCCL kernels (Test Result).
    • Speed and quality: Test Result.
  • Existing kernel/backend registry and capability-rejection reasons:
    • The Turing prefill attention is chosen inside FLASH_ATTN's forward (FlashAttentionImpl._use_turing_prefill).
    • The host all-reduce's rejection_reason gains copy_engine_mode_not_capturable; a rejected payload goes to the next all-reduce in the chain.
    • The overlap rejects unsupported configurations and models at startup with a ValueError naming the field.
  • Per-engine configuration and deprecated-variable compatibility:
    • Three new fields: attention_config.flash_attn_turing_prefill_min_query_len, kernel_config.tp_prefill_overlap_min_tokens and kernel_config.host_reduce.copy_engine_max_bytes. No environment variables.
    • tp_prefill_overlap_min_tokens and host_reduce are outside the compile hash.
  • New/changed environment metadata and generated reference check: no environment metadata. docs/design/architecture/runtime_reference.md is regenerated (HostReduceConfig post_init, KernelConfig and AttentionConfig anchors) and checked by the check-architecture-reference hook (Test Result).
  • Route snapshot command and intentional changed rows: none. No selector registry changes: the Turing attention is chosen inside the FLASH_ATTN forward and the NVFP4 tile inside the dequantization op.
  • SM70 registered variables / unregistered reads / model-parameter restrictions, before -> after: no change.

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).

  • Order: one uncounted cold boot of the changed build, then reference a, changed a, reference b, changed b; two counted boots per arm.
  • Prefill time in s, lower is better, and throughput in tok/s, higher is better (reference a / changed a / reference b / changed b):
    • four prompts of 29,167 to 29,243 tokens per boot: 33.0-33.1 / 19.1-19.3 / 33.0-33.1 / 19.1-19.3 s, -41.9 % in the mean (881-886 -> 1,515-1,528 tok/s)
    • 184,134 tokens: 269.05 / 174.34 / 269.21 / 174.38 s, -35.2 % (684 -> 1,056 tok/s)
    • 249,992 tokens: 402.27 / 269.06 / 402.47 / 269.29 s, -33.1 % (621-622 -> 928-929 tok/s)
  • Decode unchanged. tok/s, higher is better, reference -> changed:
    • short prompts: prose 74.6-75.0 -> 74.8-75.2, code 114.3-115.1 -> 115.4-115.9, edit 119.1-119.9 -> 119.8-120.1, at 33-34 ms per step in both;
    • MTP acceptance per round equal (2.47 / 3.82 / 3.99);
    • at 184,134 tokens of context 54.3-54.4 ms per step (50.6-50.7 tok/s) -> 54.3 ms (50.7 tok/s);
    • at 249,992 tokens 62.3 ms (40.8 tok/s) -> 62.2-62.4 ms (41.0-41.1 tok/s).
  • Output:
    • Greedy output of three fixed prompts was identical to the stored reference in all five boots.
    • Eight quality answers per boot, compared byte for byte: seven are identical between the arms. The eighth (a speed calculation) is worded differently from character 909 on; both versions follow the same correct derivation and stop at the 600-token limit. Each arm is byte-identical across its two boots.
    • Outputs of the long prompts were not compared text by text.
  • Memory: peak memory.used per RTX 8000 during the measurements was changed 48,285 / reference 47,979 MiB (+306 MiB), and 47,867 MiB in both during boot and greedy. The startup memory profile runs whole dummy batches, never the halves, so it does not see the overlap's extra allocations: the KV cache was 1,038,640 tokens in both arms.

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):

  • 29k: 33.0-33.1 / 28.5-29.0 / 21.7-21.9 / 21.1-21.2 / 19.1-19.3
  • 184k: 269.05 / 260.28 / 216.81 / 185.25 / 174.34
  • 250k: 402.27 / 400.14 / 341.66 / 283.50 / 269.06

Commit 1 (Turing prefill attention). The model A/B has it in both arms, so its own evidence is:

  • Two nsys single boots on the same entry, not A/B. Fork builds: reference 4da0bcf, changed f9ac81b.
    • f9ac81b also carries eight further commits: MoE and DeepSeek-V4 kernels, the M <= 8 decode split-K, and a native sm_75 build of the SM70 kernel family, which [Perf][SM75] Turing tensor-core paths for block-FP8 QPN8 and the grouped skinny MoE; bound the MoE grid #1278 measured without gain on this model. The kernel times below put the difference in the attention.
    • Prefill of 150,033 tokens took 267.91 -> 207.87 s (lower is better).
    • Kernel time per card: FA2 attention 108.0-108.1 s (1,995 calls) -> FA2 0.1 s plus paged_prefill_attention 46.8-47.0 s (1,615 calls); NCCL 95.7 -> 96.3-96.4 s; all other kernels 63.6-63.8 -> 64.0 s.
  • Output quality in a bundled A/B/A/B. Fork builds: reference 79e07bf, changed a1ac0c8. The changed build has 21 more commits, including commit 1, and a patched Triton; its speed cannot be attributed to commit 1 and is not reported here.
    • Greedy output and all eight quality answers were byte-identical between the arms in all counted boots.
    • The greedy prompts are 18 to 52 tokens long, below the 65-token threshold. The quality prompts are 65 to 132 tokens with the chat template; whether prefix caching kept some of them below the threshold was not traced.
  • Unit test against an fp32 reference (Test Plan).

nsys profiles behind commit 4: single boots, kernel time per call on one RTX 8000, lower is better.

  • Builds:
    • without overlap: fork build a1ac0c8, 150k-token prompt;
    • overlap with NCCL: fork build 88d25b8, 100k-token prompt, prefill 118.22 s;
    • overlap with the copy-engine mode: fork build c181629, 100k-token prompt, prefill 95.85 s.
      The dequantization work per call does not depend on the prompt length.
  • Dequantization kernels per call, without overlap / with NCCL / with the copy-engine mode, in us:
    • NVFP4 1081.4 / 1412.0 / 1138.3 and 569.9 / 611.2 / 631.7;
    • FP8 285.0 / 389.9 / 316.1, 280.4 / 377.4 / 291.0 and 118.0 / 139.5 / 123.4.
      That is +7 to +37 % beside NCCL and +4 to +11 % beside the copy engines.
  • The largest cuBLAS GEMM tile per tile row: +21.8 % beside NCCL, +0.3 % beside the copy engines.
  • Route hit of the copy-engine mode: NCCL kernels per card 16,832 (75.6 s) -> 448 (0.3 s); 260,166 host-to-device and 260,164 device-to-host copies of 135 GB each, and 16,384 FP16 add kernels.

Kernel microbenchmarks, from the commit messages. Time per call, lower is better.

  • Turing prefill attention (40831e9), Quadro RTX 8000, Qwen3.8-27B per-rank shapes (12 query heads, 2 KV heads), causal chunk at the end of the context; whether the timing used CUDA graphs is not recorded. FA2 -> new kernel:
    • 2048 tokens after 32k context 35.3 -> 13.1 ms; after 150k 165.8 -> 61.5 ms (about 23 -> 61 TFLOPS);
    • 512 tokens after 150k 52.5 -> 20.0 ms;
    • 96 tokens after 150k 24.6 -> 19.8 ms;
    • 64 tokens after 150k 5.3 ms (FA2) against 19.9 ms, hence the threshold of 65.
      Errors against an fp32 reference equal FA2's in every case.
  • Split-KV (e8f5c6e), RTX 8000, same heads, captured in CUDA graphs. Without split-KV (the kernel of commit 1) / with the automatic split:
    • 1600 tokens after 30k 12.40 / 9.71 ms, after 100k 40.42 / 31.75 ms, after 184k 75.59 / 58.47 ms;
    • 801 tokens after 100k 27.08 / 17.14 ms, after 184k 49.65 / 32.07 ms;
    • 1600 tokens after 1600 1.28 / 1.08 ms;
    • one forced split 40.41 ms at 100k, the time without split-KV.
  • Copy-engine all-reduce (1c2d228), RTX 8000 pair, 8,192,000 B per call (800 tokens x 5120 x 2 B), eager calls (the mode refuses capture):
    • 3.15 ms (2.60 GB/s) against NCCL's 3.93 ms (2.08 GB/s);
    • a GEMM loop running beside it changed its time by -2.8 % against +18.2 % beside NCCL, and a memory-bound loop by +1.8 % against +6.0 %.
  • NVFP4 dequantization (0d83b1b), RTX 8000, Qwen3.8-27B TP2 shapes per rank; whether the timing used CUDA graphs is not recorded. 64 -> 16 groups per program:
    • 17408 x 5120 1.090 -> 0.496 ms (460 GB/s);
    • 5120 x 8704 0.580 -> 0.250 ms;
    • 5120 x 2560 0.166 -> 0.075 ms;
    • 8192 x 5120 0.525 -> 0.230 ms.
      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:

  • Quadro RTX 8000: test_prefill_overlap.py 14 passed; test_gpu_model_runner_v2.py 5 passed, 2 failed; test_paged_prefill_attention_sm75.py 21 passed; test_nvfp4_qpn2_dequant.py 11 passed; test_flash_v100_dense_prefill_availability.py 2 passed; test_flash_attn_packed_gqa_verify.py 1 skipped.
  • Tesla V100: test_prefill_overlap.py 14 passed; test_gpu_model_runner_v2.py 5 passed, 2 failed; test_paged_prefill_attention_sm75.py 5 passed, 16 skipped (the kernel cases need SM75; the selection cases run); test_nvfp4_qpn2_dequant.py 11 passed; test_flash_v100_dense_prefill_availability.py 2 passed; test_flash_attn_packed_gqa_verify.py 1 skipped.
  • Two Quadro RTX 8000 with NCCL_P2P_DISABLE=1: test_host_reduce.py 3 passed.

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

Peuqui and others added 14 commits October 28, 2026 06:24
….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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant