Skip to content

Add TMR prepared state pipeline - #1588

Open
Crane-Liu wants to merge 5 commits into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-b5-tmr-prepared-state
Open

Add TMR prepared state pipeline#1588
Crane-Liu wants to merge 5 commits into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-b5-tmr-prepared-state

Conversation

@Crane-Liu

Copy link
Copy Markdown
Contributor

Stack

Depends on #1587 (codex/worker-async-b4b-hbg-prepared-epoch).

Summary

  • add TMR slot-private prepared state while retaining one shared heap/SM/runtime image and SDMA workspace
  • gate preparation with a resolved RuntimeEnv compatibility key and explicitly reject incompatible prepared runs
  • prewarm the default A2A3 TMR arena key before the worker becomes ready
  • open successor preparation only after the active run publishes the native sticky launch ACK; TASK_ACTIVE no longer masquerades as launch acceptance
  • bind the same native ACK for prepared activation so later pipeline epochs retain the same O/S boundary
  • retain AICPU streams and reuse AICore streams for the same callable image; recreate AICore only when the image changes and poison failed retirement paths
  • add CPU and real-device coverage for same-image reuse, A-A-B-B-A image transitions, prepared FIFO activation, compatibility, and failure behavior

No simulation code was changed or run in this PR.

Validation

Remote CPU (liteserver-hps-148e, commit 3a6d6494 source state):

  • Python UT: 881 passed, 6 skipped
  • C++ no-hardware UT: 66/66 passed
  • scheduler acceptance/prepared targeted tests: 5/5 passed
  • HostWorker targeted tests: 91 passed
  • Ruff: passed

Queued A2A3 hardware:

  • architecture probe: task_20260729_044137_250311224272, exit 0, Ascend 910 A2A3
  • TMR prepared FIFO: task_20260729_055400_87277419590, 1 passed, exit 0
  • HBG stream reuse and A-A-B-B-A image transition: task_20260729_055400_87288330884, 2 passed, exit 0

TMR profiling from the final hardware run:

  • S1 runner_run: 17.495 ms
  • O2 simpler_prepare: starts 3.139 ms after S1 starts and lasts 0.170 ms
  • O2/S1 overlap: 0.170 ms (the full prepare interval)
  • S2 starts 105.901 ms after S1 ends, so S1/S2 overlap is 0 ms

- Separate control traffic from two identified task frames
- Accept a queued frame while one sequential child executor is active
- Bound parent dispatch capacity and fill every available frame
- Retire publish ordering across every pre-publish failure path
- Reject oversized argument blobs before publishing mailbox state
- Preserve single-frame fallbacks on unsupported local backends
- Keep failure-path tests and host-buffer cleanup terminal-safe
- Release the GIL around native execution so admission remains live
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds generation-safe pipeline leases, whole-run FIFO admission, prepared-run runtime APIs, image-aware AICore stream reuse, and a two-frame mailbox protocol with prepare/activate states. Scheduler, worker, binding, runtime, documentation, and test coverage are updated accordingly.

Changes

Prepared pipeline execution

Layer / File(s) Summary
Lease protocol and run admission
src/common/hierarchical/*, src/common/worker/pipeline_slot_pool.h, python/simpler/worker.py
Runs acquire generation-safe pipeline leases, task slots carry leases, and admission is bounded by configurable pipeline depth.
Run-aware queues and dispatch
src/common/hierarchical/types.*, scheduler.*, worker.*
Ready queues and dispatch paths track RunId, active runs, preparable runs, worker capacity, and FIFO promotion.
Prepared runtime ABI and execution
src/common/platform/onboard/host/*, src/common/worker/chip_worker.*, src/common/worker/pto_runtime_c_api.h
Prepared runtimes can be probed, prepared, executed, aborted, and validated against stable run identities.
Two-frame mailbox activation
src/common/hierarchical/worker_manager.*, python/simpler/worker.py
Mailbox frames implement generation checks and PREPARE_READY, BACKEND_READY, ACTIVATE, acceptance, and terminal states.
Image-aware stream reuse
src/common/platform/include/host/run_stream_slots.h, src/a2a3/platform/onboard/host/device_runner.*
AICore streams are reused for matching image hashes and recreated on image transitions, with poison handling for destroy failures.
Bindings and validation
python/bindings/*, tests/ut/*, tests/st/a2a3/*, docs/*
Bindings expose prepared execution and mailbox sizing; tests cover admission, activation, compatibility, stream reuse, and end-to-end workloads.

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

Possibly related issues

Possibly related PRs

Poem

A rabbit hops through frames of light,
With leases safe from stale moonlight.
Prepare, activate, streams align,
FIFO paths now march in time.
“Thump!” says the hare, “the slots are bright!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.36% 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
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding the TMR prepared state pipeline.
Description check ✅ Passed The description is clearly related to the changeset and matches the prepared pipeline, stream reuse, and validation work.
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.

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: 12

🧹 Nitpick comments (10)
tests/ut/py/test_callable_identity.py (1)

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

Use the shared pipeline-depth constant instead of the literal.

In this test path, the default depth comes from PTO_PIPELINE_MAX_DEPTH, so this assertion should reference the exported worker constant rather than hard-coding 2.

Suggested change
-        assert fake_c_worker.pipeline_depth == 2
+        assert fake_c_worker.pipeline_depth == worker_mod.PTO_PIPELINE_MAX_DEPTH
🤖 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_callable_identity.py` at line 604, Update the assertion for
fake_c_worker.pipeline_depth to compare against the exported
PTO_PIPELINE_MAX_DEPTH constant instead of the hard-coded literal 2, preserving
the existing default-depth validation.
tests/ut/cpp/hierarchical/test_orchestrator.cpp (1)

752-772: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Fatal assertions can leave a pending std::async future, turning a failed invariant into a CI hang. In each site, a ASSERT_* on wait_for(...) == ready returns from the test body while the async task may still be blocked, and ~std::future from std::async then blocks forever.

  • tests/ut/cpp/hierarchical/test_orchestrator.cpp#L752-L772: unconditionally drain the remaining lease (consume prepared) before the fatal assertion on third.
  • tests/ut/cpp/hierarchical/test_orchestrator.cpp#L805-L817: unconditionally consume active before the fatal assertion on replacement.
  • tests/ut/cpp/hierarchical/test_scheduler.cpp#L555-L566: unblock the second dispatch (write TASK_ACTIVE) before the fatal assertion, or downgrade it to EXPECT_* plus an explicit unblock.
🤖 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 752 - 772,
Prevent fatal assertions from destroying still-blocked std::async futures: in
tests/ut/cpp/hierarchical/test_orchestrator.cpp lines 752-772, always consume
prepared via its task slot before the fatal third readiness assertion; in
tests/ut/cpp/hierarchical/test_orchestrator.cpp lines 805-817, always consume
active before the fatal replacement readiness assertion; in
tests/ut/cpp/hierarchical/test_scheduler.cpp lines 555-566, write TASK_ACTIVE to
unblock the second dispatch before the fatal assertion, or replace it with an
EXPECT_* followed by explicit unblocking.
src/common/hierarchical/scheduler.cpp (1)

365-402: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared preparable-eligibility predicate.

preparable_work_ready(), dispatch_preparable_next_level_group(), and dispatch_preparable_next_level_singles() each re-implement the same READY/run_id/worker_type/group/diagnostics/supports_prepare_activate/capacity/ACK checks. Any future change has to be made in three places consistently, and a divergence silently turns the CV predicate into a spurious wakeup source or a missed wakeup. A single bool preparable_slot_eligible(TaskSlot, RunId, RunId) helper used by all three would remove that risk.

Also applies to: 414-476

🤖 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 365 - 402, Extract the
shared eligibility checks from preparable_work_ready(),
dispatch_preparable_next_level_group(), and
dispatch_preparable_next_level_singles() into a
preparable_slot_eligible(TaskSlot, RunId, RunId) helper. Have all three callers
use this helper for the READY, run, worker type, group, diagnostics,
supports_prepare_activate, capacity, and active-run ACK checks, preserving their
existing group/single-specific behavior.
src/common/platform/onboard/host/c_api_shared.cpp (1)

681-688: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

format_prepared_attrs emits duplicated values under different keys.

request=/run= both print identity.run_id, and epoch=/generation= both print identity.generation. Traces will look like they carry four independent identity fields when only two exist — drop the aliases (or populate them from real fields).

♻️ Proposed trim
-        attrs, attrs_size, "request=%llu run=%llu epoch=%llu slot=%u generation=%llu dispatch=%llu",
-        static_cast<unsigned long long>(identity.run_id), static_cast<unsigned long long>(identity.run_id),
-        static_cast<unsigned long long>(identity.generation), identity.slot_id,
-        static_cast<unsigned long long>(identity.generation), static_cast<unsigned long long>(identity.dispatch_id)
+        attrs, attrs_size, "run=%llu slot=%u generation=%llu dispatch=%llu",
+        static_cast<unsigned long long>(identity.run_id), identity.slot_id,
+        static_cast<unsigned long long>(identity.generation), static_cast<unsigned long long>(identity.dispatch_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 `@src/common/platform/onboard/host/c_api_shared.cpp` around lines 681 - 688,
Update format_prepared_attrs to stop emitting duplicate identity values under
request/run and epoch/generation aliases; retain only keys backed by distinct
PreparedRunIdentity fields, while preserving the existing slot and dispatch
attributes.
src/common/hierarchical/worker_manager.h (2)

148-156: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add the symmetric bound assert for the control-frame region.

MAILBOX_OFF_TASK_ARGS_BLOB is now guarded against the frame-protocol trailer, but MAILBOX_OFF_CONTROL_CALLABLE_HASH (+ CALLABLE_HASH_DIGEST_SIZE) has no equivalent check even though the control frame shares these offsets. It fits comfortably today; the assert is what keeps a future CTRL_SHM_NAME_BYTES or digest-size change from silently overwriting the trailer.

🛡️ Proposed assert
 static_assert(
     MAILBOX_OFF_TASK_ARGS_BLOB < MAILBOX_OFF_FRAME_PROTOCOL,
     "mailbox task-args region must precede the frame protocol trailer"
 );
+static_assert(
+    MAILBOX_OFF_CONTROL_CALLABLE_HASH + static_cast<ptrdiff_t>(CALLABLE_HASH_DIGEST_SIZE) <=
+        MAILBOX_OFF_FRAME_PROTOCOL,
+    "mailbox control region must precede the frame protocol trailer"
+);
🤖 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/worker_manager.h` around lines 148 - 156, Add a
static_assert next to the existing task-args bound check that verifies
MAILBOX_OFF_CONTROL_CALLABLE_HASH plus CALLABLE_HASH_DIGEST_SIZE remains before
MAILBOX_OFF_FRAME_PROTOCOL. Keep the existing offset and capacity definitions
unchanged.

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

Declare MAILBOX_TASK_PROTOCOL_VERSION as uint64_t to match its wire slot.

The field occupies the 8-byte MAILBOX_OFF_FRAME_PROTOCOL slot and is unpacked as "=Q" on the Python side, so run_two_frame has to widen it into a local uint64_t protocol before the memcpy. Declaring the constant at wire width documents the slot and drops the conversion step.

🤖 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/worker_manager.h` at line 97, Change
MAILBOX_TASK_PROTOCOL_VERSION from uint32_t to uint64_t so it matches the 8-byte
MAILBOX_OFF_FRAME_PROTOCOL wire slot and the Python "=Q" unpacking; update
run_two_frame to use the widened uint64_t protocol directly in the memcpy
without an intermediate conversion.
python/simpler/worker.py (2)

1564-1569: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

publish_native_acceptance is never passed False.

Both call sites (the single-frame _run_mailbox_loop default and the two-frame handle_task(...) at Line 1975) use True, so the flag and the two conditionals it guards at Lines 1609–1610 are currently unreachable configurability. Dropping it keeps the dispatch path to one shape until a caller actually needs the other.

Also applies to: 1972-1976

🤖 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 1564 - 1569, Remove the unused
publish_native_acceptance parameter from handle_task and delete the conditionals
it controls around the native acceptance publication. Update both handle_task
call sites, including _run_mailbox_loop and the two-frame dispatch, so they use
the simplified signature without passing True.

198-206: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider exporting the frame-trailer offsets from the bindings instead of recomputing them here.

These five offsets, _TASK_ACCEPTED, _TASK_PROTOCOL_VERSION and _MAILBOX_ARGS_CAPACITY are hand-mirrored from worker_manager.h (MAILBOX_OFF_FRAME_*, MAILBOX_TASK_ACCEPTED, MAILBOX_TASK_PROTOCOL_VERSION, MAILBOX_ARGS_CAPACITY). They agree today, but a future C++ layout change would silently produce garbage reads on the child side rather than a build/import error — MAILBOX_FRAME_SIZE was exported precisely to avoid that. Exporting the trailer constants the same way (and asserting them in _assert_bindings_match_source_tree) removes the drift surface.

🤖 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 198 - 206, The worker bindings
currently hand-mirror frame trailer constants, risking drift from the C++
layout. Export MAILBOX_OFF_FRAME_*, MAILBOX_TASK_ACCEPTED,
MAILBOX_TASK_PROTOCOL_VERSION, and MAILBOX_ARGS_CAPACITY from the bindings,
replace the local values used by python/simpler/worker.py, and extend
_assert_bindings_match_source_tree to validate them against worker_manager.h.
src/common/hierarchical/worker_manager.cpp (1)

606-893: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Consider a fail_endpoint(msg) helper to collapse the repeated poison/notify/release/return block.

run_two_frame has thirteen return points, and eight of them repeat the identical five-line sequence: set endpoint_poisoned_, publish_cv_.notify_all(), set outcome/error_message, release_claim(), return completion. Every one of them is load-bearing — dropping release_claim() on a single path permanently strands a frame, and dropping the notify_all() strands every publish-sequence waiter. A single helper (or an RAII guard for the claim, given release_claim is already idempotent) makes that invariant structural rather than something each new early-return has to remember.

🤖 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/worker_manager.cpp` around lines 606 - 893, Refactor
the repeated poisoned-endpoint failure paths in
LocalMailboxEndpoint::run_two_frame by introducing a local fail_endpoint helper
that marks endpoint_poisoned_, notifies publish_cv_, assigns the failure outcome
and message, releases the claimed frame, and returns completion. Replace the
existing matching early-return blocks with this helper while preserving
non-poisoning failures and the catch-path cleanup behavior.
src/common/worker/chip_worker.cpp (1)

252-255: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the function-pointer teardown into one helper.

The prepared-run pointers are now the fourth group duplicated across three rollback blocks and finalize(). Every future ABI symbol has to be added in all of them, and a miss leaves a dangling pointer into a dlclosed SO. A single clear_runtime_bindings() called from each site removes that class of drift.

Also applies to: 302-305, 404-407

🤖 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/worker/chip_worker.cpp` around lines 252 - 255, Extract the
prepared-run function-pointer teardown from finalize() and the rollback blocks
into a shared clear_runtime_bindings() helper. Move all assignments for
supports_prepared_run_fn_, prepare_run_fn_, execute_prepared_fn_, and
abort_prepared_fn_ into that helper, then call it at each existing teardown
site, including the locations around the referenced rollback blocks.
🤖 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 `@python/bindings/task_interface.cpp`:
- Around line 1489-1499: Update the prepare-with-lease binding around
prepare_with_lease so the prepared runtime owns a persistent copy of storage
rather than receiving the stack-local &storage. Ensure that the owned args
remain valid through execute_prepared or abort_prepared, then are released when
the prepared run is consumed or aborted.

In `@python/simpler/worker.py`:
- Around line 1898-1901: Update all four abort_prepared cleanup blocks in the
worker flow to record exceptions instead of silently passing, using the module’s
existing stderr/error-reporting mechanism. Preserve the cleanup attempt and
subsequent fail_frame behavior, while including the abort_prepared exception
details so failed prepared-state reclamation is diagnosable and Ruff S110 is
resolved.
- Around line 1845-1863: Add a short yield to admission_loop after each scan
when no frame is in an actionable state, using time.sleep(0) or the existing
minimal sleep convention. Track whether the scan found a frame in _TASK_READY,
_PREPARE_READY, or _ACTIVATE, and preserve immediate polling when actionable
work is present while preventing idle full-core spinning.
- Around line 1939-1942: Guard the body of the daemon admission_loop so
exceptions from identity/frame parsing or failure reporting are captured as a
terminal action instead of terminating the producer silently. Preserve the
executor’s existing wait predicate and enqueue a shutdown action carrying the
recorded exception cause; when handling that shutdown action, pass the cause to
_write_error before returning.

In `@src/common/hierarchical/orchestrator.cpp`:
- Around line 345-354: Update the failure handling loop in cancel_unstarted_run
so failure_message is written only after successfully transitioning a PENDING or
READY slot to FAILED. Use the slot-state mutex synchronization path established
by slot_state and the corresponding completion/poison handling, ensuring
competing writers and readers cannot race.

In `@src/common/hierarchical/scheduler.cpp`:
- Around line 404-412: Update dispatch_preparable_next_level_group() and
dispatch_preparable_next_level_singles() so each preparable run’s staged-slot
availability is checked per worker before staging. Ensure group selection is
completed or isolated before singles are dispatched, preventing a group and
single from the same preparable FIFO from staging the same run in one
dispatch_ready() pass and preserving dispatch_prepared()’s one-staged-run
assertion.

In `@src/common/platform/onboard/host/device_runner_base.cpp`:
- Line 254: Synchronize all accesses to the single-entry prebuilt arena cache
used by simpler_prepare_run and simpler_execute_prepared, since
selected_arena_bank() is thread-local and the existing shared
g_prepared_run_gate lock does not protect cache mutation. Add and consistently
acquire a mutex around prebuilt_runtime_arena_cache_* reads, rebuilds, and
clears, preserving the existing bank-0 behavior.

In `@src/common/platform/onboard/host/device_runner_base.h`:
- Around line 415-416: Update the documentation for activate_launch_shape to
clearly state that the prepared Runtime’s geometry is latched immediately before
the device launch submit step, replacing the truncated “Device S.” wording.

In `@src/common/worker/chip_worker.cpp`:
- Around line 669-689: Update ChipWorker::abort_prepared to claim the matching
PREPARED slot while holding prepared_slots_mu_ before invoking
abort_prepared_fn_. Transition it to an ABORTING state or clear it atomically
under the lock so concurrent aborts and execute_prepared cannot reuse the slot;
retain the identity validation and ensure the claimed slot is finalized
consistently after the backend call.
- Around line 636-658: Ensure pre-ABI failures in the prepared execution flow
abort the runtime before clearing its slot state. Update the
set_task_accepted_state_fn_ failure path and the select_slot_resources exception
path to invoke abort_prepared_fn_ for the corresponding runtime buffer, while
preserving the existing slot reset and exception propagation behavior; do not
add abort handling to the successful execute_prepared_fn_ path where the ABI
owns runtime destruction.

In `@tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py`:
- Around line 136-139: Update the docstrings in the Python wrapper definitions
around worker stream reuse, specifically in simpler.worker and
bindings.task_interface, to describe flat-stream semantics:
ChipWorker::run_stream_set_create_count advances when the loaded image changes,
while the AICore stream is reused across runs with the same image. Remove
wording that says the counter advances once per run, keeping the behavior and
API unchanged.

In
`@tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py`:
- Around line 113-122: Throttle the observation loop around
saw_active_and_accepted with the suite’s established 1 ms sleep so polling does
not starve dispatch. Make the assertion tolerate run.done occurring before a
sample by preserving success when both frames complete normally, and increase
_CHAIN_LENGTH from 64 to a longer value such as the FIFO suite’s 4096 to widen
the active window for sampling.

---

Nitpick comments:
In `@python/simpler/worker.py`:
- Around line 1564-1569: Remove the unused publish_native_acceptance parameter
from handle_task and delete the conditionals it controls around the native
acceptance publication. Update both handle_task call sites, including
_run_mailbox_loop and the two-frame dispatch, so they use the simplified
signature without passing True.
- Around line 198-206: The worker bindings currently hand-mirror frame trailer
constants, risking drift from the C++ layout. Export MAILBOX_OFF_FRAME_*,
MAILBOX_TASK_ACCEPTED, MAILBOX_TASK_PROTOCOL_VERSION, and MAILBOX_ARGS_CAPACITY
from the bindings, replace the local values used by python/simpler/worker.py,
and extend _assert_bindings_match_source_tree to validate them against
worker_manager.h.

In `@src/common/hierarchical/scheduler.cpp`:
- Around line 365-402: Extract the shared eligibility checks from
preparable_work_ready(), dispatch_preparable_next_level_group(), and
dispatch_preparable_next_level_singles() into a
preparable_slot_eligible(TaskSlot, RunId, RunId) helper. Have all three callers
use this helper for the READY, run, worker type, group, diagnostics,
supports_prepare_activate, capacity, and active-run ACK checks, preserving their
existing group/single-specific behavior.

In `@src/common/hierarchical/worker_manager.cpp`:
- Around line 606-893: Refactor the repeated poisoned-endpoint failure paths in
LocalMailboxEndpoint::run_two_frame by introducing a local fail_endpoint helper
that marks endpoint_poisoned_, notifies publish_cv_, assigns the failure outcome
and message, releases the claimed frame, and returns completion. Replace the
existing matching early-return blocks with this helper while preserving
non-poisoning failures and the catch-path cleanup behavior.

In `@src/common/hierarchical/worker_manager.h`:
- Around line 148-156: Add a static_assert next to the existing task-args bound
check that verifies MAILBOX_OFF_CONTROL_CALLABLE_HASH plus
CALLABLE_HASH_DIGEST_SIZE remains before MAILBOX_OFF_FRAME_PROTOCOL. Keep the
existing offset and capacity definitions unchanged.
- Line 97: Change MAILBOX_TASK_PROTOCOL_VERSION from uint32_t to uint64_t so it
matches the 8-byte MAILBOX_OFF_FRAME_PROTOCOL wire slot and the Python "=Q"
unpacking; update run_two_frame to use the widened uint64_t protocol directly in
the memcpy without an intermediate conversion.

In `@src/common/platform/onboard/host/c_api_shared.cpp`:
- Around line 681-688: Update format_prepared_attrs to stop emitting duplicate
identity values under request/run and epoch/generation aliases; retain only keys
backed by distinct PreparedRunIdentity fields, while preserving the existing
slot and dispatch attributes.

In `@src/common/worker/chip_worker.cpp`:
- Around line 252-255: Extract the prepared-run function-pointer teardown from
finalize() and the rollback blocks into a shared clear_runtime_bindings()
helper. Move all assignments for supports_prepared_run_fn_, prepare_run_fn_,
execute_prepared_fn_, and abort_prepared_fn_ into that helper, then call it at
each existing teardown site, including the locations around the referenced
rollback blocks.

In `@tests/ut/cpp/hierarchical/test_orchestrator.cpp`:
- Around line 752-772: Prevent fatal assertions from destroying still-blocked
std::async futures: in tests/ut/cpp/hierarchical/test_orchestrator.cpp lines
752-772, always consume prepared via its task slot before the fatal third
readiness assertion; in tests/ut/cpp/hierarchical/test_orchestrator.cpp lines
805-817, always consume active before the fatal replacement readiness assertion;
in tests/ut/cpp/hierarchical/test_scheduler.cpp lines 555-566, write TASK_ACTIVE
to unblock the second dispatch before the fatal assertion, or replace it with an
EXPECT_* followed by explicit unblocking.

In `@tests/ut/py/test_callable_identity.py`:
- Line 604: Update the assertion for fake_c_worker.pipeline_depth to compare
against the exported PTO_PIPELINE_MAX_DEPTH constant instead of the hard-coded
literal 2, preserving the existing default-depth validation.
🪄 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: ab67315d-725f-410e-ac84-b0da89488e6d

📥 Commits

Reviewing files that changed from the base of the PR and between 33e5450 and 3a6d649.

📒 Files selected for processing (41)
  • docs/task-flow.md
  • python/bindings/task_interface.cpp
  • python/bindings/worker_bind.h
  • 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/docs/RUNTIME_LOGIC.md
  • src/a2a3/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/include/host/run_stream_slots.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/worker/chip_worker.cpp
  • src/common/worker/chip_worker.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/test_run_stream_reuse.py
  • tests/st/a2a3/host_build_graph/worker_async_endpoint/kernels/orchestration/long_vector_orch.cpp
  • tests/st/a2a3/host_build_graph/worker_async_endpoint/test_worker_async_endpoint.py
  • tests/st/a2a3/host_build_graph/worker_async_fifo/kernels/orchestration/pipelined_vector_orch.cpp
  • tests/st/a2a3/host_build_graph/worker_async_fifo/test_worker_async_fifo.py
  • tests/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/common/test_trb_runtime_temp_buffer.cpp
  • tests/ut/cpp/hierarchical/test_orchestrator.cpp
  • tests/ut/cpp/hierarchical/test_pipeline_contract.cpp
  • tests/ut/cpp/hierarchical/test_run_stream_slots.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/py/test_callable_identity.py
  • tests/ut/py/test_worker/test_host_worker.py

Comment on lines +1489 to +1499
[](ChipWorker &self, int32_t callable_id, uint64_t args_blob_ptr, size_t blob_capacity,
const CallConfig &config, uint32_t pipeline_slot, uint64_t pipeline_generation, uint64_t run_id,
uint64_t dispatch_id) {
nb::gil_scoped_release release;
TaskArgsView view = read_blob(reinterpret_cast<const uint8_t *>(args_blob_ptr), blob_capacity);
ChipStorageTaskArgs storage = view_to_chip_storage(view);
self.prepare_with_lease(
callable_id, &storage, config, PipelineSlotLease{pipeline_slot, 0, pipeline_generation}, run_id,
dispatch_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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'chip_worker.(h|cpp)' src | xargs -r ast-grep outline --items all
rg -nP -C25 '\bprepare_with_lease\s*\(' --type=cpp --type=cc src

Repository: hw-native-sys/simpler

Length of output: 8886


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== chip_worker.h prepare/execute declarations =="
rg -n -C 40 'execute_prepared|prepare_with_lease|PreparedSlot|prepared_slots_' src/common/worker/chip_worker.h src/common/worker/chip_worker.cpp

echo
echo "== task_interface binding slice =="
sed -n '1450,1535p' python/bindings/task_interface.cpp

Repository: hw-native-sys/simpler

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== source files mentioning SimplerPrepareRunFn/simpler_prepare_run =="
rg -n -C 12 'SimplerPrepareRunFn|simpler_prepare_run|prepare_with_lease|PreparedSlot' src python bindings || true

echo
echo "== exact chip_worker.cpp implementation slices =="
sed -n '542,690p' src/common/worker/chip_worker.cpp

Repository: hw-native-sys/simpler

Length of output: 45358


Keep the args lifetime matching the prepared-run contract.

prepare_with_lease is defined to construct a live runtime object bound with these args, persist it until execute_prepared/abort_prepared, then consume/destroy it. Passing this lambda’s stack-local &storage violates that contract and makes the later prepared execution read freed memory; store the args on a heap/runtime-owned object, or otherwise ensure the prepared runtime owns a copy until execute/abort completion.

🤖 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/bindings/task_interface.cpp` around lines 1489 - 1499, Update the
prepare-with-lease binding around prepare_with_lease so the prepared runtime
owns a persistent copy of storage rather than receiving the stack-local
&storage. Ensure that the owned args remain valid through execute_prepared or
abort_prepared, then are released when the prepared run is consumed or aborted.

Comment thread python/simpler/worker.py
Comment on lines +1845 to +1863
def admission_loop() -> None:
nonlocal control_queued
while not stop_admission.is_set():
control_state = _mailbox_load_i32(state_addr)
with action_cv:
if control_state == _SHUTDOWN:
actions.append(("shutdown", -1, None))
action_cv.notify()
return
if control_state == _CONTROL_REQUEST and not control_queued:
control_queued = True
actions.append(("control", -1, None))
action_cv.notify()

for index, (frame_buf, frame_addr) in enumerate(zip(frame_bufs, frame_addrs, strict=True)):
frame_state_addr = frame_addr + _OFF_STATE
frame_state = _mailbox_load_i32(frame_state_addr)
if frame_state not in (_TASK_READY, _PREPARE_READY, _ACTIVATE):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

The admission thread spins with no yield, so the child now burns a full core and contends for the GIL with the executor.

admission_loop polls the control word and both frame state words in a tight while with no sleep/yield. The single-frame loop could justify that as the task's own latency path, but this is now a second Python thread running for the child's entire lifetime alongside the executor thread; every GIL slice it takes delays the executor's Python-level phases (identity reads, _write_error, state stores) between device calls. A very short time.sleep(0)/sleep(1e-5) on an idle scan (no frame in an actionable state) keeps the hot handoff fast while giving back the core.

🤖 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 1845 - 1863, Add a short yield to
admission_loop after each scan when no frame is in an actionable state, using
time.sleep(0) or the existing minimal sleep convention. Track whether the scan
found a frame in _TASK_READY, _PREPARE_READY, or _ACTIVATE, and preserve
immediate polling when actionable work is present while preventing idle
full-core spinning.

Comment thread python/simpler/worker.py
Comment on lines +1898 to +1901
try:
abort_prepared(prepared_identity)
except Exception: # noqa: BLE001
pass

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

Silently swallowed abort_prepared failures leave unreclaimable prepared state with no diagnostic.

The same try/except: pass appears four times (Lines 1898–1901, 1917–1920, 1959–1962, 1983–1986). abort_prepared is the only path that reclaims an unpublished prepared run; if it throws, the device-side epoch leaks and nothing records why — the subsequent fail_frame message describes the identity mismatch, not the failed reclaim. Log the exception (stderr, as the rest of this module does) or fold it into the frame's error message. Flagged by Ruff (S110).

🧰 Tools
🪛 Ruff (0.16.0)

[error] 1900-1901: try-except-pass detected, consider logging the exception

(S110)

🤖 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 1898 - 1901, Update all four
abort_prepared cleanup blocks in the worker flow to record exceptions instead of
silently passing, using the module’s existing stderr/error-reporting mechanism.
Preserve the cleanup attempt and subsequent fail_frame behavior, while including
the abort_prepared exception details so failed prepared-state reclamation is
diagnosable and Ruff S110 is resolved.

Source: Linters/SAST tools

Comment thread python/simpler/worker.py
Comment on lines +1939 to +1942
while True:
with action_cv:
action_cv.wait_for(lambda: bool(actions))
kind, index, identity = actions.pop(0)

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 | ⚡ Quick win

An unhandled exception in admission_loop wedges the child forever.

admission_loop is a daemon thread whose body is not guarded: read_identity, _mailbox_load_i32, fail_frame/_write_error and _PIPELINE_LEASE_FMT.unpack_from can all raise (e.g. a struct.error on a corrupted frame). If it dies, the thread disappears silently and this wait_for(lambda: bool(actions)) blocks with no timeout and no producer left to notify it. The child stays alive, so the parent's check_child_death liveness sampling never trips either, and the dispatch hangs until the whole tree is torn down.

Wrap the loop body so a producer failure becomes a terminal action the executor observes.

🛡️ Proposed fix: surface a producer crash instead of hanging
         def admission_loop() -> None:
             nonlocal control_queued
-            while not stop_admission.is_set():
+            try:
+                admission_scan(),
+            except BaseException as exc:  # noqa: BLE001 -- producer death must not wedge the executor
+                with action_cv:
+                    admission_error.append(_format_exc(f"chip_process dev={device_id} admission", exc))
+                    actions.append(("shutdown", -1, None))
+                    action_cv.notify()

and make the executor's predicate lambda: bool(actions) unchanged while ("shutdown", ...) now also carries the recorded cause into _write_error before returning.

🤖 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 1939 - 1942, Guard the body of the
daemon admission_loop so exceptions from identity/frame parsing or failure
reporting are captured as a terminal action instead of terminating the producer
silently. Preserve the executor’s existing wait predicate and enqueue a shutdown
action carrying the recorded exception cause; when handling that shutdown
action, pass the cause to _write_error before returning.

Comment on lines +345 to +354
for (TaskSlot slot : slots) {
TaskSlotState &state = slot_state(slot);
TaskState current = state.state.load(std::memory_order_acquire);
if (current == TaskState::PENDING || current == TaskState::READY) {
state.failure_message = message;
(void)state.state.compare_exchange_strong(
current, TaskState::FAILED, std::memory_order_release, std::memory_order_acquire
);
}
}

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C6 '\bfailure_message\b' src/common/hierarchical
ast-grep run --pattern 'struct TaskSlotState { $$$ }' --lang cpp src/common/hierarchical

Repository: hw-native-sys/simpler

Length of output: 7926


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant orchestrator sections =="
sed -n '190,370p' src/common/hierarchical/orchestrator.cpp
sed -n '620,715p' src/common/hierarchical/orchestrator.cpp

echo "== task state declaration and state field =="
rg -n -C4 'enum class TaskState|struct TaskSlotState|compare_exchange_strong|task_state|slot_state' src/common/hierarchical/types* src/common/hierarchical/scheduler.cpp

echo "== scheduler state transitions around failure =="
sed -n '220,310p' src/common/hierarchical/scheduler.cpp

Repository: hw-native-sys/simpler

Length of output: 27094


Synchronize failure_message on live task slots.

failure_message is assigned from cancel_unstarted_run() before the CAS and under no slot lock, while Scheduler::on_task_complete() and poison_task() can also write failure_message after transitioning slots to FAILED. These unsynchronized string writes/reads on live task slots can race; assign the message only after winning the transition and publish it through the slot’s established slot-state mutex path.

🤖 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 345 - 354, Update the
failure handling loop in cancel_unstarted_run so failure_message is written only
after successfully transitioning a PENDING or READY slot to FAILED. Use the
slot-state mutex synchronization path established by slot_state and the
corresponding completion/poison handling, ensuring competing writers and readers
cannot race.

Comment on lines +415 to +416
/** Latch a prepared Runtime's geometry immediately before Device S. */
void activate_launch_shape(const Runtime &runtime);

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

Truncated doc comment.

"immediately before Device S." reads as a cut-off sentence; state what is latched and before which step (e.g. "immediately before the device launch submit").

🤖 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 415 -
416, Update the documentation for activate_launch_shape to clearly state that
the prepared Runtime’s geometry is latched immediately before the device launch
submit step, replacing the truncated “Device S.” wording.

Comment on lines +636 to +658
int rc = -1;
if (accepted_state != nullptr && set_task_accepted_state_fn_ != nullptr) {
int bind_rc = set_task_accepted_state_fn_(device_ctx_, accepted_state, accepted_value);
if (bind_rc != 0) {
std::scoped_lock lk(prepared_slots_mu_);
prepared_slots_[lease.slot_id] = PreparedSlot{};
throw std::runtime_error("set_task_accepted_state_ctx failed with code " + std::to_string(bind_rc));
}
}
auto clear_accepted_state = [&]() {
if (accepted_state != nullptr && set_task_accepted_state_fn_ != nullptr) {
(void)set_task_accepted_state_fn_(device_ctx_, nullptr, 0);
}
};
try {
(void)select_slot_resources(lease.slot_id);
rc = execute_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), callable_id, &config, &identity);
} catch (...) {
clear_accepted_state();
std::scoped_lock lk(prepared_slots_mu_);
prepared_slots_[lease.slot_id] = PreparedSlot{};
throw;
}

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 | ⚡ Quick win

