Agent Telemetry

V100 推論叢集 · DGX 訓練研究
快照2026-08-10_1154UTC+8 · 每 3 小時更新
現況 · 2026-08-10

生產(9B · 64K · TP4)單人 decode 152.8 tok/s,八人各 76.8。8/6 關掉 VLLM_SM70_QWEN_GDN_FULL_FORWARD 之後 +24%,commit 9972c07,上線後兩趟複驗。

本頁分成七個大類,每類裡最新的在最上面,每段標了產出日期。打「約」的日期是回填的,可能差一天。總表裡標 GDN 開 的列是 8/6 那次變更之前量的 —— 當時是對的,但不代表現在的生產。

最新 · 2026-08-10

測了llama.cpp 的 KV dtype 受控組。llama-bench 的 -ctk/-ctv 是位置配對,先前那批其實只量到 V —— 重跑後:V 量化花 8% 生成速度,K 量化再花約 30%(K 在 QKT 內圈)。
測了llama.cpp 四卡三種 split-mode:layer 38.9(等於單卡)、row 22.0(慢 43%)、tensor NCCL 崩潰。llama.cpp 在這台沒有可用的 TP4,所以它只能當參照組。
排除flash attention 不是變數(f16 開關 FA:703.5 對 710.5)。先前 llama-bench 中止是因為 tbq3 必須要有 FA。
找到一份公開的 3090 紀錄跑同一顆 27B、同樣在 vLLM 上,MTP 逐位置接受率 97/95/91%、接受長度 3.4–3.8、125K 下 85 tok/s。我們是 50.5/15.2%、接受長度 1.50。差的是配置,不是架構。
更正我先前寫「只有一顆 MTP 頭,所以 position 1 崩到 15.2% 是架構決定的」。那個解釋錯了 —— 對照組同樣一顆 nextn 頭,position 3 是 91%。
否定draft 頭的量化不是低接受率的原因。四種量化(Q4_0 / Q8_0 / IQ4_XS)全部落在 40–49%,彼此在雜訊內,而且量化最重的 IQ4_XS 反而最高。把 blk.64 gate 到 Q8_0 重做 requant 這條路不用走了。
否定也不是我 8/6 關掉 GDN 旗標造成的。原始碼裡那個旗標只是 force,另有 auto 會自行武裝;8/8 與 8/9 的伺服器 log 都印了 guard armed (force=False, auto=True)。
修好llama.cpp 的 -sm tensor 崩潰根因是 NCCL 版本:系統的 2.18.3+cuda12.0 對上 CUDA 13 driver,查 entry point 失敗被誤譯成 「CUDA driver is a stub library」。LD_PRELOAD vLLM venv 的 2.27.5+cuda12.9 就跑起來了。
撤回所有 llama.cpp 四卡的速度數字,以及「四卡贏不了單卡」這個結論。-ts 是斜線分隔,我用了逗號 —— 在 llama-bench 那是「掃多組設定」,結果四個臂全部跑在 GPU 0。破綻是 pp512 四種配置差 0.15%。重跑中,這次會驗每張卡有沒有吃到權重。
在跑決定性對照:官方 safetensors 直轉的 IQ4_XS,量化型別與我們那顆合併版相同,唯一的變數是官方 vs 七路合併。
做了本頁改成分類 + 類內最新在上,並補上 TPS 與 TTFT 兩張總表。

TPS 總表 · 每人 decode

每一列都是量過的,沒有推算值。併發欄是同時的請求數,tok/s 是每人而非合計。「參照」列是別的引擎或別台機器,只用來對照,不是我們的產出。

模型 · 量化拓樸context併發tok/s時期註記
9B · Q4_K_MTP4~200 tok1152.8現行現行生產
9B · Q4_K_MTP4~200 tok876.8現行每人
27B · Q4_K_MTP4105,856 tok135.1現行兩趟 35.1 / 35.0
27B · Q4_K_MTP4105,856 tok88.1現行兩趟 8.1 / 8.1
27B · Q4_K_STP4~200 tok169.9現行比 Q4_K_M 快且小 0.9GB
27B · Q4_K_MTP4~200 tok166.5現行
9B · Q4_K_MTP4~200 tok1123.1GDN 開8/6 之前
9B · Q4_K_MTP2~200 tok196.2GDN 開
9B · Q4_K_M單卡~200 tok169.7GDN 開
9B · Q4_K_MTP4~28K tok1106.2GDN 開
9B · Q4_K_MTP4~28K tok845.9GDN 開每人
27B · Q4_0TP4~200 tok160.4GDN 開
27B · Q4_0TP4~200 tok830.9GDN 開每人
27B · IQ4_XSllama.cpp 單卡~200 tok138.8參照不開推測
27B · IQ4_XSllama.cpp 單卡~200 tok155.9參照MTP n=3 · +44%
27B · IQ4_XSllama.cpp 單卡24,704 tok133.4參照f16 KV
27B · IQ4_XSllama.cpp 單卡24,704 tok125.5參照tbq3 KV · 反而更慢
27B · INT4vLLM · 3090 單卡125K tok185.0參照外部紀錄 · MTP 接受長度 3.4

屋頂線:27B IQ4_XS 是 15.13 GB,單卡 900 GB/s 給出 59.5 tok/s 的單 token 上限,四卡 238。llama.cpp 單卡不開推測拿到 65%,開 MTP 拿到 94% —— 單卡幾乎榨乾了。我們的 vLLM 四卡在 29%,差距不在頻寬。

TTFT 總表 · 首 token 延遲

冷 = prefix cache 沒命中,暖 = 同一份提示再問一次。長 context 的代價幾乎全在冷啟動,所以 128K 能不能用,取決於同一份文件會不會被重複問。

情境context冷 TTFT暖 TTFTprefix 命中時期註記
9B · TP4 · c1~200 tok0.23s0.16s0%GDN 開
9B · TP4 · c1~8K tok1.13s0.76s57%GDN 開
9B · TP4 · c1~28K tok8.93s0.57s98%GDN 開
9B · TP4 · c8~28K tok8.93s3.28s98%GDN 開
27B · TP4 · c1~200 tok1.30s0.35s0%GDN 開
27B · TP4 · c1105,856 tok390.90s31.10s96%現行128K 配置
27B · TP4 · c8105,856 tok390.90s247.50s96%現行每人
27B · TP4 · c1115,645 tok492.80s16.70s命中現行needle 探針同一份提示
冷啟動買回來的部分 · 只改設定沒改 kernel

max-num-batched-tokens 16384 → 65536:冷 TTFT 412.42s → 304.04s(−26.3%)。block-size 16 → 64:暖 TTFT −15%。

服務

fable-9b · V100:8600
無回應 ()
Qwopus-27B regen · V100:8900
無回應 ()
工具呼叫
已上線 · 8/8 + 7/7
Hermes gateway
待啟用

背景產線

DSpark drafter 訓練 · DGX GB1013440 / 13440 · 100.0%
1400 樣本子集 × 10 epoch,梯度累積 64。記憶體已打平,約 2.53 s / micro-step
DSpark target cache 建置 · DGX4948 / 5093 · 97.2%
已完成,4948 樣本 / 103GB
Qwopus-27B regen 資料生成 · V100 TP28931 / 8931 · 100.0%
可斷點續傳,供訓練資料擴量

吞吐量實測

9B-Q4_K_M · 64K · TP4×1人 · GDN 融合關閉
現行生產
152.8 tok/s
9B-Q4_K_M · 64K · TP4×1人 · GDN 融合開啟
2026-08-06 前
123.4 tok/s
9B-Q4_K_M · 64K · TP2×1人 · TQ-2bit s96
90.5 tok/s
9B-Q4_K_M · 64K · 單卡×1人 · TQ-2bit s96
切換前
66.6 tok/s
9B-Q4_K_M · 64K · TP4×8人 · GDN 融合關閉
每人
76.8 tok/s
27B-Q4_0 · 8K · TP2×1人 · KV auto
37.2 tok/s
27B-Q4_0 · 8K · TP2×4人 · KV auto
每人
22.5 tok/s
0每使用者 tok/s153
生產與吞吐7 段
2026-08-06

併發天花板是我們自己設的

先排除記憶體

伺服器自報 GPU KV cache size: 17,794,448 tokens、Maximum concurrency for 65,536 tokens per request: 271.52x —— KV 空間可以撐 271 個滿長度請求,而我們設 max-num-seqs 8。那不是記憶體限制,是人為上限。(這也順帶關掉一條路:DeepSeek V4 那套 CSA/HCA 序列軸壓縮是為了把 KV 壓到能落 disk,我們的 KV 已經過剩 271 倍,壓它換不到任何東西。)

併發seqs=8 每人seqs=32 每人seqs=8 合計seqs=32 合計合計增益
1123.2123.571.471.5—
868.468.3261.9286.0+9%
1667.855.4228.8403.2+76%
3267.736.9227.0619.0+173%
那條「打平在 68」的平原是排隊,不是極限

seqs=8 時 c16、c32 的每人速度紋風不動(67.8 / 67.7),因為超過 8 的請求只是在等。放寬到 32 之後,總吞吐在 c32 從 227 升到 619,2.7 倍。

但這是取捨不是純賺:每人從 67.7 掉到 36.9,同時服務的人數變四倍。佐證在 chk 欄(decode×併發 ÷ 總吞吐):從 9.55 降到 1.91 —— 牆鐘時間真的花在解碼而不是排隊了。

所以要單人最快就壓低併發(123.5),要總產能最大就放寬上限(619)。兩者不可兼得,取決於這台機器要服務幾個人。已套用 max-num-seqs 32(2026-08-06,commit 8aed933):Wayne 給的使用者上限是 16,而 32 在 c1–c8 量到與 8 完全相同,所以它只會在人多時起作用,不會犧牲單人速度。

2026-08-06

每卡 29GB 是怎麼來的 · 砍到 9.5GB 沒有代價

佔用的不是權重,是 KV 預留

9B 的模型檔是 6.5GB,TP4 之下每卡約 1.6GB 權重,但 nvidia-smi 每卡讀到 29GB。差額幾乎全是 --gpu-memory-utilization 0.90 預留的 KV pool:17,794,448 tokens,而 maxlen 65536 × max-num-seqs 8 最多只用得到 524,288 —— 過剩 33.9 倍。

ctx / 併發util 0.90 decode/sutil 0.30 decode/s差
200 / c1123.6123.0−0.5%
200 / c479.879.7−0.1%
200 / c868.168.2+0.1%
28000 / c1106.0106.00.0%
28000 冷 TTFT6.58s6.58s0.0%
已套用(commit 8aed933)

全部落在噪音以內,所以 0.90 → 0.30 已進生產:每卡 29.0GB → 9.5GB,四張卡共釋出約 78GB。KV pool 降到 4,319,677 tokens,仍是滿長度請求需求的 65.91 倍。驗收:health 200、工具呼叫 ALL_PASS。回滾:cp fable9b_start.sh.bak_util fable9b_start.sh 後重啟。

但它換到的不是「實驗與生產並存」

我原本的理由是釋出記憶體之後實驗不必再停生產。那個理由不成立 —— 擋住並存的不是記憶體,是 PSU:先前疑似燒掉電源的正是「四張卡上同時載入兩個模型」的功耗尖峰,記憶體有餘裕並不解除這條紀律。

真正換到的是故障半徑。先前一次沒收乾淨的實驗留下四個 VLLM::Worker 孤兒各佔 29GB,生產重啟每次都撞 Free memory 2.83 GiB < 28.56,在 Restart=always 之下變成 34 分鐘的失敗迴圈。現在生產只需要 9.5GB 就能起來,同樣的孤兒擋不住它。

量測方法也修了

第一版 probe 一個臂都沒跑完就結束:local u=$1 label="util${u/./}" —— bash 在 local 賦值之前就展開了整行所有的詞,所以 ${u/./} 展開時 u 還沒有值,在 set -u 之下函式直接失敗,EXIT trap 23 秒後把生產放回去。第二版順便改了方法:生產本身就是 0.90 那個臂,直接量它、再改一個值重啟,停機從兩個 20 分鐘窗口降到兩次 4 分鐘重啟,而且量到的就是要上線的東西,不是它的手打近似版。

2026-08-06

生產 +24% TPS:一個開著的旗標本身就是瓶頸

找法:對固定成本做消去,而不是再推論一次

三個嫌疑各做一臂。--enforce-eager 得到 10.3 tok/s(基準 123.6)—— CUDA graph 每 token 省掉約 89 毫秒的 launch 成本,而且 log 顯示解碼是 Capturing CUDA graphs (decode, FULL),單一完整 graph,所以「分段啟動成本」這條也死了。意外的是第三臂:關掉 VLLM_SM70_QWEN_GDN_FULL_FORWARD 反而快。

指標GDN=1(原生產)GDN=0差
c1 decode/s123.4 / 123.3153.1 / 152.8+24.0%
c8 decode/s68.0 / 68.076.3 / 76.8+12.5%
c8 總吞吐272.8 / 273.3275.7 / 272.7持平
退化指標1.001.00—
工具呼叫ALL_PASSALL_PASS—
已上生產(commit 9972c07)

A/B 用的是生產的完整配置(maxlen 65536、max-num-seqs 32、TP4、turboquant 2-bit KV),每臂跑兩趟。上線後再驗兩趟:c1 152.6 / 152.9、c8 76.5 / 77.1、deg 1.00、ALL_PASS。

性質要說清楚:這是單人延遲的改進,不是總產能的 —— c8 的總吞吐兩邊都是 273 左右。回滾:cp fable9b_start.sh.bak_gdn fable9b_start.sh 後重啟。

我差點把它退掉

第一次套用時量到 c8 只有 14.6 tok/s,我判定「探針配置沒轉移到生產」並退回。那是一個沒有重複驗證的樣本 —— 它的冷 TTFT 是 0.43,而其他每一次 run 都是 0.16,典型的剛重啟未暖。在生產完整配置上各跑兩趟之後,+24% 完全重現。凡是要決定生產變更的量測,一律每臂兩趟,這條從今天起是預設。

順帶修正前一節的模型:T(n) = 6.02 + 8.32/n 是在融合路徑開著時擬合的。關掉之後一個 token 是 6.53 ms —— 那個看起來不可縮減的固定成本,有一大塊其實就是這個旗標。

2026-08-06

27B 也一樣,而且長 context 併發下最明顯

context / 併發GDN=1(兩趟)GDN=0(兩趟)差
200 / c154.3 / 54.365.3 / 66.5+21.4%
200 / c828.0 / 28.130.8 / 31.3+10.7%
28000 / c145.9 / 46.454.2 / 54.2+17.4%
28000 / c817.0 / 17.021.7 / 21.6+27.4%
28000 / c8 總吞吐47.472.7+53.7%
28000 這兩列是第一次量到

先前它們被報成 only 0 content deltas 並被我當成模型失敗。那是量測工具數錯欄位(見前一節),把 reasoning 補上、max_tokens 提到 512 之後就有數字了。27B 不在生產上,所以沒有東西要套用 —— 但之後任何一次跑 27B,這個旗標都該是關的。

一個要講清楚的品質疑點

28000 / c1 的退化指標 GDN=1 是 1.00、GDN=0 是 0.61,看起來變差。但同一格的 think% 是 100 —— GDN=0 那臂把整個 512 token 預算都花在思考上,所以指標評的是思考文字而不是答案,而思考文字本來就比較重複。這是量測混淆,不是已證實的品質退化,但我也不能宣稱兩邊等價。要判定得把思考與答案分開評分,探針還沒有這個能力。

可判別的正確性檢查(複誦、質數序列、三個工具呼叫、toolcall_tests)兩臂全過,與 9B 一致。

2026-08-05

完整矩陣 · 一次跑完

為什麼重測全部

今天有六次「反常結果」最後都是量測方法的問題,不是被測物。零散修補會留下不同批次混用的數字,所以整個矩陣用同一套工具重跑一次:4 個配置 × 3 種 context × 5 種併發。

關鍵改變:prefill 與 decode 分開量。先前的每 token 速率是 tokens / 總耗時,把一次性的冷 prefill 攤進了每 token 成本。現在用串流,TTFT 與間隔中位數各自成欄,冷暖也分開,並附伺服器自報的 prefix 命中率。

配置context併發冷 TTFT暖 TTFTdecode tok/s總吞吐prefix退化
9B · TP4~200c10.23s0.16s123.170.80%0.91
c20.23s0.32s103.192.30%0.91
c40.23s0.33s79.7140.10%0.87
c80.23s0.36s67.9286.60%0.91
c160.23s0.96s68.0199.20%0.83
9B · TP4~8Kc11.13s0.76s117.926.757%0.91
c21.13s1.51s97.158.857%0.80
c41.13s2.85s73.846.957%0.82
c81.13s4.43s60.973.857%0.81
c161.13s6.43s58.970.857%0.79
9B · TP4~28Kc18.93s0.57s106.232.598%0.92
c28.93s1.05s83.341.198%0.92
c48.93s1.86s63.4105.098%0.78
c88.93s3.28s45.9105.198%0.79
c168.93s3.89s44.2101.798%0.74
9B · TP2~200c10.29s0.22s96.258.80%0.93
c20.29s0.44s72.072.40%0.93
c40.29s0.40s57.4126.90%0.91
c80.29s0.66s50.0175.70%0.87
c160.29s1.18s49.7198.30%0.91
9B · TP2~8Kc12.59s1.39s90.015.657%0.91
c22.59s3.27s66.232.357%0.76
c42.59s5.36s51.243.757%0.80
c82.59s8.41s41.230.657%0.83
c162.59s12.22s40.337.157%0.80
9B · TP2~28Kc117.39s0.89s77.930.698%0.95
c217.39s1.70s55.146.898%0.82
c417.39s3.21s42.067.498%0.80
c817.39s5.99s27.996.798%0.72
c1617.39s7.90s27.993.998%0.72
9B · 單卡~200c10.44s0.36s69.740.80%0.93
c20.44s0.72s47.742.50%0.91
c40.44s0.59s39.887.50%0.91
c80.44s0.73s33.0138.20%0.91
c160.44s2.00s32.9123.50%0.91
9B · 單卡~8Kc13.68s2.59s63.98.757%0.91
c23.68s5.13s42.821.157%0.76
c43.68s10.08s33.715.257%0.82
c83.68s19.67s25.626.557%0.81
c163.68s23.60s25.322.457%0.76
9B · 單卡~28Kc133.83s1.57s53.313.298%0.92
c233.83s3.10s34.015.898%0.92
c433.83s5.94s24.232.098%0.82
c833.83s11.43s18.844.498%0.77
c1633.83s14.30s16.443.198%0.80
27B · TP4~200c11.30s0.35s60.452.20%0.76
c21.30s0.70s45.473.20%0.70
c41.30s0.70s32.0109.30%0.74
c81.30s0.80s30.9208.60%0.70
27B · TP4~8K / ~28K—失敗待重跑:8K 回 0 個 content delta(疑似 max_tokens=128 被 think 吃光,即已知的串流空白陷阱);28K 回 HTTP 500(疑似超過我設的 maxlen 32768)。兩者皆未證實。
純 TPS(decode tok/s)

這是不含 prefill 的逐字生成速度,也就是使用者實際感受到的吐字速率。先前報的 111.5 是 tokens / 總耗時,被一次性的 prefill 稀釋過;當時真正的單人天花板是 123.1。這個數字已被取代:關掉 GDN 融合之後是 152.8,見本頁上方。

context單卡TP2TP4單卡→TP4
~200 · 1 人69.796.2123.1+77%
~8K · 1 人63.990.0117.9+85%
~28K · 1 人53.377.9106.2+99%
~200 · 8 人33.050.067.9+106%
~8K · 8 人25.641.260.9+138%
~28K · 8 人18.827.945.9+144%

TPS 的最大殺手是併發,不是 context。TP4 從 1 人的 123.1 掉到 8 人的 67.9(−45%),而同樣是 TP4、從短提示拉到 28K 只掉 14%(123.1 → 106.2)。所以要提升 TPS,該攻的是併發時的排程與批次效率。

另一個方向:卡加得越多,在越吃緊的情境越值得 —— 單卡→TP4 的增益從短提示單人的 +77% 一路升到 28K 八人的 +144%。

附帶發現:TP 在 prefill 的增益比 decode 更大

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 才是穩健的指標(串流間隔中位數,不受回應長度影響);總吞吐只適合看數量級。

2026-08-04 約

併發曲線 · 舊工具 · GDN 融合開啟時期(保留供對照)

配置1 人2 人4 人8 人8 人合計量測工具
9B · TP4 · 生產111.588.864.660.5393.1v2
9B · TP285.162.747.744.6282.2v2
9B · 單卡65.744.635.129.8177.8v2
27B · TP4 · 8K · 捕捉集合僅 [1,2]56.240.95.25.140.7v1 · 待重量
更正:並行增益沒有遞減

先前我說「TP2→TP4 只有 +19%,all-reduce 已是主導項,再加卡不會有回報」—— 那是錯的,是拿兩套不同量測工具的數字相比得出的。四個拓樸現在都在 v2 下量過:單卡→TP2 +29.5%、TP2→TP4 +31.0%,每翻倍的增益一致。單卡→TP4 的正確數字是 +70%,不是先前報的 +61%。

27B TP4 的 c4 斷崖是我的設定錯誤

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,兩者本來就不能直接比。

2026-08-04 約

長 context · 生產 TP4 · GDN 融合開啟時期

提示長度1 人4 人8 人16 人8 人合計
~900 tok106.451.849.634.8347.4
~7K tok40.317.99.69.189.8
~28K tok13.24.65.32.732.7
撤回:「長 context 慢八倍」是量測假象

上表的每 token 速率是 completion_tokens / 總耗時,把一次性的 prefill 攤進了每 token 成本,而每次只生成 128 個 token —— 8.85 秒的冷 prefill 除以 128,就把整個數字壓垮了。那量到的不是「長 context 很慢」,是「冷啟動成本被除進了速率」。Wayne 指出 prefill 只有第一次要付,後續輪次是逐步增長的 context,這是對的。

用串流把兩段拆開重量(同一份 context 連發三輪):

context輪次TTFT每 token 間隔decode tok/sprefix 命中
~8Kr11.16s0.0085s117.90%
~8Kr2–r30.75s0.0085s117.958%
~28Kr18.85s0.0094s106.316%
~28Kr2–r30.49s0.0094s106.398%

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 相當。

併發拐點確實在 8

這一條不受上述假象影響,因為它是同一列內的比較:短提示 c8 合計 347.4、c16 反而 332.4;28K 下 c8 32.7、c16 33.4 幾乎不動。超過 max-num-seqs 8 之後請求只是排隊,總吞吐不再增加。

量測工具 v1 的三個缺陷(已修)

舊工具的比例並不自洽 ——「每人 × 人數」與「合計」相差達 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 量的,重量需要停生產的維護窗口。

27B TP4 為何沒有數字

那一臂在切換前執行,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 當提示,掩蓋了輸出退化,而退化的輸出解碼更快,反而灌高數字。基準測試現已內建真實文本的詞彙重複度斷言。

長 context · 128K5 段
2026-08-07

目標:27B Fable · 128K · 8 併發 —— 全線驗證通過

先確認這不是品質問題

GGUF metadata:qwen35.context_length = 262144 —— 模型原生訓練到 256K,128K 不需要 rope 外推,所以它純粹是供應問題。同一份 metadata 還帶出兩件事:full_attention_interval = 4(只有四分之一的層保留 KV,這解釋了為什麼 pool 用 token 數算起來這麼大,也解釋了為什麼那個 GDN 旗標影響這麼大 —— 四分之三的層都走那條路),以及 GQA 24/4、key/value length 各 256。

第一次啟動失敗,但修法是改設定不是改 kernel

RuntimeError: Unsupported decode partition_size: 2048。partition_size 是 ceil(maxlen / num_kv_splits) 往上取到合法磚塊,而 131072/96 = 1366 → 2048。flash_attn_interface.py 的 VALID_DECODE_PARTITION_SIZES 裡有 2048,但編譯好的 CUDA 擴充從來沒實例化它 —— 兩邊不同步,所以錯誤要拖到 kernel 呼叫才爆。把 tq_max_kv_splits_for_cuda_graph 從 96 調到 160,per-split 變 820、磚塊回到 1024,問題就沒了。不用重編 kernel。(96 是當初為 64K 挑的:65536/96 = 683,剛好也落在 1024。)

項目實測
KV pool4,877,750 tokens = 131,072 之下 37.21x 併發(需要 8)
每卡記憶體19 GB / 32(util 0.60)
105,856 token 提示 · c135.1 / 35.0 tok/s(兩趟)
105,856 token 提示 · c88.1 / 8.1 tok/s
冷 TTFT390.9s
暖 TTFT(prefix 命中 96%)31.1s(c1) / 247.5s(c8)
needle 檢索 · 115,645 token5/5 全過(深度 5/25/50/75/95%)
工具呼叫ALL_PASS
代價集中在冷啟動,不在解碼

116K 提示第一次要 492.8 秒(8.2 分鐘),prefix cache 命中之後同一份提示只要 16.7 秒。所以 128K 能不能用,取決於同一份長文件會不會被重複問 —— 會的話代價攤掉了,每次換新文件就是每次八分鐘。

prefill 是平方成長的:4.3 倍的 token 換到 16.5 倍的時間(4.3² = 18.5)。這也順帶反證那個 context 是真的長,不是標籤寫大而已。

量測工具第五個缺陷:context 標籤從來不是 token 數

拿 tokenizer 實際數過之後:matrix 的標籤高估 1.13 倍(~120000 其實是 105,856),而 needle 探針的英文填充文字差 4.65 倍 —— 它第一次要 120000、實際只拿到 25,804,我差點把 26K 的五連過報成 128K 通過。差別在填充文字:matrix 用中文(約 1.4 字元/token),needle 用英文(約 6.5)。探針現在用真的 tokenizer 長到目標,並把實際拿到多少印出來。

2026-08-07

128K 的成本全在 prefill,而其中一半可以買回來

105,856 token 提示batched-tokens 16384batched-tokens 65536差
冷 TTFT412.42s304.04s−26.3%
暖 TTFT · c131.08s31.14s不變
暖 TTFT · c8247.73s246.34s完全不變
decode c135.035.0不變
decode c87.57.5不變
猜對一半:冷 prefill 是分塊綁的,c8 的懲罰不是

暖起來之後 c8 是 247.5s、c1 是 31.1s —— 比值 7.97,幾乎剛好是 8,看起來就是八個 prefill 嚴格排隊。而 --max-num-batched-tokens 整個 session 都固定在 16384,是唯一沒動過、又直接管 prefill 進度的旋鈕。

結果:冷 prefill 真的是分塊綁的,加寬到 65536 省下 26.3%(412 → 304 秒)—— 而 128K 的成本正好集中在這個數字,所以這個要拿。但 c8 的懲罰一動也不動(247.73 → 246.34)。那代表八倍是每個請求各自要付的工作,不是排程器扣住產能不放,加寬 prefill 批次藏不掉它。這是本 session 第四個被量測打掉的結構性推論。

數字是穩的

16384 那一臂與前一輪獨立跑出來的結果吻合到小數點:暖 c1 31.08 vs 31.14、暖 c8 247.73 vs 247.56。所以這不是單一樣本,是可重複的行為。

目前沒有套用到任何地方 —— 生產跑的是 9B / 64K,冷 TTFT 本來就只有 6.6 秒。這是 27B / 128K 那套配置真的架起來時要帶的設定。

2026-08-07

八個人問同一份 128K 文件:每人還是 31 秒,而且不共用

先前的解釋是錯的

我把 c8 的八倍懲罰歸給「量測工具給每個併發請求不同的尾巴,所以 prefix 只命中 96%,缺的約 4,200 個 token 各自要對整段做注意力」。照這個說法,八個人問同一份文件(逐位元組相同)應該很便宜。不是。

105,856 token · 完全相同的提示TTFTprefix 命中
第 1 次(冷)303.23s—
第 2 次30.95s96%
第 3 次30.95s96%
8 人同時問同一份最快 31.05s · 中位數 245.13s · 最慢 245.14s96%
兩件事,都是操作面的

一、暖起來的 106K 文件,每一次問都要 31 秒,而且永遠不會更便宜。第 2 次和第 3 次吻合到小數點兩位(30.95 / 30.95)—— 那是決定性的重算,不是還沒暖完。

二、八個人同時問同一份文件不會共用那份工作。一個人拿到 31 秒,其餘排隊,牆鐘 245 秒 —— 每多一個併發使用者就多約 31 秒。

機制還沒定案,而我今天已經猜錯四次

最順的候選是 GDN 狀態重掃:full_attention_interval = 4,65 層裡有 49 層是遞迴的,它們的狀態沒辦法放進 KV prefix cache,每次都得重掃。但 SSM 掃描是線性的,而實測的暖 TTFT 是平方的 —— 24,704 → 105,856 token 是 4.28 倍,1.82s → 31.0s 是 17.0 倍(平方預測 18.4,線性預測 4.28)。所以那個解釋不成立。

另一個相容的說法是「固定約 4% 的區塊沒命中,而這些 token 各自要對整段做注意力」,那是 0.04n²,形狀對得上。但為什麼逐位元組相同的提示會有 4% 沒命中,我沒有答案。要定案得拿一個沒有 GDN 層的模型來對照,而這裡每個 Fable 變體都是同一種混合架構。先記錄,不再往下猜 —— 今天已經有四個從架構往下推的假設被實測打掉。

2026-08-07

那 31 秒不是固定成本 —— 它長得比文件還快

文件大小暖 TTFT(兩次)對前一列的成長
24,704 tokens1.79 / 1.78 s—
49,4085.90 / 5.90 stoken ×2.00 → 時間 ×3.31(n^1.72)
74,11212.58 / 12.59 s×1.50 → ×2.13(n^1.87)
105,85630.98 / 30.97 s×1.43 → ×2.46(n^2.53)
指數在往上跑

1.72 → 1.87 → 2.53。不只是平方,而且越長越糟。每一組兩次量測吻合到 0.01 秒,所以這是決定性的計算量,不是雜訊。照最後一段的斜率外推,131,072 token 的暖 TTFT 約 53 秒。

實務上的意思:文件放大一倍,等待時間不只放大一倍。128K 不是「64K 的兩倍慢」,是更糟。如果要壓這個成本,把文件切小比買更多卡有效得多。

這一輪的冷數字不能用,我先講

四個尺寸是巢狀前綴(同一段文字重複到不同長度),所以量到 120K 時前面 84K 早就在快取裡了 —— 這一輪的「冷」欄(216.67s)被污染,真正的冷數字是專門那一輪量的 412.42s(bt 16384)與 304.04s(bt 65536)。暖數字不受影響:它是同一份完整文件被問過之後再問的結果。

機制:三個解釋都被排除,我停在這裡

依序死掉的是 —— 量測工具尾巴不同(逐位元組相同的提示成本一樣)、prefill 批次寬度(16384 vs 65536 對暖 TTFT 影響為 0)、以及 GDN 狀態沒被快取(排程器裡有 _mamba_block_aligned_split,就是專門在快取它的)。我不再往下猜。今天從架構往下推的假設已經死了五個,而這個數字的形狀對決策就夠用了:成長比 n² 還快,所以答案在文件大小,不在硬體。

2026-08-07

把 KV pool 調小不會讓 TPS 變快 —— 而且有個下限

27B · 128K 配置KV pool併發餘裕106K · c8 decode/s
util 0.60 · bt 163844,877,75037.21x7.5
util 0.60 · bt 655362,523,13619.25x7.5
util 0.25 · bt 65536起不來 —— No available memory for the cache blocks
7.5 是不動的

今天轉過的每一個旋鈕 —— gpu-memory-utilization、max-num-batched-tokens、GDN 融合開關 —— 都沒能移動 c8 的 7.5。決定它的是每產生一個 token 要掃自己那 106K 的 KV,八個人就是八份,而那份 KV 是實際在用的,不會因為池子變小而變少。池子只是預留:預留少一點會省記憶體,不會省時間(9B 上也量過同一件事,pool 砍到四分之一,123.6 → 123.0)。

下限也找到了:util 0.25 之下,權重 4.3 GB/卡 加上 bt 65536 的 activation workspace 就吃光 8.1 GB 的預算,KV 一格不剩。

順帶:先前那個看起來過剩的 37.21x 已經自己降到 19.25x —— 是 bt 65536 多吃 workspace 順手擠掉的,不用另外調。

要讓 8 人的 TPS 上去,只剩三條路,而且都不在配置裡:人數減半、文件切小(見 n^2.5 那節)、或換小模型。

MTP · 推測解碼4 段
2026-08-10

外部對照組把「單頭」這個解釋否掉了

再更正一次,這次是我上面那段

我在上一節寫:nextn_predict_layers = 1,只有一顆 MTP 頭,所以 position 1 只有 15.2% 是架構決定的。這個解釋是錯的。

一份公開的單張 RTX 3090 紀錄跑同一顆 Qwen3.6-27B、同樣一顆 nextn 頭、同樣在 vLLM 上、同樣用 TurboQuant KV,拿到的是逐位置 97 / 95 / 91%、平均接受長度 3.4–3.8。單頭如果真的會讓 position 1 崩到 15%,那份紀錄的 position 3 不可能是 91%。架構不是原因,我們這台的配置才是。

該紀錄(3090,單卡)我們(V100 ×4)
逐位置接受率97 / 95 / 91%50.5 / 15.2%
平均接受長度3.4–3.81.50
不開推測38 tok/s66.5(四卡)
開 MTP63.8 → 85(加 TurboQuant)30.2(倒扣)
模型格式AutoRound INT4,mtp.fc 解量化成 BF16GGUF,blk.64 跟著主體量化
兩個具名嫌疑,而且可以切開

(a) draft 頭被量化壞了。對照組刻意把 MTP 那層留在 BF16。我們指定使用的 IQ4_XS 版本,blk.64 整層(含 nextn.eh_proj)都是 IQ4_XS。draft 頭失真不會產生錯誤輸出 —— target 每個 token 都會驗 —— 它只會提出一堆被打回票的 token,症狀就是低接受率。

(b) 七路合併位移了 hidden state。這個檔是 DavidAU 七個微調的合併,而 nextn 頭是對原始 base 訓的。若是這個,requant 救不回來,只能換權重檔。

切開的方法:Q4_0-requant 對 Q4_0-ehq8-requant —— 同一次 requant,只差 eh_proj 一個張量(Q4_0 對 Q8_0)。分岔 → 是 (a),修法是把 blk.64 gate 到 Q8_0 重做一次 requant;不分岔 → 是 (b)。

模型eh_proj接受率tg
Q4_0-requantQ4_041.7%42.85
Q4_0-ehq8-requantQ8_040.5%42.35
IQ4_XSIQ4_XS47.2%47.19
Q4_K_MQ8_049.4%35.19
(a) 死了:draft 頭的量化不是原因

分離對照沒有分岔 —— 41.7% 對 40.5% 在雜訊內,而且量化最重的 IQ4_XS 接受率反而最高。四種量化全部落在 40–49%,和 eh_proj 的精度沒有相關性。所以把 blk.64 gate 到 Q8_0 重做 requant 不會有用,這條路不用走了。

另外兩個獨立佐證都指向同一邊:換引擎不改變答案(vLLM 量到 50.5%,llama.cpp 量到 40–49%),而模型作者自己的 card 就寫著「接受率低於 50% 就不該用 MTP 版」—— 我們正好卡在那條線上。

順帶劃掉我自己的嫌疑

我一度懷疑是 8/6 為了 +24% 關掉的 VLLM_SM70_QWEN_GDN_FULL_FORWARD 污染了接受率 —— fork 的註解把 full forward 稱為「active-MTP 品質防護」。

不是。原始碼裡那個旗標只是 force,另有 auto 會在偵測到 speculative config 時自行武裝;而 8/8 與 8/9 兩次量測的伺服器 log 都印了 full-forward guard armed (force=False, auto=True, spec_core=False)。防護當時是開著的,用當時的 log 就能判定,不必重跑。

官方 vs 合併:合併有代價,但只佔一小半

官方 safetensors 直轉的 Qwen3.6-27B-IQ4_XS-mtp,同量化型別、同引擎、同提示、同 session,各兩趟:

模型接受率tg
官方 Qwen3.6-27B60.7% / 60.7%52.8 / 53.9
合併 Fable-Fus-71147.2% / 46.2%47.1 / 47.0
但真正的大頭是我出的題目

60.7% 離對照組的 97/95/91% 還差 34 個百分點,而那段幾乎全在量測條件。同一顆合併模型,只換內容類型:

內容接受率tg實際產出開頭
高重複99.0%77.10The quick brown fox jumps over…
結構化 JSON85.4%69.56[ { "id": 1, "name": "Alice Johnson"…
程式碼70.9%60.68from __future__ import annotations…
散文(原本下結論用的那題)47.2%47.35—
所以先前「MTP 不划算」是在最不利的那一格判的

同一顆模型從 47.2% 到 99.0%。我拿來下結論的那題是短提示、新散文、temp 0、冷啟動,而且 200 個 token 全被思考前言吃掉。對照組報的是 warmed 的 code/narrative,而且它自己就把兩者分開報(79.7 對 63.8 TPS)—— 兩邊根本不在同一格。

這對生產有直接意義:我們的工作負載是工具呼叫,那是結構化輸出,落在 85–99% 那一端,不是 47%。

量測紀律:指標騙得了人,產物騙不了

第一次的內容對照整組作廢:官方模型對兩個完全不同的提示回傳一模一樣的 drafted 171 / accepted 141。我的驗證腳本比對輸出 hash、判定「巧合,數字有效」—— 判定是錯的。把生成內容印出來才看見兩份都是 <think> Here's a thinking process: 開頭的同一套樣板:這是推理模型,n_predict 200 根本走不到答案,每個臂量的都是同一段前言。

現在每個臂都印出實際生成的前 80 字。題目沒生效當場就看得出來,不必等某個數字剛好可疑。

還沒解決:官方那份 GGUF 不吃 /no_think(四個臂全都還是思考前言),所以官方欄仍在量前言,不能直接跟合併版的 70.9 / 85.4 / 99.0 比;另有兩格回傳空字串,標為無效,原因未查。

2026-08-08

MTP 的兩層問題,以及模型作者本人給的對照

先更正我自己的說法

我先前寫「推測解碼在這裡結構上不可能贏,加速比 = τ/uq < 1」。那是錯的。這個技術的前提就是「驗證 k 個 draft 只花約一次 forward」——k+1 個 query 位置共用同一次 KV 掃描,所以加速比是 τ。產生 τ/uq 的是這個 fork 的後端,不是技術本身。

模型作者(DavidAU)的 model card 給了獨立佐證:同一顆模型在 llama.cpp 上,一般 GGUF 約 75 t/s、MTP 版可超過 90 t/s —— 那邊 MTP 贏 20%,我們這邊虧 65%。變數是引擎,不是模型也不是量化。

spec-tokens接受率(greedy)τposition 0position 1
229.9%1.6044.7%15.2%
150.5%1.5050.5%—
第二層問題:spec-tokens 2 在架構上就不對

作者的 card 說接受率低於 50% 就不該用 MTP 版。我們在 spec-tokens 2 是 29.9%,降成 1 之後是 50.5% —— 剛好跨過門檻。原因在 metadata:qwen35.nextn_predict_layers = 1,這顆模型只有一顆 MTP 頭。要它吐第二個 token,只能拿第一個 draft 的隱藏狀態再推一次,誤差滾雪球 —— position 1 只有 15.2% 就是這麼來的。

溫度不是主因:作者建議的 temp 1.0 之下是 34.1%(spec 2)與 45.4%(spec 1),和 greedy 同一個量級。

修正給 Codex 的驗收目標

接手檔原本寫「kernel 修好之後 τ ≈ 1.5–2.5x」。實測 τ 是 1.45–1.60,所以實際期望是 1.5–1.6x。已更正 —— 給錯的驗收目標比不給更糟,會讓一個成功的修正看起來像失敗。

2026-08-03 約

推測解碼:找到根因,修掉大半,但還沒對

症狀與根因

27B 開 MTP 之後輸出崩壞,而且只在 spec-tokens ≥ 2 時發生。根因在 turboquant_attn.py:reorder_batch_threshold 是 2,所以 qlen 為 2(spec-tokens 1)走 _spec_decode_attention 正常,qlen ≥ 3 掉進 prefill 路徑,每一題都錯。那個 uniform-decode 迴圈寫成 for j in range(uq),本來就通用,只有這個門檻把 3 以上擋在外面。

臂threshold算術複誦序列工具呼叫degdecode/s
CTL_spec3(修正前)2錯錯錯0/30.2121.7
FIX_spec28錯對對3/31.0024.9
BASE_nospec(不開推測)—錯對對3/31.0054.2
FIX_spec38錯對對3/31.0020.8
修正前是全面崩壞

threshold 2 之下 12345×6789 答成 8333333333…、複誦只吐出一個 X、質數序列變成 2, 3, 5, 5, 5, 5, …1111111…,三個工具呼叫一個都沒發出來。threshold 改成 8 之後複誦、序列、三個工具呼叫全部通過,退化指標從 0.21 回到 1.00。

更正:算術那題是模型不會,不是 bug

我原本從「兩個 threshold 8 的臂算術都錯,而且錯成不同的數」推論還有殘留 bug。無推測基準線把這一半推翻了:BASE_nospec 完全不開推測,算術一樣錯(8380905)。這題是 12345×6789,27B 心算五位乘四位本來就會錯 —— 那道題不是有效的正確性判別,複誦、序列與三個工具呼叫才是,而它們在 threshold 8 之下全過。

剩下的疑點沒有消失但弱得多:三個臂給出三個不同的錯答案(8380905 / 83810106 / 83809155),greedy 之下本該逐 token 相同。但推測解碼會把 qlen 從 1 變成 3,GEMM 形狀跟著變、浮點歸約順序跟著變,在接近平手的 argmax 上翻面是合理的 —— 這無法區分殘留 bug 與浮點非決定性。要判定得換測法:同一個提示,把開與不開推測的完整 token 序列做 diff,而不是問它答案對不對。這個探針要改。

而且它更慢 —— 先前報的退化幅度我算錯了

同條件的無推測基準線量到 54.2 tok/s,對上 spec-tokens 2 的 24.9 與 spec-tokens 3 的 20.8 —— 退化 −54%。(先前我引的 54.3 來自另一個 session,當時不該那樣比;基準線重現到 54.2,所以那個比較事後看是站得住的,但理由是運氣不是方法。)

慢的機制是清楚的:_spec_decode_attention 把 uq 個驗證位置拆成 uq 次獨立的全 KV 掃描(out[j::uq] = self._decode_attention(...)),每一次都重掃整個 cache,所以成本是 1/uq。而接受長度 τ 實測 1.5–2.5,永遠不可能超過 uq —— 結構上就贏不了。

真正的解法是把 triton_turboquant_decode.py 改成多 query 的 kernel(目前簽章 Q_rot_ptr # [B, Hq, D] 只吃一個 query,累加器是 tl.zeros([BLOCK_D]))。天花板是 τ 倍,即 27B 約 80–135 tok/s。

委員會的第一建議被原始碼推翻

fusion 委員會建議「把反量化融合進 attention kernel,可再擠 15–20%」。讀 triton_turboquant_decode.py(820 行)之後確認:反量化本來就在 kernel 內 —— 2/3-bit 索引解包、scale/zero 重建、centroid 查表全都在裡面,沒有先物化成 FP16 這一步。這條路沒有東西可以省。

2026-08-02 約

推測解碼 sweep · GPU3

假設

MTP 在自由文本上是淨損,但工具呼叫的輸出高度重複(JSON 骨架、欄位名、<tool_call> 標籤幾乎每次相同)—— 那是 n-gram 命中率最高的地形。因此每個臂同時測工具呼叫與中文散文兩種負載:前者驗證假設,後者當對照組,確認它不會拖累一般對話。三種方法都不需要額外的 draft 模型。

方法工具 c1工具 c4 合計散文 c1uniq備註
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
中文正常輸出 ×30.92–1.001.000
英文正常輸出0.5780.713
單字重複 300 次0.0031.000
短句迴圈0.0221.000
兩句交替0.0341.000
英文詞迴圈0.0080.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 世代。

引擎 · 量化 · 對照3 段
2026-08-10

llama.cpp 對照 · MTP 會贏,但 KV 量化與四卡都輸

為什麼還是留在 vLLM

llama.cpp 是參照組,不是候選。它證明了 MTP 在這顆模型上本來就該賺,所以 vLLM 這邊虧是我們的配置有問題 —— 這個證據值錢。但它自己不能當產線。

項目llama.cppvLLM(我們的 fork)
MTP38.8 → 55.9 t/s(+44%,單卡短 context)虧,qlen 2 的 target forward 是 qlen 1 的 1.88x
TurboQuant KV 解量化沒融合。K 量化在 QKT 內圈,pp 掉 54%融合進 attention kernel
四卡四種模式沒有一個贏過單卡,見下TP4 可用
撤回:所有四卡數字,包括我剛貼上來的那批

我先後報過 row 四卡 22.0(慢 43%)、再更正成 37.82、layer 38.9、tensor 34.47,並據此下結論說「四張卡沒有一個贏過單卡、多的三張卡什麼都沒做」。那些數字一個都不成立,結論也不成立。

破綻是 pp512:提示處理是算力綁的,四張卡該接近四倍,我卻拿到單卡 852.93、row2 853.34、row4 853.91、關 peer copy 854.24 —— 差 0.15%。真的分到四張卡不可能和單卡撞得這麼準。

原因在 llama-bench 自己印的表裡,我沒去看:ts 欄是 1.00,不是 1.00/1.00/1.00/1.00,而且 row2 印了兩對數據、row4 印了四對。-ts 的語法是 <ts0/ts1/..> —— 斜線分隔;逗號在 llama-bench 是「掃多組設定」。所以 -ts 1,1,1,1 被讀成「跑四次,每次 tensor-split = 1.00」,也就是全部塞在 GPU 0。四個臂量到的是同一件事。

這是同一天的第三次同類錯誤(還有 -ctk/-ctv 的位置配對、llama-cli 的 22.0):旗標吃進去、沒報錯、但做的不是我以為的事。重跑的版本不再下 -ts(預設就按 VRAM 平均切),並且每個臂在跑的時候取樣四張卡的記憶體,1–3 號卡沒吃到權重就把該臂標成 INVALID —— 不看旗標,看卡上有沒有東西。

llama.cpp 拓樸各卡峰值 (MiB)pp512tg128
單卡14645 / 10 / 10 / 10855.5439.24
layer 四卡4041 / 3629 / 3835 / 4365863.4238.46
row 四卡4891 / 4681 / 4685 / 505588.9922.12
tensor 四卡4519 / 4313 / 4313 / 43132333.0933.61
重跑之後,兩個結論都反過來

提示處理是會縮放的:tensor 模式 2333 對單卡 855,2.7 倍。我先前說「多的三張卡什麼都沒做」是錯的。

row 模式是病態的,不是「不會變快」:pp512 掉到 88.99,比單卡慢十倍。先前那個假的 37.82 剛好把這件事整個蓋住。

撐過來的只有一項:解碼確實不隨卡數變快,三種模式都輸給單卡。但這次是真的量到的,而且理由清楚 —— batch 1 每層都要四路 all-reduce,65 層的同步次數吃掉頻寬省下的量。成本在屏障的數量,不在它的大小。

唯一沒被波及的:tensor 模式崩潰的根因

-sm tensor 一直 abort 在 ggml_backend_cuda_comm_allreduce_nccl,NCCL 印的是 Cuda failure 'CUDA driver is a stub library'。但 libcuda.so.1 指到的是真的 driver(580.173.02,96 MB),不是 stub —— 那句話是舊 NCCL 對「driver entry point 查不到」的誤譯。

版本對不上:系統 NCCL 是 2.18.3+cuda12.0(2023 年),driver 卻回報 cudaDriverVersion 13000。把 vLLM venv 自帶的 2.27.5+cuda12.9 用 LD_PRELOAD 蓋上去,tensor 模式立刻跑起來(exit 0)。

這一條不受上面那個 -ts 錯誤影響:崩潰與不崩潰是二元的,和權重切在哪張卡無關。但它當時測到的速度(34.47)跟其他四卡數字一起作廢。

硬體側也確認過,而且這部分是獨立量的:四卡全 NV2 網狀、nvidia-smi topo -p2p r 全 OK。所以四卡到底能不能加速,現在是未知而不是「不能」—— 重跑中。

apples-to-apples 的 KV 修正

先前的 llama.cpp 數字用 f16 KV,對上 vLLM 的 2-bit —— 那不是比較。補測之後,llama.cpp 開 TurboQuant KV 反而更慢,MTP 的增益也從 +26% 塌到 +2%。

受控組(llama-bench 的 -ctk/-ctv 是位置配對,所以這三列其實只量到 V):

type_ktype_vpp24576tg128
f16f16711.639.41
f16tbq3_0330.3(−54%)36.30(−8%)
f16tbq4_0377.3(−47%)36.38(−8%)
所以 K 才是貴的那一半

V 量化只花 8% 的生成速度。但 K 與 V 都量化時整體掉到 25.5 t/s,多出來的約 30% 全記在 K 頭上 —— K 在 QKT 的內圈,每個 query 對每個 key 都要重讀重解一次,V 只在加權求和時碰一次。-ctk f16 -ctv tbq3 是還沒測的中間操作點。

順帶排除:flash attention 不是變數(f16 開關 FA 是 703.5 對 710.5)。早先 llama-bench 中止是因為 tbq3 必須要有 FA,-fa 0 連 context 都建不起來。

2026-08-05

解碼不是頻寬綁的 —— 砍掉 18% 的位元組只換到 5%

起點:兩個模型都只跑到記憶體屋頂的四分之一

V100 32GB 的 HBM2 是 900 GB/s。TP4 之下每卡每 token 要串的權重:9B 是 1.63 GB、27B 是 4.31 GB,對應的屋頂是 552 與 209 tok/s。實測 123.6 與 54.2 —— 22.4% 與 25.9%。而且 TP 每翻倍只 +30%(理想 +100%),擬合 T(n)=固定+w/n 得到約 45% 的解碼時間不隨卡數縮減。

更正:那不是「權重要在卡之間搬」

我一開始把它講成互連頻寬問題。那是錯的 —— TP 之下權重切開放在各卡,跨卡的只有 activation,而且只在 row-parallel 層(o_proj、down_proj)之後把部分和加起來。payload 只有 hidden×2 bytes ≈ 8 KB,每 token 全部加起來約 1 MB,在 NVLink(NV2,每卡 6 條 ×25.78 GB/s)上是 20 微秒,可忽略。真正的成本是每 token 128 道同步屏障,不是頻寬。(拓樸已確認:四卡兩兩 NV2,不是 PCIe。)

於是找到一個看起來很肥的目標:沒量化的 LM head

output.weight 是 BF16,而其他所有權重都是 Q4_K/Q6_K。詞表 248,320 很大,所以這一個張量就是 9B 的 29.2%(1940 MB)、27B 的 13.8%(2425 MB),而且每產生一個 token 就要完整讀一次。

臂大小c1 decode/sc4 decode/sthink%deg
Q4_0 · bf16head6,359 MB128.985.930.98
Q4_0 · q6head5,198 MB(−18.3%)136.188.41000.76
結果:假設推翻,而這是有用的負面結果

純頻寬模型預測 +22.3%,實測 +5.6%(c1)、+2.9%(c4)。位元組不是綁住解碼的東西 —— 這與「只跑到屋頂 25%」互相印證:真正的瓶頸在反量化的計算量、同步屏障與 kernel 效率,不在記憶體頻寬。所以「把 head 量化掉」這條路關掉:5% 的收益還要付 deg 0.98 → 0.76 的品質代價。

連帶也修正了先前那個 +20% TPS 的推估 —— 那是從位元組比例外推的,不是量出來的,實際只有四分之一。

量測工具第四個缺陷:欄位名猜錯

這一輪有三個結果被丟掉(27B 的 28000/c4、c8,以及整個 q6head 臂),報成 only 0 content deltas,我一度當成模型壞掉。真相:工具只數 delta.content,而思考 token 走別的欄位。第一次修我改成也數 reasoning_content —— 還是 0,因為這個 fork 送的欄位叫 reasoning,不是 OpenAI 慣用的 reasoning_content。抓原始 SSE 才看到。工具現在兩個都吃,並多一個 think% 欄 —— 上表 q6head 的 c1 是 100% 思考,這種臂跟 3% 思考的臂放在一起比,本來就該標出來。

2026-08-05

每個 token 有 6.02 毫秒不隨卡數縮減 —— 而且不是通訊

拓樸c1 decode/s該拓樸的記憶體屋頂屋頂效率每 token固定成本佔比
TP169.7138.750.3%14.35 ms42%
TP296.2277.334.7%10.40 ms58%
TP4(生產)123.4554.722.2%8.10 ms74%
擬合:T(n) = 6.02 + 8.32/n 毫秒

加卡只把可縮減的 8.32 ms 越切越薄,那 6.02 ms 動都不動 —— 所以屋頂效率一路從 50.3% 掉到 22.2%,到了 TP4 每個 token 有 四分之三 是這個固定成本。

第三個被實測打掉的推論:不是同步屏障

我先前說固定成本是 row-parallel 那 128 道 all-reduce 屏障。錯了:TP1 一次 all-reduce 都沒有,固定成本照樣是 6.02 ms。今天被打掉的三個結構性推論依序是 —— 互連頻寬、LM head 位元組、同步屏障。共同模式是我從架構往下推,而不是先量。

順帶更正數字:先前的 TP1→TP2 +29.5%、TP2→TP4 +31.0% 是把兩組不同量測鏈接算的。同 session 直接量是 +38.0% 與 +28.3%,TP1→TP4 是 +77.0% 不是 +70%。

操作面:不要加卡,改開副本

既然邊際報酬這麼差,同樣四張卡拆成四個獨立副本反而多得多:

配置4 使用者總吞吐16 使用者總吞吐單人速度
1 × TP4(現況)123.4153.4123.4
4 × TP1 副本278.8(2.3x)333.6(2.2x)69.7
代價與前提

單人速度掉 44%(123.4 → 69.7),前面要加一個 router。9B 只有 6.5 GB,一張卡一個副本很寬鬆(util 0.30 之後每卡才 9.5GB)。但啟動必須序列化 —— 四個模型同時載入正是先前疑似燒掉 PSU 的功耗型態,記憶體有餘裕不解除這條紀律。

那 6.02 ms 還沒定位。已知不是頻寬也不是通訊;剩下的嫌疑是 piecewise CUDA graph 在 21 個 splitting_ops(整條 qwen_gdn_* 都在裡面)上被切斷造成的逐層 kernel launch、GDN 本身、以及 248,320 詞表的 logits 與取樣。下一輪量這個。

基礎設施 · 根因2 段
2026-08-06

每次「V100 離線」的根因:它掛在公司 WiFi 上,網路孔沒插線

機器沒事,線路有事

V100 唯一的對外介面是 USB 無線網卡 wlx6c4cbc1c49f1,SSID Luxshare-Mobile(5GHz,WPA-Enterprise 802.1X,RADIUS 是 Cisco ISE)。主機板的有線埠 enp8s0 存在,但狀態是 NO-CARRIER —— 網路線沒插。開機至今三天沒重開過(boot 8/3 13:41)。

指標實測
3 天內重新關聯次數91(約每 48 分鐘一次)
其中硬斷線11
訊號強度−66 dBm
漫遊經過的 AP 數至少 7 個 BSSID
NAT 型態MappingVariesByDestIP: true(硬 NAT,被迫走 DERP 中繼)
08-06 00:53 那次對得上分秒

00:53:43 tailscaled 同時失去 IPv4 與 IPv6 上游:控制面 no route to host、所有 v6 目標 network is unreachable、DERP hkg 連不上、PollNetMap 失敗。00:54:22 wpa_supplicant 記下 CTRL-EVENT-DISCONNECTED reason=7 —— AP 端早就把它踢掉了,主機晚了 40 秒才發現。00:54:29 接上另一台 AP。一台不會移動的桌機之所以一直在漫遊,是因為 −66 dBm 剛好卡在好幾個 cell 的邊緣。

結論與唯一的解

先前我對這次離線給過四個錯誤診斷,其中最糟的一個是拿家用網段的 192.168.1.17 去做 ARP 測試然後宣告「網卡沒供電」—— 那台機器在公司,網段是 172.30.166.0/23,根本不在那個子網。現在有實證了:純粹是網路,而且是 WiFi。

軟體端沒有解 —— 沒有任何設定能讓 802.1X 無線關聯穩定下來。唯一的修法是實體的:插一條網路線到 enp8s0,人要在辦公室才做得到。

順帶一個量測紀律:對這台主機單發一次 ICMP 或 TCP 探測毫無意義,每天約 30 次的重新關聯裡任何一次都會讓它失敗。監視器已改成連續兩次失敗才報,並且打應用層端點而不是 ping。

追查二:能不能同時連兩個 WiFi?不能,硬體就做不到

網卡是 TP-Link Archer T2U PLUS(RTL8821AU,單天線 AC600),驅動 rtl8821au。iw phy 明講 interface combinations are not supported —— 單一 radio、不支援介面組合,沒辦法同時掛兩個 STA 關聯。整台機器只有這一個 phy,沒有第二支網卡也沒有 PCI 無線。要第二條路就必須有第二個 radio:再一支 USB 網卡、一支 4G dongle,或者最省事的 —— 那條沒插的網路線。

但找到一個可能更大的槓桿:它待在 DFS 頻道上

管制區是 TW: DFS-FCC。它現在關聯的 AP 在 5720 MHz(ch 144),而 iw phy 對這個頻率標著 (radar detection) —— 那是 DFS 頻道。DFS 的規定是 AP 一旦偵測到雷達就必須在 10 秒內清空頻道,把所有 client 踢掉。那正好produce「突然、全斷、短暫」的斷線型態。

而且它待的那台訊號只有 59,同一個 SSID 有一台 78:F1:C6:3F:9F:0E 在 5180 MHz(ch 36,非 DFS)、訊號 77。11 次硬斷線裡 8 次是 reason=15(4-way handshake timeout),也就是重新協商金鑰失敗,而不是訊號不足 —— 與 DFS 強制清場後的重連失敗一致。

建議順序,以及我做不到的部分

1. 插網路線到 enp8s0 —— 免費、一勞永逸。
2. 沒線的話,把連線釘在 78:F1:C6:3F:9F:0E(ch 36、非 DFS、訊號 77)。
3. 真的要雙路徑,加第二支 USB 網卡;第二個網路現場有 Luxshare-Office(802.1X,77)可選。

這三件都要人在現場。我連 power_save off 都改不了 —— 那台機器上 sudo 要密碼,我沒有 root。(網卡目前 Power save: on,USB 無線開省電本身也是掉線的常見原因。)

2026-08-06

修法與實測

修法修法前修法後說明
num_anchors 512 → 128跑不到第一個 micro-step穩定推進Q_LEN 3584 → 896,scores 面積少 6.2 倍。硬體逼出的偏離,τ 需註明在 128 anchors 下測得。
expandable_segments49→99 步流失 12.3GB流失 1.7GB治裝置端碎片。有效但不足以收斂。
pin_memory=False步 464 剩 12.3GB 後陣亡步 900 仍有 37.4GB治主機端釘選快取。GB10 主機與裝置共用實體 RAM,釘選換不到 DMA 好處 —— 每步耗時反而從 5.0s 降到 2.53s,全程預估從 19 小時縮到 9.4 小時。
DGX · DSpark2 段
2026-08-09

DSpark 記憶體診斷 · DGX

為什麼四層才到底

前三個假設都被實測推翻,而且它們都很有說服力 —— 留在這裡是為了不再重走。真正的轉折是不再靠推論、改看哪一套帳本對不上:當 cuda_reserved 與程序 RSS 同時持平、系統可用記憶體卻持續下降,這個組合只指向一個地方。

推翻
27B 目標模型全載吃掉 54GB
lowmem 路徑早已只從 safetensors 取出 embed_tokens 與 lm_head。它只在失敗時列印訊息,所以「log 裡沒有訊息」正好代表它有生效。
推翻
103GB 的 target cache 被 mmap 讀爆
輾轉當下 buff/cache 只有 1GB。mmap 的頁面計入 page cache 且可回收,不會逼出 swap。為此建的 1400 樣本子集沒有解決問題,但保留 —— 它讓每輪實驗快很多。
推翻
凍結的 embed / lm_head 仍被配了 Adam 狀態
optim.py 已濾掉 requires_grad=False 的參數,而且用的是 bitsandbytes AdamW8bit。
成立
① 孤兒 CUDA 子程序扣住 59GB
kill 父程序不會殺掉 multiprocessing.spawn 的子程序;它孤兒化後仍持有 CUDA context。GB10 的 GPU 記憶體從系統 RAM 切出,驅動那份不計入 /proc/meminfo 任何類別(所有具名項目加總只有 23GB / 121GB),nvidia-smi 回報 [N/A],該程序 RSS 僅 2.4GB —— ps 和 nvidia-smi 都看不見它。基準線因此從 121GB 悄悄掉到 61GB。
成立
② 注意力 scores 矩陣被實體化
drafter 的注意力是 Q=3584 × KV=7680。sm_121a 拿不到融合核心:flex_attention 不能編譯,換 SDPA 又因自訂遮罩退回 math 後端。兩條路都會實體化整個 fp32 scores 矩陣。
成立
③ 釘選主機記憶體快取無限成長
最後一層,也最隱蔽:cuda_allocated、cuda_reserved 與程序 RSS 三者同時持平,系統可用記憶體卻在 365 步內掉了 9.2GB。釘選區塊不在任何一套帳本裡,而 torch 依尺寸快取且從不釋放 —— 這份資料每個樣本長度都不同,每個新形狀就留下一塊永久快取。
2026-08-08

DSpark drafter 評估

checkpointτ 接受長度verify rate位置 0位置 1位置 2位置 3+auc 整體auc@0
step_210 · 2 層 · 128 anchors · 6.8GB1.560.1950.4910.0450.015<0.0040.5280.861
step_70 · 5 層 · 512 anchors · 9.0GB1.240.1540.1800.0340.013<0.0050.4850.397
輕量 drafter 每一項都贏

τ 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。

尾2 段
2026-08-10

本期紀事

修復
TPFIX2 — 多卡輸出從此正確
此 vLLM fork 的張量並行輸出從來沒對過(9B / 27B 都是亂碼)。逐層 dump 定位到 out_proj 的 rank 切片與各卡持有的 head 集合對不上,修好後 TP2 / TP4 首次產出正確文字。
修復
SPECFIX — CUDA graph capture 缺陷根治
capture 時把 CPU 純量烘進圖裡,導致「快但輸出壞」與「正確但很慢」二選一。改成 verify-as-decode 後兩難消失。
翻案
27B 從判死到可用
單卡 14.5 tok/s(判定不可用)→ TP4 單人 38.4,首次越過 30 的互動門檻。
交付
fable-9b 工具呼叫上線
hermes parser + qwen3 reasoning + 支援 tools 的對話模板。非串流 8/8、串流 7/7 通過;含 tool_calls JSON、結果回灌、think 不外洩。
研究
DSpark 訓練方向修正
比對論文後找出三個方向性錯誤:drafter 容量過大、資料規模不足、confidence 排程未部署。已用輕量 drafter 重啟訓練。
事故
V100「掛掉」三小時 — 但它從未離線
21:35 起完全連不上,一度判定是機器斷電、準備請 Wayne 到辦公室查 PSU。00:54 恢復後查證:uptime 2 天 11 小時<b>從未重開</b>、tailscaled PID 未變、核心無網卡 link 事件、四張 GPU 一路載著模型、生產服務從未中斷。斷的只是它到 Tailscale 控制平面的連線。我在這件事上連錯四次,最嚴重的是拿本機區網的 192.168.1.17 做 ARP 測試當成「網卡未通電」的硬證據 —— 那個 IP 根本不是 V100,是我沒問它在哪就假設了,還讓 Wayne 跟著下錯結論。監視器已改用應用層健康檢查:走 DERP 中繼時 ICMP 與裸 TCP 會假陰性。
解鎖
TP2+PP2 不是不相容,是被自家的除錯訊息擋住
委員會的第二建議是改測 TP2+PP2(TP2→TP4 只有 +19%,all-reduce 已是主導項)。查到 07-17 其實試過並崩潰:'IntermediateTensors' object has no attribute 'shape'。但那是 PP 的正確行為 —— 非最後一個 stage 本來就回傳 IntermediateTensors。崩潰點在 fork 自己加的 _sm70_profile_trace,不在 TurboQuant backend,也不在 vLLM 的 PP 支援(get_pp_group / is_last_rank / IntermediateTensors 處理都完整)。它在任何推論發生前就死了,所以 PP 與 TurboQuant 相不相容從未被驗證到。已修並驗過,等窗口。
治理
生產 vLLM fork 納入版本控管
這個 fork 有 21 個檔案被手工修補過卻完全不在 git,只有零散的 .bak/.pre_ 副本,沒有任何記錄說每個修補改了什麼 —— 生產推論堆疊出事時無人能重建。已建立基準(2444 檔 / 30MB),只納管原始碼:.so 與 .pyc 佔 666MB 中的 579MB 且從不手改。
否定
委員會的頭號優化建議不成立 — 反量化早已融合
建議是「把 2-bit KV 反量化融進 attention kernel,可擠 15-20%」。讀原始碼判定:解碼路徑 _tq_decode_stage1 是 Triton kernel,直接讀打包位元組、在 kernel 內解 2/3-bit 索引、重建 scale/zero、查 centroid、累加到 fp32 —— 從未物化 FP16 的 KV 張量,沒有可融合的東西。物化只發生在分塊 prefill(k_full = torch.empty 後串接餵 SDPA),影響 TTFT 與長 context 首塊,不是穩態每 token 吞吐。省下一個維護窗口。
交付
生產切換 TP4 — 每人 66.6 → 107.5 tok/s
量測鏈在 regen 釋放 GPU 後自動完成 27B TP2 / 9B TP2 量測並切換生產,帶健康檢查與工具呼叫閘門、失敗自動回滾。切換後 8/8 驗收全過(純文字對話、tool_calls JSON、結果回灌、think 不外洩),8 人併發每人仍有 60.3、合計 391.4。
診斷
DSpark 訓練記憶體 — 四層才到底
訓練一啟動就把 121GB 吃光並開始輾轉。前三個歸因(27B 全載 / mmap cache / Adam 狀態)都被實測推翻。真正的原因有三層:孤兒 CUDA 子程序扣住 59GB、注意力 scores 矩陣被實體化、釘選主機快取無限成長。修好後記憶體打平,每步耗時反而從 5.0s 降到 2.53s。
研究
推測解碼 sweep 啟動
單卡旋鈕已測到盡頭(全在 ±3% 雜訊內),TP4 又被 regen 擋著,而 GPU3 整晚閒置。改測三種免 draft 模型的推測方法,針對工具呼叫這種高重複輸出。
事故
PSU 陣亡並更換
多模型同時載入的主板軌尖峰疑似壓垮電供,停機 15 小時。已改為序列載入紀律;四卡與載板無損。
2026-08-10

下一步

  • WayneHermes gateway 端對端測試,確認後啟用工具呼叫
  • agentDSpark 訓練完成 → 以 accepted length 對比舊 checkpoint,裁決容量修正是否正確
  • agent維護窗口:改測 TP2+PP2 拓樸。TP2→TP4 只有 +19%,all-reduce 已是主導項,而單 token 解碼下 PP 的點對點延遲通常低於 TP 的 all-reduce
  • agent推測解碼:工具已重建(spec_isolate2.sh),等一張空卡即可跑,並看實際回應內容
  • agent27B TP4 補量(需先讓出 GPU0),確認 38.4 tok/s 是否仍成立
  • agentregen 8931 筆已收齊 → 資料擴量重訓
自動產生 · 自有硬體推論與訓練研究狀態2026-08-10_1154