快取重用與狀態轉換 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 並儲存結果。
參數
| 名稱 | 型別 | 預設 | 描述 |
|---|---|---|---|
model | Model* | — | 已載入的模型指標。不得為 nullptr。 |
tokens | const int32_t* | — | Prompt token id,長度為 seq_len。 |
seq_len | int | — | 1 <= seq_len <= max_seq_len。Cache 大小恰為 seq_len;後續 decode 會在此之上增長。 |
n_loops | int | 8 | 計算 prefill 所用的 loop 深度。必須與下游 generate_from_prefix_cache 呼叫的 config.loops 相符。 |
cache_window | int | 0 | Cache 層的滑動視窗上限。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 成本。
參數
| 名稱 | 型別 | 預設 | 描述 |
|---|---|---|---|
model | Model* | — | 必須是與建構 prefix cache 相同的模型實例;混用未定義。 |
prefix_cache | const PrefixCache* | — | 預建 prefix handle。 |
config | const 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。
參數
| 名稱 | 型別 | 預設 | 描述 |
|---|---|---|---|
model | Model* | — | 已載入的模型指標。 |
tokens | const int32_t* | — | Prompt token id。 |
seq_len | int | — | 1 <= seq_len <= max_seq_len。 |
L_used | int | — | Handle 初始化所用的 loop 深度。超過第 L_used 次迭代的 cache 層以零填充為 placeholder。 |
max_loops | int | 32 | resume_generate 可呼叫的 loop 深度上限。Handle 依此深度配置 cache 容量。 |
cache_window | int | 0 | 滑動視窗上限。 |
回傳
ResumeHandle* — 不透明 handle。失敗時回傳 nullptr。
錯誤條件
L_used < 1或L_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) 區間的迭代。
參數
| 名稱 | 型別 | 預設 | 描述 |
|---|---|---|---|
model | Model* | — | 建構此 handle 的相同模型。 |
handle | const ResumeHandle* | — | 預建 resume handle。 |
new_L | int | — | 目標 loop 深度。必須滿足 handle.L_used <= new_L <= handle.max_loops。 |
config | const GenerateConfig& | — | Decode config。Prefill 部分忽略 config.loops(由 new_L 覆寫);decode 每步的 loop 次數仍使用該值。 |
回傳
std::vector<int32_t> — 完整輸出序列。
內部狀態機
- 將 handle 的 cache 複製到 scratch 工作 buffer。
- 將 residual 快照還原至
buffers.main。 - 在共享 loop block 上執行
run_loop_iters_range(L_used, new_L),將 residual 從深度L_used推進到深度new_L。 - 由推進後的狀態合成一個暫時 prefix cache。
- 以
config.loops每步的迭代次數執行標準 decode loop。 - 離開時釋放 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);
建構空池。
參數
| 名稱 | 型別 | 預設 | 描述 |
|---|---|---|---|
capacity | size_t | 16 | 已註冊 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 的生命週期。
參數
| 名稱 | 型別 | 預設 | 描述 |
|---|---|---|---|
pool | PrefixPool* | — | 目標池。 |
model | Model* | — | 用於建構的模型。 |
tokens | const int32_t* | — | Prefix token id。 |
seq_len | int | — | Prefix 長度。 |
n_loops | int | 8 | Prefix cache 的 loop 深度。 |
cache_window | int | 0 | 選擇性的 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。
參數
| 名稱 | 型別 | 預設 | 描述 |
|---|---|---|---|
model | Model* | — | 與池的模型相同。 |
pool | PrefixPool* | — | 預建池。 |
prompt | const int32_t* | — | 完整 prompt id。 |
prompt_len | int | — | Prompt token 數。 |
config | const 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 依呼叫間可變的維度切分重用圖景:
| 維度 | PrefixCache | ResumeHandle | PrefixPool |
|---|---|---|---|
| Prefix(prompt 前綴) | 固定 | 固定 | 可變(最長匹配) |
| Loop 深度 | 固定(n_loops) | 可升級(L_used → new_L) | 每個註冊項目固定 |
| Decode config | 可變 | 可變 | 可變 |
| 每次呼叫成本 | 僅 decode | loop-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 從已快取狀態的起點開始推進。
另見
- Generation API — 非重用的 decode 進入點。
- 生命週期與記憶體模型 — handle 記憶體於整體中的帳目。
- 批次化與整合模式 — 如何在並行服務中組合 handle。