AkiYang推理工程手记

一次Ascend 910C推理调优复盘真正有效的优化、失败假设与实验成本

版本与源码依据
首次提交 最近更新 文章修改记录 ↗
本页目录

这轮 DeepSeek-V4-Flash 调优里,最明确的一次吞吐提升来自一个很小的配置变化:把 P 端 --prefill-max-requests 从 1 调到 2。相同 640 条请求下,原生稳态窗口的输出吞吐从 179.04 提高到 236.83 token/s/卡,提升 32.27%;完整窗口也提高了 30.26%。

相比之下,投入了不少适配时间的 DSPARK Compact,在修复正确性问题之后,独立对照仍没有测出吞吐收益。128K 场景专属的静态专家布局确实完成了采集、拟合和加载,同负载吞吐却只提高约 0.81%。

这些结果值得放在一起讲。只列最终配置,看不出收益是怎么来的;只写成功案例,又容易把“功能跑通”“修复错误”和“吞吐优化”混成一件事。

本文复盘的是 2026 年 10 月 18 日至 21 日的一轮 PD 调优,依据当时固定的运行快照与结果,不代表当前 SGLang 上游版本的默认行为,也不混入后续独立的动态 EPLB 实验。实验流程本身的反复返工,在另一篇复盘里展开;这里重点回答:哪些改动有效,为什么有些合理假设没变成收益,以及这些结论花了多少成本。

关于小收益和负例的逐项推演,另见负例分析:为什么合理的优化没有带来吞吐收益,其中补充了到达速率的whole变化,以及Compact实际逻辑验证槽的节省空间。

一、先明确:这次究竟优化什么

部署采用 Prefill / Decode 分离:P 端处理输入并交接 KV 状态,D 端生成后续输出。P、D 各使用 8 张 Ascend 910C,共 16 张物理卡。本文的“每卡吞吐”始终按系统输出吞吐除以这 16 张卡计算,不按逻辑 rank 数,也不只除以 D 端的 8 张卡。

目标是在平均 TPOT 严格小于 20ms 的前提下,让原生稳态窗口输出吞吐超过 230 token/s/卡。TTFT 仍然报告,但不作为这次目标的淘汰条件。因此,达成目标并不表示首 token 延迟已经适合所有在线业务。

实验分成两个独立场景:16K 正文、1K 输出、零前缀缓存,以及 128K 正文、1K 输出、逐请求实际前缀缓存至少 90%。两者后期都采用容易预测的原文复制任务,明确定位为高接受长度展示负载。它有助于研究投机解码,但不能代表一般问答、代码生成或推理任务的容量。

正文长度也与服务实际收到的长度分开。16K 正文为 16384 token,加上该次聊天编码后为 16388;128K 正文为 131072,实际为 131076。正式请求均核验实际输出 1024 token,不通过提前结束缩短工作量。权重和 W8A8 量化保持在各自对照组内一致。

统计上同时保留两种窗口:whole 是整轮正式测量,包含升载和排空;native 是 AISBench 原生算法选出的稳态区间。这里没有自己删除慢请求来构造“稳态”。本项目约定 native 至少纳入 128 条请求且持续 60 秒,用来排除过短窗口;这个门槛并不等于长期稳定性或统计显著性证明。

最终两轮合格结果如下。TTFT、TPOT、E2E 均为各自窗口均值;所有正式请求成功,失败数为 0。

场景与统计窗口 纳入请求数 时间(s) TTFT(s) TPOT(ms) E2E(s) 输出吞吐(token/s/卡)
16K 零缓存,native 493 133.23 24.70 9.4 34.33 236.83
同轮 whole 640 189.15 23.17 9.4 32.76 216.55
128K、缓存90.6222%,native 549 143.49 12.25 10.4 22.87 244.86
同轮 whole 640 179.58 12.68 10.3 23.24 228.09

16K 使用 C128,128K 这一轮使用 C90,配置请求速率都是 6 req/s。这里的 C 是客户端并发上限,不是实际平均并发,也不是完成速率。两种场景的最终配置不同,不能拿 236.83 和 244.86 直接计算“128K 优化了多少”。完整数值摘录保留了比较条件和未观测字段。

二、最有效的改变:让四个token的尾块有机会合批

一开始,很容易把注意力放在 D 端。投机解码直接影响一次能生成多少 token,D 端 graph、验证长度和专家布局也有很多参数可调。但在高 AL 已经达到约 5.7、TPOT 只有几毫秒时,系统吞吐仍然不高。这时,继续提高接受长度未必能填满系统的处理能力。

已有采样里,P 端持续存在排队,D 端等待队列却很低。这还不足以证明 P 是唯一瓶颈,但足以让下一项实验转向 P 的调度:输入究竟怎样分块,每一轮真正带了多少工作?

当时 P 端使用 noCP、DP16,全局 chunked-prefill-size 为 131072,该版本路径下有效 chunk 是 8192。一个实际长度 16388 的请求,在请求数上限为 1 的 CPU 调度验证中,被处理为:

请求 A:8192 → 8192 → 4
请求 B:8192 → 8192 → 4

最后 4 个 token 只占完整 chunk 的很小一部分,却仍需要一次独立的调度和执行。它们的计算量小,不意味着这轮执行没有固定成本。

这里必须分清两个预算。8192 约束一轮可以接纳的 token 工作量,prefill-max-requests 约束一轮接纳的请求数。后者为 1 时,即便 token 预算还空着,也不能再装进下一个请求;改成 2,调度器才有机会同时接纳尾块与下一条请求的一部分。

验证调用的是当时真实的 PrefillAdder 主机方法,而不是另写一个理想化装箱算法。放宽请求数上限后,前几轮实际构造出的批次包括:

cap1:[8192] → [8192] → [4] → [8192] …
cap2:[8192] → [8192] → [4, 8064] → [8192] → [132, 7936] …

第二行是 CPU 验证的真实序列,不是设备时间线,也不意味着所有批次都会恰好装满 8192。在相同 320 条请求、token 总量守恒的条件下,调度轮数从 960 减到 645。它证明了这条合批路径确实可达,但不能用轮数比例直接预测 NPU 提速:批次变大后,执行成本和通信行为也会变化。

随后在设备运行日志的已观察窗口中,看到了两请求 prefill 批次,且没有再出现单独 4-token 批次的报告。这与尾块合批的解释一致。日志没有完整请求 ID,也不是每次 forward 的设备计时,所以最终仍要回到相同负载的端到端对照。

确认轮固定了数据、640 请求、C128、6 req/s,以及同一组静态布局、Compact、IndexCache 和采样设置,只把 P 合批上限从 1 改为 2:

指标 cap1 cap2 变化
native 输出吞吐(token/s/卡) 179.04 236.83 +32.27%
whole 输出吞吐(token/s/卡) 166.24 216.55 +30.26%
native 平均TPOT(ms) 8.8 9.4 略有增加
native 平均TTFT(s) 37.38 24.70 降低

TPOT 没有变得更小,系统完成吞吐却明显提高,首 token 等待也缩短了。这是本轮最有说明力的对照:D 端单请求生成速度已经不错时,改善 P 的批次组织,仍然可以提高整个 PD 系统的产出。

还值得保留一个反例:Pcap2 在较早的另一组配置和负载下曾经负向。因此不能把结论写成“prefill请求数设2总是更快”。这次正向结果成立于当前 chunk、输入长度、静态布局、投机和并发组合。旧负例应该保留这些条件,而不是变成以后永远不准再试的规则。

三、另外两类正向结果:IndexCache与P端静态布局

IndexCache 的对照相对直接。在同一组 16K 普通数据、N640/C64/r3 上,开启 freq2 跨层 TopK 复用后,native 输出吞吐从 164.10 提高到 172.99 token/s/卡,提升 5.41%;whole 从 147.01 提高到 152.08,提升 3.45%。native TPOT 从 18.9ms 降到 17.7ms。

它减少了相邻层重复进行索引选择的工作,但复用也改变了索引选择行为。这轮拿到的是性能证据,没有完整任务质量评估,因此还不能称为严格无损优化。实施时启用的是已绑定源码中的实现,也不应把原有特性写成本轮从零开发的算法。

静态 EPLB 则需要更长的链路。MoE 的逻辑专家被分配到不同执行位置;如果路由经常集中到少数专家或设备,集体执行可能被较慢的一侧拖住。静态布局根据已采到的路由分布调整专家放置,必要时为热点专家设置冗余副本。本轮 P 的候选使用 272 个物理专家槽,包含 16 个冗余槽;D 的候选为 256 槽。

关键在于,P 和 D 不是同一份工作量的两个名字。P 处理输入序列,D 在草稿和验证过程中处理后续 token;16K 零缓存与128K高缓存的有效计算也不同。因此,两端分别采集,场景变化后再评估布局,不能因为文件名写着“128K”就认为已经适用。

真正测下来,P 和 D 的收益差别很大。16K 高 AL 复制集、N320/C128/r6 下,P 从原布局+dynamic副本分发改为场景布局+static副本分发,whole 从 139.17 提高到 158.90 token/s/卡,提升 14.18%。这是布局与分发策略的组合收益,不能单独归给布局文件。这里的“动态放置配置”不表示开启了在线动态 EPLB。候选 native 虽然也上涨,但窗口只有 59.34 秒,低于约定门槛,不能当作充分稳态证明。

D 端换成场景布局,同320负载的 native 只提高约0.35%,whole提高约2.35%。后来128K、90%warm场景重新采集并拟合两端布局,同N160/C64/r6的whole也只从147.91到149.11,约0.81%。

这些小幅变化,没有重复运行和统计分析,不能包装成显著收益。它们说明布局确实适配了场景,但当时限制吞吐的因素没有因此大幅消失。尤其不能从“拟合模型里更均衡”直接跳到“设备吞吐一定同比提高”。

采集过程本身还修过代码:原统计dump路径发生HCCL分配失败,留下的PT无法用于拟合。后续将计数转到CPU,通过已有的同成员Gloo组做整数SUM,再由rank0保存,才完成有效导出。正式性能轮关闭统计记录器。这项修复让布局流程可用,不能计成每token推理提速。

布局与分发的区别详见P端场景静态EPLB专题。

四、Compact为什么没有成为这轮的吞吐突破口

高 AL 负载下,一个自然想法是继续减少验证中的无效工作。原来使用草稿3、验证宽度4时,每轮AL的理论空间不足以达到5.5;改为草稿5、验证宽度6后,才具备研究这一目标的条件。

Compact允许不同请求使用不同验证长度,并配合SPS预算规划减少不必要的验证工作。它看上去与高吞吐目标很贴近,但开启环境变量和指定SPS成本表,只是把执行路径打开。SPS表也不是EPLB专家布局文件:前者服务于投机规划,后者决定专家放置。

当前PD环境还需要解决空闲worker的行为。没有有效请求的worker仍要参与相同的集体执行几何,其dummy行却不能被当成有效请求去更新压缩状态。另外,ragged验证长度必须正确进入压缩位置、graph bucket和replay元数据。只让一个分支能运行,不等于整条状态链已经正确。

适配中找到的一个具体错误位于 dsv4_common_hooks.py:部分位置计算仍假定每条请求固定验证6个token。当同批请求实际验证长度不一致时,这种固定步长可能把写入位置算到另一个请求。

CPU最小反例重现了错误位置,并走过当时安装的hook与backend方法。修复后,相同普通数据320请求中,“替换字符超过10个”的请求数从14降到0;最大SSE间隔从58.26秒降到1.59秒。但native吞吐约下降3.59%,whole基本持平。

这项改动应该保留,因为不能回到已知错误的请求位置去追求更高分数。与此同时,字符和间隔改善也不是完整精度认证;异常消失与任务答案正确,需要不同的验证。

在修复后的高AL复制集上继续做同负载对照,Compact的native吞吐比原静态验证约低3.35%,whole约低4.50%。减少的验证工作没有体现为净吞吐收益。规划、元数据组织、实际验证长度和执行形状都可能影响收支,但这轮短profile没有可靠拆出完整draft/verify设备阶段成本,不能指定其中某项为已证实根因。