Prepared Runtime leaks when execution fails before entering the ABI.

If set_task_accepted_state_fn_ fails (Line 639) or select_slot_resources throws (Line 651), the slot is reset to PreparedSlot{} without calling abort_prepared_fn_. The Runtime that simpler_prepare_run constructed in runtime_bufs_[slot] is still live, but the identity is now gone from prepared_slots_, so neither abort_prepared nor the finalize() sweep can ever reclaim it — its tensor leases and arena state are held until the process exits. Only the execute_prepared_fn_ return path is safe, because the ABI destroys the Runtime itself.

🛡️ Abort the prepared runtime on the pre-ABI failure paths
     int rc = -1;
     if (accepted_state != nullptr && set_task_accepted_state_fn_ != nullptr) {
         int bind_rc = set_task_accepted_state_fn_(device_ctx_, accepted_state, accepted_value);
         if (bind_rc != 0) {
+            (void)abort_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), &identity);
             std::scoped_lock lk(prepared_slots_mu_);
             prepared_slots_[lease.slot_id] = PreparedSlot{};
             throw std::runtime_error("set_task_accepted_state_ctx failed with code " + std::to_string(bind_rc));
         }
     }
@@
+    bool entered_abi = false;
     try {
         (void)select_slot_resources(lease.slot_id);
+        entered_abi = true;
         rc = execute_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), callable_id, &config, &identity);
     } catch (...) {
         clear_accepted_state();
+        if (!entered_abi) {
+            (void)abort_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), &identity);
+        }
         std::scoped_lock lk(prepared_slots_mu_);
         prepared_slots_[lease.slot_id] = PreparedSlot{};
         throw;
     }
