一群机器人同时打一个大模型服务,为什么 batch 一加就变慢?我拆开 KV cache 的两次断崖
做推理服务的人对这块监控面板都不陌生:单机能扛 16 个并发的时候一切正常,GPU 利用率 60%,P90 延迟两百毫秒;你把上限调到 32,利用率冲到 90%,但业务那边的超时率反而涨了,P90 延迟翻了三倍,吞吐没涨多少。你以为是显存不够,加了一张卡,问题还在。
arXiv 上的这篇《Serving Agentic Robot Fleets》把这种"加并发反而变慢"的现象放进了一个具体的场景里:一群机器人 agent 同时打一个"大脑 + 动作层"的双层推理服务。它的结论不靠建模,而靠回放——80 条 LIBERO-PRO 机器人任务轨迹、1,862 次大脑调用、1,582 个动作块,先在 H800 上分别测出两层模型的真实代价,再把这些请求按原始时序在 vLLM 上重放半小时。
结论很硬:机器人从 20 个加到 32 个,大脑侧的吞吐从 2.40 requests/s 掉到 0.60,全前缀命中率从 99.95% 掉到 47.5%,同时 prefill 的 token 量从每分钟 0.7M 涨到 1.5M,生成量从 27k 掉到 4k。而加一道"活跃 episode 上限"的准入控制,吞吐回到 2.24–2.25 requests/s,机器人程序的完成量从 1.30 涨到 5.00–5.55 episodes/min。
这篇论文对我有意义的地方在于:它把"batch 一加就变慢"这句抱怨拆成了两个完全不同的故障,一个在延迟维度,一个在缓存维度,而且给出了各自的判据和数字。
文章目录
一分钟速览
三句话版本
- 问题:机器人舰队的大脑是长上下文、高频回访的 VLM,动作层是硬截止时间的 VLA。前者按 KV 工作集算容量,后者按截止时间算容量,两个约束互相顶。
- 机制:上下文只增不减(中位 78.1k、最大 214.7k token),每轮只追加 3,279 个 token,却要复用整段前缀。并发一旦超过缓存池能同时容下的会话数,LRU 就开始互相踢,踢一次就要重算整段前缀,重算又拖慢别人,形成正反馈。
- 结果:32 个机器人时吞吐只有 20 个时的四分之一;卡一个活跃 episode 上限 K=16,吞吐回到 2.24 requests/s、命中率回到 100%、完成量涨 3.8–4.3 倍。
这篇为什么值得读
因为它给的不是"某个调度器更好",而是三个可以直接搬到线上的判据:一是容量要按两层分别算,动作层看截止时间,大脑层看常驻工作集;二是监控要看"可复用前缀是否还活着",而不是只看 KV 占用率;三是准入要卡在 episode 边界,不要卡在请求边界。
如果你正在做 agent 类产品的推理部署——不管是机器人、浏览器 agent 还是代码 agent——这篇的数字未必对你的模型成立,但它的两个诊断信号是通用的:computed prompt tokens 与 generated output tokens 的比值一旦开始反向走,你就已经进入抖动区了。
两层负载:大脑和动作层是两种机器
一个回合里有两种请求
论文里的机器人程序是这样一个循环:大脑(GPT-6-Astra,走 RPent 的工具框架)看到场景、判断进度、选一个工具;工具可能是渲染、几何推理、脚本化移动,也可能是一个动作策略 π0.5。动作策略把指令变成一小段电机动作,也就是一个 action chunk。执行完,环境往前走几步,大脑再被调用一次。
这个循环在网络上的投影是两类完全不同的请求:
- 大脑请求:输入是整段累积的多模态对话,输出是一小段决策(中位 144 个 token)。上下文长、有状态、可复用。
- 动作请求:输入是一帧观测加一句指令,输出是 5 个去噪步算出的一段动作。上下文短、无状态、延迟敏感。
论文的关键设计是把这两层放在分开的 GPU 池上分别测,再把大脑层的请求序列按原始时序回放到 vLLM 上。所以它报的每个数字都能追到"哪个模型、什么并行度、什么负载"。

图 1|机器人程序向大脑发"带增长上下文的请求"、向动作层发"动作块请求",两层在各自的 GPU 池上测量,大脑的依赖请求再回放到 vLLM。
三层时钟:7.78 秒在等什么
论文区分了三种时间,这一点很值得抄到自己的埋点里。大脑请求延迟是一次托管调用的实际耗时;环境步间隔是两次大脑调用之间环境推进了多少步、除以 20 Hz 的仿真频率;仿真器墙钟间隔是同一段间隔的真实墙钟时间,里面混进了软件渲染和逆运动学的开销。
| 时间量 | 中位(秒) | P90(秒) |
|---|---|---|
| 大脑请求延迟(Astra) | 7.78 | 13.84 |
| 环境步间隔 | 1.00 | 3.10 |
| 仿真器工具墙钟间隔 | 8.40 | 22.20 |
| 大脑延迟 + 环境步间隔 | 9.40 | 15.38 |
| 大脑延迟 + 仿真器墙钟间隔 | 17.96 | 32.77 |
这张表最刺眼的一行是前两行:大脑一次要 7.78 秒,而机器人"物理上"只前进了 1.00 秒。也就是说在一个回合里,机器人有八成以上的时间是站着等模型想明白。最长的单次大脑请求达到 48.26 秒。
论文报告的是中位、P90 和最大值,没有报告 P99,也没有报告尾延迟的分位数曲线。这一点在落地时要自己补:对 deadline 型负载,P99 才是决定 starvation 的那个数。
大脑的调用密度是动作层的 8.6 倍
80 条轨迹(四个 suite,每个 suite 十个任务两个种子,74 条成功)里,大脑被调用 1,862 次,动作工具入口只有 216 次(186 次 pi0_pick、30 次 pi0_doubled),动作模型内部执行了 1,582 个推理块。大脑轮次是动作工具入口的 8.62 倍,是单个动作块的 1.18 倍。
把环境推进时间加起来是 2,309.05 秒,对应每仿真秒 0.806 次大脑调用。四个 suite 分别是 1.010、1.141、0.820、0.640 次。这个数字是"推理需求相对物理进度"的密度:密度越高,同一个机器人在单位物理时间里越要吃 GPU。
对做容量规划的人来说,这三个数的含义是:大脑是常客,动作层是稀客。所以共享 GPU 池里最先被压垮的一般不是动作模型,而是那个被反复回访的大模型。
负载画像:上下文只增不减
上下文是怎么长到 215k 的
1,862 次调用里,输入长度中位 78.1k token,P90 131k,最大 214.7k。每次调用中位引用 21 张图,最多 141 张。整条 episode 的输入 token 总量中位 1.31M。
按 suite 拆开看,差别主要来自"任务有多长":
| suite | 成功 | 大脑轮次 | 峰值上下文 | 总输入 | 动作块 | 环境推进(秒) |
|---|---|---|---|---|---|---|
| object_swap | 19/20 | 17.0 | 89.5k | 1.07M | 8 | 16.1 |
| spatial_swap | 20/20 | 18.5 | 91.1k | 1.27M | 8 | 14.9 |
| goal_swap | 19/20 | 19.0 | 95.5k | 1.32M | 15 | 21.8 |
| 10_swap(长任务) | 16/20 | 33.5 | 136.3k | 3.05M | 27 | 44.0 |
| 全部 | 74/80 | 19.0 | 95k | 1.31M | 12.5 | 19.5 |
表里的数字除了成功数都是每条 episode 的中位数。可以看到长任务 suite 的轮次是其他 suite 的近两倍,峰值上下文高 40%,总输入高 2.3 倍。这就是为什么"同一个服务、不同任务"的容量曲线会差一个档位:平均上下文长度直接决定你能同时装下几个会话。

图 2|四条 suite 的输入上下文随大脑轮次的增长曲线(中位数与四分位区间):前 5 轮就爬到 60k,之后近似线性,长任务一路走到 180k 以上。
曲线形状比数字更重要:它不是"涨到某个阈值然后截断",而是单调、近似线性、没有回落。这意味着任何一个按"平均长度"做的容量估算都会偏乐观,因为峰值总在最后几轮出现。
3,279 个新 token 撬动整段前缀
这是全文我最喜欢的一个数字。中位一次调用只追加 3,279 个输入 token(P90 10,475),却要带着整段前缀一起进模型;全量 prompt 长度与新增长度的比值中位是 25.5 倍。
换句话说:如果前缀还在缓存里,这一轮只需要算 3,279 个 token 的 prefill;如果前缀被换出,就要算 95k 个 token 的 prefill。25.5 倍的重算量,换来的是同一个输出——144 个 token 的决策。
R = L f u l l Δ L ≈ 25.5 R = \frac{L_{\mathrm{full}}}{\Delta L} \approx 25.5 R=ΔLLfull≈25.5
其中 L_full 是整段 prompt 长度,ΔL 是新追加的 token 数。这个比值就是"前缀缓存被打掉一次"的代价倍数,也是后面所有抖动的放大器:缓存命中率每掉一个点,prefill 量就按这个倍数往上翻。
38.6% 的工具间隔是零步
1,782 个 episode 内间隔里,环境步间隔中位 1.0 秒、P90 3.1 秒、最大 7.5 秒。其中 688 个间隔(38.6%)一步都没推进——连续观察、连续推理、或者调用了不产生位移的工具。
这个数字对缓存策略的含义是反直觉的:超过三分之一的"工具等待"其实没有物理时间流逝,等待长度由 agent 自己的决策节奏决定,而不是由机器人运动决定。同一段间隔在仿真器墙钟上是中位 8.40 秒(渲染和逆运动学撑起来的),但真正决定回放调度的是那个 1.0 秒。

