~/blog/fastllm-dual-2080ti-nvfp4-self-quant

改裝 2080 Ti 22G · part 22

2080 Ti 沒有 FP4 也能跑 NVFP4:自己轉 Qwen3.8-27B,寫程式快 31%

2026-09-2815 分鐘閱讀#fastllm#nvfp4#2080-ti#qwen3.8English
❯ cat --toc

TL;DR

兩張改裝 2080 Ti 22G 把 Qwen3.8-27B 換成自己轉的 NVFP4:寫程式 151.7 tok/s(FP8 是 116.1)、數學 178.8、散文 101.9,四條請求同時跑合計 297。2080 Ti 沒有 FP4 硬體,FastLLM 在 kernel 裡解包成 FP16 再算,生成卡在讀權重,權重從 29GB 降到 20GB 就快了。140 題 131/128,跟 FP8 的 132/127 差不多。代價是長 prompt 讀得慢 25%。

改裝 2080 Ti 22G 系列 #22 封面:兩張並排的雙風扇顯示卡,前方一疊厚重的資料磚壓成一半高度的薄片,同樣的發光資料流更快地穿過

前言

搬家公司算時間看的是跑幾趟,不是你有幾本書。同一套書換成小開本,一趟車就搬得完更多頁。

顯卡吐字也是這樣:每吐一個字,就要把整份權重從 VRAM 讀一遍。上一篇的 Qwen3.8-27B 用 FP8,每次要讀 29GB;換成 NVFP4,大部分權重從 8 bit 變成 4 bit,每次只讀 20GB。

2080 Ti 沒有 FP4 硬體,我原本以為 NVFP4 在這張卡上沒戲。結果 FastLLM 在 kernel 裡把 4 bit 解包回 FP16 再算,寫程式從 116.1 跳到 151.7。這篇是怎麼轉、用哪一顆、怎麼開。

這版跟上一篇差在哪:換成 NVFP4,KV 改 fp8,四條並行

兩張 2080 Ti,TP=2,同一個自編的 FastLLM(上一篇那一版),同一支量測腳本。看前兩列:

上一篇(FP8)這一版(自製 NVFP4)
寫程式 / 數學 / 散文(tok/s)116.1 / 135.5 / 74.2151.7 / 178.8 / 101.9
同時跑的請求:數量 / 合計 tok/s2 / 175.84 / 297
54K token 的 prompt 讀完40.5 秒50.6 秒
每個請求的 context 上限222,080257,920
140 題(不開思考 / 開思考)132 / 127131 / 128

主模型從 29GB 變成 20GB。兩條同時跑時每條從約 88 升到 107.8;長請求讀 prompt 時插進來的短請求,第一個字 0.52 秒、5.5 秒答完(原本 1.18 秒 / 3.8 秒)。

量法跟上一篇一樣:真實的 chat 請求,temperature 0、每次 512 token,三種題目各打三發取中位數。140 題是在改 KV 和並行數之前、同一顆 NVFP4 上跑的。

改了三件事:

  1. 主模型換成 NVFP4,而且是自己從 BF16 轉的,照一個已經證實在這張卡上跑得快的配方。
  2. KV cache 從 fp4 改成 fp8。 權重省下來的 VRAM 夠放 fp8 的 KV,而且並行時 fp8 反而比較快。
  3. --max_batch 從 2 改成 4。

沒有 FP4 硬體,為什麼還會變快

2080 Ti 沒有能直接算 4 bit 的硬體。FastLLM 的做法是權重用 4 bit 存著,每次要算之前,才在顯卡上把它展開回 16 bit,這一步叫解包。

這樣還會變快,是因為生成的時候,顯卡大部分時間不是在算,是在把權重從顯卡記憶體(VRAM)搬到運算單元。就像前言那台搬家的車,一趟要搬的少,就跑得快。解包是多出來的工,但做這件事的時候運算單元本來就閒著。

技術細節:顯卡裡專門算矩陣乘法的單元叫 tensor core,2080 Ti 這一代(Turing)的 tensor core 支援 FP16、INT8、INT4,沒有 FP4。FastLLM 在 kernel(顯卡上跑的那段程式)裡把 NVFP4 解包成 FP16,再交給 tensor core。上游 README 也提到一組針對 2080 Ti 調校的 NVFP4 kernel,對象是幾組寬度 5120 的矩陣;5120 正好是 Qwen3.8-27B 的 hidden size。

不開投機解碼時,差距最明顯:

不開投機,寫程式tok/s
FP8(29 GB)32.9
自製 NVFP4(20 GB)45.1(+37%)
示意圖:每吐一個字都要把整份權重從 VRAM 讀一遍。FP8 每次讀 29GB,不開投機 32.9 tok/s;NVFP4 每次讀 20GB,要多做一次解包,但讀得少,不開投機 45.1 tok/s
每個字從讀 29GB 變成讀 20GB,不開投機解碼從 32.9 到 45.1 tok/s。

開了 DFlash2,一次驗 8 個位置,讀一次權重收好幾個字,所以寫程式從 116.1 到 151.7,比例跟不開投機時差不多。

同樣是 NVFP4,三顆差了兩倍多

一開始我直接下載現成的 NVFP4,結果差很多。三顆都用同一個 FastLLM、同一組啟動參數(KV fp4、--max_batch 2)量,看第二列跟最後一列:

模型格式寫程式 / 數學 / 散文不開投機140 題
FP8(上一篇)線性層 FP8116.6 / 135.8 / 74.632.9132 / 127
orcarouter 官方 NVFP4只有部分 MLP 是 NVFP4,其餘 FP865.3 / 77.9 / 43.632.4沒測
lyf Heretic-ARA NVFP4全部線性層 NVFP4136.0 / 185.5 / 102.145.3124 / 129
自製 orcarouter NVFP4全部線性層 NVFP4149.9 / 178.0 / 104.345.1131 / 128
格式對照圖:orcarouter 官方 NVFP4 只有前 56 層的 MLP 是 NVFP4,attention、GDN 投影和最後 8 層 MLP 是 FP8,lm_head 留 BF16;lyf 和自製版所有線性層都是 NVFP4,只有 embedding、lm_head、視覺、conv1d、MTP 留 BF16
同樣標成 NVFP4,官方那顆有 232 組線性層還是 FP8,速度反而比 FP8 版慢。
  • orcarouter/Qwen3.8-27B-Uncensored-NVFP4:跟上一篇的 FP8 是同一家出的去審查版,但它是混合精度。config 裡 168 組 MLP 是 NVFP4,232 組是 FP8。不開投機 32.4,跟 FP8 一樣;一開 DFlash2 寫程式只剩 65.3,比 FP8 版還慢一半。草稿的接受率其實比 FP8 高,所以慢的不是猜不中;我的推測是那些 per-channel FP8 權重在一次驗 9 個 token 時走了比較慢的路徑,這點沒有單獨驗證。
  • lyf/Qwen3.8-27B-Heretic-ARA-NVFP4-MTP-VL:用 NVIDIA ModelOpt 轉的,所有線性層都是 NVFP4,速度一下子就上來。但它的底模是另一個去審查版本(Heretic ARA),140 題不開思考掉到 124,主要掉在 MuSR 和 HumanEval+。
  • 自製版:拿 orcarouter 的 BF16 原檔,照 lyf 的格式自己轉。底模跟 FP8 同一顆,速度跟 lyf 同一級,能力跟 FP8 差不多。

自製版寫程式比 lyf 還快一點,草稿第 1 到第 3 個位置的接受率是 88.8 / 73.4 / 55.0%,lyf 是 82.8 / 64.3 / 52.4%。我的猜測是 orcarouter 去審查改動比較輕,輸出比較接近原版 Qwen3.8,而 DFlash2 草稿正是拿原版訓練的。

自己轉:一支 numpy 腳本,不用 GPU、不用校準

需要三樣東西:

  1. BF16 原檔:orcarouter/Qwen3.8-27B-Uncensored(約 56GB,要先在 Hugging Face 上同意條款)。
  2. lyf 那個 repo 的設定檔,只要 json 和 yaml,不用下載權重。轉換器照它的張量命名、分片方式和 quantization_config 輸出。
  3. 轉換腳本:qwen38-nvfp4-convert.py,只依賴 numpy。
hf download orcarouter/Qwen3.8-27B-Uncensored --local-dir /mnt/nvme1t/models/orcarouter-qwen38-uncensored-bf16
hf download lyf/Qwen3.8-27B-Heretic-ARA-NVFP4-MTP-VL --include "*.json" "recipe.yaml" --local-dir ./nvfp4-ref
curl -O https://ai-muninn.com/tools/qwen38-nvfp4-convert.py
python3 qwen38-nvfp4-convert.py \
  /mnt/nvme1t/models/orcarouter-qwen38-uncensored-bf16 \
  /mnt/nvme1t/models/orcarouter-qwen38-uncensored-nvfp4-self \
  --reference ./nvfp4-ref

轉出來是 20GB,三個分片。腳本做的事:

  • 所有線性層轉 NVFP4:每 16 個值共用一個 FP8(E4M3)scale,每個張量一個 FP32 global scale,值本身打包成 4 bit(E2M1)。
  • 有三組要共用 global scale:attention 的 q/k/v、GDN 的四個輸入投影(in_proj_qkv / z / a / b)、MLP 的 gate/up。這是比對 lyf 的權重才發現的,各算各的格式對,數值就對不上。
  • 留 BF16 的:lm_head、embed_tokens、視覺塔、conv1d、MTP。
  • 不做校準:ModelOpt 另外會用資料校準 activation 的 scale(input_global_scale),那是給 W4A4 用的。FastLLM 在 2080 Ti 上 activation 走 FP16,讀檔時不讀這個欄位,腳本直接填 1.0。

怎麼確定轉得對:把 lyf 的 NVFP4 權重解回 BF16,再用這支腳本重新量化一次,attention、GDN、MLP 各挑一層,打包後的權重、block scale、global scale 三個張量都跟 lyf 的逐位元相同。自製成品的 2,687 個張量,名稱、dtype、shape、分片也跟 lyf 完全一致。

啟動:KV 改 fp8、max_batch 改 4

FastLLM 用上一篇那一版(a2bf07fd、撤掉 d4b04876、合併 #749 和 #756),不用重編,只換模型路徑和兩個參數:

export LD_PRELOAD=/home/coolthor/nccl-install/lib/libnccl.so.2
export CUDA_VISIBLE_DEVICES=0,1
export FASTLLM_CUDA_GRAPH=0
export FASTLLM_DRAFT_QUANT=nvfp4
export FASTLLM_COOPERATIVE_LONG_PREFILL=1

~/venvs/ftllm-dual/bin/ftllm server \
  -p /mnt/nvme1t/models/orcarouter-qwen38-uncensored-nvfp4-self --model_name qwen38-27b-ud \
  --tp 2 --max_batch 4 --chunked_prefill_size 4096 \
  --gpu_mem_ratio 0.95 --kv_cache_dtype fp8_e4m3 \
  --speculative_algorithm dflash \
  --speculative_draft_model_path /mnt/nvme1t/models/dflash2-qwen38-zlab \
  --speculative_num_draft_tokens 8 \
  --prefix_cache true \
  --chat_template /mnt/nvme1t/models/qwen-fastllm-low/chat_template.jinja \
  --host 0.0.0.0 --port 8082

一樣不要帶 --tokens,帶了並行數會被壓回 1(原因在上一篇)。

KV fp8 加四條並行:單條速度不變,合計接近兩倍

權重小了 9GB,每張卡多出約 3GB 給 KV cache。我先在 --max_batch 2 比 KV fp4 和 fp8,再用 fp8 往上加並行數。看「合計」那一欄:

KV / 並行數單條 寫程式 / 數學 / 散文N 條同時,每條合計KV 池
fp4 / 2149.8 / 178.3 / 104.099.9199.8564,480
fp8 / 2151.0 / 178.3 / 101.5108.3216.6321,152
fp8 / 3150.5 / 178.2 / 101.481.0243.1302,976
fp8 / 4150.6 / 178.3 / 101.573.4293.4257,920
長條圖:KV fp8 下,一條到四條同時寫程式。單條約 150 tok/s;兩條每條 108.3、合計 216.6;三條每條 81.0、合計 243.1;四條每條 73.4、合計 293.4
每多一條,每條慢一點,合計一直往上。單條請求的速度完全不受影響。
  • fp8 KV 在並行時反而比較快:兩條同時,每條從 99.9 升到 108.3。我的解讀是 fp4 每次讀 KV 都要解包,並行時讀的量多,解包的成本才顯出來。
  • 四條同時合計 293,是一條的將近兩倍。生成的時候算力大部分閒著,多一條請求幾乎不用多讀權重。
  • KV 池從 56 萬降到 25.8 萬 token,每個請求的上限是 257,920,比 FP8 那版的 222,080 還大。

上線後在正式環境重測一次:單條 151.7 / 178.8 / 101.9,兩條每條 107.8,四條每條 74.3。

代價:長 prompt 讀得慢 25%

生成變快,prompt 讀取反而變慢。一段 54K token 的 prompt,FP8 讀完要 40.5 秒,NVFP4 要 50.6 秒。

讀 prompt 跟生成不一樣:它一次處理幾千個 token,卡的是算力,不是讀權重。NVFP4 要先解包才能算,這一步在算力吃緊的時候就是純粹多出來的工。重複的前綴有 prefix cache,第二次送同一段 8K 的前綴,只要 1.8 秒。

所以這一版適合大部分是短 prompt、同時有好幾個人在用的情況,像是好幾個 agent 在互相傳訊息。如果你大部分時間都在丟幾萬 token 的新文件,FP8 那版會比較快。

能力:140 題 131 / 128

看合計那欄,同一種模式上下兩列比:

GSM8KMATH-500MuSRHumanEval+合計
FP8,不開思考50301636132
自製 NVFP4,不開思考50291735131
FP8,開思考 + low49301236127
自製 NVFP4,開思考 + low50301236128

lyf 那顆是不開思考 50 / 30 / 12 / 32 = 124、開思考 50 / 28 / 14 / 37 = 129。

同一顆底模從 FP8 換成 NVFP4,兩輪都只差 1 題,在誤差範圍內。lyf 那顆不開思考掉了 8 題,但它同時換了量化和底模,分不出是哪一個造成的。


進階:除錯與實驗紀錄

不讀這節不影響使用。這裡是轉換器的驗證細節、DFlash 參數掃描,以及兩條走不通的 4 bit 路。

共用 global scale:各算各的,數值對不上

轉換器第一版每個張量各算一個 global scale,輸出的格式、形狀都對,但跟 lyf 的權重比,數值對不上。回頭看 ModelOpt 的做法,有三組張量共用同一個 global scale,取整組的絕對值最大值來算:

  • attention 的 q/k/v(16 層,16 組)
  • GDN 的 in_proj_qkv / in_proj_z / in_proj_a / in_proj_b(48 層,48 組)
  • MLP 的 gate/up(64 層,64 組)

共 128 組。in_proj_a 最容易漏:它自己的最大值比較小,單獨算 scale 會漏掉另外三個投影裡更大的值;照整組算之後才逐位元對上。

驗證方式是把 lyf 的權重解回 BF16(保留 FP4 的負零),再重新量化:

類別層打包權重block scaleglobal scale
Attention第 11 層 self_attn.q_proj31,457,280 / 31,457,280 bytes3,932,160 / 3,932,1604 / 4
GDN第 0 層 linear_attn.out_proj15,728,640 / 15,728,6401,966,080 / 1,966,0804 / 4
MLP第 0 層 mlp.gate_proj44,564,480 / 44,564,4805,570,560 / 5,570,5604 / 4

in_proj_qkv 由解回來的 BF16 重估 global scale 會差 1 ULP,打包權重和 block scale 仍逐位元相同;這是因為解回來的值沒辦法完全還原原始 BF16 的極值。我另外拿原始 BF16,把 in_proj_a、q_proj、gate_proj 各重新量化一次,三種張量也都跟成品逐位元一致。

DFlash 參數:主模型換成 NVFP4,最佳組合沒變

我擔心主模型變快之後,原本的 n=8 加草稿 NVFP4 不再是最好的組合,在 lyf 那顆上重掃:

草稿 token 數 / 草稿量化寫程式數學散文接受率前 3 位
8 / nvfp4136.0185.6102.580.8 / 59.1 / 44.7%
8 / nvfp4_head118.6174.494.884.5 / 63.2 / 46.8%
8 / off123.8162.692.583.6 / 65.6 / 50.4%
6 / nvfp4129.6156.497.282.7 / 64.8 / 48.5%
4 / nvfp4113.2124.797.385.4 / 68.1 / 51.2%

草稿量化得越少,猜得越準,但草稿自己跑得慢,整體是虧的。只量化輸出頭(nvfp4_head)反而比完全不量化還慢,原因我沒查。猜的 token 越少,每一位的接受率越高,每一步能收的字也越少,數學最明顯。大於 8 這份草稿 checkpoint 不接受。

KV 與並行的完整數字

KV / 並行數草稿接受率前 3 位54K prompt 讀完插短請求,第一個字
fp4 / 281.9 / 63.1 / 47.8%49.9 秒0.53 秒
fp8 / 279.3 / 61.4 / 50.1%50.5 秒0.47 秒
fp8 / 380.3 / 62.8 / 51.7%50.6 秒0.51 秒
fp8 / 480.3 / 62.9 / 52.5%50.6 秒0.53 秒

四種設定跑 54K prompt 都答對,每個請求的 context 上限在 fp8 / 2 和 fp8 / 3 都還是完整的 262,144,到 fp8 / 4 才降到 257,920。

orcarouter 官方 NVFP4 的細節

它的 config.json 有兩組量化設定:168 個目標是 NVFP4 W4A4(前 56 層的 MLP),232 個是 FP8 W8A8(attention 的 q/k/v/o、GDN 的 in_proj_qkv / in_proj_z / out_proj、最後 8 層 MLP),lm_head 留 BF16。FastLLM 載入正常,log 裡有 NVFP4 的 prefill 和 decode kernel,17*23 答 391,看圖也對,只是慢。

寫程式 / 數學 / 散文不開投機接受率前 3 位(英文)KV 池
FP8116.6 / 135.8 / 74.632.982.7 / 67.3 / 52.1%222,080
orcarouter 官方 NVFP465.3 / 77.9 / 43.632.486.3 / 68.3 / 55.5%447,104
自製 NVFP4149.9 / 178.0 / 104.345.188.8 / 73.4 / 55.0%564,480

接受率比 FP8 還高,不開投機也跟 FP8 一樣,只有開了投機、一次驗好幾個 token 時才慢。

兩條走不通的 4 bit 路

在找到 NVFP4 之前試過 FastLLM 自己的 INT4(int4g,每 128 個值一組):

  • TP=2 沒有 INT4 的矩陣乘法 kernel。 載入正常,每張卡 12GB,但一送請求 GPU 使用率 0%、CPU 110%,16 個 token 120 秒出不來。原因是 FastllmCudaMatMulFloatInt4Group 只接在單卡的 cudadevice.cpp,雙卡的 multicudadevice.cpp 沒有,於是默默掉到 CPU 算。卡死的 process 不理 SIGTERM,要 kill -9。
  • 單卡 INT4 不開投機只有 20.2 tok/s,以單卡約 15GB 的權重估算,每秒讀約 300GB,只用到 2080 Ti 頻寬的一半,解包成本太高。單卡再開 DFlash2 會崩(Resource deadlock avoided)。

另外一個容易踩的:--dtype fp8_e4m3 遇到 BF16 原檔不會轉,會默默用 FP16 載入。要 FP8 或 NVFP4 都得先離線轉好。

版本鎖定

  • FastLLM:a2bf07fd + 撤掉 d4b04876 + #749 + #756(跟上一篇同一版)
  • 主模型:orcarouter/Qwen3.8-27B-Uncensored BF16 → 自製 NVFP4,total_size 20,558,935,392 bytes
  • 草稿:z-lab/Qwen3.8-27B-DFlash2,執行時轉 NVFP4
  • CUDA 12.4、sm_75,NCCL 2.31.2(自編)

還沒做的

  • KV fp8 和四條並行是在跑完 140 題之後才改的,改完沒有重跑能力測試。
  • 轉換器只驗過 Qwen3.8-27B 這個架構,換別的模型要對照那個模型自己的 NVFP4 參照。
  • 長 prompt 讀取慢 25% 沒有解法,要改得從 prefill 的 NVFP4 kernel 下手。

同系列:

常見問題

RTX 2080 Ti 沒有 FP4 硬體,跑 NVFP4 權重會變快嗎?
在 FastLLM 上會。Turing 的 tensor core 不能直接算 FP4,FastLLM 把 NVFP4 權重存在 VRAM 裡,運算時在 kernel 裡解包成 FP16 再算。生成階段卡在讀權重不是卡在算,權重變小就快:兩張 2080 Ti 跑 Qwen3.8-27B,不開投機解碼從 FP8 的 32.9 到 45.1 tok/s,開 DFlash2 寫程式從 116.1 到 151.7。
orcarouter 的 Qwen3.8-27B-Uncensored-NVFP4 在 FastLLM 上為什麼比 FP8 還慢?
那顆是混合精度:只有前 56 層的 MLP 是 NVFP4,attention、GDN 投影和最後 8 層 MLP 都是 FP8(W8A8 per-channel)。在兩張 2080 Ti 上開 DFlash2,寫程式只有 65.3 tok/s,FP8 是 116.6。改用全部線性層都轉 NVFP4 的配方,同一顆底模自己轉,寫程式 149.9。
怎麼把 Qwen3.8-27B 的 BF16 權重轉成 FastLLM 能讀的 NVFP4?
照 lyf/Qwen3.8-27B-Heretic-ARA-NVFP4-MTP-VL 的格式轉:所有線性層轉 NVFP4(每 16 個值一個 FP8 scale、每個張量一個 FP32 global scale,q/k/v、GDN 四個輸入投影、MLP gate/up 共用 global scale),lm_head、embedding、視覺、conv1d、MTP 留 BF16。我寫了一支只用 numpy 的腳本,不需要 GPU、不需要校準,跟 lyf 的權重逐位元對得上。
NVFP4 會讓 Qwen3.8-27B 變笨嗎?
在我的 140 題上幾乎沒變:同一顆 orcarouter 底模,FP8 是不開思考 132、開思考 127,自己轉的 NVFP4 是 131、128。另一顆 lyf 的 NVFP4 不開思考掉到 124,但它換了底模(Heretic ARA),掉分分不出是量化還是底模。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。