Scheduler unloadedCh is an untyped (chan any) wakeup signal

问题出现在 Ollama 调度器(Scheduler)中,当多个请求并发加载/卸载模型时触发。具体场景是: - 请求 A 加载模型 M(runner R1,refCount=1) - 请求 B 加载模型 N,需要驱逐 runner R1,标记 R1 过期并等待 unloadedCh - 同时,请求

Scheduler unloadedCh is an untyped (chan any) wakeup signal

Scheduler unloadedCh is an untyped (chan any) wakeup signal

快速结论:该报错由 bug-hunt-council 在 Ollama 调度器的代码审计中发现,涉及 server/sched.gounloadedCh 通道为无类型 chan any 的设计问题。优先排查是否因并发卸载模型时信号无身份标识导致 runner 泄漏或状态不一致,但最终结论认为这是一个良性唤醒信号,删除操作已按 modelKey 正确索引。

问题场景

问题出现在 Ollama 调度器(Scheduler)中,当多个请求并发加载/卸载模型时触发。具体场景是:
– 请求 A 加载模型 M(runner R1,refCount=1)
– 请求 B 加载模型 N,需要驱逐 runner R1,标记 R1 过期并等待 unloadedCh
– 同时,请求 C 加载模型 P,驱逐另一个 runner R2
– R2 的卸载先完成,发送信号到 unloadedCh
– 请求 B 错误地收到了 R2 的信号,以为自己等待的 R1 已卸载,但 R1 仍在运行
– 导致 loaded map 状态与实际 runner 状态不一致,可能引发 runner 泄漏或损坏。

报错原文

// server/sched.go:65
unloadedCh    chan any

// server/sched.go:472 — sender (inside expiredCh case, after runner.unload())
s.unloadedCh <- struct{}{}  // NO runner identity

// server/sched.go:361,366,1644 — receivers
case <-s.unloadedCh:  // receives ANY unload event, can't tell which runner

原因分析

可能原因:unloadedChchan any 类型,发送端仅发送裸 struct{}{} 而不携带 runner 身份标识。当多个 runner 并发过期时,等待特定 runner 的接收方可能错误地消费另一个 runner 的信号,导致 loaded map 中删除错误的 runner 或未能删除正确的 runner,违反“每个 unloadedCh 信号必须对应特定被过期 runner”的不变量。

但 Issue 评论中,提交者经复查认为:删除操作在 PR #10819 中已修复为按 modelKey 索引,因此该无类型通道仅是一个良性唤醒信号,不会导致错误的 runner 删除。问题严重性被高估。

环境排查

  • 确认 Ollama 版本:需包含 PR #10819 修复(检查 server/sched.godelete 操作是否按 modelKey 索引)
  • 确认是否使用并发加载/卸载模型的操作(例如同时加载多个不同模型)
  • 压力测试时可使用 go test -race ./server/ -run TestScheduler -count 100 触发竞态

解决步骤

  1. (可优先尝试) 升级到包含 PR #10819 修复的 Ollama 版本。该 PR 已修正 loaded map 中的删除操作为按 modelKey 索引,确保即使信号无身份标识,删除逻辑也不出错。
  2. 若问题仍复现,考虑实施 Issue 中建议的修复方案之一:
    – 将 unloadedCh 类型从 chan any 改为 chan string(modelKey),在发送时传入 runner.modelKey,接收方只处理匹配的 key。
    – 或改为每个 runner 使用独立的 chan struct{},等待方阻塞在自己 runner 的通道上,彻底消除信号混淆。
  3. 添加并发身份验证测试,例如:
    func TestSchedulerConcurrentEvictionNoCrossTalk(t *testing.T) {
        // 加载3个模型,触发并发驱逐其中2个
        // 断言:每个 unloadedCh 信号对应正确的 runner
        // 断言:loaded map 状态与驱逐后实际 runner 状态一致
    }

验证方法

运行压力测试 go test -race ./server/ -run TestScheduler -count 100,确认无竞态错误且 loaded map 与 runner 实际状态一致。在业务代码中观察是否仍有 runner 泄漏(例如加载多个模型后卸载,看进程是否持续存在残余 runner 线程)。

参考来源

ollama/ollama #17309

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

celebrityanime
celebrityanime
文章: 14702

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注