- BrainTools - https://www.braintools.ru -

Вы запустили LLM-инференс в продакшене. Поток запросов не менялся, нагрузка та же, что вчера, — а Time to First Token внезапно вырос в три раза. Первая мысль: что-то с моделью. На деле причина почти никогда не в модели — она прячется в планировщике, аллокаторе GPU-памяти, очереди стримингового вывода или сети.
Чем длиннее контекст, тем больнее. Для классического attention вычислительная сложность растёт очень быстро. KV-кеш раздувается до десятков и сотен гигабайт, а в облаке всё это происходит на совершенно произвольном трафике: у одного клиента кодовый ассистент, у другого — аналитика на миллион запросов в день, у третьего — голосовой робот. На таких смешанных нагрузках всплывает то, чего не видно на референсных замерах вендора.
Привет! Меня зовут Андрей, я занимаюсь разработкой сервисов машинного обучения [1] для Yandex AI Studio — платформы, которая объединяет модели и инструменты Яндекса, чтобы вы могли создавать собственные ИИ-решения и внедрять их в бизнес‑процессы и продукты. Чтобы запустить инференс флагманских моделей в продакшене, нам пришлось глубоко закопаться в опенсорс, найти и починить баги в чужом коде — суммарно больше 50 контрибьютов в разные репозитории — и переписать куски рантайма под свои конфигурации.
Эта статья точно для вас, если вы:
Уже запускали LLM-инференс в продакшене.
Видели, как метрика Time to First Token (TTFT) выросла в 3 раза под нагрузкой, — и не понимали, почему.
Думали, что проблема в модели, а оказалось — в планировщике или сети.
Дальше расскажу, на какие проблемы наткнулись на смешанной нагрузке и какие оптимизации внедрили: от разделения prefill и decode и контекстного параллелизма до спекулятивного декодирования, балансировки с учётом кешей и доставки 700-гигабайтной модели на кластер.
Прежде всего скажу, что в машинном обучении есть две ключевые задачи: обучение моделей и их инференс. Про обучение LLM в этой статье говорить не будем: это отдельная большая область со своими командами, инфраструктурой и метриками.
Здесь сфокусируемся на инференсе — этапе, где уже готовая модель обслуживает реальные пользовательские запросы в продакшене. Основная цель этапа — обеспечить пользователям быстрый и надёжный сервис. Об этом и поговорим.

Какие метрики нас будут интересовать в этом случае? По сравнению с абстрактной бэкенд-разработкой в инференсе их больше.
Время до первого токена, TTFT — промежуток времени от момента, когда модель получила запрос и до того, как она написала первое слово.
Время между токенами, ТВТ (Time between tokens). Токен — это единица, на которую токенизатор разбивает текст: часть слова, слово, знак препинания или фрагмент кода. В среднем для обычного текста один токен часто соответствует нескольким символам, но точное соотношение зависит от языка и токенизатора.
Время полного ответа, U2E (User to end) — время от получения пользовательского запроса до завершения генерации ответа. Эта метрика особенно важна для сценариев, где пользователю нужен не первый токен, а готовый результат целиком.
Пропускная способность на единицу ресурсов, то есть на одну видеокарту — измеряется в токенах в секунду на GPU.
Стоимость инференса миллиона или тысячи токенов.
Важно понимать, что нельзя оптимизировать все эти метрики одновременно. В зависимости от сценария, мы делаем выбор в пользу какой-то из метрик: времени до первого токена, или времени полной генерации, или же пропускной способности.
Например, в случае голосового робота, который должен отвечать пользователю синхронно, требуется маленькое время до первого токена. Странно, если я куда-нибудь позвоню, и модель мне будет отвечать через десять секунд.
Есть время полной генерации, например, кодовый сценарий, когда модель пишет файл с кодом. В этом случае всё равно, когда она начала писать первую строчку, важно, когда она допишет файл до конца.
Ещё есть сценарий аналитики, когда за день накопился миллион обращений в саппорт, и их необходимо обработать. Тут неважно время, проходящее до генерации первого или последнего токена. Важно, чтобы это было сделано за минимальную стоимость.
Метрики понятны, определена постановка задачи, наверняка есть даже готовые решения. Теперь расскажу про одну из самых больших проблем в современном инференсе, которую мы и будем решать. Это длинный контекст, который присутствует во всех агентских и кодовых сценариях.
Для классического attention стоимость обработки контекста растёт очень быстро: чем длиннее вход, тем дороже prefill по вычислениям и памяти [2]. В современных реализациях есть оптимизации attention, но общий эффект остаётся: длинные контексты резко увеличивают TTFT, объем KV-кеша и нагрузку на GPU.
Если контекст вырос с 4 000 до 32 000 токенов, нагрузка на обработку входа может вырасти кратно. Поэтому длинный и короткий контекст, обрабатываемые параллельно, начинают мешать друг другу. Для решения этой проблемы инференс часто разделяют на две стадии: prefill и decode — обработку входного запроса и генерацию ответа.
Более того, длинный контекст и короткий контекст, обрабатываемые параллельно, мешают друг другу.
В нашей архитектуре мы отдельно управляем prefill- и decode-нагрузкой: для них важны разные ресурсы и разные SLO. Prefill чувствителен к длине входа и compute, decode — к памяти, batch scheduling и стабильному TBT. Такое разделение позволяет не давать длинным запросам «забивать» интерактивные сценарии и отдельно оптимизировать каждый этап.
Prefill — это compute‑bound задача. Она считает все эти матрицы с квадратичной сложностью, где есть большие контексты.
Decode — это скорее memory-bound задача. Там модель генерирует токены по одному, так оптимизируется та самая метрика TBT.

Современные GPU так устроены, что у операций на GPU нет приоритетов, неважно, prefill или decode — всё выполняется независимо. Если делать всё в рамках одного сервера, все будут страдать. Раньше в Yandex Cloud мы разделяли инференс на пользователей с короткими и длинными запросами: чтобы короткие запросы не провисали.
Важно сказать пару слов про KV-кеш. Всем знаком кеш в бэкенд-разработке: пришёл запрос, мы видим, что такой запрос уже был, и возвращаем ответ. В случае с инференсом LLM это работает не так, потому что запрос представляет собой абстрактный текст. Кешировать полный текст запроса почти бесполезно: целые запросы редко совпадают байт-в-байт. Зато в диалогах, агентских сценариях и RAG часто повторяются длинные префиксы: системный промпт, история диалога, инструкции, документы. Именно такие префиксы и имеет смысл переиспользовать через KV-кеш.
KV-кеш в LLM — это не кеш готовых ответов. Во время обработки входного текста модель строит промежуточные key/value-состояния для attention. Если в следующем запросе повторяется длинный префикс — например, системный промпт, история диалога или набор документов в RAG, — эти состояния можно переиспользовать и не пересчитывать префикс заново.
Цена такого ускорения — память. Для больших моделей и длинных контекстов KV-кеш занимает десятки и сотни гигабайт, поэтому в продакшене важно не только уметь кешировать, но и правильно управлять памятью, вытеснением и маршрутизацией запросов.
Например, модель DeepSeek-V3.2 весит 700 ГБ, а кеши занимают десятки и сотни гигабайт. Как правило, в LLM-моделях надо заниматься тем, чтобы как-то оптимизировать объём кеша и улучшать кеш-хит.
Самое время определиться с архитектурой — образом того, что мы строим в облаке. У нас есть большая языковая модель, стадия prefill и decode, есть некоторый кеш. Задача построить такую систему, чтобы у нас был кластер, состоящий из виртуальных машин с GPU. Все эти виртуальные машины связаны некоторой быстрой сетью, и над ними поставлен умный балансировщик, который умеет учитывать распределение кешей.

Многие в мире уже разрабатывают подобные системы, есть решения с открытым исходным кодом: например, vLLM, SGLang, TRT-LLM. Всё уже есть, но эти фреймворки дают данные на референсных замерах. А в облачной платформе мы сталкиваемся с тем, что нагрузка совершенно произвольна. У кого-то может быть сценарий аналитики, у кого-то сценарий кода, у кого-то сценарий голосового робота.
Поэтому на таких смешанных нагрузках всплывает много подводных камней, которые нам предстояло решить.
Мы начали с того, что взяли модель DeepSeek-V3.2 и развернули опенсорс-решение. Померили и начали проводить внутреннее тестирование. Дальше возникли определённые проблемы: расскажу, как их решали и какие оптимизации для этого внедряли.
В начале мы столкнулись с тем, что после выпуска рабочей модели все тексты в какой-то момент начали писаться в три раза медленнее. Нагрузка на карты была стабильно высокая, поток запросов внешне никак не поменялся, а производительность очень сильно просела.
Мы достаточно глубоко закапывались в опенсорс-проект и правили код: суммарно больше 50 контрибьюшенов в разные репозитории. Однако покопавшись в строках, мы смогли выяснить, что при смешении коротких и длинных запросов происходили частые аллокации и освобождения GPU-памяти для промежуточных буферов в одном из kernel path, специфичном для DeepSeek. На смешанной нагрузке это било по decode: память становилась узким местом, и TBT резко рос.
Эту локацию мы поправили, законтрибьютили в опенсорс и получили то время между токенами, которое указывал DeepSeek в своих отчётах.
Но на этом наши проблемы не закончились. Дальше пришли ребята, которые хотели писать код с использованием всевозможных кодовых ассистентов. Здесь часто бывает потребность [3] генерировать такой ответ модели, который имел бы какую-то структуру, например JSON, — потом его можно распарсить и дальше с этим работать.
Внезапно выяснилось, что на 5% запросов JSON оказывается неполным. Там буквально не было куска текста, естественно, это портило всю бизнес-логику клиентов.
Мы снова разбирались в опенсорс-коде: с кодовыми ассистентами, дебагом и логами. Проблема оказалась в streaming path SGLang и воспроизводилась только при высокой конкуренции запросов и определённой комбинации параметров. Очередь могла содержать несколько готовых фрагментов, а обработчик после чтения одного элемента очищал буфер целиком. В результате часть токенов терялась, и структурированный ответ, например JSON, становился неполным.
Фикс достался нам: проблема не проявлялась на простых сценариях и долго оставалась незаметной. Мы поправили реализацию и отправили изменение в опенсорс.
Дальше мы натолкнулись на работу с KV-кешами, у которых в инференсе три уровня:
уровень памяти видеокарты,
уровень оперативной памяти,
уровень дисков.
А раз KV-кеши большие по объёму, нам нужно использовать всю память, которая есть.
Реализация кеширования в SGLang архитектурно была устроена так, что объём памяти, выделенный под GPU-кеш, полностью дублировался в оперативной памяти. Порядок чисел около 700 ГБ — это суммарная память видеокарт, а 2 ТБ — это оперативная память. Мы на ровном месте теряли почти треть оперативки (которая нынче дорогая).
Такое решение хоть и упрощало реализацию, но для наших конфигураций оказалось слишком дорогим: RAM фактически резервировалась под зеркало GPU-кеша, хотя мы хотели использовать её как самостоятельный уровень иерархии KV-кеша.
Мы изменили реализацию так, чтобы не дублировать GPU cache в RAM, и освободили порядка 700 ГБ оперативной памяти на узел, тем самым увеличив кеш и улучшив кеш-хит.
Кажется, что мы поправили все проблемы, связанные с багами в опенсорсе на конкретной модели, нагрузках и сценариях. Однако скорость ответов нас всё равно не устроила, и мы продолжили оптимизацию.
Итак, есть длинный контекст на входе, prefill и decode разделены. Но инференс устроен так, что prefill запроса всё равно обрабатывается на одной видеокарте и может упираться в её вычислительную способность. Получается большой темп TTFT.
Однако есть такая техника, как параллелизм по контексту (CP – context parallelism): длинный prefill считается сразу на восьми видеокартах, как правило, в рамках одного вычислительного узла. Технику уже внедрили в vLLM, в SGLang — но не поддержали её вместе с DeepSeek-V3.2, потому что модель и архитектура новые, там был новый sparse attention.
Пришлось поддерживать контекстный параллелизм для DeepSeek-V3.2 и контрибьютить его в SGLang. В ходе наших тестов на восьмикарточном сервере удалось сократить Time to First Token в четыре раза при работе с очень длинным контекстом, достигающим 160K токенов. Это максимальный контекст, поддерживаемый DeepSeek.
Вот какие замеры получаются для одного запроса на 160K токенов:
без CP (TP = 8): TTFT = 36,9 с
с CP (TP = 8, CP = 8): TTFT = 8,8 с (−76%)
Помимо этого мы занимались внедрением спекулятивного декодирования — это оптимизация decode-стадии.
Современная модель, когда происходит decode, генерирует ответ по одному токену. Спекулятивное декодирование работает так: обучается специальная маленькая модель, условно в 100 раз меньше, чем основная. Она заранее пытается угадать токены, которые сгенерировала бы большая модель. При этом маленькая модель генерирует не один токен, а например 20. Большая LLM за один проход валидирует все 20 токенов и в лучшем случае говорит: да, они совпадают с тем, что сгенерировано. Если же совпадает только часть, тогда генерация продолжается с определённого момента.
Это позволяет очень сильно сэкономить время на decode. Понятно, что всё зависит от профиля нагрузки и ещё очень многих факторов. В конкретной конфигурации и на нашем профиле нагрузки TBT снижался примерно до 5 раз относительно baseline без speculative decoding. На некоторых сценариях эта цифра гораздо меньше, на других — гораздо больше, но в среднем примерно так.
Итак, у нас получилось внедрить новую модель и провести её оптимизацию с точки зрения [4] стадий prefill и decode, а также починить разные баги.
Осталось оптимизировать KV-кеш, но не только. Нужно помнить, что мы на самом деле работаем в облаке, и есть некоторое количество ограничений, которые накладываются. Поэтому также взглянем в сторону железа и сетей.
Начать можно с того, что изначально у нас есть кеш в рамках каждого узла, и мы маршрутизируем запросы пользователей как попало.

Когда мы делаем это с помощью Round-robin, стандартного способа балансировки, то как-то попадаем в кеш, как-то не попадаем. Кеш-хит 5–10% — означает, что из кеша вынуто 5–10% всех токенов. А современные агентские сценарии устроены так, что есть чат, и он постоянно пополняется: всегда есть общий префикс, в который просто доливаются тексты, и понятно, что префикс постоянно повторяется.
Хочется такие запросы всегда обрабатывать в рамках одного и того же узла, и ввести привязку по сессиям, когда один и тот же диалог обрабатывается одним узлом. Мы это внедрили и поддержали из коробки Responses API, который сделан под агентский сценарий.

На нашем профиле трафика это дало кеш-хит порядка 30% по токенам и заметно снизило затраты на prefill. В сценариях с длинными повторяющимися префиксами это напрямую конвертируется в меньший TTFT и более эффективное использование GPU.
Однако этот способ балансировки имеет проблемы, потому что происходит неравномерная нагрузка на железо: если узел вышел из строя и кеш потерялся, произойдет перебалансировка.
Ещё один способ балансировки — специальный роутер, умный балансировщик. Он учитывает, какой кеш лежит на каждом вычислительном узле, сколько он там находится, и в зависимости от этого маршрутизирует запросы на соответствие.
Такой более умный способ балансировки, во-первых, решает проблему неравномерной нагрузки на железо, во-вторых, ещё сильнее улучшает кеш-хит. В наших замерах кеш-хит доходил примерно до 54% токенов на характерном профиле нагрузки.

Естественно, это зависит от нагрузки и её профиля: если засылать одинаковые запросы на синтетике, то будет почти 100%, если совсем рандомные, то 0%. Но вот в среднем получилось 54%.
Конечно, это сказывается на TTFT, потому что модель не пересчитывает уже обработанную часть префикса, а переиспользует сохранённые KV-состояния. Поэтому prefill становится короче, и TTFT уменьшается.
Помимо проблем с железом, когда перегрелась или сгорела сетевая карта, или выпала GPU, существует трудность, связанная с тем, что многие фреймворки по-прежнему плохо работают с Dual-Stack-сетями. В паре мест поддержку Dual-Stack приходилось коммитить: некоторые фреймворки хотели только IPv4, другие не работали с Dual-Stack.
Ещё одна проблема: модели надо как-то доставлять на кластер.
DeepSeek весит 700 ГБ. Если скачивать из какого-нибудь S3 или с Hugging Face, может оказаться долго, да и скачивание может упасть. В итоге часть серверов запустится, часть не запустится. Поэтому надо здесь правильно настроить подгрузку модели. Например, заранее разложить её на какие-то быстрые диски и подгружать LLM в память уже оттуда, или как-то обмениваться частями модели между серверами по интерконнекту.
Помимо этого необходимо сразу же делать прогрев модели и создать несколько запросов. Проверить, что они работают, чтобы пользователи не видели никаких проблем в продакшен-среде. Часто бывает, что первые несколько запросов модели работают медленно, а потом быстро.
Также важный блок в продакшен-сервисе — это observability. Здесь мы тоже столкнулись с некоторыми сложностями. Например, vLLM-сервер падал при включённом трейсинге, если пользователь делал отмену запроса. Здесь тоже внесли некоторое количество контрибьюшенов, чтобы у нас заработал трейсинг и логи были нормальными. Это позволяло быстрее обнаруживать, где находится проблема, и быстрее вносить фиксы. Все наши 50 контрибьюшенов во многом состоялись благодаря хорошей observability.

Подводя итог, в этой истории не было одной волшебной оптимизации, которая сразу сделала инференс быстрым и дешёвым. Результат сложился из большого числа инженерных решений: разделения prefill и decode, исправлений в рантайме, контекстного параллелизма, спекулятивного декодирования, работы с KV-кешем и липкими сессиями, маршрутизации с учётом кешей, прогрева моделей, observability и инфраструктурных доработок — от GPU-памяти до сети и доставки весов.
Проект длился больше года, и каждая из этих оптимизаций дала вклад в итоговую картину. На части профилей нагрузки мы приблизились к референсным показателям вендора и авторов модели, но уже не в лабораторных условиях, а в облачной платформе: с произвольным трафиком, разными клиентскими сценариями, отказоустойчивостью, мониторингом и эксплуатационными ограничениями.
Главный вывод: инференс больших моделей — это не только про саму модель. Это отдельная инженерная система вокруг неё. И прежде чем инвестировать в собственный LLM-инференс, важно понимать, что здесь нужны не только GPU, но и экспертиза, которая копится месяцами или даже годами.
Если вам интересно узнать больше о том, что происходит у нас на кухне разработки, — заходите в сообщество Yandex AI Studio [5] в Telegram. Там можно позадавать вопросы, обсудить детали и первыми узнавать о новых возможностях платформы. А если хочется следить за тем, что вообще происходит в Yandex Cloud, — подписывайтесь на канал Inside Yandex Cloud [6].
Автор: hotckisss
Источник [7]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/32786
URLs in this post:
[1] обучения: http://www.braintools.ru/article/5125
[2] памяти: http://www.braintools.ru/article/4140
[3] потребность: http://www.braintools.ru/article/9534
[4] зрения: http://www.braintools.ru/article/6238
[5] Yandex AI Studio: https://t.me/YFM_Community
[6] Inside Yandex Cloud: https://t.me/+g41wkzII0Jg1MzJi
[7] Источник: https://habr.com/ru/companies/yandex/articles/1056694/?utm_campaign=1056694&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.