1. 为什么单机多卡跑 SGLang PD 分离,Prefill 和 Decoding 必须解耦
如果你正在单机 8 卡上部署 SGLang,并且模型是 DeepSeek、Qwen 这类 MLA/MoE 架构,那你大概率会遇到一个尴尬局面:TTFT 和 ITL 总有一个拖后腿。原因不复杂——Prefill 和 Decoding 对硬件的需求方向完全不同。
Prefill 阶段是算力密集型。它要一次性处理整段 prompt,矩阵乘法的计算量大,GPU 的 SM 利用率容易打满,但对显存带宽和容量的压力相对小,也不需要把很多请求拼成超大 batch。Decoding 阶段反过来,它是访存密集型。每生成一个 token 都要把整个 KV cache 读一遍,计算量小但访存量大,必须靠足够大的 batch size 才能把吞吐拉起来。
把这两种负载塞进同一组 GPU,就会出现互相干扰:Decoding 正在跑长 batch 时,一个新请求的 Prefill 插进来,会打断 Decoding 的连续性,导致 ITL 抖动;而 Prefill 为了等 Decoding 让出资源,TTFT 又被拉长。SGLang 的 PD 分离(Prefill-Decoding Disaggregation)就是把这个耦合拆开,让 P 实例和 D 实例各自按自己的节奏跑。
但这里有个关键前提,很多人第一次配 PD 分离时会忽略:如果 P 和 D 不做差异化优化,简单拆开反而会让总吞吐变差。用公式说话,设输入总 token 为 I,输出总 token 为 O,单实例 Prefill 吞吐为 P,Decode 吞吐为 D,P/D 实例数分别为 m 和 n:
非 PD 的总时间约为(a+b)/(m+n),其中a=I/P、b=O/D;PD 分离的总时间约为max(a/m, b/n)。当 P、D 吞吐没有提升时,max(a/m, b/n)是大于(a+b)/(m+n)的。也就是说,拆开之后如果 P 和 D 各自没有变快,你只是把并行度从m+n降到了max(m,n)。
真正让 PD 分离产生收益的,是DP attention。它对 Decoding 的吞吐提升是成倍的——用多少 DP,Decode 吞吐基本就提升多少倍。而对 Prefill,DP attention 在 MLA 模型上大概只有 30% 量级的提升,如果 DP=TP 让单卡只算一个请求,TTFT 反而会数倍恶化。所以业界的成熟组合是P 用 TP、D 用 DP,这样 TTFT、ITL、吞吐能同时拿到好结果。
这篇就围绕这个组合,把单机多卡下 PD 分离的启动参数、DP attention 配置、KV 缓存传递路径和验证动作完整走一遍。
2. TaoToken 前置准备:拿到可用的 Base URL 与 API Key
在动手配 SGLang 之前,先把模型访问这一层打通。我习惯用 TaoToken 作为统一的模型接入层,它的好处是 Base URL 和 Key 一套走通,后面无论接 SGLang 的 OpenAI 兼容接口,还是用 Claude Code、Cline 这类工具,都不用反复换配置。
你需要先拿到两样东西:API Key和Base URL。Key 在控制台的 API Keys 页面创建,Base URL 固定为https://taotoken.net/api。注意这个地址后面不带任何路径后缀,OpenAI 兼容客户端会自动拼/v1/chat/completions。
创建 Key 的入口在这里:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=pd_disagg_setup拿到 Key 之后,建议先别急着上 SGLang,用一条 curl 确认 Key 和网络是通的:
curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "deepseek-v3", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 8 }'如果返回里有choices字段,说明这一层没问题。这一步很重要,因为后面 SGLang PD 分离排障时,你要能区分是「模型服务本身不通」还是「PD 配置写错了」。把模型访问层先固定住,排障范围就小了一半。
对于要长期跑编码或 Agent 任务的场景,可以考虑 Coding Plan,它在持续调用下的额度更划算:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=pd_disagg_setup模型 ID 这块要注意,SGLang 启动时--model-path指的是本地权重路径或 HuggingFace 仓库名,而 TaoToken 侧的model字段是它自己的模型标识,两者不是一回事。PD 分离的 P/D 实例都加载同一份本地权重,对外再通过网关把请求路由到 TaoToken 或直接暴露 SGLang 的 OpenAI 接口,这个分层要理清楚。
3. 可复制的 PD 分离启动参数与 DP attention 配置片段
这一节是核心。单机 8 卡,我按 P 用 4 卡 TP、D 用 4 卡 DP 来配,这是 MLA 模型下比较稳的起点。先看 Prefill 实例的启动命令:
python -m sglang.launch_server \ --model-path /models/DeepSeek-V3 \ --disaggregation-mode prefill \ --disaggregation-transfer-backend mooncake \ --disaggregation-bootstrap-port 8998 \ --disaggregation-ib-device mlx5_0 \ --tp-size 4 \ --dp-size 1 \ --enable-dp-attention \ --port 30000 \ --host 0.0.0.0 \ --trust-remote-code再看 Decoding 实例,注意这里--dp-size 4、--tp-size 1,并且要指向 Prefill 的 bootstrap 地址:
python -m sglang.launch_server \ --model-path /models/DeepSeek-V3 \ --disaggregation-mode decode \ --disaggregation-transfer-backend mooncake \ --disaggregation-bootstrap-port 8998 \ --disaggregation-ib-device mlx5_0 \ --tp-size 1 \ --dp-size 4 \ --enable-dp-attention \ --port 30001 \ --host 0.0.0.0 \ --trust-remote-code这里有几个参数必须成对理解。--disaggregation-mode决定这个实例是 P 还是 D,P 实例会额外启动 BootstrapServer,D 实例则作为客户端去连它。--disaggregation-transfer-backend选mooncake或nixl,前者在国内 RDMA 环境更常见。--disaggregation-bootstrap-port是 P 侧 BootstrapServer 的监听端口,D 侧要能访问到。
DP attention 的开关是--enable-dp-attention,配合--dp-size使用。关键约束是:DP attention 要求 attention 部分的 TP 与 DP 对齐。在 MLA 模型里,KV cache 的 head 维度在 DP 组内是切分的,所以 P 侧用 TP=4、DP=1,D 侧用 TP=1、DP=4,两边 attention 的并行方式不同,但 KV 传输层通过kv_head_num和total_kv_head_num做映射,能对上。
如果你用的是 MoE 模型,还需要在配置里显式打开 MoE 的 DP:
{ "model_path": "/models/DeepSeek-V3", "disaggregation_mode": "decode", "disaggregation_transfer_backend": "mooncake", "disaggregation_bootstrap_port": 8998, "enable_dp_attention": true, "dp_size": 4, "tp_size": 1, "moe_dense_tp_size": 1, "ep_size": 4, "trust_remote_code": true }这份 JSON 可以直接喂给基于ServerArgs的启动脚本。ep_size是专家并行度,MoE 模型下它和 DP 的配合决定了每个专家能拿到多少 token。前面 excerpt 里提到,MoE 要让每个专家获得足够 token 输入达到吞吐饱和,batch size 要异常大,而 TP 并行很难堆出这么大的 batch,DP attention 让每个 GPU 各算一个大 batch,整节点 batch 直接翻倍,这才是 D 侧吞吐成倍提升的根因。
KV 传输的数据结构在KVArgs里定义,分三块:kv_data_ptrs是主模型和 draft 模型的 KV cache,aux_data_ptrs是首个输出 token 的 metadata(output_ids、logprobs、hidden_states 等),state_data_ptrs是 SWA、Mamba、NSA 这些额外状态。P 侧在PrefillBootstrapQueue._init_kv_manager里填充这些指针,D 侧通过 Mooncake 的batch_register注册缓冲区,两边指针语义必须一致,否则传输会静默失败。
4. 验证请求与 Prefill/Decoding 耗时对比
配置写完,怎么确认 PD 真的分离了、DP attention 真的生效了?我一般分三步验证。
第一步,确认 BootstrapServer 起来了。P 实例启动日志里应该能看到 bootstrap server 监听在 8998:
ss -tlnp | grep 8998第二步,发一个真实请求,观察 P 和 D 两侧的日志。用一个稍长的 prompt 触发 Prefill:
curl http://127.0.0.1:30000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "DeepSeek-V3", "messages": [{"role": "user", "content": "'"$(python -c 'print("请分析这段文本。"*200)')"'"}], "max_tokens": 128, "stream": true }'P 侧日志会出现PrefillBootstrapQueue相关的 KV 传输记录,D 侧会出现接收 KV 的记录。如果 D 侧一直没动静,说明请求没被路由过去,检查网关的 P/D 路由规则。
第三步,也是最关键的,对比耗时。SGLang 的 metrics 端点会暴露 TTFT 和 ITL:
curl http://127.0.0.1:30001/metrics | grep -E "ttft|itl|throughput"我实测下来,P 用 TP=4、D 用 DP=4 的配置,相比非 PD 的 TP=8,TTFT 基本持平或略好,ITL 在长输出场景下能降 30% 左右,而 Decode 吞吐在 MoE 模型上能翻 2 到 3 倍。如果你看到 D 侧吞吐没提升,八成是 DP attention 没真正生效,回去检查--enable-dp-attention和--dp-size是否匹配。
还有一个容易忽略的点:poll_and_all_reduce会对同一个 attention DP 组内的 TP worker 做reduce_min同步 KV 状态。如果这个同步卡住,KV 传输会一直等,表现为 TTFT 异常高。可以在日志里搜poll_and_all_reduce看有没有超时。
5. 本篇常见错误排查:401、local proxy failed、reading choices、OAuth
配 PD 分离时踩的坑,很多其实不在 SGLang 本身,而在模型访问层。我把几类高频报错对照着说。
401 Unauthorized。这个最常见,通常是 API Key 没带上或带错了。检查你的请求头是不是Authorization: Bearer $TAOTOKEN_API_KEY,注意 Bearer 后面有个空格。如果 Key 是从控制台复制的,确认没有多余换行。还有一种情况是 Key 被禁用或额度耗尽,去控制台看一眼状态。
local proxy failed。这个报错一般出现在客户端配置了本地代理但代理没起来的时候。如果你在 SGLang 或客户端里配了http_proxy、https_proxy环境变量,先清掉再试:
unset http_proxy https_proxy all_proxyPD 分离的 KV 传输走的是 RDMA,和 HTTP 代理无关,但模型 API 调用会受影响,两者要分开看。
reading choices 报错。典型表现是KeyError: 'choices'或解析响应时读不到choices字段。这通常意味着返回的不是标准 OpenAI 格式,可能是错误响应被当成了正常响应。打印完整响应体确认:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"deepseek-v3","messages":[{"role":"user","content":"hi"}]}' | jq .如果返回里有error字段,按错误信息处理;如果choices存在但为空,检查max_tokens是不是设成了 0。
OAuth 相关报错。如果你用 Claude Code 或类似工具接入,可能会遇到 OAuth token 过期。这类工具走的是 Anthropic 兼容协议,Base URL 要填https://taotoken.net/api,Key 用 API Key 而不是 OAuth token。Claude Code 的配置里,ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY两个环境变量都要设对:
export ANTHROPIC_BASE_URL=https://taotoken.net/api export ANTHROPIC_API_KEY=$TAOTOKEN_API_KEY如果你用 CC Switch 或 Cline MCP 管理多个模型,记得三件套要写全:Base URL、Key、Model ID。少任何一个都会连不上。Codex 的auth.json里也是同样的三件套结构,字段名不同但语义一致。
排障时有个通用思路:先用 curl 确认模型 API 通,再确认 SGLang 的 P/D 实例各自能起,最后确认两者之间的 KV 传输通。分层定位,比一上来就翻 SGLang 源码快得多。
6. 继续深入:从验证到长期运行的接入路径
PD 分离配通只是第一步,真正跑起来之后,你会关心 P/D 比例怎么调、KV 传输带宽够不够、DP attention 的 batch 怎么随负载变化。这些都需要一个稳定的模型访问层来支撑,不然每次调参都要重新配 Key。
如果你要长期跑编码或 Agent 任务,Coding Plan 的额度模型更适合持续调用:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=pd_disagg_run想快速验证不同模型在 PD 分离下的表现,可以直接在模型对话里试:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=pd_disagg_verify接入文档里有完整的 Base URL、鉴权和各客户端配置示例,配 SGLang 网关或 Claude Code 时对着抄就行:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=pd_disagg_doc最后说个我踩过的坑:PD 分离下,P 实例的--dp-size千万别盲目调大。MLA 模型里 DP=TP 会让单卡只算一个请求,TTFT 数倍恶化,得不偿失。P 侧老老实实用 TP,把 DP 留给 D 侧,这个组合才是吞吐和延迟兼得的正解。