Background
pypto-lib-side problem. models/deepseek_v4_pro/prefill_attention_csa.py,
prefill_attention_hca.py and prefill_attention_swa.py fail on device with:
[ERROR] validate_runtime_impl: [runtime_maker.cpp:945] simpler runtime failed:
orch_error_code=2 sched_error_code=0 runtime_status=-2
[ERROR] error detail: orch_error_code=2 HEAP_RING_DEADLOCK -
the task allocator could not reserve the heap bytes required for another task
These are single-chip (L2) kernels. Their per-task args and intermediates
exceed the runtime's compile-time default output heap
(CHIP_HEAP_SIZE, 256 MiB per ring —
simpler src/a5/runtime/tensormap_and_ringbuffer/runtime/runtime_types.h:72).
Measured on device: 512 MiB per ring is already enough for all three.
The runtime exposes exactly the knob needed —
CallConfig.runtime_env.ring_heap — and pypto surfaces it as
RunConfig.ring_heap. There is no way to reach it from an L2 dispatch
driven by execute_compiled.
Reproduction environment
| Component |
Version |
| pypto |
750131ca |
simpler (pypto runtime gitlink) |
dbdd041e |
pto-isa (runtime/pto_isa.pin) |
be5ccb76 |
ptoas (toolchain/versions.env) |
v0.60 |
| pypto-lib |
4e488fb |
| Platform / runtime |
a5 (Ascend 950), tensormap_and_ringbuffer |
Repro: DEEPSEEK_V4_VARIANT=pro python models/deepseek_v4_pro/prefill_attention_swa.py -p a5 -d <dev>
Summary
execute_compiled cannot carry per-task ring sizing
(ring_task_window / ring_heap / ring_dep_pool), so an L2 dispatch driven
by it always runs at the runtime's compile-time defaults. Please make
execute_compiled able to pass a RunConfig (or at minimum the ring fields)
through to CallConfig.runtime_env.
Motivation / Use Case
pypto has two parallel L2 dispatch surfaces, and ring sizing exists on only
one of them.
Surface A — the work_dir surface (execute_compiled). No ring parameter:
def execute_compiled(work_dir, args, *, platform, device_id, dfx=_DfxOpts(),
level=2, aicpu_thread_num=None,
analyze_auto_scopes_for_deps=False) -> None
It calls compile_and_assemble(work_dir, platform) and then
execute_on_device(...), which builds a fresh CallConfig() and sets exactly
aicpu_thread_num, the five DFX flags and output_prefix — and nothing else:
cfg = CallConfig()
if aicpu_thread_num is not None:
cfg.aicpu_thread_num = aicpu_thread_num
cfg.enable_chip_swimlane = enable_chip_swimlane
cfg.enable_dump_args = enable_dump_args
cfg.enable_pmu = enable_pmu
cfg.enable_dep_gen = enable_dep_gen
cfg.enable_scope_stats = enable_scope_stats
if output_prefix:
cfg.output_prefix = output_prefix
cfg.runtime_env occurs zero times in python/pypto/runtime/device_runner.py,
and ring_heap / ring_task_window / ring_dep_pool occur zero times in both
device_runner.py and ir/compiled_program.py (checked at 750131ca).
Surface B — the RunConfig / CompiledProgram surface. ring_heap is a
RunConfig field and reaches the device through a single application point:
RunConfig(ring_heap=...)
-> compiled.build_call_config(rc)
-> _build_call_config(...) # runtime/runner.py:979
-> _apply_ring_overrides(cfg, rc) # called at runner.py:1019,
-> cfg.runtime_env.ring_heap = ... # defined at runner.py:957
Only three entry points reach it: ChipWorker.run(compiled, *args, config=rc),
benchmark(compiled, args, config=rc), and the L3 path
(distributed_runner._make_call_config).
Note compiled(*args, config=rc) is not one of them. _invoke_compiled
(ir/compiled_program.py:658) already holds a RunConfig and hands it to
execute_compiled — but only four fields survive the call:
execute_compiled(
output_dir,
coerced,
platform=execution_platform,
device_id=config.device_id,
dfx=_DfxOpts.from_run_config(config),
aicpu_thread_num=config.aicpu_thread_num,
)
The ring fields are dropped here silently. This is also why the fix below is
natural: the RunConfig is already in hand at the call site, it just has
nowhere to go.
Why this only became a problem recently. Ring sizing used to be the
PTO2_RING_* environment variables, which are process-global and therefore
worked on both surfaces. When simpler retired them and sizing moved onto
CallConfig.runtime_env, only Surface B was rewired. Surface A was left
behind, and with the env route gone an L2 execute_compiled dispatch now has
no way to size its rings.
Why this matters to pypto-lib specifically. run_jit(runtime_cfg=...) is
deliberately designed as a pass-through to execute_compiled's keyword
arguments — no lib-owned field vocabulary, so pypto-lib does not have to
re-adapt every time the runtime surface changes. A knob that exists in
RunConfig but not on execute_compiled breaks that contract: the lib either
carries a custom field with custom routing, or the kernel simply cannot ask for
the knob. We would like to keep runtime_cfg a pure pass-through.
Measured today at the last Python frame before the mailbox
(simpler.worker.Worker.run), an L2 dispatch through execute_compiled
receives ring_heap = [0, 0, 0, 0], i.e. "fall through to the compile-time
default", with no way for the caller to change it.
Proposed API / Behavior
Preferred — let execute_compiled take a RunConfig:
def execute_compiled(work_dir, args, *, platform, device_id, dfx=_DfxOpts(),
level=2, aicpu_thread_num=None,
analyze_auto_scopes_for_deps=False,
config: RunConfig | None = None) -> None
forwarded to execute_on_device, which applies it with the existing
_apply_ring_overrides — the same code path Surface B already uses, so both
surfaces stay in step by construction.
This is preferred over adding three individual keywords because it makes the
two surfaces converge permanently: any future RunConfig knob then reaches
L2 with no pypto-lib change at all, which is exactly the property
runtime_cfg was designed to have.
Minimum acceptable — three explicit keywords, forwarded the same way:
execute_compiled(..., ring_task_window=None, ring_heap=None, ring_dep_pool=None)
Secondary, same area. execute_on_device already has a parameter literally
named runtime_env:
def execute_on_device(..., runtime_env: dict[str, str] | None = None) -> None
but it is OS environment variables applied through _temporary_env(), not
CallConfig.runtime_env. The collision reads exactly like the surface being
asked for here, and since the PTO2_RING_* knobs were retired, setting it has
no effect on ring sizing at all. Renaming it (e.g. os_env / env_overrides)
would remove a real trap.
Alternatives Considered
- Environment variables — gone.
PTO2_RING_TASK_WINDOW / PTO2_RING_HEAP
/ PTO2_RING_DEP_POOL were retired in simpler.
compiled(*args, config=RunConfig(ring_heap=...)) — silently ineffective;
_invoke_compiled drops the ring fields (ir/compiled_program.py:658).
- Route pypto-lib's L2 dispatch through
ChipWorker instead — works, and is
what pypto-lib PR
#1089 currently does
as a stopgap. We consider it the wrong layer: it forces pypto-lib to own a
ring-key vocabulary and a second dispatch route, which is precisely what
runtime_cfg-as-pass-through was meant to avoid. It also loses
execute_compiled's dep-capture subprocess, so that route has to refuse
swimlane / dep_gen runs. We intend to revert it once execute_compiled
carries the knob.
- Raising the compile-time default — a global change that costs every
program device memory for the benefit of a few.
Additional Context
Evidence that this is ordinary under-provisioning and nothing structural —
injected at simpler.worker.Worker.run, all outputs validated:
| Case |
ring_heap (all four rings) |
Result |
prefill_attention_swa |
default ([0,0,0,0] -> 256 MiB) |
HEAP_RING_DEADLOCK |
prefill_attention_swa |
512 MiB |
PASS |
prefill_attention_swa |
1 GiB |
PASS |
prefill_attention_csa |
512 MiB / 1 GiB |
PASS |
prefill_attention_hca |
512 MiB / 1 GiB |
PASS |
One doubling of the default is enough. A stale comment in our tree claimed
4 GiB on all four rings still deadlocked; that measurement went through a route
which dropped the value, and does not reproduce.
Related: the ring sizing must also cover the benchmark dispatch, which is a
second, independent launch — sizing only the correctness run moves the deadlock
rather than removing it. benchmark() already accepts config=, so this is
only a note for whoever wires execute_compiled.
Background
pypto-lib-side problem.
models/deepseek_v4_pro/prefill_attention_csa.py,prefill_attention_hca.pyandprefill_attention_swa.pyfail on device with:These are single-chip (L2) kernels. Their per-task args and intermediates
exceed the runtime's compile-time default output heap
(
CHIP_HEAP_SIZE, 256 MiB per ring —simpler
src/a5/runtime/tensormap_and_ringbuffer/runtime/runtime_types.h:72).Measured on device: 512 MiB per ring is already enough for all three.
The runtime exposes exactly the knob needed —
CallConfig.runtime_env.ring_heap— and pypto surfaces it asRunConfig.ring_heap. There is no way to reach it from an L2 dispatchdriven by
execute_compiled.Reproduction environment
750131caruntimegitlink)dbdd041eruntime/pto_isa.pin)be5ccb76toolchain/versions.env)v0.604e488fba5(Ascend 950),tensormap_and_ringbufferRepro:
DEEPSEEK_V4_VARIANT=pro python models/deepseek_v4_pro/prefill_attention_swa.py -p a5 -d <dev>Summary
execute_compiledcannot carry per-task ring sizing(
ring_task_window/ring_heap/ring_dep_pool), so an L2 dispatch drivenby it always runs at the runtime's compile-time defaults. Please make
execute_compiledable to pass aRunConfig(or at minimum the ring fields)through to
CallConfig.runtime_env.Motivation / Use Case
pypto has two parallel L2 dispatch surfaces, and ring sizing exists on only
one of them.
Surface A — the
work_dirsurface (execute_compiled). No ring parameter:It calls
compile_and_assemble(work_dir, platform)and thenexecute_on_device(...), which builds a freshCallConfig()and sets exactlyaicpu_thread_num, the five DFX flags andoutput_prefix— and nothing else:cfg.runtime_envoccurs zero times inpython/pypto/runtime/device_runner.py,and
ring_heap/ring_task_window/ring_dep_pooloccur zero times in bothdevice_runner.pyandir/compiled_program.py(checked at750131ca).Surface B — the
RunConfig/CompiledProgramsurface.ring_heapis aRunConfigfield and reaches the device through a single application point:Only three entry points reach it:
ChipWorker.run(compiled, *args, config=rc),benchmark(compiled, args, config=rc), and the L3 path(
distributed_runner._make_call_config).Note
compiled(*args, config=rc)is not one of them._invoke_compiled(
ir/compiled_program.py:658) already holds aRunConfigand hands it toexecute_compiled— but only four fields survive the call:The ring fields are dropped here silently. This is also why the fix below is
natural: the
RunConfigis already in hand at the call site, it just hasnowhere to go.
Why this only became a problem recently. Ring sizing used to be the
PTO2_RING_*environment variables, which are process-global and thereforeworked on both surfaces. When simpler retired them and sizing moved onto
CallConfig.runtime_env, only Surface B was rewired. Surface A was leftbehind, and with the env route gone an L2
execute_compileddispatch now hasno way to size its rings.
Why this matters to pypto-lib specifically.
run_jit(runtime_cfg=...)isdeliberately designed as a pass-through to
execute_compiled's keywordarguments — no lib-owned field vocabulary, so pypto-lib does not have to
re-adapt every time the runtime surface changes. A knob that exists in
RunConfigbut not onexecute_compiledbreaks that contract: the lib eithercarries a custom field with custom routing, or the kernel simply cannot ask for
the knob. We would like to keep
runtime_cfga pure pass-through.Measured today at the last Python frame before the mailbox
(
simpler.worker.Worker.run), an L2 dispatch throughexecute_compiledreceives
ring_heap = [0, 0, 0, 0], i.e. "fall through to the compile-timedefault", with no way for the caller to change it.
Proposed API / Behavior
Preferred — let
execute_compiledtake aRunConfig:forwarded to
execute_on_device, which applies it with the existing_apply_ring_overrides— the same code path Surface B already uses, so bothsurfaces stay in step by construction.
This is preferred over adding three individual keywords because it makes the
two surfaces converge permanently: any future
RunConfigknob then reachesL2 with no pypto-lib change at all, which is exactly the property
runtime_cfgwas designed to have.Minimum acceptable — three explicit keywords, forwarded the same way:
Secondary, same area.
execute_on_devicealready has a parameter literallynamed
runtime_env:but it is OS environment variables applied through
_temporary_env(), notCallConfig.runtime_env. The collision reads exactly like the surface beingasked for here, and since the
PTO2_RING_*knobs were retired, setting it hasno effect on ring sizing at all. Renaming it (e.g.
os_env/env_overrides)would remove a real trap.
Alternatives Considered
PTO2_RING_TASK_WINDOW/PTO2_RING_HEAP/
PTO2_RING_DEP_POOLwere retired in simpler.compiled(*args, config=RunConfig(ring_heap=...))— silently ineffective;_invoke_compileddrops the ring fields (ir/compiled_program.py:658).ChipWorkerinstead — works, and iswhat pypto-lib PR
#1089 currently does
as a stopgap. We consider it the wrong layer: it forces pypto-lib to own a
ring-key vocabulary and a second dispatch route, which is precisely what
runtime_cfg-as-pass-through was meant to avoid. It also losesexecute_compiled's dep-capture subprocess, so that route has to refuseswimlane / dep_gen runs. We intend to revert it once
execute_compiledcarries the knob.
program device memory for the benefit of a few.
Additional Context
Evidence that this is ordinary under-provisioning and nothing structural —
injected at
simpler.worker.Worker.run, all outputs validated:ring_heap(all four rings)prefill_attention_swa[0,0,0,0]-> 256 MiB)prefill_attention_swaprefill_attention_swaprefill_attention_csaprefill_attention_hcaOne doubling of the default is enough. A stale comment in our tree claimed
4 GiB on all four rings still deadlocked; that measurement went through a route
which dropped the value, and does not reproduce.
Related: the ring sizing must also cover the benchmark dispatch, which is a
second, independent launch — sizing only the correctness run moves the deadlock
rather than removing it.
benchmark()already acceptsconfig=, so this isonly a note for whoever wires
execute_compiled.