Update protocol version and improve connection handling

Updated protocol version and added error handling for packet size limits. Enhanced connection handling with new timeout and retry parameters.
This commit is contained in:
nyueki
2026-10-25 00:50:01 -04:00
committed by GitHub
parent d6b16fbb1a
commit c71fc4803d
+329 -326
View File
@@ -1,4 +1,5 @@
import os
import math
import json
import time
import hmac
@@ -11,7 +12,7 @@ import folder_paths
import comfy.sd
import comfy.utils
DEFAULT_PORT = 8181
PROTOCOL_VERSION = 2
PROTOCOL_VERSION = 3
HEADER_LEN_FMT = ">Q"
HEADER_LEN_SIZE = struct.calcsize(HEADER_LEN_FMT)
SOCKET_TIMEOUT = 120
@@ -22,7 +23,6 @@ RECV_CHUNK = 1 << 20
MAX_HEADER_BYTES = 8 * 1024 * 1024
MAX_BLOB_BYTES = 512 * 1024 * 1024
PATCHED_CLIP_CACHE = 4
EMBED_CACHE = 64
ALLOWED_DTYPES = {
"torch.float32": torch.float32,
"torch.float16": torch.float16,
@@ -39,7 +39,7 @@ def resolve_transport_dtype(mode, ip):
if mode == "fp16":
return "torch.float16"
if mode == "fp32":
return None
return "torch.float32"
is_local = ip in _LOCAL_HOSTS
return None if is_local else "torch.float16"
def _recv_exact(sock, size):
@@ -54,6 +54,8 @@ def _recv_exact(sock, size):
return b"".join(chunks)
def send_packet(sock, header, blob=b""):
header_bytes = json.dumps(header).encode("utf-8")
if len(header_bytes) > MAX_HEADER_BYTES or len(blob) > MAX_BLOB_BYTES:
raise ValueError("Packet exceeds protocol size limits")
prefix = struct.pack(HEADER_LEN_FMT, len(header_bytes))
sock.sendall(prefix + header_bytes)
if blob:
@@ -62,10 +64,13 @@ def recv_header(sock):
header_len = struct.unpack(HEADER_LEN_FMT, _recv_exact(sock, HEADER_LEN_SIZE))[0]
if header_len > MAX_HEADER_BYTES:
raise ValueError(f"Header too large: {header_len} bytes")
return json.loads(_recv_exact(sock, header_len).decode("utf-8"))
header = json.loads(_recv_exact(sock, header_len).decode("utf-8"))
if not isinstance(header, dict):
raise ValueError("Packet header must be an object")
return header
def recv_blob(sock, header):
blob_size = header.get("blob_size", 0)
if blob_size > MAX_BLOB_BYTES:
if type(blob_size) is not int or not 0 <= blob_size <= MAX_BLOB_BYTES:
raise ValueError(f"Blob too large: {blob_size} bytes")
return _recv_exact(sock, blob_size) if blob_size else b""
def recv_packet(sock):
@@ -109,57 +114,95 @@ def unpack_tensors(meta, blob):
if dtype_name not in ALLOWED_DTYPES:
raise ValueError(f"Refusing to decode disallowed dtype: {dtype_name}")
dtype = ALLOWED_DTYPES[dtype_name]
start = info["offset"]
raw = bytearray(view[start:start + info["size"]])
t = torch.frombuffer(raw, dtype=dtype).reshape(info["shape"]).clone()
start, size, shape = info["offset"], info["size"], info["shape"]
if (type(start) is not int or type(size) is not int or
start < 0 or size < 0 or start + size > len(blob)):
raise ValueError("Invalid tensor byte range")
if not isinstance(shape, list) or any(type(d) is not int or d < 0 for d in shape):
raise ValueError("Invalid tensor shape")
count = math.prod(shape)
if count * torch.empty((), dtype=dtype).element_size() != size:
raise ValueError("Tensor shape does not match byte size")
if count == 0:
t = torch.empty(shape, dtype=dtype)
else:
raw = bytearray(view[start:start + size])
t = torch.frombuffer(raw, dtype=dtype).reshape(shape).clone()
orig = info.get("orig_dtype")
if orig in ALLOWED_DTYPES and ALLOWED_DTYPES[orig] != dtype:
t = t.to(ALLOWED_DTYPES[orig])
out[name] = t
return out
def extract_tensors(obj, prefix, out):
# Tag every container so user dictionaries cannot mimic tensor references.
if isinstance(obj, torch.Tensor):
out[prefix] = obj
return {"__tensor__": prefix}
key = f"tensor_{len(out)}"
out[key] = obj
return {"kind": "tensor", "value": key}
if isinstance(obj, dict):
return {k: extract_tensors(v, f"{prefix}.{k}", out) for k, v in obj.items()}
return {"kind": "dict", "value": [
[extract_tensors(k, prefix, out), extract_tensors(v, prefix, out)]
for k, v in obj.items()]}
if isinstance(obj, (list, tuple)):
return [extract_tensors(v, f"{prefix}.{i}", out) for i, v in enumerate(obj)]
return obj
return {"kind": "tuple" if isinstance(obj, tuple) else "list",
"value": [extract_tensors(v, prefix, out) for v in obj]}
return {"kind": "value", "value": obj}
def restore_tensors(obj, tensors):
if isinstance(obj, dict):
if len(obj) == 1 and "__tensor__" in obj:
return tensors[obj["__tensor__"]]
return {k: restore_tensors(v, tensors) for k, v in obj.items()}
if isinstance(obj, list):
return [restore_tensors(v, tensors) for v in obj]
return obj
kind, value = obj["kind"], obj["value"]
if kind == "tensor":
return tensors[value]
if kind == "dict":
return {restore_tensors(k, tensors): restore_tensors(v, tensors) for k, v in value}
if kind in ("list", "tuple"):
values = [restore_tensors(v, tensors) for v in value]
return tuple(values) if kind == "tuple" else values
if kind != "value":
raise ValueError(f"Invalid structure kind: {kind}")
return value
def pack_object(obj, transport_dtype=None):
tensors = {}
structure = extract_tensors(obj, "", tensors)
meta, blob = pack_tensors(tensors, transport_dtype)
return {"struct": structure, "tensors": meta, "blob_size": len(blob)}, blob
def unpack_object(header, blob):
return restore_tensors(header["struct"], unpack_tensors(header["tensors"], blob))
class _Connection:
def __init__(self, ip, port, auth_token=""):
self.ip = ip
self.port = port
self.auth_token = auth_token
def __init__(self, ip, port, auth_token="", connect_timeout=SOCKET_TIMEOUT,
request_attempts=CONNECT_RETRIES, connect_backoff=CONNECT_BACKOFF,
encode_timeout=SOCKET_TIMEOUT):
self.ip, self.port, self.auth_token = ip, port, auth_token
self.connect_timeout = float(connect_timeout)
self.request_attempts = int(request_attempts)
self.connect_backoff = float(connect_backoff)
self.encode_timeout = float(encode_timeout)
for value in (self.connect_timeout, self.encode_timeout):
if not math.isfinite(value) or value <= 0:
raise ValueError("Timeouts must be finite and positive")
if self.request_attempts < 1:
raise ValueError("Request attempts must be at least 1")
if not math.isfinite(self.connect_backoff) or self.connect_backoff < 0:
raise ValueError("Retry delay must be finite and nonnegative")
self._sock = None
self._lock = threading.Lock()
def _connect(self):
last_err = None
backoff = CONNECT_BACKOFF
for attempt in range(1, CONNECT_RETRIES + 1):
try:
sock = socket.create_connection((self.ip, self.port), timeout=SOCKET_TIMEOUT)
_set_socket_opts(sock)
self._sock = sock
log(f"Connected to worker {self.ip}:{self.port}")
return
except OSError as e:
last_err = e
log(f"Connect attempt {attempt}/{CONNECT_RETRIES} failed: {e}")
if attempt < CONNECT_RETRIES:
time.sleep(backoff)
backoff *= 2
raise ConnectionError(
f"Could not connect to worker {self.ip}:{self.port}: {last_err}"
)
sock = socket.create_connection((self.ip, self.port), timeout=self.connect_timeout)
try:
_set_socket_opts(sock)
except Exception:
sock.close()
raise
self._sock = sock
log(f"Connected to worker {self.ip}:{self.port}")
def _close(self):
if self._sock is not None:
try:
@@ -167,351 +210,311 @@ class _Connection:
except OSError:
pass
self._sock = None
def request(self, header, blob=b"", response_timeout=None, retries=2):
if self.auth_token:
header = {**header, "auth": self.auth_token}
def request(self, header, blob=b"", response_timeout=None, replay_safe=True):
header = {**header, "auth": self.auth_token}
timeout = self.encode_timeout if response_timeout is None else response_timeout
if not math.isfinite(timeout) or timeout <= 0:
raise ValueError("Response timeout must be finite and positive")
with self._lock:
for attempt in range(1, retries + 1):
backoff = self.connect_backoff
for attempt in range(1, self.request_attempts + 1):
sending = False
try:
if self._sock is None:
self._connect()
if response_timeout is not None:
self._sock.settimeout(response_timeout)
self._sock.settimeout(timeout)
sending = True
send_packet(self._sock, header, blob)
resp = recv_packet(self._sock)
if response_timeout is not None:
self._sock.settimeout(SOCKET_TIMEOUT)
return resp
except (ConnectionError, OSError, struct.error, ValueError) as e:
log(f"Request failed ({e}); reconnecting (attempt {attempt}/{retries})")
result = recv_packet(self._sock)
self._sock.settimeout(self.encode_timeout)
return result
except (OSError, ConnectionError, struct.error, ValueError):
self._close()
if attempt == retries:
# Generation may have executed even when its response was lost.
if attempt == self.request_attempts or (sending and not replay_safe):
raise
log(f"Request attempt {attempt}/{self.request_attempts} failed; retrying")
time.sleep(backoff)
backoff *= 2
class RemoteCLIPProxy:
def __init__(self, ip, port, auth_token="", transport_mode="auto",
connection=None, lora_stack=None):
self.ip = ip
self.port = port
connection=None, lora_stack=None, connect_timeout=SOCKET_TIMEOUT,
request_attempts=CONNECT_RETRIES, connect_backoff=CONNECT_BACKOFF,
encode_timeout=SOCKET_TIMEOUT, generate_timeout=GENERATE_TIMEOUT,
layer_idx=None):
self.ip, self.port = ip, port
self.transport_mode = transport_mode
self.transport_dtype_name = resolve_transport_dtype(transport_mode, ip)
self.transport_dtype = (
ALLOWED_DTYPES[self.transport_dtype_name]
if self.transport_dtype_name is not None
else None
)
self._conn = connection or _Connection(ip, port, auth_token)
self.transport_dtype = ALLOWED_DTYPES.get(self.transport_dtype_name)
self.generate_timeout = float(generate_timeout)
if not math.isfinite(self.generate_timeout) or self.generate_timeout <= 0:
raise ValueError("Generation timeout must be finite and positive")
self._conn = connection or _Connection(
ip, port, auth_token, connect_timeout, request_attempts,
connect_backoff, encode_timeout)
self.lora_stack = list(lora_stack or [])
self.layer_idx = layer_idx
def clone(self, disable_dynamic=False):
return RemoteCLIPProxy(
self.ip, self.port,
transport_mode=self.transport_mode,
connection=self._conn,
lora_stack=self.lora_stack,
)
self.ip, self.port, transport_mode=self.transport_mode,
connection=self._conn, lora_stack=self.lora_stack,
generate_timeout=self.generate_timeout, layer_idx=self.layer_idx)
def clip_layer(self, layer_idx):
self.layer_idx = layer_idx
def with_lora(self, lora_name, strength_clip):
c = self.clone()
c.lora_stack = self.lora_stack + [[lora_name, float(strength_clip)]]
return c
def add_patches(self, *_args, **_kwargs):
raise NotImplementedError(
"Apply LoRAs to a remote CLIP with the 'LoraLoaderCLIPOnly' node, "
"which forwards them to the worker."
)
"Apply LoRAs to a remote CLIP with the 'LoraLoaderCLIPOnly' node.")
def _rpc(self, cmd, payload, generation=False):
# Token tensors retain their original numerical values. Transport precision
# applies to image/tokenizer inputs and embedding responses, not token IDs.
header, blob = pack_object(payload)
header.update(cmd=cmd, proto=PROTOCOL_VERSION,
lora_stack=self.lora_stack, layer_idx=self.layer_idx,
transport_dtype=self.transport_dtype_name)
response, data = self._conn.request(
header, blob,
response_timeout=self.generate_timeout if generation else None,
replay_safe=not generation)
if response.get("error"):
raise RuntimeError(f"Remote CLIP worker error: {response['error']}")
return unpack_object(response, data)
def tokenize(self, text, return_word_ids=False, **kwargs):
if return_word_ids:
kwargs["return_word_ids"] = True
return {"text": text, "kwargs": kwargs, "lora_stack": self.lora_stack}
def _encode(self, tokens):
kwargs = dict(tokens.get("kwargs", {}))
tensor_inputs = {}
kwargs = extract_tensors(kwargs, "kwargs", tensor_inputs)
if tensor_inputs:
in_meta, in_blob = pack_tensors(tensor_inputs, self.transport_dtype)
else:
in_meta, in_blob = {}, b""
header = {
"cmd": "encode",
"proto": PROTOCOL_VERSION,
"text": tokens["text"],
"kwargs": kwargs,
"lora_stack": tokens.get("lora_stack", self.lora_stack),
"tensor_inputs": in_meta,
"transport_dtype": self.transport_dtype_name,
"blob_size": len(in_blob),
}
log("Sending encode request")
resp, blob = self._conn.request(header, in_blob)
if resp.get("error"):
raise RuntimeError(f"Remote CLIP worker error: {resp['error']}")
tensors = unpack_tensors(resp["tensors"], blob)
out = restore_tensors(resp["struct"], tensors)
log("Received embeddings")
return out
# Return actual token dictionaries: callers such as SDXL edit l/g streams.
return self._rpc("tokenize", {"text": text, "kwargs": {
**kwargs, "return_word_ids": return_word_ids}})
def encode_from_tokens(self, tokens, return_pooled=False, return_dict=False):
out = self._encode(tokens)
out = self._rpc("encode", {"tokens": tokens, "return_pooled": return_pooled})
if return_dict:
return out
cond = out["cond"]
if return_pooled:
return cond, out.get("pooled_output")
return cond
def encode_from_tokens_scheduled(self, tokens, unprojected=False, add_dict: dict = None, show_pbar=True):
return_pooled = "unprojected" if unprojected else True
pooled_dict = self.encode_from_tokens(tokens, return_pooled=return_pooled, return_dict=True)
cond = pooled_dict.pop("cond")
if add_dict:
pooled_dict.update(add_dict)
return [[cond, pooled_dict]]
return out["cond"], out.get("pooled_output")
return out["cond"]
def encode_from_tokens_scheduled(self, tokens, unprojected=False, add_dict=None, show_pbar=True):
return self._rpc("encode_scheduled", {
"tokens": tokens, "unprojected": unprojected,
"add_dict": {} if add_dict is None else add_dict, "show_pbar": show_pbar})
def generate(self, tokens, **gen_kwargs):
text = tokens["text"]
kwargs = dict(tokens.get("kwargs", {}))
lora_stack = tokens.get("lora_stack", self.lora_stack)
tensor_inputs = {}
kwargs = extract_tensors(kwargs, "kwargs", tensor_inputs)
if tensor_inputs:
meta, blob = pack_tensors(tensor_inputs, self.transport_dtype)
else:
meta, blob = {}, b""
header = {
"cmd": "generate",
"proto": PROTOCOL_VERSION,
"text": text,
"kwargs": kwargs,
"gen_kwargs": gen_kwargs,
"lora_stack": lora_stack,
"tensor_inputs": meta,
"transport_dtype": self.transport_dtype_name,
"blob_size": len(blob),
}
log("Sending generate request")
resp, _ = self._conn.request(
header, blob, response_timeout=GENERATE_TIMEOUT, retries=1
)
if resp.get("error"):
raise RuntimeError(f"Remote CLIP worker error: {resp['error']}")
log("Received generated text")
return resp["text"]
def decode(self, generated):
return self._rpc("generate", {"tokens": tokens, "gen_kwargs": gen_kwargs}, generation=True)
def decode(self, generated, skip_special_tokens=True):
return generated
class _Worker:
def __init__(self, clip, auth_token=""):
self.base_clip = clip
self.auth_token = auth_token
self._cache_lock = threading.Lock()
self._infer_lock = threading.Lock()
self._patched_clips = OrderedDict()
self._embed_cache = OrderedDict()
@staticmethod
def _sig(obj):
return json.dumps(obj, sort_keys=True)
def _get_clip(self, lora_stack):
if not lora_stack:
return self.base_clip
sig = self._sig(lora_stack)
clip = self._patched_clips.get(sig)
if clip is not None:
self._infer_lock = threading.Lock()
self._patched_clips = OrderedDict()
def _get_clip(self, lora_stack, layer_idx):
sig = json.dumps([lora_stack, layer_idx], sort_keys=True)
if sig in self._patched_clips:
self._patched_clips.move_to_end(sig)
return clip
clip = self.base_clip
return self._patched_clips[sig]
clip = self.base_clip.clone()
if layer_idx is not None:
clip.clip_layer(layer_idx)
for lora_name, strength in lora_stack:
if strength == 0:
continue
lora_path = folder_paths.get_full_path("loras", lora_name)
if lora_path is None:
path = folder_paths.get_full_path("loras", lora_name)
if path is None:
raise FileNotFoundError(f"LoRA not found on worker: {lora_name}")
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
lora = comfy.utils.load_torch_file(path, safe_load=True)
_, clip = comfy.sd.load_lora_for_models(None, clip, lora, 0, strength)
self._patched_clips[sig] = clip
while len(self._patched_clips) > PATCHED_CLIP_CACHE:
self._patched_clips.popitem(last=False)
return clip
def encode(self, text, kwargs, lora_stack):
def execute(self, cmd, payload, lora_stack, layer_idx):
with self._infer_lock, torch.inference_mode():
clip = self._get_clip(lora_stack, layer_idx)
if cmd == "tokenize":
return clip.tokenize(payload["text"], **payload["kwargs"])
if cmd == "generate":
return clip.decode(clip.generate(payload["tokens"], **payload["gen_kwargs"]))
if cmd == "encode":
out = clip.encode_from_tokens(payload["tokens"],
return_pooled=payload["return_pooled"], return_dict=True)
elif cmd == "encode_scheduled":
out = clip.encode_from_tokens_scheduled(**payload)
else:
raise ValueError(f"Unknown command: {cmd}")
return out
class _Server:
def __init__(self, clip, host, port, token):
self.worker = _Worker(clip, token)
self.token = token
self.stopped = threading.Event()
self._clients = set()
self._clients_lock = threading.Lock()
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
cache_key = self._sig([text, kwargs, lora_stack])
except TypeError:
cache_key = None
if cache_key is not None:
with self._cache_lock:
cached = self._embed_cache.get(cache_key)
if cached is not None:
self._embed_cache.move_to_end(cache_key)
return cached
with self._infer_lock:
if cache_key is not None:
with self._cache_lock:
cached = self._embed_cache.get(cache_key)
if cached is not None:
self._embed_cache.move_to_end(cache_key)
return cached
clip = self._get_clip(lora_stack)
with torch.inference_mode():
tokens = clip.tokenize(text, **kwargs)
out = clip.encode_from_tokens(tokens, return_dict=True)
cond = out.get("cond")
if cond is None:
raise RuntimeError("encode produced no cond tensor")
result = {}
for k, v in out.items():
result[k] = v.detach().cpu().clone() if isinstance(v, torch.Tensor) else v
if result.get("pooled_output") is None:
result["pooled_output"] = torch.zeros((cond.shape[0], cond.shape[-1]))
if cache_key is not None:
with self._cache_lock:
self._embed_cache[cache_key] = result
while len(self._embed_cache) > EMBED_CACHE:
self._embed_cache.popitem(last=False)
return result
def generate(self, text, kwargs, gen_kwargs, lora_stack):
with self._infer_lock:
clip = self._get_clip(lora_stack)
with torch.inference_mode():
tokens = clip.tokenize(text, **kwargs)
generated_ids = clip.generate(tokens, **gen_kwargs)
generated_text = clip.decode(generated_ids)
return generated_text
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind((host, port))
self.socket.listen(16)
self.socket.settimeout(0.5)
except Exception:
self.socket.close()
raise
self.thread = threading.Thread(target=self._accept_loop, daemon=True)
self.thread.start()
@staticmethod
def _close_socket(sock):
try:
sock.shutdown(socket.SHUT_RDWR)
except OSError:
pass
sock.close()
def close(self):
self.stopped.set()
self._close_socket(self.socket)
with self._clients_lock:
clients = list(self._clients)
for conn in clients:
self._close_socket(conn)
self.thread.join(timeout=1)
def _accept_loop(self):
while not self.stopped.is_set():
try:
conn, addr = self.socket.accept()
except socket.timeout:
continue
except OSError:
break
with self._clients_lock:
if self.stopped.is_set():
conn.close()
break
self._clients.add(conn)
threading.Thread(target=self._handle, args=(conn, addr), daemon=True).start()
def _handle(self, conn, addr):
try:
_set_socket_opts(conn)
while not self.stopped.is_set():
header = recv_header(conn)
auth = header.get("auth", "")
if self.token and (not isinstance(auth, str) or not hmac.compare_digest(
auth.encode("utf-8"), self.token.encode("utf-8"))):
send_packet(conn, {"error": "unauthorized", "blob_size": 0})
break
if header.get("proto") != PROTOCOL_VERSION:
send_packet(conn, {"error": f"Protocol mismatch: install v{PROTOCOL_VERSION} on both ends", "blob_size": 0})
break
blob = recv_blob(conn, header)
try:
payload = unpack_object(header, blob)
if self.stopped.is_set():
break
out = self.worker.execute(header["cmd"], payload,
header.get("lora_stack", []), header.get("layer_idx"))
if self.stopped.is_set():
break
transport = header.get("transport_dtype")
if transport not in (None, "torch.float16", "torch.float32"):
raise ValueError("Invalid transport precision")
dtype = ALLOWED_DTYPES.get(transport) if header["cmd"] in ("encode", "encode_scheduled") else None
response, data = pack_object(out, dtype)
send_packet(conn, response, data)
except Exception as e:
if self.stopped.is_set():
break
log(f"Worker request failed: {e}")
send_packet(conn, {"error": str(e), "blob_size": 0})
except (ConnectionError, OSError, ValueError, TypeError, KeyError, struct.error) as e:
log(f"Client {addr} disconnected: {e}")
finally:
with self._clients_lock:
self._clients.discard(conn)
self._close_socket(conn)
class SendRemoteCLIP:
_servers = {}
_servers = {}
_servers_lock = threading.Lock()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"CLIP": ("CLIP",),
"listen_port": ("INT", {"default": DEFAULT_PORT, "min": 1, "max": 65535}),
},
"optional": {
"bind_host": ("STRING", {"default": "0.0.0.0"}),
"auth_token": ("STRING", {"default": ""}),
},
}
return {"required": {
"CLIP": ("CLIP",),
"listen_port": ("INT", {"default": DEFAULT_PORT, "min": 1, "max": 65535}),
}, "optional": {
"bind_host": ("STRING", {"default": "0.0.0.0"}),
"auth_token": ("STRING", {"default": ""}),
}}
RETURN_TYPES = ()
FUNCTION = "start_worker"
OUTPUT_NODE = True
CATEGORY = "Remote CLIP"
def start_worker(self, CLIP, listen_port, bind_host="0.0.0.0", auth_token=""):
token = auth_token or os.environ.get("REMOTE_CLIP_TOKEN", "")
existing = SendRemoteCLIP._servers.pop(listen_port, None)
if existing is not None:
try:
existing.close()
except OSError:
pass
worker = _Worker(CLIP, token)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((bind_host, listen_port))
server.listen(16)
SendRemoteCLIP._servers[listen_port] = server
with self._servers_lock:
previous = self._servers.pop(listen_port, None)
if previous is not None:
previous.close()
self._servers[listen_port] = _Server(CLIP, bind_host, listen_port, token)
if bind_host == "0.0.0.0" and not token:
log("WARNING: worker bound to 0.0.0.0 with no auth token. Anyone on "
"the network can use this CLIP. Set auth_token or bind to a "
"specific host.")
log("WARNING: worker is accessible on all interfaces without authentication")
log(f"Worker listening on {bind_host}:{listen_port}")
def handle(conn, addr):
log(f"Client connected: {addr}")
try:
_set_socket_opts(conn)
while True:
header = recv_header(conn)
if token and not hmac.compare_digest(header.get("auth", ""), token):
send_packet(conn, {"error": "unauthorized", "blob_size": 0})
log(f"Rejected unauthorized client {addr}")
break
client_proto = header.get("proto", 0)
if client_proto != PROTOCOL_VERSION:
send_packet(conn, {
"error": (f"protocol mismatch: worker speaks v{PROTOCOL_VERSION}, "
f"client sent v{client_proto}"),
"blob_size": 0,
})
log(f"Rejected client {addr}: protocol v{client_proto} "
f"!= v{PROTOCOL_VERSION}")
break
blob_data = recv_blob(conn, header)
cmd = header.get("cmd")
if cmd == "encode" and "text" in header:
try:
transport = header.get("transport_dtype")
transport_dtype = ALLOWED_DTYPES.get(transport) if transport else None
input_tensors = unpack_tensors(
header.get("tensor_inputs", {}), blob_data
)
enc_kwargs = restore_tensors(
header.get("kwargs", {}), input_tensors
)
out = worker.encode(
header["text"],
enc_kwargs,
header.get("lora_stack", []),
)
tensors = {}
out_struct = extract_tensors(out, "out", tensors)
meta, blob = pack_tensors(tensors, transport_dtype)
send_packet(
conn,
{"struct": out_struct, "tensors": meta, "blob_size": len(blob)},
blob,
)
log(f"Sent embeddings ({len(blob)} bytes)")
except Exception as e:
log(f"Encode failed: {e}")
send_packet(conn, {"error": str(e), "blob_size": 0})
elif cmd == "generate" and "text" in header:
try:
input_tensors = unpack_tensors(
header.get("tensor_inputs", {}), blob_data
)
kwargs = restore_tensors(
header.get("kwargs", {}), input_tensors
)
generated_text = worker.generate(
header["text"],
kwargs,
header.get("gen_kwargs", {}),
header.get("lora_stack", []),
)
send_packet(conn, {"text": generated_text, "blob_size": 0})
log("Sent generated text")
except Exception as e:
log(f"Generate failed: {e}")
send_packet(conn, {"error": str(e), "blob_size": 0})
else:
send_packet(conn, {"error": "bad request", "blob_size": 0})
except (ConnectionError, OSError) as e:
log(f"Client {addr} disconnected: {e}")
finally:
try:
conn.close()
except OSError:
pass
def accept_loop():
while True:
try:
conn, addr = server.accept()
except OSError:
log("Server socket closed; stopping accept loop")
break
threading.Thread(target=handle, args=(conn, addr), daemon=True).start()
threading.Thread(target=accept_loop, daemon=True).start()
return {"ui": {"text": [f"Remote CLIP worker on {bind_host}:{listen_port}"]}}
class LoadRemoteCLIP:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"worker_ip": ("STRING", {"default": "127.0.0.1"}),
"port": ("INT", {"default": DEFAULT_PORT, "min": 1, "max": 65535}),
},
"optional": {
"auth_token": ("STRING", {"default": ""}),
"transport_precision": (["auto", "fp16", "fp32"], {"default": "auto"}),
},
}
return {"required": {
"worker_ip": ("STRING", {"default": "127.0.0.1"}),
"port": ("INT", {"default": DEFAULT_PORT, "min": 1, "max": 65535}),
}, "optional": {
"auth_token": ("STRING", {"default": ""}),
"transport_precision": (["auto", "fp16", "fp32"], {"default": "auto"}),
"connect_timeout": ("FLOAT", {"default": SOCKET_TIMEOUT, "min": 0.1, "max": 3600.0, "step": 0.1, "tooltip": "Seconds per connection attempt."}),
"request_attempts": ("INT", {"default": CONNECT_RETRIES, "min": 1, "max": 10, "tooltip": "Total attempts per operation, including the first. Connection failures and safe request retries share this single limit. Generation is never replayed after sending starts."}),
"connect_backoff": ("FLOAT", {"default": CONNECT_BACKOFF, "min": 0.0, "max": 60.0, "step": 0.1, "tooltip": "Initial retry delay in seconds; doubles after each failure."}),
"encode_timeout": ("FLOAT", {"default": SOCKET_TIMEOUT, "min": 0.1, "max": 86400.0, "step": 0.1, "tooltip": "Socket timeout for tokenization and encoding; not a total operation deadline."}),
"generate_timeout": ("FLOAT", {"default": GENERATE_TIMEOUT, "min": 0.1, "max": 86400.0, "step": 0.1, "tooltip": "Generation socket timeout; not a total operation deadline."}),
}}
RETURN_TYPES = ("CLIP",)
FUNCTION = "load_remote"
CATEGORY = "Remote CLIP"
def load_remote(self, worker_ip, port, auth_token="", transport_precision="auto"):
def load_remote(self, worker_ip, port, auth_token="", transport_precision="auto",
connect_timeout=SOCKET_TIMEOUT, request_attempts=CONNECT_RETRIES,
connect_backoff=CONNECT_BACKOFF, encode_timeout=SOCKET_TIMEOUT,
generate_timeout=GENERATE_TIMEOUT):
token = auth_token or os.environ.get("REMOTE_CLIP_TOKEN", "")
return (RemoteCLIPProxy(worker_ip, port, token, transport_precision),)
return (RemoteCLIPProxy(worker_ip, port, token, transport_precision,
connect_timeout=connect_timeout, request_attempts=request_attempts,
connect_backoff=connect_backoff, encode_timeout=encode_timeout,
generate_timeout=generate_timeout),)
class LoraLoaderCLIPOnly:
@classmethod
def INPUT_TYPES(s):