多级缓存架构:推理结果缓存、特征缓存和模型缓存分层设计

📅 2026/7/19 0:44:54 👁️ 阅读次数 📝 编程学习
多级缓存架构:推理结果缓存、特征缓存和模型缓存分层设计

多级缓存架构:推理结果缓存、特征缓存和模型缓存分层设计

基础设施不需要漂亮话。当一个用户重复问同一个问题时,回答不该消耗昂贵的 GPU 算力。

一、从一次 3 秒的推理看缓存的层级

用户在一个知识库问答产品中输入问题"Kubernetes Pod 的 QoS 类型有哪些?"。系统经历了以下链路:

  1. 文本 Embedding 模型将问题向量化:45ms(GPU)
  2. 向量检索 Top-5 文档片段:12ms(CPU + 向量索引)
  3. LLM 推理生成回答:2800ms(GPU)

总计 2857ms,其中 98% 的时间花在 LLM 推理上。

5 分钟后,另一个用户问了完全相同的问题。链路完全重复,又消耗了 2800ms 的 GPU 推理时间。如果系统有任何一层缓存命中,这 2800ms 的 GPU 算力可以被省下来。

多级缓存的核心思想是:在请求链路的每一层都检查是否可以复用已有的计算结果。对于 RAG 场景,自然的缓存分层是:

层级缓存内容命中条件节省的算力
L1:结果缓存完整回答文本完全相同的输入LLM 推理(最昂贵)
L2:特征缓存文本 Embedding 向量相同的文本片段Embedding 推理 + 检索
L3:模型缓存模型权重文件模型已加载到显存模型加载时间
flowchart TD A[用户查询] --> B{L1: 结果缓存} B -->|命中| R1[直接返回回答<br/>延迟 < 5ms] B -->|未命中| C[文本 Embedding] C --> D{L2: 特征缓存} D -->|命中| E[跳过 Embedding<br/>直接检索] D -->|未命中| F[GPU Embedding 推理] F --> G[向量检索] E --> G G --> H[LLM 推理生成] H --> I[写入 L1 结果缓存] I --> R2[返回回答] style B fill:#e8f5e9 style D fill:#fff3e0 style R1 fill:#c8e6c9

二、L1:推理结果缓存的 Key 设计与语义匹配

结果缓存是最直接、收益最高的缓存层。但"完全相同的问题"在生产环境中并不常见——用户可能换个表述问同一件事。

精确匹配缓存:对原始输入做 SHA256 哈希作为 Key。命中率取决于用户群的提问习惯。对于搜索引擎类产品(用户倾向于输入关键词),精确命中率通常在 5%-15%。

语义匹配缓存:对输入做 Embedding 后,在缓存池中检索语义相似的已有回答。当相似度 > 0.95 时,直接复用。这种方式可以将命中率提升到 25%-40%,但代价是每次请求都需要计算一次 Embedding。

type ResultCache struct { exact *redis.ClusterClient // 精确匹配缓存 vector *VectorIndex // 语义缓存索引 } func (c *ResultCache) Get(ctx context.Context, query string) (*Answer, bool) { // 先尝试精确匹配 key := fmt.Sprintf("result:%x", sha256.Sum256([]byte(query))) if ans, err := c.exact.Get(ctx, key).Bytes(); err == nil { return unmarshalAnswer(ans), true } // 再尝试语义匹配 emb := embedding(ctx, query) candidates := c.vector.Search(emb, 5, 0.95) if len(candidates) > 0 { return candidates[0].Answer, true } return nil, false }

语义匹配缓存有一个陷阱:相似度阈值的设定。设得太高(>0.98),命中率提升不明显;设得太低(<0.90),可能出现"答非所问"。实践中 0.95 是经验上的安全阈值。

三、L2:特征缓存的细粒度复用

特征缓存主要针对文本 Embedding。在 RAG 流水线中,需要 Embedding 的数据有两类:

  1. 用户查询:每次请求都不同,缓存收益有限。
  2. 知识库文档片段:稳定不变,缓存收益极高。

对于知识库文档,正确的做法是在文档入库时计算并持久化 Embedding。但很多团队忽略了文档更新时的增量 Embedding——每次更新任意一个文档片段,都触发全量重新 Embedding。这导致了大量重复计算。

特征缓存的 Key 设计:

emb:{version}:{content_sha256}

其中version是 Embedding 模型的版本号。当模型升级(如从 ada-002 切换到 text-embedding-3-small)时,只需要修改 version 前缀,旧缓存自然失效,新缓存逐步填充。

特征缓存存储的数据量通常很大。一个 1536 维的 float32 向量占 6KB。100 万条知识库片段占用 6GB。如果使用 Redis 存储全部 Embedding,内存成本较高。折中方案是使用 Redis 作为热缓存,PostgreSQL + pgvector 作为全量持久化存储,二者通过 LRU 策略保持一致性。

四、L3:模型缓存的层级存储

模型缓存与其他缓存不同——它的"命中"不是按请求计算的,而是按 Pod 生命周期计算的。一个模型文件被加载到 GPU 显存后,只要 Pod 不回收,就不需要重复加载。

在这个层级,缓存优化的方向不是提高命中率,而是缩短命中后的加载路径

L3-A: GPU 显存(已加载) → 0ms(热 Pod) L3-B: 本地 NVMe → 1-60s(冷 Pod,本地有模型) L3-C: 网络存储 / 对象存储 → 2-20 分钟(冷 Pod,需下载模型)

这是典型的层级存储架构。关键策略:

  • Keep-Warm Pod 池:维护 N 个预加载模型的热 Pod,请求优先路由到热 Pod。
  • 节点亲和调度:推理调度器优先将 Pod 调度到已缓存该模型的节点。
  • 模型预热 DaemonSet:在 GPU 节点上运行 DaemonSet,提前将常用模型拉取到本地 NVMe。

对于多模型平台,一个节点可能同时需要缓存 10+ 个模型。本地 NVMe 容量有限,需要在 DaemonSet 中实现基于使用频率的淘汰策略:

磁盘使用率 > 80% → 淘汰最近 7 天未被加载过的模型 磁盘使用率 > 90% → 淘汰最近 24 小时未被加载过的模型 磁盘使用率 > 95% → 只保留当前正在运行的模型,触发告警

五、总结:缓存的 ROI 要从最贵的算力开始算

多级缓存架构的正确建设顺序是:先做最顶层(收益最大),再逐层向下延伸

  1. L1 结果缓存:ROI 最高,一行 Redis SET 能省下价值数美元的 GPU 推理。
  2. L2 特征缓存:ROI 中等,收益主要来自于避免知识库全量重新 Embedding。
  3. L3 模型缓存:ROI 体现在服务可用性上(减少冷启动时间),而不是单次请求的成本。

还有一个容易被忽视的设计点:缓存穿透的一致性保障。当 L1 结果缓存未命中时,应使用分布式锁或 singleflight 模式,避免相同请求并发穿透缓存层,造成 GPU 资源的重复消耗。Go 语言中golang.org/x/sync/singleflight是实现这个功能的推荐方案。

最后,所有缓存层都应该暴露 Prometheus 指标:命中率、缓存大小、淘汰数量。没有这些指标的多级缓存架构,等于在黑盒中做决策。