[core/loader] Add presharded load format (#24256)
Co-authored-by: Shu Wang <shuwanguc@google.com>
This commit is contained in:
@@ -87,6 +87,7 @@ dependencies = [
|
||||
"uvloop",
|
||||
"watchfiles",
|
||||
"xgrammar==0.2.1",
|
||||
"xxhash",
|
||||
"zstandard",
|
||||
]
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ dependencies = [
|
||||
"triton==3.7.0",
|
||||
"uvicorn",
|
||||
"uvloop",
|
||||
"xxhash",
|
||||
"xgrammar==0.2.1",
|
||||
"zstandard",
|
||||
]
|
||||
|
||||
@@ -65,6 +65,7 @@ dependencies = [
|
||||
"transformers==5.12.1",
|
||||
"uvicorn",
|
||||
"uvloop",
|
||||
"xxhash",
|
||||
"xgrammar==0.2.1",
|
||||
]
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ runtime_common = [
|
||||
"transformers==5.12.1",
|
||||
"uvicorn",
|
||||
"uvloop",
|
||||
"xxhash",
|
||||
"xgrammar==0.2.1",
|
||||
]
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ dependencies = [
|
||||
"tqdm",
|
||||
"transformers==5.12.1",
|
||||
"uvicorn",
|
||||
"xxhash",
|
||||
"uvloop",
|
||||
# "xgrammar==0.2.1", xgrammar depends on CUDA PyTorch and Triton only
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@ class LoadFormat(str, enum.Enum):
|
||||
NPCACHE = "npcache"
|
||||
DUMMY = "dummy"
|
||||
SHARDED_STATE = "sharded_state"
|
||||
PRESHARDED = "presharded"
|
||||
GGUF = "gguf"
|
||||
BITSANDBYTES = "bitsandbytes"
|
||||
MISTRAL = "mistral"
|
||||
|
||||
@@ -6,15 +6,18 @@ from __future__ import annotations
|
||||
|
||||
# ruff: noqa: SIM117
|
||||
import collections
|
||||
import concurrent.futures
|
||||
import dataclasses
|
||||
import fnmatch
|
||||
import gc
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -1497,6 +1500,15 @@ class ShardedStateLoader(BaseModelLoader):
|
||||
result: Dict[str, torch.Tensor] = {}
|
||||
for group in same_storage_groups.values():
|
||||
for k, t in group:
|
||||
if not t.is_contiguous():
|
||||
# End-pointer dedup assumes a flat view; non-contiguous
|
||||
# tensors (e.g. produced by
|
||||
# ``.transpose(...).contiguous().transpose(...)`` in some
|
||||
# quant ``post_load_weights`` paths) cannot be flattened
|
||||
# via ``view(-1)``. Include them directly; downstream
|
||||
# writers call ``.contiguous()`` before save.
|
||||
result[k] = t
|
||||
continue
|
||||
a, b = t.data_ptr(), get_end_ptr(t)
|
||||
for k2, t2 in group:
|
||||
if not t2.is_contiguous():
|
||||
@@ -1629,6 +1641,839 @@ class ShardedStateLoader(BaseModelLoader):
|
||||
)
|
||||
|
||||
|
||||
class PreshardedModelLoader(DefaultModelLoader):
|
||||
"""Dump/reload post-process weights under ``<model_path>/presharded/<subdir>/``.
|
||||
|
||||
Optional roots in ``model_loader_extra_config`` (subdir still appended):
|
||||
``presharded_path`` (target), ``draft_presharded_path`` (speculative draft).
|
||||
Dump dir must be shared across ranks/nodes.
|
||||
"""
|
||||
|
||||
DEFAULT_SUBDIR = "presharded"
|
||||
MAX_FILE_BYTES = 20 * (1024**3)
|
||||
CHECKSUM_FILENAME = "checksum.json"
|
||||
READY_FILENAME = "READY"
|
||||
TMP_SUBDIR = "_tmp_presharding"
|
||||
PLAN_VERSION = 1
|
||||
DEFAULT_HASH_NUM_THREADS = 8
|
||||
_CONTENT_HASH_HEX_LEN = 32
|
||||
|
||||
def __init__(self, load_config: LoadConfig):
|
||||
extra = (
|
||||
{}
|
||||
if load_config.model_loader_extra_config is None
|
||||
else dict(load_config.model_loader_extra_config)
|
||||
)
|
||||
self._presharded_path_override = extra.pop("presharded_path", None)
|
||||
self._draft_presharded_path_override = extra.pop("draft_presharded_path", None)
|
||||
self._max_file_bytes = int(extra.pop("max_file_bytes", self.MAX_FILE_BYTES))
|
||||
self._hash_num_threads = int(
|
||||
extra.pop("hash_num_threads", self.DEFAULT_HASH_NUM_THREADS)
|
||||
)
|
||||
self._verify_on_load = bool(extra.pop("verify_on_load", False))
|
||||
load_config.model_loader_extra_config = extra
|
||||
load_config.load_format = LoadFormat.AUTO
|
||||
super().__init__(load_config)
|
||||
|
||||
def download_model(self, model_config: ModelConfig) -> None:
|
||||
presharded_dir = self._presharded_dir(model_config)
|
||||
if not self._presharded_ready(presharded_dir):
|
||||
super().download_model(model_config)
|
||||
|
||||
def load_model(
|
||||
self,
|
||||
*,
|
||||
model_config: ModelConfig,
|
||||
device_config: DeviceConfig,
|
||||
) -> nn.Module:
|
||||
shard_config = self._collect_shard_config(model_config)
|
||||
presharded_dir = self._presharded_dir(model_config, shard_config)
|
||||
if self._presharded_ready(presharded_dir) and self._shard_config_matches(
|
||||
presharded_dir, shard_config
|
||||
):
|
||||
logger.info("Loading from presharded checkpoint at %s", presharded_dir)
|
||||
return self._load_from_presharded(
|
||||
model_config, device_config, presharded_dir
|
||||
)
|
||||
logger.info(
|
||||
"No presharded checkpoint at %s; doing first-time load and dump.",
|
||||
presharded_dir,
|
||||
)
|
||||
return self._first_time_load_and_dump(
|
||||
model_config, device_config, presharded_dir, shard_config
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _presharded_ready(cls, presharded_dir: str) -> bool:
|
||||
return os.path.isfile(os.path.join(presharded_dir, cls.READY_FILENAME))
|
||||
|
||||
def _presharded_dir(
|
||||
self,
|
||||
model_config: ModelConfig,
|
||||
shard_config: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
if shard_config is None:
|
||||
shard_config = self._collect_shard_config(model_config)
|
||||
subfolder = self._build_subfolder_name(shard_config)
|
||||
if model_config.is_draft_model:
|
||||
root = self._draft_presharded_path_override
|
||||
else:
|
||||
root = self._presharded_path_override
|
||||
if root is None:
|
||||
root = os.path.join(model_config.model_path, self.DEFAULT_SUBDIR)
|
||||
return os.path.join(root, subfolder)
|
||||
|
||||
def _collect_shard_config(self, model_config: ModelConfig) -> Dict[str, Any]:
|
||||
def _safe(fn) -> int:
|
||||
try:
|
||||
return fn()
|
||||
except (AssertionError, AttributeError, RuntimeError):
|
||||
return 1
|
||||
|
||||
parallel = get_parallel()
|
||||
server_args = get_server_args()
|
||||
return {
|
||||
"tp": _safe(lambda: parallel.tp_size),
|
||||
"dp": _safe(lambda: parallel.moe_dp_size),
|
||||
"ep": _safe(lambda: parallel.moe_ep_size),
|
||||
"pp": _safe(lambda: parallel.pp_size),
|
||||
"moe_dense_tp_size": server_args.moe_dense_tp_size,
|
||||
"moe_dp_size": server_args.moe_dp_size,
|
||||
"enable_dp_lm_head": server_args.enable_dp_lm_head,
|
||||
"enable_fp32_lm_head": server_args.enable_fp32_lm_head,
|
||||
"quantization": model_config.quantization,
|
||||
"model_dtype": str(model_config.dtype),
|
||||
"ep_num_redundant_experts": server_args.ep_num_redundant_experts,
|
||||
"enable_eplb": server_args.enable_eplb,
|
||||
"init_expert_location": self._normalize_init_expert_location(
|
||||
server_args.init_expert_location
|
||||
),
|
||||
"structural_signature": self._compute_structural_signature(model_config),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_init_expert_location(value: Optional[str]) -> Optional[str]:
|
||||
if value is None or value == "trivial":
|
||||
return value
|
||||
if value.endswith((".json", ".pt")) and os.path.isfile(value):
|
||||
h = hashlib.sha1()
|
||||
with open(value, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
return f"file:{os.path.basename(value)}:sha1:{h.hexdigest()[:16]}"
|
||||
return value
|
||||
|
||||
def _build_subfolder_name(self, shard_config: Dict[str, Any]) -> str:
|
||||
combined = hashlib.sha1(
|
||||
json.dumps(shard_config, sort_keys=True).encode()
|
||||
).hexdigest()[:16]
|
||||
return f"TP-{shard_config['tp']}-sig-{combined}"
|
||||
|
||||
def _shard_config_matches(
|
||||
self, presharded_dir: str, shard_config: Dict[str, Any]
|
||||
) -> bool:
|
||||
try:
|
||||
with open(os.path.join(presharded_dir, self.CHECKSUM_FILENAME)) as f:
|
||||
stored = json.load(f).get("shard_config")
|
||||
except (OSError, ValueError):
|
||||
stored = None
|
||||
current = json.loads(json.dumps(shard_config))
|
||||
if stored == current:
|
||||
return True
|
||||
logger.warning(
|
||||
"Presharded checkpoint at %s was dumped with a different shard "
|
||||
"config than the current launch (stored=%s, current=%s). "
|
||||
"Treating as a cache miss and re-dumping.",
|
||||
presharded_dir,
|
||||
stored,
|
||||
current,
|
||||
)
|
||||
return False
|
||||
|
||||
def _compute_structural_signature(self, model_config: ModelConfig) -> Optional[str]:
|
||||
local_sig = self._compute_local_structural_signature(model_config)
|
||||
return self._make_rank_invariant_structural_signature(local_sig)
|
||||
|
||||
def _compute_local_structural_signature(
|
||||
self, model_config: ModelConfig
|
||||
) -> Optional[str]:
|
||||
from sglang.srt.layers.rotary_embedding.factory import _ROPE_DICT
|
||||
|
||||
def _clear_meta_rope_cache() -> None:
|
||||
meta_keys = [
|
||||
k
|
||||
for k, v in _ROPE_DICT.items()
|
||||
if any(p.device.type == "meta" for p in v.parameters())
|
||||
or any(b.device.type == "meta" for b in v.buffers())
|
||||
]
|
||||
for k in meta_keys:
|
||||
del _ROPE_DICT[k]
|
||||
|
||||
try:
|
||||
quant_config = _get_quantization_config(model_config, self.load_config)
|
||||
with set_default_torch_dtype(model_config.dtype):
|
||||
with torch.device("meta"):
|
||||
meta_model = _initialize_model(
|
||||
model_config, self.load_config, quant_config
|
||||
)
|
||||
state_dict = meta_model.state_dict()
|
||||
sig_input = sorted(
|
||||
(name, tuple(t.shape), str(t.dtype))
|
||||
for name, t in state_dict.items()
|
||||
)
|
||||
del meta_model
|
||||
return self._hash_structural_signature(sig_input)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to build structural signature for presharded cache key "
|
||||
"(model_type=%s): %s",
|
||||
getattr(
|
||||
getattr(model_config, "hf_config", None), "model_type", "unknown"
|
||||
),
|
||||
e,
|
||||
)
|
||||
return None
|
||||
finally:
|
||||
_clear_meta_rope_cache()
|
||||
|
||||
@classmethod
|
||||
def _make_rank_invariant_structural_signature(
|
||||
cls, local_sig: Optional[str]
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
from sglang.srt.distributed import get_world_group
|
||||
|
||||
group = get_world_group()
|
||||
if group.world_size <= 1:
|
||||
return local_sig
|
||||
all_sigs = group.all_gather_object(local_sig)
|
||||
except (AssertionError, AttributeError, RuntimeError):
|
||||
return local_sig
|
||||
|
||||
if all(s is None for s in all_sigs):
|
||||
return None
|
||||
return hashlib.sha1(repr(all_sigs).encode()).hexdigest()[:16]
|
||||
|
||||
@staticmethod
|
||||
def _hash_structural_signature(
|
||||
sig_input: List[Tuple[str, Tuple[int, ...], str]],
|
||||
) -> str:
|
||||
h = hashlib.sha1(repr(sig_input).encode())
|
||||
return h.hexdigest()[:16]
|
||||
|
||||
@staticmethod
|
||||
def _world_rank_and_size() -> Tuple[int, int]:
|
||||
from sglang.srt.distributed import get_world_group
|
||||
|
||||
try:
|
||||
g = get_world_group()
|
||||
return g.rank_in_group, g.world_size
|
||||
except (AssertionError, AttributeError):
|
||||
return 0, 1
|
||||
|
||||
@staticmethod
|
||||
def _world_barrier() -> None:
|
||||
from sglang.srt.distributed import get_world_group
|
||||
|
||||
try:
|
||||
get_world_group().barrier()
|
||||
except (AssertionError, AttributeError):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _new_content_hasher():
|
||||
import xxhash
|
||||
|
||||
return xxhash.xxh3_128()
|
||||
|
||||
@staticmethod
|
||||
def _hash_tensor(tensor: torch.Tensor) -> str:
|
||||
# CPU copy so concurrent dump workers cannot race CUDA D2H hashing.
|
||||
t = tensor.detach()
|
||||
prefix = str(tuple(t.shape)).encode() + str(t.dtype).encode()
|
||||
h = PreshardedModelLoader._new_content_hasher()
|
||||
h.update(prefix)
|
||||
|
||||
if t.numel() == 0:
|
||||
return h.hexdigest()
|
||||
|
||||
cpu = t.contiguous().to(device="cpu", copy=True).contiguous()
|
||||
flat_u8 = cpu.reshape(-1).view(torch.uint8)
|
||||
h.update(memoryview(flat_u8.numpy()))
|
||||
return h.hexdigest()
|
||||
|
||||
def _verify_rank_checksum(
|
||||
self,
|
||||
verify_hashes: List[Tuple[str, str]],
|
||||
plan: Dict[str, Any],
|
||||
rank: int,
|
||||
presharded_dir: str,
|
||||
) -> None:
|
||||
expected = plan.get("rank_checksums", {}).get(str(rank))
|
||||
if expected is None:
|
||||
raise ValueError(
|
||||
f"Plan at {presharded_dir} has no rank_checksums entry for "
|
||||
f"rank {rank}; cannot verify. Set "
|
||||
f"--model-loader-extra-config '{{\"verify_on_load\": false}}' "
|
||||
f"to skip verification, or re-dump the checkpoint."
|
||||
)
|
||||
|
||||
total = 0
|
||||
for name, content_hash in verify_hashes:
|
||||
d = PreshardedModelLoader._fold_name_content_digest(name, content_hash)
|
||||
total = (total + int.from_bytes(d[:8], "big")) & 0xFFFFFFFFFFFFFFFF
|
||||
actual = format(total, "016x")
|
||||
|
||||
if actual != expected:
|
||||
raise ValueError(
|
||||
f"Rank-{rank} checksum mismatch for presharded checkpoint at "
|
||||
f"{presharded_dir}: expected {expected}, got {actual}. The "
|
||||
f"checkpoint files may be corrupted; re-dump or skip "
|
||||
f"verification with --model-loader-extra-config "
|
||||
f"'{{\"verify_on_load\": false}}'."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _fold_name_content_digest(name: str, content_hash: str) -> bytes:
|
||||
h = PreshardedModelLoader._new_content_hasher()
|
||||
h.update((name + ":" + content_hash).encode("utf-8"))
|
||||
return h.digest()
|
||||
|
||||
@staticmethod
|
||||
def _collect_extra_tensors(model: nn.Module) -> Dict[str, torch.Tensor]:
|
||||
seen: set = set()
|
||||
param_storages: set = set()
|
||||
for name, tensor in model.state_dict().items():
|
||||
seen.add(name)
|
||||
if tensor.numel() > 0:
|
||||
param_storages.add((tensor.device, tensor.untyped_storage().data_ptr()))
|
||||
extras: Dict[str, torch.Tensor] = {}
|
||||
for module_name, module in model.named_modules():
|
||||
prefix = f"{module_name}." if module_name else ""
|
||||
for attr_name in list(vars(module).keys()):
|
||||
if attr_name.startswith("_"):
|
||||
continue
|
||||
try:
|
||||
val = getattr(module, attr_name)
|
||||
except AttributeError:
|
||||
continue
|
||||
if isinstance(val, torch.Tensor) and not isinstance(
|
||||
val, torch.nn.Parameter
|
||||
):
|
||||
full_name = f"{prefix}{attr_name}"
|
||||
if full_name in seen:
|
||||
continue
|
||||
if val.numel() > 0:
|
||||
key = (val.device, val.untyped_storage().data_ptr())
|
||||
if key in param_storages:
|
||||
continue
|
||||
extras[full_name] = val
|
||||
return extras
|
||||
|
||||
@staticmethod
|
||||
def _rebind_parameter_aliases(model: nn.Module) -> None:
|
||||
for _, module in model.named_modules():
|
||||
gemma_w = getattr(module, "gemma_weight", None)
|
||||
weight = getattr(module, "weight", None)
|
||||
if (
|
||||
isinstance(gemma_w, torch.Tensor)
|
||||
and isinstance(weight, torch.nn.Parameter)
|
||||
and gemma_w.shape == weight.shape
|
||||
):
|
||||
torch.add(weight.data, 1.0, out=gemma_w)
|
||||
|
||||
attn = getattr(module, "attn", None)
|
||||
conv1d = getattr(module, "conv1d", None)
|
||||
if attn is None:
|
||||
continue
|
||||
if hasattr(module, "A_log") and hasattr(attn, "A_log"):
|
||||
attn.A_log = module.A_log
|
||||
if hasattr(module, "dt_bias") and hasattr(attn, "dt_bias"):
|
||||
attn.dt_bias = module.dt_bias
|
||||
if conv1d is None:
|
||||
continue
|
||||
cweight = getattr(conv1d, "weight", None)
|
||||
if cweight is not None and hasattr(attn, "conv_weights"):
|
||||
if cweight.dim() == 3 and cweight.size(1) == 1:
|
||||
attn.conv_weights = cweight.view(cweight.size(0), cweight.size(2))
|
||||
else:
|
||||
attn.conv_weights = (
|
||||
cweight.squeeze() if cweight.dim() > 2 else cweight
|
||||
)
|
||||
if hasattr(conv1d, "bias") and hasattr(attn, "bias"):
|
||||
attn.bias = conv1d.bias
|
||||
|
||||
def _ensure_presharded_dir_writable(self, presharded_dir: str) -> None:
|
||||
rank, _ = self._world_rank_and_size()
|
||||
try:
|
||||
os.makedirs(presharded_dir, exist_ok=True)
|
||||
if rank == 0:
|
||||
probe = os.path.join(presharded_dir, ".presharded_write_probe")
|
||||
last_err: Optional[OSError] = None
|
||||
for _ in range(5):
|
||||
try:
|
||||
with open(probe, "w") as f:
|
||||
f.write("ok")
|
||||
os.unlink(probe)
|
||||
last_err = None
|
||||
break
|
||||
except OSError as e:
|
||||
last_err = e
|
||||
os.makedirs(presharded_dir, exist_ok=True)
|
||||
time.sleep(0.05)
|
||||
if last_err is not None:
|
||||
raise last_err
|
||||
except OSError as e:
|
||||
raise RuntimeError(
|
||||
f"Presharded dump directory is not writable: {presharded_dir}. "
|
||||
"Set model_loader_extra_config "
|
||||
'\'{"presharded_path": "..."}\' (or draft_presharded_path for '
|
||||
"the draft model) to a writable shared filesystem path. "
|
||||
f"Original error: {e}"
|
||||
) from e
|
||||
self._world_barrier()
|
||||
|
||||
def _first_time_load_and_dump(
|
||||
self,
|
||||
model_config: ModelConfig,
|
||||
device_config: DeviceConfig,
|
||||
presharded_dir: str,
|
||||
shard_config: Dict[str, Any],
|
||||
) -> nn.Module:
|
||||
self._ensure_presharded_dir_writable(presharded_dir)
|
||||
target_device = torch.device(device_config.device)
|
||||
quant_config = _get_quantization_config(model_config, self.load_config)
|
||||
with set_default_torch_dtype(model_config.dtype):
|
||||
with target_device:
|
||||
model = _initialize_model(model_config, self.load_config, quant_config)
|
||||
self.load_weights_and_postprocess(
|
||||
model,
|
||||
self._get_all_weights(model_config, model),
|
||||
target_device,
|
||||
)
|
||||
|
||||
state_dict = dict(model.state_dict())
|
||||
extras = self._collect_extra_tensors(model)
|
||||
self._dump_state_to_disk(state_dict, extras, presharded_dir, shard_config)
|
||||
del state_dict
|
||||
del extras
|
||||
gc.collect()
|
||||
|
||||
self.counter_after_loading_weights = time.perf_counter()
|
||||
return model.eval()
|
||||
|
||||
def _dump_state_to_disk(
|
||||
self,
|
||||
state_dict: Dict[str, torch.Tensor],
|
||||
extras: Dict[str, torch.Tensor],
|
||||
presharded_dir: str,
|
||||
shard_config: Dict[str, Any],
|
||||
) -> None:
|
||||
rank, world_size = self._world_rank_and_size()
|
||||
tmp_dir = os.path.join(presharded_dir, self.TMP_SUBDIR)
|
||||
if rank == 0:
|
||||
ready_path = os.path.join(presharded_dir, self.READY_FILENAME)
|
||||
if os.path.isfile(ready_path):
|
||||
os.unlink(ready_path)
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
self._world_barrier()
|
||||
|
||||
items: List[Tuple[str, torch.Tensor, bool]] = []
|
||||
items.extend((n, t, False) for n, t in state_dict.items())
|
||||
items.extend((n, t, True) for n, t in extras.items())
|
||||
|
||||
def _entry(item: Tuple[str, torch.Tensor, bool]) -> Tuple[str, Dict[str, Any]]:
|
||||
name, tensor, is_extra = item
|
||||
return name, {
|
||||
"checksum": self._hash_tensor(tensor),
|
||||
"size": tensor.numel() * tensor.element_size(),
|
||||
"dtype": str(tensor.dtype),
|
||||
"shape": list(tensor.shape),
|
||||
"is_extra": is_extra,
|
||||
}
|
||||
|
||||
manifest: Dict[str, Dict[str, Any]] = {}
|
||||
num_workers = min(max(1, len(items)), self._hash_num_threads)
|
||||
if num_workers <= 1:
|
||||
for it in items:
|
||||
name, info = _entry(it)
|
||||
manifest[name] = info
|
||||
else:
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=num_workers,
|
||||
thread_name_prefix="presharded-hash",
|
||||
) as ex:
|
||||
for name, info in ex.map(_entry, items):
|
||||
manifest[name] = info
|
||||
|
||||
with open(os.path.join(tmp_dir, f"manifest_{rank:05d}.json"), "w") as f:
|
||||
json.dump(manifest, f)
|
||||
self._world_barrier()
|
||||
|
||||
if rank == 0:
|
||||
plan = self._build_dump_plan(world_size, tmp_dir, self._max_file_bytes)
|
||||
plan["shard_config"] = shard_config
|
||||
with open(os.path.join(presharded_dir, self.CHECKSUM_FILENAME), "w") as f:
|
||||
json.dump(plan, f, indent=2)
|
||||
self._world_barrier()
|
||||
|
||||
with open(os.path.join(presharded_dir, self.CHECKSUM_FILENAME)) as f:
|
||||
plan = json.load(f)
|
||||
all_tensors = {**state_dict, **extras}
|
||||
self._dump_files_for_rank(all_tensors, plan, rank, presharded_dir)
|
||||
self._world_barrier()
|
||||
|
||||
if rank == 0:
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
ready_path = os.path.join(presharded_dir, self.READY_FILENAME)
|
||||
with open(ready_path, "w") as f:
|
||||
json.dump(
|
||||
{
|
||||
"plan_version": self.PLAN_VERSION,
|
||||
"world_size": world_size,
|
||||
"created_at": time.time(),
|
||||
},
|
||||
f,
|
||||
)
|
||||
self._world_barrier()
|
||||
|
||||
@staticmethod
|
||||
def _make_filename(
|
||||
file_id: int, rank_list: Tuple[int, ...], is_common: bool
|
||||
) -> str:
|
||||
if is_common:
|
||||
return f"model-{file_id:05d}-common.safetensor"
|
||||
rank_str = ",".join(f"{r:03d}" for r in rank_list)
|
||||
return f"model-{file_id:05d}-rank-{rank_str}.safetensor"
|
||||
|
||||
@classmethod
|
||||
def _build_dump_plan(
|
||||
cls, world_size: int, tmp_dir: str, max_file_bytes: int
|
||||
) -> Dict[str, Any]:
|
||||
rank_to_manifest: Dict[int, Dict[str, Dict[str, Any]]] = {}
|
||||
for r in range(world_size):
|
||||
manifest_path = os.path.join(tmp_dir, f"manifest_{r:05d}.json")
|
||||
try:
|
||||
with open(manifest_path) as f:
|
||||
rank_to_manifest[r] = json.load(f)
|
||||
except FileNotFoundError as e:
|
||||
raise FileNotFoundError(
|
||||
f"Rank {r} did not write {manifest_path}. The presharded "
|
||||
"dump directory must be on a filesystem shared by all "
|
||||
"ranks/nodes (set presharded_path / draft_presharded_path "
|
||||
"to a shared path if model_path is node-local)."
|
||||
) from e
|
||||
|
||||
checksum_to_entries: Dict[str, List[Tuple[int, str, Dict[str, Any]]]] = (
|
||||
collections.defaultdict(list)
|
||||
)
|
||||
name_to_is_extra: Dict[Tuple[int, str], bool] = {}
|
||||
for r, manifest in rank_to_manifest.items():
|
||||
for name, info in manifest.items():
|
||||
checksum_to_entries[info["checksum"]].append((r, name, info))
|
||||
name_to_is_extra[(r, name)] = bool(info.get("is_extra", False))
|
||||
|
||||
tensor_records: List[Dict[str, Any]] = []
|
||||
for checksum, entries in checksum_to_entries.items():
|
||||
sizes = {info["size"] for _, _, info in entries}
|
||||
if len(sizes) != 1:
|
||||
raise RuntimeError(
|
||||
f"Checksum {checksum} maps to inconsistent sizes {sizes}; "
|
||||
f"this indicates a hash collision or stale manifest."
|
||||
)
|
||||
size = next(iter(sizes))
|
||||
ranks = sorted({r for r, _, _ in entries})
|
||||
rank_to_names: Dict[str, List[str]] = collections.defaultdict(list)
|
||||
for r, n, _ in entries:
|
||||
rank_to_names[str(r)].append(n)
|
||||
tensor_records.append(
|
||||
{
|
||||
"checksum": checksum,
|
||||
"size": size,
|
||||
"rank_list": ranks,
|
||||
"rank_to_names": {k: sorted(v) for k, v in rank_to_names.items()},
|
||||
}
|
||||
)
|
||||
|
||||
by_rank_list: Dict[Tuple[int, ...], List[Dict[str, Any]]] = (
|
||||
collections.defaultdict(list)
|
||||
)
|
||||
for rec in tensor_records:
|
||||
by_rank_list[tuple(rec["rank_list"])].append(rec)
|
||||
|
||||
files: List[Dict[str, Any]] = []
|
||||
next_file_id = 0
|
||||
for rank_tuple, recs in by_rank_list.items():
|
||||
recs.sort(key=lambda r: -r["size"])
|
||||
is_common = len(rank_tuple) == world_size and rank_tuple == tuple(
|
||||
range(world_size)
|
||||
)
|
||||
writer_load = {wr: 0 for wr in rank_tuple}
|
||||
writer_records: Dict[int, List[Dict[str, Any]]] = {
|
||||
wr: [] for wr in rank_tuple
|
||||
}
|
||||
for rec in recs:
|
||||
wr = min(rank_tuple, key=lambda r: writer_load[r])
|
||||
writer_records[wr].append(rec)
|
||||
writer_load[wr] += rec["size"]
|
||||
|
||||
for wr, wr_recs in writer_records.items():
|
||||
cur_size = 0
|
||||
cur_tensors: List[Dict[str, Any]] = []
|
||||
for rec in wr_recs:
|
||||
if cur_tensors and cur_size + rec["size"] > max_file_bytes:
|
||||
files.append(
|
||||
{
|
||||
"filename": cls._make_filename(
|
||||
next_file_id, rank_tuple, is_common
|
||||
),
|
||||
"writer_rank": wr,
|
||||
"rank_list": (None if is_common else list(rank_tuple)),
|
||||
"is_common": is_common,
|
||||
"tensors": cur_tensors,
|
||||
}
|
||||
)
|
||||
next_file_id += 1
|
||||
cur_size = 0
|
||||
cur_tensors = []
|
||||
cur_tensors.append(
|
||||
{
|
||||
"stored_key": rec["checksum"],
|
||||
"size": rec["size"],
|
||||
"rank_to_names": rec["rank_to_names"],
|
||||
}
|
||||
)
|
||||
cur_size += rec["size"]
|
||||
if cur_tensors:
|
||||
files.append(
|
||||
{
|
||||
"filename": cls._make_filename(
|
||||
next_file_id, rank_tuple, is_common
|
||||
),
|
||||
"writer_rank": wr,
|
||||
"rank_list": (None if is_common else list(rank_tuple)),
|
||||
"is_common": is_common,
|
||||
"tensors": cur_tensors,
|
||||
}
|
||||
)
|
||||
next_file_id += 1
|
||||
|
||||
rank_to_reads: Dict[int, List[Dict[str, Any]]] = collections.defaultdict(list)
|
||||
for f in files:
|
||||
for t in f["tensors"]:
|
||||
for r_str, names in t["rank_to_names"].items():
|
||||
for name in names:
|
||||
rank_to_reads[int(r_str)].append(
|
||||
{
|
||||
"filename": f["filename"],
|
||||
"stored_key": t["stored_key"],
|
||||
"name": name,
|
||||
"is_extra": name_to_is_extra.get(
|
||||
(int(r_str), name), False
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
rank_checksums: Dict[str, str] = {}
|
||||
for r in range(world_size):
|
||||
total = 0
|
||||
for rec in rank_to_reads.get(r, []):
|
||||
d = cls._fold_name_content_digest(rec["name"], rec["stored_key"])
|
||||
total = (total + int.from_bytes(d[:8], "big")) & 0xFFFFFFFFFFFFFFFF
|
||||
rank_checksums[str(r)] = format(total, "016x")
|
||||
|
||||
return {
|
||||
"version": cls.PLAN_VERSION,
|
||||
"world_size": world_size,
|
||||
"files": files,
|
||||
"rank_to_reads": {str(r): v for r, v in rank_to_reads.items()},
|
||||
"rank_checksums": rank_checksums,
|
||||
}
|
||||
|
||||
def _dump_files_for_rank(
|
||||
self,
|
||||
state_dict: Dict[str, torch.Tensor],
|
||||
plan: Dict[str, Any],
|
||||
rank: int,
|
||||
presharded_dir: str,
|
||||
) -> None:
|
||||
from safetensors.torch import save_file
|
||||
|
||||
for f in plan["files"]:
|
||||
if f["writer_rank"] != rank:
|
||||
continue
|
||||
tensors_to_save: Dict[str, torch.Tensor] = {}
|
||||
for t in f["tensors"]:
|
||||
names_for_this_rank = t["rank_to_names"].get(str(rank))
|
||||
if not names_for_this_rank:
|
||||
raise RuntimeError(
|
||||
f"writer_rank {rank} is missing tensor {t['stored_key']} "
|
||||
f"for file {f['filename']}; plan is inconsistent."
|
||||
)
|
||||
name_for_this_rank = names_for_this_rank[0]
|
||||
tensor = (
|
||||
state_dict[name_for_this_rank]
|
||||
.detach()
|
||||
.to(device="cpu", copy=False)
|
||||
.contiguous()
|
||||
)
|
||||
tensors_to_save[t["stored_key"]] = tensor
|
||||
save_file(tensors_to_save, os.path.join(presharded_dir, f["filename"]))
|
||||
|
||||
@staticmethod
|
||||
def _read_presharded_file(
|
||||
full_path: str, stored_keys: List[str]
|
||||
) -> Dict[str, torch.Tensor]:
|
||||
from safetensors.torch import safe_open
|
||||
|
||||
with safe_open(full_path, framework="pt") as fh:
|
||||
return {key: fh.get_tensor(key) for key in stored_keys}
|
||||
|
||||
def _apply_presharded_file(
|
||||
self,
|
||||
*,
|
||||
items: List[Dict[str, Any]],
|
||||
cached: Dict[str, torch.Tensor],
|
||||
model: nn.Module,
|
||||
state_dict: Dict[str, torch.Tensor],
|
||||
target_device: torch.device,
|
||||
loaded_param_keys: set,
|
||||
verify_hashes: List[Tuple[str, str]],
|
||||
) -> None:
|
||||
if self._verify_on_load:
|
||||
keys = list(cached.keys())
|
||||
n_workers = min(max(1, len(keys)), self._hash_num_threads)
|
||||
|
||||
def _hash_one(key, _cached=cached):
|
||||
return key, self._hash_tensor(_cached[key])
|
||||
|
||||
if n_workers <= 1:
|
||||
key_to_hash = dict(_hash_one(k) for k in keys)
|
||||
else:
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=n_workers,
|
||||
thread_name_prefix="presharded-verify",
|
||||
) as ex:
|
||||
key_to_hash = dict(ex.map(_hash_one, keys))
|
||||
for r in items:
|
||||
verify_hashes.append((r["name"], key_to_hash[r["stored_key"]]))
|
||||
|
||||
for r in items:
|
||||
tensor = cached[r["stored_key"]]
|
||||
if r.get("is_extra"):
|
||||
module_path, _, attr_name = r["name"].rpartition(".")
|
||||
module = model.get_submodule(module_path) if module_path else model
|
||||
if hasattr(module, attr_name):
|
||||
try:
|
||||
delattr(module, attr_name)
|
||||
except AttributeError:
|
||||
pass
|
||||
setattr(module, attr_name, tensor.to(target_device))
|
||||
continue
|
||||
if r["name"] not in state_dict:
|
||||
continue
|
||||
param_data = state_dict[r["name"]].data
|
||||
param_shape = state_dict[r["name"]].shape
|
||||
for dim, size in enumerate(tensor.shape):
|
||||
if size < param_shape[dim]:
|
||||
param_data = param_data.narrow(dim, 0, size)
|
||||
if tensor.shape != param_data.shape:
|
||||
raise ValueError(
|
||||
f"Presharded tensor shape mismatch for '{r['name']}': "
|
||||
f"dumped {tuple(tensor.shape)} vs parameter slice "
|
||||
f"{tuple(param_data.shape)} (full param {tuple(param_shape)}). "
|
||||
"Re-dump with matching quant/parallel config, or set "
|
||||
"verify_on_load and check process_weights_after_loading."
|
||||
)
|
||||
param_data.copy_(tensor)
|
||||
loaded_param_keys.add(r["name"])
|
||||
|
||||
cached.clear()
|
||||
del cached
|
||||
|
||||
def _load_from_presharded(
|
||||
self,
|
||||
model_config: ModelConfig,
|
||||
device_config: DeviceConfig,
|
||||
presharded_dir: str,
|
||||
) -> nn.Module:
|
||||
target_device = torch.device(device_config.device)
|
||||
quant_config = _get_quantization_config(model_config, self.load_config)
|
||||
|
||||
with set_default_torch_dtype(model_config.dtype):
|
||||
with target_device:
|
||||
model = _initialize_model(model_config, self.load_config, quant_config)
|
||||
|
||||
for _, module in model.named_modules():
|
||||
quant_method = getattr(module, "quant_method", None)
|
||||
if quant_method is not None:
|
||||
with device_loading_context(module, target_device):
|
||||
quant_method.process_weights_after_loading(module)
|
||||
|
||||
rank, _ = self._world_rank_and_size()
|
||||
with open(os.path.join(presharded_dir, self.CHECKSUM_FILENAME)) as f:
|
||||
plan = json.load(f)
|
||||
if plan.get("version") != self.PLAN_VERSION:
|
||||
raise ValueError(
|
||||
f"Unsupported presharded plan version {plan.get('version')!r} "
|
||||
f"at {presharded_dir}; expected {self.PLAN_VERSION}."
|
||||
)
|
||||
|
||||
state_dict = dict(model.state_dict())
|
||||
reads = plan.get("rank_to_reads", {}).get(str(rank), [])
|
||||
|
||||
by_file: Dict[str, List[Dict[str, Any]]] = collections.defaultdict(list)
|
||||
for r in reads:
|
||||
by_file[r["filename"]].append(r)
|
||||
|
||||
loaded_param_keys: set = set()
|
||||
verify_hashes: List[Tuple[str, str]] = []
|
||||
for filename, items in by_file.items():
|
||||
stored_keys = list(dict.fromkeys(r["stored_key"] for r in items))
|
||||
cached = self._read_presharded_file(
|
||||
os.path.join(presharded_dir, filename), stored_keys
|
||||
)
|
||||
self._apply_presharded_file(
|
||||
items=items,
|
||||
cached=cached,
|
||||
model=model,
|
||||
state_dict=state_dict,
|
||||
target_device=target_device,
|
||||
loaded_param_keys=loaded_param_keys,
|
||||
verify_hashes=verify_hashes,
|
||||
)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.synchronize()
|
||||
|
||||
loaded_storages: set = set()
|
||||
for k in loaded_param_keys:
|
||||
t = state_dict[k]
|
||||
if t.numel() > 0:
|
||||
loaded_storages.add((t.device, t.untyped_storage().data_ptr()))
|
||||
missing = []
|
||||
for k, t in state_dict.items():
|
||||
if k in loaded_param_keys:
|
||||
continue
|
||||
if t.numel() == 0:
|
||||
continue
|
||||
storage_key = (t.device, t.untyped_storage().data_ptr())
|
||||
if storage_key not in loaded_storages:
|
||||
missing.append(k)
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"Missing keys {tuple(sorted(missing))} in presharded "
|
||||
f"checkpoint at {presharded_dir}."
|
||||
)
|
||||
|
||||
self._rebind_parameter_aliases(model)
|
||||
|
||||
if self._verify_on_load:
|
||||
self._verify_rank_checksum(verify_hashes, plan, rank, presharded_dir)
|
||||
|
||||
self.counter_after_loading_weights = time.perf_counter()
|
||||
return model.eval()
|
||||
|
||||
|
||||
class BitsAndBytesModelLoader(BaseModelLoader):
|
||||
"""Model loader to load model weights with BitAndBytes quantization."""
|
||||
|
||||
@@ -3271,6 +4116,9 @@ def get_model_loader(
|
||||
if load_config.load_format == LoadFormat.SHARDED_STATE:
|
||||
return ShardedStateLoader(load_config)
|
||||
|
||||
if load_config.load_format == LoadFormat.PRESHARDED:
|
||||
return PreshardedModelLoader(load_config)
|
||||
|
||||
if load_config.load_format == LoadFormat.BITSANDBYTES:
|
||||
return BitsAndBytesModelLoader(load_config)
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ LOAD_FORMAT_CHOICES = [
|
||||
"npcache",
|
||||
"dummy",
|
||||
"sharded_state",
|
||||
"presharded",
|
||||
"gguf",
|
||||
"bitsandbytes",
|
||||
"mistral",
|
||||
@@ -517,14 +518,31 @@ class ServerArgs:
|
||||
"quantization."
|
||||
'"layered" loads weights layer by layer so that one can quantize a '
|
||||
"layer before loading another to make the peak memory envelope "
|
||||
"smaller.",
|
||||
"smaller."
|
||||
'"presharded" performs a normal first-time load (with quantization), '
|
||||
"then dumps a per-rank/per-tensor sharded checkpoint with content "
|
||||
"deduplication into "
|
||||
"<model_path>/presharded/<parallelism+quant subfolder>/. "
|
||||
"Subsequent runs with the same parallelism+quantization config "
|
||||
"load directly from this presharded checkpoint and skip "
|
||||
"re-quantization. "
|
||||
"The dump directory must be on a shared filesystem across all "
|
||||
"ranks/nodes. Optional model_loader_extra_config roots: "
|
||||
"presharded_path (target) and draft_presharded_path (speculative "
|
||||
"draft); each replaces <model_path>/presharded and still gets a "
|
||||
"config subfolder appended. Use a writable path when model_path "
|
||||
"is read-only (e.g. HF cache mounts).",
|
||||
choices=LOAD_FORMAT_CHOICES,
|
||||
),
|
||||
NS("model"),
|
||||
] = "auto"
|
||||
model_loader_extra_config: A[
|
||||
str,
|
||||
"Extra config for model loader. This will be passed to the model loader corresponding to the chosen load_format.",
|
||||
"Extra config for model loader. This will be passed to the model loader "
|
||||
"corresponding to the chosen load_format. For load_format=presharded, "
|
||||
"JSON may include presharded_path (target cache root), "
|
||||
"draft_presharded_path (draft cache root), max_file_bytes, "
|
||||
"hash_num_threads, and verify_on_load.",
|
||||
NS("model"),
|
||||
] = "{}"
|
||||
trust_remote_code: A[
|
||||
|
||||
@@ -0,0 +1,960 @@
|
||||
"""Unit tests for PreshardedModelLoader's pure helpers.
|
||||
|
||||
These tests exercise the deterministic pieces of the presharding algorithm
|
||||
(tensor hashing, plan construction, file naming, dedup, file-size cap, and
|
||||
per-rank workload balance) without needing a GPU or distributed setup.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.model_loader.loader import PreshardedModelLoader
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
|
||||
register_cpu_ci(est_time=10, suite="base-a-test-cpu")
|
||||
|
||||
|
||||
class TestPreshardedHashTensor(unittest.TestCase):
|
||||
def test_same_content_same_hash(self):
|
||||
a = torch.arange(100, dtype=torch.float32).reshape(10, 10)
|
||||
b = torch.arange(100, dtype=torch.float32).reshape(10, 10)
|
||||
self.assertEqual(
|
||||
PreshardedModelLoader._hash_tensor(a),
|
||||
PreshardedModelLoader._hash_tensor(b),
|
||||
)
|
||||
|
||||
def test_different_content_different_hash(self):
|
||||
a = torch.arange(100, dtype=torch.float32)
|
||||
b = a.clone()
|
||||
b[0] = 999
|
||||
self.assertNotEqual(
|
||||
PreshardedModelLoader._hash_tensor(a),
|
||||
PreshardedModelLoader._hash_tensor(b),
|
||||
)
|
||||
|
||||
def test_dtype_changes_hash(self):
|
||||
a = torch.zeros(8, dtype=torch.float32)
|
||||
b = torch.zeros(16, dtype=torch.float16) # same byte content (zeros)
|
||||
self.assertNotEqual(
|
||||
PreshardedModelLoader._hash_tensor(a),
|
||||
PreshardedModelLoader._hash_tensor(b),
|
||||
)
|
||||
|
||||
def test_shape_changes_hash(self):
|
||||
a = torch.zeros(16, dtype=torch.float32)
|
||||
b = torch.zeros((4, 4), dtype=torch.float32)
|
||||
self.assertNotEqual(
|
||||
PreshardedModelLoader._hash_tensor(a),
|
||||
PreshardedModelLoader._hash_tensor(b),
|
||||
)
|
||||
|
||||
def test_empty_tensor_is_hashable(self):
|
||||
a = torch.empty(0, dtype=torch.float32)
|
||||
digest = PreshardedModelLoader._hash_tensor(a)
|
||||
self.assertIsInstance(digest, str)
|
||||
self.assertEqual(
|
||||
len(digest), PreshardedModelLoader._CONTENT_HASH_HEX_LEN
|
||||
) # xxh3-128 hex
|
||||
|
||||
def test_cuda_and_cpu_digests_agree(self):
|
||||
# Streaming D2H + host xxh3 must match a full CPU hash of the same
|
||||
# bytes; otherwise dump/reload verify would false-fail across devices.
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("CUDA required")
|
||||
cpu = torch.arange(10_000, dtype=torch.float32)
|
||||
gpu = cpu.cuda()
|
||||
self.assertEqual(
|
||||
PreshardedModelLoader._hash_tensor(cpu),
|
||||
PreshardedModelLoader._hash_tensor(gpu),
|
||||
)
|
||||
|
||||
|
||||
class TestPreshardedFilename(unittest.TestCase):
|
||||
def test_common_filename(self):
|
||||
self.assertEqual(
|
||||
PreshardedModelLoader._make_filename(0, (0, 1, 2, 3), is_common=True),
|
||||
"model-00000-common.safetensor",
|
||||
)
|
||||
|
||||
def test_rank_filename_three_digit_padding(self):
|
||||
self.assertEqual(
|
||||
PreshardedModelLoader._make_filename(5, (1, 3, 5, 7), is_common=False),
|
||||
"model-00005-rank-001,003,005,007.safetensor",
|
||||
)
|
||||
|
||||
def test_file_id_zero_padding(self):
|
||||
self.assertTrue(
|
||||
PreshardedModelLoader._make_filename(42, (0,), is_common=False).startswith(
|
||||
"model-00042-"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestBuildDumpPlan(unittest.TestCase):
|
||||
def _write_manifests(self, tmp_dir, manifests):
|
||||
for r, m in manifests.items():
|
||||
with open(os.path.join(tmp_dir, f"manifest_{r:05d}.json"), "w") as f:
|
||||
json.dump(m, f)
|
||||
|
||||
def test_dedup_across_ranks(self):
|
||||
# Same content (same checksum) on all 4 ranks → single file marked common.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
shared = {
|
||||
"checksum": "deadbeef",
|
||||
"size": 1024,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [256],
|
||||
}
|
||||
manifests = {r: {"shared.weight": shared} for r in range(4)}
|
||||
self._write_manifests(tmp, manifests)
|
||||
plan = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=4, tmp_dir=tmp, max_file_bytes=10**12
|
||||
)
|
||||
self.assertEqual(len(plan["files"]), 1)
|
||||
self.assertTrue(plan["files"][0]["is_common"])
|
||||
self.assertIn("common", plan["files"][0]["filename"])
|
||||
# Each rank should still have a read entry pointing at the file.
|
||||
for r in range(4):
|
||||
reads = plan["rank_to_reads"][str(r)]
|
||||
self.assertEqual(len(reads), 1)
|
||||
self.assertEqual(reads[0]["name"], "shared.weight")
|
||||
self.assertEqual(reads[0]["filename"], plan["files"][0]["filename"])
|
||||
self.assertEqual(reads[0]["stored_key"], "deadbeef")
|
||||
self.assertIn("rank_checksums", plan)
|
||||
self.assertEqual(set(plan["rank_checksums"].keys()), {"0", "1", "2", "3"})
|
||||
|
||||
def test_per_rank_unique_tensors(self):
|
||||
# Each rank has its own tensor (different content). 4 distinct files,
|
||||
# filenames should be rank-{rrr}.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
manifests = {
|
||||
r: {
|
||||
"layer.weight": {
|
||||
"checksum": f"hash_{r}",
|
||||
"size": 2048,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [512],
|
||||
}
|
||||
}
|
||||
for r in range(4)
|
||||
}
|
||||
self._write_manifests(tmp, manifests)
|
||||
plan = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=4, tmp_dir=tmp, max_file_bytes=10**12
|
||||
)
|
||||
self.assertEqual(len(plan["files"]), 4)
|
||||
for f in plan["files"]:
|
||||
self.assertFalse(f["is_common"])
|
||||
self.assertEqual(len(f["rank_list"]), 1)
|
||||
# Writer is the only rank in the rank_list.
|
||||
self.assertEqual(f["writer_rank"], f["rank_list"][0])
|
||||
self.assertIn(f"-rank-{f['rank_list'][0]:03d}.safetensor", f["filename"])
|
||||
|
||||
def test_partial_share_has_correct_rank_list(self):
|
||||
# Tensor shared by ranks 1,3,5,7 only.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
shared = {
|
||||
"checksum": "shared_hash",
|
||||
"size": 1024,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [256],
|
||||
}
|
||||
manifests = {
|
||||
r: ({"x": shared} if r in (1, 3, 5, 7) else {}) for r in range(8)
|
||||
}
|
||||
self._write_manifests(tmp, manifests)
|
||||
plan = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=8, tmp_dir=tmp, max_file_bytes=10**12
|
||||
)
|
||||
self.assertEqual(len(plan["files"]), 1)
|
||||
f = plan["files"][0]
|
||||
self.assertFalse(f["is_common"])
|
||||
self.assertEqual(f["rank_list"], [1, 3, 5, 7])
|
||||
self.assertIn(f["writer_rank"], (1, 3, 5, 7))
|
||||
self.assertEqual(f["filename"], "model-00000-rank-001,003,005,007.safetensor")
|
||||
# Only the 4 sharing ranks have read entries.
|
||||
for r in range(8):
|
||||
reads = plan["rank_to_reads"].get(str(r), [])
|
||||
if r in (1, 3, 5, 7):
|
||||
self.assertEqual(len(reads), 1)
|
||||
else:
|
||||
self.assertEqual(len(reads), 0)
|
||||
|
||||
def test_max_file_size_caps_files(self):
|
||||
# Two tensors of 1 MiB each shared by all 2 ranks; cap = 1.5 MiB →
|
||||
# two files.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
t1 = {
|
||||
"checksum": "h1",
|
||||
"size": 1024 * 1024,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [256, 1024],
|
||||
}
|
||||
t2 = dict(t1, checksum="h2")
|
||||
manifests = {0: {"a": t1, "b": t2}, 1: {"a": t1, "b": t2}}
|
||||
self._write_manifests(tmp, manifests)
|
||||
plan = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=2,
|
||||
tmp_dir=tmp,
|
||||
max_file_bytes=int(1.5 * 1024 * 1024),
|
||||
)
|
||||
# Both tensors share the same rank_list (0,1). With balanced packing,
|
||||
# each writer (rank 0 and rank 1) gets one tensor → 2 files.
|
||||
self.assertEqual(len(plan["files"]), 2)
|
||||
for f in plan["files"]:
|
||||
self.assertEqual(len(f["tensors"]), 1)
|
||||
# rank_list is full world ⇒ marked common
|
||||
self.assertTrue(f["is_common"])
|
||||
|
||||
def test_workload_balanced_within_rank_list(self):
|
||||
# 4 same-size tensors all shared by ranks (0,1,2,3) → with balanced
|
||||
# packing each writer rank should get exactly one tensor.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tensors = {
|
||||
f"t{i}": {
|
||||
"checksum": f"h{i}",
|
||||
"size": 1024,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [256],
|
||||
}
|
||||
for i in range(4)
|
||||
}
|
||||
manifests = {r: dict(tensors) for r in range(4)}
|
||||
self._write_manifests(tmp, manifests)
|
||||
plan = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=4,
|
||||
tmp_dir=tmp,
|
||||
max_file_bytes=10**12,
|
||||
)
|
||||
# 4 tensors, 4 writers, 4 files (one per writer).
|
||||
self.assertEqual(len(plan["files"]), 4)
|
||||
writers = sorted(f["writer_rank"] for f in plan["files"])
|
||||
self.assertEqual(writers, [0, 1, 2, 3])
|
||||
|
||||
def test_round_trip_dump_and_read_back(self):
|
||||
# End-to-end on disk for world_size=1: build manifests for tensors,
|
||||
# construct plan, write safetensors per the plan, read back and
|
||||
# verify both checksums and bit-identity.
|
||||
from safetensors.torch import safe_open, save_file
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tensors = {
|
||||
"embed.weight": torch.arange(64, dtype=torch.float32).reshape(8, 8),
|
||||
"norm.weight": torch.full((16,), 0.5, dtype=torch.float32),
|
||||
"head.weight": torch.linspace(-1, 1, 32, dtype=torch.float32),
|
||||
}
|
||||
manifest_dir = os.path.join(tmp, "manifests")
|
||||
presharded_dir = os.path.join(tmp, "presharded")
|
||||
os.makedirs(manifest_dir)
|
||||
os.makedirs(presharded_dir)
|
||||
|
||||
checksums = {
|
||||
name: PreshardedModelLoader._hash_tensor(t)
|
||||
for name, t in tensors.items()
|
||||
}
|
||||
manifest = {
|
||||
name: {
|
||||
"checksum": checksums[name],
|
||||
"size": t.numel() * t.element_size(),
|
||||
"dtype": str(t.dtype),
|
||||
"shape": list(t.shape),
|
||||
}
|
||||
for name, t in tensors.items()
|
||||
}
|
||||
with open(os.path.join(manifest_dir, "manifest_00000.json"), "w") as f:
|
||||
json.dump(manifest, f)
|
||||
|
||||
plan = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=1, tmp_dir=manifest_dir, max_file_bytes=10**12
|
||||
)
|
||||
|
||||
# Single rank → all tensors live in one common file.
|
||||
self.assertEqual(len(plan["files"]), 1)
|
||||
f = plan["files"][0]
|
||||
self.assertTrue(f["is_common"])
|
||||
|
||||
# Write the file with stored_keys mapped to tensor content.
|
||||
to_save = {}
|
||||
for t_entry in f["tensors"]:
|
||||
name = t_entry["rank_to_names"]["0"][0]
|
||||
to_save[t_entry["stored_key"]] = tensors[name]
|
||||
save_file(to_save, os.path.join(presharded_dir, f["filename"]))
|
||||
|
||||
# Read back: verify each tensor's checksum and content.
|
||||
with safe_open(
|
||||
os.path.join(presharded_dir, f["filename"]), framework="pt"
|
||||
) as fh:
|
||||
for r in plan["rank_to_reads"]["0"]:
|
||||
loaded = fh.get_tensor(r["stored_key"])
|
||||
self.assertEqual(
|
||||
PreshardedModelLoader._hash_tensor(loaded), r["stored_key"]
|
||||
)
|
||||
torch.testing.assert_close(loaded, tensors[r["name"]])
|
||||
|
||||
def test_dedup_with_multiple_names_per_rank(self):
|
||||
# Same checksum can appear under MULTIPLE param names on the same
|
||||
# rank (e.g., k_scale and v_scale both default to 1.0). Both names
|
||||
# must end up in rank_to_reads.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
shared = {
|
||||
"checksum": "scale_hash",
|
||||
"size": 4,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [],
|
||||
}
|
||||
manifests = {
|
||||
0: {
|
||||
"layers.0.attn.k_scale": shared,
|
||||
"layers.0.attn.v_scale": shared,
|
||||
"layers.1.attn.k_scale": shared,
|
||||
},
|
||||
1: {
|
||||
"layers.0.attn.k_scale": shared,
|
||||
"layers.0.attn.v_scale": shared,
|
||||
"layers.1.attn.k_scale": shared,
|
||||
},
|
||||
}
|
||||
self._write_manifests(tmp, manifests)
|
||||
plan = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=2, tmp_dir=tmp, max_file_bytes=10**12
|
||||
)
|
||||
# All 6 (rank,name) pairs must be readable, even though there is one
|
||||
# underlying tensor stored on disk.
|
||||
self.assertEqual(len(plan["files"]), 1)
|
||||
for r in (0, 1):
|
||||
reads = plan["rank_to_reads"][str(r)]
|
||||
names = sorted(rd["name"] for rd in reads)
|
||||
self.assertEqual(
|
||||
names,
|
||||
[
|
||||
"layers.0.attn.k_scale",
|
||||
"layers.0.attn.v_scale",
|
||||
"layers.1.attn.k_scale",
|
||||
],
|
||||
)
|
||||
# All point at the same stored_key (deduplicated content).
|
||||
self.assertEqual({rd["stored_key"] for rd in reads}, {"scale_hash"})
|
||||
|
||||
def test_collision_size_mismatch_raises(self):
|
||||
# Same checksum but different sizes ⇒ plan builder rejects.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
manifests = {
|
||||
0: {
|
||||
"x": {
|
||||
"checksum": "same",
|
||||
"size": 1024,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [256],
|
||||
}
|
||||
},
|
||||
1: {
|
||||
"x": {
|
||||
"checksum": "same",
|
||||
"size": 2048,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [512],
|
||||
}
|
||||
},
|
||||
}
|
||||
self._write_manifests(tmp, manifests)
|
||||
with self.assertRaises(RuntimeError):
|
||||
PreshardedModelLoader._build_dump_plan(
|
||||
world_size=2, tmp_dir=tmp, max_file_bytes=10**12
|
||||
)
|
||||
|
||||
def test_rank_checksum_deterministic(self):
|
||||
# rank_checksums must be reproducible from the same manifest input
|
||||
# and depend on (name, content-SHA) pairs of every tensor a rank
|
||||
# owns. Permuting the manifest's insertion order must not change
|
||||
# the rank checksum.
|
||||
with tempfile.TemporaryDirectory() as tmp_a, tempfile.TemporaryDirectory() as tmp_b:
|
||||
base_entries = {
|
||||
"alpha.weight": {
|
||||
"checksum": "h_alpha",
|
||||
"size": 16,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [4],
|
||||
},
|
||||
"beta.weight": {
|
||||
"checksum": "h_beta",
|
||||
"size": 16,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [4],
|
||||
},
|
||||
}
|
||||
self._write_manifests(tmp_a, {0: dict(base_entries)})
|
||||
# Insertion-order-permuted copy.
|
||||
permuted = {k: base_entries[k] for k in reversed(list(base_entries))}
|
||||
self._write_manifests(tmp_b, {0: permuted})
|
||||
plan_a = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=1, tmp_dir=tmp_a, max_file_bytes=10**12
|
||||
)
|
||||
plan_b = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=1, tmp_dir=tmp_b, max_file_bytes=10**12
|
||||
)
|
||||
self.assertEqual(plan_a["rank_checksums"], plan_b["rank_checksums"])
|
||||
|
||||
def test_rank_checksum_distinguishes_content(self):
|
||||
# Changing one tensor's content-SHA must change the rank checksum.
|
||||
with tempfile.TemporaryDirectory() as tmp_a, tempfile.TemporaryDirectory() as tmp_b:
|
||||
entries_a = {
|
||||
"x.weight": {
|
||||
"checksum": "ha",
|
||||
"size": 16,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [4],
|
||||
},
|
||||
}
|
||||
entries_b = {
|
||||
"x.weight": {
|
||||
"checksum": "hb", # different content
|
||||
"size": 16,
|
||||
"dtype": "torch.float32",
|
||||
"shape": [4],
|
||||
},
|
||||
}
|
||||
self._write_manifests(tmp_a, {0: entries_a})
|
||||
self._write_manifests(tmp_b, {0: entries_b})
|
||||
plan_a = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=1, tmp_dir=tmp_a, max_file_bytes=10**12
|
||||
)
|
||||
plan_b = PreshardedModelLoader._build_dump_plan(
|
||||
world_size=1, tmp_dir=tmp_b, max_file_bytes=10**12
|
||||
)
|
||||
self.assertNotEqual(
|
||||
plan_a["rank_checksums"]["0"],
|
||||
plan_b["rank_checksums"]["0"],
|
||||
)
|
||||
|
||||
def test_presharded_ready_sentinel(self):
|
||||
# Loader treats a dir as a valid presharded ckpt only when the
|
||||
# READY sentinel exists. A bare checksum.json (or partial files)
|
||||
# is NOT enough.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
self.assertFalse(PreshardedModelLoader._presharded_ready(tmp))
|
||||
# checksum.json alone is not sufficient.
|
||||
with open(
|
||||
os.path.join(tmp, PreshardedModelLoader.CHECKSUM_FILENAME), "w"
|
||||
) as f:
|
||||
f.write("{}")
|
||||
self.assertFalse(PreshardedModelLoader._presharded_ready(tmp))
|
||||
# READY makes it ready.
|
||||
with open(
|
||||
os.path.join(tmp, PreshardedModelLoader.READY_FILENAME), "w"
|
||||
) as f:
|
||||
f.write("{}")
|
||||
self.assertTrue(PreshardedModelLoader._presharded_ready(tmp))
|
||||
|
||||
def test_separate_presharded_path_overrides_for_target_and_draft(self):
|
||||
# Target and draft get distinct roots via presharded_path vs
|
||||
# draft_presharded_path. Failure mode if this regresses: draft
|
||||
# re-dump collides with / wipes target READY under a shared path.
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
target_root = os.path.join(tmp, "target_cache")
|
||||
draft_root = os.path.join(tmp, "draft_cache")
|
||||
loader._presharded_path_override = target_root
|
||||
loader._draft_presharded_path_override = draft_root
|
||||
cfg = {"tp": 8, "structural_signature": "same_sig"}
|
||||
sub = loader._build_subfolder_name(cfg)
|
||||
target_mc = SimpleNamespace(model_path="/models/dsv3", is_draft_model=False)
|
||||
draft_mc = SimpleNamespace(model_path="/models/dsv3", is_draft_model=True)
|
||||
target_dir = loader._presharded_dir(target_mc, cfg)
|
||||
draft_dir = loader._presharded_dir(draft_mc, cfg)
|
||||
self.assertEqual(target_dir, os.path.join(target_root, sub))
|
||||
self.assertEqual(draft_dir, os.path.join(draft_root, sub))
|
||||
self.assertNotEqual(target_dir, draft_dir)
|
||||
|
||||
# Target-only override: draft must not fall back into the target
|
||||
# root (same model_path is common for DeepSeek MTP).
|
||||
loader._draft_presharded_path_override = None
|
||||
draft_fallback = loader._presharded_dir(draft_mc, cfg)
|
||||
self.assertEqual(
|
||||
draft_fallback,
|
||||
os.path.join(
|
||||
"/models/dsv3",
|
||||
PreshardedModelLoader.DEFAULT_SUBDIR,
|
||||
sub,
|
||||
),
|
||||
)
|
||||
self.assertFalse(draft_fallback.startswith(target_root))
|
||||
|
||||
# No overrides: both use model_path/presharded/<subfolder>.
|
||||
loader._presharded_path_override = None
|
||||
self.assertEqual(
|
||||
loader._presharded_dir(target_mc, cfg),
|
||||
os.path.join(
|
||||
"/models/dsv3",
|
||||
PreshardedModelLoader.DEFAULT_SUBDIR,
|
||||
sub,
|
||||
),
|
||||
)
|
||||
|
||||
def test_apply_shape_mismatch_raises(self):
|
||||
# Reload must not silently copy_ into a wrong layout when process
|
||||
# shapes and dumped tensors disagree (previously only warned).
|
||||
# Use a *larger* dumped tensor so prefix-narrow does not paper over it.
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
loader._verify_on_load = False
|
||||
param = torch.nn.Parameter(torch.zeros(2, 2))
|
||||
state_dict = {"w": param}
|
||||
items = [{"name": "w", "stored_key": "w", "is_extra": False}]
|
||||
cached = {"w": torch.ones(4, 4)}
|
||||
loaded: set = set()
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
loader._apply_presharded_file(
|
||||
items=items,
|
||||
cached=cached,
|
||||
model=torch.nn.Module(),
|
||||
state_dict=state_dict,
|
||||
target_device=torch.device("cpu"),
|
||||
loaded_param_keys=loaded,
|
||||
verify_hashes=[],
|
||||
)
|
||||
self.assertIn("shape mismatch", str(ctx.exception).lower())
|
||||
|
||||
def test_build_dump_plan_missing_manifest_mentions_shared_fs(self):
|
||||
# Multi-node without a shared dump dir fails at plan build with a
|
||||
# clear message (not a bare FileNotFoundError path).
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with self.assertRaises(FileNotFoundError) as ctx:
|
||||
PreshardedModelLoader._build_dump_plan(
|
||||
world_size=2, tmp_dir=tmp, max_file_bytes=1024
|
||||
)
|
||||
msg = str(ctx.exception)
|
||||
self.assertIn("Rank 0", msg)
|
||||
self.assertIn("shared", msg.lower())
|
||||
|
||||
def test_ensure_presharded_dir_writable_rejects_readonly(self):
|
||||
# Guard against spending a full source load before discovering a
|
||||
# read-only dump root (HF cache mounts). Mock OSError because root
|
||||
# can often still write to mode-0555 dirs under CAP_DAC_OVERRIDE.
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
with mock.patch.object(
|
||||
loader, "_world_rank_and_size", return_value=(0, 1)
|
||||
), mock.patch.object(loader, "_world_barrier"), mock.patch(
|
||||
"os.makedirs", side_effect=OSError("Read-only file system")
|
||||
):
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
loader._ensure_presharded_dir_writable("/ro/presharded")
|
||||
self.assertIn("not writable", str(ctx.exception).lower())
|
||||
self.assertIn("presharded_path", str(ctx.exception))
|
||||
|
||||
def test_ensure_presharded_dir_writable_ok_rank0(self):
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
leaf = os.path.join(tmp, "TP-8-sig-test")
|
||||
with mock.patch.object(
|
||||
loader, "_world_rank_and_size", return_value=(0, 1)
|
||||
), mock.patch.object(loader, "_world_barrier") as barrier:
|
||||
loader._ensure_presharded_dir_writable(leaf)
|
||||
self.assertTrue(os.path.isdir(leaf))
|
||||
barrier.assert_called_once()
|
||||
|
||||
|
||||
class TestStructuralSignature(unittest.TestCase):
|
||||
"""The structural signature is the long-term fix for the underlying
|
||||
problem `moe_dense_tp_size` was an instance of: instead of hand-
|
||||
enumerating every parallelism knob that might change a rank's tensor
|
||||
shapes, hash the (name, shape, dtype) of every parameter in a
|
||||
meta-device model skeleton built under the live parallel state. Any
|
||||
future sharding knob that changes shapes is automatically caught
|
||||
without touching `_build_subfolder_name`."""
|
||||
|
||||
def test_hash_changes_with_shape_dtype_or_added_tensor(self):
|
||||
# Regression: a future sharding knob that changes shapes/dtypes or
|
||||
# adds per-rank buffers must change the digest, otherwise cache
|
||||
# collisions load wrong weights. One case covers the three axes that
|
||||
# `_hash_structural_signature` is responsible for.
|
||||
base = [("a.weight", (4, 4), "torch.float32")]
|
||||
shape = [("a.weight", (4, 8), "torch.float32")]
|
||||
dtype = [("a.weight", (4, 4), "torch.float16")]
|
||||
extra = [
|
||||
("a.weight", (4, 4), "torch.float32"),
|
||||
("a.extra_buf", (4,), "torch.float32"),
|
||||
]
|
||||
h = PreshardedModelLoader._hash_structural_signature
|
||||
self.assertNotEqual(h(base), h(shape))
|
||||
self.assertNotEqual(h(base), h(dtype))
|
||||
self.assertNotEqual(h(base), h(extra))
|
||||
|
||||
def test_local_signature_sorts_state_dict_order(self):
|
||||
# Production path sorts state_dict items before hashing. If that
|
||||
# sorted(...) is dropped, two identical models whose state_dict
|
||||
# iteration order differs would get different signatures and thrash
|
||||
# the cache. Guard the sort, not just the pure hash helper.
|
||||
import torch.nn as nn
|
||||
|
||||
class OrderedModule(nn.Module):
|
||||
def __init__(self, order):
|
||||
super().__init__()
|
||||
for name in order:
|
||||
self.register_parameter(
|
||||
name, nn.Parameter(torch.zeros(2, 2), requires_grad=False)
|
||||
)
|
||||
|
||||
def _init_stub(model_config, load_config, quant_config):
|
||||
return OrderedModule(model_config.order)
|
||||
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
loader.load_config = SimpleNamespace()
|
||||
with mock.patch(
|
||||
"sglang.srt.model_loader.loader._get_quantization_config",
|
||||
return_value=None,
|
||||
), mock.patch(
|
||||
"sglang.srt.model_loader.loader._initialize_model",
|
||||
side_effect=_init_stub,
|
||||
), mock.patch(
|
||||
"sglang.srt.model_loader.loader.set_default_torch_dtype",
|
||||
return_value=mock.MagicMock(__enter__=mock.Mock(), __exit__=mock.Mock()),
|
||||
):
|
||||
sig_ab = loader._compute_local_structural_signature(
|
||||
SimpleNamespace(
|
||||
quantization=None, dtype=torch.float32, order=["a", "b"]
|
||||
)
|
||||
)
|
||||
sig_ba = loader._compute_local_structural_signature(
|
||||
SimpleNamespace(
|
||||
quantization=None, dtype=torch.float32, order=["b", "a"]
|
||||
)
|
||||
)
|
||||
self.assertIsNotNone(sig_ab)
|
||||
self.assertEqual(sig_ab, sig_ba)
|
||||
|
||||
def test_rank_invariant_signature_aggregates_per_rank_locals(self):
|
||||
# Under PP, ranks build different local digests. The shared cache key
|
||||
# must still agree across ranks: all-gather then hash the ordered
|
||||
# list. Without this, each PP stage would pick a different
|
||||
# presharded subfolder and the multi-rank dump protocol breaks.
|
||||
fake_group = mock.Mock()
|
||||
fake_group.world_size = 2
|
||||
# Simulate two ranks each calling with their own local sig; both
|
||||
# must see the same gathered list and thus the same aggregate.
|
||||
fake_group.all_gather_object.side_effect = lambda local: ["sig-pp0", "sig-pp1"]
|
||||
|
||||
with mock.patch(
|
||||
"sglang.srt.distributed.get_world_group", return_value=fake_group
|
||||
):
|
||||
agg_from_rank0 = (
|
||||
PreshardedModelLoader._make_rank_invariant_structural_signature(
|
||||
"sig-pp0"
|
||||
)
|
||||
)
|
||||
agg_from_rank1 = (
|
||||
PreshardedModelLoader._make_rank_invariant_structural_signature(
|
||||
"sig-pp1"
|
||||
)
|
||||
)
|
||||
self.assertIsNotNone(agg_from_rank0)
|
||||
self.assertEqual(agg_from_rank0, agg_from_rank1)
|
||||
# Changing any rank's local contribution must change the aggregate.
|
||||
fake_group.all_gather_object.side_effect = lambda local: [
|
||||
"sig-pp0",
|
||||
"sig-pp1-changed",
|
||||
]
|
||||
with mock.patch(
|
||||
"sglang.srt.distributed.get_world_group", return_value=fake_group
|
||||
):
|
||||
agg_changed = (
|
||||
PreshardedModelLoader._make_rank_invariant_structural_signature(
|
||||
"sig-pp0"
|
||||
)
|
||||
)
|
||||
self.assertNotEqual(agg_from_rank0, agg_changed)
|
||||
|
||||
def test_compute_structural_signature_returns_none_on_failure(self):
|
||||
# No self.load_config (and no real model class behind
|
||||
# SimpleNamespace) means _get_quantization_config / _initialize_model
|
||||
# will raise; this must be swallowed and return None, never raise,
|
||||
# since a model class that can't be built on meta device must not
|
||||
# break the overall load.
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
model_config = SimpleNamespace(quantization=None)
|
||||
self.assertIsNone(loader._compute_structural_signature(model_config))
|
||||
|
||||
def test_compute_structural_signature_picks_up_meta_model_shapes(self):
|
||||
# End-to-end on a tiny real nn.Module standing in for the model
|
||||
# class, to prove the meta-device construction + hashing wiring
|
||||
# actually reflects shapes that depend on the live parallel state
|
||||
# (here simulated via a width captured at construction time).
|
||||
import torch.nn as nn
|
||||
|
||||
class FakeModelLoader(PreshardedModelLoader):
|
||||
def __init__(self, width):
|
||||
self._width = width
|
||||
self.load_config = SimpleNamespace()
|
||||
|
||||
def _initialize_model_stub(model_config, load_config, quant_config):
|
||||
return nn.Linear(4, model_config.width, bias=False)
|
||||
|
||||
with mock.patch(
|
||||
"sglang.srt.model_loader.loader._get_quantization_config",
|
||||
return_value=None,
|
||||
), mock.patch(
|
||||
"sglang.srt.model_loader.loader._initialize_model",
|
||||
side_effect=_initialize_model_stub,
|
||||
), mock.patch(
|
||||
"sglang.srt.model_loader.loader.set_default_torch_dtype",
|
||||
return_value=mock.MagicMock(__enter__=mock.Mock(), __exit__=mock.Mock()),
|
||||
):
|
||||
loader_narrow = FakeModelLoader(width=2)
|
||||
loader_wide = FakeModelLoader(width=8)
|
||||
model_config_narrow = SimpleNamespace(
|
||||
quantization=None, dtype=torch.float32, width=2
|
||||
)
|
||||
model_config_wide = SimpleNamespace(
|
||||
quantization=None, dtype=torch.float32, width=8
|
||||
)
|
||||
sig_narrow = loader_narrow._compute_structural_signature(
|
||||
model_config_narrow
|
||||
)
|
||||
sig_wide = loader_wide._compute_structural_signature(model_config_wide)
|
||||
self.assertIsNotNone(sig_narrow)
|
||||
self.assertIsNotNone(sig_wide)
|
||||
self.assertNotEqual(sig_narrow, sig_wide)
|
||||
|
||||
def test_meta_rope_cache_cleared_even_on_failure(self):
|
||||
# If _initialize_model partially populates _ROPE_DICT with meta-device
|
||||
# entries before raising, _compute_structural_signature must still
|
||||
# clean them up (via finally), otherwise the real model init reuses
|
||||
# the meta module and fails with "Cannot copy out of meta tensor".
|
||||
import torch.nn as nn
|
||||
|
||||
from sglang.srt.layers.rotary_embedding.factory import _ROPE_DICT
|
||||
|
||||
# Plant a fake meta-device rotary module in the global cache.
|
||||
fake_key = ("_test_meta_rope_cleanup_sentinel",)
|
||||
with torch.device("meta"):
|
||||
fake_module = nn.Linear(4, 4)
|
||||
_ROPE_DICT[fake_key] = fake_module
|
||||
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
loader.load_config = SimpleNamespace()
|
||||
|
||||
with mock.patch(
|
||||
"sglang.srt.model_loader.loader._get_quantization_config",
|
||||
return_value=None,
|
||||
), mock.patch(
|
||||
"sglang.srt.model_loader.loader._initialize_model",
|
||||
side_effect=RuntimeError("simulated init failure"),
|
||||
), mock.patch(
|
||||
"sglang.srt.model_loader.loader.set_default_torch_dtype",
|
||||
return_value=mock.MagicMock(__enter__=mock.Mock(), __exit__=mock.Mock()),
|
||||
):
|
||||
result = loader._compute_structural_signature(
|
||||
SimpleNamespace(quantization=None, dtype=torch.float32)
|
||||
)
|
||||
|
||||
self.assertIsNone(result)
|
||||
self.assertNotIn(fake_key, _ROPE_DICT)
|
||||
|
||||
|
||||
class TestShardConfig(unittest.TestCase):
|
||||
"""Bookkeeping guards for the enumerated cache-key fields and the
|
||||
load-time match path. Dropping a field from `_collect_shard_config` or
|
||||
breaking `_shard_config_matches` would silently collide caches; these
|
||||
cases pin the failure modes the PR is meant to prevent."""
|
||||
|
||||
def _base_config(self, **overrides):
|
||||
cfg = {
|
||||
"tp": 8,
|
||||
"dp": 1,
|
||||
"ep": 1,
|
||||
"pp": 1,
|
||||
"moe_dense_tp_size": None,
|
||||
"moe_dp_size": 1,
|
||||
"enable_dp_lm_head": False,
|
||||
"enable_fp32_lm_head": False,
|
||||
"quantization": None,
|
||||
"model_dtype": "torch.bfloat16",
|
||||
"ep_num_redundant_experts": 0,
|
||||
"enable_eplb": False,
|
||||
"init_expert_location": "trivial",
|
||||
"structural_signature": "deadbeef",
|
||||
}
|
||||
cfg.update(overrides)
|
||||
return cfg
|
||||
|
||||
def test_collect_shard_config_includes_required_keys(self):
|
||||
# Registry completeness: dropping a key from the dict literal in
|
||||
# `_collect_shard_config` is the exact failure mode that left
|
||||
# moe_dense_tp_size / LM-head flags out of the cache key before.
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
server_args = SimpleNamespace(
|
||||
moe_dense_tp_size=1,
|
||||
moe_dp_size=2,
|
||||
enable_dp_lm_head=True,
|
||||
enable_fp32_lm_head=True,
|
||||
ep_num_redundant_experts=4,
|
||||
enable_eplb=True,
|
||||
init_expert_location="trivial",
|
||||
)
|
||||
model_config = SimpleNamespace(quantization="fp8", dtype=torch.bfloat16)
|
||||
required = {
|
||||
"tp",
|
||||
"dp",
|
||||
"ep",
|
||||
"pp",
|
||||
"moe_dense_tp_size",
|
||||
"moe_dp_size",
|
||||
"enable_dp_lm_head",
|
||||
"enable_fp32_lm_head",
|
||||
"quantization",
|
||||
"model_dtype",
|
||||
"ep_num_redundant_experts",
|
||||
"enable_eplb",
|
||||
"init_expert_location",
|
||||
"structural_signature",
|
||||
}
|
||||
parallel = SimpleNamespace(tp_size=8, moe_dp_size=2, moe_ep_size=4, pp_size=1)
|
||||
with mock.patch(
|
||||
"sglang.srt.model_loader.loader.get_server_args",
|
||||
return_value=server_args,
|
||||
), mock.patch(
|
||||
"sglang.srt.model_loader.loader.get_parallel",
|
||||
return_value=parallel,
|
||||
), mock.patch.object(
|
||||
loader, "_compute_structural_signature", return_value="sig16"
|
||||
):
|
||||
cfg = loader._collect_shard_config(model_config)
|
||||
self.assertEqual(required, set(cfg.keys()))
|
||||
self.assertEqual(cfg["tp"], 8)
|
||||
self.assertEqual(cfg["dp"], 2)
|
||||
self.assertEqual(cfg["ep"], 4)
|
||||
self.assertEqual(cfg["pp"], 1)
|
||||
self.assertEqual(cfg["moe_dense_tp_size"], 1)
|
||||
self.assertEqual(cfg["moe_dp_size"], 2)
|
||||
self.assertTrue(cfg["enable_dp_lm_head"])
|
||||
self.assertTrue(cfg["enable_fp32_lm_head"])
|
||||
self.assertEqual(cfg["init_expert_location"], "trivial")
|
||||
|
||||
def test_enumerated_fields_change_subfolder_hash(self):
|
||||
# Each enumerated content/shape knob must feed the subfolder name.
|
||||
# A silent drop from `_collect_shard_config` would leave this red.
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
base = self._base_config()
|
||||
base_name = loader._build_subfolder_name(base)
|
||||
field_variants = {
|
||||
"moe_dense_tp_size": 1,
|
||||
"moe_dp_size": 2,
|
||||
"enable_dp_lm_head": True,
|
||||
"enable_fp32_lm_head": True,
|
||||
"ep_num_redundant_experts": 8,
|
||||
"enable_eplb": True,
|
||||
"init_expert_location": "file:map.json:sha1:abcd",
|
||||
"structural_signature": "cafebabe",
|
||||
"quantization": "fp8",
|
||||
}
|
||||
for field, value in field_variants.items():
|
||||
with self.subTest(field=field):
|
||||
other = self._base_config(**{field: value})
|
||||
other_name = loader._build_subfolder_name(other)
|
||||
self.assertNotEqual(
|
||||
base_name,
|
||||
other_name,
|
||||
f"changing {field} must change the cache subfolder name",
|
||||
)
|
||||
|
||||
def test_shard_config_matches_equality_and_missing(self):
|
||||
# Match must require exact stored equality; missing/mismatched
|
||||
# shard_config is a cache miss (never raises) so reload re-dumps.
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
cfg = self._base_config()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
# No checksum.json → miss.
|
||||
self.assertFalse(loader._shard_config_matches(tmp, cfg))
|
||||
# Matching stored config → hit.
|
||||
with open(
|
||||
os.path.join(tmp, PreshardedModelLoader.CHECKSUM_FILENAME), "w"
|
||||
) as f:
|
||||
json.dump({"shard_config": cfg}, f)
|
||||
self.assertTrue(loader._shard_config_matches(tmp, cfg))
|
||||
# One field differs → miss.
|
||||
mismatched = self._base_config(moe_dense_tp_size=1)
|
||||
self.assertFalse(loader._shard_config_matches(tmp, mismatched))
|
||||
# Plan without shard_config key → miss (upgrade path).
|
||||
with open(
|
||||
os.path.join(tmp, PreshardedModelLoader.CHECKSUM_FILENAME), "w"
|
||||
) as f:
|
||||
json.dump({"version": 1}, f)
|
||||
self.assertFalse(loader._shard_config_matches(tmp, cfg))
|
||||
|
||||
def test_init_expert_location_hashes_file_contents(self):
|
||||
# Overwriting the same path with a different expert map must bust
|
||||
# the cache; keying on the path alone would silently reuse weights.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = os.path.join(tmp, "experts.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump({"logical_count": [[1, 0], [0, 1]]}, f)
|
||||
key_a = PreshardedModelLoader._normalize_init_expert_location(path)
|
||||
with open(path, "w") as f:
|
||||
json.dump({"logical_count": [[0, 1], [1, 0]]}, f)
|
||||
key_b = PreshardedModelLoader._normalize_init_expert_location(path)
|
||||
self.assertNotEqual(key_a, key_b)
|
||||
self.assertTrue(key_a.startswith("file:experts.json:sha1:"))
|
||||
self.assertEqual(
|
||||
PreshardedModelLoader._normalize_init_expert_location("trivial"),
|
||||
"trivial",
|
||||
)
|
||||
|
||||
def test_redump_clears_ready_before_rewrite(self):
|
||||
# Config-mismatch re-dump into an already-ready dir must drop READY
|
||||
# before mutating files, otherwise concurrent readers can observe
|
||||
# READY + partial checksum/safetensors.
|
||||
loader = object.__new__(PreshardedModelLoader)
|
||||
loader._hash_num_threads = 1
|
||||
loader._max_file_bytes = 10**12
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ready_path = os.path.join(tmp, PreshardedModelLoader.READY_FILENAME)
|
||||
with open(ready_path, "w") as f:
|
||||
f.write("{}")
|
||||
self.assertTrue(os.path.isfile(ready_path))
|
||||
|
||||
# Force rank 0 / world 1 so the method runs the rank-0 prologue
|
||||
# and then fails early on empty state (no need for full dump).
|
||||
with mock.patch.object(
|
||||
PreshardedModelLoader,
|
||||
"_world_rank_and_size",
|
||||
return_value=(0, 1),
|
||||
), mock.patch.object(
|
||||
PreshardedModelLoader, "_world_barrier", return_value=None
|
||||
), mock.patch.object(
|
||||
PreshardedModelLoader,
|
||||
"_build_dump_plan",
|
||||
return_value={
|
||||
"version": PreshardedModelLoader.PLAN_VERSION,
|
||||
"files": [],
|
||||
"rank_to_reads": {"0": []},
|
||||
"rank_checksums": {"0": "0"},
|
||||
},
|
||||
), mock.patch.object(
|
||||
PreshardedModelLoader, "_dump_files_for_rank", return_value=None
|
||||
):
|
||||
loader._dump_state_to_disk(
|
||||
state_dict={},
|
||||
extras={},
|
||||
presharded_dir=tmp,
|
||||
shard_config=self._base_config(),
|
||||
)
|
||||
# Dump rewrote READY at the end; the critical property is that
|
||||
# the prologue unlinked the *previous* READY before rewriting
|
||||
# checksum.json. Assert checksum was written and READY exists
|
||||
# only as the fresh sentinel from this dump.
|
||||
self.assertTrue(os.path.isfile(ready_path))
|
||||
with open(ready_path) as f:
|
||||
sentinel = json.load(f)
|
||||
self.assertIn("plan_version", sentinel)
|
||||
with open(os.path.join(tmp, PreshardedModelLoader.CHECKSUM_FILENAME)) as f:
|
||||
plan = json.load(f)
|
||||
self.assertEqual(plan["shard_config"]["tp"], 8)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user