Skip to content

hrx: claim host buffers only when asked, so -ngl 0 stays on the CPU - #217

Merged
bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
1bit/1078-ngl0-cpu-placement
Nov 6, 2026
Merged

bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
1bit/1078-ngl0-cpu-placement

Conversation

@bong-water-water-bong

Copy link
Copy Markdown

Why

engine#1078: with an HRX device present, -ngl 0 is not the CPU route. The device reports that it supports every
host buffer type, so ggml_backend_sched_backend_from_buffer (ggml/src/ggml-backend.cpp) picks HRX - the
highest-priority backend - for host-resident weights, and the scheduler's expand passes spread the device over the
graph that follows. Before this change, -dev HRX0 -ngl 0 put 180 of the 834 non-view nodes on the CPU (RMS_NORM
144, FLASH_ATTN_EXT 36) and ran the rest on HRX over host tensors in place.

That changes the arithmetic: first-token logprob -1.369900 against -1.084810 on -dev none, two of the top-8
members differ, and a 200-token greedy answer loops ("The paragraph should be in English...") where the CPU route
explains Rayleigh scattering. The Flash Attention half of the issue is already fixed (#205 is in this branch); FA is
enabled in every arm below.

Change (1 file, +7/-1)

device_supports_buffer_type keeps claiming the device's own buffer and its host arena, and claims another
backend's host buffer only with GGML_HRX_CLAIM_HOST_BUFTS=1. The in-place host path stays available when it is
asked for; by default a graph whose weights are on the CPU stays on the CPU.

Measured

strixhalo, gfx1151, Qwen3-4B-IQ4_XS, -c 512 -b 32 -ub 32, greedy. One gpu-hold per measurement set; raw logs in
~/wt/1078-runs on that box.

Numeric and placement (server /completion, n_predict: 1, n_probs: 8; placement from the prefill graph):

arm first-token lp top-8 vs CPU CPU nodes graph splits
-dev none -ngl 0 (reference) -1.084810 - - 1
-dev HRX0 -ngl 0 before -1.369900 2 of 8 differ 180 of 834 361 (bs=1), 73 (bs=32)
-dev HRX0 -ngl 0 after -1.084810 bit-identical 834 of 834 1
after + GGML_HRX_CLAIM_HOST_BUFTS=1 -1.369900 same as before 180 361
-dev HRX0 -ngl 99 before -1.374510 - 1 of 834 (GET_ROWS) 2
-dev HRX0 -ngl 99 after -1.374510 unchanged 1 of 834 (GET_ROWS) 2

200-token greedy answer to the sky prompt:

arm chars worst repeated sentence text
-dev none 1049 1x Rayleigh-scattering prose
-dev HRX0 -ngl 0 before 1012 15x "The paragraph should be in English..."
-dev HRX0 -ngl 0 after 1049 1x byte-identical to -dev none

pp512 / tg128 (llama-bench -p 512 -n 128 -r 3 -b 32 -ub 32), two interleaved rounds, temperature and current
sclk recorded per arm. The box's clock swung from 600 to 2480 MHz between arms; the CPU-reference arms show it
(pp 110.5 then 123.9). Per-arm placement in the same run confirms which binary ran.

arm r1 pp512 r1 tg128 r2 pp512 r2 tg128
-dev none -ngl 0 110.50 35.36 123.90 37.18
-dev HRX0 -ngl 0 before 146.99 16.99 228.29 23.74
-dev HRX0 -ngl 0 after 123.31 37.35 123.55 37.36
-dev HRX0 -ngl 99 before 729.56 71.30 725.27 72.18
-dev HRX0 -ngl 99 after 725.27 72.07 717.86 71.66

So -ngl 0 becomes the CPU route (same numbers as -dev none within the clock swing) instead of a hybrid that
decoded 1.6-2.1x slower than the CPU reference. -ngl 99 is unchanged in placement (at bs=1) and in pp/tg; at
bs=32 the after run places one GET_ROWS on the CPU as a result of no longer accepting the CPU index tensor in
place, which does not move pp/tg.

Notes

  • The -ngl 99 repetition loop on this prompt is not placement and is unchanged. Later comments in [Bug(CMake 3.17)] CUDA::cublasLt not found but can be specified absolutely ggml-org/llama.cpp#1078 trace
    it to the quantised matmul path; this change neither introduces nor fixes it.
  • GGML_HRX_CLAIM_HOST_BUFTS=1 restores the previous placement exactly (-1.369900, 180 of 834 CPU nodes, 361
    splits), so the in-place host path is still available for host-weight residency work.
  • Device measurement, not a ctest: a placement test needs a live HRX device.

Engine follow-up

Bump third_party/llama.cpp to this branch's head. After the bump -dev HRX0 -ngl 0 is the CPU route
(bit-identical to -dev none), so CPU-reference numbers and "is it coherent?" checks taken with a device present
stop measuring HRX kernels. A lane that wants the old in-place host path for host-weight residency work sets
GGML_HRX_CLAIM_HOST_BUFTS=1.

A device that supports every host buffer type is the highest-priority backend for host-resident
weights, so the scheduler places host-weight nodes on HRX and then spreads the device over the graph
that follows. At -ngl 0 that ran most of the graph on HRX over host tensors: 180 of 1266 nodes on the
CPU, first-token logprob -1.369900 against -1.084810 on the CPU route, and a greedy essay that loops
(engine ggml-org#1078).

HRX keeps claiming its own device buffer and host arena; another backend's host buffer is claimed only
with GGML_HRX_CLAIM_HOST_BUFTS=1, which keeps the in-place host path available when it is asked for.

Measured on strixhalo, Qwen3-4B-IQ4_XS, -c 512 -b 32 -ub 32: -dev HRX0 -ngl 0 now decodes exactly like
-dev none (max |dlogprob| 0 over the top-8, 1266 of 1266 nodes on the CPU, essay prose again), and
-dev HRX0 -ngl 99 placement and pp/tg are unchanged.

Assisted-by: DeepSeek Harness
@bong-water-water-bong
bong-water-water-bong merged commit c2c078b into 1bit/hrx-vulkan-patched Nov 6, 2026
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