☰
vLLM高并发推理引擎:从原理到部署,解决大模型并发瓶颈
2026/10/13 22:46:54 网站建设 项目流程

如果你也在本地跑大模型推理,大概率遇到过一个很现实的问题:模型能跑,但顶不住几个人同时用。我一开始就是最简单的做法,后端一个进程加载模型,谁来了谁生成,其他人排队等。结果单个人测试延迟挺正常,一旦同事把请求发上来,整个服务直接变成“假死”状态,GPU利用率低得可怜。后来我把推理模块换成vLLM,才算体会到什么叫高吞吐推理引擎——同样一块卡,并发从十几个请求同时卡住,变成几十个并发还能稳定输出。这篇就围绕我搭建本地高并发推理服务的完整过程来聊,把vLLM的原理、部署、调优和常见坑一次讲透。对正准备做私有化部署、给团队做统一推理API、或者跑批量文本处理的人来说,参考价值应该不小。

1. 项目背景与核心痛点:为什么需要高吞吐推理引擎

1.1 传统推理方案卡在哪

大模型生成一次回答,业内一般分成两个阶段:预填充和解码。预填充要一次性处理整个输入提示词,把中间计算结果写入KV Cache;解码则是一个token一个token往后生成,每一步都依赖上一步的结果,没法真正并行加速。

传统推理框架的常见做法,是“一个请求跑完再跑下一个”,或者做成静态批处理:凑满一批请求,一起算,等这一批全部生成完,再释放显存接下一批。听起来没问题,但静态批处理有一个致命浪费——它必须为每个请求预留整个最大生成长度的KV Cache,而且所有请求要一直占着batch位置,哪怕某一个序列早就生成完了,也得等其他长序列结束才能一起释放。

放在高并发场景下,这个问题会被放大得非常明显。请求长短不一,短的几十个token,长的几百个,一个batch里只要出现两三个长生成任务,短任务就得陪着等。GPU表面上看在跑,实际计算资源大量浪费在不必要的padding和显存等待上,最终结果就是服务卡顿、延迟飙高、GPU利用率长期低走。

1.2 为什么我最终选了vLLM

刚开始调优时,我翻过很多方案。最朴素的“换个大显存显卡”治标不治本,静态batch只是把排队延后,并没有解决显存空转。后来接触过一些需要改模型结构或做编译优化的推理框架,性能是能上去,但每次换模型都要折腾半天,还得啃一堆算子实现,实在不适合我这种想快速把服务跑起来的场景。

选vLLM最直接的原因有三个。第一,它对现有开源模型生态的兼容性最好,绝大多数常见模型格式可以直接加载,不用改模型代码。第二,它把显存管理和请求调度做到了引擎层,我只需要配置几个参数,就能在原来的硬件上获得几倍的并发吞吐提升。第三,它自带兼容业界通用格式的API服务,前端调用方不需要改代码,从“单体脚本”切换到“服务化部署”非常顺。

当然,vLLM也不是万能。超大规模多机部署、特殊算子自定义这些场景,它不一定是最优解。但对本地单机或双卡场景,一个7B到70B范围的开源模型,vLLM几乎是我能选到的性价比最高的高吞吐推理引擎。

1.3 高并发推理服务的影响范围

把这套服务搭好之后,能覆盖的使用场景比我预想中宽很多。

一个是团队内部的问答机器人。之前同事用的时候,只能一个个排队等回复;换成vLLM做后端以后,几十个人同时提问也能稳定响应,大家感知上像在用云端API一样。一个是批量文本处理任务,比如每天要跑几千条日志摘要、商品描述生成,这种场景需要的是“拼命压GPU”,而不是“低延迟”,vLLM的连续批处理可以持续把计算队列填满,整体吞吐提升非常明显。还有一个是私有化部署,数据不能出内网,必须把推理能力放在本地机房,vLLM是一个足够成熟的底座,不需要依赖外部云端推理服务。

