Skip to content

Commit 1f769b2

Browse files
kmbandyclaude
andcommitted
feat: semantic KV cache tiering with embedding-driven prefetch (phases 1-4)
Phase 1-2: Real per-layer K/V tensor data movement via hipMemcpy/hipMemcpy2D. - hot->warm: K contiguous copy, V transposed gather (hipMemcpy2D) - warm->hot: K direct copy, V scatter back (hipMemcpy2D) - TieredKVLayer struct with per-layer tensor pointers and warm buffers - set_kv_layers_from_cache() wired from server init Phase 3: io_uring cold tier for NVMe direct I/O. - liburing integrated for async cold storage reads - SSD path for evicted KV chunks Phase 4: Semantic embedding-driven prefetch. - bge-small (33M) fingerprints evicted KV chunks - Cosine similarity prefetch hints on new prompt arrival - Two-stage cold->warm->hot promotion for semantic cache hits - semantic_top_k and prefetch hint routing fully wired Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c83747b commit 1f769b2

9 files changed

Lines changed: 659 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ poetry.toml
144144
/*.code-workspace
145145
/.windsurf/
146146
# emscripten
147-
a.out.*
147+
a.out.*x
148148
.gitnexus
149149
.claude/
150150
.agent.md

common/arg.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,27 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
13651365
params.kv_warm_device = value;
13661366
}
13671367
).set_env("LLAMA_ARG_KV_WARM_DEVICE").set_examples({LLAMA_EXAMPLE_SERVER}));
1368+
add_opt(common_arg(
1369+
{"--kv-semantic-index"}, "PATH",
1370+
"path to embedding model (GGUF) for semantic KV cache indexing (e.g. bge-small); empty = disabled",
1371+
[](common_params & params, const std::string & value) {
1372+
params.kv_semantic_index = value;
1373+
}
1374+
).set_env("LLAMA_ARG_KV_SEMANTIC_INDEX").set_examples({LLAMA_EXAMPLE_SERVER}));
1375+
add_opt(common_arg(
1376+
{"--kv-semantic-threshold"}, "THRESHOLD",
1377+
"minimum cosine similarity threshold for semantic prefetch hints (default: 0.65)",
1378+
[](common_params & params, const std::string & value) {
1379+
params.kv_semantic_threshold = std::stof(value);
1380+
}
1381+
).set_env("LLAMA_ARG_KV_SEMANTIC_THRESHOLD").set_examples({LLAMA_EXAMPLE_SERVER}));
1382+
add_opt(common_arg(
1383+
{"--kv-semantic-topk"}, "K",
1384+
"number of prefetch hints to return (default: 5)",
1385+
[](common_params & params, int value) {
1386+
params.kv_semantic_top_k = value;
1387+
}
1388+
).set_env("LLAMA_ARG_KV_SEMANTIC_TOPK").set_examples({LLAMA_EXAMPLE_SERVER}));
13681389
add_opt(common_arg(
13691390
{"-kvu", "--kv-unified"},
13701391
{"-no-kvu", "--no-kv-unified"},

common/common.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,9 @@ struct common_params {
584584
float kv_tier_attention_threshold = 0.1f; // attention threshold for eviction
585585
int kv_warm_device = -1; // HIP device index for warm KV tier (-1 = disabled)
586586
int kv_tier_total_ctx = 0; // full ctx budget across all tiers (set at load time)
587+
std::string kv_semantic_index = ""; // path to embedding model for semantic KV index (empty = disabled)
588+
float kv_semantic_threshold = 0.65f; // minimum cosine similarity threshold for prefetch hints
589+
int kv_semantic_top_k = 5; // number of prefetch hints to return
587590

588591
std::string hostname = "127.0.0.1";
589592
std::string public_path = ""; // NOLINT

plans/kv-embed-model.md

Lines changed: 148 additions & 0 deletions
Large diffs are not rendered by default.

src/llama-kv-cache-tiered.cpp

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,62 @@ bool llama_kv_cache_tiered::evict_tokens(uint32_t n_tokens_to_evict, llama_cache
374374
return false;
375375
}
376376

377+
// Apply semantic eviction weighting if we have a current query embedding
378+
if (!current_query_emb.empty() && !fingerprints.empty()) {
379+
std::vector<std::pair<llama_pos, float>> candidates_with_similarity;
380+
381+
for (auto pos : candidates) {
382+
float similarity = 0.0f; // Default similarity if no fingerprint found
383+
384+
// Find fingerprint for this position
385+
for (const auto& fp : fingerprints) {
386+
bool found = false;
387+
for (auto fp_pos : fp.positions) {
388+
if (fp_pos == pos) {
389+
found = true;
390+
break;
391+
}
392+
}
393+
if (found) {
394+
// Compute cosine similarity (dot product since both are L2-normalized)
395+
similarity = 0.0f;
396+
for (size_t i = 0; i < current_query_emb.size() && i < fp.embedding.size(); i++) {
397+
similarity += current_query_emb[i] * fp.embedding[i];
398+
}
399+
break;
400+
}
401+
}
402+
403+
candidates_with_similarity.emplace_back(pos, similarity);
404+
}
405+
406+
// Reorder candidates: positions with similarity < 0.3 evicted first,
407+
// positions with similarity > 0.65 moved to back of eviction queue
408+
std::sort(candidates_with_similarity.begin(), candidates_with_similarity.end(),
409+
[](const auto& a, const auto& b) {
410+
if (a.second > 0.65f) return false; // Keep high similarity at back
411+
if (b.second > 0.65f) return true; // Keep high similarity at back
412+
if (a.second < 0.3f) return true; // Evict low similarity first
413+
if (b.second < 0.3f) return false; // Evict low similarity first
414+
return a.second < b.second; // Otherwise by similarity
415+
});
416+
417+
// Update candidates with reordered positions
418+
candidates.clear();
419+
for (const auto& candidate : candidates_with_similarity) {
420+
candidates.push_back(candidate.first);
421+
}
422+
423+
// Count semantic eviction saves
424+
uint32_t saves = 0;
425+
for (const auto& candidate : candidates_with_similarity) {
426+
if (candidate.second > 0.65f) {
427+
saves++;
428+
}
429+
}
430+
stats.semantic_eviction_saves += saves;
431+
}
432+
377433
std::vector<llama_pos> to_warm, to_cold;
378434

379435
// Route to warm if slots available (RAM or GPU), otherwise cold
@@ -767,6 +823,145 @@ void llama_kv_cache_tiered::reset_stats() {
767823
stats.reset();
768824
}
769825

826+
void llama_kv_cache_tiered::add_fingerprint(const std::vector<llama_pos>& positions,
827+
const std::vector<float>& embedding,
828+
llama_cache_tier tier) {
829+
std::lock_guard<std::mutex> lock(mutex);
830+
831+
semantic_fingerprint fp;
832+
fp.positions = positions;
833+
fp.embedding = embedding;
834+
fp.tier = tier;
835+
fp.turn = fingerprint_turn++;
836+
837+
// Cap at 1000 entries - evict oldest when over cap
838+
if (fingerprints.size() >= 1000) {
839+
fingerprints.erase(fingerprints.begin());
840+
}
841+
842+
fingerprints.push_back(fp);
843+
}
844+
845+
std::vector<llama_kv_cache_tiered::PrefetchHint> llama_kv_cache_tiered::score_fingerprints(
846+
const std::vector<float>& query_emb, int top_k, float threshold) const {
847+
std::lock_guard<std::mutex> lock(mutex);
848+
849+
std::vector<PrefetchHint> results;
850+
851+
if (fingerprints.empty() || query_emb.empty()) {
852+
return results;
853+
}
854+
855+
// Compute cosine similarity between query_emb and each fingerprint
856+
// Since both are L2-normalized, cosine similarity = dot product
857+
for (const auto& fp : fingerprints) {
858+
// Compute dot product
859+
float dot_product = 0.0f;
860+
size_t min_size = std::min(query_emb.size(), fp.embedding.size());
861+
for (size_t i = 0; i < min_size; ++i) {
862+
dot_product += query_emb[i] * fp.embedding[i];
863+
}
864+
865+
// Check if above threshold
866+
if (dot_product >= threshold) {
867+
PrefetchHint hint;
868+
hint.positions = fp.positions;
869+
hint.score = dot_product;
870+
hint.current_tier = fp.tier;
871+
results.push_back(hint);
872+
}
873+
}
874+
875+
// Sort by score descending
876+
std::sort(results.begin(), results.end(),
877+
[](const PrefetchHint& a, const PrefetchHint& b) {
878+
return a.score > b.score;
879+
});
880+
881+
// Return top_k results
882+
if (static_cast<int>(results.size()) > top_k) {
883+
results.resize(top_k);
884+
}
885+
886+
return results;
887+
}
888+
889+
void llama_kv_cache_tiered::set_current_query_embedding(const std::vector<float>& emb) {
890+
std::lock_guard<std::mutex> lock(mutex);
891+
current_query_emb = emb;
892+
}
893+
894+
bool llama_kv_cache_tiered::save_fingerprints_to_disk(const std::string& path) {
895+
std::lock_guard<std::mutex> lock(mutex);
896+
897+
std::ofstream file(path, std::ofstream::binary);
898+
if (!file) {
899+
return false;
900+
}
901+
902+
// Write number of entries
903+
uint32_t n_entries = uint32_t(fingerprints.size());
904+
file.write(reinterpret_cast<char*>(&n_entries), sizeof(uint32_t));
905+
906+
// Write each fingerprint
907+
for (const auto& fp : fingerprints) {
908+
// Write positions count and positions
909+
uint32_t n_pos = uint32_t(fp.positions.size());
910+
file.write(reinterpret_cast<const char*>(&n_pos), sizeof(uint32_t));
911+
file.write(reinterpret_cast<const char*>(fp.positions.data()), n_pos * sizeof(llama_pos));
912+
913+
// Write embedding dimension and embedding
914+
uint32_t n_embd = uint32_t(fp.embedding.size());
915+
file.write(reinterpret_cast<const char*>(&n_embd), sizeof(uint32_t));
916+
file.write(reinterpret_cast<const char*>(fp.embedding.data()), n_embd * sizeof(float));
917+
918+
// Write tier and turn
919+
file.write(reinterpret_cast<const char*>(&fp.tier), sizeof(llama_cache_tier));
920+
file.write(reinterpret_cast<const char*>(&fp.turn), sizeof(uint64_t));
921+
}
922+
923+
return file.good();
924+
}
925+
926+
bool llama_kv_cache_tiered::load_fingerprints_from_disk(const std::string& path) {
927+
std::ifstream file(path, std::ifstream::binary);
928+
if (!file) {
929+
return false;
930+
}
931+
932+
// Read number of entries
933+
uint32_t n_entries;
934+
file.read(reinterpret_cast<char*>(&n_entries), sizeof(uint32_t));
935+
936+
// Clear existing fingerprints
937+
fingerprints.clear();
938+
939+
// Read each fingerprint
940+
for (uint32_t i = 0; i < n_entries; i++) {
941+
semantic_fingerprint fp;
942+
943+
// Read positions
944+
uint32_t n_pos;
945+
file.read(reinterpret_cast<char*>(&n_pos), sizeof(uint32_t));
946+
fp.positions.resize(n_pos);
947+
file.read(reinterpret_cast<char*>(fp.positions.data()), n_pos * sizeof(llama_pos));
948+
949+
// Read embedding
950+
uint32_t n_embd;
951+
file.read(reinterpret_cast<char*>(&n_embd), sizeof(uint32_t));
952+
fp.embedding.resize(n_embd);
953+
file.read(reinterpret_cast<char*>(fp.embedding.data()), n_embd * sizeof(float));
954+
955+
// Read tier and turn
956+
file.read(reinterpret_cast<char*>(&fp.tier), sizeof(llama_cache_tier));
957+
file.read(reinterpret_cast<char*>(&fp.turn), sizeof(uint64_t));
958+
959+
fingerprints.push_back(fp);
960+
}
961+
962+
return file.good();
963+
}
964+
770965
std::string llama_kv_cache_tiered::get_ssd_file_path(llama_pos pos) const {
771966
// Generate unique file path for token position
772967
return ssd_path + "/token_" + std::to_string(pos) + ".bin";

src/llama-kv-cache-tiered.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ enum llama_cache_tier {
2323
TIER_COLD // SSD - less frequently accessed context
2424
};
2525

26+
// Semantic fingerprint for KV cache tokens
27+
struct semantic_fingerprint {
28+
std::vector<llama_pos> positions; // token positions this covers
29+
std::vector<float> embedding; // normalized embedding vector
30+
llama_cache_tier tier; // TIER_WARM or TIER_COLD
31+
uint64_t turn; // conversation turn when evicted
32+
};
33+
2634
// Compression types for cold tier storage
2735
enum llama_cache_compression {
2836
COMPRESSION_NONE,
@@ -145,6 +153,8 @@ struct llama_tier_stats {
145153
uint64_t cache_hits;
146154
uint64_t cache_misses;
147155
double total_migration_latency_us;
156+
uint32_t semantic_prefetch_hits = 0;
157+
uint32_t semantic_eviction_saves = 0;
148158

149159
void reset() {
150160
hot_tokens = 0;
@@ -154,6 +164,8 @@ struct llama_tier_stats {
154164
cache_hits = 0;
155165
cache_misses = 0;
156166
total_migration_latency_us = 0.0;
167+
semantic_prefetch_hits = 0;
168+
semantic_eviction_saves = 0;
157169
}
158170

159171
double get_hit_rate() const {
@@ -211,6 +223,25 @@ class llama_kv_cache_tiered {
211223
bool contains_token(llama_pos pos) const;
212224
bool load_token(llama_pos pos, llama_cache_tier target_tier);
213225

226+
// Prefetch hint for semantic similarity
227+
struct PrefetchHint {
228+
std::vector<llama_pos> positions;
229+
float score;
230+
llama_cache_tier current_tier;
231+
};
232+
233+
// Fingerprint management
234+
void add_fingerprint(const std::vector<llama_pos>& positions,
235+
const std::vector<float>& embedding,
236+
llama_cache_tier tier);
237+
bool save_fingerprints_to_disk(const std::string& path);
238+
bool load_fingerprints_from_disk(const std::string& path);
239+
240+
// Semantic similarity scoring
241+
std::vector<PrefetchHint> score_fingerprints(
242+
const std::vector<float>& query_emb, int top_k, float threshold) const;
243+
void set_current_query_embedding(const std::vector<float>& emb);
244+
214245
// Statistics
215246
llama_tier_stats get_stats() const;
216247
void reset_stats();
@@ -242,6 +273,13 @@ class llama_kv_cache_tiered {
242273
// Statistics
243274
llama_tier_stats stats;
244275

276+
// Semantic fingerprints for evicted tokens
277+
std::vector<semantic_fingerprint> fingerprints;
278+
uint64_t fingerprint_turn = 0;
279+
280+
// Current query embedding for semantic eviction weighting
281+
std::vector<float> current_query_emb;
282+
245283
// Thread safety
246284
mutable std::mutex mutex;
247285

0 commit comments

Comments
 (0)