Skip to content

Switch HSA/ROCR dispatch to hsaco/kernel-object loading - #158

Merged
erwei-xilinx merged 10 commits into
mainfrom
worktree-hsa-hsaco-dispatch
Oct 28, 2026
Merged

erwei-xilinx merged 10 commits into
mainfrom
worktree-hsa-hsaco-dispatch

Conversation

@ypapadop-amd

@ypapadop-amd ypapadop-amd commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Switches the HSA runtime from dispatching AIE kernels by raw PDI/instruction addresses to ROCR's hsaco model: aircc's artifact (xclbin on npu1, full ELF on npu2) is packed into an hsaco with mlir-aie's aie-hsaco, loaded through the HSA code-object-reader/executable flow, and dispatched by kernel object. Current ROCR has removed raw-address dispatch, so this is required to keep the HSA path working.

Changes

  • HsaRuntime.{h,cpp}: load an hsaco and dispatch by kernel object. triton_npu_hsa_prepare(hsaco_path, kernel_name) replaces (pdi_path, insts_path); triton_npu_hsa_prepare_with_variables also defines agent variables before loading. The in-tree full-ELF loader (AieFullElf.h) and triton_npu_hsa_patch_insts are removed; ROCR does that work now.
  • driver.py, hsa_launcher.py, multilaunch.py: HSA uses the same output format as XRT, packs and caches the hsaco next to the other artifacts, and generated launchers pass (hsaco_path, kernel_name).
  • HSA decode (examples/llm_q4nx/hsa_decode.py): the context length goes through the full ELF's control scratchpad. The host allocates it and defines it as scratch-pad-ctrl when loading; ROCR flushes it per dispatch. The patched-instruction-stream decode is removed.
  • utils/rocm-requirements.txt: rocm-sdk-core==10.3.0a20261027, the first nightly with feat(rocr/aie): resolve a full ELF's external symbols against defined variables ROCm/rocm-systems#14004 (scratchpad as external variable) and #13908 (PDI placement fix). Older nightlies give wrong results on repeated full-ELF dispatches or fail to load the decode.

Not supported on HSA after this PR

  • mlir-air's fused prefill (ENGINE=auto RUNTIME=hsa): hsa_xrt.bo._prepared raises, since it relied on patching instruction streams in place.
  • Gemma4-E2B decode (supports_hsa=False): its PLE engine has no full-ELF build.

Test plan

On Krackan with the pinned wheel:

  • Llama-3.2-1B HSA decode, 300 tokens: identical to XRT; 63 tok/s (XRT 64).
  • AMD_TRITON_NPU_RUNTIME=hsa: axpy, matmul_bf16_m64_n64_k64, test_softmax pass.
  • examples/llm_q4nx/test_decode_artifact.py passes.
  • scripts/test_hsa_hsaco_pack.py fails in aircc ("failed to legalize func.func"), independent of this PR's runtime changes.

🤖 Generated with Claude Code

ypapadop-amd and others added 5 commits October 8, 2026 16:02
Replace the raw PDI+instruction-address dispatch path with ROCr's
code-object-reader/executable/freeze flow: HsaRuntime now loads an
hsaco file into a frozen HSA executable and dispatches by the kernel
object HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_OBJECT resolves it to,
instead of filling in raw PDI/instruction-stream device addresses.

triton_npu_hsa_prepare(hsaco_path, kernel_name) replaces the old
(pdi_path, insts_path) signature; triton_npu_hsa_patch_insts is
removed along with the dev-pool PDI/insts loading machinery it
depended on, since there is no longer a per-dispatch instruction
stream to patch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Update hsa_launcher.py to generate C++ code that calls
triton_npu_hsa_prepare with hsaco_path and kernel_name parameters,
replacing the previous pdi and insts parameters. This matches the ABI
change made in Task 1 to the shared HSA runtime.

Changes:
- Update py_set_paths function to accept hsaco_path and kernel_name
- Update comment describing g_program handle
- Update PyMethodDef doc string for set_paths
- Update module docstring to reflect new API
- Update _generate_hsa_launcher docstring to explain kernel_name handling

Verified with Python string template substitution check.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
driver.py now lets the HSA runtime auto-detect its output format the same
way XRT does (ELF on npu2, xclbin on npu1) instead of forcing a raw "pdi"
format, packs aiecc's artifacts into an hsaco via aie.compiler.hsaco.pack,
caches it alongside the other compiled artifacts, and passes it to the
generated launcher's set_paths(hsaco_path, kernel_name).

Also fixes a latent bug in the cache-hit path: the "hsaco missing, force a
rebuild" guard only cleared cached_artifacts, not the separately-gated
cache_path the rebuild branch actually checks.

Verified the new _pack_hsaco() helper directly against real previously
compiled PDI+insts and full-ELF artifacts (both produce a structurally
valid, correctly-tagged hsaco section per aie.compiler.hsaco.dump). Could
not get a full live Triton-JIT compile through this path to green in this
session's ad hoc venv -- aircc itself fails the same generic MLIR
legalization error for every kernel tried, reproduced by invoking it
directly with no Triton/driver.py code involved, so this is an environment
issue for Task 4 (real hardware) to resolve, not a defect in this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The final whole-branch review caught a real gap the plan missed: this
hsaco/kernel-object switch silently broke examples/llm_q4nx/hsa_decode.py,
the HSA decode path for llama-3.2-1b/3b, qwen3-4b, and gemma3-4b. It called
triton_npu_hsa_prepare(pdi_path, insts_path) and patched 248 L-dependent
instruction words per token in the runtime-owned buffer the dispatch packet
pointed at -- a buffer that no longer exists once instructions live inside a
frozen ROCr executable.

Porting this to a per-context-length packed-executable cache (mirroring
mlir-aie's own DispatchTime[T] LRU) was considered and explicitly not done:
in a normal single-sequence decode the context length increments every
token with no repeats, so an LRU buys nothing and every token would pay a
full ROCr executable load instead of the old in-place memcpy -- an
unmeasured, likely-large regression on a path benchmarked at ~51 tok/s.

Instead: HsaProgram now raises immediately and clearly, before any weight
loading; the four registry entries that used to advertise HSA decode
support are flipped off (so the harness's existing up-front guard catches
this before a multi-minute weight load, not after); the guard's error
message covers both rejection reasons instead of asserting the wrong one;
and the llama32_1b README is updated to say so.

Also folds in the final review's other findings: AMD_TRITON_NPU_OUTPUT_FORMAT
=pdi under the HSA runtime now gets a clear rejection (previously it would
silently reach the xclbin-shaped packing branch and fail inside xclbinutil
with a confusing error), the aie.compiler.hsaco import failure now names what
it needs, and a few stale comments/docstrings are corrected
(_pack_hsaco's Raises section, NPULauncher's PDI-era comment,
get_npu_cache_dir's missing mention of kernel.hsaco).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 9, 2026 14:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The core ROCr dispatch seam lacks hardware validation, and the new packing verification has unresolved issues.

4 open findings
What changed in this PR

Migrates HSA dispatch from raw PDI/instruction addresses to hsaco-loaded ROCr kernel objects while disabling incompatible LLM decode paths.

Changes:

  • Adds hsaco packing, caching, loading, and kernel-object dispatch.
  • Updates generated launcher ABI and output-format handling.
  • Disables unsupported per-token instruction-patching decode paths.
File Description
scripts/​test_hsa_hsaco_pack.py Adds hsaco packing verification script.
examples/​llm_q4nx/​registry.py Disables HSA for affected models.
examples/​llm_q4nx/​hsa_decode.py Replaces legacy patching with an unsupported-path error.
examples/​llm_q4nx/​harness.py Updates early HSA rejection messaging.
examples/​llama32_1b_q4nx/​README.md Documents HSA decode removal.
amd_triton_npu/​backend/​include/​HsaRuntime/​HsaRuntime.h Updates the public HSA preparation ABI.
amd_triton_npu/​backend/​include/​HsaRuntime/​HsaRuntime.cpp Implements hsaco executable loading and kernel-object dispatch.
amd_triton_npu/​backend/​hsa_launcher.py Passes hsaco paths and kernel names.
amd_triton_npu/​backend/​driver.py Packs and caches hsaco artifacts.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/test_hsa_hsaco_pack.py Resolved
Comment thread examples/llm_q4nx/hsa_decode.py Resolved
Comment thread examples/llama32_1b_q4nx/README.md Resolved
Comment thread examples/llm_q4nx/registry.py Resolved
@ypapadop-amd
ypapadop-amd marked this pull request as draft October 9, 2026 17:13
ypapadop-amd and others added 2 commits October 9, 2026 14:16
Resolves conflicts between this branch's hsaco/kernel-object HSA dispatch
switch and main's new HSA-runtime features (single-op NPUChain, mlir-air's
fused prefill, Gemma4-E2B decode), all added after this branch's fork point
and all built against the old raw PDI+insts ABI this branch replaces.

Conflicts:
- HsaRuntime.cpp: main added PDI deduplication (shared_ptr<PoolBuffer> across
  programs prepared from the same file) to the old prepare()/triton_npu_hsa_
  program struct. Superseded, not merged: the hsaco model has no separate PDI
  pool allocation to deduplicate (PDI bytes are embedded in the packed hsaco
  and loaded by ROCr itself), so this branch's LoadedExecutable-based struct
  wins outright. Removed the now-orphaned pdis_ member and its two call sites
  that had merged in cleanly (no conflict marker) alongside it.
- harness.py, registry.py: kept this branch's more detailed HSA-rejection
  message (names both possible reasons) while keeping main's new
  supports_xrt_zero_copy=True fields and comment style.

Found via this merge (git's auto-merge produced valid-but-broken code, since
the two branches touched disjoint regions of the same underlying problem):

- registry.py: Gemma4-E2B's new supports_hsa=True (main's "Run Gemma4-E2B on
  the HSA runtime") routes through the exact same HsaProgram/_programs() path
  the other four now-disabled models do (via engine="ple"'s
  _make_hsa_ple_decoder_class) -- flipped off for the same reason.
- multilaunch.py: HsaChainRunner (single-op NPUChain-on-HSA) calls
  triton_npu_hsa_prepare with raw (pdi_path, insts_path) and does no hsaco
  packing at all; separately, this branch's _get_output_format no longer
  returns "pdi" for the hsa runtime, so it was also about to get the wrong
  artifact shape entirely. Ported properly (not disabled): unlike the
  per-token decode case, a chain compiles once and is dispatched statically,
  exactly the pattern this branch's Task 3 already solved for the main
  compile path -- MultiLaunchBuilder.compile() now packs and caches a
  kernel.hsaco the same way compile_module does, for link_profile="hsa".
- hsa_xrt.py (new file, mlir-air's fused-prefill-engine shim on HSA):
  architecturally the same per-call instruction-stream-patching problem as
  the decode path (bo._prepared() calls the removed triton_npu_hsa_prepare/
  patch_insts). Disabled the same way (fail fast, clear error), not ported:
  porting would need the same new from-memory C ABI entry point the decode
  port would have, and while prefill's repeating op shapes might suit an LRU
  better than decode's ever-increasing context length, that is an unmeasured
  hypothesis, not something to build speculatively.

Also, while in here: moved the HSA-decode-unsupported check to the very
start of HsaFusedDecoder/HsaPleDecoder's __init__, before the superclass's
own (expensive) weight-loading constructor runs -- the earlier version's
claim that HsaProgram's own raise happened "before any weight loading" was
only true via harness.py's registry-gated guard, not for a caller that
constructs a decoder class directly. Removed the two dangling references
to a design doc that was never going to be part of this repo (per an
explicit earlier decision to keep process docs untracked) in favor of
pointing at the in-tree docstrings that carry the same explanation.
Rewrote scripts/test_hsa_hsaco_pack.py to compile through XRT (with
npu_config.target set, bypassing device detection) and call _pack_hsaco
directly, rather than through compile_module's HSA link_profile, which
unconditionally builds the C++ HSA runtime against a real ROCR install
even in compile_only mode -- exactly the dependency this test was meant
to avoid needing.

Verified: HsaRuntime.cpp still builds cleanly against the target ROCr
(rocm-systems @ users/ypapadop-amd/aie-hsaco) with the same symbol check as
before (triton_npu_hsa_prepare present, triton_npu_hsa_patch_insts absent).
All touched Python files pass py_compile. The rewritten packing test is
reasoned through but unexecuted in this session (same toolchain limitation
documented on this branch's earlier commits) -- flagging honestly rather
than claiming a run that didn't happen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	amd_triton_npu/backend/include/HsaRuntime/HsaRuntime.cpp
#	examples/llama32_1b_q4nx/README.md
#	examples/llm_q4nx/hsa_decode.py
@ypapadop-amd
ypapadop-amd marked this pull request as ready for review October 21, 2026 16:19
@ypapadop-amd ypapadop-amd self-assigned this Oct 21, 2026
ypapadop-amd and others added 2 commits October 21, 2026 10:48
…repare_bytes

From a code review of the hsaco/kernel-object merge:

- compile_module forced a rebuild when kernel.hsaco alone was missing from a
  cache hit, but not when the format-specific artifacts (aie.elf/aie.xclbin)
  were themselves missing while the compiled __npu_dispatch.so survived --
  that left cache_hsaco_path/hsaco_kernel_name unassigned, raising
  UnboundLocalError instead of rebuilding. Reproduced on hardware (deleted a
  cached kernel's aie.elf, kept its .so) and confirmed fixed.
- _scratchpad_relocs never checked the scratchpad's declared size was a
  multiple of 4; HsaElfProgram then viewed it as whole uint32 words via
  integer division, silently truncating a misbuilt one instead of failing
  where the build is actually broken. Verified against the real decode ELF
  (passes) and a corrupted copy (raises).

Also, while in there:

- Consolidated the hsaco-kernel-name-derivation + pack + cache sequence,
  duplicated across compile_module's and MultiLaunchBuilder.compile's
  cache-hit and rebuild branches, into _hsaco_kernel_name/_cached_hsaco/
  _pack_and_cache_hsaco.
- Added triton_npu_hsa_prepare_bytes (loads an hsaco already in memory,
  uncached -- there's no stable key for a buffer, and each one HsaElfProgram
  builds is single-use anyway) so hsa_decode.py no longer writes its hsaco to
  a temp file solely to read it back through the path-based ABI.

Verified on hardware: matmul HSA compile from cold and warm caches, the
cache-corruption regression above, and the Qwen2.5-3B HSA decode (scratchpad
ELF, now loaded via prepare_bytes) all still produce correct results.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…spatch

# Conflicts:
#	examples/llm_q4nx/decode_build.py
erwei-xilinx added a commit to ROCm/rocm-systems that referenced this pull request Oct 25, 2026
… variables (#14004)

## Motivation

A full ELF can relocate the address of a buffer it does not carry. The
common case is the control scratchpad (`scratch-pad-ctrl`), which holds
the runtime parameters that a design reads in its preamble. MLIR-AIR's
LLM decode uses it to pass the context length to one ELF for every
token.

The AIE hsaco loader refused any such relocation ("unsupported
relocation symbol"). So a design with scratchpad parameters could not be
loaded. Applications worked around this: they allocated the scratchpad,
wrote its device address into the ELF, pointed the relocation at symbol
0 so that the loader skipped it, and repacked the hsaco for each
allocation (for example amd/Triton-XDNA#158).

## Technical Details

A relocation whose symbol is neither a PDI nor an argument index is now
an **external variable**. It is resolved the same way as a GPU code
object's undefined symbols: the application allocates the buffer and
defines it before the load.

```cpp
hsa_amd_memory_pool_allocate(dev_pool, size, 0, &scratchpad);
hsa_executable_agent_global_variable_define(exe, aie_agent, "scratch-pad-ctrl", scratchpad);
hsa_executable_load_agent_code_object(exe, aie_agent, reader, nullptr, nullptr);
```

- **Parse** (`amd_aie_elf.cpp`): the parser records each external
symbol's patch sites and `st_size` in `Kernel::external_symbols`. Only
`Address64` is accepted, as for PDIs. A site must hold 8 bytes inside
the control code. All the symbols that name one variable must have the
same `st_size`.
- **Load** (`executable.cpp`): the loader looks up each name in
`agent_symbols_`, the table that
`hsa_executable_agent_global_variable_define` fills. If the variable is
not defined, the load fails with `HSA_STATUS_ERROR_VARIABLE_UNDEFINED`.
If its `st_size` bytes are not inside one allocation that the AIE agent
can reach, the load fails with `HSA_STATUS_ERROR_INVALID_ALLOCATION`.
Otherwise the loader writes the variable's agent address plus the addend
into the control code, next to the PDI addresses, and records its BO
handle.
- **Dispatch** (`amd_xdna_driver.cpp`): `BuildFullElfCommand` flushes
each external variable from the CPU cache and adds its BO to the
command. So the host can change a parameter between dispatches with an
ordinary store, and the application does not flush. The packet still
lists only the kernel's arguments.
- **Docs**: `docs/aie-hsaco-format.md` sections 7.4, 7.5, 10 and 11, and
a new section 9.3.

No change to the hsaco format, the packer, or the packet ABI.

Two points for reviewers:

1. The command processor copies the scratchpad itself. On aie2p it does
so only from the device-SVM pool (dev heap). A scratchpad in the data
pool loads, but the dispatch fails. This PR documents the requirement
but does not enforce it, because enforcement needs an
allocation-to-region lookup that `Runtime` does not have. Should the
loader refuse the load?
2. The new test design follows MLIR-AIR's sequence: load the empty
device, then the design, then `sync_parameters()`, run, and load the
empty device again. Without the final empty load, aie2p firmware gives
each dispatch the scratchpad value of the dispatch before it. XRT gives
the same result for one ELF run through a new `xrt::run` for each
dispatch, so this comes from the firmware, not from this change. It is
noted in the design file.

## Issue Tracking

Follow-up to #12846 (AIE hsaco support). Independent of #13908.

## Test Plan

- `elf_parse_test`: 3 new tests on a real scratchpad artifact
(`vector_scalar_add_param.py`, a new IRON design that is built only as a
full ELF):
- The scratchpad is parsed as an external symbol, with the correct sites
and size.
  - Any scheme other than `Address64` is refused.
  - An out-of-range site is refused.
- `test_dispatch`: new `ExternalVariableTest` cases on hardware:
- The load refuses an undefined scratchpad, a scratchpad in host memory,
and a scratchpad that runs past the end of its allocation or of a VMM
mapping.
- One loaded kernel is dispatched 5 times, and the host changes the
parameter with a plain store before each dispatch. Each output is
checked against that dispatch's value.
- The full AIE suite, as a regression check.
- `TryLoad`/`LoadAndFreeze` take an optional list of variables to define
before the load. `add_aie_kernel`'s full-ELF half is split out as
`add_aie_full_elf` so that a full-ELF-only design can use it.

## Test Result

On an aie2p (Krackan) NPU with `hsa-runtime64` built from this branch:

- `elf_parse_test`: 16/16 (13 before this PR).
- `aie_code_test`: 12/12.
- `test_dispatch`: 56/56, run several times.
- `test_memory`: 40/40.
- `test_error_callback`: 2/2.
- `ExternalVariableTest*` and the existing `FullElf*` tests: 20
consecutive runs each, with no failures.

During development, one run of `ExternalVariableTest*` hung, right after
the artifacts were rebuilt. It did not happen again in the 30 runs after
it.

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/rocm-systems/blob/develop/CONTRIBUTING.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
ROCR's AIE loader resolves a full ELF's control scratchpad against a
variable defined with hsa_executable_agent_global_variable_define, and
flushes it on every dispatch (ROCm/rocm-systems#14004).

So the decode allocates the scratchpad, loads the cached hsaco with it
defined, and writes L with plain stores. This drops the ELF relocation
rewriting in driver.py, the per-decoder repack and prepare_bytes, the
host-side flush, and the pointer_info probe it needed.

Pin rocm-sdk-core 10.3.0a20261027, the first nightly with both
ROCm/rocm-systems#14004 and #13908. On an older ROCR, repeated full-ELF
dispatches give wrong results and the decode fails to load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@erwei-xilinx
erwei-xilinx added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 40d9183 Oct 28, 2026
30 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.

3 participants