图 3|episode 内工具间隔的经验分布:蓝线是环境步间隔(机器人物理时间),橙线是仿真器墙钟间隔,38.6% 的间隔推进零步,单独在零点处标注。
两条曲线的水平距离就是"仿真开销":同一段逻辑间隔,墙钟上要多花几倍时间。做回放或压测时如果照抄墙钟间隔,你会把负载强度低估好几倍。
这一节的两张图要合起来读:一张是上下文怎么长(图 2),一张是回访有多密(图 3)。前者决定每个会话占多少缓存,后者决定它多久回来一次,两者相乘就是缓存需要同时容下的工作集——这也是后面断崖出现的物理原因。
动作层:截止时间决定能带几个机器人
0.25 秒的截止时间只够一个机器人
π0.5 在 batch 1 下的中位推理时间是 160 毫秒:视觉编码约 19 毫秒,VLM prefill 约 19 毫秒,五个去噪步每步约 23 毫秒。论文的容量定义是:在 5 步一个 chunk、固定控制频率下,两次重复运行中 starvation 比例都不超过 1% 的最大机器人数。
D = 5 f D = \frac{5}{f} D=f5
D 是一个动作块的截止时间,5 是每块的步数,f 是控制频率。20 Hz 对应 0.25 秒,10 Hz 对应 0.5 秒,5 Hz 对应 1 秒。
| 动作块截止时间 | 1 张 GPU | 2 张 GPU |
|---|---|---|
| 0.25 秒(20 Hz) | 1 | N≥8 没有一次通过 |
| 0.50 秒(10 Hz) | 8 | 16 |
| 1.00 秒(5 Hz) | ≥32 | ≥32 |
这张表是全文最"可操作"的一张。它说的是:动作层的容量不是由 GPU 算力决定的,是由你的控制频率决定的。同一个模型、同一张卡,20 Hz 只能带 1 个机器人,5 Hz 能带 32 个以上——因为每秒钟需要的 chunk 数少了 4 倍。
注意 0.25 秒那一行两卡的格子:最小的测试规模是 8 个机器人,已经超标,更小的规模论文没有测。所以严格说"两卡在 0.25 秒下能带几个"是未测量的,不能反推。
batch 从 1 加到 64,延迟从 160 ms 到 1.03 s
这是"batch 一加就变慢"最直接的一张图。七个实测 batch:1 是 160 毫秒,8 是 249 毫秒,64 是 1.03 秒;每点 50 次运行、前置 10 次预热。

图 4|π0.5 的中位推理延迟随 batch 的变化(七个 batch 各 50 次实测中位):batch 1 到 8 还算平,16 之后开始陡,64 时到 1.03 秒。
拿这些实测延迟做一次除法,就能看出"算术吞吐"和"截止时间容量"为什么会背离:
- batch 1:每秒 6.25 个 chunk;
- batch 8:每秒约 32 个 chunk;
- batch 64:每秒约 62 个 chunk。
batch 从 1 到 64,算术吞吐涨了近 10 倍,但单次延迟涨了 6.4 倍。对 20 Hz 的机器人来说,每个机器人每秒需要 4 个 chunk,所以 batch 1 的 6.25 只够 1.5 个机器人——实测容量正好是 1。而 batch 64 的 62 个 chunk/s 理论能喂 15 个 20 Hz 机器人,可它单次就要 1.03 秒,早就超过 0.25 秒的截止时间了。对 deadline 型负载,延迟本身就是容量,吞吐再高也没用。
论文还提到一个细节:Nsight 显示小 batch 去噪时 GPU 有大量空闲,符合启动和同步开销的解释。这也解释了为什么 batch 8 之前延迟几乎不涨——那一段是"用空闲换吞吐"的免费区间。
starvation 曲线怎么读

图 5|三档截止时间下 starvation 比例随机器人数增长:横轴是机器人数,纵轴是最差一次运行里被饿死的控制 tick 比例,虚线是 1% 的 SLO。
三条曲线给出三种完全不同的运维姿态:
- 0.25 秒:几乎没有安全区。单卡 1 个机器人勉强压线,8 个就到三成多 tick 被饿死,32 个接近七成;双卡只是把曲线压低一点,没有改变形状。
- 0.50 秒:单卡 8 个机器人贴着 1%,16 个开始超,32 个到四成;双卡 16 个能过,32 个到一成多。
- 1.00 秒:单双卡在 32 个以内全部贴着零轴,测不出上限。
论文对机制的说明值得记住:一个 chunk 用完才发下一个请求,必然会等;到达相位不齐又会限制 batch 形成。所以真实容量低于"峰值算术吞吐除以每机器人需求"这个上界。拿实测延迟套一下:0.5 秒档单卡要喂 8 个机器人,需要每秒 16 个 chunk,而 batch 4 到 8 的实测区间正好提供 20 到 32 个——容量 8 落在这个区间里,是自洽的。但这类除法对"你取哪个 batch 点"极其敏感,只能当量级校验,不能当容量预测。
还有一个容易被忽略的细节:两张卡不是一张卡的两倍。0.5 秒档单卡 8 个、双卡 16 个,这是线性的;但 0.25 秒档双卡在 8 个机器人时就已经超标,而单卡在 1 个机器人时刚好压线——从 1 到 8 这一段,加一张卡并没有把容量抬上去。论文把原因归在批形成与到达相位,而不是峰值算力:每张卡只分到四个机器人,chunk 到达互相错开,批凑不起来;凑不起批,单次延迟就贴着 0.16 到 0.25 秒,任何一次相位错开都会直接变成 starvation。贴着截止时间跑的系统没有排队余量,加卡只能摊薄平均负载,摊不平抖动。
这也是论文在讨论部分把两层写成"联合包络"的原因:动作层的容量随截止时间、副本数变化,大脑层的容量随张量并行度、准入会话数变化,两个包络的交点才是这台机器真正能带多少个机器人。只优化其中一层,另一层立刻变成新的天花板。
KV 工作集:保留与不保留差 6 倍
保留前缀:6.8 秒变 1.7 秒
大脑代理是 Qwen3.6-27B,BF16,TP=4,跑在 H800 上,vLLM 0.17.1 开了自动前缀缓存,混合注意力用 784 token 一个块,实测缓存池 4,270 和 4,396 块两种。
| 上下文 | 40k | 78k | 130k | 215k |
|---|---|---|---|---|
| TTFT(前缀已换出) | 1.34 | 2.85 | 5.32 | 10.26 |
| TTFT(前缀保留) | 0.18 | 0.22 | 0.27 | 0.35 |
| 150 token 调用(已换出) | 2.66 | 4.23 | 6.78 | 11.86 |
| 150 token 调用(保留) | 1.50 | 1.61 | 1.73 | 1.95 |
单位是秒。150 token 的调用时间是实测 TTFT 加上 149 个 batch-1 解码间隔(解码约 8.9–10.7 毫秒每 token),保留那一列还额外追加了 3.3k 新 token。
读法很直接:TTFT 从 1.34 秒涨到 10.26 秒,是 7.7 倍;而保留前缀之后,同一段上下文的 TTFT 只有 0.18 到 0.35 秒,几乎不随上下文长度变化。130k 上下文时,一次 150 token 调用从 6.8 秒降到 1.7 秒,约 4 倍。

