/src/llama.cpp/common/reasoning-budget.cpp
Line | Count | Source |
1 | | #include "reasoning-budget.h" |
2 | | #include "common.h" |
3 | | #include "trie.h" |
4 | | #include "unicode.h" |
5 | | |
6 | | #include "log.h" |
7 | | |
8 | | #include <algorithm> |
9 | | #include <cmath> |
10 | | #include <cstdint> |
11 | | #include <string> |
12 | | #include <vector> |
13 | | |
14 | | struct token_matcher { |
15 | | std::vector<llama_tokens> seqs; |
16 | | common_aho_corasick ac; |
17 | | size_t state = 0; |
18 | | |
19 | 0 | token_matcher(const std::vector<llama_tokens> & seqs) : seqs(collect(seqs)), ac(build_trie(this->seqs)) {} |
20 | | |
21 | 0 | static std::vector<llama_tokens> collect(const std::vector<llama_tokens> & seqs) { |
22 | 0 | std::vector<llama_tokens> res; |
23 | 0 | for (const auto & seq : seqs) { |
24 | 0 | if (!seq.empty() && std::find(res.begin(), res.end(), seq) == res.end()) { |
25 | 0 | res.push_back(seq); |
26 | 0 | } |
27 | 0 | } |
28 | 0 | return res; |
29 | 0 | } |
30 | | |
31 | 0 | static common_trie build_trie(const std::vector<llama_tokens> & seqs) { |
32 | 0 | common_trie t; |
33 | 0 | for (const auto & seq : seqs) { |
34 | 0 | t.insert(std::vector<uint32_t>(seq.begin(), seq.end())); |
35 | 0 | } |
36 | 0 | return t; |
37 | 0 | } |
38 | | |
39 | | // returns the index into seqs of the longest sequence ending at this token, or -1 |
40 | 0 | int32_t advance(llama_token token) { |
41 | 0 | state = ac.next(state, (uint32_t) token); |
42 | 0 | const int32_t p = ac.match_pattern(state); |
43 | 0 | if (p >= 0) { |
44 | 0 | state = 0; |
45 | 0 | } |
46 | 0 | return p; |
47 | 0 | } |
48 | | |
49 | 0 | void reset() { state = 0; } |
50 | | }; |
51 | | |
52 | | struct common_reasoning_budget_ctx { |
53 | | const llama_vocab * vocab; |
54 | | |
55 | | token_matcher start_matcher; |
56 | | token_matcher end_matcher; |
57 | | llama_tokens forced_tokens; |
58 | | |
59 | | int32_t budget; // maximum tokens in reasoning block |
60 | | int32_t remaining; // tokens remaining in budget |
61 | | |
62 | | common_reasoning_budget_state state; |
63 | | |
64 | | // for forcing |
65 | | size_t force_pos; // next position in forced_tokens to force |
66 | | |
67 | | int32_t end_match; // index into end_matcher.seqs of the sequence that transitioned to DONE, -1 if none |
68 | | }; |
69 | | |
70 | 0 | static const char * common_reasoning_budget_name(const struct llama_sampler * /*smpl*/) { |
71 | 0 | return "reasoning-budget"; |
72 | 0 | } |
73 | | |
74 | 0 | static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_token token) { |
75 | 0 | auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx; |
76 | |
|
77 | 0 | switch (ctx->state) { |
78 | 0 | case REASONING_BUDGET_IDLE: |
79 | 0 | { |
80 | 0 | if (ctx->start_matcher.advance(token) >= 0) { |
81 | 0 | ctx->state = REASONING_BUDGET_COUNTING; |
82 | 0 | ctx->remaining = ctx->budget; |
83 | 0 | COM_TRC("activated, budget=%d tokens\n", ctx->budget); |
84 | |
|
85 | 0 | if (ctx->remaining <= 0) { |
86 | 0 | ctx->state = REASONING_BUDGET_FORCING; |
87 | 0 | ctx->force_pos = 0; |
88 | 0 | COM_TRC("%s", "budget=0, forcing immediately\n"); |
89 | 0 | } |
90 | 0 | } |
91 | 0 | break; |
92 | 0 | } |
93 | 0 | case REASONING_BUDGET_COUNTING: |
94 | 0 | case REASONING_BUDGET_WAITING_UTF8: |
95 | 0 | { |
96 | 0 | const int32_t match = ctx->end_matcher.advance(token); |
97 | 0 | if (match >= 0) { |
98 | 0 | ctx->state = REASONING_BUDGET_DONE; |
99 | 0 | ctx->end_match = match; |
100 | 0 | COM_TRC("%s", "deactivated (natural end)\n"); |
101 | 0 | break; |
102 | 0 | } |
103 | | |
104 | 0 | bool utf8_complete = true; |
105 | 0 | if (ctx->vocab != nullptr) { |
106 | 0 | const std::string piece = common_token_to_piece(ctx->vocab, token, false); |
107 | 0 | utf8_complete = common_utf8_is_complete(piece); |
108 | 0 | } |
109 | |
|
110 | 0 | if (ctx->state == REASONING_BUDGET_WAITING_UTF8) { |
111 | 0 | if (utf8_complete) { |
112 | 0 | ctx->state = REASONING_BUDGET_FORCING; |
113 | 0 | ctx->force_pos = 0; |
114 | 0 | ctx->end_matcher.reset(); |
115 | 0 | COM_TRC("%s", "UTF-8 complete, now forcing end sequence\n"); |
116 | 0 | } |
117 | 0 | } else if (ctx->state == REASONING_BUDGET_COUNTING) { |
118 | 0 | ctx->remaining--; |
119 | 0 | if (ctx->remaining <= 0) { |
120 | 0 | if (utf8_complete) { |
121 | 0 | ctx->state = REASONING_BUDGET_FORCING; |
122 | 0 | ctx->force_pos = 0; |
123 | 0 | ctx->end_matcher.reset(); |
124 | 0 | COM_TRC("%s", "budget exhausted, forcing end sequence\n"); |
125 | 0 | } else { |
126 | 0 | ctx->state = REASONING_BUDGET_WAITING_UTF8; |
127 | 0 | ctx->end_matcher.reset(); |
128 | 0 | COM_TRC("%s", "budget exhausted, waiting for UTF-8 completion\n"); |
129 | 0 | } |
130 | 0 | } |
131 | 0 | } |
132 | 0 | break; |
133 | 0 | } |
134 | 0 | case REASONING_BUDGET_FORCING: |
135 | 0 | { |
136 | | // track the end sequence within forced_tokens so it is also reported on DONE |
137 | 0 | const int32_t match = ctx->end_matcher.advance(token); |
138 | 0 | ctx->force_pos++; |
139 | 0 | if (ctx->force_pos >= ctx->forced_tokens.size()) { |
140 | 0 | ctx->state = REASONING_BUDGET_DONE; |
141 | 0 | ctx->end_match = match; |
142 | 0 | COM_TRC("%s", "forced sequence complete, done\n"); |
143 | 0 | } |
144 | 0 | break; |
145 | 0 | } |
146 | 0 | case REASONING_BUDGET_DONE: |
147 | | // Re-arm on a new start tag: some models emit multiple <think> blocks |
148 | | // per response, and each should get a fresh budget window. |
149 | 0 | if (ctx->start_matcher.advance(token) >= 0) { |
150 | 0 | ctx->state = REASONING_BUDGET_COUNTING; |
151 | 0 | ctx->remaining = ctx->budget; |
152 | 0 | ctx->end_matcher.reset(); |
153 | 0 | ctx->end_match = -1; |
154 | 0 | COM_TRC("re-activated on new start tag, budget=%d tokens\n", ctx->budget); |
155 | |
|
156 | 0 | if (ctx->remaining <= 0) { |
157 | 0 | ctx->state = REASONING_BUDGET_FORCING; |
158 | 0 | ctx->force_pos = 0; |
159 | 0 | COM_TRC("%s", "budget=0, forcing immediately\n"); |
160 | 0 | } |
161 | 0 | } |
162 | 0 | break; |
163 | 0 | } |
164 | 0 | } |
165 | | |
166 | 0 | static void common_reasoning_budget_apply(struct llama_sampler * smpl, llama_token_data_array * cur_p) { |
167 | 0 | auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx; |
168 | |
|
169 | 0 | if (ctx->state != REASONING_BUDGET_FORCING) { |
170 | | // passthrough — don't modify logits |
171 | 0 | return; |
172 | 0 | } |
173 | | |
174 | 0 | if (ctx->force_pos >= ctx->forced_tokens.size()) { |
175 | 0 | return; |
176 | 0 | } |
177 | | |
178 | 0 | const llama_token forced = ctx->forced_tokens[ctx->force_pos]; |
179 | | |
180 | | // set all logits to -inf except the forced token |
181 | 0 | for (size_t i = 0; i < cur_p->size; i++) { |
182 | 0 | if (cur_p->data[i].id != forced) { |
183 | 0 | cur_p->data[i].logit = -INFINITY; |
184 | 0 | } |
185 | 0 | } |
186 | 0 | } |
187 | | |
188 | 0 | static void common_reasoning_budget_reset(struct llama_sampler * smpl) { |
189 | 0 | auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx; |
190 | 0 | ctx->state = REASONING_BUDGET_IDLE; |
191 | 0 | ctx->remaining = ctx->budget; |
192 | 0 | ctx->start_matcher.reset(); |
193 | 0 | ctx->end_matcher.reset(); |
194 | 0 | ctx->force_pos = 0; |
195 | 0 | ctx->end_match = -1; |
196 | 0 | } |
197 | | |
198 | | static struct llama_sampler * common_reasoning_budget_init_state( |
199 | | const struct llama_vocab * vocab, const std::vector<llama_tokens> & start_seqs, |
200 | | const std::vector<llama_tokens> & end_seqs, const llama_tokens & forced_tokens, |
201 | | int32_t budget, common_reasoning_budget_state initial_state); |
202 | | |
203 | | static struct llama_sampler * common_reasoning_budget_clone(const struct llama_sampler * smpl); |
204 | | |
205 | 0 | static void common_reasoning_budget_free(struct llama_sampler * smpl) { |
206 | 0 | delete (common_reasoning_budget_ctx *) smpl->ctx; |
207 | 0 | } |
208 | | |
209 | | static struct llama_sampler_i common_reasoning_budget_i = { |
210 | | /* .name = */ common_reasoning_budget_name, |
211 | | /* .accept = */ common_reasoning_budget_accept, |
212 | | /* .apply = */ common_reasoning_budget_apply, |
213 | | /* .reset = */ common_reasoning_budget_reset, |
214 | | /* .clone = */ common_reasoning_budget_clone, |
215 | | /* .free = */ common_reasoning_budget_free, |
216 | | /* .backend_init = */ nullptr, |
217 | | /* .backend_accept = */ nullptr, |
218 | | /* .backend_apply = */ nullptr, |
219 | | /* .backend_set_input = */ nullptr, |
220 | | /* .backend_reset = */ nullptr, |
221 | | /* .copy_state = */ nullptr, |
222 | | }; |
223 | | |
224 | 0 | static struct llama_sampler * common_reasoning_budget_clone(const struct llama_sampler * smpl) { |
225 | 0 | const auto * ctx = (const common_reasoning_budget_ctx *) smpl->ctx; |
226 | |
|
227 | 0 | return llama_sampler_init( |
228 | 0 | /* .iface = */ &common_reasoning_budget_i, |
229 | 0 | /* .ctx = */ new common_reasoning_budget_ctx(*ctx) |
230 | 0 | ); |
231 | 0 | } |
232 | | |
233 | | static struct llama_sampler * common_reasoning_budget_init_state( |
234 | | const struct llama_vocab * vocab, |
235 | | const std::vector<llama_tokens> & start_seqs, |
236 | | const std::vector<llama_tokens> & end_seqs, |
237 | | const llama_tokens & forced_tokens, |
238 | | int32_t budget, |
239 | 0 | common_reasoning_budget_state initial_state) { |
240 | | // promote COUNTING with budget <= 0 to FORCING |
241 | 0 | if (initial_state == REASONING_BUDGET_COUNTING && budget <= 0) { |
242 | 0 | initial_state = REASONING_BUDGET_FORCING; |
243 | 0 | } |
244 | |
|
245 | 0 | return llama_sampler_init( |
246 | 0 | /* .iface = */ &common_reasoning_budget_i, |
247 | 0 | /* .ctx = */ new common_reasoning_budget_ctx { |
248 | 0 | /* .vocab = */ vocab, |
249 | 0 | /* .start_matcher = */ token_matcher(start_seqs), |
250 | 0 | /* .end_matcher = */ token_matcher(end_seqs), |
251 | 0 | /* .forced_tokens = */ forced_tokens, |
252 | 0 | /* .budget = */ budget, |
253 | 0 | /* .remaining = */ budget, |
254 | 0 | /* .state = */ initial_state, |
255 | 0 | /* .force_pos = */ 0, |
256 | 0 | /* .end_match = */ -1, |
257 | 0 | } |
258 | 0 | ); |
259 | 0 | } |
260 | | |
261 | | struct llama_sampler * common_reasoning_budget_init( |
262 | | const struct llama_vocab * vocab, |
263 | | const std::vector<llama_tokens> & start_seqs, |
264 | | const std::vector<llama_tokens> & end_seqs, |
265 | | const llama_tokens & forced_tokens, |
266 | | int32_t budget, |
267 | 0 | common_reasoning_budget_state initial_state) { |
268 | 0 | return common_reasoning_budget_init_state(vocab, start_seqs, end_seqs, forced_tokens, budget, initial_state); |
269 | 0 | } |
270 | | |
271 | 0 | common_reasoning_budget_state common_reasoning_budget_get_state(const struct llama_sampler * smpl) { |
272 | 0 | if (!smpl) { |
273 | 0 | return REASONING_BUDGET_IDLE; |
274 | 0 | } |
275 | 0 | return ((const common_reasoning_budget_ctx *)smpl->ctx)->state; |
276 | 0 | } |
277 | | |
278 | 0 | const llama_tokens * common_reasoning_budget_get_end_match(const struct llama_sampler * smpl) { |
279 | 0 | if (!smpl) { |
280 | 0 | return nullptr; |
281 | 0 | } |
282 | | |
283 | 0 | const auto * ctx = (const common_reasoning_budget_ctx *) smpl->ctx; |
284 | 0 | if (ctx->end_match < 0) { |
285 | 0 | return nullptr; |
286 | 0 | } |
287 | | |
288 | 0 | return &ctx->end_matcher.seqs[ctx->end_match]; |
289 | 0 | } |
290 | | |
291 | 0 | bool common_reasoning_budget_force(struct llama_sampler * smpl) { |
292 | 0 | if (!smpl) { |
293 | 0 | return false; |
294 | 0 | } |
295 | | |
296 | 0 | auto * ctx = (common_reasoning_budget_ctx *) smpl->ctx; |
297 | | |
298 | | // only a sampler that is actively counting down the budget may be forced; |
299 | | // any other state (idle, already forcing/waiting, or done) is left untouched |
300 | 0 | if (ctx->state != REASONING_BUDGET_COUNTING) { |
301 | 0 | return false; |
302 | 0 | } |
303 | | |
304 | 0 | ctx->state = REASONING_BUDGET_FORCING; |
305 | 0 | ctx->force_pos = 0; |
306 | 0 | ctx->end_matcher.reset(); |
307 | 0 | COM_TRC("%s", "forced into forcing state (manual transition)\n"); |
308 | |
|
309 | 0 | return true; |
310 | 0 | } |