1. 项目背景与调优目标1.1 硬件与模型的基本情况手里这块 V100 已经吃灰了半年前几天接到一个任务要在单卡上把 Qwen2.5-27B 跑起来目标是最少能接受的速度。说实话我一开始没太当回事直接拉了模型按惯性思维启动服务结果预填阶段还凑合生成阶段只有 4 tok/s慢到完全没法用。于是才有了这篇调优实录——从 4 tok/s 一路压到 64 tok/s整个过程没有玄学全是显存、带宽和算子的算计。先说硬件。我手上这块是 V100-PCIe-32GBHBM2 显存带宽 900GB/s16nm 工艺发布于 2017 年。放到今天它的浮点算力不算亮眼但有一个特性非常适合大模型推理显存大、带宽高、二手价格便宜。另外 V100 支持 FP16 的 Tensor Core但不支持 BF16这决定了我们在模型格式上不能直接照搬最新的 bf16 权重必须做一些转换和量化。再说模型。Qwen2.5-27B-Instruct 是阿里 Qwen 系列里的一个甜点尺寸27B 参数官方 bf16 权重大概 54GB。它用的是 GQA 架构KV head 数量只有 4 个这意味着 KV cache 比传统 MHA 结构小很多是能在 16GB 到 32GB 显存上部署的重要前提。如果你也纠结为什么要选 27B 而不是 7B 或者 72B我的答案是7B 对小规模私有部署来说能力不太够72B 量化后勉强塞进两张 V100 但成本翻倍27B 刚好卡在一张卡能装、能力接近 70B 档位的边缘是性价比最好的一档。1.2 初始状态4 tok/s 是怎么来的第一次启动我用的是 llama-server 的默认参数心想模型放进去应该就能用结果被现实教育了。默认情况下 llama.cpp 并不会主动把所有层加载到 GPU-ngl即--n-gpu-layers不设置的话模型就跑在 CPU 上。27B 量化到 Q4_K_M 之后权重仍然有 16.5GBCPU 从内存里反复读这 16.5GB 数据内存带宽就成了天花板。我这台机器的内存通道是双通道 DDR4 3200理论带宽也就 50GB/s 左右折算下来 16.5GB 除以 50GB/s极限就是 3~4 token/s。实测稳定在 3.8 左右和理论值几乎吻合。这其实是一个很典型的没调优状态GPU 利用率接近 0CPU 核心全部打满显存占用只有几百 MB。怎么快速判断自己是不是踩了同一个坑直接看两个东西nvidia-smi里 GPU 利用率如果长时间在 0%~20% 徘徊而htop里 CPU 是满载的那权重一定没有全部进显存。还有个隐藏坑是只 offload 了一部分层比如-ngl 20这种配置下 GPU 和 CPU 之间要频繁通过 PCIe 搬运中间结果PCIe 3.0 x16 的实际带宽也就 12GB/s 左右同样能把速度压到个位数。所以调优的第一步永远不是调什么 batch、什么量化等级而是先把模型完整放进显存里。1.3 调优的核心思路大模型自回归生成本质上是一个带宽饥饿型任务。每生成一个 token都需要把整个模型的权重从显存读一遍参与计算。这意味着最终决定 tok/s 的第一要素不是算力而是显存带宽除以权重体积这个比值。我用一个食堂窗口来类比厨师GPU 算力炒菜速度再快窗口一次只能出一个餐生成一个 token真正卡住速度的是把菜品端出来的通道显存带宽。所以整轮调优的方向就很清楚了减小权重体积让每个 token 需要搬移的数据变少确保权重完全驻留在 GPU 显存消除 CPU 和 PCIe 造成的额外瓶颈减少生成过程中除了权重之外还要读取的数据量比如 KV cache用并行手段摊薄固定开销比如投机解码和连续批处理。这四条路我后面会逐一展开。先记住一个结论别一上来就去调那些看起来很高级的算子参数先把模型的体重降下来、把它放到该放的地方速度上你已经赢了一半。2. 显存账本与量化选型这块 V100 到底能装下什么2.1 先算一笔显存账Qwen2.5-27B 的参数总量约 27B。FP16 每个参数占 2 字节权重文件就是 54GB32GB 的 V100 连一半都装不下所以第一步必须做量化。我在选量化档位前先列了一张显存预算表把各档 GGUF 文件的体积和可行性摸清楚量化格式权重体积(约)32GB V10016GB V100备注Q2_K11.5GB轻松可以质量损失大一般不推荐Q3_K_M13.5GB轻松较宽裕速度最快质量中等Q4_014.6GB轻松偏紧老式 4bit质量略粗糙Q4_K_S15.4GB轻松很紧4bit 小体积版Q4_K_M16.5GB可以装不下4bit 综合质量最好Q5_K_M19.8GB可以装不下质量接近原版Q8_028.5GB勉强装不下近无损但留给 KV cache 的空间很少除了权重本身还要算上 CUDA context 占用、KV cache 和激活值。CUDA context 大概吃掉 300~500MB这部分省不掉。KV cache 的占用可以精确算Qwen2.5-27B 是 64 层、4 个 KV head、head_dim 128所以每个 token 的 KV 参数是 2 × 4 × 128 × 64 65536 个FP16 存储就是 128KB/token。如果上下文开到 8192KV cache 要吃掉约 1GB量化到 8bit 后降到 0.5GB 左右。我最终选择了 Q4_K_M 作为主线方案因为它在 32GB 显存上能装下且 4bit K-quants 的质量在多数业务场景下和原版差别不大。如果你只有 16GB 版本后面 5.4 节会给一套降级配置。2.2 量化方案横向对比量化不是只有 GGUF 一种选择业界常见的还有 GPTQ、AWQ以及 transformers 里自带的 bitsandbytes。我把它们放在一起对比过方案典型显存占用推理框架V100 适配度上手难度FP16 / BF1654GBtransformers / vLLMV100 不支持 BF16低GPTQ 4bit约 17GBvLLM / ExLlama一般需反量化中AWQ 4bit约 17GBvLLM一般老卡效率偏低中GGUF Q4_K_M16.5GBllama.cpp / Ollama好低GGUF 的优势在于它本质上是一个打包好的模型容器权重、tokenizer、超参都在一个文件里部署时只要一个二进制文件就能加载不需要额外的 Python 环境和模型仓库依赖。对于生产环境这种单文件特性非常友好回滚也方便。GPTQ 和 AWQ 的优势是配合 vLLM 的 PagedAttention 和连续批处理在高并发场景下吞吐更强但它们在 V100 上有一个绕不开的问题V100 的 Tensor Core 只支持 FP16 累加原生 INT4 算力很弱实际推理时几乎都要先反量化到 FP16 再计算这跟 GGUF 的 dequant 流程本质相同但 llama.cpp 针对老卡的 Kernel 打磨得更久实测效率反而更高。2.3 为什么我选了 GGUF 而不是 AWQ/GPTQ这里多说一句 V100 的特殊性。V100 是 Volta 架构它的 Tensor Core 为 FP16 设计不支持 BF16也没有像 Ampere 那样完善的 INT8/INT4 支持。现在很多新模型的量化方案都是默认在 Ampere 或更新的架构上优化的放到 V100 上时不时会遇到算子不兼容或者效率偏低的问题。GGUF 在这条路上的积累要深厚得多。llama.cpp 社区常年服务于各种老显卡用户量化内核针对 sm_70V100 的架构代号做过很多轮优化解量化后走 FP16 Tensor Core 的路径非常成熟。实测下来同一个 Q4_K_M 模型在 llama.cpp 里加载到 V100 后速度表现相当稳定没有出现某些框架里能加载但跑起来莫名其妙慢的情况。另外还有一个系统层面的原因llama.cpp 对显存的管理是有多少放多少-ngl参数可以精确控制加载层数显存不够时还能优雅地降级而 vLLM 这一类框架倾向于一次性把模型和 KV cache 全部规划好显存不够就直接报错灵活性差一些。2.4 模型获取与量化实操如果你手里已经有 Qwen2.5-27B-Instruct 的官方权重可以用 llama.cpp 自带的转换脚本先生成 F16 的 GGUF再做量化python convert_hf_to_gguf.py /models/Qwen2.5-27B-Instruct \ --outfile qwen2.5-27b-f16.gguf --outtype f16 ./llama-quantize qwen2.5-27b-f16.gguf \ qwen2.5-27b-q4_k_m.gguf Q4_K_M转换和量化整个过程大概十几分钟取决于 CPU 速度。如果你不想自己转也可以直接下载社区已经量化好的 GGUF 文件Hugging Face 和国内一些模型托管平台上都有 Qwen 官方的 GGUF 版本Qwen/Qwen2.5-27B-Instruct-GGUF这个仓库里就有现成的 Q4_K_M 文件省时省力。有一个细节值得提下载 GGUF 时注意看文件命名Qwen 官方仓库里同一档位可能同时存在q4_k_m和q4_k_s这类变体。K-quants 是现在的主流质量控制比老的q4_0好不少建议优先选带_K_的版本。3. 核心调优实操从 4 到 64 的每一步3.1 第一步把模型完整加载进 GPU这是整个调优过程里收益最大的一步操作却最简单。前提是你要先确认编译出来的 llama.cpp 带 CUDA 支持。执行llama-server --version时输出里要有cuBLAS或CUDA字样如果只有 CPU 版本需要重新编译git clone https://github.com/ggerganov/llama.cpp cd llama.cpp cmake -B build -DGGML_CUDAON \ -DCMAKE_CUDA_ARCHITECTURES70 cmake --build build --config Release -j注意CMAKE_CUDA_ARCHITECTURES70这一步不能省70 对应的就是 V100 的 sm_70 架构。如果你不指定有些 CUDA 版本会默认编出包含多种架构的胖二进制编译耗时更长而且可能出现编译成功但运行时找不到 kernel的诡异问题。启动命令我一开始用得非常简单./build/bin/llama-server \ -m /models/qwen2.5-27b-q4_k_m.gguf \ -ngl 99 \ -c 8192-ngl 99的意思是尽可能把所有层都放到 GPU99 只是一个比层数更大的保险数字。启动日志里如果能看到offloaded 64/64 layers说明权重全部进显存了。这一步做完生成速度直接从 3.8 tok/s 跳到 41 tok/s接近 11 倍的提升。这个坑我印象太深了很多人跑大模型明明用的是 NVIDIA 显卡结果在默认参数下走了 CPU 推理还以为是模型太大显卡带不动。实际上只要-ngl没拉满一切参数调优都是空中楼阁。3.2 第二步KV Cache 量化把显存和带宽同时省下来权重进显存之后下一步要盘剥的就是 KV cache。默认情况下 KV cache 用 FP16 存前面算过8192 上下文大概要 1GB 显存。如果把上下文开到 32K就是 4GB这个数量级已经不容忽视了。llama.cpp 支持把 KV cache 单独量化参数是--cache-type-k和--cache-type-v。我实践下来最稳的组合是 k 和 v 都设成q8_0./build/bin/llama-server \ -m /models/qwen2.5-27b-q4_k_m.gguf \ -ngl 99 -c 8192 \ --cache-type-k q8_0 --cache-type-v q8_0开完 KV 量化后KV cache 占用直接减半更重要的是后续如果要挂投机解码模型或者跑长上下文省下来的显存就是可用空间。这一步对速度的影响在短上下文场景下提升并不明显我实测只从 41 提到 44 左右但如果你经常处理长文档、跑 RAG上下文动辄几千 tokenKV cache 的读取同样占用显存带宽量化后的收益会被明显放大。提示KV cache 也可以量化到 q4_0 来进一步省显存但对长文本的推理质量伤害比权重量化更明显不是显存实在不够用不建议。3.3 第三步Flash Attention长提示词的救星Flash Attention 在 llama.cpp 里就是一个开关-fa。它的主要作用是减少 attention 计算时的显存读写降低显存峰值同时显著提升长 prompt 的 prefill 速度。V100 的 sm_70 架构是支持 llama.cpp 里 Flash Attention 的实测下来对 decode 阶段的帮助没有想象中大但 prefill 阶段提升明显。我拿一段 4000 token 的长文本做测试开 FA 之前 prefill 要 8 秒左右开 FA 之后只要 5 秒。如果你的业务里有大量长文档问答、代码库分析这一步基本是必选项。关于线程参数纯 GPU 推理时其实不太吃 CPU 线程。我通常会设-t 8 --threads-batch 16给 prompt 处理阶段留多一点并行度生成阶段则交给 GPU。没必要把机器上的所有 CPU 核都塞给 llama.cpp线程太多反而会增加调度开销速度不升反降。这步做完单流生成速度从 44 左右提到 47。幅度不大但胜在稳定和免费而且它配合后面的投机解码会有更好的效果因为验证阶段通常要依赖 prompt 处理速度。3.4 第四步投机解码和连续批处理在 V100 上让单请求生成速度逼近带宽极限还有一个常规手段投机解码。原理是挂一个很小的草稿模型快速生成候选 token再用大模型并行验证这样本来只能生成 1 个 token 的时间可以一次性验证 4~6 个。我用的是同系列的 Qwen2.5-0.5B-Instruct Q8_0 作为草稿模型显存只占 0.5GB 左右在 V100 上跑得飞快。启动命令需要在前面基础上加几个参数./build/bin/llama-server \ -m /models/qwen2.5-27b-q4_k_m.gguf \ -md /models/qwen2.5-0.5b-instruct-q8_0.gguf \ --draft-max 6 --draft-min 3 \ -ngl 99 -c 8192 \ --cache-type-k q8_0 --cache-type-v q8_0 -fa实测单流生成速度从 47 提到 58 左右提升约 23%。这个方案有几个注意点草稿模型和目标模型的 tokenizer 必须一致所以最好选同一个系列的模型--draft-max不是越大越好对 27B 这种规模的模型6 个候选已经是性价比比较高的区间再大收益开始递减如果用户的请求都很短几十个 token投机解码的收益会明显缩水因为草稿还没跑满就结束了。除了让单请求变快还有一个思路是提升整体吞吐。llama-server 支持--parallel参数允许同时跑多路请求llama.cpp 会在内部做连续批处理把多个请求的 decode 阶段合并成一个大 batch。实测开--parallel 4之后同时压两路请求服务端每秒产出的总 token 数能从单流的 58 提升到 64 左右。注意这不是每个请求都变快而是整体吞吐变高适合多用户并发访问的场景。3.5 完整速度变化表把整个调优过程按步骤汇总时间线和提升幅度如下阶段主要动作单流 tok/s整体吞吐 tok/s说明基线默认参数CPU 推理3.83.8权重没进 GPU第一步-ngl 99全量进 GPU41.241.2收益最大的一步第二步KV cache q8_0 量化44.544.5短上下文提升有限省显存第三步开启 Flash Attention47.347.3prefill 提升明显第四步挂 0.5B 草稿模型58.758.7投机解码生效最终--parallel 4双路压测43.564.1整体吞吐到 64需要说明一下最终行的单流 43.5 是因为双路并发后两条请求平分了生成资源但用户感知到的等待时间并没有翻倍因为批处理摊薄了固定开销。你如果要对外宣传这卡能跑 64 tok/s指的是服务整体产出能力如果你关心单用户体感55~59 这个区间是常态。4. 服务化部署与压测实录4.1 最终启动命令把前面所有的优化项合并成一份可以直接抄的启动命令./build/bin/llama-server \ -m /models/qwen2.5-27b-q4_k_m.gguf \ -md /models/qwen2.5-0.5b-instruct-q8_0.gguf \ --draft-max 6 --draft-min 3 \ -ngl 99 \ -c 8192 \ -b 2048 -ub 2048 \ --cache-type-k q8_0 --cache-type-v q8_0 \ -fa \ -t 8 --threads-batch 16 \ --parallel 4 \ --host 0.0.0.0 --port 8080几个参数再解释一下-b是逻辑 batch size-ub是 prompt 处理阶段的上传 batch这两个主要影响 prefill 速度算力够的情况下建议都设成 2048--parallel 4允许服务同时保留 4 条会话状态配合连续批处理吸收并发请求--host和--port决定监听地址生产环境建议前面再套一层 Nginx 做 TLS 和限流。启动之后llama-server 会提供两个常用的健康检查接口/health返回服务状态/v1/chat/completions是兼容 OpenAI 格式的对话接口。对接现有应用时只需要把 base_url 指到http://你的IP:8080/v1即可。4.2 用真实请求验证速度启动成功后我习惯写一个小脚本做基准测试避免用浏览器反复刷新那个既不准确也容易堆积连接。核心逻辑是发一个流式请求设定固定输出长度统计首字延迟和平均生成速度。import requests import time resp requests.post( http://127.0.0.1:8080/v1/chat/completions, json{ model: qwen2.5-27b, messages: [{role: user, content: 写一篇关于推理优化的简短博客。}], max_tokens: 400, temperature: 0.7, stream: True, }, streamTrue, ) start time.time() first_token None count 0 for line in resp.iter_lines(): if not line: continue if first_token is None: first_token time.time() count 1 total time.time() - start print(f首字延迟: {first_token - start:.2f}s) print(f生成 {count} tokens 耗时: {total - start:.2f}s) print(f平均速度: {count / (total - start):.2f} tok/s)实测结果首字延迟 300ms 左右生成 400 个 token 大约 7.2 秒单流速度稳定在 55~58 tok/s。如果你测试时发现速度只有 30 多先检查是不是有别的进程在吃显存V100 的 HBM 带宽是共享的另一个 CUDA 进程哪怕只占 1GB 显存也可能明显拖慢推理速度。4.3 需要更强的并发时再考虑 vLLM如果你的应用场景是几十个人同时提问、请求短而频繁llama.cpp 的连续批处理能力会到瓶颈这时候 vLLM 是更合适的选择。vLLM 的核心优势是 PagedAttention 和更激进的 continuous batching显存利用率和请求调度效率更高。但 V100 上跑 vLLM 需要注意模型建议用 AWQ 或 GPTQ 量化版因为 vLLM 对 GGUF 的官方支持还不算稳定同时 V100 没有 bf16加载 FP16 权重时显存压力会更大。我个人的建议是单卡 32GB 显存、并发小于 10 路的场景llama.cpp 完全够用没必要为了先进引入一套更复杂的部署链路。如果你之后要扩到两张 V100vLLM 的张量并行会是更好的路径。单卡阶段别给自己找麻烦。5. 常见问题与排查技巧实录5.1 模型加载 OOM显存不够不是只有一种解法我在调优过程中遇到过两次 OOM。第一次是因为显存里已经跑了一个测试进程没退出nvidia-smi一看占用率 80% 才知道先杀掉再启动就正常了。第二次是上下文开太大-c 32768加上 FP16 KV cache直接把余量顶爆。排查 OOM 的正确姿势是先看这几个维度显存里有没有别的进程、权重占了多少、上下文开的多大、KV cache 是不是全精度。按照频率排序最有效的解药分别是换更小的量化档位、缩短上下文、开启 KV cache 量化。如果你已经用了 Q4_K_M 还把上下文开到 32K那 OOM 是正常的27B 模型在 32GB 卡上不建议长期跑超大上下文。5.2 编译报错V100 的架构参数别填错llama.cpp 在 V100 上编译最容易踩的坑是no kernel image is available for execution on the device。这个报错十有八九是 CUDA 架构没指定清楚编译时一定要加-DCMAKE_CUDA_ARCHITECTURES70。另一个常见现象是 Flash Attention 开关不生效日志里出现flash attention not supported, falling back。先确认 llama.cpp 版本是不是太老现在主流版本对 sm_70 的支持已经没有问题了其次确认编译时 CUDA 版本不低于 11.0太老的 CUDA 工具链也可能导致相关 kernel 缺失。5.3 量化完效果变差用 imatrix 找回精度Q4_K_M 在大多数场景下质量都够用但如果你发现模型回答开始出现明显的逻辑混乱、数字错误变多说明量化精度已经影响到了具体业务。优先升级到 Q5_K_M体积多 3GB 左右速度降到 38 上下但质量恢复很明显。如果你不想牺牲速度还有一个进阶技巧用 imatriximportance matrix重新量化。具体做法是准备一批与你业务相关的文本先让模型在 FP16 或 Q8 状态下计算重要性矩阵再带着这个矩阵做低比特量化。社区实践表明同样的 Q4_K_M带 imatrix 的版本在专业领域的表现明显更好这是免费的质量提升手段。5.4 16GB 显存版 V100 怎么抄作业如果和我一样手头是 32GB可以用上面全套配置。但市面上大量二手 V100 是 16GB 版本这里给一套降级方案量化档位上下文KV cache预计单流 tok/s说明Q4_K_S2048q8_055 左右显存最紧张勉强放下Q3_K_M4096q8_060 左右更稳质量略降Q4_04096q8_058 左右老式量化备选16GB 的方案里我建议放弃投机解码因为草稿模型会额外占显存或者草稿模型用 Q4_0 量化的 0.5B体积压缩到 0.3GB勉强也能塞进去。速度上 Q3_K_M 全 GPU 推理时理论上可以摸到 60 以上实际会受 KV cache 读取影响稳定在 55~60 之间。5.5 64 之后还想更快怎么办到了这一步你需要理解自己已经站在硬件的物理极限附近了。V100 的显存带宽是 900GB/sQ4_K_M 权重 16.5GB理论极限是 900 / 16.5 ≈ 54 tok/s加上 KV cache 量化和投机解码的优化能跑到 64 已经算是压榨干净了。想再快只有几条路换显存带宽更高的卡比如 H100 的 3.35TB/s 带宽同模型推理速度能翻几倍或者进一步降低模型精度比如 Q3_K 甚至 Q2_K但质量的代价可能需要你重新评估业务是否接受再或者把模型换成 MoE 架构比如 Qwen3 的 30B-A3B激活参数只有 3B同样带宽下速度会快很多但这是模型层面的替换不在本次调优范围内。6. 写在最后一点实操体会这轮调优做完我最大的体会是大模型推理本质上是带宽生意而不是算力生意。V100 这块卡虽然老但它的 HBM2 带宽放到今天依然能打只要把权重量化好、完整放进显存、再做一些针对性优化跑 27B 级别模型完全够用。那些一上来就追求最新框架、最新算力的人反而容易忽略最基础的显存分布问题。另外一个让我印象很深的小技巧是 imatrix。最开始我在 16GB 卡上测试时死活不愿意从 Q4 降到 Q3因为担心质量崩掉后来用 imatrix 重新量化了一版 Q4_K_S业务测试集上的表现几乎和 Q8 一样好但速度差出了一大截。所以遇到质量不够的问题时先别急着往上调精度档位试一下重量化往往有惊喜。这套部署方案我目前已经跑了大半个月稳定性很满意。后续如果想扩展可以直接在 llama-server 前面接一个 Open WebUI 做前端或者把接口接到 RAG 管线上27B 这个体量足够支撑中等规模的内部业务。希望这份实录能帮你少踩几个坑。
