Entropic 2.9.5
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
140 std::lock_guard<std::mutex> lock(transition_mutex_);
141
142 // Hook: ON_MODEL_UNLOAD — informational (v1.9.1)
143 if (hooks_.fire_info != nullptr) {
144 std::string json = "{\"state\":\""
145 + std::string(state_name(state())) + "\"}";
146 hooks_.fire_info(hooks_.registry,
147 ENTROPIC_HOOK_ON_MODEL_UNLOAD, json.c_str());
148 }
149
150 logger->info("[VRAM] Unloading from {}", state_name(state()));
151
152 do_unload();
153 state_.store(ModelState::COLD, std::memory_order_release);
154
155 logger->info("[VRAM] Unloaded");
156}
157
166 if (!load(config)) {
167 return false;
168 }
169 return activate();
170}
171
172// ── Generation ─────────────────────────────────────────────
173
183 const std::vector<Message>& messages,
184 const GenerationParams& params)
185{
186 if (!is_active()) {
189 err.error_message = "generate() requires ACTIVE state";
190 err.finish_reason = "error";
191 logger->error("{}", err.error_message);
192 return err;
193 }
194
195 auto start = entropic::log::now();
196 auto result = do_generate(messages, params);
197 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
198 return result;
199}
200
207 const std::vector<Message>& messages,
208 const GenerationParams& params,
209 std::atomic<bool>& cancel)
210{
211 if (!is_active()) {
214 err.error_message = "generate() requires ACTIVE state";
215 err.finish_reason = "error";
216 logger->error("{}", err.error_message);
217 return err;
218 }
219
220 auto start = entropic::log::now();
221 auto result = do_generate(messages, params, cancel);
222 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
223 return result;
224}
225
235std::vector<GenerationResult> InferenceBackend::generate_batch(
236 const std::vector<std::vector<Message>>& requests,
237 const std::vector<GenerationParams>& params,
238 std::atomic<bool>& cancel)
239{
240 if (!is_active()) {
243 err.error_message = "generate_batch() requires ACTIVE state";
244 err.finish_reason = "error";
245 logger->error("{}", err.error_message);
246 return {err};
247 }
248 auto start = entropic::log::now();
249 auto results = do_generate_batch(requests, params, cancel);
250 double ms = entropic::log::elapsed_ms(start, entropic::log::now());
251 for (auto& r : results) { r.total_ms = ms; }
252 return results;
253}
254
266 const std::vector<Message>& messages,
267 const GenerationParams& params,
268 std::function<void(std::string_view token)> on_token,
269 std::atomic<bool>& cancel)
270{
271 if (!is_active()) {
274 err.error_message = "generate_streaming() requires ACTIVE state";
275 err.finish_reason = "error";
276 logger->error("{}", err.error_message);
277 return err;
278 }
279
280 auto start = entropic::log::now();
281 auto result = do_generate_streaming(messages, params, on_token, cancel);
282 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
283 return result;
284}
285
303 const std::vector<Message>& messages,
304 const GenerationParams& params,
305 std::function<void(std::string_view token)> on_token,
306 std::atomic<bool>& cancel)
307{
308 if (!is_active()) {
311 err.error_message =
312 "generate_speculative() requires ACTIVE state";
313 err.finish_reason = "error";
314 logger->error("{}", err.error_message);
315 return err;
316 }
317 auto start = entropic::log::now();
318 auto result = do_generate_speculative(
319 messages, params, std::move(on_token), cancel);
320 result.generation_time_ms =
321 entropic::log::elapsed_ms(start, entropic::log::now());
322 return result;
323}
324
341 const std::vector<Message>& /*messages*/,
342 const GenerationParams& /*params*/,
343 std::function<void(std::string_view)> /*on_token*/,
344 std::atomic<bool>& /*cancel*/)
345{
346 GenerationResult result;
348 result.error_message =
349 "speculative decoding not implemented for this backend";
350 result.finish_reason = "error";
351 return result;
352}
353
363 const std::string& prompt,
364 const GenerationParams& params)
365{
366 if (!is_active()) {
369 err.error_message = "complete() requires ACTIVE state";
370 err.finish_reason = "error";
371 logger->error("{}", err.error_message);
372 return err;
373 }
374
375 auto start = entropic::log::now();
376 auto result = do_complete(prompt, params);
377 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
378 return result;
379}
380
381// ── Evaluation (v1.9.10) ───────────────────────────────────
382
398 const int32_t* tokens,
399 int n_tokens)
400{
401 if (!is_active()) {
402 logger->error("evaluate_logprobs: model not ACTIVE (state={})",
403 state_name(state()));
404 throw std::runtime_error("Model must be ACTIVE for evaluation");
405 }
406
407 if (n_tokens < 2) {
408 logger->error("evaluate_logprobs: need >= 2 tokens, got {}",
409 n_tokens);
410 throw std::runtime_error(
411 "Need at least 2 tokens for logprob evaluation");
412 }
413
414 std::lock_guard<std::mutex> lock(eval_mutex_);
415
416 logger->info("evaluate_logprobs: {} tokens, first=[{},{},{}...]",
417 n_tokens, tokens[0],
418 n_tokens > 1 ? tokens[1] : 0,
419 n_tokens > 2 ? tokens[2] : 0);
420 auto start = entropic::log::now();
421
422 LogprobResult result = do_evaluate_logprobs(tokens, n_tokens);
423
424 result.total_logprob = 0.0f;
425 for (float lp : result.logprobs) {
426 result.total_logprob += lp;
427 }
428 float mean_lp = result.total_logprob /
429 static_cast<float>(result.n_logprobs);
430 result.perplexity = std::exp(-mean_lp);
431
432 auto ms = entropic::log::elapsed_ms(start, entropic::log::now());
433 logger->info("evaluate_logprobs: perplexity={:.2f}, "
434 "total_lp={:.4f}, {:.2f}ms",
435 result.perplexity, result.total_logprob, ms);
436 for (int i = 0; i < result.n_logprobs; ++i) {
437 logger->info(" logprob[{}]={:.4f}", i, result.logprobs[i]);
438 }
439
440 return result;
441}
442
456 const int32_t* tokens,
457 int n_tokens)
458{
459 return evaluate_logprobs(tokens, n_tokens).perplexity;
460}
461
462// ── Hook helpers (v1.9.1) ──────────────────────────────────
463
472 if (hooks_.fire_pre == nullptr) {
473 return false;
474 }
475 std::string json = "{\"model_path\":\""
476 + config.path.string() + "\"}";
477 char* mod = nullptr;
478 int rc = hooks_.fire_pre(hooks_.registry,
479 ENTROPIC_HOOK_ON_MODEL_LOAD, json.c_str(), &mod);
480 free(mod);
481 if (rc != 0) {
482 logger->info("[VRAM] ON_MODEL_LOAD hook cancelled");
483 }
484 return rc != 0;
485}
486
487// ── Queries ────────────────────────────────────────────────
488
496int InferenceBackend::count_tokens(const std::string& text) const {
497 if (is_loaded()) {
498 return do_count_tokens(text);
499 }
500 return static_cast<int>(text.size()) / 4;
501}
502
503// ── Capability queries (v1.9.13) ───────────────────────────
504
513 return do_supports(cap);
514}
515
522std::vector<BackendCapability> InferenceBackend::capabilities() const {
523 std::vector<BackendCapability> result;
524 int count = static_cast<int>(BackendCapability::_COUNT);
525 for (int i = 0; i < count; ++i) {
526 auto cap = static_cast<BackendCapability>(i);
527 if (supports(cap)) {
528 result.push_back(cap);
529 }
530 }
531 return result;
532}
533
541 return do_info();
542}
543
544// ── Model state management (v1.9.13) ──────────────────────
545
555 int seq_id, std::vector<uint8_t>& buffer) const
556{
557 if (!is_active()) {
558 logger->warn("save_state: not ACTIVE ({})", state_name(state()));
559 return false;
560 }
561 auto start = entropic::log::now();
562 bool ok = do_save_state(seq_id, buffer);
563 if (ok) {
564 logger->info("save_state: seq={} {}B {:.2f}ms",
565 seq_id, buffer.size(), entropic::log::elapsed_ms(start, entropic::log::now()));
566 }
567 return ok;
568}
569
579 int seq_id, const std::vector<uint8_t>& buffer)
580{
581 if (!is_active()) {
582 logger->warn("restore_state: not ACTIVE ({})",
583 state_name(state()));
584 return false;
585 }
586 auto start = entropic::log::now();
587 bool ok = do_restore_state(seq_id, buffer);
588 if (ok) {
589 logger->info("restore_state: seq={} {}B {:.2f}ms",
590 seq_id, buffer.size(), entropic::log::elapsed_ms(start, entropic::log::now()));
591 }
592 return ok;
593}
594
603 if (state() == ModelState::COLD) {
604 logger->warn("clear_state: model is COLD");
605 return false;
606 }
607 bool ok = do_clear_state(seq_id);
608 if (ok) {
609 logger->info("clear_state: seq={}", seq_id);
610 }
611 return ok;
612}
613
614// ── Multi-sequence generation (v1.9.13) ────────────────────
615
626 int seq_id,
627 const std::vector<Message>& messages,
628 const GenerationParams& params)
629{
630 if (!is_active()) {
633 err.error_message = "generate_seq() requires ACTIVE state";
634 err.finish_reason = "error";
635 logger->error("{}", err.error_message);
636 return err;
637 }
638
639 auto start = entropic::log::now();
640 auto result = do_generate_seq(seq_id, messages, params);
641 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
642 result.seq_id = seq_id;
643 return result;
644}
645
658 int seq_id,
659 const std::vector<Message>& messages,
660 const GenerationParams& params,
661 std::function<void(std::string_view token)> on_token,
662 std::atomic<bool>& cancel)
663{
664 if (!is_active()) {
667 err.error_message =
668 "generate_streaming_seq() requires ACTIVE state";
669 err.finish_reason = "error";
670 logger->error("{}", err.error_message);
671 return err;
672 }
673
674 auto start = entropic::log::now();
675 auto result = do_generate_streaming_seq(
676 seq_id, messages, params, on_token, cancel);
677 result.generation_time_ms = entropic::log::elapsed_ms(start, entropic::log::now());
678 result.seq_id = seq_id;
679 return result;
680}
681
682// ── Default virtual implementations (v1.9.13) ─────────────
683
692 return false;
693}
694
702 BackendInfo bi;
703 bi.name = do_backend_name();
704 return bi;
705}
706
716 int /*seq_id*/, std::vector<uint8_t>& /*buffer*/) const
717{
718 return false;
719}
720
730 int /*seq_id*/, const std::vector<uint8_t>& /*buffer*/)
731{
732 return false;
733}
734
743 return false;
744}
745
756 int /*seq_id*/,
757 const std::vector<Message>& messages,
758 const GenerationParams& params)
759{
760 return do_generate(messages, params);
761}
762
775 int /*seq_id*/,
776 const std::vector<Message>& messages,
777 const GenerationParams& params,
778 std::function<void(std::string_view token)> on_token,
779 std::atomic<bool>& cancel)
780{
781 return do_generate_streaming(messages, params, on_token, cancel);
782}
783
784} // 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:774
GenerationResult generate_seq(int seq_id, const std::vector< Message > &messages, const GenerationParams &params)
Generate with explicit sequence ID.
Definition backend.cpp:625
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:302
float compute_perplexity(const int32_t *tokens, int n_tokens)
Compute perplexity for a token sequence.
Definition backend.cpp:455
virtual std::vector< GenerationResult > do_generate_batch(const std::vector< std::vector< Message > > &requests, const std::vector< GenerationParams > &params, std::atomic< bool > &cancel)
Subclass same-prefix batch generation (gh#98, v2.8.0).
Definition backend.h:535
std::string last_error_
Last error message for diagnostics.
Definition backend.h:726
virtual BackendInfo do_info() const
Populate backend metadata.
Definition backend.cpp:701
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:554
bool supports(BackendCapability cap) const
Query whether this backend supports a capability.
Definition backend.cpp:512
bool restore_state(int seq_id, const std::vector< uint8_t > &buffer)
Restore model state from buffer.
Definition backend.cpp:578
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:729
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:691
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:540
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:340
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:522
void unload()
Full unload (→ COLD).
Definition backend.cpp:139
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:602
virtual GenerationResult do_generate_seq(int seq_id, const std::vector< Message > &messages, const GenerationParams &params)
Generate with sequence ID.
Definition backend.cpp:755
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:235
GenerationResult generate(const std::vector< Message > &messages, const GenerationParams &params)
Generate a complete response.
Definition backend.cpp:182
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:742
int count_tokens(const std::string &text) const
Count tokens using model's tokenizer.
Definition backend.cpp:496
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:715
bool fire_model_load_hook(const ModelConfig &config)
Fire ON_MODEL_LOAD pre-hook.
Definition backend.cpp:471
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:265
LogprobResult evaluate_logprobs(const int32_t *tokens, int n_tokens)
Evaluate per-token log-probabilities for a token sequence.
Definition backend.cpp:397
bool load_and_activate(const ModelConfig &config)
Convenience: load() + activate().
Definition backend.cpp:165
GenerationResult complete(const std::string &prompt, const GenerationParams &params)
Raw text completion without chat template.
Definition backend.cpp:362
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:657
std::atomic< ModelState > state_
State transition slot accessible to subclasses for test-only injection.
Definition backend.h:752
@ ENTROPIC_ERROR_NOT_SUPPORTED
Capability not supported by this backend (v1.9.13)
Definition error.h:84
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
Definition error.h:39
@ ENTROPIC_HOOK_ON_MODEL_UNLOAD
14: Model unloaded from backend
Definition hooks.h:50
@ ENTROPIC_HOOK_ON_MODEL_LOAD
13: Model loaded into backend
Definition hooks.h:49
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.
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:302
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:148
std::filesystem::path path
Resolved model file path.
Definition config.h:149