Mac mini 上 AI Workload 调度:如何避免 Ollama + Claude Code + GitHub Runner 导致 Swap

L2-Q03 · 内存调度层

2026.06.04  ·  约 14 分钟  ·  运维 Runbook,非安装教程

Mac mini 统一内存与 AI workload 调度示意:Ollama、Claude Code、GitHub Actions 并行时通过排班避免 Swap

在 Mac mini / Cloud Mac 上同时运行 Ollama、Claude Code 和 GitHub Actions 时,最常见的问题不是「性能不够」,而是系统开始 Swap、CLI 卡顿、CI 构建变慢

本篇不讨论工具安装,而是解释:为什么三种 AI workload 同时存在时会出现内存抖动,以及如何通过调度避免它。下文给出反例、内存预算、阈值调度、30 秒 Runbook 与完整脚本;也不重复16GB vs 24GB benchmark

TL;DR

  • Ollama 常驻 + CI burst → 在 M4 上 Swap 非常常见(见 反例
  • 解法通常不是升级 Mac,而是给 burst / interactive / background 定义 workload 排班
  • CI 前 ollama stop 是最关键、也最容易落地的一步(30 秒版

深入阅读时会看到 AI workload scheduling 模型:在我们观测的 Cloud Mac 场景中,大多数 Swap/OOM 更多来自调度问题,而非绝对内存不足

AI
Workload Scheduling
30s
最小 Runbook
0
条 brew install

真正的问题:大多数情况不是内存不够,是没有优先级

表面症状是「本地模型空转 + CI 峰值 + 编码 Agent 变卡」——根因常被误读为「该升配了」。

在我们观测的 Cloud Mac 场景中,大多数 Swap/OOM 更多来自 workload 调度问题,而非绝对内存不足。 同一台 Mac mini 上同时存在三类负载(详见下节):

  • Burst——GitHub Runner / xcodebuild,push 时瞬时 +4–8 GB
  • Interactive——Claude Code,IDE + 终端 + 大仓索引
  • Background——Ollama 等本地推理,8B loaded 后静默占 5–7 GB

三者默认平等抢统一内存,没有人「必须让路」。修正方向:定义 workload 优先级与触发条件,把 background 推理从「装完就常驻」改成可卸载、可被 CI 事件抢占的可调度资源。

错误排班反例(M4 上非常常见,不是偶发)

下面是我们在小团队 Cloud Mac 上真实踩过的组合——可作为 onboarding 时的「禁止模式」:

错误模式 · 三种 workload 默认「全开」

  qwen3:8b  常驻 loaded          →  5–7 GB(background,零调用)
  push 触发 xcodebuild           →  +4–8 GB 峰值(burst)
  Claude Code 同时 index 大仓     →  +2–3 GB(interactive)

实测结果(24GB M4 Mac mini)
  Swap  0 → 2.1 GB
  xcodebuild 链接阶段延迟  +约 40%
  Claude Code 终端响应明显变卡

在我们观测的 M4 统一内存场景下,8B 常驻 + CI burst + 交互式编码同时发生,Swap 非常常见。反例的价值在于:让读者相信 workload scheduling 不是锦上添花,而是多负载并存的前置条件

三种负载形态:burst / interactive / background

排班不能只看时钟。Cloud Mac 上三类 workload 的内存曲线完全不同:

形态 代表 内存特征 调度优先级
Burst xcodebuild、链接、模拟器 L1 Runner 尖峰、不可预测 最高——Fact 不能失败
Interactive Claude Code、IDE、SSH 会话 L3 中等、人在场 高——但推理走 API,不占大模型
Background Ollama embedding、日志摘要 L2 可延迟、可卸载 最低——必须让路给 burst

一句话:L2 是 Stack 里唯一「可以被踢下线」的一层——不是因为它不重要,而是因为它的任务大多可异步、可重试。

内存预算:三种 workload 各占多少

下面以 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 推荐规则

当 memory_pressure 进入 warn / critical(或等效 > ~70%):
  → 自动 ollama stop qwen3:8b / qwen3:14b

当 memory_pressure 正常(< ~50%)且 idle > 10 min:
  → 自动 preload nomic-embed-text(keepalive 10m)

当 CI event trigger(Runner job start):
  → 强制 CI mode:Ollama 大模型全停,优先级 L1 > L3 > L2

当 CI job 成功结束 + 内存回落:
  → 异步触发 L2 批任务(日志摘要 / embedding 重建)

时段排班是 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 RunnerFact——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   # 等待内存回收,勿与 xcodebuild 同秒

- 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
# 然后跑你的日志摘要 / embedding 重建脚本

完整版(生产 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 标准 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() {
  # CI 前:强制 CI mode,L1 优先
  ollama ps || true
  ollama stop qwen3:8b 2>/dev/null || true
  ollama stop qwen3:14b 2>/dev/null || true
  sleep 30
}

ci_post() {
  # CI 后:恢复轻量 embedding(background 最低档)
  ensure_ollama
  ollama run nomic-embed-text --keepalive 10m
}

day_start() {
  # 白天登录 / 开机:只留 embedding 或全停
  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() {
  # 夜间批处理(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 阈值守护
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

# 可选:压力回落 + 无 Runner job 时恢复 embedding
# if echo "$PRESSURE" | grep -qi 'normal'; then ... fi

接入方式示例:

  • GitHub Actions——workflow 首 step 调 ci-pre,末 step 调 ci-post
  • LaunchAgent——登录时 day-start;22:00 cron night-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-pre24GB 口诀:白天可 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 的第一层设计

常见问题

Mac mini 上 Ollama、Claude Code、GitHub Actions 同时跑会 Swap 吗?
会,若 burst / interactive / background 三种 workload 没有优先级。见 § 真正的问题反例

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 workload 如何调度

Cloud Mac AI Stack · 明日预告

Claude Code + MCP:GitHub / 本地文件 / API 怎么连成一条链

L2 排班钉死后,下一层是 L4 Context:MCP 如何把 GitHub、仓库文件与 API 接进 Claude Code 工作流。

返回博客 · Cloud Mac AI Stack 全系列
AI 排班 Cloud Mac 定价