Skip to content

[ROCm][DSV4][Perf] Use FP8 WO_A output projection - #54894

Merged
shen-shanshan merged 3 commits into
vllm-project:mainfrom
LiuYinfeng01:rocm-dsv4-fp8-woa-mxscale
Sep 20, 2026
Merged

shen-shanshan merged 3 commits into
vllm-project:mainfrom
LiuYinfeng01:rocm-dsv4-fp8-woa-mxscale

Conversation

@LiuYinfeng01

@LiuYinfeng01 LiuYinfeng01 commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Replace the ROCm DeepSeek V4 BF16 wo_a output-projection path on gfx950 with:

  1. AITER inverse_rope_group_quant, which fuses inverse RoPE with per-token
    E8M0 group quantization.
  2. AITER batched_gemm_a8w8_mxscale, which consumes the quantized activation
    and the checkpoint's native FP8 wo_a weight.

This removes the BF16 grouped einsum and keeps wo_a in its checkpoint FP8
form. Unsupported weights, scales, devices, and older AITER installations
retain the existing BF16 fallback.

Dependency

This fast path requires AITER >= 0.1.20, which added the MX-scale batched GEMM.
It therefore depends on #52826. Current main remains safe: the capability
probe logs once and falls back to BF16 when the op is unavailable.

Implementation

  • Reuse the existing VLLM_ROCM_USE_AITER_FP8BMM gate and restrict the path to
    gfx950.
  • Accept native float8_e8m0fnu/uint8 scales, or losslessly convert positive
    power-of-two FP32 scales to biased E8M0 exponent bytes.
  • Validate the group-128 weight and scale layouts before enabling the path.
  • Cache contiguous BF16 cosine/sine cache views once after weight loading.
  • Preserve the existing rocm_inv_rope_einsum path as the fallback.

Performance

Measured on 8 x MI355X, TP1/PP8, one 100,000-token prefill request, 8K prefill
chunks, prefix caching disabled, three runs:

Path TTFT runs (ms) Median TTFT Input throughput
OPUS sparse MLA + tuned GEMMs 2470.2 / 2457.9 / 2459.0 2459.0 ms 40,667.0 tok/s
+ FP8 wo_a 2288.8 / 2276.1 / 2278.4 2278.4 ms 43,891.0 tok/s

Delta: TTFT -7.35% and input throughput +7.93%.

TP8/PP1 prefill concurrency

Measured on 8 x MI355X with TP8/PP1, fixed 8,192-token prompts and one output
token, prefix caching disabled, and 10 requests per concurrency slot. Both arms
use the same image and differ only in the FP8 wo_a gate.

Max concurrency BF16 wo_a total tok/s FP8 wo_a total tok/s Gain
1 16,218.62 16,345.65 +0.78%
2 23,337.44 23,782.97 +1.91%
4 23,618.92 24,004.20 +1.63%
8 23,621.81 24,051.77 +1.82%
16 23,623.62 24,069.49 +1.89%
32 23,624.64 24,069.36 +1.88%
48 23,621.17 24,069.38 +1.90%

All 1,110 requests in each arm completed without errors.

Revisions and runtime:

  • vLLM PR commit: a9cf3bc3498eb71bb2d6f5d44c4b0049b62feb7a
  • ROCm nightly base digest:
    sha256:cef549da00e0efaeadd9338ac8f351f2b96ff71a5ab8651a99bf989458bf1684
  • AITER commit: a6d2b564fd671724a3720b8edf70e8d674e4d694
  • FlyDSL: 0.3.1
  • Image ID: sha256:0f851f268bc6bdb41c5ed22663a106339b3b269a88afbef17dbda156f2fb7893
  • Common settings: TP8/PP1/DCP1/PCP1/EP1, FP8 KV cache, AITER MoE,
    Triton sparse prefill, 8K max batched tokens, no prefix cache
  • A/B gates: VLLM_ROCM_USE_AITER_FP8BMM=0/1

The enabled-arm logs load module_inverse_rope_group_quant and
opus_bmm_a8w8_mxscale; neither appears in the disabled-arm logs.

TP1/PP8 prefill concurrency

Measured with the same fixed 8,192-token prompts, one output token, disabled
prefix caching, and 10 requests per concurrency slot. The image, revisions,
runtime settings, and A/B gate are identical to the TP8/PP1 measurements above;
only the topology changes to TP1/PP8.

Max concurrency BF16 wo_a total tok/s FP8 wo_a total tok/s Gain
1 4,119.72 4,271.13 +3.68%
2 6,436.17 6,865.25 +6.67%
4 6,461.36 6,896.31 +6.73%
8 6,460.00 6,891.96 +6.69%
16 6,457.00 6,891.06 +6.72%
32 6,459.55 6,889.21 +6.65%
48 6,460.06 6,889.74 +6.65%

All 1,110 requests in each arm completed without errors. The enabled-arm logs
load module_inverse_rope_group_quant and opus_bmm_a8w8_mxscale on all eight
pipeline stages; neither appears in the disabled-arm logs.

Correctness

Greedy GSM8K, zero invalid responses:

  • 100-question gate: 93/100
  • Full 1319 questions: 1256/1319 = 95.22%
  • Clean fixed-stack reference: 1249/1319 = 94.69%

TP8/PP1 GSM8K

Topology-matched greedy 5-shot evaluation on the TP8 stack above:

Path Accuracy Correct Invalid
BF16 wo_a 93.9348% 1239/1319 0
FP8 wo_a 94.9204% 1252/1319 1

The FP8 candidate first passed the 100-question gate at 93/100 with zero
invalid responses
. Full-set accuracy was +0.99 percentage points versus the
topology-matched BF16 reference; one response did not contain a parseable
answer.

Validation

  • git diff --check: passed
  • pre-commit hooks for both changed files: passed
  • tests/models/test_deepseek_v4_rocm_wo_a.py: 7 passed
  • TP1/PP8 100K prefill: 3/3 requests completed
  • GSM8K-1319: zero invalid responses

Related work

#45103 fused inverse RoPE and cached a BF16 wo_a; this PR retains that path as
fallback and uses the native FP8 weight on supported gfx950 configurations.

AI assistance

Cursor assisted with implementation, testing, benchmark analysis, and drafting.
The human submitter reviewed the resulting change and is responsible for it.


Re-measurement on the older DeepSeek-V4-Pro checkpoint

Scope note. Everything above this heading was measured on the
0813 checkpoint. Everything below was re-measured on the older
DeepSeek-V4-Pro snapshot
(models--deepseek-ai--DeepSeek-V4-Pro/snapshots/b5968e9190ef611bbf34a7229255be88a0e937c1,
staged locally as DeepSeek-V4-Pro-old). The two sets are not
interchangeable; they are reported side by side so the gate can be judged on
both checkpoints. Nothing above has been edited.

Setup

Item Value
Hardware 8 x MI355X (gfx950), single node
Image vllm/vllm-openai-rocm:nightly-d9105ea8001e0a6d77a96327d17515bb5791fb36
vLLM 0.28.1rc1.dev472+gd9105ea80 + this PR applied to vllm/models/deepseek_v4/amd/rocm.py
ROCm / torch 7.2.3 / 2.12.0+git6bbd260
Model DeepSeek-V4-Pro (older snapshot), FP4
A/B gate VLLM_ROCM_USE_AITER_FP8BMM=0 vs =1, same image and same patched file
Tuned GEMMs AITER_CONFIG_GEMM_A8W8_BLOCKSCALE pointed at a merged a8w8-blockscale tuning table
Workload Identical to the tables above: fixed 8,192-token prompts, one output token, prefix caching disabled, 10 requests per concurrency slot

