优化Qwen3 - FP8模型L40S卡性能提升18%

我在之前的博客中介绍了自己写的 SM89 上的 FP8 Blockwise GEMM CUDA kernel。后来看到 Qwen3 官方文档中的 H20 卡上的性能测试数据,本文仿照 Qwen3 文档的测试方法,对新 Kernel 进行更多 Qwen3 FP8 模型在 L40S 的对比性能测试。

总结放最前

对下面的速度指标可以有几个观察:

  1. 新 Kernel 平均提升性能 18%。依照 Qwen3 官方的测试用例,平均下来新 Kernel 对端到端推理的性能优化幅度是 18%。
  2. 速度优化的比例随着模型参数的增加先升后降。大概是因为最小模型的参数量太小,调度占比更高;而更大的模型参数量太大,访存占比升高。
  3. A3B 的模型优于 4B 的模型。但我在更多的测试后发现,A3B 仅在 batch_size 2 以下性能优于 4B,在超过 2 以后,A3B 的性能曲线增长远不如 4B 的性能曲线。我的测试在 L40S 上,4B 的最大吞吐大约是 A3B 的 1.7 倍,8B 的最大吞吐大约是 A3B 的 1.4 倍。这大概是因为 A3B 模型计算时需要更多的显存带宽。
  4. 新 Kernel 下 L40S 的平均吞吐是 H20 的 56% (triton:47%)。可以用作单卡运行 Qwen3 模型时,L40S 和 H20 的性价比大略预估。

下面是具体的测试环境和测试数据:

1. 测试环境

硬件使用的是 NVIDIA L40S,评测的每个模型都跑在 1 张 L40S 卡上。Docker 使用的是 SGLang 的开发镜像:lmsysorg/sglang:dev。

SGLang 和 SGL-Kernel 可以根据我的这篇博客中介绍的方法自行编译,或者从下面链接下载我编译好的 Python wheel 包: https://github.com/solrex/sglang/releases/tag/sm89-fp8 然后通过下面命令安装:

pip install sgl_kernel-0.1.5-cp39-abi3-linux_x86_64.whl
pip install sglang-0.4.6.post5-py3-none-any.whl

2. 测试设置

SGLang 的测试设置基本与 Qwen3 官方的测试设置相同,启动参数主要设置了这些。因为下面测试的 batch size 只有 1,所以 --cuda-graph-max-bs 只设置了 8,正常批量压测时最好设置到 64 以上。

python3 -m sglang.launch_server --model-path MODEL_PATH --tp 1 --allow-auto-truncate --enable-mixed-chunk --context-length 40960 --enable-torch-compile --torch-compile-max-bs 8 --cuda-graph-max-bs 8 --port ****

使用以上方式启动,使用的是默认的 FP8 Blockwise Triton Kernel;如果希望使用新的 CUDA 实现的 FP8 Blockwise Kernel,只需要在启动前增加环境变量:

export SGLANG_SUPPORT_CUTLASS_BLOCK_FP8=1

所以后面的测试,主要是比较 Triton 和 CUDA 实现的 FP8 Blockwise GEMM Kernel 的性能区别。

指标中加上“triton”前后缀的,即为 Triton Kernel 的指标;指标中加上“cuda”前后缀的,即为我新实现的 CUDA Kernel 的指标。

3. 准确率速测

首先测一下新 Kernel 的实现是否有严重的问题,这里使用 SGLang 代码库中的 benchmark/gsm8k 进行一下速测。速测有 512 token 截断,测试 500 条,仅用于粗略比较是否有 bug,不能充分说明模型的能力差距。

速测命令如下,通过 port 将请求发送给不同的模型:

python3 bench_sglang.py --num-questions 500 --port ****
模型triton-准确率cuda-准确率
Qwen3-0.6B-FP841.0%43.4%
Qwen3-1.7B-FP869.0%68.8%
Qwen3-4B-FP885.4%86.2%
Qwen3-8B-FP892.0%92.2%
Qwen3-14B-FP889.2%86.2%
Qwen3-32B-FP880.8%83.8%
Qwen3-30B-A3B-FP890.6%88.2%

可以看到我写的 CUDA Kernel 在准确率上与官方的 triton Kernel 没有明显的差距。

4. 性能测试结果

为了与 Qwen3 官方的性能指标进行对齐,我采用了完全相同的请求参数,即:batch_size 固定为 1,生成长度固定为 2048,输入长度使用 1、6144、14336、30720,速度指标使用 (输入+输出)/时间。

Qwen3 官方如此设置大概是为了照顾 Transformers 库,但这种测试方式并不常见。batch_size 为 1 很难测出来最大吞吐,30K 的输入长度在业务场景下也不算常见,最终指标受 prefill 影响很大。为了显示生成的性能,我增加了一列平均 ITL 指标。

指标列说明:

  • 输入:输入的长度,单位是 token;
  • H20:Qwen3 官方文档中 H20 的速度,单位是 tokens/s;
  • L40S-triton:L40S 上 SGLang 官方 triton FP8 blockwise GEMM 实现的速度,单位是 tokens/s;
  • L40S-cuda:L40S 上替换为我实现的 CUDA Kernel 实现的速度,单位是 tokens/s;
  • 速度提升的百分比:L40S-cuda / L40S-triton - 100%
  • ITL-triton:SGLang 官方 triton 实现下生成每个 token 的平均间隔,单位是毫秒 ms;
  • ITL-cuda:我的 CUDA 实现下生成每个 token 的平均间隔,单位是毫秒 ms;

4.1 Qwen3-0.6B-FP8 (SGLang sm89-fp8 branch)

输入H20L40S-tritonL40S-cuda速度提升ITL-tritonITL-cuda
1458.03250.36303.09+21.1%3.983.28
61441572.95805.75936.57+16.0%4.944.24
143362689.081270.001427.09+12.0%6.265.57
307203819.861773.351918.70+8.0%8.978.28

4.2 Qwen3-1.7B-FP8 (SGLang sm89-fp8 branch)

输入H20L40S-tritonL40S-cuda速度提升ITL-tritonITL-cuda
1333.90148.35197.24+32.9%6.735.05
61441198.20518.43661.93+27.7%7.696.01
143362095.61877.281071.52+22.1%9.087.43
307203165.321356.381576.85+16.2%11.7410.07

4.3 Qwen3-30B-A3B-FP8 (SGLang sm89-fp8 branch)

输入H20L40S-tritonL40S-cuda速度提升ITL-tritonITL-cuda
1155.55102.83117.38+14.2%9.708.49
6144551.34377.75426.18+12.8%10.549.34
14336945.13680.58757.84+11.4%11.7010.50
307201405.911133.151238.20+9.3%14.0512.85

4.4 Qwen3-4B-FP8 (SGLang sm89-fp8 branch)

输入H20L40S-tritonL40S-cuda速度提升ITL-tritonITL-cuda
1200.6175.19103.18+37.2%13.289.67
6144662.26278.90370.24+32.8%14.3110.76
143361066.23498.17638.03+28.1%16.0212.50
307201467.71821.341002.76+22.1%19.4215.90

4.5 Qwen3-8B-FP8 (SGLang sm89-fp8 branch)

输入H20L40S-tritonL40S-cuda速度提升ITL-tritonITL-cuda
1150.2553.4766.56+24.5%18.6915.00
6144516.64202.20248.00+22.7%19.7516.10
14336859.92371.91447.97+20.5%21.4717.82
307201242.24641.60751.65+17.2%24.8821.23

4.6 Qwen3-14B-FP8 (SGLang sm89-fp8 branch)

输入H20L40S-tritonL40S-cuda速度提升ITL-tritonITL-cuda
197.1134.2039.30+14.9%29.2225.46
6144342.95130.36148.56+13.9%30.6526.89
14336587.33246.01278.15+13.1%32.4828.72
30720880.72441.62492.80+11.6%36.1732.40

4.7 Qwen3-32B-FP8 (SGLang sm89-fp8 branch)

输入H20L40S-tritonL40S-cuda速度提升ITL-tritonITL-cuda
146.1715.9518.31+14.8%62.7054.59
6144165.7161.3670.13+14.3%65.1456.98
14336287.60117.39133.28+13.5%68.0959.96
30720436.59FAILFAIL

一个能提升 DeepSeek/Qwen3 在 L40S/L20 卡上 FP8 量化计算性能的 CUDA Kernel

从 DeepSeek-V3 开始,blockwise FP8 量化逐渐进入大家的视野,DeepGEMM 的开源更是扩大了这种量化方式的影响。但无论是 DeepGEMM 还是 CUTLASS,目前都只在 Hopper (SM_90) 或以后的架构上支持 blockwise FP8 量化。

Hopper 之前的 Ada Lovelace (SM_89) 一代的卡,比如 L40S、L20、40x0,虽然也支持 FP8 计算,但目前大部分实现都是通过 Python 写的 Triton Kernel 实现 blockwise FP8 量化计算。

支持 SM_89 上 FP8 Blockwise 量化的 CUDA Kernel

为了优化 blockwise FP8 量化在 SM_89 系列 GPU 上的计算性能,我基于 CUTLASS 实现了支持 blockwise FP8 量化的 CUDA Kernel,源代码在:https://github.com/NVIDIA/cutlass/pull/2328

这个 Kernel 不仅支持 SM_89 之后 GPU 上的 FP8 blockwise 量化,还支持 SM_80 之后 GPU 上的 INT8 blockwise 量化。使用方法可以参考 examples/85_ada_ampere_gemm_with_blockwise_scaling 下的用例。

