Skip to content

ggml-cpu: remove the per-matmul global barrier in ggml_compute_forward_mul_mat - #38

Open
codspeed-hq[bot] wants to merge 1 commit into
masterfrom
codspeed-optim-remove-the-per-matmul-global-barrier-in-ggml-compu-1785318592407
Open

ggml-cpu: remove the per-matmul global barrier in ggml_compute_forward_mul_mat#38
codspeed-hq[bot] wants to merge 1 commit into
masterfrom
codspeed-optim-remove-the-per-matmul-global-barrier-in-ggml-compu-1785318592407

Conversation

@codspeed-hq

@codspeed-hq codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown

What

ggml_compute_forward_mul_mat() ran a global ggml_barrier() on every matmul node, even when there was nothing to synchronize. The barrier had two jobs:

  1. publish the src1 rows converted to vec_dot_type (only needed when a conversion actually happens), and
  2. make thread 0's re-initialization of the shared chunk counter (current_chunk = nth) visible before any thread steals a chunk.

For an F32 matmul (src1->type == vec_dot_type) no conversion happens at all: threads only read src0/src1 and write to disjoint parts of dst. The barrier existed purely for the counter reset - a full n-thread rendezvous per matmul, and a transformer layer contains 9 of them.

How

  • The barrier is moved inside the src1->type != vec_dot_type branch, so it only runs when converted rows really have to be published.
  • The counter reset is eliminated by giving matmul a dedicated counter (mul_mat_chunk) with the invariant "it is always nth when a matmul starts":
    • it is initialized once per graph (single-threaded, before the workers start), and
    • it is restored by the thread that draws the last ticket of the matmul. Starting from nth, a matmul hands out exactly nchunk tickets (nchunk - nth remaining chunks + one out-of-range ticket per thread, which is how each thread leaves the loop), so the thread receiving nth + nchunk - 1 is provably the last one to touch the counter and can reset it for the next matmul.
  • The pre-existing shared current_chunk counter is left untouched, so the other chunked ops (repack, sgemm, kleidiai, spacemit, ...) keep their current behaviour.

Dynamic chunk stealing (and therefore load balancing) is preserved - only the rendezvous disappears. The per-node barrier in ggml_graph_compute_thread() still separates consecutive nodes, so the restored counter is visible to every thread before the next matmul starts.

Correctness

  • test-backend-ops -b CPU -o MUL_MAT: 1177/1177 tests pass against the reference implementation (the CONV_2D set, which drives matmul through ggml_call_mul_mat, also passes: 1578/1578).
  • A multi-matmul graph (F32 x F32, Q8_0, Q4_K; 1/7/64 tokens; 2/4/8/13 threads; 3 consecutive graph executions with changing inputs so a skipped chunk would leave a stale value) produces results bit-identical to the single-threaded reference in all 36 configurations, with and without OpenMP.
  • A temporary assertion build additionally verified that the counter is back to n_threads after every graph execution (and that the assertion does fire if the reset is removed).
  • The same test suite under ThreadSanitizer (-DGGML_SANITIZE_THREAD=ON -DGGML_OPENMP=OFF) reports no warnings.
  • test-barrier, test-quantize-fns, test-rope, test-col2im-1d and test-opt all pass.

Measured effect

CodSpeed simulation on the macro benchmarks (same machine, base vs head):

Benchmark Base Head Change
decode_layer[f32] 275.1 ms 231.9 ms +18.65%
decode_deep[f32] 1,033.6 ms 906.9 ms +13.97%
decode_layer[q4_0], decode_layer[q4_k], decode_layer[q8_0], prompt_layer[f32], prompt_layer[q4_k] - - unchanged

The gain concentrates on graphs whose matmuls need no src1 conversion; quantized matmuls keep their (still necessary) publication barrier and are unchanged.

Note: the simulation instrument amplifies synchronization costs (libgomp spins much longer when threads are serialized under Valgrind), so the walltime numbers from the macro-runner job on this PR are the ones to trust for the real-world magnitude.

…d_mul_mat

The barrier at the start of every matmul only had to publish the src1 rows
converted to vec_dot_type and to make thread 0's reset of the shared chunk
counter visible. When no conversion is needed (src1->type == vec_dot_type)
threads only read src0/src1 and write to disjoint parts of dst, so nothing
has to be published.

Move the barrier inside the conversion branch and give matmul a dedicated
chunk counter that is always nth when a matmul starts: it is initialized
once per graph and restored by the thread drawing the last ticket of the
matmul (exactly nchunk tickets are handed out from nth, so nth + nchunk - 1
is provably the last one). Dynamic chunk stealing is preserved.

The shared current_chunk counter is left untouched for the other chunked ops.
@github-actions github-actions Bot added the ggml label Jul 29, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown
Author

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codspeed-optim-remove-the-per-matmul-global-barrier-in-ggml-compu-1785318592407 (5bb839b) with master (46819c9)

Open in CodSpeed

@codspeed-hq
codspeed-hq Bot marked this pull request as ready for review July 29, 2026 10:29
@codspeed-hq
codspeed-hq Bot requested a review from coco-speed July 29, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant