
开头先讲一个我印象挺深的现场。前阵子组里一个在线推理服务在晚高峰出现诡异的抖动集群总览页面看下来明明还有大几百张卡的空闲可新请求就是进不了推理引擎想扩容扩不动想缩容又怕下一波流量上来直接打穿。后来我顺着状态面板一层层往下翻发现问题压根不在模型也不在某个推理框架的单点配置而是出在一个被很多人默认了的前提上我们都假设集群是一块完整、同构、随时可用的“大 GPU”但真实集群早就碎成了无数形状不一的碎片。FlexPipe 就是围绕这个问题长出来的推理调度框架。它做的事情可以用一句话概括让 LLM 推理流水线在碎片化 GPU 集群里像汽车变速箱一样动态换挡。GPU 充足但凑不齐一个完整执行单元时它把模型拆成适配当前碎片分布的形状请求量突然上来时它能把流水线从“低速挡”切到“高速挡”流量回落后再自动缩挡把资源吐回去。这篇文章我会把设计思路、关键机制、落地实现和测试中踩过的坑全部拆开讲适合正在做 LLM 推理优化、平台资源调度或自建推理服务的同学参考。1. 碎片化 GPU 集群LLM 推理真正面对的现场1.1 碎片不是偶发事故而是分布式系统的常态很多人规划 LLM 推理资源时会画一张干净的拓扑图几十张同型号 GPU 整整齐齐挂在一个高速互联域里模型并行维度随便选容量核算也用“总显存除以单卡显存”这种简单除法。但只要你真正维护过集群超过一个月就会知道这种图只存在于 PPT 里。真实集群的碎片来自至少四个方向。第一是多租户预留训练任务、评测任务、别人的推理服务都可能在同一批物理机上占地盘今天凌晨释放 16 张卡明天下午又预占 8 张这些资源进出是不对齐的。第二是故障隔离GPU 掉卡、节点报错、交换机端口闪断之后平台通常会把问题节点踢出调度池等人工确认这会在本来连续的资源域里凿出一个个空洞。第三是硬件代际混杂几次采购周期下来集群里大概率同时存在不同算力、不同显存、不同互联带宽的卡表面是“都有 80GB 显存”背后却可能差着一倍的带宽。第四则是分时业务的潮汐离线批量任务和在线推理高峰经常互相踩踏谁也不敢保证自己拿到的是一整块。这里有个很关键的认知碎片是随时间动态变化的。同一个物理集群在时间轴上的不同时刻会呈现出完全不同的可调度形态。我整理过一张当时集群的实拍快照可以看到局部情况大致是这样资源块GPU 型号可用显存规格可用互联当前可调度卡数块 A高算力新卡80GBNVLink 全域12块 B中算力旧卡80GB普通 RDMA9块 C中算力旧卡48GB部分因故障隔离PCIe 跨机6这些块之间的边界还会随时间漂移。一个静态并行配置如果按照“全部 12 张卡组成一个完整并行组”去设计当资源块被挤占成 9 张时整个执行计划就废了。碎片化最致命的地方在于六个变量同时在变节点空闲显存、节点间互联带宽、卡的健康状态、机架物理位置、当前已占用量、上游资源预留策略。把它们揉在一起任何单维度的排序都会失效。1.2 标准推理并行方案在碎片面前的窘境LLM 推理常用的几种并行方式在碎片集群上都会遇到各自的麻烦。张量并行是把一个 Transformer 层的权重和计算切到多张卡上比如把一个注意力层切成 4 份由 4 张卡分别算完再合并。这种方式对卡间通信带宽极其敏感因为每个算子都要跨卡做 allreduce。理想情况下它应该跑在 NVLink 全互联的同一机柜内可碎片集群里能凑齐 4 张、8 张连续同型号卡的窗口本来就少强行把通信距离拉长后单步延迟会非线性上涨。更要命的是大部分推理系统的 TP 维度是写死在模型配置里的启动时一次定死之后不管 cluster 怎么变化都不再调整。流水线并行则是把模型按层切成几段每段放到不同节点上数据像工厂流水线一样依次流过。它的问题在于关键路径上的同步一旦某一个流水级因为节点负载、卡故障或者通信抖动变慢整个链路的尾延迟都会被拉高。在碎片集群里流水线的每一级往往落在异构资源上跑得快的级和跑得慢的级之间会产生持续的“空转等待”跟流水线上最慢工位一个道理。数据并行/多副本并行看起来对碎片很友好把模型复制成多份分摊请求就行了。但它只在高并发、大批量的离线场景下才高效。在线 LLM 交互的长尾请求往往是几十个小 batch 并发副本多了之后每个副本的 batch 都喂不饱算力利用率反而下降。碎片集群里不同副本还落在不同代际的卡上快卡慢卡完成任务的时间差距会被在线请求的严格延迟要求放大成排队问题。更麻烦的是现有的连续批处理技术虽然能把单个节点上的 GPU 利用率提得很高但它优化的前提是“一个执行计划可以长期稳定占据一组 GPU”。一旦集群形态发生变化比如节点被回收、预留被抢占、故障隔离连续批处理的调度单元就得整体搬迁而这恰恰是传统推理框架最不擅长的事。1.3 为什么说是“换挡”一个变速箱类比汽车变速箱做的事情是让发动机在不同转速和负载下都能匹配合理的轮上扭矩与速度。低挡位适合重载爬坡高挡位适合高速巡航。如果一辆车只有一个挡位那它在城市堵车和高速巡航之间必有一项表现极差。LLM 推理流水线其实面临同样的问题。预填充阶段要处理大量输入 token是计算密集型重负载类似“爬坡”解码阶段每个请求只生成少量 token是访存密集型轻负载类似“高速巡航”。不同业务场景对这两类负载的配比完全不同离线批量总结是长输入、高并发预填充在线聊天则是大量短请求、持续小步解码。一个固定并行度的推理系统本质上就是一台只有单挡位的车无论怎么调参数都只能保一头。FlexPipe 的思路是把“模型参数”和“执行方式”彻底解耦把并行策略、资源绑定、KV 缓存分区方式都变成运行时可调整的“挡位”。引擎根据当前集群碎片形态、请求负载、SLO 要求动态决定当前该挂哪个挡并且能在不停服务的前提下完成切换。下面我会从整体设计开始逐层拆开这套机制的实现。2. FlexPipe 的整体设计把模型形状和集群形状解耦2.1 执行计划作为一等公民在传统推理系统中模型加载意味着“权重 固定并行配置”比如“TP8PP1共需 8 卡”这套组合一旦启动就和 GPU 绑定到生命周期结束。FlexPipe 改变了这个模型权重是唯一不可变的静态事实并行配置只是运行时产物可以随时被替换。我们把一次可运行的推理拓扑定义为一个“执行计划”。执行计划里包含并行策略TP 大小、PP 大小、是否做预填充/解码分离、层到物理 GPU 的映射关系、通信原语集合每个通信域涉及哪些卡、走什么物理链路、KV 缓存分区方案。同一个模型权重可以同时对应多份执行计划就像同一台发动机可以匹配多个挡位的传动比一样。这个抽象带来的直接好处是当资源块变化时我们不需要卸载模型再重新加载只需要生成一个新执行计划并把运行时状态迁移过去。既然模型权重没变新计划的所有层参数可以直接映射到同一份显存权重变化的只是计算图如何切分、数据如何路由。2.2 集群拓扑感知先把碎片“底数”摸清FlexPipe 的调度决策建立在实时拓扑快照之上。每个节点上的 Agent 每 15 到 30 秒会上报一次可用卡列表、每张卡的剩余显存、当前算力负载、节点间探测到的实际互联带宽、健康状态。控制面把这些信息汇总成一棵“可调度资源树”。但不是所有空闲卡都能直接参与执行计划。我们还要做一层“原子块”抽象一组物理上连续、互联带宽满足并行要求的 GPU 会被封装成一个资源原子块。比如 8 张位于同一 NVLink 域的卡是一个原子块4 张跨机走普通网络的卡则是另一个原子块。执行计划生成时只能消耗完整的原子块不能强行把两块互不相邻的卡拼成一个张量并行组。这个抽象很关键因为它把物理层面的硬约束提前消化掉了。计划生成器不需要再关心每张卡的位置和链路质量只需要在原子块列表中做组合。碎片再怎么漂移最终呈现给决策引擎的总是一批“能用”和“不能用”的清晰边界。2.3 三种挡位模式换挡图形的核心FlexPipe 定义了三种可切换的挡位模式分别对应不同的负载特性和资源形态。第一种是预填充/解码分离模式。把预填充阶段放到算力强、带宽高的资源块上把解码阶段放到访存能力充足但算力相对弱的资源块上两个阶段之间通过 KV 缓存转移衔接。这种模式特别适合碎片异构严重的场景高算力卡专门吃重负载旧卡专门跑轻负载各得其所。第二种是流水线重塑模式。当可用资源块变大或变小时调整并行策略的维度组合。比如原来在一个 8 卡块上跑 TP8被压缩到 4 卡块后就改成 TP4原来碎片块太小跑不了 TP就改成 PP2 或 PP3 的串行结构。层与层之间的切分边界不再是静态的而是按当前块大小重算。第三种是多计划混跑模式。同一时刻允许同一模型的不同执行计划并行存在不同资源块各跑一个挡位请求按队列长度、响应时间预测路由到最合适的计划上。这相当于一台车同时挂了多个挡位在碎片极多、单块都很小的场景下几乎是必然选择。这三种模式不是互斥的。实际切换经常是组合动作先做预填充/解码分离再在新结构上把流水线重塑成更合适的维度最终形成一个“挡位组合”。2.4 主控循环决定什么时候换挡FlexPipe 的决策引擎运行一个固定周期的主循环每一轮做四件事。第一拉取最新拓扑快照和推理负载指标第二枚举当前模型可能挂上的候选挡位第三对每个候选估算切换后的收益和切换成本第四只有当净收益超过阈值时才真正执行切换。这个循环必须跑得快不能把全局锁卡在决策上。我们的实现里循环周期默认取 10 到 30 秒但拓扑变化事件节点上线、节点隔离、大块资源被抢占会立刻触发一次非周期决策。负载指标则采用滑动窗口统计避免单次抖动把挡位带偏。决定触发之后动作本身并不由主循环直接做而是交给独立的转换执行器。主循环只负责“挂挡”执行器负责“换挡”两者彻底分离避免决策延迟阻塞执行过程。3. 动态换挡的核心机制不中断请求的热重排3.1 KV 缓存迁移换挡时最重的家务活换挡过程中最微妙的是 KV 缓存。Transformer 推理时每个请求的注意力键值状态都要缓存下来用于后续 token 生成。这个缓存的体量相当惊人。按公式粗略算一下每个 token 的 KV 缓存字节数等于 2K 和 V乘以层数乘以隐藏维度乘以精度字节数。以 70B 参数规模的常见配置为例hidden size 假设 8192、层数 64、FP16 精度每个 token 的 KV 缓存大约是 2MB一条 2K 长度的请求就要占 4GB 显存一批 10 个长请求就是几十 GB。所以换挡不能简单粗暴地把所有 KV 全量搬到新计划不然网络和显存都会被压垮。FlexPipe 的做法是把 KV 缓存按执行计划的分区方式切片。新计划启动后先只迁移那些已有请求中“必须继续生成”的活跃序列的 KV 分片已经处于空闲等待状态的请求则走惰性迁移。迁移路径也做了优化充分利用节点间已有的专用通信通道同时把目标显存提前预分配好避免迁移过程中出现临时显存申请失败。如果集群资源块之间带宽确实太低KV 迁移成本高到不可接受还有一个保底手段是“重放”。新计划从最近一个检查点重新预填充这些序列。重放会消耗额外算力但能彻底绕开网络瓶颈。实际使用中FlexPipe 会根据 KV 体积、带宽、算力负载三者估算出迁移和重放两个方案的成本自动选便宜的。3.2 两层调度请求调度和流水线调度分家换挡过程中的流量控制需要两层调度协同。第一层是请求级调度负责把用户请求分发到对应挡位的入口队列第二层是流水线级调度负责决定当前哪些请求推进到具体执行计划里。传统系统往往只有第一层请求一旦进了某个执行计划就跟到底。FlexPipe 在触发换挡时会冻结旧计划的入口不再把新序列送给旧计划同时让旧计划把手上已有的序列跑到一个安全边界再移交。这个“安全边界”由 KV 缓存分区和当前微批次的位置共同决定一般取一个解码迭代的结束点。新计划则从启动那一刻就开始接收新请求和部分迁移请求形成“旧计划收尾、新计划起步”的重叠状态。两层调度通过一个共享的队列元数据服务同步状态保证一个序列不会被两个计划同时处理。3.3 微批次叠加交换把切换做成交接班为了不让换挡过程出现真空期我们借鉴了在线服务发布时的滚动更新思路把切换动作拆成三个微阶段。第一个微阶段新计划以“影子模式”启动加载权重、创建通信域、预热 KV 缓存分发器但不接正式流量第二个微阶段新计划开始接收一小部分新请求同时旧计划继续处理老序列两个计划并行运行几个微批次第三个微阶段全部请求路由到新计划旧计划进入排空状态等最后一个微批次完成后再释放资源。这种叠加交换的好处是延迟曲线不会断崖。旧计划在收尾时负载渐降新计划在启动时负载渐升两者的吞吐曲线交叉整体服务的吞吐量在切换过程中只出现一个短暂的小凹坑而不是完全归零。代价是新旧两套执行计划的显存和算力在切换窗口内需要同时存在但窗口通常控制在几十秒到几分钟对资源碎片比较宽裕的场景完全可接受。3.4 该不该换挡收益计算要算细账换挡不是免费的所以不能看着负载升高就立刻挂高挡。我们给决策引擎内置了一个成本估算器每次评估候选挡位时都计算三笔账切换期间的吞吐损失、KV 迁移和权重加载的显存带宽占用、新计划启动后的“冷启动惩罚”。冷启动惩罚很容易被忽略。新计划刚建好时GPU 上的缓存是空的第一批请求的 prefill 全部要重新计算即使权重已经在显存里计算路径的预热也需要几百个请求才能达到稳态。因此决策公式里引入了滞回因子必须满足“预估收益大于切换成本乘上一个安全系数”才允许换挡。经验值是安全系数取 1.3 到 1.5也就是除非新挡位带来至少三到五成的改善否则宁可维持现状。这段伪代码基本表达了决策逻辑的核心def decide_gear(current_plan, topo_snapshot): candidates planner.enumerate(current_plan, topo_snapshot) for cand in candidates: cost estimate_switch_cost(current_plan, cand) gain estimate_throughput_gain(cand, current_load) - estimate_cold_start(cand) if gain cost * SWITCH_HYSTERESIS: return cand return None实际实现中这个函数会被主循环反复调用但候选枚举顺序做了缓存避免每次把整棵资源树遍历一遍。4. 关键模块的实现细节4.1 碎片监控器与拓扑推断监控模块的职责不是机械地拉取指标而是把裸数据清洗成决策可用的拓扑视图。每个节点 Agent 上报的实时数据会先做一次本地聚合只推送“变化量”而非全量减少控制面压力。控制面收到的数据里凡是心跳超时或者连续多次校验失败的节点会被直接标记为不可调度不再进入候选资源块。碎片监控器还会维护一张“历史碎片表”记录每个资源块在过去几小时内的大小变化轨迹。这张表不是因为好奇而是为了预测未来。如果某个块在过去一小时里持续被挤占那么即使当前还有空间新执行计划也不会把它当作长期依赖反之如果某个块持续稳定则更值得把低延迟要求高的请求放上去。4.2 计划生成器约束条件下的排列组合计划生成器的输入是“模型定义 当前原子块列表 负载画像”输出是一个候选执行计划列表。它先按模型大小确定最小并行度下限比如显存不足以放下单层权重时TP 必须至少为 2再按块间通信带宽确定 PP 的可行边界通信太差的块之间不会安排跨节点 PP。候选枚举是一个带了大量剪枝的排列组合过程。我们约束每个执行计划只能在一个资源块内做 TP跨块只允许做 PP 或预填充/解码分离。这样一来候选数量从“卡数级别”降到了“块数级别”即使集群有几十个碎片块枚举也能在几十毫秒内完成。生成器还会顺带估算每个计划的稳态吞吐、P99 延迟、批处理容量这些估算值会被决策引擎用来做收益比较。注意这里只需要相对准确不需要绝对精确因为最终牵引决策的是收益差值而不是单个方案的绝对值。4.3 转换执行器三阶段提交协议真正的换挡动作由一个独立的转换执行器负责它实现了类似分布式事务的三阶段提交语义。第一阶段是准备阶段执行器校验目标资源块的可用性、预分配显存、加载权重到影子执行计划、建立新的通信域这个阶段不对外提供任何服务变更。第二阶段是切换阶段执行器冻结旧计划入口按计划迁移 KV 缓存迁移完成后原子地更新路由表把流量切到新计划。第三阶段是清理阶段旧计划排空剩余请求、释放显存、通知资源池回收 GPU。三阶段提交最怕的是中间状态不一致。我们的做法是给每个转换动作分配一个全局递增的序号所有阶段的状态都持久化到协调存储中。任何一个阶段失败时执行器可以按状态记录回滚到上一个稳定状态或者重新发起补偿动作。实测下来最常见的失败点是第二阶段里资源被抢占处理方式是超时重试重试两次仍失败就整体回滚到旧计划。4.4 控制面选型不抢流量通道的数据面FlexPipe 的控制面业务量并不大但正确性要求很高。我们用了一个独立的协调服务来管理执行计划状态机和转换序号节点只和它保持轻量心跳。所有控制指令与推理数据完全分离控制指令走独立通道绝不和 KV 迁移、token 生成的数据通道混在一起。这样设计是在一次故障之后学到的教训。当时我们把控制指令放在和 KV 迁移相同的消息通道里结果大块 KV 迁移一启动控制指令的延迟飙到几百毫秒导致调度器误判节点失联差点引发级联切换。后来把通道拆开控制面和数据面彻底隔离这类问题就再没出现过。5. 实测结果与真实反馈5.1 测试环境与压力场景为了验证 FlexPipe 的效果我们在一套模拟真实碎片环境的集群上做了对照测试。集群被刻意划分成三个差异明显的资源块12 张高算力新卡组成块 A9 张中算力旧卡组成块 B6 张不同型号卡组成块 C块间通信带宽低于块内。测试模型分别用了 7B 和 70B 两个参数规模的通用开源模型负载混合了长输入批量总结和短输入在线对话两类任务。对照组使用固定执行计划即模型启动后按 TP8 配置绑定在块 A 上不感知其他资源块实验组运行 FlexPipe允许在三个块之间动态换挡。测试持续了 8 小时中间人为注入了三次资源挤占事件模拟其他任务抢占节点。5.2 关键数据延迟、吞吐与资源占用先看最重要的延迟指标。固定计划在资源挤占发生后P95 延迟从基线约 380ms 一路涨到 2.1s并且在资源恢复后仍然持续了十几分钟的高延迟因为排队请求积压严重。FlexPipe 在第一次挤占事件发生后的 40 秒内完成了从块 A 到部分块 B 的换挡P95 延迟峰值控制在 850ms 以内并且随着新计划预热逐步回落到正常水位。再看吞吐。固定计划在 8 小时内的总 token 吞吐约为 FlexPipe 的 67%差距主要来自资源挤占期间的排队空转。FlexPipe 还能在低峰期自动把多余副本缩掉把资源吐回资源池。整体 GPU 显存利用率比固定计划提高了大约 22%算力利用率提高了约 18%。换挡本身的耗时和模型规模直接相关。7B 模型的典型切换时间在 3 到 5 秒70B 模型则要 8 到 15 秒主要时间花在 KV 缓存迁移上。这个耗时对我们接手的在线业务是可接受的因为我们只在资源形态或负载画像发生显著变化时才切换而不是频繁跳动。5.3 实战中踩过的三个大坑第一个坑是低估了 KV 迁移的显存带宽占用。第一次做全量 KV 迁移时我们把迁移任务和新请求的 prefill 放在同一批 GPU 上执行结果迁移占掉了大量显存带宽吞吐直接掉了三成。后来改成只在负载低谷窗口迁移并且对 KV 做分片优先级排序长序列优先迁移短序列留在旧计划跑完问题就缓解了。第二个坑是“影子计划”的权重加载会触发显存抖动。新计划刚创建时如果目标 GPU 上正好有临时峰值预分配的显存可能失败。我们加了提前预占机制即转换执行器在准备阶段就把目标显存锁定住不让其他调度器看到这部分可用资源虽然浪费一点点调度灵活性但换来了切换的高成功率。第三个坑是切换触发过于积极造成挡位抖动。早期我们的滞回系数设得太低晚高峰负载稍微一抖引擎就在两个挡位之间反复横跳每次切换都带来几秒钟的延迟毛刺看起来像服务本身不稳定。后来把滞回窗口拉长并在短时间内的第二次切换加了冷却时间才算压住。5.4 常见问题速查表这里整理一份我在维护过程中反复用到的问题排查表直接照着看可以省不少时间。现象可能原因处理方法切换后 P99 仍然偏高新计划缓存冷启动prefill 全量重算提前为热门模型预热新计划或者让新计划先承接低优先级请求切换一直卡在准备阶段目标资源块被其他任务抢占预分配失败查询资源状态并回滚将预占提前到触发决策时同步执行KV 迁移队列持续积压迁移带宽规划不足长序列挤占容量限制单批次迁移量对短序列使用惰性迁移不阻塞主流程挡位频繁来回切换滞回因子过小负载抖动触发了决策调大滞回系数增加最短冷却时间一般建议 90 秒以上节点上报心跳正常但通信异常控制面和数据面共用通道被大流量阻塞拆通道控制指令走独立优先级链路这块速查表不是一次性总结出来的是我们在多次演练和线上排障里一条条攒下来的。每次排障我都会顺手把“现象、根因、解法”记进内部知识库几个月后就自然形成了一张覆盖绝大多数换挡问题的清单。6. 如何把“可换挡”思路移植到自己的系统里6.1 从调度边界开始不要一上来就重写推理框架如果你的系统还在早期我的建议是先别急着把 FlexPipe 的整套机制都搬过去。多数推理框架已经支持动态批处理和一定程度的多副本扩展真正缺的是“把执行计划当成可替换对象”的思维。第一步可以只做一件事把请求路由从执行计划中抽离出来让多个固定配置的执行计划同时存在按队列状态分发请求。这一步不需要改框架内部只需要在框架前面加一层轻量路由。这一步做完之后你会发现类的资源碎片问题已经缓解了一大半因为不同执行计划天然适配不同资源块。等需要更激进地提升资源利用率时再逐步引入 KV 迁移、影子计划、三阶段切换这些重机制。6.2 把挡位本身当成一等监控对象我的经验里踩过的大多数换挡事故都不是“换挡没实现”而是“换个挡没人看见”。所以监控系统里不仅要看 GPU 利用率、延迟、队列长度这些传统指标更要把当前计划 ID、切换阶段、切换耗时、迁移队列长度、影子计划状态全部暴露出来。理想情况下任何一次换挡都应该能在监控面板上完整复盘包括触发原因、候选收益、成本估算、各阶段耗时。有了这套数据你会发现自己很快就能总结出集群碎片的活跃规律比如哪个时间窗口最容易出现大块资源释放哪个业务的任务总是造成碎片漂移这些规律反过来又能指导上游调度策略形成正向循环。6.3 先用离线批处理验证再接在线业务如果你对这套机制信心不足我的建议是先拿离线批处理场景试水。离线请求的延迟敏感性低即使切换过程抖动个几秒也影响不大非常适合验证 KV 迁移的正确性和决策引擎的稳定性。等离线场景跑了稳定再把同一个执行管道接到在线业务上。在线业务一定要额外加“紧急降挡”按钮也就是允许运维手工把系统强制切回某个基线计划。自动化不会永远正确手工逃生通道在极端故障时能救命。我自己的体会是LLM 推理系统越往后发展就越会把“模型能力”和“运行形态”分开来看。模型权重只是一份静态资产怎么把它跑出最佳性价比完全取决于你能不能灵活地匹配当前集群的碎片形态。FlexPipe 现在做的动态换挡只是这个思路的一个落地实践未来更常见的形态可能是同一个推理服务在同一时刻用多种执行计划服务不同负载像变速箱一样在多个挡位之间无感切换那才是真正成熟的推理基础设施。