Agent Telemetry

V100 推論叢集 · DGX 訓練研究
快照2026-10-02_0254UTC+8 · 每 3 小時更新
MTP · 推測解碼12 段
2026-08-13

對照組:遞迴不是驗證變貴的原因

配置

Muse Glimmer 30B kquant-dynamic(19.7 GB)· llama.cpp 8e7f22b(自建,arch 70,gcc 12.4)· 單卡 V100-SXM2-32GB · -ngl 99 -fa on · KV f16 · -c 8192 · /v1/chat/completions · temp 0 · max_tokens 300 · 三趟取中位數。對照的 Qwen3.6-27B 數字出自 /opt/llama.cpp(未動過的參照樹)

臂tok/s草稿接受接受率每步 tokenc
base(不開推測)32.63———1.000—
DFlash 預設36.5542815536.2%2.0690.847
DFlash n-max 831.5590118420.4%——
DFlash n-max 1631.051,56519112.2%——
把成本除以「每步多驗幾格」遞迴 block多驗幾格c每格的 c
Qwen3.6-27B · llama.cpp 單卡 · MTP48 / 6510.2980.298
Muse Glimmer 30B · llama.cpp 單卡 · DFlash0 / 522.950.8470.287
Qwen3.6-27B · vLLM TP4 · MTP(最佳旗標)48 / 6511.5951.595
0.298 對 0.287

同一個引擎、同一張卡,一顆 65 層裡 48 個帶 GDN 遞迴狀態、一顆 52 層全注意力零遞迴,每多驗一格的邊際成本差 4%。遞迴狀態不是驗證變貴的原因。

我今晚一直把「48/65 帶遞迴」當成 c=2.01 的根因,寫進報告、也寫進給 fusion 委員會的知識庫 —— 委員會回來的三個候選全部建立在這個錯誤前提上,包括「qlen=2 對遞迴層是時間軸掃描」那個看起來最合理的。對照組花了兩小時,殺掉的是一整晚的推論方向。

差別在引擎:vLLM 每多驗一格要 1.595 個基準步,llama.cpp 兩顆模型都是 0.29。這讓 spec_sequence_masks is None 那個守衛從候選之一變成主嫌 —— 它是一條純引擎側、與模型架構無關的路徑選擇。

限制要講清楚:llama.cpp 兩顆都是單卡,vLLM 是 TP4,那一列同時跨了引擎和拓樸,不能當成純引擎差異的定量證據。但 llama.cpp 內部那兩列是同引擎、同卡數、兩種極端不同的架構,那組就足以殺掉遞迴假設。

2026-08-12

KV 精度掃描:接受率一動也不動

配置

27B Fable-Fus-711…MTP-IQ4_XS · vLLM 1.2.1 SM70 · TP4(V100-SXM2-32GB ×4,NVLink NV2 全網)· max-model-len 65536 · util 0.50 · block 32 · batched 16384 · seqs 8 · prefix caching 開 · MTP draft=1 · capture [2,4,8,16] · IQ4XS batched-MMVQ 與 tile-MMQ 兩顆都開 · 六臂同一 session、只換 --kv-cache-dtype

KVKV 池c1 解碼c1 接受率c4 解碼c4 接受率
2-bit · 不開 MTP3,191,98859.25—48.52—
turboquant 2-bit2,513,30533.8051.5%24.8550.3%
turboquant 3-bit2,010,64433.4150.0%26.4961.5%
turboquant 4-bit1,615,14533.4752.3%27.1062.4%
K 8-bit · V 4-bit1,230,79834.7351.5%27.9561.9%
f16(完全不壓)546,61333.7151.5%27.7761.8%
這一欄就是結論

單人接受率那一欄從 2-bit 到 f16 是 51.5 / 50.0 / 52.3 / 51.5 / 51.5%。壓到 2 位元和完全不壓,同一個數字。所以 llama.cpp 那邊的 72.4% 不是 KV 換來的,我先前把它寫成因果是錯的。

四併發那一欄確實會動(50.3% → 62.4%,3-bit 之後打平),但吞吐只從 24.85 走到 27.95 —— α 動了、S 沒動,正是 c 綁住時公式預測的樣子。這一輪反而是 c 是主因的正面證據。

f16 的代價很實在:KV 池只剩 546,613 token(2-bit 的 17%),四併發 TTFT 從 1.25 s 掉到 5.11 s。工具閘門六臂全 ALL_PASS。

