spec: Add benchmark-only synthetic speculative acceptance options (#27711)

* Add benchmark-only synthetic speculative acceptance to llama-server and llama-cli

* Address review comments

* Address review comments

* Add some comments in the code
This commit is contained in:
Gaurav Garg
2026-08-27 16:23:42 +05:30
committed by GitHub
parent deae5ee133
commit 2bb9bddafa
11 changed files with 427 additions and 17 deletions
+68 -2
View File
@@ -23,6 +23,7 @@
#include <exception>
#include <memory>
#include <filesystem>
#include <random>
#include <utility>
#include <fstream>
@@ -51,6 +52,50 @@ static common_speculative_output_limits server_output_limits(const common_params
return result;
}
// synthetic draft verification for benchmarking - accept draft tokens at random instead of by match with the target
// on replay the draft was already accepted before a context checkpoint restore, so repeat the same decisions
static std::vector<llama_token> server_sample_and_accept_synth(
common_sampler * smpl,
llama_context * ctx,
const std::vector<int32_t> & idxs,
const llama_tokens & draft,
const std::vector<double> & synth_probs,
std::mt19937 & rng,
bool is_replay) {
GGML_ASSERT(idxs.size() == draft.size() + 1);
GGML_ASSERT(synth_probs.size() >= draft.size());
std::vector<llama_token> result;
result.reserve(idxs.size());
const llama_vocab * vocab = llama_model_get_vocab(llama_get_model(ctx));
std::uniform_real_distribution<double> dist(0.0, 1.0);
for (size_t i = 0; i < draft.size(); ++i) {
const llama_token id = common_sampler_sample(smpl, ctx, idxs[i]);
const bool accept = is_replay || dist(rng) < synth_probs[i];
// do not accept a drafted EOG token - it would end the generation early
// on replay the last token is from the target and can be EOG, so skip this check
if (accept && (is_replay || !llama_vocab_is_eog(vocab, draft[i]))) {
// synthetic draft tokens do not advance grammar or reasoning state
// the last replay token is from the target and must advance both
const bool is_replay_target = is_replay && i + 1 == draft.size();
common_sampler_accept(smpl, draft[i], is_replay_target);
result.push_back(draft[i]);
continue;
}
common_sampler_accept(smpl, id, true);
result.push_back(id);
return result;
}
const llama_token id = common_sampler_sample(smpl, ctx, idxs[draft.size()]);
common_sampler_accept(smpl, id, true);
result.push_back(id);
return result;
}
// state diagram: https://github.com/ggml-org/llama.cpp/pull/9283
enum slot_state {
SLOT_STATE_IDLE,
@@ -211,6 +256,7 @@ struct server_slot {
std::vector<int32_t> spec_i_batch;
common_prompt_checkpoint spec_ckpt;
bool spec_is_replay = false;
std::mt19937 spec_synth_rng;
// TODO: move members that belong to the task (such as `generated_text`, `has_new_line`) to task_results_state
// see https://github.com/ggml-org/llama.cpp/pull/18283#issuecomment-3710175837
@@ -1194,6 +1240,9 @@ private:
spec.reset(common_speculative_init(params_base.speculative, params_base.n_parallel));
} catch (const std::exception & e) {
SRV_ERR("failed to initialize speculative decoding context: %s\n", e.what());
if (params_base.speculative.has_synth()) {
return false;
}
}
}
@@ -1209,6 +1258,11 @@ private:
model_dft = nullptr;
}
if (!spec && params_base.speculative.has_synth()) {
SRV_ERR("%s", "synthetic acceptance requires an initialized speculative decoding context\n");
return false;
}
for (int i = 0; i < params_base.n_parallel; i++) {
server_slot & slot = slots[i];
@@ -1717,6 +1771,13 @@ private:
SLT_TRC(slot, "sampler chain: %s\n", common_sampler_print(slot.smpl.get()).c_str());
SLT_TRC(slot, "sampler params: \n%s\n", task.params.sampling.print().c_str());
if (spec && !common_speculative_get_synth_probs(spec.get()).empty()) {
const uint32_t seed = task.params.sampling.seed == LLAMA_DEFAULT_SEED
? std::random_device{}()
: task.params.sampling.seed;
slot.spec_synth_rng.seed(seed);
}
} else {
slot.smpl.reset();
}
@@ -3802,7 +3863,12 @@ private:
common_sampler_ptr smpl_save(common_sampler_clone(slot.smpl.get()));
GGML_ASSERT(slot.spec_i_batch.size() == n_draft + 1);
auto accepted = common_sampler_sample_and_accept_n(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft);
const auto & synth_probs = common_speculative_get_synth_probs(spec.get());
auto accepted = synth_probs.empty()
? common_sampler_sample_and_accept_n(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft)
: server_sample_and_accept_synth(
slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft,
synth_probs, slot.spec_synth_rng, slot.spec_is_replay);
slot.spec_i_batch.clear();
GGML_ASSERT(accepted.size() >= 1);
@@ -3868,7 +3934,7 @@ private:
auto & n_accepted_per_pos = slot.n_accepted_per_pos;
if (n_accepted_per_pos.empty()) {
n_accepted_per_pos.resize(common_speculative_n_max(&params_base.speculative), 0);
n_accepted_per_pos.resize(common_speculative_n_max(spec.get()), 0);
}
for (size_t i = 0; i < n_accepted && i < n_accepted_per_pos.size(); ++i) {
n_accepted_per_pos[i]++;