Короткий ответ: vLLM может заметно опережать llama.cpp на этапе prefill, когда GPU получает длинные промпты или несколько запросов одновременно. Преимущество обычно связано с планировщиком, continuous batching, управлением KV-кэшем и специализированными CUDA-ядрами. При одном коротком запросе разница может исчезнуть, а иногда llama.cpp даст меньший TTFT.
Prefill обрабатывает весь входной промпт и готовит KV-кэш для дальнейшей генерации. vLLM старается загрузить GPU крупными матричными операциями и плотнее разместить несколько последовательностей в памяти. PagedAttention при этом не уменьшает число вычислений для одного токена, зато помогает вместить больше активных запросов и реже терять доступную VRAM из-за фрагментации.
Поэтому сравнение в режиме «одна модель, один запрос» описывает лишь один сценарий. Для серверной нагрузки нужно отдельно измерять время до первого токена, скорость обработки входных токенов, задержку в очереди, throughput при нескольких пользователях и скорость decode. Ниже разберём, откуда берётся разница и как проверить её без ложных выводов.
Что такое prefill и почему он важен
LLM-инференс состоит из двух разных по характеру фаз. Во время prefill движок получает токенизированный промпт, прогоняет его через все слои модели, вычисляет logits для следующего токена и сохраняет ключи и значения attention в KV-кэш. Во время decode движок генерирует последующие токены, используя уже накопленный кэш.
Prefill vs Decode: две фазы инференса
Prefill хорошо распараллеливается по токенам входной последовательности. Модель может обрабатывать большой фрагмент промпта одной серией матричных операций, соблюдая causal mask, которая запрещает токену смотреть в будущее. Последовательный характер проявляется главным образом при генерации: следующий выходной токен появляется после предыдущего, поэтому decode выполняет много небольших шагов.
Для prefill обычно важны размеры матриц, эффективность attention и загрузка вычислительных блоков GPU. На длинных промптах матричные операции могут хорошо использовать tensor cores. На коротких запросах заметную долю времени занимают запуск ядер, копирование данных, токенизация и служебные операции. Decode чаще упирается в задержку отдельных шагов и пропускную способность памяти, потому что на каждом шаге обрабатывается всего один новый токен на последовательность.
Пример: промпт на 1 000 токенов требует обработать весь вход до появления первого ответа. Если модель затем выдаёт 100 токенов, эти 100 шагов decode выполняются последовательно. При длинном контексте prefill может занимать большую часть времени до первого ответа, хотя итоговая генерация окажется короткой.
Для оценки используют несколько метрик:
- TTFT, Time To First Token, время от отправки запроса до первого токена ответа;
- prefill throughput, количество входных токенов в секунду;
- decode TPS, скорость генерации последующих токенов;
- aggregate throughput, суммарная скорость обработки всех запросов при заданной нагрузке.
TTFT не равен чистому времени prefill. В него могут входить токенизация, ожидание в очереди, планирование, передача данных и первый шаг decode. Для клиентского теста это нормальная пользовательская метрика, но для сравнения ядер и attention нужно по возможности отделять очередь и сетевые задержки.
Ключевые архитектурные отличия vLLM от llama.cpp
vLLM создавался прежде всего как серверный движок для обслуживания большого числа запросов на GPU. llama.cpp исторически ориентирован на локальный запуск моделей, включая CPU, интегрированную графику и разные GPU-бэкенды. Это не делит движки на быстрый и медленный навсегда: итог зависит от модели, формата весов, backend, размера контекста и количества пользователей.
Главное различие проявляется в том, как движок распоряжается общим вычислительным бюджетом. vLLM старается планировать токены разных последовательностей вместе. llama.cpp тоже умеет работать с batch и серверными запросами, но его типичная локальная конфигурация чаще нацелена на предсказуемую работу одного или небольшого числа запросов.
Continuous batching: как vLLM утилизирует GPU
При static batching движок ждёт, пока соберётся группа запросов, объединяет их и обрабатывает как один batch. Короткий запрос может ждать длинный, а свободные вычислительные блоки простаивают, если последовательности имеют разную длину.
Continuous batching добавляет и убирает последовательности на границах итераций планировщика. Новые запросы могут подключаться к работе, пока другие последовательности находятся на decode. Такой подход уменьшает простои и повышает суммарное число обработанных токенов за секунду.
Для prefill это особенно заметно на сервере с несколькими пользователями. Допустим, в очереди находятся четыре промпта по 2 048 токенов. При лимите 8 192 batched tokens их можно рассматривать как условный объём одной итерации. Если одновременно пришли ещё четыре запроса, планировщик разделит работу между итерациями, учитывая свободную память и активные decode-шаги.
Увеличение batch не даёт бесплатного ускорения. Крупный batch чаще повышает aggregate throughput, но может увеличить ожидание отдельного запроса. Серверу приходится выбирать между быстрым первым ответом и максимальной загрузкой GPU. vLLM получает преимущество именно там, где этот компромисс можно использовать: запросов достаточно, а GPU способен выполнять крупные операции параллельно.
PagedAttention и управление KV-кэшем
KV-кэш растёт вместе с длиной контекста. Для каждой активной последовательности движок хранит ключи и значения attention на каждом слое. При нескольких пользователях объём кэша быстро становится одним из главных ограничений VRAM.
PagedAttention разбивает KV-кэш на блоки фиксированного размера. Логическая последовательность видит непрерывный контекст, а физически её блоки могут находиться в разных участках памяти. Планировщик хранит таблицу соответствия между логическими позициями и блоками памяти.
Такой подход уменьшает потери памяти на заранее зарезервированных, но ещё не заполненных участках. Последовательности разной длины занимают ближе к реальному размеру нужного кэша. Освободившиеся блоки можно отдавать новым запросам, что увеличивает число одновременно обслуживаемых последовательностей.
Прямого сокращения FLOPs для одного промпта здесь нет. Если запустить одну и ту же последовательность без конкурирующих запросов, PagedAttention не обязан сделать отдельную матрицу быстрее. Выигрыш появляется через вместимость: больше активных последовательностей помещается в VRAM, а continuous batching получает больше материала для общей работы.
В llama.cpp пользователь чаще работает с заранее заданным контекстным буфером и параметрами batch, хотя конкретное поведение зависит от версии, backend и настроек KV-кэша. Сравнивать такую схему с vLLM нужно по фактическому объёму занятой памяти и числу активных последовательностей, а не по названию функции.
Оптимизации ядер и поддержка GPU
На совместимом GPU vLLM может использовать FlashAttention, FlashInfer и специализированные CUDA-ядра для отдельных операций. Их задача состоит в том, чтобы уменьшить лишние обращения к глобальной памяти, объединить несколько этапов вычисления и подобрать форму операций под архитектуру видеокарты.
Эффект зависит от формы задачи. Для prompt на 512 токенов и prompt на 16 384 токена GPU выполняет разные по размеру матрицы и получает разную загрузку. Поведение меняется и при квантизации: низкая точность уменьшает объём данных, но добавляет операции распаковки или использует другие kernels.
llama.cpp тоже содержит CUDA-оптимизации, Flash Attention и отдельные пути для CPU, Metal и других бэкендов. Широкая поддержка устройств не означает автоматического штрафа на каждом GPU. Она означает, что результат сильнее зависит от конкретной сборки, версии драйвера, параметров n_batch, n_ubatch и выбранного пути вычислений.
Почему сравнение «одна модель, один запрос» вводит в заблуждение
Одиночный запрос удобен для воспроизводимости, но он почти не показывает работу серверного планировщика. В таком тесте vLLM не получает главного преимущества, ради которого его часто выбирают: возможности распределить GPU между несколькими активными последовательностями.
Влияние batching на метрики
Batching меняет сразу две величины. Individual latency показывает, сколько ждал конкретный запрос. Throughput показывает, сколько токенов или запросов система обработала за единицу времени. Эти метрики могут двигаться в разные стороны.
При concurrency 1 vLLM и llama.cpp обрабатывают один входной массив. Разница будет зависеть от kernels, формата весов, копирования данных и служебного overhead. При concurrency 10 vLLM может объединять prompt-токены разных запросов и лучше заполнять матричные операции. Это способно резко поднять суммарный throughput, хотя TTFT отдельного запроса вырастет из-за очереди.
Похожий эффект возникает при смешанной нагрузке. Один пользователь генерирует ответ, второй отправляет длинный RAG-промпт, третий ждёт короткую классификацию. Планировщик должен распределить токены prefill и шаги decode так, чтобы длинная обработка входа не блокировала весь сервис.
Практический разбор GLM-5.3-Flash хорошо показывает, почему нельзя переносить результат одной фазы на другую: в описанной конфигурации prefill llama.cpp и TensorSharp находились почти на одном уровне, а decode у TensorSharp был примерно вдвое быстрее. Это сравнение относится к конкретной модели, сборкам и настройкам, но сам принцип универсален: движок может выигрывать на decode и не выигрывать на prefill.
Роль планировщика и очереди запросов
Планировщик решает, сколько последовательностей и сколько токенов отдать GPU на очередной итерации. Для этого ему нужны сведения о длине промпта, текущем размере KV-кэша, доступной памяти, фазе каждого запроса и лимитах batch.
Условный параметр max_num_batched_tokens ограничивает число токенов, обрабатываемых за итерацию. max_num_seqs ограничивает число последовательностей. Первый лимит влияет на объём работы prefill, второй защищает планировщик от слишком большого количества активных запросов.
Очередь добавляет отдельный компонент задержки. Запрос с быстрым prefill может показать высокий TTFT, если он несколько сотен миллисекунд ждал свободного token budget. Поэтому сравнение API без раздельной записи времени постановки в очередь и времени вычислений смешивает скорость движка с политикой обслуживания.
llama.cpp в серверном режиме тоже может принимать несколько запросов, однако его поведение зависит от конкретного сервера, параметров batch и используемой сборки. Формулировка «vLLM всегда быстрее» здесь неверна. Корректнее говорить о преимуществе vLLM в нагрузках, где его планировщик и управление памятью получают достаточно параллельной работы.
Как корректно бенчмаркать prefill
Честный тест начинается с фиксации условий. Нужны одна и та же модельная архитектура, одинаковый размер контекста, сопоставимый формат весов, один GPU и одинаковая нагрузка. Сравнение BF16-модели в vLLM с сильно квантованной GGUF-моделью в llama.cpp измеряет несколько изменений сразу.
Метрики: TTFT, TPS, throughput
Для пользовательского сценария записывайте TTFT и его перцентили, например p50 и p95. Среднее значение может скрыть редкие длинные ожидания в очереди. Для чистого prefill фиксируйте количество входных токенов и измеряйте время обработки prompt отдельно.
Простая формула выглядит так: prefill tokens/s = число входных токенов / время prefill. Если 8 000 токенов обработаны за 0,8 секунды, результат составляет 10 000 входных токенов в секунду. Это пример расчёта, а не универсальный показатель конкретного движка.
Decode измеряйте отдельно: decode TPS = число сгенерированных токенов / время генерации. Для сервера с несколькими пользователями добавляйте aggregate prompt throughput, aggregate decode throughput, количество запросов в секунду и p95 TTFT.
В клиентском тесте TTFT можно представить как сумму сетевого времени, токенизации, ожидания в очереди, prefill, первого шага decode и передачи первого токена. Если цель состоит в сравнении GPU-ядер, используйте телеметрию движка или профилировщик, чтобы выделить вычислительную часть.
Типичные ошибки при сравнении
- Разное железо. RTX, MI и Apple Silicon используют разные backend и пути kernels. Даже две видеокарты одной серии могут работать на разных частотах и иметь разную пропускную способность памяти.
- Разная точность. FP16, BF16, INT8, FP8 и GGUF-кванты меняют объём памяти, набор операций и качество модели. Сравнивайте сопоставимые варианты.
- Разная длина prompt. Запуск на 512 токенах не описывает поведение на 8 192 или 16 384 токенах. Соберите несколько точек по длине контекста.
- Разная concurrency. Значения для одного запроса, четырёх запросов и шестнадцати запросов отвечают на разные вопросы. Их нельзя сводить в одну таблицу без подписи.
- Отсутствие прогрева. Первый запуск может включать загрузку модели, создание CUDA-контекста и компиляцию kernels. Отдельно записывайте cold start и прогретый запуск.
- Повторное использование KV-кэша. Prefix caching и сохранённое состояние способны резко уменьшить повторный prefill. Это уже тест warm cache, а не обработка нового prompt.
- Скрытое влияние диска и RAM. mmap, загрузка модели с SSD, перенос части весов и нехватка VRAM меняют результат ещё до вычислений.
- Смешивание TTFT и TPS. Быстрый decode не доказывает быстрый prefill. Записывайте время до первого токена и скорость последующих токенов отдельными колонками.
- Разный серверный overhead. Токенизация, HTTP, логирование и форматирование ответа могут занимать заметное время на коротком prompt.
Минимальная матрица теста может включать prompt на 512, 2 048, 8 192 и 16 384 токена, concurrency 1, 4 и 16, а также прогретый и холодный запуск. Для каждой точки фиксируйте p50, p95, prefill tokens/s, decode TPS, peak VRAM и число активных последовательностей.
В качестве стартовой конфигурации vLLM можно проверить параметры такого вида:
vllm serve MODEL --max-num-batched-tokens 8192 --max-num-seqs 32 --enable-chunked-prefill
Названия и значения флагов зависят от версии vLLM и поддерживаемого backend. Команду нужно сверить с выводом --help, а результат подтвердить несколькими сериями запусков. Для llama.cpp отдельно фиксируйте значения n_batch, n_ubatch, число GPU-слоёв и параметры контекстного окна.
Публичные цифры полезны как ориентир, но не как обещание для любой системы. В разборе запуска GLM-5.2 на 16 AMD MI50 указаны 12,2 tok/s через llama.cpp RPC и 14 tok/s через vLLM-gfx906. Эти значения относятся к конкретной модели, распределению памяти, сетевому сценарию и сборкам. Они не доказывают, что vLLM будет быстрее на prefill в другой конфигурации.
Для методики сравнения локальных движков полезен разбор запуска Qwen 3.8 27B, где отдельно обсуждаются формат модели, NVFP4, MTP, KV-кэш и условия сравнения nInfer, vLLM и llama.cpp. Такой подход снижает риск приписать ускорение одному параметру, когда его дали сразу несколько изменений.
Кэш особенно легко маскирует реальную скорость. В материале о CachyLLama и SSD-кэшировании KV приведён пример сокращения времени повторной обработки с 9,3 секунды до 0,41 секунды. Это показывает масштаб эффекта сохранённого контекста, но такой результат нельзя использовать как измерение cold prefill vLLM или llama.cpp.
Ограничения vLLM и когда llama.cpp всё же лучше
vLLM хорошо раскрывается при высокой параллельной нагрузке и доступном GPU. В локальной работе его преимущества могут не компенсировать требования к VRAM, настройке окружения и совместимости модели.
Сценарии для llama.cpp
- Ноутбук или домашний ПК. llama.cpp может работать на CPU, Metal или CUDA и не требует отдельного серверного кластера.
- Ограниченная VRAM. GGUF-квантизация и гибридная загрузка позволяют распределять веса между GPU, RAM и CPU, если конкретная модель поддерживает такой режим.
- Один пользователь. При последовательной работе одного человека простой локальный рантайм может дать сопоставимый TTFT без очереди и серверного overhead.
- Эксперименты с квантами. Форматы и уровни квантования llama.cpp удобны, когда нужно подобрать баланс между качеством, размером модели и скоростью.
- Предсказуемая локальная среда. Для офлайн-инструмента, личного ассистента или прототипа иногда важнее простой запуск и понятное потребление памяти.
- Особая архитектура или backend. Если нужная модель уже стабильно работает в llama.cpp, переход на другой движок может добавить технические риски без практического выигрыша.
На одной видеокарте и при одном запросе llama.cpp может быть удобнее и не медленнее. Его стоит сравнивать с vLLM на реальном prompt, а не с абстрактным рейтингом движков.
Когда vLLM незаменим
- Много одновременных пользователей. Continuous batching помогает объединять работу активных последовательностей и повышать суммарный throughput.
- Длинные входные документы. Большие prompt лучше загружают GPU, а блочное управление KV-кэшем помогает удерживать больше запросов в памяти.
- RAG и агентные сервисы. Когда запросы имеют разную длину и постоянно приходят новые задачи, планировщик важнее результата одиночного запуска.
- Единый API для приложений. OpenAI-compatible API vLLM упрощает подключение клиентов, которые уже ожидают такой протокол.
- Приоритет aggregate throughput. Если задача состоит в обработке большого потока запросов, небольшая задержка в очереди может быть приемлемой ради большего числа ответов за минуту.
Слово «незаменим» здесь относится к конкретному профилю нагрузки, а не к любой установке LLM. При нехватке VRAM, неподдерживаемой архитектуре или жёстком требовании к минимальному cold start llama.cpp может оказаться практичнее.
Практические советы по ускорению prefill в vLLM
Сначала определите, что именно нужно ускорить: время первого ответа, обработку длинного prompt или суммарный throughput. Один и тот же параметр может улучшить одну метрику и ухудшить другую.
Chunked prefill: что это и зачем
Chunked prefill делит длинный prompt на несколько частей. Движок обрабатывает эти части по очереди и может вставлять между ними decode-шаги других запросов. Это снижает риск, что один документ на десятки тысяч токенов заблокирует уже работающих пользователей.
Цена такой схемы состоит в дополнительных границах планирования и возможном снижении максимального throughput для одиночного длинного prompt. При смешанной нагрузке выигрыш часто выражается в меньшем p95 TTFT для коротких запросов, а не в рекордном prefill tokens/s одной последовательности.
max_num_batched_tokens задаёт верхнюю границу токенов в одной итерации планировщика. Увеличение лимита может повысить throughput длинных prompt, если GPU и VRAM справляются с размером работы. Слишком большой лимит способен задержать decode и увеличить пиковое потребление памяти.
max_num_seqs ограничивает число активных последовательностей. Высокое значение полезно при большом количестве коротких запросов, но увеличивает нагрузку на планировщик и KV-кэш. Подбирайте оба параметра вместе с concurrency и длиной контекста.
Attention backend выбирайте по результатам теста на своей GPU и своей модели. FlashAttention или FlashInfer могут дать выигрыш, но поддержка зависит от архитектуры, версии библиотек, длины последовательности и точности весов. Название backend само по себе не заменяет измерение.
При повторяющемся системном prompt можно включить prefix caching, если его поддерживает используемая конфигурация. Кэш уменьшит повторную обработку общего префикса, но изменит смысл бенчмарка. Для оценки cold prefill кэш нужно очищать или использовать уникальный префикс, для оценки реального RAG-сервиса, наоборот, полезно тестировать оба режима.
Следите за тремя показателями после каждой настройки: p95 TTFT, aggregate throughput и peak VRAM. Улучшение только одной цифры не доказывает, что сервис стал быстрее для целевого пользователя.
Заключение: что выбрать для вашего сценария
| Сценарий | Предпочтительный выбор | Причина |
|---|---|---|
| Один пользователь, локальный ПК | llama.cpp | Простой запуск, квантование, CPU/GPU-гибрид и предсказуемое потребление памяти |
| Много параллельных запросов на мощном GPU | vLLM | Continuous batching, планирование токенов и эффективное использование KV-кэша |
| Длинные RAG-промпты и смешанная нагрузка | vLLM | Большие batch-операции и chunked prefill помогают балансировать prefill и decode |
| Ограниченная VRAM или CPU-only | llama.cpp | Широкий выбор квантов и гибкая загрузка модели |
| Сервис с требованием к API и aggregate throughput | vLLM | Серверная модель работы и OpenAI-compatible API |
vLLM чаще выигрывает у llama.cpp на prefill, когда есть длинные промпты, параллельные пользователи и GPU, способный выполнять крупные batch-операции. Continuous batching повышает загрузку вычислительных блоков, а PagedAttention помогает вместить больше KV-кэша. Ни один из этих механизмов не гарантирует преимущество на одиночном коротком запросе.
Для выбора измеряйте полный профиль: TTFT, чистое время prefill, aggregate throughput, decode TPS, p95 задержку, peak VRAM и поведение cold/warm cache. Такой тест быстро покажет, ускоряет ли vLLM именно ваш рабочий процесс или llama.cpp уже даёт нужный баланс скорости, памяти и совместимости.