Day 3 的結論是「調參救不回來,只剩量化跟推測解碼兩條路」
Day 4 走完第一條
今天走第二條,然後把 Phase 1 收掉
先預告今天的結局:我們會得到一個結論,叫做**「單一模型不夠」**。而且這個結論不是我宣稱的,是前四天的數據自己疊出來的
再貼一次昨天的式子:
理論上限 tok/s ≈ 記憶體頻寬 ÷ 每個 token 需要讀取的權重量
這條式子有個隱藏假設:一次只服務一個序列
如果我同時服務 4 個請求呢?
權重還是只要搬一次
然後這一次搬進來的權重,可以同時給 4 個序列算 4 個 token
這就是 batch 的全部原理。 它不是什麼高深的優化,它就是「既然瓶頸在搬資料,那搬一次多用幾次」
理論講完,來看真的
我拿同一個 prompt(一頁財報的 OCR 任務),分別用併發 1、2、4 各跑一輪,全部實測落檔:
| 併發 | 單請求 tok/s | 單請求延遲 | 總吞吐 | 吞吐倍率 |
|---|---|---|---|---|
| 1 | 48.9 | 14.53s | 48.9 tok/s | 1.00x |
| 2 | 46.6 | 15.52s | 93.1 tok/s | 1.90x |
| 4 | 40.0 | 18.43s | 156.9 tok/s | 3.21x |
看兩個欄位就好:
總吞吐:1 → 2 → 4,成長 1.90x → 3.21x,接近線性
單請求延遲:14.53s → 15.52s → 18.43s,併發翻 4 倍,單一使用者只多等 27%
💡Tip: 這張表是「decode 是 memory-bound」最直接的證據。如果瓶頸是算力,併發 4 倍會讓每個請求慢 4 倍,總吞吐完全不會增加。實際上總吞吐漲了 3.2 倍——因為 GPU 原本大部分時間都在等記憶體,算力根本沒吃滿。
昨天我算出這顆模型的單序列理論上限是 143 tok/s
今天併發 4 的總吞吐是 156.9 tok/s
它超過了。
超過的不是物理定律,是「單序列」那個假設。權重搬一次服務 4 個序列,等於把頻寬成本攤掉了 4 份
所以昨天那條式子要補一個修正版:
批次總吞吐上限 ≈ (記憶體頻寬 ÷ 每 token 權重讀取量) × batch size
(實務上不會真的乘滿,因為 KV cache 的讀取量會隨 batch 線性增加,最後會撞到另一堵牆——見下一節)
因為 KV cache 會吃光你的記憶體
回到 Day 3 那個數字:
KV cache ≈ 79 KB / token(fp8)
這是每個序列、每個 token 的成本。它會隨 batch size 線性疊加:
| 併發 | 每個序列 8K context | KV cache 總量 |
|---|---|---|
| 1 | 0.62 GiB | 0.62 GiB |
| 4 | 0.62 GiB | 2.5 GiB |
| 16 | 0.62 GiB | 9.9 GiB |
| 64 | 0.62 GiB | 39.5 GiB |
而如果每個序列是 64K context(年報這種長文件很容易):
65536 × 79 KiB ≈ 4.9 GiB ← 這是「一個」序列
併發 4 就是 19.8 GiB。併發 16 就是 79 GiB。
你的 128GB 統一記憶體還要放 12.6GB 的權重、activation、CUDA graph,而且可能還有別的容器在搶
所以批次大小的真正限制不是算力,是:
batch size × context 長度 × KV cache 單價 < 你剩下的記憶體
這三個變數是互相排擠的。長文件場景下,你想開大 batch 就得砍 context,想要長 context 就得縮 batch
💡Tip: 這件事對架構的直接影響是——「把整份年報一次丟進去」這個做法,在硬體層就先輸了。不是模型讀不懂,是你為了塞下它,得把 batch 壓到 1,於是吞吐量退回 48.9 tok/s。分段處理不是妥協,是唯一能讓 batch 開起來的做法。這條會一路影響到 Day 18 的 Region Routing 設計。
Batch 解的是「多人同時用」的吞吐問題
但如果我就是一個人、一份文件、想要它快一點呢?
這就是推測解碼(speculative decoding)要解的問題
正常 decode 是:大模型一次吐一個 token,每次都要搬一次權重
推測解碼是:
關鍵在第 3 步:驗證 k 個 token 跟生成 1 個 token,搬的權重是一樣多的
因為瓶頸是搬權重,不是算。所以猜對的 token 等於免費
vLLM 官方支援幾種模式:draft model、EAGLE、MTP、n-gram
這是這個系列我自己最想做的一個實驗,講給你聽
一般的推測解碼要養一個 draft model(例如用 Gemma 4 的 E2B 去 draft 26B)。但在 OCR 這個特定任務上,我手上有一個更好的東西:
PDF 的原生文字層。
用 PyMuPDF 抽 PDF 文字層幾乎是免費的(毫秒級,不用 GPU)。而它的內容跟「OCR 應該輸出什麼」高度重疊
所以理論上:
PDF 文字層 → 當成 draft tokens → VLM 只負責 verify
如果文字層是對的,VLM 幾乎全盤接受,速度會快非常多
如果文字層是壞的(掃描檔、亂序、缺字),VLM 會拒絕並自己生成,退化回一般 OCR
它是一個「有就賺到、沒有也不虧」的設計。
我去翻了 vLLM 的官方文件,還有 vllm/config/speculative.py 的原始碼,把它支援的 proposer 抄下來:
ngram, ngram_gpu, suffix, draft_model, medusa,
mlp_speculator, eagle / eagle3, *_mtp, custom_class, dspark
逐個對一遍我想做的事:
| Proposer | 能不能餵外部 draft token? |
|---|---|
ngram / ngram_gpu / suffix |
❌ 只在模型已持有的 prompt 與已生成內容裡比對,不吃外部來源 |
draft_model / eagle / medusa / *_mtp |
❌ 要一顆 draft 模型,不是一串 draft token |
custom_class |
⚠️ 唯一的擴充點 |
而且我特別去確認了 request schema:沒有 draft_token_ids 或 spec_token_ids 這類欄位。
所以結論是:
這件事不是「加一個 API 參數」就能做到的。 你得寫一個伺服端的
custom_classproposer,在服務啟動時掛載進去。
custom_class 這條路我認為走得通——它的職責本來就是「被問到的時候提供候選 token」,而我的候選來源是 PDF 文字層
難點在對齊:文字層是整頁一次給的,proposer 是逐步被呼叫的。中間需要一層「現在生成到哪了 → 文字層對應到哪個位置」的追蹤
而這層對齊邏輯,剛好跟 Day 25 講 diff-driven flywheel 要用的是同一套東西。所以我會留到那時候一起做
現況誠實版:
做出來我會補一篇。做不出來我也會講為什麼 🙂
💡Tip: 順帶一提——我原本這段是直接寫「vLLM 沒有這種介面」。後來被校對打回來,理由是我從「官方清單裡沒看到」推論成「不存在」。這兩件事的距離比看起來遠:清單沒列,可能是真的沒有,也可能是它叫別的名字。 真的去翻了原始碼、確認 request schema 裡沒有對應欄位,才有資格講那句話。這個系列後面講 Verifier 的時候你會一直看到同一個模式——「我找不到」跟「它不存在」是兩個不同的結論。
💡Tip: 順帶一提,就算不做推測解碼,「先抽文字層、判斷夠不夠好、不夠好才走 OCR」這個分流本身就該做——它能讓一大堆有文字層的 PDF 完全不用經過 GPU。這是整條 pipeline 最便宜的一個優化,Day 18 會實作。
這是今天最重要的一張表
左邊是 Day 2 實測到的模型失敗模式,右邊是 Day 3、Day 4 的硬體與量化限制。重點是看它們交叉的地方:
做法是這樣:對 Day 2 的每一條失敗模式,先問**「最直覺的解法是什麼」,再問「這個解法在我的硬體上行不行」**
| Day 2 的失敗模式 | 最直覺的解法 | 硬體給的答案 |
|---|---|---|
| ① 圖例清單頁只有 76.2% | 換更大的模型(31B Dense) | ❌ Dense 要全讀權重:273 ÷ 15.35GB ≈ 17.8 tok/s,比現在慢 2.7 倍 |
| ② 多頁合併會偷工 | 那就一頁一頁跑 | ⚠️ 可以,但 40 頁要 5–10 分鐘;要靠 batch 拉吞吐,而 batch 又被 KV cache 排擠 |
| ③ 專有名詞幻覺(像但錯) | 同一題跑 3 次取共識 | ❌ 成本 ×3,每份文件 15–30 分鐘;而且同一顆模型會犯同一個錯 |
| ④ 科目標籤比數字更易錯 | 把 prompt 寫得更精確 | ❌ 這是知識問題不是表達問題,沒有迴路就收斂不了 |
| ⑤ 雙表格被揉成一張 | 先做版面偵測,切開再丟 | ✅ 這條可以解——而且成本很低 |
一條一條講清楚:
31B Dense 版本的權重:30.7B × 0.5 byte ≈ 15.35 GB
Dense 表示每個 token 都要讀全部權重,沒有 MoE 的稀疏折扣:
273 GB/s ÷ 15.35 GB ≈ 17.8 tok/s
對照現在 26B MoE 的 48.5 tok/s
換大模型 = 慢 2.7 倍。 一份 40 頁的年報從 10 分鐘變成 27 分鐘
而且我還不知道它能不能把 76.2% 救起來——我為了一個沒被驗證的改善,先付出 2.7 倍的確定成本
(這不代表 31B 沒用。它在特定區塊上可能值得,這正是 Day 10 要測的東西:純文字區用快的,表格區用準的)
成本問題:每份文件從 10 分鐘變 30 分鐘,這還是在 batch 開得起來的前提下
更根本的問題:同一顆模型、同一個 temperature=0,跑三次會得到一模一樣的輸出
就算開了 temperature,它們共享同一組權重、同一套訓練資料、同一個視覺塔
「飽和聚酯樹脂」會被認成「酚醛樹脂」,是因為這顆模型的表徵空間裡這兩者很近
跑三次,它會很有共識地錯三次。
💡Tip: 這件事有個名字叫同源盲點。它是 Day 11 那篇「為什麼不能自己驗證自己」的核心論證,也是我整個系列最堅持的一條原則。驗證者必須跟被驗證者不同源——不同模型、不同架構、甚至不同技術路線(Day 22 會用到傳統 OCR 來交叉比對,就是為了這個)。
「一年內到期長期負債」被讀成「短期負債」
這不是模型沒看清楚,是它不知道這兩個科目在會計上不能互換
你要怎麼寫 prompt?把全部會計科目都列進去嗎?那 prompt 會比文件還長,而且你永遠列不完
這題的正解是外部知識 + 驗證迴路:拿 MOPS 的 XBRL 結構化資料當對照表(Day 17),或建立科目對照邏輯(Day 15)
回到 Day 1 那句話:
Agents need feedback loops, not perfect prompts.
Day 4 講過一組數據:GLM-4V 在 OCRBench 上,同樣是 8-bit,一個量化方法讓它拿 782 分,另一個讓它拿 0 分
而我今天測的 98%,是在 NVFP4 量化版上測的
我沒有 BF16 基線可以對照。
所以那 26 條錯誤裡,有多少是「模型本來就不會」,有多少是「量化打壞的」?
我不知道。 這是我目前這套設定的一個已知盲點,我把它記下來,Day 25 建 flywheel 的時候會需要處理它
把上面那張表再看一次
五條失敗模式,只有第 ⑤ 條能靠「調整單一模型的用法」解決
其他四條的共通點是什麼?
它們都需要「模型以外的東西」。
而這四件事,沒有一件是「換一顆更好的模型」能解決的
這就是 Phase 1 的結論:
單一模型不夠。
不是因為模型不夠強,是因為單一模型架構裡沒有「檢查」這個環節。
Day 2 的每一條錯誤都是安靜的——finish_reason: stop、格式正常、數字看起來合理
系統從頭到尾都認為自己成功了。
你要的東西不是一個更準的模型,是一個會發現自己錯了的系統
而「讓系統能判斷對不對、完了沒」,就是 Day 1 引用過的那句 Harness 定義:
讓 agent 根據目標,持續、正確地動作的工程。核心材料是「回饋訊號」。
你可能會想:那從明天開始講架構吧
還不行。
因為我們現在手上的失敗案例,全部來自「一份 PDF、五頁、一顆模型」
而繁體中文文件還有一整組我今天根本沒碰到的問題:
今天測的法說會簡報,版面其實相當乾淨
真正的年報附註、重大契約摘要,比這髒得多
如果我現在就開始設計架構,我會設計出一個「只能處理乾淨簡報」的架構
所以 Phase 2(Day 6-8)要先把繁中的難點攤開。 那三天會告訴你,為什麼標題裡那個「進階」不是行銷詞
明天見 👋
這五天產出的東西我都留著,後面會一直拿回來用:
| 資產 | 用途 |
|---|---|
| 5 頁 OCR baseline 的原始回應 | 後面每次改架構的對照組 |
| 26 條逐字錯誤案例 | 驗證機制的測試集雛形 |
| 覆蓋率指標(99.4% / 98.8% / 98.1% / 76.2%) | 那把尺 |
| 併發 1/2/4 的吞吐曲線 | 調度設計的依據 |
| 三頁合併的偷工紀錄 | 「不要一次丟太多」的證據 |
💡Tip: 最後提醒一件 Day 3 講過的事——這些數字要連同條件一起記:哪顆模型、什麼量化、多長的 context、併發多少、有沒有別的服務在搶記憶體。我自己就吃過虧,一個沒記條件的「150 tok/s」讓我今天差點寫出一段錯的分析。Day 26 會把這件事變成一套正式的記錄 schema。