Skip to content

Refactor: bind platform host callbacks per run instead of per thread - #1685

Open
ChaoWao wants to merge 1 commit into
hw-native-sys:mainfrom
ChaoWao:refactor/host-api-per-run
Open

Refactor: bind platform host callbacks per run instead of per thread#1685
ChaoWao wants to merge 1 commit into
hw-native-sys:mainfrom
ChaoWao:refactor/host-api-per-run

Conversation

@ChaoWao

@ChaoWao ChaoWao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Splits the platform HostApi from a process-global table of context-free
function pointers into an immutable function table (HostApiFunctions) plus a
small per-run value object (HostApi) that binds that table to one runner and
one run's pipeline slot / arena bank. Carries the run's resource selection and
trace identity across the C ABI in one required NativeRunDescriptor instead
of four TLS setter exports, and deletes the pthread-TLS resource-selection
mechanism (NativeRunThreadSelection, both pthread keys, capture/restore,
try_run_selection) that the global table made necessary.

This is PR-A of the three-way split of #1650 ("Add: bounded asynchronous
native run lane"). PR-B (the direct-L2 two-slot async lane in worker.py /
the binding) and PR-C (the HostTensorAccessScope RAII conversion) follow;
both depend only on HostApi existing, not on each other. Implemented
independently of #1650's branch; comment there links this PR so it can rebase
the remainder onto it.

Representation change only — no run behaviour moves. simpler_init keeps its
flat argument list.

What changes

  • HostApiHostApiFunctions (one static const table per backend, every
    function takes void *runner_ctx plus an explicit pipeline_slot /
    arena_bank where it indexes per-run storage) + HostApi value object
    {runner_ctx, pipeline_slot, arena_bank, functions} whose member functions
    forward to the bound table. Constructed per run, stored in NativeRunState,
    passed by const HostApi * into the runtime impls.
  • NativeRunDescriptor {pipeline_slot, arena_bank, run_id, generation, dispatch_id, run_epoch} — copied into prepared state. pipeline_slot /
    arena_bank are load-bearing (< PTO_PIPELINE_MAX_DEPTH); the rest is
    diagnostic identity, zero for a synchronous depth-one run. simpler_prepare_run
    / simpler_run take a required const NativeRunDescriptor *.
  • Launch acceptance moves from a TLS setter into the per-run launch signal:
    NativeRunLaunchSignal gains configure_acceptance / publish_acceptance;
    simpler_launch_run / simpler_run take accepted_state / accepted_value
    and the runner publishes at the real kernel-launch marker.
  • Deleted: NativeRunThreadSelection, g_runner_key, g_run_selection_key,
    try_run_selection, run_selection, capture_/restore_native_run_thread_selection,
    the two RAII selection guards, and the four setter exports
    (select_pipeline_slot_ctx, select_arena_bank_ctx,
    set_native_run_identity_ctx, set_task_accepted_state_ctx).
  • host_tensor_access_reset takes const HostApi * instead of a raw
    copy_to_device pointer (a minimal bridge; the full HostTensorAccessScope
    RAII conversion is PR-C).
  • PTO_PIPELINE_CONTRACT_ABI_VERSION bumped 1 → 2 (see ABI note).

Per-thread state: why deleting TLS is safe while the per-run executor thread still exists

The plan orders this as B6c ("parameterize selection") after B6a/B6b ("split
run()", "delete the executor thread"), whose stated motivation is "after B6b
there is no second thread". This PR does B6c before B6b, so it removes the
TLS isolation while two threads (the prepare/finalize caller and the per-run
executor) still touch the runner. It is safe because HostApi is a by-value
object, one per run, never shared — and the only runner state that crosses the
prepare/execute boundary is already synchronized or disjoint by index:

  • Per-run indexed storage (retained_temp_addrs_[slot],
    arena_banks_[bank], a2a3 run_stream_slots_[slot]): each run's HostApi
    binds its own slot/bank, and try_reserve_native_run rejects any second
    reservation that reuses a slot or bank already held, so two concurrent runs
    never address the same index. The executor reads its run's pipeline_slot
    from the run() parameter and its HostApi from state->host_api — neither
    is shared across runs.
  • active_native_run_: a single atomic CAS slot — only one run executes at
    a time (onboard std::atomic under native_run_mu_; sim std::atomic).
  • block_dim_ / worker_count_: latched by activate_launch_shape() on
    the executor thread immediately before run() uses them on that same
    thread. prepare_launch_shape() writes only the Runtime object
    (runtime.set_worker_count), never these runner members — so Add: overlap HBG successor preparation with active execution #1587's fix
    (two concurrent prepares used to race on block_dim_) still holds unchanged:
    prepares do not write them.
  • device_unusable_: std::atomic<bool> on a2a3 (acquire/release); a5
    uses a plain bool (pre-existing, unchanged by this PR).
  • native_launch_signal_: stable for the whole of run()
    try_acquire_native_run sets it before the executor thread is spawned
    (happens-before), and release_native_run clears it only after finalize
    joins the executor.

So deleting TLS changes how a callback finds its runner/slot/bank, not what
it accesses. If B6a/B6b were found necessary first, it would be because of one
of the items above, none of which this PR weakens.

ABI-version answer

get_pipeline_contract's abi_version field is checked at load time
(ChipWorker::init runs is_valid_pipeline_contract right after dlopen +
dlsym, before any run; rejection is pinned by
tests/ut/cpp/hierarchical/test_pipeline_contract.cpp). Because consumer
(ChipWorker) and producer (host_runtime.so) share the
PTO_PIPELINE_CONTRACT_ABI_VERSION constant via the same header, a signature
change does not auto-bump it — so this PR bumps it 1 → 2 in the same commit.
With the bump, a stale host_runtime.so (compiled with version 1) is rejected
cleanly at load ("host runtime returned a PipelineContract this build cannot
accept") rather than crashing at the first simpler_run call. Without the
bump, the simpler_run/prepare_run signature drift would not be caught at
load (dlsym does not validate C signatures). The four deleted setters are
caught in the inverse direction (old ChipWorker vs new .so) by the
load_symbol "dlsym failed for '...'" error; this PR's new ChipWorker no
longer asks for them.

Test changes (forced, assertion-neutral)

HostApi changing from an aggregate of function pointers to a value object
forces fixture updates in unit tests that construct it directly — no test
assertion (no expected count, stream provisioning number, or behaviour)
changes:

  • tests/ut/py/test_host_runtime_abi.py: drops the two deleted setter names
    from the required-export set (the allowed exception — the symbols are gone).
  • tests/ut/cpp/a2a3/test_hbg_tensor_access.cpp,
    tests/ut/cpp/common/test_trb_runtime_temp_buffer.cpp: rebuild their fake
    HostApi/HostApiFunctions fixtures against the new shape (fakes gain the
    runner_ctx / slot / bank parameters; bodies and all expectations unchanged).
  • tests/st/.../test_l3_launch_acceptance.py: module docstring no longer names
    the deleted setter (comment-only).

The two scene tests that would catch a slot/bank mix-up
(native_run_lifecycle, run_stream_reuse) pass unchanged.

Validation

  • All four sim libhost_runtime.so rebuilt (2 arch × 2 runtime); the box has
    no onboard cross-compiler, so onboard DSOs build in CI / on a build host.
  • nm -D on every built libhost_runtime.so: the four setter symbols are
    absent.
  • pyut: 1067 passed, 14 skipped.
  • cpput (no-hardware): 76/76 passed.
  • a2a3sim / a5sim: see PR checks.
  • a2a3 onboard (native_run_lifecycle, run_stream_reuse, worker_async_endpoint,
    worker_async_fifo): see PR checks. (a5 onboard is run by CI's st-onboard-a5;
    the a5 machine was unavailable locally.)

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@ChaoWao, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4447f660-796a-4148-bdfa-17e0f965a315

📥 Commits

Reviewing files that changed from the base of the PR and between b714174 and 2b6aabe.

📒 Files selected for processing (35)
  • docs/chip-level-arch.md
  • docs/dynamic-linking.md
  • src/a2a3/platform/onboard/host/device_runner.cpp
  • src/a2a3/platform/onboard/host/device_runner.h
  • src/a2a3/platform/sim/host/device_runner.cpp
  • src/a2a3/platform/sim/host/device_runner.h
  • src/a2a3/runtime/host_build_graph/host/host_tensor_access.cpp
  • src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp
  • src/a2a3/runtime/host_build_graph/runtime/host_tensor_access.h
  • src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • src/a5/platform/onboard/host/device_runner.cpp
  • src/a5/platform/onboard/host/device_runner.h
  • src/a5/platform/sim/host/device_runner.cpp
  • src/a5/platform/sim/host/device_runner.h
  • src/a5/runtime/host_build_graph/host/host_tensor_access.cpp
  • src/a5/runtime/host_build_graph/host/runtime_maker.cpp
  • src/a5/runtime/host_build_graph/runtime/host_tensor_access.h
  • src/a5/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • src/common/platform/include/common/host_api.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/platform/sim/host/c_api_shared.cpp
  • src/common/platform/sim/host/device_runner_base.cpp
  • src/common/platform/sim/host/device_runner_base.h
  • src/common/task_interface/prepare_callable_common.h
  • src/common/worker/chip_worker.cpp
  • src/common/worker/chip_worker.h
  • src/common/worker/native_run_launch_signal.h
  • src/common/worker/native_run_state.h
  • src/common/worker/pto_runtime_c_api.h
  • tests/st/a2a3/tensormap_and_ringbuffer/test_l3_launch_acceptance.py
  • tests/ut/cpp/a2a3/test_hbg_tensor_access.cpp
  • tests/ut/cpp/common/test_trb_runtime_temp_buffer.cpp
  • tests/ut/py/test_host_runtime_abi.py

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.

Split the platform HostApi into an immutable HostApiFunctions table plus a
per-run HostApi value object that binds it to one runner and one run's
pipeline slot / arena bank. Carry the run's resource selection and trace
identity across the C ABI in a required NativeRunDescriptor, and delete the
pthread-TLS resource-selection mechanism (NativeRunThreadSelection, both
pthread keys, capture/restore) and the four setter exports it made necessary.
Launch acceptance moves from a TLS setter into the per-run launch signal.

Bump PTO_PIPELINE_CONTRACT_ABI_VERSION 1 -> 2 so a stale host_runtime.so
(whose simpler_run/prepare_run still take the old flat argument list) is
rejected at load rather than crashing at the first call.

PR-A of the hw-native-sys#1650 three-way split; PR-B (direct-L2 async lane) and PR-C
(HostTensorAccessScope RAII) follow.

Co-Authored-By: Claude <noreply@anthropic.com>
@ChaoWao
ChaoWao force-pushed the refactor/host-api-per-run branch from 6ea6f15 to 2b6aabe Compare August 4, 2026 10:50
@ChaoWao

ChaoWao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI status — two red checks are self-hosted-runner infra, not this change

Everything that actually executes this change's code is green:

check result
build (8 DSOs incl. onboard cross-compile)
pre-commit (clang-format / markdownlint / check-headers / check-english-only)
profiling-flags-smoke, packaging-matrix (linux+mac)
ut (linux+mac, no-hw), ut-a2a3 (a2a3 hw unit)
st-sim-a2a3 (linux+mac), st-sim-a5 (linux+mac)
st-onboard-a5 (a5 hardware scene tests)
st-onboard-a2a3 ❌ (infra — see below)
ut-a5 ❌ (infra — see below)

ut-a5 — persistent a5-runner network failure

Fails at actions download / git fetch with OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443 / Failed to resolve action download info … SSL connection could not be established. It dies before checkout, so none of this change's code runs. It reproduced identically across two reruns — a self-hosted a5 runner network problem, independent of this PR.

st-onboard-a2a3 — a2a3-runner / device-state failures

  • 1st run: simpler_init → ensure_binaries_loaded → LoadAicpuOp::BootstrapDispatcher: aclrtSynchronizeStream failed: 507018 on dev=8 and dev=11, for every test. That is the dispatcher bootstrap (device bring-up) in load_aicpu_op.cpp / ensure_binaries_loadednot in this diff (this PR's device_runner_base.cpp hunks are at lines 27–413 and 1504+, leaving the 413–1578 range that contains ensure_binaries_loaded/ensure_device_initialized byte-unchanged) and it references none of what this PR removes (no HostApi/TLS/selection). It's the poisoned/hung-device signature.
  • Rerun 1: job cancelled (no log).
  • Rerun 2: failure, but the job log is unretrievable (blob 404, no failed step recorded, no 507018/SSL/FAILED tests//assert signature) — i.e. no test assertion fired; consistent with another runner-level/device failure.

The same code path this PR changes is covered green elsewhere: st-onboard-a5 (a5 hardware — same simpler_init + per-run HostApi + launch-acceptance path), st-sim-a2a3 (a2a3 ChipWorker/HostApi/NativeRunDescriptor path on sim), ut-a2a3 (a2a3 hardware units), and build (a2a3 onboard compiles).

Ask: this needs a re-run on a healthy a2a3 device (dev=8/11 were poisoned) and the a5 runner's network looked at. I don't think either red check reflects a defect in this change — if a re-run on healthy runners still fails, please ping me and I'll dig into the device log. (run 30902344820)

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