图 6|150 token 调用的估计时长随上下文变化:橙虚线是前缀被打掉,从 2.66 秒涨到 11.86 秒;蓝实线是前缀保留,几乎贴着 1.5–2 秒。
这张图解释了一件很多人忽略的事:长度本身不是问题,长度的重算才是问题。一个 215k 上下文的会话如果一直命中缓存,单次调用只花 1.95 秒;一个 40k 的会话如果一直被打断,要花 2.66 秒。前者上下文长 5 倍,却更快。
一条断崖:28 个能全留,32 个全丢
论文做了个很干净的实验:预热 N 个独立的 100k token 会话,按固定循环顺序依次回访,每次追加 3.3k token,看多少会话还能保住完整前缀。

图 7|循环回访下"完整前缀存活比例"随会话数的变化:批量预热时 28 个全留、32 个全丢;顺序预热时 29 个全留、31 个全丢。
三组配置:
- 4,270 块 + 批量预热:N=28 全部保留,N=32 全部丢失。
- 4,396 块 + 批量预热:N=29 到 31 全部丢失。
- 4,396 块 + 顺序预热:N=29 全部保留,N=31 全部丢失。
这不是"命中率慢慢下降",而是从 100% 直接掉到 0%。论文把它归到经典的 LRU 工作集机制:重新填充一个被换出的会话,正好会把下一个会话需要的前缀挤出去,于是每一个都保不住。
这个数字可以自己换算。名义容量是块数乘 784:4,270 块是 3.35M token,4,396 块是 3.45M。把三组结果换成占名义容量的比例,能保住的最高点都落在 84% 附近——4,270 批量预热在 28 个会话(2.8M,84%)时全留,4,396 顺序预热在 29 个(2.9M,84%)时全留;而丢光的那一侧,4,270 批量要到 96% 才全丢,4,396 批量在 84% 就已经全丢。所以断崖不是一个固定百分比,预热顺序会把它推来推去:最坏的顺序在 84% 就开始丢,最好的顺序能撑到 96%。
N c l i f f ≈ 0.85 × C p o o l L a v g N_{\mathrm{cliff}} \approx 0.85 \times \frac{C_{\mathrm{pool}}}{L_{\mathrm{avg}}} Ncliff≈0.85×LavgCpool
C_pool 是缓存池的 token 数(块数 × 784),L_avg 是平均上下文长度。0.85 是我按"最坏的那个预热顺序"取的保守系数——不是论文给的公式,是给读者用的估算式。用论文的工作负载代入(4,396 块、平均 95k)得约 30.8,和实测的 29 到 31 对得上;如果你只信最好的那个顺序,系数可以放到 0.95。
为什么是断崖而不是斜坡
因为换出和重算是互相喂养的。一个会话等得越久,它下一次能复用前缀的机会越小;被换出之后要重算整段前缀,重算又占住 GPU,让别人等得更久;别人的等待又提高了别人被换出的概率。论文把这称为 KV 驻留与 prefill 的反馈回路。
这解释了一个运维上很难理解的现象:系统在抖动时,KV 占用率指标往往不高。因为大量请求卡在 prefill 或者排队,真正在跑的请求反而少。论文后面那个 AIMD 控制器就是被这个指标骗了。
抖动:从 20 到 32,吞吐掉 4 倍
机器人越多,完成的活越少
持续回放的设定是:N 个虚拟机器人,每个完成一条 episode 立刻领一条新的,永远有活干。

图 8|持续舰队的吞吐与命中率:圆点是历史 20 分钟扫描,叉号是 30 分钟重复运行;吞吐在 20 个机器人处见顶,之后一路下滑,命中率同步塌陷。
数字:
- 20 个机器人:两次 30 分钟运行,2.40–2.42 requests/s,非首轮全前缀命中率约 99.95%。
- 28 个:0.66–0.83 requests/s。
- 32 个:约 0.60 requests/s,聚合命中率 47.5%。
- 48 个(更早的一次不限流运行):0.52 requests/s。
从 20 到 32,机器人数涨 60%,吞吐掉到四分之一。这不是"效率降低",是绝对完成量下降——加机器让整个系统干的活变少了。论文的措辞是 roughly fourfold fewer。
两个反向的计数器
机制证据来自 vLLM 自己的计数器。

