Привет! Я Саша Рыжов, MLOps-инженер в hh.ru, уже три года занимаюсь развитием инфраструктуры для искусственного интеллекта. Компании, которые развивают GenAI, рано или поздно приходят к задачам по запуску LLM на собственном железе. В статье я расскажу, как обстоят дела с движками инференса в 2026 году и как запустить on‑prem-прод и не изобрести при этом велосипед.

С какой проблемой часто сталкиваются при запуске больших языковых моделей на собственных серверах? Туториалов «подними LLM за пять минут» написаны сотни, но почти нет подробностей, во что упирается инференс под реальной нагрузкой и почему железо стоит таких денег.

Благодаря годам работы с собственным железом я могу сформулировать главную мысль сразу: эффективность инференса держится на управлении памятью видеокарты — выбор модели здесь вторичен.

А дальше я разберу, почему так получается и как с проблемой памяти справляются vLLM и SGLang. Основной упор в статье сделал на практику: какой движок, какой размер модели и какую схему параллелизма брать под своё железо и свой профиль нагрузки.

Почему инференс упирается в память

Чтобы все дальнейшие решения стали понятны, сначала разберёмся с KV-кэшем. Это фундамент, без него остальной разговор теряет смысл.

Представьте, что мы генерируем ответ по одному токену и не используем кэш. На каждом шаге модель заново пересчитывает все предыдущие токены, хотя мы их уже считали. Чтобы дойти до пятого токена, придётся сделать порядка 35 вычислительных шагов и ни разу не переиспользовать готовый результат. Так мы упираемся в compute видеокарты.

Теперь добавим KV-кэш — хранилище промежуточных состояний внутри трансформера, которое позволяет языковой модели не пересчитывать заново всё, что она уже посчитала, при генерации каждого нового токена. На каждом шаге мы генерируем только последний нужный токен, а все предыдущие состояния (ключи и значения) просто читаем из памяти. Вместо 35 вычислительных шагов получаем 33 чтения состояния из кэша и всего 5 настоящих вычислительных шагов.

Здесь и происходит сдвиг парадигмы. Теперь всё определяет скорость чтения памяти: decode-стадия становится memory-bound.

Без KV-cache prompt вся история пересчитываются на каждом шаге: 35 compute-шагов за 5 токенов, работа растёт как O(N²)
Без KV-cache prompt вся история пересчитываются на каждом шаге: 35 compute-шагов за 5 токенов, работа растёт как O(N²)
С KV-cache prompt считается один раз на prefill и дальше живёт в HBM: 5 compute и 30 read
С KV-cache prompt считается один раз на prefill и дальше живёт в HBM: 5 compute и 30 read

Итог: KV-cache меняет природу bottleneck’а — переводит decode из compute-bound в memory-bandwidth-bound.

Жизненный цикл запроса

Чтобы было понятно, где именно живёт KV-кэш, проследим путь запроса.

  1. Приходит HTTP-запрос с текстом.

  2. Токенизация. Превращаем текст в набор чисел.

  3. Prefill. Модель читает весь промпт и записывает состояния K/V в KV-кэш.

  4. Decode. Это собственно генерация. Для каждого нового токена мы читаем из VRAM веса модели, читаем состояние KV-кэша, делаем матричное перемножение и дописываем в кэш состояние нового токена.

Именно на стадии decode мы постоянно читаем память, поэтому она и оказывается memory-bound.

Путь запроса: tokenize, prefill (параллельный расчёт K/V), decode (генерация по одному токену). На decode GPU читает из HBM веса и KV-cache, а утилизация compute падает до 2%
Путь запроса: tokenize, prefill (параллельный расчёт K/V), decode (генерация по одному токену). На decode GPU читает из HBM веса и KV-cache, а утилизация compute падает до 2%

Движки инференса: борьба за память

Раз decode упирается в память, вся инженерия движков инференса сводится к борьбе за неё. Памяти должно хватать, и расходоваться она должна эффективно.

vLLM и PagedAttention

vLLM — это открытый движок (фреймворк) для инференса и сервинга больших языковых моделей. Проще говоря, это софт, который берёт готовую модель и эффективно обслуживает на ней запросы: принимает промпты, гоняет их через GPU и отдаёт сгенерированный текст, стараясь выжать из железа максимум. 

Все системы до vLLM страдали двумя болезнями:

1. Internal fragmentation. Под запрос приходилось резервировать память по максимальной возможной длине. Модель поддерживает 4096 токенов, значит резервируем под 4096, хотя реально сгенерировали 600. Остальные 3500 токенов памяти просто простаивают.

До vLLM каждый запрос блокировал непрерывный кусок памяти на полную длину, даже если использовал малую часть
До vLLM каждый запрос блокировал непрерывный кусок памяти на полную длину, даже если использовал малую часть

2. External fragmentation. Чтобы выделить блок под запрос, нужно было найти на GPU физически подряд идущие свободные ячейки. Под нагрузкой случается обидное: свободной памяти суммарно полно, а единого непрерывного куска под запрос нет. Память есть, воспользоваться ей нельзя.

vLLM предложил блочную схему. Память бьётся на блоки по 16 токенов, и движок оперирует только ими. Под новую длину выделяется новый блок без всякого резервирования наперёд, и internal fragmentation исчезает. Блоки при этом лежат в памяти в любом порядке и читаются по очереди, так уходит и external fragmentation. Утилизация GPU выросла кратно.

PagedAttention: все запросы делят общий пул, блоки одного запроса лежат вразброс, external fragmentation равна нулю
PagedAttention: все запросы делят общий пул, блоки одного запроса лежат вразброс, external fragmentation равна нулю

Насколько это важно? Авторы vLLM показали интересную вещь. Даже гипотетический идеальный фреймворк, который заранее знал бы длины всех приходящих запросов, всё равно проигрывал бы PagedAttention более чем вдвое. После той статьи PagedAttention перекочевал во все популярные на тот момент решения для инференса.

vLLM держит в батче в 2,2 раза больше запросов даже над гипотетическим Oracle, который знал бы все длины заранее (Kwon et al., SOSP 2023)
vLLM держит в батче в 2,2 раза больше запросов даже над гипотетическим Oracle, который знал бы все длины заранее (Kwon et al., SOSP 2023)

SGLang и RadixAttention

Параллельно с vLLM развивался SGLang (открытый движок для инференса и сервинга больших языковых моделей, как и vLLM — прямой конкурент, который во многом обгоняет vLLM на свежем железе). Его авторы посмотрели на реальные ворклоады и решили двигаться дальше. Мало просто эффективно утилизировать GPU, хочется ещё и гибко переиспользовать уже посчитанное. Для этого они сделали структуру RadixAttention (Radix Tree).

SGLang задумывался под workloads с переиспользованием общих префиксов: few-shot, общий system prompt, ветвление
SGLang задумывался под workloads с переиспользованием общих префиксов: few-shot, общий system prompt, ветвление

Разберём на примере. Приходит промпт «реши: 12 умножить на 7». Он ложится в дерево единым блоком: системный промпт плюс содержимое. Дальше приходит новый запрос «реши: корень из x, x² = 16». RadixAttention сам выделяет общий системный промпт в отдельную ноду и переиспользует его в обоих случаях. Когда первый запрос приходит снова, пересчитывать ничего не нужно: всё состояние берётся из дерева, и генерация идёт сразу.

Новый запрос с тем же system prompt: узел расщепляется, общий префикс выносится наверх и переиспользуется
Новый запрос с тем же system prompt: узел расщепляется, общий префикс выносится наверх и переиспользуется

У vLLM prefix matching шёл по совпадению хэшей блоков по 16 токенов. Дерево префиксов работает гибче и точнее покрывает реальные кейсы, поэтому prefix caching в SGLang заметно выигрывает.

Вытеснение (eviction) в RadixAttention идёт по refcount, то есть по числу упоминаний блока в живых запросах. Из памяти уходит только то, что точно не понадобится: блоки, которые переиспользуются реже всего.

Вытеснение работает как LRU по листьям с refcount = 0. Внутренние узлы защищены, пока у них есть живые потомки
Вытеснение работает как LRU по листьям с refcount = 0. Внутренние узлы защищены, пока у них есть живые потомки

Что ещё есть у SGLang

Несколько менее очевидных, но важных приёмов:

  • Compressed FSM для function calling. В конечные автоматы (finite state machine) зашиты грамматики, то есть готовые куски структурированного вывода вроде скобок и JSON. Они переиспользуются вместо генерации с нуля.

  • Cache-aware scheduling. Шедулер отдаёт приоритет запросам, которые попадают в уже прогретый кэш, вместо обычного FIFO. Кэш используется ещё плотнее.

  • Zero-overhead scheduler. Пока GPU считает батч n, CPU уже готовит батч n+1. Простой GPU сокращается.

  • dp-attention. У больших моделей вроде DeepSeek раскладывать attention по всем GPU дорого, слишком много времени уходит на коммуникации. Поэтому attention реплицируют по картам при общих весах. Коммуникаций меньше, attention считается быстрее.

  • Multinode из коробки. Для работы на нескольких нодах vLLM требует поднимать Ray-кластер. В SGLang это сделано нативно через NCCL, достаточно добавить адрес второй ноды.

Спускаемся к железу: HBM, SRAM и FlashAttention

Чтобы понять FlashAttention, нужно знать про иерархию памяти GPU.

  • HBM. Это собственно VRAM, та самая, про которую говорят «80 гигабайт на H100».

  • SRAM. Это «рабочий стол» кернела, где идут сами вычисления. Маленькая, зато очень быстрая.

FlashAttention переписывает формулу вычисления attention так, чтобы блоки плотнее умещались в SRAM, а трафик между HBM и SRAM падал. Любопытная деталь: по числу вычислений FlashAttention даже требовательнее обычного attention. Зато трафика памяти между HBM и SRAM кратно меньше, и суммарное время ожидания результата падает в разы. И снова всё решает именно память.

Между SRAM и HBM разрыв в скорости в 13 раз. Вся инженерия инференса сводится к тому, чтобы реже гонять данные между ними
Между SRAM и HBM разрыв в скорости в 13 раз. Вся инженерия инференса сводится к тому, чтобы реже гонять данные между ними
FlashAttention делает больше вычислений и при этом работает быстрее: FLOPs не предсказывают время, узкое место — это пропускная способность HBM.
FlashAttention делает больше вычислений и при этом работает быстрее: FLOPs не предсказывают время, узкое место — это пропускная способность HBM.

Эволюция attention: MHA → GQA → MLA → DSA

Отдельная история это сами реализации attention. Каждый следующий шаг бил в одну цель: уменьшить KV-кэш.

Реализация

Год

Идея

Что с KV-кэшем

MHA (Multi-Head Attention)

2017

Классика

Свой KV-кэш на каждую голову

GQA (Grouped-Query Attention)

2023

Группировка голов

Один KV-кэш на группу голов

MLA (Multi-head Latent Attention)

2024, DeepSeek

Латентное состояние

Сжатие KV-кэша в латентное состояние

DSA (DeepSeek Sparse Attention)

2025, DeepSeek v3.2

Разреженное чтение

Вычитываем только top-k нужных токенов

Пока MHA работал на маленьких контекстах, отдельный кэш на каждую голову не мешал. Контексты выросли, и началась гонка за экономию памяти. Переломной стала MLA с латентным состоянием: размер KV-кэша на запрос упал на порядок:

Модель

Контекст

KV-кэш на запрос

Llama 3.1

128k

~40 ГБ

Qwen3

128k

~32 ГБ

DeepSeek (с MLA)

любой

на порядок меньше остальных

До DeepSeek большие модели было тяжело переиспользовать. Запуск Llama 70B на длинных контекстах мог требовать вдвое больше карт просто для того, чтобы разместить кэш.

Практика: что выбирать

С теорией закончили. Какой же движок инференса выбрать в 2026 году исходя из этих знаний?

Какой движок под какое железо

  • H100 и выше: берите SGLang.

  • Ampere и ниже: берите vLLM. На старом железе он стабильнее, и возиться с SGLang смысла нет.

Hopper и новее (H100/H200/B200) тянут SGLang. Ampere и ниже (A100/A10/3090/4090) лучше отдать vLLM: проще, стабильнее, реже OOM
Hopper и новее (H100/H200/B200) тянут SGLang. Ampere и ниже (A100/A10/3090/4090) лучше отдать vLLM: проще, стабильнее, реже OOM

Какой размер модели тянет одна нода

Сначала про ёмкость. Нода H100 это 8 карт по 80 ГБ, суммарно 640 ГБ VRAM. Нода H200 даёт около 1,1 ТБ VRAM.

Теперь представим, что туда влезает:

Класс модели

Влезает на ноду?

Сессии

Вывод

GLM, DeepSeek v3.2 и гиганты

Влезает, а места под KV-кэш почти нет

Десятки

Дорого, не окупится

Qwen3 ~400B, Minimax

Влезает получше

100–200

Нормально, но впритык

MoE до ~120B / Dense до ~32B

Влезает с запасом под кэш

Достаточно

Sweet spot

Отдельно про Dense-модели. Даже 70B уже брать не стоит: железа нужно кратно больше, а прирост качества по нашим внутренним бенчмаркам и по общим заметно скромнее, чем рост в количестве карт. Модели поменьше работают отлично, хотя на них чаще возникает конкуренция за compute. В этом случае стоит разделять prefill и decode, об этом ниже.

Throughput против размера модели на 1×8×H200. Зелёная зона: underutilized; синяя: sweet spot; оранжевая: tight fit, под KV-cache почти не остаётся места. Числа: реальные SGLang бенчи (Qwen3-32B issue 7352, DeepSeek V3.2 gpustack, Qwen3-235B Baseten) + экстраполяция
Throughput против размера модели на 1×8×H200. Зелёная зона: underutilized; синяя: sweet spot; оранжевая: tight fit, под KV-cache почти не остаётся места. Числа: реальные SGLang бенчи (Qwen3-32B issue 7352, DeepSeek V3.2 gpustack, Qwen3-235B Baseten) + экстраполяция

Tensor Parallelism vs Data Parallelism

Внутри ноды масштабироваться можно двумя принципиально разными способами.

Tensor Parallelism (TP). Модель растягивается по всем GPU. Растёт число токенов в секунду на запрос, зато появляется необходимость постоянно коммуницировать между картами, и это свой overhead.

Data Parallelism (DP). На одной ноде поднимается несколько инференс-серверов. Копий модели больше, зато у каждой свой KV-кэш, и суммарный throughput выше.

Как выбирать:

Цель

Что брать

Максимальный UX, быстрые ответы (низкая latency)

Tensor Parallelism

Максимум токенов и запросов в единицу времени (throughput / RPS)

Data Parallelism

По нашим бенчмаркам latency ниже на TP, а throughput и RPS кратно выше на DP. Смешивать тоже никто не мешает. Если часть кейсов требует быстрых ответов, а часть большого throughput, разнесите нагрузку внутри одной ноды между TP- и DP-инстансами.

Выбор — не про железо, а про то, как пользователь потребляет ответ
Выбор — не про железо, а про то, как пользователь потребляет ответ

Prefill/Decode disaggregation

Самый большой throughput даёт разделение стадий prefill и decode по разным пулам GPU.

В обычной инсталляции prefill и decode конкурируют за одни и те же карты, и шедулер вынужден жонглировать расписанием: когда читать промпты, когда генерировать. Если развести стадии, каждый пул карт получает свой тип задач, конкуренции внутри пула нет, и работа идёт ровным потоком. Расплата очевидна: такая инсталляция требует заметно больше внимания и инженеров на поддержку.

Спекулятивный декодинг: не панацея

Почитаешь посты про спекулятивный декодинг, и складывается ощущение бесплатного обеда. Маленькая модель предсказывает токены, большая их одобряет, latency падает, throughput растёт. На красивых графиках всё ровно так и выглядит.

Стоит вырасти конкуренции, и начинается резкая деградация. Память, которую можно было отдать под конкурентные запросы в KV-кэш, уходит на маленькую draft-модель, и часть нагрузки, которую мы могли бы обработать, просто не вмещается.

Спекулятивный декодинг концентрирует нагрузку в небольшом числе сессий. Если же ваша цель в том, чтобы абсолютно поднять throughput, этот инструмент вам не подойдет. Берите его тогда, когда нужно показать отличный результат на небольшом числе конкурентных запросов.

Память под draft-модель отнимается у KV-cache. На высоком RPS каждый гигабайт VRAM выгоднее отдать под KV реальных запросов
Память под draft-модель отнимается у KV-cache. На высоком RPS каждый гигабайт VRAM выгоднее отдать под KV реальных запросов

Чеклист перед продакшеном

Перед запуском LLM на собственном железе полезно пройти цепочку решений в правильном порядке. 

  1. Сначала оцените модель не только по весам, но и по KV-кэшу на целевой длине контекста. 

  2. Затем выберите движок: vLLM для стабильного универсального контура, SGLang для агрессивного prefix reuse и throughput на новом железе. 

  3. После этого зафиксируйте стратегию параллелизма: TP для latency, DP для throughput или смешанный режим для неоднородных SLA. 

  4. И только после этого включайте «ускорители» вроде speculative decoding: они работают хорошо, когда базовая экономика памяти уже посчитана.

Главная мысль простая: инференс LLM — это управление памятью под нагрузкой. Узкое место decode — не compute, а чтение весов и KV-кэша из VRAM. PagedAttention снимает потери VRAM от фрагментации. RadixAttention добавляет переиспользование общих префиксов. FlashAttention сокращает трафик между HBM и SRAM вместо наращивания FLOPS. Продакшн-решения — размер модели, H100 или H200, TP или DP — выводятся из этой механики.

А какими мыслями вы можете поделиться по опыту запуска LLM на собственном железе? Пишите в комментариях, буду рад обсудить)

Больше интересного про разработку в hh можно найти в телеграм-канале «Охэхэнные новости». Подписывайтесь!

Комментарии (1)


  1. keelai
    27.07.2026 06:07

    Спасибо за практический разбор. В проде я бы добавил к сравнению vLLM и SGLang еще один обязательный слой: метрики нужно снимать не только по средней скорости генерации. Для каждого профиля нагрузки полезно разделять TTFT и ITL, смотреть p95/p99, длину входа и выхода, длину живого KV-кэша, queue time и долю запросов с cache hit. Иначе prefix caching легко выглядит победителем на среднем сценарии, но проигрывает на длинном хвосте или при смешении коротких и длинных запросов.

    Еще важен тест с реальным распределением запросов: общий system prompt, повторные диалоги, отмены, burst после простоя и одновременная деградация одной GPU. Тогда выбор движка становится не спором о benchmark tokens/sec, а расчетом стоимости и качества сервиса: latency budget, throughput на одну карту и цена одного полезного ответа.