固定95%预算也是负例:同N320/C128/r6,whole从158.90降到155.77,AL从5.7645降到5.3203。少做一点验证,如果同时减少每轮有效输出,未必划算,所以恢复了自动预算,没有继续盲扫更多比例。

最终组合里保留了修复后的Compact,随后P布局和Pcap2取得收益。但缺少“相同新布局、相同Pcap2下关闭Compact”的完整匹配对照,不能倒推最终组合证明Compact本身更快。

五、合理的假设怎样变成负例

除Compact之外,还有几项尝试没有带来预期结果。它们值得记录的不是参数值本身,而是原假设被什么数据削弱。

尝试 观察结果 结论范围
P有效chunk8192/round1改为8320/round2 native约−0.97%,whole约−0.66%,TPOT却更低 这组几何与通信配置没有提高完成吞吐,不能只凭局部延迟保留
原场景请求速率4→16 req/s native约181.5392→181.5425,基本不变 该场景继续提高提交速率没有提高处理能力,不代表新场景也无效
CP4改为CP2 同负载native约−0.91%,whole约−1.64% 减少并行通信不自动带来净收益
D graph档位加入6 同N480 native约+3.93%,whole约+0.51% 短筛正向,但不能只展示较大的native涨幅

这些结果都来自有限次数观测。小幅负向不证明参数永远不好,但在没有新机制证据时,已经足以关闭当前候选,避免投入更长确认。反过来,也不能因为一个旧场景平台期,就拒绝另一个场景提高并发。

profile分析还遇到另一种诱惑:看到某个rank的kernel间隙,就把它当成能回收的性能空间。实际只有局部时间窗时,既不知道其他rank是否同时空闲,也不能从一段host提交时间得到完整设备阶段成本。旧trace的IndexCache、graph和活跃DP又可能与当前配置不同。

因此,这轮没有为小copy和局部gap无限扩展冷启动实验。它们提供了诊断线索,却不足以解释整个系统几十个百分点的吞吐缺口。优先级应该回到可能改变批次组织和P/D供给关系的候选上。

还有一个指标教训必须写清。本文AL采用整轮输出token数除以verify次数,包含首token和输出截断影响。它不同于严格草稿接受率。尤其在Compact下,实际验证槽数与实际生成proposal数未必相等,不能用固定gamma乘verify次数补造后者,也不能把 (AL-1)/gamma 标成实测接受率。

最终16K与128K C90的whole AL分别为5.7494和5.7760;实际验证槽口径的接受比例分别为96.1613%和96.4122%。严格的“接受草稿数/实际生成草稿数”分母未完整观测,标为 NOT_OBSERVED。这些整轮计数也没有搬进native窗口。

六、128K实验提醒我:缓存和并发可以改变答案

从16K零缓存切到128K、90%前缀缓存,变化不只是输入长度。相当一部分输入已经复用,P实际需要处理的新增工作不同,KV占用和D端上下文成本也不同。

第一次按目标比例设计共享前缀,经过page128取整后,理论值只有约89.9387%,已经低于要求。之后调整实体数据,再按各DP独立seed和check,正式请求才逐条达到:

实际缓存token / 实际输入token
= 118784 / 131076
= 90.6222%

“配置90%”“聚合平均90%”和“每条至少90%”是三个不同的承诺。复用同一实体数据再测并发时,还可能把独立后缀也缓存进去。因此,同640数据的C128与C90对照先清理旧缓存,再分别做16条seed与16条独立check,准备成本单列。省掉这一步会更快,却可能把缓存收益错认成并发收益。

这两轮whole结果是C128为240.37,C90为228.09 token/s/卡。C90降低了约5.11%的完整窗口吞吐,但TTFT均值从18.14秒降到12.68秒,TPOT从11.5ms降到10.3ms,E2E从29.91秒降到23.24秒。这是容量与等待时间之间的取舍,不能只选一列宣布更好。

C90的原生窗口纳入549条、持续143.49秒,得到244.86 token/s/卡、TPOT10.4ms,满足这次项目的验收目标。C128原native汇总因CPU导入路径失败而没有得到结果,因此不能把C90的native244.86与C128的whole240.37直接比较。

