Skip to content

Update: make whole-run FIFO admission failure-safe - #1541

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-b2-whole-run-fifo
Aug 2, 2026
Merged

Update: make whole-run FIFO admission failure-safe#1541
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-b2-whole-run-fifo

Conversation

@Crane-Liu

@Crane-Liu Crane-Liu commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Enforce bounded whole-run FIFO ordering across scheduling, execution, and teardown.
  • Make run, remote-buffer, CommDomain, and shared-memory ownership transactional under interruption, with exact-once cleanup publication.
  • Expand lifecycle documentation plus native, Python fault-injection, and onboard coverage.

Validation

  • Python unit suite: 999 passed, 9 skipped.
  • Native CTest suite: 70/70 passed.
  • Full pre-commit suite passed (clang-format, clang-tidy, cpplint, Ruff, Pyright, Markdown).
  • A2/A3 onboard whole-run FIFO and CommDomain scenario passed: task_20260801_014801_366612126220.
  • GitHub CI: 17 checks passed; 1 deployment check skipped as expected.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change introduces generation-safe pipeline leases, run-scoped FIFO admission, task-acceptance signaling, lease-aware mailbox dispatch, and AICore image-aware stream reuse across C++, Python bindings, device runners, documentation, and tests.

Changes

Pipeline admission and acceptance

