2026-08-30 22:22:11 +08:00
|
|
|
|
"""共享推理子进程池 — 多路摄像头复用同一组 YOLO 引擎,避免每路重复加载模型。
|
|
|
|
|
|
|
|
|
|
|
|
阶段2架构:
|
|
|
|
|
|
- 主进程启动 N 个 InferenceWorker 子进程(默认 1,可配置 analysisInferenceWorkers)
|
|
|
|
|
|
- 子进程内按 algorithm_id 缓存引擎实例
|
|
|
|
|
|
- 请求/响应通过 multiprocessing.Queue 传递;帧以 JPEG 压缩跨进程(体积可控)
|
|
|
|
|
|
"""
|
|
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import multiprocessing as mp
|
|
|
|
|
|
import os
|
|
|
|
|
|
import queue
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("analysis.inference_pool")
|
|
|
|
|
|
|
|
|
|
|
|
_POOL = None
|
|
|
|
|
|
_POOL_LOCK = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_model_path(model_file):
|
|
|
|
|
|
from app.analysis.worker_pool import resolve_model_path
|
|
|
|
|
|
return resolve_model_path(model_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _inference_worker_loop(req_queue, resp_queue, worker_id):
|
|
|
|
|
|
"""子进程推理循环(不 import Django ORM)"""
|
|
|
|
|
|
from app.utils.Logger import LOG_FORMAT
|
|
|
|
|
|
if not logging.getLogger().handlers:
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
|
|
|
|
|
|
engines = {}
|
|
|
|
|
|
log = logging.getLogger("analysis.inference_worker.%s" % worker_id)
|
|
|
|
|
|
log.info("推理 worker 启动")
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
|
|
|
|
|
msg = req_queue.get(timeout=1.0)
|
|
|
|
|
|
except queue.Empty:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if msg is None or msg.get("cmd") == "stop":
|
|
|
|
|
|
break
|
|
|
|
|
|
req_id = msg.get("req_id")
|
|
|
|
|
|
try:
|
|
|
|
|
|
algo = msg.get("algorithm") or {}
|
|
|
|
|
|
jpeg = msg.get("jpeg")
|
|
|
|
|
|
if not jpeg:
|
|
|
|
|
|
resp_queue.put({"req_id": req_id, "ok": False, "error": "no frame"})
|
|
|
|
|
|
continue
|
|
|
|
|
|
import cv2
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
arr = np.frombuffer(jpeg, dtype=np.uint8)
|
|
|
|
|
|
frame = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
|
|
|
|
|
if frame is None:
|
|
|
|
|
|
resp_queue.put({"req_id": req_id, "ok": False, "error": "decode failed"})
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
algo_id = algo.get("id", 0)
|
|
|
|
|
|
key = (
|
|
|
|
|
|
algo_id,
|
|
|
|
|
|
algo.get("inference_engine", ""),
|
|
|
|
|
|
algo.get("model_file", ""),
|
|
|
|
|
|
float(algo.get("conf_threshold", 0.4)),
|
|
|
|
|
|
float(algo.get("iou_threshold", 0.5)),
|
|
|
|
|
|
int(algo.get("input_width", 640)),
|
|
|
|
|
|
int(algo.get("input_height", 640)),
|
|
|
|
|
|
algo.get("task_type", "detect"),
|
|
|
|
|
|
algo.get("device", "cpu"),
|
2026-09-04 18:16:14 +08:00
|
|
|
|
tuple(sorted(algo.get("target_labels") or [])),
|
2026-08-30 22:22:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
eng = engines.get(key)
|
|
|
|
|
|
if eng is None:
|
|
|
|
|
|
from app.analysis.engines.factory import EngineFactory
|
|
|
|
|
|
labels = algo.get("labels", [])
|
|
|
|
|
|
if isinstance(labels, str):
|
|
|
|
|
|
try:
|
|
|
|
|
|
labels = json.loads(labels)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
labels = []
|
|
|
|
|
|
eng = EngineFactory.create(
|
|
|
|
|
|
algo.get("inference_engine", "yolo_pytorch"),
|
|
|
|
|
|
model_file=_resolve_model_path(algo.get("model_file", "")),
|
|
|
|
|
|
labels=labels,
|
|
|
|
|
|
input_size=(int(algo.get("input_width", 640)), int(algo.get("input_height", 640))),
|
|
|
|
|
|
conf_threshold=float(algo.get("conf_threshold", 0.4)),
|
|
|
|
|
|
iou_threshold=float(algo.get("iou_threshold", 0.5)),
|
|
|
|
|
|
algorithm_type=algo.get("algorithm_type", "yolo8"),
|
|
|
|
|
|
task_type=algo.get("task_type", "detect"),
|
|
|
|
|
|
device=algo.get("device", "cpu"),
|
2026-09-04 18:16:14 +08:00
|
|
|
|
target_labels=algo.get("target_labels") or [],
|
2026-08-30 22:22:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
if not eng.load():
|
|
|
|
|
|
resp_queue.put({"req_id": req_id, "ok": False, "error": "engine load failed"})
|
|
|
|
|
|
continue
|
|
|
|
|
|
engines[key] = eng
|
|
|
|
|
|
|
|
|
|
|
|
results = eng.detect(frame)
|
|
|
|
|
|
for r in results:
|
|
|
|
|
|
r["algorithm_id"] = algo_id
|
|
|
|
|
|
r["algorithm_name"] = algo.get("name", "")
|
|
|
|
|
|
resp_queue.put({"req_id": req_id, "ok": True, "detections": results})
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
log.exception("推理异常: %s", e)
|
|
|
|
|
|
resp_queue.put({"req_id": req_id, "ok": False, "error": str(e)})
|
|
|
|
|
|
|
|
|
|
|
|
for eng in engines.values():
|
|
|
|
|
|
try:
|
|
|
|
|
|
if hasattr(eng, "unload"):
|
|
|
|
|
|
eng.unload()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
log.info("推理 worker 退出")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InferenceProcessPool(object):
|
|
|
|
|
|
"""主进程侧推理池客户端"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, num_workers=1):
|
|
|
|
|
|
self.num_workers = max(1, int(num_workers))
|
|
|
|
|
|
self._req_queue = mp.Queue(maxsize=64)
|
|
|
|
|
|
self._resp_queue = mp.Queue(maxsize=64)
|
|
|
|
|
|
self._workers = []
|
|
|
|
|
|
self._pending = {}
|
|
|
|
|
|
self._lock = threading.Lock()
|
|
|
|
|
|
self._running = False
|
|
|
|
|
|
self._drain_thread = None
|
|
|
|
|
|
self._timeout_count = 0
|
|
|
|
|
|
self._last_timeout_ts = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
|
|
if self._running:
|
|
|
|
|
|
alive = sum(1 for p in self._workers if p.is_alive())
|
|
|
|
|
|
if alive > 0:
|
|
|
|
|
|
return
|
|
|
|
|
|
logger.warning("InferenceProcessPool 标记运行中但 worker 已死,重新启动")
|
|
|
|
|
|
self.stop()
|
|
|
|
|
|
ctx = mp.get_context("spawn")
|
|
|
|
|
|
self._req_queue = ctx.Queue(maxsize=64)
|
|
|
|
|
|
self._resp_queue = ctx.Queue(maxsize=64)
|
|
|
|
|
|
for i in range(self.num_workers):
|
|
|
|
|
|
p = ctx.Process(
|
|
|
|
|
|
target=_inference_worker_loop,
|
|
|
|
|
|
args=(self._req_queue, self._resp_queue, i),
|
|
|
|
|
|
name="inference-worker-%d" % i,
|
|
|
|
|
|
daemon=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
p.start()
|
|
|
|
|
|
self._workers.append(p)
|
|
|
|
|
|
self._running = True
|
|
|
|
|
|
self._drain_thread = threading.Thread(target=self._drain_responses, name="inference-drain", daemon=True)
|
|
|
|
|
|
self._drain_thread.start()
|
|
|
|
|
|
logger.info("InferenceProcessPool 已启动 workers=%d", self.num_workers)
|
|
|
|
|
|
|
|
|
|
|
|
def _drain_responses(self):
|
|
|
|
|
|
while self._running:
|
|
|
|
|
|
try:
|
|
|
|
|
|
resp = self._resp_queue.get(timeout=0.5)
|
|
|
|
|
|
except queue.Empty:
|
|
|
|
|
|
continue
|
|
|
|
|
|
req_id = resp.get("req_id")
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
evt = self._pending.pop(req_id, None)
|
|
|
|
|
|
if evt:
|
|
|
|
|
|
evt["resp"] = resp
|
|
|
|
|
|
evt["event"].set()
|
|
|
|
|
|
|
|
|
|
|
|
def detect(self, frame, algorithm, timeout=30.0):
|
|
|
|
|
|
"""同步推理:将 frame + algorithm dict 发往子进程,等待结果"""
|
|
|
|
|
|
if not self._running:
|
|
|
|
|
|
self.start()
|
|
|
|
|
|
try:
|
|
|
|
|
|
import cv2
|
|
|
|
|
|
ok, buf = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 85])
|
|
|
|
|
|
if not ok:
|
|
|
|
|
|
return []
|
|
|
|
|
|
jpeg = buf.tobytes()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("帧编码失败: %s", e)
|
|
|
|
|
|
return []
|
|
|
|
|
|
return self.detect_jpeg(jpeg, algorithm, timeout=timeout)
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_workers_alive(self):
|
|
|
|
|
|
if not self._workers:
|
|
|
|
|
|
if self._running:
|
|
|
|
|
|
self.stop()
|
|
|
|
|
|
self.start()
|
|
|
|
|
|
return
|
|
|
|
|
|
alive = sum(1 for p in self._workers if p.is_alive())
|
|
|
|
|
|
if alive == 0:
|
|
|
|
|
|
logger.warning("推理 worker 已全部退出,正在重启 InferenceProcessPool")
|
|
|
|
|
|
self.stop()
|
|
|
|
|
|
self.start()
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
def detect_jpeg(self, jpeg, algorithm, timeout=30.0, raise_errors=False):
|
2026-08-30 22:22:11 +08:00
|
|
|
|
"""同步推理:直接传已编码的 JPEG bytes,跳过主进程 imencode/imdecode,
|
|
|
|
|
|
消除 _inference_forwarder_loop 中的双重编解码与主进程 GIL 占用。"""
|
|
|
|
|
|
self._ensure_workers_alive()
|
|
|
|
|
|
if not self._running:
|
|
|
|
|
|
self.start()
|
|
|
|
|
|
if not jpeg:
|
2026-09-04 18:16:14 +08:00
|
|
|
|
if raise_errors:
|
|
|
|
|
|
raise ValueError("no frame")
|
2026-08-30 22:22:11 +08:00
|
|
|
|
return []
|
|
|
|
|
|
req_id = str(uuid.uuid4())
|
|
|
|
|
|
evt = {"event": threading.Event(), "resp": None}
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
self._pending[req_id] = evt
|
|
|
|
|
|
|
|
|
|
|
|
algo_payload = algorithm if isinstance(algorithm, dict) else _algorithm_to_dict(algorithm)
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._req_queue.put({"req_id": req_id, "algorithm": algo_payload, "jpeg": jpeg}, timeout=2.0)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
self._pending.pop(req_id, None)
|
|
|
|
|
|
logger.warning("推理请求入队失败: %s", e)
|
2026-09-04 18:16:14 +08:00
|
|
|
|
if raise_errors:
|
|
|
|
|
|
raise RuntimeError("推理请求入队失败: %s" % e) from e
|
2026-08-30 22:22:11 +08:00
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
if not evt["event"].wait(timeout=timeout):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
self._pending.pop(req_id, None)
|
|
|
|
|
|
self._timeout_count += 1
|
|
|
|
|
|
self._last_timeout_ts = time.time()
|
|
|
|
|
|
logger.warning("推理超时 req_id=%s (累计 %d)", req_id, self._timeout_count)
|
2026-09-04 18:16:14 +08:00
|
|
|
|
if raise_errors:
|
|
|
|
|
|
raise TimeoutError("模型推理超时(%ss)" % timeout)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
return []
|
|
|
|
|
|
resp = evt.get("resp") or {}
|
|
|
|
|
|
if not resp.get("ok"):
|
|
|
|
|
|
logger.warning("推理失败: %s", resp.get("error"))
|
2026-09-04 18:16:14 +08:00
|
|
|
|
if raise_errors:
|
|
|
|
|
|
raise RuntimeError(resp.get("error") or "模型推理失败")
|
2026-08-30 22:22:11 +08:00
|
|
|
|
return []
|
|
|
|
|
|
return resp.get("detections") or []
|
|
|
|
|
|
|
|
|
|
|
|
def status(self):
|
|
|
|
|
|
self._ensure_workers_alive()
|
|
|
|
|
|
alive = sum(1 for p in self._workers if p.is_alive())
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
tc = self._timeout_count
|
|
|
|
|
|
lts = self._last_timeout_ts
|
|
|
|
|
|
degraded = alive > 0 and tc >= 3 and (time.time() - lts) < 120
|
|
|
|
|
|
return {
|
|
|
|
|
|
"workers": self.num_workers,
|
|
|
|
|
|
"workers_alive": alive,
|
|
|
|
|
|
"running": self._running,
|
|
|
|
|
|
"timeout_count": tc,
|
|
|
|
|
|
"inference_degraded": degraded,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def instance_count(self):
|
|
|
|
|
|
"""共享推理 worker 进程数(模型在 worker 内按需加载,非 worker_pool 计数)。"""
|
|
|
|
|
|
return sum(1 for p in self._workers if p.is_alive())
|
|
|
|
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
|
|
self._running = False
|
|
|
|
|
|
for _ in self._workers:
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._req_queue.put({"cmd": "stop"}, timeout=1.0)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
for p in self._workers:
|
|
|
|
|
|
try:
|
|
|
|
|
|
p.join(timeout=3)
|
|
|
|
|
|
if p.is_alive():
|
|
|
|
|
|
p.terminate()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._workers = []
|
|
|
|
|
|
logger.info("InferenceProcessPool 已停止")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _algorithm_to_dict(a):
|
|
|
|
|
|
labels = a.labels
|
|
|
|
|
|
if isinstance(labels, str):
|
|
|
|
|
|
try:
|
|
|
|
|
|
labels = json.loads(labels)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
labels = []
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": a.id,
|
|
|
|
|
|
"name": a.name,
|
|
|
|
|
|
"inference_engine": a.inference_engine,
|
|
|
|
|
|
"model_file": a.model_file,
|
|
|
|
|
|
"labels": labels,
|
|
|
|
|
|
"input_width": a.input_width,
|
|
|
|
|
|
"input_height": a.input_height,
|
|
|
|
|
|
"conf_threshold": a.conf_threshold,
|
|
|
|
|
|
"iou_threshold": a.iou_threshold,
|
|
|
|
|
|
"algorithm_type": a.algorithm_type,
|
|
|
|
|
|
"task_type": getattr(a, "task_type", "detect"),
|
|
|
|
|
|
"device": getattr(a, "device", "cpu"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_inference_pool(num_workers=None):
|
|
|
|
|
|
global _POOL
|
|
|
|
|
|
with _POOL_LOCK:
|
|
|
|
|
|
if _POOL is None:
|
|
|
|
|
|
n = num_workers
|
|
|
|
|
|
if n is None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.utils.GlobalUtils import g_config
|
|
|
|
|
|
n = int(getattr(g_config, "analysisInferenceWorkers", 1))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
n = 1
|
|
|
|
|
|
_POOL = InferenceProcessPool(num_workers=n)
|
|
|
|
|
|
_POOL.start()
|
|
|
|
|
|
return _POOL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def shutdown_inference_pool():
|
|
|
|
|
|
global _POOL
|
|
|
|
|
|
with _POOL_LOCK:
|
|
|
|
|
|
if _POOL:
|
|
|
|
|
|
_POOL.stop()
|
|
|
|
|
|
_POOL = None
|