автор команда XIMTRX

GPU inference: почему vLLM у нас по умолчанию

Когда клиент приходит с 'разверните наш LLM', первый инфра-вопрос это движок инференса. Объясняем, почему наш дефолт vLLM, что именно он выигрывает по cost-per-token и где мы всё-таки берём TensorRT-LLM.

#ai #llm #vllm #gpu #inference

Когда клиент приходит с задачей «разверните наш LLM в проде», первый инженерный вопрос не про модель и не про GPU, а про движок инференса. От него зависит, сколько токенов в секунду вы снимете с одной и той же карты, а значит и cost-per-token, по которому живёт весь сервис. Наш дефолт это vLLM. Не потому что хайп, а потому что на нашем полигоне он стабильно даёт лучший cost-per-token на типовых serving-нагрузках. Дальше объясняем, что именно он выигрывает и где мы от него отступаем.

Что мы оптимизируем: cost-per-token, не QPS

«Сколько запросов в секунду» это неправильная первичная метрика для LLM-сервиса. Запросы разной длины стоят радикально по-разному: prefill 4000 токенов контекста и декод 50 токенов это две несопоставимые нагрузки на GPU. Мы считаем cost-per-token раздельно для prefill и decode, плюс p99 на time-to-first-token и на inter-token latency.

GPU стоит денег каждую секунду, занят он полезной работой или ждёт. Поэтому вопрос звучит так: какая доля GPU-времени уходит в реальный счёт за токены, а не в простой между запросами. Движок, который держит карту загруженной, выигрывает по cost-per-token, даже если в синтетическом бенче на одном запросе показывает не топовую latency.

Почему vLLM по умолчанию

Два механизма делают vLLM дефолтом для serving-нагрузок.

Continuous batching. Наивный сервер собирает батч, гонит его целиком и только потом берёт следующий: короткие запросы в батче ждут самый длинный. vLLM добавляет и убирает запросы из батча на каждом шаге декода, поэтому закончивший запрос немедленно освобождает место новому. На смешанном трафике, где длины запросов скачут, это поднимает реальную загрузку карты в разы относительно static batching.

PagedAttention. KV-cache это основной потребитель GPU-памяти на инференсе, и наивно он аллоцируется непрерывным блоком под максимальную длину, отчего память фрагментируется и простаивает. vLLM раскладывает KV-cache страницами, как виртуальная память ОС. Фрагментация падает, на ту же карту влезает больше одновременных последовательностей, throughput растёт, cost-per-token падает.

Поверх этого vLLM закрывает рутину, которая иначе ложится на нас: tensor parallelism на несколько карт, совместимый с OpenAI API эндпоинт, потоковую выдачу токенов. Меньше нашего кода между клиентом и GPU означает меньше нашего кода, который ломается в три ночи.

Где vLLM не выигрывает

Дефолт это не догма. Мы уходим с vLLM в нескольких случаях:

  • Жёсткий потолок по latency на одном запросе. Когда клиенту критичен абсолютный минимум time-to-first-token на низком concurrency, а не throughput на высоком, TensorRT-LLM на собранном под конкретную карту engine'е выжимает latency, которой vLLM не достаёт. Цена это потеря гибкости: engine надо пересобирать под модель и под GPU.
  • Зоопарк моделей и модальностей на одном сервере. Когда на одной инфраструктуре крутится разнородный набор моделей и препроцессинг, Triton Inference Server как оркестратор бьёт монодвижок: он разруливает несколько backend'ов и пайплайны за одним фронтом.
  • Экзотические или сильно квантованные веса, которые vLLM на нужной нам версии ещё не держит чисто. Тогда выбор движка диктует поддержка конкретной модели, а не наши предпочтения.

Правило простое: vLLM по умолчанию для serving-нагрузок с переменной длиной и высоким concurrency, специализированный движок там, где у задачи узкое требование, которое дефолт не закрывает.

Операционная сторона

Выбор движка это начало, а не конец. В проде ломается обычно не сам vLLM, а то, что вокруг: KV-cache упирается в память и запросы начинают вытесняться, при росте контекста OOM прилетает не сразу, а на хвосте распределения длин, tensor parallelism чувствителен к топологии NVLink. Поэтому мы алертим не на «процесс жив», а на p99 inter-token latency, на долю preempted-запросов и на занятость KV-cache. Эти сигналы ловят деградацию до того, как её увидит клиентский пользователь.

Как это у клиента

На полигоне мы прогоняем движки на реальных профилях трафика клиента, а не на синтетике: смотрим cost-per-token и p99 на его распределении длин, и уже из чисел выбираем дефолт или отступление от него. Дальше это превращается в развёртывание и сопровождение: какой движок, какая раскладка по картам, какие алерты, кто дежурит.

Если вам нужно поднять LLM-инференс так, чтобы cost-per-token был просчитан, а не «как пойдёт», это то, что мы держим в ai/llm и закрываем через deploy и operate. Хотите прикинуть cost-per-token под вашу модель и трафик: напишите нам.

← Все статьи