Skip to content

Refactor: declare K=1 runtime pipeline contracts#1463

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-pr4-pipeline-contract
Jul 26, 2026
Merged

Refactor: declare K=1 runtime pipeline contracts#1463
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
Crane-Liu:codex/worker-async-pr4-pipeline-contract

Conversation

@Crane-Liu

@Crane-Liu Crane-Liu commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • define the versioned PipelineContract C ABI and the HOST_PER_RUN / DEVICE_SCRATCH / EXEC_HANDLE resource classes
  • let A2/A3 host_build_graph and tensormap_and_ringbuffer declare the resources that cross KernelLaunch
  • keep pipeline_depth as the only replication count; a resource's copy count follows from its class
  • reserve kind 0 as unspecified, so a declaration that overstates resource_count is rejected whatever its filled entries are
  • put the admission check in pipeline_contract.h beside the contract, with unit tests
  • publish the contract only after the runtime is up, so a failed init() cannot report another runtime's shape
  • preserve the existing synchronous simpler_run -> launch_run -> reap_run path

Why one replication count

HOST_PER_RUN and EXEC_HANDLE need one instance per in-flight run;
DEVICE_SCRATCH needs exactly one, because it is not rewritten per run and
device ops run one at a time. So the copy count is derivable from the class, and
a runtime that wants a small resource replicated while a large one stays shared
expresses that by classifying them — not by carrying a second global count whose
binding to particular resource kinds would live outside the contract.

That is exactly the difference between the two runtimes:

HOST_PER_RUN DEVICE_SCRATCH EXEC_HANDLE
host_build_graph GM_HEAP, GM_SM, RUNTIME_IMAGE both streams
tensormap_and_ringbuffer TASK_ARGS GM_HEAP, GM_SM, RUNTIME_IMAGE both streams

host_build_graph materializes this run's own graph into the image it uploads,
so every device-resident region it declares carries per-run content.
tensormap_and_ringbuffer orchestrates on the device, so only its task args
carry per-run content.

The class turns on rewritten per run or not, not on who writes the bytes. Its
heap, SM and runtime image are host-built and uploaded by
build_and_cache_prebuilt_arena, but only on a cache miss or eager prewarm —
once per (callable, sizing). A repeat run of the same callable takes
bind_cached_runtime_image, which performs no copy_to_device. One instance is
therefore correct, and the header comment states that rather than the earlier
claim that the device builds these regions.

Admission

is_valid_depth1_pipeline_contract rejects a wrong ABI version, more resources
than fit, a depth other than 1, an unspecified or out-of-range kind, an
out-of-range class, and a non-zero reserved bytes_per_copy. An entry a runtime
never filled in stays zeroed, so the unspecified-kind rule is what catches a
resource_count larger than the entries actually filled in. A runtime that
exports no contract keeps the default depth-1 shape, so the declaration is
additive; a runtime that ships a contract this build cannot honor fails init().

Repeated kinds are legal: a kind names a resource type, not one instance of it,
so a runtime that needs two of a kind can say so.

Scope

One runtime buffer and one pipeline slot throughout. A depth above 1, slot
selection, asynchronous acceptance and overlap remain later changes. Nothing
consumes the declaration yet.

Series context

Part of the worker async-preparation series, following #1459 (per-run identity
and completion fence), #1460 (Worker.submit and run handles) and #1462
(launch/reap split), all merged. This declares what each runtime owns across the
KernelLaunch boundary that #1462 established.

Validation

Rebased onto merged main; the three superseded ancestor commits are dropped,
so this is a single commit.

  • changed-file pre-commit hooks: passed
  • C++ unit tests: 61/61 passed, including 12 new PipelineContract cases
  • the two new admission rules were mutation-checked: removing the
    bytes_per_copy rule or the unspecified-kind rule fails exactly the cases
    covering it, the latter failing both RejectsAnUnspecifiedKind and
    RejectsAResourceCountPastTheFilledEntries
  • RejectsAResourceCountPastTheFilledEntries loops over two different filled
    kinds, so it cannot pass by coincidence of which kind the declaration uses
  • Python unit tests: 821 passed, 2 skipped
  • A2/A3 onboard libhost_runtime.so rebuilt for both runtimes, both exporting
    get_pipeline_contract
  • both declarations were exercised end to end against the admission check on
    simulation — vector_example (host_build_graph) and mixed_example
    (tensormap_and_ringbuffer) each pass, which requires ChipWorker::init to
    load and accept the runtime's contract, including the enlarged six-resource
    tensormap_and_ringbuffer declaration

The hardware runs quoted in earlier revisions were against a commit no longer in
this branch's history and have not been re-run; the onboard CI job is the
meaningful check.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change replaces drain-based execution with explicit run lifecycle APIs, asynchronous Python RunHandle completion, run-scoped TensorMap state, failure callbacks, and failure-safe cleanup. It also adds optional pipeline contract discovery and separates device launch from reap processing.

Changes

Run-scoped execution lifecycle

Layer / File(s) Summary
Run and TensorMap contracts
src/common/hierarchical/types.*, src/common/hierarchical/tensormap.*, src/common/hierarchical/orchestrator.h
Introduces RunId/RunState, run lifecycle APIs, per-run task tracking, and mutex-protected (RunId, TensorKey) TensorMap entries.
Native run orchestration and failure routing
src/common/hierarchical/orchestrator.cpp, src/common/hierarchical/scheduler.*, src/common/hierarchical/worker*, src/common/hierarchical/ring.*, tests/ut/cpp/hierarchical/*
Migrates submission, consumption, failure reporting, worker completion, and allocator reclamation to run-scoped state.
Python RunHandle execution and teardown
python/bindings/worker_bind.h, python/simpler/orchestrator.py, python/simpler/worker.py, tests/ut/py/test_worker/*
Adds asynchronous Worker.submit() and RunHandle, makes Worker.run() wait on the handle, and updates close, timeout, error, and lifetime behavior.
Run-fence documentation
docs/comm-domain.md, docs/orchestrator.md, docs/remote-l3-worker-design.md, docs/task-flow.md, docs/scheduler.md, docs/hierarchical_level_runtime.md
Documents run-fence completion, failure-safe cleanup, explicit endpoint outcomes, run-scoped TensorMap behavior, and globally quiescent Ring compaction.

Runtime pipeline contracts

Layer / File(s) Summary
Pipeline contract ABI and runtime exports
src/common/worker/pto_runtime_c_api.h, src/a2a3/runtime/*/host/runtime_maker.cpp
Adds pipeline contract types and an optional get_pipeline_contract() C ABI export in host runtimes.
Pipeline validation and device run structure
src/common/worker/chip_worker.*, src/a2a3/platform/onboard/host/device_runner.*
Loads and validates runtime pipeline metadata, exposes resource counts, resets contract state during finalization, and splits device execution into launch and reap phases.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Worker
  participant RunHandle
  participant Orchestrator
  Caller->>Worker: submit(callable)
  Worker->>Orchestrator: begin_run and build DAG
  Worker-->>Caller: return RunHandle
  Worker->>Orchestrator: close_run_submission(run_id)
  Caller->>RunHandle: wait(timeout)
  RunHandle->>Orchestrator: wait_run(run_id)
  RunHandle->>Worker: perform cleanup
  RunHandle->>Orchestrator: release_run(run_id)
Loading

Possibly related PRs

Poem

A bunny submits, then hops away,
While RunHandle guards the finish line’s day.
Fences wait, and domains free,
Tensor keys stay run-safe as can be.
Pipelines bloom with contracts bright—
Launch, reap, and sleep goodnight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.89% 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 accurately summarizes the main change: declaring K=1 runtime pipeline contracts.
Description check ✅ Passed The description is clearly aligned with the changeset and describes the contract refactor and related runtime updates.

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-pr4-pipeline-contract branch from 4561329 to 9d1850f Compare July 24, 2026 23:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
python/bindings/worker_bind.h (1)

371-371: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider releasing the GIL for _release_run.

Orchestrator::release_run can acquire the scheduler loop mutex and run allocator_->reset_to_empty() (src/common/hierarchical/orchestrator.cpp lines 177-183). That blocks every other Python thread for the duration since the native scheduler never needs the GIL, there is no deadlock risk in releasing it.

♻️ Proposed change
-        .def("_release_run", &Orchestrator::release_run, nb::arg("run_id"));
+        .def(
+            "_release_run", &Orchestrator::release_run, nb::arg("run_id"),
+            nb::call_guard<nb::gil_scoped_release>()
+        );
🤖 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 `@python/bindings/worker_bind.h` at line 371, Update the `_release_run` binding
for `Orchestrator::release_run` to release the Python GIL while the native
operation executes, using the binding library’s established call-guard
mechanism. Preserve the existing `run_id` argument and release behavior.
python/simpler/orchestrator.py (1)

143-150: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale docstring reference to Worker.run().

The module docstring above (lines 27-32) was updated to center on Worker.submit()/RunHandle, but this class docstring still says "DAG builder. Valid only inside the orch function passed to Worker.run()." Update to mention Worker.submit() (with Worker.run() as the blocking compatibility wrapper) for consistency with the rest of this file's updated terminology.

📝 Proposed doc fix
 class Orchestrator:
-    """DAG builder. Valid only inside the orch function passed to Worker.run().
+    """DAG builder. Valid only inside the orch function passed to Worker.submit()
+    (or its blocking Worker.run() wrapper).
🤖 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 `@python/simpler/orchestrator.py` around lines 143 - 150, Update the
Orchestrator class docstring to state that it is valid only inside the
orchestration function passed to Worker.submit(), while noting Worker.run() as
the blocking compatibility wrapper. Keep the existing ownership and lifetime
explanation unchanged.
python/simpler/worker.py (1)

5738-5790: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

callable parameter shadows the Python builtin across the new submit API and its test mock. The root cause is the new public Worker.submit/run signatures choosing callable as the parameter name; the test mock mirrors it only because it monkeypatches that exact signature.

  • python/simpler/worker.py#L5738-L5790: rename the callable parameter (e.g. to callback or orch_fn) in submit, run, _submit_locked, and _submit_l3_locked.
  • tests/ut/py/test_worker/test_host_worker.py#L1575-L1575: update fake_submit's parameter name to match once the production signature is renamed.
🤖 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 `@python/simpler/worker.py` around lines 5738 - 5790, Rename the callable
parameter to a non-shadowing name consistently in Worker.submit, Worker.run,
_submit_locked, and _submit_l3_locked, updating their internal references while
preserving behavior; also rename the corresponding parameter in
tests/ut/py/test_worker/test_host_worker.py lines 1575-1575 in fake_submit to
match the production signature.

Source: Linters/SAST tools

🤖 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/task-flow.md`:
- Around line 381-383: Revise the documentation around ring.slot_state(id) and
reset_to_empty() to state that CONSUMED ends only the slot’s
logical/task-resource lifetime; the TaskSlotState entry remains in the deque
until reset_to_empty() resets the ring. Remove the implication that slots are
individually reclaimed while preserving the existing worker-level reset
condition.

In `@src/common/hierarchical/orchestrator.cpp`:
- Around line 197-207: Make report_task_error best-effort so late or reset run
IDs cannot throw from the scheduler thread. Add a tolerant try_get_run lookup
alongside get_run, update record_run_error to use it and silently drop errors
when no run exists, while preserving first-error recording for valid runs.
- Around line 164-184: The drain decision in Orchestrator::release_run is made
after releasing runs_mu_, allowing concurrent begin_run() activity between the
quiescence check and allocator reset. Move the allocator active_count() check
and the resulting reset decision into the runs_mu_ critical section, preserving
scheduler-loop mutex protection around reset_to_empty(); alternatively enforce
and document that begin_run() cannot run concurrently, but ensure the invariant
is explicit.

In `@src/common/hierarchical/scheduler.cpp`:
- Around line 75-78: Update the failure-callback flow in the scheduler around
is_failure, cfg_.on_task_failed_cb, and the aggregation path near lines 153-155
so each failed task or grouped failure is reported only once. Track whether the
task/group failure has already been reported across incoming completions,
suppressing the later aggregated callback while preserving reporting for
genuinely new failures.

In `@src/common/worker/chip_worker.cpp`:
- Around line 163-174: In the initialization flow, defer assigning
resolved_contract to pipeline_contract_ until create_device_context and
simpler_init complete successfully. Update the rollback paths in init() to avoid
exposing metadata from a failed runtime, while preserving contract validation
and handle cleanup.

In `@tests/ut/py/test_worker/test_startup_readiness.py`:
- Line 768: Rename the local variable callable in the ChipCallable.build test
setup to chip_callable, and update any references to it within the test.

---

Nitpick comments:
In `@python/bindings/worker_bind.h`:
- Line 371: Update the `_release_run` binding for `Orchestrator::release_run` to
release the Python GIL while the native operation executes, using the binding
library’s established call-guard mechanism. Preserve the existing `run_id`
argument and release behavior.

In `@python/simpler/orchestrator.py`:
- Around line 143-150: Update the Orchestrator class docstring to state that it
is valid only inside the orchestration function passed to Worker.submit(), while
noting Worker.run() as the blocking compatibility wrapper. Keep the existing
ownership and lifetime explanation unchanged.

In `@python/simpler/worker.py`:
- Around line 5738-5790: Rename the callable parameter to a non-shadowing name
consistently in Worker.submit, Worker.run, _submit_locked, and
_submit_l3_locked, updating their internal references while preserving behavior;
also rename the corresponding parameter in
tests/ut/py/test_worker/test_host_worker.py lines 1575-1575 in fake_submit to
match the production signature.
🪄 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: fc67cfa5-37eb-4853-a8ca-05ad6fe1cd2f

📥 Commits

Reviewing files that changed from the base of the PR and between 6284040 and 9d1850f.

📒 Files selected for processing (37)
  • docs/comm-domain.md
  • docs/hierarchical_level_runtime.md
  • docs/orchestrator.md
  • docs/remote-l3-worker-design.md
  • docs/scheduler.md
  • docs/task-flow.md
  • python/bindings/worker_bind.h
  • python/simpler/orchestrator.py
  • python/simpler/task_interface.py
  • python/simpler/worker.py
  • src/a2a3/platform/onboard/host/device_runner.cpp
  • src/a2a3/platform/onboard/host/device_runner.h
  • src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp
  • src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
  • src/common/hierarchical/orchestrator.cpp
  • src/common/hierarchical/orchestrator.h
  • src/common/hierarchical/ring.cpp
  • src/common/hierarchical/ring.h
  • src/common/hierarchical/scheduler.cpp
  • src/common/hierarchical/scheduler.h
  • src/common/hierarchical/tensormap.cpp
  • src/common/hierarchical/tensormap.h
  • src/common/hierarchical/types.cpp
  • src/common/hierarchical/types.h
  • src/common/hierarchical/worker.cpp
  • src/common/hierarchical/worker_manager.cpp
  • src/common/hierarchical/worker_manager.h
  • src/common/worker/chip_worker.cpp
  • src/common/worker/chip_worker.h
  • src/common/worker/pto_runtime_c_api.h
  • tests/ut/cpp/hierarchical/test_orchestrator.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/cpp/hierarchical/test_tensormap.cpp
  • tests/ut/py/test_worker/test_error_propagation.py
  • tests/ut/py/test_worker/test_host_worker.py
  • tests/ut/py/test_worker/test_l3_l2_orch_comm.py
  • tests/ut/py/test_worker/test_startup_readiness.py

Comment thread docs/task-flow.md
Comment thread src/common/hierarchical/orchestrator.cpp
Comment thread src/common/hierarchical/orchestrator.cpp Outdated
Comment thread src/common/hierarchical/scheduler.cpp
Comment thread src/common/worker/chip_worker.cpp Outdated
Comment thread tests/ut/py/test_worker/test_startup_readiness.py
@ChaoWao
ChaoWao force-pushed the codex/worker-async-pr4-pipeline-contract branch from 9d1850f to a9ec299 Compare July 26, 2026 01:25
@ChaoWao ChaoWao changed the title Refactor: declare K=1 runtime pipeline contracts (4/7) Refactor: declare K=1 runtime pipeline contracts Jul 26, 2026
@ChaoWao
ChaoWao force-pushed the codex/worker-async-pr4-pipeline-contract branch 3 times, most recently from 52e266c to b3e89f4 Compare July 26, 2026 03:50
Define the versioned PipelineContract C ABI and the HOST_PER_RUN /
DEVICE_SCRATCH / EXEC_HANDLE resource classes, and let the A2/A3
host_build_graph and tensormap_and_ringbuffer runtimes declare the
resources that cross KernelLaunch.

A resource's copy count follows from its class: HOST_PER_RUN and
EXEC_HANDLE need one instance per in-flight run, DEVICE_SCRATCH needs
exactly one because it is not rewritten per run and device ops run one
at a time. That leaves pipeline_depth as the only replication count, so
a runtime that wants a small resource replicated and a large one shared
says so by classifying them rather than by carrying a second count.

host_build_graph materializes this run's own graph into the image it
uploads, so every device region it declares is HOST_PER_RUN.
tensormap_and_ringbuffer orchestrates on the device, so only its task
args carry per-run content; its heap, SM and runtime image are built and
uploaded once per callable sizing and then served from the
prebuilt-arena cache, so they are DEVICE_SCRATCH.

Zero is reserved as an unspecified kind. An entry a runtime never filled
in stays zeroed, so a declaration that overstates resource_count is
rejected whatever its filled entries are, and a kind may repeat for a
runtime that needs several resources of one type.

ChipWorker loads the declaration as optional runtime metadata and
rejects one it cannot honor: wrong ABI version, too many resources, a
depth other than 1, an unspecified or out-of-range kind, an
out-of-range class, or a non-zero reserved bytes_per_copy. A runtime
that exports no contract keeps the default depth-1 shape. The contract
is published only once the runtime is up, so a failed init never reports
the shape of a runtime this worker is not bound to.

One runtime buffer and one pipeline slot are kept throughout. A depth
above 1, slot selection, asynchronous acceptance and overlap remain
later changes; the existing synchronous
simpler_run -> launch_run -> reap_run path is unchanged.
@ChaoWao

ChaoWao commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Revision: kind sentinel replaces the repeated-kind rule; DEVICE_SCRATCH rationale corrected

Pushed b3e89f41. Three findings from the latest review pass, two fixed and one studied.

1. The repeated-kind rule had a hole. It caught an overstated resource_count
only when the declaration happened to use GM_HEAP (the old kind 0), because the
rule fired on a collision with the filled entry. A runtime declaring, say,
TASK_ARGS alone with resource_count = 2 was accepted with a zeroed trailing
entry. Verified with a probe before the fix (accepted) and after (rejected).

The rule was also wrong on its own terms: a kind names a resource type, so a
runtime needing two AICore streams could not say so.

Both go away with a sentinel. PTO_PIPELINE_KIND_UNSPECIFIED = 0 shifts the real
kinds to 1..6; the validator rejects kind 0. An unfilled entry is zeroed, so an
overstated count is now rejected whatever the filled entries are, and repeated
kinds are legal. RejectsAResourceCountPastTheFilledEntries loops over two
different filled kinds so it cannot pass by coincidence, and AcceptsARepeatedKind
pins the new allowance. Mutation-checked: removing the kind-0 check fails both.

2. The DEVICE_SCRATCH comment claimed the device builds those regions. It
does not. Corrected in the header — see 3.

3. Studied: are tensormap_and_ringbuffer's arenas per-run? No. The upload
is a host copy_to_device inside build_and_cache_prebuilt_arena, reached only
on a cache miss (runtime_maker.cpp:909) or eager prewarm (:952). A repeat run
of the same callable takes bind_cached_runtime_image, which contains no
copy_to_device at all. So heap / SM / runtime image are written once per
(callable, sizing) and then served from cache.

The DEVICE_SCRATCH classification is therefore right, but for a different
reason than stated: the class turns on rewritten per run or not, not on who
writes the bytes. The comment now says that. This also enlarges the
tensormap_and_ringbuffer declaration from 4 resources to 6 — GM_HEAP and
GM_SM were previously omitted rather than classified.

Validation

61/61 C++ (12 PipelineContract cases), 821 passed / 2 skipped Python, and both
runtimes exercised end to end on sim (vector_example, mixed_example) so
ChipWorker::init actually loads and accepts the enlarged declaration.

Still open for the author's call

  • whether to bump abi_version to 2, since the kind numbering moved. Nothing
    consumes the contract yet and both producers are in-tree, so I left it at 1.
  • the size_t -> uint64_t change on the wire struct is arguably separable, but
    it is a one-line correctness fix for a host/device struct and splitting it
    would leave the ABI briefly wrong.

@ChaoWao
ChaoWao merged commit 05c76c0 into hw-native-sys:main Jul 26, 2026
16 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