Feat: add launch-acceptance flight fence - #1467
Conversation
📝 WalkthroughWalkthroughThe change introduces an explicit run-acceptance boundary between dispatch and completion. Acceptance is tracked per run, published by supported device paths, propagated through worker callbacks, exposed through bindings, and used to gate subsequent Python submissions independently of cleanup. ChangesRun acceptance lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant PythonWorker
participant Orchestrator
participant WorkerManager
participant DeviceRunner
PythonWorker->>Orchestrator: submit run and register acceptance obligations
WorkerManager->>DeviceRunner: dispatch task
DeviceRunner->>DeviceRunner: enqueue kernels
DeviceRunner->>WorkerManager: publish TASK_ACCEPTED
WorkerManager->>Orchestrator: mark_task_accepted
PythonWorker->>Orchestrator: wait for run acceptance
Orchestrator-->>PythonWorker: acceptance boundary reached
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
307e6f7 to
99b3674
Compare
99b3674 to
c2e19ed
Compare
|
Rewrote this PR on current Key changes:
Validation for commit
This rewrite intentionally does not add or alter the GitHub simulation workflow; hardware validation remains the evidence for this stage. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/worker-manager.md`:
- Around line 175-181: Update LocalMailboxEndpoint::run(...) and its callers to
accept and forward the acceptance hook consistently with run_with_accept(...).
In the dispatch polling loop, track whether TASK_ACCEPTED has already been
observed and invoke on_accept() only on the first observation per dispatch,
while preserving TASK_DONE as the loop termination condition.
In `@tests/ut/cpp/hierarchical/test_scheduler.cpp`:
- Around line 384-401: In the test around the caller thread and child lifecycle,
replace fatal ASSERT_TRUE/ASSERT_EQ checks executed after creating caller with
non-fatal expectations, ensuring execution always reaches child.complete() and
caller.join(). Preserve the existing timeout and success assertions while
guaranteeing caller is joined even when an expectation fails.
🪄 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: afbe5c14-f8c5-419b-9945-e6d9f510056b
📒 Files selected for processing (23)
docs/orchestrator.mddocs/task-flow.mddocs/worker-manager.mdpython/bindings/task_interface.cpppython/bindings/worker_bind.hpython/simpler/orchestrator.pypython/simpler/worker.pysrc/a2a3/platform/onboard/host/device_runner.cppsrc/common/hierarchical/orchestrator.cppsrc/common/hierarchical/orchestrator.hsrc/common/hierarchical/types.hsrc/common/hierarchical/worker.cppsrc/common/hierarchical/worker_manager.cppsrc/common/hierarchical/worker_manager.hsrc/common/platform/onboard/host/c_api_shared.cppsrc/common/platform/onboard/host/device_runner_base.cppsrc/common/platform/onboard/host/device_runner_base.hsrc/common/worker/chip_worker.cppsrc/common/worker/chip_worker.hsrc/common/worker/pto_runtime_c_api.htests/ut/cpp/hierarchical/test_orchestrator.cpptests/ut/cpp/hierarchical/test_scheduler.cpptests/ut/py/test_worker/test_host_worker.py
c2e19ed to
8023d91
Compare
|
Stack update: this PR is now rebased onto #1539 (A0b: code-image-safe AICore stream generations), matching the intended dependency order A0b -> A1. Combined head: Validation on the combined stack:
The acceptance commit remains a single commit above #1539. Once #1539 merges, this PR contracts to the acceptance-flight change only. |
8023d91 to
78dde41
Compare
Compute a stable identity from each callable's AICore child binaries and function mapping, then carry it through callable registration. Reuse a slot's AICPU stream independently, but recreate its AICore stream whenever the loaded image changes. Extend hardware coverage with alternating add/sub images and retain same-image reuse checks.
78dde41 to
d9d81e5
Compare
Revision: acceptance is a sticky mailbox word, not a MailboxStatePushed The transient-state race is real, and the PR admitted it
FixA dedicated sticky word, carved out of the args region so the error message keeps the tail: static constexpr int32_t MAILBOX_TASK_ACCEPTED = 1;
static constexpr ptrdiff_t MAILBOX_OFF_ACCEPTED = MAILBOX_OFF_ERROR_MSG - 8;
Deterministic test
Mutation-checked by regressing only the production read back to the state word (the mock keeps writing the sticky word, as the real child does): both this test and the pre-existing Doc mismatch
Validation
One process note: the first onboard attempt aborted at collection with Still carried, unchangedThe acceptance fence's lost-wakeup fix (decrementing under |
d9d81e5 to
640a676
Compare
Separate the point at which a run's dispatches have been launched from the point at which they complete, so a later submit() can build its graph while the previous run is still executing on the device. RunState carries pending_accepts: submit_impl raises it by the dispatch's group size, and each endpoint dispatch lowers it once after its launch is accepted. A2A3 onboard publishes acceptance after both the AICore and AICPU launches succeed, and the parent observes it inside the poll it already runs, without releasing mailbox ownership before TASK_DONE. Endpoints with no launch ACK — the remote protocol, A5, sim — keep the conservative fallback: WorkerThread satisfies the fence when the dispatch returns, so it degrades to completion rather than hanging. Acceptance is a sticky mailbox word, not a MailboxState. A state carrying it is lost whenever the child reaches TASK_DONE between two parent polls, which is exactly the short-task case the fence exists to pipeline; the loss is silent and leaves the next submit waiting for completion. The child sets the word before TASK_DONE and the parent clears it only when it publishes the next TASK_READY, and the parent reads it after the state so a TASK_DONE observation implies the word is visible. Acceptance is elected separately from completion on RunHandle. worker.run() is submit().wait(), so a thread parked on the previous handle's completion is the normal case, and queueing the acceptance drain behind that election would make every drain a completion drain. A waiter that races the run's release observes a released run as accepted rather than raising. The fence advances from a WorkerThread, where an escaping exception ends the process, so the accept path is non-throwing throughout: an unknown run or slot is ignored, a count mismatch is recorded as the run's error the way decrement_run_tasks already does, and the post-dispatch fallback cannot propagate out of the thread. It advances even when the dispatch failed, or a waiter would block forever. The decrement runs under completion_mu, the mutex a waiter evaluates its predicate on. Without it the notify can land between that predicate check and the block and be lost, leaving the waiter parked with the fence satisfied. WorkerManager::start takes on_accept without a default: an omitted callback leaves pending_accepts non-zero forever, which is a hang rather than a degraded mode. This is an acceptance bridge, not multi-run admission. There is one mailbox frame, no generation-safe frame reuse and no K=2 slot lease; for a dependent DAG, acceptance can land close to completion.
640a676 to
a89fb11
Compare
All three addressed — pushed
|
Summary
Separate the point at which a run's dispatches have been launched from the point
at which they complete, so a later
submit()can build its graph while theprevious run is still executing on the device.
TASK_DONERunHandle, so a thread parked inwait()does not turn every acceptance drain into a completion drainDependency
Stacked on #1539 (A0b, code-image-safe AICore stream generations). Merge after #1539.
#1466 is merged, so the run-resource work is already in the base. The branch
still carries #1539's commit below the acceptance commit; once #1539 lands this
PR rebases down to the single acceptance change.
Why acceptance is a sticky word, not a
MailboxStateA state word carrying
TASK_ACCEPTEDloses the ACK whenever the child reachesTASK_DONEbetween two parent polls — precisely the short-task case the fenceexists to pipeline. The loss is silent: the fence falls back to completion and
the pipeline engages or not depending on a race.
The child sets
MAILBOX_OFF_ACCEPTEDbeforeTASK_DONE; the parent clears itonly when publishing the next
TASK_READY, and reads it after the state, soobserving
TASK_DONEimplies the word is visible.MailboxState::TASK_ACCEPTEDis gone and
MAILBOX_ARGS_CAPACITYdrops by 8 on both sides of the wire.Correctness properties this carries
WorkerThread, where an escaping exception ends the process. An unknown run or slot is ignored; a count mismatch is recorded as the run's error, the waydecrement_run_tasksalready does; the post-dispatch fallback cannot propagate out of the thread.wait_run_acceptedblocks forever.completion_muworker.run()issubmit().wait(), so a thread parked on the previous handle's completion is the normal case.WorkerManager::starttakeson_acceptwith no defaultpending_acceptsnon-zero forever, which is a hang, not a degraded mode.Scope
An acceptance bridge, not multi-run admission. No dual frames, no generation-safe
frame reuse, no K=2 slot lease, no prepared FIFO. For a dependent DAG, acceptance
can land close to completion.
Validation
Against
cdcbb71c.a2a3onboard, the exact CI command (examples tests/stminus the two SDMA demos, 4 devices undertask-submit): 118 passed, 1 skipped, 0 failed — the mailbox layout moved, so this is the check that mattersMutation-checked, each against its own regression:
TASK_DONEAcceptanceIsReadableAfterTaskDoneAcceptanceFenceNeverThrowsOutOfAWorkerThreadget_run/slot_staterestored on the accept pathAcceptanceWaitOnAReleasedRunReturnsget_runrestored inwait_run_acceptedtest_acceptance_wait_does_not_block_completion_waiter_wait_in_progressTwo gaps stated rather than papered over:
completion_mu) has no regression test. The window is the few instructions between the waiter's predicate check and its block, with the mutex held throughout; 3000 forced attempts never hit it. A test that passes on the defect is worse than none, so it was removed. The fix rests on the structural argument: mutate the state a predicate reads under the mutex that predicate is evaluated on — the disciplinefinish_run_if_readyalready follows.AcceptanceIsReadableAfterTaskDonedoes not force the parent to skip a poll between the child's two writes; the parent is already spinning and nothing in the test can stop it. It pins the property that makes that interleaving harmless. A genuinely forced ordering needs a poll seam inLocalMailboxEndpoint, which was judged a worse trade than naming the test honestly.utwas red on AppleClang only —arithmetic on a pointer to voidin the testmock, which GCC accepts as an extension. Fixed; verified with
-Werror=pointer-arithas a local clang proxy.