vLLM本地部署与调优实战:从串行脚本到高吞吐并发推理

📅 发布时间:2026/10/16 4:35:09
vLLM本地部署与调优实战:从串行脚本到高吞吐并发推理
最近一直被一件事追着跑本地那台带GPU的机器同时被好几个业务方轮着要推理结果。我之前用的那套串行脚本单条请求跑起来看着还行可一旦并发上来就开始排队更难受的是前一批还没算完新请求已经在门口超时。忍了一周之后我把服务整体迁到了vLLM这一层高吞吐推理引擎上接入方式几乎没变但本地高并发推理的体验完全换了个样。这篇文章就把我这次完整部署、调参、压测的过程摊开讲重点说清楚vLLM为什么能把吞吐拉起来以及本地部署时最容易被人忽略的几个坑。适合刚接触大模型服务化、或者正在对比不同推理框架的人参考。1. 一个开源推理引擎凭什么是高吞吐的关键1.1 传统推理脚本的三笔浪费先说个背景。我原来的做法很朴素GPU上起一个常驻进程来一个HTTP请求就跑一次完整的前向推理算完把结果返回。这个方案的问题不是“不能用”而是“扛不住并发”。第一笔浪费在算力。一次推理里注意力计算、采样、KV Cache更新是环环相扣的GPU在等待请求间隙全是空窗。单路推理时显卡利用率经常连30%都不到。第二笔浪费在KV Cache上。大模型推理时每解码一个token都要把前面所有token的Key和Value缓存下来避免重复计算。串行脚本里每个请求都自己存一份完整KV请求结束就丢掉下个请求又从头开始算。如果一段对话历史有几千token这部分重复计算的代价非常夸张。第三笔浪费在批处理尾部阻塞。稍微进阶一点的方案是静态批处理攒一批请求进去等整批都生成完再换下一批。问题在于一批里总有长有短短的早就结束了也要被慢的乘客卡住GPU在那儿干等。这三笔浪费叠加下来结果就是单看一个请求延迟还行一旦把二十个请求同时灌进来整个服务的吞吐就崩了。1.2 连续批处理和分页显存管理是怎么协同的vLLM能解决这些问题靠的是两个机制Continuous Batching和PagedAttention。Continuous Batching连续批处理核心是“不等整批结束”。传统静态批处理一锅里煮等所有人吃完再上下一锅连续批处理则是谁吃完了谁先离席空出来的位置立刻让新请求补上。GPU不用在每批之间等待算力空窗被压到很低。这个机制对短请求特别友好——短请求完成得快腾位也快整体吞吐自然高。PagedAttention分页注意力解决的是显存碎片和KV Cache利用率问题。它借鉴操作系统的虚拟内存分页把KV Cache切成一枚一枚固定大小的块用的时候按需分配不用的时候随时回收。这样请求之间的KV缓存可以先分散存储、逻辑上连续显存利用率大幅提高。最直观的效果是单卡能同时容下的并发请求数量明显变多了。这两者配合起来vLLM等于同时把“算”和“存”两个方向的吞吐都拉高。而且它对外提供兼容主流大模型客户端的API协议很多现成的Chat客户端、脚本只需改一下服务地址就能接入这一点后面细说。2. 部署前先把三笔账算清楚动手部署之前我建议大家先算清三笔账显存预算、依赖版本、模型文件。这三件事没理清后面一定返工。2.1 第一笔账模型权重和KV Cache的显存估算推理服务的显存占用主要来自三部分模型权重、KV Cache、以及CUDA上下文和激活值。很多人只算权重结果一启动就OOM。权重部分有个快速估算方法FP16精度下大约每10亿参数占2GB显存。一个7B模型就是14GB左右8B模型16GB左右70B模型则要到140GB单张24GB的卡根本放不下。量化到INT4之后这个数会降到26%左右8B模型大约5GB出头。KV Cache部分没那么直观。它跟模型架构强相关层数、KV头数、每头维度都影响单个token所需的缓存大小。以常见的分组查询注意力架构为例KV头数为8、每头128维、32层、FP16精度单个token大概对应128KB的KV缓存。一个8000token的请求KV Cache就接近1GB。如果用的是老式多头注意力这个数字还会成倍上涨。所以部署前建议列一个这样的表模型规模FP16权重显存单请求8K上下文预留KV推荐单卡显存7B级别约14GB约1GB24GB以上8B级别约16GB约1GB24GB以上13B级别约26GB约1.5GB48GB或双卡24GB70B级别约140GB约4GB多卡80GB方案这只是估算。真正启动时CUDA context还要吃掉半GB到1GBCUDA graph预热的激活值也占空间。我会把gpu-memory-utilization参数控制在0.9以下留10%余量。2.2 第二笔账GPU驱动、CUDA、Python和PyTorch版本vLLM对编译环境比较挑剔版本不对会出现编译失败、算子缺失、甚至莫名其妙的段错误。我自己常用的组合是显卡驱动支持CUDA 12.1、Python 3.10到3.12、PyTorch 2.1以上。依赖矩阵建议以官方文档为准但有个通用规律Python不要用最新的PyTorch不要用太旧的。依赖我用的版本说明显卡驱动支持CUDA 12.1的驱动用nvidia-smi确认驱动版本带的CUDA分支Python3.10 / 3.113.12也行但避免追3.13PyTorch2.1及以上太老版本缺算子支持vLLM与PyTorch版本匹配的版本优先看官方兼容矩阵这里有个经验能省事就省事别在自己机器上把所有东西从源码编译一遍。不是说编译这条路不行而是编译时间太长且容易遇到系统库缺失的问题。后面我会说直接使用官方发布镜像的路线这是最稳的。2.3 第三笔账模型文件得先落地不要指望服务启动时才现下载模型。我第一次部署时直接把模型名填上去期望启动过程中自动下载结果启动卡了快二十分钟网速稍微抖一下整个进程就假死。更麻烦的是如果部署环境是内网启动时根本连不上外部网络。正确做法是提前用模型托管平台的命令行工具把模型文件完整拉到一个本地目录。我习惯放在/models下面目录结构里包含权重文件、配置文件、分词器文件然后用本地路径引用。启动vLLM时模型参数直接指到本地绝对路径同时把离线模式的环境变量打开告诉框架别尝试联网。这样既省启动时间也保证了断网环境可复现。模型这块还有一个细节同一个模型名在不同平台或不同版本下可能存在权重格式差异。所以下载完之后我通常检查一下权重文件的类型确认是FP16还是量化过的格式因为这直接影响下一步的显存估算和启动参数。3. 从“能跑”到“能扛”完整部署拆解3.1 选Docker还是裸机这两条路线我都跑过各有明显取舍。Docker路线的最大好处是依赖自动隔离。官方发布的容器里CUDA、PyTorch、FlashAttention这些基础组件都是配好版本关系的启动即用不用自己折腾。缺点是镜像体积大几个GB起步而且默认共享内存设置会让服务在并发稍高时崩溃后面会专门讲这个坑。裸机路线的优势是性能完全透明显存管理和日志直接调用宿主机资源排查问题更直接代价是需要自己维护一套Python环境的兼容关系升级时容易出连锁反应。我自己当前的主力部署方式是Docker原因很简单多项目共用一台服务器时容器隔离能避免各种依赖污染升级引擎版本时只需要拉新镜像旧服务先留着随时可以回滚。建议你如果还在纠结先走Docker踩坑面小很多。3.2 一条能扛住业务的启动命令环境准备好之后我用这样一条命令启动一个8B模型的推理服务docker run --runtime nvidia \ --gpus all \ -v /models:/models \ -p 8000:8000 \ --shm-size 16g \ 官方发布的开源推理镜像 \ --model /models/Llama-3-8B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --max-num-seqs 64如果不用Docker裸机则是这样pip install vllm python -m vllm.entrypoints.openai.api_server \ --model /models/Llama-3-8B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --max-num-seqs 64参数逐个说。--model指定本地模型路径--host 0.0.0.0表示监听所有网络接口不然只能本机访问--gpu-memory-utilization 0.9表示分配90%显存给推理程序剩余留给CUDA context--max-model-len 8192是最长上下文长度--max-num-seqs 64是同一批次最多容纳64个请求序列。这几个参数是最容易影响稳定性的不要乱给值。启动过程有个观察点加载完权重之后进程会做一段CUDA算子的初始化和预热日志停在某个“准备就绪”的状态然后才监听端口。我通常再等十几秒确保预热完全结束再去发请求否则第一个请求会被首Token延迟拖得特别久。3.3 首次验证探活接口与常用请求服务起来后先探一下模型列表curl http://localhost:8000/v1/models能返回模型元信息说明服务正常。再发一条对话请求验证输出curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: /models/Llama-3-8B-Instruct, messages: [ {role: user, content: 用一句话介绍你自己} ], max_tokens: 256, temperature: 0.7 }响应里有个usage字段包含prompt_tokens和completion_tokens。后面压测时我统计吞吐就靠这个字段汇总。首次验证时如果响应正常可以把max_tokens调大跑一个较长输出看看服务在连续解码时是否稳定。4. 让吞吐再上台阶参数调优与量化服务跑通之后接下来才是关键怎么把它调到适合你的业务场景。vLLM本身是“吞吐优先”的引擎它会主动把GPU塞满但这不意味着每个业务都该一味追求最大吞吐还要看延迟能不能接受。4.1 先决定你是吞吐型还是延迟型在调参之前我建议先明确业务侧的需求。通常有三类目标典型场景关注指标吞吐优先离线批量处理、数据标注、评测脚本tokens/s整体完成时间首Token延迟敏感聊天机器人、对话助手首Token延迟越短越好均衡型混合业务、内部网关二者都要盯为什么这个判断很重要因为同一套参数往吞吐方向调会让首Token延迟变差往低延迟方向调又会损失整体吞吐。没有两全其美的配置你得先知道自己是谁。我那个内部项目属于混合型所以我最后选了均衡参数。4.2 调度参数逐个调vLLM提供的参数很多真正需要关注的调度类参数就这几个参数默认值影响我的调法max-num-seqs256并发序列上限决定队列能同时跑多少请求显存允许时调到64-128描述max-num-batched-tokens自动单次调度的token总预算长上下文场景适当限制避免调度过猛block-size16PagedAttention每块token数量保持默认短文本密集时试32gpu-memory-utilization0.9显存分配比例专用卡给0.9-0.95共用卡给0.6max-num-seqs是我调得最频繁的参数。并发从16调到64吞吐会有明显爬升但继续调到128时如果显存预算跟不上KV Cache分配开始崩这时不是参数问题是硬件上限到了。max-num-batched-tokens在长上下文场景里很容易被忽略。它限制的是典型的连续批处理中单次迭代最多塞进多少token。如果max-model-len给到32768、并发又高单次调度就会塞进过多长请求调度延迟反而变大。做法是把这个值限定在比如4096到8192之间让连续批处理更平滑地迭代。还有一个容易被忽视的block-size。它是分页缓存的最小分配单位。默认16在大多数场景够用如果业务里至少有大量超短请求可以试试32减少块表开销。但这不是必调项我一般放在最后一步观察性调整。4.3 量化与多卡并行显存不够时优先考虑量化而不是换卡。常用做法是把FP16权重量化到INT4或FP8。命令行加上量化参数vLLM会在加载权重时做反量化或直接跑量化算子。代价是精度轻微损失但对很多对话生成任务这个损失肉眼基本看不出。量化带来的最大收益是权重显存变小省出来的空间可以容纳更多并发序列、更大的KV Cache。我实测8B模型FP16下显存吃紧时量化后同一张卡可以多开一倍的并发吞吐反而因为并发放开而上升。如果单卡显存实在不够则用多卡并行。tensor-parallel-size设为2模型会按注意力头维度切到两张卡上并行计算。只要机器有PCIe带宽或NVLink效果比较直接70B模型用两张80GB的卡就能放下。4.4 长上下文要舍得减并发长上下文和并发是天然的对手。max-model-len每翻一倍KV Cache预算就翻一倍。很多人习惯把上下文拉到32768并发还想去到32结果一张卡直接爆显存。我的经验是上下文长度优先级高时就把max-num-seqs压下来例如8K上下文可以并发6432K上下文就只保留16到24。如果业务里长短请求混杂可以考虑让长请求单独走一个服务短请求走另一套配置后台再统一分流。这种做法比试图用单一配置同时满足两端要省心得多。拿我自己测试结果做参考8B模型单卡24GBmax-model-len设8192并发16时吞吐稳定在几百到近千tokens每秒区间量化后并发放开到32整体吞吐进一步提升。和最初串行脚本相比基本是数量级上的差异。5. 接入业务层用并发压测说话服务调完之后要接进业务链路。这一步不是“配置完成万事大吉”而是要用真实请求把吞吐数字测出来才能放心交给业务方。5.1 一个能直接用的压测脚本我用一个Python脚本做基础压测同时发64个请求每个请求要求生成256个token左右统计完成数量、平均延迟和整体吞吐。import concurrent.futures import time import requests url http://localhost:8000/v1/chat/completions payload_template { model: /models/Llama-3-8B-Instruct, messages: [{role: user, content: 请用三句话介绍本地大模型推理的特征}], max_tokens: 256, temperature: 0.7 } def send_one(_): start time.time() resp requests.post(url, jsonpayload_template, timeout60) elapsed time.time() - start completion_tokens resp.json().get(usage, {}).get(completion_tokens, 0) return elapsed, completion_tokens with concurrent.futures.ThreadPoolExecutor(max_workers16) as pool: results list(pool.map(send_one, range(64))) total_tokens sum(r[1] for r in results) total_time max(r[0] for r in results) print(f完成请求数: {len(results)}) print(f平均单请求耗时: {sum(r[0] for r in results) / len(results):.2f}s) print(f最长耗时: {total_time:.2f}s) print(f总生成token数: {total_tokens}) print(f吞吐: {total_tokens / total_time:.2f} tokens/s)这个脚本有几个细节值得注意。一是max_workers设为16不是64目的是避免客户端自身连接池成为瓶颈。二是统计吞吐时分母取“最长耗时”表示从第一个请求发出到最后一个请求完成的时间跨度更接近服务在真实压力下的吞吐定义。三是timeout给到60秒防止某个异常请求让整个测试挂很久。5.2 从压测结果能读出什么每次压测之后我会记录三个数平均单请求耗时、最长耗时、整体吞吐。这两个数一旦出现背离就是在暗示服务内部正在排队。举例并发16时吞吐500 tokens/s平均单请求2秒并发升到64时吞吐到了700 tokens/s但平均单请求变成8秒。这说明吞吐还在涨但每个请求的体感延迟已经恶化紧接着再测首Token延迟大概率也涨了。这时要根据业务类型做取舍——如果业务方在意首Token延迟就得下调并发上限或优化长请求占比。压测过程中还要看一眼GPU利用率。nvidia-smi显示的利用率如果一直徘徊在50%以下问题可能不在显存而在连续批处理调度没满常见原因是max-num-seqs太小请求根本没进到GPU里去。这时候调大并发参数比更换硬件更有用。5.3 业务接入的对外策略压测通过之后接入业务层还需要定几个策略。第一是超时。单个请求设置超时建议比“平均单请求耗时”高3到5倍给慢请求留余地。第二是重试。对于503、超时这类异常可以重试一次但重试请求建议丢到队列尾端不要让同一个请求连续打穿服务。第三是限流。如果不做限流业务爆发时请求会全部堆在vLLM的排队伍里最终表现为首Token延迟失控。限流值可以参考压测里“首Token延迟开始恶化”的并发点。另外如果一张卡要同时服务多个业务不要让所有业务都挤在一个模型实例上。我会给不同业务开不同的服务端口各自分配显存比例比如A业务分60%显存B业务分30%。这样某个业务出现并发尖峰时不会把另一个业务的延迟拉到崩溃。确实会牺牲一些整体吞吐但隔离性是值得的。6. 本地实操踩坑实录那些让我返工的细节最后这一部分我把本次部署过程中真实踩过的几个坑集中说一下。每个坑都花了我不少时间排查写下来供你参考。6.1 Docker共享内存太小导致进程假死现象Docker方式启动服务后端口监听了但第一个并发请求发过去进程就像被冻住一样完全没有响应。日志里没有任何报错只有反复的超时。排查链路我先怀疑是模型加载问题重启再试依旧假死又怀疑是网络问题但curl探活接口能通。最后用docker stats看容器内存发现共享内存使用量顶到了默认64MB的上限才意识到是Docker的/dev/shm设置太小。原因vLLM在并发场景下会把张量数据传输放到共享内存或临时文件缓冲默认64MB根本不够用。解决方式是在Docker命令里加上--shm-size 16g。这个参数和模型显存无关但漏掉它Docker方式在并发一高时就会踩雷。6.2 一压测就CUDA OOM可能不是真爆显存现象参数给得很宽——max-model-len设32768、并发设128一启动压测就报CUDA OOM。我当时几乎第一时间去调低了gpu-memory-utilization从0.9调回0.8结果照样OOM。排查链路把报错信息里的显存分配数字和KV Cache预算对照了一下发现自己给的32768上下文和128并发相乘KV预算已经离谱即使把整体显存利用率降到0.5也装不下。后来把上下文降到8192、并发降到64问题立刻消失。原因CUDA OOM不一定是权重放不下更多时候是KV Cache的预算按“最大上下文×并发序列数”预分配两个参数乘起来超过显存剩余空间。不要只看gpu-memory-utilization先算账再给参数。6.3 高并发下首Token延迟飙到几十秒现象并发从16提到64后整体吞吐确实上去了但压测脚本里的平均单请求耗时从2秒涨到10秒以上个别请求甚至能到20秒。排查链路起初怀疑是后端解码速度变慢但看了GPU利用率发现其实很高而且每秒产出的token总数在增加说明算力没有浪费。最后确认是请求在vLLM内部调度队列里排队过久并发上限64没问题但长请求一直占着位置后续短请求只能等。原因连续批处理虽然能动态补位但当请求长度差异很大时短请求依然会排在长请求后面。解决方式有两条一是把max-num-seqs适当调低给短请求更早的调度机会二是在业务层按请求长度分队列长短分开处理。如果只是吞吐优先这条可以忽略但对聊天类业务一定要重视。6.4 模型下载与“启动时联网”的教训现象第一次在内网环境部署时指定了模型名指望启动时自动下载。结果服务启动后一直停在拉取状态等了半小时也没好后来才发现内网根本联不上外部模型托管站点。排查链路把模型文件在其他能联网的机器上提前准备好拷到内网服务器的/models目录再改启动参数为本地路径同时打开离线模式环境变量启动时间立刻降到一分钟以内。原因推理引擎本身不存权重它只是个加载器。只要模型文件没在本地启动就会尝试联网。所以养成一个好习惯部署前先用命令行工具把模型完整拉下来放入本地固定目录并记录版本号。这个目录就是整个部署的基线后续任何环境迁移都带上它。最后说一个我现在的习惯每次调整完参数都会保留一组压测基线记录包括并发数、上下文长度、平均延迟、吞吐和GPU利用率。下次再改配置直接拿新数据跟基线比而不是看感觉。另外一个实用技巧是新服务启动后先做一次预热请求等响应时间降到稳定值再开始压测否则首Token延迟会虚高干扰判断。整体看下来vLLM的部署并不复杂真正的功夫都在部署前的显存规划和部署后的参数取舍上这两步想清楚本地高并发推理服务基本就不会出大岔子。