如何在自己的顯卡上獲得SDXL的最佳質(zhì)量和性能,以及如何選擇適當(dāng)?shù)膬?yōu)化方法和工具,這一讓GenAI用戶倍感困惑的問題,業(yè)內(nèi)一直沒有一份清晰而詳盡的評測報告可供參考。直到全棧開發(fā)者Félix San出手。
在本文中,F(xiàn)élix介紹了相關(guān)SDXL優(yōu)化的方法論、基礎(chǔ)優(yōu)化、Pipeline優(yōu)化以及組件和參數(shù)優(yōu)化。值得一提的是,基于實測表現(xiàn),他高度評價并推薦了由硅基流動研發(fā)的圖片/視頻推理加速引擎OneDiff,“I?just wanted to say that onediff is the fastest of them all! so great job!!(我只想說,OneDiff是所有圖像推理引擎中最快的!非常棒的工作?。。?/p>
由于本文內(nèi)容相當(dāng)扎實,篇幅相對較長,不過,他很貼心地提醒讀者,可以直接翻到末尾看結(jié)論。
感謝Félix出色的專業(yè)評測報告。關(guān)于Stable Diffusion XL優(yōu)化指南,讀這一篇就夠了。
(本文由OneFlow編譯發(fā)布,轉(zhuǎn)載請聯(lián)系授權(quán)。原文:https://www.felixsanz.dev/articles/ultimate-guide-to-optimizing-stable-diffusion-xl)
本文將介紹Stable Diffusion XL優(yōu)化,旨在盡可能減少內(nèi)存使用的同時實現(xiàn)最優(yōu)性能,從而加快圖像生成速度。我們將能夠僅用4GB內(nèi)存生成SDXL圖像,因此可以使用低端顯卡。
由于本文以腳本/開發(fā)為導(dǎo)向,因此將使用Hugging Face的diffusers庫。即便如此,了解不同的優(yōu)化技術(shù)及其相互作用將有助于我們在各種應(yīng)用中充分利用這些技術(shù),例如Automatic1111的Stable Diffusion webUI,尤其是ComfyUI。
本文可能顯得冗長而深奧,但你無需一次性閱讀完畢。我的目標(biāo)是讓讀者了解各種現(xiàn)存的優(yōu)化技術(shù),并教會你何時以及如何使用和組合它們,盡管其中一些技術(shù)本身就已經(jīng)有了實質(zhì)性的差異。
你也可以直接跳到結(jié)論部分,其中包括所有測試的總結(jié)表格,以及針對追求質(zhì)量、速度或內(nèi)存受限條件下運行推理時的建議。
作者 |?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區(qū)域,擁有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),這是一個基礎(chǔ)模板,沒有其他額外內(nèi)容。因為我們將對其進(jìn)行更改,所以PyTorch的版本并不重要,但該模板提供了Ubuntu、Python 3.10和CUDA 11.8作為標(biāo)準(zhǔn)配置。只需兩次點擊并等待30秒,我們就已經(jīng)準(zhǔn)備好所需的一切。
如果你要在本地運行模型,請確保已安裝Python 3.10和CUDA或等效平臺(本文將使用CUDA)。
所有測試都是在虛擬環(huán)境中進(jìn)行的:
創(chuàng)建虛擬環(huán)境
python -m venv .venv
激活虛擬環(huán)境
# 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張圖片,并比較不同的優(yōu)化技術(shù),其中一些我相信你以前可能沒有見過。這些不同主題的圖像是使用stabilityai/stable-diffusion-xl-base-1.0模型生成的,僅使用一個正向提示和一個固定種子。其余參數(shù)將保持默認(rèn)值:無負(fù)向提示,1024x1024尺寸,CFG值為5,步數(shù)為50(采樣步數(shù))。
提示和種子
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,
}])
以下是默認(rèn)生成的圖像:
? ? ? ? ? ?<左右滑動查看更多圖片>
以下是對比測試的結(jié)果:
-
圖像的感知質(zhì)量(希望我是位優(yōu)秀的評判者)。
-
生成每張圖像所需的時間,以及總編譯時間(如果有的話)。
-
使用的最大內(nèi)存量。
每項測試都運行了5次,并使用平均值進(jìn)行比較。
時間測量采用了以下結(jié)構(gòu):
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)
為了找出所使用的最大內(nèi)存量,文件末尾包含以下語句:
max_memory = round(torch.cuda.max_memory_allocated(device='cuda') / 1000000000, 2)
print('Max. memory used:', max_memory, 'GB')
每個測試中包含的內(nèi)容都是所需的最小代碼。雖然每個測試都有自己的結(jié)構(gòu),但代碼大致如下。
# 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優(yōu)化。
其中許多測試使用了diffusers庫中的pipeline,以便抽象復(fù)雜性并使代碼更清晰簡潔。當(dāng)測試需要時,抽象級別會降低,但最終我們一直會使用該庫提供的方法。另外,模型始終以safetensors格式加載,使用use_safetensors=True屬性。
文章中顯示的圖像尺寸最大為512x512,以便瀏覽,但你可以在新標(biāo)簽頁/窗口中打開圖像,查看其原始大小。
你可以在GitHub上的文章存儲庫(github.com/felixsanz/felixsanz_dev)中找到所有單獨的測試文件。
讓我們開始吧!
2
基本優(yōu)化
CUDA和PyTorch版本
我進(jìn)行該測試是想知道使用CUDA 11.8或CUDA 12.1之間是否存在差異,以及在不同版本的PyTorch(始終在2.0以上)之間可能存在的差異。
測試結(jié)果:
結(jié)論:
真令人失望,它們的性能沒什么區(qū)別。差異是如此之小,也許如果我做更多的測試,這一差異可能會消失。
何時使用:關(guān)于該使用哪個版本,我仍然有一個理論:CUDA版本11.8發(fā)布的時間更長,理論上講,該版本的庫和應(yīng)用程序性能會優(yōu)于更新的版本。另一方面,對于PyTorch而言,版本越新,它應(yīng)該提供的功能也就越多,包含的bug也就越少。因此,即使只是心理作用,我也會堅持選擇CUDA 11.8 + PyTorch 2.2.0。
注意力機(jī)制
過去,注意機(jī)制必須通過安裝xFormers或FlashAttention等庫來進(jìn)行優(yōu)化。
如果你好奇為什么本文沒有提及上述優(yōu)化,那是因為已無必要。自PyTorch 2.0發(fā)布以來,通過各種實現(xiàn)(如上文中提到的這兩種),以上算法的優(yōu)化已經(jīng)被集成到庫里面。PyTorch會根據(jù)輸入和正在使用的硬件進(jìn)行適當(dāng)?shù)膶崿F(xiàn)。
FP16
默認(rèn)情況下,Stable Diffusion XL使用32 bit浮點格式(FP32)來表示其所處理和執(zhí)行計算的數(shù)字。
一個顯而易見的問題:能否降低精度?答案是肯定的。通過使用參數(shù)torch_dtype=torch.float16,模型會以半精度浮點格式(FP16)加載到內(nèi)存中。為了避免不斷進(jìn)行這種轉(zhuǎn)換,我們可以直接下載以FP16格式分發(fā)的模型變體。只需包括variant='fp16'參數(shù)即可。
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')
測試結(jié)果:
? ?<左右滑動查看更多圖片>
結(jié)論:
通過使用半精度的數(shù)字,內(nèi)存占用大幅降低,計算速度也顯著提高。
唯一的“不足”是生成圖像質(zhì)量的降低,但實際上幾乎不可能看到任何區(qū)別,因為FP16足夠了。
此外,多虧了variant='fp16' 參數(shù),我們節(jié)省了磁盤空間,因為該變體占用的空間只有原來的一半(5GB 而不是 10GB)。
何時使用:隨時可用。
TF32
TensorFloat-32是介于FP32和FP16之間的一種格式,以使一些NVIDIA顯卡(如A100或H100)來使用張量核心執(zhí)行計算。它使用與FP32相同的bit來表示指數(shù),使用與FP16相同的bit來表示小數(shù)部分。
盡管在我們的測試平臺(RTX 3090)中無法使用此格式進(jìn)行計算,但出乎意料的是,有一些十分奇特的事發(fā)生。
有兩個屬性用于激活此數(shù)字格式:torch.backends.cudnn.allow_tf32(默認(rèn)情況下已激活)和torch.backends.cuda.matmul.allow_tf32(應(yīng)手動激活)。第一個屬性啟用了由cuDNN執(zhí)行的卷積操作中的TF32,而第二個屬性則啟用了矩陣乘法操作中的TF32。
torch.backends.cudnn.allow_tf32屬性默認(rèn)啟用,與你的顯卡是什么無關(guān),這樣的設(shè)定有點奇怪。如果我們將該屬性禁用,對其賦值False ,讓我們看看會發(fā)生什么。
torch.backends.cudnn.allow_tf32 = False
# it's already disabled by default
# torch.backends.cuda.matmul.allow_tf32 = False
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')
另外,出于好奇,我還使用啟用了TF32的NVIDIA A100顯卡進(jìn)行了測試。
# 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')
權(quán)衡:要使用TF32,必須禁用FP16格式,因此我們無法使用 torch_dtype=torch.float16 或 ?variant='fp16' 。
測試結(jié)果:
結(jié)論:
在使用RTX 3090 時,如果禁用 torch.backends.cudnn.allow_tf32 屬性,內(nèi)存占用會減少 7%。為什么呢?我不知道,原則上講,我認(rèn)為這可能是一個 bug,因為在不支持TF32的顯卡上啟用TF32毫無意義。
使用A100顯卡時,使用FP16能夠顯著減少推理時間和內(nèi)存占用。就像在RTX 3090上一樣,通過禁用torch.backends.cudnn.allow_tf32屬性能夠進(jìn)一步減少內(nèi)存占用。至于使用TF32,則介于FP32和FP16之間,它無法超越 FP16。
何時使用:對于不支持TF32的顯卡,明智的選擇顯然是禁用默認(rèn)啟用的屬性。在使用A100時,如果可以使用FP16,就不值得使用TF32。
3
Pipeline優(yōu)化
以下優(yōu)化方法改進(jìn)了pipeline以改善某些方面的性能。
前三個優(yōu)化改進(jìn)了何時將Stable Diffusion的不同組件加載到內(nèi)存中,以便它們不會同時加載。以上技術(shù)實現(xiàn)了減少內(nèi)存使用量的目的。
當(dāng)由于顯卡和內(nèi)存限制而需要這些優(yōu)化時,請使用這些優(yōu)化方法。如果在Linux上收到RuntimeError: CUDA out of memory報錯,本節(jié)內(nèi)容就是你所需要的。在Windows上,默認(rèn)情況下存在虛擬內(nèi)存(共享GPU內(nèi)存),盡管很難出現(xiàn)這種報錯,但推理時間會呈指數(shù)級增長,因此本節(jié)也是你需要關(guān)注的內(nèi)容。
至于本節(jié)中的最后三個優(yōu)化方法,它們以不同方式優(yōu)化pipeline的庫,以盡可能減少推理時間。
Model CPU Offload
Model CPU Offload優(yōu)化方法來自 accelerate 庫。當(dāng)執(zhí)行pipeline時,所有模型都會加載到內(nèi)存中。通過這一優(yōu)化,我們讓pipeline每次只在需要時將模型移入內(nèi)存。在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"
實現(xiàn)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')
重要提醒:與其他優(yōu)化不同,我們不應(yīng)該使用 to('cuda')?將pipeline移至顯卡上。當(dāng)必要時,該優(yōu)化會進(jìn)行自動處理。(感謝Terrence Goh的提醒)
pipe = AutoPipelineForText2Image.from_pretrained(
# ...
).to('cuda')
測試結(jié)果:
結(jié)論:
使用這一技術(shù)將取決于我們擁有的顯卡。如果顯卡有6-8GB內(nèi)存,這種優(yōu)化將有所幫助,因為內(nèi)存使用量正好減少了一半。
至于推理時間,不會受到太大影響以至于成為一個問題。
何時使用:需要減少內(nèi)存消耗時使用。由于消耗最多內(nèi)存的組件是噪聲預(yù)測器(U-Net),我們無法通過應(yīng)用優(yōu)化到VAE來進(jìn)一步減少內(nèi)存消耗。
Sequential CPU Offload
這種優(yōu)化與Model CPU Offload類似,只是更加激進(jìn)。它不是將整個組件移入內(nèi)存,而是將每個組件的子模塊移入內(nèi)存。例如,該優(yōu)化不是將整個U-Net模型移入內(nèi)存,而是在使用時移動特定部分,以盡可能少地占用內(nèi)存。這意味著,如果噪聲預(yù)測器必須在50步內(nèi)清理一個張量,那么子模塊必須進(jìn)出內(nèi)存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')。
測試結(jié)果:
結(jié)論:
該優(yōu)化會考驗我們的耐心。為了盡可能減少內(nèi)存使用,推理時間會大幅增加。
何時使用:如果你需要不超過4GB的內(nèi)存,那么將該優(yōu)化與VAE FP16 fix或Tiny VAE一起使用是你的唯一選擇,但如果你不需要這么做,那再好不過。
批處理
該技術(shù)是從文章“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)”中獲取的學(xué)習(xí)成果,我才了解到這一技術(shù)。通過這些文章,你會找到一些我將使用但不再解釋的部分代碼信息。
這有關(guān)批處理中的執(zhí)行組件。其背后的理念與“Model CPU Offload”技術(shù)類似,問題在于官方pipeline實現(xiàn)并未最大程度地優(yōu)化內(nèi)存使用。當(dāng)你啟動pipeline,只想獲取文本編碼器時卻做不到。
也就是說,我們應(yīng)該能夠這樣做:
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')
但實際上卻不能這樣做。當(dāng)你啟動pipeline時,它需要訪問U-Net模型配置 (self.unet.config.*),以及VAE配置 (self.vae.config.*)。
因此(并且無需創(chuàng)建分支),我們將手動使用文本編碼器,而不依賴于pipeline。
第一步是復(fù)制pipeline中的encode_prompt函數(shù),并對其進(jìn)行調(diào)整/簡化。
該函數(shù)負(fù)責(zé)對提示進(jìn)行詞元化并處理,以獲取已轉(zhuǎn)換的嵌入張量。你可以在“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')
現(xiàn)在我們需要把這兩部分組合起來。我們調(diào)用encode_prompt函數(shù),并將相同的提示傳遞給第一個文本編碼器和第二個文本編碼器,并為其傳遞組件以供使用。
with torch.no_grad():
for generation in queue:
generation['embeddings'] = encode_prompt(
[generation['prompt'], generation['prompt']],
[tokenizer, tokenizer_2],
[text_encoder, text_encoder_2],
)
得到的張量作為結(jié)果存儲在變量中以供后續(xù)使用。
由于我們已經(jīng)處理了所有提示,可以從內(nèi)存中刪除這些組件:
del tokenizer, text_encoder, tokenizer_2, text_encoder_2
gc.collect()
torch.cuda.empty_cache()
現(xiàn)在,讓我們創(chuàng)建一個只能訪問U-Net和VAE的pipeline,無需實例化文本編碼器來節(jié)省內(nèi)存。
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')
預(yù)熱:由于每個部分都是分開的,這個測試的預(yù)熱有點復(fù)雜。盡管如此,我們將使用以下代碼來預(yù)熱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創(chuàng)建了一個充滿噪音的張量,并在50步中對其進(jìn)行清理(同時受到我們的嵌入向量的引導(dǎo))。
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將被加載到內(nèi)存中以返回圖像,這將導(dǎo)致兩個模型同時占用資源。所以,就像我們之前刪除文本編碼器一樣,我們先刪除U-Net模型。
del pipe.unet
gc.collect()
torch.cuda.empty_cache()
現(xiàn)在,我們將存儲的無噪聲張量轉(zhuǎn)換為圖像:
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下它無法正常工作。
此循環(huán)負(fù)責(zé)將處于潛在空間的張量解碼,以將其轉(zhuǎn)換為圖像空間。然后,使用 pipe.image_processor.postprocess方法,將其轉(zhuǎn)換為圖像并保存。
測試結(jié)果:
結(jié)論:
這是我決定撰寫這篇文章的原因之一。推理時間沒有受到影響的情況下,我們將內(nèi)存占用減少了一半?,F(xiàn)在,甚至可以使用一張只有6GB內(nèi)存的顯卡來生成圖像。
何時使用:雖然Model CPU Offload只是多了一行代碼,但推理時間有所增加。因此,如果你不介意寫更多的代碼,使用這種技術(shù),你將擁有絕對的控制權(quán),并獲得更好的性能。你還可以使用專家去噪器集成(Ensemble of Expert Denoisers)方法添加精煉模型,而內(nèi)存消耗將保持不變。
Stable Fast
Stable Fast項目能夠通過一系列技術(shù)來加速任何擴(kuò)散模型(如使用增強(qiáng)版本的torch.jit.trace 進(jìn)行跟蹤模型、xFormers、高級的Channels-last-memory-format實現(xiàn)等)。事實上,他們做得非常出色。
他們承諾的結(jié)果是創(chuàng)下推理時間的記錄,遠(yuǎn)遠(yuǎn)超過torch.compile API,并趕上TensorRT。最有趣的是,由于這些是運行時優(yōu)化,就無需等待數(shù)十分鐘進(jìn)行初始編譯。
要集成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修改腳本以導(dǎo)入并啟用這些庫:
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')
此外,該項目還因其簡易性而脫穎而出,只需幾行代碼就能運行。現(xiàn)在讓我們看看它是否符合期望。
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
它超出了期望值。你可以看到該項目背后的出色工作。
最引人注目的是速度的提高。我們生成的第一張圖像需要較長時間(19秒),但如果在這些測試中進(jìn)行了預(yù)熱,就不重要了。
內(nèi)存使用量有所增加,但仍然相當(dāng)可控。
至于視覺效果,構(gòu)圖略有變化。在某些圖像中,某些元素的質(zhì)量甚至已經(jīng)提高,所以……眼見為實。
何時使用:我想說,隨時都可用。
DeepCache
DeepCache項目稱,要成為用戶可以實施的最佳優(yōu)化方法之一,幾乎沒有什么缺點,且易于添加。它利用緩存系統(tǒng)來重用高級別函數(shù),并以更高效的方式更新低級別函數(shù)。
首先,我們安裝所需的庫:
pip install 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')
有兩個參數(shù)可以修改,以實現(xiàn)更高的速度,盡管會在結(jié)果中引入更大的質(zhì)量損失。
cache_interval=3:指定緩存在幾個步數(shù)后更新一次。
cache_branch_id=0:指定神經(jīng)網(wǎng)絡(luò)負(fù)責(zé)執(zhí)行緩存過程的分支(按降序排列,0 是第一層)。
讓我們看看使用默認(rèn)推薦參數(shù)的結(jié)果。
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
哇!在略微犧牲內(nèi)存使用量的情況下,推理時間可以減少一半以上。
至于圖像質(zhì)量,你可能已經(jīng)注意到變化很大,且不幸的是,變得更糟了。根據(jù)圖像的風(fēng)格,這一點可能更重要或不那么重要,但這個劣勢的確存在(在物體圖像中似乎并沒有太大影響)。
增加cache_branch_id的值似乎可以提供更高的視覺質(zhì)量,盡管可能還不夠。
何時使用:由于DeepCache大幅降低了推理時間,理所當(dāng)然地會稍微降低圖像質(zhì)量。毫無疑問,當(dāng)用于測試提示或參數(shù)時,這是一個非常有用的優(yōu)化方法,不過,當(dāng)你希望輸出更好的圖像質(zhì)量時就不適用了。
TensorRT
TensorRT是NVIDIA推出的高性能推理優(yōu)化器和運行時環(huán)境,旨在加速神經(jīng)網(wǎng)絡(luò)推理過程。
但我們從一開始就遇到了問題。我們的測試使用的是diffusers庫中的pipeline,目前還沒有與Stable Diffusion XL兼容的TensorRT pipeline。針對Stable Diffusion 2.x(txt2img、img2img 或 inpainting)有社區(qū)提供的pipeline。我也看到一些針對Stable Diffusion 1.x的pipeline,但正如我所說的,都不適用于SDXL。
另一方面,在HuggingFace上,我們可以找到官方的stabilityai/stable-diffusion-xl-1.0-tensorrt庫。其中包含了使用TensorRT執(zhí)行推理過程的說明,但不幸的是,它使用的腳本非常復(fù)雜,幾乎不可能適應(yīng)我們的測試。
由于使用的腳本甚至沒有相同的調(diào)度器(Euler),因此結(jié)果看起來會有很大不同。盡管如此,我盡可能地重用了許多數(shù)值,包括正向提示、無負(fù)向提示、相同的種子、相同的CFG值和相同的圖像尺寸。
以下是腳本使用說明,便于你進(jìn)行深入研究:
# 克隆整個倉庫或從此文件夾下載文件
# https://github.com/rajeevsrao/TensorRT/tree/release/8.6/demo/Diffusion
# 像往常一樣創(chuàng)建并激活一個虛擬環(huán)境
python -m venv .venv
## 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 cuda-python nvtx onnx colored scipy polygraphy
pip install --pre --extra-index-url https://pypi.nvidia.com tensorrt
pip 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
# 進(jìn)行推理
python demo_txt2img_xl.py "macro shot of a bee collecting nectar from lavender flowers"
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
經(jīng)過模型準(zhǔn)備后(約半個小時,僅在第一次準(zhǔn)備時需要),推理過程似乎加速了很多,每張圖像的生成時間僅為 8 秒,而未經(jīng)優(yōu)化的代碼則需要14秒。我無法確定內(nèi)存消耗情況,因為TensorRT使用了不同的API。
至于圖像的質(zhì)量……在初始狀態(tài)下看起來很驚艷。
何時使用:如果你可以將TensorRT集成到你的流程中,可以嘗試一下??雌饋硎且粋€不錯的優(yōu)化方法,值得一試。
4
組件優(yōu)化
這些優(yōu)化措施對于 Stable Diffusion XL 的各個組件進(jìn)行了修改,從而通過多種不同方式提升其性能。每個單獨的改進(jìn)可能只會帶來一點點提升,但將它們?nèi)拷Y(jié)合起來,就會產(chǎn)生顯著影響。
torch.compile
使用PyTorch 2或更高版本時,我們可以通過 [torch.compile] API(https://pytorch.org/docs/stable/generated/torch.compile.html) 對模型進(jìn)行編譯,以獲得更好的性能。盡管編譯需要一定時間,但后續(xù)調(diào)用將受益于額外的速度提升。
在以前的PyTorch版本中,也可以通過torch.jit.trace API使用跟蹤技術(shù)對模型進(jìn)行編譯。這種即時(just-in-time / JIT)運行時的編譯方法不如新方法高效,因此我們可以忽略此API。
在torch.compile方法中,mode參數(shù)接受以下值:default、reduce-overhead、max-autotune和max-autotune-no-cudagraphs。理論上它們是不同的,但我沒有看到任何區(qū)別,因此我們將使用 reduce-overhead。
Windows操作系統(tǒng)如下所示:
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')
我們將評估編譯模型所需的時間以及每一個連續(xù)生成所需的時間。
測試結(jié)果:
結(jié)論:
一項能很快帶來成效的簡單優(yōu)化。
何時使用:當(dāng)生成的圖片足夠多,值得承受編譯時間時就可以使用這種技術(shù)。
OneDiff
OneDiff是一個適配了Diffusers、ComfyUI和Stable Diffusion webUI應(yīng)用框架的優(yōu)化庫。其名字的字面意思是:一行代碼就能加速擴(kuò)散模型。
該庫采用了量化、注意力機(jī)制改進(jìn)和模型編譯等技術(shù)。
安裝該加速引擎只需添加幾個庫,但如果你使用的是其他CUDA版本,或者想要使用不同的安裝方法,可以參考技術(shù)文檔進(jìn)行安裝(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創(chuàng)建者還提供了一個企業(yè)版,承諾額外提供20%的速度(甚至更多),盡管我無法驗證這一點,而且他們也沒有提供太多細(xì)節(jié)。
類似于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')
讓我們看看它是否符合預(yù)期。
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
OneDiff稍微改變了圖像結(jié)構(gòu),但這是一個有利的改變。在interior圖像中,我們可以看到有一個bug通過變成一個陰影的方式被修復(fù)了,。
編譯時間非常短,比torch.compile快得多。
OneDiff使推理時間縮短了45%,超過了所有競品的優(yōu)化速度(Stable Fast、TensorRT 和 torch.compile)。
令人驚訝的是(與Stable Fast不同),其內(nèi)存使用量并沒有增加。
何時使用:建議一直使用。它提高了生成結(jié)果的視覺質(zhì)量,推理時間幾乎減半,唯一的代價是在編譯時需要稍微等待。非常漂亮的工作!
Channels-last內(nèi)存格式
Channels-last內(nèi)存格式組織數(shù)據(jù),用以將顏色通道(color channel)儲存在張量的最后一個緯度中。
默認(rèn)情況下,張量采用的是NCHW格式,對應(yīng)著以下四個維度:
-
N(數(shù)量):同時生成多少張圖像(批大?。?。
-
C(通道):圖像具有多少個通道。
-
H(高度):圖像的高度(以像素為單位)。
-
W(寬度):圖像的寬度(以像素為單位)。
相比之下,使用這種技術(shù),以NHWC格式將張量數(shù)據(jù)重新排序,將通道數(shù)放在最后。
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內(nèi)存格式在某些情況下可能會提高效率并減少內(nèi)存使用,但它不兼容某些神經(jīng)網(wǎng)絡(luò),甚至可能會降低性能。因此,我們可以將其排除。
測試結(jié)果:
結(jié)論:
在Stable Diffusion XL中,U-Net模型似乎并沒有從這種優(yōu)化中受益,但即使這樣,知識也不會占用太多空間對吧?
何時使用:永遠(yuǎn)別用。
FreeU
FreeU是第一個也是唯一一個不改善推理時間或內(nèi)存使用情況,而改善圖像結(jié)果質(zhì)量的優(yōu)化技術(shù)。
這種技術(shù)平衡了U-Net架構(gòu)中兩個關(guān)鍵元素的貢獻(xiàn):skip connections(跳躍連接,引入高頻細(xì)節(jié))和backbone feature maps(主干特征圖,提供語義信息)。
換句話說,F(xiàn)reeU 抵消了圖像中不自然細(xì)節(jié)的引入,提供了更真實的視覺結(jié)果。
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')
你可以調(diào)整這些值,盡管它們是Stable Diffusion XL的推薦值。
如需要更多信息,可查看項目:https://github.com/ChenyangSi/FreeU#parameters
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
我之前從未嘗試過FreeU,但這個結(jié)果給我留下了深刻印象。盡管圖片的結(jié)構(gòu)與原始輸入有所不同,但我認(rèn)為它們更忠實于提示,并專注于提供最佳視覺質(zhì)量,而不會陷入細(xì)節(jié)的瑣碎之中。
同時我發(fā)現(xiàn)這個技術(shù)也有一個問題,即圖片失去了一些連貫性。例如,沙發(fā)頂部有一盆植物,蜜蜂有三只翅膀。這表明盡管圖片視覺上引人注目,但可能缺乏一定的邏輯一致性或現(xiàn)實感。
何時使用:當(dāng)我們想要獲得更具創(chuàng)意、更高視覺質(zhì)量的結(jié)果時使用(盡管這也取決于我們所追求的圖像風(fēng)格)。
VAE FP16 fix
正如在批處理優(yōu)化中看到的,Stable Diffusion XL中默認(rèn)包含的VAE模型無法在FP16格式下運行。在解碼圖像之前,pipeline會執(zhí)行一個方式,以使強(qiáng)制模型以FP32格式工作 (pipe.upcast_vae())。而在之前的FP16優(yōu)化中,將模型以FP32格式運行是一種不必要的資源浪費。
用戶madebyollin(也是TAESD的創(chuàng)作者,稍后我們將看到)已經(jīng)創(chuàng)建了這個模型的一個修復(fù)版,使其可以在FP16格式下運行。
我們只需導(dǎo)入這個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')
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
圖像視覺質(zhì)量與原始模型幾乎相同,沒有質(zhì)量損失。
內(nèi)存使用量減少了將近15%,對于這一簡單改進(jìn)來說是相當(dāng)不錯的結(jié)果。
何時使用:可以一直使用,除非你更傾向于使用Tiny VAE優(yōu)化(https://www.felixsanz.dev/articles/ultimate-guide-to-optimizing-stable-diffusion-xl#tiny-vae)。
VAE slicing
當(dāng)同時生成多張圖像時(增加批大?。?,VAE會同時解碼所有張量(并行)。這會大大增加內(nèi)存使用量。為避免這種情況,可以使用VAE切片技術(shù)逐個解碼張量(串行)。這與我們在批處理優(yōu)化中手動操作的方式幾乎相同。
舉例來說,無論使用的批大小為1、2、8還是32,VAE的內(nèi)存消耗都會保持不變,與此相對應(yīng)的是,會有一個幾乎不可察覺的少量時間損失。
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時,這一優(yōu)化技術(shù)沒有任何作用。由于在測試中使用的批處理大小為1,因此我們將跳過測試結(jié)果,直接給出結(jié)論。
結(jié)論:
這一優(yōu)化技術(shù)試圖在增加批大小時減少內(nèi)存使用,而批大小的增加恰恰是導(dǎo)致內(nèi)存使用增加的最關(guān)鍵因素。因此,這種優(yōu)化技術(shù)本身存在矛盾。
何時使用:建議僅在有一個完善的流程,同時生成多張圖像,并且VAE執(zhí)行是瓶頸時使用這種優(yōu)化技術(shù)。換句話說,適合使用這種技術(shù)的情況很少。
VAE tiling
當(dāng)生成高分辨率圖像(如4K/8K)時,VAE往往成為瓶頸。解碼這么大尺寸的圖像不僅需要花費幾分鐘時間,而且還會消耗大量內(nèi)存。經(jīng)常會遇到如下問題:torch.cuda.OutOfMemoryError: CUDA out of memory.
通過這種優(yōu)化技術(shù),張量被分割成幾部分(就像它們是切片一樣),然后逐個解碼,最后再重新連接起來形成圖像。這樣,VAE不必一次性解碼所有內(nèi)容。
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')
因為圖像被分割成了多個部分,然后再重新連接起來,所以連接處可能會出現(xiàn)一些微小的顏色差異或瑕疵。然而,通常情況下這種差異都不太常見或不容易被察覺。
測試結(jié)果:
結(jié)論:
這種優(yōu)化技術(shù)相對簡單易懂:如果需要生成非常高分辨率的圖像,而你的顯卡內(nèi)存不足,這將是實現(xiàn)這一目標(biāo)的唯一選擇。
何時使用:永遠(yuǎn)不要使用這種優(yōu)化技術(shù)。非常高分辨率的圖像存在缺陷,因為Stable Diffusion模型并沒有針對這種任務(wù)進(jìn)行訓(xùn)練。如果需要增加分辨率,應(yīng)該使用一個上采樣器(upscaler)。
Tiny VAE
在Stable Diffusion XL中使用了一個擁有5000萬個參數(shù)的32 bit VAE。由于這個組件是可互換的,我們將使用一個名為TAESD的VAE。這個小模型只有100萬個參數(shù),是原始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')
犧牲圖像質(zhì)量,以獲取更快的速度和更少的內(nèi)存使用是否值得?
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
由于Tiny VAE是一個更小的模型,并且能夠在16 bit格式下運行,因此內(nèi)存使用量大幅減少。
Tiny VAE并沒有顯著減少推理時間。
盡管圖像略微改變,尤其是似乎增加了一些對比度和紋理,但我認(rèn)為這種變化不明顯。因此,圖像質(zhì)量的損失是可以接受的。
何時使用:如果你需要一直減少內(nèi)存使用量,那么建議始終使用Tiny VAE。有了這個模型,甚至可以在不采用其他優(yōu)化策略的情況下,用8GB顯卡運行推理過程。即使不需要減少內(nèi)存使用,使用Tiny VAE也是一個不錯的選擇,因為它似乎沒有負(fù)面影響。
5
參數(shù)優(yōu)化
在這個類別中,我們將以犧牲圖像質(zhì)量為代價,修改一些參數(shù)以獲得額外速度,希望這種犧牲不會太大。
Stable Diffusion XL使用Euler作為默認(rèn)采樣器。雖然可能有更快的采樣器,但Euler本身已經(jīng)屬于快速采樣器范疇,因此將Euler替換為其他采樣器并不會帶來顯著的優(yōu)化效果。
步數(shù)(Step)
采用默認(rèn)SDXL,通過50步來清除一個充滿噪音的張量。步數(shù)越多,噪音清除效果就越好,但推理時間也會相應(yīng)增加。使用num_inference_steps參數(shù),我們可以指定想要使用的步數(shù)。
我們將分別使用30、25、20和15步,來生成一系列圖像。我們將使用默認(rèn)值(50)作為比較基準(zhǔn)。
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')
盡管減少步數(shù)可以縮短推理時間,但我們更感興趣的是保持在一定范圍的步數(shù),以盡可能地維持圖像的質(zhì)量和結(jié)構(gòu)。如果我們的圖像質(zhì)量大幅下降,就算節(jié)省了大量時間也沒有意義。接下來我們來探索步數(shù)數(shù)量的極限。
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
肖像照片(Portrait):在15和20步時,質(zhì)量尚可,但結(jié)構(gòu)有所不同。25步及以上時,我發(fā)現(xiàn)圖像質(zhì)量和結(jié)構(gòu)相當(dāng)不錯。
室內(nèi)照片(Interior):在15步時,仍然沒有達(dá)到期望的結(jié)構(gòu)。在20步時,結(jié)果相當(dāng)不錯,但某些元素仍有缺失。因此,我認(rèn)為至少需要25步。
微距攝影(Macro):在微距攝影中,即使只有15步,細(xì)節(jié)水平也相當(dāng)驚人。我不知道應(yīng)該選哪個步數(shù),因為所有選項都是有效和正確的。
3D圖像:在3D風(fēng)格的圖像中,少量步數(shù)會導(dǎo)致產(chǎn)生大量缺陷,甚至在某些區(qū)域會出現(xiàn)模糊。盡管30步的圖像還不錯,但我更傾向于使用50步(或者40步)的結(jié)果。
總的來說,根據(jù)生成的圖像風(fēng)格,可以選擇使用更多或更少的步數(shù)。但是,在25-30步時可以獲得相當(dāng)不錯的質(zhì)量。這樣做可以將推理時間縮短約40%,是一種相當(dāng)顯著的提升。
何時使用:當(dāng)測試提示或調(diào)整參數(shù),并且想要快速生成圖像時,這是一個很好的優(yōu)化方法。調(diào)試完所有參數(shù)和提示后,可以增加步數(shù),以獲取最高質(zhì)量的圖像。根據(jù)具體的使用情況,可以選擇是否永久采用這種優(yōu)化方法。
禁用 CFG
正如文章“How Stable Diffusion works”所說,無分類器引導(dǎo)(classifier-free guidance)技術(shù)負(fù)責(zé)調(diào)整噪音預(yù)測器與特定標(biāo)簽之間的距離。
舉例來說,假設(shè)我們有一個關(guān)于汽車的正向提示和一個關(guān)于玩具的負(fù)向提示。CFG技術(shù)可以調(diào)整噪音預(yù)測器與“汽車”標(biāo)簽之間的距離,使其更接近“汽車”標(biāo)簽所代表的概念,同時遠(yuǎn)離“玩具”標(biāo)簽所代表的概念。這樣做可以確保生成的圖像更符合“汽車”的特征,而不受“玩具”的影響。這是一種非常有效的控制條件圖像生成法。
“How to implement Stable Diffusion(https://www.felixsanz.dev/articles/how-to-implement-stable-diffusion)”一文介紹了如何實現(xiàn)CFG技術(shù),并且說明了它引入需要復(fù)制張量的需求:
# 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)
這意味著噪音預(yù)測器在每步上需要花費兩倍的時間。
在生成圖像的早期階段,啟用CFG技術(shù)對于獲得質(zhì)量良好且符合我們提示的圖像至關(guān)重要。一旦噪音預(yù)測器成功地開始產(chǎn)生符合預(yù)期的圖像,我們就可能不再需要繼續(xù)使用CFG技術(shù)了。在這一優(yōu)化方法中,我們將探索在圖像生成過程中停用CFG技術(shù)的影響。
代碼很簡單,我們創(chuàng)建了一個函數(shù),負(fù)責(zé)在步數(shù)達(dá)到指定值后禁用CFG技術(shù)(pipe._guidance_scale = 0.0)。此外,停止使用CFG技術(shù)后,將不再需要復(fù)制張量。
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參數(shù),在每一步結(jié)束時這個函數(shù)作為回調(diào)函數(shù)被執(zhí)行。我們需要使用callback_on_step_end_tensor_inputs參數(shù),確定我們將在回調(diào)函數(shù)內(nèi)部修改的張量。
我們來看看在圖像生成過程的最后25%和后半部分(50%),停用CFG技術(shù)會發(fā)生什么。
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
正如我們所預(yù)期的,在50%的情況下停用CFG,可以使每張圖像的推理時間減少25%(并非總體時間,因為模型加載時間也計入其中)。這是因為如果使用CFG執(zhí)行50步操作,模型實際上清理了100個張量。而使用這種優(yōu)化方法,模型在前25步清理了50個張量,在接下來的25步中清理了一半(25個張量)。所以,75/100相當(dāng)于跳過了25%的工作量。在停用CFG達(dá)到75%的情況下,每張圖像的推理時間減少12.5%。
圖像質(zhì)量似乎有所下降但不太明顯。這可能是因為沒有使用負(fù)向提示,而使用CFG的主要優(yōu)勢就在于能夠應(yīng)用負(fù)向提示。使用更好的提示肯定會提高質(zhì)量,但在停用CFG達(dá)到75%的情況下,這種影響幾乎可以忽略不計。
何時使用:當(dāng)你對圖像生成速度要求較高,且能夠接受一定程度的質(zhì)量損失,那么可以積極采用這種優(yōu)化方法(例如,用于測試提示或參數(shù)時)。通過稍后停用CFG,可以提高速度而不犧牲質(zhì)量。
細(xì)化模型(Refiner)
那么細(xì)化模型呢?雖然我們已經(jīng)優(yōu)化了基礎(chǔ)模型,但Stable Diffusion XL的一個主要優(yōu)勢是,它還有一個專門細(xì)化細(xì)節(jié)的模型。這個模型顯著提高了生成的圖像質(zhì)量。
默認(rèn)情況下,基礎(chǔ)模型使用11.24 GB的內(nèi)存。當(dāng)同時使用細(xì)化模型時,內(nèi)存需求增加到了17.38 GB。但要記住,由于它具有相同的組件(除了第一個文本編碼器),大多數(shù)優(yōu)化也可以應(yīng)用于這個模型。
在使用細(xì)化模型進(jìn)行預(yù)熱時,因為需要預(yù)熱兩個不同的模型,所以會有些復(fù)雜。為實現(xiàn)這一點,我們首先從基礎(chǔ)模型獲取結(jié)果,然后將其通過細(xì)化模型進(jìn)行處理:
for generation in queue:
image = base(generation['prompt'], output_type='latent').images
refiner(generation['prompt'], image=image)
細(xì)化模型有兩種不同的使用方式,我們將分別討論。
專家去噪器集成(Ensemble of Expert Denoisers)
專家去噪器集成是指圖像生成的方法,其過程從基礎(chǔ)模型開始,最后使用細(xì)化模型結(jié)束。在整個過程中,不會生成任何圖像,而是基礎(chǔ)模型在指定數(shù)量的步數(shù)(總步數(shù)的一部分)內(nèi)清理張量,然后將張量傳遞給細(xì)化模型以完成處理。
可以這樣說,它們共同工作以生成結(jié)果(基礎(chǔ)模型+細(xì)化器)。
就代碼而言,基礎(chǔ)模型在使用denoising_end=0.8參數(shù)時,會在處理過程的80%處停止其工作,并且通過 output_type='latent' 返回張量。
細(xì)化模型通過image參數(shù)接收到這個張量(諷刺的是,它并不是一個圖像)。然后,細(xì)化模型開始清理這個張量,通過參數(shù)denoising_start=0.8假設(shè)已經(jīng)完成了80%的工作。我們再次指定了整個處理過程的步數(shù) (num_inference_steps),以便它計算剩余需要清理的步數(shù)。也就是說,如果我們使用50步,并且在80%處進(jìn)行了變化,那么基礎(chǔ)模型將會在前40步清理張量,細(xì)化模型將為剩余的10步進(jìn)行精細(xì)化處理,以完善剩余細(xì)節(jié)。
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時切換到細(xì)化模型。
作為參考,我們還將包括在所有比較中作為基礎(chǔ)的圖像(只使用基礎(chǔ)模型,共50步)。
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
毫無疑問,使用細(xì)化模型顯然會極大地改善結(jié)果。
那么應(yīng)該在何時使用細(xì)化模型來處理圖像呢?顯然,可以看出在 0.9 處得到的結(jié)果比在 0.8 處更好,因為細(xì)化模型旨在優(yōu)化最終細(xì)節(jié),不應(yīng)該用于改變圖像結(jié)構(gòu)。
我認(rèn)為,無論步數(shù)是多少,細(xì)化模型似乎都能提供非常高的視覺質(zhì)量結(jié)果。唯一會改變的是圖像結(jié)構(gòu),但即使只有30步,視覺質(zhì)量也很高。
同時,我們還要考慮到當(dāng)步數(shù)減少到40以下時,所需時間會顯著減少。
何時使用:每當(dāng)我們想要利用細(xì)化模型來提高圖像的視覺質(zhì)量時,就可以使用它。至于參數(shù),只要我們不追求最佳的質(zhì)量,就可以使用30或40步。當(dāng)然始終要在0.9處切換到細(xì)化模型。
圖像到圖像(Image-to-image)
在Stable Diffusion XL中,經(jīng)典的圖像到圖像(img2img)方法并不新鮮。這種方法是使用基礎(chǔ)模型生成完整圖像,然后將生成的圖像和原始提示一起傳遞給細(xì)化模型,細(xì)化模型使用這些條件生成新的圖像。
換句話說,在img2img方法中,這兩個模型是獨立工作的(基礎(chǔ)模型->細(xì)化模型)。
由于兩個過程是獨立的,因此相對容易應(yīng)用本文中的優(yōu)化方法。盡管如此,代碼并沒有太大的差異,只是簡單地生成了一個圖像,并將其用作細(xì)化模型的參數(shù)。
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')
我們將使用基礎(chǔ)模型在50、40、30和20步處生成圖像,然后再通過細(xì)化模型添加額外的20步和10步的組合。
作為參考,我們還包含了所有比較的基礎(chǔ)圖像,這張圖像是基礎(chǔ)模型處理的結(jié)果,只進(jìn)行了50步的處理。
測試結(jié)果:
?<左右滑動查看更多圖片>
結(jié)論:
在圖像到圖像(img2img)模式中,細(xì)化模型的表現(xiàn)不如人意。
當(dāng)我們在基礎(chǔ)模型中使用足夠的步數(shù)時,似乎細(xì)化模型被迫向本不需要的部分添加細(xì)節(jié)。換句話說,這是在畫蛇添足。
另一方面,如果我們在基礎(chǔ)模型中使用較少的步數(shù),結(jié)果會稍微好一些。這是因為使用如此少的步數(shù),基礎(chǔ)模型無法添加細(xì)微的細(xì)節(jié),為細(xì)化模型提供了更大的發(fā)揮空間。
同時,我們也必須考慮通過減少步數(shù)來減少時間。如果我們使用太多步數(shù),將會受到顯著的損失。
何時使用:首先要記住的是,使用細(xì)化模型的目的是最大化視覺質(zhì)量。在這種情況下,我們可以增加步數(shù),因此,“專家去噪器集成”方法是最佳選擇。我認(rèn)為,使用少量步數(shù)無法獲得更好的視覺質(zhì)量,也不會提高生成速度,與其他方法相比也不具備優(yōu)勢。因此,在圖像到圖像模式下使用細(xì)化模型有其優(yōu)勢,但其優(yōu)勢并不突出。
6
結(jié)論
開始寫這篇文章時,我并沒有想到會深入研究到這個程度。我能夠理解直接跳到結(jié)論部分的讀者,同時我也很佩服看了所有優(yōu)化內(nèi)容的讀者。希望閱讀完本文后,讀者們能夠有所所獲。
根據(jù)目標(biāo)和可用的硬件,我們需要應(yīng)用不同的優(yōu)化方法。讓我們以表格的形式,總結(jié)所有的優(yōu)化措施以及它們引入的改進(jìn)(或損失)。
理論上來說,“中立”的優(yōu)化方法是該類別中一個有利的改變,但其可解釋性可能有爭議,或者僅適用于某些特定用例。
最快速度
使用基礎(chǔ)模型結(jié)合OneDiff+Tiny VAE+75%處禁用CFG+30步,可以在幾乎質(zhì)量無損的情況下實現(xiàn)最短的生成時間,從而達(dá)到最快速度。
在擁有RTX 3090顯卡的情況下,可以在僅4.0秒的時間內(nèi)生成圖像,內(nèi)存消耗為6.91 GB,因此甚至可以在具有8 GB內(nèi)存的顯卡上運行。
我們還可以添加DeepCache以進(jìn)一步加快流程,但問題是,它與禁用CFG優(yōu)化不兼容,一旦禁用它,最終速度就會增加。
使用相同的配置,A100顯卡可以在2.7秒內(nèi)生成圖像。在全新的 H100 上,推理時間僅為2.0秒。
不到4GB的內(nèi)存使用量
在使用Sequential CPU Offload時,瓶頸在于VAE。因此,將這種優(yōu)化與VAE FP16 fix或Tiny VAE結(jié)合使用,將分別需要2.56 GB和0.68 GB的內(nèi)存使用量。雖然內(nèi)存使用量低得離譜,但推理時間會讓你覺得有必要去換一張擁有更多內(nèi)存的新顯卡。
不到6GB的內(nèi)存使用量
通過使用批處理優(yōu)化,內(nèi)存使用量降低至5.77 GB,從而使得在擁有6 GB內(nèi)存的顯卡上可以使用 Stable Diffusion XL 生成圖像。在這種情況下,沒有質(zhì)量損失或推理時間增加,如果我們想使用細(xì)化模型也沒有問題,內(nèi)存消耗是一樣的。
另一個選擇是使用Model CPU Offload,這也足以減少內(nèi)存使用,只不過會有一點時間上的損失。
通過使用VAE FP16 fix或Tiny VAE優(yōu)化VAE,我們可以稍微加快推理過程。
如果我們想要稍微加快推理過程并在12.9秒內(nèi)生成圖像,我們可以通過使用VAE FP16 fix來實現(xiàn)這一點。而且,如果我們不介意稍微改變結(jié)果,我們還可以進(jìn)一步優(yōu)化,通過使用Tiny VAE,將內(nèi)存消耗降低到5.6 GB,生成時間縮短到12.6秒。
請記住,仍然可以應(yīng)用其他優(yōu)化措施來減少生成時間。
不到8GB的內(nèi)存使用量
突破了6 GB的內(nèi)存限制之后,就可以開啟新的優(yōu)化選擇。
正如之前所看到的,使用OneDiff+Tiny VAE將內(nèi)存使用量降至6.91 GB,并實現(xiàn)了可能的最低推理時間。因此,如果你的顯卡至少有8 GB內(nèi)存,這可能是你的最佳選擇。
【OneDiff v0.12.1正式發(fā)布(生產(chǎn)環(huán)境穩(wěn)定加速SD&SVD)】本次更新包含以下亮點,歡迎體驗新版本:github.com/siliconflow/onediff
*??更新SDXL和SVD的SOTA性能
*? 全面支持SD和SVD動態(tài)分辨率運行
*? 編譯/保存/加載HF Diffusers的pipeline
*? HF Diffusers的快速LoRA加載和切換
*? 加速了InstantID(加速1.8倍)
*? 加速了SDXL Lightning
(SDXL?E2E?Time)
(SVD?E2E?Time)
更多詳情:https://medium.com/@SiliconFlowAI/
其他人都在看
-
800+頁免費“大模型”電子書
-
LLM推理的極限速度
-
強(qiáng)化學(xué)習(xí)之父:通往AGI的另一種可能
-
好久不見!OneFlow 1.0全新版本上線
-
LLM推理入門指南②:深入解析KV緩存
-
僅需50秒,AI讓你的通話彩鈴變身短視頻
-
OneDiffx“圖生生”,電商AI圖像處理新范式文章來源:http://www.zghlxwxcb.cn/news/detail-858740.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-858740.html
到了這里,關(guān)于Stable Diffusion XL優(yōu)化終極指南的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!