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
+51
View File
@@ -0,0 +1,51 @@
# iAOP-Core · 推理后端抽象层(Inference Backend)
对应 PRD 5.6「⑥ 部署底座」与 EPIC #8:
NVIDIA GPU(5090,Triton/ONNX)与华为昇腾 NPU(ACL/CANN)实现**同一**
`InferenceBackend` 接口,业务代码仅依赖接口、不感知硬件;
**切换后端 = 改适配层配置**(`backend: gpu|npu`),业务代码零改动。
## 模块结构
```
core/inference-backend/
├── __init__.py 包入口(导出接口/请求/结果/工厂)
├── base.py InferenceBackend 统一抽象接口(load_model/infer/health/unload)
├── gpu_backend.py NVIDIA 5090 后端(Triton/ONNX,OpenAI 兼容)
├── npu_backend.py 华为昇腾 NPU 后端(ACL/CANN,MindIE OpenAI 兼容)
├── factory.py 可插拔工厂(backend: gpu|npu -> 实现类)
├── config/
│ └── backends.template.yaml 模板后端适配层配置资产(ti-cl4 示例)
└── tests/
├── _bootstrap.py 测试引导(目录含连字符,挂载包名 inference_backend)
└── test_inference_backend.py 接口/工厂/行为单元测试
```
## 统一接口(PRD 5.6:loadModel / infer / health / unload)
业务代码只依赖 `InferenceBackend` 接口,不 import 任何具体后端:
```python
from inference_backend.base import InferRequest
from inference_backend.factory import build_backend, load_backend_config
backend = build_backend(load_backend_config("config/backends.template.yaml"))
backend.load_model()
health = backend.health() # 健康巡检
result = backend.infer(InferRequest(prompt="请解释炉温报警")) # result.text
backend.unload()
```
## 切换后端(仅改配置)
`config/backends.template.yaml` 中 `backend: gpu` 改为 `backend: npu` 即切换到
昇腾适配层;同一份业务代码、同一调用面,硬件差异被适配层隔离。
新增硬件:在 `factory.py` 的 `BACKEND_REGISTRY` 注册实现类即可。
## 运行测试
```bash
cd core/inference-backend/tests
python -m unittest discover -s . -p "test_*.py"
```
+18
View File
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""iAOP-Core · 推理后端抽象层(Inference Backend)。
对应 PRD 5.6「⑥ 部署底座」与 EPIC #8:
NVIDIA GPU(5090,Triton/ONNX)与华为昇腾 NPU(ACL/CANN)实现同一
`InferenceBackend` 接口,业务代码仅依赖接口、不感知硬件;
切换后端 = 改适配层配置(`backend: gpu|npu`),业务代码零改动。
"""
from inference_backend.base import InferRequest, InferResult, InferenceBackend
from inference_backend.factory import build_backend, load_backend_config
__all__ = [
"InferenceBackend",
"InferRequest",
"InferResult",
"build_backend",
"load_backend_config",
]
+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})
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# 模板「推理后端适配层」配置资产示例:ti-cl4(氯化车间/海绵钛,Template-Ti 一期)。
#
# 说明:
# - 这是「推理后端选择」配置点(PRD 5.6 ⑥ 部署底座):
# 切换后端(NVIDIA 5090 GPU ↔ 华为昇腾 NPU)**只改本文件 backend 字段**,
# 业务代码零改动(统一走 InferenceBackend 接口);
# - backend: gpu —— NVIDIA 5090(Triton/ONNX,vLLM/Triton OpenAI 兼容接口)
# npu —— 华为昇腾(ACL/CANN,MindIE/onnxruntime-ascend)
# - endpoint 为空时进入 dry-run 模式(适配层就绪,便于离线验证/联调);
# - 该配置由 Helm 渲染注入(deploy/k8s/helm/iaop/values.yaml -> ConfigMap)。
template: ti-cl4
version: 1.0.0
# ---- 推理后端选择(切换后端只改这一处) ----
backend: gpu # gpu | npu
inference:
# 与 Helm values.inference 保持同构,便于一键部署时直接覆盖
model: iaop-ti-cl4-v1
endpoint: http://iaop-inference.iaop.svc.cluster.local:8000/v1
timeout_seconds: 60
runtime: vllm # gpu: vllm|triton;npu: mindie|onnx-ascend
device: nvidia-5090 # gpu: nvidia-5090;npu: ascend-310p|ascend-910b
cann_version: "" # npu: CANN 工具链版本(如 8.0),gpu 忽略
max_tokens: 1024
temperature: 0.1
# ---- 资源配额(Helm 部署时以此为默认值) ----
resources:
requests:
cpu: "2"
memory: 8Gi
limits:
cpu: "8"
memory: 32Gi
# ---- 灰度发布策略(ArgoCD 滚动发布参数,PRD 5.6 配置点) ----
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
+64
View File
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""推理后端工厂:按配置选择后端实现(可插拔适配层)。
PRD 5.6「配置点:资源配额、推理后端选择、灰度发布策略」——
`backend: gpu|npu` 即推理后端选择;业务代码只调用 `build_backend()`
返回的接口对象,不感知具体硬件。
"""
import os
from inference_backend.base import InferenceBackend
from inference_backend.gpu_backend import NvidiaGpuBackend
from inference_backend.npu_backend import AscendNpuBackend
#: 可插拔后端注册表:配置名 -> 实现类。新增硬件只需注册新类。
BACKEND_REGISTRY = {
"gpu": NvidiaGpuBackend, # NVIDIA 5090(Triton/ONNX)
"npu": AscendNpuBackend, # 华为昇腾(ACL/CANN)
}
def build_backend(config: dict) -> InferenceBackend:
"""依据配置构建推理后端实例(切换后端仅改配置,业务代码零改动)。
Args:
config: 后端配置字典(见 config/backends.template.yaml),
至少包含 ``backend`` 键(gpu | npu)。
Returns:
实现了 :class:`InferenceBackend` 接口的后端实例。
Raises:
ValueError: 配置缺失或指定了未注册的后端。
"""
if not isinstance(config, dict) or not config.get("backend"):
raise ValueError("推理后端配置缺失:需要 backend: gpu|npu")
name = str(config["backend"]).lower()
if name not in BACKEND_REGISTRY:
raise ValueError(
f"未注册的推理后端: {name!r},可用: {sorted(BACKEND_REGISTRY)}"
)
cls = BACKEND_REGISTRY[name]
inf = config.get("inference", {}) or {}
return cls(
endpoint=inf.get("endpoint", ""),
model=inf.get("model", "iaop-default"),
timeout_seconds=float(inf.get("timeout_seconds", 10)),
runtime=inf.get("runtime", ""),
device=inf.get("device", ""),
cann_version=inf.get("cann_version", ""),
)
def load_backend_config(path: str) -> dict:
"""从 YAML 配置资产加载后端配置(模板可覆盖资产)。"""
import yaml
with open(path, "r", encoding="utf-8") as fh:
return yaml.safe_load(fh) or {}
def default_config_path() -> str:
"""返回本模块模板配置资产的默认路径。"""
return os.path.join(os.path.dirname(os.path.abspath(__file__)),
"config", "backends.template.yaml")
+74
View File
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""NVIDIA GPU 推理后端实现(5090,Triton/ONNX)。
对应 PRD 5.6:5090 实现(Triton/ONNX)——通过 OpenAI 兼容接口
(vLLM / Triton Inference Server)对外提供推理,业务侧统一走
`InferenceBackend` 接口,不感知硬件。
"""
from inference_backend.base import InferenceBackend, InferRequest, InferResult
class NvidiaGpuBackend(InferenceBackend):
"""NVIDIA GPU 后端:面向 5090 服务器(Triton/ONNX,OpenAI 兼容)。"""
backend_name = "gpu"
def __init__(self, endpoint: str = "", model: str = "iaop-default",
timeout_seconds: float = 10.0, runtime: str = "vllm",
device: str = "nvidia-5090", **kwargs):
super().__init__(endpoint, model, timeout_seconds)
self.runtime = runtime # vllm | triton
self.device = device # 硬件型号(默认 5090)
self._loaded = False
def load_model(self, model_name: str | None = None) -> dict:
"""加载模型到 GPU。无 endpoint 时仅维护本地状态(适配层就绪)。"""
model_name = model_name or self.model
if not self.endpoint:
self._loaded = True
return {"status": "ok", "backend": self.backend_name,
"model": model_name, "device": self.device,
"reason": "dry-run(未配置 endpoint)"}
body = self._post_json("/v1/models/load",
{"model": model_name, "device": self.device})
self._loaded = body.get("status") in ("ok", "loaded", "ready")
return body
def infer(self, request: InferRequest) -> InferResult:
"""OpenAI 兼容生成(/v1/chat/completions)。"""
started = __import__("time").monotonic()
payload = request.to_payload()
payload["model"] = payload["model"] or self.model
body = self._post_json("/v1/chat/completions", payload)
latency_ms = round((__import__("time").monotonic() - started) * 1000, 2)
try:
text = body["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError):
text = str(body)
return InferResult(
text=text,
backend=self.backend_name,
latency_ms=latency_ms,
meta={"runtime": self.runtime, "device": self.device,
"model": self.model, "raw": body},
)
def health(self) -> dict:
"""健康巡检:探测 /health,返回后端/设备/运行时信息。"""
base = self._healthz()
base.update({
"backend": self.backend_name,
"runtime": self.runtime,
"device": self.device,
"model": self.model,
"loaded": self._loaded,
})
return base
def unload(self) -> dict:
"""卸载模型、释放显存。"""
self._loaded = False
if not self.endpoint:
return {"status": "ok", "backend": self.backend_name,
"reason": "dry-run(未配置 endpoint)"}
return self._post_json("/v1/models/unload", {"model": self.model})
+81
View File
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""华为昇腾 NPU 推理后端实现(ACL/CANN)。
对应 PRD 5.6:昇腾实现(ACL/CANN)——通过昇腾推理服务
(MindIE / onnxruntime-ascend)的 OpenAI 兼容接口对外提供推理,
与 NVIDIA GPU 后端实现同一 `InferenceBackend` 接口:
**切换后端仅改适配层配置,业务代码零改动**。
"""
from inference_backend.base import InferenceBackend, InferRequest, InferResult
class AscendNpuBackend(InferenceBackend):
"""华为昇腾 NPU 后端:面向昇腾 310P/910B(CANN/MindIE)。"""
backend_name = "npu"
def __init__(self, endpoint: str = "", model: str = "iaop-default",
timeout_seconds: float = 10.0, runtime: str = "mindie",
device: str = "ascend-910b", cann_version: str = "8.0",
**kwargs):
super().__init__(endpoint, model, timeout_seconds)
self.runtime = runtime # mindie | onnx-ascend
self.device = device # ascend-310p | ascend-910b
self.cann_version = cann_version # CANN 工具链版本
self._loaded = False
def load_model(self, model_name: str | None = None) -> dict:
"""加载模型到 NPU(ACL aclmdlLoadFromFile 语义)。"""
model_name = model_name or self.model
if not self.endpoint:
self._loaded = True
return {"status": "ok", "backend": self.backend_name,
"model": model_name, "device": self.device,
"reason": "dry-run(未配置 endpoint)"}
body = self._post_json("/acl/models/load",
{"model": model_name, "device": self.device,
"cann_version": self.cann_version})
self._loaded = body.get("status") in ("ok", "loaded", "ready")
return body
def infer(self, request: InferRequest) -> InferResult:
"""昇腾推理(MindIE OpenAI 兼容 /v1/chat/completions)。"""
import time
started = time.monotonic()
payload = request.to_payload()
payload["model"] = payload["model"] or self.model
body = self._post_json("/v1/chat/completions", payload)
latency_ms = round((time.monotonic() - started) * 1000, 2)
try:
text = body["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError):
text = str(body)
return InferResult(
text=text,
backend=self.backend_name,
latency_ms=latency_ms,
meta={"runtime": self.runtime, "device": self.device,
"cann_version": self.cann_version, "model": self.model,
"raw": body},
)
def health(self) -> dict:
"""健康巡检:探测 /health,返回后端/设备/CANN 版本信息。"""
base = self._healthz()
base.update({
"backend": self.backend_name,
"runtime": self.runtime,
"device": self.device,
"cann_version": self.cann_version,
"model": self.model,
"loaded": self._loaded,
})
return base
def unload(self) -> dict:
"""卸载模型、释放 NPU 资源(ACL aclmdlUnload 语义)。"""
self._loaded = False
if not self.endpoint:
return {"status": "ok", "backend": self.backend_name,
"reason": "dry-run(未配置 endpoint)"}
return self._post_json("/acl/models/unload", {"model": self.model})
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
"""测试引导:把 `core/inference-backend` 以包名 `inference_backend` 挂载到 sys.modules。
目录名 `inference-backend` 含连字符,无法直接以包名 import;挂载后模块内相对导入
(`from inference_backend.base import ...`)在 unittest 发现机制下可正常解析。
"""
import os
import sys
import types
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BACKEND_DIR)
if "inference_backend" not in sys.modules:
pkg = types.ModuleType("inference_backend")
pkg.__path__ = [BACKEND_DIR]
sys.modules["inference_backend"] = pkg
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
"""推理后端抽象层单元测试(EPIC #8 ⑥ 部署底座 / PRD 5.6)。
覆盖:
1. 模板配置资产可解析、含 backend 选择键;
2. 工厂按配置构建 GPU / NPU 后端(可插拔);
3. 统一接口 load_model / infer / health / unload 行为;
4. 切换后端仅改配置:同一业务调用面(接口方法)在两种后端下等价;
5. 非法配置报错。
"""
import os
import sys
import unittest
from unittest import mock
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import _bootstrap # noqa: F401
from inference_backend.base import InferRequest, InferenceBackend # noqa: E402
from inference_backend.factory import ( # noqa: E402
BACKEND_REGISTRY,
build_backend,
default_config_path,
load_backend_config,
)
CONFIG_PATH = default_config_path()
class TestTemplateConfig(unittest.TestCase):
"""模板配置资产:可解析且包含后端选择配置点。"""
def test_template_parses_and_has_backend_key(self):
cfg = load_backend_config(CONFIG_PATH)
self.assertIn("backend", cfg)
self.assertIn(cfg["backend"].lower(), ("gpu", "npu"))
self.assertIn("inference", cfg)
def test_registry_covers_gpu_and_npu(self):
self.assertEqual(sorted(BACKEND_REGISTRY), ["gpu", "npu"])
class TestFactory(unittest.TestCase):
"""工厂:按配置选择后端实现(可插拔适配层)。"""
def test_build_gpu_from_template(self):
cfg = load_backend_config(CONFIG_PATH)
backend = build_backend(cfg)
self.assertIsInstance(backend, InferenceBackend)
self.assertEqual(backend.backend_name, cfg["backend"].lower())
self.assertEqual(backend.model, cfg["inference"]["model"])
def test_build_npu_by_config_switch(self):
cfg = load_backend_config(CONFIG_PATH)
cfg["backend"] = "npu"
backend = build_backend(cfg)
self.assertEqual(backend.backend_name, "npu")
# 业务调用面不变(接口等价,不感知硬件)
self.assertTrue(callable(backend.load_model))
self.assertTrue(callable(backend.infer))
self.assertTrue(callable(backend.health))
self.assertTrue(callable(backend.unload))
def test_build_rejects_unknown_backend(self):
with self.assertRaises(ValueError):
build_backend({"backend": "tpu"})
def test_build_rejects_missing_backend(self):
with self.assertRaises(ValueError):
build_backend({"inference": {"model": "x"}})
class TestBackendBehavior(unittest.TestCase):
"""统一接口行为(dry-run:未配置 endpoint 时适配层就绪)。"""
def setUp(self):
cfg = load_backend_config(CONFIG_PATH)
cfg["inference"]["endpoint"] = "" # dry-run
self.gpu = build_backend(cfg)
cfg["backend"] = "npu"
self.npu = build_backend(cfg)
def test_dry_run_load_health_unload(self):
for backend in (self.gpu, self.npu):
loaded = backend.load_model()
self.assertEqual(loaded["status"], "ok")
health = backend.health()
self.assertEqual(health["status"], "ok")
self.assertEqual(health["backend"], backend.backend_name)
self.assertTrue(health["loaded"])
unloaded = backend.unload()
self.assertEqual(unloaded["status"], "ok")
def test_infer_extracts_text_from_openai_response(self):
fake_body = {
"choices": [{"message": {"content": "炉温偏高,建议降低加料比"}}],
}
for backend in (self.gpu, self.npu):
with mock.patch.object(backend, "_post_json",
return_value=fake_body) as post:
result = backend.infer(InferRequest(prompt="请解释炉温报警"))
post.assert_called_once_with(
"/v1/chat/completions",
mock.ANY,
)
self.assertEqual(result.text, "炉温偏高,建议降低加料比")
self.assertEqual(result.backend, backend.backend_name)
# mock 调用耗时可能为 0,仅断言字段类型与量级
self.assertIsInstance(result.latency_ms, float)
self.assertGreaterEqual(result.latency_ms, 0.0)
def test_infer_payload_carries_model(self):
with mock.patch.object(self.gpu, "_post_json",
return_value={"choices": []}) as post:
self.gpu.infer(InferRequest(prompt="hi", max_tokens=64))
_, payload = post.call_args[0]
self.assertEqual(payload["model"], self.gpu.model)
self.assertEqual(payload["max_tokens"], 64)
def test_health_dry_run_notes_reason(self):
health = self.gpu.health()
self.assertIn("reason", health)
self.assertIn("dry-run", health["reason"])
if __name__ == "__main__":
unittest.main()
+16
View File
@@ -0,0 +1,16 @@
apiVersion: v2
name: iaop
description: |
iAOP(云美工业AI优化平台)一键 Helm 部署底座(PRD 5.6「⑥ 部署底座」,
EPIC #8):内核(采集/数据总线/LLM 网关/RAG)+ 行业模板一键部署;
推理后端(NVIDIA 5090 GPU / 华为昇腾 NPU)可插拔,切换后端仅改 values。
type: application
version: 1.0.0
appVersion: "v1.0.0"
keywords:
- iAOP
- industrial-ai
- k8s
- helm
- inference
- ascend
+65
View File
@@ -0,0 +1,65 @@
# iAOP Helm Chart(一键部署底座)
对应 PRD 5.6「⑥ 部署底座」与 EPIC #8:
K8s/Helm/ArgoCD 部署底座;GPU/NPU 推理后端可插拔(5090 / 华为昇腾)。
**验收:一套 Chart 部署内核 + 模板;切换后端仅改 values,不动业务代码。**
## 目录结构
```
deploy/k8s/helm/iaop/
├── Chart.yaml Chart 元信息(name/version/appVersion)
├── values.yaml 全部配置点(镜像/后端/副本/资源/存储/灰度/域名)
├── templates/
│ ├── _helpers.tpl 标签/名称辅助模板
│ ├── backend-configmap.yaml 推理后端适配层配置(按 backend 分支渲染)
│ ├── deployment.yaml 推理服务 Deployment(nodeSelector 按后端分流)
│ ├── service.yaml ClusterIP 服务(:8000/v1)
│ ├── ingress.yaml 可选域名入口
│ ├── pvc.yaml 模型权重/日志持久化
│ └── NOTES.txt 部署后提示
└── README.md
```
## 一键部署
```bash
helm repo add iaop http://git.xayunmei.com/iaop/charts # 或本地路径
helm install iaop deploy/k8s/helm/iaop -n iaop --create-namespace
```
## 切换推理后端(仅改 values,业务代码零改动)
```bash
# 默认 gpu(NVIDIA 5090,vLLM/Triton):
helm install iaop deploy/k8s/helm/iaop -n iaop \
--set inference.backend=gpu \
--set inference.device=nvidia-5090
# 切换为华为昇腾 NPU(ACL/CANN/MindIE):
helm upgrade iaop deploy/k8s/helm/iaop -n iaop \
--set inference.backend=npu \
--set inference.device=ascend-910b \
--set inference.cannVersion=8.0
```
切换行为(由模板自动处理):
- `backend-configmap.yaml`:渲染对应后端适配层配置(npu 追加 `cann_version`);
- `deployment.yaml`:nodeSelector 自动切到 `ascend.com/npu=true`(GPU 为 `nvidia.com/gpu=true`),
并在 Pod 标签标注 `iaop.ai/inference-backend`;
- 后端实现差异全部收敛在 `core/inference-backend/` 适配层,业务代码零改动。
## 其他配置点(PRD 5.6)
| 配置点 | values 路径 | 说明 |
| --- | --- | --- |
| 资源配额 | `resources.requests/limits` | CPU/内存配额 |
| 推理后端选择 | `inference.backend` | `gpu` / `npu` |
| 灰度发布策略 | `rollingUpdate` | `maxUnavailable: 0, maxSurge: 1`(滚动) |
| 存储 | `storage.*` | 模型权重持久卷 |
| 域名入口 | `ingress.*` | 可选 Ingress |
## 说明
- Chart 未引入任何外部依赖(dependencies 为空),`helm template` 可离线渲染;
- 推理服务镜像对应 `core/inference-backend/` 适配层容器化(本文档发布配套镜像时更新 `image.tag`)。
+75
View File
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""Helm Chart 结构 sanity 检查(无 helm CLI 环境下的离线基本验证)。
检查项:
1. Chart.yaml / values.yaml 均为合法 YAML;
2. templates/ 下所有模板文件存在且非空;
3. 模板中引用的 `.Values.xxx` 键均能在 values.yaml 中找到(防拼写错误);
4. 每个模板文件的 Go template 标记 `{{` / `}}` 数量配平。
用法:python _sanity_check.py
"""
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
TEMPLATES = os.path.join(HERE, "templates")
import yaml # noqa: E402
def load(path):
with open(path, "r", encoding="utf-8") as fh:
return yaml.safe_load(fh)
def values_paths(text):
"""提取模板文本中的 .Values.<a>.<b> 路径集合。"""
return set(re.findall(r"\.Values\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)", text))
def resolve(values, dotted):
node = values
for key in dotted.split("."):
if not isinstance(node, dict) or key not in node:
return False
node = node[key]
return True
def main():
failures = []
chart = load(os.path.join(HERE, "Chart.yaml"))
assert chart.get("apiVersion") == "v2", "Chart.yaml 应为 Helm v2 chart"
assert chart.get("name") == "iaop", "Chart.name 应为 iaop"
values = load(os.path.join(HERE, "values.yaml"))
assert values.get("inference", {}).get("backend") in ("gpu", "npu"), \
"values.inference.backend 应为 gpu|npu"
for name in sorted(os.listdir(TEMPLATES)):
path = os.path.join(TEMPLATES, name)
if not os.path.isfile(path):
continue
text = open(path, "r", encoding="utf-8").read()
if not text.strip():
failures.append(f"{name}: 模板文件为空")
continue
if text.count("{{") != text.count("}}"):
failures.append(f"{name}: Go template 标记 {{/}} 数量不配平")
for dotted in values_paths(text):
if not resolve(values, dotted):
failures.append(f"{name}: 引用了 values.yaml 中不存在的键 .Values.{dotted}")
if failures:
print("FAIL")
for f in failures:
print(" -", f)
sys.exit(1)
print(f"OK: Chart/values 合法,templates/{len(os.listdir(TEMPLATES))} 个模板校验通过")
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
Thank you for installing {{ .Chart.Name }} {{ .Chart.Version }} (backend: {{ .Values.inference.backend }}).
iAOP 部署底座(PRD 5.6 / EPIC #8)已就绪,Release 名:{{ .Release.Name }}。
快速使用:
1. 查看部署状态:
kubectl get deploy,svc -l app.kubernetes.io/instance={{ .Release.Name }}
2. 健康巡检:
curl http://{{ include "iaop.fullname" . }}:{{ .Values.service.port }}/health
3. 切换推理后端(NVIDIA GPU ↔ 华为昇腾 NPU),仅改 values 后升级:
helm upgrade {{ .Release.Name }} . --set inference.backend=npu
4. 如需域名入口:
helm upgrade {{ .Release.Name }} . --set ingress.enabled=true --set ingress.host=iaop.example.com
@@ -0,0 +1,43 @@
{{/*
iAOP Helm Chart 辅助模板(_helpers.tpl)
*/}}
{{/*
展开 Chart 全名(release 名 + chart 名,截断到 63 字符)。
*/}}
{{- define "iaop.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Chart 名称标签。
*/}}
{{- define "iaop.name" -}}
{{- .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
通用标签。
*/}}
{{- define "iaop.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ include "iaop.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
选择器标签。
*/}}
{{- define "iaop.selectorLabels" -}}
app.kubernetes.io/name: {{ include "iaop.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
推理后端镜像完整名(repository:tag)。
*/}}
{{- define "iaop.image" -}}
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
{{- end -}}
@@ -0,0 +1,36 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "iaop.fullname" . }}-backend
labels:
{{- include "iaop.labels" . | nindent 4 }}
data:
# 推理后端适配层配置(PRD 5.6:切换后端仅改 values.inference.backend)。
# 该配置与 core/inference-backend/config/backends.template.yaml 同构,
# 由推理服务容器在启动时读取。
backends.yaml: |
template: ti-cl4
version: 1.0.0
backend: {{ .Values.inference.backend }}
inference:
model: {{ .Values.inference.model | quote }}
endpoint: http://{{ include "iaop.fullname" . }}:{{ .Values.service.port }}/v1
timeout_seconds: {{ .Values.inference.timeoutSeconds }}
runtime: {{ .Values.inference.runtime | quote }}
device: {{ .Values.inference.device | quote }}
max_tokens: {{ .Values.inference.maxTokens }}
temperature: {{ .Values.inference.temperature }}
{{- if eq .Values.inference.backend "npu" }}
# 昇腾适配层专用字段(CANN 工具链版本;gpu 后端忽略)
cann_version: {{ .Values.inference.cannVersion | default "" | quote }}
{{- end }}
resources:
requests:
cpu: {{ .Values.resources.requests.cpu | quote }}
memory: {{ .Values.resources.requests.memory | quote }}
limits:
cpu: {{ .Values.resources.limits.cpu | quote }}
memory: {{ .Values.resources.limits.memory | quote }}
rollingUpdate:
maxUnavailable: {{ .Values.rollingUpdate.maxUnavailable }}
maxSurge: {{ .Values.rollingUpdate.maxSurge }}
@@ -0,0 +1,72 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "iaop.fullname" . }}
labels:
{{- include "iaop.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: {{ .Values.rollingUpdate.maxUnavailable }}
maxSurge: {{ .Values.rollingUpdate.maxSurge }}
selector:
matchLabels:
{{- include "iaop.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "iaop.selectorLabels" . | nindent 8 }}
iaop.ai/inference-backend: {{ .Values.inference.backend }}
spec:
{{- if eq .Values.inference.backend "gpu" }}
# NVIDIA GPU(5090)节点选择:推理节点打标 nvidia.com/gpu=true
nodeSelector:
nvidia.com/gpu: "true"
{{- else }}
# 华为昇腾 NPU 节点选择:推理节点打标 ascend.com/npu=true
nodeSelector:
ascend.com/npu: "true"
{{- end }}
containers:
- name: inference
image: "{{ include "iaop.image" . }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
env:
- name: IAOP_BACKEND_CONFIG
value: /etc/iaop/backends.yaml
volumeMounts:
- name: backend-config
mountPath: /etc/iaop
readOnly: true
{{- if .Values.storage.enabled }}
- name: model-store
mountPath: /models
{{- end }}
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: backend-config
configMap:
name: {{ include "iaop.fullname" . }}-backend
{{- if .Values.storage.enabled }}
- name: model-store
persistentVolumeClaim:
claimName: {{ include "iaop.fullname" . }}-models
{{- end }}
@@ -0,0 +1,23 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "iaop.fullname" . }}
labels:
{{- include "iaop.labels" . | nindent 4 }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "iaop.fullname" . }}
port:
name: http
{{- end }}
+17
View File
@@ -0,0 +1,17 @@
{{- if .Values.storage.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "iaop.fullname" . }}-models
labels:
{{- include "iaop.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
{{- if .Values.storage.className }}
storageClassName: {{ .Values.storage.className }}
{{- end }}
resources:
requests:
storage: {{ .Values.storage.size }}
{{- end }}
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "iaop.fullname" . }}
labels:
{{- include "iaop.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "iaop.selectorLabels" . | nindent 4 }}
+63
View File
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# iAOP Helm Chart 默认 values(PRD 5.6「⑥ 部署底座」配置点)。
#
# 切换推理后端(NVIDIA 5090 GPU ↔ 华为昇腾 NPU)只需改:
# inference.backend: gpu | npu
# 其余部署(内核+模板、资源配额、灰度、存储、域名)无需改动。
# ---- 镜像 ----
image:
repository: iaop/inference
tag: v1.0.0
pullPolicy: IfNotPresent
# ---- 推理后端选择(PRD 5.6 配置点:切换后端仅改此处) ----
inference:
backend: gpu # gpu | npu
model: iaop-ti-cl4-v1
runtime: vllm # gpu: vllm|triton;npu: mindie|onnx-ascend
device: nvidia-5090 # gpu: nvidia-5090;npu: ascend-310p|ascend-910b
cannVersion: "" # npu: CANN 工具链版本(如 "8.0"),gpu 忽略
maxTokens: 1024
temperature: 0.1
timeoutSeconds: 60
# ---- 副本与滚动(灰度发布策略,PRD 5.6 配置点) ----
replicaCount: 2
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
# ---- 服务 ----
service:
type: ClusterIP
port: 8000
# ---- 域名入口 ----
ingress:
enabled: false
host: iaop.example.com
className: ""
# ---- 资源配额(PRD 5.6 配置点) ----
resources:
requests:
cpu: "2"
memory: 8Gi
limits:
cpu: "8"
memory: 32Gi
# ---- 存储(模型权重/日志持久化) ----
storage:
enabled: true
className: "" # 空 = 使用集群默认 StorageClass
size: 100Gi
# ---- 探针 ----
livenessProbe:
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
initialDelaySeconds: 10
periodSeconds: 5