算力过剩但推理慢?大模型硬件调度是破局关键
2026/9/9 9:50:36 网站建设 项目流程

前阵子有个朋友找我诉苦:他们组里躺着几十张A100,账面算力看着非常充裕,可线上一个对话模型的推理延迟就是压不下来,首token延迟经常飙到好几秒,用户投诉不断。他很困惑——算力明明过剩,为什么推理还是跑不快?

这个问题的答案,恰恰藏在大模型推理和训练的本质差异里。训练看的是算力吞吐,追求单位时间吃掉多少样本;推理看的是延迟上限和吞吐的平衡,追求每个请求多快能回来。算力是“量”,延迟是“路径”,而连接两者的关键,是硬件调度。统计上你拥有一堆卡,和调度系统能不能在正确的时刻把正确的卡分给正确的请求,是两码事。

这篇文章我就从“算力过剩但推理慢”这个反直觉现象出发,把我对硬件调度演进的理解、主流框架的设计思路、以及实际部署中反复踩过的坑,系统性地梳理一遍。适合刚接触大模型部署的同学建立全景认知,也适合正在调优推理性能的同学对照排查。

1. “算力过剩”与“推理慢”并存的真相:你的GPU到底在忙什么

1.1 算力利用率不等于请求延迟

很多团队衡量GPU使用情况,习惯只看nvidia-smi里的利用率百分比。但这里有个极其常见的认知误区:GPU利用率高,只能说明GPU没有闲着,不能说明算力被用在了“让你更快看到结果”这件事上。

推理过程中有两种截然不同的计算阶段。Prefill阶段处理用户输入的提示词,是典型的计算密集型任务,矩阵乘法吃满Tensor Core,这时候GPU利用率确实能跑得很高。但到了Decode阶段,模型开始一个token一个token地往外吐,每个token依赖前一个token的输出,天然串行,同时每个token要对整个KV Cache做读写,变成了访存密集型任务。实际跑7B左右的模型,Decode阶段的GPU算力利用率能到百分之十几就已经很不错了,大部分时间都在等显存带宽。

所以你会看到一种诡异的现象:GPU利用率60%甚至80%,但线上请求依然排长队。因为那60%的算力可能正在做低效的批量拼接、过大的KV Cache分配、或者为了一批不紧急的请求做密集计算。利用率是物理层面的指标,延迟是服务层面的体验,中间隔着一层调度策略。

1.2 静态算力充裕但动态资源不足

另一个容易忽略的点是,算力“过剩”往往指静态总量充足——你数了数手里的卡,发现很多卡的空闲率不低。但推理服务要处理的是动态到达的请求流,可能上一秒只有几个请求,下一秒突然涌进几百个。

静态资源充足意味着你买得起一堆GPU,动态资源不足意味着调度系统没办法在一瞬间把足够多的算力集中到突发的请求潮上。想象一个餐厅,后厨有很多厨师(算力),但前厅点单系统是手工传纸条(调度),高峰期厨师们要么闲着等纸条,要么集中在某一两桌猛炒,其他桌干等。换更大的厨房(加卡)解决不了根本问题,因为瓶颈在传纸条的方式。

1.3 调度才是算力和延迟之间的那座桥

那调度到底在做什么?简单说,它负责回答三个问题:模型参数放在哪几张卡上、请求进来之后去哪个设备执行、多个请求之间怎么共享同一批GPU资源。

这三个问题对应着硬件的静态布局、请求的动态路由、以及细粒度的计算编排。任何一个环节回答得不好,都会让“充裕的算力”变成“憋屈的算力”。我见过不少团队在下游环节花了大量精力调CUDA算子、换推理框架,结果性能提升并不明显,最后发现是最上游的调度策略把请求都挤到了同一张卡上,其他卡闲置着看热闹。这种问题不是换框架能解决的,得从调度架构上动手。

2. 先看清瓶颈再谈优化:推理任务为什么经常喂不饱GPU

2.1 Prefill和Decode是两种“性格完全不同”的任务

要理解硬件调度,必须先接受一个事实:大模型推理不是一个匀速过程,它由Prefill和Decode两个阶段组成,而这两个阶段对硬件的需求几乎是相反的。

Prefill阶段,你要把用户输入的几百上千个token整体做一次前向计算。这个阶段并行度极高,可以一次性做大量的矩阵运算,是GPU最喜欢的活,算力利用率可以拉得很高。但代价是显存占用大,因为要缓存所有层的中间状态用于生成KV Cache。

Decode阶段则完全不同。每生成一个token,都要把之前所有的KV Cache读出来做attention计算,然后写入新的KV Cache。一次只算一个token(或者一个批次里有限几个token),并行度低,计算量小,但访存开销一点不少。所以Decode的吞吐上限,很大程度上由显存带宽决定,而不是由算力决定。A100的HBM带宽大约是2TB/s,H100能到3.35TB/s左右。如果KV Cache的读写占用大量带宽,那么你能在一秒钟内生成的token数量,就是“带宽除以每个token需要搬运的数据量”。

2.2 显存墙:KV Cache正在悄悄吃掉你的资源

很多人规划推理服务的时候会算GPU显存,但只算了模型权重占了多少,忘记了KV Cache才是真正的“显存吞金兽”。

拿一个常见的中型模型来算:假设32层、每层8个KV头、每个头的维度是128,使用FP16存储,那么每个token的KV Cache大小大约是2乘以层数乘以KV头数乘以头维度乘以2字节,算下来大约是128KB。注意,这是每个token的量。如果并发请求数是100,每个请求的平均上下文长度是4000 token,那么KV Cache占用的显存大约就是100乘以4000乘以128KB,约48GB。

也就是说,你能支持的并发数和上下文长度,有时候不是由GPU算力决定的,而是由显存里的KV Cache能塞多少决定的。调度系统的任务之一,就是在这块有限的显存里做精细化管理。如果调度器不够聪明,给每个请求预留了过大的缓存空间,那能同时跑的请求就会变少;如果预留太小,又会出现缓存溢出、被迫重新计算Prefill的情况,反而拖慢整体。

2.3 队列等待:算力闲着和请求等着可以同时出现

再回到那个餐馆类比。厨师再多,如果点单系统只能同时处理一桌客人的菜单,那其他桌就只能干等着。GPU调度也存在同样的现象。

早期的朴素调度方式,是请求级别的并发控制:一个请求占满一整批资源,跑完再放下一个。如果一批最多8个请求,那第九个请求就得等前面某一批完全结束。更麻烦的是,批次里的每个请求长度往往不一样,有的请求3个token就回答结束了,有的要生成500个token。调度器只能等最短的跑完?还是等最长的跑完?当时很多实现是等整个批次全部结束后再释放资源,这就导致生成快的请求被生成慢的请求“绑架”,大家一起等,GPU算力没有被有效地利用起来。

这种场景下,算力看着是忙的,但你真正关心的那些请求其实在排队。这就是“算力过剩”和“推理慢”能同时成立的核心原因之一。

3. 从手工作坊到流水线工厂:大模型硬件调度的演进路径

3.1 早期朴素部署:一张卡装不下,两张卡不知道怎么分

大模型刚流行的时候,大家的部署方式非常原始。模型小一点的,比如7B参数,可以用FP16放在一张A100或者A800上跑。但一旦单个请求进来,整张卡就独占住了,吞吐非常低。模型再大一点,比如70B,一张卡显存放不下,就只能做模型并行,把不同的层切开放在多张卡上。

这个阶段的调度,基本是“人工调度”加“静态配置”。运维人员手动指定哪些层放哪张卡,请求进来之后按预设的路由规则分发。这种方式可以解决“能不能跑”的问题,但离“跑得好”差得很远。每张卡的利用率高度依赖请求的具体形态,某个请求如果一次性输入特别长的上下文,那张卡就会忙很长时间,其他卡只能等着。