说白了,只要你的瓶颈在“GPU算力充足但并发上不去”,vLLM基本都能派上用场。

2. vLLM核心机制解读:高吞吐从哪里来

2.1 PagedAttention:显存分页解决KV Cache浪费

vLLM能高吞吐,根源在于它没有用传统静态显存分配方式,而是实现了类似操作系统虚拟内存的“分页”机制,官方叫PagedAttention。这个思路很直接:KV Cache不再一次性按最大长度全量预留,而是切成一页一页,每个请求用到多少就分配多少,空闲页可以立刻被其他请求复用。

我算过一笔账,一个典型的13B模型,假设40层、40个注意力头、每个头维度128,FP16精度下,每个token的KV Cache大小大约是 2×40×40×128×2 字节,也就是约800KB。如果按传统方式预分配8192个token的最大长度,单个请求就要占掉6.4GB显存。可实际问答请求平均只生成几百个token,这6.4GB里大部分是“刚占坑、没用上”的空间。一旦并发上来,这种浪费直接让显存爆炸。

PagedAttention把浪费砍掉了,显存利用率大幅提升,同一块卡能同时驻留的序列数量自然变多。这也是为什么很多人升级到vLLM之后,感觉“显存忽然变大了”,本质是分配逻辑变了。

2.2 Continuous Batching:让GPU一直干活的调度方式

如果说PagedAttention解决的是“放不放得下”,Continuous Batching解决的就是“跑不跑得满”。

传统静态批处理有个很别扭的点:必须等一个batch全部结束,才开始下一个batch。vLLM的做法是每个iteration都检查一遍序列状态——已经生成完的请求立刻从当前batch里摘掉,腾出的算力马上分配给新进入的请求,不需要等整批结束。同时,新请求的预填充阶段可以和已有请求的解码阶段塞在同一次前向计算里,GPU计算单元始终在处理有效token。

这就像小饭馆翻桌:不会等所有客人吃完饭才接待新客人,而是谁吃完谁买单,空出来的桌子立刻给下一桌。静态批处理相当于“按轮次营业”,每轮必须等全场清空再开下一轮,翻台率自然低。Continuous Batching把这个“营业模式”变成了真正随时有空位随时进的流水线,吞吐提升的关键就是这种调度上的优化。

2.3 这套机制的优势与边界

实测下来,同样的显卡、同样的模型,vLLM在并发场景下的吞吐量经常能比朴素方案翻两到三倍,首token延迟也更稳定。这是它最吸引人的地方——不需要换硬件,只需要换个推理引擎。

但它也有自己的边界。PagedAttention以固定block为单位缓存,如果请求序列特别短、block设得又较大,照样会产生内部碎片;连续batching调度器的CPU开销比静态批处理要高,如果机器主频太低或核心数太少,调度本身可能成为瓶颈。另外,所有模型结构并不都能无缝跑在vLLM上,某些自定义注意力结构的模型还是需要额外适配。说得直白一点,vLLM解决了大多数场景的痛点,但别指望它把所有模型所有场景都优化到极致。

3. 本地高并发推理服务搭建全过程

3.1 环境准备与安装注意点

先说硬件底线。最少需要一张支持CUDA的显卡,显存大小决定你能跑多大的模型以及支撑多少并发。我自己测试常用的是24G显存的单卡和双卡48G的机器,24G卡跑7B模型和量化后的13B模型很舒服,双卡则更适合跑权重更大或并发需求更高的服务。

系统方面,Linux是最顺的,Windows虽然也能装,但会遇到更多莫名其妙的兼容问题。软件层面建议用conda建独立环境,Python版本选3.9以上。安装vLLM最省事的方式是直接pip安装:

pip install vllm

装完先用一行代码验证一下:

python -c "import vllm; print(vllm.__version__)"

