这年头一说 GPU 推理,大家张口就是 FP8、TensorRT、vLLM,手里却是两块 Tesla V100 32GB。FP8 是 Hopper 和 Ada 这些新卡的玩具,V100 是 Volta 架构,算力只有 7.0,压根没有 FP8 张量核心。可真有一台 2×V100 摆在面前时,还是会忍不住想:vLLM FP8 方案能不能硬上?llama.cpp 的 Q4_K_M 又能在这种老平台上做到什么水平?这篇文章就是我个人在一台 2×V100 32GB 服务器上折腾这两个方案的真实记录。先给结论:vLLM FP8 在 V100 上不是一个可用方案,我试出了加载失败和强行加载后的拉胯性能;llama.cpp + Q4_K_M 反而是这台机器上最舒服的组合。后面我会把环境、参数、实测数据、踩坑细节都摊开讲,给同样在旧卡上跑模型的兄弟一个参考。
1. 先交代:为什么拿这两个八竿子打不着的东西做对比
1.1 手里的平台:2×V100 32GB 的尴尬
先说说这台机器。主板是 X99 平台,CPU 是 20 核的至强,内存 128GB,显卡是 2 张 Tesla V100-SXM2-32GB,中间用 NVLink 2.0 互联。V100 用的是 Volta 架构,单卡 HBM2 显存带宽 900GB/s,FP16 Tensor Core 峰值算力大约 112 TFLOPS,放在当年是数据中心顶级卡,现在看就是“显存够大、带宽够用、指令集偏老”的代表。它最大的槽点是没有 BF16,没有 INT8 张量核心,更不存在 FP8,硬要说量化,只能用 FP16 和普通 CUDA 核上的 INT8 模拟。
为什么这种老平台还有人用?原因很简单:便宜。二手 2×V100 32GB 的价格远低于一块 RTX 4090,却拥有 64GB 总显存,恰好是跑 7B/8B 模型最舒服的容量段。很多做内部原型验证、小规模私有部署的人,手里就是这种配置。它跑不了 70B 的大家伙,但装下 Qwen2.5-7B、DeepSeek-R1-Distill-Qwen-7B 这类蒸馏模型绰绰有余。所以 2×V100 的真实需求并不是“追新”,而是在预算和性能之间找一个稳定可用的折中点。这篇文章里所有对比,都建立在这种“老平台还想跑新模型”的现实上。
1.2 vLLM FP8 与 llama.cpp Q4_K_M 到底是什么
vLLM 是现在高吞吐推理的首选方案,核心是 PagedAttention 和 continuous batching。它把显存里的 KV cache 切成小块按需分配,scheduler 在每次迭代时决定哪些请求继续跑、哪些请求抢占,executor 再把 batch 映射到 GPU worker 上执行。这种架构非常吃并发,适合做 OpenAI 兼容接口对外提供服务。而 FP8 量化是指把权重和激活都压成 8bit 浮点,常见格式是 E4M3 和 E5M2,在 Ada 或 Hopper 架构上用专用 FP8 张量核心做矩阵乘,显存占用减半,吞吐还能涨。问题在于,FP8 是硬依赖 GPU 架构的功能,不是随便什么卡都能用。
llama.cpp 是另一条技术路线,纯 C++ 实现,主打单机、跨平台,模型格式统一用 GGUF。Q4_K_M 是 GGUF 生态里的 4bit 量化方案,属于 K-quant 家族。它的做法不是把整层权重统一压到 4bit,而是按张量的重要性分桶:大部分权重用 Q4_K 的 4bit,关键的部分偷偷升到 Q5_K 或 Q6_K,整体平均下来约 4.5~4.8 bit。这样既把模型体积压到接近四分之一,又保持住了推理质量。而且它不挑硬件,CUDA、CPU、Android 都能跑,甚至手机上的 llama.cpp 版本也常常见到这个格式。所以这两个方案的对比,本质是“新硬件专属量化”和“老硬件友好量化”之间的碰撞。
1.3 这场对比的真实结论(先剧透)
直接说结果,省得大家往后翻。纯从“能不能用”来看,vLLM + FP8 在 V100 上启动就会失败,vLLM 自身会检查 compute capability,sm_70 直接被拒。我通过换老版本、改源码、跳过检查的方式强行加载,确实能生成文字,但速度完全没有 FP8 该有的样子,甚至比 FP16 还慢,因为计算路径被逼到了软件模拟和反量化上。这里没有奇迹,只有架构限制。
llama.cpp + Q4_K_M 则全程顺畅。从 convert 到 quantize,再到双卡 split layer,都没有大问题。单请求 decode 稳定在 60 token/s 左右,4 并发总吞吐能到 140 token/s 以上。虽然 prefill 比 vLLM 慢,但对单机小并发、长文本生成的场景,它是最省心、最划算的方案。如果问我值不值得在 V100 上试 vLLM FP8,我的答案很直接:不值得,除非你打算顺便换掉这两张卡。
2. 方案设计与关键参数选择
2.1 模型选型:为什么拿 Qwen2.5-7B-Instruct 当基准
这次对比需要一个能同时跑在 vLLM 和 llama.cpp 上的模型,我选了 Qwen2.5-7B-Instruct。原因有几个:7B 规模在 2×V100 32GB 上很舒服,FP16 权重约 15GB,TP=2 之后每卡不到 8GB;Qwen 系列在 vLLM 和 llama.cpp 两边支持都非常完整,GGUF 社区资源也多,不容易踩格式坑;而且它的分词器、模型结构和主流部署框架兼容性都极好,换 DeepSeek-R1-Distill-Qwen-7B 这类同结构模型也能直接套用。
没有选 13B 或 32B 是因为不想把“量化差异”和“显存溢出”混在一起。13B 的 FP16 权重 26GB,没有 NVLink 的双卡跑 TP 会非常难受;32B 更是直接出界。7B 是一个足够干净的单变量窗口:同样的模型、同样的权重,只看两种推理栈和两种量化方式的效果。这样出来的数据才有参照价值。另外我也用同一模型跑了 vLLM FP16 作为基线,后续对比才公平。
2.2 量化路径对比:FP8 格式、Q4_K_M 格式的底层差异
FP8 格式分为 E4M3 和 E5M2 两种。E4M3 精度高,适合权重和激活;E5M2 动态范围大,通常用在梯度或特殊场景。vLLM 的 FP8 支持的是 W8A8,也就是权重和激活都是 8bit,矩阵乘要用 FP8 张量核心指令。这个指令在 sm_89(Ada)和 sm_90(Hopper)上存在,在 sm_70(Volta)上完全不存在。所以哪怕你把权重从 FP16 转成 FP8,V100 也只能先把 8bit 权重反量化回 FP16,再用老 Tensor Core 算。多一步转换,多一层开销,实际收益为负。
Q4_K_M 的逻辑完全不一样。它是纯后训练量化,不依赖任何特殊指令,权重以 4bit 整数块加 scale 的形式存储,计算时在 kernel 内部反量化成 FP16 参与矩阵乘。llama.cpp 对这种 dequant 路径做了大量 CUDA 优化,向量化加载、寄存器复用都做得非常激进。直观理解就是:Q4_K_M 用显存带宽换模型体积,把“访存量”这个瓶颈直接砍到四分之一。在 V100 这种带宽 900GB/s 但算力老、指令集受限的卡上,正好扬长避短。这也是它能在 V100 上表现这么好的根本原因。
2.3 为什么没有选 INT8 / GPTQ / AWQ 做中间对照
vLLM 在 V100 上其实能跑 GPTQ 和 AWQ INT4,也能跑几种 INT8 方案,这些都比 FP8 兼容得多。但我没把它们拉进正式对比,因为标题里就两个主角——vLLM FP8 和 llama.cpp Q4_K_M。如果把 INT4 全家桶都塞进来,文章会变得既散又长。为了避免“vLLM 在 V100 上完全不能用”的误会,我在第 4.4 节单独补充了 vLLM + FP16 的基线数据,作为正常可用方案的参照。
另外一个原因是,SGLang、TensorRT-LLM 这些框架也常被拿来和 vLLM 放在一起聊,它们各有优势,但本质上都会遇到同一个问题:老卡的指令集限制了高级量化格式。真正决定量化方案能不能用的,不是框架宣传,而是 GPU 的 SM 版本和 Tensor Core 能力。V100 上排前两位的合理选择就是 FP16 和 4bit 整数量化,FP8 是给后来者准备的。所以我不额外铺开 INT8/GPTQ/AWQ,只把结论放在可操作层面。
3. 环境搭建与实操过程
3.1 驱动、CUDA、Docker 镜像选择
系统我用的 Ubuntu 22.04,内核 5.15,NVIDIA 驱动 550.54.14。vLLM 官方支持 Linux,在 Windows 上不是原生跑法,必须靠 Docker 或 WSL2 兜底。驱动装好后先干一件事:nvidia-smi确认两张 V100 都在,NVLink 状态正常。这里有个老卡专属坑:V100 如果之前在 Windows 下被改成 WDDM 模式,切回 Linux 后显存和计算资源可能不干净,服务器环境务必确保 TCC 模式,让显存完全归 CUDA 调度。
vLLM 我直接用官方 Docker 镜像,选 release 版本,不追最新。镜像本身不带模型权重,很多人第一次用会懵:docker run进去后找不到模型。解决方式是把模型目录通过-v挂载进去。我给一个可复现的启动命令:
docker run --gpus all \ -v /data/models:/models \ -v /data/cache:/root/.cache \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model /models/Qwen2.5-7B-Instruct \ --tensor-parallel-size 2这里的latest要换成你验证过的稳定 tag,不要用什么冷门编译版。启动后用curl http://localhost:8000/v1/models验证服务状态,再通过 ChatBox 这类 OpenAI 兼容客户端连接就行。llama.cpp 不走 Docker,直接源码编译,流程见下一节。
3.2 vLLM 在 V100 上加载 FP8 权重的实际操作与报错
先准备 FP8 权重。我用 AutoFP8 对 Qwen2.5-7B-Instruct 做了静态 W8A8 量化,output 目录里是 FP8 的 checkpoint。接着用 vLLM 启动:
docker run --gpus all \ -v /data/models:/models \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model /models/Qwen2.5-7B-FP8 \ --quantization fp8 \ --tensor-parallel-size 2启动日志里很快出现类似FP8 is not supported on this GPU type (sm_70)的报错。这是 vLLM 主动做的安全拦截,防止老卡跑到一半崩溃。如果你在别的平台看到这个错误,第一反应应该是去查 compute capability,而不是怀疑模型损坏。
我没死心,试了老版本 vLLM 并改了 capability 检查,让它强行加载。运行后观察nvidia-smi,GPU 利用率非常低,decode 速度只有个位数 token/s,prefill 也被反量化拖住。翻源码后确认:vLLM 根本没有为 sm_70 编写 FP8 kernel,唯一的路径是走 dequant 到 FP16 的通用 kernel。所以强行跑通只能算技术验证,不能作为生产方案。这个平台上的正确选择,要么 FP16,要么直接上 llama.cpp。
3.3 手动编译 llama.cpp CUDA 版并量化出 Q4_K_M
llama.cpp 编译不复杂,但有个关键参数:CMAKE_CUDA_ARCHITECTURES。如果不指定为 70,生成的 kernel 可能不含 V100 的 SASS,只能靠 PTX JIT 编译,性能打折。我用的命令:
git clone https://github.com/ggml-org/llama.cpp cd llama.cpp cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=70 -DCMAKE_BUILD_TYPE=Release cmake --build build -j16量化流程分两步:先把 HuggingFace 格式转成 GGUF FP16,再用 llama-quantize 转成 Q4_K_M。
python convert_hf_to_gguf.py /data/models/Qwen2.5-7B-Instruct \ --outfile /data/models/qwen2.5-7b-f16.gguf --outtype f16 ./build/bin/llama-quantize /data/models/qwen2.5-7b-f16.gguf \ /data/models/qwen2.5-7b-q4_k_m.gguf Q4_K_M转出来的 Q4_K_M 文件大概 4.4GB,比 FP16 的 15GB 小很多。加载测试:
./build/bin/llama-cli -m /data/models/qwen2.5-7b-q4_k_m.gguf \ -ngl 99 --split-mode layer -n 128 \ -p "你好,请简单介绍你自己" --no-display-prompt-ngl 99表示所有层都放到 GPU,--split-mode layer表示把网络层按顺序分到两张卡上。这样就把 llama.cpp 在 V100 上的运行环境跑通了。
3.4 双卡并行配置:vLLM TP=2 与 llama.cpp split-mode layer
vLLM 这边,--tensor-parallel-size 2是标准张量并行。每张卡只保留部分权重,矩阵乘中间结果需要跨卡通信。V100 的 NVLink 2.0 带宽约 300GB/s,在 7B 模型上足够用,所以 TP=2 的表现正常。如果模型和 KV cache 占满显存,建议设--gpu-memory-utilization 0.9,留一点余量给 CUDA context,避免 OOM。
llama.cpp 的双卡方式和 vLLM 完全不同。它的 split-mode layer 是把 Transformer 层切成两半,卡 0 跑前一半,卡 1 跑后一半,前层输出通过 NVLink 传给后层。--tensor-split 1:1是均匀分配。看起来简单,但有个隐性代价:单请求生成时,每个 token 都要跨卡传递中间结果,NVLink 延迟会吃掉一部分性能。当 batch 小的时候,双卡不一定比单卡快;当 batch 上去之后,两张卡并行处理多个请求,吞吐优势才显现。所以双卡拆分不是无脑更快,具体用哪种模式要看你的实际负载。
4. 实测数据与结果解读
4.1 测试方法:单请求、并发、prefill/decode
为了公平,统一用 Qwen2.5-7B-Instruct,输入固定 512 token 的中文 prompt,输出限制 128 token,温度设为 0,关闭采样。单请求场景测两个指标:prefill tokens/s(处理 prompt 的速度)和 decode tokens/s(生成阶段每个 token 的速率)。并发场景用 4 个请求同时打,统计总吞吐。vLLM 用自带 benchmark 脚本加 OpenAI 兼容接口各核对一轮,llama.cpp 用 llama-bench 加手写计时脚本。
每组测试跑 5 次取中位数,不是极限压测,但足够说明趋势。所有测试在同一天、同一台机器上完成,没有超频,没有动态频率干扰。值得一提的是,老卡的散热会影响成绩,V100 如果是被动散热,机箱风道不好,测试时温度会冲到 85°C 以上,导致频率下降。我这次特意把风扇拉高,确保结果不受降频污染。
4.2 关键结果汇总表
下面是这次实测的关键数据汇总:
| 方案 | 权重格式 | 双卡显存占用/卡 | Prefill (tok/s) | Decode (tok/s) | 4并发总吞吐 (tok/s) |
|---|---|---|---|---|---|
| vLLM FP8(强行加载) | FP8→FP16 反量化 | 约 8.5 GB | 约 1400 | 约 8 | 不稳定 |
| vLLM FP16(TP=2) | FP16 | 约 10.5 GB | 约 1900 | 约 38 | 约 115 |
| llama.cpp Q4_K_M(split-layer) | Q4_K_M | 约 2.8 GB | 约 720 | 约 62 | 约 145 |
先看“强行加载”那一行:prefill 看起来还有 1400 token/s,但 decode 只有 8 token/s,几乎无法正常对话。这说明反量化路径只在长 prompt 的大矩阵阶段勉强能用,到逐步生成阶段就彻底拉胯。vLLM FP16 的 prefill 优势非常明显,1900 token/s 的来源是 PagedAttention 和大矩阵的 Tensor Core 调度,这是 vLLM 的绝对强项。llama.cpp Q4_K_M 的 prefill 只有 720 token/s,但 decode 和并发吞吐都反超了。
4.3 为什么 Q4_K_M 在 V100 上反而高效
decode 阶段有个很好用的估算模型:假设生成一个 token 需要把模型全部权重读一遍,吞吐上限就是显存带宽除以权重大小。V100 的 HBM2 带宽约 900GB/s,FP16 权重 15GB,理论极限 900/15=60 token/s,实测 38 已经接近天花板;Q4_K_M 权重 4.4GB,理论极限 900/4.4≈204 token/s,实测 62 是因为还有 KV cache 读写、dequant 开销和层间同步。但天花板差距摆在那,4bit 的优势是实打实的。
prefill 阶段则完全反过来。它主要吃算力,vLLM 的 FP16 Tensor Core 能把 V100 的矩阵算力吃满,而 llama.cpp 的 CUDA kernel 虽然优化不错,但毕竟不是为大规模并发调度设计的,对 prefill 大矩阵的处理不够激进。所以我在实际使用里的组合策略是:长 prompt、高并发、多用户场景用 vLLM;长生成、低并发、单用户场景用 llama.cpp。两个方向正好互补。
4.4 如果一定要用 vLLM,V100 上该选什么量化
vLLM 在 V100 上不是没有活路,只是别碰 FP8。官方支持的老牌量化里,GPTQ 和 AWQ 都跑得动,权重用 INT4 后显存压力比 FP16 小很多,decode 也会比 FP16 略快,prefill 则有一些反量化开销。如果你的应用是内部 API、并发不高、对延迟不敏感,FP16 反而最省事,不用额外准备量化模型。参考数据里 vLLM FP16 单请求 decode 38 token/s、4 并发总吞吐 115 token/s,已经完全够支撑小规模团队实用。
另外,vLLM 的架构优势集中在并发调度:EngineCore 统一管理 KV cache,scheduler 决定连续批处理里每个 sequence 的增删和抢占,executor 把调度结果派发给 GPU worker。这套流程在几十路并发下很强,但在 1~2 张老卡的小规模场景里,调度开销会明显挤占算力。所以我的建议是:如果只是给自己或三五个人用,别盲目给 vLLM 加并发负载;如果一定要上 vLLM,也尽量锁定稳定版本,别追新,因为 scheduler/executor 的重构在老卡上不总是正向优化。
5. 常见问题与排查实录
5.1 FP8 模型加载直接报“not supported on this GPU”
这个报错很直白:你的 GPU 算力低于 FP8 要求。先确认nvidia-smi输出里的 Compute Capability,如果是 7.x,就说明这张卡没有 FP8 硬件。别试图靠改代码硬闯,vLLM 后续版本会不断收紧检查,hack 也会失效。更合理的做法是换个量化方案,比如 Q4_K_M 或 GPTQ。如果确实想完整感受 FP8 的速度,建议用 RTX 4090、RTX 6000 Ada 或者 L40S 以上架构测试,再回来对比老卡上的 4bit,你会直观感受到“新卡+FP8”和“老卡+4bit”的代差。
这里也有个常见误判:FP8 模型文件能下载、能加载到内存,不代表 GPU 能正确加速。很多人以为 vLLM 只要能启动就万事大吉,其实启动后的 kernel 是否走专用路径才是关键。V100 上 FP8 即使跳过检查,也只是通用 dequant 路径,性能和稳定性都没有保障。
5.2 双卡拆分后速度不升反降
llama.cpp 用--split-mode layer后,单请求 decode 比单卡慢是正常现象,因为每一层输出都要从一张卡传到另一张卡。NVLink 再快也有延迟,更何况很多 V100 实际是 PCIe 互联,没接 NVLink。这时候双卡不仅没有收益,通信还会成为瓶颈。解决办法有三种:小 batch 场景直接用单卡跑;或者用--tensor-split手动调比例,让显存大的卡多放几层;再或者换 row split 模式,如果编译版本支持的话。
vLLM 的 TP=2 如果出现性能下降,优先检查 NVLink 是否启用。nvidia-smi nvlink -s能看到链路状态。如果显示 DISABLED,要么是驱动问题,要么是两张卡插在走 PCIe Switch 的不同槽位上。X99 平台上尤其要注意插槽路径,很多主板的 PCIe 通道分配很怪,两张卡都跑在 x8 或共享带宽时,TP 通信会受到明显限制。
5.3 Windows 下 V100 驱动与 TCC/WDDM 模式
V100 是数据中心卡,在 Windows 下要特别注意驱动模式。TCC(Tesla Compute Cluster)模式不输出显示信号,显存完全给计算用,CUDA context 切换开销也小;WDDM 模式把它当成普通显示卡,显存会被桌面占用,性能打折。很多人在 Windows 下用显卡坞跑 V100,发现速度不对,第一件事就是查驱动模式。切换命令大致是nvidia-smi -g <id> -dm 1,然后重启,具体以当前驱动版本的支持为准。
vLLM 官方不支持 Windows,想在 Windows 上用就走 WSL2 或 Docker Desktop,底层还是 Linux 容器,省心很多。llama.cpp 在 Windows 下可以直接编译运行,但如果想追性能,照样建议切成 TCC 模式。这个细节容易被忽略,却是 V100 在桌面平台上最影响体验的几个点之一。
5.4 vLLM 版本频繁更新,性能反而波动
vLLM 迭代速度极快,至少在 V100 这种老卡上,新版本不一定是正向优化。我遇到过同一个服务从某个 commit 升级到下一个 minor release 后,小 batch 的 prefill 延迟反而升高,原因就是 scheduler 逻辑变了,对低并发场景的调度更重。所以不要因为某个新功能就盲目升级。生产环境固定一个经过验证的 release 版本,模型版本、镜像 tag、启动参数都锁死,线上稳定性比功能多重要得多。
另外,镜像 tag 也不要随便用网上的自定义版本。vLLM 官方 Docker 镜像很大,但省去了手动装 CUDA 依赖的麻烦。再次强调,镜像里不会带任何模型权重,所有模型都要通过 volume 挂载进去。先跑通最简启动,再谈优化。我自己就吃过“镜像换了个 tag,模型路径变了”的亏,重新梳理以后才把环境彻底固定下来。
5.5 常见问题速查表
| 现象 | 原因 | 处理建议 |
|---|---|---|
| FP8 加载报错 | V100 算力 sm_70,不支持 FP8 | 换 FP16 / INT4 / Q4_K_M |
| llama.cpp 双卡慢 | NVLink 未启用或 split 层分配不均 | 检查 NVLink,调整 tensor-split,或单卡 |
| Windows 下性能低 | WDDM 驱动模式 | 切 TCC,重启用 Linux 容器 |
| vLLM 新版本变慢 | scheduler/executor 重构 | 固定 release 版本 |
| Docker 镜像找不到模型 | 镜像里没带模型 | 挂载外部 volume |
| V100 ECC 报错 | 显存老化 | nvidia-smi 查 ECC,必要时换卡 |
6. 收尾:一点个人实操心得
整套折腾下来,我个人最深的印象不是 FP8 怎么失败,而是 llama.cpp 把 V100 的带宽吃得非常干净。FP16 权重的 15GB 让这张卡在 decode 时始终被显存带宽卡住,换成 Q4_K_M 之后,4.4GB 的权重跑起来就像换了一张卡。如果你也是 2×V100 在手,我的建议很简单:日常单机部署用 llama.cpp + Q4_K_M,要接多用户、做并发接口就用 vLLM + FP16 或 INT4,千万别在 FP8 上浪费时间。我后来也在自己的 AI 工作流里把默认推理栈换成了这套组合,热度退去之后,老平台反而更耐打。