Skip to main content
一次运行里同时跑几条 Attempt,由两级并发限制共同决定:全局并发位管这台机器和 Provider 撑得住多少,实验自己的 maxConcurrency 管这个实验最多敢并行几条。两道都过才真正开跑。 嫌慢的时候,并发只是其中一个旋钮。先判断时间花在哪:

两级并发限制各管什么

全局值的解析顺序是 --max-concurrency → 配置里的 maxConcurrency → 当前 Sandbox Provider 的推荐默认值(docker 10、e2b 20、vercel 1、local 1)。推荐值反映的是 Provider 侧的约束。你自己 Agent 接口的限速用 --max-concurrency 压。

名额怎么分

下面是一批混跑:全局 3 个并发位,fast 是普通实验,slow 声明了 maxConcurrency: 1 三件事从这张图里读出来:
  • slow 永远只占一条道。 它的第二条 Attempt 要等第一条的 teardown 和 Sandbox 销毁完成才开始。同批其它实验不受它拖累,照常用剩下的位。
  • 名额优先给要跑最多轮才能跑完的实验。 快慢实验混在一次命令里是安全的,不需要手工分波、也不需要为快任务预留名额。快的见缝插针补空位。
  • 退避中的 Attempt 让出全局位,但不让出实验自己的位。 所以撞限流时 live 面板的 running 会超过上限,超出的行数恰好等于正在退避的行数——任一瞬间真正在执行的仍然不超过上限。这不是并发失效。
等待实验级 setup(起隧道、起共享服务)的 Attempt 既不持有也不预留并发位,计数里保持 queued。启动慢的隧道不会让「0 running、N queued 长时间不动」变成一个并发配置问题。

在 live 面板确认并发落到位

PLAN 面板先告诉你本次开几路、这个数来自哪里。声明了 maxConcurrency 的实验跟在后面,各自列出自己的上限:
(from flag) 表示 19 来自 --max-concurrency。没传 flag、配置里也没写时会是 (from vercel default) 这类 Provider 推荐值——只开 1 路不是调度坏了,vercel 的推荐值就是 1。slow ≤1 表示这个实验被自己的并发上限限制,把 --max-concurrency 调大也不会让它开更多路。 运行中看首行计数:
running 稳定顶在上限、queued 逐步消化,说明并发位就是瓶颈,还有余量就往上调。running 长期不满则瓶颈在别处:实验自己的并发上限(PLAN 行已列出)、Provider 的独占串行,或者在等实验级 setup

跨 Attempt 共享状态时串行

多条评估用例载入、修改并回存同一份宿主机文件或中心服务状态时,把这个实验降到一条道:
maxConcurrency: 1 只串行本 Invocation 的 Attempt。sharedState.key 才跨终端保护同一 checkpoint。 租约在 Experiment 和 Sandbox 的 setup 之前取得,到 Sandbox teardown、Provider finalizer 与 Experiment teardown 完成后才释放。等待方不创建 Sandbox。租约释放后,等待方继续自己的既定计划;它不读取或沿用另一 Record 的结果。 sharedState 只做互斥,不替你存 checkpoint,也不能回滚强杀前的半次写入。做不到原子回存时,换新 key 和干净 cohort 从头重建。 只有一个 Agent 服务频繁限流时,同样在这个实验上设 maxConcurrency: N,不要去降全局上限——降全局会连累同批其它实验。实验级并发上限退避时不放行第 N+1 条,所以服务不会在已经限流时继续被加压。

Hook 里的状态按 Sandbox 存

并发时同一个模块同时服务多个 Sandbox,setup 拿到的句柄不能放进普通模块变量——会被后一个并发 Attempt 覆写。以 Sandbox 实例为键存取:
如果这些 Attempt 本来就必须按顺序读写同一份业务状态,不要用 WeakMap 掩盖语义,直接把实验降到 maxConcurrency: 1

让相关评估按组复用 Sandbox

只有几组评估需要共享环境时,用 defineEvalGroup() 显式列出兼容成员。每个组同时只派发一条 Attempt,并复用至多一台 Sandbox;不同组和未分组评估用例继续竞争剩余并发位。你不需要把 整个 Experiment 设成 maxConcurrency: 1 evals 数组只声明成员,不声明业务顺序。Runner 按规范化 Eval ID 稳定串行; attempts > 1 时,同一成员需要真实派发的各次 Attempt 连续进入组内泳道。完整目录、 Experiment 写法和 --dry 输出见用评估组复用 Sandbox 评估组不是任务依赖图。结果沿用和 CLI 过滤可能让前一项不执行,题间重置也会撤销前一项对 workdir 的修改。后一步必须读取前一步文件时,把它们写进同一条评估用例;不要把正确性押在 组内派发顺序上。

只要读起来有序就不必串行

结果始终按发现顺序输出,与这次开了多大并发无关。终端里的行序和报告里的行序因此是稳定的、可以直接 diff。只希望输出有序的话到这里就够了,命名前缀照样生效,吞吐不用付代价。

让首过即停真的省钱

earlyExit 停的是「还没派发的 Attempt」。默认并发下同一条评估用例的多次 Attempt 可能已经同时派发出去,第一次通过时后面几条已经在跑,省不下来。要「跑一次,过了就停、没过才跑下一次」这种一个接一个的效果:

多开终端时隔离 Record

同一 Record root 不支持两条 Invocation 自动分工。确认本机、Provider 和 Agent 服务还有余量时,为第二个终端指定不同 root:
两边各自规划完整选择,可能重复执行相同评估用例,也不会读取或携入对方运行中的 Attempt。每个 root 独立产生 Run、Sample 和 Report;NiceEval 不自动合并它们。 三个边界:
  • 两边的 CLI 上限会相加,Provider 容量不会。配额紧张时把两边都调低。
  • 实验自己的 maxConcurrency 也是每个终端各算各的:两条命令都声明 3 时,合计最多跑六条。
  • 两个终端的 Sandbox 池也各自独立。只有确实共享外部 checkpoint 时才声明 sharedState.key;这不会合并 Record。
  • 指向同一 root 的第二条命令立即失败,不等待、不接管,也不读取第一条命令的中间数据。

边界

  • 并发上限管的是资源占用,不是花钱。封顶花费用 --budget
  • local 这类声明独占串行的 Provider 有一道 Provider 级串行限制,--max-concurrency 不解除它——那是正确性约束,不是调度参数。
  • 按「同时在跑的 run 数」计费或限额的 Agent 服务,贴着限额值配全局上限压不稳:退避让出的位立刻被新 Attempt 顶上,服务侧并发恒在上限。单个终端可以用实验级 maxConcurrency 留余量。多开时各边的值会相加,账户级配额仍要分别调低或交给外部编排。
  • 声明 sandboxReuse: true 的实验,每个 Sandbox 内部串行、Sandbox 之间并行,见复用 Sandbox
  • 评估组内部稳定串行,不同组仍可并行,见评估组

接着看

重跑与沿用

哪些结果这次不用再花钱。

复用 Sandbox

公共准备只付一次的另一条提速路径。

评估组

同组稳定串行复用 Sandbox,不同组继续并行。

写实验

maxConcurrency 和实验级生命周期写在哪。

运行器

发现、派发、重试与预算的完整机制。