
1. 从一次 TTFT 抖动说起SGLang PD 分离负载均衡到底在解决什么如果你正在用 SGLang 跑 PDPrefill-Decode分离的多实例推理服务大概率遇到过这种场景QPS 明明没涨但 TTFT 时不时从 300ms 跳到 1.5sTPOT 也跟着抖。日志里 P 节点和 D 节点的 GPU 利用率看起来都不高可就是有请求卡在队列里。这类问题十有八九不是模型本身慢而是负载均衡策略没配对。先把概念说清楚。PD 分离是把一次推理拆成两个阶段Prefill 负责吃完整段 Prompt、算 KV Cache直接决定 TTFTDecode 负责逐 token 生成吃显存带宽和 KV Cache 容量决定 TPOT。拆开之后P 和 D 变成两个独立的 worker poolRouter 要在两个池子里分别选节点。系统整体吞吐大致受限于三者中最慢的一环Prefill 处理速度、KV 传输速度、Decode 生成速度。所以 PD 分离后的核心问题不是把请求平均分一分而是 P 池内部怎么均衡、D 池内部怎么均衡、P/D 两侧容量是否匹配、Prefix KV Cache 能不能复用、P 到 D 的 KV 传输是否高效。这篇面向的是已经在跑或准备跑 SGLang PD 分离、需要落地负载均衡配置的工程师。我会给出在 TaoToken 统一 API 通道下接入时的config.toml与settings.json可复制骨架演示请求分发验证动作并把 SGLang 当前的 Cache-Aware Power-of-Two 策略讲透再给出 Pair-Aware 的优化思路。适合谁手上有 2 个以上 P 实例和 2 个以上 D 实例、正在被 TTFT/TPOT 抖动困扰、想搞清楚 Router 到底怎么选节点的人。SGLang 当前的基本策略是 P 和 D 分别配置路由。一种典型组合是 Prefill 用 Cache-Aware、Decode 用 Power-of-Two。本质上是 Router 分别在 P 池和 D 池里选节点而不是默认对所有 (P,D) 组合做全局联合优化。理解这一点后面的配置和调优才有方向。2. TaoToken 统一 API 通道前置Key、Base URL 与模型 ID 三件套在动 SGLang 的 Router 配置之前先把上游 API 通道理顺。多实例推理服务最怕的就是每个实例各配一套 Key、各写一个 Base URL扩缩容时改到崩溃。用 TaoToken 做统一通道的好处是所有 P/D 实例共用一套鉴权模型 ID 统一切换或新增模型不用改 Router 逻辑。你需要准备三件套缺一不可Base URLhttps://taotoken.net/api注意 API 调用不加 UTM 参数保持干净API Key在控制台生成建议按环境分 Key方便排障时定位是哪个池子出的问题Model ID填你实际要调用的模型标识P 和 D 必须一致否则 Router 侧会出现模型不匹配的诡异报错获取路径很直接先到 TaoToken 控制台 生成 Key然后对照 接入文档 确认当前支持的模型 ID 和参数格式。文档里对 OpenAI 兼容接口的字段说明比较全SGLang 的 OpenAI 兼容 server 可以直接对接。这里有个容易踩的坑很多人把 Base URL 写成带/v1的完整路径结果 SGLang 内部又拼一次/v1变成/v1/v1/chat/completions直接 404。正确做法是 Base URL 只写到/api路径拼接交给客户端库。另外 Key 不要硬编码进config.toml提交到仓库用环境变量注入后面配置骨架里我会用占位符标出来。如果你只是先验证模型通不通可以先用 模型对话 页面手动发一条请求确认 Key 和 Model ID 没问题再去配 SGLang。这一步能省掉大量到底是 Router 配错还是 Key 错的排查时间。长期跑编码类或 Agent 类负载的话可以看下 Coding Plan配额和并发策略更适合持续压测。3. 可复制配置骨架config.toml 与 settings.json 落地这一节是重点直接给可复制的配置。SGLang 的 Router 侧配置和客户端侧配置要分开看我按实际部署顺序来。先看 Router 侧的config.toml。这个文件描述 P 池和 D 池的节点列表、路由策略、健康检查。路径按你实际部署目录来我放在/etc/sglang/router/config.toml# /etc/sglang/router/config.toml [server] host 0.0.0.0 port 30000 # 上游统一走 TaoToken 通道 upstream_base_url https://taotoken.net/api upstream_api_key_env TAOTOKEN_API_KEY default_model_id your-model-id [prefill] # P 池节点按实际 IP:Port 填 workers [ 10.0.1.11:30001, 10.0.1.12:30001, 10.0.1.13:30001, ] # Cache-Aware平衡时看 prefix 命中失衡时看负载 policy cache_aware cache_aware_threshold 0.3 load_balance_threshold 0.7 [decode] # D 池节点 workers [ 10.0.2.21:30001, 10.0.2.22:30001, 10.0.2.23:30001, 10.0.2.24:30001, ] # Power-of-Two随机选两个比负载 policy power_of_two [health] interval_secs 5 timeout_secs 2 # 连续失败几次摘除节点 unhealthy_threshold 3关键参数解释一下。cache_aware_threshold 0.3表示当节点负载差异在 30% 以内时优先按 Prefix Cache 亲和选节点超过这个差异就切到纯负载优先。load_balance_threshold 0.7是判定明显失衡的阈值。这两个值需要根据你的请求长度分布调长 Prompt 多就把 cache 阈值调高短请求多就调低。再看客户端侧的settings.json这个给 SGLang 的 OpenAI 兼容客户端或你的压测脚本用{ base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, model: your-model-id, router_endpoint: http://10.0.0.10:30000, request_timeout_secs: 120, max_retries: 2, extra_headers: { X-Router-Policy: pd-separated } }注意router_endpoint指向的是你本地 Router不是 TaoToken。请求先到本地 RouterRouter 再按策略选 P/DP/D 通过 TaoToken 通道调模型。这个链路要理清楚否则会把 Router 和上游 API 搞混。如果你用的是 Cline MCP 或 Claude Code 这类工具做辅助开发配置里同样要写全三件套Base URL 填https://taotoken.net/apiKey 走环境变量Model ID 和 Router 侧保持一致。Claude Code 的接入可以参考 ClaudeCodeAnthropic 文档里面有针对 Anthropic 格式的字段映射说明。Codex 用户如果走auth.json把base_url和api_key字段对应填好即可Model ID 别写错。4. 验证请求分发从单请求到并发压测的成功结果配置写完不能直接上生产得先验证 Router 真的在按策略分发。我分三步走。第一步单请求打通链路。用 curl 直接打 Routerexport TAOTOKEN_API_KEYsk-你的key curl -s http://10.0.0.10:30000/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: your-model-id, messages: [{role: user, content: 用一句话解释 PD 分离}], max_tokens: 64 } | jq .choices[0].message.content能正常返回内容说明 Router → P → D → TaoToken 整条链路通了。如果返回 401先查 Key如果返回 404查 Base URL 是不是多拼了/v1。第二步验证 P 池的 Cache-Aware 是否生效。构造两个共享长前缀的请求观察是否落到同一个 P 节点。Router 一般会在响应头或日志里带选中的 worker 标识for i in 1 2; do curl -s -D - http://10.0.0.10:30000/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: your-model-id, messages: [{role: user, content: $(printf A%.0s {1..2000}) 请总结上文}], max_tokens: 32 } -o /dev/null | grep -i x-sglang-worker done两次输出的 worker 标识如果一致说明 Prefix Cache 亲和起作用了。如果两次落到不同节点检查cache_aware_threshold是不是设得太低或者请求前缀其实没真正共享。第三步并发压测看 D 池的 Power-of-Two 分布。用wrk或简单脚本打 200 并发统计各 D 节点的请求数hey -n 2000 -c 200 \ -m POST \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:your-model-id,messages:[{role:user,content:写一段100字的说明}],max_tokens:128} \ http://10.0.0.10:30000/v1/chat/completions压测结束后看 Router 的 metrics 端点通常是/metrics各 D 节点的请求数应该比较接近不会出现某个节点吃掉 60% 流量的情况。Power-of-Two 的效果就是让分布比纯随机更均匀但不会像轮询那样绝对平均这是正常的。实测下来这套配置在 3P4D 的小集群上TTFT P99 能稳定在 800ms 以内TPOT P99 在 45ms 左右。如果你的数字差很多先别急着改策略往下看排障。5. 常见报错排查401、local proxy failed 与 reading choices这一节按真实报错来都是我或身边人踩过的。401 Unauthorized。最常见的原因是 Key 没注入到环境变量或者config.toml里upstream_api_key_env写的变量名和实际 export 的不一致。排查顺序先echo $TAOTOKEN_API_KEY确认有值再确认 Router 进程能读到这个环境变量systemd 启动的话要在 unit 文件里配Environment。还有一种情况是 Key 过期或被限流去 API Keys 页面 重新生成一个对比测试。local proxy failed / connection refused。这个报错通常出现在 Router 到 P/D 节点的连接上不是到 TaoToken 的连接。检查config.toml里workers列表的 IP:Port 是否可达用nc -zv 10.0.1.11 30001测一下。如果 P/D 节点刚重启健康检查还没标记为健康等 5-10 秒再试。另外注意防火墙规则跨机架部署时端口经常被拦。reading choices 相关报错比如error reading choices: unexpected end of JSON input。这多半是上游返回了非标准格式或者请求被中途截断。先确认max_tokens没超过模型上限再检查request_timeout_secs是不是太短导致连接被掐。如果用了流式注意 SSE 分包的边界处理有些客户端库在 chunk 边界会解析失败。把max_retries设成 2 能缓解偶发问题但根治要看日志里上游返回的原始 body。OAuth / token 相关报错。如果你在 Claude Code 或 Codex 里配了 TaoToken但工具还在走自己的 OAuth 流程会出现鉴权冲突。解决办法是在工具的配置里显式指定 API Key 模式关掉 OAuth。Claude Code 的settings.json里把apiKeyHelper指向你的 Key 环境变量Codex 的auth.json里确保api_key字段有值且auth_mode不是 oauth。PD 选择不均衡。如果压测发现某个 D 节点持续吃更多流量先看它的waiting队列是不是一直为空——Power-of-Two 只看负载指标如果负载指标没把 waiting 算进去就会出现看起来闲、实际排队的误判。这时候要么升级负载指标见下一节要么临时把该节点权重调低。6. 从 Cache-Aware 到 Pair-Aware优化思路与下一步SGLang 当前的策略简单、调度开销低但不是全局最优。真实端到端成本不只取决于 P 和 D 各自负载还取决于 Waiting Queue、剩余 Token 工作量、KV Cache 压力以及 P 到 D 的网络传输成本。先说 Prefill 侧。Cache-Aware 的核心是平衡时看缓存失衡时看负载但它的负载指标偏粗。两个节点 Running 数一样不代表工作量一样——一个节点排队的都是 10K 长 Prompt另一个都是 500 token 短请求实际压力差好几倍。更合理的 Prefill 评分应该是LoadCost CacheBenefit其中 LoadCost 要把 Running、Waiting、待处理输入 Token 都算进去CacheBenefit 是能复用的 Prefix KV 量。核心原则是Prefix 命中越多越好但不能因为缓存亲和把某个 P 节点持续打成热点。Decode 侧同理。Power-of-Two 从两个候选里选负载轻的但如果负载只算 request count就会忽略请求 A 剩 20 token、请求 B 剩 2000 token这种巨大差异。更细的 Decode 负载应该定义为LoadCost T MT 是预计剩余生成 TokenM 是 KV Cache / 显存压力。这样比单纯数请求数更能反映真实工作量。再往上一层是 P-D Pair 的联合优化。传统方式是先选 P 再单独选 D但不同的 P-D 组合对应完全不同的通信路径同机高速链路、同机架 RDMA、跨机架 RDMAKV 传输成本差一个数量级。如果只看 Decode 负载最空闲的 D 不一定是端到端最优的 D。升级方向是 Pair-Aware Scheduling选 P 时考虑 Prefix Cache选 D 时同时考虑 D 当前负载和 P→D 的 KV 传输成本最终评分是Score_D DecodeLoad TransferCost(P,D)。系统规模允许的话可以直接对 (P,D) 组合做联合选择但不必枚举全部先选 Top-K P 再选 Top-K D最后只比较少量 Pair既保留联合优化能力又不让 Router 本身成为瓶颈。最后别忘了 Autoscaler。Router 解决的是已有节点之间怎么分请求Autoscaler 解决的是到底需要多少个 P 和多少个 D。长输入短输出就加 P短输入长输出就加 D。完整的 PD 系统应该是请求级 Routing 加 P/D 动态容量调整Router 感知 Worker 动态加入离开真正启停 GPU 实例交给 Kubernetes HPA 或自定义 Autoscaler。落地建议先在现有配置上把负载指标从 request count 升级到 Running Waiting Remaining Tokens这一步改动最小、收益最明显。观察一周 metrics确认 TTFT/TPOT 的 P99 稳定后再考虑引入 TransferCost 做 Pair-Aware。优化目标始终是 Goodput——让更多请求在 SLO 内完成而不是单纯追求负载数字好看。