Compare commits

...
Author SHA1 Message Date
Ruben Ortlam af2146b768 allow dmabuf p2p 2026-08-25 13:08:16 +02:00
Ruben Ortlam b4acc6d3d6 fix compile issue 2026-08-25 12:33:50 +02:00
Ruben Ortlam 6634f71f4a Merge branch 'pr-25051' into 0cc4m/vulkan-allreduce 2026-08-25 12:26:42 +02:00
Ruben Ortlam e46b3ae5ff reverse check order 2026-08-25 12:19:26 +02:00
Ruben Ortlam 50e0a2dd81 fix compile issues 2026-08-25 11:48:52 +02:00
Ruben Ortlam 4d879b9cfa use memcpy for small copies across host-visible memory 2026-08-25 11:42:13 +02:00
Ruben Ortlam bce45fbe90 add semi-async staging copy and use it over syncfd for small copies 2026-08-25 11:42:13 +02:00
Ruben Ortlam bac1369095 swap dmabuf check order 2026-08-25 11:42:13 +02:00
Ruben Ortlam 86ad441765 use allocator instead of hop2 2026-08-25 11:42:13 +02:00
Ruben Ortlam 5f512ae71b use sync_fd binary semaphores for cross-driver synchronization 2026-08-25 11:42:13 +02:00
Ruben Ortlam 4f4cea3112 double buffering 2026-08-25 11:42:13 +02:00
Ruben Ortlam 3e53e38de3 fixes, disable semaphore sharing on Nvidia + non-Nvidia 2026-08-25 11:42:13 +02:00
Ruben Ortlam bf316ac516 add async copy 2026-08-25 11:42:13 +02:00
Ruben Ortlam 740c12733e vulkan: add optimized device to device copy function 2026-08-25 11:42:13 +02:00
Piotr Wilkin 13ac901567 vulkan: fix -sm tensor decode crawl, comm init robustness, add opt-in tree all-reduce
Three related changes to the Vulkan -sm tensor (tensor-parallel) all-reduce,
developed and measured on 2x Radeon AI PRO R9700 (RDNA4/GFX1201, Mesa 26.1.2):

1. Shrink the comm staging buffers on the prefill->decode transition.
   ensure() only ever grew the host/tmp buffers, to the peak prefill micro-batch
   (~10 MB at n_ubatch=512), then reused them for the tiny (~20 KB) decode
   all-reduces. On RADV the imported external host memory is made visible across
   devices on every timeline-semaphore signal, at a cost proportional to the
   resident buffer size, so an oversized leftover cap stalled every decode step
   (cross-device wait ~770 us vs ~30 us), collapsing decode from ~30 to ~2.4 t/s
   and staying stuck for the whole session (the AMD "multi-turn crawl"). ensure()
   now also shrinks when the request is much smaller than cap, with a one-time
   semaphore wait so the realloc is safe against the previous async all-reduce.
   Decode after a large prefill: 2.4 -> ~31 t/s, flat across prefill sizes.

2. Decide proxy vs native cross-device sync before creating the progress
   timelines. They were created as exportable up front, which aborted init on
   devices that cannot export timeline semaphores (e.g. llvmpipe, RADV on older
   Mesa) instead of falling back to the portable CPU proxy. Now create exportable
   timelines only for the native path and plain ones for the proxy path.

3. Add ggml_backend_vk_comm_allreduce_tree: an opt-in recursive halving/doubling
   all-reduce for power-of-two device counts (2*log2(n) cross-device steps vs the
   ring's 2*(n-1); bandwidth-optimal). Enabled via GGML_VK_COMM_TREE; the ring
   stays the default and handles non-power-of-two counts. The step schedule is
   built as in a reference simulation verified for n=2..8; validated at n=2
   (native and forced proxy) to match the ring's greedy output byte-for-byte.

Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SazLJJfgpjt9Kq7JXKKnuw
2026-06-29 11:49:48 +01:00
Piotr WilkinandClaude Opus 4.8 75e77503b2 vulkan: remove the all-to-all pipeline and fp32 staging from the comm
With the ring as the default there is no reason to keep the slower paths:
- Remove ggml_backend_vk_comm_allreduce_pipeline (the O(n^2) all-to-all) and
  GGML_VK_COMM_PIPELINE. The ring is now the unconditional large-tensor path;
  the comm->ring flag and pipe_round are gone, and pipeline_ok (the "has two
  queues" gate the ring needs) is renamed ring_ok.
- Remove fp32 staging and GGML_VK_COMM_FP32. The ring always stages F16 (fp32
  accumulator preserved); its fp32 branch and use_f16 are removed.

Net ~-260 lines. Verified byte-identical greedy output (ring / proxy) and clean
build on 4x A16. The decode single-shot and the meta-backend butterfly fallback
are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApKCQ32VLqUW4Kus6tUvBL
2026-06-28 21:06:05 +02:00
Piotr WilkinandClaude Opus 4.8 bd1d3189b2 vulkan: drop D2D, make the ring the default, remove the OFF toggle
- Remove the GGML_VK_COMM_D2D peer-buffer path entirely (helpers,
  VK_KHR_external_memory_fd / VK_EXT_external_memory_dma_buf detection+enable,
  comm fields, init validation, ensure() branch). On AMD it regresses badly:
  the dmabuf import lands in GTT (PCIe P2P not established on stock kernels),
  so peer reads stall the GPU (pp/tg down 40-90%, power collapses). Not viable
  without box access to validate true VRAM P2P; revisit later.
- Make the O(n) ring the default large-tensor AllReduce. The old all-to-all
  pipeline is now opt-in via GGML_VK_COMM_PIPELINE (was: ring opt-in via
  GGML_VK_COMM_RING).
- Remove the GGML_VK_COMM_OFF toggle (forced the meta-backend butterfly on
  Vulkan); the custom comm is always better. The generic butterfly fallback in
  the meta backend stays -- it is the shared fallback for CUDA/SYCL and for
  Vulkan configs without a usable custom comm (e.g. MoltenVK).

Verified byte-identical greedy across ring / pipeline / proxy / fp32 on 4x A16.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApKCQ32VLqUW4Kus6tUvBL
2026-06-28 20:20:26 +02:00
Piotr WilkinandClaude Opus 4.8 5a829a9ac9 vulkan: strip comments from the -sm tensor comm code
Remove all comment lines and trailing comments from the tensor-parallel comm
implementation and its device-extension hooks. No behaviour change (verified
byte-identical greedy output across ring/proxy/D2D/butterfly after the strip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApKCQ32VLqUW4Kus6tUvBL
2026-06-28 17:04:22 +02:00
Piotr WilkinandClaude Opus 4.8 831bc206da vulkan: F16 staging for the ring AllReduce
The ring transported fp32 chunks; the pipeline already halves host/peer traffic
by staging F16. Bring the ring to parity: keep tensors[i] as the fp32
accumulator (matching the pipeline's precision) but cast each chunk to F16 for
transport. The cast is folded into the recv step (the just-reduced chunk is the
next step's send), so it costs only one extra pre-cast prog value rather than a
doubled scheme; per-step up16 slots avoid a send-buffer WAR. GGML_VK_COMM_FP32
still forces fp32.

Verified byte-identical greedy output vs fp32 ring / pipeline / butterfly on
4x A16, and no regression (ring-f16 ~= ring-fp32 ~= pipeline on A16 and 4090).
The bandwidth win only shows when comm is exposed (comm-bound hosts); on this
NVIDIA box the ring's transfer/compute overlap hides the comm, so F16 is neutral
here -- same comm-hidden reason the other comm micro-opts are neutral on NVIDIA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApKCQ32VLqUW4Kus6tUvBL
2026-06-28 16:56:58 +02:00
Piotr WilkinandClaude Opus 4.8 aa52642881 vulkan: prototype device-to-device peer buffers for -sm tensor (GGML_VK_COMM_D2D)
Opt-in path that replaces host-memory staging in the tensor-parallel AllReduce
with direct peer reads of another GPU's VRAM over PCIe P2P: each device's
partial lives in an exportable device buffer (DMA_BUF), peers import it, and the
existing comm reads host_buf[k][i] -- now peer VRAM -- unchanged. Targets the
O(n^2)-host-bandwidth scaling collapse; pairs with the O(n) ring. Ordering still
uses native/CPU-proxy semaphores (proxy auto-selects on RADV), so this is the
"D2D data + proxy semaphores" combo.

Enables VK_KHR_external_memory_fd + VK_EXT_external_memory_dma_buf. DMA_BUF is
the cross-device handle type (OPAQUE_FD memory is spec-locked to one physical
device); this matches the amdgpu PCIe-P2P dma-buf mechanism RADV/ROCm use.

Status: the fast path is AMD-targeted and UNVALIDATED -- NVIDIA's Vulkan driver
rejects cross-device fd import (vkGetMemoryFdPropertiesKHR -> memoryTypeBits=0;
confirmed against the Vulkan spec's same-deviceUUID rule and NVIDIA's own
statements), and NVIDIA has no fd-import or device-group P2P for unlinked GPUs.
So on NVIDIA it logs once and gracefully falls back to host staging -- verified
byte-identical and at host speed (pp2048 692 vs 695) on 4x A16, not the slow
butterfly. An AMD multi-GPU rig is needed to validate the actual P2P fast path
(test recipe accompanies this work, incl. how to prove real VRAM P2P vs a silent
amdgpu GTT fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApKCQ32VLqUW4Kus6tUvBL
2026-06-28 15:58:08 +02:00
Piotr WilkinandClaude Opus 4.8 f192a63855 vulkan: add CPU-proxy bridge support to the ring AllReduce
The ring AllReduce was gated to the native peer-import path (!comm->proxy),
so on RADV/cross-vendor setups (which use the CPU-proxy bridge because
OPAQUE_FD timeline export is unsupported) GGML_VK_COMM_RING had no effect.

Mirror the pipeline's proxy pattern in the ring: the per-step recv wait on
the previous neighbour's transfer timeline, and the cross-round WAR wait on
the next neighbour's compute timeline, are routed through the device's pxy
semaphore and a bridge enqueued for the helper thread. Reserve nsteps+1 pxy
values per round (nsteps recv bridges + 1 WAR bridge).

Validated byte-identical: native ring == proxy ring == butterfly
(660b3d04a269) on 4x A16 forced-proxy. fp32 staging only; F16 is a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApKCQ32VLqUW4Kus6tUvBL
2026-06-27 22:54:17 +02:00
Piotr Wilkin 01ea44ef95 vulkan: add O(n) ring AllReduce for -sm tensor (GGML_VK_COMM_RING)
Assisted-by: Claude Opus 4.8
2026-06-27 19:47:10 +02:00
Piotr Wilkin e578ca2c83 ggml-backend : resolve multi-buffer wrappers to sub-buffers in meta backend
Assisted-by: Claude Opus 4.8
2026-06-27 17:16:03 +02:00
Piotr Wilkin 362fdd2b7f vulkan: generalize -sm tensor AllReduce to >2 devices
Assisted-by: Claude Opus 4.8
2026-06-27 13:29:37 +02:00
Piotr Wilkin a448deb85c fix Windows build 2026-06-26 23:06:28 +02:00
Piotr Wilkin 68c289bd76 vulkan: portable CPU-proxy fallback for cross-driver -sm tensor AllReduce
The GPU-side cross-device ordering imports each peer's OPAQUE_FD timeline
semaphore, but OPAQUE_FD payloads are driver-private, so the import only works
when all devices share a driver (e.g. two NVIDIA GPUs). On mixed drivers or
vendors it is out of spec.

Add a portable fallback: a helper thread polls each peer's progress/upload
timeline and host-signals a local timeline that the consumer's download is
parked on (core timeline semaphores plus host signal/wait, no imported handle).
Both the chunked pipeline (prefill) and the single-shot (decode) paths are
bridged, so proxy mode no longer drops decode to the meta-backend butterfly.

A capability gate (vkGetPhysicalDeviceExternalSemaphoreProperties plus a
driverUUID match) selects the proxy deterministically on unsupported configs;
GGML_VK_COMM_PROXY forces it, and the import try/catch stays as a safety net.
Measured within ~4% of the native-import path on decode and on par for
prefill, with byte-identical output.

Assisted-by: Claude Opus 4.8
2026-06-26 22:59:26 +02:00
Piotr Wilkin 5dc559cdd7 vulkan: GPU-pipelined multi-GPU AllReduce for -sm tensor
Implements the backend-agnostic comm hook (ggml_backend_comm_init /
_allreduce_tensor / _free, discovered by the meta backend via
get_proc_address) for the Vulkan backend, so tensor-parallel inference no
longer falls back to the meta backend's CPU-barriered butterfly AllReduce.

Consumer GPUs have no P2P here, so the reduce stages through host memory, but
everything is ordered on the GPU via exported timeline semaphores (no CPU
barriers between layers). Each slice is split into chunks: the dedicated
transfer queue streams this device's slice out to shared host memory while the
compute queue pulls each peer chunk back as soon as it lands, so the two PCIe
directions overlap (full-duplex). Partials are cast to F16 before the host
transfer to halve the bytes on the bandwidth-bound link and added straight into
the fp32 result via the mixed-type add pipeline. Large prefill activations use
this pipeline; small (decode) tensors take a single-shot path where the fixed
per-call overhead dominates.

Roughly 2.5-3x the butterfly fallback; at long context it overtakes -sm layer
and is competitive with CUDA/NCCL on prefill. GGML_VK_COMM_OFF disables the
custom comm (falls back to butterfly); GGML_VK_COMM_FP32 forces fp32 staging.

Assisted-by: Claude Opus 4.8
2026-06-26 22:59:26 +02:00
Piotr Wilkin 325cceab0f ggml-backend-meta: fix SPLIT_MODE_TENSOR segfault on multi_buffer KV-cache views
When a per-device allocation exceeds the backend's max buffer size (e.g. a large
KV cache), ggml-alloc returns a multi_buffer wrapping several real buffers.
Compute-graph views inherited that multi_buffer as their backend buffer, so a
backend that casts tensor->buffer->context to its own buffer-context type (the
Vulkan backend does, e.g. in ggml_vk_tensors_overlap) dereferenced garbage and
crashed with -sm tensor (issue #22197).

A view aliases its source's storage, so it must reference the source's real
sub-buffer: set t_ij->buffer = t_ij->view_src->buffer. This is the correct ggml
invariant and a no-op in the single-buffer case.

Assisted-by: Claude Opus 4.8
2026-06-26 22:59:26 +02:00
4 changed files with 2361 additions and 40 deletions
+3
View File
@@ -83,6 +83,9 @@ extern "C" {
GGML_API ggml_backend_buffer_t ggml_backend_multi_buffer_alloc_buffer(ggml_backend_buffer_t * buffers, size_t n_buffers);
GGML_API bool ggml_backend_buffer_is_multi_buffer(ggml_backend_buffer_t buffer);
GGML_API void ggml_backend_multi_buffer_set_usage(ggml_backend_buffer_t buffer, enum ggml_backend_buffer_usage usage);
// resolve the physical sub-buffer whose memory range contains addr (NULL if none); sub-buffers have a
// usable context/get_base, the multi-buffer wrapper does not, so backends need this to find the real buffer
GGML_API ggml_backend_buffer_t ggml_backend_multi_buffer_get_buffer(ggml_backend_buffer_t buffer, const void * addr);
//
// Backend (meta)
+15 -1
View File
@@ -1255,10 +1255,24 @@ static enum ggml_status ggml_backend_meta_buffer_init_tensor_impl(ggml_backend_m
}
if (t_ij->view_src != nullptr) {
t_ij->data = (char *) t_ij->view_src->data + t_ij->view_offs;
} else if (simple_buf != nullptr) {
if (t_ij->view_src->buffer != nullptr) {
t_ij->buffer = t_ij->view_src->buffer;
}
} else if (simple_buf != nullptr && !ggml_backend_buffer_is_multi_buffer(simple_buf)) {
// single contiguous buffer: mirror the offset. A multi-buffer has no usable base, so leave the
// tensor for the gallocr to place (alloc_ctx_tensors assigns each tensor a real sub-buffer).
t_ij->data = (char *) ggml_backend_buffer_get_base(simple_buf)
+ size_t(tensor->data) - size_t(ggml_backend_buffer_get_base(tensor->buffer));
}
// Backends require the physical buffer, not the multi-buffer wrapper (its context is unusable).
// Resolve it from the tensor's data address -- works for any tensor, not just views.
if (t_ij->buffer != nullptr && t_ij->data != nullptr
&& ggml_backend_buffer_is_multi_buffer(t_ij->buffer)) {
ggml_backend_buffer_t sub = ggml_backend_multi_buffer_get_buffer(t_ij->buffer, t_ij->data);
if (sub != nullptr) {
t_ij->buffer = sub;
}
}
t_ij->extra = tensor->extra;
for (int i = 0; i < GGML_MAX_SRC; i++) {
t_ij->src[i] = tensor->src[i];
+14
View File
@@ -725,6 +725,20 @@ bool ggml_backend_buffer_is_multi_buffer(ggml_backend_buffer_t buffer) {
return buffer->iface.free_buffer == ggml_backend_multi_buffer_free_buffer;
}
ggml_backend_buffer_t ggml_backend_multi_buffer_get_buffer(ggml_backend_buffer_t buffer, const void * addr) {
GGML_ASSERT(ggml_backend_buffer_is_multi_buffer(buffer));
ggml_backend_multi_buffer_context * ctx = (ggml_backend_multi_buffer_context *) buffer->context;
for (size_t i = 0; i < ctx->n_buffers; i++) {
ggml_backend_buffer_t sub = ctx->buffers[i];
const char * base = (const char *) ggml_backend_buffer_get_base(sub);
const size_t size = ggml_backend_buffer_get_size(sub);
if ((const char *) addr >= base && (const char *) addr < base + size) {
return sub;
}
}
return NULL;
}
void ggml_backend_multi_buffer_set_usage(ggml_backend_buffer_t buffer, enum ggml_backend_buffer_usage usage) {
GGML_ASSERT(buffer);
GGML_ASSERT(ggml_backend_buffer_is_multi_buffer(buffer));
File diff suppressed because it is too large Load Diff