vLLM高吞吐推理引擎:本地高并发服务部署与调优实践
提到 vLLM 高吞吐推理引擎很多人第一反应是“显存管理很牛”。但我在本地搭过高并发推理服务之后最大的感受其实是另一件事把单机吞吐做上去不只是显存的问题更是排队策略和批处理策略的问题。这篇文章就围绕标题里的“本地高并发推理服务”展开聊一聊 vLLM 解决的核心问题、部署该踩的坑以及我实际压测时总结出来的调优经验。vLLM 这个开源项目适合谁用我的判断是已经在本地或内网环境部署了开源大模型但对并发能力不满意想把几十个甚至上百个请求同时跑起来并且希望接口能稳定兼容常见应用的人。如果你只是临时跑一两个演示脚本可能不需要它但凡是面向多个用户、多个业务方开放推理能力的场景vLLM 几乎是用最少改动换来最大吞吐的选择。接下来我不会把 vLLM 的所有源码细节都铺开而是从“高并发推理服务”这个结果出发反推它背后的设计逻辑再给出一套能直接拿去用的部署方案和排查手册。1. 项目出发点为什么需要本地高并发推理服务1.1 从一次真实压测说起我们先聊一个场景某个内部平台需要为多个团队提供模型推理能力客户端侧一拨并发请求会持续打到同一张显卡上——比如 30 路请求几乎同时进来。如果按传统的方式逐个推理每个请求平均耗时 2 秒那 30 个请求串行下来要 1 分钟前端的超时时间根本撑不住。如果强行用多线程并发显存里每个请求都要持有一份完整的 KV Cache很快就把显存打爆OOM 之后服务直接崩掉。我第一次在本地压 vLLM 的时候用的是一张 24G 显存的卡模型是 70 亿参数左右的开源模型上下文长度卡在 4096。同样的硬件常规推理框架在 30 并发下吞吐大概只有每秒 20 个请求左右换成 vLLM 之后压测数据直接提升到每秒 60 个请求以上而且还留出了显存余量。这里面的差距不完全来自算子优化更多来自 vLLM 对显存和请求调度方式的重构。这是我决定深入用它做本地高并发推理服务的直接原因。1.2 传统推理服务在并发下的瓶颈要理解 vLLM 的价值得先看不做这些优化时会发生什么。传统推理服务主要有三个瓶颈。第一静态批处理框架会等一批请求攒够数量才一起推理第一个请求和最后一个请求延迟差异非常大吞吐曲线像过山车。第二显存碎片化每个请求的 KV Cache 预分配一个固定大小不管这个请求实际生成多少 token都会占据固定显存请求多了之后显存里到处都是“用不掉也腾不开”的空洞。第三串行 / 简单并行如果同一时间只有一个请求在算GPU 的算力利用率特别低尤其是输入很短、输出很长的时候大部分计算单元都在空转。这三个瓶颈加起来导致一个很常见的现象硬件看起来很强但实际服务能力只有理论值的零头。这也解释了为什么很多团队换了更大显存的卡并发上不去问题依旧。因为问题往往不在算力而在显存管理和调度算法。1.3 vLLM 高吞吐推理引擎解决了什么问题vLLM 的定位是“高吞吐推理引擎”它的核心逻辑是把 GPU 当成一个有上限的共享资源池每次请求不再独占一整段显存空间而是由调度器统一分配、按需持有。请求进来之后调度器会动态决定谁先用、谁排在后面尽可能让同一批次里的不同请求处于不同生成阶段。这样一来单个请求的响应时间可能不会比专用框架快太多但整体吞吐量会有非常明显的提升。对于本地服务这种“同时服务多个业务方”的场景吞吐比单请求首 token 延迟往往更关键。因为用户感受到的是系统整体的稳定性而不是某一个请求的快慢。vLLM 高吞吐推理引擎解决的正是“并发请求多时系统还能稳定服务、GPU 还能吃饱”的问题。2. 高吞吐的核心原理PagedAttention 与动态批处理2.1 什么是 KV Cache为什么它决定并发上限大模型在生成 token 的时候每往前推进一步都要用到前面所有 token 的 Key 和 Value 向量。这些向量按序列长度和注意力头数计算后需要临时放在显存里称为 KV Cache。序列越长、batch 越大KV Cache 占用就越大。它的大小直接影响并发上限——显存还剩多少就决定还能塞进多少请求。很多框架的做法是预先分配一个最大长度对应的 KV Cache 块比如限制 max_length4096那么每个请求一来就预留 4096 的显存。可实际请求平均生成长度可能只有几百 token预留的部分大量浪费。本地高并发推理服务最怕的就是这种浪费每个请求都“抢大鱼塘”结果池子很快被占满剩余请求只能排队。2.2 PagedAttention 如何减少显存浪费vLLM 用的是 PagedAttention思路借鉴操作系统的虚拟内存分页。它不再给每个请求一次性分配完整连续的 KV Cache而是把显存划分成固定大小的块按需给请求分配不够了再加块。这样显存里几乎不会出现“大块预留却只用了五分之一”的情况空闲块可以被其他请求复用。用生活化的类比传统方案就像在超市租一个固定大小的储物柜无论你最后放多少东西都占一整格PagedAttention 则是按车票行李架的逻辑能放多少放多少放不下再找下一个空位。差别看起来不大但高并发下每个请求节省几百 MB 显存几十个请求之下释放出的空间就是非常可观的容量直接体现在并发上限上。2.3 Continuous Batching 如何把请求“塞满”GPUPagedAttention 解决的是显存空间问题Continuous Batching 解决的则是时间片问题。传统框架是静态批处理要等一个 batch 全部生成完再换下一批批与批之间有空隙而且批次里的请求如果输出长度差别大短请求要等长请求全部跑完才算结束导致 GPU 空闲等待。Continuous Batching 的思路是“边跑边进”在每个解码步骤结束之后允许已经完成的请求退出批次同时把排队的请求加入批次这样 GPU 在每个时刻都在处理一批尽可能“满”的工作量。vLLM 的调度器会持续追踪每个请求的状态。新的请求进来只要显存剩余空间允许就能立刻进入当前迭代已经生成完的请求立刻释放资源不会拖住整个批次。对于本地高并发服务来说这种“动态进出”的模型特别适合流量不恒定、请求长短混合的实际场景。2.4 其他配合机制前缀缓存与投机解码除了上面两个核心机制vLLM 还提供了几个对高并发服务很有用的配合功能。前缀缓存Prefix Caching会缓存相同前缀请求的 KV Cache。例如很多应用会携带一大段 system prompt如果多路请求的 system prompt 一样vLLM 能直接复用已缓存的前缀计算结果减少重复计算显著降低首 token 延迟和显存占用。投机解码Speculative Decoding则是在不改变输出的前提下用小模型先草拟多个 token再由大模型一次验证等于用额外的计算换更大吞吐适合对延迟敏感的服务。这两个功能并不总是默认开启需要根据模型和请求情况测试后再决定。另外vLLM 还支持Chunked Prefill用于把较长的输入切块处理避免一个超长输入独占 GPU导致其他流式请求长时间等待。对于高并发服务这些机制都必须按业务特点组合使用。3. 本地高并发推理服务的部署与配置3.1 安装环境从依赖到启动部署前先把环境理清。vLLM 对硬件有要求主要依赖 CUDA 和对应的显卡驱动主流的 N 卡基本都能跑但不同架构的显卡在优化程度上差别很大。安装方式很简单建议直接用 Python 包管理器安装预编译版本省去编译时间。如果经常需要改源码或调试再考虑从源码编译。一个比较稳的安装流程是先用 conda 建一个独立环境Python 3.10 或 3.11安装 CUDA 工具链对应的深度学习框架再安装 vLLM 包。注意 vLLM 和底层框架的版本有对应关系不要随意组合否则容易在导入阶段遇到符号缺失的报错。装完之后可以用一条命令把某个开源模型加载起来python -m vllm.entrypoints.openai.api_server \ --model /path/to/local/model \ --port 8000 \ --gpu-memory-utilization 0.9如果模型不在本地也可以直接填公共模型仓库里的模型标识但我更建议先下载到本地避免服务启动时网络波动影响稳定性。第一次启动会进行模型权重读取和图编译耗时比较久之后再次启动会有缓存速度快很多。3.2 关键启动参数并发、显存与序列长度怎么配启动参数是本地高并发推理服务调优的重头戏。我用过的最关键的参数是这几个--gpu-memory-utilization控制 vLLM 最多使用多少比例显存。并不是越高越好因为显卡驱动本身也要占少量显存一般设为 0.85~0.95具体看显存总容量和模型大小。--max-model-len单个请求允许的最大上下文长度。设得越大KV Cache 预留越保守并发上限越低设得太小长文本请求会被截断。需要根据业务的实际输入长度取一个平衡值。--max-num-seqs最大同时处理的序列数限定了并发窗口。常见误区是把这个参数调得特别大实际上它会受到显存和模型长度的约束设太大只会让请求排队等待内存并不一定提升吞吐。--tensor-parallel-size张量并行度多卡时使用。如果是单卡不要设置多卡时一般设置为卡数但也要先测试通信开销。这些参数之间的关系是联动的模型权重占一部分显存剩余显存扣掉 KV Cache 后再除以每个请求的最大 KV Cache 占用量就决定了能并发的请求数。在实际配置时我习惯先固定一个合理的 max-model-len再根据监控显存占用逐步上调 max-num-seqs直到吞吐不再提升或出现 OOM。3.3 使用标准 Chat Completions 接口接入业务vLLM 自带一套兼容主流大模型平台接口的 HTTP 服务这是它比较容易落地的原因之一。启动之后客户端不需要修改太多逻辑直接把 base_url 指向本地地址即可。常用的请求方式是流式输出。对于高并发场景流式响应能让用户看到 token 逐渐生成避免长时间静默。我遇到过把流式关掉的团队结果 30 秒都等不到完整响应前端不断报超时改成流式之后体验立刻改善因为首 token 一般在几百毫秒内就会到来。vLLM 的接口还支持补全和对话两种格式根据自己的业务选一个。接入时要注意设置合理的超时时间。本地高并发下如果排队太长个别请求会明显变慢把超时设太短会导致请求被前端反复重试反而增加服务端压力。一般建议把全局超时放到 1~2 分钟并配合重试机制与幂等请求设计。3.4 吞吐压测怎么测出真实性能部署完成之后不能只拿几个脚本请求验证“能通”就结束一定要做压力测试。我用的方式是用脚本并发发送固定数量的请求统计整体完成时间。比如 1000 个请求、并发 30记录响应码、首 token 延迟、完整耗时。比较简单的脚本逻辑大致是这样的import asyncio import aiohttp async def send_one(session, url, payload): async with session.post(url, jsonpayload) as resp: return await resp.text() async def main(): url http://127.0.0.1:8000/v1/chat/completions tasks [] async with aiohttp.ClientSession() as session: for i in range(1000): payload { model: local-model, messages: [{role: user, content: 写一段关于高并发服务的介绍。}], max_tokens: 512, stream: False } tasks.append(send_one(session, url, payload)) results await asyncio.gather(*tasks) print(len(results)) asyncio.run(main())压测时要特别注意不要用同一份 prompt 测试所有请求因为前缀缓存会影响结果。我一般准备 10~20 个不同 prompt 轮流发得到的数据更接近真实场景。同时要监控显存和 GPU 利用率如果显存还有大量剩余但 GPU 利用率不高说明并发窗口或参数调度还有优化空间如果显存占用已经很高但吞吐没有提升就要减小 max-num-seqs 或调低 gpu-memory-utilization给 KV Cache 重新分配空间。4. 调优经验与常见问题排查4.1 显存不够、请求排队怎么调本地高并发服务最常见的报错是 CUDA OOM或者表现为所有请求都在排队、吞吐骤降。这时候先别急着加卡先看三个数据项模型权重占用、KV Cache 分配、活跃请求数。vLLM 会打印服务统计日志可以看到平均 KV Cache 使用量。如果是 KV Cache 被占满导致排队优先考虑降低 max-model-len 或降低 max-num-seqs。这里有个反直觉的地方max-num-seqs 设得过大会让调度器同时接受太多请求每个请求都需要分配 KV Cache整体吞吐反而因为资源争抢下降。我调过一段 24G 显存的服务模型占 14Gmax-model-len 4096max-num-seqs 从 256 降到 64吞吐反而提升了 20%。所以这个参数不是越大越好要结合实际并发曲线。如果模型权重本身就占了绝大部分显存比如一个很大的量化模型在单卡上跑那高并发基本无从谈起。这时要么换更小的量化版本要么采用张量并行把权重拆到多卡上再谈并发。4.2 响应变慢、超时增多怎么定位高并发下出现响应变慢通常不是某一个请求执行慢而是排队时间变长。可以先看服务日志里的时间分布vLLM 的日志会显示每个请求的排队时间、prefill 时间、decode 时间。如果排队时间占比高说明系统还在承受能力之上如果 decode 时间占比高说明 batch 太大导致单个 token 生成变慢需要减少并发窗口。这里提醒一句流式接口下客户端看到的第一个 token 时间包含排队时间 prefill 时间。如果很多请求的 prompt 很长prefill 阶段会消耗大量计算资源后面的短请求也会被拖慢。使用 Chunked Prefill 后长 prompt 请求会被切块执行明显降低对短请求的影响。对于聊天场景我会建议把 system prompt 控制在一定长度内既能省 KV Cache也能减少 prefill 阻塞。4.3 多卡场景下的张量并行怎么配当单卡放不下模型或者单卡并发能力不够就需要多卡方案。vLLM 支持张量并行把 Transformer 的权重切到多张卡上每张卡只计算一部分注意力头和全连接层再用通信把结果拼起来。这能让单张卡上容纳更少的权重腾出显存给 KV Cache。启用方式很简单启动时加上--tensor-parallel-size 4。但要注意张量并行需要卡间通信开销如果机器是共享带宽受限的虚拟显卡环境吞吐反而可能下降。在多卡场景下压测重点观察 GPU 利用率曲线是否同步如果某张卡利用率明显低于其他卡说明并行切分配比不均衡或者数据加载成了瓶颈。另外多卡时尽量不要把--gpu-memory-utilization拉满到 0.95因为通信库也会占显存保留 2~3% 的余量可以让长期运行更稳定。4.4 一个可复现的配置参考与注意事项最后给出一套我在本地环境中稳定跑过的参考配置。硬件是单卡 24G模型是 70 亿参数开源模型上下文限制 2048支持约 30 并发。启动指令大致如下python -m vllm.entrypoints.openai.api_server \ --model /data/models/example-7b \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 2048 \ --max-num-seqs 64 \ --enforce-eager--enforce-eager是关闭 CUDA Graph 缓存。这个参数通常会降低吞吐但在显存紧张或偶尔 OOM 时很有用如果你的服务显存充裕就不要加这一项。我踩过一次坑为了追求启动速度长期开着 enforce-eager结果压测吞吐掉了三成。所以要在启动时间和吞吐之间做取舍。还有一些零碎但重要的注意事项容器部署时要给共享内存足够的空间vLLM 的调度器在多进程下对共享内存依赖较高同时要锁住 CPU 核心数量避免推理线程被打扰。日志要定期清理避免磁盘写满导致服务异常退出。模型热更新目前需要重启服务如果业务需要多个模型切换建议提前规划多套服务和路由层。