ggml-cpu: run the token-generation matmul without a global barrier - #49
Open
codspeed-hq[bot] wants to merge 1 commit into
Conversation
During token generation src1 is a single row, so converting it to vec_dot_type costs a small fraction of one matmul chunk. Instead of splitting that conversion across the threads and rendezvousing to publish it, every thread converts the row into its own slot of the work buffer and the row chunks are handed out statically (one chunk per thread), so no thread reads the shared chunk counter. This removes the ggml_barrier() from every matmul on that path. ggml_graph_plan() sizes the MUL_MAT scratch as n_tasks * row_size when ggml_nrows(src1) == 1. All other shapes keep the existing code path.
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
During token generation every matmul has a single
src1row (one token). For a quantizedsrc0that row still has to be converted tovec_dot_type(Q8_0/Q8_1/Q8_K/F16), and today that conversion is split across the threads and published with a globalggml_barrier()- one full rendezvous per matmul, and a transformer layer contains 9 of them. The same barrier also publishes thread 0's reset of the shared chunk counter.Converting one row is a tiny fraction of one matmul chunk (2048 floats vs. millions of weight elements), so this change makes every thread convert the whole row into its own slot of the work buffer instead:
ggml_graph_plan()sizes theMUL_MATscratch asn_tasks * row_sizewhenggml_nrows(src1) == 1(a few KB more),nchunk0 = nth). With a singlesrc1row all chunks carry the same work, so there is nothing for the dynamic chunk stealing to rebalance - and because no thread ever reads the shared counter, the counter reset (the barrier's second job) is not needed either.The barrier is therefore skipped entirely on this path. Every other shape (prompt processing,
lm_head, F32 weights,mul_mat_id) keeps the existing code path unchanged, and the tinyBLAS/GGML_LLAMAFILEand KleidiAI paths are unaffected - they set up and publish their own chunk counter.Correctness
The arithmetic is untouched: same kernels, same operand order; only who converts
src1and how the row chunks are handed out changes.Verified locally against a build of the unmodified tree with a harness that dumps the raw result buffers and compares them byte-for-byte: 450 cases byte-identical -
F32/F16/Q8_0/Q4_0/Q4_Kx 9 shapes (includingn = 1/2/3/5/16columns and batched/broadcast 3-D shapes) x 1/2/5/8/13 threads, each shape run both as a single matmul and as a chain of 3 matmuls executed 3 times with changing inputs (so a skipped or double-processed chunk, or a stale chunk counter leaking into the next matmul, would show up).test-backend-ops -o MUL_MAT,test-barrier,test-quantize-fnsandtest-ropealso pass.Measured (CodSpeed, CPU simulation, 8 threads)
Macro suite, base vs. head (overall impact +18%):
decode_deep[q4_k]decode_layer[q8_0]decode_layer[q4_0]decode_layer[q4_k]decode_layer[f32](control, unchanged path)prompt_layer[*],lm_head[*]The single-threaded micro suite (15 benchmarks, deterministic) is unchanged across the board - no regression on the prompt/matmul kernels.
Caveat
Most of the gain is removed synchronization. Under CPU simulation the threads are serialized by Valgrind, which makes the OpenMP runtime spin far longer than on real hardware, so the walltime numbers from the macro runner will be smaller than the ~20% above. What transfers to the macro runner is 9 fewer rendezvous per decoded token, minus the cost of converting one row
nthtimes instead of once (a few thousand instructions per matmul).