很多人在地端跑大型 AI 模型時,都會遇到一個很實際的問題,模型到底應該放在本機 NVMe SSD,還是放在 NAS好呢?這個問題看起來只要比一下讀取速度就有答案,直覺答案通常是:「當然放本機,SSD 比網路快。」,但 CyberQ 實際測試之後,得到的結論和一開始的直覺幾乎完全相反。同一台 NVIDIA DGX Spark GB10、同一條 10GbE 網路,換不同模型與不同模型載入程式之後,結果甚至會完全相反。
本篇的重點摘要如下,詳細的說明後述 :
你的 NVMe 很可能只跑了三分之一的速度,因為 Linux 的 readahead 預設值是 128 KB。改成 1 MB,循序讀取從 2206 MB/s 變成 6178 MB/s。
nconnect=8 出現在 mount 輸出裡,不代表它生效了,而且在這次測試的兩台 NAS 上實測都並沒有發生顯著的效果,原因後述。
同一台機器,兩個模型,結論卻完全相反。 29 GB 的模型用 vLLM 載入,把儲存拉快 2.3 倍,載入時間紋風不動。87 GB 的模型用 ds4-server 載入,放 NAS 比放本機慢 3.8 倍。決定你屬於哪一邊的不是儲存,是 loader 的效率和你選用的模型大小能否一次放得進記憶體。
以下是 CyberQ 這次的實作與測試資料。
測試環境
運算節點:NVIDIA DGX Spark GB10,128 GB 統一記憶體,本機 1 TB NVMe,10GbE 網路
NAS A:QNAP TS-855X,內建 10GbE 卡,64 GB RAM
NAS B:QNAP TS-464,加裝 10GbE 卡,16 GB RAM
測試資料一:Qwen3.8-27B 的 FP8 版本,29 GB,66 個 safetensors 分片,vLLM(無 MTP,max-model-len 32768)
測試資料二:DeepSeek-V4-Flash,86.7 GB,單一 GGUF,ds4-server(llama.cpp 系的原生 binary)
| 測試模型 | 大小 | 檔案形式 | 載入程式 |
|---|---|---|---|
| Qwen3.8-27B FP8 | 29 GB | 66 個 safetensors | vLLM |
| DeepSeek-V4-Flash | 86.7 GB | 單一 GGUF | ds4-server |
兩份資料的差異是刻意的,一個放得進記憶體、分片多、loader 慢,一個放不進記憶體、單一大檔、loader 快。結論剛好相反,而這正是本文的重點。
每次量測前都 sync; echo 3 > /proc/sys/vm/drop_caches,把本機的 page cache 清乾淨。
先來看最直觀的結果:本機 SSD 確實快很多
首先 CyberQ 先測把 29 GB 資料讀完需要多少時間。
| 儲存位置 | 最高實測速度 | 讀完 29 GB |
| 本機 NVMe | 6178 MB/s | 約 5 秒 |
| TS-855X,10GbE | 1172 MB/s | 約 26 秒 |
| TS-464,10GbE | 556 MB/s | 約 56 秒 |
這裡其實很好理解,10GbE 的理論上限大約是 1250 MB/s。TS-855X 實測 1172 MB/s,已經達到理論值約 94%。
所以這時候不是 NAS 不夠快,而是 10GbE 本身已經接近極限。TS-464 則只有約 556 MB/s,這次測試主要受到 NAS 裡 RAID5 硬碟陣列的讀取能力限制。
也就是說,同樣叫做「NAS」,瓶頸可能完全不同。可能是網路,也可能是硬碟,也可能是 NAS 本身的處理能力。
一、Linux 有一個小設定,讓 NVMe 快了 2.8 倍
Linux 對區塊裝置的預設 readahead 是 128 KB。這個值對一般工作負載沒問題,但對一次讀完 29 GB 大檔這種模式來說太小了。先修好 readahead,否則你量的是預設值而不是硬體。簡單講,readahead 就是 Linux 預測你接下來還會讀資料,所以提前幫你讀進來。
$ cat /sys/block/nvme0n1/queue/read_ahead_kb 128 $ echo 1024 | sudo tee /sys/block/nvme0n1/queue/read_ahead_kb
同一顆碟、同一份檔案、只改這一個值:
| readahead | cat 到 /dev/null | mmap 逐頁碰觸 |
|---|---|---|
| 128 KB | 2206 MB/s | 1506 MB/s |
| 1 MB | 6178 MB/s | — |
| 8 MB | — | 5235 MB/s |
2.8 倍。 讀完 29 GB 從 14 秒變成 5 秒。
再往上(8 MB、15 MB)增益就遞減了,所以我取 1 MB 這個轉折點,畢竟過大的 readahead 會讓隨機讀取白做工,還多佔 page cache。
mmap 那一欄值得特別注意,safetensors 是用 mmap 讀的,而 mmap 的缺頁路徑比 read(2) 慢一截,拉大 readahead 之後同樣受惠。要模擬模型載入的存取模式,量 mmap 比量 dd 或 cat 有代表性。
這個值重開機會還原,用 udev rule 固定,/etc/udev/rules.d/60-nvme-readahead.rules:
ACTION==”add|change”, SUBSYSTEM==”block”, KERNEL==”nvme[0-9]n[0-9]”, ATTR{queue/read_ahead_kb}=”1024″
不用重開機就能驗:
sudo udevadm control –reload sudo udevadm trigger –subsystem-match=block –action=change cat /sys/block/nvme0n1/queue/read_ahead_kb # 應該變成 1024
做任何儲存對比之前,先確認這個值,不然比的只是誰的預設值比較好而已。
二、NFS 掛載參數,以及一個很容易踩到的坑
QNAP 端在「控制台 → Win/Mac/NFS」啟用 NFS 服務,對共享資料夾設定存取權限,限制只允許內網運算節點的 IP。
運算節點端,用 systemd 的 mount unit,/etc/systemd/system/mnt-nas.mount:
[Mount] What=192.168.2.2:/Public Where=/mnt/nas Type=nfs Options=rw,hard,nconnect=8,timeo=600,retrans=2,noatime,nofail,vers=4.1 TimeoutSec=60
寫成 fstab 也一樣:
192.168.2.2:/Public /mnt/models nfs rw,noatime,vers=4.1,nconnect=8,hard,timeo=600,retrans=2,_netdev 0 0
設定值說明:
rsize/wsize 是跟 NAS 協商出來的,這台協商結果本來就是上限 1 MiB,寫不寫進去沒差別。
noatime 省掉讀取時回寫存取時間。
hard 確保 NAS 短暫離線時 IO 等待重試而不是回錯。載入 safetensors 時 soft 回錯會讀到半截權重,比卡住更危險。
timeo=600 的單位是十分之一秒,也就是 60 秒。很多範例寫的 timeo=30 其實只有 3 秒。
mount 輸出裡有 nconnect,不代表它生效
Linux NFS 客戶端對同一台 server 共用同一個傳輸層,nconnect 只在第一個建立該傳輸層的掛載上生效,之後掛同一台 server 的掛載點會被靜默忽略,連選項字串裡都不會出現。
所以驗證要數 TCP 連線,不能看 mount 輸出:
$ findmnt -no OPTIONS /mnt/nas | tr ‘,’ ‘\n’ | grep nconnect nconnect=8 $ ss -tn ‘dst 192.168.0.5:2049’ | tail -n +2 | wc -l 8
回傳 8 才算真的開起來。如果是 1,先用 mount | grep 192.168.2.2 找有沒有第二個掛到同一台 NAS 的掛載點,卸載多餘的,再重掛正式那個。
但掛兩台 NAS 不會踩到這個坑
這一點很容易誤讀成掛越多越危險。並不是這樣滴,成立條件是同一台 server。Linux 是以 server 為單位共用傳輸層,兩台不同的 NAS 各自建立自己的傳輸層,nconnect 各算各的,互不干擾。
同一台運算節點同時掛兩台 NAS 的實測:
$ findmnt -t nfs4 TARGET SOURCE OPTIONS /mnt/nas 192.168.2.2:/Public …,nconnect=8,timeo=600,… /mnt/models 192.168.2.22:/Public …,nconnect=8,timeo=600,… $ ss -tn ‘dst 192.168.2.2:2049’ | tail -n +2 | wc -l 8 $ ss -tn ‘dst 192.168.2.22:2049’ | tail -n +2 | wc -l 8
兩邊各自 8 條。所以判斷標準不看掛了幾個掛載點,是同一個 IP 掛載了幾次。
三、三個地方,三個完全不同的瓶頸
| 儲存 | 最佳吞吐 | 讀完 29 GB | 瓶頸在哪一層 |
|---|---|---|---|
| 本機 NVMe | 6178 MB/s | 5 秒 | 核心的 readahead 預設值 |
| TS-855X(內建 10GbE) | 1172 MB/s | 26 秒 | 10GbE 線路本身 |
| TS-464 (10GbE 卡) | 556 MB/s | 56 秒 | NAS 自己的碟陣列 |
每一層的瓶頸不同,代表你該調哪個設定值也不同,這是實務上是實用的。
QNAP TS-855X 有 64 GB RAM,29 GB 的模型整份塞得進它的快取。drop_caches 只清運算節點自己的 page cache,清不掉 NAS 的,所以那 1172 MB/s 是「NAS 的 RAM 經過網路」的速度,此時硬碟的讀取被排除在外,量到的是網路路徑的上限。而 1172 MB/s 已經是 10GbE 理論值 1250 MB/s 的 94%。
另一台較小的 NAS TS-464 這邊沒有這個問題,它的記憶體吃不下 29 GB,所以量到的接近真實碟速。556 MB/s 約等於 4.4 Gbps,遠高於 2.5GbE 的上限,所以不是網路卡住,是 4 顆 HDD 組 RAID5 的循序讀取到頂了。另外,寫入cp -a 把 29 GB 複製過去花了 279 秒,110 MB/s,這是 RAID5 的寫入代價。
四、nconnect 到底要不要開?
在 TS-855X 上把 nconnect 和 readahead 兩個都掃過:
| nconnect | ra=128 KB | ra=15 MB |
|---|---|---|
| 1 | 1061 MB/s | 1172 MB/s |
| 8 | 922 MB/s | 1168 MB/s |
readahead 拉滿之後,1 條連線 1172 MB/s,8 條連線 1168 MB/s,差 0.3%。 表格左邊那欄的差距不要當真,同條件跨時段重測的雜訊有 ±15%,那個差距在雜訊範圍內。
TS-464 上同樣掃了六格(nconnect 1/8 × readahead 128 KB/1 MB/15 MB),全部落在 539–556 MB/s。
所以測試結果顯示,nconnect 要有效,前提是單一連線還沒把瓶頸吃滿。 這兩台 NAS 都不符合,TS-855X 的單一連線已經跑到線路的 85%,TS-464 的單一連線則已經到它硬碟速度上限。
所以 CyberQ 建議 nconnect 可以開,成本很低也不會變慢,但一定要自己量。真正該先問的是瓶頸在哪一層,它能幫上忙的是單一連線受限於延遲或 CPU、而線路還有餘裕的場景,例如高延遲鏈路,或 NAS 端單執行緒處理不過來的時候。
順帶一提,NFS 的 readahead 不在 mount option 裡,而在 bdi,而且裝置編號每次掛載都會變:
$ cat /proc/fs/nfsfs/volumes # 取 DEV 欄,例如 0:86 $ cat /sys/class/bdi/0:86/read_ahead_kb 128
要固定的話得綁一個 oneshot service 在 mount unit 上:
[Unit] Description=Raise NFS readahead for /mnt/nas After=mnt-nas.mount BindsTo=mnt-nas.mount [Service] Type=oneshot RemainAfterExit=yes ExecStart=/bin/sh -c ‘echo 15360 > /sys/class/bdi/$(findmnt -no MAJ:MIN -t nfs,nfs4 /mnt/nas | tail -1 | tr -d “[:space:]”)/read_ahead_kb’ [Install] WantedBy=mnt-nas.mount
五、最重要的一件事:吞吐變快,載入時間沒有變
前面那些數字很好看,但真正要回答的問題是模型載入會不會比較快呢,很抱歉,答案是不會。
同一台機器、同一份權重、同一組 vLLM 設定,只改 readahead:
| readahead 128 KB | readahead 1 MB | |
|---|---|---|
| 純循序讀取 | 2206 MB/s | 5148 MB/s |
vLLM Loading weights took(第一輪) | 193.2 秒 | 191.8 秒 |
vLLM Loading weights took(第二輪) | 246.4 秒 | 218.5 秒 |
讀取能力提升 2.3 倍,載入時間在雜訊範圍內沒有動。(同一組設定連跑兩輪自己就會差 14%,所以第二輪那個差別不算數。)
原因從數字就看得出來:29 GB 分成 66 個分片,每片約 468 MB,載入時每片花 2.9 秒,換算 161 MB/s。而這顆碟能給 5148 MB/s,vLLM 只用掉了 3%。瓶頸在載入路徑本身,包括 safetensors 反序列化、FP8 處理、把張量搬上 GPU,這些都不是儲存能加速的。
模型該放哪裡的影響很大,在這個組合下,如果你的痛點是「服務重啟太慢」,把模型從 NAS 搬到本機 NVMe 不會解決問題。 真正該量的是 Loading weights took 這一行,不是 dd 或 cat 的數字。
但這個結論有適用範圍,換一個 loader、換一個大小級距,它會整個反過來。下一節就是 CyberQ 這次測試的實證。
六、換一個 loader 和更大的模型,結論完全相反
前面五節都是 Qwen3.8-27B(29 GB、66 個 safetensors 分片、vLLM)。如果我們把同一台機器、同一條網路的環境,將 LLM 換成 DeepSeek-V4-Flash(86.7 GB、單一 GGUF、ds4-server 這個 llama.cpp 系的原生 binary),結論整個翻過來。
ds4-server 的 log 剛好把載入拆成兩段,這是 vLLM 給不了的:
ds4: warmed tensor pages in 19.930s ← 純儲存(mmap 讀完整份 GGUF) ds4: q2k aligned repack base: … in 7.2s ← 純 CPU(threads=6) ds4: iq2 aligned repack base: … in 44.0s ← 純 CPU
readahead 在這裡效果巨大
同一份 log 的歷史紀錄,剛好橫跨這次改 readahead 的前後:
| readahead | warmed tensor pages | 換算 |
|---|---|---|
| 128 KB(六次) | 53.5 / 55.5 / 55.5 / 56.4 / 57.0 / 57.6 秒 | 1506–1621 MB/s |
| 1 MB(三次) | 19.2 / 19.7 / 19.9 秒 | 4351–4517 MB/s |
2.8 倍,直接省下 36 秒。 同一個 readahead 改動,對 vLLM 完全沒效果,對 ds4-server 效果驚人。差別在這裡:
| loader | 載入時的實際吞吐 | 用掉這顆碟的 |
|---|---|---|
| vLLM(safetensors) | 161 MB/s | 3% |
| ds4-server(GGUF mmap) | 4351 MB/s | 73% |
一個受儲存限制,一個不受。這才是決定「調儲存有沒有用」的真正變數,不是儲存本身有多快。
本機 vs NAS 在大檔案模型載入測試中差 3.8 倍
| 階段 | 本機 NVMe | NAS (NFS) | 倍數 |
|---|---|---|---|
| warmed tensor pages(儲存) | 19.93 秒 / 4351 MB/s | 186.09 秒 / 466 MB/s | 9.3× |
| repack q2k(CPU) | 7.2 秒 | 67.1 秒 | 9.3× |
| repack iq2(CPU) | 44.0 秒 | 104.4 秒 | 2.4× |
| repack q8(CPU) | 6.2 秒 | 11.3 秒 | 1.8× |
| CUDA 準備 | 11.6 秒 | 9.4 秒 | — |
| 總計到可服務 | 100.8 秒 | 380.6 秒 | 3.8× |
簡單說明如下 :
一、連「純 CPU」的階段都爆掉了。 repack 是 threads=6 的 CPU 工作,理論上不該隨儲存位置改變,但 q2k 從 7.2 秒變成 67.1 秒。CPU 沒有變慢,是 repack 在讀取 mmap 時的缺頁跑到網路上去了。儲存階段只解釋了 166 秒的落差,剩下 114 秒出現在名義上與儲存無關的階段。
二、466 MB/s 遠低於 10GbE 的 1170 MB/s。 因為 mmap 的缺頁走 NFS 比循序 read() 慢得多,這和第一節量到的「NAS 上 mmap 346 MB/s vs read(2) 834 MB/s」是同一件事。用 dd 量出來的網路吞吐,不代表 mmap 型的 loader 拿得到。
三、根本原因是模型放不進記憶體。 這台記憶體 128 GB,模型就佔了 80.76 GiB,repack 產生的對齊副本另外要 78.7 GiB,合計 159 GiB。ds4-server 自己在 log 裡講了:leaving the 80.76 GiB model mmap unpinned,它知道放不下,所以讓 mmap 頁面可以被回收。本機上被回收再讀回來是 4.3 GB/s 的事,NFS 上是 466 MB/s、而且是隨機缺頁。
所以「模型放 NAS」真正的風險不是初次載入慢,而是整個載入過程中每一次重新觸碰都要走網路。模型越接近記憶體容量,這個效應越可怕。
怎麼判斷你屬於哪一邊
兩個問題就能分出來:
你的 loader 吃掉多少頻寬? 用「權重總位元組 ÷ 載入秒數」和 cat 量到的裝置吞吐比。差一個數量級(像 vLLM 的 3%)→ 調儲存不會有感。同一個數量等級(像 ds4 的 73%)→ 調儲存直接反映在啟動時間上。
模型 + 執行期副本在現有的記憶體放得下嗎? 如果能放得下,NFS 只是初次載入慢一點,放不下,NFS 會在整個載入過程中持續付網路成本,代價遠超過你的頻寬計算。
七、vLLM 的一個隱藏行為:只對網路檔案系統自動 prefetch
追這件事的時候,在 vLLM 的 log 裡看到這行:
Filesystem type for checkpoints: EXT4. Checkpoint size: 28.75 GiB. Available RAM: 83.89 GiB. Auto-prefetch is disabled because the filesystem (EXT4) is not a recognized network FS (NFS/Lustre). If you want to force prefetching, start vLLM with –safetensors-load-strategy=prefetch
原始碼(vllm/model_executor/model_loader/weight_utils.py)的判斷是:
is_net_fs = fs_type in (“nfs”, “nfs4”, “lustre”) … if is_net_fs and fits_in_ram: should_prefetch = True
prefetch 做的事是用執行緒池把整份權重先 read() 進 page cache,之後 mmap 就不用等 I/O。vLLM 只在檔案系統是 NFS/Lustre、且權重塞得進 RAM 時才自動啟用,本機 ext4 一律不開。
看起來像是個可以撿的免費加速,CyberQ 實測後發現:
| 本機 NVMe | 第一輪 | 第二輪 |
|---|---|---|
| 預設(不 prefetch) | 191.8 秒 | 218.5 秒 |
--safetensors-load-strategy=prefetch | 190.9 秒 | 238.2 秒 |
也沒有用。^_^ 這其實和上一節同一個道理,prefetch 只能省掉 I/O 等待,而本機的 I/O 本來就只佔載入時間的 3%。log 顯示 prefetch 本身只花了 6.35 秒就把 29 GB 讀進 page cache,然後載入還是花了 190 秒。
如果你的權重放在 NAS 上、而且 vLLM 版本較舊沒有這個自動判斷,那手動加這個旗標是有意義的,放本機的話省不到。
八、儲存位置最多值 11%,剩下的還沒解開
為了把「資料在不在 RAM」和「頁面的後端是誰」分開,把同一份權重放進 /dev/shm(tmpfs,純 RAM,沒有任何檔案系統後端)再跑一輪。四個第一輪的數字會呈現出一條乾淨的階梯:
| 權重位置 | 每片(共 66 片) | Loading weights took | 載入時資料在哪 |
|---|---|---|---|
| NAS(NFS4,自動 prefetch) | 1.82 秒 | 120.5 秒 | 本機 RAM(page cache) |
tmpfs(/dev/shm) | 2.56 秒 | 169.7 秒 | 本機 RAM(無檔案系統後端) |
| 本機 ext4 + 強制 prefetch | 2.89 秒 | 190.9 秒 | 本機 RAM(page cache) |
| 本機 ext4(預設 mmap) | 2.90 秒 | 191.8 秒 | NVMe |
這條階梯拆出兩個獨立的結論。
一、儲存位置最多值 11%。 tmpfs 是純 RAM、連檔案系統後端都沒有,是這條路徑的理論上限,它只比本機 ext4 快 11%(190.9 → 169.7 秒)。換句話說,「把權重搬到更快的儲存」能獲得的極限就是 11%,而且已經需要把 29 GB 常駐在記憶體裡才拿得到。這也回頭強化了第五節的結論。
二、NAS 比純 RAM 還快 29%(120.5 vs 169.7 秒)。比 RAM 還快,就代表這 29% 根本不是「資料放在哪裡」造成的,這是還沒解開的謎題。
已排除的清單:熱節流(四輪的 torch.compile 時間只差 3%,NVMe 全程 47–51°C)、原始頻寬、readahead、mmap 缺頁路徑、記憶體壓力(載入時有 83 GiB 可用,KV cache 是載入完成後才配置的)、引擎設定差異(兩邊的 non-default argsdiff 過完全相同),以及 prefetch 旗標本身,在 ext4 上強制打開完全沒有效果。
剩下的唯一差別是 vLLM 對 NFS 走了不同的程式碼分支。但那個分支做的事(把檔案先讀進 page cache)對 tmpfs 來說等於免費,tmpfs 卻沒有拿到同樣的好處。所以「NFS 分支比較快」也還不是答案。
在這個奇怪的問題解開之前,請不要把「NAS 載入比較快」當成選型依據,它比純 RAM 還快,這件事本身就說明在我們的實驗中,真正的變因還沒被找出來。
九、實務建議
透過 udev rule 將 NVMe 裝置的 readahead 調整為 1 MB 是成本極低且效益顯著的改動,能直接改善支援高效讀取的載入程式。
啟用 NFS 的 nconnect 參數前,應先確認單一連線是否已佔滿網路或硬碟頻寬,並在設定後透過檢查 TCP 實際連線數確認設定已生效。
在決定是否將模型移至本機前,應先以「模型位元組大小除以載入秒數」計算載入程式的實際吞吐量,並與硬碟讀取頻寬比較。若頻寬使用率極低(如 vLLM 的 3%),調整儲存設備無法顯著改善啟動時間;若頻寬使用率極高(如 ds4-server 的 73%),儲存效能將直接決定服務啟動速度。
當模型檔案加上執行階段所需的記憶體超出實體記憶體總量時,切勿將模型放置於網路儲存。記憶體不足導致的頁面回收與網路隨機重新讀取,會讓包含 CPU 運算在內的整體載入時間暴增數倍。
評估模型載入效能時,應直接量測載入程式回報的日誌時間(例如 Loading weights took 或 warmed tensor pages),避免單純依賴 dd 或 cat 等指令的吞吐數字,兩者與實際模型載入耗時往往缺乏直接關聯。
附錄:怎麼自己量
純讀取吞吐(清 cache 後讀完所有權重檔):
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches time find /path/to/model -name ‘*.safetensors’ -print0 | xargs -0 cat > /dev/null
模擬 safetensors 的 mmap 存取模式:
import mmap, os, sys, time t0 = time.time(); total = 0 for path in sys.argv[1:]: size = os.path.getsize(path) with open(path, ‘rb’) as f: mm = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) for off in range(0, size, 4096): # 每頁碰一個 byte,逼出缺頁 total += mm[off] mm.close() print(f”{time.time() – t0:.1f}s”)
vLLM 的實際載入時間,直接從 log 抓:
grep -oE ‘Loading weights took [0-9.]+ seconds’ vllm.log