这个例子也解释了为什么要同时保留两种窗口:确认持续处理阶段有没有达到目标,用native;评估整轮有限请求完成得怎样,看whole。一个满足稳态目标的配置,仍可能在升载、排空或延迟上有明显代价。

七、实验成本里,哪些是必要工作,哪些是可避免损耗

负例实验并不等于浪费。提出一个有源码依据的假设,用较短同负载对照否定它,能帮助下一步决策。真正的问题是:一个已经被否定的方向反复重测,或者正式问题还没开始回答,时间就花在了脚本适配和旧断言上。

一次noCP候选,从准备到冻结用了23分02秒。后续控制器又在两轮预热和256条路由验证完成之后,因不再适用的旧环境断言停止,正式负载尚未发送。这不是模型性能回退,而是配置合同检查得太晚。

类似错误还包括:基础脚本写DP16,末尾参数却覆盖成DP4并开启CP;外层dispatch容量调到128,helper又改回64;AIS自定义类在父进程能构造,新worker却无法完成registry构建;正式推理全部完成,native汇总子进程又缺少原payload的导入路径。

这些问题不能靠继续堆局部检查解决。应当让少量验证走过实际边界:配置生成、dump、新进程重载、实际入口与子进程环境。没有变化的代码和分词结果继承旧证明,不能每次调C都重新核一遍。

时间账也说明,不是所有窗口外耗时都能删除:

事件 控制器墙时 正式窗口 需要怎样解释
一次128K布局比较 748.23s 68.68s 本轮模型已加载,但新布局下两轮预热和路由验证仍分别花约255.72s、275.75s
最后一轮128K C90热测 309.16s 179.58s 新增启动、预热、路由均为0,缓存seed/check仍花47.91s

C90控制器比正式窗口多129.58秒。扣除缓存准备后仍有约81.67秒,分布在前置检查和收尾,现有记录没有完整细分。不能把它全叫必要成本,也不能假装已经消除了。表里的阶段互相包含,同一段时间不能重复相加,单请求延迟之和也不能代替并行墙时。

两轮预热和完整路由验证当时是服务变更流程的一部分,缩减方案没有完成等效实测,不能事后宣称全部可删。但只改N、C或请求速率,服务和代码未变时,就应该复用热状态。缓存场景的清理、seed和check另算,不能跟模型重启混在一起。

采集失败的处理也应该直接。推理已完成、服务健康,缺的是CPU导入路径,就只修后处理并读取原数据;一端已成功启动,另一端失败,就保留成功的一端。重新发送模型请求或者机械地回滚两端,会把可修复的编排错误放大成新的机器成本。

这轮之后,我更愿意用几个可观察的结果衡量流程是否改进:纯参数变化是否只需要一份CASE;能否提前说清这次实验决定保留什么;短筛是否及时关闭负例;原结果能否一次给全;收尾错误是否做到零模型重发。写下更多规则,不算已经达到这些目标。

最终保留下来的性能结论并不平均:P合批上限带来了最明确的大幅收益,IndexCache和P静态布局有正向对照;Compact修复解决正确性,但没有独立吞吐收益;D布局、部分graph调整的收益较小。后续若继续优化,仍应从新的瓶颈证据选择下一项实验,同时补齐真实业务质量、严格proposal接受分母和完整draft/verify阶段成本。现有高AL展示成绩不能代替这三件事。


数据说明:本文数字来自上述时间段固定运行快照及原AISBench导出;吞吐变化使用原始精度计算,正文保留两位小数。数值摘录包含场景、窗口、N/C/r、缓存、延迟、接受计数与成本范围;调度验证摘录仅保留CPU批次序列和守恒计数。内部地址、账号、请求正文和私有部署细节不公开。

返回阅读地图 →
读到这里

从一条请求,看见整个系统。

返回文章目录

本页目录

搜索文章与 Skills

↑ ↓ 选择 · Enter 打开 · Esc 关闭 · Ctrl / ⌘ K 搜索

工程图