[diffusion] refactor: resolve indexed component weight sets (#36883)
This commit is contained in:
@@ -25,9 +25,9 @@ from sglang.multimodal_gen.runtime.loader.minimax_h3_weights import (
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.transformer_load_utils import (
|
||||
TransformerQuantLoadSpec,
|
||||
resolve_transformer_checkpoint_files,
|
||||
resolve_transformer_gguf_to_load,
|
||||
resolve_transformer_quant_load_spec,
|
||||
resolve_transformer_safetensors_to_load,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import _normalize_component_type
|
||||
from sglang.multimodal_gen.runtime.loader.weight_load_plan import WeightLoadPlan
|
||||
@@ -214,10 +214,13 @@ class TransformerLoader(ComponentLoader):
|
||||
# A GGUF file holds the whole transformer; the remaining components
|
||||
# still load from the base model path.
|
||||
safetensors_list = []
|
||||
transformer_override_config_path = None
|
||||
else:
|
||||
safetensors_list = resolve_transformer_safetensors_to_load(
|
||||
checkpoint_files = resolve_transformer_checkpoint_files(
|
||||
component_server_args, component_model_path
|
||||
)
|
||||
safetensors_list = list(checkpoint_files.safetensors)
|
||||
transformer_override_config_path = checkpoint_files.config_path
|
||||
|
||||
# 2. dit config
|
||||
# Config from Diffusers supersedes sgl_diffusion's model config
|
||||
@@ -286,6 +289,7 @@ class TransformerLoader(ComponentLoader):
|
||||
component_name=component_name,
|
||||
gguf_file=gguf_file,
|
||||
checkpoint_quant_config=checkpoint_quant_config,
|
||||
transformer_override_config_path=transformer_override_config_path,
|
||||
)
|
||||
if quant_spec.gguf_file is not None and is_minimax_h3:
|
||||
assert quant_spec.quant_config is not None
|
||||
|
||||
@@ -45,11 +45,6 @@ from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import (
|
||||
hf_hub_download,
|
||||
maybe_download_model,
|
||||
snapshot_download,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.utils.precision import resolve_precision
|
||||
from sglang.multimodal_gen.runtime.utils.quantization_utils import (
|
||||
@@ -58,6 +53,11 @@ from sglang.multimodal_gen.runtime.utils.quantization_utils import (
|
||||
get_quant_config,
|
||||
get_quant_config_from_safetensors_metadata,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.weights.source import (
|
||||
materialize_weight_set,
|
||||
materialize_weight_set_config,
|
||||
resolve_safetensors_weight_set,
|
||||
)
|
||||
from sglang.srt.utils.hf_transformers import (
|
||||
check_gguf_file,
|
||||
resolve_hf_gguf_reference,
|
||||
@@ -71,11 +71,6 @@ _PRECISION_VARIANT_SUFFIX_RE = re.compile(
|
||||
r"^(?P<stem>.+?)(?P<precision>\.(?:fp16|bf16|fp32))(?P<shard>-\d+-of-\d+)?(?P<ext>\.safetensors)$"
|
||||
)
|
||||
_MIXED_SAFETENSORS_RE = re.compile(r".*-mixed(?:-\d+-of-\d+)?\.safetensors$")
|
||||
_HF_SAFETENSORS_URL_RE = re.compile(
|
||||
r"https?://huggingface\.co/(?P<repo>[^/]+/[^/]+)/"
|
||||
r"(?:blob|resolve)/(?P<revision>[^/]+)/(?P<filename>.+\.safetensors)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _get_quant_config_name(config: Optional[QuantizationConfig]) -> Optional[str]:
|
||||
@@ -213,6 +208,14 @@ class TransformerQuantLoadSpec:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransformerCheckpointFiles:
|
||||
"""Files from one checkpoint revision needed during transformer loading."""
|
||||
|
||||
safetensors: tuple[str, ...]
|
||||
config_path: str | None
|
||||
|
||||
|
||||
class _TransformerQuantAdapter:
|
||||
def prepare(self) -> None:
|
||||
"""initialize"""
|
||||
@@ -612,86 +615,55 @@ def resolve_transformer_gguf_to_load(
|
||||
return resolved
|
||||
|
||||
|
||||
def resolve_transformer_safetensors_to_load(
|
||||
def resolve_transformer_checkpoint_files(
|
||||
server_args: ServerArgs, component_model_path: str
|
||||
) -> list[str]:
|
||||
) -> TransformerCheckpointFiles:
|
||||
"""Resolve transformer weights from the base component path or an override."""
|
||||
quantized_path = server_args.transformer_weights_path
|
||||
|
||||
if quantized_path:
|
||||
original_quantized_path = quantized_path
|
||||
direct_url = _HF_SAFETENSORS_URL_RE.fullmatch(original_quantized_path)
|
||||
if direct_url is not None:
|
||||
quantized_path = hf_hub_download(
|
||||
repo_id=direct_url.group("repo"),
|
||||
filename=direct_url.group("filename"),
|
||||
revision=direct_url.group("revision"),
|
||||
)
|
||||
else:
|
||||
parts = original_quantized_path.strip("/").split("/")
|
||||
is_hub_file = (
|
||||
not os.path.exists(original_quantized_path)
|
||||
and not os.path.isabs(original_quantized_path)
|
||||
and not original_quantized_path.startswith((".", "~"))
|
||||
and len(parts) > 2
|
||||
and original_quantized_path.endswith(".safetensors")
|
||||
)
|
||||
quantized_path = (
|
||||
hf_hub_download(
|
||||
repo_id="/".join(parts[:2]),
|
||||
filename="/".join(parts[2:]),
|
||||
revision=server_args.revision,
|
||||
)
|
||||
if is_hub_file
|
||||
else maybe_download_model(original_quantized_path)
|
||||
)
|
||||
logger.info("using quantized transformer weights from: %s", quantized_path)
|
||||
if os.path.isfile(quantized_path) and quantized_path.endswith(".safetensors"):
|
||||
safetensors_list = [quantized_path]
|
||||
else:
|
||||
safetensors_list = _list_safetensors_files(quantized_path)
|
||||
if not safetensors_list and not os.path.exists(original_quantized_path):
|
||||
logger.warning(
|
||||
"No safetensors files found in cached transformer weights path "
|
||||
"%s; refreshing snapshot for %s",
|
||||
quantized_path,
|
||||
original_quantized_path,
|
||||
)
|
||||
quantized_path = snapshot_download(
|
||||
repo_id=original_quantized_path,
|
||||
ignore_patterns=["*.onnx", "*.msgpack"],
|
||||
allow_patterns=[
|
||||
"*.json",
|
||||
"*.safetensors",
|
||||
"*.safetensors.index.json",
|
||||
],
|
||||
max_workers=8,
|
||||
)
|
||||
safetensors_list = _list_safetensors_files(quantized_path)
|
||||
else:
|
||||
safetensors_list = _list_safetensors_files(component_model_path)
|
||||
resolved_set = resolve_safetensors_weight_set(
|
||||
quantized_path,
|
||||
revision=server_args.revision,
|
||||
select_unindexed_weight=_select_single_mixed_safetensors_file,
|
||||
)
|
||||
safetensors_list = materialize_weight_set(resolved_set)
|
||||
logger.info(
|
||||
"using transformer weight set from %s: %s",
|
||||
quantized_path,
|
||||
safetensors_list,
|
||||
)
|
||||
return TransformerCheckpointFiles(
|
||||
safetensors=safetensors_list,
|
||||
config_path=materialize_weight_set_config(resolved_set),
|
||||
)
|
||||
|
||||
safetensors_list = _list_safetensors_files(component_model_path)
|
||||
if safetensors_list:
|
||||
# Diffusers repos occasionally ship more than one shard split for the
|
||||
# same checkpoint (e.g. a 4-way and an 8-way split side by side). The
|
||||
# index file is the authoritative source for which files belong to
|
||||
# the checkpoint that was actually exported; anything else is a
|
||||
# leftover sibling variant.
|
||||
# Preserve legacy cleanup for the base component. Explicit overrides
|
||||
# are resolved above, where an index is already the final authority.
|
||||
safetensors_list = filter_duplicate_safetensors_files(
|
||||
safetensors_list,
|
||||
os.path.dirname(safetensors_list[0]),
|
||||
SAFE_WEIGHTS_INDEX_NAME,
|
||||
)
|
||||
|
||||
safetensors_list = _prefer_mixed_safetensors_files(safetensors_list)
|
||||
safetensors_list = _filter_duplicate_precision_variant_safetensors(safetensors_list)
|
||||
|
||||
if not safetensors_list:
|
||||
raise ValueError(
|
||||
f"no safetensors files found in {quantized_path or component_model_path}"
|
||||
safetensors_list = _prefer_mixed_safetensors_files(safetensors_list)
|
||||
safetensors_list = _filter_duplicate_precision_variant_safetensors(
|
||||
safetensors_list
|
||||
)
|
||||
|
||||
return safetensors_list
|
||||
if not safetensors_list:
|
||||
raise ValueError(f"no safetensors files found in {component_model_path}")
|
||||
|
||||
return TransformerCheckpointFiles(tuple(safetensors_list), None)
|
||||
|
||||
|
||||
def _select_single_mixed_safetensors_file(
|
||||
candidates: tuple[str, ...],
|
||||
) -> str | None:
|
||||
"""Preserve the transformer's established mixed-export preference."""
|
||||
mixed = tuple(path for path in candidates if _MIXED_SAFETENSORS_RE.fullmatch(path))
|
||||
return mixed[0] if len(mixed) == 1 else None
|
||||
|
||||
|
||||
def _prefer_mixed_safetensors_files(safetensors_list: list[str]) -> list[str]:
|
||||
@@ -772,6 +744,7 @@ def resolve_transformer_quant_load_spec(
|
||||
component_name: str | None = None,
|
||||
gguf_file: str | None = None,
|
||||
checkpoint_quant_config: QuantizationConfig | None = None,
|
||||
transformer_override_config_path: str | None = None,
|
||||
) -> TransformerQuantLoadSpec:
|
||||
if gguf_file is not None:
|
||||
if checkpoint_quant_config is not None:
|
||||
@@ -803,6 +776,7 @@ def resolve_transformer_quant_load_spec(
|
||||
server_args=server_args,
|
||||
safetensors_list=safetensors_list,
|
||||
component_model_path=component_model_path,
|
||||
transformer_override_config_path=transformer_override_config_path,
|
||||
)
|
||||
|
||||
if quant_config is not None:
|
||||
@@ -945,37 +919,15 @@ def _build_transformer_quant_adapters(
|
||||
|
||||
|
||||
def _resolve_quant_config_from_transformer_override(
|
||||
transformer_weights_path: str,
|
||||
override_config_path: str,
|
||||
) -> Optional[QuantizationConfig]:
|
||||
"""Resolve quant config from an override transformer repo or directory."""
|
||||
expanded_path = os.path.expanduser(transformer_weights_path)
|
||||
if os.path.isfile(expanded_path):
|
||||
return None
|
||||
|
||||
# A single local safetensors file does not carry a directory-level config.json.
|
||||
# Let downstream metadata probing handle it instead of misrouting it through HF.
|
||||
if expanded_path.endswith(".safetensors") and (
|
||||
os.path.isabs(expanded_path)
|
||||
or expanded_path.startswith(".")
|
||||
or os.sep in expanded_path
|
||||
or (os.path.altsep and os.path.altsep in expanded_path)
|
||||
):
|
||||
return None
|
||||
|
||||
override_quantized_path = maybe_download_model(transformer_weights_path)
|
||||
if not os.path.isdir(override_quantized_path):
|
||||
return None
|
||||
|
||||
override_config_path = os.path.join(override_quantized_path, "config.json")
|
||||
if not os.path.isfile(override_config_path):
|
||||
return None
|
||||
|
||||
with open(override_config_path, encoding="utf-8") as f:
|
||||
override_hf_config = json.load(f)
|
||||
|
||||
return get_quant_config(
|
||||
override_hf_config,
|
||||
override_quantized_path,
|
||||
os.path.dirname(override_config_path),
|
||||
)
|
||||
|
||||
|
||||
@@ -985,6 +937,7 @@ def _resolve_quant_config(
|
||||
server_args: ServerArgs,
|
||||
safetensors_list: list[str],
|
||||
component_model_path: str,
|
||||
transformer_override_config_path: str | None = None,
|
||||
) -> Optional[QuantizationConfig]:
|
||||
"""
|
||||
resolve quant config from checkpoints' metadata
|
||||
@@ -1055,11 +1008,11 @@ def _resolve_quant_config(
|
||||
fallback_group_size,
|
||||
)
|
||||
quant_config = _merge_modelopt_fp4_configs(quant_config, inferred_nvfp4_config)
|
||||
if quant_config is not None or not server_args.transformer_weights_path:
|
||||
if quant_config is not None or transformer_override_config_path is None:
|
||||
return quant_config
|
||||
|
||||
quant_config = _resolve_quant_config_from_transformer_override(
|
||||
server_args.transformer_weights_path
|
||||
transformer_override_config_path,
|
||||
)
|
||||
quant_config = _merge_modelopt_fp4_configs(quant_config, inferred_nvfp4_config)
|
||||
if quant_config is not None:
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Literal
|
||||
from urllib.parse import unquote, urlparse
|
||||
@@ -14,6 +16,8 @@ from huggingface_hub.utils import validate_repo_id
|
||||
WeightSourceKind = Literal["local", "huggingface"]
|
||||
|
||||
_WEIGHT_SUFFIXES = (".safetensors", ".gguf", ".bin", ".pt", ".pth", ".ckpt")
|
||||
_SAFETENSORS_INDEX_SUFFIX = ".safetensors.index.json"
|
||||
_WEIGHT_REFERENCE_SUFFIXES = _WEIGHT_SUFFIXES + (_SAFETENSORS_INDEX_SUFFIX,)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -40,6 +44,17 @@ class ResolvedWeight:
|
||||
selected_file: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResolvedWeightSet:
|
||||
inventory: WeightInventory
|
||||
selected_files: tuple[str, ...]
|
||||
index_file: str | None = None
|
||||
|
||||
|
||||
class NoSafetensorsWeightsError(FileNotFoundError):
|
||||
"""The source has no safetensors payload to resolve as a weight set."""
|
||||
|
||||
|
||||
def is_explicit_weight_file_reference(source: str) -> bool:
|
||||
"""Whether a component override names one weight file, not a component root."""
|
||||
expanded = os.path.expanduser(source)
|
||||
@@ -65,6 +80,23 @@ def _merge_revision(url_revision: str | None, revision: str | None) -> str | Non
|
||||
return url_revision or revision
|
||||
|
||||
|
||||
def _local_index_inventory(index_path: Path) -> tuple[str, ...]:
|
||||
"""List only an exact local index and the shards it declares."""
|
||||
with index_path.open(encoding="utf-8") as index_stream:
|
||||
index = json.load(index_stream)
|
||||
weight_map = index.get("weight_map") if isinstance(index, Mapping) else None
|
||||
shard_names = weight_map.values() if isinstance(weight_map, Mapping) else ()
|
||||
files = {index_path.name}
|
||||
for shard_name in shard_names:
|
||||
if not isinstance(shard_name, str):
|
||||
continue
|
||||
shard_name = _validate_relative_hub_path(shard_name, "index shard")
|
||||
shard_path = index_path.parent / shard_name
|
||||
if shard_path.is_file():
|
||||
files.add(shard_path.relative_to(index_path.parent).as_posix())
|
||||
return tuple(sorted(files))
|
||||
|
||||
|
||||
def _parse_huggingface_url(source: str, revision: str | None) -> WeightSource:
|
||||
parsed = urlparse(source)
|
||||
if parsed.netloc.lower() not in ("huggingface.co", "www.huggingface.co"):
|
||||
@@ -149,7 +181,7 @@ def parse_weight_source(
|
||||
tail = "/".join(parts[2:]) or None
|
||||
filename = (
|
||||
_validate_relative_hub_path(tail, "filename")
|
||||
if tail is not None and tail.lower().endswith(_WEIGHT_SUFFIXES)
|
||||
if tail is not None and tail.lower().endswith(_WEIGHT_REFERENCE_SUFFIXES)
|
||||
else None
|
||||
)
|
||||
subfolder = tail if filename is None else None
|
||||
@@ -169,12 +201,11 @@ def _filter_inventory_files(
|
||||
files: tuple[str, ...], source: WeightSource
|
||||
) -> tuple[str, ...]:
|
||||
if source.filename is not None:
|
||||
selected = tuple(path for path in files if path == source.filename)
|
||||
if not selected:
|
||||
if source.filename not in files:
|
||||
raise FileNotFoundError(
|
||||
f"Weight file {source.filename!r} was not found in {source.repo_id}"
|
||||
)
|
||||
return selected
|
||||
return (source.filename,)
|
||||
if source.subfolder is None:
|
||||
return files
|
||||
prefix = source.subfolder.rstrip("/") + "/"
|
||||
@@ -245,7 +276,7 @@ def select_weight_file(
|
||||
path for path in inventory.files if path.lower().endswith(_WEIGHT_SUFFIXES)
|
||||
)
|
||||
if inventory.source.filename is not None:
|
||||
return inventory.files[0]
|
||||
return _select_named_file(candidates, inventory.source.filename)
|
||||
if weight_name is not None:
|
||||
return _select_named_file(candidates, weight_name)
|
||||
if len(candidates) == 1:
|
||||
@@ -274,18 +305,210 @@ def resolve_weight(
|
||||
)
|
||||
|
||||
|
||||
def materialize_weight(resolved: ResolvedWeight) -> str:
|
||||
"""Return the selected local file, downloading one pinned Hub file if needed."""
|
||||
source = resolved.inventory.source
|
||||
def _materialize_inventory_file(inventory: WeightInventory, filename: str) -> str:
|
||||
source = inventory.source
|
||||
if source.kind == "local":
|
||||
assert source.local_path is not None
|
||||
if os.path.isfile(source.local_path):
|
||||
return source.local_path
|
||||
return os.path.join(source.local_path, resolved.selected_file)
|
||||
local_path = Path(source.local_path)
|
||||
if filename == local_path.name:
|
||||
return source.local_path
|
||||
return str(local_path.parent / filename)
|
||||
return os.path.join(source.local_path, filename)
|
||||
|
||||
assert source.repo_id is not None
|
||||
return hf_hub_download(
|
||||
repo_id=source.repo_id,
|
||||
filename=resolved.selected_file,
|
||||
revision=resolved.inventory.resolved_revision,
|
||||
filename=filename,
|
||||
revision=inventory.resolved_revision,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_index_shard(
|
||||
inventory: WeightInventory, index_file: str, shard_name: str
|
||||
) -> str:
|
||||
shard_name = _validate_relative_hub_path(shard_name, "index shard")
|
||||
if not shard_name.lower().endswith(".safetensors"):
|
||||
raise ValueError(
|
||||
f"Safetensors index {index_file!r} references a non-safetensors "
|
||||
f"shard: {shard_name!r}"
|
||||
)
|
||||
index_parent = PurePosixPath(index_file).parent
|
||||
candidates = (shard_name, (index_parent / shard_name).as_posix())
|
||||
matches = tuple(
|
||||
dict.fromkeys(path for path in candidates if path in inventory.files)
|
||||
)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if not matches:
|
||||
raise FileNotFoundError(
|
||||
f"Safetensors index {index_file!r} references missing shard {shard_name!r}"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Safetensors index shard {shard_name!r} is ambiguous: {list(matches)}"
|
||||
)
|
||||
|
||||
|
||||
def _read_safetensors_index(
|
||||
inventory: WeightInventory, index_file: str
|
||||
) -> tuple[str, ...]:
|
||||
with open(
|
||||
_materialize_inventory_file(inventory, index_file), encoding="utf-8"
|
||||
) as index_stream:
|
||||
index = json.load(index_stream)
|
||||
weight_map = index.get("weight_map") if isinstance(index, Mapping) else None
|
||||
if not isinstance(weight_map, Mapping) or not weight_map:
|
||||
raise ValueError(
|
||||
f"Safetensors index {index_file!r} must contain a non-empty weight_map"
|
||||
)
|
||||
shard_names = tuple(weight_map.values())
|
||||
if not all(isinstance(name, str) for name in shard_names):
|
||||
raise ValueError(
|
||||
f"Safetensors index {index_file!r} contains a non-string shard name"
|
||||
)
|
||||
return tuple(
|
||||
sorted(
|
||||
{
|
||||
_resolve_index_shard(inventory, index_file, shard_name)
|
||||
for shard_name in shard_names
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def resolve_safetensors_weight_set(
|
||||
source: str,
|
||||
*,
|
||||
revision: str | None = None,
|
||||
weight_name: str | None = None,
|
||||
select_unindexed_weight: Callable[[tuple[str, ...]], str | None] | None = None,
|
||||
) -> ResolvedWeightSet:
|
||||
"""Resolve one safetensors checkpoint, using its index as shard authority."""
|
||||
parsed_source = parse_weight_source(source, revision=revision)
|
||||
if (
|
||||
parsed_source.kind == "local"
|
||||
and parsed_source.local_path is not None
|
||||
and os.path.isfile(parsed_source.local_path)
|
||||
and parsed_source.local_path.lower().endswith(_SAFETENSORS_INDEX_SUFFIX)
|
||||
):
|
||||
inventory = WeightInventory(
|
||||
parsed_source,
|
||||
None,
|
||||
_local_index_inventory(Path(parsed_source.local_path)),
|
||||
)
|
||||
elif parsed_source.kind == "huggingface" and parsed_source.filename is not None:
|
||||
parent = PurePosixPath(parsed_source.filename).parent
|
||||
scoped_inventory = resolve_weight_inventory(
|
||||
replace(
|
||||
parsed_source,
|
||||
filename=None,
|
||||
subfolder=None if parent == PurePosixPath(".") else parent.as_posix(),
|
||||
)
|
||||
)
|
||||
inventory = WeightInventory(
|
||||
parsed_source,
|
||||
scoped_inventory.resolved_revision,
|
||||
scoped_inventory.files,
|
||||
)
|
||||
else:
|
||||
inventory = resolve_weight_inventory(parsed_source)
|
||||
weights = tuple(
|
||||
path for path in inventory.files if path.lower().endswith(".safetensors")
|
||||
)
|
||||
indexes = tuple(
|
||||
path
|
||||
for path in inventory.files
|
||||
if path.lower().endswith(_SAFETENSORS_INDEX_SUFFIX)
|
||||
)
|
||||
selected_name = inventory.source.filename or weight_name
|
||||
if (
|
||||
selected_name is None
|
||||
and inventory.source.kind == "local"
|
||||
and inventory.source.local_path is not None
|
||||
and os.path.isfile(inventory.source.local_path)
|
||||
):
|
||||
selected_name = Path(inventory.source.local_path).name
|
||||
if selected_name is not None:
|
||||
if not selected_name.lower().endswith(
|
||||
(".safetensors", _SAFETENSORS_INDEX_SUFFIX)
|
||||
):
|
||||
raise NoSafetensorsWeightsError(
|
||||
f"Selected file is not safetensors: {selected_name!r}"
|
||||
)
|
||||
selected = _select_named_file(weights + indexes, selected_name)
|
||||
if selected in weights:
|
||||
return ResolvedWeightSet(inventory, (selected,))
|
||||
return ResolvedWeightSet(
|
||||
inventory,
|
||||
_read_safetensors_index(inventory, selected),
|
||||
index_file=selected,
|
||||
)
|
||||
if len(indexes) == 1:
|
||||
return ResolvedWeightSet(
|
||||
inventory,
|
||||
_read_safetensors_index(inventory, indexes[0]),
|
||||
index_file=indexes[0],
|
||||
)
|
||||
if len(indexes) > 1:
|
||||
raise ValueError(
|
||||
"Source contains multiple safetensors indexes; select one with an "
|
||||
f"exact index name. Candidates: {list(indexes)}"
|
||||
)
|
||||
if len(weights) == 1:
|
||||
return ResolvedWeightSet(inventory, weights)
|
||||
if not weights:
|
||||
raise NoSafetensorsWeightsError("Source contains no safetensors weights")
|
||||
if select_unindexed_weight is not None:
|
||||
selected = select_unindexed_weight(weights)
|
||||
if selected is not None:
|
||||
if selected not in weights:
|
||||
raise ValueError(
|
||||
"Unindexed weight selector returned a file outside the source: "
|
||||
f"{selected!r}"
|
||||
)
|
||||
return ResolvedWeightSet(inventory, (selected,))
|
||||
raise ValueError(
|
||||
"Source contains multiple safetensors files without an index; they may "
|
||||
"be independent variants. Select one exact file or provide a standard "
|
||||
f"safetensors index. Candidates: {list(weights)}"
|
||||
)
|
||||
|
||||
|
||||
def materialize_weight_set(resolved: ResolvedWeightSet) -> tuple[str, ...]:
|
||||
"""Materialize every selected file from one pinned checkpoint revision."""
|
||||
return tuple(
|
||||
_materialize_inventory_file(resolved.inventory, filename)
|
||||
for filename in resolved.selected_files
|
||||
)
|
||||
|
||||
|
||||
def materialize_weight_set_config(resolved: ResolvedWeightSet) -> str | None:
|
||||
"""Materialize adjacent runtime configuration from the pinned revision."""
|
||||
source = resolved.inventory.source
|
||||
if source.kind == "local" and source.local_path is not None:
|
||||
local_path = Path(source.local_path)
|
||||
if local_path.is_file():
|
||||
config_path = local_path.with_name("config.json")
|
||||
return str(config_path) if config_path.is_file() else None
|
||||
|
||||
anchor = resolved.index_file or resolved.selected_files[0]
|
||||
config_file = PurePosixPath(anchor).with_name("config.json").as_posix()
|
||||
if config_file not in resolved.inventory.files:
|
||||
return None
|
||||
config_parent = PurePosixPath(config_file).parent
|
||||
metadata_files = tuple(
|
||||
path
|
||||
for path in resolved.inventory.files
|
||||
if PurePosixPath(path).parent == config_parent
|
||||
and PurePosixPath(path).name.startswith("quant_model_description")
|
||||
and path.lower().endswith(".json")
|
||||
)
|
||||
config_path = _materialize_inventory_file(resolved.inventory, config_file)
|
||||
for metadata_file in metadata_files:
|
||||
_materialize_inventory_file(resolved.inventory, metadata_file)
|
||||
return config_path
|
||||
|
||||
|
||||
def materialize_weight(resolved: ResolvedWeight) -> str:
|
||||
"""Return the selected local file, downloading one pinned Hub file if needed."""
|
||||
return _materialize_inventory_file(resolved.inventory, resolved.selected_file)
|
||||
|
||||
@@ -3,7 +3,6 @@ This unittest is introduced in #22360, preventing duplicate transformer safetens
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
@@ -100,8 +99,8 @@ from sglang.multimodal_gen.runtime.loader.transformer_load_utils import (
|
||||
_Flux2Nvfp4FallbackAdapter,
|
||||
_needs_device_weight_postprocess,
|
||||
_resolve_quant_config,
|
||||
resolve_transformer_checkpoint_files,
|
||||
resolve_transformer_quant_load_spec,
|
||||
resolve_transformer_safetensors_to_load,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.weight_load_plan import WeightLoadPlan
|
||||
from sglang.multimodal_gen.runtime.models.dits.flux import FluxSingleTransformerBlock
|
||||
@@ -262,50 +261,79 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(backend)
|
||||
|
||||
def test_resolve_transformer_safetensors_to_load_uses_single_override_file(self):
|
||||
def test_resolve_transformer_checkpoint_files_uses_single_override_file(self):
|
||||
with tempfile.NamedTemporaryFile(suffix=".safetensors") as f:
|
||||
server_args = self._make_server_args(transformer_weights_path=f.name)
|
||||
resolved = resolve_transformer_safetensors_to_load(
|
||||
server_args, "/unused/component/path"
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.weights.source.HfApi.model_info"
|
||||
) as model_info,
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.weights.source.hf_hub_download"
|
||||
) as download,
|
||||
):
|
||||
resolved = resolve_transformer_checkpoint_files(
|
||||
server_args, "/unused/component/path"
|
||||
)
|
||||
|
||||
self.assertEqual(resolved, [f.name])
|
||||
self.assertEqual(resolved.safetensors, (f.name,))
|
||||
self.assertIsNone(resolved.config_path)
|
||||
model_info.assert_not_called()
|
||||
download.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.hf_hub_download",
|
||||
return_value="/cache/model.safetensors",
|
||||
)
|
||||
def test_resolve_transformer_safetensors_to_load_uses_hf_file_reference(
|
||||
self, mock_download
|
||||
):
|
||||
filename = "diffusion_models/minimax_h3_fl2va_pruned_bf16.safetensors"
|
||||
def test_resolve_transformer_checkpoint_files_uses_one_hf_revision(self):
|
||||
filename = "weights/model.safetensors"
|
||||
references = (
|
||||
(
|
||||
f"https://huggingface.co/Comfy-Org/MiniMax-H3/resolve/main/{filename}",
|
||||
f"https://huggingface.co/owner/repo/resolve/main/{filename}",
|
||||
"main",
|
||||
),
|
||||
(f"Comfy-Org/MiniMax-H3/{filename}", "test-revision"),
|
||||
(f"owner/repo/{filename}", "test-revision"),
|
||||
)
|
||||
|
||||
for reference, revision in references:
|
||||
with self.subTest(reference=reference):
|
||||
server_args = self._make_server_args(transformer_weights_path=reference)
|
||||
with patch(
|
||||
"os.path.isfile",
|
||||
side_effect=lambda path: path == "/cache/model.safetensors",
|
||||
):
|
||||
self.assertEqual(
|
||||
resolve_transformer_safetensors_to_load(
|
||||
server_args, "/unused/component/path"
|
||||
),
|
||||
["/cache/model.safetensors"],
|
||||
)
|
||||
mock_download.assert_called_once_with(
|
||||
repo_id="Comfy-Org/MiniMax-H3",
|
||||
filename=filename,
|
||||
server_args = self._make_server_args(
|
||||
transformer_weights_path=reference,
|
||||
revision=revision,
|
||||
)
|
||||
mock_download.reset_mock()
|
||||
model_info = SimpleNamespace(
|
||||
sha="immutable-sha",
|
||||
siblings=[
|
||||
SimpleNamespace(rfilename=filename),
|
||||
SimpleNamespace(rfilename="weights/config.json"),
|
||||
],
|
||||
)
|
||||
|
||||
def download(*, filename, **_kwargs):
|
||||
return f"/cache/{filename.rsplit('/', 1)[-1]}"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.weights.source.HfApi.model_info",
|
||||
return_value=model_info,
|
||||
) as model_info_call,
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.weights.source.hf_hub_download",
|
||||
side_effect=download,
|
||||
) as download,
|
||||
):
|
||||
resolved = resolve_transformer_checkpoint_files(
|
||||
server_args, "/unused/component/path"
|
||||
)
|
||||
self.assertEqual(resolved.safetensors, ("/cache/model.safetensors",))
|
||||
self.assertEqual(resolved.config_path, "/cache/config.json")
|
||||
model_info_call.assert_called_once_with("owner/repo", revision=revision)
|
||||
self.assertEqual(
|
||||
{call.kwargs["filename"] for call in download.call_args_list},
|
||||
{filename, "weights/config.json"},
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
call.kwargs["revision"] == "immutable-sha"
|
||||
for call in download.call_args_list
|
||||
)
|
||||
)
|
||||
|
||||
def test_inspect_minimax_h3_safetensors_detects_curve_and_comfy_format(self):
|
||||
marker = json.dumps(
|
||||
@@ -739,13 +767,7 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
||||
checkpoint_quant_config=ComfyFp8Config({}),
|
||||
)
|
||||
|
||||
@patch(
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.maybe_download_model",
|
||||
side_effect=lambda path, **kw: path,
|
||||
)
|
||||
def test_resolve_transformer_safetensors_to_load_prefers_mixed_export(
|
||||
self, _mock_download
|
||||
):
|
||||
def test_resolve_transformer_override_prefers_single_mixed_export(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
mixed = f"{tmpdir}/flux2-dev-nvfp4-mixed.safetensors"
|
||||
full = f"{tmpdir}/flux2-dev-nvfp4.safetensors"
|
||||
@@ -753,39 +775,11 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
||||
open(full, "a").close()
|
||||
|
||||
server_args = self._make_server_args(transformer_weights_path=tmpdir)
|
||||
resolved = resolve_transformer_safetensors_to_load(
|
||||
resolved = resolve_transformer_checkpoint_files(
|
||||
server_args, "/unused/component/path"
|
||||
)
|
||||
|
||||
self.assertEqual(resolved, [mixed])
|
||||
|
||||
@patch(
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.snapshot_download",
|
||||
)
|
||||
@patch(
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.maybe_download_model",
|
||||
)
|
||||
def test_resolve_transformer_safetensors_to_load_refreshes_empty_cached_repo(
|
||||
self, mock_download_model, mock_snapshot_download
|
||||
):
|
||||
with tempfile.TemporaryDirectory() as cached_dir:
|
||||
repo_id = "black-forest-labs/FLUX.2-dev-NVFP4"
|
||||
mixed = os.path.join(cached_dir, "flux2-dev-nvfp4-mixed.safetensors")
|
||||
mock_download_model.return_value = cached_dir
|
||||
|
||||
def _snapshot_download(**_kwargs):
|
||||
open(mixed, "a").close()
|
||||
return cached_dir
|
||||
|
||||
mock_snapshot_download.side_effect = _snapshot_download
|
||||
|
||||
server_args = self._make_server_args(transformer_weights_path=repo_id)
|
||||
resolved = resolve_transformer_safetensors_to_load(
|
||||
server_args, "/unused/component/path"
|
||||
)
|
||||
|
||||
self.assertEqual(resolved, [mixed])
|
||||
mock_snapshot_download.assert_called_once()
|
||||
self.assertEqual(resolved.safetensors, (mixed,))
|
||||
|
||||
def test_filter_transformer_precision_variants_prefers_canonical_file(self):
|
||||
files = [
|
||||
@@ -957,9 +951,6 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.build_nvfp4_config_from_safetensors_list",
|
||||
return_value=None,
|
||||
)
|
||||
@patch(
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.maybe_download_model"
|
||||
)
|
||||
@patch(
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.get_quant_config_from_safetensors_metadata",
|
||||
return_value=None,
|
||||
@@ -967,21 +958,12 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
||||
@patch(
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.get_metadata_from_safetensors_file"
|
||||
)
|
||||
@patch(
|
||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.maybe_download_model",
|
||||
side_effect=lambda path, **kw: path,
|
||||
)
|
||||
def test_resolve_transformer_quant_load_spec_keeps_nunchaku_hook(
|
||||
self,
|
||||
_mock_download,
|
||||
mock_metadata,
|
||||
_mock_quant_metadata,
|
||||
mock_maybe_download,
|
||||
_mock_nvfp4,
|
||||
):
|
||||
mock_maybe_download.side_effect = AssertionError(
|
||||
"local safetensors path should not trigger maybe_download_model"
|
||||
)
|
||||
mock_metadata.return_value = {
|
||||
"config": json.dumps({"_class_name": _FakeFluxTransformer.__name__})
|
||||
}
|
||||
@@ -1006,7 +988,6 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
||||
self.assertIsNone(spec.param_dtype)
|
||||
self.assertEqual(len(spec.post_load_hooks), 1)
|
||||
self.assertIs(nunchaku_config.model_cls, _FakeFluxTransformer)
|
||||
mock_maybe_download.assert_not_called()
|
||||
|
||||
def test_flux2_mixed_nvfp4_fallback_disables_conflicting_offloads(self):
|
||||
server_args = self._make_server_args(
|
||||
|
||||
@@ -4,9 +4,13 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from sglang.multimodal_gen.runtime.weights.source import (
|
||||
NoSafetensorsWeightsError,
|
||||
is_explicit_weight_file_reference,
|
||||
materialize_weight,
|
||||
materialize_weight_set,
|
||||
materialize_weight_set_config,
|
||||
parse_weight_source,
|
||||
resolve_safetensors_weight_set,
|
||||
resolve_weight,
|
||||
resolve_weight_inventory,
|
||||
)
|
||||
@@ -18,6 +22,9 @@ def test_explicit_weight_file_reference_does_not_claim_directories(tmp_path):
|
||||
|
||||
assert not is_explicit_weight_file_reference(str(component))
|
||||
assert is_explicit_weight_file_reference("owner/repo/model.safetensors")
|
||||
assert not is_explicit_weight_file_reference(
|
||||
"owner/repo/model.safetensors.index.json"
|
||||
)
|
||||
assert is_explicit_weight_file_reference(
|
||||
"https://huggingface.co/owner/repo/resolve/main/model.gguf?download=true"
|
||||
)
|
||||
@@ -95,6 +102,137 @@ def test_weight_source_rejects_ambiguous_files(tmp_path):
|
||||
resolve_weight(str(tmp_path))
|
||||
|
||||
|
||||
def test_safetensors_index_selects_only_declared_shards(tmp_path):
|
||||
(tmp_path / "model-00001-of-00002.safetensors").write_bytes(b"one")
|
||||
(tmp_path / "model-00002-of-00002.safetensors").write_bytes(b"two")
|
||||
(tmp_path / "alternate.safetensors").write_bytes(b"other variant")
|
||||
(tmp_path / "model.safetensors.index.json").write_text(
|
||||
'{"weight_map":{"a":"model-00001-of-00002.safetensors",'
|
||||
'"b":"model-00002-of-00002.safetensors"}}'
|
||||
)
|
||||
|
||||
resolved = resolve_safetensors_weight_set(str(tmp_path))
|
||||
|
||||
assert resolved.index_file == "model.safetensors.index.json"
|
||||
assert resolved.selected_files == (
|
||||
"model-00001-of-00002.safetensors",
|
||||
"model-00002-of-00002.safetensors",
|
||||
)
|
||||
assert materialize_weight_set(resolved) == tuple(
|
||||
str(tmp_path / filename) for filename in resolved.selected_files
|
||||
)
|
||||
|
||||
|
||||
def test_exact_local_safetensors_index_resolves_adjacent_shards(tmp_path):
|
||||
shard = tmp_path / "model-00001-of-00001.safetensors"
|
||||
shard.write_bytes(b"one")
|
||||
index = tmp_path / "model.safetensors.index.json"
|
||||
index.write_text('{"weight_map":{"a":"model-00001-of-00001.safetensors"}}')
|
||||
|
||||
resolved = resolve_safetensors_weight_set(str(index))
|
||||
|
||||
assert resolved.index_file == index.name
|
||||
assert materialize_weight_set(resolved) == (str(shard),)
|
||||
|
||||
|
||||
def test_safetensors_weight_set_rejects_unindexed_variants(tmp_path):
|
||||
(tmp_path / "base.safetensors").write_bytes(b"base")
|
||||
(tmp_path / "distilled.safetensors").write_bytes(b"distilled")
|
||||
|
||||
with pytest.raises(ValueError, match="without an index"):
|
||||
resolve_safetensors_weight_set(str(tmp_path))
|
||||
|
||||
|
||||
def test_safetensors_index_rejects_non_weight_shard(tmp_path):
|
||||
(tmp_path / "config.json").write_text("{}")
|
||||
(tmp_path / "model.safetensors.index.json").write_text(
|
||||
'{"weight_map":{"a":"config.json"}}'
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="non-safetensors shard"):
|
||||
resolve_safetensors_weight_set(str(tmp_path))
|
||||
|
||||
|
||||
def test_safetensors_index_missing_shard_is_not_no_weights_fallback(tmp_path):
|
||||
(tmp_path / "model.safetensors.index.json").write_text(
|
||||
'{"weight_map":{"a":"missing.safetensors"}}'
|
||||
)
|
||||
|
||||
with pytest.raises(FileNotFoundError, match="missing shard") as error:
|
||||
resolve_safetensors_weight_set(str(tmp_path))
|
||||
|
||||
assert not isinstance(error.value, NoSafetensorsWeightsError)
|
||||
|
||||
|
||||
def test_source_without_safetensors_has_distinct_error(tmp_path):
|
||||
(tmp_path / "pytorch_model.bin").write_bytes(b"bin")
|
||||
|
||||
with pytest.raises(NoSafetensorsWeightsError):
|
||||
resolve_safetensors_weight_set(str(tmp_path))
|
||||
|
||||
|
||||
def test_exact_non_safetensors_file_has_distinct_error(tmp_path):
|
||||
weights = tmp_path / "pytorch_model.bin"
|
||||
weights.write_bytes(b"bin")
|
||||
|
||||
with pytest.raises(NoSafetensorsWeightsError):
|
||||
resolve_safetensors_weight_set(str(weights))
|
||||
|
||||
|
||||
def test_remote_safetensors_shards_use_one_pinned_revision(tmp_path):
|
||||
index_path = tmp_path / "model.safetensors.index.json"
|
||||
index_path.write_text(
|
||||
'{"weight_map":{"a":"model-00001-of-00002.safetensors",'
|
||||
'"b":"model-00002-of-00002.safetensors"}}'
|
||||
)
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.write_text("{}")
|
||||
model_info = SimpleNamespace(
|
||||
sha="immutable-sha",
|
||||
siblings=[
|
||||
SimpleNamespace(rfilename=f"transformer/{filename}")
|
||||
for filename in (
|
||||
"model.safetensors.index.json",
|
||||
"model-00001-of-00002.safetensors",
|
||||
"model-00002-of-00002.safetensors",
|
||||
"config.json",
|
||||
"quant_model_description_w8a8.json",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def download(*, filename, **_kwargs):
|
||||
if filename.endswith("index.json"):
|
||||
return str(index_path)
|
||||
if filename.endswith("config.json"):
|
||||
return str(config_path)
|
||||
if "quant_model_description" in filename:
|
||||
return str(tmp_path / "quant_model_description_w8a8.json")
|
||||
return f"/{filename}"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.weights.source.HfApi.model_info",
|
||||
return_value=model_info,
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.weights.source.hf_hub_download",
|
||||
side_effect=download,
|
||||
) as hub_download,
|
||||
):
|
||||
resolved = resolve_safetensors_weight_set("owner/repo/transformer")
|
||||
materialize_weight_set(resolved)
|
||||
assert materialize_weight_set_config(resolved) == str(config_path)
|
||||
|
||||
assert all(
|
||||
call.kwargs["revision"] == "immutable-sha"
|
||||
for call in hub_download.call_args_list
|
||||
)
|
||||
assert "transformer/quant_model_description_w8a8.json" in {
|
||||
call.kwargs["filename"] for call in hub_download.call_args_list
|
||||
}
|
||||
|
||||
|
||||
def test_materialize_local_weight_returns_selected_file(tmp_path):
|
||||
checkpoint = tmp_path / "model.safetensors"
|
||||
checkpoint.write_bytes(b"fixture")
|
||||
|
||||
Reference in New Issue
Block a user