iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
IT Operation

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

Day 12|熱與統一記憶體守護:取樣器、自動降載與長時間負載實測

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260925/20141816jEAcBdoYeD.png

系列專案原始碼: ivanusto/gb10-ops (v0.1.0 / v0.2.0)
前情提要: Day 11 完成了雙節點 DGX Spark 的系統基線整平。今天來談談在長時間極限負載下,如何讓這兩台節點穩定活著,而不是無預警消失在叢集中。


前言:這台機器有兩種別的 GPU 伺服器沒有的死法

在傳統分離式架構(獨立 CPU + 獨立 PCIe GPU)的伺服器上,記憶體爆掉頂多引發 Linux Kernel 的 OOM Killer 砍掉程序,溫度過高頂多觸發 GPU 降頻(Thermal Throttling)。但在配備統一記憶體架構的 GB10 上,遭遇過兩次常規監控完全抓不到的致命事故:

  1. 熱浸透斷電(Thermal Soak Hard Shutdown, 2026-09-01):
    長時間的高負載影片生成將機箱溫度維持在 88 °C 以上連續長達 417 秒。最終機器直接在硬體/韌體層硬生生斷電。沒有 Kernel Panic、沒有關機流程、事後開機更讀不到任何核心日誌。然而,同一個工作如果從冷機開始跑,卻能完全正常結束。
  2. 統一記憶體死鎖(Unified Memory Livelock, 2026-09-06):
    統一記憶體耗盡時,系統沒有拋出任何 OOM 例外,OOM killer 全程沒有介入。整台機器除了回應 ICMP ping 之外,SSH 與所有服務完全癱瘓。核心日誌在 23:12:53 印下第一行 NV_ERR_NO_MEMORY,最後一筆系統紀錄停在 23:56:00——警訊其實比機器徹底失聯早到了足足 43 分鐘。

這兩起事故的共同盲點是:標準維運監控工具在這裡全面失明。

  • 溫度的致命點不在瞬時峰值,而在時間累積(熱浸透): 任何以瞬時溫度觸發的靜態告警,在長任務渲染的正常平台期都會誤殺健康工作。
  • 記憶體的盲點在於 cgroup 與 RSS 完全看不見統一記憶體: 你在 Docker 下的 --memory 限制,根本管不到填滿這台機器的隱形怪物。

今天依據 gb10-ops v0.1.0(commit 5aa8b22)實作,拆解如何讀取底層數值、由事故量測反推門檻值、建立兩道守護機制,並透過最小權限的 systemd unit 將守護常駐化。


一、先學會讀這台機器:為什麼傳統監控會失明?

GB10 的硬體架構特殊,三個關鍵狀態的真實位置都不在傳統習慣的路徑上:

觀測指標 正確讀取路徑 為什麼傳統監控在此失效?
記憶體餘量 /proc/meminfo 的 MemAvailable NVML 對統一記憶體回報 N/A,nvidia-smi 查詢 memory.total 顯示 [N/A],nvtop 的記憶體欄位完全空白。傳統監控若看「已用百分比」會致命——在 Livelock 前的 43 分鐘,系統回報的「已用率」穩定在漂亮的 29.6%,但實際上 MemAvailable 只剩 0.16 GiB。
機箱溫度 /sys/class/thermal/thermal_zone*/temp(7 個 thermal zone 取最大值) 此平台沒有獨立的 GPU hwmon。雖然 GPU 晶片溫度能透過 nvidia-smi 讀取,但真正決定韌體是否硬斷電的是機箱溫度。
即時功耗 nvidia-smi --query-gpu=power.draw 系統只回報 GPU 核心功耗,模組、記憶體功耗與上限皆為 N/A。hwmon 下無 power*_input,無 INA 驅動,/sys/class/power_supply/ 為空。它僅代表 GPU 供電軌,整機真實功耗需依賴外掛瓦特計(Day 9 實測為 327.77 W)。

100 GiB 的幽靈:cgroup 為什麼看不到?

在一般伺服器(例如 Day 7 的 QNAP 實驗)上,透過 mem_limit 限制容器記憶體非常有效,因為 GPU 有獨立 VRAM,主機記憶體歸 cgroup 與系統管理。

但在 GB10 上,統一記憶體是由 NVIDIA 驅動直接配置。在 2026-09-09 量測到的記憶體分佈如下:

cgroup memory.current    :  11 GiB
cgroup memory.peak       :  37 GiB
容器內所有程序 RSS 加總    :   4 GiB
---------------------------------------------
主機實際已用記憶體 (Used) : 113 GiB

中間足足相差了 100 GiB!這 100 GiB 被 NVIDIA 驅動納為統一記憶體空間,cgroup 與程序 RSS 完全無法計算。

唯一能看見真相的只有這條指令:

nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv

它誠實回報了 VLLM::Worker_TP0 實佔 102,843 MiB。

結論:
守護機制不能建立在容器限制層(Container limit),必須下沉至主機層(Host level):以 /proc/meminfo 的 MemAvailable 判斷全局水位,再以 nvidia-smi compute-apps 鎖定具體消耗記憶體的目標 PID。


二、資料倒推門檻:不憑感覺設定的六個數值

gb10-ops 中的六個核心閾值,每一個都是從硬體真實事故中精確倒推出來的,換機器時必須重新校準:

門檻項目 設定值 推導來源與依據
熱浸透預算 機箱區 ≥ 88 °C 連續 240 秒 致命斷電事故在 88 °C 以上浸透了 417 秒,設定 240 秒能在韌體硬斷電前 3 分鐘精準煞車。
受控中止極限 連續 330 秒 超過此上限,程式強制中止(丟棄任務)的成本,遠低於伺服器不受控斷電損壞硬體的風險。
冷卻閘門 GPU 降至 ≤ 55 °C 才允許啟動 實測兩次相同重載任務,起始機箱溫度僅差 8 °C,就是「平安跑完」與「過熱被中止」的分水嶺。
記憶體警告線 MemAvailable ≤ 6 GiB 雙機生產服務穩態約 7.85 GiB,叢集重開機瞬態最低點為 6.91 GiB。設在 6 GiB 能做到整夜零誤報。
記憶體動作線 MemAvailable ≤ 3 GiB Livelock 事故在失聯前水位降至 0.20 GiB,歷史驚險值曾觸及 2 GiB。設在 3 GiB 能確保及時挽救系統。
NVRM 報錯頻率 60 秒內出現 5 行 Livelock 事故發生時的頻率是 21 秒內爆出 24 行。單行警報太吵(歷史背景雜音有數千行),必須以短時間突發速率判定。

注意

校準經驗談:
原本直覺將記憶體警告線設在 8 GiB,結果正式服務一跑就持續常態觸發。正確的標定方法是:取樣一整夜正常服務的穩態,加上一次完整的重啟過程,將警告線劃在最低谷值之下,將動作線劃在事故歷史最高點之上。兩條線之間,就是「系統異常但還有挽救時間」的黃金介入區間。

延伸思考:Day 10 的極限實驗遇到守護會發生什麼?

Day 10 的模型實驗啟動後,可用記憶體穩態約在 1.5~2.0 GiB,暖機瞬間甚至會跌到 500 MB,完全落在 3 GiB 動作線之下。

但這時守護程式並不會殺掉它:

  1. 守護連續 3 次取樣發現 MemAvailable < 3 GiB,觸發處置流程。
  2. 守護透過 --protect 規則過濾白名單(預設保護名稱帶有 VLLM:: 的核心進程)。
  3. 實驗程序剛好叫 VLLM::Worker_TP0,符合保護條件,守護標記為 no_target,只記日誌、不動手。
  4. 之後計數歸零,等待 60 秒冷卻,大約每 90 秒寫下一組 trigger 與 no_target。

這反映出一個維運哲理:--protect 比對的是程序標籤,而不是工作角色。當實驗程序披著生產服務的名字時,全域守護會把它當成生產服務來保護。針對這種高壓實驗,必須自帶具備特定辨識能力的本機看門狗(如配方中低於 250 MB 就強制重啟容器的 mem-watchdog.sh)。


三、雙守護架構:三種介入模式

守護不是粗暴地寫個無窮迴圈去殺 Process。依照工作能否被打斷,gb10-ops 設計了兩種守護與三種介入機制。

                  +---------------------------------------------------+
                  |                 gb10-ops 守護矩陣                 |
                  +---------------------------------------------------+
                                            |
        +-----------------------------------+-----------------------------------+
        |                                                                       |
 [記憶體維度:全域常駐]                                                  [溫度維度:依任務型態]
        |                                                                       |
  gb10-host-guard                                      +------------------------+------------------------+
  - 監控 MemAvailable                                  |                        |                        |
  - 追蹤 NVRM 錯誤速率                         [信號中止型]              [HTTP API 降載型]         [段落標記型]
  - SIGTERM -> SIGKILL                         thermal-run              thermal-guard-http       hw-sample
                                               - 包裝批次腳本            - ComfyUI / vLLM         - 放旗標檔 (/tmp/HOT)
                                               - 獨立 Process Group      - 呼叫 /interrupt API    - 段落交接處檢查
                                               - SIGKILL 終止            - 清空待處理 Queue        - 散熱追上再啟動

1. 記憶體守護:gb10-host-guard.py

此腳本每 10 秒讀取一次 MemAvailable,同時以非阻塞串流監聽 journalctl -k -f,統計過去 60 秒內的 NV_ERR_NO_MEMORY 次數。

觸發條件(滿足其一):

  • MemAvailable 低於動作線(3 GiB)連續 3 次取樣。
  • 60 秒內 NVRM 錯誤達 5 次,且 MemAvailable 低於警告線(6 GiB)。

滿足後,立即將現場的程序記憶體佔用寫入日誌,接著對消耗最大且未受保護的程序發送 SIGTERM,寬限 20 秒,若未退出則補上 SIGKILL,隨後進入 60 秒冷卻期以待記憶體釋放回穩。

工程師必須知道的 4 個實作細節:

  1. 每行日誌強制 fsync: 硬體瞬間斷電時,留在記憶體緩衝區(Buffer)裡的日誌等於沒寫。
  2. 日誌追蹤程序自癒與半行拼接: 如果背景 tail journal 的子程序當掉,讀取會變為空值,會造成 NVRM 警報條件靜默失效。因此每輪必須檢查子程序狀態,且非阻塞讀取遇到的半行字元必須暫存,避免 NV_ERR_NO_MEMORY 恰好被截斷在緩衝區邊界。
  3. 嚴禁使用 pkill -f 與 pgrep -f: 這兩者會比對包含呼叫腳本在內的完整命令列,極容易誤殺守護進程自身所在的 Session。守護一律只對 nvidia-smi 查詢回傳的精確 PID 送出信號。
  4. 0.0 GiB 不得視為 falsy: 在 Python 邏輯中,avail = 0.0 絕不能被視為空值而跳過,因為 0.0 GiB 是真實存在且極度危險的瀕死讀數。

2. 熱守護的三種介入實作

  • 模式 A(信號強殺):thermal-run.py
    適合一般獨立執行、可被中斷的批次任務(如 LoRA 訓練)。啟動前先檢查冷卻閘門(GPU ≤ 55 °C,最多等候 5 分鐘)。透過 setsid 將任務置於專屬 Process Group 中執行,每 5 秒讀取機箱溫度。若持續高於 88 °C,秒數開始累加(溫度回落則計數歸零,代表風扇散熱追上了)。累積達 240 秒或瞬時突破 96 °C,立即殺掉整個 Process Group,Exit code 設為 75。
  • 模式 B(API 優雅降載):thermal-guard-http.py
    適合提供對外服務的常駐伺服器(如 ComfyUI、vLLM-Omni)。同樣計算熱浸透預算,但觸發時不是送信號殺程序,而是向 ComfyUI 的 /interrupt 與 /queue API 發出 HTTP 請求,中止當前渲染並清空排隊佇列。犧牲單次生成,換取整台伺服器免於硬斷電。
  • 模式 C(批次旗標):hw-sample.py
    適用於流水線任務(Pipeline)。當取樣器偵測到熱浸透即將超標時,不主動中斷執行中的工作,而是在檔案系統建立 /tmp/HOT 旗標檔。驅動批次處理的外部 Shell 腳本在每段任務完成後檢查該旗標,只要存在就暫停派工,直到機器冷卻降溫。

四、生產化常駐:最小權限的 Systemd Unit

早期的 README 採用 nohup setsid ... & 配上 crontab 的 @reboot 來啟動守護。這種作法在生產環境有兩大嚴重缺陷:

  1. 權限不足: 模型多以容器或 root 身分跑在底層,一般使用者身分的守護程序在執行 kill 時會拋出 Operation not permitted,只留下毫無用處的 kill_failed 日誌。
  2. 生命週期不穩定: systemd user unit 若未開啟 lingering,使用者登出時守護進程就會被清除。

在 gb10-ops v0.2.0 中為其量身打造了系統級服務,建立專屬無登入權限的服務帳號 svc-guard,並利用 Linux Capabilities 嚴密限制其權限:

  • 保留特權:
    • CAP_KILL:允許對系統中其他非 root / root 程序發送信號。
    • systemd-journal 群組:允許讀取核心日誌 /dev/log 與 Journal 檔案。
  • 其餘全部收緊封鎖: 禁止建立新特權、隱藏除必要路徑外的所有主機目錄。

/etc/systemd/system/gb10-host-guard.service

[Unit]
Description=GB10 Host Memory Guard
After=network.target

[Service]
Type=simple
User=svc-guard
SupplementaryGroups=systemd-journal
EnvironmentFile=/etc/default/gb10-host-guard
ExecStart=/usr/bin/python3 /usr/local/bin/gb10-host-guard.py \
    --interval ${INTERVAL} --warn-gib ${WARN_GIB} --act-gib ${ACT_GIB} \
    --sustain ${SUSTAIN} --nvrm-rate ${NVRM_RATE} --protect ${PROTECT} \
    --log /var/log/gb10-ops/host-guard.jsonl ${DRY_RUN}
