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
