☰
并非所有预填充都相同:TaoToken 多轮 LLM 服务 PPD 分离配置实战
2026/9/26 14:23:27 网站建设 项目流程

1. 多轮对话里那个让人抓狂的首 token 延迟

如果你正在做多轮 LLM 服务,大概率遇到过这个现象:第一轮响应挺快,到了第三、第四轮,明明用户只补了一句话,首 token 却要等两三秒。你去看监控,发现 Prefill 阶段耗时暴涨,GPU 利用率却不高,网络带宽倒是快打满了。

这个问题的根子在于传统 PD 分离架构的单向 KV 传输协议。Prefill 节点是生产者,Decode 节点是消费者,没有反向通道。上一轮生成的 KV 缓存明明躺在 Decode 节点的显存里,Prefill 节点却拿不到,只能把整个对话历史重新算一遍,再把 KV 传回去。有实测数据显示,这种重计算能占多轮 Prefill 成本的 99%。

更细一点看,Prefill 其实分两种。全预填充(Full Prefill)处理没有任何缓存的新提示词,注意力复杂度是 O(n²),对 Decode 的干扰极大,批量 200 时 TPOT 减速能到 48%。追加预填充(Append-Prefill)只处理新增的那几个 token,复用已有 KV,复杂度是 O(m(n+m)),干扰只有 2% 左右。差了一个数量级。

所以思路就出来了:第 2 轮及以后的追加预填充,完全可以放到 Decode 节点本地做,省掉重计算和 KV 传输。但静态地把所有追加预填充都丢给 Decode 也不是万能药,P 节点稀缺时它是最优解,P 节点充足时反而拖累 TPOT。这就是 PPD(Prefill-capable Decode)动态路由要解决的问题。

这篇不讲论文推导,讲怎么落地。我会用 TaoToken 统一 Key/API 通道接入,给你一份可复制的 config.toml 和 settings.json 骨架,然后演示怎么验证多轮会话下 Prefill 阶段耗时和吞吐的变化。适合正在调多轮服务延迟、想搞清楚 PPD 分离怎么配的工程师。

2. 用 TaoToken 打通统一接入通道

PPD 分离架构的验证需要一个稳定的模型调用入口。多轮会话测试要反复发请求、对比不同路由策略下的 TTFT 和 TPOT,如果每个模型、每个环境都要单独配 Key,光切换就够烦的。

TaoToken 在这里的角色是统一 Key/API 通道。你申请一个 Key,就能通过同一套 API 格式访问不同模型,base_url 指向https://taotoken.net/api即可。对于 PPD 验证场景,这意味着你可以把 Prefill 节点和 Decode 节点的模型调用都收敛到同一个通道,日志和耗时统计也好对齐。

具体操作:先到控制台创建 API Key。地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,登录后在 API Keys 页面新建一个,复制出来存好。这个 Key 后面会写进 settings.json。

如果你还没决定用哪个模型做验证,可以先去模型对话页面试一下手感,确认模型在多轮上下文里的表现符合预期:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。选一个上下文窗口够大、支持前缀缓存的模型,PPD 的效果会更明显。

接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,里面有完整的请求格式和参数说明。API 端点本身不带 UTM:https://taotoken.net/api 。

有一点要注意:TaoToken 是统一接入通道,不是让你绕过什么。它的价值在于把多模型、多环境的调用收敛成一套配置,减少 PPD 验证时的变量干扰。你该做的缓存策略、路由逻辑,一样都不能少。

3. 可复制的 PPD 分离配置骨架

下面这份配置分两部分:config.toml 管服务端的 PPD 路由和节点分配,settings.json 管客户端的 TaoToken 接入。你可以直接拿去改。

3.1 config.toml:PPD 路由与节点池

# config.toml - PPD 分离服务配置骨架 [server] host = "0.0.0.0" port = 8000 model = "your-model-name" max_model_len = 32768 [ppd] # 启用 PPD 动态路由 enabled = true # 第 1 轮强制走 Prefill 节点(无缓存上下文) first_turn_route = "prefill" # 第 2 轮及以后:dynamic 表示按查找表决策 append_prefill_route = "dynamic" # 决策权重:wtft 调大偏向降低首 token 延迟,wtpot 调大偏向稳定生成速度 wtft = 1.0 wtpot = 1.0 # 查找表离线校准文件 lookup_table = "./ppd_lookup.json" # 决策延迟上限(毫秒),超过则回退到默认路由 decision_timeout_ms = 1 [prefill_pool] # Prefill 节点列表,格式 host:port nodes = ["127.0.0.1:8101", "127.0.0.1:8102"] # 每个节点的最大并发预填充数 max_concurrent = 4 [decode_pool] nodes = ["127.0.0.1:8201", "127.0.0.1:8202", "127.0.0.1:8203"] max_concurrent = 8 # 允许 Decode 节点本地执行追加预填充 local_append_prefill = true [kv_transfer] # KV 传输协议:nccl 或 rdma protocol = "nccl" # 传输超时(秒) timeout_s = 30 # 传输队列上限,超过则触发背压 max_queue = 64 [metrics] # 开启 TTFT / TPOT / 吞吐量采集 enable = true export_interval_s = 5

几个关键参数说明。append_prefill_route设成dynamic才会走 PPD 查找表;如果你只想做静态对比实验,可以改成prefill(全走 P 节点,等价传统 PD)或decode(全本地,等价 x=1)。wtft和wtpot是单旋钮控制,调大wtft会让路由更偏向 Decode 本地执行以降低 TTFT,调大wtpot则更偏向 Prefill 节点以稳定 TPOT。

lookup_table指向离线校准生成的 JSON。校准方法:在 x=0 和 x=1 两个极端下,分别测量不同上下文长度、输入输出比、QPS 组合的 TTFT 和 TPOT,算出决策分数存表。在线阶段每个请求查表,1ms 内返回路由决策。

3.2 settings.json:TaoToken 客户端接入

{ "api_base": "https://taotoken.net/api", "api_key": "sk-你的TaoToken密钥", "model": "your-model-name", "default_headers": { "Content-Type": "application/json" }, "timeout_s": 60, "retry": { "max_attempts": 3, "backoff_s": 1.5 }, "session": { "enable_multi_turn": true, "max_history_tokens": 16384, "cache_prefix": true }, "ppd_client": { "report_ttft": true, "report_tpot": true, "route_hint": "auto" } }

api_base固定指向 TaoToken 的 API 端点。session.cache_prefix打开前缀缓存,这样客户端侧也能复用部分 KV,和服务端的 PPD 路由形成配合。ppd_client.route_hint设成auto时,客户端会把每轮的上下文长度和输入输出比上报给服务端,辅助查找表决策。

如果你用的是 Coding Plan 做长期编码或 Agent 场景,配置结构类似,只是模型和会话参数按 Agent 的调用模式调整:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

4. 验证多轮会话下的 Prefill 耗时与吞吐

配置写好了,接下来要证明 PPD 确实起作用。验证的核心是对比三种路由策略在多轮会话下的表现:全走 Prefill(x=0)、全走 Decode 本地(x=1)、PPD 动态路由。

4.1 构造多轮测试请求

写一个简单的 Python 脚本,模拟 5 轮对话,每轮追加新内容,记录每轮的 TTFT 和 Prefill 耗时。

import time import json import requests API_BASE = "https://taotoken.net/api" API_KEY = "sk-你的TaoToken密钥" MODEL = "your-model-name" def multi_turn_test(turns=5, rounds=10): history = [] results = [] for r in range(rounds): session = [] for t in range(turns): user_msg = f"第{t+1}轮:请基于上文继续分析,补充第{t+1}个要点。" session.append({"role": "user", "content": user_msg}) payload = { "model": MODEL, "messages": session, "max_tokens": 128, "stream": False } headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } start = time.perf_counter() resp = requests.post( f"{API_BASE}/v1/chat/completions", headers=headers, json=payload, timeout=60 ) elapsed = time.perf_counter() - start data = resp.json() reply = data["choices"][0]["message"]["content"] session.append({"role": "assistant", "content": reply}) results.append({ "round": r, "turn": t + 1, "ttft_ms": round(elapsed * 1000, 2), "prompt_tokens": data.get("usage", {}).get("prompt_tokens", 0) }) return results if __name__ == "__main__": res = multi_turn_test() for item in res[:15]: print(json.dumps(item, ensure_ascii=False))

这个脚本每轮把完整历史发过去,服务端会根据 PPD 配置决定第 2 轮及以后走 Prefill 还是 Decode 本地。你可以在 config.toml 里切换append_prefill_route的值,跑三组对比。

4.2 采集 Prefill 阶段耗时

服务端 metrics 开启后,每 5 秒导出一次。重点看两个指标:prefill_duration_ms和prefill_queue_depth。前者是 Prefill 阶段实际计算耗时,后者是排队深度。

# 拉取 metrics 端点 curl -s http://127.0.0.1:8000/metrics | grep -E "prefill_duration|prefill_queue|ttft|tpot"

预期结果:x=0 模式下,第 2 轮及以后prefill_duration_ms随轮次线性增长,因为每轮都在重算全部历史。x=1 模式下,第 2 轮及以后prefill_duration_ms大幅下降,因为只算新增 token。PPD 动态路由在 P 节点负载低时可能把部分请求分给 Prefill,负载高时切到 Decode 本地,整体 TTFT 和 TPOT 的帕累托前沿最优。

4.3 吞吐量对比

吞吐量用每秒完成的 token 数衡量。在相同 QPS 下跑三组配置,记录成功率和平均吞吐。

路由策略第2轮+ TTFTTPOT 减速成功率吞吐量
x=0 全 Prefill高,随轮次增长低高负载下可能低于 95%受 KV 传输带宽限制
x=1 全 Decode 本地低,约降 68%略高稳定较高
PPD 动态接近 x=1接近 x=0100%最优或接近最优

这张表是预期趋势,你的实际数字会因模型、硬件、网络不同而变化。关键看相对关系:PPD 应该在 TTFT 上接近 x=1,在 TPOT 上接近 x=0,同时成功率保持 100%。

4.4 网络变慢时的鲁棒性

PPD 的优势在网络变慢时会扩大。因为第 2 轮及以后的请求不再走 P→D 的 KV 传输通道,网络带宽下降对它的影响小。你可以在测试环境里用tc命令模拟带宽限制,观察 x=0 和 PPD 的 TTFT 差距是否拉大。

# 在 Prefill 节点上模拟 100GbE 带宽(示例,按实际网卡调整) sudo tc qdisc add dev eth0 root tbf rate 10gbit burst 32kbit latency 400ms

跑完记得删掉规则:sudo tc qdisc del dev eth0 root。

5. 本篇常见错排查

5.1 第 2 轮 TTFT 没降下来

先检查append_prefill_route是不是真的设成了dynamic或decode。如果还是prefill,那第 2 轮照样走 Prefill 节点重算。再看lookup_table文件是否存在且格式正确,查找表缺失时系统会回退到默认路由。

另一个可能是客户端没开cache_prefix。服务端 PPD 路由依赖前缀缓存命中,如果客户端每次发完整历史但没标记缓存前缀,服务端可能无法正确识别追加预填充。

5.2 成功率掉到 95% 以下

大概率是 KV 传输队列打满。检查kv_transfer.max_queue和timeout_s。在高 QPS 下,如果 P 节点稀缺,x=0 模式的 KV 传输会饱和带宽,导致请求排队超时。把append_prefill_route切到dynamic,让部分追加预填充走 Decode 本地,能显著降低传输负载。

5.3 TPOT 波动大

PPD 动态路由在 P 节点和 D 节点之间切换时,如果wtpot设得太小,可能频繁把追加预填充丢给 Decode 本地,增加 Decode 节点的计算负担,导致 TPOT 抖动。把wtpot调大一点,让路由更偏向 Prefill 节点,TPOT 会稳一些。代价是 TTFT 可能略升。

5.4 TaoToken 请求返回 401

检查 settings.json 里的api_key是否和控制台创建的一致。注意 Key 有有效期,过期了要去 API Keys 页面重新生成:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。另外确认api_base是https://taotoken.net/api,不要多加路径后缀。

5.5 查找表决策延迟超过 1ms

查找表太大或者索引结构不合理。离线校准时控制网格粒度,三个轴(上下文长度、输入输出比、QPS)各分 8 到 10 档就够了,太细会导致查表变慢。如果还是超时,把decision_timeout_ms调大,或者优化查找表的索引方式,比如用有序数组加二分。

6. 把 PPD 验证跑起来

整套流程走下来,核心动作就三个:配好 config.toml 的 PPD 路由参数,用 settings.json 接入 TaoToken 统一通道,然后跑多轮测试脚本对比三种策略的 TTFT 和吞吐。

我自己的经验是,先在单机双卡环境把 x=0 和 x=1 的差距跑出来,确认前缀缓存和 KV 复用逻辑没问题,再上 PPD 动态路由。这样出问题时容易定位是配置问题还是路由逻辑问题。

如果你要长期跑多轮 Agent 或编码场景,Coding Plan 的接入方式可以省掉不少 Key 管理的麻烦:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。模型对话页面适合快速验证模型在多轮上下文里的表现:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。接入文档里有完整的 API 参数说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

最后提醒一句:PPD 的查找表是离线校准的,硬件或工作负载分布大变时它会优雅降级但不再最优。生产环境里最好加一个在线监控,观察 TTFT 和 TPOT 的 P99,偏离校准集太多时重新跑一遍离线校准。

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

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

立即咨询