Prefix cache
當很多請求共用相同的 prompt 前綴(例如 RAG system prompt、chat template、few-shot preamble)時,只做一次前綴 prefill,然後讓後續 decode 重用它,就可以避免每個請求都重跑一次 L 次 loop iteration 的 prefill。
這個原語叫做 PrefixCache。它是一個 opaque object,內部持有共享 prompt 在 loop block 所有 L 次迭代下的 KV 狀態。一個 PrefixCache 可以跨很多次 generate_from_prefix_cache 呼叫持續存在,也能支援同一前綴下多個並發或串行 decode。
Model.build_prefix_cache(prompt, loops, cache_window=0)
prefix = model.build_prefix_cache(prompt, loops=8, cache_window=0)
這個呼叫會對 prompt 在指定 loops 下跑完整 prefill,並回傳一個 PrefixCache handle,其 K/V tensors 會覆蓋 shared loop block 的每一次 loop iteration。這一步不會 decode;它只建立好一個可被一或多個 generate_from_prefix_cache 消費的前綴狀態。
參數
| 名稱 | 型別 | 預設 | 說明 |
|---|---|---|---|
prompt | np.ndarray[int32] | — | 共享前綴 token ids,形狀為 [seq_len]。後續所有 generate_from_prefix_cache 呼叫都必須使用相同的 loops 值,因為前綴是 depth-specialized。 |
loops | int | model.config()["default_loops"] | 建立前綴時使用的 loop 深度。 |
cache_window | int | 0 | Sliding-window 上限。0 代表完整快取(不淘汰);>0 則只保留最後 cache_window 個 token 的 KV,會失去更早位置資訊。 |
回傳
tinyloop_py.PrefixCache,一個 opaque object。其內部狀態包含:
- 一份完整的
RuntimeKVCache,容量為seq_len + (max_new_tokens from callers),並且在全部n_pre_blocks + loops個 cache layer 中都已填滿到seq_len - 一份 prompt token array 的拷貝(因此呼叫端原始 buffer 後續可自由修改)
- 一個 loop 深度欄位(後續
generate_from_prefix_cache會用它驗證參數)
其記憶體成本對 FP16 KV 而言約為 seq_len × D × (n_pre_blocks + loops) × 2 bytes。例如 seq_len=1024, D=2048, L=8 時約為 80 MB。若開啟 TINYLOOP_KV_H_MODE=1 TINYLOOP_KV_INT4=1,則可降到約 14 MB(詳見 KV cache modes)。
可能拋出的錯誤
RuntimeError:model已關閉、cache allocation OOM、或seq_len > max_seq_lenValueError:loops < 1或cache_window < 0
生命週期
只要你的共享工作負載還存在,就保留這個 PrefixCache。它由 Python 參考計數管理;底層 GPU 記憶體會在 handle 離開作用域或被顯式覆蓋時釋放。它不像 Model 那樣提供 close();交給 GC 處理即可。
常見兩種模式:
- 模組層級 cache。 啟動時建一次,之後每個請求都重用。最適合固定 system prompt 的情境。
- 按 prefix 分配 cache。 維護一個
{prefix_hash: PrefixCache}的字典,當數量或 VRAM 超出預算時再依 LRU 淘汰。
Model.generate_from_prefix_cache(prefix, max_tokens=32, loops=8, ...)
這個 API 會從 prefix 內已快取的狀態開始 decode。prefill skip 是隱含的,所以第一個新 token 的產生不需要再把 prompt 重跑一遍。
generated = model.generate_from_prefix_cache(
prefix,
max_tokens=32,
loops=8,
temperature=0.0,
top_k=50,
top_p=1.0,
cache_window=0,
repetition_penalty=1.0,
stop_sequences=[],
)
參數
| 名稱 | 型別 | 預設 | 說明 |
|---|---|---|---|
prefix | PrefixCache | — | 由 build_prefix_cache 回傳。 |
loops | int | — | 必須與建立 prefix 時的 loops 完全相同。binding 會驗證,不符就直接 raise。 |
max_tokens、temperature、top_k、top_p、cache_window、repetition_penalty、stop_sequences | 與 Model.generate 相同。 |
回傳
List[int],也就是完整 token stream:prefix + decode 後的 token。總長度為 len(prefix.prompt) + emitted。
可能拋出的錯誤
RuntimeError:model已關閉、OOM、或prefix與本次呼叫的loops不一致ValueError:max_tokens < 1
實際發生了什麼
內部流程如下:
- 複製
prefix的RuntimeKVCache。在h模式下這很便宜,主要是 per-layer pointer copy;在 K/V 模式下則是完整cudaMemcpy。這樣每次呼叫都拿到自己的 scratch-safe 副本,因此多個並發呼叫共享同一個prefix時不會競態。 - Decode
max_tokens個 token,像平常一樣重用剛複製出的 cache。 - 丟棄 這份 clone。原本的
prefix完全不變。
第 (1) 步就是你能讓一個 PrefixCache 同時供很多個請求使用的關鍵:每次呼叫都有自己的 KV 副本。主要成本是 clone 的記憶體配置,但相比它省下的 prefill 計算,通常非常划算。
多請求吞吐的實測
由於 loop block 會在每次 forward 中套用 L 次,TinyLoop 的 prefix cache 會在這 L 次迭代的每一層都保存 K/V。當多個 decode 呼叫共享同一個 PrefixCache 時,就能避免把整段 L-iteration prefill 重做 N − 1 次。絕對節省量是 (N − 1) × prefill_cost;理論上的速度提升會漸近於 (prefill + decode) / (prefill / N + decode)。
以下為 2026-04-17 在 model_407m_gptq_int4.tinyloop / H100 上,以 tests/bench_prefix_cache.py 量測的結果(NO_CACHE 每次請求都重建整段 prefix;SHARED 則只建一次 prefix,之後 N 次 continuation 全部重用;兩者都在相同 loop 深度 L = 8 下運行):
| backend | prefix | N | NO_CACHE | SHARED | speedup |
|---|---|---|---|---|---|
| INT4 scalar(預設) | 500 tok | 10 | 12 847 ms | 5 415 ms | 2.37× |
| INT4 scalar(預設) | 1024 tok | 10 | 23 852 ms | 8 329 ms | 2.86× |
| INT4 scalar(預設) | 1024 tok | 50 | 104 605 ms | 34 531 ms | 3.03× |
| FP16 body tensor core ¹ | 1024 tok | 10 | 5 123 ms | 4 732 ms | 1.08× |
¹ TINYLOOP_EXPERIMENTAL_FP16_BODY=1。在 tensor core 路徑下,prefill 本來就已接近 memory-bound,因此 prefix-share 的相對收益會變小。
實務建議
- 在預設 INT4-scalar body 下,對長前綴(大於約 500 token)的 RAG / 多使用者共享前綴工作負載,預期可拿到 2–3× 吞吐提升。
- FP16-body tensor-core 路徑 的相對收益會比較小,因為 prefill 本來就很便宜。prefix cache 仍然正確、也仍然便宜,只是沒有那麼戲劇性。
- 重用同一個
PrefixCache。 不要每個請求都重建一次,否則就失去 prefix cache 的意義。 loops必須一致。 在L=8建好的 prefix,不能直接拿去服務L=16的 decode。若你需要多個深度,請使用 warm-start mid-loop,或維護一份「每個深度各一個 PrefixCache」。
與其他 API 的關係
- Prefix cache vs warm-start mid-loop: prefix cache 是為了在相同
L下,讓很多請求共用一段 prompt;warm-start 則是在單一請求內,讓不同L之間共用同一段 prompt。兩者可以在概念上組合,但 API 還沒接通(已列為後續事項)。 - Prefix cache vs speculative decode: speculative decode 是在一次生成過程中,用同一權重的兩個深度互相配合;prefix cache 則是讓不同生成共用相同深度下的前綴狀態。兩者是正交能力,可以疊加。
- Prefix cache 與 KV cache modes: prefix cache 會繼承
generate所使用的 KV 儲存模式(FP16、INT8,或三種h模式)。h模式可讓 prefix cache 大約再縮一半;若使用 INT4-h +fp16_body,前綴共享與延遲降低可以雙重疊加。
錯誤條件
| 錯誤 | 原因 | 解法 |
|---|---|---|
RuntimeError: model was closed | 在已關閉的 Model 上呼叫 | 重新建立 Model |
RuntimeError: loops mismatch | generate_from_prefix_cache 的 loops 與建構時不同 | 對齊參數,或在目標深度重建 prefix |
RuntimeError: out of memory | 前綴太長 / cache 預算不足 | 縮短 seq_len、降低 L、開啟 INT4-h、或限制 cache_window |
ValueError: max_tokens < 1 | 字面意思 | 改成正整數 |