Как вы объединяете несколько LoRA адаптеров для разных задач?

Краткий тезис

LoRA (Low-Rank Adaptation) позволяет дообучать большие модели с минимальным числом добавляемых параметров. Когда у нас есть несколько адаптеров, каждый для своей задачи (например, перевод, суммаризация, классификация), возникает необходимость объединить их в одну модель без потери качества. Основные методы: SLERP (сферическая линейная интерполяция), Task Vector Arithmetic (векторная арифметика) и TIES-Merging (урезание, согласование знаков, усреднение). Однако на практике лучший подход — не смешивать адаптеры, а использовать task prompt routing: хранить отдельные адаптеры и динамически выбирать нужный по задаче. Это полностью устраняет конфликты.


1. Термин: LoRA (Low-Rank Adaptation)

LoRA — метод параметро-эффективного дообучения (PEFT), при котором веса предобученной модели остаются замороженными, а для каждого слоя (обычно attention) обучается пара низкоранговых матриц A и B Если исходная матрица весовW имеет размерность d × k, то:

W' = W + ΔW, где ΔW = B·A, B ∈ ℝ^{d×r}, A ∈ ℝ^{r×k}, r << min(d, k)

Ранг r — гиперпараметр (обычно 8, 16, 32). После обучения мы сохраняем только матрицы A и B — это и есть LoRA-адаптер. Для нескольких задач у нас есть несколько пар (A₁, B₁), (A₂, B₂), ... Каждая пара представляет собой дельта-веса ΔW относительно базовой модели.

Объединение адаптеров — это процесс сведения нескольких ΔW в одно ΔW_merged, чтобы модель могла выполнять все задачи одновременно без переключения.


2. Зачем объединять адаптеры?

Сценарии:

  • Развёртывание одного сервиса, который обрабатывает несколько доменов (медицина, юриспруденция, техподдержка) — хочется иметь одну модель, а не несколько инстансов.
  • Перенос знаний: адаптер для задачи A и адаптер для задачи B вместе могут дать синергию.
  • Экономия памяти: вместо N загруженных адаптеров (каждый с матрицами B·A) храним один суммарный.

Однако прямое сложение или усреднение часто приводит к конфликту адаптеров: разные задачи требуют противоположных изменений одних и тех же весов. Например, одна задача может увеличивать активацию определённого нейрона, а другая — уменьшать. В результате смесь ухудшает обе.


3. Метод 1: SLERP (Spherical Linear Interpolation)

SLERP — интерполяция между двумя точками на поверхности гиперсферы. Применяется для сглаженного перехода между двумя наборами весов.

В контексте LoRA:

  • Пусть есть два адаптера ΔW₁ и ΔW₂, приведённые к одинаковой размерности (можно представить их как плоские векторы).
  • Вместо линейной интерполяции (1-α)ΔW₁ + αΔW₂ (которая не сохраняет норму), SLERP учитывает геометрию сферы:
    1. Вычислить угол между векторами: θ = arccos( (ΔW₁·ΔW₂) / (||ΔW₁||·||ΔW₂||) ).
    2. Итоговый вектор: ΔW = sin((1-α)θ)/sin(θ) · ΔW₁ + sin(αθ)/sin(θ) · ΔW₂.

Когда применимо:

  • Только два адаптера (для трёх и более нужны иерархические схемы).
  • Предполагается, что адаптеры лежат в одном семантическом пространстве (например, оба дообучены на похожих данных).
  • Простая реализация, но не решает конфликты знаков.

Пример кода (NumPy):

import numpy as np

def slerp(v1, v2, alpha):
    # нормируем векторы
    v1_norm = v1 / np.linalg.norm(v1)
    v2_norm = v2 / np.linalg.norm(v2)
    dot = np.dot(v1_norm, v2_norm)
    # избегаем выхода за [-1,1] из-за погрешностей
    dot = np.clip(dot, -1.0, 1.0)
    theta = np.arccos(dot)
    if theta < 1e-10:  # векторы почти коллинеарны
        return (1 - alpha) * v1 + alpha * v2
    coef1 = np.sin((1 - alpha) * theta) / np.sin(theta)
    coef2 = np.sin(alpha * theta) / np.sin(theta)
    return coef1 * v1_norm + coef2 * v2_norm

4. Метод 2: Task Vector Arithmetic

Простая идея: взять task vectors — разности между весами дообученного адаптера и базовой модели (или между адаптером и нулевым смещением). Для LoRA это сами матрицы ΔW.

Формула для объединения N адаптеров:

ΔW_merged = ΔW₁ + ΔW₂ + ... + ΔW_N

Или взвешенная сумма с коэффициентами λᵢ, которые можно подобрать на валидации.

Преимущества: простота, можно комбинировать любое число адаптеров.

Недостатки: игнорирует знаковые конфликты. Если для одного параметра ΔW₁[i,j] = +0.5, а ΔW₂[i,j] = -0.3, то сумма даст +0.2, что может ослабить эффект обеих задач. Кроме того, простое сложение может сильно сместить распределение весов.

Модификации: scale-and-add (умножить каждый ΔW на скаляр перед сложением) или добавление регуляризации (например, L2 к норме итогового адаптера).


5. Метод 3: TIES-Merging (Trim, Elect Sign, Merge)

TIES — современный алгоритм, предложенный для объединения нескольких дообученных моделей (не только LoRA, но и полных весов). Состоит из трёх шагов:

  1. Trim (урезание): удалить малозначимые изменения. Для каждого параметра оставить только те задачи, в которых изменение по модулю превышает порог (например, топ-20% по абсолютной величине). Остальные занулить.

  2. Elect Sign (выбор знака): для каждого параметра определить доминирующий знак среди оставшихся ненулевых изменений (голосование большинством). Если знак различается — параметр считается конфликтным и может быть полностью занулён, либо усреднён только с согласованными.

  3. Merge (слияние): усреднить оставшиеся ненулевые изменения (только с выбранным знаком) с весами, равными числу задач, поддерживающих этот знак.

Формально (для одного параметра p):

  • Есть N изменений d₁,...,d_N.
  • Trim: d'_i = d_i если |d_i| > τ, иначе 0.
  • Elect: sign = majority_vote(sign(d'_i) for i where d'_i ≠ 0). Если голосов поровну или нет ненулевых — знак не определён, параметр остаётся нулевым.
  • Merge: d_merged = (1/|S|) * Σ_{i in S} d'_i, где S — индексы, у которых sign(d'_i) = выбранный sign.

TIES существенно уменьшает конфликты и часто даёт лучший компромисс. Для LoRA применяется отдельно к каждой паре матриц A и B.


6. Проблема конфликтов адаптеров

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

Демонстрация конфликта (гипотетическая):

  • Задача A (перевод) усиливает веса attention на 10%.
  • Задача B (суммаризация) ослабляет те же веса на 5%.
  • После сложения ΔW_merged = +5% → ни перевод, ни суммаризация не работают идеально.

Для решения этой проблемы и разработаны TIES и аналоги (DARE, RegMean, Fisher Merging).


7. Практическая альтернатива: Task Prompt Routing

Вместо слияния весов — task prompt routing. Храним N отдельных адаптеров и переключаемся между ними в зависимости от запроса. Routing может реализовываться двумя основными способами:

  • Prompt-level routing: добавляем к входу задачи специальный токен (e.g., <TASK_A>) или natural language instruction. Модель (например, PeftModel) выбирает адаптер по этому токену.
  • Classifier-based routing: обучаем небольшой классификатор (например, на основе эмбеддингов запроса) предсказывать, какой адаптер использовать.

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

  • Нет конфликтов — каждый адаптер обучен только на своей задаче.
  • Можно добавлять/удалять адаптеры без пересчёта остальных.
  • Простота деплоя: базовая модель + набор файлов адаптеров.

Недостатки: память растёт линейно с числом задач, но для сотен адаптеров это приемлемо (каждый ~ несколько МБ). Также требуется routing-логика на стороне инференса.


8. Реализация routing с помощью PEFT (Hugging Face)

Библиотека peft поддерживает множественные адаптеры через PeftModel. Пример:

from peft import PeftModel, get_peft_model
from transformers import AutoModelForCausalLM

base = AutoModelForCausalLM.from_pretrained("base-model")

# Загружаем адаптеры
model = PeftModel.from_pretrained(base, "adapter-task-a")
model.load_adapter("adapter-task-b", adapter_name="task_b")

# Переключение по имени
model.set_adapter("task_a")   # активен адаптер задачи A
output_a = model.generate(...)
model.set_adapter("task_b")   # активен адаптер задачи B
output_b = model.generate(...)

Можно также использовать PeftMixedModel для одновременного применения нескольких адаптеров на разных слоях, но это сложнее и редко нужно.

Router можно реализовать как простую функцию, которая по содержимому запроса (или метаданным) вызывает set_adapter().


9. Сравнение методов объединения

МетодЧисло адаптеровКонфликтыКачество после слиянияСложность реализации
SLERP2Частично решаются (геометрия)Хорошо для близких задачСредняя
Task Vector ArithmeticЛюбоеСильные (знаковые)Часто ухудшаетсяНизкая
TIES-MergingЛюбоеХорошо подавляютсяОбычно лучшее среди слиянийВысокая (алгоритм)
Task Prompt RoutingЛюбоеОтсутствуютСохраняется исходное качество каждого адаптераНизкая (переключение)

Вывод: для production чаще выбирают Routing из-за простоты и надёжности. Если же необходимо физически объединить адаптеры (ограничения по памяти, latency), используют TIES-Merging.


10. Лучший подход: routing, а не слияние

На собеседовании стоит подчеркнуть, что на практике я предпочитаю не смешивать адаптеры, а использовать task prompt routing. Это интуитивно понятный, отказоустойчивый и легко масштабируемый подход. Если интервьюер настаивает на слиянии, можно показать знание TIES-Merging, объяснив, как он решает конфликты знаков.

Дополнительные соображения:

  • Routing также позволяет комбинировать адаптеры в runtime: для одного запроса можно последовательно применить несколько (cascade).
  • Для задач, где доступно небольшое количество данных, слияние может дать выигрыш за счёт регуляризации, но это требует тюнинга.

Пет-проект для закрепления

Задача: Создать сервис для обработки трёх задач (перевод EN→RU, тональный анализ, извлечение именованных сущностей) с использованием единой модели T5-small и трёх LoRA-адаптеров. Реализовать routing через классификатор на основе эмбеддингов BERT.

Инструменты: Python, HuggingFace Transformers, PEFT, SentenceTransformers, FastAPI.

Шаги:

  1. Дообучить три LoRA-адаптера на соответствующих датасетах (например, WMT16 для перевода, SST-2 для тональности, CoNLL-2003 для NER).
  2. Обучить классификатор (логистическая регрессия) на эмбеддингах all-MiniLM-L6-v2 по небольшому размеченному набору «запрос → задача».
  3. Написать FastAPI-приложение, которое:
    • получает текст запроса;
    • классифицирует задачу;
    • загружает соответствующий адаптер (set_adapter);
    • инферит и возвращает результат.
  4. Сравнить latency и качество с вариантом, где используется TIES-Merging на тех же адаптерах (объединить веса один раз).

Ожидаемый результат:

  • Routing покажет более высокое качество по каждой задаче (особенно на граничных примерах).
  • TIES-Merging будет работать быстрее на одном инстансе (нет переключения), но качество может упасть на 1–5%.
  • Убедиться, что routing интуитивно понятен и легко деплоится.

Связь с другими вопросами

ВопросТема
38Как работает LoRA и чем отличается от AdaLoRA
39Как выбрать ранг LoRA и от чего он зависит
41Как обучать LoRA для нескольких задач одновременно (multi-task)
44Что такое Q-LoRA и когда его использовать
45Что такое DoRA и как он улучшает LoRA
47Как вы загружаете и выгружаете LoRA адаптеры в production

Навигация