feat(fam-edge): 启动时预热 Ollama,避免兜底触发时冷启动等 1-2 分钟
排查"Ollama 模型没有常驻内存":systemd 里 OLLAMA_KEEP_ALIVE=-1 其实一直配置 正确,但这个环境变量只保证"加载过之后不因为空闲被换出",不负责启动时主动 预加载。查 model_calls 表,ollama provider 从来没有一条调用记录——因为它是 问答链路最后一级兜底,前面 NVIDIA/Gemini 一直调用成功,从未真正轮到它, 自然也就从未被加载进内存过。 新增 OllamaAdapter.warm_up():送一条 num_predict=1 的最小请求强制模型加载, 超时给够 180s(冷启动实测能到 1-2 分钟)。app.py 启动时在后台线程调用,不 阻塞主服务启动;找不到 ollama 配置或预热失败都只记警告,不影响服务本身。 实测部署验证:重启后 36 秒完成预热,ollama ps 显示 qwen2.5:7b 已加载, expires_at 显示不过期(keep_alive=-1 生效)——真正需要兜底的那一刻不会再有 冷启动延迟。 新增 test_ollama_adapter.py 4 个用例覆盖预热成功/HTTP失败/异常不上抛/超时 时长。
This commit is contained in:
@@ -8,6 +8,7 @@ FAM-Edge 主应用 - Flask 单进程(新架构 v2.1)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from flask import Flask, jsonify
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -18,6 +19,7 @@ from .api_gateway.api_gateway import api_bp
|
||||
from . import state
|
||||
from .video_queue import VideoQueue
|
||||
from .person_service import PersonService
|
||||
from .model_adapters.ollama_adapter import OllamaAdapter
|
||||
|
||||
logger = setup_logger('fam-edge.app')
|
||||
|
||||
@@ -47,6 +49,21 @@ try:
|
||||
except Exception as e:
|
||||
logger.error(f"后台服务启动失败: {e}", exc_info=True)
|
||||
|
||||
# 启动时后台预热 Ollama(问答链路最末位兜底,实测从未被自然触发过,
|
||||
# OLLAMA_KEEP_ALIVE=-1 只保证加载后不换出、不负责主动预加载)。后台线程跑,
|
||||
# 不阻塞 gunicorn worker 启动;找不到 ollama 配置项或预热失败都只记警告。
|
||||
try:
|
||||
_ollama_cfg = next(
|
||||
(m for m in load_config().get('models', []) if m.get('provider') == 'ollama'),
|
||||
None)
|
||||
if _ollama_cfg and _ollama_cfg.get('enabled', False):
|
||||
threading.Thread(
|
||||
target=lambda: OllamaAdapter(_ollama_cfg).warm_up(),
|
||||
daemon=True, name='ollama-warmup').start()
|
||||
logger.info("Ollama 预热任务已在后台启动")
|
||||
except Exception as e:
|
||||
logger.warning(f"Ollama 预热任务启动失败(不影响主服务): {e}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cfg = load_config()
|
||||
|
||||
@@ -33,6 +33,32 @@ class OllamaAdapter(BaseModelAdapter):
|
||||
enabled=cb_cfg.get('enabled', False) # 本地模型默认不启用
|
||||
)
|
||||
|
||||
def warm_up(self) -> bool:
|
||||
"""启动时主动送一次最小请求,把模型强制加载进内存。
|
||||
|
||||
背景:OLLAMA_KEEP_ALIVE=-1(systemd 环境变量已配置)只保证"一旦加载过
|
||||
就不再因为空闲被换出",但不会在服务启动时主动预加载——Ollama 现在只在
|
||||
问答链路最末位兜底(前面 NVIDIA/Gemini 一直成功的话永远轮不到它),
|
||||
实测 model_calls 表里从来没有一条 ollama 记录,说明模型从未被加载过。
|
||||
真正需要兜底的那一刻才现加载,用户会等上首次冷启动的 ~1-2 分钟
|
||||
(见 README 6.2 冷启动实测数据)。启动时主动预热一次,之后就一直
|
||||
常驻内存,兜底真正触发时不再有冷启动延迟。
|
||||
"""
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{self.base_url}/api/generate",
|
||||
json={"model": self.model_name, "prompt": "hi", "stream": False,
|
||||
"options": {"num_predict": 1}},
|
||||
timeout=180, # 冷启动可能到 1-2 分钟,给足时间
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
logger.info(f"Ollama 模型预热完成: {self.model_name}")
|
||||
return True
|
||||
logger.warning(f"Ollama 预热失败: HTTP {resp.status_code} {resp.text[:200]}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Ollama 预热异常(不影响服务启动,问答兜底时会正常现加载): {e}")
|
||||
return False
|
||||
|
||||
def health_check(self) -> bool:
|
||||
"""GET /api/tags,检查模型是否可用"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user