[diffusion] feat: support distilled vae generic (#14195)
Co-authored-by: BBuf <1182563586@qq.com> Co-authored-by: Mick <mickjagger19@icloud.com>
This commit is contained in:
@@ -47,9 +47,21 @@ class FluxVAEConfig(VAEConfig):
|
|||||||
) * 2
|
) * 2
|
||||||
|
|
||||||
def post_init(self):
|
def post_init(self):
|
||||||
self.arch_config.vae_scale_factor = 2 ** (
|
# Calculate vae_scale_factor: prefer block_out_channels, fallback to dim_mult or scale_factor_spatial
|
||||||
len(self.arch_config.block_out_channels) - 1
|
if (
|
||||||
)
|
hasattr(self.arch_config, "block_out_channels")
|
||||||
|
and self.arch_config.block_out_channels
|
||||||
|
):
|
||||||
|
self.arch_config.vae_scale_factor = 2 ** (
|
||||||
|
len(self.arch_config.block_out_channels) - 1
|
||||||
|
)
|
||||||
|
elif self.arch_config.dim_mult:
|
||||||
|
self.arch_config.vae_scale_factor = 2 ** (
|
||||||
|
len(self.arch_config.dim_mult) - 1
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.arch_config.vae_scale_factor = self.arch_config.scale_factor_spatial
|
||||||
|
|
||||||
self.arch_config.spatial_compression_ratio = self.arch_config.vae_scale_factor
|
self.arch_config.spatial_compression_ratio = self.arch_config.vae_scale_factor
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ from sglang.multimodal_gen.configs.pipeline_configs.base import (
|
|||||||
SlidingTileAttnConfig,
|
SlidingTileAttnConfig,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.flux import FluxPipelineConfig
|
from sglang.multimodal_gen.configs.pipeline_configs.flux import FluxPipelineConfig
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.flux_finetuned import (
|
||||||
|
Flux2FinetunedPipelineConfig,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.hunyuan import (
|
from sglang.multimodal_gen.configs.pipeline_configs.hunyuan import (
|
||||||
FastHunyuanConfig,
|
FastHunyuanConfig,
|
||||||
HunyuanConfig,
|
HunyuanConfig,
|
||||||
@@ -23,6 +26,7 @@ __all__ = [
|
|||||||
"HunyuanConfig",
|
"HunyuanConfig",
|
||||||
"FastHunyuanConfig",
|
"FastHunyuanConfig",
|
||||||
"FluxPipelineConfig",
|
"FluxPipelineConfig",
|
||||||
|
"Flux2FinetunedPipelineConfig",
|
||||||
"PipelineConfig",
|
"PipelineConfig",
|
||||||
"SlidingTileAttnConfig",
|
"SlidingTileAttnConfig",
|
||||||
"WanT2V480PConfig",
|
"WanT2V480PConfig",
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ class PipelineConfig:
|
|||||||
return image_latents
|
return image_latents
|
||||||
|
|
||||||
# called after scale_and_shift, before vae decoding
|
# called after scale_and_shift, before vae decoding
|
||||||
def preprocess_decoding(self, latents):
|
def preprocess_decoding(self, latents, server_args=None, vae=None):
|
||||||
return latents
|
return latents
|
||||||
|
|
||||||
def gather_latents_for_sp(self, latents):
|
def gather_latents_for_sp(self, latents):
|
||||||
@@ -487,9 +487,30 @@ class PipelineConfig:
|
|||||||
raise ValueError("model_path is required in kwargs")
|
raise ValueError("model_path is required in kwargs")
|
||||||
|
|
||||||
# 1. Get the pipeline config class from the registry
|
# 1. Get the pipeline config class from the registry
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.flux import (
|
||||||
|
Flux2PipelineConfig,
|
||||||
|
)
|
||||||
|
|
||||||
model_info = get_model_info(model_path)
|
model_info = get_model_info(model_path)
|
||||||
|
|
||||||
pipeline_config = model_info.pipeline_config_cls()
|
# 1.5. Adjust pipeline config for fine-tuned VAE if needed
|
||||||
|
pipeline_config_cls = model_info.pipeline_config_cls
|
||||||
|
vae_path = kwargs.get(prefix_with_dot + "vae_path") or kwargs.get("vae_path")
|
||||||
|
|
||||||
|
# Check if this is a Flux2 model with fal/FLUX.2-Tiny-AutoEncoder
|
||||||
|
if (
|
||||||
|
isinstance(pipeline_config_cls, type)
|
||||||
|
and issubclass(pipeline_config_cls, Flux2PipelineConfig)
|
||||||
|
and vae_path is not None
|
||||||
|
and "FLUX.2-Tiny-AutoEncoder" in vae_path
|
||||||
|
):
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.flux_finetuned import (
|
||||||
|
Flux2FinetunedPipelineConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
pipeline_config_cls = Flux2FinetunedPipelineConfig
|
||||||
|
|
||||||
|
pipeline_config = pipeline_config_cls()
|
||||||
|
|
||||||
# 2. Load PipelineConfig from a json file or a PipelineConfig object if provided
|
# 2. Load PipelineConfig from a json file or a PipelineConfig object if provided
|
||||||
if isinstance(pipeline_config_or_path, str):
|
if isinstance(pipeline_config_or_path, str):
|
||||||
|
|||||||
@@ -215,10 +215,10 @@ def _prepare_latent_ids(
|
|||||||
t = torch.arange(1) # [0] - time dimension
|
t = torch.arange(1) # [0] - time dimension
|
||||||
h = torch.arange(height)
|
h = torch.arange(height)
|
||||||
w = torch.arange(width)
|
w = torch.arange(width)
|
||||||
l = torch.arange(1) # [0] - layer dimension
|
layer = torch.arange(1) # [0] - layer dimension
|
||||||
|
|
||||||
# Create position IDs: (H*W, 4)
|
# Create position IDs: (H*W, 4)
|
||||||
latent_ids = torch.cartesian_prod(t, h, w, l)
|
latent_ids = torch.cartesian_prod(t, h, w, layer)
|
||||||
|
|
||||||
# Expand to batch: (B, H*W, 4)
|
# Expand to batch: (B, H*W, 4)
|
||||||
latent_ids = latent_ids.unsqueeze(0).expand(batch_size, -1, -1)
|
latent_ids = latent_ids.unsqueeze(0).expand(batch_size, -1, -1)
|
||||||
@@ -289,9 +289,9 @@ def _prepare_text_ids(
|
|||||||
t = torch.arange(1) if t_coord is None else t_coord[i]
|
t = torch.arange(1) if t_coord is None else t_coord[i]
|
||||||
h = torch.arange(1)
|
h = torch.arange(1)
|
||||||
w = torch.arange(1)
|
w = torch.arange(1)
|
||||||
l = torch.arange(L)
|
layer = torch.arange(L)
|
||||||
|
|
||||||
coords = torch.cartesian_prod(t, h, w, l)
|
coords = torch.cartesian_prod(t, h, w, layer)
|
||||||
out_ids.append(coords)
|
out_ids.append(coords)
|
||||||
|
|
||||||
return torch.stack(out_ids)
|
return torch.stack(out_ids)
|
||||||
@@ -500,7 +500,6 @@ class Flux2PipelineConfig(FluxPipelineConfig):
|
|||||||
return image_latents
|
return image_latents
|
||||||
|
|
||||||
def get_freqs_cis(self, prompt_embeds, width, height, device, rotary_emb, batch):
|
def get_freqs_cis(self, prompt_embeds, width, height, device, rotary_emb, batch):
|
||||||
|
|
||||||
txt_ids = _prepare_text_ids(prompt_embeds).to(device=device)
|
txt_ids = _prepare_text_ids(prompt_embeds).to(device=device)
|
||||||
|
|
||||||
img_ids = batch.latent_ids
|
img_ids = batch.latent_ids
|
||||||
@@ -550,19 +549,51 @@ class Flux2PipelineConfig(FluxPipelineConfig):
|
|||||||
image_latents = _patchify_latents(image_latents)
|
image_latents = _patchify_latents(image_latents)
|
||||||
return image_latents
|
return image_latents
|
||||||
|
|
||||||
def preprocess_decoding(self, latents):
|
def _check_vae_has_bn(self, vae):
|
||||||
latents = _unpatchify_latents(latents)
|
"""Check if VAE has bn attribute (cached check to avoid repeated hasattr calls)."""
|
||||||
|
if not hasattr(self, "_vae_has_bn_cache"):
|
||||||
|
self._vae_has_bn_cache = hasattr(vae, "bn") and vae.bn is not None
|
||||||
|
return self._vae_has_bn_cache
|
||||||
|
|
||||||
|
def preprocess_decoding(self, latents, server_args=None, vae=None):
|
||||||
|
"""Preprocess latents before decoding.
|
||||||
|
|
||||||
|
Dynamically adapts based on VAE type:
|
||||||
|
- Standard Flux2 VAE (has bn): needs unpatchify (128 channels -> 32 channels)
|
||||||
|
- Distilled VAE (no bn): keeps patchified latents (128 channels)
|
||||||
|
"""
|
||||||
|
if vae is not None and self._check_vae_has_bn(vae):
|
||||||
|
return _unpatchify_latents(latents)
|
||||||
return latents
|
return latents
|
||||||
|
|
||||||
def get_decode_scale_and_shift(self, device, dtype, vae):
|
def get_decode_scale_and_shift(self, device, dtype, vae):
|
||||||
|
"""Get scale and shift for decoding.
|
||||||
|
|
||||||
|
Dynamically adapts based on VAE type:
|
||||||
|
- Standard Flux2 VAE (has bn): uses BatchNorm statistics
|
||||||
|
- Distilled VAE (no bn): uses scaling_factor from config
|
||||||
|
"""
|
||||||
vae_arch_config = self.vae_config.arch_config
|
vae_arch_config = self.vae_config.arch_config
|
||||||
latents_bn_mean = (
|
|
||||||
vae.bn.running_mean.view(1, -1, 1, 1).to(device=device).to(device, dtype)
|
if self._check_vae_has_bn(vae):
|
||||||
|
# Standard Flux2 VAE: use BatchNorm statistics
|
||||||
|
latents_bn_mean = vae.bn.running_mean.view(1, -1, 1, 1).to(device, dtype)
|
||||||
|
latents_bn_std = torch.sqrt(
|
||||||
|
vae.bn.running_var.view(1, -1, 1, 1) + vae_arch_config.batch_norm_eps
|
||||||
|
).to(device, dtype)
|
||||||
|
return 1 / latents_bn_std, latents_bn_mean
|
||||||
|
|
||||||
|
# Distilled VAE or unknown: use scaling_factor
|
||||||
|
scaling_factor = (
|
||||||
|
getattr(vae.config, "scaling_factor", None)
|
||||||
|
if hasattr(vae, "config")
|
||||||
|
else getattr(vae, "scaling_factor", None)
|
||||||
|
) or getattr(vae_arch_config, "scaling_factor", 0.13025)
|
||||||
|
|
||||||
|
scale = torch.tensor(scaling_factor, device=device, dtype=dtype).view(
|
||||||
|
1, 1, 1, 1
|
||||||
)
|
)
|
||||||
latents_bn_std = torch.sqrt(
|
return 1 / scale, None
|
||||||
vae.bn.running_var.view(1, -1, 1, 1) + vae_arch_config.batch_norm_eps
|
|
||||||
).to(device, dtype)
|
|
||||||
return 1 / latents_bn_std, latents_bn_mean
|
|
||||||
|
|
||||||
def post_denoising_loop(self, latents, batch):
|
def post_denoising_loop(self, latents, batch):
|
||||||
latent_ids = batch.latent_ids
|
latent_ids = batch.latent_ids
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"""
|
||||||
|
Pipeline configuration for Flux fine-tuned/distilled models.
|
||||||
|
|
||||||
|
This module provides specialized handling for Flux fine-tuned models from HuggingFace,
|
||||||
|
such as fal/FLUX.2-Tiny-AutoEncoder and other community fine-tuned variants.
|
||||||
|
|
||||||
|
Key differences from standard Flux2PipelineConfig:
|
||||||
|
- Handles custom VAE architectures loaded via auto_map
|
||||||
|
- Supports both patchified (128 channels) and unpatchified (32 channels) latents
|
||||||
|
- Dynamically adapts scale/shift based on VAE type
|
||||||
|
- Properly handles 5D latents (batch, channels, frames, height, width) for decoding
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.flux import (
|
||||||
|
Flux2PipelineConfig,
|
||||||
|
_unpatchify_latents,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Flux2FinetunedPipelineConfig(Flux2PipelineConfig):
|
||||||
|
"""
|
||||||
|
Pipeline configuration for Flux fine-tuned/distilled models.
|
||||||
|
|
||||||
|
This configuration automatically detects and handles custom VAE architectures
|
||||||
|
(e.g., Flux2TinyAutoEncoder) loaded via HuggingFace's auto_map mechanism.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Automatic VAE type detection (standard vs. distilled)
|
||||||
|
- Proper handling of patchified/unpatchified latents
|
||||||
|
- Support for custom scaling factors from fine-tuned models
|
||||||
|
- 5D latents support for both single-frame and multi-frame generation
|
||||||
|
"""
|
||||||
|
|
||||||
|
def preprocess_decoding(
|
||||||
|
self, latents: torch.Tensor, server_args=None, vae=None
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Preprocess latents before decoding.
|
||||||
|
|
||||||
|
Handles both standard Flux2 VAE and fine-tuned/distilled VAEs:
|
||||||
|
- Standard Flux2 VAE (has bn): needs unpatchify (128 channels -> 32 channels)
|
||||||
|
- Distilled/Finetuned VAE (no bn): keeps patchified latents (128 channels)
|
||||||
|
|
||||||
|
Also handles 5D latents (batch, channels, frames, height, width) by converting
|
||||||
|
to 4D (batch, channels, height, width) for single-frame cases.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
latents: Input latents tensor, can be 4D or 5D
|
||||||
|
server_args: Server arguments (optional, for compatibility)
|
||||||
|
vae: VAE model instance for dynamic type detection
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Preprocessed latents ready for VAE decoding
|
||||||
|
"""
|
||||||
|
# Handle 5D latents (batch, channels, frames, height, width)
|
||||||
|
if latents.ndim == 5:
|
||||||
|
batch_size, channels, frames, height, width = latents.shape
|
||||||
|
if frames == 1:
|
||||||
|
latents = latents.squeeze(2)
|
||||||
|
else:
|
||||||
|
latents = latents.permute(0, 2, 1, 3, 4).contiguous()
|
||||||
|
latents = latents.view(batch_size * frames, channels, height, width)
|
||||||
|
|
||||||
|
if vae is not None and self._check_vae_has_bn(vae):
|
||||||
|
latents = _unpatchify_latents(latents)
|
||||||
|
return latents
|
||||||
|
|
||||||
|
def get_decode_scale_and_shift(self, device, dtype, vae):
|
||||||
|
"""
|
||||||
|
Get scale and shift for decoding.
|
||||||
|
|
||||||
|
Dynamically adapts based on VAE type:
|
||||||
|
- Standard Flux2 VAE (has bn): uses BatchNorm statistics
|
||||||
|
- Distilled/Finetuned VAE (no bn): uses scaling_factor from config
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device: Target device for tensors
|
||||||
|
dtype: Target dtype for tensors
|
||||||
|
vae: VAE model instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (scaling_factor, shift_factor)
|
||||||
|
- scaling_factor: Tensor or scalar to divide latents by
|
||||||
|
- shift_factor: Tensor or scalar to add to latents (None for distilled VAEs)
|
||||||
|
"""
|
||||||
|
vae_arch_config = self.vae_config.arch_config
|
||||||
|
|
||||||
|
if self._check_vae_has_bn(vae):
|
||||||
|
# Standard Flux2 VAE: use BatchNorm statistics
|
||||||
|
latents_bn_mean = vae.bn.running_mean.view(1, -1, 1, 1).to(device, dtype)
|
||||||
|
latents_bn_std = torch.sqrt(
|
||||||
|
vae.bn.running_var.view(1, -1, 1, 1) + vae_arch_config.batch_norm_eps
|
||||||
|
).to(device, dtype)
|
||||||
|
return 1 / latents_bn_std, latents_bn_mean
|
||||||
|
|
||||||
|
# Distilled/Finetuned VAE: Flux2TinyAutoEncoder doesn't need external scaling
|
||||||
|
scale = torch.tensor(1.0, device=device, dtype=dtype).view(1, 1, 1, 1)
|
||||||
|
return scale, None
|
||||||
@@ -4,10 +4,11 @@
|
|||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import glob
|
import glob
|
||||||
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC
|
||||||
from collections.abc import Generator, Iterable
|
from collections.abc import Generator, Iterable
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
@@ -101,17 +102,13 @@ def load_native(library, component_module_path: str, server_args: ServerArgs):
|
|||||||
revision=server_args.revision,
|
revision=server_args.revision,
|
||||||
)
|
)
|
||||||
elif library == "diffusers":
|
elif library == "diffusers":
|
||||||
import diffusers
|
from diffusers import AutoModel
|
||||||
|
|
||||||
config = get_diffusers_component_config(model_path=component_module_path)
|
return AutoModel.from_pretrained(
|
||||||
class_name = config.pop("_class_name", None)
|
component_module_path,
|
||||||
if class_name:
|
revision=server_args.revision,
|
||||||
cls = getattr(diffusers, class_name)
|
trust_remote_code=server_args.trust_remote_code,
|
||||||
return cls.from_pretrained(
|
)
|
||||||
component_module_path, revision=server_args.revision, **config
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError("Cannot determine class name for generic diffusers loader")
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported library: {library}")
|
raise ValueError(f"Unsupported library: {library}")
|
||||||
|
|
||||||
@@ -202,13 +199,6 @@ class ComponentLoader(ABC):
|
|||||||
f"load_customized not implemented for {self.__class__.__name__}"
|
f"load_customized not implemented for {self.__class__.__name__}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def load_customized(
|
|
||||||
self, model_path: str, server_args: ServerArgs, module_name: str
|
|
||||||
) -> Any:
|
|
||||||
"""Implement the minimal core load logic in subclasses."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_module_type(
|
def for_module_type(
|
||||||
cls, module_type: str, transformers_or_diffusers: str
|
cls, module_type: str, transformers_or_diffusers: str
|
||||||
@@ -503,7 +493,6 @@ class ImageEncoderLoader(TextEncoderLoader):
|
|||||||
encoder_config.update_model_arch(model_config)
|
encoder_config.update_model_arch(model_config)
|
||||||
|
|
||||||
# Always start with local device; load_model will adjust for offload if needed
|
# Always start with local device; load_model will adjust for offload if needed
|
||||||
should_offload = self.should_offload(server_args)
|
|
||||||
# TODO(will): add support for other dtypes
|
# TODO(will): add support for other dtypes
|
||||||
return self.load_model(
|
return self.load_model(
|
||||||
component_model_path,
|
component_model_path,
|
||||||
@@ -555,7 +544,7 @@ class VAELoader(ComponentLoader):
|
|||||||
):
|
):
|
||||||
"""Load the VAE based on the model path, and inference args."""
|
"""Load the VAE based on the model path, and inference args."""
|
||||||
config = get_diffusers_component_config(model_path=component_model_path)
|
config = get_diffusers_component_config(model_path=component_model_path)
|
||||||
class_name = config.pop("_class_name")
|
class_name = config.pop("_class_name", None)
|
||||||
assert (
|
assert (
|
||||||
class_name is not None
|
class_name is not None
|
||||||
), "Model config does not contain a _class_name attribute. Only diffusers format is supported."
|
), "Model config does not contain a _class_name attribute. Only diffusers format is supported."
|
||||||
@@ -571,23 +560,42 @@ class VAELoader(ComponentLoader):
|
|||||||
|
|
||||||
target_device = self.target_device(server_args.vae_cpu_offload)
|
target_device = self.target_device(server_args.vae_cpu_offload)
|
||||||
|
|
||||||
with set_default_torch_dtype(
|
# Check for auto_map first (custom VAE classes)
|
||||||
PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision]
|
auto_map = config.get("auto_map", {})
|
||||||
), skip_init_modules():
|
auto_model_map = auto_map.get("AutoModel")
|
||||||
|
if auto_model_map:
|
||||||
|
module_path, cls_name = auto_model_map.rsplit(".", 1)
|
||||||
|
custom_module_file = os.path.join(component_model_path, f"{module_path}.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("_custom", custom_module_file)
|
||||||
|
custom_module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(custom_module)
|
||||||
|
vae_cls = getattr(custom_module, cls_name)
|
||||||
|
vae_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision]
|
||||||
|
with set_default_torch_dtype(vae_dtype):
|
||||||
|
vae = vae_cls.from_pretrained(
|
||||||
|
component_model_path,
|
||||||
|
revision=server_args.revision,
|
||||||
|
trust_remote_code=server_args.trust_remote_code,
|
||||||
|
)
|
||||||
|
vae = vae.to(device=target_device, dtype=vae_dtype)
|
||||||
|
return vae.eval()
|
||||||
|
|
||||||
|
# Load from ModelRegistry (standard VAE classes)
|
||||||
|
with (
|
||||||
|
set_default_torch_dtype(
|
||||||
|
PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision]
|
||||||
|
),
|
||||||
|
skip_init_modules(),
|
||||||
|
):
|
||||||
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||||
vae = vae_cls(vae_config).to(target_device)
|
vae = vae_cls(vae_config).to(target_device)
|
||||||
|
|
||||||
# Find all safetensors files
|
|
||||||
safetensors_list = _list_safetensors_files(component_model_path)
|
safetensors_list = _list_safetensors_files(component_model_path)
|
||||||
# TODO(PY)
|
|
||||||
assert (
|
assert (
|
||||||
len(safetensors_list) == 1
|
len(safetensors_list) == 1
|
||||||
), f"Found {len(safetensors_list)} safetensors files in {component_model_path}"
|
), f"Found {len(safetensors_list)} safetensors files in {component_model_path}"
|
||||||
loaded = safetensors_load_file(safetensors_list[0])
|
loaded = safetensors_load_file(safetensors_list[0])
|
||||||
vae.load_state_dict(
|
vae.load_state_dict(loaded, strict=False)
|
||||||
loaded, strict=False
|
|
||||||
) # We might only load encoder or decoder
|
|
||||||
|
|
||||||
return vae.eval()
|
return vae.eval()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -281,7 +281,6 @@ class ComposedPipelineBase(ABC):
|
|||||||
transformers_or_diffusers,
|
transformers_or_diffusers,
|
||||||
architecture,
|
architecture,
|
||||||
) in tqdm(iterable=model_index.items(), desc="Loading required modules"):
|
) in tqdm(iterable=model_index.items(), desc="Loading required modules"):
|
||||||
|
|
||||||
if transformers_or_diffusers is None:
|
if transformers_or_diffusers is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Module %s in model_index.json has null value, removing from required_config_modules",
|
"Module %s in model_index.json has null value, removing from required_config_modules",
|
||||||
@@ -304,7 +303,19 @@ class ComposedPipelineBase(ABC):
|
|||||||
else:
|
else:
|
||||||
load_module_name = module_name
|
load_module_name = module_name
|
||||||
|
|
||||||
component_model_path = os.path.join(self.model_path, load_module_name)
|
# Use custom VAE path if provided, otherwise use default path
|
||||||
|
if module_name == "vae" and server_args.vae_path is not None:
|
||||||
|
component_model_path = server_args.vae_path
|
||||||
|
# Download from HuggingFace Hub if path doesn't exist locally
|
||||||
|
if not os.path.exists(component_model_path):
|
||||||
|
component_model_path = maybe_download_model(component_model_path)
|
||||||
|
logger.info(
|
||||||
|
"Using custom VAE path: %s instead of default path: %s",
|
||||||
|
component_model_path,
|
||||||
|
os.path.join(self.model_path, load_module_name),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
component_model_path = os.path.join(self.model_path, load_module_name)
|
||||||
module = PipelineComponentLoader.load_module(
|
module = PipelineComponentLoader.load_module(
|
||||||
module_name=load_module_name,
|
module_name=load_module_name,
|
||||||
component_model_path=component_model_path,
|
component_model_path=component_model_path,
|
||||||
|
|||||||
@@ -27,6 +27,26 @@ from sglang.multimodal_gen.utils import PRECISION_TO_TYPE
|
|||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_tensor_decode_output(decode_output):
|
||||||
|
"""
|
||||||
|
Ensure VAE decode output is a tensor.
|
||||||
|
|
||||||
|
Some VAE implementations return DecoderOutput objects with a .sample attribute,
|
||||||
|
tuples, or tensors directly. This function normalizes the output to always be a tensor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
decode_output: Output from VAE.decode(), can be DecoderOutput, tuple, or torch.Tensor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
torch.Tensor: The decoded image tensor
|
||||||
|
"""
|
||||||
|
if isinstance(decode_output, tuple):
|
||||||
|
return decode_output[0]
|
||||||
|
if hasattr(decode_output, "sample"):
|
||||||
|
return decode_output.sample
|
||||||
|
return decode_output
|
||||||
|
|
||||||
|
|
||||||
class DecodingStage(PipelineStage):
|
class DecodingStage(PipelineStage):
|
||||||
"""
|
"""
|
||||||
Stage for decoding latent representations into pixel space.
|
Stage for decoding latent representations into pixel space.
|
||||||
@@ -106,7 +126,10 @@ class DecodingStage(PipelineStage):
|
|||||||
|
|
||||||
# scale and shift
|
# scale and shift
|
||||||
latents = self.scale_and_shift(latents, server_args)
|
latents = self.scale_and_shift(latents, server_args)
|
||||||
latents = server_args.pipeline_config.preprocess_decoding(latents)
|
# Preprocess latents before decoding (e.g., unpatchify for standard Flux2 VAE)
|
||||||
|
latents = server_args.pipeline_config.preprocess_decoding(
|
||||||
|
latents, server_args, vae=self.vae
|
||||||
|
)
|
||||||
|
|
||||||
# Decode latents
|
# Decode latents
|
||||||
with torch.autocast(
|
with torch.autocast(
|
||||||
@@ -120,7 +143,8 @@ class DecodingStage(PipelineStage):
|
|||||||
pass
|
pass
|
||||||
if not vae_autocast_enabled:
|
if not vae_autocast_enabled:
|
||||||
latents = latents.to(vae_dtype)
|
latents = latents.to(vae_dtype)
|
||||||
image = self.vae.decode(latents)
|
decode_output = self.vae.decode(latents)
|
||||||
|
image = _ensure_tensor_decode_output(decode_output)
|
||||||
|
|
||||||
# De-normalize image to [0, 1] range
|
# De-normalize image to [0, 1] range
|
||||||
image = (image / 2 + 0.5).clamp(0, 1)
|
image = (image / 2 + 0.5).clamp(0, 1)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# Inspired by SGLang: https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/server_args.py
|
# Inspired by SGLang: https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/server_args.py
|
||||||
"""The arguments of sglang-diffusion Inference."""
|
"""The arguments of sglang-diffusion Inference."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
@@ -252,6 +253,9 @@ class ServerArgs:
|
|||||||
# (Wenxuan) prefer to keep it here instead of in pipeline config to not make it complicated.
|
# (Wenxuan) prefer to keep it here instead of in pipeline config to not make it complicated.
|
||||||
lora_path: str | None = None
|
lora_path: str | None = None
|
||||||
lora_nickname: str = "default" # for swapping adapters in the pipeline
|
lora_nickname: str = "default" # for swapping adapters in the pipeline
|
||||||
|
|
||||||
|
# VAE parameters
|
||||||
|
vae_path: str | None = None # Custom VAE path (e.g., for distilled autoencoder)
|
||||||
# can restrict layers to adapt, e.g. ["q_proj"]
|
# can restrict layers to adapt, e.g. ["q_proj"]
|
||||||
# Will adapt only q, k, v, o by default.
|
# Will adapt only q, k, v, o by default.
|
||||||
lora_target_modules: list[str] | None = None
|
lora_target_modules: list[str] | None = None
|
||||||
@@ -374,6 +378,12 @@ class ServerArgs:
|
|||||||
type=str,
|
type=str,
|
||||||
help="Directory containing StepVideo model",
|
help="Directory containing StepVideo model",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--vae-path",
|
||||||
|
type=str,
|
||||||
|
default=ServerArgs.vae_path,
|
||||||
|
help="Custom path to VAE model (e.g., for distilled autoencoder). If not specified, VAE will be loaded from the main model path.",
|
||||||
|
)
|
||||||
|
|
||||||
# attention
|
# attention
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -825,17 +835,15 @@ class ServerArgs:
|
|||||||
if self.ulysses_degree is None:
|
if self.ulysses_degree is None:
|
||||||
self.ulysses_degree = 1
|
self.ulysses_degree = 1
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Ulysses degree not set, " f"using default value {self.ulysses_degree}"
|
f"Ulysses degree not set, using default value {self.ulysses_degree}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.ring_degree is None:
|
if self.ring_degree is None:
|
||||||
self.ring_degree = 1
|
self.ring_degree = 1
|
||||||
logger.info(
|
logger.info(f"Ring degree not set, using default value {self.ring_degree}")
|
||||||
f"Ring degree not set, " f"using default value {self.ring_degree}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.ring_degree > 1:
|
if self.ring_degree > 1:
|
||||||
if self.attention_backend != None and self.attention_backend != "fa":
|
if self.attention_backend is not None and self.attention_backend != "fa":
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Ring Attention is only supported for flash attention backend for now"
|
"Ring Attention is only supported for flash attention backend for now"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -135,33 +135,33 @@ def get_diffusers_component_config(
|
|||||||
"""Gets a configuration of a submodule for the given diffusers model.
|
"""Gets a configuration of a submodule for the given diffusers model.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model_path: the path of the submodule
|
model_path: the path of the submodule (can be local path or HuggingFace model ID)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The loaded configuration.
|
The loaded configuration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check if the model path exists
|
# Download from HuggingFace Hub if path doesn't exist locally
|
||||||
if os.path.exists(model_path):
|
if not os.path.exists(model_path):
|
||||||
# tokenizer
|
model_path = maybe_download_model(model_path)
|
||||||
config_names = ["generation_config.json"]
|
|
||||||
# By default, we load config.json, but scheduler_config.json for scheduler
|
|
||||||
if "scheduler" in model_path:
|
|
||||||
config_names.append("scheduler_config.json")
|
|
||||||
else:
|
|
||||||
config_names.append("config.json")
|
|
||||||
|
|
||||||
config_file_paths = [
|
# tokenizer
|
||||||
os.path.join(model_path, config_name) for config_name in config_names
|
config_names = ["generation_config.json"]
|
||||||
]
|
# By default, we load config.json, but scheduler_config.json for scheduler
|
||||||
|
if "scheduler" in model_path:
|
||||||
combined_config = reduce(
|
config_names.append("scheduler_config.json")
|
||||||
lambda acc, path: acc | load_dict(path), config_file_paths, {}
|
|
||||||
)
|
|
||||||
|
|
||||||
return combined_config
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Diffusers config file not found at {model_path}")
|
config_names.append("config.json")
|
||||||
|
|
||||||
|
config_file_paths = [
|
||||||
|
os.path.join(model_path, config_name) for config_name in config_names
|
||||||
|
]
|
||||||
|
|
||||||
|
combined_config = reduce(
|
||||||
|
lambda acc, path: acc | load_dict(path), config_file_paths, {}
|
||||||
|
)
|
||||||
|
|
||||||
|
return combined_config
|
||||||
|
|
||||||
|
|
||||||
# Models don't use the same configuration key for determining the maximum
|
# Models don't use the same configuration key for determining the maximum
|
||||||
@@ -390,9 +390,10 @@ def maybe_download_model(
|
|||||||
logger.info(
|
logger.info(
|
||||||
"Downloading model snapshot from HF Hub for %s...", model_name_or_path
|
"Downloading model snapshot from HF Hub for %s...", model_name_or_path
|
||||||
)
|
)
|
||||||
with get_lock(model_name_or_path).acquire(
|
with (
|
||||||
poll_interval=2
|
get_lock(model_name_or_path).acquire(poll_interval=2),
|
||||||
), suppress_other_loggers(not_suppress_on_main_rank=True):
|
suppress_other_loggers(not_suppress_on_main_rank=True),
|
||||||
|
):
|
||||||
local_path = snapshot_download(
|
local_path = snapshot_download(
|
||||||
repo_id=model_name_or_path,
|
repo_id=model_name_or_path,
|
||||||
ignore_patterns=["*.onnx", "*.msgpack"],
|
ignore_patterns=["*.onnx", "*.msgpack"],
|
||||||
|
|||||||
Reference in New Issue
Block a user