diff --git a/.github/workflows/build-cpu.yml b/.github/workflows/build-cpu.yml index b62fe55d6c5f..690e371142ed 100644 --- a/.github/workflows/build-cpu.yml +++ b/.github/workflows/build-cpu.yml @@ -43,6 +43,9 @@ env: jobs: build-cmake-pkg: + # its runner is self-hosted ([self-hosted, Linux]) and exists only in ggml-org's repo; in a fork it + # queues for 24 h and is cancelled, which a merge gate reads as a failure + if: github.repository == 'ggml-org/llama.cpp' uses: ./.github/workflows/build-cmake-pkg.yml ubuntu: diff --git a/common/build-info.h b/common/build-info.h index 1e564591a612..a1e8796c5812 100644 --- a/common/build-info.h +++ b/common/build-info.h @@ -1,5 +1,7 @@ #pragma once +#include + int llama_build_number(void); const char * llama_commit(void); diff --git a/common/common.h b/common/common.h index 1fb343b93eca..8b6f12f3f802 100644 --- a/common/common.h +++ b/common/common.h @@ -8,6 +8,7 @@ #include "ggml.h" #include "llama.h" +#include #include #include #include diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index 6999ef7d6706..5c50ece6b6b7 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -5,6 +5,9 @@ // for converting from JSON to jinja values #include "json.h" +#include +#include +#include #include #include #include diff --git a/common/reasoning-budget.cpp b/common/reasoning-budget.cpp index 4884299f3011..2b4ca880cd85 100644 --- a/common/reasoning-budget.cpp +++ b/common/reasoning-budget.cpp @@ -135,6 +135,19 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to { // track the end sequence within forced_tokens so it is also reported on DONE const int32_t match = ctx->end_matcher.advance(token); + if (match >= 0 && (ctx->force_pos >= ctx->forced_tokens.size() || token != ctx->forced_tokens[ctx->force_pos])) { + // the reasoning already ended (a prefilled ``): nothing is left to force + ctx->state = REASONING_BUDGET_DONE; + ctx->end_match = match; + COM_TRC("%s", "deactivated (natural end while forcing)\n"); + break; + } + if (ctx->force_pos >= ctx->forced_tokens.size() || token != ctx->forced_tokens[ctx->force_pos]) { + // only the forced token advances the forced end: a prefilled token accepted in + // FORCING (budget 0 with the generation prompt's `\n`) is not a forced token, + // and consuming one reached DONE without ever sampling the end tag + break; + } ctx->force_pos++; if (ctx->force_pos >= ctx->forced_tokens.size()) { ctx->state = REASONING_BUDGET_DONE; diff --git a/ggml/src/gguf.cpp b/ggml/src/gguf.cpp index 6c7b5817812b..63363ba784d8 100644 --- a/ggml/src/gguf.cpp +++ b/ggml/src/gguf.cpp @@ -3,6 +3,7 @@ #include "ggml-impl.h" #include "gguf.h" +#include #include #include #include diff --git a/src/llama-adapter.cpp b/src/llama-adapter.cpp index a6ac5596353b..ed002c6abc55 100644 --- a/src/llama-adapter.cpp +++ b/src/llama-adapter.cpp @@ -4,6 +4,8 @@ #include "llama-mmap.h" #include "llama-model.h" +#include +#include #include #include #include diff --git a/src/llama-graph.h b/src/llama-graph.h index 4c7f60bd8137..05cc00f37d27 100644 --- a/src/llama-graph.h +++ b/src/llama-graph.h @@ -5,6 +5,7 @@ #include "llama-hparams.h" #include "llama-adapter.h" +#include #include #include #include diff --git a/src/llama-mmap.cpp b/src/llama-mmap.cpp index a8d26a420d91..7fee61ebf75c 100644 --- a/src/llama-mmap.cpp +++ b/src/llama-mmap.cpp @@ -4,6 +4,7 @@ #include "ggml.h" +#include #include #include #include diff --git a/src/llama-vocab.cpp b/src/llama-vocab.cpp index ff926ceecd17..52fa166b4928 100644 --- a/src/llama-vocab.cpp +++ b/src/llama-vocab.cpp @@ -7,6 +7,7 @@ #include "unicode.h" +#include #include #include #include diff --git a/tests/test-reasoning-budget.cpp b/tests/test-reasoning-budget.cpp index 3bcc77e1733c..b072585fb3e3 100644 --- a/tests/test-reasoning-budget.cpp +++ b/tests/test-reasoning-budget.cpp @@ -335,6 +335,68 @@ static void test_reasoning_budget_end_match() { // UTF-8 boundary detection unit test // Tests common_utf8_is_complete() from reasoning-budget.h +// what this catches: budget 0 with the generation prompt's `\n` prefilled. The prefilled start +// moves IDLE -> FORCING, then the prefilled newline was accepted IN FORCING and advanced the forced +// end, so the sampler reached DONE without ever sampling the end tag and the model thought to its +// cap (Qwen3.5-0.8B on 0d7dda6ae, 2026-11-06: reasoning_budget_tokens 0 thought 400 of 400 tokens, +// 1 was bounded). A prefilled token is not a forced token; a prefill that already closed the think +// leaves nothing to force. +static void test_reasoning_budget_prefill_while_forcing() { + const std::vector start = {100}; + const std::vector end = {101}; + + // budget 0, end tag alone forced: the prefilled newline must not consume it + { + auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, {101}, 0, REASONING_BUDGET_IDLE); + llama_sampler_accept(sampler, 100); // prefilled : FORCING + llama_sampler_accept(sampler, 50); // prefilled newline + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_FORCING && "a prefilled token ended the forcing"); + GGML_ASSERT(get_forced_token(sampler, 101) == 101 && "the end tag is not forced after the prefill"); + llama_sampler_accept(sampler, 101); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE); + llama_sampler_free(sampler); + } + + // budget 0, a message then the end tag: both are forced in order after the prefill + { + auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, {102, 101}, 0, REASONING_BUDGET_IDLE); + llama_sampler_accept(sampler, 100); + llama_sampler_accept(sampler, 50); + GGML_ASSERT(get_forced_token(sampler, 102) == 102 && "the forced message was skipped by the prefill"); + llama_sampler_accept(sampler, 102); + GGML_ASSERT(get_forced_token(sampler, 102) == 101); + llama_sampler_accept(sampler, 101); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE); + GGML_ASSERT(common_reasoning_budget_get_end_match(sampler) != nullptr && "the forced end tag is not reported"); + llama_sampler_free(sampler); + } + + // budget 0, the prefill already closed the think (`\n\n`): nothing is left to force + { + auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, {102, 101}, 0, REASONING_BUDGET_IDLE); + llama_sampler_accept(sampler, 100); + llama_sampler_accept(sampler, 50); + llama_sampler_accept(sampler, 101); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE && "a closed think was forced again"); + GGML_ASSERT(common_reasoning_budget_get_end_match(sampler) != nullptr); + llama_sampler_free(sampler); + } + + // budget 1 and N: the prefilled newline is counted as before, then the end is forced + for (const int32_t budget : {1, 3}) { + auto * sampler = common_reasoning_budget_init(nullptr, {start}, {end}, {101}, budget, REASONING_BUDGET_IDLE); + llama_sampler_accept(sampler, 100); + for (int32_t i = 0; i < budget; i++) { + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_COUNTING); + llama_sampler_accept(sampler, 50 + i); + } + GGML_ASSERT(get_forced_token(sampler, 101) == 101 && "the budget did not force the end tag"); + llama_sampler_accept(sampler, 101); + GGML_ASSERT(common_reasoning_budget_get_state(sampler) == REASONING_BUDGET_DONE); + llama_sampler_free(sampler); + } +} + static void test_utf8_boundary_detection() { // Complete sequences GGML_ASSERT(common_utf8_is_complete("hello")); @@ -379,11 +441,12 @@ int main(void) { // Flow: i=0 apply()->passthrough, accept(100)->COUNTING; i=1 accept(50)->remaining=1 // i=2 accept(51)->remaining=0->FORCING; i=3 apply() forces token[0]; i=4 apply() forces token[1] // At i=4, accept() advances force_pos to 2 which equals forced_tokens.size(), so state becomes DONE + // (forced positions are sampled as the forced tokens, as the masked logits make them) { const std::vector start = {100}; const std::vector end = {101}; const std::vector forced = {102, 101}; // forced message + end - const std::vector sequence = {100, 50, 51, 52, 53}; // start + 4 tokens (budget=2) + const std::vector sequence = {100, 50, 51, 102, 101}; // start + 2 tokens (budget=2) + forced test_reasoning_budget("budget exhausted forcing", sequence, {start}, {end}, forced, 2, // budget of 2 tokens @@ -393,18 +456,19 @@ int main(void) { } // Test 3: Activate immediately with budget=0, forcing should start right away - // Flow: init promotes COUNTING+budget=0 to FORCING, so apply() sees FORCING at i=0 + // Flow: init promotes COUNTING+budget=0 to FORCING, so apply() sees FORCING at i=0; the start + // token accepted at i=0 is not a forced token and does not advance the forced end { const std::vector start = {100}; const std::vector end = {101}; const std::vector forced = {102, 101}; - const std::vector sequence = {100, 50, 51, 52}; // start token first, then 3 tokens + const std::vector sequence = {100, 102, 101, 52}; // start token, the forced end, one more test_reasoning_budget("activate immediately budget=0", sequence, {start}, {end}, forced, 0, // budget of 0 tokens REASONING_BUDGET_COUNTING, // starts counting, promoted to FORCING since budget=0 0, // forcing starts at i=0 (initialized in FORCING, apply forces immediately) - 1); // forcing continues through i=1 (accept at i=1 transitions to DONE) + 2); // forcing continues through i=2 (accept of the last forced token at i=2 transitions to DONE) } // Test 4: No start/end tokens configured - passthrough (no forcing) @@ -494,8 +558,9 @@ int main(void) { test_reasoning_budget_clone_mid_forcing(); test_reasoning_budget_force_manual(); test_reasoning_budget_end_match(); + test_reasoning_budget_prefill_while_forcing(); - printf("OK (12 tests passed)\n"); + printf("OK (13 tests passed)\n"); printf("Testing UTF-8 boundary detection... "); test_utf8_boundary_detection();