分布式推理六项关键技术:从张量并到弹性容错的系统实践 最近我在整理大模型推理服务的时候反反复复看了一套叫 llm-d 的分布式推理实现。它解决的并不是某个单点性能问题而是把模型并行、显存管理、调度器、通信优化、容错这几条线拧在一起让上百亿参数模型能以服务的形式稳定对外输出。标题里“六项关键技术”这个提法很准确因为分布式推理和大模型服务之间的支撑关系确实可以拆成六个层次来看。这篇文章我尽量用从业者的视角把每一层拆开讲同时把我实际部署中踩过的坑和验证过的规律一并放进去给正在准备把推理从单卡搬到多卡、或者已经在多卡上服务却受困于吞吐和延迟的读者做一个参考。1. 先算清楚显存账本分布式推理为什么躲不开很多团队一开始是抗拒分布式的。模型不大时单卡推理很简单代码少问题少。但一旦模型规模上来或者并发请求上来显存和算力很快就成了两道必须跨过去的门槛。llm-d 的设计起点恰恰就是把“一张卡装不下、一张卡忙不过来”这两件事当成系统目标来处理。要理解它的每一项技术选择首先得把显存账本算明白。1.1 权重、KV Cache、激活值三者怎么瓜分显存推理时的显存占用主要有三块模型权重、KV Cache键值缓存和激活值。很多人只盯着权重觉得模型能加载进去就行等并发一上来才发现KV Cache 才是真正的显存吞金兽。权重的估算很直观参数量乘上每个参数占用的字节数。一个 10B 参数量级的模型用 FP16 存权重就有 20GB 左右100B 量级直接超过 200GB。这张表是绕不开的哪怕把算力抛到一边单卡放不下就是放不下。KV Cache 的计算公式也不复杂大致是KV Cache 总量 ≈ 2 × 层数 × 隐藏维度 × 并发数 × 平均序列长度 × 单个 KV 的字节数套一个典型场景100B 参数级别模型假设 60 层、隐藏维度 800032 路并发请求平均序列长度 2048用 FP16 存储。带入算一下2 × 60 × 8000 × 32 × 2048 × 2 Byte ≈ 125GB。这个数字相当震撼它比很多小模型的全部权重还大。激活值则在 prefill预填充阶段最凶猛。prompt 越长、batch 越大中间激活矩阵就越大甚至可能短时间占据几十 GB。所以真实部署里经常出现这种现象模型权重明明放进去了显存剩余看着也还够结果压测一打直接显存溢出崩溃。1.2 三种并行路线的取舍复制、切层、还是层内切分面对单卡放不下的模型业界大概有三条路数据并行、流水线并行、张量并行。它们切的对象完全不同代价也完全不同。并行方式切什么典型通信适用场景数据并行复制完整模型切分请求数据只在同步时通信单卡放得下、但单卡吞吐不够流水线并行按层切分设备间传激活层间传递激活值超大模型、层数极多张量并行层内切分权重设备间做归约每层多次跨卡归约单卡放不下、卡间互联好数据并行最简单但解决不了“单卡物理放不下”的问题。流水线并行能把超深网络切到多台设备上但存在流水线气泡设备越多空闲等待越明显。llm-d 选择的主干路线是张量并行因为它能把每一个 Transformer 层的计算均匀摊到多张卡上配合 NVLink 这类高速互联通信成本在可接受范围内。当然它不是只靠张量并行后面几层围绕显存、调度和通信的优化才是整套系统真正值钱的地方。实际部署中我见过不少团队在单卡还勉强放得下时硬上分布式结果性能和成本双双难看。分布式推理的启动条件是显存或吞吐确实到瓶颈了而不是为了在架构评审里显得先进。2. 张量并行把一层计算摊到多张卡的代价与收益张量并行的核心思想不复杂既然一张卡算不动一个 Transformer 层那就把这一层切成几份让每张卡各算一份最后合并结果。但真正落地时怎么切、什么时候通信、通信多久一次差别非常大。llm-d 在层内切分上花了很多功夫这部分能直接影响整个服务的吞吐上限。2.1 注意力头与 FFN 的切分方式先看注意力模块。QKV 矩阵和输出投影矩阵都可以按头切分。常见做法是让每张卡只负责一部分注意力头这样每张卡只需要计算自己那部分 Q、K、V减少重复计算。关键点在于多头注意力的输出需要把所有头的结果拼回去这一步必须做一次跨卡的全归约All-Reduce否则后续计算拿不到完整结果。再看前馈网络FFN它通常包含两个线性变换。第一层把隐藏维度放大到中间维度适合按列切分每张卡各自算一部分第二层把中间维度还原回隐藏维度按行切分计算完必须再做一次 All-Reduce 把结果合并。所以一个 Transformer 层里至少有两处不可避免的跨卡归约注意力输出一次FFN 输出一次。这就是张量并行最关键的权衡切得越细单卡计算量越小但每一层仍然要完成同样次数的通信。也就是说通信次数不会因为卡数增加而减少反而可能因为切分粒度更细单次通信的相对影响更大。2.2 为什么 4 卡到 8 卡的性能提升不是线性的很多人第一次上张量并行时都会产生一个疑惑卡翻了一倍吞吐怎么只涨了三四成原因就是通信开销随卡数上升。假设一个模型层单卡计算耗时 10ms每层通信耗时 2ms4 卡并行时计算摊到 4 份理论上单层只要 2.5ms 加 2ms 通信总耗时 4.5ms看起来挺赚。但切到 8 卡时单层计算降到 1.25ms通信还是 2ms总耗时反而变成 3.25ms速度提升远不如卡数翻倍那么美好。更麻烦的是All-Reduce 传输的激活值总量不随卡数缩减。序列长度、batch、隐藏维度保持不变所以在同样并发下跨卡传的数据量虽然可以分成多路并行但在弱互联条件下容易撞带宽。实测中张量并行规模从 4 卡扩到 8 卡性能能到 1.5 倍就值得庆幸从 8 卡扩到 16 卡如果没有超强卡间互联甚至可能不升反降。所以我的建议是上张量并行之前先用一个小模型跑通链路测量每层通信时间和计算时间算清楚通信占比。如果通信时间超过单层计算时间的 30%扩张卡数的收益会非常有限这时候更该考虑的是优化通信方式而不是继续切更多份。3. KV Cache 分页管理显存浪费是怎么被挤出去的权重切分解决了模型能不能放下的问题但真正决定一个推理服务能同时跑多少路请求的往往是 KV Cache 的管理效率。传统做法非常粗暴按照最大序列长度为每个请求预留连续的显存块相当于每个请求刚进门就按最贵套餐占了一张桌子。llm-d 在显存管理上做的事情可以类比成把餐厅改成了按菜加菜的模式吃多少上多少桌子流转率自然高很多。3.1 静态预留给显存带来的窒息效应如果所有并发请求都按最大上下文长度预留 KV 空间会发生什么以 32 路并发、每路最大 8192 token 上下文来估算KV Cache 的理论预留量可能高达 500GB 以上。即使显存有 80GB也经不住几个请求折腾。更尴尬的是大多数请求根本不会跑到最长上下文很多甚至只交互几十轮就结束了预留的空间全程闲置。这就是静态预留最明显的浪费服务没有跑满算力但显存占用已经拉满新请求进不来。我以前在压测场景里见过特别典型的现象GPU 利用率只有 30%显存占用却已经 90% 以上所有请求都慢吞吞地在排队原因就是 KV Cache 被预先占光了。llm-d 采用的做法是分页式管理把 KV Cache 切成固定大小的块每个请求按需申请当前请求结束时立即回收空闲块放回全局池。这样显存利用率可以大幅提高同一个物理显存能承载的并发请求数几乎能翻倍。3.2 共享前缀和请求抢占显存管理的两个隐藏收益分页不只是能按需分配它还有一个很容易被忽视的价值共享前缀。很多线上服务的系统提示词是固定的所有请求的前几轮都长得很像。如果 KV Cache 按块管理相同前缀对应的 KV 块可以被多个请求共用新请求进来只需要映射到同一批物理块上不用重新计算和存储显存省了prefill 的计算量也省了。当显存真的不够时调度器还要有能力腾出空间。llm-d 引入的机制类似操作系统里的内存回收选择低优先级或者可重算成本较低的请求把它的 KV 块释放掉等资源空出来后再重新计算这些 KV。这个操作对短上下文请求代价不大但对长上下文请求可能意味着一次明显的延迟惩罚。所以抢占策略必须结合请求优先级和已经消耗的预算一起判断而不是一刀切。这里有个工程细节值得注意分页块的大小直接决定管理粒度。块太小块表膨胀查找开销变大块太大浪费又回来了。llm-d 的做法是按照注意力头数和层数去对齐块大小让每一块恰好容纳若干个 token 的完整 KV兼顾分配效率和碎片率。按这个思路调整过块大小后我们的显存碎片率下降了不少。4. 调度逻辑是吞吐的分水岭从静态批处理到连续批处理与 PD 分离模型放得下了显存也能动态分配了下一步是调度。调度器决定每时每刻哪些请求在 GPU 上执行、哪些请求排队等待、显存如何分配给请求。调度策略的好坏对吞吐和延迟的影响可能比硬件本身还大。llm-d 在调度上做了两件关键的事迭代级调度和 prefill/decode 分离。4.1 迭代级调度让短请求不再拖长请求的后腿早期的大模型推理服务大多采用静态批处理一批请求同时进入直到这批次所有请求都生成完才一起退出。问题很明显只要批里有一个生成长文本的请求其他短请求就只能陪着等。GPU 利用率看起来高实际上很多算力浪费在了等待上。llm-d 的调度粒度精细到了解码迭代级别。每一步生成结束后系统都会重新评估当前批里的请求状态已经结束的请求立刻离开排队等待的请求马上补位。短请求不需要等长请求结束长请求也不会被后续请求插队打断。这种“边跑边补”的方式就是常说的连续批处理它能让 GPU 上的每一刻都尽可能跑满。实际效果差异非常大。我维护的一套服务在切换到迭代级调度后同样一批请求整体吞吐提升了将近 60%而且长尾延迟改善明显原因就是那些中等长度的请求不再被极长请求绑死。4.2 为什么要把 prefill 和 decode 拆开调度prefill 阶段计算 prompt 的 KV计算密集耗时跟 prompt 长度强相关decode 阶段逐步生成 token访存密集每步只产生一个 token。两种阶段的特性完全不同混在一起调度会互相拖累。一个长 prompt 的 prefill 请求可能让正在 decode 的所有请求集体等待造成 P99 延迟剧烈抖动。llm-d 的做法是把 prefill 和 decode 拆到不同的执行阶段甚至拆到不同的实例上。prefill 完成后的 KV 结果通过设备间传输交给 decode 实例继续生成。这样两个阶段可以各自按最优参数调度prefill 实例侧重并行计算decode 实例重点优化访存和连续批处理。完全分离也有代价如果 prefill 很快而 decode 很慢解码实例不够时prefill 实例会空转反过来也一样。于是 llm-d 还引入了分块 prefill 的思路把一个长 prompt 切成若干块边算边交而不是一次性全算完。这样一个实例可以交错处理多个请求的 prefill 和 decode利用率更高。我自己的经验是PD 分离不是一把万能钥匙分离后要分别监控两端利用率。如果 decode 端队列长期积压扩 decode 实例比优化 prefill 更有效如果 prefill 端频繁空转就要考虑缩小切块粒度让 prefill 工作更均匀地填进时间片。最怕的是无脑拆开然后两端都喂不饱最后总吞吐反而比混合调度还差。5. 通信与计算重叠把 All-Reduce 的等待时间藏进矩阵乘法的缝隙张量并行把计算摊开之后跨卡通信就成了新的瓶颈。解决通信问题不是让通信消失而是让通信和计算在同一段时间里发生。这个道理很像生产线上的备料工位与其等整批零件加工完再统一转运不如边加工边转运让机器等待的时间尽量短。llm-d 在通信优化上的思路基本就是这个工业工程原理的高性能计算版本。5.1 分块传输算完一个子块就开始归约一个直观但经常被忽略的事实是Transformer 一层里的矩阵乘法并不是一瞬间完成的它内部也可以拆成很多子块依次计算。如果非要等整个矩阵乘完再触发 All-Reduce通信就必须排在计算后面形成串行等待。llm-d 对通信算子的处理方式是拆分触发时机把一个大矩阵乘切分成多个连续的子矩阵乘每算完一个子块就立刻把对应的部分激活值送进归约通道。这样计算还在跑后半段的时候前半段的通信已经在网络上传输了。从墙钟时间上看通信和计算发生了重叠总耗时接近两者的较大值而不是两者之和。这种优化理论上一听就懂真正麻烦的是工程细节。拆分粒度太粗重叠效果不明显拆分太细通信启动次数暴增每小段消息的发送调度开销反而压过收益。llm-d 的做法是根据模型隐藏维度和卡间带宽动态选择拆分大小并纳入了实际延迟测量而不是用固定值。5.2 小消息合并与归约算法选择容易被忽略的两处细节除了和计算重叠通信自身的效率也有优化空间。一个最容易踩的坑是一层层地发起多个小尺寸 All-Reduce。每个集合通信操作都有不小的启动开销集中在几十个小操作上的开销可能远大于数据量本身。把多个相邻层的小消息合并成一个大的通信操作能显著降低启动开销提高带宽利用率。归约算法的选择也很讲究。数据量大的时候环形归约比树状归约更有优势因为每张卡只需要和邻居通信避免单点成为瓶颈但小消息场景里树状归约的延迟更低。llm-d 的实现会在运行时根据消息大小动态切换算法这个细节在文档里往往看不出来但对实测性能影响不小。还有一个部署层面的建议卡间拓扑直接影响通信质量。如果张量并行用到的多张卡分布在不同的带宽域里通信延迟和带宽都会变得很难看。我在一次排查中遇到过 8 卡张量并行速度反而不如 4 卡的情况后来发现是两张卡跨了带宽域的边界调整进程绑核之后才恢复正常。通信优化是分布式推理里典型的“每个环节省一点、整体省很多”的领域。6. 从单实例到服务化弹性扩展与故障转移里容易被忽略的细节前面的技术解决了如何拿多卡算好一个模型但一个合格的大模型服务还要能应对机器故障、流量波动和版本更新。这一层最容易被低估因为它在模型性能和显存技术的“光芒”之外却恰恰是线上稳定性的关键。llm-d 让我印象最深的不是那些花哨的并行算法而是整套系统在服务化层面做得相当扎实。6.1 健康检查要细化到模型实例而不是机器分布式推理服务的一个常见事故是机器级健康检查正常但模型实例实际已经无法工作。比如某张卡因为显存错误导致推理进程异常或者卡间通信断开机器本身还活着CPU 还跑着探活脚本负载均衡器仍然把新请求分发过来结果全部失败或者超时。这种问题极其隐蔽因为你查机器状态怎么看都是好的。llm-d 的做法是把健康检查下沉到模型实例级不仅看机器是否能 ping 通还要看模型实例是否能正常完成一次最小规模的推理自检。一旦自检失败实例立刻从流量分发目标中摘除不再接收新请求。这个机制配合自动拉起能把故障对用户的影响控制在很短时间内。我维护的系统上过一次这样的故障当时排查了一个下午才发现问题出在探活粒度过粗。后来把健康检查改成真实推理自检故障恢复时间从分钟级降到了秒级线上投诉大幅减少。服务化这边的每个小坑最后都对应着真实的用户体验损失。6.2 扩容的权重热加载与缩容的优雅下线流量高峰来临时系统要能快速拉起新的模型实例。看起来只是启动一个进程的事但一个 100B 参数模型的权重有几百 GB每次都从远端存储拉取启动时间可能长达几十分钟。llm-d 的做法是给权重加载加一层本地缓存和增量校验优先利用同一节点上已有的权重文件只在缺失时从共享存储拉取启动时间能缩短到原来的几分之一。扩容速度直接决定了你在流量尖峰来临时能不能接得住。缩容侧对应的是优雅下线。直接把进程杀掉是灾难性的正在处理的请求会全部断裂。正确的顺序有三个步骤先把实例标记为不可调度拒绝新请求然后等待已进入的请求自然生成完或超时退出最后才回收显存和进程。这个过程在 llm-d 里是有状态管理的调度器会持续跟踪实例上的活跃请求数归零之前不会真正释放资源。这部分的工程味道很重但恰恰是大模型服务从“能跑”走向“能稳定赚钱”的必经之路。我见过太多团队把所有精力花在推理性能和显存优化上结果上线第一次大流量就把服务打垮原因只是扩容脚本没做权重热加载。最后再分享一点个人感受。分布式推理的优化链条很长显存账本、并行切分、KV Cache 管理、调度策略、通信重叠、弹性容错每一层都在咬合另一层。只看单一维度很容易误判瓶颈比如以为加卡就能解决吞吐结果显存被 KV Cache 吃满或者以为调度器能解决一切结果通信占了大半时间。在 llm-d 里把这些链路串起来看最大的体会是先量后调。把每一层的时间分布和显存分布测清楚再决定动哪里通常比经验直觉更靠谱。如果让我给一个起步建议那就是别一上来就铺 16 卡先在 4 卡上把 KV Cache 动态分配和迭代级调度调明白再考虑横向扩张。这套基本功补上之后分布式推理的复杂度会变得可控很多。