改裝 2080 Ti 22G · part 16
[Benchmark] 我把 2080 Ti 的 NCCL 修好了,然後發現它沒快多少
❯ cat --toc
TL;DR
Ubuntu 的 libnccl2 2.22.3 跟 CUDA 13 驅動不相容,一呼叫 collective 就回 CUDA driver is a stub library。自編 NCCL 2.31.2-1 帶 compute_75 修好,不用重編 llama.cpp。修好之後 prefill 的 PCIe 流量從每卡 1,286 MB/s 掉到 15 MB/s——掉了 99%,而寫 code 的 tok/s 打平、深 context 首字快 13%。那個看起來很糟的 fallback,其實沒讓你付多少代價。順帶推翻我自己在 part 14 寫的「NCCL 判定 NO-GO」。

前言
家裡跳電之後,你會先去看總開關,不會先懷疑是台電的問題。但如果總開關看起來好好的,你就很容易卡住——因為下一步要去哪找,沒人告訴你。
上一篇量的是 AllReduce 在 PCIe 上吃掉多少頻寬。那篇有個前提我當時寫得很輕:這台機器走的是繞主機記憶體的那條路。
為什麼走那條?因為 part 14 的時候 NCCL 起不來,我在 FAQ 裡寫下「目前判定是 NO-GO」。
那句話是錯的。 而且錯得很瞎——我當時連錯誤訊息都沒存下來,就直接說它不能用。
症狀:init 成功,第一次 collective 就死
先把症狀說清楚,因為它會誤導人。
裝好 libnccl2、CMake 找得到、ldd 也連得上 libnccl.so.2,伺服器啟動正常,模型載入正常。然後第一個真正的請求進來,整個 server 直接死掉。
看起來很像「這代卡不支援」,所以我當時就往那個方向想了。
要看清楚它,得把 llama.cpp 整個排除掉。我寫了一支四十行的最小程式,只做三件事:初始化、跑一次 fp32 的 AllReduce、跑一次 bf16 的:
== [1] ncclCommInitAll ==
init OK
== [2] AllReduce fp32 ==
!! NCCL FAIL -> unhandled cuda error
初始化成功,第一次 collective 失敗。 這個分界很重要:init 大部分是主機端的工作,第一次 collective 才是第一次真的在 GPU 上跑 kernel。
它自己叫我開 debug,照做之後關鍵的一行出現了:
misc/strongstream.cc:60 NCCL WARN Cuda failure 'CUDA driver is a stub library'
根因:發行版的套件配不上新驅動
CUDA_ERROR_STUB_LIBRARY 是個真的錯誤碼(34),意思是有人呼叫到了 stub 版的 CUDA 驅動進入點。CUDA 工具包會附一份 stubs/libcuda.so——那是給沒有驅動的機器編譯時連結用的 stub library,裡面的函式沒有實作,被呼叫就直接回這個錯誤碼。
所以我第一個念頭是:一定是它跑進搜尋路徑了。
不是。符號連結鏈是對的:
libcuda.so → libcuda.so.1 → libcuda.so.580.173.02 (96 MB,真驅動)
stubs/libcuda.so (70 KB,不在路徑上)
而且直接呼叫 driver API 全部正常:cuInit(0) 回 0、cuDriverGetVersion 回 13000、cuDeviceGetCount 看得到三張卡。
驅動是好的。是 NCCL 拿到了一個 stub 的進入點。
⚠️ 到這裡我卡住了,而且我沒有把它查完。 libnccl.so.2 沒有指向 stubs 的 RUNPATH、stubs 目錄不在任何搜尋路徑、行程載入的也確實是真驅動——但錯誤碼就是說有人呼叫到了 stub 的進入點。那條路徑我沒有追出來。
(我原本在這裡寫了一個「cuGetProcAddress 會回傳 stub 指標」的解釋。查證時被打掉了:NVIDIA 文件說找不到符號會回 CUDA_SUCCESS 加上 pfn == NULL,不是可呼叫的 stub。那個機制是我為了讓故事說得通自己補的,而這篇文章通篇在講的就是不要這樣做。)
我能確定的只有這個組合會壞:
| 版本 | |
|---|---|
| 驅動 | 580.173.02(CUDA 13.0) |
| NCCL | Ubuntu 的 libnccl2 2.22.3(CUDA 12 時代) |
apt 上只有 2.22.3 一個版本。所以只剩一條路:自己編。
這一節的標題只能寫到「配不上」,不能寫成「因為 X 所以壞」——我沒有那個 X。能證明的是:這個組合會壞,換成自編的新版就好了。
修法(可以照抄)
git clone https://github.com/NVIDIA/nccl /home/you/nccl-src
cd /home/you/nccl-src
git checkout v2.31.2-1
export CUDA_HOME=/usr # ⚠️ 見下面坑二
make -j 16 src.build \
NVCC_GENCODE="-gencode=arch=compute_75,code=sm_75" \
PREFIX=/home/you/nccl-install
make src.install \
NVCC_GENCODE="-gencode=arch=compute_75,code=sm_75" \
PREFIX=/home/you/nccl-install
掛上去:
LD_LIBRARY_PATH=/home/you/nccl-install/lib
# 而且不要設 GGML_CUDA_ALLREDUCE=internal —— Linux 預設就會走 NCCL
不用重編 llama.cpp。 llama-server 是動態連結 libnccl.so.2,掛上路徑就換過去了:
libnccl.so.2 => /home/you/nccl-install/lib/libnccl.so.2
系統那顆完全不動,要退回去就是拿掉那行環境變數。
坑一:src.install 不會沿用你的 gencode
這個最陰,因為它不會報錯。
make src.build NVCC_GENCODE=... 編好之後,如果 src.install 那行沒有再傳一次同樣的 NVCC_GENCODE,install 會拿預設值沒跳任何訊息就重編一次,然後裝上一份沒有 sm_75 的 library。我這次實際撞到,log 裡逐字印著 compute_60/61/70/80/90——沒有 75——而且真的開始編了。
⚠️ 那組預設值跟你的 CUDA toolkit 版本有關:我這台是 CUDA 12.4,所以預設不含 sm_75;換成 CUDA 13 的 toolkit,預設就會包含它。所以這個坑會不會咬到你,要看你的 toolkit。
你會以為自己編對了。驗法:
cuobjdump --list-elf <lib> | grep -oE 'sm_[0-9]+' | sort -u
# 應該看到 sm_75
坑二:CUDA_HOME 沒設會在五秒內失敗
上游的 Makefile 預設去找 /usr/local/cuda/bin/nvcc,但發行版套件把 nvcc 裝在 /usr/bin。沒設 CUDA_HOME=/usr 的話,make 會在幾秒內死掉,錯誤是找不到檔案。
修好之後:PCIe 流量掉了 99%,而速度是往上的
先把兩件事並排放,因為這兩件事很容易混在一起:
| prefill 階段 | 走 PCIe(internal) | 走 NCCL | |
|---|---|---|---|
| PCIe 流量(每卡 rx) | 1,286 MB/s | 15 MB/s | ↓ 99% |
| PCIe 流量(每卡 tx) | 1,342 MB/s | 5 MB/s | ↓ 99.6% |
| 速度 | 800 tok/s | 917 tok/s | ↑ 14.6% |
MB/s 是匯流排上搬了多少資料,tok/s 才是速度。 這兩個在這裡的變化方向是相反的 —— 流量掉到剩零頭,速度反而上去。
流量掉不是因為工作變少,是資料改走別條路了:

走 internal 的時候,AllReduce 的每個位元組都要過 PCIe 兩次:自己算的那半寫下去到主機記憶體,對方那半再讀回來。prefill 一個 batch 的 payload 很大,所以那 1.3 GB/s 幾乎全部是 AllReduce。
換成 NCCL,資料直接從 GPU0 走 NVLink 到 GPU1,主機記憶體那一站整個不存在。PCIe 上就只剩本來就該走 PCIe 的東西:prompt 上傳一次、logits 回來。
一個旁證:它掉回了單卡的地板
怎麼確定是「歸零」而不只是「變少」?拿沒有 AllReduce 的單卡跑同一件事來比:
| prefill 每卡 rx | |
|---|---|
| 單卡(完全沒有 AllReduce) | 10 MB/s |
| 雙卡 + NCCL | 15 MB/s |
| 雙卡 + internal | 1,286 MB/s |
NCCL 那組掉回單卡的地板。 也就是 AllReduce 對 PCIe 的貢獻真的歸零了,剩下那 15 MB/s 本來就跟 AllReduce 無關。
decode 也一樣掉回地板(35 對單卡的 31),只是幅度看起來小一點——因為 decode 的 132 MB/s 裡本來就只有約 101 是 AllReduce,剩下是 token I/O 和 MTP 草稿。兩個階段的 AllReduce 都歸零,百分比不同只是因為它原本佔的比例不同。
而 NVLink 那一側,消費卡是讀不到吞吐計數器的。所以那些位元組是從儀表上消失,不是從機器上消失。
然後速度幾乎沒動
這才是這篇真正的內容。
流量掉了 99%,你會預期速度大漲。實測不是這樣:
寫 code(這台平常在做的事)
| tok/s 中位 | 範圍 | MTP 接受率 | |
|---|---|---|---|
| 走 PCIe | 55.65 | 50.88–56.72 | 77.9% |
| 走 NCCL | 56.24 | 55.66–59.01 | 73.1% |
打平。 中位數只差 1%,而且兩組的範圍大幅重疊——這樣算沒有顯著差異,不能說 NCCL 比較快。
有意思的是走 PCIe 那組的草稿接受率反而更高(77.9% 對 73.1%),卻沒有換到更快。猜得更準還追不上,表示 NCCL 每單位時間做的實功確實多一點,只是不夠多到看得出來。
首字延遲(TTFT)
這個有差,而且差在該差的地方:
| context 深度 | 走 PCIe | 走 NCCL | |
|---|---|---|---|
| 短 prompt | 592 ms | 569 ms | 範圍重疊,不算數 |
| 約 4K | 1,723 ms | 1,514 ms | 快 12% |
| 約 20K | 6,632 ms | 5,779 ms | 快 13% |
context 越深差越多,而且深的那組兩邊範圍完全不重疊(5,729–5,786 對 6,598–6,865)。這是唯一一項站得住腳的差距。
穩定度
走 PCIe 那組的 code tok/s 擺盪 50.88–56.72(11%),NCCL 是 55.66–59.01(6%)。
「偶爾特別慢」比「平均慢一點」更影響體感,而那個 50.88 就是偶爾特別慢的樣子。
為什麼流量掉 99%,速度卻沒跟著漲
因為上一篇量到的那件事:那條匯流排本來就沒被用滿。
走 PCIe 的時候,最忙的一格也只佔 Gen3 x16 的 8.2%。把 8.2% 降到 0.1%,你省下來的是一條本來就空著的路。
真正卡住吐字的是往返次數——每個 token、每一層都要對一次帳,把自己算的那半寫出去、等對方、再讀回來。換更快的線可以讓每一趟短一點,但趟數一趟都沒少。
所以結果很合理:prefill 一次搬一大包,傳輸佔比高,換線有感(+14.6%);吐字一次搬幾十 KB 但要來回幾十次,延遲主導,換線幾乎沒用。
順帶更新上一篇的一個建議
上一篇量到 prefill 最快的是雙卡 layer split(944.8 tok/s),tensor 只有 844.9,所以那篇建議「長 prompt 短回答的工作偏 prefill,layer split 比較划算」。
那個建議在那篇量的路徑上成立。換成 NCCL 之後,tensor 的 prefill 從 800 上到 917,跟 layer split 的 944.8 已經很接近——這個取捨基本上被抹平了。
換句話說,上一篇那句話的有效範圍,就是它自己宣告的那個範圍。
而 TTFT 之所以有 12–13%,是因為首字之前要先把整個 prompt 推過一遍——那本質上是一次 prefill。
這篇要修正的事
part 14 的 FAQ 裡我寫了「目前判定是 NO-GO」。那是錯的,而且錯的方式值得記下來:當時沒有留下錯誤訊息,就把「試不起來」寫成了「不能用」。
從「試不起來」到「不能用」中間差了一整個診斷,而我跳過了。
進階
底下是完整的診斷過程、被打掉的假說,以及一件跟 NCCL 無關但值得知道的事。不讀不影響你照著上面修。
五個假說,一個一個死掉
診斷花的時間比修久,而且大部分花在推翻自己。
假說一:bf16 在 Turing 上不支援。 llama.cpp 的大張量 AllReduce 會先壓成 bf16 再送,而 Turing 沒有原生 bf16。聽起來很合理。
被推翻的原因:探針裡 fp32 那條也一樣掛。跟資料型別無關。
假說二:那顆套件沒編 sm_75。
cuobjdump 拆開 Ubuntu 的 libnccl.so.2,架構清單是 sm_50 sm_60 sm_61 sm_70 sm_80 sm_90——沒有 sm_75,而且 --list-ptx 是空的(沒有 PTX 可以即時編譯補上)。看起來像鐵證。
被推翻的原因:CUDA 的二進位相容規則是「為 X.y 編的 cubin 可以在 X.z(z ≥ y)上執行」。sm_70 是 7.0,2080 Ti 是 7.5,同一個主版本、次版本更高——sm_70 的 kernel 本來就該能跑。
這條是被別人一句話點醒的:「都有硬體了,怎麼會不能編?」對。我把一個觀察(沒有 sm_75)直接當成了因果(所以跑不動),中間那步從來沒驗過。
假說三:驅動壞了。 被推翻的原因:直接呼叫 driver API 全部正常。
假說四:P2P / NVLink 不通。 被推翻的原因:cudaDeviceCanAccessPeer 回 1,而且 NCCL 自己的 log 顯示四條 channel 全部用 via P2P/direct pointer 建立成功。
假說五:環境變數繞得掉。 試了 NCCL_CUMEM_ENABLE=0、NCCL_P2P_DISABLE=1、兩者併用、NCCL_GRAPH_MIXING_SUPPORT=0、NCCL_LAUNCH_MODE=PARALLEL。五種全滅,失敗位置一模一樣。
到這裡才確定是版本相容性,而不是任何一種「設定不對」。
一件跟 NCCL 無關,但影響雙卡的事
翻 llama.cpp 的 CUDA 後端時看到這段:
if (!use_cuda_graph || ggml_backend_cuda_get_device_count() != 1) {
return;
}
只要裝置數不是 1,CUDA graph 的最佳化整段跳過。
也就是說,開雙卡等於放棄 CUDA graph——那是一筆跟 AllReduce 完全無關的固定成本,兩條路都一樣付。這大概也說明了為什麼雙卡的 tok/s 成長,總是達不到「兩張卡應該快兩倍」的預期。
沒有隔離出來的變因
老實說:我同時換了版本(2.22.3 → 2.31.2)和 gencode(沒有 sm_75 → 有)。沒有隔離出是哪一個修好的。
間接證據偏向版本:錯誤是 CUDA driver is a stub library(driver API 相容性),不是 no kernel image is available(架構缺失)。而且按上面那條 CUDA 相容規則,sm_70 本來就能在 7.5 上跑。
但我沒有真的去編一份「2.22.3 帶 sm_75」來驗。所以這篇證明的是「換新版就好了」,不是「舊版壞在哪一行」。
完整量測條件
同一台 EPYC + 兩張改裝 2080 Ti 22G(GPU0/GPU1,PCIe Gen3 x16),llama.cpp b10064。
PCIe 流量與 prefill/decode 吞吐那組:Huihui Qwen3.8-27B abliterated Q4_K、-sm tensor、262K context、--parallel 2、MTP n=3、KV f16,各 3 rep。
code tok/s 與 TTFT 那組:實際在服務的組態——orcarouter Qwen3.8-27B Q8_0、-ctk q8_0 -ctv q8_0、其餘同上,code 各 5 rep、TTFT 各 7 rep。
⚠️ 這兩組模型不同(Q4 對 Q8_0),數字不能互相對照。 前者是為了對齊 part 14 的已發表基準,後者是為了回答「我平常在用的那個到底有沒有變快」。
TTFT 是用串流量的:從送出請求到第一個含 content 的 chunk 抵達。不是 timings 裡那些跑完才算得出來的數字——那些量不到你盯著螢幕等的那段。
這篇的收穫
- 沒有錯誤訊息就不要下判決。 「試不起來」和「不能用」中間差一整個診斷。我在 part 14 跳過了,結果那句話在網站上掛了一整天。
- 觀察不等於因果。 「這顆庫沒編 sm_75」是真的;「所以它跑不動」是我加上去的,而且是錯的。
- 流量掉 99% 而速度沒動,本身就是結論。 它證明了瓶頸不在那裡——這比修好之後快 15% 更有用,因為它會擋下一整批「換條更快的線就好了」的錯誤建議。
同系列其他文章:Part 15:讀者問雙卡 AllReduce 吃多少 PCIe · Part 14:兩張改裝 2080 Ti 玩 tensor parallel
常見問題
- NCCL 在 RTX 2080 Ti 上到底能不能用?
- 能。我先前判它是死路,那是錯的。Ubuntu 的 `libnccl2 2.22.3` 跟 CUDA 13 驅動(580.x)不相容,第一次呼叫 collective 就回 `CUDA driver is a stub library`。自己編一份新版(2.31.2-1)並在 gencode 帶上 `compute_75`,fp32 與 bf16 的 AllReduce 都會過。
- 自編 NCCL 之後要重編 llama.cpp 嗎?
- 不用。llama-server 是動態連結 `libnccl.so.2`,把自編那份的路徑掛在 `LD_LIBRARY_PATH` 就會換過去。裝在自己家目錄、不動系統那顆,要退回去就是拿掉那行環境變數。
- 從 PCIe fallback 換成 NCCL,速度差多少?
- 比想像中少。實測同一台雙 2080 Ti:寫 code 的 tok/s 兩者打平(56.2 對 55.7,範圍重疊);長 context 的首字延遲快 13%;prefill 吞吐快 14.6%。但 prefill 的 PCIe 流量掉了 99%——流量幾乎全部消失,速度卻只動這麼點。
接著讀
- 2026-08-23[Benchmark] 讀者問雙卡 AllReduce 吃多少 PCIe:只佔 8%,因為搬得少、跑得勤
有人來信要我在跑雙 2080 Ti tensor parallel 時開 nvidia-smi dmon -s t 看 PCIe 頻寬。實測 AllReduce 吐字時只用掉不到 1%、prefill 約 6.6%,最忙的一格是 8%——瓶頸不是頻寬是延遲。
- 2026-08-22[Benchmark] 兩張改裝 2080 Ti 玩 tensor parallel,Qwen3.8-27B 衝到 59.6 tok/s
兩張改裝 2080 Ti 22G 用 llama.cpp 的 tensor parallel 接上 Qwen3.8-27B,配 MTP 投機解碼從單卡 37.1 tok/s 衝到雙卡 59.632 tok/s,還能開到 262K context。內含現在正式在跑的組態、三個上游踩坑,和雙 5060 Ti 社群數字的對照。
- 2026-08-21[Benchmark] 換一個檔案、改一個數字,2080 Ti 上的 27B 免費快 15%
Qwen3.8-27B 在 2080 Ti 的免費加速實測:修復版 chat template 讓 KV 快取命中 94.5%,MTP 草稿深度 2 開到 4 讓 decode 從 41.6 到 47.6 tok/s。附完整驗證方法。
- 2026-08-20[趣味競賽 進階 #12] 為什麼你的 4-bit 模型在 2080 Ti 上快不起來?——拆開 .so 才發現這代卡只有兩種齒輪
為什麼 GGUF 量化後檔案變小,tok/s 卻沒變快?拆開 CUDA 後端 .so 才看懂:Turing(sm_75)的整數 tensor core 只有 s4×s4 跟 s8×s8 兩種同寬組合,4-bit 權重配 fp16 activation 這格在任何架構都不存在。GGUF、AWQ、Marlin 全靠反量化撐過去。
不想錯過新文章?
訂閱我確保不漏接!
隨時一鍵退訂。