在 Mac mini / Cloud Mac 上同时运行 Ollama、Claude Code 和 GitHub Actions 时,最常见的问题不是「性能不够」,而是系统开始 Swap、CLI 卡顿、CI 构建变慢。
本篇不讨论工具安装,而是解释:为什么三种 AI workload 同时存在时会出现内存抖动,以及如何通过调度避免它。下文给出反例、内存预算、阈值调度、30 秒 Runbook 与完整脚本;也不重复16GB vs 24GB benchmark。
深入阅读时会看到 AI workload scheduling 模型:在我们观测的 Cloud Mac 场景中,大多数 Swap/OOM 更多来自调度问题,而非绝对内存不足。
真正的问题:大多数情况不是内存不够,是没有优先级
表面症状是「本地模型空转 + 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 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 # 等待内存回收,勿与 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 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 三种 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 全系列