mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-07-26 20:51:01 -05:00
* common : extract trie/ac to a separate file * common : support multiple token sequences in the reasoning budget sampler * common/trie : return matched word index * common/trie : rename "word" to "pattern" * common/reasoning-budget : expose matched end sequence * common/sampling : replay end sequence when reasoning budget is done * cont : update to use multiple end sequences * cont : clean up
309 lines
11 KiB
C++
309 lines
11 KiB
C++
#include "reasoning-budget.h"
|
|
#include "common.h"
|
|
#include "trie.h"
|
|
#include "unicode.h"
|
|
|
|
#include "log.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
struct token_matcher {
|
|
std::vector<llama_tokens> seqs;
|
|
common_aho_corasick ac;
|
|
size_t state = 0;
|
|
|
|
token_matcher(const std::vector<llama_tokens> & seqs) : seqs(collect(seqs)), ac(build_trie(this->seqs)) {}
|
|
|
|
static std::vector<llama_tokens> collect(const std::vector<llama_tokens> & seqs) {
|
|
std::vector<llama_tokens> res;
|
|
for (const auto & seq : seqs) {
|
|
if (!seq.empty() && std::find(res.begin(), res.end(), seq) == res.end()) {
|
|
res.push_back(seq);
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static common_trie build_trie(const std::vector<llama_tokens> & seqs) {
|
|
common_trie t;
|
|
for (const auto & seq : seqs) {
|
|
t.insert(std::vector<uint32_t>(seq.begin(), seq.end()));
|
|
}
|
|
return t;
|
|
}
|
|
|
|
// returns the index into seqs of the longest sequence ending at this token, or -1
|
|
int32_t advance(llama_token token) {
|
|
state = ac.next(state, (uint32_t) token);
|
|
const int32_t p = ac.match_pattern(state);
|
|
if (p >= 0) {
|
|
state = 0;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
void reset() { state = 0; }
|
|
};
|
|
|
|
struct common_reasoning_budget_ctx {
|
|
const llama_vocab * vocab;
|
|
|
|
token_matcher start_matcher;
|
|
token_matcher end_matcher;
|
|
llama_tokens forced_tokens;
|
|
|
|
int32_t budget; // maximum tokens in reasoning block
|
|
int32_t remaining; // tokens remaining in budget
|
|
|
|
common_reasoning_budget_state state;
|
|
|
|
// for forcing
|
|
size_t force_pos; // next position in forced_tokens to force
|
|
|
|
int32_t end_match; // index into end_matcher.seqs of the sequence that transitioned to DONE, -1 if none
|
|
};
|
|
|
|
static const char * common_reasoning_budget_name(const struct llama_sampler * /*smpl*/) {
|
|
return "reasoning-budget";
|
|
}
|
|
|
|
static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_token token) {
|
|
auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx;
|
|
|
|
switch (ctx->state) {
|
|
case REASONING_BUDGET_IDLE:
|
|
{
|
|
if (ctx->start_matcher.advance(token) >= 0) {
|
|
ctx->state = REASONING_BUDGET_COUNTING;
|
|
ctx->remaining = ctx->budget;
|
|
COM_TRC("activated, budget=%d tokens\n", ctx->budget);
|
|
|
|
if (ctx->remaining <= 0) {
|
|
ctx->state = REASONING_BUDGET_FORCING;
|
|
ctx->force_pos = 0;
|
|
COM_TRC("%s", "budget=0, forcing immediately\n");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case REASONING_BUDGET_COUNTING:
|
|
case REASONING_BUDGET_WAITING_UTF8:
|
|
{
|
|
const int32_t match = ctx->end_matcher.advance(token);
|
|
if (match >= 0) {
|
|
ctx->state = REASONING_BUDGET_DONE;
|
|
ctx->end_match = match;
|
|
COM_TRC("%s", "deactivated (natural end)\n");
|
|
break;
|
|
}
|
|
|
|
bool utf8_complete = true;
|
|
if (ctx->vocab != nullptr) {
|
|
const std::string piece = common_token_to_piece(ctx->vocab, token, false);
|
|
utf8_complete = common_utf8_is_complete(piece);
|
|
}
|
|
|
|
if (ctx->state == REASONING_BUDGET_WAITING_UTF8) {
|
|
if (utf8_complete) {
|
|
ctx->state = REASONING_BUDGET_FORCING;
|
|
ctx->force_pos = 0;
|
|
ctx->end_matcher.reset();
|
|
COM_TRC("%s", "UTF-8 complete, now forcing end sequence\n");
|
|
}
|
|
} else if (ctx->state == REASONING_BUDGET_COUNTING) {
|
|
ctx->remaining--;
|
|
if (ctx->remaining <= 0) {
|
|
if (utf8_complete) {
|
|
ctx->state = REASONING_BUDGET_FORCING;
|
|
ctx->force_pos = 0;
|
|
ctx->end_matcher.reset();
|
|
COM_TRC("%s", "budget exhausted, forcing end sequence\n");
|
|
} else {
|
|
ctx->state = REASONING_BUDGET_WAITING_UTF8;
|
|
ctx->end_matcher.reset();
|
|
COM_TRC("%s", "budget exhausted, waiting for UTF-8 completion\n");
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case REASONING_BUDGET_FORCING:
|
|
{
|
|
// track the end sequence within forced_tokens so it is also reported on DONE
|
|
const int32_t match = ctx->end_matcher.advance(token);
|
|
ctx->force_pos++;
|
|
if (ctx->force_pos >= ctx->forced_tokens.size()) {
|
|
ctx->state = REASONING_BUDGET_DONE;
|
|
ctx->end_match = match;
|
|
COM_TRC("%s", "forced sequence complete, done\n");
|
|
}
|
|
break;
|
|
}
|
|
case REASONING_BUDGET_DONE:
|
|
// Re-arm on a new start tag: some models emit multiple <think> blocks
|
|
// per response, and each should get a fresh budget window.
|
|
if (ctx->start_matcher.advance(token) >= 0) {
|
|
ctx->state = REASONING_BUDGET_COUNTING;
|
|
ctx->remaining = ctx->budget;
|
|
ctx->end_matcher.reset();
|
|
ctx->end_match = -1;
|
|
COM_TRC("re-activated on new start tag, budget=%d tokens\n", ctx->budget);
|
|
|
|
if (ctx->remaining <= 0) {
|
|
ctx->state = REASONING_BUDGET_FORCING;
|
|
ctx->force_pos = 0;
|
|
COM_TRC("%s", "budget=0, forcing immediately\n");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void common_reasoning_budget_apply(struct llama_sampler * smpl, llama_token_data_array * cur_p) {
|
|
auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx;
|
|
|
|
if (ctx->state != REASONING_BUDGET_FORCING) {
|
|
// passthrough — don't modify logits
|
|
return;
|
|
}
|
|
|
|
if (ctx->force_pos >= ctx->forced_tokens.size()) {
|
|
return;
|
|
}
|
|
|
|
const llama_token forced = ctx->forced_tokens[ctx->force_pos];
|
|
|
|
// set all logits to -inf except the forced token
|
|
for (size_t i = 0; i < cur_p->size; i++) {
|
|
if (cur_p->data[i].id != forced) {
|
|
cur_p->data[i].logit = -INFINITY;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void common_reasoning_budget_reset(struct llama_sampler * smpl) {
|
|
auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx;
|
|
ctx->state = REASONING_BUDGET_IDLE;
|
|
ctx->remaining = ctx->budget;
|
|
ctx->start_matcher.reset();
|
|
ctx->end_matcher.reset();
|
|
ctx->force_pos = 0;
|
|
ctx->end_match = -1;
|
|
}
|
|
|
|
static struct llama_sampler * common_reasoning_budget_init_state(
|
|
const struct llama_vocab * vocab, const std::vector<llama_tokens> & start_seqs,
|
|
const std::vector<llama_tokens> & end_seqs, const llama_tokens & forced_tokens,
|
|
int32_t budget, common_reasoning_budget_state initial_state);
|
|
|
|
static struct llama_sampler * common_reasoning_budget_clone(const struct llama_sampler * smpl);
|
|
|
|
static void common_reasoning_budget_free(struct llama_sampler * smpl) {
|
|
delete (common_reasoning_budget_ctx *) smpl->ctx;
|
|
}
|
|
|
|
static struct llama_sampler_i common_reasoning_budget_i = {
|
|
/* .name = */ common_reasoning_budget_name,
|
|
/* .accept = */ common_reasoning_budget_accept,
|
|
/* .apply = */ common_reasoning_budget_apply,
|
|
/* .reset = */ common_reasoning_budget_reset,
|
|
/* .clone = */ common_reasoning_budget_clone,
|
|
/* .free = */ common_reasoning_budget_free,
|
|
/* .backend_init = */ nullptr,
|
|
/* .backend_accept = */ nullptr,
|
|
/* .backend_apply = */ nullptr,
|
|
/* .backend_set_input = */ nullptr,
|
|
};
|
|
|
|
static struct llama_sampler * common_reasoning_budget_clone(const struct llama_sampler * smpl) {
|
|
const auto * ctx = (const common_reasoning_budget_ctx *) smpl->ctx;
|
|
|
|
return llama_sampler_init(
|
|
/* .iface = */ &common_reasoning_budget_i,
|
|
/* .ctx = */ new common_reasoning_budget_ctx(*ctx)
|
|
);
|
|
}
|
|
|
|
static struct llama_sampler * common_reasoning_budget_init_state(
|
|
const struct llama_vocab * vocab,
|
|
const std::vector<llama_tokens> & start_seqs,
|
|
const std::vector<llama_tokens> & end_seqs,
|
|
const llama_tokens & forced_tokens,
|
|
int32_t budget,
|
|
common_reasoning_budget_state initial_state) {
|
|
// promote COUNTING with budget <= 0 to FORCING
|
|
if (initial_state == REASONING_BUDGET_COUNTING && budget <= 0) {
|
|
initial_state = REASONING_BUDGET_FORCING;
|
|
}
|
|
|
|
return llama_sampler_init(
|
|
/* .iface = */ &common_reasoning_budget_i,
|
|
/* .ctx = */ new common_reasoning_budget_ctx {
|
|
/* .vocab = */ vocab,
|
|
/* .start_matcher = */ token_matcher(start_seqs),
|
|
/* .end_matcher = */ token_matcher(end_seqs),
|
|
/* .forced_tokens = */ forced_tokens,
|
|
/* .budget = */ budget,
|
|
/* .remaining = */ budget,
|
|
/* .state = */ initial_state,
|
|
/* .force_pos = */ 0,
|
|
/* .end_match = */ -1,
|
|
}
|
|
);
|
|
}
|
|
|
|
struct llama_sampler * common_reasoning_budget_init(
|
|
const struct llama_vocab * vocab,
|
|
const std::vector<llama_tokens> & start_seqs,
|
|
const std::vector<llama_tokens> & end_seqs,
|
|
const llama_tokens & forced_tokens,
|
|
int32_t budget,
|
|
common_reasoning_budget_state initial_state) {
|
|
return common_reasoning_budget_init_state(vocab, start_seqs, end_seqs, forced_tokens, budget, initial_state);
|
|
}
|
|
|
|
common_reasoning_budget_state common_reasoning_budget_get_state(const struct llama_sampler * smpl) {
|
|
if (!smpl) {
|
|
return REASONING_BUDGET_IDLE;
|
|
}
|
|
return ((const common_reasoning_budget_ctx *)smpl->ctx)->state;
|
|
}
|
|
|
|
const llama_tokens * common_reasoning_budget_get_end_match(const struct llama_sampler * smpl) {
|
|
if (!smpl) {
|
|
return nullptr;
|
|
}
|
|
|
|
const auto * ctx = (const common_reasoning_budget_ctx *) smpl->ctx;
|
|
if (ctx->end_match < 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
return &ctx->end_matcher.seqs[ctx->end_match];
|
|
}
|
|
|
|
bool common_reasoning_budget_force(struct llama_sampler * smpl) {
|
|
if (!smpl) {
|
|
return false;
|
|
}
|
|
|
|
auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx;
|
|
|
|
// only a sampler that is actively counting down the budget may be forced;
|
|
// any other state (idle, already forcing/waiting, or done) is left untouched
|
|
if (ctx->state != REASONING_BUDGET_COUNTING) {
|
|
return false;
|
|
}
|
|
|
|
ctx->state = REASONING_BUDGET_FORCING;
|
|
ctx->force_pos = 0;
|
|
ctx->end_matcher.reset();
|
|
COM_TRC("%s", "forced into forcing state (manual transition)\n");
|
|
|
|
return true;
|
|
}
|