Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

779 lines
30 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Diffusers backend pipeline wrapper.
This module provides a wrapper that allows running any diffusers-supported model
through sglang's infrastructure using vanilla diffusers pipelines.
"""
import argparse
import inspect
import re
import warnings
from typing import Any
import numpy as np
import torch
import torchvision.transforms as T
from diffusers import DiffusionPipeline
from PIL import Image
from sglang.multimodal_gen.configs.pipeline_configs.base import PipelineConfig
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager import (
ComponentResidencyStrategy,
get_global_component_residency_manager,
)
from sglang.multimodal_gen.runtime.pipelines_core.composed_pipeline_base import (
ComposedPipelineBase,
)
from sglang.multimodal_gen.runtime.pipelines_core.executors.pipeline_executor import (
PipelineExecutor,
)
from sglang.multimodal_gen.runtime.pipelines_core.executors.sync_executor import (
SyncExecutor,
)
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
from sglang.multimodal_gen.runtime.pipelines_core.stages import PipelineStage
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
current_platform,
)
from sglang.multimodal_gen.runtime.server_args import ServerArgs
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import maybe_download_model
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.runtime.utils.precision import resolve_precision
from sglang.multimodal_gen.runtime.utils.vision import load_image as load_vision_image
logger = init_logger(__name__)
class DiffusersExecutionStage(PipelineStage):
"""Pipeline stage that wraps diffusers pipeline execution."""
def __init__(self, diffusers_pipe: DiffusionPipeline):
super().__init__()
self.diffusers_pipe = diffusers_pipe
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
"""Execute the diffusers pipeline."""
kwargs = self._build_pipeline_kwargs(batch)
# Filter kwargs to only those supported by the pipeline, warn about ignored args
kwargs, _ = self._filter_pipeline_kwargs(kwargs)
# Request tensor output for cleaner handling
if "output_type" not in kwargs:
kwargs["output_type"] = "pt"
with torch.no_grad(), warnings.catch_warnings(record=True):
warnings.simplefilter("always")
try:
output = self.diffusers_pipe(**kwargs)
except TypeError as e:
# Some pipelines don't support output_type="pt"
if "output_type" in str(e):
kwargs.pop("output_type", None)
output = self.diffusers_pipe(**kwargs)
else:
raise
batch.output = self._extract_output(output)
if batch.output is not None:
batch.output = self._postprocess_output(batch.output)
return batch
def _filter_pipeline_kwargs(
self, kwargs: dict[str, Any], *, strict: bool = False
) -> tuple[dict[str, Any], list[str]]:
"""Filter kwargs to those accepted by the pipeline's __call__.
Args:
kwargs: Arguments to filter
strict: If True, raise ValueError on unsupported args; otherwise warn
Returns:
Tuple of (filtered_kwargs, ignored_keys)
"""
try:
sig = inspect.signature(self.diffusers_pipe.__call__)
except (ValueError, TypeError):
return kwargs, []
params = sig.parameters
accepts_var_kwargs = any(
p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
)
if accepts_var_kwargs:
return kwargs, []
valid = set(params.keys()) - {"self"}
filtered = {}
ignored = []
for k, v in kwargs.items():
if k in valid:
filtered[k] = v
else:
ignored.append(k)
if ignored:
pipe_name = type(self.diffusers_pipe).__name__
msg = (
f"Pipeline '{pipe_name}' does not support: {', '.join(sorted(ignored))}. "
"These arguments will be ignored."
)
if strict:
raise ValueError(msg)
logger.warning(msg)
return filtered, ignored
def _extract_output(self, output: Any) -> torch.Tensor | None:
"""Extract tensor output from pipeline result."""
for attr in ["images", "frames", "video", "sample", "pred_original_sample"]:
data = getattr(output, attr, None)
if data is None:
continue
result = self._convert_to_tensor(data)
if result is not None:
logger.debug(
"Extracted output from '%s': shape=%s, dtype=%s",
attr,
result.shape,
result.dtype,
)
return result
logger.warning("Could not extract output from pipeline result")
return None
def _convert_to_tensor(self, data: Any) -> torch.Tensor | None:
"""Convert various data formats to a tensor."""
if isinstance(data, torch.Tensor):
return data
if isinstance(data, np.ndarray):
tensor = torch.from_numpy(data).float()
if tensor.max() > 1.0:
tensor = tensor / 255.0
# (B, H, W, C) -> (B, C, H, W) or (B, T, H, W, C) -> (B, C, T, H, W)
if tensor.ndim == 4:
tensor = tensor.permute(0, 3, 1, 2)
elif tensor.ndim == 5:
tensor = tensor.permute(0, 4, 1, 2, 3)
return tensor
if isinstance(data, Image.Image):
return T.ToTensor()(data)
if isinstance(data, list) and len(data) > 0:
return self._convert_list_to_tensor(data)
return None
def _convert_list_to_tensor(self, data: list) -> torch.Tensor | None:
"""Convert a list of items to a tensor."""
first = data[0]
# Nested list (e.g., [[frame1, frame2, ...]] for video batches)
if isinstance(first, list) and len(first) > 0:
data = first
first = data[0]
if isinstance(first, Image.Image):
tensors = [T.ToTensor()(img) for img in data]
stacked = torch.stack(tensors)
if len(tensors) > 1:
return stacked.permute(1, 0, 2, 3) # (T, C, H, W) -> (C, T, H, W)
return stacked[0]
if isinstance(first, torch.Tensor):
stacked = torch.stack(data)
if len(data) > 1:
return stacked.permute(1, 0, 2, 3)
return stacked[0]
if isinstance(first, np.ndarray):
tensors = [torch.from_numpy(arr).float() for arr in data]
if tensors[0].max() > 1.0:
tensors = [t / 255.0 for t in tensors]
if tensors[0].ndim == 3:
tensors = [t.permute(2, 0, 1) for t in tensors]
stacked = torch.stack(tensors)
if len(data) > 1:
return stacked.permute(1, 0, 2, 3)
return stacked[0]
return None
def _postprocess_output(self, output: torch.Tensor) -> torch.Tensor:
"""Post-process output tensor to ensure valid values and correct shape."""
output = output.cpu().float()
# Handle NaN or Inf values
if torch.isnan(output).any() or torch.isinf(output).any():
logger.warning("Output contains invalid values, fixing...")
output = torch.nan_to_num(output, nan=0.5, posinf=1.0, neginf=0.0)
# Normalize to [0, 1] range if needed
min_val, max_val = output.min().item(), output.max().item()
if min_val < -0.5 or max_val > 1.5:
output = (output + 1) / 2
output = output.clamp(0, 1)
# Ensure correct shape for downstream processing
output = self._fix_output_shape(output)
logger.debug("Final output tensor shape: %s", output.shape)
return output
def _fix_output_shape(self, output: torch.Tensor) -> torch.Tensor:
"""Fix tensor shape for downstream processing.
Expected: (B, C, H, W) for images or (B, C, T, H, W) for videos.
"""
if output.dim() == 5:
# Video: (B, T, C, H, W) -> (B, C, T, H, W)
return output.permute(0, 2, 1, 3, 4)
if output.dim() == 4:
if output.shape[0] == 1 or output.shape[1] in [1, 3, 4]:
return output # Already (B, C, H, W)
# (T, C, H, W) -> (1, C, T, H, W)
return output.unsqueeze(0).permute(0, 2, 1, 3, 4)
if output.dim() == 3:
c, h, w = output.shape
if c > 4 and w <= 4:
output = output.permute(2, 0, 1)
if output.shape[0] == 1:
output = output.repeat(3, 1, 1)
return output.unsqueeze(0)
if output.dim() == 2:
return output.unsqueeze(0).repeat(3, 1, 1).unsqueeze(0)
return output
def _build_pipeline_kwargs(self, batch: Req) -> dict[str, Any]:
"""Build kwargs dict for diffusers pipeline call."""
kwargs = {}
if batch.prompt is not None:
kwargs["prompt"] = batch.prompt
if batch.negative_prompt:
kwargs["negative_prompt"] = batch.negative_prompt
if batch.num_inference_steps is not None:
kwargs["num_inference_steps"] = batch.num_inference_steps
if batch.guidance_scale is not None:
kwargs["guidance_scale"] = batch.guidance_scale
if batch.true_cfg_scale is not None:
kwargs["true_cfg_scale"] = batch.true_cfg_scale
if batch.height is not None:
kwargs["height"] = batch.height
if batch.width is not None:
kwargs["width"] = batch.width
if batch.num_frames is not None and batch.num_frames > 1:
kwargs["num_frames"] = batch.num_frames
# Generator for reproducibility
if batch.generator is not None:
kwargs["generator"] = batch.generator
elif batch.seed is not None:
device = self._get_generator_device(batch)
kwargs["generator"] = torch.Generator(device=device).manual_seed(batch.seed)
# Image input for img2img or inpainting
image = self._load_input_image(batch)
if image is not None:
kwargs["image"] = image
if batch.num_outputs_per_prompt > 1:
kwargs["num_images_per_prompt"] = batch.num_outputs_per_prompt
# Extra diffusers-specific kwargs
if batch.extra:
diffusers_kwargs = batch.extra.get("diffusers_kwargs", {})
if diffusers_kwargs:
kwargs.update(diffusers_kwargs)
return kwargs
def _get_generator_device(self, batch: Req) -> str:
"""Resolve RNG device consistently with the non-diffusers path.
Diffusers CPU offload can temporarily park modules on CPU, but that
should not silently switch a CUDA request to CPU RNG, otherwise the
same seed produces different outputs depending on runtime placement.
"""
if batch.generator_device == "cpu":
return "cpu"
return current_platform.device_type
def _load_input_image(self, batch: Req) -> Image.Image | None:
"""Load input image from batch."""
# Check for PIL image in condition_image or pixel_values
if batch.condition_image is not None and isinstance(
batch.condition_image, Image.Image
):
return batch.condition_image
if batch.pixel_values is not None and isinstance(
batch.pixel_values, Image.Image
):
return batch.pixel_values
if not batch.image_path:
return None
if isinstance(batch.image_path, list):
batch.image_path = batch.image_path[0]
try:
image = load_vision_image(batch.image_path)
return image.convert("RGB")
except Exception as e:
logger.error("Failed to load image from %s: %s", batch.image_path, e)
return None
class DiffusersPipeline(ComposedPipelineBase):
"""
Pipeline wrapper that uses vanilla diffusers pipelines.
This allows running any diffusers-supported model through sglang's infrastructure
without requiring native sglang implementation.
"""
pipeline_name = "DiffusersPipeline"
is_video_pipeline = False
_required_config_modules: list[str] = []
def __init__(
self,
model_path: str,
server_args: ServerArgs,
required_config_modules: list[str] | None = None,
loaded_modules: dict[str, torch.nn.Module] | None = None,
executor: PipelineExecutor | None = None,
):
self.server_args = server_args
self.model_path = model_path
self._stages: list[PipelineStage] = []
self._stage_name_mapping: dict[str, PipelineStage] = {}
self.modules: dict[str, Any] = {}
self.memory_usages: dict[str, float] = {}
self.component_residency_strategies: dict[str, ComponentResidencyStrategy] = {}
self.component_residency_manager = None
self.post_init_called = False
self.executor = executor or SyncExecutor(server_args=server_args)
self._cache_dit_enabled = False
logger.info("Loading diffusers pipeline from %s", model_path)
self.diffusers_pipe = self._load_diffusers_pipeline(model_path, server_args)
self._detect_pipeline_type()
def _load_diffusers_pipeline(
self, model_path: str, server_args: ServerArgs
) -> DiffusionPipeline:
"""Load the diffusers pipeline.
Optimizations applied:
- device_map: Loads models directly to GPU, warming up CUDA caching allocator
to avoid small tensor allocations during inference.
- Parallel shard loading: When using device_map with accelerate, model shards
are loaded in parallel for faster initialization.
"""
original_model_path = model_path # Keep original for custom_pipeline
model_path = maybe_download_model(model_path, force_diffusers_model=True)
self.model_path = model_path
dtype = self._get_dtype(server_args)
logger.info("Loading diffusers pipeline with dtype=%s", dtype)
# Build common kwargs for from_pretrained
load_kwargs = {
"torch_dtype": dtype,
"trust_remote_code": server_args.trust_remote_code,
"revision": server_args.revision,
}
# Add quantization config if provided (e.g., BitsAndBytesConfig for 4/8-bit)
quant_config = getattr(server_args.pipeline_config, "quantization_config", None)
if quant_config is not None:
load_kwargs["quantization_config"] = quant_config
logger.info("Using quantization config: %s", type(quant_config).__name__)
try:
pipe = DiffusionPipeline.from_pretrained(model_path, **load_kwargs)
except AttributeError as e:
if "has no attribute" in str(e):
# Custom pipeline class not in diffusers - try loading with custom_pipeline
logger.info(
"Pipeline class not found in diffusers, trying custom_pipeline from repo..."
)
try:
custom_kwargs = {
**load_kwargs,
"custom_pipeline": original_model_path,
}
custom_kwargs["trust_remote_code"] = True
pipe = DiffusionPipeline.from_pretrained(
model_path, **custom_kwargs
)
except Exception as e2:
match = re.search(r"has no attribute (\w+)", str(e))
class_name = match.group(1) if match else "unknown"
raise RuntimeError(
f"Pipeline class '{class_name}' not found in diffusers and no custom pipeline.py in repo. "
f"Try: pip install --upgrade diffusers (some pipelines require latest version). "
f"Original error: {e}"
) from e2
else:
raise
except Exception as e:
# Only retry with float32 for dtype-related errors
if "dtype" in str(e).lower() or "float" in str(e).lower():
logger.warning(
"Failed with dtype=%s, falling back to float32: %s", dtype, e
)
load_kwargs["torch_dtype"] = torch.float32
pipe = DiffusionPipeline.from_pretrained(model_path, **load_kwargs)
else:
raise
# Use CPU offload (all-or-nothing in diffusers) if any component offload is requested.
any_offload = (
server_args.dit_cpu_offload
or server_args.text_encoder_cpu_offload
or server_args.image_encoder_cpu_offload
or server_args.vae_cpu_offload
)
if any_offload:
device = get_local_torch_device()
gpu_id = device.index if device.index is not None else 0
pipe.enable_model_cpu_offload(gpu_id=gpu_id)
logger.info(
"Enabled model CPU offload for diffusers pipeline (gpu_id=%d)", gpu_id
)
else:
pipe = pipe.to(get_local_torch_device())
# Apply VAE memory optimizations from pipeline config
self._apply_vae_optimizations(pipe, server_args)
# Apply attention backend if specified
self._apply_attention_backend(pipe, server_args)
# Apply cache-dit acceleration if configured
pipe = self._apply_cache_dit(pipe, server_args)
# Apply torch.compile if enabled and supported
pipe = self._apply_torch_compile(pipe, server_args)
logger.info("Loaded diffusers pipeline: %s", pipe.__class__.__name__)
return pipe
def _apply_vae_optimizations(
self, pipe: DiffusionPipeline, server_args: ServerArgs
) -> None:
"""Apply VAE memory optimizations (tiling, slicing) from pipeline config."""
config = server_args.pipeline_config
# VAE slicing: decode latents slice-by-slice for lower peak memory
# https://huggingface.co/docs/diffusers/optimization/memory#vae-slicing
if config.vae_slicing:
if hasattr(pipe, "vae") and hasattr(pipe.vae, "enable_slicing"):
pipe.vae.enable_slicing()
logger.info("Enabled VAE slicing for lower memory usage")
elif hasattr(pipe, "enable_vae_slicing"):
pipe.enable_vae_slicing()
logger.info("Enabled VAE slicing for lower memory usage")
else:
logger.warning(
"VAE slicing is not available: neither "
"`pipe.vae.enable_slicing()` nor `pipe.enable_vae_slicing()` was found."
)
# VAE tiling: decode latents tile-by-tile for large images
# https://huggingface.co/docs/diffusers/optimization/memory#vae-tiling
if config.vae_tiling:
if hasattr(pipe, "vae") and hasattr(pipe.vae, "enable_tiling"):
pipe.vae.enable_tiling()
logger.info("Enabled VAE tiling for large image support")
elif hasattr(pipe, "enable_vae_tiling"):
pipe.enable_vae_tiling()
logger.info("Enabled VAE tiling for large image support")
else:
logger.warning(
"VAE tiling is not available: neither "
"`pipe.vae.enable_tiling()` nor `pipe.enable_vae_tiling()` was found."
)
def _apply_attention_backend(
self, pipe: DiffusionPipeline, server_args: ServerArgs
) -> None:
"""Apply attention backend setting from pipeline config or server_args.
See: https://huggingface.co/docs/diffusers/main/en/optimization/attention_backends
Available backends: flash, _flash_3_hub, sage, xformers, native, etc.
"""
backend = server_args.attention_backend
if backend is None:
backend = getattr(
server_args.pipeline_config, "diffusers_attention_backend", None
)
if backend is None:
return
backend = backend.lower()
sglang_backends = {e.name.lower() for e in AttentionBackendEnum} | {
"fa3",
"fa4",
}
if backend in sglang_backends:
logger.debug(
"Skipping diffusers attention backend '%s' because it matches a "
"SGLang backend name. Use diffusers backend names when running "
"the diffusers backend.",
backend,
)
return
for component_name in ["transformer", "unet"]:
component = getattr(pipe, component_name, None)
if component is not None and hasattr(component, "set_attention_backend"):
try:
component.set_attention_backend(backend)
logger.info(
"Set attention backend '%s' on %s", backend, component_name
)
except Exception as e:
logger.warning(
"Failed to set attention backend '%s' on %s: %s",
backend,
component_name,
e,
)
def _apply_cache_dit(
self, pipe: DiffusionPipeline, server_args: ServerArgs
) -> DiffusionPipeline:
"""Enable cache-dit for diffusers pipeline if configured."""
cache_dit_config = server_args.cache_dit_config
if not cache_dit_config:
return pipe
try:
import cache_dit
except ImportError as e:
raise RuntimeError(
"cache-dit is required for --cache-dit-config. "
"Install it with `pip install cache-dit`."
) from e
if not hasattr(cache_dit, "load_configs"):
raise RuntimeError(
"cache-dit>=1.2.0 is required for --cache-dit-config. "
"Please upgrade cache-dit."
)
try:
cache_options = cache_dit.load_configs(cache_dit_config)
except Exception as e:
raise ValueError(
"Failed to load cache-dit config. Provide a YAML/JSON path (or a dict "
"supported by cache-dit>=1.2.0)."
) from e
try:
pipe = cache_dit.enable_cache(pipe, **cache_options)
except Exception:
# cache-dit is an external integration and can raise a variety of errors.
logger.exception("Failed to enable cache-dit for diffusers pipeline")
raise
logger.info("Enabled cache-dit for diffusers pipeline")
self._cache_dit_enabled = True
return pipe
def _apply_torch_compile(self, pipe: Any, server_args: ServerArgs) -> Any:
"""Apply torch.compile to the pipeline if configured and supported."""
if not server_args.enable_torch_compile:
return pipe
# check if the pipeline has 'transformer' or 'unet' components which are
# typically the most expensive parts to compile. 'transformer_2' for some
# video pipelines, e.g, Wan 2.2 series, also check for that.
compilable_components = ["transformer", "transformer_2", "unet"]
if not any(hasattr(pipe, comp) for comp in compilable_components):
logger.warning(
"Pipeline does not have 'transformer' or 'unet' components. "
"torch.compile may not provide significant benefits and could increase latency."
)
return pipe
if self._cache_dit_enabled:
try:
import cache_dit
if hasattr(cache_dit, "set_compile_configs"):
cache_dit.set_compile_configs()
except Exception as e:
logger.warning(
f"Failed to set torch_compile configs for cache-dit: {e}"
)
for comp in compilable_components:
if hasattr(pipe, comp):
try:
component = getattr(pipe, comp)
repeated_blocks = getattr(component, "_repeated_blocks", None)
if (
isinstance(component, torch.nn.Module)
and repeated_blocks
and hasattr(component, "compile_repeated_blocks")
):
# Regional compilation: compile a single instance of each
# repeated transformer block and let inductor's cache reuse
# it for all repeats, instead of compiling the whole DiT as
# one graph
component.compile_repeated_blocks()
elif isinstance(component, torch.nn.Module) and hasattr(
component, "compile"
):
# Prefer in-place compilation if supported. According to PyTorch documentation:
# https://docs.pytorch.org/docs/stable/generated/torch.compile.html
component.compile()
else:
compiled_component = torch.compile(component)
setattr(pipe, comp, compiled_component)
logger.info(
f"Applied torch.compile to {comp} component of the pipeline"
)
except Exception as e:
logger.warning(f"Failed to apply torch.compile to {comp}: {e}")
return pipe
def _get_dtype(self, server_args: ServerArgs) -> torch.dtype:
"""
Determine the dtype to use for model loading.
"""
if hasattr(server_args, "pipeline_config") and server_args.pipeline_config:
return resolve_precision(server_args, "dit", precision_attr="dit_precision")
# precision-constraint: legacy fallback for callers without pipeline_config;
# prefer explicit dit_precision policy when available.
return torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
def _detect_pipeline_type(self) -> None:
"""Detect if this is an image or video pipeline."""
pipe_class_name = self.diffusers_pipe.__class__.__name__.lower()
video_indicators = ["video", "animat", "cogvideo", "wan", "hunyuan"]
self.is_video_pipeline = any(ind in pipe_class_name for ind in video_indicators)
logger.debug(
"Detected pipeline type: %s",
"video" if self.is_video_pipeline else "image",
)
def load_modules(
self,
server_args: ServerArgs,
loaded_modules: dict[str, torch.nn.Module] | None = None,
) -> dict[str, Any]:
"""Skip sglang's module loading - diffusers handles it."""
return {"diffusers_pipeline": self.diffusers_pipe}
def create_pipeline_stages(self, server_args: ServerArgs) -> None:
"""Create the execution stage wrapping the diffusers pipeline."""
self.add_stage(
stage_name="diffusers_execution",
stage=DiffusersExecutionStage(self.diffusers_pipe),
)
def initialize_pipeline(self, server_args: ServerArgs) -> None:
pass
def post_init(self) -> None:
"""Post initialization hook."""
if self.post_init_called:
return
self.post_init_called = True
self.initialize_pipeline(self.server_args)
self.create_pipeline_stages(self.server_args)
def add_stage(self, stage_name: str, stage: PipelineStage) -> None:
"""Add a stage to the pipeline."""
if stage_name is None:
stage_name = self._infer_stage_name(stage)
if stage_name in self._stage_name_mapping:
raise ValueError(f"Duplicate stage name detected: {stage_name}")
stage.set_registered_stage_name(stage_name)
stage.set_profile_stage_name(self._profile_stage_name(stage, stage_name))
self._stages.append(stage)
self._stage_name_mapping[stage_name] = stage
return self
@property
def stages(self) -> list[PipelineStage]:
"""List of stages in the pipeline."""
return self._stages
@torch.no_grad()
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
"""Execute the pipeline on the given batch."""
if not self.post_init_called:
self.post_init()
self.component_residency_manager = get_global_component_residency_manager(
self, server_args
)
self.executor.component_residency_manager = self.component_residency_manager
return self.executor.execute_with_profiling(self.stages, batch, server_args)
@classmethod
def from_pretrained(
cls,
model_path: str,
device: str | None = None,
torch_dtype: torch.dtype | None = None,
pipeline_config: str | PipelineConfig | None = None,
args: argparse.Namespace | None = None,
required_config_modules: list[str] | None = None,
loaded_modules: dict[str, torch.nn.Module] | None = None,
**kwargs,
) -> "DiffusersPipeline":
"""Load a pipeline from a pretrained model using diffusers backend."""
kwargs["model_path"] = model_path
server_args = ServerArgs.from_kwargs(**kwargs)
pipe = cls(
model_path,
server_args,
required_config_modules=required_config_modules,
loaded_modules=loaded_modules,
)
pipe.post_init()
return pipe
def get_module(self, module_name: str, default_value: Any = None) -> Any:
"""Get a module by name."""
if module_name == "diffusers_pipeline":
return self.diffusers_pipe
return self.modules.get(module_name, default_value)
EntryClass = DiffusersPipeline