我当时参与过一些早期项目的调优,印象最深的就是:为了把一个大模型塞进有限的显卡里,经常要手工调整切层方案,什么形状的切分对这个模型最好,完全靠一遍遍试,根本没有通用方法论。每次模型版本一更新,切分方案又要推倒重来。

3.2 静态并行策略:张量并行、流水线并行和它们的代价

后来业界逐步抽象出几种并行方式,核心思路都是把“一张卡干所有事”变成“多张卡分工协作”。

张量并行是把一层的权重矩阵切分成几份,分别放在不同GPU上,计算的时候通过AllReduce之类的集合通信把结果拼起来。这种方案能让非常大的模型跑起来,但通信开销不容小觑。每算一层就要做一次全量同步,跨卡通信频繁,如果机器之间的网络带宽不行,通信时间甚至可能超过计算时间。

流水线并行则是按层切,把模型的不同层放在不同的GPU上,像工厂流水线一样,第一张卡算完第一段,把中间结果传给第二张卡,第二张卡接着算。这种方式的通信频率比张量并行低,但存在“流水线气泡”问题——前几层在算的时候,后几层的卡在空等。

这些静态并行策略解决了“模型放不下”的问题,但没有解决“请求如何共享算力”的问题。本质上还是以模型为单位做资源划分,而不是以请求为单位做动态调度。从“硬件调度”的角度看,它解决的是横向扩展的问题,解决不了纵向利用率的问题。

3.3 动态批处理:让GPU像快递分拣线一样运转

真正的转折点,是业界把调度粒度从“模型”和“请求”进一步细化到了“迭代”(iteration)级别,思路演变成了现在大家熟知的Continuous Batching,国内一般叫动态批处理或连续批处理。

之前那种静态batch的做法,是把一批请求固定好,等这一整批全部跑完才换下一批。动态批处理的核心差异在于:每个迭代(step)结束之后,调度器都会重新审视全局请求队列,把已经完成的请求踢出去,把新到的请求插进来。这样每个迭代都在跑一个“当时最优”的批次,GPU的每一次前向计算都被最大化利用,不会出现“被慢请求绑架”的情况。

打个比方:静态batch就像一辆固定线路的班车,只有坐满才发车,车上的人必须一起到终点;动态批处理则像网约车拼车,随时有人上车下车,司机永远在往最顺路的下一站走。这一变化带来的吞吐提升,往往不是百分之几十,而是数倍。这也是vLLM这类框架能迅速火起来的核心原因之一。

3.4 前后文分离与更细粒度的调度:把两种任务拆开分别伺候

动态批处理解决了请求之间的共存问题,但Prefill和Decode这两种阶段打架的问题依然存在。想象一个批次里既有大量Prefill请求需要消耗大算力,又有不少Decode请求每次只想读一小段显存里的数据,GPU就会在两种模式间来回切换,算力忽高忽低,整体并不高效。

于是出现了新的调度思路:Pre/Decode分离,把Prefill和Decode两个阶段分给不同的GPU实例去执行。让算力强劲的卡集中处理Prefill,让高带宽的卡专注做Decode,两个阶段之间通过缓存和通信来衔接。这种架构在长上下文的场景下收益尤其明显,因为长上下文的Prefill会占用很久,如果和Decode混在一起,Decode阶段的实时性会很难保证。

这个方向的本质,是把“请求调度”进一步深化为“计算阶段调度”。调度器不再区分“哪个请求给哪张卡”,而是区分“请求的哪个阶段给哪张卡”。大模型推理的硬件调度,也随之进入了更精细的编排时代。

4. 当代主流推理框架的调度侧重点与实测感悟

4.1 vLLM:动态批处理和PagedAttention的普及者

vLLM是我个人在生产环境中用得最多的推理框架,也是把动态批处理和PagedAttention带给主流社区的最大功臣。PagedAttention解决的问题是KV Cache碎片化。它借鉴操作系统虚拟内存的分页管理思路,把KV Cache切分成固定大小的块,不要求物理连续,从而让显存利用率大幅提升。

