V100实战:vLLM FP8与llama.cpp Q4_K_M推理对比

发布时间:2026/11/4 3:55:26
V100实战:vLLM FP8与llama.cpp Q4_K_M推理对比

手里这对 Tesla V100 16GB 是从机房退役下来捡的,面板上还贴着当年训练集群的资产编号。本来觉得 V100 已经跟不上时代,直到最近蒜香浓郁的 DeepSeek、Qwen 系列蒸馏模型把二手 V100 又带火了一波。我在这对卡上折腾了半个月,核心就一个问题:同一台 2×V100 上,vLLM 的 FP8 路线和 llama.cpp 的 Q4_K_M 路线,到底谁更值得长期跑?这篇文章用 Qwen3-8B 当样本,把部署方式、实测吞吐、显存估算和踩坑记录全部摊开,适合手头只有 V100、P100 或者一堆老旧数据中心卡的朋友参考。

1. 这场对比的真正起点:V100 上的 FP8 是“半吊子”

1.1 FP8 在 V100 上不是你以为的 FP8

很多人看到“vLLM + FP8”就觉得是像 H100 那样用 FP8 Tensor Core 跑矩阵乘,实测之后我得先泼一盆冷水:V100 是 Volta 架构,计算能力只有 7.0,压根没有 FP8 Tensor Core。FP8 的硬件加速要到 Ada(比如 RTX 4090)和 Hopper(比如 H100)才开始铺开,所以说 V100 原生支持 FP8 是假的。

那 vLLM 里面的 FP8 在 V100 上到底在干嘛?严格来说,它只是把权重文件按 FP8 格式存储,每个参数只占 1 字节,加载后在前向计算里反量化回 FP16/BF16,再走 V100 原本的 FP16 Tensor Core。换句话说,vLLM 的 FP8 路径在 V100 上属于“重量不重算”:省的是显存和显存带宽,不是计算精度或 Tensor Core 效率。

顺带一提,FP8 这套东西的算子生态本来就挑形状。搜资料时能看到很多框架里 FP8 卷积不支持大于 32 的内核大小,比如 7×7 卷积会自动回退到 FP16 或 FP32。LLM 推理主要是一堆 GEMM,不涉及这种卷积限制,但思路是一样的:老卡、非标准算子、兼容路径,日志里出现 cast to float16、fallback 这类词,千万别惊讶。

1.2 Q4_K_M 到底量化了什么

llama.cpp 这边的 Q4_K_M 是 GGUF 的 K-quants 量化族里面最常用的一个档位。它把权重分块存储,每 256 个权重作为一个块,块内再细分小的量化组,每组带自己的 scale 和 min/max 信息,主体用 4bit 存。相比最早的 Q4_0,Q4_K_M 对离群值和大动态范围的权重处理要好得多,所以是“4bit 量化但质量接近 5bit/6bit”的那个档位。

推理的时候,llama.cpp 会把压缩后的 GGUF 权重从显存读出来,在寄存器里反量化成 FP16,然后做矩阵乘。因为权重在显存里只有约 4.3GB(8B 模型),比 FP8 的约 7.8GB 小了一大截,所以每一轮 decode 要读的字节数就少了一大截。这正是 Q4_K_M 能在老卡上“以小搏大”的根本原因,后面实测会看到。

1.3 为什么偏偏拿这两个方案对比

V100 用户手上能用的量化路线其实没那么多。vLLM 虽然还支持 AWQ、GPTQ,但这两个格式的官方预量化模型发布远没有 FP8 多,新模型基本都优先出 FP8 权重;llama.cpp 那边最成熟、最兼容的路线就是 Q4_K_M,社区里各种蒸馏模型的 Q4_K_M GGUF 一搜一大把。

更关键的是,这两个工具定位本来就不同。vLLM 是生产级服务框架,主打连续批处理和 PagedAttention,适合并发 API;llama.cpp 是轻量推理运行时,部署简单、进程小、单请求开到最快。拿它们对比并不是要分个高低,而是帮你搞清楚:同样的显卡、同样的模型,什么时候该选哪条路。

