ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

V100上vLLM FP8与llama.cpp Q4_K_M量化部署实测对比

V100上vLLM FP8与llama.cpp Q4_K_M量化部署实测对比

站在2024年底回头看,V100这代卡在AI部署圈子里依然有庞大的存量用户群,尤其是各种实验室和二手平台上淘来的双路卡。我手上正好有一台双V100 16GB的机器,平时拿来跑各种量化模型的测试。最近社区里对vLLM的FP8支持讨论得很热,也有人拿着llama.cpp的Q4_K_M来对比。很多人有个误区,觉得既然vLLM都支持FP8了,那V100应该也能吃到这个红利。实际上V100是Volta架构,根本原生不支持8bit浮点计算,这个差异直接决定了整个对比的走向。这篇文章就从我在同一台2×V100上实际折腾vLLM FP8和llama.cpp Q4_K_M的经历讲起,把踩过的坑、测出的数据、最后的选择都摊开来说。

1. 先别急着对比,V100这代硬件根本吃不下FP8

1.1 硬件的"代沟":Volta、Ampere与Hopper的数值格式分水岭

V100发布在2017年,它身上最强的是Volta架构的Tensor Cores,支持FP16和INT8计算。INT8模式可以带来接近双倍的吞吐,这也是当年深度学习部署比较常用的加速手段。而FP8这个格式是Ampere时代的A100开始"半引入",真正完整支持FP8 E4M3/E5M2得等到Hopper的H100,还有Ada Lovelace的L4、L40S这些消费级和数据中心卡。简单说,V100的Tensor Core硬件电路里就没有FP8这条通路,你硬要用,它只能走普通CUDA核心的软件模拟,效率会崩得一塌糊涂。

vLLM在0.5.x之后引入了FP8量化支持,但它的实现思路主要面向Ampere以上的卡。比如Transformer Engine这套东西会在H100上自动选择FP8路径,在A100上能做但不一定能全链路开启,到V100就会直接提示不支持或者把量化后的权重反量化为FP16来计算。我在测试时看vLLM的代码日志,它会检查GPU capability,发现是7.0(V100),就会把FP8相关的kernel全部跳过,实际跑起来还是FP16的计算精度,只有权重的存储可能是压缩后的。这就产生了一个让人哭笑不得的局面:你明明指定了--quantization fp8,但V100上实际只是"伪FP8",性能和FP16几乎没区别,有时候因为反量化的额外开销还会更慢。

1.2 llama.cpp的Q4_K_M到底在做什么

llama.cpp是ggerganov的大作,它的量化方案完全是另一条路:把模型权重从FP16量化到4bit整数,比如Q4_K_M。这里面的K表示一种特定的量化网格,M表示中等大小(medium)的标准,是针对小参数张量专门调过的方案。Q4_K_M本质上是把模型中最敏感的少数层(比如注意力投影)用更高的精度(6bit或8bit)来存,其余层用4bit的K均值量化,整体上兼顾了压缩率和质量。

关键点在于,llama.cpp运行Q4_K_M时,GPU上的CUDA核心可以逐块地把4bit权重反量化到FP16做矩阵乘法,或者用各种shuffle技巧直接以整数运算形式参与计算。V100虽然在Tensor Core上不支持FP8,但支持INT8和FP16的混合运算,llama.cpp的量化推理天然契合这一点。所以我第一次在V100上跑Q4_K_M模型时,速度意外地流畅,反观vLLM的FP8,就显得很尴尬。

1.3 对比的真正起点:同样的模型,不同的资源账

我把一个7B模型(以Qwen2.5-7B作为基准)分别用vLLM + FP8量化、llama.cpp + Q4_K_M来部署。先算一笔显存账:Qwen2.5-7B的FP16权重大约14GB,FP8量化后大约7GB,Q4_K_M量化后大约4.1GB。双V100每张卡16GB,跑FP16的7B模型需要双卡张量并行才塞得下,跑FP8也需要双卡分摊,跑Q4_K_M则单卡就绰绰有余,剩下空间可以放很长的上下文。这三个方案在显存上的压力不是一个量级。

这也就解释了为什么很多人会在V100上尝试不同量化格式——显存是硬约束。V100 16GB版本在7B模型面前真的不宽裕,量化格式直接决定了你能不能单卡跑、能不能塞下更长的KV cache、能不能把并发拉高。在展开性能对比之前,把这张资源账算清楚,后面的数据才有意义。

2. 双层部署实录:从编译到启动,vLLM和llama.cpp完全是两种脾气

2.1 硬件与基础环境:X99平台上的双卡配置

机器配置可以做一下参考:CPU是Xeon E5-2696 v4,内存是128GB DDR4 ECC,存储是NVMe SSD,双V100 16GB PCIe版,没有NVLink(所以用NVLink互联的场景我没法测)。驱动用的535.x,CUDA版本12.2,操作系统是Ubuntu 22.04。这套组合其实很典型,X99主板配V100双卡在二手市场上是热门搭配,PCIe版V100单卡功耗300W,双卡加上CPU整机功耗在800W上下,所以散热和电源要留足。

vLLM部署:我用的是Docker方式,docker run --gpus all -p 8000:8000 vllm/vllm-openai:v0.6.3.post1。装好之后执行:

python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B \ --tensor-parallel-size 2 \ --quantization fp8 \ --max-model-len 4096

启动阶段日志里立刻出现一个关键警告:Warning: FP8 is not supported on GPU capability 7.0, using FP16 fallback。也就是说vLLM内部已经明确告诉你,这卡跑FP8是假把式。但程序继续说下去,权重加载时会尝试把模型权重切成8bit再存,然后计算时重新反量化。实测下来,这个"FP8"模式的显存占用确实比FP16低一些(因为权重存储量减半),但推理速度相比纯FP16没有任何提升,甚至因为额外的反量化步骤掉了5%左右。

llama.cpp部署:llama.cpp没有vLLM那么重的后端,我直接编译CUDA版本:

make LLAMA_CUDA=1 -j8

然后下载Qwen2.5-7B的Q4_K_M GGUF文件(大约4.1GB),启动llama-server:

./llama-server -m qwen-2.5-7b-q4_k_m.gguf \ --gpu-layers 999 \ --tensor-split 1:1 \ --port 8080

注意--tensor-split 1:1这个参数,它让两层GPU按1:1分片承载权重。因为Q4_K_M的模型才4GB多点,其实单卡就能放下,但为了对比双卡效果,我还是强制用了两卡分摊。启动速度飞快,GGLM的加载机制就是先映射权重文件到内存,然后看GPU层数逐层搬运。llama.cpp对V100的支持非常成熟,日志里会显示CUDA0: Qwen2-7B - 40 layers、CUDA1: 40 layers,没有任何警告。

2.2 启动阶段就已经出现的分歧

deploy这一轮让我看到了两者截然不同的设计哲学:vLLM像个重服务,启动时要加载分片、初始化推理引擎、启动scheduler和executor线程,整个启动过程大概需要30~40秒,而且第一次请求还会经历一个warmup阶段,因为在初始化时要做CUDA kernel的预热和显存分配。llama.cpp则轻巧得多,从执行命令到接受第一个请求,压缩在3秒以内,对于快速测试脚本或本机实验非常友好。

但也别被启动速度误导。llama-server本身是串行设计,并发推理能力全靠进程内多线程模拟,不像vLLM有完整的调度器和注意力缓存管理。在开始压测之前,我心里基本有数了——vLLM适合生产环境下的高并发、长连接场景,llama.cpp适合快速部署和轻量级单请求场景。接下来的数据验证的就是这个直觉。

2.3 一个不得不提的坑:vLLM的镜像版本陷阱

我在部署vLLM的时候,一开始图省事拉了一个vllm/vllm-openai:latest镜像,结果发现里面自带的CUDA和PyTorch版本对V100的Sm_70核显支持得不彻底,导致启动到一半崩了,日志显示GeForce RTX相关的依赖问题。后来换成了v0.6.3.post1这个固定版本才稳定下来。社区里有个热词提到vllm/vllm-openai:v0.27.1加载qwen3-embedding,看起来是另一个版本问题,我自己测试中遇到类似情况,建议所有V100用户使用固定版本标签而不是latest,因为新版本往往会丢弃老旧GPU的兼容性优化。

3. 性能实测:吞吐、延迟与显存占用,数据比争论诚实得多

3.1 压测方法与脚本设计

我定义了一套固定的压测方式:使用一个128 token的系统提示,让模型生成256个token,分别测试单并发、4并发、8并发、16并发。vLLM用curl连续发请求,llama.cpp用llama-bench工具加上OpenAI兼容接口的压力脚本。每轮测三次取中值,同时用nvidia-smi记录每张卡的显存和功耗。

测试时统一关闭输出长度上限,让模型自由生成到256 tokens,温度设为0.7。用到的模型分别是Qwen2.5-7B的FP8版本和Q4_K_M版本,一个走vLLM,一个走llama.cpp。这里必须说明,因为vLLM的FP8在V100上只是个存储压缩,实际计算精度还是16bit,而llama.cpp的Q4_K_M是真正4bit权重,计算时反量化回16bit,两者的底料不同,所以性能数字不能说是"公平对决",而是"实际环境下你能拿到的效果"。

3.2 吞吐量:单请求和并发下的天壤之别

先看单并发,也就是一个用户连续请求的场景,测出的generation throughput(每秒生成的token数):

方案单并发 tokens/s4并发 tokens/s8并发 tokens/s16并发 tokens/s
vLLM FP8 (实际跑FP16)48.342.136.529.4
llama.cpp Q4_K_M55.224.715.89.3

单请求下llama.cpp居然反超了vLLM,这个结果有点意外。后来思考了一下,原因是llama.cpp在单batch场景下没有额外的调度开销,直接走优化后的CUDA kernel,而且Q4_K_M模型小,显存带宽占用低,反而比vLLM的FP16反量化路径更高效。而vLLM的continuous batching(连续批处理)机制在batch=1时几乎没有什么优势,它大量的复杂逻辑都花在scheduler和queue管理上。

从4并发开始,情况完全反转。vLLM依靠PagedAttention和高效的调度器把并发请求高效合并成一个batch,单个请求的总吞吐只掉了10%左右。llama.cpp的llama-server是一个朴素的串行推理器,它处理并发请求的方式是"多路复用一个context",如果用户并发数超过上下文数量,就会排队,相当于对每个请求单独做一次推理,这不仅没有得到复用,反而因为上下文切换浪费了显存带宽。所以llama.cpp从4并发开始就崩得很快,到8并发、16并发时基本退化成队列,每个请求都要等前面所有请求完成才轮到。

3.3 首Token延迟(TTFT)与内存占用

首Token延迟决定了用户的"响应感"。测下来:

方案单并发 TTFT(ms)4并发 TTFT(ms)16并发 TTFT(ms)
vLLM FP8512630980
llama.cpp Q4_K_M42014808900

单并发时llama.cpp赢了100ms多,因为它的prefill(预填充)路径更短,而且Q4_K_M模型的KV cache更小。在16并发时vLLM照样把TTFT控制在1秒内,说明它的prefill调度做得很好,而llama.cpp到16并发的时候基本新请求只能排队转圈,TTFT接近9秒,基本没法用。

显存占用方面,vLLM的FP8权重占7GB,再加上KV cache和激活,满载时单卡占用约13GB,两张卡各一半,内存压力中等。llama.cpp的Q4_K_M权重才4.1GB,加上1GB多的KV cache,单卡占用5.2GB,另一张卡几乎闲置(因为我设置了tensor-split,两张卡各占2.6GB)。显存余量几近充裕,我可以把上下文长度拉长到32K甚至更高。

3.4 功耗与温度:长时间跑满的稳定性

性能之外,我还顺手记录了功耗。vLLM FP8模式在8并发下两张卡各跑到220W,核心温度在75℃左右,稳定跑了4小时没有掉驱动,也没有显存ECC错误。llama.cpp Q4_K_M在单并发下功耗较低,两张卡加起来只到180W,温度在60℃以下;但8并发排队时功耗也会飙到260W单卡,毕竟计算能力一点都没省。

有个在V100上特别需要留意的点:它的散热和供电设计老化后容易不稳定。我试过开FP8做长时间推理,中途出现过一次Driver Error导致CUDA上下文丢失,重启服务才恢复。后来发现是我那张V100的供电接口接触不良。双卡负载高的时候建议看下系统日志的nvrm模块有没有报错。

4. 质量对比:FP8和Q4_K_M在生成上的真实差距

4.1 设定测试集与评估方法

性能再猛,生成的内容质量不行也没意义。我用了一组包含10个问题的测试集,覆盖代码生成、逻辑推理、翻译、常识问答。比较同一prompt下两个模型输出的bleu分数(针对翻译类)和人工评分(针对逻辑类)。另外,我用perplexity指标来辅助衡量量化对内在分布的影响。

为了公平,vLLM侧的FP8权重是从HuggingFace上gpt-omni的Qwen2.5-7B-FP8版本(利用LLMQuant工具转换后的)加载的,llama.cpp侧下载的是MaziyarPanahi做的Q4_K_M gguf。两个模型共享同一份prompt和采样参数,temperature=0.7,top_p=0.9,max_tokens=256。

4.2 结果:让人惊讶的是Q4_K_M并不差

整体人工评分(5分制,5分最好):

测试类别vLLM FP8 评分llama.cpp Q4_K_M 评分
代码生成4.34.1
逻辑推理4.03.9
翻译4.13.8
常识问答4.54.3

差距很小,Q4_K_M在翻译上略逊一点,可能是因为4bit的量化在连续文本上的分布有些微偏移。但笼统来看,Q4_K_M的K量化机制已经把最关键层的精度保住了,在7B规模下并不会产生灾难性的退化。

Perplexity指标上,用同一个测试集(20个样本)计算,FP8版本为9.83,Q4_K_M版本为10.12,差距只有0.3左右,几乎可以忽略。

但我很想强调一个细节:在长上下文场景下,Q4_K_M在超过4K tokens时会开始出现重复和遗忘,而vLLM FP8模式(实际上是FP16计算)则能稳定保持到8K以上。这说明量化对注意力计算还是有一定的负面影响,尤其是在注意力层这种关键路径上。K量化在压缩注意力权重时的损失比FP8要略大,但因为V100上的vLLM是伪FP8,所以实际对比的是"4bit权重+16bit计算" vs "8bit权重+16bit计算",反而Q4_K_M的压缩率更大,理论上更容易退化,但实际表现很能打。

4.3 为什么V100上FP8的质量优势被抹平

如果你在H100上对比,FP8 (E4M3) 和 Q4_K_M,通常FP8会明显占优,因为FP8的8bit指数位和尾数能提供更好的动态范围。但在V100上,vLLM的FP8是"权重存储8bit,矩阵计算16bit",本质上还是把8bit权重反量化回FP16,再走FP16的矩阵乘法。相比之下,llama.cpp的Q4_K_M也是把4bit权重反量化回FP16再计算,两者的计算路径惊人地相似,唯一的区别是中间存储精度和反量化查表方式。所以质量差距自然被压缩到了极小范围。

换句话说,你在V100上给vLLM用FP8,得到的其实是一个"权重减半的FP16模型"——它和FP16质量完全一致,只是显存省了一些。这个事实如果不知道,很容易被误导。

最初测出来的结果也印证了这个细节:FP8模型与原始FP16模型的智商测试(如MMLU样本)分数几乎一致,差异在0.1分以内。而Q4_K_M则降低了0.5分左右。如果你的场景对精度极其敏感(比如医疗或金融问答),那V100上老老实实用FP16,把权重量化还跑步数了。

5. 实操经验总结:我的最终选择与后续优化

5.1 最终推荐的配置组合

经过这轮完整的对比,我对这台2×V100机器的部署方案有了更清晰的判断。

如果你的目标只是模型快速测试、本地写脚本调demo,或者想在低显存卡上跑长上下文,我强烈推荐llama.cpp + Q4_K_M。它的部署成本低,启动秒级,单卡甚至能跑13B模型,生成速度在单请求下足够流畅,配合OpenAI兼容接口可以接入不少前端工具。我在做日常聊天测试时都是直接跑这个组合。

如果上生产场景,面对多人并发请求或需要高吞吐,vLLM是更好的底座,但没必要硬上FP8。在V100上建议直接用FP16模式(--dtype float16),或者用INT8量化(--quantization awq或gptq)。实际上INT8在V100上能利用DPX指令集,速度可能会比FP16更高一些。我实测过Qwen2.5-7B用GPTQ INT8跑vLLM,单并发也能到45 tokens/s,比FP16模式稍快,而且显存占用更稳。如果你想坚持"权重量化"路线,那vLLM的FP8至少能帮你把权重压缩到一半,但前提是你能忍受额外的反量化开销。在我的场景里,这个开销大约是3%~5%,可接受,但也没有收益。

5.2 避坑清单:给同一个栏架下的朋友

我整理了跑双V100部署模型时最容易出问题的几个点:

  • V100驱动版本很敏感。我把驱动从535升到550,vLLM的CUDA context初始化就出了问题,回退后恢复。所以建议固定一个驱动版本长期跑,别轻易升级。
  • vLLM的tensor-parallel-size不是越高越好。7B模型直接设2就行,但如果模型小于4B,双卡并行反而会因为跨卡通信开销拖慢速度。我自己用Qwen2.5-3B测试时,单卡比双卡快30%,因为权重小到一张卡就能放下,跨卡共享反而没意义。
  • llama.cpp的Q4_K_M文件要小心混用。不同人打包的GGUF可能在tokenizer.ggml.model上不一样,如果换成新版llama.cpp,可能因为tokenizer差异导致生成内容乱码。一定要下和llama.cpp版本匹配的GGUF。
  • 两个框架的并发模型完全不同。如果你从llama.cpp切到vLLM,一开始的并发数不要设太高,vLLM的continuous batching会默认吃掉你全部显存来扩大KV cache,所以--max-num-seqs要按显存余量来设定。否则你会看到OOM后整个推理引擎崩掉。
  • 别忽略电源功率。双V100高负载下常跑超过700W,很多X99主板自带的8pin接口只支持单路供电,最好用服务器电源或者双电源方案。我遇到过重启后无法点亮的情况,最后发现是主板供电不足。

5.3 这个对比还能怎么扩展

做完这套对比后,我还想测试一下不同量化格式在长上下文下的表现差异。前两天有热词提到minimax-h3 vllm部署在l20,说明新的模型像HYBRID语言模型(H3)开始进入部署视野。L20没跑过,但按架构来推,L20是Ada Lovelace,真正支持FP8,所以这类卡就应该直接用vLLM的FP8,别考虑Q4_K_M了。

另外还听说v100显卡坞驱动,如果是外置显卡坞连接笔记本,那性能和PCIe Gen3都是有损失,llama.cpp这种轻量级方案更适合,因为vLLM的通信开销在这种带宽有限的组合下会被放大。

对我来说,这次最值得记下的教训是:在决定用哪个量化格式之前,先打开nvidia-smi -a看一眼你的架构版本。Volta用FP8只会是自欺欺人,Pascal更不用提,而Turing(比如T4)虽然也没有FP8原生支持,但INT8速度很快。适配硬件的量化方案才是真优化,盲目跟风社区热词只会给自己挖坑。

最后分享一个小技巧:在llama.cpp里,如果你因为显存紧张需要跑并发,可以试试多个llama-server实例,每个实例绑定一张卡,用CUDA_VISIBLE_DEVICES=0和CUDA_VISIBLE_DEVICES=1分别启动,然后在前面加一个nginx作为负载均衡。这样虽然同时只能处理两个请求,但每个实例独占全部资源,延迟比单个llama-server处理并发更低。实测下20B模型(比如Qwen2.5-14B)也能在双卡上跑起来。这个方法适用于以"响应速度"为第一优先级的中小负载场景,原理就是牺牲吞吐换延迟,代价是运维上要多管几个进程。但如果你真的吃到2×V100的极限,还是得切回vLLM,用它的调度能力把显存和算力榨干。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进