同一轮压测里,平均 TPOT 已经降到 8.7ms,整套 PD 的输出吞吐却只有 166.24 token/s/卡,离 230 的目标还有距离。
最后带来最大收益的改动,没有继续降低 TPOT。我们把 P 端 --prefill-max-requests 从 1 改到 2:相同 640 条请求,whole 吞吐提高 30.26%,AISBench native 区间提高 32.27%。平均 TPOT 反而略高了。
这件事改变了后续排查的顺序。此前一直盯着草稿长度、接受长度和 Decode 的执行,真正值得先处理的,却是请求怎样从 Prefill 端交付出来。顺着调度过程看下去,一个只有 4 个 token 的尾块,成了这次实验最有用的线索。
本文展开的是 2026 年 10 月 21 日保存的 DeepSeek-V4-Flash / Ascend 910C 实验,使用当时冻结的本地 SGLang 适配。数据、调度验证与结果整理在证据摘录中。相关优化的横向对照见调优总复盘,这里跟着这一个问题走到底。
如果正在排查类似现象,先读第一节的 TTFT、TPOT 与系统吞吐关系,再看 P 端排队和尾块的证据。whole 与 native 的统计窗口贯穿后面的比较,不熟悉时可对照指标口径说明;这里的收益数字仅对应本文保存的模型、负载和配置。
一、先看清这 8.7ms 包含了什么
部署采用真实的 Prefill / Decode 分离:P、D 各 8 张 Ascend 910C,共 16 张物理卡。本文所有单卡吞吐都用系统输出吞吐除以这 16 张卡。模型使用同一份 W8A8 权重,负载是容易预测的原文复制展示集,正文 16384 token,经本次聊天编码后为 16388 token,实际输出固定为 1024 token。
两组对照均发送物理 640 条请求,并发上限 C128,配置到达速率 6 req/s;正式请求全部成功,审计缓存命中为 0。D 端的草稿 5 / verify 6、Compact 自动 SPS、IndexCache freq2,以及 P/D 场景静态专家布局保持一致。变化只有 P 的单次 Prefill 接纳请求上限。
先看 cap1 的 whole 结果:平均 TTFT 为 34.0264 秒,平均 E2E 为 42.9285 秒,而 TPOT 是 8.7ms。请求的大部分生命周期,发生在首 token 到达之前。
TPOT 反映生成过程摊到输出 token 上的时间。它很低,说明这条请求开始持续输出后推进得快;首 token 之前的等待、输入处理和 PD 交接,并不会因此消失。这轮请求输出 1024 token,从首 token 到完成的客户端时间大约是 9 秒,前面却已经等了约 34 秒。
这 34 秒还没有拆成独立的设备阶段:TTFT 同时包含排队、Prefill、交接以及首 token 的相关执行。单看它还不知道哪段最慢,但已经足以提出一个更有用的问题:为什么请求那么久才开始输出?
如果只按 1 / 0.0087 估算速度,得到的是约 115 token/s 的单请求生成速度量级。整套系统却要不断完成新请求,处理它们的 16K 输入,并让 P、D 之间的供给接续起来。系统吞吐最终按整个窗口的输出 token 总量计数,二者用的时间范围不同。
二、C128 已经打满,为什么还要看 P 端
当时最容易想到的是再加并发或速率。请求多一些,D 端也许就能组成更大的批次。
但 C 是客户端在途请求上限:正在 P 前面排队的请求、正在做 Prefill 的请求、已经进入 D 的请求,都可能占着这个上限。客户端显示有 128 条在途,并没有告诉我们其中多少条正在 Decode。
cap1 的 native 区间,AISBench 统计的平均系统并发就是 128。继续往系统里堆请求之前,先看这些请求卡在哪里更划算。已有采样里,P 侧持续排队,D 等待队列为零。这让 Prefill 的供给和批次组织成为优先调查方向;P 内部到底是计算、通信还是调度在限制交付,还要继续往下看。
可以把系统简化成下面这条链路。图里的每条边都可能积累等待,D 上输出流畅的请求,只是已经走过前面阶段的那一部分。
flowchart LR
A[客户端在途请求 C128] --> B[P 等待队列]
B --> C[分块 Prefill 与合批]
C --> D[PD 状态交接]
D --> E[Decode 持续输出]
E --> F[完成并释放客户端名额]
P 交付一个新请求,D 才多一条可继续生成的序列。即使 D 每条序列生成得很快,前面交付慢,输出流也会受影响。反过来,D 已经饱和时,继续加速 P 又可能只会把等待移到另一处。
这也是为什么低 running 数值本身很难指导调参。请求槽之外,还有 token 预算、KV 资源和各 DP 实例的分布。要知道这次为什么没接更多请求,得走到 Prefill 的接纳逻辑里。
三、16388 个 token,为什么会留下一个单独的 4
本次 P 的有效 chunk 是 8192 token。启动配置里的全局 chunked-prefill-size 为 131072,DP16 路径将其除以 DP 数,得到每个 DP 实例使用的 8192。这里的 DP16 是软件并行参数;资源统计仍按前面的 P8+D8 物理卡口径。
所以,这条实际输入为 16388 token 的请求会分成:
16388 = 8192 + 8192 + 4
前两个 chunk 正好填满预算,最后只剩 4 个 token。对这条请求而言,它们仍然要完成 Prefill,之后才能交付后续生成。尾块需要走模型前向和分布式执行,只是新输入 token 数非常少。
接下来要分清两个上限。AISBench 的 C128 控制客户端最多多少请求在途;P 的 prefill-max-requests=1 控制一次 Prefill 批次能接纳多少条请求。后者为 1 时,即使 token 预算还剩很多,当前批次已经放入一条请求,就没有第二条请求的位置了。
在当时的实际 PrefillAdder 主机侧验证里,cap1 的前几个批次是:
请求 A: [8192] → [8192] → [4]
请求 B: [8192] → [8192] → [4]
这里的方括号表示一个批次内各请求贡献的新输入 token 数。[4] 就是一轮只有这个尾块的批次。虽然 chunk 预算设成了 8192,本轮却只带入 4 个新 token。
这时先增大客户端并发,并不会改变批次最多接纳一条请求的事实。队列里可以再多等几条,尾块批次仍然是尾块批次。
四、cap2 改变了批次的接缝
把 prefill-max-requests 改为 2 后,A 的尾块放入批次,接纳器还可以继续考虑 B。token 预算没变,第二条请求只取当前能放下的部分。
实际验证的序列是这样的:
第 1 批:A 8192
第 2 批:A 8192
第 3 批:A 4 + B 8064
第 4 批:B 8192
第 5 批:B 132 + C 7936
第 6 批:C 8192
第 7 批:C 260 + 下一请求 7808
B 在第 3 批先处理 8064,下一批再处理 8192,于是还剩 16388 - 8064 - 8192 = 132。这个尾块又与 C 的第一部分合批,后面的请求沿着同样方式接续。
为什么 A 只有 4 个 token,B 却放入 8064,而非看起来能装下的 8188?因为这条路径的预算按 128-token 页对齐计费。A 的 4 个 token 占用一页预算,余下是 8192 - 128 = 8064。B 剩余的 132 又按两页计费,给 C 留下 8192 - 256 = 7936。
因此 cap2 增加的是请求槽,实际新 token 总量仍受原 chunk 和页对齐约束。它让一个请求的短尾块,接上另一个请求的输入;没有把每批 token 上限翻倍。
在单个接纳流程里依次送入同一组 320 条、每条 16388 token 的输入,实际方法的 CPU 验证给出:
表格可左右滑动 →
| 项目 | cap1 | cap2 |
|---|---|---|
| 完成请求数 | 320 | 320 |
| 输入 token 总量 | 5244160 | 5244160 |
| 调度批次数 | 960 | 645 |
| 单批最大新输入 token 数 | 8192 | 8192 |
cap1 每条三批,320 条正好是 960 批。cap2 变成 645 批,总 token 没少,减少的是这些工作被拆成多少次交给执行端。
CPU 验证时提供了宽松的可用内存,目的是观察接纳与预算行为。真实运行还会受队列到达、KV 资源和 DP 分布影响,所以它先回答“这个参数有没有改变我们想改的东西”,实际能快多少留给下一轮 NPU 压测。
随后短筛保留下来的一段 P 日志有 458 条 Prefill 报告,其中 217 条是两请求批次,单请求 4-token 批次计数为零。这段日志覆盖的是短筛中的一个窗口,与主机验证指向同一种调度变化。
五、先得到一个信号,再做同负载确认
第一次 320 请求短筛,whole 单卡输出从同 N 的 158.8958 提高到 199.4288 token/s,约 25.51%。native 看起来更高,达到了 239.9814,但区间只有约 50.67 秒。
这个信号值得继续。它还没达到当时约定的 60 秒门槛,所以接下来发了一次物理 640 请求确认,服务沿用已经验证的 cap2 配置,没有重新启动模型、重做 MF 或路由验证。cap2 短筛之前的服务切换与配套准备,已经花了约 716.50 秒;确认轮的正式窗口为 189.15 秒,控制器为 292.58 秒。性能表用正式测量窗口,前面的准备成本另留记录。
下面才是本文的主对照:cap1、cap2 各一轮 640 请求,C128、6 req/s,相同输入、输出、源码、静态布局与投机配置。两轮正式请求均为 640 成功、0 失败,whole 缓存审计为 0%。列名中的 native 指原 AISBench 算法选出的区间。
表格可左右滑动 →
| 指标 | cap1 whole | cap2 whole | cap1 native | cap2 native |
|---|---|---|---|---|
| 纳入请求数 | 640 | 640 | 496 | 493 |
| 区间时长(s) | 246.3904 | 189.1495 | 177.2971 | 133.2282 |
| 平均系统并发 | 111.5070 | 110.8591 | 128.0000 | 127.0185 |
| TTFT 平均 / P90(s) | 34.0264 / 39.0140 | 23.1727 / 25.0642 | 37.3785 / 39.2014 | 24.7007 / 25.1003 |
| TPOT 平均 / P90(ms) | 8.7 / 9.3 | 9.4 / 9.6 | 8.8 / 9.4 | 9.4 / 9.6 |
| ITL 平均(ms) | 48.9 | 52.7 | 49.2 | 52.9 |
| E2E 平均(s) | 42.9285 | 32.7640 | 46.3392 | 34.3254 |
| 实际完成速率(req/s) | 2.5975 | 3.3836 | 2.7976 | 3.7004 |
| 输出总吞吐(token/s) | 2659.8434 | 3464.7731 | 2864.7054 | 3789.2292 |
| 输出 / PD16 物理卡(token/s) | 166.2402 | 216.5483 | 179.0441 | 236.8268 |
whole 提高 30.26%,native 提高 32.27%。两种窗口都指向同一方向,native 的两个区间也都超过 60 秒、纳入超过 128 条请求。320 短筛用于决定继续做确认;640 对照用于比较 cap1 和 cap2,前后两个 N 的高低没有混在一起算收益。
cap2 的平均 TPOT 和 ITL 都略高,但 TTFT、E2E 大幅下降,完成速率上升。对照这些指标,收益应从请求等待与供给去解释,不能归为 Decode 每个 token 算得更快。
接受长度也几乎没动。whole AL 按整轮输出 token 数除以 verify 调用数计算,cap1 为 655360 / 114009 = 5.7483,cap2 为 655360 / 113988 = 5.7494。草稿长度、verify 宽度与 D 配置都没变化。这次 Compact 的实际 proposal 分母未采全,严格接受率在证据里保留为未观测;AL 则按上述定义比较。
完整原统计、分位数、配置与计数范围都保留在结果文件里。ITL 使用原 AISBench 口径,投机输出存在多 token 流式事件,因此与 TPOT 分开报告。
六、吞吐提高时,请求究竟少等了什么
先用 whole 窗口看一个简单关系:平均系统并发大约等于完成速率乘以平均 E2E。cap1 是 2.5975 × 42.9285 ≈ 111.507,cap2 是 3.3836 × 32.7640 ≈ 110.859。这只是把同一窗口的统计量连起来理解,没有增加一份独立的测量。
系统里同时在途的请求量相近,每条请求的生命周期却缩短了约 10 秒,于是同样的客户端名额能更快完成一批、再接下一批。固定输出 1024 token 时,完成速率提高,输出吞吐就随之提高。
TTFT 从约 34 秒降到约 23 秒,变化主要发生在首 token 之前;后面的 TPOT 没有改善。结合接纳方法的 token 守恒、批次数减少,以及真实 P 日志里的合批,我们把这轮收益归到 P 端批次组织改善和交付加快上。
更细的时间分解还没有采全:少掉的调度轮次各省多少 host 时间、模型前向与通信怎样变化、交接等待又减少多少,现有数据只解释到这一级。645 与 960 是 CPU 验证中的批次数,30.26% 与 32.27% 是真实系统吞吐变化,两组结果分别回答机制和性能。
这里也没有固定并测量 D 的活跃 batch 大小。P 更快交付后,D 处理的并发组合可能发生变化;生成阶段的 TPOT 略高,正说明端到端变快时,局部指标可以向另一个方向移动。继续追原因,需要对应的 D 批次和阶段计时。
TTFT 降下来之后仍有二十多秒。这次目标是平均 TPOT 小于 20ms 时尽量提高吞吐,TTFT 作为参考保留。如果业务更在意首 token 等待,下一轮的选择还会变化。吞吐达标和在线体验之间,还隔着具体的时延要求。
七、下一次再看到“低 TPOT、低吞吐”
我会先把请求生命周期展开:首 token 前花多久,输出阶段花多久,在途请求分别等在哪里。只拿 TPOT 推算整机容量,容易漏掉输入处理与交接;只看 C 是否打满,又容易把排队误认为 Decode 已经饱和。
然后查实际生效的预算和请求槽。全局 CLI、每 DP 实例的有效 chunk、页大小、一次能接几条请求,几项放到一起,才看得出当前 batch 为什么结束。像这次的 4-token 尾块,如果只看 16K 这个整数标签,很容易被略过去。
机制有线索以后,再做一个只改变相关变量的对照。cap1→2 的关键条件,是当前确实有短尾块、队列里有后续请求,而且 token 与 KV 预算允许继续接纳。换成另一种长度、缓存状态或分布,接缝会变化;我们更早在普通负载上就测过 Pcap2 的负向结果。
因此后续保留的是这一组场景和配置:16K 正文、实际 Chat16388、零缓存、高 AL 复制负载,以及对应的 P/D 源码与布局。对这个组合,native 达到 236.8268 token/s/PD16 卡、平均 TPOT 为 9.4ms;whole 是 216.5483。性能差异指向哪里,下一轮就从哪里继续,而不是把最大吞吐写在配置名旁边就结束。
这次最有用的一步,是从“怎样再省一点 Decode 时间”,转而追问“这些请求什么时候才有机会开始 Decode”。沿着这个问题往前走,才看见 Prefill 批次里那段很短的尾巴。