Skip to content

ggml-metal: add chunked SSD MMA for Mamba-2 prefill optimization - #26647

Merged
ggerganov merged 15 commits into
ggml-org:masterfrom
dpantaleoni:ssd-metal
Aug 26, 2026
Merged

ggml-metal: add chunked SSD MMA for Mamba-2 prefill optimization#26647
ggerganov merged 15 commits into
ggml-org:masterfrom
dpantaleoni:ssd-metal

Conversation

@dpantaleoni

Copy link
Copy Markdown
Contributor

Overview

Similar to BLSharda's PR, this PR adds a chunked SSD matmul path using Mamba-2 style prefill, but for Metal devices. For token sequences that fit 64 token chunks, parallel simdgroup matmuls are used in place of sequential, per-token scans. Any remaining tokens that do not fit in the 64 token chunks fallback to using sequential scan.

Performance gains are best at batched, larger prefills, see the benchmark results below:

Prompt tokens Parallel prompts Baseline (t/s) MMA (t/s) Δ
1024 1 660.03 693.80 +5.12%
1024 4 1010.01 1058.19 +4.77%
1024 8 1087.13 1137.92 +4.67%
4096 1 1319.01 1445.44 +9.59%
4096 4 1613.06 1784.91 +10.65%
4096 8 1658.10 1826.80 +10.17%
8192 1 1609.57 1797.35 +11.67%
8192 4 1790.94 2005.24 +11.97%
8192 8 1819.37 2029.98 +11.58%

Ran on M4 Pro:

./build/bin/llama-batched-bench -m <granite-4.0-h-1b-Q8_0.gguf> -c 131072 -b 2048 -ub 2048 -npp 1024,4096,8192 -ntg 128 -npl 1,4,8 -ngl 99

I found no change in Perplexity:

Metric Baseline SSD Difference
Final PPL 9.1594 9.1593 -0.0001
PPL uncertainty ±0.06642 ±0.06642 0
./build/bin/llama-perplexity -m <granite-4.0-h-1b-Q8_0.gguf> -f <wikitext-2-raw> -ngl 99 -c 8192 -fa 1

Additional information

Files changed:

  • ggml/src/ggml-metal/ggml-metal-device.cpp and ggml/src/ggml-metal/ggml-metal-device.h : added new pipeline struct to use the new ssd kernel
  • ggml/src/ggml-metal/ggml-metal-impl.h : added constants for new kernel op's Chunk Size and Number of Simdgroups; added token offset tracking args to kernel args struct
  • ggml/src/ggml-metal/ggml-metal-ops.cpp : added gate to the new ssd path so gpus without simdgroup architecture remain on sequential scan; added dispatch for the new ssd path with sequential scan fallback and token offset tail for remaining tokens
  • ggml/src/ggml-metal/ggml-metal.metal : added the new kernel and updated the existing sequential scan kernel to be used for leftover tokens via token offset tracking
  • tests/test-backend-ops.cpp : added one test_ssm_scan test case for a single 64 token chunk with no tail.

Sources:

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes, AI was used to help draft, refine and optimize the new kernel pipeline based on my initial designs I derived from the sources mentioned in Additional Information above.

@github-actions github-actions Bot added testing Everything test related ggml changes relating to the ggml tensor library for machine learning Apple Metal https://en.wikipedia.org/wiki/Metal_(API) labels Aug 5, 2026

@gabe-l-hart gabe-l-hart 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.

This looks really good! I've got a number of questions around places where things might be brittle by device or where some opaque acronyms could use comments. I haven't thoroughly reviewed the kernel math itself yet, but I think it's safe to take it out of Draft as is.

Comment thread ggml/src/ggml-metal/ggml-metal-ops.cpp Outdated

GGML_ASSERT(d_state <= ggml_metal_pipeline_max_theads_per_threadgroup(pipeline));
const int64_t mma_tokens = n_seq_tokens / CHUNK * CHUNK;

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.

Does this need parens? I always forget my exact order of operations in c++. Might be good to add them for clarity.

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.

Added parenthesis for clarity and a comment as well

Comment thread ggml/src/ggml-metal/ggml-metal-ops.cpp Outdated
const int64_t mma_tokens = n_seq_tokens / CHUNK * CHUNK;
const bool use_mma =
mma_tokens > 0 &&
ne30 == 1 &&

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.

Do you know what this specific constraint is doing? I think this is the A matrix and I think this is saying that dim-0 must be unit, but I'm not sure if this is the batch dimension or what.

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.

For reference, here's the primary place where ssm_scan is called: https://github.com/ggml-org/llama.cpp/blob/master/src/models/mamba-base.cpp#L121

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.

