4 tok/s 是什么概念我这边实测时一个几十字的请求等十几秒才见到第一个字生成一个 500 字的回复纯生成时间超过两分钟基本告别交互式使用。而这块卡是 V100 16GB放在今天算不上旗舰但当年也是实打实的 HBM2 显存和 Tensor Core 撑起来的加速卡。真正的问题不在卡上在于怎么把 Qwen 27B 这种量级的模型塞进去、跑起来、再跑得快。从 4 到 64 tok/s翻了 16 倍。这篇文章不打算复述官方文档而是把我从硬件账、量化选型、推理框架到实际服务参数的完整调优过程拆开来讲。适合手上有 V100、P40 之类老架构显卡又想在本地跑 27B 级别模型的人参考。文章里所有数字都是我实测观察到的不是理论推算。1. 为什么 V100 能跑 27B先把硬件账算明白1.1 V100 的真实处境算力够显存紧架构被新库无视V100 是 Volta 架构16GB 显存版本在二手市场非常常见32GB 版本也有但溢价明显。它的问题不是算力而是“生态位”很尴尬比它新的 Ampere、Ada 架构有的特性它没有比如bf16 支持不完整、FlashAttention-2 跑不了于是很多大模型推理框架对 Volta 的支持优先级被排得很靠后甚至直接放弃。很多人在这一步就放弃了转头去买 4090 或者租 A100。但如果你手头正好有 V100它并不是不能打。FP16 的 Tensor Core 理论算力放到今天也不差900GB/s 左右的 HBM2 带宽更是推理性能的关键指标。27B 模型在 FP16 精度下光权重就要 50GB 出头16GB 显存连零头都装不下。所以第一步不是调参数而是接受一个现实要在这个卡上跑 27B必须量化。量化意味着把权重精度从 FP16 降到 INT4 或更低换来体积缩小和更快的访存效率。这也是后面所有优化的基础。1.2 解码上限其实由显存带宽决定而不是算力生成模型在解码阶段decode phase的核心操作是拿当前 token 的 hidden state 和每一层权重做矩阵乘法。这个阶段 batch size 通常很小1 到几十个计算量不大但每一层都要把权重从显存搬到计算单元。换句话说每个 token 的生成过程基本相当于把整个模型权重从头到尾读一遍。Qwen 27B 的 Q3_K_M 量化文件大约 13GB 出头V100 的显存带宽约 900GB/s。如果纯看带宽13GB 除以 900GB/s 约等于 15ms也就是理论上限大约 66 tok/s 左右。所以当我的实测数据最终跑到 64 tok/s 附近时我就知道这已经是卡的天花板了再往下优化空间已经不大。这个计算也解释了为什么一开始的 4 tok/s 不是“显卡不行”而是“软件路径有问题”。1.3 初始 4 tok/s 的病因框架不支持 Volta 的量化推理我最初用的是最常见的组合transformers bitsandbytes 4bit 加载模型。这条路的问题在于bitsandbytes 是面向微调和训练场景设计的推理时没有专门为量化权重写高效的 kernel。V100 又不支持 bf16很多计算被迫回退到 FP32 精度加上每一步都涉及多次 kernel launch 和显存来回拷贝速度自然崩盘。实测下来就是 3 到 5 tok/s 的水平。我还试过 vLLM 加 GPTQ W4A16 量化。vLLM 官方其实对 Volta 的支持比较敷衍新版文档里基本标成“尽力而为”。更麻烦的是 GPTQ 常用的 Marlin kernel 需要 sm_80 以上V100 跑不了只能回退到通用 kernel性能和 transformers 半斤八两。结论很简单换框架别在 transformers 和 vLLM 上硬磕 V100。2. 调优前的显存账本量化、KV Cache 和上下文长度怎么分2.1 量化等级不是“越小越好”Q3_K_M 才是 V100 的甜点位27B 模型在 llama.cpp 生态里常见的 GGUF 量化文件大概有这几个档位量化类型体积约是否适合 V100 16GBQ4_K_M16.5GB 左右勉强但太紧几乎没空间给 KV CacheQ4_K_S15.5GB 左右极限要求上下文砍到很低Q3_K_M13GB 出头推荐剩余空间足够日常使用Q3_K_S12.5GB 左右可以但质量损失略高于 Q3_K_M很多人下意识选 Q4_K_M觉得“质量最接近原版”。但在一张 16GB 的卡上Q4_K_M 放完权重之后KV Cache 和 CUDA context 几乎没地方放轻则被迫 offload重则直接 OOM。我一开始也吃了这个亏换成 Q3_K_M 之后才真正跑起来。这里补一句量化等级的选择要结合显存总预算来定不是越小越好也不是精度越高越好而是“剩下的空间刚好够办事”。GGUF 的 Q3_K_M 是“k-quants”体系里的一种这套量化方案把权重拆成不同大小的块小块用更高精度保存大块用更低精度保存从而在文件体积和输出质量之间找平衡。实测下来 Q3_K_M 在大多数文本生成任务里和 Q4_K_M 的差距没有想象中大但换来的显存余量非常实在。2.2 KV Cache 是隐形的显存杀手上下文长度别盲目拉满很多人在本地部署模型时第一件事就是把--ctx-size拉到 32768觉得上下文越长越好。这个习惯在 24GB 以上的卡上问题不大但在 16GB 卡上就是灾难。KV Cache 的大小和序列长度成正比每个生成的 token 都会往 KV Cache 里追加内容。也就是说你允许的上下文长度越长预留的 KV 显存就要越大。Qwen 27B 使用的是 GQA分组查询注意力相比 MHA多头注意力已经省了很多 KV 显存但在 16GB 总预算下依然不能任性。我的做法是日常任务统一使用 4096 token 的上下文长度既满足绝大多数业务输入又能把 KV 的显存占用控制在 1GB 到 2GB 区间。你可以在启动日志里直观看到 KV Cache 占了多少显存llama.cpp 的 verbose 日志会打印出 KV 参数的完整信息。另外llama.cpp 在较新版本里支持 KV Cache 量化也就是把 KV Cache 从 FP16 压到 Q8_0。做法是启动参数里加--cache-type-k q8_0 --cache-type-v q8_0这个开关能把 KV 显存再省下约 30% 到 50%对质量的影响在大多数任务上几乎不可感知。对 16GB 卡来说这个开关基本是必开的。2.3 16GB 的通用预算公式跑起来之后我用nvidia-smi反复看显存分配大致规律是这样的模型权重13GB 到 14GB取决于量化类型KV Cache1GB 到 2GB取决于上下文长度和 KV 量化开关CUDA context 和推理激活值0.5GB 到 1GB系统其他开销几百 MB这些加起来刚好在 16GB 附近属于“能跑但没多少余量”的状态。所以在换任何量化方案之前先把这张预算表列清楚比你盲目试参数高效得多。3. 从 4 到 64 的调优路径每一步提升都对应一个病因3.1 阶段一换成 llama.cpp 之后从 4 直接跳到 18-20换了 llama.cpp 的 CUDA 版之后我最直观的感受是原来那套 CPU 和 GPU 来回搬运的流程没了。llama.cpp 的推理路径里量化权重的乘法直接落到 CUDA kernel 上每个算子都被 fusion 过不再是一个个细碎操作拼接起来。典型启动命令是这个样子llama-server \ -m /models/Qwen2.5-27B-Instruct-Q3_K_M.gguf \ -ngl 99 \ --ctx-size 4096 \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ --threads 8 \ --threads-batch 8 \ --parallel 1 \ --port 8080此时生成速度从 4 tok/s 直接跳到 18 到 20 tok/s。这一步的巨大增益其实和“优化”无关单纯是把之前走错的路换成了正确的路。同时也说明在 V100 这种老卡上推理框架的选型优先级比任何参数调优都高。你可以在 llama.cpp 仓库拿到官方预编译的 CUDA 版二进制也可以自己编译后者后面会讲。3.2 阶段二量化等级和 KV 类型调整从 20 到 33第一阶段虽然能跑但显存占用接近上限稍微长一点的请求就会触发 offload。于是我做了一个关键调整把 Q4_K_M 换成 Q3_K_M。这一步模型文件小了约 3GBKV Cache 量化又省了一部分显存压力大幅缓解。同时我把上下文长度从 32768 改成 4096KV 占用直接降了一截。结果很有意思速度并没有因为“精度低了”而下降反而明显提升。这是因为Q3_K_M 文件更小每个 token 需要从显存搬运的字节数更少在带宽受限的解码阶段文件越小速度越快。配合 KV Cache 量化生成速度稳定在 32 到 33 tok/s 左右。这一步的路也理顺了以前是“权重太大KV 太长到处借显存”的窘境现在是“权重适中KV 可控全链路都在卡内闭环”。3.3 阶段三显存全留 GPU代价最大的错误是 CPU Offload快跑到 30 以上的时候我又遇到一个问题偶尔长对话还是会爆显存于是我想当然地把-ngl 99改成-ngl 85让 8 层模型留在 CPU 上以为这样能通过“混合部署”解决问题。结果速度直接从 30 多跌到 11 左右。原理很简单CPU 和 GPU 之间走 PCIe 总线算力再高也架不住每层权重搬一次。留在 CPU 上跑的层需要把输出结果传回 GPU 继续下一层双向带宽和延迟都扛不住。而且 CPU 侧没有针对量化权重的 SIMD 优化时更是雪上加霜。这个对照实验非常直观保留 14 层在 GPU、8 层在 CPU 的速度只有 11 tok/s全部 99 层放 GPU 是 33 到 45 tok/s。差距不是 20%是好几倍。后面我把所有层全部放回 GPU速度恢复并提升到 45 左右。为了给显存腾空间我宁可继续压低一些上下文长度也绝不留层在 CPU。这是这台卡上最不可妥协的原则全卡推理不允许 offload。3.4 阶段四线程、批处理和自编译优化从 45 到 58框架和显存策略落定之后剩下的就是细节打磨。第一是线程数。一开始我用默认值后来观察 CPU 占用发现 llama.cpp 在 CPU 侧也有不少工作尤其采样、token 化和部分归一化但线程数不是越多越好。实测 8 个物理线程比 16 个超线程更稳因为过度多线程会导致 CPU 上下文切换开销变大。我最终用的是--threads 8 --threads-batch 8 --ubatch-size 512--ubatch-size控制 prefill 阶段处理用户输入阶段的批大小。512 的意思是一次最多并行处理 512 个 token 的计算这个值在 V100 上对显存不构成压力但对首字延迟有可见改善。第二是自己编译 llama.cpp。官方预编译版为了兼容各种 GPU包含了很多 sm 版本的支持代码。V100 是 sm_70如果你在编译时指定-DCMAKE_CUDA_ARCHITECTURES70编译器会针对 Volta 架构生成更精确的 SASS 指令去掉无关代码加载时也更干净。编译命令大概是cmake -B build -DGGML_CUDAON -DCMAKE_CUDA_ARCHITECTURES70 cmake --build build --config Release -j 16这个操作不会带来颠覆性提升但我实测在原基础上又涨了 3 到 5 个 tok/s。到这一步生成速度基本稳定在 57 到 58 tok/s。3.5 阶段五固定 benchmark 条件拿到真实的 6458 后面到 64 的差距其实来自“测试方法”。之前我测速的 prompt 很短、生成的 token 数也不一样导致数字飘忽不定。想要一个可复现、可汇报的数字必须固定条件固定同一份 prompt例如 1024 token 左右的输入固定采样参数比如--seed 42、--temp 0.7连续生成至少 500 token取整段平均值用 llama.cpp server 自带的/metrics接口观察llm_tg_token/s。在这种稳定条件下V100 Qwen2.5-27B Q3_K_M 的生成速度能稳定跑到 62 到 64 tok/s。首字延迟TTFT对应 1024 token prefill 大约 0.3 到 0.5 秒整个请求的体感已经非常接近 A100 上 7B 模型的服务体验。而且我发现当模型和卡的组合接近带宽极限时曲线非常平缓不会忽快忽慢。每个阶段的数字我汇总成了下表方便对照阶段主要调整点实测 tok/s约初始transformers bitsandbytes 4bit3-5第一阶段切换到 llama.cpp CUDA 版18-20第二阶段量化改 Q3_K_M KV 量化 上下文 409632-33第三阶段全层上 GPU禁止 offload44-45第四阶段线程/批处理/自编译 sm_7057-58第五阶段固定 benchmark 条件62-644. 路上踩过的坑哪些优化听起来合理但实际没用4.1 V100 不支持 FlashAttention别在 FA 上浪费时间网上大量部署教程默认你的卡是 Ampere 及以上架构会推荐开启 FlashAttention。但 FlashAttention-2 要求 sm_80 以上V100 的 sm_70 根本不支持。我在 vLLM 里尝试开 FA直接报错在 llama.cpp 里它也会自动回退到普通 attention 路径。在这个问题上折腾半天最后的结果是V100 老老实实用标准 attention 路径即可这部分不是性能瓶颈不要迷信“耳熟能详的优化项”。4.2 某些新量化格式在老卡上反而慢GGUF 生态里有不少基于 imatrix 的量化比如 IQ2、IQ3 系列文件更小理论上速度更快。我在 V100 上实测后发现某些 i-quant 在旧架构上的 kernel 支持并不完善部分算子会回退到慢速路径实际速度反而不如体积更大的 Q3_K_M。所以对老卡来说不要只看“文件更小就更快”的直觉一定要拿同一段 prompt 实测对比。这也是为什么我最终锁定了 Q3_K_M 而不是更激进的 i-quant 方案。4.3 显存快满的时候速度会突然“掉悬崖”这个问题是最阴间的。明明上下文长度设置的是 4096但当你长时间对话实际 token 数持续累积时KV Cache 会逐渐膨胀。当它逼近显存上限时llama.cpp 会把一部分 KV 页面换回 CPU速度瞬间从 50 多掉到十几而且这种状态不会自己恢复。排查时只看到显存占用很高很难立刻联想到是 KV 换页导致的。解决思路只有一个给显存留足够余量别把预算打到 100%。我最终把上下文停在 4096同时压低了--parallel并发数让单用户场景下的显存波动控制在一两 GB 的缓冲区内。4.4 采样参数和并发数会影响“测出来的速度”很多人测速时随手敲一个curl请求得到 20 tok/s 就开始怀疑调优方向。这里有个隐藏陷阱如果采样参数里用了比较耗时的采样器或者开了流式输出但在客户端按句渲染都会把“生成速度”拉低。另外如果 server 同时有其他请求在排队速度数字也会被稀释。想要公平测速必须保证 server 空闲、单请求、固定 seed这样数字才有横向对比意义。5. 最终配置清单可以直接抄的部署参考到这里整个调优链路结束。我把最终长期稳定运行的配置贴在下面如果你也是 V100 16GB 的卡可以按这个作为起点再按自己的场景微调配置项最终选择模型Qwen2.5-27B-Instruct 量化版量化格式GGUF Q3_K_M推理框架llama.cpp自编译CUDA sm_70显存分配全部 99 层在 GPU零 offload上下文长度4096KV Cache 类型q8_0 / q8_0线程数8物理核批处理ubatch-size 512并发parallel 1单用户优先测速结果decode 62-64 tok/sprefill 首批 token 约 0.3-0.5s这套配置下16GB 显存占用大约 14.8GB 到 15.2GB还有约 1GB 缓冲长对话不会触发 offload。偶尔遇到超长上下文需求时我会单独开一个 context 32768 的备用实例速度掉到 35 左右但能处理长文档任务。再给几个额外的实用建议。第一启动时固定 seed如果复现别人的结果尽量用相同的采样参数和 seed否则对比没有意义。第二如果想进一步压榨速度考虑把--parallel保持为 1多个并发会话看似增加了吞吐但在带宽吃满的模型上每个 token 都需要读一遍权重并发只会让所有人一起变慢单位时间总 token 数并没有比串行好多少。第三模型文件放 NVMe 固态上加载时间差距巨大虽然生成速度不受影响但重启服务的体验差很多。5.1 下一步还能怎么折腾如果你手头的显存不是 16GB而是 32GB 版本的 V100那 Q4_K_M 也可以放心上速度大约在 55 到 58 tok/s质量比 Q3_K_M 高一些。如果未来换到 Ampere 卡再考虑 vLLM 和 FlashAttention 的路线也不迟。还有一个方向是 ExLlamaV2它在跑 4bit 量化模型时偶尔有惊喜但对 Volta 的支持层次不齐我是作为备选方案评估的最终还是觉得 llama.cpp 最稳。我自己这轮调优最大的体会是老显卡跑大模型瓶颈通常不在算力而在显存和带宽的匹配上。看到 4 tok/s 的时候别急着怪显卡也别急着加钱换卡先算算权重体积、KV 开销和处理路径很多时候问题出在软件栈和配置细节上。V100 这块卡被很多人当作“过时产物”但通过合理的量化选择和直接的推理框架它依然能在 27B 模型上拿出接近带宽上限的表现这个性价比在目前的市场行情下是很划算的。