Skip to content

Refactor: reuse native execution thread per runner - #1654

Open
Crane-Liu wants to merge 3 commits into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-w4-progress-consolidation
Open

Refactor: reuse native execution thread per runner#1654
Crane-Liu wants to merge 3 commits into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-w4-progress-consolidation

Conversation

@Crane-Liu

@Crane-Liu Crane-Liu commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

This is W4 on current main (b5261a7f), stacked on #1650 and #1588.

  • replace per-run compatibility executors with one persistent native execution thread per runner
  • keep the endpoint/direct-L2 path as the sole FIFO, prepare, launch, poll, and finalize progress owner
  • park the persistent execution thread on a condition variable between runs
  • preserve blocking run() and native wait behavior through per-run completion conditions
  • expose a test-only creation count proving repeated runs reuse exactly one execution thread

The change adds no scheduling surface and keeps device execution strictly sequential. The execution task sends its final launch fallback notification before publishing completion; after publication it no longer dereferences run-owned state, preventing finalization from destroying state still used by the persistent executor.

Dependency

Merge order: #1650, then #1588, then this PR. GitHub displays the unmerged dependency commits until the earlier PRs merge.

Validation

  • Python non-hardware tests: 1054 passed, 14 deselected.
  • C++ non-hardware tests: 75/75 passed.
  • Changed-files pre-commit hooks passed.
  • A2A3 hardware: task_20260803_050755_199972530808, all cases passed (7 L3, 6 L2 HBG, 3 L2 TMR); persistent execution thread creation stayed exactly 1.
  • Latest stacked rebase to b5261a7f was conflict-free.
  • PyPTO Q1 points to this exact head 999e70b73cf50bdffa7f9d5833d28e87523dc74d; its real-hardware test passed in task_20260803_064735_169883129670.

No simulation run was used as acceptance evidence.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds asynchronous L2 run handles, concurrent successor preparation, prepared-run compatibility checks, persistent native execution threads, optional runtime capabilities, and expanded lifecycle tests and documentation.

Changes

Native pipeline lifecycle

Layer / File(s) Summary
Runtime contracts and optional capabilities
src/common/worker/*, python/bindings/task_interface.cpp, python/simpler/task_interface.py, src/common/platform/sim/...
Runtime symbols and native-run identifiers are optional where supported. New error and execution-thread count APIs are exposed.
Persistent native execution and completion
src/common/platform/onboard/host/*, src/common/worker/native_run_state.h, src/common/worker/native_run_launch_signal.h, src/a2a3/platform/onboard/host/device_runner.cpp
Native execution uses a persistent serialized worker thread. Completion uses condition-variable synchronization. Finalization shuts down native execution before releasing resources.
Prepared-run compatibility and fallback
src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp, src/common/platform/onboard/host/c_api_shared.cpp, tests/ut/cpp/common/test_trb_runtime_temp_buffer.cpp, tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py
Successor preparation checks resolved ring configuration against cached arena resources. Incompatible successors defer preparation and retry after the active run completes.
L2 run handles and pipeline control
python/simpler/worker.py, tests/ut/py/test_worker/test_host_worker.py, tests/ut/py/test_worker/test_startup_readiness.py
L2 submission now returns after launch. Run handles drive polling, waiting, finalization, FIFO successor admission, backpressure, and deferred preparation.
Two-frame staging and lifecycle validation
docs/*.md, src/a2a3/runtime/tensormap_and_ringbuffer/docs/*, tests/st/a2a3/host_build_graph/*
Documentation and tests cover validation-only staging, FIFO activation, fresh AICore streams, asynchronous completion, diagnostic deferral, and persistent execution-thread reuse.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Worker
  participant ChipWorker
  participant DeviceRunner
  participant NativeRunState
  Client->>Worker: submit L2 run
  Worker->>ChipWorker: prepare and launch run
  ChipWorker->>DeviceRunner: submit native execution
  DeviceRunner->>NativeRunState: publish completion
  Worker->>ChipWorker: poll and finalize
  Client->>Worker: wait on RunHandle
  Worker-->>Client: return completion
Loading

Possibly related PRs

Poem

I’m a rabbit with a run handle bright,
Preparing successors through the night.
One thread persists, streams bloom anew,
FIFO hops keep each frame in view.
The fences clear—then outputs appear! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.57% 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: reusing one persistent native execution thread per runner.
Description check ✅ Passed The description directly explains persistent thread reuse, completion signaling, ownership, dependencies, 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: 7

🧹 Nitpick comments (5)
tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py (1)

482-489: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the new test to the standalone-runner hook.

TestWorkerAsyncWholeRunFifo._run_and_validate_l3 invokes the three parent tests by name. This subclass does not override that hook, so test_incompatible_runtime_env_falls_back_to_depth_one runs under pytest but never runs through the standalone scene runner. Override the hook in the subclass so both entry points cover the TMR fallback.

♻️ Proposed override
 `@scene_test`(level=3, runtime="tensormap_and_ringbuffer")
 class TestWorkerAsyncWholeRunFifoTmr(TestWorkerAsyncWholeRunFifo):
     """TMR uses the common FIFO with shared-arena compatibility fallback."""
 
+    def _run_and_validate_l3(self, worker, compiled_callables, sub_handles, case, **kwargs):
+        super()._run_and_validate_l3(worker, compiled_callables, sub_handles, case, **kwargs)
+        platform = str(worker._config["platform"])  # noqa: SLF001 -- scene-test white-box validation
+        self.test_incompatible_runtime_env_falls_back_to_depth_one(platform, worker)
+
     def test_incompatible_runtime_env_falls_back_to_depth_one(self, st_platform, st_worker):
🤖 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/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py`
around lines 482 - 489, Override TestWorkerAsyncWholeRunFifoTmr’s
_run_and_validate_l3 hook to invoke the inherited standalone-runner tests plus
test_incompatible_runtime_env_falls_back_to_depth_one, preserving the existing
parent hook behavior so both pytest and standalone execution cover the TMR
fallback.
tests/ut/py/test_worker/test_host_worker.py (1)

2521-2541: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add acceptance coverage for a deferred L2 run.

This test pins the deferred phase and the later prepare/launch, but no test calls the acceptance wait while a run sits in phase "deferred". That is the exact gap behind the _wait_run_handle_accepted defect flagged in python/simpler/worker.py at lines 8633-8646, where the method returns immediately for a deferred run. Add an assertion that the acceptance wait blocks until run 2 leaves the deferred phase.

🤖 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/py/test_worker/test_host_worker.py` around lines 2521 - 2541, Add
coverage in
test_incompatible_successor_defers_prepare_without_failing_its_handle for
_wait_run_handle_accepted: invoke it for run 2 while worker._l2_runs[2].phase is
"deferred" and assert it remains blocked, then release the first run and verify
the wait completes only after run 2 leaves the deferred phase. Preserve the
existing prepare/launch and handle-completion assertions.
python/simpler/worker.py (1)

3174-3176: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document and centralize the depth-one fallback sentinel.

_native_prepare_requires_depth_one matches a free-text substring produced by ChipWorker::prepare_native_run_on_slot in src/common/worker/chip_worker.cpp (the PTO_RUNTIME_ERR_PREPARED_INCOMPATIBLE branch). If that message is reworded, this predicate silently returns False, and an incompatible successor becomes a hard submit failure instead of a deferred retry. Extract the literal into a named module constant and name the producing C++ site in the comment, so the coupling is discoverable from both ends.

♻️ Proposed change
+# Must match the message emitted by ChipWorker::prepare_native_run_on_slot for
+# PTO_RUNTIME_ERR_PREPARED_INCOMPATIBLE in src/common/worker/chip_worker.cpp.
+_NATIVE_PREPARE_DEPTH_ONE_SENTINEL = "native prepare requires depth-one fallback"
+
+
 def _native_prepare_requires_depth_one(error: BaseException) -> bool:
     """Recognize the backend's explicit, correctness-preserving fallback."""
-    return "native prepare requires depth-one fallback" in str(error)
+    return _NATIVE_PREPARE_DEPTH_ONE_SENTINEL in str(error)
🤖 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/worker.py` around lines 3174 - 3176, Extract the depth-one
fallback sentinel string used by _native_prepare_requires_depth_one into a named
module-level constant, and have the predicate reference that constant for its
substring match. Document the constant or predicate comment with the producing
ChipWorker::prepare_native_run_on_slot PTO_RUNTIME_ERR_PREPARED_INCOMPATIBLE
site so this cross-language coupling remains discoverable.
tests/ut/py/test_worker/test_startup_readiness.py (1)

697-697: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep completion state per native run.

_wait_native_run() sets one chip-wide complete flag. After the first run completes, _poll_native_run() returns True for every later token. This can hide stale-completion bugs in repeated submissions.

Store completion by token, or reset the state during _prepare_native_run_with_pipeline_lease().

Proposed test-double fix
-        self.complete = False
+        self._completed_tokens = set()

-        return self.complete
+        return token in self._completed_tokens

-        self.complete = True
+        self._completed_tokens.add(token)

Also applies to: 719-725

🤖 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/py/test_worker/test_startup_readiness.py` at line 697, Update the
test double’s completion tracking used by _wait_native_run() and
_poll_native_run() so completion is scoped to each native-run token rather than
shared through one chip-wide self.complete flag. Store per-token completion
state, or reset self.complete in _prepare_native_run_with_pipeline_lease(),
ensuring later submissions cannot inherit completion from an earlier run.
src/common/platform/onboard/host/device_runner_base.cpp (1)

409-437: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Harden submit_native_execution's single-task invariant.

The occupancy check tests native_execution_task_ but not native_execution_active_. Once the worker thread takes the task with std::move(native_execution_task_) (line 422), native_execution_task_ becomes empty while task() is still executing (native_execution_active_ stays true until line 427). During that window, a second submit_native_execution call passes the occupancy check and overwrites the queue slot, even though the first task has not finished.

Today, callers only reach this function after try_acquire_native_run succeeds, and that exclusive claim is not released until after wait_for_execution_complete() runs in simpler_finalize_run, so this is not reachable under the current call graph. Add native_execution_active_ to the guard so the function's own invariant does not silently depend on external callers always honoring the acquire/release protocol.

Additionally, task() at line 425 runs with no catch-all in the worker loop. An exception that escapes the submitted lambda calls std::terminate() and aborts the whole process, not just the one run. The submitted lambda in simpler_launch_run wraps the risky calls internally, but a defensive catch-all here removes the fragility of relying on every future caller getting that coverage right for a thread that is now reused across the runner's lifetime.

🔒 Proposed hardening for the occupancy check and task execution
 bool DeviceRunnerBase::submit_native_execution(std::function<void()> fn) {
     std::lock_guard<std::mutex> lock(native_execution_mu_);
-    if (native_execution_stop_ || native_execution_task_) return false;
+    if (native_execution_stop_ || native_execution_task_ || native_execution_active_) return false;

     if (!native_execution_thread_.joinable()) {
         native_execution_thread_ = create_thread([this]() {
             std::unique_lock<std::mutex> lock(native_execution_mu_);
             while (true) {
                 native_execution_cv_.wait(lock, [this]() {
                     return native_execution_stop_ || static_cast<bool>(native_execution_task_);
                 });
                 if (native_execution_stop_ && !native_execution_task_) return;

                 std::function<void()> task = std::move(native_execution_task_);
                 native_execution_active_ = true;
                 lock.unlock();
-                task();
+                try {
+                    task();
+                } catch (...) {
+                    // A native run task must never crash the persistent worker thread.
+                }
                 lock.lock();
                 native_execution_active_ = false;
                 native_execution_cv_.notify_all();
             }
         });
         native_execution_thread_create_count_.fetch_add(1, std::memory_order_relaxed);
     }
🤖 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 409 -
437, Update DeviceRunnerBase::submit_native_execution so its occupancy guard
rejects submissions when either native_execution_task_ is queued or
native_execution_active_ is true, preserving the single-task invariant. In the
worker loop created there, wrap task() execution in a catch-all handler so
exceptions from submitted tasks are contained and do not terminate the process;
ensure the worker still reacquires the mutex and clears native_execution_active_
afterward.
🤖 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/task-flow.md`:
- Around line 730-731: Update the chip_0 native-run path walkthrough to
distinguish active-frame and successor preparation: the active frame prepares
after staging, while a capable successor may be prepared before ACTIVATE but
must not launch until activation. Keep the existing launch, polling,
finalization, and compatibility endpoint behavior unchanged.

In `@python/simpler/worker.py`:
- Around line 8291-8320: Refactor _l2_progress_locked so the FIFO state lock is
separate from the progress-ownership lock, and release the state lock before
blocking in _wait_native_run or sleeping during progress polling. Replace
time.sleep with _l2_progress_cv.wait using the remaining timeout, while
preserving FIFO handoff and completion handling. Ensure nested callers such as
_submit_l2_locked do not leave an RLock recursion level held across native
waits, so RunHandle.done remains non-blocking.
- Line 8322: Rename the callable parameter in _submit_l2_locked to avoid
shadowing Python’s callable builtin, and update all references within the
method, including the RunHandle keepalive tuple. If compatibility requires
retaining the parameter name in surrounding submit or _submit_locked methods,
suppress A002 only on those signatures.
- Around line 8633-8646: Update _wait_run_handle_accepted so L2 runs in the
"deferred" phase continue waiting instead of being treated as accepted; only
exit once the run crosses the launch fence into a non-prepared, non-deferred
state, while preserving existing error propagation.

In
`@tests/st/a2a3/host_build_graph/native_run_lifecycle/test_native_run_lifecycle.py`:
- Around line 136-139: Update the setup around supports_concurrent_prepare to
call chip_worker.supports_concurrent_native_prepare() and store its returned
capability value, rather than storing the bound method; keep the subsequent
concurrent-preparation condition using that boolean result.
- Line 153: Update the run-stream count assertions in the native lifecycle
tests, including the assertion near chip_worker.run_stream_set_create_count and
its concurrent-successor equivalents, so they execute only for the a2a3
platform. Skip these checks for a2a3sim while preserving all other lifecycle
assertions.

In `@tests/ut/py/test_worker/test_startup_readiness.py`:
- Around line 803-814: Wrap the lifecycle assertions around run_handle and
chip_worker events in a try/finally block, and call w.close() in the finally
clause so the initialized worker is always released even when an assertion or
wait fails.

---

Nitpick comments:
In `@python/simpler/worker.py`:
- Around line 3174-3176: Extract the depth-one fallback sentinel string used by
_native_prepare_requires_depth_one into a named module-level constant, and have
the predicate reference that constant for its substring match. Document the
constant or predicate comment with the producing
ChipWorker::prepare_native_run_on_slot PTO_RUNTIME_ERR_PREPARED_INCOMPATIBLE
site so this cross-language coupling remains discoverable.

In `@src/common/platform/onboard/host/device_runner_base.cpp`:
- Around line 409-437: Update DeviceRunnerBase::submit_native_execution so its
occupancy guard rejects submissions when either native_execution_task_ is queued
or native_execution_active_ is true, preserving the single-task invariant. In
the worker loop created there, wrap task() execution in a catch-all handler so
exceptions from submitted tasks are contained and do not terminate the process;
ensure the worker still reacquires the mutex and clears native_execution_active_
afterward.

In `@tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py`:
- Around line 482-489: Override TestWorkerAsyncWholeRunFifoTmr’s
_run_and_validate_l3 hook to invoke the inherited standalone-runner tests plus
test_incompatible_runtime_env_falls_back_to_depth_one, preserving the existing
parent hook behavior so both pytest and standalone execution cover the TMR
fallback.

In `@tests/ut/py/test_worker/test_host_worker.py`:
- Around line 2521-2541: Add coverage in
test_incompatible_successor_defers_prepare_without_failing_its_handle for
_wait_run_handle_accepted: invoke it for run 2 while worker._l2_runs[2].phase is
"deferred" and assert it remains blocked, then release the first run and verify
the wait completes only after run 2 leaves the deferred phase. Preserve the
existing prepare/launch and handle-completion assertions.

In `@tests/ut/py/test_worker/test_startup_readiness.py`:
- Line 697: Update the test double’s completion tracking used by
_wait_native_run() and _poll_native_run() so completion is scoped to each
native-run token rather than shared through one chip-wide self.complete flag.
Store per-token completion state, or reset self.complete in
_prepare_native_run_with_pipeline_lease(), ensuring later submissions cannot
inherit completion from an earlier run.
🪄 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: 37d88084-439e-4df6-a0f9-36fe1fe40368

📥 Commits

Reviewing files that changed from the base of the PR and between c975b61 and 002b256.

📒 Files selected for processing (32)
  • docs/callable-identity-registration.md
  • docs/dynamic-linking.md
  • docs/task-flow.md
  • docs/worker-manager.md
  • python/bindings/task_interface.cpp
  • python/simpler/task_interface.py
  • python/simpler/worker.py
  • src/a2a3/platform/onboard/host/device_runner.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/docs/RUNTIME_LOGIC.md
  • src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • src/a5/platform/onboard/host/device_runner.cpp
  • src/a5/runtime/host_build_graph/host/runtime_maker.cpp
  • src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • 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/worker/chip_worker.cpp
  • src/common/worker/chip_worker.h
  • src/common/worker/native_run_launch_signal.h
  • src/common/worker/native_run_state.h
  • src/common/worker/pto_runtime_c_api.h
  • tests/st/a2a3/host_build_graph/native_run_lifecycle/kernels/orchestration/long_vector_orch.cpp
  • tests/st/a2a3/host_build_graph/native_run_lifecycle/test_native_run_lifecycle.py
  • tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py
  • tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py
  • tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py
  • tests/ut/cpp/common/test_trb_runtime_temp_buffer.cpp
  • tests/ut/cpp/hierarchical/test_pipeline_contract.cpp
  • tests/ut/py/test_host_runtime_abi.py
  • tests/ut/py/test_worker/test_host_worker.py
  • tests/ut/py/test_worker/test_startup_readiness.py
💤 Files with no reviewable changes (3)
  • src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • tests/ut/py/test_host_runtime_abi.py
  • src/a5/runtime/host_build_graph/host/runtime_maker.cpp

Comment thread docs/task-flow.md
Comment on lines +730 to +731
| 6 | chip_0 child process | validate the frame and resolve its digest, then publish `FRAME_STAGED`; a successor waits for `ACTIVATE`, while the active frame may proceed immediately |
| 7 | chip_0 native-run path | after activation, prepare and launch the native run; poll it to completion and finalize it before another staged frame may launch. Compatibility endpoints perform the equivalent operation through blocking `ChipWorker::run` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the walkthrough consistent with successor preparation.

Line 731 states that preparation occurs after activation. This conflicts with Lines 385-403, which allow a capable backend to prepare a successor before ACTIVATE.

State that an active frame prepares after staging, while a capable successor can already be prepared but cannot launch until activation.

🤖 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 `@docs/task-flow.md` around lines 730 - 731, Update the chip_0 native-run path
walkthrough to distinguish active-frame and successor preparation: the active
frame prepares after staging, while a capable successor may be prepared before
ACTIVATE but must not launch until activation. Keep the existing launch,
polling, finalization, and compatibility endpoint behavior unchanged.

Comment thread python/simpler/worker.py
Comment on lines +8291 to +8320
completed = False
progress_error: BaseException | None = None
try:
if block and deadline is None:
self._chip_worker._wait_native_run(front.native_run)
completed = True
else:
completed = bool(self._chip_worker._poll_native_run(front.native_run))
except BaseException as exc: # noqa: BLE001
progress_error = exc
completed = True

if completed:
self._l2_finish_front_locked(front, progress_error)
# Launch the successor at the same ordered handoff boundary.
if self._l2_fifo:
successor = self._l2_runs[self._l2_fifo[0]]
if successor.phase == "deferred":
self._l2_prepare_front_locked(successor)
if successor.phase == "prepared":
self._l2_launch_front_locked(successor)
continue

if not block:
return False
if deadline is not None:
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
time.sleep(min(remaining, _RUN_HANDLE_WAIT_RECHECK_S))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

_l2_progress_locked holds _l2_progress_cv across the blocking native wait.

Line 8295 calls _wait_native_run and line 8320 calls time.sleep while _l2_progress_cv is held. Every other lane entry point takes the same lock: _run_handle_done, _wait_run_handle, _wait_run_handle_accepted, and _submit_l2_locked. A RunHandle.done probe on run B therefore blocks for the full native execution of run A, and RunHandle.done is documented as a non-blocking read.

The straightforward fix is to replace time.sleep with self._l2_progress_cv.wait(...). That alone is not sufficient here: _l2_progress_cv wraps an RLock, and _submit_l2_locked enters _l2_progress_locked with the lock already held, so wait would release only one recursion level. Separating the FIFO-state lock from the progress-ownership lock, and dropping the state lock around the native wait, restores the non-blocking probe.

🤖 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/worker.py` around lines 8291 - 8320, Refactor
_l2_progress_locked so the FIFO state lock is separate from the
progress-ownership lock, and release the state lock before blocking in
_wait_native_run or sleeping during progress polling. Replace time.sleep with
_l2_progress_cv.wait using the remaining timeout, while preserving FIFO handoff
and completion handling. Ensure nested callers such as _submit_l2_locked do not
leave an RLock recursion level held across native waits, so RunHandle.done
remains non-blocking.

Comment thread python/simpler/worker.py
return False
time.sleep(min(remaining, _RUN_HANDLE_WAIT_RECHECK_S))

def _submit_l2_locked(self, callable, args, cfg: CallConfig) -> RunHandle:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename the callable parameter or silence the rule.

Ruff reports A002: the argument shadows the callable builtin. Ruff flags this changed line, so the rule is enabled for this file.

🔧 Proposed fix
-    def _submit_l2_locked(self, callable, args, cfg: CallConfig) -> RunHandle:
+    def _submit_l2_locked(self, callable_handle, args, cfg: CallConfig) -> RunHandle:
         assert self._chip_worker is not None
-        callable_state = self._resolve_handle(callable, expected_namespace="LOCAL_CHIP")
+        callable_state = self._resolve_handle(callable_handle, expected_namespace="LOCAL_CHIP")

Update the RunHandle(self, run_id, (callable, args, cfg)) keepalive tuple at line 8375 to use the new name. If the surrounding submit / _submit_locked signatures must keep the callable name for API compatibility, add # noqa: A002 instead.

🧰 Tools
🪛 Ruff (0.16.0)

[error] 8322-8322: Function argument callable is shadowing a Python builtin

(A002)

🤖 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/worker.py` at line 8322, Rename the callable parameter in
_submit_l2_locked to avoid shadowing Python’s callable builtin, and update all
references within the method, including the RunHandle keepalive tuple. If
compatibility requires retaining the parameter name in surrounding submit or
_submit_locked methods, suppress A002 only on those signatures.

Source: Linters/SAST tools

Comment thread python/simpler/worker.py
Comment on lines 8633 to 8646
def _wait_run_handle_accepted(self, run_id: int) -> None:
if self.level == 2:
with self._l2_progress_cv:
while True:
state = self._l2_runs.get(run_id)
if state is None:
raise RuntimeError(f"unknown direct L2 run id {run_id}")
if state.phase != "prepared":
if state.error is not None:
raise state.error
return
self._l2_progress_locked(run_id, None, block=True)
assert self._orch is not None
self._orch._wait_run_accepted(run_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

_wait_run_handle_accepted returns early for a deferred L2 run.

The loop exits as soon as state.phase != "prepared". A run in phase "deferred" has not been prepared and has not been launched, so the method reports acceptance for a run that never crossed its launch fence. This is exactly the state produced by the new depth-one fallback path in _submit_l2_locked. Treat "deferred" as not-yet-accepted.

🐛 Proposed fix
     def _wait_run_handle_accepted(self, run_id: int) -> None:
         if self.level == 2:
             with self._l2_progress_cv:
                 while True:
                     state = self._l2_runs.get(run_id)
                     if state is None:
                         raise RuntimeError(f"unknown direct L2 run id {run_id}")
-                    if state.phase != "prepared":
+                    if state.phase not in ("deferred", "prepared"):
                         if state.error is not None:
                             raise state.error
                         return
                     self._l2_progress_locked(run_id, None, block=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _wait_run_handle_accepted(self, run_id: int) -> None:
if self.level == 2:
with self._l2_progress_cv:
while True:
state = self._l2_runs.get(run_id)
if state is None:
raise RuntimeError(f"unknown direct L2 run id {run_id}")
if state.phase != "prepared":
if state.error is not None:
raise state.error
return
self._l2_progress_locked(run_id, None, block=True)
assert self._orch is not None
self._orch._wait_run_accepted(run_id)
def _wait_run_handle_accepted(self, run_id: int) -> None:
if self.level == 2:
with self._l2_progress_cv:
while True:
state = self._l2_runs.get(run_id)
if state is None:
raise RuntimeError(f"unknown direct L2 run id {run_id}")
if state.phase not in ("deferred", "prepared"):
if state.error is not None:
raise state.error
return
self._l2_progress_locked(run_id, None, block=True)
assert self._orch is not None
self._orch._wait_run_accepted(run_id)
🤖 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/worker.py` around lines 8633 - 8646, Update
_wait_run_handle_accepted so L2 runs in the "deferred" phase continue waiting
instead of being treated as accepted; only exit once the run crosses the launch
fence into a non-prepared, non-deferred state, while preserving existing error
propagation.

Comment on lines +136 to +139
supports_concurrent_prepare = chip_worker.supports_concurrent_native_prepare
chip_worker._register_callable_at_slot(_SLOT, callable_obj)
private_slot_registered = True
public_handle = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Call the capability method.

Line 136 stores a bound method. A bound method is truthy, so Line 201 enters the concurrent-preparation block even when the backend reports no support.

Call chip_worker.supports_concurrent_native_prepare() before the condition.

🤖 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/st/a2a3/host_build_graph/native_run_lifecycle/test_native_run_lifecycle.py`
around lines 136 - 139, Update the setup around supports_concurrent_prepare to
call chip_worker.supports_concurrent_native_prepare() and store its returned
capability value, rather than storing the bound method; keep the subsequent
concurrent-preparation condition using that boolean result.

first_run = native_run
expected_stream_count = stream_count_before_prepare + int(supports_concurrent_prepare)
assert chip_worker.run_stream_set_create_count == expected_stream_count
assert chip_worker.run_stream_set_create_count == stream_count_before_prepare + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skip run-stream count assertions on simulation.

CASES includes a2a3sim, but run_stream_set_create_count returns zero on simulation because it uses persistent bootstrap streams. Line 153 expects one new stream and fails on that platform.

Gate this assertion, and the equivalent concurrent-successor assertions, to a2a3.

🤖 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/st/a2a3/host_build_graph/native_run_lifecycle/test_native_run_lifecycle.py`
at line 153, Update the run-stream count assertions in the native lifecycle
tests, including the assertion near chip_worker.run_stream_set_create_count and
its concurrent-successor equivalents, so they execute only for the a2a3
platform. Skip these checks for a2a3sim while preserving all other lifecycle
assertions.

Comment on lines +803 to +814
assert not run_handle.done
assert run_handle.wait() is None
assert run_handle.done
chip_worker = w._chip_worker
assert chip_worker is not None
assert [event[0] for event in chip_worker.events] == [
"prepare",
"launch",
"poll",
"wait",
"finalize",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Close the worker in a finally block.

If run_handle.wait() or the event-order assertion fails, execution skips Line 815. The initialized worker then remains open and its resources are not released. Wrap the lifecycle assertions in try/finally and call w.close() from the finally block.

🤖 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/py/test_worker/test_startup_readiness.py` around lines 803 - 814,
Wrap the lifecycle assertions around run_handle and chip_worker events in a
try/finally block, and call w.close() in the finally clause so the initialized
worker is always released even when an assertion or wait fails.

@Crane-Liu
Crane-Liu force-pushed the codex/worker-async-w4-progress-consolidation branch 2 times, most recently from 70f978f to 4952338 Compare August 3, 2026 13:21
Rebase the common active-plus-prepared ownership on current main without weakening the uniform pipeline ABI from hw-native-sys#1587 or the runner geometry, stream, and TLS contracts from hw-native-sys#1653. Add generation-bound direct L2 RunHandles, bounded two-slot admission, launch-only acceptance waiting, and deterministic depth-one fallback while preserving HBG inactive-bank preparation. Remove timing-dependent endpoint assertions and keep RequestSession absent.
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.

1 participant