2×V100实测:vLLM FP8与llama.cpp Q4_K_M量化推理选型对比 1. 为什么要在 2×V100 上做这场对比手里有两张 Tesla V100 32GB这是很多小团队和个人开发者能接触到的最具性价比的数据中心级算力卡之一。但 V100 是 Volta 架构SM70不支持 BF16也没有原生 FP8 张量核心——这两点直接决定了它在大模型推理这条路上的所有取舍。我最近在折腾本地推理服务核心问题就一个同一台 2×V100 机器上到底该用 vLLM 跑 FP8还是用 llama.cpp 跑 Q4_K_M这个问题看起来简单实际上牵扯到量化格式、硬件代际、推理引擎调度策略、显存带宽瓶颈、并发模型等一大堆东西。网上关于 vLLM 和 llama.cpp 的对比文章不少但绝大多数要么是拿 A100/H100 跑的数据要么是单卡消费级显卡的体验真正针对 V100 这种老数据中心卡的实测分析非常稀缺。而 V100 的处境很特殊它有 900GB/s 的 HBM2 显存带宽比很多新卡都宽但算力代际落后FP8 支持缺失这就导致量化方案的选择会极大影响最终体验。我写这篇东西的目的很直接把我在 2×V100 上折腾 vLLM FP8 和 llama.cpp Q4_K_M 的完整过程、踩过的坑、实测数据、以及最终的选择逻辑摊开来讲。如果你手里也有 V100或者正在纠结量化格式和推理引擎的搭配这篇应该能帮你省下不少试错时间。涉及的关键词包括 vLLM、llama.cpp、FP8、Q4_K_M、V100、量化推理、张量并行等我会尽量把每个技术决策背后的为什么讲清楚而不是只丢一堆命令和数字。先说结论方向这两个方案不是简单的谁快谁慢而是服务于完全不同的使用场景。vLLM FP8 在 V100 上其实是个伪命题——因为 V100 根本不支持 FP8 计算你跑 FP8 权重最终还是要反量化到 FP16 来算收益有限而 llama.cpp 的 Q4_K_M 是 CPU/GPU 混合推理的经典方案在 V100 上能跑但并发能力弱。具体怎么选往下看。2. V100 的硬件底子决定了量化方案的天花板2.1 Volta 架构的三个硬约束要把这场对比讲清楚必须先认清 V100 的硬件限制。Tesla V100 基于 GV100 核心SM70 架构这三个约束是绕不开的第一不支持 BF16。BF16 是 AmpereSM80之后才引入的格式V100 只有 FP16 和 FP32。这意味着所有现代推理框架默认的 BF16 路径在 V100 上要么回退到 FP16要么直接报错。很多新版本的 vLLM 镜像默认按 BF16 加载模型在 V100 上会直接崩这是第一个大坑。第二没有原生 FP8 张量核心。FP8E4M3/E5M2是 HopperSM90才有的硬件能力。V100 上跑 FP8 权重计算时必然要反量化dequantize到 FP16这个转换过程本身有开销而且反量化后的计算仍然是 FP16 精度。所以FP8 加速在 V100 上基本不成立你得到的主要是显存占用降低而不是计算加速。第三张量核心只支持 FP16。V100 的 Tensor Core 是第一代只吃 FP16 输入、FP32 累加。这意味着任何量化方案最终都要落到 FP16 上做矩阵乘量化带来的收益主要体现在显存带宽和容量上而不是算力。把这三点串起来结论就清晰了在 V100 上量化的核心价值是省显存和省带宽而不是提算力。这直接决定了 FP8 和 Q4_K_M 的对比逻辑。2.2 显存带宽才是 V100 的真正优势很多人只盯着 V100 的算力约 15.7 TFLOPS FP16 单卡却忽略了它的显存带宽——900GB/s HBM2。这个数字放到今天依然能打比 RTX 4090 的 1008GB/s 略低但远超大多数消费级卡。对于大模型推理这种内存带宽敏感型任务带宽往往比算力更关键。为什么因为自回归生成decode 阶段是逐 token 输出的每一步都要把整个模型的权重从显存读一遍。以 7B 模型为例FP16 权重约 14GB每生成一个 token 就要读 14GB900GB/s 带宽下理论极限约 64 token/s。如果量化到 4bit约 3.5GB理论极限能到 250 token/s 以上。这就是量化的真正意义——减少每步要读的数据量。所以对 V100 来说量化格式的选择直接决定了理论吞吐上限。FP8 权重约 7GBQ4_K_M 约 3.5-4GB后者的带宽优势是实打实的。2.3 2×V100 的互联与张量并行两张 V100 之间如果没有 NVLink 桥接走的是 PCIe 3.0 x16带宽约 16GB/s。这个带宽做张量并行Tensor Parallelism时每层都要做 all-reduce 通信会成为明显瓶颈。如果有 NVLinkV100 的 NVLink 2.0 是 300GB/s情况会好很多。我手上这两张是带 NVLink 桥的版本所以 vLLM 的 TP2 能跑得比较舒服。但即便如此张量并行的通信开销依然存在尤其是小 batch 场景下通信占比会很高。这一点在后面的实测数据里会体现出来。提示如果你手里的 V100 没有 NVLink强烈建议优先考虑单卡能装下的量化方案而不是硬上 TP2。PCIe 互联下的 TP 性能衰减可能超过 30%。3. vLLM 跑 FP8 在 V100 上的真实链路3.1 vLLM 的调度与执行架构简述要理解 vLLM 在 V100 上的表现得先知道它的内部是怎么跑的。vLLM 的核心是EngineCore Scheduler Executor三层结构EngineCore引擎主循环负责接收请求、驱动整个推理流程。Scheduler调度器决定每一步哪些请求进入 batch核心是 PagedAttention 的块管理。它维护一个 block table把 KV Cache 切成固定大小的块来管理避免显存碎片。Executor执行器负责实际在 GPU 上跑模型。单卡是单进程多卡时用 Ray 或 MPI 做分布式执行。在 2×V100 场景下Executor 会走 TP2 的分布式路径每层的线性层被切分到两张卡上前向传播时需要 all-reduce 同步。Scheduler 则根据显存里 KV Cache 的剩余块数决定能塞多少请求进 batch。这套架构的优势是高并发下的吞吐PagedAttention 让显存利用率很高continuous batching 让 GPU 不空转。但它的前提是显存要够因为 KV Cache 本身很吃显存。3.2 FP8 在 V100 上到底发生了什么这是最关键的一节。当你在 vLLM 里指定--quantization fp8加载一个 FP8 模型时实际发生的是权重加载模型权重以 FP8 格式存在显存里约 7GB for 7B 模型。前向计算每一层的矩阵乘之前FP8 权重被反量化到 FP16然后和 FP16 的激活值做 Tensor Core 矩阵乘。结果累加FP32 累加输出 FP16。问题就出在第 2 步。V100 没有 FP8 张量核心反量化是纯软件操作需要额外的 kernel 来做格式转换。这个转换本身要读一遍 FP8 权重、写一遍 FP16 权重或直接在寄存器里转带宽和算力都有额外开销。实测下来在 V100 上跑 FP8 相比直接跑 FP16吞吐提升非常有限有时甚至因为反量化开销而变慢。唯一实打实的收益是显存占用从 14GB 降到 7GB能多塞一些 KV Cache从而支持更大的 batch 或更长的上下文。3.3 实测配置与启动命令我的测试环境项目配置GPU2× Tesla V100 32GBNVLink 桥接CPUXeon Gold 6248R内存256GB DDR4系统Ubuntu 22.04驱动数据中心驱动 535 系列CUDA12.1vLLM0.6.xDocker 镜像 vllm/vllm-openai模型Qwen2.5-7B-Instruct启动命令FP8 路径docker run --gpus all --shm-size 16g \ -v /models:/models \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model /models/Qwen2.5-7B-Instruct-FP8 \ --quantization fp8 \ --tensor-parallel-size 2 \ --dtype float16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.90注意几个关键参数--dtype float16必须显式指定否则 vLLM 可能尝试 BF16 导致 V100 报错。--tensor-parallel-size 2两张卡做张量并行。--gpu-memory-utilization 0.90留 10% 给反量化临时缓冲和 CUDA 上下文。--max-model-len 8192控制 KV Cache 上限太长会挤爆显存。注意V100 上跑 vLLM 一定要确认镜像里的 PyTorch 编译时包含了 SM70 的 kernel。有些新版本镜像为了减小体积砍掉了老架构支持加载时会报 no kernel image is available。遇到这种情况要么换镜像版本要么自己编译。3.4 FP8 路径的实测数据用 vLLM 自带的 benchmark 脚本固定输入 512 token、输出 256 token测不同并发下的吞吐并发数输出吞吐 (token/s)首 token 延迟 (ms)显存占用/卡1428518GB821012024GB1634018028GB3241032030GB可以看到并发越高吞吐越好这是 vLLM continuous batching 的典型特征。单请求只有 42 token/s但 32 并发能到 410 token/sGPU 利用率被拉满。首 token 延迟随并发上升因为要排队等调度。但这里有个隐患显存占用随并发快速上升32 并发时已经到 30GB/卡接近 32GB 上限。再往上加并发就会 OOM。这就是 FP8 在 V100 上的尴尬——省下的显存被 KV Cache 吃掉了最终能支持的并发数并没有比 FP16 多太多。4. llama.cpp 的 Q4_K_M 在 V100 上怎么跑4.1 Q4_K_M 是什么为什么它适合 V100Q4_K_M 是 llama.cpp 的 K-quant 系列量化格式之一属于 4bit 量化的中等档位。它的核心设计是混合精度量化对模型里不同的张量用不同的量化策略重要的层如 attention 的 Q/K/V用更高精度不重要的层用更低精度整体平均下来约 4.5bit。为什么它适合 V100三个原因显存占用极低7B 模型量化后约 4GB两张 V100 加起来能轻松装下还能留大量显存给 KV Cache。带宽需求小每步只需读 4GB 权重900GB/s 带宽下理论极限超过 200 token/s。llama.cpp 对老架构支持好它的 CUDA backend 一直保留 SM70 支持不像某些新框架会砍掉老卡。但 Q4_K_M 也有代价量化损失比 FP8 大。4bit 量化在复杂推理任务上会有明显的质量下降尤其是数学、代码这类对精度敏感的任务。这是它和 FP8 最本质的取舍。4.2 llama.cpp 的 GPU offload 策略llama.cpp 默认是 CPU 推理要用 GPU 需要显式 offload 层。关键参数是-nglnumber of GPU layers控制把多少层放到 GPU 上。在 2×V100 上llama.cpp 的多卡支持是通过split mode实现的有两种--split-mode layer按层切分每张卡负责一部分层。这是默认模式通信开销小但负载可能不均。--split-mode row按行切分每层都切到两张卡上类似张量并行。通信开销大但负载均衡。对 V100 这种有 NVLink 的卡row模式能利用 NVLink 带宽但实测下来layer模式在大多数场景下更稳因为通信少。启动命令./llama-server \ -m /models/qwen2.5-7b-instruct-q4_k_m.gguf \ -ngl 99 \ --split-mode layer \ --tensor-split 1,1 \ -c 8192 \ --host 0.0.0.0 \ --port 8080 \ -t 8参数说明-ngl 99把所有层都 offload 到 GPU99 是全部的惯用写法。--split-mode layer按层切分到两张卡。--tensor-split 1,1两张卡平均分配。-c 8192上下文长度。-t 8CPU 线程数用于处理非 offload 的部分。4.3 Q4_K_M 路径的实测数据同样的测试条件输入 512、输出 256并发数输出吞吐 (token/s)首 token 延迟 (ms)显存占用/卡178456GB495908GB810221011GB169848015GB数据很说明问题单请求吞吐比 vLLM FP8 高78 vs 42但并发能力差很多。8 并发之后吞吐基本不涨了首 token 延迟飙升。这是因为 llama.cpp 的 batching 能力弱它本质上是为单用户或低并发场景设计的。显存占用是巨大优势16 并发才 15GB/卡还有一半显存空着。但空着也没用因为 llama.cpp 调度不了那么多并发。5. 两条路径的横向拆解与选型逻辑5.1 关键维度对比把上面的数据整理成一张对比表看得更清楚维度vLLM FP8llama.cpp Q4_K_M权重显存~7GB~4GB单请求吞吐42 token/s78 token/s高并发吞吐410 token/s (32并发)102 token/s (8并发)首 token 延迟85ms45ms量化质量高接近 FP16中4bit 有损部署复杂度高Docker 依赖低单二进制多卡支持成熟TP一般split适合场景多用户 API 服务单用户/边缘部署5.2 为什么单请求 llama.cpp 更快这个反直觉的结果值得解释。vLLM 单请求慢是因为它的架构为高并发优化PagedAttention 的块管理、continuous batching 的调度、TP2 的通信开销这些在单请求时都是纯负担。而 llama.cpp 单请求路径极短没有调度开销没有跨卡通信layer 模式下每张卡独立算自己的层所以延迟低。这就是架构决定场景的典型例子。vLLM 是吞吐机器llama.cpp 是延迟利器。5.3 量化质量的真实差异我拿几个任务做了对比测试用同一组 prompt 分别跑两个方案简单问答两者输出几乎一致Q4_K_M 偶尔有措辞差异。代码生成Q4_K_M 在复杂算法题上错误率明显上升FP8 基本和 FP16 一致。数学推理Q4_K_M 在多步计算上容易出错FP8 表现稳定。长文本摘要两者差异不大Q4_K_M 偶尔漏细节。结论如果任务对精度敏感FP8 是更安全的选择如果只是日常对话、信息检索Q4_K_M 完全够用。5.4 选型决策树基于以上分析我总结了一个简单的决策逻辑你是多用户 API 服务追求吞吐→ vLLM FP8或 FP16。你是单用户本地使用追求低延迟→ llama.cpp Q4_K_M。你的任务对精度敏感代码/数学→ vLLM FP8。你的显存紧张想跑更大模型→ llama.cpp Q4_K_M。你需要快速部署、不想折腾依赖→ llama.cpp。6. 实操中踩过的坑与经验6.1 V100 驱动与 TCC 模式的坑V100 是数据中心卡默认工作在 TCC 模式Tesla Compute Cluster这个模式下不能直接当显示输出用。如果你在 Windows 上想用 V100 做推理需要把 TCC 改成 WDDM 模式但改了之后计算性能会受影响。建议在 Linux 下用 TCC 模式跑推理性能最稳。驱动版本也有讲究。太新的驱动可能对 V100 支持不完善太旧的又不支持新 CUDA。我实测 535 系列比较稳配合 CUDA 12.1。6.2 vLLM 镜像里到底带不带模型这是个高频问题。vLLM 的官方 Docker 镜像不带模型只带推理框架。模型需要你自己挂载进去。很多人第一次跑 vLLM 报 model not found 就是这个原因。正确做法是用-v把本地模型目录挂载到容器里然后--model指向容器内路径。另外镜像版本选择很关键。新版本 vLLM 有时会为了性能优化砍掉老架构支持V100 上可能跑不起来。如果遇到 kernel 缺失退回到 0.5.x 或 0.6.x 的稳定版本。6.3 FP8 模型的来源问题vLLM 支持直接加载 FP8 量化模型但你需要先有一个 FP8 模型。获取方式有两种用官方或社区已经量化好的 FP8 模型如 Qwen 系列的 FP8 版本。自己用llm-compressor或类似工具把 FP16 模型量化成 FP8。自己量化时要注意校准数据集的选择直接影响量化质量。用通用语料校准的 FP8 模型在特定领域任务上可能表现不佳最好用领域相关数据校准。6.4 llama.cpp 的 split mode 选择前面提到layer和row两种 split mode。我实测下来在 V100 NVLink 环境下7B 模型用layer模式更快因为通信少。但如果是 70B 这种大模型单卡装不下所有层row模式可能更均衡。具体要试。还有一个坑--tensor-split的比例要和实际显存匹配。如果两张卡显存不一样比如一张 32GB 一张 16GB比例要按显存大小分配否则小的那张会 OOM。6.5 并发测试的正确姿势测吞吐时不要用单次请求的结果下结论。vLLM 的 continuous batching 需要持续压测才能体现优势。我用的是vllm bench serve脚本固定请求速率打一段时间看稳定态的吞吐。llama.cpp 则用llama-bench或自己写脚本并发打。另外首 token 延迟和输出吞吐要分开看。有些场景首 token 延迟更重要如交互式对话有些场景吞吐更重要如批量处理。别用一个指标概括所有。7. 我最终的选择与一些延伸想法折腾了这么久我现在的实际配置是日常交互用 llama.cpp Q4_K_M批量任务和 API 服务用 vLLM FP8。两个都留着按场景切换。这不是和稀泥而是因为这两个工具确实服务于不同需求硬要二选一是给自己找麻烦。如果只能选一个我会看主要用途如果是给自己用的本地助手llama.cpp 的简单、低延迟、低显存占用更香如果是要给团队提供 APIvLLM 的高并发吞吐无可替代。最后分享一个我在 V100 上跑推理的小技巧把模型的 embedding 层和 lm_head 层保持在 FP16只量化中间的 transformer 层。这两层对精度影响大但参数量占比小保持高精度能明显改善输出质量显存增加却很有限。这个策略在 FP8 和 Q4_K_M 上都适用实测下来是性价比很高的优化。V100 虽然是老卡但 900GB/s 的带宽和 32GB 的显存让它在大模型推理上依然有一席之地。关键是想清楚自己的场景选对量化格式和推理引擎的搭配而不是盲目追新。