一、背景
1.1 PD 分离及其配比困境
LLM 推理的一个请求生命周期包含两个阶段:Prefill 阶段一次性并行处理全部输入 token,计算密集、单次前向耗时长,直接决定首 token 延迟(TTFT);Decode 阶段逐个生成输出 token,访存密集、单步前向耗时短,决定 token 间延迟(ITL)。两类负载在同一引擎内混合调度会互相干扰——长的 Prefill 计算会阻塞同批 Decode 步进,导致 ITL 抖动。
PD 分离(Prefill-Decode Disaggregation)将两类负载拆分到独立副本组,Prefill 副本计算完后将 KV Cache 传输至 Decode 副本继续生成,从而各自按特征调优。随之而来的问题是配比决策:Prefill 与 Decode 副本数量应为多少?离线 profiling 得到的静态配比建立在特定 ISL/OSL(输入/输出序列长度)分布假设之上,一旦线上流量模式变化(如提示词显著变长、突发请求涌入),静态配比要么让 Prefill 过剩、Decode 欠载造成 GPU 闲置,要么反向失衡导致 TTFT 或 ITL 违约。
1.2 动态 PD 分离的定义
本文所讨论的"动态 PD 分离",指系统能够依据实时负载状态,在不中断服务的前提下自动调整 Prefill 与 Decode 两组副本的数量配比,在满足延迟 SLA 约束的同时最小化 GPU 占用。它包含三个核心子问题:
1. 感知:如何以足够低的开销和足够高的频率获得引擎内部的真实负载状态;
2. 决策:如何将负载状态映射为副本数调整动作,且决策需对资源约束、最小副本数、正在进行的扩缩容等条件鲁棒;
3. 执行:如何将决策可靠地落地到 Kubernetes 等基础设施,并保证调度安全。
Dynamo 的 planner 组件对这三个子问题分别给出了工程化的答案。
二、总体架构:Planner 与连接器
2.1 部署形态与四种模式
planner 是一个独立运行的守护进程,通过 python -m dynamo.planner 启动,入口位于 __main__.py,由 construct_planner 按运行模式装配。源码提供了四种模式,对应 core/adapters.py 中的四个适配器子类:
• prefill / decode:只管辖单侧副本(用于仅一侧需要弹性调整的场景);
• agg:管辖混合负载副本组(聚合模式);
• disagg:PD 分离模式,同时管辖 Prefill 与 Decode 两组副本,是本文的主角。
四种模式共享同一套决策内核,仅观测对象与决策分量不同,体现了"机制与策略分离"的设计取向。
2.2 决策落地链路
planner 的核心决策产物是一个二元组 ScalingDecision(num_prefill, num_decode)——目标 Prefill 与 Decode 副本数。决策本身不直接操作 Pod,而是经连接器(Connector)落地:
• KubernetesConnector:生产环境默认连接器。通过 set_component_replicas 将决策 PATCH 到自定义资源 DGD(DynamoGraphDeployment CRD);deploy/operator/ 下的 Go Operator 监听 DGD 变更,调和派生出 StatefulSet 等底层资源,完成副本增减。
• VirtualConnector:非 Kubernetes 环境(如裸机测试)使用,仅维护虚拟组件副本计数;
• GlobalPlanner:跨 DGD 的全局 GPU 预算协调器,在多个部署之间分配 GPU 配额。
这一分层使决策算法完全与基础设施解耦:算法只关心"应该有几个副本",而"如何安全地变成几个副本"(PDB、亲和性、优雅驱逐等)交由 Operator 生态处理。
三、双循环扩缩容:快与慢的协同
planner 最核心的设计是将扩缩容决策拆分为频率与逻辑都不同的两个循环,源码位于 core/throughput_scaling.py 与 core/load_scaling.py。
3.1 慢循环:吞吐预测定下界
慢循环(Throughput-based scaling)以 Prometheus 采集的流量指标为输入:请求速率、平均 ISL/OSL、KV 命中率、投机解码 accept length 等。其决策链为:
流量指标 → 负载预测器 → 性能模型 → 需求副本数
负载预测器(core/load/predictors.py)提供 Constant、ARIMA、Kalman、Prophet 四种实现,对未来负载区间做出预测;性能模型 find_engine_capacity_rps 计算单引擎在 SLA 内可承载的每秒请求数(RPS);两者相除向上取整即得所需副本数。
关键设计在于:慢循环的产物不直接成为最终决策,而是作为下界(_throughput_lower_bound_p/d)参与后续合成。理由是流量指标本身就是周期平均的产物,采样窗口内的瞬时拥塞会被平均掉——若只按平均吞吐配副本,波峰期间必然违约。以预测容量做下界,相当于为波峰预留了保护垫。
3.2 快循环:负载响应做微调
快循环(Load-based scaling)的输入是 FPM(ForwardPassMetrics):推理引擎在每次前向迭代时通过 ZMQ 上报到事件平面的实时遥测,包含调度中队列长度、各队列的 token 求和、投机解码接受长度等。相比 Prometheus 的秒级窗口聚合,FPM 是迭代级的高频信号,能捕捉到排队拥塞的形成过程。
快循环的处理逻辑:将 FPM 快照喂给在线性能回归模型,估算各引擎当前的 TTFT/ITL,与 SLA 目标(配置项 ttft_ms / itl_ms)比较——所有引擎的估算 TTFT(或 ITL)均超出 SLA 则 +1,持续低于阈值则尝试 -1。每个决策周期最多调整一个副本,以渐进方式逼近配比,避免过冲。
3.3 两循环的合成
最终决策按"快循环为主、慢循环为底"合成:快循环的负载微调结果先经过慢循环下界抬升(max(load_decision, throughput_lower_bound)),再叠加最小副本数约束、GPU 预算带与功耗预算限制(详见 3.5 节)。这种设计让两个循环各司其职——慢循环负责"从趋势上不错配",快循环负责"对当下变化快速反应"。
3.4 优化目标:optimization_target
慢循环是否启用以及 SLA 语义由 optimization_target 配置决定,四种取值对应不同策略形态:
• sla(默认):启用回归模型估算 TTFT/ITL 与 SLA 比较,前述主路径;
• throughput:以吞吐容量为决策依据,不依赖延迟估算;
• latency(即"easy mode"):绕开回归模型,使用静态阈值——Prefill 侧以排队 token 数占上下文长度的比例为信号,Decode 侧以 KV 利用率为信号。性能模型样本不足时的安全退路;
• load:完全由用户自定义阈值驱动,最大化灵活性。
3.5 约束叠加链
决策在生效前依次通过一条约束钳制链(core/budget.py):
1. 最小副本数(min_endpoint):每侧副本不得低于下限;
2. GPU 预算带:通过 proportional_clamp_pair 将两侧副本数按 GPU 配额成比例钳制,容差步长取单副本 GPU 数的最大值,保证钳制结果始终是合法的整数副本组合;
3. 功耗预算(total_gpu_power_limit):预估总功耗不得突破限额;rollout(灰度发布)期间采取 fail-closed 策略——冻结扩容,防止放量与扩容叠加造成功耗越限。
四、性能模型:在线回归与冷启动引导
快循环的核心依赖是一组在线性能回归模型(core/perf_model/),它们必须回答两类反事实问题:"如果队列再长一些,TTFT 会是多少"、"如果批量再大一些,ITL 会是多少"。
4.1 Prefill 一维回归模型
PrefillRegressionModel 以 sum_prefill_tokens → wall_time 的一维线性回归刻画 Prefill 耗时。直接用回归截距当 TTFT 并不充分——真实 TTFT 是排队的前序请求全部完成后才轮到新请求。因此 estimate_queued_prefill_time 按引擎的 max_num_batched_tokens(单次迭代最大可处理 token 数)将排队总量切块,逐块用回归函数累加 wall time,模拟了分块调度的排队演化。KV 命中率(来自慢循环指标)作为折扣因子作用到排队 token 上,上限 0.95,防止命中率异常高时过度乐观。
4.2 Decode 二维回归模型
DecodeRegressionModel 是以 [num_decode_requests, sum_decode_kv_tokens] → wall_time 的二维回归:批量中请求数与 KV cache 总 token 数共同决定一步解码耗时。ITL 估算还需除以投机解码 accept length——一次前向生成多个 token 时,有效 ITL 被成比例摊薄。引擎容量(单副本在 ITL SLA 内可稳定承载的请求数)通过 batch size 二分搜索求得,并以 Little's law(在途请求数 = 到达率 × 停留时间)换算为 RPS 容量。
4.3 样本治理:分桶退避与物理性校验
在线模型的样本质量决定决策质量。两个机制值得注意:
• 分桶退避(bucketed retirement):FPM 样本按输入特征分桶,淘汰时整桶退避而非全局滑窗,保证低频但重要的工况(如超长序列)不会被高频工况淹没;
• 负系数拒绝:回归拟合若产生负斜率(更多 token 反而更快——物理上不可能),该拟合结果被拒绝使用,避免模型外推出荒谬决策。
4.4 冷启动引导:三级数据源优先级
新部署的集群没有任何运行时样本,性能模型不启动则快循环失明。monitoring/perf_metrics.py 中的 fetch_pre_deployment_metrics 实现了按优先级递减的三级引导:
1. worker 自测基准:调用引擎的 get_perf_metrics 端点,让引擎上线时自行跑一组合成基准,生成真实 FPM 样本;
2. AIC 插值(AI Configurator):基于 aiconfigurator_core SDK,依据模型 ID、GPU 型号与后端类型,从内置 profile 数据库插值出合成 FPM(rapid 模式);
3. 离线 profile 数据:加载 profile_results_dir 下预先采集的 NPZ/JSON profiling 数据(thorough 模式)。
三级全部失败则抛出异常拒绝启动(fail-fast);即使引导成功,运行时 FPM 仍会持续积累并最终接管回归拟合。PlannerEnginePerfModel 同时充当 aiconfigurator SDK 的包装器并支持在线修正,使冷热数据无缝衔接。
五、防震荡的缩容安全机制
扩容决策的代价只是"多花一些 GPU",而错误缩容的代价是 SLA 违约与随后的补偿性扩容,形成缩容—扩容震荡。planner 在快循环中实现了 consolidation-aware scale-down(感知合并的缩容),这是源码中最具巧思的部分。
5.1 N/(N-1) 合并重预测
缩容的本质是"把一个副本上的负载合并到其余副本"。候选动作评估时,源码以 consolidation = N/(N-1) 为缩放系数,重预测缩容后幸存副本将承受的负载水平,并执行角色化安全检查。以 Decode 为例(load_scaling.py _decode_load_decision):
• KV 容量硬检查:合并后单副本的调度中 plus 排队 KV token 总量(post_sched_kv)必须小于该引擎 max_kv_tokens。一旦越过 KV 容量边界,请求被迫排队或触发块逐出,进入性能模型无法刻画的非线性区,因此直接整体拒绝缩容;
• SLA 软检查:在合并后的 KV 负载水平下重估 ITL,若超过 itl_ms × sensitivity 则拒绝。该检查与容量检查解耦——实践中引擎常在 KV cache 远未满时延迟就已饱和。
Prefill 的合并检查则聚焦排队诱发延迟:新请求自身的前向耗时 T_own 不随副本数缩小,是不可避免的下界;只有 队列诱发部分 < (ttft_ms - T_own) × sensitivity 才允许缩容(sensitivity 即 load_scaling_down_sensitivity 配置百分比)。被拒绝时记录诊断原因 scale_down_refused_consolidation,运维可从诊断报告直接看到拒绝依据。这套机制使 2→1 这类高风险合并被系统性拦截,从根源上抑制了最常见的小规模震荡模式。
5.2 执行期保护:扩缩窗口与观测对账
除决策期检查外,还有两层运行时保护:
• scaling_in_progress Guard:副本变更从下达到 Operator 调和完成存在分钟级窗口,期间 planner 只观测不决策,避免基于过渡态连发指令;
• FPM 对账(_reconcile_fpm_worker_count):周期性比对 FPM 遥测覆盖的 worker 数与 DGD 实际副本数(含 DP rank 完整性校验)。数据投影引擎缺席意味着观测盲区,此时禁用依赖 FPM 的决策路径。
六、插件化决策流水线
前述双循环作为内置能力运行于一条插件化流水线之上(plugins/ 目录),这是 planner 面向未来扩展的关键架构投资。
6.1 五阶段流水线与合并语义
每个决策 tick 顺序经过四个阶段,EXECUTE 为最终决策投影:
• PREDICT(预测):链式执行(chain_augment)——优先级升序逐个调用,首个产出者胜(first-writer-wins),部分结果沿链传递补全,最终汇入 PipelineContext.predictions;
• PROPOSE(提议):全部插件扇出并发执行,产物经类型感知合并(type_aware_merge);
• RECONCILE(调和):对提议再次扇出过滤、重加权;
• CONSTRAIN(约束):最终钳制阶段,明确禁止 SET(直接指定副本数)类输出——只允许 AT_LEAST/AT_MOST 夹逼与 REJECT,防止约束插件越权改写提议;
• EXECUTE:不为独立阶段,而是将合并产物投影为连接器调用或跳过动作(apply / skip_no_targets / skip_short_circuit / skip_tick_timeout)。
type_aware_merge 的合并语义按类型定义:多条 SET 冲突时优先级最小者胜;AT_LEAST 取最大值作地板、AT_MOST 取最小值作天棚,与 SET 推荐值夹逼出最终目标;任一 REJECT 则整轮短路熔断,直接跳过执行。携带 final=True 标志的提议可完全覆盖其他提议(运行时对非最低优先级使用 final 做检测告警)。
6.2 内置插件
双循环被包装为三个内置插件注册进流水线(engine_adapter.py):
|
插件 |
阶段 |
优先级 |
职责 |
|
builtin_load_predict |
PREDICT |
0 |
运行流量预测器,产出 predictions |
|
builtin_load_propose |
PROPOSE |
10 |
快循环负载微调提议 |
|
builtin_throughput_propose |
PROPOSE |
20 |
慢循环吞吐下界提议 |
两个 PROPOSE 插件合并的结果等价于 3.3 节的"快为主、慢为底"合成——下界语义天然由 AT_LEAST 类型表达。
6.3 节流、熔断与外部扩展
• 执行节拍:流水线基础节拍由 scale_interval_seconds 驱动;每个插件另有 execution_interval_seconds 独立周期(强制不小于基础节拍),未到期的插件以 HOLD_LAST 缓存复用上轮输出,长周期插件(如慢循环)不至于拖慢短周期决策;
• 熔断器:单插件连续失败触发 CircuitBreaker,暂停其调用并隔离故障,防止一个坏插件拖垮整条流水线;
• 外部插件:通过 gRPC Gateway 可在运行期注册外部插件(任意语言实现,FPM 采用 msgpack 跨语言编码),PipelineContext 是唯一输入通道。用户可注入自定义预测器、提议策略或约束条件,与内置逻辑同台合并——这使"动态 PD 策略"从固定算法升级为可编程框架。
七、主循环节奏与工程保障
7.1 tick 编排
planner 主循环(core/base.py 的 NativePlannerBase.run())每个 tick 顺序执行:refresh(刷新配置与集群视图)→ observe(懒拉取 Prometheus 流量、worker 计数与 FPM 快照)→ engine.tick(配置锁内驱动决策流水线)→ apply_effects(下发 ScalingDecision)→ connector.apply_scaling(连接器落地)。全流程与引擎推理路径完全异步,观测数据采集不侵入请求热路径。
7.2 可观测性与试运行
• 全链路 Prometheus 指标(插件调用耗时、合并类型计数、扩缩动作计数);
• DiagnosticsRecorder 输出 HTML 诊断报告,包含每次决策的估算 TTFT/ITL、拒绝原因(如 scale_down_refused_consolidation)、约束钳制轨迹;
• advisory(顾问)模式:只输出决策不实际执行,供新参数上线前的干跑验证。
八、局限与展望
官方设计文档(planner-design.md)坦诚列出了当前实现的边界:
1. 调整间隔与扩容延迟的错配:决策周期可到秒级,但副本冷启动(拉起、加载权重、预热)为分钟级,快循环实际"快"在检测而非生效;突发流量的第一波仍需靠过载缓冲扛过;
2. 平均值预测的天花板:负载预测器输出均值而非分布,对长尾流量形态欠拟合;
3. Local Planner 的管辖范围:当前 planner 决策限于单个 DGD 内部,跨部署的全局配比依赖 GlobalPlanner 协调,其自身仍在演进;
4. 合并假设的理想化:N/(N-1) 均摊假设缩容后负载均匀重分布,未显式建模路由器迁移成本与瞬时不均衡。
这些边界同时勾勒了演进方向:更细粒度的负载分布建模、基于分布的预测、以及与推理引擎协同的原地弹性(如 KV Cache 热迁移减少冷启动代价)。