这里有个坑:vLLM对PyTorch和CUDA版本有要求,如果环境中已经有其他深度学习库,依赖冲突的概率不小。我踩过一次,环境中原本有老版本PyTorch,装上vLLM后启动直接报算子找不到,最后把环境重建、重新安装依赖才解决。所以强烈建议别复用旧的深度学习环境。

3.2 模型加载与基础配置

vLLM加载模型很简单,核心就是LLM这个类和一个采样参数对象。下面是我常用的最小示例:

from vllm import LLM, SamplingParams llm = LLM( model="/data/models/my-chat-7b", tensor_parallel_size=1, gpu_memory_utilization=0.90, max_model_len=8192, trust_remote_code=True, ) params = SamplingParams( temperature=0.7, top_p=0.9, max_tokens=512, ) outputs = llm.generate("你好,请简单介绍下你自己", params) print(outputs[0].outputs[0].text)

几个参数值得留意。tensor_parallel_size=1表示单卡运行,多卡时改成卡数。gpu_memory_utilization=0.90表示让vLLM最多使用90%的显存,剩下10%留给其他进程和驱动预留,别贪心设到0.95以上,容易启动阶段就OOM。max_model_len非常关键,它限制了模型能处理的最大上下文长度,设得越大,KV Cache预分配压力越大,很影响并发上限。trust_remote_code要看你用的模型而定,不少开源模型为了让tokenizer正常加载,必须开这个选项。

第一次加载模型会比较慢,因为要读权重、构建图、做显存profile,看到控制台“卡住”几分钟是正常的,不是死机。

3.3 启动API服务与连通性测试

平时我更习惯把vLLM起成一个常驻服务,这样前端业务方可以通过HTTP调用,不用每次请求都重新加载模型。启动命令的大致形态如下,具体入口以你安装的vLLM版本为准:

python -m vllm.entrypoints.api_server \ --model /data/models/my-chat-7b \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --max-num-seqs 32

参数含义很直白:--host 0.0.0.0允许局域网内其他机器访问;--port 8000是服务端口;--max-num-seqs控制最大并发序列数,这是高并发服务最重要的旋钮之一,太小发挥不出引擎优势,太大又可能撑爆显存。

服务起来以后,先用curl做个简单连通性测试:

curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"my-chat-7b","messages":[{"role":"user","content":"你好"}],"max_tokens":128}'

能正常返回内容,说明服务和模型加载没问题,接下来就可以做并发压测了。

3.4 并发压测:效果差异看得见

我用标准库写了个并发脚本,简单模拟多人同时请求的场景。不用引入额外依赖,逻辑也直白:并发线程发一批请求,统计总耗时和总token数,算出吞吐。

import json import time import urllib.request from concurrent.futures import ThreadPoolExecutor url = "http://127.0.0.1:8000/v1/chat/completions" prompts = ["用三句话解释机器学习"] * 100 def call_once(prompt): payload = { "model": "my-chat-7b", "messages": [{"role": "user", "content": prompt}], "max_tokens": 256, } req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, ) t0 = time.time() with urllib.request.urlopen(req, timeout=120) as resp: body = json.loads(resp.read().decode("utf-8")) dt = time.time() - t0 return dt, body.get("usage", {}).get("total_tokens", 0) start = time.time() with ThreadPoolExecutor(max_workers=32) as pool: futures = [pool.submit(call_once, p) for p in prompts] results = [f.result() for f in futures] elapsed = time.time() - start total_tokens = sum(r[1] for r in results) print(f"elapsed: {elapsed:.2f}s") print(f"throughput: {total_tokens / elapsed:.2f} tokens/s")

我在同样硬件上做过相对对比,数字只供参考:

并发方式成功率平均单请求延迟整体吞吐
单请求串行100%1.2秒约220 tokens/s
32线程并发100%6.8秒约420 tokens/s
64线程并发92%12.4秒约380 tokens/s

能明显看到,串行方案虽然单请求延迟低,但整体吞吐被锁死;并发到32以后,vLLM开始发挥连续批处理优势,吞吐接近翻倍。继续加压到64反而有少量超时,说明并发上限不是越高越好,需要摸准硬件能承受的边界。

4. 参数调优与性能优化实战

4.1 几个直接影响吞吐量的核心参数

很多人和我一样,第一次调参就是盲目把max-num-seqs调大,结果显存爆掉,或者CPU调度不过来。实际上,这几个参数得配合着看。

gpu_memory_utilization决定了vLLM最多吃多少显存。这个值直接决定KV Cache池大小,进而决定能承载的并发序列数。默认0.9,如果显卡还要加载别的模型或做数据预处理,建议调到0.75到0.85之间。

max_model_len是另一把锁。它越大,每个序列理论上能占用的KV Cache上限越高。有些部署为了图方便直接把模型原生长度拉满,结果并发一上去,显存瞬间耗尽。更合理的做法是根据真实业务请求重新评估,比如业务里最长输入不超过2000token、输出不超过1024token,就把max_model_len设成4096,能空出大量显存放并发序列。

max_num_batched_tokens控制单次前向迭代能融合的token总量,影响最大的是吞吐平滑度。设太小,大批短请求时GPU吃不饱;设太大,某个长请求的预填充可能把一大轮计算都占住,放大长尾延迟。一般可以先保持默认,再根据压测结果调整。

max_num_seqs是并发序列数上限。我习惯用“从32起跳,逐步加,每次跑一轮压测,观察显存和延迟变化”的方式摸边界。一次加一两倍大概率不是最优,可能只是把延迟顶高。

4.2 多卡部署:张量并行的正确打开方式

如果单卡显存放不下模型,vLLM支持张量并行,简单说就是把一个模型拆分到多张卡上,每张卡只负责一部分计算和一部分参数。启动参数里把tensor_parallel_size设置成卡数,或者命令行传--tensor-parallel-size 2,vLLM会自动把模型切分好。

多卡并行不是“卡越多越快”。对7B这种小模型,两张卡的通信开销可能把单卡的计算优势吃掉,实测有时反而不如单卡。真正应该上多卡的是那些单卡放不下的模型,比如70B级模型量化到INT4后也要35GB左右,一张24G卡放不下,用两张卡做张量并行,每卡负担约17.5GB,剩余显存还能留出不少KV Cache。

多卡部署最容易出问题的环节是卡间通信。很多启动失败案例都是多卡通信库初始化报错,多半和驱动版本、卡间P2P能力有关。真遇到了,先确认所有卡都能各自正常跑单卡推理,再排查通信问题。还有一种情况是显存不均衡,比如两张卡一张接了显示器一张没接,可用的显存不一样,vLLM会按最少可用显存的那张卡来分配,这时建议优先调整卡的工作负载。

4.3 不同业务场景的调优策略

在线聊天和高吞吐离线推理是两种完全不同的思路,不能一套参数打天下。

先说在线聊天API场景。核心诉求是“首token别太慢、别超时”,所以要限制max_tokens,别让某个请求无限生成长文占住一个序列位;max_model_len也建议设保守一点,毕竟聊天场景极少用到超大上下文;并发序列数可以设到64左右,前端的请求基本不会排队太久。

再说批量离线推理场景。这种场景不需要起API服务,直接在脚本里把几百个、几千个prompt一次性传给LLM.generate,让引擎自己决定怎么调度。我试过同样一批任务,走API并发压测的吞吐不到直接进程内调用的一半。因为省掉了HTTP序列化、网络传输和请求解析的开销,GPU能更专注地跑计算。

最后是长文档摘要这类大上下文场景。每个请求都会吃掉大量KV Cache,并发数必须主动下调,比如max-num-seqs降到8或16,否则很容易显存溢出。同时建议提前做输入裁剪,把与任务无关的原文段落去掉,能显著提高吞吐稳定性。

5. 常见问题与排查技巧实录

5.1 CUDA OOM和显存不足排查

这是最常遇到的问题。启动阶段报显存不足,常见原因是gpu_memory_utilization设太高,比如0.95,驱动和CUDA context自己也要占用一部分显存,vLLM在初始化KV Cache池时就会申请失败。降到0.85左右通常能解决。

运行过程中OOM,则更可能是max_num_seqs或max_model_len设得过大。此时别急着重启,先看启动日志里的显存占用分布,找到底是权重占得多还是KV Cache池占得多。如果是KV Cache问题,优先调小max_model_len;如果是权重问题,需要考虑量化或者换更大显存。很多人同时把并发和长度都调到了“看起来很猛”的数值,结果一压测就崩,这属于典型的参数叠加失控。

5.2 并发吞吐反而下降的排查

有时候并发从32加到64,整体吞吐不升反降。这不是vLLM失效,大概率是请求特征出现了倾斜。

一种情况是某些请求的max_tokens设置得特别大,比如512甚至1024,导致一批短请求的batch里混入了一个“长尾生成任务”,它占住一个序列位持续输出,其他请求只能等。解决思路是应用层限制max_tokens,或者在网关层面对超长任务做拦截。

另一种情况是CPU成了瓶颈。连续batching的调度要在CPU上做,如果机器核心数少,HTTP解析和调度一样会拖后腿。可以观察压测时CPU占用率,如果已经满负荷,就该降低并发、升级CPU,而不是继续加压。网络开销也是一样,大量小请求频繁HTTP往返时,走进程内批量推理更划算。

5.3 模型加载失败与下游对接坑

加载失败最常见的原因是trust_remote_code没开。很多模型的tokenizer或模型代码需要现场执行,不信任就不会加载,启动日志会明确提示缺某个属性或模块。另一个常见坑是tokenizer和模型权重不匹配,输出乱码或加载报错,这时重新下载对应tokenizer文件即可。

如果用了量化模型,还要确认量化格式和vLLM版本是否兼容。比如某些低比特量化格式在旧版本里不被支持,启动时不会直接报“不支持”,而是报一些奇怪的tensor shape错误。遇到这种情况,升级vLLM版本,或者换一种量化格式,比硬调参数有效得多。

和下游系统对接时,最典型的坑是超时设置。vLLM在处理长上下文时首token延迟会明显变高,如果调用方把超时设成3秒,大概率隔一会儿就超时一次。建议把客户端超时放宽到模型正常生成时间的两到三倍,再配合限制max_tokens,才能让双方都稳定。

5.4 长期稳定运行的经验

这套服务不是跑一次压测就完事,真正上线后要考虑长期稳定性。

我现在的做法是:用进程守护工具把启动命令包一层,进程崩溃后能自动拉起;健康检查直接打vLLM自带的/health接口,负载均衡器定期探测,避免把请求调度到已挂掉的服务上;日志写完做轮转,防止磁盘被日志占满。另外,服务刚启动时CUDA context还没完全预热,我会让监控系统自动发一个短请求暖一下,等首token延迟恢复正常再接入真实流量。

还有一点,别等到线上OOM才看显存。我会写一个简单的监控脚本,定期采集显存占用、GPU利用率和请求队列长度,如果发现显存占用长期贴着上限,就主动调低max-num-seqs或者加一台机器。高并发推理服务的关键不是把参数调到极限,而是在极限和稳定之间找到可用窗口。

这套基于vLLM的本地高并发推理服务,我前后跑了好几个月,最大的感受是:不要一开始就追求极致参数,先用默认配置跑通,再根据显存和请求特征一步步调。对我个人来说,收益最明显的一件事就是把团队内部的知识库问答从“一人用还行,多人用就卡”变成了“二十个人同时提问也能稳定输出”。最后再分享一个小技巧:如果只是做离线批量生成,别起API服务,直接在脚本里用LLM.generate喂一批prompt,吞吐能再拉高一截,这也是vLLM最被低估的一种用法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询