Skip to content

perf(ggml/hip): speed up fp3 ROCMFPX matvec (Q3_0_ROCMFPX) on RDNA#19

Closed
DeanoC wants to merge 1 commit into
base/ds4-mmq-spec-b9e5804from
feat/ds4-fp3-hip-matvec
Closed

perf(ggml/hip): speed up fp3 ROCMFPX matvec (Q3_0_ROCMFPX) on RDNA#19
DeanoC wants to merge 1 commit into
base/ds4-mmq-spec-b9e5804from
feat/ds4-fp3-hip-matvec

Conversation

@DeanoC

@DeanoC DeanoC commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Optimizes the Q3_0_ROCMFPX (fp3, 3.5 bpw) vec-dot — the hottest GPU kernel in
DeepSeek-V4-Flash DSpark decode, since the expert down-projections are stored in
fp3. Three HIP-only, fp3-only changes, each verified bit-identical to the code
it replaces. All other quant types and all non-HIP builds are untouched.

Depends on (and measured against)

This stacks on two open upstream PRs — both are prerequisites and the benchmark base:

The vec_dot_rocmfpx_fp3_q8_1 / fp3 MMQ kernels optimized here come from the vendored ROCmFPX support (byte-safe / fused fp3 decode, cf. Luce-Org#503). The PR base branch base/ds4-mmq-spec-b9e5804 is exactly #496 merged with #513; only the 3 kernel changes below differ from it.

Changes (single concern: the fp3 HIP matvec)

  1. Vectorized fp3 code decode (vecdotq.cuh) — decode 4 fp3 codes with a single
    v_perm_b32 over an 8-byte LUT (rocmfpx_pack4_fp3_bits12_vec_cuda) instead of 4
    scalar decodes + make_char4; extract the 12-/24-bit code window with one
    dword-splice instead of a per-bit OR loop.
  2. Folded epilogue in vec_dot_rocmfpx_fp3_q8_1 — the MMVQ launcher only ever emits
    even iqs, so both VDR groups always land in the same half-block; fold to a single
    accumulator and select the half-block scale once, dropping a dead dp4a + branch and
    a redundant scale conversion/fma from the per-block epilogue.
  3. ds_swizzle warp reduction (mmvq.cu) — reduce the fp3 partial sums with a
    bitmask-mode ds_swizzle butterfly instead of the ds_bpermute lowering that HIP
    emits for warp_reduce_sum; identical {16,8,4,2,1} reduction order and float adds.

Benchmark

  • Hardware: AMD Ryzen AI Max (gfx1151 / Strix Halo, RDNA3.5 APU), ROCm 7.2.4, unified memory.
  • Model: DeepSeek-V4-Flash, native ROCMFPX GGUF (ROCMFP2 experts / fp3 down-proj / ROCMFP4 attention).
  • Workload: DSpark speculative decode (--verify-width 4), greedy (temperature 0), fixed 160-token prompt.
  • Clock control: mclk pinned to 1000 MHz (rocm-smi --setmclk 2). sclk is not lockable on this APU (the SMU firmware ignores perf-determinism, DPM-mask, and ceiling controls), so each config runs 2 full-length warm-ups to reach steady clock, then 7 measured reps, and we report the achieved sclk so the comparison is matched-clock.
  • Base: #496 merged with #513 (branch base/ds4-mmq-spec-b9e5804, this PR's parent); only the 3 kernel changes differ.
Config decode tok/s (median, n=7) accept_rate achieved sclk (MHz)
base 21.7 (± 0.14) 0.78 1786
this PR 24.3 (± 0.07) 0.78 1843

+12.0% median throughput; +11.0% at matched clock (at ~1855 MHz: base 21.9, PR 24.3).
accept_rate unchanged (0.78). Reproduced across two independent runs (+12.6%, +12.0%).

Correctness

Greedy output is byte-identical to base across 8 diverse prompts (code, algorithms,
exact arithmetic, logic, long-form, JSON, factual recall, repetition) on both decode
paths — the single-token MMVQ path and the DSpark verify (MMQ) path — SHA-256 of each
completion matches per prompt. Every transformation is annotated in-code with its
bit-identity argument. The one exception is a sign-of-zero corner in the epilogue fold,
which the cross-lane reduction washes out; it is documented at the call site.

Scope

HIP + GGML_TYPE_Q3_0_ROCMFPX only, gated by #if defined(GGML_USE_HIP) plus a
constexpr type check. No effect on CUDA builds or any other quant type. Apache-2.0.

@DeanoC
DeanoC requested review from a team and pramodith July 16, 2026 06:57
The Q3_0_ROCMFPX (fp3, 3.5bpw) vec-dot is the hottest GPU kernel in
DeepSeek-V4-Flash DSpark decode (the expert down-projections are stored
in fp3). Three HIP-only, fp3-only changes, each bit-identical to the code
it replaces:

- vecdotq.cuh: decode 4 fp3 codes with a single v_perm_b32 over an 8-byte
  LUT (rocmfpx_pack4_fp3_bits12_vec_cuda) instead of 4 scalar decodes +
  make_char4; extract the 12-/24-bit code window with one dword-splice
  instead of a per-bit OR loop.
- vecdotq.cuh: in vec_dot_rocmfpx_fp3_q8_1, fold the two half-block
  accumulators into one (the MMVQ launcher only ever emits even iqs, so
  both VDR groups share a half-block) and select the scale once, dropping
  a dead dp4a + branch and a scale conversion/fma from the epilogue.
- mmvq.cu: reduce the fp3 partial sums with a ds_swizzle butterfly
  (bitmask mode) instead of the ds_bpermute lowering of warp_reduce_sum;
  identical {16,8,4,2,1} order and float adds.

Safety guards: the ds_swizzle path falls back to warp_reduce_sum for any
non-wave32 HIP target; a compile-time static_assert ties the perm LUT to
the scalar decode (single source of truth); and a debug assert enforces
the fold's single-half-block (even-iqs) invariant. All other quant types
and all non-HIP builds are untouched.
@DeanoC

DeanoC commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #21, which squashes this and the stacked follow-up into a single review-friendly PR (same final kernel state, one diff off b9e5804). Closing to keep review in one place.

@DeanoC DeanoC closed this Jul 16, 2026
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