Skip to content

RDNA4 (gfx1201 / Radeon AI PRO R9700) support - #1296

Open
The-Monk wants to merge 12 commits into
AMD-AGI:mainfrom
The-Monk:rdna
Open

RDNA4 (gfx1201 / Radeon AI PRO R9700) support#1296
The-Monk wants to merge 12 commits into
AMD-AGI:mainfrom
The-Monk:rdna

Conversation

@The-Monk

Copy link
Copy Markdown

Description: what and why

Adds RDNA4 / gfx1201 (Radeon AI PRO R9700) as a first-class target. Opening this at AMD's request, following up on #1196.

Upstream validates against Instinct only: _GFX_TO_RUNNER maps consumer arches to None, so a Radeon box resolves to no runner and no dispatch identity. This makes gfx1201 resolve, run, and report.

The port is smaller than #1196 originally scoped — 4 modified files, +21 lines of core change, no refactors:

File Change
common/gpu_identity.py +7 — one row, r9700 → (gfx1201, 64). The gpu_identity refactor means _AMD_GPU_TYPES and the rocm-smi product tags both derive from this table, so the row also makes the board CLI-accepted and autodetectable. CU count read off the board (amd-smi NUM_COMPUTE_UNITS), not a spec sheet.
inference_optimizer/gpu_types.py +5 — gfx1201 → r9700 in _GFX_TO_RUNNER.
orchestrator/phases/quantization_schemes.py +9 — docstring only. Records why mxfp4 is refused here: the MX scaled-converts are gfx950/gfx1250-gated, so an assembler capability probe grades them REJECTED. No hardware datapath, not a missing kernel.
inference_optimizer/tests/test_rdna4_r9700_support.py +73 — new, 8 tests.

Everything else is additive and touches no core path: examples/rdna/custom_r9700.sh (llama.cpp scriptable runner — the bypass path already resolves an operator script by the custom_{runner_type}.sh convention, and it writes the InferenceX-shaped inferencex_result.json your collectors already consume, so nothing downstream needed changing), a container recipe, a support probe, docs/rdna-port/, and an issue template.

Why llama.cpp and not vLLM/SGLang: below ~32 GB the serving stacks' VRAM and concurrency assumptions do not transfer, and llama.cpp is what this class of card is actually served with.

Two things left deliberately negative, and tested that way

  • gfx1200 (Navi 44) is unmapped. Same ISA family, but nobody has measured it here, and a runner label is a claim of support.
  • mxfp4 raises rather than silently degrading on gfx1201.

Three files in this diff are fork-only — flagging rather than pre-deciding

This branch doubles as the fork's landing page, so it carries framing that is correct there and wrong here. I've left it visible instead of quietly stripping it, because where the line falls is your call, not mine:

  • README.md (+23) — a "community port, not an AMD product, not endorsed by AMD" banner. Obviously not something upstream's README should say about itself. Assume this one is dropped unless you want some form of it.
  • docs/rdna-port/README.md — line 3 opens with the same disclaimer; line 149 describes the fork's own branch layout.
  • AGENTS.md — line 249 carries the disclaimer in the SPDX/licensing note.

Say the word and I'll strip all three in a follow-up commit — it's a five-minute change and it does not touch the core diff. Separately: AGENTS.md and the README.md banner are the only root-level files here, so if you'd rather this PR carry nothing project-wide, dropping both leaves everything else intact.

Linked issue(s)

Tests: added/updated? commands run?

Added test_rdna4_r9700_support.py (8 tests), including the two negative cases above.

Measured on 2× Radeon AI PRO R9700 (gfx1201, ROCm 7.14, amdgpu 6.19.4) — verified, not asserted:

  • Autodetect resolves r9700 → ('gfx1201', 64).
  • Correctness vs the CPU reference: test-backend-ops MUL_MAT 1196/1196, MUL_MAT_ID 865/865.
  • Runner benchmarks end-to-end: prefill ~4476 t/s, decode ~169 t/s on an 8B Q2_0 at pp512/tg64.
  • The result JSON is accepted by Hyperloom's own is_valid_measurement(), not by my reading of it.
  • Reproduced in a clean container against stock upstream llama.cpp — see examples/rdna/container/.
  • Full suite: 756 existing tests pass, 0 failures.
  • Gates on this branch rebased onto current main: ruff check clean, ruff format --check clean, reuse lint compliant.

Not tested, and labelled as such in the docs: gfx1200, anything above 2 cards, RDNA3. The 2-card host reports peers: false with host↔GPU capped at PCIe Gen3 (an on-board switch's upstream port downgrades the link; the leaf link reads Gen5 and hides it). A 4-card rig will hit walls this one can't see.

Breaking changes: no

Additive. No existing target's behaviour changes; the Instinct rows and paths are untouched.


One thing this port surfaces: the profile-configuration feedback loop

Raising this as a question, not a change — it needs your call on a cross-system contract, so I haven't touched it.

The optimizer's whole value here is finding a profile configuration that beats the shipped baseline. On RDNA it does. But storing that result so it survives is where the current KB doesn't quite reach:

  1. Keying is fine. The canonical id is inference:{model}:{hardware}:{framework_name}:{model_type}:{architectures}:{framework_version}:{precision}hardware is a dimension, so an r9700 recipe is already a distinct row and can't collide with an Instinct one. Good.
  2. Staleness detection is not. StackFingerprint is vllm_version / aiter_commit / rocm_version — vLLM/Instinct-shaped by construction, and its docstring says it mirrors arbor's exactly "to keep binary readability." On the llama.cpp scriptable path there is no vLLM and no aiter, so two of three fields are empty.
  3. And the framework version collapses. detect_framework_version resolves via _FRAMEWORK_VERSION_MODULES = {sglang, vllm, atom} by importing the package. llama.cpp is a binary, not an importable module, so it falls through to DEFAULT_FRAMEWORK_VERSION_SLUG = "unknown_version".

Net effect: a better-than-baseline configuration measured on gfx1201 can be written, but never invalidated or attributed to the build that produced it. Every llama.cpp recipe lands in the same unknown_version bucket with a fingerprint that can't answer "is this stale?" — so the loop that should make the optimizer compound across runs and across users doesn't close on this path.

The ask: is there an intended way to record an engine identity for a non-importable, non-serving runner? A llama_cpp_commit (or a generic engine_version) on StackFingerprint would do it, but that field mirrors arbor and I'm not going to unilaterally change a contract shared with a system I can't see. If you point me at the right shape I'll implement it and measure it on the R9700s — and if the answer is "out of scope for now," that's a fine answer too; the port stands without it.

The broader version of the same point: contributors with hardware AMD doesn't have will find configurations that beat the provided defaults. Right now there's no path for those to flow back. examples/rdna/rdna-support-probe.sh and the RDNA hardware-report issue template in this PR are a deliberately low-tech stand-in — one command, paste-ready output, CONFIRMED / PARTIAL / UNSUPPORTED, with a required "what you did NOT test" field. That's a bug tracker being used as a database, and it would be better as a real one.


Related work — RDNA4 findings this port draws on

All measured on gfx1201; linked so the numbers above have provenance rather than being claims.

The stack this runner drives

  • The-Monk/The-Rock8 — RDNA4 inference appliance (container + fp8 GGUFs), the reference the runner's numbers come from.
  • The-Monk/llama.cpp @ roc8 — the gfx1201 kernel work: int4 decode, 2:4-sparse SWMMAC prefill, e4m3 dot2, and a MUL_MAT_ID out-of-bounds fix.
  • PrismML-Eng/llama.cpp#116 — HIP-path Q1_0/Q2_0 vec_dots (+37% / +16% decode on RDNA4), opt-in quant dedup, opt-in hipBLASLt prefill routes.

Upstream fixes filed from this hardware

  • ROCm/composable_kernel#3759 — 3 correctness bugs in ck_tile 2:4-sparse SWMMAC on gfx1201, with a fail→pass repro. Open.
  • ROCm/clr#283__byte_perm sign-replicate selector modes on the device path. Open.
  • ROCm discussion #6618 — the fuller inventory: ISA findings, the gfx1201 rocprofv3 gaps that docs/rdna-port/README.md documents (host_trap-only PC sampling, FETCH_SIZE zeros, GL2C perf-level gating), and host tuning.

The profiler gaps are worth a look for anyone porting Instinct-shaped tooling: profile_standard un-gates the counters but throttles the card to ~1593 MHz vs ~2330, so it silently suppresses anything benchmarking alongside it.

Hardware I don't own: @kyuubyN reported gfx1100/gfx1102 results in #1041, and a field-tester independently validated the ISA census and cache behaviour on an RX 7600 XT (gfx1102, ROCm 7.2.4). Both are RDNA3 — out of scope for this PR, and the reason the support matrix in docs/rdna-port/README.md marks RDNA3 as methodology validated, runner map not covered.

/cc @arsalanfrq-a11y @l33tm4st3r @kyuubyN

jmonk and others added 12 commits August 26, 2026 12:30
CCA (Claude Code Augmented)
Upstream maps consumer arches to None, so a Radeon box resolves to no runner
and no dispatch identity. Makes gfx1201 a first-class target.

  gpu_identity.py   r9700 -> (gfx1201, 64). CU count read from the board
                    (amd-smi NUM_COMPUTE_UNITS), not a spec sheet. Because
                    _AMD_GPU_TYPES and the rocm-smi product tags derive from
                    this table, one row also makes the board accepted by the
                    CLI and detectable by autodetect.
  gpu_types.py      gfx1201 -> r9700 runner. gfx1200 (Navi 44) deliberately
                    left unmapped: same ISA family, never measured here, and a
                    runner label is a claim of support.
  examples/rdna/    custom_r9700.sh — llama.cpp scriptable runner. No core
                    change was needed: the bypass path already resolves an
                    operator script by the custom_{runner_type}.sh convention.
  quantization      Documents why mxfp4 is unavailable on RDNA4 — the MX
                    scaled-converts are gfx950/gfx1250-gated silicon, so a
                    capability probe grades them REJECTED. fp8 stays available.

VERIFIED ON HARDWARE (2x Radeon AI PRO R9700, gfx1201), not asserted:
  - rocm-smi autodetect resolves r9700 -> ('gfx1201', 64)
  - the runner benchmarks end to end: prefill 4488.33 t/s, decode 165.81 t/s
    (Bonsai-8B Q2_0, pp512/tg64/r2)
  - its inferencex_result.json is accepted by Hyperloom's OWN validator:
    is_valid_measurement() -> True, scriptable detected. The harness judges
    the output, not the author of the script.
  - 756 upstream tests pass (gpu/quant/preflight/parser/provenance), 0 failures

Two negatives are deliberate and tested: gfx1200 unmapped, mxfp4 refused.
docs/rdna-port/SCOPE.md is corrected rather than deleted — it was written
against a 402-commit-stale clone and scoped a much larger change than upstream
now needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
The issue AMD-AGI#1196 comment sends people here, and the landing page was still
upstream's README with nothing identifying it as the RDNA port or disclaiming
AMD endorsement. Banner only; upstream's text is untouched below it, and PR
branches are cut from main, so this adds no diff noise upstream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
Upstream AMD-AGI#1041 (@kyuubyN) is the RDNA3 counterpart to AMD-AGI#1196: Hyperloom's
methodology independently re-implemented against their OWN HIP engine on
gfx1102 (RX 7600) and gfx1100 (W7900), with measured results including a run
whose honest verdict was "revert everything".

Stated precisely, because the easy overstatement is wrong: that validates the
APPROACH on gfx11. It does NOT establish Hyperloom running on RDNA3 — the
runner map still excludes gfx11 and upstream's suite asserts the exclusion
(_GFX_TO_RUNNER.get("gfx1100") is None). A result measured on A does not close
B until B is measured; that rule is what the rest of this port is built on.

Also carries over their engagement check: compare the actual rendered source
each instance compiled rather than inferring engagement from log markers. It
caught a knob that looked plausible but never reached the GPU — the same
failure mode as optimizing a path that is not the live dispatched one.

Invites RDNA3 support here and says what it needs. AMD-AGI#1041 was auto-close-bot
pressured and closed by its own author earlier today; the work behind it is
real and should not be stranded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
The banner named only AMD-AGI#1196. Anyone arriving from the RDNA3 side should see
their thread acknowledged and the invitation, without digging into docs/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
Agents arriving on someone else's RDNA box need three things: what to do, a way
to produce evidence, and somewhere to put it.

AGENTS.md
  THE ONE RULE: a claim is MEASURED or ASSUMED, and an ASSUMED claim never
  becomes a support-matrix entry. A support entry is a promise to a stranger
  whose hardware you cannot see.
  §1 confirm support on hardware you have; §2 add a target that is not mapped
  (one identities row + _GFX_TO_RUNNER + runner + tests incl. the negatives +
  a matrix row); §3 the playbook for when it does not work.

  §3 is the part that makes a remote agent useful rather than just loud:
  exhaust the mundane before the exotic (perf-level gating -> env traps ->
  vendor tracker -> versions -> build flags -> only then kernel internals);
  isolate bottom-up by layer with that layer's own ground-truth tool, because
  cross-layer misattribution is the biggest time-waster there is; a symptom
  table for the failures this port actually produces; and the rules for
  claiming a fix (it ran, a correctness gate passed, the path you changed is
  the one that executed, speedup = baseline / candidate, suspect the baseline
  when a win looks huge, and a result above the roofline means you measured
  cache).

examples/rdna/rdna-support-probe.sh
  Turns all of that into one command. Prints a paste-ready report and exits
  CONFIRMED (0) / PARTIAL (1) / UNSUPPORTED (2) — and on UNSUPPORTED it emits
  the exact rows needed to map that arch. The CONFIRMED verdict requires
  Hyperloom's own is_valid_measurement() to accept the run: the harness grades
  the output, never the agent that produced it.

  Dogfooded here, and three bugs it exposed are fixed: VRAM lives in a nested
  block (not a flat key), ROCm version comes from amd-smi, and the GPU count
  now counts amdgpu cards ONLY -- the first version counted this board's ASPEED
  BMC and reported a 2-GPU host as 3-GPU, which is exactly the sort of wrong
  detail that makes a whole report untrustworthy.

  Verified end to end on 2x R9700: CONFIRMED, exit 0, prefill 4448.74 t/s /
  decode 168.11 t/s -- within ~1% of the independent earlier run.

.github/ISSUE_TEMPLATE/rdna-hardware-report.yml
  Requires the verbatim probe block AND a "what you did NOT test" field, since
  that is what keeps the matrix honest. Says plainly that negative results are
  welcome: nobody without your hardware can produce them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
Reported by JM: correctness was never mentioned. It was worse than an omission.
AGENTS.md preached "a wrong-but-fast kernel is worth zero" in §3.4 while the
probe stamped CONFIRMED on throughput alone, and the run I reported as our own
confirmation had quality_gate: None — no correctness evidence of any kind.

Hyperloom's is_valid_measurement() treats a MISSING quality_gate as
non-blocking. That is right for the harness (a scriptable run need not carry
one) and the wrong bar for a support claim, so the probe now adds its own gate
instead of leaning on the harness for something the harness never promised.

  probe    --bench now runs llama.cpp test-backend-ops FIRST, comparing ops
           against the CPU reference on this arch, and CONFIRMED requires it to
           pass AND a harness-accepted measurement. A fast run with no
           correctness evidence now prints, in the verdict line itself,
           "PARTIAL (throughput only — CORRECTNESS UNVERIFIED)" rather than
           passing quietly. Default CORRECTNESS_OPS=MUL_MAT,MUL_MAT_ID (the
           matmul paths a quant or arch port actually breaks); CORRECTNESS_OPS=ALL
           widens it. The model-level PPL gate is surfaced as its own row.
  AGENTS   New §1.1a: correctness is half the verdict, not a footnote. Says
           explicitly what is_valid_measurement() does not do, so nobody infers
           a correctness claim from an ACCEPTED measurement. §2.3: if
           correctness FAILS on a new arch, report it and STOP — do not tune a
           kernel computing the wrong answer, and do not add a matrix row.
  docs     Matrix cites the correctness gate for gfx1201, not just the benchmark.
  template Correctness is now a REQUIRED field, and a correctness failure is
           declared a first-class report.

Op-level and model-level gates fail differently and both are worth having: ops
prove the kernels compute the right thing, perplexity proves the assembled
model still produces sane output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
…cost

The row read "(MUL_MAT,MUL_MAT_ID):0 MUL_MAT:1196/1196 ..." because the
accumulator was initialised to 0 rather than empty. Cosmetic, but this string
is the artifact strangers paste into issues, so it should not carry a phantom
count next to real ones.

Default gate measured at ~90s on gfx1201 (MUL_MAT 1196/1196, MUL_MAT_ID
865/865 vs the CPU reference) — cheap enough to be non-negotiable. Documented
in AGENTS.md and carried into the support matrix alongside the throughput, so
the matrix states both halves of the claim rather than speed alone.

Our own CONFIRMED re-earned under the corrected gate: correctness PASSED +
prefill 4476.34 t/s / decode 169.14 t/s ACCEPTED by is_valid_measurement().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
The port was validated only against our own llama.cpp fork and a pinned ROCm.
That proves it runs here and says nothing about anyone else's machine, which is
the only question that matters for someone with their own card.

examples/rdna/container/ builds a clean rocm/dev-ubuntu-24.04 image, compiles
STOCK upstream llama.cpp from source for gfx1201, clones this fork from GitHub,
and runs the probe. Nothing of ours is in the stack: if the runner only worked
against our kernels, that would be a fork feature, not RDNA support.

RESULT — CONFIRMED, exit 0, in-container, ROCm 7.14.60850, upstream llama.cpp:
  correctness  MUL_MAT 1193/1193, MUL_MAT_ID 869/869 vs the CPU reference
  benchmark    prefill 3883.66 t/s / decode 93.97 t/s (Qwen3-8B-Q4_K_M)
  harness      ACCEPTED by is_valid_measurement()

Not comparable to the host figures: different model AND different quant
(Bonsai-8B Q2_0 there, Qwen3-8B Q4_K_M here, since Q2_0 is a fork-specific type
upstream cannot load). The claim is portability, not a speed comparison.

TWO REAL BUGS, both found by running it, both of which would have read as
"RDNA support is broken" to a newcomer:

  libhipblas.so.3 not found — the ROCm dev image ships libraries in
    /opt/rocm/lib but registers that path with NEITHER ld.so.conf.d NOR
    LD_LIBRARY_PATH, so ldconfig -p knows no hipblas. Compilation succeeds
    (cmake passes -L), then every ROCm-linked binary dies at LOAD time. Fixed
    in the image, and the fix verifies itself in the same layer so a regression
    fails the build instead of the run.
  unable to find group render — --group-add resolves the group name INSIDE the
    image, which has none. The host's numeric GID is what grants /dev/kfd and
    /dev/dri. Corrected in both the Containerfile header and the README.

Worth recording: during the broken run the probe reported PARTIAL with the
linker error in the correctness row rather than a false CONFIRMED. The gate
refusing to pass is the gate working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
Bug 1 (libhipblas.so.3 not found) was already fixed in the image. Verified it
is a real fix and not a masked one: ldconfig -p now reports 4 hipblas entries,
and llama-bench and test-backend-ops each have 0 unresolved libraries.

Bug 2 (unable to find group render) had only been documented — I wrote the
numeric-GID workaround into the README and moved on without asking whether the
flag was needed at all. It is not. The container runs as root, which reaches
/dev/kfd and /dev/dri through CAP_DAC_OVERRIDE; verified with no group flags at
all: rocminfo sees both gfx1201 GPUs and test-backend-ops passes. --group-add
is now gone from the documented command and is added only when dropping to a
non-root user, where the host's numeric GID is genuinely required.

Adds run.sh so the incantation does not have to be rediscovered: resolves the
model path and mounts its directory read-only, passes the devices, forwards the
knobs, supports DOCKER=podman and RUN_AS_USER, and fails immediately with a
clear message when /dev/kfd is absent instead of 90 seconds into a run.

Verified end to end through the wrapper: CONFIRMED, exit 0, correctness
MUL_MAT_ID 869/869 vs the CPU reference, on stock upstream llama.cpp.

A workaround in a README is not a fix; it is a note asking the next person to
pay the same cost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
The README claimed DOCKER=podman works. It did not: podman has no
unqualified-search registry by default, so the short name rocm/dev-ubuntu-24.04
fails with "did not resolve to an alias and no unqualified-search registries are
defined". Docker silently assumes docker.io; podman does not.

Found while porting this recipe into the Rocky toolkit, where podman is the
auto-detected engine — i.e. the claim was falsified by the first person to use
the other engine, which is the whole reason to state such claims testably.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
…aim was engine-specific

I verified that root in the container reaches /dev/kfd via CAP_DAC_OVERRIDE and
wrote it up as a general fact. It is true for DOCKER (rootful) and false for
ROOTLESS PODMAN, which is what podman defaults to here.

Under rootless podman, in-container uid 0 maps to the invoking host uid, so
/dev/kfd shows as nobody:nogroup and CAP_DAC_OVERRIDE does not apply. Host
supplementary groups are dropped without --group-add keep-groups.

The failure mode is the dangerous kind, and I hit it: rocminfo reported ZERO
GPUs, test-backend-ops silently fell back to CPU-only and printed "1/1 backends
passed" (vs 2/2 with a GPU), and the run exited 0. A green verify with no GPU
attached is worse than a red one.

run.sh now detects rootless podman and adds keep-groups; the README documents
both engines with the measured evidence for each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oPUZALTu4ZPwMdHDdwiQt
@The-Monk
The-Monk requested a review from a team as a code owner August 26, 2026 17:55
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