Skip to content
Closed
7 changes: 7 additions & 0 deletions common/arg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3214,6 +3214,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.speculative.cache_type_k = kv_cache_type_from_str(value);
}
).set_env("LLAMA_ARG_CACHE_TYPE_K_DRAFT"));
add_opt(common_arg(
{"-mtp", "--multi-token-prediction"},
string_format("Activate multi-token-prediction (if supported) (default: %s)", params.mtp ? "true" : "false"),
[](common_params & params) {
params.mtp = true;
}
));
Comment on lines +3217 to +3223

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.

Suggested change
add_opt(common_arg(
{"-mtp", "--multi-token-prediction"},
string_format("Activate multi-token-prediction (if supported) (default: %s)", params.mtp ? "true" : "false"),
[](common_params & params) {
params.mtp = true;
}
));
add_opt(common_arg(
{"-mtp", "--multi-token-prediction"},
{"-no-mtp", "--no-multi-token-prediction"},
string_format("whether to use multi-token-prediction (if supported) (default: %s)", params.mtp ? "true" : "false"),
[](common_params & params, bool value) {
params.mtp = value;
}
));

add_opt(common_arg(
{"-ctvd", "--cache-type-v-draft"}, "TYPE",
string_format(
Expand Down
1 change: 1 addition & 0 deletions common/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,7 @@ struct llama_model_params common_model_params_to_llama(common_params & params) {
mparams.check_tensors = params.check_tensors;
mparams.use_extra_bufts = !params.no_extra_bufts;
mparams.no_host = params.no_host;
mparams.mtp = params.mtp;

if (params.kv_overrides.empty()) {
mparams.kv_overrides = NULL;
Expand Down
1 change: 1 addition & 0 deletions common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ struct common_params {
bool no_op_offload = false; // globally disable offload host tensor operations to device
bool no_extra_bufts = false; // disable extra buffer types (used for weight repacking)
bool no_host = false; // bypass host buffer allowing extra buffers to be used
bool mtp = false; // enable MTP if supported by the model

bool single_turn = false; // single turn chat conversation

Expand Down
26 changes: 26 additions & 0 deletions common/sampling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,29 @@ std::vector<common_sampler_type> common_sampler_types_from_chars(const std::stri

return samplers;
}

/**
* Specialized sampling for speculative drafting.
*
* Prioritizes performance by using a direct ArgMax loop (Greedy).
* Penalties and complex sampling logic are bypassed to minimize
* drafting latency.
*/
llama_token common_sampler_sample_speculative(struct common_sampler * gsmpl, struct llama_context * ctx, int idx) {
const auto & params = gsmpl->params;

float * logits = llama_get_logits_ith(ctx, idx);
const int n_vocab = llama_n_vocab(llama_model_get_vocab(llama_get_model(ctx)));

int best_id = 0;
float max_val = logits[0];

for (int i = 1; i < n_vocab; ++i) {
if (logits[i] > max_val) {
max_val = logits[i];
best_id = i;
}
}

return best_id;
}
2 changes: 2 additions & 0 deletions common/sampling.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,5 @@ struct common_sampler_deleter {
};

typedef std::unique_ptr<common_sampler, common_sampler_deleter> common_sampler_ptr;

llama_token common_sampler_sample_speculative(struct common_sampler * gsmpl, struct llama_context * ctx, int idx);
115 changes: 115 additions & 0 deletions common/speculative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,118 @@ llama_tokens common_speculative_gen_draft(
}
return result;
}

llama_tokens mtp_speculative_gen_draft(
struct common_sampler * smpl,
struct llama_context * ctx,
struct common_speculative_params params,
llama_token id_last,
int32_t n_past,
llama_seq_id seq_id) {

int n_draft = params.n_draft;

llama_tokens drafts;
drafts.reserve(n_draft);

if (!smpl) return drafts;

llama_batch mtp_batch = llama_batch_init(1, 0, 1);
llama_set_mtp_op_type(ctx, MTP_OP_DRAFT_GEN);

llama_token current_input_id = id_last;
int32_t current_n_past = n_past;

for (int i = 0; i < n_draft; ++i) {
mtp_batch.n_tokens = 0;
common_batch_add(mtp_batch, current_input_id, current_n_past, {seq_id}, true);

// Perform the MTP draft generation decode. This writes the MTP layer's
// KV state for the draft token into the cache.
if (llama_decode(ctx, mtp_batch) != 0) {
break;
}

llama_token id_next = common_sampler_sample_speculative(smpl, ctx, 0);

// Drafting stops if token probability drops below `p_min` to save compute.
const auto * cur_p = common_sampler_get_candidates(smpl, true);
if (cur_p && cur_p->size > 0) {
float prob = cur_p->data[0].p;

if (prob < params.p_min) {
drafts.push_back(id_next);
current_n_past++;
break;
}
}

drafts.push_back(id_next);

current_input_id = id_next;
current_n_past++;
}
llama_batch_free(mtp_batch);
llama_set_mtp_op_type(ctx, MTP_OP_NONE);

// CRITICAL: Purge the metadata for the draft token we just wrote.
// This makes the physical cell available again for the main model's validation pass,
// preventing a cache state corruption where two cells map to the same logical position.
if (!drafts.empty()) {
llama_memory_seq_rm(llama_get_memory(ctx), seq_id, n_past, current_n_past);
}

return drafts;
}


void mtp_update_kv_cache(struct llama_context * ctx, const llama_batch& batch, bool is_prompt_warmup) {
if (batch.n_tokens == 0) {
return;
}

LOG_DBG("[MTP-UPDATE|%s] Updating %d tokens...\n", is_prompt_warmup ? "PROMPT_WARMUP" : "GEN_ACCEPTED", batch.n_tokens);

llama_batch mtp_batch = batch;
if (is_prompt_warmup) {
llama_set_mtp_op_type(ctx, MTP_OP_WARMUP);
} else {
llama_set_mtp_op_type(ctx, MTP_OP_UPDATE_ACCEPTED);
}

for (int i = 0; i < mtp_batch.n_tokens; ++i) {
mtp_batch.logits[i] = true;
}
llama_decode(ctx, mtp_batch);
llama_set_mtp_op_type(ctx, MTP_OP_NONE);
}

void mtp_accept_tokens(
struct llama_context * ctx,
const std::vector<llama_token> & ids,
int32_t n_past_base,
llama_seq_id seq_id
) {
if (ids.empty()) {
return;
}

// Prepare a resized copy of the validation sinfo to match the number of accepted tokens.
// This sets up the context for a "forced sinfo" decode.
if (!llama_mtp_prepare_sinfo_for_update(ctx, ids.size())) {
return;
}

// Build a new batch containing only the accepted tokens.
llama_batch accepted_batch = llama_batch_init(ids.size(), 0, 1);
for (size_t i = 0; i < ids.size(); ++i) {
common_batch_add(accepted_batch, ids[i], n_past_base + i, { seq_id }, true);
}

mtp_update_kv_cache(ctx, accepted_batch, false);

// Clean up the forced state to not affect subsequent, normal decode calls.
llama_mtp_cancel_sinfo_update(ctx);

llama_batch_free(accepted_batch);
}
47 changes: 43 additions & 4 deletions common/speculative.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ struct common_speculative_params {
float p_min = 0.75f; // min probability required to accept a token in the draft
};

struct mtp_kv_update_data {
llama_token id;
int32_t n_past;
int32_t tok_idx;
};

struct common_speculative * common_speculative_init(
struct llama_context * ctx_tgt,
struct llama_context * ctx_dft
Expand All @@ -29,7 +35,40 @@ void common_speculative_add_replacement_tgt_dft(

// sample up to n_draft tokens and add them to the batch using the draft model
llama_tokens common_speculative_gen_draft(
struct common_speculative * spec,
struct common_speculative_params params,
const llama_tokens & prompt,
llama_token id_last);
struct common_speculative * spec,
struct common_speculative_params params,
const llama_tokens & prompt,
llama_token id_last);

/**
* @brief Generates speculative draft tokens using the Multi-Token Prediction (MTP) architecture.
*
* This function performs a recursive generation loop using the MTP head (e.g., Eagle/NextN).
* It uses the fixed hidden state from the main model's last step and updates the MTP layer's
* internal KV cache autoregressively.
*
* @param smpl The sampler instance.
* @param ctx The llama context (shared between Main and MTP).
* @param params Speculative parameters (n_draft, p_min).
* @param id_last The last confirmed token ID from the main model.
* @param n_past The number of tokens in the validated past (start position for drafting).
* @param seq_id The sequence ID to use for drafting.
*
* @return std::vector<llama_token> The generated draft tokens.
*/
llama_tokens mtp_speculative_gen_draft(
struct common_sampler * smpl,
struct llama_context * ctx,
struct common_speculative_params params,
llama_token id_last,
int32_t n_past,
llama_seq_id seq_id);

void mtp_update_kv_cache(struct llama_context * ctx, const llama_batch& batch, bool is_prompt_warmup);

void mtp_accept_tokens(
struct llama_context * ctx,
const std::vector<llama_token> & ids,
int32_t n_past_base,
llama_seq_id seq_id
);
39 changes: 39 additions & 0 deletions include/llama.h
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,13 @@ extern "C" {
// - if not: only the last token is output
// )
//
enum llama_mtp_op_type {
MTP_OP_NONE,
MTP_OP_WARMUP,
MTP_OP_UPDATE_ACCEPTED,
MTP_OP_DRAFT_GEN,
};

typedef struct llama_batch {
int32_t n_tokens;

Expand Down Expand Up @@ -314,6 +321,7 @@ extern "C" {
bool use_extra_bufts; // use extra buffer types (used for weight repacking)
bool no_host; // bypass host buffer allowing extra buffers to be used
bool no_alloc; // only load metadata and simulate memory allocations
bool mtp; // use mtp if is supported by the Model
};

// NOTE: changing the default values of parameters marked as [EXPERIMENTAL] may cause crashes or incorrect results in certain configurations
Expand Down Expand Up @@ -536,6 +544,8 @@ extern "C" {

LLAMA_API int32_t llama_vocab_n_tokens(const struct llama_vocab * vocab);

LLAMA_API int32_t llama_model_n_nextn_layer(const struct llama_model * model);

// Functions to access the model's GGUF metadata scalar values
// - The functions return the length of the string on success, or -1 on failure
// - The output string is always null-terminated and cleared on failure
Expand Down Expand Up @@ -939,6 +949,9 @@ extern "C" {
// If true, all model tensors are activated during llama_decode() to load and cache their weights.
LLAMA_API void llama_set_warmup(struct llama_context * ctx, bool warmup);

// Set which, if any, MTP operation the context will use
LLAMA_API void llama_set_mtp_op_type(struct llama_context * ctx, enum llama_mtp_op_type mtp_op_type);

// Set abort callback
LLAMA_API void llama_set_abort_callback(struct llama_context * ctx, ggml_abort_callback abort_callback, void * abort_callback_data);

Expand Down Expand Up @@ -1442,6 +1455,32 @@ extern "C" {
ggml_opt_epoch_callback callback_train,
ggml_opt_epoch_callback callback_eval);

//
// MTP
//

LLAMA_API void llama_set_draft_input_hidden_state(struct llama_context * ctx, const float * hidden_state);

/**
* @brief Prepares the context for an MTP KV cache update by creating a resized copy of the last sinfo.
* This is used after speculative validation when only a subset of draft tokens are accepted.
* @param n_accepted The number of tokens that were accepted and for which the sinfo should be resized.
* @return true on success.
*/
LLAMA_API bool llama_mtp_prepare_sinfo_for_update(struct llama_context * ctx, size_t n_accepted);

/**
* @brief Prepares the context for an MTP KV cache update by reusing the sinfo from the last main model decode.
* This is used for the prompt warmup to ensure the MTP and main model KV caches are perfectly aligned.
* @return true on success.
*/
LLAMA_API bool llama_mtp_prepare_sinfo_for_warmup(struct llama_context * ctx);

/**
* @brief Clears the forced sinfo state from the context. Must be called after a decode that used a prepared sinfo.
*/
LLAMA_API void llama_mtp_cancel_sinfo_update(struct llama_context * ctx);

#ifdef __cplusplus
}
#endif
Expand Down
13 changes: 7 additions & 6 deletions src/llama-arch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2370,12 +2370,13 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
{LLM_TENSOR_VISEXP_FFN_UP, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
// NextN/MTP tensors are currently ignored (reserved for future MTP support)
// These tensors only exist in the last layer(s) and are treated as output tensors
{LLM_TENSOR_NEXTN_EH_PROJ, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
{LLM_TENSOR_NEXTN_EMBED_TOKENS, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_GET_ROWS}},
{LLM_TENSOR_NEXTN_ENORM, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_GET_ROWS}},
{LLM_TENSOR_NEXTN_HNORM, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}},
{LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
{LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}},
// Changed to LLM_TENSOR_LAYER_REPEATING because we saved these under a blk with a non-negative id
{LLM_TENSOR_NEXTN_EH_PROJ, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_NEXTN_EMBED_TOKENS, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_GET_ROWS}},
{LLM_TENSOR_NEXTN_ENORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_GET_ROWS}},
{LLM_TENSOR_NEXTN_HNORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_NEXTN_SHARED_HEAD_HEAD, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_NEXTN_SHARED_HEAD_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
};

LLM_KV::LLM_KV(llm_arch arch, const char * suffix) : arch(arch), suffix(suffix) {}
Expand Down
34 changes: 17 additions & 17 deletions src/llama-batch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -301,17 +301,17 @@ bool llama_batch_allocr::init(
ok = false;
}

if (!ok) {
LLAMA_LOG_ERROR(
"%s: the tokens of sequence %d in the input batch have inconsistent sequence positions:\n"
" - the last position stored in the memory module of the context (i.e. the KV cache) for sequence %d is X = %d\n"
" - the tokens for sequence %d in the input batch have a starting position of Y = %d\n"
" it is required that the sequence positions remain consecutive: Y = X + 1\n",
__func__, s, s, p0, s, seq_pos_min(s));
// if (!ok) {
// LLAMA_LOG_ERROR(
// "%s: the tokens of sequence %d in the input batch have inconsistent sequence positions:\n"
// " - the last position stored in the memory module of the context (i.e. the KV cache) for sequence %d is X = %d\n"
// " - the tokens for sequence %d in the input batch have a starting position of Y = %d\n"
// " it is required that the sequence positions remain consecutive: Y = X + 1\n",
// __func__, s, s, p0, s, seq_pos_min(s));

return false;
}
}
// return false;
// }
}

if (seq_pos_max(s) - seq_pos_min(s) + 1 > (int) seq_pos[s].size()) {
LLAMA_LOG_ERROR("%s: sequence %d positions are not continuous\n", __func__, s);
Expand Down Expand Up @@ -874,13 +874,13 @@ struct llama_batch llama_batch_get_one(

struct llama_batch llama_batch_init(int32_t n_tokens_alloc, int32_t embd, int32_t n_seq_max) {
llama_batch batch = {
/*n_tokens =*/ 0,
/*tokens =*/ nullptr,
/*embd =*/ nullptr,
/*pos =*/ nullptr,
/*n_seq_id =*/ nullptr,
/*seq_id =*/ nullptr,
/*logits =*/ nullptr,
/*n_tokens =*/ 0,
/*tokens =*/ nullptr,
/*embd =*/ nullptr,
/*pos =*/ nullptr,
/*n_seq_id =*/ nullptr,
/*seq_id =*/ nullptr,
/*logits =*/ nullptr,
};

if (embd) {
Expand Down
Loading