[diffusion] refactor: consolidate plain state-dict component loaders (#38128)
This commit is contained in:
@@ -21,6 +21,9 @@ class ArchConfig:
|
||||
stacked_params_mapping: list[tuple[str, str, str]] = field(
|
||||
default_factory=list
|
||||
) # mapping from huggingface weight names to custom names
|
||||
param_names_mapping: dict[str, str | tuple[str, int, int]] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
extra_attrs: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
@@ -53,6 +56,9 @@ class ModelConfig:
|
||||
# sglang-diffusion-specific parameters here
|
||||
# i.e. STA, quantization, teacache
|
||||
|
||||
def post_diffusers_config_update(self) -> None:
|
||||
"""Normalize external configuration before constructing the runtime model."""
|
||||
|
||||
def __getattr__(self, name):
|
||||
# Only called if 'name' is not found in ModelConfig directly
|
||||
if hasattr(self.arch_config, name):
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import re
|
||||
|
||||
from sglang.multimodal_gen.configs.models.adapter.ltx_2_connector import (
|
||||
LTX2ConnectorConfig,
|
||||
)
|
||||
@@ -9,97 +7,12 @@ from sglang.multimodal_gen.configs.models.adapter.ltx_2_duration_head import (
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
||||
PlainStateDictComponentLoader,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
load_safetensors_state_dict,
|
||||
set_default_torch_dtype,
|
||||
skip_init_modules,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.registry import ModelRegistry
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.precision import resolve_precision
|
||||
|
||||
|
||||
class AdapterLoader(PlainStateDictComponentLoader):
|
||||
"""Loader for small adapter-style modules (e.g., LTX-2 connectors).
|
||||
|
||||
This loader intentionally avoids FSDP sharding and just:
|
||||
1) Instantiates the module from `config.json`.
|
||||
2) Loads the safetensors state_dict (single-file or sharded).
|
||||
"""
|
||||
|
||||
component_names = ["connectors", "duration_head"]
|
||||
expected_library = "diffusers"
|
||||
|
||||
# `update_model_arch` fills each from the component's `config.json`.
|
||||
_CONFIG_CLASSES = {
|
||||
config_classes = {
|
||||
"connectors": LTX2ConnectorConfig,
|
||||
"duration_head": LTX2DurationHeadConfig,
|
||||
}
|
||||
|
||||
def load_customized(
|
||||
self,
|
||||
component_model_path: str,
|
||||
server_args: ServerArgs,
|
||||
component_name: str = "connectors",
|
||||
*args,
|
||||
):
|
||||
config = self.load_component_config(component_model_path, component_name)
|
||||
component_weights_path = self.resolve_component_weights_path(
|
||||
component_model_path, server_args, component_name
|
||||
)
|
||||
|
||||
cls_name = config.pop("_class_name", None)
|
||||
if cls_name is None:
|
||||
raise ValueError(
|
||||
"Model config does not contain a _class_name attribute. "
|
||||
"Only diffusers format is supported."
|
||||
)
|
||||
|
||||
config.pop("_diffusers_version", None)
|
||||
config.pop("_name_or_path", None)
|
||||
|
||||
server_args.model_paths[component_name] = component_model_path
|
||||
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(cls_name)
|
||||
|
||||
# Not a fixed name: connectors follow DiT offload, while the duration
|
||||
# head stays resident unless selected explicitly.
|
||||
target_device = self.target_device(
|
||||
server_args.should_start_component_on_cpu(component_name)
|
||||
)
|
||||
default_dtype = resolve_precision(
|
||||
server_args, component_name, precision_attr="dit_precision"
|
||||
)
|
||||
|
||||
component_type = self.structural_component_type(component_name)
|
||||
config_cls = self._CONFIG_CLASSES[component_type]
|
||||
with set_default_torch_dtype(default_dtype), skip_init_modules():
|
||||
adapter_cfg = config_cls()
|
||||
adapter_cfg.update_model_arch(config)
|
||||
model = model_cls(adapter_cfg).to(device=target_device, dtype=default_dtype)
|
||||
|
||||
loaded = load_safetensors_state_dict(component_weights_path)
|
||||
mapping = adapter_cfg.arch_config.param_names_mapping
|
||||
loaded = {_remap_connector_key(k, mapping): v for k, v in loaded.items()}
|
||||
|
||||
missing, unexpected = model.load_state_dict(loaded, strict=False)
|
||||
# `strict=False` because a checkpoint carries either the shared
|
||||
# `text_proj_in` or the per-modality projections, never both. Anything
|
||||
# else uninitialized would surface later as garbage embeddings.
|
||||
if missing or unexpected:
|
||||
raise ValueError(
|
||||
f"Adapter weights at '{component_weights_path}' do not match the "
|
||||
f"instantiated {cls_name}. Missing: {sorted(missing)}. "
|
||||
f"Unexpected: {sorted(unexpected)}. This usually means the "
|
||||
"adapter config or its weight-name mapping is wrong."
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def _remap_connector_key(key: str, param_names_mapping: dict[str, str]) -> str:
|
||||
for pattern, replacement in param_names_mapping.items():
|
||||
key, replaced = re.subn(pattern, replacement, key)
|
||||
if replaced:
|
||||
break
|
||||
return key
|
||||
|
||||
@@ -1,127 +1,11 @@
|
||||
from copy import deepcopy
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
|
||||
from sglang.multimodal_gen.configs.models.bridges.mova_dual_tower import (
|
||||
MOVADualTowerConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
||||
PlainStateDictComponentLoader,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.fsdp_load import maybe_load_fsdp_model
|
||||
from sglang.multimodal_gen.runtime.loader.utils import _list_safetensors_files
|
||||
from sglang.multimodal_gen.runtime.loader.weight_load_plan import WeightLoadPlan
|
||||
from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency import (
|
||||
RESIDENT,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.registry import ModelRegistry
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.utils.precision import resolve_precision
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class BridgeLoader(PlainStateDictComponentLoader):
|
||||
"""Loader for MOVA dual tower bridge with FSDP support."""
|
||||
|
||||
pipeline_bridge_config_attr: str = "bridge_config"
|
||||
|
||||
component_names = ["dual_tower_bridge"]
|
||||
expected_library = "diffusers"
|
||||
|
||||
def load_customized(
|
||||
self, component_model_path: str, server_args: ServerArgs, component_name: str
|
||||
):
|
||||
config = self.load_component_config(component_model_path, component_name)
|
||||
component_weights_path = self.resolve_component_weights_path(
|
||||
component_model_path, server_args, component_name
|
||||
)
|
||||
hf_config = deepcopy(config)
|
||||
class_name = config.pop("_class_name", None)
|
||||
if class_name is None:
|
||||
raise ValueError(
|
||||
"Model config does not contain a _class_name attribute. "
|
||||
"Only diffusers format is supported."
|
||||
)
|
||||
server_args.model_paths[component_name] = component_model_path
|
||||
|
||||
# Try to get bridge config from pipeline config, fallback to creating one
|
||||
bridge_config = getattr(
|
||||
server_args.pipeline_config, self.pipeline_bridge_config_attr, None
|
||||
)
|
||||
if bridge_config is not None:
|
||||
bridge_config.update_model_arch(config)
|
||||
else:
|
||||
# Create a minimal config from hf_config
|
||||
from sglang.multimodal_gen.configs.models.bridges.mova_dual_tower import (
|
||||
MOVADualTowerConfig,
|
||||
)
|
||||
|
||||
bridge_config = MOVADualTowerConfig()
|
||||
bridge_config.update_model_arch(config)
|
||||
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
|
||||
# Find all safetensors files
|
||||
safetensors_list = _list_safetensors_files(component_weights_path)
|
||||
if not safetensors_list:
|
||||
raise ValueError(f"No safetensors files found in {component_weights_path}")
|
||||
|
||||
default_dtype = resolve_precision(
|
||||
server_args, component_name, precision_attr="dit_precision"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Loading %s from %s safetensors files, default_dtype: %s",
|
||||
class_name,
|
||||
len(safetensors_list),
|
||||
default_dtype,
|
||||
)
|
||||
|
||||
use_fsdp = server_args.should_use_fsdp_for_component(component_name)
|
||||
component_starts_on_cpu = server_args.should_start_component_on_cpu(
|
||||
component_name
|
||||
)
|
||||
|
||||
# Use the FSDP loader when FSDP is requested or shard rules are declared.
|
||||
fsdp_shard_conditions = getattr(model_cls, "_fsdp_shard_conditions", None)
|
||||
if (
|
||||
component_weights_path != component_model_path
|
||||
or use_fsdp
|
||||
or (
|
||||
server_args.residency_mode(component_name) == RESIDENT
|
||||
and server_args.hsdp_shard_dim is not None
|
||||
and fsdp_shard_conditions
|
||||
)
|
||||
):
|
||||
local_torch_device = get_local_torch_device()
|
||||
# Load with FSDP support
|
||||
model = maybe_load_fsdp_model(
|
||||
model_cls=model_cls,
|
||||
init_params={"config": bridge_config, "hf_config": hf_config},
|
||||
weight_dir_list=safetensors_list,
|
||||
device=local_torch_device,
|
||||
hsdp_replicate_dim=server_args.hsdp_replicate_dim,
|
||||
hsdp_shard_dim=server_args.hsdp_shard_dim,
|
||||
component_starts_on_cpu=component_starts_on_cpu,
|
||||
pin_cpu_memory=server_args.pin_cpu_memory,
|
||||
fsdp_inference=use_fsdp,
|
||||
param_dtype=default_dtype,
|
||||
reduce_dtype=torch.float32,
|
||||
output_dtype=None,
|
||||
strict=False,
|
||||
weight_load_plan=WeightLoadPlan(
|
||||
checkpoint_load_device=local_torch_device
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Fallback to simple loading (for non-FSDP or legacy models)
|
||||
model = model_cls.from_pretrained(
|
||||
component_model_path, torch_dtype=default_dtype
|
||||
)
|
||||
target_device = self.target_device(component_starts_on_cpu)
|
||||
model = model.to(device=target_device, dtype=default_dtype)
|
||||
|
||||
total_params = sum(p.numel() for p in model.parameters())
|
||||
logger.info("Loaded bridge model with %.2fM parameters", total_params / 1e6)
|
||||
|
||||
return model
|
||||
config_classes = {"dual_tower_bridge": MOVADualTowerConfig}
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import pkgutil
|
||||
import traceback
|
||||
from abc import ABC
|
||||
from collections.abc import Callable, Iterator
|
||||
from typing import Any, Type
|
||||
|
||||
import torch
|
||||
@@ -21,17 +22,29 @@ from transformers import (
|
||||
)
|
||||
from transformers.quantizers import AutoHfQuantizer
|
||||
|
||||
from sglang.multimodal_gen.configs.models.base import ModelConfig
|
||||
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
|
||||
from sglang.multimodal_gen.runtime.layers.attention.selector import (
|
||||
ComponentAttentionBackendNotAppliedError,
|
||||
component_attn_backend_context_manager,
|
||||
get_component_attn_backend_context,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.fsdp_load import maybe_load_fsdp_model
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
_list_safetensors_files,
|
||||
_normalize_component_type,
|
||||
component_name_to_loader_cls,
|
||||
finalize_loaded_model,
|
||||
format_component_residency,
|
||||
get_memory_usage_of_component,
|
||||
get_param_names_mapping,
|
||||
hf_to_custom_state_dict,
|
||||
initialize_model,
|
||||
load_model_state_dict,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.weight_load_plan import WeightLoadPlan
|
||||
from sglang.multimodal_gen.runtime.loader.weight_utils import (
|
||||
checkpoint_weights_iterator,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency import (
|
||||
RESIDENT,
|
||||
@@ -40,6 +53,8 @@ from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency
|
||||
from sglang.multimodal_gen.runtime.managers.memory_managers.component_residency_strategies import (
|
||||
is_fsdp_managed_module,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.dits.base import BaseDiT
|
||||
from sglang.multimodal_gen.runtime.models.registry import ModelRegistry
|
||||
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 (
|
||||
@@ -48,7 +63,10 @@ from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import (
|
||||
prepare_diffusers_component_path_for_loading,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.utils.precision import resolve_component_precision
|
||||
from sglang.multimodal_gen.runtime.utils.precision import (
|
||||
resolve_component_precision,
|
||||
resolve_precision,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.weights.source import (
|
||||
materialize_weight,
|
||||
resolve_weight,
|
||||
@@ -450,7 +468,7 @@ class ComponentLoader(ABC):
|
||||
# a parallel capability declaration for FSDP support.
|
||||
server_args.disable_fsdp_for_component(component_name)
|
||||
if isinstance(component, nn.Module):
|
||||
component = component.eval()
|
||||
component = finalize_loaded_model(component)
|
||||
if (
|
||||
not is_fsdp_managed_module(component)
|
||||
and not self._native_load_manages_placement
|
||||
@@ -678,6 +696,55 @@ class ComponentLoader(ABC):
|
||||
class WeightOverrideComponentLoader(ComponentLoader):
|
||||
"""Base for loaders that consume an exact weights-only override."""
|
||||
|
||||
ignored_checkpoint_prefixes: tuple[str, ...] = ()
|
||||
|
||||
def load_state_dict_model(
|
||||
self,
|
||||
model_cls: type[nn.Module],
|
||||
init_params: dict[str, Any],
|
||||
weight_files: list[str],
|
||||
server_args: ServerArgs,
|
||||
component_name: str,
|
||||
dtype: torch.dtype,
|
||||
*,
|
||||
component_starts_on_cpu: bool,
|
||||
weight_load_plan: WeightLoadPlan | None = None,
|
||||
checkpoint_key_filter: Callable[[str], bool] | None = None,
|
||||
weights_iterator: Iterator[tuple[str, torch.Tensor]] | None = None,
|
||||
) -> nn.Module:
|
||||
"""Restore mapped model state with optional TP/FSDP materialization."""
|
||||
return maybe_load_fsdp_model(
|
||||
model_cls=model_cls,
|
||||
init_params=init_params,
|
||||
weight_dir_list=weight_files,
|
||||
device=get_local_torch_device(),
|
||||
hsdp_replicate_dim=server_args.hsdp_replicate_dim,
|
||||
hsdp_shard_dim=server_args.hsdp_shard_dim,
|
||||
component_starts_on_cpu=component_starts_on_cpu,
|
||||
pin_cpu_memory=server_args.pin_cpu_memory,
|
||||
fsdp_inference=server_args.should_use_fsdp_for_component(component_name),
|
||||
param_dtype=dtype,
|
||||
reduce_dtype=torch.float32,
|
||||
strict=False,
|
||||
weight_load_plan=weight_load_plan,
|
||||
checkpoint_key_filter=checkpoint_key_filter,
|
||||
weights_iterator=weights_iterator,
|
||||
)
|
||||
|
||||
def validate_checkpoint_keys(
|
||||
self, missing: list[str] | set[str], unexpected: list[str], component_name: str
|
||||
) -> None:
|
||||
unexpected = [
|
||||
name
|
||||
for name in unexpected
|
||||
if not name.startswith(self.ignored_checkpoint_prefixes)
|
||||
]
|
||||
if missing or unexpected:
|
||||
raise ComponentCheckpointUnsupportedError(
|
||||
f"Checkpoint weights do not match {component_name!r}. "
|
||||
f"Missing: {sorted(missing)}. Unexpected: {sorted(unexpected)}."
|
||||
)
|
||||
|
||||
def resolve_component_weight_override(
|
||||
self, server_args: ServerArgs, component_name: str
|
||||
) -> str | None:
|
||||
@@ -711,7 +778,97 @@ class OnlineQuantizationComponentLoader(WeightOverrideComponentLoader):
|
||||
|
||||
|
||||
class PlainStateDictComponentLoader(WeightOverrideComponentLoader):
|
||||
"""Base for native loaders whose current materializer expects plain weights."""
|
||||
"""Construct registered modules and restore a complete plain state dict."""
|
||||
|
||||
expected_library = "diffusers"
|
||||
config_classes: dict[str, type[ModelConfig]] = {}
|
||||
default_precision_attr = "dit_precision"
|
||||
default_dtype = torch.bfloat16
|
||||
|
||||
def load_customized(
|
||||
self, component_model_path: str, server_args: ServerArgs, component_name: str
|
||||
) -> nn.Module:
|
||||
config = self.load_component_config(component_model_path, component_name)
|
||||
class_name = config.pop("_class_name", None) or self.component_architecture
|
||||
if class_name is None:
|
||||
raise ComponentCheckpointUnsupportedError(
|
||||
f"{component_name!r} must declare _class_name in config.json "
|
||||
"or its architecture in model_index.json"
|
||||
)
|
||||
weights_path = self.resolve_component_weights_path(
|
||||
component_model_path, server_args, component_name
|
||||
)
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
model_config = self.build_model_config(config, component_name)
|
||||
dtype = self.resolve_dtype(server_args, component_name)
|
||||
component_starts_on_cpu = server_args.should_start_component_on_cpu(
|
||||
component_name
|
||||
)
|
||||
server_args.model_paths[component_name] = component_model_path
|
||||
if issubclass(model_cls, BaseDiT):
|
||||
weight_files = _list_safetensors_files(weights_path)
|
||||
return self.load_state_dict_model(
|
||||
model_cls,
|
||||
{"config": model_config, "hf_config": config},
|
||||
weight_files,
|
||||
server_args,
|
||||
component_name,
|
||||
dtype,
|
||||
component_starts_on_cpu=component_starts_on_cpu,
|
||||
weights_iterator=(
|
||||
None if weight_files else checkpoint_weights_iterator(weights_path)
|
||||
),
|
||||
)
|
||||
|
||||
target_device = self.target_device(component_starts_on_cpu)
|
||||
model = initialize_model(
|
||||
model_cls,
|
||||
model_config
|
||||
if isinstance(model_config, dict)
|
||||
else {"config": model_config},
|
||||
dtype,
|
||||
).to(target_device)
|
||||
|
||||
try:
|
||||
state_dict, _ = hf_to_custom_state_dict(
|
||||
checkpoint_weights_iterator(weights_path),
|
||||
get_param_names_mapping(
|
||||
model_config.arch_config.param_names_mapping
|
||||
if isinstance(model_config, ModelConfig)
|
||||
else {}
|
||||
),
|
||||
valid_target_names=set(model.state_dict()),
|
||||
strict=True,
|
||||
)
|
||||
missing, unexpected = load_model_state_dict(model, state_dict, strict=False)
|
||||
except (RuntimeError, ValueError) as error:
|
||||
raise ComponentCheckpointUnsupportedError(
|
||||
f"Cannot restore checkpoint for {component_name!r}: {error}"
|
||||
) from error
|
||||
self.validate_checkpoint_keys(missing, unexpected, component_name)
|
||||
return model
|
||||
|
||||
def build_model_config(
|
||||
self, config: dict[str, Any], component_name: str
|
||||
) -> ModelConfig | dict[str, Any]:
|
||||
config_cls = self.config_classes.get(
|
||||
self.structural_component_type(component_name)
|
||||
)
|
||||
if config_cls is not None:
|
||||
model_config = config_cls()
|
||||
model_config.update_model_arch(config)
|
||||
return model_config
|
||||
return {key: value for key, value in config.items() if not key.startswith("_")}
|
||||
|
||||
def resolve_dtype(
|
||||
self, server_args: ServerArgs, component_name: str
|
||||
) -> torch.dtype:
|
||||
try:
|
||||
return resolve_precision(
|
||||
server_args, component_name, precision_attr=self.default_precision_attr
|
||||
)
|
||||
except AttributeError:
|
||||
return self.default_dtype
|
||||
|
||||
def component_load_precision(
|
||||
self, server_args: ServerArgs, component_name: str
|
||||
|
||||
+3
-49
@@ -6,57 +6,11 @@ from sglang.multimodal_gen.configs.models.decoders.ltx_2_5_diffusion_decoder imp
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
||||
PlainStateDictComponentLoader,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
load_safetensors_state_dict,
|
||||
set_default_torch_dtype,
|
||||
skip_init_modules,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.registry import ModelRegistry
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.precision import resolve_precision
|
||||
|
||||
|
||||
class DiffusionDecoderLoader(PlainStateDictComponentLoader):
|
||||
"""Loader for the standalone, replicated LTX-2.5 diffusion decoder."""
|
||||
"""Standalone, replicated LTX-2.5 diffusion decoder."""
|
||||
|
||||
component_names = ["diffusion_decoder"]
|
||||
expected_library = "diffusers"
|
||||
|
||||
def load_customized(
|
||||
self,
|
||||
component_model_path: str,
|
||||
server_args: ServerArgs,
|
||||
component_name: str = "diffusion_decoder",
|
||||
*args,
|
||||
):
|
||||
config = self.load_component_config(component_model_path, component_name)
|
||||
component_weights_path = self.resolve_component_weights_path(
|
||||
component_model_path, server_args, component_name
|
||||
)
|
||||
class_name = config.pop("_class_name", None)
|
||||
if class_name is None:
|
||||
raise ValueError(
|
||||
"Model config does not contain a _class_name attribute. "
|
||||
"Only diffusers format is supported."
|
||||
)
|
||||
config.pop("_diffusers_version", None)
|
||||
config.pop("_name_or_path", None)
|
||||
|
||||
server_args.model_paths[component_name] = component_model_path
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
target_device = self.target_device(
|
||||
server_args.should_start_component_on_cpu(component_name)
|
||||
)
|
||||
dtype = resolve_precision(
|
||||
server_args, component_name, precision_attr="vae_precision"
|
||||
)
|
||||
|
||||
decoder_config = LTX25DiffusionDecoderConfig()
|
||||
decoder_config.update_model_arch(config)
|
||||
with set_default_torch_dtype(dtype), skip_init_modules():
|
||||
model = model_cls(decoder_config).to(device=target_device, dtype=dtype)
|
||||
|
||||
model.load_state_dict(
|
||||
load_safetensors_state_dict(component_weights_path), strict=True
|
||||
)
|
||||
return model
|
||||
config_classes = {"diffusion_decoder": LTX25DiffusionDecoderConfig}
|
||||
default_precision_attr = "vae_precision"
|
||||
|
||||
+3
-54
@@ -1,15 +1,7 @@
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.text_encoder_loader import (
|
||||
TextEncoderLoader,
|
||||
_resolve_and_configure_encoder_quantization,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.encoders.base import finalize_encoder_folding
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import (
|
||||
get_diffusers_component_config,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class ImageEncoderLoader(TextEncoderLoader):
|
||||
@@ -23,52 +15,9 @@ class ImageEncoderLoader(TextEncoderLoader):
|
||||
component_name, server_args.pipeline_config.image_encoder_precision
|
||||
)
|
||||
|
||||
def load_customized(
|
||||
self,
|
||||
component_model_path: str,
|
||||
server_args: ServerArgs,
|
||||
component_name: str = "image_encoder",
|
||||
def build_model_config(
|
||||
self, component_model_path, model_config, server_args, component_name
|
||||
):
|
||||
"""Load the text encoders based on the model path, and inference args."""
|
||||
component_weights_path = self.resolve_component_weights_path(
|
||||
component_model_path,
|
||||
server_args,
|
||||
component_name,
|
||||
)
|
||||
# model_config: PretrainedConfig = get_hf_config(
|
||||
# model=model_path,
|
||||
# trust_remote_code=server_args.trust_remote_code,
|
||||
# revision=server_args.revision,
|
||||
# model_override_args=None,
|
||||
# )
|
||||
model_config = get_diffusers_component_config(
|
||||
component_path=component_model_path
|
||||
)
|
||||
|
||||
encoder_config = server_args.pipeline_config.image_encoder_config
|
||||
encoder_config.update_model_arch(model_config)
|
||||
_resolve_and_configure_encoder_quantization(
|
||||
encoder_config,
|
||||
model_config,
|
||||
component_model_path,
|
||||
component_weights_path,
|
||||
component_name,
|
||||
server_args.component_quantizations.get(component_name),
|
||||
)
|
||||
# real dims are populated now; resolve fold vs replicate
|
||||
finalize_encoder_folding(
|
||||
encoder_config,
|
||||
server_args.encoder_parallel,
|
||||
)
|
||||
|
||||
# Always start with local device; load_model will adjust for offload if needed
|
||||
# TODO(will): add support for other dtypes
|
||||
image_encoder_dtype = self.component_load_precision(server_args, component_name)
|
||||
assert image_encoder_dtype is not None
|
||||
return self.load_model(
|
||||
component_weights_path,
|
||||
encoder_config,
|
||||
server_args,
|
||||
image_encoder_dtype,
|
||||
component_name=component_name,
|
||||
)
|
||||
return encoder_config
|
||||
|
||||
+5
-61
@@ -1,71 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
||||
PlainStateDictComponentLoader,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
load_safetensors_state_dict,
|
||||
set_default_torch_dtype,
|
||||
skip_init_modules,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.registry import ModelRegistry
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.utils.precision import resolve_component_precision
|
||||
from sglang.multimodal_gen.utils import PRECISION_TO_TYPE
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class SoundTokenizerLoader(PlainStateDictComponentLoader):
|
||||
component_names = ["sound_tokenizer"]
|
||||
expected_library = "diffusers"
|
||||
default_precision_attr = "vae_precision"
|
||||
# the native tokenizer is decoder-only; encoder weights are unused
|
||||
ignored_checkpoint_prefixes = ("encoder.",)
|
||||
|
||||
def load_customized(
|
||||
self, component_model_path: str, server_args: ServerArgs, component_name: str
|
||||
):
|
||||
config = self.load_component_config(component_model_path, component_name)
|
||||
component_weights_path = self.resolve_component_weights_path(
|
||||
component_model_path, server_args, component_name
|
||||
)
|
||||
class_name = config.pop("_class_name", None) or self.component_architecture
|
||||
assert class_name is not None, (
|
||||
"Sound tokenizer class name must be available from component config."
|
||||
)
|
||||
|
||||
server_args.model_paths[component_name] = component_model_path
|
||||
|
||||
dtype = resolve_component_precision(server_args, component_name)
|
||||
if dtype is None:
|
||||
try:
|
||||
dtype = PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision]
|
||||
except AttributeError:
|
||||
dtype = torch.bfloat16
|
||||
target_device = self.target_device(
|
||||
server_args.should_start_component_on_cpu(component_name)
|
||||
)
|
||||
|
||||
with set_default_torch_dtype(dtype), skip_init_modules():
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
model = model_cls(config).to(device=target_device, dtype=dtype)
|
||||
|
||||
loaded = load_safetensors_state_dict(component_weights_path)
|
||||
incompatible = model.load_state_dict(loaded, strict=False)
|
||||
missing = getattr(incompatible, "missing_keys", [])
|
||||
# The tokenizer is decoder-only; the checkpoint's encoder weights are
|
||||
# expected leftovers, so they're excluded from the load warning.
|
||||
unexpected = [
|
||||
k
|
||||
for k in getattr(incompatible, "unexpected_keys", [])
|
||||
if not k.startswith("encoder.")
|
||||
]
|
||||
if missing or unexpected:
|
||||
logger.warning(
|
||||
"Loaded sound_tokenizer with missing_keys=%d unexpected_keys=%d",
|
||||
len(missing),
|
||||
len(unexpected),
|
||||
)
|
||||
model.eval()
|
||||
return model
|
||||
def build_model_config(self, config, component_name):
|
||||
return {"config": config}
|
||||
|
||||
+68
-293
@@ -1,9 +1,6 @@
|
||||
import dataclasses
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Callable, Generator, Iterable
|
||||
from typing import cast
|
||||
from collections.abc import Generator
|
||||
|
||||
import torch
|
||||
import transformers
|
||||
@@ -67,20 +64,14 @@ from sglang.multimodal_gen.runtime.loader.gguf_weights import (
|
||||
remap_gguf_tensor_meta,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
_list_safetensors_files,
|
||||
checkpoint_bytes,
|
||||
get_param_names_mapping,
|
||||
initialize_model,
|
||||
keep_checkpoint_mapped,
|
||||
set_default_torch_dtype,
|
||||
skip_init_modules,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.weight_utils import (
|
||||
filter_files_not_needed_for_inference,
|
||||
pt_weights_iterator,
|
||||
safetensors_weights_iterator,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.managers.memory_managers.host_memory_budget import (
|
||||
host_copies_would_not_fit,
|
||||
host_memory_available_bytes,
|
||||
checkpoint_weights_iterator,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.encoders.base import (
|
||||
EncoderTensorParallelMixin,
|
||||
@@ -101,10 +92,10 @@ from sglang.multimodal_gen.runtime.utils.quantization_utils import (
|
||||
get_quant_config,
|
||||
get_quant_config_from_safetensors_metadata,
|
||||
inspect_comfy_quant_markers,
|
||||
process_model_weights_after_loading,
|
||||
resolve_comfy_checkpoint_quantization,
|
||||
)
|
||||
from sglang.multimodal_gen.utils import PRECISION_TO_TYPE
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.linear import LinearBase as SrtLinearBase
|
||||
from sglang.srt.layers.quantization.fp8 import Fp8Config as SrtFp8Config
|
||||
from sglang.srt.layers.quantization.unquant import (
|
||||
@@ -113,7 +104,6 @@ from sglang.srt.layers.quantization.unquant import (
|
||||
from sglang.srt.model_loader.checkpoint_quantization import (
|
||||
resolve_checkpoint_quant_spec,
|
||||
)
|
||||
from sglang.srt.model_loader.post_load import stage_module_for_post_load
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -340,7 +330,7 @@ def _resolve_and_configure_encoder_quantization(
|
||||
explicit_quantization: str | None = None,
|
||||
ignored_layers: list[str] | None = None,
|
||||
) -> type[nn.Module]:
|
||||
architectures = getattr(model_config, "architectures", [])
|
||||
architectures = model_config.arch_config.architectures
|
||||
try:
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(architectures)
|
||||
except Exception as resolution_error:
|
||||
@@ -384,35 +374,6 @@ def _resolve_and_configure_encoder_quantization(
|
||||
return model_cls
|
||||
|
||||
|
||||
def _process_quantized_encoder_weights(
|
||||
model: nn.Module,
|
||||
process_device: torch.device | None,
|
||||
component_name: str,
|
||||
) -> int:
|
||||
processed_layers = 0
|
||||
for module in model.modules():
|
||||
if not isinstance(module, (LinearBase, SrtLinearBase)):
|
||||
continue
|
||||
quant_method = module.quant_method
|
||||
if quant_method is None or isinstance(
|
||||
quant_method,
|
||||
(UnquantizedLinearMethod, SrtUnquantizedLinearMethod),
|
||||
):
|
||||
continue
|
||||
if process_device is None:
|
||||
quant_method.process_weights_after_loading(module)
|
||||
else:
|
||||
with stage_module_for_post_load(module, process_device):
|
||||
quant_method.process_weights_after_loading(module)
|
||||
processed_layers += 1
|
||||
if processed_layers == 0:
|
||||
raise ValueError(
|
||||
f"The {component_name!r} checkpoint declares quantization, but the "
|
||||
"model did not construct any quantized linear layers"
|
||||
)
|
||||
return processed_layers
|
||||
|
||||
|
||||
def _require_quantized_encoder_layers(
|
||||
model: nn.Module,
|
||||
component_name: str,
|
||||
@@ -463,21 +424,6 @@ def _require_quantized_encoder_layers(
|
||||
)
|
||||
|
||||
|
||||
def _keep_this_checkpoint_mapped(model_path: str) -> bool:
|
||||
"""Whether this encoder's weights should stay on their file mapping."""
|
||||
weight_bytes = checkpoint_bytes(model_path)
|
||||
if not host_copies_would_not_fit(weight_bytes):
|
||||
return False
|
||||
logger.info(
|
||||
"Text encoder checkpoint is %.2f GiB against %.2f GiB of host memory, "
|
||||
"so its compatible weights stay on the checkpoint mapping instead of "
|
||||
"being copied in.",
|
||||
weight_bytes / 1024**3,
|
||||
host_memory_available_bytes() / 1024**3,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
"""Loader for text encoders."""
|
||||
|
||||
@@ -491,7 +437,7 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
if override is not None:
|
||||
return override
|
||||
return server_args.pipeline_config.text_encoder_precisions[
|
||||
self._extract_encoder_index(component_name)
|
||||
self._extract_encoder_index(self.structural_component_name(component_name))
|
||||
]
|
||||
|
||||
def should_raise_customized_load_error(
|
||||
@@ -510,22 +456,6 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
f"no {current_platform.device_type} implementation"
|
||||
)
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Source:
|
||||
"""A source for weights."""
|
||||
|
||||
model_or_path: str
|
||||
"""The model ID or path."""
|
||||
|
||||
prefix: str = ""
|
||||
"""A prefix to prepend to all weights."""
|
||||
|
||||
fall_back_to_pt: bool = True
|
||||
"""Whether .pt weights can be used."""
|
||||
|
||||
allow_patterns_overrides: list[str] | None = None
|
||||
"""If defined, weights will load exclusively using these patterns."""
|
||||
|
||||
def resolve_native_transformers_model_class(self, config: PretrainedConfig) -> type:
|
||||
"""Resolve the concrete transformers class for a text encoder.
|
||||
|
||||
@@ -543,149 +473,23 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
return transformers_model_class
|
||||
return transformers.AutoModel
|
||||
|
||||
def _prepare_weights(
|
||||
self,
|
||||
model_name_or_path: str,
|
||||
fall_back_to_pt: bool,
|
||||
allow_patterns_overrides: list[str] | None,
|
||||
key_filter: Callable[[str], bool] | None = None,
|
||||
) -> tuple[str, list[str], bool]:
|
||||
"""Prepare weights for the model.
|
||||
|
||||
If the model is not local, it will be downloaded."""
|
||||
# model_name_or_path = (self._maybe_download_from_modelscope(
|
||||
# model_name_or_path, revision) or model_name_or_path)
|
||||
|
||||
if os.path.isfile(model_name_or_path):
|
||||
if model_name_or_path.endswith(".safetensors"):
|
||||
return os.path.dirname(model_name_or_path), [model_name_or_path], True
|
||||
if fall_back_to_pt and model_name_or_path.endswith((".bin", ".pt")):
|
||||
return os.path.dirname(model_name_or_path), [model_name_or_path], False
|
||||
raise ValueError(
|
||||
"Native encoder weight overrides currently support one "
|
||||
f"safetensors, bin, or pt file, got {model_name_or_path!r}"
|
||||
)
|
||||
if not os.path.isdir(model_name_or_path):
|
||||
raise ValueError(
|
||||
f"Model path must be a local file or directory: {model_name_or_path!r}"
|
||||
)
|
||||
|
||||
use_safetensors = False
|
||||
index_file = SAFE_WEIGHTS_INDEX_NAME
|
||||
allow_patterns = ["*.safetensors", "*.bin"]
|
||||
|
||||
if fall_back_to_pt:
|
||||
allow_patterns += ["*.pt"]
|
||||
|
||||
if allow_patterns_overrides is not None:
|
||||
allow_patterns = allow_patterns_overrides
|
||||
|
||||
hf_folder = model_name_or_path
|
||||
|
||||
hf_weights_files: list[str] = []
|
||||
for pattern in allow_patterns:
|
||||
if pattern == "*.safetensors":
|
||||
hf_weights_files = _list_safetensors_files(
|
||||
hf_folder,
|
||||
index_file=index_file,
|
||||
key_filter=key_filter,
|
||||
)
|
||||
else:
|
||||
hf_weights_files = glob.glob(os.path.join(hf_folder, pattern))
|
||||
if len(hf_weights_files) > 0:
|
||||
if pattern == "*.safetensors":
|
||||
use_safetensors = True
|
||||
break
|
||||
|
||||
if not use_safetensors:
|
||||
hf_weights_files = filter_files_not_needed_for_inference(hf_weights_files)
|
||||
|
||||
if len(hf_weights_files) == 0:
|
||||
raise RuntimeError(
|
||||
f"Cannot find any model weights with `{model_name_or_path}`"
|
||||
)
|
||||
|
||||
# Sort weight files when SGLANG_SORT_WEIGHT_FILES >= 0 (default).
|
||||
# Staggering is not applicable to text-encoder loading (no TP split).
|
||||
if envs.SGLANG_SORT_WEIGHT_FILES.get() >= 0:
|
||||
hf_weights_files.sort()
|
||||
|
||||
return hf_folder, hf_weights_files, use_safetensors
|
||||
|
||||
def _get_weights_iterator(
|
||||
self,
|
||||
source: "Source",
|
||||
to_cpu: bool,
|
||||
key_filter: Callable[[str], bool] | None = None,
|
||||
) -> Generator[tuple[str, torch.Tensor], None, None]:
|
||||
"""get an iterator for the model weights based on the load format."""
|
||||
source_key_filter: Callable[[str], bool] | None
|
||||
if key_filter is None:
|
||||
source_key_filter = None
|
||||
else:
|
||||
|
||||
def include_source_weight(name: str) -> bool:
|
||||
return key_filter(source.prefix + name)
|
||||
|
||||
source_key_filter = include_source_weight
|
||||
|
||||
hf_folder, hf_weights_files, use_safetensors = self._prepare_weights(
|
||||
source.model_or_path,
|
||||
source.fall_back_to_pt,
|
||||
source.allow_patterns_overrides,
|
||||
key_filter=source_key_filter,
|
||||
)
|
||||
if use_safetensors:
|
||||
weights_iterator = safetensors_weights_iterator(
|
||||
hf_weights_files,
|
||||
to_cpu=to_cpu,
|
||||
key_filter=source_key_filter,
|
||||
)
|
||||
else:
|
||||
weights_iterator = pt_weights_iterator(hf_weights_files, to_cpu=to_cpu)
|
||||
if source_key_filter is not None:
|
||||
weights_iterator = (
|
||||
(name, tensor)
|
||||
for name, tensor in weights_iterator
|
||||
if source_key_filter(name)
|
||||
)
|
||||
|
||||
# apply the prefix.
|
||||
return ((source.prefix + name, tensor) for (name, tensor) in weights_iterator)
|
||||
|
||||
def _get_all_weights(
|
||||
self,
|
||||
model: EncoderTensorParallelMixin,
|
||||
model_path: str,
|
||||
to_cpu: bool,
|
||||
) -> Generator[tuple[str, torch.Tensor], None, None]:
|
||||
key_filter = model.should_materialize_checkpoint_weight
|
||||
|
||||
def include_checkpoint_weight(name: str) -> bool:
|
||||
return not name.endswith(".comfy_quant") and key_filter(name)
|
||||
return not name.endswith(
|
||||
".comfy_quant"
|
||||
) and model.should_materialize_checkpoint_weight(name)
|
||||
|
||||
primary_weights = TextEncoderLoader.Source(
|
||||
yield from checkpoint_weights_iterator(
|
||||
model_path,
|
||||
prefix="",
|
||||
fall_back_to_pt=getattr(model, "fall_back_to_pt_during_load", True),
|
||||
allow_patterns_overrides=getattr(model, "allow_patterns_overrides", None),
|
||||
to_cpu=to_cpu,
|
||||
key_filter=include_checkpoint_weight,
|
||||
index_file=SAFE_WEIGHTS_INDEX_NAME,
|
||||
)
|
||||
yield from self._get_weights_iterator(
|
||||
primary_weights,
|
||||
to_cpu,
|
||||
include_checkpoint_weight,
|
||||
)
|
||||
|
||||
secondary_weights = cast(
|
||||
Iterable[TextEncoderLoader.Source],
|
||||
getattr(model, "secondary_weights", ()),
|
||||
)
|
||||
for source in secondary_weights:
|
||||
yield from self._get_weights_iterator(
|
||||
source,
|
||||
to_cpu,
|
||||
include_checkpoint_weight,
|
||||
)
|
||||
|
||||
def load_customized(
|
||||
self,
|
||||
@@ -700,35 +504,13 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
server_args,
|
||||
component_name,
|
||||
)
|
||||
diffusers_pretrained_config = get_config(
|
||||
component_model_path, trust_remote_code=True
|
||||
)
|
||||
model_config = get_diffusers_component_config(
|
||||
component_path=component_model_path
|
||||
)
|
||||
|
||||
# TODO(mick): had to throw an exception for different text-encoder arch
|
||||
encoder_index = self._extract_encoder_index(
|
||||
self.structural_component_name(component_name)
|
||||
encoder_config = self.build_model_config(
|
||||
component_model_path, model_config, server_args, component_name
|
||||
)
|
||||
assert encoder_index < len(
|
||||
server_args.pipeline_config.text_encoder_configs
|
||||
) and encoder_index < len(server_args.pipeline_config.text_encoder_precisions)
|
||||
|
||||
encoder_config = server_args.pipeline_config.text_encoder_configs[encoder_index]
|
||||
encoder_config.update_model_arch(model_config)
|
||||
encoder_config.generation_config = load_dict(
|
||||
os.path.join(component_model_path, "generation_config.json")
|
||||
)
|
||||
|
||||
if encoder_index == 0:
|
||||
for key, value in diffusers_pretrained_config.__dict__.items():
|
||||
setattr(encoder_config.arch_config, key, value)
|
||||
post_diffusers_config_update = getattr(
|
||||
encoder_config, "post_diffusers_config_update", None
|
||||
)
|
||||
if post_diffusers_config_update is not None:
|
||||
post_diffusers_config_update()
|
||||
encoder_config.post_diffusers_config_update()
|
||||
model_cls = _resolve_and_configure_encoder_quantization(
|
||||
encoder_config,
|
||||
model_config,
|
||||
@@ -778,6 +560,34 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
f"Failed to load quantized native {component_name!r}: {error}"
|
||||
) from error
|
||||
|
||||
def build_model_config(
|
||||
self,
|
||||
component_model_path: str,
|
||||
model_config: dict,
|
||||
server_args: ServerArgs,
|
||||
component_name: str,
|
||||
) -> EncoderConfig:
|
||||
diffusers_pretrained_config = get_config(
|
||||
component_model_path, trust_remote_code=True
|
||||
)
|
||||
encoder_index = self._extract_encoder_index(
|
||||
self.structural_component_name(component_name)
|
||||
)
|
||||
assert encoder_index < len(
|
||||
server_args.pipeline_config.text_encoder_configs
|
||||
) and encoder_index < len(server_args.pipeline_config.text_encoder_precisions)
|
||||
|
||||
encoder_config = server_args.pipeline_config.text_encoder_configs[encoder_index]
|
||||
encoder_config.update_model_arch(model_config)
|
||||
encoder_config.generation_config = load_dict(
|
||||
os.path.join(component_model_path, "generation_config.json")
|
||||
)
|
||||
|
||||
if encoder_index == 0:
|
||||
for key, value in diffusers_pretrained_config.__dict__.items():
|
||||
setattr(encoder_config.arch_config, key, value)
|
||||
return encoder_config
|
||||
|
||||
@staticmethod
|
||||
def _extract_encoder_index(component_name: str) -> int:
|
||||
"""
|
||||
@@ -872,20 +682,20 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
use_tensor_parallel_group(encoder_tp_group),
|
||||
set_default_torch_dtype(PRECISION_TO_TYPE[dtype]),
|
||||
):
|
||||
with model_device, skip_init_modules():
|
||||
architectures = getattr(model_config, "architectures", [])
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(architectures)
|
||||
enable_image_understanding = isinstance(
|
||||
server_args.pipeline_config,
|
||||
(QwenImageEditPipelineConfig, LongCatImageEditPipelineConfig),
|
||||
)
|
||||
model_config.enable_image_understanding = enable_image_understanding
|
||||
# LongCat feeds its padded body to the DiT, so it must mask
|
||||
# padding on the cache-free path; scoped so others are unchanged.
|
||||
model_config.honor_cache_free_padding_mask = isinstance(
|
||||
server_args.pipeline_config, LongCatImagePipelineConfig
|
||||
)
|
||||
model = model_cls(model_config)
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(
|
||||
model_config.arch_config.architectures
|
||||
)
|
||||
model_config.enable_image_understanding = isinstance(
|
||||
server_args.pipeline_config,
|
||||
(QwenImageEditPipelineConfig, LongCatImageEditPipelineConfig),
|
||||
)
|
||||
# longcat consumes the padded body without an attention cache
|
||||
model_config.honor_cache_free_padding_mask = isinstance(
|
||||
server_args.pipeline_config, LongCatImagePipelineConfig
|
||||
)
|
||||
model = initialize_model(
|
||||
model_cls, {"config": model_config}, param_dtype, model_device
|
||||
)
|
||||
|
||||
if not isinstance(model, EncoderTensorParallelMixin):
|
||||
raise TypeError(
|
||||
@@ -904,15 +714,11 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
)
|
||||
|
||||
if component_starts_on_cpu and (
|
||||
current_platform.is_mps() or _keep_this_checkpoint_mapped(model_path)
|
||||
current_platform.is_mps()
|
||||
or keep_checkpoint_mapped(
|
||||
weight_bytes=checkpoint_bytes(model_path), component=component_name
|
||||
)
|
||||
):
|
||||
# The encoder is layered immediately after this loader returns,
|
||||
# so compatible CPU safetensors can stay mapped instead of being
|
||||
# copied. On MPS that is always the right call -- the memory is
|
||||
# unified. On any host it becomes the only call once the
|
||||
# checkpoint is larger than host memory, because the copy is
|
||||
# what does not fit: H3's encoder is 62.13 GiB against a 32 GiB
|
||||
# target.
|
||||
model._keep_checkpoint_mapping = True
|
||||
|
||||
weights_to_load = {name for name, _ in model.named_parameters()}
|
||||
@@ -931,6 +737,9 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
if isinstance(quant_config, QuantoInt8Config):
|
||||
checkpoint_weights = normalize_quanto_int8_weights(checkpoint_weights)
|
||||
loaded_weights = model.load_weights(checkpoint_weights)
|
||||
self.validate_checkpoint_keys(
|
||||
weights_to_load - loaded_weights, [], component_name
|
||||
)
|
||||
|
||||
if quant_config is not None and not isinstance(quant_config, GGUFConfig):
|
||||
postprocess_device: torch.device | None = local_torch_device
|
||||
@@ -939,10 +748,10 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
and quant_config.is_checkpoint_int8_serialized
|
||||
):
|
||||
postprocess_device = None
|
||||
processed_layers = _process_quantized_encoder_weights(
|
||||
processed_layers = process_model_weights_after_loading(
|
||||
model,
|
||||
postprocess_device,
|
||||
component_name,
|
||||
quantized_only=True,
|
||||
)
|
||||
logger.info(
|
||||
"Processed %d %s linear layers for %s",
|
||||
@@ -961,39 +770,5 @@ class TextEncoderLoader(OnlineQuantizationComponentLoader):
|
||||
model = model.to("cpu")
|
||||
else:
|
||||
model = model.to(local_torch_device)
|
||||
# We only enable strict check for non-quantized models
|
||||
# that have loaded weights tracking currently.
|
||||
# if loaded_weights is not None:
|
||||
weights_not_loaded = weights_to_load - loaded_weights
|
||||
if weights_not_loaded:
|
||||
# NOTE:
|
||||
# If we silently continue with uninitialized weights, the text encoder can
|
||||
# produce NaNs/garbage embeddings that later fail stage verification in a
|
||||
# hard-to-debug way (e.g., `prompt_embeds` fails the NaN check).
|
||||
#
|
||||
# We allow a small set of known-optional parameters to be missing, but
|
||||
# default to strict behavior for the rest.
|
||||
allowed_missing_patterns = (
|
||||
getattr(model, "_allowed_missing_weights_patterns", []) or []
|
||||
)
|
||||
unexpected_missing = {
|
||||
n
|
||||
for n in weights_not_loaded
|
||||
if not any(pat in n for pat in allowed_missing_patterns)
|
||||
}
|
||||
if unexpected_missing:
|
||||
raise ValueError(
|
||||
"Following text encoder weights were not initialized from checkpoint: "
|
||||
f"{sorted(unexpected_missing)}. "
|
||||
"This usually indicates a checkpoint/model-arch mismatch or a broken "
|
||||
"weight-name mapping. If these are truly optional, set "
|
||||
"`model._allowed_missing_weights_patterns` to whitelist patterns."
|
||||
)
|
||||
logger.warning(
|
||||
"Following (allowed) text encoder weights were not initialized from "
|
||||
"checkpoint: %s (allowed patterns: %s)",
|
||||
sorted(weights_not_loaded),
|
||||
allowed_missing_patterns,
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
+5
-12
@@ -16,7 +16,6 @@ from sglang.multimodal_gen.runtime.layers.attention.selector import (
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
||||
OnlineQuantizationComponentLoader,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.fsdp_load import maybe_load_fsdp_model
|
||||
from sglang.multimodal_gen.runtime.loader.gguf_weights import gguf_weights_iterator
|
||||
from sglang.multimodal_gen.runtime.loader.minimax_h3_weights import (
|
||||
comfy_quant_key_filter,
|
||||
@@ -525,20 +524,14 @@ class TransformerLoader(OnlineQuantizationComponentLoader):
|
||||
# Model construction resolves attention implementations, so apply the
|
||||
# quantization-specific default around FSDP initialization and loading.
|
||||
with attn_backend_context:
|
||||
model = maybe_load_fsdp_model(
|
||||
model = self.load_state_dict_model(
|
||||
model_cls=model_cls,
|
||||
init_params=init_params,
|
||||
weight_dir_list=safetensors_list,
|
||||
device=local_torch_device,
|
||||
hsdp_replicate_dim=server_args.hsdp_replicate_dim,
|
||||
hsdp_shard_dim=server_args.hsdp_shard_dim,
|
||||
weight_files=safetensors_list,
|
||||
server_args=component_server_args,
|
||||
component_name=component_name,
|
||||
component_starts_on_cpu=component_starts_on_cpu,
|
||||
pin_cpu_memory=component_server_args.pin_cpu_memory,
|
||||
fsdp_inference=use_fsdp,
|
||||
param_dtype=quant_spec.param_dtype,
|
||||
reduce_dtype=torch.float32,
|
||||
output_dtype=None,
|
||||
strict=False,
|
||||
dtype=quant_spec.param_dtype,
|
||||
weight_load_plan=weight_load_plan,
|
||||
checkpoint_key_filter=checkpoint_key_filter,
|
||||
weights_iterator=(
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from safetensors.torch import load_file as safetensors_load_file
|
||||
from safetensors.torch import safe_open
|
||||
from safetensors.torch import save_file as safetensors_save_file
|
||||
from torch.nn.utils import parametrize
|
||||
|
||||
from sglang.multimodal_gen import envs
|
||||
from sglang.multimodal_gen.configs.models.vaes.base import VAEConfig
|
||||
@@ -25,10 +23,12 @@ from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader imp
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
_list_safetensors_files,
|
||||
_normalize_component_type,
|
||||
adopt_plain_weight_norm_state,
|
||||
checkpoint_bytes,
|
||||
initialize_model,
|
||||
keep_checkpoint_mapped,
|
||||
load_model_state_dict,
|
||||
set_default_torch_dtype,
|
||||
skip_init_modules,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.weight_utils import (
|
||||
safetensors_weights_iterator,
|
||||
@@ -283,56 +283,6 @@ def _hold_decoder_weights_in_decode_dtype(
|
||||
)
|
||||
|
||||
|
||||
def _match_checkpoint_dtypes(loaded: dict, target_state: dict) -> dict:
|
||||
"""Convert checkpoint tensors whose dtype differs from their parameter's.
|
||||
|
||||
Assignment replaces the parameter rather than writing through it, so a
|
||||
mismatched dtype would silently change the module's. Converting makes a
|
||||
copy, which is the point: only the tensors that already match can stay on
|
||||
the mapping.
|
||||
"""
|
||||
for name, tensor in list(loaded.items()):
|
||||
param = target_state.get(name)
|
||||
if param is not None and param.dtype != tensor.dtype:
|
||||
loaded[name] = tensor.to(dtype=param.dtype)
|
||||
return loaded
|
||||
|
||||
|
||||
def _adopt_plain_weight_norm_state(
|
||||
module: nn.Module, loaded_names: Iterable[str]
|
||||
) -> int:
|
||||
"""Make deparameterized checkpoint weights native module state.
|
||||
|
||||
PyTorch's weight-norm load hook accepts legacy ``weight_g``/``weight_v``
|
||||
tensors, while inference exports commonly fold those tensors into one
|
||||
plain ``weight``. Removing only the matching parametrizations preserves
|
||||
that already-computed weight exactly and leaves every other parameterized
|
||||
module untouched.
|
||||
"""
|
||||
state_names = set(module.state_dict())
|
||||
module_by_name = dict(module.named_modules())
|
||||
owners: set[str] = set()
|
||||
for name in loaded_names:
|
||||
if name == "weight":
|
||||
owner_name = ""
|
||||
elif name.endswith(".weight"):
|
||||
owner_name = name.removesuffix(".weight")
|
||||
else:
|
||||
continue
|
||||
state_prefix = f"{owner_name}." if owner_name else ""
|
||||
if {
|
||||
f"{state_prefix}parametrizations.weight.original0",
|
||||
f"{state_prefix}parametrizations.weight.original1",
|
||||
}.issubset(state_names):
|
||||
owners.add(owner_name)
|
||||
|
||||
for owner_name in sorted(owners):
|
||||
parametrize.remove_parametrizations(
|
||||
module_by_name[owner_name], "weight", leave_parametrized=True
|
||||
)
|
||||
return len(owners)
|
||||
|
||||
|
||||
def _vae_checkpoint_arch_metadata_names(
|
||||
vae_config: VAEConfig,
|
||||
target_state: dict[str, torch.Tensor],
|
||||
@@ -444,7 +394,7 @@ def _assign_direct_gpu_vae_state(
|
||||
vae_config: VAEConfig,
|
||||
) -> tuple[int, tuple[str, ...]]:
|
||||
"""Stream a complete standard VAE state directly onto its target device."""
|
||||
num_deparameterized = _adopt_plain_weight_norm_state(
|
||||
num_deparameterized = adopt_plain_weight_norm_state(
|
||||
vae, _vae_checkpoint_tensor_names(weight_files)
|
||||
)
|
||||
target_state, slots = _direct_gpu_vae_state_slots(vae, component_name)
|
||||
@@ -664,21 +614,15 @@ class VAELoader(WeightOverrideComponentLoader):
|
||||
return vae
|
||||
|
||||
# Load from ModelRegistry (standard VAE classes)
|
||||
if direct_gpu_weight_loading:
|
||||
with (
|
||||
set_default_torch_dtype(vae_dtype),
|
||||
skip_init_modules(),
|
||||
torch.device("meta"),
|
||||
):
|
||||
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
vae = vae_cls(vae_config)
|
||||
else:
|
||||
with (
|
||||
set_default_torch_dtype(vae_dtype),
|
||||
skip_init_modules(),
|
||||
):
|
||||
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
vae = vae_cls(vae_config).to(target_device)
|
||||
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
vae = initialize_model(
|
||||
vae_cls,
|
||||
{"config": vae_config},
|
||||
vae_dtype,
|
||||
torch.device("meta") if direct_gpu_weight_loading else None,
|
||||
)
|
||||
if not direct_gpu_weight_loading:
|
||||
vae = vae.to(target_device)
|
||||
|
||||
if os.path.isfile(component_weights_path):
|
||||
if not component_weights_path.endswith(".safetensors"):
|
||||
@@ -729,7 +673,7 @@ class VAELoader(WeightOverrideComponentLoader):
|
||||
for sf_path in safetensors_list:
|
||||
loaded.update(safetensors_load_file(sf_path))
|
||||
_backfill_ltx2_audio_vae_latent_stats(loaded, component_type)
|
||||
num_deparameterized = _adopt_plain_weight_norm_state(vae, loaded)
|
||||
num_deparameterized = adopt_plain_weight_norm_state(vae, loaded)
|
||||
target_state = vae.state_dict()
|
||||
consumed_metadata = _consume_vae_checkpoint_arch_metadata(
|
||||
loaded, vae_config, target_state
|
||||
@@ -759,9 +703,8 @@ class VAELoader(WeightOverrideComponentLoader):
|
||||
component=f"{component_name or 'vae'} (VAE)",
|
||||
)
|
||||
)
|
||||
if keep_mapping:
|
||||
_match_checkpoint_dtypes(loaded, target_state)
|
||||
vae.load_state_dict(
|
||||
load_model_state_dict(
|
||||
vae,
|
||||
loaded,
|
||||
strict=strict_load,
|
||||
assign=keep_mapping,
|
||||
|
||||
@@ -1,80 +1,13 @@
|
||||
import re
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.configs.models.vocoder.ltx_vocoder import LTXVocoderConfig
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
||||
PlainStateDictComponentLoader,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
load_safetensors_state_dict,
|
||||
set_default_torch_dtype,
|
||||
skip_init_modules,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.registry import ModelRegistry
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.utils.precision import resolve_component_precision
|
||||
from sglang.multimodal_gen.utils import PRECISION_TO_TYPE
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class VocoderLoader(PlainStateDictComponentLoader):
|
||||
component_names = ["vocoder"]
|
||||
expected_library = "diffusers"
|
||||
|
||||
def load_customized(
|
||||
self, component_model_path: str, server_args: ServerArgs, component_name: str
|
||||
):
|
||||
config = self.load_component_config(component_model_path, component_name)
|
||||
component_weights_path = self.resolve_component_weights_path(
|
||||
component_model_path, server_args, component_name
|
||||
)
|
||||
class_name = config.pop("_class_name", None) or self.component_architecture
|
||||
assert class_name is not None, (
|
||||
"Vocoder class name must be available from component config or pipeline config."
|
||||
)
|
||||
|
||||
server_args.model_paths[component_name] = component_model_path
|
||||
|
||||
from sglang.multimodal_gen.configs.models.vocoder.ltx_vocoder import (
|
||||
LTXVocoderConfig,
|
||||
)
|
||||
|
||||
vocoder_config = LTXVocoderConfig()
|
||||
vocoder_config.update_model_arch(config)
|
||||
|
||||
resolved_vocoder_dtype = resolve_component_precision(server_args, "vocoder")
|
||||
vocoder_dtype = (
|
||||
resolved_vocoder_dtype
|
||||
if resolved_vocoder_dtype is not None
|
||||
else PRECISION_TO_TYPE["fp32"]
|
||||
)
|
||||
|
||||
component_starts_on_cpu = server_args.should_start_component_on_cpu(
|
||||
component_name
|
||||
)
|
||||
target_device = self.target_device(component_starts_on_cpu)
|
||||
|
||||
with set_default_torch_dtype(vocoder_dtype), skip_init_modules():
|
||||
vocoder_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
vocoder = vocoder_cls(vocoder_config).to(target_device)
|
||||
|
||||
loaded = load_safetensors_state_dict(component_weights_path)
|
||||
mapping = vocoder_config.arch_config.param_names_mapping
|
||||
loaded = {_remap_vocoder_key(k, mapping): v for k, v in loaded.items()}
|
||||
|
||||
missing_keys, unexpected_keys = vocoder.load_state_dict(loaded, strict=False)
|
||||
# A half-loaded vocoder produces plausible but wrong audio.
|
||||
if missing_keys or unexpected_keys:
|
||||
raise ValueError(
|
||||
f"Vocoder weights at '{component_weights_path}' do not match the "
|
||||
f"instantiated {class_name}. Missing: {sorted(missing_keys)}. "
|
||||
f"Unexpected: {sorted(unexpected_keys)}."
|
||||
)
|
||||
return vocoder
|
||||
|
||||
|
||||
def _remap_vocoder_key(key: str, param_names_mapping: dict[str, str]) -> str:
|
||||
# Applied in order, not first-match: one key can need several rules.
|
||||
for pattern, replacement in param_names_mapping.items():
|
||||
key = re.sub(pattern, replacement, key)
|
||||
return key
|
||||
config_classes = {"vocoder": LTXVocoderConfig}
|
||||
default_precision_attr = "audio_vae_precision"
|
||||
default_dtype = torch.float32
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Callable, Generator
|
||||
from itertools import chain
|
||||
from types import MethodType
|
||||
from typing import Any
|
||||
|
||||
@@ -40,9 +39,10 @@ from sglang.multimodal_gen.runtime.layers.quantization.bitsandbytes import (
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader import rank_local_checkpoint
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
finalize_loaded_model,
|
||||
get_param_names_mapping,
|
||||
hf_to_custom_state_dict,
|
||||
set_default_torch_dtype,
|
||||
initialize_model,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.weight_load_plan import WeightLoadPlan
|
||||
from sglang.multimodal_gen.runtime.loader.weight_utils import (
|
||||
@@ -50,10 +50,10 @@ from sglang.multimodal_gen.runtime.loader.weight_utils import (
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.utils.quantization_utils import (
|
||||
process_model_weights_after_loading,
|
||||
)
|
||||
from sglang.multimodal_gen.utils import set_mixed_precision_policy
|
||||
from sglang.srt.utils import is_npu
|
||||
|
||||
_is_npu = is_npu()
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -293,8 +293,9 @@ def maybe_load_fsdp_model(
|
||||
mp_policy=mp_policy,
|
||||
)
|
||||
|
||||
with set_default_torch_dtype(default_torch_dtype), torch.device("meta"):
|
||||
model = model_cls(**init_params)
|
||||
model = initialize_model(
|
||||
model_cls, init_params, default_torch_dtype, torch.device("meta")
|
||||
)
|
||||
|
||||
# Check if we should use FSDP
|
||||
use_fsdp = fsdp_inference
|
||||
@@ -447,26 +448,10 @@ def maybe_load_fsdp_model(
|
||||
# move to device to perform postprocessing
|
||||
_move_to_device_preserving_meta(model, weight_postprocess_device)
|
||||
|
||||
for _, module in model.named_modules():
|
||||
quant_method = getattr(module, "quant_method", None)
|
||||
if quant_method is not None and hasattr(
|
||||
quant_method, "process_weights_after_loading"
|
||||
):
|
||||
if _is_npu and not isinstance(quant_method, UnquantizedLinearMethod):
|
||||
# Activate the NZ format for storing weights,
|
||||
# which is a specific optimization for Ascend NPU
|
||||
torch.npu.config.allow_internal_format = True
|
||||
quant_method.process_weights_after_loading(module)
|
||||
if _is_npu:
|
||||
torch.npu.empty_cache()
|
||||
process_model_weights_after_loading(model)
|
||||
model.post_load_weights()
|
||||
|
||||
for n, p in chain(model.named_parameters(), model.named_buffers()):
|
||||
if p.is_meta:
|
||||
raise RuntimeError(f"Unexpected param or buffer {n} on meta device.")
|
||||
# Avoid unintended computation graph accumulation during inference
|
||||
if isinstance(p, torch.nn.Parameter):
|
||||
p.requires_grad = False
|
||||
finalize_loaded_model(model)
|
||||
|
||||
# 4. deferred cpu offload
|
||||
if defer_cpu_placement:
|
||||
|
||||
@@ -10,12 +10,14 @@ import json
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterator
|
||||
from collections.abc import Callable, Iterable, Iterator
|
||||
from itertools import chain
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
import torch
|
||||
from safetensors.torch import load_file as safetensors_load_file
|
||||
from torch import nn
|
||||
from torch.nn.utils import parametrize
|
||||
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.weights.source import (
|
||||
@@ -45,6 +47,78 @@ def set_default_torch_dtype(dtype: torch.dtype):
|
||||
torch.set_default_dtype(old_dtype)
|
||||
|
||||
|
||||
def initialize_model(
|
||||
model_cls: type[nn.Module],
|
||||
init_params: dict[str, Any],
|
||||
dtype: torch.dtype,
|
||||
device: torch.device | None = None,
|
||||
) -> nn.Module:
|
||||
"""Construct a checkpoint-backed module without initializing replaceable weights."""
|
||||
with (
|
||||
set_default_torch_dtype(dtype),
|
||||
skip_init_modules(),
|
||||
device if device is not None else contextlib.nullcontext(),
|
||||
):
|
||||
return model_cls(**init_params)
|
||||
|
||||
|
||||
def finalize_loaded_model(model: nn.Module) -> nn.Module:
|
||||
"""Reject unmaterialized state and freeze parameters before inference."""
|
||||
for name, tensor in chain(model.named_parameters(), model.named_buffers()):
|
||||
if tensor.is_meta:
|
||||
raise RuntimeError(f"Unexpected param or buffer {name} on meta device.")
|
||||
if isinstance(tensor, nn.Parameter):
|
||||
tensor.requires_grad = False
|
||||
return model.eval()
|
||||
|
||||
|
||||
def adopt_plain_weight_norm_state(
|
||||
module: nn.Module, loaded_names: Iterable[str]
|
||||
) -> int:
|
||||
"""Restore folded weights without recomputing their checkpoint values."""
|
||||
state_names = set(module.state_dict())
|
||||
module_by_name = dict(module.named_modules())
|
||||
owners: set[str] = set()
|
||||
for name in loaded_names:
|
||||
if name == "weight":
|
||||
owner_name = ""
|
||||
elif name.endswith(".weight"):
|
||||
owner_name = name.removesuffix(".weight")
|
||||
else:
|
||||
continue
|
||||
state_prefix = f"{owner_name}." if owner_name else ""
|
||||
if {
|
||||
f"{state_prefix}parametrizations.weight.original0",
|
||||
f"{state_prefix}parametrizations.weight.original1",
|
||||
}.issubset(state_names):
|
||||
owners.add(owner_name)
|
||||
|
||||
for owner_name in sorted(owners):
|
||||
parametrize.remove_parametrizations(
|
||||
module_by_name[owner_name], "weight", leave_parametrized=True
|
||||
)
|
||||
return len(owners)
|
||||
|
||||
|
||||
def load_model_state_dict(
|
||||
model: nn.Module,
|
||||
state_dict: dict[str, torch.Tensor],
|
||||
*,
|
||||
strict: bool = True,
|
||||
assign: bool = False,
|
||||
):
|
||||
"""Restore plain module state, preserving constructor-declared mixed dtypes."""
|
||||
adopt_plain_weight_norm_state(model, state_dict)
|
||||
if assign:
|
||||
target_state = model.state_dict()
|
||||
# assignment replaces storage; unlike copy loading it does not cast
|
||||
for name, tensor in state_dict.items():
|
||||
target = target_state.get(name)
|
||||
if target is not None and tensor.dtype != target.dtype:
|
||||
state_dict[name] = tensor.to(dtype=target.dtype)
|
||||
return model.load_state_dict(state_dict, strict=strict, assign=assign)
|
||||
|
||||
|
||||
def get_param_names_mapping(
|
||||
mapping_dict: dict[str, str | tuple[str, int, int]],
|
||||
) -> Callable[[str], tuple[str, Any, Any]]:
|
||||
@@ -111,6 +185,8 @@ def hf_to_custom_state_dict(
|
||||
hf_param_sd: dict[str, torch.Tensor] | Iterator[tuple[str, torch.Tensor]],
|
||||
param_names_mapping: Callable[[str], tuple[str, Any, Any]],
|
||||
valid_target_names: set[str] | None = None,
|
||||
*,
|
||||
strict: bool = False,
|
||||
) -> tuple[dict[str, torch.Tensor], dict[str, tuple[str, Any, Any]]]:
|
||||
"""
|
||||
Converts a Hugging Face parameter state dictionary to a custom parameter state dictionary.
|
||||
@@ -149,6 +225,10 @@ def hf_to_custom_state_dict(
|
||||
num_params_to_merge,
|
||||
)
|
||||
if merge_index is not None:
|
||||
if strict and merge_index in to_merge_params[target_param_name]:
|
||||
raise ValueError(
|
||||
f"Duplicate checkpoint slice for {target_param_name!r}"
|
||||
)
|
||||
to_merge_params[target_param_name][merge_index] = full_tensor
|
||||
if len(to_merge_params[target_param_name]) == num_params_to_merge:
|
||||
# cat at output dim according to the merge_index order
|
||||
@@ -161,6 +241,8 @@ def hf_to_custom_state_dict(
|
||||
else:
|
||||
continue
|
||||
existing_tensor = custom_param_sd.get(target_param_name)
|
||||
if strict and existing_tensor is not None:
|
||||
raise ValueError(f"Duplicate checkpoint mapping for {target_param_name!r}")
|
||||
if existing_tensor is not None and existing_tensor.dtype != full_tensor.dtype:
|
||||
existing_is_quantized = existing_tensor.dtype in _QUANTIZED_DTYPES
|
||||
current_is_quantized = full_tensor.dtype in _QUANTIZED_DTYPES
|
||||
@@ -180,6 +262,8 @@ def hf_to_custom_state_dict(
|
||||
full_tensor.dtype,
|
||||
)
|
||||
custom_param_sd[target_param_name] = full_tensor
|
||||
if strict and to_merge_params:
|
||||
raise ValueError(f"Incomplete checkpoint slices for {sorted(to_merge_params)}")
|
||||
return custom_param_sd, reverse_param_names_mapping
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ from torch.distributed.tensor import DTensor
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
_DEFAULT_SAFETENSORS_INDEX,
|
||||
_list_safetensors_files,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.weight_load_plan import WeightLoadPlan
|
||||
from sglang.multimodal_gen.runtime.loader.weight_readers import (
|
||||
FALLBACK_READER,
|
||||
@@ -33,6 +37,40 @@ from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def checkpoint_weights_iterator(
|
||||
model_path: str,
|
||||
*,
|
||||
to_cpu: bool = True,
|
||||
key_filter: Callable[[str], bool] | None = None,
|
||||
index_file: str = _DEFAULT_SAFETENSORS_INDEX,
|
||||
) -> Generator[tuple[str, torch.Tensor], None, None]:
|
||||
"""Read a materialized component checkpoint, preferring indexed safetensors."""
|
||||
files = _list_safetensors_files(
|
||||
model_path, index_file=index_file, key_filter=key_filter
|
||||
)
|
||||
if files:
|
||||
yield from safetensors_weights_iterator(
|
||||
files, to_cpu=to_cpu, key_filter=key_filter
|
||||
)
|
||||
return
|
||||
if os.path.isfile(model_path):
|
||||
files = [model_path] if model_path.endswith((".bin", ".pt")) else []
|
||||
else:
|
||||
for suffix in ("*.bin", "*.pt"):
|
||||
files = filter_files_not_needed_for_inference(
|
||||
sorted(str(path) for path in Path(model_path).glob(suffix))
|
||||
)
|
||||
if files:
|
||||
break
|
||||
if not files:
|
||||
raise ValueError(
|
||||
f"No safetensors, bin, or pt checkpoint found at {model_path!r}"
|
||||
)
|
||||
for name, tensor in pt_weights_iterator(files, to_cpu=to_cpu):
|
||||
if key_filter is None or key_filter(name):
|
||||
yield name, tensor
|
||||
|
||||
|
||||
def _disable_runai_streamer_rank_discovery_collective() -> None:
|
||||
"""RunAI Model Streamer's ``find_local_ranks()`` fires a full-world
|
||||
collective on the first ``stream_files()`` of every streamer instance even
|
||||
|
||||
@@ -35,9 +35,11 @@ from sglang.multimodal_gen.runtime.loader.component_loaders.text_encoder_loader
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.fsdp_load import (
|
||||
load_model_from_full_model_state_dict,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
get_param_names_mapping,
|
||||
set_default_torch_dtype,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import get_param_names_mapping
|
||||
from sglang.multimodal_gen.runtime.models.dits.hunyuan3d_paint import (
|
||||
Hunyuan3DPaintUNet,
|
||||
)
|
||||
|
||||
@@ -6,8 +6,14 @@ import struct
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
from torch import nn
|
||||
|
||||
from sglang.multimodal_gen.runtime.layers.linear import (
|
||||
LinearBase,
|
||||
UnquantizedLinearMethod,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.quantization import (
|
||||
QuantizationConfig,
|
||||
get_quantization_config,
|
||||
@@ -26,15 +32,77 @@ from sglang.multimodal_gen.runtime.layers.quantization.configs.kitchen_w4a8_conf
|
||||
KitchenW4A8Config,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.mxfp8 import MXFP8Config
|
||||
from sglang.multimodal_gen.runtime.layers.vocab_parallel_embedding import (
|
||||
UnquantizedEmbeddingMethod,
|
||||
VocabParallelEmbedding,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.srt.layers.linear import LinearBase as SrtLinearBase
|
||||
from sglang.srt.layers.modelopt_utils import canonicalize_modelopt_quant_algo
|
||||
from sglang.srt.layers.quantization.unquant import (
|
||||
UnquantizedEmbeddingMethod as SrtUnquantizedEmbeddingMethod,
|
||||
)
|
||||
from sglang.srt.layers.quantization.unquant import (
|
||||
UnquantizedLinearMethod as SrtUnquantizedLinearMethod,
|
||||
)
|
||||
from sglang.srt.layers.vocab_parallel_embedding import (
|
||||
VocabParallelEmbedding as SrtVocabParallelEmbedding,
|
||||
)
|
||||
from sglang.srt.model_loader.checkpoint_quantization import (
|
||||
resolve_checkpoint_quant_spec,
|
||||
)
|
||||
from sglang.srt.model_loader.post_load import stage_module_for_post_load
|
||||
from sglang.srt.utils import is_npu
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def process_model_weights_after_loading(
|
||||
model: nn.Module,
|
||||
process_device: torch.device | None = None,
|
||||
*,
|
||||
quantized_only: bool = False,
|
||||
) -> int:
|
||||
"""Process native and SRT layers once, optionally staging one layer at a time."""
|
||||
processed_layers = 0
|
||||
for module in model.modules():
|
||||
if not isinstance(
|
||||
module,
|
||||
(
|
||||
LinearBase,
|
||||
SrtLinearBase,
|
||||
VocabParallelEmbedding,
|
||||
SrtVocabParallelEmbedding,
|
||||
),
|
||||
):
|
||||
continue
|
||||
method = module.quant_method
|
||||
if method is None:
|
||||
continue
|
||||
unquantized = isinstance(
|
||||
method,
|
||||
(
|
||||
UnquantizedLinearMethod,
|
||||
SrtUnquantizedLinearMethod,
|
||||
UnquantizedEmbeddingMethod,
|
||||
SrtUnquantizedEmbeddingMethod,
|
||||
),
|
||||
)
|
||||
if quantized_only and unquantized:
|
||||
continue
|
||||
if is_npu() and not unquantized:
|
||||
torch.npu.config.allow_internal_format = True
|
||||
if process_device is None:
|
||||
method.process_weights_after_loading(module)
|
||||
else:
|
||||
with stage_module_for_post_load(module, process_device):
|
||||
method.process_weights_after_loading(module)
|
||||
if is_npu():
|
||||
torch.npu.empty_cache()
|
||||
processed_layers += 1
|
||||
return processed_layers
|
||||
|
||||
|
||||
def inspect_comfy_quant_markers(
|
||||
safetensors_list: list[str],
|
||||
param_name_mapper: Callable[[str], str] | None = None,
|
||||
|
||||
@@ -5,7 +5,6 @@ import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.adapter_loader import (
|
||||
@@ -229,76 +228,6 @@ class TestComponentQuantizationAdmission(unittest.TestCase):
|
||||
|
||||
self.assertIs(loaded, config)
|
||||
|
||||
def test_bridge_consumes_exact_component_weight_override(self):
|
||||
loader = BridgeLoader()
|
||||
bridge_config = SimpleNamespace(update_model_arch=lambda _config: None)
|
||||
server_args = SimpleNamespace(
|
||||
component_weights_paths={
|
||||
"dual_tower_bridge": "owner/repo/bridge.safetensors"
|
||||
},
|
||||
model_paths={},
|
||||
pipeline_config=SimpleNamespace(bridge_config=bridge_config),
|
||||
should_use_fsdp_for_component=lambda _name: False,
|
||||
should_start_component_on_cpu=lambda _name: False,
|
||||
hsdp_replicate_dim=1,
|
||||
hsdp_shard_dim=1,
|
||||
pin_cpu_memory=False,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
loader,
|
||||
"load_component_config",
|
||||
return_value={"_class_name": "MOVADualTowerModel"},
|
||||
),
|
||||
patch.object(
|
||||
loader,
|
||||
"resolve_component_weights_path",
|
||||
return_value="/cache/bridge.safetensors",
|
||||
) as resolve_weights,
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.loader.component_loaders."
|
||||
"bridge_loader._list_safetensors_files",
|
||||
return_value=["/cache/bridge.safetensors"],
|
||||
) as list_weights,
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.loader.component_loaders."
|
||||
"bridge_loader.ModelRegistry.resolve_model_cls",
|
||||
return_value=(nn.Linear, None),
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.loader.component_loaders."
|
||||
"bridge_loader.resolve_precision",
|
||||
return_value=torch.bfloat16,
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.loader.component_loaders."
|
||||
"bridge_loader.get_local_torch_device",
|
||||
return_value=torch.device("cpu"),
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.loader.component_loaders."
|
||||
"bridge_loader.maybe_load_fsdp_model",
|
||||
return_value=nn.Linear(1, 1),
|
||||
) as load_weights,
|
||||
):
|
||||
loaded = loader.load_customized(
|
||||
"/base/dual_tower_bridge",
|
||||
server_args,
|
||||
"dual_tower_bridge",
|
||||
)
|
||||
|
||||
self.assertIsInstance(loaded, nn.Linear)
|
||||
resolve_weights.assert_called_once_with(
|
||||
"/base/dual_tower_bridge", server_args, "dual_tower_bridge"
|
||||
)
|
||||
list_weights.assert_called_once_with("/cache/bridge.safetensors")
|
||||
self.assertEqual(
|
||||
load_weights.call_args.kwargs["weight_dir_list"],
|
||||
["/cache/bridge.safetensors"],
|
||||
)
|
||||
self.assertFalse(load_weights.call_args.kwargs["fsdp_inference"])
|
||||
|
||||
def test_all_quantization_metadata_layouts_fail_closed(self):
|
||||
configs = {
|
||||
"quantization_config": {
|
||||
@@ -358,7 +287,7 @@ class TestComponentQuantizationAdmission(unittest.TestCase):
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.loader.component_loaders."
|
||||
"adapter_loader.ModelRegistry.resolve_model_cls"
|
||||
"component_loader.ModelRegistry.resolve_model_cls"
|
||||
) as resolve_model,
|
||||
self.assertRaises(ComponentCheckpointUnsupportedError),
|
||||
):
|
||||
|
||||
@@ -41,6 +41,9 @@ class TestImageEncoderQuantizationAdmission(unittest.TestCase):
|
||||
),
|
||||
component_weights_paths={},
|
||||
component_quantizations={},
|
||||
component_quantization_ignored_layers={},
|
||||
component_paths={},
|
||||
batching_max_size=1,
|
||||
component_precisions={},
|
||||
encoder_parallel="replicate",
|
||||
resolve_component_attention_backend=lambda _name: (None, None),
|
||||
@@ -61,7 +64,7 @@ class TestImageEncoderQuantizationAdmission(unittest.TestCase):
|
||||
def _config_patch(self, config):
|
||||
return mock.patch(
|
||||
"sglang.multimodal_gen.runtime.loader.component_loaders."
|
||||
"image_encoder_loader.get_diffusers_component_config",
|
||||
"text_encoder_loader.get_diffusers_component_config",
|
||||
return_value=config,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from safetensors.torch import save_file
|
||||
from torch import nn
|
||||
|
||||
from sglang.multimodal_gen.configs.models.adapter.ltx_2_connector import (
|
||||
LTX2ConnectorConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.models.adapter.ltx_2_duration_head import (
|
||||
LTX2DurationHeadConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.models.base import ArchConfig, ModelConfig
|
||||
from sglang.multimodal_gen.configs.models.bridges.mova_dual_tower import (
|
||||
MOVADualTowerConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.models.decoders.ltx_2_5_diffusion_decoder import (
|
||||
LTX25DiffusionDecoderConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.models.vocoder.ltx_vocoder import LTXVocoderConfig
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
|
||||
ComponentCheckpointUnsupportedError,
|
||||
PipelineComponentLoader,
|
||||
PlainStateDictComponentLoader,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import set_default_torch_dtype
|
||||
from sglang.multimodal_gen.runtime.models.registry import ModelRegistry
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
|
||||
# real architectures with reduced widths; checkpoint names use the external layout
|
||||
CASES = [
|
||||
(
|
||||
"dual_tower_bridge",
|
||||
"DualTowerConditionalBridge",
|
||||
MOVADualTowerConfig,
|
||||
{
|
||||
"visual_layers": 1,
|
||||
"audio_layers": 1,
|
||||
"visual_hidden_dim": 16,
|
||||
"audio_hidden_dim": 16,
|
||||
"head_dim": 8,
|
||||
},
|
||||
),
|
||||
(
|
||||
"duration_head",
|
||||
"LTX2DurationHead",
|
||||
LTX2DurationHeadConfig,
|
||||
{
|
||||
"video_cross_attention_dim": 8,
|
||||
"audio_cross_attention_dim": 8,
|
||||
"pooler_hidden_dim": 8,
|
||||
"num_pooler_heads": 2,
|
||||
"mlp_hidden_dim": 8,
|
||||
},
|
||||
),
|
||||
(
|
||||
"connectors",
|
||||
"LTX2TextConnectors",
|
||||
LTX2ConnectorConfig,
|
||||
{
|
||||
"caption_channels": 8,
|
||||
"text_proj_in_factor": 2,
|
||||
"per_modality_projections": True,
|
||||
"video_hidden_dim": 8,
|
||||
"audio_hidden_dim": 8,
|
||||
"video_connector_num_attention_heads": 2,
|
||||
"video_connector_attention_head_dim": 4,
|
||||
"video_connector_num_layers": 1,
|
||||
"video_connector_num_learnable_registers": 2,
|
||||
"audio_connector_num_attention_heads": 2,
|
||||
"audio_connector_attention_head_dim": 4,
|
||||
"audio_connector_num_layers": 1,
|
||||
"audio_connector_num_learnable_registers": 2,
|
||||
},
|
||||
),
|
||||
(
|
||||
"diffusion_decoder",
|
||||
"LTX2VideoDiffusionDecoderModel",
|
||||
LTX25DiffusionDecoderConfig,
|
||||
{
|
||||
"latent_channels": 4,
|
||||
"decoder_head_dim": 8,
|
||||
"decoder_t_emb_dim": 8,
|
||||
"decoder_stage_channels": [8, 8, 8, 8, 8],
|
||||
"decoder_stage_depths": [1, 1, 1, 1, 1],
|
||||
"decoder_upsample_channel_reductions": [1, 1, 1, 1],
|
||||
},
|
||||
),
|
||||
(
|
||||
"vocoder",
|
||||
"LTX2VocoderWithBWE",
|
||||
LTXVocoderConfig,
|
||||
{
|
||||
"hidden_channels": 32,
|
||||
"upsample_factors": [2],
|
||||
"upsample_kernel_sizes": [4],
|
||||
"resnet_kernel_sizes": [3],
|
||||
"resnet_dilations": [[1, 3, 5]],
|
||||
"bwe_hidden_channels": 32,
|
||||
"bwe_upsample_factors": [2],
|
||||
"bwe_upsample_kernel_sizes": [4],
|
||||
"bwe_resnet_kernel_sizes": [3],
|
||||
"bwe_resnet_dilations": [[1, 3, 5]],
|
||||
"input_sampling_rate": 16000,
|
||||
"output_sampling_rate": 32000,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _write_checkpoint(path, config, weights):
|
||||
path.mkdir(exist_ok=True)
|
||||
(path / "config.json").write_text(json.dumps(config))
|
||||
save_file(
|
||||
{name: tensor.contiguous() for name, tensor in weights.items()},
|
||||
path / "model.safetensors",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role,class_name,config_cls,raw_config", CASES)
|
||||
@pytest.mark.parametrize("residency", ["component-offload", "resident"])
|
||||
@pytest.mark.parametrize("precision", ["fp32", "bf16"])
|
||||
def test_real_components_restore_weights_and_exact_policy(
|
||||
tmp_path, role, class_name, config_cls, raw_config, residency, precision
|
||||
):
|
||||
if residency == "resident" and not torch.cuda.is_available():
|
||||
pytest.skip("CUDA is required")
|
||||
config = config_cls()
|
||||
config.update_model_arch(raw_config)
|
||||
model_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||
dtype = torch.float32 if precision == "fp32" else torch.bfloat16
|
||||
with set_default_torch_dtype(dtype):
|
||||
reference = model_cls(config).eval()
|
||||
if role != "vocoder":
|
||||
reference = reference.to(dtype=dtype)
|
||||
# custom linear constructors allocate empty storage, not checkpoint values
|
||||
generator = torch.Generator().manual_seed(0)
|
||||
with torch.no_grad():
|
||||
for parameter in reference.parameters():
|
||||
parameter.uniform_(-0.1, 0.1, generator=generator)
|
||||
weights = {}
|
||||
for name, tensor in reference.state_dict().items():
|
||||
assert torch.isfinite(tensor).all(), name
|
||||
name = name.replace("video_aggregate_embed.", "video_text_proj_in.").replace(
|
||||
"audio_aggregate_embed.", "audio_text_proj_in."
|
||||
)
|
||||
if role == "vocoder":
|
||||
name = name.replace(".conv_pre.", ".conv_in.").replace(
|
||||
".conv_post.", ".conv_out."
|
||||
)
|
||||
name = name.replace(".act_post.", ".act_out.").replace(
|
||||
".ups.", ".upsamplers."
|
||||
)
|
||||
name = name.replace(".resblocks.", ".resnets.").replace(
|
||||
".downsample.lowpass.filter", ".downsample.filter"
|
||||
)
|
||||
weights[name] = tensor
|
||||
component = tmp_path / role
|
||||
_write_checkpoint(component, {"_class_name": class_name, **raw_config}, weights)
|
||||
# the exact key differs from the structural role, including for vocoders
|
||||
name = role + "_2"
|
||||
args = ServerArgs(
|
||||
model_path="x",
|
||||
component_precisions={name: precision},
|
||||
component_residency={name: residency},
|
||||
component_weights_paths={name: str(component / "model.safetensors")},
|
||||
)
|
||||
model, _ = PipelineComponentLoader.load_component(
|
||||
name, str(component), "diffusers", args, component_type=role
|
||||
)
|
||||
device = "cuda" if residency == "resident" else "cpu"
|
||||
assert not model.training
|
||||
assert args.model_paths[name] == str(component)
|
||||
for key, expected in reference.state_dict().items():
|
||||
actual = model.state_dict()[key]
|
||||
assert actual.device.type == device
|
||||
assert actual.dtype == expected.dtype
|
||||
torch.testing.assert_close(actual.cpu(), expected, rtol=0, atol=0)
|
||||
# constructor-owned nonpersistent filters must survive common placement too
|
||||
for key, expected in reference.named_buffers():
|
||||
torch.testing.assert_close(
|
||||
model.get_buffer(key).cpu(), expected, rtol=0, atol=0
|
||||
)
|
||||
if role == "duration_head":
|
||||
reference = reference.to(device)
|
||||
inputs = torch.ones(1, 2, 8, device=device, dtype=dtype)
|
||||
with torch.inference_mode():
|
||||
torch.testing.assert_close(model(inputs), reference(inputs), rtol=0, atol=0)
|
||||
|
||||
|
||||
class _DecoderOnly(nn.Module):
|
||||
def __init__(self, config):
|
||||
super().__init__()
|
||||
self.decoder = nn.Linear(config["width"], 2)
|
||||
|
||||
|
||||
class _WeightNormModule(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.proj = torch.nn.utils.parametrizations.weight_norm(
|
||||
nn.Linear(2, 4, bias=False)
|
||||
)
|
||||
|
||||
|
||||
def test_plain_component_restores_folded_weight_norm(tmp_path):
|
||||
expected = torch.arange(8, dtype=torch.float32).reshape(4, 2)
|
||||
_write_checkpoint(
|
||||
tmp_path, {"_class_name": "TestWeightNormModule"}, {"proj.weight": expected}
|
||||
)
|
||||
args = ServerArgs(
|
||||
model_path="x",
|
||||
component_precisions={"auxiliary": "fp32"},
|
||||
component_residency={"auxiliary": "component-offload"},
|
||||
)
|
||||
with patch.dict(ModelRegistry.registered_models):
|
||||
ModelRegistry.register_model("TestWeightNormModule", _WeightNormModule)
|
||||
model, _ = PlainStateDictComponentLoader().load(
|
||||
str(tmp_path), args, "auxiliary", "diffusers"
|
||||
)
|
||||
torch.testing.assert_close(model.proj.weight, expected, rtol=0, atol=0)
|
||||
assert set(model.state_dict()) == {"proj.weight"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class _MappedArch(ArchConfig):
|
||||
param_names_mapping: dict = field(
|
||||
default_factory=lambda: {
|
||||
r"^q.weight$": ("proj.weight", 0, 2),
|
||||
r"^k.weight$": ("proj.weight", 1, 2),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _MappedConfig(ModelConfig):
|
||||
arch_config: ArchConfig = field(default_factory=_MappedArch)
|
||||
|
||||
|
||||
class _MappedModule(nn.Module):
|
||||
def __init__(self, config):
|
||||
super().__init__()
|
||||
self.proj = nn.Linear(2, 4, bias=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", [None, "collision", "incomplete"])
|
||||
def test_plain_components_use_shared_fused_weight_mapping(tmp_path, invalid):
|
||||
weights = {"q.weight": torch.ones(2, 2), "k.weight": torch.zeros(2, 2)}
|
||||
if invalid == "collision":
|
||||
weights["proj.weight"] = torch.ones(4, 2)
|
||||
elif invalid == "incomplete":
|
||||
weights.pop("k.weight")
|
||||
_write_checkpoint(tmp_path, {"_class_name": "TestMappedModule"}, weights)
|
||||
loader = PlainStateDictComponentLoader()
|
||||
loader.config_classes = {"auxiliary": _MappedConfig}
|
||||
args = ServerArgs(
|
||||
model_path="x", component_residency={"auxiliary": "component-offload"}
|
||||
)
|
||||
with patch.dict(ModelRegistry.registered_models):
|
||||
ModelRegistry.register_model("TestMappedModule", _MappedModule)
|
||||
if invalid:
|
||||
with pytest.raises(ComponentCheckpointUnsupportedError):
|
||||
loader.load(str(tmp_path), args, "auxiliary", "diffusers")
|
||||
else:
|
||||
model, _ = loader.load(str(tmp_path), args, "auxiliary", "diffusers")
|
||||
expected = torch.cat([weights["q.weight"], weights["k.weight"]]).bfloat16()
|
||||
torch.testing.assert_close(model.proj.weight, expected, rtol=0, atol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", [None, "missing", "unexpected", "shape"])
|
||||
def test_sound_tokenizer_only_ignores_encoder_weights(tmp_path, invalid):
|
||||
reference = _DecoderOnly({"width": 4})
|
||||
weights = dict(reference.state_dict(), **{"encoder.weight": torch.ones(2, 4)})
|
||||
if invalid == "missing":
|
||||
weights.pop("decoder.bias")
|
||||
elif invalid == "unexpected":
|
||||
weights["other.weight"] = torch.ones(2, 4)
|
||||
elif invalid == "shape":
|
||||
weights["decoder.weight"] = torch.ones(3, 4)
|
||||
_write_checkpoint(tmp_path, {"_class_name": "TestDecoderOnly", "width": 4}, weights)
|
||||
args = ServerArgs(
|
||||
model_path="x", component_residency={"sound_tokenizer": "component-offload"}
|
||||
)
|
||||
with patch.dict(ModelRegistry.registered_models):
|
||||
ModelRegistry.register_model("TestDecoderOnly", _DecoderOnly)
|
||||
if invalid:
|
||||
with pytest.raises(ComponentCheckpointUnsupportedError):
|
||||
PipelineComponentLoader.load_component(
|
||||
"sound_tokenizer", str(tmp_path), "diffusers", args
|
||||
)
|
||||
else:
|
||||
model, _ = PipelineComponentLoader.load_component(
|
||||
"sound_tokenizer", str(tmp_path), "diffusers", args
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
model.decoder.weight,
|
||||
reference.decoder.weight,
|
||||
rtol=0,
|
||||
atol=0,
|
||||
)
|
||||
@@ -5,11 +5,13 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import transformers
|
||||
from safetensors.torch import save_file
|
||||
from torch import nn
|
||||
|
||||
from sglang.multimodal_gen.configs.models.encoders.t5 import T5Config
|
||||
from sglang.multimodal_gen.runtime.layers.linear import LinearBase
|
||||
from sglang.multimodal_gen.runtime.layers.quantization.comfy_nvfp4 import (
|
||||
ComfyFullPrecisionNvfp4LinearMethod,
|
||||
@@ -35,11 +37,13 @@ from sglang.multimodal_gen.runtime.loader.component_loaders.text_encoder_loader
|
||||
TextEncoderLoader,
|
||||
_configure_encoder_quantization,
|
||||
_get_encoder_quant_config,
|
||||
_process_quantized_encoder_weights,
|
||||
_require_quantized_encoder_layers,
|
||||
_resolve_and_configure_encoder_quantization,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.gguf_weights import GGUFTensorMeta
|
||||
from sglang.multimodal_gen.runtime.loader.weight_utils import (
|
||||
checkpoint_weights_iterator,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.encoders.base import (
|
||||
EncoderTensorParallelMixin,
|
||||
TextEncoder,
|
||||
@@ -49,9 +53,67 @@ from sglang.multimodal_gen.runtime.models.encoders.minimax_h3_qwen3vl import (
|
||||
MiniMaxH3Qwen3VLEncoder,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl import Qwen3VLTextModel
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.quantization_utils import (
|
||||
process_model_weights_after_loading,
|
||||
)
|
||||
from sglang.srt.layers.linear import LinearBase as SrtLinearBase
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing", [False, True])
|
||||
@pytest.mark.parametrize("competing_index", [False, True])
|
||||
def test_native_encoder_restoration_checks_checkpoint_before_fallback(
|
||||
tmp_path, missing, competing_index
|
||||
):
|
||||
config = transformers.T5Config(
|
||||
vocab_size=32,
|
||||
d_model=8,
|
||||
d_kv=4,
|
||||
d_ff=16,
|
||||
num_layers=1,
|
||||
num_heads=2,
|
||||
architectures=["T5EncoderModel"],
|
||||
)
|
||||
reference = transformers.T5EncoderModel(config)
|
||||
config.save_pretrained(tmp_path)
|
||||
weights = {name: tensor.clone() for name, tensor in reference.state_dict().items()}
|
||||
required = "encoder.final_layer_norm.weight"
|
||||
if missing:
|
||||
weights.pop(required)
|
||||
save_file(weights, tmp_path / "model.safetensors")
|
||||
if competing_index:
|
||||
# LTX-2 ships both indexes in text_encoder; only the HF one owns this model
|
||||
(tmp_path / "model.safetensors.index.json").write_text(
|
||||
json.dumps({"weight_map": {name: "model.safetensors" for name in weights}})
|
||||
)
|
||||
alternate = "diffusion_pytorch_model.safetensors"
|
||||
save_file({"diffusion.weight": torch.ones(2, 2)}, tmp_path / alternate)
|
||||
(tmp_path / "diffusion_pytorch_model.safetensors.index.json").write_text(
|
||||
json.dumps({"weight_map": {"diffusion.weight": alternate}})
|
||||
)
|
||||
alternate_weights = dict(checkpoint_weights_iterator(str(tmp_path)))
|
||||
assert set(alternate_weights) == {"diffusion.weight"}
|
||||
args = ServerArgs(
|
||||
model_path="x",
|
||||
component_precisions={"text_encoder": "fp32"},
|
||||
component_residency={"text_encoder": "component-offload"},
|
||||
)
|
||||
args.pipeline_config.text_encoder_configs = (T5Config(),)
|
||||
loader = TextEncoderLoader()
|
||||
with mock.patch.object(
|
||||
loader, "load_native", side_effect=AssertionError("fallback")
|
||||
):
|
||||
if missing:
|
||||
with pytest.raises(ComponentCheckpointUnsupportedError, match=required):
|
||||
loader.load(str(tmp_path), args, "text_encoder", "transformers")
|
||||
else:
|
||||
model, _ = loader.load(str(tmp_path), args, "text_encoder", "transformers")
|
||||
torch.testing.assert_close(
|
||||
model.state_dict()[required], weights[required], rtol=0, atol=0
|
||||
)
|
||||
assert all(not parameter.requires_grad for parameter in model.parameters())
|
||||
|
||||
|
||||
class TestTextEncoderWeightDiscovery(unittest.TestCase):
|
||||
def test_prepare_weights_prefers_canonical_over_fp16_variant(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
@@ -59,22 +121,13 @@ class TestTextEncoderWeightDiscovery(unittest.TestCase):
|
||||
canonical = model_dir / "model.safetensors"
|
||||
variant = model_dir / "model.fp16.safetensors"
|
||||
|
||||
canonical.touch()
|
||||
variant.touch()
|
||||
weight = torch.ones(2, 2)
|
||||
save_file({"weight": weight}, canonical)
|
||||
save_file({"weight": weight.half()}, variant)
|
||||
|
||||
(
|
||||
hf_folder,
|
||||
weight_files,
|
||||
use_safetensors,
|
||||
) = TextEncoderLoader()._prepare_weights(
|
||||
str(model_dir),
|
||||
fall_back_to_pt=True,
|
||||
allow_patterns_overrides=None,
|
||||
)
|
||||
|
||||
self.assertEqual(hf_folder, str(model_dir))
|
||||
self.assertTrue(use_safetensors)
|
||||
self.assertEqual(weight_files, [str(canonical)])
|
||||
state = dict(checkpoint_weights_iterator(str(model_dir)))
|
||||
self.assertEqual(state["weight"].dtype, torch.float32)
|
||||
torch.testing.assert_close(state["weight"], weight)
|
||||
|
||||
|
||||
class TestTextEncoderClassResolution(unittest.TestCase):
|
||||
@@ -757,7 +810,10 @@ class TestTextEncoderQuantization(unittest.TestCase):
|
||||
),
|
||||
):
|
||||
_resolve_and_configure_encoder_quantization(
|
||||
SimpleNamespace(architectures=[architecture], quant_config=None),
|
||||
SimpleNamespace(
|
||||
arch_config=SimpleNamespace(architectures=[architecture]),
|
||||
quant_config=None,
|
||||
),
|
||||
component_config,
|
||||
"/model/text_encoder",
|
||||
"/model/text_encoder",
|
||||
@@ -791,7 +847,10 @@ class TestTextEncoderQuantization(unittest.TestCase):
|
||||
):
|
||||
_resolve_and_configure_encoder_quantization(
|
||||
SimpleNamespace(
|
||||
architectures=["ThirdPartyTextEncoder"], quant_config=None
|
||||
arch_config=SimpleNamespace(
|
||||
architectures=["ThirdPartyTextEncoder"]
|
||||
),
|
||||
quant_config=None,
|
||||
),
|
||||
{
|
||||
"quantization_config": {
|
||||
@@ -812,7 +871,10 @@ class TestTextEncoderQuantization(unittest.TestCase):
|
||||
):
|
||||
_resolve_and_configure_encoder_quantization(
|
||||
SimpleNamespace(
|
||||
architectures=["ThirdPartyTextEncoder"], quant_config=None
|
||||
arch_config=SimpleNamespace(
|
||||
architectures=["ThirdPartyTextEncoder"]
|
||||
),
|
||||
quant_config=None,
|
||||
),
|
||||
{
|
||||
"quantization_config": {
|
||||
@@ -887,10 +949,10 @@ class TestQuantizedTextEncoderPostprocess(unittest.TestCase):
|
||||
quant_method = _RecordingQuantMethod()
|
||||
model = _SRTQuantizedLinear(quant_method)
|
||||
|
||||
processed = _process_quantized_encoder_weights(
|
||||
processed = process_model_weights_after_loading(
|
||||
model,
|
||||
torch.device("cpu"),
|
||||
"image_encoder",
|
||||
quantized_only=True,
|
||||
)
|
||||
|
||||
self.assertEqual(processed, 1)
|
||||
@@ -926,10 +988,10 @@ class TestQuantizedTextEncoderPostprocess(unittest.TestCase):
|
||||
quant_method = _RecordingQuantMethod()
|
||||
model = _QuantizedEncoder(quant_method)
|
||||
|
||||
processed = _process_quantized_encoder_weights(
|
||||
processed = process_model_weights_after_loading(
|
||||
model,
|
||||
torch.device("cpu"),
|
||||
"text_encoder",
|
||||
quantized_only=True,
|
||||
)
|
||||
|
||||
self.assertEqual(processed, 1)
|
||||
@@ -941,10 +1003,10 @@ class TestQuantizedTextEncoderPostprocess(unittest.TestCase):
|
||||
quant_method = _RecordingQuantMethod()
|
||||
model = _QuantizedEncoder(quant_method)
|
||||
|
||||
processed = _process_quantized_encoder_weights(
|
||||
processed = process_model_weights_after_loading(
|
||||
model,
|
||||
torch.device("cuda", torch.cuda.current_device()),
|
||||
"text_encoder",
|
||||
quantized_only=True,
|
||||
)
|
||||
|
||||
self.assertEqual(processed, 1)
|
||||
@@ -957,10 +1019,10 @@ class TestQuantizedTextEncoderPostprocess(unittest.TestCase):
|
||||
model = _QuantizedEncoder(_RecordingQuantMethod(error=RuntimeError("boom")))
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "boom"):
|
||||
_process_quantized_encoder_weights(
|
||||
process_model_weights_after_loading(
|
||||
model,
|
||||
torch.device("cuda", torch.cuda.current_device()),
|
||||
"text_encoder",
|
||||
quantized_only=True,
|
||||
)
|
||||
|
||||
self.assertEqual(model.quantized.weight.device, torch.device("cpu"))
|
||||
|
||||
@@ -27,18 +27,17 @@ from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader imp
|
||||
ComponentCheckpointUnsupportedError,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.component_loaders.vae_loader import (
|
||||
_adopt_plain_weight_norm_state,
|
||||
_assign_direct_gpu_vae_state,
|
||||
_backfill_ltx2_audio_vae_latent_stats,
|
||||
_consume_vae_checkpoint_arch_metadata,
|
||||
_direct_gpu_vae_state_slots,
|
||||
_match_checkpoint_dtypes,
|
||||
_require_native_loader_for_quantized_vae,
|
||||
_should_use_channels_last_3d,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.loader.utils import (
|
||||
checkpoint_bytes,
|
||||
keep_checkpoint_mapped,
|
||||
load_model_state_dict,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.managers.memory_managers import (
|
||||
host_memory_budget,
|
||||
@@ -47,6 +46,7 @@ from sglang.multimodal_gen.runtime.models.vaes import wanvae
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.ltx_2.decoding_av import (
|
||||
LTX2AVDecodingStage,
|
||||
)
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
|
||||
class _FakeServerArgs:
|
||||
@@ -131,28 +131,26 @@ class TestKeepCheckpointMapped(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestMatchCheckpointDtypes(unittest.TestCase):
|
||||
class TestMatchCheckpointDtypes(CustomTestCase):
|
||||
"""Assignment replaces a parameter, so only matching dtypes may stay mapped."""
|
||||
|
||||
def test_a_matching_tensor_is_left_alone(self):
|
||||
loaded = {"w": torch.zeros(4, dtype=torch.float32)}
|
||||
before = loaded["w"]
|
||||
_match_checkpoint_dtypes(loaded, {"w": torch.zeros(4, dtype=torch.float32)})
|
||||
self.assertIs(loaded["w"], before)
|
||||
|
||||
def test_a_mismatched_tensor_is_converted(self):
|
||||
loaded = {"w": torch.zeros(4, dtype=torch.float32)}
|
||||
_match_checkpoint_dtypes(loaded, {"w": torch.zeros(4, dtype=torch.bfloat16)})
|
||||
self.assertEqual(loaded["w"].dtype, torch.bfloat16)
|
||||
|
||||
def test_a_tensor_the_module_does_not_want_is_left_alone(self):
|
||||
loaded = {"extra": torch.zeros(4, dtype=torch.float32)}
|
||||
before = loaded["extra"]
|
||||
_match_checkpoint_dtypes(loaded, {})
|
||||
self.assertIs(loaded["extra"], before)
|
||||
def test_assignment_preserves_mixed_dtypes_and_matching_storage(self):
|
||||
model = nn.Linear(4, 4, bias=False, dtype=torch.bfloat16)
|
||||
model.register_buffer("scale", torch.zeros(4, dtype=torch.float32))
|
||||
weights = {
|
||||
"weight": torch.ones(4, 4, dtype=torch.float32),
|
||||
"scale": torch.ones(4, dtype=torch.float32),
|
||||
}
|
||||
checkpoint_weight = weights["weight"]
|
||||
load_model_state_dict(model, weights, assign=True)
|
||||
self.assertEqual(model.weight.dtype, torch.bfloat16)
|
||||
self.assertEqual(model.scale.dtype, torch.float32)
|
||||
self.assertEqual(model.scale.data_ptr(), weights["scale"].data_ptr())
|
||||
self.assertNotEqual(model.weight.data_ptr(), checkpoint_weight.data_ptr())
|
||||
self.assertTrue(torch.equal(model.weight.float(), checkpoint_weight))
|
||||
|
||||
|
||||
class TestPlainWeightNormCheckpoint(unittest.TestCase):
|
||||
class TestPlainWeightNormCheckpoint(CustomTestCase):
|
||||
def test_adopts_a_folded_weight_without_reconstructing_it(self):
|
||||
module = nn.Sequential(
|
||||
torch.nn.utils.parametrizations.weight_norm(
|
||||
@@ -162,8 +160,7 @@ class TestPlainWeightNormCheckpoint(unittest.TestCase):
|
||||
expected = torch.arange(18, dtype=torch.float32).reshape(3, 2, 3) / 19
|
||||
loaded = {"0.weight": expected}
|
||||
|
||||
self.assertEqual(_adopt_plain_weight_norm_state(module, loaded), 1)
|
||||
module.load_state_dict(loaded, strict=True)
|
||||
load_model_state_dict(module, loaded)
|
||||
|
||||
self.assertEqual(set(module.state_dict()), {"0.weight"})
|
||||
self.assertTrue(torch.equal(module[0].weight, expected))
|
||||
@@ -180,8 +177,7 @@ class TestPlainWeightNormCheckpoint(unittest.TestCase):
|
||||
"0.weight_v": original_state["0.parametrizations.weight.original1"].clone(),
|
||||
}
|
||||
|
||||
self.assertEqual(_adopt_plain_weight_norm_state(module, loaded), 0)
|
||||
module.load_state_dict(loaded, strict=True)
|
||||
load_model_state_dict(module, loaded)
|
||||
|
||||
self.assertIn("0.parametrizations.weight.original0", module.state_dict())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user