KV 快取投毒與利用
專家4 分鐘閱讀更新於 2026-03-13
KV 快取於 transformer 推論中如何運作、共享部署中的跨請求快取投毒、前綴快取攻擊,以及跨租戶資料洩漏。
KV 快取 是 transformer 推論的基礎最佳化。透過快取先前 token 的注意力 key 與 value 張量,模型便可避免自迴歸生成時反覆計算。在多租戶部署中,KV 快取共享製造了位於應用層之下的跨租戶攻擊面。
KV 快取如何運作
自迴歸生成時,每個新 token 皆注意所有先前 token。無快取時,這需要在每一步對整個序列重算 key 與 value 投影:
# 無 KV 快取:n 個 token 總計算 O(n^2)
for t in range(seq_len):
# 每步對所有 token 0..t 重算 K、V
K = W_k @ X[:, :t+1, :] # [batch, t+1, d_k]
V = W_v @ X[:, :t+1, :] # [batch, t+1, d_v]
Q = W_q @ X[:, t:t+1, :] # [batch, 1, d_k]
output = attention(Q, K, V)
# 有 KV 快取:總計算 O(n)
kv_cache = {}
for t in range(seq_len):
# 只計算「新」token 的 K、V
k_new = W_k @ X[:, t:t+1, :]
v_new = W_v @ X[:, t:t+1, :]
kv_cache['K'] = torch.cat([kv_cache.get('K', empty), k_new], dim=1)
kv_cache['V'] = torch.cat([kv_cache.get('V', empty), v_new], dim=1)
Q = W_q @ X[:, t:t+1, :]
output = attention(Q, kv_cache['K'], kv_cache['V'])快取記憶體需求
| 模型大小 | 層數 | 頭數 | d_k | 每 token 快取 | 4K 上下文快取 |
|---|---|---|---|---|---|
| 7B | 32 | 32 | 128 | 256 KB | 1 GB |
| 70B | 80 | 64 | 128 | 1.3 MB | 5.2 GB |
| 405B | 126 | 128 | 128 | 4.1 MB | 16.4 GB |
攻擊向量 1:前綴快取投毒
前綴快取(vLLM、TGI 與多數生產推論框架採用)將共通前綴的 KV 狀態存下並跨請求重用。這於請求間建立共享狀態。
攻擊
若攻擊者能影響已快取的前綴狀態,所有後續重用該前綴的請求皆繼承被投毒的注意力脈絡:
辨識共享前綴
判定哪個系統提示或前綴被跨請求共享(系統提示通常對同一服務所有使用者皆相同)。
打造投毒請求
送出在處理過程中會修改該共享前綴快取 KV 狀態的請求。這需要推論框架對快取失效處理不當。
後續請求繼承被投毒狀態
其他使用者重用該快取前綴的請求,其注意力脈絡即含攻擊者之影響。
# 概念:vLLM 式系統中前綴快取如何運作
class PrefixCache:
def __init__(self):
self.cache = {} # hash(prefix_tokens) -> kv_states
def get_or_compute(self, prefix_tokens, model):
key = hash(tuple(prefix_tokens))
if key not in self.cache:
# 計算並快取此前綴的 KV 狀態
self.cache[key] = model.compute_kv(prefix_tokens)
return self.cache[key]
# 漏洞:若快取條目是可變參照,
# 某請求就地修改 kv_states 將投毒所有未來
# 共享該前綴的請求攻擊向量 2:跨租戶 KV 快取洩漏
在多使用者共享 GPU 記憶體的部署中,KV 快取重用可能於租戶間洩漏資訊。
資訊洩漏通道
| 通道 | 機制 | 洩漏資訊 |
|---|---|---|
| 快取命中時序 | 共享前綴產生較快回應 | 另一租戶是否使用相同前綴 |
| 記憶體重用 | 前租戶未清除的 GPU 記憶體 | 先前 KV 狀態的片段 |
| 容量爭用 | 某租戶的長上下文逐出另一租戶快取 | 依時序推論其他租戶活動 |
時序側通道攻擊
import time
import httpx
async def probe_prefix_cache(api_url: str, test_prefix: str) -> float:
"""量測回應延遲以偵測被快取的前綴。
快取命中(共享前綴)會產生明顯較低延遲。"""
start = time.perf_counter()
response = await httpx.AsyncClient().post(api_url, json={
"prompt": test_prefix + " Continue.",
"max_tokens": 1, # 最小化生成時間
})
elapsed = time.perf_counter() - start
return elapsed
# 比較各候選系統提示的延遲
# 延遲較低 = 可能為被快取前綴 = 另一租戶的系統提示
candidates = [
"You are a helpful assistant for AcmeCorp...",
"You are a financial advisor. Never disclose...",
"You are a medical chatbot. Always recommend..."
]
for prefix in candidates:
latency = await probe_prefix_cache(api_url, prefix)
print(f"Latency: {latency:.4f}s - {prefix[:50]}...")攻擊向量 3:PagedAttention 利用
PagedAttention(vLLM 採用)以類似作業系統虛擬記憶體的頁表管理 KV 快取記憶體。這雖啟動記憶體共享,但也引入頁層級攻擊面:
頁表操弄
- Copy-on-write 繞過 —— 若頁表未正確實作 COW 語意,某序列的寫入可能影響共享分頁
- 頁逐出攻擊 —— 藉由產生大量並行長序列強制關鍵快取頁被逐出,造成其他租戶的快取抖動
- 碎片分析 —— 已配置但未初始化的頁可能含先前請求的 KV 殘留
# 透過分析歷史脈絡上的異常注意力模式,
# 偵測可能自前一請求留下的 KV 快取殘留
def detect_cache_residue(model, clean_prompt, suspicious_prompt):
"""比較乾淨與疑遭汙染快取狀態之注意力模式。"""
clean_attn = get_attention_weights(model, clean_prompt, use_cache=False)
cached_attn = get_attention_weights(model, suspicious_prompt, use_cache=True)
# 殘留會以「注意到目前提示之外位置」呈現
divergence = compute_kl_divergence(clean_attn, cached_attn)
return divergence防禦:快取隔離架構
隔離層級
| 層級 | 機制 | 效能成本 | 安全性 |
|---|---|---|---|
| 無隔離 | 共享快取、共享分頁 | 基線 | 無 |
| 前綴隔離 | 各系統提示獨立快取 | 10–20% 記憶體開銷 | 中 |
| 租戶隔離 | 各租戶獨立快取池 | 30–50% 記憶體開銷 | 高 |
| 請求隔離 | 請求間不重用快取 | 延遲增加 2–5 倍 | 最高 |
落實檢核表
- 不可變快取條目 —— 將已快取的 KV 狀態存為唯讀張量,永不允許就地修改
- 快取鍵含租戶 ID —— 即便前綴相同,也防止跨租戶快取命中
- 記憶體歸零 —— 於重新配置前清除 GPU 記憶體頁以防殘留洩漏
- 快取命中監控 —— 逐租戶記錄快取命中率以偵測探測攻擊
相關主題
Knowledge Check
在共享 LLM 部署中,前綴快取如何可用於擷取另一租戶的系統提示?
參考資料
- Efficient Memory Management for Large Language Model Serving with PagedAttention (Kwon et al., 2023) -- PagedAttention / vLLM
- SGLang: Efficient Execution of Structured Language Model Programs (Zheng et al., 2023) -- RadixAttention 前綴快取