feat: 完成 issue #8 ⑥ K8s/Helm 部署底座 + 昇腾适配层

This commit is contained in:
2026-08-04 22:04:18 +08:00
parent 691a812fcf
commit e620d4937e
20 changed files with 1013 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
# -*- 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})