from __future__ import annotations
import base64
import hashlib
import json
import logging
import time
import uuid
from dataclasses import dataclass
from typing import NamedTuple
from modelexpress import envs, p2p_pb2
from modelexpress.client import MxClient
from modelexpress.metadata.publisher import PublisherThread
from modelexpress.refit.reshard.slice_plan import Shard
from modelexpress.refit.reshard.transfer_plan import SourceInfo
logger = logging.getLogger("modelexpress.refit.reshard.rendezvous")
_SCHEMA = "mx.reshard.shard_table.v1"
def _mx_version() -> str:
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as pkg_version
try:
return pkg_version("modelexpress")
except PackageNotFoundError:
return "0.0.0"
@dataclass
class PublishedShard:
agent_name: str
device_id: int
addr: int
shard_offset: tuple
shape: tuple
digest: str | None = None
@dataclass
class PublishedTensor:
name: str
dtype: str elsize: int
full_shape: tuple
shards: list
def _encode_shard(shard) -> dict:
encoded = {
"agent_name": shard.agent_name,
"device_id": shard.device_id,
"addr": shard.addr,
"shard_offset": list(shard.shard_offset),
"shape": list(shard.shape),
}
if shard.digest is not None:
encoded["digest"] = shard.digest
return encoded
def _encode_tensor_entries(tensors: list) -> list[dict]:
return [
{
"name": tensor.name,
"dtype": tensor.dtype,
"elsize": tensor.elsize,
"full_shape": list(tensor.full_shape),
"shards": [_encode_shard(shard) for shard in tensor.shards],
}
for tensor in tensors
]
def encode_shard_table(tensors: list) -> bytes:
payload = {"schema": _SCHEMA, "tensors": _encode_tensor_entries(tensors)}
return json.dumps(payload).encode("utf-8")
def decode_shard_table(blob: bytes) -> list:
payload = json.loads(blob.decode("utf-8"))
schema = payload.get("schema")
if schema != _SCHEMA:
raise ValueError(f"unexpected shard-table schema {schema!r} (want {_SCHEMA!r})")
return decode_shard_entries(payload["tensors"])
def decode_shard_entries(entries: list) -> list:
tensors = []
for t in entries:
shards = [
PublishedShard(
agent_name=s["agent_name"],
device_id=int(s["device_id"]),
addr=int(s["addr"]),
shard_offset=tuple(s["shard_offset"]),
shape=tuple(s["shape"]),
digest=s.get("digest"),
)
for s in t["shards"]
]
tensors.append(
PublishedTensor(
name=t["name"],
dtype=t["dtype"],
elsize=int(t["elsize"]),
full_shape=tuple(t["full_shape"]),
shards=shards,
)
)
return tensors
def _torch_dtype(label: str):
import torch
return getattr(torch, label.split(".")[-1])
def build_sources(tensors: list) -> tuple:
sources = {}
session_to_agent = {}
session_to_device = {}
for t in tensors:
dtype = _torch_dtype(t.dtype)
shards = []
for s in t.shards:
session = s.agent_name
shards.append(
Shard(
shard_offset=s.shard_offset,
shape=s.shape,
session=session,
addr=s.addr,
elsize=t.elsize,
digest=s.digest,
)
)
session_to_agent[session] = s.agent_name
session_to_device[session] = s.device_id
sources[t.name] = SourceInfo(
global_shape=t.full_shape,
dtype=dtype,
elsize=t.elsize,
shards=shards,
)
return sources, session_to_agent, session_to_device
def merge_shard_tables(tables: list) -> list:
merged: dict = {}
candidates: dict = {}
for table in tables:
for t in table:
cur = merged.get(t.name)
if cur is None:
merged[t.name] = PublishedTensor(
t.name, t.dtype, t.elsize, t.full_shape, []
)
candidates[t.name] = {}
elif cur.full_shape != t.full_shape or cur.dtype != t.dtype:
raise ValueError(
f"tensor {t.name!r} published with inconsistent shape/dtype across ranks: "
f"{cur.full_shape}/{cur.dtype} vs {t.full_shape}/{t.dtype}"
)
per_geometry = candidates[t.name]
for shard in t.shards:
geometry = (tuple(shard.shard_offset), tuple(shard.shape))
per_geometry.setdefault(geometry, shard)
for name, tensor in merged.items():
tensor.shards.extend(candidates[name].values())
return list(merged.values())
def wrap_rendezvous_blob(
agent_metadata: bytes,
agent_name: str,
metadata_endpoint: str,
tensors: list,
publisher_step: int | None = None,
) -> bytes:
payload = {
"schema": _SCHEMA,
"agent_name": agent_name,
"metadata_endpoint": metadata_endpoint,
"agent_meta_b64": base64.b64encode(agent_metadata).decode("ascii"),
"tensors": _encode_tensor_entries(tensors),
}
if publisher_step is not None:
payload["publisher_step"] = int(publisher_step)
return json.dumps(payload).encode("utf-8")
def structural_manifest_digest(blob: bytes) -> str:
try:
payload = json.loads(blob.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
logger.warning(
"rendezvous manifest is not a decodable JSON blob; falling back to a "
"content-sensitive digest, which disables transfer-plan reuse"
)
return hashlib.sha256(blob).hexdigest()
if not isinstance(payload, dict):
logger.warning(
"rendezvous manifest did not decode to an object; falling back to a "
"content-sensitive digest, which disables transfer-plan reuse"
)
return hashlib.sha256(blob).hexdigest()
payload.pop("publisher_step", None)
for tensor in payload.get("tensors", ()):
for shard in tensor.get("shards", ()):
shard.pop("digest", None)
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(encoded).hexdigest()
class RendezvousPayload(NamedTuple):
agent_metadata: bytes
agent_name: str
metadata_endpoint: str
tensors: list
publisher_step: int | None = None
tensor_count: int | None = None
def entry_count(self) -> int:
return len(self.tensors) if self.tensor_count is None else self.tensor_count
def unwrap_rendezvous_blob(
blob: bytes, *, with_tensors: bool = True
) -> RendezvousPayload:
payload = json.loads(blob.decode("utf-8"))
if payload.get("schema") != _SCHEMA:
raise ValueError(f"unexpected rendezvous blob schema {payload.get('schema')!r}")
agent_metadata = base64.b64decode(payload["agent_meta_b64"])
agent_name = payload["agent_name"]
metadata_endpoint = payload.get("metadata_endpoint", "")
entries = payload["tensors"]
tensors = decode_shard_entries(entries) if with_tensors else []
raw_step = payload.get("publisher_step")
publisher_step = None if raw_step is None else int(raw_step)
return RendezvousPayload(
agent_metadata,
agent_name,
metadata_endpoint,
tensors,
publisher_step,
len(entries),
)
class MxReshardRendezvous:
def __init__(
self,
client: MxClient,
role: str,
rank: int,
model_name: str,
worker_id: str = "",
) -> None:
self.client = client
self.role = role
self.rank = rank
self.model_name = model_name
self.worker_id = worker_id or str(uuid.uuid4())
self._mx_source_id: str | None = None
self._publisher: PublisherThread | None = None
def _identity(self, role: str) -> p2p_pb2.SourceIdentity:
return p2p_pb2.SourceIdentity(
mx_version=_mx_version(),
mx_source_type=p2p_pb2.MX_SOURCE_TYPE_WEIGHTS,
model_name=self.model_name,
backend_framework=p2p_pb2.BACKEND_FRAMEWORK_VLLM,
extra_parameters={"role": role},
)
def publish(self, blob: bytes) -> str:
heartbeat_period = envs.MX_HEARTBEAT_INTERVAL_SECS
if heartbeat_period <= 0:
raise ValueError(
f"MX_HEARTBEAT_INTERVAL_SECS must be positive, got {heartbeat_period}"
)
if self._publisher is not None:
self._publisher.stop()
self._publisher = None
worker = p2p_pb2.WorkerMetadata(
worker_rank=self.rank,
nixl_metadata=blob,
status=p2p_pb2.SOURCE_STATUS_READY,
)
self._mx_source_id = self.client.publish_metadata(
self._identity(self.role), worker, self.worker_id
)
self._publisher = PublisherThread(
mx_client=self.client,
mx_source_id=self._mx_source_id,
worker_id=self.worker_id,
worker_rank=self.rank,
interval_secs=heartbeat_period,
)
self._publisher.start()
return self._mx_source_id
def close(self) -> None:
if self._publisher is not None:
self._publisher.stop()
self._publisher = None
self._mx_source_id = None
def _fetch_metadata(self, instances: list) -> list:
return [self._get_metadata_or_none(inst) for inst in instances]
def _get_metadata_or_none(self, inst):
try:
return self.client.get_metadata(inst.mx_source_id, inst.worker_id)
except Exception as exc: logger.warning(
"[reshard] get_metadata failed for source %s worker %s: %s",
inst.mx_source_id,
inst.worker_id,
exc,
)
return None
def discover_trainers(
self,
expected_trainers: int,
timeout: float = 1200.0,
poll_interval: float = 1.0,
with_tensors: bool = True,
) -> list:
trainer_id = self._identity("trainer")
deadline = time.monotonic() + timeout
fetch_s = 0.0
parse_s = 0.0
polls = 0
while True:
list_t0 = time.perf_counter()
resp = self.client.list_sources(
trainer_id,
status_filter=p2p_pb2.SOURCE_STATUS_READY,
)
instances = list(resp.instances)
fetch_s += time.perf_counter() - list_t0
payloads, empty = [], 0
fetch_t0 = time.perf_counter()
metas = self._fetch_metadata(instances)
fetch_s += time.perf_counter() - fetch_t0
for meta in metas:
if meta is None or not meta.found:
continue
parse_t0 = time.perf_counter()
payload = unwrap_rendezvous_blob(
meta.worker.nixl_metadata, with_tensors=with_tensors
)
parse_s += time.perf_counter() - parse_t0
if payload.entry_count() == 0:
empty += 1
continue
payloads.append(payload)
if len(payloads) >= expected_trainers:
break
polls += 1
if len(payloads) >= expected_trainers:
break
if time.monotonic() >= deadline:
raise TimeoutError(
f"timed out after {timeout}s waiting for {expected_trainers} "
f"trainer ranks (saw {len(instances)} READY source(s), "
f"{len(payloads)} with a non-empty shard table, {empty} empty)"
)
time.sleep(poll_interval)
logger.warning(
"MX_DISCOVER_COST %s",
json.dumps(
{
"schema": "mx-discover-cost-v1",
"rank": self.rank,
"sources": len(payloads),
"tensors": sum(p.entry_count() for p in payloads),
"tables_built": with_tensors,
"grpc_fetch_s": round(fetch_s, 6),
"blob_parse_s": round(parse_s, 6),
"polls": polls,
}
),
)
logger.info(
"[reshard] discovered %d trainer rank(s)%s: %s",
len(payloads),
f" ({empty} skipped as empty)" if empty else "",
", ".join(
f"{p.agent_name}@{p.metadata_endpoint}[{len(p.tensors)}]"
for p in payloads
),
)
return payloads
def gather_sources(
client: MxClient,
expected_trainers: int,
model_name: str,
role: str = "inference",
rank: int = 0,
timeout: float = 1200.0,
) -> tuple:
rdv = MxReshardRendezvous(client, role=role, rank=rank, model_name=model_name)
payloads = rdv.discover_trainers(expected_trainers, timeout=timeout)
tables = [p.tensors for p in payloads]
agent_endpoints = {p.agent_name: p.metadata_endpoint for p in payloads}
merged = merge_shard_tables(tables)
sources, session_to_agent, session_to_device = build_sources(merged)
return sources, session_to_agent, session_to_device, agent_endpoints