ggml-metal: add chunked SSD MMA for Mamba-2 prefill optimization - #26647
Conversation
gabe-l-hart
left a comment
There was a problem hiding this comment.
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.
|
|
||
| GGML_ASSERT(d_state <= ggml_metal_pipeline_max_theads_per_threadgroup(pipeline)); | ||
| const int64_t mma_tokens = n_seq_tokens / CHUNK * CHUNK; |
There was a problem hiding this comment.
Does this need parens? I always forget my exact order of operations in c++. Might be good to add them for clarity.
There was a problem hiding this comment.
Added parenthesis for clarity and a comment as well
| const int64_t mma_tokens = n_seq_tokens / CHUNK * CHUNK; | ||
| const bool use_mma = | ||
| mma_tokens > 0 && | ||
| ne30 == 1 && |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
| @@ -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 | |||
There was a problem hiding this comment.
Let's add comments on these explaining the acronyms and clarifying that they're Apple device constants (ideally with some citation)
|
|
||
| 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); |
There was a problem hiding this comment.
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, | |||
There was a problem hiding this comment.
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.
| // 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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| ushort sgitg[[simdgroup_index_in_threadgroup]], | ||
| ushort tiisg[[thread_index_in_simdgroup]]) { | ||
| constexpr short CS = OP_SSM_SCAN_SSD_CS; | ||
| constexpr short TC = 8; |
There was a problem hiding this comment.
Would be good to have a comment for each of these explaining the magic numbers and what they physically correspond to
There was a problem hiding this comment.
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 + |
There was a problem hiding this comment.
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).
|
@dpantaleoni Thanks for the contribution. Could you rebase this on the latest |
|
@ggerganov thank you for reviewing! I just rebased |
|
Here's some additional benchmarking on a m4 air 24 gb:
./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 99I 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.
So the new kernel was able to improve throughput over the baseline kernel by ~35% of the theoretical limit of improvement. |
|
@forforever73 thank you for the thorough review and requested changes! I believe they are all complete now and I also rebased |
|
@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. |
|
On the sequential path the function constant caused a regression, so I switched it to a template. And test on my M4 Max
|
…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>
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:
Ran on M4 Pro:
I found no change in Perplexity:
Additional information
Files changed:
Sources:
Requirements