Skip to content

gemma4: graph optimizations (massive gains on pp for E2B and E4B) - #31356

Merged
ggerganov merged 13 commits into
masterfrom
maxk/gemma-4-optimization
Nov 3, 2026
Merged

ggerganov merged 13 commits into
masterfrom
maxk/gemma-4-optimization

Conversation

@max-krasnyansky

@max-krasnyansky max-krasnyansky commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Overview

While optimizing ggml-hexagon we realized that gemma-4 E2B and E4B still runs compute for all the layers that do not contribute to the KV$. This PR fixes that.

Original work by @Meet91721 and @huangwei858

Drop unused prompt tokens before KV-shared layers. Since the upper layers (!hparams.has_kv(il)) reuse earlier layers' KV cache and write nothing to the cache, we prune non- output token rows (inpL, inp_pos, kq_mask, and inp_this_layer) right before the first KV-shared layer instead of waiting until the last layer.
During prefill, those layers become a no-op on intermediate ubatches (n_outputs == 0) and only run 1 token row (n_outputs == 1) on the final ubatch.

The layer trimming is support only for the unified-KV.

Simplify PLE (per-layer embedding) projection.
Fold the two PLE scaling constants into the RMSNorm eps, the 256-element norm weight, and tok_embd_scale, and slice the permuted 3D PLE tensor directly using its strides instead of making a contiguous copy (ggml_cont).

Additional information

The gains are quite huge for the E2B, a bit less impressive for the E4B and none for 26B (all layers feed KV$).
I tested all the common use-cases with this models: text-to-text, image-to-text, audio-to-text and the outputs are identical to the master branch.
Also tested 16K and 32K prompts with Needle in a Haystack content and the outputs are good.

Here are some perf numbers vs master with the official Google QAT Q4_0 models.

## M4 Pro (Metal)

| model            |      size |  params | backend    | threads |    test | master     t/s | this PR       t/s |
| ---------------- | --------: | ------: | ---------- | ------: | ------: | -------------: | ----------------: |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | MTL,BLAS   |      10 |   pp512 | 1613.78 ± 0.79 |    4603.18 ± 4.99 |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | MTL,BLAS   |      10 |  pp2048 | 1535.38 ± 1.01 |   4451.68 ± 40.58 |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | MTL,BLAS   |      10 |  pp8192 | 1323.11 ± 0.89 |    3748.03 ± 4.43 |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | MTL,BLAS   |      10 |   tg128 |  109.65 ± 1.86 |     110.98 ± 0.55 |
| ---------------- | --------: | ------: | ---------- | ------: | ------: | -------------: | ----------------: |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | MTL,BLAS   |      10 |   pp512 |  805.91 ± 1.06 |   1318.62 ± 23.13 |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | MTL,BLAS   |      10 |  pp2048 |  785.84 ± 0.22 |   1306.18 ± 10.30 |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | MTL,BLAS   |      10 |  pp8192 |  722.91 ± 4.20 |    1213.38 ± 5.43 |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | MTL,BLAS   |      10 |   tg128 |   63.39 ± 0.22 |      63.62 ± 0.21 |

## M4 Pro (CPU)

| model            |      size |  params | backend    | threads | dev   |    test | master     t/s | this PR    t/s |
| ---------------- | --------: | ------: | ---------- | ------: | ----- | ------: | -------------: | -------------: |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | MTL,BLAS   |      10 | none  |   pp512 |  629.04 ± 2.09 | 1811.26 ± 4.54 |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | MTL,BLAS   |      10 | none  |  pp2048 |  538.81 ± 4.61 | 1517.79 ± 2.51 |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | MTL,BLAS   |      10 | none  |   tg128 |  113.95 ± 2.64 |  115.88 ± 0.41 |
| ---------------- | --------: | ------: | ---------- | ------: | ----- | ------: | -------------: | -------------: |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | MTL,BLAS   |      10 | none  |   pp512 |  326.06 ± 1.35 |  543.23 ± 0.89 |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | MTL,BLAS   |      10 | none  |  pp2048 |  294.85 ± 3.40 |  489.73 ± 7.55 |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | MTL,BLAS   |      10 | none  |   tg128 |   65.10 ± 0.13 |   65.24 ± 0.12 |


