Skip to content

Commit 64533a2

Browse files
kmbandyclaude
andcommitted
mt:: + server — wipe tier state on whole-seq seq_rm (agent task lifecycle)
Setup for the agent army: long-running workers reuse a slot for many unrelated tasks throughout the day. Without explicit per-task cleanup, two failure modes accumulate: 1. Memory leak: warm slots from prior tasks stay occupied (the existing inline comment at the seq_rm site even acknowledged this — "we only free a warm slot when the warm copy itself becomes obsolete, e.g. via clear()" — but clear() only fires on whole-cache wipe, never per slot). 2. Worse: stale semantic fingerprints from prior tasks persist in the SemanticIndex. A new task's query embedding can cosine-match against a prior task's chunk and trigger restoration of that chunk's K/V from warm/cold — attention then reads garbage, model confabulates. This silent-failure mode would be hard to debug in production. Fix in two pieces: - mt::seq_rm: when called with the whole-seq sentinel (p0<0 or p1<0, i.e. "task done, wipe the slot"), clear warm_pos_to_slot_, evicted_to_warm_, warm_insertion_order_, cold_positions_, warm_recur_buf_, semantic_, refill warm_free_slots_, reset pressure_announced_. Logs the wipe count for visibility. n_seq_max=1 today so the wipe is global; will need per-seq scoping when mt:: grows multi-seq support. - server-context.cpp: self-healing kv_evict_through cursor in the proactive eviction trigger. When the cursor exceeds n_tokens, the slot was truncated (new task with no shared prefix wipes the cache via memory_seq_rm); reset to 0. Avoids having to instrument every memory_seq_rm site in the server. Smoke (Qwen3.6-35B-A3B-Q5, --kv-tier-semantic-index bge-small.gguf, --ctx-size 8192). Two-task isolation test with distinct needles: TASK A (drop NEEDLE_A, force eviction): backup [0,409) -> fingerprint, backup [409,818) -> fingerprint restore_semantic: 1 hint, 409 positions restored IMPLICIT WIPE (task B's prompt has no shared prefix): mt::seq_rm: whole-seq wipe for seq 0 — freed 818 warm slots, dropped 0 cold entries, cleared 2 semantic fingerprints TASK B (drop NEEDLE_B, ask for code): backup [0,409) -> fingerprint (FRESH content), backup [409,818) restore_semantic: 1 hint, 409 positions restored Model answer: "MERIDIAN-4920-SILVERFROST-PEREGRINE" (NEEDLE_B) NEEDLE_A nowhere in answer Without the fix, task B either 500'd ("Context size has been exceeded" because the stale cursor blocked eviction) or — worse — recalled NEEDLE_A from a stale fingerprint match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 3a29bc6 commit 64533a2

2 files changed

Lines changed: 54 additions & 4 deletions

File tree

src/memory-tier/mt-tiered.cpp

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -936,15 +936,54 @@ bool llama_memory_tiered::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos p1
936936
if (ok) {
937937
// Drop the removed positions from the eviction store. We keep
938938
// the warm copies — those are now the only source of truth for
939-
// restoration. (warm_pos_to_slot_ is unchanged here; we only
940-
// free a warm slot when the warm copy itself becomes obsolete,
941-
// e.g. via clear() or when the same position is re-added then
942-
// re-removed.)
939+
// restoration.
943940
if (p0 >= 0 && p1 > p0) {
944941
for (llama_pos p = p0; p < p1; ++p) {
945942
eviction_.remove(p);
946943
}
947944
}
945+
946+
// Whole-seq wipe (sentinel p0=-1 or p1=-1) is the "task done"
947+
// signal for an agent-style worker reusing a slot for unrelated
948+
// tasks. Free warm slots so VRAM/RAM is reclaimed, drop cold-tier
949+
// index entries (the on-disk file persists as scratch and gets
950+
// overwritten by the next eviction), and crucially wipe the
951+
// semantic fingerprint index — without this, fingerprints from
952+
// the prior task can cosine-match against the new task's queries
953+
// and trigger restoration of stale K/V from warm/cold, which
954+
// attention would then read as garbage.
955+
//
956+
// n_seq_max=1 today so "for this seq" === "all of it." When mt::
957+
// grows multi-seq tracking, scope these wipes by seq_id.
958+
if (p0 < 0 || p1 < 0) {
959+
const size_t n_warm = warm_pos_to_slot_.size();
960+
const size_t n_cold = cold_positions_.size();
961+
const size_t n_finger = semantic_.size();
962+
963+
warm_pos_to_slot_.clear();
964+
evicted_to_warm_.clear();
965+
warm_insertion_order_.clear();
966+
cold_positions_.clear();
967+
warm_recur_buf_.clear();
968+
semantic_.clear();
969+
pressure_announced_ = false;
970+
971+
if (warm_initialized_) {
972+
warm_free_slots_.clear();
973+
warm_free_slots_.reserve(warm_capacity_);
974+
for (uint32_t s = warm_capacity_; s-- > 0; ) {
975+
warm_free_slots_.push_back((int) s);
976+
}
977+
}
978+
979+
if (n_warm + n_cold + n_finger > 0) {
980+
LLAMA_LOG_INFO("mt::seq_rm: whole-seq wipe for seq %d — freed "
981+
"%zu warm slots, dropped %zu cold entries, "
982+
"cleared %zu semantic fingerprints\n",
983+
seq_id, n_warm, n_cold, n_finger);
984+
}
985+
}
986+
948987
update_tier_state();
949988
}
950989
return ok;

tools/server/server-context.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,17 @@ struct server_context_impl {
14901490
}
14911491

14921492
const int evict_threshold = (int)(cap * 0.80f);
1493+
1494+
// If the slot was truncated (new task with no shared prefix
1495+
// wipes the inner cache via memory_seq_rm), kv_evict_through
1496+
// is stale — it points past the now-much-smaller live cache.
1497+
// Reset to start eviction from scratch on this fresh slot.
1498+
// Self-heals without having to instrument every memory_seq_rm
1499+
// site in the server.
1500+
if ((int)slot.kv_evict_through > n_tokens) {
1501+
slot.kv_evict_through = 0;
1502+
}
1503+
14931504
// Live-in-hot count = total positions minus what we've already
14941505
// backed up. Without subtracting, the trigger keeps firing on
14951506
// every step once n_tokens passes the threshold but does nothing

0 commit comments

Comments
 (0)