Skip to content

[Kernel][Fusion] Fuse GELU-tanh+Mul with dynamic per-token FP8 quantization - #64212

Open
subhashpolisetti wants to merge 1 commit into
vllm-project:mainfrom
subhashpolisetti:feat-gelu-fp8-fusion-v3
Open

subhashpolisetti wants to merge 1 commit into
vllm-project:mainfrom
subhashpolisetti:feat-gelu-fp8-fusion-v3

Conversation

@subhashpolisetti

@subhashpolisetti subhashpolisetti commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Adds a CUDA kernel, gelu_tanh_and_mul_per_token_quant, and the (GeluAndMul, per-token dynamic FP8) row in the manual activation+quant fusion registry, as part of the manual fusion port (#43501, #43224). When GELU runs as the CUDA gelu_and_mul op, Gemma3 and Gemma 4 FP8-dynamic MLPs produce the down projection's input in one kernel instead of gelu_tanh_and_mul followed by dynamic_per_token_scaled_fp8_quant.

Claims

  • Output is unchanged: byte-identical to the two kernels it replaces on every call of real Gemma3 and Gemma 4 generations, on L4, H100 and B200.
  • On the CUDA-op path, MLP activation+quant is 1.41-1.79× faster. On Gemma-4-31B (H100) it is 1.47× faster than main and 1.36× faster than the compiled kernel.
  • bs32 latency improves 1.46% on Gemma-4-31B (H100), and 3.35% (L4), 1.63% (H100) and 2.71% (B200) on Gemma3-1B.
  • The torch.compile default path is unchanged. The row only replaces the CUDA gelu_and_mul op.

Validation

CUDA 13.0, torch 2.14, bf16, --kernel-config.linear_backend=cutlass, RedHatAI/gemma-*-it-FP8-dynamic.

pytest tests/kernels/test_fused_quant_activation.py                    # 173 passed
pytest tests/compile/passes/test_silu_mul_quant_manual_fusion.py       # 38 passed, 12 skipped (50 passed on B200)
pytest tests/quantization/test_compressed_tensors.py -k gemma3_dynamic_fp8_gelu_fusion  # 2 passed
pytest tests/compile/passes/test_functionalization.py                  # 14 passed

H100 ran these on this commit's base, 5d55ef1590. Generation also passes with VLLM_GPU_SYNC_CHECK=error, with CUDA custom ops and with --enforce-eager, on Gemma3-1B and Gemma-4-31B.

Exactness. During a real generation (128 gsm8k prompts × 128 tokens), every call also ran the two original kernels on the same input, and FP8 values and scales were compared byte for byte:

GPU model calls token rows differences
L4 Gemma3-1B 3,380 852,228 0
L4 Gemma3-4B 4,420 1,140,054 0
H100 Gemma3-1B 3,380 1,494,220 0
B200 Gemma3-1B 3,432 1,501,396 0
H100 Gemma-4-31B 7,800 3,478,740 0

On Gemma-4-31B, greedy token IDs also match with and without the row on 5 prompts (H100, CUDA ops).

End to end, CUDA ops, main → this PR, mean of 2 runs in main-PR-PR-main order:

GPU, model bs1 latency (s) bs32 latency (s) throughput (tok/s) compiled default (bs1 / bs32 / tok/s)
L4, Gemma3-1B 0.9721 → 0.9653 (+0.70%) 2.4744 → 2.3915 (+3.35%) 12853 → 13231 (+2.94%) 0.9628 / 2.3807 / 13300
L4, Gemma3-4B 2.8926 → 2.8881 (+0.16%) 9.0574 → 8.9037 (+1.70%) 3499 → 3558 (+1.71%) 2.8808 / 8.8920 / 3569
H100, Gemma3-1B 0.2839 → 0.2776 (+2.20%) 0.4207 → 0.4139 (+1.63%) 97945 → 99990 (+2.09%) 0.2601 / 0.3887 / 98275
B200, Gemma3-1B 0.2418 → 0.2372 (+1.92%) 0.3241 → 0.3153 (+2.71%) 142050 → 146751 (+3.31%) 0.2200 / 0.2966 / 152154
H100, Gemma-4-31B not run 5.7351 → 5.6516 (+1.46%) not resolved not run

Throughput varies 2.6-3% between runs of one setup on H100 and up to 7% on B200, so I don't count those H100 and B200 cells as gains. The Gemma-4-31B run hit its time limit before the compiled arm and the last main throughput run.

GPU time, torch.profiler, 32 prompts × 1024 in / 128 out, ms:

GPU, model MLP act+quant, main → PR same step, compiled total, main → PR total, compiled
L4, Gemma3-1B 245.8 → 137.4 137.9 2426.6 → 2334.7 (−3.79%) 2316.0
H100, Gemma3-1B about 36.6 → 24.7 25.9 385.7 → 374.2 (−2.99%) 356.9
B200, Gemma3-1B about 34.8 → 24.6 24.7 314.0 → 304.4 (−3.07%) 281.6
H100, Gemma-4-31B about 275.9 → 188.1 255.0 5652.2 → 5571.3 (−1.43%) 5578.9
Kernel benchmark and gsm8k
python benchmarks/kernels/benchmark_activation.py --func-name gelu_and_mul_tanh --quantize-fp8

L4, µs, median of triton.testing.do_bench_cudagraph, bf16, the two kernels → this kernel. All 20 shapes are 1.12-3.2× faster, and fp16 is within 2%.

width (Gemma3) 1 token 16 128 512 2048
6912 (1B) 4.87 → 3.27 4.98 → 3.33 9.29 → 7.12 26.1 → 23.2 424 → 281
10240 (4B) 6.23 → 4.25 6.38 → 4.36 12.6 → 9.93 43.5 → 33.4 739 → 410
15360 (12B) 7.56 → 5.20 7.73 → 5.26 16.7 → 13.4 171 → 53.2 1165 → 613
21504 (27B) 9.71 → 6.73 10.1 → 6.79 23.1 → 20.3 292 → 163 1639 → 859

Standalone comparison at the same widths and 1-8192 tokens, L2-warm / flushed: H100 1.22-1.72× / 1.28-1.72×, B200 1.08-1.54× / 1.11-1.50×, faster on 24/24 shapes on both. Random inputs are bit-exact on 72/72 cases on both (bf16 and fp16, with and without scale_ub).

gsm8k, greedy, 1319 questions, CUDA ops, L4. End-to-end runs are not deterministic on main, and these stay within its spread:

run main this PR
4B, offline 0.7672, 0.7627 0.7604
4B, server 0.763 0.754
1B, server 0.267 0.262

Benchmarks and exactness ran on older trees with this same kernel file: e96b490b12 (H100, Gemma3) and e0d00e4b8e (B200, and H100 Gemma-4-31B).

Details


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.

@subhashpolisetti
subhashpolisetti requested a review from a team October 28, 2026 06:09

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

@mergify

mergify Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--64212.org.readthedocs.build/en/64212/

@mergify mergify Bot added documentation Improvements or additions to documentation ci/build performance Performance-related issues quantization nvidia torch.compile labels Oct 28, 2026
t239573445 added a commit to t239573445/vllm that referenced this pull request Oct 31, 2026
GELU-family models (Gemma 1/2/3, Gemma3n, Rnj1) run `gelu_tanh_and_mul`
followed by `scaled_fp8_quant` when the checkpoint carries static FP8
per-tensor quantization. That path writes a bf16 activation of
`intermediate_size` elements per token and then reads it back for the
quantization, so the same data crosses DRAM twice.

This adds `torch.ops._C.gelu_tanh_and_mul_quant`. The kernel reuses the
existing `act_and_mul_quant_kernel` template with a tanh-GELU functor,
so only the activation differs from `silu_and_mul_quant`. The op is
registered in the manual act+quant fusion registry for
`(GeluAndMul, kFp8StaticTensorSym)`, gated on `approximate == "tanh"`
so `GeluAndMul(approximate="none")` keeps the unfused path.

Numerics: bit-identical to
`F.gelu(x, approximate="tanh") * up -> scaled_fp8_quant` over 33.5M
elements (verified up to 8192x4096). An algebraically equivalent
erfc-based formulation was measured to differ in 8 of 12.5M elements,
so the tanh formulation already used by `gelu_tanh_kernel` is kept,
which also keeps fused and unfused paths identical.

Performance (RTX 5090 D v2, SM120, bf16 in / fp8 out, median of 200
iters after 50 warmup iters):

| shape       | unfused | fused   | speedup |
|-------------|---------|---------|---------|
| 1x4096      | 6.0 us  | 2.7 us  | 2.21x   |
| 64x4096     | 5.9 us  | 2.7 us  | 2.15x   |
| 512x4096    | 8.2 us  | 4.1 us  | 2.00x   |
| 2048x4096   | 16.3 us | 12.3 us | 1.33x   |
| 2048x14336  | 165.6 us| 105.3 us| 1.57x   |

geomean 1.82x. 2048x14336 matches the down_proj input width of a
Gemma-4-31B style MLP.

Tests: op-level bit-exactness test, a `maybe_fused_act_quant`
registry-level test, and a fallback test for `approximate="none"`.
`tests/kernels/test_fused_quant_activation.py`: 204 passed.

Also updates `docs/design/fusions.md` and adds
`benchmarks/kernels/benchmark_gelu_tanh_fp8_quant.py` (--check mode asserts
bit-exactness against the unfused path).

Adds the `fix_functionalization` branch for the new op, mirroring
`silu_and_mul_quant`; without it the op falls into the kwargs path, which the
existing comment there documents as returning the wrong answer.

Wires `maybe_fused_act_quant` into `Gemma2MLP` and `Gemma4MLP` (one line each,
the same pattern `llama.py` uses); without it the registry row is inert on real
models. `gemma3.py` is intentionally left to vllm-project#64212, which already touches that
file for the dynamic-per-token variant, to avoid duplicate wiring.

Signed-off-by: Alan <239573445@qq.com>
t239573445 added a commit to t239573445/vllm that referenced this pull request Oct 31, 2026
GELU-family models (Gemma 1/2/3, Gemma3n, Rnj1) run `gelu_tanh_and_mul`
followed by `scaled_fp8_quant` when the checkpoint carries static FP8
per-tensor quantization. That path writes a bf16 activation of
`intermediate_size` elements per token and then reads it back for the
quantization, so the same data crosses DRAM twice.

This adds `torch.ops._C.gelu_tanh_and_mul_quant`. The kernel reuses the
existing `act_and_mul_quant_kernel` template with a tanh-GELU functor,
so only the activation differs from `silu_and_mul_quant`. The op is
registered in the manual act+quant fusion registry for
`(GeluAndMul, kFp8StaticTensorSym)`, gated on `approximate == "tanh"`
so `GeluAndMul(approximate="none")` keeps the unfused path.

Numerics: bit-identical to
`F.gelu(x, approximate="tanh") * up -> scaled_fp8_quant` over 33.5M
elements (verified up to 8192x4096). An algebraically equivalent
erfc-based formulation was measured to differ in 8 of 12.5M elements,
so the tanh formulation already used by `gelu_tanh_kernel` is kept,
which also keeps fused and unfused paths identical.

Performance (RTX 5090 D v2, SM120, bf16 in / fp8 out, median of 200
iters after 50 warmup iters):

| shape       | unfused | fused   | speedup |
|-------------|---------|---------|---------|
| 1x4096      | 6.0 us  | 2.7 us  | 2.21x   |
| 64x4096     | 5.9 us  | 2.7 us  | 2.15x   |
| 512x4096    | 8.2 us  | 4.1 us  | 2.00x   |
| 2048x4096   | 16.3 us | 12.3 us | 1.33x   |
| 2048x14336  | 165.6 us| 105.3 us| 1.57x   |

geomean 1.82x. 2048x14336 matches the down_proj input width of a
Gemma-4-31B style MLP.

Tests: op-level bit-exactness test, a `maybe_fused_act_quant`
registry-level test, and a fallback test for `approximate="none"`.
`tests/kernels/test_fused_quant_activation.py`: 204 passed.

Also updates `docs/design/fusions.md` and adds
`benchmarks/kernels/benchmark_gelu_tanh_fp8_quant.py` (--check mode asserts
bit-exactness against the unfused path).

Adds the `fix_functionalization` branch for the new op, mirroring
`silu_and_mul_quant`; without it the op falls into the kwargs path, which the
existing comment there documents as returning the wrong answer.

Wires `maybe_fused_act_quant` into `Gemma2MLP` and `Gemma4MLP` (one line each,
the same pattern `llama.py` uses); without it the registry row is inert on real
models. `gemma3.py` is intentionally left to vllm-project#64212, which already touches that
file for the dynamic-per-token variant, to avoid duplicate wiring.

Write the tanh-GELU inner term as `KAPPA * (f * f * f)` with an explicit
`x_cube`, matching `gelu_tanh_kernel` in `csrc/activation_kernels.cu`
term-for-term. The previous `KAPPA * f * f * f` parses as `((KAPPA*f)*f)*f`,
which is a different floating-point association; it measured identical after
bf16 rounding and fp8 quantization, but there is no reason to rely on that.

Signed-off-by: Alan <239573445@qq.com>
t239573445 added a commit to t239573445/vllm that referenced this pull request Oct 31, 2026
GELU-family models (Gemma 1/2/3, Gemma3n, Rnj1) run `gelu_tanh_and_mul`
followed by `scaled_fp8_quant` when the checkpoint carries static FP8
per-tensor quantization. That path writes a bf16 activation of
`intermediate_size` elements per token and then reads it back for the
quantization, so the same data crosses DRAM twice.

This adds `torch.ops._C.gelu_tanh_and_mul_quant`. The kernel reuses the
existing `act_and_mul_quant_kernel` template with a tanh-GELU functor,
so only the activation differs from `silu_and_mul_quant`. The op is
registered in the manual act+quant fusion registry for
`(GeluAndMul, kFp8StaticTensorSym)`, gated on `approximate == "tanh"`
so `GeluAndMul(approximate="none")` keeps the unfused path.

Numerics: bit-identical to
`F.gelu(x, approximate="tanh") * up -> scaled_fp8_quant` over 33.5M
elements (verified up to 8192x4096). An algebraically equivalent
erfc-based formulation was measured to differ in 8 of 12.5M elements,
so the tanh formulation already used by `gelu_tanh_kernel` is kept,
which also keeps fused and unfused paths identical.

Performance (RTX 5090 D v2, SM120, bf16 in / fp8 out, median of 200
iters after 50 warmup iters):

| shape       | unfused | fused   | speedup |
|-------------|---------|---------|---------|
| 1x4096      | 6.0 us  | 2.7 us  | 2.21x   |
| 64x4096     | 5.9 us  | 2.7 us  | 2.15x   |
| 512x4096    | 8.2 us  | 4.1 us  | 2.00x   |
| 2048x4096   | 16.3 us | 12.3 us | 1.33x   |
| 2048x14336  | 165.6 us| 105.3 us| 1.57x   |

geomean 1.82x. 2048x14336 matches the down_proj input width of a
Gemma-4-31B style MLP.

Tests: op-level bit-exactness test, a `maybe_fused_act_quant`
registry-level test, and a fallback test for `approximate="none"`.
`tests/kernels/test_fused_quant_activation.py`: 204 passed.

Also updates `docs/design/fusions.md` and adds
`benchmarks/kernels/benchmark_gelu_tanh_fp8_quant.py` (--check mode asserts
bit-exactness against the unfused path).

Adds the `fix_functionalization` branch for the new op, mirroring
`silu_and_mul_quant`; without it the op falls into the kwargs path, which the
existing comment there documents as returning the wrong answer.

Wires `maybe_fused_act_quant` into `Gemma2MLP` and `Gemma4MLP` (one line each,
the same pattern `llama.py` uses); without it the registry row is inert on real
models. `gemma3.py` is intentionally left to vllm-project#64212, which already touches that
file for the dynamic-per-token variant, to avoid duplicate wiring.

Write the tanh-GELU inner term as `KAPPA * (f * f * f)` with an explicit
`x_cube`, matching `gelu_tanh_kernel` in `csrc/activation_kernels.cu`
term-for-term. The previous `KAPPA * f * f * f` parses as `((KAPPA*f)*f)*f`,
which is a different floating-point association; it measured identical after
bf16 rounding and fp8 quantization, but there is no reason to rely on that.

Adds tests that pin the fused op against the actual unfused path
(`gelu_tanh_and_mul` + `scaled_fp8_quant`), plus odd/sub-vector-width hidden
sizes and non-2D inputs.

Signed-off-by: Alan <239573445@qq.com>
t239573445 added a commit to t239573445/vllm that referenced this pull request Oct 31, 2026
GELU-family models (Gemma 1/2/3, Gemma3n, Rnj1) run `gelu_tanh_and_mul`
followed by `scaled_fp8_quant` when the checkpoint carries static FP8
per-tensor quantization. That path writes a bf16 activation of
`intermediate_size` elements per token and then reads it back for the
quantization, so the same data crosses DRAM twice.

This adds `torch.ops._C.gelu_tanh_and_mul_quant`. The kernel reuses the
existing `act_and_mul_quant_kernel` template with a tanh-GELU functor,
so only the activation differs from `silu_and_mul_quant`. The op is
registered in the manual act+quant fusion registry for
`(GeluAndMul, kFp8StaticTensorSym)`, gated on `approximate == "tanh"`
so `GeluAndMul(approximate="none")` keeps the unfused path.

Numerics: bit-identical to
`F.gelu(x, approximate="tanh") * up -> scaled_fp8_quant` over 33.5M
elements (verified up to 8192x4096). An algebraically equivalent
erfc-based formulation was measured to differ in 8 of 12.5M elements,
so the tanh formulation already used by `gelu_tanh_kernel` is kept,
which also keeps fused and unfused paths identical.

Performance (RTX 5090 D v2, SM120, bf16 in / fp8 out, median of 200
iters after 50 warmup iters):

| shape       | unfused | fused   | speedup |
|-------------|---------|---------|---------|
| 1x4096      | 6.0 us  | 2.7 us  | 2.21x   |
| 64x4096     | 5.9 us  | 2.7 us  | 2.15x   |
| 512x4096    | 8.2 us  | 4.1 us  | 2.00x   |
| 2048x4096   | 16.3 us | 12.3 us | 1.33x   |
| 2048x14336  | 165.6 us| 105.3 us| 1.57x   |

geomean 1.82x. 2048x14336 matches the down_proj input width of a
Gemma-4-31B style MLP.

Tests: op-level bit-exactness test, a `maybe_fused_act_quant`
registry-level test, and a fallback test for `approximate="none"`.
`tests/kernels/test_fused_quant_activation.py`: 204 passed.

Also updates `docs/design/fusions.md` and adds
`benchmarks/kernels/benchmark_gelu_tanh_fp8_quant.py` (--check mode asserts
bit-exactness against the unfused path).

Adds the `fix_functionalization` branch for the new op, mirroring
`silu_and_mul_quant`; without it the op falls into the kwargs path, which the
existing comment there documents as returning the wrong answer.

Wires `maybe_fused_act_quant` into `Gemma2MLP` and `Gemma4MLP` (one line each,
the same pattern `llama.py` uses); without it the registry row is inert on real
models. `gemma3.py` is intentionally left to vllm-project#64212, which already touches that
file for the dynamic-per-token variant, to avoid duplicate wiring.

Write the tanh-GELU inner term as `KAPPA * (f * f * f)` with an explicit
`x_cube`, matching `gelu_tanh_kernel` in `csrc/activation_kernels.cu`
term-for-term. The previous `KAPPA * f * f * f` parses as `((KAPPA*f)*f)*f`,
which is a different floating-point association; it measured identical after
bf16 rounding and fp8 quantization, but there is no reason to rely on that.

Adds tests that pin the fused op against the actual unfused path
(`gelu_tanh_and_mul` + `scaled_fp8_quant`), plus odd/sub-vector-width hidden
sizes and non-2D inputs.

Reworks the docs entry: the GELU row no longer links at the SiLU section and no
longer claims to be driven by the `fuse_act_quant` PassConfig flag. It is a
manual fusion applied in the model forward, so it is active at every
optimization level and gets its own section.

Signed-off-by: Alan <239573445@qq.com>
t239573445 added a commit to t239573445/vllm that referenced this pull request Oct 31, 2026
GELU-family models (Gemma 1/2/3, Gemma3n, Rnj1) run `gelu_tanh_and_mul`
followed by `scaled_fp8_quant` when the checkpoint carries static FP8
per-tensor quantization. That path writes a bf16 activation of
`intermediate_size` elements per token and then reads it back for the
quantization, so the same data crosses DRAM twice.

This adds `torch.ops._C.gelu_tanh_and_mul_quant`. The kernel reuses the
existing `act_and_mul_quant_kernel` template with a tanh-GELU functor,
so only the activation differs from `silu_and_mul_quant`. The op is
registered in the manual act+quant fusion registry for
`(GeluAndMul, kFp8StaticTensorSym)`, gated on `approximate == "tanh"`
so `GeluAndMul(approximate="none")` keeps the unfused path.

Numerics: bit-identical to
`F.gelu(x, approximate="tanh") * up -> scaled_fp8_quant` over 33.5M
elements (verified up to 8192x4096). An algebraically equivalent
erfc-based formulation was measured to differ in 8 of 12.5M elements,
so the tanh formulation already used by `gelu_tanh_kernel` is kept,
which also keeps fused and unfused paths identical.

Performance (RTX 5090 D v2, SM120, bf16 in / fp8 out, median of 200
iters after 50 warmup iters):

| shape       | unfused | fused   | speedup |
|-------------|---------|---------|---------|
| 1x4096      | 6.0 us  | 2.7 us  | 2.21x   |
| 64x4096     | 5.9 us  | 2.7 us  | 2.15x   |
| 512x4096    | 8.2 us  | 4.1 us  | 2.00x   |
| 2048x4096   | 16.3 us | 12.3 us | 1.33x   |
| 2048x14336  | 165.6 us| 105.3 us| 1.57x   |

geomean 1.82x. 2048x14336 matches the down_proj input width of a
Gemma-4-31B style MLP.

Tests: op-level bit-exactness test, a `maybe_fused_act_quant`
registry-level test, and a fallback test for `approximate="none"`.
`tests/kernels/test_fused_quant_activation.py`: 204 passed.

Also updates `docs/design/fusions.md` and adds
`benchmarks/kernels/benchmark_gelu_tanh_fp8_quant.py` (--check mode asserts
bit-exactness against the unfused path).

Adds the `fix_functionalization` branch for the new op, mirroring
`silu_and_mul_quant`; without it the op falls into the kwargs path, which the
existing comment there documents as returning the wrong answer.

Wires `maybe_fused_act_quant` into `Gemma2MLP` and `Gemma4MLP` (one line each,
the same pattern `llama.py` uses); without it the registry row is inert on real
models. `gemma3.py` is intentionally left to vllm-project#64212, which already touches that
file for the dynamic-per-token variant, to avoid duplicate wiring.

Write the tanh-GELU inner term as `KAPPA * (f * f * f)` with an explicit
`x_cube`, matching `gelu_tanh_kernel` in `csrc/activation_kernels.cu`
term-for-term. The previous `KAPPA * f * f * f` parses as `((KAPPA*f)*f)*f`,
which is a different floating-point association; it measured identical after
bf16 rounding and fp8 quantization, but there is no reason to rely on that.

Adds tests that pin the fused op against the actual unfused path
(`gelu_tanh_and_mul` + `scaled_fp8_quant`), plus odd/sub-vector-width hidden
sizes and non-2D inputs.

Reworks the docs entry: the GELU row no longer links at the SiLU section and no
longer claims to be driven by the `fuse_act_quant` PassConfig flag. It is a
manual fusion applied in the model forward, so it is active at every
optimization level and gets its own section.

Use the anchor form that mkdocs actually renders (single hyphen): its slugifier
collapses runs of separators, so the `--` used by the other rows in that table
does not resolve.

Signed-off-by: Alan <239573445@qq.com>
t239573445 added a commit to t239573445/vllm that referenced this pull request Nov 1, 2026
GELU-family models (Gemma 1/2/3, Gemma3n, Rnj1) run `gelu_tanh_and_mul`
followed by `scaled_fp8_quant` when the checkpoint carries static FP8
per-tensor quantization. That path writes a bf16 activation of
`intermediate_size` elements per token and then reads it back for the
quantization, so the same data crosses DRAM twice.

This adds `torch.ops._C.gelu_tanh_and_mul_quant`. The kernel reuses the
existing `act_and_mul_quant_kernel` template with a tanh-GELU functor,
so only the activation differs from `silu_and_mul_quant`. The op is
registered in the manual act+quant fusion registry for
`(GeluAndMul, kFp8StaticTensorSym)`, gated on `approximate == "tanh"`
so `GeluAndMul(approximate="none")` keeps the unfused path.

Numerics: bit-identical to
`F.gelu(x, approximate="tanh") * up -> scaled_fp8_quant` over 33.5M
elements (verified up to 8192x4096). An algebraically equivalent
erfc-based formulation was measured to differ in 8 of 12.5M elements,
so the tanh formulation already used by `gelu_tanh_kernel` is kept,
which also keeps fused and unfused paths identical.

Performance (RTX 5090 D v2, SM120, bf16 in / fp8 out, median of 200
iters after 50 warmup iters):

| shape       | unfused | fused   | speedup |
|-------------|---------|---------|---------|
| 1x4096      | 6.0 us  | 2.7 us  | 2.21x   |
| 64x4096     | 5.9 us  | 2.7 us  | 2.15x   |
| 512x4096    | 8.2 us  | 4.1 us  | 2.00x   |
| 2048x4096   | 16.3 us | 12.3 us | 1.33x   |
| 2048x14336  | 165.6 us| 105.3 us| 1.57x   |

geomean 1.82x. 2048x14336 matches the down_proj input width of a
Gemma-4-31B style MLP.

Tests: op-level bit-exactness test, a `maybe_fused_act_quant`
registry-level test, and a fallback test for `approximate="none"`.
`tests/kernels/test_fused_quant_activation.py`: 204 passed.

Also updates `docs/design/fusions.md` and adds
`benchmarks/kernels/benchmark_gelu_tanh_fp8_quant.py` (--check mode asserts
bit-exactness against the unfused path).

Adds the `fix_functionalization` branch for the new op, mirroring
`silu_and_mul_quant`; without it the op falls into the kwargs path, which the
existing comment there documents as returning the wrong answer.

Wires `maybe_fused_act_quant` into `Gemma2MLP` and `Gemma4MLP` (one line each,
the same pattern `llama.py` uses); without it the registry row is inert on real
models. `gemma3.py` is intentionally left to vllm-project#64212, which already touches that
file for the dynamic-per-token variant, to avoid duplicate wiring.

Write the tanh-GELU inner term as `KAPPA * (f * f * f)` with an explicit
`x_cube`, matching `gelu_tanh_kernel` in `csrc/activation_kernels.cu`
term-for-term. The previous `KAPPA * f * f * f` parses as `((KAPPA*f)*f)*f`,
which is a different floating-point association; it measured identical after
bf16 rounding and fp8 quantization, but there is no reason to rely on that.

Adds tests that pin the fused op against the actual unfused path
(`gelu_tanh_and_mul` + `scaled_fp8_quant`), plus odd/sub-vector-width hidden
sizes and non-2D inputs.

Reworks the docs entry: the GELU row no longer links at the SiLU section and no
longer claims to be driven by the `fuse_act_quant` PassConfig flag. It is a
manual fusion applied in the model forward, so it is active at every
optimization level and gets its own section.

Use the anchor form that mkdocs actually renders (single hyphen): its slugifier
collapses runs of separators, so the `--` used by the other rows in that table
does not resolve.

Add a non-contiguous-input test: like `silu_and_mul_quant`, this op does not
guard contiguity, so pin that the fused path agrees with the unfused path on
such a view instead of drifting silently.

Signed-off-by: Alan <239573445@qq.com>
@subhashpolisetti
subhashpolisetti force-pushed the feat-gelu-fp8-fusion-v3 branch 2 times, most recently from 413cc3d to d44e150 Compare November 1, 2026 09:53
…zation

Add gelu_tanh_and_mul_per_token_quant, a CUDA kernel that computes
gelu_tanh(gate) * up and quantizes it to FP8 with one scale per token in a
single pass over the input, bit-identical to gelu_tanh_and_mul followed by
dynamic_per_token_scaled_fp8_quant. Register it as the (GeluAndMul,
per-token FP8) row of the manual activation+quant fusion, used when GELU
runs as the CUDA op, and wire it into Gemma3 and Gemma 4. The CUTLASS FP8 linear now
accepts the per-token input quantization key.

Signed-off-by: subhash polisetti <subhashr161347@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/build documentation Improvements or additions to documentation nvidia performance Performance-related issues quantization torch.compile

Projects

Status: No status
Status: To triage

Development

Successfully merging this pull request may close these issues.

1 participant