Layer / File(s) Summary
Lease, contract, and callable identity contracts
src/common/worker/*, src/common/hierarchical/types.*, src/common/task_interface/*, src/common/platform/onboard/host/*
Pipeline leases, run-scoped queues, contract validation, callable AICore image hashes, arena selection, and runtime C-API entry points are added.
Run admission and scheduling
src/common/hierarchical/orchestrator.*, src/common/hierarchical/scheduler.*
Runs acquire generation-safe leases, track pending acceptances, activate through FIFO ordering, cancel unstarted work on failure, and dispatch only tasks belonging to the active run.
Mailbox and Python integration
src/common/hierarchical/worker_manager.*, python/bindings/*, python/simpler/*, docs/*
Mailbox payloads carry leases, TASK_ACCEPTED is observed before completion, acceptance callbacks update orchestration, and Python wrappers expose acceptance and lease-aware execution.
Device execution and stream reuse
src/common/worker/*, src/a2a3/*, src/common/platform/onboard/host/device_runner_base.*
Device execution selects leased slots and arena banks, validates generations, publishes acceptance after kernel enqueue, and recreates AICore streams when image hashes change.
Validation
tests/st/*, tests/ut/*
Tests cover lease generations, pipeline contracts, acceptance ordering, FIFO admission, stream reuse, callable identity, and concurrent waiters.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit hops through slots of two,
With leases fresh and generations new.
“Accepted!” the mailbox sings,
While streams remember AICore wings.
FIFO runs now safely flow—
Carrots queued in order, ho ho!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: failure-safe whole-run FIFO admission.
Description check ✅ Passed The description directly covers bounded FIFO admission, failure-safe cleanup, lifecycle changes, and validation for the changeset.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/common/hierarchical/orchestrator.cpp (1)

177-206: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid racing fail_run_submission against scheduler activation. The run phase is sampled without the completion/run lock, and cancel_unstarted_run is later allowed to mark slots PENDING/READY/FREE as FAILED; if the closed run has already been activated to EXECUTING, the scheduler can dispatch the same slot as RUNNING concurrently, losing one terminal transition. Guard cancellation by the current run phase and use stateful atomic transitions for slot states.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/hierarchical/orchestrator.cpp` around lines 177 - 206, Update
fail_run_submission and cancel_unstarted_run to synchronize phase inspection
with the run/completion state, preventing cancellation from racing scheduler
activation. Recheck the current phase while holding the appropriate lock before
cancelling, and make each slot-state update use compare-and-exchange transitions
that only convert valid non-running states; never mark an EXECUTING/RUNNING slot
FAILED or overwrite a concurrent scheduler transition.
🧹 Nitpick comments (5)
src/common/platform/onboard/host/device_runner_base.h (1)

297-320: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc block missing the new aicore_image_hash param. The @param list enumerates every other argument; add a line for the new one so the staging contract stays self-describing (same for record_host_orch_callable at Line 332).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/platform/onboard/host/device_runner_base.h` around lines 297 -
320, Update the documentation for record_device_orch_callable to add an `@param`
entry describing aicore_image_hash, matching its position and purpose in the
function signature. Also add the corresponding parameter documentation to
record_host_orch_callable, without changing implementation behavior.
src/common/hierarchical/scheduler.cpp (1)

366-369: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: fold the active_run_cb ? scoped : unscoped branching into helpers. The same "resolve active run, bail on invalid, pick scoped vs unscoped queue op" shape is now repeated in all four dispatch paths plus the wait predicate; a pair of small private helpers (e.g. resolve_active_run() returning std::optional<RunId> and pop_ready(...)) would keep future queue-API changes in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/hierarchical/scheduler.cpp` around lines 366 - 369, Consolidate
the repeated active-run resolution and scoped/unscoped ready-queue branching
across all four dispatch paths and the wait predicate in the scheduler class.
Add small private helpers such as resolve_active_run() and pop_ready(...) that
preserve invalid-run early returns while centralizing queue API selection, then
update the affected paths to use them.
python/simpler/task_interface.py (1)

1277-1284: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add one-line docstrings to match the neighbouring properties.

Every other property in this class documents its unit/semantics; pipeline_depth and runtime_slot_count are the odd ones out.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/simpler/task_interface.py` around lines 1277 - 1284, Add concise
one-line docstrings to the pipeline_depth and runtime_slot_count properties in
the task interface, matching the neighboring properties’ style and documenting
each value’s unit or semantics.
tests/ut/cpp/hierarchical/test_orchestrator.cpp (1)

656-665: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

A failed ASSERT_* between here and third.get() turns the test into a hang.

ASSERT_TRUE returns from the test body, and ~future from std::async blocks until the task completes — but begin_run() only returns once a slot frees, which is exactly what the skipped lines would have done. Consider releasing the admission slot from a scope guard so an assertion failure fails fast instead of wedging the suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/cpp/hierarchical/test_orchestrator.cpp` around lines 656 - 665,
Ensure the asynchronous third begin_run test always releases an admission slot
before any ASSERT_* can exit the test, by adding a scope guard around the first
run’s slot ownership and consumption cleanup. Preserve the existing
successful-path behavior while making cleanup execute during assertion-induced
early returns, so third.get() cannot leave the test hanging.
src/common/platform/onboard/host/device_runner_base.cpp (1)

143-150: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Bank validation accepts < PTO_PIPELINE_MAX_DEPTH but only two banks exist.

select_arena_bank admits any id below PTO_PIPELINE_MAX_DEPTH, while every consumer is a binary bank == 0 ? bank0 : bank1. With today's depth of 2 this is benign, but raising the depth constant would silently alias banks 2..N onto bank1 and share arenas across concurrently admitted runs. Prefer indexing an array of banks, or validate against the actual bank count.

♻️ Sketch
-    if (bank_id >= PTO_PIPELINE_MAX_DEPTH) {
-        LOG_ERROR("arena bank %u is outside [0, %u)", bank_id, PTO_PIPELINE_MAX_DEPTH);
+    if (bank_id >= kArenaBankCount) {
+        LOG_ERROR("arena bank %u is outside [0, %u)", bank_id, kArenaBankCount);
         return -1;
     }

with gm_heap_arena_[kArenaBankCount] etc. so the selection is gm_heap_arena_[g_arena_bank].

Also applies to: 193-211

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/platform/onboard/host/device_runner_base.cpp` around lines 143 -
150, Update select_arena_bank and the associated arena consumers to validate
against the actual number of supported banks rather than PTO_PIPELINE_MAX_DEPTH.
Define or reuse a two-bank count, reject IDs outside that range, and replace
binary bank-selection logic with indexed access so each accepted bank maps to
its own arena without aliasing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/orchestrator.md`:
- Around line 101-106: The documentation around the “later submit” behavior must
distinguish admission from acceptance: clarify that acceptance unblocks only an
already-admitted successor, while a third submission with bounded depth two
remains blocked in begin_run() until a terminal run releases its lease. Preserve
the existing endpoint acceptance and completion fallback details.

In `@docs/worker-manager.md`:
- Around line 175-181: Update the documentation around the mailbox polling loop
to state that on_accept() is a one-shot callback, invoked only on the first
observation of TASK_ACCEPTED. Document the acceptance_observed guard and
preserve TASK_DONE as the mailbox ownership boundary, so implementations do not
repeatedly decrement per-run acceptance accounting.

In `@python/simpler/worker.py`:
- Line 1544: In the comment near the worker implementation, replace the
ambiguous multiplication symbol “×” in “N×40B” with the ASCII character “x”,
preserving the rest of the comment unchanged.
- Around line 180-186: Update _PIPELINE_LEASE_FMT to use the C ABI’s packed
12-byte layout for PipelineSlotLease: uint32 slot_id, uint32 reserved, and
uint64 generation with no padding between fields. Ensure _OFF_ARGS resolves to
the established MAILBOX_OFF_ARGS offset and remains compatible with the C++
definition in pto_runtime_c_api.h.

In `@src/a2a3/platform/onboard/host/device_runner.cpp`:
- Around line 621-624: Make acceptance durable across the mailbox transition in
the dispatch flow around publish_task_accepted and
LocalMailboxEndpoint::run_with_accept: ensure a parent that observes TASK_DONE
without previously sampling TASK_ACCEPTED still invokes on_accept exactly once.
Use a latched acceptance flag/counter or equivalent state, and preserve the
existing behavior for parents that observe TASK_ACCEPTED first so
decrement_run_accepts is not duplicated.

In `@src/common/hierarchical/orchestrator.cpp`:
- Around line 360-371: Update Orchestrator::decrement_run_accepts to acquire
run->completion_mu before changing pending_accepts and before calling
completion_cv.notify_all(). Keep the decrement, underflow handling, error
recording, and notification behavior intact while ensuring the predicate
transition is synchronized with wait_run_accepted.

In `@src/common/hierarchical/types.cpp`:
- Around line 70-82: Update ReadyQueue’s unscoped access paths, including
try_pop(TaskSlot&) and try_front(TaskSlot&), so they no longer select buckets by
unordered_map iteration order; remove these overloads or add separate
run-insertion-order tracking that preserves FIFO, and update callers such as
try_pop_group(TaskSlot&) and try_pop_single(worker_id, TaskSlot&) accordingly.

In `@src/common/hierarchical/worker_manager.cpp`:
- Around line 397-404: Move the acceptance callback out of the mailbox-locked
section in the worker task flow around read_mailbox_state and
LocalMailboxEndpoint::run. Record that TASK_ACCEPTED was observed while polling,
then invoke on_accept_ only after the mailbox round-trip and mailbox_mu_ have
been released, preserving exactly-once callback behavior.

In `@tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py`:
- Line 114: After unpacking tensors into first_a, first_b, first_out, second_a,
second_b, and second_out, explicitly delete those references before
free_host_buffer runs; apply the same cleanup at the corresponding later
unpacking block, while guarding it for early-failure paths where the names may
not be bound.

---

Outside diff comments:
In `@src/common/hierarchical/orchestrator.cpp`:
- Around line 177-206: Update fail_run_submission and cancel_unstarted_run to
synchronize phase inspection with the run/completion state, preventing
cancellation from racing scheduler activation. Recheck the current phase while
holding the appropriate lock before cancelling, and make each slot-state update
use compare-and-exchange transitions that only convert valid non-running states;
never mark an EXECUTING/RUNNING slot FAILED or overwrite a concurrent scheduler
transition.

---

Nitpick comments:
In `@python/simpler/task_interface.py`:
- Around line 1277-1284: Add concise one-line docstrings to the pipeline_depth
and runtime_slot_count properties in the task interface, matching the
neighboring properties’ style and documenting each value’s unit or semantics.

In `@src/common/hierarchical/scheduler.cpp`:
- Around line 366-369: Consolidate the repeated active-run resolution and
scoped/unscoped ready-queue branching across all four dispatch paths and the
wait predicate in the scheduler class. Add small private helpers such as
resolve_active_run() and pop_ready(...) that preserve invalid-run early returns
while centralizing queue API selection, then update the affected paths to use
them.

In `@src/common/platform/onboard/host/device_runner_base.cpp`:
- Around line 143-150: Update select_arena_bank and the associated arena
consumers to validate against the actual number of supported banks rather than
PTO_PIPELINE_MAX_DEPTH. Define or reuse a two-bank count, reject IDs outside
that range, and replace binary bank-selection logic with indexed access so each
accepted bank maps to its own arena without aliasing.

In `@src/common/platform/onboard/host/device_runner_base.h`:
- Around line 297-320: Update the documentation for record_device_orch_callable
to add an `@param` entry describing aicore_image_hash, matching its position and
purpose in the function signature. Also add the corresponding parameter
documentation to record_host_orch_callable, without changing implementation
behavior.

In `@tests/ut/cpp/hierarchical/test_orchestrator.cpp`:
- Around line 656-665: Ensure the asynchronous third begin_run test always
releases an admission slot before any ASSERT_* can exit the test, by adding a
scope guard around the first run’s slot ownership and consumption cleanup.
Preserve the existing successful-path behavior while making cleanup execute
during assertion-induced early returns, so third.get() cannot leave the test
hanging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 976e895c-87d2-41d9-ab42-f455679ef0bc

📥 Commits

Reviewing files that changed from the base of the PR and between f44c715 and ae22766.

📒 Files selected for processing (45)
  • docs/orchestrator.md
  • docs/task-flow.md
  • docs/worker-manager.md
  • python/bindings/task_interface.cpp
  • python/bindings/worker_bind.h
  • python/simpler/orchestrator.py
  • python/simpler/task_interface.py
  • python/simpler/worker.py
  • src/a2a3/platform/onboard/host/device_runner.cpp
  • src/a2a3/platform/onboard/host/device_runner.h
  • src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • src/a5/runtime/host_build_graph/host/runtime_maker.cpp
  • src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • src/common/hierarchical/orchestrator.cpp
  • src/common/hierarchical/orchestrator.h
  • src/common/hierarchical/scheduler.cpp
  • src/common/hierarchical/scheduler.h
  • src/common/hierarchical/types.cpp
  • src/common/hierarchical/types.h
  • src/common/hierarchical/worker.cpp
  • src/common/hierarchical/worker.h
  • src/common/hierarchical/worker_manager.cpp
  • src/common/hierarchical/worker_manager.h
  • src/common/platform/onboard/host/c_api_shared.cpp
  • src/common/platform/onboard/host/device_runner_base.cpp
  • src/common/platform/onboard/host/device_runner_base.h
  • src/common/platform/sim/host/c_api_shared.cpp
  • src/common/task_interface/chip_callable_layout.h
  • src/common/task_interface/prepare_callable_common.h
  • src/common/utils/fnv1a_64.h
  • src/common/worker/chip_worker.cpp
  • src/common/worker/chip_worker.h
  • src/common/worker/pipeline_contract.h
  • src/common/worker/pipeline_slot_pool.h
  • src/common/worker/pto_runtime_c_api.h
  • tests/st/a2a3/host_build_graph/run_stream_reuse/kernels/aiv/kernel_sub.cpp
  • tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py
  • tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py
  • tests/ut/cpp/hierarchical/test_orchestrator.cpp
  • tests/ut/cpp/hierarchical/test_pipeline_contract.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/cpp/types/test_chip_callable_upload_immutable.cpp
  • tests/ut/py/test_callable_identity.py
  • tests/ut/py/test_worker/test_host_worker.py

Comment thread docs/orchestrator.md
Comment thread docs/worker-manager.md Outdated
Comment thread python/simpler/worker.py
Comment thread python/simpler/worker.py Outdated
Comment thread src/a2a3/platform/onboard/host/device_runner.cpp
Comment thread src/common/hierarchical/orchestrator.cpp
Comment thread src/common/hierarchical/types.cpp Outdated
Comment thread src/common/hierarchical/worker_manager.cpp
@Crane-Liu
Crane-Liu force-pushed the codex/worker-async-b2-whole-run-fifo branch 2 times, most recently from 8fcabcf to 17d945b Compare July 29, 2026 07:05
@Crane-Liu

Copy link
Copy Markdown
Contributor Author

B2 was rebuilt and force-with-lease updated as a single commit on the current main.

Key correction: the FIFO head can execute while its graph callback is still open, so existing L3-L2 interactive callbacks no longer deadlock. A successor still cannot dispatch until the previous run is terminal. The busy-SUB requeue path now preserves the active run partition, and unscoped queue access preserves run insertion order.

Validation is recorded in the PR body. Final hardware task task_20260729_000230_235780427710 passed the B2 FIFO case and both previously timing-out L3-L2 examples on A2/A3.

@ChaoWao

ChaoWao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Head is fe60768a,rebase 到 upstream/main@8e89f014。三条 P1 和 P2 都修了,每条都带变异验证过的回归测试 —— 因为这三个缺陷在原有的 65 + 867 个测试下完全不可见

[P1] 取消与 dispatch 竞态

两半都修了。前半:三条派发路径(SUB / NEXT_LEVEL group / single)的「读 READY … 写 RUNNING」合并成一次原子交换,抽成 claim_for_dispatch;claim 失败就把 slot 留给移动它的那一方。后半(你指出的、我第一轮漏了的):cancellation 只 CAS 一次会漏掉并发 PENDING → READY,改成重试。

dispatchable_run_id() 现在是 scheduler 真正询问的东西 —— head 还必须是 EXECUTING 且仍持有 lease。之前 can_dispatch_run() 存在、有 4 处测试,但生产路径从不调用它:把它整个改成 return true,65 个测试里只挂 1 个(直接断言它的那个)。

确定性竞态测试用的是真实取消路径Scheduler::Config::before_claim_cb(生产为空)是唯一能观察那个窗口的点——其余任何位置都在 pop 之前或 launch 之后。测试在回调里跑真实的 fail_run_submission,断言 slot 不是 RUNNING 且 mock_worker.dispatched 为空。变异(claim 退回无条件 store)时精确命中 Which is: 03(RUNNING) vs 05(FAILED)。

[P1] 构图失败提前释放 RUNNING task 使用的 producer

第一个循环现在记录本次成功 PENDING/READY → FAILED 的 slot,后两个循环只遍历它。CancelKeepsProducerRefsHeldByARunningConsumer 在变异(退回遍历全部 slot)时报 Which is: 2 vs 1 —— 正是那次多余的释放;配套的 CancelReleasesProducerRefsHeldByAnUnstartedConsumer 保证没有反向过度约束。

[P1] whole-run FIFO 未覆盖 callback 中的直接 device control

malloc / free / copy_to / copy_from 现在阻塞到调用方的 run 持有 FIFO 头;activate_fifo_head() 补上了缺失的 runs_cv_.notify_all(),否则被挡住的 copy_to 收不到「你成为 active 了」的唤醒。

四个入口我全部当成 device effect,没有做 host-only / device-effect 二分——它们都跨 mailbox 在 child 上执行真实操作,malloc 也会改 child 的 allocator 状态。如果某一个应当允许在 prepared 状态执行以换取 overlap,那需要单独论证它不影响 N,我不替这条线做决定。

这条修复本身引入了一个我没预料的缺陷,是 onboard 回归测试抓出来的:那四个 nanobind 绑定不释放 GIL,于是一个可能无限期的 runs_cv_.wait 冻住了整个解释器:

[T] callback entered at 0.030s
[T] entered observed at 30.008s     ← 主线程 30 秒后才看到

主线程连 event.wait() 都跑不动,连让 active run 收尾的那一步也执行不了,SUB 只能超时失败。四个绑定加了 nb::call_guard<nb::gil_scoped_release>() 后,用例耗时从 41s 降到 12s。若只加 gate 不加你要求的回归测试,这个冻结会直接进 merge。

[P2] API 与文档

submit() docstring 改成按协商深度表述,并写明 depth=1 时第二个提交即阻塞、依赖后续 callback 解锁前序 run 的调用方会死锁。三处矛盾文档一并更新:task-flow.md("production run 不携带 lease" —— 这段是我在 #1540 写的,本 PR 让它过时了)、orchestrator.md(admission 等待前序 acceptance → 改为 lease 预留)、scheduler.md(全局 shared queue → 按 run 分区,并说明原子 claim)。

Validation

结果
pre-commit 全部通过
C++ ctest 65/65
pytest tests/ut 867 passed, 9 skipped
a2a3 onboard worker_async_fifo 2 passed
Mutant 结果
claim 退回无条件 store 抓到 — 竞态测试 + claim 单测同时挂
cancellation 遍历全部 slot(含 RUNNING) 抓到Which is: 2 vs 1
gate 整个禁用(return true 抓到 — 只有直接断言它的测试挂,正是这条证明了它此前不在生产路径上

原 onboard FIFO 用例的 second_out == 0 只在一个瞬间检查,保持期内不复查;已在 hold 结束后补一次复查。

@ChaoWao
ChaoWao force-pushed the codex/worker-async-b2-whole-run-fifo branch from fe60768 to 7c65ace Compare July 30, 2026 04:28
@ChaoWao

ChaoWao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Head is 7c65ace0。这一轮的四条修完了,P1-D 我不在这个 PR 里修 —— 见末尾。

[P1] poison 与 cancellation 争同一 slot 的清理权

核实成立:poison_task 是 load 后无条件 store(FAILED),能把 cancellation 已经推到 CONSUMED 的 slot 拉回 FAILED、再 consume 一次、再释放一次 fanin。两条路径现在共享同一个 CAS claim,只有 PENDING/READY 可被取走;failure_message 移到 claim 成功之后写,你指出的 std::string write-write race 随之消失。

[P1] group front→pop 窗口会终止进程

核实成立,且 Scheduler::run() 确实没有任何 handler。try_pop_group 返回 false 现在被当作合法结果(并发 cancellation 消费了 slot 并擦除分区)而直接返回;popped != slot 则把取到的重新入队再退出,不丢任务、不 throw。

[P1] device-control admission 既漏拦又误拦

三个子问题一个正解,按你的处方改了:

  • 漏拦allocate_domain / create_l3_l2_region / create_l3_l2_queue 补上 gate。
  • 误拦:不再从全局 building_run_id_ 猜调用者。新增 thread-local _CALLBACK_RUN_callback_run(run_id) 包住 graph callback),C++ 侧改成 await_run_admission(RunId) 由调用方显式指名。另一线程的公开 Worker.copy_* 不再被记到正在 building 的那个 run 名下。
  • 锁环:admission wait 移到 Python 侧,在 _child_prov_lock 之前执行。

binding 释放 GIL —— 上一轮我在这上面栽过:持 GIL 做无界等待会冻住整个解释器,连让 active run 收尾的线程都跑不动。

[P2] 已终止 run 的 partition 可被重新创建

enqueue_ready 先确认 run 仍在且未 terminal。回归测试 ATerminalRunsQueuePartitionIsNotRebuilt,变异(去掉该检查)时报 a terminal run's partition was rebuilt by a late enqueue

[P2] depth 文档矛盾

task-flow.md 的 admission 段改成按协商深度表述(并写明 depth=1 时第二个提交即阻塞、依赖后续 callback 解锁前序 run 会死锁);worker.py 里 "only one live device run" 那条注释改成区分「device 执行一次一个」与「admission 深度由后端协商」。

Validation

结果
pre-commit 全部通过
C++ ctest 65/65
pytest tests/ut 867 passed, 9 skipped
a2a3 onboard worker_async_fifo 2 passed

[P1-D] 采纳你的方案 1,并按你的拆分做成独立 PR

我同意 control lane 那条路不成立 —— 它只排序 control、排不了 Scheduler task,而且按到达顺序反而会把 N 的 cleanup 挡在 N+1 之后;要补齐就得引入 run epoch、cleanup marker、task/control 共同 ownership 和多 mailbox 原子占用,等于把 run fence 重做一遍且更大。

所以我按你给的形状实现 B2a:automatic run finalization——两阶段 native fence(EXECUTING → DEVICE_DRAINED → CLEANING → COMPLETED/FAILED)、每个 L3+ Worker 一条 FIFO finalizer 线程、RunHandle 降为 observer、cleanup 边界按「fence 前 / 激活后」划分,暂时保持 depth=1。

它会是独立 PR,本 PR 需要 rebase 到它之上,不先合。我不把 P1-D 塞进这个 diff:它改的是 run 终结的所有权(RunHandle.wait() 不再负责 cleanup,否则用户不调 wait() 时流水线无法自主推进),和 K=2/FIFO 是两件事,混在一起没法审。

有一点我会在 B2a 里一并处理,因为它属于同一个契约:callback control 和 cleanup control 都显式携带 run_id / cleanup token,所有 admission wait 都在取 Python 锁之前。本 PR 已经把 callback 侧做成这样了,cleanup 侧要等 finalizer 存在才有地方挂。

@ChaoWao
ChaoWao force-pushed the codex/worker-async-b2-whole-run-fifo branch 4 times, most recently from 76e877d to 6310890 Compare July 31, 2026 04:08
@ChaoWao
ChaoWao force-pushed the codex/worker-async-b2-whole-run-fifo branch 2 times, most recently from bb43d90 to 5deb624 Compare August 1, 2026 08:51
Enforce bounded whole-run FIFO ordering across scheduling, execution, and teardown.

Make run, remote-buffer, CommDomain, and shared-memory ownership transactional under interruption, with exact-once cleanup publication.

Expand lifecycle documentation plus native, Python fault-injection, and onboard coverage.
@ChaoWao ChaoWao changed the title Feat: add bounded whole-run FIFO admission Update: make whole-run FIFO admission failure-safe Aug 2, 2026
@ChaoWao
ChaoWao merged commit 9a2a5f7 into hw-native-sys:main Aug 2, 2026
18 checks passed
lterrac added a commit to lterrac/simpler that referenced this pull request Aug 6, 2026
`_child_prov_lock` is held across the *native* half of malloc / free /
copy_to / copy_from, so every device op of every next-level worker
serializes on one lock belonging to the parent worker. The bindings
already release the GIL there, which makes the effect easy to miss:
unrelated Python threads keep running, but a `copy_to` on chip 0 blocks
a `malloc` on chip 1 for its whole duration.

Keep `_child_prov_lock` as the bookkeeping lock — it still makes each
provenance mutation/read atomic, and the safety-first ordering is
unchanged (record after a successful alloc, revoke before a native
free) — and take a per-worker lock around the native call. Ops on the
same worker stay mutually exclusive, so a copy can still never overlap
that buffer's free; ops on different workers now overlap. The
per-worker lock is always acquired before `_child_prov_lock` and never
the reverse, so the pair cannot deadlock.

This is necessary but, on this base, not sufficient: `_control_reservation`
(hw-native-sys#1541) holds `_submit_mu` across the same native call at the same
granularity, so an 8-chip upload still serializes there. Measured on
8 x 910B2 with this commit alone, uploading rank-stacked weights one
thread per shard: threaded 6.5-8.0 GB/s vs serial 8.6-10.3 GB/s — no
gain. The following commit addresses that lock; with both, the same
harness reaches 19.2-34.0 GB/s threaded.
lterrac added a commit to lterrac/simpler that referenced this pull request Aug 6, 2026
A control command that belongs to no run takes `_submit_mu` through
`_control_reservation` and holds it across the native call, so on this
base it — not `_child_prov_lock` — is what serializes an 8-chip upload:
all 8 threads enter `Orchestrator.copy_to` within 14 us of each other,
then each child starts its copy within ~1 ms of the previous one
finishing, at full link speed.

What such a command needs is "no run may be admitted while I run",
which is a property of the worker; two commands on different chips can
both have that at the same time. Make `_submit_mu` a shared/exclusive
lock: run admission takes it exclusively, control takes it shared.
Writer-preferring, so control traffic cannot starve a submit. The
reservation's re-entrancy is untouched: it short-circuits on the
thread-local set before reaching the lock.

Measured on 8 x 910B2, one upload thread per shard, fresh
shared-memory bands so every page is read cold exactly once:

  provenance locks only   threaded  6.5-8.0 GB/s   serial 8.6-10.3
  + this commit           threaded 19.2-34.0 GB/s  serial 9.4-10.5

Draft: this is a change to the serializer hw-native-sys#1541 introduced, and the
reviewer of that area may prefer a different shape. Sent as a separate
commit so it can be dropped or replaced without touching the
provenance-lock fix.
lterrac added a commit to lterrac/simpler that referenced this pull request Aug 6, 2026
`_child_prov_lock` is held across the *native* half of malloc / free /
copy_to / copy_from, so every device op of every next-level worker
serializes on one lock belonging to the parent worker. The bindings
already release the GIL there, which makes the effect easy to miss:
unrelated Python threads keep running, but a `copy_to` on chip 0 blocks
a `malloc` on chip 1 for its whole duration.

Keep `_child_prov_lock` as the bookkeeping lock — it still makes each
provenance mutation/read atomic, and the safety-first ordering is
unchanged (record after a successful alloc, revoke before a native
free) — and take a per-worker lock around the native call. Ops on the
same worker stay mutually exclusive, so a copy can still never overlap
that buffer's free; ops on different workers now overlap. The
per-worker lock is always acquired before `_child_prov_lock` and never
the reverse, so the pair cannot deadlock.

This narrows an invariant an existing test pins down, so that test is
updated rather than left passing by accident:
`test_free_holds_lock_across_native_free` asserted that the *parent
worker's* lock is held across the native free. It now asserts the
narrower exclusion actually needed — that worker's own lock held across
the native call, `_child_prov_lock` released, and the revoke committed
first. Provenance is keyed by (worker_id, ptr) and revoked before the
native free, so a concurrent dispatch reads the table under
`_child_prov_lock` and finds the address already gone, or is about a
different chip entirely.

This is necessary but, on this base, not sufficient: `_control_reservation`
(hw-native-sys#1541) holds `_submit_mu` across the same native call at the same
granularity, so an 8-chip upload still serializes there. Measured on
8 x 910B2 with this commit alone, uploading rank-stacked weights one
thread per shard: threaded 6.5-8.0 GB/s vs serial 8.6-10.3 GB/s — no
gain. The following commit addresses that lock; with both, the same
harness reaches 19.2-34.0 GB/s threaded.
lterrac added a commit to lterrac/simpler that referenced this pull request Aug 6, 2026
A control command that belongs to no run takes `_submit_mu` through
`_control_reservation` and holds it across the native call, so on this
base it — not `_child_prov_lock` — is what serializes an 8-chip upload:
all 8 threads enter `Orchestrator.copy_to` within 14 us of each other,
then each child starts its copy within ~1 ms of the previous one
finishing, at full link speed.

What such a command needs is "no run may be admitted while I run",
which is a property of the worker; two commands on different chips can
both have that at the same time. Make `_submit_mu` a shared/exclusive
lock: run admission takes it exclusively, control takes it shared.
Writer-preferring, so control traffic cannot starve a submit. The
reservation's re-entrancy is untouched: it short-circuits on the
thread-local set before reaching the lock.

Measured on 8 x 910B2, one upload thread per shard, fresh
shared-memory bands so every page is read cold exactly once:

  provenance locks only   threaded  6.5-8.0 GB/s   serial 8.6-10.3
  + this commit           threaded 19.2-34.0 GB/s  serial 9.4-10.5

Draft: this is a change to the serializer hw-native-sys#1541 introduced, and the
reviewer of that area may prefer a different shape. Sent as a separate
commit so it can be dropped or replaced without touching the
provenance-lock fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants