V100 for Qwen 3.5 Deployment

V100 上部署 Qwen 3.5 的实践记录与性能分析

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,这卡很老,但是还有一些价值可以压榨:

  1. 架构老,但是核心规模比较大,单卡算力不算太差;
  2. 32GB HBM显存,容量尚可,带宽很大;
  3. 部分卡之间有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模式,但也有一定的提升。

modelquantizationframeworkconcurrency1 gpu2 gpu4 gpu8 gpu
Qwen3 14BQ4_K_Mllama_cpp (-sm layer)153.4255.3652.5048.74
Qwen3 14BQ4_K_Mllama_cpp (-sm layer)851.9854.1852.3947.71
Qwen3 14BQ4_K_Mvllm_tp129.3550.6368.6387.53
Qwen3 14BQ4_K_Mvllm_tp845.5986.00150.41248.92
Qwen3 14BQ4_K_Mvllm_pp129.1827.6627.4525.49
Qwen3 14BQ4_K_Mvllm_pp845.7363.0482.41102.23

上面的实验完成后很久,我才知道 llama.cpp 其实也有一个 -sm row 的实现,“类似” vLLM 的 tp 模式。这个实现的性能更差,甚至比 -sm layer 还要差。

modelquantizationsmgpustestt/s
Qwen 3.5 122B A10BQ4_K_Mlayer4pp512386.83 ± 7.14
Qwen 3.5 122B A10BQ4_K_Mlayer4tg51239.78 ± 1.18
Qwen 3.5 122B A10BQ4_K_Mrow4pp512253.60 ± 2.24
Qwen 3.5 122B A10BQ4_K_Mrow4tg51222.24 ± 0.72

并发性能观察

同样是上面的表格,llama.cpp 似乎不太擅长处理并发。

不同量化精度的吞吐量观察

modelquantizationgpust/s
Qwen3 14BIQ4_NL157.25
Qwen3 14BIQ4_XS159.44
Qwen3 14BQ2_K153.13
Qwen3 14BQ2_K_L153.06
Qwen3 14BQ3_K_M147.14
Qwen3 14BQ3_K_S143.39
Qwen3 14BQ4_0158.32
Qwen3 14BQ4_1156.84
Qwen3 14BQ4_K_M153.42
Qwen3 14BQ4_K_S155.69
Qwen3 14BQ5_K_M149.67
Qwen3 14BQ5_K_S150.86
Qwen3 14BQ6_K142.49
Qwen3 14BQ8_0139.20

从这张表里,我认为最主要的结论有三点。

第一,llama.cpp 在 V100 上并不是“量化越低,速度越快”。比如 Q2_K 并没有明显快过 Q4_0Q4_1Q8_0 反而更慢。这说明吞吐量不只是看模型体积,还很受 kernel 实现和反量化开销影响。

第二,常规量化里表现最好的其实是 Q4_0IQ4_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,只有少数几个是可以用的。

Quantizationget_min_capabilitywork on V100
AWQConfig75
AWQMarlinConfig75
BitsAndBytesConfig70Y
ExpertsInt8Config80
FBGEMMFp8Config80
Fp8Config75
FPQuantConfig100
GGUFConfig60Y
GPTQConfig60Y
GPTQMarlinConfig75
INCConfig60Y
ModelOptFp8Config89
ModelOptMixedPrecisionConfig89
ModelOptMxFp8Config100
ModelOptNvFp4Config75
MoeWNA16Config70Y
Mxfp4Config80
PetitNvFp4Config90
PTPCFp8Config75
QuarkConfig70Y
TorchAOConfig75

这样看起来,BNB,GPTQ 和 GGUF 是比较常见的,有不少模型可以直接下载。

BNB不兼容TP,GGUF吞吐量欠佳

直接安排测试,为了尽可能保持公平,我使用Qwen3 32B,同样是4bit,固定 4 GPU,分别在3个量化方式下进行测试,结果如下:

modelframeworkconcurrency 1concurrency 8
Qwen3-32B-GPTQ-Int4vllm_tp58.27188.38
Qwen3-32B-Q4_K_M.ggufvllm_tp35.6670.49
Qwen3-32B-unsloth-bnb-4bitvllm_tpN/AN/A
Qwen3-32B-GPTQ-Int4vllm_pp25.51121.08
Qwen3-32B-Q4_K_M.ggufvllm_pp12.5538.55
Qwen3-32B-unsloth-bnb-4bitvllm_pp8.313.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个调优的方向:

  1. Expert Parallel,看看tp和ep哪一个更好
  2. 搭配 Data Parallel,看看tp+dp和tp单独哪个更好

汇总的数据如下,结论就是直接开启TP=8就完事了,其他的配置反而会降低性能,这可能是因为我的GPU互联还算不错。

configrequest_rate=1request_rate=2request_rate=4request_rate=8request_rate=16request_rate=32
ep461.0185.67107.79149.13183.2757.15
ep4dp249.27107.27152.15115.2278.74121.18
ep883.93121.9162.42241.2570.17107.25
tp479.22115.05161.15228.42269.92256.15
tp4dp278.21130.76198.2289.77397.23476.05
tp8101.89165257.78379.65472.96531.33

那么接下来就是正式部署了,部署的配置我放在了开头的TL;DR里,主要是开启tp8,关闭ep和dp。

后续工作

接下来有机会我要tune一下MoE WNA16的config,这个部分应该会是性能的一个重要提升点。log当中提示,MoE的config未找到,因此自动选择了default,提示我最好调整一下。