Skip to content

Commit 2bb9bdd

Browse files
authored
spec: Add benchmark-only synthetic speculative acceptance options (ggml-org#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
1 parent deae5ee commit 2bb9bdd

11 files changed

Lines changed: 427 additions & 17 deletions

File tree

common/arg.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4132,6 +4132,38 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
41324132
params.speculative.draft.n_min = value;
41334133
}
41344134
).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_LOOKUP, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_DRAFT_N_MIN"));
4135+
add_opt(common_arg(
4136+
{"--spec-synth-len"}, "L",
4137+
"target mean synthetic acceptance length, including the target token (benchmarking only)",
4138+
[](common_params & params, const std::string & value) {
4139+
const std::string text = string_strip(value);
4140+
size_t pos = 0;
4141+
const double length = std::stod(text, &pos);
4142+
if (pos != text.size() || length == -1.0) {
4143+
throw std::invalid_argument("invalid value");
4144+
}
4145+
params.speculative.synth_len = length;
4146+
}
4147+
).set_spec().set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_SYNTH_LEN"));
4148+
add_opt(common_arg(
4149+
{"--spec-synth-rates"}, "P0,P1,...",
4150+
"comma-separated unconditional per-position synthetic acceptance probabilities (benchmarking only)",
4151+
[](common_params & params, const std::string & value) {
4152+
const auto values = string_split<std::string>(value, ',');
4153+
std::vector<double> rates;
4154+
rates.reserve(values.size());
4155+
for (const auto & raw : values) {
4156+
const std::string text = string_strip(raw);
4157+
size_t pos = 0;
4158+
const double rate = std::stod(text, &pos);
4159+
if (pos != text.size()) {
4160+
throw std::invalid_argument("invalid value");
4161+
}
4162+
rates.push_back(rate);
4163+
}
4164+
params.speculative.synth_rates = std::move(rates);
4165+
}
4166+
).set_spec().set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_SYNTH_RATES"));
41354167

41364168
add_opt(common_arg(
41374169
{"--spec-draft-p-split", "--draft-p-split"}, "P",

common/common.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ struct common_params_speculative_ngram_cache {
370370
struct common_params_speculative {
371371
std::vector<enum common_speculative_type> types = { COMMON_SPECULATIVE_TYPE_NONE };
372372

373+
double synth_len = -1.0;
374+
std::vector<double> synth_rates;
375+
373376
// used by Simple, MTP, Eagle3, etc. - all methods that require some kind of draft model
374377
common_params_speculative_draft draft;
375378

@@ -384,6 +387,10 @@ struct common_params_speculative {
384387
return !draft.mparams.empty();
385388
}
386389

390+
bool has_synth() const {
391+
return synth_len != -1.0 || !synth_rates.empty();
392+
}
393+
387394
uint32_t need_n_rs_seq() const {
388395
bool needs_rs_seq = std::any_of(types.begin(), types.end(), [&](auto t) {
389396
return t == COMMON_SPECULATIVE_TYPE_DRAFT_MTP || t == COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3 || t == COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH || t == COMMON_SPECULATIVE_TYPE_DRAFT_DSPARK;

common/speculative.cpp

Lines changed: 142 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include <algorithm>
1616
#include <cassert>
17+
#include <cmath>
1718
#include <cstring>
1819
#include <iomanip>
1920
#include <map>
@@ -138,6 +139,7 @@ struct common_speculative_impl {
138139
const common_speculative_type type;
139140

140141
uint32_t n_seq;
142+
int32_t n_max; // maximum draft length after implementation-specific limits
141143

142144
size_t n_call_begin = 0; // number of times this implementation was called for refresh.
143145
size_t n_call_draft = 0; // number of times this implementation was called for generation.
@@ -157,7 +159,7 @@ struct common_speculative_impl {
157159
int64_t t_draft_us = 0; // total time spent in generating drafts in this implementation in microseconds.
158160
int64_t t_accept_us = 0; // total time spent in accumulation of this implementation in microseconds.
159161

160-
common_speculative_impl(common_speculative_type type, uint32_t n_seq) : type(type), n_seq(n_seq) {}
162+
common_speculative_impl(common_speculative_type type, uint32_t n_seq, int32_t n_max) : type(type), n_seq(n_seq), n_max(n_max) {}
161163

162164
virtual ~common_speculative_impl() = default;
163165

@@ -182,7 +184,7 @@ struct common_speculative_impl_draft_simple : public common_speculative_impl {
182184
std::vector<common_sampler_ptr> smpls;
183185

184186
common_speculative_impl_draft_simple(const common_params_speculative & params, uint32_t n_seq)
185-
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_SIMPLE, n_seq)
187+
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_SIMPLE, n_seq, params.draft.n_max)
186188
, params(params.draft)
187189
{
188190
auto * ctx_dft = this->params.ctx_dft;
@@ -452,7 +454,7 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl {
452454
std::vector<float> g_embd_buf;
453455

454456
common_speculative_impl_draft_eagle3(const common_params_speculative & params, uint32_t n_seq)
455-
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3, n_seq)
457+
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3, n_seq, params.draft.n_max)
456458
, params(params.draft)
457459
{
458460
SPC_TRC("%s", "adding speculative implementation 'draft-eagle3'\n");
@@ -937,7 +939,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
937939

938940
common_speculative_impl_draft_dflash(const common_params_speculative & params, uint32_t n_seq,
939941
common_speculative_type type = COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH)
940-
: common_speculative_impl(type, n_seq)
942+
: common_speculative_impl(type, n_seq, params.draft.n_max)
941943
, params(params.draft)
942944
, is_dspark(type == COMMON_SPECULATIVE_TYPE_DRAFT_DSPARK)
943945
{
@@ -983,6 +985,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
983985
this->params.n_max = std::min(this->params.n_max, n_draft_max);
984986
this->params.n_min = std::min(this->params.n_min, n_draft_max);
985987
}
988+
this->n_max = this->params.n_max;
986989

987990
batch = llama_batch_init(llama_n_batch(ctx_dft), 0, n_seq);
988991
batch_inject = llama_batch_init(llama_n_batch(ctx_dft), n_embd_dec, n_seq);
@@ -1315,7 +1318,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
13151318
std::vector<std::vector<float>> chain_h;
13161319

13171320
common_speculative_impl_draft_mtp(const common_params_speculative & params, uint32_t n_seq)
1318-
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_MTP, n_seq)
1321+
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_MTP, n_seq, params.draft.n_max)
13191322
, params(params.draft)
13201323
{
13211324
auto * ctx_tgt = this->params.ctx_tgt;
@@ -1382,6 +1385,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
13821385
c.reserve((size_t) (this->params.n_max + 1) * n_embd);
13831386
}
13841387
}
1388+
this->n_max = this->params.n_max;
13851389

13861390
pending_h.assign(n_seq, std::vector<float>(n_embd, 0.0f));
13871391

@@ -1726,7 +1730,7 @@ struct common_speculative_impl_ngram_simple : public common_speculative_impl {
17261730
common_speculative_impl_ngram_simple(
17271731
const common_params_speculative & params, uint32_t n_seq,
17281732
common_ngram_simple_config config)
1729-
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_SIMPLE, n_seq)
1733+
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_SIMPLE, n_seq, params.ngram_simple.size_m)
17301734
, params(params.ngram_simple)
17311735
, config(config)
17321736
{
@@ -1770,7 +1774,7 @@ struct common_speculative_impl_ngram_map_k : public common_speculative_impl {
17701774
const common_ngram_map & config,
17711775
uint32_t n_seq)
17721776
: common_speculative_impl(config.key_only ? COMMON_SPECULATIVE_TYPE_NGRAM_MAP_K
1773-
: COMMON_SPECULATIVE_TYPE_NGRAM_MAP_K4V, n_seq)
1777+
: COMMON_SPECULATIVE_TYPE_NGRAM_MAP_K4V, n_seq, config.size_value)
17741778
{
17751779
for (uint32_t i = 0; i < n_seq; i++) {
17761780
this->config.push_back(config);
@@ -1841,7 +1845,7 @@ struct common_speculative_impl_ngram_mod : public common_speculative_impl {
18411845
common_speculative_impl_ngram_mod(
18421846
const common_params_speculative & params,
18431847
uint32_t n_seq)
1844-
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_MOD, n_seq)
1848+
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_MOD, n_seq, params.ngram_mod.n_max)
18451849
, params(params.ngram_mod)
18461850
, mod(params.ngram_mod.n_match, 4*1024*1024)
18471851
, verbose(std::getenv("LLAMA_TRACE") != nullptr) {
@@ -2017,7 +2021,7 @@ struct common_speculative_impl_ngram_cache : public common_speculative_impl {
20172021
const std::string & path_dynamic,
20182022
bool save_dynamic,
20192023
bool save_static)
2020-
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_CACHE, n_seq)
2024+
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_CACHE, n_seq, n_draft)
20212025
, params(params.ngram_cache)
20222026
, n_draft(n_draft)
20232027
, save_dynamic(save_dynamic)
@@ -2138,6 +2142,8 @@ struct common_speculative {
21382142

21392143
// which implementaion was used for a given seq_id
21402144
std::vector<common_speculative_impl *> impl_last;
2145+
2146+
std::vector<double> synth_probs;
21412147
};
21422148

21432149
static common_ngram_map get_common_ngram_map(
@@ -2316,6 +2322,101 @@ int32_t common_speculative_n_max(const common_params_speculative * spec) {
23162322
return n_max;
23172323
}
23182324

2325+
int32_t common_speculative_n_max(const common_speculative * spec) {
2326+
int32_t n_max = 0;
2327+
2328+
if (spec == nullptr) {
2329+
return n_max;
2330+
}
2331+
2332+
for (const auto & impl : spec->impls) {
2333+
n_max = std::max(n_max, std::max(0, impl->n_max));
2334+
}
2335+
2336+
return n_max;
2337+
}
2338+
2339+
std::vector<double> common_speculative_synth_rates_resolve(const common_params_speculative * spec, int32_t n_max) {
2340+
const bool has_length = spec->synth_len != -1.0;
2341+
const bool has_rates = !spec->synth_rates.empty();
2342+
2343+
if (!has_length && !has_rates) {
2344+
return {};
2345+
}
2346+
if (has_length && has_rates) {
2347+
throw std::invalid_argument("synthetic acceptance length and rates are mutually exclusive");
2348+
}
2349+
2350+
if (n_max <= 0) {
2351+
throw std::invalid_argument("synthetic acceptance requires at least one speculative token");
2352+
}
2353+
2354+
if (has_rates) {
2355+
const auto & rates = spec->synth_rates;
2356+
if (rates.size() != (size_t) n_max) {
2357+
throw std::invalid_argument(string_format(
2358+
"synthetic acceptance rates must contain %d values, got %zu", n_max, rates.size()));
2359+
}
2360+
2361+
for (size_t i = 0; i < rates.size(); ++i) {
2362+
if (!std::isfinite(rates[i]) || rates[i] < 0.0 || rates[i] > 1.0) {
2363+
throw std::invalid_argument("synthetic acceptance rates must be finite and within [0, 1]");
2364+
}
2365+
if (i > 0 && rates[i] > rates[i - 1]) {
2366+
throw std::invalid_argument("synthetic acceptance rates must be monotonically non-increasing");
2367+
}
2368+
}
2369+
2370+
return rates;
2371+
}
2372+
2373+
const double length = spec->synth_len;
2374+
const double length_max = (double) n_max + 1.0;
2375+
if (!std::isfinite(length) || length < 1.0 || length > length_max) {
2376+
throw std::invalid_argument(string_format(
2377+
"synthetic acceptance length must be finite and within [1, %.0f]", length_max));
2378+
}
2379+
2380+
double p = 0.0;
2381+
if (length == length_max) {
2382+
p = 1.0;
2383+
} else if (length > 1.0) {
2384+
double p_min = 0.0;
2385+
double p_max = 1.0;
2386+
for (int i = 0; i < 32; ++i) {
2387+
const double p_mid = 0.5 * (p_min + p_max);
2388+
double sum = 0.0;
2389+
double term = p_mid;
2390+
for (int32_t j = 0; j < n_max; ++j) {
2391+
sum += term;
2392+
term *= p_mid;
2393+
}
2394+
2395+
if (sum < length - 1.0) {
2396+
p_min = p_mid;
2397+
} else {
2398+
p_max = p_mid;
2399+
}
2400+
}
2401+
p = 0.5 * (p_min + p_max);
2402+
}
2403+
2404+
std::vector<double> rates;
2405+
rates.reserve(n_max);
2406+
double rate = p;
2407+
for (int32_t i = 0; i < n_max; ++i) {
2408+
rates.push_back(rate);
2409+
rate *= p;
2410+
}
2411+
2412+
return rates;
2413+
}
2414+
2415+
const std::vector<double> & common_speculative_get_synth_probs(const common_speculative * spec) {
2416+
GGML_ASSERT(spec);
2417+
return spec->synth_probs;
2418+
}
2419+
23192420
common_params common_base_params_to_speculative(const common_params & params) {
23202421
const bool has_draft = params.speculative.has_dft();
23212422

@@ -2568,13 +2669,39 @@ common_speculative * common_speculative_init(common_params_speculative & params,
25682669
return nullptr;
25692670
}
25702671

2571-
auto * result = new common_speculative {
2572-
/* .dparams = */ common_speculative_draft_params_vec(n_seq),
2573-
/* .impls = */ std::move(impls),
2574-
/* .impl_last = */ std::vector<common_speculative_impl *>(n_seq, nullptr)
2575-
};
2672+
common_speculative_ptr result(new common_speculative {
2673+
/* .dparams = */ common_speculative_draft_params_vec(n_seq),
2674+
/* .impls = */ std::move(impls),
2675+
/* .impl_last = */ std::vector<common_speculative_impl *>(n_seq, nullptr),
2676+
/* .synth_probs = */ {},
2677+
});
25762678

2577-
return result;
2679+
const int32_t n_max_configured = common_speculative_n_max(&params);
2680+
const int32_t n_max_effective = common_speculative_n_max(result.get());
2681+
const auto rates = common_speculative_synth_rates_resolve(&params, n_max_effective);
2682+
2683+
std::vector<std::string> rates_str;
2684+
rates_str.reserve(rates.size());
2685+
result->synth_probs.reserve(rates.size());
2686+
double rate_prev = 1.0;
2687+
double acceptance_length = 1.0;
2688+
for (const double rate : rates) {
2689+
result->synth_probs.push_back(rate_prev > 0.0 ? rate / rate_prev : 0.0);
2690+
rates_str.push_back(string_format("%.6g", rate));
2691+
rate_prev = rate;
2692+
acceptance_length += rate;
2693+
}
2694+
if (!result->synth_probs.empty()) {
2695+
SPC_WRN("%s", "synthetic speculative acceptance is enabled for benchmarking; generated output is not valid\n");
2696+
if (n_max_effective != n_max_configured) {
2697+
SPC_WRN("synthetic acceptance draft limit was reduced from %d to %d by the initialized speculative implementations\n",
2698+
n_max_configured, n_max_effective);
2699+
}
2700+
SPC_INF("synthetic acceptance: n_max = %zu, mean length = %.6f, rates = [%s]\n",
2701+
rates.size(), acceptance_length, string_join(rates_str, ", ").c_str());
2702+
}
2703+
2704+
return result.release();
25782705
}
25792706

25802707
void common_speculative_free(common_speculative * spec) {

common/speculative.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ std::string common_speculative_type_to_str(enum common_speculative_type type);
2626
// return the max number of draft tokens based on the speculative parameters
2727
int32_t common_speculative_n_max(const common_params_speculative * spec);
2828

29+
// return the max number of draft tokens from the initialized implementations
30+
int32_t common_speculative_n_max(const common_speculative * spec);
31+
32+
// validate and resolve the unconditional synthetic acceptance rates
33+
std::vector<double> common_speculative_synth_rates_resolve(const common_params_speculative * spec, int32_t n_max);
34+
35+
// return the conditional synthetic acceptance probabilities
36+
const std::vector<double> & common_speculative_get_synth_probs(const common_speculative * spec);
37+
2938
common_params common_base_params_to_speculative(const common_params & params);
3039

3140
struct common_speculative_output_limits {

docs/speculative.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,15 @@ Use `--backend-sampling` to run supported target-model samplers on the model bac
212212

213213
Unsupported samplers and device layouts fall back to CPU sampling. Tensor split mode does not support backend sampling. A fixed seed produces repeatable random draws, but stochastic CPU and backend sampling can still select different tokens because floating-point operations can differ between implementations and devices. Use greedy sampling when exact output matching is required.
214214

215+
### Synthetic Acceptance
216+
217+
`llama-server` and `llama-cli` can replace normal speculative verification with synthetic decisions for benchmarking. The generated output is not valid model output because accepted draft tokens do not have to match the target model.
218+
219+
Use exactly one of these options:
220+
221+
- `--spec-synth-rates P0,P1,...` sets unconditional per-position acceptance probabilities. Entry `i` is the probability that the first `i+1` draft tokens are all accepted. The number of entries must match the effective maximum draft length. Values must be finite, within `[0, 1]`, and monotonically non-increasing.
222+
- `--spec-synth-len L` sets the target mean acceptance length, including the target token. For `K` maximum draft tokens, `L` must be within `[1, K+1]`. The server finds a constant conditional probability `p` such that `p + p^2 + ... + p^K = L - 1`, then uses unconditional rates `[p, p^2, ..., p^K]`.
223+
215224
### General Speculative Parameters
216225

217226
```

0 commit comments

Comments
 (0)