Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 258 additions & 3 deletions common/speculative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,19 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
// call to pair with, so it's stashed here until that next call fires.
std::vector<std::vector<float>> pending_h; // [n_seq][n_embd]

// catch-up rows deferred from process() and prepended to the first draft
// decode, which merges the two evals. Only for the single-head own-memory
// path; flushed standalone when drafting does not follow.
static constexpr int32_t defer_max = 64;
bool defer_enabled = false;
bool chain_graph = false;
struct {
std::vector<llama_token> tok;
std::vector<llama_pos> pos;
std::vector<llama_seq_id> seq;
std::vector<float> embd;
} defer;

std::vector<int32_t> i_batch_beg;
std::vector<int32_t> i_batch_end;

Expand Down Expand Up @@ -1345,9 +1358,10 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
s.reset(common_sampler_init(llama_get_model(ctx_dft), sparams));
}

// offload draft sampling to the backend
// offload draft sampling to the backend (chained drafting outputs several
// rows per sequence, which backend sampling does not support)
backend_chains.assign(n_seq, nullptr);
if (this->params.backend_sampling) {
if (this->params.backend_sampling && getenv("LLAMA_SPEC_CHAIN") == nullptr) {
for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) {
llama_sampler * chain = llama_sampler_chain_init(llama_sampler_chain_default_params());
llama_sampler_chain_add(chain, llama_sampler_init_top_k(10));
Expand All @@ -1367,6 +1381,14 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
is_mem_shared = llama_get_ctx_other(ctx_dft) == ctx_tgt;
chain_heads = n_mtp_layers > 1 && !is_mem_shared;

// experimental: draft all n_max tokens with one chained decode (in-graph argmax
// feeds each next step); replaces n_max sequential draft decodes
chain_graph = !is_mem_shared && !chain_heads && getenv("LLAMA_SPEC_CHAIN") != nullptr;
if (chain_graph) {
// the chain decode absorbs the deferred catch-up rows, one eval per round
defer_enabled = true;
}

if (chain_heads) {
this->params.n_max = std::min(this->params.n_max, n_mtp_layers);

Expand Down Expand Up @@ -1406,7 +1428,66 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
llama_batch_free(batch);
}

// decode deferred catch-up rows standalone (no logits); used when no draft decode
// follows to absorb them
bool flush_deferred() {
if (defer.tok.empty()) {
return true;
}

auto * ctx_dft = this->params.ctx_dft;
const size_t row_bytes = (size_t) n_embd * sizeof(float);

common_batch_clear(batch);
for (size_t k = 0; k < defer.tok.size(); ++k) {
common_batch_add(batch, defer.tok[k], defer.pos[k], { defer.seq[k] }, false);
std::memcpy(batch.embd + (size_t) (batch.n_tokens - 1) * n_embd, defer.embd.data() + k * (size_t) n_embd, row_bytes);
}
defer.tok.clear();
defer.pos.clear();
defer.seq.clear();
defer.embd.clear();

const int32_t rc = llama_decode(ctx_dft, batch);
if (rc != 0) {
SPC_ERR("llama_decode(ctx_dft) deferred flush failed rc=%d\n", (int) rc);
return false;
}
return true;
}

// drop deferred rows of seq_id with pos >= pos_from; returns true if any dropped
bool drop_deferred_from(llama_seq_id seq_id, llama_pos pos_from) {
const size_t row_bytes = (size_t) n_embd * sizeof(float);

size_t w = 0;
for (size_t k = 0; k < defer.tok.size(); ++k) {
if (defer.seq[k] == seq_id && defer.pos[k] >= pos_from) {
continue;
}
if (w != k) {
defer.tok[w] = defer.tok[k];
defer.pos[w] = defer.pos[k];
defer.seq[w] = defer.seq[k];
std::memmove(defer.embd.data() + w * (size_t) n_embd,
defer.embd.data() + k * (size_t) n_embd, row_bytes);
}
w++;
}

const bool dropped = w < defer.tok.size();
defer.tok.resize(w);
defer.pos.resize(w);
defer.seq.resize(w);
defer.embd.resize(w * (size_t) n_embd);

return dropped;
}

void begin(llama_seq_id seq_id, const llama_tokens & prompt) override {
// note: the server calls begin() after the prefill decode, so stale defer
// rows are already handled by the position-rewind trim in process(). Rows
// that remain here belong to this prompt and feed the next draft decode.
const int32_t N = (int32_t) prompt.size();
if (N <= 0) {
return;
Expand Down Expand Up @@ -1458,8 +1539,58 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {

const size_t row_bytes = (size_t) n_embd * sizeof(float);

// deferred rows at or past the incoming batch positions are stale: either a
// new prompt rewound the sequence, or they hold candidates a later verify
// rejected. Drop them and clear matching draft cells before any decode.
if (defer_enabled && !defer.tok.empty()) {
for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) {
if (i_batch_beg[seq_id] < 0) {
continue;
}
const llama_pos pos_min_in = batch_in.pos[i_batch_beg[seq_id]];
if (drop_deferred_from(seq_id, pos_min_in)) {
llama_memory_seq_rm(llama_get_memory(ctx_dft), seq_id, pos_min_in, -1);
}
}
}

// if kv is shared with target (e.g Gemma4), then we can skip this catch-up decode
if (!is_mem_shared) {
if (!is_mem_shared && defer_enabled && n_tokens <= defer_max) {
// defer the catch-up rows; the first draft decode absorbs them, which saves
// one eval per round. Flush first if rows from an undrafted round remain.
if (!defer.tok.empty() && (int32_t) defer.tok.size() + n_tokens > defer_max) {
if (!flush_deferred()) {
return false;
}
}

const size_t k0 = defer.tok.size();
defer.tok.resize(k0 + n_tokens);
defer.pos.resize(k0 + n_tokens);
defer.seq.resize(k0 + n_tokens);
defer.embd.resize((k0 + n_tokens) * (size_t) n_embd);

const float * h_tgt = llama_get_embeddings_nextn(ctx_tgt);
for (int k = 0; k < n_tokens; ++k) {
defer.tok[k0 + k] = batch_in.token[k];
defer.pos[k0 + k] = batch_in.pos[k];
defer.seq[k0 + k] = batch_in.seq_id[k][0];
// shift the tgt embeddings to the right by one position (see the non-deferred path)
if (k > 0) {
std::memcpy(defer.embd.data() + (k0 + k) * (size_t) n_embd, h_tgt + (size_t) (k - 1) * n_embd, row_bytes);
}
}
for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) {
if (i_batch_beg[seq_id] < 0) {
continue;
}
std::memcpy(defer.embd.data() + (k0 + i_batch_beg[seq_id]) * (size_t) n_embd, pending_h[seq_id].data(), row_bytes);
}
} else if (!is_mem_shared) {
if (!flush_deferred()) {
return false;
}

common_batch_clear(batch);

for (int k = 0; k < n_tokens; ++k) {
Expand Down Expand Up @@ -1553,6 +1684,124 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {

const size_t row_bytes = (size_t) n_embd * sizeof(float);

bool any_drafting = false;
for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) {
any_drafting = any_drafting || dparams[seq_id].drafting;
}

// chained drafting: one decode drafts n_max tokens for a single sequence.
// this block must run before the generic defer merge below - it consumes
// the deferred rows itself and prepends them to the chain batch
if (chain_graph && any_drafting) {
llama_seq_id seq_one = -1;
int n_seq_drafting = 0;
for (llama_seq_id s = 0; s < (llama_seq_id) n_seq; ++s) {
if (dparams[s].drafting) {
n_seq_drafting++;
seq_one = s;
}
}

if (n_seq_drafting == 1) {
auto & dp = dparams[seq_one];
auto * smpl = smpls[seq_one].get();
common_sampler_reset(smpl);

// dp.n_max caps the round so the draft fits the remaining context
const int n_chain = dp.n_max > 0 ? std::min(params.n_max, dp.n_max) : params.n_max;

// deferred rows at or past n_past hold candidates the verify
// rejected; the committed prefix ends at n_past - 1. Drop them.
drop_deferred_from(seq_one, dp.n_past);

// rows of other sequences cannot join a single-sequence chain
// batch; decode all remaining rows standalone in that case
bool defer_other = false;
for (size_t k = 0; k < defer.tok.size(); ++k) {
defer_other = defer_other || defer.seq[k] != seq_one;
}
if (defer_other && !flush_deferred()) {
return;
}

common_batch_clear(batch);

// deferred catch-up rows ride along, before the chain rows
const int n_catchup = (int) defer.tok.size();
for (int k = 0; k < n_catchup; ++k) {
common_batch_add(batch, defer.tok[k], defer.pos[k], { defer.seq[k] }, false);
std::memcpy(batch.embd + (size_t) k * n_embd, defer.embd.data() + (size_t) k * n_embd, row_bytes);
}
defer.tok.clear();
defer.pos.clear();
defer.seq.clear();
defer.embd.clear();

for (int j = 0; j < n_chain; ++j) {
common_batch_add(batch, j == 0 ? dp.id_last : 0, dp.n_past + j, { seq_one }, true);
if (j == 0) {
std::memcpy(batch.embd + (size_t) n_catchup * n_embd, pending_h[seq_one].data(), row_bytes);
} else {
std::memset(batch.embd + (size_t) (n_catchup + j) * n_embd, 0, row_bytes);
}
}

llama_set_mtp_chain(ctx_dft, true);
const int ret = llama_decode(ctx_dft, batch);
llama_set_mtp_chain(ctx_dft, false);

if (ret != 0) {
SPC_ERR("llama_decode(chain) returned %d\n", ret);
return;
}

// the chain samples greedily in-graph and emits [token id, top prob]
// pairs as 2-float rows, packed from the start of the logits buffer;
// no host-side sampling pass runs over the draft logits
const float * lp = llama_get_logits(ctx_dft);

auto & result = *dp.result;
for (int j = 0; j < n_chain; ++j) {
const llama_token id = (llama_token) lp[2*j + 0];
const float p = lp[2*j + 1];

SPC_DBG(" - seq_id %d, chain candidate %3d: %6d (%8.3f) '%s'\n",
seq_one, j, id, p,
common_token_to_piece(ctx_dft, id).c_str());

if (p < params.p_min) {
break;
}

result.push_back(id);

if ((int) result.size() >= params.n_max) {
break;
}
}

if (dp.result->size() < (size_t) params.n_min) {
dp.result->clear();
}
return;
}
}

// deferred catch-up rows ride along with the first draft decode. They carry
// earlier positions, so they must precede the draft rows: recurrent state
// advances in batch order. If nothing drafts this round they stay deferred
// and process() flushes them later.
if (any_drafting && !defer.tok.empty()) {
for (size_t k = 0; k < defer.tok.size(); ++k) {
common_batch_add(batch, defer.tok[k], defer.pos[k], { defer.seq[k] }, false);
std::memcpy(batch.embd + (size_t) (batch.n_tokens - 1) * n_embd, defer.embd.data() + k * (size_t) n_embd, row_bytes);
}
defer.tok.clear();
defer.pos.clear();
defer.seq.clear();
defer.embd.clear();
}

for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) {
auto & dp = dparams[seq_id];

Expand Down Expand Up @@ -2349,6 +2598,12 @@ common_params common_base_params_to_speculative(const common_params & params) {
}
}

// chained MTP drafting outputs logits for every chain step in one decode
if (getenv("LLAMA_SPEC_CHAIN") != nullptr) {
const int32_t per_seq = std::max(1, params_spec.n_max);
result.n_outputs_max = std::max(result.n_outputs_max, params.n_parallel * per_seq);
}

return result;
}

Expand Down
Loading