说实话,把“6卡serve qwen失败”这几个字搜索一遍,会看到一大堆帖子,但多数都停留在贴报错、求答案的阶段。今天我想把自己从踩坑到跑通的完整过程写下来:一台6张RTX 3090的机器,试图用vLLM部署Qwen2.5-72B-Instruct,折腾了大半天,反复CUDA OOM、不断重启。拆开看之后发现,所谓的“失败”其实是好几个问题叠在一起,既有显存算账没算明白,也有并行策略和依赖版本的前置条件没核对。这篇文章默认读者有一定基础,但我会尽量把关键计算和排查步骤写全,适合准备用多卡做模型serving、以及刚接触vLLM/Qwen部署的人参考。
1. 一台6卡机器,半天没跑起来
先说背景。手上的机器是一台双路服务器,插了6张RTX 3090,每张24GB,CPU是64核,系统内存256GB,Ubuntu 22.04。软件环境一开始是Python 3.10、PyTorch 2.3.0、CUDA 11.8,vLLM版本0.6.3.post1,transformers 4.44.0。想跑的模型是Qwen2.5-72B-Instruct,这是当时新版Qwen系列里参数量比较大的一个,意图很简单:用6卡张量并行方式提供OpenAI兼容接口,供内部工具调用。
1.1 首次启动:看起来一切正常,直到OOM
初始化环境后,启动命令也很常规,就是装好vLLM后一行命令:
vllm serve Qwen/Qwen2.5-72B-Instruct --tensor-parallel-size 6 --max-model-len 8192日志刚开始很顺,模型从模型仓库拉权重,然后初始化GPU内核,这时候会出现一段比较长的等待,大概两三分钟。我一度以为是在加载72B权重,还觉得6卡张量并行应该没问题。结果日志从Initializing GPU kernel...跳过去之后,直接出现:
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB.更有意思的是,这个OOM报错并不是在某一张卡上,而是前两张卡依次报,第三张卡卡了更久,最后全部卡都变成“风扇狂转、显存占用几乎满格”的状态。我特意用nvidia-smi去看,确认第0、1、2号卡显存占到了23.4GB左右,第3、4号卡只有17GB左右,第5号卡还剩不少。这种占用不均的情况让我一度怀疑是vLLM分配bug,后来才意识到是权重切分之后,加上KV cache预分配的叠加效应,导致部分卡超额。
1.2 第一次怀疑方向:是vLLM版本还是显存不够
遇到OOM,第一反应是改配置。我把--max-model-len从8192降到4096,又把--gpu-memory-utilization从默认的0.9改成0.85,重新启动。这次日志往前走了一段,加载权重阶段过了,但在计算attention的KV cache时再次报错,而且这次报错的是NCCL相关的runtime error,不是单纯的显存不足。那时候我已经开始怀疑是不是vLLM 0.6.3.post1和CUDA 11.8兼容性有问题,甚至想换vLLM 0.5版本重来。
不过冷静下来后,我做了个最笨但最有效的操作:找一张空卡,手动加载模型权重,验证权重本身、transformers和模型文件是否正常。因为如果权重坏了或者依赖不完整,根本不需要讨论多卡并行的问题。我先把一个1.5B的小模型放到单卡上跑通,确认vLLM服务本身没有坏;然后又单独用transformers在单卡上尝试加载Qwen2.5-72B的fp16权重,结果当然是直接OOM——因为单卡24GB压根装不下144GB的权重。这就说明问题根源很可能确实是显存分配,而不是框架bug。
2. 显存账单算清楚:为什么六张卡还是不够
很多人一听说“6张卡”,就觉得显存应该绰绰有余,毕竟4090或3090也有24GB,6张就是144GB。但类似Qwen2.5-72B这种级别的稠密模型,144GB只是权重的理想下限,真正做起serving来要花的远不止这些。
2.1 BF16权重一算就露馅
先算最基础的模型权重。72B参数量按700亿出头算,BF16格式每个参数占2字节,裸权重就是:
72B * 2 bytes = 144GB但这里还没算embedding、layernorm、额外的bias项,以及模型文件本身的padding和切分对齐。Qwen2.5-72B-Instruct实际在磁盘上的BF16权重接近150GB。6张3090加起来是144GB,严格说已经不够,所以在权重映射阶段就存在理论上的失败风险。这根本不是优化参数能解决的,属于物理账算错了。
2.2 KV Cache、CUDA Context 和进程分配的隐性开销
如果权重刚好能压线放下,还有三笔开销等着:
- CUDA context:每个进程初始化时会为GPU保留一部分显存,常见在1GB到2GB之间,取决于PyTorch和CUDA版本。哪怕什么都不加载,先占1.5GB,6张卡就额外消耗约9GB。
- KV cache:vLLM默认会把
gpu_memory_utilization=0.9对应到显存分配策略,也就是说它会在一开始给KV cache预留一大块连续空间。如果权重已经吃掉90%,KV cache基本无空间可分配,于是只能缩减--max-model-len,甚至报错。 - activation和临时buffer:attention计算时的中间张量、beam search时的候选张量、以及量化算子变换时的临时buffer,都会根据batch size和序列长度动态产生。
这些开销单看都不大,但叠加之后,一张24GB的卡很容易在“看起来应该够”的情况下瞬间超标。我后来用torch.cuda.max_memory_reserved做过统计,在纯权重的加载阶段,每张卡额外预留的运行时显存大约是1.8GB到2.2GB。
2.3 张量并行切分的对齐冗余
张量并行不是简单地把权重均分到每张卡。vLLM在切分时会把每个线性层按列或按行切块,要求切块数能整除head数、intermediate size等参数。对于某些层,实际切分后会有少量冗余。以Qwen2.5-72B为例,TP=6是可行的,但每张卡分配到的参数并不严格等于150/6=25GB,而是在局部层上存在不对齐,导致某张卡多占几百MB或更大。这就是我在第一次启动时观察到第0号卡先OOM的原因——它不是最慢加载的,而是被分配了更多切片。
2.4 显存预算表:部署前先自己算一遍
后来我做了一张简单的预算表,每次部署前都会套一下:
| 项目 | 计算公式/建议值 | 72B BF16示例 |
|---|---|---|
| 模型权重 | 参数量 × 每参数字节数 | 约150GB |
| CUDA context | 每卡预留1.0~2.0GB | 6卡合计约9GB |
| KV cache | max_len × 层数 × 每层KV张量大小 | 视max_len而定 |
| activation等临时buffer | 按batch和seq长度估算 | 若干GB |
| 总需求 | 上述四项相加 | 远超144GB |
算完之后就非常清楚:6张24GB卡跑BF16的72B,注定失败。如果想继续用这套显卡,就必须把权重缩小到“每卡约8GB以下”的程度,才可能在保留KV cache和context的前提下稳定运行。
3. 排查链路:从显存怀疑到量化选型
踩坑之后,我没有立刻换模型,而是做了一套可以复用的排查链路,这样以后部署其他模型也能直接用。整个排查过程大概分四步。
3.1 先验证服务链路是否正常
用0.5B或1.5B的小模型,比如Qwen/Qwen2.5-0.5B-Instruct,不加张量并行,直接单卡启动。这一步的目的是确认vLLM能否正常加载模型、监听端口、响应请求。如果连小模型都报错,问题出在环境而非显存。这一步我跑得很顺利,也顺手验证了OpenAI接口的/v1/chat/completions能正常返回结果。
3.2 检查驱动、CUDA与pytorch的匹配关系
vLLM底层依赖NCCL做多卡通信,NCCL对CUDA driver和PyTorch的CUDA版本比较敏感。我用命令确认了:
nvidia-smi # Driver Version: 535.183.06 CUDA Version: 12.2 python -c "import torch; print(torch.version.cuda)" # 11.8这里出现了一个小坑:显卡驱动支持CUDA 12.2,但PyTorch编译用的是CUDA 11.8,vLLM又是基于PyTorch的运行时来初始化的。虽然多数情况下API能兼容,但NCCL在跨卡通信时偶尔会挑版本。保险起见,我把PyTorch换成了CUDA 12.1对应的版本,升级到torch==2.3.1+cu121,再配合同样的vLLM版本,通信错误明显减少。
3.3 量化模型是绕不开的解药
既然BF16放不下,自然想到量化。Qwen官方仓库提供了已经量化好的AWQ版本可以直接下载,比如Qwen/Qwen2.5-72B-Instruct-AWQ。AWQ一般按4bit或8bit存储权重,4bit大约比FP16少4倍左右。这样72B模型的4bit权重大约37GB到40GB,6张卡每张只承担6.5GB左右。当时我还对比了GPTQ和AWQ,实测下来在vLLM中AWQ的支持更顺,性能和显存占用也符合预期。
3.4 实操:加载AWQ模型并调整并行参数
加载AWQ模型有两种常见方式。一种是让vLLM自动识别模型目录里的quantization配置,另一种是显式声明。我习惯显式声明,避免版本自动检测出岔子:
vllm serve Qwen/Qwen2.5-72B-Instruct-AWQ \ --tensor-parallel-size 6 \ --max-model-len 4096 \ --quantization awq \ --gpu-memory-utilization 0.9启动之后,六张卡的显存占用基本稳定在19GB到21GB之间,已经没有单卡爆满的情况。第一次成功启动后,我立刻做了一次并发请求测试:16路并发、每路输入512 token、输出256 token,整体吞吐大约在每秒280 tokens左右。对于内部工具来说,这个数字完全够用。如果想让某一卡的压力再小一点,也可以把--gpu-memory-utilization调成0.85,但那样会影响KV cache容量,导致最大并发数下降,需要根据线上流量自己权衡。
4. 真正能跑通的三个方案
如果你手头的卡不是3090,而是A100或者H100,显存更大,可能不需要走到量化那一步。但如果你和我一样是“小显存多卡”,那么下面三个方案基本覆盖了能跑通的所有路径。
4.1 方案A:4bit量化权重,成本最低、见效最快
这个方案就是我上面说的AWQ。优点是不用改业务代码,模型接口形式不变,直接换一个模型路径和一行量化参数即可。缺点是把精度从BF16降到4bit,会遇到一点质量损失。以Qwen2.5-72B的基准测试数据看,AWQ后的得分下降大多在1到2个百分点内,日常问答和代码生成几乎无感知。
需要注意,vLLM对AWQ和GPTQ的kernel支持版本有要求。如果遇到“Unsupported quantization method”报错,优先升级vLLM到较新版本,或者确认模型目录下是否存在quantize_config.json。有些用户自己用AutoAWQ量化,但量化后的格式和vLLM不完全一致,也会导致加载失败。
4.2 方案B:换MoE或更小的Qwen模型,从根源降低显存需求
如果你的应用场景并不苛求72B这种体量,那更稳的做法是降低模型规模。Qwen2.5系列里有两个更合适的模型:
Qwen2.5-57B-A14B-Instruct:虽然是57B总参数,但属于MoE结构,每个token只激活14B参数,BF16权重大约40GB,6张24GB卡完全兜得住,推理速度也不错。Qwen2.5-32B-Instruct:BF16权重约70GB,6张卡每卡约12GB,留12GB给KV cache和activation,基本可以跑8192以上上下文。
如果业务方明确要求“必须用72B”,那在6张24GB卡上强行BF16就是性价比极低的事情,不如直接和业务对齐需求,换32B或MoE。但如果只是做技术验证,量化的72B依然值得选。
4.3 方案C:调整vLLM参数、保留FP16的最后抢救
还有一个不做量化的办法:降低上下文长度和显存利用率,同时打开swap空间,把KV cache换到CPU内存。比如:
vllm serve Qwen/Qwen2.5-72B-Instruct \ --tensor-parallel-size 6 \ --max-model-len 1024 \ --gpu-memory-utilization 0.72 \ --swap-space 8这样确实能启动,但实践下来非常不推荐。因为序列长度只有1024,基本只能做单轮短问答;一旦并发请求变多,KV cache会频繁写回CPU,响应速度会慢到让人怀疑人生。这条路只适合“必须要F16精度、只用短文本、并发极低”的边缘场景,正常生产环境别选它。
4.4 三个方案怎么挑
我把选型逻辑整理成一张表:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 小显存多卡,要求最大模型 | 4bit AWQ量化 | 显存释放明显,推理性能稳定 |
| 对输出质量非常敏感 | 换MoE或32B模型 | 保留更高精度,规避量化损失 |
| 仅技术验证,不要求并发 | 降低max_len+F16 | 不额外下载量化模型,但性能受限 |
| 生产环境 | 量化大模型或MoE小模型 | 并发、吞吐、延迟都更可控 |
5. 踩坑总结与给后来者的清单
这次“6卡serve qwen失败”折腾下来,最大的教训不是“不要用6张3090跑72B”,而是很多部署问题其实能通过启动前的两分钟计算避免。我把后来一直在用的检查步骤和速查表放在这里,希望你不用重复踩这些坑。
5.1 部署前必做的三项检查
第一,先算权重账单。确认目标模型的裸权重大小、每张卡的显存、卡数,三者做乘法,判断是否留出至少15%的空闲显存。如果是72B BF16和6×24GB这种临界组合,直接默认不行即可。
第二,确认并行规模。--tensor-parallel-size必须是总卡数或能够整除的数值,同时模型内部一些维度的配置也需要匹配。vLLM对TP大小有限制,例如某些attention head不能整除TP=5或7,如果遇到“Tensor parallel size should divide”之类的提示,不要硬扛,改成可用值。
第三,确认驱动和CUDA版本。用nvidia-smi查看驱动支持的CUDA版本,在检查python -c "import torch; print(torch.version.cuda)",尽量让PyTorch的CUDA版本不高于驱动支持的版本,否则NCCL通信会出现玄学报错。
5.2 常见报错与解决方案速查表
整理一下我遇到以及同事遇到过的几类高频错误:
| 现象 | 常见原因 | 解决方向 |
|---|---|---|
| CUDA out of memory,加载阶段就报 | 权重+运行时开销超过显存 | 换量化模型、换小模型、降max_len |
| 启动后卡在Initializing GPU kernels | 首次加载内核较慢,或vLLM/CUDA版本不匹配 | 等待观察;检查日志尾部;升级vLLM |
| NCCL unhandled system error / timeout | 多卡通信初始化失败,P2P、共享内存、驱动问题 | 检查nvidia-smi topo -m,升级CUDA驱动,增加NCCL_DEBUG=INFO |
| ValueError: TP size not supported | 并行度与模型结构不匹配 | 换为合法TP值,例如2、4、8 |
| 端口被占用或服务启动后无响应 | 容器/进程未退出或绑定失败 | 用`ps aux |
5.3 一些个人经验
我在实际部署中还有一个习惯:先跑--max-model-len最小值,确保服务能起来,再去逐步调大。因为一旦max_len过大,KV cache会直接挤占权重之外的空间,出现“权重加载成功,但启动后等待请求时OOM”的诡异现象。锁死GPU也没必要,但建议在容器里用CUDA_VISIBLE_DEVICES=0,1,2,3,4,5显式指定设备顺序,避免多卡编号错乱导致不必要的通信异常。
另外,如果你需要下载来自ModelScope或Hugging Face的Qwen权重,强烈建议直接用各自提供的命令行工具拉取,不要在服务器上开代理或者手动拼接下载地址,容易毁文件。下载后务必校验模型目录下的文件完整性,至少检查权重文件大小是否与config.json中的预期一致。
多卡serve Qwen失败,绝大多数不是Qwen的问题,也不是vLLM的问题,而是显存预算和并行配置这两件事没在启动前想清楚。把这篇文章里的步骤走一遍,大部分卡脖子的场景都能解开。如果还有更特殊的报错,不急着换框架,先试着把日志尾部几十行完整贴出来,查一下对应的vLLM issue。很多看似无解的问题,最后都是因为一些极其基础的版本差异导致的。