Files
llama.cpp/tools/server/server-cache-disk.cpp
T
Xuan Son Nguyen 5234b9d267 demo, wip
2026-08-18 00:43:50 +02:00

581 lines
18 KiB
C++

#include "server-cache-disk.h"
#include "common.h"
#include "llama.h"
#include "xxhash/xxhash.h"
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
namespace {
constexpr uint32_t SERVER_CACHE_DISK_MAGIC = 0x3143564B; // "KVC1"
constexpr uint32_t SERVER_CACHE_DISK_VERSION = 1;
// seed for the chained prefix hash - changing it invalidates all filenames
constexpr uint64_t SERVER_CACHE_DISK_CHAIN_SEED = 0x6b7663636861696eULL;
struct server_cache_disk_file_header {
uint32_t magic = SERVER_CACHE_DISK_MAGIC;
uint32_t version = SERVER_CACHE_DISK_VERSION;
uint64_t compat_hash = 0; // full 64-bit value (the filename only carries the low 32 bits)
uint64_t chain_hash = 0;
uint32_t n_tokens = 0;
uint32_t pad = 0;
uint64_t tokens_size = 0; // bytes of the server_tokens::serialize() section
uint64_t state_size = 0; // bytes of the llama_state_seq_get_data section
};
static_assert(sizeof(server_cache_disk_file_header) == 48, "unexpected header size");
std::string make_filename(uint64_t compat_hash, uint32_t n_tokens, uint64_t chain_hash) {
char buf[64];
snprintf(buf, sizeof(buf), "%08x-%u-%016" PRIx64 ".kvc", (uint32_t) compat_hash, n_tokens, chain_hash);
return buf;
}
bool parse_filename(const std::string & name, uint32_t & compat32, uint32_t & n_tokens, uint64_t & chain_hash) {
if (sscanf(name.c_str(), "%8x-%u-%16" SCNx64 ".kvc", &compat32, &n_tokens, &chain_hash) != 3) {
return false;
}
// reject padding/case/suffix variations by requiring the canonical spelling
return name == make_filename(compat32, n_tokens, chain_hash);
}
int64_t file_mtime(const std::filesystem::path & path) {
std::error_code ec;
const auto t = std::filesystem::last_write_time(path, ec);
return ec ? 0 : (int64_t) t.time_since_epoch().count();
}
uint64_t covered_key(uint32_t n_tokens, uint64_t chain_hash) {
const uint64_t buf[2] = { n_tokens, chain_hash };
return XXH64(buf, sizeof(buf), 0);
}
// walk the chained hash over the token list, invoking cb(n, h) at every valid prefix boundary:
// after each text token and after each complete media chunk (never mid-chunk)
// returns true if the walk reached n_max
bool tokens_chain_hash_walk(const server_tokens & tokens, size_t n_max, const std::function<bool(size_t, uint64_t)> & cb) {
uint64_t h = SERVER_CACHE_DISK_CHAIN_SEED;
size_t i = 0;
try {
while (i < n_max) {
const llama_token tok = tokens[i];
if (tok == LLAMA_TOKEN_NULL) {
// media chunk - fold in its content id instead of the placeholder token ids,
// otherwise different images would hash identically
const auto & chunk = tokens.find_chunk(i);
const char * id = mtmd_input_chunk_get_id(chunk.get());
const size_t n_tok = mtmd_input_chunk_get_n_tokens(chunk.get());
if (id == nullptr || id[0] == '\0' || n_tok == 0 || i + n_tok > n_max) {
return false;
}
std::vector<uint8_t> buf;
buf.reserve(5 + strlen(id));
buf.push_back(0x01);
for (int b = 0; b < 4; ++b) {
buf.push_back((uint8_t) (n_tok >> (8*b)));
}
buf.insert(buf.end(), id, id + strlen(id));
h = XXH64(buf.data(), buf.size(), h);
i += n_tok;
} else {
uint8_t buf[5] = { 0x00 };
memcpy(buf + 1, &tok, sizeof(tok));
h = XXH64(buf, sizeof(buf), h);
i += 1;
}
if (!cb(i, h)) {
return false;
}
}
} catch (const std::exception & e) {
SRV_WRN("failed to hash token list: %s\n", e.what());
return false;
}
return true;
}
} // namespace
server_prompt_cache_disk::server_prompt_cache_disk(const std::string & dir_, uint64_t compat_hash, bool has_mtmd, int32_t limit_mib, bool write_through) :
write_through(write_through),
dir(dir_.empty() || dir_.back() == DIRECTORY_SEPARATOR ? dir_ : dir_ + DIRECTORY_SEPARATOR),
compat_hash(compat_hash),
has_mtmd(has_mtmd),
limit_bytes(limit_mib < 0 ? 0 : 1024ull*1024ull*limit_mib) {
scan_dir();
}
void server_prompt_cache_disk::scan_dir() {
namespace fs = std::filesystem;
std::error_code ec;
for (const auto & ent : fs::directory_iterator(dir, ec)) {
if (!ent.is_regular_file(ec)) {
continue;
}
const std::string name = ent.path().filename().string();
// leftover temporary files from a previous crash
if (name.size() > 4 && name.compare(name.size() - 4, 4, ".tmp") == 0 && name[0] == '.') {
fs::remove(ent.path(), ec);
continue;
}
uint32_t compat32 = 0;
uint32_t n_tokens = 0;
uint64_t chain = 0;
if (!parse_filename(name, compat32, n_tokens, chain)) {
continue;
}
server_cache_disk_file file;
file.name = name;
file.chain_hash = chain;
file.n_tokens = n_tokens;
file.n_bytes = ent.file_size(ec);
file.mtime = file_mtime(ent.path());
total_bytes += file.n_bytes;
if (compat32 == (uint32_t) compat_hash) {
index[n_tokens][chain] = std::move(file);
} else {
foreign.push_back(std::move(file));
}
}
SRV_INF("disk prompt cache '%s': %zu usable entries, %zu from other configurations, %.3f MiB total (budget: %.3f MiB)\n",
dir.c_str(), n_files(), foreign.size(), total_bytes / (1024.0 * 1024.0), limit_bytes / (1024.0 * 1024.0));
}
size_t server_prompt_cache_disk::n_files() const {
size_t res = 0;
for (const auto & [n, files] : index) {
res += files.size();
}
return res;
}
server_cache_disk_file * server_prompt_cache_disk::find_file(uint32_t n_tokens, uint64_t chain_hash) {
const auto it = index.find(n_tokens);
if (it == index.end()) {
return nullptr;
}
const auto it_file = it->second.find(chain_hash);
return it_file == it->second.end() ? nullptr : &it_file->second;
}
const server_cache_disk_file * server_prompt_cache_disk::lookup(const server_tokens & tokens, size_t n_max) const {
if (index.empty()) {
return nullptr;
}
// no file can be longer than the largest indexed length - cap the walk
n_max = std::min<size_t>(n_max, index.rbegin()->first);
const server_cache_disk_file * best = nullptr;
tokens_chain_hash_walk(tokens, n_max, [&](size_t n, uint64_t h) {
const auto it = index.find((uint32_t) n);
if (it != index.end()) {
const auto it_file = it->second.find(h);
if (it_file != it->second.end()) {
best = &it_file->second;
}
}
return true;
});
return best;
}
void server_prompt_cache_disk::touch(const server_cache_disk_file & file) {
std::error_code ec;
std::filesystem::last_write_time(dir + file.name, std::filesystem::file_time_type::clock::now(), ec);
if (auto * f = find_file(file.n_tokens, file.chain_hash)) {
f->mtime = file_mtime(dir + file.name);
}
}
void server_prompt_cache_disk::forget(const server_cache_disk_file & file) {
// copy the fields first - the reference may point into the index entry being erased
const uint32_t n_tokens = file.n_tokens;
const uint64_t chain = file.chain_hash;
const uint64_t n_bytes = file.n_bytes;
const auto it = index.find(n_tokens);
if (it == index.end()) {
return;
}
if (it->second.erase(chain) > 0) {
total_bytes -= std::min<size_t>(total_bytes, n_bytes);
}
if (it->second.empty()) {
index.erase(it);
}
}
void server_prompt_cache_disk::remove_file(const server_cache_disk_file & file) {
SRV_WRN("disk prompt cache: removing '%s'\n", file.name.c_str());
std::error_code ec;
std::filesystem::remove(dir + file.name, ec);
forget(file);
}
void server_prompt_cache_disk::enforce_budget(const std::string & name_protected) {
if (limit_bytes == 0) {
return;
}
while (total_bytes > limit_bytes) {
// find the oldest file, ours and foreign alike
const server_cache_disk_file * oldest = nullptr;
bool oldest_foreign = false;
for (const auto & [n, files] : index) {
for (const auto & [h, file] : files) {
if (file.name != name_protected && (!oldest || file.mtime < oldest->mtime)) {
oldest = &file;
oldest_foreign = false;
}
}
}
for (const auto & file : foreign) {
if (file.name != name_protected && (!oldest || file.mtime < oldest->mtime)) {
oldest = &file;
oldest_foreign = true;
}
}
if (!oldest) {
break;
}
SRV_INF("disk prompt cache: size %.3f MiB over budget %.3f MiB, evicting oldest entry '%s'\n",
total_bytes / (1024.0 * 1024.0), limit_bytes / (1024.0 * 1024.0), oldest->name.c_str());
if (oldest_foreign) {
std::error_code ec;
std::filesystem::remove(dir + oldest->name, ec);
total_bytes -= std::min<size_t>(total_bytes, oldest->n_bytes);
foreign.erase(foreign.begin() + (oldest - foreign.data()));
} else {
remove_file(*oldest);
}
}
}
bool server_prompt_cache_disk::store(const server_tokens & tokens, const std::vector<uint8_t> & state_main) {
if (tokens.empty() || state_main.empty()) {
return false;
}
std::vector<std::pair<size_t, uint64_t>> bounds;
if (!tokens_chain_hash_walk(tokens, tokens.size(), [&](size_t n, uint64_t h) { bounds.emplace_back(n, h); return true; }) ||
bounds.empty() || bounds.back().first != tokens.size()) {
SRV_WRN("%s", "disk prompt cache: token list cannot be hashed, skipping\n");
return false;
}
const uint32_t n_tokens = (uint32_t) tokens.size();
const uint64_t chain = bounds.back().second;
if (auto * existing = find_file(n_tokens, chain)) {
SRV_TRC("disk prompt cache: '%s' already exists, refreshing\n", existing->name.c_str());
touch(*existing);
return true;
}
if (covered.count(covered_key(n_tokens, chain)) > 0) {
SRV_TRC(" - prompt with %u tokens is a prefix of an already persisted entry, skipping\n", n_tokens);
return true;
}
std::vector<char> tok_data;
try {
tok_data = tokens.serialize();
} catch (const std::exception & e) {
SRV_WRN("disk prompt cache: failed to serialize tokens: %s\n", e.what());
return false;
}
server_cache_disk_file_header header;
header.compat_hash = compat_hash;
header.chain_hash = chain;
header.n_tokens = n_tokens;
header.tokens_size = tok_data.size();
header.state_size = state_main.size();
const std::string name = make_filename(compat_hash, n_tokens, chain);
char tmp_buf[64];
snprintf(tmp_buf, sizeof(tmp_buf), ".%08x-%u.tmp", (uint32_t) (uintptr_t) this, tmp_counter++);
const std::string path_tmp = dir + tmp_buf;
const std::string path = dir + name;
{
std::ofstream out(path_tmp, std::ios::binary | std::ios::trunc);
out.write((const char *) &header, sizeof(header));
out.write(tok_data.data(), tok_data.size());
out.write((const char *) state_main.data(), state_main.size());
if (!out.good()) {
SRV_ERR("disk prompt cache: failed to write '%s'\n", path_tmp.c_str());
out.close();
std::error_code ec;
std::filesystem::remove(path_tmp, ec);
return false;
}
}
std::error_code ec;
std::filesystem::rename(path_tmp, path, ec);
if (ec) {
SRV_ERR("disk prompt cache: failed to rename '%s' to '%s': %s\n", path_tmp.c_str(), path.c_str(), ec.message().c_str());
std::filesystem::remove(path_tmp, ec);
return false;
}
server_cache_disk_file file;
file.name = name;
file.chain_hash = chain;
file.n_tokens = n_tokens;
file.n_bytes = sizeof(header) + tok_data.size() + state_main.size();
file.mtime = file_mtime(path);
total_bytes += file.n_bytes;
index[n_tokens][chain] = std::move(file);
for (const auto & [n, h] : bounds) {
covered.insert(covered_key((uint32_t) n, h));
}
SRV_INF("disk prompt cache: saved prompt with %u tokens, %.3f MiB to '%s'\n",
n_tokens, (sizeof(header) + tok_data.size() + state_main.size()) / (1024.0 * 1024.0), name.c_str());
SRV_DBG("%s", "__TEST_TAG_CACHE_DISK_STORE__\n");
enforce_budget(name);
return true;
}
server_prompt_cache_disk::load_status server_prompt_cache_disk::load(
server_cache_disk_file file, const server_tokens & tokens_new, llama_context * ctx, int32_t id_slot, server_tokens & tokens_out) {
const std::string path = dir + file.name;
std::error_code ec;
const uint64_t n_bytes = std::filesystem::file_size(path, ec);
if (ec) {
// deleted by another process - not an error, just a miss
forget(file);
return LOAD_MISS;
}
std::ifstream in(path, std::ios::binary);
if (!in.good()) {
forget(file);
return LOAD_MISS;
}
server_cache_disk_file_header header;
in.read((char *) &header, sizeof(header));
if (!in.good() ||
header.magic != SERVER_CACHE_DISK_MAGIC ||
header.version != SERVER_CACHE_DISK_VERSION ||
header.chain_hash != file.chain_hash ||
header.n_tokens != file.n_tokens ||
header.tokens_size % sizeof(llama_token) != 0 ||
sizeof(header) + header.tokens_size + header.state_size != n_bytes) {
SRV_WRN("disk prompt cache: '%s' is corrupt\n", file.name.c_str());
remove_file(file);
return LOAD_MISS;
}
if (header.compat_hash != compat_hash) {
// same low 32 bits, different configuration - leave the file for its owner
SRV_WRN("disk prompt cache: '%s' belongs to a different configuration, ignoring\n", file.name.c_str());
forget(file);
return LOAD_MISS;
}
llama_tokens packed(header.tokens_size / sizeof(llama_token));
in.read((char *) packed.data(), header.tokens_size);
if (!in.good()) {
SRV_WRN("disk prompt cache: '%s' is truncated\n", file.name.c_str());
remove_file(file);
return LOAD_MISS;
}
server_tokens loaded;
try {
loaded = server_tokens::deserialize(packed, has_mtmd);
} catch (const std::exception & e) {
SRV_WRN("disk prompt cache: failed to deserialize tokens from '%s': %s\n", file.name.c_str(), e.what());
remove_file(file);
return LOAD_MISS;
}
// the filename hash only proves an exact prefix probabilistically - verify against the actual tokens
if (loaded.size() != file.n_tokens ||
loaded.get_common_prefix(tokens_new) != file.n_tokens ||
!loaded.validate(ctx)) {
SRV_WRN("disk prompt cache: token mismatch in '%s' (hash collision?)\n", file.name.c_str());
remove_file(file);
return LOAD_MISS;
}
std::vector<uint8_t> state;
try {
state.resize(header.state_size);
} catch (const std::bad_alloc &) {
SRV_ERR("disk prompt cache: failed to allocate %" PRIu64 " bytes for '%s'\n", header.state_size, file.name.c_str());
return LOAD_MISS;
}
in.read((char *) state.data(), state.size());
if (!in.good()) {
SRV_WRN("disk prompt cache: '%s' is truncated\n", file.name.c_str());
remove_file(file);
return LOAD_MISS;
}
const size_t n = llama_state_seq_set_data_ext(ctx, state.data(), state.size(), id_slot, 0);
if (n != state.size()) {
SRV_WRN("disk prompt cache: failed to restore state from '%s' (%zu / %zu bytes)\n", file.name.c_str(), n, state.size());
// the sequence may hold a partial state now - clear it and let the caller recover
llama_memory_seq_rm(llama_get_memory(ctx), id_slot, -1, -1);
return LOAD_FAIL_SEQ_DIRTY;
}
tokens_out = std::move(loaded);
covered.insert(covered_key(file.n_tokens, file.chain_hash));
touch(file);
SRV_INF("disk prompt cache: restored prompt with %u tokens, %.3f MiB from '%s'\n",
file.n_tokens, state.size() / (1024.0 * 1024.0), file.name.c_str());
SRV_DBG("%s", "__TEST_TAG_CACHE_DISK_HIT__\n");
return LOAD_OK;
}
//
// compat hash
//
namespace {
template <typename T>
void hash_pod(std::string & blob, const T & value) {
static_assert(std::is_trivially_copyable<T>::value, "hash_pod requires a POD type");
blob.append((const char *) &value, sizeof(value));
}
void hash_str(std::string & blob, const std::string & value) {
blob += value;
blob += '\0';
}
// path + size + mtime: conservative, but never misses a changed file
void hash_file_meta(std::string & blob, const std::string & path) {
hash_str(blob, path);
std::error_code ec;
const uint64_t size = path.empty() ? 0 : (uint64_t) std::filesystem::file_size(path, ec);
hash_pod(blob, ec ? (uint64_t) 0 : size);
hash_pod(blob, path.empty() ? (int64_t) 0 : file_mtime(path));
}
} // namespace
uint64_t server_cache_disk_compat_hash(const common_params & params) {
std::string blob;
// format versions
hash_pod(blob, (uint32_t) SERVER_CACHE_DISK_VERSION);
hash_pod(blob, (uint32_t) LLAMA_STATE_SEQ_VERSION);
hash_pod(blob, (uint32_t) server_tokens::SERVER_TOKENS_STATE_VERSION);
// model identity
hash_file_meta(blob, params.model.path);
hash_file_meta(blob, params.mmproj.path);
for (const auto & la : params.lora_adapters) {
hash_file_meta(blob, la.path);
hash_pod(blob, la.scale);
}
// KV cache layout
hash_pod(blob, (int32_t) params.cache_type_k);
hash_pod(blob, (int32_t) params.cache_type_v);
hash_pod(blob, (uint8_t) params.swa_full);
// rope params change the KV content for the same tokens
hash_pod(blob, params.rope_freq_base);
hash_pod(blob, params.rope_freq_scale);
hash_pod(blob, (int32_t) params.rope_scaling_type);
hash_pod(blob, params.yarn_ext_factor);
hash_pod(blob, params.yarn_attn_factor);
hash_pod(blob, params.yarn_beta_fast);
hash_pod(blob, params.yarn_beta_slow);
hash_pod(blob, params.yarn_orig_ctx);
return XXH64(blob.data(), blob.size(), 0);
}