## S26+ (Snapdragon)

| model            |      size |  params | backend    | ngl | threads | n_ubatch | dev  |    test | master       t/s |  this PR       t/s |
| ---------------- | --------: | ------: | ---------- | --: | ------: | -------: | ---- | ------: | ---------------: | -----------------: |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | OpenCL,HTP |  -1 |       6 |     1024 | HTP0 |   pp512 |  2303.07 ± 13.23 |    5099.01 ± 60.77 |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | OpenCL,HTP |  -1 |       6 |     1024 | HTP0 |  pp2048 |  2266.44 ± 13.04 |    5591.61 ± 10.95 |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | OpenCL,HTP |  -1 |       6 |     1024 | HTP0 |  pp8192 |  1896.88 ± 56.42 |    4686.80 ± 68.26 |
| gemma4 E2B Q4_0  |  3.10 GiB |  4.63 B | OpenCL,HTP |  -1 |       6 |     1024 | HTP0 |   tg128 |     36.63 ± 0.10 |       36.84 ± 0.09 |
| ---------------- | --------: | ------: | ---------- | --: | ------: | -------: | ---- | ------: | ---------------: | -----------------: |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | OpenCL,HTP |  -1 |       6 |     1024 | HTP0 |   pp512 |  1259.42 ± 48.45 |    1994.65 ± 60.95 |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | OpenCL,HTP |  -1 |       6 |     1024 | HTP0 |  pp2048 |   957.19 ± 15.37 |    1614.33 ± 43.40 |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | OpenCL,HTP |  -1 |       6 |     1024 | HTP0 |  pp8192 |   893.70 ± 14.15 |    1514.63 ± 17.36 |
| gemma4 E4B Q4_0  |  4.79 GiB |  7.46 B | OpenCL,HTP |  -1 |       6 |     1024 | HTP0 |   tg128 |     19.73 ± 0.13 |       19.53 ± 0.17 |

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes, Anti.G to double check the math/logic, for writing custom test harness and code reviews.

@max-krasnyansky
max-krasnyansky requested a review from a team as a code owner October 29, 2026 07:16
@max-krasnyansky

Copy link
Copy Markdown
Member Author

@ngxson @ggerganov
This basically resolves the original TODOs.
Let me know how this looks, and feel free to update directly.

@github-actions github-actions Bot added the model Model specific label Oct 29, 2026
Comment thread src/models/gemma4.cpp
Comment on lines +192 to +198
// KV-shared layers do not write to the KV cache, so prune non-output token rows before the first KV-shared layer
bool prune_shared = crop_before_nextn(inp_out_ids) && n_outputs < n_tokens && cparams.causal_attn &&
hparams.n_layer_kv_from_start > 0 && hparams.n_layer_kv_from_start < n_layer &&
!cparams.embeddings && inp_attn->get_kq_mask()->ne[3] == 1;
for (int il = (int) hparams.n_layer_kv_from_start; prune_shared && il <= n_layer; ++il) {
prune_shared = !cparams.embeddings_layer_inp[il];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When the MTP is enabled, there will be no benefit because we still need to extract the full embeddings.

Also, this will create branches in the compute graph when using split KV cache. Can we avoid the inp_attn->get_kq_mask()->ne[3] == 1; condition somehow?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah. That's one of the use-cases I missed.
Will play with that and see if we can avoid branches.

@CISC

CISC commented Oct 29, 2026

Copy link
Copy Markdown
Member

I'm a bit surprised this hasn't been PRed yet. I did the search in the PRs and Issues and can't seem to find anything similar.

#29189

@max-krasnyansky

Copy link
Copy Markdown
Member Author

I'm a bit surprised this hasn't been PRed yet. I did the search in the PRs and Issues and can't seem to find anything similar.

#29189

Oops. Clearly my coding skills are a bit better than my searching skills :). Not sure how I missed that.
I'll loop the original folks into the iterations.

@max-krasnyansky

Copy link
Copy Markdown
Member Author

@ggerganov the MTP issue should be resolved.
I tried to minimize the changes and to make sure the other speculative paths are not affected.

@am17an can you please check it out.

Here are some numbers on S26+

MTP

./scripts/snapdragon/run.py --target adb --devices 'HTP0' --hex-hostbuf=1 -- bin/llama-cli -m ../gguf/gemma-4-E2B_q4_0-it.gguf -md ../gguf/gemma-4-E2B-it-assistant.Q8_0.gguf --spec-type draft-mtp --spec-draft-n-max 2 -f ../sample_prompt_4096.txt --jinja -st --ctx-size $((32 * 1024)) --ubatch-size 1024 -n 128
...
[ Prompt: 5171.5 t/s | Generation: 56.2 t/s ]

No-MTP

./scripts/snapdragon/run.py --target adb --devices 'HTP0' --hex-hostbuf=1 -- bin/llama-cli -m ../gguf/gemma-4-E2B_q4_0-it.gguf -f ../sample_prompt_4096.txt --jinja -st --ctx-size $((32 * 1024)) --ubatch-size 1024 -n 128
...
[ Prompt: 5345.8 t/s | Generation: 34.7 t/s ]

And the M4

MTP

/build-macos/bin/llama-cli -m ../gguf/gemma-4-E2B_q4_0-it.gguf -md ../gguf/gemma-4-E2B-it-assistant.Q8_0.gguf --spec-type draft-mtp --spec-draft-n-max 2 -f ../sample_prompt_4096.txt --jinja -st --ctx-size $((32 * 1024)) --ubatch-size 1024 -n 128
...
[ Prompt: 4182.1 t/s | Generation: 127.7 t/s ]

No-MTP

./build-macos/bin/llama-cli -m ../gguf/gemma-4-E2B_q4_0-it.gguf -f ../sample_prompt_4096.txt --jinja -st --ctx-size $((32 * 1024)) --ubatch-size 1024 -n 128
...
[ Prompt: 3709.9 t/s | Generation: 104.7 t/s ]

Working on the split-KV issue next

@max-krasnyansky

Copy link
Copy Markdown
Member Author

@ggerganov @am17an @ngxson
I experimented quite a bit with trying to make pruning to work with split-KV but it seems a little tricky and needs a big surgery.
For now I just made it work cleanly with the unified-KV which covers a good chunk of the usecases.
Perhaps, we can take another pass at it later (another PR) to see if we can make it work with split-KV.
Please see the summary of the combos in the server test below.

Basically, we get full pruning gains for E2B and E4B with and without MTP with the unified-kv (no graph branches/reallocs). With split-KV we disable the pruning.

Please let me know how this looks. Feel free to push updates directly if you want to tweak something.
I'd be happy to re-test & followup.

Here is a little test report on M4 Pro with Metal backend (tested hexagon as well)

Tested short prompts and my usual ~16K sample prompt (13,789 tokens) with llama-cli with and without MTP:

## No MTP (llama-cli -f context_16k_test.txt -n 16 --single-turn -v)

  I sched_reserve: graph: nodes = 1524, splits = 2, input objects = 5, input tensors = 14
  I slot print_timing: id  0 | task 0 | prompt eval time =    4381.65 ms / 13789 tokens (    0.32 ms per token,  3146.98 tokens per second)
  I slot print_timing: id  0 | task 0 |        eval time =     170.00 ms /    16 tokens (   10.62 ms per token,    94.12 tokens per second)
  I slot print_timing: id  0 | task 0 |    graphs reused =         15
  I slot print_timing: id  0 | task 0 |   graph reallocs =          0    /     0        (same size) / (changed size)

