Entropic 2.11.1
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
38void apply_adapter(llama_context* ctx,
39 llama_adapter_lora* adapter, float scale)
40{
41 llama_set_adapters_lora(ctx, &adapter, 1, &scale);
42}
43
50void clear_adapters(llama_context* ctx) {
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 bool precondition_failed =
83 adapters_.find(name) != adapters_.end() || !model;
84 if (precondition_failed) {
85 logger->error("Cannot load adapter '{}': {}",
86 name, !model ? "base model is null" : "duplicate name");
87 return false;
88 }
89
90 auto t_start = now();
91 auto* lora = llama_adapter_lora_init(model, adapter_path.c_str());
92 if (!lora) {
93 logger->error("llama_adapter_lora_init failed for '{}' at {}",
94 name, adapter_path.string());
95 return false;
96 }
97
98 AdapterEntry entry;
99 entry.name = name;
100 entry.path = adapter_path;
101 entry.handle = lora;
102 entry.model = model;
103 entry.scale = scale;
104 entry.state = AdapterState::WARM;
105 adapters_.emplace(name, std::move(entry));
106
107 logger->info("Loaded adapter '{}' from {} in {:.1f}ms (scale={:.2f})",
108 name, adapter_path.string(),
109 elapsed_ms(t_start, now()), scale);
110 return true;
111}
112
113// ── Unload ──────────────────────────────────────────────────
114
126void AdapterManager::unload(const std::string& name, llama_context* ctx) {
127 std::lock_guard<std::mutex> lock(adapter_mutex_);
128
129 auto it = adapters_.find(name);
130 if (it == adapters_.end()) {
131 return;
132 }
133
134 auto& entry = it->second;
135 if (entry.state == AdapterState::HOT && ctx) {
136 clear_adapters(ctx);
137 active_name_.clear();
138 }
139
140 if (entry.handle) {
141 llama_adapter_lora_free(entry.handle);
142 entry.handle = nullptr;
143 }
144
145 entry.state = AdapterState::COLD;
146 logger->info("Unloaded adapter '{}'", name);
147
148 adapters_.erase(it);
149}
150
151// ── Activate ────────────────────────────────────────────────
152
167bool AdapterManager::activate(const std::string& name, llama_context* ctx) {
168 std::lock_guard<std::mutex> lock(adapter_mutex_);
169
170 auto it = adapters_.find(name);
171 bool cannot_activate =
172 it == adapters_.end() || it->second.state == AdapterState::COLD;
173 if (it != adapters_.end() && it->second.state == AdapterState::HOT) {
174 return true; // Already active — no-op
175 }
176 if (cannot_activate) {
177 logger->error("Cannot activate adapter '{}': {}",
178 name, it == adapters_.end() ? "not found" : "state is COLD");
179 return false;
180 }
181
182 auto& entry = it->second;
183
184 // Mark previous HOT adapter as WARM
185 if (!active_name_.empty() && active_name_ != name) {
186 auto active_it = adapters_.find(active_name_);
187 if (active_it != adapters_.end()) {
188 active_it->second.state = AdapterState::WARM;
189 }
190 }
191
192 apply_adapter(ctx, entry.handle, entry.scale);
193 entry.state = AdapterState::HOT;
194 active_name_ = name;
195
196 logger->info("Activated adapter '{}' (scale={:.2f})", name, entry.scale);
197 return true;
198}
199
200// ── Deactivate ──────────────────────────────────────────────
201
211void AdapterManager::deactivate(llama_context* ctx) {
212 std::lock_guard<std::mutex> lock(adapter_mutex_);
213
214 if (active_name_.empty()) {
215 return;
216 }
217
218 auto it = adapters_.find(active_name_);
219 if (it != adapters_.end()) {
220 if (ctx) {
221 clear_adapters(ctx);
222 }
223 it->second.state = AdapterState::WARM;
224 }
225
226 logger->info("Deactivated adapter '{}'", active_name_);
227 active_name_.clear();
228}
229
230// ── Swap ────────────────────────────────────────────────────
231
246bool AdapterManager::swap(const std::string& name, llama_context* ctx) {
247 std::lock_guard<std::mutex> lock(adapter_mutex_);
248
249 auto it = adapters_.find(name);
250 bool cannot_swap = it == adapters_.end()
251 || it->second.state == AdapterState::COLD;
252 if (active_name_ == name && !cannot_swap) {
253 return true; // Already active — no-op
254 }
255 if (cannot_swap || !fire_swap_hook(active_name_, name, it->second.path)) {
256 logger->error("Cannot swap to adapter '{}': {}",
257 name, cannot_swap ? "not found or COLD" : "cancelled by hook");
258 return false;
259 }
260
261 auto t_start = now();
262
263 // Mark current HOT as WARM
264 if (!active_name_.empty()) {
265 auto active_it = adapters_.find(active_name_);
266 if (active_it != adapters_.end()) {
267 active_it->second.state = AdapterState::WARM;
268 }
269 }
270
271 // Apply target
272 auto& target = it->second;
273 apply_adapter(ctx, target.handle, target.scale);
274 target.state = AdapterState::HOT;
275
276 std::string previous = active_name_;
277 active_name_ = name;
278
279 logger->info("Swapped adapter '{}' -> '{}' in {:.1f}ms",
280 previous, name, elapsed_ms(t_start, now()));
281 return true;
282}
283
284// ── Unload All ──────────────────────────────────────────────
285
298 llama_model* model, llama_context* ctx)
299{
300 std::lock_guard<std::mutex> lock(adapter_mutex_);
301
302 bool cleared_context = false;
303 std::vector<std::string> to_remove;
304
305 for (auto& [name, entry] : adapters_) {
306 if (entry.model != model) {
307 continue;
308 }
309
310 if (entry.state == AdapterState::HOT
311 && ctx && !cleared_context)
312 {
313 clear_adapters(ctx);
314 cleared_context = true;
315 }
316
317 if (entry.handle) {
318 llama_adapter_lora_free(entry.handle);
319 entry.handle = nullptr;
320 }
321
322 entry.state = AdapterState::COLD;
323 to_remove.push_back(name);
324 }
325
326 for (const auto& name : to_remove) {
327 if (name == active_name_) {
328 active_name_.clear();
329 }
330 adapters_.erase(name);
331 }
332
333 logger->info("Unloaded {} adapter(s) for model", to_remove.size());
334}
335
349 std::lock_guard<std::mutex> lock(adapter_mutex_);
350 size_t freed = 0;
351 for (auto& [name, entry] : adapters_) {
352 if (entry.handle) {
353 llama_adapter_lora_free(entry.handle);
354 entry.handle = nullptr;
355 ++freed;
356 }
357 entry.state = AdapterState::COLD;
358 }
359 adapters_.clear();
360 active_name_.clear();
361 if (freed > 0) {
362 logger->info("Unloaded all {} adapter(s) on shutdown", freed);
363 }
364}
365
366// ── Queries ─────────────────────────────────────────────────
367
375AdapterState AdapterManager::state(const std::string& name) const {
376 std::lock_guard<std::mutex> lock(adapter_mutex_);
377 auto it = adapters_.find(name);
378 if (it == adapters_.end()) {
379 return AdapterState::COLD;
380 }
381 return it->second.state;
382}
383
391AdapterInfo AdapterManager::info(const std::string& name) const {
392 std::lock_guard<std::mutex> lock(adapter_mutex_);
393 auto it = adapters_.find(name);
394 if (it == adapters_.end()) {
395 return {};
396 }
397 return make_info(it->second);
398}
399
406std::vector<AdapterInfo> AdapterManager::list_adapters() const {
407 std::lock_guard<std::mutex> lock(adapter_mutex_);
408 std::vector<AdapterInfo> result;
409 result.reserve(adapters_.size());
410 for (const auto& [name, entry] : adapters_) {
411 result.push_back(make_info(entry));
412 }
413 return result;
414}
415
424 std::lock_guard<std::mutex> lock(adapter_mutex_);
425 return active_name_;
426}
427
434void AdapterManager::set_hook_interface(const HookInterface& hooks) {
435 hooks_ = hooks;
436}
437
438// ── Private ─────────────────────────────────────────────────
439
447AdapterInfo AdapterManager::make_info(const AdapterEntry& entry) {
449 info.name = entry.name;
450 info.path = entry.path;
451 info.state = entry.state;
452 info.scale = entry.scale;
453 info.tier_name = entry.tier_name;
454 info.ram_bytes = entry.ram_bytes;
455 info.metadata = entry.metadata;
456 return info;
457}
458
473bool AdapterManager::fire_swap_hook(
474 const std::string& current,
475 const std::string& target,
476 const std::filesystem::path& target_path)
477{
478 if (!hooks_.fire_pre || !hooks_.registry) {
479 return true; // No hook registered — proceed
480 }
481
482 nlohmann::json ctx;
483 ctx["current_adapter"] = current;
484 ctx["target_adapter"] = target;
485 ctx["adapter_path"] = target_path.string();
486 std::string ctx_str = ctx.dump();
487
488 char* modified = nullptr;
489 int rc = hooks_.fire_pre(
490 hooks_.registry,
492 ctx_str.c_str(),
493 &modified);
494
495 if (modified) {
496 free(modified);
497 }
498
499 return rc == 0;
500}
501
502} // namespace entropic
AdapterManager — LoRA adapter lifecycle and hot-swap.
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 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).
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