标签 SGlang 下的文章

近日,国际顶级学术会议 NDSS 2026(Network and Distributed System Security Symposium)公布录用结果,奇安信技术研究院合作完成的5篇论文成功被录用。NDSS 2026将于2026年2月23日至27日在美国圣地亚哥举办。此次多篇论文被录用,充分展现了奇安信技术研究院在网络安全前沿技术研究领域的深厚实力。

1. 大语言模型(LLM)推理服务框架中的缓存安全问题

第一篇论文是由奇安信技术研究院、中国海洋大学和清华大学联合完成的AI安全研究工作,论文题目为《Cache Me, Catch You: Cache Related Security Threats in LLM Serving Frameworks》。这项工作由中国海洋大学和奇安信联合培养的硕士研究生吴祥凡在奇安信技术研究院联培期间主导完成,导师为应凌云博士(奇安信星图实验室)和曲海鹏教授(中国海洋大学),其他作者为陈国强(奇安信星图实验室),谷雅聪(清华大学)。这项研究聚焦于大语言模型(LLM)推理服务框架中的安全威胁,深入分析了 KV Cache、多模态缓存及语义缓存 三大核心机制。

这项工作揭示了上述机制中严重的安全隐患:攻击者可利用这些漏洞操纵模型输出,实施数据投毒,甚至绕过现有的安全审核与防御体系。团队在 vLLM、SGLang 和 GPTCache 等主流推理服务框架中定位到了具体的实现缺陷,并提出了针对性的修复方案。目前,相关漏洞已被厂商确认并修复,因此获得了 3 个 CVE 漏洞编号,为提升 LLM 基础设施的安全性做出了实质性贡献。

2. npm 生态漏洞传播影响分析

第二篇论文是由奇安信技术研究院和清华大学合作完成的关于软件供应链安全的工作。论文题目为《From Noise to Signal: Precisely Identify Affected Packages of Known Vulnerabilities in npm Ecosystem》,作者为蒲应元(奇安信星图实验室)、应凌云(奇安信星图实验室)和谷雅聪(清华大学)。这项研究提出了基于函数调用关系的细粒度漏洞传播关系识别方法,结果表明传统的基于包依赖关系识别的漏洞影响结果中约 70% 都是误报。

npm作为全球最大的开源软件生态,其错综复杂的依赖关系使得漏洞极易在供应链中传播,给软件安全带来巨大隐患。现有的包级别软件成分分析(SCA)工具普遍存在严重的误报问题,无法区分漏洞代码是否真实被调用,同时现有的函数级分析工具在面对大规模生态时,往往面临计算成本过高和对JavaScript动态特性支持不足等瓶颈。为解决这些问题,我们设计开发了 VulTracer,一款面向npm生态的高精度、可扩展的函数级漏洞传播分析框架。该工具创新性地提出了“一次分析,多次复用”的模块化分析模式,通过对每一个 npm 包构建不可变的富语义图(RSG)、提取形式化接口以及按需组合合成技术,成功解决了大规模静态分析中的性能与精度挑战。

同时,VulTracer基于全量npm生态(覆盖3400万个npm 包版本,超 9 亿条依赖关系)进行了迄今为止最大规模的函数级漏洞影响实证研究。实验结果表明,该工具在调用图构建上达到了0.905的F1分数(SOTA),相比npm audit降低了94%的误报率;同时研究结果进一步揭示,现有包级别分析工具产生的警报中 68.28% 均为“噪声”(即漏洞代码不可达),且真实的漏洞传播往往随依赖层级加深而迅速衰减。该工作为缓解开发者的警报疲劳提供了切实可行的技术路径,使安全修复工作能聚焦于真实存在的威胁。

3.JavaScript脚本的自动化反混淆

第三篇论文是由奇安信技术研究院和北京邮电大学合作完成的关于JavaScript反混淆的工作。论文题目为《From Obfuscated to Obvious: A Comprehensive JavaScript Deobfuscation Tool for Security Analysis》,这项工作由北京邮电大学和奇安信联合培养的卓越工程师计划博士研究生周董超在奇安信技术研究院联培期间主导完成,导师为应凌云博士(奇安信星图实验室)和王东滨教授(北京邮电大学),参与该项工作的还有柴华君(奇安信星图实验室)。这篇论文也是我们继PowerPeeler (CCS 2024)和Invoke-Deobfuscation (DSN 2022)之后的又一项脚本反混淆工作。

JavaScript作为互联网核心脚本语言的广泛应用,使其成为恶意攻击者的重要载体。攻击者利用复杂的代码混淆技术隐藏恶意行为,给安全分析带来严峻挑战。现有反混淆工具普遍存在处理复杂样本能力有限、仅支持特定混淆类型、输出代码难以阅读等关键局限。为解决这些问题,我们设计开发了JSIMPLIFIER,一款集成多阶段处理流程与大语言模型增强的综合性JavaScript反混淆工具。该工具创新性地结合代码预处理、静态AST分析与动态执行监控的双引擎协同,以及基于LLM的智能变量重命名,实现从复杂样本格式化到语义增强的全流程反混淆。JSIMPLIFIER基于44,421个真实混淆样本构建了目前最大规模数据集,实验证明其100%覆盖20种主流混淆技术,达到100%处理成功率和正确率,代码复杂度降低88.2%,可读性提升超过4倍。该工具已成功还原JSFireTruck等复杂恶意样本的混淆行为,相关研究成果、工具及数据集已开源共享。

4. Windows 代码签名滥用分析

第四篇论文是由奇安信技术研究院、清华大学和中关村实验室合作完成的关于代码签名滥用检测的工作。论文题目为《Understanding the Status and Strategies of the Code Signing Abuse Ecosystem》,这项工作由清华大学和奇安信联合培养的卓越工程师计划博士研究生赵汉卿主导完成,导师为段海新教授(清华大学)和应凌云博士(奇安信星图实验室)。其他作者分别为张一铭(清华大学)、张明明(中关村实验室)、刘保君(清华大学)、游子权(清华大学)、张书豪(奇安信星图实验室)。

近年来,软件供应链安全事件频发,为了保护软件真实性与完整性,代码签名机制应运而生。代码签名主要依赖公钥基础设施 PKI 技术,旨在确保软件来自真实来源且软件内容未被篡改。然而,攻击者有时会反过来利用代码签名PKI信任体系中的安全缺陷,帮助恶意软件绕过操作系统和杀毒软件的检查。深入理解代码签名滥用生态系统的演变过程以及滥用者的策略,对于完善相关检测与防御机制至关重要。

在这项工作中,我们利用从真实世界中收集的 3,216,113 个已签名的恶意 PE 文件,对代码签名滥用行为进行了大规模测量。通过细粒度的代码签名滥用检测分类算法,我们检测到了 43,286 张滥用证书,构建了迄今为止最大的滥用标记数据集。分析发现当前代码签名滥用现象普遍存在,影响了 46 家 CA 厂商以及 114 个国家或地区的证书。我们发现了 5 种滥用者的攻击策略,并根据当前代码签名 PKI 存在的安全缺陷提出了若干缓解措施。

5. 4G LTE 飞基站系统性安全评估

第五篇论文是由清华大学、奇安信技术研究院、CableLabs、Carleton University 及泉城实验室合作完成的关于 4G LTE femtocell 安全风险评估的研究工作。论文题目为 《Small Cell, Big Risk: A Security Assessment of 4G LTE Femtocells in the Wild》。该项工作由清华大学和奇安信联合培养的卓越工程师计划博士研究生杨雅儒主导完成,导师为段海新教授(清华大学、泉城实验室)和汤舒俊(奇安信技术研究院)。其他作者分别为张一铭(清华大学)、万涛(CableLabs & Carleton University)、常得量(奇安信技术研究院)、李义申(清华大学)。

近年来,为了提升室内覆盖、降低部署成本并分担宏基站流量,femtocell 作为一种小型、低功耗的运营商基站,被部署于个人家庭等场景。与传统基站不同,femtocell 通常直接接入公共互联网,并在物理与网络层面更容易被攻击者接触。一旦被攻破,femtocell 会以“受信任节点”的身份接入核心网,可能对用户隐私与核心网安全造成严重威胁。

这项工作对真实世界中的 4G LTE femtocell 进行了系统性的安全评估。我们分析了 4 款来自不同厂商的商用 femtocell 设备,从硬件与软件两个层面识别出 5 类可导致本地或远程攻破的共性漏洞。在此基础上,我们在受控实验环境中进一步分析了被攻破 femtocell 对用户侧业务的安全影响,验证了对数据业务、语音通话以及短信服务的威胁,例如用户通信内容的机密性可能在特定场景下面临风险。随后,我们进一步开展了互联网规模测量,基于 IKEv2、TR-069 及管理接口等协议特征,在全球 IPv4 空间中识别出 86,108 个疑似 femtocell 部署实例,其中超过六成为高置信度目标。研究结果表明,femtocell 在真实网络中的暴露程度与安全风险显著,一台被攻破的 femtocell 即可能成为攻击用户与核心网络的重要入口。基于上述发现,我们讨论了其安全影响,并提出了针对设备、部署与标准层面的缓解建议。

=========  我 是 分 割 线  =========

星图实验室隶属于奇安信技术研究院,专注于软件与系统安全的核心技术研究与系统平台研发,对外输出“天穹”软件动态分析沙箱、“天问”软件供应链分析平台、“天象”软件漏洞挖掘系统等核心能力和工具系统。

我们目前正在招聘,工作地点覆盖北京、南京、成都等城市,详情请参见:https://research.qianxin.com/recruitment/

0x01 研究背景

在自回归生成模型(Autoregressive Model)中,LLM每生成一个新token,都会将此前生成的序列作为输入。若每一步都重新计算全部注意力(Q、K、V 矩阵),计算量将随序列长度平方级增长。在长上下文和高并发场景下,这一开销会迅速成为系统瓶颈。为此,主流推理框架普遍引入KV-Cache技术。 KV-Cache通过缓存此前token的Key(K)和Value(V)向量,在下一步生成时只需计算新的Query(Q),即可直接复用前面的K/V,从而显著降低重复计算量。实践中,KV-Cache 通常能在保持模型精度不变的前提下,带来约5-8倍的推理加速。这一机制已经成为vLLM、SGLang、DeepSpeed-Inference等高性能推理引擎,以及Hugging Face generate(use_cache=True)接口的默认能力。

随着2024–2025年多租户推理服务(如vLLM、SGLang、TensorRT-LLM)的大规模部署,系统在单模型、多租户共享的前提下,又进一步引入跨请求的前缀缓存共享(prefix caching)。当不同请求的prompt存在相同前缀时,系统可以直接复用已有KV-Cache,大幅摊薄Prefill成本并提升吞吐。然而,当这种共享与复用机制扩展到多租户并发环境时,KV-Cache不再只是一个“性能优化组件”,而是演变成新的攻击面:攻击者可以通过观测Prefill 时间、TTFT等性能差异发起时序侧信道攻击,通过篡改缓存内容实施History Swapping(生成轨迹劫持),或者通过对Key向量注入扰动发动Cache Corruption(缓存腐败),从而导致跨租户信息泄露、话题漂移甚至下游任务性能显著下降。

0x02 KV缓存工作机制与共享复用原理

下面是KV-Cache工作原理的示意图。

KV-Cache工作原理

KV-Cache工作原理图

接下来我们用文字详细拆解,更深入了解KV缓存工作机制。

2.1 两阶段推理:Prefill与Decode

KV-Cache的核心做法分为两阶段。

(1)Prefill阶段(Prompt阶段)一次性计算输入序列的K/V并写入缓存 模型读取完整输入的prompt,计算出所有token的Key/Value向量并写入缓存。 公式表示为:

image-20251229205745845

此时缓存中的K/V向量构成了后续生成阶段的基础。

(2)Decode阶段(生成阶段)仅对新token计算Q/K/V,并复用历史K/V完成注意力计算 当模型生成新token时,仅需计算该token对应的Q、K、V向量。

image-20251229205757073

然后与缓存中已有的K/V拼接,直接完成注意力计算。这样便避免了重复计算前面N−1个token的注意力结果。

2.2 past_key_values

在Hugging Face Transformers框架中,KV-Cache在接口层面通过 past_key_values 对象实现。该对象并非一个抽象的控制开关,而是模型前向推理过程中实际生成、并可跨生成步骤复用的中间状态。它以分层的结构保存已处理历史Token的Key和Value张量,从而支撑自回归生成的增量计算。

从结构上看,past_key_values通常是一个长度为模型层数的列表或元组,其中每一层对应一对 (K, V)张量。不同模型的具体维度布局可能存在差异,但其核心语义一致:存储历史序列的注意力键值表示,以便后续生成时直接复用。

在推理流程中,Prefill 阶段会对完整的提示词进行计算,并首次生成past_key_values。进入Decode阶段后,若将此缓存作为输入传递给模型,模型通常只需为新输入的Token计算其对应的Key和Value,并将其追加至现有缓存末尾,从而避免了历史部分的重复计算。这种基于past_key_values的复用是框架的原生机制,其带来的加速直接源于注意力计算的真实削减,因此更适合作为评估系统性能及分析相关安全影响的工程基准。相比之下,通过sleep()或人为插桩制造“快慢差异”的方法仅能模拟现象,难以反映实际推理系统的缓存行为。此外,Transformers框架的generate()接口通常通过参数use_cache=True来启用此缓存机制。vLLM、SGLang、DeepSpeed-Inference在系统层面也普遍实现了类似机制,以降低生成延迟并提升吞吐量。

2.3 多租户场景下的前缀缓存与最长前缀匹配

在多请求并发且显存资源受限的推理服务中,为提升吞吐并降低重复的Prefill开销,系统常采用前缀缓存策略。其核心思想是当新请求的提示词(更准确地说是其Token序列)与某条已缓存的序列存在前缀重合时,系统可直接复用该前缀部分对应的KV-Cache,仅需对未命中的后续Token执行增量计算。

当缓存池中存在多个可能的候选前缀时,命中判定通常遵循最长前缀匹配(LPM)原则:在所有缓存条目中,系统会选择与新请求Token序列匹配长度最长的那一条作为复用对象,以最大化缓存利用率,减少重复计算。在工程实现上,这依赖于能够高效进行Token序列前缀匹配的数据结构或索引机制,例如前缀树(Trie)、基于前N个Token的分层哈希,或基于序列哈希值的多级索引。

根据匹配程度,命中效果可分为两类:一是完全命中,即请求的绝大部分或全部前缀已在缓存中,Prefill阶段的计算量显著下降;二是部分命中,即仅能复用较短的前缀,系统仍需对剩余后缀执行完整的Prefill计算。无论是“是否命中”还是“命中长度”,都会直接反映在可观测的系统性能指标上,例如Prefill时间、首Token延迟的分布等。

当前主流引擎(如vLLM的PagedAttention、LMCache)进一步通过分页管理和压缩技术缓解显存碎片,但前缀共享引入的侧信道与内存安全风险依然突出,这也是后续攻击面的根源。

0x03 KV-Cache的主要攻击面原理介绍

在理解KV-Cache的核心优化机制与共享原理后,我们可以看到其高效性背后隐藏的脆弱性。下面详解三大主要攻击面:时序侧信道攻击、操纵攻击与腐败攻击。

3.1 KV-Cache时序侧信道攻击

在共享KV-Cache的系统中,攻击者通过测量响应时间或请求处理顺序,推断缓存是否命中(hit),从而还原其他用户的Prompt(提示词)。

image-20251028173225854

时序侧信道攻击完流程图

设定还原的语句是"Imagine you are an IT expert",攻击者已经成功还原出"Imagine you are",并尝试还原下一个token "an"。下面我们根据上图分步骤拆解一下攻击过程。

步骤1:Generate candidates

攻击者在本地用小模型、模板或启发式方法生成可能的下一个token候选集合,例如:

  • Imagine you are an
  • Imagine you are a
  • Imagine you are the

把未知的victim prompt逐步转化为一系列候选前缀/后缀,便于后续probe。优点是减少搜索空间。


步骤2:Generate dummy

  • Candidate请求:每个请求包含一个候选后缀(比如Imagine you are an)。目标是看哪一个candidate与victim的缓存前缀最长匹配而“命中”缓存。
  • Dummy请求:随机或不相关的prompt(用来制造队列/填充调度槽位),以便控制调度顺序或避免直接暴露自己的probe请求导致缓存污染判断混淆。

步骤3:Send three request batches in turn

攻击者按这个顺序把三组请求发到服务器(可能是同一API key,也可能跨多个短时间窗口发出)。核心就是在调度队列里把candidate放在中间,观察它是否因为缓存命中而更快返回。

步骤4:Observe the returning order

攻击者记录三批请求的返回顺序和时间(TTFT/latency)。若candidate的响应比其前后的dummy显著更快或优先到达,就可推断该candidate是命中了缓存(即victim的prompt与该candidate共享较长前缀)。

3.2 History Swapping 攻击

image-20251230101845847

History Swapping操纵攻击原理图

攻击者通过结构化替换或注入KV-Cache内容,来“劫持”模型的生成轨迹,强制引导输出转向攻击者指定的主题或行为。这种攻击利用KV-Cache编码了不仅仅是上下文,还包括话题规划(topic trajectory)和结构化推理(structural planning)的特性。

设定攻击场景:受害者Prompt为“Give a precise technical explanation of espresso extraction variables”(讨论咖啡萃取),攻击者希望劫持输出到恒星生命周期主题。用户可见Prompt不变。

步骤1: 预生成目标主题KV-Cache

攻击者离线使用相同模型,基于目标主题Prompt生成一段完整KV-Cache块(topic_cache)。

步骤2: 启动正常生成并等待替换点

从受害者Prompt开始自回归生成,监控已生成token数,直到达到预设swap_token(例如序列的20%-60%处)。

步骤3: 执行块级覆盖替换

计算替换段长度(swap_percent,如25%-75%最近timestep),在全层(或指定早/晚层)用topic_cache对应部分直接覆盖当前缓存。

步骤4: 继续生成并观察劫持

模型基于篡改缓存继续输出。常见效果:立即/延迟主题偏移、原主题与攻击主题交替、或生成重复崩溃。

3.3 KV-Cache 腐败攻击

image-20251230101806716

KV-Cache腐败攻击原理图

攻击者通过向KV-Cache注入扰动(perturbation),破坏注意力机制的完整性,导致输出偏差、性能下降或幻觉增加。这种攻击视KV-Cache为“内存腐败”类似漏洞,扰动键向量(Key vectors)即可放大影响。

设定攻击场景:在正常生成或RAG任务中,攻击者向KV-Cache的Key向量注入扰动,导致注意力偏差、性能下降或幻觉增加。

步骤1: 选择目标层与时机

确定最脆弱层(通常中层,如LLaMA-2第12层)和扰动应用频率(连续或间歇)。

步骤2: 选择扰动变体

  • MTI-Gaussian:添加高斯噪声(σ=0.1-5.0)
  • MTI-Zeroing:概率置零Key条目
  • MTI-Rotation:施加正交旋转(15°-90°)
  • 可结合梯度优化以最大化目标影响

步骤3: 注入扰动到Key向量

在生成过程中,按选定策略对Key向量应用扰动δ。

步骤4: 观察输出效果

监控下一token分布偏移(KL散度上升)、下游任务性能下降15–30%、或RAG幻觉率增加5%-12%。中层扰动放大效果最显著。

0x04 代码实现

测试为纯CPU环境下完成,基于Python3.8+的Hugging Face Transformers与PyTorch运行124M参数的gpt2模型。

4.1 KV-Cache时序侧信道攻击

实验1:基础缓存时序测量

验证KV-Cache复用是否产生物理上可观测的时间差异。

我们实现了一个多租户LLM服务的 KVServer 类,支持:

  1. 最长前缀匹配 (LPM):实现类似vLLM的Prefix Caching
  2. 精确计时:仅测量Prefill阶段的KV 计算,排除tokenization开销
  3. 缓存管理:LRU淘汰策略

核心实现

@dataclass
class _CacheEnt:
    """KV-Cache 条目"""
    prompt: str
    input_ids: torch.Tensor
    past_kv: Tuple
    ts: float

class KVServer:
    """多租户KV-Cache服务器"""

    def _lpm(self, q_ids: torch.Tensor):
        """Longest Prefix Match - 缓存必须是查询的前缀"""
        best = None
        best_len = 0

        for cached, ent in self._cache.items():
            c_ids = ent.input_ids[0].tolist()
            q = q_ids[0].tolist()

            # 计算共同前缀长度
            mlen = 0
            for i, (a, b) in enumerate(zip(c_ids, q)):
                if a == b:
                    mlen = i + 1
                else:
                    break

            # 缓存有效条件:缓存是查询的前缀(mlen == len(cached))
            if mlen > best_len and mlen == len(c_ids) and len(c_ids) <= len(q):
                best = ent
                best_len = mlen

        return (best, best_len) if best else None

    def process(self, prompt: str, max_new=1, uid="anon", write_cache=True):
        """处理请求,返回详细的时序数据"""
        input_ids = self.tok.encode(prompt, return_tensors="pt")
        t0 = time.perf_counter()

        cache_r = self._lpm(input_ids)

        with torch.no_grad():
            if cache_r:
                # 缓存命中路径:复用past_key_values
                ent, matched = cache_r
                self._hits += 1

                if input_ids.shape[1] > matched:
                    # 部分匹配:计算增量部分
                    delta_ids = input_ids[:, matched:]
                    out = self.model(
                        delta_ids,
                        past_key_values=ent.past_kv,
                        use_cache=True
                    )
                    past_kv = out.past_key_values
                else:
                    # 完全命中:直接复用
                    past_kv = ent.past_kv

                prefill_t = (time.perf_counter() - t0) * 1000
                hit = True
            else:
                # 缓存未命中路径:完整前向传播
                self._miss += 1
                out = self.model(input_ids, use_cache=True)
                past_kv = out.past_key_values
                prefill_t = (time.perf_counter() - t0) * 1000
                hit = False

        # ... 生成阶段与缓存写回

运行结果

可以看到上面第一次请求Prefill用了大约380ms,这是模型执行完整前向传播的时间。对于GPT-2,这意味着要进行12层TransformerBlock的矩阵乘法运算。

然后当二次请求完全相同的时候Prefill仅仅为0.024ms ,几乎就只有内存操作时间。这个原因是因为past_key_values已存在,模型跳过了所有Attention层的Q×KT​计算,仅需简单的张量拼接。

实验2:prompt探测攻击

为了验证攻击者能否通过时序差异识别受害者的Prompt

核心代码:

def experiment_2_exact_match_attack():
    # 步骤1: 受害者缓存敏感Prompt
    victim_prompts = [
        "My secret password is hunter2",
        "My API key is sk-1234567890abcdef",
    ]

    for prompt in victim_prompts:
        server.process(prompt, uid="victim", write_cache=True)

    # 步骤2: 攻击者构造候选列表
    candidates = [
        "My secret password is hunter2",      # ✓ 匹配
        "My secret password is wrong",        # ✗ 不匹配
        "My secret code is hunter2",          # ✗ 不匹配
        "My API key is sk-1234567890abcdef",  # ✓ 匹配
        "My API key is sk-wrong-key",         # ✗ 不匹配
    ]

    # 步骤3: 逐个探测
    discovered = []
    for cand in candidates:
        r = server.process(cand, uid="attacker", write_cache=False)
        t = r['prefill_ms']

        if t < 1.0:  # 阈值判定
            discovered.append((cand, t))

    return discovered

运行结果

image-20251230222154860

我们可以从上面实验结果看到当探测内容与缓存完全一致时,模型无需任何计算,直接返回缓存指针。时间差异非常大。

我们可以从攻击者视角的视角来看到这件事:

1.攻击者构造"密码候选列表"(类似字典攻击),逐个探测。

2.只要有一个候选的响应时间<1ms,攻击者就能推断出被攻击者的完整prompt。

攻击简易流程图如下。

1

4.2 History Swapping攻击

实验目标:在不改变用户可见Prompt的情况下,通过替换推理过程中的past_key_values片段,把模型输出从“受害者话题”劫持到“攻击者话题”。

  • 受害者请求:正常的业务问题(例如“如何制作咖啡”)。
  • 攻击者能力


    • 能在同一推理进程/同一GPU的Worker内“写入或污染”共享的KV-Cache(例如:推理引擎实现了前缀缓存复用、调度/缓存对象复用存在隔离缺陷、或插件/监控/扩展组件可触达缓存对象)。
    • 攻击者提前离线生成一段目标主题的K-Cache。
    • 攻击效果:用户看到的prompt没变,但输出内容发生明显“叙事漂移”。

核心思路

  1. 攻击者用目标主题prompt跑一次Prefill,得到attacker_cache
  2. 受害者开始生成,达到某个swap_at_token时刻。
  3. 在所有层(或关键层)将受害者cache的一段时间步区间(如中间30%-60%)用attacker_cache的片段覆盖。

核心实现:

def gen_with_swap(model, tok, prompt: str, max_tok=30,
                 atk_cache=None, swap_at=2):
    """带缓存替换的生成函数"""
    ids = tok.encode(prompt, return_tensors="pt")
    generated = []

    # Prefill 阶段
    with torch.no_grad():
        out = model(input_ids=ids, use_cache=True)
        past = out.past_key_values
        logits = out.logits[0, -1, :]

    # 逐 token 生成
    for step in range(max_tok):
        tok_id = torch.argmax(logits).item()
        generated.append(tok_id)

        # 关键:在指定步数替换缓存
        if step == swap_at and atk_cache is not None:
            past = _swap_mix(past, atk_cache)

        nxt = torch.tensor([[tok_id]])
        with torch.no_grad():
            out = model(input_ids=nxt, past_key_values=past, use_cache=True)
            past = out.past_key_values
            logits = out.logits[0, -1, :]

    return tok.decode(generated, skip_special_tokens=True)

def _swap_mix(vic_cache, atk_cache):
    """混合策略:保留 10% 受害者前缀,替换中间 85% 为攻击者缓存"""
    from transformers.cache_utils import DynamicCache

    # 提取张量
    if hasattr(vic_cache, "key_cache"):
        v_k = [vic_cache.key_cache[i].clone() for i in range(len(vic_cache.key_cache))]
        v_v = [vic_cache.value_cache[i].clone() for i in range(len(vic_cache.value_cache))]
        a_k = [atk_cache.key_cache[i] for i in range(len(atk_cache.key_cache))]
        a_v = [atk_cache.value_cache[i] for i in range(len(atk_cache.value_cache))]
    else:
        # 兼容 tuple 格式
        v_k = [vic_cache[i][0].clone() for i in range(len(vic_cache))]
        v_v = [vic_cache[i][1].clone() for i in range(len(vic_cache))]
        a_k = [atk_cache[i][0] for i in range(len(atk_cache))]
        a_v = [atk_cache[i][1] for i in range(len(atk_cache))]

    new_cache = DynamicCache()

    for layer in range(len(v_k)):
        vk, vv = v_k[layer], v_v[layer]
        ak, av = a_k[layer], a_v[layer]

        seq_len = vk.shape[2]
        atk_len = ak.shape[2]

        # 替换策略:保留 10%,替换 10%-95%
        start = int(seq_len * 0.1)
        end = int(seq_len * 0.95)
        swap_sz = min(end - start, atk_len)

        nk = vk.clone()
        nv = vv.clone()

        if swap_sz > 0:
            # 关键:切片替换
            nk[:, :, start:start+swap_sz, :] = ak[:, :, :swap_sz, :]
            nv[:, :, start:start+swap_sz, :] = av[:, :, :swap_sz, :]

        new_cache.key_cache.append(nk)
        new_cache.value_cache.append(nv)

    return new_cache

image-20251229201348226

在我们进行swap_at_token 之后,输出出现明显话题漂移,原本应该是咖啡的制作方面的东西,结果话题漂移到了星空上面。

4.3 KV-Cache腐败攻击

实验:Cache Corruption(扰动Key 向量)

实验目标:在生成过程中对KV-Cache的Key张量注入扰动(噪声/置零/旋转),观察注意力机制被破坏后带来的输出质量退化(重复、语义漂移、幻觉倾向上升)。

更贴近实战的场景设定

  • 共享显存/共享推理Worker:攻击者通过越权写入或内存破坏类漏洞(例如缓存指针复用错误、越界写、错误的张量视图复用)影响到其他请求的KV。
  • RAG/Agent场景:缓存腐败会显著增加“把检索内容读错/拼接错”的概率,表现为幻觉或逻辑断裂。

扰动策略

  • corrupt_gaussian:K = K + N(0, σ^2)
  • corrupt_zeroing:以概p 将Key条目置零
  • corrupt_rotation:对Key的embedding子空间做正交旋转(简化实现为对最后维度两两旋转)

核心实现:

#高斯噪声
def corrupt_gaussian(cache, sig=1.0):
    """对 Key 向量添加高斯噪声:K = K + N(0, σ²)"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:  # 中层更敏感
            noise = torch.randn_like(k) * sig
            out.append((k + noise, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

#随机置零
def corrupt_zeroing(cache, p=0.3):
    """以概率 p 将 Key 条目置零"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:
            mask = (torch.rand_like(k) > p).float()
            out.append((k * mask, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

#正交旋转
def corrupt_rotation(cache, deg=45.0):
    """对 Key 的 embedding 子空间做正交旋转"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    rad = np.radians(deg)
    c, s = np.cos(rad), np.sin(rad)

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:
            nk = k.clone()
            d = k.shape[-1]
            # 对最后维度两两旋转
            for j in range(0, d - 1, 2):
                kj = k[:, :, :, j].clone()
                kj1 = k[:, :, :, j + 1].clone()
                nk[:, :, :, j] = c * kj - s * kj1
                nk[:, :, :, j + 1] = s * kj + c * kj1
            out.append((nk, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

实验结果如下

image-20251230223603113

可以看到在不同扰动策略下模型输出的内容发生明显变化。

0x05 防御与缓解措施

以下从架构、系统、审计三层总结主流缓解措施,结合最新研究(如SafeKV、KV-Cloak),旨在平衡安全性、性能与部署成本。

5.1 架构层防御

  • 租户级缓存隔离:通过Tenant ID、Session Scope或用户唯一标识符划分KV命名空间,完全禁止跨租户共享。适用于高敏感场景,虽牺牲部分吞吐,但彻底消除侧信道。
  • 选择性共享:仅允许非敏感前缀共享,结合细粒度隐私策略(如基于内容分类)决定复用范围。
  • 缓存生命周期管理:单次请求后自动清除,或设置TTL过期策略,减少驻留时间泄露风险。
  • LPM随机化与分区:在最长前缀匹配中引入随机扰动,或按哈希分区缓存池,打乱命中可预测性。

5.2 系统层防御

  • 噪声注入与延迟模糊化:在缓存命中路径插入±Δt随机延迟,或对时间指标添加噪声,隐藏TTFT/顺序差异(针对时序攻击)。
  • 缓存内容混淆:使用可逆矩阵变换对KV向量加密/混淆,仅授权方可逆转。
  • 扰动检测与完整性校验:实时监控KV向量范数/哈希变化,检测异常扰动(针对腐败攻击)或引入dropout-mask随机化/注意力平滑,减轻操纵影响。
  • 参数与接口限制:禁止外部暴露use_cache、position_ids等敏感参数;结合速率限制(throttling)阻断高频探测请求。
  • 机密计算集成:利用TEE(Trusted Execution Environment)加密KV-Cache内存,防止物理/侧信道访问。

5.3 审计与合规层

  • 缓存审计器:记录每条请求的缓存命中/共享日志,绘制租户-缓存命中矩阵,便于事后追溯。
  • 异常行为监控:基于机器学习检测异常调度模式(如批量相似前缀探测),自动告警或隔离。
  • 合规框架支持:在SOC 2、ISO 27001或GDPR下,强制日志不可篡改,并定期审计共享安全性。

参考资料

https://openreview.net/pdf?id=gUj2fxQcLZ

https://www.ndss-symposium.org/wp-content/uploads/2025-1772-paper.pdf

https://huggingface.co/docs/transformers/en/kv_cache

https://arxiv.org/abs/2312.07104

https://www.arxiv.org/pdf/2510.17098

https://arxiv.org/pdf/2511.12752

https://pub.towardsai.net/lets-build-an-optimizer-for-a-gpt-model-from-scratch-in-pytorch-kv-caching-4d3f1f9516fa

导读

阿里云 Tair KVCache 团队联合 SGLang HiCache Team 、蚂蚁 AI Infra-推理服务团队、阿里云服务器震旦异构计算团队,共同推出面向 Sparse Attention 的分层稀疏化框架,本文详细介绍该框架的架构设计与实现细节。

在前文《智能体式推理对 KVCache 的挑战与 SGLang HiCache 技术深度剖析》中,我们详细介绍了 HiCache 如何通过分层存储架构(GPU → CPU → 远端存储)突破 KVCache 的容量瓶颈,将有效缓存容量从 40GB 扩展至 TB 级别,使长上下文、高并发的 LLM 推理服务得以规模化部署。

然而,当上下文长度跨越 128K 甚至迈向百万 Token 时,两个新的瓶颈开始凸显:

  • 计算瓶颈:Attention 计算成本随序列长度线性增长,并受限于 HBM 带宽。动态稀疏注意力(DSA)通过"Select-then-Compute"范式选择 Topk Token 参与 Attention 计算,成功突破了这一瓶颈。
  • 容量瓶颈:引入 DSA 后,主要瓶颈从 HBM 带宽转移到了 HBM 容量——为确保低延迟,全量 KV Cache 仍需驻留 GPU,导致并发推理能力受限。

本文引入了分层稀疏化——将全量 KV Cache 存储在 CPU,GPU 中仅维护 Top-k 的 LRU Buffer——为破解这一双重约束提供了可行路径。本文将系统性介绍 SGLang 的分层稀疏化框架设计,包括:

  • 整体架构:SparseCoordinator、Algorithm、BackendAdaptor、SparseKVCacheManager 的模块化设计;
    *
  • 核心机制:Sparse Diff Kernel 的增量传输、I/O Kernel 的高性能传输优化;
  • 实践案例:DeepSeek DSA 的深度集成,实现单请求显存占用从 8GB 降至 200MB,3倍单机吞吐提升;
    分层稀疏化标志着 KVCache 管理范式的又一次跃迁:从 HiCache 的"分层存储 → 扩展容量",到本文的"稀疏化 + 分层 → 突破带宽与容量双重约束",为超长上下文推理开辟了全新的技术路径。(注:此项目目前处于开发阶段,尚未正式发布。)

本系列技术文章将系统性拆解面向智能体推理的KVCache技术演进路径:

1.智能体式推理对 KVCache 的挑战与 SGLang HiCache 技术深度剖析

2.3FS-KVCache 工程化落地:企业级部署、高可用运维与性能调优实践

3.Hybrid Model Support:SGLang 对 Mamba-Transformer 等混合架构模型的支持方案

4.Tair KVCache Manager:企业级全局 KVCache 管理服务的架构设计与实现

5.KVCache 仿真分析:高精度的计算和缓存模拟设计与实现

  1. 本文|Hierarchical Sparse Attention:分层稀疏注意力框架下的 KV 分层管理与按需加载
  2. 展望:KVCache驱动的软硬结合演进

1.引言:双重瓶颈与协同破解之道

1.1 HiCache:容量扩展的胜利与新的战场

在前文《智能体式推理对 KVCache 的挑战与 SGLang HiCache 技术深度剖析》中,我们详细介绍了 HiCache 如何通过分层存储架构(GPU 显存 → CPU 内存 → 3FS 远端存储)突破 KVCache 的容量瓶颈。通过智能的热度感知调度与异步预取机制,HiCache 将原本仅有 40GB 的 GPU 显存扩展至 TB 级别的有效缓存容量,使得长上下文、高并发的 LLM 推理服务得以实现规模化部署。

在真实生产环境中,HiCache 已展现出显著价值:缓存命中率从 40% 提升至 80%,平均 TTFT 下降 56%,推理 QPS 提升 2 倍。

然而,当我们将目光投向更极致的长上下文场景——例如 128K 甚至百万级别的上下文窗口时,新的瓶颈开始浮现。

1.2 长上下文的 HBM 带宽墙:从线性增长到稀疏化破局

1.2.1 Attention 计算的带宽瓶颈

在长上下文推理中,Attention 计算呈现出独特的性能特征。每个 Decode 步骤需要将完整的 KV Cache 从 HBM 加载到计算单元,执行Attention计算。由于 KV Cache 随序列长度线性扩展,Attention 计算成本也随之线性增长。

关键问题在于 Attention 的计算强度(Arithmetic Intensity) 过低——相对于海量的访存操作,实际的浮点运算量不足以饱和 GPU 的计算单元。这使得 Attention 成为典型的 memory-bound 操作,Attention 计算受限于 HBM 带宽。随着上下文长度从 32K 扩展至 128K 甚至百万级别,这一带宽瓶颈成为长上下文推理的主要性能制约因素。

1.2.2 动态稀疏注意力(DSA)的 Select-then-Compute 范式

为突破带宽瓶颈,动态稀疏注意力算法(Dynamic SparseAttention, 后文简称 DSA)从 Attention 机制的本质特性出发:在自回归生成过程中,并非所有历史 Token 都对当前输出贡献相同的权重。研究发现,Attention 分布往往呈现显著的长尾特征——少数关键 Token 占据了绝大部分的 Attention Score,而大量 Token 的贡献可以忽略不计。更重要的是,这些关键 Token 的集合在不同 Query 间动态变化,无法通过静态规则预先确定。

DSA 将这一观察转化为 "Select-then-Compute" 工作流,通过三个协同阶段实现稀疏化:

  • 分块与元数据抽象:将 KV Cache 划分为固定大小的块(block size 通常为 32 tokens),每个块维护轻量级的元数据结构。这些元数据可以是 Key 向量的统计摘要(均值、方差)、Bounding Box(每维度的最大/最小值)、或经过降维的紧凑表示。元数据的存储开销通常不到完整 KV Cache 的 1%,可以常驻 GPU 内存。
  • 快速重要性评估:对于每个新生成的 Query Token,算法无需访问完整的 Key Cache,而是基于元数据快速计算每个块的 Criticality Score。这一评估过程的计算量远小于完整 Attention(通常为 O(n/32) vs O(n)),且可以高效并行化。随后通过 Top-k 选择算法(如 heap-based selection),筛选出最相关的 k 个块(典型值 k=2048,对应 64 个块)。
  • 按需稀疏计算:仅对选中的 Top-k 块加载其完整的 Key 和 Value Cache,执行标准的 Scaled Dot-Product Attention。未被选中的块则完全跳过,避免了不必要的 HBM 访问。

代表性 DSA 算法包括

  • Quest:训练无关的启发式算法,利用 Query-Key Bounding Box 的几何关系近似估计 Attention Score 上界。通过维护每个块在各维度上的 Key 最大/最小值,Quest 可以在不访问完整 Key 的情况下,快速排除不重要的块。
  • ClusterKV: 将 Prefill 阶段的所有 Keys 向量进行聚类(如 K-means),生成 C 个聚类中心;每个原始 key 被映射到其最近的 centroid; Decode 阶段 Query 跟聚类中心计算,获取最具相关的 Topk。
  • DeepSeek DSA:作为模型原生的稀疏注意力机制,通过专门训练的 Indexer 模块动态预测 Token 重要性,Indexer 的输出直接指导 Top-k 选择。

1.3 隐形的显存墙:稀疏计算的容量困境

尽管稀疏注意力在计算层面取得突破,但其执行流程存在固有的先后依赖关系:

在 Stage 1 中,算法需要评估每个 Token/Page 的重要性(计算 );在 Stage 2 中,基于评分选择 Top-k;只有在 Stage 2 完成后,Stage 3 才知道应该对哪些 KV 进行计算。

这一依赖链导致了一个根本性问题:在确定 Top-k 之前,系统无法预知需要哪些 KV 数据,因此必须将全量 KVCache 保留在 GPU 中。

关键约束:稀疏化实现了计算复杂度的降低(O(n) \rightarrow O(k)),但显存占用复杂度依然保持 O(n);也即采用 DSA 后,主要性能瓶颈从 HBM 带宽转移到了 HBM 容量;这一容量约束导致:

  1. HBM 容量利用率低下(Poor HBM Capacity Utilization):98.4% 的 KV Cache 在每步中未被访问,却占据宝贵的 HBM 空间。
  2. 并行能力受限(Limited Parallelism):小 Batch Size 无法充分发挥 GPU 的并行计算能力,推理吞吐难以提升;例如对于 DeepSeek V32,单个 128K 请求的 Latent Cache 占 8 GB,H200 扣除模型权重后,最多只能支持 Batch=5,这严重限制了 GPU 并行计算能力的发挥。
  3. 分层存储价值受阻(Value Blockage):传统的 KV Cache Offload 方案要求所有数据在 Decode 前加载到 HBM,无法与 DSA 的动态选择特性协同工作。

1.4 分层稀疏化:存储与计算的协同优化

破解显存墙的关键洞察在于:既然 Attention 计算只需要 Topk 部分,何不只在 GPU 中存储 Topk 部分,并结合 CPU HICache,在计算完 Topk 后动态加载增量的 Topk 部分?

分层稀疏化的关键是改变 KVCache 的存储位置和加载时机(下面以 DeepSeek DSA 为例):

  • 传统流程:完整 Latent Cache 必须驻留在 GPU 显存中。Decode 阶段执行 Indexer 选择 Top-2k,然后对选中的部分进行 Attention 计算。单个 128K 请求,虽然理论计算量降低了 60+ 倍,但显存占用依然是 O(n),占用 8 GB,H200 最多支持 Batch=5;
  • 分层稀疏化流程:Prefill 后将完整 Latent Cache(8 GB)Offload 至 Host 内存,GPU 仅保留轻量 Sparse Indexer 元数据。Decode 时基于元数据在 GPU 执行 Indexer 选择 Top-2k,Host 筛选对应的 Latent 子集并增量传输至 GPU,最后执行 Attention 计算;

    • 单请求 GPU 显存占用降至 < 200 MB,单 GPU 可支持$B_{max} = \min \left( \frac{M_{host}}{M_{req}}, B_{SLO} \right)$
    • 其中,$M_{host}$代表单卡可分配的最大 CPU 内存容量;B\_{SLO}代表满足 SLO 延迟要求的最大 Batch Size。

核心优势:

  • 完整 KVCache 存储在 Host,突破 GPU 显存物理空间限制;
  • GPU 侧只需存储轻量 Sparse 元数据和 Topk 部分 KVCache,Req 显存占用从O(n)降至 O(k);
  • 高性能传输:结合 HICache IO Kernel 实现 Topk Cache 高性能传输,单层 IO 延迟控制在 us 级别;并结合 Overlap 能力将 IO 延迟隐藏在计算中。

分层稀疏化不仅解决了计算问题,更从根本上破解了显存容量的刚性约束,实现了计算效率与存储效率的协同优化,为超长上下文推理开辟了全新的技术路径。

2.SGLang 分层稀疏化框架设计

2.1 整体框架设计

SGLang 的分层稀疏化框架采用模块化、可插拔的三层架构设计,通过标准化接口实现算法解耦、后端兼容与非侵入式集成。框架核心由以下模块构成:

  • SparseCoordinator(协调层):通过生命周期钩子编排三大功能模块的协同工作

    • Algorithm(算法层):提供可插拔的 Top-k 选择策略实现;
    • BackendAdaptor(适配层):完成稀疏索引到物理地址的转换与后端对接;
    • SparseKVCacheManager(传输层):基于 Diff & IO Kernel 实现 Host-GPU 间的高效、增量数据传输。
  • RequestTrackers(状态管理):维护每个请求的稀疏化状态管理。

该架构既原生支持模型内置的稀疏化机制(如 DeepSeekV32 DSA),也允许 Training Free 的稀疏化算法(Quest / SnapKV)与通用 Attention 后端(FlashAttention / Triton)灵活组合,为长上下文推理场景提供统一且高度可扩展的分层稀疏化方案。

2.2 SparseCoordinator:稀疏化流程编排器

SparseCoordinator 是分层稀疏化框架的中枢控制器,通过生命周期钩子函数(Lifecycle Hooks)在模型推理的关键节点精确编排 Algorithm、BackendAdaptor 和 SparseKVCacheManager 三大模块的协同工作。其设计遵循事件驱动模式,将 Retrievable Sparse 的完整流程解耦为标准化的钩子接口,实现了算法与模型的零侵入集成。

SparseCoordinator 将稀疏化推理划分为两个核心阶段:

  • Representation Construction Phase(Prefill 结束或 Decode 初期):通过 attention\_end hook 调用 Algorithm 的 construct\_representations() 和 update\_representations() 方法,将原始 KVCache 压缩为语义表示并存入 Representation Pool,此阶段执行完整 Attention 计算以确保表示质量;
  • Query-Guided Decoding Phase:每个 Decode Step 在 attention\_begin hook 中,Coordinator 驱动 Algorithm 基于当前 Query 从 Representation Pool 中执行 retrieve\_topk() 选择最相关的 Top-k 表示,随后由 BackendAdaptor 完成逻辑索引到物理索引的转换并触发 SparseKVCacheManager 的增量数据传输(通过 Diff Kernel 计算 Topk 集合差异,仅加载变化部分),最终动态重构 Attention Metadata(如 FlashAttention 的 PageTable)供 Attention 后端执行稀疏化计算;
  • 通过这种"捕获-计算-转换-注入"的闭环设计,SparseCoordinator 在保持框架灵活性的同时,实现了高效的 KVCache 分层管理。

2.3 可插拔的稀疏化策略

在 SparseCoordinator 的编排下,Algorithm 和 BackendAdaptor 作为两个核心功能模块,分别负责"选择什么"和"如何映射"的问题,通过清晰的接口定义实现了高度的可插拔性和扩展性。

2.3.1 Algorithm:抽象的 Top-k 选择策略

Algorithm 层采用抽象基类 BaseSparseAlgorithm 定义统一接口,将稀疏化算法的核心逻辑解耦为三个标准化方法:

  • retrieve\_topk(queries, layer\_id, ...):

    • 基于当前 Query 从 Representation Pool 中检索 Top-k 重要 Token/Page 的逻辑索引;
    • 算法只需返回"逻辑索引"(Token ID 或 Page ID),无需关心底层 KVCache 的物理存储布局和 Attention 后端的实现细节(FlashAttention / Triton)。
  • construct\_representations(...):在 Prefill 阶段或 Decode 阶段初期构建用于检索的 Representation Pool 语义表示(如 Key 的压缩表示)。
  • update\_representations(...):在 Decode 阶段增量更新 Representation Pool。

    以 Quest 算法为例:

  • Quest 是一个 Training Free 的 page-wise 稀疏注意力算法,通过为每个 KV Page 维护 per-dimension 的 Key Bounding Box(min/max 值)来避免完整的 Query-Key 点积计算;
  • 在 construct\_representations 阶段,算法遍历所有 Pages 提取 Keys 并计算 Keys 在每个维度的最小/最大值存入 page\_k\_min/max Representation Pool (内存开销约为完整 Key 存储的 1%);
  • 在 retrieve\_topk 阶段,通过 criticality 计算 Attention Score 的上界估计,快速筛选 Top-k Pages 后交由 BackendAdaptor 完成物理地址转换。
2.3.2 BackendAdaptor:索引转换与后端对接的桥梁

BackendAdaptor 层解决了"逻辑世界"到"物理世界"的映射问题。不同的 Attention 后端(DSA Backend、FlashAttention、Triton FA3)对输入数据的格式和索引方式有不同要求,Adaptor 负责屏蔽这些差异。

以 FlashAttention Adaptor 为例:FlashAttentionAdaptor 通过 req\_to\_token 映射表将逻辑 Page IDs 转换为物理页号,重构 PageTable 并更新序列长度元数据(cache\_seqlens, cu\_seqlens\_k),使 FlashAttention 能够基于 Top-k 选中的稀疏页执行注意力计算。

2.4 DeepSeek DSA 接入实践

2.4.1 DeepSeek SparseAttention 介绍

和 DeepSeek-V3.1 相比,DeepSeek-V3.2 的架构改动是在继续训练过程中引入了 DeepSeek Sparse Attention(DSA)。

DSA的原型设计由两部分进行构成,Lightning Indexer(闪电索引器)和 Fine-grained Token Selection Mechanism(细粒度 Token 选择机制)。其首先通过一个轻量级的索引器,进行快速筛选出与当前查询 Token 最相关的候选 Tokens,然后仅在这部分稀疏的候选集上执行高精度的注意力计算。

(注:图片出自 DeepSeek 论文)

2.4.2 DeepSeek DSA 整体接入流程


关键设计包括:

  • 双缓存映射:系统维护两套独立的物理地址映射表(DSADecodeReqToTokenPool):req\_to\_token 中存储每个 Req 对应的  Latent Cache LRU Buffer 页表(LRU Size = 2~4KB),req\_to\_dsa\_index\_k 存储 indexer\_k 页表。Prefill 阶段,Indexer 模块为每个 Token 生成 index\_k,存储至 GPU 端;同时完整的 Latent Cache 被 Offload 至 CPU 内存。Prefill 阶段结束后,每个 Req 占用的显存空间会固定在 LRU Size。
  • 增量传输机制:Decode 阶段,每个 Token 生成时,Indexer 基于当前 Query 和历史缓存的 index\_k 高效计算出 Top-2K Tokens 逻辑索引;随后 Sparse Diff Kernel 通过集合差分算法比较 prev\_topk 和 curr\_topk,精确计算出需要新加载的索引变化量 Δ;SparseKVCacheManager 据此调用 load\_to\_device\_per\_layer 仅传输 Δ 对应的 Latent Cache 块到 GPU 的 LRU Buffer,最小化 PCIe 带宽消耗。
  • 零侵入集成:DeepSeek DSA 通过 SparseCoordinator 的生命周期钩子与模型解耦集成,DeepSeekDSAAlgorithm 作为 Algorithm 层的具体实现直接调用模型原生的 Indexer;DSABackendAdaptor 负责将逻辑 Top-k 索引转换为物理设备地址并触发增量传输;最终由 DSA Backend(支持 flashmla\_sparse/flashmla\_kv/fa3 等多种实现)基于稀疏页表执行 Attention 计算。这一设计使得 128K 长上下文推理的 GPU 显存占用从约 8GB 降至约 200MB。
2.4.3 Sparse Diff Kernel: 增量 Cache 传输基石

动机:时间局部性带来的优化空间

DSA 的 Top-k 选择结果在时间维度上呈现显著的局部性:相邻 Decode Steps 的 Top-k 集合高度重叠。实验表明,相邻 Steps 的 Top-k 重合度通常达到80%~90%,这意味着每个 Decode Step 理论上仅需加载不到 20% 的新 Cache,为增量传输提供了天然的优化空间。

Buffer 容量与命中率的权衡

然而,随着序列长度的增长,Top-k 选择的候选范围线性扩展,相邻步的差异逐渐放大。不同的 LRU Buffer 容量配置会直接影响 Cache 命中率。

可以看到,当 Buffer 容量仅为 Top-k 大小(2K)时,长序列场景下命中率显著下降,I/O 延迟成为瓶颈。而将 Buffer 扩大至 4K~8K,可以用可控的显存开销换取成倍的 I/O 效率提升。

LRU Diff Kernel 设计与实现

为充分利用 DSA 的时间局部性,我们设计了基于 LRU 淘汰策略的 Diff Kernel。其核心思想是:在 GPU 侧维护一个 Top-k 的 2~4 倍容量的 LRU Buffer(典型配置为 4K~8K Token),通过智能淘汰策略容纳 Top-k 的短期波动。

Kernel 工作流程分为三个阶段:

阶段 1:集合交集计算

比较 prev\_topk 和 curr\_topk,识别出两步共同选中的 Token。这部分 Cache 已驻留 GPU,无需重新加载,直接更新 PageTable(curr\_device\_indices)以复用现有数据。

阶段 2:LRU 淘汰决策

这是与严格 Top-k Buffer 的核心差异。Kernel 不会简单驱逐所有 prev\_topk 中未在 curr\_topk 出现的 Token,而是:

  • 仅当 Buffer 空间不足时才触发淘汰;
  • 优先淘汰过去多个 Step 中均未被命中的 Cache 页(基于 LRU 策略);
  • 计算 evict\_device\_indices,标记最冷的物理页位置可被覆写。

阶段 3:增量加载映射

从 curr\_topk 中提取新增的 Token(未命中部分),生成一对一的加载映射关系:

  • load\_host\_indices:这些 Token 在 Host Memory 中的物理地址;
  • load\_device\_indices:它们在 GPU 中的目标物理页号(复用阶段 2 淘汰的位置)。

这种启发式策略充分利用了 DSA Top-k 选择的时间连续性,为每个 Request 动态维护一个高效的缓存窗口, 使得系统可以用较少的 GPU 缓存空间维持长序列场景下至少 80%+ 的缓存命中率,达到空间和效率的动态平衡。

2.4.4 I/O Transfer Kernel: 高性能传输利器 TODO

为了实现 GPU-CPU 异构内存层次间的高效数据迁移,SGLang HiCache 设计了专门的 IO Kernel 传输引擎。该引擎采用 CUDA 底层优化技术,通过 warp-level 细粒度并行最大化 PCIe 带宽利用率。

IO Kernel 支持多种内存布局模式(layer\_first、page\_first、page\_head),实现了对 MHA和 MLA 架构的统一抽象。在 CPU 侧采用 pinned memory 和 CUDA host register 机制确保零拷贝传输,结合 per-layer 和 all-layer 两种传输粒度的动态调度策略,在 prefill 阶段后进行批量全层 offload,在 decode 阶段进行增量单层传输,有效平衡了传输延迟与带宽开销。

实测表明,通过 NUMA 绑定,该 IO Kernel 在 8×H20 上可达到接近\~40GB/s per GPU,为分层 KV cache 架构提供了低延迟、高吞吐的数据搬运基础设施。

3.性能评估

我们在 SGLang 分层稀疏注意力框架上接入了 DeepSeek V32 DSA,并在长上下文推理场景下进行了系统性能评估。实验采用 DeepSeek-V32 模型,针对 16k、32k 和 64k 三种序列长度配置,在 8×H200 GPU With 1TB 内存上测试了不同 batch size 下的输入吞吐量(input tokens/s)。

实验结果表明,相较于传统的全量 KV cache 方案,分层稀疏注意力方案(Hierarchical Sparse)通过结合KV cache 分层管理、GPU-CPU 异构存储以及动态 TopK 检索机制,在长序列场景下展现出显著的性能优势。具体而言:

  1. 内存效率&吞吐量突破:传统方案受限于 GPU 显存容量,在 64k/32k/16k 序列长度下分别仅能支持最大 batch size 为 32/64/128,而 Hierarchical Sparse 方案通过将 KVCache Offload 至 CPU 内存,可支持的最大 batch size 分别达到 160/304/600,实现了 5 倍的批处理能力提升,2~3 倍的 Through 提升。
  2. 可扩展性验证:随着 batch size 增加,Hierarchical Sparse 方案的吞吐量呈现近线性增长趋势,验证了分层缓存架构和稀疏注意力机制在大规模并发推理场景下的良好扩展性。

该结果证明了分层稀疏注意力架构在突破 GPU 显存墙、支持超长上下文大规模并发推理方面的有效性。

4.展望与Roadmap

技术深化方向

  • 算法与后端扩展:适配更多 Sparse 算法(如 StreamingLLM、PQCache)与 Attention 后端(如 FlashInfer、Triton),提升框架的生态兼容性。
  • 性能优化

    • IO 掩藏:通过 TwoBatch Overlap、Kernel Fused 等技术进一步降低 I/O 延迟开销,逼近理论性能上限。
    • 异步检索: 基于相邻 Token 的 Query 具有高度相似性原则,通过前序 Token 的 Query 提前异步检索 当前 Step 的 Topk,减少检索开销。

架构演进方向:随着超节点架构的普及,GPU 通过 Scale-Up 网络访问共享内存池的带宽已显著超越传统 PCIe 带宽。在此硬件趋势下,KVCache 的内存池化管理(Memory Pooling)成为自然选择。我们将协助实现超节点内的 KVCache 统一池化调度,充分发挥 Scale-Up 网络的带宽优势,突破传统 PCIe 瓶颈,为超长上下文推理提供更高效的分层稀疏化基础设施。

了解更多

欢迎搜索钉钉群号:109765011301入群与技术专家交流!

0x01 研究背景

在自回归生成模型(Autoregressive Model)中,LLM每生成一个新token,都会将此前生成的序列作为输入。若每一步都重新计算全部注意力(Q、K、V 矩阵),计算量将随序列长度平方级增长。在长上下文和高并发场景下,这一开销会迅速成为系统瓶颈。为此,主流推理框架普遍引入KV-Cache技术。 KV-Cache通过缓存此前token的Key(K)和Value(V)向量,在下一步生成时只需计算新的Query(Q),即可直接复用前面的K/V,从而显著降低重复计算量。实践中,KV-Cache 通常能在保持模型精度不变的前提下,带来约5-8倍的推理加速。这一机制已经成为vLLM、SGLang、DeepSpeed-Inference等高性能推理引擎,以及Hugging Face generate(use_cache=True)接口的默认能力。

随着2024–2025年多租户推理服务(如vLLM、SGLang、TensorRT-LLM)的大规模部署,系统在单模型、多租户共享的前提下,又进一步引入跨请求的前缀缓存共享(prefix caching)。当不同请求的prompt存在相同前缀时,系统可以直接复用已有KV-Cache,大幅摊薄Prefill成本并提升吞吐。然而,当这种共享与复用机制扩展到多租户并发环境时,KV-Cache不再只是一个“性能优化组件”,而是演变成新的攻击面:攻击者可以通过观测Prefill 时间、TTFT等性能差异发起时序侧信道攻击,通过篡改缓存内容实施History Swapping(生成轨迹劫持),或者通过对Key向量注入扰动发动Cache Corruption(缓存腐败),从而导致跨租户信息泄露、话题漂移甚至下游任务性能显著下降。

0x02 KV缓存工作机制与共享复用原理

下面是KV-Cache工作原理的示意图。

KV-Cache工作原理

KV-Cache工作原理图

接下来我们用文字详细拆解,更深入了解KV缓存工作机制。

2.1 两阶段推理:Prefill与Decode

KV-Cache的核心做法分为两阶段。

(1)Prefill阶段(Prompt阶段)一次性计算输入序列的K/V并写入缓存 模型读取完整输入的prompt,计算出所有token的Key/Value向量并写入缓存。 公式表示为:

image-20251229205745845

此时缓存中的K/V向量构成了后续生成阶段的基础。

(2)Decode阶段(生成阶段)仅对新token计算Q/K/V,并复用历史K/V完成注意力计算 当模型生成新token时,仅需计算该token对应的Q、K、V向量。

image-20251229205757073

然后与缓存中已有的K/V拼接,直接完成注意力计算。这样便避免了重复计算前面N−1个token的注意力结果。

2.2 past_key_values

在Hugging Face Transformers框架中,KV-Cache在接口层面通过 past_key_values 对象实现。该对象并非一个抽象的控制开关,而是模型前向推理过程中实际生成、并可跨生成步骤复用的中间状态。它以分层的结构保存已处理历史Token的Key和Value张量,从而支撑自回归生成的增量计算。

从结构上看,past_key_values通常是一个长度为模型层数的列表或元组,其中每一层对应一对 (K, V)张量。不同模型的具体维度布局可能存在差异,但其核心语义一致:存储历史序列的注意力键值表示,以便后续生成时直接复用。

在推理流程中,Prefill 阶段会对完整的提示词进行计算,并首次生成past_key_values。进入Decode阶段后,若将此缓存作为输入传递给模型,模型通常只需为新输入的Token计算其对应的Key和Value,并将其追加至现有缓存末尾,从而避免了历史部分的重复计算。这种基于past_key_values的复用是框架的原生机制,其带来的加速直接源于注意力计算的真实削减,因此更适合作为评估系统性能及分析相关安全影响的工程基准。相比之下,通过sleep()或人为插桩制造“快慢差异”的方法仅能模拟现象,难以反映实际推理系统的缓存行为。此外,Transformers框架的generate()接口通常通过参数use_cache=True来启用此缓存机制。vLLM、SGLang、DeepSpeed-Inference在系统层面也普遍实现了类似机制,以降低生成延迟并提升吞吐量。

2.3 多租户场景下的前缀缓存与最长前缀匹配

在多请求并发且显存资源受限的推理服务中,为提升吞吐并降低重复的Prefill开销,系统常采用前缀缓存策略。其核心思想是当新请求的提示词(更准确地说是其Token序列)与某条已缓存的序列存在前缀重合时,系统可直接复用该前缀部分对应的KV-Cache,仅需对未命中的后续Token执行增量计算。

当缓存池中存在多个可能的候选前缀时,命中判定通常遵循最长前缀匹配(LPM)原则:在所有缓存条目中,系统会选择与新请求Token序列匹配长度最长的那一条作为复用对象,以最大化缓存利用率,减少重复计算。在工程实现上,这依赖于能够高效进行Token序列前缀匹配的数据结构或索引机制,例如前缀树(Trie)、基于前N个Token的分层哈希,或基于序列哈希值的多级索引。

根据匹配程度,命中效果可分为两类:一是完全命中,即请求的绝大部分或全部前缀已在缓存中,Prefill阶段的计算量显著下降;二是部分命中,即仅能复用较短的前缀,系统仍需对剩余后缀执行完整的Prefill计算。无论是“是否命中”还是“命中长度”,都会直接反映在可观测的系统性能指标上,例如Prefill时间、首Token延迟的分布等。

当前主流引擎(如vLLM的PagedAttention、LMCache)进一步通过分页管理和压缩技术缓解显存碎片,但前缀共享引入的侧信道与内存安全风险依然突出,这也是后续攻击面的根源。

0x03 KV-Cache的主要攻击面原理介绍

在理解KV-Cache的核心优化机制与共享原理后,我们可以看到其高效性背后隐藏的脆弱性。下面详解三大主要攻击面:时序侧信道攻击、操纵攻击与腐败攻击。

3.1 KV-Cache时序侧信道攻击

在共享KV-Cache的系统中,攻击者通过测量响应时间或请求处理顺序,推断缓存是否命中(hit),从而还原其他用户的Prompt(提示词)。

image-20251028173225854

时序侧信道攻击完流程图

设定还原的语句是"Imagine you are an IT expert",攻击者已经成功还原出"Imagine you are",并尝试还原下一个token "an"。下面我们根据上图分步骤拆解一下攻击过程。

步骤1:Generate candidates

攻击者在本地用小模型、模板或启发式方法生成可能的下一个token候选集合,例如:

  • Imagine you are an
  • Imagine you are a
  • Imagine you are the

把未知的victim prompt逐步转化为一系列候选前缀/后缀,便于后续probe。优点是减少搜索空间。


步骤2:Generate dummy

  • Candidate请求:每个请求包含一个候选后缀(比如Imagine you are an)。目标是看哪一个candidate与victim的缓存前缀最长匹配而“命中”缓存。
  • Dummy请求:随机或不相关的prompt(用来制造队列/填充调度槽位),以便控制调度顺序或避免直接暴露自己的probe请求导致缓存污染判断混淆。

步骤3:Send three request batches in turn

攻击者按这个顺序把三组请求发到服务器(可能是同一API key,也可能跨多个短时间窗口发出)。核心就是在调度队列里把candidate放在中间,观察它是否因为缓存命中而更快返回。

步骤4:Observe the returning order

攻击者记录三批请求的返回顺序和时间(TTFT/latency)。若candidate的响应比其前后的dummy显著更快或优先到达,就可推断该candidate是命中了缓存(即victim的prompt与该candidate共享较长前缀)。

3.2 History Swapping 攻击

image-20251230101845847

History Swapping操纵攻击原理图

攻击者通过结构化替换或注入KV-Cache内容,来“劫持”模型的生成轨迹,强制引导输出转向攻击者指定的主题或行为。这种攻击利用KV-Cache编码了不仅仅是上下文,还包括话题规划(topic trajectory)和结构化推理(structural planning)的特性。

设定攻击场景:受害者Prompt为“Give a precise technical explanation of espresso extraction variables”(讨论咖啡萃取),攻击者希望劫持输出到恒星生命周期主题。用户可见Prompt不变。

步骤1: 预生成目标主题KV-Cache

攻击者离线使用相同模型,基于目标主题Prompt生成一段完整KV-Cache块(topic_cache)。

步骤2: 启动正常生成并等待替换点

从受害者Prompt开始自回归生成,监控已生成token数,直到达到预设swap_token(例如序列的20%-60%处)。

步骤3: 执行块级覆盖替换

计算替换段长度(swap_percent,如25%-75%最近timestep),在全层(或指定早/晚层)用topic_cache对应部分直接覆盖当前缓存。

步骤4: 继续生成并观察劫持

模型基于篡改缓存继续输出。常见效果:立即/延迟主题偏移、原主题与攻击主题交替、或生成重复崩溃。

3.3 KV-Cache 腐败攻击

image-20251230101806716

KV-Cache腐败攻击原理图

攻击者通过向KV-Cache注入扰动(perturbation),破坏注意力机制的完整性,导致输出偏差、性能下降或幻觉增加。这种攻击视KV-Cache为“内存腐败”类似漏洞,扰动键向量(Key vectors)即可放大影响。

设定攻击场景:在正常生成或RAG任务中,攻击者向KV-Cache的Key向量注入扰动,导致注意力偏差、性能下降或幻觉增加。

步骤1: 选择目标层与时机

确定最脆弱层(通常中层,如LLaMA-2第12层)和扰动应用频率(连续或间歇)。

步骤2: 选择扰动变体

  • MTI-Gaussian:添加高斯噪声(σ=0.1-5.0)
  • MTI-Zeroing:概率置零Key条目
  • MTI-Rotation:施加正交旋转(15°-90°)
  • 可结合梯度优化以最大化目标影响

步骤3: 注入扰动到Key向量

在生成过程中,按选定策略对Key向量应用扰动δ。

步骤4: 观察输出效果

监控下一token分布偏移(KL散度上升)、下游任务性能下降15–30%、或RAG幻觉率增加5%-12%。中层扰动放大效果最显著。

0x04 代码实现

测试为纯CPU环境下完成,基于Python3.8+的Hugging Face Transformers与PyTorch运行124M参数的gpt2模型。

4.1 KV-Cache时序侧信道攻击

实验1:基础缓存时序测量

验证KV-Cache复用是否产生物理上可观测的时间差异。

我们实现了一个多租户LLM服务的 KVServer 类,支持:

  1. 最长前缀匹配 (LPM):实现类似vLLM的Prefix Caching
  2. 精确计时:仅测量Prefill阶段的KV 计算,排除tokenization开销
  3. 缓存管理:LRU淘汰策略

核心实现

@dataclass
class _CacheEnt:
    """KV-Cache 条目"""
    prompt: str
    input_ids: torch.Tensor
    past_kv: Tuple
    ts: float

class KVServer:
    """多租户KV-Cache服务器"""

    def _lpm(self, q_ids: torch.Tensor):
        """Longest Prefix Match - 缓存必须是查询的前缀"""
        best = None
        best_len = 0

        for cached, ent in self._cache.items():
            c_ids = ent.input_ids[0].tolist()
            q = q_ids[0].tolist()

            # 计算共同前缀长度
            mlen = 0
            for i, (a, b) in enumerate(zip(c_ids, q)):
                if a == b:
                    mlen = i + 1
                else:
                    break

            # 缓存有效条件:缓存是查询的前缀(mlen == len(cached))
            if mlen > best_len and mlen == len(c_ids) and len(c_ids) <= len(q):
                best = ent
                best_len = mlen

        return (best, best_len) if best else None

    def process(self, prompt: str, max_new=1, uid="anon", write_cache=True):
        """处理请求,返回详细的时序数据"""
        input_ids = self.tok.encode(prompt, return_tensors="pt")
        t0 = time.perf_counter()

        cache_r = self._lpm(input_ids)

        with torch.no_grad():
            if cache_r:
                # 缓存命中路径:复用past_key_values
                ent, matched = cache_r
                self._hits += 1

                if input_ids.shape[1] > matched:
                    # 部分匹配:计算增量部分
                    delta_ids = input_ids[:, matched:]
                    out = self.model(
                        delta_ids,
                        past_key_values=ent.past_kv,
                        use_cache=True
                    )
                    past_kv = out.past_key_values
                else:
                    # 完全命中:直接复用
                    past_kv = ent.past_kv

                prefill_t = (time.perf_counter() - t0) * 1000
                hit = True
            else:
                # 缓存未命中路径:完整前向传播
                self._miss += 1
                out = self.model(input_ids, use_cache=True)
                past_kv = out.past_key_values
                prefill_t = (time.perf_counter() - t0) * 1000
                hit = False

        # ... 生成阶段与缓存写回

运行结果

可以看到上面第一次请求Prefill用了大约380ms,这是模型执行完整前向传播的时间。对于GPT-2,这意味着要进行12层TransformerBlock的矩阵乘法运算。

然后当二次请求完全相同的时候Prefill仅仅为0.024ms ,几乎就只有内存操作时间。这个原因是因为past_key_values已存在,模型跳过了所有Attention层的Q×KT​计算,仅需简单的张量拼接。

实验2:prompt探测攻击

为了验证攻击者能否通过时序差异识别受害者的Prompt

核心代码:

def experiment_2_exact_match_attack():
    # 步骤1: 受害者缓存敏感Prompt
    victim_prompts = [
        "My secret password is hunter2",
        "My API key is sk-1234567890abcdef",
    ]

    for prompt in victim_prompts:
        server.process(prompt, uid="victim", write_cache=True)

    # 步骤2: 攻击者构造候选列表
    candidates = [
        "My secret password is hunter2",      # ✓ 匹配
        "My secret password is wrong",        # ✗ 不匹配
        "My secret code is hunter2",          # ✗ 不匹配
        "My API key is sk-1234567890abcdef",  # ✓ 匹配
        "My API key is sk-wrong-key",         # ✗ 不匹配
    ]

    # 步骤3: 逐个探测
    discovered = []
    for cand in candidates:
        r = server.process(cand, uid="attacker", write_cache=False)
        t = r['prefill_ms']

        if t < 1.0:  # 阈值判定
            discovered.append((cand, t))

    return discovered

运行结果

image-20251230222154860

我们可以从上面实验结果看到当探测内容与缓存完全一致时,模型无需任何计算,直接返回缓存指针。时间差异非常大。

我们可以从攻击者视角的视角来看到这件事:

1.攻击者构造"密码候选列表"(类似字典攻击),逐个探测。

2.只要有一个候选的响应时间<1ms,攻击者就能推断出被攻击者的完整prompt。

攻击简易流程图如下。

1

4.2 History Swapping攻击

实验目标:在不改变用户可见Prompt的情况下,通过替换推理过程中的past_key_values片段,把模型输出从“受害者话题”劫持到“攻击者话题”。

  • 受害者请求:正常的业务问题(例如“如何制作咖啡”)。
  • 攻击者能力


    • 能在同一推理进程/同一GPU的Worker内“写入或污染”共享的KV-Cache(例如:推理引擎实现了前缀缓存复用、调度/缓存对象复用存在隔离缺陷、或插件/监控/扩展组件可触达缓存对象)。
    • 攻击者提前离线生成一段目标主题的K-Cache。
    • 攻击效果:用户看到的prompt没变,但输出内容发生明显“叙事漂移”。

核心思路

  1. 攻击者用目标主题prompt跑一次Prefill,得到attacker_cache
  2. 受害者开始生成,达到某个swap_at_token时刻。
  3. 在所有层(或关键层)将受害者cache的一段时间步区间(如中间30%-60%)用attacker_cache的片段覆盖。

核心实现:

def gen_with_swap(model, tok, prompt: str, max_tok=30,
                 atk_cache=None, swap_at=2):
    """带缓存替换的生成函数"""
    ids = tok.encode(prompt, return_tensors="pt")
    generated = []

    # Prefill 阶段
    with torch.no_grad():
        out = model(input_ids=ids, use_cache=True)
        past = out.past_key_values
        logits = out.logits[0, -1, :]

    # 逐 token 生成
    for step in range(max_tok):
        tok_id = torch.argmax(logits).item()
        generated.append(tok_id)

        # 关键:在指定步数替换缓存
        if step == swap_at and atk_cache is not None:
            past = _swap_mix(past, atk_cache)

        nxt = torch.tensor([[tok_id]])
        with torch.no_grad():
            out = model(input_ids=nxt, past_key_values=past, use_cache=True)
            past = out.past_key_values
            logits = out.logits[0, -1, :]

    return tok.decode(generated, skip_special_tokens=True)

def _swap_mix(vic_cache, atk_cache):
    """混合策略:保留 10% 受害者前缀,替换中间 85% 为攻击者缓存"""
    from transformers.cache_utils import DynamicCache

    # 提取张量
    if hasattr(vic_cache, "key_cache"):
        v_k = [vic_cache.key_cache[i].clone() for i in range(len(vic_cache.key_cache))]
        v_v = [vic_cache.value_cache[i].clone() for i in range(len(vic_cache.value_cache))]
        a_k = [atk_cache.key_cache[i] for i in range(len(atk_cache.key_cache))]
        a_v = [atk_cache.value_cache[i] for i in range(len(atk_cache.value_cache))]
    else:
        # 兼容 tuple 格式
        v_k = [vic_cache[i][0].clone() for i in range(len(vic_cache))]
        v_v = [vic_cache[i][1].clone() for i in range(len(vic_cache))]
        a_k = [atk_cache[i][0] for i in range(len(atk_cache))]
        a_v = [atk_cache[i][1] for i in range(len(atk_cache))]

    new_cache = DynamicCache()

    for layer in range(len(v_k)):
        vk, vv = v_k[layer], v_v[layer]
        ak, av = a_k[layer], a_v[layer]

        seq_len = vk.shape[2]
        atk_len = ak.shape[2]

        # 替换策略:保留 10%,替换 10%-95%
        start = int(seq_len * 0.1)
        end = int(seq_len * 0.95)
        swap_sz = min(end - start, atk_len)

        nk = vk.clone()
        nv = vv.clone()

        if swap_sz > 0:
            # 关键:切片替换
            nk[:, :, start:start+swap_sz, :] = ak[:, :, :swap_sz, :]
            nv[:, :, start:start+swap_sz, :] = av[:, :, :swap_sz, :]

        new_cache.key_cache.append(nk)
        new_cache.value_cache.append(nv)

    return new_cache

image-20251229201348226

在我们进行swap_at_token 之后,输出出现明显话题漂移,原本应该是咖啡的制作方面的东西,结果话题漂移到了星空上面。

4.3 KV-Cache腐败攻击

实验:Cache Corruption(扰动Key 向量)

实验目标:在生成过程中对KV-Cache的Key张量注入扰动(噪声/置零/旋转),观察注意力机制被破坏后带来的输出质量退化(重复、语义漂移、幻觉倾向上升)。

更贴近实战的场景设定

  • 共享显存/共享推理Worker:攻击者通过越权写入或内存破坏类漏洞(例如缓存指针复用错误、越界写、错误的张量视图复用)影响到其他请求的KV。
  • RAG/Agent场景:缓存腐败会显著增加“把检索内容读错/拼接错”的概率,表现为幻觉或逻辑断裂。

扰动策略

  • corrupt_gaussian:K = K + N(0, σ^2)
  • corrupt_zeroing:以概p 将Key条目置零
  • corrupt_rotation:对Key的embedding子空间做正交旋转(简化实现为对最后维度两两旋转)

核心实现:

#高斯噪声
def corrupt_gaussian(cache, sig=1.0):
    """对 Key 向量添加高斯噪声:K = K + N(0, σ²)"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:  # 中层更敏感
            noise = torch.randn_like(k) * sig
            out.append((k + noise, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

#随机置零
def corrupt_zeroing(cache, p=0.3):
    """以概率 p 将 Key 条目置零"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:
            mask = (torch.rand_like(k) > p).float()
            out.append((k * mask, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

#正交旋转
def corrupt_rotation(cache, deg=45.0):
    """对 Key 的 embedding 子空间做正交旋转"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    rad = np.radians(deg)
    c, s = np.cos(rad), np.sin(rad)

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:
            nk = k.clone()
            d = k.shape[-1]
            # 对最后维度两两旋转
            for j in range(0, d - 1, 2):
                kj = k[:, :, :, j].clone()
                kj1 = k[:, :, :, j + 1].clone()
                nk[:, :, :, j] = c * kj - s * kj1
                nk[:, :, :, j + 1] = s * kj + c * kj1
            out.append((nk, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

实验结果如下

image-20251230223603113

可以看到在不同扰动策略下模型输出的内容发生明显变化。

0x05 防御与缓解措施

以下从架构、系统、审计三层总结主流缓解措施,结合最新研究(如SafeKV、KV-Cloak),旨在平衡安全性、性能与部署成本。

5.1 架构层防御

  • 租户级缓存隔离:通过Tenant ID、Session Scope或用户唯一标识符划分KV命名空间,完全禁止跨租户共享。适用于高敏感场景,虽牺牲部分吞吐,但彻底消除侧信道。
  • 选择性共享:仅允许非敏感前缀共享,结合细粒度隐私策略(如基于内容分类)决定复用范围。
  • 缓存生命周期管理:单次请求后自动清除,或设置TTL过期策略,减少驻留时间泄露风险。
  • LPM随机化与分区:在最长前缀匹配中引入随机扰动,或按哈希分区缓存池,打乱命中可预测性。

5.2 系统层防御

  • 噪声注入与延迟模糊化:在缓存命中路径插入±Δt随机延迟,或对时间指标添加噪声,隐藏TTFT/顺序差异(针对时序攻击)。
  • 缓存内容混淆:使用可逆矩阵变换对KV向量加密/混淆,仅授权方可逆转。
  • 扰动检测与完整性校验:实时监控KV向量范数/哈希变化,检测异常扰动(针对腐败攻击)或引入dropout-mask随机化/注意力平滑,减轻操纵影响。
  • 参数与接口限制:禁止外部暴露use_cache、position_ids等敏感参数;结合速率限制(throttling)阻断高频探测请求。
  • 机密计算集成:利用TEE(Trusted Execution Environment)加密KV-Cache内存,防止物理/侧信道访问。

5.3 审计与合规层

  • 缓存审计器:记录每条请求的缓存命中/共享日志,绘制租户-缓存命中矩阵,便于事后追溯。
  • 异常行为监控:基于机器学习检测异常调度模式(如批量相似前缀探测),自动告警或隔离。
  • 合规框架支持:在SOC 2、ISO 27001或GDPR下,强制日志不可篡改,并定期审计共享安全性。

参考资料

https://openreview.net/pdf?id=gUj2fxQcLZ

https://www.ndss-symposium.org/wp-content/uploads/2025-1772-paper.pdf

https://huggingface.co/docs/transformers/en/kv_cache

https://arxiv.org/abs/2312.07104

https://www.arxiv.org/pdf/2510.17098

https://arxiv.org/pdf/2511.12752

https://pub.towardsai.net/lets-build-an-optimizer-for-a-gpt-model-from-scratch-in-pytorch-kv-caching-4d3f1f9516fa

在大模型(LLM)服务极速发展的当下,效率至关重要。为了降低延迟并控制算力成本,主流推理框架广泛引入了先进的缓存机制。然而,这种追求极致速度的设计是否埋下了安全隐患?

本论文是由奇安信技术研究院、中国海洋大学和清华大学联合完成的AI安全研究工作说明了缓存机制如果实现不恰当的话,就会造成安全隐患。论文题目为《Cache Me, Catch You: Cache Related Security Threats in LLM Serving Frameworks》。这项工作由中国海洋大学和奇安信联合培养的硕士研究生吴祥凡在奇安信技术研究院联培期间主导完成,导师为应凌云博士(奇安信星图实验室)和曲海鹏教授(中国海洋大学),其他作者为陈国强(奇安信星图实验室),谷雅聪(清华大学)。这项研究聚焦于大语言模型(LLM)推理服务框架中的安全威胁,深入分析了 KV Cache、多模态缓存及语义缓存 三大核心机制。

1. LLM推理加速背后的隐忧

随着模型参数规模的不断膨胀,推理计算的开销急剧上升。为了优化用户体验,vLLM、SGLang、GPTCache等主流服务框架引入了多种缓存策略,包括前缀缓存(Prefix Cache)、语义缓存(Semantic Cache)和多模态缓存(Multimodal Cache)。

虽然这些机制通过存储中间状态极大地减少了重复计算,但我们的研究发现,现有的缓存实现往往“重效率、轻安全”。非加密哈希函数的滥用、有缺陷的对象序列化以及模糊的语义匹配标准,共同构成了一个全新的、尚未被充分探索的攻击面。与以往关注训练阶段的数据投毒不同,这是一类发生在推理阶段的全新安全威胁。

2. Cache Me, Catch You:首个LLM缓存安全系统性研究

为了揭示这一风险,我们对主流LLM服务框架的缓存实现进行了全面的解构与分析,并提出了六种新颖的攻击向量。这些攻击利用了哈希碰撞和语义模糊匹配的特性,能够在不接触模型权重的情况下,通过污染共享缓存来操纵模型输出。

主要发现与攻击向量:

我们将发现的威胁归纳为两大类:一是面向用户的欺诈攻击,即攻击者利用系统渠道向用户传递恶意信息 ,具体手段包括利用哈希碰撞替换合法提示词以劫持对话逻辑的系统提示词碰撞、针对语义缓存构造高相似度恶意查询诱导错误回答的语义模糊投毒 ,以及在检索增强生成场景下利用文档相似性扩大攻击面的RAG语义投毒 ;二是系统完整性攻击,旨在破坏服务功能或绕过安全审查 ,具体涵盖构造与目标完整前缀碰撞以劫持响应的提示词碰撞劫持 、通过精心构造padding token让恶意代码块对LLM“隐形”以绕过审计的分块碰撞劫持 ,以及利用图像处理忽略元数据(如尺寸)缺陷构造哈希碰撞图片以绕过审核的多模态碰撞 。

细节详解:

以多模态为例,其核心漏洞根源在于当前主流推理框架(如vLLM)在对多模态数据进行序列化时存在严重的逻辑缺陷。具体而言,vLLM默认调用PIL 的 tobytes() 方法来提取图像数据以计算哈希,该方法虽然能获取原始像素字节流,但在vLLM的后续操作中完全忽略了图像宽高等尺寸信息以及调色板等关键元数据。攻击者利用这一特性实施“尺寸伪装”攻击,通过重塑图像维度(例如将 H*W的图像变形为W*H)而不改变像素排列顺序,使得原本违规的图片变成一团毫无意义的噪点,从而生成与原图完全一致的哈希值。此外,攻击者还能利用“调色板模式”漏洞,构造出索引数据相同但颜色定义截然相反的图片对(如黑底白字与白底黑字),由于序列化过程仅读取索引而忽略调色板定义,这两张视觉迥异的图片在系统眼中却拥有相同的“指纹”。

同样的隐患也出现在SGLang框架中,其为了适配张量数值范围将SHA256哈希值进行了取模截断,导致哈希空间被压缩至极易发生碰撞的范围。

下图是我们操纵图片当中的尺寸和PNG当中的P格式的调色盘,实现看上去不同的图片但是hash一致。

3. 实验效果与影响评估

我们在vLLM、SGLang及GPTCache等主流开源框架上进行了实测,证实了这些攻击路径的高可用性与低门槛:攻击者仅需不到1美元的成本即可完成一次投毒 。以针对vLLM的前缀缓存攻击为例,我们在30分钟内便成功搜索到碰撞哈希,实现了100%的缓存命中 。

实验还还原了真实的威胁场景违规图片如何利用多模态缓存缺陷骗过内容审核系统。下图展示一个示意图,成功命中图片之后会复用之前的图片预处理结果,导致生成了错误回复。

4. 防御方案与行业响应

针对发现的漏洞,我们提出了五层防御策略,包括引入随机化哈希(Salting)、采用强加密哈希函数、强制规范化序列化流程、使用更鲁棒的Embedding模型以及增加LLM辅助过滤层。我们的理论分析和实际验证表明,上述的防御方案是可行的、有效的。

我们在第一时间将发现的漏洞通报给了受影响的厂商和社区,包括 vLLM、SGLang、GPTCache、AIBrix、rtp-llm 和 LMDeploy,并分配了 3个 CVE 编号。值得注意的是,vLLM、GPTCache 和 AIBrix 已经采纳了我们提出的缓解措施(如引入随机盐值、规范化图像序列化等)并完成了修复。(在本文发表时,SGLang也反馈采纳了我们的缓解措施。)

5. 讨论与未来展望

我们的研究再次表明,高性能不应成为忽视底层系统安全的理由。本研究证明,即便模型本身无懈可击,外围缓存框架的设计缺陷仍足以瓦解整个系统的信任基石;特别是在云端共享算力场景下,必须实施严格的多租户隔离与键值空间分离以防御跨租户攻击。作为填补推理侧缓存安全空白的先行工作,本研究旨在推动社区正视这一隐蔽威胁,共同构建更稳健的大模型服务基础设施。

更多参考

想了解更多技术细节?欢迎阅读我们的学术论文或访问项目主页:

代码仓库:https://github.com/XingTuLab/Cache_Me_Catch_You

感谢您的阅读,期待能为您的AI安全研究与工程实践带来启发!

在大模型(LLM)服务极速发展的当下,效率至关重要。为了降低延迟并控制算力成本,主流推理框架广泛引入了先进的缓存机制。然而,这种追求极致速度的设计是否埋下了安全隐患?

本论文是由奇安信技术研究院、中国海洋大学和清华大学联合完成的AI安全研究工作说明了缓存机制如果实现不恰当的话,就会造成安全隐患。论文题目为《Cache Me, Catch You: Cache Related Security Threats in LLM Serving Frameworks》。这项工作由中国海洋大学和奇安信联合培养的硕士研究生吴祥凡在奇安信技术研究院联培期间主导完成,导师为应凌云博士(奇安信星图实验室)和曲海鹏教授(中国海洋大学),其他作者为陈国强(奇安信星图实验室),谷雅聪(清华大学)。这项研究聚焦于大语言模型(LLM)推理服务框架中的安全威胁,深入分析了 KV Cache、多模态缓存及语义缓存 三大核心机制。

1. LLM推理加速背后的隐忧

随着模型参数规模的不断膨胀,推理计算的开销急剧上升。为了优化用户体验,vLLM、SGLang、GPTCache等主流服务框架引入了多种缓存策略,包括前缀缓存(Prefix Cache)、语义缓存(Semantic Cache)和多模态缓存(Multimodal Cache)。

虽然这些机制通过存储中间状态极大地减少了重复计算,但我们的研究发现,现有的缓存实现往往“重效率、轻安全”。非加密哈希函数的滥用、有缺陷的对象序列化以及模糊的语义匹配标准,共同构成了一个全新的、尚未被充分探索的攻击面。与以往关注训练阶段的数据投毒不同,这是一类发生在推理阶段的全新安全威胁。

2. Cache Me, Catch You:首个LLM缓存安全系统性研究

为了揭示这一风险,我们对主流LLM服务框架的缓存实现进行了全面的解构与分析,并提出了六种新颖的攻击向量。这些攻击利用了哈希碰撞和语义模糊匹配的特性,能够在不接触模型权重的情况下,通过污染共享缓存来操纵模型输出。

主要发现与攻击向量:

我们将发现的威胁归纳为两大类:一是面向用户的欺诈攻击,即攻击者利用系统渠道向用户传递恶意信息 ,具体手段包括利用哈希碰撞替换合法提示词以劫持对话逻辑的系统提示词碰撞、针对语义缓存构造高相似度恶意查询诱导错误回答的语义模糊投毒 ,以及在检索增强生成场景下利用文档相似性扩大攻击面的RAG语义投毒 ;二是系统完整性攻击,旨在破坏服务功能或绕过安全审查 ,具体涵盖构造与目标完整前缀碰撞以劫持响应的提示词碰撞劫持 、通过精心构造padding token让恶意代码块对LLM“隐形”以绕过审计的分块碰撞劫持 ,以及利用图像处理忽略元数据(如尺寸)缺陷构造哈希碰撞图片以绕过审核的多模态碰撞 。

细节详解:

以多模态为例,其核心漏洞根源在于当前主流推理框架(如vLLM)在对多模态数据进行序列化时存在严重的逻辑缺陷。具体而言,vLLM默认调用PIL 的 tobytes() 方法来提取图像数据以计算哈希,该方法虽然能获取原始像素字节流,但在vLLM的后续操作中完全忽略了图像宽高等尺寸信息以及调色板等关键元数据。攻击者利用这一特性实施“尺寸伪装”攻击,通过重塑图像维度(例如将 H*W的图像变形为W*H)而不改变像素排列顺序,使得原本违规的图片变成一团毫无意义的噪点,从而生成与原图完全一致的哈希值。此外,攻击者还能利用“调色板模式”漏洞,构造出索引数据相同但颜色定义截然相反的图片对(如黑底白字与白底黑字),由于序列化过程仅读取索引而忽略调色板定义,这两张视觉迥异的图片在系统眼中却拥有相同的“指纹”。

同样的隐患也出现在SGLang框架中,其为了适配张量数值范围将SHA256哈希值进行了取模截断,导致哈希空间被压缩至极易发生碰撞的范围。

下图是我们操纵图片当中的尺寸和PNG当中的P格式的调色盘,实现看上去不同的图片但是hash一致。

3. 实验效果与影响评估

我们在vLLM、SGLang及GPTCache等主流开源框架上进行了实测,证实了这些攻击路径的高可用性与低门槛:攻击者仅需不到1美元的成本即可完成一次投毒 。以针对vLLM的前缀缓存攻击为例,我们在30分钟内便成功搜索到碰撞哈希,实现了100%的缓存命中 。

实验还还原了真实的威胁场景违规图片如何利用多模态缓存缺陷骗过内容审核系统。下图展示一个示意图,成功命中图片之后会复用之前的图片预处理结果,导致生成了错误回复。

4. 防御方案与行业响应

针对发现的漏洞,我们提出了五层防御策略,包括引入随机化哈希(Salting)、采用强加密哈希函数、强制规范化序列化流程、使用更鲁棒的Embedding模型以及增加LLM辅助过滤层。我们的理论分析和实际验证表明,上述的防御方案是可行的、有效的。

我们在第一时间将发现的漏洞通报给了受影响的厂商和社区,包括 vLLM、SGLang、GPTCache、AIBrix、rtp-llm 和 LMDeploy,并分配了 3个 CVE 编号。值得注意的是,vLLM、GPTCache 和 AIBrix 已经采纳了我们提出的缓解措施(如引入随机盐值、规范化图像序列化等)并完成了修复。(在本文发表时,SGLang也反馈采纳了我们的缓解措施。)

5. 讨论与未来展望

我们的研究再次表明,高性能不应成为忽视底层系统安全的理由。本研究证明,即便模型本身无懈可击,外围缓存框架的设计缺陷仍足以瓦解整个系统的信任基石;特别是在云端共享算力场景下,必须实施严格的多租户隔离与键值空间分离以防御跨租户攻击。作为填补推理侧缓存安全空白的先行工作,本研究旨在推动社区正视这一隐蔽威胁,共同构建更稳健的大模型服务基础设施。

更多参考

想了解更多技术细节?欢迎阅读我们的学术论文或访问项目主页:

代码仓库:https://github.com/XingTuLab/Cache_Me_Catch_You

感谢您的阅读,期待能为您的AI安全研究与工程实践带来启发!