Generation
這一頁介紹 Model 上所有 decode API。它們共享同一組 sampling kwargs(temperature、top_k、cache_window、repetition_penalty、stop_sequences);你只需要根據回傳形狀選擇適合的那一個。
本頁中使用的符號:
D=model.config()["dim"]V=model.config()["vocab_size"]- Token id 型別為
int32,範圍受V限制 - Loop 深度
L指共享 loop block 被套用的次數;總有效深度為n_pre_blocks + L
Model.generate(...)
標準的 batch 自回歸解碼。它會先對 prompt 做 prefill,之後逐 token 取樣,直到達到 max_tokens 或命中 stop sequence。
generated = model.generate(
prompt,
max_tokens=64,
loops=8,
temperature=0.8,
top_k=50,
cache_window=0,
repetition_penalty=1.3,
stop_sequences=[[13], [198, 198]], # [".", "\n\n"] encoded as BPE ids
early_exit_patience=0,
early_exit_min_L=2,
early_exit_tau=1e-3,
early_exit_peek_every=1,
early_exit_logit_margin=0.0,
)
參數
| 名稱 | 型別 | 預設 | 說明 |
|---|---|---|---|
prompt | np.ndarray[int32] | — | Prompt token ids,形狀 [seq_len]。binding 內部會先做 np.asarray(..., dtype=np.int32);直接傳 Python int list 也能用,但會多一次拷貝。需滿足 seq_len + max_tokens <= model.max_seq_len。 |
max_tokens | int | 64 | 生成 token 數量上限(不含 prompt)。若命中 stop_sequences 或 early exit,可能提前結束。必須 >= 1。 |
loops | int | model.config()["default_loops"] | Loop 深度。L 越高,每個位置的計算量越大,品質通常也越好,直到飽和。 |
temperature | float | 0.0 | Softmax temperature。0.0 代表 argmax(greedy);任何 <= 0.01 也會被視為 greedy。必須 >= 0。 |
top_k | int | 50 | 取樣前只保留 top-k token。**TinyLoop 特有約束:**採樣路徑要求 top_k >= 1;目前 runtime 不支援用 top_k=0 當成「關閉 top-k」的 sentinel。當 temperature == 0 時會忽略。 |
top_p | float | 1.0 | Nucleus sampling。保留累積機率超過 top_p 所需的最小 token 集合。1.0 代表關閉。套用順序在 top_k 之後。 |
cache_window | int | 0 | KV cache 的 sliding-window 上限。0 代表完整 cache(不淘汰);>0 只保留最後 cache_window 個 token,更早位置會 FIFO 淘汰。適合 VRAM 緊張的長上下文情境,但會犧牲超出視窗之外的資訊。 |
repetition_penalty | float | 1.0 | 採 HuggingFace 習慣的重複懲罰,套用順序在 temperature / top_k / top_p 之前。1.0 代表關閉。> 1.0 會壓低已經出現過 token 的 logits,常見範圍約 1.05–1.30。 |
stop_sequences | List[List[int]] | [] | 一組 token-id 序列,只要出現在已生成輸出的尾端,就立即停止。**匹配到的 token 仍會包含在回傳值裡。**空清單代表關閉。若多個 stop 序列都可匹配,較短者先贏。 |
early_exit_patience | int | 0 | Early-exit probe 次數。0 代表完全關閉,直接把所有 loops 跑完。>0 時:在 early_exit_min_L 之後,每隔 early_exit_peek_every 次迭代檢查一次穩定性,若連續 patience 次都穩定,就提前退出。 |
early_exit_min_L | int | 2 | 第一次 probe 前的最小迭代數,用來避免在前期高雜訊階段過早退出。 |
early_exit_tau | float | 1e-3 | **Hidden-L2 模式門檻。**若相對變化 `rho = |
early_exit_peek_every | int | 1 | 過了 min_L 之後的 probe 頻率。數值越大,probe 攤銷得越少,但也可能稍微晚一點才退出。 |
early_exit_logit_margin | float | 0.0 | **Logit-margin 模式門檻。**若 > 0,就會從 hidden-L2 切換成 logit-margin:只有當 top-1 logit 領先 top-2 超過 margin,才算穩定。每次 probe 成本會多一次完整 head projection,但對訓練良好的模型比較常觸發。1B-effective 上實測甜蜜點約為:margin=3.0, min_L=4, peek_every=2, patience=1,可達 −13 % decode 且看不出品質損失。 |
beam_size | int | 0 | Beam search。0 或 1 代表關閉(使用標準 sampling)。>= 2 時啟用 deterministic beam-search。當 beam search 啟用時,temperature / top_k / top_p 都會被忽略。詳見下方 Beam search。 |
length_penalty | float | 1.0 | 僅對 beam search 生效。最終 beam 會以 log_prob / max(1, new_len)^length_penalty 排序,以免短輸出天然佔優。1.0 表示未正規化 log-prob;0.6–0.8 常見於翻譯;>1.0 反而偏好更短輸出。當 beam_size <= 1 時忽略。 |
early_stopping | bool | True | 僅對 beam search 生效。為真時,只要當前最優 beam 輸出 EOS 就停止;為假時,所有 beam 都會跑到 max_tokens。 |
grammar | tl.Grammar / None | None | **Constrained decoding。**非 None 時,grammar 會在每一步遮罩分布,保證只有那些「其 byte sequence 仍讓 grammar 存活」的 token 可以被採樣。可與一般 sampling、beam search、以及所有 early-exit / repetition-penalty 設定自由組合。完整說明見 Constrained decoding。 |
回傳
List[int],即完整 token stream:前面是 prompt,後面是新生成的 token。總長度為 seq_len + emitted,其中 emitted <= max_tokens。
可能拋出的錯誤
RuntimeError:模型已關閉、seq_len + max_tokens > max_seq_len、或配置 KV cache 時 OOMValueError/RuntimeError:輸入形狀或範圍非法(例如max_tokens < 1、loops <= 0、採樣時top_k <= 0),或內部 guard 失敗
語義補充
- Cached decode 是預設路徑。 Runtime 會先配置一份大小為
seq_len + max_tokens的RuntimeKVCache,對 prompt 在loops次迭代下做 prefill,之後每次 decode 都重用同一份 cache。若開啟TINYLOOP_DISABLE_KV_CACHE=1,則會走 uncached reference path,速度大約慢 10 倍,只保留給 parity test。 cache_window > 0時,runtime 會改配一個只有cache_windowtoken 的小型 cache,並把它當 ring buffer 使用。每次淘汰都會推進cache.start_index。所有 h-mode reconstruction 路徑都能正確處理 wrap。stop_sequences的匹配方式。 匹配視窗只會看最後max(len(s) for s in stop_sequences)個已生成 token,且與 prompt 無重疊;也就是說,不會因為 prompt 末尾本來就帶有某段字串而提前停掉。- GIL。 在 CUDA forward 與 sampler 期間會釋放 GIL;若需要 Python callback,請看
generate_stream。
內部執行流程
在 Python 端,Model.generate(...) 只是 tinyloop::generate 的薄包裝:當 beam_size <= 1 時,會走 generate_stream(..., on_token=nullptr);當 beam_size > 1 時,則切到專用 beam path。
非 beam 解碼的大致流程是:
- 以選定模式(FP16 / INT8 / store-h 等)配置
RuntimeKVCache - 對 prompt 做一次 prefill(
prefill_with_kv_cache) - 對最後一個 hidden 做 scoring(
score_hidden_vector)取得 logits - 依序套用懲罰與遮罩:
- repetition penalty
- grammar legality mask(若啟用)
- temperature scaling
- 取樣下一個 token
- 追加 decode state(
decode_with_kv_cache*)並重複
這個順序是刻意的:grammar 是絕對合法性遮罩,必須直接作用於 logits;而 repetition penalty 則要在未經溫度縮放前套用,才能保留一般採樣慣例。
Beam search
當 beam_size >= 2 時,generate 會切換到 deterministic beam search:每一步保留 beam_size 條最高 log-prob 假設,對每條 beam 展開其 top beam_size 個下一步候選,然後全域選出最好的 beam_size 個倖存者。由於 beam 是 deterministic,temperature / top_k / top_p 都會被忽略。但 repetition penalty、stop sequences、grammar mask 與 early-exit 設定都仍然生效,而且是逐 beam 各自處理。
Cache 管理
每個 beam 都有自己的 RuntimeKVCache。每步 decode 完某 beam 的最後一個 token 後,這份 cache 正好就是它的子 beam 下一步所需的起始狀態。若多個候選都源自同一個 parent,第一個子 beam 會直接接管 parent 的 cache,之後其他兄弟則會透過 clone_runtime_kv_cache_prefix 複製出自己的版本;沒有被任何子 beam 繼承的 parent cache 會直接釋放。
Peak cache VRAM 約等於 beam_size × cache_bytes。以 beam=4、D=2048、seq=512、L=10 為例,約為 320 MB。當 beam ≥ 16 時,成本會迅速上升。
長度正規化
最終 beam 會用 log_prob / max(1, new_len)^length_penalty 排序:
length_penalty = 1.0:未正規化 log-prob(預設,beam 偏好短輸出)length_penalty = 0.6–0.8:對較長輸出有溫和偏好(翻譯 / 摘要常見)length_penalty > 1.0:明確偏好較短輸出
實測
條件:H100 / 407M GPTQ-INT4 / prefix=16 / max=12 / L=4:
greedy: [11, 564, 250, 464, 968, 1971, 3782, 447, ...]
beam_size=3: [11, 564, 250, 40, 836, 447, 247, 83, ...]
Beam 在第 3 個位置開始與 greedy 分歧,這正是 beam search 存在的原因:更深的 lookahead 揭露出一條總累積 log-prob 更高、但 greedy 當下 argmax 看不到的路徑。
範例
# 偏向較長輸出的翻譯式 decoding
tokens = model.generate(
prompt, max_tokens=128, loops=8,
beam_size=4, length_penalty=0.7, early_stopping=True,
)
更多現成驗證與 length_penalty 掃描可見 tests/test_beam_search.py。
Constrained decoding
只要把編譯好的 tl.Grammar 傳進 grammar=,每一步採樣時就會遮掉所有不符合 grammar 的 token。它可與 greedy、sampling、beam search 同時使用,而且由於遮罩直接作用在 logit space,temperature / top_k / top_p 都會自然忽略非法 token。
import tinyloop_py as tl
g = tl.Grammar(r"\{\"name\":\"[a-z]+\"\}")
g.set_token_vocabulary(token_bytes) # one bytes object per token id
out = model.generate(prompt, grammar=g, max_tokens=32, temperature=0.0)
# out decodes to a string shaped like '{"name":"abc"}'
完整說明請見 Constrained decoding,包括:
- 支援哪些 regex 語法
- 如何從 Hugging Face GPT-2 tokenizer 建立
token_bytes - 如何與 beam search 組合
- JSON / function-calling 的典型 pattern
- 常見陷阱(例如 BPE token 帶前導空白、special tokens)
Model.generate_stream(...)
這是 generate 的 streaming 版本,提供逐 token callback 與 timing breakdown。
result = model.generate_stream(
prompt,
on_token=lambda tok: print(tok, end="", flush=True) or True,
max_tokens=64,
loops=8,
temperature=0.8,
top_k=50,
cache_window=0,
repetition_penalty=1.3,
stop_sequences=[],
)
# result = {"tokens": List[int],
# "prefill_ms": float, "first_token_ms": float,
# "decode_steady_ms": float, "tokens_emitted": int}
參數
與 generate 相同,另外加上:
| 名稱 | 型別 | 預設 | 說明 |
|---|---|---|---|
on_token | Callable[[int], bool] | — | 每生成一個 token 就同步呼叫一次。若回傳 False,生成會在把該 token 納入輸出後立即停止。適合 UI cancel button 或聊天早停。GIL 會短暫被取回來執行 callback,因此 callback 內容應盡量短。 |
回傳
一個 dict:
| 鍵 | 型別 | 意義 |
|---|---|---|
tokens | List[int] | 含 prompt 在內的完整 token stream,形狀與 generate 相同 |
prefill_ms | float | prompt prefill 所耗的壁鐘時間(主要由量化 GEMM 主導) |
first_token_ms | float | 從呼叫開始到第一次 on_token 觸發的壁鐘時間,包含 prefill + 第一個 decode step + sampler |
decode_steady_ms | float | token 2..N 的總 decode 壁鐘時間(不含 prefill)。若要算穩態單 token 延遲,可再除以 tokens_emitted - 1 |
tokens_emitted | int | 新生成 token 的數量(不含 prompt) |
適用場景
- 需要分開觀察 first-token latency 與 steady-state decode rate 的聊天 UI
- 即時回顯 token 的互動式 CLI
- 包裝到外部串流協定(SSE、WebSocket、gRPC server-streaming)時,用 callback 把 TinyLoop 的同步解碼橋接到外部 async sink
Timing 細節
三個延遲欄位的切法如下:
first_token_ms= prefill + 第一個 decode iter + sampler + 第一次 callbackdecode_steady_ms= 第 2..N 個 decode iter 的總和(每步包含 loop block ×loops+ sampler + callback)
因此這次呼叫的總壁鐘大致等於 first_token_ms + decode_steady_ms。on_token callback 的額外成本會算進它所屬的那一段時間裡。
範例:可取消串流
import queue, threading
output_q = queue.Queue()
cancel = threading.Event()
def on_tok(tok):
output_q.put(tok)
return not cancel.is_set()
# 在 worker thread 裡執行;主執行緒從 output_q 消費,或設置 cancel
t = threading.Thread(target=lambda: model.generate_stream(
prompt, on_tok, max_tokens=256, loops=8))
t.start()
Model.generate_speculative(...)
Self-speculative decoding:同一個 checkpoint 在不同 loop 深度下,同時扮演 draft 與 verifier。
result = model.generate_speculative(
prompt,
max_tokens=64,
draft_loops=2,
verify_loops=8,
draft_ahead=4,
temperature=0.0,
top_k=50,
top_p=0.9,
cache_window=0,
)
tokens = result["tokens"]
參數
| 名稱 | 型別 | 預設 | 說明 |
|---|---|---|---|
prompt | np.ndarray[int32] | — | 與 generate 相同 |
max_tokens | int | 64 | 與 generate 相同 |
draft_loops | int | 2 | Draft forward 的 loop 深度。越短越快,但 accept rate 往往也越低。 |
verify_loops | int | 8 | Verification forward 的 loop 深度。在 greedy 模式下,驗證後的輸出與 generate(prompt, loops=verify_loops) 無法區分。 |
draft_ahead | int | 4 | 每輪 speculative draft 最多提出的 token 數量。越大時,在 draft 準確的情況下平行化收益越高;若不準則浪費工作也越多。 |
temperature、top_k、top_p、cache_window | 與 generate 相同。temperature 作用於 verifier 的採樣分布。 |
回傳
{"tokens": List[int]}
目前 Python binding 只回傳 tokens,尚未暴露 accept_rate、draft_calls、verify_calls 等欄位。
早期 wiki 版本裡那些額外欄位屬於理想設計;目前實際出貨的 python/tinyloop_py.cpp 綁定只提供 {"tokens": ...}。
何時該用
目前主要適合在你刻意測試 self-speculative 行為時使用。當前 TinyLoop 的 speculative path 在某些配置下仍有已知回歸,距離 serving-grade 驗證還有一段距離;詳見 runtime modes → speculate。
內部流程與取捨
Speculative decode 會在 draft / verify 回合之間交替進行:
- draft 在
draft_loops下先提出最多draft_ahead個 token - verifier 以
verify_loops進行評分 - 每個 draft token 都會被接受或替換(在 stochastic decode 下則走 residual resampling)
取捨輪廓如下:
draft_loops越低 → draft 越快,但接受率通常也越低draft_ahead越高 → 接受率高時 amortization 越好,但接受率低時浪費工作也越多- 一旦接受率下降,verify path 很快就會同時成為品質與延遲的主導項
Model.generate_tree_speculative(...)
Tree speculative decoding 是一個架構專屬原語:它用一次 tree-mask-gated forward pass,取代 chain speculation 中 K 次序列 verify decode。它不需要任何輔助參數(不需要 Medusa / EAGLE heads),而是利用權重共享 loop,讓 draft 與 verify 在不同深度下共用同一個 checkpoint。
result = model.generate_tree_speculative(
prompt,
max_tokens=64,
draft_loops=2,
verify_loops=8,
draft_ahead=2, # 每個 branch 的深度
K_branches=2, # 1 = 純 chain;>1 = top-K multi-branch tree
temperature=0.0, # 目前只有 greedy 接好
top_k=50,
top_p=0.9,
cache_window=0,
)
tokens = result["tokens"]
參數
| 名稱 | 型別 | 預設 | 說明 |
|---|---|---|---|
prompt | np.ndarray[int32] | — | 與 generate 相同 |
max_tokens | int | 64 | 新 token 的 soft cap;最多可能超出 draft_ahead 那麼多 |
draft_loops | int | 2 | Draft forward 的 loop 深度 |
verify_loops | int | 8 | Verify forward 的 loop 深度 |
draft_ahead | int | 4 | 每個 branch 的深度 |
K_branches | int | 1 | 平行 draft branch 數。K_branches=1 是 chain mode;K_branches >= 2 會從 draft root distribution 中取 top-K,再對每條 branch 做 chain extension,這時才是真正的「tree」。需滿足 K_branches * draft_ahead <= 64。 |
temperature、top_k、top_p、cache_window | 與 generate 相同。目前只實作 temperature = 0; sampling 仍是後續工作。 |
回傳
{"tokens": [...]}
保證
在 greedy decoding(temperature = 0)下,輸出的 token sequence 與
model.generate(prompt, loops=verify_loops, temperature=0) 位元級一致。這對 chain(K_branches=1)與 multi-branch(K_branches >= 2)都成立。Speculative 只改變「哪些 forward 可以一起打包驗證」,不改變最終 token 結果。
何時該用
目前主要是為了這個正確性保證,以及為了實驗不同 draft / verify loop 深度。相對於 generate_speculative 的實際速度增益,取決於 draft 與 verify 在 root 位置上的一致率(也就是 draft 的 top-K 裡有多常包含 verify argmax)。對訓練良好的 checkpoint,tree 通常能比 chain 帶來更可觀的收益;對訓練不足或 draft / verify 不匹配的 checkpoint,tree 甚至可能比 chain 更慢,因為額外 drafting 成本無法換回足夠 accept。
限制
- 目前只支援 greedy;
temperature > 0會在 API 邊界直接被拒絕 - 只支援標準 FP16 KV cache;
TINYLOOP_KV_H_MODE/TINYLOOP_KV_INT8/TINYLOOP_KV_INT4目前都不接受 - Verify kernel 尚未處理 ring-buffer wrap(要求
start_index = 0) - Kernel 限制
K_branches * draft_ahead <= 64
架構說明
就單一 checkpoint 部署而言,self-speculative decoding 是只有權重共享 looped Transformer 才能乾淨做到的能力。標準解碼器需要另一個更小的 draft model(例如 Medusa、EAGLE)以及它自己的訓練成本;TinyLoop 則直接從同一組權重中,在不同 L 下同時取得 draft 與 verifier。
常見模式
帶 early exit 的 greedy decode
out = model.generate(
prompt, max_tokens=32, loops=16, temperature=0.0,
early_exit_patience=1, early_exit_min_L=4, early_exit_peek_every=2,
early_exit_logit_margin=3.0,
)
對訓練良好的模型,logit-margin 模式通常能達成約 −10 % 到 −13 % decode,且看不出品質變化。
Top-p sampling + repetition penalty
out = model.generate(
prompt, max_tokens=128, loops=8,
temperature=0.8, top_k=50, top_p=0.9,
repetition_penalty=1.15,
)
repetition_penalty=1.15 是一個中等強度的設定,能抑制逐字重複而不至於把分布壓得太平。若工作負載本身重複傾向很高,可以再升到 1.3,但也要小心模型可能因此避開明明正確、只是剛好在 prompt 裡出現過的答案。
串流 + timing breakdown
r = model.generate_stream(prompt, on_token=lambda t: True,
max_tokens=64, loops=8, temperature=0.0)
print(f"TTFT {r['first_token_ms']:.1f} ms "
f"decode {(r['decode_steady_ms'] / (r['tokens_emitted'] - 1)):.1f} ms/tok")