Skip to content

Feat/hrx port ae91949 - #115

Open
bong-water-water-bong wants to merge 10 commits into
AMD-Ecosystem:masterfrom
bong-water-water-bong:feat/hrx-port-ae91949
Open

bong-water-water-bong wants to merge 10 commits into
AMD-Ecosystem:masterfrom
bong-water-water-bong:feat/hrx-port-ae91949

Conversation

@bong-water-water-bong

Copy link
Copy Markdown

Overview

Additional information

Requirements

stellaraccident and others added 10 commits August 16, 2026 20:09
Proposal for a new AMD/ROCm backend.

See RFC discusion: `Insert discussion link`
… graphs)

The fused Qwen3-MoE dispatch covers the embedding inside fused kernels, but
dense (unfused) graphs fail-closed: 'unsupported HRX node 0: GET_ROWS' on the
very first decode step, because eager_capability_declared claims GET_ROWS yet
no standalone dispatch implements it.

Add a SingleOp GET_ROWS dispatch + Loom kernel:
- kernel-corpus/kernels/hrx_owned/get_rows_f32.loom: generic
  output[row, col] = source[ids[row], col] with f32 and q8_0 sources
  (q8_0 dequantized inline: f16 block scale x int8 quant), wave64 gfx11.
- dispatch-get-rows.{h,cpp}: matcher for GGML_OP_GET_ROWS (f32 or q8_0
  source, i32 ids, contiguous), binds source/ids/output and sets the
  source_format workload param.  Registered in dispatch-registry.cpp and
  added to ggml-hrx CMake sources + kernel-corpus manifest.

Verified: kernel parses and compiles to gfx1151 HSACO via the Loom JIT
(ggml-hrx-compile-kernel), and the dispatch engages at runtime — the
planner now covers the embedding and the fail-closed error moves past
GET_ROWS to the next unsupported op (q8_0 MUL_MAT, a separate generic-
backend item).
- loom-jit.cpp: root link uses LOOMC_LINK_MODE_LINK with config bindings
  materialized at link time (typed config.def), replacing the removed
  compile-time binding array; artifact bytes via contiguous span API;
  launch-config program load without nullptr args
- CMakeLists.txt: IREE_HAL_DRIVER_TASK replaces LOCAL_SYNC/LOCAL_TASK
- generate_kernel_corpus.py: --mode=merge replaces removed --mode=archive
- kernels: func.def inline/template.decl/def/apply migration; template.apply
  in launch-config regions marked pure; launch template impl renamed to
  avoid decl==def name collision (merge dropped the def otherwise)
- manifest: per-file shas + digests updated for migrated kernels

Verified: fused Qwen3-30B-A3B-Instruct-2507 Q4_K_M generates on HRX0
(gfx1151) at ~81 tok/s with deterministic output and zero graph errors.
…d models to CPU

- device_supports_op: detect (via the NONE weight probe) models that use quant
  types with no Loom kernel and delegate the whole graph to the CPU backend
  instead of letting the fused-pattern dispatcher fail-close.
- device_buffer_type: use the host-visible buffer so delegation needs no copies.
- kernel-executable-cache: accept i32 launch/workload scalars (packer previously
  rejected them, so the standalone GET_ROWS path could never run).
- dispatch-get-rows: source_row_count is ne[1] (row count), not ne[0] (width).
- manifest: source_format is a launch scalar, not a workload specialization.
…ls to CPU

Kernels (Loom, in hrx_owned/):
- dequant_iq3xxs_f32.loom  : IQ3_XXS codebook dequantize (grid/sign tables)
- mul_mat_vec_iq3xxs_f32.loom : IQ3_XXS matrix-vector product (per-row decode)
Both pass the Loom verifier and are registered in the corpus manifest.

Dispatch: new llm.matmul.iq3xxs_matvec_f32 route (dispatch-llm-matmul.cpp) that
matches 2D f32 MUL_MAT with an IQ3_XXS weight and binds the codebook as a
constant transient buffer.

Runtime: because the HRX dispatcher is fused-pattern based and ggml_backend_sched
splits per-op, device_supports_op now detects models containing weight types the
corpus has no kernel for and delegates the whole graph to the CPU backend, so
sub-4-bit UD files run correctly on HRX0.

Also fixes real bugs found while bringing the IQ3_XXS path up:
- kernel-executable-cache: accept i32 launch/workload scalars
- dispatch-get-rows: source_row_count is ne[1], not ne[0] (width)
- manifest: source_format is a launch scalar, not a workload specialization
…in-model

The HRX dispatcher is fused-pattern based while ggml_backend_sched assigns nodes
per-op; three coupled defects made IQ3_XXS standalone dispatches silently produce
zeros or the wrong layer's weights:

- ggml-hrx: device_supports_op claimed every op in eager_capability_declared()
  whenever the model contained no "unsupported" weight type, and otherwise delegated
  the whole model to the CPU through a sticky per-device flag. Replace both with an
  honest per-node check (can_execute_standalone_op_as_graph), so a node is claimed
  only when the dispatcher can actually execute it. This removes the whole-model CPU
  fallback and lets IQ3_XXS MUL_MAT nodes execute on HRX0.

- graph: import_ggml_graph() classified a value as External only when it was never a
  node source, so activations produced by another backend were imported as Transients:
  they were never staged/uploaded, kernels read uninitialised buffer memory and wrote
  their result into a transient arena slot while the consumer read the host tensor.
  Classify by "is it produced by a node of this graph" instead, keep in-graph graph
  outputs external, and keep the classification consistent for a whole storage so a
  view (RESHAPE/VIEW) terminal value and its base share one binding.

- command-program-executor: a prepared command program is reused by every node of the
  same shape (e.g. all layers of a model). Resident host-weight buffers were captured
  once at preparation time and never re-bound, so every layer after the first executed
  with layer 0's weights. Stage weights through the per-execution host staging path,
  which is re-bound to the live tensors on every execution.

- dispatch-scheduler: treat GGML_OP_NONE leaf/parameter nodes as covered instead of
  failing the graph with "unsupported HRX node".

Also fix a latent duplicate Loom symbol in the IQ3_XXS kernels: the check.benchmark
result symbol shadowed the exported kernel symbol, so loom-link without --strip-check
failed.

Verified with Qwen3-0.6B-UD-IQ2_M (IQ3_XXS weights):
  test-backend-ops -b HRX0 -o MUL_MAT         30/30 (11/11 IQ3_XXS)
  llama-perplexity wikitext-2 20x512  HRX0 58.85  CPU 59.11
  llama-perplexity ppl.txt  8x512     HRX0 12.94  CPU 13.02
Every op/shape now claimed by device_supports_op is actually executed correctly;
test-backend-ops -b HRX0 passes 41/41 with 0 failures.

- dispatch-add: the HRX/loom runtime submits through a fixed-size kernarg ring and
  fails with OUT_OF_RANGE when the grid does not fit (36 kernarg blocks per
  workgroup against a 262144-entry ring), and that failed submission leaves the
  device stream unusable for every later submission. Cap the claimed add grid at
  4096 workgroups (~1M elements), well below the measured ~1.9M-element failure
  point, so f32 ADD over 16.7M elements is left to the CPU instead of breaking the
  stream for the rest of the process. -o ADD is now 11/11.

- dispatch-get-rows: stop registering the standalone GET_ROWS kernel. It fails every
  case the claim accepts today (the Loom module fails to compile for some widths,
  large row counts exceed the submission ring, and the remaining shapes return wrong
  values), so claiming it made the backend return wrong embeddings silently. GET_ROWS
  nodes now fall to the CPU backend, which needs no copies because the HRX buffer
  type is host-visible.

- get_rows_f32.loom: the launch-config signature declared a source_format scalar it
  never used. It is only needed by the kernel body, and the JIT feeds the launch
  config exactly the index workload parameters, so the extra parameter made the
  launch config (and the earlier "expects 4 arguments but received 3" failure)
  unsatisfiable.
The ggml-hrx backend files added by this port carried no license metadata. Add the
HRX copyright/SPDX header to the sources and Loom kernels in the change set, and
record the corpus license as a manifest key (JSON has no comment syntax). Refresh
the manifest source digests for the two edited Loom kernels.

Re-verified after the change: test-backend-ops -b HRX0 41/41, 0 failures;
Qwen3-0.6B-UD-IQ2_M llama-perplexity -f ppl.txt -c 128 --chunks 2
HRX0 5.3308 vs CPU 5.2928.
…y run

register_get_rows_dispatch() stopped registering common.get_rows_f32 because the
claim accepted shapes the dispatcher could not execute. The kernel itself is sound:
a direct HRX0-vs-CPU probe (f32 and q8_0 sources, random id lists) is bit-exact for
every shape inside its domain. The claim was simply far wider than that domain:

- get_rows_f32.loom's `index.assume` bounds are compile-time promises, not runtime
  guards: specialising width outside [32, 16384], source rows outside
  [1, 131072] or output rows outside [1, 2048] is a hard Loom compile error
  (confirmed: width 1/31/20000/76800, src 131073 and out 2049 all fail to compile).
- The kernel indexes a flat 2D view, so batched (ne[2]/ne[3] > 1) and higher-rank
  gathers - which the old claim accepted - were silently wrong.
- The HRX runtime charges each dispatch's kernarg footprint against a fixed
  262144-entry ring (aql_block_processor: ceil_div(kernarg_length, block_size)), and
  that footprint scales with the *source row count*: measured on gfx1151 it is about
  16 * ceil(width/256) * (source_row_count + output_row_count) blocks. Crossing the
  ring aborts the submission and leaves the device stream unusable, which is what
  turned one bad case into a cascade of failures.

The claim now mirrors the kernel exactly (flat 2D only, f32/q8_0 sources with q8_0
width block-aligned, width in [32, 16384], rows within the assume bounds, and the
estimated kernarg footprint under 0.75 x the ring). Shapes outside it are reported
"not supported [HRX0]" and stay on the CPU, which needs no copies because the HRX
buffer type is host-visible.

Measured footprint boundaries: width=1024 -> source rows <= 4090, width=256 ->
source rows <= 16383 (i.e. ceil(width/256) * source_rows <= 16384).

Verified: test-backend-ops -b HRX0 -o GET_ROWS 2/2, 0 failures; full
test-backend-ops -b HRX0 43/43, 0 failures; Qwen3-0.6B-UD-IQ2_M reference
perplexity unchanged at HRX0 5.3308 vs CPU 5.2928.
Follow-up to the per-op claim change. Three defects made the honest claim path
either reject valid graphs or hang on models it used to run:

- command-program-bindings: a zero-byte external binding was reported as an error
  ("external value N has an empty binding"). ggml legitimately produces empty tensors
  - a GET_ROWS over an empty id list during decode yields ne=[0,...] and its outputs
  are [width,0] - so the whole graph was rejected and llama_decode failed with -3 on
  UD-IQ2_M at c=512. A zero-length binding is a no-op, not a failure.

- dispatch-llm-matmul: the dense q4k/q6k route accepted output_size up to 262144 and
  any prefill query length, so the vocab-sized LM head (151936) was claimed and
  dispatched through the graph replay, whose kernarg footprint scales with
  output_size x token_count and overflowed the fixed 262144-entry ring. The failed
  submission left the device stream unusable, which showed up as a hang. Measured on
  gfx1151 with input_size 1024: 16384x64 needs 284957 entries, 8192x512 needs 402833,
  while everything up to 4096x512 fits. Cap the claim at output_size <= 4096 and
  token_count <= 256; larger shapes stay on the CPU instead of breaking the stream.

- dispatch-get-rows: reject degenerate gathers with zero output or source rows.

Verified on the deployed /opt/hrx build:
  test-backend-ops -b HRX0            43/43, 0 failures (IQ3_XXS MUL_MAT 11/11)
  UD-IQ2_M   (IQ3_XXS)  c=128 HRX0 5.3308 vs CPU 5.2928; ppl.txt 20x512 12.9439;
                        wikitext-2 20x512 58.8460 vs CPU 59.1075
  Q4_K_M     (supported) c=128 HRX0 3.4908 vs CPU 3.5289; c=512 7.1220;
                        wikitext-2 20x512 24.6865 vs CPU 24.6607
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