Skip to content

CUDA: fix MMQ out-of-bounds reads - #29953

Merged
IMbackK merged 1 commit into
ggml-org:masterfrom
JohannesGaessler:cuda-fix-mmq-src1-ncols-rounding
Oct 8, 2026
Merged

IMbackK merged 1 commit into
ggml-org:masterfrom
JohannesGaessler:cuda-fix-mmq-src1-ncols-rounding

Conversation

@JohannesGaessler

Copy link
Copy Markdown
Contributor

Fixup to #29941 . See comments starting at #27044 (comment) .

I was unable to reproduce further issues locally but looking at the way I implemented the utility function ggml_cuda_mmq_get_J_max it is definitely wrong on master in a way that is consistent with the reported issues. On master the number of columns that are being checked for viable configs is being rounded down for the allocation but rounded up when selecting the actual config to run.

The lowest-effort fix would be to modify ggml_cuda_mmq_get_J_max so that the rounding is consistent. However, the fact that the rounding can be inconsistent in the first place is a design flaw. So this PR instead moves the code for determining the optimal tile size forward and directly uses that to calculate how much padding is needed. This also plays nicer with recent efforts to modify the logic for selecting the optimal tile size for MoE models.

The way fallback is being calculated on master can in principle also be inconsistent so I deduplicated it as well.

Requirements

@JohannesGaessler
JohannesGaessler requested a review from a team as a code owner October 4, 2026 15:22
@github-actions github-actions Bot added ggml changes relating to the ggml tensor library for machine learning CUDA Related to the CUDA backend labels Oct 4, 2026
@hclsys

hclsys commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Checked on a GB10 (sm_121a, CUDA 13.0). src1_q8_1 in its own exact-size cudaMalloc (the pool alloc swapped for a cudaMalloc of the same size), compute-sanitizer memcheck, cases from the #27044 thread.

Plain memcheck, q4_0, 512 experts, 10 used, m=640, n=100: master 1194 errors (b=0) / 274 (b=1), this PR 0 / 0. n=508 and the q4_K shapes are 0 on both. MUL_MAT + MUL_MAT_ID on CUDA0: 2243/2243.

With --padding 65536 it's still not clean on some routings: reads up to 112 B past the end of src1_q8_1 (size 2896128, your formula), in mul_mat_q<q4_0,112>. The tile_y load loops in mul_mat_q_process_tile run to the next multiple of nthreads (J=112: 4032 ints -> 4096 with 256 threads) and mmq_get_nbytes_shared pads nbs_y for that, but the global padding is only J_bestsizeof(block_q8_1_mmq). With a fixed seed for the ids shuffle in test-backend-ops, 10 of 16 seeds hit it at n=100, and 0 of 16 with GGML_PAD(J_bestsizeof(block_q8_1_mmq), config.nthreads*sizeof(int)) as the padding.

ids_dst gets read up to J_best-1 ints past ne_get_rows too with exact-size allocs; padding it by J_best clears that.

@JohannesGaessler JohannesGaessler changed the title CUDA: fix MMQ ncols rounding direction for alloc CUDA: fix inconsistent MMQ ncols selection Oct 4, 2026
@JohannesGaessler

Copy link
Copy Markdown
Contributor Author

Thank you for checking. I agree with those fixes and pushed a corresponding patch, please re-test.

@hclsys

hclsys commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Re-tested 20c3140 on the GB10, same harness (src1_q8_1, ids_src1, ids_dst at your ne_get_rows + J_best - 1, and expert_bounds each in an exact-size cudaMalloc, memcheck with --padding 65536). The 8 cases from before are 0 errors on 3 seeds, and q4_0 n=100 is 0 on 16/16 fixed ids-shuffle seeds (5bd8b00 failed 10/16).

Also swept q4_0/q4_K/q6_K/q8_0 x n in {9, 33, 65, 129, 257, 400} x m in {512, 576} (128 experts, 8 used), 2 seeds: 96/96 clean. MUL_MAT + MUL_MAT_ID on CUDA0: 2291/2291.

@JohannesGaessler
JohannesGaessler force-pushed the cuda-fix-mmq-src1-ncols-rounding branch from 20c3140 to 3070d92 Compare October 4, 2026 18:54
@JohannesGaessler JohannesGaessler changed the title CUDA: fix inconsistent MMQ ncols selection CUDA: fix MMQ out-of-bounds reads Oct 4, 2026
@glennneuber