## With MTP (--spec-type draft-mtp -md gemma-4-E2B-it-assist-Q4_0.gguf)

  I sched_reserve: graph: nodes = 1524, splits = 2, input objects = 5, input tensors = 14
  I slot print_timing: id  0 | task 0 | prompt eval time =    4467.19 ms / 13789 tokens (    0.32 ms per token,  3086.73 tokens per second)
  I slot print_timing: id  0 | task 0 |        eval time =     119.59 ms /    16 tokens (    7.97 ms per token,   125.43 tokens per second)
  I slot print_timing: id  0 | task 0 |    graphs reused =          3 
  I slot print_timing: id  0 | task 0 |   graph reallocs =          0    /     0        (same size) / (changed size)

No graph reallocs across all 27 prompt ubatches (n_tokens = 512, n_outputs = 0 for the first 26 ubatches and n_outputs = 1 on the final ubatch) and the transition into t.g

llama-server (with Metal) tests

Used Anti.G to generate a little test-server-harness to test all the combos with the server

 Config                  | kv_unified | MTP (dra… | sched_rese… | Short Prompt (pp / tg) |     16k Prompt (pp / tg)     | Concur 2-Slot (pp / tg) | graph realloc
------------------------ | ---------- | --------- | ----------- | ---------------------- | ---------------------------- | ----------------------- | -----------
 1. Unified KV, no MTP   |    true    |    No     |    1524     |   484.7 / 109.7 t/s    | 3261.9 /  94.4 t/s (4.22 s)  |    662.3 / 79.7 t/s     |    0 / 0
 2. Unified KV, with MTP |    true    |    Yes    |    1524     |   251.5 / 115.4 t/s    | 3264.4 / 166.4 t/s (4.22 s)  |    735.5 / 80.2 t/s     |    0 / 0
 3. Split KV, no MTP     |   false    |    No     |    1529     |   155.3 / 111.5 t/s    | 1175.2 /  94.0 t/s (11.72 s) |    338.7 / 77.5 t/s     |    0 / 0
 4. Split KV, with MTP   |   false    |    Yes    |    1529     |   213.3 / 115.8 t/s    | 1175.0 / 166.0 t/s (11.72 s) |    375.5 / 95.1 t/s     |    0 / 0

Zero graph reallocations (0 / 0) in all 4 configurations across short prompts, 16k multi-ubatch prompts, and concurrent multi-slot requests.

All the pruning gains (~2.8x) preserved with unified KV (3,262 t/s vs. 1,175 t/s on split KV).

@max-krasnyansky

Copy link
Copy Markdown
Member Author

@ggerganov @am17an sorry for bugging you guys. Just wanted to bump this in your feeds while it's still fresh.

imaami pushed a commit to imaami/llama.cpp that referenced this pull request Nov 2, 2026
Picked from ggml-org#31356 (open at the time of picking),
head df6b479, net diff against its merge base 792fcf4:

  75e417d Optimizing Gemma4 graph for prompt processing
  8485adb Not filtering the rows when they are unmasked
  4f70ec7 Cropping out the attention mask too.
  1ef40ac gemma-4: improved pruning, mask cropping and PLE scaling
  f81e718 spec-mtp: Enable masked h_nextn extraction only when the draft context shares the target K
  99444d3 spec-mtp: make sure to not drop first draft round
  7dadb47 gemma4: enable pruning based on static cparams.kv_unified/n_seq_max=1
  df6b479 gemma4-mtp: simplify prunning logic and avoid graph branches/re-allocs

Co-authored-by: Max Krasnyansky <maxk@qti.qualcomm.com>

Merge notes: common/speculative.cpp keeps the fork's draft-context setup
(nextn embeddings only when n_max != 1), moved below is_mem_shared like
the PR's target-context call.

Assisted-by: Claude (Opus 5.5), conflict resolution
imaami added a commit to imaami/llama.cpp that referenced this pull request Nov 2, 2026
ggml-org#31356 (picked above) returns the permuted per-layer
input tensor without ggml_cont(). Each layer then reads a strided 2D slice
whose byte span covers nearly the whole tensor. When the layer runs on a
different backend than the tensor (e.g. -sm layer over two GPUs, or the
projection staying on the CPU input layer), the scheduler copies the
whole view, so each such layer transfers the inputs of all layers: about
n_layer times the traffic and a compute buffer about n_layer times larger
for these copies. Unified-memory backends, where the PR was measured,
don't copy and so don't see it.

Restore the ggml_cont() that master has. The slice helper from the PR
works on the contiguous tensor unchanged, and the row pruning and PLE
scale folding are not affected.

Assisted-by: Claude (Opus 5.5)
@max-krasnyansky
max-krasnyansky force-pushed the maxk/gemma-4-optimization branch from df6b479 to 1ed6351 Compare November 2, 2026 22:48
@max-krasnyansky

Copy link
Copy Markdown
Member Author

I just realized that latest master broke this branch.
Updating ...

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.

changes here seem unrelated, should it be a follow-up?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Technically needed to make speculative-simple behave like spec-common in this case but I yeah the change is kind of hacky (I didn't like adding that include for sure). Reverting for now.

@max-krasnyansky

Copy link
Copy Markdown
Member Author

Fixed compiler errors and pruning gates after #29510

@max-krasnyansky

Copy link
Copy Markdown
Member Author

metal failures with GGML_METAL_DEVICES=1 ./build-macos/bin/test-llama-archs -s 1 are fixed in #31570.
If I cherry-pick that pr/commit that test passes on my M4 Pro.

@am17an am17an left a comment

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.

I think deepseek 4.1 would have a similar issue, will take a look.

@max-krasnyansky max-krasnyansky added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Nov 3, 2026
@max-krasnyansky

Copy link
Copy Markdown
Member Author

@ggerganov please review/merge when you get a chance

@ggerganov

Copy link
Copy Markdown
Member

Will take a look today

@ggerganov ggerganov self-assigned this Nov 3, 2026
@ggerganov

Copy link
Copy Markdown
Member

@max-krasnyansky Please rebase now that #31570 is merged.

@max-krasnyansky
max-krasnyansky force-pushed the maxk/gemma-4-optimization branch from b61ab2b to 7fff406 Compare November 3, 2026 05:59
@max-krasnyansky

Copy link
Copy Markdown
Member Author

#31558 changed the api again and broke this :)
@ggerganov if you have a quick fix please add otherwise I'll need to update tomorrow.

@ggerganov

Copy link
Copy Markdown
Member

I think need to directly use the raw without any ggml_cast.

Meet91721 and others added 12 commits November 3, 2026 10:41
TODO:
	- Currently complete microbatch needs to contain tokens which does not require the logits; need to optimize this further
	- There are a few more operations that are performed after the kv cache is calculated, need to safely optimize them.
Prune before the first KV-shared layer with the single-stream, causal, non-embedding safety gates.
Use cropped masks only during shared-layer attention.
Preserve requested layer-input embeddings in the KV prefix.
Remove the PLE contiguous materialization and fold equivalent PLE scaling into RMSNorm parameters/weights.
…t shares the target KV cache

This lets Gemma 4 prune unused prompt rows before its shared layers so that we keep the full perf P.P gains with MTP.

Avoid extracting target hidden states for output-free prompt chunks in that shared-MTP path.
When a batch has a selected final token, extract only that row unless the whole verification batch is selected.

Keep ordinary MTP and classic speculative decoding behavior unchanged: they still use unmasked h_nextn because the draft
context must catch up on all target rows.

speculative-simple needs an update to select the final prompt row only for shared-KV MTP,
so the optimization works there without changing other speculative modes.
And it looks like the server already selects its final prompt row for first-token sampling.
@ggerganov
ggerganov force-pushed the maxk/gemma-4-optimization branch from 219ada5 to 0971a7a Compare November 3, 2026 08:41
Comment thread src/models/gemma4.cpp
// KV-shared layers do not write to the KV cache, so prune non-output token rows before the first KV-shared layer
bool prune_shared = crop_before_nextn(inp_out_ids) &&
hparams.n_layer_kv_from_start > 0 && hparams.n_layer_kv_from_start < n_layer &&
!cparams.embeddings && cparams.n_seq_max == 1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@max-krasnyansky You mentioned that the approach works with unified KV cache, but here you actually have a stronger restriction: cparams.n_seq_max == 1. So for example, the default server config uses 4 sequences with unified KV cache and it does not benefit from the pruning because of this check.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@ggerganov
The earlier check was n_seq_max == 1 || unified_kv == true but it stopped working after #29510 got merged.
It probably worked earlier just because unified_kv was not used witn n_seq_max > 1, or perhaps I misread the code.

As things are right now it only works with the Single-seq configurations.
Are you OK merging with this restriction or should we try to make it work with Multi-seq first?
It seems pretty tricky :) and I'm not sure there is a good use-case for Multi-seq with these smaller models. It'd be very useful for 26B but all layers feed the KV$ in 26B so we can't prune anything.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's not very difficult to implement - I added a TODO in the code. If you prefer, you can merge it like this and resolve it later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok. I'd say let's merge to enable the default case, and I'll do another pass to resolve the TODO.
Approvals expired. You'd have to merge or approve :)

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.

Hi @max-krasnyansky ,
If you are not planning to implement that change in this PR, I would like to take it up. I was trying to understand a few changes that you made on top of my commits.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hi @max-krasnyansky , If you are not planning to implement that change in this PR, I would like to take it up. I was trying to understand a few changes that you made on top of my commits.

Go for it!
I think we're in a very good shape for the default use cases: single-seq with/without MTP.
Multi-seq support is what's missing. If you have cycles please give it a go.
Please tag me in the next PR just so that it shows up in my feed (thank you thank you).

@ggerganov
ggerganov merged commit 6cb3e67 into master Nov 3, 2026
2 checks passed
edwardyoon pushed a commit to edwardyoon/focus-llama that referenced this pull request Nov 3, 2026
…ml-org#31356)

* Optimizing Gemma4 graph for prompt processing

TODO:
	- Currently complete microbatch needs to contain tokens which does not require the logits; need to optimize this further
	- There are a few more operations that are performed after the kv cache is calculated, need to safely optimize them.

* Not filtering the rows when they are unmasked

* Cropping out the attention mask too.

* gemma-4: improved pruning, mask cropping and PLE scaling

Prune before the first KV-shared layer with the single-stream, causal, non-embedding safety gates.
Use cropped masks only during shared-layer attention.
Preserve requested layer-input embeddings in the KV prefix.
Remove the PLE contiguous materialization and fold equivalent PLE scaling into RMSNorm parameters/weights.

* spec-mtp: Enable masked h_nextn extraction only when the draft context shares the target KV cache

This lets Gemma 4 prune unused prompt rows before its shared layers so that we keep the full perf P.P gains with MTP.

Avoid extracting target hidden states for output-free prompt chunks in that shared-MTP path.
When a batch has a selected final token, extract only that row unless the whole verification batch is selected.

Keep ordinary MTP and classic speculative decoding behavior unchanged: they still use unmasked h_nextn because the draft
context must catch up on all target rows.

speculative-simple needs an update to select the final prompt row only for shared-KV MTP,
so the optimization works there without changing other speculative modes.
And it looks like the server already selects its final prompt row for first-token sampling.

* spec-mtp: make sure to not drop first draft round

* gemma4: enable pruning based on static cparams.kv_unified/n_seq_max=1

* gemma4-mtp: simplify prunning logic and avoid graph branches/re-allocs

* gemma4: update pruning gates to use only n_seq_max==1, kv_unified can now be used for multi-seq

* spec-simple: revert spec-simple changes for now

* gemma4: remove causal_attn from pruning gate (last source of reallocs)

* cont : adapt to llm_attn_mask changes

* cont : add TODO

---------

Co-authored-by: Meet Amin <meetamin378@gmail.com>
Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
iki007 added a commit to iki007/llama.cpp that referenced this pull request Nov 4, 2026
…a61c46)

The -X theirs replay of 190 commits onto master 162e2bb dropped upstream's side where both changed a hunk.
This commit sets the 9 files that differ to the one-time merge resolution (73fa61c46), so the tip's tree equals it:
ggml-sycl/fattn-mkl.cpp and fattn-onednn.cpp (ours: upstream's ggml-org#30761 fixes the same strides and per-sequence
offsets as 8e35c17 / a4cc5c9; the replay applied both offsets, ib * nb3 on top of K_batch), ggml-sycl/dmmv.cpp
(upstream's templated launchers, ggml-org#31325, without the three reorder launchers they replace), ggml-sycl/ggml-sycl.cpp
(upstream's comm_exchange pointer, ggml-org#30956, kept next to our host exchange, which returns first for one context),
ggml-sycl/CMakeLists.txt (upstream's pkg-config lookup, ggml-org#31076, our include line in its fallback),
common/speculative.cpp (upstream's DFlash device features, ggml-org#31506, next to our pos_inject; the MTP nextn setters
after is_mem_shared, ggml-org#31356), src/llama-context.cpp (ggml-org#31506's wait before our ubatch loop), src/models/dflash.cpp,
tests/test-backend-ops.cpp.

Assisted-by: Claude Opus 5.5
@angt

angt commented Nov 5, 2026

Copy link
Copy Markdown
Member

Nice!

image

@max-krasnyansky

Copy link
Copy Markdown
Member Author

Nice!

image

Yep. The gains are quite awesome.
A bit of a bummer that the 26B stays the same since we can't prune anything (ie all layers feed KV during prompt) but the E2B and E4B are 2-3X gains.

@am17an

am17an commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

@angt what is this dashboard?

@angt

angt commented Nov 5, 2026

Copy link
Copy Markdown
Member

@am17an a small dashboard I maintain to track ROCm perf: https://huggingface.co/spaces/llama-benches/amd

wanghqc pushed a commit to qualcomm/llama.cpp that referenced this pull request Nov 5, 2026
…ml-org#31356)

* Optimizing Gemma4 graph for prompt processing

TODO:
	- Currently complete microbatch needs to contain tokens which does not require the logits; need to optimize this further
	- There are a few more operations that are performed after the kv cache is calculated, need to safely optimize them.

* Not filtering the rows when they are unmasked

* Cropping out the attention mask too.

* gemma-4: improved pruning, mask cropping and PLE scaling

Prune before the first KV-shared layer with the single-stream, causal, non-embedding safety gates.
Use cropped masks only during shared-layer attention.
Preserve requested layer-input embeddings in the KV prefix.
Remove the PLE contiguous materialization and fold equivalent PLE scaling into RMSNorm parameters/weights.

* spec-mtp: Enable masked h_nextn extraction only when the draft context shares the target KV cache

This lets Gemma 4 prune unused prompt rows before its shared layers so that we keep the full perf P.P gains with MTP.

Avoid extracting target hidden states for output-free prompt chunks in that shared-MTP path.
When a batch has a selected final token, extract only that row unless the whole verification batch is selected.

Keep ordinary MTP and classic speculative decoding behavior unchanged: they still use unmasked h_nextn because the draft
context must catch up on all target rows.

speculative-simple needs an update to select the final prompt row only for shared-KV MTP,
so the optimization works there without changing other speculative modes.
And it looks like the server already selects its final prompt row for first-token sampling.

* spec-mtp: make sure to not drop first draft round

* gemma4: enable pruning based on static cparams.kv_unified/n_seq_max=1

* gemma4-mtp: simplify prunning logic and avoid graph branches/re-allocs

* gemma4: update pruning gates to use only n_seq_max==1, kv_unified can now be used for multi-seq

* spec-simple: revert spec-simple changes for now

* gemma4: remove causal_attn from pruning gate (last source of reallocs)

* cont : adapt to llm_attn_mask changes

* cont : add TODO

---------

Co-authored-by: Meet Amin <meetamin378@gmail.com>
Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>

(cherry picked from commit 6cb3e67; the shared-layer mask
crop uses self_kq_mask_cnv / self_kq_mask_swa_cnv, since this branch predates the llm_attn_mask
change of ggml-org#29510)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

examples merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. model Model specific

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants