跳至主要内容

Generation

這一頁介紹 Model 上所有 decode API。它們共享同一組 sampling kwargs(temperaturetop_kcache_windowrepetition_penaltystop_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,
)

參數

名稱型別預設說明
promptnp.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_tokensint64生成 token 數量上限(不含 prompt)。若命中 stop_sequences 或 early exit,可能提前結束。必須 >= 1
loopsintmodel.config()["default_loops"]Loop 深度。L 越高,每個位置的計算量越大,品質通常也越好,直到飽和。
temperaturefloat0.0Softmax temperature。0.0 代表 argmax(greedy);任何 <= 0.01 也會被視為 greedy。必須 >= 0
top_kint50取樣前只保留 top-k token。**TinyLoop 特有約束:**採樣路徑要求 top_k >= 1;目前 runtime 不支援用 top_k=0 當成「關閉 top-k」的 sentinel。當 temperature == 0 時會忽略。
top_pfloat1.0Nucleus sampling。保留累積機率超過 top_p 所需的最小 token 集合。1.0 代表關閉。套用順序在 top_k 之後。
cache_windowint0KV cache 的 sliding-window 上限。0 代表完整 cache(不淘汰);>0 只保留最後 cache_window 個 token,更早位置會 FIFO 淘汰。適合 VRAM 緊張的長上下文情境,但會犧牲超出視窗之外的資訊。
repetition_penaltyfloat1.0採 HuggingFace 習慣的重複懲罰,套用順序在 temperature / top_k / top_p 之前。1.0 代表關閉。> 1.0 會壓低已經出現過 token 的 logits,常見範圍約 1.05–1.30
stop_sequencesList[List[int]][]一組 token-id 序列,只要出現在已生成輸出的尾端,就立即停止。**匹配到的 token 仍會包含在回傳值裡。**空清單代表關閉。若多個 stop 序列都可匹配,較短者先贏。
early_exit_patienceint0Early-exit probe 次數。0 代表完全關閉,直接把所有 loops 跑完。>0 時:在 early_exit_min_L 之後,每隔 early_exit_peek_every 次迭代檢查一次穩定性,若連續 patience 次都穩定,就提前退出。
early_exit_min_Lint2第一次 probe 前的最小迭代數,用來避免在前期高雜訊階段過早退出。
early_exit_taufloat1e-3**Hidden-L2 模式門檻。**若相對變化 `rho =
early_exit_peek_everyint1過了 min_L 之後的 probe 頻率。數值越大,probe 攤銷得越少,但也可能稍微晚一點才退出。
early_exit_logit_marginfloat0.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_sizeint0Beam search。01 代表關閉(使用標準 sampling)。>= 2 時啟用 deterministic beam-search。當 beam search 啟用時,temperature / top_k / top_p 都會被忽略。詳見下方 Beam search
length_penaltyfloat1.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_stoppingboolTrue僅對 beam search 生效。為真時,只要當前最優 beam 輸出 EOS 就停止;為假時,所有 beam 都會跑到 max_tokens
grammartl.Grammar / NoneNone**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 時 OOM
  • ValueError / RuntimeError:輸入形狀或範圍非法(例如 max_tokens < 1loops <= 0、採樣時 top_k <= 0),或內部 guard 失敗

語義補充

  • Cached decode 是預設路徑。 Runtime 會先配置一份大小為 seq_len + max_tokensRuntimeKVCache,對 prompt 在 loops 次迭代下做 prefill,之後每次 decode 都重用同一份 cache。若開啟 TINYLOOP_DISABLE_KV_CACHE=1,則會走 uncached reference path,速度大約慢 10 倍,只保留給 parity test。
  • cache_window > 0 時,runtime 會改配一個只有 cache_window token 的小型 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 解碼的大致流程是:

  1. 以選定模式(FP16 / INT8 / store-h 等)配置 RuntimeKVCache
  2. 對 prompt 做一次 prefill(prefill_with_kv_cache
  3. 對最後一個 hidden 做 scoring(score_hidden_vector)取得 logits
  4. 依序套用懲罰與遮罩:
    • repetition penalty
    • grammar legality mask(若啟用)
    • temperature scaling
  5. 取樣下一個 token
  6. 追加 decode state(decode_with_kv_cache*)並重複

這個順序是刻意的:grammar 是絕對合法性遮罩,必須直接作用於 logits;而 repetition penalty 則要在未經溫度縮放前套用,才能保留一般採樣慣例。

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=2048seq=512L=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_tokenCallable[[int], bool]每生成一個 token 就同步呼叫一次。若回傳 False,生成會在把該 token 納入輸出後立即停止。適合 UI cancel button 或聊天早停。GIL 會短暫被取回來執行 callback,因此 callback 內容應盡量短。

回傳

一個 dict:

型別意義
tokensList[int]含 prompt 在內的完整 token stream,形狀與 generate 相同
prefill_msfloatprompt prefill 所耗的壁鐘時間(主要由量化 GEMM 主導)
first_token_msfloat從呼叫開始到第一次 on_token 觸發的壁鐘時間,包含 prefill + 第一個 decode step + sampler
decode_steady_msfloattoken 2..N 的總 decode 壁鐘時間(不含 prefill)。若要算穩態單 token 延遲,可再除以 tokens_emitted - 1
tokens_emittedint新生成 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 + 第一次 callback
  • decode_steady_ms = 第 2..N 個 decode iter 的總和(每步包含 loop block × loops + sampler + callback)

因此這次呼叫的總壁鐘大致等於 first_token_ms + decode_steady_mson_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"]

參數

名稱型別預設說明
promptnp.ndarray[int32]generate 相同
max_tokensint64generate 相同
draft_loopsint2Draft forward 的 loop 深度。越短越快,但 accept rate 往往也越低。
verify_loopsint8Verification forward 的 loop 深度。在 greedy 模式下,驗證後的輸出與 generate(prompt, loops=verify_loops) 無法區分。
draft_aheadint4每輪 speculative draft 最多提出的 token 數量。越大時,在 draft 準確的情況下平行化收益越高;若不準則浪費工作也越多。
temperaturetop_ktop_pcache_windowgenerate 相同。temperature 作用於 verifier 的採樣分布。

回傳

{"tokens": List[int]}

目前 Python binding 只回傳 tokens,尚未暴露 accept_ratedraft_callsverify_calls 等欄位。

早期 wiki 版本裡那些額外欄位屬於理想設計;目前實際出貨的 python/tinyloop_py.cpp 綁定只提供 {"tokens": ...}

何時該用

目前主要適合在你刻意測試 self-speculative 行為時使用。當前 TinyLoop 的 speculative path 在某些配置下仍有已知回歸,距離 serving-grade 驗證還有一段距離;詳見 runtime modes → speculate

內部流程與取捨

Speculative decode 會在 draft / verify 回合之間交替進行:

  1. draft 在 draft_loops 下先提出最多 draft_ahead 個 token
  2. verifier 以 verify_loops 進行評分
  3. 每個 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"]

參數

名稱型別預設說明
promptnp.ndarray[int32]generate 相同
max_tokensint64新 token 的 soft cap;最多可能超出 draft_ahead 那麼多
draft_loopsint2Draft forward 的 loop 深度
verify_loopsint8Verify forward 的 loop 深度
draft_aheadint4每個 branch 的深度
K_branchesint1平行 draft branch 數。K_branches=1 是 chain mode;K_branches >= 2 會從 draft root distribution 中取 top-K,再對每條 branch 做 chain extension,這時才是真正的「tree」。需滿足 K_branches * draft_ahead <= 64
temperaturetop_ktop_pcache_windowgenerate 相同。目前只實作 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")