Entropic 2.9.4
Local-first agentic inference engine
Loading...
Searching...
No Matches
llama_cpp_backend.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
16#include "llama_cpp_backend.h"
17#include "llama_cpp_sampler.h"
18#include "llama_cpp_tokenizer.h"
19#include "warm_keep_util.h" // gh#96: common_prefix_len / warm_keep_cut
20#include "tool_call_markers.h" // gh#103: family-aware tool-call close marker
21#include "batch_util.h" // gh#98: batch_shared_prefix_len / batch_is_viable
22#include "mtp_envelope.h" // gh#108: mtp_unsupported_reason (fail-loud envelope)
23
24#include <entropic/inference/adapters/adapter_base.h> // gh#90 coerce_string_typed_args
26
27#include <common.h>
28#include <chat.h>
29#include <sampling.h>
30#include <speculative.h>
31#include <mtmd.h>
32#include <mtmd-helper.h>
33
34#include <nlohmann/json.hpp>
35
36#include <cmath>
37#include <cstring>
38#include <optional>
39#include <stdexcept>
40
41namespace entropic {
42
43namespace {
44
45auto logger = entropic::log::get("inference.llama_cpp");
46
55bool ends_with(const std::string& text, const std::string& suffix) {
56 return text.size() >= suffix.size()
57 && text.compare(text.size() - suffix.size(), suffix.size(), suffix) == 0;
58}
59
68bool check_stop_sequences(
69 const std::string& text,
70 const std::vector<std::string>& stop_sequences)
71{
72 for (const auto& stop : stop_sequences) {
73 if (!stop.empty() && ends_with(text, stop)) {
74 return true;
75 }
76 }
77 return false;
78}
79
86GenerationResult prefill_error() {
87 GenerationResult r;
88 r.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
89 r.error_message = "Prefill decode failed";
90 r.finish_reason = "error";
91 return r;
92}
93
100void log_sampler_config(const GenerationParams& params) {
101 logger->info("Sampler: temp={:.2f}, top_k={}, top_p={:.2f}, "
102 "repeat_penalty={:.2f}, thinking={}",
103 params.temperature, params.top_k, params.top_p,
104 params.repeat_penalty, params.enable_thinking);
105}
106
114void finalize_result(GenerationResult& result,
115 std::chrono::steady_clock::time_point start_time)
116{
117 auto end = entropic::log::now();
118 result.generation_time_ms = entropic::log::elapsed_ms(
119 start_time, end);
120 if (result.token_count > 0 && result.generation_time_ms > 0.0) {
121 result.throughput_tok_s =
122 static_cast<double>(result.token_count)
123 / result.generation_time_ms * 1000.0;
124 }
125 logger->info("Generated: {} tokens, finish={}, {:.0f}ms, "
126 "{:.1f} tok/s",
127 result.token_count, result.finish_reason,
128 result.generation_time_ms, result.throughput_tok_s);
129 logger->info("Content:\n{}", result.content);
130}
131
151void finalize_generation(GenerationResult& result,
152 const std::string& generated, int n_generated,
153 const GenerationParams& params,
154 std::chrono::steady_clock::time_point t0)
155{
156 if (n_generated >= params.max_tokens
157 && result.finish_reason.empty()) {
158 result.finish_reason = "length";
159 }
160 result.content = generated;
161 result.token_count = n_generated;
162 finalize_result(result, t0);
163}
164
177GenerationResult sampler_init_error(
178 std::chrono::steady_clock::time_point t0)
179{
180 GenerationResult r;
181 r.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
182 r.error_message = "Sampler factory not initialized";
183 r.finish_reason = "error";
184 finalize_result(r, t0);
185 return r;
186}
187
197ggml_type parse_kv_cache_type(const std::string& s) {
198 static const std::pair<const char*, ggml_type> kTable[] = {
199 {"f16", GGML_TYPE_F16},
200 {"f32", GGML_TYPE_F32},
201 {"bf16", GGML_TYPE_BF16},
202 {"q8_0", GGML_TYPE_Q8_0},
203 {"q4_0", GGML_TYPE_Q4_0},
204 };
205 for (const auto& [name, type] : kTable) {
206 if (s == name) { return type; }
207 }
208 logger->warn("Unknown cache_type '{}' — defaulting to f16", s);
209 return GGML_TYPE_F16;
210}
211
221llama_split_mode parse_split_mode(const std::string& s) {
222 if (s.empty()) { return LLAMA_SPLIT_MODE_LAYER; }
223 static const std::pair<const char*, llama_split_mode> kTable[] = {
224 {"none", LLAMA_SPLIT_MODE_NONE},
225 {"layer", LLAMA_SPLIT_MODE_LAYER},
226 {"row", LLAMA_SPLIT_MODE_ROW},
227 };
228 for (const auto& [name, mode] : kTable) {
229 if (s == name) { return mode; }
230 }
231 logger->warn("Unknown split_mode '{}' — defaulting to layer", s);
232 return LLAMA_SPLIT_MODE_LAYER;
233}
234
245llama_model_params build_load_mparams(const entropic::ModelConfig& cfg) {
246 llama_model_params m = llama_model_default_params();
247 m.n_gpu_layers = cfg.gpu_layers;
248 m.use_mmap = true;
249 m.use_mlock = cfg.use_mlock;
250 m.split_mode = parse_split_mode(cfg.split_mode);
251 // gh#23 MVP item 7 (v2.3.19): main_gpu. Effective when split_mode
252 // is "none" (pin) or "row" (small-tensor placement). 0 keeps
253 // pre-v2.3.19 load bit-for-bit.
254 m.main_gpu = cfg.main_gpu;
255 return m;
256}
257
258} // anonymous namespace
259
260// ── Lifecycle ──────────────────────────────────────────────
261
274 llama_model_params mparams = llama_model_default_params();
275 mparams.n_gpu_layers = 0;
276 mparams.use_mmap = true;
277 mparams.use_mlock = config.use_mlock;
278
279 model_ = llama_model_load_from_file(config.path.c_str(), mparams);
280 if (!model_) {
281 last_error_ = "llama_model_load_from_file failed: " + config.path.string();
282 return false;
283 }
284
285 vocab_ = llama_model_get_vocab(model_);
286 is_recurrent_ = llama_model_is_recurrent(model_);
287 is_hybrid_ = llama_model_is_hybrid(model_); // gh#97: attn + recurrent/SSM
288 // v2.3.10: wire the Tokenizer seam now that vocab_ is valid.
289 // Lifetime: tokenizer_ borrows vocab_; do_unload resets
290 // tokenizer_ BEFORE freeing the model so the borrow never dangles.
291 tokenizer_ = std::make_unique<LlamaCppTokenizer>(vocab_);
292 logger->info("Model loaded (CPU): {} tokens in vocab, recurrent={}",
293 llama_vocab_n_tokens(vocab_), is_recurrent_);
294 return true;
295}
296
307namespace {
321llama_context_params build_cparams(const entropic::ModelConfig& cfg) {
322 llama_context_params c = llama_context_default_params();
323 c.n_ctx = static_cast<uint32_t>(cfg.context_length);
324 c.n_batch = static_cast<uint32_t>(cfg.n_batch);
325 // gh#23 MVP item 5 (v2.3.17): n_ubatch. 0 keeps llama.cpp's default
326 // (== n_batch in practice), preserving pre-v2.3.17 chunking.
327 if (cfg.n_ubatch > 0) {
328 c.n_ubatch = static_cast<uint32_t>(cfg.n_ubatch);
329 }
330 c.n_threads = cfg.n_threads > 0
331 ? static_cast<uint32_t>(cfg.n_threads)
332 : std::thread::hardware_concurrency();
333 c.flash_attn_type = cfg.flash_attn
334 ? LLAMA_FLASH_ATTN_TYPE_ENABLED
335 : LLAMA_FLASH_ATTN_TYPE_DISABLED;
336 c.type_k = parse_kv_cache_type(cfg.cache_type_k);
337 c.type_v = parse_kv_cache_type(cfg.cache_type_v);
338 // gh#23 MVP item 8 (v2.3.20): offload_kqv. true (default) matches
339 // llama.cpp's default — bit-identical for callers not opting out.
340 c.offload_kqv = cfg.offload_kqv;
341 // gh#23 MVP items 9 + 10 (v2.3.21 + v2.3.22): RoPE frequency
342 // overrides. Both 0.0 = use model's trained value — bit-identical.
343 c.rope_freq_base = cfg.rope_freq_base;
344 c.rope_freq_scale = cfg.rope_freq_scale;
345 // gh#23 MVP item 11 (v2.3.23): n_parallel maps to cparams.n_seq_max.
346 // 1 (default) matches llama.cpp's default — bit-identical.
347 c.n_seq_max = static_cast<uint32_t>(cfg.n_parallel);
348 // gh#98 (v2.8.0): a unified KV buffer is REQUIRED for llama_memory_seq_cp
349 // (the same-prefix batch fan-out) — seq_cp asserts on per-sequence buffers.
350 // llama.cpp also recommends kv_unified exactly when sequences share a large
351 // prefix (our case). Only enabled when batching is configured (n_parallel>1)
352 // so single-sequence handles keep llama.cpp's default.
353 c.kv_unified = (cfg.n_parallel > 1);
354 // gh#108 (v2.9.2): llama_context_default_params() returns swa_full=true (a
355 // full-context SWA cache), but the CLI default is false. For Gemma-4 (mostly
356 // sliding-window: window=512, 5:1 SWA:global) the un-windowed cache wastes
357 // ~5 GB at 128k. Set false — the memory-efficient windowed mode. Validated
358 // against warm-keep / prompt-cache reuse over a >window prefix (the SWA layers
359 // keep only the last `window` tokens, so KV reuse must not assume full-context
360 // SWA residency — covered by the long-context warm-keep model test).
361 c.swa_full = false;
362 return c;
363}
364} // anonymous namespace
365
379 if (!load_gpu_model()) { return false; }
380 if (!create_inference_context()) { return false; }
381 // v2.3.10: wire the Sampler seam once ctx_ / vocab_ are live.
382 // Lifetime: factory borrows ctx_ + vocab_; do_deactivate /
383 // do_unload reset sampler_factory_ BEFORE freeing those handles
384 // so the borrow never dangles.
385 sampler_factory_ = std::make_unique<LlamaCppSamplerFactory>(
386 ctx_, vocab_);
388 return true;
389}
390
409 llama_model_params mparams = build_load_mparams(config());
410
411 if (!config().tensor_split.empty()) {
412 // TODO: parse tensor_split string into float array for multi-GPU
413 logger->warn("tensor_split not yet implemented, ignoring");
414 }
415
416 // tokenizer_ borrows the old vocab_; reset it before the free so the
417 // borrow never dangles. Then free the WARM model and null the
418 // handles so a failed reload below leaves the backend in a clean,
419 // recoverable state rather than a dangling one.
420 tokenizer_.reset();
421 if (model_ != nullptr) {
422 llama_model_free(model_);
423 model_ = nullptr;
424 vocab_ = nullptr;
425 }
426
427 model_ = llama_model_load_from_file(config().path.c_str(), mparams);
428 if (model_ == nullptr) {
429 // llama.cpp returns null with no error string — the actual
430 // reason (OOM, CUDA init failure, GGUF parse error, etc.) only
431 // surfaces in ggml's log stream. Point the operator at it so
432 // multi-handle GPU failures (gh#58 v2.2.7 follow-up) are
433 // diagnosable without source-diving llama.cpp.
434 last_error_ = "Failed to reload model with GPU layers "
435 "(path=" + config().path.string()
436 + ", gpu_layers=" + std::to_string(config().gpu_layers)
437 + ") — check llama_ggml.log in the engine's log_dir "
438 "for the underlying llama.cpp/CUDA error";
439 return false;
440 }
441
442 vocab_ = llama_model_get_vocab(model_);
443 tokenizer_ = std::make_unique<LlamaCppTokenizer>(vocab_);
444 return true;
445}
446
454 llama_context_params cparams = build_cparams(config());
455
456 ctx_ = llama_init_from_model(model_, cparams);
457 if (!ctx_) {
458 last_error_ = "llama_init_from_model failed";
459 return false;
460 }
461
462 logger->info("Context created: n_ctx={}, n_batch={}, "
463 "flash_attn={}, type_k={}, type_v={}",
464 config().context_length, config().n_batch,
465 config().flash_attn,
466 config().cache_type_k, config().cache_type_v);
467
468 // Initialize prompt cache if not already created
469 if (!prompt_cache_) {
470 prompt_cache_ = std::make_unique<PromptCache>(
472 logger->info("Prompt cache initialized: max_bytes={}",
474 }
475 return true;
476}
477
491 if (config().mmproj_path.empty()) {
492 has_vision_ = false;
493 return;
494 }
495 auto ctx_params = mtmd_context_params_default();
496 ctx_params.use_gpu = (config().gpu_layers != 0);
497 ctx_params.flash_attn_type = config().flash_attn
498 ? LLAMA_FLASH_ATTN_TYPE_ENABLED
499 : LLAMA_FLASH_ATTN_TYPE_DISABLED;
500 ctx_params.print_timings = false;
501 mtmd_ctx_ = mtmd_init_from_file(
502 config().mmproj_path.c_str(), model_, ctx_params);
503 if (mtmd_ctx_ == nullptr) {
504 logger->error("mtmd_init_from_file failed for {} — "
505 "continuing in text-only mode",
506 config().mmproj_path.string());
507 has_vision_ = false;
508 return;
509 }
510 has_vision_ = mtmd_support_vision(mtmd_ctx_);
511 logger->info("mmproj loaded from {} — vision={}",
512 config().mmproj_path.string(), has_vision_);
513}
514
530 if (mtp_draft_ctx_ != nullptr) {
531 llama_free(mtp_draft_ctx_);
532 mtp_draft_ctx_ = nullptr;
533 }
534 if (mtp_draft_model_ != nullptr) {
535 llama_model_free(mtp_draft_model_);
536 mtp_draft_model_ = nullptr;
537 }
538 mtp_head_path_.clear();
539}
540
554bool LlamaCppBackend::setup_mtp_draft(const std::string& head_path, int n_max) {
555 mtp_n_max_ = (n_max > 0) ? n_max : 16;
556 if (mtp_draft_ctx_ != nullptr && mtp_head_path_ == head_path) {
557 return true; // live head already bound to this ctx_
558 }
560 return build_mtp_head(head_path);
561}
562
568bool LlamaCppBackend::build_mtp_head(const std::string& head_path) {
569 if (ctx_ == nullptr) {
570 last_error_ = "MTP setup requires an ACTIVE target context";
571 return false;
572 }
573 if (head_path.empty()) {
574 // gh#108: fail loud before llama_model_load_from_file("") — a bare
575 // mtp=true with no draft.path is a config error, not a load to attempt.
576 last_error_ = "MTP requires speculative.draft.path (the head GGUF); "
577 "none configured";
578 return false;
579 }
580 llama_model_params mparams = llama_model_default_params();
581 mparams.n_gpu_layers = config().gpu_layers; // head is tiny — follow target
582 mparams.use_mmap = true;
583 mtp_draft_model_ = llama_model_load_from_file(head_path.c_str(), mparams);
584 if (mtp_draft_model_ != nullptr) {
585 llama_context_params cparams = build_cparams(config());
586 cparams.ctx_type = LLAMA_CONTEXT_TYPE_MTP;
587 cparams.ctx_other = ctx_; // share the target's KV memory
588 cparams.n_rs_seq = 0;
589 mtp_draft_ctx_ = llama_init_from_model(mtp_draft_model_, cparams);
590 }
591 bool ok = (mtp_draft_ctx_ != nullptr);
592 if (ok) {
593 mtp_head_path_ = head_path;
594 logger->info("MTP head ready: {} (n_max={}, ctx_other=target, "
595 "shared-KV)", head_path, mtp_n_max_);
596 } else {
597 last_error_ = "MTP head setup failed: " + head_path;
599 }
600 return ok;
601}
602
619 // gh#108 (v2.9.1): serialise vs an in-flight generate_mtp — it holds
620 // mtp_mutex_ across its decode, so this blocks until that decode finishes
621 // before freeing the MTP head + ctx_ (no deactivate-during-generate UAF).
622 std::lock_guard<std::mutex> lk(mtp_mutex_);
623 // gh#106 (v2.9.0): the MTP head borrows ctx_ via ctx_other — free it
624 // FIRST so the borrow never dangles past the context.
626 // v2.3.10: sampler factory borrows ctx_ + vocab_. Release it
627 // BEFORE freeing the context so the borrow never dangles.
628 sampler_factory_.reset();
629 // v2.1.8: mtmd holds a reference to the live llama_model — free
630 // it before the GPU model is unloaded below.
631 if (mtmd_ctx_ != nullptr) {
632 mtmd_free(mtmd_ctx_);
633 mtmd_ctx_ = nullptr;
634 has_vision_ = false;
635 }
636 if (ctx_) {
637 llama_free(ctx_);
638 ctx_ = nullptr;
639 }
640 invalidate_resident_kv(); // gh#96: KV is gone with the context
641
642 // Free the GPU model FIRST (releasing VRAM — the point of
643 // deactivate), then reload CPU-only for the WARM state. tokenizer_
644 // borrows the old vocab_, so reset it before the free.
645 tokenizer_.reset();
646 if (model_ != nullptr) {
647 llama_model_free(model_);
648 model_ = nullptr;
649 vocab_ = nullptr;
650 }
652}
653
664 llama_model_params mparams = llama_model_default_params();
665 mparams.n_gpu_layers = 0;
666 mparams.use_mmap = true;
667 mparams.use_mlock = config().use_mlock;
668
669 model_ = llama_model_load_from_file(config().path.c_str(), mparams);
670 if (model_ != nullptr) {
671 vocab_ = llama_model_get_vocab(model_);
672 tokenizer_ = std::make_unique<LlamaCppTokenizer>(vocab_);
673 } else {
674 // VRAM is released, but the warm-reload failed: leave the handle
675 // null (state stays recoverable — the next activate reloads from
676 // scratch). Error, not warn: a same-file CPU reload failing here
677 // signals real trouble (disk/OOM).
678 logger->error("Failed to reload CPU model during deactivate "
679 "(path={}); backend left unloaded until next activate",
680 config().path.string());
681 }
682}
683
698
713 std::unique_ptr<Tokenizer> tokenizer)
714{
715 tokenizer_ = std::move(tokenizer);
716 state_.store(ModelState::WARM, std::memory_order_release);
717}
718
734 std::unique_ptr<SamplerFactory> factory)
735{
736 sampler_factory_ = std::move(factory);
737}
738
745 // gh#108 (v2.9.1): serialise vs in-flight generate_mtp (see do_deactivate).
746 std::lock_guard<std::mutex> lk(mtp_mutex_);
747 if (prompt_cache_) {
748 prompt_cache_->clear();
749 }
750 // gh#106 (v2.9.0): MTP head borrows ctx_ — free it before the context.
752 // v2.3.10: sampler factory borrows ctx_ + vocab_ — release it
753 // BEFORE the context/model are freed below so the borrow never
754 // points into freed memory. (do_deactivate normally releases
755 // this earlier; this reset is the WARM→COLD safety net.)
756 sampler_factory_.reset();
757 // v2.3.10: tokenizer borrows vocab_ — release it BEFORE the model
758 // is freed so the borrow never points into freed memory.
759 tokenizer_.reset();
760 // v2.1.8: mtmd must be freed before the underlying llama_model.
761 if (mtmd_ctx_ != nullptr) {
762 mtmd_free(mtmd_ctx_);
763 mtmd_ctx_ = nullptr;
764 has_vision_ = false;
765 }
766 if (ctx_) {
767 llama_free(ctx_);
768 ctx_ = nullptr;
769 }
770 invalidate_resident_kv(); // gh#96: KV is gone with the context
771 if (model_) {
772 llama_model_free(model_);
773 model_ = nullptr;
774 }
775 vocab_ = nullptr;
776}
777
778// ── Tokenization ───────────────────────────────────────────
779
788std::vector<llama_token> LlamaCppBackend::tokenize(
789 const std::string& text, bool add_special) const
790{
791 // v2.3.10: route through the Tokenizer seam. tokenizer_ is set
792 // in do_load (real impl) or via inject_tokenizer_for_test (mock).
793 // Returns empty when no tokenizer is wired — matches the prior
794 // failure-path return shape.
795 if (!tokenizer_) { return {}; }
796 auto ids = tokenizer_->tokenize(text, add_special);
797 // llama_token is int32_t; vector conversion is a copy through
798 // iterators since the value type matches.
799 return {ids.begin(), ids.end()};
800}
801
809std::string LlamaCppBackend::detokenize(llama_token token) const {
810 // v2.3.10: route through Tokenizer seam. The special=false /
811 // gh#68 history + defensive rationale now lives in
812 // LlamaCppTokenizer::detokenize. Returns empty when no
813 // tokenizer is wired — matches prior failure-path return.
814 if (!tokenizer_) { return {}; }
815 return tokenizer_->detokenize(static_cast<int32_t>(token));
816}
817
825int LlamaCppBackend::do_count_tokens(const std::string& text) const {
826 auto tokens = tokenize(text, false);
827 return static_cast<int>(tokens.size());
828}
829
838 const std::string& text) const {
839 auto tokens = tokenize(text, true);
840 return {tokens.begin(), tokens.end()};
841}
842
843// ── Evaluation (v1.9.10) ──────────────────────────────────
844
860 const int32_t* tokens,
861 int n_tokens)
862{
863 int n_vocab = llama_vocab_n_tokens(vocab_);
864 LogprobResult result;
865 result.tokens.assign(tokens, tokens + n_tokens);
866 result.n_tokens = n_tokens;
867 result.n_logprobs = n_tokens - 1;
868 result.logprobs.reserve(result.n_logprobs);
869
870 auto* mem = llama_get_memory(ctx_);
871 llama_memory_clear(mem, true);
872
873 for (int i = 0; i < n_tokens; i++) {
874 llama_token tok = tokens[i];
875 llama_batch batch = llama_batch_get_one(&tok, 1);
876 int rc = llama_decode(ctx_, batch);
877 if (rc != 0) {
878 llama_memory_clear(mem, true);
879 throw std::runtime_error("llama_decode failed at logprob pos");
880 }
881 if (i < n_tokens - 1) {
882 const float* logits = llama_get_logits_ith(ctx_, -1);
883 float lp = extract_token_logprob(
884 logits, tokens[i + 1], n_vocab);
885 result.logprobs.push_back(lp);
886 }
887 }
888
889 float sum = 0.0f;
890 for (float lp : result.logprobs) { sum += lp; }
891 result.total_logprob = sum;
892 result.perplexity = std::exp(
893 -sum / static_cast<float>(result.n_logprobs));
894
895 llama_memory_clear(mem, true);
896 return result;
897}
898
915 std::lock_guard<std::mutex> lock(seq_id_mutex_);
916 if (!free_seq_ids_.empty()) {
917 auto id = free_seq_ids_.back();
918 free_seq_ids_.pop_back();
919 return id;
920 }
921 return next_temp_seq_id_++;
922}
923
930void LlamaCppBackend::release_temp_seq_id(llama_seq_id seq_id) {
931 std::lock_guard<std::mutex> lock(seq_id_mutex_);
932 free_seq_ids_.push_back(seq_id);
933}
934
949 const float* logits,
950 int32_t next_token,
951 int n_vocab)
952{
953 float max_logit = logits[0];
954 for (int v = 1; v < n_vocab; v++) {
955 if (logits[v] > max_logit) {
956 max_logit = logits[v];
957 }
958 }
959 float sum_exp = 0.0f;
960 for (int v = 0; v < n_vocab; v++) {
961 sum_exp += std::exp(logits[v] - max_logit);
962 }
963 float log_sum_exp = max_logit + std::log(sum_exp);
964 return logits[next_token] - log_sum_exp;
965}
966
967// ── Chat template ──────────────────────────────────────────
968
976static std::vector<llama_chat_message> to_llama_chat(
977 const std::vector<Message>& messages) {
978 std::vector<llama_chat_message> chat_msgs;
979 chat_msgs.reserve(messages.size());
980 for (const auto& msg : messages) {
981 chat_msgs.push_back({msg.role.c_str(), msg.content.c_str()});
982 }
983 return chat_msgs;
984}
985
999static std::vector<common_chat_msg> to_common_chat(
1000 const std::vector<Message>& messages) {
1001 std::vector<common_chat_msg> out;
1002 out.reserve(messages.size());
1003 for (const auto& msg : messages) {
1004 common_chat_msg cm;
1005 cm.role = msg.role;
1006 cm.content = msg.content;
1007 out.push_back(std::move(cm));
1008 }
1009 return out;
1010}
1011
1026static std::vector<common_chat_tool> mcp_tools_to_common_chat(
1027 const std::string& tools_json) {
1028 std::vector<common_chat_tool> out;
1029 if (tools_json.empty()) { return out; }
1030 auto arr = nlohmann::json::parse(tools_json, nullptr, false);
1031 if (!arr.is_array()) { return out; }
1032 for (const auto& t : arr) {
1033 common_chat_tool ct;
1034 ct.name = t.value("name", "");
1035 ct.description = t.value("description", "");
1036 if (t.contains("inputSchema")) {
1037 ct.parameters = t["inputSchema"].dump();
1038 }
1039 if (!ct.name.empty()) { out.push_back(std::move(ct)); }
1040 }
1041 return out;
1042}
1043
1056static ToolCall to_entropic_tool_call(const common_chat_tool_call& cc) {
1057 ToolCall tc;
1058 tc.id = cc.id;
1059 tc.name = cc.name;
1060 tc.arguments_json = cc.arguments;
1061 auto j = nlohmann::json::parse(cc.arguments, nullptr, false);
1062 if (j.is_object()) {
1063 for (auto it = j.begin(); it != j.end(); ++it) {
1064 tc.arguments[it.key()] =
1065 it->is_string() ? it->get<std::string>() : it->dump();
1066 }
1067 }
1068 return tc;
1069}
1070
1090static std::optional<common_chat_params> render_common_chat(
1091 llama_model* model,
1092 const std::vector<Message>& messages,
1093 const GenerationParams& params,
1094 const std::vector<common_chat_tool>& tools) {
1095 if (model == nullptr) { return std::nullopt; }
1096 auto tmpls = common_chat_templates_init(model, "");
1097 std::optional<common_chat_params> out;
1098 if (tmpls) {
1099 common_chat_templates_inputs inputs;
1100 inputs.messages = to_common_chat(messages);
1101 inputs.add_generation_prompt = true;
1102 inputs.use_jinja = true;
1103 inputs.enable_thinking = params.enable_thinking; // gh#86
1104 inputs.tools = tools;
1105 if (!tools.empty()) {
1106 inputs.tool_choice = COMMON_CHAT_TOOL_CHOICE_AUTO;
1107 }
1108 try {
1109 out = common_chat_templates_apply(tmpls.get(), inputs);
1110 } catch (const std::exception& e) {
1111 logger->warn("jinja chat template apply failed ({}); "
1112 "falling back to low-level template", e.what());
1113 }
1114 }
1115 return out;
1116}
1117
1125static std::string concat_messages_fallback(
1126 const std::vector<Message>& messages) {
1127 std::string fallback;
1128 for (const auto& msg : messages) {
1129 fallback += msg.role + ": " + msg.content + "\n";
1130 }
1131 return fallback;
1132}
1133
1155 const std::vector<Message>& messages,
1156 const GenerationParams& params) const
1157{
1158 auto rendered = render_common_chat(model_, messages, params, {});
1159 return rendered ? rendered->prompt
1160 : apply_chat_template_lowlevel(messages);
1161}
1162
1176 const std::vector<Message>& messages,
1177 const GenerationParams& params)
1178{
1179 if (!active_tools_json_.empty()) {
1180 return render_with_tools(messages, params);
1181 }
1182 have_chat_params_ = false;
1183 return apply_chat_template(messages, params);
1184}
1185
1192void LlamaCppBackend::set_active_tools(const std::string& tools_json) {
1193 active_tools_json_ = tools_json;
1194 logger->info("Active tools staged for common_chat render: {} bytes",
1195 tools_json.size());
1196}
1197
1211 const std::vector<Message>& messages,
1212 const GenerationParams& params)
1213{
1214 have_chat_params_ = false;
1216 auto rendered = render_common_chat(model_, messages, params, tools);
1217 std::string prompt;
1218 if (rendered) {
1219 last_chat_format_ = static_cast<int>(rendered->format);
1220 last_generation_prompt_ = rendered->generation_prompt;
1221 last_parser_ = rendered->parser;
1222 have_chat_params_ = true;
1223 // gh#105: snapshot this TOOLED render for the engine's later re-parse.
1224 // A toolless interleave (validator critique) clears have_chat_params_
1225 // but NOT this snapshot, so parse_response still decodes the main call.
1229 parse_params_valid_ = true;
1230 prompt = rendered->prompt;
1231 logger->info("render_with_tools: format={}, {} tool(s), captured "
1232 "parser ({} bytes)", last_chat_format_, tools.size(),
1233 last_parser_.size());
1234 } else {
1235 prompt = apply_chat_template_lowlevel(messages);
1236 }
1237 return prompt;
1238}
1239
1252 // gh#105: read the sticky last-TOOLED snapshot, not the live capture — the
1253 // engine queries this AFTER a toolless validator render would have cleared
1254 // have_chat_params_, so the live flag is unreliable here.
1255 return parse_params_valid_
1256 && parse_chat_format_ == COMMON_CHAT_FORMAT_PEG_GEMMA4;
1257}
1258
1268 // last_chat_format_ is stored as int (the captured common_chat_format).
1269 return have_chat_params_
1271 static_cast<common_chat_format>(last_chat_format_))
1272 : "";
1273}
1274
1286std::vector<std::string> LlamaCppBackend::effective_stop(
1287 const GenerationParams& params) const {
1288 GenerationParams p = params;
1289 const std::size_t before = p.stop.size();
1291 if (p.stop.size() > before) {
1292 logger->info("Sequential tier: tool-call close marker injected "
1293 "post-render (gh#105) — hard-stop at first tool call");
1294 }
1295 return p.stop;
1296}
1297
1323void strip_thinking_channels(std::string& content, std::string* reasoning_out) {
1324 static const std::string kOpen = "<|channel>";
1325 static const std::string kClose = "<channel|>";
1326 bool stripped = false;
1327 bool truncated_unclosed = false;
1328 std::size_t pos;
1329 while ((pos = content.find(kOpen)) != std::string::npos) {
1330 stripped = true;
1331 std::size_t end = content.find(kClose, pos + kOpen.size());
1332 if (end == std::string::npos) { truncated_unclosed = true; }
1333 std::size_t span_end =
1334 (end == std::string::npos) ? content.size() : end + kClose.size();
1335 if (reasoning_out != nullptr) {
1336 std::size_t inner = pos + kOpen.size();
1337 std::size_t inner_end =
1338 (end == std::string::npos) ? content.size() : end;
1339 reasoning_out->append(content, inner, inner_end - inner);
1340 }
1341 content.erase(pos, span_end - pos);
1342 }
1343 if (stripped) {
1344 std::size_t nb = content.find_first_not_of(" \t\r\n");
1345 content.erase(0, nb == std::string::npos ? content.size() : nb);
1346 }
1347 if (truncated_unclosed && content.empty()) {
1348 logger->warn("strip_thinking_channels: generation hit max_tokens "
1349 "while still inside a <|channel> reasoning block — no "
1350 "answer was ever produced, so content is empty (not a "
1351 "parse error). Raise max_tokens or investigate why this "
1352 "config/prompt doesn't converge within budget.");
1353 }
1354}
1355
1374 const std::string& raw) const
1375{
1376 CommonChatResult result;
1377 // gh#105: decode from the sticky last-TOOLED snapshot (parse_*), NOT the
1378 // live capture — a toolless validator render between the main generation
1379 // and this re-parse would have cleared the live params.
1380 if (!parse_params_valid_) {
1381 result.content = raw;
1382 return result;
1383 }
1384 common_chat_parser_params pp;
1385 pp.format = static_cast<common_chat_format>(parse_chat_format_);
1386 pp.generation_prompt = parse_generation_prompt_;
1387 pp.parser.load(parse_parser_); // mandatory — see header
1388 try {
1389 auto msg = common_chat_parse(raw, /*is_partial=*/false, pp);
1390 result.content = msg.content;
1391 result.reasoning_content = msg.reasoning_content;
1392 // gh#106: Gemma 4 QAT reasoning channels common_chat doesn't parse.
1394 for (const auto& tc : msg.tool_calls) {
1395 result.tool_calls.push_back(to_entropic_tool_call(tc));
1396 }
1397 // gh#90: gemma <|"|> string-escape loses type through PEG_GEMMA4 —
1398 // restore string typing for params the staged schema declares string.
1400 } catch (const std::exception& e) {
1401 logger->warn("common_chat_parse failed ({}); raw kept as content",
1402 e.what());
1403 result.content = raw;
1404 }
1405 return result;
1406}
1407
1421 const std::vector<Message>& messages) const
1422{
1423 auto chat_msgs = to_llama_chat(messages);
1424
1425 int n = llama_chat_apply_template(
1426 nullptr, chat_msgs.data(), chat_msgs.size(),
1427 true, nullptr, 0);
1428 if (n < 0) {
1429 logger->error("llama_chat_apply_template failed (size query)");
1430 return concat_messages_fallback(messages);
1431 }
1432
1433 std::vector<char> buf(static_cast<size_t>(n + 1));
1434 int written = llama_chat_apply_template(
1435 nullptr, chat_msgs.data(), chat_msgs.size(),
1436 true, buf.data(), static_cast<int32_t>(buf.size()));
1437 if (written < 0) {
1438 logger->error("llama_chat_apply_template failed (render)");
1439 return concat_messages_fallback(messages);
1440 }
1441
1442 return std::string(buf.data(), static_cast<size_t>(written));
1443}
1444
1445// ── Sampler ────────────────────────────────────────────────
1446
1468std::unique_ptr<Sampler> LlamaCppBackend::create_sampler(
1469 const GenerationParams& params) const
1470{
1471 if (!sampler_factory_) { return nullptr; }
1472 return sampler_factory_->create(params);
1473}
1474
1475// ── Decode loop ────────────────────────────────────────────
1476
1484bool LlamaCppBackend::run_prefill(const std::vector<llama_token>& tokens) {
1485 llama_memory_clear(llama_get_memory(ctx_), true);
1486
1487 const int n_batch = config().n_batch;
1488 const int n_tokens = static_cast<int>(tokens.size());
1489
1490 for (int i = 0; i < n_tokens; i += n_batch) {
1491 int chunk = std::min(n_batch, n_tokens - i);
1492 std::vector<llama_token> slice(
1493 tokens.begin() + i, tokens.begin() + i + chunk);
1494 llama_batch batch = llama_batch_get_one(
1495 slice.data(), static_cast<int32_t>(chunk));
1496 if (llama_decode(ctx_, batch) != 0) {
1497 logger->error("Prefill decode failed at offset {}", i);
1498 return false;
1499 }
1500 }
1501 last_prefill_tokens_ += n_tokens; // gh#96: count tokens decoded in prefill
1502 return true;
1503}
1504
1522 Sampler& sampler,
1523 std::string& generated,
1524 std::function<void(std::string_view)>& on_token,
1525 const std::vector<std::string>& stop)
1526{
1527 llama_token new_token = sampler.sample();
1528
1529 if (new_token == llama_vocab_eos(vocab_)
1530 || llama_vocab_is_eog(vocab_, new_token)) {
1531 return "eos";
1532 }
1533
1534 std::string piece = detokenize(new_token);
1535 generated += piece;
1536 if (on_token) {
1537 on_token(std::string_view(piece));
1538 }
1539 if (check_stop_sequences(generated, stop)) {
1540 return "stop";
1541 }
1542
1543 llama_token tok = new_token;
1544 llama_batch single = llama_batch_get_one(&tok, 1);
1545 return (llama_decode(ctx_, single) == 0) ? "continue" : "error";
1546}
1547
1563 const std::vector<llama_token>& tokens,
1564 const GenerationParams& params,
1565 std::function<void(std::string_view)> on_token,
1566 std::atomic<bool>* cancel)
1567{
1568 // v2.3.10: Sampler seam — factory installed in do_activate.
1569 auto sampler = create_sampler(params);
1570 if (!sampler) {
1571 GenerationResult result;
1573 result.error_message = "Sampler factory not initialized";
1574 result.finish_reason = "error";
1575 return result;
1576 }
1577
1578 if (!run_prefill(tokens)) {
1579 GenerationResult result;
1581 result.error_message = "Prefill decode failed";
1582 result.finish_reason = "error";
1583 return result;
1584 }
1585
1586 return generate_after_prefill(*sampler, params, std::move(on_token), cancel);
1587}
1588
1605 Sampler& sampler,
1606 const GenerationParams& params,
1607 std::function<void(std::string_view)> on_token,
1608 std::atomic<bool>* cancel)
1609{
1610 GenerationResult result;
1611 std::string generated;
1612 int n_generated = 0;
1613 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
1614
1615 while (n_generated < params.max_tokens) {
1616 bool cancelled = cancel && cancel->load(std::memory_order_acquire);
1617 if (cancelled) {
1618 result.finish_reason = "cancelled";
1620 break;
1621 }
1622
1623 auto status = step_token(sampler, generated, on_token, stop);
1624 if (status == "continue") {
1625 ++n_generated;
1626 } else {
1627 result.finish_reason = (status == "error") ? "error" : "stop";
1628 if (status == "error") {
1630 }
1631 break;
1632 }
1633 }
1634
1635 if (n_generated >= params.max_tokens && result.finish_reason.empty()) {
1636 result.finish_reason = "length";
1637 }
1638
1639 result.content = generated;
1640 result.token_count = n_generated;
1641 return result;
1642}
1643
1644// ── gh#98: same-prefix multi-seq batched generation ────────
1645
1651static GenerationResult batch_error_result(const std::string& msg) {
1654 e.error_message = msg;
1655 e.finish_reason = "error";
1656 return e;
1657}
1658
1664static void fill_batch_cell(llama_batch& b, int k, llama_token tok,
1665 llama_pos pos, llama_seq_id seq, bool want_logits) {
1666 b.token[k] = tok;
1667 b.pos[k] = pos;
1668 b.n_seq_id[k] = 1;
1669 b.seq_id[k][0] = seq;
1670 b.logits[k] = want_logits ? 1 : 0;
1671}
1672
1680 std::vector<BatchSeq>& seqs,
1681 const std::vector<GenerationParams>& params) {
1682 for (std::size_t i = 0; i < seqs.size(); ++i) {
1683 seqs[i].sampler = create_sampler(params[i]);
1684 auto* ls = dynamic_cast<LlamaCppSampler*>(seqs[i].sampler.get());
1685 if (ls == nullptr) { return false; }
1686 seqs[i].chain = ls->native_chain();
1687 seqs[i].seq_id = (i == 0) ? 0 : allocate_temp_seq_id();
1688 seqs[i].max_tokens = params[i].max_tokens;
1689 }
1690 return true;
1691}
1692
1699 std::vector<BatchSeq>& seqs, const std::vector<llama_token>& seq0,
1700 std::size_t shared) {
1701 std::vector<llama_token> prefix(
1702 seq0.begin(), seq0.begin() + static_cast<long>(shared));
1703 if (!decode_tokens_from(prefix, 0)) { return false; } // into seq 0
1704 auto* mem = llama_get_memory(ctx_);
1705 for (std::size_t i = 1; i < seqs.size(); ++i) {
1706 llama_memory_seq_cp(mem, 0, seqs[i].seq_id, 0,
1707 static_cast<llama_pos>(shared));
1708 }
1709 for (auto& s : seqs) { s.pos = static_cast<int>(shared); }
1710 return true;
1711}
1712
1722 std::vector<BatchSeq>& seqs,
1723 const std::vector<std::vector<llama_token>>& toks,
1724 std::size_t shared) {
1725 int total = 0;
1726 // shared <= shortest-1 < every t.size() by batch_shared_prefix_len, but
1727 // guard the unsigned subtraction defensively (a bad `shared` would else
1728 // underflow to a huge alloc).
1729 for (const auto& t : toks) {
1730 total += static_cast<int>(t.size() - std::min(shared, t.size()));
1731 }
1732 llama_batch batch = llama_batch_init(total, 0,
1733 static_cast<int32_t>(seqs.size()));
1734 int k = 0;
1735 for (std::size_t i = 0; i < seqs.size(); ++i) {
1736 int len = static_cast<int>(toks[i].size());
1737 for (int p = static_cast<int>(shared); p < len; ++p) {
1738 fill_batch_cell(batch, k, toks[i][p], p, seqs[i].seq_id,
1739 p == len - 1);
1740 if (p == len - 1) { seqs[i].logits_idx = k; }
1741 ++k;
1742 }
1743 seqs[i].pos = len;
1744 }
1745 batch.n_tokens = k;
1747 bool ok = (llama_decode(ctx_, batch) == 0);
1748 llama_batch_free(batch);
1749 return ok;
1750}
1751
1757void LlamaCppBackend::sample_batch_active(std::vector<BatchSeq>& seqs) {
1758 for (auto& s : seqs) {
1759 if (!s.active) { continue; }
1760 // llama_sampler_sample() accepts the drawn token into the chain
1761 // internally (advancing grammar/penalties) — matching the single-seq
1762 // step_token path. A second accept would double-advance the grammar.
1763 llama_token tok = llama_sampler_sample(s.chain, ctx_, s.logits_idx);
1764 if (llama_vocab_is_eog(vocab_, tok)) {
1765 s.active = false;
1766 s.finish = "stop";
1767 continue;
1768 }
1769 s.out.push_back(tok);
1770 ++s.n_gen;
1771 if (s.n_gen >= s.max_tokens) { s.active = false; s.finish = "length"; }
1772 }
1773}
1774
1786 std::vector<BatchSeq>& seqs, int max_steps, std::atomic<bool>& cancel) {
1787 llama_batch batch = llama_batch_init(static_cast<int32_t>(seqs.size()), 0,
1788 static_cast<int32_t>(seqs.size()));
1789 for (int step = 0; step < max_steps; ++step) {
1790 if (cancel.load(std::memory_order_acquire)) { break; }
1791 sample_batch_active(seqs);
1792 int k = 0;
1793 for (auto& s : seqs) {
1794 if (!s.active) { continue; }
1795 fill_batch_cell(batch, k, s.out.back(), s.pos, s.seq_id, true);
1796 s.logits_idx = k;
1797 ++s.pos;
1798 ++k;
1799 }
1800 if (k == 0) { break; }
1801 batch.n_tokens = k;
1803 if (llama_decode(ctx_, batch) != 0) { break; }
1804 }
1805 llama_batch_free(batch);
1806}
1807
1813std::vector<GenerationResult> LlamaCppBackend::build_batch_results(
1814 std::vector<BatchSeq>& seqs) {
1815 std::vector<GenerationResult> out;
1816 out.reserve(seqs.size());
1817 for (auto& s : seqs) {
1819 for (llama_token t : s.out) { r.content += detokenize(t); }
1820 r.token_count = s.n_gen;
1821 r.finish_reason = s.finish;
1822 out.push_back(std::move(r));
1823 }
1824 return out;
1825}
1826
1832void LlamaCppBackend::release_temp_seqs(std::vector<BatchSeq>& seqs) {
1833 for (std::size_t i = 1; i < seqs.size(); ++i) {
1834 if (seqs[i].seq_id != 0) { release_temp_seq_id(seqs[i].seq_id); }
1835 }
1836}
1837
1849std::vector<GenerationResult> LlamaCppBackend::run_batched_decode(
1850 const std::vector<std::vector<llama_token>>& toks,
1851 const std::vector<GenerationParams>& params,
1852 std::size_t shared,
1853 std::atomic<bool>& cancel)
1854{
1855 const std::size_t n = toks.size();
1856 std::vector<BatchSeq> seqs(n);
1857 if (!prepare_batch_seqs(seqs, params)) {
1858 release_temp_seqs(seqs); // don't leak ids allocated before the failure
1859 return std::vector<GenerationResult>(
1860 n, batch_error_result("batch sampler init"));
1861 }
1862 int max_steps = 0;
1863 for (const auto& p : params) { max_steps = std::max(max_steps, p.max_tokens); }
1864
1865 llama_memory_clear(llama_get_memory(ctx_), true);
1869
1870 bool ok = prefill_shared_and_fanout(seqs, toks[0], shared)
1871 && prefill_batch_suffixes(seqs, toks, shared);
1872 if (ok) { run_batch_gen_loop(seqs, max_steps, cancel); }
1873
1874 auto out = ok ? build_batch_results(seqs)
1875 : std::vector<GenerationResult>(
1876 n, batch_error_result("batch prefill"));
1877 release_temp_seqs(seqs);
1879 logger->info("gh#98 batch: requests={} prefix.tokens_shared={} "
1880 "prefix.tokens_saved={} total_prefill_tokens={} gen_decodes={}",
1881 n, shared, shared * (n - 1), last_prefill_tokens_,
1883 return out;
1884}
1885
1903std::vector<GenerationResult> LlamaCppBackend::do_generate_batch(
1904 const std::vector<std::vector<Message>>& requests,
1905 const std::vector<GenerationParams>& params,
1906 std::atomic<bool>& cancel)
1907{
1908 const std::size_t n = requests.size();
1909 std::vector<std::vector<llama_token>> toks(n);
1910 for (std::size_t i = 0; i < n; ++i) {
1911 toks[i] = tokenize(render_prompt(requests[i], params[i]), true);
1912 }
1913 const std::size_t shared = batch_shared_prefix_len(toks);
1914 std::size_t total_suffix = 0;
1915 for (const auto& t : toks) { total_suffix += t.size() - shared; }
1916
1917 const bool hybrid = is_hybrid_ || is_recurrent_;
1918 if (!batch_is_viable(n, config().n_parallel, shared, hybrid,
1919 total_suffix, config().n_batch)) {
1920 return InferenceBackend::do_generate_batch(requests, params, cancel);
1921 }
1922 return run_batched_decode(toks, params, shared, cancel);
1923}
1924
1925// ── Prompt cache helpers ───────────────────────────────────
1926
1935 const std::vector<Message>& messages)
1936{
1937 for (const auto& msg : messages) {
1938 if (msg.role == "system") {
1939 return msg.content;
1940 }
1941 }
1942 return "";
1943}
1944
1958 const std::vector<llama_token>& tokens, int start_offset)
1959{
1960 int total = static_cast<int>(tokens.size());
1961 if (start_offset >= total) { return true; }
1962
1963 int n_batch = llama_n_batch(ctx_);
1964 int n_remaining = total - start_offset;
1965 last_prefill_tokens_ += n_remaining; // gh#96: count tokens decoded here
1966 for (int off = 0; off < n_remaining; off += n_batch) {
1967 int chunk = std::min(n_batch, n_remaining - off);
1968 llama_batch batch = llama_batch_get_one(
1969 const_cast<llama_token*>(tokens.data())
1970 + start_offset + off,
1971 chunk);
1972 if (llama_decode(ctx_, batch) != 0) {
1973 logger->error("Decode chunk failed (start={}, off={}, "
1974 "chunk={})", start_offset, off, chunk);
1975 return false;
1976 }
1977 }
1978 return true;
1979}
1980
1998 const CacheEntry* cached,
1999 const std::vector<llama_token>& tokens)
2000{
2001 auto* mem = llama_get_memory(ctx_);
2002 llama_memory_clear(mem, true);
2003
2004 size_t restored = llama_state_seq_set_data(
2005 ctx_, cached->data.data(), cached->data_size, 0);
2006 if (restored == 0) {
2007 logger->warn("KV state restore failed, falling back to full prefill");
2008 return false;
2009 }
2010
2011 return decode_tokens_from(tokens, cached->token_count);
2012}
2013
2027 const CacheKey& key, int prefix_tokens)
2028{
2029 size_t state_size = llama_state_seq_get_size(ctx_, 0);
2030 if (state_size == 0) {
2031 return;
2032 }
2033
2034 std::vector<uint8_t> buf(state_size);
2035 size_t written = llama_state_seq_get_data(
2036 ctx_, buf.data(), buf.size(), 0);
2037 if (written > 0) {
2038 buf.resize(written);
2039 prompt_cache_->store(key, std::move(buf), prefix_tokens);
2040 }
2041}
2042
2052 const std::vector<Message>& messages,
2053 const GenerationParams& params)
2054{
2055 std::vector<Message> sys_msgs;
2056 for (const auto& msg : messages) {
2057 if (msg.role == "system") {
2058 sys_msgs.push_back(msg);
2059 }
2060 }
2061 if (sys_msgs.empty()) {
2062 return 0;
2063 }
2064
2065 std::string sys_prompt = apply_chat_template(sys_msgs, params);
2066 auto sys_tokens = tokenize(sys_prompt, true);
2067 return static_cast<int>(sys_tokens.size());
2068}
2069
2092 const std::vector<llama_token>& tokens,
2093 int prefix_tokens,
2094 const CacheKey& key)
2095{
2096 int total = static_cast<int>(tokens.size());
2097 if (prefix_tokens <= 0 || prefix_tokens >= total) {
2098 return run_prefill(tokens);
2099 }
2100
2101 // Pass 1: prefill only the prefix — `run_prefill` calls
2102 // llama_memory_clear, so seq 0 ends up holding exactly
2103 // prefix_tokens positions.
2104 std::vector<llama_token> prefix(
2105 tokens.begin(), tokens.begin() + prefix_tokens);
2106 if (!run_prefill(prefix)) {
2107 return false;
2108 }
2109
2110 // Save now: state contains exactly the prefix.
2111 save_prefix_to_cache(key, prefix_tokens);
2112
2113 // Pass 2: continue prefilling the remainder. No clear — decode
2114 // appends after the saved prefix positions.
2115 return decode_tokens_from(tokens, prefix_tokens);
2116}
2117
2135 const std::vector<llama_token>& tokens,
2136 const std::string& system_prompt,
2137 const std::vector<Message>& messages,
2138 const GenerationParams& params)
2139{
2140 // gh#96 (v2.7.5): count tokens actually pushed through llama_decode during
2141 // prefill this turn. run_prefill / decode_tokens_from accumulate into
2142 // last_prefill_tokens_; a prompt-cache HIT restores the system prefix
2143 // without a decode, so this counts the re-decoded post-system remainder —
2144 // the per-turn waste that climbs today and should collapse to the appended
2145 // delta once warm-keep reuse lands. (llama_perf n_p_eval proved unreliable
2146 // across the state-restore boundary, so we count the decodes directly.)
2148 last_input_tokens_ = static_cast<int>(tokens.size()); // gh#97
2149 auto t_pre = entropic::log::now();
2150 bool ok;
2151 if (is_hybrid_ || is_recurrent_) {
2152 // gh#97 (v2.7.6): hybrid/recurrent (SSM) memory rejects the partial
2153 // seq_rm warm-keep needs (state can't be partially erased at the tail),
2154 // and the prompt-cache restore lands non-contiguous cells — both desync
2155 // KV positions (pos_max inflates → eventual decode slot-failure with the
2156 // cache mostly empty). Plain full prefill (clear + contiguous decode) is
2157 // the only correct path for these archs at this llama.cpp pin. Mirrors
2158 // the speculative-decoding guard. Forfeits the gh#96 reuse for them.
2161 } else {
2162 // gh#96 warm-keep: reuse the resident KV prefix + decode only the delta;
2163 // fall back to a cold prefill (clear + system-prefix cache) when reuse
2164 // is off, the prefix diverged, or the KV was mutated out-of-band.
2166 if (!ok) {
2167 ok = prefill_dispatch(tokens, system_prompt, messages, params);
2168 if (ok) {
2170 } else {
2172 }
2173 }
2174 }
2175 last_prefill_ms_ = entropic::log::elapsed_ms(t_pre, entropic::log::now());
2176 logger->info("Prefill (gh#96): {} tokens / {:.1f} ms decoded this turn",
2178 return ok;
2179}
2180
2200bool LlamaCppBackend::try_warm_reuse(const std::vector<llama_token>& tokens) {
2201 if (!prompt_cache_config_.warm_keep || ctx_ == nullptr) {
2202 return false;
2203 }
2204 auto* mem = llama_get_memory(ctx_);
2205 long pos_max = static_cast<long>(llama_memory_seq_pos_max(mem, 0));
2206 std::size_t cut = warm_keep_cut(resident_tokens_, tokens, pos_max);
2207 if (cut == 0) {
2208 return false; // nothing reusable — cold prefill
2209 }
2210 // Drop the divergent tail (and any prior generated tokens past `cut`),
2211 // then decode only the appended delta. A single exit (returns <= 3 gate):
2212 // success records the new resident set; failure invalidates and reports it.
2213 llama_memory_seq_rm(mem, 0, static_cast<llama_pos>(cut), -1);
2214 bool ok = decode_tokens_from(tokens, static_cast<int>(cut));
2215 if (ok) {
2218 logger->info("Warm-keep: reused {} resident tokens, decoded {} "
2219 "delta (of {} total)", cut, tokens.size() - cut,
2220 tokens.size());
2221 }
2222 } else {
2224 }
2225 return ok;
2226}
2227
2241
2259 const std::vector<llama_token>& tokens,
2260 const std::string& system_prompt,
2261 const std::vector<Message>& messages,
2262 const GenerationParams& params)
2263{
2264 bool cache_enabled = prompt_cache_
2266 && !system_prompt.empty();
2267
2268 if (!cache_enabled) {
2269 return run_prefill(tokens);
2270 }
2271
2273 system_prompt, config().path.string());
2274 const CacheEntry* cached = prompt_cache_->lookup(key);
2275
2276 if (cached != nullptr) {
2278 logger->info("Prompt cache HIT: {} bytes, {} prefix tokens",
2279 cached->data_size, cached->token_count);
2280 }
2281 if (restore_cached_prefix(cached, tokens)) {
2282 return true;
2283 }
2284 logger->warn("Cache restore failed, falling back to full prefill");
2285 } else if (prompt_cache_config_.log_hits) {
2286 logger->info("Prompt cache MISS: processing full prompt");
2287 }
2288
2289 int prefix_tokens = compute_prefix_token_count(messages, params);
2290 return prefill_and_cache_prefix(tokens, prefix_tokens, key);
2291}
2292
2293// ── Multimodal generation (v1.9.11 Phases 5–7 + v2.1.8) ────
2294
2295namespace {
2296
2302bool any_image_in(const std::vector<Message>& messages) {
2303 for (const auto& m : messages) {
2304 if (has_images(m.content_parts)) { return true; }
2305 }
2306 return false;
2307}
2308
2321std::vector<Message> strip_image_parts(
2322 const std::vector<Message>& messages) {
2323 std::vector<Message> out = messages;
2324 for (auto& m : out) {
2325 if (m.content_parts.empty()) { continue; }
2326 m.content = extract_text(m.content_parts);
2327 m.content_parts.clear();
2328 }
2329 return out;
2330}
2331
2349std::vector<Message> substitute_image_markers(
2350 const std::vector<Message>& messages,
2351 ::mtmd_context* ctx,
2352 std::vector<::mtmd_bitmap*>& bitmaps_out) {
2353 std::vector<Message> out;
2354 out.reserve(messages.size());
2355 const std::string marker = mtmd_default_marker();
2356 for (const auto& m : messages) {
2357 Message copy;
2358 copy.role = m.role;
2359 if (m.content_parts.empty()) {
2360 copy.content = m.content;
2361 out.push_back(std::move(copy));
2362 continue;
2363 }
2364 std::string built;
2365 for (const auto& p : m.content_parts) {
2366 if (p.type != ContentPartType::IMAGE) {
2367 built += p.text;
2368 continue;
2369 }
2370 ::mtmd_bitmap* bm = nullptr;
2371 if (!p.image_path.empty()) {
2372 bm = mtmd_helper_bitmap_init_from_file(
2373 ctx, p.image_path.c_str(), /*placeholder=*/false).bitmap;
2374 }
2375 if (bm == nullptr) { return {}; }
2376 bitmaps_out.push_back(bm);
2377 built += marker;
2378 }
2379 copy.content = std::move(built);
2380 out.push_back(std::move(copy));
2381 }
2382 return out;
2383}
2384
2385} // anonymous namespace
2386
2398 const std::string& prompt,
2399 const std::vector<::mtmd_bitmap*>& bitmaps,
2400 std::string& err_msg)
2401{
2402 llama_memory_clear(llama_get_memory(ctx_), true);
2403 ::mtmd_input_text mt{prompt.c_str(), true, true};
2404 auto* chunks = mtmd_input_chunks_init();
2405 std::vector<const ::mtmd_bitmap*> bm_cptrs(
2406 bitmaps.begin(), bitmaps.end());
2407 int32_t tok_rc = mtmd_tokenize(
2408 mtmd_ctx_, chunks, &mt, bm_cptrs.data(), bm_cptrs.size());
2409 if (tok_rc != 0) {
2410 mtmd_input_chunks_free(chunks);
2411 err_msg = "mtmd_tokenize failed (rc="
2412 + std::to_string(tok_rc) + ")";
2414 }
2415 llama_pos new_n_past = 0;
2416 int32_t eval_rc = mtmd_helper_eval_chunks(
2417 mtmd_ctx_, ctx_, chunks, 0, 0,
2418 static_cast<int32_t>(config().n_batch),
2419 true, &new_n_past);
2420 mtmd_input_chunks_free(chunks);
2421 if (eval_rc != 0) {
2422 err_msg = "mtmd_helper_eval_chunks failed (rc="
2423 + std::to_string(eval_rc) + ")";
2425 }
2426 logger->info("Multimodal prefill complete: n_past={}", new_n_past);
2427 return ENTROPIC_OK;
2428}
2429
2441 const GenerationParams& params,
2442 std::function<void(std::string_view token)> on_token,
2443 std::atomic<bool>* cancel,
2444 const std::chrono::steady_clock::time_point& t0)
2445{
2446 GenerationResult result;
2447 // v2.3.10: Sampler seam.
2448 auto sampler = create_sampler(params);
2449 if (!sampler) {
2451 result.error_message = "Sampler factory not initialized";
2452 result.finish_reason = "error";
2453 finalize_result(result, t0);
2454 return result;
2455 }
2456 std::string generated;
2457 int n_generated = 0;
2458 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
2459 while (n_generated < params.max_tokens) {
2460 if (cancel != nullptr
2461 && cancel->load(std::memory_order_acquire)) {
2462 result.finish_reason = "cancelled";
2464 break;
2465 }
2466 auto status = step_token(
2467 *sampler, generated, on_token, stop);
2468 if (status == "continue") { ++n_generated; continue; }
2469 result.finish_reason = (status == "error") ? "error" : "stop";
2470 if (status == "error") {
2472 }
2473 break;
2474 }
2475 finalize_generation(result, generated, n_generated, params, t0);
2476 return result;
2477}
2478
2485 const std::vector<Message>& messages,
2486 const GenerationParams& params,
2487 std::function<void(std::string_view token)> on_token,
2488 std::atomic<bool>* cancel)
2489{
2490 auto t0 = entropic::log::now();
2491 invalidate_resident_kv(); // gh#96: mtmd_prefill mutates seq 0 out-of-band
2492 std::vector<::mtmd_bitmap*> bitmaps;
2493 auto marked = substitute_image_markers(
2494 messages, mtmd_ctx_, bitmaps);
2495 if (marked.empty()) {
2496 for (auto* b : bitmaps) { mtmd_bitmap_free(b); }
2497 GenerationResult err;
2499 err.error_message =
2500 "mtmd_helper_bitmap_init_from_file failed";
2501 return err;
2502 }
2503 auto prompt = render_prompt(marked, params);
2504 logger->info("Multimodal generate: {} images, prompt={} chars, max_tokens={}",
2505 bitmaps.size(), prompt.size(), params.max_tokens);
2506 std::string prefill_err;
2507 auto rc = mtmd_prefill(prompt, bitmaps, prefill_err);
2508 for (auto* b : bitmaps) { mtmd_bitmap_free(b); }
2509 if (rc != ENTROPIC_OK) {
2510 GenerationResult err;
2511 err.error_code = rc;
2512 err.error_message = std::move(prefill_err);
2513 return err;
2514 }
2515 return run_sampling_loop(params, on_token, cancel, t0);
2516}
2517
2518// ── Generation entry points ────────────────────────────────
2519
2536 const std::vector<Message>& messages,
2537 const GenerationParams& params)
2538{
2539 if (!any_image_in(messages)) {
2540 return do_generate_text_only(messages, params);
2541 }
2542 if (has_vision_ && mtmd_ctx_ != nullptr) {
2543 return generate_multimodal(messages, params, nullptr, nullptr);
2544 }
2545 logger->warn("Image content present but model has no vision "
2546 "capability — stripping image parts");
2547 return do_generate_text_only(strip_image_parts(messages), params);
2548}
2549
2556 const std::vector<Message>& messages,
2557 const GenerationParams& params)
2558{
2559 auto t0 = entropic::log::now();
2560 std::string prompt = render_prompt(messages, params);
2561 auto tokens = tokenize(prompt, true);
2562 std::string sys = extract_system_prompt(messages);
2563
2564 logger->info("Generate: {} input tokens, max_tokens={}",
2565 tokens.size(), params.max_tokens);
2566 log_sampler_config(params);
2567
2568 // v2.3.10: Sampler seam.
2569 auto sampler = create_sampler(params);
2570 if (!sampler) { return sampler_init_error(t0); }
2571
2572 if (!run_prefill_cached(tokens, sys, messages, params)) {
2573 return prefill_error();
2574 }
2575
2576 GenerationResult result;
2577 std::string generated;
2578 int n_generated = 0;
2579 std::function<void(std::string_view)> no_cb = nullptr;
2580 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
2581
2582 while (n_generated < params.max_tokens) {
2583 auto status = step_token(
2584 *sampler, generated, no_cb, stop);
2585 if (status == "continue") { ++n_generated; }
2586 else {
2587 result.finish_reason =
2588 (status == "error") ? "error" : "stop";
2589 if (status == "error") {
2591 }
2592 break;
2593 }
2594 }
2595
2596 finalize_generation(result, generated, n_generated, params, t0);
2597 return result;
2598}
2599
2611 const std::vector<Message>& messages,
2612 const GenerationParams& params,
2613 std::atomic<bool>& cancel)
2614{
2615 if (!any_image_in(messages)) {
2616 return do_generate_text_only(messages, params, cancel);
2617 }
2618 if (has_vision_ && mtmd_ctx_ != nullptr) {
2619 return generate_multimodal(messages, params, nullptr, &cancel);
2620 }
2621 logger->warn("Image content present but model has no vision "
2622 "capability — stripping image parts");
2623 return do_generate_text_only(strip_image_parts(messages), params, cancel);
2624}
2625
2637 const std::vector<Message>& messages,
2638 const GenerationParams& params,
2639 std::atomic<bool>& cancel)
2640{
2641 auto t0 = entropic::log::now();
2642 std::string prompt = render_prompt(messages, params);
2643 auto tokens = tokenize(prompt, true);
2644 std::string sys = extract_system_prompt(messages);
2645
2646 logger->info("Generate (cancellable): {} input tokens, max_tokens={}",
2647 tokens.size(), params.max_tokens);
2648 log_sampler_config(params);
2649
2650 auto sampler = create_sampler(params);
2651 if (!sampler) { return sampler_init_error(t0); }
2652
2653 if (!run_prefill_cached(tokens, sys, messages, params)) {
2654 return prefill_error();
2655 }
2656
2657 GenerationResult result;
2658 std::string generated;
2659 int n_generated = 0;
2660 std::function<void(std::string_view)> no_cb = nullptr;
2661
2662 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
2663 while (n_generated < params.max_tokens) {
2664 if (cancel.load(std::memory_order_acquire)) {
2665 result.finish_reason = "cancelled";
2667 break;
2668 }
2669 auto status = step_token(
2670 *sampler, generated, no_cb, stop);
2671 if (status == "continue") { ++n_generated; }
2672 else {
2673 result.finish_reason =
2674 (status == "error") ? "error" : "stop";
2675 if (status == "error") {
2677 }
2678 break;
2679 }
2680 }
2681
2682 finalize_generation(result, generated, n_generated, params, t0);
2683 return result;
2684}
2685
2697 const std::vector<Message>& messages,
2698 const GenerationParams& params,
2699 std::function<void(std::string_view token)> on_token,
2700 std::atomic<bool>& cancel)
2701{
2702 if (!any_image_in(messages)) {
2704 messages, params, on_token, cancel);
2705 }
2706 if (has_vision_ && mtmd_ctx_ != nullptr) {
2707 return generate_multimodal(messages, params, on_token, &cancel);
2708 }
2709 logger->warn("Image content present but model has no vision "
2710 "capability — stripping image parts");
2712 strip_image_parts(messages), params, on_token, cancel);
2713}
2714
2721 const std::vector<Message>& messages,
2722 const GenerationParams& params,
2723 std::function<void(std::string_view token)> on_token,
2724 std::atomic<bool>& cancel)
2725{
2726 auto t0 = entropic::log::now();
2727 auto prompt = render_prompt(messages, params);
2728 auto tokens = tokenize(prompt, true);
2729 auto sys = extract_system_prompt(messages);
2730 logger->info("Stream: {} input tokens, max_tokens={}",
2731 tokens.size(), params.max_tokens);
2732 log_sampler_config(params);
2733
2734 // v2.3.10: Sampler seam.
2735 auto sampler = create_sampler(params);
2736 if (!sampler) { return sampler_init_error(t0); }
2737 if (!run_prefill_cached(tokens, sys, messages, params)) {
2738 return prefill_error();
2739 }
2740 GenerationResult result;
2741 std::string generated;
2742 int n_generated = 0;
2743 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
2744 while (n_generated < params.max_tokens) {
2745 if (cancel.load(std::memory_order_acquire)) {
2746 result.finish_reason = "cancelled";
2748 break;
2749 }
2750 auto status = step_token(
2751 *sampler, generated, on_token, stop);
2752 if (status == "continue") { ++n_generated; }
2753 else {
2754 result.finish_reason =
2755 (status == "error") ? "error" : "stop";
2756 if (status == "error") {
2758 }
2759 break;
2760 }
2761 }
2762 finalize_generation(result, generated, n_generated, params, t0);
2763 return result;
2764}
2765
2779 const std::vector<Message>& /*messages*/,
2780 const GenerationParams& /*params*/,
2781 std::function<void(std::string_view)> /*on_token*/,
2782 std::atomic<bool>& /*cancel*/)
2783{
2784 GenerationResult result;
2786 result.error_message =
2787 "LlamaCppBackend speculative requires an explicit draft "
2788 "backend handle — orchestrator dispatches via "
2789 "generate_speculative_with_draft";
2790 result.finish_reason = "error";
2791 return result;
2792}
2793
2794namespace {
2795
2808common_params_sampling to_common_sampling(
2809 const GenerationParams& params) {
2810 common_params_sampling cps;
2811 cps.temp = params.temperature;
2812 cps.top_k = params.top_k;
2813 cps.top_p = params.top_p;
2814 cps.penalty_repeat = params.repeat_penalty;
2815 // gh#23 MVP items 2 + 3 (v2.3.14 + v2.3.15): wire presence +
2816 // frequency penalty into common-sampling. Counterparts of the
2817 // 3rd + 4th args to `llama_sampler_init_penalties` in the plain
2818 // decode path. Default 0.0f on both preserves bit-for-bit
2819 // speculative output.
2820 cps.penalty_freq = params.frequency_penalty;
2821 cps.penalty_present = params.presence_penalty;
2822 // gh#23 MVP item 4 (v2.3.16): forward logit_bias to common-sampling.
2823 // Empty (default) leaves the speculative chain bit-for-bit
2824 // identical to pre-v2.3.16.
2825 for (auto& [tok, val] : params.logit_bias) {
2826 cps.logit_bias.push_back({tok, val});
2827 }
2828 if (params.seed >= 0) {
2829 cps.seed = static_cast<uint32_t>(params.seed);
2830 }
2831 cps.no_perf = true;
2832 // Mirror entropic's standard sampler chain ordering so the
2833 // speculative path produces output bit-identical to plain decode
2834 // (the v2.1.11 correctness contract). Entropic's `create_sampler`
2835 // builds: penalties → top_k → top_p → min_p → temperature → dist,
2836 // AND SKIPS the temperature sampler when temp == 0 (greedy mode).
2837 // common_sampler appends an extended-temperature sampler that
2838 // differs subtly from "no temp at all" — we omit it for temp=0
2839 // to match entropic exactly. min_p (v2.3.10, gh#23) appended only
2840 // when caller opted in (params.min_p > 0); 0.0 preserves the
2841 // pre-v2.3.10 chain shape bit-for-bit. Other extended filters
2842 // (top_n_sigma, dry, xtc, typical_p) remain stripped.
2843 cps.samplers = {COMMON_SAMPLER_TYPE_PENALTIES,
2844 COMMON_SAMPLER_TYPE_TOP_K,
2845 COMMON_SAMPLER_TYPE_TOP_P};
2846 if (params.min_p > 0.0f) {
2847 cps.samplers.push_back(COMMON_SAMPLER_TYPE_MIN_P);
2848 }
2849 if (params.temperature > 0.0f) {
2850 cps.samplers.push_back(COMMON_SAMPLER_TYPE_TEMPERATURE);
2851 }
2852 cps.min_p = params.min_p;
2853 cps.dry_multiplier = 0.0f;
2854 cps.top_n_sigma = -1.0f;
2855 return cps;
2856}
2857
2874bool spec_prefill_minus_last(
2875 llama_context* ctx, const std::vector<llama_token>& tokens) {
2876 int total = static_cast<int>(tokens.size()) - 1;
2877 if (total <= 0) { return true; }
2878 int n_batch = llama_n_batch(ctx);
2879 for (int off = 0; off < total; off += n_batch) {
2880 int chunk = std::min(n_batch, total - off);
2881 llama_batch batch = llama_batch_get_one(
2882 const_cast<llama_token*>(tokens.data()) + off, chunk);
2883 if (llama_decode(ctx, batch) != 0) { return false; }
2884 }
2885 return true;
2886}
2887
2893GenerationResult spec_error(entropic_error_t code, std::string msg) {
2894 GenerationResult r;
2895 r.error_code = code;
2896 r.error_message = std::move(msg);
2897 r.finish_reason = "error";
2898 return r;
2899}
2900
2901} // anonymous namespace
2902
2911 common_speculative* spec = nullptr;
2912 common_sampler* smpl = nullptr;
2913 llama_context* ctx_tgt = nullptr;
2914 llama_context* ctx_dft = nullptr;
2915 llama_batch batch_tgt{};
2916 bool batch_initialized = false;
2917 llama_seq_id seq_id = 0;
2918 int n_past = 0;
2919 llama_token id_last = 0;
2920 std::vector<llama_token> prompt_tgt;
2921 std::vector<llama_token> draft;
2922 std::string generated;
2923 std::vector<std::string> stop;
2924 int n_generated = 0;
2925 int n_drafted = 0;
2926 int n_accepted = 0;
2927 bool has_eos = false;
2928 std::string finish_reason;
2929 entropic_error_t error_code = ENTROPIC_OK;
2930 std::string error_message;
2931
2932 // ── Checkpoint state (v2.1.11) ──────────────────────────
2933 // Activated when either context reports FULL-only seq_rm
2934 // (no partial removal). The kernel saves+restores draft/target
2935 // state across each speculative round so the underlying
2936 // memory module never sees an attempted partial removal.
2937 // Mirrors the use_ckpt_tgt / use_ckpt_dft flow in upstream's
2938 // speculative-simple example.
2939 bool use_ckpt_tgt = false;
2940 bool use_ckpt_dft = false;
2941 common_prompt_checkpoint ckpt;
2942};
2943
2951 if (state.spec) { common_speculative_free(state.spec); }
2952 if (state.smpl) { common_sampler_free(state.smpl); }
2953 if (state.batch_initialized) {
2954 llama_batch_free(state.batch_tgt);
2955 }
2956}
2957
2965 common_batch_clear(state.batch_tgt);
2966 common_batch_add(state.batch_tgt, state.id_last,
2967 state.n_past, {state.seq_id}, true);
2968 int pos = state.n_past + 1;
2969 for (auto draft_token : state.draft) {
2970 common_batch_add(state.batch_tgt, draft_token, pos,
2971 {state.seq_id}, true);
2972 ++pos;
2973 }
2974}
2975
2984 spec_build_batch(state);
2985 int rc_tgt = llama_decode(state.ctx_tgt, state.batch_tgt);
2986 if (rc_tgt != 0) {
2987 logger->error("Speculative target decode failed: rc={}, "
2988 "n_past={}, draft_size={}",
2989 rc_tgt, state.n_past, state.draft.size());
2990 state.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
2991 state.error_message = "target llama_decode failed";
2992 state.finish_reason = "error";
2993 return false;
2994 }
2995 int rc_dft = llama_decode(state.ctx_dft, state.batch_tgt);
2996 if (rc_dft != 0) {
2997 logger->error("Speculative draft decode failed: rc={}, "
2998 "n_past={}, draft_size={}",
2999 rc_dft, state.n_past, state.draft.size());
3000 state.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
3001 state.error_message = "draft llama_decode failed";
3002 state.finish_reason = "error";
3003 return false;
3004 }
3005 return true;
3006}
3007
3015 auto& dp = common_speculative_get_draft_params(
3016 state.spec, state.seq_id);
3017 dp.drafting = true;
3018 dp.n_max = -1;
3019 dp.n_past = state.n_past;
3020 dp.id_last = state.id_last;
3021 dp.prompt = &state.prompt_tgt;
3022 dp.result = &state.draft;
3023 common_speculative_draft(state.spec);
3024 return static_cast<int>(state.draft.size());
3025}
3026
3040static std::string spec_emit_token(
3041 SpeculativeRunState& state, llama_token id,
3042 const llama_vocab* vocab, int max_tokens,
3043 std::function<void(std::string_view)>& on_token,
3044 std::atomic<bool>& cancel)
3045{
3046 std::string signal;
3047 state.prompt_tgt.push_back(state.id_last);
3048 state.id_last = id;
3049 state.n_generated++;
3050 if (llama_vocab_is_eog(vocab, id)) {
3051 state.has_eos = true;
3052 state.finish_reason = "stop";
3053 signal = "eos";
3054 } else {
3055 const std::string piece =
3056 common_token_to_piece(state.ctx_tgt, id);
3057 state.generated += piece;
3058 if (on_token) { on_token(piece); }
3059 // gh#108: honor stop sequences (params.stop + gh#103 sequential-tool
3060 // close marker) so MTP stops where plain decode would, instead of
3061 // over-generating past the first tool call. state.stop is empty for the
3062 // gh#36 path, so this is a no-op there.
3063 if (check_stop_sequences(state.generated, state.stop)) {
3064 state.finish_reason = "stop";
3065 signal = "stop";
3066 } else if (cancel.load(std::memory_order_acquire)) {
3067 state.error_code = ENTROPIC_ERROR_CANCELLED;
3068 state.finish_reason = "cancelled";
3069 signal = "cancel";
3070 } else if (state.n_generated >= max_tokens) {
3071 state.finish_reason = "length";
3072 signal = "length";
3073 }
3074 }
3075 return signal;
3076}
3077
3090 state.ckpt.update_pos(
3091 static_cast<int64_t>(state.prompt_tgt.size()),
3092 llama_memory_seq_pos_min(
3093 llama_get_memory(state.ctx_tgt), state.seq_id),
3094 llama_memory_seq_pos_max(
3095 llama_get_memory(state.ctx_tgt), state.seq_id));
3096 if (state.use_ckpt_dft) {
3097 state.ckpt.update_dft(state.ctx_dft, state.seq_id,
3098 LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY
3099 | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE);
3100 }
3101}
3102
3110 if (state.use_ckpt_tgt && !state.draft.empty()) {
3111 state.ckpt.update_tgt(state.ctx_tgt, state.seq_id,
3112 LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY
3113 | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE);
3114 }
3115}
3116
3124 constexpr auto flags = LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY
3125 | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE;
3126 if (state.use_ckpt_dft) {
3127 state.ckpt.load_dft(state.ctx_dft, state.seq_id, flags);
3128 }
3129 llama_memory_seq_rm(llama_get_memory(state.ctx_dft),
3130 state.seq_id, state.ckpt.pos_max + 1, -1);
3131}
3132
3143 SpeculativeRunState& state, common_sampler* smpl_save,
3144 std::vector<llama_token>& ids) {
3145 constexpr auto flags = LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY
3146 | LLAMA_STATE_SEQ_FLAGS_ON_DEVICE;
3147 state.draft = std::move(ids);
3148 state.ckpt.load_tgt(state.ctx_tgt, state.seq_id, flags);
3149 llama_memory_seq_rm(llama_get_memory(state.ctx_tgt),
3150 state.seq_id, state.ckpt.pos_max + 1, -1);
3151 state.ckpt.load_dft(state.ctx_dft, state.seq_id, flags);
3152 llama_memory_seq_rm(llama_get_memory(state.ctx_dft),
3153 state.seq_id, state.ckpt.pos_max + 1, -1);
3154 state.prompt_tgt.resize(static_cast<size_t>(state.ckpt.n_tokens));
3155 state.n_past = static_cast<int>(state.prompt_tgt.size());
3156 // Sampler clone is non-null only when use_ckpt_tgt is set
3157 common_sampler_free(state.smpl);
3158 state.smpl = smpl_save;
3159}
3160
3186 llama_memory_seq_rm(llama_get_memory(state.ctx_tgt),
3187 state.seq_id, state.n_past, -1);
3188 llama_memory_seq_rm(llama_get_memory(state.ctx_dft),
3189 state.seq_id, state.n_past, -1);
3190}
3191
3199 SpeculativeRunState& state,
3200 const std::vector<llama_token>& ids,
3201 const llama_vocab* vocab, int max_tokens,
3202 std::function<void(std::string_view)>& on_token,
3203 std::atomic<bool>& cancel) {
3204 bool stop = false;
3205 for (auto id : ids) {
3206 auto signal = spec_emit_token(
3207 state, id, vocab, max_tokens, on_token, cancel);
3208 if (!signal.empty()) { stop = true; break; }
3209 }
3210 return stop;
3211}
3212
3234 // Skip drafting if the previous round restored a partial accept
3235 // into state.draft (carry-over from rollback).
3236 if (!state.draft.empty()) {
3237 return static_cast<int>(state.draft.size());
3238 }
3239 spec_ckpt_save_dft(state);
3240 int drafted = spec_run_draft(state);
3241 spec_ckpt_save_tgt(state);
3242 spec_ckpt_restore_dft(state);
3243 return drafted;
3244}
3245
3252 SpeculativeRunState& state,
3253 const llama_vocab* vocab,
3254 int max_tokens,
3255 std::function<void(std::string_view)>& on_token,
3256 std::atomic<bool>& cancel)
3257{
3258 int draft_size_before = spec_prepare_draft(state);
3259
3260 if (!spec_decode_both(state)) { return false; }
3261
3262 common_sampler* smpl_save = nullptr;
3263 if (state.use_ckpt_tgt) {
3264 smpl_save = common_sampler_clone(state.smpl);
3265 }
3266 auto ids = common_sampler_sample_and_accept_n(
3267 state.smpl, state.ctx_tgt, state.draft);
3268 int accepted = static_cast<int>(ids.size()) - 1;
3269 if (accepted < 0) { accepted = 0; }
3270
3271 // Partial acceptance on a FULL-seq_rm context: rollback to
3272 // checkpoint, set draft = accepted, re-loop without emitting.
3273 if (state.use_ckpt_tgt
3274 && static_cast<int>(ids.size()) - 1
3275 < static_cast<int>(state.draft.size())) {
3276 spec_rollback_partial(state, smpl_save, ids);
3277 return true;
3278 }
3279 if (smpl_save) { common_sampler_free(smpl_save); }
3280
3281 common_speculative_accept(state.spec, state.seq_id, accepted);
3282 state.n_drafted += draft_size_before;
3283 state.n_accepted += accepted;
3284 // n_past advances by ids.size() total: one slot for id_last
3285 // (the post-id_last position the next id will occupy), plus
3286 // `accepted` slots for the drafted tokens the sampler agreed
3287 // with. Matches speculative-simple's n_past++ in batch_add +
3288 // n_past += ids.size() - 1 sequence.
3289 state.n_past += static_cast<int>(ids.size());
3290
3291 bool stop = spec_commit_accepted(
3292 state, ids, vocab, max_tokens, on_token, cancel);
3293 state.draft.clear();
3295 return !stop;
3296}
3297
3310static std::string spec_check_preconditions(
3311 bool target_active, bool draft_active,
3312 llama_context* ctx_tgt, llama_context* ctx_dft) {
3313 // Defense-in-depth arch gate — orchestrator's
3314 // check_speculative_compat is the primary gate; a direct caller
3315 // into the kernel must also be refused on recurrent / hybrid
3316 // targets (Session 5 Gate A: hybrid SSM state diverges across
3317 // split-prefill boundaries; bit-identical unreachable at this pin).
3318 std::string err;
3319 const llama_model* model_tgt = llama_get_model(ctx_tgt);
3320 int cap_tgt = common_context_can_seq_rm(ctx_tgt);
3321 int cap_dft = common_context_can_seq_rm(ctx_dft);
3322 logger->info("Speculative seq_rm capability: target={}, draft={} "
3323 "(0=NO, 1=PART, 2=FULL)", cap_tgt, cap_dft);
3324 if (!target_active || !draft_active) {
3325 err = "speculative requires ACTIVE target + draft";
3326 } else if (llama_model_is_recurrent(model_tgt)
3327 || llama_model_is_hybrid(model_tgt)) {
3328 err = "speculative refused: architecture (target is "
3329 "recurrent or hybrid; see proposal Implementation "
3330 "Log Gate A)";
3331 } else if (cap_tgt == COMMON_CONTEXT_SEQ_RM_TYPE_NO
3332 || cap_dft == COMMON_CONTEXT_SEQ_RM_TYPE_NO) {
3333 // NO is the only unsupported seq_rm case — the kernel has
3334 // both PART fast-path and FULL checkpoint paths.
3335 err = "speculative kernel requires at least FULL seq_rm "
3336 "(target/draft reported NO seq_rm at all)";
3337 }
3338 return err;
3339}
3340
3370 SpeculativeRunState& state, llama_model* model_tgt,
3371 const GenerationParams& params, int n_draft_max,
3372 const std::string& draft_path) {
3373 auto common_sampling = to_common_sampling(params);
3374 state.smpl = common_sampler_init(model_tgt, common_sampling);
3375 if (!state.smpl) { return "common_sampler_init failed"; }
3376
3377 common_params_speculative spec_params;
3378 spec_params.types = {COMMON_SPECULATIVE_TYPE_DRAFT_SIMPLE};
3379 spec_params.draft.n_max = (n_draft_max > 0) ? n_draft_max : 16;
3380 spec_params.draft.ctx_tgt = state.ctx_tgt;
3381 spec_params.draft.ctx_dft = state.ctx_dft;
3382 // Upstream gates DRAFT_SIMPLE on a non-empty draft path
3383 // (see common/speculative.cpp:875). Required even though we
3384 // provide already-loaded contexts.
3385 spec_params.draft.mparams.path = draft_path;
3386 state.spec = common_speculative_init(spec_params, 1);
3387 if (!state.spec) {
3388 common_sampler_free(state.smpl);
3389 state.smpl = nullptr;
3390 return "common_speculative_init failed";
3391 }
3392
3393 common_speculative_begin(state.spec, state.seq_id, state.prompt_tgt);
3394 state.batch_tgt = llama_batch_init(llama_n_batch(state.ctx_tgt), 0, 1);
3395 state.batch_initialized = true;
3396 // Checkpoint flow lights up when either context can only do
3397 // FULL-sequence removal. Mirrors speculative-simple's
3398 // use_ckpt_{tgt,dft}.
3399 state.use_ckpt_tgt = common_context_can_seq_rm(state.ctx_tgt)
3400 == COMMON_CONTEXT_SEQ_RM_TYPE_FULL;
3401 state.use_ckpt_dft = common_context_can_seq_rm(state.ctx_dft)
3402 == COMMON_CONTEXT_SEQ_RM_TYPE_FULL;
3403 return "";
3404}
3405
3411static std::string spec_init_run(
3412 SpeculativeRunState& state, llama_model* model_tgt,
3413 const std::vector<llama_token>& tokens,
3414 const GenerationParams& params, int n_draft_max,
3415 const std::string& draft_path) {
3416 state.id_last = tokens.back();
3417 state.prompt_tgt.assign(tokens.begin(), tokens.end() - 1);
3418 state.n_past = static_cast<int>(tokens.size()) - 1;
3419
3420 llama_memory_clear(llama_get_memory(state.ctx_tgt), true);
3421 llama_memory_clear(llama_get_memory(state.ctx_dft), true);
3422
3423 if (!spec_prefill_minus_last(state.ctx_tgt, tokens)
3424 || !spec_prefill_minus_last(state.ctx_dft, tokens)) {
3425 return "speculative prefill failed";
3426 }
3428 state, model_tgt, params, n_draft_max, draft_path);
3429}
3430
3436static void spec_run_loop(
3437 SpeculativeRunState& state, const llama_vocab* vocab,
3438 int max_tokens,
3439 std::function<void(std::string_view)>& on_token,
3440 std::atomic<bool>& cancel) {
3441 while (state.n_generated < max_tokens) {
3442 if (cancel.load(std::memory_order_acquire)) {
3443 state.error_code = ENTROPIC_ERROR_CANCELLED;
3444 state.finish_reason = "cancelled";
3445 break;
3446 }
3447 if (!spec_accept_round(state, vocab, max_tokens,
3448 on_token, cancel)) {
3449 break;
3450 }
3451 }
3452 if (state.finish_reason.empty()) {
3453 state.finish_reason = (state.n_generated >= max_tokens)
3454 ? "length" : "stop";
3455 }
3456}
3457
3470 SpeculativeRunState& state,
3471 std::chrono::steady_clock::time_point t0) {
3472 GenerationResult result;
3473 result.content = state.generated;
3474 result.token_count = state.n_generated;
3475 result.finish_reason = state.finish_reason;
3476 result.error_code = state.error_code;
3477 result.error_message = state.error_message;
3478 // gh#106: surface the draft/accept counts so callers (and the MTP
3479 // engagement test) can verify the kernel actually ran + accepted.
3480 result.n_drafted = state.n_drafted;
3481 result.n_accepted = state.n_accepted;
3482 result.generation_time_ms =
3483 entropic::log::elapsed_ms(t0, entropic::log::now());
3484 // gh#108: the speculative path previously left throughput_tok_s=0.0 — the
3485 // one metric the feature exists for. Compute it like finalize_result.
3486 if (result.token_count > 0 && result.generation_time_ms > 0.0) {
3487 result.throughput_tok_s =
3488 static_cast<double>(result.token_count)
3489 / result.generation_time_ms * 1000.0;
3490 }
3491 if (state.n_drafted > 0) {
3492 const float accept_rate =
3493 static_cast<float>(state.n_accepted)
3494 / static_cast<float>(state.n_drafted);
3495 logger->info("Speculative: generated={}, drafted={}, "
3496 "accepted={}, accept_rate={:.3f}",
3497 state.n_generated, state.n_drafted,
3498 state.n_accepted, accept_rate);
3499 }
3500 spec_cleanup(state);
3501 return result;
3502}
3503
3545 llama_context* ctx_tgt, llama_context* ctx_dft, llama_model* model_tgt,
3546 const std::vector<llama_token>& tokens, const GenerationParams& params,
3547 std::function<void(std::string_view)>& on_token,
3548 std::atomic<bool>& cancel, int n_draft_max,
3549 const std::string& draft_path,
3550 std::chrono::steady_clock::time_point t0) {
3551 SpeculativeRunState state;
3552 state.ctx_tgt = ctx_tgt;
3553 state.ctx_dft = ctx_dft;
3554 auto init_err = spec_init_run(state, model_tgt, tokens, params,
3555 n_draft_max, draft_path);
3556 if (!init_err.empty()) {
3557 spec_cleanup(state);
3558 return spec_error(ENTROPIC_ERROR_GENERATE_FAILED,
3559 std::move(init_err));
3560 }
3561 spec_run_loop(state, llama_model_get_vocab(model_tgt),
3562 params.max_tokens, on_token, cancel);
3563 return spec_finalize(state, t0);
3564}
3565
3572 const std::vector<Message>& messages,
3573 const GenerationParams& params,
3574 std::function<void(std::string_view)> on_token,
3575 std::atomic<bool>& cancel,
3576 LlamaCppBackend& draft,
3577 int n_draft_max,
3578 const std::string& draft_path)
3579{
3580 auto t0 = entropic::log::now();
3581 invalidate_resident_kv(); // gh#96: speculative path manages seq 0 itself
3582 auto pre_err = spec_check_preconditions(
3583 is_active(), draft.is_active(), ctx_, draft.ctx_);
3584 GenerationResult result;
3585 if (!pre_err.empty()) {
3586 entropic_error_t code =
3587 (pre_err.find("requires ACTIVE") != std::string::npos)
3590 result = spec_error(code, std::move(pre_err));
3591 } else {
3592 auto prompt = render_prompt(messages, params);
3593 auto tokens = tokenize(prompt, true);
3594 if (tokens.size() < 2) {
3595 result = spec_error(ENTROPIC_ERROR_GENERATE_FAILED,
3596 "speculative prompt must have at least 2 tokens");
3597 } else {
3598 logger->info("Speculative: {} input tokens, max_tokens={}, "
3599 "n_draft_max={}",
3600 tokens.size(), params.max_tokens, n_draft_max);
3601 result = spec_run_from_tokens(
3602 ctx_, draft.ctx_, model_, tokens, params, on_token,
3603 cancel, n_draft_max, draft_path, t0);
3604 }
3605 }
3606 return result;
3607}
3608
3609// ── gh#106 (v2.9.0): target-owned MTP speculative kernel ───────────
3610//
3611// Distinct from the gh#36 separate-draft kernel above. The MTP head
3612// (ctx_dft) shares the target's KV via ctx_other, so the CALLER only
3613// ever decodes ctx_tgt; the impl owns every ctx_dft decode. The loop is
3614// draft → decode(ctx_tgt) → process → sample_and_accept_n → accept,
3615// mirroring extern/llama.cpp/tools/server/server-context.cpp. Reuses the
3616// gh#36 file-local helpers (spec_build_batch / spec_emit_token /
3617// spec_commit_accepted / spec_trim_rejected_drafts / spec_finalize /
3618// spec_cleanup / spec_error / to_common_sampling) — only the decode step
3619// and the prefill differ. NO checkpoint dance: shared-KV gemma4 targets
3620// are PART-seq_rm, so the FULL-only rollback path never applies.
3621
3622namespace {
3623
3634int mtp_run_draft(SpeculativeRunState& state, int n_max) {
3635 auto& dp = common_speculative_get_draft_params(state.spec, state.seq_id);
3636 dp.drafting = true;
3637 dp.n_max = n_max;
3638 dp.n_past = state.n_past;
3639 dp.id_last = state.id_last;
3640 dp.prompt = &state.prompt_tgt;
3641 dp.result = &state.draft;
3642 common_speculative_draft(state.spec);
3643 return static_cast<int>(state.draft.size());
3644}
3645
3657bool mtp_decode_and_process(SpeculativeRunState& state) {
3658 spec_build_batch(state); // [id_last@n_past, draft@n_past+1 ...]
3659 if (llama_decode(state.ctx_tgt, state.batch_tgt) != 0) {
3660 state.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
3661 state.error_message = "MTP target decode failed";
3662 state.finish_reason = "error";
3663 return false;
3664 }
3665 if (!common_speculative_process(state.spec, state.batch_tgt)) {
3666 state.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
3667 state.error_message = "common_speculative_process failed";
3668 state.finish_reason = "error";
3669 return false;
3670 }
3671 return true;
3672}
3673
3680bool mtp_accept_round(
3681 SpeculativeRunState& state, int n_max, const llama_vocab* vocab,
3682 int max_tokens, std::function<void(std::string_view)>& on_token,
3683 std::atomic<bool>& cancel) {
3684 int drafted = mtp_run_draft(state, n_max);
3685 if (!mtp_decode_and_process(state)) { return false; }
3686 auto ids = common_sampler_sample_and_accept_n(
3687 state.smpl, state.ctx_tgt, state.draft);
3688 int accepted = static_cast<int>(ids.size()) - 1;
3689 if (accepted < 0) { accepted = 0; }
3690 // gh#108: only accept into the spec when this round actually drafted.
3691 // common_speculative_accept asserts impl_last[seq] (speculative.cpp:1650),
3692 // which is set ONLY for a non-empty draft (1604/1614) — a zero-draft round
3693 // would abort. The round still progresses by one token (the bonus in ids),
3694 // and process() already updated pending_h, so skipping accept is equivalent.
3695 if (drafted > 0) {
3696 common_speculative_accept(state.spec, state.seq_id, accepted);
3697 }
3698 state.n_drafted += drafted;
3699 state.n_accepted += accepted;
3700 // Same layout as gh#36: id_last fills one slot at n_past, the
3701 // `accepted` drafts fill the next slots — n_past advances by ids.size().
3702 state.n_past += static_cast<int>(ids.size());
3703 bool stop = spec_commit_accepted(
3704 state, ids, vocab, max_tokens, on_token, cancel);
3705 state.draft.clear();
3707 return !stop;
3708}
3709
3718bool mtp_process_chunk(SpeculativeRunState& state, int off, int chunk) {
3719 common_batch_clear(state.batch_tgt);
3720 for (int j = 0; j < chunk; ++j) {
3721 common_batch_add(state.batch_tgt, state.prompt_tgt[off + j],
3722 off + j, {state.seq_id}, false);
3723 }
3724 if (llama_decode(state.ctx_tgt, state.batch_tgt) != 0) { return false; }
3725 return common_speculative_process(state.spec, state.batch_tgt);
3726}
3727
3735bool mtp_prefill_and_seed(SpeculativeRunState& state) {
3736 int total = static_cast<int>(state.prompt_tgt.size());
3737 if (total == 0) { return true; } // 1-token prompt: round 1 drafts cold
3738 int n_batch = llama_n_batch(state.ctx_tgt);
3739 for (int off = 0; off < total; off += n_batch) {
3740 int chunk = std::min(n_batch, total - off);
3741 if (!mtp_process_chunk(state, off, chunk)) { return false; }
3742 }
3743 return true;
3744}
3745
3755std::string mtp_init_decoder(
3756 SpeculativeRunState& state, llama_model* model_tgt,
3757 const GenerationParams& params, int n_max) {
3758 auto common_sampling = to_common_sampling(params);
3759 state.smpl = common_sampler_init(model_tgt, common_sampling);
3760 if (!state.smpl) { return "common_sampler_init failed"; }
3761 common_params_speculative sp;
3762 sp.types = {COMMON_SPECULATIVE_TYPE_DRAFT_MTP};
3763 sp.draft.n_max = n_max;
3764 sp.draft.ctx_tgt = state.ctx_tgt;
3765 sp.draft.ctx_dft = state.ctx_dft;
3766 state.spec = common_speculative_init(sp, 1);
3767 if (!state.spec) {
3768 common_sampler_free(state.smpl);
3769 state.smpl = nullptr;
3770 return "common_speculative_init (MTP) failed";
3771 }
3772 state.batch_tgt = llama_batch_init(llama_n_batch(state.ctx_tgt), 0, 1);
3773 state.batch_initialized = true;
3774 return "";
3775}
3776
3786std::string mtp_init_run(
3787 SpeculativeRunState& state, llama_model* model_tgt,
3788 const std::vector<llama_token>& tokens,
3789 const GenerationParams& params, int n_max) {
3790 state.id_last = tokens.back();
3791 state.prompt_tgt.assign(tokens.begin(), tokens.end() - 1);
3792 state.n_past = static_cast<int>(tokens.size()) - 1;
3793 llama_memory_clear(llama_get_memory(state.ctx_tgt), true);
3794 auto err = mtp_init_decoder(state, model_tgt, params, n_max);
3795 if (!err.empty()) { return err; }
3796 if (!mtp_prefill_and_seed(state)) { return "MTP prefill/process failed"; }
3797 common_speculative_begin(state.spec, state.seq_id, state.prompt_tgt);
3798 return "";
3799}
3800
3806void mtp_run_loop(
3807 SpeculativeRunState& state, int n_max, const llama_vocab* vocab,
3808 int max_tokens, std::function<void(std::string_view)>& on_token,
3809 std::atomic<bool>& cancel) {
3810 while (state.n_generated < max_tokens) {
3811 if (cancel.load(std::memory_order_acquire)) {
3812 state.error_code = ENTROPIC_ERROR_CANCELLED;
3813 state.finish_reason = "cancelled";
3814 break;
3815 }
3816 if (!mtp_accept_round(state, n_max, vocab, max_tokens,
3817 on_token, cancel)) {
3818 break;
3819 }
3820 }
3821 if (state.finish_reason.empty()) {
3822 state.finish_reason = (state.n_generated >= max_tokens)
3823 ? "length" : "stop";
3824 }
3825}
3826
3832GenerationResult mtp_run_from_tokens(
3833 llama_context* ctx_tgt, llama_context* ctx_dft, llama_model* model_tgt,
3834 const std::vector<llama_token>& tokens, const GenerationParams& params,
3835 std::function<void(std::string_view)>& on_token,
3836 std::atomic<bool>& cancel, int n_max,
3837 const std::vector<std::string>& stop,
3838 std::chrono::steady_clock::time_point t0) {
3839 SpeculativeRunState state;
3840 state.ctx_tgt = ctx_tgt;
3841 state.ctx_dft = ctx_dft;
3842 state.stop = stop; // gh#108: MTP honors stop sequences (effective_stop)
3843 auto init_err = mtp_init_run(state, model_tgt, tokens, params, n_max);
3844 if (!init_err.empty()) {
3845 spec_cleanup(state);
3846 return spec_error(ENTROPIC_ERROR_GENERATE_FAILED,
3847 std::move(init_err));
3848 }
3849 mtp_run_loop(state, n_max, llama_model_get_vocab(model_tgt),
3850 params.max_tokens, on_token, cancel);
3851 return spec_finalize(state, t0);
3852}
3853
3854} // anonymous namespace
3855
3873 const GenerationParams& params,
3874 const std::function<void(std::string_view)>& on_token,
3875 const std::string& head_path, int n_max) {
3876 GenerationResult r; // ENTROPIC_OK by default → proceed
3877 std::string reason = mtp_unsupported_reason(
3878 params.temperature, !params.grammar.empty(),
3879 static_cast<bool>(on_token));
3880 if (!is_active()) {
3881 r = spec_error(ENTROPIC_ERROR_INVALID_STATE,
3882 "MTP requires an ACTIVE target");
3883 } else if (!reason.empty()) {
3884 r = spec_error(ENTROPIC_ERROR_SPECULATIVE_INCOMPATIBLE_CONFIG, reason);
3885 } else if (!setup_mtp_draft(head_path, n_max)) {
3886 r = spec_error(ENTROPIC_ERROR_LOAD_FAILED, last_error_);
3887 } else if (1 + mtp_n_max_ > llama_n_batch(ctx_)) {
3889 "speculative.n_draft+1 (" + std::to_string(1 + mtp_n_max_)
3890 + ") exceeds n_batch (" + std::to_string(llama_n_batch(ctx_))
3891 + "); reduce n_draft or raise n_batch");
3892 }
3893 return r;
3894}
3895
3907 const std::vector<Message>& messages,
3908 const GenerationParams& params,
3909 std::function<void(std::string_view)> on_token,
3910 std::atomic<bool>& cancel,
3911 const std::string& head_path,
3912 int n_max)
3913{
3914 auto t0 = entropic::log::now();
3915 std::lock_guard<std::mutex> lk(mtp_mutex_); // serialise vs teardown
3916 GenerationResult result = mtp_guard(params, on_token, head_path, n_max);
3917 if (result.error_code != ENTROPIC_OK) {
3918 return result;
3919 }
3920 invalidate_resident_kv(); // MTP kernel owns seq 0 itself
3921 auto tokens = tokenize(render_prompt(messages, params), true);
3922 if (tokens.size() < 2) {
3923 return spec_error(ENTROPIC_ERROR_GENERATE_FAILED,
3924 "MTP prompt must have at least 2 tokens");
3925 }
3926 logger->info("MTP: {} input tokens, max_tokens={}, n_max={}",
3927 tokens.size(), params.max_tokens, mtp_n_max_);
3928 return mtp_run_from_tokens(ctx_, mtp_draft_ctx_, model_, tokens, params,
3929 on_token, cancel, mtp_n_max_,
3930 effective_stop(params), t0); // gh#108: honor stops
3931}
3932
3942 const std::string& prompt,
3943 const GenerationParams& params)
3944{
3945 auto t0 = entropic::log::now();
3946 invalidate_resident_kv(); // gh#96: decode_loop/run_prefill mutate seq 0
3947 auto tokens = tokenize(prompt, false);
3948
3949 logger->info("Complete: {} input tokens, max_tokens={}",
3950 tokens.size(), params.max_tokens);
3951 log_sampler_config(params);
3952 auto result = decode_loop(tokens, params, nullptr, nullptr);
3953 finalize_result(result, t0);
3954 return result;
3955}
3956
3957// ── Architecture detection (v1.9.13) ───────────────────────
3958
3966 return is_recurrent_;
3967}
3968
3969// ── Capability overrides (v1.9.13) ─────────────────────────
3970
3979 int idx = static_cast<int>(cap);
3980 int count = static_cast<int>(BackendCapability::_COUNT);
3981 if (idx < 0 || idx >= count) {
3982 return false;
3983 }
3984
3985 // Static capabilities: true = always supported. Length must equal
3986 // BackendCapability::_COUNT — trailing entries get appended as new
3987 // capabilities are introduced (gh#53 added AUDIO at index 12).
3988 static constexpr bool always[] = {
3989 false, false, true, true, true, true,
3990 false, true, true, false, false, true,
3991 false, // AUDIO — dynamic only (mtmd_support_audio)
3992 };
3993
3994 // Dynamic capabilities override the static table
3995 bool result = always[idx];
3996 if (!result) {
3997 result = (cap == BackendCapability::KV_CACHE && !is_recurrent())
3999 || (cap == BackendCapability::VISION
4000 && !config().mmproj_path.empty())
4001 || (cap == BackendCapability::AUDIO
4002 && mtmd_ctx_ != nullptr
4003 && mtmd_support_audio(mtmd_ctx_))
4005 && !is_recurrent());
4006 }
4007 return result;
4008}
4009
4017 return "llama.cpp";
4018}
4019
4027 BackendInfo bi;
4028 bi.name = "llama.cpp";
4029#if defined(ENTROPIC_BACKEND_CUDA)
4030 bi.compute_device = "cuda";
4031#elif defined(ENTROPIC_BACKEND_VULKAN)
4032 bi.compute_device = "vulkan";
4033#else
4034 bi.compute_device = "cpu";
4035#endif
4036 bi.model_format = "gguf";
4037
4038 if (state() != ModelState::COLD && model_ != nullptr) {
4039 bi.architecture = is_recurrent() ? "recurrent" : "transformer";
4042 bi.parameter_count = llama_model_n_params(model_);
4043 bi.vram_bytes = 0;
4044 bi.ram_bytes = llama_model_size(model_);
4045
4046 char desc[256] = {};
4047 llama_model_desc(model_, desc, sizeof(desc));
4048 bi.quantization = desc;
4049 }
4050 return bi;
4051}
4052
4061 if (ctx_ == nullptr) {
4062 return false;
4063 }
4064 auto mem = llama_get_memory(ctx_);
4065 if (seq_id < 0) {
4066 llama_memory_clear(mem, true);
4067 } else {
4068 llama_memory_seq_rm(mem, seq_id, -1, -1);
4069 }
4070 return true;
4071}
4072
4090 int seq_id, std::vector<uint8_t>& buffer) const {
4091 if (ctx_ == nullptr) { return false; }
4092 size_t sz = llama_state_seq_get_size(
4093 ctx_, static_cast<llama_seq_id>(seq_id));
4094 if (sz == 0) { return false; }
4095 buffer.resize(sz);
4096 size_t written = llama_state_seq_get_data(
4097 ctx_, buffer.data(), sz,
4098 static_cast<llama_seq_id>(seq_id));
4099 return written == sz;
4100}
4101
4117 int seq_id, const std::vector<uint8_t>& buffer) {
4118 if (ctx_ == nullptr || buffer.empty()) { return false; }
4119 size_t result = llama_state_seq_set_data(
4120 ctx_, buffer.data(), buffer.size(),
4121 static_cast<llama_seq_id>(seq_id));
4122 return result > 0;
4123}
4124
4125} // namespace entropic
ChatAdapter concrete base class.
gh#98 (v2.8.0) same-prefix batch-generation decision logic.
virtual std::vector< GenerationResult > do_generate_batch(const std::vector< std::vector< Message > > &requests, const std::vector< GenerationParams > &params, std::atomic< bool > &cancel)
Subclass same-prefix batch generation (gh#98, v2.8.0).
Definition backend.h:535
std::string last_error_
Last error message for diagnostics.
Definition backend.h:726
bool is_active() const
True when state is ACTIVE.
Definition backend.h:249
ModelState state() const
Current lifecycle state (lock-free read).
Definition backend.h:241
const ModelConfig & config() const
Stored model config.
Definition backend.h:320
int context_length() const
Model's context window size.
Definition backend.h:282
std::atomic< ModelState > state_
State transition slot accessible to subclasses for test-only injection.
Definition backend.h:752
LlamaCppBackend — common llama.cpp patterns (15% layer).
bool parse_params_valid_
True once a tooled render snapshotted.
int last_gen_decode_calls_
gh#98: batched-decode step count of last batch
bool load_gpu_model()
Load the GGUF model onto the GPU (do_activate step 1).
bool do_load(const ModelConfig &config) override
Load model into CPU RAM (COLD → WARM).
bool do_supports(BackendCapability cap) const override
Declare llama.cpp backend capabilities.
std::vector< GenerationResult > build_batch_results(std::vector< BatchSeq > &seqs)
Detokenize each sequence into a GenerationResult.
std::vector< std::string > effective_stop(const GenerationParams &params) const
params.stop + the sequential tool-call close marker, if applicable.
double last_prefill_ms_
gh#96: prefill wall-clock ms of last generate()
int last_input_tokens_
gh#97: tokenized prompt size of last generate()
GenerationResult decode_loop(const std::vector< llama_token > &tokens, const GenerationParams &params, std::function< void(std::string_view)> on_token, std::atomic< bool > *cancel)
Core decode loop — shared by generate and streaming.
bool is_recurrent_
True if loaded model is recurrent (GDN/Mamba/RWKV).
bool try_warm_reuse(const std::vector< llama_token > &tokens)
gh#96 (v2.7.5): try incremental prefill against resident KV.
LogprobResult do_evaluate_logprobs(const int32_t *tokens, int n_tokens) override
Evaluate per-token log-probabilities via sequential decode.
std::string do_backend_name() const override
Return backend name.
bool is_hybrid_
gh#97: attention + recurrent/SSM memory
bool do_save_state(int seq_id, std::vector< uint8_t > &buffer) const override
Capture a sequence's KV cache into a byte buffer.
std::string render_prompt(const std::vector< Message > &messages, const GenerationParams &params)
Generation render seam: common_chat-with-tools or legacy (gh#87).
std::unique_ptr< PromptCache > prompt_cache_
KV prefix cache (v1.8.3)
void teardown_mtp_draft()
Free the MTP head context + model (gh#106 lifecycle).
std::string parse_generation_prompt_
Last TOOLED render's gen prompt.
std::vector< GenerationResult > run_batched_decode(const std::vector< std::vector< llama_token > > &toks, const std::vector< GenerationParams > &params, std::size_t shared, std::atomic< bool > &cancel)
Run the gh#98 multi-seq batched decode (v2.8.0).
GenerationResult do_generate(const std::vector< Message > &messages, const GenerationParams &params) override
Generate a complete response using chat template.
void reload_model_cpu_only()
Reload the model CPU-only for the WARM state (do_deactivate tail).
std::string render_with_tools(const std::vector< Message > &messages, const GenerationParams &params)
Render messages through common_chat WITH the active tools.
void sample_batch_active(std::vector< BatchSeq > &seqs)
Sample+accept+classify each still-active sequence.
bool common_chat_parse_reliable() const
True iff common_chat parsing is reliable for the last render (gh#87).
std::string active_tools_json_
MCP tool defs for next render.
GenerationResult do_complete(const std::string &prompt, const GenerationParams &params) override
Raw text completion without chat template.
int last_prefill_tokens_
gh#96: prompt tokens decoded by last generate()
std::vector< llama_token > tokenize(const std::string &text, bool add_special) const
Tokenize text using model vocabulary.
bool create_inference_context()
Create the llama context + prompt cache (do_activate step 2).
const llama_vocab * vocab_
Vocabulary (from model_)
std::string tool_call_close_marker() const override
Tool-call close marker for the captured chat format (gh#103).
bool have_chat_params_
True once a tool render captured params.
int compute_prefix_token_count(const std::vector< Message > &messages, const GenerationParams &params)
Compute token count of system messages only.
std::unique_ptr< SamplerFactory > sampler_factory_
Factory used by the decode loop to build per-generation samplers.
void release_temp_seqs(std::vector< BatchSeq > &seqs)
Release every batch sequence's temp seq_id (seq 0 excluded).
std::string detokenize(llama_token token) const
Detokenize a single token.
void set_active_tools(const std::string &tools_json)
Stage tool definitions for the next common_chat render (gh#87).
void init_mmproj_if_configured()
Initialize the libmtmd context if mmproj is configured.
int last_chat_format_
Captured common_chat_format.
GenerationResult generate_speculative_with_draft(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel, LlamaCppBackend &draft, int n_draft_max, const std::string &draft_path)
Speculative-decoding kernel with explicit draft backend.
llama_context * ctx_
Inference context (ACTIVE)
bool run_prefill(const std::vector< llama_token > &tokens)
Run batched prefill on input tokens.
GenerationResult run_sampling_loop(const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > *cancel, const std::chrono::steady_clock::time_point &t0)
Sample tokens until stop / max_tokens / cancel.
llama_seq_id next_temp_seq_id_
gh#98: monotonic high-water for NEW temp seq_ids (the old 1 + size() handed out duplicates when the p...
std::string last_generation_prompt_
Captured generation_prompt.
GenerationResult mtp_guard(const GenerationParams &params, const std::function< void(std::string_view)> &on_token, const std::string &head_path, int n_max)
Validate MTP run preconditions (gh#108, fail-fast/fail-loud).
bool restore_cached_prefix(const CacheEntry *cached, const std::vector< llama_token > &tokens)
Restore KV state from cache and decode remaining tokens.
void save_prefix_to_cache(const CacheKey &key, int prefix_tokens)
Capture seq 0 KV state and store under the given key.
std::vector< int32_t > tokenize_text(const std::string &text) const override
Tokenize text to token IDs using model vocabulary.
int mtp_n_max_
MTP draft window (n_max) of the live head.
bool is_recurrent() const
Check if loaded model is recurrent.
std::string step_token(Sampler &sampler, std::string &generated, std::function< void(std::string_view)> &on_token, const std::vector< std::string > &stop)
Generate one token and append to output.
GenerationResult generate_after_prefill(Sampler &sampler, const GenerationParams &params, std::function< void(std::string_view)> on_token, std::atomic< bool > *cancel)
The post-prefill sampling loop (extracted from decode_loop).
entropic_error_t mtmd_prefill(const std::string &prompt, const std::vector<::mtmd_bitmap * > &bitmaps, std::string &err_msg)
Run mtmd_tokenize + mtmd_helper_eval_chunks on a prompt.
void run_batch_gen_loop(std::vector< BatchSeq > &seqs, int max_steps, std::atomic< bool > &cancel)
Decode all sequences together until each finishes.
bool run_prefill_cached(const std::vector< llama_token > &tokens, const std::string &system_prompt, const std::vector< Message > &messages, const GenerationParams &params)
Run prefill with prompt cache integration.
std::string mtp_head_path_
Path the live mtp_draft_ctx_ was built from.
GenerationResult do_generate_text_only(const std::vector< Message > &messages, const GenerationParams &params)
Text-only batch generation (extracted from do_generate).
bool do_restore_state(int seq_id, const std::vector< uint8_t > &buffer) override
Restore a sequence's KV cache from a byte buffer.
std::string apply_chat_template(const std::vector< Message > &messages, const GenerationParams &params) const
Apply chat template to messages.
CommonChatResult parse_response(const std::string &raw) const
Parse a raw model emission via the last captured render params.
bool prefill_batch_suffixes(std::vector< BatchSeq > &seqs, const std::vector< std::vector< llama_token > > &toks, std::size_t shared)
Prefill each request's suffix; set per-seq logits_idx.
std::unique_ptr< Tokenizer > tokenizer_
Tokenizer used by tokenize_text / do_count_tokens / internal tokenize/detokenize.
GenerationResult do_generate_streaming(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel) override
Streaming generation with per-token callback.
bool has_vision_
Cached mtmd_support_vision(mtmd_ctx_) result.
void inject_tokenizer_for_test(std::unique_ptr< Tokenizer > tokenizer)
Inject a tokenizer for unit testing (v2.3.10).
bool decode_tokens_from(const std::vector< llama_token > &tokens, int start_offset)
Decode tokens starting at a given offset.
bool prefill_dispatch(const std::vector< llama_token > &tokens, const std::string &system_prompt, const std::vector< Message > &messages, const GenerationParams &params)
Cache-aware prefill dispatch (gh#96 v2.7.5: extracted body of run_prefill_cached so the wrapper owns ...
void release_temp_seq_id(llama_seq_id seq_id)
Release a temporary sequence ID back to the pool.
std::unique_ptr< Sampler > create_sampler(const GenerationParams &params) const
Build a Sampler for one generation from params.
int do_count_tokens(const std::string &text) const override
Count tokens in text.
::mtmd_context * mtmd_ctx_
libmtmd context, or nullptr if no mmproj loaded.
std::string last_parser_
Captured serialized PEG arena.
GenerationResult generate_multimodal(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > *cancel)
Multimodal generation core (v1.9.11 Phases 5–7).
std::mutex seq_id_mutex_
Guards temp seq_id pool (v1.9.10)
bool do_clear_state(int seq_id) override
Clear KV cache or recurrent hidden state.
bool prefill_shared_and_fanout(std::vector< BatchSeq > &seqs, const std::vector< llama_token > &seq0, std::size_t shared)
Prefill shared prefix into seq 0 + seq_cp fan-out.
std::string apply_chat_template_lowlevel(const std::vector< Message > &messages) const
Low-level GGUF template path (gh#86 fallback, v2.6.1).
static float extract_token_logprob(const float *logits, int32_t next_token, int n_vocab)
Extract log-probability for a token from logits.
void do_deactivate() override
Deactivate: free context, reload model CPU-only.
GenerationResult generate_mtp(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel, const std::string &head_path, int n_max)
Speculative generation via a target-owned MTP head (gh#106).
BackendInfo do_info() const override
Populate backend metadata from llama.cpp model.
std::vector< GenerationResult > do_generate_batch(const std::vector< std::vector< Message > > &requests, const std::vector< GenerationParams > &params, std::atomic< bool > &cancel) override
Same-prefix batch generation (gh#98, v2.8.0).
std::string parse_parser_
Last TOOLED render's PEG arena.
bool do_activate() override
Activate model on GPU (WARM → ACTIVE).
bool prepare_batch_seqs(std::vector< BatchSeq > &seqs, const std::vector< GenerationParams > &params)
Build per-request sampler chains + seq ids.
bool build_mtp_head(const std::string &head_path)
Load the MTP head GGUF + create its shared-KV context (gh#106).
bool prefill_and_cache_prefix(const std::vector< llama_token > &tokens, int prefix_tokens, const CacheKey &key)
Two-pass prefill: prefix-only prefill → save → rest.
std::mutex mtp_mutex_
gh#108: serialises MTP head setup/teardown vs in-flight generate_mtp (no deactivate-during-generate U...
llama_seq_id allocate_temp_seq_id()
Allocate a temporary sequence ID for evaluation.
PromptCacheConfig prompt_cache_config_
Cache config (v1.8.3)
int parse_chat_format_
Last TOOLED render's format.
std::vector< llama_token > resident_tokens_
gh#96: tokens resident in KV seq 0 (warm-keep)
llama_model * mtp_draft_model_
MTP head GGUF (separate, trunk-sharing)
void do_unload() override
Full unload — free all resources, clear prompt cache.
llama_model * model_
Loaded model (WARM+)
~LlamaCppBackend() override
Free llama.cpp + mtmd resources on destruction.
void invalidate_resident_kv()
gh#96 (v2.7.5): drop the warm-keep resident-KV record.
std::vector< llama_seq_id > free_seq_ids_
Available temporary seq_ids (v1.9.10)
GenerationResult do_generate_speculative(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel) override
Speculative streaming via the abstract InferenceBackend interface (kept as NOT_SUPPORTED — see kernel...
GenerationResult do_generate_streaming_text_only(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel)
Text-only streaming generation (extracted from streaming).
static std::string extract_system_prompt(const std::vector< Message > &messages)
Extract the system prompt from messages.
llama_context * mtp_draft_ctx_
MTP context (ctx_type=MTP, ctx_other=ctx_)
void inject_sampler_factory_for_test(std::unique_ptr< SamplerFactory > factory)
Inject a SamplerFactory for unit testing (v2.3.10).
bool setup_mtp_draft(const std::string &head_path, int n_max)
Lazily build the MTP head context against the live ctx_ (gh#106).
Sampler adapter that wraps a llama_sampler* chain.
llama_sampler * native_chain() const
Expose the underlying chain for legacy call sites that have not yet been ported to the Sampler API.
static CacheKey make_key(std::string_view prompt_text, std::string_view model_path)
Compute a cache key from prompt text and model path.
Pure-virtual per-generation sampler used by the decode loop.
Definition sampler.h:48
virtual int32_t sample()=0
Sample one token from the current decode position.
entropic_error_t
Error codes returned by all C API functions.
Definition error.h:35
@ ENTROPIC_OK
Success.
Definition error.h:36
@ ENTROPIC_ERROR_CANCELLED
Operation cancelled via cancel token.
Definition error.h:48
@ ENTROPIC_ERROR_IMAGE_LOAD_FAILED
Image file could not be read or decoded (v1.9.11)
Definition error.h:80
@ ENTROPIC_ERROR_SPECULATIVE_INCOMPATIBLE_CONFIG
MTP/speculative enabled but the request can't run correctly (temp>0, grammar, tools,...
Definition error.h:90
@ ENTROPIC_ERROR_NOT_SUPPORTED
Capability not supported by this backend (v1.9.13)
Definition error.h:84
@ ENTROPIC_ERROR_GENERATE_FAILED
Generation failed (context overflow, model error)
Definition error.h:42
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
Definition error.h:39
@ ENTROPIC_ERROR_LOAD_FAILED
Model load failed (corrupt file, OOM, unsupported format)
Definition error.h:41
LlamaCppBackend — llama.cpp C API integration.
Concrete llama.cpp Sampler + SamplerFactory (v2.3.10 seam impl).
Concrete llama.cpp tokenizer (v2.3.10 seam impl).
spdlog initialization and logger access.
auto now()
Get current time for timing measurements.
Definition logging.h:193
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
double elapsed_ms(std::chrono::steady_clock::time_point start, std::chrono::steady_clock::time_point end)
Compute elapsed milliseconds between two time points.
Definition logging.h:203
Pure envelope check for the MTP speculative path (gh#108).
Activate model on GPU (WARM → ACTIVE).
@ IMAGE
Image content (local path or data URI)
BackendCapability
Capabilities that an inference backend may or may not support.
@ SPECULATIVE_DECODING
Speculative decoding compatibility.
@ HIDDEN_STATE
Recurrent hidden state management (save/load/reset)
@ VISION
Vision / multimodal input (v1.9.11)
@ KV_CACHE
KV cache state management (save/load/clear)
@ AUDIO
Audio input via mtmd audio projector (gh#53, v2.3.0)
@ _COUNT
Sentinel — must be last. Used for iteration/array sizing.
void append_sequential_stop(GenerationParams &params, const std::string &marker)
Append a tool-call close marker to params.stop for sequential mode.
void coerce_string_typed_args(std::vector< ToolCall > &calls, const std::string &tools_json)
gh#90: coerce numeric scalars back to strings for string-typed tool parameters.
bool batch_is_viable(std::size_t n, int n_parallel, std::size_t shared, bool hybrid, std::size_t total_suffix, int n_batch)
Decide whether the same-prefix batch fast-path is safe + worthwhile.
Definition batch_util.h:80
bool has_images(const std::vector< ContentPart > &parts)
Check if content parts contain any image parts.
Definition content.cpp:41
static bool spec_decode_both(SpeculativeRunState &state)
Decode the speculative batch on both contexts.
@ tokens
Gate on generated tokens since the last tool call.
@ off
Disabled (default) — no thinking-budget gating.
std::string extract_text(const std::vector< ContentPart > &parts)
Extract concatenated text from content parts.
Definition content.cpp:20
std::string close_marker_for_format(common_chat_format fmt)
Map a resolved common_chat format to its single-tool-call close marker.
static void spec_ckpt_save_tgt(SpeculativeRunState &state)
Snapshot target state right before the target decode of the speculative batch (when use_ckpt_tgt + no...
@ ok
Tool dispatched, returned non-empty content.
std::size_t batch_shared_prefix_len(const std::vector< std::vector< Tok > > &seqs)
Longest shared token prefix across N request sequences (gh#98).
Definition batch_util.h:43
static void spec_trim_rejected_drafts(SpeculativeRunState &state)
Clear any stale KV positions left by rejected draft tokens.
static bool spec_commit_accepted(SpeculativeRunState &state, const std::vector< llama_token > &ids, const llama_vocab *vocab, int max_tokens, std::function< void(std::string_view)> &on_token, std::atomic< bool > &cancel)
Walk accepted ids, emit tokens via callback, update state.
static std::vector< common_chat_tool > mcp_tools_to_common_chat(const std::string &tools_json)
Convert entropic MCP tool JSON to common_chat_tool defs (gh#87).
static std::string spec_emit_token(SpeculativeRunState &state, llama_token id, const llama_vocab *vocab, int max_tokens, std::function< void(std::string_view)> &on_token, std::atomic< bool > &cancel)
Emit on_token for one accepted id, updating state and returning a stop signal when terminating condit...
std::size_t warm_keep_cut(const std::vector< Tok > &resident, const std::vector< Tok > &incoming, long kv_pos_max)
Decide how many resident-KV tokens warm-keep may reuse this turn.
static void spec_rollback_partial(SpeculativeRunState &state, common_sampler *smpl_save, std::vector< llama_token > &ids)
Partial-acceptance rollback: restore both contexts and the sampler to their pre-draft state,...
static void fill_batch_cell(llama_batch &b, int k, llama_token tok, llama_pos pos, llama_seq_id seq, bool want_logits)
Fill one cell of a multi-seq llama_batch.
static std::string spec_check_preconditions(bool target_active, bool draft_active, llama_context *ctx_tgt, llama_context *ctx_dft)
Validate speculative preconditions and reject NO-seq_rm.
@ WARM
mmap'd + mlock'd in RAM
@ COLD
On disk only, no RAM consumed.
void strip_thinking_channels(std::string &content, std::string *reasoning_out)
Strip Gemma 4 QAT reasoning channels (<|channel>…<channel|>) from content, accumulating the stripped ...
static std::optional< common_chat_params > render_common_chat(llama_model *model, const std::vector< Message > &messages, const GenerationParams &params, const std::vector< common_chat_tool > &tools)
Shared common_chat render core for both template paths (gh#87).
static std::string concat_messages_fallback(const std::vector< Message > &messages)
Plain "role: content" join used when templating fails.
static int spec_run_draft(SpeculativeRunState &state)
Trigger draft generation via common_speculative_draft.
static void spec_run_loop(SpeculativeRunState &state, const llama_vocab *vocab, int max_tokens, std::function< void(std::string_view)> &on_token, std::atomic< bool > &cancel)
Run the accept-round loop until completion / EOS / cancel.
std::string mtp_unsupported_reason(float temperature, bool has_grammar, bool streaming)
Reason MTP cannot run for a request, or "" when the envelope is safe.
static std::vector< common_chat_msg > to_common_chat(const std::vector< Message > &messages)
Convert engine messages to common_chat_msg (gh#86, v2.6.1).
static ToolCall to_entropic_tool_call(const common_chat_tool_call &cc)
Map a common_chat_tool_call to entropic's ToolCall (gh#87).
static std::vector< llama_chat_message > to_llama_chat(const std::vector< Message > &messages)
Convert engine messages to llama_chat_message views.
static GenerationResult spec_finalize(SpeculativeRunState &state, std::chrono::steady_clock::time_point t0)
Speculative kernel against an explicit draft backend.
static bool spec_accept_round(SpeculativeRunState &state, const llama_vocab *vocab, int max_tokens, std::function< void(std::string_view)> &on_token, std::atomic< bool > &cancel)
Run one speculative accept round; return false to stop.
static GenerationResult spec_run_from_tokens(llama_context *ctx_tgt, llama_context *ctx_dft, llama_model *model_tgt, const std::vector< llama_token > &tokens, const GenerationParams &params, std::function< void(std::string_view)> &on_token, std::atomic< bool > &cancel, int n_draft_max, const std::string &draft_path, std::chrono::steady_clock::time_point t0)
Public entry point for the speculative-decoding kernel.
static GenerationResult batch_error_result(const std::string &msg)
Build a single error GenerationResult (gh#98 batch failures).
static void spec_build_batch(SpeculativeRunState &state)
Build the target batch [id_last, draft0, ..., draftN-1].
static int spec_prepare_draft(SpeculativeRunState &state)
Drive one accept round: optional draft generation, decode on both contexts, sample-and-accept,...
static void spec_ckpt_save_dft(SpeculativeRunState &state)
Drive one accept round: draft → decode → sample-and-accept → emit tokens.
static void spec_cleanup(SpeculativeRunState &state)
Free everything allocated by the kernel.
static std::string spec_init_sampler_and_decoder(SpeculativeRunState &state, llama_model *model_tgt, const GenerationParams &params, int n_draft_max, const std::string &draft_path)
Initialize the kernel state: clear KV, prefill, sampler, speculative context, batch,...
static void spec_ckpt_restore_dft(SpeculativeRunState &state)
Restore the draft's pre-draft state so the upcoming target-batch decode on the draft re-fills cleanly...
static std::string spec_init_run(SpeculativeRunState &state, llama_model *model_tgt, const std::vector< llama_token > &tokens, const GenerationParams &params, int n_draft_max, const std::string &draft_path)
Initialize speculative run state (prefill + sampler + decoder).
Backend metadata for introspection.
size_t ram_bytes
RAM consumed by loaded model (bytes). 0 if COLD.
int max_context_length
Maximum context length.
size_t parameter_count
Number of parameters (from model metadata).
std::string architecture
Architecture family of the loaded model.
std::string compute_device
"cuda", "vulkan", "cpu", "npu"
std::string name
Backend identifier (e.g. "llama.cpp", "axcl")
std::string quantization
Quantization type (e.g. "IQ3_XXS", "Q8_0", "fp16").
size_t vram_bytes
VRAM consumed by loaded model (bytes). 0 if COLD.
std::string model_format
"gguf", "axmodel", "onnx", etc.
Single cached KV state snapshot.
std::vector< uint8_t > data
Raw KV cache bytes.
size_t data_size
data.size() for quick byte accounting
int token_count
Prompt tokens covered by this entry.
64-bit hash used as cache lookup key.
Generation parameters for a single inference call.
Definition config.h:302
std::string grammar
GBNF grammar string (empty = unconstrained)
Definition config.h:359
int top_k
Top-K sampling.
Definition config.h:305
float repeat_penalty
Repetition penalty.
Definition config.h:306
float temperature
Sampling temperature.
Definition config.h:303
float frequency_penalty
Frequency-penalty term in llama.cpp's penalties sampler (gh#23 MVP item 3).
Definition config.h:349
float presence_penalty
Presence-penalty term in llama.cpp's penalties sampler (gh#23 MVP item 2).
Definition config.h:322
bool enable_thinking
Enable <think> blocks (false if reasoning_budget == 0)
Definition config.h:358
float min_p
Min-p nucleus sampling threshold (gh#23 MVP item 1).
Definition config.h:315
int max_tokens
Maximum tokens to generate.
Definition config.h:351
float top_p
Nucleus sampling threshold.
Definition config.h:304
int seed
RNG seed for reproducible sampling.
Definition config.h:356
std::vector< std::string > stop
Stop sequences.
Definition config.h:365
Result of a single generation call.
entropic_error_t error_code
Error code (ENTROPIC_OK if no error)
double generation_time_ms
Wall-clock generation time.
int n_drafted
Tokens proposed by the draft/MTP head across all rounds.
int seq_id
Sequence identifier for multi-sequence backends.
double throughput_tok_s
Measured throughput for this generation (tok/s).
std::string finish_reason
Finish reason: "stop", "length", "error".
std::string content
Generated text (cleaned by adapter)
int n_accepted
Draft tokens the target accepted (≤ n_drafted).
std::string error_message
Error description (empty if no error)
int token_count
Generated token count.
Result of a common_chat parse: native tool calls + split content.
std::vector< ToolCall > tool_calls
Extracted native tool calls.
std::string content
Content with calls + reasoning removed.
std::string reasoning_content
Extracted reasoning/thought block.
Per-token log-probability evaluation result.
std::vector< float > logprobs
Log-prob for each token transition (N-1 values)
int n_logprobs
Number of logprob values (n_tokens - 1)
int n_tokens
Number of input tokens.
float total_logprob
Sum of all logprob values.
float perplexity
exp(-mean(logprobs)) — lower = less surprising
std::vector< int32_t > tokens
Input tokens echoed back for verification.
Model configuration for a single tier.
Definition config.h:148
std::filesystem::path mmproj_path
Vision projector GGUF path.
Definition config.h:244
int gpu_layers
GPU offload layers (-1 = all)
Definition config.h:152
int n_ubatch
Physical micro-batch size for prompt processing (gh#23 MVP item 5).
Definition config.h:172
int context_length
Context window size (512–131072)
Definition config.h:151
std::filesystem::path path
Resolved model file path.
Definition config.h:149
float rope_freq_scale
RoPE frequency scaling factor (gh#23 MVP item 10).
Definition config.h:222
int main_gpu
Primary GPU index for model load (gh#23 MVP item 7).
Definition config.h:194
int n_threads
CPU threads (0 = auto-detect)
Definition config.h:174
bool offload_kqv
Offload KQV ops (incl.
Definition config.h:202
int n_parallel
Max parallel sequences per context (gh#23 MVP item 11).
Definition config.h:232
std::string cache_type_k
KV cache key quantization type.
Definition config.h:158
std::string cache_type_v
KV cache value quantization type.
Definition config.h:159
std::string split_mode
Multi-GPU split mode for model load (gh#23 MVP item 6).
Definition config.h:186
int n_batch
Batch size for prompt processing.
Definition config.h:160
bool flash_attn
Enable flash attention.
Definition config.h:233
bool use_mlock
Lock model in system RAM.
Definition config.h:154
float rope_freq_base
RoPE base frequency override (gh#23 MVP item 9).
Definition config.h:212
size_t max_bytes
Maximum cache RAM (512 MB default)
Definition config.h:267
bool log_hits
Log cache hit/miss at INFO level.
Definition config.h:269
bool enabled
Master switch (false = no caching)
Definition config.h:268
bool warm_keep
gh#96 (v2.7.5): keep the prior turn's KV resident and re-decode only the appended delta (warm-keep / ...
Definition config.h:274
Bundles per-kernel-run mutable state to keep the loop body focused on its responsibility (knots: cogn...
std::vector< std::string > stop
gh#108: stop seqs (effective_stop); empty for gh#36
A tool call request parsed from model output.
Definition tool_call.h:31
std::unordered_map< std::string, std::string > arguments
Tool arguments as string key-value pairs.
Definition tool_call.h:34
std::string id
Unique call ID (UUID)
Definition tool_call.h:32
std::string arguments_json
Original JSON string (for passthrough dispatch)
Definition tool_call.h:35
std::string name
Tool name (e.g. "filesystem.read_file")
Definition tool_call.h:33
gh#103 (v2.8.2): family-aware tool-call CLOSE markers, derived from the resolved common_chat format.
gh#96 (v2.7.5) warm-keep / incremental-prefill decision logic.