单任务 Ring 尺寸配置(Per-Task Ring Sizing)¶
PyPTO 通过 RunConfig 上的三个可选
覆盖项暴露 Simpler 的单任务 ring 尺寸配置。它们让你在单次派发中为运行时的
单任务 ring 资源设置尺寸,而无需改动已编译产物或任何全局状态。该能力同时适用于
L2 单 chip 路径(run() / ChipWorker.run())和 L3 分布式路径
(DistributedWorker.run() / 一次性的 compiled(...))。
运行时把任务启动相关资源保存在 ring buffer(环形缓冲)中。每个覆盖项与
Simpler 的 CallConfig.runtime_env 上的同名字段一一对应,并且按 每次任务提交
生效 —— 即每次 run() / rt.run() 调用,因此同一 kernel 的不同提交可以使用不同
的 ring 尺寸。在 L3 路径上,覆盖项按每次派发生效,叠加在程序的 DistributedConfig
基线(aicpu_thread_num)之上,并作用于该次派发的所有 chip。
字段对照表¶
RunConfig 字段 |
CallConfig.runtime_env 成员 |
作用 | 约束 |
|---|---|---|---|
ring_task_window: int \| None |
ring_task_window |
task ring 中在途 task slot 的数量 | 2 的幂,>= 4 |
ring_heap: int \| None |
ring_heap |
每个 ring 的 task 输出堆字节数 | 2 的幂,>= 1024 |
ring_dep_pool: int \| None |
ring_dep_pool |
依赖边池容量 | [4, INT32_MAX] |
None(默认值)表示该字段 未设置(在 CallConfig 上为 0)。PyPTO 仅在值
不为 None 时才写入 CallConfig.runtime_env,因此未设置的字段完全交由运行时
决定。
优先级¶
对每个值,运行时按如下顺序解析出生效尺寸:
单任务 CallConfig.runtime_env 值 (RunConfig 覆盖 —— 最高优先级)
└─ 回退到 → PTO2_RING_* 环境变量 (进程级)
└─ 回退到 → 编译期默认值 (最低优先级)
因此 RunConfig 覆盖优先于 PTO2_RING_TASK_WINDOW / PTO2_RING_HEAP /
PTO2_RING_DEP_POOL 环境变量,而后者又优先于运行时内置的默认值。
校验¶
RunConfig 在构造时(__post_init__ 中)校验这些覆盖项,与运行时的
RuntimeEnv::validate() 保持一致。这样可以在调用处直接抛出清晰的 ValueError,
而不是在派发时深陷运行时自身的 CallConfig::validate() 失败:
RunConfig(platform="a2a3", ring_heap=1000)
# ValueError: ring_heap must be a power of 2 >= 1024 (bytes per ring), got 1000
非整数(包括 bool)会以相同的 ValueError 被拒绝,而非从 2 的幂判断中抛出
难以理解的 TypeError。
用法¶
L2 单 chip(run)¶
from pypto.runtime import run, RunConfig
compiled = run(
MyProgram,
a, b, c,
config=RunConfig(
platform="a2a3",
ring_task_window=128, # 128 个在途 task slot
ring_heap=8 * 1024 * 1024, # 每个 ring 8 MiB 输出堆
ring_dep_pool=256, # 256 个依赖边条目
),
)
L3 分布式(DistributedWorker / compiled(...))¶
把同样的 RunConfig 传给每次派发调用。已准备好的 worker 的共享配置不会被改动,
因此连续多次派发 —— 以及多程序 serving 场景下的不同程序(如 prefill 与 decode)——
都可以各自使用不同的 ring 尺寸:
from pypto.runtime import RunConfig
prefill_cfg = RunConfig(platform="a2a3", ring_task_window=256)
decode_cfg = RunConfig(platform="a2a3", ring_task_window=64)
with compiled.prepare(prefill_cfg) as rt: # 一次性 setup + 预热
rt(host_x, weight, host_out, config=prefill_cfg) # prefill 用较大的 window
rt(host_x, weight, host_out, config=decode_cfg) # decode 用较小的 window
# 一次性路径同样支持该覆盖:
compiled(a, b, c, config=RunConfig(platform="a2a3", ring_heap=8 * 1024 * 1024))
在 L3 派发路径上,仅消费 RunConfig 的 ring_* 字段;编译期字段与 DFX 字段会被
忽略(它们在编译 / prepare 时设置,而非按派发设置)。
只设置你想覆盖的字段即可;其余字段保持未设置,并按上述优先级回退。
Arena 预热与单槽缓存¶
某个 ring 尺寸对应的运行时 arena,首次使用时需要约 800ms 构建。worker 现在会在
init 时主动构建它 —— prepare(config) / ChipWorker / execute_on_device ——
使这次冷构建落在 setup 阶段,而不是落在第一次(通常被计时的)派发里。
arena 缓存以完整的 per-ring 尺寸向量为 key(4 个 ring 的 ring_task_window /
ring_heap / ring_dep_pool 一起 hash)。所以"一次派发内各 ring 尺寸不同"——即
list 形式,如 ring_task_window=[256, 128, 64, 0]——是一个 key、一块 arena:
只要 prepare(...) 传的是同一个 RunConfig,预热就完整覆盖它。下面的单槽限制只针对
不同派发之间使用不同尺寸向量,与"一次派发内各 ring 不同"无关。
缓存每个 worker 只保留一个尺寸向量,且每次构建 arena 都会覆盖那个槽,因此:
- 把派发用的
RunConfig传给prepare(...)。它只用于预热、不会被保存,所以每次 派发仍需各自传config=。如上例,prefill_cfg要同时传给prepare(...)和首次 prefill 派发;decode 派发使用decode_cfg。 - 预热只能省掉首个派发的冷构建,且仅当首个派发的尺寸与预热尺寸一致时才生效——所以 请预热你首个派发使用的尺寸,而非最频繁的那个。任何首个派发用了别的尺寸都会 miss、 重建、覆盖掉预热槽,预热白费。
- 同一个 worker 的多次派发之间尽量保持 ring 尺寸不变。单槽是每个 worker 各一个、每次切换
都被覆盖,所以交替尺寸时每次切换都会重建 arena(与预热无关;上例
decode_cfg那次付 一次构建,紧接着的prefill_cfg又付一次)。把不同尺寸拆到不同 worker 通常行不通——之所以 要常驻一个 worker(prepare(..., extra_compiled=[...])),正是为了共享 KV cache 这类 设备驻留状态,而分开的 worker 无法共享。所以应挑一个能服务该 worker 所有派发形状的尺寸,而 不是逐次派发改尺寸。 - L2 派发的 ring 尺寸来自每次调用的
RunConfig,因此ChipWorker预热的是运行时的默认 尺寸;若某次调用的RunConfig指定了不同的 ring 尺寸,则会重建一次。
相关文档¶
- Simpler 运行时侧实现:
runtime/src/<arch>/runtime/tensormap_and_ringbuffer/下的tensormap_and_ringbuffer运行时,以及runtime/src/common/task_interface/call_config.h中的RuntimeEnv/CallConfig定义。 - Worker API 示例:
runtime/examples/workers/{l2,l3}/per_task_runtime_env/。 - 接受单次派发
RunConfig的 L3 派发入口:distributed_runner.py中的DistributedWorker.run/__call__以及execute_distributed。 - 同一
RunConfig上正交的运行时诊断特性: 03-runtime-dfx.md。