Skip to content

A HIP lane: ! on AMD GPUs (for the hip branch, #891) - #958

Merged
VictorTaelin merged 2 commits into
bendlang:hipfrom
costamatheus97:hip-lane
Sep 21, 2026
Merged

VictorTaelin merged 2 commits into
bendlang:hipfrom
costamatheus97:hip-lane

Conversation

@costamatheus97

@costamatheus97 costamatheus97 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

An AMD lane for !, through HIP and HIPRTC, as discussed in #891. This is for the hip branch, not main. That branch does not exist yet, so the PR points at main for now and I will retarget it once hip is created. One commit on main at e52cda4 (2.0.24).

Tested on one card: Radeon RX 7800 XT (gfx1101, 16 GB), ROCm 7.2.1, WSL2. I have no CUDA or Metal hardware.

What it changes

  • main.ts: without CUDA, a ! program builds with -DBEND_HIP=1 when ROCm is at $ROCM_PATH, else /opt/rocm.
  • comp.ts, device code: HIPRTC compiles the CUDA lane's device code as written. It defines __HIPCC_RTC__, so one BEND_RTC macro replaces the six __CUDACC_RTC__ tests. It ignores -default-device, so #pragma clang force_cuda_host_device begin stands in.
  • comp.ts, host code: a BEND_HIP block beside BEND_CUDA, with the same hash-keyed .gpu cache. gpu_hash was identical in both lanes, so it moved above them.
  • tests/run/gpu_turns.bend: three ! turns with the host rebuilding and extending a device-built tree between them. It passes on every lane; a lane that misses a page gets it wrong.
  • gates/repo.ts and AGENTS.md: the cap (below) and the word "HIP" in the runtime list.

What it touches outside BEND_HIP: the six __CUDACC_RTC__ tests, the move of gpu_hash, and two macros around cube_run in corpus_eval that are empty on the other lanes.

Why the HIP block is larger than CUDA's

The host block is 247 lines; CUDA's is 126 and Metal's 99.

About 120 of the 247 mirror CUDA one to one: probe, compile, cache load, launch, pass. The other 125 exist because a consumer Radeon has no shared or migrating memory. It reports concurrentManagedAccess = 0: a managed block is pinned host memory that the device reads over PCIe. The lanes keep their stacks, rings and free lists there, so queens takes 47 s.

So the corpus is host memory with a twin in VRAM, which a ! copies in and out. This is legal because every Loc is an index and the host never runs during a device turn. The 125 lines are:

part lines what it buys
the corpus as a memfd mapped twice 25 a fault fills a chunk through the second mapping while the first still traps, so no other host thread sees it half filled
gpu_copy, gpu_sync 25 the header, static image and live bank entries, both ways
gpu_rings 25 copies the ring counters and only the slot planes with live tasks; the whole ring and free-list region is 270 MB per !
gpu_heap, gpu_fault, gpu_trap 50 the lazy heap: 2 MB chunks under mprotect. After a turn every chunk under the bump traps; the first host touch downloads that chunk and marks it dirty, and the next ! uploads only dirty chunks. The handler is chained in front of err_trap in pool_stack

Tracking by the bump alone is unsound: heap_free and heap_alloc rewrite freed slots under it.

The alternative is a twin that copies everything under the bump both ways on each !: about 30 lines, for a lane of about 160. I measured it. On the large-heap programs it is 2 to 3 times slower (tree-radix 0.55 → 1.4 s, hashmap 0.7 → 1.1 s, tree-matmul 0.5 → 0.8 s; tree-radix moves 3.7 GB instead of 17 MB), and the lane goes from beating 16 threads on 11 of 16 benches to 7. I kept the fast version. The 125 lines are one contiguous block, so they can be cut to the simple version later without touching the rest.

The code comments keep only the invariants an edit must respect; the reasoning is here and in the commit message. There is one memory model for every AMD device. An APU would copy more than it needs to, but stays correct.

The comp.ts cap: 65000 → 70000

comp.ts is at 64936 ttok on main, 64 under its cap, so any lane needs the cap raised. This commit adds 9.5 KB to the file. At the file's own bytes-per-token ratio that is about 3200 ttok, for a total near 68100. That is an estimate: I do not have ttok on this machine. 70000 is the next round number; set it to the measured count if you prefer a tight cap. The change is on this branch only.

Checks

  • A one-machine copy of gates/test.ts's rules: 1410 / 1413 with the patch and 1409 / 1412 without (the new test is the difference). The failures are the same: gfx_clicks, gfx_window, io_audio_open, which need a display or an audio device. I left io_tcp_send_slow_peer out of both runs; it hangs under WSL on unpatched main.
  • The 20 runnable ! tests print the same bytes as the CPU lane. --gpu on fails with its message when no device is visible, and the HIP log shows the kernel launches.
  • Two multi-turn tests match a Python model.
  • Two tests with 16 host threads faulting at once: 200 of 200 runs correct. An earlier instrumented build counted 41,000 faults waiting on the handler's lock over 300 runs.
  • Out of heap at --gpu 440MB reports in 0.14 s.
  • Not run: the gates themselves, CUDA, Metal, native Linux, a second AMD part.

bench/runtime/, perf.ts spans, wall seconds, median of 3 runs, all 16 checksums equal on both lanes:

bench 16 threads HIP
gameoflife 1.83 0.19
nbody 0.87 0.19
raytrace 1.10 0.26
mandelbrot 0.69 0.17
merkle 0.74 0.20
kmeans 0.99 0.33
tree-radix 1.37 0.55
tree-bitonic 3.10 1.77
tree-matmul 0.81 0.50
symreg 0.77 0.53
bfs 0.69 0.54
queens 1.53 1.70
hashmap 0.63 0.71
terrain 0.53 0.76
editdist 0.46 0.95
lexer 0.59 1.28

Known limits

  • The fault handler calls hipMemcpy from SIGSEGV. The fault can only come from Bend's own evaluation code, never from inside ROCm, but POSIX does not promise it.
  • The host side of the twin is a memfd, so it gets no transparent huge pages.
  • The default span is 1 GB (--gpu 2GB for more), not taken from free VRAM.
  • effs/window_frame.c still has only its CUDA device path; a HIP build fills pixels on the host.
  • Under WSL, ROCm spends about 0.5 s at first use looking its tools up along a $PATH full of Windows directories. Run with a short PATH there; the numbers above were.

nicolas-abril and others added 2 commits September 21, 2026 10:28
…rpus (bendlang#891)

HIPRTC compiles the CUDA lane's device code as written. It defines
__HIPCC_RTC__, so BEND_RTC replaces the six __CUDACC_RTC__ tests, and it
ignores -default-device, so force_cuda_host_device stands in. The host side
is a BEND_HIP block beside BEND_CUDA; main.ts builds with it when there is
no CUDA and ROCm is at $ROCM_PATH, else /opt/rocm.

A consumer Radeon has no migrating managed memory (a managed block is pinned
host memory read over PCIe: queens 47 s), so the corpus is host memory with
a twin in VRAM that a ! copies in and out: every Loc is an index, and the
host never runs during a device turn. The twin's heap is lazy, 2 MB chunks
under mprotect: after a turn a chunk is downloaded on its first host touch,
and the next turn uploads only the chunks the host touched. The handler is
chained in front of err_trap in pool_stack.

tests/run/gpu_turns pins three ! turns with the host rebuilding a
device-built tree between them. gpu_hash, the same in both RTC lanes, moves
above them. The comp.ts cap goes to 70000 on this branch (an estimate:
about 68100 ttok).

On an RX 7800 XT (ROCm 7.2.1, WSL2): the 20 runnable ! tests print the CPU
lane's bytes, and the lane beats 16 threads on 11 of the 16 runtime benches.
Not run: the gates, CUDA, Metal, native Linux.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018yDkB3RYnpnX3x1Uf12tEW
@VictorTaelin
VictorTaelin changed the base branch from main to hip September 21, 2026 13:24
@VictorTaelin
VictorTaelin merged commit 2ba2bd1 into bendlang:hip Sep 21, 2026
@VictorTaelin

Copy link
Copy Markdown
Contributor

Thank you, and welcome to the repo as the keeper of this lane. The hip branch exists now, this PR is retargeted to it and merged there, fast-forward, as your one commit (2ba2bd1e).

The arrangement, so it is written down:

  • hip = main plus the AMD lane. It is yours to maintain: rebase or merge main into it when you like and open PRs against hip; we merge them after the CPU gate passes. It is not in the release channel and not in the perf gate, since the gate has no AMD hardware; the CPU and Metal gates still run on it.
  • What we checked before merging: the repo gate 46/46; the test gate 1413/1414 on the cluster (the one miss is a node without clang, and compile_array_dynamic_closure prints 9 on C and JS locally); tests/run/gpu_turns.bend prints 691345207 on Metal on an M4 Max, matching its pin.
  • Measured: comp.ts on this branch is 68028 tokens (ttok), under the 70000 cap you set; main is at 64941.
  • The 6 → 1 BEND_RTC collapse and the gpu_hash hoist are the kind of change that could go to main on their own. If you want them there, open a small PR against main with just those and we will look at it.

The benchmark table is impressive, and the memfd-plus-mprotect lazy heap is a clean way to live without managed memory.

Note: this reply was written by an AI after it reported the PR to me and I made the decision. If anything here is wrong, reply and I will review it myself.

costamatheus97 added a commit to costamatheus97/bend that referenced this pull request Sep 21, 2026
…ve the lanes

Two behaviour-neutral pieces of the HIP lane (bendlang#958, on `hip`) that do not
depend on it, as offered there.

BEND_RTC is defined when the file is compiled by the device's runtime
compiler, and the six `__CUDACC_RTC__` tests read it. A second RTC lane
then adds its macro to one definition and leaves the six sites alone.

gpu_hash moves out of the CUDA block to just above the lane blocks, under
`#if BEND_CUDA`. It reads only BEND_SRC and CUBE_LOG, so a lane that keeps
a `.gpu` cache can share it by widening that one guard.

With both on main, `hip` differs from main in shared code by
`|| defined(__HIPCC_RTC__)` and `|| BEND_HIP` on those two lines.
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