Fast-path confirmation: the enabled arm's server log loads
module_inverse_rope_group_quant and the mxscale BMM; the disabled arm's log
contains neither (verified per run).

TP8/PP1 prefill concurrency

Max concurrency BF16 wo_a total tok/s FP8 wo_a total tok/s Gain
1 15,544.93 15,623.10 +0.50%
2 20,474.39 21,122.52 +3.17%
4 20,883.86 20,965.92 +0.39%
8 20,963.71 21,157.26 +0.92%
16 20,959.80 21,099.28 +0.67%
32 20,845.46 21,224.69 +1.82%
48 20,848.19 21,178.18 +1.58%

Mean gain across the sweep: +1.29%. All 1,110 requests per arm completed successfully.

TP1/PP8 prefill concurrency

Max concurrency BF16 wo_a total tok/s FP8 wo_a total tok/s Gain
1 4,228.93 4,403.25 +4.12%
2 6,179.04 6,528.63 +5.66%
4 6,176.99 6,595.96 +6.78%
8 6,189.02 6,591.95 +6.51%
16 6,188.72 6,591.11 +6.50%
32 6,190.38 6,591.60 +6.48%
48 6,190.03 6,596.08 +6.56%

Mean gain across the sweep: +6.09%. All 1,110 requests per arm completed successfully.

TP8/PP1 decode performance (8K input / 1K output)

Re-measured for the reviewer-requested TPOT comparison on the old checkpoint,
8 x MI355X, TP8/PP1, prefix caching disabled. Each concurrency uses four
measured waves (at least 16 requests) after a full unmeasured wave with a
different random seed. The only A/B variable is
VLLM_ROCM_USE_AITER_FP8BMM=0/1. The enabled log loads
module_inverse_rope_group_quant and opus_bmm_a8w8_mxscale.

Concurrency BF16 wo_a output tok/s FP8 wo_a output tok/s Throughput change BF16 TPOT FP8 TPOT TPOT change BF16 TTFT FP8 TTFT
1 63.35 64.55 +1.89% 15.43 ms 15.15 ms -1.85% 375.74 ms 367.52 ms
4 213.53 219.99 +3.02% 17.76 ms 17.23 ms -2.97% 1009.75 ms 986.39 ms
16 662.89 677.29 +2.17% 22.27 ms 21.75 ms -2.34% 1899.74 ms 1908.81 ms
64 1386.37 1431.21 +3.23% 42.15 ms 40.72 ms -3.39% 3999.62 ms 3984.62 ms

The FP8 wo_a path improves mean TPOT by 1.85%-3.39% and output
throughput by 1.89%-3.23% across the measured decode concurrency sweep.

TP1/PP8 single 100,000-token prefill

Three runs per arm, 8K prefill chunks, prefix caching disabled. The first run of
each arm carries a cold-start spike (~13.8 s on both arms) and is excluded from
the median.

Path TTFT runs (ms) Median TTFT Input throughput
BF16 wo_a 13878.3 / 4309.2 / 4328.5 4328.5 ms 23,100.5 tok/s
FP8 wo_a 13757.7 / 4096.0 / 4203.3 4203.3 ms 23,789.1 tok/s

Delta: TTFT -2.89%, input throughput +2.98%.

GSM8K (full 1,319, greedy, 5-shot)

Re-run after the E8M0 conversion documentation update on the exact old
checkpoint and the same TP8/PP1 stack used for the decode table. Prefix caching
was disabled; both arms used seed 42 and concurrency 64. The only A/B variable
was VLLM_ROCM_USE_AITER_FP8BMM=0/1.

Path Accuracy Correct Invalid Output tok/s
BF16 wo_a 94.7688% 1250/1319 0 879.44
FP8 wo_a 94.7688% 1250/1319 0 889.21

The FP8 WO_A path is accuracy-neutral in this full-set run, with zero invalid
responses in both arms. Output token throughput improves by 1.11%.

Summary on this checkpoint

