在 Mac mini / Cloud Mac 上同時執行 Ollama、Claude Code 和 GitHub Actions 時,最常見的問題不是「效能不夠」,而是系統開始 Swap、CLI 卡頓、CI 建置變慢。
本篇不討論工具安裝,而是說明:為什麼三種 AI 工作負載同時存在時會出現記憶體抖動,以及如何透過排班避免它。下文提供反例、記憶體預算、閾值排班、30 秒 Runbook 與完整腳本;也不重複16GB vs 24GB 實測。
深入閱讀時會看到 AI workload scheduling 模型:在我們觀測的 Cloud Mac 場景中,大多數 Swap/OOM 更多來自排班問題,而非絕對記憶體不足。
真正的問題:多數情況不是記憶體不夠,是沒有優先順序
表面症狀是「本機模型空轉 + CI 尖峰 + 編碼 Agent 變卡」——根因常被誤讀為「該升級了」。
在我們觀測的 Cloud Mac 場景中,大多數 Swap/OOM 更多來自工作負載排班問題,而非絕對記憶體不足。 同一台 Mac mini 上同時存在三類負載(詳見下節):
- Burst——GitHub Runner /
xcodebuild,push 時瞬間 +4–8 GB - Interactive——Claude Code,IDE + 終端機 + 大儲存庫索引
- Background——Ollama 等本機推理,8B loaded 後靜默佔 5–7 GB
三者預設平等搶統一記憶體,沒有人「必須讓路」。修正方向:定義工作負載優先順序與觸發條件,把背景推理從「裝完就常駐」改成可卸載、可被 CI 事件搶占的可排班資源。
錯誤排班反例(M4 上非常常見,不是偶發)
下面是我們在小團隊 Cloud Mac 上真實踩過的組合——可作為 onboarding 時的「禁止模式」:
Bad pattern · all three workloads default to "always on" qwen3:8b always loaded → 5–7 GB (background, zero calls) push triggers xcodebuild → +4–8 GB peak (burst) Claude Code indexing large repo → +2–3 GB (interactive) Observed on 24GB M4 Mac mini Swap 0 → 2.1 GB xcodebuild link phase latency +~40% Claude Code terminal noticeably slower
在我們觀測的 M4 統一記憶體場景下,8B 常駐 + CI burst + 互動式編碼同時發生,Swap 非常常見。反例的價值在於:讓讀者相信 workload scheduling 不是錦上添花,而是多負載並存的前置條件。
三種負載形態:burst / interactive / background
排班不能只看時鐘。Cloud Mac 上三類工作負載的記憶體曲線完全不同:
| 形態 | 代表 | 層 | 記憶體特徵 | 排班優先順序 |
|---|---|---|---|---|
| Burst | xcodebuild、連結、模擬器 |
L1 Runner | 尖峰、不可預測 | 最高——Fact 不能失敗 |
| Interactive | Claude Code、IDE、SSH 工作階段 | L3 | 中等、人在場 | 高——但推理走 API,不佔大模型 |
| Background | Ollama embedding、日誌摘要 | L2 | 可延遲、可卸載 | 最低——必須讓路給 burst |
一句話:L2 是 Stack 裡唯一「可以被踢下線」的一層——不是因為它不重要,而是因為它的任務大多可非同步、可重試。
記憶體預算:三種工作負載各佔多少
下面以 Ollama / Claude Code / GitHub Runner 為範例元件,數字來自M4 Mac mini 實測(穩態,非編譯尖峰):
| 元件 | 層 | 典型佔用 | 尖峰說明 |
|---|---|---|---|
| macOS + 系統快取 | L0 | 3–4 GB | 相對穩定 |
| Claude Code 工作區 | L3 | 1–3 GB | 推理走 API,不佔模型權重 |
| GitHub Runner job | L1 | 2–6 GB(穩態) | 連結階段瞬間 +4–8 GB |
| Ollama · qwen3:8b | L2 | 5–7 GB | ollama stop 可釋放 |
| Ollama · qwen3:14b | L2 | 9–13 GB | 與 burst 並存易 Swap 2GB+ |
| Ollama · nomic-embed-text | L2 | 0.3–0.8 GB | 白天可常駐的輕量 background |
粗算 24GB 機白天「編碼 + CI + 8B 常駐」≈ 17 GB——尚餘餘量;疊 14B 常駐則輕鬆超 22 GB。預算表解決「能不能」;排班解決「誰該在什麼時候佔」。
排班模型:從時段表到 memory_pressure 閾值
入門可以用「日/夜時段表」(見下文模式 ②),但更可維運的升級是按記憶體壓力閾值排班——不依賴人記得「22 點該跑批處理」:
AI Workload Scheduler · L2-Q03 recommended rules When memory_pressure enters warn / critical (or equivalent > ~70%): → auto ollama stop qwen3:8b / qwen3:14b When memory_pressure normal (< ~50%) and idle > 10 min: → auto preload nomic-embed-text (keepalive 10m) When CI event trigger (Runner job start): → force CI mode: stop all large Ollama models, priority L1 > L3 > L2 When CI job succeeds and memory recovers: → async trigger L2 batch (log summary / embedding rebuild)
時段排班是 baseline;閾值排班是 upgrade。 小團隊可以先抄 Runbook + CI 前 stop;穩定後再上 memory_pressure guard 腳本(見 Runbook 一節)。
三種基礎排班模式
與閾值排班配合使用的基礎策略:
| 模式 | 做法 | 適合 |
|---|---|---|
| ① 輕量並存 | 白天只常駐 nomic-embed-text;8B/14B 按需 load |
16GB Mac mini、以編碼為主 |
| ② 時段拆分 | 09–18 編碼+CI / 22–06 夜間批處理 | 24GB、有定時 embedding / 日誌任務 |
| ③ CI 觸發讓路 | job 前 ollama stop,結束後非同步 L2 |
push 頻繁、xcodebuild 尖峰高 |
推薦組合:① + ③ 全員預設;② 作為夜間批處理補充;閾值 guard 兜底。
Pipeline 分流:什麼走 L2、什麼走 L3
排班之前先釘死 pipeline,否則 Ollama 仍會空轉(見 L2-Q01 · 典型誤判):
| 任務 | 層 | 排班備註 |
|---|---|---|
| 改儲存庫、產生 patch | L3 Claude Code(API) | 不佔本機大模型 |
| 建置、測試、歸檔 | L1 Runner | burst 最高優先;尖峰前停 L2 |
| CI 失敗日誌摘要 | L2 qwen3:8b |
job 結束後非同步,或夜間批 |
| CodeGraph / RAG embedding | L2 nomic-embed-text |
可白天常駐(<1GB) |
Runner 尖峰與 CI 錯峰
L1 Runner 產 Fact——Inference 不能取代建置結果。CI 側最小改動:
# .github/workflows/ios.yml · self-hosted macOS runner - name: Enter CI mode — free memory for xcodebuild run: | ollama ps ollama stop qwen3:8b 2>/dev/null || true ollama stop qwen3:14b 2>/dev/null || true sleep 30 # wait for memory reclaim; do not start xcodebuild same second - name: Build run: xcodebuild ...
實測:24GB M4 上 ollama stop qwen3:8b 後約 5–15 秒內釋放 5–7 GB;Swap 已產生時完全回收需數分鐘——故 CI 前置 stop 必須提前至少 30 秒。
Runbook:30 秒版與完整腳本
30 秒版(大多數人只需要這三段)
不想讀完整腳本?複製下面三段即可覆蓋 80% 場景:
① CI 前(最關鍵)——放進 GitHub Actions 或 Runner hook:
ollama stop qwen3:8b ollama stop qwen3:14b sleep 30
② 白天——只留輕量 embedding,不佔大模型權重:
ollama run nomic-embed-text --keepalive 30m
③ 夜間——批處理時段再 load 8B:
ollama run qwen3:8b # then run your log-summary / embedding-rebuild scripts
完整版(生產 Runbook)
需要 LaunchAgent / cron / 多環境複用時,儲存為 ~/bin/cloud-mac-stack-runbook.sh:
完整 Runbook · 子命令
day-start · ci-pre · ci-post · night-batch
#!/usr/bin/env bash # cloud-mac-stack-runbook.sh — L2-Q03 standard Runbook set -euo pipefail OLLAMA_HOST="${OLLAMA_HOST:-127.0.0.1:11434}" export OLLAMA_MAX_LOADED_MODELS=1 ensure_ollama() { curl -sf "http://${OLLAMA_HOST}/api/tags" >/dev/null || ollama serve & sleep 2 } ci_pre() { # Before CI: force CI mode, L1 priority ollama ps || true ollama stop qwen3:8b 2>/dev/null || true ollama stop qwen3:14b 2>/dev/null || true sleep 30 } ci_post() { # After CI: restore light embedding (lowest background tier) ensure_ollama ollama run nomic-embed-text --keepalive 10m } day_start() { # Day login / boot: embedding only or stop all ensure_ollama ollama stop qwen3:8b 2>/dev/null || true ollama stop qwen3:14b 2>/dev/null || true ollama run nomic-embed-text --keepalive 30m } night_batch() { # Night batch (cron: 0 22 * * *) ensure_ollama ollama run qwen3:8b --keepalive 6h # ./your-log-summary-or-embed-rebuild.sh } case "${1:-}" in day-start) day_start ;; ci-pre) ci_pre ;; ci-post) ci_post ;; night-batch) night_batch ;; *) echo "Usage: $0 {day-start|ci-pre|ci-post|night-batch}"; exit 1 ;; esac
Memory guard(建議 cron 每 5 分鐘,或 LaunchAgent):當系統記憶體壓力升高時自動卸載大模型——這是「閾值排班」的最小實作。
#!/usr/bin/env bash # memory-guard.sh — memory_pressure threshold guard PRESSURE=$(memory_pressure 2>/dev/null | head -1 || true) if echo "$PRESSURE" | grep -qiE 'warn|critical|urgent'; then logger -t cloud-mac-stack "memory guard: stopping Ollama 8B/14B ($PRESSURE)" ollama stop qwen3:8b 2>/dev/null || true ollama stop qwen3:14b 2>/dev/null || true fi # Optional: restore embedding when pressure normal and no Runner job # if echo "$PRESSURE" | grep -qi 'normal'; then ... fi
接入方式範例:
- GitHub Actions——workflow 首 step 調
ci-pre,末 step 調ci-post - LaunchAgent——登入時
day-start;22:00 cronnight-batch - cron——
*/5 * * * * /path/memory-guard.sh
16GB Mac mini 怎麼排
- 禁止 14B 常駐;8B 僅
night-batch - 白天 Runbook 只用
day-start(embedding 或全停) - 每次 CI 必須
ci-pre,無例外 - 預設「桌面 + 8B + Claude Code 同時在線」→ 直接選 24GB,排班救不了硬體邊界
16GB 口訣:白天無大模型,夜間單模型單任務,CI 前必 ci-pre。24GB 口訣:白天可 embedding,8B 與 CI 錯峰,14B 僅夜間。
決策表:你該用哪種策略
| 你的情況 | 推薦 |
|---|---|
| 24GB · push 少 · mainly Claude Code | day-start + embedding;無夜間批 |
| 24GB · 每日 CI · 要日誌摘要 | ci-pre/post + night-batch + memory guard |
| 16GB · 必須本機 8B | 僅 night-batch;白天 Claude API |
| Ollama 仍一週零呼叫 | 先回 L2-Q01 定義 pipeline |
系列定位 · Cloud Mac AI Stack
L2-Q03 · 記憶體排班層——對外回答「Mac mini 如何避免 Swap」;對內是 L2-Q01 私有推理層 的排班續篇:
- L2-Q01 — Inference 是什麼(定位)
- L2-Q03 · 本篇 — 記憶體排班層(同機排班)
- 計畫中 — 模型 pin、11434 健康檢查、CI 側呼叫 Ollama
- 下游 — L4 Context + MCP 排班(明日 L4-Q03)
它不是 Ollama 使用教學,也不是純 CI/CD 優化文——而是 Apple Silicon 上 AI Workload Scheduler 的第一層設計。
與已發文章的關係
- L2-Q01 · 私有推理層——L2 定位;本篇是其排班續篇。
- L2-Q02 · 16GB vs 24GB——Swap 數字來源;本篇引用不重複 benchmark。
- L1-Q01 · Runner——burst 優先順序最高。
- L3 · Claude Code——interactive 主路徑。
常見問題
Mac mini 上 Ollama、Claude Code、GitHub Actions 同時跑會 Swap 嗎?
會,若 burst / interactive / background 三種工作負載沒有優先順序。見 真正的問題與 反例。
Ollama 需要一直執行嗎?
不需要。 應把本機推理當成可排班資源:白天可只留 nomic-embed-text,大模型按需 load 或僅在夜間批處理。
CI 建置時要停 Ollama 嗎?
建議停。 見 30 秒 Runbook · CI 前,或完整版 ci-pre。
Claude Code 和 Ollama 能同時跑嗎?
能。 編碼主路徑走 API;本機爭用的是模型權重與 CI 尖峰。用 CI 前 stop 或時段拆分即可。
Cloud Mac 上 OOM 是記憶體不夠嗎?
在我們觀測的場景中,大多數 OOM 更多來自排班問題,而非絕對記憶體不足。16GB 硬邊界見 L2-Q02 實測。
時段排班和 memory_pressure 閾值怎麼選?
先上 30 秒 Runbook;穩定後加 memory-guard.sh(見完整 Runbook)。
本篇和 L2-Q01 有什麼不同?
Q01 講 Inference Service 定位;Q03(記憶體排班層)講同機 AI 工作負載如何排班。
Cloud Mac AI Stack · 明日預告
Claude Code + MCP:GitHub / 本機檔案 / API 怎麼連成一條鏈
L2 排班釘死後,下一層是 L4 Context:MCP 如何把 GitHub、儲存庫檔案與 API 接進 Claude Code 工作流。
返回部落格 · Cloud Mac AI Stack 全系列