Skip to content

[Perf] qwen3: Fuse SiLU*mul with the NVFP4 quantization of down_proj in Qwen2MLP - #44491

Merged
BBuf merged 4 commits into
sgl-project:mainfrom
gkswns0531:perf/qwen3-silu-fp4-quant-fusion
Nov 5, 2026
Merged

BBuf merged 4 commits into
sgl-project:mainfrom
gkswns0531:perf/qwen3-silu-fp4-quant-fusion

Conversation

@gkswns0531

@gkswns0531 gkswns0531 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Qwen3.5's dense MLP runs SiLU*mul and the NVFP4 quantization of the down_proj input as one FlashInfer kernel. Qwen3 uses Qwen2MLP, which lacks that path, so its NVFP4 W4A4 MLPs write the activation in 16 bits (act_and_mul) and read it back (fp4_quantize).

Modifications

  • Move the fused call and its enabling check from Qwen2MoeMLP / qwen3_5.py to models/utils.py and give Qwen2MLP the same branch, enabled only by Qwen3DecoderLayer when both projections use ModelOpt NVFP4 W4A4. Other Qwen2MLP users are unchanged.
  • Keep it off where down_proj cannot use the fused output. Qwen3.5's dense MLP shares these checks, so it gets them too.
    • NVFP4 AWQ: the fused kernel skips pre_quant_scale ([Bugfix] Preserve NVFP4 AWQ prescaling in MLP fusion and Marlin paths #38864 adds the same check to the Qwen3.5 copy).
    • The Marlin FP4 GEMM backend, the default on SM80-SM90: it takes a floating-point input.
    • A down_proj K that is not a multiple of 64: the fused kernel leaves the padded scale columns unwritten.
    • Per forward, sequence parallelism or a LoRA-wrapped down_proj: both read the activation tensor (as DualGemm.can_run does). Without the LoRA check, a Qwen3 NVFP4 server with a down_proj adapter fails at startup.
  • CPU unit tests for each gate, and that the fusion turns on for W4A4.

Accuracy Tests

  • The fused FP4 data and block scales are bit-identical to the two-kernel path (M = 128 to 40960, intermediate size 9728).
  • Qwen3-Embedding-4B NVFP4 on 60 questions each of Ko-StrategyQA / MIRACL-ko: Recall@10 0.876 / 0.767 and the distance to BF16 scores are identical to main.
  • Qwen3-Reranker-4B NVFP4 as a causal LM (CUDA graphs on), and Qwen3-Reranker-0.6B NVFP4 with a down_proj LoRA adapter: greedy outputs match main.
  • On an L4 (SM89, auto picks Marlin), greedy outputs and logprobs match main (Qwen3-Reranker 0.6B and 4B NVFP4).
  • test_qwen3_5_modelopt_fp4.py: 15 passed.

Speed Tests and Profiling

RTX PRO 6000 (SM120), FlashInfer 0.7.1. Qwen3-Embedding-4B NVFP4, GPU time of one encode call on 128 sequences:

128 sequences main this PR
320 tokens 455.6 ms 419.0 ms (-8.0%)
640 tokens 925.1 ms 844.2 ms (-8.7%)

Decode throughput of Qwen3-Reranker-4B NVFP4 at batch 1 / 16 / 64 is unchanged.

The kernels alone (CUPTI, cold L2), SiLU*mul + NVFP4 quantization of a [M, 2 x 9728] BF16 input
M silu_and_mul + fp4_quantize fused
1024 28.4 us 9.4 us
8192 444.1 us 249.9 us
40960 2294.8 us 1251.9 us

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): ✅ Run #41621617029
Latest PR Test (Extra): ❌ Run #41621617024
Latest PR Test (AMD ROCm 10): ✅ Run #41621617068

Tip

Base CI only covers the core cases, and its workload may not exercise your change. Use /rerun-test to run the tests your change touches, or copy this prompt for an agent:

Find the registered tests this PR affects and run them with /rerun-test, following https://github.com/sgl-project/sglang/blob/main/.agents/skills/rerun-test/SKILL.md

@gkswns0531

Copy link
Copy Markdown
Contributor Author

@Fridge003 @ishandhanani @Qiaolin-Yu Could one of you take a look and add run-ci when you have a moment? It shares Qwen3.5's dense-MLP SiLU+NVFP4 fusion with Qwen3 (models/ has no CODEOWNERS entry, so no reviewer was requested). Thanks!

@gkswns0531
gkswns0531 force-pushed the perf/qwen3-silu-fp4-quant-fusion branch from c9f0a75 to 1a613a0 Compare October 19, 2026 08:55
@gkswns0531

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main so CI can run. Gentle ping @Fridge003 in case this got buried. Thanks!

@gkswns0531
gkswns0531 force-pushed the perf/qwen3-silu-fp4-quant-fusion branch from 1a613a0 to f91c752 Compare October 25, 2026 07:02
@gkswns0531

Copy link
Copy Markdown
Contributor Author