Measurement Result
TP8/PP1 throughput +1.29% mean (+0.39% to +3.17%)
TP1/PP8 throughput +6.09% mean (+4.12% to +6.78%)
100K prefill TTFT -2.89%
GSM8K full 1319 94.7688% for both arms; zero invalid responses

The direction and magnitude match the 0813 numbers above: a small gain at
TP8/PP1 and a larger one at TP1/PP8, with accuracy at or above the BF16
reference.

@mergify mergify Bot added deepseek Related to DeepSeek models DSv4 rocm Related to AMD ROCm labels Sep 2, 2026
@github-project-automation github-project-automation Bot moved this to Todo in AMD Sep 2, 2026
@LiuYinfeng01
LiuYinfeng01 force-pushed the rocm-dsv4-fp8-woa-mxscale branch from c9628e5 to a9cf3bc Compare September 2, 2026 05:29
@LiuYinfeng01
LiuYinfeng01 marked this pull request as ready for review September 2, 2026 07:47

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

@LiuYinfeng01
LiuYinfeng01 marked this pull request as draft September 2, 2026 07:53
@Fangzhou-Ai

Copy link
Copy Markdown
Collaborator

please do a full 1319 gsm8k test, current result seems to be lower than the baseline. We expect the full test score near 0.96

@LiuYinfeng01
LiuYinfeng01 marked this pull request as ready for review September 3, 2026 02:01
@LiuYinfeng01

LiuYinfeng01 commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

please do a full 1319 gsm8k test, current result seems to be lower than the baseline. We expect the full test score near 0.96

Path flexible-extract strict-match
BF16 wo_a 0.9621 ± 0.0053 0.9629 ± 0.0052
FP8 wo_a 0.9651 ± 0.0051 0.9651 ± 0.0051

@shen-shanshan shen-shanshan self-assigned this Sep 12, 2026
@shen-shanshan shen-shanshan added the verified Run pre-commit for new contributors without triggering other tests label Sep 12, 2026

@shen-shanshan shen-shanshan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall LGTM, left some minor suggestions. Btw, could you please add some performance comparison about TPOT, not only TTFT and throughput?

Comment thread vllm/models/deepseek_v4/amd/rocm.py Outdated
Comment on lines +610 to +632
if (
weight is None
or scale is None
or weight.dim() != 2
or scale.dim() != 2
or weight.dtype not in (torch.float8_e4m3fn, torch.float8_e4m3fnuz)
):
return

