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
@@ -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()