Skip to content

Refactor: give each run its own host tensor accessor - #1695

Merged
ChaoWao merged 2 commits into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-h1-run-owned-orchestration
Aug 6, 2026
Merged

Refactor: give each run its own host tensor accessor#1695
ChaoWao merged 2 commits into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-h1-run-owned-orchestration

Conversation

@Crane-Liu

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

Copy link
Copy Markdown
Contributor

Summary

The host views a run stages lived in two file-scope globals in libhost_runtime.so — a region list and the HostApi pointer serving mirror-mode writes. One instance per process, shared by every worker, so two runs staging tensors overwrite each other's regions. The window was bounded by a hand-paired host_tensor_access_reset(api) / reset(nullptr) plus an RAIIScopeGuard, and the mapping each region needed was registered by the caller but released far away in cleanup.

HostTensorAccessor replaces both globals with one object per run:

  • It owns every mapping it registers and releases them in close(), which the destructor also calls, so no exit path can leak one. Registration reserves both tables before it registers, so a throwing push_back cannot strand a mapping it has no record of.
  • register_device_memory_to_host moves inside add(), which takes the staging buffer as the fallback view. Register and unregister are now balanced per run — previously a run that bound more than once per validate leaked the extra mappings.
  • A null HostApi admits no region, so a mirrored write can no longer reach a null copy_to_device and write() needs no second check.

The runtime carries the accessor in a field past its first two — the only ones the orchestration .so's partial PTO2Runtime definition can see. get_tensor_data / set_tensor_data read it from there, so the ops table and every author-facing signature are unchanged and no kernel needs editing.

run_host_orchestration no longer binds the host library's own copy of framework_current_runtime. Nothing outside the orchestration .so includes pto_orchestration_api.h, so nothing read it; rt_scope_* and rt_orchestration_done take the runtime as an argument. The .so's own copy is still resolved and bound — its inline rt_submit_* read that one.

Scope change from the previous revision

This PR previously also threaded an explicit OrchestrationContext parameter through every orchestration API (194 files, +2676 −2217). That half is not in this revision. Three findings, each checked against the code rather than reasoned about:

  1. The host-side bind was dead. Every reader of current_runtime() is in pto_orchestration_api.h, and no source under host/, aicpu/, runtime/, runtime/orchestrator_core/ or runtime/shared/ #includes it — the only two greps that hit are comments:

    $ git grep -ln "pto_orchestration_api.h" main -- 'src/**'
      .../orchestration/{common.cpp,pto_arg_with_deps.h,pto_orchestration_api.h}
      .../runtime/{common.h,pto_runtime2.h}      # comments, not #include
    
  2. The .so-side global is per-callable, not per-process. register_callable_impl creates a unique temp file per callable and dlopens it RTLD_LOCAL; TMR keys orch_so_table_[callable_id]. Two workers get two .so mappings and two independent globals.

  3. Pipelining does not produce concurrent orchestration. In [orch N] → [device N] overlapped with [orch N+1], orch N has already returned before orch N+1 starts — orch being shorter than the device phase is exactly what keeps the write/read serialized. TMR runs several orchestrator threads, but the comment on that code records that "All orchestrator threads bind the same rt value".

So the global region table was the only state a concurrent run could actually corrupt, and it is what this PR fixes. Separately, pto_orchestration_api.h is a Tier C external contract under codestyle.md rule 10pypto's orchestration codegen (src/codegen/orchestration/orchestration_codegen.cpp) emits rt_submit_aic_task / PTO2_SCOPE() / alloc_tensors( against it, and five collectives templates #include it directly — so changing it needs a coordinated cross-repo change, not a unilateral sweep. If explicit context is wanted later (the concrete trigger would be W1c "direct depth two" genuinely putting two runs in prepare at once), rule 10's incremental doctrine applies: additive overloads plus the pypto change, not one sweep.

Deliberately left out

  • The AICPU executors' framework_bind_runtime calls (HBG teardown, TMR bind + teardown) are dead by the same argument, but they live in a different binary and a different file from this change. Separate cleanup.
  • Pre-existing doc drift found while checking: SUBMIT_BY_CLUSTER.md documents rt_submit_task(PTO2Runtime*, Arg*, int32_t), and the TMR RUNTIME_LOGIC.md export section documents aicpu_orchestration_entry(uint64_t*, int). Both are wrong on main today and unrelated to this change, so they are not fixed here.

