TL;DR
最终部署结果
部署 Qwen 3.5 122B A10B 量化版本
services:
vllm-qwen3_5-122b-a10b-gptq-int4:
image: vllm/vllm-openai:v0.17.0
ipc: host
shm_size: 16g
environment:
VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS: 3000
volumes:
- /home/ubuntu/models:/models:ro
- /home/ubuntu/vllm/vllm:/usr/local/lib/python3.12/dist-packages/vllm
container_name: vllm-qwen3_5-122b-a10b-gptq-int4
ports:
- "8018:8000"
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ["0", "1", "2", "3", "4", "5", "6", "7"]
capabilities: [gpu]
command: >
/models/Qwen3.5-122B-A10B-GPTQ-Int4
--host 0.0.0.0
--port 8000
--served-model-name Qwen3.5-122B-A10B-GPTQ-Int4
--mm-encoder-attn-backend TORCH_SDPA
--mm-processor-cache-type shm
--max-model-len auto
--reasoning-parser qwen3
--enable-auto-tool-choice
--tool-call-parser qwen3_coder
--gpu-memory-utilization 0.75
--tensor-parallel-size 8
--enable-prefix-caching
llama.cpp
- 多GPU性能很差,高并发性能很差
- 对老硬件的兼容性非常好,基本点击即用
- 支持多种精度
vLLM
- 多GPU性能不错,尤其在高并发场景下表现出色
- 对老硬件的兼容性一般,有不少问题需要解决
- 吞吐量方面,pp不如tp
- 对GGUF量化的支持比较差,单GPU不如llama.cpp
我的硬件
GPU 规格
我的机器是 8 张 Tesla V100-SXM2-32GB,这卡很老,但是还有一些价值可以压榨:
- 架构老,但是核心规模比较大,单卡算力不算太差;
- 32GB HBM显存,容量尚可,带宽很大;
- 部分卡之间有Nvlink,通信性能不错。
GPU 拓扑
从 nvidia-smi topo -m 看,我这 8 张卡并不是完全对称全互联,而是带有多组 NVLink 的拓扑,这个是第一代DGX平台的老毛病,并不像A100是全互联。
- 部分 GPU 之间是
NV1; - 部分 GPU 之间是
NV2; - 还有一些路径是
PHB,也就是要经过 PCIe Host Bridge。
其他硬件
- CPU 是
Intel Xeon Gold 6248 @ 2.50GHz,共 40 核,QEMU虚拟机; - 内存大约
472 GiB; - 硬盘 1TB。
llama.cpp 的性能观察
我这里主要关注输出吞吐量,所以我会在不同 GPU 数量、不同并发数、不同量化精度的情况下,做一些简单的分析。
使用的模型是 Qwen3 14B Q4_K_M,GGUF格式,在 vLLM 和 llama.cpp 上分别测试了不同 GPU 数量(1、2、4、8)和不同并发数(1、8)的吞吐量表现。
多GPU性能观察
测试结果很明显,对于llama.cpp 来说,增加 GPU 数量并没有带来性能提升,这说明 llama.cpp 的多GPU实现可能存在瓶颈或者没有充分利用多GPU的能力。
而 vLLM 则表现出明显的多GPU加速效果,尤其在并发数为 8 的情况下,性能提升非常显著。为了对比公平,我也测试了 vLLM 的pp模式,虽然性能不如tp模式,但也有一定的提升。
| model | quantization | framework | concurrency | 1 gpu | 2 gpu | 4 gpu | 8 gpu |
|---|---|---|---|---|---|---|---|
| Qwen3 14B | Q4_K_M | llama_cpp (-sm layer) | 1 | 53.42 | 55.36 | 52.50 | 48.74 |
| Qwen3 14B | Q4_K_M | llama_cpp (-sm layer) | 8 | 51.98 | 54.18 | 52.39 | 47.71 |
| Qwen3 14B | Q4_K_M | vllm_tp | 1 | 29.35 | 50.63 | 68.63 | 87.53 |
| Qwen3 14B | Q4_K_M | vllm_tp | 8 | 45.59 | 86.00 | 150.41 | 248.92 |
| Qwen3 14B | Q4_K_M | vllm_pp | 1 | 29.18 | 27.66 | 27.45 | 25.49 |
| Qwen3 14B | Q4_K_M | vllm_pp | 8 | 45.73 | 63.04 | 82.41 | 102.23 |
上面的实验完成后很久,我才知道 llama.cpp 其实也有一个 -sm row 的实现,“类似” vLLM 的 tp 模式。这个实现的性能更差,甚至比 -sm layer 还要差。
| model | quantization | sm | gpus | test | t/s |
|---|---|---|---|---|---|
| Qwen 3.5 122B A10B | Q4_K_M | layer | 4 | pp512 | 386.83 ± 7.14 |
| Qwen 3.5 122B A10B | Q4_K_M | layer | 4 | tg512 | 39.78 ± 1.18 |
| Qwen 3.5 122B A10B | Q4_K_M | row | 4 | pp512 | 253.60 ± 2.24 |
| Qwen 3.5 122B A10B | Q4_K_M | row | 4 | tg512 | 22.24 ± 0.72 |
并发性能观察
同样是上面的表格,llama.cpp 似乎不太擅长处理并发。
不同量化精度的吞吐量观察
| model | quantization | gpus | t/s |
|---|---|---|---|
| Qwen3 14B | IQ4_NL | 1 | 57.25 |
| Qwen3 14B | IQ4_XS | 1 | 59.44 |
| Qwen3 14B | Q2_K | 1 | 53.13 |
| Qwen3 14B | Q2_K_L | 1 | 53.06 |
| Qwen3 14B | Q3_K_M | 1 | 47.14 |
| Qwen3 14B | Q3_K_S | 1 | 43.39 |
| Qwen3 14B | Q4_0 | 1 | 58.32 |
| Qwen3 14B | Q4_1 | 1 | 56.84 |
| Qwen3 14B | Q4_K_M | 1 | 53.42 |
| Qwen3 14B | Q4_K_S | 1 | 55.69 |
| Qwen3 14B | Q5_K_M | 1 | 49.67 |
| Qwen3 14B | Q5_K_S | 1 | 50.86 |
| Qwen3 14B | Q6_K | 1 | 42.49 |
| Qwen3 14B | Q8_0 | 1 | 39.20 |
从这张表里,我认为最主要的结论有三点。
第一,llama.cpp 在 V100 上并不是“量化越低,速度越快”。比如 Q2_K 并没有明显快过 Q4_0、Q4_1,Q8_0 反而更慢。这说明吞吐量不只是看模型体积,还很受 kernel 实现和反量化开销影响。
第二,常规量化里表现最好的其实是 Q4_0、IQ4_XS 这一档,基本能跑到 58~59 t/s;而 Q4_K_M 这种常用格式只有 53.42 t/s,速度上并不占优。也就是说,如果我的目标只是单卡吞吐,Q4_K_M 未必是最好的选择。
vLLM 的打地鼠之旅
看完上面的表格,那我必须换成 vLLM 了,毕竟多GPU性能太差了。但是在部署 vLLM 的过程中,我遇到了不少麻烦,主要是因为我的V100太老了。
量化方法剩下3种
快速扫描了一下vllm.model_executor.layers.quantization下,各个class的get_min_capability,发现很多量化方法都不太适合我的V100,只有少数几个是可以用的。
| Quantization | get_min_capability | work on V100 |
|---|---|---|
| AWQConfig | 75 | |
| AWQMarlinConfig | 75 | |
| BitsAndBytesConfig | 70 | Y |
| ExpertsInt8Config | 80 | |
| FBGEMMFp8Config | 80 | |
| Fp8Config | 75 | |
| FPQuantConfig | 100 | |
| GGUFConfig | 60 | Y |
| GPTQConfig | 60 | Y |
| GPTQMarlinConfig | 75 | |
| INCConfig | 60 | Y |
| ModelOptFp8Config | 89 | |
| ModelOptMixedPrecisionConfig | 89 | |
| ModelOptMxFp8Config | 100 | |
| ModelOptNvFp4Config | 75 | |
| MoeWNA16Config | 70 | Y |
| Mxfp4Config | 80 | |
| PetitNvFp4Config | 90 | |
| PTPCFp8Config | 75 | |
| QuarkConfig | 70 | Y |
| TorchAOConfig | 75 |
这样看起来,BNB,GPTQ 和 GGUF 是比较常见的,有不少模型可以直接下载。
BNB不兼容TP,GGUF吞吐量欠佳
直接安排测试,为了尽可能保持公平,我使用Qwen3 32B,同样是4bit,固定 4 GPU,分别在3个量化方式下进行测试,结果如下:
| model | framework | concurrency 1 | concurrency 8 |
|---|---|---|---|
| Qwen3-32B-GPTQ-Int4 | vllm_tp | 58.27 | 188.38 |
| Qwen3-32B-Q4_K_M.gguf | vllm_tp | 35.66 | 70.49 |
| Qwen3-32B-unsloth-bnb-4bit | vllm_tp | N/A | N/A |
| Qwen3-32B-GPTQ-Int4 | vllm_pp | 25.51 | 121.08 |
| Qwen3-32B-Q4_K_M.gguf | vllm_pp | 12.55 | 38.55 |
| Qwen3-32B-unsloth-bnb-4bit | vllm_pp | 8.3 | 13.65 |
BNB与TP不兼容,并且PP模式下性能也不太好;GGUF虽然兼容,但性能远不如GPTQ,所以选择GPTQ作为后续测试的量化方法。
vLLM 版本问题
vLLM 0.16.0 版本没有完全支持Qwen3.5,因此需要使用 vLLM 0.17.0 版本或者nightly版本(或对应的docker镜像),才能正确加载模型并进行推理。
MM Encoder 问题
从 vLLM 0.17.0 开始,MM Encoder 的默认后端变为了Triton。Triton本身是个好东西,但有两个大毛病:
- 编译极其缓慢,而且是JIT,第一次调用的时候需要经历漫长的编译
- 运行效率不如原本的Torch SDPA实现 因此在我的部署中,我选择了关闭 Triton,强制使用 Torch SDPA 来进行MM Encoder的计算
MoE WNA16 问题
我在部署过程中遇到的最大挑战,莫过于 MoE WNA16 这个量化方法了。这个方法理论上是支持 V100 的,但实际使用时却频繁遇到 TORCH_CHECK 报错,提示 BLOCK_SIZE_K // group_size ∈ {1,2,4,8} 的约束被违反了。
顺着报错我找到了 invoke_fused_moe_wna16_cuda_kernel这个函数,使用print做了一个简单的探针,打印调用 get_moe_wna16_block_config() 时的参数,发现num_experts=1024,而实际上 Qwen3.5 122B 的 MoE expert 数量应该是 256。
接下来的步骤我就交给了codex。它发现 vLLM 在 invoke_fused_moe_wna16_{cuda,triton}_kernel 这条路径里,把 num_experts 传成了 B.size(1),但这里正确的值其实应该是 B.size(0)。
这里的 B 是 MoE expert 的权重张量,可以简单理解成“所有 expert 的那一大块权重”。
B.size(0)是 expert 数量,也就是num_experts;B.size(1)是输出通道数,也就是size_n;- 所以把
B.size(1)当成num_experts,本质上就是把输出维度错当成 expert 数。
Issue已经提交,等待 vLLM 团队修复,我自己目前用了一个monkey patch,把本地vLLM目录覆盖到容器内的位置,暂时解决了这个问题。
API 兼容性问题
在 vLLM 0.17.0 版本中,推理过程从reasoning_content改名为reasoning了,这导致包括Dify在内的一些客户端(暂时)无法正确处理 vLLM 的输出,出现了 KeyError: 'reasoning_content' 的错误。同样monkey patch了一下,暂时缓解了这个问题。
vLLM 的性能压榨
总算是跑通了,接下来就是性能调试了,我们的目标是尽可能提升8卡的吞吐量。此前我们已经确认,pp不如tp,所以接下来基于tp有2个调优的方向:
- Expert Parallel,看看tp和ep哪一个更好
- 搭配 Data Parallel,看看tp+dp和tp单独哪个更好
汇总的数据如下,结论就是直接开启TP=8就完事了,其他的配置反而会降低性能,这可能是因为我的GPU互联还算不错。
| config | request_rate=1 | request_rate=2 | request_rate=4 | request_rate=8 | request_rate=16 | request_rate=32 |
|---|---|---|---|---|---|---|
| ep4 | 61.01 | 85.67 | 107.79 | 149.13 | 183.27 | 57.15 |
| ep4dp2 | 49.27 | 107.27 | 152.15 | 115.22 | 78.74 | 121.18 |
| ep8 | 83.93 | 121.9 | 162.42 | 241.25 | 70.17 | 107.25 |
| tp4 | 79.22 | 115.05 | 161.15 | 228.42 | 269.92 | 256.15 |
| tp4dp2 | 78.21 | 130.76 | 198.2 | 289.77 | 397.23 | 476.05 |
| tp8 | 101.89 | 165 | 257.78 | 379.65 | 472.96 | 531.33 |
那么接下来就是正式部署了,部署的配置我放在了开头的TL;DR里,主要是开启tp8,关闭ep和dp。
后续工作
接下来有机会我要tune一下MoE WNA16的config,这个部分应该会是性能的一个重要提升点。log当中提示,MoE的config未找到,因此自动选择了default,提示我最好调整一下。