Fix: release prepared collectors and keep arming failures safe - #1714
Conversation
📝 WalkthroughWalkthroughThe change makes launch arming exception-safe across onboard and simulated runners. Pre-submission failures return ChangesExecution lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant DeviceRunner
participant Collectors
participant ExecutionStream
participant Worker
DeviceRunner->>Collectors: Start per-run collectors
DeviceRunner->>ExecutionStream: Publish prepared execution
DeviceRunner->>Worker: Submit execution
DeviceRunner-->>DeviceRunner: Return -1 if arming throws
DeviceRunner->>Collectors: Finalize owned collectors during cleanup
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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/a2a3/platform/onboard/host/device_runner.cpp (1)
585-632: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueArming guard is correct; the failure log text is narrower than the guarded region.
The
tryblock coversmark_submitted(slot)at Line 604.mark_submittedpublishes the stream set. If a later statement in the block throws, the catch at Line 629 logs "arming failed before any stream submission", which is inaccurate for that window.The window is small. The statements after
mark_submittedonly readruntime.get_workers()and writeaicore_done. The rollback path still retires the AICore stream throughabandon_prepared_execution, so no resource leaks. Consider movingmark_submittedand theaicore_donereset out of thetryblock, or adjusting the log text, so the message matches the guarded region.🤖 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/a2a3/platform/onboard/host/device_runner.cpp` around lines 585 - 632, Adjust the guarded launch sequence around mark_submitted and the aicore_done reset so the catch message accurately describes all operations it covers. Either move mark_submitted and the subsequent worker-state reset outside the try block, or broaden the catch log in the launch_run path to avoid claiming that no stream submission occurred.
🤖 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 `@src/a5/platform/onboard/host/device_runner.cpp`:
- Around line 341-388: Move the run_poll_slot_ and run_poll_state_ stores out of
the rollbackable arming try block in exact_launch_transaction. Perform them only
after arming completes successfully and immediately before the actual submission
path, ensuring exceptions from dep_gen_collector_.start or core-type allocation
cannot publish a stale slot/state pair.
---
Nitpick comments:
In `@src/a2a3/platform/onboard/host/device_runner.cpp`:
- Around line 585-632: Adjust the guarded launch sequence around mark_submitted
and the aicore_done reset so the catch message accurately describes all
operations it covers. Either move mark_submitted and the subsequent worker-state
reset outside the try block, or broaden the catch log in the launch_run path to
avoid claiming that no stream submission occurred.
🪄 Autofix
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: 4ddd43a5-6940-48d9-9d25-60e68a89878c
📒 Files selected for processing (8)
src/a2a3/platform/onboard/host/device_runner.cppsrc/a2a3/platform/onboard/host/device_runner.hsrc/a2a3/platform/sim/host/device_runner.cppsrc/a5/platform/onboard/host/device_runner.cppsrc/a5/platform/onboard/host/device_runner.hsrc/a5/platform/sim/host/device_runner.cppsrc/common/worker/native_run_execution.htests/ut/cpp/common/test_native_run_execution.cpp
92da7d0 to
b9431df
Compare
A prepared execution initializes its diagnostics collectors during native prepare, but cleanup_execution() only released them when the run had reached the streams. Any run that failed in prepare, was abandoned before launch, or whose launch failed at the AICore submission therefore left collector shared memory, its host registration and its already-started mgmt/poll threads behind, and left is_initialized() set so the next run on the same runner re-initialized a live collector. Release them on every path: an overlapping predecessor can own no collector, because a prepared successor is admitted only when both runs declare no diagnostics, so the release is this run's alone. a5 keeps the flag for the sticky terminal poll state, which only a launched run may publish. The launch transaction also treated any exception from the AICore step as an uncertain partial launch. That step arms the run before it submits — latching launch shape, resetting the device-wall buffer, starting collectors, publishing stream ownership — so an allocation or thread-spawn failure there poisoned the device context and forced a reset, though no stream had been touched. Each submit callback now catches its own arming failures and reports them as a non-zero return, leaving the transaction's exception path to mean what the contract says: the submission itself was attempted and the outcome is uncertain.
An abandoned prepared run releases its diagnostics collectors, but nothing covered that: the existing prepare-then-finalize case runs with every diagnostic off, so no collector is initialized and a retained one is invisible. Drive the same path with scope_stats enabled, twice in a row. A collector that survives the first abandon fails the second prepare, because every collector refuses to initialize while its shared memory is still mapped. The two extra invocations never reach simpler_run.runner_run, so the STRACE expectations move only in their total. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b9431df to
49ee3d3
Compare
Summary
Follow-up to #1700, fixing two defects in the launch seam it introduced.
Prepared collectors were never released unless the run launched. A prepared
execution initializes its diagnostics collectors during native prepare, but
cleanup_execution()gatedfinalize_collectors()on alaunchedflag thatabandon_prepared_execution()and the prepare rollback both pass as false. Threereachable paths leaked as a result:
init_*succeeded;!entered_runabandon after anattach_current_threadfailure);NotStartedafter the AICore closure already started thecollectors —
mark_submittedfailure, orlaunch_aicore_kernelfailure, i.e.the 207001 path this file carries a recovery routine for. Here the mgmt/poll
threads are already running and nothing stops them.
Each leaked the collector's shared memory and its host registration, and left
is_initialized()set — and every collector refuses a secondinitialize()whileits shm is still mapped (
return -1), so a leaked collector makes the nextprepare on that runner fail outright.
The gate was guarding against a state that cannot occur: an overlapping
predecessor can own no collector, because
try_reserve_native_runadmits aprepared successor only when the successor declares no diagnostics and the
predecessor's own
permits_prepared_successorsays the same. With no overlappossible, every
finalize_collectors()branch is alreadyis_initialized()-guardedand is a no-op in exactly the case the gate was protecting. Released on every path
now. a5 keeps the flag for the sticky terminal poll state, which only a launched
run may publish.
An arming failure poisoned the device.
ExactLaunchTransactionclassified anyexception escaping the AICore step as an uncertain partial launch. But that step
arms the run before it submits — latching launch shape, resetting the device-wall
buffer, starting collectors, publishing stream ownership, and on sim the whole
dlsym'd setter block — so a
std::bad_allocfrom thecore_typesvector or astd::system_errorfromcreate_threadmarked the device unusable and forced areset at finalize, with no stream ever touched.
contracts.md: "Failure before thefirst stream submission is
FAILED_SAFE."Each submit callback now catches its own arming failures and returns non-zero, so
the transaction's exception path means what the contract says: the submission
itself was attempted and the outcome is uncertain. The callback contract is stated
on
ExactLaunchTransaction.Testing
Regression barrier for the collector leak —
test_native_run_lifecycle.pygainsa diagnostics-enabled prepare → finalize-without-launch → prepare cycle. The leak is
onboard-only (sim's
abandon_prepared_executionalready released collectivesunconditionally via
cleanup_active_run()), so it was verified on a2a3 silicon bothways:
launchedgate restored byte-for-byte, on devices 3,5 —fails, with exactly the error the leak causes:
The test's STRACE invocation counts move 3→5 (sim) and 7→9 (onboard); the two extra
invocations are the abandoned prepares, which never reach
simpler_run.runner_run,so
launched_countis unchanged.The arming-classification fix ships without a regression test. The
ArmingFailureReportedAsRcStaysSafecase added here documents the callback contract,but it passes against the pre-fix code too — the
if (result.rc != 0) return result;early return predates this change — so it is not a barrier. Guarding it would mean
injecting an allocation or thread-spawn failure into the production closures, which
has no hook today. Flagging rather than implying coverage.
Other evidence, all on the current head:
a2a3simanda5simscene suites exit 0task-submiton free devices, exit 0An earlier onboard run on
--device autoreported three507018s; all were atLoadAicpuOp::BootstrapDispatcherinsidesimpler_init, before any run is preparedor launched, and all on device 1, which
task-submit --listshowed held by anotheruser's live task. Re-running pinned to free devices is the green result above.