Testing

  • C++ no-hardware UT: 86 passed on a clean rebuild. The host-tensor-access suite is 12 tests, up from 9.
  • All 105 orchestration sources parse unmodified (g++ -fsyntax-only against each runtime's build_config.py include set) — direct evidence the author-facing API is untouched.
  • clang-format clean; examples/ and tests/st/ have zero changed files.
  • Not run here: scene tests, simulation, hardware.

New coverage

test_hbg_tensor_access.cpp gains the cases the old global-based tests could not express:

  • a null api admits no region — the invariant that lets write() skip re-checking the copy hook;
  • two regions in one accessor each resolve independently, which is the production shape (a run stages several tensors) and the only case that walks past the first table entry;
  • a span starting before a region base, alongside the existing overrun case;
  • an unregistered write fails and issues no device copy;
  • two concurrent runs each stage, read and close their own accessor over overlapping device addresses, so a reintroduced shared region table surfaces as a wrong value rather than passing silently.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 57437baa-1367-454e-9724-03a05df22335

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change replaces global host tensor mappings with per-run HostTensorAccessor instances. Runtime orchestration and tensor reads/writes now use the accessor context. Cleanup unregisters only mappings owned by the current run.

Changes

Host tensor access

Layer / File(s) Summary
Accessor contract
src/a2a3/runtime/host_build_graph/runtime/host_tensor_access.h, src/a5/runtime/host_build_graph/runtime/host_tensor_access.h, src/*/runtime/pto_runtime2.h
Adds the non-copyable HostTensorAccessor API and stores its pointer in PTO2Runtime. Removes global registration APIs.
Accessor storage and lifecycle
src/a2a3/runtime/host_build_graph/host/host_tensor_access.cpp, src/a5/runtime/host_build_graph/host/host_tensor_access.cpp
Stores regions and mappings per accessor. Supports fallback views, mirrored writes, null checks, and idempotent cleanup.
Orchestration and tensor access integration
src/*/runtime/host_build_graph/host/runtime_maker.cpp, src/*/runtime/host_build_graph/runtime/orchestrator_core/pto_runtime2.cpp
Creates and registers a per-run accessor, passes it into orchestration and tensor operations, and closes it after execution.
Isolation, cleanup, and runtime documentation
tests/ut/cpp/a2a3/test_hbg_tensor_access.cpp, src/*/runtime/host_build_graph/docs/RUNTIME_LOGIC.md
Tests isolated and concurrent access, registration failures, copy failures, and cleanup. Updates runtime ownership documentation.

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

Possibly related PRs

Poem

A rabbit saw mappings once shared in a queue,
Now each run has an accessor to view.
It stages every tensor, then cleans with care,
While concurrent runs keep their regions fair.
Hop, hop—the runtime is tidy and bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main implemented change: replacing shared host tensor state with a run-owned host tensor accessor.
Description check ✅ Passed The description directly explains the host tensor accessor refactor, its motivation, scope, testing, and deliberate exclusions.

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.

@Crane-Liu
Crane-Liu force-pushed the codex/worker-async-h1-run-owned-orchestration branch 2 times, most recently from ac437d2 to ba69d43 Compare August 5, 2026 08:40
@Crane-Liu Crane-Liu changed the title Refactor: make HBG orchestration context explicit Refactor: pass orchestration context explicitly Aug 5, 2026
@ChaoWao
ChaoWao force-pushed the codex/worker-async-h1-run-owned-orchestration branch from de9d197 to 1434cb3 Compare August 5, 2026 13:06
@ChaoWao

ChaoWao commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Status of this branch

I rebased this onto current main and pushed to 1434cb32. It had gone CONFLICTING because #1444 (Graph Execution) and #1450 (A5 TMR scenes) landed after this forked. Resolving that needed more than a mechanical rebase:

  • Add Graph Execution to host_build_graph #1444 added a whole graph-execution API (rt_graph_begin/end/commit, rt_submit_graph, GraphFunction) still resolving the runtime from current_runtime(), so it needed migrating too — including making GraphFunction/GraphFunctionWithConfig carry the context, so a Graph body reaches the runtime the same way its caller does.
  • 12 orchestration files that landed after the base were still on the old shape (4 graph_execution tests + 8 A5 TMR scenes). All migrated.
  • I also fixed the review findings: the four orchestration/common.cpp comments (two still named the deleted current_runtime(), two had a broken mid-sentence substitution), contract comments on OrchestrationContext/HostTensorAccessor, restored 3 dropped UT cases + added null-api coverage, and 27 documents that were teaching signatures that no longer compile.

Everything is squashed into one commit with Co-authored-by: you. Sorry for the force-push over your branch — happy to hand it back in whatever shape you prefer.

CI so far: green. pre-commit, ut, ut-a2a3, st-sim-a2a3sim, st-sim-a5sim, st-onboard-a2a3 (real hardware), packaging, profiling-flags-smoke all pass. macOS lanes, st-onboard-a5 and ut-a5 still running. So the migration itself is not broken — sim compiles every one of the 130 orchestration sources.


A scoping question worth settling before merge

The change bundles two independent things, and I think only one of them has to be in it:

What Author-visible?
(a) process-global region table → run-owned HostTensorAccessor no
(b) current_runtime() → explicit leading OrchestrationContext parameter on every API yes — 119 files

(a) is unambiguously right and I'd merge it as-is. g_regions / g_host_api live in libhost_runtime.so, one instance per process, shared by every worker. Two workers staging tensors genuinely do overwrite each other's regions. The RAII accessor with self-owned mappings and reserve-before-register is the correct fix.

(b) I could not find a buyer for. Three things I checked:

1. The host-side bind was dead code. Every reader of current_runtime() is in pto_orchestration_api.h, and that header is included only by orchestration .so sources — nothing in the host library, AICPU, or AICore binary reads it:

$ git grep -ln "pto_orchestration_api.h" main -- 'src/**'
  .../orchestration/{common.cpp,pto_arg_with_deps.h,pto_orchestration_api.h}
  .../runtime/{common.h,pto_runtime2.h}          # comments only

So runtime_maker.cpp:511's framework_bind_runtime(rt) wrote a global nobody read. The comment at :351 half-admits it — it says the .so's copy is "the .so-private g_current_runtime its inline rt_submit_* reads". That bind, its bind(nullptr) teardown, and the dlsym("framework_bind_runtime") can all be deleted without (b).

2. The .so-side global is per-callable, not per-process. register_callable_impl creates a unique temp file per callable and dlopens it RTLD_LOCAL; TMR keys orch_so_table_[callable_id]. Two workers get two .so mappings and two independent globals.

3. Pipelining does not produce concurrent orchestration. In [orch N] → [device N] overlapped with [orch N+1], orch N has already returned before orch N+1 starts — the write/read of the global is serialized, and orch being shorter than the device phase is exactly what keeps it that way. TMR does run several orchestrator threads, but the deleted comment records that "All orchestrator threads bind the same rt value" — they write the same value.

So the concurrency property (b) buys has no user I could find, today or under the pipelined design. What's left is readability — real, but the price is 119 files of author-visible churn.


The part that I think blocks (b) regardless

pto_orchestration_api.h is a Tier C external contract under codestyle.md rule 10"Ask the user before touching one; land it only with a compatibility alias or a coordinated cross-repo change."

pypto has a ~4000-line orchestration code generator emitting against this exact API:

pypto/src/codegen/orchestration/orchestration_codegen.cpp
  :267   func = core_type == CUBE ? "rt_submit_aic_task" : "rt_submit_aiv_task";
  :1209  code_ << "PTO2_SCOPE() {\n";
  :1270  code_ << "PTO2_SCOPE(PTO2ScopeMode::MANUAL) {\n";
  :2814  code_ << "TaskOutputTensors " << alloc_var << " = alloc_tensors(";

plus five hand-written collectives templates (allreduce/allgather/broadcast/barrier/reduce_scatter) that #include "pto_orchestration_api.h" and call rt_submit_aiv_task(0, params).

Those templates still use L2TaskArgs/Tensor, so this API line already has one uncoordinated break in flight from #1681. (b) would add a second.

And the entry ABI break is the silent one. aicpu_orchestration_entry goes from void(const ChipTaskArgs&) to void(const OrchestrationContext&, const ChipTaskArgs&), but the runtime dlsyms a user-supplied func_name and reinterpret_casts it — a stale out-of-tree .so still resolves and gets called with a shifted argument list. Every other part of (b) fails loudly at compile time; this one does not.


Proposal

Split (b) out and merge (a) alone:

  1. HostTensorAccessor as a per-run RAII object (as here).
  2. Delete the dead host-side bind / bind(nullptr) / dlsym("framework_bind_runtime") — free, and it simplifies the .so's export contract to just aicpu_orchestration_entry + aicpu_orchestration_config.
  3. Hang the accessor off the host variant's PTO2Runtime so the inline wrappers reach it internally — ops table and author-facing signatures unchanged.
  4. Revert the 119 examples/ + tests/st/ files.

That's ~400 lines in src/, reviewable in one pass, no cross-repo coordination, and the dlsym hazard disappears because the entry ABI stops changing.

If (b) is wanted later — the concrete trigger would be W1c "direct depth two" genuinely putting two runs in prepare at once — the migration should be additive overloads per rule 10's incremental doctrine, landed together with the pypto codegen change and a retirement condition for the implicit overload.

@Crane-Liu happy to do the split myself, or hand the branch back — your call. Either way I'd like to record the three findings above in docs/investigations/ so the next person doesn't re-derive them.

The host views a run stages lived in two file-scope globals in
libhost_runtime.so — a region list and the HostApi pointer serving
mirror-mode writes. One instance per process, shared by every worker, so
two runs staging tensors overwrite each other's regions. The window was
bounded by a hand-paired host_tensor_access_reset(api) /
reset(nullptr) plus an RAIIScopeGuard, and the mapping each region needed
was registered by the caller but released far away in cleanup.

HostTensorAccessor replaces both globals with one object per run:

- It owns every mapping it registers and releases them in close(), which
  the destructor also calls, so no exit path can leak one. Registration
  reserves both tables before it registers, so a throwing push_back
  cannot strand a mapping it has no record of.
- register_device_memory_to_host moves inside add(), which takes the
  staging buffer as the fallback view. Register and unregister are now
  balanced per run; previously a run that bound more than once per
  validate leaked the extra mappings.
- A null HostApi admits no region, so a mirrored write can no longer
  reach a null copy_to_device and write() needs no second check.

The runtime carries the accessor in a field past its first two, which are
the only ones the orchestration .so's partial PTO2Runtime definition can
see. get_tensor_data / set_tensor_data read it from there, so the ops
table and every author-facing signature are unchanged and no kernel needs
editing.

run_host_orchestration no longer binds the host library's own copy of
framework_current_runtime. Nothing outside the orchestration .so includes
pto_orchestration_api.h, so nothing read it; rt_scope_* and
rt_orchestration_done take the runtime as an argument. The .so's own copy
is still resolved and bound — its inline rt_submit_* read that one.

Tests cover null-api rejection, two regions resolving independently
within one accessor, a span starting before a region base, an
unregistered write issuing no device copy, and two concurrent runs each
staging, reading and closing their own accessor over overlapping device
addresses.

Co-authored-by: Crane-Liu <c.wliu@outlook.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ChaoWao
ChaoWao force-pushed the codex/worker-async-h1-run-owned-orchestration branch from 1434cb3 to ae36953 Compare August 5, 2026 14:41
@ChaoWao ChaoWao changed the title Refactor: pass orchestration context explicitly Refactor: give each run its own host tensor accessor Aug 5, 2026
@ChaoWao

ChaoWao commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

CI triage on ae36953b

Five red checks. I read every log; none is caused by this diff, and the argument for each is below rather than asserted.

What this revision changes: 13 files, all under src/{a2a3,a5}/runtime/host_build_graph/ plus one C++ UT. Zero Python, zero tensormap_and_ringbuffer, zero examples/, zero tests/st/.

1–2. ut (ubuntu + macOS) — pre-existing break on main

Both fail on the same three tests in tests/ut/py/test_worker/test_remote_zero_residual.py:

RuntimeError: remote L3 session startup failed for worker 0:
ValueError: only host_tcp transport is accepted by simpler-remote-worker

The guard raising it is python/simpler/remote_l3_worker.py:68, and git log -S puts its introduction in #1688 (e78d8437). The tests it breaks were added earlier by #1692 (c866c827) and pass transport="sim" (line 160). git merge-base --is-ancestor c866c827 e78d8437 confirms the ordering: the tests landed first, then the guard that rejects them.

Both commits are on main and neither is in this PR's range. I reproduced the identical three failures locally at this branch's HEAD, and the failing path is pure Python that this diff does not touch.

main is red on ut for both OSes right now — worth a separate fix; either the guard should accept sim, or those three tests should be updated.

3–4. st-sim-a2a3sim / st-sim-a5sim (macOS only) — job timeouts

Both ended in ##[error]Process completed with exit code 124 — a timeout, not an assertion. The ubuntu counterparts of both jobs pass (st-sim-a2a3sim 10m28s, st-sim-a5sim 9m47s), and sim is exactly the lane that compiles every orchestration source, so a real break here would surface on ubuntu too.

5. st-pod-onboard-a2a3 — a tensormap_and_ringbuffer example

##[error]pod examples failed: vector_add_mixed_l3, from a device-side poll_native_run failed with code -1 / finalize_native_run failed with code -100. examples/workers/l4/vector_add_mixed_l3/main.py:231 defaults to --runtime tensormap_and_ringbuffer, which this diff leaves untouched. This job is also new — added by the same #1688.

Green, and relevant

st-onboard-a2a3 (8m39s) and st-onboard-a5 (13m26s) both pass on real hardware, ut-a2a3 passes, both ubuntu sim lanes pass, plus packaging (ubuntu), profiling-flags-smoke, pre-commit and detect-changes. st-onboard-a2a3 is the job that actually exercises the HBG host-orchestration path this PR changes.

Local

  • C++ UT: 86 passed on a clean rebuild; the host-tensor-access suite is 12 tests.
  • Python UT at this HEAD: 1139 passed, 3 failed — the same three test_remote_zero_residual.py cases described above.
  • All 105 orchestration sources parse unmodified.

@ChaoWao
ChaoWao merged commit 05b6a94 into hw-native-sys:main Aug 6, 2026
17 of 19 checks passed
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.

2 participants