Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
adapter_manager.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
17
18#include <llama.h>
19#include <nlohmann/json.hpp>
20
21#include <cstring>
22
23namespace entropic {
24
25namespace {
26auto logger = entropic::log::get("inference.adapter_manager");
29
39 llama_adapter_lora* adapter, float scale)
40{
41 llama_set_adapters_lora(ctx, &adapter, 1, &scale);
42}
43
51 llama_set_adapters_lora(ctx, nullptr, 0, nullptr);
52}
53
54} // anonymous namespace
55
56// ── Load ────────────────────────────────────────────────────
57
75 const std::string& name,
76 const std::filesystem::path& adapter_path,
77 llama_model* model,
78 float scale)
79{
80 std::lock_guard<std::mutex> lock(adapter_mutex_);
81
82 // gh#164 (v2.13.0): an entry left behind by `release_handles_for_model`
83 // is a REGISTRATION without a handle. Re-binding it against the reloaded
84 // model is exactly this call, so it is not a duplicate. An entry that
85 // still holds a handle is, and stays refused.
86 auto existing = adapters_.find(name);
87 const bool released = existing != adapters_.end()
88 && existing->second.handle == nullptr;
90 (existing != adapters_.end() && !released) || !model;
92 logger->error("Cannot load adapter '{}': {}",
93 name, !model ? "base model is null" : "duplicate name");
94 return false;
95 }
96
97 auto t_start = now();
98 auto* lora = llama_adapter_lora_init(model, adapter_path.c_str());
99 if (!lora) {
100 logger->error("llama_adapter_lora_init failed for '{}' at {}",
101 name, adapter_path.string());
102 return false;
103 }
104
105 // Creates the entry on a first load; on a gh#164 re-bind this is the
106 // SAME entry, so tier_name / metadata set elsewhere survive.
107 AdapterEntry& entry = adapters_[name];
108 entry.name = name;
109 entry.path = adapter_path;
110 entry.handle = lora;
111 entry.model = model;
112 entry.scale = scale;
113 entry.state = AdapterState::WARM;
114
115 logger->info("Loaded adapter '{}' from {} in {:.1f}ms (scale={:.2f})",
116 name, adapter_path.string(),
117 elapsed_ms(t_start, now()), scale);
118 return true;
119}
120
121// ── Unload ──────────────────────────────────────────────────
122
134void AdapterManager::unload(const std::string& name, llama_context* ctx) {
135 std::lock_guard<std::mutex> lock(adapter_mutex_);
136
137 auto it = adapters_.find(name);
138 if (it == adapters_.end()) {
139 return;
140 }
141
142 auto& entry = it->second;
143 if (entry.state == AdapterState::HOT && ctx) {
145 active_name_.clear();
146 }
147
148 if (entry.handle) {
149 llama_adapter_lora_free(entry.handle);
150 entry.handle = nullptr;
151 }
152
153 entry.state = AdapterState::COLD;
154 logger->info("Unloaded adapter '{}'", name);
155
156 adapters_.erase(it);
157}
158
159// ── Activate ────────────────────────────────────────────────
160
175bool AdapterManager::activate(const std::string& name, llama_context* ctx) {
176 std::lock_guard<std::mutex> lock(adapter_mutex_);
177
178 auto it = adapters_.find(name);
179 bool cannot_activate =
180 it == adapters_.end() || it->second.state == AdapterState::COLD;
181 if (it != adapters_.end() && it->second.state == AdapterState::HOT) {
182 return true; // Already active — no-op
183 }
184 if (cannot_activate) {
185 logger->error("Cannot activate adapter '{}': {}",
186 name, it == adapters_.end() ? "not found" : "state is COLD");
187 return false;
188 }
189
190 auto& entry = it->second;
191
192 // Mark previous HOT adapter as WARM
193 if (!active_name_.empty() && active_name_ != name) {
194 auto active_it = adapters_.find(active_name_);
195 if (active_it != adapters_.end()) {
196 active_it->second.state = AdapterState::WARM;
197 }
198 }
199
200 apply_adapter(ctx, entry.handle, entry.scale);
201 entry.state = AdapterState::HOT;
202 active_name_ = name;
203
204 logger->info("Activated adapter '{}' (scale={:.2f})", name, entry.scale);
205 return true;
206}
207
208// ── Deactivate ──────────────────────────────────────────────
209
220 std::lock_guard<std::mutex> lock(adapter_mutex_);
221
222 if (active_name_.empty()) {
223 return;
224 }
225
226 auto it = adapters_.find(active_name_);
227 if (it != adapters_.end()) {
228 if (ctx) {
230 }
231 it->second.state = AdapterState::WARM;
232 }
233
234 logger->info("Deactivated adapter '{}'", active_name_);
235 active_name_.clear();
236}
237
238// ── Swap ────────────────────────────────────────────────────
239
254bool AdapterManager::swap(const std::string& name, llama_context* ctx) {
255 std::lock_guard<std::mutex> lock(adapter_mutex_);
256
257 auto it = adapters_.find(name);
258 bool cannot_swap = it == adapters_.end()
259 || it->second.state == AdapterState::COLD;
260 if (active_name_ == name && !cannot_swap) {
261 return true; // Already active — no-op
262 }
263 if (cannot_swap || !fire_swap_hook(active_name_, name, it->second.path)) {
264 logger->error("Cannot swap to adapter '{}': {}",
265 name, cannot_swap ? "not found or COLD" : "cancelled by hook");
266 return false;
267 }
268
269 auto t_start = now();
270
271 // Mark current HOT as WARM
272 if (!active_name_.empty()) {
273 auto active_it = adapters_.find(active_name_);
274 if (active_it != adapters_.end()) {
275 active_it->second.state = AdapterState::WARM;
276 }
277 }
278
279 // Apply target
280 auto& target = it->second;
281 apply_adapter(ctx, target.handle, target.scale);
282 target.state = AdapterState::HOT;
283
284 std::string previous = active_name_;
285 active_name_ = name;
286
287 logger->info("Swapped adapter '{}' -> '{}' in {:.1f}ms",
288 previous, name, elapsed_ms(t_start, now()));
289 return true;
290}
291
292// ── Unload All ──────────────────────────────────────────────
293
307{
308 std::lock_guard<std::mutex> lock(adapter_mutex_);
309
310 bool cleared_context = false;
311 std::vector<std::string> to_remove;
312
313 for (auto& [name, entry] : adapters_) {
314 if (entry.model != model) {
315 continue;
316 }
317
318 if (entry.state == AdapterState::HOT
319 && ctx && !cleared_context)
320 {
322 cleared_context = true;
323 }
324
325 if (entry.handle) {
326 llama_adapter_lora_free(entry.handle);
327 entry.handle = nullptr;
328 }
329
330 entry.state = AdapterState::COLD;
331 to_remove.push_back(name);
332 }
333
334 for (const auto& name : to_remove) {
335 if (name == active_name_) {
336 active_name_.clear();
337 }
338 adapters_.erase(name);
339 }
340
341 logger->info("Unloaded {} adapter(s) for model", to_remove.size());
342}
343
358{
359 std::lock_guard<std::mutex> lock(adapter_mutex_);
360
361 bool cleared_context = false;
362 size_t released = 0;
363
364 for (auto& [name, entry] : adapters_) {
365 if (entry.model != model) { continue; }
366 if (entry.state == AdapterState::HOT && ctx && !cleared_context) {
368 cleared_context = true;
369 }
370 if (name == active_name_) { active_name_.clear(); }
371 if (entry.handle) {
372 llama_adapter_lora_free(entry.handle);
373 entry.handle = nullptr;
374 }
375 // The model is about to be freed; the registration is not.
376 entry.model = nullptr;
377 entry.state = AdapterState::COLD;
378 ++released;
379 }
380
381 logger->info("Released {} adapter handle(s); registrations kept "
382 "(gh#164)", released);
383}
384
398 std::lock_guard<std::mutex> lock(adapter_mutex_);
399 size_t freed = 0;
400 for (auto& [name, entry] : adapters_) {
401 if (entry.handle) {
402 llama_adapter_lora_free(entry.handle);
403 entry.handle = nullptr;
404 ++freed;
405 }
406 entry.state = AdapterState::COLD;
407 }
408 adapters_.clear();
409 active_name_.clear();
410 if (freed > 0) {
411 logger->info("Unloaded all {} adapter(s) on shutdown", freed);
412 }
413}
414
415// ── Queries ─────────────────────────────────────────────────
416
424AdapterState AdapterManager::state(const std::string& name) const {
425 std::lock_guard<std::mutex> lock(adapter_mutex_);
426 auto it = adapters_.find(name);
427 if (it == adapters_.end()) {
428 return AdapterState::COLD;
429 }
430 return it->second.state;
431}
432
440AdapterInfo AdapterManager::info(const std::string& name) const {
441 std::lock_guard<std::mutex> lock(adapter_mutex_);
442 auto it = adapters_.find(name);
443 if (it == adapters_.end()) {
444 return {};
445 }
446 return make_info(it->second);
447}
448
455std::vector<AdapterInfo> AdapterManager::list_adapters() const {
456 std::lock_guard<std::mutex> lock(adapter_mutex_);
457 std::vector<AdapterInfo> result;
458 result.reserve(adapters_.size());
459 for (const auto& [name, entry] : adapters_) {
460 result.push_back(make_info(entry));
461 }
462 return result;
463}
464
473 std::lock_guard<std::mutex> lock(adapter_mutex_);
474 return active_name_;
475}
476
484 hooks_ = hooks;
485}
486
487// ── Private ─────────────────────────────────────────────────
488
496AdapterInfo AdapterManager::make_info(const AdapterEntry& entry) {
498 info.name = entry.name;
499 info.path = entry.path;
500 info.state = entry.state;
501 info.scale = entry.scale;
502 info.tier_name = entry.tier_name;
503 info.ram_bytes = entry.ram_bytes;
504 info.metadata = entry.metadata;
505 return info;
506}
507
522bool AdapterManager::fire_swap_hook(
523 const std::string& current,
524 const std::string& target,
525 const std::filesystem::path& target_path)
526{
527 if (!hooks_.fire_pre || !hooks_.registry) {
528 return true; // No hook registered — proceed
529 }
530
531 nlohmann::json ctx;
532 ctx["current_adapter"] = current;
533 ctx["target_adapter"] = target;
534 ctx["adapter_path"] = target_path.string();
535 std::string ctx_str = ctx.dump();
536
537 char* modified = nullptr;
538 int rc = hooks_.fire_pre(
539 hooks_.registry,
541 ctx_str.c_str(),
542 &modified);
543
544 if (modified) {
545 free(modified);
546 }
547
548 return rc == 0;
549}
550
551} // namespace entropic
AdapterManager — LoRA adapter lifecycle and hot-swap.
long long elapsed_ms
Wall time, spawn to reap.
Definition bash.cpp:102
bool swap(const std::string &name, llama_context *ctx)
Swap to a different adapter atomically.
std::vector< AdapterInfo > list_adapters() const
List all known adapters.
bool activate(const std::string &name, llama_context *ctx)
Activate adapter on context (WARM -> HOT).
std::string active_adapter() const
Get the currently HOT adapter name.
void unload_all_for_model(llama_model *model, llama_context *ctx)
Unload all adapters for a given base model.
void release_handles_for_model(llama_model *model, llama_context *ctx)
Free a base model's adapter handles, KEEPING the registrations (gh#164).
void deactivate(llama_context *ctx)
Deactivate current HOT adapter (HOT -> WARM).
bool load(const std::string &name, const std::filesystem::path &adapter_path, llama_model *model, float scale=1.0f)
Load a LoRA adapter into RAM (COLD -> WARM).
void unload(const std::string &name, llama_context *ctx)
Unload adapter (any state -> COLD).
AdapterInfo info(const std::string &name) const
Get metadata for an adapter.
void unload_all()
Free every loaded adapter handle (gh#58 close-out, v2.3.0).
AdapterState state(const std::string &name) const
Get adapter state.
void set_hook_interface(const HookInterface &hooks)
Set hook interface for ON_ADAPTER_SWAP dispatch.
@ ENTROPIC_HOOK_ON_ADAPTER_SWAP
16: Adapter/LoRA swap requested
Definition hooks.h:59
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
Activate model on GPU (WARM → ACTIVE).
@ optional
optional: every escape asks the host's path approver
AdapterState
LoRA adapter lifecycle state.
Definition config.h:112
@ WARM
Loaded in RAM via llama_adapter_lora_init(). Ready to activate.
@ COLD
Not loaded. No resources consumed.
@ HOT
Active on context via llama_set_adapter_lora(). Influencing generation.
Metadata for a loaded LoRA adapter.
Definition config.h:126
std::string tier_name
Tier this adapter is assigned to (empty = unassigned)
Definition config.h:131
size_t ram_bytes
RAM consumption when WARM/HOT (0 if COLD)
Definition config.h:133
std::filesystem::path path
Resolved path to .gguf adapter file.
Definition config.h:128
AdapterState state
Current lifecycle state.
Definition config.h:129
std::string name
Unique adapter identifier.
Definition config.h:127
std::unordered_map< std::string, std::string > metadata
Adapter-specific metadata for routing decisions.
Definition config.h:136
float scale
LoRA scaling factor (alpha/rank)
Definition config.h:130