Entropic 2.14.3
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"
18#include "generation_summary.h" // gh#151: one summary line, both paths
19#include "grammar_source.h"
20#include "llama_cpp_sampler.h"
21#include "llama_cpp_tokenizer.h"
22#include "session_pool_util.h"
23#include "batch_kv_util.h" // gh#158: batch sequence plan
24#include "context_fit.h" // v2.13.0: prompt-vs-context admission gate
25#include "expert_offload_buft.h" // gh#153 #42(iii): expert-tensor offload
26#include "warm_keep_util.h" // gh#96: common_prefix_len / warm_keep_cut
27#include "tool_call_markers.h" // gh#103: family-aware tool-call close marker
28#include "batch_util.h" // gh#98: batch_shared_prefix_len / batch_is_viable
29#include "mtp_envelope.h" // gh#108: mtp_unsupported_reason (fail-loud envelope)
30
31#include <entropic/inference/adapters/adapter_base.h> // gh#90 coerce_string_typed_args
33
34#include <common.h>
35#include <chat.h>
36#include <sampling.h>
37#include <speculative.h>
38#include <mtmd.h>
39#include <mtmd-helper.h>
40
41#include <nlohmann/json.hpp>
42
43#include <cmath>
44#include <cstdlib> // std::atoi — GGUF expert_count arrives as a string
45#include <cstring>
46#include <optional>
47#include <stdexcept>
48
49namespace entropic {
50
66static void fill_batch_cell(llama_batch& b, int k, llama_token tok,
68 bool want_logits);
69
70namespace {
71
72auto logger = entropic::log::get("inference.llama_cpp");
73
82bool ends_with(const std::string& text, const std::string& suffix) {
83 return text.size() >= suffix.size()
84 && text.compare(text.size() - suffix.size(), suffix.size(), suffix) == 0;
85}
86
96 const std::string& text,
97 const std::vector<std::string>& stop_sequences)
98{
99 for (const auto& stop : stop_sequences) {
100 if (!stop.empty() && ends_with(text, stop)) {
101 return true;
102 }
103 }
104 return false;
105}
106
113void log_sampler_config(const GenerationParams& params) {
114 logger->info("Sampler: temp={:.2f}, top_k={}, top_p={:.2f}, "
115 "repeat_penalty={:.2f}, thinking={}",
116 params.temperature, params.top_k, params.top_p,
117 params.repeat_penalty, params.enable_thinking);
118}
119
127void finalize_result(GenerationResult& result,
128 std::chrono::steady_clock::time_point start_time)
129{
130 auto end = entropic::log::now();
131 result.generation_time_ms = entropic::log::elapsed_ms(
132 start_time, end);
133 if (result.token_count > 0 && result.generation_time_ms > 0.0) {
134 result.throughput_tok_s =
135 static_cast<double>(result.token_count)
136 / result.generation_time_ms * 1000.0;
137 }
138 // gh#151: through the shared formatter, so this line and the
139 // speculative path's cannot drift apart — the drift IS the defect.
140 logger->info("{}", entropic::format_generation_summary(
141 {result.token_count, result.finish_reason,
142 result.generation_time_ms, result.throughput_tok_s, 0, 0}));
143 logger->info("Content:\n{}", result.content);
144}
145
166void finalize_generation(GenerationResult& result,
167 const std::string& generated, int n_generated,
168 const GenerationParams& params,
169 std::chrono::steady_clock::time_point t0,
170 int prefill_tokens)
171{
172 // gh#194 (v2.13.2): publish the prefill, which was computed and dropped.
173 //
174 // `last_prefill_tokens_` is set by run_prefill_cached — the path every
175 // decode takes — and was only ever LOGGED. The published field was
176 // assigned in exactly one place, spec_finalize, from a counter local to
177 // the speculative loop that counts only the chunks that loop decodes
178 // itself; the shared prefill has normally already done the prompt, so it
179 // read 0 even there.
180 //
181 // It goes HERE rather than in a dispatcher because there are four decode
182 // workers (plain, cancellable, streaming, streaming-cancellable) and they
183 // all end at this call. Patching dispatchers missed the streaming path
184 // twice while writing this fix — the overload pair is exactly the seam a
185 // per-path fix falls through.
186 result.prefill_tokens = prefill_tokens;
187 if (n_generated >= params.max_tokens
188 && result.finish_reason.empty()) {
189 result.finish_reason = "length";
190 }
191 // gh#136 (4th type_error.316 recurrence): sanitize at INGRESS, where model
192 // bytes first become a std::string, rather than at each of the ~18 .dump()
193 // sites downstream. gh#112/113 were closed as "permanent closure of the
194 // 316 family", then gh#114, gh#118, gh#132 and gh#136 each patched one more
195 // egress. The exits keep multiplying; the entries do not. Guarding here
196 // makes every downstream dump safe by construction, and a NEW .dump()
197 // anywhere cannot reintroduce the bug.
198 //
199 // Once at finalization, never per-token: a multi-byte codepoint can split
200 // across token boundaries and per-token sanitize would corrupt valid
201 // output (see the same reasoning at response_generator.cpp:360, which has
202 // guarded the streaming accumulator this way since v2.1.1).
203 //
204 // Safe for raw_content consumers: raw_content is a COPY of content
205 // (orchestrator.cpp:514), and sanitize only replaces bytes that are
206 // ALREADY invalid UTF-8 — which cannot form part of any valid JSON token,
207 // so the gh#88 envelope recovery and fenced-JSON fallbacks are unaffected.
208 result.content = entropic::mcp::sanitize_utf8(generated);
209 result.token_count = n_generated;
210 finalize_result(result, t0);
211}
212
225GenerationResult sampler_init_error(
226 std::chrono::steady_clock::time_point t0)
227{
228 GenerationResult r;
230 r.error_message = "Sampler factory not initialized";
231 r.finish_reason = "error";
233 return r;
234}
235
245ggml_type parse_kv_cache_type(const std::string& s) {
246 static const std::pair<const char*, ggml_type> kTable[] = {
247 {"f16", GGML_TYPE_F16},
248 {"f32", GGML_TYPE_F32},
249 {"bf16", GGML_TYPE_BF16},
250 {"q8_0", GGML_TYPE_Q8_0},
251 {"q4_0", GGML_TYPE_Q4_0},
252 };
253 for (const auto& [name, type] : kTable) {
254 if (s == name) { return type; }
255 }
256 logger->warn("Unknown cache_type '{}' — defaulting to f16", s);
257 return GGML_TYPE_F16;
258}
259
269llama_split_mode parse_split_mode(const std::string& s) {
270 if (s.empty()) { return LLAMA_SPLIT_MODE_LAYER; }
271 static const std::pair<const char*, llama_split_mode> kTable[] = {
272 {"none", LLAMA_SPLIT_MODE_NONE},
273 {"layer", LLAMA_SPLIT_MODE_LAYER},
274 {"row", LLAMA_SPLIT_MODE_ROW},
275 };
276 for (const auto& [name, mode] : kTable) {
277 if (s == name) { return mode; }
278 }
279 logger->warn("Unknown split_mode '{}' — defaulting to layer", s);
281}
282
305llama_load_mode mmap_load_mode(bool use_mlock) {
307}
308
313std::atomic<std::uint64_t> g_model_file_loads{0};
314
328llama_model* load_tier_model(const char* path,
330 g_model_file_loads.fetch_add(1, std::memory_order_relaxed);
332}
333
356 m.n_gpu_layers = cfg.gpu_layers;
357 m.load_mode = mmap_load_mode(cfg.use_mlock);
358 m.split_mode = parse_split_mode(cfg.split_mode);
359 // gh#23 MVP item 7 (v2.3.19): main_gpu. Effective when split_mode
360 // is "none" (pin) or "row" (small-tensor placement). 0 keeps
361 // pre-v2.3.19 load bit-for-bit.
362 m.main_gpu = cfg.main_gpu;
363 // gh#153 #42(iii) (v2.13.0, EXPERIMENTAL): routed-expert tensors to the
364 // host so attention and its KV keep the VRAM. nullptr when off.
365 m.tensor_buft_overrides = moe.data();
366 return m;
367}
368
383int model_expert_count(const llama_model* model) {
384 char arch[64] = {0};
385 if (llama_model_meta_val_str(model, "general.architecture",
386 arch, sizeof(arch)) < 0) {
387 return 0;
388 }
389 const std::string key = std::string(arch) + ".expert_count";
390 char value[32] = {0};
391 if (llama_model_meta_val_str(model, key.c_str(),
392 value, sizeof(value)) < 0) {
393 return 0;
394 }
395 return std::atoi(value);
396}
397
398} // anonymous namespace
399
400// ── Lifecycle ──────────────────────────────────────────────
401
415 mparams.n_gpu_layers = 0;
417
419 if (!model_) {
420 last_error_ = "llama_model_load_from_file failed: " + config.path.string();
421 return false;
422 }
423
425 logger->info("Model loaded (CPU): {} tokens in vocab, recurrent={}",
427 return true;
428}
429
447 is_hybrid_ = llama_model_is_hybrid(model_); // gh#97: attn + recurrent/SSM
448 tokenizer_ = std::make_unique<LlamaCppTokenizer>(vocab_);
449}
450
461namespace {
477 // gh#144 (v2.12.0): with max_sessions > 1 the geometry is DERIVED, not
478 // set knob-by-knob — see session_pool_util.h for why non-unified and why
479 // n_ctx multiplies (context_length is PER SESSION). max_sessions == 1
480 // reproduces the previous values exactly, so an existing deployment is
481 // bit-identical.
482 const auto pool = derive_pool_geometry(cfg);
483 c.n_ctx = static_cast<uint32_t>(pool.n_ctx);
484 c.n_batch = static_cast<uint32_t>(cfg.n_batch);
485 // gh#23 MVP item 5 (v2.3.17): n_ubatch. 0 keeps llama.cpp's default
486 // (== n_batch in practice), preserving pre-v2.3.17 chunking.
487 if (cfg.n_ubatch > 0) {
488 c.n_ubatch = static_cast<uint32_t>(cfg.n_ubatch);
489 }
490 c.n_threads = cfg.n_threads > 0
491 ? static_cast<uint32_t>(cfg.n_threads)
492 : std::thread::hardware_concurrency();
493 c.flash_attn_type = cfg.flash_attn
496 c.type_k = parse_kv_cache_type(cfg.cache_type_k);
497 c.type_v = parse_kv_cache_type(cfg.cache_type_v);
498 // gh#23 MVP item 8 (v2.3.20): offload_kqv. true (default) matches
499 // llama.cpp's default — bit-identical for callers not opting out.
500 c.offload_kqv = cfg.offload_kqv;
501 // gh#23 MVP items 9 + 10 (v2.3.21 + v2.3.22): RoPE frequency
502 // overrides. Both 0.0 = use model's trained value — bit-identical.
503 c.rope_freq_base = cfg.rope_freq_base;
504 c.rope_freq_scale = cfg.rope_freq_scale;
505 // gh#23 MVP item 11 (v2.3.23): n_parallel maps to cparams.n_seq_max.
506 // 1 (default) matches llama.cpp's default — bit-identical.
507 c.n_seq_max = static_cast<uint32_t>(pool.n_seq_max);
508 // gh#98 (v2.8.0): a unified KV buffer is REQUIRED for llama_memory_seq_cp
509 // (the same-prefix batch fan-out) — seq_cp asserts on per-sequence buffers.
510 // llama.cpp also recommends kv_unified exactly when sequences share a large
511 // prefix (our case). Only enabled when batching is configured (n_parallel>1)
512 // so single-sequence handles keep llama.cpp's default.
513 c.kv_unified = pool.kv_unified;
514 // gh#108 (v2.9.2): llama_context_default_params() returns swa_full=true (a
515 // full-context SWA cache), but the CLI default is false. For Gemma-4 (mostly
516 // sliding-window: window=512, 5:1 SWA:global) the un-windowed cache wastes
517 // ~5 GB at 128k. Set false — the memory-efficient windowed mode. Validated
518 // against warm-keep / prompt-cache reuse over a >window prefix (the SWA layers
519 // keep only the last `window` tokens, so KV reuse must not assume full-context
520 // SWA residency — covered by the long-context warm-keep model test).
521 c.swa_full = false;
522 return c;
523}
524} // anonymous namespace
525
539 if (!load_gpu_model()) { return false; }
540 return finish_activation();
541}
542
554 if (!create_inference_context()) { return false; }
555 // v2.3.10: wire the Sampler seam once ctx_ / vocab_ are live.
556 // Lifetime: factory borrows ctx_ + vocab_; do_deactivate /
557 // do_unload reset sampler_factory_ BEFORE freeing those handles
558 // so the borrow never dangles.
559 sampler_factory_ = std::make_unique<LlamaCppSamplerFactory>(
560 ctx_, vocab_);
562 return true;
563}
564
589 // Outlives the load below — llama.cpp borrows the pattern strings.
593
595 if (model_ == nullptr) {
596 // llama.cpp returns null with no error string — the reason (OOM,
597 // CUDA init failure, GGUF parse error) is only in ggml's log stream.
598 last_error_ = "Failed to load model into target residency "
599 "(path=" + config.path.string()
600 + ", gpu_layers=" + std::to_string(config.gpu_layers)
601 + ") — check llama_ggml.log in the engine's log_dir "
602 "for the underlying llama.cpp/CUDA error";
603 return false;
604 }
605
607 if (!expert_offload_admits(config)) { return false; }
608 logger->info("Model loaded into target residency: gpu_layers={}, "
609 "{} tokens in vocab, recurrent={}",
612 return finish_activation();
613}
614
638 const std::string why = expert_offload_load_refusal(
640 if (why.empty()) {
641 if (config.cpu_moe_layers > 0) {
642 // The engagement signal. A knob that silently places nothing
643 // measures as "the technique does not help", so say what was
644 // asked for and point at the numbers that prove it landed:
645 // llama.cpp's own `load_tensors:` buffer-size lines shift from
646 // the device buffer to CPU_Mapped, and at DEBUG it names every
647 // tensor it overrode.
648 logger->info("[expert-offload] EXPERIMENTAL: experts of the "
649 "first {} of {} layers -> host; attention, KV, "
650 "router, dense/shared FFN and norms follow "
651 "gpu_layers={}. Confirm placement in "
652 "llama_ggml.log ('load_tensors:' buffer sizes).",
654 }
655 return true;
656 }
657
658 last_error_ = "cpu_moe_layers refused: " + why;
659 logger->error("{}", last_error_);
660 tokenizer_.reset();
662 model_ = nullptr;
663 vocab_ = nullptr;
664 return false;
665}
666
689 // Outlives the load below — llama.cpp borrows the pattern strings.
690 const ExpertOffloadOverrides moe(config().cpu_moe_layers,
693
694 if (!config().tensor_split.empty()) {
695 // TODO: parse tensor_split string into float array for multi-GPU
696 logger->warn("tensor_split not yet implemented, ignoring");
697 }
698
699 // tokenizer_ borrows the old vocab_; reset it before the free so the
700 // borrow never dangles. Then free the WARM model and null the
701 // handles so a failed reload below leaves the backend in a clean,
702 // recoverable state rather than a dangling one.
703 tokenizer_.reset();
704 if (model_ != nullptr) {
706 model_ = nullptr;
707 vocab_ = nullptr;
708 }
709
710 model_ = load_tier_model(config().path.c_str(), mparams);
711 if (model_ == nullptr) {
712 // llama.cpp returns null with no error string — the actual
713 // reason (OOM, CUDA init failure, GGUF parse error, etc.) only
714 // surfaces in ggml's log stream. Point the operator at it so
715 // multi-handle GPU failures (gh#58 v2.2.7 follow-up) are
716 // diagnosable without source-diving llama.cpp.
717 last_error_ = "Failed to reload model with GPU layers "
718 "(path=" + config().path.string()
719 + ", gpu_layers=" + std::to_string(config().gpu_layers)
720 + ") — check llama_ggml.log in the engine's log_dir "
721 "for the underlying llama.cpp/CUDA error";
722 return false;
723 }
724
727}
728
737
739 if (!ctx_) {
740 last_error_ = "llama_init_from_model failed";
741 return false;
742 }
743
744 logger->info("Context created: n_ctx={}, n_batch={}, "
745 "flash_attn={}, type_k={}, type_v={}",
746 config().context_length, config().n_batch,
747 config().flash_attn,
748 config().cache_type_k, config().cache_type_v);
749
750 // Initialize prompt cache if not already created
751 if (!prompt_cache_) {
752 prompt_cache_ = std::make_unique<PromptCache>(
754 logger->info("Prompt cache initialized: max_bytes={}",
756 }
757 return true;
758}
759
773 if (config().mmproj_path.empty()) {
774 has_vision_ = false;
775 return;
776 }
778 ctx_params.use_gpu = (config().gpu_layers != 0);
779 ctx_params.flash_attn_type = config().flash_attn
782 ctx_params.print_timings = false;
784 config().mmproj_path.c_str(), model_, ctx_params);
785 if (mtmd_ctx_ == nullptr) {
786 logger->error("mtmd_init_from_file failed for {} — "
787 "continuing in text-only mode",
788 config().mmproj_path.string());
789 has_vision_ = false;
790 return;
791 }
793 logger->info("mmproj loaded from {} — vision={}",
794 config().mmproj_path.string(), has_vision_);
795}
796
813 if (mtp_draft_ctx_ != nullptr) {
815 mtp_draft_ctx_ = nullptr;
816 }
817 if (mtp_draft_model_ != nullptr) {
819 mtp_draft_model_ = nullptr;
820 }
821 mtp_head_path_.clear();
822}
823
837static int effective_n_draft(int n_max) {
838 return (n_max > 0) ? n_max : 16;
839}
840
854bool LlamaCppBackend::setup_mtp_draft(const std::string& head_path, int n_max) {
856 if (mtp_draft_ctx_ != nullptr && mtp_head_path_ == head_path) {
857 return true; // live head already bound to this ctx_
858 }
861}
862
869 if (ctx_ == nullptr) {
870 last_error_ = "MTP setup requires an ACTIVE target context";
871 return false;
872 }
873 if (head_path.empty()) {
874 // gh#108: fail loud before llama_model_load_from_file("") — a bare
875 // mtp=true with no draft.path is a config error, not a load to attempt.
876 last_error_ = "MTP requires speculative.draft.path (the head GGUF); "
877 "none configured";
878 return false;
879 }
881 mparams.n_gpu_layers = config().gpu_layers; // head is tiny — follow target
882 mparams.load_mode = LLAMA_LOAD_MODE_MMAP; // mmap, never mlock (was use_mmap=true)
884 if (mtp_draft_model_ != nullptr) {
887 cparams.ctx_other = ctx_; // share the target's KV memory
888 cparams.n_rs_seq = 0;
890 }
891 bool ok = (mtp_draft_ctx_ != nullptr);
892 if (ok) {
894 logger->info("MTP head ready: {} (n_max={}, ctx_other=target, "
895 "shared-KV)", head_path, mtp_n_max_);
896 } else {
897 last_error_ = "MTP head setup failed: " + head_path;
899 }
900 return ok;
901}
902
919 // gh#108 (v2.9.1): serialise vs an in-flight generate_mtp — it holds
920 // mtp_mutex_ across its decode, so this blocks until that decode finishes
921 // before freeing the MTP head + ctx_ (no deactivate-during-generate UAF).
922 std::lock_guard<std::mutex> lk(mtp_mutex_);
923 // gh#106 (v2.9.0): the MTP head borrows ctx_ via ctx_other — free it
924 // FIRST so the borrow never dangles past the context.
926 // v2.3.10: sampler factory borrows ctx_ + vocab_. Release it
927 // BEFORE freeing the context so the borrow never dangles.
928 sampler_factory_.reset();
929 // v2.1.8: mtmd holds a reference to the live llama_model — free
930 // it before the GPU model is unloaded below.
931 if (mtmd_ctx_ != nullptr) {
933 mtmd_ctx_ = nullptr;
934 has_vision_ = false;
935 }
936 if (ctx_) {
938 ctx_ = nullptr;
939 }
940 invalidate_all_resident_kv(); // gh#96: KV is gone with the context
941
942 // Free the GPU model FIRST (releasing VRAM — the point of
943 // deactivate), then reload CPU-only for the WARM state. tokenizer_
944 // borrows the old vocab_, so reset it before the free.
945 tokenizer_.reset();
946 if (model_ != nullptr) {
948 model_ = nullptr;
949 vocab_ = nullptr;
950 }
952}
953
965 mparams.n_gpu_layers = 0;
966 mparams.load_mode = mmap_load_mode(config().use_mlock);
967
968 model_ = load_tier_model(config().path.c_str(), mparams);
969 if (model_ != nullptr) {
971 } else {
972 // VRAM is released, but the warm-reload failed: leave the handle
973 // null (state stays recoverable — the next activate reloads from
974 // scratch). Error, not warn: a same-file CPU reload failing here
975 // signals real trouble (disk/OOM).
976 logger->error("Failed to reload CPU model during deactivate "
977 "(path={}); backend left unloaded until next activate",
978 config().path.string());
979 }
980}
981
989 return g_model_file_loads.load(std::memory_order_relaxed);
990}
991
998 g_model_file_loads.store(0, std::memory_order_relaxed);
999}
1000
1015
1030 std::unique_ptr<Tokenizer> tokenizer)
1031{
1032 tokenizer_ = std::move(tokenizer);
1033 state_.store(ModelState::WARM, std::memory_order_release);
1034}
1035
1051 std::unique_ptr<SamplerFactory> factory)
1052{
1053 sampler_factory_ = std::move(factory);
1054}
1055
1069 // gh#108 (v2.9.1): serialise vs in-flight generate_mtp (see do_deactivate).
1070 std::lock_guard<std::mutex> lk(mtp_mutex_);
1071
1072 // gh#108 (v2.10.3): invalidate the sticky parse snapshot. parse_params_valid_
1073 // was set once by a tooled render (gh#105) and never cleared by anything —
1074 // not render_prompt (which clears only have_chat_params_), not unload. If a
1075 // backend instance were reused across a load of a different GGUF, the stale
1076 // PEG arena would survive AND parse_chat_format_ would still report the old
1077 // family, so common_chat_parse_reliable() would answer true and parse the
1078 // new model's output with the previous model's parser. Clearing here makes
1079 // that unreachable by construction rather than by luck of instance reuse.
1080 parse_params_valid_ = false;
1083 parse_parser_.clear();
1084 // gh#134 (v2.10.4): same reasoning — a reused backend must not apply the
1085 // previous model's tool-call grammar to a new model's decode.
1086 tool_grammar_.clear();
1087 tool_grammar_lazy_ = false;
1088 if (prompt_cache_) {
1089 prompt_cache_->clear();
1090 }
1091 // gh#106 (v2.9.0): MTP head borrows ctx_ — free it before the context.
1093 // v2.3.10: sampler factory borrows ctx_ + vocab_ — release it
1094 // BEFORE the context/model are freed below so the borrow never
1095 // points into freed memory. (do_deactivate normally releases
1096 // this earlier; this reset is the WARM→COLD safety net.)
1097 sampler_factory_.reset();
1098 // v2.3.10: tokenizer borrows vocab_ — release it BEFORE the model
1099 // is freed so the borrow never points into freed memory.
1100 tokenizer_.reset();
1101 // v2.1.8: mtmd must be freed before the underlying llama_model.
1102 if (mtmd_ctx_ != nullptr) {
1104 mtmd_ctx_ = nullptr;
1105 has_vision_ = false;
1106 }
1107 if (ctx_) {
1109 ctx_ = nullptr;
1110 }
1111 invalidate_all_resident_kv(); // gh#96: KV is gone with the context
1112 if (model_) {
1114 model_ = nullptr;
1115 }
1116 vocab_ = nullptr;
1117}
1118
1119// ── Tokenization ───────────────────────────────────────────
1120
1129std::vector<llama_token> LlamaCppBackend::tokenize(
1130 const std::string& text, bool add_special) const
1131{
1132 // v2.3.10: route through the Tokenizer seam. tokenizer_ is set
1133 // in do_load (real impl) or via inject_tokenizer_for_test (mock).
1134 // Returns empty when no tokenizer is wired — matches the prior
1135 // failure-path return shape.
1136 if (!tokenizer_) { return {}; }
1137 auto ids = tokenizer_->tokenize(text, add_special);
1138 // llama_token is int32_t; vector conversion is a copy through
1139 // iterators since the value type matches.
1140 return {ids.begin(), ids.end()};
1141}
1142
1151 // v2.3.10: route through Tokenizer seam. The special=false /
1152 // gh#68 history + defensive rationale now lives in
1153 // LlamaCppTokenizer::detokenize. Returns empty when no
1154 // tokenizer is wired — matches prior failure-path return.
1155 if (!tokenizer_) { return {}; }
1156 return tokenizer_->detokenize(static_cast<int32_t>(token));
1157}
1158
1166int LlamaCppBackend::do_count_tokens(const std::string& text) const {
1167 auto tokens = tokenize(text, false);
1168 return static_cast<int>(tokens.size());
1169}
1170
1179 const std::string& text) const {
1180 auto tokens = tokenize(text, true);
1181 return {tokens.begin(), tokens.end()};
1182}
1183
1184// ── Evaluation (v1.9.10) ──────────────────────────────────
1185
1201 const int32_t* tokens,
1202 int n_tokens)
1203{
1205 LogprobResult result;
1206 result.tokens.assign(tokens, tokens + n_tokens);
1207 result.n_tokens = n_tokens;
1208 result.n_logprobs = n_tokens - 1;
1209 result.logprobs.reserve(result.n_logprobs);
1210
1211 auto* mem = llama_get_memory(ctx_);
1212 llama_memory_clear(mem, true);
1213
1214 for (int i = 0; i < n_tokens; i++) {
1217 int rc = llama_decode(ctx_, batch);
1218 if (rc != 0) {
1219 llama_memory_clear(mem, true);
1220 throw std::runtime_error("llama_decode failed at logprob pos");
1221 }
1222 if (i < n_tokens - 1) {
1223 const float* logits = llama_get_logits_ith(ctx_, -1);
1224 float lp = extract_token_logprob(
1225 logits, tokens[i + 1], n_vocab);
1226 result.logprobs.push_back(lp);
1227 }
1228 }
1229
1230 float sum = 0.0f;
1231 for (float lp : result.logprobs) { sum += lp; }
1232 result.total_logprob = sum;
1233 result.perplexity = std::exp(
1234 -sum / static_cast<float>(result.n_logprobs));
1235
1236 llama_memory_clear(mem, true);
1237 return result;
1238}
1239
1256 std::lock_guard<std::mutex> lock(seq_id_mutex_);
1257 if (!free_seq_ids_.empty()) {
1258 auto id = free_seq_ids_.back();
1259 free_seq_ids_.pop_back();
1260 return id;
1261 }
1262 return next_temp_seq_id_++;
1263}
1264
1272 std::lock_guard<std::mutex> lock(seq_id_mutex_);
1273 free_seq_ids_.push_back(seq_id);
1274}
1275
1290 const float* logits,
1292 int n_vocab)
1293{
1294 float max_logit = logits[0];
1295 for (int v = 1; v < n_vocab; v++) {
1296 if (logits[v] > max_logit) {
1297 max_logit = logits[v];
1298 }
1299 }
1300 float sum_exp = 0.0f;
1301 for (int v = 0; v < n_vocab; v++) {
1302 sum_exp += std::exp(logits[v] - max_logit);
1303 }
1304 float log_sum_exp = max_logit + std::log(sum_exp);
1305 return logits[next_token] - log_sum_exp;
1306}
1307
1308// ── Chat template ──────────────────────────────────────────
1309
1317static std::vector<llama_chat_message> to_llama_chat(
1318 const std::vector<Message>& messages) {
1319 std::vector<llama_chat_message> chat_msgs;
1320 chat_msgs.reserve(messages.size());
1321 for (const auto& msg : messages) {
1322 chat_msgs.push_back({msg.role.c_str(), msg.content.c_str()});
1323 }
1324 return chat_msgs;
1325}
1326
1340static std::vector<common_chat_msg> to_common_chat(
1341 const std::vector<Message>& messages) {
1342 std::vector<common_chat_msg> out;
1343 out.reserve(messages.size());
1344 for (const auto& msg : messages) {
1346 cm.role = msg.role;
1347 cm.content = msg.content;
1348 out.push_back(std::move(cm));
1349 }
1350 return out;
1351}
1352
1367static std::vector<common_chat_tool> mcp_tools_to_common_chat(
1368 const std::string& tools_json) {
1369 std::vector<common_chat_tool> out;
1370 if (tools_json.empty()) { return out; }
1371 auto arr = nlohmann::json::parse(tools_json, nullptr, false);
1372 if (!arr.is_array()) { return out; }
1373 for (const auto& t : arr) {
1375 ct.name = t.value("name", "");
1376 ct.description = t.value("description", "");
1377 if (t.contains("inputSchema")) {
1378 ct.parameters = t["inputSchema"].dump();
1379 }
1380 if (!ct.name.empty()) { out.push_back(std::move(ct)); }
1381 }
1382 return out;
1383}
1384
1398 ToolCall tc;
1399 tc.id = cc.id;
1400 tc.name = cc.name;
1401 tc.arguments_json = cc.arguments;
1402 auto j = nlohmann::json::parse(cc.arguments, nullptr, false);
1403 if (j.is_object()) {
1404 for (auto it = j.begin(); it != j.end(); ++it) {
1405 tc.arguments[it.key()] =
1406 it->is_string() ? it->get<std::string>() : it->dump();
1407 }
1408 }
1409 return tc;
1410}
1411
1437static std::optional<common_chat_params> render_common_chat(
1438 llama_model* model,
1439 const std::vector<Message>& messages,
1440 const GenerationParams& params,
1441 const std::vector<common_chat_tool>& tools,
1442 bool require_tool_call) {
1443 if (model == nullptr) { return std::nullopt; }
1444 auto tmpls = common_chat_templates_init(model, "");
1445 std::optional<common_chat_params> out;
1446 if (tmpls) {
1448 inputs.messages = to_common_chat(messages);
1449 inputs.add_generation_prompt = true;
1450 inputs.use_jinja = true;
1451 inputs.enable_thinking = params.enable_thinking; // gh#86
1452 inputs.tools = tools;
1453 if (!tools.empty()) {
1454 // gh#134 (v2.10.4): was hardcoded AUTO, so a mandatory-tool tier
1455 // could always answer in prose. REQUIRED makes upstream build the
1456 // tool-call grammar eagerly (grammar_lazy=false), which the
1457 // sampler now actually applies — see to_common_sampling.
1458 inputs.tool_choice = require_tool_call
1461 }
1462 try {
1464 } catch (const std::exception& e) {
1465 logger->warn("jinja chat template apply failed ({}); "
1466 "falling back to low-level template", e.what());
1467 }
1468 }
1469 return out;
1470}
1471
1479static std::string concat_messages_fallback(
1480 const std::vector<Message>& messages) {
1481 std::string fallback;
1482 for (const auto& msg : messages) {
1483 fallback += msg.role + ": " + msg.content + "\n";
1484 }
1485 return fallback;
1486}
1487
1509 const std::vector<Message>& messages,
1510 const GenerationParams& params) const
1511{
1512 // No tools staged, so tool_choice is moot — pass false.
1513 auto rendered = render_common_chat(model_, messages, params, {}, false);
1514 return rendered ? rendered->prompt
1515 : apply_chat_template_lowlevel(messages);
1516}
1517
1532 const std::vector<Message>& messages,
1533 const GenerationParams& params)
1534{
1535 if (!active_tools_json_.empty()) {
1536 return render_with_tools(messages, params);
1537 }
1538 have_chat_params_ = false;
1539 return apply_chat_template(messages, params);
1540}
1541
1555 logger->info("Active tools staged for common_chat render: {} bytes",
1556 tools_json.size());
1557}
1558
1581 const std::vector<Message>& messages,
1582 const GenerationParams& params)
1583{
1584 have_chat_params_ = false;
1586 auto rendered = render_common_chat(model_, messages, params, tools,
1588 std::string prompt;
1589 if (rendered) {
1590 last_chat_format_ = static_cast<int>(rendered->format);
1591 last_generation_prompt_ = rendered->generation_prompt;
1592 last_parser_ = rendered->parser;
1593 have_chat_params_ = true;
1594 // gh#105: snapshot this TOOLED render for the engine's later re-parse.
1595 // A toolless interleave (validator critique) clears have_chat_params_
1596 // but NOT this snapshot, so parse_response still decodes the main call.
1600 parse_params_valid_ = true;
1601 // gh#134 (v2.10.4): keep the tool-call grammar the render derived from
1602 // the staged schemas. Discarding it is why tools-staged tiers decode
1603 // unconstrained today.
1604 tool_grammar_ = rendered->grammar;
1605 tool_grammar_lazy_ = rendered->grammar_lazy;
1606 prompt = rendered->prompt;
1607 logger->info("render_with_tools: format={}, {} tool(s), captured "
1608 "parser ({} bytes), grammar ({} bytes, lazy={})",
1609 last_chat_format_, tools.size(), last_parser_.size(),
1611 // gh#138: the silent failure a consumer cannot see. A tier asking for
1612 // tool_choice=REQUIRED whose render yields no grammar is unconstrained
1613 // — the flag is set, tools are staged, and nothing enforces the call.
1614 // Loud, because the alternative is what gh#138 actually experienced:
1615 // prose-only turns with no diagnostic anywhere.
1616 if (require_tool_call_ && tool_grammar_.empty()) {
1617 logger->error(
1618 "require_tool_call is set for this tier but the chat template "
1619 "derived NO tool-call grammar from {} staged tool(s) "
1620 "(format={}). The turn will decode unconstrained and may end "
1621 "in prose with no tool call. This template may not support "
1622 "tool_choice=REQUIRED.",
1623 tools.size(), last_chat_format_);
1624 }
1625 } else {
1626 // gh#137: this fallback CANNOT honor enable_thinking — the low-level
1627 // llama_chat_apply_template API has no slot for it. Saying so is the
1628 // difference between a diagnosable config problem and a mystery: a
1629 // tier that set enable_thinking:false still reasons here, and on
1630 // gemma4 the whole generation can be one unterminated <|channel>
1631 // block, which the reasoning strip then correctly reduces to zero
1632 // characters. The consumer sees "0 chars, 0 tool calls" and no cause.
1633 if (!params.enable_thinking) {
1634 logger->warn(
1635 "Falling back to the low-level chat template, which cannot "
1636 "honor enable_thinking:false — this tier WILL still emit "
1637 "reasoning, and a generation that is entirely reasoning is "
1638 "stripped to empty content. The jinja render was unavailable "
1639 "for this turn (no tools staged, or the template could not be "
1640 "applied).");
1641 }
1642 prompt = apply_chat_template_lowlevel(messages);
1643 }
1644 return prompt;
1645}
1646
1659 // gh#105: read the sticky last-TOOLED snapshot, not the live capture — the
1660 // engine queries this AFTER a toolless validator render would have cleared
1661 // have_chat_params_, so the live flag is unreliable here.
1662 return parse_params_valid_
1664}
1665
1675 // last_chat_format_ is stored as int (the captured common_chat_format).
1676 return have_chat_params_
1679 : "";
1680}
1681
1700std::vector<std::string> LlamaCppBackend::effective_stop(
1701 const GenerationParams& params) const {
1703 const std::size_t before = p.stop.size();
1705 if (p.stop.size() > before) {
1706 logger->info("Sequential tier: tool-call close marker injected "
1707 "post-render (gh#105) — hard-stop at first tool call");
1708 }
1709 return p.stop;
1710}
1711
1737void strip_thinking_channels(std::string& content, std::string* reasoning_out) {
1738 static const std::string kOpen = "<|channel>";
1739 static const std::string kClose = "<channel|>";
1740 bool stripped = false;
1741 bool truncated_unclosed = false;
1742 std::size_t pos;
1743 while ((pos = content.find(kOpen)) != std::string::npos) {
1744 stripped = true;
1745 std::size_t end = content.find(kClose, pos + kOpen.size());
1746 if (end == std::string::npos) { truncated_unclosed = true; }
1747 std::size_t span_end =
1748 (end == std::string::npos) ? content.size() : end + kClose.size();
1749 if (reasoning_out != nullptr) {
1750 std::size_t inner = pos + kOpen.size();
1751 std::size_t inner_end =
1752 (end == std::string::npos) ? content.size() : end;
1753 reasoning_out->append(content, inner, inner_end - inner);
1754 }
1755 content.erase(pos, span_end - pos);
1756 }
1757 if (stripped) {
1758 std::size_t nb = content.find_first_not_of(" \t\r\n");
1759 content.erase(0, nb == std::string::npos ? content.size() : nb);
1760 }
1761 if (truncated_unclosed && content.empty()) {
1762 logger->warn("strip_thinking_channels: a <|channel> reasoning block "
1763 "was never closed, so the strip removed the whole "
1764 "generation and content is empty. Not a parse error. The "
1765 "orchestrator reports whether this was a token budget or "
1766 "the model ending its own turn — only it holds "
1767 "finish_reason. gh#137.");
1768 }
1769}
1770
1790 const std::string& raw) const
1791{
1792 CommonChatResult result;
1793 // gh#105: decode from the sticky last-TOOLED snapshot (parse_*), NOT the
1794 // live capture — a toolless validator render between the main generation
1795 // and this re-parse would have cleared the live params.
1796 if (!parse_params_valid_) {
1797 result.content = raw;
1798 return result;
1799 }
1801 pp.format = static_cast<common_chat_format>(parse_chat_format_);
1802 pp.generation_prompt = parse_generation_prompt_;
1803 pp.parser.load(parse_parser_); // mandatory — see header
1804 try {
1805 auto msg = common_chat_parse(raw, /*is_partial=*/false, pp);
1806 result.content = msg.content;
1807 result.reasoning_content = msg.reasoning_content;
1808 // gh#106: Gemma 4 QAT reasoning channels common_chat doesn't parse.
1810 for (const auto& tc : msg.tool_calls) {
1811 result.tool_calls.push_back(to_entropic_tool_call(tc));
1812 }
1813 // gh#90: gemma <|"|> string-escape loses type through PEG_GEMMA4 —
1814 // restore string typing for params the staged schema declares string.
1816 } catch (const std::exception& e) {
1817 logger->warn("common_chat_parse failed ({}); raw kept as content",
1818 e.what());
1819 result.content = raw;
1820 }
1821 return result;
1822}
1823
1837 const std::vector<Message>& messages) const
1838{
1839 auto chat_msgs = to_llama_chat(messages);
1840
1842 nullptr, chat_msgs.data(), chat_msgs.size(),
1843 true, nullptr, 0);
1844 if (n < 0) {
1845 logger->error("llama_chat_apply_template failed (size query)");
1846 return concat_messages_fallback(messages);
1847 }
1848
1849 std::vector<char> buf(static_cast<size_t>(n + 1));
1851 nullptr, chat_msgs.data(), chat_msgs.size(),
1852 true, buf.data(), static_cast<int32_t>(buf.size()));
1853 if (written < 0) {
1854 logger->error("llama_chat_apply_template failed (render)");
1855 return concat_messages_fallback(messages);
1856 }
1857
1858 return std::string(buf.data(), static_cast<size_t>(written));
1859}
1860
1861// ── Sampler ────────────────────────────────────────────────
1862
1884std::unique_ptr<Sampler> LlamaCppBackend::create_sampler(
1885 const GenerationParams& params) const
1886{
1887 if (!sampler_factory_) { return nullptr; }
1888 return sampler_factory_->create(params);
1889}
1890
1891// ── Decode loop ────────────────────────────────────────────
1892
1900bool LlamaCppBackend::run_prefill(const std::vector<llama_token>& tokens) {
1901 // gh#144 (v2.12.0): clear only this session's sequence when a pool is
1902 // configured. A whole-context clear here wiped every OTHER session's KV
1903 // on any cold prefill, which is both a correctness hazard and the end of
1904 // any cross-session reuse. max_sessions == 1 keeps the original clear so
1905 // the single-session path stays byte-identical.
1906 if (residency_.slots() > 1) {
1908 static_cast<llama_seq_id>(active_slot_), -1, -1);
1909 } else {
1910 // gh#158: instrumented, see kv_full_clear_count().
1913 }
1914
1915 // gh#144 (v2.12.0): chunking and position arithmetic now live in
1916 // decode_tokens_into_slot. The memory was just cleared for this slot, so
1917 // its sequence is empty and positions start at 0 — exactly what the
1918 // previous auto-positioned llama_batch_get_one produced.
1920 logger->error("Prefill decode failed (slot={})", active_slot_);
1921 return false;
1922 }
1923 return true;
1924}
1925
1943 Sampler& sampler,
1944 std::string& generated,
1945 std::function<void(std::string_view)>& on_token,
1946 const std::vector<std::string>& stop)
1947{
1948 llama_token new_token = sampler.sample();
1949
1952 return "eos";
1953 }
1954
1955 std::string piece = detokenize(new_token);
1956 generated += piece;
1957 if (on_token) {
1958 on_token(std::string_view(piece));
1959 }
1960 if (check_stop_sequences(generated, stop)) {
1961 return "stop";
1962 }
1963
1964 // gh#144 (v2.12.0): the freshly sampled token must extend THIS session's
1965 // sequence. Under a pool, auto-positioning into seq 0 would append one
1966 // session's generated token onto another's KV.
1968 auto* mem = llama_get_memory(ctx_);
1969 const auto seq = static_cast<llama_seq_id>(active_slot_);
1971 single.n_tokens = 1;
1974 /*want_logits=*/true);
1975 const bool ok = (llama_decode(ctx_, single) == 0);
1977 return ok ? "continue" : "error";
1978}
1979
1995 const std::vector<llama_token>& tokens,
1996 const GenerationParams& params,
1997 std::function<void(std::string_view)> on_token,
1998 std::atomic<bool>* cancel)
1999{
2000 // v2.3.10: Sampler seam — factory installed in do_activate.
2001 auto sampler = create_sampler(params);
2002 if (!sampler) {
2003 GenerationResult result;
2005 result.error_message = "Sampler factory not initialized";
2006 result.finish_reason = "error";
2007 return result;
2008 }
2009
2010 if (!run_prefill(tokens)) {
2011 GenerationResult result;
2013 result.error_message = "Prefill decode failed";
2014 result.finish_reason = "error";
2015 return result;
2016 }
2017
2018 return generate_after_prefill(*sampler, params, std::move(on_token), cancel);
2019}
2020
2041 Sampler& sampler,
2042 const GenerationParams& params,
2043 std::function<void(std::string_view)> on_token,
2044 std::atomic<bool>* cancel)
2045{
2046 GenerationResult result;
2047 std::string generated;
2048 int n_generated = 0;
2049 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
2050
2051 while (n_generated < params.max_tokens) {
2052 bool cancelled = cancel && cancel->load(std::memory_order_acquire);
2053 if (cancelled) {
2054 result.finish_reason = "cancelled";
2056 break;
2057 }
2058
2059 auto status = step_token(sampler, generated, on_token, stop);
2060 if (status == "continue") {
2061 ++n_generated;
2062 } else {
2063 result.finish_reason = (status == "error") ? "error" : "stop";
2064 if (status == "error") {
2066 }
2067 break;
2068 }
2069 }
2070
2071 if (n_generated >= params.max_tokens && result.finish_reason.empty()) {
2072 result.finish_reason = "length";
2073 }
2074
2075 // gh#136 (4th type_error.316 recurrence): sanitize at INGRESS, where model
2076 // bytes first become a std::string, rather than at each of the ~18 .dump()
2077 // sites downstream. gh#112/113 were closed as "permanent closure of the
2078 // 316 family", then gh#114, gh#118, gh#132 and gh#136 each patched one more
2079 // egress. The exits keep multiplying; the entries do not. Guarding here
2080 // makes every downstream dump safe by construction, and a NEW .dump()
2081 // anywhere cannot reintroduce the bug.
2082 //
2083 // Once at finalization, never per-token: a multi-byte codepoint can split
2084 // across token boundaries and per-token sanitize would corrupt valid
2085 // output (see the same reasoning at response_generator.cpp:360, which has
2086 // guarded the streaming accumulator this way since v2.1.1).
2087 //
2088 // Safe for raw_content consumers: raw_content is a COPY of content
2089 // (orchestrator.cpp:514), and sanitize only replaces bytes that are
2090 // ALREADY invalid UTF-8 — which cannot form part of any valid JSON token,
2091 // so the gh#88 envelope recovery and fenced-JSON fallbacks are unaffected.
2092 result.content = entropic::mcp::sanitize_utf8(generated);
2093 result.token_count = n_generated;
2094 return result;
2095}
2096
2097// ── gh#98: same-prefix multi-seq batched generation ────────
2098
2104static GenerationResult batch_error_result(const std::string& msg) {
2107 e.error_message = msg;
2108 e.finish_reason = "error";
2109 return e;
2110}
2111
2119 b.token[k] = tok;
2120 b.pos[k] = pos;
2121 b.n_seq_id[k] = 1;
2122 b.seq_id[k][0] = seq;
2123 b.logits[k] = want_logits ? 1 : 0;
2124}
2125
2133 std::vector<BatchSeq>& seqs,
2134 const std::vector<GenerationParams>& params) {
2135 // gh#158 (v2.13.0): EVERY arm gets a temp sequence, arm 0 included. It
2136 // used to take sequence 0 — which is a SESSION slot — so a batch wrote
2137 // over whatever session slot 0 held before it had cleared anything at
2138 // all. The plan below then refuses any id that would still land inside
2139 // the session range.
2140 std::vector<int> minted;
2141 minted.reserve(seqs.size());
2142 for (std::size_t i = 0; i < seqs.size(); ++i) {
2143 seqs[i].sampler = create_sampler(params[i]);
2144 auto* ls = dynamic_cast<LlamaCppSampler*>(seqs[i].sampler.get());
2145 if (ls == nullptr) { return false; }
2146 seqs[i].chain = ls->native_chain();
2147 seqs[i].max_tokens = params[i].max_tokens;
2148 minted.push_back(static_cast<int>(allocate_temp_seq_id()));
2149 }
2150 const auto plan = plan_batch_kv(
2151 seqs.size(), derive_pool_geometry(config()).temp_seq_base, minted);
2152 for (std::size_t i = 0; i < seqs.size() && i < plan.seq_ids.size(); ++i) {
2153 seqs[i].seq_id = static_cast<llama_seq_id>(plan.seq_ids[i]);
2154 }
2155 return true;
2156}
2157
2164 std::vector<BatchSeq>& seqs, const std::vector<llama_token>& seq0,
2165 std::size_t shared) {
2166 std::vector<llama_token> prefix(
2167 seq0.begin(), seq0.begin() + static_cast<long>(shared));
2168 // gh#158: into the batch's OWN lead sequence, never into `active_slot_`
2169 // (which decode_tokens_from targets) and never into sequence 0.
2170 const llama_seq_id lead = seqs[0].seq_id;
2171 if (!decode_tokens_into_slot(prefix, 0, static_cast<int>(lead))) {
2172 return false;
2173 }
2174 auto* mem = llama_get_memory(ctx_);
2175 for (std::size_t i = 1; i < seqs.size(); ++i) {
2176 llama_memory_seq_cp(mem, lead, seqs[i].seq_id, 0,
2177 static_cast<llama_pos>(shared));
2178 }
2179 for (auto& s : seqs) { s.pos = static_cast<int>(shared); }
2180 return true;
2181}
2182
2192 std::vector<BatchSeq>& seqs,
2193 const std::vector<std::vector<llama_token>>& toks,
2194 std::size_t shared) {
2195 int total = 0;
2196 // shared <= shortest-1 < every t.size() by batch_shared_prefix_len, but
2197 // guard the unsigned subtraction defensively (a bad `shared` would else
2198 // underflow to a huge alloc).
2199 for (const auto& t : toks) {
2200 total += static_cast<int>(t.size() - std::min(shared, t.size()));
2201 }
2203 static_cast<int32_t>(seqs.size()));
2204 int k = 0;
2205 for (std::size_t i = 0; i < seqs.size(); ++i) {
2206 int len = static_cast<int>(toks[i].size());
2207 for (int p = static_cast<int>(shared); p < len; ++p) {
2208 fill_batch_cell(batch, k, toks[i][p], p, seqs[i].seq_id,
2209 p == len - 1);
2210 if (p == len - 1) { seqs[i].logits_idx = k; }
2211 ++k;
2212 }
2213 seqs[i].pos = len;
2214 }
2215 batch.n_tokens = k;
2217 bool ok = (llama_decode(ctx_, batch) == 0);
2219 return ok;
2220}
2221
2227void LlamaCppBackend::sample_batch_active(std::vector<BatchSeq>& seqs) {
2228 for (auto& s : seqs) {
2229 if (!s.active) { continue; }
2230 // llama_sampler_sample() accepts the drawn token into the chain
2231 // internally (advancing grammar/penalties) — matching the single-seq
2232 // step_token path. A second accept would double-advance the grammar.
2233 llama_token tok = llama_sampler_sample(s.chain, ctx_, s.logits_idx);
2235 s.active = false;
2236 s.finish = "stop";
2237 continue;
2238 }
2239 s.out.push_back(tok);
2240 ++s.n_gen;
2241 if (s.n_gen >= s.max_tokens) { s.active = false; s.finish = "length"; }
2242 }
2243}
2244
2256 std::vector<BatchSeq>& seqs, int max_steps, std::atomic<bool>& cancel) {
2257 llama_batch batch = llama_batch_init(static_cast<int32_t>(seqs.size()), 0,
2258 static_cast<int32_t>(seqs.size()));
2259 for (int step = 0; step < max_steps; ++step) {
2260 if (cancel.load(std::memory_order_acquire)) { break; }
2262 int k = 0;
2263 for (auto& s : seqs) {
2264 if (!s.active) { continue; }
2265 fill_batch_cell(batch, k, s.out.back(), s.pos, s.seq_id, true);
2266 s.logits_idx = k;
2267 ++s.pos;
2268 ++k;
2269 }
2270 if (k == 0) { break; }
2271 batch.n_tokens = k;
2273 if (llama_decode(ctx_, batch) != 0) { break; }
2274 }
2276}
2277
2283std::vector<GenerationResult> LlamaCppBackend::build_batch_results(
2284 std::vector<BatchSeq>& seqs) {
2285 std::vector<GenerationResult> out;
2286 out.reserve(seqs.size());
2287 for (auto& s : seqs) {
2289 for (llama_token t : s.out) { r.content += detokenize(t); }
2290 r.token_count = s.n_gen;
2291 r.finish_reason = s.finish;
2292 out.push_back(std::move(r));
2293 }
2294 return out;
2295}
2296
2302void LlamaCppBackend::release_temp_seqs(std::vector<BatchSeq>& seqs) {
2303 // gh#158 (v2.13.0): arm 0 now holds a temp id too (it used to be hard
2304 // sequence 0, a session slot), so every arm is released — and each one's
2305 // cells are dropped, which is what replaces the whole-cache clear.
2306 auto* mem = ctx_ != nullptr ? llama_get_memory(ctx_) : nullptr;
2307 for (auto& s : seqs) {
2308 if (s.seq_id == 0) { continue; }
2309 if (mem != nullptr) { llama_memory_seq_rm(mem, s.seq_id, -1, -1); }
2310 release_temp_seq_id(s.seq_id);
2311 }
2312}
2313
2325std::vector<GenerationResult> LlamaCppBackend::run_batched_decode(
2326 const std::vector<std::vector<llama_token>>& toks,
2327 const std::vector<GenerationParams>& params,
2328 std::size_t shared,
2329 std::atomic<bool>& cancel)
2330{
2331 const std::size_t n = toks.size();
2332 std::vector<BatchSeq> seqs(n);
2334 release_temp_seqs(seqs); // don't leak ids allocated before the failure
2335 return std::vector<GenerationResult>(
2336 n, batch_error_result("batch sampler init"));
2337 }
2338 int max_steps = 0;
2339 for (const auto& p : params) { max_steps = std::max(max_steps, p.max_tokens); }
2340
2341 // gh#158 (v2.13.0): this used to open with
2342 // `llama_memory_clear(mem, true)` + `invalidate_all_resident_kv()` — the
2343 // WHOLE cache, so a batch on session A wiped session B's resident prefix
2344 // and B's next turn paid a silent cold prefill. The batch now owns only
2345 // its own temp sequences and drops exactly those in release_temp_seqs,
2346 // so no session slot is touched at either end.
2349
2353
2354 auto out = ok ? build_batch_results(seqs)
2355 : std::vector<GenerationResult>(
2356 n, batch_error_result("batch prefill"));
2358 logger->info("gh#98 batch: requests={} prefix.tokens_shared={} "
2359 "prefix.tokens_saved={} total_prefill_tokens={} gen_decodes={}",
2362 return out;
2363}
2364
2382std::vector<GenerationResult> LlamaCppBackend::do_generate_batch(
2383 const std::vector<std::vector<Message>>& requests,
2384 const std::vector<GenerationParams>& params,
2385 std::atomic<bool>& cancel)
2386{
2387 const std::size_t n = requests.size();
2388 std::vector<std::vector<llama_token>> toks(n);
2389 for (std::size_t i = 0; i < n; ++i) {
2390 toks[i] = tokenize(render_prompt(requests[i], params[i]), true);
2391 }
2392 const std::size_t shared = batch_shared_prefix_len(toks);
2393 std::size_t total_suffix = 0;
2394 for (const auto& t : toks) { total_suffix += t.size() - shared; }
2395
2396 const bool hybrid = is_hybrid_ || is_recurrent_;
2397 if (!batch_is_viable(n, config().n_parallel, shared, hybrid,
2398 total_suffix, config().n_batch)) {
2400 }
2402}
2403
2404// ── Prompt cache helpers ───────────────────────────────────
2405
2414 const std::vector<Message>& messages)
2415{
2416 for (const auto& msg : messages) {
2417 if (msg.role == "system") {
2418 return msg.content;
2419 }
2420 }
2421 return "";
2422}
2423
2450 const std::vector<llama_token>& tokens, int start_offset, int slot)
2451{
2452 const int total = static_cast<int>(tokens.size());
2453 if (start_offset >= total) { return true; }
2454
2455 auto* mem = llama_get_memory(ctx_);
2456 const auto seq = static_cast<llama_seq_id>(slot);
2457 // -1 on an empty sequence, so base is 0 for a cold slot.
2459
2460 const int n_batch = llama_n_batch(ctx_);
2461 const int n_remaining = total - start_offset;
2463
2464 bool ok = true;
2465 for (int off = 0; off < n_remaining && ok; off += n_batch) {
2466 const int chunk = std::min(n_batch, n_remaining - off);
2468 batch.n_tokens = chunk;
2469 for (int k = 0; k < chunk; ++k) {
2471 tokens[static_cast<std::size_t>(
2472 start_offset + off + k)],
2473 base + off + k, seq, /*want_logits=*/false);
2474 }
2475 // Each chunk's last token carries logits. llama_batch_get_one passes
2476 // logits = nullptr, which makes llama.cpp emit logits for the last
2477 // token of EACH decode call — so requesting them per chunk, not just
2478 // on the final one, keeps this byte-identical to the path it
2479 // replaces. Cheaper to compute a few unused logits than to differ
2480 // from a path whose divergence would be silent.
2481 batch.logits[chunk - 1] = 1;
2482 if (llama_decode(ctx_, batch) != 0) {
2483 logger->error("Decode chunk failed (slot={}, start={}, off={}, "
2484 "chunk={})", slot, start_offset, off, chunk);
2485 ok = false;
2486 }
2488 }
2489 return ok;
2490}
2491
2505 const std::vector<llama_token>& tokens, int start_offset)
2506{
2507 // gh#144 (v2.12.0): decode into whichever slot this generation is bound
2508 // to. active_slot_ is 0 unless a session pool is configured, so the
2509 // single-session path is unchanged.
2511}
2512
2530 const CacheEntry* cached,
2531 const std::vector<llama_token>& tokens)
2532{
2533 auto* mem = llama_get_memory(ctx_);
2534 const auto seq = static_cast<llama_seq_id>(active_slot_);
2535
2536 // gh#144 (v2.12.0): restore into THIS session's sequence, and clear only
2537 // it. Both halves were wrong for a pool and the model test caught it:
2538 // the cached prefix landed in sequence 0 while the remainder decoded into
2539 // the caller's own slot, so every session except the one that happened to
2540 // own slot 0 attended to a fragment with no system prompt — and the
2541 // whole-context clear wiped the other sessions on the way past.
2542 if (residency_.slots() > 1) {
2543 llama_memory_seq_rm(mem, seq, -1, -1);
2544 } else {
2545 llama_memory_clear(mem, true);
2546 }
2547
2549 ctx_, cached->data.data(), cached->data_size, seq);
2550 if (restored == 0) {
2551 logger->warn("KV state restore failed, falling back to full prefill");
2552 return false;
2553 }
2554
2555 return decode_tokens_from(tokens, cached->token_count);
2556}
2557
2571 const CacheKey& key, int prefix_tokens)
2572{
2574 if (state_size == 0) {
2575 return;
2576 }
2577
2578 std::vector<uint8_t> buf(state_size);
2580 ctx_, buf.data(), buf.size(), 0);
2581 if (written > 0) {
2582 buf.resize(written);
2583 prompt_cache_->store(key, std::move(buf), prefix_tokens);
2584 }
2585}
2586
2596 const std::vector<Message>& messages,
2597 const GenerationParams& params)
2598{
2599 std::vector<Message> sys_msgs;
2600 for (const auto& msg : messages) {
2601 if (msg.role == "system") {
2602 sys_msgs.push_back(msg);
2603 }
2604 }
2605 if (sys_msgs.empty()) {
2606 return 0;
2607 }
2608
2610 auto sys_tokens = tokenize(sys_prompt, true);
2611 return static_cast<int>(sys_tokens.size());
2612}
2613
2636 const std::vector<llama_token>& tokens,
2637 int prefix_tokens,
2638 const CacheKey& key)
2639{
2640 int total = static_cast<int>(tokens.size());
2642 return run_prefill(tokens);
2643 }
2644
2645 // Pass 1: prefill only the prefix — `run_prefill` calls
2646 // llama_memory_clear, so seq 0 ends up holding exactly
2647 // prefix_tokens positions.
2648 std::vector<llama_token> prefix(
2649 tokens.begin(), tokens.begin() + prefix_tokens);
2650 if (!run_prefill(prefix)) {
2651 return false;
2652 }
2653
2654 // Save now: state contains exactly the prefix.
2656
2657 // Pass 2: continue prefilling the remainder. No clear — decode
2658 // appends after the saved prefix positions.
2660}
2661
2669static int staged_tool_count(const std::string& tools_json) {
2670 if (tools_json.empty()) { return 0; }
2671 auto arr = nlohmann::json::parse(tools_json, nullptr, false);
2672 return arr.is_array() ? static_cast<int>(arr.size()) : 0;
2673}
2674
2692 const std::vector<llama_token>& tokens,
2693 const std::string& system_prompt)
2694{
2696 fit.prompt_tokens = static_cast<int>(tokens.size());
2697 fit.context_length = config().context_length;
2698 if (!context_fit_overflows(fit)) {
2699 prefill_refusal_.clear();
2700 return false;
2701 }
2702 fit.tool_bytes = active_tools_json_.size();
2704 fit.tool_tokens =
2705 static_cast<int>(tokenize(active_tools_json_, false).size());
2706 fit.system_tokens =
2707 static_cast<int>(tokenize(system_prompt, false).size());
2709 logger->error("{}", prefill_refusal_);
2710 return true;
2711}
2712
2722 r.finish_reason = "error";
2723 if (!prefill_refusal_.empty()) {
2725 r.error_message = prefill_refusal_;
2726 return r;
2727 }
2728 r.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
2729 r.error_message = "Prefill decode failed";
2730 return r;
2731}
2732
2755 const std::vector<llama_token>& tokens,
2756 const std::string& system_prompt,
2757 const std::vector<Message>& messages,
2758 const GenerationParams& params)
2759{
2760 // gh#96 (v2.7.5): count tokens actually pushed through llama_decode during
2761 // prefill this turn. run_prefill / decode_tokens_from accumulate into
2762 // last_prefill_tokens_; a prompt-cache HIT restores the system prefix
2763 // without a decode, so this counts the re-decoded post-system remainder —
2764 // the per-turn waste that climbs today and should collapse to the appended
2765 // delta once warm-keep reuse lands. (llama_perf n_p_eval proved unreliable
2766 // across the state-restore boundary, so we count the decodes directly.)
2768 last_input_tokens_ = static_cast<int>(tokens.size()); // gh#97
2769 // v2.13.0: a prompt that cannot fit is refused HERE, before llama_decode
2770 // is called even once. Proceeding is what produced the release-gate
2771 // symptom: chunked decode failures every turn, a cache restore that
2772 // could not land, and an empty-turn allowance spent on a prompt no
2773 // retry could shrink.
2774 if (refuse_over_context(tokens, system_prompt)) {
2775 return false;
2776 }
2777 // A prefill without a context cannot succeed, and saying so is cheaper
2778 // than dereferencing null inside llama_get_memory further down. Both
2779 // branches below reach llama.cpp, so the check belongs here rather than
2780 // in either one. Production never arrives un-ACTIVE (the base class
2781 // gates generate()); this is what lets the CPU unit tier drive the
2782 // decode path at all.
2783 if (ctx_ == nullptr) {
2784 logger->error("Prefill requested with no context (not ACTIVE)");
2785 return false;
2786 }
2787 auto t_pre = entropic::log::now();
2788 bool ok;
2789 if (is_hybrid_ || is_recurrent_) {
2790 // gh#97 (v2.7.6): hybrid/recurrent (SSM) memory rejects the partial
2791 // seq_rm warm-keep needs (state can't be partially erased at the tail),
2792 // and the prompt-cache restore lands non-contiguous cells — both desync
2793 // KV positions (pos_max inflates → eventual decode slot-failure with the
2794 // cache mostly empty). Plain full prefill (clear + contiguous decode) is
2795 // the only correct path for these archs at this llama.cpp pin. Mirrors
2796 // the speculative-decoding guard. Forfeits the gh#96 reuse for them.
2799 } else {
2800 // gh#96 warm-keep: reuse the resident KV prefix + decode only the delta;
2801 // fall back to a cold prefill (clear + system-prefix cache) when reuse
2802 // is off, the prefix diverged, or the KV was mutated out-of-band.
2804 if (!ok) {
2805 ok = prefill_dispatch(tokens, system_prompt, messages, params);
2806 if (ok) {
2808 } else {
2810 }
2811 }
2812 }
2813 last_prefill_ms_ = entropic::log::elapsed_ms(t_pre, entropic::log::now());
2814 logger->info("Prefill (gh#96): {} tokens / {:.1f} ms decoded this turn",
2816 return ok;
2817}
2818
2851bool LlamaCppBackend::try_warm_reuse(const std::vector<llama_token>& tokens) {
2852 if (!prompt_cache_config_.warm_keep || ctx_ == nullptr) {
2853 return false;
2854 }
2855 // gh#144 (v2.12.0): ask THIS session's slot what it holds. Against the
2856 // previous single shared vector, a second session sharing the system
2857 // prompt scored a non-zero common prefix and reused destructively.
2858 const int slot = active_slot_;
2859 auto* mem = llama_get_memory(ctx_);
2860 long pos_max = static_cast<long>(
2863 pos_max);
2864 if (cut == 0) {
2865 return false; // nothing reusable — cold prefill
2866 }
2867 // Drop the divergent tail (and any prior generated tokens past `cut`),
2868 // then decode only the appended delta. A single exit (returns <= 3 gate):
2869 // success records the new resident set; failure invalidates and reports it.
2871 static_cast<llama_pos>(cut), -1);
2872 bool ok = decode_tokens_from(tokens, static_cast<int>(cut));
2873 if (ok) {
2876 logger->info("Warm-keep: reused {} resident tokens, decoded {} "
2877 "delta (of {} total)", cut, tokens.size() - cut,
2878 tokens.size());
2879 }
2880 } else {
2882 }
2883 return ok;
2884}
2885
2897 // gh#144 (v2.12.0): only this session's slot. A caller that wiped the
2898 // WHOLE context must call invalidate_all_resident_kv() instead — believing
2899 // another slot is still resident after that is how a stale prefix gets
2900 // reused.
2902}
2903
2911void LlamaCppBackend::forget_session_kv(const std::string& session_key) {
2912 const int slot = residency_.forget_session(session_key);
2913 if (slot == kNoSessionSlot) { return; }
2914 if (ctx_ != nullptr) {
2916 static_cast<llama_seq_id>(slot), -1, -1);
2917 }
2918 logger->info("Session '{}' KV dropped from slot {} (context restored)",
2919 session_key, slot);
2920}
2921
2935
2953 const std::vector<llama_token>& tokens,
2954 const std::string& system_prompt,
2955 const std::vector<Message>& messages,
2956 const GenerationParams& params)
2957{
2960 && !system_prompt.empty();
2961
2962 if (!cache_enabled) {
2963 return run_prefill(tokens);
2964 }
2965
2967 system_prompt, config().path.string());
2968 const CacheEntry* cached = prompt_cache_->lookup(key);
2969
2970 if (cached != nullptr) {
2972 logger->info("Prompt cache HIT: {} bytes, {} prefix tokens",
2973 cached->data_size, cached->token_count);
2974 }
2976 return true;
2977 }
2978 logger->warn("Cache restore failed, falling back to full prefill");
2979 } else if (prompt_cache_config_.log_hits) {
2980 logger->info("Prompt cache MISS: processing full prompt");
2981 }
2982
2985}
2986
2987// ── Multimodal generation (v1.9.11 Phases 5–7 + v2.1.8) ────
2988
2989namespace {
2990
2996bool any_image_in(const std::vector<Message>& messages) {
2997 for (const auto& m : messages) {
2998 if (has_images(m.content_parts)) { return true; }
2999 }
3000 return false;
3001}
3002
3015std::vector<Message> strip_image_parts(
3016 const std::vector<Message>& messages) {
3017 std::vector<Message> out = messages;
3018 for (auto& m : out) {
3019 if (m.content_parts.empty()) { continue; }
3020 m.content = extract_text(m.content_parts);
3021 m.content_parts.clear();
3022 }
3023 return out;
3024}
3025
3043std::vector<Message> substitute_image_markers(
3044 const std::vector<Message>& messages,
3046 std::vector<::mtmd_bitmap*>& bitmaps_out) {
3047 std::vector<Message> out;
3048 out.reserve(messages.size());
3049 const std::string marker = mtmd_default_marker();
3050 for (const auto& m : messages) {
3051 Message copy;
3052 copy.role = m.role;
3053 if (m.content_parts.empty()) {
3054 copy.content = m.content;
3055 out.push_back(std::move(copy));
3056 continue;
3057 }
3058 std::string built;
3059 for (const auto& p : m.content_parts) {
3060 if (p.type != ContentPartType::IMAGE) {
3061 built += p.text;
3062 continue;
3063 }
3064 ::mtmd_bitmap* bm = nullptr;
3065 if (!p.image_path.empty()) {
3066 // b11009 added a trailing `mtmd_helper_init_opt` carrying
3067 // video decode params. `_default()` is the behaviour-
3068 // preserving value for a still image; the returned
3069 // wrapper's `video_ctx` is null on that path, so only
3070 // `.bitmap` needs owning.
3072 ctx, p.image_path.c_str(), /*placeholder=*/false,
3074 }
3075 if (bm == nullptr) { return {}; }
3076 bitmaps_out.push_back(bm);
3077 built += marker;
3078 }
3079 copy.content = std::move(built);
3080 out.push_back(std::move(copy));
3081 }
3082 return out;
3083}
3084
3085} // anonymous namespace
3086
3098 const std::string& prompt,
3099 const std::vector<::mtmd_bitmap*>& bitmaps,
3100 std::string& err_msg)
3101{
3102 ++kv_full_clear_count_; // gh#158: see kv_full_clear_count()
3104 ::mtmd_input_text mt{prompt.c_str(), true, true};
3106 std::vector<const ::mtmd_bitmap*> bm_cptrs(
3107 bitmaps.begin(), bitmaps.end());
3109 mtmd_ctx_, chunks, &mt, bm_cptrs.data(), bm_cptrs.size());
3110 if (tok_rc != 0) {
3112 err_msg = "mtmd_tokenize failed (rc="
3113 + std::to_string(tok_rc) + ")";
3115 }
3118 mtmd_ctx_, ctx_, chunks, 0, 0,
3119 static_cast<int32_t>(config().n_batch),
3120 true, &new_n_past);
3122 if (eval_rc != 0) {
3123 err_msg = "mtmd_helper_eval_chunks failed (rc="
3124 + std::to_string(eval_rc) + ")";
3126 }
3127 logger->info("Multimodal prefill complete: n_past={}", new_n_past);
3128 return ENTROPIC_OK;
3129}
3130
3148 const GenerationParams& params,
3149 std::function<void(std::string_view token)> on_token,
3150 std::atomic<bool>* cancel,
3151 const std::chrono::steady_clock::time_point& t0)
3152{
3153 GenerationResult result;
3154 // v2.3.10: Sampler seam.
3155 auto sampler = create_sampler(params);
3156 if (!sampler) {
3158 result.error_message = "Sampler factory not initialized";
3159 result.finish_reason = "error";
3160 finalize_result(result, t0);
3161 return result;
3162 }
3163 std::string generated;
3164 int n_generated = 0;
3165 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
3166 while (n_generated < params.max_tokens) {
3167 if (cancel != nullptr
3168 && cancel->load(std::memory_order_acquire)) {
3169 result.finish_reason = "cancelled";
3171 break;
3172 }
3173 auto status = step_token(
3174 *sampler, generated, on_token, stop);
3175 if (status == "continue") { ++n_generated; continue; }
3176 result.finish_reason = (status == "error") ? "error" : "stop";
3177 if (status == "error") {
3179 }
3180 break;
3181 }
3182 finalize_generation(result, generated, n_generated, params, t0,
3184 return result;
3185}
3186
3203 const std::vector<Message>& messages,
3204 const GenerationParams& params,
3205 std::function<void(std::string_view token)> on_token,
3206 std::atomic<bool>* cancel)
3207{
3208 auto t0 = entropic::log::now();
3209 invalidate_all_resident_kv(); // gh#96: mtmd_prefill mutates seq 0 out-of-band
3210 std::vector<::mtmd_bitmap*> bitmaps;
3212 messages, mtmd_ctx_, bitmaps);
3213 if (marked.empty()) {
3214 for (auto* b : bitmaps) { mtmd_bitmap_free(b); }
3217 err.error_message =
3218 "mtmd_helper_bitmap_init_from_file failed";
3219 return err;
3220 }
3221 auto prompt = render_prompt(marked, params);
3222 logger->info("Multimodal generate: {} images, prompt={} chars, max_tokens={}",
3223 bitmaps.size(), prompt.size(), params.max_tokens);
3224 std::string prefill_err;
3225 auto rc = mtmd_prefill(prompt, bitmaps, prefill_err);
3226 for (auto* b : bitmaps) { mtmd_bitmap_free(b); }
3227 if (rc != ENTROPIC_OK) {
3229 err.error_code = rc;
3230 err.error_message = std::move(prefill_err);
3231 return err;
3232 }
3233 return run_sampling_loop(params, on_token, cancel, t0);
3234}
3235
3236// ── Generation entry points ────────────────────────────────
3237
3256 if (residency_.slots() != pool) {
3258 }
3259 if (pool <= 1) {
3260 active_slot_ = 0;
3261 return;
3262 }
3263 std::string evicted;
3264 active_slot_ = residency_.acquire(params.session_key, &evicted);
3265 if (!evicted.empty() && ctx_ != nullptr) {
3266 // The slot is being reused, so the departing session's cells must go
3267 // with it — otherwise the incoming session inherits a prefix that
3268 // matches nothing it will send.
3270 static_cast<llama_seq_id>(active_slot_), -1, -1);
3272 logger->info("Session pool: evicted '{}' from slot {} for '{}'",
3273 evicted, active_slot_, params.session_key);
3274 }
3275}
3276
3293 const std::vector<Message>& messages,
3294 const GenerationParams& params)
3295{
3296 // gh#144 (v2.12.0): every generate path goes through here, so this is the
3297 // one place a session has to be bound to a sequence.
3299 if (!any_image_in(messages)) {
3300 return do_generate_text_only(messages, params);
3301 }
3302 if (has_vision_ && mtmd_ctx_ != nullptr) {
3303 return generate_multimodal(messages, params, nullptr, nullptr);
3304 }
3305 logger->warn("Image content present but model has no vision "
3306 "capability — stripping image parts");
3308}
3309
3316 const std::vector<Message>& messages,
3317 const GenerationParams& params)
3318{
3319 auto t0 = entropic::log::now();
3320 std::string prompt = render_prompt(messages, params);
3321 auto tokens = tokenize(prompt, true);
3322 std::string sys = extract_system_prompt(messages);
3323
3324 logger->info("Generate: {} input tokens, max_tokens={}",
3325 tokens.size(), params.max_tokens);
3327
3328 // v2.3.10: Sampler seam.
3329 auto sampler = create_sampler(params);
3330 if (!sampler) { return sampler_init_error(t0); }
3331
3332 if (!run_prefill_cached(tokens, sys, messages, params)) {
3333 return prefill_error();
3334 }
3335
3336 GenerationResult result;
3337 std::string generated;
3338 int n_generated = 0;
3339 std::function<void(std::string_view)> no_cb = nullptr;
3340 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
3341
3342 while (n_generated < params.max_tokens) {
3343 auto status = step_token(
3344 *sampler, generated, no_cb, stop);
3345 if (status == "continue") { ++n_generated; }
3346 else {
3347 result.finish_reason =
3348 (status == "error") ? "error" : "stop";
3349 if (status == "error") {
3351 }
3352 break;
3353 }
3354 }
3355
3356 finalize_generation(result, generated, n_generated, params, t0,
3358 return result;
3359}
3360
3372 const std::vector<Message>& messages,
3373 const GenerationParams& params,
3374 std::atomic<bool>& cancel)
3375{
3376 if (!any_image_in(messages)) {
3377 return do_generate_text_only(messages, params, cancel);
3378 }
3379 if (has_vision_ && mtmd_ctx_ != nullptr) {
3380 return generate_multimodal(messages, params, nullptr, &cancel);
3381 }
3382 logger->warn("Image content present but model has no vision "
3383 "capability — stripping image parts");
3385}
3386
3407 const std::vector<Message>& messages,
3408 const GenerationParams& params,
3409 std::atomic<bool>& cancel)
3410{
3411 auto t0 = entropic::log::now();
3412 std::string prompt = render_prompt(messages, params);
3413 auto tokens = tokenize(prompt, true);
3414 std::string sys = extract_system_prompt(messages);
3415
3416 logger->info("Generate (cancellable): {} input tokens, max_tokens={}",
3417 tokens.size(), params.max_tokens);
3419
3420 auto sampler = create_sampler(params);
3421 if (!sampler) { return sampler_init_error(t0); }
3422
3423 if (!run_prefill_cached(tokens, sys, messages, params)) {
3424 return prefill_error();
3425 }
3426
3427 GenerationResult result;
3428 std::string generated;
3429 int n_generated = 0;
3430 std::function<void(std::string_view)> no_cb = nullptr;
3431
3432 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
3433 while (n_generated < params.max_tokens) {
3434 if (cancel.load(std::memory_order_acquire)) {
3435 result.finish_reason = "cancelled";
3437 break;
3438 }
3439 auto status = step_token(
3440 *sampler, generated, no_cb, stop);
3441 if (status == "continue") { ++n_generated; }
3442 else {
3443 result.finish_reason =
3444 (status == "error") ? "error" : "stop";
3445 if (status == "error") {
3447 }
3448 break;
3449 }
3450 }
3451
3452 finalize_generation(result, generated, n_generated, params, t0,
3454 return result;
3455}
3456
3468 const std::vector<Message>& messages,
3469 const GenerationParams& params,
3470 std::function<void(std::string_view token)> on_token,
3471 std::atomic<bool>& cancel)
3472{
3473 if (!any_image_in(messages)) {
3475 messages, params, on_token, cancel);
3476 }
3477 if (has_vision_ && mtmd_ctx_ != nullptr) {
3478 return generate_multimodal(messages, params, on_token, &cancel);
3479 }
3480 logger->warn("Image content present but model has no vision "
3481 "capability — stripping image parts");
3483 strip_image_parts(messages), params, on_token, cancel);
3484}
3485
3498 const std::vector<Message>& messages,
3499 const GenerationParams& params,
3500 std::function<void(std::string_view token)> on_token,
3501 std::atomic<bool>& cancel)
3502{
3503 auto t0 = entropic::log::now();
3504 auto prompt = render_prompt(messages, params);
3505 auto tokens = tokenize(prompt, true);
3506 auto sys = extract_system_prompt(messages);
3507 logger->info("Stream: {} input tokens, max_tokens={}",
3508 tokens.size(), params.max_tokens);
3510
3511 // v2.3.10: Sampler seam.
3512 auto sampler = create_sampler(params);
3513 if (!sampler) { return sampler_init_error(t0); }
3514 if (!run_prefill_cached(tokens, sys, messages, params)) {
3515 return prefill_error();
3516 }
3517 GenerationResult result;
3518 std::string generated;
3519 int n_generated = 0;
3520 const auto stop = effective_stop(params); // gh#105: per-call sequential marker
3521 while (n_generated < params.max_tokens) {
3522 if (cancel.load(std::memory_order_acquire)) {
3523 result.finish_reason = "cancelled";
3525 break;
3526 }
3527 auto status = step_token(
3528 *sampler, generated, on_token, stop);
3529 if (status == "continue") { ++n_generated; }
3530 else {
3531 result.finish_reason =
3532 (status == "error") ? "error" : "stop";
3533 if (status == "error") {
3535 }
3536 break;
3537 }
3538 }
3539 finalize_generation(result, generated, n_generated, params, t0,
3541 return result;
3542}
3543
3557 const std::vector<Message>& /*messages*/,
3558 const GenerationParams& /*params*/,
3559 std::function<void(std::string_view)> /*on_token*/,
3560 std::atomic<bool>& /*cancel*/)
3561{
3562 GenerationResult result;
3564 result.error_message =
3565 "LlamaCppBackend speculative requires an explicit draft "
3566 "backend handle — orchestrator dispatches via "
3567 "generate_speculative_with_draft";
3568 result.finish_reason = "error";
3569 return result;
3570}
3571
3572namespace {
3573
3589static void apply_grammar_source(
3591 const GenerationParams& params,
3592 const std::string& tool_grammar,
3593 bool tool_grammar_lazy,
3594 const std::string& generation_prompt) {
3595 // gh#108 (v2.10.0): propagate GBNF grammar to the MTP sampler chain so
3596 // grammar-constrained tiers are correctly enforced under speculative.mtp.
3597 //
3598 // gh#134 (v2.10.4): a SECOND grammar source now reaches here — the
3599 // tool-call GBNF common_chat_templates_apply derives from the staged tool
3600 // schemas, which was previously discarded at the render. Precedence is
3601 // fail-loud rather than a silent pick: an explicit request grammar
3602 // alongside a tool-derived one is a config error, because the two
3603 // constrain the output to different languages and quietly honouring one
3604 // would make the other's absence undiagnosable.
3606 logger->error(
3607 "Both a request grammar and a tool-call grammar are active. These "
3608 "constrain decoding to different languages and cannot compose; the "
3609 "request grammar is being used and the staged tools will NOT be "
3610 "structurally enforced. Drop one: clear params.grammar to let the "
3611 "tool schemas constrain, or unstage tools to use your grammar.");
3612 }
3613 const auto source = resolve_grammar_source(params.grammar, tool_grammar);
3614 // gh#154: ask the predicate, not the enum value. `tier` is a
3615 // request-side source that differs only in REPORTING, and comparing
3616 // against `request` alone would silently stop applying it.
3617 if (is_request_grammar(source)) {
3619 } else if (source == GrammarSource::tool_call) {
3620 // TOOL_CALLS, not USER: common_grammar_needs_prefill() is true for this
3621 // type, so the sampler prefills generation_prompt into the grammar —
3622 // required because the model's output starts mid-template. The USER
3623 // type skips prefill and the grammar would reject from token one.
3625 tool_grammar);
3626 cps.grammar_lazy = tool_grammar_lazy;
3627 // REQUIRED, not optional: common_sampler_init only prefills when
3628 // generation_prompt is non-empty (sampling.cpp:268, :285). Without it
3629 // the grammar sampler never accepts the tokens the chat template
3630 // already placed in the prompt, so it starts mid-rule and rejects the
3631 // very first sampled token — surfacing as GENERATE_FAILED with empty
3632 // content rather than as a grammar error. Measured exactly that on
3633 // gemma4 before this line existed.
3634 cps.generation_prompt = generation_prompt;
3635 // gh#138: the ONLY observable that the tool-call grammar is in force.
3636 // The orchestration line reports params.grammar, which is the REQUEST
3637 // grammar — it prints "unconstrained" even when this grammar is
3638 // active, so a consumer reading logs cannot tell the difference
3639 // between "tool grammar applied" and "nothing constrained the
3640 // decode". INFO, not DEBUG: this is the diagnostic that would have
3641 // answered gh#138 from the reporter's own logs.
3642 logger->info(
3643 "Tool-call grammar applied: {} bytes, lazy={}, prefill={} bytes",
3645 generation_prompt.size());
3646 } else if (!tool_grammar.empty()) {
3647 // Tools were staged and a grammar derived, but the request grammar
3648 // won the precedence rule — say so rather than letting the tool
3649 // grammar vanish silently.
3650 logger->warn(
3651 "Tool-call grammar ({} bytes) NOT applied — an explicit request "
3652 "grammar takes precedence. Tool calls are not structurally "
3653 "enforced this turn.",
3654 tool_grammar.size());
3655 }
3656}
3657
3685 const GenerationParams& params,
3686 const std::string& tool_grammar,
3687 bool tool_grammar_lazy,
3688 const std::string& generation_prompt) {
3690 cps.temp = params.temperature;
3693 cps.top_k = params.top_k;
3694 cps.top_p = params.top_p;
3695 cps.penalty_repeat = params.repeat_penalty;
3696 // gh#23 MVP items 2 + 3 (v2.3.14 + v2.3.15): wire presence +
3697 // frequency penalty into common-sampling. Counterparts of the
3698 // 3rd + 4th args to `llama_sampler_init_penalties` in the plain
3699 // decode path. Default 0.0f on both preserves bit-for-bit
3700 // speculative output.
3701 cps.penalty_freq = params.frequency_penalty;
3702 cps.penalty_present = params.presence_penalty;
3703 // gh#23 MVP item 4 (v2.3.16): forward logit_bias to common-sampling.
3704 // Empty (default) leaves the speculative chain bit-for-bit
3705 // identical to pre-v2.3.16.
3706 for (auto& [tok, val] : params.logit_bias) {
3707 cps.logit_bias.push_back({tok, val});
3708 }
3709 if (params.seed >= 0) {
3710 cps.seed = static_cast<uint32_t>(params.seed);
3711 }
3712 cps.no_perf = true;
3713 // Mirror entropic's standard sampler chain ordering so the
3714 // speculative path produces output bit-identical to plain decode
3715 // (the v2.1.11 correctness contract). Entropic's `create_sampler`
3716 // builds: penalties → top_k → top_p → min_p → temperature → dist,
3717 // AND SKIPS the temperature sampler when temp == 0 (greedy mode).
3718 // common_sampler appends an extended-temperature sampler that
3719 // differs subtly from "no temp at all" — we omit it for temp=0
3720 // to match entropic exactly. min_p (v2.3.10, gh#23) appended only
3721 // when caller opted in (params.min_p > 0); 0.0 preserves the
3722 // pre-v2.3.10 chain shape bit-for-bit. Other extended filters
3723 // (top_n_sigma, dry, xtc, typical_p) remain stripped.
3727 if (params.min_p > 0.0f) {
3728 cps.samplers.push_back(COMMON_SAMPLER_TYPE_MIN_P);
3729 }
3730 if (params.temperature > 0.0f) {
3731 cps.samplers.push_back(COMMON_SAMPLER_TYPE_TEMPERATURE);
3732 }
3733 cps.min_p = params.min_p;
3734 cps.dry_multiplier = 0.0f;
3735 cps.top_n_sigma = -1.0f;
3736 return cps;
3737}
3738
3756 llama_context* ctx, const std::vector<llama_token>& tokens) {
3757 int total = static_cast<int>(tokens.size()) - 1;
3758 if (total <= 0) { return true; }
3759 int n_batch = llama_n_batch(ctx);
3760 for (int off = 0; off < total; off += n_batch) {
3761 int chunk = std::min(n_batch, total - off);
3763 const_cast<llama_token*>(tokens.data()) + off, chunk);
3764 if (llama_decode(ctx, batch) != 0) { return false; }
3765 }
3766 return true;
3767}
3768
3785GenerationResult spec_error(entropic_error_t code, std::string msg) {
3786 logger->error("Speculative decode failed ({}): {}",
3788 GenerationResult r;
3789 r.error_code = code;
3790 r.error_message = std::move(msg);
3791 r.finish_reason = "error";
3792 return r;
3793}
3794
3795} // anonymous namespace
3796
3805 common_speculative* spec = nullptr;
3806 common_sampler* smpl = nullptr;
3807 llama_context* ctx_tgt = nullptr;
3808 llama_context* ctx_dft = nullptr;
3809 llama_batch batch_tgt{};
3810 bool batch_initialized = false;
3811 llama_seq_id seq_id = 0;
3812 int n_past = 0;
3813 llama_token id_last = 0;
3814 std::vector<llama_token> prompt_tgt;
3815 std::vector<llama_token> draft;
3816 std::string generated;
3817 std::vector<std::string> stop;
3818 int n_generated = 0;
3825 int n_drafted = 0;
3826 int n_accepted = 0;
3827 bool has_eos = false;
3828 std::string finish_reason;
3829 entropic_error_t error_code = ENTROPIC_OK;
3830 std::string error_message;
3831
3832 // ── Checkpoint state (v2.1.11) ──────────────────────────
3833 // Activated when either context reports FULL-only seq_rm
3834 // (no partial removal). The kernel saves+restores draft/target
3835 // state across each speculative round so the underlying
3836 // memory module never sees an attempted partial removal.
3837 // Mirrors the use_ckpt_tgt / use_ckpt_dft flow in upstream's
3838 // speculative-simple example.
3839 bool use_ckpt_tgt = false;
3840 bool use_ckpt_dft = false;
3842};
3843
3851 if (state.spec) { common_speculative_free(state.spec); }
3852 if (state.smpl) { common_sampler_free(state.smpl); }
3853 if (state.batch_initialized) {
3854 llama_batch_free(state.batch_tgt);
3855 }
3856}
3857
3865 common_batch_clear(state.batch_tgt);
3866 common_batch_add(state.batch_tgt, state.id_last,
3867 state.n_past, {state.seq_id}, true);
3868 int pos = state.n_past + 1;
3869 for (auto draft_token : state.draft) {
3870 common_batch_add(state.batch_tgt, draft_token, pos,
3871 {state.seq_id}, true);
3872 ++pos;
3873 }
3874}
3875
3884 spec_build_batch(state);
3885 int rc_tgt = llama_decode(state.ctx_tgt, state.batch_tgt);
3886 if (rc_tgt != 0) {
3887 logger->error("Speculative target decode failed: rc={}, "
3888 "n_past={}, draft_size={}",
3889 rc_tgt, state.n_past, state.draft.size());
3890 state.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
3891 state.error_message = "target llama_decode failed";
3892 state.finish_reason = "error";
3893 return false;
3894 }
3895 int rc_dft = llama_decode(state.ctx_dft, state.batch_tgt);
3896 if (rc_dft != 0) {
3897 logger->error("Speculative draft decode failed: rc={}, "
3898 "n_past={}, draft_size={}",
3899 rc_dft, state.n_past, state.draft.size());
3900 state.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
3901 state.error_message = "draft llama_decode failed";
3902 state.finish_reason = "error";
3903 return false;
3904 }
3905 return true;
3906}
3907
3916 state.spec, state.seq_id);
3917 dp.drafting = true;
3918 dp.n_max = -1;
3919 dp.pos0 = state.n_past; // b11009 renamed n_past -> pos0 (same meaning)
3920 dp.id_last = state.id_last;
3921 dp.prompt = &state.prompt_tgt;
3922 dp.result = &state.draft;
3923 common_speculative_draft(state.spec);
3924 return static_cast<int>(state.draft.size());
3925}
3926
3940static std::string spec_emit_token(
3942 const llama_vocab* vocab, int max_tokens,
3943 std::function<void(std::string_view)>& on_token,
3944 std::atomic<bool>& cancel)
3945{
3946 std::string signal;
3947 state.prompt_tgt.push_back(state.id_last);
3948 state.id_last = id;
3949 state.n_generated++;
3950 if (llama_vocab_is_eog(vocab, id)) {
3951 state.has_eos = true;
3952 state.finish_reason = "stop";
3953 signal = "eos";
3954 } else {
3955 const std::string piece =
3956 common_token_to_piece(state.ctx_tgt, id);
3957 state.generated += piece;
3958 if (on_token) { on_token(piece); }
3959 // gh#108: honor stop sequences (params.stop + gh#103 sequential-tool
3960 // close marker) so MTP stops where plain decode would, instead of
3961 // over-generating past the first tool call. state.stop is empty for the
3962 // gh#36 path, so this is a no-op there.
3963 if (check_stop_sequences(state.generated, state.stop)) {
3964 state.finish_reason = "stop";
3965 signal = "stop";
3966 } else if (cancel.load(std::memory_order_acquire)) {
3967 state.error_code = ENTROPIC_ERROR_CANCELLED;
3968 state.finish_reason = "cancelled";
3969 signal = "cancel";
3970 } else if (state.n_generated >= max_tokens) {
3971 state.finish_reason = "length";
3972 signal = "length";
3973 }
3974 }
3975 return signal;
3976}
3977
3990 state.ckpt.update_pos(
3991 static_cast<int64_t>(state.prompt_tgt.size()),
3993 llama_get_memory(state.ctx_tgt), state.seq_id),
3995 llama_get_memory(state.ctx_tgt), state.seq_id));
3996 if (state.use_ckpt_dft) {
3997 state.ckpt.update_dft(state.ctx_dft, state.seq_id,
4000 }
4001}
4002
4010 if (state.use_ckpt_tgt && !state.draft.empty()) {
4011 state.ckpt.update_tgt(state.ctx_tgt, state.seq_id,
4014 }
4015}
4016
4026 if (state.use_ckpt_dft) {
4027 state.ckpt.load_dft(state.ctx_dft, state.seq_id, flags);
4028 }
4030 state.seq_id, state.ckpt.pos_max + 1, -1);
4031}
4032
4044 std::vector<llama_token>& ids) {
4047 state.draft = std::move(ids);
4048 state.ckpt.load_tgt(state.ctx_tgt, state.seq_id, flags);
4050 state.seq_id, state.ckpt.pos_max + 1, -1);
4051 state.ckpt.load_dft(state.ctx_dft, state.seq_id, flags);
4053 state.seq_id, state.ckpt.pos_max + 1, -1);
4054 state.prompt_tgt.resize(static_cast<size_t>(state.ckpt.n_tokens));
4055 state.n_past = static_cast<int>(state.prompt_tgt.size());
4056 // Sampler clone is non-null only when use_ckpt_tgt is set
4057 common_sampler_free(state.smpl);
4058 state.smpl = smpl_save;
4059}
4060
4087 state.seq_id, state.n_past, -1);
4089 state.seq_id, state.n_past, -1);
4090}
4091
4099 SpeculativeRunState& state,
4100 const std::vector<llama_token>& ids,
4101 const llama_vocab* vocab, int max_tokens,
4102 std::function<void(std::string_view)>& on_token,
4103 std::atomic<bool>& cancel) {
4104 bool stop = false;
4105 for (auto id : ids) {
4106 auto signal = spec_emit_token(
4107 state, id, vocab, max_tokens, on_token, cancel);
4108 if (!signal.empty()) { stop = true; break; }
4109 }
4110 return stop;
4111}
4112
4134 // Skip drafting if the previous round restored a partial accept
4135 // into state.draft (carry-over from rollback).
4136 if (!state.draft.empty()) {
4137 return static_cast<int>(state.draft.size());
4138 }
4139 spec_ckpt_save_dft(state);
4140 int drafted = spec_run_draft(state);
4141 spec_ckpt_save_tgt(state);
4142 spec_ckpt_restore_dft(state);
4143 return drafted;
4144}
4145
4152 SpeculativeRunState& state,
4153 const llama_vocab* vocab,
4154 int max_tokens,
4155 std::function<void(std::string_view)>& on_token,
4156 std::atomic<bool>& cancel)
4157{
4159
4160 if (!spec_decode_both(state)) { return false; }
4161
4162 common_sampler* smpl_save = nullptr;
4163 if (state.use_ckpt_tgt) {
4164 smpl_save = common_sampler_clone(state.smpl);
4165 }
4167 state.smpl, state.ctx_tgt, state.draft);
4168 int accepted = static_cast<int>(ids.size()) - 1;
4169 if (accepted < 0) { accepted = 0; }
4170
4171 // Partial acceptance on a FULL-seq_rm context: rollback to
4172 // checkpoint, set draft = accepted, re-loop without emitting.
4173 if (state.use_ckpt_tgt
4174 && static_cast<int>(ids.size()) - 1
4175 < static_cast<int>(state.draft.size())) {
4177 return true;
4178 }
4180
4181 common_speculative_accept(state.spec, state.seq_id, accepted);
4182 state.n_drafted += draft_size_before;
4183 state.n_accepted += accepted;
4184 // n_past advances by ids.size() total: one slot for id_last
4185 // (the post-id_last position the next id will occupy), plus
4186 // `accepted` slots for the drafted tokens the sampler agreed
4187 // with. Matches speculative-simple's n_past++ in batch_add +
4188 // n_past += ids.size() - 1 sequence.
4189 state.n_past += static_cast<int>(ids.size());
4190
4191 bool stop = spec_commit_accepted(
4192 state, ids, vocab, max_tokens, on_token, cancel);
4193 state.draft.clear();
4195 return !stop;
4196}
4197
4210static std::string spec_check_preconditions(
4211 bool target_active, bool draft_active,
4212 llama_context* ctx_tgt, llama_context* ctx_dft) {
4213 // Defense-in-depth arch gate — orchestrator's
4214 // check_speculative_compat is the primary gate; a direct caller
4215 // into the kernel must also be refused on recurrent / hybrid
4216 // targets (Session 5 Gate A: hybrid SSM state diverges across
4217 // split-prefill boundaries; bit-identical unreachable at this pin).
4218 std::string err;
4219 const llama_model* model_tgt = llama_get_model(ctx_tgt);
4220 int cap_tgt = common_context_can_seq_rm(ctx_tgt);
4221 int cap_dft = common_context_can_seq_rm(ctx_dft);
4222 logger->info("Speculative seq_rm capability: target={}, draft={} "
4223 "(0=NO, 1=PART, 2=FULL)", cap_tgt, cap_dft);
4224 if (!target_active || !draft_active) {
4225 err = "speculative requires ACTIVE target + draft";
4228 err = "speculative refused: architecture (target is "
4229 "recurrent or hybrid; see proposal Implementation "
4230 "Log Gate A)";
4233 // NO is the only unsupported seq_rm case — the kernel has
4234 // both PART fast-path and FULL checkpoint paths.
4235 err = "speculative kernel requires at least FULL seq_rm "
4236 "(target/draft reported NO seq_rm at all)";
4237 }
4238 return err;
4239}
4240
4272 const std::string& draft_path,
4273 const std::string& tool_grammar, // gh#134 (v2.10.4)
4274 bool tool_grammar_lazy,
4275 const std::string& generation_prompt) {
4276 auto common_sampling =
4280 if (!state.smpl) { return "common_sampler_init failed"; }
4281
4284 spec_params.draft.n_max = (n_draft_max > 0) ? n_draft_max : 16;
4285 spec_params.draft.ctx_tgt = state.ctx_tgt;
4286 spec_params.draft.ctx_dft = state.ctx_dft;
4287 // Upstream gates DRAFT_SIMPLE on a non-empty draft path
4288 // (see common/speculative.cpp:875). Required even though we
4289 // provide already-loaded contexts.
4290 spec_params.draft.mparams.path = draft_path;
4291 state.spec = common_speculative_init(spec_params, 1);
4292 if (!state.spec) {
4293 common_sampler_free(state.smpl);
4294 state.smpl = nullptr;
4295 return "common_speculative_init failed";
4296 }
4297
4298 common_speculative_begin(state.spec, state.seq_id, state.prompt_tgt);
4299 state.batch_tgt = llama_batch_init(llama_n_batch(state.ctx_tgt), 0, 1);
4300 state.batch_initialized = true;
4301 // Checkpoint flow lights up when either context can only do
4302 // FULL-sequence removal. Mirrors speculative-simple's
4303 // use_ckpt_{tgt,dft}.
4304 state.use_ckpt_tgt = common_context_can_seq_rm(state.ctx_tgt)
4306 state.use_ckpt_dft = common_context_can_seq_rm(state.ctx_dft)
4308 return "";
4309}
4310
4316static std::string spec_init_run(
4318 const std::vector<llama_token>& tokens,
4320 const std::string& draft_path,
4321 const std::string& tool_grammar, bool tool_grammar_lazy,
4322 const std::string& generation_prompt) {
4323 state.id_last = tokens.back();
4324 state.prompt_tgt.assign(tokens.begin(), tokens.end() - 1);
4325 state.n_past = static_cast<int>(tokens.size()) - 1;
4326
4327 llama_memory_clear(llama_get_memory(state.ctx_tgt), true);
4328 llama_memory_clear(llama_get_memory(state.ctx_dft), true);
4329
4330 if (!spec_prefill_minus_last(state.ctx_tgt, tokens)
4331 || !spec_prefill_minus_last(state.ctx_dft, tokens)) {
4332 return "speculative prefill failed";
4333 }
4337}
4338
4344static void spec_run_loop(
4345 SpeculativeRunState& state, const llama_vocab* vocab,
4346 int max_tokens,
4347 std::function<void(std::string_view)>& on_token,
4348 std::atomic<bool>& cancel) {
4349 while (state.n_generated < max_tokens) {
4350 if (cancel.load(std::memory_order_acquire)) {
4351 state.error_code = ENTROPIC_ERROR_CANCELLED;
4352 state.finish_reason = "cancelled";
4353 break;
4354 }
4355 if (!spec_accept_round(state, vocab, max_tokens,
4356 on_token, cancel)) {
4357 break;
4358 }
4359 }
4360 if (state.finish_reason.empty()) {
4361 state.finish_reason = (state.n_generated >= max_tokens)
4362 ? "length" : "stop";
4363 }
4364}
4365
4378 SpeculativeRunState& state,
4379 std::chrono::steady_clock::time_point t0) {
4380 GenerationResult result;
4381 // gh#136 (4th type_error.316 recurrence): sanitize at INGRESS, where model
4382 // bytes first become a std::string, rather than at each of the ~18 .dump()
4383 // sites downstream. gh#112/113 were closed as "permanent closure of the
4384 // 316 family", then gh#114, gh#118, gh#132 and gh#136 each patched one more
4385 // egress. The exits keep multiplying; the entries do not. Guarding here
4386 // makes every downstream dump safe by construction, and a NEW .dump()
4387 // anywhere cannot reintroduce the bug.
4388 //
4389 // Once at finalization, never per-token: a multi-byte codepoint can split
4390 // across token boundaries and per-token sanitize would corrupt valid
4391 // output (see the same reasoning at response_generator.cpp:360, which has
4392 // guarded the streaming accumulator this way since v2.1.1).
4393 //
4394 // Safe for raw_content consumers: raw_content is a COPY of content
4395 // (orchestrator.cpp:514), and sanitize only replaces bytes that are
4396 // ALREADY invalid UTF-8 — which cannot form part of any valid JSON token,
4397 // so the gh#88 envelope recovery and fenced-JSON fallbacks are unaffected.
4398 result.content = entropic::mcp::sanitize_utf8(state.generated);
4399 result.token_count = state.n_generated;
4400 // gh#144 (v2.12.0): carry the prefill count out of the speculative path,
4401 // which previously reported nothing and left last_prefill_tokens() at 0
4402 // for every MTP turn.
4403 result.prefill_tokens = state.n_prefilled;
4404 result.finish_reason = state.finish_reason;
4405 result.error_code = state.error_code;
4406 result.error_message = state.error_message;
4407 // gh#106: surface the draft/accept counts so callers (and the MTP
4408 // engagement test) can verify the kernel actually ran + accepted.
4409 result.n_drafted = state.n_drafted;
4410 result.n_accepted = state.n_accepted;
4411 result.generation_time_ms =
4412 entropic::log::elapsed_ms(t0, entropic::log::now());
4413 // gh#108: the speculative path previously left throughput_tok_s=0.0 — the
4414 // one metric the feature exists for. Compute it like finalize_result.
4415 if (result.token_count > 0 && result.generation_time_ms > 0.0) {
4416 result.throughput_tok_s =
4417 static_cast<double>(result.token_count)
4418 / result.generation_time_ms * 1000.0;
4419 }
4420 // gh#151: the speculative path computed throughput_tok_s (gh#108) and
4421 // then never logged it, emitting only the "Speculative:" line below.
4422 // So an MTP-vs-plain A/B measured from logs could read tok/s on one arm
4423 // and not the other — the one comparison the feature exists to be judged
4424 // by. A consumer ESTIMATED the MTP arm's output volume from
4425 // drafted/accepted arithmetic and had to publish an upper bound, when
4426 // state.n_generated was the true figure all along.
4427 logger->info("{}", entropic::format_generation_summary(
4428 {result.token_count, result.finish_reason,
4429 result.generation_time_ms, result.throughput_tok_s,
4430 state.n_drafted, state.n_accepted}));
4431 // The original line is KEPT, not replaced: it is an existing log
4432 // contract and at least one consumer parses these logs. It carries the
4433 // same numbers, so this costs one line and breaks nobody.
4434 if (state.n_drafted > 0) {
4435 const float accept_rate =
4436 static_cast<float>(state.n_accepted)
4437 / static_cast<float>(state.n_drafted);
4438 logger->info("Speculative: generated={}, drafted={}, "
4439 "accepted={}, accept_rate={:.3f}",
4440 state.n_generated, state.n_drafted,
4441 state.n_accepted, accept_rate);
4442 }
4443 spec_cleanup(state);
4444 return result;
4445}
4446
4488 llama_context* ctx_tgt, llama_context* ctx_dft, llama_model* model_tgt,
4489 const std::vector<llama_token>& tokens, const GenerationParams& params,
4490 std::function<void(std::string_view)>& on_token,
4491 std::atomic<bool>& cancel, int n_draft_max,
4492 const std::string& draft_path,
4493 std::chrono::steady_clock::time_point t0,
4494 const std::string& tool_grammar, bool tool_grammar_lazy,
4495 const std::string& generation_prompt) {
4496 SpeculativeRunState state;
4497 state.ctx_tgt = ctx_tgt;
4498 state.ctx_dft = ctx_dft;
4503 if (!init_err.empty()) {
4504 spec_cleanup(state);
4506 std::move(init_err));
4507 }
4509 params.max_tokens, on_token, cancel);
4510 return spec_finalize(state, t0);
4511}
4512
4519 const std::vector<Message>& messages,
4520 const GenerationParams& params,
4521 std::function<void(std::string_view)> on_token,
4522 std::atomic<bool>& cancel,
4523 LlamaCppBackend& draft,
4524 int n_draft_max,
4525 const std::string& draft_path)
4526{
4527 auto t0 = entropic::log::now();
4528 invalidate_all_resident_kv(); // gh#96: speculative path manages seq 0 itself
4530 is_active(), draft.is_active(), ctx_, draft.ctx_);
4531 GenerationResult result;
4532 if (!pre_err.empty()) {
4534 (pre_err.find("requires ACTIVE") != std::string::npos)
4537 result = spec_error(code, std::move(pre_err));
4538 } else {
4539 auto prompt = render_prompt(messages, params);
4540 auto tokens = tokenize(prompt, true);
4541 if (tokens.size() < 2) {
4543 "speculative prompt must have at least 2 tokens");
4544 } else {
4545 logger->info("Speculative: {} input tokens, max_tokens={}, "
4546 "n_draft_max={}",
4547 tokens.size(), params.max_tokens, n_draft_max);
4548 result = spec_run_from_tokens(
4549 ctx_, draft.ctx_, model_, tokens, params, on_token,
4552 }
4553 }
4554 return result;
4555}
4556
4557// ── gh#106 (v2.9.0): target-owned MTP speculative kernel ───────────
4558//
4559// Distinct from the gh#36 separate-draft kernel above. The MTP head
4560// (ctx_dft) shares the target's KV via ctx_other, so the CALLER only
4561// ever decodes ctx_tgt; the impl owns every ctx_dft decode. The loop is
4562// draft → decode(ctx_tgt) → process → sample_and_accept_n → accept,
4563// mirroring extern/llama.cpp/tools/server/server-context.cpp. Reuses the
4564// gh#36 file-local helpers (spec_build_batch / spec_emit_token /
4565// spec_commit_accepted / spec_trim_rejected_drafts / spec_finalize /
4566// spec_cleanup / spec_error / to_common_sampling) — only the decode step
4567// and the prefill differ. NO checkpoint dance: shared-KV gemma4 targets
4568// are PART-seq_rm, so the FULL-only rollback path never applies.
4569
4570namespace {
4571
4582int mtp_run_draft(SpeculativeRunState& state, int n_max) {
4583 auto& dp = common_speculative_get_draft_params(state.spec, state.seq_id);
4584 dp.drafting = true;
4585 dp.n_max = n_max;
4586 dp.pos0 = state.n_past; // b11009 renamed n_past -> pos0 (same meaning)
4587 dp.id_last = state.id_last;
4588 dp.prompt = &state.prompt_tgt;
4589 dp.result = &state.draft;
4590 common_speculative_draft(state.spec);
4591 return static_cast<int>(state.draft.size());
4592}
4593
4605bool mtp_decode_and_process(SpeculativeRunState& state) {
4606 spec_build_batch(state); // [id_last@n_past, draft@n_past+1 ...]
4607 if (llama_decode(state.ctx_tgt, state.batch_tgt) != 0) {
4608 state.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
4609 state.error_message = "MTP target decode failed";
4610 state.finish_reason = "error";
4611 return false;
4612 }
4613 if (!common_speculative_process(state.spec, state.batch_tgt)) {
4614 state.error_code = ENTROPIC_ERROR_GENERATE_FAILED;
4615 state.error_message = "common_speculative_process failed";
4616 state.finish_reason = "error";
4617 return false;
4618 }
4619 return true;
4620}
4621
4628bool mtp_accept_round(
4629 SpeculativeRunState& state, int n_max, const llama_vocab* vocab,
4630 int max_tokens, std::function<void(std::string_view)>& on_token,
4631 std::atomic<bool>& cancel) {
4632 int drafted = mtp_run_draft(state, n_max);
4633 if (!mtp_decode_and_process(state)) { return false; }
4635 state.smpl, state.ctx_tgt, state.draft);
4636 int accepted = static_cast<int>(ids.size()) - 1;
4637 if (accepted < 0) { accepted = 0; }
4638 // gh#108: only accept into the spec when this round actually drafted.
4639 // common_speculative_accept asserts impl_last[seq] (speculative.cpp:1650),
4640 // which is set ONLY for a non-empty draft (1604/1614) — a zero-draft round
4641 // would abort. The round still progresses by one token (the bonus in ids),
4642 // and process() already updated pending_h, so skipping accept is equivalent.
4643 if (drafted > 0) {
4644 common_speculative_accept(state.spec, state.seq_id, accepted);
4645 }
4646 state.n_drafted += drafted;
4647 state.n_accepted += accepted;
4648 // Same layout as gh#36: id_last fills one slot at n_past, the
4649 // `accepted` drafts fill the next slots — n_past advances by ids.size().
4650 state.n_past += static_cast<int>(ids.size());
4651 bool stop = spec_commit_accepted(
4652 state, ids, vocab, max_tokens, on_token, cancel);
4653 state.draft.clear();
4655 return !stop;
4656}
4657
4666bool mtp_process_chunk(SpeculativeRunState& state, int off, int chunk) {
4667 common_batch_clear(state.batch_tgt);
4668 for (int j = 0; j < chunk; ++j) {
4669 common_batch_add(state.batch_tgt, state.prompt_tgt[off + j],
4670 off + j, {state.seq_id}, false);
4671 }
4672 if (llama_decode(state.ctx_tgt, state.batch_tgt) != 0) { return false; }
4673 return common_speculative_process(state.spec, state.batch_tgt);
4674}
4675
4683bool mtp_prefill_and_seed(SpeculativeRunState& state, int start = 0) {
4684 int total = static_cast<int>(state.prompt_tgt.size());
4685 if (total == 0) { return true; } // 1-token prompt: round 1 drafts cold
4686 // gh#144 (v2.12.0): decode only from `start`. Tokens below it are already
4687 // resident and provably unchanged — the caller matched them against the
4688 // KV's own occupancy before choosing the cut.
4689 int n_batch = llama_n_batch(state.ctx_tgt);
4690 for (int off = start; off < total; off += n_batch) {
4691 int chunk = std::min(n_batch, total - off);
4692 if (!mtp_process_chunk(state, off, chunk)) { return false; }
4693 // gh#144 (v2.12.0): count what this path actually decodes, so
4694 // last_prefill_tokens() is meaningful under speculative.mtp.
4695 state.n_prefilled += chunk;
4696 }
4697 return true;
4698}
4699
4709std::string mtp_init_decoder(
4710 SpeculativeRunState& state, llama_model* model_tgt,
4711 const GenerationParams& params, int n_max,
4712 const std::string& tool_grammar, // gh#134 (v2.10.4)
4713 bool tool_grammar_lazy,
4714 const std::string& generation_prompt) {
4715 auto common_sampling =
4719 if (!state.smpl) { return "common_sampler_init failed"; }
4722 sp.draft.n_max = n_max;
4723 sp.draft.ctx_tgt = state.ctx_tgt;
4724 sp.draft.ctx_dft = state.ctx_dft;
4725 state.spec = common_speculative_init(sp, 1);
4726 if (!state.spec) {
4727 common_sampler_free(state.smpl);
4728 state.smpl = nullptr;
4729 return "common_speculative_init (MTP) failed";
4730 }
4731 state.batch_tgt = llama_batch_init(llama_n_batch(state.ctx_tgt), 0, 1);
4732 state.batch_initialized = true;
4733 return "";
4734}
4735
4745std::string mtp_init_run(
4746 SpeculativeRunState& state, llama_model* model_tgt,
4747 const std::vector<llama_token>& tokens,
4748 const GenerationParams& params, int n_max,
4749 const std::string& tool_grammar, bool tool_grammar_lazy,
4750 const std::string& generation_prompt, int reuse_cut) {
4751 state.id_last = tokens.back();
4752 state.prompt_tgt.assign(tokens.begin(), tokens.end() - 1);
4753 state.n_past = static_cast<int>(tokens.size()) - 1;
4754
4755 // gh#144 (v2.12.0): retain a prefix this run can prove unchanged.
4756 //
4757 // This used to be an unconditional llama_memory_clear(ctx_tgt, true)
4758 // followed by a full re-prefill, on EVERY generation. Nothing about
4759 // speculative decoding requires discarding the cache — it was an
4760 // implementation choice in this path, and it cost the whole prompt every
4761 // turn: a consumer measured 18611 tokens prefilled against 196 generated
4762 // (95:1) on a three-turn review, with ~85% of that being an invariant
4763 // prefix of constitution, identity and a 16 KB staged tool-schema block.
4764 //
4765 // `reuse_cut` is 0 when nothing is reusable, which reproduces the old
4766 // behaviour exactly; the caller clears the sequence in that case.
4767 auto* mem = llama_get_memory(state.ctx_tgt);
4768 if (reuse_cut > 0) {
4769 // Drop only the divergent tail, then decode the delta.
4770 llama_memory_seq_rm(mem, state.seq_id,
4771 static_cast<llama_pos>(reuse_cut), -1);
4772 } else {
4773 llama_memory_clear(mem, true);
4774 }
4775 auto err = mtp_init_decoder(state, model_tgt, params, n_max,
4778 if (!err.empty()) { return err; }
4779 if (!mtp_prefill_and_seed(state, reuse_cut)) {
4780 return "MTP prefill/process failed";
4781 }
4782 common_speculative_begin(state.spec, state.seq_id, state.prompt_tgt);
4783 return "";
4784}
4785
4791void mtp_run_loop(
4792 SpeculativeRunState& state, int n_max, const llama_vocab* vocab,
4793 int max_tokens, std::function<void(std::string_view)>& on_token,
4794 std::atomic<bool>& cancel) {
4795 while (state.n_generated < max_tokens) {
4796 if (cancel.load(std::memory_order_acquire)) {
4797 state.error_code = ENTROPIC_ERROR_CANCELLED;
4798 state.finish_reason = "cancelled";
4799 break;
4800 }
4801 if (!mtp_accept_round(state, n_max, vocab, max_tokens,
4802 on_token, cancel)) {
4803 break;
4804 }
4805 }
4806 if (state.finish_reason.empty()) {
4807 state.finish_reason = (state.n_generated >= max_tokens)
4808 ? "length" : "stop";
4809 }
4810}
4811
4817GenerationResult mtp_run_from_tokens(
4818 llama_context* ctx_tgt, llama_context* ctx_dft, llama_model* model_tgt,
4819 const std::vector<llama_token>& tokens, const GenerationParams& params,
4820 std::function<void(std::string_view)>& on_token,
4821 std::atomic<bool>& cancel, int n_max,
4822 const std::vector<std::string>& stop,
4823 std::chrono::steady_clock::time_point t0,
4824 const std::string& tool_grammar, bool tool_grammar_lazy,
4825 const std::string& generation_prompt, int reuse_cut) {
4826 SpeculativeRunState state;
4827 state.ctx_tgt = ctx_tgt;
4828 state.ctx_dft = ctx_dft;
4829 state.stop = stop; // gh#108: MTP honors stop sequences (effective_stop)
4833 if (!init_err.empty()) {
4834 spec_cleanup(state);
4836 std::move(init_err));
4837 }
4839 params.max_tokens, on_token, cancel);
4840 return spec_finalize(state, t0);
4841}
4842
4843} // anonymous namespace
4844
4873 const GenerationParams& params,
4874 const std::function<void(std::string_view)>& on_token,
4875 const std::string& head_path, int n_max) {
4876 GenerationResult r; // ENTROPIC_OK by default → proceed
4877 std::string reason = mtp_unsupported_reason(
4878 params.temperature, !params.grammar.empty(),
4879 static_cast<bool>(on_token));
4880 if (!is_active()) {
4882 "MTP requires an ACTIVE target");
4883 } else if (!reason.empty()) {
4885 } else if (1 + effective_n_draft(n_max) > llama_n_batch(ctx_)) {
4886 // Bound-check BEFORE setup_mtp_draft allocates. Ordering this the
4887 // other way made the guard's ANSWER depend on process history: an
4888 // out-of-envelope window (e.g. n_draft=100000) is still handed to
4889 // setup_mtp_draft, whose allocation SUCCEEDS in a fresh process —
4890 // so the guard fell through and returned the correct
4891 // SPECULATIVE_INCOMPATIBLE_CONFIG — but FAILS after a prior MTP
4892 // session, returning LOAD_FAILED for the same config. Same input,
4893 // two different typed errors, decided by allocator state. Measured
4894 // as `5 == 54` when a second MTP case ran in one process.
4895 //
4896 // Must use the same normalisation setup_mtp_draft applies, or the
4897 // check disagrees with the value actually used.
4899 "speculative.n_draft+1 ("
4900 + std::to_string(1 + effective_n_draft(n_max))
4901 + ") exceeds n_batch (" + std::to_string(llama_n_batch(ctx_))
4902 + "); reduce n_draft or raise n_batch");
4903 } else if (!setup_mtp_draft(head_path, n_max)) {
4905 }
4906 return r;
4907}
4908
4928 const std::vector<llama_token>& tokens) const {
4929 if (!prompt_cache_config_.warm_keep || ctx_ == nullptr) {
4930 return 0;
4931 }
4932 const auto seq = static_cast<llama_seq_id>(active_slot_);
4933 const long pos_max = static_cast<long>(
4935 const std::size_t cut = warm_keep_cut(
4937 return static_cast<int>(
4938 std::min(cut, tokens.empty() ? std::size_t{0} : tokens.size() - 1));
4939}
4940
4962 const std::vector<Message>& messages,
4963 const GenerationParams& params,
4964 std::function<void(std::string_view)> on_token,
4965 std::atomic<bool>& cancel,
4966 const std::string& head_path,
4967 int n_max)
4968{
4969 auto t0 = entropic::log::now();
4970 std::lock_guard<std::mutex> lk(mtp_mutex_); // serialise vs teardown
4971 GenerationResult result = mtp_guard(params, on_token, head_path, n_max);
4972 if (result.error_code != ENTROPIC_OK) {
4973 return result;
4974 }
4976 auto tokens = tokenize(render_prompt(messages, params), true);
4977 if (tokens.size() < 2) {
4979 "MTP prompt must have at least 2 tokens");
4980 }
4981
4982 const int reuse_cut = mtp_reuse_cut(tokens);
4983 if (reuse_cut == 0) {
4984 invalidate_all_resident_kv(); // cold run: nothing survives the clear
4985 }
4986 logger->info("MTP: {} input tokens, reusing {}, max_tokens={}, n_max={}",
4987 tokens.size(), reuse_cut, params.max_tokens, mtp_n_max_);
4990 on_token, cancel, mtp_n_max_,
4991 effective_stop(params), t0, // gh#108: honor stops
4994 // gh#144 (v2.12.0): publish it so last_prefill_tokens() is meaningful
4995 // under speculative.mtp, exactly as it is on the plain decode path.
4996 last_prefill_tokens_ = mtp_result.prefill_tokens;
4997 if (mtp_result.error_code == ENTROPIC_OK) {
4998 // Record what this slot now holds so the NEXT turn can match against
4999 // it. Only on success: a failed run leaves the KV in a state we did
5000 // not author and must not claim to know.
5002 } else {
5004 }
5005 return mtp_result;
5006}
5007
5017 const std::string& prompt,
5018 const GenerationParams& params)
5019{
5020 auto t0 = entropic::log::now();
5021 invalidate_all_resident_kv(); // gh#96: decode_loop/run_prefill mutate seq 0
5022 auto tokens = tokenize(prompt, false);
5023
5024 logger->info("Complete: {} input tokens, max_tokens={}",
5025 tokens.size(), params.max_tokens);
5027 auto result = decode_loop(tokens, params, nullptr, nullptr);
5028 finalize_result(result, t0);
5029 return result;
5030}
5031
5032// ── Architecture detection (v1.9.13) ───────────────────────
5033
5041 return is_recurrent_;
5042}
5043
5044// ── Capability overrides (v1.9.13) ─────────────────────────
5045
5062 int idx = static_cast<int>(cap);
5063 int count = static_cast<int>(BackendCapability::_COUNT);
5064 if (idx < 0 || idx >= count) {
5065 return false;
5066 }
5067
5068 // Static capabilities: true = always supported. Length must equal
5069 // BackendCapability::_COUNT — trailing entries get appended as new
5070 // capabilities are introduced (gh#53 added AUDIO at index 12).
5071 static constexpr bool always[] = {
5072 false, false, true, true, true, true,
5073 false, true, true, false, false, true,
5074 false, // AUDIO — dynamic only (mtmd_support_audio)
5075 };
5076
5077 // Dynamic capabilities override the static table
5078 bool result = always[idx];
5079 if (!result) {
5080 result = (cap == BackendCapability::KV_CACHE && !is_recurrent())
5083 && !config().mmproj_path.empty())
5085 && mtmd_ctx_ != nullptr
5088 && !is_recurrent());
5089 }
5090 return result;
5091}
5092
5100 return "llama.cpp";
5101}
5102
5111 bi.name = "llama.cpp";
5112#if defined(ENTROPIC_BACKEND_CUDA)
5113 bi.compute_device = "cuda";
5114#elif defined(ENTROPIC_BACKEND_VULKAN)
5115 bi.compute_device = "vulkan";
5116#else
5117 bi.compute_device = "cpu";
5118#endif
5119 bi.model_format = "gguf";
5120
5121 if (state() != ModelState::COLD && model_ != nullptr) {
5122 bi.architecture = is_recurrent() ? "recurrent" : "transformer";
5123 bi.max_context_length =
5125 bi.parameter_count = llama_model_n_params(model_);
5126 bi.vram_bytes = 0;
5127 bi.ram_bytes = llama_model_size(model_);
5128
5129 char desc[256] = {};
5130 llama_model_desc(model_, desc, sizeof(desc));
5131 bi.quantization = desc;
5132 }
5133 return bi;
5134}
5135
5144 if (ctx_ == nullptr) {
5145 return false;
5146 }
5147 auto mem = llama_get_memory(ctx_);
5148 if (seq_id < 0) {
5149 llama_memory_clear(mem, true);
5150 } else {
5151 llama_memory_seq_rm(mem, seq_id, -1, -1);
5152 }
5153 return true;
5154}
5155
5173 int seq_id, std::vector<uint8_t>& buffer) const {
5174 if (ctx_ == nullptr) { return false; }
5176 ctx_, static_cast<llama_seq_id>(seq_id));
5177 if (sz == 0) { return false; }
5178 buffer.resize(sz);
5180 ctx_, buffer.data(), sz,
5181 static_cast<llama_seq_id>(seq_id));
5182 return written == sz;
5183}
5184
5200 int seq_id, const std::vector<uint8_t>& buffer) {
5201 if (ctx_ == nullptr || buffer.empty()) { return false; }
5202 size_t result = llama_state_seq_set_data(
5203 ctx_, buffer.data(), buffer.size(),
5204 static_cast<llama_seq_id>(seq_id));
5205 return result > 0;
5206}
5207
5208} // namespace entropic
ChatAdapter concrete base class.
bool cancelled
The run was interrupted and the group was killed (gh#172)
Definition bash.cpp:101
gh#158: which sequences a same-prefix batch may touch, purely.
gh#98 (v2.8.0) same-prefix batch-generation decision logic.
Owning, null-terminated tensor-buffer override list for expert offload.
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:611
std::string last_error_
Last error message for diagnostics.
Definition backend.h:802
bool is_active() const
True when state is ACTIVE.
Definition backend.h:263
ModelState state() const
Current lifecycle state (lock-free read).
Definition backend.h:255
const ModelConfig & config() const
Stored model config.
Definition backend.h:334
int context_length() const
Model's context window size.
Definition backend.h:296
std::atomic< ModelState > state_
State transition slot accessible to subclasses for test-only injection.
Definition backend.h:843
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.
SessionResidency< llama_token > residency_
gh#144 (v2.12.0): per-sequence warm-keep residency.
int kv_full_clear_count_
gh#158: whole-KV-cache clears since activation.
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).
static std::uint64_t model_file_loads()
Whole-file loads of a tier model in this process (gh#148).
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 refuse_over_context(const std::vector< llama_token > &tokens, const std::string &system_prompt)
Refuse this turn when its prompt cannot fit the tier context.
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).
int active_slot_
Sequence slot the current generation is bound to (gh#144).
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).
bool finish_activation()
Context + sampler + mmproj, shared by both activation paths.
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).
void invalidate_all_resident_kv()
Drop EVERY slot's warm-keep record (gh#144, v2.12.0).
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.
static void reset_model_file_loads()
Reset the whole-file load counter (gh#148, test surface).
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 bind_model_handles()
Bind vocab, tokenizer and architecture flags to model_.
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...
void bind_session_slot(const GenerationParams &params)
Bind this generation to its session's sequence slot (gh#144).
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.
int mtp_reuse_cut(const std::vector< llama_token > &tokens) const
How much of an MTP prompt is already resident (gh#144).
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 expert_offload_admits(const ModelConfig &config)
Refuse a loaded model that cannot honour cpu_moe_layers.
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.
GenerationResult prefill_error() const
Build the error result for a prefill that did not run.
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.
bool require_tool_call_
gh#134 (v2.10.4): when true the render asks llama.cpp for COMMON_CHAT_TOOL_CHOICE_REQUIRED,...
std::string tool_grammar_
Render-derived tool-call GBNF.
std::string last_parser_
Captured serialized PEG arena.
void forget_session_kv(const std::string &session_key) override
Drop one session's resident KV and release its slot (gh#165).
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).
bool tool_grammar_lazy_
Arm on trigger vs bind eagerly.
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.
bool decode_tokens_into_slot(const std::vector< llama_token > &tokens, int start_offset, int slot)
Decode a token run into an explicit sequence slot (gh#144).
PromptCacheConfig prompt_cache_config_
Cache config (v1.8.3)
int parse_chat_format_
Last TOOLED render's format.
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+)
std::string prefill_refusal_
v2.13.0: why the last prefill refused to run ("" = it ran).
~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).
bool do_load_active(const ModelConfig &config) override
COLD → ACTIVE in ONE whole-file read (gh#148).
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.
Per-session llama sequence slots and their resident tokens.
const std::vector< Tok > & resident(int slot) const
Tokens recorded as resident in a slot.
int slots() const
Number of sequence slots in the pool.
void invalidate_all()
Forget every slot's residency.
void invalidate(int slot)
Forget a slot's residency (its KV was cleared or invalidated).
int forget_session(const std::string &key)
Drop one SESSION's residency and release its slot (gh#165).
int acquire(const std::string &key, std::string *evicted)
Assign a slot to this session, evicting the least-recently used one when the pool is full.
void set_resident(int slot, std::vector< Tok > tokens)
Record what a slot now holds after a decode.
v2.13.0 prompt-vs-context admission arithmetic for one turn.
int * cancel
Consumer's cancel flag, nullable.
Definition engine.cpp:4074
ENTROPIC_EXPORT const char * entropic_error_name(entropic_error_t code)
Get the human-readable name for an error code.
Definition error.cpp:86
entropic_error_t
Error codes returned by all C API functions.
Definition error.h:37
@ ENTROPIC_OK
Success.
Definition error.h:38
@ ENTROPIC_ERROR_CANCELLED
Operation cancelled via cancel token.
Definition error.h:50
@ ENTROPIC_ERROR_IMAGE_LOAD_FAILED
Image file could not be read or decoded (v1.9.11)
Definition error.h:82
@ ENTROPIC_ERROR_EVAL_CONTEXT_FULL
Sequence exceeds model context window (v1.9.10)
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:92
@ ENTROPIC_ERROR_NOT_SUPPORTED
Capability not supported by this backend (v1.9.13)
Definition error.h:86
@ ENTROPIC_ERROR_GENERATE_FAILED
Generation failed (context overflow, model error)
Definition error.h:44
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
Definition error.h:41
@ ENTROPIC_ERROR_LOAD_FAILED
Model load failed (corrupt file, OOM, unsupported format)
Definition error.h:43
EXPERIMENTAL (v2.13.0): the llama.cpp binding for expert offload — an owning, null-terminated llama_m...
std::string err
size_exceeded, when set
One post-generation summary line, formatted identically on every decode path.
Which grammar source constrains a decode (gh#134).
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:200
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:210
Pure envelope check for the MTP speculative path (gh#108).
Activate model on GPU (WARM → ACTIVE).
@ IMAGE
Image content (local path or data URI)
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, const std::string &tool_grammar, bool tool_grammar_lazy, const std::string &generation_prompt)
Public entry point for the speculative-decoding kernel.
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 int effective_n_draft(int n_max)
Draft-window size setup_mtp_draft will actually use (gh#106).
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.
constexpr int kNoSessionSlot
Sentinel for "no slot assigned".
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
@ optional
optional: every escape asks the host's path approver
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...
PoolGeometry derive_pool_geometry(const ModelConfig &cfg)
Derive the context geometry a tier's config implies.
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 (defined below).
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.
std::string context_overflow_message(const ContextFit &f)
The refusal an operator reads, carrying every number they need.
bool grammar_sources_collide(const std::string &request_grammar, const std::string &tool_grammar)
Whether both sources are active — a config error worth reporting.
@ tool_call
Render-derived (COMMON_GRAMMAR_TYPE_TOOL_CALLS, needs prefill)
@ count
Sentinel — MUST remain last.
@ WARM
mmap'd + mlock'd in RAM
@ COLD
On disk only, no RAM consumed.
BatchKvPlan plan_batch_kv(std::size_t n, int session_slots, const std::vector< int > &temp_ids)
Plan the sequence ids a same-prefix batch may use.
void strip_thinking_channels(std::string &content, std::string *reasoning_out)
Strip Gemma 4 QAT reasoning channels (<|channel>…<channel|>) from content, accumulating the stripped ...
std::string format_generation_summary(const GenerationSummary &s)
Format the one-line generation summary.
static std::string concat_messages_fallback(const std::vector< Message > &messages)
Plain "role: content" join used when templating fails.
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, bool require_tool_call)
Shared common_chat render core for both template paths (gh#87).
static int staged_tool_count(const std::string &tools_json)
Number of definitions in a staged MCP tool-list JSON array.
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, const std::string &tool_grammar, bool tool_grammar_lazy, const std::string &generation_prompt)
Initialize speculative run state (prefill + sampler + decoder).
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.
bool is_request_grammar(GrammarSource source)
Whether a source is applied as a request-side grammar.
GrammarSource resolve_grammar_source(const std::string &request_grammar, const std::string &tool_grammar, bool request_from_tier=false)
Resolve which grammar wins.
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 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, const std::string &tool_grammar, bool tool_grammar_lazy, const std::string &generation_prompt)
Initialize the kernel state: clear KV, prefill, sampler, speculative context, batch,...
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 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.
bool context_fit_overflows(const ContextFit &f)
Whether this turn's prompt cannot fit the tier's context at all.
Definition context_fit.h:65
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...
std::string expert_offload_load_refusal(int cpu_moe_layers, int n_layer, int n_expert)
Post-load refusal, against the numbers only the loaded model knows.
gh#144: derive a tier's KV geometry from max_sessions, purely.
Backend metadata for introspection.
std::string name
Backend identifier (e.g. "llama.cpp", "axcl")
Single cached KV state snapshot.
64-bit hash used as cache lookup key.
One turn's irreducible prompt, measured against the tier budget.
Definition context_fit.h:41
int prompt_tokens
Rendered + tokenized prompt for this turn.
Definition context_fit.h:42
Generation parameters for a single inference call.
Definition config.h:389
std::vector< std::string > stop
Stop sequences.
Definition config.h:481
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.
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 prefill_tokens
Prompt tokens actually pushed through llama_decode during this run's prefill (gh#144,...
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:154
std::filesystem::path mmproj_path
Vision projector GGUF path.
Definition config.h:326
int gpu_layers
GPU offload layers (-1 = all)
Definition config.h:158
int context_length
Context window size (512–131072)
Definition config.h:157
std::filesystem::path path
Resolved model file path.
Definition config.h:155
int cpu_moe_layers
EXPERIMENTAL: layers whose ROUTED-EXPERT tensors go to the host while everything else follows gpu_lay...
Definition config.h:219
bool flash_attn
Enable flash attention.
Definition config.h:315
bool use_mlock
Lock model in system RAM.
Definition config.h:186
int n_seq_max
cparams.n_seq_max
size_t max_bytes
Maximum cache RAM (512 MB default)
Definition config.h:349
bool log_hits
Log cache hit/miss at INFO level.
Definition config.h:351
bool enabled
Master switch (false = no caching)
Definition config.h:350
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:356
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
int n_prefilled
gh#144 (v2.12.0): prompt tokens actually pushed through llama_decode during this run's prefill.
A tool call request parsed from model output.
Definition tool_call.h:31
std::string id
Unique call ID (UUID)
Definition tool_call.h:32
gh#103 (v2.8.2): family-aware tool-call CLOSE markers, derived from the resolved common_chat format.
UTF-8 validation + replacement at every system boundary where bytes change ownership.
ENTROPIC_EXPORT std::string sanitize_utf8(std::string_view input)
Replace invalid UTF-8 byte sequences with U+FFFD.
gh#96 (v2.7.5) warm-keep / incremental-prefill decision logic.