Динамическая загрузка LoRA для улучшения производительности и оптимизированного использования ресурсов

Оптимизированное использование ресурсов и улучшение производительности с помощью динамической загрузки LoRa

Мы смогли значительно ускорить процесс вывода в Hub для моделей на основе Diffusion. Это позволило нам сэкономить вычислительные ресурсы и обеспечить лучший пользовательский опыт.

Для выполнения вывода по заданной модели есть два этапа:

  1. Этап подготовки – включает в себя загрузку модели и настройку сервиса (25 секунд).
  2. Само задание вывода (10 секунд).

Благодаря этим усовершенствованиям мы смогли сократить время подготовки с 25 секунд до 3 секунд. Мы можем обслуживать вывод для сотен отдельных моделей на основе Diffusion с помощью менее чем 5 GPUs A10G, при этом время отклика на пользовательские запросы сократилось с 35 секунд до 13 секунд.

Давайте подробнее поговорим о том, как мы можем использовать некоторые последние возможности, разработанные в библиотеке Diffusers, чтобы обслуживать множество отдельных моделей на основе Diffusion с помощью одного единственного сервиса.

LoRA

LoRA – это метод тонкой настройки, который относится к семейству “эффективных по количеству параметров” (PEFT), который пытается сократить количество обучаемых параметров, затрагиваемых процессом тонкой настройки. Он увеличивает скорость тонкой настройки, при этом уменьшая размер точно настроенных контрольных точек.

Вместо тонкой настройки модели путем выполнения незначительных изменений всех ее весов мы замораживаем большую часть слоев и обучаем только некоторые конкретные слои в блоках внимания. Более того, мы избегаем влияния на параметры этих слоев, добавляя произведение двух меньших матриц к исходным весам. Эти маленькие матрицы – это те матрицы, весы которых обновляются во время процесса тонкой настройки и сохраняются на диске. Это означает, что все исходные параметры модели сохраняются, и мы можем загружать веса LoRA сверху с использованием метода адаптации.

Название LoRA (Low Rank Adaptation) происходит от этих маленьких матриц, о которых мы упоминали ранее. Для получения дополнительной информации о методе, пожалуйста, обратитесь к этой публикации или к оригинальной статье.

Декомпозиция LoRA

На приведенной выше диаграмме показаны две меньшие оранжевые матрицы, которые сохраняются как часть адаптера LoRA. Позже мы можем загрузить адаптер LoRA и объединить его с базовой моделью цвета синего, чтобы получить модель мелко настроенного цвета желтого. Важно отметить, что выгрузка адаптера также возможна, поэтому мы можем вернуться к исходной базовой модели в любой момент.

Другими словами, адаптер LoRA похож на дополнение к базовой модели, которое можно добавлять и удалять по требованию. И из-за меньшего ранга матриц А и В он очень легкий по сравнению с размером модели. Поэтому загрузка происходит намного быстрее, чем загрузка всей базовой модели.

Если вы посмотрите, например, внутрь репозитория Stable Diffusion XL Base 1.0 model, который широко используется в качестве базовой модели для многих адаптеров LoRA, вы увидите, что его размер примерно равен 7 ГБ. Однако типичные адаптеры LoRA, например, этот, занимают всего лишь 24 МБ места!

Существует гораздо меньше базовых моделей синего цвета, чем моделей желтого цвета на Hub. Если мы можем быстро перейти от синего к желтому и обратно, то у нас есть способ обслуживать множество отдельных моделей желтого цвета с помощью только нескольких различных разверток синего цвета.

Для более подробного описания того, что такое LoRA, пожалуйста, обратитесь к следующей блог-публикации: Использование LoRA для эффективной стабильной тонкой настройки методом диффузии, или обратитесь непосредственно к оригинальной статье.

Преимущества

У нас есть примерно 130 отдельных LoRA в Хабе. Подавляющее большинство (~92%) из них являются LoRA, основанными на модели Stable Diffusion XL Base 1.0.

Раньше для этого было бы необходимо развернуть отдельный сервис для каждого из них (например, для всех желтых объединенных матриц на диаграмме выше); выделить и зарезервировать как минимум одну новую GPU. Время создания сервиса и готовности к обслуживанию запросов для конкретной модели составляет примерно 25 секунд, на которые добавляется время вывода (~10 секунд для вывода 1024×1024 SDXL-расчета диффузии с помощью 25 шагов вывода на A10G). Если адаптер запрашивается только время от времени, его сервис останавливается, чтобы освободить ресурсы, зарезервированные другими моделями.

Если вы запрашиваете LoRA, который не так популярен, даже если он основан на модели SDXL, как подавляющее большинство адаптеров находящихся в Хабе до сих пор, для его “разогрева” и получения ответа на первый запрос потребуется примерно 35 секунд (последующие запросы будут занимать время вывода, например, 10 секунд).

Теперь время запроса снизилось с 35 секунд до 13 секунд, так как адаптеры будут использовать только несколько отдельных “синих” базовых моделей (например, 2 значимых модели для диффузии). Даже если ваш адаптер не очень популярен, есть хороший шанс, что его “синий” сервис уже разогрет. Другими словами, есть хороший шанс избежать времени “разогрева” в 25 секунд, даже если вы не часто запрашиваете свою модель. Синяя модель уже загружена и готова, все, что нам нужно сделать, это выгрузить предыдущий адаптер и загрузить новый, что занимает 3 секунды, как мы видим ниже.

В целом, для обслуживания всех отдельных моделей требуется меньше GPU, хотя у нас уже был способ взаимодействия между развертываниями для максимального использования их вычислительных возможностей. В течение временного промежутка 2 минуты, запрашивается примерно 10 отдельных весов LoRA. Вместо создания 10 развертываний и их сохранения, мы просто обслуживаем все из них с помощью 1-2 GPU (или более, если есть всплеск запросов).

Реализация

Мы реализовали совместное использование LoRA в API вывода результатов. При выполнении запроса на доступную модель в нашей платформе мы сначала определяем, является ли она LoRA. Затем мы определяем базовую модель для LoRA и направляем запрос в общую ферму бэкэнда, способную обслуживать запросы для данной модели. Запросы вывода результатов обрабатываются путем поддержания базовой модели в разогретом состоянии и загрузки/выгрузки LoRA на лету. Таким образом, мы можем повторно использовать одни и те же вычислительные ресурсы для обслуживания множества отдельных моделей одновременно.

Структура LoRA

В Хабе LoRA можно идентифицировать по двум атрибутам:

Hub

У LoRA есть атрибут base_model. Это просто модель, для которой предназначен LoRA и к которой его следует применять при выполнении вывода.

Поскольку у LoRA не только есть такой атрибут (любая дублированная модель имеет один), LoRA также нуждается в теге lora для правильной идентификации.

Загрузка/выгрузка LoRA для Diffusers 🧨

Для загрузки и выгрузки различных весов LoRA в библиотеке Diffusers используется 4 функции:

load_lora_weights и fuse_lora для загрузки и объединения весов с основными слоями. Обратите внимание, что объединение весов с основной моделью перед выполнением вывода результатов может сократить время вывода на 30%.

unload_lora_weights и unfuse_lora для выгрузки.

Ниже приведен пример того, как можно использовать библиотеку Diffusers для быстрой загрузки нескольких весов LoRA поверх базовой модели:

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"Базовая модель загружена, затрачено времени: {elapsed:.2f} секунд")def inference(adapter, weightname):    start = time.time()    model.load_lora_weights(adapter, weight_name=weightname)    # Совмещение весов Lora со основными слоями повышает время вывода на 30%!    model.fuse_lora()    elapsed = time.time() - start    print(f"Адаптер Lora загружен и совмещен с основной моделью, затрачено времени: {elapsed:.2f} секунд")    start = time.time()    data = model(inputs, num_inference_steps=25).images[0]    elapsed = time.time() - start    print(f"Время вывода, затрачено времени: {elapsed:.2f} секунд")    start = time.time()    model.unfuse_lora()    model.unload_lora_weights()    elapsed = time.time() - start    print(f"Адаптер Lora удален/разгружен из базовой модели, затрачено времени: {elapsed:.2f} секунд")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": "слон", "parameters": {"num_inference_steps": 20}}' > /tmp/base.jpg# Запрос одного адаптера$ curl -H 'lora: minimaxir/sdxl-wrong-lora' 0:8888 -d '{"inputs": "слон", "parameters": {"num_inference_steps": 20}}' > /tmp/adapter1.jpg# Запрос еще одного$ curl -H 'lora: nerijs/pixel-art-xl' 0:8888 -d '{"inputs": "слон", "parameters": {"num_inference_steps": 20}}' > /tmp/adapter2.jpg

А что насчет пакетной обработки?

Недавно вышла очень интересная статья, в которой описывается, как увеличить пропускную способность, выполняя пакетную обработку на моделях LoRA. В кратце, все запросы на вывод собираются в пакет, вычисления, связанные с общей базовой моделью, выполняются одновременно, затем вычисляются остальные адаптер-специфичные артефакты. Мы не реализовали такую ​​технику (близкую к подходу, принятому в text-generation-inference для LLM). Вместо этого мы придерживаемся одиночных последовательных запросов вывода. Причина в том, что мы обнаружили, что пакетная обработка не является интересной для диффузоров: пропускная способность практически не увеличивается с увеличением размера пакета. На простом бенчмарке по генерации изображений, который мы выполнили, пропускная способность увеличилась всего на 25% при размере пакета 8, взамен увеличения задержки в 6 раз! В сравнении с этим пакетная обработка гораздо более интересна для LLM, потому что вы получаете 8-кратное последовательное пропускание с увеличением задержки всего на 10%. Именно поэтому мы не реализовали пакетную обработку для диффузоров.

Заключение: Время!

Используя динамическую загрузку LoRA, мы смогли сохранить вычислительные ресурсы и улучшить пользовательский опыт в Hub Inference API. Несмотря на дополнительное время, затрачиваемое на процесс выгрузки ранее загруженного адаптера и загрузки того, который нас интересует, тот факт, что процесс обслуживания в большинстве случаев уже работает, делает время реакции вывода в целом гораздо короче. Пожалуйста, сообщите нам, если вы примените тот же метод к своему развертыванию!