Skip to content

vulkan: allow proxy collectives without external semaphore FD functions - #23

Open
AndrewTilson wants to merge 15 commits into
pwilkin:vulkan-tp-p2pfrom
AndrewTilson:review/proxy-init-fix
Open

AndrewTilson wants to merge 15 commits into
pwilkin:vulkan-tp-p2pfrom
AndrewTilson:review/proxy-init-fix

Conversation

@AndrewTilson

@AndrewTilson AndrewTilson commented Sep 9, 2026 •

Copy link
Copy Markdown

Would you be willing to merge this into your branch so if/when you pr makes it to llama repo it incudes these minor changes. As you can see below there was some significant gains from this minor change on my windows system. I am running dual rtx 9700's.

Scope

Allow the existing portable CPU-proxy collective path to initialize when Linux
external semaphore FD functions are unavailable. Host-importable memory remains
required. Missing FD functions select the proxy rather than disabling the fast
collective implementation; the explicit proxy option preserves that selection.

One source file, four additions and three deletions. No harness changes, tuning
switches, diagnostic logging, or experimental Win32 import code are included.

Dependency and attribution

This PR deliberately targets review/pr25051-base, not main, so it shows only
our incremental fix to the proposed allreduce implementation:

The allreduce and DFlash implementations belong to their respective authors.
The tested combined tree also needed port adaptations; those are not part of
this small patch. Do not merge this base into main as if it were our own work.

Historical measurements and limits

Two R9700s, Windows Vulkan, driver 32.0.23033.1002, Q4_K_S target and Q4 DFlash
drafter, Q8 K/V, flash attention, 32K context, one slot, concurrency one.
Short JSON/code/tool fixtures, same workload across comparisons:

Combined integration | Passing requests | Aggregate output tokens/s -- | -- | -- Before proxy initialization fix | 36/36 | 33.891 With proxy initialization fix | 36/36 | 38.797 Same-period single-card control | 12/12 | 55.703 Same-period dual-layer control | 12/12 | 44.395

These are historical combined-build results, NOT a hardware benchmark of this
exact isolated patch on its PR25051-only base. Later post-reboot improvements
are not attributed to this fix. Tensor still did not beat single-card.
Short syntax/JSON/tool checks do not establish broad quality or correctness.

Direct TG16 diagnostic (one repetition, different settings from API tests):
17.235 before versus 19.540 tokens/s after; trace changed from fast=0 to
fast=1/proxy=1. No universal speedup is asserted.

Validation

  •  Patch applies to the exact pinned base.
  •  Applied contents match expected extraction; reverse apply check passes.
  •  git diff --check passes.
  •  Clean isolated patch build and matched hardware benchmark.
  •  Regression test for capability/transport selection.
  •  Backend-op correctness and perplexity validation.
  •  Linux native-FD regression and additional Windows workload coverage.

The fix is backend-level, not Python-harness-specific, but affects only
consumers using this proposed Vulkan meta-backend collective implementation.
Coordinate with the upstream PR author before any upstream submission.

Scope Allow the existing portable CPU-proxy collective path to initialize when Linux external semaphore FD functions are unavailable. Host-importable memory remains required. Missing FD functions select the proxy rather than disabling the fast collective implementation; the explicit proxy option preserves that selection.

One source file, four additions and three deletions. No harness changes, tuning
switches, diagnostic logging, or experimental Win32 import code are included.

Dependency and attribution
This PR deliberately targets review/pr25051-base, not main, so it shows only
our incremental fix to the proposed allreduce implementation:

ggml-org#25051
Base: 13ac901.
Historical DFlash integration tests also used
ggml-org#27858
Revision: 3286b03.
Related startup issue: ggml-org#27833.
The allreduce and DFlash implementations belong to their respective authors.
The tested combined tree also needed port adaptations; those are not part of
this small patch. Do not merge this base into main as if it were our own work.

Historical measurements and limits
Two R9700s, Windows Vulkan, driver 32.0.23033.1002, Q4_K_S target and Q4 DFlash
drafter, Q8 K/V, flash attention, 32K context, one slot, concurrency one.
Short JSON/code/tool fixtures, same workload across comparisons:

Combined integration Passing requests Aggregate output tokens/s
Before proxy initialization fix 36/36 33.891
With proxy initialization fix 36/36 38.797
Same-period single-card control 12/12 55.703
Same-period dual-layer control 12/12 44.395
These are historical combined-build results, NOT a hardware benchmark of this
exact isolated patch on its PR25051-only base. Later post-reboot improvements
are not attributed to this fix. Tensor still did not beat single-card.
Short syntax/JSON/tool checks do not establish broad quality or correctness.

Direct TG16 diagnostic (one repetition, different settings from API tests):
17.235 before versus 19.540 tokens/s after; trace changed from fast=0 to
fast=1/proxy=1. No universal speedup is asserted.

Validation
Patch applies to the exact pinned base.
Applied contents match expected extraction; reverse apply check passes.
git diff --check passes.
Clean isolated patch build and matched hardware benchmark.
Regression test for capability/transport selection.
Backend-op correctness and perplexity validation.
Linux native-FD regression and additional Windows workload coverage.
The fix is backend-level, not Python-harness-specific, but affects only
consumers using this proposed Vulkan meta-backend collective implementation.
Coordinate with the upstream PR author before any upstream submission.

pwilkin and others added 15 commits June 26, 2026 22:59
…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 ggml-org#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
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
…duce

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
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
…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
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
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
- 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
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
… 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
Prepared with AI assistance for personal-fork review; not an upstream submission.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants