~/blog/2080ti-turing-fp32-fallback

改裝 2080 Ti 22G · part 10

[趣味競賽 進階 #10] 同一張 2080 Ti,一台生圖慢 3.4 倍——兇手是啟動 log 裡一行 dtype fallback

cat --toc

TL;DR

兩台機器,同一張改裝 2080 Ti 22G。一台生 Z-Image 暖機 median 46.08s,實測擺著、看起來就是卡的天花板——GPU 全程 100%、模型常駐、卡也對。但我記得另一台一樣的卡只要 ~9.5s(那筆沒進知識庫)。兇手不是卡,是啟動 log 一行精度 fallback:Turing 沒有 bf16 tensor core,ComfyUI 為了保精度把權重留在 fp32,算得慢 3.4 倍。一個 --force-fp16,median 46.08→13.66s,免費、畫質沒差。誠實提醒:不省 VRAM,對影片模型也沒用。

短版:兩台同一張卡差 3.4 倍,問題根本不在卡

同一張改裝 2080 Ti 22G,插在兩台不同的機器上:一台生 Z-Image 暖機 median ~9.5s,另一台跑到 ~46s;換算掉解析度差之後還差 4-5 倍。跟晶片一點關係都沒有——差別藏在啟動 log 裡一行精度 fallback,而一個旗標就把暖機 median 從 46s 打到 13.66s。

看數據就該結案了——但我記得 forge 跑過 10 秒,而那筆沒進知識庫

先講清楚 AI 跟我看到的不是同一件事。當天早上,記錄上的結論很明確:Linux 那台實測 Z-Image 暖機 median 46.08s,就是這張 2080 Ti 的算力天花板、不是設定爛。這結論本身說得通——生圖全程 GPU 100%、模型常駐不用重新載入、卡也是對的那張;AI 手上只有這份 bench,從沒看過更快的數字,對它來說 46s 就是現實。

問題是,我記得一件記錄裡沒有的事:同一個 model,我在 forge 那台跑過,暖機後大概 10 秒就出圖。那個 session 從來沒進知識庫——所以對 AI 而言那筆不存在,只有我記得。而且我沒記錯。

forge 是台舊 Windows 機,裝的是同一張 2080 Ti。翻出它還活著的 log,白紙黑字寫著 Prompt executed in 9.49 seconds——暖機、跑 768²/8 步、穩定 1.01 s/it。慢的那台跑 1024²,像素是多了些,這條我認,但多的那點像素撐死解釋 2 倍,不到 4-5 倍。

於是問題變得很難看:兩張一模一樣的卡、同一個生成器,一台慢好幾倍。「這就是天花板」原本是數據撐得最穩的那條結論——現在它站不住了。

清三個嫌疑:OS、缺套件、attention backend——三條都走死了

我先列了三個最直覺的嫌疑,每個配一個一刀砍下去的決定性測試。

嫌疑一:OS。 Win vs Linux 是最順手的猜測,但我原本的預期是「純 Linux 應該更快才對」。一對日期就發現方向整個反了——慢的那台就是 Linux。想像中該更快的那邊反而在拖後腿,這條假設在「慢的是 Linux」那一刻就當場死了。OS 無罪。

嫌疑二:缺套件。 那也許是快的那台裝了什麼加速套件(comfy-kitchen 之類的)、慢的那台漏了?查版本——慢的那台是 0.2.20,比快的 0.2.10 還新。慢的機器版本更前面、東西一樣不缺。套件無罪。

嫌疑三:attention backend。 這條最陰。早上我其實在慢機上測過 xformers 的 A/B,結果 -0.85%(不但沒快、還更慢),我就切回原設定了。當下沒意識到的是:那場 A/B 量的,是跑在錯的那條計算路徑上的 attention。lever 沒選錯,我只是在一條本來就已經有問題的路上拉它,拉了當然沒感覺。

三條全走死,但每一條都幫我收窄範圍:不是 OS、不是套件、不是 attention kernel——問題出在「這個 model 到底怎麼跑」,不是「裝了什麼」。

破案:兩行 startup log,差在一個 dtype

兩台同一張卡的 ComfyUI 啟動 log 對 diff,分岔就在 weight dtype 與 manual cast 這兩行:快的走 fp16 tensor core,慢的被留在 fp32 manual cast——改裝 2080 Ti 22G 系列 #10。

破案關鍵就在 ComfyUI 啟動 log 的兩行,而我手上還留著這兩行純屬僥倖——forge 的 log 剛好還活著。那是一份 6,743 bytes 的 stderr 殘片,只涵蓋那張卡被拔走前最後 15 分鐘(22G 卡現在已經拔掉、換插了張 GTX 970,這份殘片是唯一還在的證人)。要不是它留著,這案子就斷在這了。

第一行是對齊用的指紋:Prompt executed in Ns。兩邊的冷跑、暖跑各自對齊,先確認「差是真的」,不是我某一次剛好卡到。

第二行才是破案關鍵:

  • 快的那台:weight dtype torch.float16, manual cast: torch.float16
  • 慢的那台:weight dtype torch.bfloat16, manual cast: torch.float32

同一個 checkpoint、同一張卡——快的整路跑 fp16,慢的被悄悄留在 fp32 的 manual cast。

機制其實很乾淨:Turing(sm_75)有 fp16 tensor core、但沒有 bf16 tensor core(bf16 硬體加速要 Ampere 之後才有)。ComfyUI 不是把每個 bf16 model 一律轉 fp32,而是依每個 model class 自己宣告支援的 dtype 挑計算精度;這顆 Z-Image checkpoint 在 Turing 上,預設就落在保精度的 fp32 manual cast,而 fp32 算起來比 fp16 tensor core 慢大約 3.4 倍。(這個「挑哪種精度是看 model class」的細節,正好就是為什麼等一下同一個旗標對另一顆 model 完全沒用。)

這裡藏著整篇最反直覺的一句:GPU 全程 100%,但 100% 忙 ≠ 撞到天花板。它可以 100% 地忙著算一條慢精度的路。忙,不代表滿。

解法:一個 --force-fp16,median 46.08→13.66s

修這件事只要一個旗標:--force-fp16。開下去,log 那行從 manual cast: torch.float32 變成 manual cast: None——終於肯吃 fp16 tensor core 了。

跑單變因 A/B(唯一的差就是這個旗標),1024²/8 步暖機:

  • 暖機 median:46.08 → 13.66s
  • sampler:5.64 → 1.59 s/it
  • 3.37×,免費,輸出完整(真的 PNG、不是黑圖、沒有 NaN,畫質跟 fp32 沒差)

這個旗標我已經烙進那台天天在生圖的 ComfyUI 啟動參數,每次生圖自動帶。

再回頭看 xformers 那個 -0.85%:那是因為當初量在 fp32 這條路上。路徑修好之後再測,attention backend 幾乎不影響結果——這 3.4× 從來就不是換 kernel 換得到的——差距其實來自 kernel 上游那個 dtype 的挑法。 我早上在錯的路上拉對的 lever,難怪拉不動。

誠實提醒一:它不會省 VRAM,反而多吃 0.5G

--force-fp16 沒有省記憶體,fp16 反而多用了 0.5G(15,982 vs 15,498 MiB)。理由很無聊:bf16 跟 fp16 的權重都是 2 bytes,本來就沒空間可省;省下的那點 activation 又被 allocator 的 pool 蓋過去了。

我一直隨身帶著的那個「這個 checkpoint 省 5G」的印象,是駐機 session 舊的自報數字,一跑乾淨 bench 就被打臉。所以講清楚:它換來的是速度、不是記憶體。 想靠它「多出來的空間」去 size 下一個 model,會踩雷。

誠實提醒二:影片模型沒受益——這招有沒有用要看 model family

拿到 3.4× 我當然貪心,把同一個旗標指向影片模型試試——LTX-2(Sulphur-2,3 秒短片)。同一張卡、同樣的單變因 A/B,結果是 1.00×(140.29 → 140.93s,VRAM 一模一樣,util 也一樣)。一點都沒動。

同一個 --force-fp16 旗標:Z-Image(Lumina2)吃到 3.37× 加速,LTX-2 影片模型 1.00× 完全沒動,分野在 model config 而非卡——改裝 2080 Ti 22G 系列 #10。

log 解釋了為什麼:旗標確實開了,權重也確實翻成 fp16,但那行 manual cast 還是被釘死在 torch.float32——LTX-2 的 model class 沒把 fp16 列進支援清單,cast 就一直是 fp32,旗標壓不過去。Z-Image(Lumina2 家族)吃這個旗標、LTX-2 不吃。

所以這招有沒有用,是看 model family、不是卡本身的性質。Turing 上這顆影片 model 真的就只有 fp32 這一條路可走;好消息是,現在至少有一組對照,能確認那 140s 就是實測結果,不是我設定沒調好。

收穫

最花時間的不是修,是分清「數據的結論」和「我記得的事」。 修就一個旗標。當天記錄上的結論很硬:46s、三個跡象同指一個方向,對只有這份 bench 的 AI 來說完全合理。真正戳破它的不是更聰明的推理,是我記得一件知識庫沒收進去的事——forge 跑過 10 秒。教訓:當你的知識庫說「這台從來沒更好過」,別忘了它可能只是漏記了——一個反例(哪怕只在你腦裡),勝過三個鐵證如山的訊號。

真正能帶去別的專案用的除錯手法,全是無聊動作。 兩台同卡機器,把最無聊的啟動 log 對 diff,一整個答案就藏在一行裡。把 100% GPU 讀成「忙」而不是「滿」。一個假設就配一個決定性測試(OS 那條在「慢的是 Linux」那刻就結束了,不用再糾結)。還有一條最容易忽略:當你的 KB / 筆記說「這台從來沒更好過」,要信機器本地的 log、別信摘要——forge 是個非同步節點,我自己的筆記兩度堅稱它那次快跑「沒發生過」,結果離線那台機器的 log 才是權威。

⚠️ 一個 bench 踩雷點順手記下:固定 seed 重送同一張圖,ComfyUI 會 cache-hit,直接印 Prompt executed in 0.00 seconds——你以為量到暖機速度,其實整排數字都是假的。解法是餵一個 graph 會忽略、但足以讓它強制重算的 nonce,seed 跟真正的輸入都不動。

通用原則一句話: 說穿了,慢的不是這張卡,是它被逼著用一種沒有硬體加速的精度格式在算——而「GPU 100%」這個最有說服力的訊號,剛好也是最會騙人的那個。

收工前的五條 checklist

  1. 先 diff 兩台同卡的啟動 log。model weight dtypemanual cast 這兩行;bf16 checkpoint 跑在 pre-Ampere 卡上、又出現 manual cast: torch.float32,就是 fallback。
  2. 別把 100% GPU 當天花板。 忙不等於最優——它可能 100% 忙在一條慢精度的路上。
  3. Turing 上的 bf16 checkpoint,先試 --force-fp16 model family 吃這套的話就是 3× 級的加速;驗收看 log 那行有沒有翻成 None、輸出是不是 NaN/黑圖。
  4. 別指望它省 VRAM。 fp16 跟 bf16 的 bytes 差不多,把它當速度優化就好、別算進省空間。
  5. 每個 model family 各自 A/B。 它幫了 Z-Image、對 LTX-2 完全沒用,別假設一招通吃。

同系列上一篇也是「log 把證據吃掉」的母題,只是那次死的是一個服務、不是速度:0xc0000409:當我的 AI 服務靜默暴斃,而 log 把死因吃掉了。工具本身:ComfyUI


常見問題

同一張 2080 Ti,兩台機器生圖速度為什麼差 3.4 倍?
不是卡的差異,是 dtype。ComfyUI 不是把所有 bf16 model 一律轉 fp32,而是依每個 model class 支援的 dtype 挑計算精度。Turing(sm_75)有 fp16 tensor core、但沒有 bf16 tensor core(bf16 加速要 Ampere 之後才有);這顆 Z-Image checkpoint 在 Turing 上,預設就落在保精度的 fp32 manual cast——而 fp32 算比 fp16 tensor core 慢大約 3.4 倍。加上 `--force-fp16` 讓它改走 fp16 tensor core,同一張卡的暖機 median 就從 46.08s 掉到 13.66s。
`--force-fp16` 會省 VRAM 嗎?
不會,反而多吃一點(15,982 vs 15,498 MiB)。bf16 跟 fp16 的權重都是 2 bytes,沒有省下來的空間;省掉的那點 activation 又被 allocator 的 pool 蓋掉了。它換來的是速度、不是記憶體——要拿它多出來的空間去塞下一個 model,會踩雷。
為什麼影片模型(LTX-2)加了同一個旗標卻沒變快?
因為這招有沒有用,得看 model family。LTX-2 的 model class 沒把 fp16 列進支援清單,manual cast 就一直釘在 fp32、旗標壓不過去;Z-Image(Lumina2 家族)吃這個旗標、LTX-2 不吃。同一張卡上,LTX-2 這顆影片 model 真的就只有 fp32 這條路,140s 是實測跑出來的,不是設定沒調好。
怎麼知道自己有沒有踩到這個坑?
拿兩台同卡機器的 ComfyUI 啟動 log 對 diff,盯 `model weight dtype` 跟 `manual cast` 這兩行。bf16 checkpoint 跑在 pre-Ampere 卡上、log 出現 `manual cast: torch.float32`,那就是 fallback。加 `--force-fp16` 之後那行會變 `manual cast: None`,再確認輸出不是 NaN、不是黑圖,就對了。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。