Когда клиент приходит с задачей «разверните наш 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 под вашу модель и трафик: напишите нам.