
先交代一下背景我手头有一块老旧的 Tesla V100 32GBPCIE 版没有 NVLink单纯是上次项目淘汰下来的卡。本来想用它跑 7B 模型凑合一下结果发现手头刚好有一个 Qwen 27B 的模型需求。27B 听起来不大但放到只有 32GB 显存、并且是 Volta 架构的 V100 上处处都是坎。刚开始我用 FP16 直接加载模型权重 54GB32GB 显存根本放不下推理速度慢到怀疑人生生成一个 token 要 250 毫秒也就是 4 tok/s 左右。后面经过量化、框架替换、KV Cache 调整、并发配置等一系列操作最终把单条流的生成速度干到了 64 tok/s整体提升了 16 倍。这篇文章就是我当时完整的调优实录从为什么慢、慢在哪到每一步动了什么参数、带来了多少收益全部摊开讲。如果你手里也有一块 V100、P40、T4 这类老卡或者只是不想一上来就烧 H100 的预算那这篇文章应该能帮你少走很多弯路。整个过程不涉及玄学靠的就是算明白“权重放不下、带宽不够用、框架没吃透”这三件事。1. 先摸清家底V100 与 27B 模型的真实差距1.1 V100 的优势与短板先说 V100 这卡。Volta 架构2017 年发布到现在确实不年轻了但它有几个底子到今天依然能打HBM2 显存带宽约 900GB/s这个数字放在今天依然不算低FP16 算力官方标称 112 TFLOPS 左右Tensor Core 加持。对于大模型推理尤其是 decode 阶段显存带宽往往比算力更关键所以 V100 跑量化后的小模型并没有想象中那么不堪。但短板也很明显不支持 BF16 硬件指令FP8 就更别想了。很多新框架和 kernel 默认针对 Ampere、Hopper 优化放到 V100 上要么不兼容要么吞吐惨不忍睹。再加上 PCIE 3.0 的带宽上限大约 12GB/s这决定了模型权重只要有一丁点放在 CPU 内存就会成为灾难。记住这句话后面 4 tok/s 的锅就在这。1.2 27B 模型有多大欲望有多大我们先给 27B 模型算一笔账。假设参数是 27B不同精度的权重大致如下FP1627B × 2 Bytes ≈ 54GBINT827B × 1 Byte ≈ 27GB4bit 量化27B × 0.5 Byte ≈ 13.5GB加上 embedding、lm head 等实际约 14.5GB所以只要你还想要一点 KV Cache 和激活值空间FP16 和 INT8 在单卡 V100 上基本是没戏的。4bit 量化不是“可选优化”而是“能不能跑起来”的硬门槛。顺带算一下 KV Cache。我用的是 GQA 结构的 27B 版本假设 48 层、8 组 KV 头、每组 head_dim 128那么每 token 的 KV Cache 大约是 2 × 48 × 8 × 128 × 2 Bytes ≈ 384KB。如果给 4096 上下文就是约 1.5GB给 8192 上下文就是约 3GB。这个数在后续调参时非常关键。1.3 一开始 4 tok/s 的真相我第一版压根没做量化直接拿 HuggingFace Transformers 加载 FP16device_mapauto 让它自动分配。结果也很符合预期54GB 权重显存只放得下 60%剩下 40% 被放到系统内存。这就引出了最致命的问题。decode 阶段每个 token 都需要读取几乎所有权重参与计算一旦部分权重在 CPU 内存里模型只能先把这些层从 PCIE 搬回 GPU算完再等下一轮。PCIE 3.0 实际带宽也就 8-10GB/s配合 CPU 侧内存分配效率最后生成一个 token 要 250ms 左右也就是 4 tok/s。这不是模型笨是“数据搬运速度”锁死了性能。我当时用 nvidia-smi 一看GPU 利用率不到 20%但 CPU 占用和内存拷贝量高得离谱基本就实锤了。所以第一步调优思路也很明确想办法把权重完整塞进显存哪怕牺牲一点精度。2. 第一刀把量化这件小事做对2.1 主流量化方案怎么选现在模型量化方案很多我重点对比几个当时真实用过的AWQ、GPTQ、GGUF 的 Q4_K_M/Q4_0、IQ4_XS。AWQ基于激活感知的权重量化通常保存为 safetensors配合 Transformers 或 vLLM 使用加载简单精度损失小。GPTQ经典的后训练量化方法和 AWQ 在同一梯队但推理时 kernel 依赖较高。GGUF Q4_K_Mllama.cpp 生态的标准量化格式K-quant 方法对权重逐块分配不同 bit属于精度和体积平衡的“万金油”文件也容易下载。IQ4_XS比 Q4_K_M 更激进的量化文件更小但 V100 上实测没有太明显的速度优势反而偶尔精度损失更明显。我的结论是如果走 Transformers 路线用 AWQ如果走 llama.cpp 路线用 Q4_K_M。至于 Q4_0除了文件稍小精度和速度都不如 Q4_K_M不推荐。2.2 Transformers AWQ 的实操记录第一次替换方案我下载了对应 27B 模型的 AWQ 4bit 版本代码很简单from transformers import AutoModelForCausalLM, AutoTokenizer model_name local_path/qwen-27b-instruct-awq model AutoModelForCausalLM.from_pretrained( model_name, device_mapauto, torch_dtypeauto ) tokenizer AutoTokenizer.from_pretrained(model_name)加载后显存占用约 15GB全部塞进 V100 32GB 没问题。实测生成速度直接跳到 28 tok/s相比最初的 4 tok/s提升了 7 倍。这一步也验证了前面的判断只要权重全部在显存里哪怕 Transformers 这种相对厚重的推理链路也能跑出像样的速度。28 tok/s 这个数字看起来不错但其实还没到 V100 的极限。原因有两方面一是 Transformers 的 attention 实现没有用 flash attentionKV Cache 读取开销大二是一次只生成一个 tokenkernel 启动和管理开销占比高。所以接着我换了一条更“底层”的路llama.cpp。2.3 从 28 到 40把 KV Cache 和显存布局摆正在换框架之前我还做了一件值得单独说的事把 KV Cache 量化打开并严格控制上下文长度。llama.cpp 支持--cache-type-k q8_0 --cache-type-v q8_0也就是把 K 和 V 从 FP16 压到 8bit。这一步不会显著影响质量但能减少 KV Cache 的显存占用和带宽消耗。对于 decode 这种每个 token 都要读 KV 的计算模式带宽就是钱。同时我把上下文从默认的 8192 压到 4096。前面算过KV Cache 从约 3GB 降到约 1.5GB省下来的空间不仅让显存更宽裕也让每次解码时读 KV 的数据量变小。这一顿操作后在还没换框架前Transformers 单独跑也能摸到 40 tok/s 左右。不过这一步的收益在不同模型上会有差异我建议你动手时先用默认配置测一次再开 KV Cache 量化、缩上下文逐一对比。3. 第二刀换框架正确姿势比努力重要3.1 vLLM 与 llama.cpp 在 V100 上的真实表现接下来进入框架选型。市面上主流的推理框架有两个绕不开的选择vLLM 和 llama.cpp服务端形态是 llama-server也有很多人用 ollama 套壳。先说 vLLM。它是目前大模型服务化部署的标配支持 PagedAttention、continuous batching并发能力很强。但问题在于新版 vLLM 对 Volta 架构的支持越来越差很多 kernel 只针对 Ampere 和 Hopper 优化我在 V100 上直接跑最新版会遇到大量算子不支持或退化到慢速实现的情况。即使降级到旧版本能跑起来推理速度也谈不上惊艳。如果你确实想用 vLLM建议查清楚自己 CUDA 版本和显卡架构对应的兼容版本做好踩坑准备。llama.cpp 则相反。它本身面向消费级显卡和 CPU 运行对老架构兼容性好CUDA 后端至今仍然把 SM70 列入支持列表。虽然连续批处理能力不如 vLLM 那么强但胜在稳定、直接、可控。我最终的主方案就是 llama.cpp 的 llama-server 模式。3.2 llama.cpp 编译与启动参数解析先从编译开始。V100 是 compute capability 7.0编译时最好显式指定 CUDA 架构否则可能编出默认架构导致运行时掉到慢速兼容模式。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编译好之后把模型转成 GGUF 格式或者直接下载现成的 Q4_K_M 文件。然后启动服务./build/bin/llama-server \ -m ./models/qwen-27b-instruct-q4_k_m.gguf \ --host 0.0.0.0 \ --port 8080 \ -ngl 99 \ -c 4096 \ --flash-attn \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ -np 4 \ -b 1024几个参数逐个解释-ngl 99把尽可能多的层放到 GPU。如果显存不够可以降低这个数字但一旦有层留在 CPU速度会断崖式下降。-c 4096最大上下文长度和 KV Cache 显存直接挂钩。--flash-attn启用 flash attention对长上下文的 attention 加速和显存节省效果明显。--cache-type-k/q8_0KV Cache 量化前面已经说过。-np 44 个并行序列槽位配合 continuous batching 可以让多个请求同时解码而不必排队等待。-b 1024batch 大小影响 prefill 阶段的处理能力。启动后用简单请求测试curl http://localhost:8080/v1/chat/completions \ -H Content-Type: application/json \ -d {model:qwen,messages:[{role:user,content:用一句话介绍自己}],max_tokens:200}3.3 实测 48 tok/s 的现场配置这套配置跑下来单条流的 decode 速度稳定在 48 tok/s比 Transformers AWQ 又高了 60% 左右。提升主要来自三个方面一是 llama.cpp 的显存布局更紧凑权重格式和 kernel 配合得更紧密decode 时每次读取的数据量更少二是 flash attention 降低了 attention 部分的计算和带宽开销三是 CUDA 侧的 kernel 启动经过极简优化没有 Transformers 内部那么多抽象层浪费。这里要注意的是-np 4并不代表单条流会变快而是让服务能同时处理 4 路请求。对单个请求而言48 tok/s 已经是一个不错的水位但离 V100 的带宽天花板还有距离。4. 第三刀让每块卡算得值——并发与批处理4.1 单流瓶颈和并发增益先理解一个概念decode 阶段生成每个 token 都需要把模型权重从 HBM 显存搬到计算单元这是典型的“带宽密集型”任务。V100 的 HBM2 带宽 900GB/s而 Q4_K_M 量化后的 27B 模型权重大约 14.5GB所以理论上单流 decode 的极限是900GB/s ÷ 14.5GB ≈ 62 token/s你看到没64 tok/s 几乎就贴着这个理论值。这也是为什么调到最后单流速度很难再往上走因为不是算力不够而是内存带宽被吃满了。但这不代表 V100 没有余量。因为实际 decode 时多路请求的权重读取是可以重叠的。只要显存带宽还有富余连续批处理就能在不牺牲单条流太多速度的前提下把整体吞吐拉高。我后来用 4 路并发连续请求整体吞吐可以到 120 tok/s单条流依然能维持 60 左右。4.2 CUDA Graphs 和 Flash Attention 的实际意义很多人容易忽略小请求的开销。decode 阶段每一步计算量很小如果框架频繁 launch 几千个小 kernel光是 CPU 和 GPU 之间的调用开销就可能吃掉 20%-30% 性能。CUDA Graphs 的作用是把一串 kernel 调用打包成一次提交降低启动开销。llama.cpp 在较新版本中默认会使用类似技术优化重复执行路径这也是它比 Transformers 快的重要原因之一。Flash Attention 则把标准 attention 从“读取完整 KV、计算、写回”的多次显存访问压缩成一次高效计算尤其在长上下文场景下收益非常大。4.3 最终达到 64 tok/s 的完整服务配置最终稳定跑生产的配置在 3.2 小节的基础上还做了一处微调把-np从 4 调整到 2同时减少-b到 512。为什么要减因为某些外部监控脚本会周期性打请求进来-np 4时如果 4 个 slot 都被占满新请求就要排队反而影响体验改成 2 个 slot 后每个并发请求都能更快拿到计算资源单条流的速度反而稳定到 64 tok/s。最终配置如下./build/bin/llama-server \ -m ./models/qwen-27b-instruct-q4_k_m.gguf \ --host 0.0.0.0 \ --port 8080 \ -ngl 99 \ -c 4096 \ --flash-attn \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ -np 2 \ -b 512如果你只想快速验证当前配置下的生成速度可以写一段简单的 Python 脚本测量import requests import time url http://127.0.0.1:8080/v1/chat/completions payload { model: qwen, messages: [{role: user, content: 写一段两三百字的城市介绍}], max_tokens: 300, stream: False } start time.time() resp requests.post(url, jsonpayload) elapsed time.time() - start data resp.json() completion_tokens data[usage][completion_tokens] print(f生成 {completion_tokens} tokens耗时 {elapsed:.2f}s速度 {completion_tokens / elapsed:.1f} tok/s)实测这个配置下300 token 的生成大概 4.7 秒速度约 64 tok/s。5. 调优全程数据复盘5.1 四阶段成绩单我把整个调优过程整理成一张表方便你对着看每一步的收益来源阶段方案显存占用单流速度说明初始状态Transformers FP16 device_map auto32GB 不够部分权重在 CPU4 tok/s权重跨 PCIE 搬运速度被卡死第一轮Transformers AWQ 4bit约 15GB28 tok/s权重全部进显存性能立刻起飞第二轮llama.cpp Q4_K_M Flash Attention约 16GB48 tok/s框架和 kernel 优化进一步榨干带宽第三轮KV Cache 量化 上下文控制 并发调整约 15.5GB64 tok/s贴近 V100 单流带宽上限5.2 为什么 64 以后再也上不去直接说结论V100 的 900GB/s 显存带宽对 14.5GB 的 4bit 模型权重单流 decode 的物理上限就是 62 左右。我测到 64 tok/s已经算是贴着天花板走了。想继续往上只剩几条路。一是换更激进的量化格式比如 3bit 或更低 bit权重体积降到 10GB 左右理论上能到 80-90 tok/s但质量损失要自己评估二是换卡A100、H100 的显存带宽是 V100 的 2-3 倍速度自然上去了三是用多卡并行不过 27B 拆到多卡时通信开销不小单机单卡反而是收益最高的方案。5.3 部署后稳定运行的效果整套配置跑了一阵子稳定性不错。首 token 延迟在几百毫秒量级连续多轮对话没有明显劣化4 个 slot 并发时也不会出现某个请求被饿死的情况。这个性能水平适合什么场景个人知识库问答、内部工具嵌入、中低并发的 API 服务都够用。如果要做高并发的线上产品V100 显然不是最优解但对个人研究和中小团队来说这已经是一个“花小钱办大事”的可行路径。6. 常见坑与排查手册6.1 显存 OOM 怎么排查如果你不是 32GB 版本而是 V100 16GB那么跑 27B Q4_K_M 会非常紧张。模型权重 14.5GB加上 KV Cache 和激活值很容易直接 OOM。排查顺序建议是先确认没有其他进程占显存用nvidia-smi看一下然后把-c从 4096 降到 2048KV Cache 占用直接减半再把--cache-type-k/v q8_0打开又省一截。如果还是 OOM就把-ngl往下调比如-ngl 80让部分层留在 CPU。但你得做好心理准备速度会明显下降因为 CPU 和 GPU 之间要来回搬权重。6.2 速度忽高忽低的元凶有一阵我测试时发现速度在 30 到 60 之间反复横跳排查了很久原因居然有三个一是后台有另一个 Python 进程周期性地加载数据把显存占了一部分导致模型层被挤出一部分二是 GPU 温度过高触发降频V100 被动散热版本尤其明显三是 CPU 侧喂数据跟不上虽然权重在 GPU 上但 tokenizer 和采样部分还是在 CPU如果 CPU 主频不稳也会拖慢整体节奏。建议用nvidia-smi dmon -s puc实时看 GPU 利用率、算力频率和温度再对照业务日志做判断。6.3 V100 16GB 版本怎么救场如果你是 16GB 的 V100也不是完全没戏但要把期望放低。我的建议方案是用更小的量化比如 IQ3_XS 或者 Q3_K_M权重体积大约 11-12GB留出 4GB 给 KV Cache 和激活值上下文限制到 2048 或 1024关闭所有不必要的特性只保留--flash-attn和 KV Cache 量化。这个配置下 27B 模型还是能跑的速度大约 40-50 tok/s只是长上下文基本告别了。如果连 16GB 都跑不动那就只能考虑换一个更小的基座模型比如 7B 或 14B。V100 跑 7B Q4 量化单流速度可以轻松上 100 tok/s也是一种非常实际的选择。最后再分享一个体会很多人一听到 V100 就觉得“老卡跑不动大模型”但实际调下来它最大的价值在于带宽并不差。27B 模型 4bit 量化后正好卡在带宽甜蜜点64 tok/s 几乎就是这个架构的物理极限。如果你也想在这类老卡上做部署别急着怀疑硬件先看模型有没有完整放进显存再看框架是不是针对 V100 优化过。把这两件事做好性能翻几倍并不是什么难事。