Prefill 与 Decode 分离部署的收益窗口与 KV 传输约束
在一组 8 卡 ****(单卡 64GB HBM,卡间互联 392 GB/s)上用 SGLang 起一个长上下文模型服务,负载形态是长 prompt、高并发。未分离时 prefill 批与 decode 批在同一引擎内混排,新到的大 prompt 反复打断进行中的 decode 批,decode 阶段 ITL p95 抖动、TTFT p95 持续超过 SLO。把两阶段拆成独立实例、按 --disaggregation-mode prefill 与 decode 分别部署后,TTFT p95 收敛,端到端吞吐却下降,decode 实例的稳定性窗口反而不达标。两组现象指向同一处:分离把两阶段的资源画像解耦,随之把 KV cache 的跨实例传输引入关键路径,传输量与互连带宽成为新的约束项。
两阶段的资源画像差异
prefill 阶段对整段输入序列做前向,主体是随 prompt token 数放大的矩阵乘,消耗算力。decode 阶段每步只生成一个 token,却要回读整条序列累积的 KV cache,消耗显存带宽。两者的峰值落在不同资源上,这是分离的动机。
| 阶段 | 主体计算 | 瓶颈资源 | 随什么放大 |
|---|---|---|---|
| prefill | 输入序列的注意力与 FFN 矩阵乘 | 算力(FLOPs) | prompt token 数、批内 prompt 总量 |
| decode | 每步一个 token 的注意力与 FFN | 显存带宽(KV 回读) | 并发序列数、序列长度、层数 |
统一调度时两类批互相干扰。SGLang 的说明列出两种形态:一是 Prefill Interruption,新到 prefill 批频繁打断进行中的 decode 批,token 生成被推迟;二是 DP Attention Imbalance,数据并行下某个 DP worker 处理 prefill、另一个处理 decode,两侧负载不均,decode 延迟被抬高。分离对这两类的处理方式是让两个阶段各自按最优并行度与批策略运行。
分离后新增的约束:KV 传输
分离后,prefill 实例算完的 KV cache 必须传给 decode 实例,decode 才能继续生成。SGLang 当前支持 Mooncake 与 NIXL 作为传输引擎,传输在互连网络上完成。传输量与 KV cache 的大小同阶:
传输量 ≈ 2 × L × H_kv × D × S × b × N
# L 注意力层数
# H_kv 每层 KV 头数(GQA/MQA 下远小于 query 头数)
# D 单头维度
# S 已预填的 token 数(即 prompt 长度)
# b KV 每元素字节数(bf16 为 2,fp8 为 1)
# N 并发序列数
prompt 越长、并发越高,单次请求要搬运的 KV 越多,且每个 decode 实例都在与其它 decode 实例争用同一份传输带宽。SGLang 用多条并行传输队列把这些请求分片,使它们共享线程与带宽;队列数为 1 时退化为按 fcfs 逐个传输。传输相关的控制项以环境变量给出:
| 变量 | 作用 | 默认 |
|---|---|---|
SGLANG_DISAGGREGATION_THREAD_POOL_SIZE |
每个 TP rank 上 KV 传输的 worker 线程总数 | 由 int(0.75 * os.cpu_count()) // 8 计算,限制在 4 到 12 之间 |
SGLANG_DISAGGREGATION_QUEUE_SIZE |
并行传输队列数,多 decode 实例的请求分片共享带宽 | 4 |
SGLANG_DISAGGREGATION_BOOTSTRAP_TIMEOUT |
请求初始化阶段等待目的 KV 索引的超时(秒) | 300 |
SGLANG_DISAGGREGATION_WAITING_TIMEOUT |
请求初始化后等待收到 KV cache 的超时(秒) | 300 |
SGLANG_DISAGGREGATION_HEARTBEAT_INTERVAL |
decode 侧对 prefill bootstrap 服务健康检查的间隔(秒) | 5.0 |
vLLM 一侧对应的是 NIXL 的 KV 传输,走 READ(拉取)模式:decode 端从 prefill 端拉取 KV,原理同样是把 KV 从 prefill 实例搬到 decode 实例;它用超时释放而非租约续租,超时由环境变量 VLLM_NIXL_ABORT_REQUEST_TIMEOUT 控制(默认 480 秒)。传输后端与设备在 vLLM 侧经 --kv-transfer-config 传入 JSON(配置类 KVTransferConfig)指定;SGLang 侧则用参数指定,取值形如 --disaggregation-ib-device ${ib_device},接受共享设备列表、按 GPU 映射的 JSON、或该 JSON 的文件路径三种格式。
收益窗口
分离的收益在长 prompt 加高并发时最明显。此时 prefill 的算力占用与 decode 的带宽占用都接近各自上限,二者在同一引擎里争抢调度机会,混排的损失最大;拆开后 prefill 实例可以按算力导向配并行度,decode 实例按带宽与并发导向配实例数,各自贴近上限运行。prompt 短时两端 KV 传输量小,传输与 bootstrap 的固定开销占比上升,预填又极快,分离带来的调度收益被这段开销抵消甚至转负。
现象
- TTFT 改善、端到端吞吐下降。 首 token 不再被 decode 批拖延,首字变快;但每条请求多了一次 KV 传输,goodput 由吞吐端决定,传输期间 decode 实例空等,端到端 token/s 下降。
- 延迟抖动变大。 decode 实例要等 prefill 侧的 KV 到达才能进入稳态,等待时长受传输队列与带宽争用影响,ITL p95 与稳定性窗口
stability_window_min波动放大。 - 超时字段触发。 传输慢或对端失联时,
SGLANG_DISAGGREGATION_BOOTSTRAP_TIMEOUT或SGLANG_DISAGGREGATION_WAITING_TIMEOUT到达,请求失败;把超时按需放宽,只抬高平均 TTFT 容忍度,并延长对端失联时 prefill 侧释放受影响显存的时间。 - 分段收益错配。 prefill 段的指标改善,decode 段并未同步兑现,只看单阶段数据会把整体结论高估。判定时把 prefill 段与 decode 段的指标变化分开记录,再对照端到端 TTFT、ITL 与吞吐。
定位步骤
- 分开 profile 两个阶段。 因 torch profiler 的限制,prefill 与 decode worker 必须在两次采集中分别 profile,不能一次同时覆盖。SGLang 的压测命令为此提供专用开关:
--pd-separated配合--profile-prefill-url <prefill 实例>采 prefill 侧、配合--profile-decode-url <decode 实例>采 decode 侧,采集前需在相关工作节点设置SGLANG_TORCH_PROFILER_DIR。 - 分段读资源利用率。 prefill 实例看算力利用率曲线,decode 实例看 HBM 带宽利用率曲线;若 decode 侧带宽长期贴顶而算力空闲,瓶颈在 KV 回读或传输,不在算力。
- 量 KV 传输量并对比互连带宽。 代入上面的估算式,用实际 prompt 长度与并发算出单次传输字节数,乘以请求速率,与互连有效带宽对照;传输需求超过带宽即命中新瓶颈。
- 按指标形态区分瓶颈位置。 参考 KV 压力判定的口径:ITL p50 超过 SLO 上限而 TTFT p50 未超,说明瓶颈在 decode 稳态(KV 回读/带宽),不在 prefill;TTFT 也一并越界时则不成立。
- 核对 router 分发。 分离部署由 router 在 prefill 与 decode 实例间分发请求,策略不当会让某些实例过热、另一些空转。
# 示意:分离部署的启动骨架(参数名取自框架,取值按本机规格填)
# prefill 实例
python -m sglang.launch_server \
--model-path <model> \
--disaggregation-mode prefill \
--disaggregation-ib-device ${ib_device} \
--port 30000
# decode 实例
python -m sglang.launch_server \
--model-path <model> \
--disaggregation-mode decode \
--disaggregation-ib-device ${ib_device} \
--max-running-requests 128 \
--port 30001
# router:两个 endpoint 分别指向上面两个实例(取值形如 127.0.0.1:30000)
python -m sglang_router.launch_router \
--pd-disaggregation \
--prefill <prefill-endpoint> \
--decode <decode-endpoint> \
--port 8000
处置
| 动作 | 作用 | 代价 / 前提 |
|---|---|---|
调大 SGLANG_DISAGGREGATION_QUEUE_SIZE |
多 decode 实例的传输请求分片,共享带宽 | 队列过多时线程争用,收益递减 |
调大 SGLANG_DISAGGREGATION_THREAD_POOL_SIZE |
提高单 rank 传输并行度 | 受 CPU 核数与调度开销约束 |
提高 max-running-requests |
让 decode 实例批更满 | 受 KV 池容量与带宽约束 |
| 按阶段分别配置并行度 | prefill 与 decode 各自逼近资源上限 | 需两套实例,硬件占用上升 |
| 放宽 bootstrap/waiting 超时 | 减少传输慢导致的请求失败 | 平均 TTFT 容忍度抬高,失联时资源释放变慢 |
| 非长 prompt / 低并发时退回统一引擎 | 省掉传输与 bootstrap 固定开销 | 长 prompt 高并发下混排损失回归 |
| 拓扑无全互联时退回统一引擎或纯 TP | 分离收益依赖实例间的高带宽互连,互连不足时传输本身成为主瓶颈 | 回到混排的调度损失;需先验证在给定拓扑下拆分是否带来净收益 |
记录字段
# PD 分离结论记录清单(字段名示意)
topology: # prefill / decode 实例数与并行度配置
model_name: # 模型
prompt_len_dist: # prompt 长度分布区间及占比
concurrency: # 并发上限,如 max-running-requests
transfer_engine: # Mooncake / NIXL
ib_device: # SGLang 的 --disaggregation-ib-device 取值形态
kv_bytes_transferred: # 按估算式算出的单次传输字节数
interconnect_bw: # 互连有效带宽
prefill_util: # prefill 实例算力利用率
decode_bw_util: # decode 实例 HBM 带宽利用率
ttft_p50_before / after: # 分离前后 TTFT
itl_p95_before / after: # 分离前后 ITL 尾延迟
throughput_before / after:# 分离前后端到端吞吐
stability_window_min: # 稳定运行时长
verdict: # 收益成立 / 被传输开销抵消 / 工况不匹配
小结
分离把 prefill 的算力瓶颈与 decode 的带宽瓶颈解耦,让两阶段各自贴近资源上限,收益在长 prompt 加高并发时最明显。代价是 KV cache 要在实例间搬运,传输量随 prompt 长度与并发线性增长,与互连带宽对照后才知是否成为新瓶颈。定位时先分开 profile 两阶段并分段读资源利用率,再用 ITL 与 TTFT 的相对越界区分瓶颈落在 decode 稳态还是 prefill;短 prompt、低并发的负载不必分离。