Skip to content

[Perf] per_token_group_quant_8bit: multiply by reciprocal instead of dividing per element - #61343

Merged
gau-nernst merged 4 commits into
vllm-project:mainfrom
gau-nernst:gau/fp8-quant-fastdiv
Oct 13, 2026
Merged

gau-nernst merged 4 commits into
vllm-project:mainfrom
gau-nernst:gau/fp8-quant-fastdiv

Conversation

@gau-nernst

@gau-nernst gau-nernst commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Speed up FP8 per-token-group quantization by computing the per-group scale reciprocal once and multiplying, instead of dividing per element. This covers both CUDA group-quant kernels: per_token_group_quant_8bit (smem and register-resident schedules) and the fused RMSNorm+group-quant kernel (layernorm_utils.cuh), so that all CUDA fp8 group-quant paths share the same reciprocal-multiply numerics. ~1.2-1.3x kernel speedup on GB300 across M=1..32768; ~1-2% e2e TPOT improvement on Qwen3.5-397B-A17B-FP8 (TP4).

Claims

  • 1.15-1.29x faster per_token_group_fp8_quant CUDA kernel on GB300 across M=1..32768 (details below).
  • All CUDA fp8 group-quant paths (unfused kernel, fused RMSNorm+quant kernel, Triton fallback, UE8M0 path) now use the same reciprocal-multiply numerics; previously the fused and unfused kernels could disagree by 1 fp8 ulp on ~0.05% of elements.
  • ~0.6-2.0% e2e TPOT improvement on Qwen3.5-397B-A17B-FP8, TP4, 8k input / 1k output, at concurrency 1-64.
  • Accuracy-neutral: GSM8K / GPQA-Diamond deltas within noise (details below).

Validation

Correctness

  • pytest tests/kernels/quantization/test_per_token_group_quant.py: 848 passed, including test_per_token_group_quant_fp8_register_matches_smem (bitwise register-vs-smem equality; both schedules are changed identically, so the invariant is preserved).
  • Scale outputs are bit-identical to main; quantized values can differ by 1 fp8 ulp on ~0.05% of elements (randn data) when a quotient lands within 1 fp32 ulp of an fp8 rounding boundary.

Microbenchmarks

GB300, K=4096, group=128, bf16 -> fp8e4m3 + fp32 scales. FlashInfer CUPTI with CUDA graph and cold L2; median of interleaved rounds. Both columns are standalone same-toolchain builds (nvcc 13.1) of the register-resident kernel; repro harness: benchmarks/kernels/benchmark_per_token_group_quant.py.

M main (us) this PR (us) speedup achieved BW (this PR)
1 2.30 1.92 1.20x 6 GB/s
4 2.34 1.92 1.22x 26 GB/s
16 2.56 2.14 1.20x 93 GB/s
64 2.69 2.27 1.19x 350 GB/s
256 3.20 2.78 1.15x 1.14 TB/s
1024 6.14 5.02 1.22x 2.53 TB/s
4096 15.23 12.19 1.25x 4.17 TB/s
16384 52.35 41.09 1.27x 4.95 TB/s
32768 101.25 78.34 1.29x 5.19 TB/s

The kernel is memory-bound (~2 FLOPs/element; TFLOPS is not the meaningful metric here). This PR lifts achieved bandwidth from 4.0 to 5.2 TB/s at M=32768. A flat-index Triton variant we prototyped reaches 6.7 TB/s at M=32768 but loses at small M; modifying the existing CUDA kernel is the lower-risk change.

E2E perf

Qwen3.5-397B-A17B-FP8, TP4, 8k input / 1k output, vllm bench serve; median TPOT. main (c077dc663d) and the branch (1aca0a263f) ran sequentially on the same GB300 node:

Concurrency main (ms) this PR (ms) Change
1 6.27 6.23 -0.6%
4 8.30 8.14 -1.9%
16 12.06 11.82 -2.0%
64 24.98 24.67 -1.2%

Request throughput +1.2% at concurrency 64. Note: the e2e delta includes a toolchain effect (branch built from source with local nvcc 13.1; baseline is the nvcc 13.0 wheel, whose stock quant kernel is mis-scheduled -- see Details), so it mildly overstates the pure source-change effect. The same-toolchain kernel numbers above are the honest claim.

Accuracy

Qwen3.5-397B-A17B-FP8, TP4, identical server flags on the same GB300 node:

Task main this PR delta
GSM8K flexible-extract (n=1319) 0.8211 0.8112 -0.99pp
GSM8K strict-match (n=1319) 0.8158 0.7991 -1.67pp
GPQA-Diamond (n=198) 0.8586 0.8687 +1.01pp

Mixed and within noise (binomial std error ~1.06pp per arm at n=1319), but GSM8K drifts down directionally; consistent with the ~0.05% of elements whose fp8 rounding flips under reciprocal-multiply.

Details

  • Root cause: div.rn.f32 is a multi-instruction slow sequence, and the kernel issued it once per element when one division per group suffices. The kernel's own UE8M0 path already uses reciprocal-multiply (a power-of-two scale makes the reciprocal exact); this PR applies the same transform to the fp32-scale path.
  • The per-element division was inherited from the SGLang kernel this file was ported from ([Perf] Cuda Kernel for Per Token Group Quant #21083); the register-resident path ([Kernel][Perf] Register-resident path for per-token-group 8-bit quant #55330) kept it so the new schedule stayed bit-exact with the smem one. That invariant is a test convenience, not an accuracy requirement, and is preserved by changing both schedules identically.
  • Numerics: this is the same rcp-mul numerics class already shipped by this kernel's UE8M0 path, vLLM's Triton fallback (_per_token_group_quant_fp8), and SGLang's production quant kernel.
  • Scope: no Python/dispatch changes. The file is also hipified into ROCm builds; the change uses only portable float ops, so ROCm gets the same numerics (not separately benchmarked there).
  • Related work: [Perf][Kernel][Quantization] Triton row kernel for per-token-group FP8 quant at large token counts #60903 adds a separate Triton row kernel for prefill-sized inputs (large M only); this PR instead speeds up the existing CUDA kernel at all M and is complementary. No open PR duplicates this change.
  • Consistency: the fused RMSNorm+group-quant kernel (layernorm_utils.cuh, the target of RMSNormQuantFusionPass for block-FP8 models, on both CUDA and ROCm via hipify) is switched to the same reciprocal-multiply. Without this, fused and unfused paths diverge by 1 fp8 ulp on ~0.05% of elements (caught by test_fused_quant_layernorm and test_mamba_prefix_cache). The fused kernel also drops 4 div.rn per vec4. Per-token fp8 keeps exact per-element division for FBGemm parity; int8 paths are unchanged.
Toolchain note: nvcc 13.0 x sm_100 mis-schedules the stock kernel

All official artifacts (release pipeline nvcc 13.0.2, Dockerfile CUDA_VERSION=13.0.3) target ... 10.0 ... (no sm_103). nvcc 13.0 targeting sm_100 mis-schedules this kernel's div.rn slow paths: 864 instructions / 3095 static stall cycles vs 568 / 1655 under nvcc 13.1; the shipped wheel runs the stock kernel at 167.6us vs 101.2us for a 13.1 rebuild at M=32768 (sm_100 SASS loads natively on sm_103; verified with CUDA_DISABLE_PTX_JIT=1). This PR's variant is insensitive to the compiler (78.3us on nvcc 13.0 vs 78.1us on 13.1) because it removes the per-element div.rn entirely. Worth a separate build-toolchain discussion; out of scope here.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing

AI assistance: Kimi Code helped develop, benchmark, and validate this change.

@mergify mergify Bot added the quantization label Oct 13, 2026
@gau-nernst
gau-nernst force-pushed the gau/fp8-quant-fastdiv branch from b02d17b to c04f719 Compare October 13, 2026 05:44
…dividing per element

Both the smem and register-resident schedules now scale with v * (1/y_s),
computing one div.rn per 128-element group instead of one per element.
Quantization is division-throughput-bound at large M; this is ~1.25x on
GB300 (e.g. 101 -> 76 us at M=32768, K=4096; 2.34 -> 1.95 us at M=1).

Numerics: scales are unchanged (bit-exact). The quantized values can differ
by 1 fp8 ulp on ~0.05% of elements (reciprocal rounding vs division) -- the
same class of numerics the UE8M0 path in this kernel and SGLang's production
quant kernel already use. The two schedules use identical math so the
bitwise register-vs-smem invariant (test_per_token_group_quant_fp8_register_
matches_smem) is preserved; UE8M0 behavior is unchanged (reciprocal of a
power of two is exact).

Co-authored-by: Kimi Code
Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>
@gau-nernst
gau-nernst force-pushed the gau/fp8-quant-fastdiv branch from c04f719 to 1aca0a2 Compare October 13, 2026 05:46
@gau-nernst
gau-nernst marked this pull request as ready for review October 13, 2026 05:48

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@gau-nernst

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #94388 for commit 18d719dfe482.

@vllm-agent

vllm-agent commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

CI selector (shadow): 41 test steps (80 jobs) instead of 195 (271 jobs)

Shadow mode: this changes nothing about what CI runs. It shows what the evidence-based selector would pick for this PR, next to today's rules. How it works.

Feedback welcome: reply here if it would skip a step this change needs, or runs something unrelated.

steps (jobs) Today's rules Selector Would skip Would add
NVIDIA, CPU and others 195 (271) 41 (80) 154 (191) 0 (0)
AMD mirrors 163 (227) 163 (227) 0 (0) 0 (0)

Today's rules run everything for this diff.

Selector would run (41)
  • amd-fp8-moe-kernels-mi355
  • amd-kernels-mi355
  • amd-lm-eval-small-models-harness
  • amd-native-quantization-kernels-mi355
  • amd-qwen3-next-mtp-async-eplb-accuracy
  • arm-cpu-test ×3
  • basic-models-tests-extra-initialization ×14
  • basic-models-tests-initialization
  • batch-invariance-b200
  • batch-invariance-h100
  • cpu-kernel-tests ×2
  • dsv41-mono-decode-numerics-amd
  • e2e-core-1-gpu
  • e2e-core-large-memory
  • fusion-and-compile-unit-tests-2xb200
  • fusion-e2e-quick-h100
  • fusion-e2e-tp2-ar-rms-dynamo-partition-amd
  • fusion-e2e-tp2-ar-rms-inductor-partition-amd
  • fusion-e2e-tp2-b200
  • fusion-e2e-tp2-quick-h100
  • kernels-b200 ×3
  • kernels-core-operation-test ×3
  • kernels-deepgemm-test-h100
  • kernels-fusedmoe-layer-test-2-b200s
  • kernels-fusedmoe-layer-test-2-h100s
  • kernels-moe-test ×5
  • kernels-quantization-test ×6
  • kernels-root-misc-test-b200
  • kimi-k3-prefix-cache-4xb200 ×2
  • kv-offload-large
  • lm-eval-dspark-watermark-2xh100
  • lm-eval-small-models
  • pytorch-compilation-passes-unit-tests
  • pytorch-compilation-unit-tests-h100
  • pytorch-fullgraph-test
  • quantization ×4
  • quantized-moe-test-b200
  • spec-decode-draft-model ×4
  • spec-decode-eagle-1-deepseek-qwen
  • spec-decode-mtp-deepseek-mimo
  • v1-kv-connectors ×4
Would skip (today's rules run them) (154)
  • ascend-npu-test
  • async-engine-inputs-utils-worker
  • basic-correctness ×2
  • basic-correctness-cpu-offload
  • basic-correctness-cumem
  • basic-correctness-prefetch-offload
  • basic-correctness-sleep-mode
  • basic-models-test-other-cpu
  • basic-models-tests-other
  • benchmarks-cli-test
  • cpu-distributed-tests-dp-tp
  • cpu-distributed-tests-pp-tp
  • cpu-language-generation-and-pooling-model-tests ×3
  • cpu-multi-modal-model-tests-n ×4
  • cpu-multimodal-config
  • cpu-params-env-tokenizers-parser
  • cpu-quantization-model-tests
  • cpu-qwen2-5-vl-multimodal-tests
  • cpu-reasoning-renderers
  • cpu-spec-decode-tests
  • cpu-tool-parsers
  • crcr-report
  • crosslayer-kv-layout-distributed-nixlconnector-pd-accuracy-tests-4-gpus
  • cudagraph
  • deepseek-v4-kernel-test-b200
  • deepseek-v4-kernel-test-h100
  • distributed-comm-ops
  • distributed-compile-comm-4-gpus
  • distributed-compile-rpc-tests-2-gpus
  • distributed-compile-unit-tests-2xh100
  • distributed-dp-tests-2-gpus
  • distributed-dp-tests-4-gpus
  • distributed-flashinfer-nixlconnector-pd-accuracy-4-gpus
  • distributed-model-tests-2-gpus ×3
  • distributed-mooncakeconnector-pd-accuracy-4-gpus
  • distributed-nixlconnector-pd-accuracy-4-gpus
  • distributed-torchrun-examples-4-gpus
  • distributed-torchrun-shutdown-tests-2-gpus
  • docker-build-metadata
  • dp-ep-distributed-nixlconnector-pd-accuracy-tests-4-gpus
  • e2e-scheduling-1-gpu
  • e2e-scheduling-accuracy-1-gpu
  • elastic-ep-scaling-test
  • engine
  • engine-1-gpu
  • entrypoints-integration-api-server ×4
  • entrypoints-integration-api-server-generate
  • entrypoints-integration-api-server-openai-chat_completion
  • entrypoints-integration-api-server-openai-completion
  • entrypoints-integration-llm
  • entrypoints-integration-multimodal
  • entrypoints-integration-pooling
  • entrypoints-integration-responses-api
  • entrypoints-integration-speech_to_text
  • entrypoints-unit-tests
  • eplb-algorithm
  • eplb-execution
  • examples
  • extract-hidden-states-integration
  • extract-hidden-states-integration-2-gpus
  • fault-tolerance-e2e-2xh100
  • fusion-e2e-config-sweep-h100
  • fusion-e2e-tp2-ar-rms-config-sweep-h100
  • gemm-rs-ar-2xb200
  • glm5next-unit-tests
  • hybrid-ssm-nixlconnector-pd-accuracy-tests-4-gpus
  • hybrid-ssm-nixlconnector-pd-prefix-cache-2-gpus
  • inkling-unit-tests-b200
  • intel-hpu-test
  • jit-monitor-no-runtime-jit
  • kernels-attention-diffkv-test-h100
  • kernels-attention-test ×7
  • kernels-fla-ops-test-b200
  • kernels-flashmla-test-h100
  • kernels-helion-test ×5
  • kernels-mamba-test
  • kernels-mhc-test-b200
  • kernels-minimax-m3-attention-b200
  • kernels-minimax-reduce-rms-test-2-gpus
  • kimi-k3-unit-tests-b200
  • kv-offload-medium
  • kv-offload-small
  • language-models-tests-extra-standard ×2
  • language-models-tests-granite-l4-compatibility
  • language-models-tests-hybrid ×2
  • language-models-tests-standard
  • lm-eval-turboquant-k3v4nc
  • lm-eval-turboquant-k8v4
  • lm-eval-turboquant-t3nc
  • lm-eval-turboquant-t4nc
  • lm-eval-watermarking
  • lora ×4
  • lora-tp-distributed ×4
  • metrics-tracing-2-gpus
  • model-executor
  • mooncake-ec-tcp-e2e-2-gpus
  • mrcr-eval-small-models
  • multi-modal-accuracy-eval-small-models
  • multi-modal-models-standard-1-qwen2
  • multi-modal-models-standard-2-qwen3-gemma
  • multi-modal-models-standard-3-llava-qwen2-vl
  • multi-modal-models-standard-4-other-whisper
  • multi-modal-processor ×4
  • multi-modal-processor-cpu ×4
  • multiconnector-nixl-offloading-pd-accuracy-2-gpus
  • multiconnector-nixl-offloading-pd-edge-cases-2-gpus
  • nixlconnector-pd-edge-cases-2-gpus
  • openai-api-correctness
  • pipeline-context-parallelism-4-gpus
  • platform-tests
  • plugin-tests-2-gpus
  • push-nixlconnector-pp-prefill-pd-accuracy-4-gpus
  • pytorch-compilation-dynamic-shapes
  • pytorch-compilation-unit-tests
  • pytorch-fullgraph-cudagraph-l4-compatibility
  • pytorch-nightly-dependency-override-check
  • quantized-fusions
  • quantized-models-test
  • qwen4-exp-sp-tests-4-gpus
  • qwen4-exp-unit-tests
  • qwen4-exp-unit-tests-cpu
  • ray-dependency-compatibility-check
  • rayexecutorv2-4-gpus
  • regression
  • replayssm-e2e
  • rl-entrypoints-tests
  • rust-frontend-cargo-style-clippy
  • rust-frontend-cargo-tests
  • rust-frontend-core-correctness
  • rust-frontend-distributed
  • rust-frontend-openai-coverage
  • rust-frontend-serve-admin-coverage
  • rust-frontend-tool-use
  • samplers-multimodal-beam-search
  • samplers-test
  • scale-out-ec-e2e-2-gpus
  • sharded-rdt-weight-transfer
  • spec-decode-eagle-2-llama3-qwen-vl-other
  • spec-decode-mtp-gemma4
  • spec-decode-mtp-qwen3-5
  • spec-decode-ngram-suffix
  • spec-decode-speculators
  • torch-stable-abi-audit
  • v1-attention-b200 ×2
  • v1-attention-h100-mi300 ×2
  • v1-core
  • v1-executor-worker
  • v1-kv-offload
  • v1-logits-oracle
  • v1-metrics-lmeval
  • v1-others-cpu
  • v1-sample
  • v1-spec-decode
  • vllm-ir-tests
Would add (today's rules do not run them) (0)

none

AMD mirrors: would skip (0)

none

AMD mirrors: would add (0)

none

2 changed files · base 380638afbb · head 6669cace62 · Python record: build 93039 at 1e5d0ea888 · kernel record: table 86b1c2e (build 94391), map 86b1c2e · not counted: 11 build steps, 5 A100 steps the generator no longer emits, 98 optional steps the selector would also run

The fp8 group path divided per element while per_token_group_quant_8bit
now multiplies by the reciprocal; the divergence broke
test_fused_quant_layernorm's strict outlier budget and mixes numerics
when a model uses both paths. Align on reciprocal-multiply via
ScaledQuant<..., is_scale_inverted=true> (also drops 4 div.rn per vec4).

Co-authored-by: Kimi Code
Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>
@gau-nernst
gau-nernst force-pushed the gau/fp8-quant-fastdiv branch from 6669cac to 3995312 Compare October 13, 2026 08:27
@gau-nernst

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #94421 for commit 0f294c3bb7a3.

@gau-nernst
gau-nernst merged commit 0fae39a into vllm-project:main Oct 13, 2026
480 checks passed
@gau-nernst
gau-nernst deleted the gau/fp8-quant-fastdiv branch October 13, 2026 13:37
AndreasKaratzas pushed a commit to AndreasKaratzas/vllm that referenced this pull request Oct 16, 2026
…dividing per element (vllm-project#61343)

Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>

Signed-off-by: Andreas Karatzas <AndreasKaratzas@users.noreply.github.com>
michael-gpu pushed a commit to practical-inference/vllm-ampere-extended that referenced this pull request Oct 20, 2026
…dividing per element (vllm-project#61343)

Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>
faresobeid added a commit to PrimeIntellect-ai/prime-rl that referenced this pull request Nov 3, 2026
…LM 0.33

vllm-project/vllm#61343 quantizes per-token groups as x * (1/s) instead of x / s,
so the trainer's Triton casts (meant to be bit-identical to vLLM's) differed in
0.03-0.08% of fp8 values. Multiply by the exact reciprocal (div_rn(1, s)) on the
non-ue8m0 path; test_fp8_utils passes again (25/25 on an RTX PRO 6000).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants