第 5 章:Scheduler——把显存、并发和生命周期变成队列问题¶
一、为什么 handler 不能直接 NewLlamaServer¶
一个请求要用模型,并不等于应该为它启动一个新进程。真实约束包括:
- 同一个模型已经在内存中,应该复用。
- 多个请求可能共享 runner 的 slots。
- 新模型加载前,可能必须卸载旧模型释放显存。
- CPU/GPU 混合卸载要依据设备能力决定。
- 自动 context/batch 可能需要在 OOM 后降低再试。
keep_alive决定 runner 是否在空闲后保留。
这些都是跨请求的状态,所以放在 server/Scheduler 中集中管理。Scheduler 的主要类型和循环见 server/sched.go。
二、核心对象¶
LlmRequest
├── model / options / keepAlive
├── completion / embedding / vision 等请求属性
├── response channel
└── runner channel
Scheduler
├── pending requests
├── loaded runners
├── completed requests
├── systemInfo / GPU snapshot
└── wake/signal channels
runnerRef
├── model
├── llama.LlamaServer
├── session expiry
├── busy slots
└── memory placement
准确字段会随版本变化,但设计关系稳定:请求是短生命周期,runner 是长生命周期,Scheduler 负责把二者绑定和解绑。
三、调度循环¶
stateDiagram-v2
[*] --> Pending
Pending --> ReuseLoaded: 找到可用 runner
Pending --> Load: 没有 runner 或需换模型
Load --> Ready: 进程启动并加载成功
Load --> RetryWithSmallerContext: 自动参数导致内存不足
RetryWithSmallerContext --> Ready
Load --> Failed: 无法加载
Ready --> Running: 分配 slot
ReuseLoaded --> Running
Running --> Completed: 请求完成/取消
Completed --> KeepAlive: keep_alive 未到期
Completed --> Unload: 过期或回收
KeepAlive --> Running: 新请求复用
Unload --> Pending: 资源释放后继续排队
Scheduler.Run 启动处理循环;processPending 负责决定谁可以被加载/运行,processCompleted 负责释放、更新状态和继续排队。读源码时要特别区分“请求完成”和“runner 卸载”:两者不是同一事件。
四、getRunner 的等待语义¶
scheduleRunner 不是同步调用 getRunner 返回一个对象,而是等待 runnerCh 或 errCh。这种 channel 设计表达了一个重要事实:runner 可能要等,等待期间 handler 可以阻塞当前请求,但不会阻塞整个 HTTP server。
如果队列达到 OLLAMA_MAX_QUEUE,请求会在进入真正加载前被拒绝;如果 runner 已加载但没有 slot,Scheduler 可以让请求保持 pending,而不是重新启动模型。
五、模型匹配与复用¶
Scheduler 会用模型标识和关键 options 计算 runner key。复用不能只看模型名,因为以下参数可能改变 runner 的兼容性:
- context length
- batch size / parallel slots
- flash attention 与 KV cache 类型
- draft model / speculative decoding
- projector、adapter、vision 配置
- context shift
这就是为什么 schedulerModelKey、effectiveContext、resolveContextShift 等函数值得单独读:它们把“请求参数”翻译成“运行时可复用性”。
六、显存不足时的自动降级¶
当用户没有显式设置 num_ctx 或 num_batch 时,Scheduler 可以预测显存并选择一个自动值。如果加载失败,reduceAutoNumCtxForLoadOOM 会尝试降低自动 context;这只对自动值生效,避免悄悄改变用户明确指定的资源要求。
显式 num_ctx
→ 失败就报告,不能擅自改语义
自动 num_ctx
→ 预测/加载失败
→ 降低 context
→ 重新计算 batch / VRAM
→ 再尝试
这是一种“可用性优先但尊重显式配置”的降级策略。
七、keep-alive 与回收¶
请求结束时 runner 通常进入可复用状态,附带一个过期时间。keep_alive 为 0 时立即卸载;正值则保留;某些默认值由 envconfig.KeepAlive() 控制。
回收不仅释放进程,还要更新:
- loaded model 列表
- GPU/VRAM 占用
/api/ps结果- 等待中的请求可否继续
八、为什么 Scheduler 是独立包内的状态机¶
如果每个 handler 自己管理 runner,会出现:
/api/chat和/api/embed各自加载同一模型。- 一个请求卸载了另一个请求仍在使用的进程。
- OOM 重试策略不一致。
- API 取消时没有可靠地停止子进程。
Scheduler 让这些动作具有单一所有者;handler 只提交 LlmRequest 意图。这也是 Ollama 从“命令行启动一个模型”成长为“多客户端本地服务”的关键一步。
九、阅读练习¶
按以下顺序阅读,比直接扫 900 行调度代码更有效:
LlmRequest、Scheduler、runnerRef类型。InitScheduler和Run。getRunner的请求入队处。processPending中“复用 vs 加载”的分支。load中设备探测、OOM 降级和 runner 启动。processCompleted与expireRunner。