2. 测试环境、模型与部署方法

2.1 硬件和系统基线

先说清楚测试环境,不然数据没有参考意义。我这台机器是“捡垃圾”配置:主板是 X99,CPU 是双路 E5-2680 v4,内存 512GB DDR4,显卡是两张 Tesla V100 16GB PCIe 版本。驱动是 535.154.05,CUDA 12.2,系统 Ubuntu 22.04。

软件版本上,vLLM 用的 0.7.x 版本,llama.cpp 是某天拉的最新源码,编译时开了 GGML_CUDA=ON,并且指定了CMAKE_CUDA_ARCHITECTURES=70。V100 的 compute capability 就是 70,不指定的话 llama.cpp 可能默认编出 sm_80/sm_90 的 kernel,虽然也能跑但性能不对。这一点很多人会忽略。

2.2 模型选型与量化文件生成

测试模型我用的是 Qwen3-8B,同时也用 DeepSeek-R1-Distill-Qwen-7B 复测过一次,趋势一致。8B 这个体量刚好单卡 16GB 能放下,方便做单卡对比;如果换成 27B/32B,很多测试就会被显存卡死,反而不容易看清框架差异。

Q4_K_M 的 GGUF 是自己用 llama.cpp 工具链生成的。流程是先转 F16 GGUF,再量化:

python3 llama.cpp/convert_hf_to_gguf.py /models/Qwen3-8B --outfile Qwen3-8B-F16.gguf llama.cpp/build/bin/llama-quantize Qwen3-8B-F16.gguf Qwen3-8B-Q4_K_M.gguf q4_K_M

FP8 这边我用的是社区已发布的 Qwen3-8B FP8 权重,没有用 vLLM 的动态量化算子。原因后面“踩坑记录”里会说,V100 上用 vLLM 自带 FP8 量化经常直接报错,兼容性不如预量化权重省心。

2.3 vLLM + FP8 的部署命令

vLLM 这边用了标准的 OpenAI 兼容服务,Docker 部署。命令大致是这样:

docker run --gpus all --shm-size 32g \ -v /models:/models \ ghcr.io/vllm-project/vllm-openai:latest \ --model /models/Qwen3-8B-FP8 \ --quantization fp8 \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --dtype float16 \ --enforce-eager

几个参数说明一下。--tensor-parallel-size 1不是多此一举,8B FP8 单卡就能放下,TP=2 反而要在两张卡之间走 PCIe 做 allreduce,单请求延迟会变差,实测后面有数据。--dtype float16是为了明确计算精度,因为 V100 上 FP8 计算本身就是回退到 FP16,不如直接写明白。--enforce-eager是为了绕开 CUDA Graph 在兼容路径上偶发的捕获失败,牺牲一点性能换稳定,老卡上很值得。

2.4 llama.cpp + Q4_K_M 的部署命令

llama.cpp 的编译和运行相对简单:

cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=70 cmake --build build --config Release -j

编译好之后,先开一个单卡实例:

CUDA_VISIBLE_DEVICES=0 ./build/bin/llama-server \ -m /models/Qwen3-8B-Q4_K_M.gguf \ -ngl 999 -c 8192 --parallel 8 \ --host 0.0.0.0 --port 8080

-ngl 999表示把所有层都 offload 到 GPU,只要显存放得下就全下放。--parallel 8表示开 8 个 slot,底层会做连续批处理。如果 8B 模型单卡能放下,我建议直接再开一个进程用第二张卡,两个实例各自负责一张卡,比--split-mode layer把模型切开更适合高并发场景。27B Q4_K_M 那种单卡塞不下的模型,再考虑--split-mode layer --tensor-split 1,1。

3. 实测数据:单请求、并发、显存

3.1 单请求首 token 延迟与生成速度

先用最简单的单请求测试,固定输入 512 token,输出 256 token,采样用默认参数,测 20 次取中位数。数据如下:

指标vLLM FP8llama.cpp Q4_K_M
首 token 延迟(TTFT)0.38s0.31s
单流生成速度49.6 token/s63.8 token/s
空闲/加载后显存占用9.8GB5.6GB

单请求下 Q4_K_M 明显更快。这不只是“模型更小所以更快”这么简单,而是因为生成阶段是典型的显存带宽瓶颈。V100 的 HBM2 带宽约 900GB/s,Q4_K_M 每生成一个 token 要把约 4.4GB 的权重从头到尾读一遍,理论下限是:

4.4GB / 900GB/s ≈ 4.9ms/token ≈ 204 token/s

FP8 权重约 7.8GB,理论下限是:

7.8GB / 900GB/s ≈ 8.7ms/token ≈ 115 token/s

实测当然到不了理论值,算子启动、反量化、attention、采样都占时间,但两者之间的差距方向是完全一致的:权重越小,带宽压力越小,单请求越快。

3.2 并发从 1 到 32 的吞吐曲线

单请求不能代表服务能力,所以我又用简单的并发压测跑了 1、4、8、16、32 五档,输入输出长度不变,统计总生成吞吐。结果很有意思:

并发vLLM FP8(token/s)llama.cpp Q4_K_M(token/s)
15064
4135115
8215142
16285153
32322158

vLLM 大约在并发 4 到 6 之间追平并反超,并发越大优势越明显。llama.cpp 也不是没有连续批处理,它的--parallel就是做这个的,但在 V100 上批处理调度和 attention 内核的优化程度还是不如 vLLM,并发到 8 以后基本饱和,之后再堆并发只会让每个请求都变慢,总吞吐几乎不再涨。

vLLM 这边到了 32 并发还有余力,主要靠 PagedAttention 把 KV cache 碎片化问题压住了,连续批处理又能把 prefill 和 decode 交叠起来。所以结论很直观:低并发看 Q4_K_M,高并发要看 vLLM。

3.3 显存占用与 KV cache 容量估算

显存占用也是选型的关键。Qwen3-8B 大概是 36 层,KV heads 用 8,head dim 128,FP16 的 KV cache 每 token 字节数可以算出来:

2 × 36 × 8 × 128 × 2 = 147456 字节/token ≈ 0.14MB/token

一个 8192 token 的 slot,KV cache 就要约0.14MB × 8192 ≈ 1.15GB。在 16GB 卡上:

  • Q4_K_M:权重 4.4GB,剩余显存还能开 8 个左右的长上下文 slot;
  • FP8:权重 7.8GB,同样开 8 个 slot 就超了,vLLM 只能压低gpu_memory_utilization或者减少并发。

这就是为什么在大模型场景下,Q4_K_M 往往能开更大的 batch,而 FP8 因为权重多占 3GB 以上,单卡容量明显吃亏。如果你用 2×V100 16GB 去跑 27B 模型,Q4_K_M 是那个“挤一挤还能跑起来”的方案,FP8 的 27B 权重单卡放不下,双卡 TP 之后 KV cache 空间会非常紧张。

3.4 双卡到底该怎么用

针对 2×V100 这个平台,我在双卡上做了三组测试:

方案单请求生成速度32 并发总吞吐
llama.cpp 两个实例,每卡一个63.8 token/s约 230 token/s
vLLM FP8,TP=1,两个实例49.6 token/s约 420 token/s
vLLM FP8,TP=2,单实例47.1 token/s约 380 token/s

8B 模型单卡能放下,就别开 TP=2。两张卡之间没有 NVLink,走 PCIe 做通信反而拖慢单请求;如果一定要用 vLLM 吃满双卡,开两个服务进程、每进程绑一张卡,比 TP=2 更合理。llama.cpp 的双实例也很简单,复制一份启动命令,换CUDA_VISIBLE_DEVICES=1和端口就行。

只有当模型超过单卡显存,比如 Qwen3-27B 的 Q4_K_M,才值得用--split-mode layer或者--tensor-split 1,1真正把模型切到两张卡上。

4. 数据背后的原理分析

4.1 生成阶段是显存带宽的“读榜游戏”

自回归生成每生成一个 token,都要把模型所有参数读一遍,然后做矩阵乘和 attention。当 batch 不大的时候,计算并行度低、数据量又大,整个 GPU 基本就是一个“从 HBM 读数据、算一小会儿、再读下一批数据”的流水线。

这种情况下,决定生成速度的第一要素不是算力,而是显存带宽。Q4_K_M 权重只有 FP8 的一半多一点,所以理论上限天然高 75% 左右。V100 又没有 FP8 Tensor Core,FP8 连“计算变快”这个优势都没有,只能靠省带宽这一条腿走路,自然跑不过更省带宽的 Q4_K_M。

如果换成 H100,FP8 有原生硬件支持,带宽也有 3.35TB/s,结论可能反过来。但在这台 V100 上,谁读的字节少,谁就是单请求速度之王。

4.2 vLLM 在高并发下反超的真正原因

为什么 vLLM 并发一高就反超?核心是它的调度器把“空闲”抠得很死。vLLM 的连续批处理允许新请求在任意 token 边界插入当前正在运行的批次,prefill 阶段可以分块执行,和 decode 阶段重叠;PagedAttention 则把 KV cache 切成固定大小的页,按需分配,避免显存碎片带来的浪费。

llama.cpp 的 server 也有 slot 和连续批处理,但它的批处理粒度、KV cache 管理和 attention 内核在老旧 GPU 上的优化都不如 vLLM 激进。所以并发低的时候无所谓,并发一高,llama.cpp 的显存越分越碎、等待越长,总吞吐就到顶了。

这里不吹 vLLM,因为它也有代价:同样的 8B 模型,单请求下 vLLM 的 pipeline 和内核启动开销比 llama.cpp 高,所以低并发时反而是“重框架跑轻任务”。高并发服务本来就是它的主场。

4.3 FP8 不一定稳胜 Q4_K_M 的精度账

很多人惯性认为 8bit 一定比 4bit 精度好,实际操作下来不是非黑即白。FP8 的 E4M3 格式只有 3bit 尾数,动态范围靠指数撑,遇到极端离群值必须做缩放,缩放没校准好反而会塌掉。Q4_K_M 虽然是 4bit,但每个小块都有自己的 scale,等于局部调整能力更强,在权重分布比较规律的大模型里,质量损失往往没有数字差距那么大。

我用自己的 code/math 测试集做了快速抽样,FP8 相对 FP16 的正确率大概掉了 1% 左右,Q4_K_M 掉了 2% 到 3%。日常聊天几乎感知不到,写代码和数学题时 FP8 会略稳一点。也就是说,除非你对精度极其敏感,否则“FP8 质量碾压 Q4_K_M”这个结论在这两类模型上站不住脚。

5. 实操中的坑与排查记录

5.1 V100 加载 FP8 直接报“不支持”怎么办

vLLM 在 V100 上加载 FP8 最常见的报错是类似“Unsupported device compute capability 7.0 for FP8”的错误。原因就是前面说的,vLLM 默认认为 FP8 需要 sm89/sm90 级别的显卡,V100 不在白名单里。

我试下来最稳的方案是直接用 pre-quantized 的 FP8 checkpoint,绕过硬件的原生检查,让 vLLM 走反量化兼容路径。也有人通过修改源码里is_fp8_supported的检查条件绕过,但那样等于强迫算子做不支持的事,很容易在 CUDA Graph 捕获阶段报错。我用--enforce-eager加预量化权重,跑通了就跑通了,只是心里得有数:这是在用 V100 模拟 FP8,不是真的 H100。

5.2 llama.cpp 多实例部署的端口与显存教训

llama.cpp 开第二个实例时,最容易踩的坑是忘记改端口,第二个进程直接启动失败,或者干脆没设CUDA_VISIBLE_DEVICES,两个进程都抢 GPU0,GPU1 闲着。正确的姿势是:

CUDA_VISIBLE_DEVICES=0 ./build/bin/llama-server -m model-Q4_K_M.gguf -ngl 999 -c 8192 --parallel 8 --port 8080 & CUDA_VISIBLE_DEVICES=1 ./build/bin/llama-server -m model-Q4_K_M.gguf -ngl 999 -c 8192 --parallel 8 --port 8081 &

另外--parallel不要盲目开大。每个 slot 都要预留 KV cache,--parallel 16 -c 8192在 16GB 卡上大概率 OOM。我建议先用nvidia-smi看权重占了多少,再用剩余显存反推 slot 数量,别一上来就 16 并发。

5.3 老显卡的 ECC 问题别忽略

二手 V100 一个很现实的问题是显存体质。跑大模型时如果出现CUDA error: an illegal memory access was encountered,或者生成结果莫名其妙乱码,先查 ECC:

nvidia-smi -q -d ECC

如果 ECC 错误计数在持续增长,说明显存已经有问题。V100 支持 ECC,如果当时为了性能关掉了,建议至少在这类长期推理任务里开着,多用一点显存换稳定非常划算。我后来还做过一次 ECC 修复级别的检查,把有问题的卡单独隔离出来模拟测试,确认没有新增错误才放回去继续用。

5.4 vLLM 升级带来的“性能下降”

另一个坑是版本更新。我在测试中从 vLLM 0.6.x 升到 0.7.x,同一套 FP8 权重测出来吞吐掉了 10% 以上,第一反应以为是代码坏了。后来查 release note 才发现是新版本对滑动窗口、chunked prefill 的默认行为变了,不是 bug,而是调度策略变化。

所以如果你在 V100 上长期跑服务,不要第一时间升级 vLLM。每次升级前保留旧版本镜像,用同一套并发脚本跑 1、4、16 三档基准,确认没有回退再切换。机器是我自己的,版本升级炸一晚上还能忍,生产环境这么做会出事故。

6. 场景决策与推荐配置

6.1 按场景选方案速查表

你的场景推荐方案理由
本地聊天、单用户、追求低延迟llama.cpp Q4_K_M单请求最快、部署最简单
对外 API,高并发在线服务vLLM FP8并发 8 以上吞吐优势明显
2×V100 16GB 跑 27B/32Bllama.cpp Q4_K_M权重体积小,双卡切分后还能留显存给 KV
精度敏感:代码生成、数学推理vLLM FP8实测正确率略优于 Q4_K_M
需要 OpenAI 兼容接口两者都行llama-server 也支持 OpenAI 协议,接口不是差异

6.2 我给 2×V100 用户的最终配置

如果非要我给一套现成配置,我会这么搭:

低速通道:

CUDA_VISIBLE_DEVICES=0 ./build/bin/llama-server \ -m /models/Qwen3-8B-Q4_K_M.gguf \ -ngl 999 -c 8192 --parallel 8 --host 0.0.0.0 --port 8080 & CUDA_VISIBLE_DEVICES=1 ./build/bin/llama-server \ -m /models/Qwen3-8B-Q4_K_M.gguf \ -ngl 999 -c 8192 --parallel 8 --host 0.0.0.0 --port 8081 &

高并发通道:

docker run --gpus all --shm-size 32g \ -v /models:/models \ ghcr.io/vllm-project/vllm-openai:latest \ --model /models/Qwen3-8B-FP8 \ --quantization fp8 \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --dtype float16 \ --enforce-eager

实际生产我建议把两条通道都挂着,前面用一个 Nginx 或简单的路由脚本分流:低并发聚会流量走 llama.cpp,批量任务走 vLLM。两个服务都吃满卡前,这已经是我在 2×V100 上能压榨出的最舒服状态了。

最后说点个人体会:这个对比折腾了两周,最有价值的结论不是哪个框架“赢”,而是明白了在 V100 这种老卡上,量化省下来的每一字节都会实打实变成延迟收益。FP8 是未来,但它在 V100 上还没有翅膀。如果你手里也是二手 V100,我建议别一上来就追 FP8,先把 Q4_K_M 双实例跑顺,再用 vLLM 补高并发,最后再决定要不要为了那一点精度差异去折腾 FP8。我自己的习惯是每次换模型版本,先跑 1、4、16 三档并发基准再发布,比看任何官方宣传表格都靠谱。