~/blog/qwen4exp-177b-three-2080ti

改裝 2080 Ti 22G · part 17

[Benchmark] 1770 億參數塞進三張二手 2080 Ti:改檔案快 3.5 倍,寫散文一點都沒快

cat --toc

TL;DR

三張改裝 2080 Ti(66 GB VRAM)跑 Qwen3.8-Flash-Next,176.94B 全部塞得下,32K 那格 48 層零卸載。128K context 下用 46 層、decode 23.14 tok/s;開 --spec-type ngram-mod 之後,改檔案類的工作衝到 77.56(3.51 倍,輸出位元完全不變),寫散文則是 +0.17%,等於沒有。原生 262,144 context 真的載得起來、255,125 token 的 needle 三針全過,但首輪 prefill 要等 66.68 分鐘。

前言

搬家的時候最重的那幾箱,常常是你最少打開的。

這一顆 Qwen3.8-Flash-Next 就是那種行李:官方標 1770 億參數,我手上的 GGUF 載入時印 176.94B——而其中 51B 是一張查表,每個 token 只翻一次,翻完就闔上。

Part 3 那時候我在一張卡上跟 27B 的 KV cache 打架,Part 14 開始用兩張卡跑 tensor parallel。這篇換三張卡、換一顆大一個量級的模型,問題也換了:它塞得下嗎,塞下去之後日常該怎麼設。

176.94B 塞進 66 GB,因為其中 51B 是查表不是矩陣

VRAM 只有 66 GB,權重印出來 176.94B,中間差了兩倍多。這個差不是靠壓縮補的,是靠架構。

GGUF 的表頭直接寫著,不用猜:

qwen4exp.block_count        = 48
qwen4exp.expert_count       = 512     expert_used_count = 10
qwen4exp.context_length     = 262144
qwen4exp.ple.layers         = 1
qwen4exp.ple.ngram_size     = 3       heads_per_ngram = 8

兩件事讓它縮下來。第一件是稀疏:512 顆專家,每個 token 只點名 10 顆 routed 加 1 顆 shared,真正參與運算的大約 6B。這個大家比較熟。

第二件比較少見。那 51B 是一張 n-gram 查表(模型會把「連續兩三個 token 的組合」直接記成一筆,用的時候查出來就好,不用每次重新算)。重點是它不是一張要整片拿去相乘的權重矩陣:每個 token 查出來的是稀疏的幾列,那幾列後續還是會被投影、加權,再混回主幹,但吃到運算的是那幾列,不是那 51B。而 ple.layers = 1 那行是關鍵:48 層裡只有一層掛著這張表,所以「每個 token 只查一次」不是修辭。

48 層的組成與 87.24 GiB 的分配:12 層全注意力(每 4 層一個)、36 層線性注意力、第 1 層掛 PLE 查表;磁碟上 MoE 專家佔 55.43 GiB(63.5%)、PLE n-gram 查表 26.82 GiB(30.7%),其餘 4.99 GiB;每個 token 只點名 11 顆專家、查表各 head 取一列,合計約 6B

順帶把三個到處流傳的參數量講清楚。官方 model card 寫的是「125B with 6B activated, plus 51B n-gram embedding and 4B MTP」,加起來 180B;我的 GGUF 印 176.94B,少的 4B 是轉檔時被丟掉的 MTP 頭(我掃過 1224 個張量,nextn 相關的是 0 個)。三個數字都對,只是在數不同的東西。

直接抄這組:128K context、23.14 tok/s

如果只是要直接拿來跑,我會用這組:

llama-server -m Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf \
  --split-mode layer -np 1 --no-mmap -fa on -t 24 \
  -ngl 46 -c 131072 -ctk q8_0 -ctv q8_0 \
  --spec-type ngram-mod

三個旗標值得單獨講:

不需要 -ot 我整個過程都以為必須把專家層趕去 CPU,實測三個操作點全部零 tensor override。32K 那格甚至 48 層全上 VRAM。

-np 1 要寫死。 預設會開 4 個 slot,prefill 直接掉一半。

KV 用 q8_0。 f16 在 128K 只能撐到 -ngl 45(22.66 tok/s),換成 q8_0 省下的量剛好夠多塞一層上去,變成 -ngl 46、23.14 tok/s。換 q8_0 不只是讓 VRAM 數字變小,而是真的能多放一層進去。

改檔案快 3.5 倍,寫散文一點都沒快

--spec-type ngram-mod 是這次最有效的加速手段,但不是每種工作都吃得到。同一台機器、同一份權重、同一組旗標,只換工作內容:

工作關投機開 ngram-mod倍率
寫散文(冷)22.9823.02+0.17%
改檔案(冷)22.0977.563.51 倍

這表看第二列:同一顆模型,換個工作就差 3.5 倍。

機制很直白。ngram 投機解碼(不用另外養一顆小模型,直接從 context 裡的重複片段猜下一段)是從你貼進去的東西裡草擬候選。你丟一份 130 行的 Python 進去、要它把一個 class 改名,輸出幾乎全都已經在輸入裡,它一次可以草擬幾十個 token 讓大模型驗證,草稿接受率量到 90.3%。你叫它寫散文,沒有東西可抄,每個 token 都得老實生成。

改檔案時 ngram 投機從 22.09 衝到 77.56 tok/s、3.51 倍、草稿接受率 90.3%,因為輸出幾乎全在輸入裡;寫散文從 22.98 到 23.02、只有 +0.17%,因為沒有東西可抄、連 draft 都沒產生

這不會犧牲品質。 投機解碼不會改變最後的輸出——大模型會逐一驗證草擬的 token,不對就丟掉。我把三次的輸出做 SHA256 比對,完全相同,兩份 diff 都是空的。

128K 是甜蜜點,262K 是懸崖

轉折點和懸崖只隔一格:

32K → 128K    decode 26.16 → 23.14    −11.5%   換到 4 倍 context
128K → 262K   decode 23.14 →  4.05    −82.5%   只換到 2 倍

三個操作點的 decode 速度長條圖:32K 在 -ngl 48 配 f16 KV 是 26.16 tok/s、128K 在 -ngl 46 配 q8 KV 是 23.14、262K 在 -ngl 40 是 4.05;32K 到 128K 只掉 11.5% 換到四倍 context,128K 到 262K 掉 82.5% 只換到兩倍

前半段只掉一成速度就換到四倍 context,很划算;後半段完全不划算。而且 262K 那格的首輪 prefill 要 66.68 分鐘——那不是慢,是不能互動。

那格還是值得跑一次,因為它證明了一件事:官方標示的原生 262,144 context,三張二手卡真的載得起來。 我塞了 255,125 個 token 進去,把三個各不相同的獨特事實埋在大約 10%、50%、90% 的深度,三針全部答對。

⚠️ 但「載得進去」不等於「跑得完」。-ngl 45-ub 512 可以正常啟動、正常回應,然後在餵到第 83,968 個 token 時,CUDA1 還要再分配 1245.72 MiB,接著 OOM、segfault。這個坑 Part 3 在 27B 上踩過一次,換一顆完全不同架構的模型,它又長回來了。

三個量化怎麼挑:更小的那個不會比較快

同一顆 binary、同一組旗標、同樣 8,121 token 的長 prompt,三個量化並排:

量化檔案最大 ngldecode最大可用 context
UD-IQ1_S68 G4826.74未確認
UD-IQ4_XS88 G4825.56262,144(needle 3/3)
UD-Q4_K_XL104 G3616.02131,072(needle 3/3)

這表看 decode 那一欄:IQ1_S 比 IQ4_XS 只快 4.6%。

1-bit 雖然少用了 20 GB,速度卻沒快多少,因為這兩個量化本來都塞得進 66 GB,48 層也都放得上 VRAM。Part 12 拆過這件事的底層原因——這張卡上量化省的是 VRAM 不是時間——換到 177B 的量級還是一樣成立。

真正掉速的是 Q4_K_XL:104 G 塞不進 66 GB,只能上 36 層,十二層掉到 CPU 上算,decode 剩 16.02。它的 context 上限也只驗到 131,072。

至於聰不聰明,我另外設計了七題想分辨低位元的退化。結果三個量化全部 6/7,一題也分不出來——細節在下面第二幕。所以我選 4-bit,看的是速度、context 上限跟 unsloth 公布的 KLD 曲線,不是因為我實測出它比較聰明


進階:量測紀錄

不讀這節不影響使用,上面那組指令抄走就能跑。這節是十輪量測裡三個轉折,還有完整數據。

我以為要把專家層趕去 CPU

遇到什麼問題:176.94B 的權重、66 GB 的 VRAM,第一直覺是一定塞不下,得靠 -ot 把 MoE 的專家層丟去 CPU。

原本打算怎麼解:我從第一發指令就掛著 -ot,而且整晚都在調要卸幾層,-ngl 一路壓在 45 附近。心裡的模型是「熱權重塞不進去,所以要挑一部分放 CPU」。

所以做了什麼:後來排取捨曲線時,我讓它從 -ngl 48 往下掃,想找每個 context 下的上限。

結果:32K 那格 -ngl 48 直接過了,三卡合計 62,720 MiB,decode 26.16。48 層是這顆模型的全部層數,也就是零卸載。三個操作點回頭再驗,全部都不需要 -ot

回頭看哪裡想錯了:我那個「塞不下」的直覺算的是 176.94B,但真正要進 VRAM 的是熱權重——51B 的查表本來就會落到 CPU(那張表是 IQ4_NL,CUDA host buffer 不收),而它佔了將近三分之一。我把「模型有多大」跟「要放進 VRAM 的有多大」當成同一件事,前面幾輪的 -ngl 45 全是白白把三層留在 CPU 上。

176.94B 分成兩種去處:51B 的 n-gram 查表每個 token 只查出稀疏幾列、自動落到 CPU 佔 27,466 MiB;約 125B 的專家權重進三張卡的 VRAM,48 層熱權重共 18,692 + 18,686 + 18,369 MiB

順帶一提,我後來還測了「顯式寫 -ot per_layer_token_embd=CPU」跟「靠自動放置」的差別:兩邊的 buffer 行逐字相同,CPU 27465.95 MiB、三張卡 18692.63 / 18686.69 / 18369.99。那張表本來就會自己下去。

能力題那張表,第一版整張是假的

遇到什麼問題:前三題(解釋投機解碼、多步年齡推理、寫一個 merge_intervals)三個量化都過,分不出高下。1-bit 的退化應該要看得見才對。

原本打算怎麼解:加四題難的——多步算術配精確輸出格式、三重指令限制、40 到 60 行的程式、嚴格 JSON。想著位元越低,遵守格式的能力應該越先崩。

所以做了什麼:跑完拿到 IQ1_S 3/7、IQ4_XS 5/7、Q4_K_XL 4/7。漂亮的單調關係,正好證明我的假設。差一點就這樣寫進文章。

結果:我去翻原始 response,看到這個:

Q4          finish=length  completion_tokens=512   content_len=0
Q6          finish=length  completion_tokens=2048  content_len=0
Q5 (IQ1_S)  finish=length  completion_tokens=256   content_len=0
Q5 (IQ4_XS) finish=stop    completion_tokens=255   content_len=34

finish=length 加上內容為空,意思是模型把整個 token 預算燒在 thinking 區塊裡,可見輸出是空的。那不是答錯,是沒答。 而我的判定程式把「沒答」記成 FAIL。

重跑的時候改三件事:max_tokens 拉到 4096、關思考改用 request 層的 "chat_template_kwargs":{"enable_thinking":false}、判定改成三態——PASS / FAIL / INVALID(不計入分母)。重跑後 21 格全部有效,結果是:

量化通過唯一失敗的那題
UD-IQ1_S6/7Q6
UD-IQ4_XS6/7Q6
UD-Q4_K_XL6/7Q6

而 Q6 那題三個都敗,敗法還一模一樣:程式碼通過 py_compile、通過我寫的所有 assert,只是寫了 38 行,而我的判準要求 40 到 60 行。 那是我的尺太挑,不是模型不會。

回頭看哪裡想錯了:我以為「加難題就能拉開差距」,錯的不是題目難度,是我沒有讓判準去分辨「答錯」和「沒答」。開跑前我還特地在工單裡寫了「判準要事先凍結,不准看完輸出才決定什麼算過」——那條寫對了,但它只擋住「事後改標準」,擋不住「標準本身分不出兩種零分」。

順便得到一個別的測試也用得上的結論:只在 prompt 裡寫 /no_think 不可靠。舊那批照樣產生了 221 個字元的 reasoning_content;改用 request 層的 chat_template_kwargs 之後,21 個 request 的 reasoning 長度全部是 0。

262K 載得進去,跑到 83,968 個 token 就死

遇到什麼問題:262K 那格要找能跑滿的最大 -ngl

原本打算怎麼解:載得起來就算數,往上加到 OOM 為止,最後一個 READY 的就是答案。

所以做了什麼-ngl 45 -c 262144 -ub 512 正常啟動、model loaded、正常回應短請求。看起來就是它了。

結果:真的餵長 prompt 進去,跑到第 83,968 個 token 時 CUDA1 還要再分配 1245.72 MiB,接著 OOM、segfault。往下退到 -ngl 40 才真的跑得完那 255,125 個 token。

回頭看哪裡想錯了:我當時只看能不能啟動,卻漏算了 KV cache 會隨 context 變大——剛啟動時它還沒長出來。這件事 Part 3 在 27B 上已經寫過一次,我還是在一顆完全不同架構的模型上照樣踩。所以這不是哪顆模型特有的問題:服務能啟動只代表當下塞得下,不保證 context 長大之後也跑得完。

對照:同一顆模型在 DGX Spark 上

同一顆 Qwen3.8-Flash-Next,換到 GB10(121 GB 統一記憶體),跑更大的 UD-Q4_K_XL

工作GX10(Q4_K_XL)ai-lab(IQ4_XS)
寫散文(冷)20.5123.02
改檔案(冷)76.6577.56
改檔案(第 2 / 3 次)88.02 / 78.7467.14 / 65.42
prefill366.19(18,092 token)63.77(255,125 token)

有 121 GB 統一記憶體之後,就不用在量化階和 context 之間取捨——104 G 的權重直接放、262,144 直接開、載入 4 分 40 秒、常駐約 104 GB。三張 2080 Ti 要跑同一個量化只能上 36 層,掉到 16.02。

但單串流的吐字速度並沒有比較快。改檔案那格幾乎打平(76.65 對 77.56)。

還有兩個異常沒查清楚。一是載入那 4 分半期間,Tailscale 跟 SSH 斷了大約三分鐘(主機沒有 reboot、dmesg 沒有 OOM kill 紀錄,server 最後正常 health)。二是 GX10 上第 3 次改檔的輸出少了 Markdown 的 code fence,所以「三次逐字相同」沒過——而 ai-lab 那邊三次 SHA256 完全相同。投機解碼理論上輸出不該變,這條還沒追。

還有一個沒解釋的:在 128K 這格重複同一個請求是越來越慢(77.56 → 67.14 → 65.42,草稿接受率同步從 90.3% 掉到 79.3%),但在 32K 那格是相反的(82.54 → 101.99 → 123.18)。兩邊都有關投機的對照組,數字是真的,原因我還沒查出來。

完整數據

環境:EPYC 7402(24C/48T)、三張 RTX 2080 Ti 22 GiB、123 GB RAM。llama.cpp 走 PR #277426c5afc86a,binary SHA256 5963505c…83d1077b,開工前後都驗過。模型是 unsloth/Qwen3.8-Flash-Next-GGUF

NCCL 每一輪都用 /proc/<pid>/maps 確認實際載入的是自編的 2.31.2——系統那顆 2.22.3 在 CUDA 13 驅動上是 stub,會偽裝成 OOM,這條 Part 16 已經拆過,這次只是照著避開。

contextnglKVdecode t/sprefill t/s三卡 VRAM
8,19245f1622.6258,020 MiB
8,19245q8_022.4157,910 MiB
32,76849f16啟動 OOM
32,76848f1626.1662,720 MiB
32,76848q8_025.7162,304 MiB
131,07247q8_0啟動 OOM
131,07246q8_023.1461,942 MiB
131,07245f1622.66
262,14445q8_083,968 token 時 OOM
262,14440q8_04.0563.7757,044 MiB

⚠️ prefill 那一欄只有最後一列填得出來。前面那些格子我一開始是用 43 到 81 個 token 的短 prompt 量的,那個長度量不出 prefill——固定開銷佔了壓倒性比重。同一組設定我在短 prompt 上量到過方向完全相反的結論,兩邊都不成立,所以那些數字全部作廢,只留下真的用 8,000 個 token 以上量的。

三量化那組的完整版:

量化檔案最大 ngldecode(中位/全距)prefill(8,121 token)能力
UD-IQ1_S68 G4826.74(26.70-26.76)533.086/7
UD-IQ4_XS88 G4825.56(25.48-25.78)529.026/7
UD-Q4_K_XL104 G3616.02(16.01-16.04)329.526/7

IQ1_S 的 context 上限沒驗出來:262,144 可以 READY,但 255K 的 needle 探到 247,850 個 token(97%)之後 client 端逾時,所以那格留白,不寫成「支援」。

Q4_K_XL 的 -ngl 37 可以 READY 但長 prompt 會 runtime OOM,跟上面 262K 那格是同一種病。


十輪量測裡有兩批數字被我自己作廢:第一批速度數字量的時候 build 落後上游 11 個 commit,第一批能力分數把「沒答」讀成「答錯」。兩次出錯的都是量測方法,不是模型本身。

真正留下來的那句話很短:這顆模型在這張老卡上,速度取決於你在做什麼,不取決於你挑哪一階量化。

常見問題

1770 億參數的模型要多少 VRAM 才跑得動?
這顆不用 1770 億份的 VRAM。Qwen3.8-Flash-Next 的 176.94B 裡有 51B 是一張 n-gram 查表——它不是拿整片去相乘的權重矩陣,每個 token 只查出稀疏的幾列;512 個專家每次只點名 10 顆 routed 加 1 顆 shared。實測三張改裝 2080 Ti(合計 66 GB)跑 UD-IQ4_XS,32K context 下 48 層全部放得進 VRAM、decode 26.16 tok/s;128K 用 46 層,decode 23.14 tok/s。
ngram 投機解碼可以加速多少?
看你在做什麼。實測改檔案類的工作(貼一份 130 行的 Python 進去、要它改一個 class 名)從 22.09 衝到 77.56 tok/s,3.51 倍;同一台機器同一組設定,叫它寫一篇散文只有 +0.17%,等於沒有。差別在 ngram 是從 context 的重複裡草擬候選,散文沒東西可抄。
量化階選越低會不會比較快?
在這張卡上不會。UD-IQ1_S(68 G)比 UD-IQ4_XS(88 G)只快 4.6%,因為兩個都塞得進 66 GB VRAM、48 層全上。真正掉速的是 UD-Q4_K_XL(104 G),塞不下只能上 36 層,十二層掉到 CPU 上算,decode 剩 16.02 tok/s。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。