大模型推理性能瓶颈:显存墙、带宽墙与调度墙的硬件演进 算力经常过剩推理还是跑不快这个矛盾我跟不少做推理服务的朋友都聊过。去年我们内部搭过一套多卡推理集群监控面板上GPU利用率平均只有百分之十几显存却常年吃掉大半接口的P95延迟还是高得离谱。领导说“算力都空着怎么还这么慢”我盯着面板也没法马上解释。后来把问题一层层拆开才发现推理延迟的上限很多时候根本不取决于算力芯片有多少FLOPS而是被显存容量、数据搬运速度和调度策略这几道墙死死卡住。这篇文章就聊聊我对大模型推理硬件调度演进的一些个人思考结合本地部署、集群调度、推理引擎选型这些实战场景把背后的逻辑摊开讲清楚。1. 一个反直觉的现象GPU利用率不高推理延迟却降不下来先说一个我反复遇到的典型场景。你打开nvidia-smi去看GPU利用率可能只有20%但推理接口的响应时间已经到了两三秒。很多人第一反应是“算力过剩”觉得卡没跑满所以瓶颈应该不在GPU上。这个判断其实只对了一半卡的算力确实没跑满但造成延迟的恰恰不是算力本身而是显存、带宽和调度策略。1.1 我们常说的“算力过剩”到底在说哪个指标“算力过剩”这个词太笼统了。我们常常用峰值算力TFLOPS来衡量一张卡强不强觉得A100有312 TFLOPS的FP16算力跑个7B模型绰绰有余。但这个数字只是GPU理论上每秒能执行多少次浮点运算推理服务根本不可能把每一笔浮点运算都变成用户看到的token。推理任务的形态和训练完全不一样。训练可以把大量样本堆成超大batch让GPU的矩阵运算单元长时间满负荷运转所以训练场景下“峰值算力”和“实际吞吐”高度相关。推理却是交互式的用户发一段话过来模型要一个token一个token地生成回复。生成第N个token之前必须先产生第N-1个token串行依赖决定了它没法像训练那样疯狂堆并行度。这时候你会看到GPU的计算单元经常在等待等待权重和中间状态从显存被搬进计算单元。所以“算力过剩”在很多部署场景里其实是一个错觉。我们看到的空闲是计算单元的闲置而不是存储系统、数据通路的闲置。后者可能已经忙到冒烟了只是监控面板上体现不出来。1.2 为什么GPU看起来“闲着”请求却在排队把一次推理请求拆开来看瓶颈往往不在算子执行而在几件不起眼的事上权重要从显存搬到缓存和计算单元这个搬运过程极其耗时KV Cache占用了大量显存导致可用显存不足没办法容纳更大的batch请求调度器要决定先算哪些请求、后算哪些请求调度本身有时间开销显存碎片化严重明明总量还剩不少却找不到一整块连续空间给一个新的请求。这些开销都不会让GPU利用率飙升却会实打实地把延迟拉高。更麻烦的是它们之间会互相叠加显存不够导致batch上不去batch上不去导致算力空转可用的并发度低又让单个请求的调度排队时间变长。最后呈现在用户侧就是机器看起来不忙服务却慢得像在挤早高峰地铁。1.3 本地部署和线上服务对“算力”的感受完全相反这个话题还要分成两个场景看因为本地部署和线上服务对算力的体感是反的。本地用Ollama之类工具跑大模型时你打开任务管理器GPU占用率很可能高达80%以上速度却只有每秒二十来个token。你会觉得“算力根本不够用显卡太弱了”。线上服务呢集群规模一大单张卡的算力反而不是稀缺资源显存和带宽先撑不住了GPU计算单元反倒闲下来。这两件事同时成立恰恰说明大模型推理的性能瓶颈不在“算力总量”。如果只盯着TFLOPS去规划硬件很容易出现一种尴尬局面花大价钱堆出来的卡实际吞吐并没有等比增长。要打破这个局面得先搞明白推理到底被什么东西卡住。2. 推理慢的上限不是FLOPS而是显存墙、带宽墙与调度墙我把推理延迟的瓶颈归纳成三道墙显存墙、带宽墙、调度墙。实际排查问题的时候按这三条线去捋速度会快很多。2.1 显存墙模型权重和KV Cache怎么一步步吃光显存第一道墙是显存。很多人在评估“这张卡能不能跑这个模型”时只算了模型权重的大小。比如7B模型FP16权重约14GB一张24GB的4090感觉随便跑。这个算法漏掉了一个大头KV Cache。Transformer推理时每个请求的每一步都要缓存历史token的Key和Value矩阵用来计算注意力。KV Cache的大小大约是2K和V × 层数 × 注意力头数 × 头维度 × seq_len × batch_size × 每个元素的字节数假如一个7B模型有32层KV头数加起来是32每头维度128序列长度2048batch_size为1FP16存储那么单条请求的KV Cache大约为2 × 32 × 128 × 2048 × 2字节 ≈ 33.6MB单条请求看起来不多但线上推理不可能只服务一个请求。batch_size到32序列长度再涨到4096KV Cache会迅速膨胀到几个GB甚至十几GB。也就是说完int4量化权重省出来的显存很快会被KV Cache重新吃掉。这也是为什么很多推理引擎要引入KV Cache的量化、淘汰和分页管理。显存墙带来的直接后果是你不能无限提高batch size。batch小GPU计算单元就吃不满吞吐上不去batch大又可能直接OOM。线上服务为了稳定性往往只能在“能用”和“够快”之间找一个很窄的窗口。2.2 带宽墙解码阶段其实是个“访存密集”任务第二道墙比显存更隐蔽也更容易被忽视显存带宽。大模型推理的Decode阶段每生成一个token都需要把模型权重从头到尾读一遍。虽然计算量看起来不大但搬运的数据量很大。GPU的FLOPS越高单位字节对应的计算次数计算强度就越低这时候访存速度就成了真正的天花板。拿A100来算笔账。A100的FP16算力约312 TFLOPS显存带宽约2TB/s。假设你只做矩阵乘法理论上每读一个字节可以执行大约156次浮点运算。但在推理生成阶段逐token的矩阵向量运算远达不到这个计算强度因为权重只被读一次却只算一小批数据。结果就是GPU计算单元在那里等待权重数据从显存里搬运过来利用率自然上不去。这也是为什么同一张卡跑不同模型速度差很多。模型越大需要搬运的权重越多解码速度就越受带宽限制。网上很多评测里7B模型int4量化在4090上能跑出七八十token每秒但到了70B模型就跌到十几token每秒主要就是被带宽卡住了。2.3 调度墙请求排队、显存碎片和细碎任务都在拖后腿第三道墙是调度。这是很多人最容易忽略的也是“硬件调度演进”的核心。传统的批处理方式是把一段时间内的请求攒在一起凑够一个batch再统一计算。在线推理场景下这么做会出现一个经典问题早来的请求要等慢请求一起算延迟波动极大。而且当一个batch里某个请求生成长度特别长其他请求就算已经生成完了也要陪着它一起算。调度墙最麻烦的地方在于它很难通过堆硬件解决。你加再多GPU如果调度器不能把请求合理地分配到每一张卡上不能让batch里的token动态补位照样会出现“卡多但忙闲不均”的局面。更常见的是显存碎片化——模型权重和KV Cache分布得七零八落可用显存总数还有但单个请求能用的连续空间被切得很碎结果只能把batch调小算力继续闲置。三道墙的排查方向也不太一样我整理了一个简单的自查表瓶颈类型典型现象怎么看显存墙显存占用很高batch提不上去OOM时有发生看nvidia-smi显存使用测不同并发下的显存增量带宽墙GPU利用率不高但生成速度上不去看模型速度是否与显存带宽趋势大致一致测试不同量化精度下的速度调度墙单请求延迟不高但P95/P99延迟波动大排队时间长看请求队列长度、推理引擎内部排队耗时、batch重组频率3. 大模型推理硬件调度演进从人等卡到卡等人把这几年推理引擎和硬件调度的发展串起来看核心其实就是一句话从“人等卡”走向“卡等人”。3.1 早期阶段离线批处理凑满一车才发车最早用GPU做深度学习推理时大家习惯像训练一样做静态Batching。比如一次来32个请求必须等这32个请求全部到齐填满了batch才开始计算。这个模式下“人等卡”非常明显早到的请求可能在队列里等几百毫秒就为了等后面的请求一起算。这种方案处理离线百批任务没问题因为不要求响应时间延迟高点无所谓。但到了对话式大模型时代用户敲完一句话盯着光标闪等一秒钟都觉得卡静态Batching的体验就完全崩了。3.2 重大转折Continuous Batching和PagedAttention让调度粒度变细后来推理引擎做了一件非常关键的事把“请求级调度”变成“token级调度”。Continuous Batching不再等一个batch填满才计算而是每个token生成完只要这个请求还没结束下一轮继续往里加一个请求生成完了它的位置立刻让给排队中的新请求。这样GPU的计算单元始终在处理实际存在的token而不是干等batch凑满。早到的请求再也不用等慢请求了整体吞吐和延迟表现都上了一个台阶。PagedAttention则是从显存管理角度解决KV Cache碎片化的问题。它的思路和操作系统的分页很像把KV Cache切成固定大小的块不要求物理连续用页表做映射。这样显存利用率大幅提升也让KV Cache的换入换出成为可能。可以说推理引擎从“看不见调度”到“精细调度”PagedAttention是脚下面那块踏实的砖。3.3 更进一步Prefill/Decode分离与异构硬件的分工调度粒度细下来之后大家发现Prefill和Decode这两个阶段对硬件的要求完全不一样。Prefill阶段要处理整段输入计算量大是典型的计算密集任务正好可以把GPU的矩阵算力打满。Decode阶段是逐token生成刚才说了是访存密集任务你把它放在同一张卡上两个阶段会互相拖累Prefill把算力用满时Decode的token就排队等着Decode在慢慢搬权重时Prefill又抢不过带宽。所以现在很多团队开始把Prefill和Decode分开部署。专门用少数几张算力强的卡处理Prefill再让更多带宽还行的卡专心做Decode。这本质上是一种以“任务特征”为依据的硬件调度策略。引入多机后还需要在调度层把Prefill和Decode的负载动态路由到对应设备让每一类硬件都干自己最擅长的事。3.4 多卡多机场景从单卡显存并行到全局负载调度再往大了说真正把“硬件调度”这四个字体现得最充分的还是多卡多机集群。推理服务常用的多卡方式有张量并行和流水线并行张量并行把一层网络的参数切成多份放到多张卡上共同算一次矩阵运算流水线并行按层切分每张卡负责其中一段。张量并行需要高速卡间通信最好用NVLink流水线并行对卡间通信要求低一些但存在流水线气泡。实际部署时选哪种要看你“缺的是显存还是带宽”——显存不够用张量并行带宽够不够直接看GPU之间的通信量。到了多机层面调度问题就更复杂了。怎么统一管理多台算力服务器业界通常会有这么几层集群资源管理层比如Kubernetes或SLURM负责把GPU资源做抽象按“卡”甚至“GPU切片”来分配推理服务层的框架自带的路由调度比如vLLM、TGI里的调度器负责把请求发给具体的实例如果要做更细的混部会用Ray这类框架做任务级调度或者自研控制面。我个人不推荐上来就自研一套调度系统。除非你对业务特征非常清楚否则先用现成框架解决80%的问题剩下20%再针对性改。很多团队折腾半天最后发现瓶颈根本不在调度系统而在显存碎片或者带宽瓶颈。4. 先看清瓶颈再调优本地部署和集群部署的实操路径做推理性能优化我强烈建议先回答一个问题你到底是哪种场景单卡本地部署和集群在线服务调优策略完全是两条路。4.1 本地单卡部署先把显存和量化账算明白本地部署是很多人第一次接触大模型推理的场景常见工具就是Ollama之类的。这类工具胜在“零门槛”但它的默认配置不一定适合你的显卡。本地部署最重要的是先算显存账。不光要看模型权重还要给KV Cache、推理引擎本身留下余量。我一般建议直接按权重大小乘以一个系数来估算FP16权重大约占显存的2倍权重本身激活和KVINT4量化权重大约占显存的0.7到1倍。比如8GB显存的卡跑7B模型时最好用Q4量化不然很容易发生部分层被换到内存、速度骤降的情况。另一个容易踩的坑是本地部署不要盲目追求大模型。24GB的4090可以跑7B甚至14B的量化模型但如果你强行上70B模型哪怕量化后只有40GB左右超出显存的部分就会被交换到内存速度可能掉到个位数token每秒体感反而不如一个14B量化模型来得流畅。不同显存档位的合理选择我整理了一张表供参考显存容量推荐模型量级推荐精度备注8GB7B左右INT4需要给KV Cache和系统留余量12-16GB7B-14BINT4/INT8生成速度和显存能兼顾24GB14B-34BINT4/INT8本地比较均衡的甜点档48GB及以上70BINT4再大就要考虑多卡并行4.2 量化不是魔法带宽-精度-效果的三方博弈很多人在推理提速时第一反应就是量化。量化确实可以减少权重占用的显存也能减少搬运的数据量对带宽瓶颈有明显改善。但量化不是白拿的精度损失和算子支持都是要还的。我的经验是INT8量化在很多场景下损失很小但INT4量化在不同模型上的效果差异很大。有的模型INT4之后回答质量几乎没有变化有的模型会开始胡言乱语。还是要按具体任务做评测。另外有些老显卡对低精度算子的支持并不好强行用INT4反而可能因为算子没有走低精度优化速度不升反降。所以量化的正确姿势是先测带宽敏感度如果瓶颈明显在显存带宽上量化收益最大如果瓶颈在调度或者batch策略上量化解决不了根本问题。4.3 集群部署先做性能诊断再谈统一管理多台算力服务器的统一管理第一步永远是摸底。直接把所有卡接进来调度大部分时候会变成“所有请求都涌到一张卡上其他卡看热闹”。我会从下面几个指标入手做性能诊断每张卡的GPU利用率和显存使用率用来区分是算力瓶颈还是显存瓶颈服务的QPS和延迟分布重点看P95和P99有没有明显抬高请求在推理引擎内部的排队时间单独看调度环节的耗时Decode速度和batch size的关系曲线找到吞吐拐点。把这些数据拉出来再判断“统一管理”该落到什么层面。如果只是希望把多个模型实例部署在不同卡上用Kubernetes加推理服务框架就能解决。如果希望跑多个模型、多租户、动态伸缩那需要引入更完整的资源队列。我记得有次帮客户排查发现所有请求都被调度器默认塞到第一张卡上后面几张卡闲到发慌——这种问题纯靠堆算力解决不了先把路由策略改掉延迟立刻就下去了。4.4 评估token算力需求时别漏掉并发和生成长度最后说一个很多人都会估算失误的点token算力需求。网上常能看到“每秒需要处理多少token就配多少张卡”的估算公式听起来很合理实际用起来却经常差出好几倍。问题出在估算公式里的两个变量并发数和最大生成长度。用QPS乘以平均输出长度只能得到一个均值但线上请求的输出长度波动非常大。如果你按平均50个token来算结果某个高峰期出现了大量长文生成请求每一条都输出1000个token算力需求瞬间可能膨胀十倍以上。比较稳妥的做法是按P95或P99的生成长度来估算并预留30%到50%的缓冲。更重要的还是尽量跑压测用真实数据和真实用户行为去压。模型推理性能在很多情况下不是线性扩展的batch增加到一定程度吞吐增长会变缓延迟反而加速恶化。压测能帮你找到这个拐点避免买了过度冗余的卡。5. 重新理解算力过剩与硬件调度的几个个人体会聊到最后说几点我在实践中沉淀下来的体会。第一算力过剩经常是“计算单元过剩”而不是“存储和调度能力过剩”。如果你的GPU利用率低但服务还是慢不要急着加卡先看显存带宽和KV Cache再看调度策略。换一张带宽更大、显存更多的卡有时比加两张算力一样但带宽捉急的卡更有效果。第二调度演进的本质不是让GPU更忙而是让token的流动更顺畅。Continuous Batching和PagedAttention之所以重要是因为它们把“调度”的粒度从请求级降到了token级和页级。硬件调度未来的方向大概率也是更细粒度地看任务特征比如把Prefill和Decode拆开、把推理任务里计算密集的部分和访存密集的部分分给不同硬件。第三不要盲目迷信单一推理框架。同样是跑一个7B模型不同框架在不同硬件上的效果差异很大。有的框架在A卡上调度效率高有的在B卡上对部分算子支持更好。做技术选型时多跑几次压测用数据说话比自己凭印象拍脑袋靠谱得多。我个人的习惯是接到一个“推理慢”的问题第一件事不是急着换卡、加卡或者换框架而是先去看实时指标确认瓶颈在哪一道墙再决定动哪一块。很多时候答案早就写在监控数据里了只是我们习惯性把锅甩给算力却没仔细看数据到底在说什么。