做AI应用落地这两年,我最大的感受是:模型训出来只算完成了三分之一,把推理服务稳定地扛住线上并发,才是真正让人头秃的部分。无论是给内部业务做一个大模型问答助手,还是对外提供图像识别API,到了上线那天,你都要面对同一个问题——几十上百个请求同时打进来,GPU就那么一两块,显存就那么多,怎么让每个请求都合理拿到计算资源,同时保证延迟和吞吐达标?这套东西,就是并发推理架构要解决的事。
我不是在讲理论。下文所有内容,都来自我给多个项目做推理服务部署、压测,以及线上故障救火的真实经验。如果你正准备给自己的模型搭推理服务,或者已经在用vLLM、Triton这类框架但搞不懂参数怎么调,希望这篇能让你少走点弯路。读完你能带走三样东西:一套判断并发推理瓶颈的分析思路,一份显存和并发量的估算方法,以及一份可以直接参考的压测调参记录。
1. 并发推理为什么难做:底层逻辑和你想的不一样
1.1 推理和训练完全是两回事
我见过不少后端团队,把训练时的batch逻辑直接搬到推理服务里,结果一上并发就出问题。原因在于推理和训练对计算的要求根本不一样。
训练阶段,核心目标是吞吐。一个batch塞多大就塞多大,让GPU尽量吃满,哪怕一个step跑几十秒,只要总吞吐上去了就行。推理阶段恰恰相反,用户在线等着结果,延迟是硬指标。你说我这个模型每秒能处理1万个token,但单个请求要等30秒才出第一个字,产品经理当场就得炸。
更麻烦的是,大模型推理有prefill和decode两个阶段。prefill阶段要并行处理用户输入的提示词,计算密集,能充分利用GPU;decode阶段是逐token生成,每一步的输入只有上一个token,吃的是访存带宽,GPU算力反而闲下来。一个请求从进入到结束,计算资源的需求是动态变化的,这就让调度变得非常复杂。可以把这个过程类比成餐厅出菜:prefill像是一次性把一锅汤底准备好,decode则是按点单顺序一桌一桌地上菜,汤底准备再快,上菜速度却受限于服务员能端几盘。
1.2 三个指标之间的博弈
做并发推理架构,身边始终有几个数字:吞吐量、延迟、资源利用率。它们之间是典型的互相挤兑关系。
吞吐量常用tokens/s或者每秒请求数来量,代表系统单位时间能处理多少活。延迟又分成两个关键节点:TTFT(首token延迟)和TPOT(每个token的生成时间)。TTFT决定用户多久看到第一个字,TPOT决定生成速度。资源利用率则是GPU算力利用和显存占用的情况。
我把这三者的关系理解成一家餐厅:GPU是厨房,吞吐量是能出多少桌菜,延迟是上菜速度,资源利用率是厨师的忙碌程度。如果只想着让厨师一刻不停,把所有订单一股脑全接进来,就会出现厨房堆满食材但灶台不够,每桌客人都饿到骂街。架构设计的所有工作,本质就是在三个指标之间找一个针对业务场景的平衡点。
比如你做的是离线批量处理,几百个文档要做摘要,那延迟可以放宽,把吞吐拉满就行。但如果是线上聊天机器人,TTFT超过2秒用户就开始流失,就得牺牲一部分吞吐换低延迟。先想清楚业务要什么,再谈架构设计,否则一切参数调优都是自嗨。
1.3 请求特征决定架构形态
并发量的数字本身只是个起点,真正影响架构设计的是请求特征。我接手过不少“看着QPS不高但还是卡死”的项目,排查到最后基本都是请求特征和调度策略不匹配。
第一个特征是请求的到达模式。是匀速到达还是突发性强?比如内部BI系统,白天工作时段大量查询集中;对外API,凌晨流量低、白天有波峰。突发流量对队列和弹性扩缩容的要求就高,这时候入口层的排队和限流能力比模型本身的性能更关键。
第二个特征是请求的多样性。是大模型文本生成这种长任务,还是向量检索、图像分类这种短任务?长任务占用的显存和计算时间是动态的,调度复杂;短任务则更强调周转率。一个服务里如果混着两种极端任务,很容易互相拖累。
第三个特征是输入输出长度的分布。同样一个7B模型,输入20个token和输入2000个token,prefill阶段的计算量和显存占用差着数量级。如果业务里既有短输入又有长输入,又不做长度控制,批处理很容易被长请求拖垮整体延迟。这时候架构上要么限制最大长度,要么做长度分桶,要么用连续批处理动态调配。
所以看到并发这两个字,别急着上Kafka、上K8s,先把自己业务的请求特征摸清楚。架构设计的第一步永远是理解业务模型,而不是先选技术栈。
2. 拆开看:并发推理架构的三个关键模块
2.1 请求入口层:排队、限流与超时
先说入口。很多同学会觉得入口层无非是加个网关,请求转发到模型服务就完事了。但真正的并发推理,入口层至少要承担三件事:排队、限流、超时控制。
排队的意义在于平滑流量尖峰。GPU是最稀缺的资源,不可能每个请求来了都立刻计算。当瞬时请求量超过服务处理能力时,先把请求放进队列,按顺序一个一个喂给后端,总比把所有请求都压到模型层让显存直接爆掉要强。队列可以基于内存,也可以基于Redis或消息队列。内存队列延迟低、逻辑简单,适合单机场景;Redis Stream、RabbitMQ这类消息队列适合多实例场景,能保证请求在实例之间均匀分配。
限流则是保护系统的另一种方式。常用令牌桶或者漏桶算法,给每个用户或每个调用方设定一个配额。比如一个账号每秒最多10次调用,超出的请求直接返回429,避免某个业务方写了个死循环把你的推理集群打挂。这个在接入外部客户时尤其重要,没有限流就等于把系统资源对所有人敞开,线上事故往往就是这么来的。
超时控制往往被忽略。模型推理是个“可能很快也可能很慢”的操作,同一个请求在排队3秒和排队30秒之间,用户体验完全是两回事。架构上应该对队列等待时间、单次推理时间都设定超时上限,超时直接返回错误或者走降级逻辑。很多生产事故的连锁反应,就是从某个请求一直占着slot不放开始的。入口层能做的,是把那些注定没救的请求尽早掐掉,把资源让给能正常完成的请求。
2.2 调度层:动态批处理才是吞吐的命门
进入模型服务内部,调度层是并发推理架构最核心的部分。这里最大的坑,是很多人以为并发推理就是把请求攒成一个batch一起算,攒得越多吞吐越高。早期框架确实是这么干的,比如静态batching:等队列里攒够N个请求,或者到时间阈值,就组成一个batch丢给GPU。这套逻辑在短任务上能用,但放到大模型文本生成上就非常浪费。
原因还是prefill和decode这两个阶段。静态batching在生成时会卡在“最慢的那一个请求”上,只要一个请求还在慢慢decode,整个batch的GPU资源都得陪着它耗。一个batch里5个请求,4个已经生成完了,只有1个还在生成,剩下的显存和算力全浪费了。这种模式下,并发越多,浪费越多。
后来业界主流的做法是连续批处理(continuous batching),代表的实现就是vLLM的PagedAttention。它把批处理粒度从“请求级别”降到了“token级别”。一个请求的某个token生成完了,立刻给同一批里的其他请求分配计算资源;请求之间不互相等。调度器可以在每一步迭代时动态决定哪些请求继续算、哪些请求已经结束、哪些新请求可以插进来。效果就是GPU的空闲窗口被大幅压缩,吞吐能翻好几倍。我实测同一个7B模型,在静态batching下吞吐大约只有连续批处理的30%左右,差距就是这么明显。
调度层还有一个很关键的隐藏主题:KV cache。大模型生成时,每算一个token都要缓存历史token的Key和Value向量,缓存大小随并发数和上下文长度线性增长。vLLM把KV cache做成分页式管理,像操作系统的虚拟内存一样,解决了显存碎片和预分配浪费的问题。所以选型的时候,KV cache管理能力几乎可以直接决定一个推理框架的上限。如果你的架构是自己搭的,这一块一定要想清楚,否则并发一上来,显存碎片会让你痛不欲生。
2.3 执行层:显存和计算怎么伺候好
调度层把请求分配好之后,执行层要解决的是如何在GPU上高效完成计算。
显存是绕不开的第一道坎。一个模型能不能上某张卡,先看权重占多少显存。拿7B模型举例,FP16精度下权重本身约14GB,加上激活值、CUDA context以及其他开销,单卡24GB显存勉强能跑,但要留出KV cache的空间就比较紧张了。如果切到INT4量化,权重能降到4GB左右,同样的卡能扛的并发量就完全不一样。后面我会单独讲怎么算这笔账。
第二个问题是计算特性不同。prefill阶段是计算密集型,适合把batch加大,让GPU算力跑满;decode阶段是访存密集型,瓶颈在于显存带宽,算力再高也快不起来。不少推理框架会针对这两个阶段采用不同的调度和优化手段,比如把prefill和decode拆到不同的GPU池子里,或者使用不同的kernel实现。对中小团队来说,自己实现太复杂,用成熟框架的默认策略就够了。
第三个问题是CPU和GPU的协作。GPU计算速度快,但数据要从CPU侧拷贝过去,如果输入预处理、tokenizer、结果后处理都串在GPU计算链路里,很容易出现GPU利用率忽高忽低的情况。实操上,预处理和后处理应该用异步线程池提前做好,让GPU侧尽量不等待数据。我在压测时见过一个项目,GPU利用率只有30%,排查下来发现是tokenizer在请求处理的临界区里卡了小一半时间。这类问题不跑压测根本看不出来。
3. 从零搭一套并发推理服务:选型、算账与压测
3.1 框架选型:别一上来就造轮子
接下来聊实操。如果你准备自己搭一套并发推理服务,我的建议是:先别急着造轮子,站在别人的肩膀上把业务跑通,再考虑替换。目前主流的方案就几个。
vLLM是当前大模型推理里最常用的开源框架,核心优势是continuous batching和PagedAttention,自带OpenAI兼容API,部署方便,吞吐表现在开源方案里属于第一梯队。如果你做的是LLM文本生成,没有特殊算子,直接用vLLM是最合适的选择。TensorRT-LLM是NVIDIA官方的优化方案,能做到极致的低延迟和高吞吐,但要求把模型编译成TensorRT engine,部署链路长,对Ops团队的要求也高。Triton Inference Server则是NVIDIA推出的通用推理服务框架,不绑定模型类型,可以做动态批处理、并发模型执行、模型集成(ensemble),适合同时部署多个不同模型、要把LLM和CV模型混在一起对外服务的场景。
如果你只是原型验证、内部工具、并发量很低,直接用FastAPI写个简单服务也可以。但我要提醒一句:不要拿FastAPI硬扛大模型长文本生成。它的请求处理模型是基于线程池的,长任务会很快耗尽线程,导致后续请求全部排队,真正的吞吐远不如专门的推理框架。我自己的习惯是:LLM场景,单模型单卡,用vLLM;多模型混合部署,用Triton;需要极致性能且有专门人力,再上TensorRT-LLM。选型不是越高级越好,而是匹配团队现有能力和业务需求。下面这张表是我常用的选型对照。
| 框架 | 核心优势 | 主要缺点 | 适用场景 |
|---|---|---|---|
| vLLM | 连续批处理、PagedAttention、吞吐高 | 对自定义算子支持有限 | 单模型LLM高并发 |
| TensorRT-LLM | 极致延迟与吞吐、NV深度优化 | 编译复杂、调试困难 | 有专人的极致性能场景 |
| Triton | 多模型管理、动态batch、不绑模型类型 | 本身缺少continuous batching | 多模型混合部署 |
| FastAPI手写 | 灵活、上手快 | 长任务会耗尽线程 | 原型验证、极低并发 |
3.2 算清显存和并发量的账
这部分是很多人最容易拍脑袋的地方。我见过直接把max_num_seqs改成256,然后等着OOM的;也见过并发设成2,GPU在那吃灰的。算账的方法其实不复杂,关键是要理解显存都花在哪。
第一块是模型权重。以FP16为例,一个参数量为P的模型,权重显存大约是2P字节。7B模型约14GB,13B约26GB,70B约140GB。如果用INT8,大概减半;INT4再减半。量化是省显存最直接的手段,但会带来一定的精度损失,需要根据业务验证。第二块是激活值和临时计算缓存,这个和输入长度、batch大小有关,数值波动大,通常预留几GB。第三块也是大头,KV cache。粗略估算公式是:KV cache大小 = 层数 × 隐藏层维度 × 2(K和V两个向量) × 序列总长度 × 字节数。
以Llama 2 7B为例,32层、hidden size 4096,每个token每层需要4096 × 2 × 2字节 = 16KB,乘上32层就是512KB/token。如果并发16个请求、每个请求上下文平均2000 token,KV cache大约需要512KB × 32000 = 16GB。这样一看,24GB的卡跑7B模型,留给KV cache的空间并不多。所以在vLLM这类框架里,max_num_seqs(最多同时处理的序列数)和max_model_len(最大上下文长度)不是越大越好,而是要和显存预算对齐。最稳妥的方法是把模型权重和预留激活值算清楚,再用剩下的显存除以单个序列在最大长度下的KV cache开销,得出并发上限。然后再根据业务要求的QPS和延迟,决定是把并发尽量做大,还是限制并发保证单请求速度。
还要提一个容易被忽略的点:GPU型号不同,显存带宽和算力差异很大。A10G这种偏推理的卡,24GB显存但带宽只有600GB/s左右,A100 80G带宽超过2TB/s。同样跑7B模型,decode速度能差出两到三倍。如果你追求低延迟,卡的选型比框架选型影响更大。
3.3 压测与调参:跑一轮完整压测
架构搭好之后,不管参数算得多准,都要靠压测验证。我用的是vLLM加JMeter的组合,先说怎么快速起一个服务。假设你有一张24GB显存的卡,要用vLLM部署一个7B模型:
docker run --gpus '"'"'device=0'"'"' \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model /path/to/model \ --max-model-len 4096 \ --max-num-seqs 8 \ --gpu-memory-utilization 0.9这里--max-model-len控制最大上下文长度,--max-num-seqs控制并发序列数,--gpu-memory-utilization告诉框架最多用多少比例的显存做KV cache缓存。这几个参数直接影响并发上限,压测时优先调它们。压测工具方面,JMeter加Thread Group就能模拟并发请求,核心是观察两个维度的数据:一个是服务端日志和指标,比如TTFT、TPOT、每个请求的生成token数;另一个是GPU侧的状态,nvidia-smi的显存占用和利用率。光看“接口有没有报错”是远远不够的。
我举一个实测例子。用7B模型跑一批单轮问答,请求输入约500 token、输出约200 token,max-num-seqs设成8。并发50个用户持续压测,前两分钟表现不错,用户逐渐增加到100的时候,TTFT从1.2秒涨到4秒,同时GPU利用率冲到95%,显存接近极限,日志里开始出现个别超时。这时候的处理思路不是简单扩大并发,而是把--max-num-seqs降到6,让单个请求获得更多计算资源,TTFT回落到2秒以内,整体吞吐反而因为重试减少而稳定了。压测这件事最忌讳凭空拍参数,一定要用数据反推。改一个参数,重新压,看三个指标怎么变,反复几次,你就能找到这个模型在这张卡上的最优工作点。
3.4 单卡扛不住时:分布式是最后一步
单卡并发上限顶到天花板之后,才轮到分布式架构出场。大模型推理的分布式方案主要有两种:张量并行(Tensor Parallelism)把模型切到多张卡上,适合单模型超大的情况;数据并行或者多副本则是一模一样的模型多份部署,再用负载均衡分发请求。
70B模型单卡根本塞不下,至少要用TP并行到2张或4张80G卡。多副本则更适合并发量上来的场景:一张卡的并发上限到了,加副本水平扩展即可。此时网关的负载均衡和队列设计就成了系统瓶颈,可能需要引入K8s这类集群调度。但请记住,分布式是最后的扩容手段,不是第一方案,先把单卡压测吃透再说。很多团队一上来就追求分布式,结果连单卡参数都没调明白,分布式只会把问题放大。
4. 常见故障与排查技巧实录
4.1 显存OOM:先分清是权重还是KV cache
先讲最头大的显存OOM。症状一般是两种:进程直接被系统杀掉,或者CUDA报错out of memory。前者往往是显存超得太多,连driver都被拖崩;后者是在某个时刻显存分配失败。排查的第一步永远是看nvidia-smi,确认是权重层占了大头,还是KV cache吃满了。
多数情况下,KV cache超限是主因。改动优先给vLLM这类框架调低--max-num-seqs和--gpu-memory-utilization,或者限制max-model-len。另外,量化是治本方案,7B模型INT4量化后权重只要4GB左右,能腾出大量空间给KV cache。还有一个隐蔽的原因:显存碎片。模型长时间跑,分配和释放交替进行,显存碎片化严重时,即使总量够,也会分配失败。vLLM的PagedAttention很大程度上解决了这个问题,但如果你用的是自研方案,记得加上显存池复用。
4.2 延迟飙升与队列积压:先看队列再看GPU
高并发下最常出现的现象是延迟飙升,TTFT从1秒涨到8秒,伴随客户端大面积超时。很多人的第一反应是GPU性能不行,其实先要看队列。如果请求量已经超过系统的处理能力,再牛的GPU也会排队。此时队列深度在快速增长,而GPU利用率往往已经打满。
对策从三方面考虑:入口限流,不要再放新请求进来;横向扩容,加GPU实例分担;内部优化,比如增大batch、开启continuous batching、调整并发上限。如果你用的是单实例服务,扩容前可以先做一件事——看看是不是有长尾请求堵住了批处理。有些框架默认按请求到达顺序调度,一个超长输入的请求可能占据大量计算资源,短请求全在后面排着。这时候调度策略改成按长度或者按优先级调度,效果立竿见影。
4.3 多模型共享GPU:隔离与配额缺一不可
很多团队会遇到一个场景:一张卡上既要跑LLM,又要跑一个图像识别模型。图省事直接两个进程各占一半显存,结果LLM压测的时候把显存吃完,图像推理进程直接OOM。多模型共享GPU,最稳妥的方式是用能感知显存和调度的框架,比如Triton Inference Server,它支持在同一进程内管理多个模型实例,并为每个模型配置显存和并发度。NVIDIA的MIG技术也能把物理GPU切成多个独立实例,隔离性最好,但只有A100、H100这类卡支持。实在要用进程隔离,也要在容器层面加上显存配额和资源限制,别让一个进程把整个GPU吃光。至于CPU和内存资源,同样需要限制,别让模型服务把机器整体拖垮。
| 症状 | 常见原因 | 处理优先级 |
|---|---|---|
| CUDA OOM | KV cache超限 / 显存碎片 | 降并发、限长度、加显存池 |
| 进程被杀 | 显存超物理上限 | 减模型尺寸、量化、限实例 |
| TTFT飙升 | 队列积压 / 长尾请求 | 限流、切连续批处理、扩容 |
| GPU占用率低 | CPU侧预处理阻塞 | 异步化tokenizer与前后处理 |
| 多进程互杀 | 显存配额未隔离 | 上Triton/MIG、容器限显存 |
实话说,并发推理架构做到最后,拼的不是花哨的框架,而是对业务请求特征的理解和对显存、延迟、吞吐这三本账的熟悉程度。我早期犯过的最大错误,就是总想着上更复杂的框架、堆更多的并发,结果把系统调得越来越脆弱。后来学乖了,每次都是先把业务跑通,用压测数据说话,再一步步优化。如果你正在做类似的事,建议也从最小可用的架构开始,先把线上跑稳,再谈极致性能。最后再分享一个小技巧:不管用什么框架,把每次压测的参数、指标和改动都记录下来,半年后你会感谢这份记录。