Skip to content

Storage and Eviction.zh

sikkatu edited this page Nov 2, 2026 · 9 revisions

English | Русский | 中文

存储与淘汰

预算模型

默认 1.5 TB 刻意多留一些:更多 prefill 历史保留在 NVMe 上, 因此重新打开对话时可节省首次加载的重算时间和能耗。

一个对话能有多长?

两个不同的上限不要混淆:

  • 单个对话的 token 上限(约 1M) —— 受模型上下文窗口限制 (模型 config.json 中的 max_position_embeddings),而非本缓存。
  • 缓存在磁盘上保留多少历史 —— 仅受 KVP_CACHE_BUDGET_GB 限制。 这里没有 token 上限:超出 100 万 token 后,有意义的只是单个上下文的 前缀复用边界(见「架构」页),而非 kv-persistence 本身的限制。
限制 默认值 执行机制
硬上限 KVP_CACHE_BUDGET_GB=1500 GB(1.5 TB) L2 适配器的 max_capacity_gb —— 超出的写入被拒绝
物理上限 共享 VM 磁盘(同盘还有 Docker 与模型权重) 设备的容量(预检确认) 通过 KVP_CACHE_DEVICE 检查可用空间
软阈值 预算的 0.9 trigger_watermark —— 自动淘汰从此开始
回收目标 一个周期后约为预算的 0.81(0.9 × 已用容量的 0.9) eviction_ratio 为已用容量的 0.1
命名空间配额 可选,quotas.json kvp-admin quota 策略层

预算仅涵盖持久 KV 缓存数据。模型权重、系统和其他文件不在此核算, kv-persistence 的任何脚本都不会触碰它们。

容量核算

  • KVP_CACHE_ROOT 下每个 KV 块对象一个文件(临时文件 + 原子重命名)。适配器 nixl_store_dynamic 写入扁平的 *.bin 文件 —— 没有按命名空间的子目录; 身份命名空间编码在文件名前缀中(净化后的模型名)。
  • 没有单独的磁盘索引 —— 确定性文件名本身就是索引,启动时惰性重建。 used_bytes 是真实缓存文件大小之和(加上遗留的 kvp-* 目录,如有); 没有单独的“索引开销”条目。
  • 写入的准入由 LMCache 适配器通过 max_capacity_gb(L2 硬上限)强制执行: 超出的写入被拒绝。仓库自带的 BudgetLedger 为报告和规划计算相同数字,但 未接入生产写入路径 —— 不要依赖它来拦截写入。
  • 重启后的核算(NEEDS TESTING,评审修正):缓存服务重启后,内存索引按 文件被访问惰性重建 —— 启动时并不扫描磁盘,因此真实文件仍在,而 used_bytes 从零开始。在硬件运行确认固定版本会把恢复的尺寸回填到 get_usage() 之前,不要假设重启前的文件计入 max_capacity_gb。必须的 测试:填满缓存 → 重启缓存服务 → 继续写入 → 验证 used_bytes 增长且保持 受限。
  • 崩溃残余:被强杀(SIGKILL、OOM、断电)后,适配器可能留下 <final>.bin.tmp.<uuid> —— 未能重命名的文件。它们不属于缓存索引 (绝不会作为 KV 返回),但确实占用磁盘,因此 status 单独报告它们 (tmp_files、tmp_files_bytes)。删除:purge --tmp --yes(保留所有真实 缓存文件)或 purge --all --yes。进行中的写入由活跃写入注册表 (kvpersist.tmp_guard)保护:每次写入在 tmp 文件出现前登记标记、原子重命名后 清除。标记是否活跃由写入进程是否存活判定(按 (pid, 进程启动时间) 核对, PID 复用无法蒙混)—— 已登记的写入无论持续多久都不会被删除(超过一小时的写入 同样受保护;判据是存活,而非年龄)。若无法判定存活(无 /proc、标记不可读) 则保留标记:宁可多留一个 tmp 文件,也不删除活跃写入的文件。写入者确已退出的 标记在一小时后清扫。对于不用注册表的写入者,额外以 15 分钟 mtime 窗口兜底: 近期 tmp 文件一律跳过。 随时查看报告:
bash scripts/budget-report.sh status

字段:used_bytes、free_bytes、usage_fraction、soft_pressure、 per_namespace_bytes、evictions、reclaimed_bytes、rejected_writes、 tmp_files、tmp_files_bytes。

自动淘汰(强制、始终开启)

  • 后台线程监视 get_usage()。超过 trigger_watermark 后,策略淘汰直至 用量下降 eviction_ratio。
  • 生产环境中真正运行的策略是 LRU(适配器原生,位于 LMCache 内部), 作用于已存块键。长且高频复用的历史仅受 LRU 新近度规则保护 —— 目前没有 额外的基于价值的防护。
  • 基于价值的排序(implementation/kvpersist/retention.py)是一个独立的 规划机制,未接入生产淘汰路径:它按新鲜度(24 小时衰减)、复用次数、 前缀长度和存储成本排序,并由单元测试覆盖 —— 但今天删除块的是 LMCache 自己的 LRU。不要指望长而有价值的前缀因它而幸存。
  • 存储压力告警:状态报告中 soft_pressure: true;启动器在文件系统空闲 空间低于预算时也会在启动时告警。

手动清理 —— 仅显式执行

bash scripts/budget-report.sh status                    # 查看真实命名空间名称
bash scripts/budget-report.sh purge --namespace=NAME --yes
bash scripts/budget-report.sh purge --all --yes
bash scripts/budget-report.sh purge --tmp --yes         # 仅崩溃残余

缓存布局是扁平的:*.bin 文件直接位于 KVP_CACHE_ROOT 下,命名空间是 文件名前缀(净化后的模型名,例如 --models--MyModel--a84fadf1)。真实名称 请从 status → namespaces 获取,不要猜测。由于真实名称通常以 - 开头, 请用 --namespace=NAME 形式或位置参数(purge --namespace=NAME --yes / purge NAME --yes),不要用 --namespace NAME。匹配是精确的(不做子串 匹配)。需要 --yes。删除范围严格限定在 KVP_CACHE_ROOT 内的缓存文件 —— 其他路径一律拒绝并返回退出码 2。启动绝不会自动清空缓存。

去重与共享块

LMCache 以内容派生键存储块:相同块内容 ⇒ 相同键 ⇒ 一个存储对象,而非两个。 具有相同稳定前缀的两个会话在物理上共享对象:

  • 同一兼容性命名空间 ⇒ 共享键;不同命名空间永不冲突。
  • 淘汰一个条目只删除其键;共享对象保留到没有任何其他键指向它为止。 若共享对象最终被淘汰(LRU 认为该键久未使用),并发正确性仍然保持: 缺失的查询只是重新计算 —— 活性上的取舍,绝非正确性问题。
  • 去重节省的存储可通过 used_bytes 前后对比衡量:content-key 模型由 tests/unit/test_dedup_model.py 覆盖(模拟,无 LMCache),真实 L2 文件由 tests/integration/test_l2_dedup_real.py 对活跃 lmcache server 计数 (共享前缀块只写入一次)。

命名空间与清理策略

缓存布局是扁平的:KVP_CACHE_ROOT 下每个 KV 块一个 *.bin 文件, 身份命名空间是文件名前缀(净化后的模型名,例如 --models--MyModel--a84fadf1)。 版本变更会创建新命名空间(新的文件名前缀);旧命名空间绝不自动删除 (策略已记录:清理是管理员的显式决定):

bash scripts/budget-report.sh status      # 列出命名空间名称("namespaces" 字段)
bash scripts/budget-report.sh purge --namespace=NAME --yes

请使用 --namespace=NAME(= 形式)或位置参数 —— 真实命名空间名通常**以 - 开头,--namespace NAME 会被解析成选项。

修改上限

  1. 在 configs/generic/cache.env.local 中修改 KVP_CACHE_BUDGET_GB。
  2. bash scripts/stop-cache.sh && bash scripts/start-cache.sh(数据保留)。
  3. 用 bash scripts/budget-report.sh status 确认。

Clone this wiki locally