1. PD分离推理里,Prefill 和 Decode 到底该分多少卡
做 LLM 推理服务的朋友大概率都遇到过这个场景:模型上线后,TTFT(首 token 延迟)和 TPOT(每 token 输出延迟)两个指标总是顾此失彼。你给 Prefill 多分两张卡,TTFT 是好看了,但 Decode 那边排队排到天荒地老,TPOT 直接爆掉;反过来把资源倾斜给 Decode,用户又抱怨"怎么半天不出第一个字"。
这就是 PD 分离(Prefill-Decode Disaggregation)架构要解决的核心矛盾。Prefill 阶段是计算密集型,一次要处理整段 prompt 的注意力计算;Decode 阶段是访存密集型,每个 step 只生成一个 token 却要反复读取 KV Cache。两者的硬件瓶颈完全不同,混在一起部署时,Prefill 的长请求会阻塞 Decode 的连续批处理,导致 TPOT 抖动严重。PD 分离把这两个阶段拆到不同的实例组上,各自独立扩缩容,理论上可以分别优化 TTFT 和 TPOT。
但问题来了:拆开之后,P 和 D 各需要多少资源?这个配比怎么算?
业界目前没有特别靠谱的通用方法。拍脑袋按 1:1 分?按输入输出长度比例分?还是按 QPS 估?我见过不少团队直接拿压测数据试出来的配比,换个业务场景就完全失效。因为 P/D 的资源需求同时受四个变量影响:总吞吐需求、请求的输入长度分布、输出长度分布、以及 P/D 各自能达到的实际吞吐。这四个变量里前三个是业务侧已知的,真正难估的是 P/D 各自的吞吐能力——而它又和 SLO 约束强耦合。
这篇要聊的,就是怎么在 TaoToken 统一 API 通道下,把这套配比算清楚并且落地验证。TaoToken 在这里的角色是提供一个稳定的、可观测的模型调用入口,让你在压测和验证阶段能拿到干净的延迟数据,不被上游路由抖动干扰。它的 API 地址是 https://taotoken.net/api,统一 Key 走一个通道,P/D 两侧的推理服务都可以通过它做基准测试和 SLO 校验。
适合谁看:正在做 LLM 推理服务部署、需要给 PD 分离集群定容量的工程师;或者你已经上了 PD 分离但配比靠猜、SLO 经常飘的团队。下面会从理论公式讲到可复制的配置模板,再到压测验证的完整动作。
2. TaoToken 统一 API 通道的前置准备与 Key 获取
在开始算配比之前,得先把测量工具准备好。因为 P/D 配比计算依赖两个关键实测值:Prefill 的极限吞吐和 Decode 在不同 batch 下的 TPOT 曲线。这两个值都需要你对着真实推理服务打流量测出来,而打流量的客户端如果直连推理引擎,很容易被网络抖动、连接池、重试策略干扰,测出来的延迟不准。
TaoToken 在这里的作用是做一个统一的调用层。你把推理服务注册到 TaoToken 的通道后面,压测客户端统一走 https://taotoken.net/api 发请求,这样延迟数据里排除了客户端到不同实例的网络差异,P/D 两侧的测量基准是一致的。
先拿 Key。打开 https://taotoken.net/api-keys ,登录后创建一个 API Key。这个 Key 同时用于模型对话测试和后续的压测脚本。创建时注意权限范围,压测用的 Key 建议单独建一个,方便后面看调用量。
拿到 Key 之后,先做一次连通性验证。用 curl 发一个最小请求:
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "你的模型ID", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 1 }'如果返回正常,说明通道通了。这一步别跳过,我见过有人 Key 复制时带了空格,压测跑了一半全 401,排查半天。
接下来要确认你的推理服务已经挂在 TaoToken 通道后面。如果你用的是 SGLang 或 vLLM 起的 PD 分离集群,需要在 TaoToken 的控制台里把 P 组和 D 组的 endpoint 分别注册。注册时注意 Base URL 填你推理服务的实际地址,Model ID 填引擎里加载的模型名,要和请求体里的 model 字段一致。
这里有个容易踩的坑:PD 分离架构下,P 组和 D 组的服务发现是分开的。SGLang 的 PD 分离模式里,Prefill 实例和 Decode 实例通过 bootstrap 端口通信,KV Cache 从 P 传到 D。你在 TaoToken 里注册的时候,如果只注册了 P 组的地址,Decode 请求会找不到后端。正确做法是把 P/D 两组都注册,或者注册一个聚合入口(如果你的推理框架提供了 router)。
Key 和通道都就绪后,建议先跑一轮小流量确认端到端链路通。用模型对话页面手动发几个请求,观察 TTFT 和 TPOT 是否在合理范围。如果这一步延迟就很高,先别急着算配比,把链路问题排掉再说。
3. SLO 感知的 P/D 配比计算与可复制配置模板
现在进入核心部分。配比计算的逻辑分三步:先算 Prefill 需要多少资源,再算 Decode 需要多少资源,最后用 SLO 约束把两者卡在一起。
3.1 Prefill 资源量计算
Prefill 阶段的吞吐可以用 M/M/1 排队论建模。核心关系是:实际达成的 prefill throughput 取决于极限 prefill throughput 和系统利用率。极限吞吐怎么测?用输入长度为实际业务输入长度(可以加一些长短波动)、输出长度为 1 的请求,大并发打满 Prefill 节点,从引擎日志或 benchmark 工具里读出来的吞吐就是极限值。
公式简化后是这样:实际 prefill 吞吐 = 极限 prefill 吞吐 × (1 - 排队概率)。而排队概率和 TTFT 目标、请求到达率相关。你给定 TTFT SLO(比如 P99 TTFT < 500ms),就能反推出 Prefill 节点需要支撑的请求到达率,再除以单节点的极限吞吐,就是 Prefill 需要的实例数。
注意一个细节:如果开了 prefix cache,输入长度要换成"未命中 prefix cache 的输入长度"。因为命中缓存的部分不参与实际计算。
3.2 Decode 资源量计算
Decode 没有像 Prefill 那样的理论极限吞吐——因为达到极限吞吐需要的 batch size 远超显存能支撑的范围。所以 Decode 的吞吐只能实测。
实测方法是:固定一个 Decode 实例,逐步增大 decode batch,记录每个 batch 下的 TPOT 和实际吞吐。你会发现两者正相关:batch 越大,吞吐越高,但 TPOT 也越大。然后根据你的 TPOT SLO(比如 P99 TPOT < 50ms),找到满足约束的最大 batch,这个 batch 对应的吞吐就是单 D 实例的有效吞吐。
用总输出 token 吞吐需求除以单 D 实例有效吞吐,就是 Decode 需要的实例数。
3.3 配比参数表与配置模板
把上面两步的结果整理成配置。下面是一个可复制的 JSON 配置模板,你可以直接改参数用:
{ "slo": { "ttft_p99_ms": 500, "tpot_p99_ms": 50, "target_qps": 20 }, "workload": { "avg_input_tokens": 2048, "avg_output_tokens": 512, "prefix_cache_hit_rate": 0.3 }, "prefill": { "instance_type": "A100-80G", "max_prefill_throughput_tokens_per_s": 12000, "effective_input_tokens": 1434, "required_instances": 3 }, "decode": { "instance_type": "A100-80G", "max_batch_size_at_slo": 64, "throughput_at_slo_tokens_per_s": 2800, "required_instances": 4 }, "ratio": { "prefill_to_decode": "3:4" } }这个模板里的数字是示例,你要用自己的实测值替换。effective_input_tokens是avg_input_tokens × (1 - prefix_cache_hit_rate)算出来的。required_instances是总需求除以单实例能力后向上取整。
如果你用 SGLang 部署,对应的启动参数大概长这样:
# Prefill 节点 python -m sglang.launch_server \ --model-path /models/your-model \ --disaggregation-mode prefill \ --disaggregation-bootstrap-port 8998 \ --tp-size 4 \ --max-prefill-tokens 8192 # Decode 节点 python -m sglang.launch_server \ --model-path /models/your-model \ --disaggregation-mode decode \ --disaggregation-bootstrap-port 8998 \ --tp-size 4 \ --max-running-requests 64--max-running-requests就是根据 TPOT SLO 定下来的 decode batch 上限。这个值设太大,TPOT 会飘;设太小,吞吐上不去。
配比算出来之后,建议留 20% 余量。因为实际流量有波动,SLO 是 P99 不是平均值,压测环境和生产环境也有差异。
4. 压测验证:确认配比是否达标
配比算完不算完,必须压测验证。验证的目标是:在目标 QPS 下,TTFT P99 和 TPOT P99 都满足 SLO。
压测客户端统一走 TaoToken 的 API 通道,这样测出来的延迟是端到端的真实值。用 locust 或 wrk 都可以,关键是请求的输入输出长度分布要贴近真实业务。
下面是一个用 Python 写的简单压测脚本,走 TaoToken API:
import time import requests import concurrent.futures API_URL = "https://taotoken.net/api/v1/chat/completions" API_KEY = "sk-你的Key" MODEL = "你的模型ID" def send_request(prompt_len, output_len): payload = { "model": MODEL, "messages": [{"role": "user", "content": "x" * prompt_len}], "max_tokens": output_len, "stream": True } headers = {"Authorization": f"Bearer {API_KEY}"} start = time.time() ttft = None token_count = 0 with requests.post(API_URL, json=payload, headers=headers, stream=True) as r: for line in r.iter_lines(): if line: if ttft is None: ttft = time.time() - start token_count += 1 total = time.time() - start tpot = (total - ttft) / max(token_count - 1, 1) return ttft, tpot # 模拟 20 QPS,输入 2048,输出 512 with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor: futures = [executor.submit(send_request, 2048, 512) for _ in range(200)] results = [f.result() for f in futures] ttfts = sorted([r[0] for r in results]) tpots = sorted([r[1] for r in results]) print(f"TTFT P99: {ttfts[int(len(ttfts)*0.99)]*1000:.1f}ms") print(f"TPOT P99: {tpots[int(len(tpots)*0.99)]*1000:.1f}ms")跑完之后看两个 P99 值。如果 TTFT 超标,说明 Prefill 实例不够,加 P;如果 TPOT 超标,说明 Decode batch 太大或 D 实例不够,减 batch 或加 D。如果两个都超标,先加资源再调 batch。
实测下来,配比调优通常要迭代 2-3 轮才能稳定。第一轮按公式算,第二轮根据压测结果微调,第三轮确认。
5. 常见报错与排查对照
压测和接入过程中,几个高频报错:
401 Unauthorized:Key 不对或没带。检查Authorizationheader 格式是不是Bearer sk-xxx,注意 Bearer 后面有空格。如果 Key 是从控制台复制的,检查有没有多余换行。
local proxy failed / connection refused:TaoToken 通道到推理后端的连接断了。检查推理服务是否还在跑,P/D 两组的 endpoint 是否都注册了。PD 分离架构下,如果只注册了 P 组,Decode 请求会报这个错。
reading choices 时返回空:请求发出去了但响应体里没有 choices 字段。常见原因是 model ID 写错,或者 max_tokens 设成了 0。检查请求体里的 model 字段和 TaoToken 控制台注册的 Model ID 是否完全一致。
OAuth / token expired:Key 过期了。去 https://taotoken.net/api-keys 重新生成一个。压测用的 Key 建议设长一点的有效期,避免跑一半失效。
TTFT 正常但 TPOT 抖动大:Decode batch 设太大了,或者 D 实例的显存不够导致 KV Cache 换出。降低--max-running-requests,或者加 D 实例。
排查顺序建议:先确认 401 类问题(Key 和权限),再确认连接类问题(endpoint 注册),最后看性能类问题(batch 和资源量)。
6. 把配比固化到你的部署流程里
配比算一次不够,业务流量变了要重算。建议把上面那套 JSON 配置模板纳入你的部署流水线,每次扩容或模型切换时重新跑一遍压测验证。
TaoToken 的 API 通道在这里可以持续用:日常监控走 https://taotoken.net/api 的调用延迟,定期用压测脚本校验 SLO。如果发现 TTFT 或 TPOT 连续超标,就触发配比重算。
长期做编码类或 Agent 类负载的话,可以考虑 Coding Plan,把推理通道和开发工具链打通。模型对话页面适合快速验证单个请求的延迟表现,接入文档里有完整的 API 参数说明。
配比这件事,公式是骨架,实测是血肉。别信拍脑袋的比例,也别信一次压测的结果。多跑几轮,把 SLO 卡死,配比自然就稳了。