Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
backend.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
15
16#include <cmath>
17#include <cstdlib>
18#include <stdexcept>
19#include <string>
20
21namespace entropic {
22
23namespace {
24
25auto logger = entropic::log::get("inference.backend");
26
34const char* state_name(ModelState s) {
35 static constexpr const char* names[] = {"COLD", "WARM", "ACTIVE"};
36 int idx = static_cast<int>(s);
37 return (idx >= 0 && idx <= 2) ? names[idx] : "UNKNOWN";
38}
39
40} // anonymous namespace
41
42// ── Lifecycle ──────────────────────────────────────────────
43
55 std::lock_guard<std::mutex> lock(transition_mutex_);
56
57 if (state() != ModelState::COLD) {
58 logger->info("[VRAM] load() no-op: already {}", state_name(state()));
59 return true;
60 }
61
62 // Hook: ON_MODEL_LOAD — can cancel (v1.9.1)
64 if (cancelled) {
65 return false;
66 }
67
68 logger->info("[VRAM] Loading: {}", config.path.string());
69 auto start = entropic::log::now();
70
71 config_ = config;
72 bool ok = do_load(config);
73 if (!ok) {
74 logger->error("[VRAM] Load failed: {}", last_error_);
75 } else {
76 state_.store(ModelState::WARM, std::memory_order_release);
77 logger->info("[VRAM] Warm in {:.2f}ms", entropic::log::elapsed_ms(start, entropic::log::now()));
78 }
79 return ok;
80}
81
89 std::lock_guard<std::mutex> lock(transition_mutex_);
90 return promote_to_active();
91}
92
106 if (state() == ModelState::ACTIVE) {
107 logger->info("[VRAM] activate() no-op: already ACTIVE");
108 return true;
109 }
110 if (state() != ModelState::WARM) {
111 logger->error("[VRAM] activate() failed: not WARM ({})", state_name(state()));
112 return false;
113 }
114
115 logger->info("[VRAM] Activating");
116 auto start = entropic::log::now();
117 bool ok = do_activate();
118 if (!ok) {
119 logger->error("[VRAM] Activate failed: {}", last_error_);
120 } else {
121 state_.store(ModelState::ACTIVE, std::memory_order_release);
122 logger->info("[VRAM] Active in {:.2f}ms", entropic::log::elapsed_ms(start, entropic::log::now()));
123 }
124 return ok;
125}
126
133 std::lock_guard<std::mutex> lock(transition_mutex_);
134
135 if (state() != ModelState::ACTIVE) {
136 logger->info("[VRAM] deactivate() no-op: {}", state_name(state()));
137 return;
138 }
139
140 logger->info("[VRAM] Deactivating");
141 auto start = entropic::log::now();
142
144 state_.store(ModelState::WARM, std::memory_order_release);
145
146 logger->info("[VRAM] Deactivated in {:.2f}ms", entropic::log::elapsed_ms(start, entropic::log::now()));
147}
148
160 std::lock_guard<std::mutex> lock(transition_mutex_);
161
162 // Hook: ON_MODEL_UNLOAD — informational (v1.9.1)
163 if (hooks_.fire_info != nullptr) {
164 std::string json = "{\"state\":\""
165 + std::string(state_name(state())) + "\"}";
166 hooks_.fire_info(hooks_.registry,
167 ENTROPIC_HOOK_ON_MODEL_UNLOAD, json.c_str());
168 }
169
170 logger->info("[VRAM] Unloading from {}", state_name(state()));
171
172 do_unload();
173 state_.store(ModelState::COLD, std::memory_order_release);
174
175 logger->info("[VRAM] Unloaded");
176}
177
201 std::lock_guard<std::mutex> lock(transition_mutex_);
202
203 if (state() != ModelState::COLD) { return promote_to_active(); }
204
205 // Hook: ON_MODEL_LOAD — can cancel (v1.9.1). Same contract as load().
206 if (fire_model_load_hook(config)) { return false; }
207
208 logger->info("[VRAM] Loading into target residency: {} (gpu_layers={})",
209 config.path.string(), config.gpu_layers);
210 auto start = entropic::log::now();
211
212 config_ = config;
213 bool ok = do_load_active(config);
214 if (!ok) {
215 logger->error("[VRAM] Load+activate failed: {}", last_error_);
216 } else {
217 state_.store(ModelState::ACTIVE, std::memory_order_release);
218 logger->info("[VRAM] Active in {:.2f}ms (one file read)",
219 entropic::log::elapsed_ms(start, entropic::log::now()));
220 }
221 return ok;
222}
223
224// ── Generation ─────────────────────────────────────────────
225
235 const std::vector<Message>& messages,
237{
238 if (!is_active()) {
241 err.error_message = "generate() requires ACTIVE state";
242 err.finish_reason = "error";
243 logger->error("{}", err.error_message);
244 return err;
245 }
246
247 auto start = entropic::log::now();
248 auto result = do_generate(messages, params);
249 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
250 return result;
251}
252
265 const std::vector<Message>& messages,
267 std::atomic<bool>& cancel)
268{
269 if (!is_active()) {
272 err.error_message = "generate() requires ACTIVE state";
273 err.finish_reason = "error";
274 logger->error("{}", err.error_message);
275 return err;
276 }
277
278 auto start = entropic::log::now();
279 auto result = do_generate(messages, params, cancel);
280 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
281 return result;
282}
283
293std::vector<GenerationResult> InferenceBackend::generate_batch(
294 const std::vector<std::vector<Message>>& requests,
295 const std::vector<GenerationParams>& params,
296 std::atomic<bool>& cancel)
297{
298 if (!is_active()) {
301 err.error_message = "generate_batch() requires ACTIVE state";
302 err.finish_reason = "error";
303 logger->error("{}", err.error_message);
304 return {err};
305 }
306 auto start = entropic::log::now();
308 double ms = entropic::log::elapsed_ms(start, entropic::log::now());
309 for (auto& r : results) { r.total_ms = ms; }
310 return results;
311}
312
326 const std::vector<Message>& messages,
328 std::function<void(std::string_view token)> on_token,
329 std::atomic<bool>& cancel)
330{
331 if (!is_active()) {
334 err.error_message = "generate_streaming() requires ACTIVE state";
335 err.finish_reason = "error";
336 logger->error("{}", err.error_message);
337 return err;
338 }
339
340 auto start = entropic::log::now();
341 auto result = do_generate_streaming(messages, params, on_token, cancel);
342 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
343 return result;
344}
345
363 const std::vector<Message>& messages,
365 std::function<void(std::string_view token)> on_token,
366 std::atomic<bool>& cancel)
367{
368 if (!is_active()) {
371 err.error_message =
372 "generate_speculative() requires ACTIVE state";
373 err.finish_reason = "error";
374 logger->error("{}", err.error_message);
375 return err;
376 }
377 auto start = entropic::log::now();
378 auto result = do_generate_speculative(
379 messages, params, std::move(on_token), cancel);
380 result.generation_time_ms =
381 entropic::log::elapsed_ms(start, entropic::log::now());
382 return result;
383}
384
401 const std::vector<Message>& /*messages*/,
402 const GenerationParams& /*params*/,
403 std::function<void(std::string_view)> /*on_token*/,
404 std::atomic<bool>& /*cancel*/)
405{
406 GenerationResult result;
408 result.error_message =
409 "speculative decoding not implemented for this backend";
410 result.finish_reason = "error";
411 return result;
412}
413
423 const std::string& prompt,
425{
426 if (!is_active()) {
429 err.error_message = "complete() requires ACTIVE state";
430 err.finish_reason = "error";
431 logger->error("{}", err.error_message);
432 return err;
433 }
434
435 auto start = entropic::log::now();
436 auto result = do_complete(prompt, params);
437 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
438 return result;
439}
440
441// ── Evaluation (v1.9.10) ───────────────────────────────────
442
460 const int32_t* tokens,
461 int n_tokens)
462{
463 if (!is_active()) {
464 logger->error("evaluate_logprobs: model not ACTIVE (state={})",
465 state_name(state()));
466 throw std::runtime_error("Model must be ACTIVE for evaluation");
467 }
468
469 if (n_tokens < 2) {
470 logger->error("evaluate_logprobs: need >= 2 tokens, got {}",
471 n_tokens);
472 throw std::runtime_error(
473 "Need at least 2 tokens for logprob evaluation");
474 }
475
476 std::lock_guard<std::mutex> lock(eval_mutex_);
477
478 logger->info("evaluate_logprobs: {} tokens, first=[{},{},{}...]",
479 n_tokens, tokens[0],
480 n_tokens > 1 ? tokens[1] : 0,
481 n_tokens > 2 ? tokens[2] : 0);
482 auto start = entropic::log::now();
483
484 LogprobResult result = do_evaluate_logprobs(tokens, n_tokens);
485
486 result.total_logprob = 0.0f;
487 for (float lp : result.logprobs) {
488 result.total_logprob += lp;
489 }
490 float mean_lp = result.total_logprob /
491 static_cast<float>(result.n_logprobs);
492 result.perplexity = std::exp(-mean_lp);
493
494 auto ms = entropic::log::elapsed_ms(start, entropic::log::now());
495 logger->info("evaluate_logprobs: perplexity={:.2f}, "
496 "total_lp={:.4f}, {:.2f}ms",
497 result.perplexity, result.total_logprob, ms);
498 for (int i = 0; i < result.n_logprobs; ++i) {
499 logger->info(" logprob[{}]={:.4f}", i, result.logprobs[i]);
500 }
501
502 return result;
503}
504
518 const int32_t* tokens,
519 int n_tokens)
520{
521 return evaluate_logprobs(tokens, n_tokens).perplexity;
522}
523
524// ── Hook helpers (v1.9.1) ──────────────────────────────────
525
534 if (hooks_.fire_pre == nullptr) {
535 return false;
536 }
537 std::string json = "{\"model_path\":\""
538 + config.path.string() + "\"}";
539 char* mod = nullptr;
540 int rc = hooks_.fire_pre(hooks_.registry,
541 ENTROPIC_HOOK_ON_MODEL_LOAD, json.c_str(), &mod);
542 free(mod);
543 if (rc != 0) {
544 logger->info("[VRAM] ON_MODEL_LOAD hook cancelled");
545 }
546 return rc != 0;
547}
548
549// ── Queries ────────────────────────────────────────────────
550
558int InferenceBackend::count_tokens(const std::string& text) const {
559 if (is_loaded()) {
560 return do_count_tokens(text);
561 }
562 return static_cast<int>(text.size()) / 4;
563}
564
565// ── Capability queries (v1.9.13) ───────────────────────────
566
578
591std::vector<BackendCapability> InferenceBackend::capabilities() const {
592 std::vector<BackendCapability> result;
593 int count = static_cast<int>(BackendCapability::_COUNT);
594 for (int i = 0; i < count; ++i) {
595 auto cap = static_cast<BackendCapability>(i);
596 if (supports(cap)) {
597 result.push_back(cap);
598 }
599 }
600 return result;
601}
602
611 return do_info();
612}
613
614// ── Model state management (v1.9.13) ──────────────────────
615
626 int seq_id, std::vector<uint8_t>& buffer) const
627{
628 if (!is_active()) {
629 logger->warn("save_state: not ACTIVE ({})", state_name(state()));
630 return false;
631 }
632 auto start = entropic::log::now();
633 bool ok = do_save_state(seq_id, buffer);
634 if (ok) {
635 logger->info("save_state: seq={} {}B {:.2f}ms",
636 seq_id, buffer.size(), entropic::log::elapsed_ms(start, entropic::log::now()));
637 }
638 return ok;
639}
640
651 int seq_id, const std::vector<uint8_t>& buffer)
652{
653 if (!is_active()) {
654 logger->warn("restore_state: not ACTIVE ({})",
655 state_name(state()));
656 return false;
657 }
658 auto start = entropic::log::now();
659 bool ok = do_restore_state(seq_id, buffer);
660 if (ok) {
661 logger->info("restore_state: seq={} {}B {:.2f}ms",
662 seq_id, buffer.size(), entropic::log::elapsed_ms(start, entropic::log::now()));
663 }
664 return ok;
665}
666
676 if (state() == ModelState::COLD) {
677 logger->warn("clear_state: model is COLD");
678 return false;
679 }
680 bool ok = do_clear_state(seq_id);
681 if (ok) {
682 logger->info("clear_state: seq={}", seq_id);
683 }
684 return ok;
685}
686
687// ── Multi-sequence generation (v1.9.13) ────────────────────
688
699 int seq_id,
700 const std::vector<Message>& messages,
702{
703 if (!is_active()) {
706 err.error_message = "generate_seq() requires ACTIVE state";
707 err.finish_reason = "error";
708 logger->error("{}", err.error_message);
709 return err;
710 }
711
712 auto start = entropic::log::now();
713 auto result = do_generate_seq(seq_id, messages, params);
714 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
715 result.seq_id = seq_id;
716 return result;
717}
718
731 int seq_id,
732 const std::vector<Message>& messages,
734 std::function<void(std::string_view token)> on_token,
735 std::atomic<bool>& cancel)
736{
737 if (!is_active()) {
740 err.error_message =
741 "generate_streaming_seq() requires ACTIVE state";
742 err.finish_reason = "error";
743 logger->error("{}", err.error_message);
744 return err;
745 }
746
747 auto start = entropic::log::now();
748 auto result = do_generate_streaming_seq(
749 seq_id, messages, params, on_token, cancel);
750 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
751 result.seq_id = seq_id;
752 return result;
753}
754
755// ── Default virtual implementations (v1.9.13) ─────────────
756
765 return false;
766}
767
779
789 int /*seq_id*/, std::vector<uint8_t>& /*buffer*/) const
790{
791 return false;
792}
793
803 int /*seq_id*/, const std::vector<uint8_t>& /*buffer*/)
804{
805 return false;
806}
807
816 return false;
817}
818
829 int /*seq_id*/,
830 const std::vector<Message>& messages,
832{
833 return do_generate(messages, params);
834}
835
848 int /*seq_id*/,
849 const std::vector<Message>& messages,
851 std::function<void(std::string_view token)> on_token,
852 std::atomic<bool>& cancel)
853{
854 return do_generate_streaming(messages, params, on_token, cancel);
855}
856
857} // namespace entropic
bool cancelled
The run was interrupted and the group was killed (gh#172)
Definition bash.cpp:101
virtual bool do_load_active(const ModelConfig &config)
Load COLD → ACTIVE in one step.
Definition backend.h:519
virtual GenerationResult do_complete(const std::string &prompt, const GenerationParams &params)=0
Subclass raw completion.
virtual GenerationResult do_generate_streaming_seq(int seq_id, const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel)
Streaming generation with sequence ID.
Definition backend.cpp:847
GenerationResult generate_seq(int seq_id, const std::vector< Message > &messages, const GenerationParams &params)
Generate with explicit sequence ID.
Definition backend.cpp:698
virtual LogprobResult do_evaluate_logprobs(const int32_t *tokens, int n_tokens)=0
Backend-specific logprob evaluation.
GenerationResult generate_speculative(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel)
Generate via the speculative-decoding kernel (v2.1.11).
Definition backend.cpp:362
float compute_perplexity(const int32_t *tokens, int n_tokens)
Compute perplexity for a token sequence.
Definition backend.cpp:517
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
virtual BackendInfo do_info() const
Populate backend metadata.
Definition backend.cpp:774
virtual 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)=0
Subclass streaming generation.
bool save_state(int seq_id, std::vector< uint8_t > &buffer) const
Save model state to buffer.
Definition backend.cpp:625
bool supports(BackendCapability cap) const
Query whether this backend supports a capability.
Definition backend.cpp:575
bool restore_state(int seq_id, const std::vector< uint8_t > &buffer)
Restore model state from buffer.
Definition backend.cpp:650
bool activate()
Promote to GPU (WARM → ACTIVE).
Definition backend.cpp:88
virtual bool do_restore_state(int seq_id, const std::vector< uint8_t > &buffer)
Restore model state.
Definition backend.cpp:802
virtual int do_count_tokens(const std::string &text) const =0
Subclass token counting.
virtual bool do_supports(BackendCapability cap) const
Declare supported capabilities.
Definition backend.cpp:764
void deactivate()
Release GPU layers (ACTIVE → WARM).
Definition backend.cpp:132
bool promote_to_active()
WARM → ACTIVE promotion.
Definition backend.cpp:105
virtual void do_unload()=0
Full unload.
virtual bool do_activate()=0
Promote loaded model to GPU.
BackendInfo info() const
Get backend metadata.
Definition backend.cpp:610
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
virtual bool do_load(const ModelConfig &config)=0
Load model into CPU RAM.
virtual 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)
Subclass speculative-decoding streaming generation.
Definition backend.cpp:400
virtual void do_deactivate()=0
Release GPU, keep CPU.
virtual GenerationResult do_generate(const std::vector< Message > &messages, const GenerationParams &params)=0
Subclass generation.
std::vector< BackendCapability > capabilities() const
Get all supported capabilities as a vector.
Definition backend.cpp:591
void unload()
Full unload (→ COLD).
Definition backend.cpp:159
const ModelConfig & config() const
Stored model config.
Definition backend.h:334
bool clear_state(int seq_id=-1)
Clear/reset model state for a sequence.
Definition backend.cpp:675
virtual GenerationResult do_generate_seq(int seq_id, const std::vector< Message > &messages, const GenerationParams &params)
Generate with sequence ID.
Definition backend.cpp:828
virtual std::string do_backend_name() const =0
Return backend name identifier.
bool is_loaded() const
True when state is WARM or ACTIVE.
Definition backend.h:271
std::vector< GenerationResult > generate_batch(const std::vector< std::vector< Message > > &requests, const std::vector< GenerationParams > &params, std::atomic< bool > &cancel)
Generate N independent same-prefix requests together.
Definition backend.cpp:293
GenerationResult generate(const std::vector< Message > &messages, const GenerationParams &params)
Generate a complete response.
Definition backend.cpp:234
bool load(const ModelConfig &config)
Load model into CPU RAM (COLD → WARM).
Definition backend.cpp:54
virtual bool do_clear_state(int seq_id)
Clear/reset model state.
Definition backend.cpp:815
int count_tokens(const std::string &text) const
Count tokens using model's tokenizer.
Definition backend.cpp:558
virtual bool do_save_state(int seq_id, std::vector< uint8_t > &buffer) const
Save model state (KV cache or hidden state).
Definition backend.cpp:788
bool fire_model_load_hook(const ModelConfig &config)
Fire ON_MODEL_LOAD pre-hook.
Definition backend.cpp:533
GenerationResult generate_streaming(const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel)
Generate with per-token streaming callback.
Definition backend.cpp:325
LogprobResult evaluate_logprobs(const int32_t *tokens, int n_tokens)
Evaluate per-token log-probabilities for a token sequence.
Definition backend.cpp:459
bool load_and_activate(const ModelConfig &config)
Bring a model to ACTIVE in as few file reads as possible.
Definition backend.cpp:200
GenerationResult complete(const std::string &prompt, const GenerationParams &params)
Raw text completion without chat template.
Definition backend.cpp:422
GenerationResult generate_streaming_seq(int seq_id, const std::vector< Message > &messages, const GenerationParams &params, std::function< void(std::string_view token)> on_token, std::atomic< bool > &cancel)
Streaming generation with explicit sequence ID.
Definition backend.cpp:730
std::atomic< ModelState > state_
State transition slot accessible to subclasses for test-only injection.
Definition backend.h:843
int * cancel
Consumer's cancel flag, nullable.
Definition engine.cpp:4074
@ ENTROPIC_ERROR_NOT_SUPPORTED
Capability not supported by this backend (v1.9.13)
Definition error.h:86
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
Definition error.h:41
std::string err
size_exceeded, when set
@ ENTROPIC_HOOK_ON_MODEL_UNLOAD
14: Model unloaded from backend
Definition hooks.h:55
@ ENTROPIC_HOOK_ON_MODEL_LOAD
13: Model loaded into backend
Definition hooks.h:54
InferenceBackend concrete base class.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
Activate model on GPU (WARM → ACTIVE).
BackendCapability
Capabilities that an inference backend may or may not support.
@ _COUNT
Sentinel — must be last. Used for iteration/array sizing.
@ tokens
Gate on generated tokens since the last tool call.
@ ok
Tool dispatched, returned non-empty content.
@ optional
optional: every escape asks the host's path approver
@ count
Sentinel — MUST remain last.
ModelState
C++ enum class for model VRAM lifecycle states.
Definition config.h:96
@ WARM
mmap'd + mlock'd in RAM
@ ACTIVE
GPU layers loaded, full speed.
@ COLD
On disk only, no RAM consumed.
Backend metadata for introspection.
std::string name
Backend identifier (e.g. "llama.cpp", "axcl")
Generation parameters for a single inference call.
Definition config.h:389
Result of a single generation call.
entropic_error_t error_code
Error code (ENTROPIC_OK if no error)
std::string finish_reason
Finish reason: "stop", "length", "error".
std::string error_message
Error description (empty if no error)
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)
float total_logprob
Sum of all logprob values.
float perplexity
exp(-mean(logprobs)) — lower = less surprising
Model configuration for a single tier.
Definition config.h:154
int gpu_layers
GPU offload layers (-1 = all)
Definition config.h:158
std::filesystem::path path
Resolved model file path.
Definition config.h:155