[diffusion] quant: add support for svdquant and nunchaku (#18549)

Co-authored-by: AichenF <aichenf@nvidia.com>
Co-authored-by: jianyingzhu <53300651@qq.com>
This commit is contained in:
Mick
2026-02-15 20:43:00 +08:00
committed by GitHub
co-authored by AichenF jianyingzhu
parent 88010e9601
commit 3feb48139e
20 changed files with 1304 additions and 123 deletions
@@ -28,6 +28,10 @@ class QwenImageArchConfig(DiTArchConfig):
default_factory=lambda: { default_factory=lambda: {
# LoRA mappings # LoRA mappings
r"^(transformer_blocks\.\d+\.attn\..*\.lora_[AB])\.default$": r"\1", r"^(transformer_blocks\.\d+\.attn\..*\.lora_[AB])\.default$": r"\1",
# SVDquant mappings
r"(.*)\.add_qkv_proj\.(.+)$": r"\1.to_added_qkv.\2",
r"(transformer_blocks\.\d+\.(img_mlp|txt_mlp)\..*\.(smooth_factor_orig|wcscales))$": r"\1",
r".*\.wtscale$": r"",
} }
) )
@@ -186,6 +186,13 @@ class QwenImagePipelineConfig(ImagePipelineConfig):
] * batch_size ] * batch_size
txt_seq_lens = [prompt_embeds[0].shape[1]] txt_seq_lens = [prompt_embeds[0].shape[1]]
if rotary_emb is None:
return {
"img_shapes": img_shapes,
"txt_seq_lens": txt_seq_lens,
"freqs_cis": None,
}
freqs_cis = self.get_freqs_cis( freqs_cis = self.get_freqs_cis(
img_shapes, txt_seq_lens, rotary_emb, device, dtype img_shapes, txt_seq_lens, rotary_emb, device, dtype
) )
@@ -195,6 +202,7 @@ class QwenImagePipelineConfig(ImagePipelineConfig):
return { return {
"txt_seq_lens": txt_seq_lens, "txt_seq_lens": txt_seq_lens,
"freqs_cis": (img_cache, txt_cache), "freqs_cis": (img_cache, txt_cache),
"img_shapes": img_shapes,
} }
def prepare_pos_cond_kwargs(self, batch, device, rotary_emb, dtype): def prepare_pos_cond_kwargs(self, batch, device, rotary_emb, dtype):
@@ -254,6 +262,14 @@ class QwenImageEditPipelineConfig(QwenImagePipelineConfig):
], ],
] * batch_size ] * batch_size
txt_seq_lens = [prompt_embeds[0].shape[1]] txt_seq_lens = [prompt_embeds[0].shape[1]]
if rotary_emb is None:
return {
"img_shapes": img_shapes,
"txt_seq_lens": txt_seq_lens,
"freqs_cis": None,
}
freqs_cis = QwenImagePipelineConfig.get_freqs_cis( freqs_cis = QwenImagePipelineConfig.get_freqs_cis(
img_shapes, txt_seq_lens, rotary_emb, device, dtype img_shapes, txt_seq_lens, rotary_emb, device, dtype
) )
@@ -271,6 +287,7 @@ class QwenImageEditPipelineConfig(QwenImagePipelineConfig):
return { return {
"txt_seq_lens": txt_seq_lens, "txt_seq_lens": txt_seq_lens,
"freqs_cis": (img_cache, txt_cache), "freqs_cis": (img_cache, txt_cache),
"img_shapes": img_shapes,
} }
def preprocess_condition_image( def preprocess_condition_image(
@@ -272,7 +272,6 @@ class ZImagePipelineConfig(ImagePipelineConfig):
device=device, device=device,
).flatten(0, 2) ).flatten(0, 2)
C = self.dit_config.num_channels_latents
F = 1 F = 1
H = height // self.vae_config.arch_config.spatial_compression_ratio H = height // self.vae_config.arch_config.spatial_compression_ratio
W = width // self.vae_config.arch_config.spatial_compression_ratio W = width // self.vae_config.arch_config.spatial_compression_ratio
@@ -0,0 +1,174 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import os
import re
from dataclasses import dataclass
from typing import Any
import torch
from sglang.multimodal_gen.runtime.layers.quantization.configs.nunchaku_config import (
is_nunchaku_available,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.utils import StoreBoolean
logger = init_logger(__name__)
@dataclass
class NunchakuSVDQuantArgs:
"""CLI-facing configuration for Nunchaku (SVDQuant) inference.
This is intentionally lightweight and only contains arguments needed to
construct `runtime.layers.quantization.nunchaku_config.NunchakuConfig`.
"""
enable_svdquant: bool = False
quantized_model_path: str | None = None
quantization_precision: str | None = None # "int4" or "nvfp4"
quantization_rank: int | None = None
quantization_act_unsigned: bool = False
def _adjust_config(self) -> None:
"""infer precision and rank from filename if not provided"""
if self.quantized_model_path and not self.enable_svdquant:
self.enable_svdquant = True
if not self.enable_svdquant or not self.quantized_model_path:
return
inferred_precision = None
inferred_rank = None
filename = os.path.basename(self.quantized_model_path)
# Expected pattern: svdq-{precision}_r{rank}-...
# e.g., svdq-int4_r32-qwen-image.safetensors
match = re.search(r"svdq-(int4|fp4)_r(\d+)", filename)
if match:
p_str, r_str = match.groups()
inferred_precision = "nvfp4" if p_str == "fp4" else "int4"
inferred_rank = int(r_str)
if self.quantization_precision is None:
self.quantization_precision = inferred_precision or "int4"
if inferred_precision:
logger.info(
f"inferred --quantization-precision: {self.quantization_precision} "
f"from --quantized-model-path: {self.quantized_model_path}"
)
if self.quantization_rank is None:
self.quantization_rank = inferred_rank or 32
if inferred_rank:
logger.info(
f"inferred --quantization-rank: {self.quantization_rank} "
f"from --quantized-model-path: {self.quantized_model_path}"
)
def validate(self) -> None:
# TODO: warn if the served model doesn't support nunchaku
self._adjust_config()
if not self.enable_svdquant:
return
if not current_platform.is_cuda():
raise ValueError(
"Nunchaku SVDQuant is only supported on NVIDIA CUDA GPUs "
"(Ampere SM8x or SM12x)."
)
device_count = torch.cuda.device_count()
unsupported: list[str] = []
for i in range(device_count):
major, minor = torch.cuda.get_device_capability(i)
if major == 9:
unsupported.append(f"cuda:{i} (SM{major}{minor}, Hopper)")
elif major not in (8, 12):
unsupported.append(f"cuda:{i} (SM{major}{minor})")
if unsupported:
raise ValueError(
"Nunchaku SVDQuant is currently only supported on Ampere (SM8x) or SM12x GPUs; "
"Hopper (SM90) is not supported. "
f"Unsupported devices: {', '.join(unsupported)}. "
"Disable it with --enable-svdquant false."
)
if not self.quantized_model_path:
raise ValueError(
"--enable-svdquant requires --quantized-model-path to be set"
)
if not is_nunchaku_available():
raise ValueError(
"Nunchaku is enabled, but not installed. Please refer to https://nunchaku.tech/docs/nunchaku/installation/installation.html for detailed installation methods."
)
if self.quantization_precision not in ("int4", "nvfp4"):
raise ValueError(
f"Invalid --quantization-precision: {self.quantization_precision}. "
"Must be one of: int4, nvfp4"
)
if self.quantization_rank <= 0:
raise ValueError(
f"Invalid --quantization-rank: {self.quantization_rank}. Must be > 0"
)
@staticmethod
def add_cli_args(parser) -> None:
parser.add_argument(
"--enable-svdquant",
action=StoreBoolean,
default=NunchakuSVDQuantArgs.enable_svdquant,
help="Enable Nunchaku SVDQuant (W4A4-style) inference.",
)
parser.add_argument(
"--quantized-model-path",
type=str,
default=NunchakuSVDQuantArgs.quantized_model_path,
help=(
"Path to pre-quantized Nunchaku weights. Can be a single .safetensors "
"file or a directory containing .safetensors."
),
)
parser.add_argument(
"--quantization-precision",
type=str,
default=None,
help="Quantization precision: int4 or nvfp4. If not specified, inferred from model path or defaults to int4.",
)
parser.add_argument(
"--quantization-rank",
type=int,
default=None,
help="SVD low-rank dimension (e.g., 32). If not specified, inferred from model path or defaults to 32.",
)
parser.add_argument(
"--quantization-act-unsigned",
action=StoreBoolean,
default=NunchakuSVDQuantArgs.quantization_act_unsigned,
help="Use unsigned activation quantization (if supported).",
)
@classmethod
def from_dict(cls, kwargs: dict[str, Any]) -> "NunchakuSVDQuantArgs":
# Map CLI/config keys to dataclass fields (keep backwards compatibility).
return cls(
enable_svdquant=bool(kwargs.get("enable_svdquant", cls.enable_svdquant)),
quantized_model_path=kwargs.get(
"quantized_model_path", cls.quantized_model_path
),
quantization_precision=kwargs.get("quantization_precision"),
quantization_rank=kwargs.get("quantization_rank"),
quantization_act_unsigned=bool(
kwargs.get("quantization_act_unsigned", cls.quantization_act_unsigned)
),
)
@@ -0,0 +1,123 @@
# Quantization
This document introduces the model quantization schemes supported in SGLang and how to use them to reduce memory usage and accelerate inference.
## Nunchaku (SVDQuant)
### Introduction
**SVDQuant** is a Post-Training Quantization (PTQ) technique for diffusion models that quantizes model weights and activations to 4-bit precision (W4A4) while maintaining high visual quality. This method uses Singular Value Decomposition (SVD) to decompose the weight matrix into low-rank components and residuals, effectively absorbing outliers in activations, making 4-bit quantization possible.
**Nunchaku** is a high-performance inference engine that implements SVDQuant, optimized for low-bit neural networks. It is not Quantization-Aware Training (QAT), but directly quantizes pre-trained models.
Paper: [SVDQuant: Absorbing Outliers by Low-Rank Components for 4-Bit Diffusion Models](https://arxiv.org/abs/2411.05007) (ICLR 2025 Spotlight)
### Key Features
SVDQuant significantly reduces memory usage and accelerates inference while maintaining visual quality:
- **Memory Optimization**: Reduces memory usage by **3.6×** compared to BF16 models.
- **Inference Acceleration**:
- **3.0×** faster than the NF4 (W4A16) baseline on desktop/laptop RTX 4090 GPUs.
- **8.7×** speedup on laptop RTX 4090 by eliminating CPU offloading compared to 16-bit models.
- **3.1×** faster than BF16 and NF4 models on RTX 5090 GPUs with NVFP4.
### Supported Precisions
Nunchaku supports two quantization precisions:
- **INT4**: Standard INT4 quantization, supported on NVIDIA GPUs with Compute Capability 7.0+ (RTX 20 series and above).
- **NVFP4**: FP4 quantization, providing better image quality on newer cards like the RTX 5090.
### Usage
#### 1. Install Nunchaku
```bash
pip install nunchaku
```
For more installation information, please refer to the [Nunchaku Official Documentation](https://nunchaku.tech/docs/nunchaku/installation/installation.html).
#### 2. Download Quantized Models
Nunchaku provides pre-quantized model weights available on Hugging Face:
- [nunchaku-ai/nunchaku-qwen-image](https://huggingface.co/nunchaku-ai/nunchaku-qwen-image)
- [nunchaku-ai/nunchaku-flux](https://huggingface.co/nunchaku-ai/nunchaku-flux)
Taking Qwen-Image as an example, several quantized models with different configurations are provided:
| Filename | Precision | Rank | Usage |
|----------|-----------|------|-------|
| `svdq-int4_r32-qwen-image.safetensors` | INT4 | 32 | Standard Version |
| `svdq-int4_r128-qwen-image.safetensors` | INT4 | 128 | High-Quality Version |
| `svdq-fp4_r32-qwen-image.safetensors` | NVFP4 | 32 | RTX 5090 Standard Version |
| `svdq-fp4_r128-qwen-image.safetensors` | NVFP4 | 128 | RTX 5090 High-Quality Version |
| `svdq-int4_r32-qwen-image-lightningv1.0-4steps.safetensors` | INT4 | 32 | Lightning 4-Step Version |
| `svdq-int4_r128-qwen-image-lightningv1.1-8steps.safetensors` | INT4 | 128 | Lightning 8-Step Version |
> **Note**: Higher Rank usually means better image quality, but with slightly increased memory usage and computation.
#### 3. Run Quantized Models
SGLang features **smart auto-detection** for Nunchaku models. In most cases, you only need to provide the path to the quantized weights, and the precision and rank will be automatically inferred from the filename.
**Simplified Command (Recommended):**
```bash
sglang generate \
--model-path Qwen/Qwen-Image \
--prompt "change the raccoon to a cute cat" \
--save-output \
--quantized-model-path /path/to/svdq-int4_r32-qwen-image.safetensors
```
**Manual Override (If needed):**
If your filename doesn't follow the standard naming convention, or you want to force specific settings:
- `--enable-svdquant`: Manually enable SVDQuant.
- `--quantization-precision`: Set to `int4` or `nvfp4`.
- `--quantization-rank`: Set the SVD rank (e.g., 32, 128).
- `--quantization-act-unsigned` (Optional): Use unsigned activation quantization.
Example with manual overrides:
```bash
sglang generate \
--model-path Qwen/Qwen-Image \
--prompt "a beautiful sunset" \
--enable-svdquant \
--quantized-model-path /path/to/custom_model.safetensors \
--quantization-precision int4 \
--quantization-rank 128
```
#### 4. Configuration Recommendations
Choose the appropriate configuration based on your hardware and requirements:
| Scenario | Recommended Config | Description |
|----------|-------------------|-------------|
| Standard Use (20/30/40 Series GPU) | INT4 + Rank 32 | Balanced performance and quality |
| Quality Focus (Sufficient VRAM) | INT4 + Rank 128 | Better image quality |
| RTX 5090 Standard Use | NVFP4 + Rank 32 | Utilizes FP4 hardware acceleration |
| RTX 5090 Quality Focus | NVFP4 + Rank 128 | Best image quality |
| Fast Prototyping/Preview | Lightning 4-Step Version | Extremely fast generation, slightly reduced quality |
### Notes
1. Model Path Correspondence: `--model-path` should point to the original non-quantized model (for loading config and tokenizer, etc.), while `--quantized-model-path` points to the quantized weight file.
2. Auto-Detection Requirements: For auto-detection to work, the filename must contain the pattern `svdq-{precision}_r{rank}` (e.g., `svdq-int4_r32`).
3. GPU Compatibility:
- INT4: Supports NVIDIA GPUs with Compute Capability 7.0+ (RTX 20 series and above).
- NVFP4: Optimized mainly for newer cards like the RTX 50 series that support FP4.
4. Lightning Models: When using Lightning versions, adjust `--num-inference-steps` accordingly (usually 4 or 8 steps).
### Custom Model Quantization
If you want to quantize your own models, you can use the [DeepCompressor](https://github.com/mit-han-lab/deepcompressor) tool. For detailed instructions, please refer to the Nunchaku official documentation.
@@ -17,7 +17,7 @@ from sglang.multimodal_gen.runtime.distributed import (
tensor_model_parallel_all_gather, tensor_model_parallel_all_gather,
tensor_model_parallel_all_reduce, tensor_model_parallel_all_reduce,
) )
from sglang.multimodal_gen.runtime.layers.quantization.base_config import ( from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig, QuantizationConfig,
QuantizeMethodBase, QuantizeMethodBase,
) )
@@ -165,7 +165,6 @@ class LinearBase(torch.nn.Module):
Args: Args:
input_size: input dimension of the linear layer. input_size: input dimension of the linear layer.
output_size: output dimension of the linear layer. output_size: output dimension of the linear layer.
bias: If true, add bias.
skip_bias_add: If true, skip adding bias but instead return it. skip_bias_add: If true, skip adding bias but instead return it.
params_dtype: Data type for the parameters. params_dtype: Data type for the parameters.
quant_config: Quantization configure. quant_config: Quantization configure.
@@ -2,7 +2,7 @@
from typing import Literal, get_args from typing import Literal, get_args
from sglang.multimodal_gen.runtime.layers.quantization.base_config import ( from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig, QuantizationConfig,
) )
@@ -23,17 +23,7 @@ def register_quantization_config(quantization: str):
Args: Args:
quantization (str): The quantization method name. quantization (str): The quantization method name.
Examples:
>>> from sglang.multimodal_gen.runtime.layers.quantization import register_quantization_config
>>> from sglang.multimodal_gen.runtime.layers.quantization import get_quantization_config
>>> from sglang.multimodal_gen.runtime.layers.quantization.base_config import QuantizationConfig
>>>
>>> @register_quantization_config("my_quant")
... class MyQuantConfig(QuantizationConfig):
... pass
>>>
>>> get_quantization_config("my_quant")
<class 'MyQuantConfig'>
""" # noqa: E501 """ # noqa: E501
def _wrapper(quant_config_cls): def _wrapper(quant_config_cls):
@@ -63,7 +53,7 @@ def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
return method_to_config[quantization] return method_to_config[quantization]
all = [ __all__ = [
"QuantizationMethods", "QuantizationMethods",
"QuantizationConfig", "QuantizationConfig",
"get_quantization_config", "get_quantization_config",
@@ -65,6 +65,9 @@ def method_has_implemented_embedding(method_class: type[QuantizeMethodBase]) ->
class QuantizationConfig(ABC): class QuantizationConfig(ABC):
"""Base class for quantization configs.""" """Base class for quantization configs."""
# for quantization frameworks with a separate quantized model provided, e.g. Nunchaku
quantized_model_path: str | None = None
def __init__(self): def __init__(self):
super().__init__() super().__init__()
# mapping is updated by models as they initialize # mapping is updated by models as they initialize
@@ -0,0 +1,289 @@
# SPDX-License-Identifier: Apache-2.0
import json
import os
from dataclasses import dataclass
from functools import lru_cache
from typing import Any, Optional
import torch
from safetensors.torch import load_file as safetensors_load_file
from torch import nn
from sglang.multimodal_gen.runtime.layers.linear import LinearBase
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from .base_config import QuantizationConfig, QuantizeMethodBase
logger = init_logger(__name__)
SVDQ_W4A4_LAYER_PATTERNS = [
"attn.to_qkv",
"attn.to_out",
"attn.add_qkv_proj",
"attn.to_add_out",
"img_mlp",
"txt_mlp",
]
AWQ_W4A16_LAYER_PATTERNS = [
"img_mod",
"txt_mod",
]
SKIP_QUANTIZATION_PATTERNS = [
"norm",
"embed",
"rotary",
"pos_embed",
]
@lru_cache(maxsize=1)
def is_nunchaku_available() -> bool:
try:
import nunchaku # noqa
return True
except Exception:
return False
@dataclass
class NunchakuConfig(QuantizationConfig):
"""
Configuration for Nunchaku (SVDQuant) W4A4-style quantization.
Attributes:
precision: Quantization precision type. Options:
- "int4": Standard INT4 quantization
- "nvfp4": FP4 quantization
rank: SVD low-rank dimension for absorbing outliers
group_size: Quantization group size (automatically set based on precision)
act_unsigned: Use unsigned activation quantization
quantized_model_path: Path to pre-quantized model weights (.safetensors)
"""
precision: str = "int4" # "int4" or "nvfp4"
rank: int = 32
group_size: Optional[int] = None
act_unsigned: bool = False
quantized_model_path: Optional[str] = None
@classmethod
def get_name(cls) -> str:
return "svdquant"
@classmethod
def get_supported_act_dtypes(cls) -> list[torch.dtype]:
return [torch.bfloat16, torch.float16]
@classmethod
def get_min_capability(cls) -> int:
return 70
@staticmethod
def get_config_filenames() -> list[str]:
return ["quantization_config.json", "quant_config.json"]
@classmethod
def from_config(cls, config: dict[str, Any]) -> "NunchakuConfig":
return cls(
precision=config.get("precision", "int4"),
rank=int(config.get("rank", 32)),
group_size=config.get("group_size"),
act_unsigned=bool(config.get("act_unsigned", False)),
quantized_model_path=config.get("quantized_model_path"),
)
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
if not isinstance(layer, LinearBase):
return None
for pattern in SKIP_QUANTIZATION_PATTERNS:
if pattern in prefix.lower():
return None
for pattern in SVDQ_W4A4_LAYER_PATTERNS:
if pattern in prefix:
from ..nunchaku_linear import NunchakuSVDQLinearMethod
return NunchakuSVDQLinearMethod(
precision=self.precision,
rank=self.rank,
act_unsigned=self.act_unsigned,
)
for pattern in AWQ_W4A16_LAYER_PATTERNS:
if pattern in prefix:
from ..nunchaku_linear import NunchakuAWQLinearMethod
return NunchakuAWQLinearMethod(
group_size=64,
)
from ..nunchaku_linear import NunchakuSVDQLinearMethod
return NunchakuSVDQLinearMethod(
precision=self.precision,
rank=self.rank,
act_unsigned=self.act_unsigned,
)
def __post_init__(self):
if self.group_size is None:
if self.precision == "nvfp4":
self.group_size = 16
elif self.precision == "int4":
self.group_size = 64
else:
raise ValueError(
f"Invalid precision: {self.precision}. Must be 'int4' or 'nvfp4'"
)
if self.precision not in ["int4", "nvfp4"]:
raise ValueError(
f"Invalid precision: {self.precision}. Must be 'int4' or 'nvfp4'"
)
if self.rank <= 0:
raise ValueError(f"Rank must be positive, got {self.rank}")
@classmethod
def from_dict(cls, config_dict: dict) -> "NunchakuConfig":
"""Create configuration from dictionary."""
return cls(**config_dict)
def to_dict(self) -> dict:
"""Convert configuration to dictionary."""
return {
"precision": self.precision,
"rank": self.rank,
"group_size": self.group_size,
"act_unsigned": self.act_unsigned,
"quantized_model_path": self.quantized_model_path,
}
@classmethod
def from_pretrained(cls, model_path: str) -> Optional["NunchakuConfig"]:
for filename in cls.get_config_filenames():
config_path = os.path.join(model_path, filename)
if os.path.exists(config_path):
with open(config_path, "r") as f:
config_dict = json.load(f)
if config_dict.get("quant_method") == cls.get_name():
return cls.from_config(config_dict)
return None
def _patch_native_svdq_linear(
module: nn.Module, tensor: Any, svdq_linear_cls: type
) -> bool:
if (
isinstance(module, svdq_linear_cls)
and getattr(module, "wtscale", None) is not None
):
module.wtscale = tensor
return True
return False
def _patch_sglang_svdq_linear(
module: nn.Module, tensor: Any, svdq_method_cls: type
) -> bool:
quant_method = getattr(module, "quant_method", None)
if not isinstance(quant_method, svdq_method_cls):
return False
existing = getattr(module, "wtscale", None)
if isinstance(existing, nn.Parameter):
with torch.no_grad():
existing.data.copy_(tensor.to(existing.data.dtype))
else:
module.wtscale = tensor
# Keep alpha in sync (kernel reads `layer._nunchaku_alpha`)
try:
module._nunchaku_alpha = float(tensor.detach().cpu().item())
except Exception:
module._nunchaku_alpha = None
return True
def _patch_sglang_svdq_wcscales(
module: nn.Module, tensor: Any, svdq_method_cls: type
) -> bool:
quant_method = getattr(module, "quant_method", None)
if not isinstance(quant_method, svdq_method_cls):
return False
existing = getattr(module, "wcscales", None)
if isinstance(existing, nn.Parameter):
with torch.no_grad():
existing.data.copy_(tensor.to(existing.data.dtype))
else:
module.wcscales = tensor
return True
def _patch_nunchaku_scales(
model: nn.Module,
safetensors_list: list[str],
) -> None:
"""Patch transformer module with Nunchaku scale tensors from safetensors weights.
For NVFP4 checkpoints, correctness depends on `wtscale` and attention
`wcscales`. The FSDP loader may skip some of these metadata tensors.
"""
if not safetensors_list:
return
if len(safetensors_list) != 1:
logger.warning(
"Nunchaku scale patch expects a single safetensors file, "
"but got %d files. Skipping.",
len(safetensors_list),
)
return
from nunchaku.models.linear import SVDQW4A4Linear # type: ignore[import]
state_dict = safetensors_load_file(safetensors_list[0])
if state_dict is None:
return
num_wtscale = 0
num_wcscales = 0
from ..nunchaku_linear import NunchakuSVDQLinearMethod
for name, module in model.named_modules():
wt = state_dict.get(f"{name}.wtscale")
if wt is not None:
if _patch_native_svdq_linear(module, wt, SVDQW4A4Linear):
num_wtscale += 1
elif _patch_sglang_svdq_linear(module, wt, NunchakuSVDQLinearMethod):
num_wtscale += 1
wc = state_dict.get(f"{name}.wcscales")
if wc is not None:
# Some modules may have wcscales as a direct attribute/Parameter.
existing = getattr(module, "wcscales", None)
if isinstance(existing, nn.Parameter):
with torch.no_grad():
existing.data.copy_(wc.to(existing.data.dtype))
num_wcscales += 1
elif existing is not None:
setattr(module, "wcscales", wc)
num_wcscales += 1
elif _patch_sglang_svdq_wcscales(module, wc, NunchakuSVDQLinearMethod):
num_wcscales += 1
if num_wtscale > 0:
logger.info("Patched wtscale for %d layers", num_wtscale)
if num_wcscales > 0:
logger.info("Patched wcscales for %d layers", num_wcscales)
@@ -0,0 +1,291 @@
# SPDX-License-Identifier: Apache-2.0
from typing import List, Optional
import torch
import torch.nn as nn
from torch.nn.parameter import Parameter
from sglang.multimodal_gen.runtime.layers.linear import LinearMethodBase
from sglang.multimodal_gen.runtime.models.utils import set_weight_attrs
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
try:
from nunchaku.ops.gemm import svdq_gemm_w4a4_cuda
from nunchaku.ops.gemv import awq_gemv_w4a16_cuda
from nunchaku.ops.quantize import svdq_quantize_w4a4_act_fuse_lora_cuda
except ImportError:
svdq_gemm_w4a4_cuda = None
awq_gemv_w4a16_cuda = None
svdq_quantize_w4a4_act_fuse_lora_cuda = None
class NunchakuSVDQLinearMethod(LinearMethodBase):
def __init__(
self,
precision: str = "int4",
rank: int = 32,
act_unsigned: bool = False,
):
self.precision = precision
self.rank = rank
self.act_unsigned = act_unsigned
if precision == "nvfp4":
self.group_size = 16
else:
self.group_size = 64
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
) -> None:
output_size_per_partition = sum(output_partition_sizes)
qweight = Parameter(
torch.empty(
output_size_per_partition,
input_size_per_partition // 2,
dtype=torch.int8,
),
requires_grad=False,
)
set_weight_attrs(qweight, {"input_dim": 1, "output_dim": 0})
num_groups = input_size_per_partition // self.group_size
if self.precision == "nvfp4":
scale_dtype = torch.float8_e4m3fn
else:
scale_dtype = params_dtype
wscales = Parameter(
torch.empty(num_groups, output_size_per_partition, dtype=scale_dtype),
requires_grad=False,
)
smooth_factor = Parameter(
torch.empty(input_size_per_partition, dtype=params_dtype),
requires_grad=False,
)
smooth_factor_orig = Parameter(
torch.empty(input_size_per_partition, dtype=params_dtype),
requires_grad=False,
)
proj_down = Parameter(
torch.empty(input_size_per_partition, self.rank, dtype=params_dtype),
requires_grad=False,
)
proj_up = Parameter(
torch.empty(output_size_per_partition, self.rank, dtype=params_dtype),
requires_grad=False,
)
if self.precision == "nvfp4":
wcscales = Parameter(
torch.empty(
output_size_per_partition,
dtype=params_dtype,
),
requires_grad=False,
)
wtscale = Parameter(
torch.empty(1, dtype=params_dtype),
requires_grad=False,
)
else:
wcscales = None
wtscale = None
layer.register_parameter("qweight", qweight)
layer.register_parameter("wscales", wscales)
layer.register_parameter("smooth_factor", smooth_factor)
layer.register_parameter("smooth_factor_orig", smooth_factor_orig)
layer.register_parameter("proj_down", proj_down)
layer.register_parameter("proj_up", proj_up)
if wcscales is not None:
layer.register_parameter("wcscales", wcscales)
if wtscale is not None:
layer.register_parameter("wtscale", wtscale)
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
layer.precision = self.precision
layer.rank = self.rank
layer.group_size = self.group_size
layer.act_unsigned = self.act_unsigned
weight_loader = extra_weight_attrs.get("weight_loader")
if weight_loader is not None:
set_weight_attrs(qweight, {"weight_loader": weight_loader})
set_weight_attrs(wscales, {"weight_loader": weight_loader})
set_weight_attrs(smooth_factor, {"weight_loader": weight_loader})
set_weight_attrs(smooth_factor_orig, {"weight_loader": weight_loader})
set_weight_attrs(proj_down, {"weight_loader": weight_loader})
set_weight_attrs(proj_up, {"weight_loader": weight_loader})
if wcscales is not None:
set_weight_attrs(wcscales, {"weight_loader": weight_loader})
if wtscale is not None:
set_weight_attrs(wtscale, {"weight_loader": weight_loader})
def process_weights_after_loading(self, layer: nn.Module) -> None:
layer.qweight = Parameter(layer.qweight.data, requires_grad=False)
layer.wscales = Parameter(layer.wscales.data, requires_grad=False)
layer.smooth_factor = Parameter(layer.smooth_factor.data, requires_grad=False)
layer.smooth_factor_orig = Parameter(
layer.smooth_factor_orig.data, requires_grad=False
)
layer.proj_down = Parameter(layer.proj_down.data, requires_grad=False)
layer.proj_up = Parameter(layer.proj_up.data, requires_grad=False)
if hasattr(layer, "wcscales") and layer.wcscales is not None:
layer.wcscales = Parameter(layer.wcscales.data, requires_grad=False)
if hasattr(layer, "wtscale") and layer.wtscale is not None:
layer.wtscale = Parameter(layer.wtscale.data, requires_grad=False)
alpha: float | None = None
wtscale = getattr(layer, "wtscale", None)
if wtscale is not None:
if isinstance(wtscale, Parameter):
wtscale = wtscale.data
if isinstance(wtscale, torch.Tensor):
alpha = float(wtscale.detach().cpu().item())
else:
alpha = float(wtscale)
layer._nunchaku_alpha = alpha
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
orig_shape = x.shape
x_2d = x.reshape(-1, orig_shape[-1])
quantized_x, ascales, lora_act_out = svdq_quantize_w4a4_act_fuse_lora_cuda(
x_2d,
lora_down=layer.proj_down,
smooth=layer.smooth_factor,
fp4=layer.precision == "nvfp4",
pad_size=256,
)
out_2d = torch.empty(
x_2d.shape[0],
layer.output_size_per_partition,
dtype=x_2d.dtype,
device=x_2d.device,
)
alpha: float | None = getattr(layer, "_nunchaku_alpha", None)
wcscales = getattr(layer, "wcscales", None)
svdq_gemm_w4a4_cuda(
act=quantized_x,
wgt=layer.qweight,
out=out_2d,
ascales=ascales,
wscales=layer.wscales,
lora_act_in=lora_act_out,
lora_up=layer.proj_up,
bias=bias,
fp4=layer.precision == "nvfp4",
alpha=alpha,
wcscales=wcscales,
act_unsigned=getattr(layer, "act_unsigned", False),
)
out = out_2d.reshape(*orig_shape[:-1], layer.output_size_per_partition)
return out
class NunchakuAWQLinearMethod(LinearMethodBase):
def __init__(self, group_size: int = 64):
self.group_size = group_size
self.pack_factor = 8
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
) -> None:
output_size_per_partition = sum(output_partition_sizes)
qweight = Parameter(
torch.empty(
output_size_per_partition // 4,
input_size_per_partition // 2,
dtype=torch.int32,
),
requires_grad=False,
)
set_weight_attrs(qweight, {"input_dim": 1, "output_dim": 0})
num_groups = input_size_per_partition // self.group_size
wscales = Parameter(
torch.empty(num_groups, output_size_per_partition, dtype=params_dtype),
requires_grad=False,
)
wzeros = Parameter(
torch.empty(num_groups, output_size_per_partition, dtype=params_dtype),
requires_grad=False,
)
layer.register_parameter("qweight", qweight)
layer.register_parameter("wscales", wscales)
layer.register_parameter("wzeros", wzeros)
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
layer.group_size = self.group_size
layer.pack_factor = self.pack_factor
weight_loader = extra_weight_attrs.get("weight_loader")
if weight_loader is not None:
set_weight_attrs(qweight, {"weight_loader": weight_loader})
set_weight_attrs(wscales, {"weight_loader": weight_loader})
set_weight_attrs(wzeros, {"weight_loader": weight_loader})
def process_weights_after_loading(self, layer: nn.Module) -> None:
layer.qweight = Parameter(layer.qweight.data, requires_grad=False)
layer.wscales = Parameter(layer.wscales.data, requires_grad=False)
layer.wzeros = Parameter(layer.wzeros.data, requires_grad=False)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
orig_shape = x.shape
x_2d = x.reshape(-1, orig_shape[-1])
in_features = layer.input_size_per_partition
out_features = layer.output_size_per_partition
out_2d = awq_gemv_w4a16_cuda(
in_feats=x_2d,
kernel=layer.qweight,
scaling_factors=layer.wscales,
zeros=layer.wzeros,
m=x_2d.shape[0],
n=out_features,
k=in_features,
group_size=layer.group_size,
)
if bias is not None:
view_shape = [1] * (out_2d.ndim - 1) + [-1]
out_2d.add_(bias.view(view_shape))
out = out_2d.reshape(*orig_shape[:-1], out_features)
return out
@@ -15,7 +15,7 @@ from sglang.multimodal_gen.runtime.distributed import (
get_tp_group, get_tp_group,
tensor_model_parallel_all_reduce, tensor_model_parallel_all_reduce,
) )
from sglang.multimodal_gen.runtime.layers.quantization.base_config import ( from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig, QuantizationConfig,
QuantizeMethodBase, QuantizeMethodBase,
method_has_implemented_embedding, method_has_implemented_embedding,
@@ -1,9 +1,16 @@
import inspect
import json
import logging
import os import os
from copy import deepcopy from copy import deepcopy
from typing import Any
import torch import torch
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
from sglang.multimodal_gen.runtime.layers.quantization.configs.nunchaku_config import (
_patch_nunchaku_scales,
)
from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import ( from sglang.multimodal_gen.runtime.loader.component_loaders.component_loader import (
ComponentLoader, ComponentLoader,
) )
@@ -16,8 +23,9 @@ from sglang.multimodal_gen.runtime.models.registry import ModelRegistry
from sglang.multimodal_gen.runtime.server_args import ServerArgs from sglang.multimodal_gen.runtime.server_args import ServerArgs
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import ( from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import (
get_diffusers_component_config, get_diffusers_component_config,
get_metadata_from_safetensors_file,
) )
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger from sglang.multimodal_gen.runtime.utils.logging_utils import get_log_level, init_logger
from sglang.multimodal_gen.utils import PRECISION_TO_TYPE from sglang.multimodal_gen.utils import PRECISION_TO_TYPE
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -29,6 +37,34 @@ class TransformerLoader(ComponentLoader):
component_names = ["transformer", "audio_dit", "video_dit"] component_names = ["transformer", "audio_dit", "video_dit"]
expected_library = "diffusers" expected_library = "diffusers"
def get_list_of_safetensors_to_load(
self, server_args: ServerArgs, component_model_path: str
) -> list[str]:
"""
get list of safetensors to load.
For some quantization framework, if --quantized-model-path is provided, load from this path instead of main model
"""
nunchaku_config = server_args.nunchaku_config
if nunchaku_config is not None and nunchaku_config.quantized_model_path:
# load from quantized_model_path if applicable
weights_path = nunchaku_config.quantized_model_path
logger.info("Using quantized model weights from: %s", weights_path)
if os.path.isfile(weights_path) and weights_path.endswith(".safetensors"):
safetensors_list = [weights_path]
else:
safetensors_list = _list_safetensors_files(weights_path)
else:
weights_path = component_model_path
safetensors_list = _list_safetensors_files(weights_path)
if not safetensors_list:
raise ValueError(f"No safetensors files found in {weights_path}")
return safetensors_list
def load_customized( def load_customized(
self, component_model_path: str, server_args: ServerArgs, component_name: str self, component_model_path: str, server_args: ServerArgs, component_name: str
): ):
@@ -57,46 +93,50 @@ class TransformerLoader(ComponentLoader):
model_cls, _ = ModelRegistry.resolve_model_cls(cls_name) model_cls, _ = ModelRegistry.resolve_model_cls(cls_name)
# Find all safetensors files nunchaku_config = server_args.nunchaku_config
safetensors_list = _list_safetensors_files(component_model_path)
if not safetensors_list:
raise ValueError(f"No safetensors files found in {component_model_path}")
# Check if we should use custom initialization weights if nunchaku_config is not None:
custom_weights_path = getattr( # respect dtype from checkpoint
server_args, "init_weights_from_safetensors", None # TODO: improve the condition
param_dtype = None
# check if the specified nunchaku quantized model path matches with the specified model path
original_dit_cls_name = json.loads(
get_metadata_from_safetensors_file(
nunchaku_config.quantized_model_path
).get("config")
)["_class_name"]
specified_dit_cls_name = str(model_cls.__name__)
if original_dit_cls_name != specified_dit_cls_name:
raise Exception(
f"Class name of DiT specified in nunchaku quantized model_path: {original_dit_cls_name} does not match that of specified DiT name: {specified_dit_cls_name}"
)
else:
param_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.dit_precision]
safetensors_list = self.get_list_of_safetensors_to_load(
server_args, component_model_path
) )
use_custom_weights = False
if use_custom_weights:
logger.info(
"Using custom initialization weights from: %s", custom_weights_path
)
assert (
custom_weights_path is not None
), "Custom initialization weights must be provided"
if os.path.isdir(custom_weights_path):
safetensors_list = _list_safetensors_files(custom_weights_path)
else:
assert custom_weights_path.endswith(
".safetensors"
), "Custom initialization weights must be a safetensors file"
safetensors_list = [custom_weights_path]
default_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.dit_precision]
logger.info( logger.info(
"Loading %s from %s safetensors files, default_dtype: %s", "Loading %s from %s safetensors files %s, param_dtype: %s",
cls_name, cls_name,
len(safetensors_list), len(safetensors_list),
default_dtype, f": {safetensors_list}" if get_log_level() == logging.DEBUG else "",
param_dtype,
) )
init_params: dict[str, Any] = {"config": dit_config, "hf_config": hf_config}
if (
nunchaku_config is not None
and "quant_config" in inspect.signature(model_cls.__init__).parameters
):
init_params["quant_config"] = nunchaku_config
# Load the model using FSDP loader # Load the model using FSDP loader
assert server_args.hsdp_shard_dim is not None
model = maybe_load_fsdp_model( model = maybe_load_fsdp_model(
model_cls=model_cls, model_cls=model_cls,
init_params={"config": dit_config, "hf_config": hf_config}, init_params=init_params,
weight_dir_list=safetensors_list, weight_dir_list=safetensors_list,
device=get_local_torch_device(), device=get_local_torch_device(),
hsdp_replicate_dim=server_args.hsdp_replicate_dim, hsdp_replicate_dim=server_args.hsdp_replicate_dim,
@@ -105,17 +145,22 @@ class TransformerLoader(ComponentLoader):
pin_cpu_memory=server_args.pin_cpu_memory, pin_cpu_memory=server_args.pin_cpu_memory,
fsdp_inference=server_args.use_fsdp_inference, fsdp_inference=server_args.use_fsdp_inference,
# TODO(will): make these configurable # TODO(will): make these configurable
param_dtype=torch.bfloat16, param_dtype=param_dtype,
reduce_dtype=torch.float32, reduce_dtype=torch.float32,
output_dtype=None, output_dtype=None,
strict=False, strict=False,
) )
if nunchaku_config is not None:
_patch_nunchaku_scales(model, safetensors_list)
total_params = sum(p.numel() for p in model.parameters()) total_params = sum(p.numel() for p in model.parameters())
logger.info("Loaded model with %.2fB parameters", total_params / 1e9) logger.info("Loaded model with %.2fB parameters", total_params / 1e9)
assert ( # considering the existent of mixed-precision models (e.g., nunchaku)
next(model.parameters()).dtype == default_dtype if next(model.parameters()).dtype != param_dtype:
), "Model dtype does not match default dtype" logger.warning(
f"Model dtype does not match expected param dtype, {next(model.parameters()).dtype} vs {param_dtype}"
)
return model return model
@@ -30,6 +30,7 @@ from sglang.multimodal_gen.runtime.loader.utils import (
from sglang.multimodal_gen.runtime.loader.weight_utils import ( from sglang.multimodal_gen.runtime.loader.weight_utils import (
safetensors_weights_iterator, safetensors_weights_iterator,
) )
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.logging_utils import init_logger
from sglang.multimodal_gen.utils import set_mixed_precision_policy from sglang.multimodal_gen.utils import set_mixed_precision_policy
@@ -40,7 +41,12 @@ def _make_param_like(
actual_param: torch.nn.Parameter, tensor: torch.Tensor actual_param: torch.nn.Parameter, tensor: torch.Tensor
) -> torch.nn.Parameter: ) -> torch.nn.Parameter:
cls = actual_param.__class__ cls = actual_param.__class__
new_param = cls.__new__(cls, tensor) # nn.Parameter defaults to requires_grad=True, which is illegal for non-floating/complex dtypes (e.g., int8/FP8
# quantized weights).
try:
new_param = cls.__new__(cls, tensor, requires_grad=False)
except TypeError:
new_param = cls.__new__(cls, tensor)
new_param.__dict__.update(actual_param.__dict__) new_param.__dict__.update(actual_param.__dict__)
new_param.requires_grad = False new_param.requires_grad = False
return new_param return new_param
@@ -74,26 +80,25 @@ def maybe_load_fsdp_model(
""" """
# NOTE(will): cast_forward_inputs=True shouldn't be needed as we are # NOTE(will): cast_forward_inputs=True shouldn't be needed as we are
# manually casting the inputs to the model # manually casting the inputs to the model
default_torch_dtype = param_dtype if param_dtype else torch.bfloat16
mp_policy = MixedPrecisionPolicy( mp_policy = MixedPrecisionPolicy(
param_dtype, reduce_dtype, output_dtype, cast_forward_inputs=False default_torch_dtype, reduce_dtype, output_dtype, cast_forward_inputs=False
) )
set_mixed_precision_policy( set_mixed_precision_policy(
param_dtype=param_dtype, param_dtype=default_torch_dtype,
reduce_dtype=reduce_dtype, reduce_dtype=reduce_dtype,
output_dtype=output_dtype, output_dtype=output_dtype,
mp_policy=mp_policy, mp_policy=mp_policy,
) )
with set_default_torch_dtype(param_dtype), torch.device("meta"): with set_default_torch_dtype(default_torch_dtype), torch.device("meta"):
model = model_cls(**init_params) model = model_cls(**init_params)
# Check if we should use FSDP # Check if we should use FSDP
use_fsdp = fsdp_inference use_fsdp = fsdp_inference
# Disable FSDP for MPS as it's not compatible # Disable FSDP for MPS as it's not compatible
from sglang.multimodal_gen.runtime.platforms import current_platform
if current_platform.is_mps(): if current_platform.is_mps():
use_fsdp = False use_fsdp = False
logger.info("Disabling FSDP for MPS platform as it's not compatible") logger.info("Disabling FSDP for MPS platform as it's not compatible")
@@ -191,7 +196,7 @@ def shard_model(
# TODO(will): don't reshard after forward for the last layer to save on the # TODO(will): don't reshard after forward for the last layer to save on the
# all-gather that will immediately happen Shard the model with FSDP, # all-gather that will immediately happen Shard the model with FSDP,
for n, m in reversed(list(model.named_modules())): for n, m in reversed(list(model.named_modules())):
if any([shard_condition(n, m) for shard_condition in fsdp_shard_conditions]): if any([shard_condition(n, m) for shard_condition in fsdp_shard_conditions]): # type: ignore
fully_shard(m, **fsdp_kwargs) fully_shard(m, **fsdp_kwargs)
num_layers_sharded += 1 num_layers_sharded += 1
@@ -209,7 +214,7 @@ def load_model_from_full_model_state_dict(
model: FSDPModule | torch.nn.Module, model: FSDPModule | torch.nn.Module,
full_sd_iterator: Generator[tuple[str, torch.Tensor], None, None], full_sd_iterator: Generator[tuple[str, torch.Tensor], None, None],
device: torch.device, device: torch.device,
param_dtype: torch.dtype, param_dtype: torch.dtype | None,
strict: bool = False, strict: bool = False,
cpu_offload: bool = False, cpu_offload: bool = False,
param_names_mapping: Callable[[str], tuple[str, Any, Any]] | None = None, param_names_mapping: Callable[[str], tuple[str, Any, Any]] | None = None,
@@ -221,7 +226,7 @@ def load_model_from_full_model_state_dict(
model (Union[FSDPModule, torch.nn.Module]): Model to generate fully qualified names for cpu_state_dict model (Union[FSDPModule, torch.nn.Module]): Model to generate fully qualified names for cpu_state_dict
full_sd_iterator (Generator): an iterator yielding (param_name, tensor) pairs full_sd_iterator (Generator): an iterator yielding (param_name, tensor) pairs
device (torch.device): device used to move full state dict tensors device (torch.device): device used to move full state dict tensors
param_dtype (torch.dtype): dtype used to move full state dict tensors param_dtype (torch.dtype): dtype used to move full state dict tensors. If none, respect original dtype from checkpoint
strict (bool): flag to check if to load the model in strict mode strict (bool): flag to check if to load the model in strict mode
cpu_offload (bool): flag to check if FSDP offload is enabled cpu_offload (bool): flag to check if FSDP offload is enabled
param_names_mapping (Optional[Callable[[str], str]]): a function that maps full param name to sharded param name param_names_mapping (Optional[Callable[[str], str]]): a function that maps full param name to sharded param name
@@ -245,6 +250,9 @@ def load_model_from_full_model_state_dict(
# sort parameter names to ensure all ranks process parameters in the same order # sort parameter names to ensure all ranks process parameters in the same order
sorted_param_names = sorted(custom_param_sd.keys()) sorted_param_names = sorted(custom_param_sd.keys())
requires_grad = False
# shard from loaded state_dict, custom_param_sd -> sharded_sd
for target_param_name in sorted_param_names: for target_param_name in sorted_param_names:
full_tensor = custom_param_sd[target_param_name] full_tensor = custom_param_sd[target_param_name]
meta_sharded_param = meta_sd.get(target_param_name) meta_sharded_param = meta_sd.get(target_param_name)
@@ -259,8 +267,10 @@ def load_model_from_full_model_state_dict(
f"Parameter '{target_param_name}' from checkpoint not found in model; skipping. This is expected for optional parameters." f"Parameter '{target_param_name}' from checkpoint not found in model; skipping. This is expected for optional parameters."
) )
continue continue
target_dtype = param_dtype if param_dtype else full_tensor.dtype
if not hasattr(meta_sharded_param, "device_mesh"): if not hasattr(meta_sharded_param, "device_mesh"):
full_tensor = full_tensor.to(device=device, dtype=param_dtype) full_tensor = full_tensor.to(device=device, dtype=target_dtype)
actual_param = param_dict.get(target_param_name) actual_param = param_dict.get(target_param_name)
weight_loader = ( weight_loader = (
getattr(actual_param, "weight_loader", None) getattr(actual_param, "weight_loader", None)
@@ -270,12 +280,20 @@ def load_model_from_full_model_state_dict(
if weight_loader is not None: if weight_loader is not None:
assert actual_param is not None assert actual_param is not None
sharded_tensor = torch.empty_like( sharded_tensor = torch.empty_like(
meta_sharded_param, device=device, dtype=param_dtype meta_sharded_param, device=device, dtype=target_dtype
) )
# Preserve requires_grad flag to avoid errors with non-floating dtypes
requires_grad = getattr(meta_sharded_param, "requires_grad", False)
temp_param = _make_param_like(actual_param, sharded_tensor) temp_param = _make_param_like(actual_param, sharded_tensor)
if not (
sharded_tensor.is_floating_point() or sharded_tensor.is_complex()
):
requires_grad = False
temp_param.requires_grad = requires_grad
weight_loader(temp_param, full_tensor) weight_loader(temp_param, full_tensor)
sharded_tensor = temp_param.data sharded_tensor = temp_param.data
else: else:
# In cases where parts of the model aren't sharded, some parameters will be plain tensors
sharded_tensor = full_tensor sharded_tensor = full_tensor
# Important: `cpu_offload` is intended for FSDP-managed parameter movement. # Important: `cpu_offload` is intended for FSDP-managed parameter movement.
@@ -290,7 +308,7 @@ def load_model_from_full_model_state_dict(
if cpu_offload and not is_fsdp_model: if cpu_offload and not is_fsdp_model:
sharded_tensor = sharded_tensor.cpu() sharded_tensor = sharded_tensor.cpu()
else: else:
full_tensor = full_tensor.to(device=device, dtype=param_dtype) full_tensor = full_tensor.to(device=device, dtype=target_dtype)
sharded_tensor = distribute_tensor( sharded_tensor = distribute_tensor(
full_tensor, full_tensor,
meta_sharded_param.device_mesh, meta_sharded_param.device_mesh,
@@ -298,16 +316,27 @@ def load_model_from_full_model_state_dict(
) )
if cpu_offload: if cpu_offload:
sharded_tensor = sharded_tensor.to("cpu") sharded_tensor = sharded_tensor.to("cpu")
sharded_sd[target_param_name] = nn.Parameter(sharded_tensor)
requires_grad = False
sharded_sd[target_param_name] = nn.Parameter(
sharded_tensor, requires_grad=requires_grad
)
model.reverse_param_names_mapping = reverse_param_names_mapping model.reverse_param_names_mapping = reverse_param_names_mapping
# parameters in nn.Module that doesn't exist in safetensor files
unused_keys = set(meta_sd.keys()) - set(sharded_sd.keys()) unused_keys = set(meta_sd.keys()) - set(sharded_sd.keys())
if unused_keys: if unused_keys:
logger.warning("Found unloaded parameters in meta state dict: %s", unused_keys) logger.warning("Found unloaded parameters in meta state dict: %s", unused_keys)
# List of allowed parameter name patterns # for nunchaku
ALLOWED_NEW_PARAM_PATTERNS = ["gate_compress"] # Can be extended as needed ALLOWED_NEW_PARAM_PATTERNS = [
"gate_compress",
"wcscales",
"wtscale",
"bias",
]
for new_param_name in unused_keys: for new_param_name in unused_keys:
# check unallowed missing params
if not any(pattern in new_param_name for pattern in ALLOWED_NEW_PARAM_PATTERNS): if not any(pattern in new_param_name for pattern in ALLOWED_NEW_PARAM_PATTERNS):
logger.error( logger.error(
"Unsupported new parameter: %s. Allowed patterns: %s", "Unsupported new parameter: %s. Allowed patterns: %s",
@@ -318,17 +347,22 @@ def load_model_from_full_model_state_dict(
f"New parameter '{new_param_name}' is not supported. " f"New parameter '{new_param_name}' is not supported. "
f"Currently only parameters containing {ALLOWED_NEW_PARAM_PATTERNS} are allowed." f"Currently only parameters containing {ALLOWED_NEW_PARAM_PATTERNS} are allowed."
) )
meta_sharded_param = meta_sd.get(new_param_name) meta_sharded_param = meta_sd.get(new_param_name)
if "wcscales" in new_param_name or "wtscale" in new_param_name:
init_like = torch.ones_like
else:
init_like = torch.zeros_like
if not hasattr(meta_sharded_param, "device_mesh"): if not hasattr(meta_sharded_param, "device_mesh"):
# Initialize with zeros sharded_tensor = init_like(
sharded_tensor = torch.zeros_like(
meta_sharded_param, device=device, dtype=param_dtype meta_sharded_param, device=device, dtype=param_dtype
) )
if cpu_offload and not is_fsdp_model: if cpu_offload and not is_fsdp_model:
sharded_tensor = sharded_tensor.cpu() sharded_tensor = sharded_tensor.cpu()
else: else:
# Initialize with zeros and distribute full_tensor = init_like(
full_tensor = torch.zeros_like(
meta_sharded_param, device=device, dtype=param_dtype meta_sharded_param, device=device, dtype=param_dtype
) )
sharded_tensor = distribute_tensor( sharded_tensor = distribute_tensor(
@@ -86,6 +86,8 @@ def hf_to_custom_state_dict(
target_param_name, merge_index, num_params_to_merge = param_names_mapping( target_param_name, merge_index, num_params_to_merge = param_names_mapping(
source_param_name source_param_name
) )
if target_param_name == "" or target_param_name is None: # type: ignore[comparison-overlap]
continue
reverse_param_names_mapping[target_param_name] = ( reverse_param_names_mapping[target_param_name] = (
source_param_name, source_param_name,
merge_index, merge_index,
@@ -352,6 +352,7 @@ OOM detected. Possible solutions:
2. Enable SP and/or TP 2. Enable SP and/or TP
3. Opt for a sparse-attention backend 3. Opt for a sparse-attention backend
4. Enable FSDP by `--use-fsdp-inference` (in a multi-GPU setup) 4. Enable FSDP by `--use-fsdp-inference` (in a multi-GPU setup)
5. Enable quantization (e.g. nunchaku)
Or, open an issue on GitHub https://github.com/sgl-project/sglang/issues/new/choose Or, open an issue on GitHub https://github.com/sgl-project/sglang/issues/new/choose
""" """
@@ -5,10 +5,12 @@
import functools import functools
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
import diffusers
import numpy as np import numpy as np
import torch import torch
import torch.nn as nn import torch.nn as nn
import torch.nn.functional as F import torch.nn.functional as F
from diffusers.models.attention import FeedForward
from diffusers.models.embeddings import TimestepEmbedding, Timesteps from diffusers.models.embeddings import TimestepEmbedding, Timesteps
from diffusers.models.modeling_outputs import Transformer2DModelOutput from diffusers.models.modeling_outputs import Transformer2DModelOutput
from diffusers.models.normalization import AdaLayerNormContinuous from diffusers.models.normalization import AdaLayerNormContinuous
@@ -26,8 +28,16 @@ from sglang.multimodal_gen.runtime.layers.layernorm import (
) )
from sglang.multimodal_gen.runtime.layers.linear import ( from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear, ColumnParallelLinear,
MergedColumnParallelLinear,
RowParallelLinear, RowParallelLinear,
) )
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.nunchaku_config import (
NunchakuConfig,
is_nunchaku_available,
)
from sglang.multimodal_gen.runtime.layers.rotary_embedding import ( from sglang.multimodal_gen.runtime.layers.rotary_embedding import (
apply_flashinfer_rope_qk_inplace, apply_flashinfer_rope_qk_inplace,
) )
@@ -45,19 +55,36 @@ from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__) # pylint: disable=invalid-name logger = init_logger(__name__) # pylint: disable=invalid-name
_is_cuda = current_platform.is_cuda() _is_cuda = current_platform.is_cuda()
try:
from nunchaku.models.attention import NunchakuFeedForward # type: ignore[import]
except Exception:
NunchakuFeedForward = None
def _get_qkv_projections( def _get_qkv_projections(
attn: "QwenImageCrossAttention", hidden_states, encoder_hidden_states=None attn: "QwenImageCrossAttention", hidden_states, encoder_hidden_states=None
): ):
img_query, _ = attn.to_q(hidden_states) if attn.use_fused_qkv:
img_key, _ = attn.to_k(hidden_states) img_qkv, _ = attn.to_qkv(hidden_states)
img_value, _ = attn.to_v(hidden_states) img_query, img_key, img_value = [
x.contiguous() for x in img_qkv.chunk(3, dim=-1)
]
else:
img_query, _ = attn.to_q(hidden_states)
img_key, _ = attn.to_k(hidden_states)
img_value, _ = attn.to_v(hidden_states)
txt_query = txt_key = txt_value = None txt_query = txt_key = txt_value = None
if encoder_hidden_states is not None and attn.added_kv_proj_dim is not None: if encoder_hidden_states is not None and attn.added_kv_proj_dim is not None:
txt_query, _ = attn.add_q_proj(encoder_hidden_states) if attn.use_fused_added_qkv:
txt_key, _ = attn.add_k_proj(encoder_hidden_states) txt_qkv, _ = attn.to_added_qkv(encoder_hidden_states)
txt_value, _ = attn.add_v_proj(encoder_hidden_states) txt_query, txt_key, txt_value = [
x.contiguous() for x in txt_qkv.chunk(3, dim=-1)
]
else:
txt_query, _ = attn.add_q_proj(encoder_hidden_states)
txt_key, _ = attn.add_k_proj(encoder_hidden_states)
txt_value, _ = attn.add_v_proj(encoder_hidden_states)
return img_query, img_key, img_value, txt_query, txt_key, txt_value return img_query, img_key, img_value, txt_query, txt_key, txt_value
@@ -71,14 +98,27 @@ class GELU(nn.Module):
dim_out (`int`): The number of channels in the output. dim_out (`int`): The number of channels in the output.
approximate (`str`, *optional*, defaults to `"none"`): If `"tanh"`, use tanh approximation. approximate (`str`, *optional*, defaults to `"none"`): If `"tanh"`, use tanh approximation.
bias (`bool`, defaults to True): Whether to use a bias in the linear layer. bias (`bool`, defaults to True): Whether to use a bias in the linear layer.
quant_config: Quantization configure.
prefix: The name of the layer in the state dict.
""" """
def __init__( def __init__(
self, dim_in: int, dim_out: int, approximate: str = "none", bias: bool = True self,
dim_in: int,
dim_out: int,
approximate: str = "none",
bias: bool = True,
quant_config=None,
prefix: str = "",
): ):
super().__init__() super().__init__()
self.proj = ColumnParallelLinear( self.proj = ColumnParallelLinear(
dim_in, dim_out, bias=bias, gather_output=False dim_in,
dim_out,
bias=bias,
gather_output=False,
quant_config=quant_config,
prefix=f"{prefix}.proj" if prefix else "",
) )
self.approximate = approximate self.approximate = approximate
@@ -95,9 +135,7 @@ class FeedForward(nn.Module):
dim (`int`): The number of channels in the input. dim (`int`): The number of channels in the input.
dim_out (`int`, *optional*): The number of channels in the output. If not given, defaults to `dim`. dim_out (`int`, *optional*): The number of channels in the output. If not given, defaults to `dim`.
mult (`int`, *optional*, defaults to 4): The multiplier to use for the hidden dimension. mult (`int`, *optional*, defaults to 4): The multiplier to use for the hidden dimension.
dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use.
activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward.
final_dropout (`bool` *optional*, defaults to False): Apply a final dropout.
bias (`bool`, defaults to True): Whether to use a bias in the linear layer. bias (`bool`, defaults to True): Whether to use a bias in the linear layer.
""" """
@@ -194,14 +232,6 @@ class QwenEmbedRope(nn.Module):
dim=1, dim=1,
) )
# self.rope = NDRotaryEmbedding(
# rope_dim_list=axes_dim,
# rope_theta=theta,
# use_real=False,
# repeat_interleave_real=False,
# dtype=torch.float32 if current_platform.is_mps() or current_platform.is_musa() else torch.float64,
# )
# DO NOT USING REGISTER BUFFER HERE, IT WILL CAUSE COMPLEX NUMBERS LOSE ITS IMAGINARY PART # DO NOT USING REGISTER BUFFER HERE, IT WILL CAUSE COMPLEX NUMBERS LOSE ITS IMAGINARY PART
self.scale_rope = scale_rope self.scale_rope = scale_rope
@@ -546,6 +576,8 @@ class QwenImageCrossAttention(nn.Module):
context_pre_only: bool = False, context_pre_only: bool = False,
parallel_attention=False, parallel_attention=False,
out_dim: int = None, out_dim: int = None,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None: ) -> None:
assert dim % num_heads == 0 assert dim % num_heads == 0
super().__init__() super().__init__()
@@ -557,38 +589,92 @@ class QwenImageCrossAttention(nn.Module):
self.eps = eps self.eps = eps
self.parallel_attention = parallel_attention self.parallel_attention = parallel_attention
self.added_kv_proj_dim = added_kv_proj_dim self.added_kv_proj_dim = added_kv_proj_dim
self.prefix = prefix
self.use_fused_qkv = isinstance(quant_config, NunchakuConfig)
# Use separate Q/K/V projections
self.inner_dim = out_dim if out_dim is not None else head_dim * num_heads self.inner_dim = out_dim if out_dim is not None else head_dim * num_heads
self.inner_kv_dim = self.inner_dim self.inner_kv_dim = self.inner_dim
self.to_q = ColumnParallelLinear(
dim, self.inner_dim, bias=True, gather_output=True if self.use_fused_qkv:
) # Use fused QKV projection for nunchaku quantization
self.to_k = ColumnParallelLinear( self.to_qkv = MergedColumnParallelLinear(
dim, self.inner_dim, bias=True, gather_output=True dim,
) [self.inner_dim] * 3,
self.to_v = ColumnParallelLinear( bias=True,
dim, self.inner_dim, bias=True, gather_output=True quant_config=quant_config,
) prefix=f"{prefix}.to_qkv",
)
else:
# Use separate Q/K/V projections for non-quantized models
self.to_q = ColumnParallelLinear(
dim,
self.inner_dim,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.to_q",
)
self.to_k = ColumnParallelLinear(
dim,
self.inner_dim,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.to_k",
)
self.to_v = ColumnParallelLinear(
dim,
self.inner_dim,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.to_v",
)
if self.qk_norm: if self.qk_norm:
self.norm_q = RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity() self.norm_q = RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity() self.norm_k = RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity()
if added_kv_proj_dim is not None: if added_kv_proj_dim is not None:
self.add_q_proj = ColumnParallelLinear( self.use_fused_added_qkv = isinstance(quant_config, NunchakuConfig)
added_kv_proj_dim, self.inner_dim, bias=True, gather_output=True if self.use_fused_added_qkv:
) self.to_added_qkv = MergedColumnParallelLinear(
self.add_k_proj = ColumnParallelLinear( added_kv_proj_dim,
added_kv_proj_dim, self.inner_dim, bias=True, gather_output=True [self.inner_dim] * 3,
) bias=True,
self.add_v_proj = ColumnParallelLinear( quant_config=quant_config,
added_kv_proj_dim, self.inner_dim, bias=True, gather_output=True prefix=f"{prefix}.to_added_qkv",
) )
else:
# Use separate Q/K/V projections for non-quantized models
self.add_q_proj = ColumnParallelLinear(
added_kv_proj_dim,
self.inner_dim,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.add_q_proj",
)
self.add_k_proj = ColumnParallelLinear(
added_kv_proj_dim,
self.inner_dim,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.add_k_proj",
)
self.add_v_proj = ColumnParallelLinear(
added_kv_proj_dim,
self.inner_dim,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.add_v_proj",
)
if context_pre_only is not None and not context_pre_only: if context_pre_only is not None and not context_pre_only:
self.to_add_out = ColumnParallelLinear( self.to_add_out = ColumnParallelLinear(
self.inner_dim, self.dim, bias=out_bias, gather_output=True self.inner_dim,
self.dim,
bias=out_bias,
gather_output=True,
quant_config=quant_config,
prefix=f"{prefix}.to_add_out",
) )
else: else:
self.to_add_out = None self.to_add_out = None
@@ -597,7 +683,12 @@ class QwenImageCrossAttention(nn.Module):
self.to_out = nn.ModuleList([]) self.to_out = nn.ModuleList([])
self.to_out.append( self.to_out.append(
ColumnParallelLinear( ColumnParallelLinear(
self.inner_dim, self.dim, bias=out_bias, gather_output=True self.inner_dim,
self.dim,
bias=out_bias,
gather_output=True,
quant_config=quant_config,
prefix=f"{prefix}.to_out.0",
) )
) )
else: else:
@@ -719,20 +810,29 @@ class QwenImageTransformerBlock(nn.Module):
attention_head_dim: int, attention_head_dim: int,
qk_norm: str = "rms_norm", qk_norm: str = "rms_norm",
eps: float = 1e-6, eps: float = 1e-6,
quant_config: Optional[QuantizationConfig] | NunchakuConfig = None,
prefix: str = "",
zero_cond_t: bool = False, zero_cond_t: bool = False,
): ):
super().__init__() super().__init__()
self.prefix = prefix
self.dim = dim self.dim = dim
self.num_attention_heads = num_attention_heads self.num_attention_heads = num_attention_heads
self.attention_head_dim = attention_head_dim self.attention_head_dim = attention_head_dim
self.quant_config = quant_config
self.zero_cond_t = zero_cond_t self.zero_cond_t = zero_cond_t
# Image processing modules # Image processing modules
self.img_mod = nn.Sequential( self.img_mod = nn.Sequential(
nn.SiLU(), nn.SiLU(),
ColumnParallelLinear( ColumnParallelLinear(
dim, 6 * dim, bias=True, gather_output=True dim,
6 * dim,
bias=True,
gather_output=True,
quant_config=quant_config,
prefix=f"{prefix}.img_mod",
), # For scale, shift, gate for norm1 and norm2 ), # For scale, shift, gate for norm1 and norm2
) )
self.img_norm1 = LayerNormScaleShift( self.img_norm1 = LayerNormScaleShift(
@@ -745,19 +845,23 @@ class QwenImageTransformerBlock(nn.Module):
added_kv_proj_dim=dim, added_kv_proj_dim=dim,
context_pre_only=False, context_pre_only=False,
head_dim=attention_head_dim, head_dim=attention_head_dim,
quant_config=quant_config,
prefix=f"{prefix}.attn",
) )
self.img_norm2 = ScaleResidualLayerNormScaleShift( self.img_norm2 = ScaleResidualLayerNormScaleShift(
hidden_size=dim, eps=eps, elementwise_affine=False dim, eps=eps, elementwise_affine=False
)
self.img_mlp = FeedForward(
dim=dim, dim_out=dim, activation_fn="gelu-approximate"
) )
# Text processing modules # Text processing modules
self.txt_mod = nn.Sequential( self.txt_mod = nn.Sequential(
nn.SiLU(), nn.SiLU(),
ColumnParallelLinear( ColumnParallelLinear(
dim, 6 * dim, bias=True, gather_output=True dim,
6 * dim,
bias=True,
gather_output=True,
quant_config=quant_config,
prefix=f"{prefix}.txt_mod",
), # For scale, shift, gate for norm1 and norm2 ), # For scale, shift, gate for norm1 and norm2
) )
self.txt_norm1 = LayerNormScaleShift( self.txt_norm1 = LayerNormScaleShift(
@@ -767,12 +871,38 @@ class QwenImageTransformerBlock(nn.Module):
self.txt_norm2 = ScaleResidualLayerNormScaleShift( self.txt_norm2 = ScaleResidualLayerNormScaleShift(
hidden_size=dim, eps=eps, elementwise_affine=False hidden_size=dim, eps=eps, elementwise_affine=False
) )
self.txt_mlp = FeedForward(
dim=dim, dim_out=dim, activation_fn="gelu-approximate"
)
# Utils # Utils
self.fuse_mul_add = MulAdd() self.fuse_mul_add = MulAdd()
nunchaku_enabled = (
quant_config is not None
and hasattr(quant_config, "get_name")
and quant_config.get_name() == "svdquant"
and is_nunchaku_available()
)
ff_class = (
diffusers.models.attention.FeedForward if nunchaku_enabled else FeedForward
)
self.img_mlp = ff_class(
dim=dim,
dim_out=dim,
activation_fn="gelu-approximate",
)
self.txt_mlp = ff_class(
dim=dim,
dim_out=dim,
activation_fn="gelu-approximate",
)
if nunchaku_enabled:
nunchaku_kwargs = {
"precision": quant_config.precision,
"rank": quant_config.rank,
"act_unsigned": quant_config.act_unsigned,
}
self.img_mlp = NunchakuFeedForward(self.img_mlp, **nunchaku_kwargs)
self.txt_mlp = NunchakuFeedForward(self.txt_mlp, **nunchaku_kwargs)
def _modulate( def _modulate(
self, self,
x: torch.Tensor, x: torch.Tensor,
@@ -880,6 +1010,24 @@ class QwenImageTransformerBlock(nn.Module):
img_mod_params = self.img_mod[1](temb_img_silu)[0] # [B, 6*dim] img_mod_params = self.img_mod[1](temb_img_silu)[0] # [B, 6*dim]
txt_mod_params = self.txt_mod[1](temb_txt_silu)[0] # [B, 6*dim] txt_mod_params = self.txt_mod[1](temb_txt_silu)[0] # [B, 6*dim]
if (
self.quant_config is not None
and hasattr(self.quant_config, "get_name")
and self.quant_config.get_name() == "svdquant"
):
# When NOT using nunchaku, reshape mod_params from [B, 6*dim] to [B, dim*6]
# When using nunchaku (svdquant), keep original format
img_mod_params = (
img_mod_params.view(img_mod_params.shape[0], -1, 6)
.transpose(1, 2)
.reshape(img_mod_params.shape[0], -1)
)
txt_mod_params = (
txt_mod_params.view(txt_mod_params.shape[0], -1, 6)
.transpose(1, 2)
.reshape(txt_mod_params.shape[0], -1)
)
# Split modulation parameters for norm1 and norm2 # Split modulation parameters for norm1 and norm2
img_mod1, img_mod2 = img_mod_params.chunk(2, dim=-1) # Each [B, 3*dim] img_mod1, img_mod2 = img_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
txt_mod1, txt_mod2 = txt_mod_params.chunk(2, dim=-1) # Each [B, 3*dim] txt_mod1, txt_mod2 = txt_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
@@ -923,8 +1071,11 @@ class QwenImageTransformerBlock(nn.Module):
gate_x=img_gate1, gate_x=img_gate1,
residual_x=hidden_states, residual_x=hidden_states,
) )
img_mlp_output = self.img_mlp(img_modulated2) img_mlp_output = self.img_mlp(img_modulated2)[0]
hidden_states = self.fuse_mul_add(img_mlp_output[0], img_gate2, hidden_states)
if img_mlp_output.dim() == 2:
img_mlp_output = img_mlp_output.unsqueeze(0)
hidden_states = self.fuse_mul_add(img_mlp_output, img_gate2, hidden_states)
# Process text stream - norm2 + MLP # Process text stream - norm2 + MLP
txt_shift2, txt_scale2, txt_gate2_raw = txt_mod2.chunk(3, dim=-1) txt_shift2, txt_scale2, txt_gate2_raw = txt_mod2.chunk(3, dim=-1)
@@ -936,9 +1087,12 @@ class QwenImageTransformerBlock(nn.Module):
scale=txt_scale2, scale=txt_scale2,
) )
txt_gate2 = txt_gate2_raw.unsqueeze(1) txt_gate2 = txt_gate2_raw.unsqueeze(1)
txt_mlp_output = self.txt_mlp(txt_modulated2) txt_mlp_output = self.txt_mlp(txt_modulated2)[0]
if txt_mlp_output.dim() == 2:
txt_mlp_output = txt_mlp_output.unsqueeze(0)
encoder_hidden_states = self.fuse_mul_add( encoder_hidden_states = self.fuse_mul_add(
txt_mlp_output[0], txt_gate2, encoder_hidden_states txt_mlp_output, txt_gate2, encoder_hidden_states
) )
# Clip to prevent overflow for fp16 # Clip to prevent overflow for fp16
@@ -973,6 +1127,7 @@ class QwenImageTransformer2DModel(CachableDiT, OffloadableDiTMixin):
self, self,
config: QwenImageDitConfig, config: QwenImageDitConfig,
hf_config: dict[str, Any], hf_config: dict[str, Any],
quant_config: Optional[QuantizationConfig] = None,
): ):
super().__init__(config=config, hf_config=hf_config) super().__init__(config=config, hf_config=hf_config)
patch_size = config.arch_config.patch_size patch_size = config.arch_config.patch_size
@@ -1019,9 +1174,11 @@ class QwenImageTransformer2DModel(CachableDiT, OffloadableDiTMixin):
dim=self.inner_dim, dim=self.inner_dim,
num_attention_heads=num_attention_heads, num_attention_heads=num_attention_heads,
attention_head_dim=attention_head_dim, attention_head_dim=attention_head_dim,
quant_config=quant_config,
prefix=f"transformer_blocks.{layer_idx}",
zero_cond_t=self.zero_cond_t, zero_cond_t=self.zero_cond_t,
) )
for _ in range(num_layers) for layer_idx in range(num_layers)
] ]
) )
@@ -74,8 +74,11 @@ class ImageEncodingStage(PipelineStage):
self.move_to_device("cpu") self.move_to_device("cpu")
def move_to_device(self, device): def move_to_device(self, device):
if self.server_args.use_fsdp_inference:
return
fields = [ fields = [
"image_processor", "image_processor",
"image_encoder",
] ]
for field in fields: for field in fields:
processor = getattr(self, field, None) processor = getattr(self, field, None)
@@ -21,6 +21,10 @@ import yaml
from sglang.multimodal_gen import envs from sglang.multimodal_gen import envs
from sglang.multimodal_gen.configs.pipeline_configs.base import PipelineConfig from sglang.multimodal_gen.configs.pipeline_configs.base import PipelineConfig
from sglang.multimodal_gen.configs.quantization import NunchakuSVDQuantArgs
from sglang.multimodal_gen.runtime.layers.quantization.configs.nunchaku_config import (
NunchakuConfig,
)
from sglang.multimodal_gen.runtime.platforms import ( from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum, AttentionBackendEnum,
current_platform, current_platform,
@@ -309,6 +313,11 @@ class ServerArgs:
disable_autocast: bool | None = None disable_autocast: bool | None = None
# Quantization / Nunchaku SVDQuant configuration
nunchaku_config: NunchakuSVDQuantArgs | NunchakuConfig | None = field(
default_factory=NunchakuSVDQuantArgs, repr=False
)
# Master port for distributed inference # Master port for distributed inference
# TODO: do not hard code # TODO: do not hard code
master_port: int | None = None master_port: int | None = None
@@ -362,6 +371,23 @@ class ServerArgs:
""" """
return self.host is None or self.port is None return self.host is None or self.port is None
def adjust_quant_config(self):
"""validate and adjust"""
# nunchaku
ncfg = self.nunchaku_config
ncfg.validate()
if not ncfg.enable_svdquant or not ncfg.quantized_model_path:
# if nunchaku is not applied
self.nunchaku_config = None
else:
self.nunchaku_config = NunchakuConfig(
precision=self.nunchaku_config.quantization_precision,
rank=self.nunchaku_config.quantization_rank,
act_unsigned=self.nunchaku_config.quantization_act_unsigned,
quantized_model_path=self.nunchaku_config.quantized_model_path,
)
def adjust_offload(self): def adjust_offload(self):
if self.pipeline_config.task_type.is_image_gen(): if self.pipeline_config.task_type.is_image_gen():
logger.info( logger.info(
@@ -429,6 +455,7 @@ class ServerArgs:
configure_logger(server_args=self) configure_logger(server_args=self)
self.adjust_offload() self.adjust_offload()
self.adjust_quant_config()
if self.attention_backend in ["fa3", "fa4"]: if self.attention_backend in ["fa3", "fa4"]:
self.attention_backend = "fa" self.attention_backend = "fa"
@@ -684,6 +711,9 @@ class ServerArgs:
help="Disable autocast for denoising loop and vae decoding in pipeline sampling", help="Disable autocast for denoising loop and vae decoding in pipeline sampling",
) )
# Nunchaku SVDQuant quantization parameters
NunchakuSVDQuantArgs.add_cli_args(parser)
# Master port for distributed inference # Master port for distributed inference
parser.add_argument( parser.add_argument(
"--master-port", "--master-port",
@@ -846,6 +876,9 @@ class ServerArgs:
pipeline_config = PipelineConfig.from_kwargs(kwargs) pipeline_config = PipelineConfig.from_kwargs(kwargs)
logger.debug(f"Using PipelineConfig: {type(pipeline_config)}") logger.debug(f"Using PipelineConfig: {type(pipeline_config)}")
server_args_kwargs["pipeline_config"] = pipeline_config server_args_kwargs["pipeline_config"] = pipeline_config
elif attr == "nunchaku_config":
nunchaku_config = NunchakuSVDQuantArgs.from_dict(kwargs)
server_args_kwargs["nunchaku_config"] = nunchaku_config
elif attr in kwargs: elif attr in kwargs:
server_args_kwargs[attr] = kwargs[attr] server_args_kwargs[attr] = kwargs[attr]
@@ -977,6 +1010,7 @@ class ServerArgs:
def check_server_args(self) -> None: def check_server_args(self) -> None:
"""Validate inference arguments for consistency""" """Validate inference arguments for consistency"""
# layerwise offload # layerwise offload
if current_platform.is_mps(): if current_platform.is_mps():
self.use_fsdp_inference = False self.use_fsdp_inference = False
@@ -38,6 +38,7 @@ from huggingface_hub.errors import (
) )
from requests.exceptions import ConnectionError as RequestsConnectionError from requests.exceptions import ConnectionError as RequestsConnectionError
from requests.exceptions import RequestException from requests.exceptions import RequestException
from safetensors import safe_open
from transformers import AutoConfig, PretrainedConfig from transformers import AutoConfig, PretrainedConfig
from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES
@@ -836,3 +837,12 @@ def snapshot_download(
} }
hf_kwargs.update(kwargs) hf_kwargs.update(kwargs)
return _hf_snapshot_download(**hf_kwargs) return _hf_snapshot_download(**hf_kwargs)
def get_metadata_from_safetensors_file(file_path: str):
try:
with safe_open(file_path, framework="pt", device="cpu") as f:
metadata = f.metadata()
return metadata
except Exception as e:
logger.warning(e)
@@ -375,6 +375,12 @@ def configure_logger(server_args, prefix: str = ""):
set_uvicorn_logging_configs() set_uvicorn_logging_configs()
@lru_cache(maxsize=1)
def get_log_level() -> int:
root = logging.getLogger()
return root.level
def suppress_loggers(loggers_to_suppress: list[str], level: int = logging.WARNING): def suppress_loggers(loggers_to_suppress: list[str], level: int = logging.WARNING):
original_levels = {} original_levels = {}