Added comments for clarity! ne30 == 1 specifically checks that the first dimension of the A matrix is 1, since Mamba-2 is scalar decay per head, rather than Mamba-1's full state-space decay matrix

Comment thread ggml/src/ggml-metal/ggml-metal-impl.h Outdated
@@ -155,6 +155,9 @@
#define OP_SUM_ROWS_NUM_SUM_ROWS 10
#define OP_SUM_ROWS_NUM_MEAN 11

#define OP_SSM_SCAN_SSD_CS 64

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.

Let's add comments on these explaining the acronyms and clarifying that they're Apple device constants (ideally with some citation)

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.

Added

Comment thread ggml/src/ggml-metal/ggml-metal-ops.cpp Outdated

args.n_seq_tokens = n_seq_tokens - mma_tokens;
args.token_offset = mma_tokens;
dispatch(ggml_metal_library_get_pipeline_ssm_scan(lib, op), d_state, d_inner);

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.

Iiiinteresting. I think this is dispatching two underlying kernels for a single GGML op. That should be ok, but it's not what I've seen in most op implementations. I do see at least one more example in ggml_op_set, so just noting that this is a bit different.

@@ -1717,6 +1718,8 @@ int ggml_metal_op_ssm_scan(ggml_metal_op_t ctx, int idx) {
/*.n_head =*/ n_head,
/*.n_group =*/ n_group,
/*.n_seq_tokens =*/ n_seq_tokens,
/*.n_seq_tokens_total =*/ n_seq_tokens,

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.

I don't think this added struct size should have any significant impact, but worth double checking since this is what traverses the device boundary on dispatch.

Comment thread ggml/src/ggml-metal/ggml-metal.metal Outdated
// MMA fast path for the common Mamba-2 layout (head_dim=64). One threadgroup owns a complete
// (head, sequence) pair so the channel-independent C*B^T matrix is computed once per chunk and
// reused for all eight 8-channel output tiles.
kernel void kernel_ssm_scan_ssd_mma_f32(

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.

One question that always comes up for me: Is there any benefit to making this a template over dtype and supporting bf16, f16, and potentially lower precision types as well? The downside is that extra template expanded kernels take up bloat in the JIT compile for the metal kernels, but it's possible that this could give an added perf boost for lower-precision models. Worth testing at least.

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.

Looked into this and currently all ssm state Granite Hybrid models are allocated as f32 regardless of size, but it would be a great addition in the future.

Comment thread ggml/src/ggml-metal/ggml-metal.metal Outdated
ushort sgitg[[simdgroup_index_in_threadgroup]],
ushort tiisg[[thread_index_in_simdgroup]]) {
constexpr short CS = OP_SSM_SCAN_SSD_CS;
constexpr short TC = 8;

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.

Would be good to have a comment for each of these explaining the magic numbers and what they physically correspond to

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.

added comments for these as well

}

// acs/exp(acs)/state-decay vectors + dtX + SAM rows + two 8x8 tiles per simdgroup
res.smem = (3*OP_SSM_SCAN_SSD_CS +

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.

This is some classic kernel math magic! I'm assuming that these numbers are tied to concrete device constraints. It would be good to figure out if this can be tested across a range of Apple Silicon devices (I know M5 made some big changes).

@ggerganov ggerganov self-assigned this Aug 7, 2026
@dpantaleoni
dpantaleoni marked this pull request as ready for review August 7, 2026 22:02
@dpantaleoni
dpantaleoni requested review from a team and ggerganov as code owners August 7, 2026 22:02
@ggerganov

Copy link
Copy Markdown
Member

@dpantaleoni Thanks for the contribution. Could you rebase this on the latest master?

@dpantaleoni

Copy link
Copy Markdown
Contributor Author

@ggerganov thank you for reviewing! I just rebased

@dpantaleoni

dpantaleoni commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Here's some additional benchmarking on a m4 air 24 gb:

Prompt tokens Parallel prompts Baseline (t/s) MMA (t/s) Δ
1024 1 668.83 673.09 +0.64%
1024 4 651.41 752.51 +15.52%
1024 8 642.02 755.21 +17.63%
4096 1 630.43 719.84 +14.18%
4096 4 624.16 716.51 +14.80%
4096 8 611.04 705.75 +15.50%
8192 1 591.15 691.77 +17.02%
8192 4 588.31 683.29 +16.15%
8192 8 575.49 652.93 +13.46%
./build/bin/llama-batched-bench -m <granite-4.0-h-1b-Q8_0.gguf> -c 131072 -b 2048 -ub 2048 -npp 1024,4096,8192 -ntg 128 -npl 1,4,8 -ngl 99

I believe the inconsistency in throughput is due to throttling (because of the lack of cooling) on the device.

On this MacBook Air, I also initially ran the noop experiment mentioned in PR #16982 to find the theoretical limit of prefill throughput by removing all compute cost in the ssm scan kernel.

Prompt tokens Parallel prompts Baseline (t/s) MMA (t/s) Noop ceiling (t/s) Ceiling ratio Captured
1024 1 668.83 673.09 979.93 1.47x 1%
1024 4 651.41 752.51 966.58 1.48x 32%
1024 8 642.02 755.21 944.28 1.47x 37%
4096 1 630.43 719.84 922.60 1.46x 31%
4096 4 624.16 716.51 914.41 1.46x 32%
4096 8 611.04 705.75 886.46 1.45x 34%
8192 1 591.15 691.77 854.09 1.44x 38%
8192 4 588.31 683.29 829.82 1.41x 39%
8192 8 575.49 652.93 797.71 1.39x 35%

So the new kernel was able to improve throughput over the baseline kernel by ~35% of the theoretical limit of improvement.

Comment thread ggml/src/ggml-metal/ggml-metal-ops.cpp Outdated
Comment thread ggml/src/ggml-metal/ggml-metal.metal Outdated
Comment thread ggml/src/ggml-metal/ggml-metal-device.cpp Outdated
Comment thread ggml/src/ggml-metal/ggml-metal-ops.cpp Outdated
Comment thread ggml/src/ggml-metal/ggml-metal.metal Outdated
Comment thread ggml/src/ggml-metal/ggml-metal.metal Outdated
Comment thread ggml/src/ggml-metal/ggml-metal.metal Outdated
Comment thread ggml/src/ggml-metal/ggml-metal.metal Outdated
Comment thread ggml/src/ggml-metal/ggml-metal.metal Outdated
Comment thread ggml/src/ggml-metal/ggml-metal-impl.h Outdated
@dpantaleoni

Copy link
Copy Markdown
Contributor Author

@forforever73 thank you for the thorough review and requested changes! I believe they are all complete now and I also rebased

@dpantaleoni

Copy link
Copy Markdown
Contributor Author

@forforever73 @ggerganov Just a check in to see if I could get a review after making the requested changes. I rebased again to accommodate the ggml-metal.metal refactor.

@forforever73

Copy link
Copy Markdown
Contributor

On the sequential path the function constant caused a regression, so I switched it to a template.

And test on my M4 Max

Prompt tokens Parallel prompts Baseline PP (t/s) PR PP (t/s) Delta PP Baseline TG (t/s) PR TG (t/s) Delta TG
1024 1 2699.28 2805.40 +3.93% 120.91 121.48 +0.47%
1024 4 2766.71 2907.77 +5.10% 184.69 185.79 +0.60%
1024 8 2769.68 2925.28 +5.62% 201.46 201.48 +0.01%
4096 1 2690.70 2826.57 +5.05% 119.64 119.60 -0.03%
4096 4 2702.04 2838.20 +5.04% 182.77 183.20 +0.24%
4096 8 2702.52 2838.89 +5.05% 198.84 199.08 +0.12%
8192 1 2666.94 2798.30 +4.93% 119.03 118.84 -0.16%
8192 4 2670.34 2803.82 +5.00% 180.28 180.47 +0.11%
8192 8 2671.50 2804.47 +4.98% 195.89 196.01 +0.06%

@ggerganov ggerganov added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Aug 26, 2026
@ggerganov
ggerganov merged commit 11cd988 into ggml-org:master Aug 26, 2026
33 of 35 checks passed
ravel7524 pushed a commit to ravel7524/llama.cpp that referenced this pull request Aug 30, 2026
…l-org#26647)

* metal: WIP chunked SSD SSM_SCAN kernels for multi-token prefill

* metal: drop scalar SSD path; MMA + sequential tail

* drop WIP ssm scan test noise

* remove state_from_dst and rename CS and NSG constants

* remove unrelated  added whitespace padding

* added clarity to mma_tokens calculation

* added clarity to use_mma bool checks

* added comments to metal ssd op constants for clarity

* reserve K tokens for sequential kernel rollback snapshots

* reset concurrency between mma and seq tail

* remove print args no longer used

* fixed comment to no longer point to specific line

* add FC_SSM_SCAN so seq path skips token offlset unless it's mma tail

* added changes to new ssm.metal for rebase after ggml-metal.metal refactor

* specialize ssm_scan tail with a template instead of a function constant

---------

Co-authored-by: dpantaleoni <dominikpantaleoni@gmail.com>
Co-authored-by: forforever73 <690105611@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Apple Metal https://en.wikipedia.org/wiki/Metal_(API) 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. testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants