Skip to content

Refactor: own the AICore register tables per device context - #2277

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:refactor/own-the-aicore-register-tables-per-device
Sep 17, 2026
Merged

ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:refactor/own-the-aicore-register-tables-per-device

Conversation

@ChaoWao

@ChaoWao ChaoWao commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

The AICore MMIO register-address tables are device constants — one 8-byte entry per physical sub-core, derived from the driver for a device id and a register kind, unchanged for as long as the context holds that card. Two of their owners disagreed with that:

  • SlotPersistentArgs::regs held the Ctrl table per pipeline slot, so a depth-two runner derived and uploaded two byte-identical tables and paid the driver query twice. On a5 that query is a halResMap loop over every physical core, so the second copy also doubled the live driver-side mappings a worker holds.
  • a2a3's Pmu table had no cache at all. prepare_execution queried the driver, allocated and uploaded it on every PMU-enabled run; cleanup_execution freed it again — 600 bytes of addresses that cannot change. Refactor: reuse each pipeline slot's device argument blocks across runs #2222 recorded this as "the DFX buffers that genuinely vary per run, such as pmu_reg_addrs", but the Pmu table is built by the same function and loop as Ctrl with only a different addr_type. It is PMU-gated, not per-run varying; only the gate belongs to the run.

Both tables now live on DeviceRunnerBase for the context's lifetime, committed on first use by the arch's ensure_aicore_reg_table and released in finalize_common() beside the device-wall buffers. Only the driver query stays arch-specific, which is why the entry point is on the subclass. Kernel mode already owns the same table for its own lifetime via PersistentKernelArgs, and a context latches one mode permanently, so exactly one owner is ever live.

Each table carries an address and a committed flag rather than relying on a non-zero address. init_aicore_register_addresses records the address before the H2D copy and clears it afterwards only when a failed copy's rollback release also succeeded, so a retained address is owned but unwritten. Release keys on the address (such a block still has to be freed); reuse keys on the flag (handing the AICPU handshake an uncommitted table would deadlock the next task on a stream-sync timeout rather than fail the prepare). The retained address is passed back into the driver entry so it can reuse that block instead of stranding it.

Part of #2254, which proposed this as step 4. Step 1 is #2257.

Testing

A passing test cannot distinguish a cached table from a re-derived one, so reuse was measured with a temporary probe on a2a3 silicon (removed before commit):

suite preparations Ctrl commits Pmu commits
three DFX suites, --enable-pmu 2 14 (11 PMU-on) 1 1
pipeline_slots 5, spanning slot 0 and slot 1 1 (same address in both slots)
  • Hardware tests pass — full onboard sweep in CI shape (examples tests/st -m 'not sdma' --exclude-level 4), zero failures
  • Onboard DFX PMU smoke (--enable-pmu 2), both arches' path exercised on a2a3
  • tests/st/vis_isolation — the guard for init_aicore_register_addresses going through acl_to_hal_device_id
  • cpput 150/150, pyut 2393 passed / 7 skipped
  • Simulation tests pass — no simulation source is touched; simulation sizes its register blocks from a run's own worker count and genuinely is per-run
  • clang-format, tests/lint/clang_tidy.py, cpplint, check_retired_names.py

Not verified locally: a5 onboard. This box is a2a3, which is the right silicon for the Pmu half since a5 has no PMU register table; the a5 half is Ctrl-only and relies on CI.

🤖 Generated with Claude Code

The AICore MMIO register-address tables are device constants: one 8-byte entry
per physical sub-core, derived from the driver for a device id and a register
kind, and unchanged for as long as the context holds that card. Two of their
three owners disagreed with that.

`SlotPersistentArgs::regs` held the Ctrl table per pipeline slot, so a depth-two
runner derived and uploaded two byte-identical tables and paid the driver query
twice. On a5 that query is a `halResMap` loop over every physical core, so the
second copy also doubled the live driver-side mappings a worker holds.

a2a3's Pmu table had no cache at all: `prepare_execution` queried the driver,
allocated and uploaded it on every PMU-enabled run and `cleanup_execution` freed
it again, for 600 bytes of addresses that cannot change. hw-native-sys#2222 recorded the
reasoning as "the DFX buffers that genuinely vary per run, such as
`pmu_reg_addrs`, keep their per-run lifetime", but the Pmu table is built by the
same function and the same loop as Ctrl with only a different `addr_type`. It
does not vary per run; it is PMU-*gated*. Gating and lifetime are orthogonal,
and only the gate belongs to the run.

Both tables now live on `DeviceRunnerBase` for the context's lifetime, committed
on first use by the arch's `ensure_aicore_reg_table` and released in
`finalize_common()` beside the device-wall buffers. Only the driver query stays
arch-specific, which is why the entry point is on the subclass: a2a3 maps two
MMIO pages selected by an `AicoreRegKind`, a5 one through a different signature.
Kernel mode already owns the same table for its own lifetime through
`PersistentKernelArgs`, and a context latches one mode permanently, so exactly
one of the two owners is ever live.

Each table carries an address and a committed flag rather than relying on a
non-zero address. `init_aicore_register_addresses` records the address before
the host-to-device copy and clears it afterwards only when a failed copy's
rollback release also succeeded, so a retained address is owned but unwritten.
Release keys on the address, because such a block still has to be freed; reuse
keys on the flag, because handing the AICPU handshake an uncommitted table would
deadlock the next task on a stream-sync timeout instead of failing the prepare.
The retained address is passed back into the driver entry, which is what lets it
reuse that block instead of stranding it.

One consequence is worth stating: both kinds fold in the live AICore
power-gating bitmap, so a committed table is a snapshot taken at first use.
That was already true of Ctrl, whose dispatch addresses were never refreshed
once a slot committed them. Committing Pmu the same way removes a disagreement
rather than creating one — a per-run Pmu query could only ever produce a mask
newer than the dispatch table it was measuring.

A single shared commit flag needs no lock: `try_reserve_native_run` admits a
second reservation only once the first is the active run, so a successor's
preparation begins after its predecessor has launched and at most one thread is
inside `prepare_execution`. A prepared successor overlaps device execution, not
another preparation. `ensure_device_wall_buffer` already relies on this. If
submission depth is ever generalised beyond the fixed two slots, every
single-guard `ensure_*` on the prepare path has to be revisited together.

Verified on a2a3 silicon with a temporary probe, since a passing test cannot
distinguish a cached table from a re-derived one. Across three DFX suites, 14
preparations of which 11 enabled PMU produced one Ctrl commit and one Pmu commit,
every preparation carrying the same two addresses. The pipeline-slot suite's five
preparations spanned both slots and produced one Ctrl commit, both slots reading
the same address. Full onboard sweep, the DFX PMU smoke, `vis_isolation`, cpput
and pyut all pass; no simulation source is touched, since simulation sizes its
register blocks from a run's own worker count and genuinely is per-run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 28 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: bcd3bf4a-3b97-446a-97d8-85b7797c2667

📥 Commits

Reviewing files that changed from the base of the PR and between d0ed672 and 49559c6.

📒 Files selected for processing (10)
  • src/a2a3/platform/include/common/kernel_args.h
  • src/a2a3/platform/onboard/host/device_runner.cpp
  • src/a2a3/platform/onboard/host/device_runner.h
  • src/a5/platform/include/common/kernel_args.h
  • src/a5/platform/onboard/host/device_runner.cpp
  • src/a5/platform/onboard/host/device_runner.h
  • src/common/platform/onboard/host/device_runner_base.cpp
  • src/common/platform/onboard/host/device_runner_base.h
  • src/common/platform/onboard/host/device_runner_helpers.cpp
  • src/common/platform/onboard/host/device_runner_helpers.h

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 31f348e into hw-native-sys:main Sep 17, 2026
20 checks passed
@ChaoWao
ChaoWao deleted the refactor/own-the-aicore-register-tables-per-device branch September 17, 2026 03:42
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