NCS (services/node-capabilities/metrics.py): - NodeLoad: inflight_jobs, queue_depth, concurrency_limit, estimated_wait_ms, cpu_load_1m, mem_pressure (macOS + Linux), rtt_ms_to_hub - RuntimeLoad: per-runtime healthy, p50_ms, p95_ms from rolling 50-sample window - POST /capabilities/report_latency for node-worker → NCS reporting - NCS fetches worker metrics via NODE_WORKER_URL Node Worker: - GET /metrics endpoint (inflight, concurrency, latency buffers) - Latency tracking per job type (llm/vision) with rolling buffer - Fire-and-forget latency reporting to NCS after each successful job Router (model_select v3): - score_candidate(): wait + model_latency + cross_node_penalty + prefer_bonus - LOCAL_THRESHOLD_MS=250: prefer local if within threshold of remote - ModelSelection.score field for observability - Structured [score] logs with chosen node, model, and score breakdown Tests: 19 new (12 scoring + 7 NCS metrics), 36 total pass Docs: ops/runbook_p3_1.md, ops/CHANGELOG_FABRIC.md No breaking changes to JobRequest/JobResponse or capabilities schema. Made-with: Cursor
178 lines
6.2 KiB
Python
178 lines
6.2 KiB
Python
"""Tests for P3.1 scoring-based model selection."""
|
|
import sys
|
|
import os
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "services", "router"))
|
|
|
|
from model_select import (
|
|
score_candidate,
|
|
select_best_model,
|
|
ProfileRequirements,
|
|
ModelSelection,
|
|
LOCAL_THRESHOLD_MS,
|
|
)
|
|
|
|
|
|
def _caps(served, node_load=None, runtime_load=None, nodes=None):
|
|
return {
|
|
"served_models": served,
|
|
"node_load": node_load or {},
|
|
"runtime_load": runtime_load or [],
|
|
"nodes": nodes or {},
|
|
}
|
|
|
|
|
|
def _model(name, typ="llm", local=True, node="n1", runtime="ollama", **kw):
|
|
return {"name": name, "type": typ, "local": local, "node": node,
|
|
"runtime": runtime, "base_url": "http://x", **kw}
|
|
|
|
|
|
def _reqs(typ="llm", prefer=None):
|
|
return ProfileRequirements("test", typ, prefer or [])
|
|
|
|
|
|
# ── 1) local wins when scores close ────────────────────────────────────────
|
|
|
|
def test_local_wins_when_scores_close():
|
|
caps = _caps(
|
|
served=[
|
|
_model("qwen3:14b", local=True, node="n1"),
|
|
_model("qwen3:14b", local=False, node="n2"),
|
|
],
|
|
node_load={"estimated_wait_ms": 0, "rtt_ms_to_hub": None},
|
|
)
|
|
sel = select_best_model(_reqs(), caps)
|
|
assert sel is not None
|
|
assert sel.local is True
|
|
assert sel.node == "n1"
|
|
|
|
|
|
# ── 2) remote wins when local wait is high ─────────────────────────────────
|
|
|
|
def test_remote_wins_when_local_wait_high():
|
|
caps = _caps(
|
|
served=[
|
|
_model("qwen3:14b", local=True, node="n1"),
|
|
_model("qwen3:14b", local=False, node="n2"),
|
|
],
|
|
node_load={"estimated_wait_ms": 5000, "rtt_ms_to_hub": None},
|
|
nodes={"n2": {"node_id": "n2", "node_load": {"estimated_wait_ms": 0, "rtt_ms_to_hub": 50}}},
|
|
)
|
|
sel = select_best_model(_reqs(), caps)
|
|
assert sel is not None
|
|
assert sel.local is False
|
|
assert sel.node == "n2"
|
|
|
|
|
|
# ── 3) exclude_nodes works ─────────────────────────────────────────────────
|
|
|
|
def test_exclude_nodes_works():
|
|
caps = _caps(served=[
|
|
_model("qwen3:14b", local=False, node="n2"),
|
|
_model("qwen3:14b", local=False, node="n3"),
|
|
])
|
|
sel = select_best_model(_reqs(), caps, exclude_nodes={"n2"})
|
|
assert sel is not None
|
|
assert sel.node == "n3"
|
|
|
|
|
|
# ── 4) breaker open → node excluded (via exclude_nodes) ───────────────────
|
|
|
|
def test_breaker_excludes_node():
|
|
caps = _caps(served=[
|
|
_model("qwen3:14b", local=False, node="broken"),
|
|
_model("qwen3:14b", local=True, node="n1"),
|
|
])
|
|
sel = select_best_model(_reqs(), caps, exclude_nodes={"broken"})
|
|
assert sel is not None
|
|
assert sel.node == "n1"
|
|
|
|
|
|
# ── 5) required_type filter ────────────────────────────────────────────────
|
|
|
|
def test_required_type_filter():
|
|
caps = _caps(served=[
|
|
_model("qwen3:14b", typ="llm"),
|
|
_model("llava:13b", typ="vision"),
|
|
])
|
|
sel = select_best_model(_reqs(typ="vision"), caps)
|
|
assert sel is not None
|
|
assert sel.name == "llava:13b"
|
|
|
|
|
|
# ── 6) prefer list filter ─────────────────────────────────────────────────
|
|
|
|
def test_prefer_list_selects_preferred():
|
|
caps = _caps(served=[
|
|
_model("qwen3:14b"),
|
|
_model("qwen3.5:35b"),
|
|
])
|
|
sel = select_best_model(_reqs(prefer=["qwen3.5:35b"]), caps)
|
|
assert sel is not None
|
|
assert sel.name == "qwen3.5:35b"
|
|
|
|
|
|
# ── 7) score formula — prefer bonus lowers score ──────────────────────────
|
|
|
|
def test_prefer_bonus_lowers_score():
|
|
m1 = _model("qwen3:14b")
|
|
m2 = _model("qwen3.5:35b")
|
|
caps = _caps(served=[m1, m2])
|
|
s1 = score_candidate(m1, caps, prefer=["qwen3:14b"])
|
|
s2 = score_candidate(m2, caps, prefer=["qwen3:14b"])
|
|
assert s1 < s2
|
|
|
|
|
|
# ── 8) score formula — cross_penalty for remote ──────────────────────────
|
|
|
|
def test_cross_penalty_for_remote():
|
|
local = _model("m", local=True)
|
|
remote = _model("m", local=False, node="r1")
|
|
caps = _caps(served=[local, remote])
|
|
sl = score_candidate(local, caps, prefer=[])
|
|
sr = score_candidate(remote, caps, prefer=[], rtt_hint_ms=50)
|
|
assert sr > sl
|
|
|
|
|
|
# ── 9) score formula — wait increases score ──────────────────────────────
|
|
|
|
def test_wait_increases_score():
|
|
m = _model("m", local=True)
|
|
caps_idle = _caps(served=[m], node_load={"estimated_wait_ms": 0})
|
|
caps_busy = _caps(served=[m], node_load={"estimated_wait_ms": 3000})
|
|
s_idle = score_candidate(m, caps_idle, prefer=[])
|
|
s_busy = score_candidate(m, caps_busy, prefer=[])
|
|
assert s_busy > s_idle
|
|
|
|
|
|
# ── 10) no candidates → None ─────────────────────────────────────────────
|
|
|
|
def test_no_candidates_returns_none():
|
|
caps = _caps(served=[_model("m", typ="stt")])
|
|
sel = select_best_model(_reqs(typ="llm"), caps)
|
|
assert sel is None
|
|
|
|
|
|
# ── 11) local threshold: local wins within threshold even if remote lower ─
|
|
|
|
def test_local_threshold():
|
|
caps = _caps(
|
|
served=[
|
|
_model("qwen3:14b", local=True, node="n1"),
|
|
_model("qwen3:14b", local=False, node="n2"),
|
|
],
|
|
node_load={"estimated_wait_ms": 100},
|
|
nodes={"n2": {"node_id": "n2", "node_load": {"estimated_wait_ms": 0, "rtt_ms_to_hub": 10}}},
|
|
)
|
|
sel = select_best_model(_reqs(), caps)
|
|
assert sel.local is True
|
|
|
|
|
|
# ── 12) code type cross-filters with llm ─────────────────────────────────
|
|
|
|
def test_code_type_finds_llm_models():
|
|
caps = _caps(served=[_model("qwen3:14b", typ="llm")])
|
|
sel = select_best_model(_reqs(typ="code"), caps)
|
|
assert sel is not None
|
|
assert sel.name == "qwen3:14b"
|