
前阵子有个朋友找我诉苦他们组里躺着几十张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. 先看清瓶颈再谈优化推理任务为什么经常喂不饱GPU2.1 Prefill和Decode是两种“性格完全不同”的任务要理解硬件调度必须先接受一个事实大模型推理不是一个匀速过程它由Prefill和Decode两个阶段组成而这两个阶段对硬件的需求几乎是相反的。Prefill阶段你要把用户输入的几百上千个token整体做一次前向计算。这个阶段并行度极高可以一次性做大量的矩阵运算是GPU最喜欢的活算力利用率可以拉得很高。但代价是显存占用大因为要缓存所有层的中间状态用于生成KV Cache。Decode阶段则完全不同。每生成一个token都要把之前所有的KV Cache读出来做attention计算然后写入新的KV Cache。一次只算一个token或者一个批次里有限几个token并行度低计算量小但访存开销一点不少。所以Decode的吞吐上限很大程度上由显存带宽决定而不是由算力决定。A100的HBM带宽大约是2TB/sH100能到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利用率之外还有TTFTTime To First Token从请求发出到第一个token返回的时间反映调度和Prefill综合效率。TPOTTime 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分离。每次改动都要有监控数据和压测结果支撑记录清楚改了哪些参数、效果如何。时间一长你手里就会有一张完全属于你们服务特征的“性能调优地图”再遇到新问题看一眼地图就能知道缺哪块拼图。这也是我写这篇文章的原因。算力过剩的时代真正决定推理体验的不是谁手里的卡多而是谁能把已有的卡调度得更聪明。希望我的这些踩坑和思考能帮你少走几段弯路。