图 9|上:全前缀命中率;中:本地计算的 prompt token 量;下:生成的 output token 量。N=20(蓝)三项都平,N=32(橙)在第 4 分钟命中率塌陷,prompt 量翻倍,生成量掉到不足七分之一。
健康的 N=20 运行:每分钟算约 0.7M prompt token,产出约 27k output token。抖动的 N=32 运行:prompt 涨到约 1.5M/min,output 掉到约 4k/min。
prompt 涨 2.1 倍,output 掉 6.75 倍。GPU 更忙了,干的活更少了——多出来的算力全部花在把同一段前缀重新算一遍。这就是"batch 一加就变慢"在计数器上的样子:如果你的 prompt_tokens_total 增速超过 generation_tokens_total,你不是在扩容,你是在自燃。
抖动有潜伏期
这一点最容易被值班同学忽略:抖动不是一开始就发生的。
- 六次 30 分钟的 N=24 运行里,四次分别在 480、960、1080、1200 秒首次跨过 80% 命中率阈值,另外两次到 30 分钟结束都没跨(右删失)。
- 两次 N=28 运行都在 360 秒跨过。
也就是说:同一个配置,有的跑 8 分钟才崩,有的跑 20 分钟才崩,有的跑满 30 分钟都不崩。如果你的压测只跑 5 分钟,你会得到一个完全错误的容量结论。论文用 Kaplan–Meier 估计来刻画这个"首次低命中窗口"的时间分布,并把未发生的事件按删失处理——这个做法本身值得抄进压测报告。
反过来,恢复也不是瞬间的。两次过载/撤载实验里,命中率在过载撤掉之后恢复,其中一次大约 6 分钟恢复。还有三组 N=22 的到达路径回放:冷启动 2.36、斜坡上升 2.38、斜坡下降 1.73 requests/s。同样的平均并发,到达历史不同,稳态差 27%。
有限批次会自己好起来
论文做了一个很重要的对照:让每个机器人只跑两条 episode 就退出(有限批次),而不是永远补新活。
结果:N=24 时 48 条 episode 全部完成,N=28 时 56 条全部完成;在 N=28 的有限回放里,随着活跃工作集排空,命中率最终回到了全前缀命中。
而持续补给的那一组,同样的种子、同样的 N=28,一直停在低命中区。这个对照说明:CONCUR 那类针对"有限 agent 批次"的结论不能直接搬到"永久在线舰队"上——批处理任务会自然排空,在线舰队不会。如果你跑的是离线批处理,你的抖动可能会自愈;如果你跑的是常驻服务,它不会。
准入控制:把并发和机器人数拆开
卡的是活跃 episode,不是请求
论文的准入很简单:设一个上限 K,最多允许 K 条 episode 同时活跃,多出来的机器人排队等下一轮再开始。判断时机在 episode 边界,不在请求边界。
为什么是边界而不是请求?因为一个 episode 内部的请求是依赖链:第 5 轮必须基于第 4 轮的上下文。中途拒掉一个请求,前面已经付掉的 prefill 全部作废,重试还要再付一次。而 episode 边界拒绝的代价只是"晚一点开始",没有沉没成本。
这条设计选择把两个平时混在一起的控制变量拆开了:舰队规模 N 决定有多少程序想被服务,活跃工作集 K 决定引擎能不能保住它们的 KV。N 是业务需求,K 是运维旋钮。
一个静态上限把吞吐拉回 2.24 req/s
N=48 时的静态上限扫描:
| 上限 K | requests/s | 全前缀命中率 | 有效运行数 |
|---|---|---|---|
| 12 | 2.027 | 100.00% | 1 |
| 16 | 2.269 | 100.00% | 1 |
| 20 | 2.377 | 99.95% | 2 |
| 24 | 2.132 | 98.01% | 1 |
K=20 最好,2.377 requests/s,比历史上不限流的 N=48(约 0.52)高 4.6 倍。K=24 反而回落(2.132),论文没有解释这一档的具体原因,但从命中率掉到 98.01% 能看出它已经开始出现换出——上限并不是越大越好。
N=32 的对照更干净:不限流 0.598 requests/s,K=16 的两次运行是 2.235 和 2.253,且非首轮全前缀命中率 100%。
更值得注意的是程序级的收益:

