Skip to content

P1-B: BufferHandle memory-model ABI + user-facing Tensor cutover - #1453

Draft
YunjiQin wants to merge 10 commits into
hw-native-sys:mainfrom
YunjiQin:p1-b-buffer-handle-abi
Draft

P1-B: BufferHandle memory-model ABI + user-facing Tensor cutover#1453
YunjiQin wants to merge 10 commits into
hw-native-sys:mainfrom
YunjiQin:p1-b-buffer-handle-abi

Conversation

@YunjiQin

@YunjiQin YunjiQin commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

P1-B: BufferHandle memory-model ABI + user-facing Tensor cutover

Replaces the legacy "raw pointer + child_memory bool" L3+ memory mechanism with a typed,
self-describing BufferHandle ABI, and flips the live L4→L3 / L3→L2 / L3→SUB dispatch wire from
address-carrying tensor blobs to identity-carrying ones.

The user-facing argument type stays named Tensor at every level — handle.tensor(shapes, dtype),
args.add_tensor(t, tag). What changed underneath it is what a Tensor carries: a canonical
identity plus a BufferHandleDescriptor, instead of a raw address and an is_child flag. BufferRef
is the C++ wire struct's name and does not appear in user code; the chip-side POD keeps its own name,
ChipTensor, and stays internal to the L2 runtime.

User guide: docs/buffer-handle-abi.md — including the
"Why Tensor and ChipTensor are two types" section, which is the design question a reviewer is most
likely to raise (field-by-field, with the two rejected unifications and why).

Please review commit by commit. The diff is large, but each commit is self-contained and
ordered so the wire ABI lands before its consumers. Reviewing the squashed diff loses that order.

Commits (read in order)

  1. Add the BufferHandle/BufferRef memory-model ABI + owner create_buffer — wire types
    (child_memoryaddress_space), versioned blob codec, owner BufferHandle + ImportRegistry
    (lazy map-once by canonical identity), self-describing refs, FORK_SHM COW base VA,
    Worker.create_buffer.
  2. Flip the live dispatch wire; three-way consumer split; device/remote/strided
    TaskArgs carries the new wire element through the C++ dispatch; infer_deps keys on canonical
    identity; chip → materialize → run_from_blob, sub → MappedArg (torch.frombuffer), L4→L3 →
    re-export; DEVICE_MALLOC + REMOTE_SIDECAR + strided materialization.
  3. Resource APIs + ABI doc + cpput migration; remove TensorTaskArgs; L2 in-process materialization
    alloc_shared_tensor / alloc_child_tensor; migrate the C++ GoogleTest suite; fold TensorTaskArgs
    onto ChipStorageTaskArgs; uniform L2 in-process materialization.
  4. Move device-memory ownership from Orchestrator to Workermalloc/free/copy_to/copy_from/
    alloc_child_tensor live on the Worker (keyed by owner_worker_id); orch.* are thin forwards; the
    raw-pointer orch.malloc / orch.device_handle are removed.
  5. Comm-domain VMM_WINDOW buffers; migrate examples & scene tests — a memoized FORK_SHM handle over
    a pre-fork host tensor; allocate_domain hands back VMM_WINDOW BufferHandles
    (domain.buffers[name].ref); migrate every collective / demo / L2-L3 example and the scene-test
    framework.
  6. ChipStorageTaskArgs chip-only; delete create_host_buffer; sub scalars; orch.alloc→handle
    ChipStorageTaskArgs is the chip-only POD (ChipWorker._run_slot); Worker.run at L2 takes a
    TaskArgs/None; delete the create_host_buffer/MAP_HOST subsystem (superseded by
    create_buffer + lazy import); sub callables receive scalars; orch.alloc returns a BufferHandle.
  7. Capability matrix + access⊆granted dispatch guards — reject an unsupported
    address_space × backend_kind on both owner construction and wire decode; reject an argument whose
    TensorArgType needs access the handle does not grant.
  8. Slim the buffer ABI, validate it, and stage host copiesCanonicalIdentity 96→32 B,
    BufferRef 272→144 B; the owner worker path leaves the identity and becomes a diagnostic
    owner_worker_path_id that takes part in no routing decision; one shared validate_buffer_ref at
    the three boundaries that accept untrusted bytes (builder, blob decode, materialization); a
    DEVICE-space tensor bound for a Python sub-worker is refused at submit; control-plane copies stage
    through a POSIX shm instead of sending a host address across the fork (a host BufferHandle
    transfers by name with no intermediate copy). Also fixes an out-of-bounds table read in
    get_element_size reachable from the wire.
  9. Submit-time argument guard and a copy-on-write backend tag — re-check access ⊆ granted where
    the values are final (a tag stays mutable after add_tensor), and reject two arguments of one task
    that name intersecting bytes of one backing — nothing downstream can catch that pair. Split the
    fork-inherited backend into FORK_SHM / FORK_COW so the decoder can enforce that a
    copy-on-write page grants READ only. Also fixes a stale handle-memo hit when the allocator reuses a
    storage address at a different size.
  10. Rename Worker.make_ref_argmake_tensor_arg — the last place the wire type's old name
    reached user code.

Verification

  • a2a3sim: 43 scene tests — allreduce collective ST corpus + L2/L3 examples (allreduce,
    domain_rank_map, dual_domain_overlap, ffn_tp_parallel, async/deferred notify, vector_add,
    create_buffer, child_memory, l3_dependency/group, dynamic_register) — golden 0.
  • onboard a2a3 (Ascend910): L3→L2 chip dispatch, mixed host+device, L4→L3→sub re-export,
    sdma_async_completion.
  • pyut: 938 passed, 2 skipped.
  • cpput: 67/67 (no-hardware subset).
  • ruff / pyright / clang-format / cpplint / markdownlint clean; every commit passes the pre-commit gate.

Two bugs the new validators caught in existing code, both true positives: a scene-test fixture holding
a 16×int32 view at offset 4 in a 64-byte backing, and a C++ make_ref() that never set
handle.nbytes.

Rebased onto current main. Three of the landed changes interact with this branch and are folded into
the commits that own them, not bolted on at the tip: the launch-acceptance fence's extra run_from_blob
arguments now ride the materialized blob; interior copy_to/copy_from ranges keep their
allocation-extent check after the ops moved onto the Worker, so a BufferHandle's recorded extent is
what admits a partial update; and the depth-two pipeline-slot tests build Tensor args for
Worker.run while keeping the runtime.so-ABI POD for the direct-lease path.

Remaining (follow-ups, not in this PR)

  • Buffer lifecycle: release_buffer, in-flight retain / deferred free, generation bump on slot
    reuse, and dropping an import mapping with its handle.
  • P2 (remote): the send side (RemoteTaskArgsWire) still carries chip-tensor metadata over the
    wire; full remote e2e. REMOTE_SIDECAR materialization stays reserved for P2.
  • Heap separation (SharedBufferArena): alloc_shared_tensor is the interim runtime-managed
    intermediate; the dedicated arena is a follow-up.
  • Dependency-key precision: identity-keyed infer_deps deliberately keeps the same behavior as
    today's L3 + TensorMap mainline; tightening it is out of scope here.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

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: c60163b2-5eac-467e-a8fd-7507132696e0

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

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.

@YunjiQin
YunjiQin force-pushed the p1-b-buffer-handle-abi branch from ec3f15f to d82c15b Compare July 27, 2026 03:31
@YunjiQin YunjiQin changed the title P1-B: BufferHandle/BufferRef memory-model ABI + L3→L2 live cutover (phases B+C) P1-B: BufferHandle/BufferRef memory-model ABI + full user-facing Tensor→BufferRef cutover Jul 27, 2026
@YunjiQin
YunjiQin force-pushed the p1-b-buffer-handle-abi branch from d82c15b to 41cb93e Compare July 27, 2026 06:18
@YunjiQin
YunjiQin force-pushed the p1-b-buffer-handle-abi branch 3 times, most recently from 3e6cde6 to cc65ce6 Compare July 27, 2026 12:40
@YunjiQin YunjiQin changed the title P1-B: BufferHandle/BufferRef memory-model ABI + full user-facing Tensor→BufferRef cutover P1-B: BufferHandle memory-model ABI + user-facing Tensor cutover Jul 30, 2026
YunjiQin and others added 2 commits July 30, 2026 01:43
The typed, self-describing BufferHandle / BufferRef ABI that replaces the raw-pointer + child_memory
mechanism: wire types (child_memory→address_space), versioned blob codec (write/read_bufferref_blob),
owner-side BufferHandle + create_host_shared_buffer + ImportRegistry (lazy map-once by canonical
identity), self-describing refs (embedded descriptor, no eager handshake), the BufferRef view algebra
(slice/transpose/permute/view/reshape), FORK_SHM COW-inherited base VA, Worker.create_buffer, and
MAILBOX_SIZE 32768→40960 for the 272 B refs wire. Python buffer_handle.py mirrors buffer_handle.h
byte-for-byte (sizes pinned to the binding).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…evice/remote/strided

Flip TaskArgs Tensor→BufferRef end-to-end through the C++ dispatch; infer_deps keys a local ref by
canonical identity. Three-way consumer split (only the chip leaf rebuilds a C++ Tensor): chip →
materialize → run_from_blob; Python sub-worker → MappedArg (torch.frombuffer); nested L4→L3 → re-export
(no map on the forwarding hop, canonical identity invariant across the edge — frozen model §5/§8: an
L4-owned backing forwarded L4→L3→L2 keeps ONE identity at all three layers, so deps key on identity not
VA and aliases/retain-release do not split). Device-memory path (DEVICE_MALLOC materialize/wrap),
REMOTE_SIDECAR backend-rewrite at add_ref, and strided-ref → strided-Tensor materialization. Proven
L3→L2 + L4→L3→sub in sim and onboard a2a3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@YunjiQin
YunjiQin force-pushed the p1-b-buffer-handle-abi branch from 4aa2ab6 to 2b2e49d Compare July 30, 2026 09:50
YunjiQin and others added 8 commits July 30, 2026 03:04
… L2 in-process materialization

C-phase resource APIs (Worker.alloc_shared_tensor, Orchestrator.alloc_child_tensor); docs/
buffer-handle-abi.md (identity invariant across every edge; re-export preserves it); migrate the C++
GoogleTest orchestrator/scheduler/remote/child_memory tests to the BufferRef ABI; remove TensorTaskArgs
(fold onto ChipStorageTaskArgs); restore device-pointer provenance over the BufferRef wire; relax
create_buffer to L2 and materialize BufferRef args in-process on an L2 run (uniform user API at every
level).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Worker owns and manages the device-memory lifecycle (malloc / free / copy_to / copy_from /
alloc_child_tensor, keyed by owner_worker_id); the Orchestrator's memory APIs become thin forwards. The
raw-pointer orch.malloc / orch.device_handle are removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… scene tests to BufferRef/handle

Worker.make_ref_arg + simpler_setup.make_tensor_ref name a pre-fork host tensor as a memoized FORK_SHM
ref; alloc_shared_tensor becomes the alloc→BufferRef managed intermediate; comm-domain windows are
exposed as VMM_WINDOW BufferHandles (domain.buffers[name].ref). Migrate the L2 examples, task_timing st,
child_memory, and the scene-test L3 rehost path / L3 group + dynamic_register scene tests / post-fork
host-buffer ST onto create_buffer + BufferRef args; delete the dead _rewrite_blob_host_addrs; sync
docs/comm-domain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alars; orch.alloc→handle

Complete the Tensor removal from the user-facing path:
- ChipStorageTaskArgs is chip-only (ChipWorker._run_slot); Worker.run at L2 takes a BufferRef TaskArgs
  or None (legacy passthrough removed); migrate the remaining wire-TaskArgs pyut + remote
  materialization + test_callable_identity + scene-rehost UT fakes off add_tensor.
- Delete the create_host_buffer / MAP_HOST subsystem (superseded by create_buffer + lazy ref).
- handle.ref(dtype=) accepts a DataType enum; Python sub callables receive scalars (MappedArgs).
- orch.alloc returns a BufferHandle over a single identity-keyed Orchestrator::alloc; finish Tensor
  removal from the L3-L2 message queue / region.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two O(1) warm-path static checks per the frozen model (worker-memory-model §1.4/§6, bufferhandle-abi
§3/§4.1), no ABI/wire change:

- Capability matrix: BufferHandleDescriptor.__post_init__ rejects an unsupported
  address_space×backend_kind (the four illegal combos HOST×VMM_WINDOW / HOST×DEVICE_MALLOC /
  DEVICE×FORK_SHM / DEVICE×POSIX_SHM). Runs on both owner construction and wire decode, so a bad combo
  fails before dispatch and cannot ride the wire. REMOTE_SIDECAR rows stay allowed here (its P1 blanket
  reject is ImportRegistry.materialize's job).
- access⊆granted: TaskArgs.add_ref (C++ binding) rejects an arg whose TensorArgType needs access the
  handle does not grant (INPUT→READ, OUTPUT_EXISTING→WRITE, INOUT→READWRITE). Catches e.g. a READ-only
  FORK_SHM COW buffer tagged OUTPUT_EXISTING (a forked child's writes would silently not reach the
  parent).

Not implemented here (already covered / P2, per the task doc): device→owner-chip topology
(_child_prov_check_dispatch), overlapping-alias (infer_deps serializes by identity), unknown
abi/backend/REMOTE_SIDECAR reject (already), stale-generation/ABA/visibility (P2).

Tests: test_descriptor_rejects_bad_capability_combo / accepts_legal_combos;
TestAddRefAccessSubset. Verified a2a3sim async_notify golden 0 (guards don't misfire on legit flows).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Name the Python task-argument type `Tensor` so data is named the same way
at every level. The chip-side POD keeps its own name, `ChipTensor`, and
stays internal: users write `handle.tensor(shapes, dtype)` and
`args.add_tensor(t, tag)`, and the wire type name does not appear in user
code at all. The view algebra leaves the Python surface — it had no
in-repo caller and no precedent on the C++ binding — while the wire keeps
explicit strides so a singleton dimension is never normalized away.

Slim the wire ABI while changing it is still cheap:

- `CanonicalIdentity` 96 -> 32 B, fixed-length with no length field, so
  hashing and comparison stay in bounds for any bytes that arrive. The
  owning worker path leaves the identity and becomes a diagnostic
  `owner_worker_path_id` whose side table lives only in the owning
  process; it takes part in no routing, visibility or identity decision.
- `owner_instance_id` 16 -> 8 B, still a full-width random draw, since it
  is now the sole source of cross-incarnation uniqueness.
- Drop `descriptor_version` and `visibility`, turn `abi_version` into a
  `magic` sentinel, and shrink `body` 96 -> 32 B. `BufferRef` is 144 B.
- `generation` starts at 1 and 0 is rejected on decode. Padding is
  excluded from equality and hashing, so two decodes of one backing
  always key alike instead of splitting into two buckets.

Validate every wire element through one shared `validate_buffer_ref` at
the three boundaries that accept untrusted bytes: the builder, blob
decode, and materialization. It bounds `body_len` and `ndims`, checks the
address_space x backend matrix, requires strides > 0 and a known dtype,
and rejects a view that extends past its backing.

Refuse a DEVICE-space tensor bound for a Python sub-worker at submit
instead of letting the child dereference a device address as a host
pointer, with a second check at materialization for any later submit path.

Stage control-plane copies through a POSIX shm rather than sending a host
address across the fork. A parent address is only meaningful in the child
when its allocation predates the fork, which callers cannot be required to
guarantee; the child_memory example now allocates its H2D source after
init() to cover that case. A host BufferHandle transfers by name with no
intermediate copy. copy_to/copy_from also validate the handle they are
given: address space, backend reachability, access direction and length.

Also fixes get_element_size reading past its table for an out-of-range
dtype — reachable from the wire, and inherited by the new validation.
Check two things where the argument values are final, in Orchestrator's single
submit path so no entry point can skip them.

`access ⊆ granted` is re-checked at submit rather than trusted from add time,
because a tag stays mutable after its element is added — the pair that governs
the dispatch is the one present now, not the one that was checked.

Two arguments of one task that name intersecting bytes of the same backing are
rejected. Nothing downstream can catch that pair: they belong to one node, so
there is no order between them to express, and a host backing staged to the
device becomes two independent copies that do not alias for the L2 overlap map
to notice. Disjoint slices of one buffer stay legal, and group members are not
compared against each other — naming one buffer as every member's output is how
a group publishes a completion token for a downstream task.

Split the fork-inherited backend in two. FORK_SHM and FORK_COW resolve
identically, but a child's write to a MAP_SHARED page reaches the owner while a
write to a copy-on-write page splits it into a private copy the owner never
sees. Making that a classification rather than something inferred from `access`
lets the decoder enforce it: FORK_COW grants READ only. `make_ref_arg` now
measures which one it has instead of granting READWRITE unconditionally — a
plain tensor is copy-on-write only where a fork stands between the writer and
this process, so an L2 leaf, which consumes its args in-process, still gets a
writable grant.

Also fixes make_ref_arg's handle memo returning a stale entry when the
allocator reuses a storage address at a different size. The memo is keyed by
address so that every view of one storage shares an identity; a hit whose size
no longer matches is a different backing and now gets a fresh identity, instead
of lending its nbytes to views that overrun it and fusing two unrelated buffers
into one dependency node.
The Python surface names its task-argument type `Tensor`, so the method that
builds one from a host tensor should say so too — `make_ref_arg` was the last
place the wire type's old name reached user code, and examples call it directly.

`simpler_setup.torch_interop.make_tensor_arg` keeps its name: it lives in a
different namespace and builds the chip POD for the direct ChipWorker path. The
two now sit in one file, so each states at its definition and at its use which
side of materialization it produces — a `ChipTensor` carrying an address, or a
`Tensor` carrying an identity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant