Skip to content

qwen4exp: sum the indexer heads by slices - #28023

Merged
ServeurpersoCom merged 2 commits into
ggml-org:masterfrom
ServeurpersoCom:qwen4exp-indexer-sum
Sep 1, 2026
Merged

qwen4exp: sum the indexer heads by slices#28023
ServeurpersoCom merged 2 commits into
ggml-org:masterfrom
ServeurpersoCom:qwen4exp-indexer-sum

Conversation

@ServeurpersoCom

@ServeurpersoCom ServeurpersoCom commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Overview

Split out of #27977 as requested, one PR per change. This one is prompt processing only, and it grows with context and ubatch size.

Summing the indexer heads went through a transpose and a reduction over a dimension of 4, which is about the worst shape for that kernel, and the transpose was copying a large surface twice for nothing. The heads sit next to each other in memory, so a short chain of adds over strided views does the same thing without moving anything.

Additional information

The head reduction went through a transpose and a sum_rows over ne[1], which left sum_rows with ne0 = 4, one block per row for a four element reduction, and the transpose copied the whole block by token surface twice on the way in.

The heads are adjacent on ne[1], so each one is a strided view and the sum is a short chain of adds.

RTX PRO 6000, Qwen3.8-Flash-Next UD-Q4_K_XL, fa on, 55k context, warm runs on top of #28011:

prompt processing 2170 -> 2366 t/s

Generation is unaffected. The removed work scales with n_blocks by n_tokens, so the gain grows with context and with ubatch size.

Requirements

@ServeurpersoCom
ServeurpersoCom requested a review from CISC as a code owner August 30, 2026 13:00
@github-actions github-actions Bot added the model Model specific label Aug 30, 2026
The head reduction went through a transpose and a sum_rows over ne[1],
which left sum_rows with ne0 = 4, one block per row for a four element
reduction, and the transpose copied the whole block by token surface
twice on the way in.

The heads are adjacent on ne[1], so each one is a strided view and the
sum is a short chain of adds.

RTX PRO 6000, Qwen3.8-Flash-Next UD-Q4_K_XL, fa on, 55k context, warm
runs on top of ggml-org#28011:

  prompt processing   2170 -> 2366 t/s

Generation is unaffected. The removed work scales with n_blocks by
n_tokens, so the gain grows with context and with ubatch size.
@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

(rewording commit)

Comment thread src/models/qwen4exp.cpp
Comment on lines +583 to +589
// the heads sit side by side on ne[1] and there are only a few of them
ggml_tensor * summed = nullptr;
for (int64_t h = 0; h < n_idx_h; ++h) {
ggml_tensor * slice = ggml_view_3d(ctx0, score, n_blocks, n_tps, n_stream,
score->nb[2], score->nb[3], h*score->nb[1]);
summed = summed ? ggml_add(ctx0, summed, slice) : ggml_cont(ctx0, slice);
}

@am17an am17an Aug 30, 2026

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.

instead of this thing, can we add an operation in the backends which are able to sum along as axis, similar to pytorch sum(axis=), it would be make a lot of permute->cont->sum_rows operations better supported. cc @ggerganov

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.

Not required for this PR though, just a suggestion

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.

Sounds good, I've added ggml ops before and I'd be happy to do this one as a follow up, with the tests and the backends. I'm getting the M5 shortly for Metal, and the CI should cover me for AMD.
It would clean up the permute -> cont -> sum_rows pattern in several models here. And reducing along ne1 with a large ne0 is the good shape on GPU anyway, so it should beat the transpose on top of removing the copy.

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.

Before adding an op, take a look if the graph is optimally constructed. Often cases, having to do such permute->cont->sum_rows means that earlier in the graph the data wasn't arranged properly. I'm not sure if this is the case here, but I would first look for that.

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.

Had a look: the layout is already fine, the matmul puts the heads on ne[1] with n_blocks as the fast axis so the slices are plain views, and what forces the reduction is the relu between the matmul and the sum.

I also tried dropping the redundant ggml_cont on q before the matmul, since rope already returns it contiguous. It works, but alternated A/B in both orders shows no measurable difference on prefill, the run to run drift is larger than the gap, so I left it out.

On the op: with only 4 heads a chain of adds does the job here, so it would mostly be for readability. The cases worth looking at are the models where the reduced axis is big enough that a chain of adds stops being reasonable, since that is where we are forced to transpose today.

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.

I also tried dropping the redundant ggml_cont on q before the matmul, since rope already returns it contiguous. It works, but alternated A/B in both orders shows no measurable difference on prefill, the run to run drift is larger than the gap, so I left it out.

It's better to remove the cont - it is a redundant op. Even if it is not measurable, there is no reason to have redundant nodes in the graph.

@ServeurpersoCom ServeurpersoCom Aug 31, 2026

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.

Yes, same goes for this one also, since ggml_add allocates a contiguous result anyway:

-        summed = summed ? ggml_add(ctx0, summed, slice) : ggml_cont(ctx0, slice);
+        summed = summed ? ggml_add(ctx0, summed, slice) : slice;

Greedy output is unchanged. It only holds because n_idx_h is 4 here, so the loop always runs at least one add: with a single head summed would stay a view, and the paths downstream would be the ones materialising it.

rope returns a freshly allocated, contiguous tensor, so the reshape that
feeds the matmul does not need a copy. ggml_reshape_3d asserts
contiguity, so a layout that would need the cont cannot slip through
silently.

Greedy output is unchanged token for token.

Address review from @ggerganov
@am17an

am17an commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

@ggerganov could you give guidance on how the sum(axis=1) should look like? it will be super fast on GPUs and probably be faster than doing this. Probably a new op with llm_graph_fused?

@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

For what it's worth, with 4 heads the chain of adds is already close to optimal, so I would not expect the op to be measurably faster in this particular spot, it would mostly be for readability. The gain should show up where the reduced axis is big enough that a chain of adds is not an option and we have to transpose today. I have a list of the models using sum_rows and I plan to go through them to find a case that actually justifies it before writing the op.

@am17an

am17an commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

@ServeurpersoCom it will show up on fast GPUs for sure. At least 1%

@ggerganov

Copy link
Copy Markdown
Member

We can add ggml_sum_rows_ext(ctx, x, dim) and ggml_sum_rows_ext(ctx, x, 0) == ggml_sum_rows(ctx, x)? Btw, dim != 0 might be difficult to support for quantized types - maybe disallow it for now.

@ServeurpersoCom

ServeurpersoCom commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@am17an Can confirm: the more bottlenecks you remove, the more the remaining optimisations are worth, and the faster the GPU, the more any small bottleneck shows up as a penalty. I still have a few optimisations in stock to PR, split up as much as possible so there is time to test and review each one properly.

@ServeurpersoCom

ServeurpersoCom commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

We can add ggml_sum_rows_ext(ctx, x, dim) and ggml_sum_rows_ext(ctx, x, 0) == ggml_sum_rows(ctx, x)? Btw, dim != 0 might be difficult to support for quantized types - maybe disallow it for now.

That works, and keeping ggml_sum_rows as the dim == 0 case means nothing existing changes. Agreed on disallowing dim != 0 for quantized types: reducing along ne0 stays inside the quantization blocks, anything else crosses them and would need per element dequantization. All the permute -> cont -> sum_rows sites I found are f32 anyway.

Q. One thing to confirm: the reduced dimension stays at 1 like ggml_sum_rows does, rather than being dropped?

Answered myself: ggml_sum_rows sets ne[0] = 1 and keeps the rest, and since a tensor always has GGML_MAX_DIMS dims there is nothing to drop anyway. So ne[dim] = 1 with the rest unchanged, which makes dim == 0 identical to ggml_sum_rows.

@ggerganov

Copy link
Copy Markdown
Member

So ne[dim] = 1

Yes, one can always reshape if they need to drop the reduced dimension.

@ServeurpersoCom

Copy link
Copy Markdown
Contributor Author

I did a POC on DeepSeek-V4 to see what the op actually buys: ServeurpersoCom/llama.cpp@7da3e10. It drops two conts per site, but prefill and decode are unchanged on my RTX PRO 6000, and perplexity is the same within reassociation noise. The same pattern shows up in the glm-dsa, deepseek32 and dots3note indexers, though those sit in the else branch of the fused lightning indexer and never run on the backends that have it. So on the model side it is mostly a readability win. Where it looks more promising is audio models, which reduce along the time axis all the time and pay a transpose in and out every time.

@ServeurpersoCom
ServeurpersoCom merged commit 09412af into ggml-org:master Sep 1, 2026
21 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model Model specific

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants