任务完成后,ollama ps 里的模型已经消失,但 Mac 的内存压力和 Runner 进程占用仍然不降。
本周最快动作:先不要继续扩大内存上限,也不要反复缩短上下文;在作业边界使用 keep_alive: 0 或 ollama stop 临时止血,并同时记录 Runner PID、物理内存压力、交换活动和下一次冷加载结果。 如果进程堆随请求持续增长,卸载只是回收手段,不是底层泄漏修复;持续运行的 Agent 服务应进一步采用进程隔离或独立算力节点。
这篇文章适合三类人:
- 批处理开发者:需要把模型回收纳入每个批次的生命周期。
- AI Agent 工程师:服务必须持续运行,不能因为一次卸载破坏会话与工具调用。
- 技术负责人:需要在冷加载成本、任务稳定性和扩容方式之间做容量决策。
最后更新于 2026 年 9 月 15 日,数据核实自 Ollama 官方 v0.34.0 发布页、API 文档、macOS 日志说明、Apple Metal 文档及相关公开 Issue。
先确认 Ollama 0.34.0 DeepSeek-R1 内存不释放属于哪一类问题
Ollama 默认会让模型在请求结束后继续驻留一段时间。官方 API 文档将 keep_alive 的默认值写为 5 分钟,而设置为 0 才表示在请求结束后立即卸载模型。模型仍显示在运行列表中,并不能直接证明发生了泄漏。(Ollama API 文档)
在 Apple Silicon 上,CPU 与 GPU 共享统一内存。Metal 的 hasUnifiedMemory 用来表示 GPU 是否与 CPU 共享全部内存,而 recommendedMaxWorkingSetSize 只是建议的工作集范围,不是可以随意覆写的“安全上限”。因此,系统出现交换活动或内存压力,不一定意味着 Ollama 没有释放模型;也可能是模型权重、文件映射、KV Cache、压缩内存和 Runner 堆同时存在。(Apple Metal 统一内存说明)
任务结束后为什么 Mac 仍然占用大量内存?
通常要先排除三种情况:
- 模型仍在 keep-alive 驻留期内。
ollama ps还能看到模型,说明服务认为它仍可复用。 - 模型列表已清空,但系统缓存尚未立即回落。 macOS 会保留文件缓存,并通过压缩内存和交换机制维持活跃进程。
- 模型已卸载,但旧 Runner 进程的堆或匿名内存没有随模型一起释放。 这才是需要重点验证的路径。
Apple 对活动监视器的说明也提醒,Memory Pressure 由空闲内存、交换速率、wired memory 和文件缓存等因素共同决定;单看“内存已用”或某一次 Activity Monitor 读数,无法确认回收是否成功。(Apple 活动监视器内存压力说明)
降低上下文不一定有效,先找出真正的增长变量
如果内存增长主要来自 KV Cache,缩短上下文通常会改变占用;但如果增长跟随请求数量或并发度,继续把上下文从 16K 降到 8K、再降到更低,可能只会牺牲输出能力,却没有解决 Runner 堆增长。
建议使用同一个 DeepSeek-R1 模型建立三个最小复现序列:
- 固定请求内容,只改变
num_ctx; - 固定上下文,只增加请求数量;
- 固定请求数量,分别使用单并发和多并发。
每一轮都记录请求编号、上下文参数、并发数量、Runner PID、vmmap 物理占用、交换活动和 ollama ps 返回值。macOS 上可以使用:
pgrep -alf 'ollama|runner|llama'
vmmap -summary <Runner-PID>
公开社区案例显示,Ollama 0.32.15 在 macOS / Metal 上出现过“/api/ps 中模型占用稳定,但 llama-server 进程堆随请求量增长”的报告;该案例使用的是其他模型,并非 DeepSeek-R1,也不是 Ollama 0.34.0 的官方确认结论。报告中的一次测试记录了进程物理占用从 8.25 GB 增长到 13.74 GB,且降低上下文没有改变增长趋势。这个数字只能作为排查线索,不能直接外推到当前环境。(Ollama 社区 Issue:macOS / Metal Runner 堆增长案例)
因此,读者应使用自己的 DeepSeek-R1 请求序列完成最小复现,再决定是否采用回收方案。不要把其他模型、其他版本的社区现象直接改写成 Ollama 0.34.0 的已确认缺陷。
第二步:分别验证 API 卸载、命令行停止和服务重启
keep_alive=0 只是在 API 层请求模型卸载。官方文档规定,当请求内容为空并且 keep_alive 为 0 时,接口会返回 done_reason: "unload";这说明卸载动作被 API 接受,但不等于宿主机所有内存立刻回到基线。(Ollama API 文档中的卸载语义)
API 服务场景
可以先记录 PID 和内存状态,再发起卸载:
MODEL='deepseek-r1:你的标签'
pgrep -alf 'ollama|runner|llama'
vm_stat
curl http://127.0.0.1:11434/api/chat \
-d "{\"model\":\"${MODEL}\",\"messages\":[],\"keep_alive\":0}"
curl http://127.0.0.1:11434/api/ps
重点检查四项:
- 返回对象是否包含
done_reason: "unload"; /api/ps是否不再列出目标模型;- 原 Runner PID 是否退出或发生变化;
- 新请求是否出现新的加载过程,而不是继续复用旧进程。
如果模型从 /api/ps 消失,但原 PID 仍然存在,或者物理占用没有回落,就不能把“列表清空”当成“内存已经释放”。
命令行场景
可以使用:
ollama ps
ollama stop deepseek-r1:你的标签
ollama ps
pgrep -alf 'ollama|runner|llama'
如果是手动执行 ollama serve,服务日志就在启动它的终端中;如果是 Ollama.app,macOS 日志通常位于 ~/.ollama/logs/server.log,GUI 相关日志还可能出现在 app.log。(Ollama macOS 日志与故障排查说明)
如果服务重启后问题消失,建议保留重启前后的日志、PID 和 vmmap 摘要,而不是立即删除日志。若需要撤销临时调试设置,应关闭服务后移除 OLLAMA_DEBUG 或其他实验性环境变量,再重新启动 Ollama.app 或 ollama serve。
用这张表判断是驻留、缓存还是 Runner 堆增长
| 观察结果 | 更可能的原因 | 下一步验证 | 临时处理 |
|---|---|---|---|
ollama ps 仍有模型,等待后消失 |
keep-alive 驻留 | 查看模型过期时间和 API 返回状态 | 在作业边界使用 keep_alive: 0 |
| 模型从列表消失,Runner PID 也退出 | 模型已完成卸载 | 冷加载一次,比较新旧 PID 与加载结果 | 批次结束回收即可 |
| 模型从列表消失,但旧 PID 仍在 | Runner 或服务层未完全退出 | vmmap -summary、服务日志、交换活动 |
完整重启服务并保存证据 |
| 上下文改变后占用明显变化 | KV Cache 或工作集受上下文影响 | 固定请求量,比较不同 num_ctx |
降低上下文,但不要把它当泄漏修复 |
| 请求数量增加时堆持续增长 | 可能是请求路径或 Runner 堆问题 | 固定上下文和并发,按请求编号采样 | 按作业边界隔离或卸载 |
| 多个调用方同时请求,卸载后出现重复加载 | 并发误卸载 | 检查队列、请求状态和 PID 变化 | 排空队列,使用独占回收锁 |
第三步:不要把每个请求都变成一次冷加载
keep_alive=0 的成本不是抽象的“性能下降”,而是下一次请求必须重新加载模型。重新加载时间、首个请求延迟和模型切换成本取决于模型文件、统一内存容量、磁盘、并发策略及实际服务方式,不能用一个通用秒数替代实测。
因此,回收边界应按工作负载选择:
- 单次短任务: 请求结束后不必立即卸载,先按批次回收。
- 短批处理: 一个批次完成后卸载,再启动下一个批次。
- 连续对话: 不要逐请求卸载,否则会破坏上下文复用。
- Agent 队列: 在队列排空、工具调用完成、失败重试状态清空后回收。
- 多人共享 API: 不能由某一个调用方直接执行卸载。
把 keep_alive 设为 0 能不能修复 Ollama 内存泄漏?
不能。它能请求模型立即卸载,并且在 Runner 随模型退出时回收一部分堆内存;但它无法修复底层分配器、请求路径、Metal 工作集或 Runner 生命周期中的缺陷。官方文档确认的是卸载语义,不是“泄漏修复保证”。
并发 Agent 必须先排空队列,再执行回收
持续运行的 DeepSeek-R1 Agent 服务最容易出现误卸载:任务 A 认为当前批次结束,调用 keep_alive=0;任务 B 仍在等待工具结果,随后不得不重新冷加载模型,甚至收到连接中断。
我们建议在 API 层增加以下保护:
- 使用独占作业锁,只有持锁任务可以发起回收;
- 回收前把新请求标记为排队,不再直接送入 Runner;
- 等待生成、工具调用和重试全部完成;
- 检查活动请求数为零,再发送卸载请求;
- 卸载后确认旧 PID 已退出或服务状态已变化;
- 用一个最小健康请求验证模型能够重新加载;
- 失败时只重试一次,随后转入隔离节点或人工处理。
定时批处理则应把“任务成功率”和“回收后的首次加载是否成功”一起记录。不要把定期强杀进程写成所有生产环境的标准修复;强杀可能丢失请求、破坏工具调用状态,还会掩盖真正的版本问题。
第四步:持续失控时,停止修改内核参数
如果已经完成了以下对照,仍然出现请求后堆持续增长,就不建议继续试验未经官方确认的 sysctl 数值或扩大系统内存上限:
- 固定模型和请求序列;
- 固定上下文与并发;
- 记录每个阶段的 Runner PID;
- 对照
/api/ps与vmmap; - 比较卸载前后的物理占用和交换活动;
- 完成一次重新加载;
- 保存
server.log与失败请求证据。
Apple 的 Metal 文档提供的是资源工作集和当前分配量的定义,不是为某个 Ollama 版本给出可直接套用的内核覆写值。(Apple Metal 工作集指标说明)
持续运行 DeepSeek-R1 时多久重启一次 Runner?
没有可靠的通用时间间隔。重启频率应由请求数量、任务完成率、冷加载影响、内存增长曲线和节点可替换性决定,而不是由“每隔几小时重启一次”的经验值决定。
如果每次完成一小段任务就必须回收,说明当前服务边界与负载不匹配;如果回收会频繁打断 Agent 会话,也说明应该改用进程隔离,而不是继续让多个调用方共享一个长期 Runner。
最终决策:继续本机、拆分作业,还是迁移独立 Mac 算力
可以用下面的清单完成一次可复现验收:
- [ ] 已确认模型是否仍处于官方 keep-alive 驻留期;
- [ ] 已同时记录
ollama ps、Runner PID、物理占用和交换活动; - [ ] 已区分模型权重、KV Cache、文件映射与进程堆;
- [ ] 已分别测试请求数量、上下文长度和并发度;
- [ ] 已验证
keep_alive=0返回的卸载状态; - [ ] 已确认旧 Runner 是否退出,而不是只看模型列表消失;
- [ ] 已完成一次冷加载,并记录任务是否中断;
- [ ] 已为共享 Agent 增加队列排空与独占回收锁;
- [ ] 已保留服务日志,避免把重启后的“恢复”误判为根因修复;
- [ ] 已设定退出条件:任务失败、冷加载影响或恢复频率超过业务可接受范围。
如果增长只出现在单个批次,且作业边界卸载后能稳定回收,可以继续本机运行;如果连续服务不稳定,可以先拆分批次、隔离 Runner 和调用方;如果必须频繁回收才能完成任务,就应把节点可替换性和故障恢复纳入评估。
对于 Mac 选型,可以先参考我们的 M4 Mac mini 与 M5 MacBook Air 节点选择分析,再结合 Mac mini 租赁交付验收要点核对远程连接、服务启动和故障恢复边界。
如果当前方案是单台本地 Mac 长期承载共享 Agent,它的真实缺点通常是:Runner 与其他开发任务争用统一内存、故障后需要人工重启、冷加载会影响所有调用方,而且无法快速替换节点。此时,先用自己的真实请求批次完成一次“运行—卸载—重新加载”对照;若仍需频繁回收才能保证成功率,可以进一步评估 JexMac 的 Mac 节点方案,把独立进程、可替换算力和故障恢复成本一起纳入测试,而不是继续冒险覆写系统内存参数。
内存迟迟不释放?把 AI 任务迁移到 JexMac 独享 Mac
使用 JexMac 真实 Mac mini M4 裸金属云主机,将批处理、长文本和 AI Agent 任务从本地设备分离,减少内存压力。