Как вы объединяете несколько 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 учитывает геометрию сферы:- Вычислить угол между векторами:
θ = arccos( (ΔW₁·ΔW₂) / (||ΔW₁||·||ΔW₂||) ). - Итоговый вектор:
Δ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, но и полных весов). Состоит из трёх шагов:
-
Trim (урезание): удалить малозначимые изменения. Для каждого параметра оставить только те задачи, в которых изменение по модулю превышает порог (например, топ-20% по абсолютной величине). Остальные занулить.
-
Elect Sign (выбор знака): для каждого параметра определить доминирующий знак среди оставшихся ненулевых изменений (голосование большинством). Если знак различается — параметр считается конфликтным и может быть полностью занулён, либо усреднён только с согласованными.
-
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. Сравнение методов объединения
| Метод | Число адаптеров | Конфликты | Качество после слияния | Сложность реализации |
|---|---|---|---|---|
| SLERP | 2 | Частично решаются (геометрия) | Хорошо для близких задач | Средняя |
| 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.
Шаги:
- Дообучить три LoRA-адаптера на соответствующих датасетах (например, WMT16 для перевода, SST-2 для тональности, CoNLL-2003 для NER).
- Обучить классификатор (логистическая регрессия) на эмбеддингах all-MiniLM-L6-v2 по небольшому размеченному набору «запрос → задача».
- Написать FastAPI-приложение, которое:
- получает текст запроса;
- классифицирует задачу;
- загружает соответствующий адаптер (
set_adapter); - инферит и возвращает результат.
- Сравнить 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 |
Навигация
- Предыдущий: 39
- Следующий: 41
- Индекс: 00. Индекс разборов