Entropic 2.11.1
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)
63 bool cancelled = fire_model_load_hook(config);
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
91 if (state() == ModelState::ACTIVE) {
92 logger->info("[VRAM] activate() no-op: already ACTIVE");
93 return true;
94 }
95 if (state() != ModelState::WARM) {
96 logger->error("[VRAM] activate() failed: not WARM ({})", state_name(state()));
97 return false;
98 }
99
100 logger->info("[VRAM] Activating");
101 auto start = entropic::log::now();
102 bool ok = do_activate();
103 if (!ok) {
104 logger->error("[VRAM] Activate failed: {}", last_error_);
105 } else {
106 state_.store(ModelState::ACTIVE, std::memory_order_release);
107 logger->info("[VRAM] Active in {:.2f}ms", entropic::log::elapsed_ms(start, entropic::log::now()));
108 }
109 return ok;
110}
111
118 std::lock_guard<std::mutex> lock(transition_mutex_);
119
120 if (state() != ModelState::ACTIVE) {
121 logger->info("[VRAM] deactivate() no-op: {}", state_name(state()));
122 return;
123 }
124
125 logger->info("[VRAM] Deactivating");
126 auto start = entropic::log::now();
127
129 state_.store(ModelState::WARM, std::memory_order_release);
130
131 logger->info("[VRAM] Deactivated in {:.2f}ms", entropic::log::elapsed_ms(start, entropic::log::now()));
132}
133
145 std::lock_guard<std::mutex> lock(transition_mutex_);
146
147 // Hook: ON_MODEL_UNLOAD — informational (v1.9.1)
148 if (hooks_.fire_info != nullptr) {
149 std::string json = "{\"state\":\""
150 + std::string(state_name(state())) + "\"}";
151 hooks_.fire_info(hooks_.registry,
152 ENTROPIC_HOOK_ON_MODEL_UNLOAD, json.c_str());
153 }
154
155 logger->info("[VRAM] Unloading from {}", state_name(state()));
156
157 do_unload();
158 state_.store(ModelState::COLD, std::memory_order_release);
159
160 logger->info("[VRAM] Unloaded");
161}
162
171 if (!load(config)) {
172 return false;
173 }
174 return activate();
175}
176
177// ── Generation ─────────────────────────────────────────────
178
188 const std::vector<Message>& messages,
189 const GenerationParams& params)
190{
191 if (!is_active()) {
194 err.error_message = "generate() requires ACTIVE state";
195 err.finish_reason = "error";
196 logger->error("{}", err.error_message);
197 return err;
198 }
199
200 auto start = entropic::log::now();
201 auto result = do_generate(messages, params);
202 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
203 return result;
204}
205
218 const std::vector<Message>& messages,
219 const GenerationParams& params,
220 std::atomic<bool>& cancel)
221{
222 if (!is_active()) {
225 err.error_message = "generate() requires ACTIVE state";
226 err.finish_reason = "error";
227 logger->error("{}", err.error_message);
228 return err;
229 }
230
231 auto start = entropic::log::now();
232 auto result = do_generate(messages, params, cancel);
233 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
234 return result;
235}
236
246std::vector<GenerationResult> InferenceBackend::generate_batch(
247 const std::vector<std::vector<Message>>& requests,
248 const std::vector<GenerationParams>& params,
249 std::atomic<bool>& cancel)
250{
251 if (!is_active()) {
254 err.error_message = "generate_batch() requires ACTIVE state";
255 err.finish_reason = "error";
256 logger->error("{}", err.error_message);
257 return {err};
258 }
259 auto start = entropic::log::now();
260 auto results = do_generate_batch(requests, params, cancel);
261 double ms = entropic::log::elapsed_ms(start, entropic::log::now());
262 for (auto& r : results) { r.total_ms = ms; }
263 return results;
264}
265
279 const std::vector<Message>& messages,
280 const GenerationParams& params,
281 std::function<void(std::string_view token)> on_token,
282 std::atomic<bool>& cancel)
283{
284 if (!is_active()) {
287 err.error_message = "generate_streaming() requires ACTIVE state";
288 err.finish_reason = "error";
289 logger->error("{}", err.error_message);
290 return err;
291 }
292
293 auto start = entropic::log::now();
294 auto result = do_generate_streaming(messages, params, on_token, cancel);
295 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
296 return result;
297}
298
316 const std::vector<Message>& messages,
317 const GenerationParams& params,
318 std::function<void(std::string_view token)> on_token,
319 std::atomic<bool>& cancel)
320{
321 if (!is_active()) {
324 err.error_message =
325 "generate_speculative() requires ACTIVE state";
326 err.finish_reason = "error";
327 logger->error("{}", err.error_message);
328 return err;
329 }
330 auto start = entropic::log::now();
331 auto result = do_generate_speculative(
332 messages, params, std::move(on_token), cancel);
333 result.generation_time_ms =
334 entropic::log::elapsed_ms(start, entropic::log::now());
335 return result;
336}
337
354 const std::vector<Message>& /*messages*/,
355 const GenerationParams& /*params*/,
356 std::function<void(std::string_view)> /*on_token*/,
357 std::atomic<bool>& /*cancel*/)
358{
359 GenerationResult result;
361 result.error_message =
362 "speculative decoding not implemented for this backend";
363 result.finish_reason = "error";
364 return result;
365}
366
376 const std::string& prompt,
377 const GenerationParams& params)
378{
379 if (!is_active()) {
382 err.error_message = "complete() requires ACTIVE state";
383 err.finish_reason = "error";
384 logger->error("{}", err.error_message);
385 return err;
386 }
387
388 auto start = entropic::log::now();
389 auto result = do_complete(prompt, params);
390 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
391 return result;
392}
393
394// ── Evaluation (v1.9.10) ───────────────────────────────────
395
413 const int32_t* tokens,
414 int n_tokens)
415{
416 if (!is_active()) {
417 logger->error("evaluate_logprobs: model not ACTIVE (state={})",
418 state_name(state()));
419 throw std::runtime_error("Model must be ACTIVE for evaluation");
420 }
421
422 if (n_tokens < 2) {
423 logger->error("evaluate_logprobs: need >= 2 tokens, got {}",
424 n_tokens);
425 throw std::runtime_error(
426 "Need at least 2 tokens for logprob evaluation");
427 }
428
429 std::lock_guard<std::mutex> lock(eval_mutex_);
430
431 logger->info("evaluate_logprobs: {} tokens, first=[{},{},{}...]",
432 n_tokens, tokens[0],
433 n_tokens > 1 ? tokens[1] : 0,
434 n_tokens > 2 ? tokens[2] : 0);
435 auto start = entropic::log::now();
436
437 LogprobResult result = do_evaluate_logprobs(tokens, n_tokens);
438
439 result.total_logprob = 0.0f;
440 for (float lp : result.logprobs) {
441 result.total_logprob += lp;
442 }
443 float mean_lp = result.total_logprob /
444 static_cast<float>(result.n_logprobs);
445 result.perplexity = std::exp(-mean_lp);
446
447 auto ms = entropic::log::elapsed_ms(start, entropic::log::now());
448 logger->info("evaluate_logprobs: perplexity={:.2f}, "
449 "total_lp={:.4f}, {:.2f}ms",
450 result.perplexity, result.total_logprob, ms);
451 for (int i = 0; i < result.n_logprobs; ++i) {
452 logger->info(" logprob[{}]={:.4f}", i, result.logprobs[i]);
453 }
454
455 return result;
456}
457
471 const int32_t* tokens,
472 int n_tokens)
473{
474 return evaluate_logprobs(tokens, n_tokens).perplexity;
475}
476
477// ── Hook helpers (v1.9.1) ──────────────────────────────────
478
487 if (hooks_.fire_pre == nullptr) {
488 return false;
489 }
490 std::string json = "{\"model_path\":\""
491 + config.path.string() + "\"}";
492 char* mod = nullptr;
493 int rc = hooks_.fire_pre(hooks_.registry,
494 ENTROPIC_HOOK_ON_MODEL_LOAD, json.c_str(), &mod);
495 free(mod);
496 if (rc != 0) {
497 logger->info("[VRAM] ON_MODEL_LOAD hook cancelled");
498 }
499 return rc != 0;
500}
501
502// ── Queries ────────────────────────────────────────────────
503
511int InferenceBackend::count_tokens(const std::string& text) const {
512 if (is_loaded()) {
513 return do_count_tokens(text);
514 }
515 return static_cast<int>(text.size()) / 4;
516}
517
518// ── Capability queries (v1.9.13) ───────────────────────────
519
529 return do_supports(cap);
530}
531
544std::vector<BackendCapability> InferenceBackend::capabilities() const {
545 std::vector<BackendCapability> result;
546 int count = static_cast<int>(BackendCapability::_COUNT);
547 for (int i = 0; i < count; ++i) {
548 auto cap = static_cast<BackendCapability>(i);
549 if (supports(cap)) {
550 result.push_back(cap);
551 }
552 }
553 return result;
554}
555
564 return do_info();
565}
566
567// ── Model state management (v1.9.13) ──────────────────────
568
579 int seq_id, std::vector<uint8_t>& buffer) const
580{
581 if (!is_active()) {
582 logger->warn("save_state: not ACTIVE ({})", state_name(state()));
583 return false;
584 }
585 auto start = entropic::log::now();
586 bool ok = do_save_state(seq_id, buffer);
587 if (ok) {
588 logger->info("save_state: seq={} {}B {:.2f}ms",
589 seq_id, buffer.size(), entropic::log::elapsed_ms(start, entropic::log::now()));
590 }
591 return ok;
592}
593
604 int seq_id, const std::vector<uint8_t>& buffer)
605{
606 if (!is_active()) {
607 logger->warn("restore_state: not ACTIVE ({})",
608 state_name(state()));
609 return false;
610 }
611 auto start = entropic::log::now();
612 bool ok = do_restore_state(seq_id, buffer);
613 if (ok) {
614 logger->info("restore_state: seq={} {}B {:.2f}ms",
615 seq_id, buffer.size(), entropic::log::elapsed_ms(start, entropic::log::now()));
616 }
617 return ok;
618}
619
629 if (state() == ModelState::COLD) {
630 logger->warn("clear_state: model is COLD");
631 return false;
632 }
633 bool ok = do_clear_state(seq_id);
634 if (ok) {
635 logger->info("clear_state: seq={}", seq_id);
636 }
637 return ok;
638}
639
640// ── Multi-sequence generation (v1.9.13) ────────────────────
641
652 int seq_id,
653 const std::vector<Message>& messages,
654 const GenerationParams& params)
655{
656 if (!is_active()) {
659 err.error_message = "generate_seq() requires ACTIVE state";
660 err.finish_reason = "error";
661 logger->error("{}", err.error_message);
662 return err;
663 }
664
665 auto start = entropic::log::now();
666 auto result = do_generate_seq(seq_id, messages, params);
667 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
668 result.seq_id = seq_id;
669 return result;
670}
671
684 int seq_id,
685 const std::vector<Message>& messages,
686 const GenerationParams& params,
687 std::function<void(std::string_view token)> on_token,
688 std::atomic<bool>& cancel)
689{
690 if (!is_active()) {
693 err.error_message =
694 "generate_streaming_seq() requires ACTIVE state";
695 err.finish_reason = "error";
696 logger->error("{}", err.error_message);
697 return err;
698 }
699
700 auto start = entropic::log::now();
701 auto result = do_generate_streaming_seq(
702 seq_id, messages, params, on_token, cancel);
703 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
704 result.seq_id = seq_id;
705 return result;
706}
707
708// ── Default virtual implementations (v1.9.13) ─────────────
709
718 return false;
719}
720
728 BackendInfo bi;
729 bi.name = do_backend_name();
730 return bi;
731}
732
742 int /*seq_id*/, std::vector<uint8_t>& /*buffer*/) const
743{
744 return false;
745}
746
756 int /*seq_id*/, const std::vector<uint8_t>& /*buffer*/)
757{
758 return false;
759}
760
769 return false;
770}
771
782 int /*seq_id*/,
783 const std::vector<Message>& messages,
784 const GenerationParams& params)
785{
786 return do_generate(messages, params);
787}
788
801 int /*seq_id*/,
802 const std::vector<Message>& messages,
803 const GenerationParams& params,
804 std::function<void(std::string_view token)> on_token,
805 std::atomic<bool>& cancel)
806{
807 return do_generate_streaming(messages, params, on_token, cancel);
808}
809
810} // namespace entropic
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:800
GenerationResult generate_seq(int seq_id, const std::vector< Message > &messages, const GenerationParams &params)
Generate with explicit sequence ID.
Definition backend.cpp:651
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:315
float compute_perplexity(const int32_t *tokens, int n_tokens)
Compute perplexity for a token sequence.
Definition backend.cpp:470
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:536
std::string last_error_
Last error message for diagnostics.
Definition backend.h:727
virtual BackendInfo do_info() const
Populate backend metadata.
Definition backend.cpp:727
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:578
bool supports(BackendCapability cap) const
Query whether this backend supports a capability.
Definition backend.cpp:528
bool restore_state(int seq_id, const std::vector< uint8_t > &buffer)
Restore model state from buffer.
Definition backend.cpp:603
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:755
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:717
void deactivate()
Release GPU layers (ACTIVE → WARM).
Definition backend.cpp:117
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:563
bool is_active() const
True when state is ACTIVE.
Definition backend.h:249
ModelState state() const
Current lifecycle state (lock-free read).
Definition backend.h:241
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:353
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:544
void unload()
Full unload (→ COLD).
Definition backend.cpp:144
const ModelConfig & config() const
Stored model config.
Definition backend.h:320
bool clear_state(int seq_id=-1)
Clear/reset model state for a sequence.
Definition backend.cpp:628
virtual GenerationResult do_generate_seq(int seq_id, const std::vector< Message > &messages, const GenerationParams &params)
Generate with sequence ID.
Definition backend.cpp:781
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:257
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:246
GenerationResult generate(const std::vector< Message > &messages, const GenerationParams &params)
Generate a complete response.
Definition backend.cpp:187
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:768
int count_tokens(const std::string &text) const
Count tokens using model's tokenizer.
Definition backend.cpp:511
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:741
bool fire_model_load_hook(const ModelConfig &config)
Fire ON_MODEL_LOAD pre-hook.
Definition backend.cpp:486
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:278
LogprobResult evaluate_logprobs(const int32_t *tokens, int n_tokens)
Evaluate per-token log-probabilities for a token sequence.
Definition backend.cpp:412
bool load_and_activate(const ModelConfig &config)
Convenience: load() + activate().
Definition backend.cpp:170
GenerationResult complete(const std::string &prompt, const GenerationParams &params)
Raw text completion without chat template.
Definition backend.cpp:375
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:683
std::atomic< ModelState > state_
State transition slot accessible to subclasses for test-only injection.
Definition backend.h:753
@ 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
@ 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.
@ 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:313
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
std::filesystem::path path
Resolved model file path.
Definition config.h:155