Skip to content

Vulkan: batched decode throughput cliff at n_tokens=9 on many-expert MoE (fixed 8-token thresholds in MMV dispatch) #25356

Description

@ftoleedo

Summary

Batched decoding of a many-expert MoE model (Qwen3-Coder-Next 30B-A3B, 512 experts, top-10,
Q4_K_XL) on an AMD Strix Halo iGPU (gfx1151, RADV, Vulkan backend) shows a large throughput
cliff at exactly 9 concurrent sequences: aggregate TG drops from 122.5 t/s (B=8) to
82.9 t/s (B=9) and only recovers above B≥24. I traced it to two fixed batch-size thresholds
in the Vulkan backend that switch from the mul_mat_vec kernels to the tiled matmul kernels
above 8 tokens. Raising them removes the cliff on this hardware/model: +56% at B=9, +34% at
B=16, +20% at B=32, with monotonic scaling through B=32 and no regression at B≤8.

Environment

  • HW: AMD Ryzen AI MAX+ 395 (Strix Halo), Radeon 8060S iGPU (gfx1151), 128 GB unified LPDDR5X
  • Driver: RADV (Mesa), Vulkan backend, coopmat enabled
  • llama.cpp: reproduced on b9293 and on current master (20a04b2)
  • Model: Qwen3-Coder-Next-UD-Q4_K_XL (30B-A3B MoE, 512 experts, top-10)

Reproduction

llama-batched-bench -m Qwen3-Coder-Next-UD-Q4_K_XL.gguf -c 32768 -ngl 999 -fa 1 \
  -ctk q4_0 -ctv q4_0 --no-mmap -npp 512 -ntg 128 -npl 1,2,4,8,9,12,16,24,32

Aggregate TG t/s, master (20a04b2), stock:

B 1 2 4 8 9 12 16 24 32
stock 45.4 69.0 96.2 122.5 82.9 93.1 103.8 118.9 130.9

The cliff is at the B=8→9 boundary and recovers slowly — a dispatch-boundary signature, not
bandwidth saturation. GGML_VK_DISABLE_COOPMAT=1 makes B=16 worse (86.4), so it is not a
coopmat tile issue: any mul_mm path at small n loses to the MMV path on this bandwidth-bound
device.

Root cause

Two fixed thresholds switch away from the mul_mat_vec kernels above 8 tokens:

  1. mul_mat_vec_max_cols = 8 (ggml-vulkan.cpp) — dense projections;

  2. ggml_vk_use_mul_mat_vec_id(): src2->ne[1] <= 8 — the MUL_MAT_ID expert path, which
    dominates. GGML_VK_PERF_LOGGER=1 shows the expert matmuls growing super-linearly across
    the boundary (per-call, decode phase):

    • MUL_MAT_ID q4_K m=512 n=10 k=2048 n_expert=512: 166 µs at batch=8 → 440 µs at batch=12 (2.65×)
    • MUL_MAT_ID q5_K m=2048 n=10 k=512 n_expert=512: 213 µs → 488 µs (2.29×)

    Dense MUL_MAT_VEC entries scale sub-linearly per token across the same boundary (healthy).

The <= 8 heuristic makes sense for few-expert MoEs (8-expert Mixtral-style), where tokens
share experts and the gather-based mm-id amortizes weight reads. For a 512-expert/top-10
model, token–expert overlap at these batch sizes is near zero (B=12 → 120 draws over 512
experts), so switching to mm-id amortizes nothing and pays the tile-inefficiency cost.
vec-id dispatches per token (descriptor sets = nei1) and has no structural limit.

Experiment: raising the thresholds

Local change: mul_mat_vec_max_cols 8→16, and the vec-id gate ne[1] <= 8<= 32.

B 8 9 12 16 24 32
stock 122.5 82.9 93.1 103.8 118.9 130.9
patched 126.5 129.4 135.7 139.0 150.9 157.2
  • Monotonic through B=32; B=1–4 unchanged (49.1/71.5/99.5 vs 45.4/69.0/96.2).
  • test-backend-ops test -o MUL_MAT and -o MUL_MAT_ID pass vs CPU.
  • temp=0 output is byte-identical to stock (same model/prompt).
  • End-to-end (llama-server, real prompts, ~1600-tok streams): +41% aggregate at 16 concurrent
    slots, J/token floor drops 0.61 → 0.506 on this device.

Suggested direction

A flat constant is probably wrong in both directions — for few-expert MoEs the mm-id switch
is right where it is; for many-expert MoEs vec-id keeps winning far beyond 8. A density-based
gate for MUL_MAT_ID would capture both regimes, e.g. stay on vec-id while expected rows per
activated expert is low:

use_vec_id = n_tokens * n_experts_used <= C * n_expert   // C ≈ 1–2, to be tuned

(Qwen 512/top-10 → vec-id up to ~50–100 tokens; Mixtral 8/top-2 → switches at ~8, like today.)
For the dense mul_mat_vec_max_cols, per-arch tuning similar to what PR #20905 did for the
CUDA backend may apply; on gfx1151/RADV, 16 cols showed no downside in my runs.

I'm happy to run benchmarks/validation on this hardware (Strix Halo / gfx1151 / RADV) if
useful — full benchmark logs and the perf-logger dumps are available.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions