跳到主要內容

【2026 最新】KV Cache 隔離怎麼驗?四個假請求看懂拒絕後續跑與殘留頁

最後更新: ·
KV Cache 隔離教學:拒絕請求必須終止交接,讀取前核對頁面所有權

你把兩家公司的聊天機器人放在同一套推論服務,過長請求也回了錯誤,就算完成 KV Cache 隔離了嗎?還要追問:錯誤之後,後面的工作者是否真的停下來,還是繼續讀了一批不屬於這次請求的記憶體?

這篇寫給第一次接觸模型服務的讀者。先用共用白板理解原理,再用已有的 Python 3,在 CPU 跑四個全是假資料的請求。你會學到的,是替「拒絕」寫出可檢查的完成條件;概念段不需要程式背景,操作段只需建立文字檔並執行指令。

AlphaLab 於 2026 年 10 月 31 日在 macOS、Python 3.9.6 執行下方程式並保存輸出。它以字串代表快取頁,只驗證刻意安排的狀態錯誤;沒有載入模型、呼叫線上服務或執行 GPU/RDMA 傳輸。

先說結論:KV Cache 隔離,要驗收拒絕之後發生什麼

安全讀取=有權使用的頁面+完整有效的資料+一致的請求狀態。像兩個人輪流用白板:擦不擦乾淨是一件事,誰有權看哪一區、是否已寫完、被取消的人會不會繼續寫,是另外三件事。

  • 收到錯誤只是入口結果;還要核對配置、傳輸與生成的紀錄。
  • 在本篇的提前拒絕設計中,過長請求應在交接前終止,三項計數都為零。
  • 玩具程式通過,只代表這份小程式符合斷言;正式引擎仍需自己的版本、容量與故障測試。

一、先分清四個零件:權重不是每位使用者的筆記

模型權重像共用教科書,描述模型學到的參數;KV Cache 則像處理這次文字時寫下的運算筆記。依 Hugging Face 的快取說明,它保存注意力層先前 token 的 key/value,讓後續生成重用。它是張量,不是原文檔案的逐字副本;下方可讀字串是為了把所有權問題看清楚。

  • ① 權重「教科書」:多個請求可以使用同一模型,不等於它們可讀彼此的工作狀態。
  • ② 頁池「一疊可回收白板」:引擎將可用記憶體分配給請求;釋放使用權與把內容清零是不同操作。
  • ③ Prefill「先讀題、寫筆記」:處理輸入,建立後面生成所需的 KV。
  • ④ Decode「拿筆記接著寫答案」:讀取有效 KV,逐步生成;分開部署時,兩邊還要交接資料與狀態。

SGLang 的 PD 分離文件說明兩階段的分工,並記載預設 decode 可在 prefill 完成前預留目的頁。預留位置不等於內容已準備好。想先補齊整套服務的定位,可讀 本機 LLM 推論引擎硬化指南;本文接著專攻頁面交接。

KV Cache 隔離:預留頁、寫入頁與拒絕狀態分開驗收
頁面被預留,只代表位置已分配;是否寫完、是否屬於本請求,要在讀取前另外確認。

二、為何拒絕了還會串資料?看一條錯誤交接

這個問題來自真實事故,但發生與披露日期要分開。Cloudflare 於 10 月 26 日公開的事故報告稱,2026 年 8 月 27 日至 9 月 9 日,Workers AI 的 GLM-5.3 端點曾有跨請求快取曝光。其配置允許的上下文長度大於 prefill 實際 KV 容量,使過長請求走到拒絕路徑;這是該部署與引擎交接的問題。

關鍵在於「準備回錯誤」與「已完成」被放在不同狀態欄。SGLang 修補 PR #38935記錄:拒絕先寫入 to_finish,但 finished()讀的是 finished_reason。若入場只問後者,已拒絕的請求仍可能被當成活躍工作,帶著一個 token 的替代輸入繼續交接。

用白板想像:decode 已預留五格,prefill 只交回一格,卻說「好了」。decode 若照原本五格去讀,其餘格可能仍留著別人的內容。錯誤不是模型突然學會另一位使用者的資料,而是這次計算讀到了不該讀的狀態。五格是本文的縮小例子,並非事故的實際頁數。

修補次序也要分清:較早的 #36988已在 prefill 結果回來時退役已中止請求,阻止原先的錯誤輸出;#38935 再把入場拒絕提前到交接前,減少無用工作與等待。下方示範採用提前停止的設計,不把後續最佳化當成首次修正。

另一類問題是取消時機:已送出的傳輸仍在路上,目的頁卻先被回收給下一人。SGLang 團隊的技術說明把拒絕入場與非同步傳輸的生命週期分開處理。下面先測前者;稍後再說正式環境該如何補上後者。

三、KV Cache 隔離練習:四個假請求,先寫預期再執行

第一步「留一張舊白板」:準備 A、B 兩位假租戶

建立一個新目錄,將下方完整程式存成 kv_toy.py。每個測例都新建八格頁池,先放入 A 的四個標記,再假設 A 已結束、頁面可重用,但字串仍在。B 的 prefill 容量刻意設成四格,decode 有八格;每格代表一個抽象單位,不換算真實引擎的 token 或 bytes。

這裡的痛點是:空位不一定等於空內容。先種入可辨識的假標記,才能知道 B 讀到的是誰的狀態;別用真實聊天、帳號或個資當探針。所有操作只在 Python 清單內完成。

第二步「先列考卷」:四組各要抓哪一種錯?

  • 正常:B 要三格,寫三格、讀三格,只能看到 B_ONLY_。
  • 超容量且停止:B 要五格,單節點示意分支提前回錯誤;配置、傳輸、生成都為零。
  • 拒絕後續跑:B 要五格,故意略過停止判斷,只寫一格卻讀五格;應抓到 A 的舊標記。
  • 修補後提前拒絕:同樣五格,讓分離模式也在交接前返回;三項計數為零,頁池內容與所有權維持原值。

第二與第四組故意共用相同的停止行為:比較的是錯誤是否也能跨分離路徑生效。這不是兩種引擎實作;single_node、buggy_pd、fixed_pd只是本程式的教學分支名稱。

"""CPU-only lifecycle model; strings stand in for KV tensors. No network."""
import json
import platform
import sys

class Pool:
    def __init__(self):
        self.data = [None] * 8
        self.owner = [None] * 8

    def seed(self):
        for i in range(4):
            self.data[i] = 'A_ONLY_' + str(i)
            self.owner[i] = 'A'
        # A has finished. Slots are reusable; bytes remain on purpose.


def request(pool, n, mode):
    trace = {'requested': n, 'allocated': 0, 'transferred': 0,
             'decoded': 0, 'rejected': n > 4, 'read': [], 'foreign': []}
    # Our toy's prefill capacity is 4; its decode pool holds 8 slots.
    if trace['rejected'] and mode != 'buggy_pd':
        trace['status'] = 'error'
        return trace
    slots = list(range(n))
    trace['allocated'] = len(slots)
    written = 1 if trace['rejected'] else n
    for i in slots[:written]:
        pool.data[i] = 'B_ONLY_' + str(i)
        pool.owner[i] = 'B'
    trace['transferred'] = written
    # Deliberate defect: handoff "complete" ignores rejected and written<n.
    trace['read'] = [pool.data[i] for i in slots]
    trace['foreign'] = [i for i in slots if pool.owner[i] != 'B']
    trace['decoded'] = 1
    trace['status'] = 'ok'
    return trace


def run():
    cases = [('normal', 3, 'fixed_pd'),
             ('oversize_rejected', 5, 'single_node'),
             ('rejected_but_continues', 5, 'buggy_pd'),
             ('patched_rejection', 5, 'fixed_pd')]
    result = {}
    for name, n, mode in cases:
        pool = Pool()
        pool.seed()
        before = (pool.data[:], pool.owner[:])
        row = request(pool, n, mode)
        if name == 'normal':
            assert row['read'] == ['B_ONLY_0', 'B_ONLY_1', 'B_ONLY_2']
            assert not row['foreign'] and row['transferred'] == n
        elif name == 'rejected_but_continues':
            assert row['rejected'] and row['decoded'] == 1
            assert row['transferred'] < row['requested']
            assert 'A_ONLY_1' in row['read'] and row['foreign']
        else:
            assert row['status'] == 'error'
            assert row['allocated'] == row['transferred'] == row['decoded'] == 0
            assert before == (pool.data, pool.owner)
        result[name] = row
    return {'python': sys.version.split()[0], 'platform': platform.platform(),
            'scope': 'strings, sequential CPU toy, no model/GPU/RDMA',
            'cases': result, 'assertions': 'PASS'}

if __name__ == '__main__':
    print(json.dumps(run(), ensure_ascii=False, indent=2))

第三步「拿到收據」:跑程式,再核對實際讀取

python3 --version
python3 kv_toy.py > toy-results.json
python3 -m json.tool toy-results.json

這個檔案只使用 Python 標準函式庫。若電腦已有 Python 3,可直接執行;成功時最後會有 "assertions": "PASS"。不要加 -O,因為 Python 的最佳化選項會移除 assert,讓這份練習失去檢查。若出現 AssertionError,先保留輸出並對照檔案,不把它當作通過。

本次執行中,正常組得到三個 B 標記。故障組則記錄 requested=5、allocated=5、transferred=1、decoded=1,讀到 B_ONLY_0、三個 A_ONLY_,以及一個尚未寫入的 null。foreign列的是非 B 所有的索引,包含最後一格未初始化位置;不能把四個索引全算成四份 A 資料。

兩個停止組的三項計數都是零,且頁池未被改寫。注意 decoded=1只代表玩具執行一次讀取階段,不是生成一個真實 token。PASS 的意思是:正常、預期故障與修補分支都符合事先寫好的斷言。它不是說故障分支安全。

四、把小實驗移植成正式服務的五項驗收

接正式引擎時,痛點會變成「我只看得到 HTTP,怎麼知道後面停了?」解法是沿同一 request ID 對帳,讓入口、prefill、傳輸與 decode 的事件能串成一條線。下面是你要向自己的測試環境收集的證據,而非已在 AlphaLab 完成的服務驗收。

  • ① 固定版本:記錄引擎版本、映像 digest、模型 revision、傳輸後端與啟動參數。容器內可用 python3 -c "import importlib.metadata as m; print(m.version('sglang'))"查安裝套件版本;自建映像還需核對實際 commit 與回補修補。
  • ② 量實際容量:保存每個 prefill/decode 節點的啟動日誌與 KV 容量,對照上下文限制、批次與生成預算。權重、量化、平行配置或記憶體比例改動後重查,別只抄模型宣傳上限。
  • ③ 驗拒絕終止:在授權的隔離測試部署,用假租戶做容量邊界內、邊界上、邊界外請求。若設計是入場前拒絕,就驗證拒絕後沒有 bootstrap、頁配置、傳輸或生成事件;已開始的取消路徑另列。
  • ④ 驗完整交接:對帳原始有效長度、實際完成的頁範圍與可讀範圍。部分傳輸、錯誤訊號與成功訊號不一致時,應停止讀取並清理;別把「收到完成通知」單獨當成內容完整的證據。
  • ⑤ 驗回收時機:另測中途取消、逾時、傳輸失敗與頁面重用。保留取消、排空確認、釋放與下一請求取得頁面的先後;測後再送正常請求,確認服務可恢復。

截至 2026 年 10 月 31 日本次查閱,官方 v0.5.22 release已發布;Cloudflare 的披露建議分離部署使用 v0.5.22 或更新版本,或按需求回補相關修補。把它當作這次公告的檢查起點,接著核對你用到的修補與測例,不把版本號當成整套服務安全的認證。

還要看傳輸後端:#45973另處理 NIXL/MORI 寫入失敗時的確認訊號,LMSYS 披露將它列入 v0.5.23。這是待核對實際建置的修補項目,不是本文要求新手安裝未驗證版本的指令。

取消後的延遲釋放,重點是等先前寫入排空再重用頁面。SGLang 團隊也明說:逾時本身不證明傳輸已排空;靠逾時釋放,仍需確保在途寫入已停止。下方字串程式沒有執行緒或網路,因此這一項必須留在正式後端的故障測試。想擴充跨 Worker 的觀測,可接著讀 KV Cache 路由、分層與故障演練。

五、三個常見坑:清零、salt、HTTP 各回答不同問題

「把頁面清零就好了?」清零能處理舊內容殘留,但若舊傳輸晚到,或錯誤請求仍讀取未完成頁,狀態與生命週期仍需修正。驗收應同時看所有權、有效範圍與在途使用者,不能只看頁面是否一度為零。

「加 cache_salt 就隔離了?」vLLM 現行安全文件明確區分:cache_salt降低特定前綴快取風險,並非共用服務程序的租戶隔離邊界。需要租戶隔離時,文件要求在部署架構落實,例如每租戶專屬實例與會驗證身分、限定快取識別值的 gateway。這不能用來推定另一個引擎有相同旗標。

「回 200、回答又流暢,應該成功?」流暢回答也可能建立在錯誤狀態上。正常組要驗內容歸屬;拒絕組要驗停止與錯誤傳遞。把命中率和延遲當成另一張成績單,可用 Prefix Cache 驗證教學補齊效能測試的分母。

常見問題:八個先講答案的判讀

1. KV Cache 就是聊天紀錄資料庫嗎?

不是。它主要保存推論中的注意力計算狀態;應用層聊天紀錄與長期記憶另有儲存與權限。實際服務還可能保留相容前綴供後續請求重用,生命週期要另外定義。

2. 這代表模型權重記住別人的新訊息嗎?

本案例指向服務端狀態交接。從錯頁讀取造成的輸出,不能直接推論為模型權重被即時更新。先查資料流,再決定要修哪一層。

3. 本篇四組結果算重現 Cloudflare 漏洞嗎?

不算。這是自行編寫的狀態模型,只有字串、循序執行與刻意安排的缺陷,沒有執行受影響版本或真實注意力。它讓你學會設計能抓錯的測例。

4. 沒看到 A_ONLY_,就證明沒有洩漏嗎?

不能由一次搜尋下結論。這份玩具同時核對頁所有權與實際讀取;真實模型未必逐字吐出標記。正式測試還需覆蓋取消、重試、並行、容量壓力與資料完整性。

5. 只把入口長度限制調小就夠了嗎?

還要驗引擎自己的拒絕路徑。代理層限制、各節點實際容量與錯誤傳遞各有證據;配置避開一條故障路徑,與修正該路徑,是兩份工作。

6. 正式環境可以直接貼這段玩具修補嗎?

不可以。清單與字串沒有真實引擎的頁表、引用計數、同步與資源回收。移植的是測試條件;實作應使用已審查的引擎修補,並在相同後端驗證。

7. 沒有 GPU 的新手能完成哪一步?

能完成本篇全部 CPU 練習,並寫好正式驗收單。保留程式、Python 版本與 JSON 輸出;GPU、真實生成與傳輸項目維持未測,不填成零故障。

8. 一般使用雲端聊天產品的人要做什麼?

可以先學會問供應商三個問題:租戶邊界在哪裡、拒絕是否跨節點終止、故障後何時才重用頁面。引擎內部計數由維運者提供;不要拿他人的正式服務當測試目標。

給新手的三個重點

  • 先分權重、快取與請求狀態,才能把問題交給對的系統層處理。
  • 先寫出應失敗的分支,再檢查修補是否阻止同一條路徑。
  • 成功讀取要有完整有效的資料;拒絕與取消則要有清楚的停止、排空與回收證據。

接著閱讀

左右滑動查看更多推薦

下一步:今天先讓一個拒絕案例留下完整收據

現在就建立 kv_toy.py,跑出四組 JSON,親手圈出故障組的「只寫一格、卻讀五格」。再寫下一條自己的驗收規則:有權、寫完、狀態一致,才允許讀。想把這種小測試接進完整 AI 工程流程,可從 AlphaLab 課程安排下一個實作,或到 AI 專區選一個相鄰概念接著練。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

週日 Weekly 週報+週二、週四 Alpha Signal,每週最多三封。

我們不會 spam,隨時可退訂。已訂閱?管理訂閱偏好(會寄登入連結到你的信箱)

主題篩選與退訂說明

寄送日期以台灣時間為準。主題偏好會篩選每期精選,並非每篇新文章的即時通知;當期符合主題的文章不足時會略過寄送。

未勾選任何主題會收到所有主題的精選,並不會停止寄信。

取消勾選 Alpha Signal 可停止週二、週四短報,仍保留週日週報。若要停止所有電子報,請使用信件底部的「取消訂閱」。