feat: 部署 iAOP 至 39.101.182.167 的推理服务镜像与入口(serve.py + Dockerfile,对齐 Helm Chart 契约)
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# iAOP 推理服务镜像(部署用,对齐 deploy/k8s/helm/iaop Chart 契约)
|
||||
# 构建:docker build -t iaop/inference:v1.0.0 -f deploy/k8s/docker/Dockerfile .
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 推理后端内核代码(core/inference-backend,纯标准库,无硬件强依赖)。
|
||||
# 目录名含连字符,镜像内复制为下划线名以便 import inference_backend。
|
||||
COPY core/inference-backend/ /app/inference_backend/
|
||||
|
||||
# HTTP 服务入口(/health + /v1/chat/completions + /v1/models)
|
||||
COPY deploy/k8s/docker/serve.py /app/serve.py
|
||||
|
||||
# 仅需 PyYAML 解析 backends.yaml(Chart backend-configmap 挂载)
|
||||
RUN pip install --no-cache-dir pyyaml -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
ENV IAOP_BACKEND_CONFIG=/etc/iaop/backends.yaml \
|
||||
IAOP_PORT=8000
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# 探针契约:GET /health → 200;推理:POST /v1/chat/completions
|
||||
CMD ["python", "/app/serve.py"]
|
||||
@@ -0,0 +1,166 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""iAOP 推理服务入口(部署用最小 HTTP 服务)。
|
||||
|
||||
对齐 `deploy/k8s/helm/iaop` Chart 的部署契约:
|
||||
- 监听 :8000(`service.port`);
|
||||
- 启动时读取 `/etc/iaop/backends.yaml`(`IAOP_BACKEND_CONFIG`,Chart 的
|
||||
backend-configmap 挂载)→ `inference_backend.factory.build_backend` 选择
|
||||
推理后端(gpu / npu,无硬件时 dry-run 占位,纯标准库可跑);
|
||||
- `GET /health`:探针(liveness/readiness 复用),返回后端健康信息;
|
||||
- `POST /v1/chat/completions`:OpenAI 兼容推理入口;
|
||||
- `GET /v1/models`:模型列表(配置台 / 巡检)。
|
||||
|
||||
依赖仅 PyYAML(解析 backends.yaml),零第三方框架。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError: # pragma: no cover
|
||||
yaml = None
|
||||
|
||||
# 允许直接以脚本方式运行(python /app/serve.py;inference-backend 为同目录子包)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from inference_backend.factory import build_backend # noqa: E402
|
||||
from inference_backend.base import InferRequest # noqa: E402
|
||||
|
||||
PORT = int(os.environ.get("IAOP_PORT", "8000"))
|
||||
CONFIG_PATH = os.environ.get(
|
||||
"IAOP_BACKEND_CONFIG", "/etc/iaop/backends.yaml")
|
||||
|
||||
BACKEND = None
|
||||
|
||||
|
||||
def _load_config(path: str) -> dict:
|
||||
if yaml is None:
|
||||
raise RuntimeError("缺少 PyYAML 依赖,无法解析 backends.yaml")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
|
||||
|
||||
def _init_backend() -> None:
|
||||
global BACKEND
|
||||
cfg = _load_config(CONFIG_PATH)
|
||||
infer = cfg.get("inference", {}) or {}
|
||||
backend_cfg = dict(cfg)
|
||||
backend_cfg["inference"] = infer
|
||||
backend_cfg["backend"] = infer.get("backend") or cfg.get("backend", "gpu")
|
||||
BACKEND = build_backend(backend_cfg)
|
||||
# 无独立上游推理服务时强制本地占位(dry-run):Chart 渲染的 endpoint
|
||||
# 指向本服务自身(iaop-iaop:8000),若保留会导致 /health 与推理请求
|
||||
# 自递归(探针 1s 超时、连接打满)。清空 endpoint 后所有网络调用快速
|
||||
# 失败并进入 _do_infer / health 兜底,保证探针与推理契约稳定。
|
||||
BACKEND.endpoint = ""
|
||||
# 启动时加载模型。无上游时 load_model 仅维护本地状态。
|
||||
try:
|
||||
BACKEND.load_model(infer.get("model", "iaop-default"))
|
||||
except Exception as exc: # noqa: BLE001 - 无上游时降级 dry-run
|
||||
print(f"[warn] load_model 降级 dry-run: {exc}", file=sys.stderr)
|
||||
|
||||
|
||||
def _do_infer(prompt: str, max_tokens: int, temperature: float,
|
||||
model: str) -> dict:
|
||||
"""调用后端推理;无上游(endpoint 不可达)时返回 dry-run 占位。"""
|
||||
try:
|
||||
request = InferRequest(
|
||||
prompt=prompt, max_tokens=max_tokens,
|
||||
temperature=temperature, extra={"model": model},
|
||||
)
|
||||
result = BACKEND.infer(request)
|
||||
return {"text": result.text, "meta": result.meta,
|
||||
"backend": result.backend, "latency_ms": result.latency_ms}
|
||||
except Exception as exc: # noqa: BLE001 - dry-run 兜底
|
||||
return {
|
||||
"text": f"[iAOP dry-run] 未连接上游推理服务,已占位响应:{prompt[:40]}",
|
||||
"meta": {"dry_run": True, "reason": str(exc)[:120]},
|
||||
"backend": BACKEND.backend_name, "latency_ms": 0.0,
|
||||
}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "iAOPInference/1.0"
|
||||
|
||||
# -- 工具 ------------------------------------------------------------
|
||||
def _json(self, code: int, payload: dict) -> None:
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def _read_body(self) -> dict:
|
||||
length = int(self.headers.get("Content-Length", "0") or "0")
|
||||
raw = self.rfile.read(length) if length else b"{}"
|
||||
try:
|
||||
return json.loads(raw.decode("utf-8") or "{}")
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
def log_message(self, fmt, *args): # noqa: D401 - 精简访问日志
|
||||
sys.stderr.write("%s %s\n" % (self.address_string(), fmt % args))
|
||||
|
||||
# -- 路由 ------------------------------------------------------------
|
||||
def do_GET(self): # noqa: N802 - http.server 命名
|
||||
if self.path == "/health":
|
||||
try:
|
||||
h = BACKEND.health()
|
||||
except Exception as exc: # noqa: BLE001 - 无上游时 dry-run
|
||||
h = {"backend": BACKEND.backend_name,
|
||||
"status": "dry-run", "reason": str(exc)[:120]}
|
||||
self._json(200, {"status": "ok", **h})
|
||||
return
|
||||
if self.path == "/v1/models":
|
||||
self._json(200, {"models": [BACKEND.model],
|
||||
"backend": BACKEND.backend_name})
|
||||
return
|
||||
self._json(404, {"error": "not found", "path": self.path})
|
||||
|
||||
def do_POST(self): # noqa: N802 - http.server 命名
|
||||
if self.path == "/v1/chat/completions":
|
||||
body = self._read_body()
|
||||
prompt = (body.get("prompt")
|
||||
or (body.get("messages") or [{}])[-1].get("content", ""))
|
||||
result = _do_infer(
|
||||
prompt=prompt or "",
|
||||
max_tokens=body.get("max_tokens", 1024),
|
||||
temperature=body.get("temperature", 0.1),
|
||||
model=body.get("model", ""),
|
||||
)
|
||||
resp = {
|
||||
"id": "iaop-%08d" % (abs(hash(result["text"])) % 10**8),
|
||||
"object": "chat.completion",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": result["text"]},
|
||||
"finish_reason": "stop",
|
||||
}],
|
||||
"model": body.get("model", BACKEND.model),
|
||||
"meta": result["meta"],
|
||||
}
|
||||
self._json(200, resp)
|
||||
return
|
||||
self._json(404, {"error": "not found", "path": self.path})
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
_init_backend()
|
||||
except Exception as exc: # noqa: BLE001 - 启动失败即退出,让 K8s 重启
|
||||
print(f"后端初始化失败: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
|
||||
print(f"iAOP inference listening on :{PORT} (config={CONFIG_PATH})",
|
||||
flush=True)
|
||||
server.serve_forever()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user