296 lines
13 KiB
Python
296 lines
13 KiB
Python
"""Hermes chat local compute contracts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from test_hermes_chat_support import (
|
|
HERMES,
|
|
Path,
|
|
ROOT,
|
|
SimpleNamespace,
|
|
_documents,
|
|
importlib,
|
|
json,
|
|
tomllib,
|
|
)
|
|
|
|
|
|
def test_local_flux_runtime_and_gpu_handoff_are_flux_managed():
|
|
"""FLUX and Wolf share titan-24 while text stays on titan-20."""
|
|
deployment = _documents(HERMES / "local-image-deployment.yaml")[0]
|
|
assert deployment["metadata"]["name"] == "hermes-local-image"
|
|
pod = deployment["spec"]["template"]["spec"]
|
|
assert pod["serviceAccountName"] == "hermes-gpu-runtime"
|
|
local = next(item for item in pod["containers"] if item["name"] == "local-image")
|
|
assert len(pod["containers"]) == 1
|
|
assert local["resources"]["requests"]["nvidia.com/gpu.shared"] == 1
|
|
assert local["ports"] == [{"name": "local-image", "containerPort": 9004}]
|
|
assert any(mount["mountPath"] == "/models" for mount in local["volumeMounts"])
|
|
model_env = {item["name"]: item["value"] for item in local["env"]}
|
|
assert model_env["HERMES_LOCAL_IMAGE_LISTEN_PORT"] == "9004"
|
|
assert model_env["HERMES_LOCAL_IMAGE_REVISION"] == (
|
|
"e7b7dc27f91deacad38e78976d1f2b499d76a294"
|
|
)
|
|
assert model_env["HERMES_LOCAL_IMAGE_GPU_ACTIVITY_NODE"] == "titan-24"
|
|
assert model_env["HERMES_LOCAL_IMAGE_GPU_ACTIVE_SM_PERCENT"] == "80"
|
|
assert model_env["HERMES_LOCAL_IMAGE_GPU_MAX_EXTERNAL_MEMORY_BYTES"] == (
|
|
"3221225472"
|
|
)
|
|
assert model_env["HERMES_LOCAL_IMAGE_OFFLOAD_MODE"] == "sequential"
|
|
assert "nvidia-process-exporter-local.monitoring.svc.cluster.local" in model_env[
|
|
"HERMES_LOCAL_IMAGE_GPU_ACTIVITY_URL"
|
|
]
|
|
models_volume = next(item for item in pod["volumes"] if item["name"] == "models")
|
|
assert models_volume["persistentVolumeClaim"]["claimName"] == (
|
|
"hermes-image-models"
|
|
)
|
|
|
|
services = _documents(HERMES / "service.yaml")
|
|
image_service = next(
|
|
item for item in services if item["metadata"]["name"] == "hermes-local-image"
|
|
)
|
|
assert image_service["spec"]["selector"] == {"app": "hermes-local-image"}
|
|
|
|
handoff_services = _documents(HERMES / "model-gate-deployment.yaml")
|
|
handoff = next(
|
|
item
|
|
for item in handoff_services
|
|
if item["kind"] == "Service"
|
|
and item["metadata"]["name"] == "hermes-gpu-handoff"
|
|
)
|
|
assert handoff["spec"]["ports"][0]["targetPort"] == "handoff"
|
|
|
|
ariadne = _documents(
|
|
Path(__file__).parents[2]
|
|
/ "services/maintenance/apps/ariadne-deployment.yaml"
|
|
)[0]
|
|
env = {
|
|
item["name"]: item["value"]
|
|
for item in ariadne["spec"]["template"]["spec"]["containers"][0]["env"]
|
|
if "value" in item
|
|
}
|
|
assert env["GAME_MODE_OLLAMA_URL"] == (
|
|
"http://hermes-gpu-handoff.hermes.svc.cluster.local:11434"
|
|
)
|
|
assert env["GAME_MODE_OLLAMA_MODEL"] == "flux-2-klein-4b-local"
|
|
|
|
for config_name in ("configmap.yaml", "agent-configmap.yaml", "chat-configmap.yaml"):
|
|
config = _documents(HERMES / config_name)[0]["data"]["config.yaml"]
|
|
assert "gpt-oss:20b" not in config
|
|
assert "atlas-switchyard" in config
|
|
switchyard = _documents(HERMES / "switchyard-configmap.yaml")[0]["data"][
|
|
"routes.toml"
|
|
]
|
|
assert 'id = "qwen2.5:14b-instruct-q4_0"' in switchyard
|
|
assert "qwen2.5:3b-instruct-q4_0" not in switchyard
|
|
assert "route/local/qwen2.5-14b/medium" in switchyard
|
|
assert "Anthropic and Claude name the same provider" in switchyard
|
|
assert "OpenAI and Codex name the same provider" in switchyard
|
|
assert "Choose across every configured Codex and Claude family" in switchyard
|
|
assert "Claude Fable" in switchyard
|
|
assert switchyard.count('Treat "think hard"') == 4
|
|
assert switchyard.count("Never choose below the") >= 5
|
|
switchyard_config = tomllib.loads(switchyard)
|
|
routes = switchyard_config["routes"]
|
|
configured_targets = switchyard_config["targets"]
|
|
for route_name in ("auto_fast", "auto_balanced", "auto_deep", "auto_maximum"):
|
|
leading_targets = set(routes[route_name]["targets"][:2])
|
|
assert leading_targets == {"codex_sol_xhigh", "claude_opus_xhigh"}
|
|
assert "max_output_tokens" not in routes[route_name]
|
|
for route_name in ("auto_fast", "auto_balanced", "auto_deep", "auto_maximum"):
|
|
targets = routes[route_name]["targets"]
|
|
selector_targets = routes[route_name]["response_schema"]
|
|
assert not any(target.startswith("local_") for target in targets)
|
|
assert any("fable" in target for target in targets)
|
|
assert "local_qwen" not in selector_targets
|
|
assert "not eligible for foreground" in routes[route_name]["prompt"]
|
|
for route_name in ("auto_deep", "auto_maximum"):
|
|
targets = routes[route_name]["targets"]
|
|
selector_targets = routes[route_name]["response_schema"]
|
|
assert not any(target.endswith("_low") for target in targets)
|
|
assert "_low" not in selector_targets
|
|
maximum_targets = routes["auto_maximum"]["targets"]
|
|
maximum_selector_targets = routes["auto_maximum"]["response_schema"]
|
|
assert not any(target.endswith("_medium") for target in maximum_targets)
|
|
assert "_medium" not in maximum_selector_targets
|
|
assert "absolute high effort floor" in routes["auto_maximum"]["prompt"]
|
|
assert "quality mark was missed" in routes["auto_balanced"]["prompt"]
|
|
assert "raises the next boundary to xhigh" in routes["auto_maximum"]["prompt"]
|
|
assert "Repeated quality misses require xhigh" in routes[
|
|
"worker_auto_maximum"
|
|
]["prompt"]
|
|
assert any(
|
|
target.startswith("local_")
|
|
for target in routes["manual_local_qwen"]["targets"]
|
|
)
|
|
for route_name in (
|
|
"manual_codex_luna",
|
|
"manual_codex_terra",
|
|
"manual_codex_sol",
|
|
"manual_claude_haiku",
|
|
"manual_claude_fable",
|
|
"manual_claude_sonnet",
|
|
"manual_claude_opus",
|
|
):
|
|
assert not any(
|
|
target.startswith("local_") for target in routes[route_name]["targets"]
|
|
)
|
|
for provider, families in {
|
|
"codex": ("luna", "terra", "sol"),
|
|
"claude": ("haiku", "fable", "sonnet", "opus"),
|
|
}.items():
|
|
for family in families:
|
|
for effort in ("low", "medium", "high", "xhigh"):
|
|
route = routes[f"manual_{provider}_{family}_{effort}"]
|
|
assert route["id"] == f"atlas/manual/{provider}/{family}/{effort}"
|
|
assert route["targets"][0] == f"{provider}_{family}_{effort}"
|
|
worker_target = f"worker_{provider}_{family}_{effort}"
|
|
assert worker_target in routes["worker_auto_maximum"]["targets"]
|
|
assert configured_targets[worker_target]["id"] == (
|
|
f"worker/{provider}/{family}/{effort}"
|
|
)
|
|
for route_name in ("auto_fast", "auto_balanced"):
|
|
prompt = routes[route_name]["prompt"]
|
|
assert "image tool—not the conversational model" in prompt
|
|
assert "Do not select a" in prompt
|
|
assert "local Qwen or Claude target" in prompt
|
|
assert "max_output_tokens" not in routes["worker_auto_maximum"]
|
|
model_gate = _documents(HERMES / "model-gate-configmap.yaml")[0]["data"][
|
|
"model_gate.py"
|
|
]
|
|
assert "qwen2.5:14b-instruct-q4_0" in model_gate
|
|
|
|
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_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"]}
|
|
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"]
|
|
}
|
|
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"]
|