Skip to content

perf(gqh): port the multi-column matvec optimizations from the llama.cpp fork - #33

Open
DeanoC wants to merge 1 commit into
perf/gqh-matvec-gfx1201from
perf/gqh-multicol-port
Open

perf(gqh): port the multi-column matvec optimizations from the llama.cpp fork#33
DeanoC wants to merge 1 commit into
perf/gqh-matvec-gfx1201from
perf/gqh-multicol-port

Conversation

@DeanoC

@DeanoC DeanoC commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #32 — review that first; this branch adds one commit on top of it.

What

Ports the ncols>1 work from GeometricAGI/llama.cpp#1 into the vendored gqh.cu, verbatim. The two trees' gqh.cu were byte-identical before this, so it is a clean single-file transfer.

The defect: the generic <RUNG,8,1> instantiation re-read activations per (row, superblock, column) — 1024 B per warp per column against 137 B of wire, ~12 TB/s effective (L0-saturated). Weights stream once regardless of ncols, so N=3 should cost about N=1; instead each extra column cost ~+9 ms. Fixed with exact-width instantiations sharing one activation read, wave-uniform addressing, a shortened per-superblock dependency chain, and exact-width arms out to five columns.

Honest scope: the gain is currently latent in lucebox

On the fork this took MTP n-max=2 decode from 33.4 → 50.3 tok/s. Nothing in lucebox dispatches ncols 2..3 today:

  • greedy decode is ncols 1
  • chunked prefill is ncols 512 → GEMM
  • dflash/ddtree spec-decode verify is 16–23 wide (DFLASH27B_DRAFT_BLOCK_SIZE 16, ddtree budget 22) — above the fused path's LUCE_MMVQ_MAX_NCOLS ceiling (default 3)

So this lands as dead-but-correct code until lucebox gains an MTP or narrow-draft consumer, or the cap changes (below). I'm proposing it anyway because the trees were identical and keeping them in sync is cheaper than re-deriving this later — but it should be reviewed as a sync, not as a speedup.

Verification

Greedy is unaffected, as expected since ncols 1 dispatches the same arm as before:

before after
hot_ms 33.6 ms 33.56 ms
decode 28.5 tok/s 28.5 tok/s
greedy sha 1269c41a… 1269c41a… (unchanged)

ctest -R gqh: 25/25 pass (backend + cpu-decode across gqh3/gqh2_h/gqh2_c/gqh4 × 1x256/4x512/64x1024, plus the header parser).

A separate, larger finding — the ncols cap

Measured, not changed in this PR. Raising LUCE_MMVQ_MAX_NCOLS from its default 3 lets the fused kernel take widths it now has exact arms for, and the fallback it replaces is pathological for GQH types:

cap N=1 N=4 N=8
3 (default) 33.60 ms 301.63 ms 300.76 ms
5 33.92 ms 47.82 ms 304.60 ms
8 33.69 ms 47.94 ms 112.85 ms

N=4 is 6.3× faster at cap ≥ 5. Worth noting this corrects an earlier claim of mine: the "N=4 = 304 ms cliff" was previously written off as a --profile-scaling rig artifact. It is not — it is the fused-path cap falling back to a dequant→GEMM chain.

I did not change the default because raising it also changes which kernel serves ncols 2..8, so batched-verify numerics move. The fused arm is bit-exact against the f32 reference and MMQ is not the same rounding, so this is arguably an accuracy improvement — but it needs its own frozen reference to demonstrate that, and the ncols-1 greedy sha structurally cannot provide one. Happy to do that as a follow-up if you want the default moved.

Measured on gfx1201 (R9700) only.

…cpp fork

Ports the ncols>1 work from GeometricAGI/llama.cpp#1 into the vendored
gqh.cu, verbatim (the two trees' gqh.cu were byte-identical before this).

The defect it fixes: the generic <RUNG,8,1> instantiation re-read the
activations per (row, superblock, column) -- 1024 B per warp per column
against 137 B of wire. Weights stream once regardless of ncols, so N=3
should cost about N=1; instead each extra column cost ~+9 ms. Fixed with
exact-width instantiations sharing one activation read, wave-uniform
addressing, a shortened per-superblock dependency chain, and exact-width
arms out to five columns.

On the fork this took MTP n-max=2 decode from 33.4 to 50.3 tok/s. **In
lucebox the gain is currently latent**: nothing here dispatches ncols 2..3.
Greedy decode is ncols 1, chunked prefill is ncols 512 (GEMM), and the
dflash/ddtree spec-decode verify is 16-23 wide, above the fused path's
LUCE_MMVQ_MAX_NCOLS ceiling (default 3). It becomes live if lucebox gains
an MTP/narrow-draft consumer, or with the cap change below.

Greedy is unaffected and verified: hot_ms 33.56 ms, decode 28.5 tok/s,
greedy sha 1269c41a... unchanged (ncols 1 dispatches the same arm as
before). ctest -R gqh: 25/25 pass.

Separately measured, not changed here: raising LUCE_MMVQ_MAX_NCOLS from its
default 3 lets the fused kernel take the widths it now has exact arms for,
and the fallback it replaces is pathological for GQH types --

  cap=3:  N=1 33.60   N=4 301.63   N=8 300.76   (ms/forward)
  cap=5:  N=1 33.92   N=4  47.82   N=8 304.60
  cap=8:  N=1 33.69   N=4  47.94   N=8 112.85

N=4 is 6.3x faster at cap>=5. Note this also changes which kernel serves
ncols 2..8, so batched-verify numerics move (the fused arm is bit-exact
against the f32 reference; MMQ is not the same rounding) -- it needs its own
frozen reference before shipping, which the ncols-1 greedy sha cannot
provide. Left as an env override rather than a default change.
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