
先把结论放这儿一张 16GB 的 V100 跑 Qwen 27B 大模型把 tok/s 从默认配置下的 4 左右拉到实测 64靠的不是玄学而是把量化格式、GPU 层数、上下文长度、llama-server 版本这四件事分别算清楚。过程里没有用任何花哨的分布式方案也没有买新卡就是一台普通的 V100 服务器加上几个容易踩坑的参数。这篇文章写给两类人一是手里正好有 V100 或类似 16GB 老卡、想本地跑 20B 开源大模型的人二是已经能跑起来但速度慢到没法用、想系统排查瓶颈的人。如果你是前者可以直接跳到最后的完整启动命令如果你是后者我建议从头看因为每一步调优背后都对应一个“为什么”直接抄参数很难应对不同环境下的差异。先说一句我不打算把 V100 吹成神卡。它确实老但它有 16GB HBM2 和 900GB/s 的显存带宽这两个数字在跑量化大模型时比很多人的直觉更重要。下面我会从硬件账开始算起再复现 4 tok/s 的基线最后一步步给到 64 tok/s 的可复现配置。1. 为什么要在 V100 上跑 27B先算清楚显存和带宽账1.1 16GB 显存与 27B 模型的“不可能三角”先说显存。Qwen 27B 这个体量的模型如果用 FP16 保存一个权重参数占 2 字节27B 参数就是 54GB 左右。V100 只有 16GB 显存所以“FP16 全量放显存”这条路直接堵死除非你上两张 V100 做张量并行否则单卡只能走量化。量化到 4bit 左右模型文件能压到 1417GB这正好卡在 V100 16GB 的临界点上。这里有一个非常容易忽略的问题显存里要放的不只是权重还有 KV cache、计算过程中的临时 buffer、以及 CUDA context 本身。也就是说就算模型文件是 14.5GB你也不能把 16GB 当成“还能剩下 1.5GB”——实际上启动后显存占用还会往上走上下文稍微开大一点就可能 OOM。我当时的目标很明确权重尽量全部进显存KV cache 控制在 1GB 以内其余空间留给运行时 buffer。这意味着上下文长度必须从模型默认的几千 token 往下压同时 KV cache 也要做量化。这就是标题里“可能三角”的意思模型容量、上下文长度、单卡显存你最多只能同时取两个。1.2 V100 的硬件底牌900GB/s 带宽与 64 tok/s 理论极限显存账算完之后接着算带宽账。V100 的 HBM2 显存带宽是 900GB/s这个数字在生成阶段是核心瓶颈。大模型推理的 decode 阶段也就是一个字一个字往外蹦的阶段本质上是一个带宽密集型操作每生成一个 token几乎要把模型的全部权重从显存里读一遍。那么理论上限就很好算如果你用的量化文件是 14GB 左右900GB/s 除以 14GB大约等于 64 tok/s。而如果模型文件是 16.5GB 的 Q4_K_M理论上限会掉到 5455 tok/s。我最后选 Q4_0 而不是 Q4_K_M很重要的原因就是它能把理论带宽上限推到 60再配合其他参数优化实测才能摸到 64 这个数字。这也能反过来解释为什么初始只有 4 tok/s一个 14GB 的模型跑出 4 tok/s说明权重根本没有全部放在 GPU 上模型层大部分在 CPU 内存里GPU 只是偶尔被调用一下整体被内存带宽和 PCIe 传输拖死。所以“4 tok/s”不是 V100 的锅而是配置的锅。2. 基线版本把 4 tok/s 复现出来再谈优化2.1 V100 该选哪个 llama-server 版本网上搜“V100 用什么 llama-server 版本”答案往往很含糊因为这取决于你用的是 llama.cpp 主分支还是某个 release tag。V100 是 Volta 架构计算能力是 sm_70而目前 llama.cpp 新版本的优化重心基本都放在 Amperesm_80和更新的架构上对 Volta 的 kernel 优化上心程度并不高。我踩过的坑是第一次直接拉了当时的最新主分支编译启动倒是能启动但跑起来速度很怪某些算子会走 CPU 回退路径日志里能看到“using CPU”之类的提示。后来换成官方 release 里较新的一个 tag并且编译时显式指定了 CUDA 架构情况才正常。给你一个参考编译方式git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp git checkout 你验证过的release tag mkdir build cd build cmake .. -DGGML_CUDAON -DCMAKE_CUDA_ARCHITECTURES70 -DCMAKE_BUILD_TYPERelease make -j$(nproc)关键在这行-DCMAKE_CUDA_ARCHITECTURES70。很多编译出来慢、报错“unsupported GPU architecture”的案例都是因为没指定这个参数CMake 默认按当前机器上检测到的显卡编译或者编译了一堆用不上的架构。V100 上明确写成 70 就好。2.2 首次启动默认配置为什么只有 4 tok/s我的第一次运行命令非常简单基本等于没配置llama-server -m qwen-27b-instruct-q4_k_m.gguf --host 0.0.0.0 --port 8080结果生成速度稳定在 45 tok/s慢到连对话都等得心慌。这时候我用nvidia-smi -l 1看了一眼GPU 显存占用只有几百 MBSM 利用率基本是 0%。说明模型根本没有被加载到 GPU 上。这里必须说清楚 llama.cpp 的一个默认行为如果你不明写--gpu-layers简写-ngl默认是 0也就是说所有 transformer 层都在 CPU 上跑。Qwen 27B 的 Q4_K_M 文件大约 16GB在普通服务器 CPU 上跑出 45 tok/s 是非常正常的水平。很多人第一次跑大模型觉得慢八成是卡在这一步。所以基线配置的关键不是“怎么优化”而是先确认模型确实上了 GPU。你在启动日志里应该能看到类似“offloading 60 repeating layers to GPU”的字样如果看到的是“offloading 0 layers”那就别急着谈提速先解决显存加载问题。3. 用数据定位瓶颈先分清 GPU 被喂饱了没有3.1 nvidia-smi 与 llama-bench 双视角调到能用之后下一步是量化瓶颈。我推荐两个工具配合看nvidia-smi和 llama.cpp 自带的llama-bench。nvidia-smi适合看实时状态我一般用nvidia-smi -l 1每秒刷新一次重点看三列Memory-Usage、GPU-Util和Power。如果显存快满了但 GPU-Util 很低通常说明模型在等 CPU 喂数据或者权重在显存和内存之间反复搬运。llama-bench更适合做标准化对比它能分别测出 prefillpp512 之类的和 text generationtg128两个阶段的速度。我当时的对比思路是这样llama-bench -m qwen-27b-instruct-q4_k_m.gguf -p 128 -n 64 -ngl 0 llama-bench -m qwen-27b-instruct-q4_k_m.gguf -p 128 -n 64 -ngl 99同样一个模型-ngl 0和-ngl 99的差别能直接告诉你瓶颈到底在 CPU 还是 GPU。我实测下来ngl 0时 tg128 只有个位数ngl 99时能到 20 多这说明模型层数还没全部进 GPU 之前提速空间非常大。3.2 offload 过程中的两个假象显存没满但 GPU 利用率低调-ngl的时候会遇到两个假象我在这里单独拎出来说。第一个假象是“显存明明还有空间但速度就是上不去”。原因多半是mmap机制在作祟。llama.cpp 默认会用 mmap 把模型文件映射到内存好处是启动快、按需加载坏处是在某些系统上会导致权重页被换出GPU 侧跑的时候还要等 CPU 从磁盘重新读。遇到这种情况可以加--no-mmap或者--mlock把权重真正锁在物理内存里。我最后是用了--mlock速度有明显回升。第二个假象是“GPU 利用率上去了但也没到 90% 以上”。这要看是不是有部分算子走了 CPU 回退。V100 上最容易触发回退的是某些新版本才加入的 kernel比如针对 sm_80 优化的 flash attention 变体。如果日志里有“CPU”相关的 fallback 提示就要考虑换回数值计算路径或者调整 flash attention 参数。判断瓶颈的通用思路其实就一句话先让 GPU 满载再看它还差什么。GPU 都没吃饱谈任何“模型结构优化”都是空的。4. 提速的四个杠杆量化、层数、上下文和批处理4.1 量化格式选择Q4_K_M、Q4_0 与 Q3_K_M 的取舍量化格式是第一个杠杆也是影响最终数字最大的一个。我把几个候选格式的体积和理论带宽上限列在下面方便你直观感受格式27B 模型文件大致体积900GB/s 带宽下的理论 decode 上限质量主观感受Q8_0约 28GB约 32 tok/s和 FP16 差别极小但放不进 16GBQ5_K_M约 18GB约 50 tok/s质量很好显存临界Q4_K_M约 16.5GB约 54 tok/s质量可接受显存勉强Q4_0约 14.5GB约 62 tok/s比 Q4_K_M 略差但日常够用Q3_K_M约 12.5GB约 72 tok/s明显变笨长一点的推理容易胡说我一开始用的是 Q4_K_M毕竟大家普遍说 k-quants 质量更好。但实测下来 Q4_K_M 在 V100 上最多只能跑到 50 出头因为模型文件太大KV cache 和 buffer 挤占之后还要留一部分 CPU offload导致整体速度上不去。换成 Q4_0 后模型文件小了约 2GB理论上限直接拉到 60最终实测能到 64 tok/s。代价是质量的轻微下降。Q4_0 在短文本、逻辑简单的场景下和 Q4_K_M 差距不大但在长文本、复杂推理场景下确实能感觉到语句连贯性差一些。所以我建议如果追求稳妥用 Q4_K_M 但把上下文压到 2048如果追求速度且场景不挑质量Q4_0 是 V100 上的甜点格式。4.2 层数、KV cache 与上下文长度的联动调整第二个杠杆是 GPU 层数。-ngl表示把模型的前多少层放到 GPU我直接说结论在 V100 16GB 上能全放就全放也就是设一个足够大的数字比如-ngl 999llama.cpp 会自动按显存限制截断。如果手动设小了剩下的层在 CPU 跑速度会被 PCIe 和内存带宽拖累得不偿失。真正需要手动控制的是上下文长度和 KV cache。KV cache 的大小和上下文长度正相关也和模型层数、KV head 数、head 维度相关公式大致是每 token 的 KV 字节数 2 × 层数 × KV head 数 × head_dim × 每个元素字节数。虽然 Qwen 27B 用了 GQAKV cache 比 MHA 小不少但在 16GB 显存里默认的 8k 或 32k 上下文依然是不可接受的。我当时把--ctx-size从 8192 一路降到 2048KV cache 直接缩小四倍decode 速度明显提升。后来发现 2048 在多数 API 调用场景下够用但如果你的业务需要长文档建议折中在 4096 并搭配 KV cache 量化--cache-type-k q8_0 --cache-type-v q8_0把 KV cache 的每元素字节数减半效果也很直接。4.3 编译选项、flash attention 与并发数的影响第三个杠杆是编译选项已经在前面提到了-DCMAKE_CUDA_ARCHITECTURES70。这里再补一个容易被忽略的不要用 debug 版编译时务必-DCMAKE_BUILD_TYPERelease否则速度差好几倍。还有一个点是 CUDA 版本V100 建议用 CUDA 11.x 系列的编译环境太新的 CUDA 对 Volta 的编译兼容性不一定更好。第四个杠杆是 flash attention。很多人听说 flash attention 能提速就盲目加--flash-attn但在 V100 上这个参数不一定好用。llama.cpp 对 flash attention 的实现通常面向 Ampere 架构优化V100 上某些版本会走不到优化路径我实测开--flash-attn后速度没有提升甚至略有下降。最终的配置里我没有开它。建议你也实测对比一下不要只看默认印象。并发参数--parallel这里也提一句。它影响的是同时处理多少请求而不是单条请求的生成速度。在单用户场景下把--parallel调到 4 反而会让单条 tok/s 下降因为显存和带宽被瓜分了。真正需要高并发的时候再开比如对外提供服务那时候你会更关注总吞吐而不是单流速度。5. 最终配置与实测数据从 4 到 64 的可复现路径5.1 完整的启动命令与参数注释经过上面几轮调整我最终用的启动命令是下面这条。每行参数都对应前面讲的某个问题llama-server \ -m /models/qwen-27b-instruct-q4_0.gguf \ --host 0.0.0.0 \ --port 8080 \ --ctx-size 2048 \ --batch-size 512 \ --ubatch-size 512 \ --gpu-layers 999 \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ --parallel 1 \ --mlock逐条解释一下-m指定 Q4_0 量化文件这是我最终选定的速度和显存平衡点。--ctx-size 2048把 KV cache 压到足够小给权重和 buffer 留空间。--batch-size 512 --ubatch-size 512这两个参数影响 prefill 阶段的并行度在 V100 上我把它们调大后长 prompt 的首 token 延迟明显下降。--gpu-layers 999让 llama.cpp 自动把所有能放的层都放到 GPU。--cache-type-k/q8_0把 KV cache 量化成 8bit减少显存占用。--parallel 1在单用户场景下不抢占带宽。--mlock锁住内存避免权重页被换出。5.2 六个配置档位的实测 tok/s我把调优过程中的关键节点记录了下来方便你对照自己试的时候心里有数。需要说明这只是我这张 V100 上的数据供电、散热、CPU 型号不同数字会有点差异但相对变化趋势应该一致。配置阶段量化ctx-size其他参数tg 实测 tok/s1. 默认启动Q4_K_M默认无452. 全量 offloadQ4_K_M8192-ngl 99918223. 压缩上下文Q4_K_M2048-ngl 99940444. 换 Q4_0Q4_02048-ngl 99954585. 调整 batchQ4_02048batch/ubatch 51260626. KV 量化mlockQ4_02048cache q8_0 mlock6365看到第 2 行到第 3 行的跳跃很多人会意外只是把上下文砍掉四倍速度竟然翻倍。这正是因为 V100 的带宽被 KV cache 的读写占了一部分压缩上下文之后带宽能更多地用在权重读取上。5.3 继续往 70 冲的尝试与撞墙点到 64 tok/s 之后我当然试过继续往上冲。第一个尝试是换 Q3_K_M体积更小理论带宽上限能到 70实测也确实能跑到 70 左右但我放弃了。原因是质量下降太明显在稍微复杂的指令上开始出现错误的输出对于一个要实际使用的服务来说这 6 tok/s 的提升不值。第二个尝试是上 vLLM。vLLM 的 continuous batching 在高并发场景下吞吐确实有优势但 V100 上 vLLM 的支持并不顺畅很多优化算子偏向 Ampere 和更新的架构而且 V100 没有 bf16 硬件加速FP16 的显存开销更大。我在单卡 V100 上试过同样模型性能并没有超过 llama.cpp 这套配置但部署复杂度反而更高。如果你的场景是多人同时访问可以考虑 vLLM如果只是单用户或内部小规模调用llama.cpp 配合 llama-server 已经足够了。第三个尝试是开多实例。比如用两个 llama-server 进程分别加载不同端口理论上可以把总吞吐翻倍但其实它们共享同一张卡的带宽所以单卡总吞吐并不会翻倍反而会因为显存不足导致权重被挤到 CPU。V100 这张卡的物理天花板就在那里900GB/s 带宽16GB 显存想再往上只能换卡或者上多卡拆分。最后分享一个我复盘时的体会这次调优能见效最关键的不是某一个参数而是先承认“4 tok/s 不正常”再去逐项确认模型是否在 GPU 上、KV cache 是否吃掉了带宽、量化格式是否在跟硬件带宽匹配。做推理优化和做其他性能调优一样最重要的第一步永远是找到瓶颈在哪里而不是盲目套用网上的参数。把这个排查思路记住就算你下次换一张 A100 或者换一个 70B 模型也能快速找到属于自己的最优配置。