大模型探索系列:LLM缓存优化与KVCache时间侧信道攻击

admin 2026-08-14 08:38:22 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文探讨大模型API的PromptCaching缓存机制,包括其工作原理、缓存命中与未命中的区别,以及从请求侧提升命中率的策略,如按稳定性梯度组织提示词、规范化序列化等。文章还介绍了KVCache时间侧信道攻击,攻击者可通过观察缓存命中与否推断用户输入前缀,属于安全风险。可操作建议包括优化提示词结构、实施粘性路由,并注意缓存隔离。 综合评分: 86 文章分类: AI安全,漏洞分析,威胁情报,安全工具,安全建设


cover_image

大模型探索系列:LLM缓存优化与KV Cache时间侧信道攻击

宁宇飞 宁宇飞

国家网络空间安全云社区

2026年7月27日 10:05 上海

在小说阅读器读本章

去阅读

现在做Agent工程基本都离不开LLM API,调用产生的开销是实打实要把控的硬成本,其中以缓存命中率尤为关键。

长期使用各类大模型接口的同行都能发现,同一段对话,缓存命中和未命中的花费差距巨大。我们判断服务商好不好,缓存效率与计费规则是很重要的参考标准。不少人觉得缓存策略完全由平台掌控,使用者无法优化,实则不然。

本文是“大模型探索系列”第一篇,分享大模型缓存的由来、如何自主提升缓存命中,还有可能存在的KV Cache时间侧信道攻击。

Prompt Caching 如何工作

Prompt Caching(提示词缓存,也称前缀缓存)用于复用多个请求之间相同的提示词前缀,从而减少重复的输入计算、降低首token延迟,并在支持的API中降低输入成本。

它缓存的不是模型生成的文本答案,也不是向量数据库式的语义检索结果。命中通常要求提示词从开头起具有完全相同的token前缀。

推理的两个阶段:Prefill 与 Decode

典型的大语言模型推理包含两个主要阶段:

Prefill(输入预填充)  模型处理全部输入token,计算每一层的中间表示以及注意力所需的Key/Value状态。

Decode(逐token生成)  模型逐步生成输出token,并复用此前已经计算的注意力状态。

一句话来说就是输入Prompt,到Prefill(处理输入并建立注意力状态),再Decode(逐 token 生成输出)。

如下图:

对于标准因果Transformer,在某一层中,位置 iii的状态受到此前token的影响。因此,只有当请求拥有完全相同的前缀时,之前计算的注意力状态才可以安全复用。

我们可以假设一个现象。假设应用在100个请求中重复发送相同的10,000-token系统指令或参考文档。如果没有跨请求缓存,服务端可能需要对这10,000个token重复执行100次输入计算。Prompt Caching的目的,就是避免重复计算相同前缀。

➢实际缓存的是什么

深入来说需要区分两种缓存:

请求内KV Cache

这是标准自回归生成中的缓存机制。在生成同一个响应时,模型保留已经处理过的token的注意力状态,避免每生成一个新token都重新处理全部历史内容。

跨请求Prompt Cache

跨请求缓存会保留可复用提示词前缀的模型中间状态。对于标准 Transformer,这通常表现为各注意力层的 Key/Value 张量,即 KV Cache。

这些状态可能保存在:

  • GPU 显存
  • 主机内存
  • GPU 本地高速存储;
  • 分布式或分层缓存系统。

具体存储方式属于模型服务商的实现细节,我们不能假定所有API都使用完全相同的缓存结构。

感兴趣想要深入了解这部分内容的读者可以参考OpenAI Prompt Caching文档。文档明确说明,其部分扩展保留策略会将KV张量卸载到GPU本地存储,而且不会把原始提示词文本写入该存储层。

➢接下来就是我们关注的缓存命中

缓存命令的一般过程存在一个逻辑门。

先收到API请求,第二步分词并生成前缀标识,然后查找最长的可复用前缀,这里开始出现分支。如果匹配到,就会读取缓存状态,然后只计算新增后缀最终生成输出。如果没有匹配到,就要计算完整的输入,并写入可缓存前缀,才会生成输出,给予我们响应内容。

如下图:

接下来详细拆解一下这个过程。

第一步:确定缓存键

系统需要为提示词前缀建立可比较的标识。实际实现可能使用:

  • token 块哈希
  • 递归或累计哈希
  • 前缀树
  • 路由键与前缀哈希的组合
  • 模型和请求配置等附加元数据

因此,下面的公式适合作为概念模型,但不应被描述为所有API的实际实现:

Hm=Hash(t0,t1,…,tm,metadata)

其中,metadata可能包括模型版本、工具定义、图片处理参数或缓存命名空间等信息。以OpenAI为例,请求会依据初始前缀的哈希进行路由;若提供prompt_cache_key,它也会参与路由和匹配。

第二步:精确前缀匹配

Prompt Caching的关键规则是:缓存命中依赖相同的token前缀,而不是语义相似。

如果两个请求的前1,000个token完全相同,这段前缀就可能被复用。

如果某处文本发生变化,分词结果可能从第一个不同token开始改变。该位置之后的更长前缀通常不能继续命中,但变化位置之前已经存在的较短缓存块仍可能复用。

例如:

Bash

请求 A:固定系统指令 + 文档版本 1 + 用户问题

请求 B:固定系统指令 + 文档版本 2 + 用户问题

在这个例子中,即使文档不同,二者仍可能命中前面的固定系统指令;并不一定是“整个缓存全部失效”。

第三步:复用前缀并计算后缀

这里就会出现一个Cache miss的现象,它其实就是指当用户的提示词或请求无法在系统内存中找到时,就会发生LLM缓存未命中(cache miss),迫使LLM完全从头开始处理输入。与“缓存命中”(cache hit)相比,这种情况会导致更高的计算成本、更慢的响应速度以及更多的 API Token消耗。

那么到了逻辑门可能出现的两个分支,决定了本次请求的成本。

  • Cache miss:处理完整输入,并可能把合格的前缀写入缓存。
  • Cache hit:复用已缓存前缀的中间状态,仅对未缓存的后缀执行必要的输入计算。

到了这一步,除了大幅影响Token的成本以外,还有一个开头提到了一个关键数据,如果发生了Cache Hit,此时不仅按较低的缓存读取费率计费,并且首字响应时间(TTFT,Time to First Token)也会大幅缩短。

➢API 提供的缓存控制方式

当前主流的 LLM API 主要提供自动缓存和显式断点两种方式,而且二者并不互斥。

自动缓存

开发者保持普通请求结构,由服务端自动识别可缓存前缀。

OpenAI对达到最低长度要求的提示词默认启用自动缓存。当前最低门槛为1,024 token;低于该门槛时,cached_tokens为0。

OpenAI建议把稳定内容放在前面,把用户相关或经常变化的内容放在末尾。图片、工具定义等内容若参与前缀,也必须保持一致。

如下面的JSON:

JSON {   “model”: “gpt-5.5”,   “messages”: [     {       “role”: “system”,       “content”: “这里是较长且稳定的系统指令……”     },     {       “role”: “user”,       “content”: “法国的首都是哪里?”     }   ] }

还有一类显式缓存断点

显式断点让开发者指定“可复用前缀到哪里结束”。比如Anthropic支持在内容块上放置 cache_control:

JSON {   “model”: ““,   “max_tokens”: 1024,   “messages”: [     {       “role”: “user”,       “content”: [         {           “type”: “text”,           “text”: “这里是稳定且很长的代码库或参考资料……”,           “cache_control”: {             “type”: “ephemeral”           }         },         {           “type”: “text”,           “text”: “请找出其中可能存在的内存泄漏。”         }       ]     }   ] }

cache_control标记表示此前的完整前缀可以作为缓存候选,而不是只缓存当前文本块。Anthropic目前同时支持顶层自动缓存和内容块级显式断点,默认TTL为5分钟,也提供收费更高的1小时TTL。此外,OpenAI当前的GPT-5.6及后续模型家族也支持显式 prompt_cache_breakpoint。

我们大致说完了缓存命中的定义,用下面一张表来概括和区分缓存命中成功与缓存失败。

从请求侧提升缓存命中

要系统性提高大语言模型API请求的提示词缓存命中率,提示词结构、工具声明、代理路由和数据序列化方式,应尽量符合供应商或自托管推理引擎的前缀缓存机制。

上面我们说过,提示词缓存通常复用模型在 Prefill 阶段为既有输入前缀计算出的KV状态。缓存能否命中,主要取决于模型最终看到的输入前缀是否与已有缓存前缀一致。如果某个较早位置的Token、内容块或影响提示词渲染的配置发生变化,则该位置之后的缓存通常无法继续复用,但变化点之前的较短稳定前缀仍可能命中。

这里需要注意的是,缓存命中并不受以下策略“保证”。首次请求、TTL到期、缓存驱逐、路由变化、最低长度不足以及供应商容量限制,都可能造成缓存未命中。

➢按“稳定性梯度”组织提示词

基本原则上面也讲过:

  • 越稳定、复用频率越高的内容越靠前;
  • 越动态的内容越靠后。

Bash [较稳定]

  1. 长期不变的系统或开发者指令
  2. 稳定的工具及函数 Schema
  3. 稳定的示例、政策或共享参考资料
  4. 已有对话历史
  5. 本次请求的动态上下文
  6. 当前用户问题

[较动态]

这样做可以最大化多个请求共享的连续前缀,使推理引擎能够复用更长的KV缓存,只对新增或变化的后缀执行Prefill。

这里有几个风险点:

  • 不同大模型厂商可能采用固定的内部顺序。例如Anthropic的前缀层级为tools→system→ messages。
  • 查询时临时检索出的RAG文档属于动态内容,不应一律放入长期静态前缀。
  • 如果参考资料按天、按版本或按租户变化,可分别建立稳定版本,并在相同版本内复用缓存。
  • 工具定义通常必须保留在正式的tools字段中,不能简单移入普通用户消息。

➢避免在稳定前缀中过早插入动态值

下面是一个反例:

Bash System: “You are a helpful assistant. Today is 2026-07-21. User ID is 84920.”

如果日期或用户ID位于长提示词的前部,它们的变化会使后续前缀无法复用。

比较合适的优化方案是:

SQL System: “You are a helpful assistant. Treat the trusted runtime-context block as request metadata.”

…稳定工具和参考资料…

Trusted runtime context: {   “date”: “2026-07-21”,   “user_id”: “84920” }

User: “What is on my schedule today?”

动态值应位于稳定前缀之后,但不能为了缓存而削弱安全边界。身份、权限和租户信息应由应用以可信结构注入,并在工具执行层再次验证,而不是完全依赖普通用户文本。

➢规范化模板和序列化过程

在Agent开发过程中,最好确保真正进入模型上下文的内容具有确定性:

  • 固定提示词模板版本;
  • 统一换行符、空白和Unicode规范化方式;
  • 保持工具数组及工具Schema的顺序稳定;
  • 保持结构化输出Schema稳定;
  • 对RAG文档采用确定性的排序和分块规则;
  • 避免由无序集合、随机ID或当前时间生成前缀内容;
  • 对会影响内部提示词的功能开关进行版本化。

如果应用主动把JSON序列化成提示词文本,排序对象键可以提高稳定性。但对于直接提交给供应商API的外层JSON,sort_keys=True并不是普遍成立的缓存要求,因为供应商通常会先解析请求,再按自己的格式构造模型输入。数组顺序和最终渲染内容通常比外层JSON对象键顺序更重要。

➢显式控制的key

刚才上面也说到了如OpenAI Responses/Chat API的prompt_cache_key和Anthropic Claude API的cache_control断点等,这部分可以详细参考官方文档。

➢实施粘性会话代理路由

还有一类,自托管的开源权重推理引擎(例如 vLLM、SGLang,或者像 New API / Bifrost 这样的多实例代理网关):

  • 一致性哈希 / 粘性路由: 把共享同一个前缀哈希(或相同租户system prompt)的请求路由到同一个worker节点。如果请求在10个 GPU 节点之间随机负载均衡,那么实际 KV cache 命中率会大幅下降,因为每个节点收到的都是冷请求。
  • 前缀缓存隔离与加盐: 当服务多个租户时,配置缓存隔离盐值,让租户特定前缀不会泄漏,同时还能让共享的 system 指令命中全局缓存块。

➢非常规手段

还有一种为了达到100%缓存命中的非常规手段,就是把提供方prompt caching和应用层缓存结合起来。可以参考下表:

安全时间:KV Cache时间侧信道攻击

终于到了安全时间,让我们先来看看KV Cache 时间侧信道攻击是什么意思?简而言之,通过一种攻击方式(通常是API调用),得知共享前缀缓存是否命中,会改变请求的计算量、调度行为或首Token延迟。攻击者观察这些外部差异,就可能推断某个候选Token前缀是否曾被其他请求处理。

下面是缓存侧信道在 LLM 推理系统中的变体:

严格来说,攻击者首先得到的只是:

这个完整Token 前缀可能存在于攻击者可访问的缓存隔离域中。

➢思考:攻击者真正探测的是什么

我们要知道,前缀缓存不是全文搜索引擎,也不是“某短语是否出现过”的集合。

假设受害者的最终 Prompt 是:

Bash [系统指令] [工具 Schema] [用户历史] Project Apollo merger details

攻击者发送:

Bash Project Apollo merger details

会造成什么后果 ?缓存一般不会命中,因为两者从第一个Token起就不同。

攻击者必须尽可能复现:

Bash 相同模型

  • 相同 Tokenizer
  • 相同聊天模板
  • 相同系统和工具前缀
  • 相同前序内容
  • 待验证候选内容

因此,缓存时间侧信道主要提供的是:前缀成员推断:候选前缀 (P|X) 是否已存在于共享缓存中。而不是:子串查询:短语X是否在任意用户对话的任意位置出现过。

➢基本攻击流程

阶段 1:受害者填充缓存

受害者发送:

Plain Text P || Secret || Suffix

其中:

  • P 是攻击者已知或能够猜测的公共前缀;
  • Secret 是未知内容;
  • Suffix 是后续内容。

服务端完成Prefill后,相关前缀可能留在共享KV Cache中。

阶段 2:攻击者构造候选

攻击者准备多个候选:

Plain Text P || Candidate_1 P || Candidate_2 P || Candidate_3

如果 Candidate_2 与受害者的秘密前缀一致,则它可能比其他候选多命中一个或多个缓存块。

阶段 3:观察侧信号

攻击者可能观察:

  • TTFT
  • 多个并发请求的返回顺序
  • 服务端报告的缓存 Token 数
  • 缓存驱逐行为
  • 请求是否被优先调度

阶段 4:统计分类

攻击者不能只依赖单次延迟,而会比较:

Bash [Tcandidate 与 Tknown-miss]

并使用:

  1. 重复采样
  2. 命中/未命中阈值
  3. 相对延迟
  4. 对照候选
  5. 假设检验或分类器

此时,如果某个候选的延迟分布显著偏低,攻击者便把它判断为可能命中。

➢主流的三类攻击

候选前缀成员推断

最简单的一类:

Plain Text 候选 A:Project Apollo was cancelled 候选 B:Project Apollo was approved 候选 C:Project Apollo was delayed

攻击者不一定能恢复完整 Prompt,但可能判断某个已知候选是否曾被处理。

适合此类攻击的目标包括:

  • 从有限集合中判断某种状态;
  • 判断用户是否访问过某份文档;
  • 判断某个已知模板是否被使用;
  • 推断已知格式中的枚举字段;
  • 对固定前缀后的日期、地点、部门或状态做候选测试。

这里得到的仍是“缓存域中存在该前缀”,不是对具体受害者身份的直接证明。

逐Token Prompt重建

如果攻击者已经恢复前缀 (P),可以尝试枚举下一 Token:

Plain Text P || token_1 P || token_2 P || token_3 …

若某个候选多命中一个Token或缓存块,则可能是正确延续。随后重复:

Plain Text 已恢复:P || token_i 下一轮:P || token_i || candidate_j

理论上可以逐步重建Prompt。但其实现实中存在显著困难:

  • 词表可能有数万至十几万个Token;
  • 一个错误Token会导致后续所有前缀错位;
  • 攻击者自己的探测会把错误候选写入缓存;
  • 目标缓存可能在枚举完成前被驱逐;
  • 公网延迟和队列噪声可能大于单Token差异;
  • 生产系统不允许攻击者清空或控制缓存;
  • 攻击者未必知道完整聊天模板和系统前缀。

调度顺序侧信道

侧信号不一定是绝对延迟。

攻击者同时提交:

Plain Text Dummy-before Candidate_1 Candidate_2 Candidate_3 Dummy-after

如果某个候选多匹配一个Token,调度器可能改变其服务顺序。攻击者观察返回顺序,就能获得“该候选是否匹配”的一位信息。

这类攻击利用的是:

Plain Text 秘密前缀   → 缓存匹配长度   → 调度优先级   → 返回顺序

而不只是:

Plain Text 秘密前缀   → Prefill 时间   → TTFT

➢攻击成立所需的关键条件

  1. 存在跨安全主体的缓存共享

若缓存严格按用户隔离:Victim Cache ≠ Attacker Cache

攻击者就无法通过自己的请求命中受害者的缓存。缓存共享可能分为(主流模型厂商):

  1. 攻击者能复现前缀

攻击者发送的提示词通常要求知道或猜中:

  • 系统模板;
  • Prompt 结构;
  • Tool Schema;
  • Tokenizer;
  • 固定上下文;
  • 已恢复的前序 Token。

正常来说,固定格式的业务应用比自由聊天更容易受到候选攻击。如(已知模板大幅降低搜索空间):

Bash Employee record: Name: […] Department: […] Access level: [候选值]

3. 请求进入同一缓存域或 Worker

即使逻辑上共享缓存,如果请求被路由到不同 Worker,而KV Cache没有跨节点共享,也不会命中。

反过来,以下路由方式会提高攻击可观测性:

  • 会话粘性路由
  • 前缀哈希路由
  • 缓存感知路由
  • 共享分布式 KV 存储

4. 缓存存活时间足够长

攻击者需要在以下情况发生前完成测量:

  • TTL 到期;
  • LRU 驱逐;
  • GPU 内存压力导致淘汰;
  • 模型重新部署;
  • Worker 重启;
  • 缓存被其他流量冲刷。

5. 侧信号足够可区分

命中的前缀越长,通常节省的Prefill计算越多,信号越强。

如果只多命中一个Token,而系统同时存在:

  • 强网络抖动
  • 大量并发
  • 动态批处理
  • 工具调用
  • 长排队

此时,单Token差异可能被噪声淹没。

➢系统提示词为什么可能成为目标?

系统提示词经常具备三个特征:

  • 稳定
  • 在大量请求中重复

这正是前缀缓存最容易复用的内容。

攻击者可能准备一个公开模板候选集:

Bash 框架 A 默认系统提示词 框架 B 默认系统提示词 版本 C 的安全护栏 某开源 Agent 模板

然后测试哪个候选出现缓存特征。这更准确地称为:

  1. 系统模板指纹识别;

  2. 候选系统 Prompt 成员推断;

  3. 在较强条件下的增量 Prompt 重建。

写在最后

总结来说,缓存之于LLM API是非常关键的成本与性能指标,为了深入探索可能复现出的KV Cache侧信道攻击的原理,本文从缓存原理、命中提升到攻击原理进行了梳理,对于观测与探测代码的编写,读者可自行通过大模型生成并横向对比多厂商的LLM API,获取更多观察结论。

– END –


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:国家网络空间安全云社区 宁宇飞 宁宇飞《大模型探索系列:LLM缓存优化与KV Cache时间侧信道攻击》

涉我资讯专刊-第60期 网络安全文章

涉我资讯专刊-第60期

文章总结: 涉我资讯专刊-第60期由网空闲话发布,面向国家强力部门定向报送,提供网络安全动态信息,包括网安动态、暗网论坛、智库和政济动态等,工作日出刊每期15条
评论:0   参与:  0