Skip to content

Commit 4bc2fbf

Browse files
authored
feat: add Turing (20~ Series) GPU compatibility for Z Image Turbo (#833)
* add Turing (20~ Series) GPU compatibility for Z Image Turbo * modify example * add test case * refine test case * make fuse_qkv_norm_rotary compatible with enable_sequential_cpu_offload * remove comment
1 parent 2d5d047 commit 4bc2fbf

6 files changed

Lines changed: 178 additions & 19 deletions

File tree

examples/v1/z-image-turbo.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
from diffusers.pipelines.z_image.pipeline_z_image import ZImagePipeline
33

44
from nunchaku import NunchakuZImageTransformer2DModel
5-
from nunchaku.utils import get_precision
5+
from nunchaku.utils import get_precision, is_turing
66

77
if __name__ == "__main__":
88
precision = get_precision() # auto-detect your precision is 'int4' or 'fp4' based on your GPU
99
rank = 128 # Use 32 for faster sampling; 256 (INT4 only) for best quality
10+
dtype = torch.float16 if is_turing() else torch.bfloat16 # Use float16 when Turing (20- series) GPU is used.
1011
transformer = NunchakuZImageTransformer2DModel.from_pretrained(
11-
f"nunchaku-tech/nunchaku-z-image-turbo/svdq-{precision}_r{rank}-z-image-turbo.safetensors"
12+
f"nunchaku-tech/nunchaku-z-image-turbo/svdq-{precision}_r{rank}-z-image-turbo.safetensors", torch_dtype=dtype
1213
)
1314

1415
pipe = ZImagePipeline.from_pretrained(
15-
"Tongyi-MAI/Z-Image-Turbo", transformer=transformer, torch_dtype=torch.bfloat16, low_cpu_mem_usage=False
16-
).to("cuda")
16+
"Tongyi-MAI/Z-Image-Turbo", transformer=transformer, torch_dtype=dtype, low_cpu_mem_usage=False
17+
)
18+
pipe.enable_sequential_cpu_offload() # enable sequential CPU offload for low vram
19+
# pipe = pipe.to("cuda") # or else comment the line above and uncomment this line to put all components to GPU
1720

1821
prompt = "a young military male cooking in the kitchen for therapy"
1922

@@ -26,4 +29,4 @@
2629
generator=torch.Generator().manual_seed(12345),
2730
).images[0]
2831

29-
image.save(f"z-image-turbo-{precision}_r{rank}.png")
32+
image.save(f"z-image-turbo-{precision}_r{rank}_{str(dtype)}.png")

nunchaku/models/attention_processors/zimage.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
from diffusers.models.attention_dispatch import dispatch_attention_fn
99
from diffusers.models.transformers.transformer_z_image import ZSingleStreamAttnProcessor
1010

11-
from ...ops.fused import fused_qkv_norm_rottary
12-
1311

1412
class NunchakuZSingleStreamAttnProcessor(ZSingleStreamAttnProcessor):
1513
"""
@@ -33,13 +31,8 @@ def __call__(
3331
"""
3432
Forward pass of the attention module. Adapted from diffusers.models.transformers.transformer_z_image.ZSingleStreamAttnProcessor#__call__.
3533
"""
36-
qkv = fused_qkv_norm_rottary(
37-
hidden_states,
38-
attn.to_qkv,
39-
attn.norm_q,
40-
attn.norm_k,
41-
freqs_cis,
42-
)
34+
qkv = attn.fused_module(hidden_states, freqs_cis)
35+
4336
query, key, value = qkv.chunk(3, dim=-1)
4437
query = query.unflatten(-1, (attn.heads, -1))
4538
key = key.unflatten(-1, (attn.heads, -1))

nunchaku/models/linear.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,12 @@ def from_linear(cls, linear: nn.Linear, **kwargs):
149149
SVDQW4A4Linear
150150
"""
151151
in_features = kwargs.pop("in_features", linear.in_features)
152+
torch_dtype = kwargs.pop("torch_dtype", linear.weight.dtype)
152153
return cls(
153154
in_features=in_features,
154155
out_features=linear.out_features,
155156
bias=linear.bias is not None,
156-
torch_dtype=linear.weight.dtype,
157+
torch_dtype=torch_dtype,
157158
device=linear.weight.device,
158159
**kwargs,
159160
)

nunchaku/models/transformers/transformer_zimage.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@
1111
import torch.nn as nn
1212
from diffusers.models.attention import FeedForward
1313
from diffusers.models.attention_processor import Attention
14+
from diffusers.models.normalization import RMSNorm
1415
from diffusers.models.transformers.transformer_z_image import FeedForward as ZImageFeedForward
1516
from diffusers.models.transformers.transformer_z_image import ZImageTransformer2DModel, ZImageTransformerBlock
1617
from huggingface_hub import utils
1718

1819
from nunchaku.models.unets.unet_sdxl import NunchakuSDXLFeedForward
1920

21+
from ...ops.gemm import svdq_gemm_w4a4_cuda
22+
from ...ops.quantize import svdq_quantize_w4a4_act_fuse_lora_cuda
2023
from ...utils import get_precision, pad_tensor
2124
from ..attention import NunchakuBaseAttention
2225
from ..attention_processors.zimage import NunchakuZSingleStreamAttnProcessor
2326
from ..embeddings import pack_rotemb
2427
from ..linear import SVDQW4A4Linear
2528
from ..utils import fuse_linears
26-
from .utils import NunchakuModelLoaderMixin, patch_scale_key
29+
from .utils import NunchakuModelLoaderMixin, convert_fp16, patch_scale_key
2730

2831

2932
class NunchakuZImageRopeHook:
@@ -50,6 +53,77 @@ def __call__(self, module: nn.Module, input_args: tuple, input_kwargs: dict):
5053
return input_args, new_input_kwargs
5154

5255

56+
class NunchakuZImageFusedModule(nn.Module):
57+
"""
58+
Fused module for quantized QKV projection, RMS normalization, and rotary embedding for ZImage attention.
59+
60+
Parameters
61+
----------
62+
qkv : SVDQW4A4Linear
63+
Quantized QKV projection layer.
64+
norm_q : RMSNorm
65+
RMSNorm for query.
66+
norm_k : RMSNorm
67+
RMSNorm for key.
68+
"""
69+
70+
def __init__(self, qkv: SVDQW4A4Linear, norm_q: RMSNorm, norm_k: RMSNorm):
71+
super().__init__()
72+
for name, param in qkv.named_parameters(prefix="qkv_"):
73+
setattr(self, name.replace(".", ""), param)
74+
self.qkv_precision = qkv.precision
75+
self.qkv_out_features = qkv.out_features
76+
for name, param in norm_q.named_parameters(prefix="norm_q_"):
77+
setattr(self, name.replace(".", ""), param)
78+
for name, param in norm_k.named_parameters(prefix="norm_k_"):
79+
setattr(self, name.replace(".", ""), param)
80+
81+
def forward(self, x: torch.Tensor, freqs_cis: Optional[torch.Tensor] = None):
82+
"""
83+
Fuse QKV projection, RMS normalizaion and rotary embedding.
84+
85+
Parameters
86+
----------
87+
x : torch.Tensor
88+
The hidden states tensor
89+
freqs_cis : torch.Tensor, optional
90+
The rotary embedding tensor
91+
92+
Returns
93+
-------
94+
The projection results of q, k, v. q result and k result are RMS-normalized and applied RoPE.
95+
"""
96+
batch_size, seq_len, channels = x.shape
97+
x = x.view(batch_size * seq_len, channels)
98+
quantized_x, ascales, lora_act_out = svdq_quantize_w4a4_act_fuse_lora_cuda(
99+
x,
100+
lora_down=self.qkv_proj_down,
101+
smooth=self.qkv_smooth_factor,
102+
fp4=self.qkv_precision == "nvfp4",
103+
pad_size=256,
104+
)
105+
output = torch.empty(batch_size * seq_len, self.qkv_out_features, dtype=x.dtype, device=x.device)
106+
svdq_gemm_w4a4_cuda(
107+
act=quantized_x,
108+
wgt=self.qkv_qweight,
109+
out=output,
110+
ascales=ascales,
111+
wscales=self.qkv_wscales,
112+
lora_act_in=lora_act_out,
113+
lora_up=self.qkv_proj_up,
114+
bias=getattr(self, "qkv_bias", None),
115+
fp4=self.qkv_precision == "nvfp4",
116+
alpha=1.0 if self.qkv_precision == "nvfp4" else None,
117+
wcscales=self.qkv_wcscales if self.qkv_precision == "nvfp4" else None,
118+
norm_q=self.norm_q_weight,
119+
norm_k=self.norm_k_weight,
120+
rotary_emb=freqs_cis,
121+
)
122+
123+
output = output.view(batch_size, seq_len, -1)
124+
return output
125+
126+
53127
class NunchakuZImageAttention(NunchakuBaseAttention):
54128
"""
55129
Nunchaku-optimized Attention module for ZImage with quantized and fused QKV projections.
@@ -172,6 +246,14 @@ def _convert_z_image_ff(z_ff: ZImageFeedForward) -> FeedForward:
172246
return converted_ff
173247

174248

249+
def replace_fused_module(module, incompatible_keys):
250+
assert isinstance(module, NunchakuZImageAttention)
251+
module.fused_module = NunchakuZImageFusedModule(module.to_qkv, module.norm_q, module.norm_k)
252+
del module.to_qkv
253+
del module.norm_q
254+
del module.norm_k
255+
256+
175257
class NunchakuZImageFeedForward(NunchakuSDXLFeedForward):
176258
"""
177259
Quantized feed-forward block for :class:`NunchakuZImageTransformerBlock`.
@@ -218,6 +300,7 @@ def _patch_model(self, skip_refiners: bool = False, **kwargs):
218300
def _patch_transformer_block(block_list: List[ZImageTransformerBlock]):
219301
for _, block in enumerate(block_list):
220302
block.attention = NunchakuZImageAttention(block.attention, **kwargs)
303+
block.attention.register_load_state_dict_post_hook(replace_fused_module)
221304
block.feed_forward = NunchakuZImageFeedForward(block.feed_forward, **kwargs)
222305

223306
def _convert_feed_forward(block_list: List[ZImageTransformerBlock]):
@@ -323,10 +406,12 @@ def from_pretrained(cls, pretrained_model_name_or_path: str | os.PathLike[str],
323406

324407
print(f"quantization_config: {quantization_config}, rank={rank}, skip_refiners={skip_refiners}")
325408

326-
transformer._patch_model(skip_refiners=skip_refiners, precision=precision, rank=rank)
409+
transformer._patch_model(skip_refiners=skip_refiners, precision=precision, rank=rank, **kwargs)
327410
transformer = transformer.to_empty(device=device)
328411

329412
patch_scale_key(transformer, model_state_dict)
413+
if torch_dtype == torch.float16:
414+
convert_fp16(transformer, model_state_dict)
330415

331416
transformer.load_state_dict(model_state_dict)
332417

nunchaku/models/transformers/utils.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,21 @@ def patch_scale_key(transformer_from_config: nn.Module, state_dict_from_checkpoi
164164
if k not in state_dict_from_checkpoint:
165165
assert ".wcscales" in k
166166
state_dict_from_checkpoint[k] = torch.ones_like(state_dict[k])
167-
else:
168-
assert state_dict[k].dtype == state_dict_from_checkpoint[k].dtype
169167

170168
for n, m in transformer_from_config.named_modules():
171169
if isinstance(m, SVDQW4A4Linear):
172170
if m.wtscale is not None:
173171
m.wtscale = state_dict_from_checkpoint.pop(f"{n}.wtscale", 1.0)
172+
173+
174+
def convert_fp16(transformer_from_config: nn.Module, state_dict_from_checkpoint: dict):
175+
state_dict = transformer_from_config.state_dict()
176+
for k in state_dict.keys():
177+
if state_dict[k].dtype != state_dict_from_checkpoint[k].dtype:
178+
assert (
179+
state_dict[k].dtype == torch.float16 and state_dict_from_checkpoint[k].dtype == torch.bfloat16
180+
), f"Unexpected dtype difference for key: {k}, model dtype: {state_dict[k].dtype}, \
181+
checkpoint dtype: {state_dict_from_checkpoint[k].dtype}"
182+
state_dict_from_checkpoint[k] = torch.nan_to_num(
183+
state_dict_from_checkpoint[k].to(torch.float16), nan=0.0, posinf=65504, neginf=-65504
184+
)

tests/v1/z_image/test_z_image_turbo.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44

55
import pytest
6+
import requests
67
import torch
78
from diffusers import ZImagePipeline
89

@@ -48,6 +49,7 @@
4849
]
4950

5051

52+
@pytest.mark.skipif(is_turing(), reason="Turing GPUs. Skip tests.")
5153
@pytest.mark.parametrize(
5254
"rank,expected_lpips",
5355
[
@@ -110,3 +112,67 @@ def test_zimage_turbo(rank: int, expected_lpips: dict[str, float]):
110112
lpips = compute_lpips(save_dir_16bit, save_dir_nunchaku)
111113
print(f"lpips: {lpips}")
112114
assert lpips < expected_lpips[f"{precision}-{dtype_str}"] * 1.15
115+
116+
117+
def download_ref_images(local_save_dir, filenames):
118+
for filename in filenames:
119+
try:
120+
url = f"https://huggingface.co/datasets/nunchaku-tech/test-data/resolve/main/inputs/test-ref-z-image-turbo-{filename}.png"
121+
save_path = local_save_dir / f"{filename}.png"
122+
response = requests.get(url, stream=True, timeout=10)
123+
response.raise_for_status()
124+
if not os.path.exists(local_save_dir):
125+
os.makedirs(local_save_dir)
126+
with open(save_path, "wb") as file:
127+
for chunk in response.iter_content(chunk_size=2048):
128+
file.write(chunk)
129+
print(f"ref image downloaded: url: {url}, save_path: {save_path}")
130+
except Exception as e:
131+
print(f"download ref image failed: {e}")
132+
133+
134+
@pytest.mark.parametrize(
135+
"rank,expected_lpips",
136+
[
137+
(32, {"int4-fp16": 0.4}),
138+
(128, {"int4-fp16": 0.38}),
139+
(256, {"int4-fp16": 0.37}),
140+
],
141+
)
142+
def test_zimage_turbo_turing(rank: int, expected_lpips: dict[str, float]):
143+
if f"{precision}-{dtype_str}" not in expected_lpips:
144+
return
145+
146+
if not already_generate(save_dir_16bit, len(dataset)):
147+
filenames = [d["filename"] for d in dataset]
148+
download_ref_images(save_dir_16bit, filenames)
149+
150+
save_dir_nunchaku = (
151+
Path("test_results") / "nunchaku" / model_name / f"{precision}_r{rank}-fp16" / f"{folder_name}-bs{batch_size}"
152+
)
153+
path = f"nunchaku-tech/nunchaku-z-image-turbo/svdq-{precision}_r{rank}-z-image-turbo.safetensors"
154+
transformer = NunchakuZImageTransformer2DModel.from_pretrained(path, torch_dtype=torch_dtype)
155+
156+
pipe = ZImagePipeline.from_pretrained(repo_id, transformer=transformer, torch_dtype=torch_dtype)
157+
pipe.enable_sequential_cpu_offload()
158+
159+
run_pipeline(
160+
dataset=dataset,
161+
batch_size=batch_size,
162+
pipeline=pipe,
163+
save_dir=save_dir_nunchaku,
164+
forward_kwargs={
165+
"width": width,
166+
"height": height,
167+
"num_inference_steps": num_inference_steps,
168+
"guidance_scale": guidance_scale,
169+
},
170+
)
171+
del transformer
172+
del pipe
173+
gc.collect()
174+
torch.cuda.empty_cache()
175+
176+
lpips = compute_lpips(save_dir_16bit, save_dir_nunchaku)
177+
print(f"lpips: {lpips}")
178+
assert lpips < expected_lpips[f"{precision}-fp16"] * 1.15

0 commit comments

Comments
 (0)