Diffusers documentation

The Stable Diffusion Guide 🎨

Join the Hugging Face community

to get started

# The Stable Diffusion Guide 🎨

## Intro

Stable Diffusion is a Latent Diffusion model developed by researchers from the Machine Vision and Learning group at LMU Munich, a.k.a CompVis.
Model checkpoints were publicly released at the end of August 2022 by a collaboration of Stability AI, CompVis, and Runway with support from EleutherAI and LAION. For more information, you can check out the official blog post.

Since its public release the community has done an incredible job at working together to make the stable diffusion checkpoints faster, more memory efficient, and more performant.

🧨 Diffusers offers a simple API to run stable diffusion with all memory, computing, and quality improvements.

This notebook walks you through the improvements one-by-one so you can best leverage StableDiffusionPipeline for inference.

## Prompt Engineering 🎨

When running *Stable Diffusion* in inference, we usually want to generate a certain type, or style of image and then improve upon it. Improving upon a previously generated image means running inference over and over again with a different prompt and potentially a different seed until we are happy with our generation.

So to begin with, it is most important to speed up stable diffusion as much as possible to generate as many pictures as possible in a given amount of time.

This can be done by both improving the computational efficiency (speed) and the memory efficiency (GPU RAM).

Let’s start by looking into computational efficiency first.

Throughout the notebook, we will focus on runwayml/stable-diffusion-v1-5:

model_id = "runwayml/stable-diffusion-v1-5"

## Speed Optimization

from diffusers import StableDiffusionPipeline

pipe = StableDiffusionPipeline.from_pretrained(model_id)                                                                                                                                                                                      

We aim at generating a beautiful photograph of an old warrior chief and will later try to find the best prompt to generate such a photograph. For now, let’s keep the prompt simple:

prompt = "portrait photo of a old warrior chief"

To begin with, we should make sure we run inference on GPU, so let’s move the pipeline to GPU, just like you would with any PyTorch module.

pipe = pipe.to("cuda")

To generate an image, you should use the [~StableDiffusionPipeline.__call__] method.

To make sure we can reproduce more or less the same image in every call, let’s make use of the generator. See the documentation on reproducibility here for more information.

generator = torch.Generator("cuda").manual_seed(0)

Now, let’s take a spin on it.

image = pipe(prompt, generator=generator).images[0]
image                                                                                                                                                                                                                                         

Cool, this now took roughly 30 seconds on a T4 GPU (you might see faster inference if your allocated GPU is better than a T4).

The default run we did above used full float32 precision and ran the default number of inference steps (50). The easiest speed-ups come from switching to float16 (or half) precision and simply running fewer inference steps. Let’s load the model now in float16 instead.

import torch

pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16)
pipe = pipe.to("cuda")                                                                                                                                                                                                                        

And we can again call the pipeline to generate an image.

generator = torch.Generator("cuda").manual_seed(0)

image = pipe(prompt, generator=generator).images[0]
image                                                                                                                                                                                                                                         

Cool, this is almost three times as fast for arguably the same image quality.

We strongly suggest always running your pipelines in float16 as so far we have very rarely seen degradations in quality because of it.

Next, let’s see if we need to use 50 inference steps or whether we could use significantly fewer. The number of inference steps is associated with the denoising scheduler we use. Choosing a more efficient scheduler could help us decrease the number of steps.

Let’s have a look at all the schedulers the stable diffusion pipeline is compatible with.

pipe.scheduler.compatibles
    [diffusers.schedulers.scheduling_dpmsolver_singlestep.DPMSolverSinglestepScheduler,
diffusers.schedulers.scheduling_lms_discrete.LMSDiscreteScheduler,
diffusers.schedulers.scheduling_heun_discrete.HeunDiscreteScheduler,
diffusers.schedulers.scheduling_pndm.PNDMScheduler,
diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler,
diffusers.schedulers.scheduling_euler_ancestral_discrete.EulerAncestralDiscreteScheduler,
diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler,
diffusers.schedulers.scheduling_ddpm.DDPMScheduler,
diffusers.schedulers.scheduling_ddim.DDIMScheduler]                                                                                                                                                                                      

