wip -more responsive UI, better routing
This commit is contained in:
@@ -349,25 +349,38 @@ def _attach_relay_bridge(node: StubNodeServer | TorchNodeServer, bridge: RelayHt
|
||||
_PENDING_NODE_ID = "pending"
|
||||
|
||||
|
||||
_HEARTBEAT_INTERVAL_IDLE = 20.0
|
||||
_HEARTBEAT_INTERVAL_BUSY = 3.0
|
||||
|
||||
|
||||
def _start_heartbeat(
|
||||
tracker_url: str,
|
||||
node_id: str,
|
||||
register_payload: dict,
|
||||
interval: float = 20.0,
|
||||
interval: float = _HEARTBEAT_INTERVAL_IDLE,
|
||||
node_ref: Any | None = None,
|
||||
start_time: float | None = None,
|
||||
) -> threading.Thread:
|
||||
"""Daemon thread: sends heartbeats and re-registers automatically after tracker restarts.
|
||||
|
||||
Heartbeat body carries cumulative stats (total_requests, failed_requests,
|
||||
queue_depth, uptime_seconds, status). Stats are buffered locally during
|
||||
outage and flushed on next successful heartbeat.
|
||||
queue_depth, current_requests, uptime_seconds, status). Stats are buffered
|
||||
locally during outage and flushed on next successful heartbeat.
|
||||
|
||||
Heartbeat response may include new_assignment: {model, shard_start, shard_end}
|
||||
which is logged for now (hot-reload implemented in US-026).
|
||||
"""
|
||||
_start_time = start_time or time.monotonic()
|
||||
|
||||
def _current_requests_snapshot() -> list[dict]:
|
||||
if node_ref is None:
|
||||
return []
|
||||
getter = getattr(node_ref, "current_requests", None)
|
||||
if getter is None:
|
||||
return []
|
||||
current = getter() if callable(getter) else getter
|
||||
return list(current) if isinstance(current, list) else []
|
||||
|
||||
def _get_stats() -> dict:
|
||||
uptime = time.monotonic() - _start_time
|
||||
stats: dict = {"uptime_seconds": round(uptime, 1), "status": "ready"}
|
||||
@@ -379,8 +392,16 @@ def _start_heartbeat(
|
||||
)
|
||||
stats["failed_requests"] = getattr(node_ref, "failed_requests", 0)
|
||||
stats["queue_depth"] = getattr(node_ref, "queue_depth", 0)
|
||||
current_requests = _current_requests_snapshot()
|
||||
if current_requests:
|
||||
stats["current_requests"] = current_requests
|
||||
return stats
|
||||
|
||||
def _sleep_interval() -> float:
|
||||
if _current_requests_snapshot() or (node_ref is not None and getattr(node_ref, "queue_depth", 0) > 0):
|
||||
return _HEARTBEAT_INTERVAL_BUSY
|
||||
return interval
|
||||
|
||||
def _reregister() -> bool:
|
||||
nonlocal node_id
|
||||
try:
|
||||
@@ -442,7 +463,7 @@ def _start_heartbeat(
|
||||
outage_streak = 1 if node_id == _PENDING_NODE_ID else 0
|
||||
|
||||
while True:
|
||||
time.sleep(interval)
|
||||
time.sleep(_sleep_interval())
|
||||
|
||||
if outage_streak > 0:
|
||||
# Tracker was down — attempt re-registration first (it may have restarted
|
||||
|
||||
@@ -31,6 +31,23 @@ from .server import (
|
||||
)
|
||||
|
||||
|
||||
def _write_progress_line(state: list[bool], message: str, *, final: bool = False) -> None:
|
||||
"""Rewrite one in-place progress line (\\r) or finish with a newline."""
|
||||
if final:
|
||||
if state[0]:
|
||||
sys.stdout.write("\r" + message + "\n")
|
||||
state[0] = False
|
||||
else:
|
||||
print(message, flush=True)
|
||||
return
|
||||
if state[0]:
|
||||
sys.stdout.write("\r" + message)
|
||||
else:
|
||||
sys.stdout.write(message)
|
||||
state[0] = True
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _relay_hop(
|
||||
relay_addr: str,
|
||||
path: str,
|
||||
@@ -91,6 +108,26 @@ class _TorchHTTPServer(http.server.HTTPServer):
|
||||
self.failed_requests: int = 0
|
||||
self.queue_depth: int = 0
|
||||
self._stats_lock = threading.Lock()
|
||||
self._active_requests: dict[str, dict[str, Any]] = {}
|
||||
|
||||
def snapshot_current_requests(self) -> list[dict[str, Any]]:
|
||||
"""In-flight request snapshots for tracker heartbeats."""
|
||||
now = time.monotonic()
|
||||
with self._stats_lock:
|
||||
out: list[dict[str, Any]] = []
|
||||
for rec in self._active_requests.values():
|
||||
elapsed = max(now - float(rec["started"]), 1e-6)
|
||||
tokens = int(rec.get("tokens") or 0)
|
||||
out.append({
|
||||
"request_id": str(rec["request_id"]),
|
||||
"model": str(rec.get("model") or ""),
|
||||
"kind": str(rec.get("kind") or "chat"),
|
||||
"tokens": tokens,
|
||||
"elapsed_seconds": round(elapsed, 1),
|
||||
"tokens_per_sec": round(tokens / elapsed, 2) if tokens > 0 else 0.0,
|
||||
"routing_complete": bool(rec.get("routing_complete")),
|
||||
})
|
||||
return out
|
||||
|
||||
def resolve_backend(self, model_name: str | None) -> TorchModelShard | None:
|
||||
if not model_name:
|
||||
@@ -113,10 +150,53 @@ class _TorchHandler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args): # noqa: suppress request logs in tests
|
||||
pass
|
||||
|
||||
def _request_id(self) -> str:
|
||||
return (
|
||||
self.headers.get("X-Meshnet-Request-Id")
|
||||
or self.headers.get("X-Request-Id")
|
||||
or f"local-{time.time_ns():x}"
|
||||
)
|
||||
|
||||
def _request_log_suffix(self) -> str:
|
||||
req_id = self.headers.get("X-Meshnet-Request-Id") or self.headers.get("X-Request-Id")
|
||||
return f" request_id={req_id}" if req_id else ""
|
||||
|
||||
def _track_request_begin(
|
||||
self,
|
||||
server: "_TorchHTTPServer",
|
||||
request_id: str,
|
||||
model: str,
|
||||
) -> None:
|
||||
with server._stats_lock:
|
||||
server._active_requests[request_id] = {
|
||||
"request_id": request_id,
|
||||
"model": model,
|
||||
"kind": "chat",
|
||||
"started": time.monotonic(),
|
||||
"tokens": 0,
|
||||
"routing_complete": False,
|
||||
}
|
||||
|
||||
def _track_request_progress(
|
||||
self,
|
||||
server: "_TorchHTTPServer",
|
||||
request_id: str,
|
||||
*,
|
||||
tokens: int,
|
||||
routing_complete: bool = False,
|
||||
) -> None:
|
||||
with server._stats_lock:
|
||||
rec = server._active_requests.get(request_id)
|
||||
if rec is None:
|
||||
return
|
||||
rec["tokens"] = tokens
|
||||
if routing_complete:
|
||||
rec["routing_complete"] = True
|
||||
|
||||
def _track_request_end(self, server: "_TorchHTTPServer", request_id: str) -> None:
|
||||
with server._stats_lock:
|
||||
server._active_requests.pop(request_id, None)
|
||||
|
||||
def do_POST(self):
|
||||
server: _TorchHTTPServer = self.server # type: ignore[assignment]
|
||||
if self.path == "/forward":
|
||||
@@ -294,12 +374,14 @@ class _TorchHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
def _handle_chat_completions(self) -> None:
|
||||
server: _TorchHTTPServer = self.server # type: ignore[assignment]
|
||||
request_id = self._request_id()
|
||||
with server._stats_lock:
|
||||
server.total_requests += 1
|
||||
server.queue_depth += 1
|
||||
try:
|
||||
self._do_chat_completions(server)
|
||||
self._do_chat_completions(server, request_id)
|
||||
finally:
|
||||
self._track_request_end(server, request_id)
|
||||
with server._stats_lock:
|
||||
server.queue_depth -= 1
|
||||
|
||||
@@ -308,7 +390,7 @@ class _TorchHandler(http.server.BaseHTTPRequestHandler):
|
||||
with server._stats_lock:
|
||||
server.failed_requests += 1
|
||||
|
||||
def _do_chat_completions(self, server: "_TorchHTTPServer") -> None:
|
||||
def _do_chat_completions(self, server: "_TorchHTTPServer", request_id: str) -> None:
|
||||
body = self._read_json_body()
|
||||
if body is None:
|
||||
return
|
||||
@@ -325,6 +407,7 @@ class _TorchHandler(http.server.BaseHTTPRequestHandler):
|
||||
temperature = float(body.get("temperature") or 1.0)
|
||||
top_p = float(body.get("top_p") or 1.0)
|
||||
|
||||
self._track_request_begin(server, request_id, model_name)
|
||||
print(
|
||||
f" [node] processing chat model={model_name!r} stream={stream} "
|
||||
f"max_tokens={max_tokens}{self._request_log_suffix()}",
|
||||
@@ -335,6 +418,7 @@ class _TorchHandler(http.server.BaseHTTPRequestHandler):
|
||||
# Avoids the single-token-per-forward-pass limitation of the distributed path.
|
||||
if backend.is_head and backend.is_tail:
|
||||
gen_started = time.monotonic()
|
||||
progress_line = [False]
|
||||
try:
|
||||
if stream:
|
||||
token_count = 0
|
||||
@@ -346,13 +430,19 @@ class _TorchHandler(http.server.BaseHTTPRequestHandler):
|
||||
):
|
||||
if token_text:
|
||||
token_count += 1
|
||||
self._track_request_progress(
|
||||
server, request_id, tokens=token_count, routing_complete=True,
|
||||
)
|
||||
yield token_text
|
||||
|
||||
self._stream_openai_response(_counting_stream(), model_name)
|
||||
print(
|
||||
elapsed = time.monotonic() - gen_started
|
||||
tps = token_count / max(elapsed, 1e-6)
|
||||
_write_progress_line(
|
||||
progress_line,
|
||||
f" [node] chat complete (stream) tokens={token_count} "
|
||||
f"elapsed_s={time.monotonic() - gen_started:.1f}{self._request_log_suffix()}",
|
||||
flush=True,
|
||||
f"elapsed_s={elapsed:.1f} tps={tps:.2f}{self._request_log_suffix()}",
|
||||
final=True,
|
||||
)
|
||||
else:
|
||||
text = backend.generate_text(messages, max_tokens, temperature, top_p)
|
||||
@@ -414,10 +504,12 @@ class _TorchHandler(http.server.BaseHTTPRequestHandler):
|
||||
stream_emit = None
|
||||
if stream:
|
||||
stream_emit = self._start_openai_stream(model_name)
|
||||
self._track_request_progress(server, request_id, tokens=0, routing_complete=True)
|
||||
|
||||
_GENERATION_LOG_INTERVAL = 5.0
|
||||
gen_started = time.monotonic()
|
||||
last_gen_log = gen_started
|
||||
progress_line = [False]
|
||||
|
||||
for step in range(max_tokens):
|
||||
try:
|
||||
@@ -437,20 +529,33 @@ class _TorchHandler(http.server.BaseHTTPRequestHandler):
|
||||
if stream_emit is not None:
|
||||
stream_emit(token_str)
|
||||
current_text = current_text + token_str
|
||||
self._track_request_progress(
|
||||
server,
|
||||
request_id,
|
||||
tokens=len(generated),
|
||||
routing_complete=True,
|
||||
)
|
||||
now = time.monotonic()
|
||||
if step == 0 or now - last_gen_log >= _GENERATION_LOG_INTERVAL:
|
||||
print(
|
||||
elapsed = now - gen_started
|
||||
token_count = len(generated)
|
||||
tps = token_count / max(elapsed, 1e-6)
|
||||
_write_progress_line(
|
||||
progress_line,
|
||||
f" [node] generating step={step + 1}/{max_tokens} "
|
||||
f"tokens={len(generated)} elapsed_s={now - gen_started:.1f}",
|
||||
flush=True,
|
||||
f"tokens={token_count} elapsed_s={elapsed:.1f} tps={tps:.2f}",
|
||||
)
|
||||
last_gen_log = now
|
||||
|
||||
if generated:
|
||||
print(
|
||||
f" [node] generation complete tokens={len(generated)} "
|
||||
f"elapsed_s={time.monotonic() - gen_started:.1f}",
|
||||
flush=True,
|
||||
elapsed = time.monotonic() - gen_started
|
||||
token_count = len(generated)
|
||||
tps = token_count / max(elapsed, 1e-6)
|
||||
_write_progress_line(
|
||||
progress_line,
|
||||
f" [node] generation complete tokens={token_count} "
|
||||
f"elapsed_s={elapsed:.1f} tps={tps:.2f}",
|
||||
final=True,
|
||||
)
|
||||
|
||||
result_text = "".join(generated)
|
||||
@@ -849,6 +954,12 @@ class TorchNodeServer:
|
||||
def queue_depth(self) -> int:
|
||||
return self._server.queue_depth if self._server is not None else 0
|
||||
|
||||
@property
|
||||
def current_requests(self) -> list[dict[str, Any]]:
|
||||
if self._server is None:
|
||||
return []
|
||||
return self._server.snapshot_current_requests()
|
||||
|
||||
@property
|
||||
def loaded_model_ids(self) -> list[str]:
|
||||
return list(self._backends.keys())
|
||||
|
||||
@@ -15,9 +15,10 @@ dependencies = [
|
||||
"bitsandbytes>=0.43",
|
||||
"rich>=13",
|
||||
"safetensors>=0.4",
|
||||
"torch>=2.1",
|
||||
"transformers>=5.12",
|
||||
"websockets>=13",
|
||||
"torch>=2.1",
|
||||
"transformers>=5.12",
|
||||
"triton-windows>=3.7; platform_system == 'Windows'",
|
||||
"websockets>=13",
|
||||
"zstandard>=0.22",
|
||||
"kernels>=0.11.1,<0.16",
|
||||
]
|
||||
|
||||
@@ -433,6 +433,34 @@ function hiveThroughputSummary(stats) {
|
||||
return { totalTps, samples };
|
||||
}
|
||||
|
||||
function enrichCallWallFromHeartbeat(states, map) {
|
||||
const nodes = (map && map.nodes) || [];
|
||||
for (const node of nodes) {
|
||||
const reqs = (node.stats && node.stats.current_requests) || [];
|
||||
for (const req of reqs) {
|
||||
const id = req.request_id;
|
||||
if (!id) continue;
|
||||
let rec = states.get(id);
|
||||
if (!rec) {
|
||||
rec = {
|
||||
id,
|
||||
events: [],
|
||||
status: "processing",
|
||||
started: Date.now() / 1000 - Number(req.elapsed_seconds || 0),
|
||||
model: req.model || node.model || "?",
|
||||
};
|
||||
states.set(id, rec);
|
||||
} else if (rec.status === "pending") {
|
||||
rec.status = "processing";
|
||||
}
|
||||
if (req.model) rec.model = req.model;
|
||||
if (req.tokens != null) rec.tokens = req.tokens;
|
||||
if (req.tokens_per_sec != null) rec.tps = req.tokens_per_sec;
|
||||
if (req.elapsed_seconds != null) rec.elapsed = req.elapsed_seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildCallWallStates(events) {
|
||||
const byId = new Map();
|
||||
for (const e of events) {
|
||||
@@ -453,7 +481,7 @@ function buildCallWallStates(events) {
|
||||
rec.route = f.route || f.nodes;
|
||||
rec.nodes = f.nodes;
|
||||
rec.stream = f.stream;
|
||||
} else if (msg === "proxy via relay" || msg === "proxy connected") {
|
||||
} else if (msg === "proxy via relay" || msg === "proxy connected" || msg === "proxy connecting") {
|
||||
rec.status = "processing";
|
||||
if (!rec.started) rec.started = e.ts;
|
||||
rec.model = rec.model || f.model || f.route_model || "?";
|
||||
@@ -539,10 +567,11 @@ function renderRouting(routing) {
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderCallWall(consoleData, stats) {
|
||||
function renderCallWall(consoleData, stats, map) {
|
||||
const events = (consoleData && consoleData.events) || [];
|
||||
const nowSec = Date.now() / 1000;
|
||||
const states = buildCallWallStates(events);
|
||||
enrichCallWallFromHeartbeat(states, map);
|
||||
const active = [];
|
||||
const terminal = [];
|
||||
for (const rec of states.values()) {
|
||||
@@ -577,7 +606,7 @@ function renderCallWall(consoleData, stats) {
|
||||
const note = rec.warn || (rec.route ? short(String(rec.route), 28) : "");
|
||||
const row = [
|
||||
`<span class="${statusCls}">${esc(rec.status)}</span>`,
|
||||
`<span class="num">${esc(callWallAgeSeconds(rec, nowSec).toFixed(1))}s</span>`,
|
||||
`<span class="num">${esc(rec.elapsed != null ? Number(rec.elapsed).toFixed(1) : callWallAgeSeconds(rec, nowSec).toFixed(1))}s</span>`,
|
||||
esc(short(rec.model || "?", 28)),
|
||||
esc(short(rec.id, 18)),
|
||||
`<span class="num">${esc(tps(rec.tps))}</span>`,
|
||||
@@ -1513,7 +1542,7 @@ async function refresh() {
|
||||
renderFraud(wallets, summary);
|
||||
renderStats(stats);
|
||||
renderRouting(routing);
|
||||
renderCallWall(consoleData, stats);
|
||||
renderCallWall(consoleData, stats, map);
|
||||
renderConsole(consoleData);
|
||||
renderNodeThroughput(stats);
|
||||
renderChatModels();
|
||||
|
||||
@@ -3257,6 +3257,14 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
try:
|
||||
started = time.monotonic()
|
||||
_tracker_log(
|
||||
server,
|
||||
"info",
|
||||
"proxy connecting",
|
||||
request_id=request_id,
|
||||
target_url=target_url,
|
||||
stream=is_stream or None,
|
||||
)
|
||||
upstream_result: list[Any] = []
|
||||
connect_errors: list[BaseException] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user