Skip to content

cuda: speed up Q1_0 extraction with byte permutes#73

Merged
khosravipasha merged 2 commits into
prismfrom
perf/cuda-q1-byte-perm
Jul 17, 2026
Merged

cuda: speed up Q1_0 extraction with byte permutes#73
khosravipasha merged 2 commits into
prismfrom
perf/cuda-q1-byte-perm

Conversation

@bri-prism

Copy link
Copy Markdown

What

  • Extract packed Q1_0 values with CUDA __byte_perm operations instead of scalar per-bit expansion.
  • Reuse the extraction helper in both MMVQ decode and MMQ batched/prefill paths.
  • Keep the scalar extraction fallback for HIP and MUSA builds.

Why

Scalar Q1_0 expansion adds measurable overhead before the existing int8 dot product. On an H100 with a large Q1_0 model, replacing that expansion increased TG128 from 99.32 to 110.38 tok/s and PP512 from 2767.55 to 2843.85 tok/s in an interleaved A/B.

This adapts the extraction approach from ggml-org#25628 to this fork's pre-split MMQ layout.

How

unpack_q1_0_bytes loads 16 packed bits at a time, expands them into signed int8 lanes with byte permutations, and returns four packed values ready for dp4a. vec_dot_q1_0_q8_1 uses the helper directly for MMVQ. load_tiles_q1_0 writes the same values into the existing MMQ shared-memory layout, so the downstream matrix path is unchanged.

HIP and MUSA select the original scalar signed expansion at compile time because __byte_perm is CUDA-specific.

Test plan

  • Release CUDA 12.8 build for sm_90.
  • Q1_0 test-backend-ops MUL_MAT correctness: 43/43 supported cases passed against the CPU reference.
  • H100 end-to-end A/B, three interleaved process runs per arm and five samples per process:
    • PP512: 2767.55 -> 2843.85 tok/s (+2.76%).
    • TG128: 99.32 -> 110.38 tok/s (+11.13%).
  • HIP and MUSA compilation were not run locally; both remain on the guarded scalar fallback.

@khosravipasha khosravipasha left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good, I was thinknig of grabbing the changes from main later but this branch probably hard to rebase in future so good to merge now.

Do we need to test more on the accuray side?

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.

Pull request overview

This PR optimizes CUDA Q1_0 unpacking by replacing scalar per-bit expansion with a __byte_perm-based helper, and reuses that helper in both the MMVQ (vecdot) and MMQ (tiling/prefill) paths while keeping scalar fallbacks for HIP/MUSA.

Changes:

  • Add unpack_q1_0_bytes() to expand 16 packed Q1_0 bits into 4x dp4a-ready int32s via __byte_perm (CUDA) or scalar expansion (HIP/MUSA).
  • Update vec_dot_q1_0_q8_1 to use the new helper and switch to a signed (+1/-1) dot product formulation.
  • Update load_tiles_q1_0 to populate the existing MMQ shared-memory layout using the same helper.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
ggml/src/ggml-cuda/vecdotq.cuh Introduces and uses unpack_q1_0_bytes() to speed up Q1_0 extraction in MMVQ vecdot.
ggml/src/ggml-cuda/mmq.cuh Reuses the same unpack helper when loading Q1_0 tiles for MMQ kernels.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ggml/src/ggml-cuda/vecdotq.cuh Outdated
Comment on lines +678 to +681
static __device__ __forceinline__ int4 unpack_q1_0_bytes(const int16_t q) {
#if !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA)
const int n0 = __byte_perm(0x11100100, 0x11100100, q >> 0);
const int n1 = __byte_perm(0x11100100, 0x11100100, q >> 2);
Comment on lines +713 to 715
const float d1 = bq1_0->d;
const int16_t * qs = (const int16_t *) bq1_0->qs + iqs * 2;

Comment on lines 352 to 354
const block_q1_0 * bxi = (const block_q1_0 *) x + kbx0 + i*stride + kbx;
const int qs_offset = 4*kqsx;
const int qs0 = bxi->qs[qs_offset + 0] | (bxi->qs[qs_offset + 1] << 8) |
(bxi->qs[qs_offset + 2] << 16) | (bxi->qs[qs_offset + 3] << 24);

int unpacked_bytes[8];
#pragma unroll
for (int j = 0; j < 8; ++j) {
const int shift = j * 4;
const int bits4 = (qs0 >> shift) & 0x0F;
const int b0 = (bits4 & 0x01) ? 1 : -1;
const int b1 = (bits4 & 0x02) ? 1 : -1;
const int b2 = (bits4 & 0x04) ? 1 : -1;
const int b3 = (bits4 & 0x08) ? 1 : -1;
unpacked_bytes[j] = (b0 & 0xFF) | ((b1 & 0xFF) << 8) | ((b2 & 0xFF) << 16) | ((b3 & 0xFF) << 24);
}
const int16_t * qxi = (const int16_t *) bxi->qs + kqsx * 2;

unpack_q1_0_bytes() built the __byte_perm selectors by right-shifting a
signed int16_t, so a packed halfword with the top bit set sign-extends
through the shift and corrupts the selector nibbles. Take the packed word
as uint16_t (widened through uint32_t before the shift) at the helper and
at both the MMVQ and MMQ call sites.
@khosravipasha

Copy link
Copy Markdown
Collaborator

This was also merged in llama.cpp for Q1_0, tested it there myself and was safe so should be good to mrege.
ggml-org#25628

@khosravipasha

Copy link
Copy Markdown
Collaborator

@bri-prism the ones is llama.cpp does not have your changes on top though, are they importatn enough to send a follow up to llama.cpp?
e6fdfcc

@khosravipasha
khosravipasha merged commit 41e362d into prism Jul 17, 2026
6 of 22 checks passed
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.

3 participants