Files
neuron-tai/packages/node/meshnet_node/torch_server.py
Dobromir Popov e9a094b620 ram pool map
2026-07-07 18:35:36 +03:00

894 lines
35 KiB
Python

"""HTTP server for real PyTorch-backed shard nodes."""
from __future__ import annotations
import base64
import http.server
import json
import sys
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
from pathlib import Path
from typing import Any
from .model_backend import (
InsufficientVRAMError,
MissingModelDependencyError,
Quantization,
TorchModelShard,
validate_quantization,
)
from .server import (
_WIRE_VERSION,
_compress_body,
_decompress_body,
_parse_shape,
_validate_activation_body,
)
def _relay_hop(
relay_addr: str,
path: str,
body: bytes,
headers: dict[str, str],
timeout: float = 120.0,
) -> tuple[int, dict[str, str], bytes]:
"""Send a single HTTP-shaped request through a relay RPC WebSocket.
relay_addr is the wss://relay.../rpc/{peer_id} URL.
Returns (status, response_headers_lower, response_body).
Raises on connection failure so callers can fall back to direct.
"""
import websockets.sync.client as wsc # type: ignore[import]
request_id = f"{time.time_ns():x}"
payload = json.dumps({
"request_id": request_id,
"method": "POST",
"path": path,
"headers": headers,
"body_base64": base64.b64encode(body).decode(),
})
with wsc.connect(relay_addr, open_timeout=timeout) as ws:
ws.send(payload)
raw = ws.recv(timeout=timeout)
resp = json.loads(raw)
status = int(resp.get("status", 503))
resp_headers = {k.lower(): v for k, v in (resp.get("headers") or {}).items()}
body_b64 = resp.get("body_base64")
resp_body = base64.b64decode(body_b64) if body_b64 else (resp.get("body") or "").encode()
return status, resp_headers, resp_body
class _TorchHTTPServer(http.server.HTTPServer):
def __init__(
self,
addr,
handler,
backend: TorchModelShard,
tracker_mode: bool = False,
tracker_url: str | None = None,
route_timeout: float = 30.0,
debug: bool = False,
max_loaded_shards: int = 1,
):
super().__init__(addr, handler)
self.backend = backend
self.backends: dict[str, TorchModelShard] = {backend.model_id: backend}
self.received_activations = False
self.forward_chunk_count = 0
self.tracker_mode = tracker_mode
self.tracker_url = tracker_url
self.route_timeout = route_timeout
self.debug = debug
self.max_loaded_shards = max(1, max_loaded_shards)
self.total_requests: int = 0
self.failed_requests: int = 0
self.queue_depth: int = 0
self._stats_lock = threading.Lock()
def resolve_backend(self, model_name: str | None) -> TorchModelShard | None:
if not model_name:
return self.backend
wanted = model_name.strip().lower()
for key, shard_backend in self.backends.items():
key_l = key.lower()
if key_l == wanted or key_l.rsplit("/", 1)[-1] == wanted:
return shard_backend
return self.backend
def chat_enabled(self) -> bool:
return any(
shard_backend.is_head
for shard_backend in self.backends.values()
)
class _TorchHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, fmt, *args): # noqa: suppress request logs in tests
pass
def do_POST(self):
server: _TorchHTTPServer = self.server # type: ignore[assignment]
if self.path == "/forward":
self._handle_forward()
elif self.path == "/v1/infer":
self._handle_infer()
elif self.path == "/v1/chat/completions" and server.chat_enabled():
self._handle_chat_completions()
else:
self.send_response(404)
self.end_headers()
def _handle_infer(self) -> None:
body = self._read_json_body()
if body is None:
return
messages = body.get("messages", [])
prompt = ""
if isinstance(messages, list) and messages:
last = messages[-1]
if isinstance(last, dict):
prompt = str(last.get("content", ""))
server: _TorchHTTPServer = self.server # type: ignore[assignment]
try:
payload = server.backend.encode_prompt(prompt)
if server.backend.is_tail:
text = server.backend.decode_tail(
server.backend.torch.frombuffer(
bytearray(payload.body),
dtype=server.backend.torch.bfloat16,
)
.reshape(payload.shape)
.to(server.backend.device)
)
self._send_json(200, {"text": text})
return
self._send_json(200, {"activations": {"shape": payload.shape, "dtype": "bfloat16"}})
except Exception as exc:
self._send_json(500, {"error": str(exc)})
def _handle_forward(self) -> None:
content_type = self.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
self._handle_prompt_forward()
return
self._handle_binary_forward()
def _handle_prompt_forward(self) -> None:
body = self._read_json_body()
if body is None:
return
prompt = str(body.get("prompt", ""))
server: _TorchHTTPServer = self.server # type: ignore[assignment]
try:
payload = server.backend.encode_prompt(prompt)
except Exception as exc:
self._send_json(400, {"error": str(exc)})
return
self._send_activation(payload)
def _handle_binary_forward(self) -> None:
server: _TorchHTTPServer = self.server # type: ignore[assignment]
try:
shape = _parse_shape(self.headers.get("X-Meshnet-Shape"))
dtype = self.headers.get("X-Meshnet-Dtype", "")
session = self.headers["X-Meshnet-Session"]
chunk_index = self.headers["X-Meshnet-Chunk-Index"]
chunk_total = self.headers["X-Meshnet-Chunk-Total"]
encoding = self.headers.get("X-Meshnet-Encoding")
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
raw_body = _decompress_body(body, encoding)
_validate_activation_body(raw_body, shape, dtype)
if dtype != "bfloat16":
raise ValueError("real model backend requires bfloat16 activation input")
chunk_index_value = int(chunk_index)
chunk_total_value = int(chunk_total)
if chunk_total_value <= 0 or not 0 <= chunk_index_value < chunk_total_value:
raise ValueError("invalid chunk index/total")
except (KeyError, ValueError, TypeError):
self.send_response(400)
self.send_header("X-Meshnet-Wire", _WIRE_VERSION)
self.end_headers()
return
server.forward_chunk_count += 1
if int(self.headers.get("X-Meshnet-Hop-Index", "0")) > 0:
server.received_activations = True
start_layer_header = self.headers.get("X-Meshnet-Start-Layer")
start_layer = int(start_layer_header) if start_layer_header else None
try:
result = server.backend.forward_bytes(
raw_body,
shape,
self.headers.get("X-Meshnet-Attn-Mask"),
self.headers.get("X-Meshnet-Position-Ids"),
start_layer=start_layer,
)
except Exception as exc:
self._send_json(500, {"error": str(exc)})
return
if isinstance(result, str):
self._send_json(200, {"text": result})
return
response_body = _compress_body(result.body, encoding)
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(response_body)))
self.send_header("X-Meshnet-Wire", _WIRE_VERSION)
self.send_header("X-Meshnet-Shape", ",".join(str(dim) for dim in result.shape))
self.send_header("X-Meshnet-Dtype", "bfloat16")
self.send_header("X-Meshnet-Session", session)
self.send_header("X-Meshnet-Chunk-Index", chunk_index)
self.send_header("X-Meshnet-Chunk-Total", chunk_total)
if encoding:
self.send_header("X-Meshnet-Encoding", encoding)
if result.attention_mask_header:
self.send_header("X-Meshnet-Attn-Mask", result.attention_mask_header)
if result.position_ids_header:
self.send_header("X-Meshnet-Position-Ids", result.position_ids_header)
self.end_headers()
self.wfile.write(response_body)
def _send_activation(self, payload) -> None:
body = payload.body
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(body)))
self.send_header("X-Meshnet-Wire", _WIRE_VERSION)
self.send_header("X-Meshnet-Shape", ",".join(str(dim) for dim in payload.shape))
self.send_header("X-Meshnet-Dtype", "bfloat16")
if payload.attention_mask_header:
self.send_header("X-Meshnet-Attn-Mask", payload.attention_mask_header)
if payload.position_ids_header:
self.send_header("X-Meshnet-Position-Ids", payload.position_ids_header)
self.end_headers()
self.wfile.write(body)
def _read_json_body(self) -> dict | None:
length = int(self.headers.get("Content-Length", 0))
try:
body = json.loads(self.rfile.read(length) or b"{}")
except (json.JSONDecodeError, ValueError):
self._send_json(400, {"error": "invalid JSON body"})
return None
if not isinstance(body, dict):
self._send_json(400, {"error": "JSON body must be an object"})
return None
return body
def _send_json(self, status: int, data: dict) -> None:
payload = json.dumps(data).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
try:
self.wfile.write(payload)
except BrokenPipeError:
pass # client disconnected before we could respond — not an error
def _handle_chat_completions(self) -> None:
server: _TorchHTTPServer = self.server # type: ignore[assignment]
with server._stats_lock:
server.total_requests += 1
server.queue_depth += 1
try:
self._do_chat_completions(server)
finally:
with server._stats_lock:
server.queue_depth -= 1
def _record_failed_request(self) -> None:
server: _TorchHTTPServer = self.server # type: ignore[assignment]
with server._stats_lock:
server.failed_requests += 1
def _do_chat_completions(self, server: "_TorchHTTPServer") -> None:
body = self._read_json_body()
if body is None:
return
messages = body.get("messages", [])
if not isinstance(messages, list):
messages = []
stream = bool(body.get("stream", False))
model_name = str(body.get("model", ""))
backend = server.resolve_backend(model_name)
if backend is None or not backend.is_head:
self._send_json(400, {"error": "model not loaded on this node"})
return
max_tokens = int(body.get("max_tokens") or body.get("max_new_tokens") or 256)
temperature = float(body.get("temperature") or 1.0)
top_p = float(body.get("top_p") or 1.0)
# Fast path: this node owns the complete model — use HF generate() with KV cache.
# Avoids the single-token-per-forward-pass limitation of the distributed path.
if backend.is_head and backend.is_tail:
try:
if stream:
self._stream_openai_response(
backend.generate_text_streaming(messages, max_tokens, temperature, top_p),
model_name,
)
else:
text = backend.generate_text(messages, max_tokens, temperature, top_p)
self._send_openai_response(text, model_name, False, messages, backend=backend)
except Exception as exc:
self._record_failed_request()
self._send_json(500, {"error": f"generation failed: {exc}"})
return
# Distributed path: autoregressive generation across shards.
# We do N single-step forward passes (no cross-node KV cache), which is slow
# but correct. Each step: head encodes current sequence → forwards through route
# → tail returns the next token string → append → repeat.
remaining_route = self._get_remaining_route(model_name, backend=backend)
print(
f" [node] chat route model={model_name!r} max_tokens={max_tokens} "
f"downstream={remaining_route}",
flush=True,
)
if not remaining_route:
self._send_openai_response(
"error: no downstream route — check tracker connectivity",
model_name, False, messages, backend=backend,
)
return
# Format with chat template so the model knows it's in assistant mode.
try:
if hasattr(backend.tokenizer, "apply_chat_template"):
prompt_text: str = backend.tokenizer.apply_chat_template(
messages, add_generation_prompt=True, tokenize=False,
)
else:
raise AttributeError("no apply_chat_template")
except Exception:
prompt_text = " ".join(
str(m.get("content", ""))
for m in messages
if isinstance(m, dict) and m.get("role") == "user"
)
eos_token: str = getattr(backend.tokenizer, "eos_token", "") or ""
generated: list[str] = []
current_text = prompt_text
stream_emit = None
if stream:
stream_emit = self._start_openai_stream(model_name)
for _ in range(max_tokens):
try:
payload = backend.encode_prompt(current_text)
except Exception as exc:
print(f" [node] distributed encode error: {exc}", flush=True)
break
token_str = self._run_downstream_pipeline(payload, remaining_route, backend=backend)
if not token_str:
break
# Stop on error responses or EOS.
if token_str.startswith(("pipeline error", "decode error", "no downstream", "error:")):
break
if eos_token and token_str == eos_token:
break
generated.append(token_str)
if stream_emit is not None:
stream_emit(token_str)
current_text = current_text + token_str
result_text = "".join(generated)
if stream_emit is not None:
stream_emit(None)
return
self._send_openai_response(result_text, model_name, stream, messages, backend=backend)
def _get_remaining_route(self, model: str, *, backend: TorchModelShard | None = None) -> list[dict]:
"""Return downstream hops as dicts with endpoint, start_layer, and optional relay_addr.
Fast path reads X-Meshnet-Route header injected by the tracker.
Slow path queries the tracker's /v1/route endpoint as a fallback.
start_layer tells each downstream node which layer to begin from,
enabling correct execution when shard ranges overlap.
"""
# Fast path: tracker pre-resolved the downstream route and injected it as a header.
injected = self.headers.get("X-Meshnet-Route")
if injected:
try:
route = json.loads(injected)
if isinstance(route, list):
hops: list[dict] = []
for item in route:
if isinstance(item, dict):
hop = {
"endpoint": str(item["endpoint"]),
"start_layer": int(item.get("start_layer", 0)),
}
if item.get("relay_addr"):
hop["relay_addr"] = str(item["relay_addr"])
hops.append(hop)
elif isinstance(item, str):
hops.append({"endpoint": item, "start_layer": 0})
print(f" [node] using injected downstream route: {[h['endpoint'] for h in hops]}", flush=True)
return hops
except (json.JSONDecodeError, TypeError, KeyError):
pass
# Slow path: query the tracker (direct node-to-node calls, or tracker didn't inject).
server: _TorchHTTPServer = self.server # type: ignore[assignment]
active_backend = backend or server.backend
if server.tracker_url is None:
return []
route_model = getattr(active_backend, "model_id", None) or model
try:
url = f"{server.tracker_url}/v1/route?model={urllib.parse.quote(route_model)}"
with urllib.request.urlopen(url, timeout=server.route_timeout) as r:
route_resp = json.loads(r.read())
own_port = server.server_address[1]
nodes_info = route_resp.get("nodes", [])
hops = []
covered_up_to: int | None = None
for node_info in nodes_info:
ep = node_info.get("endpoint", "")
if ep.rstrip("/").endswith(f":{own_port}"):
covered_up_to = node_info.get("shard_end")
continue
if covered_up_to is None:
covered_up_to = (node_info.get("shard_start") or 1) - 1
hop = {"endpoint": ep, "start_layer": covered_up_to + 1}
if node_info.get("relay_addr"):
hop["relay_addr"] = str(node_info["relay_addr"])
hops.append(hop)
covered_up_to = node_info.get("shard_end", covered_up_to)
print(f" [node] tracker downstream route: {[h['endpoint'] for h in hops]}", flush=True)
return hops
except Exception as exc:
print(f" [node] WARNING: route lookup failed for {route_model!r}: {exc}", flush=True)
return []
def _run_downstream_pipeline(self, payload: object, route: list[dict], *, backend: TorchModelShard | None = None) -> str:
server: _TorchHTTPServer = self.server # type: ignore[assignment]
active_backend = backend or server.backend
if not route:
# Partial shard at tail: decode the activation from the previous node.
# Full single-node (head+tail) is handled before entering this method.
if active_backend.is_tail:
try:
tensor = active_backend.torch.frombuffer(
bytearray(payload.body), # type: ignore[union-attr]
dtype=active_backend.torch.bfloat16,
).reshape(payload.shape).to(active_backend.device) # type: ignore[union-attr]
return active_backend.decode_tail(tensor)
except Exception as exc:
return f"decode error: {exc}"
return "no downstream route available for non-tail shard"
session = str(uuid.uuid4())
shape = payload.shape # type: ignore[union-attr]
attn_mask = payload.attention_mask_header # type: ignore[union-attr]
pos_ids = payload.position_ids_header # type: ignore[union-attr]
current_body = payload.body # type: ignore[union-attr]
current_shape = shape
current_attn = attn_mask
current_pos = pos_ids
for hop_index, hop in enumerate(route):
node_url = hop["endpoint"]
start_layer = hop.get("start_layer", 0)
relay_addr = hop.get("relay_addr")
if server.debug:
print(
f" [node] pipeline hop {hop_index}: {node_url} start_layer={start_layer}"
+ (f" relay={relay_addr}" if relay_addr else ""),
flush=True,
)
headers: dict[str, str] = {
"Content-Type": "application/octet-stream",
"X-Meshnet-Wire": _WIRE_VERSION,
"X-Meshnet-Shape": ",".join(str(d) for d in current_shape),
"X-Meshnet-Dtype": "bfloat16",
"X-Meshnet-Session": session,
"X-Meshnet-Chunk-Index": "0",
"X-Meshnet-Chunk-Total": "1",
"X-Meshnet-Hop-Index": str(hop_index),
"X-Meshnet-Start-Layer": str(start_layer),
}
if current_attn:
headers["X-Meshnet-Attn-Mask"] = current_attn
if current_pos:
headers["X-Meshnet-Position-Ids"] = current_pos
if relay_addr:
try:
status, resp_headers, resp_body = _relay_hop(
relay_addr, "/forward", current_body, headers, timeout=120.0,
)
if status >= 400:
print(
f" [node] relay hop {hop_index} returned {status} from {relay_addr}",
flush=True,
)
return f"pipeline error at {node_url} via relay: status {status}"
except Exception as exc:
print(
f" [node] relay hop {hop_index} failed at {relay_addr}: {exc}; "
f"falling back to direct {node_url}",
flush=True,
)
relay_addr = None # fall through to direct
if not relay_addr:
req = urllib.request.Request(
f"{node_url}/forward",
data=current_body,
headers=headers,
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=120.0) as r:
resp_body = r.read()
resp_headers = {k.lower(): v for k, v in r.headers.items()}
except Exception as exc:
print(f" [node] pipeline hop {hop_index} failed at {node_url}: {exc}", flush=True)
return f"pipeline error at {node_url}: {exc}"
content_type = resp_headers.get("content-type", "")
if "application/json" in content_type:
try:
data = json.loads(resp_body)
text = str(data.get("text", ""))
if server.debug:
print(f" [node] pipeline hop {hop_index} returned text={text!r}", flush=True)
return text
except json.JSONDecodeError:
return resp_body.decode("utf-8", errors="replace")
# Binary activation — update and forward to next node
shape_header = resp_headers.get("x-meshnet-shape", ",".join(str(d) for d in current_shape))
current_shape = _parse_shape(shape_header)
current_body = resp_body
current_attn = resp_headers.get("x-meshnet-attn-mask")
current_pos = resp_headers.get("x-meshnet-position-ids")
return ""
def _stream_openai_response(self, token_iter, model: str) -> None:
"""Stream tokens from an iterator as SSE chunks."""
emit = self._start_openai_stream(model)
for token_text in token_iter:
if not token_text:
continue
emit(token_text)
emit(None)
def _start_openai_stream(self, model: str):
"""Open an OpenAI-compatible SSE response and return a token emitter."""
chunk_id = "chatcmpl-node"
created = int(time.time())
self.send_response(200)
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
def _emit(data: str) -> None:
try:
self.wfile.write(f"data: {data}\n\n".encode())
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError):
pass
_emit(json.dumps({
"id": chunk_id, "object": "chat.completion.chunk", "created": created,
"model": model,
"choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "finish_reason": None}],
}))
def emit_token(token_text: str | None) -> None:
if token_text is None:
_emit(json.dumps({
"id": chunk_id, "object": "chat.completion.chunk", "created": created,
"model": model,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
}))
try:
self.wfile.write(b"data: [DONE]\n\n")
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError):
pass
return
_emit(json.dumps({
"id": chunk_id, "object": "chat.completion.chunk", "created": created,
"model": model,
"choices": [{"index": 0, "delta": {"content": token_text}, "finish_reason": None}],
}))
return emit_token
def _send_openai_response(
self,
text: str,
model: str,
stream: bool,
messages: list[dict] | None = None,
backend: TorchModelShard | None = None,
) -> None:
chunk_id = "chatcmpl-node"
created = int(time.time())
active_backend = backend or self.server.backend # type: ignore[attr-defined]
if not stream:
usage = _usage_for_response(active_backend, messages or [], text)
self._send_json(200, {
"id": chunk_id,
"object": "chat.completion",
"created": created,
"model": model,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop",
}],
"usage": usage,
})
return
self.send_response(200)
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
def _emit(data: str) -> None:
try:
self.wfile.write(f"data: {data}\n\n".encode())
self.wfile.flush()
except BrokenPipeError:
pass
_emit(json.dumps({
"id": chunk_id, "object": "chat.completion.chunk", "created": created,
"model": model,
"choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "finish_reason": None}],
}))
_emit(json.dumps({
"id": chunk_id, "object": "chat.completion.chunk", "created": created,
"model": model,
"choices": [{"index": 0, "delta": {"content": text}, "finish_reason": None}],
}))
_emit(json.dumps({
"id": chunk_id, "object": "chat.completion.chunk", "created": created,
"model": model,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
}))
try:
self.wfile.write(b"data: [DONE]\n\n")
self.wfile.flush()
except BrokenPipeError:
pass
def _usage_for_response(backend: object, messages: list[dict], completion_text: str) -> dict[str, int]:
prompt_tokens = _backend_token_count(
backend,
"count_prompt_tokens",
messages,
fallback=_fallback_message_token_count(messages),
)
completion_tokens = _backend_token_count(
backend,
"count_text_tokens",
completion_text,
fallback=_fallback_text_token_count(completion_text),
)
return {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": prompt_tokens + completion_tokens,
}
def _backend_token_count(backend: object, method_name: str, value: object, fallback: int) -> int:
method: Any = getattr(backend, method_name, None)
if callable(method):
try:
return max(0, int(method(value)))
except Exception:
pass
return max(0, int(fallback))
def _fallback_message_token_count(messages: list[dict]) -> int:
text = " ".join(
str(message.get("content", ""))
for message in messages
if isinstance(message, dict)
)
return _fallback_text_token_count(text)
def _fallback_text_token_count(text: str) -> int:
parts = text.split()
if parts:
return len(parts)
return 1 if text else 0
class TorchNodeServer:
"""HTTP server backed by a HuggingFace causal language model shard."""
def __init__(
self,
host: str = "127.0.0.1",
port: int = 0,
model_id: str = "openai-community/gpt2",
shard_start: int = 0,
shard_end: int = 6,
quantization: str = "bfloat16",
backend: TorchModelShard | None = None,
tracker_mode: bool | None = None,
tracker_url: str | None = None,
route_timeout: float = 30.0,
cache_dir: Path | None = None,
debug: bool = False,
max_loaded_shards: int = 1,
) -> None:
self._host = host
self._requested_port = port
self._max_loaded_shards = max(1, max_loaded_shards)
self._backend = backend or _load_backend(
model_id,
shard_start,
shard_end,
quantization,
cache_dir,
)
self._backends: dict[str, TorchModelShard] = {self._backend.model_id: self._backend}
# Auto-detect tracker mode: enabled when shard_start == 0 or explicitly set
self._tracker_mode = tracker_mode if tracker_mode is not None else (shard_start == 0)
self._tracker_url = tracker_url
self._route_timeout = route_timeout
self._cache_dir = cache_dir
self._debug = debug
self._server: _TorchHTTPServer | None = None
self._thread: threading.Thread | None = None
self.port: int | None = None
@property
def route_timeout(self) -> float:
return self._route_timeout
@property
def backend(self) -> TorchModelShard:
return self._backend
@property
def received_activations(self) -> bool:
return self._server.received_activations if self._server is not None else False
@property
def forward_chunk_count(self) -> int:
return self._server.forward_chunk_count if self._server is not None else 0
@property
def total_requests(self) -> int:
return self._server.total_requests if self._server is not None else 0
@property
def failed_requests(self) -> int:
return self._server.failed_requests if self._server is not None else 0
@property
def queue_depth(self) -> int:
return self._server.queue_depth if self._server is not None else 0
@property
def loaded_model_ids(self) -> list[str]:
return list(self._backends.keys())
def apply_tracker_directives(self, directives: list[dict]) -> dict | None:
"""Apply tracker shard directives (LOAD_SHARD replace, ADD_SHARD load-more)."""
add_directive = next(
(directive for directive in reversed(directives) if directive.get("action") == "ADD_SHARD"),
None,
)
load_directive = next(
(directive for directive in reversed(directives) if directive.get("action") == "LOAD_SHARD"),
None,
)
directive = add_directive or load_directive
if directive is None:
return None
shard_start = int(directive["shard_start"])
shard_end = int(directive["shard_end"])
quantization = str(directive.get("quantization") or self._backend.quantization)
model_id = str(directive.get("model") or self._backend.model_id)
replacing = directive.get("action") == "LOAD_SHARD"
if not replacing and len(self._backends) >= self._max_loaded_shards:
print(
f" [node] WARNING: ignoring ADD_SHARD for {model_id!r}"
f"loaded {len(self._backends)}/{self._max_loaded_shards} slots full",
flush=True,
)
return None
action_label = "reassigned" if replacing else "additional"
print(
f" [node] loading {action_label} shard: {model_id} layers {shard_start}-{shard_end}",
flush=True,
)
try:
new_backend = _load_backend(model_id, shard_start, shard_end, quantization, self._cache_dir)
except TypeError:
new_backend = _load_backend(model_id, shard_start, shard_end, quantization)
self._backends[model_id] = new_backend
if replacing or shard_start == 0:
self._backend = new_backend
self._tracker_mode = shard_start == 0
print(
f" [node] loaded {action_label} shard: {model_id} layers {shard_start}-{shard_end}",
flush=True,
)
if self._server is not None:
self._server.backends = dict(self._backends)
if replacing or shard_start == 0:
self._server.backend = new_backend
self._server.tracker_mode = self._tracker_mode
return {
"action": directive.get("action"),
"model": model_id,
"shard_start": shard_start,
"shard_end": shard_end,
"quantization": quantization,
"tracker_mode": shard_start == 0,
}
def start(self) -> int:
if self._server is not None:
raise RuntimeError("TorchNodeServer is already running")
self._server = _TorchHTTPServer(
(self._host, self._requested_port),
_TorchHandler,
self._backend,
self._tracker_mode,
self._tracker_url,
self._route_timeout,
self._debug,
self._max_loaded_shards,
)
self._server.backends = dict(self._backends)
self.port = self._server.server_address[1]
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
self._thread.start()
return self.port
def stop(self) -> None:
if self._server is None:
return
self._server.shutdown()
self._server.server_close()
if self._thread is not None:
self._thread.join(timeout=1)
self._server = None
self._thread = None
self.port = None
def _load_backend(
model_id: str,
shard_start: int,
shard_end: int,
quantization: str,
cache_dir: Path | None = None,
) -> TorchModelShard:
from .model_backend import load_torch_shard
quant = validate_quantization(quantization)
try:
return load_torch_shard(model_id, shard_start, shard_end, quant, cache_dir)
except MissingModelDependencyError:
raise
except InsufficientVRAMError as exc:
print(f"ERROR: {exc}", file=sys.stderr, flush=True)
raise