Skip to content

Commit 5dda173

Browse files
pcuencapatil-surajpatrickvonplaten
authored
Inference support for mps device (huggingface#355)
* Initial support for mps in Stable Diffusion pipeline. * Initial "warmup" implementation when using mps. * Make some deterministic tests pass with mps. * Disable training tests when using mps. * SD: generate latents in CPU then move to device. This is especially important when using the mps device, because generators are not supported there. See for example pytorch/pytorch#84288. In addition, the other pipelines seem to use the same approach: generate the random samples then move to the appropriate device. After this change, generating an image in MPS produces the same result as when using the CPU, if the same seed is used. * Remove prints. * Pass AutoencoderKL test_output_pretrained with mps. Sampling from `posterior` must be done in CPU. * Style * Do not use torch.long for log op in mps device. * Perform incompatible padding ops in CPU. UNet tests now pass. See pytorch/pytorch#84535 * Style: fix import order. * Remove unused symbols. * Remove MPSWarmupMixin, do not apply automatically. We do apply warmup in the tests, but not during normal use. This adopts some PR suggestions by @patrickvonplaten. * Add comment for mps fallback to CPU step. * Add README_mps.md for mps installation and use. * Apply `black` to modified files. * Restrict README_mps to SD, show measures in table. * Make PNDM indexing compatible with mps. Addresses huggingface#239. * Do not use float64 when using LDMScheduler. Fixes huggingface#358. * Fix typo identified by @patil-suraj Co-authored-by: Suraj Patil <surajp815@gmail.com> * Adapt example to new output style. * Restore 1:1 results reproducibility with CompVis. However, mps latents need to be generated in CPU because generators don't work in the mps device. * Move PyTorch nightly to requirements. * Adapt `test_scheduler_outputs_equivalence` ton MPS. * mps: skip training tests instead of ignoring silently. * Make VQModel tests pass on mps. * mps ddim tests: warmup, increase tolerance. * ScoreSdeVeScheduler indexing made mps compatible. * Make ldm pipeline tests pass using warmup. * Style * Simplify casting as suggested in PR. * Add Known Issues to readme. * `isort` import order. * Remove _mps_warmup helpers from ModelMixin. And just make changes to the tests. * Skip tests using unittest decorator for consistency. * Remove temporary var. * Remove spurious blank space. * Remove unused symbol. * Remove README_mps. Co-authored-by: Suraj Patil <surajp815@gmail.com> Co-authored-by: Patrick von Platen <patrick.v.platen@gmail.com>
1 parent 98f3468 commit 5dda173

15 files changed

Lines changed: 92 additions & 14 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ pip install --upgrade diffusers # should install diffusers 0.2.4
3939
conda install -c conda-forge diffusers
4040
```
4141

42+
**Apple Silicon (M1/M2) support**
43+
44+
Please, refer to [the documentation](https://huggingface.co/docs/diffusers/optimization/mps).
45+
4246
## Contributing
4347

4448
We ❤️ contributions from the open-source community!

src/diffusers/models/attention.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def _set_attention_slice(self, slice_size):
146146
self.attn2._slice_size = slice_size
147147

148148
def forward(self, x, context=None):
149+
x = x.contiguous() if x.device.type == "mps" else x
149150
x = self.attn1(self.norm1(x)) + x
150151
x = self.attn2(self.norm2(x), context=context) + x
151152
x = self.ff(self.norm3(x)) + x

src/diffusers/models/resnet.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,15 @@ def upfirdn2d_native(input, kernel, up=1, down=1, pad=(0, 0)):
448448
kernel_h, kernel_w = kernel.shape
449449

450450
out = input.view(-1, in_h, 1, in_w, 1, minor)
451+
452+
# Temporary workaround for mps specific issue: https://github.com/pytorch/pytorch/issues/84535
453+
if input.device.type == "mps":
454+
out = out.to("cpu")
451455
out = F.pad(out, [0, 0, 0, up_x - 1, 0, 0, 0, up_y - 1])
452456
out = out.view(-1, in_h * up_y, in_w * up_x, minor)
453457

454458
out = F.pad(out, [0, 0, max(pad_x0, 0), max(pad_x1, 0), max(pad_y0, 0), max(pad_y1, 0)])
459+
out = out.to(input.device) # Move back to mps if necessary
455460
out = out[
456461
:,
457462
max(-pad_y0, 0) : out.shape[1] - max(-pad_y1, 0),

src/diffusers/models/unet_2d_condition.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ def forward(
171171
if not torch.is_tensor(timesteps):
172172
timesteps = torch.tensor([timesteps], dtype=torch.long, device=sample.device)
173173
elif torch.is_tensor(timesteps) and len(timesteps.shape) == 0:
174-
timesteps = timesteps[None].to(sample.device)
174+
timesteps = timesteps.to(dtype=torch.float32)
175+
timesteps = timesteps[None].to(device=sample.device)
175176

176177
# broadcast to batch dimension in a way that's compatible with ONNX/Core ML
177178
timesteps = timesteps.expand(sample.shape[0])

src/diffusers/models/vae.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,10 @@ def __init__(self, parameters, deterministic=False):
338338
self.var = self.std = torch.zeros_like(self.mean).to(device=self.parameters.device)
339339

340340
def sample(self, generator: Optional[torch.Generator] = None) -> torch.FloatTensor:
341-
x = self.mean + self.std * torch.randn(self.mean.shape, generator=generator, device=self.parameters.device)
341+
device = self.parameters.device
342+
sample_device = "cpu" if device.type == "mps" else device
343+
sample = torch.randn(self.mean.shape, generator=generator, device=sample_device).to(device)
344+
x = self.mean + self.std * sample
342345
return x
343346

344347
def kl(self, other=None):

src/diffusers/pipeline_utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ class ImagePipelineOutput(BaseOutput):
7272

7373

7474
class DiffusionPipeline(ConfigMixin):
75-
7675
config_name = "model_index.json"
7776

7877
def register_modules(self, **kwargs):

src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,22 @@ def __call__(
198198
text_embeddings = torch.cat([uncond_embeddings, text_embeddings])
199199

200200
# get the initial random noise unless the user supplied it
201+
202+
# Unlike in other pipelines, latents need to be generated in the target device
203+
# for 1-to-1 results reproducibility with the CompVis implementation.
204+
# However this currently doesn't work in `mps`.
205+
latents_device = "cpu" if self.device.type == "mps" else self.device
201206
latents_shape = (batch_size, self.unet.in_channels, height // 8, width // 8)
202207
if latents is None:
203208
latents = torch.randn(
204209
latents_shape,
205210
generator=generator,
206-
device=self.device,
211+
device=latents_device,
207212
)
208213
else:
209214
if latents.shape != latents_shape:
210215
raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}")
211-
latents = latents.to(self.device)
216+
latents = latents.to(self.device)
212217

213218
# set timesteps
214219
accepts_offset = "offset" in set(inspect.signature(self.scheduler.set_timesteps).parameters.keys())

src/diffusers/schedulers/scheduling_pndm.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,8 @@ def add_noise(
355355
noise: Union[torch.FloatTensor, np.ndarray],
356356
timesteps: Union[torch.IntTensor, np.ndarray],
357357
) -> torch.Tensor:
358-
358+
# mps requires indices to be in the same device, so we use cpu as is the default with cuda
359+
timesteps = timesteps.to(self.alphas_cumprod.device)
359360
sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5
360361
sqrt_alpha_prod = self.match_shape(sqrt_alpha_prod, original_samples)
361362
sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5

src/diffusers/schedulers/scheduling_sde_ve.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ def get_adjacent_sigma(self, timesteps, t):
139139
return np.where(timesteps == 0, np.zeros_like(t), self.discrete_sigmas[timesteps - 1])
140140
elif tensor_format == "pt":
141141
return torch.where(
142-
timesteps == 0, torch.zeros_like(t), self.discrete_sigmas[timesteps - 1].to(timesteps.device)
142+
timesteps == 0,
143+
torch.zeros_like(t.to(timesteps.device)),
144+
self.discrete_sigmas[timesteps - 1].to(timesteps.device),
143145
)
144146

145147
raise ValueError(f"`self.tensor_format`: {self.tensor_format} is not valid.")
@@ -196,8 +198,11 @@ def step_pred(
196198
) # torch.repeat_interleave(timestep, sample.shape[0])
197199
timesteps = (timestep * (len(self.timesteps) - 1)).long()
198200

201+
# mps requires indices to be in the same device, so we use cpu as is the default with cuda
202+
timesteps = timesteps.to(self.discrete_sigmas.device)
203+
199204
sigma = self.discrete_sigmas[timesteps].to(sample.device)
200-
adjacent_sigma = self.get_adjacent_sigma(timesteps, timestep)
205+
adjacent_sigma = self.get_adjacent_sigma(timesteps, timestep).to(sample.device)
201206
drift = self.zeros_like(sample)
202207
diffusion = (sigma**2 - adjacent_sigma**2) ** 0.5
203208

src/diffusers/testing_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
global_rng = random.Random()
1010
torch_device = "cuda" if torch.cuda.is_available() else "cpu"
11+
torch_device = "mps" if torch.backends.mps.is_available() else torch_device
1112

1213

1314
def parse_flag_from_env(key, default=False):

0 commit comments

Comments
 (0)