我们已经成功地将基于扩散模型的 LoRA Hub 推断速度大大提高。这使得我们能够节省计算资源并提供更好的用户体验。
要对给定的模型进行推断,有两个步骤:
- 预热阶段 – 包括下载模型和设置服务(25秒)。
- 推断作业本身(10秒)。
通过这些改进,我们能够将预热时间从25秒减少到3秒。我们能够为数百个不同的 LoRA 提供推断服务,只需要不到 5 个 A10G GPU,同时用户请求的响应时间从 35 秒减少到 13 秒。
让我们更详细地讨论如何利用在 Diffusers 库中开发的一些最新功能,以一种动态方式使用单一服务为许多不同的 LoRA 提供服务。
LoRA
LoRA 是一种微调技术,属于“参数高效”(PEFT)方法家族,试图减少受微调过程影响的可训练参数的数量。它增加了微调速度,同时减小了微调检查点的大小。
与通过对其所有权重进行微小更改来微调模型不同,我们冻结大部分层,并只对注意力块中的几个特定层进行训练。此外,我们通过将两个较小矩阵的乘积添加到原始权重中来避免触及这些层的参数。这些小矩阵是在微调过程中更新权重并保存到磁盘的矩阵。这意味着所有模型的原始参数都得到保留,我们可以在其上面使用适应方法加载 LoRA 权重。
LoRA 名称(低秩适应)来自我们提到的小矩阵。有关该方法的更多信息,请参考此帖子或原论文。

上图显示了两个较小的橙色矩阵,它们作为 LoRA 适配器的一部分保存。我们可以稍后加载 LoRA 适配器并将其与蓝色基础模型合并,以获得黄色微调模型。重要的是,也可以卸载适配器,因此可以随时还原回原始基础模型。
换句话说,LoRA 适配器就像是基础模型的附加组件,可以按需添加和删除。由于 A 和 B 较小的秩,它在模型大小上非常轻巧。因此,加载比加载整个基础模型要快得多。
例如,如果您查看Stable Diffusion XL Base 1.0 模型仓库内部,该模型被广泛用作许多 LoRA 适配器的基础模型,您会发现它的大小约为7GB。然而,像这个普通的 LoRA 适配器仅占用 24MB 的空间!
Hub 上的蓝色基础模型要比黄色基模型少得多。如果我们能够轻松从蓝色模型转换为黄色模型,反之亦然,我们就能够使用很少的蓝色部署为许多不同的黄色模型提供服务。
有关 LoRA 的更详尽介绍,请参考以下博客文章:使用 LoRA 进行高效稳定扩散微调,或直接参阅原论文。
好处
我们在Hub上拥有大约130个独特的LoRA。其中绝大多数(约92%)是基于Stable Diffusion XL Base 1.0模型的LoRA。
在此共享之前,这意味着我们需要为所有的LoRA部署一个专门的服务(例如上图中所有黄色的融合矩阵);释放+预留至少一个新的GPU。生成服务并使其准备好为特定模型提供服务的时间约为25秒,然后还需要推理时间(对于A10G上的1024×1024 SDXL推理扩散,使用25个推理步骤,大约10秒)。如果适配器只是偶尔被请求,它的服务将停止以释放被其他服务抢占的资源。
如果您请求的LoRA不是很受欢迎,即使它是基于SDXL模型(就像迄今在Hub上找到的大多数适配器一样),它也需要35秒来热启动并在第一个请求上获得答案(以下的请求将需要推理时间,例如10秒)。
现在:由于适配器只使用少量不同的“蓝色”基础模型(例如扩散中的2个重要模型),请求时间从35秒减少到13秒。即使您的适配器不那么受欢迎,也有很大几率它的“蓝色”服务已经准备好。换句话说,即使您不经常请求该模型,很有可能避免25秒的热启动时间。蓝色模型已经被下载并准备就绪,我们只需卸载先前的适配器并加载新的适配器,这需要3秒,如下所示:below。
总体而言,这需要更少的GPU来服务所有不同的模型,即使我们已经有一种方法在部署之间共享GPU以最大化计算利用率。在2分钟的时间范围内,大约有10个不同的LoRA权重被请求。我们只需要使用1到2个GPU(或更多,如果有请求突发)来服务所有这些模型,而不是生成10个部署并保持它们热启动。
实施
我们在推理API中实现了LoRA共享。当我们平台上有一个模型的请求时,我们首先确定这是否是一个LoRA。然后我们确定LoRA的基础模型,并将请求路由到一个常规的后端服务器,能够为该模型提供服务。推理请求通过保持基础模型热启动并在运行中加载/卸载LoRA来进行服务。这样我们最终可以重复使用同样的计算资源同时提供服务于许多不同的模型。
LoRA结构
在Hub中,LoRA可以通过两个属性来识别:

一个LoRA将具有base_model属性。这只是LoRA在进行推理时应该应用于的模型。
因为不仅LoRA具有这样一个属性(任何重复的模型也会有这个属性),所以一个LoRA还需要一个lora标签来正确地被识别。
载入/卸载Diffusers中的LoRA 🧨
Diffusers库中有4个函数用于加载和卸载不同的LoRA权重:
load_lora_weights和fuse_lora用于加载和合并权重到主要层中。需要注意的是,在进行推理之前将权重与主模型合并,可以减少推理时间30%。
unload_lora_weights和unfuse_lora用于卸载。
我们在下面提供了一个示例,展示了如何利用Diffusers库快速加载多个LoRA权重并应用于基础模型之上:
“`html
import torchfrom diffusers import ( AutoencoderKL, DiffusionPipeline,)import timebase = "stabilityai/stable-diffusion-xl-base-1.0"adapter1 = 'nerijs/pixel-art-xl'weightname1 = 'pixel-art-xl.safetensors'adapter2 = 'minimaxir/sdxl-wrong-lora'weightname2 = Noneinputs = "elephant"kwargs = {}if torch.cuda.is_available(): kwargs["torch_dtype"] = torch.float16start = time.time()# Load VAE compatible with fp16 created by madebyollinvae = AutoencoderKL.from_pretrained( "madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch.float16,)kwargs["vae"] = vaekwargs["variant"] = "fp16"model = DiffusionPipeline.from_pretrained( base, **kwargs)if torch.cuda.is_available(): model.to("cuda")elapsed = time.time() - startprint(f"Base model loaded, elapsed {elapsed:.2f} seconds")def inference(adapter, weightname): start = time.time() model.load_lora_weights(adapter, weight_name=weightname) # Fusing lora weights with the main layers improves inference time by 30 % ! model.fuse_lora() elapsed = time.time() - start print(f"LoRA adapter loaded and fused to main model, elapsed {elapsed:.2f} seconds") start = time.time() data = model(inputs, num_inference_steps=25).images[0] elapsed = time.time() - start print(f"Inference time, elapsed {elapsed:.2f} seconds") start = time.time() model.unfuse_lora() model.unload_lora_weights() elapsed = time.time() - start print(f"LoRA adapter unfused/unloaded from base model, elapsed {elapsed:.2f} seconds")inference(adapter1, weightname1)inference(adapter2, weightname2)
加载图片
以下所有数字均以秒为单位:
| GPU | T4 | A10G |
| 加载基础模型 – 未缓存 | 20 | 20 |
| 加载基础模型 – 已缓存 | 5.95 | 4.09 |
| 加载适配器 1 | 3.07 | 3.46 |
| 卸载适配器 1 | 0.52 | 0.28 |
| 加载适配器 2 | 1.44 | 2.71 |
| 卸载适配器 2 | 0.19 | 0.13 |
| 推断时间 | 20.7 | 8.5 |
每次推断额外花费2到4秒,我们可以为许多不同的LoRA提供服务。然而,在A10G GPU上,推断时间大大减少,而适配器加载时间变化不大,所以LoRA的加载/卸载相对更加昂贵。
提供服务请求
为了提供推断请求,我们使用这个开源社区图像
您可以在TextToImagePipeline类中找到先前描述的机制。
“`
当一个LoRA被请求时,我们会查看已载入的模型,并仅在必要时进行更改,然后按照通常的方法进行推理。这样,我们就能为基础模型和许多不同的适配器提供请求。
以下是一个示例,展示了如何测试和请求这个图片:
$ git clone https://github.com/huggingface/api-inference-community.git$ cd api-inference-community/docker_images/diffusers$ docker build -t test:1.0 -f Dockerfile .$ cat > /tmp/env_file <<'EOF'MODEL_ID=stabilityai/stable-diffusion-xl-base-1.0TASK=text-to-imageHF_HUB_ENABLE_HF_TRANSFER=1EOF$ docker run --gpus all --rm --name test1 --env-file /tmp/env_file_minimal -p 8888:80 -it test:1.0
然后在另一个终端上请求基础模型和/或杂项LoRA适配器,这些适配器可以在HF Hub上找到。
# 请求基础模型$ curl 0:8888 -d '{"inputs": "elephant", "parameters": {"num_inference_steps": 20}}' > /tmp/base.jpg# 请求一个适配器$ curl -H 'lora: minimaxir/sdxl-wrong-lora' 0:8888 -d '{"inputs": "elephant", "parameters": {"num_inference_steps": 20}}' > /tmp/adapter1.jpg# 请求另一个适配器$ curl -H 'lora: nerijs/pixel-art-xl' 0:8888 -d '{"inputs": "elephant", "parameters": {"num_inference_steps": 20}}' > /tmp/adapter2.jpg
批处理如何工作?
最近有一篇非常有趣的论文出版,描述了如何通过对LoRA模型进行批处理推理来提高吞吐量。简而言之,所有推理请求将被收集到一个批次中,与通用基础模型相关的计算将一次性完成,然后计算剩余的适配器特定的生成物。我们没有实现这样的技术(接近于在text-generation-inference中LLMs的方法)。相反,我们坚持了单个顺序推理请求。原因是我们观察到对于diffusers来说,批处理并不有趣:随着批处理大小的增加,吞吐量并没有显著增加。在我们进行的简单图像生成基准测试中,批处理大小为8时,吞吐量仅增加了25%,而延迟则增加了6倍!相比之下,对于LLMs来说,批处理更加有意义,因为你可以获得8倍的顺序吞吐量,只增加了10%的延迟。这就是为什么我们没有为diffusers实现批处理的原因。
结论: 时间!
通过使用动态LoRA加载,我们能够节省计算资源并改善Hub Inference API的用户体验。尽管在卸载先前加载的适配器并加载我们感兴趣的适配器的过程中增加了额外的时间,但服务进程通常已经在运行,使得整体推理时间的响应更短。如果您在您的部署中应用了相同的方法,请务必让我们知道!