groups = self.n_local_groups
out_per_group = self.o_lora_rank
out_features, in_features = weight.shape
if (
out_features != groups * out_per_group
or out_per_group % 128 != 0
or in_features % 128 != 0
or scale.shape != (out_features // 128, in_features // 128)
):
return

e8m0_scale = _wo_a_block_scale_to_e8m0(scale)
if e8m0_scale is None:
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should add some info about the failure reason, instead of directly return them.

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.

Concurrency BF16 wo_a output tok/s FP8 wo_a output tok/s Throughput change BF16 TPOT FP8 TPOT TPOT change BF16 TTFT FP8 TTFT
1 63.35 64.55 +1.89% 15.43 ms 15.15 ms -1.85% 375.74 ms 367.52 ms
4 213.53 219.99 +3.02% 17.76 ms 17.23 ms -2.97% 1009.75 ms 986.39 ms
16 662.89 677.29 +2.17% 22.27 ms 21.75 ms -2.34% 1899.74 ms 1908.81 ms
64 1386.37 1431.21 +3.23% 42.15 ms 40.72 ms -3.39% 3999.62 ms 3984.62 ms

Comment thread vllm/models/deepseek_v4/amd/rocm.py Outdated
Comment on lines +45 to +63
def _wo_a_block_scale_to_e8m0(scale: torch.Tensor) -> torch.Tensor | None:
"""Return raw E8M0 exponent bytes for a WO_A block-scale tensor."""
if scale.dtype == torch.float8_e8m0fnu:
return scale.view(torch.uint8).contiguous()
if scale.dtype == torch.uint8:
return scale.contiguous()
if not scale.dtype.is_floating_point:
return None

scale_f32 = scale.detach().float()
if not bool(torch.isfinite(scale_f32).all()) or bool((scale_f32 <= 0).any()):
return None
exponent = torch.round(torch.log2(scale_f32))
if not torch.equal(torch.exp2(exponent), scale_f32):
return None
biased = exponent.to(torch.int32) + 127
if int(biased.min()) < 0 or int(biased.max()) > 255:
return None
return biased.to(torch.uint8).contiguous()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please add some explanations or references of why the conversion rules look like this?

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.

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.

vLLM loader compatibility / validation
↓
raw uint8 E8M0 normalization
↓
inverse_rope_group_quant -> activation raw E8M0
↓
batched_gemm_a8w8_mxscale
↓
AITER tuned dispatch -> OPUS kernel

@shen-shanshan shen-shanshan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall LGTM, leaving one final suggestion to make the code more robust. Ready label added, you can trigger the CI yourself after modification.

Comment thread vllm/models/deepseek_v4/amd/rocm.py Outdated
del mxscale_op, inverse_quant_op

weight = getattr(self.wo_a, "weight", None)
scale = getattr(self.wo_a, "weight_scale_inv", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you align this with:

wo_a_scale_param = get_fp8_block_weight_scale(wo_a)
if wo_a_scale_param is None:
# ModelOpt MXFP8 stores the multiplicative E8M0 scale without the
# historical ``_inv`` suffix.
wo_a_scale_param = getattr(wo_a, "weight_scale", None)

Use:
def get_fp8_block_weight_scale(layer: torch.nn.Module) -> torch.Tensor | None:
"""Return the block-FP8 weight scale for supported quant methods."""
# Local import avoids a circular import: quark_w8a8_fp8 imports this module.
from vllm.model_executor.layers.quantization.quark.schemes.quark_w8a8_fp8 import (
QuarkW8A8Fp8PerBlock,
)
if isinstance(
getattr(layer, "scheme", None),
QuarkW8A8Fp8PerBlock,
):
return getattr(layer, "weight_scale", None)
return getattr(layer, "weight_scale_inv", None)

@shen-shanshan shen-shanshan added the ready ONLY add when PR is ready to merge/full CI is needed label Sep 20, 2026
@github-actions

Copy link
Copy Markdown

✅ @LiuYinfeng01, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • Your branch must contain every commit currently on its upstream target branch. Merge or rebase onto the latest target branch, then rerun the command. Append --allow-stale to a run command to test an outdated branch at your own risk.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
Resolve the wo_a block scale through get_fp8_block_weight_scale() with the
ModelOpt MXFP8 weight_scale fallback, matching rocm_aiter_mla_sparse, and
use is_fp8() for the weight dtype check. Log the concrete reason whenever
the FP8 WO_A fast path falls back to BF16.

Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
@LiuYinfeng01
LiuYinfeng01 force-pushed the rocm-dsv4-fp8-woa-mxscale branch from 37a5796 to d910c96 Compare September 20, 2026 05:16
@shen-shanshan

Copy link
Copy Markdown
Contributor

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #90044 for commit c40f7fb71014.

@shen-shanshan
shen-shanshan enabled auto-merge (squash) September 20, 2026 06:43
@shen-shanshan
shen-shanshan merged commit 0e110f6 into vllm-project:main Sep 20, 2026
88 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in AMD Sep 20, 2026
michael-gpu pushed a commit to practical-inference/vllm-ampere-extended that referenced this pull request Oct 20, 2026
Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
Co-authored-by: Shanshan Shen <467638484@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deepseek Related to DeepSeek models DSv4 ready ONLY add when PR is ready to merge/full CI is needed rocm Related to AMD ROCm verified Run pre-commit for new contributors without triggering other tests

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants