批次化與整合模式
本頁涵蓋生產等級的整合考量:如何批次化請求、如何結構化服務,以及如何組合 runtime 模式而不踩到未定義行為。TinyLoop 的批次為靜態 — N 個 prompt 以 lockstep decode,一個 Model* 一次。目前不支援 continuous batching(paged attention)。
整合決策矩陣
| 需求... | 使用 |
|---|---|
| 每請求最低延遲 | 單請求 generate_stream |
| 相同形狀下最高吞吐 | 以 lockstep prompt 使用 generate_batch |
| 多請求共享 system prompt | PrefixCache + generate_from_prefix_cache |
| 多租戶且有多個已知 scaffold | PrefixPool + generate_with_pool |
| 低深度草稿 → 高深度最終 | ResumeHandle + resume_generate |
| 零輔助參數的 decode 加速 | generate_speculative |
每一項皆為明確的 runtime API — TinyLoop 中沒有隱藏的排程器會為你挑選。
靜態批次生成
std::vector<int32_t> generate_batch(Model* model,
const std::vector<std::vector<int32_t>>& prompts,
const GenerateConfig& config);
在 N = prompts.size() 條 lane 上做靜態 lockstep 批次生成。
執行形狀
- 每 lane prefill(循序)。 每個 prompt 一次一個進行 prefill,填滿其專屬 cache 區域。目前實作不跨 lane 並行 prefill。
- Lockstep decode。 所有 lane 透過批次 attention 與 GEMM kernel 並行每步一個 token。已停止的 lane(EOS、stop-sequence、max-tokens)仍參與 kernel 形狀,但其輸出被丟棄。
- 每 lane 主機端取樣。 每步 decode 後,取樣、repetition penalty 與 grammar masking 在 host 端對每個 lane 各自套用。
這是吞吐量導向的原語,並非完整 continuous batching。Lane 無法於 decode 中途加入;批次形狀在呼叫進入時即固定。
目前限制(由 src/generate.cpp 強制)
這些是明確的 runtime 契約,而非偶然行為。違反時會中止並寫出 stderr 診斷。
| 限制 | 強制時機 | 理由 |
|---|---|---|
所有 prompt 皆須滿足 prompt_len + max_tokens <= max_seq_len | 呼叫進入時 | 每 lane 靜態 cache 配置。 |
批次模式中拒絕 cache_window > 0 | 呼叫進入時 | Ring-buffer 語義目前與 lockstep kernel 難以乾淨組合。 |
批次模式中拒絕 beam_size > 1 | 呼叫進入時 | Beam × batch 會將 cache 記憶體乘以 beam_size * batch;目前不支援。 |
所有 lane 必須共享 loops、temperature、top_k、top_p、repetition_penalty | 呼叫進入時 | 每 lane 發散取樣無法高效批次化。 |
每 lane stop_sequences 與 grammar 允許 | Runtime | Masking 於 host 端每 lane 套用。 |
| Store-h KV cache 模式:完全支援 | Runtime | 所有 lane 使用相同 cache 模式。 |
何時靜態批次有幫助、何時有害
有幫助:
- 長度相近的 prompt(例:分類、短生成)。
- 穩態高吞吐量場景,lane 發散度低。
有害:
- Prompt 長度變化大 — 短 lane 等待長 lane 到達 EOS。
- 互動式工作負載 — 單一慢 lane 阻塞其他 lane 的首 token 延遲。
典型生產模式是維護多個批次大小,並將請求路由至形狀能容納它的最小批次。
整合模式
TinyLoop 是執行原語,不是服務框架。下列模式是經過驗證、能安全組合 runtime API 的編排。
模式 1 — 低複雜度單租戶服務
最簡單的正確結構:
- 每個 process 一個
Model*(不跨 thread 共享)。 - 外部請求佇列(例:stdin、HTTP endpoint、訊息佇列)。
- 一個 worker thread 抽取佇列並為每個請求呼叫
generate_stream。
// Pseudocode
tinyloop::Model* model = tinyloop::load_model("model.tinyloop", 2048);
std::queue<Request> queue;
std::thread worker([&]() {
while (running) {
Request req = queue.pop_blocking();
tinyloop::GenerateConfig cfg = build_config(req);
auto out = tinyloop::generate_stream(model, req.prompt.data(),
req.prompt.size(), cfg,
[&](int32_t tok, int pos) {
req.response.push_token(tok);
return !req.cancelled;
});
req.response.finish(out);
}
});
tinyloop::free_model(model);
取捨:最容易推理;最大吞吐量為 1× 單請求吞吐量;無 prefix 重用;延遲可預測。
模式 2 — 以分桶提升吞吐量的服務
當有持續流量且能為多個模型實例預算 VRAM 時:
- 由
N個 process 組成的 worker pool,每個具備自己的Model*。 - 派發器按相容形狀(
loops、max_tokens帶、cache_mode、beam_size)將請求分桶。 - 每桶:若桶內
>= min_batch_size個就緒請求,呼叫generate_batch;否則退回generate_stream。
需調校的關鍵參數:
min_batch_size— 低於此值,靜態批次的 prefill 成本會超過 decode 的收益。- 以
max_tokens為基準的桶邊界 — 緊湊的區間可減少短 lane 的浪費 decode 週期。 - Worker 數量 — 平衡 VRAM 預算與佇列深度。
模式 3 — 共享 scaffold 的多租戶服務
當多個請求共享共同的 system prompt / scaffold 時:
- 在 process 啟動時將 scaffold 註冊到
PrefixPool。 - 透過
generate_with_pool服務請求 — 池匹配最長已註冊 prefix 並略過其 prefill 成本。 - 全新 prompt(無匹配)乾淨地降級為標準 prefill,無任何代價。
auto* pool = tinyloop::create_prefix_pool(32);
tinyloop::register_prefix(pool, model, system_prompt.data(),
system_prompt.size(), /*n_loops=*/8);
// 於請求 handler 中
auto out = tinyloop::generate_with_pool(model, pool, prompt.data(),
prompt.size(), cfg);
取捨:當 prefix 共享率 >50 % 時會有可測得的收益;低於此值時,雜湊查找與潛在的逐出波動可能抵銷收益。
模式 4 — 具 warm-start 升級的品質分層服務
對於想要「快速草稿,按需升級」的工作負載:
- 從 prompt 建構
L_used = 4的ResumeHandle。 - 以
resume_generate(handle, 4, cfg)提供草稿。 - 若使用者點擊「精煉」,呼叫
resume_generate(handle, 16, cfg)— 前 4 次迭代被略過。
這是 warm-start 架構原語的運維形式。實測 wall-clock 節省為 407M artifact 上單次升級 32.5 %;每次建構的升級次數越多,攤平效益越好。
模式 5 — 具 β 感知 fallback 的 speculative
若你的 checkpoint 為 β ≤ 1(transparent 或 bottleneck 範圍),自 speculative decode 幾乎是免費的 2× 吞吐量提升:
tinyloop::SpeculativeConfig sc;
sc.draft_loops = 2;
sc.verify_loops = 8;
sc.draft_ahead = 4;
auto out = tinyloop::generate_speculative(model, prompt.data(),
prompt.size(), sc);
在高 β checkpoint(β > 1)上接受率下降,可能使 speculative 比 baseline 更慢。在生產承諾使用 speculative 前務必先 benchmark。安全模式為:
- 啟動時以 5–10 個代表性 prompt 進行小型 benchmark,比較
generate與generate_speculative的 wall-clock。 - 若 speculative 未達 >1.3× 加速,則對該 checkpoint 退回標準 decode。
組合 runtime 模式
TinyLoop 並未對每一種 runtime 模式組合強制互斥。有些組合可用,有些以明確錯誤拒絕,還有些為未定義。下方矩陣為目前已驗證的表面:
| 模式 | 單一 decode | 批次 decode | Speculative | Tree-speculative | Prefix 重用 | Warm-start |
|---|---|---|---|---|---|---|
| FP16 cache | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| INT8 cache | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠ 部分 |
| INT4 cache(store-h INT4) | ✅ | ✅ | ⚠ 部分 | ⚠ 部分 | ✅ | ⚠ 部分 |
| Grammar | ✅ | ✅(每 lane) | ❌ | ❌ | ✅ | ✅ |
cache_window > 0 | ✅ | ❌ | ⚠ 部分 | ❌ | ❌ | ⚠ 部分 |
| Beam search | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
✅ = 完全測試。⚠ 部分 = 目前 commit 可運作但未詳盡測試。❌ = runtime 明確拒絕。
請將此矩陣視為承重 — 拒絕組合由 src/generate.cpp 守衛,越過它們即為未定義行為。
Thread-safety 與並行摘要
- 單一
Model*非可重入。 在同一Model*上的任兩次 forward pass 必須於外部序列化。否則可變工作 buffer(buffers.main、buffers.norm、scratch)會產生競爭。 - Handle 綁定於模型。 以模型 A 建構的
PrefixCache、ResumeHandle或PrefixPool不得用於模型 B。runtime 不強制檢查 — 屬契約錯誤。 - Handle 非可重入。 對同一 handle 進行兩次
generate_from_prefix_cache呼叫會在複製路徑上彼此競爭。請於外部序列化。 - 並行 build + decode(一個 thread 建構新的
PrefixCache而另一 thread 正在 decode)同樣不安全:build 會在模型的可變 buffer 上執行完整 forward。
安全模式:
- 每 worker 一個模型。 每個 thread/process 一個
Model*,各自具備自己的 handle 集合。 - Lock-per-model。 單一
Model*以 mutex 保護;build 與 decode 呼叫序列化。 - Replica pool。
N個 process,各載入自己的模型,透過外部派發器均衡。
為什麼 TinyLoop 將此以 API 形式暴露,而非隱藏排程器邏輯
排程策略視產品而定:
- 延遲敏感產品選擇小批次的
generate_stream。 - 吞吐量敏感產品以分桶使用
generate_batch。 - 成本敏感產品加入
PrefixPool以減少重複 prefill。 - 品質分層產品組合
ResumeHandle+ speculative。
若以單一 serve() API 隱藏這些選擇的框架會作出強烈隱含假設(例:統一延遲預算、單一租戶),這對大多數真實部署都不成立。TinyLoop 的立場是:runtime 原語應明確,而 SLO 策略存在於整合程式碼(產品層)中。
實務整合檢查清單
在以 C++ 服務部署 TinyLoop 前:
- 決定並行模型 — lock-per-model vs model-per-worker。在程式碼中文件化。
- 設定 VRAM 預算 — 每模型 + 每 handle + decode cache。若預算無法滿足則中止啟動。
- 限制存活 handle 數 —
PrefixCache+ResumeHandle+PrefixPool總項目數。以上限逐出,而非以記憶體壓力逐出(記憶體壓力下的逐出會破壞延遲)。 - 針對每個 checkpoint 為 speculative 做 benchmark — 不要假設會加速;永遠驗證。
- 加入模式護欄 — 於派發器層拒絕產品不需的組合(在 runtime 自有拒絕之上做深度防禦)。
- 接上健康探針 —
load_model失敗、建構時 OOM、反覆 decode 中止應通知運維人員,而非僅記錄 log。 - 依部署明確選擇貪婪 vs stochastic — 生產確定性可重現性依賴明確種子與貪婪模式。
- 接入
GenerateStats— 每請求記錄prefill_ms、first_token_ms、decode_ms、stop_reason。這些是唯一能快速捕捉 runtime 模式誤配置的訊號。
取捨
- 靜態批次提升吞吐但可能損害尾端延遲。 短 lane 等待長 lane。
- 明確整合帶來控制但需要編排工程。 產品團隊承擔排程策略,而非 TinyLoop。
- 模式組合需要在呼叫端程式碼設護欄 — runtime 僅守護明顯的不可能,而非每個產品專屬不變式。
- VRAM 預算為手動。 無內建記憶體壓力逐出;handle 生命週期是產品責任。
另見
- Generation API — 此處組合的 decode 原語。
- 快取重用與狀態轉換 API —
PrefixCache、ResumeHandle、PrefixPool細節。 - Runtime 模式 — cache 模式、speculative、早退的 env flag 表面。
- 效能與記憶體 — VRAM 預算實例。