在矩阵计算规模较小时,这个 Kernel 在 L40S 卡上的计算性能甚至可以超过 H800 卡上的 warp specialized + tma 实现。在矩阵计算规模较大时,例如 M=4096、N=7168、K=2048 时,在 L40S 上的计算吞吐是 260 TFLOPS,对比 DeepGEMM 的 1215 TFLOPS (https://github.com/deepseek-ai/DeepGEMM/pull/86),大约是 1 : 4.67,而 L40S 对比 H800 的 FP8 算力是 733 TFLOPS : 3958 TFLOPS,大约是 1 : 5.39。可以看到这个 kernel 的性能还算过得去。

$./examples/85_ada_ampere_gemm_with_blockwise_scaling/85a_ada_fp8_gemm_with_groupwise_scaling_cute --m=4096 --n=7168 --k=2048
Problem Size: 4096x7168x2048x1
Tile shape (M, N, K): _64, _128, _128
ScaleGranularityM: 1 (ScaleMsPerTile: 64)
ScaleGranularityN: 128 (ScaleNsPerTile: 1)
Running...
Result MSE: 4.56599e-06, MRE: 15.6246, greatest error: 0.0286751
Disposition: Passed
Avg runtime: 0.462145 ms
GFLOPS: 260220

SGLang 集成

我在自己的 SGLang 分支的 sgl-kernel 中集成了这个 Kernel,在 L40S 卡上对比 SGLang 已有的 Triton 实现性能有大幅提升,最高提升幅度可达 60%。以下是一个例子,benchmark 性能的单位是 GB/s。

deepseek-ai/DeepSeek-V3 N=24576 K=7168: 
Using default W8A8 Block FP8 kernel config. Performance might be sub-optimal! Config file not found at /workspace/git/sglang/python/sglang/srt/layers/quantization/configs/N=24576,K=7168,device_name=NVIDIA_L40S,dtype=fp8_w8a8,block_shape=[128, 128].json
fp8 blockwise scaled matmul:
batch_size sgl-kernel sglang triton
0 1.0 927.460912 905.494708
1 8.0 7562.373485 7500.555847
2 16.0 15083.308993 15001.111694
3 32.0 29920.694988 29920.694988
4 64.0 59195.447003 59357.498347
5 128.0 112069.375528 114995.465571
6 256.0 208242.387702 166830.544152
7 512.0 268966.505252 185836.556807
8 1024.0 286227.561402 187468.003955
9 2048.0 300380.325376 187069.874028
10 4096.0 297778.251151 189459.651356

分支源代码在 https://github.com/solrex/sglang/tree/sm89-fp8 ,需要自行编译 sgl-kernel,benchmark 脚本是 sgl-kernel/benchmark/bench_fp8_blockwise_gemm.py,感兴趣的朋友可以自行验证。

Qwen3 FP8 模型

因为 Qwen3 的 FP8 版本参数也是采用的 blockwise 量化,可以使用 Qwen3 量化模型对比新的 CUDA Kernel 和 Triton Kernel 的端到端性能。在 L40S 上,使用我实现的 CUDA Kernel 比原 Triton Kernel,可以提升 Qwen3-8B-FP8 模型吞吐 12% 左右

# Triton Kernel
============ Serving Benchmark Result ============
Backend: sglang-oai
Traffic request rate: 64.0
Max reqeuest concurrency: 64
Successful requests: 512
Benchmark duration (s): 139.73
Total input tokens: 262144
Total generated tokens: 262144
Total generated tokens (retokenized): 262140
Request throughput (req/s): 3.66
Input token throughput (tok/s): 1876.12
Output token throughput (tok/s): 1876.12
Total token throughput (tok/s): 3752.25
Concurrency: 63.73
----------------End-to-End Latency----------------
Mean E2E Latency (ms): 17390.95
Median E2E Latency (ms): 17425.19
---------------Time to First Token----------------
Mean TTFT (ms): 1887.69
Median TTFT (ms): 1911.72
P99 TTFT (ms): 3277.92
---------------Inter-Token Latency----------------
Mean ITL (ms): 30.59
Median ITL (ms): 28.11
P95 ITL (ms): 31.14
P99 ITL (ms): 31.97
Max ITL (ms): 3035.59
==================================================

# CUDA Kernel
============ Serving Benchmark Result ============
Backend: sglang-oai
Traffic request rate: 64.0
Max reqeuest concurrency: 64
Successful requests: 512
Benchmark duration (s): 123.98
Total input tokens: 262144
Total generated tokens: 262144
Total generated tokens (retokenized): 262144
Request throughput (req/s): 4.13
Input token throughput (tok/s): 2114.39
Output token throughput (tok/s): 2114.39
Total token throughput (tok/s): 4228.78
Concurrency: 63.66
----------------End-to-End Latency----------------
Mean E2E Latency (ms): 15416.44
Median E2E Latency (ms): 15475.84
---------------Time to First Token----------------
Mean TTFT (ms): 1407.69
Median TTFT (ms): 1370.36
P99 TTFT (ms): 2305.31
---------------Inter-Token Latency----------------
Mean ITL (ms): 27.68
Median ITL (ms): 25.81
P95 ITL (ms): 28.79
P99 ITL (ms): 29.88
Max ITL (ms): 2201.85
==================================================

DeepSeek V3/R1 模型

基于我之前的工作《在 32 张 L40S/L20 上运行 DeepSeek-R1/V3 原版 FP8 模型》,在 32 张 L40S 上,单请求下,新 Kernel 的 otps 可以从 33 提升到 40 左右;压力下大概提升 5% 左右,下面是压测数据。

# Triton Kernel
============ Serving Benchmark Result ============
Backend: sglang-oai
Traffic request rate: 128.0
Max reqeuest concurrency: 128
Successful requests: 256
Benchmark duration (s): 98.63
Total input tokens: 51200
Total generated tokens: 51200
Total generated tokens (retokenized): 50971
Request throughput (req/s): 2.60
Input token throughput (tok/s): 519.11
Output token throughput (tok/s): 519.11
Total token throughput (tok/s): 1038.22
Concurrency: 127.26
----------------End-to-End Latency----------------
Mean E2E Latency (ms): 49032.00
Median E2E Latency (ms): 49199.08
---------------Time to First Token----------------
Mean TTFT (ms): 8294.87
Median TTFT (ms): 8306.07
P99 TTFT (ms): 15599.09
---------------Inter-Token Latency----------------
Mean ITL (ms): 205.47
Median ITL (ms): 183.05
P95 ITL (ms): 187.48
P99 ITL (ms): 243.63
Max ITL (ms): 11274.90
==================================================

# CUDA Kernel
============ Serving Benchmark Result ============
Backend: sglang-oai
Traffic request rate: 128.0
Max request concurrency: 128
Successful requests: 256
Benchmark duration (s): 93.56
Total input tokens: 51200
Total generated tokens: 51200
Total generated tokens (retokenized): 50980
Request throughput (req/s): 2.74
Input token throughput (tok/s): 547.24
Output token throughput (tok/s): 547.24
Total token throughput (tok/s): 1094.48
Concurrency: 127.28
----------------End-to-End Latency----------------
Mean E2E Latency (ms): 46516.28
Median E2E Latency (ms): 46715.28
---------------Time to First Token----------------
Mean TTFT (ms): 8396.97
Median TTFT (ms): 9343.49
P99 TTFT (ms): 13201.02
---------------Inter-Token Latency----------------
Mean ITL (ms): 192.20
Median ITL (ms): 172.65
P95 ITL (ms): 180.43
P99 ITL (ms): 217.66
Max ITL (ms): 10399.15
==================================================

验证方法

使用我的 SGLang sm89-fp8 分支,编译安装 sgl-kernel,安装 sglang。或者也可以下载我编译好的 wheel 直接安装。

git clone -b sm89-fp8 https://github.com/solrex/sglang.git
cd sglang/sgl-kernel
make build # 编译安装本地 sgl-kernel,耗时较久
cd ..
pip install -e "python[all]" # 安装本地 sglang

在启动 SGLang 服务时,可以通过是否设置 SGLANG_SUPPORT_CUTLASS_BLOCK_FP8 环境变量,控制使用 CUDA Kernel 还是 Triton Kernel。启用 CUDA Kernel 的命令形如:

SGLANG_SUPPORT_CUTLASS_BLOCK_FP8=1 python3 -m sglang.launch_server --model-path /workspace/Qwen3-8B-FP8/ --port 8080 --tp 1 --host 0.0.0.0 --allow-auto-truncate --enable-torch-compile --torch-compile-max-bs 64 --cuda-graph-max-bs 64

可以通过启动日志中是否有 "Using default W8A8 Block FP8 kernel config." 类似日志来判断使用的 Kernel 是哪个。当下面这条日志存在时,代表使用的是 Triton Kernel:

[2025-06-03 18:20:24] Using default W8A8 Block FP8 kernel config. Performance might be sub-optimal! Config file not found at /workspace/git/sglang/python/sglang/srt/layers/
quantization/configs/N=6144,K=4096,device_name=NVIDIA_L40S,dtype=fp8_w8a8,block_shape=[128, 128].json

压测使用的脚本是:

python3 -m sglang.bench_serving --backend sglang-oai --dataset-path /workspace/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json --dataset-name random --random-range-ratio 1 --random-input-len 512 --random-output-len 512  --request-rate 64 --num-prompt 512 --max-concurrency 64 --host 127.0.0.1 --port 8080

在实现这个 kernel 时我遇到和解决了很多问题,现在也不敢说没有 bug。如果你在验证的时候发现任何问题,请不吝留言指教。

GLM-4-Z1 模型设计做错了一件事

DeepSeek-R1 API 在 OpenAI 兼容 API 基础上为思考模型增加了 "reasoning_content" 字段,可以将 "<think>" "</think>" 中间的内容独立返回,非思考部分通过 "content" 字段返回。

SGLang/vLLM 等框架也支持了这样的返回,比如 SGLang 可以在启动时增加参数 "--reasoning-parser deepseek-r1",让服务器像 DeepSeek-R1 API 那样返回。

Chatbox 等支持 OpenAI 兼容 API 的聊天 APP,对 DeepSeek-R1 风格的返回结果支持也更好,上面截图就是一个例子,当思考部分以 "reasoning_content" 字段返回时,显示效果与正文不同,背景是灰色,而且可折叠。

上一篇博客中提到我给 SGLang 添加了 GLM-Z1 模型的支持,然后我发现 GLM-Z1 系列模型无法做到思考内容和非思考内容分开输出。如果你在启动服务时增加了 "--reasoning-parser deepseek-r1" 参数,那么所有生成的内容都会放在 "reasoning_content" 字段返回,而 "content" 为空。Chatbox 显示效果如下所示:

我仔细研究了一下,发现了问题在 GLM-Z1 模型上:GLM-Z1 系列模型在设计时,没有像 DeepSeek-R1 那样将 "</think>" 编码为一个独立的 Token。

GLM-Z1 系列模型输出的 "</think>" 由 3 个 Token 组成:"</"、 "think" 和 ">",这就会导致 框架在流式逐字输出时,没法简单地判断一个新生成的 Token 是否为 "</think>",以决策在何时结束 "reasoning_content" 字段。

可以通过下面代码简单验证:

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("GLM-Z1-9B-0414")
print(tokenizer("</think>\n"))

DeepSeek-R1 是在 tokenizer.json 中标记了 "</think>" 作为 "added_tokens",QwQ-32B 则把 "</think>" 放在了 added_tokens.json 中。这就意味着,DeepSeek-R1 和 QwQ-32B 输出的 "</think>" 是 1 个 token,很方便框架去完整地比对它是否存在。框架的确也可以兼容非一个 token 的 "</think>",但编码更为复杂,效率也会偏低。

建议思考模型的设计者都注意一下这个小小的细节。

有关 GLM-4-0414 的 SGLang 推理支持

在智谱的 GLM-4-0414 系列模型发布后,我观察到一个有意思的现象。GLM-4-0414 相较于 GLM-4 修改了模型结构,初期仅支持通过 transformers 库推理,但你去搜索它有哪些推理支持的话,什么也找不到,被大量的垃圾自媒体文章淹没,很多相较于官方模型仓库的 README 来说都是 0 信息量。

有些垃圾自媒体,可能连个 AI 都不如。

说回来 GLM-4-0414,虽然它名字看起来是 GLM-4 的延续,但是仔细看 config.json 你就会发现,GLM-4 的模型结构是 ChatGLMModel,而 GLM-4-0414 的模型结构是 Glm4ForCausalLM。这就导致很多推理框架都要对其进行重新适配。

vllm 可能是最早支持 GLM-4-0414 的开源框架,看起来是智谱员工提的 PR,但是第一版实现有两个 bug,会导致模型加载失败或者输出错误结果。由于对 GLM-Z1-9B-0414 模型在报告中声称大幅优于 DeepSeek-R1-Distill-Qwen-14B 过于好奇,我忍不住自己尝试在 SGLang 里适配了一下 GLM4-4-0414,见 PR#5485

嗯,学到了很多东西,也踩了两个 bug 其中的一个,哭。就说说这两个 bug 吧,其实都是源自于对 vllm/SGLang 库算子的不了解。

在 transformers 库中,很多算子仅实现其算子名表示的朴素的功能,但是 vllm/SGLang 代码库的一些算子,除了其朴素功能以外,往往还通过更多的参数实现对前后计算的融合、简化,导致其实际计算需要深入探究。

BUG 1: 融合 RMSNorm

以比较基础的 LLaMA 为例,transformers 中的 LLaMA DecoderLayer 的实现是这样的(删去了一些无关细节),这个跟大家理解的 Transformer 模型结构伪代码是容易对上的。

def forward(): residual = hidden_states hidden_states = self.input_layernorm(hidden_states) # Self Attention hidden_states, self_attn_weights = self.self_attn() hidden_states = residual + hidden_states # Fully Connected residual = hidden_states hidden_states = self.post_attention_layernorm(hidden_states) hidden_states = self.mlp(hidden_states) hidden_states = residual + hidden_states outputs = (hidden_states,) return outputs

但是 vllm/SGLang 中的实现是这样的:

def forward(): # Self Attention if residual is None: residual = hidden_states hidden_states = self.input_layernorm(hidden_states) else: hidden_states, residual = self.input_layernorm(hidden_states, residual) hidden_states = self.self_attn(...) # Fully Connected hidden_states, residual = self.post_attention_layernorm(hidden_states, residual) hidden_states = self.mlp(hidden_states) return hidden_states, residual

粗看你会有点懵,仔细研究就会发现,SGLang 里面通过给 RMSNorm 传入第二个参数,实现了 Norm 与 Add 的融合。但是这种融合需要调整计算顺序,影响了参数和返回值的类型,并且也影响了最后一次 model.norm() 的计算。

相似的还有 SiluAndMul() 算子。

BUG 2: get_rope 参数

GLM-4-0414 的 config.json 中提供了 partial_rotary_factor 参数,作用于 head_dim 上。有两种应用方法,一种是提前计算好 rotary_dim = int(partial_rotary_factor * self.head_dim),然后把这个参数传进去;另一种是令 rotary_dim = head_dim,然后传入 partial_rotary_factor。

def get_rope( head_size: int, rotary_dim: int, max_position: int, base: int, is_neox_style: bool = True, rope_scaling: Optional[Dict[str, Any]] = None, dtype: Optional[torch.dtype] = None, partial_rotary_factor: float = 1.0, ) -> RotaryEmbedding: ... if partial_rotary_factor < 1.0: rotary_dim = int(rotary_dim * partial_rotary_factor)

我重复犯的这个错误就是将 partial_rotary_factor 应用了两遍,rotary_dim 也计算了,partial_rotary_factor 参数也传了(因为参考了 vllm 实现和旧的 SGLang chatglm 实现,甚至看到别人提交的 bugfix 都不认为这里有错),其实就相当于应用了 partial_rotary_factor * partial_rotary_factor。这个 BUG 的现象就是导致 GLM-4 模型的输出大概率陷入死循环而无法结束。

BTW:transformers 库里的 glm4 代码没有读这个参数,而是硬编码在 apply_rotary_pos_emb() 算子中,所以这不是一个可调参数。

PR#5485 还在 Review 中,有需要的朋友可以使用 https://github.com/solrex/sglang/tree/glm4 这个分支支持 GLM-4-0414 系列模型在 SGLang 的推理。

git clone -b glm4 https://github.com/solrex/sglang.git
cd sglang
pip install -e "python[all]" --find-links https://flashinfer.ai/whl/cu124/torch2.5/flashinfer-python
python3 -m sglang.launch_server --model /workspace/GLM-Z1-9B-0414 --enable-torch-compile --torch-compile-max-bs 128 --cuda-graph-max-bs 128 --host 0.0.0.0 --port 8000

在 32 张 L40S/L20 上运行 DeepSeek-R1/V3 原版 FP8 模型

上文讲到,FP8 模型之所以无法 TP32 运行,主要因为 DeepSeek R1/V3 模型保存的参数是 FP8 128x128 量化的。Attention 还好,128 个头做 TP16 或者 TP32 都没问题,问题主要出在专家的计算上。

前三层 MLP 的 intermediate_size 是 18432,18432 做 TP16 是 1152,相当于 9 个 128。但如果做了 TP32,就是 4.5 个 128,这会导致参数无法切分在 128 边界上,无法支持按 128 block 量化。路由专家也类似,moe_intermediate_size 做 TP32 相当于 0.5 个 128。

缩小 DeepSeek-R1/V3 的量化块到 64x64

如果想支持 TP32,一个很显然的路径就是把量化方式改成按 64x64 分块量化。本来这是一个比较复杂的操作,但我想了一个取巧的办法:直接把 128x128 的缩放系数,复制到 4 份。

为了方便理解这个方案,我画了一张图。假设我们有一个 4x8 的 INT32 矩阵,按照 4x4 block 量化到 INT8,它会分成 1x2 个 4x4 的块,每块一个缩放系数,那就是 1x2 个缩放系数。如下图所示,第一个块的缩放系数是 7.3465,它是通过第一个块里的最大绝对值 |-933|/127 得到的,同理第二个缩放系数来自 974/127。

那如果我想将它的量化 block 缩小到 2x2,理论上我应该计算每个 2x2 block 的最大绝对值,然后 /127 得到缩放系数,这样精度损失最小。可是我嫌麻烦,偷个懒,我直接把 4x4 的缩放系数复制 4 份,虽然精度有损失,但好处是在计算上与 4x4 的量化结果完全一致。换句话说,就是原汁原味,纯血参数。

将这个逻辑迁移到 DeepSeek FP8 量化的 128x128 block 缩小到 64x64,原理是一样的,也是将 scale 参数矩阵进行 2x2 等值扩充。通过非常简单的参数处理,就能够实现将 DeepSeek 原始模型转成 64x64 的分块量化,然后就可以用 SGLang 加载运行了。

运行方法

我们以昨天发布的 DeepSeek-V3-0324 为例,逐步说明如何使用这种方法在 L40S 和 L20 上运行 FP8 满血+纯血版的 DeepSeek-V3-0324,不需要等待美团再发布 INT8 版本。

假设你已经下载好了模型,在 /workspace/DeepSeek-V3-0324/。那你需要先下载我的开发分支,并通过源代码安装它(如果遇到困难,建议你在 SGLang 的开发 Docker 中执行它):

git clone -b l40s-dsfp8 https://github.com/solrex/sglang.git
cd sglang
pip install -e "python[all]" --find-links https://flashinfer.ai/whl/cu124/torch2.5/flashinfer-python

然后用下面这个脚本,将 128x128 量化的 DeepSeek-V3-0324,转换到 64x64 量化的 DeepSeek-V3-0324-Block64x64:

python3 scripts/resize_block_size.py /workspace/DeepSeek-V3-0324/

当你在 4 台(或 8 台)机器上都完成了 SGLang 安装和参数拷贝后,就可以用下面的命令来启动 SGLang 服务了。注意替换 MASTER_IP、mlx5_? 和 TCP_IFACE 到正确的值。

# MASTER_IP: 主节点 IP
# TCP_IFACE: 主网卡接口名,可通过 ifconfig 获取

# 主节点
NCCL_DEBUG=INFO NCCL_IB_GID_INDEX=3 NCCL_IB_HCA=mlx5_? NCCL_SOCKET_IFNAME=TCP_IFACE GLOO_SOCKET_IFNAME=TCP_IFACE python3 -m sglang.launch_server --model /workspace/DeepSeek-V3-0324-Block64x64/ --tp 32 --dist-init-addr MASTER_IP:5000 --nnodes 4 --node-rank 0 --trust-remote --enable-torch-compile --torch-compile-max-bs 32 --cuda-graph-max-bs 32 --host 0.0.0.0 --port 8000

# 从节点 1
NCCL_DEBUG=INFO NCCL_IB_GID_INDEX=3 NCCL_IB_HCA=mlx5_? NCCL_SOCKET_IFNAME=TCP_IFACE GLOO_SOCKET_IFNAME=TCP_IFACE python3 -m sglang.launch_server --model /workspace/DeepSeek-V3-0324-Block64x64/ --tp 32 --dist-init-addr MASTER_IP:5000 --nnodes 4 --node-rank 1 --trust-remote --enable-torch-compile --torch-compile-max-bs 32 --cuda-graph-max-bs 32

# 从节点 2
NCCL_DEBUG=INFO NCCL_IB_GID_INDEX=3 NCCL_IB_HCA=mlx5_? NCCL_SOCKET_IFNAME=TCP_IFACE GLOO_SOCKET_IFNAME=TCP_IFACE python3 -m sglang.launch_server --model /workspace/DeepSeek-V3-0324-Block64x64/ --tp 32 --dist-init-addr MASTER_IP:5000 --nnodes 4 --node-rank 2 --trust-remote --enable-torch-compile --torch-compile-max-bs 32 --cuda-graph-max-bs 32

# 从节点 3
NCCL_DEBUG=INFO NCCL_IB_GID_INDEX=3 NCCL_IB_HCA=mlx5_? NCCL_SOCKET_IFNAME=TCP网卡 GLOO_SOCKET_IFNAME=TCP网卡 python3 -m sglang.launch_server --model /workspace/DeepSeek-V3-0324-Block64x64/ --tp 32 --dist-init-addr MASTER_IP:5000 --nnodes 4 --node-rank 3 --trust-remote --enable-torch-compile --torch-compile-max-bs 32 --cuda-graph-max-bs 32

性能

在 MASTER 节点使用下面的命令进行性能测试(需要先下载测试数据集 ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json),输入固定 200,输出固定 200,并发 128,测试两轮。

python3 -m sglang.bench_serving --backend sglang-oai --dataset-path /workspace/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json --dataset-name random --random-range-ratio 1 --random-input-len 200 --random-output-len 200  --request-rate 128 --num-prompt 256 --max-concurrency 128 --host localhost --port 8000

我测试的性能指标是:

============ Serving Benchmark Result ============
Backend: sglang-oai
Traffic request rate: 128.0
Max reqeuest concurrency: 128
Successful requests: 256
Benchmark duration (s): 98.63
Total input tokens: 51200
Total generated tokens: 51200
Total generated tokens (retokenized): 50971
Request throughput (req/s): 2.60
Input token throughput (tok/s): 519.11
Output token throughput (tok/s): 519.11
Total token throughput (tok/s): 1038.22
Concurrency: 127.26
----------------End-to-End Latency----------------
Mean E2E Latency (ms): 49032.00
Median E2E Latency (ms): 49199.08
---------------Time to First Token----------------
Mean TTFT (ms): 8294.87
Median TTFT (ms): 8306.07
P99 TTFT (ms): 15599.09
---------------Inter-Token Latency----------------
Mean ITL (ms): 205.47
Median ITL (ms): 183.05
P95 ITL (ms): 187.48
P99 ITL (ms): 243.63
Max ITL (ms): 11274.90
==================================================

对照上一篇博客《刷新 32 张 L40S 运行 DeepSeek-R1-INT8 的性能数据》,看起来 FP8 Block 量化的性能比 INT8 Channel 量化的性能要差一些。

代码改动

这次的代码改动不大,主要是参数转换脚本,和一些针对 64x64 的 tunning,以及 warning 的修复。整理好后我会提个 PR 给 SGLang,但这次我不确定这个 PR 是否会被接受,感兴趣的同学可以直接看这个 commit:https://github.com/solrex/sglang/commit/03d34078d8d65983aabc0386391743cc43f535ed or https://github.com/sgl-project/sglang/pull/4860

地震

正当我写到这的时候,忽然手机通知地震了。生平第一次收到,记录一下。我完全没震感,但是有朋友感觉到了。

刷新 32 张 L40S 运行 DeepSeek-R1-INT8 的性能数据

前一篇博客《使 SGLang 支持在 32 张 L40S 上运行 DeepSeek-R1》中提到我那非常特殊的 L40S 显卡配置,结果发现是个大乌龙。

首先,这台机器有 PCIE 4.0 Switch,每张 Switch 上插了 4 张 L40S 显卡。我误会的 PCI-to-PCI 应该是挂了一个给监控屏用的小显卡。

其次,有一张网卡插错位置了。本来应该每 4 张显卡配 1 张双口网卡,其中一张没插到对应的 Switch 上。

最后,3 张网卡 6 个网口,只启用了 1 个网口,有 5 个网口没启用。

所以,实际上所有机内通信走的都是 PCIE,所有跨机通信走的都是主网卡,这……只能怪自己没经验,默认以为交过来的环境都是对的。

折腾了几天,总算搞对了,正确的拓扑如下:

主网卡两个网口做了链路聚合,用作 TCP 通信;PCIE Switch 上的 4 个网口专用作 RDMA 通信。同机两个 NUMA 域双卡之间通信,走 PCIE 大概 11GB/s,走 GDRDMA 能提升到 19GB/s。最关键的是多机通信,从单网口的小水管提升到了 4 网口 GDRDMA。

采用与上篇文章同样不严谨的测试方式:

# 128 并发
[TP0] Decode batch. #running-req: 128, #token: 86816, token usage: 0.44, gen throughput (token/s): 777.92, #queue-req: 0, # 之前 565.73, 1.37x
# 32 并发
[TP0] Decode batch. #running-req: 32, #token: 8923, token usage: 0.04, gen throughput (token/s): 457.66, #queue-req: 0, # 之前 260.73,1.75x
# 4 并发
[TP0] Decode batch. #running-req: 4, #token: 1437, token usage: 0.01, gen throughput (token/s): 153.98, #queue-req: 0, # 之前 42.3,3.64x
# 1 并发
[TP0] Decode batch. #running-req: 1, #token: 482, token usage: 0.00, gen throughput (token/s): 49.02, #queue-req: 0, # 之前 26,1.88x

固定 200 输入,200 输出,128 并发,2 轮请求:

============ Serving Benchmark Result ============
Backend: sglang-oai
Traffic request rate: 128.0
Max reqeuest concurrency: 128
Successful requests: 256
Benchmark duration (s): 87.57
Total input tokens: 51200
Total generated tokens: 51200
Total generated tokens (retokenized): 51023
Request throughput (req/s): 2.92
Input token throughput (tok/s): 584.64
Output token throughput (tok/s): 584.64 # 之前 391.47,1.49x
Total token throughput (tok/s): 1169.28
Concurrency: 127.23
----------------End-to-End Latency----------------
Mean E2E Latency (ms): 43524.37
Median E2E Latency (ms): 43246.71
---------------Time to First Token----------------
Mean TTFT (ms): 7225.06
Median TTFT (ms): 6769.52
P99 TTFT (ms): 14229.37
---------------Inter-Token Latency----------------
Mean ITL (ms): 182.87
Median ITL (ms): 162.95
P95 ITL (ms): 166.60
P99 ITL (ms): 202.39
Max ITL (ms): 13711.39
==================================================

可以看到,修复网络本身存在的问题以后,推理性能提升还是很显著的。

one more thing

前一篇文章提到: L40S/L20 虽然支持 FP8 精度,却不能运行 FP8 的 DeepSeek-V3/R1。这个问题搞定了,我已经实现在 L40S 上运行原始 FP8 参数的 DeepSeek-R1,满血+纯血。等我整理一下代码,下篇博客来介绍一下。

使 SGLang 支持在 32 张 L40S/L20 上运行 DeepSeek-R1

我提交的 PR: Support serving DeepSeek-R1-Channel-INT8 with 32 L40S. #4418 [1] 已经合入到了 SGLang 的主干,也许这是第一个用 PCIE 互联的 GPU 小卡跑通 DeepSeek-R1 推理的例子。

有一些遇到的问题分享一下,在适配别的 GPU 时可以用来参考。

背景

因为 128x128 block 量化的 DeepSeek-R1/V3,参数维度在除 32 时,会遇到商无法被 128 整除的问题,所以即使 L40S 支持 FP8,也无法直接用 TP32 运行 DeepSeek-R1/V3。

感谢美团提供了 channel 量化的 DeepSeek-R1 参数 DeepSeek-R1-Channel-INT8 [2],并且在 SGLang 代码库做了适配。这让我可以在 48G 显存的 L40S 上尝试一下运行满血版(int8 量化) DeepSeek-R1,但尝试过程没有我预期的顺利,遇到了不少问题。

问题

一、shared memory OutOfResources

  File "/sgl-workspace/sglang/python/sglang/srt/layers/attention/triton_ops/extend_attention.py", line 356, in extend_attention_fwd
_fwd_kernel[grid](
...
File "/usr/local/lib/python3.10/dist-packages/triton/compiler/compiler.py", line 374, in _init_handles
raise OutOfResources(self.metadata.shared, max_shared, "shared memory")
triton.runtime.errors.OutOfResources: out of resource: shared memory, Required: 102400, Hardware limit: 101376. Reducing block sizes or `num_stages` may help.

服务启动正常,收到推理请求后,Attention kernel 报 shared memory 资源不足。我刚开始没理解这个问题,后来看了下代码,发现 triton attention 代码中对不同类型的 GPU 架构设置了不同的 block size。L40S 属于 SM89 架构,但 SM89 都归类到了 SM80 架构里。

查了一下 CUDA 编程手册,SM89 的 shared memory 大小是 100K,但 SM80 是 160K。我猜测大概率就是这个问题,所以给 SM89 单独加了个分支,缩小了 SM89 的 block size,解决了这个问题。

后来发现 sglang/test/srt/test_triton_attention_kernels.py 可以完美复现这个问题,而我却傻乎乎地每次重启整个服务去测试正确性。

二、gemm executioin failed RuntimeError

  File "/usr/local/lib/python3.10/dist-packages/sgl_kernel/ops/__init__.py", line 118, in int8_scaled_mm
return torch.ops.sgl_kernels.int8_scaled_mm(
File "/usr/local/lib/python3.10/dist-packages/torch/_ops.py", line 1116, in __call__
return self._op(*args, **(kwargs or {}))
RuntimeError: gemm executioin failed, error: Error Internal

收到推理请求后,有很大的概率触发这个错误,但也有小概率能完成一次推理。所以我没想到是算子问题,我以为是显存不足。调了半天各种显存占用的参数,后来没办法了才回过头来看实际的算子调用。

又是跟上面类似的问题,sgl_kernel 中自定义的 int8 gemm 算子将 SM89 归类到 SM80 进行矩阵计算的 dispatch。这显然会遇到与上面类似的问题,但是我又不知道 SM89 该怎么进行 dispatch,看起来需要做很多 benchmark 或者计算才能确定。

于是我就去翻 TensorRT-LLM 和 vLLM,让我给翻到了 vLLM 的实现,我就照着 vLLM 对 SM89 的 dispatch 逻辑抄了一遍。这次我学乖了,先看看有没有 test。跑通了 sglang/sgl-kernel/tests/test_int8_gemm.py,才去进行集成测试。

三、sub-optimal MoE

Using default MoE config. Performance might be sub-optimal! Config file not found at /usr/local/lib/python3.10/site-packages/sglang/srt/layers/moe/fused_moe_triton/configs/E=256,N=64,device_name=NVIDIA_L40S,dtype=int8_w8a8.json

这是一个次要问题,看起来是一个专门的配置没有找到。后来我研究了一下,应该是 triton 版本的 fused_moe 需要读取一个在每种类型的 GPU 上都 benchmark 过的最好配置来运行。

python  benchmark/kernels/fused_moe_triton/tuning_fused_moe_triton.py \
--model /workspace/DeepSeek-R1-Channel-INT8 --tp-size 32 --dtype int8_w8a8 --tune

我按照文档的要求,在 L40S 上跑了一下 bench,把最终输出的文件拷贝到对应的位置就好了。这个 bench 真的要跑好久,大概两三个小时。

性能

我这 4 台 L40S 的硬件配置有些特殊:它是单机 8 卡,PCIE 4.0 连接到主机,但它既不是 2-2-4,也不是 2-2-8。

20250322:其实是硬件配置错误,详见:《刷新 32 张 L40S 运行 DeepSeek-R1-INT8 的性能数据

其中 4 张卡直插 PCIE 上,另外 4 张卡通过一个 PCI-to-PCI Bridge 插到 PCIE 上。为了弥补这样连接的带宽缺陷,PCI-to-PCI Bridge 上还接了 2 个 100Gb/s 的 RDMA 网卡。主机上每个 NUMA 域,分别也有 2 个 100Gb/s 的 RDMA 网卡。

所以这 8 张显卡,6 张网卡,拓扑如下所示:

我也实在算不出来这玩意儿的互联带宽。逻辑上来说,这大概相当于用 4 PCIE 4.0 GPU + 2 RDMA 网卡的性能,所以也许这个 4 x 8卡,实际上相当于 8 x 4卡。以下性能评测结果供参考。

下面是加载时的显存使用情况:

[TP0] Load weight end. type=DeepseekV3ForCausalLM, dtype=torch.bfloat16, avail mem=22.43 GB, mem usage=21.27 GB.
[TP0] Memory pool end. avail mem=7.95 GB
[TP0] Capture cuda graph begin. This can take up to several minutes. avail mem=7.92 GB
[TP0] Capture cuda graph end. Time elapsed: 411.41 s. avail mem=5.89 GB. mem usage=2.02 GB.
[TP0] max_total_num_tokens=201723, chunked_prefill_size=8192, max_prefill_tokens=16384, max_running_requests=2049, context_len=163840

下面是使用固定 200 token 输入时,不同并发下的净 decode 速度。因为 bench 速度太慢,我也懒得等最终的结果,所以这里直接节取了 log。

# 128 并发
[TP0] Decode batch. #running-req: 128, #token: 86816, token usage: 0.43, gen throughput (token/s): 565.73, #queue-req: 0,
# 32 并发
[TP0] Decode batch. #running-req: 32, #token: 8923, token usage: 0.04, gen throughput (token/s): 260.73, #queue-req: 0,
# 4 并发
[TP0] Decode batch. #running-req: 4, #token: 1439, token usage: 0.01, gen throughput (token/s): 42.30, #queue-req: 0,
# 1 并发
[TP0] Decode batch. #running-req: 1, #token: 482, token usage: 0.00, gen throughput (token/s): 26.00, #queue-req: 0,

这是固定 200 输入,200 输出,128 并发,2 轮请求的完整压测结果:

============ Serving Benchmark Result ============
Backend: sglang-oai
Traffic request rate: 128.0
Max reqeuest concurrency: 128
Successful requests: 256
Benchmark duration (s): 130.79
Total input tokens: 51200
Total generated tokens: 51200
Total generated tokens (retokenized): 50992
Request throughput (req/s): 1.96
Input token throughput (tok/s): 391.47
Output token throughput (tok/s): 391.47
Total token throughput (tok/s): 782.94
Concurrency: 127.49
----------------End-to-End Latency----------------
Mean E2E Latency (ms): 65135.99
Median E2E Latency (ms): 64974.27
---------------Time to First Token----------------
Mean TTFT (ms): 17554.19
Median TTFT (ms): 19216.02
P99 TTFT (ms): 21662.98
---------------Inter-Token Latency----------------
Mean ITL (ms): 239.84
Median ITL (ms): 220.95
P95 ITL (ms): 233.20
P99 ITL (ms): 299.49
Max ITL (ms): 16077.21
==================================================

链接

[1] https://github.com/sgl-project/sglang/pull/4418

[2] https://huggingface.co/meituan/DeepSeek-R1-Channel-INT8

[3] https://github.com/vllm-project/vllm/blob/main/csrc/quantization/cutlass_w8a8/scaled_mm_c2x_sm89_int8_dispatch.cuh