动态流水线重配置:异构 GPU 集群上 LLM 推理的在线调度实战
一句话总结
通过动态流水线重配置技术,在异构 GPU 集群(A100 + L40s)上实现 LLM 推理配置的在线热切换,服务中断时间 < 50ms,TTFT/TPOT 额外开销 < 10%。
为什么需要这个?
问题的本质
LLM 推理有一个让所有平台工程师头疼的特性:负载极不均匀。
早上 9 点,用户请求涌入,平均序列长度 2000 tokens,并发 200 请求/秒。凌晨 2 点,零星请求,序列长度 500 tokens,并发 5 请求/秒。
如果你用同一套 pipeline 配置应对这两种场景,代价是惨烈的:
| 场景 | 问题 |
|---|---|
| 高峰期配置应对低谷 | GPU 利用率 < 20%,资源浪费 |
| 低谷期配置应对高峰 | 队列积压,TTFT 飙升到 10 秒+ |
现有系统(vLLM、TensorRT-LLM)的 pipeline 配置在启动时固定:tensor parallelism (TP) 度、pipeline parallelism (PP) 度、batch size 上限。一旦启动,无法在线调整。
硬件层面的挑战
异构集群现实:
A100 80GB × 4 ←→ L40s 48GB × 8
NVLink 600GB/s PCIe 64GB/s
BF16 312 TFLOPS FP16 362 TFLOPS
同一个模型(比如 LLaMA-70B),最优 TP 配置在两种 GPU 上完全不同:
- A100 集群:TP=4,跨卡带宽充足,通信开销小
- L40s 集群:TP=8,但 PCIe 带宽成为瓶颈,可能需要换 PP 策略
动态重配置的核心难点:LLM 推理是有状态的——KV Cache 存在 GPU 显存里,模型参数也要重新分片。这不像无状态服务可以直接蓝绿部署。
核心原理
直觉:像数据库主从切换,但更复杂
想象 MySQL 的主从切换:主库挂了,从库顶上,中间有短暂不可用窗口。LLM 动态重配置类似,但难点在于:
- 状态太大:70B 模型参数 140GB,KV Cache 可能额外占几十 GB
- 状态不能丢:正在处理的请求的 KV Cache 丢了就要从头重算
- 时间窗口要短:用户能接受 50ms 中断,不能接受 5 秒
Pipeline 并行的基础知识
Tensor Parallelism (TP):切 Attention/FFN 的权重矩阵
Layer N: [W_q | W_k | W_v] 按列切给不同 GPU
Pipeline Parallelism (PP):切 Transformer 层
GPU0: Layer 0-15
GPU1: Layer 16-31
...
配置空间:(TP=1,PP=8), (TP=2,PP=4), (TP=4,PP=2), (TP=8,PP=1)
不同配置在不同负载下性能差异可达 3-5x
重配置的三个阶段
Phase 1(预热):在后台启动新配置的 workers 并加载模型参数。这一步最耗时(可能几十秒),但完全不影响旧配置继续服务。关键设计是”影子启动”——新旧配置共存于同一集群,代价是短暂的显存双占用。
Phase 2(切换,< 50ms):
- 停止接收新请求,等待当前 micro-batch 完成(< 10ms)
- 将正在处理的序列的 KV Cache 从旧配置迁移到新配置(< 30ms)
- 新配置接管,恢复服务
Phase 3(清理):异步释放旧 workers 的显存,不影响服务。
50ms 目标的物理基础:A100 的 NVLink 带宽 600GB/s,50ms 内可以迁移约 30GB 数据,足以覆盖中等负载下(约 100 个活跃序列)的 KV Cache 总量。
论文的核心创新:LLM 驱动的意图识别
这是本文与传统规则调度器最根本的区别,也是最容易被忽略的部分。
传统方式的局限
传统自动扩缩容(如 Kubernetes HPA)基于硬阈值规则:
IF cpu_util > 80% THEN scale_up
IF qps > 200 THEN increase_replicas
这类规则无法表达复杂的运维意图,比如:”在保证 P99 TTFT < 2s 的前提下,尽量节省成本;但如果是付费用户,允许临时超出成本预算。”
LLM 意图识别的工作方式
论文的做法是让运维人员用自然语言描述 SLO 意图,由 LLM 将其转化为配置决策的参数化约束:
运维人员输入(自然语言):
“当前集群有 A100×4 和 L40s×4。白天高峰期保证 P99 TTFT 在 2 秒以内,晚上低谷期 GPU 利用率不要低于 40%。重配置过程中断时间尽量控制在 50ms 以内。”
LLM 解析输出(结构化约束):
{
"peak_hours": {"p99_ttft_ms": 2000, "priority": "latency"},
"off_peak_hours": {"min_gpu_util": 0.4, "priority": "efficiency"},
"reconfiguration_budget_ms": 50,
"hardware_preference": {"a100": "high_throughput", "l40s": "memory_efficient"}
}
配置选择器接收这些约束后,在可行配置空间内搜索最优方案。LLM 的作用是充当”意图-参数”的翻译层,处理自然语言中的模糊性(”尽量节省”→具体的 cost_weight 参数)和条件逻辑(”付费用户例外”→优先级覆盖规则)。
为什么这比规则有优势:运维人员不需要知道 TP/PP 是什么,只需要表达业务目标。LLM 可以处理训练数据中未见过的新硬件组合或新 SLO 描述,而规则需要手工枚举。
局限性:LLM 解析本身可能出错,论文中需要一个验证层来检查输出是否满足硬约束(如不超过 GPU 总数、配置在支持列表内)。这个验证层的设计在论文中相对简略,是实际部署的一个风险点。
代码实现
以下代码为核心逻辑的概念性实现,用于说明设计思路。实际生产实现需要处理分布式通信的错误恢复、显存碎片等工程细节。
KV Cache 的分片迁移
class KVCacheMigrator:
"""
KV Cache 在 pipeline 重配置时的迁移逻辑
核心挑战:从 (TP=2, PP=2) 迁移到 (TP=4, PP=1)
- 原配置:Layer 0-15 在 GPU0/1,Layer 16-31 在 GPU2/3,每层按 TP=2 切分
- 新配置:所有层在 GPU0-3,按 TP=4 重新切分
迁移需要先跨 PP 聚合再跨 TP 重分片,是两次通信的组合
"""
def __init__(self, src_workers, dst_workers):
self.src_workers = src_workers
self.dst_workers = dst_workers
def migrate_kv_cache(
self,
src_config: PipelineConfig,
dst_config: PipelineConfig,
active_sequences: List[Sequence],
) -> Dict[int, torch.Tensor]:
migrated = {}
for seq in active_sequences:
# Step 1: 从旧配置聚合完整 KV Cache(先跨 PP 收集,再跨 TP all-gather)
kv_full = self._gather_kv_from_src(seq.seq_id, src_config)
# Shape: [num_layers, 2, num_heads, seq_len, head_dim],2 = K 和 V
# Step 2: 按新配置重新切分并分发到新 workers
migrated[seq.seq_id] = self._scatter_kv_to_dst(kv_full, dst_config)
return migrated
def _gather_kv_from_src(self, seq_id: int, config: PipelineConfig):
# 注意:此处 dist.all_gather 调用为伪代码,实际需要通过 NCCL 通信组实现
all_layers_kv = []
for pp_rank in range(config.pp):
stage_kv = self.src_workers[pp_rank].get_kv_cache(seq_id)
# all-gather 跨此 PP stage 的所有 TP shards
tp_shards = [worker.get_kv_shard(seq_id) for worker in config.tp_group[pp_rank]]
full_kv = torch.cat(tp_shards, dim=2) # 沿 num_heads 维度拼接
all_layers_kv.append(full_kv)
return torch.cat(all_layers_kv, dim=0) # 沿 num_layers 拼接
def _scatter_kv_to_dst(self, kv_full: torch.Tensor, config: PipelineConfig):
# 按新 TP/PP 配置切分:先按 PP 切 layers,再按 TP 切 heads
# ... (分发逻辑省略)
pass
关键设计决策:为什么不直接点对点迁移,而要先 gather 再 scatter?因为 TP 切分维度可能变化(从按列切到按行切),中间态的完整张量是唯一通用的中间表示。代价是短暂的峰值显存占用(原始 + 完整 KV Cache 同时存在),这也是为什么迁移前要等当前 batch 完成。
重配置协调器
class ReconfigurationCoordinator:
"""控制重配置生命周期,目标:服务中断 < 50ms"""
async def reconfigure(self, new_config: PipelineConfig, current_engine: DynamicLLMEngine):
# Phase 1: 后台预热新配置(不中断服务,可能耗时数十秒)
new_workers = await self._warm_up_new_config(new_config)
# Phase 2: 中断窗口(目标 < 50ms)
cutover_start = time.perf_counter()
current_engine.pause_new_requests()
await current_engine.wait_for_current_batch() # 通常 < 10ms
await KVCacheMigrator(current_engine.workers, new_workers).migrate_kv_cache(
current_engine.config, new_config, current_engine.get_active_sequences()
) # 通常 < 30ms
current_engine.swap_workers(new_workers, new_config)
current_engine.resume_requests()
logging.info(f"Cutover: {(time.perf_counter() - cutover_start)*1000:.1f}ms")
# Phase 3: 异步清理旧 workers(不影响服务)
asyncio.create_task(self._cleanup_old_workers(current_engine.old_workers))
配置选择器(接收 LLM 解析后的结构化约束)
class IntentBasedConfigSelector:
"""
基于 LLM 解析后的结构化约束,在可行配置空间内选最优方案
注意:这里的输入已经是结构化参数,LLM 的自然语言→参数转换在上游完成
"""
def select_config(
self,
metrics: ClusterMetrics,
available_gpus: List[GPUInfo],
constraints: dict, # LLM 解析后的约束,如 {"p99_ttft_ms": 2000, "priority": "latency"}
) -> PipelineConfig:
avg_seq_len = metrics.avg_input_seq_len
qps = metrics.requests_per_second
gpu_mem_per_card = min(g.free_memory_gb for g in available_gpus)
# 内存约束:KV Cache 内存 ∝ batch_size × seq_len × num_layers × head_dim × 2
kv_mem_per_seq_gb = (avg_seq_len * 32 * 128 * 2 * 2) / 1e9 # BF16,LLaMA-13B 参数
if constraints.get("priority") == "latency" and qps > 100:
# 延迟优先 + 高并发:最大 TP,减少单卡内存压力,NVLink 带宽足够支撑通信
return PipelineConfig(tp=len(available_gpus), pp=1)
elif constraints.get("priority") == "efficiency" or qps < 20:
# 效率优先或低并发:减少 TP,降低不必要的通信开销
return PipelineConfig(tp=2, pp=len(available_gpus) // 2)
else:
tp = min(4, len(available_gpus) // 2)
return PipelineConfig(tp=tp, pp=len(available_gpus) // tp)
性能实测
测试环境:NVIDIA A100 80GB × 4 + L40s 48GB × 4,LLaMA-13B,CUDA 12.1
以下数据来自论文报告的实验结果,具体数值已基于原文重现,未经独立验证。
重配置中断时间
| 场景 | Active Sequences | KV 迁移时间 | 总中断时间 |
|---|---|---|---|
| 低负载 | 10 | 8ms | 18ms |
| 中负载 | 50 | 22ms | 32ms |
| 高负载 | 100 | 38ms | 47ms |
| 高负载 | 200 | 78ms | 91ms |
Active Seq > 150 时开始超过 50ms 目标。根本原因是 KV Cache 聚合是串行的(每个序列独立 all-gather),实际部署需要结合请求排队控制,或将聚合改为批量异步流水线。
TTFT/TPOT 开销(重配置后前 100 个请求,论文数据)
| 配置切换 | TTFT 增加 | TPOT 增加 |
|---|---|---|
| TP=2→TP=4 | +6.2% | +3.1% |
| TP=4→TP=2 | +8.7% | +4.3% |
| PP=2→PP=1 | +4.1% | +2.8% |
开销主要来自新配置的 KV Cache 内存碎片和缓存冷启动,几百个请求后趋于稳定。TP 缩减(4→2)比扩展(2→4)开销更高,原因是 KV Cache 需要重新合并后重切,通信量更大。
吞吐量对比(动态 vs 静态,24小时仿真负载,论文数据)
| 策略 | 平均 GPU 利用率 | P99 TTFT | 总处理请求数 |
|---|---|---|---|
| 静态 TP=4 | 41% | 3.2s | 基准 |
| 动态重配置 | 73% | 1.8s | +31% |
批判性分析:这个方法的真实局限
50ms 目标依赖活跃序列数:如表格所示,超过 150 个活跃序列就会破坏 SLO。高峰期恰恰是最需要重配置的时候,但也是活跃序列最多的时候,两者形成矛盾。论文通过”重配置触发时主动限流”来缓解,但这本身引入了额外的 TTFT 增长。
LLM 意图解析的可靠性:LLM 将自然语言转化为配置参数这个环节,在生产环境中需要严格的验证层。论文给出了验证框架的描述,但对解析错误率和边界案例的讨论不足。运维人员的表达方式差异很大,”低延迟”对不同人可能意味着不同数量级。
显存双占用问题:Phase 1 预热期间,新旧配置同时加载模型参数,峰值显存需求接近两倍。对于显存已经紧张的集群(L40s 48GB),这个窗口期可能导致 OOM。论文假设集群有足够的空闲显存,这个假设在实际部署中不总是成立。
异构通信开销被低估:A100(NVLink)和 L40s(PCIe)之间的 KV Cache 迁移涉及跨 NUMA 节点通信,实测带宽可能远低于理论峰值。论文的测试环境中 A100 和 L40s 分属不同的通信拓扑,跨 GPU 型号迁移的开销数据在论文中较少涉及。
什么时候用 / 不用?
| 适用场景 | 不适用场景 |
|---|---|
| 负载波动 > 3x 的 LLM 服务 | 负载稳定的批量推理任务 |
| 异构 GPU 集群(混合型号) | 同构集群 + 固定 SLO |
| Serverless LLM 平台 | 模型参数 < 7B(收益不明显) |
| 需要精细成本控制的场景 | 活跃序列数持续 > 150(中断时间超标) |
调试技巧
KV Cache 迁移正确性验证:迁移后的前几个请求做 golden output 对比,与未迁移版本的输出做 token-level 一致性检查。不一致通常指向 KV Cache 的 head 维度拼接顺序错误(all-gather 的 rank 排列)。
Nsight 分析迁移瓶颈:
# 分析迁移期间的 GPU 通信瓶颈
ncu --metrics \
l1tex__t_bytes_pipe_lsu_mem_global_op_ld.sum,\
sm__throughput.avg.pct_of_peak_sustained_elapsed \
--target-processes all \
python migrate_benchmark.py
# 关注 NVLink/PCIe 带宽利用率:迁移是否带宽受限
# 关注 GPU 空闲时间:理想情况下迁移期间 GPU 几乎空闲(等通信),不是计算瓶颈
常见 bug:
- 迁移后首个请求 OOM → 新配置显存估算有误,检查 KV Cache 分配逻辑
- TPOT 持续偏高(不只首 100 请求)→ 新配置的 CUDA stream 没复用,存在同步等待
- 中断时间随 active seq 线性增长过快 → all-gather 串行化,改为批量异步流水线迁移
延伸阅读
- KV Cache 管理:vLLM 的 PagedAttention 论文(SOSP’23)是理解 KV Cache 内存管理的必读材料,动态重配置的内存估算基于同样思路
- Pipeline 并行调度:GPipe、PipeDream 的调度策略直接影响重配置代价,理解 micro-batch 粒度对估算中断窗口很重要
- 热切换通信原语:NCCL 的
ncclCommInitRankConfig支持不重启进程地重建通信组,是实现热切换的关键 API——建议直接看源码注释,官方文档这块比较简略 - 异构调度 Baseline:Orca(OSDI’22)和 Sarathi(OSDI’24)解决了类似的 LLM 推理调度问题,可以作为不引入动态重配置时的性能上界参考
Comments