Skip to content

feat: support pipelined orchestration and scheduling across tokens and requests#1379

Open
TaoZQY wants to merge 17 commits into
hw-native-sys:mainfrom
Crane-Liu:20260715_hostpipeline
Open

feat: support pipelined orchestration and scheduling across tokens and requests#1379
TaoZQY wants to merge 17 commits into
hw-native-sys:mainfrom
Crane-Liu:20260715_hostpipeline

Conversation

@TaoZQY

@TaoZQY TaoZQY commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR enables host-side orchestration and device-side scheduling to run as a pipeline.

  • Allows orchestration for token N+1 to overlap with scheduling and execution for token N.
  • Supports concurrent requests through overlapping Worker.run() calls. While one request is running, another request can be accepted and prepared.
  • Adds per-token L3–L2 interaction instead of batching multiple tokens into one response.
  • Ensures each generated token is delivered to the user immediately. The producer waits until the consumer receives the current token before continuing.
  • Adds trace events for token receipt and user delivery.
  • Simplifies the Perfetto swimlane to focus on token-level and request-level overlap.

Profiling Results

The dual-request trace demonstrates:

  • O/S overlap between adjacent tokens.
  • Overlap between two independent requests.
  • Two concurrent Worker.run() calls.
  • Per-token L3 receipt and user delivery events.
  • No batching of several generated tokens before returning them to the user.
img_v3_0213l_888affc9-dd0e-4b22-81c7-1e5913d8be5g

@TaoZQY
TaoZQY marked this pull request as ready for review July 16, 2026 12:00
@coderabbitai

coderabbitai Bot commented Jul 16, 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: ae8a957c-b003-4f36-9288-433f9dcd93a6

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

This PR adds concurrent streaming request sessions for hierarchical workers, including bounded admission, per-request token delivery, cancellation, prepared host/device execution, epoch-based HostGraph publication, arena-bank coordination, concurrency synchronization, paged-attention coverage, and Perfetto profiling tools.

Changes

Streaming HostGraph execution

Layer / File(s) Summary
Session and shared-memory protocol
python/simpler/request_session.py, python/simpler/l3_l2_orch_comm.py, python/bindings/task_interface.cpp, docs/request-session.md
Adds bounded RequestSession dispatch, streamed RequestStream delivery, cancellation, shared-memory admission, token decoding, mailbox communication, and Python serialization helpers.
HostGraph epoch pipeline
src/a2a3/runtime/host_build_graph/{host,runtime,aicpu}/...
Adds graph-boundary callbacks, epoch capture/materialization, asynchronous host orchestration, token publication, scheduler epoch handling, and shared-memory epoch controls.
Prepared request and worker integration
python/simpler/worker.py, src/common/worker/..., src/common/platform/...
Adds host admission services, split prepare/execute APIs, optional runtime symbols, thread context binding, per-thread arena-bank selection, and prepared-request tracking.
Concurrent orchestration safety
src/common/hierarchical/..., src/common/log/..., tests/ut/...
Adds serialized submission, thread-owned scope frames, synchronized tensor maps, tracing context propagation, and concurrency tests.
Paged-attention validation and profiling
tests/st/a2a3/host_build_graph/..., tools/*perfetto.py
Adds batched paged-attention kernels and orchestration tests, concurrent streaming validation, and Perfetto trace generation for pipeline and cross-request timing.
Documentation updates
README.md, docs/dynamic-linking.md, docs/hierarchical_level_runtime.md
Links the streaming-session documentation and records the arena-bank TLS entry and runtime cross-reference.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant RequestSession
  participant Worker
  participant HostRequestAdmissionService
  participant ChipWorker
  participant AicpuExecutor
  RequestSession->>Worker: submit request
  Worker->>HostRequestAdmissionService: prepare host request
  HostRequestAdmissionService->>ChipWorker: prepare request
  Worker->>ChipWorker: execute prepared request
  ChipWorker->>AicpuExecutor: launch device execution
  AicpuExecutor-->>RequestSession: publish streamed token
Loading

Poem

A rabbit hops through epochs bright,
Tokens stream from left to right.
Host prepares, Device sings,
Two arena banks flap their wings.
Threads align and queues behave—
A carrot-powered pipeline wave!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.23% 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
Title check ✅ Passed The title accurately summarizes the main change: pipelined orchestration and scheduling across tokens and concurrent requests.
Description check ✅ Passed The description matches the PR scope, covering pipelined orchestration, concurrent requests, per-token streaming, and trace updates.
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.

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces Streaming Request Sessions to enable non-blocking request admission and Host O / Device S pipelining on hierarchical Workers. It implements RequestSession and RequestStream in Python, adds support for concurrent runs and async HostGraph building in the C++ runtime and scheduler, and introduces split request preparation on the ChipWorker. Additionally, comprehensive integration and unit tests are added. The review feedback suggests replacing a hardcoded magic number in the shared memory size configuration with explicit constants to improve maintainability and prevent layout synchronization issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

_CONTROL_OFF_RESPONSE = _CONTROL_OFF_REQUEST + _REQUEST.size
CONTROL_BLOCK_SIZE = _CONTROL_OFF_RESPONSE + _RESPONSE.size
CONTROL_SHM_SIZE = max(4096, CONTROL_BLOCK_SIZE)
CONTROL_SHM_SIZE = max(65536, CONTROL_BLOCK_SIZE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The hardcoded value 65536 makes the code harder to maintain as it obscures the origin of this number. It appears to be derived from HOST_REQUEST_CONTROL_OFFSET and HOST_REQUEST_CONTROL_BYTES in the new request_session.py file.

To improve clarity and avoid this magic number, please define these values as constants here. This makes the relationship between the shared memory size and the control region it contains explicit and ensures they stay in sync if the layout changes.

Suggested change
CONTROL_SHM_SIZE = max(65536, CONTROL_BLOCK_SIZE)
HOST_REQUEST_CONTROL_OFFSET = 4096
HOST_REQUEST_CONTROL_BYTES = 60 * 1024
CONTROL_SHM_SIZE = max(HOST_REQUEST_CONTROL_OFFSET + HOST_REQUEST_CONTROL_BYTES, CONTROL_BLOCK_SIZE)

@Crane-Liu
Crane-Liu force-pushed the 20260715_hostpipeline branch from 139fe3a to 7e31f63 Compare July 17, 2026 17:46

@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: 18

🤖 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/request-session.md`:
- Around line 69-79: Update the example’s request preparation flow after
emitter.prepare_host_request to call append_host_graph_prepared_request_args
with chip_args and request_id before orch.submit_next_level, ensuring the
submission is recognized as a prepared host-graph request and uses the prepared
arena bank.

In `@python/simpler/l3_l2_orch_comm.py`:
- Around line 214-220: Ensure timeout handling cannot leave either shared-memory
control lane permanently in DONE: in python/simpler/l3_l2_orch_comm.py lines
214-220, update the L3-L2 orchestration client flow around _read_response and
the polling loop to invalidate and recreate the client after a timeout, or
implement an equivalent race-safe acknowledged reset; apply the same recovery
contract to HostRequestAdmissionClient in python/simpler/request_session.py
lines 114-122. Preserve normal successful completion and IDLE transition
behavior.

In `@python/simpler/request_session.py`:
- Around line 395-408: Update close() to reject calls from a dispatcher thread
by checking _owns_current_thread() before enqueueing _STOP or releasing
ownership, and fail fast when true. Preserve normal shutdown and ownership
release for callers outside the dispatcher threads.
- Around line 246-267: The _emit method currently completes the stream
immediately when final=True, allowing later failures in Worker.run processing to
be reported as success. Remove or defer the _finish(None) call from _emit, and
perform successful termination only at the existing completion point after
Worker.run returns in _dispatch_loop around line 454; preserve cancellation and
terminal-error propagation.
- Around line 374-382: Update the request submission sequence around
_RequestWork and _live_work so each work item is inserted into _live_work before
calling self._requests.put_nowait(work). Preserve the existing
RequestBackpressureError behavior, and ensure a full queue does not leave an
unsubmitted work item registered in _live_work.

In `@python/simpler/worker.py`:
- Around line 4067-4072: Update both shared-memory cleanup sites in
python/simpler/worker.py at lines 4067-4072 and 3861-3866: make close and unlink
independent operations so a SharedMemory.close() failure does not prevent
unlink(). Preserve the bootstrap rollback’s original exception by suppressing
cleanup errors there, and retain best-effort cleanup during normal teardown.
- Around line 3835-3860: Serialize the per-worker initialization in
_ensure_l3_l2_orch_comm by guarding the readiness check, control SharedMemory
creation, service initialization, and client publication with a lock. Re-check
_l3_l2_orch_comm_ready after acquiring the lock, return the existing client when
another dispatcher completed initialization, and ensure only one child service
and dictionary entry are created per worker.
- Around line 459-476: Update _prepared_request_id_from_blob to unpack the
fourth trailer scalar following token_magic, then reject the trailer unless the
prepared request ID and stream request ID match. Preserve the existing
magic-value validation and return the validated request ID only when both IDs
agree.

In `@src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp`:
- Around line 1142-1164: Update wait_for_device_epoch to poll the device’s
orch_error_code and sched_error_code alongside the epoch. Return immediately
with an error when either field reports a fatal error, while retaining the
existing 30-second deadline only as the deadlock fallback.

In `@src/common/platform/onboard/host/c_api_shared.cpp`:
- Around line 680-682: Update both exceptional cleanup sites in
src/common/platform/onboard/host/c_api_shared.cpp: at lines 680-682, mirror the
prepare-failure cleanup by invoking validate_runtime_impl() before destroying r;
at lines 721-722, invoke validate_runtime_impl() before the runtime scope guard
destroys r. Ensure both catch paths reconcile prepared Runtime resources before
returning or scope cleanup.
- Around line 73-85: Update capture_thread_context(), bind_thread_context(), and
unbind_thread_context() to preserve the current runner together with the
selected arena-bank TLS value. Store both pieces of context in the captured
state, restore the runner and bank when binding the async thread, and clear or
restore both values when unbinding, while retaining the existing attach failure
cleanup behavior.

In `@src/common/worker/chip_worker.cpp`:
- Around line 391-417: Update the preparation flow around prepared_requests_ to
reserve request_id under prepared_mu_ before waiting for or invoking
prepare_request_fn_. Track the reservation as in-progress, then atomically
publish the completed prepared state while preserving duplicate-ID rejection.
Ensure every preparation, allocation, and publication failure releases the arena
bank and removes the reservation; add a regression test covering concurrent
preparations with the same request ID.

In
`@tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/aic/aic_qk_matmul.cpp`:
- Around line 125-144: Reject invalid dimensions before dispatch and defensively
inside each kernel entrypoint: in
tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/aic/aic_qk_matmul.cpp:125-144,
validate positive batch_count, divisibility, and exact supported
query-tile/block-size/head-dimension tuples before qk_matmul_batch_impl; apply
the equivalent combined validation in aic_pv_matmul.cpp:121-137 before its
fixed-template dispatch. In aiv_online_update.cpp:211-230, accept only (16,16),
(16,128), and (64,128), and in aiv_softmax_prepare.cpp:184-200 accept only
(16,16), (16,128), and (64,64); reject unsupported inputs at launch and repeat
the checks in each kernel to prevent invalid dispatch and out-of-bounds
execution.

In
`@tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/orchestration/paged_attention_three_layer_orch.cpp`:
- Around line 83-90: Move the layer barrier capacity validation in the
orchestration routine so it runs before any tasks are submitted for a layer.
Reject the layer when q_loop * num_chunks exceeds MAX_LAYER_DONE_DEPS, returning
through the existing failure path before creating partial dependencies. Preserve
normal submission and barrier/final-epoch behavior for valid layer sizes.

In
`@tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/test_batch_paged_attention_three_layer_l3.py`:
- Around line 223-242: Update the request synchronization around _relay_tokens
and request_a_submitted so the event is set only when A’s first token (sequence
1) is received and published, rather than immediately after submit. Keep B’s
session.submit after waiting for that event, preserving the existing stagger
only after A’s first publication.

In `@tests/ut/py/test_worker/test_request_session.py`:
- Around line 69-85: Update test_emit_waits_until_consumer_receives_token to add
a producer-entry synchronization event set immediately before
emitter.emit("token-1"), then wait for that event before asserting producer_done
is unset. Preserve the existing token consumption, completion, stream cleanup,
and thread-join assertions.

In `@tools/cross_request_host_device_perfetto.py`:
- Around line 85-131: Update parse_log and map_host_invocations so
request-to-invocation mapping does not depend on token arrival order. Parse
HOST_REQUEST_PREPARED records and associate each prepared request ID with its
corresponding simpler_prepare_request invocation, then assign the remaining
request to the sole simpler_run invocation. Preserve validation that exactly one
normal invocation and one prepare invocation exist for each later request, and
reject missing or ambiguous mappings.
- Around line 187-191: Update the delivery validation around received,
delivered, and one_to_one to count every (request, seq) occurrence with Counter
before declaring strict one-to-one delivery. Require the Counter values to
match, while retaining the existing timestamp-map calculation for shared-key
latency and nonnegative delays.
🪄 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

Run ID: 2f2b214f-ad20-412f-b4b5-74851e9b0009

📥 Commits

Reviewing files that changed from the base of the PR and between 4a2f076 and 7e31f63.

📒 Files selected for processing (53)
  • README.md
  • docs/dynamic-linking.md
  • docs/hierarchical_level_runtime.md
  • docs/request-session.md
  • python/bindings/task_interface.cpp
  • python/simpler/l3_l2_orch_comm.py
  • python/simpler/request_session.py
  • python/simpler/worker.py
  • src/a2a3/platform/onboard/host/device_runner.cpp
  • src/a2a3/platform/sim/host/device_runner.cpp
  • src/a2a3/runtime/host_build_graph/aicpu/aicpu_executor.cpp
  • src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp
  • src/a2a3/runtime/host_build_graph/orchestration/common.cpp
  • src/a2a3/runtime/host_build_graph/orchestration/pto_orchestration_api.h
  • src/a2a3/runtime/host_build_graph/runtime/host_graph_epoch.h
  • src/a2a3/runtime/host_build_graph/runtime/host_graph_token_stream.h
  • src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cpp
  • src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_runtime2.cpp
  • src/a2a3/runtime/host_build_graph/runtime/pto_runtime2.h
  • src/a2a3/runtime/host_build_graph/runtime/pto_shared_memory.h
  • src/a2a3/runtime/host_build_graph/runtime/runtime.h
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_context.h
  • src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_dispatch.cpp
  • src/a2a3/runtime/host_build_graph/runtime/shared/pto_shared_memory.cpp
  • src/a2a3/runtime/host_build_graph/runtime/shared/runtime.cpp
  • src/common/hierarchical/orchestrator.cpp
  • src/common/hierarchical/orchestrator.h
  • src/common/hierarchical/scope.cpp
  • src/common/hierarchical/scope.h
  • src/common/hierarchical/tensormap.cpp
  • src/common/hierarchical/tensormap.h
  • src/common/log/include/common/strace.h
  • 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/worker/chip_worker.cpp
  • src/common/worker/chip_worker.h
  • src/common/worker/pto_runtime_c_api.h
  • tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/aic/aic_pv_matmul.cpp
  • tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/aic/aic_qk_matmul.cpp
  • tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/aiv/aiv_online_update.cpp
  • tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/aiv/aiv_softmax_prepare.cpp
  • tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/orchestration/paged_attention_three_layer_orch.cpp
  • tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/test_batch_paged_attention_three_layer.py
  • tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/test_batch_paged_attention_three_layer_l3.py
  • tests/ut/cpp/common/test_trb_runtime_temp_buffer.cpp
  • tests/ut/cpp/hierarchical/test_scope.cpp
  • tests/ut/py/test_worker/test_request_session.py
  • tools/cross_request_host_device_perfetto.py
  • tools/host_orch_perfetto.py

Comment thread docs/request-session.md Outdated
Comment on lines +69 to +79
```python
emitter.prepare_host_request(
worker_id=0,
request_id=request_id,
callable_handle=callable_handle,
args=chip_args,
config=config,
arena_bank=1,
)
orch.submit_next_level(callable_handle, chip_args, config, worker=0)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mark chip_args as a prepared request before submission.

The example calls prepare_host_request but never appends HOST_GRAPH_PREPARED_REQUEST_MAGIC. Consequently, the chip loop uses normal run_from_blob, leaving the prepared request and arena bank stranded. Add append_host_graph_prepared_request_args(chip_args, request_id) before submit_next_level.

🤖 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 `@docs/request-session.md` around lines 69 - 79, Update the example’s request
preparation flow after emitter.prepare_host_request to call
append_host_graph_prepared_request_args with chip_args and request_id before
orch.submit_next_level, ensuring the submission is recognized as a prepared
host-graph request and uses the prepared arena bank.

Comment on lines +214 to +220
while _mailbox_load_i32(self._state_addr) != _STATE_DONE:
if time.monotonic() >= deadline:
raise TimeoutError("L3-L2 orch comm client timed out waiting for DONE")
time.sleep(_POLL_INTERVAL_S)

response = self._read_response()
_mailbox_store_i32(self._state_addr, _STATE_IDLE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Timeouts permanently poison both shared-memory control lanes. Once READY is published, either service may complete after the caller times out, leaving DONE with no client transition back to IDLE.

  • python/simpler/l3_l2_orch_comm.py#L214-L220: invalidate and recreate the orchestration client after timeout, or implement a race-safe acknowledged reset.
  • python/simpler/request_session.py#L114-L122: apply the same recovery contract to HostRequestAdmissionClient.
📍 Affects 2 files
  • python/simpler/l3_l2_orch_comm.py#L214-L220 (this comment)
  • python/simpler/request_session.py#L114-L122
🤖 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/l3_l2_orch_comm.py` around lines 214 - 220, Ensure timeout
handling cannot leave either shared-memory control lane permanently in DONE: in
python/simpler/l3_l2_orch_comm.py lines 214-220, update the L3-L2 orchestration
client flow around _read_response and the polling loop to invalidate and
recreate the client after a timeout, or implement an equivalent race-safe
acknowledged reset; apply the same recovery contract to
HostRequestAdmissionClient in python/simpler/request_session.py lines 114-122.
Preserve normal successful completion and IDLE transition behavior.

Comment on lines +246 to +267
def _emit(self, item: Any, *, final: bool = False) -> None:
consumed = threading.Event()
with self._finish_lock:
if self._done.is_set():
if isinstance(self._terminal_error, RequestCancelledError):
return
raise RuntimeError(f"request {self.request_id} stream is already complete")
if self._pending_consumed is not None:
raise RuntimeError(f"request {self.request_id} already has an outstanding item")
self._pending_consumed = consumed
self._items.put(_StreamItem(item, consumed))
consumed.wait()
with self._finish_lock:
if self._pending_consumed is consumed:
self._pending_consumed = None
if self._done.is_set():
if isinstance(self._terminal_error, RequestCancelledError):
return
if self._terminal_error is not None:
raise self._terminal_error
if final:
self._finish(None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not report terminal success before Worker.run finishes.

final=True calls _finish(None) inside the orchestration callback. If the subsequent drain, validation, or copy-back fails, _dispatch_loop cannot replace that success because _finish is already complete; users observe a successful stream. Defer successful termination to Line 454 after Worker.run returns.

🤖 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/request_session.py` around lines 246 - 267, The _emit method
currently completes the stream immediately when final=True, allowing later
failures in Worker.run processing to be reported as success. Remove or defer the
_finish(None) call from _emit, and perform successful termination only at the
existing completion point after Worker.run returns in _dispatch_loop around line
454; preserve cancellation and terminal-error propagation.

Comment on lines +374 to +382
stream = RequestStream(request_id, self)
work = _RequestWork(request, request_id, config, stream, threading.Event())
try:
self._requests.put_nowait(work)
except queue.Full as exc:
raise RequestBackpressureError(
f"RequestSession pending queue is full (request_id={request_id})"
) from exc
self._live_work[request_id] = work

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Register live work before publishing it to the dispatcher queue.

A dispatcher can consume and retire work immediately after put_nowait, before Line 382 inserts it. The completed request is then inserted permanently into _live_work, preventing reuse of its ID.

Proposed ordering fix
             stream = RequestStream(request_id, self)
             work = _RequestWork(request, request_id, config, stream, threading.Event())
+            self._live_work[request_id] = work
             try:
                 self._requests.put_nowait(work)
             except queue.Full as exc:
+                self._live_work.pop(request_id, None)
                 raise RequestBackpressureError(
                     f"RequestSession pending queue is full (request_id={request_id})"
                 ) from exc
-            self._live_work[request_id] = work
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stream = RequestStream(request_id, self)
work = _RequestWork(request, request_id, config, stream, threading.Event())
try:
self._requests.put_nowait(work)
except queue.Full as exc:
raise RequestBackpressureError(
f"RequestSession pending queue is full (request_id={request_id})"
) from exc
self._live_work[request_id] = work
stream = RequestStream(request_id, self)
work = _RequestWork(request, request_id, config, stream, threading.Event())
self._live_work[request_id] = work
try:
self._requests.put_nowait(work)
except queue.Full as exc:
self._live_work.pop(request_id, None)
raise RequestBackpressureError(
f"RequestSession pending queue is full (request_id={request_id})"
) from exc
🤖 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/request_session.py` around lines 374 - 382, Update the request
submission sequence around _RequestWork and _live_work so each work item is
inserted into _live_work before calling self._requests.put_nowait(work).
Preserve the existing RequestBackpressureError behavior, and ensure a full queue
does not leave an unsubmitted work item registered in _live_work.

Comment thread python/simpler/request_session.py Outdated
Comment on lines +395 to +408
def close(self) -> None:
should_stop = False
with self._state_lock:
if self._accepting:
self._accepting = False
should_stop = True
if should_stop:
for _thread in self._threads:
self._requests.put(_STOP)
current_ident = threading.get_ident()
for thread in self._threads:
if thread.ident != current_ident:
thread.join()
self._worker._release_request_session(self)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject close() from a dispatcher thread.

With a full queue, the sole dispatcher can block forever while inserting _STOP. Even without a full queue, ownership is released before that dispatcher exits, allowing another session or direct run to overlap it. Fail fast when _owns_current_thread() is true, or defer ownership release until the loop exits.

🤖 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/request_session.py` around lines 395 - 408, Update close() to
reject calls from a dispatcher thread by checking _owns_current_thread() before
enqueueing _STOP or releasing ownership, and fail fast when true. Preserve
normal shutdown and ownership release for callers outside the dispatcher
threads.

Comment on lines +83 to +90
constexpr uint64_t IN_CORE_BATCH = 16;
constexpr uint32_t MAX_LAYER_DONE_DEPS = 64;
uint64_t num_chunks = (batch + IN_CORE_BATCH - 1) / IN_CORE_BATCH;
PTO2TaskId prev_layer_done = PTO2TaskId::invalid();

for (uint64_t layer = 0; layer < layer_count; layer++) {
PTO2TaskId layer_done_deps[MAX_LAYER_DONE_DEPS];
uint32_t layer_done_dep_count = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject oversized layer barriers before submitting any tasks.

When q_loop * num_chunks > 64, the current check runs only after the 65th chunk has been submitted. Returning then leaves a partial layer without its barrier or final epoch, which can strand the streaming request.

Proposed fix
     constexpr uint64_t IN_CORE_BATCH = 16;
     constexpr uint32_t MAX_LAYER_DONE_DEPS = 64;
     uint64_t num_chunks = (batch + IN_CORE_BATCH - 1) / IN_CORE_BATCH;
+    uint64_t deps_per_layer = q_loop * num_chunks;
+    if (deps_per_layer > MAX_LAYER_DONE_DEPS) {
+        LOG_ERROR(
+            "batch_paged_attention_three_layer: layer requires %" PRIu64
+            " barrier dependencies; maximum is %u",
+            deps_per_layer, MAX_LAYER_DONE_DEPS
+        );
+        return;
+    }
     PTO2TaskId prev_layer_done = PTO2TaskId::invalid();
 
@@
-                if (!chunk_done.is_valid() || layer_done_dep_count >= MAX_LAYER_DONE_DEPS) {
-                    LOG_ERROR("batch_paged_attention_three_layer: layer barrier capacity exceeded");
+                if (!chunk_done.is_valid()) {
+                    LOG_ERROR("batch_paged_attention_three_layer: chunk produced no completion task");
                     return;
                 }

Also applies to: 189-193

🤖 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
`@tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/kernels/orchestration/paged_attention_three_layer_orch.cpp`
around lines 83 - 90, Move the layer barrier capacity validation in the
orchestration routine so it runs before any tasks are submitted for a layer.
Reject the layer when q_loop * num_chunks exceeds MAX_LAYER_DONE_DEPS, returning
through the existing failure path before creating partial dependencies. Preserve
normal submission and barrier/final-epoch behavior for valid layer sizes.

Comment on lines +223 to +242
orch.submit_next_level(callables.paged_attention, chip_args, cfg, worker=0)
if request_id == _REQUEST_A:
request_a_submitted.set()
self._relay_tokens(queue, request_id, expected_epochs, emitter)

def consume(stream, output, errors):
try:
output.extend(stream)
except BaseException as exc: # noqa: BLE001
errors.append(exc)

with worker.open_request_session(request_orch, max_active_runs=2) as session:
streams = {
_REQUEST_A: session.submit(requests[_REQUEST_A], config=config, request_id=_REQUEST_A),
}
assert request_a_submitted.wait(_STREAM_TIMEOUT_S)
# Place B's Host O behind A's first publication instead of profiling
# two simultaneous cold starts.
time.sleep(_CROSS_REQUEST_STAGGER_S)
streams[_REQUEST_B] = session.submit(requests[_REQUEST_B], config=config, request_id=_REQUEST_B)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Synchronize request B on A’s first token, not its submission.

request_a_submitted is set before _relay_tokens receives anything, so the sleep does not guarantee the stated “behind A’s first publication” ordering. Slow startup can profile two cold starts and make the overlap result nondeterministic. Set the event when sequence 1 is received, then submit B.

Proposed synchronization
-def _relay_tokens(self, queue, request_id: int, expected_epochs: int, emitter) -> None:
+def _relay_tokens(self, queue, request_id: int, expected_epochs: int, emitter, first_token_event=None) -> None:
     for seq in range(1, expected_epochs + 1):
         token, message = _read_token(queue, request_id, seq, expected_epochs)
+        if seq == 1 and first_token_event is not None:
+            first_token_event.set()
         ...
 
-    if request_id == _REQUEST_A:
-        request_a_submitted.set()
-    self._relay_tokens(queue, request_id, expected_epochs, emitter)
+    self._relay_tokens(
+        queue,
+        request_id,
+        expected_epochs,
+        emitter,
+        request_a_submitted if request_id == _REQUEST_A else None,
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
orch.submit_next_level(callables.paged_attention, chip_args, cfg, worker=0)
if request_id == _REQUEST_A:
request_a_submitted.set()
self._relay_tokens(queue, request_id, expected_epochs, emitter)
def consume(stream, output, errors):
try:
output.extend(stream)
except BaseException as exc: # noqa: BLE001
errors.append(exc)
with worker.open_request_session(request_orch, max_active_runs=2) as session:
streams = {
_REQUEST_A: session.submit(requests[_REQUEST_A], config=config, request_id=_REQUEST_A),
}
assert request_a_submitted.wait(_STREAM_TIMEOUT_S)
# Place B's Host O behind A's first publication instead of profiling
# two simultaneous cold starts.
time.sleep(_CROSS_REQUEST_STAGGER_S)
streams[_REQUEST_B] = session.submit(requests[_REQUEST_B], config=config, request_id=_REQUEST_B)
orch.submit_next_level(callables.paged_attention, chip_args, cfg, worker=0)
self._relay_tokens(
queue,
request_id,
expected_epochs,
emitter,
request_a_submitted if request_id == _REQUEST_A else None,
)
def consume(stream, output, errors):
try:
output.extend(stream)
except BaseException as exc: # noqa: BLE001
errors.append(exc)
with worker.open_request_session(request_orch, max_active_runs=2) as session:
streams = {
_REQUEST_A: session.submit(requests[_REQUEST_A], config=config, request_id=_REQUEST_A),
}
assert request_a_submitted.wait(_STREAM_TIMEOUT_S)
# Place B's Host O behind A's first publication instead of profiling
# two simultaneous cold starts.
time.sleep(_CROSS_REQUEST_STAGGER_S)
streams[_REQUEST_B] = session.submit(requests[_REQUEST_B], config=config, request_id=_REQUEST_B)
🤖 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
`@tests/st/a2a3/host_build_graph/batch_paged_attention_three_layer/test_batch_paged_attention_three_layer_l3.py`
around lines 223 - 242, Update the request synchronization around _relay_tokens
and request_a_submitted so the event is set only when A’s first token (sequence
1) is received and published, rather than immediately after submit. Keep B’s
session.submit after waiting for that event, preserving the existing stagger
only after A’s first publication.

Comment on lines +69 to +85
def test_emit_waits_until_consumer_receives_token() -> None:
stream = RequestStream(7, cast(RequestSession, _StreamSession()))
emitter = RequestEmitter(stream)
producer_done = threading.Event()

def produce() -> None:
emitter.emit("token-1")
producer_done.set()

thread = threading.Thread(target=produce)
thread.start()
time.sleep(0.02)
assert not producer_done.is_set()
assert stream.next(timeout=1.0) == "token-1"
assert producer_done.wait(1.0)
stream._finish(None)
thread.join()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Synchronize producer entry before asserting that emit blocks.

The producer may not have started during the fixed sleep, so this test can pass without exercising the handoff. Signal immediately before emit() and wait for that signal before checking producer_done.

Proposed test fix
     emitter = RequestEmitter(stream)
+    producer_started = threading.Event()
     producer_done = threading.Event()

     def produce() -> None:
+        producer_started.set()
         emitter.emit("token-1")
         producer_done.set()

     thread = threading.Thread(target=produce)
     thread.start()
-    time.sleep(0.02)
+    assert producer_started.wait(1.0)
     assert not producer_done.is_set()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_emit_waits_until_consumer_receives_token() -> None:
stream = RequestStream(7, cast(RequestSession, _StreamSession()))
emitter = RequestEmitter(stream)
producer_done = threading.Event()
def produce() -> None:
emitter.emit("token-1")
producer_done.set()
thread = threading.Thread(target=produce)
thread.start()
time.sleep(0.02)
assert not producer_done.is_set()
assert stream.next(timeout=1.0) == "token-1"
assert producer_done.wait(1.0)
stream._finish(None)
thread.join()
def test_emit_waits_until_consumer_receives_token() -> None:
stream = RequestStream(7, cast(RequestSession, _StreamSession()))
emitter = RequestEmitter(stream)
producer_started = threading.Event()
producer_done = threading.Event()
def produce() -> None:
producer_started.set()
emitter.emit("token-1")
producer_done.set()
thread = threading.Thread(target=produce)
thread.start()
assert producer_started.wait(1.0)
assert not producer_done.is_set()
assert stream.next(timeout=1.0) == "token-1"
assert producer_done.wait(1.0)
stream._finish(None)
thread.join()
🤖 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 `@tests/ut/py/test_worker/test_request_session.py` around lines 69 - 85, Update
test_emit_waits_until_consumer_receives_token to add a producer-entry
synchronization event set immediately before emitter.emit("token-1"), then wait
for that event before asserting producer_done is unset. Preserve the existing
token consumption, completion, stream cleanup, and thread-join assertions.

Comment on lines +85 to +131
def parse_log(log: Path) -> tuple[dict[int, list[dict]], list[dict], list[dict], list[int]]:
spans: dict[int, list[dict]] = {}
tokens: list[dict] = []
deliveries: list[dict] = []
for line in log.read_text(encoding="utf-8", errors="replace").splitlines():
if match := STRACE_RE.search(line):
spans.setdefault(int(match.group("inv")), []).append(
{
"name": match.group("name"),
"ts": int(match.group("ts")),
"dur": int(match.group("dur")),
"attrs": parse_attrs(match.group("attrs")),
}
)
if match := TOKEN_RE.search(line):
tokens.append(
{
"request": int(match.group("request")),
"seq": int(match.group("seq")),
"ts": int(match.group("ts")),
}
)
if match := DELIVERY_RE.search(line):
deliveries.append(
{
"request": int(match.group("request")),
"seq": int(match.group("seq")),
"ts": int(match.group("ts")),
}
)
request_ids = list(dict.fromkeys(token["request"] for token in tokens))
if not spans or len(request_ids) < 2:
raise ValueError("log must contain STRACE spans and tokens for at least two requests")
return spans, tokens, deliveries, request_ids


def map_host_invocations(spans: dict[int, list[dict]], request_ids: list[int]) -> dict[int, int]:
normal_invs = [inv for inv, values in spans.items() if any(span["name"] == "simpler_run" for span in values)]
prepare_invs = [
inv for inv, values in spans.items() if any(span["name"] == "simpler_prepare_request" for span in values)
]
prepare_invs.sort(
key=lambda inv: next(span["ts"] for span in spans[inv] if span["name"] == "simpler_prepare_request")
)
if len(normal_invs) != 1 or len(prepare_invs) != len(request_ids) - 1:
raise ValueError("log must contain one normal run and one Host prepare invocation per later request")
return dict(zip(request_ids, [normal_invs[0], *prepare_invs]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not infer request-to-invocation mapping from token arrival order.

request_ids preserves first token-receipt order, which is nondeterministic under concurrent execution. If B emits first, it is incorrectly assigned A’s normal invocation and every O/S window is swapped. Parse the HOST_REQUEST_PREPARED request_id=... records and map prepared invocations explicitly; the remaining request owns the normal invocation.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 131-131: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 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 `@tools/cross_request_host_device_perfetto.py` around lines 85 - 131, Update
parse_log and map_host_invocations so request-to-invocation mapping does not
depend on token arrival order. Parse HOST_REQUEST_PREPARED records and associate
each prepared request ID with its corresponding simpler_prepare_request
invocation, then assign the remaining request to the sole simpler_run
invocation. Preserve validation that exactly one normal invocation and one
prepare invocation exist for each later request, and reject missing or ambiguous
mappings.

Comment on lines +187 to +191
received = {(token["request"], token["seq"]): token["ts"] for token in tokens}
delivered = {(token["request"], token["seq"]): token["ts"] for token in deliveries}
shared_keys = received.keys() & delivered.keys()
delivery_ms = {key: (delivered[key] - received[key]) / 1_000_000.0 for key in shared_keys}
one_to_one = received.keys() == delivered.keys() and all(delay >= 0 for delay in delivery_ms.values())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Count records before claiming strict one-to-one delivery.

These dictionaries collapse duplicate receipts or deliveries by (request, seq), so duplicate events can still make strict_one_to_one true. Compare Counter values first, then use the timestamp maps for latency calculations.

Proposed duplicate validation
+from collections import Counter
 ...
+    received_counts = Counter((token["request"], token["seq"]) for token in tokens)
+    delivered_counts = Counter((token["request"], token["seq"]) for token in deliveries)
     received = {(token["request"], token["seq"]): token["ts"] for token in tokens}
     delivered = {(token["request"], token["seq"]): token["ts"] for token in deliveries}
 ...
-    one_to_one = received.keys() == delivered.keys() and all(delay >= 0 for delay in delivery_ms.values())
+    one_to_one = (
+        received_counts == delivered_counts
+        and all(count == 1 for count in received_counts.values())
+        and all(delay >= 0 for delay in delivery_ms.values())
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
received = {(token["request"], token["seq"]): token["ts"] for token in tokens}
delivered = {(token["request"], token["seq"]): token["ts"] for token in deliveries}
shared_keys = received.keys() & delivered.keys()
delivery_ms = {key: (delivered[key] - received[key]) / 1_000_000.0 for key in shared_keys}
one_to_one = received.keys() == delivered.keys() and all(delay >= 0 for delay in delivery_ms.values())
received_counts = Counter((token["request"], token["seq"]) for token in tokens)
delivered_counts = Counter((token["request"], token["seq"]) for token in deliveries)
received = {(token["request"], token["seq"]): token["ts"] for token in tokens}
delivered = {(token["request"], token["seq"]): token["ts"] for token in deliveries}
shared_keys = received.keys() & delivered.keys()
delivery_ms = {key: (delivered[key] - received[key]) / 1_000_000.0 for key in shared_keys}
one_to_one = (
received_counts == delivered_counts
and all(count == 1 for count in received_counts.values())
and all(delay >= 0 for delay in delivery_ms.values())
)
🤖 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 `@tools/cross_request_host_device_perfetto.py` around lines 187 - 191, Update
the delivery validation around received, delivered, and one_to_one to count
every (request, seq) occurrence with Counter before declaring strict one-to-one
delivery. Require the Counter values to match, while retaining the existing
timestamp-map calculation for shared-key latency and nonnegative delays.

@ChaoWao

ChaoWao commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Review 后的架构层反馈:建议先对齐方案,再拆分

先说结论:这个 PR 能达成"O/S 跨 token 流水线 + 跨请求重叠"的目标,设计很用心。但核心并发机制偏复杂(独立控制通道 admission lane + 显式 prepare/execute 两阶段 + host-orch 两级门控),core 又有 3300+ 行,建议在拆分前先就整体方案对齐。

1. 一个关键观察:当前方案只拿到 O∥S,没拿到 S∥S

chip_process 主循环是单槽 mailbox 的同步 RPC:收到 TASK_READY 就同步调 run_from_blob / execute_prepared,直到 NPU 执行完才返回(worker.py:1539/1541)。正因为主循环被 A 的 device 执行阻塞,才需要 admission 线程这条旁路去收 B 的 prepare。

execute_prepared 同样阻塞主循环,所以两个 Device S 仍是串行的——正如 PR 描述里的流水线图,B 的 S_Bwait A 让出主循环:

A bank0:  O_A | S_A -----|
B bank1:       O_B | wait | S_B

也就是说,当前这套 admission lane + 两阶段 API,换来的只是 O_B ∥ S_A,并没有 S_A ∥ S_B

2. 一个更简洁的替代方案:常驻消息线程 + run 派工作线程

把主循环改成"收到 TASK_READY 就把 run 派到工作线程,立即返回继续收消息",在同一条 mailbox 上做多路复用即可:

  • 消除独立控制通道(prepare 就是"另一个 run",走同一条 mailbox);
  • 消除显式 prepare/execute 拆分(两个完整 run 在两个工作线程跑,O_B ∥ S_A 成了线程调度的自然副产品,不必应用层显式编排两阶段);
  • 顺带拿到 S∥S 真并发(两个工作线程 + 两个 arena bank)。

代价集中在一处:mailbox 从"单槽同步"升级为"多槽 / 异步完成"(派线程后不能立即写 TASK_DONE,否则 parent 误判完成),且 parent 侧 submit_next_level 要一并非阻塞化。这是核心 ABI 改动,波及面 ≥ 当前的 admission lane——所以它属于"应该先对齐"的头号决策,而不是先动手。

需要权衡的一点:显式 prepare/execute 能精确控制"只跑 O、暂不点火 S"(对分阶段 profiling 更可控);改成完整 run 并发后,O/S 的重叠程度交给线程调度。需要确认产品是否真的需要这种精确控制,还是"自然重叠"就够。

3. 一个统一模型:bank 跟着线程走

select_arena_bank 已经是 thread-local。顺此可得一条统一规则:谁并发谁在不同线程 → 自然落到不同 bank:

场景 执行线程 bank 并发
L2 SceneTest(直连 L2 Worker) 用户单线程 恒 bank 0 无(串行,正确性验证)
L2 作为 L3 child chip_process 派的工作线程 i bank i
L3 多请求 多个 dispatcher → child 工作线程

唯一需要的小改动:ChipWorker::run 现在写死 acquire_arena_bank(0),应改成读"当前工作线程分到的 bank"。这样 L2 SceneTest 这条最需要稳定的路径零改动、零风险(单线程恒 bank 0),并发只在经 mailbox 的 child 场景启用。

4. 两个必须先修的死锁(与并发触发方式正交,走哪个方案都要修)

  • arena bank 永久泄漏chip_worker.cpp:388-417:prepare 成功后若请求被取消 / 报错、execute_prepared 从未被调用,则 prepared_bank_busy_[bank] 永不释放,下一次同 bank 的 prepare 卡死在 prepared_cv_.wait,复用 request_id 也会抛错。根因是缺一个 discard_prepared / abort 语义。
  • close() 挂起request_session.py:257 + 401-408:_emitconsumed.wait() 无超时;消费者放弃 stream(不再 next() 也不 cancel())后,dispatcher 线程永久停在 _emit,close() 直接 join → context-manager 退出永久挂起(可能卡死 CI)。close() 应先结束 / 取消存活流,再入队 _STOP 并 join。

二者当前都没有回归测试(test_request_session.py 覆盖了 emit-阻塞 与 两并发,但未覆盖这两个拆解 / 取消场景)。

5. 关于拆分

方案对齐后再拆分会自然很多。core 3300+ 行、跨四个可分离机制(arena banks / prepare-execute ABI / epoch 协议 / 并发 Orchestrator+RequestSession)。建议先合"行为等价的底座"(HostApi 线程上下文、scheduler total_tasks_ 原子化 + orchestration_done_ 闩锁、arena banks),再合把它们编织起来的上层。若采用第 2 点的方案 B,prepare/execute ABI 与 admission lane 这一整块可以直接省掉。

建议先就以上(尤其第 2 点 mailbox 是否多槽化)写一份简短 design-note 对齐,再决定拆分边界。

@Crane-Liu
Crane-Liu force-pushed the 20260715_hostpipeline branch from 34d344c to d94da9b Compare July 21, 2026 13:12
@Crane-Liu

Copy link
Copy Markdown
Contributor

@ChaoWao The branch has been rebased onto the current upstream main and updated with the architecture-aligned implementation discussed in the review.

Architecture update:

  • Keep one resident message-processing thread with a bounded two-credit mailbox.
  • A request is admitted as one complete run. Two independent run slots each own their ChipWorker, DeviceRunner, and runtime state; there is no split admission/prepared-callable ABI.
  • Host O for request B can overlap the streaming S phase of request A. Device S launch remains serialized by a process-wide gate because the AICPU executor, register/affinity state, and DFX state are process-global.
  • RequestSession is drained and closed before native Worker teardown, and the rebased code now follows the upstream lifecycle_flags state model.

Rebase compatibility fix:

  • Upstream eager Worker.init() forks the chip process before the manual concurrent case creates request tensors. The custom concurrent test now rehosts both request argument sets with Worker.create_host_buffer via _RehostedTaskArgs, then releases them only after token and output validation. This fixes the invalid post-fork host pointers that previously stalled at the first H2D copy.

Validation on a2a3 hardware:

  • Architecture probe: task_20260721_051324_104514031011, passed.
  • Focused Python request/lifecycle tests: 73 passed; rehost tests: 5 passed.
  • SmallThreeLayerStreaming: task_20260721_060741_233825812932, passed.
  • ConcurrentTwoRequestOverlapProfile with trace: task_20260721_060812_235056723769, passed; both requests delivered token sequences 1/2/3 and output comparison passed.
  • Three fresh-process stability rounds: task_20260721_060928_238272817622, 3/3 passed.
  • Final committed SHA d94da9b: task_20260721_061059_24220893007, passed.

No simulator run was performed in this iteration, per the agreed scope; GitHub sim CI remains the final automatic regression step. The final Python file passes all applicable pre-commit hooks. The broader clang-tidy hook is still blocked before analysis by a stale absolute simpler_setup path in the cluster environment; the other relevant hooks pass.

@Crane-Liu
Crane-Liu force-pushed the 20260715_hostpipeline branch from 79c286f to 6733422 Compare July 21, 2026 15:13
@Crane-Liu

Copy link
Copy Markdown
Contributor

CI follow-up for the current head 67334226e16f547694ba582aa42ef3f29ccbfe1f:

  • Fixed the Ubuntu UT failure by initializing MAILBOX_PROTOCOL_MAGIC_VERSION in the host-buffer white-box mailbox fixture.
  • Preserved native runtime thread affinity on single-slot platforms: single-slot run_from_blob executes on the chip main thread, while background run threads remain limited to the two-slot a2a3 HostGraph path. Added a regression test for this boundary.
  • Fixed the A5 multi-rank close failures. The parent used a fixed 10 s child reap grace even after establishing an HCCL communicator, while native comm_destroy may legally spend up to 120 s in coordinated teardown. Communicator-backed workers now receive a 130 s reap grace; ordinary workers retain the 10 s bound. Added a test proving the timeout is selected only when a comm base is active.

Validation:

  • CI-equivalent CPU UT: 670 passed, 10 deselected.
  • Focused lifecycle/worker UT: 126 passed.
  • a2a3 two-rank allreduce teardown: task_20260721_075320_415293317056, passed.
  • a2a3 two-request HBG concurrency: task_20260721_073831_396309820879, passed.
  • GitHub CI: all 15 check runs passed, including ut-a5, st-onboard-a5, Ubuntu/macOS UT, all sim matrices, both packaging matrices, profiling, and pre-commit.

The first macOS packaging retry encountered a runner-level OSError: [Errno 5] Input/output error after successfully building the wheel. The same tree was retriggered via a no-content amend because this fork token cannot invoke the upstream admin-only rerun API; the clean retry passed all five packaging modes.

@Crane-Liu

Copy link
Copy Markdown
Contributor

Rebased this PR onto main at c032e07e and updated the implementation following the architecture review.

Architecture update

  • Keep one ChipWorker / DeviceRunner; concurrent requests are pipelined through one runner instead of creating competing device runners.
  • Drive slot/arena ownership through PipelineContract and explicit FILL / REUSE / EXEC states.
  • HBG uses two epoch metadata slots. TMR uses two TaskArg slots while retaining one arena, matching each backend's actual overlap boundary.
  • Add TASK_ACCEPTED, publication/run gates, per-runner admission, and contract-derived endpoint credits. The process-global device mutex is removed.
  • Preserve request/token identity in profiling so every request's three tokens have distinct Host O and Device S spans.

Latest-main compatibility

The rebase includes the polling-completion scheduler and removal of legacy graph-output metadata from current main. The HBG streaming path now:

  • stages descriptors, payloads, slot states, and per-slot completion flags for each published epoch;
  • keeps dependencies as position-independent fanin_local_ids, with no dep-pool/fanout reconstruction;
  • classifies each published task on device and either routes it ready or registers it on the first incomplete producer, including cross-epoch dependencies;
  • keeps the target whole-graph image alive while reclaiming only the source capture range after upload, so graph construction can reuse its temporary heap;
  • removes the deleted dep-pool and graph-output layout/API usage.

Validation on commit 8509dec0

  • Full editable C++/Python build: passed.
  • Python worker and request-session tests: 71 passed.
  • C++ non-hardware tests: 60/60 passed.
  • Real hardware TMR two-request pipeline: task_20260723_011726_346414924655, passed.
  • Real hardware HBG two requests x three tokens: task_20260723_011621_34344002117, passed; all three epochs and cross-epoch polling/wake paths completed correctly.

I did not manually run the simulator suite; the GitHub sim jobs are left as the final automatic regression stage.

@Crane-Liu

Copy link
Copy Markdown
Contributor

CI follow-up: the first automatic pre-commit run failed only because clang-format rewrote four C++ files; every other hook, including clang-tidy, cpplint, Ruff, and Pyright, passed.

The exact formatter output is applied in 95f25827 on Crane-Liu:20260715_hostpipeline. A full editable build and the 71 worker/request-session tests pass on that commit. Its parent 8509dec0 is the real-hardware-tested implementation described above; 95f25827 changes formatting only.

GitHub currently reports an Actions partial outage and degraded Webhooks/Issues service, so PR 1379's displayed head is temporarily lagging behind the fork branch. I am leaving the GitHub sim suite to rerun automatically when the synchronize event is processed.

@Crane-Liu
Crane-Liu force-pushed the 20260715_hostpipeline branch from a3c2b2a to 95f2582 Compare July 23, 2026 08:51
@Crane-Liu

Copy link
Copy Markdown
Contributor

Fixed the remaining macOS st-sim-a2a3 failure in 22dc243f.

Root cause: the two contract-driven mailbox slots can enter the same a2a3 sim DeviceRunner concurrently. The onboard runner already has Gate B around DeviceRunner::run(), but the sim runner did not. Both sim calls consequently mutated shared per-run state (KernelArgs, executor symbols, collectors, and temporary SO handles), which explains the native thread faults followed by the child_memory, dual_domain_overlap, and per_task_runtime_env hangs until the 600 s job timeout.

The fix adds the missing per-runner Gate B to a2a3 sim. Host O/binding remains outside this lock, so the intended O||S pipeline is preserved; only the shared Device S runner state is serialized, matching onboard semantics.

Validation on exact head 22dc243f:

  • editable build passed;
  • worker/request-session Python UT: 71 passed;
  • C++ non-hardware suite: 60 passed;
  • targeted check-headers, clang-format, and cpplint passed;
  • real a2a3 hardware via task-submit task task_20260723_043018_188591123277:
    • HBG concurrent two-request, three-token streaming case passed;
    • TMR concurrent two-request pipeline case passed.

Per the current validation plan, I did not run sim manually; the refreshed GitHub sim jobs are the automatic regression check for this fix.

Crane-Liu added 10 commits July 23, 2026 04:41
- publish host graph epochs for overlapped device execution and preserve a final epoch for ordinary orchestrations
- add host request admission, prepared request execution, and concurrent Worker run support
- integrate current L3-L2 direct-region behavior and add tracing, documentation, and hardware coverage
- fix CI formatting, lint, type checking, copyright, and current-main compatibility issues
@Crane-Liu
Crane-Liu force-pushed the 20260715_hostpipeline branch from 22dc243 to 7fd379f Compare July 23, 2026 11:48
@Crane-Liu

Copy link
Copy Markdown
Contributor

CI follow-up after rebasing onto current main (d0bc661a, #1441):

  • Fixed the clean Linux build failure in a2a3sim/host_build_graph. Refactor: consolidate per-task markers into one TaskAttrs byte (a2a3/hbg) #1441 consolidated allow_early_resolve, sync-start, predicate, and timing markers into TaskAttrs, while this PR still wrote the removed standalone field during streaming graph materialization.
  • The materializer now copies the full source_slot.task_attrs to the target slot, then clears only early_resolve. This preserves sync-start, predicate, and selective timing semantics while retaining the original safety rule for materialized epochs.
  • Kept the earlier a2a3 sim DeviceRunner Gate B fix that serializes shared simulator device state without serializing Host O binding.

Validation on commit 7fd379f900c0f837971b678a0f563977954a1631:

  • clean a2a3sim runtime build: passed (compile only; no manual simulation)
  • targeted headers / clang-format / cpplint: passed
  • Python worker/session regression: 97 passed
  • C++ non-hardware regression: 60 passed
  • real a2a3 hardware via task-submit: HBG ConcurrentTwoRequestOverlapProfile and TMR ConcurrentTwoRequestPipeline both passed (task_20260723_044642_24204513226)

The branch was rebased and force-updated with lease. A fresh GitHub Actions run should now validate the merge result.

@Crane-Liu

Copy link
Copy Markdown
Contributor

Follow-up fix for the remaining macOS st-sim-a2a3 timeout (test_per_task_runtime_env):

  • Root cause: TMR advertises 2 pipeline slots but has a single PTO_PIPELINE_REUSE_MEM runtime image. The test submits three distinct RuntimeEnv configurations, so a later bind could rebuild the shared image while an earlier device run was still consuming it.
  • Fix (170332d2): add a resource-contract admission gate in ChipWorker. Runs with the same raw RuntimeEnv key may still overlap; an incompatible key waits until active users of the shared reuse image drain. The gate activates only when arena_banks < pipeline_slots and a reuse-memory resource exists, so HBG is unaffected.

Validation at exact commit 170332d25b3f43d5f2a3edce1ce0b5bd49485d41:

  • targeted header / clang-format / cpplint checks: passed
  • Python worker/session tests: 97 passed
  • non-hardware C++ tests: 60 passed
  • runtime builds: a2a3 and a2a3sim passed (no manual sim execution)
  • real A2/A3 hardware task task_20260723_051339_324193614441: HBG two-request overlap passed; TMR same-config two-request pipeline passed; per_task_runtime_env completed all three distinct-ring tasks with max error 0.000e+00

The prior A5 failures appear runner-infrastructure-related (rtSetDevice reports available range [0,0) and the pytest cache path disappeared). The current token cannot rerun individual jobs because GitHub requires admin rights.

@Crane-Liu

Copy link
Copy Markdown
Contributor

CI follow-up for run 30006827972:

  • pre-commit passed on retry. The preceding run had restored a zero-byte A5 compile database and then lost the CMake working directory during cache recovery; 154f8a0a is an empty retry commit whose tree is identical to hardware-tested 170332d2.
  • Both Ubuntu and macOS st-sim-a2a3 passed. On macOS, the formerly hanging test_per_task_runtime_env completed in 7.4s, and the full scene-test step completed successfully. This confirms the shared reuse-image admission gate fixes the original timeout.
  • All other hosted UT, simulation, packaging, and profiling jobs passed.

The remaining red jobs are self-hosted hardware failures:

  • st-onboard-a5 and ut-a5: assigned devices 2-5 all fail rtSetDevice with 107001; the same runner also loses /tmp/pytest_cache/uid-0. This is the same no-device runner failure seen in the previous run.
  • st-onboard-a2a3: the dedicated dep-gen smoke had one tasks[] missing expected ids: {0} result. The exact PR head passed the same real-hardware test once in task_20260723_053802_396892722008, then 10/10 repeated runs in task_20260723_053854_398867410868, so this is not reproducible on the cluster and there is no evidence for another business-code change.

The current token receives HTTP 403 when rerunning individual jobs. A maintainer needs to restore the A5 device allocation and rerun these three self-hosted jobs.

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.

3 participants