Skip to content

Add: CPU-NPU Comm Endpoint Model - #1696

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ccyywwen:w2-comm-endpoint-model
Aug 7, 2026
Merged

Add: CPU-NPU Comm Endpoint Model#1696
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ccyywwen:w2-comm-endpoint-model

Conversation

@ccyywwen

@ccyywwen ccyywwen commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the cpu-npu-shared-memory endpoint model in Python only.

  • Add simpler.comm_endpoints with endpoint selectors, path parsing,
    endpoint registry resolution, node-scope relation queries, capability cache
    stubs, and SingleOwner backend planning.
  • Add internal Worker._resolve_region_spec(...) and
    Worker._plan_region(...) with lazy endpoint registry construction and
    registry epoch invalidation on close.
  • Add focused Python unit tests for selector validation, registry expansion,
    provider resolution, same-node/cross-node behavior, backend planning, and
    package import surface.

Scope

This PR intentionally does not add Orchestrator.create_region(...), does not
materialize regions, and does not touch C++ / nanobind / wire ABI.

HOST_MAP_DEVICE_HBM remains a capability contract only. Real probing must validate bidirectional host-device counter visibility.

Tests

  • pytest tests/ut/py/test_worker/test_comm_endpoints.py tests/ut/py/test_package_surface.py
  • pytest tests/ut/py/test_worker/test_l4_recursive.py tests/ut/py/test_worker/test_l3_l2_orch_comm.py tests/ut/py/test_worker/test_comm_endpoints.py

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

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

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b91d361-3bb2-4b60-ab91-d4a760fff975

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request adds endpoint and backend planning, changes remote L3 transport from sim to host_tcp, introduces worker-owned host-buffer handling, and adds a two-machine mixed-L3 vector-add example with paired hardware CI validation.

Changes

Endpoint planning

Layer / File(s) Summary
Endpoint model and registry
python/simpler/comm_endpoints.py, python/simpler/__init__.py
Adds endpoint selectors, registry resolution, node scopes, platform capabilities, and backend materialization plans.
Worker endpoint lifecycle
python/simpler/worker.py, tests/ut/py/test_worker/test_comm_endpoints.py, tests/ut/py/test_package_surface.py
Integrates endpoint planning with worker readiness, nested target inspection, operation leases, closure invalidation, and unit tests.

Host TCP runtime

Layer / File(s) Summary
Host TCP profile and session buffers
python/simpler/remote_l3_protocol.py, python/simpler/remote_l3_session.py, python/simpler/remote_l3_worker.py, python/simpler/task_interface.py
Replaces sim validation with host_tcp and adds worker-owned host-buffer allocation, release, and shared-memory fallback.
Remote worker manifests and defaults
python/simpler/worker.py
Uses host_tcp defaults, serializes remote callable payloads, includes inner worker registries, and handles childless host-buffer workers.
Host TCP transport documentation
docs/capability-survey.md, docs/remote-l3-worker-design.md, docs/remote-l3-worker-design/*, docs/user/how-to/run-on-multiple-chips.md
Updates transport status, implementation records, verification plans, and multi-chip usage to describe shipped host_tcp behavior.

Mixed-L3 validation

Layer / File(s) Summary
Mixed-L3 kernels and orchestration
examples/workers/l4/vector_add_mixed_l3/kernels/*
Adds vector addition, scalar addition, multiplication, and nested orchestration for the mixed-L3 workload.
Mixed-L3 runner and integration test
examples/workers/l4/vector_add_mixed_l3/*
Adds launch scripts, two-machine setup documentation, chip-callable construction, local and remote execution, output checks, and cleanup.
Paired pod CI validation
.github/workflows/ci.yml
Adds pod branch triggers and a paired a2a3 hardware job that synchronizes machines, starts the remote daemon, runs the mixed-L3 test, cleans up, and uploads logs. Existing jobs are disabled.

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

Possibly related PRs

Poem

A rabbit hops where host_tcp flows,
Through paired machines, the workload grows.
Kernels add and multiply bright,
Endpoint plans keep paths in sight.
Logs return before the moon—
Mixed-L3 validation finishes soon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.40% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: adding the CPU-NPU communication endpoint model.
Description check ✅ Passed The description accurately summarizes the Python endpoint model, its tests, and the explicitly excluded scope.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch w2-comm-endpoint-model

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: 8

🧹 Nitpick comments (3)
python/simpler/worker.py (2)

747-760: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Name the conflicting contexts in the error message.

_chip_descriptor_context now collects contexts from three sources: this worker, every local descendant, and every remote spec. When they disagree, the message does not say which values conflict or where they came from. A user who hits this on a deep L4 tree has no signal about which child or remote spec is wrong.

Include the distinct contexts in the message.

♻️ Proposed diagnostic improvement
     if not contexts:
         return "", ""
     first = contexts[0]
     if any(ctx != first for ctx in contexts[1:]):
-        raise RuntimeError("Worker.register: heterogeneous chip child contexts require separate callable namespaces")
+        distinct = sorted(set(contexts))
+        raise RuntimeError(
+            "Worker.register: heterogeneous chip child contexts require separate callable namespaces; "
+            f"found {distinct}"
+        )
     return first
🤖 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 747 - 760, Update the
heterogeneous-context error in _chip_descriptor_context to include the distinct
conflicting platform/runtime contexts collected from the current worker, local
descendants, and remote specs. Preserve the existing validation and exception
behavior while making the message identify the differing values.

2674-2709: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Cache the encoded chip payload across remote specs.

_inner_registry_entries_for_spec runs once per RemoteWorkerSpec. Each call re-reads every chip blob with ctypes.string_at, re-hashes it with SHA-256, re-encodes it, and hex-encodes the result. With N remote specs and M chip callables this repeats N*M full-blob copies and digests.

Only descriptor depends on the spec, through spec.platform and spec.runtime. Remote specs commonly share those values.

This cost lands on the startup path. _open_remote_session derives startup_remaining_s after the manifest is built (Line 2752), so manifest construction is charged against the bounded startup budget.

Cache the blob, its digest, and the encoded payload per (platform, runtime) for the duration of one activation pass.

🤖 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 2674 - 2709, Update
_inner_registry_entries_for_spec to use an activation-scoped cache keyed by
(platform, runtime) and chip identity, storing each chip’s blob, SHA-256 digest,
and encoded payload. Reuse cached values for matching remote specs, while still
computing the descriptor per spec and validating it against state.descriptor;
retain the existing entry construction and hex encoding behavior.
tests/ut/py/test_worker/test_comm_endpoints.py (1)

92-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a mixed local and remote child case.

This test covers a remote child alone. No test covers an L4 parent that has both a local L3 child and a remote L3 child.

Both paths are formatted as L{level}[{child_index}] under the parent, and remote children hardcode L3. The paths stay distinct only because add_worker and add_remote_worker both draw child_index from the single _next_level_worker_id_count counter in worker.py. If either method ever gets its own counter, two endpoints collapse onto one path and EndpointRegistry._by_key keeps only the last record.

Add a case that calls both add_worker and add_remote_worker on one L4 parent, then asserts the two host paths differ and that the two children report different node scopes.

🤖 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_comm_endpoints.py` around lines 92 - 107, Extend
test_remote_registry_assigns_distinct_node_scope_and_planning_rejects_cross_node,
or add a focused neighboring test, to create one L4 parent with both add_worker
and add_remote_worker children. Record both L3 child paths, assert their host
paths differ, and verify EndpointRegistry.same_node reports different node
scopes for the two children.
🤖 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 @.github/workflows/ci.yml:
- Around line 984-987: Add a workflow-level concurrency group for the
st-pod-onboard-a2a3 job, keyed to the pod pair, and set cancel-in-progress to
false so overlapping runs queue instead of terminating the active run. Keep the
existing daemon cleanup and staging TTL behavior unchanged.
- Around line 19-20: Remove the hard-coded if: false from the pre-commit
workflow job so the existing CI protection remains enabled; if temporary
pod-validation gating is required, replace it with a branch-scoped condition.
Restore detect-changes and all affected jobs, and ensure every job listing
detect-changes in needs can execute once re-enabled.

In `@docs/capability-survey.md`:
- Line 37: Align the L4 status in the capability survey with the documented CI
coverage: since line 116 says no CI job starts the daemon, mark the remote
host_tcp capability as “Shipped, not CI-run” unless daemon coverage is
confirmed; otherwise update the CI statement to reflect the verified coverage.

In `@docs/remote-l3-worker-design.md`:
- Line 410: Update the communication-policy manifest schema entry in the remote
L3 worker design so host_tcp is the only accepted value; mark roce, hccs, and ub
as reserved or future values rather than supported options, consistent with the
daemon’s current behavior.

In `@docs/remote-l3-worker-design/implementation-record.md`:
- Around line 12-20: Align the host_tcp status and scope across all three audit
documents: update the entries in
docs/remote-l3-worker-design/implementation-record.md (lines 12-20) to use the
implementation-plan status definition and explicitly list remaining gaps; revise
the implemented claims in docs/remote-l3-worker-design/implementation-plan.md
(lines 8-14) to match that record; and update
docs/remote-l3-worker-design/pr-split-and-audit-plan.md (lines 369-370) to
describe PR 6 as host_tcp scope, identifying any coverage that remains
simulation-only.

In `@examples/workers/l4/vector_add_mixed_l3/kernels/aiv/kernel_add.cpp`:
- Around line 49-56: Run clang-format -i on kernel_entry in
examples/workers/l4/vector_add_mixed_l3/kernels/aiv/kernel_add.cpp (lines
49-56), kernel_add_scalar.cpp (lines 51-59), kernel_mul.cpp (lines 49-56), and
the orchestration flow in
examples/workers/l4/vector_add_mixed_l3/kernels/orchestration/vector_add_mixed_l3_orchestration.cpp
(lines 42-54).

In `@python/simpler/remote_l3_session.py`:
- Around line 667-678: Update the preferred allocation path around
inner_worker.create_host_buffer and the EXPORT_BUFFER handling to preserve an
exportable descriptor for worker-owned HostBuffer instances. Store the backing
shared-memory name or host TCP descriptor in _RemoteBufferEntry rather than
relying on shm_name to interpret entry.data, and have ExportBufferResult use
that preserved descriptor while keeping the SharedMemory fallback unchanged.

In `@python/simpler/worker.py`:
- Around line 3435-3442: Update _plan_region to call _get_endpoint_registry()
once and store the result in a local registry variable, then use that same
instance for resolve_region_spec and BackendResolver construction. Keep the
existing readiness and operation-lease flow unchanged.

---

Nitpick comments:
In `@python/simpler/worker.py`:
- Around line 747-760: Update the heterogeneous-context error in
_chip_descriptor_context to include the distinct conflicting platform/runtime
contexts collected from the current worker, local descendants, and remote specs.
Preserve the existing validation and exception behavior while making the message
identify the differing values.
- Around line 2674-2709: Update _inner_registry_entries_for_spec to use an
activation-scoped cache keyed by (platform, runtime) and chip identity, storing
each chip’s blob, SHA-256 digest, and encoded payload. Reuse cached values for
matching remote specs, while still computing the descriptor per spec and
validating it against state.descriptor; retain the existing entry construction
and hex encoding behavior.

In `@tests/ut/py/test_worker/test_comm_endpoints.py`:
- Around line 92-107: Extend
test_remote_registry_assigns_distinct_node_scope_and_planning_rejects_cross_node,
or add a focused neighboring test, to create one L4 parent with both add_worker
and add_remote_worker children. Record both L3 child paths, assert their host
paths differ, and verify EndpointRegistry.same_node reports different node
scopes for the two children.
🪄 Autofix

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: 25a3283f-0e80-4784-84bf-bca5f8821675

📥 Commits

Reviewing files that changed from the base of the PR and between c866c82 and 7b9df75.

📒 Files selected for processing (25)
  • .github/workflows/ci.yml
  • docs/capability-survey.md
  • docs/remote-l3-worker-design.md
  • docs/remote-l3-worker-design/implementation-plan.md
  • docs/remote-l3-worker-design/implementation-record.md
  • docs/remote-l3-worker-design/pr-split-and-audit-artifacts.md
  • docs/remote-l3-worker-design/pr-split-and-audit-plan.md
  • docs/user/how-to/run-on-multiple-chips.md
  • examples/workers/l4/vector_add_mixed_l3/README.md
  • examples/workers/l4/vector_add_mixed_l3/kernels/aiv/kernel_add.cpp
  • examples/workers/l4/vector_add_mixed_l3/kernels/aiv/kernel_add_scalar.cpp
  • examples/workers/l4/vector_add_mixed_l3/kernels/aiv/kernel_mul.cpp
  • examples/workers/l4/vector_add_mixed_l3/kernels/orchestration/vector_add_mixed_l3_orchestration.cpp
  • examples/workers/l4/vector_add_mixed_l3/run_parent.sh
  • examples/workers/l4/vector_add_mixed_l3/start_machine_daemon.sh
  • examples/workers/l4/vector_add_mixed_l3/test_vector_add_mixed_l3.py
  • python/simpler/__init__.py
  • python/simpler/comm_endpoints.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
  • tests/ut/py/test_package_surface.py
  • tests/ut/py/test_worker/test_comm_endpoints.py

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread docs/capability-survey.md
Comment thread docs/remote-l3-worker-design.md
Comment thread docs/remote-l3-worker-design/implementation-record.md
Comment thread examples/workers/l4/vector_add_mixed_l3/kernels/aiv/kernel_add.cpp
Comment thread python/simpler/remote_l3_session.py
Comment thread python/simpler/worker.py
@ccyywwen
ccyywwen force-pushed the w2-comm-endpoint-model branch from 7b9df75 to 74c9abf Compare August 5, 2026 07:00
@ccyywwen ccyywwen changed the title Add: CPU-NPU Shared-memory Endpoint Model Add: CPU-NPU Comm Endpoint Model Aug 5, 2026

@ChaoWao ChaoWao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

The code itself is good: clean module boundaries, no dependency on the nanobind extension (pinned by a test), deterministic resolution, structured error reasons instead of string matching, and a real test per branch. If this were a greenfield endpoint helper I would approve it.

But W2's deliverable is a frozen model, and four of the frozen items disagree with domain-membership.md in ways that get expensive once W5 builds on them. Three of those four stop being redesign work and become "reuse the types the sibling PR already froze" — see below.


First: this overlaps #1599 more than it looks

domain-membership.md §3 defines the contract as

AttachmentPlan = (member EndpointId, CanonicalBufferIdentity) -> capability + attachment

This PR builds the left key. #1599 ("Add: the Buffer/Tensor wire ABI and owner-side create_buffer", open since 2026-07-30) builds the right key — the doc names it explicitly: "CanonicalBufferIdentity(wire 类型名为 CanonicalIdentity)". Neither builds the arrow; that is W5.

The two branches merge cleanly (git merge-tree → no conflicts; they touch disjoint regions of worker.py). The collision is semantic:

Axis of the capability formula #1599 already ships this PR introduces instead
backing / backend BackendKind {FORK_SHM, POSIX_SHM, VMM_WINDOW, REMOTE_SIDECAR, DEVICE_MALLOC, FORK_COW}, wire-frozen with static_assert BackingKind {DEVICE_HBM, HOST_SHM}
authorization AccessMode {READ, WRITE, READWRITE}, validated on decode — absent
address space AddressSpace {HOST, DEVICE} folded into EndpointDeployment
a capability gate validate_buffer_descriptor, literally commented "address_space x backend_kind capability gate … (capability matrix)" BackendResolver, a different matrix over (deployment, deployment, same_node)
buffer identity CanonicalIdentity = owner_instance_id + buffer_id + generation — no buffer concept

The concrete cost. §3 gives the canonical reason backing must be in the formula:

HOST × VMM_WINDOW = ❌ — 部署与互连都对,但 backing 是 VMM ⇒ host-map 仍失败

VMM_WINDOW and DEVICE_MALLOC are separate values in #1599. This PR collapses both into BackingKind.DEVICE_HBM and asks one global boolean PlatformCapability.HOST_MAP_DEVICE_HBM — so BackendResolver will emit HOST_DIRECT_MAP_ACCESS over a VMM backing, which halHostRegister rejects at the driver level (ascend_hal_base.h:2401; hardware-constraints.md §5). The distinction that makes the rule expressible exists in #1599 and is discarded here.

Suggested ordering: #1599 lands first, this PR rebases and keys its plan on BackendKind / AccessMode / CanonicalIdentity. #1599 is older, larger, and its byte layout freezes on merge; this PR is pure Python and far cheaper to move. The reverse order freezes a second backend vocabulary a week after the first, and one of them then has to be deleted.


Must fix

1. The cross-node hard-reject reintroduces a modelling error the design retracted on 2026-08-03

comm_endpoints.py:505-510_plan_member returns CROSS_NODE_UNSUPPORTED before consulting any capability. §4.1's rewrite note calls the old boolean form "既不准确,也制造了一个特例化的恒假分支", and §9.1 states the replacement rule directly:

请求的 adapter 不可用时才失败,而不是「因为跨机所以失败」

#1623 established a cross-host domain on two-host A3 silicon with max_diff == 0. Cross-node narrows the available adapters — C/T available, D explicitly refused with a reason — it is not a pre-emptive reject.

2. node_scope_id is derived from local-vs-remote, not node identity — and a test pins the wrong answer

_RegistryBuilder.add_worker_children allocates a fresh node_scope_id per RemoteWorkerSpec. Two live consequences:

  • test_comm_endpoints.py:94-98 registers endpoint="127.0.0.1:1234" and asserts not same_node(root, remote). A loopback remote worker is on the same node by construction.
  • Two remote L3s on one physical host get distinct scope ids and are treated as cross-node.

RemoteWorkerSpec.endpoint carries the host and is already validated to numeric IPv4 / localhost at worker.py:4116; node scope should come from there.

3. EndpointId is a bare registry-local integer, and BackendPlan carries nothing else

ResolvedRegionSpec.members keeps full records, but BackendPlan.provider_endpoint_id / ordered_member_endpoint_ids / MemberMaterialization.endpoint_id are ints only, allocated from a counter that restarts at 0 for every registry. A plan is therefore uninterpretable without the exact registry instance that produced it, and nothing binds the two — epoch sits on the registry but is not part of the id, and _record_for does not check it.

§3 requires session_id precisely "才能区分同一路径的不同 incarnation"; §10 bans the bare index ("不以「第 N 个 worker」的裸索引表达"). #1599 solves the same problem for buffers with owner_instance_id (a full-width random draw) plus generation; the same shape works here.

Related: the path root is the building worker's level, so one chip is L3/L2[0] from an L3 and L4/L3[0]/L2[0] from its parent. §3 requires worker_path to be cross-process resolvable.

4. Backing is derived solely from the MemberSet, and the flagship case cannot be expressed

_backing_for_provider maps provider deployment → one BackingKind for the whole region, and plan() opens with del layout. §7's headline row — the brick this design exists to lay ("本设计要补的那块砖") — is HOST_CPU + 同机 DEVICE_*"host-map control Buffer + device-local/VMM payload Buffer;逐 Buffer 选 direct/copy adapter". That needs two backings and per-Buffer capability inside one domain. LayoutSummary already carries counter_bytes / payload_bytes hinting at the split, then ignores them. W2 ⑤ states the prohibition: "禁止仅从 MemberSet 推导后端".

This is the item that reduces to "use #1599's BackendKind and key capability on CanonicalIdentity".


Should fix

5. Every unsupported-plan message duplicates its endpoint label

Reproduced by running the module:

host access to device HBM is unsupported: L4 HOST_CPU: L4 HOST_CPU
cross-node region member is unsupported: L4 HOST_CPU: L4/L3[0]/L2[0] DEVICE_AICORE, L4 HOST_CPU

_plan_device_hbm_member / _plan_host_shm_member / _plan_member interpolate _endpoint_label(member), then _unsupported() appends the offending labels again (comm_endpoints.py:580-582). Drop the label from the call sites and let _unsupported own the formatting. The tests assert with in, which is why this passes.

6. Converge the worker-path format with #1599

work-breakdown.md §4 item 2 asks for exactly one thing to be shared — the format, not the storage:

只共享路径的格式/格式化工具,不共享存储或语义

The storage split is implemented correctly on both sides. The format is not: three spellings are now in flight.

Source Spelling
this PR, EndpointRegistry L4/L3[0]/L2[5]
#1599, worker.py::_create_buffer_locked owner_worker_path=f"L{self.level}"
#1599, buffer.py:265 intern_worker_path(f"remote/{owner_worker_id}")

f"L{self.level}" agrees with this PR's root segment by coincidence; remote/3 does not parse under _PATH_SEGMENT_RE at all. One shared format_worker_path() fixes it, and it is cheapest now — #1599's side table has no consumers yet.

7. _require_ready_for_region_planning is a weaker duplicate of the _operation_lease admission fence

worker.py:5282-5287 re-checks _lifecycle is READY with the same error string as _operation_lease (worker.py:5257), but skips the two other conditions the lease enforces — _consume_worker_host_mapped_cleanup_error_locked and _ordered_cleanup_error. It then runs twice more per call (_plan_region_get_endpoint_registry → again), so one plan takes three _hierarchical_start_cv acquisitions. P0.2 centralised admission on the lease. Keep the static level < 3 check and drop the lifecycle branch.

8. Non-canonical adapter vocabulary

MaterializationMode.{HOST_DIRECT_MAP_ACCESS, HOST_COPY_ACCESS, DEVICE_IMPORT_ACCESS, HOST_SHM_ACCESS} against §3's "用规范的 adapter 名,不要自造": direct-map/device-peer, owner-delegated copy, explicit transfer, HCCL collective. explicit transfer — the one adapter that mints a new CanonicalBufferIdentity, and per §9.1 "在跨机上是常规手段,不是降级" — has no representation. Since W2's deliverable is the frozen vocabulary, this is the part most expensive to rename later.

9. 626 new lines, one docstring, no contract comments

The load-bearing invariants a reader cannot recover from the code are all undocumented: under excludes the path itself; overlapping at + under is an error rather than a dedupe; member order in ordered_member_endpoint_ids is selector order then (level, index) — which becomes the rank order; endpoint_id is registry-local. Per .claude/rules/comments.md these are exactly the present-tense facts worth a comment, and per doc-consistency.md §5 a frozen contract belongs in docs. Nothing in-repo records what W2 froze.


Consider

  • _plan_region can never return a supported plan in production. No production code assigns _platform_capability_cache (only test_comm_endpoints.py:37), and StaticPlatformCapabilityCache().get answers False for everything — so any region with ≥2 members returns UnsupportedRegionPlan. Verified. Fine for a plan-only PR; say so in the docstring rather than leaving a working-looking entry point.
  • Registry epoch invalidation is unreachable. Topology freezes at init() and CLOSED is terminal, so no registry is ever rebuilt and a usable registry's epoch is always 0. The PR body lists it as a delivered feature.
  • EndpointRegistry.from_worker reads five private Worker attributes on the root and every child, typed Any. A topology-snapshot accessor on Worker would keep worker.py owning its own shape.
  • EndpointSelector is exported and directly constructible, bypassing at() / under() validation (test_comm_endpoints.py:47 does this). Validate in __post_init__ or drop it from __all__.
  • §5/§6 encapsulation. The root builds one global registry including remote children's spec.device_ids, whereas §6 wants subtree selectors expanded "各 L3 本地展开" so "L4 不必知道 chip 拓扑". Today's RemoteWorkerSpec already hands the root that list, so this PR is not inventing the knowledge — but if local expansion is still the intent, this is the moment to say so.

CI and stale feedback

st-onboard-a5 is red, and it is not this PR. The single failure is

FAILED tests/st/a2a3/tensormap_and_ringbuffer/spmd_paged_attention_highperf/
  ::TestSpmdPagedAttentionHighPerf::test_run - RuntimeError: run failed with code -100

a device-side AICPU rc=-100. This diff is pure Python — no C++, no kernel, no scheduler — and the only behaviour change in worker.py outside new methods is one _invalidate_endpoint_registry() call in close(). The same test has prior history with this signature (#1070). I have re-run the failed job; if it reproduces it needs its own triage rather than silence.

The 9 existing CodeRabbit items on this PR are stale. All 7 inline threads are isOutdated and anchored to files absent from this 5-file diff (.github/workflows/ci.yml, docs/remote-l3-worker-design/*, examples/workers/l4/vector_add_mixed_l3/**, python/simpler/remote_l3_session.py), and the review body reviews _chip_descriptor_context / remote-spec encoding. They predate the branch reset onto c866c827; CodeRabbit has not re-reviewed since (Review skipped: incremental reviews are disabled). Nothing there needs answering.


Verification note. I could not run the new tests locally — the build-stamp guard correctly refuses a _task_interface from another worktree, and a fresh editable install fails on missing nanobind (a local setup gap, unrelated to this PR). CI's ut job is green on both ubuntu and macOS and covers exactly these two files. The selector semantics, the default-cache planning outcome, and the duplicated-label defect above were verified by importing comm_endpoints.py directly, which needs no extension.

@ccyywwen
ccyywwen force-pushed the w2-comm-endpoint-model branch 3 times, most recently from c719170 to edd611e Compare August 7, 2026 01:53
@ccyywwen

ccyywwen commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ChaoWao for the detailed review. The PR has been updated at commit
edd611e (Update: tighten W2 backend vocabulary) and now implements
the frozen W2 Python planning contract in comm_endpoints.py, worker.py, and
the W2 unit tests. Below is the point-by-point response to the review comments.

Current status: this PR is still Python planning-only. It freezes the W2
endpoint selector, Worker-owned topology snapshot, registry identity,
SingleOwner backend plan, and adapter/profile diagnostics. It does not add a
public region API, materializer, supported cross-node execution path, C++ wire
ABI, AccessMode, or handle-enforcement semantics beyond registry epoch and
invalidation.

Response structure: the first section covers the #1599 overlap and vocabulary
alignment, then the Must fix, Should fix and Considersections answer the numbered
review items directly.

Overlap With #1599

Accepted. W2 no longer defines a second backing vocabulary. comm_endpoints
imports BackendKind directly from _task_interface, so
comm_endpoints.BackendKind is the same canonical enum used by the #1599
buffer ABI. There is no Python fallback enum for no-extension imports.
RegionPartPlan uses those canonical values (FORK_SHM, POSIX_SHM,
VMM_WINDOW, REMOTE_SIDECAR, DEVICE_MALLOC, FORK_COW) and the old W2
BackingKind has been removed.

AccessMode is intentionally not copied into W2. The current contract is only
the endpoint-side half of the later attachment formula:

(member EndpointIdentity, region part, backend kind)
    -> selected adapter/profile attachment

CanonicalIdentity remains materializer / create_buffer work. W2 does not
mint buffer identities, buffer generations, ACLs, or handle enforcement.

Must Fix

1. Cross-node hard reject

Resolved. The resolver no longer returns a cross-node-specific failure before
capability evaluation. CROSS_NODE_UNSUPPORTED has been deleted. Cross-node
members resolve normally, then the backend resolver enumerates legal
adapter/profile candidates and returns ADAPTER_UNSUPPORTED only when every
candidate fails.

The failed candidates are reported through AdapterAttempt, preserving the
attempt order. For example, cross-node device members try
DEVICE_FABRIC_V2_PEER_IMPORT before the remote-copy candidates.

2. node_scope_id derived from remote status

Resolved. Node scope is now derived from normalized node identity, not from
whether the child is local or remote. localhost and loopback addresses map to
"local"; ordinary hosts/IPs use their lowercase host string; ports are not
part of the identity. Multiple remote L3s on the same normalized host share one
node scope.

This is covered by test_remote_registry_normalizes_node_identity_by_host_not_remote_status.

3. Bare registry-local endpoint ids in plans

Resolved for W2 plans. The stable endpoint reference carried by successful
plans is now:

EndpointIdentity(session_instance_id, registry_epoch, endpoint_id)

session_instance_id comes from Worker._owner_instance_id; registry_epoch
comes from the worker registry epoch; endpoint_id remains registry-local but
is no longer used alone in BackendPlan, SingleOwnerPlan, or
MemberAttachmentPlan.

EndpointRecord.endpoint_id remains only as a compatibility property for
registry-local code. Diagnostics keep full EndpointRecord values so messages
retain path, deployment, and node relation information.

4. Backing derived only from MemberSet

Partially narrowed to the W2 boundary. The old whole-region BackingKind and
LayoutSummary(total_bytes, ...) model is gone. W2 now has explicit
PAYLOAD and COUNTER region parts, each with its own RegionPartPlan and
canonical backend_kind field.

The implementation still does not create per-buffer CanonicalIdentity values
or perform materializer-backed mixed allocation. That is intentionally outside
W2. backend_kind names the provider-owned backing family for the part;
payload and counter may share the same BackendKind while still receiving
separate part-level attachment records. Adapter/profile candidate selection is
now threaded through RegionPartKind, but W2 does not freeze a different
payload-vs-counter candidate policy yet. The current planner validates
RegionLayoutSpec(payload_bytes, counter_bytes) and records selected
adapter/profile plans, leaving final per-buffer allocation and attachment to
the later materializer work.

Should Fix

5. Duplicated endpoint labels in unsupported messages

Resolved. _unsupported() is the single formatter for unsupported plan
messages and offending endpoints. Call sites pass plain messages and records;
they no longer append endpoint labels themselves. The test suite checks that a
reported endpoint label appears only once in the unsupported message.

6. Worker-path format convergence

Resolved on the W2 side. Endpoint paths are produced by one helper,
_format_worker_path, and parsed by one grammar. Local and remote worker paths
use the same canonical spelling, for example L4/L3[0]/L2[0]; W2 no longer
uses a remote/<id> endpoint spelling. Existing remote/<id> strings belong
to buffer-side diagnostic owner paths, not endpoint paths; they remain outside
the W2 endpoint path grammar and do not participate in endpoint identity,
CanonicalIdentity, routing, or capability decisions.

With #1599 now on main, W2 aligns with it by reusing the canonical
BackendKind vocabulary and by keeping endpoint identity separate from
CanonicalIdentity. W2 uses the canonical Lk[n] endpoint path spelling for
planning and depends on the #1599 _task_interface enum at import time;
buffer-side ownership, storage, diagnostics, and generation semantics remain
owned by the #1599 buffer model.

7. Duplicate lifecycle admission check

Resolved. _resolve_region_spec and _plan_region run under
_operation_lease, so READY admission, cleanup errors, and close-race
admission are centralized there. _require_ready_for_region_planning now keeps
only the static level >= 3 check.

close() invalidates the endpoint registry and increments the registry epoch.

8. Non-canonical adapter vocabulary

Resolved. MaterializationMode has been removed. The selected attachment and
attempt diagnostics now use separated canonical vocabulary:

  • AdapterKind: DIRECT_MAP, DEVICE_PEER, OWNER_DELEGATED_COPY,
    EXPLICIT_TRANSFER, COLLECTIVE
  • AdapterProfile: HOST_SVM_MAP, HOST_VMM_COPY,
    DEVICE_VMM_PEER_IMPORT, DEVICE_FABRIC_V2_PEER_IMPORT, HOST_SHM_MAP,
    REMOTE_COPY

EXPLICIT_TRANSFER and remote-copy profiles are represented as legal
candidates even when W2 reports them unsupported because the materializer does
not exist yet. Candidate ordering is a per-region-part boundary in code; W2
currently uses the same default order for PAYLOAD and COUNTER, leaving any
counter-specific attachment policy to the later materializer/control-buffer
work.

9. Contract comments and docs

Resolved through the W2 docs and focused code comments. The frozen contract is
now recorded in w2-review-impl.md, summarized in w2-endpoint-model.md, and
mapped to code in w2-endpoint-impl.md / w2-review-todo.md.

The code keeps comments limited to load-bearing facts: for example, the default
capability cache says real probes are later materializer work, and the
BackendPlan.topology_plan construction states that W2 defines only
SingleOwnerPlan while future topology-specific plans may extend that field.
Selector semantics such as under excluding self, duplicate overlap errors,
and canonical ordering are pinned by unit tests.

Consider

Production planning with the default capability cache

Accepted as W2 scope. StaticPlatformCapabilityCache is conservative by
default and returns unsupported unless a capability is injected. This means W2
can produce successful plans in tests or future probe-backed contexts, but
does not pretend that production materialization capability probing is already
integrated.

Registry epoch invalidation

Accepted with a narrower claim. W2 implements the epoch field and the
invalidation point only. Because topology is currently built before/at init and
CLOSED is terminal, W2 does not claim complete stale-handle enforcement. The
implemented behavior is: close clears the registry, increments the epoch, and
future registries produce different endpoint identities.

EndpointRegistry.from_worker reading Worker private attrs

Resolved. EndpointRegistry.from_worker has been removed. Worker owns topology
construction and exposes a private flat _EndpointTopologySnapshot; the
registry consumes only EndpointRegistry.from_snapshot(snapshot, registry_epoch=...).

Direct EndpointSelector construction

Resolved while keeping the value type exported. EndpointSelector.__post_init__
normalizes and validates kind, path, and deployment, so direct
construction no longer bypasses validation. The convenience constructors
at(...) and under(...) use the same normalization path.

Encapsulation and local expansion

Deferred to W5 execution semantics. W2 still builds a flat planning snapshot so
selectors can be resolved without a public region API. It does not introduce
L4-to-L2 execution or materializer behavior. The intended later execution model
remains: L4 declares, L3 expands and executes locally, and results are reported
upward.

The current snapshot uses RemoteWorkerSpec.device_ids because that topology
information already exists at the current API boundary. This is a planning-only
representation, not a new wire or materialization path.

@ccyywwen
ccyywwen requested a review from ChaoWao August 7, 2026 02:32
@ccyywwen

ccyywwen commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

W2 follow-up update: commit 24370ad replaces the global platform capability gate with the RegionAccessService.

Key changes:

  • Physically removed PlatformCapability / CapabilityResult / PlatformCapabilityCache / StaticPlatformCapabilityCache from the planner path.
  • BackendKind is now still derived only from provider deployment:
    • DEVICE_AICORE / DEVICE_AICPU -> VMM_WINDOW
    • HOST_CPU -> POSIX_SHM
  • _adapter_candidates(...) is now the single candidate-order entry point and distinguishes PAYLOAD vs COUNTER.
    • same-node host consumer + device provider + VMM_WINDOW
      • PAYLOAD: only OWNER_DELEGATED_COPY / HOST_VMM_COPY
      • COUNTER: DIRECT_MAP / HOST_SVM_MAP first, then fallback to OWNER_DELEGATED_COPY / HOST_VMM_COPY
  • Production/default service supports HOST_VMM_COPY for same-node host/device access.
  • Production/default HOST_SVM_MAP remains unsupported with scoped reason NO_IMPLEMENTED_DIRECT_MAP_PROBE, so this is not encoded as a permanent hardware limitation.
  • Worker now caches _region_access_service; _invalidate_endpoint_registry() clears it with the registry.
  • StaticRegionAccessService replaces the old static capability cache for UT injection.

Tests added/updated cover:

  • default host/device payload and counter both selecting HOST_VMM_COPY
  • payload candidate attempts excluding HOST_SVM_MAP
  • counter direct-map evaluator returning NO_IMPLEMENTED_DIRECT_MAP_PROBE
  • static service injection selecting counter direct-map
  • Worker close/invalidation clearing the service
  • comm_endpoints.__all__ export boundary, without expanding top-level simpler.*

Validation:
python -m pytest tests/ut/py/test_worker/test_comm_endpoints.py tests/ut/py/test_package_surface.py

Result: 31 passed.

@ccyywwen
ccyywwen force-pushed the w2-comm-endpoint-model branch from bbc3fd6 to 24370ad Compare August 7, 2026 06:32

@ChaoWao ChaoWao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Second-round review — 24370ad7

Thanks for the rework. 9 of the 11 findings from round 1 are fixed, and one came back better than I asked for: turning the capability lookup table into a capability service (RegionAccessService) is the right shape. A query that carries both endpoint records plus the backend is not just tidier — it is the correct shape for a reason that turns out to be bigger than this PR, see below.

Fixed since round 1: cross-node hard reject · node identity · EndpointIdentity · duplicate labels · path format · admission fence · adapter vocabulary · from_worker private-attribute access · EndpointSelector bypass. EndpointId now carries session_instance_id, which closes the deviation I recorded on 08-05.

CI is green (19 checks). Requesting changes on three blockers.


The organising principle, restated — this changed how I read the PR

Our design guideline was restated today, and it reframes both this PR and two of the blockers below. Quoting it because it is not in this repo:

Physical resources (memory, communication domains) are decoupled from the logical concept (Worker).
Whether a worker may hold or access a resource is decided by where it is deployed × the hardware connectivity from there to where the resource physically lives.
A worker's position in the logical tree (level / worker_path) decides only who may initiate, never whether it is allowed.

The consequence for this PR: capability(endpoint_deployment, resource_physical_identity) → allowed? is one function, and it currently appears in five places in the repo — RegionAccessService (at planning time, this PR) · ImportContext / materialize (at materialization time, P1-B) · AttachmentPlan (at domain construction) · TransportPlan adapter selection · comm_profile (cross-node consistency assertion). Those are not five similar things. They are one function evaluated at five moments.

Two decisions follow, both taken today by the maintainer:

Decision ① — the single authority for that judgement is this PR's registry. ImportRegistry.materialize in P1-B needs an endpoint × address_space gate to enforce "a device reference only reaches its owner chip". That is the same judgement RegionAccessService already computes. So W2's registry is the sole authority and ImportContext becomes a thin consumer of it — P1-B will not build a second matrix.

This is not "two teams collided, pick an owner". It is forced: the information the judgement needs is not on the wire.

field why it is not enough
owner_instance_id opaque nonce, carries no location
owner_worker_path_id by design "DIAGNOSTIC id only … takes part in no routing, visibility or identity decision"
address_space only HOST / DEVICE — does not say which card

Only a registry that knows deployment can supply it.

What this adds here: one binding table, owner_instance_id → owner endpoint, so a consumer holding a BufferDescriptor can resolve which endpoint minted it. Small, but it is what makes single-authority real rather than nominal.

Decision ② — session_instance_id and owner_instance_id stay shared, and the sharing is documented as deliberate, because given ① it is the bridge: BufferDescriptor.identity.owner_instance_id is the key into that binding table. See blocker 1.


Blockers

1. _owner_instance_id is assigned twice, at two different widths, and the second one is load-bearing

In Worker.__init__ the field is written first as uuid.uuid4().bytes (16 bytes, added by this PR), then overwritten by mint_owner_instance_id() (8 bytes, os.urandom(OWNER_INSTANCE_ID_BYTES), from #1599). The first line is dead — but the width is wrong, and #1599's docstring states that width is load-bearing ("Must stay a full-width random draw"). A reader who deletes the second line to remove the apparent duplication silently widens the nonce and breaks that contract.

Please delete the uuid.uuid4().bytes line, and per decision ② add a comment on the remaining one stating that EndpointIdentity.session_instance_id deliberately shares this nonce with BufferDescriptor.identity.owner_instance_id, because that is the key used to resolve a descriptor back to its minting endpoint.

Why a comment and not just a deletion. There is an open gate (G4) that will move the mint point to after fork/adoption. When it lands, every EndpointIdentity minted before it changes meaning — and stable identity across incarnations is exactly what this PR is for. It does not need solving here: EndpointIdentity currently never crosses a process boundary and is never serialized, so the coupling is latent, not live. But it has to be written down, or whoever closes G4 will not know it reaches W2.

2. Both region parts get one BackendKind, and the resulting plan is one that main's C++ validator already rejects

_backend_kind_for_provider hands both parts of a device provider VMM_WINDOW. _adapter_candidates then, underneath that VMM_WINDOW, offers DIRECT_MAP / HOST_SVM_MAP for the COUNTER part.

Three independent sources say that is illegal:

  1. The capability matrix (p1b-corrected-design.md §5.1) has HOST × VMM_WINDOW = ❌, and main already enforces it in C++: validate_buffer_descriptor raises "unsupported address_space x backend_kind (capability matrix)".
  2. Hardware: halHostRegister is documented Not support vmm va (CANN 9.0, ascend_hal_base.h). One backing cannot be both VMM peer-imported and host-registered.
  3. Invariant 5 (a device reference only reaches its owner chip).

So the Python planner can emit a plan C++ on main will refuse, and test_static_service_can_select_counter_direct_map_without_payload_direct_attempt pins that plan as expected behaviour. Nothing breaks today only because NO_IMPLEMENTED_DIRECT_MAP_PROBE sits in front of it — a probe stub, not a guarantee.

This is the principle above, in miniature: the plan was derived without consulting what the physical backing can actually do. The intended shape is already written down in hardware-constraints.md §5 — split into a control_buffer (host-mappable, non-VMM backing) and a payload_buffer (device VMM), choosing the adapter per buffer. The counter is the control buffer; its backing cannot be VMM_WINDOW. Either take part into account in _backend_kind_for_provider, or drop HOST_SVM_MAP from the counter candidates under VMM_WINDOW. The test has to change either way, since it currently asserts the illegal plan.

3. comm_endpoints became a hard dependency on the extension, and the test guarding that was deleted with no replacement

from _task_interface import BackendKind is a module-level import. The previous revision carried test_comm_endpoints_import_survives_without_the_extension, docstring "The W2 endpoint model is pure Python and must not import _task_interface". That test is gone in 24370ad7 and nothing replaced it.

To be clear: using the canonical BackendKind is correct — I asked for it in round 1. The problem is that a contract changed with no record. simpler/__init__.py still lists comm_endpoints in _LAZY_SUBMODULES, and laziness only defers the failure, it does not remove it. Either restore a test asserting the new contract (importing comm_endpoints requires the extension, and the failure is legible), or drop it from _LAZY_SUBMODULES and say in the module docstring that it is now extension-dependent. Deleting the guard while leaving the lazy registration is the one combination that records nothing.


One question, not a blocker

_require_ready_for_region_planning still carries if int(worker.level) < 3: raise, and it now covers planning as well as registry construction. Under the principle above, level may decide who may initiate but never whether it is alloweddomain-membership.md §2.3 says as much, while §2.4 says control-tree visibility is level's business.

I read your gate as the §2.4 kind. If that is right, one comment saying so closes this permanently and I will stop raising it. If it is ever consulted to decide reachability, it becomes the §2.3 violation.


Where this PR now sits

Stating it explicitly, because neither side knew until today: RegionAccessQuery carries two endpoint records plus the backend — exactly the endpoint × address-domain context ImportRegistry.materialize is missing. Before decision ①, the repo was on track to grow two endpoint matrices, one deciding "may this be planned" and one deciding "may this be materialized", free to disagree and very hard to merge once both had tests.

Practically: after the three blockers, this PR is the head of a short chain — #1696 → add the deployment dimension to the domain member struct (#1623/#1624 are held until then, because that struct is already on the cross-node wire) , and separately #1696 → G4 → ImportContext. Nothing for you to act on there; I mention it so the binding table in decision ① does not read as scope creep.

@ChaoWao
ChaoWao force-pushed the w2-comm-endpoint-model branch from 24370ad to 0b59501 Compare August 7, 2026 14:34
@ChaoWao

ChaoWao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

@ccyywwen Thanks — the rework answered round 1 well, and the capability service is a better shape than the lookup table I asked for. I've pushed the remaining pieces onto this branch rather than send you round-tripping on them; the branch is now one squashed commit on current main (d1ec68a4).

Blocker 2 — direct-map over a VMM backing. Dropped DIRECT_MAP / HOST_SVM_MAP from the VMM_WINDOW candidates for both parts, and put the reason at the exclusion.

I took the weaker of the two options the review offered, deliberately. The right fix is the one hardware-constraints.md §5 names — give the counter its own non-VMM, host-mappable backing — but no platform path allocates one yet (that is W0 → W1, and W0 is still unclaimed). Naming DEVICE_MALLOC today would freeze a guess into the vocabulary this PR exists to freeze. So the constraint is recorded and _backend_kind_for_provider says why both parts share one backing until a platform path exists. Worth revisiting the moment W0 reports.

The exclusion lives in candidate enumeration rather than in a RegionAccessService verdict on purpose: a service decision is overridable by an injected service, and no service should be able to admit a pairing no materializer can honour. The test now injects a permissive service precisely to prove the candidate never reaches it.

Blocker 1 — the duplicate nonce. Deleted the 16-byte uuid.uuid4().bytes write. The surviving assignment states that the sharing with EndpointIdentity.session_instance_id is deliberate and why — it is the key of the owner binding below — plus the note that a future move of the mint point carries both identities.

Blocker 3 — the extension contract. Added test_comm_endpoints_requires_the_extension_and_stays_lazy, pinning both directions: import simpler still survives without the extension, reaching simpler.comm_endpoints does not.

Decision ① — the binding table. Implemented. Each Worker's topology entry now carries the buffer-owner nonce it mints under, and EndpointRegistry.owner_endpoint(nonce) resolves it to the minting endpoint. Two boundaries are deliberate:

  • only host endpoints bind a nonce — a device endpoint is a view of a chip, not a buffer owner;
  • a remote Worker mints in its own process, so its nonce is absent rather than guessed and resolves to a typed OWNER_NOT_REGISTERED refusal. That binding has to arrive over a session channel; inferring it here would be exactly the guess the registry exists to replace.

The level gate. Your reading was right, and it now says so in a docstring: level >= 3 is control-tree visibility — who may declare a region over a subtree — never a reachability decision.

Verification: tests/ut/py 1217 passed / 13 skipped, ruff and pyright clean, all against the rebased tree. Two tests changed rather than were added: the one that pinned the illegal counter plan, and the one that reached for the removed candidate by index.

One note on the squash: your six commits are folded into one. The separation you had — vocabulary, then the access service — was legible, and I would normally leave it; a single commit is this repo's default for a PR and that is the only reason.

ChaoWao
ChaoWao previously approved these changes Aug 7, 2026

@ChaoWao ChaoWao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — all three blockers are closed, and two of them came back stronger than asked

Verified on 0b59501b by reading the code, not the commit message.

Blocker 1 — the double _owner_instance_id assignment: fixed. One mint point remains. The comment does more than record the deletion: it states why the sharing with EndpointIdentity.session_instance_id is load-bearing (it is the key of the owner_instance_id -> owner endpoint binding, and the descriptor cannot name its own minting endpoint because the nonce is opaque, owner_worker_path_id is diagnostic by contract, and address_space does not say which card), and it states the consequence for the future mint-point move — that endpoint identity and buffer identity have to move in one change. That is exactly the record I asked for, and the reasoning is now in the file rather than in a review thread.

Blocker 2 — the illegal HOST × VMM_WINDOW plan: fixed, and fixed at the right layer. The direct-map candidate is gone from enumeration rather than from a service verdict, with the CANN reason (Not support vmm va) cited in place. The comment explains why enumeration is the right layer: a plan an injected service accepted could not be materialized, so the exclusion must not be overridable.

The test change is what makes this convincing. test_host_direct_map_is_never_offered_over_a_vmm_backing injects a deliberately permissive service — one that would happily admit the pairing — and then asserts the candidate never reaches it. That proves absence from enumeration rather than rejection downstream, which a straightforward assertion could not distinguish. test_default_service_refuses_host_svm_map_when_asked_directly is kept as the backstop with a docstring saying why both exist. This is a stronger construction than the fix I suggested.

Blocker 3 — the deleted extension guard: fixed. test_comm_endpoints_requires_the_extension_and_stays_lazy pins the new contract in a subprocess with _task_interface poisoned: import simpler still survives, reaching simpler.comm_endpoints raises. It also asserts ce.BackendKind is _task_interface.BackendKind, which pins the single-vocabulary property that motivated the change. The docstring records that the old property was traded and what replaced it — that was the actual gap, not the import itself.

Decision ① — the binding table is there. EndpointRegistry.owner_endpoint(owner_instance_id) plus the owner_bindings construction path, and a duplicate nonce raises rather than silently binding to the last writer. A remote Worker's nonce is left absent rather than guessed, which is the right call — guessing it would have made the table lie for exactly the cross-node case it exists to serve.

The level < 3 question is answered. The docstring now says it is control-tree visibility, not a capability decision, and names what a capability decision would actually consult (deployment, interconnect, backing, live attachment — none of which appear there). Closing that one for good.

CI: 18 green, 1 skip.


One thing before merge: rebase, and be careful with the single conflict

main moved to aa1d7c7d (#1729, the Tensor wire flip) after this head was pushed. This PR now conflicts on one file, one hunk: python/simpler/worker.py.

The hunk is a relocation, not a removal#1729 moved self._owner_instance_id = mint_owner_instance_id() to a different point in Worker.__init__ (now around worker.py:3881). Nothing was deleted.

⚠️ Resolving it by taking this PR's side wholesale would reintroduce blocker 1 — you would end up with the relocated assignment from #1729 and this PR's assignment, i.e. the double mint we just removed, except now split across two places where it is harder to see.

The correct resolution is to keep #1729's single assignment at its new location and move this PR's comment block onto it. Nothing else in the hunk changes.

Approving on the current content; the rebase is mechanical, so I am not asking for another round — please just double-check that one hunk and confirm CI stays green after the rebase.

A domain's members are named today by bare chip index, which binds membership
to one level of the tree and cannot express a host process and a device view of
the same card as two distinct participants. This lands the endpoint model that
replaces it — an endpoint identity, a selector language, and a plan that picks a
transport adapter per region part — in Python only, with no materializer and no
wire ABI.

An endpoint is a `worker_path` plus a `deployment` (`HOST_CPU`,
`DEVICE_AICORE`, `DEVICE_AICPU`), so one path can carry several deployment
views of the same hardware. Paths are written by one helper and parsed by one
grammar (`L4/L3[0]/L2[5]`), shared with the buffer layer's diagnostic owner
path so the two cannot drift on spelling while keeping their storage separate.

`EndpointIdentity` carries `session_instance_id` and `registry_epoch` alongside
the registry-local id, so an id from a previous incarnation cannot silently
resolve against a later registry. The session nonce is the Worker's buffer-owner
nonce: sharing it is what lets `EndpointRegistry.owner_endpoint` turn the
`owner_instance_id` of a `BufferDescriptor` back into the endpoint that minted
it. The descriptor cannot answer that itself — the nonce is opaque,
`owner_worker_path_id` is diagnostic by contract, and `address_space`
distinguishes only HOST from DEVICE, never which card — so the registry is the
only place the judgement can live. A nonce minted in another process resolves to
a typed refusal rather than a guess.

`at()` names one endpoint, `under()` expands a subtree, and overlapping
selectors are refused rather than silently deduplicated. Node scope comes from
the normalized host of each Worker's endpoint, so a loopback remote worker
shares a node with its parent and two remote L3s on one host share a scope —
being remote is not the same fact as being on another node.

Reachability is not a property of a member set. `RegionAccessService` decides
per `(part, backing, provider endpoint, consumer endpoint)` and records every
refusal as an `AdapterAttempt`, so an unsupported region reports which adapters
were tried and why each failed. Cross-node members resolve normally and then
narrow to the adapters that can serve them, rather than failing for being
cross-node.

Adapters use the canonical names — direct map, device peer, owner-delegated
copy, explicit transfer, collective — with the implementation profile kept as a
separate axis, so "which adapter" and "how it is realized" stay independent.

Two constraints are structural and therefore live in candidate enumeration
rather than in a service verdict, which an injected service could override:

- No host direct-map candidate exists over a `VMM_WINDOW` backing.
  `halHostRegister` refuses a VMM VA ("Not support vmm va", CANN 9.0
  `ascend_hal_base.h`), so one backing cannot be both VMM peer-imported and
  host-registered, and a plan selecting it is one no materializer can honour.
  A host-mappable control buffer needs its own non-VMM backing, which no
  platform path allocates yet; `_backend_kind_for_provider` records why both
  parts share one backing until one does.
- Region planning admits only a Worker that owns a control subtree. That is
  control-tree visibility — who may *declare* a region — and never a decision
  about who may share memory, which no level participates in.

No `create_region`, no materialization, no C++ or wire changes. Capability
probing is not integrated, so the default service supports only same-node host
access to a device backing; everything else reports the adapter it would need.
`comm_endpoints` takes `BackendKind` from the extension rather than mirroring
it, so there is one backing vocabulary rather than two — which makes the module
extension-dependent, and a test pins that contract in both directions.
@ChaoWao
ChaoWao force-pushed the w2-comm-endpoint-model branch from 0b59501 to cdccc1d Compare August 7, 2026 15:08
@ChaoWao
ChaoWao merged commit c9e5f3c into hw-native-sys:main Aug 7, 2026
34 of 35 checks passed
ChaoWao added a commit that referenced this pull request Aug 11, 2026
…ry (#1782)

.docs flags this repeatedly: the "can this endpoint reach this backing"
judgment exists in two independent implementations -- the domain-scoped
EndpointRegistry/RegionAccessService capability engine (#1696) and P1-B's
ImportContext, which only ever raised freeform ValueError strings. The P1-B
closure audit (2026-08-11) recorded this as a naming gap to close via
"AttachmentPlan/TransportPlan 归位", scoped here to something small and safe
rather than a full unification.

Checked two obvious unification approaches and ruled both out, recording why
on ImportContext itself so they don't get re-proposed:

- Routing ImportContext's construction through the live EndpointRegistry
  object doesn't work across the fork boundary. ImportContext is built inside
  forked child processes (_sub_worker_loop, _run_chip_main_loop -- plain
  functions, not Worker methods) or at L2's same-process lazy-materialize
  point. Worker._get_endpoint_registry() walks the entire tree and requires
  _require_ready_for_region_planning(), a precondition that doesn't obviously
  hold at either of those points -- routing through it risks raising where a
  trivial attribute read works fine today.
- Swapping ImportContext.is_host_endpoint: bool for the domain-scoped
  EndpointDeployment enum (HOST_CPU/DEVICE_AICORE/DEVICE_AICPU) doesn't map
  onto the chip-fork model: a forked chip child is one process covering both
  AICore and AICPU roles, and EndpointRegistry's own topology snapshot
  doesn't track per-chip identity anyway (_append_device_endpoint_topology
  passes no owner_instance_id for device entries) -- so adopting the enum
  wouldn't even close ImportContext's known Worker-grained-not-chip-grained
  limitation, it would just force an ill-fitting split for no gain.

What both mechanisms already answer with the same underlying values, and can
share without either problem: the vocabulary naming *why* a capability check
failed. comm_endpoints.py's RegionAccessReasonCode already has
UNSUPPORTED_ENDPOINT_RELATION for exactly this question. ImportRegistry.
materialize()'s two DEVICE-backing rejections (host endpoint attempting a
DEVICE backing; device endpoint attempting a different chip's DEVICE backing)
are both instances of it. Prefixed both raised messages with the reason
code's value, keeping the existing prose intact so the pre-existing test
match= patterns ("host endpoint", "different chip's owner") keep matching
without edits. buffer.py now imports RegionAccessReasonCode from
.comm_endpoints -- checked for cycles: comm_endpoints.py imports only from
_task_interface today, a fresh one-way edge.

No change to ImportContext's fields, materialize()'s accept/reject logic, or
anything on the hot dispatch path beyond the two error-message strings, which
only build when materialize is about to raise anyway. Out of scope, both
deliberately: ImportRegistry.materialize()'s backend_kind switch (the
un-named adapter-selection logic for FORK_SHM/DEVICE_MALLOC/VMM_WINDOW/
POSIX_SHM) is the genuinely large, hot-path-risk piece of "AttachmentPlan/
TransportPlan 归位" and stays untouched; nothing in comm_endpoints.py itself
changes.

New test: both rejection messages carry RegionAccessReasonCode.
UNSUPPORTED_ENDPOINT_RELATION.value, verified against the pre-fix code first
(reverted the prefix, confirmed the new test fails with the plain "on a host
endpoint" message and no reason code, restored the fix and confirmed it
passes). The two pre-existing tests
(test_host_endpoint_materialize_refuses_a_device_tensor_directly,
test_chip_materialization_refuses_a_foreign_chips_device_tensor) pass
unmodified, confirming the change is additive.

Verified: pyut 1319 passed / 13 skipped / 0 failed; ruff check/format and
pyright clean; no circular import between buffer.py and comm_endpoints.py.
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