這一輪的基準臂不能用。我把 capture 設成 [2,4,8,16] 套給所有臂,不開 MTP 時 batch=1 沒被捕捉、掉出 CUDA graph,所以 59.25 比同組態該有的 69.10 低。各 MTP 臂之間仍可比,但那個 59.25 不是基準。

2026-08-12

八個旗標消融:只有兩個動得了 c

配置

27B IQ4_XS · vLLM 1.2.1 SM70 · TP4 · KV turboquant_2bit_nc · max-model-len 65536 · util 0.50 · block 32 · seqs 8 · capture [2,4,8,16](無推測臂用 [1,2,4,8])· 兩顆 IQ4_XS kernel 都開 · NOSYNC=1 · MTP draft=1 · 八臂同一 session,c 用本 session 自己的無推測臂算,不從記憶拿

臂c1 解碼接受率推測步 mscTTFT
不開推測68.88—14.52—0.43s
MTP 原樣34.2951.5%44.182.0431.46s
FULL_FORWARD34.0350.0%44.082.0362.86s
SPEC_CORE_OP39.6249.3%37.681.5954.42s
003_SPEC_CORE_OP38.4944.9%37.651.5932.88s
PACKED_RECURRENT_DECODE34.7251.5%43.632.0051.36s
GDN_DECODE_FLASHQLA34.3751.5%44.082.0360.77s
FUSED_SIGMOID_MIXED_QKV34.5049.3%43.281.9813.54s
疊加c1 解碼接受率cTTFT
FLASH_GRAPH 單獨34.5551.5%2.0201.55s
FLASH_GRAPH + SPEC_CORE_OP39.9652.3%1.6250.68s
SPEC_CORE_OP + FUSED_SIGMOID38.7045.6%1.613.61s
SPEC_CORE_OP + PACKED_DECODE39.8152.3%1.6351.29s
兩個贏家落在同一個 c

SPEC_CORE_OP 與 003_SPEC_CORE_OP 的 c 是 1.595 和 1.593 —— 幾乎同值,所以兩條路徑應該接到同一個底層改動。前者吞吐較高純粹是接受率沒被拖低(49.3% vs 44.9%)。其餘五個旗標全落在 c≈2.0,是雜訊。

最好的 MTP 現在是 39.62,對基準 68.88 仍是 0.575 倍。損益平衡要 c < α = 0.493,從 1.595 還差 3.2 倍。這一刀把 29.2 ms 的額外成本壓到 23.2 ms,離 llama.cpp 的 7.6 ms 還有 15.6 ms。

注意 SPEC_CORE_OP 單獨用時 TTFT 是 4.42s,基準的十倍 —— 解碼買到的 15.5% 是拿 prefill 換的。疊上 FLASH_GRAPH 就沒事了(見上表:0.68s,接受率還回到 52.3%),所以這個代價不必付。

2026-08-12

兩台引擎的完整帳:公式對兩邊都準

配置

同一顆 GGUF Qwen3.6-27B-Fable-Fus-711…MTP-IQ4_XS · 同一台 V100-SXM2-32GB。llama.cpp = 上游 b0.15.3、單卡、KV f16、-fa on · -c 8192 · --spec-type draft-mtp · p-min 0(完全不篩)· temp 0 seed 1 · 200 tok。vLLM = 1.2.1 SM70、TP4、KV turboquant_2bit_nc、max-model-len 65536 · util 0.50 · seqs 8 · 兩顆 IQ4_XS kernel 都開 · NOSYNC=1。兩邊都是 γ=1

引擎基準 tok/s步 msαcS 預測S 實測MTP tok/s
llama.cpp · 單卡39.1625.530.7220.2981.3271.32751.97
vLLM · TP468.8614.520.5152.0100.5030.50334.64
兩邊各贏一半,而且贏的不是同一半

S = (1+α)/(1+c) 對兩台引擎、四個獨立量到的數字各自命中到三位數。損益平衡條件是 α > c:llama.cpp 是 0.722 > 0.298 所以賺,vLLM 是 0.515 < 2.010 所以虧一半。而基準反過來 —— vLLM 的 68.86 是 llama.cpp 39.16 的 1.76 倍,因為它的 all-reduce 在 CUDA graph 裡,而 llama.cpp 的多卡是逐 seam 同步。

把 llama.cpp 的優點搬過來αcStok/s(基準 68.86)
現況0.5152.0100.50334.64
只修 α0.7222.0100.57239.4 · 仍然虧
只修 c0.5150.2981.16780.4
兩個都修0.7220.2981.32791.4
順序被算式定下來了

c 是壓倒性的槓桿。只修接受率、把 α 拉到 llama.cpp 的水準,在 c=2.01 之下只有 39.4 —— 連基準的一半都不到,還是虧的。反過來只修 c、接受率原封不動,就有 80.4。我原本準備先追「草稿頭被餵錯」那條線,這個算式把順序倒過來了。

c=2.01 表示每個推測步多花 29.2 ms,而草稿頭是 65 層裡的 1 層;llama.cpp 做同一件事只花 7.6 ms。那 21.6 ms 的差距不可能是算力。

2026-08-12

拿掉合約裡的 nonzero():MTP 單人 +6.8%

配置

27B IQ4_XS · vLLM 1.2.1 SM70 · TP4 · KV turboquant_2bit_nc · max-model-len 65536 · util 0.50 · block 32 · seqs 8 · 兩顆 kernel 都開 · 三臂同一 session,MTP 兩臂只差一個環境變數

臂c1 解碼c4 解碼接受率工具閘門
不開推測(capture 含 1)68.8648.59—ALL_PASS
MTP draft=1 · NOSYNC=032.4224.4551.5%ALL_PASS
MTP draft=1 · NOSYNC=134.6425.0551.5%ALL_PASS
真的省下來了,但離門檻還很遠

build_gdn_spec_decode_state_contract 用布林遮罩挑出推測那幾列。每個 x[mask] 編出來都是 nonzero() 加 index(),而 nonzero() 必須把命中數讀回 host 才能決定輸出張量多大 —— 一次完整的裝置同步,一次 forward 好幾個。批次同質時(單人必然,多人只要沒有 prefill 插隊也是)遮罩全 True,x[mask] 就只是複製一份,x[~mask] 是空的,兩者都不需要 nonzero。而判斷同質與否的計數呼叫端已經在 CPU 上算好了,問它零成本。

基準步 14.52 ms,MTP 步從 46.73 掉到 43.74 ms → c 從 2.22 降到 2.01。損益平衡要 c < α = 0.52,還差四倍。接受率完全沒動(51.5%),證明這是純步成本的改動,沒有動到數值。

但這一輪最重要的不是那 6.8%。草稿頭是 65 層裡的 1 層,它多做的那次 forward 照理該花基準步的 1/65。實測花掉 2 個基準步 —— 差 130 倍。這個量級的落差不可能是算力,是管線。同步只佔其中 0.21。

2026-08-12

草稿長度掃描:第一個位置就只有五成

配置

同上一節,只換 num_speculative_tokens;capture 集合跟著改成 (1+γ) 的倍數(γ=1 [2,4,8,16] / γ=2 [3,6,12,24] / γ=3 [4,8,16,32]),否則會被靜默丟掉

γ草稿接受pos0pos1 條件pos2 條件總接受率c1 解碼
173036850.4%——50.4%33.21
21,37441044.8%33.1%—29.8%30.81
32,03441943.5%30.5%37.8%20.6%27.86
對照組(外部,同架構)97%95%91%——
γ 不是那個開關

position 0 的接受率 不隨 γ 改變(50.4 / 44.8 / 43.5%),所以「草稿開太短」不成立。加長只會把後面幾個爛位置算進分母,總接受率一路掉到 20.6%,吞吐跟著掉 16%。γ=1 已經是這台機器上最好的設定。

對照組的 97 / 95 / 91% 和我們的 50 / 33 / 38% 差一個量級,這不是調參數能補的距離。而 llama.cpp 在同一顆權重上報 72.4%,它跑的是 --spec-draft-p-min 0.5 —— 信心不足就不提草稿,那些位置不進分母。把 p-min 降到 0 就能分辨那 72.4% 是更好的草稿頭還是篩過的分母。

推測解碼為什麼還是虧 · 步成本拆解

配置

27B · TP4 · KV turboquant_3bit_nc · max-model-len 32768 · util 0.75 · block 32 · batched 16384 · seqs 8 · --spec-method mtp · c1 · 每個臂都在同一 session 內量,基準也在同一 session 重測

臂解碼接受長度步時間對基準
不開推測58.13—17.2 ms—
spec3 · Q4_K_S21.903.44156.9 ms9.1x
spec3 · Q4_K_S · SPEC_CORE_OP=123.863.49146.2 ms8.5x
spec3 · Q4_K_S · 003_SPEC_CORE_OP=123.553.45146.4 ms8.5x
spec3 · IQ4_XS18.313.61197.4 ms11.5x
spec3 · IQ4_XS · SPEC_DECODE_PIECEWISE=15.003.49698.2 ms40.6x
spec3 · Q4_K_S · TP112.053.49289.5 ms16.8x
接受率已經修好,虧的是機制

接受長度 3.44–3.61,和對照組的 3.4–3.8 同一區間。一個推測步該換到 3.5 個 token,結果步成本是 8.5–11.5 倍,所以淨值是虧的。接受率和步成本是兩件事,現在乾淨分開了。

設定層面全部試過,沒有一個能修:PIECEWISE 讓它慢 3.5 倍、TP1 慢一倍(所以屏障不是問題,平行反而在幫忙)、兩個 spec-core 旗標各買 7%、換掉 i-quant 買 21%。

kernel不開推測開推測比
elementwise_kernel<128,4,…>9641,292430x
_scatter_gather_elementwise_kernel08,160全新
fused_sigmoid_gating_delta_rule_update(GDN 本體)28,7048,0640.28x
它是被啟動開銷淹死的,不是被計算量

不開推測跑 300 個 token(約 300 步),開推測只跑 84 個(約 23 步)。換算成每步:0.32 次對 1,795 次,多五千倍。而 GDN 本體的 kernel 反而更少(0.28x)—— 所以問題從來不在遞迴層的計算,在把 token 拆來拆去的搬運。

程式碼裡有一條免拆分的快路,但條件是:

ddtree_tree_gdn_pure_spec = ddtree_requires_branch and num_prefills == 0 and num_decodes == 0

它綁在樹狀推測上,而 MTP 是線性鏈,所以永遠進不去。單一請求開推測時 non-spec 那半根本是空的,卻還是照拆、照跑一遍空張量。

四個被推翻的機制,以及一次無效的量測

這條線上我給過的機制:遞迴狀態頻寬(算出來差三個數量級)、GDN 對序列迴圈(profiler 顯示啟動次數 1.02x)、ticket18 中等批次 kernel(開關對照,關掉還略好)、PRE_CONV_DIAG 逐層同步(被 _diag_preconv_done 擋住,每層只印一次)。四個全錯,四次都是實測推翻的。

還有一次是量測本身無效:index_select 歸因跑了兩個臂、拿到 454 對 448 看似「沒差」,但 request done 沒印出來 —— 伺服器還在載入就被我的迴圈誤判成死掉,那些計數是模型載入與 graph 捕捉的,不是解碼的。誤判來自拿 setsid 之後立刻退出的 PID 當存活判據。已改成看 pgrep,重跑中。

fp16 真的比 Q4 快嗎 · 交叉點實測

配置

權重 17408 × 5120(此模型的 ffn 形狀)· V100-SXM2-32GB · cuda event 計時 · 30 次取平均 · fp16 那欄包含它必須付的解量化成本

列數 M量化 kernel解量化 + fp16贏家
10.074 ms0.651 ms量化 8.8x
80.177 ms0.656 ms量化 3.7x
160.328 ms0.664 ms量化 2.0x
320.647 ms0.686 ms量化 1.1x
641.286 ms0.790 msfp16 1.6x
1282.584 ms1.020 msfp16 2.5x
2565.153 ms1.068 msfp16 4.8x
102420.612 ms2.304 msfp16 8.9x
204841.325 ms4.368 msfp16 9.5x
交叉點在 32–64 之間

質疑是「fp16 比 Q4 快不合理」。在解碼那一端完全正確 —— M=1 時量化快 8.8 倍。「fp16 更快」只在 M 超過約 48 之後才成立,而那是 prefill 的區間。

從算術強度推的交叉點是 M ≈ 35(V100 的機器平衡點約 139 FLOP/byte,Q4 在 M 列的強度是 4M),實測落在同一格。贏的不是位元組,是算力等級:量化 kernel 走 dp4a 用不到 tensor core,fp16 GEMM 走 HMMA。

可用的發現:DEQUANT_CUBLAS_MIN_ROWS 目前是 256,但 64–255 這一段 fp16 已經快 1.6–4.8 倍,門檻設得保守。

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
2026-08-12

推測解碼把 capture 集合的需求乘上了 (1+draft),沒人檢查捕捉張數

症狀:16 到 24 併發之間掉 2.9 倍

27B IQ4_XS 開 MTP draft=1,每人 decode 從 c16 的 12.22 tok/s 掉到 c24 的 4.28,步時間 81.8 → 233.5 ms,而 batch 只加倍。而且 c24 與 c32 是 233.5 / 234.1 —— 幾乎同值。兩個不同 batch 花一樣的時間,通常代表它們走的是同一條路,而那條路是 eager。

一個推測步驟送出的是 席次 ×(1+draft) 個 token,vLLM 只保留能被那個倍數整除的 capture 尺寸。同一組 [1,2,4,8,16,32] 送進去,兩臂捕捉到的張數不同 —— 這是唯一的破綻,而它就印在 server log 裡:

組態要求的 capture實際捕捉可用席次上限c24 每人
不開 MTP[1,2,4,8,16,32]6 張32—
MTP draft=1[1,2,4,8,16,32]5 張164.28
MTP draft=2[1,2,4,8,16,32]0 張—連啟動都沒過
MTP draft=1[1,2,4,8,16,32,48,64]7 張328.17

draft=1 丟掉的是尺寸 1(不能被 2 整除),剩下 2,4,8,16,32 個 token,對應 1,2,4,8,16 個席次。draft=2 要 3 的倍數,那組裡一個都沒有。

三臂同場對照 —— 兩個懷疑都排除了

第一次量到改善時,同一輪 c8 也從 17.97 掉到 9.82、工具呼叫閘門從 ALL_PASS 變成 FAILED。但那兩組數字跨了 session,而本機的規則是只有同 session 的比值算數。所以三臂重跑,只差 capture 集合:

併發old [1,2,4,8,16,32]pow2 […,32,64]full […,32,48,64]
c130.2430.3830.47
c89.719.899.89
c169.589.589.56
c243.767.878.16
c323.667.597.59
toolcall ×2兩次都 FAILED兩次都 FAILED兩次都 FAILED

c8 三臂一樣,所以那個「退步」不存在 —— 17.97 在同 session 裡重現不出來。工具呼叫閘門連舊集合都失敗,所以也不是這次改動造成的。capture 對齊因此是乾淨的淨勝:c24 2.17 倍、c32 2.07 倍,小併發完全不動。含 48 比只含 2 的冪次在 c24 多 4%,沒有代價。

代價是知道了另一件事

同一組態、同樣的舊 capture,在兩個 session 之間 c8 是 17.97 對 9.71 —— 差兩倍,原因未知。這不是這次改動的問題,但它是所有併發數字的背景雜訊:跨 session 的比較在這台機器上不成立,要下結論的對照一律同場跑完。

2026-08-12

推測解碼的損益平衡是 α > c,而我們的 c 是 2.04

論文給的是閉式解,不用猜

Leviathan、Kalman、Matias(Google Research,ICML 2023)的牆鐘加速:

S = (1 − αγ+1) / ((1 − α)(γc + 1))

α = 接受率,γ = 草稿長度,c = 草稿成本 ÷ 目標模型成本。Corollary 3.9 直接給出損益平衡條件:α > c。

論文 Table 2我們
目標模型T5-XXL 11B,單張 TPU-v4,batch 127B IQ4_XS,TP4 V100
α 範圍0.53 – 0.820.39 – 0.53
c 範圍0.007 – 0.0732.04
加速1.4x – 3.4x0.49x

論文自己的數據就在證明 α 不是槓桿:Table 2 裡接受率最高的一列 —— T5-large,α = 0.82 —— 加速卻是最差的 1.7x,因為它的 c 最大(800M/11B)。而 α 只有 0.75 的 T5-small 拿到 3.4x。

我們的 c 不是草稿頭的成本

DeepSeek-V3 報告說 MTP 模組是一層 transformer,重用主幹最後一層的 hidden state、共用 embedding 與 output head,所以它的固有 c ≈ 1/65 ≈ 0.015 —— 正好落在論文的範圍內。

我們的 2.04 來自論文 Section 3.3 的前提被違反。原文是:「we'll assume that we can run γ+1 concurrent evaluations of Mp in parallel without increasing the walltime」。在我們機器上 M=2 的前向要 1.85 倍,那個假設是假的,而差額就是我們的 c。

再往下一層:M=1 時矩陣乘只佔 8.4 ms,非矩陣乘佔 9.0 ms(52%),一次前向發 2,297 顆 kernel。一個 27B 的 decode 該有九成是讀權重,我們只有四成八 —— 所以第二個 token 位置幾乎要付全價。這同時解釋了單人只有 69 tok/s(頻寬的 27%)和 MTP 必賠,是同一個病灶。

病因:這顆模型有 48 層帶遞迴狀態

推測解碼的整個前提是「多驗一個位置幾乎免費」。對 attention 成立 —— 多寫一格 KV,被拒絕就丟掉,零成本。對 linear attention / GDN 不成立:遞迴層沒有「丟掉一格」這種操作,它只保留最後一個 token 的狀態。

vLLM 自己的原話

gpu_model_runner._update_states_after_model_execute:

"This is used for MTP/EAGLE for hybrid models, as in linear attention, only the last token's state is kept. In MTP/EAGLE, for draft tokens the state are kept until we decide how many tokens are accepted ... and a shifting is done during the next iteration based on the number of accepted tokens."

而那組緩衝區只在 speculative_config is not None and model_config.is_hybrid 時才配置 —— 純 transformer 開 MTP 完全不會走到這條路。

這顆 27B 的 block數量
帶 SSM 遞迴狀態(ssm_a/alpha/beta/conv1d/dt/norm/out)48
純 attention(attn_q/k/v/output)17
合計65(64 層 + 1 個 MTP 層)
先前我在這裡數錯過一次

第一次我用 startswith("attn_q") 判斷注意力層,結果把 GDN block 的 attn_qkv(那是 GDN 的輸入投影)也算進去,得到「48 對 65、42%」這種加起來超過總 block 數的結果。正確是 48 對 17。而且本機七顆 27B 變形 —— 含 Qwen3.6-27B-OFFICIAL-MTP-IQ4_XS —— metadata 完全一致:qwen35、65 blocks、48 個 SSM、ssm.state_size 128。Qwen3.6-27B 本來就是混合架構。

trace 的指紋完全對上。以下 kernel 在不開 MTP 時前 25 名根本看不到,開了之後全部冒出來 —— 而且它們是搬資料的,不是算數的:

kernelnospecmtp做什麼
memcpy32_post—1879 次 / 3.58 ms狀態搬移
_scatter_gather_elementwise—268依接受數散射
indexSelectSmallIndex—268選出要保留的 slot
CatArrayBatchedCopy—268狀態串接
但回捲稅不是天花板 —— 這點我先前搞錯

我一度寫成「混合架構所以 MTP 必輸」。一份獨立實測在同一個架構的 Qwen3.6-27B 上,MTP γ=3 拿到 2.40 倍。而那篇量到的回捲稅是把 c 從 0.20 推到 0.30 —— 足以把 1.17x 變成 0.96x,但離我們的 2.04 差了近七倍。

所以回捲稅解釋得了「為什麼混合架構比純 dense 吃虧」,解釋不了「為什麼我們比別人的混合架構還差七倍」。那一段還沒找到,而它才是我們真正該修的東西。

他們算 c 的方法也比我好:用每步讀多少 GB —— 27B Q8 的 verifier 讀約 27 GB、MTP 頭讀約 0.43 GB → c ≈ 0.016,而不是像我從 TPS 反解。

三個被實測殺掉的假設

草稿頭精度:eh_proj 是 Q8_0 的那顆模型接受率更低(39.4% 對 4-bit 的 47.8%),S 兩者都是 0.45–0.49。本機 skill 裡「MTP 配方必須 gate eh_proj → q8_0」在這顆模型上不成立。

drafter 的 CUDA graph:把它完全關掉只差 0.5%(30.48 → 30.32)。往差的方向走不花錢,往好的方向自然也賺不到。

launch-bound:GPU 其實忙到 72–76%,eager launch 只有 835 次 / 9.19 ms。我先前算出「62–70% 閒置」是拿 5 個 step 的視窗去除以 16 個 token,分母錯了。

而且就算三者全修好也沒用:c = 2.04 之下,完美草稿(α→1、γ=1)也只有 2 / 3.04 = 0.66x。

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 世代。

自動產生 · 自有硬體推論與訓練研究狀態歷史快照 581 份 →2026-10-02_0254