跳至主要内容

快取重用與狀態轉換 API

TinyLoop 提供三種重用原語,各自針對不同的重用軸做最佳化。三者皆為明確、由呼叫端管理的 handle:它們擁有 GPU 記憶體、必須被釋放,且在同一個 Model* 上若無外部同步不能被並行的 generate* 呼叫共享。

Handle重用軸典型用途
PrefixCache相同 prefix、相同 loop 深度、多個請求跨使用者 session 共享 system prompt。
ResumeHandle相同 prefix、不同 loop 深度將低深度草稿升級為更深的最終生成。
PrefixPool多個 prefix、內容可定址的派發具備多個已知 scaffold 的多租戶服務。

PrefixCache

PrefixCache 捕捉固定 (prompt, loops) 組合之 post-prefill hidden state + RuntimeKVCache。每一次後續的 generate_from_prefix_cache 呼叫都會把此狀態複製至全新的工作 buffer 並從複本繼續,完全略過 prefill。

build_prefix_cache

PrefixCache* build_prefix_cache(Model* model,
const int32_t* tokens,
int seq_len,
int n_loops = 8,
int cache_window = 0);

從 prompt 建構 prefix cache。執行一次完整 prefill 並儲存結果。

參數

名稱型別預設描述
modelModel*已載入的模型指標。不得為 nullptr
tokensconst int32_t*Prompt token id,長度為 seq_len
seq_lenint1 <= seq_len <= max_seq_len。Cache 大小恰為 seq_len;後續 decode 會在此之上增長。
n_loopsint8計算 prefill 所用的 loop 深度。必須與下游 generate_from_prefix_cache 呼叫的 config.loops 相符。
cache_windowint0Cache 層的滑動視窗上限。0 = 無上限。

回傳

PrefixCache* — 不透明 handle。失敗時回傳 nullptr(罕見;大多數失敗路徑會中止)。

備註

  • 所有權。 呼叫端擁有回傳的指標。必須呼叫 free_prefix_cache 一次。
  • VRAM。 Prefix cache 保有 seq_len 下的完整 RuntimeKVCache 以及 residual 快照。記憶體依 seq_len * n_layers * 2 * head_dim 位元組(加額外 overhead)縮放;配置時請考量磁碟/VRAM 預算。
  • 建構後不可變。 無法修改已建構的 PrefixCache。若要擴充 prefix,請建構新的。

free_prefix_cache

void free_prefix_cache(PrefixCache* prefix_cache);

釋放 prefix cache 持有的所有 GPU 記憶體。

備註

  • 傳入 nullptr 是安全的(no-op)。
  • 當有 generate_from_prefix_cache 呼叫正使用此 handle 時,不得呼叫。

generate_from_prefix_cache

std::vector<int32_t> generate_from_prefix_cache(Model* model,
const PrefixCache* prefix_cache,
const GenerateConfig& config);

prefix_cache 捕捉的狀態開始 decode。等同於以相同 prompt 呼叫 generate,但省略 prefill 成本。

參數

名稱型別預設描述
modelModel*必須是與建構 prefix cache 相同的模型實例;混用未定義。
prefix_cacheconst PrefixCache*預建 prefix handle。
configconst GenerateConfig&Decode config。config.loops 必須等於建構時的 n_loops;不符即為契約錯誤。

回傳

std::vector<int32_t> — 完整 token 流:prompt token(自 prefix cache 還原)後接 decoded token。

錯誤條件

  • config.loops 與建構時的 n_loops 不符 — 以 stderr 診斷中止。
  • config.grammar 已設定但與 prefix 的 token 歷史不相容 — 中止。
  • seq_len + config.max_tokens > max_seq_len — 中止。

範例

// 為共享 system prompt 建構一次
auto* prefix = tinyloop::build_prefix_cache(model, sys_prompt.data(), sys_prompt.size(), 8);

// 跨多次請求重用
tinyloop::GenerateConfig cfg;
cfg.loops = 8;
cfg.max_tokens = 128;
for (const auto& user_turn : batch) {
cfg.stop_sequences = user_turn.stops;
auto out = tinyloop::generate_from_prefix_cache(model, prefix, cfg);
// prefix 未被修改;可安全重用
}

tinyloop::free_prefix_cache(prefix);

ResumeHandle

ResumeHandle 以 loop 深度 L_used 捕捉狀態,但預先配置 max_loops 深度所需的 cache 容量。你以淺深度執行 prefill 來建構它,之後即可「resume」生成到任意 new_L > L_used 而不必重跑前 L_used 次迭代。這就是 warm-start 路徑 — 架構特有的升級原語。

build_resume_handle

ResumeHandle* build_resume_handle(Model* model,
const int32_t* tokens,
int seq_len,
int L_used,
int max_loops = 32,
int cache_window = 0);

建構具有 loop 擴充空間的 resume handle。

參數

名稱型別預設描述
modelModel*已載入的模型指標。
tokensconst int32_t*Prompt token id。
seq_lenint1 <= seq_len <= max_seq_len
L_usedintHandle 初始化所用的 loop 深度。超過第 L_used 次迭代的 cache 層以零填充為 placeholder。
max_loopsint32resume_generate 可呼叫的 loop 深度上限。Handle 依此深度配置 cache 容量。
cache_windowint0滑動視窗上限。

回傳

ResumeHandle* — 不透明 handle。失敗時回傳 nullptr

錯誤條件

  • L_used < 1L_used > max_loops — 中止。
  • max_loops > 64 — 中止。
  • 過量配置期間 OOM — 中止。

備註

  • 刻意的過量配置。 Handle 的 cache 按 max_loops 次迭代的 cache 層配置,即便只填了 L_used 層。這提前付出記憶體成本以避免 resume 時重新配置。
  • Residual 快照。 Handle 還儲存第 L_used 次迭代時的 residual stream 快照,這是 skip-ahead resume 路徑正確性所需的。

resume_generate

std::vector<int32_t> resume_generate(Model* model,
const ResumeHandle* handle,
int new_L,
const GenerateConfig& config);

以更深的 loop 深度 new_L 繼續 decode,在啟動 decode loop 前僅執行 [L_used, new_L) 區間的迭代。

參數

名稱型別預設描述
modelModel*建構此 handle 的相同模型。
handleconst ResumeHandle*預建 resume handle。
new_Lint目標 loop 深度。必須滿足 handle.L_used <= new_L <= handle.max_loops
configconst GenerateConfig&Decode config。Prefill 部分忽略 config.loops(由 new_L 覆寫);decode 每步的 loop 次數仍使用該值。

回傳

std::vector<int32_t> — 完整輸出序列。

內部狀態機

  1. 將 handle 的 cache 複製到 scratch 工作 buffer。
  2. 將 residual 快照還原至 buffers.main
  3. 在共享 loop block 上執行 run_loop_iters_range(L_used, new_L),將 residual 從深度 L_used 推進到深度 new_L
  4. 由推進後的狀態合成一個暫時 prefix cache。
  5. config.loops 每步的迭代次數執行標準 decode loop。
  6. 離開時釋放 scratch cache;原始 handle 未被修改。

實測效益

在 407M INT4 artifact 上,從 L_used = 4 resume 至 new_L = 8 相較於從頭執行 generate(loops=8) 可節省 32.5 % wall-clock。平衡點為 N = 1 次後續使用,即建構成本在一次 resume 後即被攤平(見論文 §6)。

free_resume_handle

void free_resume_handle(ResumeHandle* handle);

釋放 handle 的 GPU 記憶體。

備註

  • 傳入 nullptr 是安全的。
  • 不得與正在使用此 handle 的進行中 resume_generate 呼叫重疊。

PrefixPool

內容可定址的 PrefixCache 實例池。呼叫 generate_with_pool 時,池會找到最長的已註冊 prefix(且為目前 prompt 的前綴),並使用該 prefix 的 cache 進行 decode;剩餘(後綴)token 會經過迷你 prefill。

create_prefix_pool

PrefixPool* create_prefix_pool(size_t capacity = 16);

建構空池。

參數

名稱型別預設描述
capacitysize_t16已註冊 prefix 的最大數量。溢出時,LRU prefix 會被逐出並釋放。

回傳

PrefixPool*

register_prefix

int register_prefix(PrefixPool* pool,
Model* model,
const int32_t* tokens,
int seq_len,
int n_loops = 8,
int cache_window = 0);

建構一個 prefix cache 並註冊到池中。回傳整數 id;如有需要可用相同 id 追蹤該 prefix 的生命週期。

參數

名稱型別預設描述
poolPrefixPool*目標池。
modelModel*用於建構的模型。
tokensconst int32_t*Prefix token id。
seq_lenintPrefix 長度。
n_loopsint8Prefix cache 的 loop 深度。
cache_windowint0選擇性的 cache window 上限。

回傳

int — 已註冊 prefix 的識別碼。值為小型正整數,可能在逐出後重用。

備註

  • 雜湊。 Prefix 查找採用 token 序列相等比較。只有精確 prefix 匹配才算數 — 以與已註冊 100-token prefix 相同的 200 個 token 開頭的 prompt 僅匹配該 100 個 token;一個 off-by-one 的 token 即不符。
  • 同一模型不變式。 註冊到同一個池的所有 prefix 必須以相同 Model* 建構;混用未定義。

generate_with_pool

std::vector<int32_t> generate_with_pool(Model* model,
PrefixPool* pool,
const int32_t* prompt,
int prompt_len,
const GenerateConfig& config);

Decode 一個 prompt,重用 pool 中最長的已註冊 prefix。

參數

名稱型別預設描述
modelModel*與池的模型相同。
poolPrefixPool*預建池。
promptconst int32_t*完整 prompt id。
prompt_lenintPrompt token 數。
configconst GenerateConfig&Decode config。config.loops 必須與池中註冊 prefix 的 n_loops 相符。

回傳

std::vector<int32_t> — 完整輸出。

行為

  • 若任何已註冊 prefix 是 prompt 的前綴,選擇最長者;其 PrefixCache 複本作為 decode 起點。
  • 若無匹配,呼叫會乾淨地降級為標準 generate — 相較於無池的基準呼叫無效能損失。
  • 呼叫後,匹配 prefix 的 LRU 位置會被提升;若池達到容量,陳舊 prefix 會被逐出。

實測效益

在 407M INT4 artifact 上,generate_with_pool 在閉卷延續任務上與 tree-speculative decoding 品質相當(temperature == 0 下位元精確),且 wall-clock 加速與 prefix 共享比例成正比。完整 benchmark 見論文 §S。

free_prefix_pool

void free_prefix_pool(PrefixPool* pool);

以單一呼叫釋放池及所有已註冊 prefix。

重用邊界摘要

三種 handle 依呼叫間可變的維度切分重用圖景:

維度PrefixCacheResumeHandlePrefixPool
Prefix(prompt 前綴)固定固定可變(最長匹配)
Loop 深度固定(n_loops)可升級(L_used → new_L)每個註冊項目固定
Decode config可變可變可變
每次呼叫成本僅 decodeloop-range + decode匹配 + 迷你 prefill + decode

三種 handle 共同的錯誤條件

  • Loop 深度不符。 使用 n_loops = 8 建構的 handle 搭配 config.loops = 16 的 decode config 是硬性契約錯誤 — 超過第 8 次迭代的 cache 層未填充(PrefixCache)或雖配置但為零(ResumeHandle[L_used, max_loops] 之外)。
  • 跨模型誤用。 以某 Model* 建構的 handle 對另一 Model* 使用為未定義。Handle 帶有建構來源模型的參照,但驗證由呼叫端負責。
  • Handle 使用中在同一模型上並行 forward — 模型的可變工作 buffer 可能被交錯呼叫覆寫,使 handle 的正確性保證失效。請在外部序列化。

取捨

  • Handle 具狀態性 — 更強重用帶來更高運維複雜度。不使用 handle 的 API 較易推理;只有當重用比例有意義地高時,handle 才真正回本。
  • 所有 handle 在存在期間都消耗 VRAM。建構策略應限制存活 handle 總數,而不僅限池容量。
  • Prefix 匹配為 token 精確,而非語義精確。 等義改寫的 prompt 不會共享 cache;這是確定性 runtime 的正確語義,但可能與應用層使用者預期不同。
  • 貪婪是重用的充分描述路徑。 使用重用 prefix 的 stochastic sampling 雖然正確,但可能產生與同種子的全新 generate 不同的 sample 流 — RNG 從已快取狀態的起點開始推進。

另見