量測 Benchmark
這一頁回答的是一個很具體的問題:
當模型本身已經是權重共享的循環式 Transformer 時,為什麼要用 TinyLoop,而不是通用 runtime?
答案不是「TinyLoop 在所有地方都打爆所有框架」。這個儲存庫目前並不支持那種說法。更精確、也更能站得住腳的答案是:
- TinyLoop 在自己的 loop-aware runtime 路徑上,已經有直接量測到的優勢。
- TinyLoop 暴露的結構性優勢,通用 runtime 除非自己也變成 loop-aware,否則補不回來。
- 如果你今天要的是廣泛 checkpoint 支援或成熟的 serving 基礎設施,通用 runtime 仍然是合理選擇。
如何正確閱讀這一頁
下面的比較分成兩類:
- 來自這個 repo、已驗證的 TinyLoop benchmark 的直接量測
- 與 vLLM、TensorRT-LLM、Hugging Face Transformers、ONNX Runtime 之類通用 runtime 的架構層部署比較
這個區分很重要。我們刻意不會把一個沒有以相同形式公開 checkpoint 的東西,硬包裝成假裝公平的「TinyLoop 對 PyTorch/vLLM latency shootout」。
TinyLoop 的直接實測 Benchmark
1. 目標 H100 產物的核心前向延遲
| 路徑 | seq_len=128, loops=8 | 相對早期版本加速 | 權重 VRAM | 適用情境 |
|---|---|---|---|---|
| 早期 TinyLoop runtime | 208.7 ms | 1.00×(基線) | INT4 216 MB | 歷史比較基線 |
| 預設低位元路徑 | 30.48 ms | 6.85× | INT4 216 MB | VRAM 緊、production 預設 |
| FP16-body 快速路徑 | 2.82 ms | 74.0× | FP16-body 619 MB | HBM 充足、延遲優先 |
在等效 407M checkpoint 上,對 generic runtime 的參考比較(同一台 H100 上的 Pythia-410M FP16,作為 CHECKLIST §0 的 north-star 指標):
| Runtime | Tokens/s | 權重 VRAM | tok/s/MB | 相對 Pythia |
|---|---|---|---|---|
| Pythia-410M(FP16) | 16,519 | 870 MB | 19.0 | 1.0×(基線) |
| TinyLoop FP16 | 68,303 | 848 MB | 80.5 | 4.2× |
| TinyLoop INT4 | 4,587 | 219 MB | 20.9 | 1.1×,但只用 25% VRAM |
| TinyLoop INT4 + FP16 body | 52,265 | 628 MB | 83.2 | 4.4× |
這裡最誠實的跨架構指標是 per-byte throughput。TinyLoop 能把相同的矽面積轉成 約 4× 的 tokens per MB of VRAM,因為同一個 loop block 被 8 次 iteration 共用,而不是展成 8 層獨立權重。
這一段代表的含義:
- TinyLoop 不只是「概念上比較漂亮」,它已經有真正最佳化後的 runtime path。
- 預設低位元路徑在相同 benchmark shape 上,已經比早期 runtime 快 6.85×。
- FP16-body 是速度路徑,但那是刻意的記憶體交換,不是免費魔法。
2. 共享前綴吞吐提升
| Prefix | Requests | NO_CACHE wall | SHARED wall | 節省時間 | 加速倍數 |
|---|---|---|---|---|---|
| 500 tokens | 10 | 12,847 ms | 5,415 ms | 7.4 s | 2.37× |
| 1024 tokens | 10 | 23,852 ms | 8,329 ms | 15.5 s | 2.86× |
| 1024 tokens | 50 | 104,605 ms | 34,531 ms | 70.1 s | 3.03× |
和 generic prefix cache 相比(例如 vLLM / SGLang):兩邊都會攤提 prefix KV,但在 looped model 上,一份 cached prefix 會同時跨 L 次 loop iteration 被重用,所以有效攤提是非 looped runtime 的 L ×。在 L = 8 時,3.03× 的 headline number 代表每個 cached prefix byte 對應大約 24× 的有效計算節省。
這對 looped model 的意義:
- 在 TinyLoop 裡,共享 prefix 會把 loop block 的成本攤提到
L次重複套用上。 - 通用 prefix cache 仍然有價值,但拿不到這種「同一區塊、多次 iteration」的完整槓桿,除非 runtime 明確保留這個結構。
- 加速幅度會隨 prefix 長度與 request 數量一起放大,
50 requests時的 3.03× 就是複利最明顯的區間。
3. 新快取模式之後的解碼吞吐
| 配置 | Decode ms | Tok/s | 相對基線變化 | KV 節省 |
|---|---|---|---|---|
| FP16 KV 基線 | 674 | 93 | — | — |
FP16 KV + fp16_body | 527 | 120 | +29 % | 0 |
FP16 h + fp16_body | 415 | 152 | +63 % | ~46 % |
INT8 h + fp16_body | 423 | 149 | +60 % | ~66 % |
INT4 h + fp16_body | 425 | 148 | +59 % | ~78 % |
和 generic INT8 KV quantizer 相比(常見於 vLLM / TensorRT-LLM 的 production baseline):
| 面向 | Generic INT8 KV | TinyLoop INT4-h + fp16_body | 優勢 |
|---|---|---|---|
| KV 節省 | ~50 % | ~78 % | 多省 28 個百分點 |
| Decode throughput | 持平或微退步 | 相對 FP16 基線 +59 % | tok/s 上升,不是下降 |
| 品質 | 通常 ± 0 PPL | ΔPPL ≤ +0.08(到 seq=4096 仍 ranking-safe) | 可比 |
Generic INT8-KV 通常只是純記憶體節省,有時還帶一點 throughput 代價。TinyLoop 的 store-h + fp16_body 把它翻成同時拿到記憶體與吞吐優勢,因為壓縮後的 K/V 會透過同一條 tensor-core GEMM pipeline 重建,而這只有在 W_k / W_v 跨 loop iteration 共享時才成立。
這也是目前 TinyLoop 最強的 runtime 論點之一:
- loop-aware KV 壓縮可以同時變成記憶體與延遲優勢。
- 這招依賴共享 loop block,不是一般深層 Transformer 自帶的特性。
INT4-h + fp16_body幾乎在釋放 4/5 KV bytes 的同時,還能做到接近 2× 於 FP16 KV 基線的速度級別。
4. RTX 4090(Ada, SM 8.9)上的 INT4 tensor-core GEMM
上游 Marlin(Frantar 2024,Apache 2.0,vendor 於 cuda/marlin/)是一個為 Ampere 與 Ada 設計的 INT4 × FP16 tensor-core GEMM。對 4090 來說,它能在權重仍放在 INT4 GMEM 的前提下逼近 FP16 tensor-core 峰值。TinyLoop 的 wrapper 加上 tests/bench_marlin_gemm 的量測結果(2026-04-19)如下:
| Shape | N × K | Naive TFLOPS | Marlin TFLOPS | 加速 | 佔 tensor-core 峰值 |
|---|---|---|---|---|---|
attn_qkv_like | 2048 × 2048 | 6.10 | 84.20 | 13.8× | 51 % |
qkv_stacked | 6144 × 2048 | 6.77 | 128.02 | 18.9× | 78 % |
mlp_down | 2048 × 8192 | 6.29 | 131.59 | 20.9× | 80 % |
mlp_gate_up | 8192 × 2048 | 6.77 | 142.30 | 21.0× | 86 % |
這說明:
- 在
batch × time = 256時,Marlin 能用 INT4-packed weights 打到 4090 理論 FP16 tensor-core 峰值的 86%。 - Naive kernel 不論 batch 多大,大約都卡在 6.8 TFLOPS 附近,因為每個輸出元素只有單一 thread 計算,讀權重頻寬成為上限。
- 單 token decode(
BT = 1)時,Marlin 仍有約 0.010 ms 的 tile floor,但 wall-clock 依然比 naive 快約 2.4×。真正的大勝區是在 prefill(BT ≥ 64)。 - Hopper(SM 9.0)不在 upstream Marlin 的目標範圍內,因此 TinyLoop 在那裡仍會回退到 FP16 body cache path。
完整 36 列(shape × BT)資料表放在 paper_draft bundle 下的 figures/marlin_vs_naive.csv 與 figures/marlin_vs_naive.{png,pdf}。
5. 非對稱校正的額外成本(Ada, SM 8.9)
TinyLoop 的 GPTQ 權重是非對稱格式(W = q·scale + zero),而 upstream Marlin 假設對稱格式(W = (q−8)·scale)。因此多出來的殘差會收斂成每個 group、每個 column 的 δ = zero + 8·scale,再以小型 reduction + 小型 GEMM 累加回輸出 C。這個流程已經作為 cuda/marlin_asymm_correction.cu ship 出去,並在 5 種 shape 上驗證到 FP16 noise floor。
4090 上的額外成本如下:
| 工作負載 | Marlin | +correction | correction alone | 相對開銷 |
|---|---|---|---|---|
decode M=1 attn_qkv 2048×2048 | 11.1 μs | 19.0 μs | 7.8 μs | +71 % |
prefill M=32 attn_qkv 2048×2048 | 23.1 μs | 27.7 μs | 7.7 μs | +33 % |
prefill M=128 attn_qkv 2048×2048 | 24.2 μs | 33.1 μs | 7.7 μs | +32 % |
prefill M=256 mlp_gate 8192×2048 | 60.5 μs | 83.0 μs | 23.0 μs | +38 % |
prefill M=256 mlp_down 2048×8192 | 65.6 μs | 96.0 μs | 30.7 μs | +47 % |
解讀重點:
- production shape grid 上,相對開銷大約落在 31–71%。
- 最差是單 token decode(
M = 1),因為固定 kernel launch latency 佔主導。 - correction kernel 的純計算量其實不到 1 μs,觀察到的 8–31 μs 大多是兩次 kernel launch 的成本。
- 如果之後改成 CUDA graphs replay 或真正的單 kernel fuse,還有再砍半空間。
- 正確性目前很緊:
cuda-vs-python max_abs ≤ 0.004、cuda-vs-FP16-reference max_abs ≤ 0.012,涵蓋到M = 32, N = K = 2048, 16 groups。
6. 消費級 Ada 上的 L2 resident 優勢
4090 有 72 MB L2,比 H100 的 50 MB 還大。407M INT4 loop block 只有 6 MB,W / L2 = 0.08,非常容易塞進 L2。tests/bench_l2_residency(2026-04-19)的量測:
| 場景 | 單次 iteration 時間 |
|---|---|
Scenario A iter 0(cold) | 0.0503 ms/iter |
Scenario A iter 1+(L2 warm) | 0.0410 ms/iter |
| Scenario C(每次都 flush L2) | 0.0480 ms/iter |
這代表:
- 從 cold 進入 warm 之後,大約能多拿到 18% 的速度。
- 如果每次都強制回 HBM,會比 warm 狀態慢約 14%。
- 這個結果在第二個硬體平台上再次支撐了「HBM-free decode」這個架構論點。
7. 1B 規模下的量化底線(誠實的負結果)
在 1B-effective、step-107K checkpoint 上,使用對稱 per-group min-max quantization,搭配 WikiText-103 validation(30K tokens、L = 8):
| 配置 | PPL | 與 FP16 差距 |
|---|---|---|
| FP16 基線 | 56.6 | — |
| INT4 naive(min-max、無 calibration) | 59.6 | +3.0 |
| INT2 naive(min-max、無 calibration) | 39,524 | +39,467 |
| INT2 + GPTQ(有 calibration) | 651 | +594 |
這裡釘死了三件事:
- INT4 naive 在這個架構上是可以用的。 權重壓到 8× 時,PPL 只增加約 3。
- INT2 naive 幾乎致命。 任何 per-token 類型的評估都會崩。
- GPTQ 雖然把 gap 拉回很多,但還是不夠。 在 1B 規模下,低於 4-bit 的量化還需要更多額外機制。
這對部署故事的意義很直接:INT4 才是目前 production sweet spot,而上面的 Marlin + 非對稱校正路徑正是我們希望 dispatch 走到的那條路。
8. 端對端 runtime dispatch:真實的 Model.score() on 1 B-class INT4
Marlin kernel stack 已經接進 src/inference.cpp::quant_gemm,所以真實的 PyModel.generate() / .score() 呼叫會在 Ampere / Ada 上直接走 tensor cores,而不需要呼叫端做任何特殊處理。完整 pipeline 可參考:Marlin INT4 on Ada — how it works。
| 路徑 | PPL | 備註 |
|---|---|---|
Native INT4(int4_gemm) | 8.9755 | 預設 TinyLoop scalar INT4 GEMM,理論上限約 6.8 TFLOPS |
| Marlin + 非對稱校正(已 dispatch) | 8.9157 | Δ −0.0597 PPL,約 0.67% drift;15 個權重已封包、約 101 MB Marlin VRAM 開銷、沒有 fallback |
同一個模型上的 wall-clock A/B(2 次 warmup 後量 5 次 Model.score(loops=8)):
seq_len | Native INT4 median | Marlin dispatch median | 加速 |
|---|---|---|---|
| 128 | 149.6 ms | 117.4 ms | 1.27× |
| 256 | 350.9 ms | 257.9 ms | 1.36× |
這一段補齊的事情:
- 一行 opt-in 就能開。
tl.pack_model_for_marlin(model)+tl.set_marlin_dispatch(True),或直接設TINYLOOP_USE_MARLIN=1。 - 不需要 checkpoint migration。 現有
.tinyloop非對稱 GPTQ 檔案,大約 2 秒內就能 pack 好後直接跑 Marlin。 - 可以優雅回退。 Marlin CALL_IF table 不支援的 shape,會自動回 native INT4。
- 不碰 Hopper。
kernels::marlin_available()在 SM 9.0 會回 false,H100 仍建議走 FP16 body cache path。
四個 parity tests 加上一個端對端 test_marlin_e2e 目前都能在 4090 上通過;前置條件不滿足時也會 cleanly skip,因此 CI matrix 不會被非 GPU runner 拖垮。
9. iteration-axis delta-KV:完整前向路徑的 PPL A/B(2026-04-19)
delta-KV 方案沿著iteration 軸壓縮 KV cache,而這正是權重共享 loop 才有的冗餘:每個 token 保留一份 FP16 anchor h⁰,再存每個 iteration 的 INT8 delta Δh^l = h^l − h^(l-1),搭配 per-head FP16 scale grid。架構背景可參考:Architecture-Exclusive Features → Iteration-axis delta-KV。
完整前向路徑的 PPL A/B(TINYLOOP_KV_DELTA=1、407M 2 B-class INT4、20 個 prompts × 16 個 decode tokens,共 320 個 scored positions):
L = 8
| 模式 | PPL | KL vs FP16 | top-1 | top-5 | wall-clock |
|---|---|---|---|---|---|
fp16 | 3.789 | 0 | 100.00 % | 100.00 % | 96.3 ms |
int8_kv | 3.780 | 1.98 × 10⁻⁴ | 99.69 % | 99.50 % | 105.2 ms |
h_int8(前一代最佳) | 3.700 | 4.03 × 10⁻³ | 96.88 % | 97.19 % | 147.2 ms |
delta_int8 | 3.795 | 8.08 × 10⁻⁵ | 99.06 % | 99.50 % | 135.4 ms |
L = 32
| 模式 | PPL | KL vs FP16 | top-1 | top-5 | wall-clock |
|---|---|---|---|---|---|
fp16 | 9.306 | 0 | 100.00 % | 100.00 % | 286.4 ms |
int8_kv | 9.275 | 1.53 × 10⁻⁴ | 98.75 % | 99.38 % | 317.7 ms |
h_int8(前一代最佳) | 8.962 | 4.71 × 10⁻³ | 97.81 % | 96.25 % | 459.1 ms |
delta_int8 | 9.292 | 9.08 × 10⁻⁴ | 99.38 % | 97.75 % | 442.7 ms |
delta-INT8 per-head 在所有測試過的 L 上,都嚴格優於舊的 store-h-INT8:
L | KL 比值(delta / h_int8) | top-1 改善 | top-5 改善 | wall-clock |
|---|---|---|---|---|
| 8 | 1 / 50 | +2.18 pp | +2.31 pp | 快 7.9 % |
| 32 | 1 / 5.2 | +1.57 pp | +1.50 pp | 快 3.6 % |
Phase A reconstruction-error backing study(20 個真實 prompts、per-row scale、pre-runtime):
L | direct_int8 final err | delta_int8 final err | delta 優勢 |
|---|---|---|---|
| 8 | 0.0247 | 0.0147 | 1.68 × |
| 16 | 0.0219 | 0.0099 | 2.21 × |
| 32 | 0.0169 | 0.0059 | 2.85 × |
優勢會隨 L 增長而擴大,因為 trajectory 本身在收斂(ρ: 0.68 → 0.04,跨 32 次 iteration)。直接 per-iter 量化每一輪都付一次完整精度成本;delta 方案則只需要支付逐步縮小的 ‖Δh^l‖。
Kernel-level validation(RTX 4090,SM_89)
| Kernel | 測試 shape | 結果 |
|---|---|---|
delta_kv_reconstruct_int8(per-row) | (T, D, L) up to (256, 2048, 32) | 對 Python reference 的 per-element drift ≤ 0.20 % |
delta_kv_write_int8(per-row) | 同上四個 shape | 98.90–98.95 % INT8-exact,剩下約 1% 是 round tie-break |
delta_kv_reconstruct_int8_layered_per_head | runtime path(prefill + decode) | ✓ 透過 test_kv_delta_e2e_smoke 端對端驗證 |
delta_kv_write_int8_per_head | runtime path | ✓ 端對端驗證 |
test_kv_delta_alloc(儲存空間) | 2 pre + 8 loop,D=2048 | total_bytes = 16,310,384 B 與解析解完全一致 |
這一段補齊的是:
- 可直接替換
TINYLOOP_KV_H_MODE=1 + TINYLOOP_KV_INT8=1。 設TINYLOOP_KV_DELTA=1即可,在相近儲存量下更接近 FP16 品質。 - 相對上一代
store-h-INT8,每個指標都更好。 KL 小 5–50×、top-1 高 1.5–2 個百分點、top-5 高 1.5–2 個百分點、wall-clock 快 3.6–7.9%。 - 這是架構專屬特性。 非權重共享 Transformer 沒有天然的
Δh^l收斂結構,因此沒有對等方案。 - runtime 已經完整 ship。 Prefill capture、attention reconstruct-layered read path、decode ring-buffer support 都已接上。