Skip to content

Add: dispatch MPI worker groups through mailbox - #1624

Merged
ChaoWao merged 2 commits into
hw-native-sys:mainfrom
sunkaixuan2018:use-mpi-and-remove-socket
Aug 14, 2026
Merged

Add: dispatch MPI worker groups through mailbox#1624
ChaoWao merged 2 commits into
hw-native-sys:mainfrom
sunkaixuan2018:use-mpi-and-remove-socket

Conversation

@sunkaixuan2018

@sunkaixuan2018 sunkaixuan2018 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the MPI worker group's Simpler command/health TCP path with one L4-owned named shared-memory mailbox attached by local rank 0
  • distribute ordered task and control envelopes with dedicated MPI communicators, including group, directed-rank, and per-rank payload semantics
  • gather ranked results and errors before completing the mailbox request, with terminal timeout and complete mpirun process-group cleanup
  • keep ordinary non-MPI Remote L3 workers on the existing TCP transport
  • route full-group Global CommDomain setup through the mailbox and a separate domain communicator
  • add protocol, lifecycle, compatibility, timeout, and compute/TLOAD smoke coverage

Review-round hardening (87489e1): the mailbox state words now use the _task_interface acquire/release helpers unconditionally (the previous import path silently fell back to plain struct reads), rank 0 parks on a shared futex instead of busy-polling the request lane, reserved header bytes [80, 256) are validated as zero with a documented version-evolution rule, a _mpi_mailbox_layout binding plus unit test pins the Python and C++ layout declarations together, gathered rank errors stay valid JSON under truncation, and the test-only payload-overwrite API is removed.

Dependency

Stacked on #1623, which is now merged; this branch is rebased onto its merge commit.

Testing

  • CI green on the mailbox head cd51a88, including st-pod-onboard-a2a3 — the two-machine test_global_tload_mpirun_l3 example runs the mailbox path end to end (PASS 15.0s, devices=[12, 13])
  • full tests/ut/py on Linux: 1440 passed, 13 skipped (includes this PR's 46 mailbox/MPI tests)
  • changed-file header, English-only, EOF, whitespace, Markdownlint, clang-format, cpplint, Ruff, and Pyright checks
  • CI revalidation of the review-fix head 87489e1: 18 checks green, including st-pod-onboard-a2a3 re-running test_global_tload_mpirun_l3 on the futex-park path (PASS 15.1s, devices=[12, 13])

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Global CommDomain contracts and backends

Layer / File(s) Summary
Domain protocol and lifecycle
python/simpler/global_comm_domain.py, src/common/platform_comm/comm.h, src/common/platform_comm/comm_sim.cpp, src/a2a3/..., src/a5/...
Adds validated wire formats, capability checks, prepare/import/release APIs, simulation support, A3 Fabric V2 support, and unsupported A5 stubs.
MPI mailbox transport
python/simpler/mpi_group_mailbox.py, python/simpler/mpi_l3_session.py, src/common/hierarchical/mpi_group_mailbox.h, src/common/hierarchical/remote_endpoint.*
Adds shared-memory mailbox states, grouped task routing, MPI dispatch, timeout handling, terminal errors, shutdown, and remote-domain control forwarding.
Worker and orchestration integration
python/simpler/worker.py, python/simpler/orchestrator.py, python/simpler/remote_l3_session.py, python/simpler/task_interface.py
Adds domain allocation, import, commit, copy, release, MPI group registration, manifest generation, worker routing, cleanup, and public handle/view APIs.
Bindings and smoke flows
python/bindings/*, python/simpler/global_comm_smoke.py, tools/mpi_*, tools/a3_l4_tcp_smoke/*
Adds Python bindings, rank-local orchestration callbacks, MPI mailbox smoke tests, and A3 compute/TLOAD validation flows.
Tests and records
tests/ut/*, docs/*, task.md
Adds protocol, lifecycle, failure, cleanup, mailbox, MPI activation, end-to-end transfer, and implementation-record coverage.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Orchestrator
  participant Worker
  participant Mailbox
  participant MpiDispatcher
  participant L3Rank
  Orchestrator->>Worker: allocate_global_domain
  Worker->>Mailbox: publish domain control
  Mailbox->>MpiDispatcher: dispatch ordered request
  MpiDispatcher->>L3Rank: prepare or import domain
  L3Rank-->>MpiDispatcher: descriptor or context result
  MpiDispatcher-->>Mailbox: publish completion
  Mailbox-->>Worker: return control response
  Worker-->>Orchestrator: commit domain view
Loading

Poem

A rabbit hops through domains bright,
Descriptors pass from rank to rank.
Mailboxes guide the MPI flight,
Windows open with careful hands.
Copy, commit, release, then rest—
The new pathways pass their test.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.94% 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.
Description check ✅ Passed The description clearly explains mailbox-based MPI worker dispatch, transport behavior, lifecycle handling, and test coverage.
Title check ✅ Passed The title clearly summarizes the main change: dispatching MPI worker groups through a mailbox.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

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

🧹 Nitpick comments (19)
python/simpler/global_comm_smoke.py (1)

22-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared prologue and tensor builders.

The four callbacks repeat the same steps: validate scalar_count() == 6, import get_inner_handle, decode the digest, and look up the domain. The group variants then repeat the body of their single-worker counterparts inside a loop. Two small helpers, one for the prologue and one per kernel argument shape, would remove most of this duplication and keep the four entry points to a few lines each.

♻️ Sketch
+def _resolve(orch, args: TaskArgs, message: str):
+    from .remote_l3_session import get_inner_handle  # noqa: PLC0415
+
+    if args.scalar_count() != 6:
+        raise ValueError(message)
+    return (
+        get_inner_handle(_digest_from_scalars(args, 2).hex()),
+        orch.get_global_domain(int(args.scalar(0))),
+        int(args.scalar(1)),
+    )
+
+
+def _compute_args(context) -> TaskArgs:
+    chip_args = TaskArgs()
+    for buffer_name in ("lhs", "rhs"):
+        chip_args.add_tensor(_domain_tensor(context, buffer_name), TensorArgType.INPUT)
+    chip_args.add_tensor(_domain_tensor(context, "input"), TensorArgType.OUTPUT_EXISTING)
+    return chip_args
🤖 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/global_comm_smoke.py` around lines 22 - 167, Refactor
remote_compute_orch, remote_rank_orch, remote_compute_group_orch, and
remote_rank_group_orch to share a helper for importing get_inner_handle,
validating the six scalars, decoding the digest, and resolving the domain. Add
reusable tensor-argument builders for the compute and TLOAD shapes, then have
the group callbacks reuse the corresponding single-worker argument construction
inside their loops while preserving worker selection and submission behavior.
tools/a3_l4_tcp_smoke/mpirun_compute_then_tload_2x2_smoke.py (1)

51-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: use iterable unpacking instead of concatenation.

Static analysis flags list(include_dirs) + [str(...)] on line 53. Use unpacking for a more idiomatic construction.

♻️ Proposed fix
-    kernel_include_dirs = list(include_dirs) + [str(compiler.project_root / "src" / "common")]
+    kernel_include_dirs = [*include_dirs, str(compiler.project_root / "src" / "common")]
🤖 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/a3_l4_tcp_smoke/mpirun_compute_then_tload_2x2_smoke.py` around lines 51
- 60, Update the kernel_include_dirs construction in _compile_aiv to use
iterable unpacking when combining include_dirs with the common source directory,
preserving the existing ordering and values.

Source: Linters/SAST tools

python/simpler/mpi_l3_session.py (1)

504-513: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Replace the assert on the shutdown payload with an explicit check.

Line 507 uses assert payload is not None. Python removes asserts under -O, and _rewrite_frame_identity would then fail on None. Raise an explicit error instead.

🤖 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/mpi_l3_session.py` around lines 504 - 513, Replace the assert
in the MailboxOpcode.SHUTDOWN branch of the request handling flow with an
explicit payload None check that raises an appropriate error before calling
_rewrite_frame_identity; preserve the existing shutdown behavior when payload is
present.
tools/mpi_group_mailbox_smoke.py (1)

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

Add a sleep to the wait helper.

_wait_until re-evaluates the predicate with no pause. Each mailbox state read maps to a shared-memory load plus an import lookup, so this pins a core for the whole wait. Sleep for a short interval between checks.

♻️ Proposed fix
 def _wait_until(predicate, *, deadline: float, label: str) -> None:
     while not predicate():
         if time.monotonic() >= deadline:
             raise TimeoutError(f"timed out waiting for {label}")
+        time.sleep(0.001)
🤖 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/mpi_group_mailbox_smoke.py` around lines 34 - 37, Update _wait_until to
pause briefly between predicate evaluations, while retaining the existing
deadline check and TimeoutError behavior; add the sleep inside the loop after a
failed predicate check.
tests/ut/py/test_global_comm_domain.py (1)

259-278: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer allocated ports over the fixed 19073 + index range.

The test never binds these endpoints, so it passes today. The file already provides _free_tcp_ports. Using it removes the fixed range and keeps the endpoint construction consistent across the file.

🤖 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_global_comm_domain.py` around lines 259 - 278, Update
_failure_injection_worker to obtain two ports through the existing
_free_tcp_ports helper, then build each RemoteWorkerSpec endpoint from those
allocated ports instead of the fixed 19073 + index range. Preserve the current
node ordering and endpoint construction format.
tests/ut/cpp/hierarchical/test_remote_endpoint.cpp (1)

659-661: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound the state wait so a regression fails instead of hanging.

Line 661 spins until RequestState::REQUEST_READY with no deadline. Line 697 uses the same pattern. If exchange_group_task stops publishing a request, the test hangs and the CI job times out with no diagnostic. Add a deadline and FAIL() when it expires.

♻️ Proposed change for the helper
-void respond_with_payloads(std::vector<uint8_t> &mailbox, const std::vector<std::vector<uint8_t>> &payloads) {
+void wait_for_request_ready(const std::vector<uint8_t> &mailbox) {
+    using namespace mpi_group_mailbox;
+    const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
+    while (mailbox_state(mailbox, OFF_REQUEST_STATE) != static_cast<int32_t>(RequestState::REQUEST_READY)) {
+        ASSERT_LT(std::chrono::steady_clock::now(), deadline) << "mailbox request was never published";
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+}
+
+void respond_with_payloads(std::vector<uint8_t> &mailbox, const std::vector<std::vector<uint8_t>> &payloads) {
     using namespace mpi_group_mailbox;
-    while (mailbox_state(mailbox, OFF_REQUEST_STATE) != static_cast<int32_t>(RequestState::REQUEST_READY)) {}
+    wait_for_request_ready(mailbox);
🤖 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/cpp/hierarchical/test_remote_endpoint.cpp` around lines 659 - 661,
Bound the polling loops in both respond_with_payloads and the matching wait near
exchange_group_task with a deadline; when RequestState::REQUEST_READY is not
observed before expiration, call FAIL() with a diagnostic, while preserving the
existing behavior when the state becomes ready.
python/simpler/mpi_group_mailbox.py (3)

320-329: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Truncated error JSON becomes unparsable.

If the encoded rank errors exceed MAILBOX_ERROR_BYTES, line 324 cuts the JSON mid-document. read_result then fails json.loads and falls back to a raw replacement decode, so the rank attribution is lost. Truncate each message before encoding instead, so the document stays valid.

♻️ Proposed fix
-        data = json.dumps([asdict(error) for error in errors], sort_keys=True).encode("utf-8")
-        if len(data) > MAILBOX_ERROR_BYTES:
-            data = data[: MAILBOX_ERROR_BYTES - 1]
+        entries = [asdict(error) for error in errors]
+        data = json.dumps(entries, sort_keys=True).encode("utf-8")
+        while len(data) > MAILBOX_ERROR_BYTES and entries:
+            budget = max(0, len(entries[-1]["message"]) // 2)
+            if budget == 0:
+                entries.pop()
+            else:
+                entries[-1]["message"] = entries[-1]["message"][:budget]
+            data = json.dumps(entries, sort_keys=True).encode("utf-8")
+        data = data[:MAILBOX_ERROR_BYTES]
🤖 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/mpi_group_mailbox.py` around lines 320 - 329, Update the error
serialization flow in the mailbox failure-writing method so oversized data
remains valid JSON. Truncate individual rank-error message fields before
`json.dumps`, then encode and write the complete serialized document without
slicing the encoded JSON at `MAILBOX_ERROR_BYTES`; preserve rank attribution and
the existing state updates.

345-357: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Narrow the exception scope around the error decode.

Line 353 catches BaseException, so a KeyboardInterrupt or SystemExit raised during the decode is converted into a plain message. Catch the decode and lookup errors only. Ruff also reports BLE001 here.

♻️ Proposed fix
-            except BaseException:
+            except (ValueError, TypeError, KeyError, UnicodeDecodeError):
                 message = raw.decode("utf-8", errors="replace") or "MPI group request failed"
🤖 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/mpi_group_mailbox.py` around lines 345 - 357, The TASK_FAILED
error decoding in the mailbox request handling must not catch control-flow
exceptions. Narrow the try/except around json decoding and entry field access to
the specific decode and lookup/type errors that can occur, replacing the broad
BaseException handler so Ruff BLE001 is resolved while preserving the
raw-message fallback.

Source: Linters/SAST tools


373-375: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the test-only mutator out of the shipped protocol class.

overwrite_request_payload_for_test writes arbitrary bytes into the request region with no state or capacity check. It is reachable in production. The single caller is tests/ut/py/test_mpi_group_mailbox.py line 96. Write through mailbox._buffer from the test, or guard the helper with a length check against MAILBOX_PAYLOAD_BYTES.

🤖 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/mpi_group_mailbox.py` around lines 373 - 375, The test-only
method overwrite_request_payload_for_test should not remain exposed on the
shipped mailbox protocol class. Remove it and update the sole caller in
test_mpi_group_mailbox.py to write the payload directly through mailbox._buffer,
or otherwise enforce MAILBOX_PAYLOAD_BYTES capacity before writing if the helper
must remain.
tests/ut/py/test_mpi_group_mailbox.py (1)

25-211: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a capacity-guard case to this suite.

The suite covers targets, sequencing, failure, and shutdown. It does not cover the capacity guard in _encode_payloads (MAILBOX_PAYLOAD_BYTES) or the truncation path in fail_request. Both are wire-protocol limits. Add one test that writes an oversized payload vector and expects ValueError.

🤖 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_mpi_group_mailbox.py` around lines 25 - 211, Add a test
covering the mailbox payload capacity guard by attempting to write a request
whose encoded payloads exceed MAILBOX_PAYLOAD_BYTES and asserting ValueError.
Exercise the write_request path and ensure the oversized payload vector is
rejected before acceptance or state progression.
tools/mpi_l3_group_smoke.py (1)

41-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Restore SIMPLER_MPI_SMOKE_DIR after the run.

Line 42 sets the variable and never removes it. The temporary directory is deleted when the with block exits, so the variable then points at a missing path. If run is ever called from another module or twice, the stale value leaks. Use try/finally or os.environ.pop at the end of the block.

🤖 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/mpi_l3_group_smoke.py` around lines 41 - 43, Restore the
SIMPLER_MPI_SMOKE_DIR environment variable after the temporary-directory run in
the surrounding run flow: save any prior value before assigning output_dir, then
restore it in a finally block (or remove it when absent) so repeated or nested
calls never retain the deleted path.
python/simpler/worker.py (2)

3515-3523: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unreachable node-identity fallback.

_build_remote_manifest runs only for ids in self._remote_worker_ids, and _remote_like_worker_ids() is the union of _remote_worker_ids and _mpi_worker_ids. The condition at Line 3515 is therefore always true, so lines 3520-3523 never execute. Drop the branch, or state the caller contract that makes it reachable.

♻️ Proposed simplification
-        if worker_id in self._remote_like_worker_ids():
-            runtime = self._resolved_global_nodes()[int(worker_id)]
-            node_rank = runtime.node_rank
-            node_count = runtime.node_count
-            global_device_ranks = runtime.global_device_ranks
-        else:
-            node_rank = 0
-            node_count = 1
-            global_device_ranks = spec.global_device_ranks or tuple(range(len(spec.device_ids)))
+        runtime = self._resolved_global_nodes()[int(worker_id)]
+        node_rank = runtime.node_rank
+        node_count = runtime.node_count
+        global_device_ranks = runtime.global_device_ranks
🤖 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 3515 - 3523, Remove the unreachable
else fallback in _build_remote_manifest and rely directly on the runtime values
from _resolved_global_nodes()[int(worker_id)] for node_rank, node_count, and
global_device_ranks. Preserve the existing remote-worker caller contract and
eliminate the redundant _remote_like_worker_ids() condition.

7295-7310: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind the release to the run that allocated the handle.

_release_global_domain_handle reads self._building_run_resources, so the pending-release queue it appends to is the run whose graph is being built at release time, not the run that allocated the handle. The local path avoids this: _allocate_domain captures the owning _RunResources in the _release_fn closure. For a retain_after_run=True domain released inside a later run, the fence that frees it is that later run's fence, and no queue entry exists on the allocating run. Double free is prevented by _release_global_domain_now memoization, so this is an ordering concern, not a corruption. Bind the owning resources at allocation for symmetry with _release_domain_handle.

♻️ Proposed binding at allocation
-            _release_fn=self._release_global_domain_handle,
+            _release_fn=lambda released, owner=resources: self._release_global_domain_handle(released, owner),

Then accept the owning resources explicitly:

-    def _release_global_domain_handle(self, handle: GlobalCommDomainHandle) -> None:
+    def _release_global_domain_handle(
+        self, handle: GlobalCommDomainHandle, resources: _RunResources | None = None
+    ) -> None:
         if self._worker is None:
             return
-        resources = self._building_run_resources
🤖 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 7295 - 7310, Bind each global domain
handle to its allocating _RunResources when created, mirroring
_allocate_domain’s _release_fn closure. Update _release_global_domain_handle to
accept and use the owning resources rather than reading
self._building_run_resources, so pending releases and fences are associated with
the allocating run; preserve the existing cleanup and _release_global_domain_now
memoization behavior.
python/simpler/orchestrator.py (1)

432-441: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct the docstring: MPI groups are also supported.

The summary line says "without MPI", and the body lists only Worker.add_worker and Worker.add_remote_worker. Worker._allocate_global_domain also routes a complete MPI group through _mpi_group_control, and docs/comm-domain.md documents add_mpirun_worker_group members. Update the text so users of MPI groups find this API.

📝 Proposed docstring fix
-        """Create a CommDomain across local and/or remote L3 nodes without MPI.
+        """Create a CommDomain across local, remote, and MPI-launched L3 nodes.
 
         Each member is ``(l3_worker_id, local_l2_worker_id)``. The L3 worker
-        may have been registered by ``Worker.add_worker`` or
-        ``Worker.add_remote_worker``. L4 collects every L2 export descriptor,
+        may have been registered by ``Worker.add_worker``,
+        ``Worker.add_remote_worker``, or ``Worker.add_mpirun_worker_group``.
+        L4 collects every L2 export descriptor,
🤖 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 432 - 441, Update the docstring
for the CommDomain creation method to state that MPI groups are supported,
removing the “without MPI” limitation and mentioning MPI group registration
alongside Worker.add_worker and Worker.add_remote_worker. Ensure the member
description reflects add_mpirun_worker_group usage while preserving the existing
lifecycle and commit behavior documentation.
src/common/platform_comm/comm_sim.cpp (1)

199-216: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider suppressing implicit moves on GlobalDomainAllocation.

GlobalDomainAllocation owns local_base and shm_name as raw members and defines a destructor that releases both. The implicitly generated move constructor copies local_base and leaves the source pointer non-null, so a moved-from object would munmap the same address again. The current code always stores the allocation in a std::unique_ptr and never moves it, so no live path is affected. Deleting the copy/move operations, or reusing GlobalPeerMapping for the local mapping, removes the hazard for future changes.

♻️ Optional hardening
 struct GlobalDomainAllocation {
+    GlobalDomainAllocation() = default;
     ~GlobalDomainAllocation() {
         if (local_base != nullptr) {
             munmap(local_base, mapping_size);
         }
         if (!shm_name.empty()) {
             shm_unlink(shm_name.c_str());
         }
     }
+    GlobalDomainAllocation(const GlobalDomainAllocation &) = delete;
+    GlobalDomainAllocation &operator=(const GlobalDomainAllocation &) = delete;
+    GlobalDomainAllocation(GlobalDomainAllocation &&) = delete;
+    GlobalDomainAllocation &operator=(GlobalDomainAllocation &&) = delete;
🤖 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 `@src/common/platform_comm/comm_sim.cpp` around lines 199 - 216, Make
GlobalDomainAllocation non-copyable and non-movable by explicitly deleting its
copy and move constructors and assignment operators, preventing duplicated
ownership of local_base and shm_name while preserving its current
unique_ptr-based usage.
src/common/worker/chip_worker.h (1)

161-165: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the new public global-domain APIs.

Every neighbouring comm_* declaration in this header carries a doc comment that states the ownership and pairing contract. The three new declarations carry none. State what comm_global_domain_prepare returns (descriptor bytes, local window base, actual mapping size), that the actual mapping size can exceed the requested window_size, that comm_global_domain_import requires a rank-ordered complete table, and that comm_global_domain_release pairs with prepare and also runs from finalize().

♻️ Suggested doc comment
+    /// Global CommDomain lifecycle (L4-brokered, independent of the
+    /// comm_init sessions above).  `prepare` creates this rank's local window
+    /// and returns (descriptor_bytes, local_window_base, mapping_size); the
+    /// mapping size may exceed `window_size` after backend alignment.
+    /// `import` takes the complete rank-ordered descriptor table and returns
+    /// the device CommContext.  `release` pairs with `prepare` and is also
+    /// driven for every tracked domain by `finalize()`.
     std::tuple<std::vector<uint8_t>, uint64_t, size_t> comm_global_domain_prepare(
🤖 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 `@src/common/worker/chip_worker.h` around lines 161 - 165, Document the public
APIs comm_global_domain_prepare, comm_global_domain_import, and
comm_global_domain_release in chip_worker.h. Specify that prepare returns
descriptor bytes, the local window base, and actual mapping size, which may
exceed window_size; import requires a complete rank-ordered descriptor table;
and release pairs with prepare and is also invoked by finalize().
python/simpler/global_comm_domain.py (1)

371-384: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider validating the decoded capability result.

Every other decoder in this module re-validates its fields. decode_comm_init_result accepts any profile, max_ranks, and descriptor_bytes. A peer that reports a different descriptor ABI size passes silently, and the mismatch surfaces later during prepare/import. A cheap check here fails fast.

♻️ Optional validation
     reader.done("COMM_INIT result")
+    if profile not in GLOBAL_DOMAIN_PROFILE_IDS:
+        raise ValueError(f"unsupported global domain profile {profile!r}")
+    if descriptor_bytes != GLOBAL_DOMAIN_DESCRIPTOR_BYTES or max_ranks == 0 or max_ranks > GLOBAL_DOMAIN_MAX_RANKS:
+        raise ValueError("global comm init result capability is invalid")
     result = GlobalCommInitResult(
🤖 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/global_comm_domain.py` around lines 371 - 384, Update
decode_comm_init_result to validate the decoded profile, max_ranks, and
descriptor_bytes before constructing GlobalCommInitResult, reusing the module’s
existing validation helpers or conventions. Reject unsupported values, including
descriptor ABI sizes that do not match the expected value, while preserving the
existing successful decode flow.
src/common/worker/chip_worker.cpp (1)

868-875: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Harden the descriptor ABI and the release-pointer use.

Two points in this block:

  1. Line 869 calls comm_global_domain_release_fn_ without a null check, unlike comm_global_domain_release at Line 900. init() resolves both symbols with the required load_symbol and clears both together, so the pointer is non-null whenever comm_global_domain_prepare_fn_ is non-null. The guard is still worth adding for consistency with the surrounding code.
  2. Lines 873-875 ship the raw CommGlobalDomainDescriptor bytes to Python, which decodes them with the fixed little-endian layout "<IIIIQII256s". Only the total size is asserted (sizeof(...) == 288 in both backends). A field reorder that keeps the size constant would silently misdecode. Add per-field offsetof assertions next to the existing size assertion.
♻️ Proposed hardening
     if (local_window_base == 0 || descriptor.mapping_size == 0) {
-        comm_global_domain_release_fn_(domain_id);
+        if (comm_global_domain_release_fn_ != nullptr) {
+            comm_global_domain_release_fn_(domain_id);
+        }
         global_domain_ids_.erase(domain_id);
         throw std::runtime_error("comm_global_domain_prepare returned an invalid window");
     }

Add next to the existing size assertion (for example in src/common/platform_comm/comm.h):

static_assert(offsetof(CommGlobalDomainDescriptor, mapping_size) == 16, "descriptor layout changed");
static_assert(offsetof(CommGlobalDomainDescriptor, handle_size) == 24, "descriptor layout changed");
static_assert(offsetof(CommGlobalDomainDescriptor, handle) == 32, "descriptor layout changed");
🤖 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 `@src/common/worker/chip_worker.cpp` around lines 868 - 875, In the
invalid-window cleanup within the descriptor preparation flow, guard
comm_global_domain_release_fn_ before invoking it, matching the existing
comm_global_domain_release handling. Also strengthen CommGlobalDomainDescriptor
ABI validation beside its existing size assertion by adding static_assert checks
for the mapping_size, handle_size, and handle field offsets required by the
Python little-endian decoder.
python/bindings/task_interface.cpp (1)

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

Consider releasing the GIL for the global-domain calls.

The three new bindings hold the GIL for the whole native call. comm_global_domain_prepare reserves and maps a VMM/Fabric window, exports a handle, and zeroes the entire window; comm_global_domain_import imports one peer window per rank and copies a CommContext to the device. Both can run for a long time and block every other Python thread in the process. The adjacent device-side helpers in this file already use nb::call_guard<nb::gil_scoped_release>() for exactly this reason, for example _l3_child_onboard_region_create at Line 1839 and _ChipWorker.init at Line 1387.

Note that the lambda for comm_global_domain_prepare constructs nb::bytes and nb::make_tuple from its result, so a whole-lambda call guard is not correct there. Wrap only the native call, or move the Python object construction after the guard scope ends.

♻️ Sketch for the prepare binding
             [](ChipWorker &self, uint64_t domain_id, uint32_t domain_rank, uint32_t rank_count, size_t window_size,
                uint32_t profile) {
-                auto [descriptor, local_window_base, actual_window_size] =
-                    self.comm_global_domain_prepare(domain_id, domain_rank, rank_count, window_size, profile);
+                std::vector<uint8_t> descriptor;
+                uint64_t local_window_base = 0;
+                size_t actual_window_size = 0;
+                {
+                    nb::gil_scoped_release release;
+                    std::tie(descriptor, local_window_base, actual_window_size) =
+                        self.comm_global_domain_prepare(domain_id, domain_rank, rank_count, window_size, profile);
+                }
                 return nb::make_tuple(

comm_global_domain_import and comm_global_domain_release return plain integers, so a nb::call_guard<nb::gil_scoped_release>() on the .def(...) is sufficient for those two.

🤖 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/task_interface.cpp` around lines 1588 - 1617, Release the GIL
while executing the native calls in comm_global_domain_prepare,
comm_global_domain_import, and comm_global_domain_release. For
comm_global_domain_prepare, scope the GIL release only around
self.comm_global_domain_prepare so nb::bytes and nb::make_tuple construction
still runs with the GIL held; add a whole-binding call guard for the
plain-integer import and release methods.
🤖 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/mpi-l3-mailbox.md`:
- Line 1: Add the mpi-l3-mailbox documentation page to the nav configuration in
mkdocs.yml, using the existing title and navigation structure so strict MkDocs
builds include docs/mpi-l3-mailbox.md.

In `@docs/remote-l3-worker-design/implementation-record.md`:
- Around line 95-96: Update the implementation record text to refer to the
shipped A3 Fabric profile as “A3 Fabric V1” rather than “A3 Fabric V2,” matching
the identifiers GLOBAL_DOMAIN_PROFILE_A3_FABRIC and
COMM_GLOBAL_DOMAIN_PROFILE_A3_FABRIC.

In `@python/simpler/global_comm_domain.py`:
- Around line 395-400: Enforce the 64-buffer limit in encode_domain_command
before serializing buffers, using a shared GLOBAL_DOMAIN_MAX_BUFFERS constant.
Update decode_domain_command to use the same constant instead of the existing
hardcoded or unrelated limit, and raise the established validation error when
command.buffers exceeds the bound.

In `@python/simpler/mpi_l3_session.py`:
- Around line 476-495: Add a short sleep/backoff to the mailbox.request_state
polling loop in the MPI session receive flow, and enforce a deadline when the
group remains terminal so it cannot spin indefinitely; also add the same pause
after each request.test() call in the dispatcher loop at
python/simpler/mpi_l3_session.py lines 71-84. Apply both changes in
python/simpler/mpi_l3_session.py:476-495 and
python/simpler/mpi_l3_session.py:71-84, preserving the existing request dispatch
behavior.
- Around line 500-517: Move the _payload_for_rank(request, rank) call inside the
existing try block in the per-rank dispatch logic, before opcode handling.
Preserve the existing MpiRankError conversion so IndexError and ValueError are
captured and included in dispatch_comm.gather rather than escaping the loop.

In `@python/simpler/task_interface.py`:
- Around line 1105-1109: Update TaskInterface.release to set _released before
invoking _release_fn(self), while preserving the existing early return for
already released handles. Keep the callback invocation unchanged so release
failures still propagate, but ensure member() and buffer_range() observe the
handle as released even when the callback raises.

In `@src/common/hierarchical/remote_endpoint.cpp`:
- Around line 904-945: Update both timeout branches in exchange_group_task,
including the dispatch-timeout path after the leader’s run_exchange, to
increment group_departed_ while holding group_mu_ before marking the rendezvous
complete and throwing. Preserve the existing timeout error, terminal marking,
group termination, notification, and exception behavior so the normal reset
logic can clear group_active_ and group_frames_.
- Around line 766-830: Update the polling loop in the request-waiting method
containing OFF_REQUEST_STATE to use bounded backoff instead of continuously
spinning. Add an escalating yield or short sleep on each iteration while
preserving prompt handling of TASK_DONE, SHUTDOWN_DONE, TASK_FAILED, terminal,
and timeout states; reset or initialize the backoff appropriately for each
request.

In `@src/common/platform_comm/comm_sim.cpp`:
- Line 219: Protect accesses to global_domain_allocations in
comm_global_domain_prepare, comm_global_domain_import, and
comm_global_domain_release with a mutex consistently across the sim and HCCL
backends, covering all reads, writes, and erases; alternatively, explicitly
document and enforce the required single-thread caller contract.

In `@task.md`:
- Around line 7-11: Remove personal and internal identifiers from the task
record: replace the branch name, myserver host, and validation host references
in the affected entries with generic placeholders while preserving the remaining
handoff metadata and structure.

In `@tools/a3_l4_tcp_smoke/mpirun_compute_then_tload_2x2_smoke.py`:
- Around line 310-313: Replace the concrete network defaults in
tools/a3_l4_tcp_smoke/mpirun_compute_then_tload_2x2_smoke.py lines 310-313 for
the --host-37, --host-35, --roce-37, and --roce-35 arguments with
documentation-reserved placeholder addresses, or make them required without
defaults. Update the example command in tools/a3_l4_tcp_smoke/README.md lines
17-23 to use the same placeholders.

---

Nitpick comments:
In `@python/bindings/task_interface.cpp`:
- Around line 1588-1617: Release the GIL while executing the native calls in
comm_global_domain_prepare, comm_global_domain_import, and
comm_global_domain_release. For comm_global_domain_prepare, scope the GIL
release only around self.comm_global_domain_prepare so nb::bytes and
nb::make_tuple construction still runs with the GIL held; add a whole-binding
call guard for the plain-integer import and release methods.

In `@python/simpler/global_comm_domain.py`:
- Around line 371-384: Update decode_comm_init_result to validate the decoded
profile, max_ranks, and descriptor_bytes before constructing
GlobalCommInitResult, reusing the module’s existing validation helpers or
conventions. Reject unsupported values, including descriptor ABI sizes that do
not match the expected value, while preserving the existing successful decode
flow.

In `@python/simpler/global_comm_smoke.py`:
- Around line 22-167: Refactor remote_compute_orch, remote_rank_orch,
remote_compute_group_orch, and remote_rank_group_orch to share a helper for
importing get_inner_handle, validating the six scalars, decoding the digest, and
resolving the domain. Add reusable tensor-argument builders for the compute and
TLOAD shapes, then have the group callbacks reuse the corresponding
single-worker argument construction inside their loops while preserving worker
selection and submission behavior.

In `@python/simpler/mpi_group_mailbox.py`:
- Around line 320-329: Update the error serialization flow in the mailbox
failure-writing method so oversized data remains valid JSON. Truncate individual
rank-error message fields before `json.dumps`, then encode and write the
complete serialized document without slicing the encoded JSON at
`MAILBOX_ERROR_BYTES`; preserve rank attribution and the existing state updates.
- Around line 345-357: The TASK_FAILED error decoding in the mailbox request
handling must not catch control-flow exceptions. Narrow the try/except around
json decoding and entry field access to the specific decode and lookup/type
errors that can occur, replacing the broad BaseException handler so Ruff BLE001
is resolved while preserving the raw-message fallback.
- Around line 373-375: The test-only method overwrite_request_payload_for_test
should not remain exposed on the shipped mailbox protocol class. Remove it and
update the sole caller in test_mpi_group_mailbox.py to write the payload
directly through mailbox._buffer, or otherwise enforce MAILBOX_PAYLOAD_BYTES
capacity before writing if the helper must remain.

In `@python/simpler/mpi_l3_session.py`:
- Around line 504-513: Replace the assert in the MailboxOpcode.SHUTDOWN branch
of the request handling flow with an explicit payload None check that raises an
appropriate error before calling _rewrite_frame_identity; preserve the existing
shutdown behavior when payload is present.

In `@python/simpler/orchestrator.py`:
- Around line 432-441: Update the docstring for the CommDomain creation method
to state that MPI groups are supported, removing the “without MPI” limitation
and mentioning MPI group registration alongside Worker.add_worker and
Worker.add_remote_worker. Ensure the member description reflects
add_mpirun_worker_group usage while preserving the existing lifecycle and commit
behavior documentation.

In `@python/simpler/worker.py`:
- Around line 3515-3523: Remove the unreachable else fallback in
_build_remote_manifest and rely directly on the runtime values from
_resolved_global_nodes()[int(worker_id)] for node_rank, node_count, and
global_device_ranks. Preserve the existing remote-worker caller contract and
eliminate the redundant _remote_like_worker_ids() condition.
- Around line 7295-7310: Bind each global domain handle to its allocating
_RunResources when created, mirroring _allocate_domain’s _release_fn closure.
Update _release_global_domain_handle to accept and use the owning resources
rather than reading self._building_run_resources, so pending releases and fences
are associated with the allocating run; preserve the existing cleanup and
_release_global_domain_now memoization behavior.

In `@src/common/platform_comm/comm_sim.cpp`:
- Around line 199-216: Make GlobalDomainAllocation non-copyable and non-movable
by explicitly deleting its copy and move constructors and assignment operators,
preventing duplicated ownership of local_base and shm_name while preserving its
current unique_ptr-based usage.

In `@src/common/worker/chip_worker.cpp`:
- Around line 868-875: In the invalid-window cleanup within the descriptor
preparation flow, guard comm_global_domain_release_fn_ before invoking it,
matching the existing comm_global_domain_release handling. Also strengthen
CommGlobalDomainDescriptor ABI validation beside its existing size assertion by
adding static_assert checks for the mapping_size, handle_size, and handle field
offsets required by the Python little-endian decoder.

In `@src/common/worker/chip_worker.h`:
- Around line 161-165: Document the public APIs comm_global_domain_prepare,
comm_global_domain_import, and comm_global_domain_release in chip_worker.h.
Specify that prepare returns descriptor bytes, the local window base, and actual
mapping size, which may exceed window_size; import requires a complete
rank-ordered descriptor table; and release pairs with prepare and is also
invoked by finalize().

In `@tests/ut/cpp/hierarchical/test_remote_endpoint.cpp`:
- Around line 659-661: Bound the polling loops in both respond_with_payloads and
the matching wait near exchange_group_task with a deadline; when
RequestState::REQUEST_READY is not observed before expiration, call FAIL() with
a diagnostic, while preserving the existing behavior when the state becomes
ready.

In `@tests/ut/py/test_global_comm_domain.py`:
- Around line 259-278: Update _failure_injection_worker to obtain two ports
through the existing _free_tcp_ports helper, then build each RemoteWorkerSpec
endpoint from those allocated ports instead of the fixed 19073 + index range.
Preserve the current node ordering and endpoint construction format.

In `@tests/ut/py/test_mpi_group_mailbox.py`:
- Around line 25-211: Add a test covering the mailbox payload capacity guard by
attempting to write a request whose encoded payloads exceed
MAILBOX_PAYLOAD_BYTES and asserting ValueError. Exercise the write_request path
and ensure the oversized payload vector is rejected before acceptance or state
progression.

In `@tools/a3_l4_tcp_smoke/mpirun_compute_then_tload_2x2_smoke.py`:
- Around line 51-60: Update the kernel_include_dirs construction in _compile_aiv
to use iterable unpacking when combining include_dirs with the common source
directory, preserving the existing ordering and values.

In `@tools/mpi_group_mailbox_smoke.py`:
- Around line 34-37: Update _wait_until to pause briefly between predicate
evaluations, while retaining the existing deadline check and TimeoutError
behavior; add the sleep inside the loop after a failed predicate check.

In `@tools/mpi_l3_group_smoke.py`:
- Around line 41-43: Restore the SIMPLER_MPI_SMOKE_DIR environment variable
after the temporary-directory run in the surrounding run flow: save any prior
value before assigning output_dir, then restore it in a finally block (or remove
it when absent) so repeated or nested calls never retain the deleted path.
🪄 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: a870b115-1c58-4057-9696-095762c08e11

📥 Commits

Reviewing files that changed from the base of the PR and between 4d735ce and 3c0e53b.

📒 Files selected for processing (50)
  • docs/comm-domain.md
  • docs/mpi-l3-mailbox.md
  • docs/remote-l3-worker-design.md
  • docs/remote-l3-worker-design/implementation-record.md
  • docs/remote-l3-worker-design/protocol.md
  • python/bindings/CMakeLists.txt
  • python/bindings/task_interface.cpp
  • python/bindings/worker_bind.h
  • python/simpler/global_comm_domain.py
  • python/simpler/global_comm_smoke.py
  • python/simpler/mpi_group_mailbox.py
  • python/simpler/mpi_group_smoke.py
  • python/simpler/mpi_l3_session.py
  • python/simpler/orchestrator.py
  • python/simpler/remote_l3_protocol.py
  • python/simpler/remote_l3_session.py
  • python/simpler/remote_l3_worker.py
  • python/simpler/task_interface.py
  • python/simpler/worker.py
  • src/a2a3/platform/onboard/host/comm_hccl.cpp
  • src/a5/platform/onboard/host/comm_hccl.cpp
  • src/common/hierarchical/mpi_group_mailbox.h
  • src/common/hierarchical/remote_endpoint.cpp
  • src/common/hierarchical/remote_endpoint.h
  • src/common/hierarchical/remote_wire.cpp
  • src/common/hierarchical/remote_wire.h
  • src/common/hierarchical/worker.cpp
  • src/common/hierarchical/worker.h
  • src/common/hierarchical/worker_manager.cpp
  • src/common/hierarchical/worker_manager.h
  • src/common/platform_comm/comm.h
  • src/common/platform_comm/comm_sim.cpp
  • src/common/worker/chip_worker.cpp
  • src/common/worker/chip_worker.h
  • task.md
  • tests/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/hierarchical/test_remote_endpoint.cpp
  • tests/ut/py/test_callable_identity.py
  • tests/ut/py/test_global_comm_domain.py
  • tests/ut/py/test_mpi_group_mailbox.py
  • tests/ut/py/test_mpi_l3_group.py
  • tests/ut/py/test_worker/test_startup_readiness.py
  • tools/a3_l4_tcp_smoke/README.md
  • tools/a3_l4_tcp_smoke/kernels/aiv/global_tload_kernel.cpp
  • tools/a3_l4_tcp_smoke/kernels/aiv/local_add_kernel.cpp
  • tools/a3_l4_tcp_smoke/kernels/orchestration/global_tload_orch.cpp
  • tools/a3_l4_tcp_smoke/kernels/orchestration/local_add_orch.cpp
  • tools/a3_l4_tcp_smoke/mpirun_compute_then_tload_2x2_smoke.py
  • tools/mpi_group_mailbox_smoke.py
  • tools/mpi_l3_group_smoke.py

Comment thread docs/mpi-l3-mailbox.md
Comment thread docs/remote-l3-worker-design/implementation-record.md Outdated
Comment thread python/simpler/global_comm_domain.py
Comment thread python/simpler/mpi_l3_session.py
Comment thread python/simpler/mpi_l3_session.py
Comment thread src/common/hierarchical/remote_endpoint.cpp
Comment thread src/common/hierarchical/remote_endpoint.cpp Outdated
Comment thread src/common/platform_comm/comm_sim.cpp
Comment thread task.md Outdated
Comment thread tools/a3_l4_tcp_smoke/mpirun_compute_then_tload_2x2_smoke.py Outdated
- Route MPI group task and control traffic through a named rank-0
  shared-memory mailbox: requests reach the ranks over one MPI bcast
  and ranked results return via gather, with no per-rank TCP sessions
- Mark group-wide domain controls with an explicit
  FRAME_FLAG_GROUP_TARGET frame flag; unmarked controls stay
  rank-targeted, so partial-group Global CommDomains keep their
  per-node fallback semantics
- Batch a full-group submit_next_level_group into one PER_RANK
  envelope through per-transport progress helper threads, keeping the
  scheduler's submit/poll non-blocking; rendezvous timeouts fail only
  the waiting task and never kill mpirun, while the mailbox round trip
  under lane_mu_ owns terminal and kill semantics
- Reject group selections that mix MPI ranks with other workers at the
  group's world size; reap mpirun children and unlink the mailbox even
  when the native worker close fails
- Preserve the TCP transport for ordinary non-MPI Remote L3 workers
- Cover the batched path in the mpirun pod ST example and
  transport-level unit tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sunkaixuan2018
sunkaixuan2018 force-pushed the use-mpi-and-remove-socket branch from c536dfe to cd51a88 Compare August 14, 2026 07:16
@ChaoWao

ChaoWao commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Reviewed at cd51a880 (merge-base cc9bfc45, the #1623 merge — this branch is already rebased onto current main, 0 behind).

Stated vs. real goal

The body says: replace the MPI group's TCP command/health path with one L4-owned named shared-memory mailbox attached by local rank 0, distribute ordered task/control envelopes over dedicated MPI communicators, keep ordinary remote L3 on TCP. Reading the code independently, that is what it does — no scope mismatch. MpiL3GroupSpec.command_port_base / health_port_base became int | None = None rather than being deleted, so a caller that still passes them is not broken; the mailbox is the new default path for MPI groups only.

Change breakdown

  Core            19 files  +1984  -391  =2375
  Test/Ex          9 files  + 617  - 34  = 651
  Docs             3 files  + 142  -  2  = 144
  Uncategorized    1 files  +   1  -  0  =   1
  TOTAL           32 files  +2744  -427  =3171

Oversized (Core 2375 lines). GitHub's 37 files / +4051 is against a stale base; the real three-dot diff is the above. Still well past the 1000-line mark, and it is a new cross-language wire protocol plus its two endpoints plus a transport swap. A reviewer cannot hold this in one pass — the protocol header (mpi_group_mailbox.h / .py) and the endpoint implementations would have been reviewable separately.

Mechanism brief

The problem: an MPI group previously needed each rank to bind fixed, pre-agreed command and health TCP ports so L4 could attach to each one as an ordinary remote L3. That means N sockets, N port reservations that survive a killed run, and a control lane that duplicates what MPI already provides between ranks.

The new shape: L4 creates one POSIX shm mailbox (MpiGroupMailbox.create) and passes its name in the group manifest. Only local rank 0 opens it; every other rank is MPI-only. Rank 0 accepts a request from the mailbox, bcasts it to the group over a dedicated dispatch communicator, each rank executes its slice, results are gathered back to rank 0, and rank 0 completes the mailbox request. Three targeting modes: GROUP (one payload, all ranks), RANK (one payload, one rank), PER_RANK (one payload per rank, validated as len(payloads) == world_size).

Layout is a 256-byte header plus two 16 MiB payload regions plus a 64 KiB error region. Two state machines: MailboxGroupState (INITIALIZING/READY/TERMINAL/CLOSED) for the group's lifetime, MailboxRequestState (IDLE→REQUEST_READY→TASK_ACCEPTED→TASK_DONE/TASK_FAILED, plus the SHUTDOWN pair) for one in-flight request. Monotonic sequence_id with accept_request(last_sequence_id=...) rejecting a replay — that's the right guard for a shared-memory lane where a stale writer can't be excluded structurally.

Ownership: L4 owns and unlinks; open() explicitly refuses to unlink (close(unlink=True) raises for non-owners) and works around Python < 3.13's lack of track=False by unregistering from the resource tracker, with a comment explaining the race it avoids. That is a real footgun handled carefully.

Failure model: mark_terminal(reason) writes the reason into the error region and sets both states, so a peer that reads the mailbox afterwards learns why rather than just that it stopped. fail_request carries per-rank MpiRankError records as JSON, so an L4-side exception names which rank failed. Terminal state is sticky — publish_closed() deliberately won't overwrite TERMINAL.

Findings

Must fix

1. The atomic accessors silently fall back to non-atomic reads — the fallback is always taken. MpiGroupMailbox._load_i32 / _store_i32 do:

try:
    from .task_interface import _mailbox_load_i32
except (ImportError, AttributeError):
    return self._read_i32(offset)

_mailbox_load_i32 / _mailbox_store_i32 are exported by the _task_interface extension and re-exported by simpler.worker, but not by simpler.task_interface. Verified on this branch after a clean rebuild:

in _task_interface: True True
re-exported by simpler.task_interface: False
simpler.worker has it: <nanobind.nb_func object ...>

So every state transition on this mailbox uses struct.pack_into / unpack_from, not the acquire/release helpers the code intends. The cross-process handshake this protocol is built on has no publication barrier, and because the fallback is a silent except ImportError, nothing reports it — the tests pass either way, which is why 46 green tests didn't catch it. Change the import to from .worker import ... (or re-export from task_interface), and consider whether the fallback should exist at all: if the extension is missing, this protocol cannot be used safely, so raising beats degrading silently.

2. Rank 0's request-wait loop is an unbounded busy-spin. In mpi_l3_session.py, the inner while True polls mailbox.request_state with no sleep, no yield, and no blocking primitive — I checked the loop body specifically for one. When IDLE it also refreshes the deadline every iteration, so an idle group holds a full core indefinitely on the machine hosting rank 0.

codestyle.md rule 5 permits spinning on the dispatch path — a wait a task's latency passes through may spin — but its stated exception is exactly this case: "When an idle spin is genuinely too expensive to leave running (a forked child that would hold a core for its whole lifetime), the answer is a blocking wakeup primitive, not a sleep." An MPI rank that lives for the whole session and is idle between requests is that shape. The C++ side (remote_endpoint.cpp) spins too, but there it is correct — it is waiting on a request it just issued, so the wait is on the task's own latency.

Should fix

3. The mailbox layout is duplicated in Python and C++ with nothing checking they agree. mpi_group_mailbox.py and mpi_group_mailbox.h each independently declare MAGIC, PROTOCOL_VERSION, all four region sizes, and all fifteen field offsets. Both sides validate magic/version/header-size at runtime, which catches a version skew — but not a field-offset divergence introduced by editing one file and not the other, since both would still report version 1 with matching header bytes. The two enum sets (GroupState/RequestState/Opcode/Target) are likewise duplicated by hand. Neither the new Python tests nor test_remote_endpoint.cpp asserts the two declarations match. A single test that pokes each offset from one side and reads it from the other, or a generated header, would close this. Compare runtime.h, where device-copied structs carry static_assert on layout for the same reason.

4. The PR body's testing section is stale in a way that undersells the PR. It lists the hardware items as unchecked with "The hardware and MPI integration items remain explicitly unvalidated until the server-37 agent returns logs." But st-pod-onboard-a2a3 is green on this head and I read the job log rather than the tick: test_global_tload_mpirun_l3 ... [PASS 15.0s, devices=[12, 13]], 1 passed in 14.66s, five pod tests selected and run. Note the runtime went 4.4s → 15.0s versus #1623, consistent with the mailbox path actually being exercised rather than skipped. Please refresh the body — a reviewer who trusts it will think the two-machine path is unproven.

5. No stated version-evolution rule for a brand-new cross-process ABI. MAILBOX_PROTOCOL_VERSION = 1 is checked on both sides, which is the important half. What's missing is the documented rule for what a v2 may change and how a mixed-version pair behaves — the header has MAILBOX_HEADER_BYTES = 256 with fields ending at offset 76, so there are ~180 unused bytes that are neither named as reserved nor validated as zero. Stating "trailing header bytes are reserved and must be zero" now, and rejecting non-zero, is what keeps them usable later; docs/mpi-l3-mailbox.md is the natural place.

Consider

6. overwrite_request_payload_for_test is production API surface for a test. A public method whose name says it's for tests is reachable by any caller. Either make it private, or if the corruption path it exercises is worth covering, have the test write through the buffer directly.

7. Error truncation is silent. fail_request clips the JSON error blob to MAILBOX_ERROR_BYTES - 1 with no marker, so a large multi-rank failure yields invalid JSON that read_result then falls back to decoding as raw text. Reserving a few bytes for a "...truncated" sentinel, or capping the per-rank message length before serializing, keeps the structured path intact when it matters most.

pto-isa pin check — advisory

ℹ️ pto_isa.pin is pinned to 0cefc9a5a1c24c62655cc345d408559595a8af32. No pto-isa include paths changed and the pin file is untouched — confirm the pinned commit is still adequate; no bump indicated.

Verification I ran

  • Full tests/ut/py: 1440 passed, 13 skipped (up from 1337 on cc9bfc45 — this PR's 46 new mailbox/MPI tests plus siblings).
  • The PR's three new/changed test files alone: 46 passed.
  • Rebuilt the extension first — the stale-extension guard fired on checkout (_task_interface was built from 254c12fc5a1f, but this source tree is at cd51a880f4c5), and finding 1 depends on a correct build to state truthfully.
  • CI: 18 pass / 1 skipping / 0 fail, including st-pod-onboard-a2a3, ut-a2a3, ut-a5.

Verdict

Request changes — on findings 1 and 2 only. Both are small and local: one import line, and one blocking primitive in the idle wait.

The design itself is good and I'd approve it otherwise. Collapsing N sockets to one mailbox plus MPI's own collectives is the right call, the sequence-id replay guard and sticky TERMINAL state show the failure modes were thought through, and the resource-tracker workaround is the kind of detail that usually gets found in production instead. Finding 1 is worth blocking on specifically because it is invisible: the protocol is designed around acquire/release semantics and currently doesn't have them, with no signal that anything is wrong.

Given Core is 2375 lines, I'd also suggest that any future protocol like this land as two PRs — header/codec first with its tests, then the endpoints — so the wire format gets reviewed on its own terms before two implementations depend on it.

- Import the mailbox acquire/release helpers from _task_interface and
  fail the import when the extension is missing: the cross-process
  handshake depends on their publication barriers, and the previous
  simpler.task_interface import always fell back to plain struct
  reads because that module does not re-export the helpers
- Park rank 0 on the request-state word between requests -- a shared
  futex on Linux, woken by every L4 publish -- with a bounded chunk
  that re-checks group state, so an idle group no longer holds a
  full host core and a lost wake cannot stall the loop
- Validate reserved header bytes [80, 256) as zero on both attach
  paths and document the version-evolution rule, so a version-1 peer
  cannot silently carry fields it does not understand
- Export the C++ wire layout as _mpi_mailbox_layout and assert the
  Python declaration matches it, so an edit to one side of the
  hand-mirrored layout fails a unit test instead of corrupting the
  lane at runtime
- Cap per-rank error messages and replace dropped tail entries with
  a sentinel entry, so the gathered error blob always parses as JSON
- Remove overwrite_request_payload_for_test from the production
  surface; the test writes through the shared-memory buffer directly
@ChaoWao

ChaoWao commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Re-reviewed at 87489e1a (previous review was cd51a880). All five findings are closed, verified against this revision rather than taken from the commit message. Approving.

Finding 1 — atomics: fixed, and I confirmed the fix actually resolves

The silent fallback is gone; it's now a hard module-level import from _task_interface with a comment stating why degradation is not acceptable here. The import is the correct one:

import OK
load : <nanobind.nb_func object ...>
store: <nanobind.nb_func object ...>
wait : <nanobind.nb_func object ...>

Worth noting how this played out: on first checkout the import raised cannot import name '_mailbox_wait_i32' because my extension was stale from the previous head. That is the guard from #1523 doing its job — and it is the same class of failure the old except ImportError was hiding. Turning a silent degrade into a loud import error is exactly right.

Finding 2 — idle spin: fixed with a futex, not a sleep

wait_request_state calls _mailbox_wait_i32, which is a FUTEX_WAIT (with a portable poll fallback for non-Linux) wrapped in nb::gil_scoped_release. Rank 0's loop now parks in 1 s chunks (_REQUEST_WAIT_CHUNK_S) bounded by the stall deadline, and re-checks the word on wake — correct handling of spurious wakeups, which the helper's docstring calls out. The C++ side wakes the word via wake_request_waiter() after publishing a request.

This is the shape codestyle.md rule 5 prescribes for the idle case ("a blocking wakeup primitive, not a sleep"), and releasing the GIL matters as much as the futex itself — a rank-0 process holding the GIL while parked would have blocked its own MPI progress.

One asymmetry I checked and am not flagging: _mailbox_store_i32 does not wake, so a Python-side state publication never wakes a waiter. It costs nothing today because the only Python waiter is rank 0 waiting for a request that the C++ L4 side publishes (and that path does wake), while the C++ side's wait for TASK_DONE is a spin on its own request's latency — which is dispatch-path and correctly left spinning. Worth a comment if a second Python waiter ever appears.

Finding 3 — layout duplication: fixed, and the guard is real

test_python_and_cpp_mailbox_layouts_agree compares the full Python constant set against _mpi_mailbox_layout(), a new binding that reads the C++ header's own constants. It covers all four region sizes, all sixteen offsets including the new RESERVED_OFFSET, and every enumerator of all four enums — not just a spot check.

I verified it is wired in the direction that catches divergence (the dict comes from mpi_group_mailbox:: constants in C++, not from a Python-side copy), so editing one file without the other now fails a test instead of producing a silent field-offset skew.

Finding 5 — reserved bytes: fixed on both sides

RESERVED_OFFSET = 80 is now named in both headers with a comment stating the v1 contract, the creator zeroes the region, and both attach paths reject non-zero (mpi_group_mailbox.py _validate_header, and the C++ loop at remote_endpoint.cpp:737). That is what keeps the ~176 spare header bytes usable by a v2 instead of accidentally load-bearing.

Consider items 6 and 7 — both taken

overwrite_request_payload_for_test is gone from the public surface, and error truncation now appends an explicit [truncated] marker per rank rather than clipping the JSON blob mid-structure.

Body

Also fixed — the stale "hardware and MPI integration items remain explicitly unvalidated" section is replaced with the actual evidence, including the re-validation on this head. That was my finding 4 and it is fully addressed.

Verification I ran on 87489e1a

  • Rebuilt the extension first (required — see finding 1).
  • Full tests/ut/py: 1444 passed, 13 skipped (up from 1440 on cd51a880; the delta is the new layout-guard and reserved-bytes tests).
  • test_python_and_cpp_mailbox_layouts_agree passes, and I confirmed the C++-sourced values are the ones being compared.
  • CI: 18 pass / 1 skipping / 0 fail. Read the pod job log rather than the tick: test_global_tload_mpirun_l3 ... [PASS 15.1s, devices=[12, 13]] — the two-machine mailbox path really re-ran on the futex-park code, and 15.1s matches the 15.0s from the pre-fix head, so parking did not change the timing profile.

Verdict

Approve.

Every blocking item was fixed at the root rather than worked around: the atomics import fails loudly instead of degrading, the idle wait parks instead of spinning, and the duplicated layout now has a test that fails when the two declarations disagree. The one structural note from my previous review still stands as advice rather than an objection — Core was ~2400 lines and a new cross-language wire protocol would have been easier to review as header/codec first, endpoints second. Worth keeping in mind for the next protocol, not worth holding this.

@ChaoWao
ChaoWao merged commit 43094d4 into hw-native-sys:main Aug 14, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants