[Diffusion] Add Qwen Image ModelOpt FP8 support (#23155)
Co-authored-by: Mick <mickjagger19@icloud.com>
This commit is contained in:
+29
-2
@@ -61,9 +61,9 @@ This repo now contains:
|
||||
- trajectory similarity validation:
|
||||
[`python/sglang/multimodal_gen/tools/compare_diffusion_trajectory_similarity.py`](../../../tools/compare_diffusion_trajectory_similarity.py)
|
||||
|
||||
Validated documentation and CI coverage currently center on six ModelOpt diffusion transformer override families:
|
||||
Validated documentation and CI coverage currently center on these ModelOpt diffusion transformer override families:
|
||||
|
||||
- FP8: FLUX.1-dev, FLUX.2-dev, Wan2.2
|
||||
- FP8: FLUX.1-dev, FLUX.2-dev, Wan2.2, Qwen Image, Qwen Image Edit
|
||||
- NVFP4: FLUX.1-dev, FLUX.2-dev, Wan2.2
|
||||
|
||||
Treat a new family, a new precision, or a new checkpoint layout as unsupported until it has a documented matrix row and a matching validation story.
|
||||
@@ -194,6 +194,33 @@ For `FLUX.1-dev`, the validated fallback set currently keeps these modules in BF
|
||||
|
||||
Use `--model-type flux1` to force that profile, or rely on `--model-type auto` when the export config identifies `FluxTransformer2DModel`.
|
||||
|
||||
Qwen Image and Qwen Image Edit share `QwenImageTransformer2DModel`, so one
|
||||
ModelOpt FP8 fallback preset covers both. The validated Qwen Image fallback set
|
||||
keeps these modules in BF16:
|
||||
|
||||
- `img_in`
|
||||
- `txt_in`
|
||||
- `time_text_embed.timestep_embedder.linear_1`
|
||||
- `time_text_embed.timestep_embedder.linear_2`
|
||||
- `norm_out.linear`
|
||||
- `proj_out`
|
||||
- `transformer_blocks.*.img_mlp.net.2`
|
||||
- `transformer_blocks.*.img_mod`
|
||||
- `transformer_blocks.*.txt_mod`
|
||||
|
||||
Use `--model-type qwen-image` to force that profile, or rely on
|
||||
`--model-type auto` when the export config identifies
|
||||
`QwenImageTransformer2DModel`.
|
||||
|
||||
Qwen modulation weights can appear in safetensors as `.img_mod.1.weight` and
|
||||
`.txt_mod.1.weight`. Canonicalize those module names to `.img_mod` and
|
||||
`.txt_mod` before fallback matching.
|
||||
|
||||
For Qwen Image FP8, explicit BF16 fallback tensors must be written before
|
||||
honoring ModelOpt ignored weights. Otherwise converter stats can report a
|
||||
fallback while the output checkpoint still retains the source FP8 tensor, which
|
||||
causes severe image-quality regressions.
|
||||
|
||||
For FLUX.1-dev NVFP4 model families that need a mixed BF16+NVFP4 checkpoint, build the merged transformer explicitly:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -10,7 +10,6 @@ import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from diffusers.models.attention import FeedForward
|
||||
from diffusers.models.embeddings import TimestepEmbedding, Timesteps
|
||||
from diffusers.models.modeling_outputs import Transformer2DModelOutput
|
||||
from diffusers.models.normalization import AdaLayerNormContinuous
|
||||
@@ -532,10 +531,27 @@ class QwenImageCrossAttention(nn.Module):
|
||||
prefix=f"{prefix}.to_qkv",
|
||||
)
|
||||
else:
|
||||
# Use separate Q/K/V projections for non-quantized models
|
||||
self.to_q = ReplicatedLinear(dim, self.inner_dim, bias=True)
|
||||
self.to_k = ReplicatedLinear(dim, self.inner_dim, bias=True)
|
||||
self.to_v = ReplicatedLinear(dim, self.inner_dim, bias=True)
|
||||
self.to_q = ReplicatedLinear(
|
||||
dim,
|
||||
self.inner_dim,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.to_q",
|
||||
)
|
||||
self.to_k = ReplicatedLinear(
|
||||
dim,
|
||||
self.inner_dim,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.to_k",
|
||||
)
|
||||
self.to_v = ReplicatedLinear(
|
||||
dim,
|
||||
self.inner_dim,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.to_v",
|
||||
)
|
||||
|
||||
if self.qk_norm:
|
||||
self.norm_q = RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity()
|
||||
@@ -552,15 +568,26 @@ class QwenImageCrossAttention(nn.Module):
|
||||
prefix=f"{prefix}.to_added_qkv",
|
||||
)
|
||||
else:
|
||||
# Use separate Q/K/V projections for non-quantized models
|
||||
self.add_q_proj = ReplicatedLinear(
|
||||
added_kv_proj_dim, self.inner_dim, bias=True
|
||||
added_kv_proj_dim,
|
||||
self.inner_dim,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.add_q_proj",
|
||||
)
|
||||
self.add_k_proj = ReplicatedLinear(
|
||||
added_kv_proj_dim, self.inner_dim, bias=True
|
||||
added_kv_proj_dim,
|
||||
self.inner_dim,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.add_k_proj",
|
||||
)
|
||||
self.add_v_proj = ReplicatedLinear(
|
||||
added_kv_proj_dim, self.inner_dim, bias=True
|
||||
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:
|
||||
@@ -705,6 +732,65 @@ class QwenImageCrossAttention(nn.Module):
|
||||
return img_attn_output, txt_attn_output
|
||||
|
||||
|
||||
class QwenImageGELU(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
inner_dim: int,
|
||||
quant_config: Optional[QuantizationConfig] = None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.proj = ReplicatedLinear(
|
||||
dim,
|
||||
inner_dim,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.proj",
|
||||
)
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
hidden_states, _ = self.proj(hidden_states)
|
||||
return F.gelu(hidden_states, approximate="tanh")
|
||||
|
||||
|
||||
class QwenImageFeedForward(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
dim_out: int,
|
||||
quant_config: Optional[QuantizationConfig] = None,
|
||||
prefix: str = "",
|
||||
mult: int = 4,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
inner_dim = dim * mult
|
||||
self.net = nn.ModuleList(
|
||||
[
|
||||
QwenImageGELU(
|
||||
dim,
|
||||
inner_dim,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.net.0",
|
||||
),
|
||||
nn.Dropout(0.0),
|
||||
ReplicatedLinear(
|
||||
inner_dim,
|
||||
dim_out,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.net.2",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
hidden_states = self.net[0](hidden_states)
|
||||
hidden_states = self.net[1](hidden_states)
|
||||
hidden_states, _ = self.net[2](hidden_states)
|
||||
return hidden_states
|
||||
|
||||
|
||||
class QwenImageTransformerBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -794,15 +880,17 @@ class QwenImageTransformerBlock(nn.Module):
|
||||
activation_fn="gelu-approximate",
|
||||
)
|
||||
else:
|
||||
self.img_mlp = FeedForward(
|
||||
self.img_mlp = QwenImageFeedForward(
|
||||
dim=dim,
|
||||
dim_out=dim,
|
||||
activation_fn="gelu-approximate",
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.img_mlp",
|
||||
)
|
||||
self.txt_mlp = FeedForward(
|
||||
self.txt_mlp = QwenImageFeedForward(
|
||||
dim=dim,
|
||||
dim_out=dim,
|
||||
activation_fn="gelu-approximate",
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.txt_mlp",
|
||||
)
|
||||
|
||||
if nunchaku_enabled:
|
||||
@@ -1124,8 +1212,20 @@ class QwenImageTransformer2DModel(CachableDiT, OffloadableDiTMixin):
|
||||
|
||||
self.txt_norm = RMSNorm(joint_attention_dim, eps=1e-6)
|
||||
|
||||
self.img_in = nn.Linear(in_channels, self.inner_dim)
|
||||
self.txt_in = nn.Linear(joint_attention_dim, self.inner_dim)
|
||||
self.img_in = ReplicatedLinear(
|
||||
in_channels,
|
||||
self.inner_dim,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix="img_in",
|
||||
)
|
||||
self.txt_in = ReplicatedLinear(
|
||||
joint_attention_dim,
|
||||
self.inner_dim,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix="txt_in",
|
||||
)
|
||||
|
||||
self.transformer_blocks = nn.ModuleList(
|
||||
[
|
||||
@@ -1144,8 +1244,12 @@ class QwenImageTransformer2DModel(CachableDiT, OffloadableDiTMixin):
|
||||
self.norm_out = AdaLayerNormContinuous(
|
||||
self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6
|
||||
)
|
||||
self.proj_out = nn.Linear(
|
||||
self.inner_dim, patch_size * patch_size * self.out_channels, bias=True
|
||||
self.proj_out = ReplicatedLinear(
|
||||
self.inner_dim,
|
||||
patch_size * patch_size * self.out_channels,
|
||||
bias=True,
|
||||
quant_config=quant_config,
|
||||
prefix="proj_out",
|
||||
)
|
||||
|
||||
self.timestep_zero = torch.zeros(
|
||||
@@ -1230,7 +1334,7 @@ class QwenImageTransformer2DModel(CachableDiT, OffloadableDiTMixin):
|
||||
if isinstance(encoder_hidden_states_mask, list):
|
||||
encoder_hidden_states_mask = encoder_hidden_states_mask[0]
|
||||
|
||||
hidden_states = self.img_in(hidden_states)
|
||||
hidden_states, _ = self.img_in(hidden_states)
|
||||
|
||||
timestep = (timestep / 1000).to(hidden_states.dtype)
|
||||
|
||||
@@ -1242,7 +1346,7 @@ class QwenImageTransformer2DModel(CachableDiT, OffloadableDiTMixin):
|
||||
modulate_index = None
|
||||
|
||||
encoder_hidden_states = self.txt_norm(encoder_hidden_states)
|
||||
encoder_hidden_states = self.txt_in(encoder_hidden_states)
|
||||
encoder_hidden_states, _ = self.txt_in(encoder_hidden_states)
|
||||
|
||||
block_attention_kwargs = attention_kwargs.copy() if attention_kwargs else {}
|
||||
if encoder_hidden_states_mask is not None:
|
||||
@@ -1295,7 +1399,7 @@ class QwenImageTransformer2DModel(CachableDiT, OffloadableDiTMixin):
|
||||
# Use only the image part (hidden_states) from the dual-stream blocks
|
||||
hidden_states = self.norm_out(hidden_states, temb_txt)
|
||||
|
||||
output = self.proj_out(hidden_states)
|
||||
output, _ = self.proj_out(hidden_states)
|
||||
return output
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ from sglang.multimodal_gen.test.server.testcase_configs import (
|
||||
MODELOPT_FLUX2_FP8_TRANSFORMER,
|
||||
MODELOPT_FLUX2_NVFP4_WEIGHTS,
|
||||
MODELOPT_NVFP4_B200_ENV_VARS,
|
||||
MODELOPT_QWEN_IMAGE_EDIT_FP8_TRANSFORMER,
|
||||
MODELOPT_QWEN_IMAGE_FP8_TRANSFORMER,
|
||||
MODELOPT_WAN22_FP8_TRANSFORMER,
|
||||
MODELOPT_WAN22_NVFP4_TRANSFORMER,
|
||||
T2V_PROMPT,
|
||||
@@ -14,6 +16,7 @@ from sglang.multimodal_gen.test.server.testcase_configs import (
|
||||
HUNYUAN3D_SHAPE_sampling_params,
|
||||
MODELOPT_T2I_CI_sampling_params,
|
||||
MODELOPT_T2V_CI_sampling_params,
|
||||
MODELOPT_TI2I_CI_sampling_params,
|
||||
MULTI_FRAME_I2I_sampling_params,
|
||||
MULTI_IMAGE_TI2I_sampling_params,
|
||||
MULTI_IMAGE_TI2I_UPLOAD_sampling_params,
|
||||
@@ -400,6 +403,20 @@ else:
|
||||
sampling_params=MODELOPT_T2V_CI_sampling_params,
|
||||
extras=["--transformer-path", MODELOPT_WAN22_FP8_TRANSFORMER],
|
||||
),
|
||||
_make_modelopt_ci_case(
|
||||
"qwen_image_modelopt_fp8_t2i",
|
||||
model_path=DEFAULT_QWEN_IMAGE_MODEL_NAME_FOR_TEST,
|
||||
modality="image",
|
||||
sampling_params=MODELOPT_T2I_CI_sampling_params,
|
||||
extras=["--transformer-path", MODELOPT_QWEN_IMAGE_FP8_TRANSFORMER],
|
||||
),
|
||||
_make_modelopt_ci_case(
|
||||
"qwen_image_edit_modelopt_fp8_ti2i",
|
||||
model_path=DEFAULT_QWEN_IMAGE_EDIT_2511_MODEL_NAME_FOR_TEST,
|
||||
modality="image",
|
||||
sampling_params=MODELOPT_TI2I_CI_sampling_params,
|
||||
extras=["--transformer-path", MODELOPT_QWEN_IMAGE_EDIT_FP8_TRANSFORMER],
|
||||
),
|
||||
_make_modelopt_ci_case(
|
||||
"flux1_modelopt_nvfp4_t2i",
|
||||
model_path=DEFAULT_FLUX_1_DEV_MODEL_NAME_FOR_TEST,
|
||||
|
||||
@@ -368,6 +368,13 @@ MODELOPT_T2I_CI_sampling_params = DiffusionSamplingParams(
|
||||
extras={"num_inference_steps": 12, "seed": 0},
|
||||
)
|
||||
|
||||
MODELOPT_TI2I_CI_sampling_params = DiffusionSamplingParams(
|
||||
prompt="Convert 2D style to 3D style",
|
||||
image_path="https://github.com/lm-sys/lm-sys.github.io/releases/download/test/TI2I_Qwen_Image_Edit_Input.jpg",
|
||||
output_size="512x512",
|
||||
extras={"num_inference_steps": 8, "seed": 0},
|
||||
)
|
||||
|
||||
TI2I_sampling_params = DiffusionSamplingParams(
|
||||
prompt="Convert 2D style to 3D style",
|
||||
image_path="https://github.com/lm-sys/lm-sys.github.io/releases/download/test/TI2I_Qwen_Image_Edit_Input.jpg",
|
||||
@@ -435,6 +442,10 @@ HUNYUAN3D_SHAPE_sampling_params = DiffusionSamplingParams(
|
||||
MODELOPT_FLUX1_FP8_TRANSFORMER = "lmsys/flux1-dev-modelopt-fp8-sglang-transformer"
|
||||
MODELOPT_FLUX2_FP8_TRANSFORMER = "lmsys/flux2-dev-modelopt-fp8-sglang-transformer"
|
||||
MODELOPT_WAN22_FP8_TRANSFORMER = "lmsys/wan22-t2v-a14b-modelopt-fp8-sglang-transformer"
|
||||
MODELOPT_QWEN_IMAGE_FP8_TRANSFORMER = "lmsys/qwen-image-modelopt-fp8-sglang-transformer"
|
||||
MODELOPT_QWEN_IMAGE_EDIT_FP8_TRANSFORMER = (
|
||||
"lmsys/qwen-image-edit-modelopt-fp8-sglang-transformer"
|
||||
)
|
||||
MODELOPT_FLUX1_NVFP4_TRANSFORMER = "lmsys/flux1-dev-modelopt-nvfp4-sglang-transformer"
|
||||
MODELOPT_FLUX2_NVFP4_WEIGHTS = "black-forest-labs/FLUX.2-dev-NVFP4"
|
||||
MODELOPT_WAN22_NVFP4_TRANSFORMER = (
|
||||
|
||||
@@ -76,6 +76,15 @@ DEFAULT_LTX2_KEEP_BF16_PATTERNS = [
|
||||
r"^transformer_blocks\.(0|43|44|45|46|47)\.(attn1|attn2|audio_attn1|audio_attn2|audio_to_video_attn|video_to_audio_attn)\.to_out\.0$",
|
||||
r"^transformer_blocks\.(0|43|44|45|46|47)\.(ff|audio_ff)\.proj_(in|out)$",
|
||||
]
|
||||
DEFAULT_QWEN_IMAGE_KEEP_BF16_PATTERNS = [
|
||||
r"^img_in$",
|
||||
r"^txt_in$",
|
||||
r"^time_text_embed\.timestep_embedder\.linear_[12]$",
|
||||
r"^norm_out\.linear$",
|
||||
r"^proj_out$",
|
||||
r"^transformer_blocks\.\d+\.img_mlp\.net\.2$",
|
||||
r"^transformer_blocks\.\d+\.(img_mod|txt_mod)$",
|
||||
]
|
||||
|
||||
|
||||
def _resolve_transformer_dir(path: str) -> str:
|
||||
@@ -173,6 +182,7 @@ def _module_name_variants(weight_name: str) -> list[str]:
|
||||
canonicalized.append(
|
||||
re.sub(r"(\.audio_ff|\.ff)\.net\.2$", r"\1.proj_out", variant)
|
||||
)
|
||||
canonicalized.append(re.sub(r"(\.(img_mod|txt_mod))\.1$", r"\1", variant))
|
||||
variants.extend(canonicalized)
|
||||
|
||||
deduped: list[str] = []
|
||||
@@ -259,12 +269,16 @@ def get_default_keep_bf16_patterns(
|
||||
return list(DEFAULT_FLUX1_KEEP_BF16_PATTERNS)
|
||||
if model_type == "flux2":
|
||||
return list(DEFAULT_FLUX2_KEEP_BF16_PATTERNS)
|
||||
if model_type == "qwen-image":
|
||||
return list(DEFAULT_QWEN_IMAGE_KEEP_BF16_PATTERNS)
|
||||
if model_type == "none":
|
||||
return []
|
||||
if class_name == "FluxTransformer2DModel":
|
||||
return list(DEFAULT_FLUX1_KEEP_BF16_PATTERNS)
|
||||
if class_name == "Flux2Transformer2DModel":
|
||||
return list(DEFAULT_FLUX2_KEEP_BF16_PATTERNS)
|
||||
if class_name == "QwenImageTransformer2DModel":
|
||||
return list(DEFAULT_QWEN_IMAGE_KEEP_BF16_PATTERNS)
|
||||
return []
|
||||
|
||||
|
||||
@@ -552,13 +566,14 @@ def build_modelopt_fp8_transformer(
|
||||
if name in fallback_scale_names:
|
||||
del shard_tensors[name]
|
||||
continue
|
||||
if name in fallback_tensors:
|
||||
shard_tensors[name] = fallback_tensors[name]
|
||||
continue
|
||||
if name.endswith(".weight") and is_ignored_by_modelopt(
|
||||
name, ignore_patterns
|
||||
):
|
||||
preserved_ignored_weight_count += 1
|
||||
continue
|
||||
if name in fallback_tensors:
|
||||
shard_tensors[name] = fallback_tensors[name]
|
||||
scale_key = _resolve_scale_key(name, fp8_scale_map)
|
||||
if (
|
||||
name.endswith(".weight")
|
||||
@@ -645,12 +660,13 @@ def _parse_args() -> argparse.Namespace:
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model-type",
|
||||
choices=["auto", "flux1", "flux2", "ltx2", "none"],
|
||||
choices=["auto", "flux1", "flux2", "ltx2", "qwen-image", "none"],
|
||||
default="auto",
|
||||
help=(
|
||||
"Optional model-family BF16 fallback profile. 'none' uses the generic "
|
||||
"conversion path. 'auto' enables the validated FLUX.1 / FLUX.2 / LTX-2 "
|
||||
"fallback set when the export config matches those transformer classes."
|
||||
"conversion path. 'auto' enables the validated FLUX.1 / FLUX.2 / LTX-2 / "
|
||||
"Qwen Image fallback set when the export config matches those transformer "
|
||||
"classes."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
|
||||
Reference in New Issue
Block a user