Skip to content

Fix A2A3 repeated-run AICore stream regression - #1807

Open
Crane-Liu wants to merge 1 commit into
hw-native-sys:mainfrom
Crane-Liu:codex/fix-1791-aicore-stream-reuse
Open

Fix A2A3 repeated-run AICore stream regression#1807
Crane-Liu wants to merge 1 commit into
hw-native-sys:mainfrom
Crane-Liu:codex/fix-1791-aicore-stream-reuse

Conversation

@Crane-Liu

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

Copy link
Copy Markdown
Contributor

Summary

  • retain proven-complete per-slot A2A3 AICore streams while no new AICore code has been published
  • mark every retained slot stream stale only after a successful AICore H2D upload; content-hash dedup hits do not invalidate streams
  • recreate stale streams on the next acquire, while preserving conservative destruction for unproven runs and failed-destroy retry ownership
  • cover repeated reuse, deduplicated registration, concurrently resident callables, cross-slot A -> B -> A address-recycle publication, retirement gating, and exact stream-creation counts
  • update docs/task-flow.md with the actual content-hash allocation and publication invariant

Fixes #1791

Validation

  • changed-file pre-commit: all hooks passed (clang-format, clang-tidy, cpplint, ruff, pyright, markdownlint)
  • RunStreamSlots C++ unit tests: 19/19 passed with GCC 15 on LCW
  • A2A3 architecture probe: passed on task-submit (task_20260814_024716_37579914368)
  • GitHub CI on a9ec96bb2: 18/18 checks completed with no failures (17 success, expected deploy skip), including ut-a2a3, ut-a5, st-onboard-a2a3, st-onboard-a5, st-pod-onboard-a2a3, both A2A3/A5 simulation matrices, packaging, profiling, and pre-commit

An additional LCW mixed-venv targeted run (task_20260814_025329_67147427068) reached its 600-second pytest session timeout in native_run_lifecycle::test_run before any assertion result. It is not treated as product evidence; the clean GitHub st-onboard-a2a3 run above passed the full suite on this exact head.

The stream-creation assertions validate the mechanism. The issue-specific HCA/SWA latency benchmark pins older PyPTO/PTO-ISA revisions, so no mixed-revision latency number is reported as comparable performance evidence.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46ff8d9f-29dc-48f7-942b-6e5a447bb121

📥 Commits

Reviewing files that changed from the base of the PR and between 50c0660 and a15632e.

📒 Files selected for processing (13)
  • python/bindings/task_interface.cpp
  • python/simpler/worker.py
  • src/a2a3/platform/onboard/host/device_runner.cpp
  • src/a2a3/platform/onboard/host/device_runner.h
  • src/common/platform/include/host/run_stream_slots.h
  • src/common/platform/onboard/host/device_runner_base.cpp
  • src/common/platform/onboard/host/device_runner_base.h
  • src/common/worker/chip_worker.h
  • src/common/worker/pto_runtime_c_api.h
  • 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/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/hierarchical/test_run_stream_slots.cpp

📝 Walkthrough

Walkthrough

The PR makes AICore stream reuse image-aware. Pipeline slots retain completed streams for the same image and recreate them after image transitions. Native preparation now provisions AICPU streams separately and passes callable image hashes through stream acquisition. Tests and documentation reflect the updated lifecycle.

Changes

Image-aware AICore stream reuse

