LMCache L2 静态加密实现解析:基于 AES-GCM 的aesgcmSerde 设计与实战
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
导读
LMCache 的多进程(MP)架构中,KV Cache 会被异步卸载到 L2 层(远程 S3、本地磁盘、Redis 等),这些"落盘"字节往往包含敏感的对话内容。本文围绕设计文档 docs/design/v1/distributed/serde/aesgcm.md,深入讲解 LMCache 如何以serde 插件形式为 L2 层提供 AES-GCM 静态加密:包括威胁模型、逐 chunk 的线格式、以cache_salt为选择器的密钥模型、完整配置参数,并结合源码、测试与端到端示例脚本给出可落地的启用方式。读完本文,你将掌握如何在任意 L2 adapter 上透明地开启 KV Cache 静态加密,并理解其信任边界与已知局限。
为什么加密要放在 serde 层
在 LMCache 的分布式存储链路中,L1(CPU 内存)与 L2(远程/本地持久化存储)之间的每一次往返都要经过序列化:store 路径执行serialize,load 路径执行deserialize。这种可逆对称变换的天然位置正是 serde 层。加密实现 lmcache/v1/distributed/serde/aesgcm.py 与量化类 serde(fp8、turboquant,位于同一 serde 目录)并列存在,但它有一个显著差异:它是第一个需要"每 key 上下文"(即cache_salt)的 serde,为此 serde 接口为serialize/deserialize/estimate_serialized_size增加了ObjectKey参数(见 base.py 中Serializer/Deserializer的签名),内容无关的量化 serde 可以忽略该参数,而加密 serde 用它来选择密钥。
接入方式上,加密 serde 通过 factory.py 的注册表按名称暴露,再由 SerdeL2AdapterWrapper 包装任意 L2 adapter。从包装器的设计文档(serde_wrapper.py)可以看到它的职责链:
store : caller → serialize → inner.store → signal store_efd load : caller → inner.load → deserialize → signal load_efd即控制器眼中它仍是一个普通 L2 adapter,数据变换在内部透明完成,adapter 与控制器代码零改动。lookup / unlock / delete / eviction 等元数据操作则直通内层 adapter,不经过任何变换。
威胁模型与适用边界
加密保护的是能读取远程存储字节的一方(如拥有 S3 bucket、磁盘或 RESP 数据访问权限的人)。其范围界定为:
- 范围内:持久化到 L2 的字节(at-rest 机密性)。
- 范围外:L1(主机 RAM)与 L0(GPU)持有明文,因此能够访问运行中 MP server 进程的一方不在防御范围内——那是完全不同的威胁模型。
换言之,这是持久化层的静态加密(at-rest confidentiality),并非端到端加密。部署时需明确这一信任边界:加密抵挡"外部读盘者",但不抵御"进程内窥探者"。
算法选型:AES-128/256-GCM
LMCache 选择AEAD算法 AES-GCM,一次完成机密性 + 完整性校验。设计文档给出的选型理由非常具体:
- 性能:在服务器 AES-NI 硬件上,AES-128 是速度最快的认证加密算法(约每核 4–8 GB/s);且加密运行在推理热路径之外(L1↔L2,而非 L0↔L1 的 CUDA-IPC 传输),其开销可隐藏在 S3 延迟之后。
- 密钥长度:默认
aes_bits=128。AES-128 在计算上已不可破解,而 KV Cache 短生命周期且可重新生成,"先收割后解密"(harvest-now-decrypt-later)的长线攻击几乎不适用;256作为配置旋钮提供给合规要求强制 256 位的场景。 - 为何不是压缩:压缩不改变数据的机密性,且对 KV 是糟糕的匹配——高熵内容几乎压不动。量化才是体积杠杆,且量化与加密是组合关系(先量化、后加密)。
逐 chunk 线格式
每个 KV chunk 在 L2 中的存储帧为:
[1B version][12B IV][ciphertext || 16B GCM tag]- version:格式字节,允许方案演进而不破坏已存储 blob。
- IV:每 chunk 全新随机 96-bit nonce,明文存储(IV 本身不是秘密,但必须唯一——GCM 下重复的 (key, IV) 组合会破坏安全性)。
- ciphertext ‖ tag:AES-GCM 输出,密文与明文等长(无 padding),tag 为完整性校验。
由于无 padding,固定开销为29 字节/chunk(1 版本 + 12 IV + 16 tag),因此 aesgcm.py 中的estimate_serialized_size返回的是精确值plaintext + 29,而非保守上界——这与其他可能膨胀输出的压缩类 serde 形成鲜明对比(base.py 中对该接口的约定是"必须返回上界",而 aesgcm 可以做到精确)。
tag 不匹配(数据被篡改或密钥错误)时,底层cryptography库会抛出InvalidTag,包装器将其转为 load 失败 → 按cache miss处理(重新抓取/重算),绝不静默返回损坏数据。
反序列化长度推导:以dst为准
load 路径的临时缓冲按estimate_serialized_size分配,可能因分配器对齐而大于实际存储帧。因此 AesGcmDeserializer.deserialize 从dst(KV 目标缓冲,其字节数恰等于原始明文长度)推导精确密文长度,而非从可能被 padding 的src推导——这与fp8serde 从dst读取长度的做法一脉相承。测试 test_aesgcm.py 中的test_roundtrip_with_over_allocated_load_temp专门验证了这一场景:在帧尾追加 64 字节垃圾数据后,deserialize仍能正确还原明文。
密钥模型:cache_salt是选择器,不是密钥
这是本设计的核心安全语义。cache_salt是公开的(它明文出现在 L2 对象名中),它只负责选择"用哪把密钥";真正的秘密材料来自可替换的KeyProvider抽象(接口定义见 key_provider.py)。
HkdfKeyProvider(默认,随仓库发布)
实现 从一个主密钥(从master_key_path指向的文件读取,例如挂载的 K8sSecret)经HKDF-SHA256派生出每个cache_salt的独立密钥:
key(salt) = HKDF-SHA256(master_key, info=prefix + cache_salt)派生结果按 salt 缓存(带锁,线程安全——serde 线程池可能并发调用get_key)。其中info前缀固定为lmcache-l2-aesgcm-v1(源码中的_HKDF_INFO_PREFIX),用于域分离,保证不同加密方案派生的密钥互不冲突。
信任模型:fleet vs. outside。任何持有主密钥的人都能推导出所有租户的密钥,因此它保护的是 L2 字节免受"外部人"读取,不提供租户之间的相互隔离。
KeyringKeyProvider(规划中,未实现)
面向"每租户独立发放密钥"(KMS / per-tenant mount)的真正跨租户隔离,代价是真实的密钥分发/轮换成本。当前未实现,工厂会对key_provider="keyring"直接抛ValueError(aesgcm.py 与测试test_factory_unsupported_provider均可佐证)。
空cache_salt是合法租户桶
匿名流量的空cache_salt被视为一个有效的租户桶,HKDF 接受空的info并为其推导稳定密钥。测试test_empty_salt_roundtrips验证了这一行为。
配置:SerdeConfig.kwargs完整参数
加密 serde 通过 L2 adapter 配置中的"serde"JSON 子对象启用(SerdeConfig的定义见 base.py,工厂解析逻辑见 aesgcm.py)。参数如下:
| Key | 默认值 | 含义 |
|---|---|---|
key_provider | hkdf | 密钥来源;当前仅hkdf已实现 |
master_key_path | — | 主密钥文件路径(hkdf必填) |
aes_bits | 128 | 128或256,其余值(如192)被工厂拒绝 |
max_workers | 1 | serde 线程池大小 |
工厂的行为要点(均有对应测试):
- 缺少
master_key_path或文件为空 →ValueError(test_factory_missing_master_key_path); aes_bits=192→ValueError(test_factory_bad_aes_bits);- 成功构建时返回
AsyncSerdeProcessor,将同步的 Serializer/Deserializer 包装为带 eventfd 完成通知的异步接口(async_processor.py,接口约定见 base.py)。
实际配置示例(磁盘 L2 + aesgcm)
仓库提供了完整的端到端示例脚本 examples/serde/aesgcm/run_serde_aesgcm_example.sh,其中 L2 adapter 的配置形如:
{ "type": "fs", "base_path": "/tmp/lmcache_serde_aesgcm_example/disk", "serde": { "type": "aesgcm", "key_provider": "hkdf", "master_key_path": "/tmp/lmcache_serde_aesgcm_example/master.key", "aes_bits": 128 } }该脚本从生成主密钥开始,完整演示了"加密存储 → 清空 L1 → L2 预取解密"的闭环,并给出了一个非常实用的可验证检查点:落盘文件的第一个字节应为0x01(帧版本字节),而非原始 KV 数据——这是确认加密确实生效的最快方法。
端到端启用流程(基于仓库示例)
以下步骤取自 run_serde_aesgcm_example.sh(前置条件:已安装 vLLM 与 lmcache CLI,1 张可用 GPU):
生成主密钥(切勿入库或打日志):
# AES-128 → 16 字节;AES-256 → 32 字节 head -c 16 /dev/urandom > "$MASTER_KEY_PATH" chmod 600 "$MASTER_KEY_PATH"启动 LMCache MP server,启用 CPU L1 + 磁盘 L2(fs adapter)+ aesgcm serde:
lmcache server \ --l1-size-gb 20 \ --eviction-policy LRU \ --l2-store-policy default \ --l2-prefetch-policy default \ --l2-adapter "$L2_ADAPTER_JSON" \ --port 6555 \ --http-port 8080启动 vLLM,通过
LMCacheMPConnector接入(kv_connector_extra_config指向 lmcache 的 ZMQ 端口)。带
cache_salt发送推理请求(冷路径:L1 → L2 加密落盘);等待数秒让存储控制器刷盘;用ls/xxd验证 L2 文件首字节为01。清空 L1 缓存:
curl -X POST http://localhost:8080/cache/clear。重发同一请求:L1 miss → L2 命中 → 预取加密字节 → AES-GCM 解密还原 KV,vLLM 直接从缓存继续。
注意:请求体中的cache_salt字段(如示例中的tenant-a)同时是租户身份选择器与密钥选择器,两次请求必须携带相同 salt 才能命中彼此的密文。
正确性与隔离性的测试证据
test_aesgcm.py 不依赖 GPU 或 L1Manager(用暴露.byte_array的 ctypes 缓冲替身验证纯变换与工厂接线),其用例覆盖了本文讨论的全部关键性质:
- 精确开销:
estimate_serialized_size恒等于 明文 + 29(含单/多 tensor group 两种布局); - 往返还原:加密 → 解密恢复原始明文,包括 load 临时缓冲被 padding 的场景;
- 随机 IV:同一明文 + 同一密钥两次加密产生不同密文;
- 租户隔离:
alicesalt 加密的帧无法用bobsalt 解密(InvalidTag); - 完整性:翻转一个 tag 字节即触发
InvalidTag; - 畸形帧:版本字节/长度异常 →
ValueError; - AES-256 往返、空 salt 往返、工厂参数校验。
已知局限与后续工作
设计文档明确列出了本 serde不解决的问题,这是判断该方案适用场景的重要边界:
- 元数据泄露:内容加密不隐藏 L2 对象元数据——
cache_salt(租户身份)与内容派生的chunk_hash仍明文存在于对象名中,bucket 观察者无需解密即可看到"哪个租户存了什么"以及跨租户内容重叠。收口该问题需单独的"元数据加固"步骤(对 chunk hash 加盐、在入口对 salt 做假名化)。 - 每租户密钥隔离:
KeyringKeyProvider+ 租户→节点放置,规划中未实现。 - 信任模型边界:主密钥持有者(fleet)可推导全部租户密钥,加密不提供租户间隔离;对运行中进程的访问者同样不在防御范围内。
从源码结构看,这两项后续工作(keyring工厂分支、元数据加盐)在设计上均已预留扩展点(KeyProvider抽象接口 + 帧首的 version 字节),未来可通过注册新 provider / 升级帧格式平滑演进。
小结
aesgcmserde 是 LMCache 分布式缓存安全能力的关键一环:它以 29 字节/chunk 的精确固定开销,在 L1↔L2 链路上提供 AES-GCM 认证加密,通过cache_salt驱动的 HKDF 派生实现"每租户密文不同",并以 serde 插件形式零侵入地挂接到任意 L2 adapter。它解决的是L2 静态机密性这一明确问题,其信任边界(不防进程内窥探、不隐藏元数据、不隔离租户间密钥)同样清晰。对需要将 KV Cache 落盘到共享 S3/磁盘的部署,这是当前仓库内开箱即用、且有完整测试与示例支撑的推荐方案。
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考