Repository navigation
gemma4: graph optimizations (massive gains on pp for E2B and E4B) #31356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
22756d3
e1c1ca2
7da7a2c
262cbca
a9b3dc6
8e7d454
3341212
f27b68f
d842bfb
d14ee0b
0455252
0971a7a
12b8b54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,8 +154,7 @@ std::unique_ptr<llm_graph_context> llama_model_gemma4::build_arch_graph(const ll | |
| // get 2D slice view from a 3D tensor, the idx corresponds to the 3rd dim | ||
| static ggml_tensor * gemma4_view_2d_slice(ggml_context * ctx0, ggml_tensor * x, int idx) { | ||
| GGML_ASSERT(idx < (int) x->ne[2]); | ||
| return ggml_view_2d(ctx0, x, x->ne[0], x->ne[1], ggml_row_size(x->type, x->ne[0]), | ||
| idx * x->ne[0] * x->ne[1] * ggml_element_size(x)); | ||
| return ggml_view_2d(ctx0, x, x->ne[0], x->ne[1], x->nb[1], idx * x->nb[2]); | ||
| } | ||
|
|
||
| llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_params & params) : | ||
|
|
@@ -183,10 +182,14 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para | |
| // TODO: is causal == true correct? might need some changes | ||
| auto * inp_attn = build_attn_inp_kv_iswa(); | ||
|
|
||
| auto inp_attn_full = *inp_attn; | ||
| auto inp_attn_prune = *inp_attn; | ||
|
|
||
| ggml_tensor * inp_out_ids = build_inp_out_ids(); | ||
|
|
||
| if (model.per_layer_tok_embd) { | ||
| const float tok_embd_scale = sqrtf((float) n_embd_per_layer); | ||
| // fold per_layer_input_scale (1/sqrt(2)) from project_per_layer_inputs() into tok_embd_scale | ||
| const float tok_embd_scale = sqrtf((float) n_embd_per_layer) / sqrtf(2.0f); | ||
|
|
||
| inp_per_layer = ggml_scale (ctx0, inp_per_layer, tok_embd_scale); | ||
| inp_per_layer = ggml_reshape_3d(ctx0, inp_per_layer, n_embd_per_layer, n_layer, inp_per_layer->ne[1]); | ||
|
|
@@ -195,7 +198,36 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para | |
| inp_per_layer = project_per_layer_inputs(inpL, inp_per_layer); | ||
| } | ||
|
|
||
| // 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; | ||
| for (int il = (int) hparams.n_layer_kv_from_start; prune_shared && il <= n_layer; ++il) { | ||
| prune_shared = !cparams.embeddings_layer_inp[il]; | ||
| } | ||
|
Comment on lines
+201
to
+207
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah. That's one of the use-cases I missed. |
||
|
|
||
| int64_t n_rows = n_tokens; | ||
|
|
||
| for (int il = 0; il < n_layer; ++il) { | ||
| if (prune_shared && il == (int) hparams.n_layer_kv_from_start) { | ||
| inpL = ggml_get_rows(ctx0, inpL, inp_out_ids); | ||
| inp_pos = ggml_reshape_1d(ctx0, ggml_get_rows(ctx0, ggml_reshape_2d(ctx0, inp_pos, 1, n_tokens), inp_out_ids), n_outputs); | ||
|
|
||
| // TODO: instead of doing get_rows here, extend llm_attn_mask to have a `out` mask that | ||
| // is construted during set_input to contain just the pruned output cells | ||
| // this can be used to enable pruning with more than 1 sequence, both with unified and split KV cache | ||
| // ref: https://github.com/ggml-org/llama.cpp/pull/31356#discussion_r4455286628 | ||
| inp_attn_prune.self_attn_mask.raw = ggml_get_rows(ctx0, inp_attn_full.self_attn_mask.raw, inp_out_ids); | ||
| inp_attn_prune.self_attn_mask_swa.raw = ggml_get_rows(ctx0, inp_attn_full.self_attn_mask_swa.raw, inp_out_ids); | ||
|
|
||
| inp_attn_prune.self_attn_mask.raw = ggml_cast(ctx0, inp_attn_prune.self_attn_mask.raw, GGML_TYPE_F16); | ||
| inp_attn_prune.self_attn_mask_swa.raw = ggml_cast(ctx0, inp_attn_prune.self_attn_mask_swa.raw, GGML_TYPE_F16); | ||
|
|
||
| n_rows = n_outputs; | ||
|
|
||
| inp_attn = &inp_attn_prune; | ||
| } | ||
|
|
||
| const int64_t n_embd_head = hparams.n_embd_head_k(il); | ||
| GGML_ASSERT(n_embd_head == hparams.n_embd_head_v(il)); | ||
|
|
||
|
|
@@ -226,14 +258,14 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para | |
| qkv_fused = build_lora_mm(model.layers[il].wqkv, cur, model.layers[il].wqkv_s); | ||
| cb(qkv_fused, "wqkv", il); | ||
| const int64_t q_dim = n_embd_head * n_head; | ||
| Qcur = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv_fused, q_dim, n_tokens, qkv_fused->nb[1], 0)); | ||
| Qcur = ggml_cont(ctx0, ggml_view_2d(ctx0, qkv_fused, q_dim, n_rows, qkv_fused->nb[1], 0)); | ||
| } else { | ||
| Qcur = build_lora_mm(model.layers[il].wq, cur, model.layers[il].wq_s); | ||
| } | ||
| { | ||
| cb(Qcur, "Qcur", il); | ||
|
|
||
| Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head, n_tokens); | ||
| Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head, n_rows); | ||
|
|
||
| Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, nullptr, LLM_NORM_RMS, il); | ||
| cb(Qcur, "Qcur_normed", il); | ||
|
|
@@ -287,9 +319,8 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para | |
| Qcur, nullptr, nullptr, nullptr, nullptr, nullptr, hparams.f_attention_scale, il); | ||
| } | ||
|
|
||
| // TODO @ngxson : strip unused token right after the last KV layer to speed up prompt processing | ||
| // keep all rows when extracting unmasked nextn embeddings (MTP target needs the hidden state for every token) | ||
| if (il == n_layer - 1 && crop_before_nextn(inp_out_ids)) { | ||
| if (!prune_shared && il == n_layer - 1 && crop_before_nextn(inp_out_ids)) { | ||
| cur = ggml_get_rows(ctx0, cur, inp_out_ids); | ||
| inpL = ggml_get_rows(ctx0, inpL, inp_out_ids); | ||
| } | ||
|
|
@@ -331,7 +362,7 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para | |
| ggml_tensor * tmp = ggml_rms_norm(ctx0, attn_out, hparams.f_norm_rms_eps); | ||
| tmp = ggml_scale(ctx0, tmp, 1.0f / sqrtf((float) n_embd)); | ||
| tmp = ggml_mul(ctx0, tmp, model.layers[il].ffn_gate_inp_s); | ||
| ggml_tensor * logits = build_lora_mm(model.layers[il].ffn_gate_inp, tmp); // [n_expert, n_tokens] | ||
| ggml_tensor * logits = build_lora_mm(model.layers[il].ffn_gate_inp, tmp); // [n_expert, n_rows] | ||
| cb(logits, "ffn_moe_logits", il); | ||
|
|
||
| cur_moe = build_moe_ffn(cur_moe, | ||
|
|
@@ -383,18 +414,17 @@ llama_model_gemma4::graph::graph(const llama_model & model, const llm_graph_para | |
| ggml_tensor * pe_in = cur; | ||
| cb(cur, "pe_in", il); | ||
|
|
||
| cur = build_lora_mm(model.layers[il].per_layer_inp_gate, cur); // [n_embd_per_layer, n_tokens] | ||
| cur = build_lora_mm(model.layers[il].per_layer_inp_gate, cur); // [n_embd_per_layer, n_rows] | ||
| cur = ggml_gelu(ctx0, cur); | ||
|
|
||
| ggml_tensor * inp_this_layer = gemma4_view_2d_slice(ctx0, inp_per_layer, il); // [n_embd_per_layer, n_tokens] | ||
|
|
||
| // TODO @ngxson : improve this | ||
| if (il == n_layer - 1 && crop_before_nextn(inp_out_ids)) { | ||
| if ((prune_shared && !hparams.has_kv(il)) || (il == n_layer - 1 && crop_before_nextn(inp_out_ids))) { | ||
| inp_this_layer = ggml_get_rows(ctx0, inp_this_layer, inp_out_ids); | ||
| } | ||
|
|
||
| cur = ggml_mul(ctx0, cur, inp_this_layer); | ||
| cur = build_lora_mm(model.layers[il].per_layer_proj, cur); // [n_embd, n_tokens] | ||
| cur = build_lora_mm(model.layers[il].per_layer_proj, cur); // [n_embd, n_rows] | ||
| cur = build_norm(cur, model.layers[il].per_layer_post_norm, nullptr, LLM_NORM_RMS, il); | ||
| cb(cur, "per_layer_embd_out", il); | ||
|
|
||
|
|
@@ -522,19 +552,22 @@ ggml_tensor * llama_model_gemma4::graph::project_per_layer_inputs(ggml_tensor * | |
| const float per_layer_input_scale = 1.0f / sqrtf(2.0f); | ||
|
|
||
| // note: this matrix multiplication will be performed in the input layer (i.e. on the CPU) | ||
| // fold per_layer_input_scale into proj_norm_w and per_layer_projection_scale into proj_eps | ||
| ggml_tensor * proj_norm_w = ggml_scale(ctx0, model.per_layer_proj_norm, per_layer_input_scale); | ||
| ggml_build_forward_expand(gf, proj_norm_w); | ||
|
|
||
| ggml_tensor * per_layer_proj; | ||
| per_layer_proj = ggml_mul_mat (ctx0, model.per_layer_model_proj, inp_batch); | ||
| per_layer_proj = ggml_scale (ctx0, per_layer_proj, per_layer_projection_scale); | ||
| per_layer_proj = ggml_reshape_3d(ctx0, per_layer_proj, n_embd_per_layer, n_layer, n_tokens); | ||
|
|
||
| per_layer_proj = build_norm(per_layer_proj, model.per_layer_proj_norm, nullptr, LLM_NORM_RMS, -1); | ||
| const float proj_eps = hparams.f_norm_rms_eps / (per_layer_projection_scale * per_layer_projection_scale); | ||
| per_layer_proj = ggml_rms_norm(ctx0, per_layer_proj, proj_eps); | ||
| per_layer_proj = ggml_mul(ctx0, per_layer_proj, proj_norm_w); | ||
| cb(per_layer_proj, "per_layer_proj", -1); | ||
|
|
||
| inp_per_layer = ggml_add (ctx0, per_layer_proj, inp_per_layer); | ||
| inp_per_layer = ggml_scale(ctx0, inp_per_layer, per_layer_input_scale); | ||
| inp_per_layer = ggml_add(ctx0, per_layer_proj, inp_per_layer); | ||
| cb(inp_per_layer, "inp_per_layer", -1); | ||
|
|
||
| // permute to shape: [n_embd_per_layer, n_tokens, n_layer] | ||
| inp_per_layer = ggml_cont(ctx0, ggml_permute(ctx0, inp_per_layer, 0, 2, 1, 3)); | ||
| return inp_per_layer; | ||
| return ggml_permute(ctx0, inp_per_layer, 0, 2, 1, 3); | ||
| } | ||
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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 == truebut it stopped working after #29510 got merged.It probably worked earlier just because
unified_kvwas not used witnn_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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).