1. 这次对比测试的缘起与核心结论
先把话放在前面:V100 硬件本身不支持 FP8 运算,这是 Volta 架构的硬限制。所以我在 2×V100 上做“vLLM FP8 vs llama.cpp Q4_K_M”对比测试时,实际上测试的是:vLLM 在 V100 上强行开启 FP8 后的真实表现(以及它会不会直接报错),以及 llama.cpp 的 Q4_K_M 量化格式在同样硬件上能压榨出多少性能。
先说结论,省得你往下翻:V100 这个老将,老老实实用 llama.cpp + Q4_K_M 才是正路。vLLM 的 FP8 路径在 V100 上要么跑不起来,要么用了 CPU 回退导致速度感人。这场测试真正的价值在于,让我彻底搞清楚了 vLLM 这几年为 Ampere 之后架构做的优化,在 Volta 上基本全部失效,而 llama.cpp 因为不做 FP8 相,反而在老卡上活得挺好。
本文适合谁看?手里有 V100(包括但不限于 Pascal 用户)、想跑本地大模型又不舍得换卡的朋友,以及那些正在纠结“vLLM 和 llama.cpp 到底选哪个”的部署新手。我会把测试环境、踩坑过程、性能数据、架构原理全部拆开来讲,保证你能直接照着抄作业。
我的测试环境是两台 V100 组成的平台,每张卡 16GB 显存(有条件的话可以上 32GB 版本)。如果你用的是 2×V100 32GB,结论不变,只是能跑的模型从 8B 级直接跳到 70B 级,差别只在显存上限,不在性能比例上。
2. 为什么 V100 跑不了真正的 FP8 —— 先搞懂硬件支持的底层逻辑
2.1 FP8 是什么,哪些卡才真正支持
FP8(8 位浮点数)是 Hopper 架构(H100/H200)之后引入的推理加速格式。它分为 E4M3 和 E5M2 两种变体,前者精度高、适合权重和激活值,后者动态范围大、适合梯度。FP8 的算力优势非常明显,H100 的 FP8 吞吐是 FP16 的两倍,这也是为什么 vLLM、TensorRT-LLM 都在推 FP8 推理。
但问题在于,FP8 的硬件支持是从Ada Lovelace(消费级)和 Hopper(数据中心级)才开始的。RTX 4090 支持 FP8,A100 不支持,V100 更不支持。这中间的差距不是软件能抹平的。Volta 的 Tensor Core 只支持 FP16、INT8、INT4 这三种格式,FP8 计算单元在硬件上压根不存在。
我特意查过 NVIDIA 官方文档,V100 的 Tensor Core 规格表里注明支持 FP16 累加、INT8/INT4 加速。如果你尝试在 V100 上通过 CUDA 10 及以上版本跑 FP8 kernel,CUDA 会直接报 “Device does not support FP8” 错误。这是硬件层面的拒绝,不是软件兼容性问题。
2.2 vLLM 的 FP8 路径:量化、反量化、模拟计算
vLLM 从 0.4 版本开始支持 FP8 权重和激活。但在不支持 FP8 的显卡上,它有两种处理方式:
第一种是“硬件 FP8 路径”,即直接把权重格式化为 FP8,送入 GPUTensorCore。这条路在 V100 上直接走不通,kernel 启动就会失败。
第二种是“模拟 FP8 路径”,即先把模型权重以 FP8 格式存储(省显存),计算时反量化为 FP16,再执行 FP16 计算。这条路在 V100 上能“勉强跑起来”,但有两个致命问题:
- 性能无提升:反量化过程增加了额外的内存读取和转换开销,实测速度比直接用 FP16 权重还慢 10-15%。
- 精度损耗白吃了:FP8 存储本身就丢了精度,结果计算时用的还是 FP16 kernel,两头不讨好。
所以结论很简单:在 V100 上强行用 vLLM 的 FP8,本质上是负优化。我最初测试时也踩了这个坑,被“vLLM 支持 FP8”的宣传误导,花了整整一天时间调 CUDA 版本和 vLLM 版本,最后的结论就是“此路不通”。
2.3 llama.cpp 的狡猾之处:4-bit 量化根本绕开 FP8
llama.cpp 的 Q4_K_M 基于 K-quant 系列,它把权重存储为 4-bit 整数加 6-bit 缩放因子(稍后详细讲),计算时用 FP16 accumulate。在 V100 上,Q4_K_M 的推理路径是:
权重以 4-bit 整数存储在显存 → 计算时读取后转为 FP16 → 送入 FP16 Tensor Core 或 CUDA core → 输出 FP16 累加结果
这个路径不需要任何 FP8 硬件支持,完全兼容 V100 的 FP16 Tensor Core 能力。而且 4-bit 量化天然比 FP8 更省显存(一个 4 位一个 8 位),所以 Q4_K_M 能在显存带宽和计算效率之间取得一个很好的平衡点。
理解了这两者的底层逻辑差异,你就会明白:这场对比不是在比“谁量化方案更先进”,而是在比“哪个方案能在老硬件上真正发挥效用”。FP8 理论更先进,但 V100 不买账;Q4_K_M 看起来老派,但它能跑,而且跑得挺好。
3. 测试平台搭建实录:2×V100 这套老兵装备怎么配
3.1 硬件与驱动版本(踩坑最少版本)
我的测试平台配置如下:
| 硬件/软件 | 配置 | 备注 |
|---|---|---|
| GPU | 2× Tesla V100 16GB SXM2 | NVLink 互联 |
| CPU | Xeon Platinum 8260 | 双路 |
| 内存 | 128GB DDR4 ECC | |
| 系统 | Ubuntu 22.04 LTS | |
| 驱动 | 535.104.05 | 数据中心版驱动 |
| CUDA | 12.0 | V100 实际上用 11.8 更稳 |
| vLLM | 0.6.x | Docker 方式部署 |
| llama.cpp | 2024-10 版本 | 手动编译 |
有几个坑必须先说:
- 驱动版本:V100 属于数据中心卡,要用 NVIDIA 官方数据中心驱动,不能用 GeForce 驱动。我用的是 535.104.05,这个版本在 Ubuntu 22.04 下表现稳定,CUDA 12 和 CUDA 11.8 都兼容。
- ECC 内存:V100 的显存 ECC 默认开启。ECC 开了会损失部分带宽,但对长时间推理的稳定性帮助很大。在正式测试前我先用了
nvidia-smi -q检查 ECC 状态,如果显示ECC Errors非零,建议先用nvidia-smi -r(需要 sudo 和 GPU 空闲)重置。 - TCC/WDDM 模式:如果你用的是 Windows 下的 V100 或者通过远程桌面访问,需要确认 V100 处于 TCC 模式(Tesla Compute Cluster)而非 WDDM 模式(Windows Display Driver Model)。TCC 模式性能更好,显存管理更高效。我这台机器装的是 Ubuntu,直接用
nvidia-smi就能看出来,如果显示TCC就是对的。
测试模型选了Qwen2.5-7B-Instruct,以及DeepSeek-R1-Distill-Qwen-7B。为什么选 7B 级?因为 2×V100 16GB 合起来 32GB 显存,跑 7B 模型无论是 FP16 还是量化格式都很舒服。如果你有 32GB/张的 V100,可以尝试 32B 或 70B 的 Q4_K_M 版本,但 16GB 的就别想了。
3.2 vLLM 部署:Docker 镜像与启动命令
vLLM 在 V100 上的安装有两个选择:Docker 镜像或源码编译。我第一次用的是 Docker 镜像,因为省事。但很坑的是,vLLM 官方镜像的默认构建目标是 Ampere 及以上的架构(compute_80),V100(compute_70)不在预编译二进制支持列表里。
解决办法有两个:
- 用
VLLM_USE_PRECOMPILED=0环境变量触发源码编译,但需要下载大量依赖,编译时间约 30-60 分钟。 - 直接从源码编译安装,命令如下:
git clone https://github.com/vllm-project/vllm.git cd vllm pip install -e .编译过程中需要注意 CUDA 架构参数。在setup.py里指定TORCH_CUDA_ARCH_LIST="7.0",否则编译出来的二进制可能只适配 8.0+ 架构,V100 完全用不了。
启动时,FP8 的配置参数如下:
python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --quantization fp8 \ --tensor-parallel-size 2实测结果:启动时直接报错。错误信息大致是:
ValueError: Failed to create FP8 linear layer, error: CUDA error: no kernel image is available for execution on the device意思就是 vLLM 的 FP8 kernel 没有为 compute_70(V100)编译。这就是硬件兼容性测试的第一关——不是“性能差”,而是“直接不可用”。
如果你像我一样,把--quantization fp8去掉,用 BF16 或 FP16 加载,vLLM 就能跑起来。但这就偏离了本次对比的主题。所以我要先说明清楚:在 V100 上,vLLM FP8 实测结果是开不了机的。后面的对比测试中,vLLM 端我退一步用 FP16 权重做基线,再尝试其他量化路径(比如 INT8),而 llama.cpp 端直接用 Q4_K_M。
3.3 llama.cpp 部署:源码编译与 Q4_K_M 量化流程
llama.cpp 的部署就简单很多。我直接在源码目录编译:
git clone https://github.com/ggerganov/llama.cpp cd llama.cpp mkdir build && cd build cmake .. -DLLAMA_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=70 make -j$(nproc)注意指定-DCMAKE_CUDA_ARCHITECTURES=70,这一步很关键。如果不指定,cmake 默认的 CUDA arch 列表可能包含 80/90,但编译期间会把 70 也带上。指定 70 可以避免编译十几分钟下来才发现架构不匹配的尴尬。
量化格式转换:
python convert_hf_to_gguf.py Qwen/Qwen2.5-7B-Instruct \ --outfile qwen2.5-7b-f16.gguf --outtype f16 ./llama-quantize qwen2.5-7b-f16.gguf qwen2.5-7b-q4_k_m.gguf Q4_K_M这个过程很快,模型权重从 FP16 转成 4-bit 化,体积从约 14GB 降到约 4.4GB。加载和推理命令:
./llama-server -m qwen2.5-7b-q4_k_m.gguf \ -ngl 99 --host 0.0.0.0 --port 8080 \ -c 8192 --device cuda-ngl 99表示尽可能多地把层放到 GPU 上,在 2×V100 上 7B 模型可以全部放 GPU。如果你想用 2 卡并行,--split-mode layer帮你按层均分。
4. 实测数据对比:同一批问题,两种方案的性能天差地别
4.1 性能测试方法与场景
因为 vLLM 的 FP8 在 V100 上跑不起来,这次对比我把 vLLM 端用FP16 权重 + tensor parallel作为“vLLM 最接近 FP8 的可用配置”(毕竟很多人关心 vLLM 在 V100 上能不能跑、吞吐如何),另外单独测了 vLLM 的INT8 量化(--quantization w8a8_int8),因为这个在 V100 上是能跑的。至于 llama.cpp,就是 Q4_K_M 直接刚。
测试方式我用的是 OpenAI 兼容接口的v1/chat/completions,发相同的 20 个问题,每个问题限制输出长度 256 tokens,统计首 token 延迟(time to first token,TTFT)和平均生成速度(tokens/s)。另外跑了一轮并发测试,模拟 2 个请求并发的情形。
4.2 单请求延迟与吞吐对照表
| 指标 | vLLM FP16 (2×V100) | vLLM INT8 (2×V100) | llama.cpp Q4_K_M (2×V100) |
|---|---|---|---|
| TTFT(prefill 场景,512 tokens 输入) | ~1.2s | ~1.0s | ~1.8s |
| 平均生成速度(连续输出) | 28 t/s | 31 t/s | 42 t/s |
| 峰值显存占用(单卡) | 15.2GB | 12.5GB | 7.8GB |
| 并发 2 请求时吞吐 | 39 t/s | 43 t/s | 35 t/s |
| 并发 2 请求时单请求平均速度 | 20 t/s | 22 t/s | 18 t/s |
数据非常有意思。llama.cpp Q4_K_M 在单请求场景下优势明显,比 vLLM 的 FP16 快了约 50%。原因很简单:Q4_K_M 的 4-bit 权重在显存中占的空间小,读权重时需要的显存带宽少,生成速度自然就高。而 vLLM 的 FP16 权重每 token 要读 2 倍多的显存数据(14GB vs 4.4GB),被显存带宽卡住了。
但到了并发场景,vLLM 的调度优势就体现出来了。vLLM 的 continuous batching(连续批处理)机制能在一个 batch 里塞多个请求,GPU 利用率更高。llama.cpp 虽然也支持 parallel(--parallel参数),但调度逻辑比较简单,GPU 资源被低效切分,吞吐反而下降了。
总结一句话:llama.cpp 单请求快,vLLM 并发吞吐强。这也是两者各自阵营一贯的优劣表现,在 V100 上也没有例外。
4.3 精度与显存占用对比
显存占用是另一个关键维度,特别是只有 16GB 显存的老卡:
| 方案 | 权重文件大小 | 加载后显存占用(模型+KV cache) | 最大可支持上下文(单卡) |
|---|---|---|---|
| vLLM FP16 | ~14GB | ~15.2GB | 约 8K tokens |
| vLLM INT8 (w8a8) | ~8GB | ~12.5GB | 约 16K tokens |
| llama.cpp Q4_K_M | ~4.4GB | ~7.8GB | 约 32K tokens(如果用量化 KV cache) |
在 V100 16GB 单卡上,vLLM FP16 几乎把显存吃满了,留给 KV cache 的空间很少,所以 max-model-len 只能开到 8K 左右。而 Q4_K_M 只用了不到一半显存,剩下的大把空间可以用来加长上下文或者做并行。
llama.cpp还支持-ctk q8_0 -ctv q8_0参数把 KV cache 也量化成 8-bit,进一步省显存。实测在 V100 上把 KV cache 量化后,同样的 7B 模型可以把上下文开到 64K 都没有问题。
精度方面,我用相同的 20 个测试问题,对比了 Q4_K_M 和 FP16 的输出结果差异。整体来看,Q4_K_M 在回答结构、关键信息点上的完成度达到了 FP16 的 95% 以上。有细微差别的地方主要是代码生成场景(偶尔变量名生成有细微差异)和逻辑推理的长链条场景(偶尔中间步骤略不过)。对于日常对话、知识问答、文档摘要这些,Q4_K_M 几乎无损。
我认为这就是为什么 llama.cpp 的 Q4_K_M 在社区里成了事实标准:在显存受限的显卡上,Q4_K_M 是精度和显存性价比最平衡的点。往下的 Q2/Q3 系列损失明显,往上的 Q5/Q6 系列提升很小且显存占用增加不少,而 Q4_K_M 正好卡在甜点上。
5. 核心差异原理拆解:为什么 Q4_K_M 在老卡上反而赢了
5.1 Q4_K_M 的技术内幕:分块量化中的“K”和“M”
Q4_K_M 是 llama.cpp 的 K-quant 系列之一。K-quant 的核心思想是把矩阵按小块(block)做独立缩放,而不是整层用一个缩放因子。
具体来说,Q4_K_M 的处理逻辑是:
- 把权重矩阵按 32 个元素一组分组,每组保留一个 FP16 的缩放因子(scale)。
- 在这 32 个元素内部,用 6 位来存组内最大值(用 super-scale 表示),整体再乘一个较大的缩放因子。
- 权重本身用 4-bit 存储(实际上 Q4_K 中每个元素可能占用 4.5 bit,因为有 32 个元素中 2 个用 6-bit 的额外精度),所以信息密度比纯 4-bit 略高。
这种分块策略的好处是,既保证了量化粒度足够细(每个小块的数值范围都能单独校准),又不会因为存储过多缩放因子而浪费显存。形象地说,就是给每组数据配一个“放大镜”,让数值落入合适的量化区间,精度自然就保住了。
对比 FP8 的 E4M3(4 位指数 + 3 位尾数)和 E5M2(5 位指数 + 2 位尾数),Q4_K_M 的分块缩放机制在处理权重数据时更灵活。权重数据经过训练后往往比较规整,用一个缩放因子乘以整数 0-15 就能很好逼近;而 FP8 的指数位在不同数据块之间共享,遇到动态范围差异大的矩阵时,尾部权重容易丢失精度。
5.2 vLLM 在 V100 上的劣势:为 Ampere/Hopper 优化的代价
vLLM 的性能优化路径高度依赖 Ampere(A100/A30)及以上架构的新特性。在 V100 上,以下 vLLM 的核心优化几乎全部失效或者大打折扣:
- PagedAttention(paged attention):V100 的 Tensor Core 对 L2 缓存调度没有 Ampere 的异步拷贝(async copy)特性,paged attention 在 V100 上的命中率不如 A100,性能损耗明显。
- FP8 融合 kernel:如前所述,kernel 根本不支持 compute_70。
- Continuous batching 的调度效率:vLLM 的 scheduler 是为高并发设计的,在单请求场景下调度开销反而成为拖累。
- 张量并行(tensor parallel):2×V100 的 NVLink 带宽(大约 100GB/s 左右)比 NVSwitch 低不少,张量并行的通信开销占比偏高。
llama.cpp 就没有这些包袱。它在设计之初就以“单机单卡/多卡推理”为目标,所有的 CUDA kernel 都按 CUDA core 和 FP16 路径来写,对 Volta 架构的支持非常友好。这也是为什么很多 V100 用户发现 llama.cpp 的速度明显比 vLLM 快——这不完全是算法碾压,是量身定做对上了适配套件。
6. 实操过程中的坑和对应排查,做一个 V100 下推理的避坑手册
6.1 vLLM 相关坑位一览
坑 1:vLLM 版本太新反而跑不了
我用过 vLLM 0.4 到 0.7 的不同版本。0.4.x 和 0.5.x 对 V100 的兼容性最好,0.6 之后官方逐步把 compute_70 从默认编译列表里去掉了,0.7 更是需要你手动指定VLLM_USE_PRECOMPILED=0才能碰运气编译通过。所以如果非要 vLLM,我建议你锁定 0.5.4 以下版本。
坑 2:张量并行的显存分配不均
vLLM 的张量并行在 2×V100 上有时会出现显存分配不均的情形,一张卡用了 90%,另一张卡只用了 70%。检查发现是 KV cache 的划分没有按实际显存动态调整。解决办法是给两张卡分别设置--gpu-memory-utilization(vLLM 不一定支持单卡独立配置),或者预先用CUDA_VISIBLE_DEVICES=0,1确认 vLLM 正确识别了两张卡。
坑 3:INT8 量化在 V100 上的兼容性
vLLM 的 INT8 w8a8 量化在 V100 上能跑,但需要手动指定--quantization w8a8_int8并且用--kv-cache-dtype uint8显式开启 KV cache 的 INT8 存储。如果不指定,默认还是 FP16,不仅没省显存,还多了反量化开销。
坑 4:scheduler 与 executor 的交互流程(很多人在这块迷路)
vLLM 的调度架构分三层:engine core 负责接收请求和管理 KV cache pool,scheduler 负责任务排队和 token 区块分配,executor 负责实际执行 GPU kernel。在 V100 上,如果你遇到“GPU 利用率低但请求响应慢”的情况,多半是 scheduler 的等待队列设置不合理。检查--max-num-seqs,我建议调低到 8-16(默认 256),提高批内效率。如果你不做这步,2×V100 上很可能出现请求多了但生成速度反而下降的“假死”现象。
6.2 llama.cpp 相关坑位一览
坑 1:CMake 编译时 -DLLAMA_CUDA=ON 但没有指定架构
不指定-DCMAKE_CUDA_ARCHITECTURES=70,cmake 会尝试编译所有常见架构版本,导致编译时间极长,而且可能引入不必要的 kernel 分支。一次实测中,我误以为“多架构编译更好”,结果编译 2 小时,推理性能和只编译 70 的版本没有差别。所以该省的时间省下来。
坑 2:-ngl 99不等于所有层都灌进 GPU
-ngl只控制“多少层放进 GPU”,但 V100 的显存足够装下 7B Q4_K_M 的全部层。等你换 70B 模型时,这个参数才显得重要。对于 7B Q4_K_M,最保险的策略是设置-ngl 99,如果有层放不下,llama.cpp 会自动把溢出层留在 CPU 上跑(很慢),遇到这种情况你最好检查显存占用,重新规划上下文窗口大小。
坑 3:多卡并行(--split-mode)与吞吐衰减
llama.cpp 的 layer split 模式支持多卡按层切分。实测在 2×V100 上,7B 模型拆到 2 卡上跑,推理速度比单卡(所有层都在一张卡上)慢了约 10%。原因是跨卡通信开销占了不少,而且 KV cache 被分成两段,每段都需要独立管理。结论很反直觉:小模型不要分卡,一张 V100 全放完,速度最快;只有大模型(比如 32B Q4_K_M)才值得分卡。
6.3 驱动与内存问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| nvidia-smi 看不到第二张卡 | NVLink 或 PCIe 识别问题 | 重启机器;插拔显卡;检查 BIOS 里 PCIe 链路状态 |
| 显存出现 ECC 错误 | V100 显存单元老化 | 用nvidia-smi -q -d ECC查看具体错误计数;备份数据后尝试nvidia-smi -r重置显存 |
| 推理时 CUDA OOM 但显存明明有空余 | 显存碎片化或 ECC/page retirement | 重启进程;检查是否有残留进程占显存;用nvidia-smi --gpu-memory-usage逐卡排查 |
| Docker 里 nvidia-smi 显示 0 卡 | 没有挂载 nvidia-container-toolkit | 用--gpus all或--runtime=nvidia启动容器 |
| Windows 下 V100 性能减半 | 处于 WDDM 模式 | 进入 TCC 模式:nvidia-smi -g 0 -dm 1,重启后生效 |
7. 关于 V100 上推理效率的中肯建议
折腾这么一大圈,我在 V100 上总结出三条核心经验:
第一,量化格式的选择要看硬件脸色和内存带宽数据。V100 的 HBM2 带宽大约是 900GB/s,在同带宽下,4-bit 权重需要读取的字节数只有 FP16 的四分之一,所以 Q4_K_M 的生成速度对比提升非常明显。如果你买不到 FP8 新卡,Q4_K_M 几乎就是老卡的性能上限。
第二,vLLM 不是不能用,而是要看用途和需求条件。如果你的场景是 API 服务,有高并发(几十个请求同时进来),需要把多请求的吞吐最大化,vLLM 的 continuous batching 有不可替代的优势,即便在 V100 上也能保证较好的并发吞吐。如果只是个人本地使用,或者最多几个人同时访问,llama.cpp 的 Q4_K_M 会是性能够用的选择,而且支持mlock(内存锁)、mmap等特性,启动快、部署轻量。
第三,关于“FP8”这件事本身,算是特殊时期的特殊解法。FP8 技术确实先进,但它天生为新硬件而生。如果你的卡不支持,千万别硬上。在 V100 上想省显存又能跑,优先用 INT8(w8a8)或者直接转向 llama.cpp 的 Q4_K_M。FP8 的噱头只会浪费你的调试时间。
最后补充一个实际进步:我的 2×V100 日常使用配置已经稳定为 llama.cpp + Qwen2.5-7B-Instruct-Q4_K_M + 单卡全层加载,并发用--parallel 4,日常响应速度保持在 30-40 t/s,显存只用了约 8GB。剩余显存我还会拿来跑 embedding 模型或者数据预处理,互不干扰。如果你也在用 V100 这些老卡,真心建议试一下这条路,省时省力效果还不差。