ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LLM分布式推理实战:显存账本、并行策略与连续批处理

LLM分布式推理实战:显存账本、并行策略与连续批处理

1. 单机装不下之后:LLM推理分布式化真正要填的三个坑

先说个反直觉的现象:同样一台八卡机器,跑训练可以舒舒服服,但真要上线对外提供LLM推理服务,并发稍微一上来就崩。原因其实很简单——训练是离线任务,可以排队、可以容忍慢,推理却是实时生意,一个请求进来,用户盯着屏幕等token,慢了就流失。很多人第一次设计LLM大规模分布式推理系统时,犯的最大错误就是把训练那套分布式思路原样搬过来,结果被压测数据狠狠教育了一轮。

所以先把问题定义清楚:分布式推理到底在解决什么?我理解下来不外乎三个坑:显存放不下、单卡算不过来、并发吞吐顶不住。这三个坑互相纠缠,光解决一个没用。

1.1 显存不是"权重放得下"这么简单

先说最直观的显存问题。一个70B参数的稠密模型,FP16精度下光权重就是70×2=140GB,单张80GB的卡根本放不下。于是很多人第一反应是"那就上多卡"。但注意,模型权重只是显存账单里的一小部分。推理时还得算激活值,还得给每一条正在生成的请求维护一份KV Cache。

KV Cache这个东西,几乎所有做过LLM推理工程的人都被它坑过。它本质上是Transformer注意力计算过程中的中间状态缓存:每生成一个token,都要拿当前token的Query去和前面所有token的Key做点积,然后按权重求和Value。如果不缓存Key和Value,每次都得从头把整个序列重新算一遍,那代价是O(n²)级别的。缓存了之后,每过来一个新token只需要O(1)级别的新增计算,但代价是显存里要一直占着一块地方。

我来算一笔账。假设一个中等规模的模型,32层Transformer,隐藏维度4096,没有做分组查询注意力,KV Cache按2字节的FP16存储,那么每个token大约要占2×32×4096×2=512KB显存。如果线上平均每个请求的序列长度是2048个token,每个请求光是KV Cache就要占掉1GB左右。再乘上并发量——1000个并发请求就是1TB。这个数字一出来你就明白了:KV Cache才是分布式推理里真正的大头,模型权重反而是一次性投入。

所以"显存放不下"这个问题,不是简简单单把权重切到多张卡就结束了。你得同时考虑权重、激活值、KV Cache、以及为了并行计算而保留的冗余显存,这四个加起来才是真实的显存需求。

1.2 推理的延迟敏感决定了架构不能照搬训练

训练分布式讲究的是总吞吐,batch size可以开得很大,数据在卡之间多绕几圈问题不大,反正离线。但推理不一样,用户请求是有SLA的,通常看P95甚至P99延迟。一个请求从进入网关到第一个token出来,中间每一跳网络、每一次序列化反序列化、每一个排队等待,都在往延迟上叠加。

这也是为什么推理系统往往不愿意做太深的网络拓扑:节点越多,通信越频繁,延迟越难控。一个常见的误区是"我训练的时候用了张量并行,推理也照抄就完事了"。训练时张量并行每一步都要做all-reduce,卡间通信时间可以藏在前向计算里;推理时每一步也是同步的,通信成本直接暴露在延迟路径上。当你把张量并行度从2提到8,理论上单卡显存压力减到四分之一,但通信开销可能让每一步变慢两倍不止。这就是为什么很多L生产系统里,有人反而宁愿用流水线并行甚至单卡推理,也不愿意把TP度拉得太高。

所以设计分布式推理系统的第一原则是:每一项分布式技术都是拿延迟换显存、拿通信换容量,没有免费的午餐。接下来这几节,我会沿着一条请求在集群里的完整路径,把每个环节为什么要这样做、怎么做、坑在哪里,逐一讲清楚。

2. 一条请求的完整旅途:从接入网关到KV Cache显存账本

要理解分布式推理系统,最好的方式不是看架构图,而是跟踪一条真实请求。假设你刚部署好一个16卡集群,某个用户发来一句"帮我写一份周报提纲",这句话接下来会走一条非常长的路。

2.1 网关与路由:请求不能落到一张满载的卡上

首先请求到达的不是模型,而是一层接入网关。这层网关不负责算token,它的职责有点像公司前台:确认请求合法、拆包、检查参数,然后决定这个请求应该交给集群里的哪一组模型实例。

这一步在单机推理里根本不存在,但在分布式里极其重要。因为集群里每张卡的负载情况是动态变化的:某张卡可能正在处理一个超长文本的续写,显存快满了;另一张卡刚清完一批请求,闲得发慌。如果网关不做任何判断,简单轮询分发,很快就会出现"忙的忙死、闲的闲死"的局面。

我们内部某次压测就踩过这个坑。当时网关只是简单地把请求按连接数均分到四路后端,结果其中一路后端正好被几个长尾请求卡住,P99延迟直接飙到十几秒。后来改成基于实例负载的路由策略:每个推理实例定期上报自己的排队长度、当前KV Cache占用率、最近一秒的吞吐均值,网关根据这些信息选一个"最闲"的实例下发。道理很简单,但效果立竿见影,P99降了一个数量级。

2.2 KV Cache的记账与显存碎片

请求进入某一组模型实例后,系统会为它初始化KV Cache。这里首先要回答一个问题:到底提前给每条请求分配多少显存?

如果按最大序列长度提前分配,比如一律预留32K token的KV Cache空间,那显存利用率会低得可怕。因为绝大部分请求写完几百个字就结束了,根本用不到32K。但如果按需动态分配,又会带来另一个问题:显存碎片。

KV Cache是按请求维度持续增长的,每条请求每多生成一个token,就要多分配一块显存。你想想,同时有几十上百条请求在生成token,每条请求都在不同时间点要新显存,显存分配器要频繁地从空闲块里切一小块出来。用久了,空出来的显存会碎成很多小块,大块连续空间越来越少,新请求可能要的连续显存反而分配不出来。这跟操作系统里的内存碎片一模一样。

业界后来普遍采用的做法是分页式KV Cache管理。思路也很直接:把KV Cache分成固定大小的块,按需申请,请求的KV Cache在物理上不要求连续,逻辑上维护一个块索引就行。这跟你电脑上的虚拟内存差不多——逻辑地址连续,物理地址可以散落在各处。这套机制极大地缓解了碎片问题,也让批处理能够更灵活地动态组合。你不一定需要自己实现这么底层的东西,很多现成的推理引擎已经内置了类似机制,但要理解它的存在意义,否则调参时看到"KV Cache利用率只有40%"这种指标都不知道从何下手。

2.3 预填充与解码:同一个请求,两个完全不同的阶段

一条请求进入真正的GPU计算之后,会经历两个阶段:预填充和解码。

预填充阶段,系统要把用户输入的整段文本一次性算一遍,得到每个token的Key和Value缓存。这个阶段是典型的计算密集型,矩阵乘又大又密集,GPU利用率能跑得很高,但总耗时会随输入长度线性增长。解码阶段则是逐token生成的,每生成一个token才做一次前向计算,算力需求小,但从第一个token到最后一个token的墙钟时间很长。

这两个阶段的资源需求完全不同,一个吃计算、一个吃延迟。所以在分布式推理系统里,调度器要做的最重要一件事就是在预填充和解码之间合理分配算力。如果所有请求都同时做解码,GPU算力利用率很低;如果某条请求带来超长的输入,预填充又可能把整张卡的计算资源抢光,让正在解码的请求卡住。后面第4节要讲的连续批处理,本质上就是在这两个阶段之间做精细化的算力调度。

3. 模型切分的三种姿势:张量并行、流水线并行与专家并行的取舍

聊完请求路径,接下来是分布式推理最核心的一个决策:模型到底怎么切到多张卡上。这里有三套主流切法,各有各的数学逻辑和应用场景。

3.1 张量并行:让多张卡一起算同一个矩阵,通信是命门

张量并行就是把一个层里的矩阵运算拆开,让多张卡各算一部分,然后用通信把结果拼起来。比如一个注意力投影矩阵是4096×4096,切成8份,每张卡算4096×512,算完合并。

张量并行的优势是显存压力分摊得很彻底,单卡只需保存原模型几分之一的权重和激活。劣势是通信极其频繁——Transformer的每一个block里都有多次all-reduce或all-gather,而且每生成一个token都要做一遍。所以张量并行度往往受限于节点内的卡间互联速度。我们都是尽量把TP限制在同一台物理机内,跨机的TP通信开销一般项目都吃不消。

经验值上,TP=8在8卡单机上通常是甜点区,跨两台机器的TP=16除非模型大到没办法,否则不太值得。一个直接的验证方法是看你自己的压测数据:把TP度翻倍之后,如果单步耗时上涨超过30%,那说明通信已经主导了计算,再往上加卡就是负优化。

3.2 流水线并行:切的是层,赌的是气泡占比

流水线并行按层切分,第1到第8层放在卡A,第9到第16层放在卡B,数据像流水线一样从一张卡流到下一张卡。它和张量并行比,通信次数少得多——每个token只在层边界做一次通信,但引入了一个新问题:流水线气泡。

打个比方,一条产线有4个工位,第一个工位加工完第一个零件后,后面的工位还在闲着等活。这种空闲时间就是气泡。流水线并行的气泡占比理论值和stage数量、micro-batch数量相关。stage越多,气泡越严重;micro-batch切得越细,气泡越小,但micro-batch切小又会降低单次矩阵计算的效率。

我的实际感受是,流水线并行最适合"卡间通信带宽一般,但单卡显存实在装不下"的场景。如果一张卡能装下整个模型,但推理延迟不达标,你优先该考虑的是换更强的卡或者加TP,而不是上PP。PP更多是用来解决"一张卡装不下"而不是"一张卡不够快"的问题。

3.3 MoE模型的额外维度:专家并行本质是路由问题

现在很多大模型走的是MoE架构,一个Transformer层里塞了几百个专家网络,每次推理只激活其中一小部分专家。这种模型天然适合另一种并行方式:把不同专家放在不同卡上,请求来了,由路由模块决定把token送到哪几张卡上的专家去。

听起来很美好,但工程上有个头疼的问题:负载不均衡。理论上每个token只会路由到top-k个专家,但实际上可能有某种 token 模式特别常见,导致某几个专家成了"爆款",流量全涌向那几张卡,其他卡闲着。所以MoE推理系统里一定会做负载均衡约束和冗余专家副本:把热门专家复制多份放到多张卡上,或者训练时就加辅助平衡损失。你如果自己搭MoE推理,第一件要检查的指标就是"专家利用率的方差",方差越大,资源浪费越严重。

3.4 切分之前先做显存算术题

不管选哪种并行,动手之前都应该先算一笔账。假设你的模型是70B,FP16权重需要140GB,KV Cache平均每请求1GB,你希望同时容纳256个并发请求,那么KV Cache就是256GB。再加上激活值、通信缓冲、以及为了并行计算留出的冗余,总显存需求粗算就是140+256+60≈456GB。如果单张卡80GB,六张卡480GB,也就刚刚够。

这个算术题看着简单,但很多人栽在"只算了权重没算KV Cache"上。我给的建议是:先做显存账本,再谈并行策略。把这个公式固化成脚本,每次模型版本升级、并发目标调整,重新算一遍,比任何经验都可靠。

4. 吞吐与延迟的钢丝绳:连续批处理、排队策略和缓存三板斧

模型切分解决的是"怎么把模型放上去",但真正让系统QPS上去的,往往是调度和批处理策略。这一节讲三件事:怎么把GPU算力榨干、怎么保证延迟不失控、怎么用缓存减少重复计算。

4.1 连续批处理的魔力:从"等最慢的人"到"来一个走一个"

传统批处理是静态的:攒够一批请求,一起算,等这一批全部算完,再接收下一批。问题在于批内的每条请求长度不同,解码速度也不同,短请求早就生成完了,却必须等最长的请求结束才能释放整批的资源。这就导致GPU在大量时间里是半空闲的。

连续批处理的核心思路是把"批"这个概念从请求级别下沉到token级别。每一条请求都在独立推进自己的解码过程,每当某条请求完成了,它占用的显存和计算资源立刻释放,让排队等待的请求插进来。系统不再等一整批结束,而是随时有空位随时补新请求。效果非常惊人:同样的集群,静态批处理的吞吐可能只有连续批处理的几分之一。

这个机制对系统的要求是:调度器必须能精细地控制显存分配和每个token的前向计算。这也是为什么之前提的分页式KV Cache很重要——没有灵活的显存管理,连续批处理就无从谈起。

4.2 排队策略与SLA:宁可拒绝,也不能让超时拖垮全局

高吞吐和低延迟本质上是一对矛盾。你把batch size开到很大,吞吐上去了,但每条请求排队等待的时间也变长了。所以系统必须有明确的SLA设计,我的经验是分三条线:

  • 队列有上限:排队中的请求数超过阈值,直接拒绝新请求,返回"系统繁忙",而不是让它们一直排队耗到超时。
  • 优先级分级:把请求分成几档,比如实时交互档、异步批处理档,调度器优先处理高优先级请求。
  • 超时熔断:每条请求设置了最大等待时间,超过就释放资源。你可能会担心误杀正常请求,但我见过太多系统是因为没有超时熔断,被积压请求活活拖死的。

这三条线听起来简单,但真正落地时要和连续批处理配合好:调度器每轮挑选下一批要计算的token时,不能只挑新的请求,也要保证已经在跑的请求不会被饿死。

4.3 前缀缓存与投机解码:用工程手段省算力

最后两招属于"进阶操作",但收益很大。

第一招是前缀缓存。很多请求共享公共前缀,比如一个企业里的所有续写请求都以固定的系统提示词开头。如果每来一个新请求,系统都从头计算这些共享前缀的KV Cache,那就是巨大的浪费。我们可以在显存里维护一个前缀→KV Cache的映射表,新请求来了先做最长公共前缀匹配,命中就直接复用缓存。注意,这要求Cache的淘汰策略要合理,否则缓存本身会成为新的瓶颈。

第二招是投机解码。它的想法是:用一个更小的草稿模型先快速生成几个候选token,然后由大模型并行校验。如果草稿模型的推测准确率够高,大模型一步就能推进多个token,解码阶段的延迟就下来了。这个方法在有些场景下实测能把解码吞吐提升两倍以上,但前提是你得有一个质量过硬的草稿模型,而且草稿模型本身就占了额外的显存和算力。小型团队先别急着上,优先把批处理和显存管理做好更实际。

5. 真机集群的暗坑:慢节点、热点请求与超时抖动的排查复盘

理论再多,真机上一跑全是意外。这一节我复盘一次真实的排查经历,涉及的三个问题几乎是每个分布式推理集群都会遇到的。

5.1 慢节点:一张卡让整个集群陪跑

某次上线后,我们监控发现集群的平均token生成速度没问题,但P99延迟经常出现刺状尖峰。一开始以为是网问题,查了交换机端口流量,正常。后来逐卡看指标,发现有一张卡的算力利用率明显低于其他卡,但任务一直有。进一步查才发现那张卡的散热出了问题,运行一段时间后降频,单步推理慢了30%。

问题就出在这里:分布式推理的同步机制意味着每生成一个token都要等最慢的那张卡算完。一张卡慢30%,整个pipeline就慢30%,而且它不影响平均指标,只会把尾部延迟拉高。这次教训之后,我们把"卡级性能指标"纳入了基础监控,另外在网关层面加了慢节点自动隔离逻辑:某张卡连续N个时间窗口性能指标异常,就把新请求调度到其他实例上,避免它继续拖累全集群。

5.2 热点请求:一条超长文本把显存账本烧穿

另一个典型问题是长文本请求。普通请求几百个token就结束了,但偶尔会出现一个需要续写几万token的长任务。它就像一个"显存黑洞",KV Cache持续膨胀,把一张卡甚至整个实例的显存都占了。更麻烦的是,如果这个长请求正好命中了某个热门主题,它产生的长序列还会继续在解码里占用算力,导致同时段其他请求大量排队。

我们第一次遇到这个问题时,直接表现为:某实例的显存使用率突然飙到95%,然后OOM,整组实例重启,所有连接断开。后来做了两个改进:

  • 按请求序列长度做分级路由。超长请求单独走一条通道,限制并发数量。
  • 给单条请求的解码进度设限。超过最大生成长度之后,自动截断收尾,而不是让用户无限续写。

这两个策略加上之前的显存账本,基本就把热点请求带来的风险控制住了。

5.3 一次完整的排查链路复盘

最后把整个排查过程串一遍,方便你以后遇到类似情况时有个参考思路。

当时现象是:业务方反馈某些用户明显感觉变卡,我们调了网关日志,发现异常的请求都集中在某一台后端实例上。于是第一步,看实例的QPS、耗时、显存三条曲线,发现显存曲线在异常时间段内持续爬升,QPS并不高。第二步,抓一条慢请求的详细trace,发现它在预填充阶段耗时正常,但在解码阶段每生成一个token的间隔越来越大。第三步,定位到这批慢请求的来源,发现都携带了超长上下文。第四步,结合显存指标反推,确认是长请求的KV Cache把显存顶满,触发了缓存的淘汰和重新计算,导致每个token都要做额外的重算,越算越慢。

整个过程其实不复杂,但顺序很重要:先看流量入口,再看实例资源,最后看请求内容。如果一开始就去查代码逻辑,可能绕很多弯路。

6. 能跑起来的最小架构,以及一条不太会走弯的演进路线

最后一节,聊一聊如果你现在接手一个新建项目,准备从零搭一套LLM大规模分布式推理系统,应该从哪里开始。

6.1 一套可落地的最小架构

我建议的最小架构包含六个组件,缺一不可:

组件职责关键选型考量
接入网关鉴权、负载均衡、超时熔断必须支持按实例负载路由
调度器请求排队、批处理调度、显存记账和推理引擎深度绑定,最好选同一生态
推理引擎模型加载、前向计算、KV Cache管理优先看显存管理和批处理能力
前缀缓存公共前缀KV Cache复用定时清理,避免缓存穿透
监控系统卡级指标、请求耗时、显存趋势必须能看到逐卡指标
配置中心模型版本、并行策略、路由策略支持热更新,跑批时不重启集群

前三个组件是主链路,后三个是支撑。很多人一开始只关注主链路,把监控和配置中心当成"以后再说",结果上线第一天就抓瞎。我强烈建议把这六块一起做,哪怕是先用最简方案,也要把数据通路打通。

6.2 演进的三条路线

架构跑起来之后,会面临三个方向的演进压力,按性价比排序是:

  • 同步转异步:一开始所有请求都是同步等结果。随着业务量上来,你会发现很多任务并不需要实时响应,把这些任务改成异步提交、结果回调,能显著降低主链路的排队压力。
  • 多集群资源池化:单集群扩容总会有上限,而且不同业务的推理负载峰值往往错峰。把多个集群的资源统一到一个池子,按业务优先级动态分配,资源利用率能再上一个台阶。
  • 跨地域部署:用户分布广时,把推理实例部署到离用户近的地方能大幅降延迟。但这一步涉及数据同步和容灾,复杂度最高,建议放到最后。

我在实际项目里见过的失败模式,几乎都是"一上来就追求大而全"。分布式推理系统的复杂度是逐步累积出来的,先把一条请求的路径跑顺,再往上加并行策略、加缓存、加多集群调度,每加一层都要有监控数据支撑,这样的节奏最稳。

如果非要总结一句个人经验,那就是:不要迷信并行度数字,要迷信显存账本和监控曲线。分布式推理的所有性能问题,最后都能从显存分布和逐卡耗时曲线里找到答案。把这个基本功练扎实,再大的模型集群也不会把你难倒。

返回列表