Restart=always
RestartSec=5s

# 最小權限沙箱配置
CapabilityBoundingSet=CAP_KILL
AmbientCapabilities=CAP_KILL
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/gb10-ops

[Install]
WantedBy=multi-user.target

備註

沙箱設定中的兩處刻意保留:

  • 未設定 PrivateDevices=true: 因為 nvidia-smi 必須與 /dev/nvidia* 裝置節點進行 ioctl 通訊。
  • 未限制 ProtectProc: 守護必須解析 /proc/meminfo,且需利用 os.kill(pid, 0) 探索進程是否存在。

五、實戰演練與驗證步驟 (Runbook)

Step 1. 取得程式碼與乾跑測試

在正式啟用殺程序功能前,務必先以 --dry-run 跑滿 24 小時,觀察其識別的穩態水位。

git clone https://github.com/ivanusto/gb10-ops.git
cd gb10-ops
git checkout v0.2.0

# 乾跑模式:只記錄 would_kill 事件,絕不動手
python3 guards/gb10-host-guard.py --dry-run

確認記錄中的 avail_gib 在一整夜的正常運作下,始終穩定高於 --warn-gib 設定值。

Step 2. 建立服務帳號與部署 Unit

# 建立無登入權限的專屬系統帳號
sudo useradd --system --no-create-home --shell /usr/sbin/nologin svc-guard

# 安裝主程式與設定檔
sudo install -m755 -o root guards/gb10-host-guard.py /usr/local/bin/
sudo install -m644 systemd/gb10-host-guard.env /etc/default/gb10-host-guard
sudo install -m644 systemd/gb10-host-guard.service /etc/systemd/system/

# 建立專屬日誌目錄並指派權限
sudo install -d -o svc-guard -g svc-guard /var/log/gb10-ops

# 啟動並設定開機自啟
sudo systemctl daemon-reload
sudo systemctl enable --now gb10-host-guard

# 檢查狀態與即時日誌
systemctl status gb10-host-guard
tail -f /var/log/gb10-ops/host-guard.jsonl

確認跑滿一天、日誌沒有誤殺判定後,即可編輯 /etc/default/gb10-host-guard 將 DRY_RUN 設為空值,並執行 sudo systemctl restart gb10-host-guard 進入真實守護模式。

Step 3. 讓長時間訓練任務走在熱守護後面

進行長達數小時的 LoRA 訓練或影像微調時,透過包裝器掛載取樣與溫度閘門:

# 1. 背景啟動硬體取樣器 (每 3 秒記錄一次資料,遇高溫產出 /tmp/HOT)
python3 samplers/hw-sample.py --out train_run.jsonl --hot-flag /tmp/HOT --interval 3 &
SAMPLER_PID=$!

# 2. 透過熱保護啟動任務:冷卻至 55°C 才開跑,超過 88°C 達 240 秒則受控退出
python3 guards/thermal-run.py \
    --cool 55 \
    --soak-zone 88 \
    --soak-seconds 240 \
    -- python3 train_lora.py --epochs 10

EXIT_CODE=$?
echo "Training finished with exit code: ${EXIT_CODE}" # 75 代表被守護程式安全阻斷

# 3. 停止取樣並輸出極值報表
kill -SIGINT ${SAMPLER_PID}
python3 bench/summarize-samples.py train_run.jsonl --skip-head 60 --skip-tail 15

輸出的統計指標(p10、p90 與平均功耗/溫度)將作為系統長期營運、電費核算與儀表板繪製的核心基準資料。


明天預告

Day 13|NFS 集中模型庫:單寫多讀、身分校驗與動態掛載實戰

在解決了「NVIDIA 地端 AI 算力機器不會突然掛掉」的問題後,明天來處理 AI 算力叢集的儲存痛點:

  • Day 8 實測中,兩個 Ollama 對同一個 NFS 目錄平行處理 pull 發生暫存檔互砍毀損。
  • Day 10 的 Engram 大模型分片被散亂丟在各節點,沒人記得原始來源。
  • Day 11 節點基線檢查更發現一台掛載了 Automount,另一台卻沒有。

明天將統一定義集中式模型庫的權限架構、Manifest 驗證機制與多節點掛載規則,讓存放在地端的每一個模型權重,都能被清晰追溯。


相關專案與參考資料


上一篇
Day 11|DGX Spark 節點基線:DGX OS 初始設定、帳號與 SSH 加固
下一篇
Day 13|別讓節點踩爆模型!NFS 集中模型庫的單寫多讀、權限凍結與完整性治理
系列文
地端機房的三十天維運:開源服務封裝、GPU 節點守護與可稽核的變更管理 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言