问题陈述:
AIGC 时代,包括 Stable Diffusion 和 DALL-E 3 等基于扩散模型的文生图模型受到了广泛关注。扩散模型通过向训练数据添加噪声,然后逆转这一过程来生成高质量图像。然而,扩散模型生成图片需要进行多步采样,这一过程相对较慢,增加了推理成本。缓慢的多步采样问题是部署这类模型时的主要瓶颈。
英特尔黑客松比赛旨在由参赛者使用 Stable Diffusion 在目标英特尔硬件上完成部署优化及指定的图片生成工作。根据指定的输入输出要求,优化指定文生图模型在端侧设备上的 Pipeline性能,在保证生图效果的情况下,降低pipeline端到端延迟,降低pipeline峰值内存占用。这对于AIGC时代文生图推理成本问题解决有极大的促进意义。
初始解决方案:
我们团队的初始解决方案为基于潜在一致性的文生图优化,针对上述问题,OpenAI 的宋飏博士在今年提出的一致性模型(Consistency Model,CM)为解决上述问题提供了一个思路。一致性模型被指出在设计上具有单步生成的能力,展现出极大的加速扩散模型的生成的潜力。然而,由于一致性模型局限于无条件图片生成,导致包括文生图、图生图等在内的许多实际应用还难以享受这一模型的潜在优势。
LCM的核心创新在于其潜在一致性架构。这一架构通过在潜空间进行图像处理,显著减少了所需处理的数据量,从而大幅提升了图像生成的速度。这种方法不仅加快了生成速度,还保持了图像的高分辨率和细节质量。
在性能方面,LCM展现出了卓越的效率。据实验数据显示,LCM在图像生成过程中,所需的算力比传统模型减少了约50%。这一显著的性能提升,使得LCM在处理大规模图像数据时更加高效。同时,LCM在图像质量上也有所提升,其出图效果已经大幅优于许多现有模型。
LCM 强调的是不要再一步步迭代了, 直接追求“一步到位”。LCM 是在 Consistency Models 的基础上引入了 Lantent Space (潜空间),进一步压缩需要处理的数据量,从而实现超快速的图像推理合成。只需要 2-4 步即可完成!
我想,针对于该赛题,该方案是最好的方法,可以看下基于潜在一致性模型生成的一些图片,首先是4步效果图:
然后是2步效果图:
1步效果图:
可以看出,基于潜在一致性的文生图,不仅推理速度方面有了提升,而且在相当少的推理步数下,推理出来的图片质量也是十分不错的。
然而目前官网只提供了Dreamshaper-V7和LCM-SDXL,2 款可以在Stable Diffusion中使用的模型,并且需要安装 LCM 插件。
我们需要先安装以下库:
pip install --upgrade diffusers # make sure to use at least diffusers >= 0.22
pip install transformers accelerate
然后运行以下代码即可:
from diffusers import DiffusionPipeline
import torch
pipe = DiffusionPipeline.from_pretrained("SimianLuo/LCM_Dreamshaper_v7")
# To save GPU memory, torch.float16 can be used, but it may compromise image quality.
pipe.to(torch_device="cpu", torch_dtype=torch.float32)
prompt = "Self-portrait oil painting, a beautiful cyborg with golden hair, 8k"
# Can be set to 1~50 steps. LCM support fast inference even <= 4 steps. Recommend: 1~8 steps.
num_inference_steps = 4
images = pipe(prompt=prompt, num_inference_steps=num_inference_steps, guidance_scale=8.0, lcm_origin_steps=50, output_type="pil").images
多线程优化:
在此次优化任务中,我们还特意使用到了多线程,多线程可以显著提高性能和吞吐量,对于此次任务,我觉得它最大的好处就是并行处理,如果你需要生成大量的图像,并且每张图像的生成是独立的,那么多线程可以同时处理多个生成任务。这样可以利用多核心CPU的能力,减少总体的等待时间,从而提高效率。
但是在进行的过程中也是遇到了些许问题,如RuntimeError: Infer Request is busy
,在多线程环境中,如果多个线程尝试同时使用同一个模型对象进行推理,就可能发生这种情况。OpenVINO™ 是一个高性能的深度学习推理引擎,但是当使用它的 Python API 时,需要确保对模型的推理请求是线程安全的。
刚开始,我们增加了一个线程锁,虽然解决了推理报错的问题,但是增加线程锁会严重影响并行处理的效率,代码如下:
import threading
import os
from PIL import Image
import ipywidgets as widgets
from transformers import CLIPTokenizer
core = ov.Core()
device = 'GPU.1'
ov_config = {"INFERENCE_PRECISION_HINT": "f32"} if device != "CPU" else {}
text_enc = core.compile_model(TEXT_ENCODER_OV_PATH, device)
unet_model = core.compile_model(UNET_OV_PATH, device)
vae_decoder = core.compile_model(VAE_DECODER_OV_PATH, device, ov_config)
vae_encoder = core.compile_model(VAE_ENCODER_OV_PATH, device, ov_config)
scheduler = LMSDiscreteScheduler.from_config(conf)
tokenizer = CLIPTokenizer.from_pretrained('clip-vit-large-patch14')
# 创建一个锁对象
lock = threading.Lock()
ov_pipe = OVStableDiffusionPipeline(
tokenizer=tokenizer,
text_encoder=text_enc,
unet=unet_model,
vae_encoder=vae_encoder,
vae_decoder=vae_decoder,
scheduler=scheduler
)
def generate(prompt, negative_prompt, seed, num_steps):
result = ov_pipe(
prompt,
negative_prompt=negative_prompt,
num_inference_steps=int(num_steps),
seed=seed,
)
return result["sample"][0]
def save_image(image, file_path):
# 确保目录存在
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# 保存图像
image.save(file_path, format="PNG", optimize=True)
def generate_and_save_image(prompt, negative_prompt, seed, num_steps, image_dir, i):
# 使用锁来确保线程安全
with lock:
image = generate(prompt, negative_prompt, seed + i, num_steps)
image = image.resize((512, 512))
file_path = os.path.join(image_dir, f"image_{i}.png")
save_image(image, file_path)
def generate_and_save_images_multithreaded(prompt, negative_prompt, seed, num_steps, image_dir, num_images=20):
threads = []
for i in range(num_images):
thread = threading.Thread(target=generate_and_save_image, args=(prompt, negative_prompt, seed, num_steps, image_dir, i))
threads.append(thread)
thread.start()
for thread in threads:
thread.join() # 等待所有线程完成
prompt = "a photo of an astronaut riding a horse on mars"
negative_prompt = "low resolution, blurry"
seed = 42
num_inference_steps = 20
output_dir = "output_images5"
# 使用多线程函数生成图片
generate_and_save_images_multithreaded(prompt, negative_prompt, seed, num_inference_steps, output_dir)
考虑到并行效率问题,我们最终摒弃了该方法,测试了线程池与队列、异步推理等方法。
from openvino.inference_engine import IECore, StatusCode
import os
from PIL import Image
import ipywidgets as widgets
from transformers import CLIPTokenizer
core = ov.Core()
device = 'GPU.1'
ov_config = {"INFERENCE_PRECISION_HINT": "f32"} if device != "CPU" else {}
text_enc = core.compile_model(TEXT_ENCODER_OV_PATH, device)
unet_model = core.compile_model(UNET_OV_PATH, device)
vae_decoder = core.compile_model(VAE_DECODER_OV_PATH, device, ov_config)
vae_encoder = core.compile_model(VAE_ENCODER_OV_PATH, device, ov_config)
scheduler = PNDMScheduler.from_config(conf)
tokenizer = CLIPTokenizer.from_pretrained('clip-vit-large-patch14')
ov_pipe = OVStableDiffusionPipeline(
tokenizer=tokenizer,
text_encoder=text_enc,
unet=unet_model,
vae_encoder=vae_encoder,
vae_decoder=vae_decoder,
scheduler=scheduler
)
def generate(prompt, negative_prompt, seed, num_steps):
result = ov_pipe(
prompt,
negative_prompt=negative_prompt,
num_inference_steps=int(num_steps),
seed=seed,
)
return result["sample"][0]
def save_image(image, file_path):
# 确保目录存在
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# 保存图像
image.save(file_path, format="PNG", optimize=True)
def generate_and_save_image(prompt, negative_prompt, seed, num_steps, image_dir, i):
# 创建推理请求
text_enc_infer_req = text_enc_exec_net.create_infer_request()
unet_infer_req = unet_exec_net.create_infer_request()
vae_encoder_infer_req = vae_encoder_exec_net.create_infer_request()
vae_decoder_infer_req = vae_decoder_exec_net.create_infer_request()
# 准备输入数据
input_blob = next(iter(text_enc.input_info))
output_blob = next(iter(text_enc.outputs))
text_enc_input_data = {"input_ids": tokenizer.encode(prompt)}
# 启动异步推理
text_enc_infer_req.start_async(request_id=0, inputs=text_enc_input_data)
status = text_enc_infer_req.wait()
if status == StatusCode.OK:
text_enc_output_data = text_enc_infer_req.outputs[output_blob]
# 准备输入数据
input_blob = next(iter(unet_model.input_info))
output_blob = next(iter(unet_model.outputs))
unet_input_data = {"input": image}
# 启动异步推理
unet_infer_req.start_async(request_id=0, inputs=unet_input_data)
# 等待推理完成
status = unet_infer_req.wait()
if status == StatusCode.OK:
unet_output_data = unet_infer_req.outputs[output_blob]
# 准备输入数据
input_blob = next(iter(vae_encoder.input_info))
output_blob = next(iter(vae_encoder.outputs))
vae_encoder_input_data = {"input": unet_output_data, "text_embed": text_enc_output_data}
# 启动异步推理
vae_encoder_infer_req.start_async(request_id=0, inputs=vae_encoder_input_data)
# 等待推理完成
status = vae_encoder_infer_req.wait()
if status == StatusCode.OK:
vae_encoder_output_data = vae_encoder_infer_req.outputs[output_blob]
# 准备输入数据
input_blob = next(iter(vae_decoder.input_info))
output_blob = next(iter(vae_decoder.outputs))
vae_decoder_input_data = {"z": vae_encoder_output_data, "text_embed": text_enc_output_data}
# 启动异步推理
vae_decoder_infer_req.start_async(request_id=0, inputs=vae_decoder_input_data)
# 等待推理完成
status = vae_decoder_infer_req.wait()
if status == StatusCode.OK:
vae_decoder_output_data = vae_decoder_infer_req.outputs[output_blob]
# 获取推理结果
image = Image.fromarray(vae_decoder_output_data[0].transpose((1, 2, 0)))
image = image.resize((512, 512))
file_path = os.path.join(image_dir, f"image_{i}.png")
save_image(image, file_path)
else:
print("VAE decoder inference failed.")
else:
print("VAE encoder inference failed.")
else:
print("Unet inference failed.")
else:
print("Text encoder inference failed.")
def generate_and_save_images_multithreaded(prompt, negative_prompt, seed, num_steps, image_dir, num_images=20, max_workers=5):
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(generate_and_save_image, prompt, negative_prompt, seed, num_steps, image_dir, i) for i in range(num_images)]
for future in futures:
future.result() # 等待每个线程任务完成
prompt = "a photo of an astronaut riding a horse on mars"
negative_prompt = "low resolution, blurry"
seed = 42
num_inference_steps = 20
output_dir = "output_images5"
# 使用多线程函数生成图片
generate_and_save_images_multithreaded(prompt, negative_prompt, seed, num_inference_steps, output_dir)
import threading
import os,random
from PIL import Image
import ipywidgets as widgets
from transformers import CLIPTokenizer
import copy
core = ov.Core()
device = 'GPU.1'
ov_config = {"INFERENCE_PRECISION_HINT": "f32"} if device != "CPU" else {}
text_enc = core.compile_model(TEXT_ENCODER_OV_PATH, device)
unet_model = core.compile_model(UNET_OV_PATH, device)
vae_decoder = core.compile_model(VAE_DECODER_OV_PATH, device, ov_config)
vae_encoder = core.compile_model(VAE_ENCODER_OV_PATH, device, ov_config)
scheduler = LMSDiscreteScheduler.from_config(conf)
tokenizer = CLIPTokenizer.from_pretrained('clip-vit-large-patch14')
ov_pipe = OVStableDiffusionPipeline(
tokenizer=tokenizer,
text_encoder=text_enc,
unet=unet_model,
vae_encoder=vae_encoder,
vae_decoder=vae_decoder,
scheduler=scheduler
)
def generate_and_save_images(prompt, negative_prompt, seed, num_steps, image_dir):
if not os.path.exists(image_dir):
os.makedirs(image_dir)
infer_request_curr = ov_pipe
infer_request_curr_result=infer_request_curr(
prompt,
negative_prompt=negative_prompt,
num_inference_steps=int(num_steps),
seed=seed[0],
)
for inx,i in enumerate(range(len(seed))):
infer_request_next = ov_pipe
infer_request_next_result=infer_request_next(
prompt,
negative_prompt=negative_prompt,
num_inference_steps=int(num_steps),
seed=seed[inx],
)
image = infer_request_curr_result["sample"][0].resize((512, 512))
# 保存图片到文件
file_path = os.path.join(image_dir, f"image_{inx-1}.png")
image.save(file_path, format="PNG", optimize=True)
infer_request_curr, infer_request_next = infer_request_next, infer_request_curr
infer_request_curr_result,infer_request_next_result = infer_request_next_result,infer_request_curr_result
prompt = "a photo of an astronaut riding a horse on mars"
negative_prompt = "low resolution, blurry"
seed = 42
num_inference_steps = 20
output_dir = "output_images6"
generate_numbers = 20
seed_li = random_numbers = random.sample(range(1, 100), generate_numbers)
generate_and_save_images(prompt, negative_prompt, seed_li, num_inference_steps, output_dir)
OneAPI—Intel® Distribution for Python*
-
Intel® Distribution for Python*是一个为Python语言提供的发行版,旨在优化Python在Intel硬件上的性能。它包含了Python解释器以及一系列常用的Python库和工具,可以帮助用户更高效地开发Python应用程序。
-
该发行版提供了对Intel硬件特性的支持,例如多核处理器、向量指令集等,从而提高了Python代码的执行效率。此外,它还提供了对并行计算和分布式计算的支持,可以帮助用户更好地利用多核处理器和集群资源进行高性能计算。
-
Intel® Distribution for Python*还包含了一些专门为数据科学和机器学习应用优化的库,例如NumPy、Pandas、SciPy和Scikit-learn等。这些库可以帮助用户更轻松地进行数据处理、模型训练和预测等操作。
-
Intel® Distribution for Python*是一个针对Python语言的优化发行版,可以帮助用户更高效地开发Python应用程序,并充分利用Intel硬件的特性进行高性能计算。通过使用该python发行版本,我们可以进一步优化模型推理速度。
在本地进行以下命令安装:
首先使用conda update conda更新所有anaconda包。
然后使用conda config --add channels intel 向Conda配置中添加一个新的频道(channel),名为"intel"。Conda是一个流行的Python包管理工具,用于安装和管理Python包和环境。
最后使用conda create -n idp intelpython3_core python=3.9进行安装。
安装完成后使用conda activate idp进入该虚拟环境。
然后基于此环境来安装我们所需要的文生图依赖库。
调度器优化:
初次之外,我们还将调度器进行了替换,使用PNDMScheduler调度器,该调度器有更快的推理速度,可以更好的优化文生图性能,减少文生图时间延迟。
def prepare_latents(self, image:PIL.Image.Image = None, latent_timestep:torch.Tensor = None):
"""
Function for getting initial latents for starting generation
Parameters:
image (PIL.Image.Image, *optional*, None):
Input image for generation, if not provided randon noise will be used as starting point
latent_timestep (torch.Tensor, *optional*, None):
Predicted by scheduler initial step for image generation, required for latent image mixing with nosie
Returns:
latents (np.ndarray):
Image encoded in latent space
"""
latents_shape = (1, 4, self.height // 8, self.width // 8)
noise = np.random.randn(*latents_shape).astype(np.float32)
if image is None:
# if we use LMSDiscreteScheduler, let's make sure latents are mulitplied by sigmas
if isinstance(self.scheduler, PNDMScheduler):
noise = noise * self.scheduler.alphas[0].numpy()
return noise, {}
input_image, meta = preprocess(image)
latents = self.vae_encoder(input_image)[self._vae_e_output]
latents = latents * 0.18215
latents = self.scheduler.add_noise(torch.from_numpy(latents), torch.from_numpy(noise), latent_timestep).numpy()
return latents, meta
推理结果: