Commit 58a4a6b49 for llama.cpp
commit 58a4a6b49ccc5a8915da562e8f384760c817d8d9
Author: Aayan Karmali <44980366+StockerMC@users.noreply.github.com>
Date: Sun Nov 15 03:05:10 2026 -0500
server : clear slot cache when the global lora config changes (#30367)
diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp
index 0d613667d..45693f4e8 100644
--- a/tools/server/server-context.cpp
+++ b/tools/server/server-context.cpp
@@ -1955,21 +1955,17 @@ private:
}
bool launch_slot_with_task(server_slot & slot, server_task && task) {
- // process per-request lora adapters
- if (!task.params.lora.empty()) {
- auto task_loras = construct_lora_list(task.params.lora);
- if (!are_lora_equal(task_loras, slot.lora)) {
- // if lora has changed, check to see if the cache should be cleared
- if (lora_should_clear_cache(slot.lora, task_loras)) {
- SLT_TRC(slot, "clearing cache for lora change. %zu loras -> %zu loras\n", slot.lora.size(), task.params.lora.size());
- slot.prompt.clear();
- } else {
- SLT_TRC(slot, "keeping cache for alora. %zu target loras\n", task_loras.size());
- }
- slot.lora = task_loras;
+ // per-request lora if given, otherwise the global config
+ auto task_loras = task.params.lora.empty() ? params_base.lora_adapters : construct_lora_list(task.params.lora);
+ if (!are_lora_equal(task_loras, slot.lora)) {
+ // if lora has changed, check to see if the cache should be cleared
+ if (lora_should_clear_cache(slot.lora, task_loras)) {
+ SLT_TRC(slot, "clearing cache for lora change. %zu loras -> %zu loras\n", slot.lora.size(), task_loras.size());
+ slot.prompt.clear();
+ } else {
+ SLT_TRC(slot, "keeping cache for alora. %zu target loras\n", task_loras.size());
}
- } else {
- slot.lora = params_base.lora_adapters;
+ slot.lora = task_loras;
}
// if using alora, make sure it's only a single one requested and active
diff --git a/tools/server/tests/unit/test_lora.py b/tools/server/tests/unit/test_lora.py
index e21d9048b..c84ba52a9 100644
--- a/tools/server/tests/unit/test_lora.py
+++ b/tools/server/tests/unit/test_lora.py
@@ -66,6 +66,62 @@ def test_lora_per_request():
assert match_regex(re_test, res.body["content"])
+def test_lora_global_change_clears_slot_cache():
+ global server
+ server.n_slots = 1
+ server.cache_ram = 0
+ server.start()
+
+ # long enough that stale kv changes the output
+ prompt = "Look in thy glass and tell the face thou viewest, now is the time that face should form another, whose fresh repair if now thou not renewest, thou dost beguile the world, unbless some mother."
+
+ def set_global_lora(scale: float):
+ res = server.make_request("POST", "/lora-adapters", data=[{"id": 0, "scale": scale}])
+ assert res.status_code == 200
+
+ def complete(lora=None, cache_prompt: bool = True) -> dict:
+ data = {
+ "prompt": prompt,
+ "cache_prompt": cache_prompt,
+ "temperature": 0.0,
+ "seed": 42,
+ "n_predict": 16,
+ }
+ if lora is not None:
+ data["lora"] = lora
+ res = server.make_request("POST", "/completion", data=data)
+ assert res.status_code == 200
+ return res.body
+
+ set_global_lora(0.0)
+ expected_zero = complete(cache_prompt=False)["content"]
+ set_global_lora(1.0)
+ expected_one = complete(cache_prompt=False)["content"]
+ assert expected_zero != expected_one
+
+ # the global config changes between two requests that do not set "lora"
+ set_global_lora(0.0)
+ res = complete()
+ assert res["timings"]["cache_n"] == 0
+ assert res["content"] == expected_zero
+ set_global_lora(1.0)
+ res = complete()
+ assert res["timings"]["cache_n"] == 0
+ assert res["content"] == expected_one
+
+ # unchanged config keeps the cache
+ res = complete()
+ assert res["timings"]["cache_n"] > 0
+
+ # a request that sets "lora", followed by one that doesn't
+ res = complete(lora=[{"id": 0, "scale": 0.0}])
+ assert res["timings"]["cache_n"] == 0
+ assert res["content"] == expected_zero
+ res = complete()
+ assert res["timings"]["cache_n"] == 0
+ assert res["content"] == expected_one
+
+
@pytest.mark.skipif(not is_slow_test_allowed(), reason="skipping slow test")
def test_with_big_model():
server = ServerProcess()