-
Notifications
You must be signed in to change notification settings - Fork 20.6k
Support Step3.5/3.7 flash mtp3 #24340
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
e98e0b3
1208d99
0b8aa51
e0fb9ff
34f68f5
ae013e3
1885d8f
f4a2c12
9a0ff26
2952d83
48a7484
2ce24fb
9b8f3b6
7a0a247
9858fd2
60b04d3
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 |
|---|---|---|
|
|
@@ -825,7 +825,13 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
|
|
||
| int32_t n_embd = 0; | ||
|
|
||
| bool is_mem_shared = false; | ||
| // One MTP draft driver, three modes (set once in the ctor): | ||
| // is_mem_shared (gemma4): shares the target KV, runs all heads in one graph. | ||
| // chain_heads (step35): n_mtp_layers trained heads, one per draft step. | ||
| // neither (qwen35 / qwen35moe): a single trained MTP head. | ||
| int32_t n_mtp_layers = 1; | ||
| bool is_mem_shared = false; // gemma4 | ||
| bool chain_heads = false; // derived in the ctor: n_mtp_layers > 1 && !is_mem_shared | ||
|
|
||
| // Per-sequence cross-batch carryover: pair (h_p, x_{p+1}) at MTP pos p+1. | ||
| // The last h-row of one process() call needs the first token of the NEXT | ||
|
|
@@ -840,10 +846,8 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| std::vector<std::vector<float>> verify_h; | ||
| std::vector<int32_t> verify_h_rows; | ||
|
|
||
| // Per-seq draft length from the last draft() call, used in accept() to | ||
| // roll back ctx_dft's recurrent state past the AR draft's redundant | ||
| // pre-advancement before process() mirrored the verify batch. | ||
| std::vector<uint16_t> last_n_drafted; | ||
| std::vector<int> i_last; | ||
| std::vector<std::vector<float>> chain_h; | ||
|
|
||
| common_speculative_impl_draft_mtp(const common_params_speculative & params, uint32_t n_seq) | ||
| : common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_MTP, n_seq) | ||
|
|
@@ -856,6 +860,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| n_embd = llama_model_n_embd_out(llama_get_model(ctx_dft)); | ||
| GGML_ASSERT(n_embd == llama_model_n_embd(llama_get_model(ctx_tgt)) && | ||
| "MTP input row width must match the target h_nextn width"); | ||
| n_mtp_layers = std::max(1, (int) llama_model_n_layer_nextn(llama_get_model(ctx_dft))); | ||
|
|
||
| LOG_INF("%s: adding speculative implementation 'draft-mtp'\n", __func__); | ||
| LOG_INF("%s: - n_max=%d, n_min=%d, p_min=%.2f, n_embd=%d, backend_sampling=%d\n", __func__, this->params.n_max, this->params.n_min, this->params.p_min, n_embd, (int) this->params.backend_sampling); | ||
|
|
@@ -902,16 +907,25 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| llama_set_embeddings_nextn(ctx_dft, true, /*masked*/ true); | ||
|
|
||
| is_mem_shared = llama_get_ctx_other(ctx_dft) == ctx_tgt; | ||
| chain_heads = n_mtp_layers > 1 && !is_mem_shared; | ||
|
|
||
| if (chain_heads) { | ||
| this->params.n_max = std::min(this->params.n_max, n_mtp_layers); | ||
|
|
||
| chain_h.assign(n_seq, {}); | ||
| for (auto & c : chain_h) { | ||
| c.reserve((size_t) (this->params.n_max + 1) * n_embd); | ||
| } | ||
| } | ||
|
|
||
| pending_h.assign(n_seq, std::vector<float>(n_embd, 0.0f)); | ||
|
|
||
| i_last.assign(n_seq, -1); | ||
| i_batch_beg.assign(n_seq, -1); | ||
| i_batch_end.assign(n_seq, -1); | ||
|
|
||
| verify_h.assign(n_seq, {}); | ||
| verify_h_rows.assign(n_seq, 0); | ||
|
|
||
| last_n_drafted.assign(n_seq, 0); | ||
| } | ||
|
|
||
| ~common_speculative_impl_draft_mtp() override { | ||
|
|
@@ -1017,9 +1031,34 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| set_h(i_batch_beg[seq_id], pending_h[seq_id].data()); | ||
| } | ||
|
|
||
| const int32_t rc = llama_decode(ctx_dft, batch); | ||
| if (rc != 0) { | ||
| LOG_ERR("%s: llama_decode(ctx_dft) failed rc=%d (pos=%d)\n", __func__, (int) rc, (int) batch_in.pos[0]); | ||
| auto * mem_dft = llama_get_memory(ctx_dft); | ||
|
|
||
| bool ok = true; | ||
| for (int head = 0; head < n_mtp_layers; ++head) { | ||
| if (chain_heads) { | ||
| // ref: https://github.com/ggml-org/llama.cpp/pull/24340/changes#r3413498544 | ||
| for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { | ||
| if (i_batch_beg[seq_id] < 0) { | ||
| continue; | ||
| } | ||
| llama_memory_seq_rm(mem_dft, seq_id, batch_in.pos[i_batch_beg[seq_id]], -1); | ||
| } | ||
| llama_set_nextn_layer_offset(ctx_dft, head); | ||
| } | ||
|
|
||
| const int32_t rc = llama_decode(ctx_dft, batch); | ||
| if (rc != 0) { | ||
| LOG_ERR("%s: llama_decode(ctx_dft) head=%d failed rc=%d (pos=%d)\n", | ||
| __func__, head, (int) rc, (int) batch_in.pos[0]); | ||
| ok = false; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (chain_heads) { | ||
| llama_set_nextn_layer_offset(ctx_dft, 0); // restore default for non-draft decodes | ||
| } | ||
| if (!ok) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
@@ -1054,7 +1093,6 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| int n_drafting = 0; | ||
| std::vector<bool> drafting(n_seq); | ||
|
|
||
| const float * h_row = nullptr; | ||
| const size_t row_bytes = (size_t) n_embd * sizeof(float); | ||
|
|
||
| for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { | ||
|
|
@@ -1069,22 +1107,43 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| common_sampler_reset(smpls[seq_id].get()); | ||
|
|
||
| common_batch_add(batch, dp.id_last, dp.n_past, { seq_id }, true); | ||
| std::memcpy(batch.embd + (size_t) (batch.n_tokens - 1) * n_embd, pending_h[seq_id].data(), row_bytes); | ||
|
|
||
| h_row = pending_h[seq_id].data(); | ||
| std::memcpy(batch.embd + n_embd*(batch.n_tokens - 1), h_row, row_bytes); | ||
| } | ||
| i_last[seq_id] = batch.n_tokens - 1; | ||
|
|
||
| int ret = llama_decode(ctx_dft, batch); | ||
| if (ret != 0) { | ||
| LOG_WRN("%s: llama_decode returned %d\n", __func__, ret); | ||
| return; | ||
| if (chain_heads) { | ||
| chain_h[seq_id].assign(pending_h[seq_id].begin(), pending_h[seq_id].end()); | ||
| } | ||
| } | ||
|
|
||
| int i = 0; | ||
|
|
||
| while (n_drafting > 0) { | ||
| int i_batch = 0; | ||
| // each step decodes under a different head, i.e. a different decoder layer, and | ||
| // KV is per layer. process() filled this layer's KV only for positions < n_past | ||
| // (prompt + accepted prefix) β nothing in the draft region yet. so reset the | ||
| // draft region (the seq_rm lower bound is n_past, leaving the prompt KV intact) | ||
| // and select head i so it rebuilds its own layer's KV there; decoding just the | ||
| // latest token would leave its attention reading cells only another head wrote. | ||
| if (chain_heads) { | ||
| auto * mem_dft = llama_get_memory(ctx_dft); | ||
| for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { | ||
| if (drafting[seq_id]) { | ||
| llama_memory_seq_rm(mem_dft, seq_id, dparams[seq_id].n_past, -1); | ||
| } | ||
| } | ||
| llama_set_nextn_layer_offset(ctx_dft, i); | ||
| } | ||
|
|
||
| int ret = llama_decode(ctx_dft, batch); | ||
| if (ret != 0) { | ||
| LOG_WRN("%s: llama_decode[%d] returned %d\n", __func__, i, ret); | ||
| break; | ||
| } | ||
|
|
||
| // rebuild the batch for the next step: the growing-KV paths re-add only the | ||
| // new token (the KV already holds the prefix), while chained heads re-add the | ||
| // whole prefix at the next head. dropped sequences are simply not re-added. | ||
| common_batch_clear(batch); | ||
|
|
||
| for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { | ||
|
|
@@ -1094,9 +1153,8 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
|
|
||
| auto * smpl = smpls[seq_id].get(); | ||
|
|
||
| common_sampler_sample(smpl, ctx_dft, i_batch, true); | ||
| h_row = llama_get_embeddings_nextn_ith(ctx_dft, i_batch); | ||
| ++i_batch; | ||
| common_sampler_sample(smpl, ctx_dft, i_last[seq_id], true); | ||
| const float * h_row = llama_get_embeddings_nextn_ith(ctx_dft, i_last[seq_id]); | ||
|
|
||
| const auto * cur_p = common_sampler_get_candidates(smpl, true); | ||
|
|
||
|
|
@@ -1130,30 +1188,41 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| continue; | ||
| } | ||
|
|
||
| if (is_mem_shared) { | ||
| if (chain_heads) { | ||
| // ref: https://github.com/ggml-org/llama.cpp/pull/24340#discussion_r3448031546 | ||
| chain_h[seq_id].insert(chain_h[seq_id].end(), h_row, h_row + n_embd); | ||
|
|
||
| const int n_rows = (int) result.size() + 1; // id_last + tokens drafted so far | ||
| for (int t = 0; t < n_rows; ++t) { | ||
| const llama_token tok = (t == 0) ? dp.id_last : result[t - 1]; | ||
| common_batch_add(batch, tok, dp.n_past + t, { seq_id }, t == n_rows - 1); | ||
| std::memcpy(batch.embd + (size_t) (batch.n_tokens - 1) * n_embd, | ||
| chain_h[seq_id].data() + (size_t) t * n_embd, row_bytes); | ||
| } | ||
|
Comment on lines
+1191
to
+1201
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. This seems incorrect - every next draft, we decode all previous tokens again. Why is that? Normally, we should decode just the latest token.
Contributor
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. You're right that every draft step re-decodes the whole prefix β that's intentional. This path isn't a normal AR draft; it's an accumulating-batch + per-step-seq_rm flow, because each step runs a different head. After process(), every head's KV is already filled for the prompt + accepted prefix (positions < n_past), and the per-step seq_rm lower bound is n_past, so we never re-decode the prompt. What gets replayed each step is only the draft region β id_last plus the few tokens drafted so far (capped at n_max=3). Why replay it under each head: since we switch heads per step, the current head hasn't written its own KV for any draft-region position yet this round. If we only decoded the latest token, its attention over the earlier draft positions would read cells that only another head ever wrote β garbage. So each step we seq_rm the draft region (so find_slot reuses the same slots and positions stay aligned), switch the head, and replay the accumulated prefix so this head fills its own KV. For contrast, the other two branches reuse a single head across steps, so their prefix KV is already valid and they just append the latest token
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. Ah yes. I'm still not used to that approach, but it seems correct. Add a reference to this explanation in the code.
Contributor
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. Agreed it's not intuitive, it took me quite a while to design this too :) Added a comment and committed the other suggestions as well.
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. Add a reference: diff --git a/common/speculative.cpp b/common/speculative.cpp
index d7a177b7b..f8a6287c2 100644
--- a/common/speculative.cpp
+++ b/common/speculative.cpp
@@ -1184,6 +1184,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
}
if (chain_heads) {
+ // ref: https://github.com/ggml-org/llama.cpp/pull/24340#discussion_r3448031546
chain_h[seq_id].insert(chain_h[seq_id].end(), h_row, h_row + n_embd);
const int n_rows = (int) result.size() + 1; // id_last + tokens drafted so far |
||
| } else if (is_mem_shared) { | ||
| // note: with shared memory (e.g. Gemma4 assistants) we use the same position for all draft tokens | ||
| // ref: https://github.com/huggingface/transformers/blob/effde20942e3f82a1b97449f60b3a48c5ff96145/docs/source/en/model_doc/gemma4_assistant.md?plain=1#L36-L37 | ||
| common_batch_add(batch, id, dp.n_past, { seq_id }, true); | ||
| std::memcpy(batch.embd + (size_t) (batch.n_tokens - 1) * n_embd, h_row, row_bytes); | ||
| } else { | ||
| common_batch_add(batch, id, dp.n_past + i + 1, { seq_id }, true); | ||
| std::memcpy(batch.embd + (size_t) (batch.n_tokens - 1) * n_embd, h_row, row_bytes); | ||
| } | ||
| std::memcpy(batch.embd + n_embd*(batch.n_tokens - 1), h_row, row_bytes); | ||
| } | ||
|
|
||
| if (batch.n_tokens == 0) { | ||
| break; | ||
| i_last[seq_id] = batch.n_tokens - 1; | ||
| } | ||
|
|
||
| // evaluate the drafted tokens on the draft model | ||
| ret = llama_decode(ctx_dft, batch); | ||
| if (ret != 0) { | ||
| LOG_WRN("%s: llama_decode[%d] returned %d\n", __func__, i, ret); | ||
| if (batch.n_tokens == 0) { | ||
| break; | ||
| } | ||
|
|
||
| ++i; | ||
| } | ||
|
|
||
| if (chain_heads) { | ||
| llama_set_nextn_layer_offset(ctx_dft, 0); // restore default for non-draft decodes | ||
| } | ||
|
|
||
| for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) { | ||
| auto & dp = dparams[seq_id]; | ||
| if (!dp.drafting) { | ||
|
|
@@ -1163,8 +1232,6 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl { | |
| if (dp.result->size() < (size_t) params.n_min) { | ||
| dp.result->clear(); | ||
| } | ||
|
|
||
| last_n_drafted[seq_id] = (uint16_t) dp.result->size(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1777,7 +1844,7 @@ common_speculative * common_speculative_init(common_params_speculative & params, | |
|
|
||
| bool has_draft_simple = (enabled_configs & (1u << COMMON_SPECULATIVE_TYPE_DRAFT_SIMPLE)); | ||
| bool has_draft_eagle3 = (enabled_configs & (1u << COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3)) && params.draft.ctx_dft != nullptr; | ||
| bool has_mtp = (enabled_configs & (1u << COMMON_SPECULATIVE_TYPE_DRAFT_MTP)) && params.draft.ctx_dft != nullptr; | ||
| bool has_draft_mtp = (enabled_configs & (1u << COMMON_SPECULATIVE_TYPE_DRAFT_MTP)) && params.draft.ctx_dft != nullptr; | ||
|
|
||
|
|
||
|
|
||
|
|
@@ -1815,7 +1882,7 @@ common_speculative * common_speculative_init(common_params_speculative & params, | |
| if (has_draft_eagle3) { | ||
| configs.push_back(common_speculative_config(COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3, params)); | ||
| } | ||
| if (has_mtp) { | ||
| if (has_draft_mtp) { | ||
| configs.push_back(common_speculative_config(COMMON_SPECULATIVE_TYPE_DRAFT_MTP, params)); | ||
| } | ||
| } | ||
|
|
||
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.
I don't understand the logic here - seems incorrect. Every
headiteration will basically erase the result of the previous iteration.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.
Each head runs a different layer, set_nextn_layer_offset(head) makes graph_mtp build layer n_layer()+head, i.e. 45/46/47. So each head writes its own k_l[il]/v_l[il].
seq_rm here doesn't drop any KV data β it just clears the cell metadata so find_slot hands back the same cells at the same positions for every head. Without it, head 46/47 would land on fresh cells and we'd get duplicate positions in v_cells .
So after the loop those cells hold valid KV for all three MTP layers at once. it's the teacher-forcing catch-up that seeds each head's layer so the next draft() round attends to a correct, target-aligned cache.
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, got it. That's interesting.