[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 (
|
from sglang.multimodal_gen.runtime.loader.transformer_load_utils import (
|
||||||
TransformerQuantLoadSpec,
|
TransformerQuantLoadSpec,
|
||||||
|
resolve_transformer_checkpoint_files,
|
||||||
resolve_transformer_gguf_to_load,
|
resolve_transformer_gguf_to_load,
|
||||||
resolve_transformer_quant_load_spec,
|
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.utils import _normalize_component_type
|
||||||
from sglang.multimodal_gen.runtime.loader.weight_load_plan import WeightLoadPlan
|
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
|
# A GGUF file holds the whole transformer; the remaining components
|
||||||
# still load from the base model path.
|
# still load from the base model path.
|
||||||
safetensors_list = []
|
safetensors_list = []
|
||||||
|
transformer_override_config_path = None
|
||||||
else:
|
else:
|
||||||
safetensors_list = resolve_transformer_safetensors_to_load(
|
checkpoint_files = resolve_transformer_checkpoint_files(
|
||||||
component_server_args, component_model_path
|
component_server_args, component_model_path
|
||||||
)
|
)
|
||||||
|
safetensors_list = list(checkpoint_files.safetensors)
|
||||||
|
transformer_override_config_path = checkpoint_files.config_path
|
||||||
|
|
||||||
# 2. dit config
|
# 2. dit config
|
||||||
# Config from Diffusers supersedes sgl_diffusion's model config
|
# Config from Diffusers supersedes sgl_diffusion's model config
|
||||||
@@ -286,6 +289,7 @@ class TransformerLoader(ComponentLoader):
|
|||||||
component_name=component_name,
|
component_name=component_name,
|
||||||
gguf_file=gguf_file,
|
gguf_file=gguf_file,
|
||||||
checkpoint_quant_config=checkpoint_quant_config,
|
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:
|
if quant_spec.gguf_file is not None and is_minimax_h3:
|
||||||
assert quant_spec.quant_config is not None
|
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.platforms import current_platform
|
||||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
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.logging_utils import init_logger
|
||||||
from sglang.multimodal_gen.runtime.utils.precision import resolve_precision
|
from sglang.multimodal_gen.runtime.utils.precision import resolve_precision
|
||||||
from sglang.multimodal_gen.runtime.utils.quantization_utils import (
|
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,
|
||||||
get_quant_config_from_safetensors_metadata,
|
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 (
|
from sglang.srt.utils.hf_transformers import (
|
||||||
check_gguf_file,
|
check_gguf_file,
|
||||||
resolve_hf_gguf_reference,
|
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)$"
|
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$")
|
_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]:
|
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:
|
class _TransformerQuantAdapter:
|
||||||
def prepare(self) -> None:
|
def prepare(self) -> None:
|
||||||
"""initialize"""
|
"""initialize"""
|
||||||
@@ -612,86 +615,55 @@ def resolve_transformer_gguf_to_load(
|
|||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
def resolve_transformer_safetensors_to_load(
|
def resolve_transformer_checkpoint_files(
|
||||||
server_args: ServerArgs, component_model_path: str
|
server_args: ServerArgs, component_model_path: str
|
||||||
) -> list[str]:
|
) -> TransformerCheckpointFiles:
|
||||||
"""Resolve transformer weights from the base component path or an override."""
|
"""Resolve transformer weights from the base component path or an override."""
|
||||||
quantized_path = server_args.transformer_weights_path
|
quantized_path = server_args.transformer_weights_path
|
||||||
|
|
||||||
if quantized_path:
|
if quantized_path:
|
||||||
original_quantized_path = quantized_path
|
resolved_set = resolve_safetensors_weight_set(
|
||||||
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,
|
quantized_path,
|
||||||
original_quantized_path,
|
revision=server_args.revision,
|
||||||
|
select_unindexed_weight=_select_single_mixed_safetensors_file,
|
||||||
)
|
)
|
||||||
quantized_path = snapshot_download(
|
safetensors_list = materialize_weight_set(resolved_set)
|
||||||
repo_id=original_quantized_path,
|
logger.info(
|
||||||
ignore_patterns=["*.onnx", "*.msgpack"],
|
"using transformer weight set from %s: %s",
|
||||||
allow_patterns=[
|
quantized_path,
|
||||||
"*.json",
|
safetensors_list,
|
||||||
"*.safetensors",
|
)
|
||||||
"*.safetensors.index.json",
|
return TransformerCheckpointFiles(
|
||||||
],
|
safetensors=safetensors_list,
|
||||||
max_workers=8,
|
config_path=materialize_weight_set_config(resolved_set),
|
||||||
)
|
)
|
||||||
safetensors_list = _list_safetensors_files(quantized_path)
|
|
||||||
else:
|
|
||||||
safetensors_list = _list_safetensors_files(component_model_path)
|
|
||||||
|
|
||||||
|
safetensors_list = _list_safetensors_files(component_model_path)
|
||||||
if safetensors_list:
|
if safetensors_list:
|
||||||
# Diffusers repos occasionally ship more than one shard split for the
|
# Preserve legacy cleanup for the base component. Explicit overrides
|
||||||
# same checkpoint (e.g. a 4-way and an 8-way split side by side). The
|
# are resolved above, where an index is already the final authority.
|
||||||
# index file is the authoritative source for which files belong to
|
|
||||||
# the checkpoint that was actually exported; anything else is a
|
|
||||||
# leftover sibling variant.
|
|
||||||
safetensors_list = filter_duplicate_safetensors_files(
|
safetensors_list = filter_duplicate_safetensors_files(
|
||||||
safetensors_list,
|
safetensors_list,
|
||||||
os.path.dirname(safetensors_list[0]),
|
os.path.dirname(safetensors_list[0]),
|
||||||
SAFE_WEIGHTS_INDEX_NAME,
|
SAFE_WEIGHTS_INDEX_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
safetensors_list = _prefer_mixed_safetensors_files(safetensors_list)
|
safetensors_list = _prefer_mixed_safetensors_files(safetensors_list)
|
||||||
safetensors_list = _filter_duplicate_precision_variant_safetensors(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}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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]:
|
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,
|
component_name: str | None = None,
|
||||||
gguf_file: str | None = None,
|
gguf_file: str | None = None,
|
||||||
checkpoint_quant_config: QuantizationConfig | None = None,
|
checkpoint_quant_config: QuantizationConfig | None = None,
|
||||||
|
transformer_override_config_path: str | None = None,
|
||||||
) -> TransformerQuantLoadSpec:
|
) -> TransformerQuantLoadSpec:
|
||||||
if gguf_file is not None:
|
if gguf_file is not None:
|
||||||
if checkpoint_quant_config is not None:
|
if checkpoint_quant_config is not None:
|
||||||
@@ -803,6 +776,7 @@ def resolve_transformer_quant_load_spec(
|
|||||||
server_args=server_args,
|
server_args=server_args,
|
||||||
safetensors_list=safetensors_list,
|
safetensors_list=safetensors_list,
|
||||||
component_model_path=component_model_path,
|
component_model_path=component_model_path,
|
||||||
|
transformer_override_config_path=transformer_override_config_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
if quant_config is not None:
|
if quant_config is not None:
|
||||||
@@ -945,37 +919,15 @@ def _build_transformer_quant_adapters(
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_quant_config_from_transformer_override(
|
def _resolve_quant_config_from_transformer_override(
|
||||||
transformer_weights_path: str,
|
override_config_path: str,
|
||||||
) -> Optional[QuantizationConfig]:
|
) -> Optional[QuantizationConfig]:
|
||||||
"""Resolve quant config from an override transformer repo or directory."""
|
"""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:
|
with open(override_config_path, encoding="utf-8") as f:
|
||||||
override_hf_config = json.load(f)
|
override_hf_config = json.load(f)
|
||||||
|
|
||||||
return get_quant_config(
|
return get_quant_config(
|
||||||
override_hf_config,
|
override_hf_config,
|
||||||
override_quantized_path,
|
os.path.dirname(override_config_path),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -985,6 +937,7 @@ def _resolve_quant_config(
|
|||||||
server_args: ServerArgs,
|
server_args: ServerArgs,
|
||||||
safetensors_list: list[str],
|
safetensors_list: list[str],
|
||||||
component_model_path: str,
|
component_model_path: str,
|
||||||
|
transformer_override_config_path: str | None = None,
|
||||||
) -> Optional[QuantizationConfig]:
|
) -> Optional[QuantizationConfig]:
|
||||||
"""
|
"""
|
||||||
resolve quant config from checkpoints' metadata
|
resolve quant config from checkpoints' metadata
|
||||||
@@ -1055,11 +1008,11 @@ def _resolve_quant_config(
|
|||||||
fallback_group_size,
|
fallback_group_size,
|
||||||
)
|
)
|
||||||
quant_config = _merge_modelopt_fp4_configs(quant_config, inferred_nvfp4_config)
|
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
|
return quant_config
|
||||||
|
|
||||||
quant_config = _resolve_quant_config_from_transformer_override(
|
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)
|
quant_config = _merge_modelopt_fp4_configs(quant_config, inferred_nvfp4_config)
|
||||||
if quant_config is not None:
|
if quant_config is not None:
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from collections.abc import Callable, Mapping
|
||||||
|
from dataclasses import dataclass, replace
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from urllib.parse import unquote, urlparse
|
from urllib.parse import unquote, urlparse
|
||||||
@@ -14,6 +16,8 @@ from huggingface_hub.utils import validate_repo_id
|
|||||||
WeightSourceKind = Literal["local", "huggingface"]
|
WeightSourceKind = Literal["local", "huggingface"]
|
||||||
|
|
||||||
_WEIGHT_SUFFIXES = (".safetensors", ".gguf", ".bin", ".pt", ".pth", ".ckpt")
|
_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)
|
@dataclass(frozen=True)
|
||||||
@@ -40,6 +44,17 @@ class ResolvedWeight:
|
|||||||
selected_file: str
|
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:
|
def is_explicit_weight_file_reference(source: str) -> bool:
|
||||||
"""Whether a component override names one weight file, not a component root."""
|
"""Whether a component override names one weight file, not a component root."""
|
||||||
expanded = os.path.expanduser(source)
|
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
|
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:
|
def _parse_huggingface_url(source: str, revision: str | None) -> WeightSource:
|
||||||
parsed = urlparse(source)
|
parsed = urlparse(source)
|
||||||
if parsed.netloc.lower() not in ("huggingface.co", "www.huggingface.co"):
|
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
|
tail = "/".join(parts[2:]) or None
|
||||||
filename = (
|
filename = (
|
||||||
_validate_relative_hub_path(tail, "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
|
else None
|
||||||
)
|
)
|
||||||
subfolder = tail if filename is None else None
|
subfolder = tail if filename is None else None
|
||||||
@@ -169,12 +201,11 @@ def _filter_inventory_files(
|
|||||||
files: tuple[str, ...], source: WeightSource
|
files: tuple[str, ...], source: WeightSource
|
||||||
) -> tuple[str, ...]:
|
) -> tuple[str, ...]:
|
||||||
if source.filename is not None:
|
if source.filename is not None:
|
||||||
selected = tuple(path for path in files if path == source.filename)
|
if source.filename not in files:
|
||||||
if not selected:
|
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Weight file {source.filename!r} was not found in {source.repo_id}"
|
f"Weight file {source.filename!r} was not found in {source.repo_id}"
|
||||||
)
|
)
|
||||||
return selected
|
return (source.filename,)
|
||||||
if source.subfolder is None:
|
if source.subfolder is None:
|
||||||
return files
|
return files
|
||||||
prefix = source.subfolder.rstrip("/") + "/"
|
prefix = source.subfolder.rstrip("/") + "/"
|
||||||
@@ -245,7 +276,7 @@ def select_weight_file(
|
|||||||
path for path in inventory.files if path.lower().endswith(_WEIGHT_SUFFIXES)
|
path for path in inventory.files if path.lower().endswith(_WEIGHT_SUFFIXES)
|
||||||
)
|
)
|
||||||
if inventory.source.filename is not None:
|
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:
|
if weight_name is not None:
|
||||||
return _select_named_file(candidates, weight_name)
|
return _select_named_file(candidates, weight_name)
|
||||||
if len(candidates) == 1:
|
if len(candidates) == 1:
|
||||||
@@ -274,18 +305,210 @@ def resolve_weight(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def materialize_weight(resolved: ResolvedWeight) -> str:
|
def _materialize_inventory_file(inventory: WeightInventory, filename: str) -> str:
|
||||||
"""Return the selected local file, downloading one pinned Hub file if needed."""
|
source = inventory.source
|
||||||
source = resolved.inventory.source
|
|
||||||
if source.kind == "local":
|
if source.kind == "local":
|
||||||
assert source.local_path is not None
|
assert source.local_path is not None
|
||||||
if os.path.isfile(source.local_path):
|
if os.path.isfile(source.local_path):
|
||||||
|
local_path = Path(source.local_path)
|
||||||
|
if filename == local_path.name:
|
||||||
return source.local_path
|
return source.local_path
|
||||||
return os.path.join(source.local_path, resolved.selected_file)
|
return str(local_path.parent / filename)
|
||||||
|
return os.path.join(source.local_path, filename)
|
||||||
|
|
||||||
assert source.repo_id is not None
|
assert source.repo_id is not None
|
||||||
return hf_hub_download(
|
return hf_hub_download(
|
||||||
repo_id=source.repo_id,
|
repo_id=source.repo_id,
|
||||||
filename=resolved.selected_file,
|
filename=filename,
|
||||||
revision=resolved.inventory.resolved_revision,
|
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 json
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import types
|
import types
|
||||||
@@ -100,8 +99,8 @@ from sglang.multimodal_gen.runtime.loader.transformer_load_utils import (
|
|||||||
_Flux2Nvfp4FallbackAdapter,
|
_Flux2Nvfp4FallbackAdapter,
|
||||||
_needs_device_weight_postprocess,
|
_needs_device_weight_postprocess,
|
||||||
_resolve_quant_config,
|
_resolve_quant_config,
|
||||||
|
resolve_transformer_checkpoint_files,
|
||||||
resolve_transformer_quant_load_spec,
|
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.loader.weight_load_plan import WeightLoadPlan
|
||||||
from sglang.multimodal_gen.runtime.models.dits.flux import FluxSingleTransformerBlock
|
from sglang.multimodal_gen.runtime.models.dits.flux import FluxSingleTransformerBlock
|
||||||
@@ -262,50 +261,79 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertIsNone(backend)
|
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:
|
with tempfile.NamedTemporaryFile(suffix=".safetensors") as f:
|
||||||
server_args = self._make_server_args(transformer_weights_path=f.name)
|
server_args = self._make_server_args(transformer_weights_path=f.name)
|
||||||
resolved = resolve_transformer_safetensors_to_load(
|
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"
|
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(
|
def test_resolve_transformer_checkpoint_files_uses_one_hf_revision(self):
|
||||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.hf_hub_download",
|
filename = "weights/model.safetensors"
|
||||||
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"
|
|
||||||
references = (
|
references = (
|
||||||
(
|
(
|
||||||
f"https://huggingface.co/Comfy-Org/MiniMax-H3/resolve/main/{filename}",
|
f"https://huggingface.co/owner/repo/resolve/main/{filename}",
|
||||||
"main",
|
"main",
|
||||||
),
|
),
|
||||||
(f"Comfy-Org/MiniMax-H3/{filename}", "test-revision"),
|
(f"owner/repo/{filename}", "test-revision"),
|
||||||
)
|
)
|
||||||
|
|
||||||
for reference, revision in references:
|
for reference, revision in references:
|
||||||
with self.subTest(reference=reference):
|
with self.subTest(reference=reference):
|
||||||
server_args = self._make_server_args(transformer_weights_path=reference)
|
server_args = self._make_server_args(
|
||||||
with patch(
|
transformer_weights_path=reference,
|
||||||
"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,
|
|
||||||
revision=revision,
|
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):
|
def test_inspect_minimax_h3_safetensors_detects_curve_and_comfy_format(self):
|
||||||
marker = json.dumps(
|
marker = json.dumps(
|
||||||
@@ -739,13 +767,7 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
|||||||
checkpoint_quant_config=ComfyFp8Config({}),
|
checkpoint_quant_config=ComfyFp8Config({}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch(
|
def test_resolve_transformer_override_prefers_single_mixed_export(self):
|
||||||
"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
|
|
||||||
):
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
mixed = f"{tmpdir}/flux2-dev-nvfp4-mixed.safetensors"
|
mixed = f"{tmpdir}/flux2-dev-nvfp4-mixed.safetensors"
|
||||||
full = f"{tmpdir}/flux2-dev-nvfp4.safetensors"
|
full = f"{tmpdir}/flux2-dev-nvfp4.safetensors"
|
||||||
@@ -753,39 +775,11 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
|||||||
open(full, "a").close()
|
open(full, "a").close()
|
||||||
|
|
||||||
server_args = self._make_server_args(transformer_weights_path=tmpdir)
|
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"
|
server_args, "/unused/component/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(resolved, [mixed])
|
self.assertEqual(resolved.safetensors, (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()
|
|
||||||
|
|
||||||
def test_filter_transformer_precision_variants_prefers_canonical_file(self):
|
def test_filter_transformer_precision_variants_prefers_canonical_file(self):
|
||||||
files = [
|
files = [
|
||||||
@@ -957,9 +951,6 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
|||||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.build_nvfp4_config_from_safetensors_list",
|
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.build_nvfp4_config_from_safetensors_list",
|
||||||
return_value=None,
|
return_value=None,
|
||||||
)
|
)
|
||||||
@patch(
|
|
||||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.maybe_download_model"
|
|
||||||
)
|
|
||||||
@patch(
|
@patch(
|
||||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.get_quant_config_from_safetensors_metadata",
|
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.get_quant_config_from_safetensors_metadata",
|
||||||
return_value=None,
|
return_value=None,
|
||||||
@@ -967,21 +958,12 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
|||||||
@patch(
|
@patch(
|
||||||
"sglang.multimodal_gen.runtime.loader.transformer_load_utils.get_metadata_from_safetensors_file"
|
"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(
|
def test_resolve_transformer_quant_load_spec_keeps_nunchaku_hook(
|
||||||
self,
|
self,
|
||||||
_mock_download,
|
|
||||||
mock_metadata,
|
mock_metadata,
|
||||||
_mock_quant_metadata,
|
_mock_quant_metadata,
|
||||||
mock_maybe_download,
|
|
||||||
_mock_nvfp4,
|
_mock_nvfp4,
|
||||||
):
|
):
|
||||||
mock_maybe_download.side_effect = AssertionError(
|
|
||||||
"local safetensors path should not trigger maybe_download_model"
|
|
||||||
)
|
|
||||||
mock_metadata.return_value = {
|
mock_metadata.return_value = {
|
||||||
"config": json.dumps({"_class_name": _FakeFluxTransformer.__name__})
|
"config": json.dumps({"_class_name": _FakeFluxTransformer.__name__})
|
||||||
}
|
}
|
||||||
@@ -1006,7 +988,6 @@ class TestTransformerQuantHelpers(unittest.TestCase):
|
|||||||
self.assertIsNone(spec.param_dtype)
|
self.assertIsNone(spec.param_dtype)
|
||||||
self.assertEqual(len(spec.post_load_hooks), 1)
|
self.assertEqual(len(spec.post_load_hooks), 1)
|
||||||
self.assertIs(nunchaku_config.model_cls, _FakeFluxTransformer)
|
self.assertIs(nunchaku_config.model_cls, _FakeFluxTransformer)
|
||||||
mock_maybe_download.assert_not_called()
|
|
||||||
|
|
||||||
def test_flux2_mixed_nvfp4_fallback_disables_conflicting_offloads(self):
|
def test_flux2_mixed_nvfp4_fallback_disables_conflicting_offloads(self):
|
||||||
server_args = self._make_server_args(
|
server_args = self._make_server_args(
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sglang.multimodal_gen.runtime.weights.source import (
|
from sglang.multimodal_gen.runtime.weights.source import (
|
||||||
|
NoSafetensorsWeightsError,
|
||||||
is_explicit_weight_file_reference,
|
is_explicit_weight_file_reference,
|
||||||
materialize_weight,
|
materialize_weight,
|
||||||
|
materialize_weight_set,
|
||||||
|
materialize_weight_set_config,
|
||||||
parse_weight_source,
|
parse_weight_source,
|
||||||
|
resolve_safetensors_weight_set,
|
||||||
resolve_weight,
|
resolve_weight,
|
||||||
resolve_weight_inventory,
|
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 not is_explicit_weight_file_reference(str(component))
|
||||||
assert is_explicit_weight_file_reference("owner/repo/model.safetensors")
|
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(
|
assert is_explicit_weight_file_reference(
|
||||||
"https://huggingface.co/owner/repo/resolve/main/model.gguf?download=true"
|
"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))
|
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):
|
def test_materialize_local_weight_returns_selected_file(tmp_path):
|
||||||
checkpoint = tmp_path / "model.safetensors"
|
checkpoint = tmp_path / "model.safetensors"
|
||||||
checkpoint.write_bytes(b"fixture")
|
checkpoint.write_bytes(b"fixture")
|
||||||
|
|||||||
Reference in New Issue
Block a user