实际配置的时候,有几个参数需要根据场景反复调。gpu_memory_utilization控制允许使用的显存比例,默认是0.9,听起来很省心,但如果你还要在同一张卡上跑别的进程,就得调低。max_num_batched_tokens控制单次迭代最多处理多少token,数值越大吞吐越高,但延迟也会相应上升。max_num_seqs控制并发序列数量,一般需要结合显存和延迟目标来权衡。

我踩过的一个坑是:把max_num_seqs调得特别大,以为能提升吞吐,结果显存被KV Cache占满,触发了频繁的显存交换,性能反而大幅下降。后来把并发调低、同时把gpu_memory_utilization适当调小,给KV Cache留了一些余量,整体吞吐才回到正常水平。这里的关键思路是:调度参数不是越大越好,而是要匹配你的请求长度分布和显存余量。

4.2 TensorRT-LLM:静态图优化的极致与局限

TensorRT-LLM的路子跟vLLM完全不同,它强调的是把模型编译成高度优化的静态图。由于图是固定的,它可以做很多预计算优化,比如把连续的算子融合成一个内核、提前规划好显存布局,在训练好的模型结构不变的情况下,性能上限非常可观。

但这套方案的精妙和局限在同一个地方:静态。一旦模型结构变了、输入形状分布差异极大、或者需要频繁动态调整并发策略,它的优势就会打折扣。适合的场景是模型结构稳定、工程团队有精力做深度优化的生产环境。小团队快速上线验证的话,工程量可能会让你头疼。

从我个人的经验看,如果请求模式比较固定、模型迭代不频繁,TensorRT-LLM确实能把硬件压榨得更狠;如果业务还在快速迭代,经常要换模型、改参数,vLLM这类动态性更强的框架反而更友好。选型的关键,是评估你的业务是在“追求极致吞吐”还是“追求灵活迭代”。

4.3 SGLang和LMDeploy等其他框架的调度亮点

SGLang值得关注的地方在于RadixAttention,它的核心思想是复用请求之间的公共前缀。比如多轮对话场景,后面的请求往往包含前面的历史,如果调度器能识别并复用已有的KV Cache,就能省掉大量重复计算。在多轮聊天、代码补全、Agent型应用等场景下收益极其明显。

LMDeploy是上海人工智能实验室开源的项目,核心优势是TurboMind推理引擎,在动态批处理和量化方面做了大量优化,对国产硬件适配也更积极。这些框架的出现,意味着调度优化的路线正在分化:有人继续挖掘动态批处理的潜力,有人另辟蹊径做前缀复用,有人在结构化和半结构化场景里做文章。没有绝对的王者,只有适不适合你的场景。

4.4 框架选型的一个重要经验:别一开始就追求“最强优化”

给刚入坑的读者一个非常诚恳的建议:别在项目起步阶段就陷入框架选型的纠结,更不要因为某个框架在某条评测里吞吐高20%就立刻切换生产环境。大模型推理的性能瓶颈往往是系统性的,框架本身的调度策略只占一部分。

我自己见过太多团队,花了两周时间把vLLM切到TensorRT-LLM,吞吐提升确实明显,结果后来发现模型结构要升级,又不得不花两周迁回来。先跑通一条完整链路,再通过监控数据判断瓶颈到底在框架、网络、数据加载还是业务逻辑,排查清楚了再决定要不要换框架。调度优化的第一步,永远是测量。

5. 调度优化之外的隐形变量:网络、监控与容量规划

5.1 网络拓扑和集合通信质量经常被忽略

很多人在单机多卡部署时容易忽略一个关键问题:你手里的8张卡,到底是怎么连起来的。同一个PCIe Switch下的卡和跨CPU访问的卡,通信延迟和带宽差距可以非常大。而张量并行这种方案对通信极其敏感,通信慢一截,整体性能能掉一大截。

