Skip to content

Commit 7b0ba48

Browse files
authored
Update Noise Autocorrelation Loss Function for Pix2PixZero Pipeline (#2942)
* Update Pix2PixZero Auto-correlation Loss * Add fast inversion tests * Clarify purpose and mark as deprecated Fix inversion prompt broadcasting * Register modules set to `None` in config for `test_save_load_optional_components` * Update new tests to coordinate with #2953
1 parent 8d5906a commit 7b0ba48

File tree

2 files changed

+123
-32
lines changed

2 files changed

+123
-32
lines changed

src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_pix2pix_zero.py

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from ...utils import (
3737
PIL_INTERPOLATION,
3838
BaseOutput,
39+
deprecate,
3940
is_accelerate_available,
4041
is_accelerate_version,
4142
logging,
@@ -721,23 +722,31 @@ def prepare_image_latents(self, image, batch_size, dtype, device, generator=None
721722
)
722723

723724
if isinstance(generator, list):
724-
init_latents = [
725-
self.vae.encode(image[i : i + 1]).latent_dist.sample(generator[i]) for i in range(batch_size)
726-
]
727-
init_latents = torch.cat(init_latents, dim=0)
725+
latents = [self.vae.encode(image[i : i + 1]).latent_dist.sample(generator[i]) for i in range(batch_size)]
726+
latents = torch.cat(latents, dim=0)
728727
else:
729-
init_latents = self.vae.encode(image).latent_dist.sample(generator)
730-
731-
init_latents = self.vae.config.scaling_factor * init_latents
732-
733-
if batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] != 0:
734-
raise ValueError(
735-
f"Cannot duplicate `image` of batch size {init_latents.shape[0]} to {batch_size} text prompts."
736-
)
728+
latents = self.vae.encode(image).latent_dist.sample(generator)
729+
730+
latents = self.vae.config.scaling_factor * latents
731+
732+
if batch_size != latents.shape[0]:
733+
if batch_size % latents.shape[0] == 0:
734+
# expand image_latents for batch_size
735+
deprecation_message = (
736+
f"You have passed {batch_size} text prompts (`prompt`), but only {latents.shape[0]} initial"
737+
" images (`image`). Initial images are now duplicating to match the number of text prompts. Note"
738+
" that this behavior is deprecated and will be removed in a version 1.0.0. Please make sure to update"
739+
" your script to pass as many initial images as text prompts to suppress this warning."
740+
)
741+
deprecate("len(prompt) != len(image)", "1.0.0", deprecation_message, standard_warn=False)
742+
additional_latents_per_image = batch_size // latents.shape[0]
743+
latents = torch.cat([latents] * additional_latents_per_image, dim=0)
744+
else:
745+
raise ValueError(
746+
f"Cannot duplicate `image` of batch size {latents.shape[0]} to {batch_size} text prompts."
747+
)
737748
else:
738-
init_latents = torch.cat([init_latents], dim=0)
739-
740-
latents = init_latents
749+
latents = torch.cat([latents], dim=0)
741750

742751
return latents
743752

@@ -759,23 +768,18 @@ def get_epsilon(self, model_output: torch.Tensor, sample: torch.Tensor, timestep
759768
)
760769

761770
def auto_corr_loss(self, hidden_states, generator=None):
762-
batch_size, channel, height, width = hidden_states.shape
763-
if batch_size > 1:
764-
raise ValueError("Only batch_size 1 is supported for now")
765-
766-
hidden_states = hidden_states.squeeze(0)
767-
# hidden_states must be shape [C,H,W] now
768771
reg_loss = 0.0
769772
for i in range(hidden_states.shape[0]):
770-
noise = hidden_states[i][None, None, :, :]
771-
while True:
772-
roll_amount = torch.randint(noise.shape[2] // 2, (1,), generator=generator).item()
773-
reg_loss += (noise * torch.roll(noise, shifts=roll_amount, dims=2)).mean() ** 2
774-
reg_loss += (noise * torch.roll(noise, shifts=roll_amount, dims=3)).mean() ** 2
775-
776-
if noise.shape[2] <= 8:
777-
break
778-
noise = F.avg_pool2d(noise, kernel_size=2)
773+
for j in range(hidden_states.shape[1]):
774+
noise = hidden_states[i : i + 1, j : j + 1, :, :]
775+
while True:
776+
roll_amount = torch.randint(noise.shape[2] // 2, (1,), generator=generator).item()
777+
reg_loss += (noise * torch.roll(noise, shifts=roll_amount, dims=2)).mean() ** 2
778+
reg_loss += (noise * torch.roll(noise, shifts=roll_amount, dims=3)).mean() ** 2
779+
780+
if noise.shape[2] <= 8:
781+
break
782+
noise = F.avg_pool2d(noise, kernel_size=2)
779783
return reg_loss
780784

781785
def kl_divergence(self, hidden_states):

tests/pipelines/stable_diffusion/test_stable_diffusion_pix2pix_zero.py

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
# limitations under the License.
1515

1616
import gc
17+
import random
18+
import tempfile
1719
import unittest
1820

1921
import numpy as np
@@ -30,7 +32,7 @@
3032
StableDiffusionPix2PixZeroPipeline,
3133
UNet2DConditionModel,
3234
)
33-
from diffusers.utils import load_numpy, slow, torch_device
35+
from diffusers.utils import floats_tensor, load_numpy, slow, torch_device
3436
from diffusers.utils.testing_utils import load_image, load_pt, require_torch_gpu, skip_mps
3537

3638
from ..pipeline_params import TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS, TEXT_GUIDED_IMAGE_VARIATION_PARAMS
@@ -69,6 +71,7 @@ def get_dummy_components(self):
6971
cross_attention_dim=32,
7072
)
7173
scheduler = DDIMScheduler()
74+
inverse_scheduler = DDIMInverseScheduler()
7275
torch.manual_seed(0)
7376
vae = AutoencoderKL(
7477
block_out_channels=[32, 64],
@@ -101,7 +104,7 @@ def get_dummy_components(self):
101104
"tokenizer": tokenizer,
102105
"safety_checker": None,
103106
"feature_extractor": None,
104-
"inverse_scheduler": None,
107+
"inverse_scheduler": inverse_scheduler,
105108
"caption_generator": None,
106109
"caption_processor": None,
107110
}
@@ -122,6 +125,90 @@ def get_dummy_inputs(self, device, seed=0):
122125
}
123126
return inputs
124127

128+
def get_dummy_inversion_inputs(self, device, seed=0):
129+
dummy_image = floats_tensor((2, 3, 32, 32), rng=random.Random(seed)).to(torch_device)
130+
generator = torch.manual_seed(seed)
131+
132+
inputs = {
133+
"prompt": [
134+
"A painting of a squirrel eating a burger",
135+
"A painting of a burger eating a squirrel",
136+
],
137+
"image": dummy_image.cpu(),
138+
"num_inference_steps": 2,
139+
"guidance_scale": 6.0,
140+
"generator": generator,
141+
"output_type": "numpy",
142+
}
143+
return inputs
144+
145+
def test_save_load_optional_components(self):
146+
if not hasattr(self.pipeline_class, "_optional_components"):
147+
return
148+
149+
components = self.get_dummy_components()
150+
pipe = self.pipeline_class(**components)
151+
pipe.to(torch_device)
152+
pipe.set_progress_bar_config(disable=None)
153+
154+
# set all optional components to None and update pipeline config accordingly
155+
for optional_component in pipe._optional_components:
156+
setattr(pipe, optional_component, None)
157+
pipe.register_modules(**{optional_component: None for optional_component in pipe._optional_components})
158+
159+
inputs = self.get_dummy_inputs(torch_device)
160+
output = pipe(**inputs)[0]
161+
162+
with tempfile.TemporaryDirectory() as tmpdir:
163+
pipe.save_pretrained(tmpdir)
164+
pipe_loaded = self.pipeline_class.from_pretrained(tmpdir)
165+
pipe_loaded.to(torch_device)
166+
pipe_loaded.set_progress_bar_config(disable=None)
167+
168+
for optional_component in pipe._optional_components:
169+
self.assertTrue(
170+
getattr(pipe_loaded, optional_component) is None,
171+
f"`{optional_component}` did not stay set to None after loading.",
172+
)
173+
174+
inputs = self.get_dummy_inputs(torch_device)
175+
output_loaded = pipe_loaded(**inputs)[0]
176+
177+
max_diff = np.abs(output - output_loaded).max()
178+
self.assertLess(max_diff, 1e-4)
179+
180+
def test_stable_diffusion_pix2pix_zero_inversion(self):
181+
device = "cpu" # ensure determinism for the device-dependent torch.Generator
182+
components = self.get_dummy_components()
183+
sd_pipe = StableDiffusionPix2PixZeroPipeline(**components)
184+
sd_pipe = sd_pipe.to(device)
185+
sd_pipe.set_progress_bar_config(disable=None)
186+
187+
inputs = self.get_dummy_inversion_inputs(device)
188+
inputs["image"] = inputs["image"][:1]
189+
inputs["prompt"] = inputs["prompt"][:1]
190+
image = sd_pipe.invert(**inputs).images
191+
image_slice = image[0, -3:, -3:, -1]
192+
assert image.shape == (1, 32, 32, 3)
193+
expected_slice = np.array([0.4833, 0.4696, 0.5574, 0.5194, 0.5248, 0.5638, 0.5040, 0.5423, 0.5072])
194+
195+
assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3
196+
197+
def test_stable_diffusion_pix2pix_zero_inversion_batch(self):
198+
device = "cpu" # ensure determinism for the device-dependent torch.Generator
199+
components = self.get_dummy_components()
200+
sd_pipe = StableDiffusionPix2PixZeroPipeline(**components)
201+
sd_pipe = sd_pipe.to(device)
202+
sd_pipe.set_progress_bar_config(disable=None)
203+
204+
inputs = self.get_dummy_inversion_inputs(device)
205+
image = sd_pipe.invert(**inputs).images
206+
image_slice = image[1, -3:, -3:, -1]
207+
assert image.shape == (2, 32, 32, 3)
208+
expected_slice = np.array([0.6672, 0.5203, 0.4908, 0.4376, 0.4517, 0.5544, 0.4605, 0.4826, 0.5007])
209+
210+
assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3
211+
125212
def test_stable_diffusion_pix2pix_zero_default_case(self):
126213
device = "cpu" # ensure determinism for the device-dependent torch.Generator
127214
components = self.get_dummy_components()

0 commit comments

Comments
 (0)