Layer / File(s) Summary
Image identity contract
src/common/platform/onboard/host/device_runner_base.*, src/a2a3/platform/onboard/host/device_runner.h
DeviceRunnerBase exposes callable AICore image hashes. Runner stream acquisition accepts an image hash.
Image-bound slot lifecycle
src/common/platform/include/host/run_stream_slots.h, tests/ut/cpp/hierarchical/test_run_stream_slots.cpp, tests/ut/cpp/CMakeLists.txt
RunStreamSlots provisions AICPU streams separately, reuses matching AICore streams, recreates streams on image changes, and handles retirement and destruction failures. Unit tests cover these states.
Image-aware runner acquisition
src/a2a3/platform/onboard/host/device_runner.cpp, tests/st/a2a3/host_build_graph/native_run_lifecycle/test_native_run_lifecycle.py
Native preparation resolves the active image hash and acquires the corresponding stream set. Drain cleanup preserves proven completion. Lifecycle assertions reflect reuse for the active run and one new stream for its successor.
Reuse behavior coverage and documentation
python/bindings/task_interface.cpp, python/simpler/worker.py, src/common/worker/*, tests/st/a2a3/host_build_graph/run_stream_reuse/test_run_stream_reuse.py
Documentation and integration tests describe and verify same-image reuse, image-transition recreation, independent slot behavior, and stable creation counts.

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

Possibly related PRs

Poem

A rabbit hops where warm streams stay,
Same-image runs reuse them all day.
When new images cross the slot,
Fresh AICore streams take their plot.
Slots remember, tests agree—
“Reuse with care!” says bunny me.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.03% 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 The implementation and tests address issue #1791 through same-image reuse, image-transition recreation, alternating-image correctness, slot independence, and count validation.
Out of Scope Changes check ✅ Passed The code, documentation, and tests remain focused on the A2A3 AICore stream lifecycle regression described in issue #1791.
Title check ✅ Passed The title clearly identifies the A2A3 repeated-run AICore stream regression fix, which matches the primary change.
Description check ✅ Passed The description directly explains stream reuse, image transitions, cleanup behavior, tests, 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.

@ChaoWao ChaoWao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at a15632e3 against merge-base 50c06606. The state machine is clean and the UT coverage around it is genuinely good, so this is not a rewrite request — but I think the reuse key is the wrong one, and picking the right one makes the patch both smaller and safer.

The invalidation trigger has a single chokepoint

I audited every RT_MEMCPY_HOST_TO_DEVICE on the onboard host path:

site writes
device_runner_base.cpp:692 (upload_chip_callable_buffer) AICore child code — the only one
host_regs.cpp:210 MMIO register address table
device_runner_helpers.cpp:76 / :105 runtime args / kernel args
device_runner_base.cpp:146 (copy_to_device) tensor payload
ensure_binaries_loaded AICPU SO dispatcher bootstrap, not AICore code

So the instruction bytes in GM change in exactly one place, and only when the content-hash dedup misses (a dedup hit returns before the rtMemcpy, so no code is written).

That means the question a slot needs to answer is "has any code been published since this stream was created?" — not "which image did this slot run last time?" Those are different predicates, and keying on the second one is wrong in both directions.

Under-invalidates: A -> B -> A across two slots

bound_image is per-slot, but the instruction cache belongs to the cores, which both pipeline slots dispatch to. upload_chip_callable_buffer allocates via mem_alloc_.allocrtMalloc, and unregister_callablerelease_chip_callable_bufferrtFree releases the block, so an address is recyclable:

  1. register A → run on slot 0S0 retained, bound_image = H_A, complete
  2. unregister A → rtFree(X)
  3. register B → rtMalloc returns X → B's children occupy A's old code range
  4. run B on slot 1 → slot 1 creates its own stream → cores fetch B at X
  5. unregister B; register A again → rtMalloc returns X; A's bytes are back at X
  6. run A on slot 0bound_image == H_Astream reused, no fresh-stream flush

Slot 0 never observed step 4 because it happened on the other slot. Under the exact hardware premise #1540 was built on, step 6 can execute B's cached lines. This is the case #1791 warned about in Additional Context:

A plain restoration of the old per-slot image-hash cache may reintroduce stale-instruction risk for an A -> B -> A sequence across depth-two slots.

Over-invalidates: two resident images alternating

When both callables stay registered, they occupy two distinct rtMalloc blocks and no code byte changes between runs — yet every transition destroys and recreates. The PR's own test pins that cost:

# test_alternating_code_images_never_execute_stale_instructions
assert st_worker.run_stream_set_create_count == before + 1   # every run

So an alternating-resident-image workload pays the full #1540 penalty and recovers nothing. Worth checking against real serving traffic, where prefill and decode are separate callables on one worker — the dsv4 HCA/SWA benchmark in #1791 is a single callable in a loop, so it is the one shape this keying does fix.

Suggested shape

One bool per slot, using the vocabulary already in the tree (stale instructions is the wording in this file's own test name and in docs/task-flow.md:293):

// RunStreamSlots::Slot
bool stale{false};

void mark_all_stale() {
    for (Slot &s : slots_) {
        std::lock_guard<std::mutex> lock(s.mutex);
        s.stale = true;
    }
}

int acquire(unsigned slot) {
    ...
    if (s.aicore != nullptr) {
        if (!s.complete) return -1;
        if (!s.stale) return 0;               // no publication since creation: reuse
        rc = destroy_(s.aicore); if (rc != 0) return rc;
        s.aicore = nullptr;
    }
    rc = create_(&s.aicore); ...
    s.stale = false;
}

Hook it after the successful rtMemcpy in upload_chip_callable_buffer (not on the dedup-hit path). run_stream_slots_ lives on the a2a3 DeviceRunner while upload lives on the base, so a default-no-op virtual on the base that a2a3 overrides — the shape abandon_native_run_resources already uses at device_runner_base.h:524.

Two notes on why this needs no extra machinery:

  • In-flight runs need no coordination. A resident callable holds a refcount and the allocator only hands out free blocks, so a publication can never overwrite the code of an executing image. A newly-set stale flag only affects the next acquire.
  • No counter or per-slot snapshot needed. Publication is rare and already does a full H2D plus stream sync, so taking the two slot mutexes is free, and those mutexes are the existing serialization point against acquire/retire/poll. (I'd also avoid the word "generation" here — this repo already uses it for the pipeline lease {slot_id, generation} and the ChipWorker high-water mark.)

This also removes most of the current diff: acquire(slot) keeps its signature and stays in provision_native_run_resources, so the callable_aicore_image_hash() accessor, the ensure_aicpu / ensure_aicpu_locked split, the move of acquire into prepare_execution, and the aicore_image_hash == 0 failure path all become unnecessary.

Other findings

Must fix

  1. docs/task-flow.md:293-301 states the invariant this PR reverses and is not updated"each run creates its own AICore stream ... and no record of which image a stream last ran is load-bearing." After this PR that record is load-bearing. Same-commit fix per .claude/rules/doc-consistency.md §1/§4. While rewriting: the paragraph's claim that "every slot publishes its image to the same GM code address" does not match upload_chip_callable_buffer, which rtMallocs per content_hash with refcounting — simultaneously-registered callables sit at different addresses, and an address is only reused after an unregister frees it.

  2. No test covers the hazard the policy weakens. Both test_alternating_code_images_never_execute_stale_instructions and the new test_depth_two_slots_rebuild_on_independent_image_transitions register add and sub concurrently and only unregister in finally. Two live leases means two distinct blocks, so no code address is ever recycled and neither test can fail from stale instructions regardless of policy. The docstring correction from "at one reused GM code address" to "on one pipeline slot" is honest, but the PR description still lists A/B/A/B as coverage for this risk. The nearest existing coverage, dynamic_register/test_dynamic_register.py::test_unregister_last_handle_allows_reprepare_same_hashid, re-registers identical content and is safe by construction. A test that walks the six steps above would close it.

  3. All five self-hosted checks are redut-a2a3, ut-a5, st-onboard-a2a3, st-onboard-a5, st-pod-onboard-a2a3, each annotated "The job was not acquired by Runner of type self-hosted even after multiple attempts." Infra starvation, not attributable to this diff — but it means no hardware validation ran in CI for a change that relaxes a hardware correctness invariant. Worth a re-run before merge.

  4. No performance evidence. #1791's acceptance list leads with "repeated same-image performance", and the description concedes "I did not report mixed-revision latency numbers as comparable performance evidence." The stream-count assertions prove the mechanism, not that the 3.81–4.36% is recovered. Happy to run the dsv4-flash HCA/SWA A/B if that helps.

Should fix

  1. The reuse gate s.complete is weaker than the aicore == nullptr gate it replaces. poll() sets complete = true from a progress thread on a successful device query, before the executor drains. A second acquire on a slot whose run has not been retired used to be hard-refused; it now succeeds and hands back the in-flight stream. Consider a flag set only by retire_aicore(Complete) and cleared by acquire/mark_submitted.

  2. retire_aicore(Complete) turns a benign no-op into an error. The old code fell through if (s.aicore == nullptr) return 0;. It now returns −1 when the handle is gone, and drain_execution propagates that as the run's error after a successful device drain (reachable in principle via abandon_all() on the device-reset path). Either keep it benign or document why absence must be an error.

  3. aicore_image_hash == 0 is a hard prepare-time failure but is not validated at registration. record_device_orch_callable / record_host_orch_callable check chip_buffer_hash and chip_dev but not this field, so a registration path that omits it registers cleanly and then fails every run with "active callable has no AICore image identity", far from the cause. (Moot if the stale shape is adopted.)

  4. test_depth_two_slots_own_separate_resources gained assertions that cannot failassert after_slot0 in (stream_sets, stream_sets + 1) and assert after_slot1 in (after_slot0, after_slot0 + 1) accept both branches. If slot warmth is order-dependent under the shared st_worker, warm the slots explicitly and assert exact counts.

Consider

  1. The description should surface the goal narrowing: #1791 recommended runner-wide, publication-aware reuse; this implements per-slot content-hash. Even with the above fixed, stating why is worth a line, and a docs/investigations/ entry next to 2026-06-aicore-cold-start-warmup.md would help — the underlying mechanism is still unproven in both directions (#1791: "PMU icache_req/icache_miss data or a stream-only toggle is still needed").

  2. _run_registered and _run_registered_with_lease each inline (base+1)*(base+2), duplicating compute_golden. Parameterizing compute_golden on subtract would avoid the third copy.

Retain proven-complete per-slot AICore streams while no new child code has been uploaded. Mark all slots stale at the sole AICore H2D publication point, recreate stale streams before reuse, and preserve conservative cleanup for unproven runs and destroy failures.\n\nFixes hw-native-sys#1791
@Crane-Liu
Crane-Liu force-pushed the codex/fix-1791-aicore-stream-reuse branch from a15632e to a9ec96b Compare August 14, 2026 10:46
@Crane-Liu

Crane-Liu commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@ChaoWao Addressed in a9ec96bb2; thanks for identifying the publication chokepoint.

The reuse policy is now runner-wide and publication-aware rather than keyed by a per-slot image hash:

  • upload_chip_callable_buffer() calls a default-no-op virtual hook only after a successful AICore H2D copy. The content-hash dedup return remains before the hook.
  • A2A3 overrides the hook with RunStreamSlots::mark_all_stale(). Each slot now has one stale bit; acquire(slot) reuses only a formally retired, non-stale stream and recreates a stale one.
  • Removed the callable image accessor, ensure_aicpu split, prepare-time image lookup, and zero-hash failure path. Acquisition remains in provision_native_run_resources().
  • poll() completion alone cannot enable reuse because submitted remains set until retire_aicore(Complete). Complete retirement without a handle is benign.
  • Updated docs/task-flow.md to describe content-hash allocations, dedup hits, address recycling, and the successful H2D publication invariant.

Coverage now includes repeated no-publication reuse, deduplicated registration without invalidation, resident A/B alternation without recreation, the cross-slot unregister/register A -> B -> A publication hazard, publication during an active run, retire gating, exact depth-two counts, and failed-destroy retry ownership. The golden calculation is shared instead of duplicated.

Validation on the new head: changed-file pre-commit passed all hooks and the RunStreamSlots C++ suite passed 19/19 with GCC 15. A2A3 architecture probe task_20260814_024716_37579914368 passed. GitHub CI completed 18/18 checks with no failures (17 success, expected deploy skip), including A2A3/A5 UT, A2A3/A5 onboard ST, pod A2A3 ST, and both simulation matrices. An additional mixed-venv LCW run task_20260814_025329_67147427068 timed out in native_run_lifecycle::test_run before producing an assertion result, so I am not treating it as product evidence; the clean GitHub A2A3 onboard suite passed this exact head.

I have not reported a mixed-revision latency number as performance evidence. Your offer to run the canonical dsv4-flash HCA/SWA A/B on this head would close that remaining evidence gap without conflating PyPTO/PTO-ISA revisions.

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.

[Performance] A2A3 fresh AICore stream per run regresses repeated HCA/SWA Effective latency by 3.8–4.4%

2 participants