图 10|完成的机器人程序数(每分钟)与 Jain 公平性指数:不限流的 N=32 只有 1.30 episodes/min,K=16 的两次是 5.00 和 5.55;N=48 配 K=20 是 5.80 和 5.60。
- N=32 不限流:完成 39 条,1.30/min,公平性 0.806。
- N=32、K=16:完成 100 和 111 条,5.00 和 5.55/min,公平性 0.959 和 0.980。
- N=48、K=20:完成 116 和 112 条,5.80 和 5.60/min,公平性 0.947。
吞吐涨 3.7 倍,完成量涨 3.8–4.3 倍,公平性从 0.806 涨到 0.95 以上。公平性这一项容易被忽略:不限流时那 0.806 意味着有相当一部分机器人几乎没被服务到——它们的 episode 一直没能完成。准入不只是让系统更快,是让服务更均匀。
为什么 AIMD 控制器一次都没降
论文还试了一个模仿 CONCUR 的 AIMD 控制器:每 30 秒读一次 KV 占用、前缀命中率、队列深度,在 episode 边界调整 K。
结果很尴尬:N=32 时 1.280 requests/s、命中率 88.4%;N=48 时 1.161 requests/s、命中率 85.8%。都明显低于静态 K。更要命的是,80 条控制记录里乘性下降一次都没触发,抖动期间 K 一路涨到 27。
原因是控制信号选错了。规则要求"KV 占用率高 且 命中率低"才降 K,但抖动时 kv_cache_usage_perc 度量的是运行中请求占用的存储,大量请求正卡在 prefill 或排队,这个占用率反而不高。于是控制器看到的是"还有余量",继续加 K。
论文的结论很克制:准入的遥测应当直接反映可复用前缀是否还活着。翻译成人话:不要用"显存用了多少"当准入信号,要用"上一轮的前缀还在不在"。
对你的服务有什么用
先分清你踩的是哪一种慢
同样是"加并发变慢",两种故障的处理方式完全相反:
- 截止时间型:单次延迟超过 deadline,任务被饿死。特征是 p99 延迟上翘、GPU 利用率不高、队列不长。解法是加副本、放宽 deadline、或者把 batch 压小。
- 工作集型:缓存被互相踢出,prefill 量暴涨。特征是 p99 延迟上翘、GPU 利用率很高、队列变长、生成 token 速率下降。解法是降并发,不是加卡。
判据只有一句话:看 prompt token 的增速和 output token 的增速是否背离。背离就是工作集型,加卡只会让抖动来得更快——因为你把更多长上下文塞进了同一个池子。
你的断崖在哪:一条可以自己算的线
用上面那个估算式,先算你自己的 N_cliff:
- 拿到你的 KV 池 token 容量:块数乘块大小(vLLM 启动日志里有)。
- 乘 0.85 得到可用工作集。
- 除以你的平均上下文长度(不是峰值,是同时在跑的会话的均值)。
举例:一个 3.45M token 的池子,平均上下文 95k,N_cliff 约 31;平均上下文如果是长任务 suite 的 136k,N_cliff 只有 21。同一个池子,任务变长 40%,能带的会话少了三分之一。
论文实测的"健康区"是 20 个左右,24 个开始六个里有四个在 8–20 分钟内崩掉,28–32 必崩。你可以把这当成一个校准点:如果你的模型和池子跟论文接近,健康并发大概在 N_cliff 的三分之二处,也就是留 33% 的余量,而不是 15%。因为抖动一旦发生,重算会进一步推高有效工作集。
监控这四个指标
论文用到的信号,加上我建议补的:
full-prefix hit:完整前缀命中率。这是最重要的一个,按 120 秒窗口聚合,低于 80% 就该报警。注意论文的口径是"排除每条 episode 第一轮",因为首轮必然不命中。computed prompt tokens / min:本地实际算的 prefill 量。它应该跟请求量同比例增长;如果请求量没涨它却涨了,说明在重算。generated output tokens / min:有效产出。它和上一个指标的比值就是"算力有效率"。num_requests_running与num_requests_waiting:队列。抖动时 waiting 会堆积,而 running 不一定高。kv_cache_usage_perc:要看,但不能单独看。论文的 AIMD 失败就是因为它。
这五个里,前两个是论文明确用的,后三个是 vLLM 暴露的标准指标(不同版本前缀可能是 vllm:,字段名略有差异)。
参数怎么调
按论文的结论,调整顺序应该是这样:
- 先定 deadline 和控制频率。这是业务约束,不是运维参数。20 Hz 和 5 Hz 之间差 32 倍以上的容量,先确认你的机器人真的需要 20 Hz。
- 再定 chunk 长度。论文用的是 5 步一块。块越短延迟越低、容量越高,但请求数变多、调度开销变大;块越长越省调度,但截止时间变松。
- 再定准入上限 K。用上面的 N_cliff 乘三分之二起步,在 episode 边界执行。
- 最后才调
max_num_seqs、gpu_memory_utilization这类引擎参数。注意max_num_seqs卡的是"同时在 decode 的序列数",它不等于你的活跃工作集——一个 32 并发、每个 100k 上下文的负载,max_num_seqs设 32 只会让 32 个会话同时抢块。
一个容易踩的坑:把 gpu_memory_utilization 调高确实能换来更大的池子,但论文的断崖说明有效容量大概只有名义容量的 85%(最坏顺序下更接近 84%),堆显存带来的边际收益会在断崖处突然归零。
压测要跑够 30 分钟
这是我从这篇论文里抄得最直接的一条经验。论文的容量扫描里,20 个机器人那一档跑 30 分钟稳定在 99.95% 命中率;24 个那一档六次里有四次分别在 8、16、18、20 分钟才崩。如果你的压测只跑 5 分钟,24 这一档看起来完全正常,你会把 K 定在 26,然后在生产环境的第 20 分钟翻车。
所以压测设计应该是这样:
- 时长按"最慢那条 episode 走完两遍"来定,论文是 30 分钟。低于 15 分钟的结论只能当烟雾测试。
- 负载形态要持续补给。每个虚拟机器人跑完就领新活,不要用"固定 N 条请求跑完即止",否则你会测出有限批次的自愈假象。
- 记录首次低命中窗口的时间,并按删失处理。跑满 30 分钟没崩的那几次不是"没问题",是"在观察窗口内没发生",这两种在统计上不能混为一谈。
- 至少两次重复。论文的容量定义本身就要求"两次运行都不超过 1% 的 starvation",单次达标不算达标。
- 压测里也要记录完成量,不能只看吞吐。论文里吞吐掉 4 倍的同时完成量掉到 1.30 episodes/min,如果你的服务是"每单位时间完成多少个任务"计费,完成量才是收入。
一个可以照抄的准入伪代码
仅示意:episode 边界准入,不是可直接运行的实现
state: active = 0, cap K, backlog = 队列
on robot_episode_start(robot):
if active >= K:
backlog.push(robot); return WAIT
active += 1
robot.begin_episode()
on robot_episode_end(robot):
active -= 1
if backlog 非空:
r = backlog.pop()
active += 1; r.begin_episode()
每 30 秒(只观测,不改 K):
hit = full_prefix_hit_rate(window=120s) # 排除每条 episode 首轮
pre = computed_prompt_tokens_per_min()
gen = generated_output_tokens_per_min()
if hit < 0.80 and pre / max(gen, 1) > baseline_ratio * 1.5:
alert("进入工作集抖动区,建议下调 K")
if hit < 0.80 and kv_cache_usage_perc < 0.7:
alert("低占用 + 低命中 = 排队型抖动,不要加 K")
关键在最后两个分支:同样是低命中,占用率高和占用低对应两种完全不同的处置。论文的 AIMD 要求"占用高、命中低"同时成立,而抖动时占用率偏偏不高,所以它永远走不到"该降 K"那一支。
局限与失效场景
回放不是真集群
这是最大的一个限制,作者自己在第 9 节写明了。整个大脑层实验是回放:把原始轨迹的 token 长度、精确前缀关系、环境步间隔搬到一个本地 vLLM 上,用随机 token ID 保持长度和前缀结构。这隔离了缓存动态和计算代价,但也意味着:
- 没有真实的多模态消息处理。图片只是被计入了 token 数,没有真的走视觉编码器。
- 没有共享集群干扰。大脑和动作层是分开 profile 的,两层抢同一张卡的情况没测。
- 没有网络。回放的 RTT 设为 0,而真实托管调用 7.78 秒里有多少是网络、多少是排队,论文没有拆。
- token 长度是用 Qwen 重新分词估算的,Astra 与 Qwen 的输入 token 比中位约 1.03,也就是有 3% 的系统性偏差。
作者把"完整集成的 brain–VLA 部署、真实多模态处理、共享集群干扰"列为下一步验证。
一个模型、一套配置、一批任务
- 大脑代理只有一个:Qwen3.6-27B,BF16,TP=4,vLLM 0.17.1,混合注意力 784 token 块。换个模型(尤其是换块大小、换注意力结构)断崖位置就变了。
- 工作负载只有一个来源:一套 RPent 配置、四个 LIBERO-PRO suite、Astra 的低推理强度模式。推理强度调高,轮次和上下文都会变。
- 硬件只有一种:8 张 H800 80GB,NVLink,大脑用 4–7 号卡。
- 动作模型只有一个:π0.5,5 个去噪步,5 步一块。
所以论文里"20 个机器人是健康区"这句话不能当常数用。它是"这套模型 + 这套池子 + 这套任务长度"下的结论。
只测了服务进度,没测任务成功率
论文的"完成量"指标叫 completed replay-episode:一条 episode 的最后一轮大脑调用返回了,就算完成。它衡量的是服务推进速度,不是任务成功率。
作者明确写了:响应延迟被改变之后,闭环任务成功率会怎样,是未来工作。这一条在落地时很关键——你把并发压下去,单位时间完成的任务变多了,但每个任务看到的延迟变了,agent 的决策质量会不会变?论文没有答案。对机器人任务,这个缺口比对聊天机器人大得多:一个慢了三秒的决策可能导致完全不同的动作序列。
论文没有报告的东西
- P99 与尾延迟曲线。只有中位、P90、最大值。
- 显存绝对值。给了块数(4,270 / 4,396)和块大小(784),但没给 GB。
- 成本。没有美元、没有 GPU 小时。
- 方差。很多 cap 配置只有 1 次有效运行(表里 Runs 那一列),K=20 和 K=16 才有两次。N=24 六次试验里两次删失。样本量偏小是这篇的软肋。
- 加速器对比。只有 H800。
- 多租户。一个大脑服务多个舰队的情况没测。
恢复不等于安全
过载撤掉之后命中率会恢复(其中一次约 6 分钟),作者用这个来论证"抖动是延迟发生的、由持续负载维持的故障模式"。但也正因为会恢复,它容易被误判成"偶发波动"。
metastable failure 的经典教训是:触发条件撤掉之后系统仍停在低效态。这篇测到的更接近"触发撤掉就能恢复,但恢复有延迟"。对 SLA 来说这两种都不可接受——用户不会因为"六分钟后会好"就不投诉。
落地判断
值得试的三种情况
- 你在做长上下文 agent 的推理服务,且上下文是单调增长的(agent、代码助手、浏览器自动化、机器人)。这篇的两个诊断信号可以直接用。
- 你的 p99 上翘但 GPU 利用率不高,队列却在涨。先按"工作集型"排查,别急着加卡。
- 你的负载是"每个会话跑一条长任务,跑完再领下一条"。这种形态最适合 episode 级准入,因为边界天然存在。
先别碰的两种情况
- 你的请求之间没有前缀关系(每个请求都是独立 prompt、短上下文)。那 KV 抖动的整套机制不适用,
max_num_seqs和 continuous batching 就够。 - 你的 SLA 是吞吐而不是延迟(离线批处理、夜间任务)。论文自己也证明了有限批次会自然排空、命中率会自己回来,为了命中率主动限流只会拉长总时长。
三条结论
第一,容量要按两层分开算,而且两层的瓶颈会互相转移。截止时间宽松时,动作层能把 32 个机器人全放下,瓶颈转到大脑的 KV 工作集;截止时间紧时,动作层自己就是瓶颈,大脑反而空闲。所以"加卡"之前先确认瓶颈在哪一层,否则你优化的是一层,卡住的是另一层。
第二,"加并发"和"加机器人"是两件事。论文最漂亮的一步是把 N(舰队规模)和 K(活跃工作集)拆成两个独立变量。前者是业务需求,后者是运维旋钮。绝大多数团队只有一个旋钮(并发上限),所以一旦遇到工作集型抖动就只能靠加卡硬扛。
第三,准入的信号要选"前缀还活着吗",不是"显存用了多少"。AIMD 那 80 次零下降是全文最有说服力的负面结果:一个看起来合理的控制器,因为读错了一个指标,在抖动期间把上限一路加到 27。这不是控制器不努力,是信号不携带信息。
如果只做一件事
不做完整改造也能拿到大部分收益,优先级从高到低是这样:
- 把"完整前缀命中率"做成一条按两分钟窗口聚合的曲线,排到值班面板第一位。它比 GPU 利用率和显存占用都更能提前预警。
- 在网关或调度器上加一个活跃会话计数,超过阈值就在任务边界排队。不需要预测,不需要反馈控制,一个静态阈值就能把论文里的吞吐拉回 2.2 requests/s。
- 把压测时长从 5 分钟改成 30 分钟,并记录首次低命中时间。这一条几乎零成本,但能避免把容量定高三分之一。
- 最后再考虑按机器人状态做缓存感知调度。论文提了这个方向(动作块长度、剩余环境步数、进度标记都可以用来估计下次回访时间),但它明确说这是机会而不是已验证的结果。
前三条都是"观测加限流",一周内能上线;第四条要改调度器,属于半年期的工程。
术语速查与延伸阅读
术语表
| 术语 | 在论文里的意思 |
|---|---|
| 大脑(brain) | 高层 VLM,负责看场景、判断进度、选工具;本文用 GPT-6-Astra 采集轨迹、Qwen3.6-27B 做代价代理 |
| 动作层(VLA) | π0.5 这类视觉—语言—动作策略,把指令变成一小段电机动作 |
| action chunk | 一次 VLA 推理产出的一小段动作,本文是 5 步一块 |
| 全前缀命中(full-prefix hit) | 本轮请求的整段前缀都在缓存里,不需要重算;统计时排除每条 episode 第一轮 |
| KV 工作集 | 同时在跑的会话需要常驻的 KV 总量;超过缓存池就开始互相换出 |
| 抖动(thrashing) | 换出与重算互相喂养,命中率塌陷、prefill 暴涨、有效产出下降的状态 |
| starvation | 控制 tick 到来时动作缓冲为空,机器人没动作可执行 |
| 截止时间(chunk deadline) | 一个动作块必须送达的时间,D = 5/f,f 是控制频率 |
| 活跃 episode 上限 K | 准入参数,最多允许 K 条 episode 同时活跃,多余的排队 |
| 环境步间隔 | 两次大脑调用之间环境推进的步数除以 20 Hz;与墙钟间隔不同 |
| 断崖 | 完整前缀存活比例从 100% 直接掉到 0% 的临界会话数 |
| 右删失 | 观察窗口结束时事件还没发生,按删失处理而不是按"没发生"处理 |
延伸阅读
这篇论文站在几篇工作的肩膀上,想深挖可以按这个顺序读。前三条的链接取自论文自己的参考文献,后两条是它反复引用的基础设施与动作模型:
- CONCUR(ICML 2026):本文的准入思路、AIMD 对照、"有限批次"这个前提都来自它,会议页 上有正式版。注意它研究的是有限 agent 批次,而本文补的正是"批次永不排空"那一半。
- Continuum(2025):给多轮 agent 的 KV 缓存加 TTL,思路是跨工具间隔尽量保活。本文的 38.6% 零步间隔正好解释了为什么这种保活对 agent 有效。
- CacheWise(2026):刻画 coding agent 的前缀特征,并据此设计复用感知的 KV 策略。把它的 coding agent 曲线和本文的机器人 agent 曲线并排看,会发现"上下文只增不减 + 小增量复用大前缀"是同一类形状。
- Efficient Memory Management for Large Language Model Serving with PagedAttention:vLLM 的块管理与前缀缓存,理解本文的 784 token 块、4,270/4,396 块池子要先读它。
- π0.5: a Vision-Language-Action Model with Open-World Generalization:本文的动作模型,五个去噪步、五步一块的配置决定了动作层的延迟结构。
- RPent:采轨迹用的机器人 agent 框架,工具接口(渲染、几何、脚本化移动、VLA 技能)决定了大脑的调用密度。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)