📝 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
int rc = -1;
if (accepted_state != nullptr && set_task_accepted_state_fn_ != nullptr) {
int bind_rc = set_task_accepted_state_fn_(device_ctx_, accepted_state, accepted_value);
if (bind_rc != 0) {
std::scoped_lock lk(prepared_slots_mu_);
prepared_slots_[lease.slot_id] = PreparedSlot{};
throw std::runtime_error("set_task_accepted_state_ctx failed with code " + std::to_string(bind_rc));
}
}
auto clear_accepted_state = [&]() {
if (accepted_state != nullptr && set_task_accepted_state_fn_ != nullptr) {
(void)set_task_accepted_state_fn_(device_ctx_, nullptr, 0);
}
};
try {
(void)select_slot_resources(lease.slot_id);
rc = execute_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), callable_id, &config, &identity);
} catch (...) {
clear_accepted_state();
std::scoped_lock lk(prepared_slots_mu_);
prepared_slots_[lease.slot_id] = PreparedSlot{};
throw;
}
int rc = -1;
if (accepted_state != nullptr && set_task_accepted_state_fn_ != nullptr) {
int bind_rc = set_task_accepted_state_fn_(device_ctx_, accepted_state, accepted_value);
if (bind_rc != 0) {
(void)abort_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), &identity);
std::scoped_lock lk(prepared_slots_mu_);
prepared_slots_[lease.slot_id] = PreparedSlot{};
throw std::runtime_error("set_task_accepted_state_ctx failed with code " + std::to_string(bind_rc));
}
}
auto clear_accepted_state = [&]() {
if (accepted_state != nullptr && set_task_accepted_state_fn_ != nullptr) {
(void)set_task_accepted_state_fn_(device_ctx_, nullptr, 0);
}
};
bool entered_abi = false;
try {
(void)select_slot_resources(lease.slot_id);
entered_abi = true;
rc = execute_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), callable_id, &config, &identity);
} catch (...) {
clear_accepted_state();
if (!entered_abi) {
(void)abort_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), &identity);
}
std::scoped_lock lk(prepared_slots_mu_);
prepared_slots_[lease.slot_id] = PreparedSlot{};
throw;
}
🤖 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/worker/chip_worker.cpp` around lines 636 - 658, Ensure pre-ABI
failures in the prepared execution flow abort the runtime before clearing its
slot state. Update the set_task_accepted_state_fn_ failure path and the
select_slot_resources exception path to invoke abort_prepared_fn_ for the
corresponding runtime buffer, while preserving the existing slot reset and
exception propagation behavior; do not add abort handling to the successful
execute_prepared_fn_ path where the ABI owns runtime destruction.

Comment on lines +669 to +689
void ChipWorker::abort_prepared(const PipelineSlotLease &lease, uint64_t run_id, uint64_t dispatch_id) {
const PreparedRunIdentity identity{run_id, lease.generation, dispatch_id, lease.slot_id, 0};
{
std::scoped_lock lk(prepared_slots_mu_);
if (lease.slot_id >= prepared_slots_.size()) return;
const PreparedSlot &slot = prepared_slots_[lease.slot_id];
if (slot.state != PreparedSlotState::PREPARED || slot.identity.run_id != run_id ||
slot.identity.generation != lease.generation || slot.identity.dispatch_id != dispatch_id) {
return;
}
}
(void)select_slot_resources(lease.slot_id);
int rc = abort_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), &identity);
{
std::scoped_lock lk(prepared_slots_mu_);
prepared_slots_[lease.slot_id] = PreparedSlot{};
}
if (rc != 0) {
throw std::runtime_error("abort prepared run failed with code " + std::to_string(rc));
}
}

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 | ⚡ Quick win

abort_prepared releases the lock between the state check and the backend call.

The scoped lock ends at Line 679, then abort_prepared_fn_ runs at Line 681 with the slot still marked PREPARED. Two consequences:

  • Two concurrent aborts (for example the finalize() sweep at Lines 358-378 racing a caller-driven abort for the same identity) both pass the check and both invoke the ABI on the same handle; simpler_abort_prepared runs r->~Runtime() each time — double destruction.
  • execute_prepared can flip the same slot PREPARED → ACTIVE in that window, so the abort destroys the Runtime the executor is about to publish.

Claiming the slot under the lock before dropping it (e.g. an ABORTING state, or setting PreparedSlot{} up front and reconstructing on failure) closes both windows.

🔒️ Claim the slot under the lock
 void ChipWorker::abort_prepared(const PipelineSlotLease &lease, uint64_t run_id, uint64_t dispatch_id) {
     const PreparedRunIdentity identity{run_id, lease.generation, dispatch_id, lease.slot_id, 0};
     {
         std::scoped_lock lk(prepared_slots_mu_);
         if (lease.slot_id >= prepared_slots_.size()) return;
-        const PreparedSlot &slot = prepared_slots_[lease.slot_id];
+        PreparedSlot &slot = prepared_slots_[lease.slot_id];
         if (slot.state != PreparedSlotState::PREPARED || slot.identity.run_id != run_id ||
             slot.identity.generation != lease.generation || slot.identity.dispatch_id != dispatch_id) {
             return;
         }
+        // Claim the slot so no peer abort or execute can reach this handle.
+        slot = PreparedSlot{};
     }
     (void)select_slot_resources(lease.slot_id);
     int rc = abort_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), &identity);
-    {
-        std::scoped_lock lk(prepared_slots_mu_);
-        prepared_slots_[lease.slot_id] = PreparedSlot{};
-    }
     if (rc != 0) {
         throw std::runtime_error("abort prepared run failed with code " + std::to_string(rc));
     }
 }
📝 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
void ChipWorker::abort_prepared(const PipelineSlotLease &lease, uint64_t run_id, uint64_t dispatch_id) {
const PreparedRunIdentity identity{run_id, lease.generation, dispatch_id, lease.slot_id, 0};
{
std::scoped_lock lk(prepared_slots_mu_);
if (lease.slot_id >= prepared_slots_.size()) return;
const PreparedSlot &slot = prepared_slots_[lease.slot_id];
if (slot.state != PreparedSlotState::PREPARED || slot.identity.run_id != run_id ||
slot.identity.generation != lease.generation || slot.identity.dispatch_id != dispatch_id) {
return;
}
}
(void)select_slot_resources(lease.slot_id);
int rc = abort_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), &identity);
{
std::scoped_lock lk(prepared_slots_mu_);
prepared_slots_[lease.slot_id] = PreparedSlot{};
}
if (rc != 0) {
throw std::runtime_error("abort prepared run failed with code " + std::to_string(rc));
}
}
void ChipWorker::abort_prepared(const PipelineSlotLease &lease, uint64_t run_id, uint64_t dispatch_id) {
const PreparedRunIdentity identity{run_id, lease.generation, dispatch_id, lease.slot_id, 0};
{
std::scoped_lock lk(prepared_slots_mu_);
if (lease.slot_id >= prepared_slots_.size()) return;
PreparedSlot &slot = prepared_slots_[lease.slot_id];
if (slot.state != PreparedSlotState::PREPARED || slot.identity.run_id != run_id ||
slot.identity.generation != lease.generation || slot.identity.dispatch_id != dispatch_id) {
return;
}
// Claim the slot so no peer abort or execute can reach this handle.
slot = PreparedSlot{};
}
(void)select_slot_resources(lease.slot_id);
int rc = abort_prepared_fn_(device_ctx_, runtime_bufs_[lease.slot_id].data(), &identity);
if (rc != 0) {
throw std::runtime_error("abort prepared run failed with code " + std::to_string(rc));
}
}
🤖 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/worker/chip_worker.cpp` around lines 669 - 689, Update
ChipWorker::abort_prepared to claim the matching PREPARED slot while holding
prepared_slots_mu_ before invoking abort_prepared_fn_. Transition it to an
ABORTING state or clear it atomically under the lock so concurrent aborts and
execute_prepared cannot reuse the slot; retain the identity validation and
ensure the claimed slot is finalized consistently after the backend call.

