Ollama 显存不释放怎么修:keep_alive 四写法实测

一句话结论:Ollama 释放显存不用重启服务,请求里传 keep_alive 0 就会卸载;代价是下一次请求要重新加载权重,本机实测多花 0.81 到 3.2 秒。批量跑 100 条任务时这样设,等于白送一分半钟给加载。想常驻就传 -1,但记得跑完手动 stop,否则它会一直挂在内存里。

一、三条命令:卸载、常驻、查状态

先把能直接粘贴的三条放前面。全部针对 11434 端口的本地服务,桌面版和 CLI 共用这一个端口。

# 立刻卸载,释放显存与内存
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"qwen3:8b","keep_alive":0}'

# 常驻 30 分钟(每次请求都会把倒计时续到 30 分钟)
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"qwen3:8b","keep_alive":"30m"}'

# 看当前谁在内存里、还剩多久
ollama ps
curl -s http://127.0.0.1:11434/api/ps

这三条不需要真跑推理。prompt 传一个字符就够,实测 0.16 秒返回,因为服务只做加载与卸载,不生成 token。ollama ps 的 UNTIL 列会直接告诉你还剩多久,API 版本看 expires_at 字段,精度到纳秒。

二、四种写法分别会发生什么

keep_alive 接受 Go 的时长格式(30m、1h、90s)和纯数字(单位秒)。正数代表驻留时长,0 代表立刻卸载,负数代表永久驻留。行为差异比文档写的细,下表是 0.40.14 上逐条跑出来的结果。

写法 服务端行为 实测结果
不传 用环境变量 OLLAMA_KEEP_ALIVE,未设则 5 分钟 新加载时 expires_at 为当前时间加 5 分钟
0 或 “0s” 响应结束后卸载 0.2 秒内 /api/ps 清空,llama-server 进程退出,RSS 归零
“90s” 或 90 90 秒内保持,每次请求重置 窗口内再发请求,expires_at 跟着后移 3 秒
-1 或 “-1m” 永久驻留 expires_at 显示 2319 年,ollama ps 的 UNTIL 列写 Forever

-1 对应的 2319 年不是玩笑,那是服务端用一个超大时长算出来的绝对值。看到这个年份就说明模型不会自己走,只能靠 stop 或者显式改短。

三、换出一次到底花多少时间

测试机是 M4 16GB 统一内存,Ollama 0.40.14,模型 qwen3:8b(5.2GB),上下文 4096。同一台机器上跑,只改一个变量,记录墙钟耗时与响应里的 load_duration。

场景 墙钟耗时 load_duration
服务刚起来,第一次加载 4.28 秒 3.86 秒
卸载后立刻重载 2.74 秒 2.56 秒
权重已被系统缓存,再重载 0.93 秒 0.81 秒
模型常驻,直接出结果 0.23 秒 0.01 秒
keep_alive 0 连发三次 0.16 / 0.99 / 0.99 秒 0 / 0.81 / 0.81 秒

最后一行值得多看两眼。第一次 0.16 秒是因为模型当时还热着,复用之后顺手卸载;后两次各自完整走了一遍加载,每次 0.8 秒。也就是说 keep_alive 0 的语义是每次请求结束都拆掉,下一条请求从零开始。

短请求的差距最刺眼。同样一句两个字的提示,常驻状态 0.23 秒,卸载状态 0.99 秒,慢了 4 倍多。如果你在跑批处理,100 条就是 100 次加载,按 0.81 秒算白丢 81 秒,冷缓存时按 3.86 秒算就是 386 秒。

四、设了 5 分钟,为什么半小时还在

因为倒计时是滑动的,每次请求都从零开始算。实测把窗口设成 90 秒,等 3 秒再发一次请求,expires_at 从 21:40:04 变成 21:40:07,正好后移了 3 秒。只要你请求的间隔小于窗口长度,模型永远到不了过期时间。

还有一个更常见的误会:用 ollama run 聊完,输入 /bye 退出,以为模型释放了。实测退出后 ollama ps 仍显示 qwen3:8b,UNTIL 列写 4 minutes from now。终端退出的只是客户端,服务端按 5 分钟窗口继续养着它。要立刻放掉就 ollama stop qwen3:8b,实测 0.09 秒完成,进程随即消失。

五、-1 之后改不回来,这是最容易踩的一条

显式传过一次 keep_alive,后续不带这个字段的请求会沿用上一次的窗口,不会回落到默认 5 分钟。实测先发 keep_alive -1,再连发两次不带 keep_alive 的请求,expires_at 一直停在 2319 年,只是跟着请求时间往后挪了两秒。

想改回来必须显式覆盖。发一次 keep_alive 传 5m,expires_at 立刻变成当前时间加 5 分钟。这条在排查时特别有用:同事抱怨机器被模型占满,你去 ollama ps 看到 Forever,就知道有人曾经发过 -1,而且后面所有请求都在默默继承这个设定。

服务重启才会复位成 5 分钟。这是唯一不靠显式传参的复位方式。

六、两个模型互相踢:16GB 机器只能留一个

Ollama 启动日志里有一行 vram-based default context,后面写着 total_vram=11.8 GiB。这就是它认为自己能用的显存上限,不是机器标称的 16GB。两个 5GB 级模型塞不进 11.8GB。

实测先常驻 qwen3:8b 和一个 1.2GB 的 embedding 模型,再加载 qwen3.5:9b,前两个当场被踢掉,/api/ps 只剩 9b。窗口长短救不了,因为把模型挤走的是容量,跟时间窗口没有关系。

动作 墙钟耗时 换出结果
加载 qwen3:8b 2.77 秒 8b 在位
切到 qwen3.5:9b 4.03 秒 8b 被踢
切回 qwen3:8b 3.25 秒 9b 被踢
再切到 qwen3.5:9b 3.80 秒 8b 被踢

来回切四次,没有一次命中缓存,每次都在重新加载。这种场景下 keep_alive 已经没有意义,因为模型是被容量挤走的。要真让两个模型共存,只有三条路:换更小的量化、把 OLLAMA_MAX_LOADED_MODELS 设成 2 并且总占用不超上限、或者干脆开两个进程分机器跑。

补充一条对比:设 OLLAMA_MAX_LOADED_MODELS=2 之后,5.28GB 的 8b 和 4.68GB 的 embedding 确实同时在位,合计 9.96GB,没超 11.8GB。上限是容量与这个变量共同决定的,谁先到谁说话。

七、OpenAI 兼容端点认 keep_alive,但不认 num_ctx

很多人以为 /v1/chat/completions 只实现了一半参数,所以 keep_alive 也无效。实测是有效的:带 keep_alive 0 请求之后 /api/ps 立刻清空,不带则新加载给 5 分钟窗口。两个端点在这件事上表现一致。

但上下文长度是反例。站内另一篇实测过,num_ctx 走 /v1 端点时传顶层或塞进 options 都返回 200,实际 context_length 恒为 4096。所以在这套接口上,能改的是驻留时长,改不了上下文。

用 Python 的 openai SDK 时,keep_alive 不是标准字段,得走 extra_body:

from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:11434/v1", api_key="ollama")
r = client.chat.completions.create(
    model="qwen3:8b",
    messages=[{"role": "user", "content": "hi"}],
    extra_body={"keep_alive": "30m"},
)

八、三个失败现场与判断方法

第一个是参数写错。传 keep_alive abc 会直接回 400,错误体是 time: invalid duration,值必须写成 30m、90s 这类 Go 时长,或者纯数字秒。中文单位写 30分钟 同样报这个错。判断方法只有一条:看 400 的 error 字段,出现 time: invalid duration 就是格式问题,与服务无关。

第二个是内存被别的东西占住,请求静默返回空结果。测试时开第二个 Ollama 实例占掉 9.9GB,再请求 32768 上下文,HTTP 状态码照样 200,响应体是 model 为空串、done 为 false、response 为空。客户端只判状态码就会把空字符串当成正常输出,然后在下游报一个跟模型毫无关系的错。判断方法:检查响应里的 model 字段,空串就是没加载起来。

第三个是卸载了但内存看着没回来。llama-server 进程确实退了,ps 查 RSS 是 0,可系统内存占用掉得慢。权重文件留在系统页缓存里,这属于正常的缓存行为,内存并没有漏。反过来这正是第二次加载只要 0.81 秒的原因,冷启动要 3.86 秒。判断方法:ps -Ao rss,comm 查不到 llama-server 就说明已经释放,别再去重启服务。

九、按场景抄这三段配置

离线批处理最怕每条都重载。把 keep_alive 放进每次请求,跑完统一 stop,比在客户端 sleep 更省事:

# 批处理:先常驻,跑完再卸
curl -s http://127.0.0.1:11434/api/generate -d '{"model":"qwen3:8b","keep_alive":"-1"}'
# ... 循环发你的任务 ...
ollama stop qwen3:8b

交互式开发给 30 分钟就够。上午写代码时模型一直热着,午休回来重新加载一次,代价 0.8 到 3 秒,可接受。给 -1 然后忘记 stop,是这台机器后来变卡的常见起点。

一次性脚本或者 CI 里,keep_alive 0 反而更干净,容器退出前不会留驻留进程。但要知道循环 100 次会多付 81 到 386 秒的加载时间,能合并成一条长请求就别拆成一百条短请求。

要在服务端统一改默认值,用环境变量启动,实测生效:

OLLAMA_KEEP_ALIVE=15s OLLAMA_MAX_LOADED_MODELS=2 ollama serve

这样起来之后,不带 keep_alive 的请求会在 15 秒后卸载,实测 expires_at 正好是当前时间加 15 秒。注意这个变量只在服务启动时读一次,改完必须重启才生效,而且它管不到已经加载的模型。

十、常见问题

问:keep_alive 0 会损坏模型文件吗?
不会。卸载只是结束推理进程、释放内存,权重文件是只读的,下一次加载重新 mmap 一遍。模型文件 5225374496 字节,反复卸载加载后大小与修改时间都没有变化。

问:ollama ps 显示 Forever,怎么恢复默认?
显式发一次带 keep_alive 5m 的请求,或者 ollama stop 直接停掉。只关终端窗口没有用,服务端会继续按 5 分钟窗口养着模型。

问:已经设了 0,为什么连着两条请求还是慢?
因为每条请求结束都卸载,下一条从零加载。实测第一次 0.16 秒是复用了已热模型,后面两次各 0.99 秒。批处理要用 -1 常驻,别用 0。