217 lines
8.4 KiB
Python
217 lines
8.4 KiB
Python
"""Local inference and sandbox execution contracts for Hermes chat."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
|
|
from testing.tests.test_hermes_chat_support import (
|
|
HERMES,
|
|
ROOT,
|
|
_documents,
|
|
)
|
|
|
|
|
|
def test_titan20_serializes_classifier_and_local_chat_model_residency():
|
|
"""Classifier and local chat share one serialized resident Qwen weight."""
|
|
deployment = _documents(
|
|
Path(__file__).parents[2] / "services/ai-llm/deployment.yaml"
|
|
)[0]
|
|
pod = deployment["spec"]["template"]["spec"]
|
|
required = pod["affinity"]["nodeAffinity"][
|
|
"requiredDuringSchedulingIgnoredDuringExecution"
|
|
]["nodeSelectorTerms"][0]["matchExpressions"][0]
|
|
assert required["values"] == ["titan-20"]
|
|
container = pod["containers"][0]
|
|
env = {item["name"]: item["value"] for item in container["env"]}
|
|
assert env["OLLAMA_MAX_LOADED_MODELS"] == "1"
|
|
assert env["OLLAMA_NUM_PARALLEL"] == "1"
|
|
assert env["OLLAMA_KEEP_ALIVE"] == "-1"
|
|
assert env["OLLAMA_CONTEXT_LENGTH"] == "8192"
|
|
warm_command = " ".join(container["command"])
|
|
assert "--keepalive=-1" not in warm_command
|
|
models = next(item for item in pod["volumes"] if item["name"] == "models")
|
|
assert models["persistentVolumeClaim"]["claimName"] == ("ollama-models-titan20")
|
|
|
|
|
|
def test_chat_tenants_use_healthy_workers_with_rpi4_fallback():
|
|
"""Keep chat placement off the failed titan-08 runtime while retaining fallback."""
|
|
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
|
affinity = statefulset["spec"]["template"]["spec"]["affinity"]
|
|
required = affinity["nodeAffinity"]["requiredDuringSchedulingIgnoredDuringExecution"]
|
|
expressions = required["nodeSelectorTerms"][0]["matchExpressions"]
|
|
allowed = next(
|
|
item
|
|
for item in expressions
|
|
if item["key"] == "kubernetes.io/hostname" and item["operator"] == "In"
|
|
)
|
|
assert allowed["values"] == [
|
|
"titan-06",
|
|
"titan-07",
|
|
"titan-11",
|
|
"titan-12",
|
|
]
|
|
assert "titan-08" not in allowed["values"]
|
|
excluded = next(
|
|
item
|
|
for item in expressions
|
|
if item["key"] == "kubernetes.io/hostname" and item["operator"] == "NotIn"
|
|
)
|
|
assert {
|
|
"titan-04",
|
|
"titan-05",
|
|
"titan-13",
|
|
"titan-14",
|
|
"titan-17",
|
|
"titan-18",
|
|
"titan-19",
|
|
} == set(excluded["values"])
|
|
spread = affinity["podAntiAffinity"]["preferredDuringSchedulingIgnoredDuringExecution"]
|
|
assert spread[0]["podAffinityTerm"]["topologyKey"] == "kubernetes.io/hostname"
|
|
assert spread[0]["podAffinityTerm"]["labelSelector"]["matchLabels"] == {
|
|
"app": "hermes-chat-tenant"
|
|
}
|
|
|
|
|
|
def test_local_image_gpu_guard_distinguishes_background_and_saturated_gpu(monkeypatch):
|
|
"""Lease-idle desktop spikes may coexist, but saturation still blocks FLUX."""
|
|
source = ROOT / "dockerfiles" / "hermes-local-image-server.py"
|
|
spec = importlib.util.spec_from_file_location("hermes_local_image_server", source)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
monkeypatch.setattr(module, "GPU_ACTIVITY_NODE", "titan-24")
|
|
monkeypatch.setattr(module, "GPU_ACTIVE_SM_PERCENT", 80.0)
|
|
monkeypatch.setattr(module, "GPU_MAX_EXTERNAL_MEMORY_BYTES", 3 << 30)
|
|
|
|
idle = module._parse_gpu_activity(
|
|
"\n".join(
|
|
[
|
|
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="host",process="Xorg"} 0',
|
|
'nvidia_process_gpu_memory_used_bytes{node="titan-24",namespace="host",process="Xorg"} 1900000000',
|
|
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="game-stream",process="wolf"} 3',
|
|
'nvidia_process_gpu_memory_used_bytes{node="titan-24",namespace="hermes",process="python"} 9000000000',
|
|
]
|
|
)
|
|
)
|
|
assert idle["interactive_active"] is False
|
|
assert idle["external_gpu_memory_bytes"] == 1900000000
|
|
assert idle["external_gpu_sm_percent"] == 3
|
|
|
|
background_spike = module._parse_gpu_activity(
|
|
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="host",process="sway"} 41\n'
|
|
)
|
|
assert background_spike["interactive_active"] is False
|
|
|
|
active = module._parse_gpu_activity(
|
|
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="host",process="steam"} 91\n'
|
|
)
|
|
assert active["interactive_active"] is True
|
|
assert "91%" in active["gpu_guard_reason"]
|
|
|
|
|
|
def test_local_flux_renderer_uses_a_disposable_cuda_worker(monkeypatch):
|
|
"""A completed render must not leave its CUDA context in the API process."""
|
|
source = ROOT / "dockerfiles" / "hermes-local-image-server.py"
|
|
spec = importlib.util.spec_from_file_location("hermes_local_image_worker", source)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
calls = []
|
|
|
|
def run(command, **kwargs):
|
|
calls.append((command, kwargs))
|
|
return SimpleNamespace(
|
|
returncode=0,
|
|
stdout=b'{"success":true,"route":"local","image_b64":"cG5n"}',
|
|
stderr=b"",
|
|
)
|
|
|
|
monkeypatch.setattr(module.subprocess, "run", run)
|
|
result = module._render({"prompt": "black cat", "aspect_ratio": "square"})
|
|
|
|
assert result["route"] == "local"
|
|
command, options = calls[0]
|
|
assert command[-1] == "--render-worker"
|
|
assert json.loads(options["input"]) == {
|
|
"prompt": "black cat",
|
|
"aspect_ratio": "square",
|
|
}
|
|
assert options["timeout"] == module.RENDER_TIMEOUT_SECONDS
|
|
assert options["check"] is False
|
|
|
|
|
|
def test_local_flux_uses_low_vram_offload_without_reducing_resolution():
|
|
"""The shared 3080 lane must trade time, not image size, for headroom."""
|
|
source = (ROOT / "dockerfiles" / "hermes-local-image-server.py").read_text()
|
|
assert "OFFLOAD_MODE = os.environ.get(" in source
|
|
assert '"HERMES_LOCAL_IMAGE_OFFLOAD_MODE", "sequential"' in source
|
|
assert "pipe.enable_sequential_cpu_offload()" in source
|
|
assert '"square": (1024, 1024)' in source
|
|
|
|
|
|
def test_chat_auth_and_relay_are_pod_lifetime_only():
|
|
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
|
pod = statefulset["spec"]["template"]["spec"]
|
|
containers = statefulset["spec"]["template"]["spec"]["containers"]
|
|
|
|
for name in ("hermes", "webui"):
|
|
container = next(item for item in containers if item["name"] == name)
|
|
env = {
|
|
item["name"]: item["value"]
|
|
for item in container["env"]
|
|
if "value" in item
|
|
}
|
|
assert env["HERMES_AUTH_FILE"] == "/runtime-access/hermes-auth.json"
|
|
mount = next(
|
|
item
|
|
for item in container["volumeMounts"]
|
|
if item["name"] == "runtime-access"
|
|
)
|
|
assert mount["mountPath"] == "/runtime-access"
|
|
assert "subPath" not in mount
|
|
|
|
hermes_env = {
|
|
item["name"]: item["value"]
|
|
for item in next(item for item in containers if item["name"] == "hermes")["env"]
|
|
if "value" in item
|
|
}
|
|
runtime = next(item for item in pod["volumes"] if item["name"] == "runtime-access")
|
|
assert runtime["emptyDir"] == {"medium": "Memory", "sizeLimit": "2Mi"}
|
|
assert not any(item["name"] == "provider-auth" for item in pod["volumes"])
|
|
init_command = next(
|
|
item for item in pod["initContainers"] if item["name"] == "init-config"
|
|
)["command"][2]
|
|
for key in (
|
|
"ANTHROPIC_API_KEY",
|
|
"API_SERVER_KEY",
|
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
"GITEA_TOKEN",
|
|
"HERMES_IMAGE_BROKER_KEY",
|
|
"OPENAI_API_KEY",
|
|
):
|
|
assert key in init_command
|
|
assert "printf 'API_SERVER_KEY=%s" not in init_command
|
|
assert hermes_env["AGENT_BROWSER_EXECUTABLE_PATH"].endswith(
|
|
"/chrome-linux/headless_shell"
|
|
)
|
|
assert "--no-sandbox" in hermes_env["AGENT_BROWSER_ARGS"]
|
|
|
|
|
|
def test_sandbox_executes_python_with_bounded_output(tmp_path: Path, monkeypatch):
|
|
source = ROOT / "dockerfiles" / "hermes-chat-sandbox-server.py"
|
|
spec = importlib.util.spec_from_file_location("hermes_chat_sandbox_server", source)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
monkeypatch.setattr(module, "WORKSPACE", tmp_path)
|
|
|
|
result = module._execute("import math\nprint(math.comb(10, 3))")
|
|
|
|
assert result["success"] is True
|
|
assert result["stdout"] == "120\n"
|
|
assert result["stderr"] == ""
|