Skip to content

metal : add Stage-1 diffusion dense sampler for DiffusionGemma (Apple Silicon)#1

Open
andreinknv wants to merge 1 commit into
danielhanchen:diffusion-visual-updatesfrom
andreinknv:diffusion-gemma-metal-sampler
Open

metal : add Stage-1 diffusion dense sampler for DiffusionGemma (Apple Silicon)#1
andreinknv wants to merge 1 commit into
danielhanchen:diffusion-visual-updatesfrom
andreinknv:diffusion-gemma-metal-sampler

Conversation

@andreinknv

Copy link
Copy Markdown

This targets the diffusion-visual-updates branch behind ggml-org#24423, adding a Metal port of the CUDA Stage-1 sampler so Apple Silicon runs on-device sampling instead of the per-step host fallback ("on-device sampling unsupported on this backend").

What

  • kernel_diffusion_dense_sample_f32 (ggml-metal.metal) — line-for-line structural mirror of diffusion_dense_sample_kernel in ggml-cuda/diffusion-sampling.cu: one 256-thread threadgroup per canvas row; parallel max→argmax tree reduction; parallel Z and T (T = Σ d·e) reductions with entropy = logZ − T/Z; slice-scanned multinomial first-crossing walk in vocab order. Same reduction structure ⇒ argmax matches the host bit-for-bit, Z/entropy differ only by FP reduction order.
  • ggml_metal_device_diffusion_sample (ggml-metal-device.m/.h) — synchronous one-shot dispatch on the device queue; grow-only shared scratch for u/argmax/entropy/sampled mirroring the CUDA per-device scratch cache; pipeline compiled lazily via ggml_metal_library_compile_pipeline.
  • Registry export (ggml-metal.cpp) — get_proc_address("ggml_backend_metal_diffusion_sample"), mirroring the CUDA boundary.
  • llama_diffusion_device_sample (diffusion-gemma.cpp) — resolves CUDA first, then MTL, so the CLI gpu-sample-reduce path lights up on Metal with no flag changes.

Verification

DG_DEVSAMPLE_CHECK=1 on diffusiongemma-26B-A4B-it-Q4_K_M (M4 Max, -ngl 99), every step of a 15-step run:

amax_mismatch=0/256   max|dH| <= 1.8e-04   tok_diff 0-4/256 (near-tie shifts only)

— the same tolerance profile documented for CUDA-vs-host.

Performance (M4 Max, Q4_K_M, 256-token canvas)

step time effective
host fallback 462 ms 36.9 tok/s
Metal sampler (-fa on) 390 ms 43.8 tok/s

The remaining step cost is the MoE forward pass, so the win is bounded (~+19%), but it also frees the CPU from the per-step 256 x n_vocab softmax/entropy reduction.

Happy to adjust style/structure to fit the PR — thanks for the DiffusionGemma work!

🤖 Generated with Claude Code

Port of the CUDA diffusion_dense_sample kernel to Metal so Apple
Silicon gets on-device sampling instead of the host fallback. One
256-thread threadgroup per canvas row: parallel max/argmax reduction,
parallel Z and T (T = sum d*e) reductions with entropy = logZ - T/Z,
then the slice-scanned multinomial first-crossing walk in vocab order
- the same structure as the CUDA kernel, so argmax matches the host
bit-for-bit and Z/entropy differ only by reduction order.

Wiring mirrors the CUDA backend boundary: the kernel is dispatched by
ggml_metal_device_diffusion_sample (grow-only shared scratch for
u/argmax/entropy/sampled), exposed through the Metal backend reg via
get_proc_address("ggml_backend_metal_diffusion_sample"), and
llama_diffusion_device_sample now resolves CUDA first, then MTL.

Verified with DG_DEVSAMPLE_CHECK=1 on diffusiongemma-26B-A4B-it
Q4_K_M (M4 Max): amax_mismatch 0/256 on every step, max|dH| <= 2e-4,
tok_diff 0-4/256 near ties - the same tolerance profile as
CUDA-vs-host. Per-step time 462ms -> 390ms, effective throughput
36.9 -> 43.8 tok/s with -fa on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
danielhanchen pushed a commit that referenced this pull request Jul 3, 2026
* Sycl tp stage1 (#1)

* SYCL: tensor parallelism (--split-mode tensor) for dual-GPU

Adds the comm_init/comm_free/comm_allreduce_tensor trio that the
meta-backend queries via get_proc_address to enable backend-specific
all-reduce, mirroring the pattern used by ggml-cuda.cu.

For N=2 (the common dual-GPU case) implements a degenerate ring
all-reduce with two size-branched paths:

  * Small (nelem < 32768): FP32 direct memcpy + per-device ADD kernel
    chained via depends_on(memcpy_event). 4 SYCL submissions/call.

  * Large (nelem >= 32768): BF16-compressed. Each device compresses
    FP32 -> BF16 in a local outbox, cross-device memcpys to the peer's
    inbox (HALF the PCIe bytes), then decompresses + adds into the
    local FP32 partial. 6 SYCL submissions/call but PCIe bytes halved
    -- wins for any tensor where PCIe dominates kernel time.

Threshold and BF16 path pattern mirror the CUDA NCCL allreduce.

Storage: ONE persistent uint8_t buffer per device, 4 * nelem bytes
(matches both path layouts: FP32 nelem floats; BF16 outbox+inbox =
2 * nelem uint16_t each). Single alloc+free per device keeps the
SYCL pool's strict-LIFO invariant trivial.

Initial impl handles N=2 FP32 contiguous tensors. Other cases return
false, causing the meta-backend to use its generic butterfly fallback.

Per-call sync is intentionally omitted. SYCL in-order queue semantics
ensure that the meta-backend's next compute on the same per-device
queue waits for our final ADD, and the next allreduce's first op on
the same persistent buffer waits via the same queue. Only comm_free
does an explicit final wait.

OneCCL is NOT used: OneCCL 2021.17 hardcodes single-device-per-process
in communicator_impl.hpp:47 (condition devices.size() == 1), which is
incompatible with llama.cpp's single-process multi-GPU model.

Measured on dual Intel Arc Pro B70 (NEO 26.05.x, oneAPI 2025.3 +
DPC++ nightly):

  Llama-3.3-70B Q4_K_M, -sm tensor -fa 1 -ctk f16 -ctv f16:
    pp512 = 377.08 t/s  (vs 313.65 layer mode = +20.2%)
    tg128 = 17.40 t/s   (vs   9.74 layer mode = +78.6%)

  Qwen3-Coder-Next-80B-A3B Q3_K_M (MoE):
    pp512 = 216.56 t/s  (vs 156.58 meta-backend butterfly = +38.3%)
    tg128 = 17.60 t/s   (vs  14.31 meta-backend butterfly = +23.0%)

  Qwen3-4B Q4_K_M:
    pp64  = 984.51 t/s, tg16 = 49.29 t/s

Llama-3.3-70B in SYCL TP now comfortably beats production layer mode
on both prefill and decode. Coder-Next-80B-A3B (MoE) also wins on
both — the BF16 path is what unlocks the many-medium-allreduces
prefill pattern.

Build/CMake: no changes. No new dependencies. ~210 lines added across
ggml-sycl.h and ggml-sycl.cpp.

* Fix comments

* documentation update to address PR feedback

* Bring over my device-to-device memcpy chagnes

* move the dev2dev_memcpy calls to the upstream 7-parameter variety

* Fix a typo and remove a trailing whitespace
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