Comment on lines +136 to 139
assert st_worker.run_stream_set_create_count == after_first, (
f"{rounds} same-image runs recreated the AICore stream: "
f"{after_first} -> {st_worker.run_stream_set_create_count}"
)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'worker.py' python | xargs rg -n -A10 'def run_stream_set_create_count'
fd -t f 'task_interface.py' python | xargs rg -n -A4 'def run_stream_set_create_count'

Repository: hw-native-sys/simpler

Length of output: 1276


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate files =="
git ls-files | rg '(^|/)(worker\.py|chip_worker\.h|task_interface\.py|test_run_stream_reuse\.py)$'

echo
echo "== Python worker property docstring =="
sed -n '6650,6675p' python/simpler/worker.py

echo
echo "== C++ property declaration/inline definition =="
rg -n -A8 -B3 'run_stream_set_create_count' src/common/worker/chip_worker.h

echo
echo "== Test context =="
sed -n '115,150p' tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py

echo
echo "== Other run_stream_set_create_count references =="
rg -n -C3 'run_stream_set_create_count' python src tests | sed -n '1,220p'

Repository: hw-native-sys/simpler

Length of output: 23607


Update the Python wrapper docstrings to match the flat-stream semantics.

Both python/simpler/worker.py:6660-6666 and python/bindings/task_interface.cpp:1572-1575 still describe the counter as advancing once per run. Update them to match ChipWorker::run_stream_set_create_count/DeviceRunnerBase, which says the AICore stream is reused while the loaded image is unchanged.

🤖 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/run_stream_reuse/test_run_stream_reuse.py`
around lines 136 - 139, Update the docstrings in the Python wrapper definitions
around worker stream reuse, specifically in simpler.worker and
bindings.task_interface, to describe flat-stream semantics:
ChipWorker::run_stream_set_create_count advances when the loaded image changes,
while the AICore stream is reused across runs with the same image. Remove
wording that says the counter advances once per run, keeping the behavior and
API unchanged.

Comment on lines +113 to +122
saw_active_and_accepted = False
deadline = time.monotonic() + 10.0
while not run.done and time.monotonic() < deadline:
states = [_mailbox_load_i32(addr) for addr in frame_state_addrs]
if _TASK_ACTIVE in states and _TASK_ACCEPTED_STATE in states:
saw_active_and_accepted = True
break

run.wait(20.0)
assert saw_active_and_accepted, "frame B was not accepted while frame A was active"

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

Unthrottled poll loop, and the assertion is racy if the run finishes first.

Two things on this observation window:

  • The loop body has no time.sleep, so it spins at full speed (holding the GIL) against the very dispatch it is trying to observe — other polling loops in this suite sleep 1 ms.
  • If both frames drain before a sample lands in the (_TASK_ACTIVE, _TASK_ACCEPTED_STATE) pair, run.done ends the loop and line 122 fails even though the protocol behaved correctly. With _CHAIN_LENGTH = 64 the first task is short, so this is a plausible flake source; consider a longer chain (the FIFO suite uses 4096) so the active window is wide enough to sample reliably.
♻️ Throttle the poll
             while not run.done and time.monotonic() < deadline:
                 states = [_mailbox_load_i32(addr) for addr in frame_state_addrs]
                 if _TASK_ACTIVE in states and _TASK_ACCEPTED_STATE in states:
                     saw_active_and_accepted = True
                     break
+                time.sleep(0.001)
🤖 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_endpoint/test_worker_async_endpoint.py`
around lines 113 - 122, Throttle the observation loop around
saw_active_and_accepted with the suite’s established 1 ms sleep so polling does
not starve dispatch. Make the assertion tolerate run.done occurring before a
sample by preserving success when both frames complete normally, and increase
_CHAIN_LENGTH from 64 to a longer value such as the FIFO suite’s 4096 to widen
the active window for sampling.

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