Skip to content

[ggml / Vulkan] queue user inputs in the backend stream; - #30169

Closed
Geramy wants to merge 2 commits into
ggml-org:masterfrom
Geramy:vulkan-staged-inputs
Closed

Geramy wants to merge 2 commits into
ggml-org:masterfrom
Geramy:vulkan-staged-inputs

Conversation

@Geramy

@Geramy Geramy commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Problem

To produce one token, llama.cpp runs a compute graph. Some of its tensors are inputs
that the CPU fills in fresh for every token. Before this change, the ggml scheduler copied
each of those inputs to the GPU one at a time, and each copy blocked: wait until the GPU
backend is idle, then write the input and wait again until the write has landed
(ggml-backend.cpp: ggml_backend_sched_compute_splits, the GGML_TENSOR_FLAG_INPUT
branch). When the CPU can write straight into GPU memory that write is just a memcpy.
When it can't, as on this Mac setup where RADV reports no CPU-visible VRAM, the Vulkan
backend's write (ggml-vulkan-buffers.cpp: ggml_vk_buffer_write_2d) records a small copy
command, submits it to the GPU on its own, waits on a fence until the GPU finishes it, then
resets the fence. With Qwen3.6 27B that happened six times per generated token. Each round
trip costs far more than the few bytes it moves, so decode spends time waiting instead of
computing.

Solution

The patch adds an optional backend function, set_tensor_async_staged. It copies the input into staging memory that the backend owns and queues the GPU copy behind work already submitted. It returns right away, and the caller can reuse its buffer immediately. The scheduler uses this function when the backend provides it and keeps the old blocking copy otherwise. In Vulkan, these copies run in the compute stream, and the staging memory is reused between tokens. Synchronization no longer sends a separate empty submission just to signal its fence: the fence rides on the last compute submission in the common decode case. For copies of tensors the CPU computed, a measured size threshold decides between the separate transfer queue and the compute stream. It is measured once per device and can be overridden with GGML_VK_COMPUTE_STREAM_COPY_MAX. Only copies large enough to pay for the extra submission and semaphore wait go on the transfer queue; smaller copies stay in the compute stream.

…thout empty submits

The scheduler copied each graph input from the user synchronously: it waited
for the split's backend and then wrote the input with a blocking set_tensor.
On Vulkan without host-visible VRAM, each write was its own queue submission,
fence wait and fence reset, six per decoded token with Qwen3.6 27B.

A new optional backend function, set_tensor_async_staged, queues a write of
host data after the backend's pending work and copies the data before it
returns, so the user may overwrite it at once, which is what the scheduler
needs for inputs. The scheduler uses it when the backend has it and falls
back to the synchronous copy otherwise. Vulkan implements it with host
staging memory owned by the backend context and a copy in the compute stream,
freed once the backend has synchronized.

ggml_vk_synchronize no longer adds an empty queue submission to signal its
fence: the fence (and the wait for transfer-queue work) goes with the last
compute submission, and when only transfer-queue work is outstanding the host
waits for its timeline semaphore.

A host-to-device copy goes on the transfer queue only when that pays off: its
own submission and the compute queue's semaphore wait must cost less than the
copy it lets overlap. The crossover, that overhead times the host-to-device
bandwidth, is measured once per device on first use (the overhead with the
host clock, the bandwidth with GPU timestamps around a staged copy) and
logged; GGML_VK_COMPUTE_STREAM_COPY_MAX sets it instead. Smaller copies go in
the compute stream.

test-backend-sched-inputs checks that inputs are taken when the graph is
queued and ordered against the work already queued on the device, and that
tensors computed on the CPU reach the device on both sides of the copy size
limit (--measured uses the measured limit).
@github-actions github-actions Bot added testing Everything test related Vulkan Issues specific to the Vulkan backend ggml changes relating to the ggml tensor library for machine learning SYCL https://en.wikipedia.org/wiki/SYCL - GPU programming language Apple Metal https://en.wikipedia.org/wiki/Metal_(API) Ascend NPU issues specific to Ascend NPUs OpenCL Issues specific to the OpenCL backend IBM zDNN issues specific to IBM zDNN Accelerator Hexagon CUDA Related to the CUDA backend AMD ZenDNN Issues related to the AMD ZenDNN backend OpenVINO WebGPU labels Oct 8, 2026
@Geramy

Geramy commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

@pwilkin my vulkan backend stream patch, this worked really well on my eGPU with the R9700 on Mac and increased my performance a lot. I believe it will also increase performance on a normal Server or Desktop with a R9700, latency is latency and for AI it costs period.

Upstream moved the scheduler's per-input copy into ggml_backend_sched_copy_input; the staged user-input write now lives there.
@JohannesGaessler

Copy link
Copy Markdown
Contributor

I did not look at this PR in detail but it should not be necessary with #25319 . Any ggml backend scheduler changes need to come after #27258 .

Also can we please establish an explicit rule against non-maintainers making changes to the ggml backend interface?

@Geramy

Geramy commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Benchmarks, Vulkan backend, llama-bench -ngl 99 -p 512 -n 128 -d 0,4096 -r 5, A = master, B = this PR:

  • Mac: AMD Radeon AI PRO R9700 (eGPU), macOS, Mesa RADV, no CPU-visible VRAM. Model unsloth Qwen3.6-27B-UD-Q4_K_XL.gguf. A = master 46847e6, B = this PR's earlier single commit 16aa486 (before the merge with master, which only moved the same scheduler change into the new helper). Measured 2026-10-04. A ran on the previous build of the Mac driver stack; an earlier version of this patch on that same build gave tg128 22.73 t/s (+9.4%).
  • Linux: AMD Radeon AI PRO R9700, kernel 7.2, Mesa 26.0.8 RADV, Resizable BAR (32 GiB BAR, all VRAM CPU-visible). Model unsloth Qwen3.8-27B-Q4_0.gguf. A = master 71ad059, B = 799519d. Runs interleaved A, B, A, B; values are mean ± stddev over 10 repetitions. The host had unrelated CPU load during the runs.
platform test A t/s B t/s change
Mac pp512 1108.82 ± 2.63 1117.00 ± 0.81 +0.7% (within run-to-run drift, other A runs 1090 to 1108)
Mac tg128 20.77 ± 0.18 22.93 ± 0.03 +10.4%
Mac pp512 @ d4096 1005.60 ± 4.24 1003.35 ± 3.15 -0.2% (within noise)
Mac tg128 @ d4096 20.47 ± 0.16 22.15 ± 0.09 +8.2%
Linux pp512 1224.66 ± 4.32 1216.69 ± 2.29 -0.7%
Linux tg128 32.46 ± 0.04 32.23 ± 0.03 -0.7%
Linux pp512 @ d4096 1144.90 ± 9.06 1140.82 ± 8.32 -0.4% (within noise)
Linux tg128 @ d4096 31.89 ± 0.03 31.70 ± 0.03 -0.6%

On Linux with ReBAR the input writes are already plain memcpys into VRAM, and B is about 0.6% slower on decode, consistent in both rounds.

test-backend-sched-inputs on B passes on the Linux R9700 (all 4 cases OK). It was not run on the Mac R9700.

@Geramy

Geramy commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

I did not look at this PR in detail but it should not be necessary with #25319 . Any ggml backend scheduler changes need to come after #27258 .

Thanks, I did not know the solution was on the way!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AMD ZenDNN Issues related to the AMD ZenDNN backend Apple Metal https://en.wikipedia.org/wiki/Metal_(API) Ascend NPU issues specific to Ascend NPUs CUDA Related to the CUDA backend ggml changes relating to the ggml tensor library for machine learning Hexagon IBM zDNN issues specific to IBM zDNN Accelerator OpenCL Issues specific to the OpenCL backend OpenVINO SYCL https://en.wikipedia.org/wiki/SYCL - GPU programming language testing Everything test related Vulkan Issues specific to the Vulkan backend WebGPU

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants