Skip to content

CUDA: add IQ1_M to MMQ - #304

Open
danielhanchen wants to merge 4 commits into
masterfrom
cuda-iq1m-mmq
Open

danielhanchen wants to merge 4 commits into
masterfrom
cuda-iq1m-mmq

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

IQ1_M has no MMQ kernel in the CUDA backend. MMVQ covers it, so decode is fine, but every batched IQ1_M matmul falls back to dequantize + cuBLAS. For MoE models (MUL_MAT_ID) that fallback is a per-expert loop and dominates prompt processing. This adds IQ1_M to mul_mat_q for both MUL_MAT and MUL_MAT_ID.

On unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF UD-IQ1_M, prompt processing on a B200 goes from about 2500 to 7700 t/s (3.1x at pp512). Decode is unchanged: n <= 8 still runs MMVQ.

Pin for the prebuilt: one change on top of upstream d80619397 (b11756), the base the nightly picks now.

Design

An IQ1_M weight is w = d * (2*s + 1) * (g + delta), with g in {-1, 0, 1} from the 11-bit grid index, delta = +-1/8 (one sign per 8 values), s a 3-bit sub-scale per 16 values and d the fp16 super-block scale. The per-8 delta is folded into the integer and the per-16 scale is divided by 8:

q        = 8*g +- 1           in {-9, -7, -1, 1, 7, 9}   (int8, exact)
scale_16 = d * (2*s + 1) / 8                             (fp32, exact)
w        = scale_16 * q                                  (identical to dequantize_row_iq1_m)

After the load the tile is "int8 values with one float scale per 16 values", the same format IQ2_XS, IQ2_S and Q3_K already use, so no new dot product is needed:

  • q8_1 layout MMQ_Q8_1_DS_LAYOUT_D4 (the delta is inside the int8, so no activation-sum term).
  • Tiles MMQ_DP4A_TXS_Q8_0_16 (DP4A) and the Q3_K SRAM layout (MMA).
  • vec_dot ggml_cuda_mmq_vec_dot_q8_0_16_q8_1_dp4a / _mma, unchanged. Each 16-value integer dot is bounded by 169127, so int32 cannot overflow.
  • New loader ggml_cuda_mmq_load_tiles_iq1_m, 8 threads per 256-value row.
  • Configs: the IQ2_XS rows are reused for IQ1_M in every mmq-config-*.cuh; on RDNA3 IQ1_M gets the same batch-size cap as IQ2_XS/IQ2_S.
  • Template instance + generate_cu_files.py entry. The IQ1_M entries sit next to IQ2_S, so the change does not touch the lines around IQ1_S.
  • test-backend-ops: IQ1_M and IQ1_S eval cases for MUL_MAT (n in {1, 2, 8, 9, 16, 64, 128, 512, 2048}, k in {256, 1024, 4096, 5376}, m = 256 and m = 129 for the bounds fallback) and MUL_MAT_ID (256 and 512 experts, top-9 and top-22, plus a broadcast case), and perf cases for 1-bit MoE up projections.

Correctness

test-backend-ops, CUDA vs CPU, B200 (sm_100):

build MUL_MAT MUL_MAT_ID iq1_m MUL_MAT / MUL_MAT_ID iq1_s MUL_MAT / MUL_MAT_ID
upstream master 1242be66f 2445/2445 931/931 49/49, 4/4 49/49, 4/4
master + this change 2543/2543 965/965 98/98, 21/21 98/98, 21/21
this pin (b11756 + change) 2543/2543 965/965 98/98, 21/21 98/98, 21/21

End to end, llama-perplexity --kl-divergence on wiki.test (c = 1024, 48 chunks), unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF UD-IQ1_M. The reference is the same file run on the CPU backend (CUDA_VISIBLE_DEVICES=, -ngl 0), so the KLD measures only how far each CUDA path is from the CPU result for the same weights:

CUDA path PPL mean KLD vs CPU 99% KLD same top p
before: dequantize + cuBLAS (master 1242be66f) 7.7214 +- 0.1273 0.008885 +- 0.000266 0.0540 95.22%
after: MMQ (this change) 7.7208 +- 0.1273 0.008588 +- 0.000179 0.0528 95.27%

CPU PPL on the same chunks: 7.7312 +- 0.1270. The MMQ path is as close to the CPU result as the old path (the KLD difference is inside the error bars), so output quality is unchanged.

Speed

llama-bench -p 512,2048 -n 128 -ub 512,2048 -b 2048 -r 3 -ngl 99, unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF UD-IQ1_M, one exclusive B200, interleaved base / head / base / head / base / head (3 rounds of 3 reps each). Base = upstream master 1242be66f, head = the same commit + this change (the MMQ files are identical between b11756 and that master). Values are the per-round means; the A/A spread is the base range over its 3 rounds.

test -ub base t/s head t/s base A/A spread speedup (median)
pp512 512 2356, 2506, 2609 7675, 7945, 7658 10.1% 3.06x
pp512 2048 2374, 2632, 2518 7895, 7039, 7820 10.2% 3.11x
pp2048 512 2480, 2499, 2436 8124, 6819, 8093 2.5% 3.26x
pp2048 2048 4403, 4798, 4628 11901, 11924, 12044 8.5% 2.58x
tg128 512 241, 248, 245 242, 165, 247 3.2% 0.99x
tg128 2048 239, 237, 238 238, 166, 230 0.8% 0.97x

tg128 is unchanged: decode stays on MMVQ (n <= 8), which this change does not touch. The one low head round (165 / 166 t/s) coincided with a CPU-heavy job on the same host; the other two head rounds match base.

A separate decode-only rerun on an idle exclusive B200 (-p 0 -n 128 -r 5, base / head interleaved, 4 rounds each): base 253.3, 252.7, 252.7, 253.5 t/s (median 253.0, A/A spread 0.3%), head 253.3, 251.8, 252.8, 252.4 t/s (median 252.6). The -0.2% difference is inside the A/A spread.

Pin set

llama_pinset.py sim with this pin appended to the current pr-set.json: merges cleanly and every pin stays intact (pin_contract) on b11756, b11757 and b11759. verify --cuda on b11756 and b11759: CPU gate, full CUDA build, test-chat, test-backend-ops MUL_MAT 2582/2582, MUL_MAT_ID 1027/1027, PAD 37/37, GET_ROWS 349/349, and all 13 feature checks on GPU.

Not tested

  • NVIDIA: only sm_100 (B200). The DP4A path was not run on Pascal, Volta, Turing, Ampere or Ada hardware.
  • HIP / MUSA: not built here. The RDNA2/3/3.5/4, CDNA and GCN config rows are copies of the IQ2_XS rows.
  • No tile tuning for IQ1_M; it reuses the IQ2_XS configs.

IQ1_M weights are w = d*(2*s+1)*(g + delta) with g in {-1,0,1}, a 3 bit
sub-scale s per 16 values and delta = +-1/8 per 8 values. The delta is folded
into the int8 tile values (8*g +- 1 in {-9,-7,-1,1,7,9}) and the per-16 scale
becomes d*(2*s+1)/8, which is exact. The tile then uses the same Q8_0_16 data
layout and vec_dot (DP4A and MMA) as IQ2_XS, with D4 q8_1 activations.

Adds test-backend-ops coverage for IQ1_M/IQ1_S MUL_MAT and MUL_MAT_ID with
many experts, and perf cases for 1-bit MoE up projections.
IQ1_M uses the IQ2_XS/IQ2_S tile layout, so its switch cases, loader,
template declaration and generator entry now sit beside IQ2_S. No functional
change.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T00:13:01.430566Z 64b2a7e PR opened
🔒 Security Review ✅ Completed 2026-10-21T00:12:22.331834Z 64b2a7e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64b2a7e7bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1315 to +1318
// IQ1_M: w = d * (2*s + 1) * (g + delta), g in {-1, 0, 1}, delta = +-1/8 per 8 values, s 3 bit per 16 values.
// The per-8 delta is folded into the int8 values: 8*(g + delta) = 8*g +- 1 in {-9, -7, -1, 1, 7, 9},
// and the per-16 scale becomes d * (2*s + 1) / 8. This is exact, the dot product then uses the
// same 16-value-scale data layout and vec_dot as IQ2_XS (Q8_0_16 / SRAM layout Q3_K).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Condense the hard-wrapped loader comment

This new four-line comment exceeds the repository's usual one-to-two-line limit and splits a sentence across lines 1317-1318, making the kernel rationale harder to scan; condense it to one or two comment lines without wrapping mid-sentence.

AGENTS.md reference: AGENTS.md:L77-L84

Useful? React with 👍 / 👎.

// row counts with and without the out-of-bounds fallback, and MoE shapes with many experts.
for (ggml_type type_a : {GGML_TYPE_IQ1_M, GGML_TYPE_IQ1_S}) {
for (int64_t k : {256, 1024, 4096, 5376}) {
for (int64_t n : {1, 2, 8, 9, 16, 64, 128, 512, 2048}) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exercise the intermediate MMQ J specializations

Despite the preceding claim that every MMQ J tile is covered, on the Ampere non-fallback path these batch sizes only select J=16, 64, or 128 once n<=8 stays on MMVQ. The newly enabled J=24, 32, 40, 48, 80, 96, and 112 configurations therefore receive no correctness coverage, so a tile-specific indexing or launch regression can pass this suite; include sizes such as 17, 25, 33, 41, 65, 81, and 97 that actually select those specializations.

Useful? React with 👍 / 👎.

On Turing and newer, MMQ for dense IQ1_M is slower than dequantization + cuBLAS
once the batch reaches 128 tokens (B200, k = 4096: 2.0x-2.3x slower at n = 512).
Keep MMQ for MoE (MUL_MAT_ID), where it is ~10x faster, and for dense batches below 128.
@danielhanchen

Copy link
Copy Markdown
Member Author

Pushed 33fdcde: dense IQ1_M at batch sizes of 128 and up goes back to dequantize + cuBLAS on Turing and newer; MoE (n_experts > 0) and smaller batches keep MMQ.

Routing every batched dense IQ1_M matmul through MMQ was slower than base at larger batches on a B200 (m=4096, n=512: 56 us on base, 128 us with MMQ). Medians of 3 interleaved rounds on an exclusive B200, k=4096, relative to base (A/A spread in brackets):

case before this commit after
dense n = 16-96 0.31-0.91 0.28-0.94 (0.97-1.04)
dense n = 128 1.14 / 1.40 0.97 / 0.99 (0.99-1.05)
dense n = 160-512 1.16-2.28 0.99-1.05 (0.98-1.05)
MoE, 128 experts, top-8, n >= 16 0.10-0.16 0.09-0.16

MMQ still wins at 96 tokens and loses at 128, so the cut is ne11 < 128, the same shape as the existing RDNA3 IQ1_M check in this function. test-backend-ops MUL_MAT 98/98 and MUL_MAT_ID 21/21 for iq1_m on the MMA path and on a forced DP4A build.

Not measured: other NVIDIA architectures (the crossover was taken on B200), AMD (unchanged by this commit), and end to end llama-bench. The pin in pr-set.json still names 64b2a7e and needs a bump to pick this up.

@danielhanchen
danielhanchen requested a review from CISC as a code owner October 25, 2026 21:50
@danielhanchen
danielhanchen changed the base branch from base/upstream-d80619397 to base/upstream-c49edf096 October 25, 2026 21:51
danielhanchen added a commit that referenced this pull request Oct 25, 2026
pr-set: repin #61, #264, #298 and #304 for the b11879 MMQ change
@danielhanchen
danielhanchen changed the base branch from base/upstream-c49edf096 to master October 25, 2026 23:32

This branch has not been deployed

No deployments
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