在单机场景,建议先用nvidia-smi topo -m查看GPU之间的拓扑连接,尽量让通信频繁的并行组跑在NVLink或PCIe直连的卡上。在多机场景,机器之间的网络交换机和网卡配置同样关键。RDMA和RoCE这类低延迟网络方案,在大规模推理集群里几乎是标配。如果你的集群网络还在用普通的TCP栈,那通信开销会成为显性的天花板。

5.2 监控指标:只看GPU利用率会骗你

要优化调度,首先要能看懂监控数据。我最常看的几个指标,除了GPU利用率之外,还有:

  • TTFT(Time To First Token):从请求发出到第一个token返回的时间,反映调度和Prefill综合效率。
  • TPOT(Time Per Output Token):每生成一个token的耗时,反映Decode阶段和吞吐瓶颈。
  • 请求排队长度和平均排队时间:这个指标能直接反映调度器是否在“动态”地响应负载变化。
  • KV Cache使用量和显存预留:能看到调度器在做显存管理时是保守还是激进。

如果一个服务TPOT已经很稳定,但TTFT经常波动,那大概率问题出在Prefill调度或者排队策略上;如果TTFT和TPOT都高,但GPU算力利用率很低,那先查网络和通信路径上有没有瓶颈。不同指标组合能帮你快速缩小问题范围,比看着一张GPU利用率图表猜来猜去靠谱得多。

5.3 容量规划:并发量、上下文长度和目标延迟怎么定

容量规划是调度优化的前置输入。没有明确的目标延迟和并发量,调度参数就很难定得有依据。我建议按下面的思路去做估算:

首先明确你的业务对延迟的容忍度。比如在线对话场景,TTFT需要在1到2秒内,TPOT最好在每秒30到50个token以上。批处理场景则可以放宽很多,但更看重总吞吐。

然后估算峰值并发和平均上下文长度。这里注意,上下文长度不能只看用户的输入,还要把系统提示词、历史消息和可能的工具调用结果都算进去。上下文越长,KV Cache占用越大,能支持的并发就越低。

最后结合显存和算力反推需要的卡数。一个常用的粗糙公式是:单卡并发能力约等于“显存减去模型权重和预留显存的空间,再除以平均每个token的KV Cache大小乘以每个请求的平均token数”。算出来之后再乘以你的目标并发,就能得到需要多少张卡。

5.4 容量规划里最容易踩的两个隐蔽坑

第一个坑是只按“平均长度”规划,没有考虑长度波动。KV Cache占用跟上下文长度是正比关系,如果用户请求的长度方差很大,就会出现短请求浪费预留空间、长请求直接溢出显存的情况。溢出之后的处理逻辑,有些框架会重新计算KV Cache,代价极高,表现为某几个请求突然慢一个数量级。建议按P90甚至P95的下文长度来规划,而不是平均值。

第二个坑是忽略了“预热”过程。很多推理框架在启动时会一次性分配显存,如果你的显存规划卡得太死,启动时可能没事,但运行时KV Cache的块分配会频繁触发碎片整理和换入换出,性能剧烈抖动。我踩过这个坑之后,现在习惯预留10%到15%的显存缓冲,虽然看起来浪费了一点,但整体稳定性提升非常多。

6. 一次真实排查经历:从“加卡没用”到“调度参数背锅”

最后分享一个我觉得很有代表性的实际案例。有个团队给一个多轮对话机器人做推理服务,线上8张A800,模型是14B,框架用的vLLM。用户反馈高峰期经常卡顿,他们第一时间想到的是“算力不够了”,准备再买8张卡。

我帮他们排查时先看了监控数据,发现问题很反常:GPU利用率平均只有30%左右,但请求平均排队时间超过3秒。这说明卡其实大部分时间在闲着,但调度器没有把请求高效地送进去。

进一步看配置,发现他们把max_num_seqs设成了128,每个请求的平均上下文长度又特别长,导致KV Cache几乎耗尽,调度器在迭代之间频繁做显存的换入换出。每换一次,整卡的计算就得等内存腾挪完成,GPU利用率自然上不去,请求也排起了长队。

max_num_seqs调到64,同时把gpu_memory_utilization从0.9调到0.85,给KV Cache留出少量空闲块之后,排队时间几乎归零,TPOT降低了将近一半。整个过程没有加一张卡,纯粹是调度的参数配置不合理导致算力被“卡在了调度层”。那次之后,团队把监控面板重新搭了一遍,把TTFT、TPOT、KV Cache使用率、排队时间这些指标都接了进去,后续再有性能问题,基本都能用数据说话。

这个案例也让我更坚定了一个观点:大模型的推理优化,算力只是基础,调度才是决定用户体验的关键。很多团队一遇到推理慢,第一反应就是加卡,但如果不先搞清楚调度的瓶颈在哪里,加再多的卡也只是把“算力过剩”现象放得更大而已。

7. 给正在部署大模型推理的团队几个实际操作建议

7.1 先定好你要优化的指标

启动任何优化之前,先把目标量化。你要优化的是TTFT、TPOT、整体吞吐、还是单用户延迟?这四个指标之间往往存在相互制约。追求极致吞吐,单个请求的延迟必然会上升;追求极低延迟,并发能力就得下降。目标不清晰,后面所有配置参数的调优都会陷入“调了A损失B,调了B损失A”的死循环。

我建议业务团队和服务团队坐下来,一起定一个明确的SLO(服务水平目标),比如“P95的TTFT低于1.5秒,P95的TPOT低于30毫秒,在200并发下稳定运行”。有了这个数字,调度参数怎么调都有方向。

7.2 建立一套可复现的压测流程

我一直强调:不压测,不优化。压测的意义不只是看看能扛多少并发,更重要的是暴露调度策略在极限负载下的行为。推荐用真实请求日志回放的方式压测,尽量还原线上请求的上下文长度分布和到达模式。

压测时要观察的不只是最高吞吐,还要关注不同并发级别下的延迟曲线。很多推理服务在低并发下表现非常漂亮,一旦并发提升到某个阈值,延迟会断崖式上升。这个拐点就是你的调度系统能力边界,也是后续做弹性伸缩和容量规划的基准数据。

7.3 预留弹性空间,但别让弹性变成摆设

大模型推理的负载天然存在潮汐特征。白天线上交互频繁,晚上批处理任务增多。如果集群是静态分配的,就很难兼顾两个方向的资源需求。现在一些调度器支持GPU实例的弹性伸缩和优先级抢占,但配置起来需要仔细设计,不然低优先级任务把高优先级任务挤掉的事情会频繁发生。

我的经验是:把在线实时推理和离线批处理任务放在同一个集群里,通过调度策略让它们共享资源。高峰期实时任务优先抢占,低谷期批处理任务自动填充空闲算力。这样既不会浪费GPU,又不会牺牲在线服务的稳定性。前提是调度器支持可靠的优先级机制和你对资源配额有清晰的划分。

7.4 小步慢跑,一条链路一条链路地调

推理性能优化是一道系统题,涉及模型压缩、算子优化、调度策略、网络通信、存储IO多个层面,没有任何一个参数能一劳永逸地解决所有问题。我的经验是,每一次只动一个环节,压测验证,再动下一个。

比如先优化KV Cache管理,测完一轮;再调整动态批处理的并发参数,再测一轮;最后再考虑要不要换模型量化精度或做PD分离。每次改动都要有监控数据和压测结果支撑,记录清楚改了哪些参数、效果如何。时间一长,你手里就会有一张完全属于你们服务特征的“性能调优地图”,再遇到新问题,看一眼地图就能知道缺哪块拼图。

这也是我写这篇文章的原因。算力过剩的时代,真正决定推理体验的,不是谁手里的卡多,而是谁能把已有的卡调度得更聪明。希望我的这些踩坑和思考,能帮你少走几段弯路。

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

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

立即咨询