Rebased onto main. #45219 added a block-FP8 decode fusion to the same branch of Qwen2MLP.forward; the two apply to different quantization methods, so both are kept. @BBuf, since you reviewed #45219, could you take a look and add run-ci when you have a moment? Thanks!

…n2MLP

Qwen3.5's dense MLP runs SiLU*mul and the down_proj NVFP4 input quantization
as one FlashInfer kernel; Qwen3, whose decoder layers use Qwen2MLP, ran them
as two. Move the fused call and its enabling check from Qwen2MoeMLP and
qwen3_5.py to models/utils.py, give Qwen2MLP the same path, and enable it for
Qwen3 layers whose MLP projections are both NVFP4 W4A4. Keep it off for NVFP4
AWQ checkpoints, whose per-input-channel scale the fused producer does not
apply, and skip it in a forward where sequence parallelism is active or
down_proj is wrapped by LoRA, since both read the activation tensor.

Signed-off-by: Hanjun Cho <gkswns0531@gmail.com>
@gkswns0531
gkswns0531 force-pushed the perf/qwen3-silu-fp4-quant-fusion branch from f91c752 to 644c16f Compare November 3, 2026 04:20

@BBuf BBuf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[BBuf Review Agent] One thing to fix before this can go in.

prefix=add_prefix("mlp", prefix),
reduce_results=False,
)
maybe_enable_silu_fp4_quant_fusion(self.mlp)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[BBuf Review Agent] Qwen3 NVFP4 with --fp4-gemm-backend marlin now sends an FP4 tuple to apply_fp4_marlin_linear, which requires a floating-point tensor, so the first forward crashes. Could we exclude Marlin in the shared enabling check and cover this MLP handoff?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@BBuf
Thanks!
On SM80-SM90 auto also picks Marlin, so this would have broken the default path there. f5c2129 keeps the fusion off on the Marlin backend. Qwen3.5's dense MLP uses the same check, so it's covered too, and test_stays_off_for_the_marlin_backend fails without the change.

Checked with Qwen3-Reranker NVFP4 checkpoints:

  • L4 (SM89, auto picks Marlin): the previous head failed on the first forward. Greedy outputs and logprobs now match main exactly (0.6B and 4B).
  • RTX PRO 6000 with --fp4-gemm-backend marlin: the previous head failed with 'tuple' object has no attribute 'dtype'. It now serves normally.
  • RTX PRO 6000 with the default backend: the fusion stays on, and outputs match main exactly.

While re-checking the handoff I found one more case. When down_proj's K is not a multiple of 64, the FP4 scales are padded, and the fused kernel leaves the padding unwritten (fp4_quantize zeroes it). In a kernel test with K = 1376 and 608, a NaN byte left there turned the whole GEMM output into NaN. 47dcb3e keeps the fusion off for such K; every Qwen3 size at TP <= 8 is a multiple of 64. 8b51234 moves the kill switch into Envs and logs the enable message once.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[BBuf Review Agent] The shared guard now excludes Marlin for both Qwen3 and Qwen3.5, and the regression test covers that check. This addresses the tuple handoff crash.

Marlin, the default FP4 GEMM backend on SM80-SM90, takes a floating-point
activation, so it cannot consume the prequantized (fp4, scale) tuple and a
Qwen3 NVFP4 model crashed on the first forward. Leave the fusion off when the
FP4 GEMM backend is Marlin. The enabling-check tests now use a stand-in
FlashInfer, so the AWQ and Marlin gates are exercised whether or not
FlashInfer is installed, and a positive case shows the fusion turns on for
W4A4 on a native FP4 GEMM backend.

Signed-off-by: Hanjun Cho <gkswns0531@gmail.com>
The NVFP4 block scales of a K-wide input are padded to a multiple of four
columns. fp4_quantize zeroes that padding, but the fused SiLU*mul+FP4 kernel
never writes it, and the FP4 GEMM reads it. On an RTX PRO 6000, with K = 1376
or 608 and freed memory holding a NaN byte, every output of the down_proj GEMM
became NaN (CUTLASS and cuDNN backends); K = 1216 was unaffected. Leave the
fusion off unless down_proj's input width is a multiple of 64. Every Qwen3
intermediate size at TP <= 8 is.

Signed-off-by: Hanjun Cho <gkswns0531@gmail.com>
Register SGLANG_DISABLE_SILU_FP4_QUANT_FUSION in Envs instead of reading
os.environ in models/utils.py, log the enable message once per process
rather than once per decoder layer, trim comments that restated the gates
or named the caller, and state in each regression test's docstring the
behavior it guards.

Signed-off-by: Hanjun Cho <gkswns0531@gmail.com>

@BBuf BBuf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[BBuf Review Agent] LGTM.

@BBuf

BBuf commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

/tag-and-rerun-ci

@github-actions github-actions Bot added the run-ci CI: run the baseline test suite on this PR label Nov 3, 2026
@BBuf

BBuf commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

/rerun-failed-ci

1 similar comment
@BBuf

BBuf commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

/rerun-failed-ci

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

Labels

run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants