iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
IT Operation

地端機房的三十天維運:開源服務封裝、GPU 節點守護與可稽核的變更管理系列 第 7

Day 7|在地自建 ComfyUI 的生存指南:自建 Image 鎖定、GPU 直通與三層目錄架構

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260921/20141816ktEhJ2uTA5.png

前情提要:前幾天我們討論的容器套件多半是「薄殼(Thin Wrapper)」——套件本體只放管理腳本,核心映像檔(Image)直接從 Docker Registry 拉取即可。
但今天的主角 ComfyUI 完全顛覆了這個模式,它沒有上游官方現成 Image,還牽涉到底層硬體指令集與 NVIDIA 驅動相依性,是很多人在自己的電腦、伺服器或 NAS 上建構會碰到問題的熱門地端 AI 服務。本文將帶你走過一趟從地端建置、GPU 直通除錯到極限記憶體調校的完整實戰。


前言:為什麼 ComfyUI 不能只做「薄殼」?

在 NAS 上打包開源工具,最理想的做法就是寫個腳本,docker pull 現成 Image 就收工。然而,在以 qnap-comfyui-qpkg 為案例實作時,會發現有很多現實上的困難:

  1. 官方並未提供統一的標準映像檔:而目前 ComfyUI 社群提供的映像檔,內部綁定的 PyTorch CUDA 版本往往是固定的。
  2. CUDA 驅動版本向下相容限制:不同伺服器、電腦要跑這個,以及每台 NAS 安裝的 QNAP NVIDIA GPU Driver 套件版本都不一定相同,版本差異是有的,而 PyTorch Wheel 要求的 CUDA 版本絕對不能高於主機驅動所支援的上限。
  3. 低功耗 CPU 的指令集地雷:部分 QNAP 機種使用的節能型 x86 CPU 不支援 AVX2 指令集。自 kornia_rs 0.1.13 版後,若在不支援 AVX2 的 CPU 上直接載入其預編譯 Wheel,會直接拋出 Illegal instruction(核心崩潰)。這個問題必須在建置當下讀取 /proc/cpuinfo 才能對症下藥。

這兩個致命條件都指向同一個事實,「每台機器的環境都不一樣」。專案作者唯一的解法,就是在 NAS 目的地本機建置(Local Build)出約 9 GB 的映像檔。

然而,地端建置直接打破了 Day 3 所建立的**版本鎖定(Digest Pinning)**體系。薄殼套件可以用 image@sha256:... 做到 100% 可重現,但自建 Image 映像檔如果沒有管理輸入源,今天建的檔案,跟下個月建出來的東西可能完全不同。

因此我這次的討論,將聚焦在這三件事上,在容器當道的情況下,伺服器、個人電腦與 NAS 去應用容器來實現服務是一條殊途同歸的路:

  • 自建 Image 的防線:如何鎖定看似不可控的 4 個外部版本來源?
  • GPU 直通三種姿勢:NAS 與 Container Station 之間的 GPU 注入機制到底差在哪?
  • 三層資料目錄分離:如何做到「升級 ComfyUI 不必重建 9 GB 映像檔,重建映像檔不必重新下載數十 GB 模型」?

快速體檢:以 Day 2~4 標準檢視專案體質

在拆解架構前,我們先為 qnap-comfyui-qpkg(v0.35.1-2)做一次架構體檢:

檢查項目 實作現況 架構評估與改進方向
套件名稱 (QPKG_NAME) ComfyUI 使用了上游名稱,若官方推出同名套件會被無預警覆蓋。遷移時建議比照改為 ComfyUIDocker
版本編號 (QPKG_VER) 0.35.1 (build -2) 與上游軟體版本同步,-2 代表包裝修訂版。
相依與啟動順序 container-station >= 3.0, NVIDIA_GPU_DRV 設定 QPKG_RC_NUM=199,確保在容器引擎與 GPU 驅動載入完成後才啟動。
超時寬容 (TIMEOUT) 1800,300 給予 30 分鐘建置寬限期,因首次建置需編譯並下載 PyTorch,停止寬限 5 分鐘。
開機競爭防護 專屬 Preflight 檢查 最多等待 600 秒,確保 Docker、/dev/nvidia0 與 Runtime 三者皆就緒才起跑。
維護機制 內建 upgraderollback 全系列重點!支援背景原子切換與自動健康檢查回滾。
供應鏈安全性 無 CI 自動化簽核 映像檔未發布 SHA256SUMS 與 Attestation,且自建 Image 的 4 個版本來源原本僅鎖了 1 個。

小結:這個專案在執行期(Runtime)的韌性極高,不僅考慮了開機時 NVIDIA 核心模組載入延遲,還具備高可用升級回滾機制,但其脆弱點在於軟體供應鏈的可重現性


一、自建 Image 的供應鏈防守:4 個版本來源的鎖定策略

Day 3 我們談過,Registry 上的 Digest 是告訴我們「這個已完成的 Image 內容是什麼」,而自建 Image 面臨的問題是:「這個 Image 到底是用哪些原料混出來的?」

攤開專案的 Dockerfile,建置輸入實際上來自 4 個管道:

[1. 基底映像檔] python:3.12-slim-bookworm (浮動 Tag)
[2. 核心運算庫] PyTorch Wheel (cu128 / cu121...)
[3. 應用相依庫] ComfyUI requirements.txt (內含未鎖定版本的傳遞相依)
[4. 主機硬體特徵] /proc/cpuinfo (AVX2 指令集支援與否)
       │
       ▼
 ┌───────────────┐
 │ Docker Build  │ ──> 產生 9 GB 的本機 Image
 └───────────────┘

以下是這 4 個來源在 v0.35.1-2 版本的鎖定現況與破口:

來源 原本寫法 鎖定狀態 潛在風險
1. 基底 Image FROM python:3.12-slim-bookworm 浮動 Tag Debian 與 Python 的小修補版會隨時間飄移。
2. PyTorch 三件組 torch==2.9.1+cu128 版本+Wheel 鎖定 不變,Wheel 檔案具備不可變性。
3. ComfyUI 依賴 ADD .../${COMFY_REF}/requirements.txt 鎖定 Git Tag 最大破口。35 個套件中僅 5 個釘死版本,其餘皆為 >= 或裸名稱,每次 build 都會解析出最新版。
4. 建置主機硬體 容器內執行 grep avx2 /proc/cpuinfo 依機器動態決定 讓建置過程具有隱式副作用(Side Effect)。

如何全面封堵這 4 個漏洞?

1. 基底 Image 釘死 Digest

不再只依賴 python:3.12-slim-bookworm,而是比照 Day 3 加入 SHA256 Digest,寫入 images.lock

FROM python:3.12-slim-bookworm@sha256:d5b839...

2. ComfyUI 原始碼改綁 Commit SHA

Git Tag(例如 v0.35.1)是可以被強制移動或重新打標的。GitHub Codeload 與 Raw 網址都支援直接使用 Commit SHA 下載 Tarball。

  • .env 中拆分為:
    • COMFY_REF=v0.35.1(供人類辨識、目錄命名與版本回報使用)
    • COMFY_SHA=856a922befab9d94cb66f36a3dce17234d7a6e31(供實際下載與驗證使用)

小技巧:在 NAS 上無需安裝 jq,利用 GitHub API 帶上特製 Header,就能直接把 Tag 轉成 Commit SHA:

/sbin/curl -fsSL -H 'Accept: application/vnd.github.sha' \
  https://api.github.com/repos/Comfy-Org/ComfyUI/commits/v0.35.1

3. 傳遞相依導入 constraints 鎖定檔

kornia_rs 之所以會無預警升級並引發 AVX2 崩潰,就是因為它屬於未被釘死的「傳遞相依(Transitive Dependency)」。
解法是在一次性容器內執行 pip freeze,將完整環境固化為 requirements.lock,並在建置時透過 constraints 檔限制安裝版本:

pip install -c requirements.lock -r requirements.txt

需要在乾淨的一次性容器內產生 lock 檔,不能在掛載了 pip-extra/ 的執行中容器執行,以免自訂節點的套件遭到混入。

4. 硬體特徵從轉為顯式變數

Dockerfile 不該在建置中途私自讀取主機的 /proc/cpuinfo。改為在套件安裝腳本(package_routines)中探測硬體能力,並將 COMFY_CPU_FLAGS 作為明確的 Build Arg 傳入。


折衷的藝術:為什麼不直接在 CI 編譯好丟 GHCR?

讀者可能會問:「既然在 NAS 本地建置這麼麻煩,為什麼不乾脆在 GitHub Actions CI 裡建好 Image,直接推送到 GHCR?」

答案在於矩陣組合爆炸(Matrix Explosion)

  • CUDA 版本:cu121、cu126、cu128、cu130
  • CPU 特徵:支援 AVX2 / 不支援 AVX2
  • 單一架構即有 $3 \times 2 = 6$ 種組合,每個組合高達 9 GB。更關鍵的是,我們一般開發者比較缺乏夠多的實體硬體來逐一驗證這 6 種 Image 的穩定性。

最終架構取中庸之道:

  1. 主流規格走高速公路:由 CI 預先建置最常見的組合(cu128 + 支援 AVX2)並推送至 GHCR。符合條件的 NAS 直接 Pull,享受薄殼秒級啟動。
  2. 特殊硬體走本地調度:針對老舊 CPU 或特殊驅動機器,保留本地建置路徑,並透過上述 4 項防守措施確保建置的「確定性(Determinism)」。

二、GPU 直通的三種方式與避坑指南

要在 QNAP Container Station 中將 NVIDIA GPU 完整交給容器,歷史上出現過三種截然不同的手法:

方式 1: Docker 原生旗標 (--gpus all)
  [Docker Daemon] ──(內建 nvidia device driver)──> [容器]

方式 2: 註冊 Runtime (runtime: nvidia-runtime + 環境變數)
  [Container Station] ──(nvidia-container-runtime Hook)──> [容器注入驅動]

方式 3: 手動掛載裝置與函式庫 (--device + volume mount)
  [主機 /usr/nvidia] ──(手動 Bind Mount 檔案)──> [容器 /usr/local/nvidia]

三種 GPU 直通方案對照表

直通方式 代表專案 驅動注入負責者 失敗時的預設行為 評估建議
--gpus all Open WebUI (Day 2) Docker Daemon 內建驅動 自動退回 CPU 模式重試 推薦(環境支援時)
runtime: nvidia-runtime + Env ComfyUI (本日案例) Container Station 專屬 Runtime Entrypoint 自檢失敗退出 推薦(Compose 架構首選)
手動掛載 /dev/nvidia* 與 lib Jellyfin (Day 5) 無人注入,純手動 Bind 退回軟體解碼 極度脆弱,僅供最後防線

實測測試 runtime: nvidia-runtime 的隱形殺手

在撰寫 Docker Compose 時,若採用第二種寫法,有三個極其致命的細節:

  1. Runtime 名稱非標準:Container Station 將 runtime 註冊為 nvidia-runtime,而非標準 Docker 的 nvidia。如果你在 Compose 使用 deploy.resources.reservations.devices,底層會發送帶有 Driver: "nvidia" 的請求,導致 QNAP 無法識別。
  2. NVIDIA_VISIBLE_DEVICES=all 絕不能漏:若缺少此環境變數,Container Runtime Hook 的行為等同於 void,容器內部將完全看不到 GPU。
  3. 最危險的陷阱:NVIDIA_DRIVER_CAPABILITIES
    • 若只設為 utility,你在容器內執行 nvidia-smi 看起來一切正常、抓得到顯卡!
    • 但因為沒有給 compute,驅動程式不會將 libcuda.so.1 注入容器。此時 PyTorch 會默默判定 CUDA 不可用。外表看似運作,實際上運算慢了 100 倍。

關鍵防禦機制採用 Fail Fast,拒絕無聲崩潰

ComfyUI 在 CPU 上產一張圖可能要耗費數十分鐘甚至數小時。若因為 GPU 注入失敗而安靜地退回 CPU 運算,使用者只會在天亮時收穫一場空。

因此,本專案在 entrypoint.sh 啟動前加入強制檢查,只要 CUDA 不可用便立即退出:

import sys, torch
print("[entrypoint] PyTorch:", torch.__version__, "| CUDA Target:", torch.version.cuda)

if not torch.cuda.is_available():
    print("[entrypoint] 嚴重錯誤:CUDA 無法存取!請檢查 nvidia-runtime 與環境變數設定。", file=sys.stderr)
    sys.exit(1)

三、三層目錄架構:極速升級與零風險回滾

這個專案最具工程啟發性的設計,在於它的儲存三層分離架構。它徹底解決了「升級程式要不要重建 Image」、「重建 Image 會不會弄丟模型」的兩難:

┌─────────────────────────────────────────────────────────────┐
│ 第 1 層:Container Image (~9 GB)                             │
│ 內容:Debian Base、Python Runtime、PyTorch、核心依賴庫        │
│ 變更時機:僅在升級 CUDA 或 Python 大版本時需要重建           │
├─────────────────────────────────────────────────────────────┤
│ 第 2 層:主機 Stack 目錄 (/share/Container/comfyui/)          │
│ 內容:ComfyUI 原始碼 (Bind mount)、custom_nodes、.env        │
│       自訂節點相依庫 (pip-extra/ 加入 PYTHONPATH)            │
│ 變更時機:ComfyUI 日常小版本升級、安裝自訂節點               │
├─────────────────────────────────────────────────────────────┤
│ 第 3 層:集中模型共用庫 (/share/Public/models -> 唯讀掛載)   │
│ 內容:SD / Flux Checkpoints、LoRA、VAE 等龐大權重檔案        │
│ 變更時機:跨主機共享(可供 DGX 或其他工作站透過 SMB/NFS 讀取)│
└─────────────────────────────────────────────────────────────┘

這套架構帶來的好處:

  1. 升級免重建 Image:ComfyUI 原始碼並不在 9 GB 的 Image 裡,而是直接從本機目錄 Bind Mount 進去。升級軟體只要下載幾 MB 的原始碼替換目錄即可。
  2. 安裝外掛節點免膨脹:自訂節點所需的額外 Python 套件會安裝至 pip-extra/ 目錄,Entrypoint 會透過 PYTHONPATH 載入它,完全不會弄髒容器本體。
  3. 無痛升級(Upgrade)與回滾(Rollback)
    • 下載新版原始碼至暫存目錄 ComfyUI.prev-<新版>/(此時舊版仍正常服務)。
    • 準備就緒後,停用容器、原子對調目錄名稱、更新 .env、啟動新版容器。
    • 呼叫內部健康端點 /system_stats。若 5 分鐘內無回應,自動反向對調,秒級回滾至舊版。

四、NAS 記憶體暗礁:ZFS ARC 與 cgroup 的致命衝突

在配備 62.5 GB 記憶體的參考機上,這次中無意發現了一個隱晦但致命的效能陷阱。

自 ComfyUI v0.35.0 起,上游引入了 PR #15927:系統會主動讀取 cgroup 限制以計算 Pinned Memory Pool(鎖定記憶體池) 的上限大小。

而在執行 QuTS hero(基於 ZFS)的 NAS 上,問題就浮現了:

[實體 RAM: 62.5 GB]
  ├── ZFS ARC 快取:高達 46.7 GB (Linux 核心不可被排擠空間)
  └── 剩餘可用記憶體:僅剩 5 ~ 8 GB

此時若容器 mem_limit 設得太高 (例如 56 GB):
  ComfyUI 誤以為可用記憶體極多 ──> 企圖鎖定 40 GB Pinned Memory
  ──> 實體記憶體不足 ──> 觸發 Host Swap 瘋狂換頁。
  ──> 原本 50 秒的生成任務會暴增至 357 秒。

記憶體設定實測對照表

mem_limit 設定 ComfyUI 識別記憶體 Pinned Memory 上限 實際運作表現
未設定 (或 v0.34 前) 62.5 GB 約 56 GB 在 QuTS hero 上會直接引起嚴重的系統換頁與卡頓
56g 56 GB 40 GB 仍高於 ZFS 釋放後的實體極限,容易震盪
42g (演算法建議值) 42 GB 約 26 GB 穩定,由 MemTotal 扣除 zfs.arc_min 與安全保留空間
40g (實測最佳值) 40 GB 24 GB 最佳平衡點,能容納 Flux 等大型模型且絕不觸發 Swap

因此,專案在安裝腳本中加入自動試算機制:
$$\text{記憶體上限} = \text{MemTotal} - \text{ARC 保留量} - 8\text{GB}$$
在標準 QTS(Ext4)上 ARC 為 0,而在 QuTS hero(ZFS)上則會自動扣除 12.5 GB 以上的保護空間,守護整體 NAS 服務的穩定性。


五、實戰檢驗:如何在你的 NAS 上自我驗證?

如果你也正打算在 QNAP NAS 上執行 GPU 容器,可以透過以下指令進行體檢。

1. 檢查 GPU 直通前提條件

# 取得驅動安裝路徑並確認 CUDA 支援版本
DRV=$(getcfg NVIDIA_GPU_DRV Install_Path -f /etc/config/qpkg.conf)
LD_LIBRARY_PATH=$DRV/usr/nvidia $DRV/usr/bin/nvidia-smi | grep "CUDA Version"

# 確認 Container Station 是否成功註冊 nvidia-runtime
D=$(getcfg container-station Install_Path -f /etc/config/qpkg.conf)/bin/docker
$D info | grep Runtimes

# 確認顯卡裝置節點存在
ls -l /dev/nvidia0

2. 檢驗映像檔的建置履歷

由於本機自建 Image 沒有 Registry Digest,可透過 image history 追蹤建置當下的參數:

$D image history --no-trunc comfyui-nas:v0.35.1 | grep -o '|[0-9] COMFY_REF=[^ ]* TORCH_INDEX=[^ ]*' | head -1

若在不具備 AVX2 的機器上,進容器檢查 kornia_rs 是否成功釘選在相容版本:

$D exec comfyui pip freeze | grep -i "^kornia"
# 正確預期應輸出:kornia_rs==0.1.10

結語與下集預告

ComfyUI 案例很清楚地讓人知道邊緣運算與 NAS 部署中最真實的挑戰,面對不標準的硬體、需要小心應對的驅動程式以及上游動態依賴時,工程師該如何透過分層隔離、依賴鎖定與資源守護,建構出兼具穩定與彈性的服務。

在下一篇文章 Day 8 中,我們將回到整個系列範本的起源,也就是目前熱門的地端 AI 推論引擎和網頁平台Open WebUI + Ollama。我們將探討如何把過去一週淬鍊出的 CI、版本鎖定、多容器架構與自我修復(Self-healing)機制回饋給它,打造真正的生產級地端 AI 入口,提供給公司內部網路使用。


參考資源與系列連結


上一篇
Day 6|案例 changedetection.io 與 Homepage:資安與資訊公告監看 + 整合入口
下一篇
Day 8|案例實戰:Open WebUI + Ollama 重構記,把專案回到通用框架會省下什麼呢?
系列文
地端機房的三十天維運:開源服務封裝、GPU 節點守護與可稽核的變更管理9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言