glennneuber commented Oct 5, 2026 •

Copy link
Copy Markdown

@JohannesGaessler , @hclsys thanks for your work. Much appreciated.

The over night agent came to the same conclusion as your corresponding patch.

Checked #29953 with your corresponding patch, all passing:

Hardware Result
RTX PRO 6000 (sm_120a) 190/190 pass
RTX 2080 Ti (sm_75) 18/18 pass
NVFP4, sm_120a all pass
Radeon 8060S (RDNA 3.5, gfx1151) 72/72 + 36/36 pass
CPU sweep, 10 architectures 0 shapes short

All GPU runs used exact-size or guard-page allocations

Since it is an MMQ padding issue, also tested on AMD ROCm platform (ggfx1151) and nvfp4 (sm_120->sma_120a).

A potential test is proposed in mmq-rules-check.cu:

How it works
The CPU check replays llama.cpp's tile selection against its real config tables, for every architecture, quant type, batch size and expert count, about 9.6 million shapes with no GPU. For each shape it tests whether the padding covers what the kernel actually reads, which is how it showed the published first #29953 short in 3.5 million shapes and the head short in none.

What it covers
It also checks every architecture, including the ones nobody here has hardware for. That prevents intermittent out-of-bounds reads that crash on users' machines and pass every normal GPU test.

Limitations
It checks the padding rules as modelled, not the compiled code. The guard mmq-tests.patch would cover the code on the respective platforms.
Still untested: MUSA, and AMD CDNA3/RDNA4/GCN hardware, which we only modelled.

Links:

Test cases: https://github.com/MaxusAI/ollama/blob/5273e495fef54972f5d265ee49699e3e39ff1e0d/docs/maxusai/tasks/mmq-tests.patch
Debug allocator: https://github.com/MaxusAI/ollama/blob/5273e495fef54972f5d265ee49699e3e39ff1e0d/docs/maxusai/tasks/mmq-debug-alloc.cuh
CPU padding sweep: https://github.com/MaxusAI/ollama/blob/5273e495fef54972f5d265ee49699e3e39ff1e0d/docs/maxusai/tasks/mmq-rules-check.cu
Write-up: https://github.com/MaxusAI/ollama/blob/5273e495fef54972f5d265ee49699e3e39ff1e0d/docs/maxusai/upstream-mmq-29953-material.md

Disclosure
All the linked code (patches, sweep, allocator, scripts) is AI-generated.

Hope this helps.

@manunicholasjacob

Copy link
Copy Markdown

I re-tested 3070d927f on an RTX 3050 Laptop (sm_86, CUDA 13.2) using an exact-size allocation pool and device-side bounds counters, with the sizes taken directly from the PR's allocations; compute-sanitizer isn't usable under WSL2 here. 3070d927f shows 0 B past both src1_q8_1 and ids_dst across all tested shapes, including the J=32 suite case where 5bd8b0013 over-read in 17/20 runs, and all 406 MMQ ids_dst launches where the earlier build over-read in 381/406. MUL_MAT_ID passes 938/938 and MUL_MAT passes 1304/1304, including with the diagnostics disabled. One additional observation is that the same loads still go past the logical end when measured against the old unpadded allocation sizes, but on 3070d927f they now land in the padding; I did not check whether those values are actually used.

RTX 3050 Laptop (sm_86), CUDA 13.2, exact-size pool + device-side bounds counters, max bytes past src1_q8_1 / ids_dst
| case                                               | master dd266785c      | #29953 5bd8b0013      | #29953 3070d927f |
| q4_0 n=100, b=0 and b=1 (J=112)                    | 2,268 / 436           | 0 / 432               | 0 / 0 |
| q4_0 n=508, b=0 and b=1 (J=128)                    | 0 / 480               | 0 / 476               | 0 / 0 |
| q4_K n=65, fallback (J=128)                        | 8,924 / 500           | 0 / 492               | 0 / 0 |
| q4_K n=100 (J=112)                                 | 2,124 / 432           | 0 / 428               | 0 / 0 |
| q4_K n=2040 (J=128)                                | 0 / 260               | 0 / 148               | 0 / 0 |
| q4_0 + mxfp4, 32 exp, 2 used, n=32 (J=32), 20 runs | 15/20 over, 364 / 120 | 17/20 over, 364 / 120 | 0/20 |
| MUL_MAT_ID suite, MMQ ids launches                 | 9,068 / 504 (381/406) | 364 / 500 (381/406)   | 0 / 0 (0/406) |
MUL_MAT_ID 938/938, MUL_MAT 1304/1304 on all three (also with diagnostics off on 3070d927f)

@JohannesGaessler JohannesGaessler added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Oct 8, 2026
@IMbackK
IMbackK merged commit fc9ce6b into ggml-org:master Oct 8, 2026
16 of 18 checks passed
GGML_UNUSED_VARS(type, J, fallback, prec_src1);
}

// FIXME all of the host functions are missing prec_src1, this can lead to inconsitent behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ynankani as a fixup to #24364 , can you please take care of this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure, I can take care of this.

feal87 added a commit to feal87/myllama.cpp that referenced this pull request Oct 8, 2026
Upstream brings CUDA top-k/argsort/mmq fixes (PR ggml-org#28713, ggml-org#29953, ggml-org#30147,
ggml-org#29453), a DFlash output-head fix (ggml-org#30111) and a UI fix (ggml-org#29668).

Conflicts and resolution:
- top-k.cu / argsort.cu: upstream reworked the top-k selection and the same
  radix path the fork added. Take upstream's shape-based dispatch and int64_t
  fixes, keep the fork workspace cap in the shared ggml_cuda_chunk_nrows
  (GGML_CUDA_ARGSORT_CHUNK_MB) and the fork benchmark-only GGML_CUDA_TOPK_IMPL
  selector used by bench-topk.py.
- mmq.cu: upstream PR ggml-org#29953 fixes the same mul_mat_id padding over-read the
  fork patched in 1386ec2, but sizes the padding from the chosen J tile in
  both branches. Take upstream's version.
- test-backend-ops.cpp: keep the fork GGML_TOPK_BENCH shapes and add upstream's
  chunk-spanning cases.

Assisted-by: pi (deepseek-flash)
mclrcha added a commit to mclrcha/llama.cpp that referenced this pull request Oct 10, 2026
…-10-10)

Conflicts:
- ggml-backend.cpp: upstream moved input copies to ggml_backend_sched_copy_input with host weights last (ggml-org#29943);
  the GGML_SCHED_BATCH_INPUTS fast path now runs in the first loop before falling back to it.
- ggml-cuda/fattn-common.cuh: launch_fattn takes upstream async_kv_preload, then our parallel_blocks_fixed
  (GQA-6 tile launcher updated).
- ggml-cuda/fattn-mma-f16.cuh: upstream swizzle refactor (ggml-org#29612); its helpers take ncols2 like the rest of
  the fork's config getters.
- ggml-cuda/gated_delta_net.cu: keep the fork's kernel and launch geometry (its columns_per_block template
  argument is not upstream's cols_per_warp, ggml-org#30087).
- ggml-cuda/mmq.cu: upstream per-tile src1 padding (ggml-org#29953); ggml_cuda_mul_mat_q_src1_nbytes and the MMQ input
  cache use the worst-case padding so producers and projections with other tiles can share the buffer.
- ggml-cuda/mmq-vec-dot.cuh: fork vec_dot kernels pass GGML_PREC_Q8 to the config helpers (ggml-org#30168).
- ggml-cuda/mmvf.cu/.cuh: keep both includes and the HIP declarations, upstream warp_size argument.
- ggml-cuda/rope.cu: upstream grid-stride rms_norm+mul+rope (ggml-org#28175) with the fork's M-RoPE theta (nchannels
  instead of gridDim.y) and rope_store_cast.
- ggml-cuda/top-k.cu: upstream shape-based selection (ggml-org#28713), RDNA4 tournament kept for few long rows.
- src/llama-batch.*: mixed token/embd batches (ggml-org#29622) copy embd rows per entry; other batches keep pointing at
  the ext rows.
- src/llama-context.cpp: expert copy callback also on the second MTP scheduler.
- src/llama-graph.cpp: build_rs keeps the GET_ROWS of the ubatch states (GDN fusions, deferred state) and gathers
  the extra states from the tail like upstream's custom getter path (ggml-org#29856).
asf0 pushed a commit to asf0/strix-llama.cpp that referenced this pull request Oct 12, 2026
The 39 upstream commits after halo-box/llama.cpp's final sync. Same rules as
the previous merge: strix's side wins where the two collide, upstream's
non-conflicting changes stay, and fixes are ported into strix's code.

- server: upstream's final ggml-org#26004 (context checkpoints across slot
  save/restore) replaces strix's earlier port of it, keeping strix's on-device
  restore flags; test_slot_save.py follows upstream.
- cuda: strix's top-k (HIP radix), gated delta net and argsort kernels kept
  whole; upstream's CCCL major-version guard fix (ggml-org#29453) applied to strix's
  argsort. MMQ: upstream's out-of-bounds fix and host-side tile choice
  (ggml-org#29953) with strix's RDNA3.5 tile overrides on top; strix's fused MMQ paths
  pad src1 for the largest tile instead of the removed get_J_max. strix's
  128-column rms_norm fast path and two mmvf calls updated to upstream's new
  signatures (ggml-org#28175, warp_size).
- vulkan: strix's flash_attn_cm2.comp and topk_nary_search.comp kept whole
  (upstream's sparse cm2 FA ggml-org#30003 stays out with the rest of sparse FA).
- llama-context: strix's MTP draft vocabulary next to upstream's MoE cache.
- graph: upstream's single recurrent-state gather (ggml-org#29856) is restored and
  combined with strix's in-place gated delta net: in-place capable consumers
  keep the full gather and get their state view next to the consumer whether
  or not the rows are in place, so in-place and gathered ubatches build the
  same graph; this fixes the realloc aborts under GGML_SCHED_DEBUG_REALLOC in
  both upstream's shared-seq test and strix's seq cp graph reuse test.
- ggml-backend: alloc_buffer_n planning no longer flushes on zero-size
  tensors, which left views of a tensor larger than max_size uninitialized
  (caught by strix's test_view_init_after_max_size_split).
- SYNC.md: this fork now syncs with ggml-org/llama.cpp directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thelittlefireman pushed a commit to thelittlefireman/llama.cpp that referenced this pull request Oct 12, 2026
aukarande added a commit to aukarande/llama.cpp that referenced this pull request Oct 13, 2026
Size the src1 padding from the tile width the kernel uses: for
MUL_MAT_ID with broadcast src1 (ne11 == 1) the old padding was 0
while the tile width follows ne12, so the last tile read past the
quantized src1 buffer. Also pad ids_dst for the unconditional loads.
Hand port of fc9ce6b without the prec_src1 changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
edwardyoon pushed a commit to edwardyoon/focus-llama that referenced this pull request Oct 14, 2026
smalinin added a commit to smalinin/llama.cpp that referenced this pull request Oct 14, 2026
Adapt upstream ggml-org#29953: pick J_best/nthreads on the host before buffer
allocation, pad src1_q8_1 to a full J tile rounded to the load chunk, pad
ids_dst to ne_get_rows + J_best - 1, drop ggml_cuda_mmq_get_J_max. Local
scatter/dedup, expert_bounds and precision selection are preserved.

Port the structural part of upstream ggml-org#30168 per user request: prec_src1 is
an explicit parameter through the MMQ template chain (Q8 at call sites, Q4
in FP4 functions); GGML_PREC_Q8/Q4 added to enum ggml_prec. Config tables
do not branch on it (the ggml-org#24364 prec policy is not ported), behavior and
kernel dispatch on NVIDIA are unchanged.

Verified: compute-sanitizer A/B (baseline reproduces OOB in dense, MoE and
ids_dst cases on Ada/Ampere, candidate is clean 18/18 on both GPUs),
test-backend-ops MUL_MAT/MUL_MAT_ID 2178/2178 on Ada and Ampere, GLM-5.3
IQ3_XXS model control with MTP gives byte-identical outputs.
edwardyoon pushed a commit to edwardyoon/focus-llama that referenced this pull request Oct 15, 2026
@BrewTestBot BrewTestBot mentioned this pull request Oct 15, 2026
1 task done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CUDA Related to the CUDA backend ggml changes relating to the ggml tensor library for machine learning merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants