Skip to content

Add: host-orchestrator access to child-memory tensors - #2212

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:feat/issue-2205-orchestrator-device-view
Sep 13, 2026
Merged

Add: host-orchestrator access to child-memory tensors#2212
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:feat/issue-2205-orchestrator-device-view

Conversation

@ChaoWao

@ChaoWao ChaoWao commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

A host_build_graph orchestration runs on the host and reads an input's bytes to decide the graph shape — 11 orchestrations call get_tensor_data, 6 call set_tensor_data. That only ever worked as a side effect of staging: the bind registered the caller's host buffer as the read window while staging the tensor. A tensor declared child_memory=True takes the is_device_memory() pass-through and skips that registration, so an access to it hit report_fatal — residency and data-dependent orchestration were mutually exclusive.

The bind now claims each child-memory span, and how to reach it is decided on first access:

bind:   add_child_memory(base, size)          // a vector push; consults nothing

miss:   acquire_child_memory_host_view(...)
          non-null -> serve from the mapping
          null     -> serve each access with a device copy

Nothing is registered for a tensor the orchestration never touches, so the mapped set is exactly the accessed set — which is what #1848 concluded after measuring unconditional registration at ~256 ms/run on qwen.

Who owns the mapping, and why it isn't the bind

The mapping covers the whole tracked allocation and is owned by the runner that owns device_malloc / device_free. Two consequences: several tensors and views inside one child buffer share it, and free_tensor drops it before the pages go — a cached host VA outliving its allocation would otherwise hand out a live mapping into a returned page.

I initially wanted to skip the cache and rebuild per run, which is less code and has three arguments in its favour that hold regardless of cost. The measurement said no, on a rule fixed before the number was seen:

device=1 iters=300 buffers=4
         bytes    reg_med  unreg_med   pair_med
            64      1.580      3.741      5.321
          4096      1.610      3.600      5.210
        262144     11.340      7.420     18.760
     268435456    115.901   1649.126   1765.027

There is a ~5.2 µs per-call floor, flat from 64 B to 4 KiB. #1848 read the cost as essentially all per-byte, inferring it from the size asymmetry of one aggregate point — its own text says the split was never measured. Above ~1 MiB the slope is 7.0 ms/GiB, which does confirm it; both readings are right in their own regime. A realistic paged_attention bind reads two control tensors for 24 µs/run, ~4% of a 0.6 ms warm chip.run, against a 10 µs budget. Full write-up, including why register is per mapped page (so 256 KiB on ordinary pages costs more than 4 MiB on huge ones), in docs/investigations/2026-09-hbg-per-run-host-view-rebuild.md.

The three rebuild arguments survive as constraints on the cache rather than reasons to skip it: invalidation is why it sits next to mem_alloc_ and is dropped in free_tensor / finalize, and the monotonic-union and pinned-page concerns are why held bytes are logged.

No mirror on the fallback

Where no mapping is available the fallback re-copies per access rather than holding a host mirror. That keeps the path stateless — no allocation, no staleness flag, no refresh policy, no write-back ordering — so a read cannot serve stale bytes and a write reaches the device immediately.

It is not an a5-only branch: a5 onboard has no host-map path (device_runner_base.h:177 returns nullptr), and #1531 refuses ordinary-page small allocations on 64 KiB-page hosts, which is exactly this size class. The cost is one PCIe round trip per access, so accesses are counted and surface as devcopy=N in the bind's host_view_close attributes; docs/testing.md says when to leave a heavily-read tensor host-staged instead.

Runtime-created graph-heap tensors stay unreadable. Both report_fatal messages are updated to say that instead of naming child memory.

Testing

  • Negative control: the new scene tests fail on main with the exact report_fatal from the issue — get_tensor_data: FATAL(code=5): no host view for device address 0xaaaadb2ee3d0 (4 bytes) — and pass here. Built both trees to confirm.
  • The cache is doing its job, onboard a2a3: --rounds 5 establishes 2 mappings, not 10.
  • Onboard a2a3 under task-submit: small1_child_memory PASSED, both control tensors mapped via halHostRegister.
  • cpput 144/144 — 9 new cases covering resolve-once, mapping vs device-copy, copy failure, fail-closed bounds, and that a platform-owned mapping is not released by the accessor's close().
  • pyut 2285 passed, 18 skipped.
  • a2a3sim and a5sim sweeps over examples tests/st: 42 + 40 L3 cases each, 0 failures.
  • ruff, ruff format, pyright (0 errors), clang-format, clang-tidy, markdownlint, check_retired_names.

clang_tidy.py only indexes sim compile databases and silently skips files absent from them, so the onboard-only changes were run against the onboard database by hand — clean.

Fixes #2205

Fixes hw-native-sys#2205

A host_build_graph orchestration reads an input's bytes on the host to
decide the graph shape: 11 orchestrations call get_tensor_data and 6 call
set_tensor_data. That worked only as a side effect of staging, because the
bind registered the caller's host buffer as the read window while staging
the tensor. A tensor declared child_memory=True takes the is_device_memory()
pass-through and skips that registration, so an access to it hit report_fatal
and residency was mutually exclusive with data-dependent orchestration.

The bind now claims each child-memory span with add_child_memory, which is a
vector push and consults nothing. The first access landing inside a span
resolves how to reach it: acquire_child_memory_host_view returns a mapping,
or null and each access becomes a device copy. The mapped set is therefore
exactly the set the orchestration touched, which is what hw-native-sys#1848 concluded
after measuring unconditional registration at ~256 ms/run.

The mapping covers the whole tracked allocation and is owned by the runner
that owns device_malloc/device_free, so several tensors in one child buffer
share it and free_tensor drops it before the pages go. Measured on a2a3 /
CANN 9.0.0, a register/unregister pair costs ~5.2 us before any bytes are
mapped and ~7.0 ms/GiB beyond that, so a two-tensor bind would pay ~24 us
every run without that ownership -- see
docs/investigations/2026-09-hbg-per-run-host-view-rebuild.md, which also
corrects hw-native-sys#1848's reading that the cost is essentially all per-byte.

The device-copy path holds no state, so a read cannot serve stale bytes and
a write reaches the device immediately; it needs no staleness tracking. It
is not an a5-only branch -- hw-native-sys#1531 refuses ordinary-page small allocations on
64 KiB-page hosts, which is this size class -- so its accesses are counted
and reported as devcopy=N in the bind's host_view_close attributes.

Runtime-created graph-heap tensors stay unreadable, and both report_fatal
messages are updated to say so instead of naming child memory.

Verified: the new paged_attention child-memory scene tests fail on main with
the exact report_fatal and pass here; onboard a2a3 with 5 rounds establishes
2 mappings rather than 10, so the cost is amortized over the allocation.
cpput 144/144, pyut 2285 passed / 18 skipped, a2a3sim and a5sim sweeps green.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 39 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 0ac99771-bca1-4c44-bf44-82306568037d

📥 Commits

Reviewing files that changed from the base of the PR and between 1d1ddc8 and d17ae01.

📒 Files selected for processing (24)
  • docs/dfx/hbg-bind-phases.md
  • docs/dfx/l2-timing.md
  • docs/investigations/2026-09-hbg-per-run-host-view-rebuild.md
  • docs/investigations/README.md
  • docs/testing.md
  • examples/a2a3/host_build_graph/paged_attention_unroll_manual_scope/test_paged_attention_unroll_manual_scope.py
  • examples/a5/host_build_graph/paged_attention_unroll_manual_scope/test_paged_attention_unroll_manual_scope.py
  • src/a2a3/runtime/host_build_graph/host/runtime_maker.cpp
  • src/a5/runtime/host_build_graph/host/runtime_maker.cpp
  • src/common/host_build_graph/host/host_tensor_access.cpp
  • src/common/host_build_graph/host/runtime_core.cpp
  • src/common/host_build_graph/host_tensor_access.h
  • src/common/platform/include/common/host_api.h
  • src/common/platform/include/host/child_memory_host_view.h
  • src/common/platform/include/host/memory_allocator.h
  • src/common/platform/onboard/host/c_api_shared.cpp
  • src/common/platform/onboard/host/device_runner_base.cpp
  • src/common/platform/onboard/host/device_runner_base.h
  • src/common/platform/sim/host/c_api_shared.cpp
  • src/common/platform/sim/host/device_runner_base.cpp
  • src/common/platform/sim/host/device_runner_base.h
  • tests/st/a2a3/host_build_graph/paged_attention/test_paged_attention.py
  • tests/st/a5/host_build_graph/paged_attention/test_paged_attention.py
  • tests/ut/cpp/a2a3/test_hbg_tensor_access.cpp

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.

@ChaoWao
ChaoWao merged commit 748c39f into hw-native-sys:main Sep 13, 2026
36 of 37 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.

[Feature] Let a host-side orchestrator read and write device-resident tensors

1 participant