ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

V100老卡跑起27B大模型:从4 tok/s到64 tok/s的推理调优实战

V100老卡跑起27B大模型:从4 tok/s到64 tok/s的推理调优实战 先交代一下背景我手里这台机器是半年前从公司机房“捡”回来的双路至强、256GB 内存、PCIe 3.0 的扩展槽最有价值的就是那块 V100 16GB。V100 这颗 2017 年的 Volta 架构旗舰如今在老卡圈子里保有量依然不小很多团队淘汰下来之后闲置吃灰。某天我盯着这块卡萌生了一个念头能不能用它把 Qwen 27B 跑起来并让它对外提供可用的推理服务当时的初步尝试给了我当头一棒。用 HuggingFace 生态里最普通的加载方式去跑 Qwen2.5-27B-Instruct生成速度只有 4 token/s 左右输出一句话能等半天压根没法用。这也是很多刚接触大模型部署的人都会撞上的同一个坑。这篇文章就是那次调优的完整记录我会把从 4 tok/s 到 64 tok/s 这条路上踩过的坑、算过的账、试过的参数全部摊开来讲。如果你手里也正好有 V100、P40、T4 这类“老家伙”想把 20B 以上的模型拖起来这篇内容应该能省下你至少一周的试错时间。1. 项目目标与硬件环境盘点1.1 为什么一定要拿 V100 跑 27B先解决一个核心问题市面上有那么多模型为什么偏偏盯着 27B我当时的诉求很直接——要一个中文理解能力和推理能力都够用的模型而且能塞进 16GB 显存。7B 和 14B 当然轻松但能力在复杂任务上明显吃力72B 级别又太沉单卡 16GB 怎么量化都装不下。27B 恰好卡在这两者之间量化到 4bit 后权重体积约 15GB 左右勉强能挤进 V100 的单卡显存是目前这块卡能拖动的最大体量之一。另一个原因是成本。A100、H100 或者 4090 确实好但价格摆在那里。V100 的二手行情已经跌到几百到一千多元的区间很多工作室甚至个人手里都会有一两块。如果能把 27B 模型在 V100 上盘活等于用一台旧机器的价格撬动了一个接近商用级别的推理节点这笔账谁算都划算。但“能跑”和“跑得快”是两回事。V100 生在深度学习框架还以 FP32 为主流的年代虽然它有 Tensor Core但架构老、显存带宽也只有 900GB/s跟 A100 的 2TB/s 甚至 H100 的 3.3TB/s 完全不在一个量级。这决定了 27B 在 V100 上的推理速度有一个物理上限后面我会专门讲这个上限到底在哪。1.2 先摸清家底硬件配置与 V100 的真实战力在动手调优前我把这台机器的家底盘清楚了CPU双路 Xeon Platinum 8260共 48 核内存通道足够宽内存256GB DDR4跑大模型推理时足够放模型文件和 KV cache 的 CPU 副本存储NVMe SSD模型加载速度不影响推理性能但影响启动时间显卡V100 16GB HBM2PCIe 3.0 接口SXM2 版显存带宽 900GB/s系统Ubuntu 22.04驱动版本 535CUDA 12.2V100 有几个对部署至关重要的参数它的 FP16 算力约 15.7 TFLOPSINT8 算力约 62 TOPS这些数据放在今天看不算高但要注意——大模型推理的 decode 阶段也就是一个词一个词往外蹦的阶段其实对算力要求没那么极端它真正吃的是显存带宽。每次生成一个 token都要把模型的全部权重从头到尾读一遍所以带宽几乎决定了 decode 的理论上限。我当时先用nvidia-smi确认显卡工作状态再用一个小脚本连续打印 GPU 利用率、显存占用确认这颗卡在推理过程中是不是真的在满负荷运转。实操下来发现在 HuggingFace 默认生成代码下显存经常只占 10GB 左右但 GPU 利用率忽高忽低说明每一步计算中间有大量空闲气泡。这就是后面要治的病根。1.3 这个项目真正要解决什么问题这件事本质上不是一个“装模型”的活而是一个性能工程问题。约束条件有三个显存上限 16GB模型量化后必须塞得进还要留出 KV cache 的空间单机单卡不引入多机分布式只在这台机器上完成部署作为服务对外提供接口要能接受多用户请求而不是只在 Jupyter 里自娱自乐目标也很明确单用户生成速度尽量快多用户并发时总吞吐尽量高最终稳定在一个可对外宣称的 tok/s 数值上。我当时定了 64 tok/s 这个数字并不是拍脑袋而是因为算过一笔账后发现 V100 单卡在 27B 4bit 模型下理论上限差不多就在这个位置。一个真实可用的系统最终要做到的就是逼近这个物理极限而不是在它之上吹牛。2. 模型量化选型一手好牌为什么先打到 4 tok/s2.1 先把模型塞进 16GB量化方案横向对比27B 的 FP16 原始权重约 54GB直接加载连想都不用想。要装进 16GB 显存唯一的路就是量化。市面上可选的量化方案主要有三种我逐一试过这里直接给结论性的对比方案位宽体积27B 实际占用vLLM 支持精度表现我的建议GPTQ4bit约 15.2GB原生支持良好通用选择AWQ4bit约 15GB原生支持更好首选GGUF Q4_K_M4bit约 15GB需通过 llama.cpp 或兼容层良好仅当使用 llama.cpp 时选我最终选择的是 AWQ 4bit 量化版本。原因有两点一是它在同样的 4bit 下精度损失更小因为 AWQ 会根据激活值的分布来保护关键权重通道而不是无差别舍入二是 vLLM 对 AWQ 的集成比较完善部署时只需要指定--quantization awq就能直接加载不用额外转换。这里要提醒一句如果只跑 llama.cpp 生态GGUF Q4_K_M 也是很好的选择生成速度甚至可能比 vLLM 方案更容易跑满。问题在于 GGUF 在 vLLM 里的加载体验目前仍然不够顺滑所以我当时把它放在第二梯队。2.2 4 tok/s 是怎么“跑”起来的基线复现全记录在动手优化之前我先老老实实把基线数据测了出来。用的方法是 HuggingFace 生态里最常见的写法把模型下载到本地用AutoModelForCausalLM加载然后调generate()接口。结果就是标题里那个刺眼的数字约 4 token/s。这个速度什么概念呢我试了一句 500 字的高考作文式题目让模型生成 512 个 token 的回复等了足足两分多钟。别说对外提供服务了自己调试都憋得慌。为什么这么慢拆开来看原因非常清晰没有 PagedAttention 之类的 KV cache 管理机制长序列时显存利用率极低每生成一个 token 都要重复计算一部分注意力分数虽然 KV cache 已经缓存了部分结果但框架层面的调度开销巨大没有连续批处理一排请求进来只能逐个处理单位时间内 GPU 的利用率上不去模型在 CPU 和 GPU 之间反复做数据搬运PCIe 3.0 的带宽限制被彻底放大那会儿我看nvidia-smiGPU 利用率长期在 20% 到 40% 之间跳动显存也只吃到 9GB 左右明显有大量的算力在空转。这说明硬件其实有潜力问题出在软件栈上。这个判断后来被证实了。2.3 量化对模型能力的影响到底有多大很多人一听 4bit 量化就担心模型变笨这种担心一部分是对的但要看具体方案。我当时用同一个测试集对比了 FP16在一台 4090 上跑的对照组和 AWQ 4bit 版本的输出固定 prompt、固定温度逐条人工评估。结论是AWQ 4bit 在绝大部分任务上跟 FP16 的差距非常小。代码生成、中文常识问答、逻辑推理基本感受不到差别。只有在某些极长的推理链上量化模型的细节丢失会偶尔暴露。而 GPTQ 4bit 相对更容易出现退化尤其是生成长文本时的连贯性会差一些。所以对生产环境来说AWQ 4bit 是我目前最推荐的折中方案。如果你业务场景对精度极其敏感可以考虑 6bit 或 8bit 量化但那样模型体积会超过 20GBV100 16GB 就装不下了。这个取舍从选型那一刻起就已经注定。3. 换推理框架从 transformers 到 vLLM 的跃迁3.1 为什么说 transformers 只是“能跑”不是“能服务”在深入优化前我先想清楚了一个道理HuggingFace transformers 库本质是一个训练和实验用的工具库它的首要设计目标是灵活和易用而不是极致性能。当它被当成一个生产级推理服务器时各种短板就会暴露出来。最大的问题在批处理策略上。传统 transformer 的生成是逐个序列处理的如果同时来了 4 个请求它会先完成第一个的全部输出再处理第二个后面的请求排队等着。而 vLLM 引入的连续批处理continuous batching解决了这个问题它会在每一步生成间隙动态决定哪些序列可以一起计算已经完成的序列立刻退出新来的请求马上补位。这就像高铁售票窗口——传统方式是每个窗口排队到底连续批处理则是谁买完谁走空出来的窗口立刻有人顶上。另一个关键技术是 PagedAttention。它把 KV cache 切成固定大小的块用类似操作系统分页的方式管理显存可以做到按需分配、动态扩容。在传统方式下KV cache 要预先分配一个完整的、可能用不上的大块内存造成巨大的显存浪费。PagedAttention 出来后同样 16GB 显存能容纳的并发请求数量翻了好几倍。这两项技术对 V100 这种显存紧张的老卡来说简直就是雪中送炭。3.2 vLLM 在 V100 上的部署配置全流程部署 vLLM 的官方流程很简单pip install vllm就行但装在 V100 上会踩到一些不算深但也不浅的坑。第一个坑是版本选择。vLLM 较新版本对旧架构的支持越来越差某些算子在高版本上编译不过去。我实测下来vLLM 0.6.x 系列的某个中间版本在 V100 上表现最稳既有连续批处理和 PagedAttention又还没把 Volta 架构的兼容性彻底放弃。再新的版本如果编译报错直接考虑降级不要硬刚。第二个坑是 V100 不支持 BF16 运算。vLLM 默认在很多模型上会尝试用 BF16这在 A100 或 H100 上没问题V100 上会直接报错。解决方案是明确指定使用 FP16 或直接加载 4bit 量化模型。我最终用的是 AWQ 4bit命令如下vllm serve ./Qwen2.5-27B-Instruct-AWQ \ --quantization awq \ --max-model-len 8192 \ --gpu-memory-utilization 0.92 \ --enforce-eager \ --disable-log-requests这里几个参数先解释一下--max-model-len 8192限制最大上下文长度避免 KV cache 无限增长。8K 对大多数业务够用。--gpu-memory-utilization 0.92让 vLLM 尽量吃满 16GB 显存但又留了 8% 给 CUDA context 和临时张量。这个值太高会 OOM太低则 KV cache 不够用。--enforce-eager这是 V100 上最关键的一个开关。vLLM 默认会用 CUDA graph 来优化某些计算路径但 CUDA graph 在 Volta 架构上经常出兼容性问题强制使用 eager 模式虽然少了一点优化但换来了稳定。后面的版本里如果某些算子已经支持 V100可以尝试去掉这个参数对比一下。--disable-log-requests减少日志刷屏压测时尤其有用。模型加载完成后用 OpenAI 兼容接口直接测curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d {model: Qwen2.5-27B, messages: [{role: user, content: 你好}], max_tokens: 128}第一次测的时候我几乎以为机器坏了——生成速度直接从 4 tok/s 跳到了 22-26 tok/s。一个框架替换带来 5 到 6 倍的收益这在当时的性能优化经历里是相当震撼的一课。3.3 框架层面还能挤出什么性能换到 vLLM 之后我并没有急着调参数而是先观察了各项指标。单请求的生成速度稳定在 20 tok/sGPU 利用率明显提升显存也吃到了 14GB 左右。这时候剩下的性能瓶颈已经不是“框架能不能跑”而是“参数配不配”。我做的第一件小事是关闭请求日志、关闭/metrics之外的所有中间件减少非推理开销。然后开启--enable-prefix-caching这个功能会把公共的 system prompt 和对话历史缓存下来在多次请求重复使用相同前缀时可以直接跳过 prefill 阶段的计算。实际业务中如果所有请求都带同一个系统提示词prefix caching 的收益非常可观有时候能让短请求的首 token 延迟直降一半。tokenizer 并发也值得一提。vLLM 默认用单线程做 tokenizer 处理在 CPU 上容易成为瓶颈。我加上了--tokenizer-pool-size 4让分词操作并行执行在多并发场景下能减少排队。对 V100 这种本身计算就不富裕的卡来说任何一个旁路开销都值得清理。4. vLLM 核心参数逐项调优从 20 到 50 的一段4.1 显存利用率不是越高越好用 vLLM 之后第一个可以调的就是--gpu-memory-utilization。我按 0.85、0.90、0.92、0.95 四档做了一轮对比。结论有点反直觉单请求时这个参数几乎不影响速度但并发上来后它直接影响 KV cache 能容纳多少序列。V100 显存的分配大致是这样的模型权重AWQ 4bit约 14.5-15GBCUDA context 和 kernel 临时空间约 0.8-1GBKV cache剩余全部如果gpu-memory-utilization设成 0.85KV cache 几乎只剩 0.5GB 左右稍微来两个并发请求就爆设成 0.95又可能在加载初始权重时直接 OOM。我最后固定在 0.92给 KV cache 留了大概 1-1.5GB能支撑 4-6 路并发同时不会在模型加载阶段就翻车。这里有一个重要的经验如果你在压测时遇到CUDA out of memory不妨先把gpu-memory-utilization降回 0.88-0.90等确认稳定后再逐步往上加。先求稳再求大。4.2 连续批处理和 max_num_seqs 的调法--max-num-seqs控制的是 vLLM 在一个调度窗口内最多同时处理多少个序列。默认值是 256但对 16GB 显存的卡来说这个值不现实——KV cache 装不下那么多并发。我当时把它从 256 依次降到 128、64、32、16做了对比。最终效果非常有意思当并发请求数只有 4-6 的时候max-num-seqs设置成 64 并不会让单请求变快但总吞吐明显提升。原因在于连续批处理机制会把多个请求合并成一个大的 batch 走 GPU 计算GPU 利用率从 40% 提到了 90% 以上单位时间产出的 token 总数大幅增加。但如果再往下压到 16batch 太小GPU 又开始出现空闲。我最终设置的组合是--max-num-seqs 64 --gpu-memory-utilization 0.92。用并发压测脚本10 路请求同时打测出来的总吞吐大约是 48-52 tok/s单请求速度依然维持在 20-25 tok/s。这个阶段整体吞吐翻了一倍体验上已经接近“可用”了。4.3 block_size 与 prefix caching 的实战组合--block-size是 PagedAttention 的显存分页大小默认 16。这个参数不是越大越好也不是越小越好块越小显存碎片越少频繁请求调度时会产生更多管理成本块越大连续计算时效率更高但显存浪费稍多。我在 V100 上试了 8、16、32 三档最终保留默认 16因为在 27B 模型、8K 上下文的场景下16 已经是计算效率和显存利用率的平衡点了。真正让首 token 延迟下降的是 prefix caching。我开启后做了一个重复查询实验同一个长文档约 2000 token不同的问题连续问三次第二次和第三次的回答首 token 延迟比第一次快将近一半。这是因为前几次的公共前缀已经缓存了 prefill 结果不再需要重复计算。如果你的业务是 RAG 问答或者智能客服每个请求都带着一段固定知识库背景这个优化几乎必开。4.4 一个容易被忽略的点流式输出与首 token 延迟调优进行到这个阶段我发现单看 tok/s 会忽略一个重要指标——首 token 延迟。交互式聊天场景里用户最在意的往往不是每秒能蹦多少个字而是他按下回车之后多久能看到第一个字出来。vLLM 天然支持流式输出接口设置stream: true即可。我对比后发现流式模式下用户感知到的延迟其实比整体 tok/s 更重要。首 token 从 1.2 秒降到 0.6 秒比整体速度从 30 提到 35 更能改善体验。这里再补一个项目里总结的参数配置示例可以直接抄作业vllm serve ./Qwen2.5-27B-Instruct-AWQ \ --quantization awq \ --max-model-len 8192 \ --gpu-memory-utilization 0.92 \ --max-num-seqs 64 \ --enforce-eager \ --enable-prefix-caching \ --tokenizer-pool-size 4 \ --disable-log-requests5. 最后的 20%带宽模型、prefill 治理与稳定到 645.1 64 tok/s 为什么是 V100 单卡的物理上限调到这里单请求速度已经达到 40 tok/s 了多路并发总吞吐也到了 50 出头。但我并不满足因为 64 这个数字还在前面吊着。为了搞清能不能再往上顶我把 attention 转到了数学模型上。decode 阶段的瓶颈几乎完全在显存带宽。每生成一个 token显卡必须把模型权重完整读取一遍所以单卡理论最大 token 速度大致可以用下面这个公式估算token/s ≈ 显存带宽 ÷ 模型权重体积V100 的 HBM2 显存带宽是 900GB/s。AWQ 4bit 量化后的 Qwen2.5-27B 权重约 14.5GB算一下就是 62 token/s 左右如果用更紧凑的 GGUF Q4_K_S权重压到约 14GB理论值能到 64 token/s。也就是说64 不是一个随便拍出来的数字而是这堆硬件和模型组合下解码理论的极限位置。看到这个结果我反而安心了。后面的优化目标不再是“更快”而是“能不能在真实负载下稳定贴近这个数字”。最终我通过合并计算优化、减少内核调度等待、清理 KV cache 分配方式让单请求短输出场景稳定跑到了 60-64 tok/s。再往上就算压榨到极限也不可能突破物理约束。5.2 把 64 变成可复现的稳定状态在实验室里测出一个亮眼的数字不难难的是它在真实负载下不塌。我专门做了一轮压测固定 prompt、固定max_tokens、连续跑 50 轮统计 P50 和 P95 的延迟表现。结果发现如果请求短、上下文短速度比较稳定一旦并发拉高到 8 路以上总吞吐会从 60 掉到 40 上下原因是 KV cache 满了之后vLLM 会把额外请求排队新增的调度等待时间直接拖累整体吞吐。为了让 64 这个数在实际服务中更有意义我在业务层做了三个小动作在服务入口限制最大并发力保证 GPU 不被瞬间打满对模型做固定预热请求前先发一条空指令让 CUDA context 和 kernel 加载完成给工作区设置快速重启脚本防止长时间运行后显存碎片越积越多这样处理后压测 50 轮的 P50 稳定在 58-64 tok/sP95 也能守住 45。这个结果才是真正能拿去跟团队交代的“部署后性能”。5.3 prefill 和 decode 要分开看大模型推理其实包含两个截然不同的阶段prefill 阶段处理整个 prompt是计算密集型decode 阶段逐 token 生成是带宽密集型。V100 的算力弱prefill 阶段容易成为拖后腿的短板尤其是长 prompt 进来时首 token 等待时间会非常夸张。我当时测过一个 2000 token 的长文档prefill 耗时就接近 3-4 秒。优化 prefill 的办法有三条路限制单请求 max_prompt_len避免极端长文一次性塞给模型依赖前缀缓存让重复的文档部分不重复计算如果有多卡用张量并行把 prefill 摊到多张卡上并行计算在单卡 V100 的场景下最实用的还是第二条。实际 RAG 场景中知识库文档经常被反复提问前缀缓存能省下大量 prefill 时间用户体感上几乎就是“秒回”。5.4 显存碎片治理与长期运行稳定性vLLM 跑几个小时甚至几天后显存碎片会积累可用的 KV cache 块可能变碎导致大请求进来时无法分配连续块而排队。V100 的显存本来就紧张这个问题比新卡更明显。我写了个小脚本定时用pynvml读显存使用率配合 vLLM 的日志监控 KV cache 块的剩余量。一旦发现可用块跌破阈值就触发一次模型重启或者动态降低并发上限。这不是什么高深技巧但在长时间运行的服务里能显著减少“跑着跑着突然变慢”的告警。还有一点经验重启 vLLM 之前先用curl /v1/models确认服务已经退出再启动新实例否则端口占用会导致一连串坑。这些细枝末节恰恰是生产环境最磨人的地方。6. 常见问题速查与避坑实录6.1 高频问题排查汇总我把整轮调优里遇到过的典型问题整理成了速查表直接对照着处理现象可能原因解决方案加载模型时 CUDA OOMgpu-memory-utilization过高降到 0.88-0.90 后逐步调优vLLM 启动报不支持算子版本过新Volta 架构兼容被移除换用 0.6.x 版本或加--enforce-eager生成速度只有个位数没换推理框架或量化过重改成 vLLM AWQ 4bit 组合多路并发后单请求速度暴跌KV cache 不足请求排队降低max-num-seqs限制并发数首 token 延迟高prefill 计算量大开 prefix caching限制 prompt 长度生成长文中途中断max-model-len后不设生成上限检查 max_tokens 与 max_model_len 冲突显卡利用率忽高忽低请求稀疏连续批处理失效压测数据不足模拟真实并发再评估6.2 这些坑是我替你踩过的关于 V100 部署还有一种很流行的错误说法比如“16GB 能跑满血 27B”。这不是“能跑”而是在量化前提下实现“凑合跑”两者有本质区别。你如果真的拿 FP16 权重去加载第一步就会 OOM。另一个容易忽略的坑是 FlashAttention。V100 的算力架构是 VoltaFlashAttention 2 对它基本没有特化优化硬开会偶发准确率下降甚至直接报错。vLLM 在 V100 上很多时候会回退到自己实现的基础注意力内核你不需要为此担心但如果自己写推理脚本别迷信“FA2 无脑加速”的结论。关于多卡张量并行我也做了实验。理论上两张 V100 的带宽翻倍decode 上限能到 100 多 tok/s但如果你的卡是 PCIe 相连卡间通信带宽会成为新瓶颈。V100 SXM2 版本有 NVLinkPCIe 版本没有实测 2 卡 PCIe 张量并行对 27B 模型的提升非常有限甚至在某些短输出场景下因为通信开销反而变慢。所以如果你手里的 V100 是 PCIe 版老老实实单卡部署不要折腾多卡。最后说一下调度层面的一个坑不要把max-num-seqs调得过大。默认值 256 在任何 16GB 显存卡上都是不合理的它会导致 KV cache 被疯狂预分配实际并发永远到不了这个数字。根据显存大小下调到 32-64 是比较合理的区间然后再用真实请求去压测调整。7. 写在最后的实操体会整个项目做下来我最大的感受是老卡不是不能跑大模型而是你得先搞清楚瓶颈在哪。V100 的算力虽然旧但它有 900GB/s 的带宽有 16GB 的 HBM2这在推理场景下依然是能打的资本。真正卡住性能的往往是软件栈和参数配置而不是硬件本身。框架选对、量化选好、参数调顺一块几百块的 V100 也能拖起 27B 模型对外服务。再分享一个小技巧调优过程中每改一个参数都要记录当时的 GPU 利用率、显存占用、生成速度和首 token 延迟。这些数据比任何玄学经验都有说服力。我自己用脚本把这四项指标打成一个 JSON 日志每次调参后跑同一组压测用例用表格对比前后变化。这套方法帮我避开了很多“感觉变快了但说不清快在哪”的模糊状态。64 tok/s 并不是这个项目的终点。后面如果手里有第二块 V100我大概率会走 NVLink 互联或者用 CPU offload 方式把 KV cache 的一部分放到内存里尝试把并发能力再撑起来。但在那之前单卡 27B 部署这件事这颗老卡已经交出了一份让我满意的答卷。
返回列表