154 lines
5.3 KiB
Python
154 lines
5.3 KiB
Python
"""Tracker-backed latency experiments and model-speed drill-down."""
|
|
|
|
import http.server
|
|
import json
|
|
import threading
|
|
import time
|
|
import urllib.request
|
|
|
|
import pytest
|
|
|
|
from meshnet_tracker.server import TrackerServer
|
|
|
|
|
|
MODELS = {
|
|
"qwen2.5-0.5b-instruct": (24, "Qwen/Qwen2.5-0.5B-Instruct"),
|
|
"qwen3.6-35b-a3b": (40, "unsloth/Qwen3.6-35B-A3B"),
|
|
}
|
|
|
|
|
|
def _post_json(url: str, payload: dict) -> dict:
|
|
request = urllib.request.Request(
|
|
url,
|
|
data=json.dumps(payload).encode(),
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(request, timeout=10.0) as response:
|
|
return json.loads(response.read())
|
|
|
|
|
|
def _get_json(url: str) -> dict:
|
|
with urllib.request.urlopen(url, timeout=10.0) as response:
|
|
return json.loads(response.read())
|
|
|
|
|
|
class _LatencyNode(http.server.BaseHTTPRequestHandler):
|
|
"""Synthetic node: every downstream Activation Seam adds deterministic delay."""
|
|
|
|
base_delay_seconds = 0.004
|
|
seam_delay_seconds = 0.006
|
|
|
|
def log_message(self, *_args):
|
|
pass
|
|
|
|
def do_POST(self):
|
|
self.rfile.read(int(self.headers.get("Content-Length", 0)))
|
|
downstream = json.loads(self.headers.get("X-Meshnet-Route", "[]"))
|
|
time.sleep(self.base_delay_seconds + self.seam_delay_seconds * len(downstream))
|
|
body = json.dumps({
|
|
"choices": [{"message": {"role": "assistant", "content": "ok " * 40}}],
|
|
"usage": {"prompt_tokens": 10, "completion_tokens": 40},
|
|
}).encode()
|
|
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)
|
|
|
|
|
|
def _start_latency_nodes(count: int):
|
|
nodes = []
|
|
threads = []
|
|
for _ in range(count):
|
|
node = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _LatencyNode)
|
|
thread = threading.Thread(target=node.serve_forever, daemon=True)
|
|
thread.start()
|
|
nodes.append(node)
|
|
threads.append(thread)
|
|
return nodes, threads
|
|
|
|
|
|
@pytest.mark.parametrize("model", MODELS)
|
|
@pytest.mark.parametrize("hardware", ["gpu", "gpu-cpu", "cpu"])
|
|
def test_tracker_records_increasing_hop_latency_for_model_and_hardware(model, hardware):
|
|
"""One through five hops must preserve a measurable seam penalty in tracker stats."""
|
|
layer_count, hf_repo = MODELS[model]
|
|
nodes, threads = _start_latency_nodes(5)
|
|
tracker = TrackerServer(model_presets={
|
|
model: {
|
|
"layers_start": 0,
|
|
"layers_end": layer_count - 1,
|
|
"hf_repo": hf_repo,
|
|
"aliases": [model],
|
|
}
|
|
})
|
|
tracker_port = tracker.start()
|
|
try:
|
|
registered_ids = []
|
|
for index, node in enumerate(nodes):
|
|
start = (layer_count * index) // 5
|
|
end = (layer_count * (index + 1)) // 5 - 1
|
|
device = "cuda" if hardware == "gpu" or (hardware == "gpu-cpu" and index == 0) else "cpu"
|
|
data = _post_json(
|
|
f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
|
|
{
|
|
"endpoint": f"http://127.0.0.1:{node.server_address[1]}",
|
|
"model": model,
|
|
"hf_repo": hf_repo,
|
|
"num_layers": layer_count,
|
|
"shard_start": start,
|
|
"shard_end": end,
|
|
"tracker_mode": index == 0,
|
|
"hardware_profile": {"device": device},
|
|
"vram_bytes": 8_000_000_000 if device == "cuda" else 0,
|
|
"ram_bytes": 32_000_000_000,
|
|
"benchmark_tokens_per_sec": 100.0,
|
|
},
|
|
)
|
|
registered_ids.append(data["node_id"])
|
|
|
|
for hops in range(1, 6):
|
|
route = registered_ids[:hops]
|
|
# Widen the final shard to make each pinned prefix a complete route.
|
|
with tracker._server.lock:
|
|
tracker._server.registry[route[-1]].shard_end = layer_count - 1
|
|
_post_json(
|
|
f"http://127.0.0.1:{tracker_port}/v1/chat/completions",
|
|
{
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": "measure"}],
|
|
"route": route,
|
|
},
|
|
)
|
|
|
|
report = _get_json(f"http://127.0.0.1:{tracker_port}/v1/model-speed?model={model}")
|
|
finally:
|
|
tracker.stop()
|
|
for node, thread in zip(nodes, threads):
|
|
node.shutdown()
|
|
node.server_close()
|
|
thread.join(timeout=1.0)
|
|
|
|
routes = {entry["hop_count"]: entry for entry in report["routes"]}
|
|
assert set(routes) == {1, 2, 3, 4, 5}
|
|
assert routes[5]["latency_ms"] > routes[1]["latency_ms"]
|
|
assert routes[5]["latency_penalty_ms"] > 0
|
|
assert routes[5]["device_mix"] == hardware
|
|
assert report["model"] == model
|
|
assert report["nodes"]
|
|
|
|
|
|
def test_model_speed_dashboard_includes_visualization_and_route_drilldown():
|
|
tracker = TrackerServer()
|
|
port = tracker.start()
|
|
try:
|
|
html = urllib.request.urlopen(f"http://127.0.0.1:{port}/dashboard").read().decode()
|
|
finally:
|
|
tracker.stop()
|
|
|
|
assert "Model inference speed" in html
|
|
assert "model-speed-chart" in html
|
|
assert "renderModelSpeed" in html
|
|
assert "/v1/model-speed" in html
|