104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""推理后端统一抽象接口(PRD 5.6「⑥ 部署底座」)。
|
||
|
||
设计约束(对齐产品设计文档 5.6):
|
||
- 统一接口 `load_model / infer / health / unload`,与底层硬件无关;
|
||
- NVIDIA 5090 实现(Triton/ONNX)与华为昇腾实现(ACL/CANN)均实现本接口;
|
||
- 业务代码只 import 本模块,不 import 任何具体后端;
|
||
- 切换后端 = 修改适配层配置(见 config/backends.template.yaml),业务代码零改动。
|
||
"""
|
||
from abc import ABC, abstractmethod
|
||
from dataclasses import dataclass, field
|
||
import json
|
||
import time
|
||
import urllib.request
|
||
|
||
|
||
@dataclass
|
||
class InferRequest:
|
||
"""统一的推理请求载荷(业务侧构造,与硬件无关)。"""
|
||
|
||
prompt: str
|
||
max_tokens: int = 1024
|
||
temperature: float = 0.1
|
||
extra: dict = field(default_factory=dict)
|
||
|
||
def to_payload(self) -> dict:
|
||
"""转换为后端服务 OpenAI 兼容的请求体。"""
|
||
return {
|
||
"model": self.extra.get("model") if self.extra.get("model") else None,
|
||
"prompt": self.prompt,
|
||
"max_tokens": self.max_tokens,
|
||
"temperature": self.temperature,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class InferResult:
|
||
"""统一的推理结果(业务侧消费,不感知硬件)。"""
|
||
|
||
text: str
|
||
backend: str
|
||
latency_ms: float = 0.0
|
||
meta: dict = field(default_factory=dict)
|
||
|
||
|
||
class InferenceBackend(ABC):
|
||
"""推理后端统一接口(对应 PRD 5.6:loadModel / infer / health / unload)。"""
|
||
|
||
#: 后端标识:gpu(NVIDIA 5090)| npu(华为昇腾)
|
||
backend_name = "base"
|
||
|
||
def __init__(self, endpoint: str = "", model: str = "iaop-default",
|
||
timeout_seconds: float = 10.0):
|
||
self.endpoint = (endpoint or "").rstrip("/")
|
||
self.model = model
|
||
self.timeout = float(timeout_seconds)
|
||
|
||
# ---- 统一接口(业务代码仅依赖以下四个方法) ----
|
||
|
||
@abstractmethod
|
||
def load_model(self, model_name: str | None = None) -> dict:
|
||
"""加载/热载模型(对应 PRD 的 loadModel)。返回加载状态。"""
|
||
|
||
@abstractmethod
|
||
def infer(self, request: InferRequest) -> InferResult:
|
||
"""执行一次推理(对应 PRD 的 infer)。返回统一结果对象。"""
|
||
|
||
@abstractmethod
|
||
def health(self) -> dict:
|
||
"""健康巡检(对应 PRD 的 health)。返回后端状态与版本信息。"""
|
||
|
||
@abstractmethod
|
||
def unload(self) -> dict:
|
||
"""卸载模型/释放显存(对应 PRD 的 unload)。返回卸载结果。"""
|
||
|
||
# ---- 内部工具:HTTP JSON 调用(各后端共享) ----
|
||
|
||
def _post_json(self, path: str, payload: dict) -> dict:
|
||
"""向后端服务发起 JSON POST 请求,返回解析后的响应体。"""
|
||
url = self.endpoint + path
|
||
data = json.dumps(payload).encode("utf-8")
|
||
req = urllib.request.Request(
|
||
url, data=data, headers={"Content-Type": "application/json"}
|
||
)
|
||
started = time.monotonic()
|
||
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
||
raw = resp.read().decode("utf-8")
|
||
meta = {"http_ms": round((time.monotonic() - started) * 1000, 2)}
|
||
body = json.loads(raw) if raw else {}
|
||
if isinstance(body, dict):
|
||
body.setdefault("_http_ms", meta["http_ms"])
|
||
return body
|
||
|
||
def _healthz(self) -> dict:
|
||
"""通用 /health 探针封装。"""
|
||
if not self.endpoint:
|
||
return {
|
||
"status": "ok",
|
||
"backend": self.backend_name,
|
||
"model": self.model,
|
||
"reason": "dry-run(未配置 endpoint,适配层就绪)",
|
||
}
|
||
return self._post_json("/health", {"model": self.model})
|