Skip to content

cuda: block-per-row FWHT for widths 512 and up (+17% / +11% decode on folded bands) - #166

Open
bri-prism wants to merge 2 commits into
prismfrom
perf/cuda-fwht-block
Open

cuda: block-per-row FWHT for widths 512 and up (+17% / +11% decode on folded bands)#166
bri-prism wants to merge 2 commits into
prismfrom
perf/cuda-fwht-block

Conversation

@bri-prism

Copy link
Copy Markdown

What

One 256-thread block per row for the Hadamard transform at widths 512 and above, replacing the warp-per-row kernel (512 to 2048) and the shared-memory kernel (4096, 8192). Stages below the warp width use shuffles, the three stages up to the block width go through shared memory, the stages above it stay in registers. Same butterfly and the same sign convention as both old kernels, so results are unchanged. Widths 64 to 256 keep the warp kernel. GGML_CUDA_FWHT_LEGACY=1 restores the previous kernels for A/B.

Why

An nsys kernel summary of a 64-token decode on an H100 PCIe put the transform first among all kernels on the folded bands, ahead of the weight mat-vecs:

share of GPU time calls per call
block 4096, shared-memory kernel 25.8% 8,385 13.8 us
block 2048, warp kernel 22.5% 7,625 7.7 us

At one row the warp kernel holds N/32 floats per lane and runs all log2(N) stages on a single warp while the rest of the GPU idles; the shared-memory kernel synchronises twice per stage for all twelve stages. This is the same latency structure the Metal side had before the threadgroup-FWHT change.

Verification (H100 PCIe, CUDA 12.8, base 8c0170d19, separate worktrees, -p 512 -n 128 -r 3, two interleaved passes)

  • test-backend-ops test -b CUDA0 -o MUL_MAT_HADAMARD: exit 0, 27/27 passed.
  • Greedy output byte-identical to base on both models tested (48 tokens each).
  • Same kernel summary after the change: transform 25.8% -> 14.0% of GPU time, 13.8 -> 6.8 us per row.
band tg128 base tg128 this PR tg128 with GGML_CUDA_FWHT_LEGACY=1 pp512 base pp512 this PR
9B, block 4096 165.9 / 167.6 194.6 / 194.9 (+16.7%) 170.7 / 166.3 6,093 / 6,078 6,313 / 6,323 (+3.7%)
2B, block 2048 355.0 / 372.1 391.7 / 415.2 (+11.3%) 354.4 / 352.1 16,288 / 18,420 16,673 / 19,102

The legacy-env column is the same binary with the old kernels, and it lands back on the base numbers, so the delta is the kernel and not the build. Prefill spread on this card is wide (pp512 error bars are 900 to 4,700), so read the prefill column as "not worse".

Profiled 9B and 2B decode on an H100: the Hadamard transform was the largest
single kernel, 25.8% of GPU time at block 4096 (shared-memory kernel, 13.8 us
per row) and 22.5% at block 2048 (warp kernel, 7.7 us per row). At one row the
warp kernel holds N/32 registers per lane and runs every stage on one warp; the
shared-memory kernel synchronises twice per stage for all log2(N) stages.

fwht_cuda_block runs one 256-thread block per row: stages below the warp width
use shuffles, the three stages up to the block width go through shared memory,
the rest stay in registers. Same butterfly and sign convention as before.
Widths 64-256 keep the warp kernel; GGML_CUDA_FWHT_LEGACY=1 restores the old
kernels for A/B.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation is correct and covered by existing FWHT tests; only minor comment-style cleanup remains.

Pull request overview

Optimizes CUDA FWHT latency for wide rows by using one 256-thread block per row.

Changes:

  • Adds a block-per-row FWHT kernel for widths 512-8192.
  • Preserves legacy kernels behind GGML_CUDA_FWHT_LEGACY.
File summaries
File Description
ggml/src/ggml-cuda/fwht.cu Adds and dispatches the optimized wide-row kernel.
Review details

Suppressed comments (1)

ggml/src/ggml-cuda/fwht.cu:254

  • This launch-policy comment is hard-wrapped across four lines and includes benchmark-specific rationale, contrary to the concise, non-hard-wrapped comment convention in AGENTS.md:66-73. Retain the dispatch rule and legacy override without the task-specific history.
    // From 512 up, one block of FWHT_BLOCK_THREADS per row (fwht_cuda_block); the warp kernel
    // serialised the whole transform on one warp and the shared-memory kernel synchronised every
    // stage, and both were the largest single kernel of a decode step on the folded models.
    // GGML_CUDA_FWHT_LEGACY=1 restores the previous kernels for A/B.
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ggml/src/ggml-cuda/fwht.cu Outdated
Both blocks hard-wrapped prose, and the first restated the stage-by-stage
implementation that the stage comments below it already document.

Kept only why this kernel is selected at these shapes, and the legacy env knob.
One sentence per line, none split across lines.

Comments only, no change in behaviour.
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