LoRA — популярный метод дообучения больших моделей на небольших датасетах, однако на этапе инференса низкоранговые адаптеры работают неэффективно, а их объединение с весами требует хранения отдельной полной копии модели для каждого адаптера.
MultiLoRA решает эту проблему, позволяя одновременно выполнять инференс с несколькими адаптерами на основе одной базовой модели.
В статье мы сравним производительность MultiLoRA-инференса в двух популярных фреймворках — vLLM и TensorRT-LLM. Тесты проведём на готовых релизных Docker-образах, оценивая, какой фреймворк эффективнее обрабатывает батчи запросов в сценариях, близких к офлайн и асинхронному инференсу.
Инференс LoRA
LoRA — эффективный способ дообучения базовой модели на небольших наборах данных. Основное преимущество — существенное сокращение числа обучаемых параметров: вместо в каждом слое дообучаются только
, где
— ранг LoRA.
При инференсе с LoRA возможны два подхода:
Запекание адаптеров в веса модели (объединение LoRA с базовыми весами);
Динамическое применение адаптеров — вычисление активаций адаптеров параллельно с основной моделью и их прибавление к выходу базовой модели.
Первый подход исключает дополнительные вычисления и, потому, эффективнее под нагрузкой. Однако в корпоративных применениях часто нет нагрузки на отдельную модель (а высоконагруженных применений - единицы). При этом те же компании куда менее охотно выделяют дорогое железо под инференс, чем под обучение.
Во втором варианте адаптеры применяются динамически, без создания отдельных копий модели. Однако здесь возникает другая проблема: на этапе инференса LoRA требует выполнения двух матричных операций с "тонкими" матрицами. Из-за их малого размера GPU загружается неэффективно, что снижает общую производительность.
Кроме того, большинство фреймворков по умолчанию не позволяют батчевать запросы, использующие разные LoRA-адаптеры. Это дополнительно ограничивает эффективность.
Multilora
Год назад вышла работа по масштабированию инференса с большим числом адаптеров.
Основная идея работы в том, чтобы не объединять веса адаптеров вместе с весами моделей, а группировать их отдельно на исполнении. Для базовой модели будет исполняться обычное матричное умножение, а для адаптеров - Grouped Gemm.
За счет группировки обе операции будут выполняться над большими размерами матриц, что позволяет достигать хорошей утилизации.

Сейчас подобная идея реализована в нескольких фреймворках, включая vLLM и TensorRT-LLM, производительность которых мы и сравним далее.
Формат тестов
Мы не будем смотреть на сетевую обвязку, только на подачу батча промптов на генерацию. И так как мы не смотрим на сетевую часть, нет смысла измерять time-to-first token, потому что он зависит от батчинга и эффективности поточной работы движка, что не совсем про тему MultiLoRA и сильно бы раздуло статью.
Мы также буду использовать только python обвязки (без явной работы с c++) из официальных release docker-ов обоих фреймворков. Возможно, поэтому tensorrt-llm окажется плох.
Но именно так большинство людей и будет использовать оба фреймворка для своих задач.
Статья скорее отвечает на вопрос - как будут работать оба фреймворка в MultiLoRA сетапе, если вы просто возьмете готовый docker и запустите как есть для своих задач (примерно так, на самом деле, большинство и сделают).
Получается, что замеры в статье не про ultra-fast-realtime инференс, а скорее про офлайн или ассинхронный его вариант (таких применений тоже много).
Сетап моделей:
Все тесты проводятся в формате bfloat16.
Квантизации за рамками замеров.
Для GPU RTX 4090 используется LLAMA-3.2-3B модель, поскольку 8B-вариант не помещается в 24 ГБ видеопамяти при контексте более 2–3 тыс. токенов.
Для H100 - LLAMA-3.2-8B модель, как жизнеспособный вариант для большинства бизнес приложений.В тестах на переменный контекст будет сразу подаваться много запросов (20) и время замеряется целиком на весь батч (выше объяснил, почему решено так делать).
Железо для замеров
Замеры сделаны на сервере с одной GPU с следующими сетапами:
Nvidia 4090 24Gb, AMD EPYC 7402, 64Gb RAM
Nvidia H100 SXM 80Gb, AMD EPYC 9554, 192Gb RAM
Код
Код замеров выложен тут.
Можно адаптировать под себя и сделать более точечные замеры или написать в комментарии полезные параметры для ускорения.
Почему не использовать transformers peft?
Пользователям, привыкшим к использованию интерфейса transformers и обучающим LoRA, может захотеться развернуть инференс самым простым образом, через set_adapter.
Peft vs vllm при увеличении контекста (перемалываем 20 запросов):
context size |
1000 |
2000 |
4000 |
---|---|---|---|
peft |
420.09 |
418.52 |
461.24 |
peft per request |
21.00 |
20.93 |
23.06 |
vllm |
5.14 |
5.60 |
6.91 |
Peft vs vllm при увеличении количества запросов при контексте 8000 токенов:
num request |
1 |
2 |
4 |
6 |
---|---|---|---|---|
peft |
23.42 |
47.2 |
94.32 |
140.86 |
peft per request |
23.42 |
23.6 |
23.58 |
23.47 |
vllm |
1.68 |
5.63 |
7.29 |
8.09 |
Однако так делать не надо. По замерам видно, что вне зависимости от небольших изменений размера контекста или числа запросов - общее время исполнения растет линейно, а среднее время выполнения одного запроса - одинаково высокое и никак не амортизируется.
Можно выделить 2 причины такой неэффективности:
Инференс будет всегда с размером батча равным 1.
Между каждыми двумя запросами вызывается set_adapter, состоящий из операций вычитания, умножения и суммы на каждый адаптируемый параметр.
Сложность установки
С точки зрения простоты установки и запуска vLLM выигрывает безоговорочно — он устанавливается через pip и работает «из коробки» без дополнительных манипуляций.
С TensorRT-LLM ситуация заметно сложнее:
Актуальные pip-сборки не работают на CUDA ≤ 12.4: при запуске возникает ошибка линковки сервисных функций.
При сборке из исходников требуется CUDA ≥ 12.8, иначе компиляция падает. Причина — в исходном коде много необёрнутых #include-ов, в том числе заголовки под Blackwell и поддержку FP4, которые не компилируются на более старых версиях.
Ручная сборка требуется править пути к .so-библиотекам вручную, так как они жёстко зашиты в файлах конфигурации.
Вывод: использовать TensorRT-LLM имеет смысл только через готовый Docker-образ или если pip wheel нормально установится. Установка вручную — потенциально долгий и болезненный процесс.
Образы, использованные в тестах:
vLLM: vllm/vllm-openai:v0.9.1-2025-06-22
TensorRT-LLM: nvcr.io/nvidia/tensorrt-llm/release:0.20.0
Multi-lora overhead
Недавно коллеги из крупных ML-команд заметили, что использование LoRA-адаптеров в продакшене влечёт за собой определённые накладные расходы — и если объёмы данных и нагрузка на каждую задачу позволяют, лучше делать полный fine-tuning. Моё изначальное мнение было противоположным: казалось, что накладные расходы LoRA не должны быть большими и в большинстве случаев ими можно пренебречь.
Давайте разберёмся, действительно ли использование LoRA-адаптера заметно влияет на производительность.

Сырые замеры для H100
H100 TensorRT LLM
Context size |
1000 |
2000 |
4000 |
8000 |
16000 |
32000 |
---|---|---|---|---|---|---|
trt_llm_nolora |
5.29 |
5.72 |
6.74 |
8.65 |
12.62 |
49.44 |
trt_llm_lora |
6.71 |
7.04 |
7.99 |
10.05 |
14.10 |
53.86 |
trt_llm_ratio |
1.27 |
1.23 |
1.19 |
1.16 |
1.12 |
1.09 |
H100 vllm
Context size |
1000 |
2000 |
4000 |
8000 |
16000 |
32000 |
---|---|---|---|---|---|---|
vllm_nolora |
4.54 |
4.92 |
5.83 |
8.19 |
11.35 |
44.06 |
vllm_lora |
5.14 |
5.60 |
6.91 |
8.65 |
12.17 |
50.48 |
vllm_ratio |
1.13 |
1.14 |
1.19 |
1.06 |
1.07 |
1.15 |
Сырые замеры для 4090
4090 TensorRT LLM
Context size |
1000 |
2000 |
4000 |
8000 |
16000 |
32000 |
---|---|---|---|---|---|---|
vllm_nolora |
6.33 |
7.34 |
9.31 |
27.04 |
46.73 |
115.50 |
vllm_lora |
6.72 |
7.69 |
10.87 |
25.21 |
55.62 |
130.82 |
vllm_ratio |
1.06 |
1.05 |
1.17 |
0.93 |
1.19 |
1.13 |
4090 TensorRT LLM
Context size |
1000 |
2000 |
4000 |
8000 |
16000 |
32000 |
---|---|---|---|---|---|---|
trt_llm_nolora |
9.46 |
19.67 |
33.35 |
58.78 |
122.44 |
250.84 |
trt_llm_lora |
10.28 |
20.63 |
35.97 |
63.01 |
135.91 |
265.34 |
trt_llm_ratio |
1.09 |
1.05 |
1.08 |
1.07 |
1.11 |
1.06 |
На первый взгляд, накладные расходы кажутся непропорционально большими: адаптируемые веса LoRA занимают всего ~12 млн параметров (12,156,928), что составляет всего 0.4 % от общего числа параметров в модели на 3 миллиарда (3,224,906,752). Казалось бы, настолько малая добавка не должна оказывать значимого влияния.
Но с другой стороны, в некоторых реальных продакшен-условиях, где:
Ограничены общие квоты на число GPU в инференсе,
Нагрузка на каждый адаптер нестабильна или невелика,
Важно гибко переключаться между задачами,
— издержки на LoRA становятся допустимыми, особенно если используется MultiLoRA. Этот подход позволяет батчевать запросы от разных адаптеров на общей базе модели, улучшая утилизацию GPU и компенсируя неэффективность отдельных операций.
Скейлинг по контексту


Таблица с замерами
Context size |
1000 |
2000 |
4000 |
8000 |
16000 |
32000 |
---|---|---|---|---|---|---|
vllm_4090 |
6.72 |
7.69 |
10.87 |
25.21 |
55.62 |
130.82 |
trt_llm_4090 |
10.28 |
20.63 |
35.97 |
63.01 |
135.91 |
265.34 |
vllm_h100 |
5.14 |
5.60 |
6.91 |
8.65 |
12.17 |
50.48 |
trt_llm_h100 |
6.71 |
7.04 |
7.99 |
10.05 |
14.10 |
53.86 |
В текущих тестах vLLM демонстрирует лучшую масштабируемость (не требуя особых настроек). Хотя по опубликованным бенчмаркам в сети TensorRT-LLM должен показывать более высокую производительность. Возможно, дело в неоптимальных настройках по умолчанию, накладных расходах на Python-обвязки или менее гибком runtime для генерации.
Но важно понимать: 95 % пользователей вряд ли будут вручную настраивать low-level параметры или переписывать инференс на C++ (особенно, в офлайн и асинхронном инференсе).
Вот два источника с бенчмарками конца 2024 года, где TensorRT-LLM показывал лучшую производительность:
Squeezebits Blog (октябрь 2024) — общее сравнение производительности vLLM и TensorRT-LLM.
vLLM Performance Update (сентябрь 2024) — данные по оптимизациям в последних релизах.
Скейлинг по числу запросов


Исходные таблицы
Num requests |
1 |
2 |
4 |
6 |
8 |
12 |
16 |
---|---|---|---|---|---|---|---|
vllm_4090 |
1.68 |
5.63 |
7.29 |
8.09 |
8.45 |
11.38 |
12.66 |
trt_llm_4090 |
9.59 |
10.27 |
11.43 |
23.64 |
26.55 |
39.92 |
53.20 |
vllm_h100 |
1.24 |
4.80 |
5.25 |
5.69 |
5.68 |
6.50 |
7.43 |
trt_llm_h100 |
6.13 |
6.09 |
6.67 |
7.18 |
7.66 |
8.47 |
9.24 |
Num requests |
1 |
2 |
4 |
6 |
8 |
12 |
16 |
---|---|---|---|---|---|---|---|
vllm_4090 |
6.09 |
7.24 |
8.62 |
11.15 |
10.19 |
28.23 |
41.75 |
trt_llm_4090 |
9.52 |
10.63 |
25.79 |
38.73 |
53.27 |
80.61 |
105.30 |
vllm_h100 |
5.01 |
5.09 |
5.77 |
7.40 |
6.76 |
8.66 |
9.72 |
trt_llm_h100 |
6.41 |
6.63 |
7.62 |
8.46 |
9.18 |
10.79 |
12.38 |
Та же самая картина. TensorRT-LLM теоретически должен быть быстрее, но в реальности — при использовании стандартных настроек и Python API — vLLM снова уверенно выигрывает.
На практике это означает: без тонкой настройки trt-llm уступает и по скорости, и по удобству.
Скейлинг по количеству LoRa адаптеров
Замеры при увеличении количества адаптеров (перемалываем 20 запросов), контекст 16000 токенов:
Num adapters |
1 |
2 |
4 |
8 |
---|---|---|---|---|
trt_llm_4090 |
143.807280 |
142.092142 |
140.954860 |
oom |
vllm_4090 |
48.812364 |
48.834726 |
48.874838 |
48.843075 |
TensorRT-LLM в дефолтной конфигурации упал по OOM при 8 LoRA-адаптерах на 3B модели и 4090 видеокарте. При этом vLLM тот же сценарий выдержал без проблем.
Как и ожидалось, увеличение числа адаптеров не приводит к сильной просадке производительности. Единственное — при большом числе адаптеров каждый из них получает меньший батч, что может немного снижать эффективность матричных операций.
Оффлоадинг для генерации
В этих тестах мы не рассматриваем все варианты оффлоадинга и работу с длинными сессиями — это отдельная и объёмная тема. Вместо этого сосредоточимся на более прикладном вопросе: имеет ли смысл включать offloading KV-кеша в типовых оффлайн или асинхронных запросах, когда нет гигантских диалогов, но есть ограниченная память GPU?

Исходная таблица
Context Size |
1000 |
2000 |
4000 |
8000 |
16000 |
32000 |
---|---|---|---|---|---|---|
vllm_4090_no_offload |
6.72 |
7.69 |
10.87 |
25.21 |
55.62 |
130.82 |
vllm_4090_offload |
262.24 |
169.41 |
158.88 |
139.15 |
887.97 |
1762.07 |
trt_llm_4090_no_offload |
10.28 |
20.63 |
35.97 |
63.01 |
135.91 |
265.34 |
trt_llm_4090_offload |
10.29 |
20.45 |
36.47 |
63.96 |
137.71 |
269.48 |
Интуитивно кажется, что при нехватке памяти можно сбросить часть KV-кеша в RAM и продолжить генерацию. Но всё не так просто:
Скорость передачи в CPU-память слишком низкая, поэтому выгоды по времени не получается — ускорения нет;
Оба фреймворка — и vLLM, и TensorRT-LLM — почти не используют CPU-память в стандартных настройках, если речь не про длинные диалоги;
Однако, при включённом оффлоадинге, vLLM показывает заметную деградацию производительности, даже в простом сценарии с одним generate() и без переполнения GPU. При этом htop не показывает большого использования оперативной памяти. Скорее похоже, что отключается какая-то из оптимизаций, типа chunked prefill.
Вывод: в базовых сценариях offloading не даёт выигрыша, а иногда даже вредит.
Выводы
vLLM оказался быстрее, стабильнее и заметно проще в использовании из коробки. Его установка не требует лишних усилий, а производительность остаётся высокой даже без дополнительной настройки.
TensorRT-LLM, напротив, в текущем виде требует ручной сборки и тонкой настройки — в дефолтной конфигурации он проигрывает по всем ключевым метрикам.
Для офлайн- и асинхронного инференса выбор очевиден — vLLM.
Онлайн-сценарии в этой статье не рассматривались.
Поддержка MultiLoRA есть в обоих фреймворках добавляет ~10-15 % накладных расходов, но существенно улучшает утилизацию GPU в задачах с несколькими адаптерами.
Канал автора: @deploy_ml