☰
SGLang PD 分离负载均衡策略与优化思路:TaoToken 统一 API 通道下的配置骨架与验证
2026/10/2 16:22:16 网站建设 项目流程

1. 从一次 TTFT 抖动说起:SGLang PD 分离负载均衡到底在解决什么

如果你正在用 SGLang 跑 PD(Prefill-Decode)分离的多实例推理服务,大概率遇到过这种场景:QPS 明明没涨,但 TTFT 时不时从 300ms 跳到 1.5s,TPOT 也跟着抖。日志里 P 节点和 D 节点的 GPU 利用率看起来都不高,可就是有请求卡在队列里。这类问题十有八九不是模型本身慢,而是负载均衡策略没配对。

先把概念说清楚。PD 分离是把一次推理拆成两个阶段:Prefill 负责吃完整段 Prompt、算 KV Cache,直接决定 TTFT;Decode 负责逐 token 生成,吃显存带宽和 KV Cache 容量,决定 TPOT。拆开之后,P 和 D 变成两个独立的 worker pool,Router 要在两个池子里分别选节点。系统整体吞吐大致受限于三者中最慢的一环: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 URL:https://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。请求先到本地 Router,Router 再按策略选 P/D,P/D 通过 TaoToken 通道调模型。这个链路要理清楚,否则会把 Router 和上游 API 搞混。

如果你用的是 Cline MCP 或 Claude Code 这类工具做辅助开发,配置里同样要写全三件套:Base URL 填https://taotoken.net/api,Key 走环境变量,Model ID 和 Router 侧保持一致。Claude Code 的接入可以参考 ClaudeCodeAnthropic 文档,里面有针对 Anthropic 格式的字段映射说明。Codex 用户如果走auth.json,把base_url和api_key字段对应填好即可,Model ID 别写错。

4. 验证请求分发:从单请求到并发压测的成功结果

配置写完不能直接上生产,得先验证 Router 真的在按策略分发。我分三步走。

第一步,单请求打通链路。用 curl 直接打 Router:

export TAOTOKEN_API_KEY="sk-你的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 + M,T 是预计剩余生成 Token,M 是 KV Cache / 显存压力。这样比单纯数请求数更能反映真实工作量。

再往上一层是 P-D Pair 的联合优化。传统方式是先选 P 再单独选 D,但不同的 P-D 组合对应完全不同的通信路径:同机高速链路、同机架 RDMA、跨机架 RDMA,KV 传输成本差一个数量级。如果只看 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 内完成,而不是单纯追求负载数字好看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询