Cool, that’s a lot of schedulers.

🧨 Diffusers is constantly adding a bunch of novel schedulers/samplers that can be used with Stable Diffusion. For more information, we recommend taking a look at the official documentation here.

Alright, right now Stable Diffusion is using the PNDMScheduler which usually requires around 50 inference steps. However, other schedulers such as DPMSolverMultistepScheduler or DPMSolverSinglestepScheduler seem to get away with just 20 to 25 inference steps. Let’s try them out.

You can set a new scheduler by making use of the from_config function.

from diffusers import DPMSolverMultistepScheduler

pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)                                                                                                                                                               

Now, let’s try to reduce the number of inference steps to just 20.

generator = torch.Generator("cuda").manual_seed(0)

image = pipe(prompt, generator=generator, num_inference_steps=20).images[0]
image                                                                                                                                                                                                                                         

The image now does look a little different, but it’s arguably still of equally high quality. We now cut inference time to just 4 seconds though 😍.

## Memory Optimization

Less memory used in generation indirectly implies more speed, since we’re often trying to maximize how many images we can generate per second. Usually, the more images per inference run, the more images per second too.

The easiest way to see how many images we can generate at once is to simply try it out, and see when we get a “Out-of-memory (OOM)” error.

We can run batched inference by simply passing a list of prompts and generators. Let’s define a quick function that generates a batch for us.

def get_inputs(batch_size=1):
generator = [torch.Generator("cuda").manual_seed(i) for i in range(batch_size)]
prompts = batch_size * [prompt]
num_inference_steps = 20

return {"prompt": prompts, "generator": generator, "num_inference_steps": num_inference_steps}                                                                                                                                              

This function returns a list of prompts and a list of generators, so we can reuse the generator that produced a result we like.

We also need a method that allows us to easily display a batch of images.

from PIL import Image

def image_grid(imgs, rows=2, cols=2):
w, h = imgs[0].size
grid = Image.new('RGB', size=(cols*w, rows*h))

for i, img in enumerate(imgs):
grid.paste(img, box=(i%cols*w, i//cols*h))
return grid                                                                                                                                                                                                                               

Cool, let’s see how much memory we can use starting with batch_size=4.

images = pipe(**get_inputs(batch_size=4)).images
image_grid(images)                                                                                                                                                                                                                            

Going over a batch_size of 4 will error out in this notebook (assuming we are running it on a T4 GPU). Also, we can see we only generate slightly more images per second (3.75s/image) compared to 4s/image previously.

However, the community has found some nice tricks to improve the memory constraints further. After stable diffusion was released, the community found improvements within days and shared them freely over GitHub - open-source at its finest! I believe the original idea came from this GitHub thread.

By far most of the memory is taken up by the cross-attention layers. Instead of running this operation in batch, one can run it sequentially to save a significant amount of memory.

It can easily be enabled by calling enable_attention_slicing as is documented here.

pipe.enable_attention_slicing()

Great, now that attention slicing is enabled, let’s try to double the batch size again, going for batch_size=8.

images = pipe(**get_inputs(batch_size=8)).images
image_grid(images, rows=2, cols=4)                                                                                                                                                                                                            

Nice, it works. However, the speed gain is again not very big (it might however be much more significant on other GPUs).

We’re at roughly 3.5 seconds per image 🔥 which is probably the fastest we can be with a simple T4 without sacrificing quality.

Next, let’s look into how to improve the quality!

## Quality Improvements

Now that our image generation pipeline is blazing fast, let’s try to get maximum image quality.

First of all, image quality is extremely subjective, so it’s difficult to make general claims here.

The most obvious step to take to improve quality is to use better checkpoints. Since the release of Stable Diffusion, many improved versions have been released, which are summarized here:

Newer versions don’t necessarily mean better image quality with the same parameters. People mentioned that 2.0 is slightly worse than 1.5 for certain prompts, but given the right prompt engineering 2.0 and 2.1 seem to be better.

Overall, we strongly recommend just trying the models out and reading up on advice online (e.g. it has been shown that using negative prompts is very important for 2.0 and 2.1 to get the highest possible quality. See for example this nice blog post.

Additionally, the community has started fine-tuning many of the above versions on certain styles with some of them having an extremely high quality and gaining a lot of traction.

We recommend having a look at all diffusers checkpoints sorted by downloads and trying out the different checkpoints.

For the following, we will stick to v1.5 for simplicity.

Next, we can also try to optimize single components of the pipeline, e.g. switching out the latent decoder. For more details on how the whole Stable Diffusion pipeline works, please have a look at this blog post.

from diffusers import AutoencoderKL

vae = AutoencoderKL.from_pretrained("stabilityai/sd-vae-ft-mse", torch_dtype=torch.float16).to("cuda")                                                                                                                                        

Now we can set it to the vae of the pipeline to use it.

pipe.vae = vae

Let’s run the same prompt as before to compare quality.

images = pipe(**get_inputs(batch_size=8)).images
image_grid(images, rows=2, cols=4)                                                                                                                                                                                                            

Seems like the difference is only very minor, but the new generations are arguably a bit sharper.

Cool, finally, let’s look a bit into prompt engineering.

Our goal was to generate a photo of an old warrior chief. Let’s now try to bring a bit more color into the photos and make the look more impressive.

Originally our prompt was ”portrait photo of an old warrior chief“.

To improve the prompt, it often helps to add cues that could have been used online to save high-quality photos, as well as add more details.
Essentially, when doing prompt engineering, one has to think:

• How was the photo or similar photos of the one I want probably stored on the internet?
• What additional detail can I give that steers the models into the style that I want?

prompt += ", tribal panther make up, blue on red, side profile, looking away, serious eyes"

and let’s also add some cues that usually help to generate higher quality images.

prompt += " 50mm portrait photography, hard rim lighting photography--beta --ar 2:3  --beta --upbeta"
prompt                                                                                                                                                                                                                                        

Cool, let’s now try this prompt.

images = pipe(**get_inputs(batch_size=8)).images
image_grid(images, rows=2, cols=4)                                                                                                                                                                                                            

Pretty impressive! We got some very high-quality image generations there. The 2nd image is my personal favorite, so I’ll re-use this seed and see whether I can tweak the prompts slightly by using “oldest warrior”, “old”, "", and “young” instead of “old”.

prompts = [
"portrait photo of the oldest warrior chief, tribal panther make up, blue on red, side profile, looking away, serious eyes 50mm portrait photography, hard rim lighting photography--beta --ar 2:3  --beta --upbeta",
"portrait photo of a old warrior chief, tribal panther make up, blue on red, side profile, looking away, serious eyes 50mm portrait photography, hard rim lighting photography--beta --ar 2:3  --beta --upbeta",
"portrait photo of a warrior chief, tribal panther make up, blue on red, side profile, looking away, serious eyes 50mm portrait photography, hard rim lighting photography--beta --ar 2:3  --beta --upbeta",
"portrait photo of a young warrior chief, tribal panther make up, blue on red, side profile, looking away, serious eyes 50mm portrait photography, hard rim lighting photography--beta --ar 2:3  --beta --upbeta",
]

generator = [torch.Generator("cuda").manual_seed(1) for _ in range(len(prompts))]  # 1 because we want the 2nd image

images = pipe(prompt=prompts, generator=generator, num_inference_steps=25).images
image_grid(images)                                                                                                                                                                                                                            

The first picture looks nice! The eye movement slightly changed and looks nice. This finished up our 101-guide on how to use Stable Diffusion 🤗.

For more information on optimization or other guides, I recommend taking a look at the following: