今天有六次「反常結果」最後都是量測方法的問題,不是被測物。零散修補會留下不同批次混用的數字,所以整個矩陣用同一套工具重跑一次:4 個配置 × 3 種 context × 5 種併發。
關鍵改變:prefill 與 decode 分開量。先前的每 token 速率是 tokens / 總耗時,把一次性的冷 prefill 攤進了每 token 成本。現在用串流,TTFT 與間隔中位數各自成欄,冷暖也分開,並附伺服器自報的 prefix 命中率。
| 配置 | context | 併發 | 冷 TTFT | 暖 TTFT | decode tok/s | 總吞吐 | prefix | 退化 |
|---|---|---|---|---|---|---|---|---|
| 9B · TP4 | ~200 | c1 | 0.23s | 0.16s | 123.1 | 70.8 | 0% | 0.91 |
| c2 | 0.23s | 0.32s | 103.1 | 92.3 | 0% | 0.91 | ||
| c4 | 0.23s | 0.33s | 79.7 | 140.1 | 0% | 0.87 | ||
| c8 | 0.23s | 0.36s | 67.9 | 286.6 | 0% | 0.91 | ||
| c16 | 0.23s | 0.96s | 68.0 | 199.2 | 0% | 0.83 | ||
| 9B · TP4 | ~8K | c1 | 1.13s | 0.76s | 117.9 | 26.7 | 57% | 0.91 |
| c2 | 1.13s | 1.51s | 97.1 | 58.8 | 57% | 0.80 | ||
| c4 | 1.13s | 2.85s | 73.8 | 46.9 | 57% | 0.82 | ||
| c8 | 1.13s | 4.43s | 60.9 | 73.8 | 57% | 0.81 | ||
| c16 | 1.13s | 6.43s | 58.9 | 70.8 | 57% | 0.79 | ||
| 9B · TP4 | ~28K | c1 | 8.93s | 0.57s | 106.2 | 32.5 | 98% | 0.92 |
| c2 | 8.93s | 1.05s | 83.3 | 41.1 | 98% | 0.92 | ||
| c4 | 8.93s | 1.86s | 63.4 | 105.0 | 98% | 0.78 | ||
| c8 | 8.93s | 3.28s | 45.9 | 105.1 | 98% | 0.79 | ||
| c16 | 8.93s | 3.89s | 44.2 | 101.7 | 98% | 0.74 | ||
| 9B · TP2 | ~200 | c1 | 0.29s | 0.22s | 96.2 | 58.8 | 0% | 0.93 |
| c2 | 0.29s | 0.44s | 72.0 | 72.4 | 0% | 0.93 | ||
| c4 | 0.29s | 0.40s | 57.4 | 126.9 | 0% | 0.91 | ||
| c8 | 0.29s | 0.66s | 50.0 | 175.7 | 0% | 0.87 | ||
| c16 | 0.29s | 1.18s | 49.7 | 198.3 | 0% | 0.91 | ||
| 9B · TP2 | ~8K | c1 | 2.59s | 1.39s | 90.0 | 15.6 | 57% | 0.91 |
| c2 | 2.59s | 3.27s | 66.2 | 32.3 | 57% | 0.76 | ||
| c4 | 2.59s | 5.36s | 51.2 | 43.7 | 57% | 0.80 | ||
| c8 | 2.59s | 8.41s | 41.2 | 30.6 | 57% | 0.83 | ||
| c16 | 2.59s | 12.22s | 40.3 | 37.1 | 57% | 0.80 | ||
| 9B · TP2 | ~28K | c1 | 17.39s | 0.89s | 77.9 | 30.6 | 98% | 0.95 |
| c2 | 17.39s | 1.70s | 55.1 | 46.8 | 98% | 0.82 | ||
| c4 | 17.39s | 3.21s | 42.0 | 67.4 | 98% | 0.80 | ||
| c8 | 17.39s | 5.99s | 27.9 | 96.7 | 98% | 0.72 | ||
| c16 | 17.39s | 7.90s | 27.9 | 93.9 | 98% | 0.72 | ||
| 9B · 單卡 | ~200 | c1 | 0.44s | 0.36s | 69.7 | 40.8 | 0% | 0.93 |
| c2 | 0.44s | 0.72s | 47.7 | 42.5 | 0% | 0.91 | ||
| c4 | 0.44s | 0.59s | 39.8 | 87.5 | 0% | 0.91 | ||
| c8 | 0.44s | 0.73s | 33.0 | 138.2 | 0% | 0.91 | ||
| c16 | 0.44s | 2.00s | 32.9 | 123.5 | 0% | 0.91 | ||
| 9B · 單卡 | ~8K | c1 | 3.68s | 2.59s | 63.9 | 8.7 | 57% | 0.91 |
| c2 | 3.68s | 5.13s | 42.8 | 21.1 | 57% | 0.76 | ||
| c4 | 3.68s | 10.08s | 33.7 | 15.2 | 57% | 0.82 | ||
| c8 | 3.68s | 19.67s | 25.6 | 26.5 | 57% | 0.81 | ||
| c16 | 3.68s | 23.60s | 25.3 | 22.4 | 57% | 0.76 | ||
| 9B · 單卡 | ~28K | c1 | 33.83s | 1.57s | 53.3 | 13.2 | 98% | 0.92 |
| c2 | 33.83s | 3.10s | 34.0 | 15.8 | 98% | 0.92 | ||
| c4 | 33.83s | 5.94s | 24.2 | 32.0 | 98% | 0.82 | ||
| c8 | 33.83s | 11.43s | 18.8 | 44.4 | 98% | 0.77 | ||
| c16 | 33.83s | 14.30s | 16.4 | 43.1 | 98% | 0.80 | ||
| 27B · TP4 | ~200 | c1 | 1.30s | 0.35s | 60.4 | 52.2 | 0% | 0.76 |
| c2 | 1.30s | 0.70s | 45.4 | 73.2 | 0% | 0.70 | ||
| c4 | 1.30s | 0.70s | 32.0 | 109.3 | 0% | 0.74 | ||
| c8 | 1.30s | 0.80s | 30.9 | 208.6 | 0% | 0.70 | ||
| 27B · TP4 | ~8K / ~28K | — | 失敗待重跑:8K 回 0 個 content delta(疑似 max_tokens=128 被 think 吃光,即已知的串流空白陷阱);28K 回 HTTP 500(疑似超過我設的 maxlen 32768)。兩者皆未證實。 | |||||
這是不含 prefill 的逐字生成速度,也就是使用者實際感受到的吐字速率。先前報的 111.5 是 tokens / 總耗時,被一次性的 prefill 稀釋過;真正的單人天花板是 123.1。
| context | 單卡 | TP2 | TP4 | 單卡→TP4 |
|---|---|---|---|---|
| ~200 · 1 人 | 69.7 | 96.2 | 123.1 | +77% |
| ~8K · 1 人 | 63.9 | 90.0 | 117.9 | +85% |
| ~28K · 1 人 | 53.3 | 77.9 | 106.2 | +99% |
| ~200 · 8 人 | 33.0 | 50.0 | 67.9 | +106% |
| ~8K · 8 人 | 25.6 | 41.2 | 60.9 | +138% |
| ~28K · 8 人 | 18.8 | 27.9 | 45.9 | +144% |
TPS 的最大殺手是併發,不是 context。TP4 從 1 人的 123.1 掉到 8 人的 67.9(−45%),而同樣是 TP4、從短提示拉到 28K 只掉 14%(123.1 → 106.2)。所以要提升 TPS,該攻的是併發時的排程與批次效率。
另一個方向:卡加得越多,在越吃緊的情境越值得 —— 單卡→TP4 的增益從短提示單人的 +77% 一路升到 28K 八人的 +144%。
28K 的冷 TTFT 是 單卡 33.83s → TP2 17.39s → TP4 8.93s(3.8 倍),大於同條件下 decode 的 2.0 倍。這不影響 TPS 結論,但先前完全沒量過。暖起來後 28K TTFT 只剩 0.57s(prefix 命中 98%),所以多輪對話裡長 context 的代價幾乎只在第一次。
它是 content delta 數 / 牆鐘,而每則回應的長度本來就不一樣,所以同一列會出現非單調(例如 8K 的 c4 46.9 低於 c2 58.8)。decode tok/s 才是穩健的指標(串流間隔中位數,不受回應長度影響);總吞吐只適合看數量級。
| 配置 | 1 人 | 2 人 | 4 人 | 8 人 | 8 人合計 | 量測工具 |
|---|---|---|---|---|---|---|
| 9B · TP4 · 生產 | 111.5 | 88.8 | 64.6 | 60.5 | 393.1 | v2 |
| 9B · TP2 | 85.1 | 62.7 | 47.7 | 44.6 | 282.2 | v2 |
| 9B · 單卡 | 65.7 | 44.6 | 35.1 | 29.8 | 177.8 | v2 |
| 27B · TP4 · 8K · 捕捉集合僅 [1,2] | 56.2 | 40.9 | 5.2 | 5.1 | 40.7 | v1 · 待重量 |
先前我說「TP2→TP4 只有 +19%,all-reduce 已是主導項,再加卡不會有回報」—— 那是錯的,是拿兩套不同量測工具的數字相比得出的。四個拓樸現在都在 v2 下量過:單卡→TP2 +29.5%、TP2→TP4 +31.0%,每翻倍的增益一致。單卡→TP4 的正確數字是 +70%,不是先前報的 +61%。
c1 56.2、c2 40.9,c4 直接掉到 5.2(牆鐘 15.7s → 63.9s)——階梯狀而非漸進,根因在 log 裡:cudagraph_capture_sizes: [1, 2]。我建量測鏈時漏傳 --compilation-config 給 27B 那一臂,用了 fork 的預設,所以 c4、c8 掉出捕捉集合退回 eager。生產的 9B 設 [1,2,4,8],曲線就平滑。這不是 27B 的效能特性,是量測設定錯誤,該臂需重跑。
另外先前記錄的 27B TP4 = 38.4 是 64K context 量的,這次是 8K,兩者本來就不能直接比。
| 提示長度 | 1 人 | 4 人 | 8 人 | 16 人 | 8 人合計 |
|---|---|---|---|---|---|
| ~900 tok | 106.4 | 51.8 | 49.6 | 34.8 | 347.4 |
| ~7K tok | 40.3 | 17.9 | 9.6 | 9.1 | 89.8 |
| ~28K tok | 13.2 | 4.6 | 5.3 | 2.7 | 32.7 |
上表的每 token 速率是 completion_tokens / 總耗時,把一次性的 prefill 攤進了每 token 成本,而每次只生成 128 個 token —— 8.85 秒的冷 prefill 除以 128,就把整個數字壓垮了。那量到的不是「長 context 很慢」,是「冷啟動成本被除進了速率」。Wayne 指出 prefill 只有第一次要付,後續輪次是逐步增長的 context,這是對的。
用串流把兩段拆開重量(同一份 context 連發三輪):
| context | 輪次 | TTFT | 每 token 間隔 | decode tok/s | prefix 命中 |
|---|---|---|---|---|---|
| ~8K | r1 | 1.16s | 0.0085s | 117.9 | 0% |
| ~8K | r2–r3 | 0.75s | 0.0085s | 117.9 | 58% |
| ~28K | r1 | 8.85s | 0.0094s | 106.3 | 16% |
| ~28K | r2–r3 | 0.49s | 0.0094s | 106.3 | 98% |
decode 幾乎不受 context 長度影響:8K 117.9 → 28K 106.3,只掉 10%。在 TurboQuant 2-bit 下,28K 的 KV 相對於每步都要讀的 5.5GB 權重不算什麼,權重才是主導 —— 所以「每個 token 要重讀整份 KV 導致變慢」的說法也不成立。
prefill 則完全符合直覺:28K 第一次 8.85s,prefix cache 命中 98% 之後降到 0.49s,18 倍。實際的多輪對話裡,長 context 暖起來之後每 token 速度與短 context 相當。
這一條不受上述假象影響,因為它是同一列內的比較:短提示 c8 合計 347.4、c16 反而 332.4;28K 下 c8 32.7、c16 33.4 幾乎不動。超過 max-num-seqs 8 之後請求只是排隊,總吞吐不再增加。
舊工具的比例並不自洽 ——「每人 × 人數」與「合計」相差達 19%。查出三件事:
① 每個併發等級用的題目不同:PROMPTS[i % 8] 讓 c1 只跑第 0 題、c8 跑滿八題,中英文與長度都不同,曲線因此混進了「題目換了」這個變因。
② prefix caching 偏袒 c1:暖機只跑第 0 題,而那正是 c1 唯一量到的請求。
③ 兩欄不可相乘:每人取的是中位數,合計是總 token ÷ 牆鐘。
v2 讓每個等級跑相同的八題、全部預熱、三輪取中位。修正後「平均 × 人數 ÷ 合計」在 c1–c4 都是 1.00 上下,三輪離散 ≤ 0.8。
但頭條數字沒有被灌高 —— 修正後 c1 反而從 107.5 升到 109–111,因為八題全暖後平均更高。真正被破壞的是曲線形狀(c4 由 67.4 修正為 65.0)與兩欄的一致性。TP2 與 27B 兩列仍是 v1 量的,重量需要停生產的維護窗口。
那一臂在切換前執行,GPU0 還被生產佔著:Free memory on device cuda:0 (0.3/31.73 GiB) —— 排程順序問題,不是 27B TP4 的退化。先前量到的 38.4 tok/s 仍然成立。
max_tokens 太小時,推理模型會把整個額度花在思考段,串流下呼叫端會收到完全空白且無錯誤的回應(實測 200 tokens 時 4/6 空白)。請給 1024 以上。
52.5 tok/s / 每人 — 當時以隨機 token 當提示,掩蓋了輸出退化,而退化的輸出解碼更快,反而灌高數字。基準測試現已內建真實文本的詞彙重複度斷言。
MTP 在自由文本上是淨損,但工具呼叫的輸出高度重複(JSON 骨架、欄位名、<tool_call> 標籤幾乎每次相同)—— 那是 n-gram 命中率最高的地形。因此每個臂同時測工具呼叫與中文散文兩種負載:前者驗證假設,後者當對照組,確認它不會拖累一般對話。三種方法都不需要額外的 draft 模型。
| 方法 | 工具 c1 | 工具 c4 合計 | 散文 c1 | uniq | 備註 |
|---|---|---|---|---|---|
| baseline(無推測) | — | — | — | — | 排隊中 |
| ngram · 3 tokens | — | — | — | — | 排隊中 |
| ngram · 5 tokens | — | — | — | — | 排隊中 |
| ngram_gpu · 5 tokens | — | — | — | — | 排隊中 |
| suffix · 5 tokens | — | — | — | — | 排隊中 |
散文臂帶退化斷言。ngram_gpu 正是它攔下的:散文 25.6 tok/s 是所有推測臂最高,但輸出已是垃圾 —— 沒有閘門會被記成「最快」。但我後來去驗這道閘門,發現它對中文是瞎的。舊寫法用空白切詞,而中文不用空白,純中文輸出常常只切出一個 token,1/1 = 1.00,看起來滿分健康。在生產上實測校準,四種退化有三種拿到滿分:
| 樣本 | 字元 3-gram | 舊的 uniq-word |
|---|---|---|
| 中文正常輸出 ×3 | 0.92–1.00 | 1.000 |
| 英文正常輸出 | 0.578 | 0.713 |
| 單字重複 300 次 | 0.003 | 1.000 |
| 短句迴圈 | 0.022 | 1.000 |
| 兩句交替 | 0.034 | 1.000 |
| 英文詞迴圈 | 0.008 | 0.008 |
改用字元 3-gram(先去掉所有空白,中英文一視同仁):健康最低 0.578、退化最高 0.034,分離度 17 倍,門檻訂 0.40。這也代表先前那批推測臂裡若有純中文的退化,我根本看不見 —— `ngram_gpu` 是因為英文混雜才被抓到。
vLLM 的 ngram proposer 比對的是精確 token ID 序列,不是「這看起來像 JSON」—— 結構相似不等於 token 序列相同,所以工具呼叫的重複性並不會轉成命中率。而且失敗是正確性問題而非速度問題:三個推測臂都產不出合法的 tool_calls。同樣的量化 KV + ngram 失敗在 RTX 3090 上也有回報(vLLM issue #40831),因此不能歸因於 SM70 世代。
| checkpoint | τ 接受長度 | verify rate | 位置 0 | 位置 1 | 位置 2 | 位置 3+ | auc 整體 | auc@0 |
|---|---|---|---|---|---|---|---|---|
| step_210 · 2 層 · 128 anchors · 6.8GB | 1.56 | 0.195 | 0.491 | 0.045 | 0.015 | <0.004 | 0.528 | 0.861 |
| step_70 · 5 層 · 512 anchors · 9.0GB | 1.24 | 0.154 | 0.180 | 0.034 | 0.013 | <0.005 | 0.485 | 0.397 |
τ 1.24 → 1.56(+26%)、位置 0 接受率 0.180 → 0.491(2.7 倍)、信心頭 auc@0 從 0.397(比亂猜還差)到 0.861,而且模型小 25%(9.0GB → 6.8GB)。論文方向的修正成立。
更有力的是:新配置是帶著資料劣勢贏的 —— 它只用了約 7% 的訓練訊號(anchors 512→128 是每樣本訊號的 ¼,樣本又只取 4948 的 28%)。若 anchors 與資料量才是主導因素,新的應該要輸。
提議 7+1 個 token 只接受 1.56 個,而且位置 1 就掉到 0.045、位置 2 之後幾乎歸零 —— 鏈條仍然沒有延續性,τ 撐不起 verify 的成本。信心頭也只在第一步有效(auc@1 = 0.355 比亂猜還差,@3–@5 剛好 0.500 等於零訊號)。
下一步是把資料劣勢拿掉:訓練跑完後記憶體全數釋放(閒置 82GB),而當初 512 anchors 吃約 59GB 撞牆是因為那時只剩 61GB 可用。以 512 anchors × 完整 4948 樣本重訓,才是只差「5 層 vs 2 層」的乾淨 A/B。
前三個假設都被實測推翻,而且它們都很有說服力 —— 留在這裡是為了不再重走。真正的轉折是不再靠推論、改看哪一套帳本對不上:當 cuda_reserved 與程序 RSS 同時持平、系統可用記憶體卻持續下降,這個組合只指向一個地方。
| 修法 | 修法前 | 修法後 | 說明 |
|---|---|---|---|
| num_anchors 512 → 128 | 跑不到第一個 micro-step | 穩定推進 | Q_LEN 3584 → 896,scores 面積少 6.2 倍。硬體逼出的偏離,τ 需註明在 128 anchors 下測得。 |
| expandable_segments | 49→99 步流失 12.3GB | 流失 1.7GB | 治裝置端碎片。有效但不足以收斂。 |
| pin_memory=False | 步 464 剩 12.3GB 後陣亡 | 步 900 仍有 37.4GB | 治主機端釘選快取。GB10 主機與裝置共用實體 RAM,釘選換不到 DMA 好處 —— 每步耗時反而從 5.0s 降到 2.53s,全程預估從 19 小時縮到 9.4 小時。 |