快速结论:当 Ollama 加载一个被截断或格式不完整的 GGUF 模型文件(blob)时,fs/gguf/gguf.go 的 Open() 会在解析 keyValues 前失败并提前返回,已由 newLazy 启动的 iter.Pull goroutine 不会被释放,同时底层文件句柄也未关闭。若反复对同一个坏模型发起 POST /api/show,每次请求都会泄漏 goroutine,长期运行会导致内存与句柄持续增长。优先排查模型 blob 是否被截断,并检查 Ollama 版本是否已包含 Open() 的清理修复。
适用环境:Issue 中确认涉及 Ollama 服务端(ollama serve)、POST /api/show 接口、GGUF 解析模块 fs/gguf/lazy.go 与 fs/gguf/gguf.go。复现环境为 Linux(使用了 stat -c%s、sha256sum、bash),模型以 blob + manifest 形式手动放置。Issue 未提供具体的 Ollama 版本号、Go 版本、CUDA、显卡或 Python 信息,这些项目无需在此补充。
最快修复方案:暂无确认的一步修复方案(没有可直接升级的已知修复版本号)。Issue 中经复现确认的修复是在 Open() 中,当第二次 newLazy(f, f.readKeyValue) 返回错误时,先调用 f.tensors.stop() 释放 iter.Pull 协程,再调用 f.file.Close() 关闭文件描述符,然后再 return nil, err。该改法由报告者在独立 Go 程序中验证,goroutine 计数差值匹配“每次失败的 Open() 泄漏 1 个 goroutine”。
注意事项:上述修复为 Issue 讨论中提出的补丁思路,评论表示愿意提 PR,但在该 Issue 讨论链内未确认已合并发布,普通用户无法仅靠配置规避。修复只针对 keyValues 解析失败这一条路径;评论还指出 os.Open 与第一次 newLazy 之间的早期 return 也会泄漏文件描述符(这些路径不泄漏 goroutine,但同样未调用 f.file.Close()),属于同一类清理缺失问题。临时规避方向是不要让截断/损坏的 GGUF blob 进入模型库并反复请求 /api/show,但该规避未在 Issue 中作为验证方案给出。
问题场景
用户在运行 ollama serve,本地模型库中某个模型的 blob 是一个被截断的 GGUF 文件(例如文件刚好在 tensor count 之后结束,缺少后续的 KV count 字节)。当客户端反复调用 POST /api/show 查询该模型信息,或服务器内部通过 server/images.go 的 GetModel() 打开该模型 blob 时,触发问题。GetModel() 每次请求会至少打开同一 blob 两次(能力探测约第 148 行、直接打开约第 687 行),因此一次请求可能泄漏一个以上 goroutine。Issue 中的复现脚本通过手写 manifest 与 blob、启动服务后循环 30 次请求 /api/show 来触发,服务日志中会反复出现 couldn't open model file error=EOF。
直接现象体现在 HTTP 层是请求返回 500 与 unexpected EOF;更深层的是后台 goroutine 与文件描述符泄漏。Issue 正文说明,纯 HTTP 复现只证明该路径可达,并不直接测量 goroutine 数;要直接测量泄漏,需要在独立 Go module 中用 replace 指向仓库路径,循环调用 gguf.Open("truncated.gguf"),对比显式 runtime.GC() 前后 runtime.NumGoroutine() 的变化。
报错原文
couldn't open model file error=EOF
=== [6/6] Firing repeated POST /api/show against the malicious model ===
request 1: HTTP_CODE=500 BODY={"error":"unexpected EOF"}
核心报错标识:Bug 4: Goroutine Leak on Partial GGUF Header Parse Failure in Lazy Reader。
原因分析
根本原因是 fs/gguf/gguf.go 中 Open() 的错误路径缺少清理逻辑,评论者已确认该泄漏并定位到修复位置。流程如下:
fs/gguf/lazy.go的newLazy[T](约第 20–51 行)先读取 8 字节 item count,再调用iter.Pull(...)(约第 30 行),这会启动一个后台 goroutine。只有返回的stop()闭包能释放它。Open()先用newLazy(f, f.readTensor)(约第 76 行)构建f.tensors,成功并启动一个 goroutine。- 随后
Open()用newLazy(f, f.readKeyValue)(约第 90 行)构建f.keyValues。如果文件在 tensor count 之后正好结束、没有字节留给 KV count,这次binary.Read会失败,newLazy返回(nil, err)。 Open()在该分支直接return nil, err(约第 92 行),没有调用f.tensors.stop(),也没有关闭f.file。唯一会同时调用这两者的地方是File.Close()(约第 310 行),但调用方拿不到*File,因此永远不会走到。- 结果:为
f.tensors启动的 goroutine 永久滞留,处于不可达、不会被回收的状态。
评论者用独立 Go 程序复现,确认每次失败的 Open() 调用泄漏的 runtime.NumGoroutine() 差值恰好为 1。另有一处次要泄漏:os.Open 与第一次 newLazy 之间的早期返回路径(约第 60–74 行)同样没有关闭 f.file,这些路径不泄漏 goroutine,但会泄漏文件描述符,属于同一类清理缺失问题。
环境排查
- 确认 Ollama 版本是否已包含
Open()错误路径的清理修复;如无法确认,请对照当前版本源码中的fs/gguf/gguf.go检查第二次newLazy失败分支是否调用f.tensors.stop()与f.file.Close()。 - 确认模型 blob 是否为完整 GGUF 文件:Issue 复现所用的文件为 16 字节截断样本(magic + version 3 + tensor_count 0,随后 EOF,缺少 kv_count)。可用文件大小与 GGUF header 结构对照,或检查服务日志是否持续出现
couldn't open model file error=EOF。 - 确认模型 manifest 与 blob 校验和是否匹配,避免手工构造 manifest 时出现 digest/size 不一致导致的异常路径。
- 确认测试是否通过真实的
POST /api/show触发GetModel()/gguf.Open()路径;Issue 中的复现使用OLLAMA_HOST=127.0.0.1:28096与独立OLLAMA_MODELS目录,不代表生产配置。 - 若要直接测量 goroutine 泄漏,需要在独立 Go module 中通过
replace github.com/ollama/ollama => <repo path>引入仓库代码,循环调用gguf.Open("truncated.gguf"),对比runtime.NumGoroutine()变化。Issue 未给出具体 Go 版本要求。
解决步骤
- 先确认问题可复现:在独立 Go module 中循环调用
gguf.Open("truncated.gguf"),每次调用失败后执行runtime.GC()并记录runtime.NumGoroutine()。若每次失败的Open()使 goroutine 计数净增 1,即与 Issue 报告的现象一致。 - 在
fs/gguf/gguf.go的Open()中定位第二次newLazy(f, f.readKeyValue)及其后的if err != nil { return nil, err }分支(约第 90–92 行)。 - 在该分支返回前补上清理(Issue 中经验证的改法,可优先尝试):调用
f.tensors.stop()释放由iter.Pull启动的协程,再调用f.file.Close()关闭文件描述符,然后才return nil, err。 - 处理次要泄漏:检查
os.Open到第一次newLazy之间的早期返回路径(约第 60–74 行),在每条return nil, err前补上f.file.Close()。 - 如果当前不在源码层修改,临时缓解做法是移除或隔离已损坏/截断的 GGUF blob,避免持续对其发起
POST /api/show或让GetModel()反复打开它;此规避方式未在 Issue 中作为验证方案,仅作应急。 - 若你在维护 Ollama 分支或等待官方修复,请关注该 Issue 后续是否合入携带上述清理逻辑的 PR。
验证方法
在独立 Go 程序中,对同一个截断 GGUF(例如 Issue 的 16 字节样本)重复调用 gguf.Open,每次失败后执行 runtime.GC() 并读取 runtime.NumGoroutine()。修复前,goroutine 计数会随失败次数线性增长(每次净增约 1);打上清理补丁后,计数应在 GC 后回落、不再随调用次数持续上升,同时文件描述符数量也应稳定不增长。另一个可观察的侧面指标是对坏模型循环请求 /api/show 后,服务端进程的 goroutine 数与打开文件数不再随请求数单调增加。Issue 中的 HTTP 复现已确认日志会持续出现 couldn't open model file error=EOF,但它本身不测量 goroutine 数,因此不能单独作为泄漏修复的验证。
参考来源
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。


