feat(v2): T2 运维层 hw_profile + llama_server 进程管理

This commit is contained in:
tzt
2026-08-30 21:00:52 +08:00
parent 1e51167ea5
commit 78a410b773
9 changed files with 851 additions and 1 deletions
+8
View File
@@ -0,0 +1,8 @@
"""测试辅助:获取空闲 TCP 端口。"""
import socket
def free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
+54
View File
@@ -0,0 +1,54 @@
"""测试替身:模拟 llama-server(供 LlamaServerManager 封闭单测,D11)。
- 解析 --port / -m / -ngl / -c(与真实 llama-server 参数对齐)
- 把 pid / 收到的参数写入环境变量 FAKE_MARKER 指向的 JSON 文件
- 在本机端口起一个最小 http 服务:/health 返回 {"status":"ok"}
- 进程被终止时正常退出
"""
import argparse
import http.server
import json
import os
import sys
def main() -> int:
parser = argparse.ArgumentParser(prog="fake-llama-server")
parser.add_argument("--port", type=int, default=8901)
parser.add_argument("-m", dest="model", default="")
parser.add_argument("-ngl", dest="ngl", default="0")
parser.add_argument("-c", dest="ctx", default="8192")
parser.add_argument("-ctk", dest="ctk", default="")
parser.add_argument("-ctv", dest="ctv", default="")
args, _ = parser.parse_known_args()
marker = os.environ.get("FAKE_MARKER")
if marker:
os.makedirs(os.path.dirname(marker) or ".", exist_ok=True)
with open(marker, "w", encoding="utf-8") as f:
json.dump({"pid": os.getpid(), "port": args.port,
"model": args.model, "args": sys.argv[1:]}, f)
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path.startswith("/health"):
body = json.dumps({"status": "ok", "server": "fake-llama"}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def log_message(self, *a):
pass
srv = http.server.ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
srv.serve_forever()
return 0
if __name__ == "__main__":
sys.exit(main())
+114
View File
@@ -0,0 +1,114 @@
"""T2 硬件档位检测单测(封闭,纯逻辑 + 注入 runner)。"""
import subprocess
import pytest
from runtime.hw_profile import (
TIER_SPECS,
detect,
nvidia_vram_gb,
pick_tier,
tier_spec,
vulkan_present,
)
class _FakeRun:
"""注入 subprocess runner:按命令返回预置输出。"""
def __init__(self, mapping):
self.mapping = mapping # {关键子串: CompletedProcess}
def __call__(self, cmd, timeout):
joined = " ".join(cmd)
for key, cp in self.mapping.items():
if key in joined:
return cp
raise FileNotFoundError(cmd)
def _cp(stdout="", rc=0):
return subprocess.CompletedProcess(args=[], returncode=rc, stdout=stdout, stderr="")
def test_pick_tier_thresholds():
assert pick_tier(None) == "cpu"
assert pick_tier(6.0) == "cpu"
assert pick_tier(8.0) == "gpu8"
assert pick_tier(11.9) == "gpu8"
assert pick_tier(12.0) == "gpu12"
assert pick_tier(24.0) == "gpu12"
def test_tier_specs_have_required_fields():
for tier, spec in TIER_SPECS.items():
assert spec["tier"] == tier
assert "ngl" in spec and "ctx" in spec and "kv_quant" in spec
# gpu12 ngl 全量卸载,cpu ngl 0
assert TIER_SPECS["gpu12"]["ngl"] == 99
assert TIER_SPECS["cpu"]["ngl"] == 0
def test_tier_spec_unknown_raises():
with pytest.raises(ValueError):
tier_spec("nonexistent")
def test_nvidia_vram_gb_parses():
fake = _FakeRun({"nvidia-smi": _cp("24576\n")})
assert nvidia_vram_gb(runner=fake) == 24.0
def test_nvidia_vram_gb_multi_gpu_takes_max():
fake = _FakeRun({"nvidia-smi": _cp("8192\n12288\n")})
assert nvidia_vram_gb(runner=fake) == 12.0
def test_nvidia_vram_gb_missing_tool_returns_none(monkeypatch):
import shutil
monkeypatch.setattr(shutil, "which", lambda name: None)
assert nvidia_vram_gb() is None
def test_vulkan_present_true():
fake = _FakeRun({"vulkaninfo": _cp("deviceName : NVIDIA GeForce RTX 4090")})
assert vulkan_present(runner=fake) is True
def test_vulkan_present_software_false():
fake = _FakeRun({"vulkaninfo": _cp("deviceName : llvmpipe (LLVM)")})
assert vulkan_present(runner=fake) is False
def test_detect_nvidia_gpu12(monkeypatch):
# 让 shutil.which 只对 nvidia-smi 生效
real_which = __import__("shutil").which
def fake_which(name):
return "C:/x/nvidia-smi.exe" if name == "nvidia-smi" else None
monkeypatch.setattr(__import__("shutil"), "which", fake_which)
fake = _FakeRun({"nvidia-smi": _cp("24576\n")})
spec = detect(runner=fake)
assert spec["tier"] == "gpu12"
assert spec["ngl"] == 99 and spec["ctx"] == 32768
assert spec["probe"] == "nvidia"
def test_detect_cpu_fallback(monkeypatch):
monkeypatch.setattr(__import__("shutil"), "which", lambda name: None)
spec = detect()
assert spec["tier"] == "cpu"
assert spec["ngl"] == 0
assert spec["probe"] == "cpu"
def test_detect_override_tier():
spec = detect(override={"tier": "gpu8"})
assert spec["tier"] == "gpu8"
assert spec["ngl"] == 14 and spec["ctx"] == 16384
assert spec["probe"] == "override"
def test_detect_override_field():
spec = detect(override={"tier": "cpu", "ctx": 16384})
assert spec["ctx"] == 16384
assert spec["tier"] == "cpu"
+152
View File
@@ -0,0 +1,152 @@
"""T2 llama-server 进程管理单测(封闭:假二进制 + 注入,D11)。"""
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
import pytest
from runtime.llama_server import LlamaServerManager, LlamaServerError, build_llama_server
from tests._ports import free_port
FAKE_SCRIPT = Path(__file__).parent / "fixtures" / "fake_llama_server.py"
PYTHON = sys.executable
def _make_fake_binary(tmp: Path) -> Path:
"""生成一个 .cmd 包装器:把 venv python + 假脚本当作"二进制"启动。"""
cmd = tmp / "fake-llama-server.cmd"
cmd.write_text(
f'@echo off\r\n"{PYTHON}" "{FAKE_SCRIPT}" %*\r\n',
encoding="utf-8",
)
return cmd
def _make_manager(tmp, binary, port, model, **kw):
marker = tmp / "marker.json"
env = dict(os.environ)
env["FAKE_MARKER"] = str(marker)
return LlamaServerManager(
binary=str(binary),
model=str(model),
port=port,
hw={"tier": "cpu"},
health_timeout_s=15.0,
poll_interval_s=0.2,
log_dir=str(tmp / "runs"),
env=env,
**kw,
), marker
def test_build_command_uses_hw_tier():
m = LlamaServerManager(binary="bin/x.exe", model="models/m.gguf", port=8901,
hw={"tier": "gpu12"})
cmd = m._build_command()
assert Path(cmd[0]) == Path("bin/x.exe")
assert cmd[1] == "-m" and Path(cmd[2]) == Path("models/m.gguf")
assert "--port" in cmd and "8901" in cmd
assert cmd[cmd.index("-ngl") + 1] == "99"
assert cmd[cmd.index("-c") + 1] == "32768"
def test_build_command_extra_args_appended():
m = LlamaServerManager(binary="bin/x.exe", model="models/m.gguf", port=1,
hw={"tier": "cpu"}, extra_args=["--cache-reuse", "256"])
cmd = m._build_command()
assert cmd[-2:] == ["--cache-reuse", "256"]
def test_start_missing_binary_raises(tmp_path):
m = LlamaServerManager(binary=str(tmp_path / "nope.exe"), model=str(tmp_path / "m.gguf"),
port=free_port())
with pytest.raises(LlamaServerError):
m.start()
def test_start_missing_model_raises(tmp_path):
binary = _make_fake_binary(tmp_path)
m = LlamaServerManager(binary=str(binary), model=str(tmp_path / "missing.gguf"),
port=free_port())
with pytest.raises(LlamaServerError):
m.start()
def test_full_lifecycle(tmp_path):
binary = _make_fake_binary(tmp_path)
model = tmp_path / "model.gguf"
model.write_bytes(b"fake")
port = free_port()
m, marker = _make_manager(tmp_path, binary, port, model)
assert m.running is False
assert m.health() is False # 无进程时不健康
ok = m.start()
assert ok is True
assert m.running is True
assert m.health() is True
# 假脚本确实收到了参数
data = json.loads(marker.read_text(encoding="utf-8"))
assert data["port"] == port
assert data["model"] == str(model)
# 再 start 幂等(已运行返回健康)
assert m.start() is True
m.stop()
assert m.running is False
assert m.health() is False
def test_stop_idempotent(tmp_path):
binary = _make_fake_binary(tmp_path)
model = tmp_path / "model.gguf"
model.write_bytes(b"fake")
m, _ = _make_manager(tmp_path, binary, free_port(), model)
m.stop() # 未启动时 stop 不抛
assert m.running is False
def test_ensure_alive_healthy_no_restart(tmp_path):
binary = _make_fake_binary(tmp_path)
model = tmp_path / "model.gguf"
model.write_bytes(b"fake")
m, _ = _make_manager(tmp_path, binary, free_port(), model)
m.start()
assert m.ensure_alive() is True
# 不应触发重启
assert m._restart_count == 0
m.stop()
def test_ensure_alive_restart_exhausted(tmp_path):
binary = _make_fake_binary(tmp_path)
model = tmp_path / "model.gguf"
model.write_bytes(b"fake")
m, _ = _make_manager(tmp_path, binary, free_port(), model, max_restarts=0)
# 未启动:restart 上限 0 -> False
assert m.ensure_alive() is False
def test_endpoint_format():
m = LlamaServerManager(binary="x", model="m", port=8901, hw={"tier": "cpu"})
assert m.endpoint() == "http://127.0.0.1:8901"
def test_build_from_config(tmp_path):
binary = _make_fake_binary(tmp_path)
cfg = {
"binary": str(binary),
"model": str(tmp_path / "m.gguf"),
"port": 8999,
"hw": {"tier": "cpu"},
"extra_args": ["-fa"],
}
m = build_llama_server(cfg)
assert m.port == 8999
assert "-fa" in m.extra_args