如何在本身的显卡上获得SDXL的最佳质量和性能,以及如何选择恰当的优化方法和工具,这一让GenAI用户倍感困惑的问题,业内一直没有一份清晰而细致的评测报告可供参考。直到全栈开发者Félix San出手。
在本文中,Félix先容了相关SDXL优化的方法论、底子优化、Pipeline优化以及组件和参数优化。值得一提的是,基于实测表现,他高度评价并保举了由硅基流动研发的图片/视频推理加快引擎OneDiff,“I just wanted to say that onediff is the fastest of them all! so great job!!(我只想说,OneDiff是全部图像推理引擎中最快的!非常棒的工作!!)”
由于本文内容相称扎实,篇幅相对较长,不外,他很贴心地提醒读者,可以直接翻到末了看结论。
感谢Félix出色的专业评测报告。关于Stable Diffusion XL优化指南,读这一篇就够了。
(本文由OneFlow编译发布,转载请联系授权。原文:https://www.felixsanz.dev/articles/ultimate-guide-to-optimizing-stable-diffusion-xl)
本文将先容Stable Diffusion XL优化,旨在尽可能淘汰内存使用的同时实现最优性能,从而加快图像生成速度。我们将可以或许仅用4GB内存生成SDXL图像,因此可以使用低端显卡。
由于本文以脚本/开发为导向,因此将使用Hugging Face的diffusers库。即便如此,了解不同的优化技术及其相互作用将有助于我们在各种应用中充分使用这些技术,例如Automatic1111的Stable Diffusion webUI,尤其是ComfyUI。
本文可能显得冗长而深奥,但你无需一次性阅读完毕。我的目标是让读者了解各种现存的优化技术,并教会你何时以及如何使用和组合它们,尽管此中一些技术本身就已经有了实质性的差异。
你也可以直接跳到结论部分,此中包括全部测试的总结表格,以及针对追求质量、速度或内存受限条件下运行推理时的发起。
作者 | Félix San
OneFlow编译
翻译|宛子琳、杨婷
1
方法论
在测试中,我使用了RunPod平台,在Secure Cloud上生成了一个GPU Pod,配备了RTX 3090显卡。尽管Secure Cloud的费用略高于Community Cloud($0.44/h vs $0.29/h),但对于测试来说,它似乎更合适。
该实例生成于EU-CZ-1区域,拥有24GB的VRAM(GPU)、32 个vCPU(AMD EPYC 7H12)和125GB的RAM(CPU和RAM值并不紧张)。至于模板,我使用RunPod PyTorch 2.1(runpod/pytorch:2.1.0-py3.10-cuda11.8.0-devel-ubuntu22.04),这是一个底子模板,没有其他额外内容。因为我们将对其举行更改,以是PyTorch的版本并不紧张,但该模板提供了Ubuntu、Python 3.10和CUDA 11.8作为标准设置。只需两次点击并等待30秒,我们就已经准备好所需的一切。
如果你要在本地运行模型,请确保已安装Python 3.10和CUDA或等效平台(本文将使用CUDA)。
全部测试都是在虚拟环境中举行的:
创建虚拟环境
激活虚拟环境
- # Unix
- source .venv/bin/activate
- # Windows
- .venv\Scripts\activate
复制代码 安装所需库:
- pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
- pip install transformers accelerate diffusers
复制代码 测试包括生成4张图片,并比力不同的优化技术,此中一些我信赖你从前可能没有见过。这些不同主题的图像是使用stabilityai/stable-diffusion-xl-base-1.0模型生成的,仅使用一个正向提示和一个固定种子。其余参数将保持默认值:无负向提示,1024x1024尺寸,CFG值为5,步数为50(采样步数)。
提示和种子
- queue = []
- # Photorealistic portrait (Portrait)
- queue.extend([{
- 'prompt': '3/4 shot, candid photograph of a beautiful 30 year old redhead woman with messy dark hair, peacefully sleeping in her bed, night, dark, light from window, dark shadows, masterpiece, uhd, moody',
- 'seed': 877866765,
- }])
- # Creative interior image (Interior)
- queue.extend([{
- 'prompt': 'futuristic living room with big windows, brown sofas, coffee table, plants, cyberpunk city, concept art, earthy colors',
- 'seed': 5567822456,
- }])
- # Macro photography (Macro)
- queue.extend([{
- 'prompt': 'macro shot of a bee collecting nectar from lavender flowers',
- 'seed': 2257899453,
- }])
- # Rendered 3D image (3D)
- queue.extend([{
- 'prompt': '3d rendered isometric fiji island beach, 3d tile, polygon, cartoony, mobile game',
- 'seed': 987867834,
- }])
复制代码 以下是默认生成的图像:
<左右滑动查看更多图片>
以下是对比测试的结果:
- 图像的感知质量(希望我是位良好的评判者)。
- 生成每张图像所需的时间,以及总编译时间(如果有的话)。
- 使用的最大内存量。
每项测试都运行了5次,并使用平均值举行比力。
时间丈量接纳了以下布局:
- from time import perf_counter
- # Import libraries
- # import ...
- # Define prompts
- # queue = []
- # queue.extend ...
- for i, generation in enumerate(queue, start=1):
- # We start the counter
- image_start = perf_counter()
- # Generate and save image
- # ...
- # We stop the counter and save the result
- generation['total_time'] = perf_counter() - image_start
- # Print the generation time of each image
- images_totals = ', '.join(map(lambda generation: str(round(generation['total_time'], 1)), queue))
- print('Image time:', images_totals)
- # Print the average time
- images_average = round(sum(generation['total_time'] for generation in queue) / len(queue), 1)
- print('Average image time:', images_average)
复制代码 为了找出所使用的最大内存量,文件末了包含以下语句:
- max_memory = round(torch.cuda.max_memory_allocated(device='cuda') / 1000000000, 2)
- print('Max. memory used:', max_memory, 'GB')
复制代码 每个测试中包含的内容都是所需的最小代码。虽然每个测试都有本身的布局,但代码大抵如下。
- # Load the model on the graphics card
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- # Create a generator
- generator = torch.Generator(device='cuda')
- # Start a loop to process prompts one by one
- for i, generation in enumerate(queue, start=1):
- # Assign the seed to the generator
- generator.manual_seed(generation['seed'])
- # Create the image
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- # Save the image
- image.save(f'image_{i}.png')
复制代码 为了使测试更真实且淘汰耗时,全部测试都将使用FP16优化。
此中许多测试使用了diffusers库中的pipeline,以便抽象复杂性并使代码更清晰简洁。当测试需要时,抽象级别会低落,但终极我们一直会使用该库提供的方法。另外,模型始终以safetensors格式加载,使用use_safetensors=True属性。
文章中显示的图像尺寸最大为512x512,以便欣赏,但你可以在新标签页/窗口中打开图像,查看其原始大小。
你可以在GitHub上的文章存储库(github.com/felixsanz/felixsanz_dev)中找到全部单独的测试文件。
让我们开始吧!
2
根本优化
CUDA和PyTorch版本
我举行该测试是想知道使用CUDA 11.8或CUDA 12.1之间是否存在差异,以及在不同版本的PyTorch(始终在2.0以上)之间可能存在的差异。
测试结果:
结论:
真令人失望,它们的性能没什么区别。差异是如此之小,大概如果我做更多的测试,这一差异可能会消失。
何时使用:关于该使用哪个版本,我仍旧有一个理论:CUDA版本11.8发布的时间更长,理论上讲,该版本的库和应用步伐性能会优于更新的版本。另一方面,对于PyTorch而言,版本越新,它应该提供的功能也就越多,包含的bug也就越少。因此,即使只是心理作用,我也会对峙选择CUDA 11.8 + PyTorch 2.2.0。
留意力机制
已往,留意机制必须通过安装xFormers或FlashAttention等库来举行优化。
如果你好奇为什么本文没有提及上述优化,那是因为已无须要。自PyTorch 2.0发布以来,通过各种实现(如上文中提到的这两种),以上算法的优化已经被集成到库内里。PyTorch会根据输入和正在使用的硬件举行恰当的实现。
FP16
默认环境下,Stable Diffusion XL使用32 bit浮点格式(FP32)来表示其所处理和实行盘算的数字。
一个显而易见的问题:能否低落精度?答案是肯定的。通过使用参数torch_dtype=torch.float16,模型会以半精度浮点格式(FP16)加载到内存中。为了避免不绝举行这种转换,我们可以直接下载以FP16格式分发的模型变体。只需包括variant='fp16'参数即可。
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 测试结果:
<左右滑动查看更多图片>
结论:
通过使用半精度的数字,内存占用大幅低落,盘算速度也显著提高。
唯一的“不敷”是生成图像质量的低落,但实际上险些不可能看到任何区别,因为FP16足够了。
别的,多亏了variant='fp16' 参数,我们节省了磁盘空间,因为该变体占用的空间只有原来的一半(5GB 而不是 10GB)。
何时使用:随时可用。
TF32
TensorFloat-32是介于FP32和FP16之间的一种格式,以使一些NVIDIA显卡(如A100或H100)来使用张量焦点实行盘算。它使用与FP32相同的bit来表示指数,使用与FP16相同的bit来表示小数部分。
尽管在我们的测试平台(RTX 3090)中无法使用此格式举行盘算,但出乎意料的是,有一些非常奇特的事发生。
有两个属性用于激活此数字格式:torch.backends.cudnn.allow_tf32(默认环境下已激活)和torch.backends.cuda.matmul.allow_tf32(应手动激活)。第一个属性启用了由cuDNN实行的卷积操作中的TF32,而第二个属性则启用了矩阵乘法操作中的TF32。
torch.backends.cudnn.allow_tf32属性默认启用,与你的显卡是什么无关,如许的设定有点奇怪。如果我们将该属性禁用,对其赋值False ,让我们看看会发生什么。
- torch.backends.cudnn.allow_tf32 = False# it's already disabled by default# torch.backends.cuda.matmul.allow_tf32 = Falsepipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 另外,出于好奇,我还使用启用了TF32的NVIDIA A100显卡举行了测试。
- # it's already activated by default
- # torch.backends.cudnn.allow_tf32 = True
- torch.backends.cuda.matmul.allow_tf32 = True
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- ).to('cuda')
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 衡量:要使用TF32,必须禁用FP16格式,因此我们无法使用 torch_dtype=torch.float16 或 variant='fp16' 。
测试结果:
结论:
在使用RTX 3090 时,如果禁用 torch.backends.cudnn.allow_tf32 属性,内存占用会淘汰 7%。为什么呢?我不知道,原则上讲,我认为这可能是一个 bug,因为在不支持TF32的显卡上启用TF32毫无意义。
使用A100显卡时,使用FP16可以或许显著淘汰推理时间和内存占用。就像在RTX 3090上一样,通过禁用torch.backends.cudnn.allow_tf32属性可以或许进一步淘汰内存占用。至于使用TF32,则介于FP32和FP16之间,它无法逾越 FP16。
何时使用:对于不支持TF32的显卡,明智的选择显然是禁用默认启用的属性。在使用A100时,如果可以使用FP16,就不值得使用TF32。
3
Pipeline优化
以下优化方法改进了pipeline以改善某些方面的性能。
前三个优化改进了何时将Stable Diffusion的不同组件加载到内存中,以便它们不会同时加载。以上技术实现了淘汰内存使用量的目标。
当由于显卡和内存限定而需要这些优化时,请使用这些优化方法。如果在Linux上收到RuntimeError: CUDA out of memory报错,本节内容就是你所需要的。在Windows上,默认环境下存在虚拟内存(共享GPU内存),尽管很难出现这种报错,但推理时间会呈指数级增长,因此本节也是你需要关注的内容。
至于本节中的末了三个优化方法,它们以不同方式优化pipeline的库,以尽可能淘汰推理时间。
Model CPU Offload
Model CPU Offload优化方法来自 accelerate 库。当实行pipeline时,全部模型都会加载到内存中。通过这一优化,我们让pipeline每次只在需要时将模型移入内存。在pipeline的源代码(https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py#L201)中可以找到这个次序,在Stable Diffusion XL的环境下,我们会找到以下代码:
- model_cpu_offload_seq = "text_encoder->text_encoder_2->image_encoder->unet->vae"
复制代码 实现Model CPU Offload的代码非常简单:
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- pipe.enable_model_cpu_offload()
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 紧张提醒:与其他优化不同,我们不应该使用 to('cuda') 将pipeline移至显卡上。当须要时,该优化会举行主动处理。(感谢Terrence Goh的提醒)
- pipe = AutoPipelineForText2Image.from_pretrained(
- # ...
- ).to('cuda')
复制代码 测试结果:
结论:
使用这一技术将取决于我们拥有的显卡。如果显卡有6-8GB内存,这种优化将有所帮助,因为内存使用量正好淘汰了一半。
至于推理时间,不会受到太大影响以至于成为一个问题。
何时使用:需要淘汰内存消耗时使用。由于消耗最多内存的组件是噪声预测器(U-Net),我们无法通过应用优化到VAE来进一步淘汰内存消耗。
Sequential CPU Offload
这种优化与Model CPU Offload类似,只是更加激进。它不是将整个组件移入内存,而是将每个组件的子模块移入内存。例如,该优化不是将整个U-Net模型移入内存,而是在使用时移动特定部分,以尽可能少地占用内存。这意味着,如果噪声预测器必须在50步内清理一个张量,那么子模块必须进出内存50次。
同样只需添加一行代码:
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- )
- pipe.enable_sequential_cpu_offload()
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 紧张提示:使用模型CPU卸载时,记得不要在pipeline中使用 to('cuda')。
测试结果:
结论:
该优化会考验我们的耐心。为了尽可能淘汰内存使用,推理时间会大幅增加。
何时使用:如果你需要不超过4GB的内存,那么将该优化与VAE FP16 fix或Tiny VAE一起使用是你的唯一选择,但如果你不需要这么做,那再好不外。
批处理
该技术是从文章“How to implement Stable Diffusion(https://www.felixsanz.dev/articles/how-to-implement-stable-diffusion)”和“PixArt-α with less than 8GB VRAM(https://www.felixsanz.dev/articles/pixart-a-with-less-than-8gb-vram)”中获取的学习结果,我才了解到这一技术。通过这些文章,你会找到一些我将使用但不再解释的部分代码信息。
这有关批处理中的实行组件。其背后的理念与“Model CPU Offload”技术类似,问题在于官方pipeline实现并未最大程度地优化内存使用。当你启动pipeline,只想获取文本编码器时却做不到。
也就是说,我们应该可以或许如许做:
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- unet=None,
- vae=None,
- ).to('cuda')
复制代码 但实际上却不能如许做。当你启动pipeline时,它需要访问U-Net模型设置 (self.unet.config.*),以及VAE设置 (self.vae.config.*)。
因此(并且无需创建分支),我们将手动使用文本编码器,而不依赖于pipeline。
第一步是复制pipeline中的encode_prompt函数,并对其举行调解/简化。
该函数负责对提示举行词元化并处理,以获取已转换的嵌入张量。你可以在“How to implement Stable Diffusion”中找到对这一过程的解释。
- def encode_prompt(prompts, tokenizers, text_encoders):
- embeddings_list = []
- for prompt, tokenizer, text_encoder in zip(prompts, tokenizers, text_encoders):
- cond_input = tokenizer(
- prompt,
- max_length=tokenizer.model_max_length,
- padding='max_length',
- truncation=True,
- return_tensors='pt',
- )
- prompt_embeds = text_encoder(cond_input.input_ids.to('cuda'), output_hidden_states=True)
- pooled_prompt_embeds = prompt_embeds[0]
- embeddings_list.append(prompt_embeds.hidden_states[-2])
- prompt_embeds = torch.concat(embeddings_list, dim=-1)
- negative_prompt_embeds = torch.zeros_like(prompt_embeds)
- negative_pooled_prompt_embeds = torch.zeros_like(pooled_prompt_embeds)
- bs_embed, seq_len, _ = prompt_embeds.shape
- prompt_embeds = prompt_embeds.repeat(1, 1, 1)
- prompt_embeds = prompt_embeds.view(bs_embed * 1, seq_len, -1)
- seq_len = negative_prompt_embeds.shape[1]
- negative_prompt_embeds = negative_prompt_embeds.repeat(1, 1, 1)
- negative_prompt_embeds = negative_prompt_embeds.view(1 * 1, seq_len, -1)
- pooled_prompt_embeds = pooled_prompt_embeds.repeat(1, 1).view(bs_embed * 1, -1)
- negative_pooled_prompt_embeds = negative_pooled_prompt_embeds.repeat(1, 1).view(bs_embed * 1, -1)
- return prompt_embeds, negative_prompt_embeds, pooled_prompt_embeds, negative_pooled_prompt_embeds
复制代码 接下来,我们实例化所需的全部组件和模型。我们还需要垃圾收集器 (gc)。
- import gc
- from transformers import CLIPTokenizer, CLIPTextModel, CLIPTextModelWithProjection
- # ...
- tokenizer = CLIPTokenizer.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- subfolder='tokenizer',
- )
- text_encoder = CLIPTextModel.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- subfolder='text_encoder',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- tokenizer_2 = CLIPTokenizer.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- subfolder='tokenizer_2',
- )
- text_encoder_2 = CLIPTextModelWithProjection.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- subfolder='text_encoder_2',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
复制代码 现在我们需要把这两部分组合起来。我们调用encode_prompt函数,并将相同的提示通报给第一个文本编码器和第二个文本编码器,并为其通报组件以供使用。
- with torch.no_grad():
- for generation in queue:
- generation['embeddings'] = encode_prompt(
- [generation['prompt'], generation['prompt']],
- [tokenizer, tokenizer_2],
- [text_encoder, text_encoder_2],
- )
复制代码 得到的张量作为结果存储在变量中以供后续使用。
由于我们已经处理了全部提示,可以从内存中删除这些组件:
- del tokenizer, text_encoder, tokenizer_2, text_encoder_2
- gc.collect()
- torch.cuda.empty_cache()
复制代码 现在,让我们创建一个只能访问U-Net和VAE的pipeline,无需实例化文本编码器来节省内存。
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- tokenizer=None,
- text_encoder=None,
- tokenizer_2=None,
- text_encoder_2=None,
- ).to('cuda')
复制代码 预热:由于每个部分都是分开的,这个测试的预热有点复杂。尽管如此,我们将使用以下代码来预热U-Net模型:
- for generation in queue:
- pipe(
- prompt_embeds=generation['embeddings'][0],
- negative_prompt_embeds =generation['embeddings'][1],
- pooled_prompt_embeds=generation['embeddings'][2],
- negative_pooled_prompt_embeds=generation['embeddings'][3],
- output_type='latent',
- )
复制代码 我们使用pipeline来处理上一步保存的嵌入张量。请记着,在这一部分中,pipeline创建了一个布满噪音的张量,并在50步中对其举行清理(同时受到我们的嵌入向量的引导)。
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- generation['latents'] = pipe(
- prompt_embeds=generation['embeddings'][0],
- negative_prompt_embeds =generation['embeddings'][1],
- pooled_prompt_embeds=generation['embeddings'][2],
- negative_pooled_prompt_embeds=generation['embeddings'][3],
- generator=generator,
- output_type='latent',
- ).images # We do not access images[0], but the entire tensor
复制代码 正如你所见,我们指示pipeline返回潜伏空间中的张量(output_type='latent')。如果不如许做,VAE将被加载到内存中以返回图像,这将导致两个模型同时占用资源。以是,就像我们之前删除文本编码器一样,我们先删除U-Net模型。
- del pipe.unet
- gc.collect()
- torch.cuda.empty_cache()
复制代码 现在,我们将存储的无噪声张量转换为图像:
- pipe.upcast_vae()
- with torch.no_grad():
- for i, generation in enumerate(queue, start=1):
- generation['latents'] = generation['latents'].to(next(iter(pipe.vae.post_quant_conv.parameters())).dtype)
- image = pipe.vae.decode(
- generation['latents'] / pipe.vae.config.scaling_factor,
- return_dict=False,
- )[0]
- image = pipe.image_processor.postprocess(image, output_type='pil')[0]
- image.save(f'image_{i}.png')
复制代码 VAE(FP32):在Stable Diffusion XL中,我们用pipe.upcast_vae()来保持VAE为FP32格式,因为在FP16下它无法正常工作。
此循环负责将处于潜伏空间的张量解码,以将其转换为图像空间。然后,使用 pipe.image_processor.postprocess方法,将其转换为图像并保存。
测试结果:
结论:
这是我决定撰写这篇文章的原因之一。推理时间没有受到影响的环境下,我们将内存占用淘汰了一半。现在,甚至可以使用一张只有6GB内存的显卡来生成图像。
何时使用:虽然Model CPU Offload只是多了一行代码,但推理时间有所增加。因此,如果你不介意写更多的代码,使用这种技术,你将拥有绝对的控制权,并获得更好的性能。你还可以使用专家去噪器集成(Ensemble of Expert Denoisers)方法添加精粹模型,而内存消耗将保持不变。
Stable Fast
Stable Fast项目可以或许通过一系列技术来加快任何扩散模型(如使用增强版本的torch.jit.trace 举行跟踪模型、xFormers、高级的Channels-last-memory-format实现等)。事实上,他们做得非常出色。
他们答应的结果是创下推理时间的记载,远远超过torch.compile API,并赶上TensorRT。最有趣的是,由于这些是运行时优化,就无需等待数非常钟举行初始编译。
要集成Stable Fast,首先需要安装项目库,还有Triton,以及与我们正在使用的PyTorch版本兼容的xFormers版本。
- pip install stable-fast
- pip install torch torchvision triton xformers --index-url https://download.pytorch.org/whl/cu118
复制代码 然后,使用Stable Fast修改脚本以导入并启用这些库:
- import xformers
- import triton
- from sfast.compilers.diffusion_pipeline_compiler import (compile, CompilationConfig)
- # ...
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- config = CompilationConfig.Default()
- config.enable_xformers = True
- config.enable_triton = True
- config.enable_cuda_graph = True
- pipe = compile(pipe, config)
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 别的,该项目还因其浅易性而脱颖而出,只需几行代码就能运行。现在让我们看看它是否符合盼望。
测试结果:
<左右滑动查看更多图片>
结论:
它超出了盼望值。你可以看到该项目背后的出色工作。
最引人注目标是速度的提高。我们生成的第一张图像需要较长时间(19秒),但如果在这些测试中举行了预热,就不紧张了。
内存使用量有所增加,但仍旧相称可控。
至于视觉结果,构图略有变革。在某些图像中,某些元素的质量甚至已经提高,以是……眼见为实。
何时使用:我想说,随时都可用。
DeepCache
DeepCache项目称,要成为用户可以实行的最佳优化方法之一,险些没有什么缺点,且易于添加。它使用缓存系统来重用高级别函数,并以更高效的方式更新低级别函数。
首先,我们安装所需的库:
然后,我们将以下代码集成到我们的pipeline中:
- from DeepCache import DeepCacheSDHelper
- # ...
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- helper = DeepCacheSDHelper(pipe=pipe)
- helper.set_params(cache_interval=3, cache_branch_id=0)
- helper.enable()
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 有两个参数可以修改,以实现更高的速度,尽管会在结果中引入更大的质量丧失。
cache_interval=3:指定缓存在几个步数后更新一次。
cache_branch_id=0:指定神经网络负责实行缓存过程的分支(按降序排列,0 是第一层)。
让我们看看使用默认保举参数的结果。
测试结果:
<左右滑动查看更多图片>
结论:
哇!在略微牺牲内存使用量的环境下,推理时间可以淘汰一半以上。
至于图像质量,你可能已经留意到变革很大,且不幸的是,变得更糟了。根据图像的风格,这一点可能更紧张或不那么紧张,但这个劣势简直存在(在物体图像中似乎并没有太大影响)。
增加cache_branch_id的值似乎可以提供更高的视觉质量,尽管可能还不够。
何时使用:由于DeepCache大幅低落了推理时间,理所固然地会轻微低落图像质量。毫无疑问,当用于测试提示或参数时,这是一个非常有用的优化方法,不外,当你希望输出更好的图像质量时就不实用了。
TensorRT
TensorRT是NVIDIA推出的高性能推理优化器和运行时环境,旨在加快神经网络推理过程。
但我们从一开始就碰到了问题。我们的测试使用的是diffusers库中的pipeline,目前还没有与Stable Diffusion XL兼容的TensorRT pipeline。针对Stable Diffusion 2.x(txt2img、img2img 或 inpainting)有社区提供的pipeline。我也看到一些针对Stable Diffusion 1.x的pipeline,但正如我所说的,都不实用于SDXL。
另一方面,在HuggingFace上,我们可以找到官方的stabilityai/stable-diffusion-xl-1.0-tensorrt库。此中包含了使用TensorRT实行推理过程的阐明,但不幸的是,它使用的脚本非常复杂,险些不可能适应我们的测试。
由于使用的脚本甚至没有相同的调度器(Euler),因此结果看起来会有很大不同。尽管如此,我尽可能地重用了许多数值,包括正向提示、无负向提示、相同的种子、相同的CFG值和相同的图像尺寸。
以下是脚本使用阐明,便于你举行深入研究:
- # 克隆整个仓库或今后文件夹下载文件# https://github.com/rajeevsrao/TensorRT/tree/release/8.6/demo/Diffusion# 像往常一样创建并激活一个虚拟环境python -m venv .venv## Unixsource .venv/bin/activate## Windows.venv\Scripts\activate# 安装所需的库pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
- pip install transformers accelerate diffusers cuda-python nvtx onnx colored scipy polygraphypip install --pre --extra-index-url https://pypi.nvidia.com tensorrtpip install --pre --extra-index-url https://pypi.ngc.nvidia.com onnx_graphsurgeon# 可以使用以下行验证 TensorRT 是否正确安装python -c "import tensorrt; print(tensorrt.__version__)"# 9.3.0.post12.dev1# 举行推理python demo_txt2img_xl.py "macro shot of a bee collecting nectar from lavender flowers"
复制代码 测试结果:
<左右滑动查看更多图片>
结论:
颠末模型准备后(约半个小时,仅在第一次准备时需要),推理过程似乎加快了很多,每张图像的生成时间仅为 8 秒,而未经优化的代码则需要14秒。我无法确定内存消耗环境,因为TensorRT使用了不同的API。
至于图像的质量……在初始状态下看起来很惊艳。
何时使用:如果你可以将TensorRT集成到你的流程中,可以尝试一下。看起来是一个不错的优化方法,值得一试。
4
组件优化
这些优化措施对于 Stable Diffusion XL 的各个组件举行了修改,从而通过多种不同方式提拔其性能。每个单独的改进可能只会带来一点点提拔,但将它们全部团结起来,就会产生显著影响。
torch.compile
使用PyTorch 2或更高版本时,我们可以通过 [torch.compile] API(https://pytorch.org/docs/stable/generated/torch.compile.html) 对模型举行编译,以获得更好的性能。尽管编译需要肯定时间,但后续调用将受益于额外的速度提拔。
在从前的PyTorch版本中,也可以通过torch.jit.trace API使用跟踪技术对模型举行编译。这种即时(just-in-time / JIT)运行时的编译方法不如新方法高效,因此我们可以忽略此API。
在torch.compile方法中,mode参数担当以下值:default、reduce-overhead、max-autotune和max-autotune-no-cudagraphs。理论上它们是不同的,但我没有看到任何区别,因此我们将使用 reduce-overhead。
Windows操作系统如下所示:
- RuntimeError: Windows not yet supported for torch.compile
复制代码- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- pipe.unet = torch.compile(pipe.unet, mode='reduce-overhead', fullgraph=True)
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 我们将评估编译模型所需的时间以及每一个连续生成所需的时间。
测试结果:
结论:
一项能很快带来成效的简单优化。
何时使用:当生成的图片足够多,值得承受编译时间时就可以使用这种技术。
OneDiff
OneDiff是一个适配了Diffusers、ComfyUI和Stable Diffusion webUI应用框架的优化库。其名字的字面意思是:一行代码就能加快扩散模型。
该库接纳了量化、留意力机制改进和模型编译等技术。
安装该加快引擎只需添加几个库,但如果你使用的是其他CUDA版本,或者想要使用不同的安装方法,可以参考技术文档举行安装(https://github.com/siliconflow/onediff#1-install-oneflow)。
- pip install --pre oneflow -f https://github.com/siliconflow/oneflow_releases/releases/expanded_assets/community_cu118
- pip install --pre onediff
复制代码 如果你使用的是Windows或macOS,则必须本身编译该库。
- RuntimeError: This package is a placeholder. Please install oneflow following the instructions in https://github.com/Oneflow-Inc/oneflow#install-oneflow
复制代码 OneDiff创建者还提供了一个企业版,答应额外提供20%的速度(甚至更多),尽管我无法验证这一点,而且他们也没有提供太多细节。
类似于torch.compile,所需的代码只有一行,可以改变pipe.unet 的行为。
- import oneflow as flow
- from onediff.infer_compiler import oneflow_compile
- # ...
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- pipe.unet = oneflow_compile(pipe.unet)
- generator = torch.Generator(device='cuda')
- with flow.autocast('cuda'):
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 让我们看看它是否符合预期。
测试结果:
<左右滑动查看更多图片>
结论:
OneDiff轻微改变了图像布局,但这是一个有利的改变。在interior图像中,我们可以看到有一个bug通过变成一个阴影的方式被修复了,。
编译时间非常短,比torch.compile快得多。
OneDiff使推理时间缩短了45%,超过了全部竞品的优化速度(Stable Fast、TensorRT 和 torch.compile)。
令人惊奇的是(与Stable Fast不同),其内存使用量并没有增加。
何时使用:发起一直使用。它提高了生成结果的视觉质量,推理时间险些减半,唯一的代价是在编译时需要轻微等待。非常美丽的工作!
Channels-last内存格式
Channels-last内存格式组织数据,用以将颜色通道(color channel)储存在张量的末了一个纬度中。
默认环境下,张量接纳的是NCHW格式,对应着以下四个维度:
- N(数量):同时生成多少张图像(批大小)。
- C(通道):图像具有多少个通道。
- H(高度):图像的高度(以像素为单位)。
- W(宽度):图像的宽度(以像素为单位)。
相比之下,使用这种技术,以NHWC格式将张量数据重新排序,将通道数放在末了。
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- pipe.unet.to(memory_format=torch.channels_last)
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 使用以下命令,检查张量是否已重新排序(将其放置在重排序之前和之后):
- print(pipe.unet.conv_out.state_dict()['weight'].stride())
复制代码 虽然channels-last内存格式在某些环境下可能会提高效率并淘汰内存使用,但它不兼容某些神经网络,甚至可能会低落性能。因此,我们可以将其清除。
测试结果:
结论:
在Stable Diffusion XL中,U-Net模型似乎并没有从这种优化中受益,但即使如许,知识也不会占用太多空间对吧?
何时使用:永久别用。
FreeU
FreeU是第一个也是唯一一个不改善推理时间或内存使用环境,而改善图像结果质量的优化技术。
这种技术平衡了U-Net架构中两个关键元素的贡献:skip connections(跳跃连接,引入高频细节)和backbone feature maps(主干特征图,提供语义信息)。
换句话说,FreeU 抵消了图像中不自然细节的引入,提供了更真实的视觉结果。
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- pipe.enable_freeu(s1=0.9, s2=0.2, b1=1.3, b2=1.4)
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 你可以调解这些值,尽管它们是Stable Diffusion XL的保举值。
如需要更多信息,可查看项目:https://github.com/ChenyangSi/FreeU#parameters
测试结果:
<左右滑动查看更多图片>
结论:
我之前从未尝试过FreeU,但这个结果给我留下了深刻印象。尽管图片的布局与原始输入有所不同,但我认为它们更老实于提示,并专注于提供最佳视觉质量,而不会陷入细节的琐碎之中。
同时我发现这个技术也有一个问题,即图片失去了一些连贯性。例如,沙发顶部有一盆植物,蜜蜂有三只翅膀。这表明尽管图片视觉上引人注目,但可能缺乏肯定的逻辑划一性或实际感。
何时使用:当我们想要获得更具创意、更高视觉质量的结果时使用(尽管这也取决于我们所追求的图像风格)。
VAE FP16 fix
正如在批处理优化中看到的,Stable Diffusion XL中默认包含的VAE模型无法在FP16格式下运行。在解码图像之前,pipeline会实行一个方式,以使逼迫模型以FP32格式工作 (pipe.upcast_vae())。而在之前的FP16优化中,将模型以FP32格式运行是一种不须要的资源浪费。
用户madebyollin(也是TAESD的创作者,稍后我们将看到)已经创建了这个模型的一个修复版,使其可以在FP16格式下运行。
我们只需导入这个VAE并替换原始版本:(https://huggingface.co/madebyollin/sdxl-vae-fp16-fix)
- from diffusers import AutoPipelineForText2Image, AutoencoderKL
- # ...
- vae = AutoencoderKL.from_pretrained(
- 'madebyollin/sdxl-vae-fp16-fix',
- use_safetensors=True,
- torch_dtype=torch.float16,
- ).to('cuda')
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- vae=vae,
- ).to('cuda')
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 测试结果:
<左右滑动查看更多图片>
结论:
图像视觉质量与原始模型险些相同,没有质量丧失。
内存使用量淘汰了将近15%,对于这一简单改进来说是相称不错的结果。
何时使用:可以一直使用,除非你更倾向于使用Tiny VAE优化(https://www.felixsanz.dev/articles/ultimate-guide-to-optimizing-stable-diffusion-xl#tiny-vae)。
VAE slicing
当同时生成多张图像时(增加批大小),VAE会同时解码全部张量(并行)。这会大大增加内存使用量。为避免这种环境,可以使用VAE切片技术逐个解码张量(串行)。这与我们在批处理优化中手动操作的方式险些相同。
举例来说,无论使用的批大小为1、2、8还是32,VAE的内存消耗都会保持不变,与此相对应的是,会有一个险些不可察觉的少量时间丧失。
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- pipe.enable_vae_slicing()
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 批大小为1时,这一优化技术没有任何作用。由于在测试中使用的批处理大小为1,因此我们将跳过测试结果,直接给出结论。
结论:
这一优化技术试图在增加批大小时淘汰内存使用,而批大小的增加恰恰是导致内存使用增加的最关键因素。因此,这种优化技术本身存在抵牾。
何时使用:发起仅在有一个完善的流程,同时生成多张图像,并且VAE实行是瓶颈时使用这种优化技术。换句话说,适合使用这种技术的环境很少。
VAE tiling
当生成高分辨率图像(如4K/8K)时,VAE往往成为瓶颈。解码这么大尺寸的图像不仅需要耗费几分钟时间,而且还会消耗大量内存。经常会碰到如下问题:torch.cuda.OutOfMemoryError: CUDA out of memory.
通过这种优化技术,张量被分割成几部分(就像它们是切片一样),然后逐个解码,末了再重新连接起来形成图像。如许,VAE不必一次性解码全部内容。
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- pipe.enable_vae_tiling()
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- height=4096,
- width=4096,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 因为图像被分割成了多个部分,然后再重新连接起来,以是连接处可能会出现一些微小的颜色差异或瑕疵。然而,通常环境下这种差异都不太常见或不容易被察觉。
测试结果:
结论:
这种优化技术相对简单易懂:如果需要生成非常高分辨率的图像,而你的显卡内存不敷,这将是实现这一目标的唯一选择。
何时使用:永久不要使用这种优化技术。非常高分辨率的图像存在缺陷,因为Stable Diffusion模型并没有针对这种任务举行练习。如果需要增加分辨率,应该使用一个上采样器(upscaler)。
Tiny VAE
在Stable Diffusion XL中使用了一个拥有5000万个参数的32 bit VAE。由于这个组件是可互换的,我们将使用一个名为TAESD的VAE。这个小模型只有100万个参数,是原始VAE的精简版,同时可以或许在16 bit格式下运行。
- from diffusers import AutoPipelineForText2Image, AutoencoderTiny
- # ...
- vae = AutoencoderTiny.from_pretrained(
- 'madebyollin/taesdxl',
- use_safetensors=True,
- torch_dtype=torch.float16,
- ).to('cuda')
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- vae=vae,
- ).to('cuda')
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 牺牲图像质量,以获取更快的速度和更少的内存使用是否值得?
测试结果:
<左右滑动查看更多图片>
结论:
由于Tiny VAE是一个更小的模型,并且可以或许在16 bit格式下运行,因此内存使用量大幅淘汰。
Tiny VAE并没有显著淘汰推理时间。
尽管图像略微改变,尤其是似乎增加了一些对比度和纹理,但我认为这种变革不明显。因此,图像质量的丧失是可以担当的。
何时使用:如果你需要一直淘汰内存使用量,那么发起始终使用Tiny VAE。有了这个模型,甚至可以在不接纳其他优化计谋的环境下,用8GB显卡运行推理过程。即使不需要淘汰内存使用,使用Tiny VAE也是一个不错的选择,因为它似乎没有负面影响。
5
参数优化
在这个种别中,我们将以牺牲图像质量为代价,修改一些参数以获得额外速度,希望这种牺牲不会太大。
Stable Diffusion XL使用Euler作为默认采样器。虽然可能有更快的采样器,但Euler本身已经属于快速采样器范畴,因此将Euler替换为其他采样器并不会带来显著的优化结果。
步数(Step)
接纳默认SDXL,通过50步来清除一个布满噪音的张量。步数越多,噪音清除结果就越好,但推理时间也会相应增加。使用num_inference_steps参数,我们可以指定想要使用的步数。
我们将分别使用30、25、20和15步,来生成一系列图像。我们将使用默认值(50)作为比力基准。
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- num_inference_steps=30,
- generator=generator,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 尽管淘汰步数可以缩短推理时间,但我们更感兴趣的是保持在肯定范围的步数,以尽可能地维持图像的质量和布局。如果我们的图像质量大幅降落,就算节省了大量时间也没故意义。接下来我们来探索步数数量的极限。
测试结果:
<左右滑动查看更多图片>
结论:
肖像照片(Portrait):在15和20步时,质量尚可,但布局有所不同。25步及以上时,我发现图像质量和布局相称不错。
室内照片(Interior):在15步时,仍旧没有到达盼望的布局。在20步时,结果相称不错,但某些元素仍有缺失。因此,我认为至少需要25步。
微距摄影(Macro):在微距摄影中,即使只有15步,细节程度也相称惊人。我不知道应该选哪个步数,因为全部选项都是有效和正确的。
3D图像:在3D风格的图像中,少量步数会导致产生大量缺陷,甚至在某些区域会出现含糊。尽管30步的图像还不错,但我更倾向于使用50步(或者40步)的结果。
总的来说,根据生成的图像风格,可以选择使用更多或更少的步数。但是,在25-30步时可以获得相称不错的质量。如许做可以将推理时间缩短约40%,是一种相称显著的提拔。
何时使用:当测试提示或调解参数,并且想要快速生成图像时,这是一个很好的优化方法。调试完全部参数和提示后,可以增加步数,以获取最高质量的图像。根据详细的使用环境,可以选择是否永久接纳这种优化方法。
禁用 CFG
正如文章“How Stable Diffusion works”所说,无分类器引导(classifier-free guidance)技术负责调解噪音预测器与特定标签之间的距离。
举例来说,假设我们有一个关于汽车的正向提示和一个关于玩具的负向提示。CFG技术可以调解噪音预测器与“汽车”标签之间的距离,使其更靠近“汽车”标签所代表的概念,同时阔别“玩具”标签所代表的概念。如许做可以确保生成的图像更符合“汽车”的特征,而不受“玩具”的影响。这是一种非常有效的控制条件图像生成法。
“How to implement Stable Diffusion(https://www.felixsanz.dev/articles/how-to-implement-stable-diffusion)”一文先容了如何实现CFG技术,并且阐明了它引入需要复制张量的需求:
- # As we're using classifier-free guidance, we duplicate the tensor to avoid making two passes
- # One pass will be for the conditioned values and another for the unconditioned values
- latent_model_input = torch.cat([latents] * 2)
复制代码 这意味着噪音预测器在每步上需要耗费两倍的时间。
在生成图像的早期阶段,启用CFG技术对于获得质量良好且符合我们提示的图像至关紧张。一旦噪音预测器成功地开始产生符合预期的图像,我们就可能不再需要继续使用CFG技术了。在这一优化方法中,我们将探索在图像生成过程中停用CFG技术的影响。
代码很简单,我们创建了一个函数,负责在步数到达指定值后禁用CFG技术(pipe._guidance_scale = 0.0)。别的,停止使用CFG技术后,将不再需要复制张量。
- pipe = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- def callback_dynamic_cfg(pipe, step_index, timestep, callback_kwargs):
- if step_index == int(pipe.num_timesteps * 0.5):
- callback_kwargs['prompt_embeds'] = callback_kwargs['prompt_embeds'].chunk(2)[-1]
- callback_kwargs['add_text_embeds'] = callback_kwargs['add_text_embeds'].chunk(2)[-1]
- callback_kwargs['add_time_ids'] = callback_kwargs['add_time_ids'].chunk(2)[-1]
- pipe._guidance_scale = 0.0
- return callback_kwargs
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = pipe(
- prompt=generation['prompt'],
- generator=generator,
- callback_on_step_end=callback_dynamic_cfg,
- callback_on_step_end_tensor_inputs=['prompt_embeds', 'add_text_embeds', 'add_time_ids'],
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 由于callback_on_step_end参数,在每一步结束时这个函数作为回调函数被实行。我们需要使用callback_on_step_end_tensor_inputs参数,确定我们将在回调函数内部修改的张量。
我们来看看在图像生成过程的末了25%和后半部分(50%),停用CFG技术会发生什么。
测试结果:
<左右滑动查看更多图片>
结论:
正如我们所预期的,在50%的环境下停用CFG,可以使每张图像的推理时间淘汰25%(并非总体时间,因为模型加载时间也计入此中)。这是因为如果使用CFG实行50步操作,模型实际上清理了100个张量。而使用这种优化方法,模型在前25步清理了50个张量,在接下来的25步中清理了一半(25个张量)。以是,75/100相称于跳过了25%的工作量。在停用CFG到达75%的环境下,每张图像的推理时间淘汰12.5%。
图像质量似乎有所降落但不太明显。这可能是因为没有使用负向提示,而使用CFG的主要上风就在于可以或许应用负向提示。使用更好的提示肯定会提高质量,但在停用CFG到达75%的环境下,这种影响险些可以忽略不计。
何时使用:当你对图像生成速度要求较高,且可以或许担当肯定程度的质量丧失,那么可以积极接纳这种优化方法(例如,用于测试提示或参数时)。通过稍后停用CFG,可以提高速度而不牺牲质量。
细化模型(Refiner)
那么细化模型呢?虽然我们已经优化了底子模型,但Stable Diffusion XL的一个主要上风是,它还有一个专门细化细节的模型。这个模型显著提高了生成的图像质量。
默认环境下,底子模型使用11.24 GB的内存。当同时使用细化模型时,内存需求增加到了17.38 GB。但要记着,由于它具有相同的组件(除了第一个文本编码器),大多数优化也可以应用于这个模型。
在使用细化模型举行预热时,因为需要预热两个不同的模型,以是会有些复杂。为实现这一点,我们首先从底子模型获取结果,然后将其通过细化模型举行处理:
- for generation in queue:
- image = base(generation['prompt'], output_type='latent').images
- refiner(generation['prompt'], image=image)
复制代码 细化模型有两种不同的使用方式,我们将分别讨论。
专家去噪器集成(Ensemble of Expert Denoisers)
专家去噪器集成是指图像生成的方法,其过程从底子模型开始,末了使用细化模型结束。在整个过程中,不会生成任何图像,而是底子模型在指定命量的步数(总步数的一部分)内清理张量,然后将张量通报给细化模型以完成处理。
可以如许说,它们共同工作以生成结果(底子模型+细化器)。
就代码而言,底子模型在使用denoising_end=0.8参数时,会在处理过程的80%处停止其工作,并且通过 output_type='latent' 返回张量。
细化模型通过image参数接收到这个张量(讽刺的是,它并不是一个图像)。然后,细化模型开始清理这个张量,通过参数denoising_start=0.8假设已经完成了80%的工作。我们再次指定了整个处理过程的步数 (num_inference_steps),以便它盘算剩余需要清理的步数。也就是说,如果我们使用50步,并且在80%处举行了变革,那么底子模型将会在前40步清理张量,细化模型将为剩余的10步举行精细化处理,以完善剩余细节。
- from diffusers import AutoPipelineForText2Image, AutoPipelineForImage2Image
- # ...
- base = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- refiner = AutoPipelineForImage2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-refiner-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = base(
- prompt=generation['prompt'],
- generator=generator,
- num_inference_steps=50,
- denoising_end=0.8,
- output_type='latent',
- ).images # Remember that here we do not access images[0], but the entire tensor
- image = refiner(
- prompt=generation['prompt'],
- generator=generator,
- num_inference_steps=50,
- denoising_start=0.8,
- image=image,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 我们将在50、40、30和20步时生成图像,并在处理到0.9和0.8时切换到细化模型。
作为参考,我们还将包括在全部比力中作为底子的图像(只使用底子模型,共50步)。
测试结果:
<左右滑动查看更多图片>
结论:
毫无疑问,使用细化模型显然会极大地改善结果。
那么应该在何时使用细化模型来处理图像呢?显然,可以看出在 0.9 处得到的结果比在 0.8 处更好,因为细化模型旨在优化终极细节,不应该用于改变图像布局。
我认为,无论步数是多少,细化模型似乎都能提供非常高的视觉质量结果。唯一会改变的是图像布局,但即使只有30步,视觉质量也很高。
同时,我们还要考虑到当步数淘汰到40以下时,所需时间会显著淘汰。
何时使用:每当我们想要使用细化模型来提高图像的视觉质量时,就可以使用它。至于参数,只要我们不追求最佳的质量,就可以使用30或40步。固然始终要在0.9处切换到细化模型。
图像到图像(Image-to-image)
在Stable Diffusion XL中,经典的图像到图像(img2img)方法并不新鲜。这种方法是使用底子模型生成完整图像,然后将生成的图像和原始提示一起通报给细化模型,细化模型使用这些条件生成新的图像。
换句话说,在img2img方法中,这两个模型是独立工作的(底子模型->细化模型)。
由于两个过程是独立的,因此相对容易应用本文中的优化方法。尽管如此,代码并没有太大的差异,只是简单地生成了一个图像,并将其用作细化模型的参数。
- from diffusers import AutoPipelineForText2Image, AutoPipelineForImage2Image
- # ...
- base = AutoPipelineForText2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-base-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- refiner = AutoPipelineForImage2Image.from_pretrained(
- 'stabilityai/stable-diffusion-xl-refiner-1.0',
- use_safetensors=True,
- torch_dtype=torch.float16,
- variant='fp16',
- ).to('cuda')
- generator = torch.Generator(device='cuda')
- for i, generation in enumerate(queue, start=1):
- generator.manual_seed(generation['seed'])
- image = base(
- prompt=generation['prompt'],
- generator=generator,
- num_inference_steps=50,
- ).images[0]
- image = refiner(
- prompt=generation['prompt'],
- generator=generator,
- num_inference_steps=10,
- image=image,
- ).images[0]
- image.save(f'image_{i}.png')
复制代码 我们将使用底子模型在50、40、30和20步处生成图像,然后再通过细化模型添加额外的20步和10步的组合。
作为参考,我们还包含了全部比力的底子图像,这张图像是底子模型处理的结果,只举行了50步的处理。
测试结果:
<左右滑动查看更多图片>
结论:
在图像到图像(img2img)模式中,细化模型的表现不如人意。
当我们在底子模型中使用足够的步数时,似乎细化模型被迫向本不需要的部分添加细节。换句话说,这是在多此一举。
另一方面,如果我们在底子模型中使用较少的步数,结果会轻微好一些。这是因为使用如此少的步数,底子模型无法添加细微的细节,为细化模型提供了更大的发挥空间。
同时,我们也必须考虑通过淘汰步数来淘汰时间。如果我们使用太多步数,将会受到显著的丧失。
何时使用:首先要记着的是,使用细化模型的目标是最大化视觉质量。在这种环境下,我们可以增加步数,因此,“专家去噪器集成”方法是最佳选择。我认为,使用少量步数无法获得更好的视觉质量,也不会提高生成速度,与其他方法相比也不具备上风。因此,在图像到图像模式下使用细化模型有其上风,但其上风并不突出。
6
结论
开始写这篇文章时,我并没有想到会深入研究到这个程度。我可以或许明确直接跳到结论部分的读者,同时我也很佩服看了全部优化内容的读者。希望阅读完本文后,读者们可以或许有所所获。
根据目标和可用的硬件,我们需要应用不同的优化方法。让我们以表格的形式,总结全部的优化措施以及它们引入的改进(或丧失)。
理论上来说,“中立”的优化方法是该种别中一个有利的改变,但其可解释性可能有争议,或者仅实用于某些特定用例。
最快速度
使用底子模型团结OneDiff+Tiny VAE+75%处禁用CFG+30步,可以在险些质量无损的环境下实现最短的生成时间,从而到达最快速度。
在拥有RTX 3090显卡的环境下,可以在仅4.0秒的时间内生成图像,内存消耗为6.91 GB,因此甚至可以在具有8 GB内存的显卡上运行。
我们还可以添加DeepCache以进一步加快流程,但问题是,它与禁用CFG优化不兼容,一旦禁用它,终极速度就会增加。
使用相同的设置,A100显卡可以在2.7秒内生成图像。在全新的 H100 上,推理时间仅为2.0秒。
不到4GB的内存使用量
在使用Sequential CPU Offload时,瓶颈在于VAE。因此,将这种优化与VAE FP16 fix或Tiny VAE团结使用,将分别需要2.56 GB和0.68 GB的内存使用量。虽然内存使用量低得离谱,但推理时间会让你觉得有须要去换一张拥有更多内存的新显卡。
不到6GB的内存使用量
通过使用批处理优化,内存使用量低落至5.77 GB,从而使得在拥有6 GB内存的显卡上可以使用 Stable Diffusion XL 生成图像。在这种环境下,没有质量丧失或推理时间增加,如果我们想使用细化模型也没有问题,内存消耗是一样的。
另一个选择是使用Model CPU Offload,这也足以淘汰内存使用,只不外会有一点时间上的丧失。
通过使用VAE FP16 fix或Tiny VAE优化VAE,我们可以轻微加快推理过程。
如果我们想要轻微加快推理过程并在12.9秒内生成图像,我们可以通过使用VAE FP16 fix来实现这一点。而且,如果我们不介意轻微改变结果,我们还可以进一步优化,通过使用Tiny VAE,将内存消耗低落到5.6 GB,生成时间缩短到12.6秒。
请记着,仍旧可以应用其他优化措施来淘汰生成时间。
不到8GB的内存使用量
突破了6 GB的内存限定之后,就可以开启新的优化选择。
正如之前所看到的,使用OneDiff+Tiny VAE将内存使用量降至6.91 GB,并实现了可能的最低推理时间。因此,如果你的显卡至少有8 GB内存,这可能是你的最佳选择。
【OneDiff v0.12.1正式发布(生产环境稳定加快SD&SVD)】本次更新包含以下亮点,欢迎体验新版本:github.com/siliconflow/onediff
* 更新SDXL和SVD的SOTA性能
* 全面支持SD和SVD动态分辨率运行
* 编译/保存/加载HF Diffusers的pipeline
* HF Diffusers的快速LoRA加载和切换
* 加快了InstantID(加快1.8倍)
* 加快了SDXL Lightning
(SDXL E2E Time)
(SVD E2E Time)
更多详情:https://medium.com/@SiliconFlowAI/
其他人都在看
- 800+页免费“大模型”电子书
- LLM推理的极限速度
- 强化学习之父:通往AGI的另一种可能
- 好久不见!OneFlow 1.0全新版本上线
- LLM推理入门指南②:深入解析KV缓存
- 仅需50秒,AI让你的通话彩铃变身短视频
- OneDiffx“图生生”,电商AI图像处理新范式
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |