你把兩家公司的聊天機器人放在同一套推論服務,過長請求也回了錯誤,就算完成 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 推論引擎硬化指南;本文接著專攻頁面交接。

二、為何拒絕了還會串資料?看一條錯誤交接
這個問題來自真實事故,但發生與披露日期要分開。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 專區選一個相鄰概念接著練。






