
前情提要:前幾天我們討論的容器套件多半是「薄殼(Thin Wrapper)」——套件本體只放管理腳本,核心映像檔(Image)直接從 Docker Registry 拉取即可。
但今天的主角 ComfyUI 完全顛覆了這個模式,它沒有上游官方現成 Image,還牽涉到底層硬體指令集與 NVIDIA 驅動相依性,是很多人在自己的電腦、伺服器或 NAS 上建構會碰到問題的熱門地端 AI 服務。本文將帶你走過一趟從地端建置、GPU 直通除錯到極限記憶體調校的完整實戰。
在 NAS 上打包開源工具,最理想的做法就是寫個腳本,docker pull 現成 Image 就收工。然而,在以 qnap-comfyui-qpkg 為案例實作時,會發現有很多現實上的困難:
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 去應用容器來實現服務是一條殊途同歸的路:
在拆解架構前,我們先為 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 三者皆就緒才起跑。 |
| 維護機制 | 內建 upgrade 與 rollback |
全系列重點!支援背景原子切換與自動健康檢查回滾。 |
| 供應鏈安全性 | 無 CI 自動化簽核 | 映像檔未發布 SHA256SUMS 與 Attestation,且自建 Image 的 4 個版本來源原本僅鎖了 1 個。 |
小結:這個專案在執行期(Runtime)的韌性極高,不僅考慮了開機時 NVIDIA 核心模組載入延遲,還具備高可用升級回滾機制,但其脆弱點在於軟體供應鏈的可重現性。
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)。 |
不再只依賴 python:3.12-slim-bookworm,而是比照 Day 3 加入 SHA256 Digest,寫入 images.lock:
FROM python:3.12-slim-bookworm@sha256:d5b839...
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
constraints 鎖定檔kornia_rs 之所以會無預警升級並引發 AVX2 崩潰,就是因為它屬於未被釘死的「傳遞相依(Transitive Dependency)」。
解法是在一次性容器內執行 pip freeze,將完整環境固化為 requirements.lock,並在建置時透過 constraints 檔限制安裝版本:
pip install -c requirements.lock -r requirements.txt
需要在乾淨的一次性容器內產生 lock 檔,不能在掛載了 pip-extra/ 的執行中容器執行,以免自訂節點的套件遭到混入。
Dockerfile 不該在建置中途私自讀取主機的 /proc/cpuinfo。改為在套件安裝腳本(package_routines)中探測硬體能力,並將 COMFY_CPU_FLAGS 作為明確的 Build Arg 傳入。
讀者可能會問:「既然在 NAS 本地建置這麼麻煩,為什麼不乾脆在 GitHub Actions CI 裡建好 Image,直接推送到 GHCR?」
答案在於矩陣組合爆炸(Matrix Explosion):
最終架構取中庸之道:
cu128 + 支援 AVX2)並推送至 GHCR。符合條件的 NAS 直接 Pull,享受薄殼秒級啟動。要在 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]
| 直通方式 | 代表專案 | 驅動注入負責者 | 失敗時的預設行為 | 評估建議 |
|---|---|---|---|---|
--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 時,若採用第二種寫法,有三個極其致命的細節:
nvidia-runtime,而非標準 Docker 的 nvidia。如果你在 Compose 使用 deploy.resources.reservations.devices,底層會發送帶有 Driver: "nvidia" 的請求,導致 QNAP 無法識別。NVIDIA_VISIBLE_DEVICES=all 絕不能漏:若缺少此環境變數,Container Runtime Hook 的行為等同於 void,容器內部將完全看不到 GPU。NVIDIA_DRIVER_CAPABILITIES:
utility,你在容器內執行 nvidia-smi 看起來一切正常、抓得到顯卡!compute,驅動程式不會將 libcuda.so.1 注入容器。此時 PyTorch 會默默判定 CUDA 不可用。外表看似運作,實際上運算慢了 100 倍。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 讀取)│
└─────────────────────────────────────────────────────────────┘
pip-extra/ 目錄,Entrypoint 會透過 PYTHONPATH 載入它,完全不會弄髒容器本體。ComfyUI.prev-<新版>/(此時舊版仍正常服務)。.env、啟動新版容器。/system_stats。若 5 分鐘內無回應,自動反向對調,秒級回滾至舊版。在配備 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 服務的穩定性。
如果你也正打算在 QNAP NAS 上執行 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
由於本機自建 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 入口,提供給公司內部網路使用。