«Давайте просто развернём свою LLM на 1Т параметров»: что пришлось построить вокруг неё в Битрикс24

Привет, Хабр! Меня зовут Сергей Нотевский, я AI Platform Lead в Битрикс24. Моя команда отвечает за модельки под капотом наших AI-сервисов.
«Давайте просто развернём свою LLM на 1Т параметров» — с таким названием я выступал в июне на Saint HighLoad++. В статье пройдём тот же путь: от продукта с одним endpoint до платформы вокруг моделей.
Допустим, модель уже запустили. Она отвечает, продукт умеет в неё ходить. Теперь рядом с коротким чатом появляется обработка длинных документов, а за ней — агент, который обращается к модели на каждом шаге. Как им поделить общие мощности? Что делать с работой, которую можно отложить? И как после очередной оптимизации понять, что модель всё ещё отвечает как надо?
По ходу будем достраивать схему — как делали на докладе. Это логическая схема: она объясняет решения, не описывая конкретный состав оборудования и размещение сервисов.
Что мы обрабатываем
У нас AI есть в CRM, чатах, генерации сайтов и агентах. После звонка помогаем менеджеру получить саммари, заполнить карточку CRM и оценить качество разговора. Для пользователя это один сценарий, и время ожидания важно на весь сценарий целиком.
За одним пользовательским запросом может стоять несколько обращений к модели. У агента число шагов ещё и зависит от того, как идёт решение задачи. Поэтому по числу запросов в API трудно понять, сколько работы предстоит платформе. Пока один запрос обрабатывает большой контекст, соседнему чату тоже нужны память и место для генерации.
Почему self-hosted
До собственной инфраструктуры мы пользовались внешними API, то есть MaaS: провайдер запускает модель, мы платим за токены. При выборе между этими вариантами нас интересовали данные, гибкость и экономика.
С данными вопрос начинается до выбора модели: что приходит в запросе, какие требования есть к его обработке и где её допустимо выполнять. Эти ограничения нужно учитывать в архитектуре, в том числе при выборе резервного пути на случай отказа.
Гибкость — это возможность выбирать модели, задавать лимиты и приоритеты под сценарии продукта. А с экономикой придётся считать. На стабильной нагрузке есть смысл сравнить цену внешнего API с расходами на вычислительные мощности и команду. Но мощности оплачены и тогда, когда полезной работы для них нет.
У провайдера распределение запросов, очереди, кэширование и обработка пиков уже спрятаны за API. При переходе на self-hosted эти задачи становятся нашими и тоже входят в себестоимость. Сам по себе переезд ещё не означает, что токен стал дешевле.
Пока нарисуем самый простой вариант: продукт отправляет запрос на endpoint, за ним работает модель. Посмотрим, где этой схемы перестанет хватать.

Модель с витрины и модель в проде
Обычно всё начинается с карточки на Hugging Face. Продукт посмотрел контекст, бенчмарки, требования к памяти и пришёл с просьбой развернуть модель. Но карточка не отвечает на все вопросы, которые появятся при запуске.
Что смотрим в карточке | Что проверяем для продукта |
|---|---|
Максимальная длина контекста | Помещается ли нужный контекст при рабочей параллельности и сохраняется ли качество на длинных сценариях |
Требования к памяти | Хватит ли ресурсов не только на веса, но и на KV-cache выполняемых запросов |
Поддержка инструментов | Совместимы ли формат ответов, parser и API-контракт с нашим приложением |
Результаты бенчмарков | Решает ли модель наши задачи на данных, похожих на продуктовые |
Отдельно проверяем движок. Он может уметь запускать модель, но ещё не поддерживать нужный формат API или оптимизацию, на которую мы рассчитывали. Формат весов тоже нужно сопоставить с доступным оборудованием. Всё это выясняется до того, как продукт начнёт зависеть от нового endpoint.
Чем обеспечивать большую модель
Большая модель требует более дорогой инфраструктуры. Причём нужно обеспечить не только её запуск, но и рабочую параллельность, резерв, тестирование обновлений. Успешно ответить на один запрос и обслуживать продукт — разные задачи.
Поэтому размер модели выбираем вместе с требованиями к качеству. Если компактная модель справляется, нет смысла занимать более дорогой вычислительный пул. Крупную стоит подключать там, где выигрыш в качестве оправдывает дополнительные ресурсы. Здесь полезно считать стоимость законченной задачи: более дешёвый вызов мало помогает, если после него задачу приходится решать заново.
Ещё до запуска читаем лицензию. Проверяем условия коммерческого использования и ограничения, которые могут оказаться существенными для продукта. Возможность скачать веса сама по себе этого вопроса не закрывает.
Допустим, модели запускаются, проходят проверки, продукты умеют в них ходить. Теперь несколько разных сценариев начинают пользоваться общими мощностями одновременно.
Большие запросы и маленькие в одном пуле
Вернёмся к чату из начала статьи. Представим, что рядом с ним в общий пул приходят обработка длинного документа и многошаговая агентская задача.
По размеру входа заранее не скажешь, сколько времени займёт генерация: модель может ответить парой слов, а может уйти в длинный reasoning. Всё это время запрос занимает память и место среди выполняемых. Когда лимит параллельности занят, следующий запрос ждёт — даже если это короткая реплика в чате.

Почему просто не добавить реплик
Первое, что хочется сделать при росте очереди, — добавить реплик. Но свободные вычислительные мощности ещё нужно получить. Затем загрузить веса, подготовить окружение и проверить, что новая реплика готова принимать запросы.
Всплеск интерактивной нагрузки может произойти быстрее, чем всё это закончится. Поэтому запас нужен заранее, а планировать его приходится с учётом того, какая работа попадёт в пул.
Пулы по профилю нагрузки
Мы разделяем нагрузку по тому, что ограничивает её выполнение:
Быстрый интерактив. Важны TTFT — время до первого токена — и скорость ответа. Нужен резерв, даже если из-за него часть мощностей не занята.
Длинный контекст. Требует много памяти под KV-cache. Это влияет на допустимую параллельность.
Многошаговые задачи. Их длительность зависит от цепочки вызовов и объёма генерации. Требования к ожиданию задаёт сам сценарий.
Разделив пулы, можно задать им разные лимиты и оставить запас для интерактива. Выбор модели при этом остаётся отдельным решением: одну и ту же модель можно запустить в разных пулах, если сценариям нужна изоляция нагрузки.
Разделить пулы мало. Нужно ещё подобрать, сколько запросов выполнять одновременно. Для этого смотрим, как меняются очередь и задержка ответа при росте нагрузки. Средняя утилизация сама по себе не говорит, что у чата всё хорошо: нас интересует и то, сколько ждут запросы в хвосте распределения.
Бесконечно дробить пулы тоже не хочется: в одном мощности простаивают, в соседнем стоит очередь. Значит, нужен следующий блок — тот, который управляет допуском работы к исполнению.
Планировщик и отложенные задачи
Планировщик получает запрос вместе с метками сценария, требованиями ко времени ответа и примерной оценкой объёма работы. Готовую к работе задачу передаём в очередь исполнения. Если в нужном пуле есть место — её можно выполнять. Если лимит параллельности занят — она ждёт. А задачи, которые не нужны пользователю прямо сейчас, можно отложить.
Для отложенной задачи сохраняем входные данные и срок выполнения. Держать HTTP-соединение до её завершения не нужно. Планировщик возвращает её в работу, когда появляются свободные мощности или приближается срок. Результат доставляется отдельно. Близкий срок сам по себе ресурсов не добавляет, поэтому его тоже нужно учитывать при планировании.

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

Кто пользуется общими мощностями
До сих пор мы смотрели на запрос как на работу для модели. Но у него есть ещё владелец: команда и сценарий продукта. Для общей платформы это важно до того, как запрос попадёт в очередь.
Планировщик отвечает за доступную ёмкость. Ему недостаточно знать только текущую загрузку: нужно учитывать, какую работу разрешено запускать и какие лимиты к ней применять. Эти сведения передаём через Gateway.
У него несколько задач:
Доступы: определить потребителя и его права.
Квоты: применить лимиты команды и сценария до отправки на исполнение.
Учёт и трассировка: связать расход ресурсов с запросом, командой и сценарием.
Алиасы: дать продукту постоянное имя, за которым можно менять модель без релиза самого продукта.
Учёт нужен и для планирования: по нему можно сравнить потребности команд, понять профиль нагрузки и обсудить лимиты. Одна общая сумма расходов для этого мало что говорит.
Общий endpoint теперь находится в Gateway:

Теперь понятно, чей это запрос. Осталось выбрать, каким способом его решать.
Когда хватает обычного чата
Возьмём простой пример: пользователь начинает разговор с «привет, как дела?». Для ответа достаточно обычного чата. Инструменты здесь не нужны.
У агента задача может быть другой: найти данные, вызвать инструмент, посмотреть на результат и продолжить. Он работает в изолированном окружении. В контексте есть системный промпт, описания инструментов, а по мере работы добавляются результаты вызовов.
На схеме сравниваем обычный ответ в чате и задачу, которой нужны несколько агентских шагов. Каждый ход — ещё одно обращение к модели. Само приветствие не означает, что агент обязательно запустит такой цикл; пример нужен, чтобы показать, почему способ решения стоит выбирать до исполнения.

Семантический роутер
Для этого перед планировщиком появляется семантический роутер. Он определяет, какой путь подходит запросу: обычный ответ, обращение к более сильной модели или работа с инструментами.
Одной последней реплики для выбора недостаточно. «Да, давай» в начале разговора и после предложения выполнить действие — разные запросы. Нужно учитывать контекст, намерение пользователя, необходимость поиска или инструментов, ожидаемый объём работы. Повторный вопрос тоже может быть сигналом, что предыдущий ответ не подошёл.
Это решение принимается до постановки запроса в очередь. Выбранному сценарию нужны подходящие модель и профиль нагрузки — по ним определяется пул. Планировщик уже решает, когда для него есть ресурсы.

Кэш-роутер: какую реплику выбрать внутри пула
В пуле могут работать несколько реплик одной модели. Кажется, можно распределять запросы по кругу. Но на одной реплике нужный префикс уже лежит в кэше, а на другой тот же вход придётся обработать заново.
Коротко напомню механику. Отправляем запрос — префикс прогревается. В следующем меняем только хвост — общую часть можно взять из кэша. Меняем слово в начале — с места расхождения всё, что дальше, нужно считать заново. Даже если остальной текст совпадает.
Раскладку промпта я подробно разбирал в статьях про антипаттерны prefix cache и кэш в агентном цикле. Здесь добавляется маршрутизация: стабильный префикс мало помогает, если следующий запрос попал на другую реплику.
Повторяющиеся инструкции и описания инструментов дают возможность переиспользовать начало запроса. Новый документ в начале контекста меняет картину. Поэтому разные сценарии дают разный эффект от кэша, даже если везде используется одна и та же модель.
И ещё: переиспользованный префикс не равен такой же экономии денег или памяти. Мы избегаем повторного вычисления общей части входа, но её KV-cache по-прежнему нужен при генерации ответа.

Кэш-роутер учитывает, есть ли на реплике нужный префикс, сколько запросов сейчас выполняется и насколько заполнена память под KV-cache. Тёплый кэш полезен, пока ожидание на этой реплике не съело весь выигрыш. Иногда выгоднее выбрать свободную и пересчитать префикс. Недоступные реплики из выбора исключаем.
Этот роутер решает уже другую задачу: сценарий и пул выбраны, теперь нужно найти место исполнения внутри пула.

Но распределением запросов возможности кэша не исчерпываются. Можно поменять и формат его хранения. Тогда нужно проверить, что оптимизация не испортила ответы.
Оптимизировали память. А что с качеством?
На длинных контекстах и при высокой параллельности много памяти уходит на KV-cache. Один из способов уменьшить этот расход — хранить его с меньшей точностью. Веса модели при этом могут остаться прежними, но вычисления уже изменились.
В докладе был пример такой проверки. Мы сравнивали конфигурации на продуктовых сценариях. На коротком контексте изменение укладывалось в требования к качеству, на длинном — уже нет. Поэтому решение о таком изменении нужно принимать отдельно для каждого сценария.
Модель та же, промпт тот же, а качество ответа может измениться. По одной задержке или расходу памяти такой результат не оценить. Нужен прогон, в котором есть именно те задачи, ради которых всё запускается.
Quality Gate на изменения системы
Поводов для проверки хватает и без замены модели:
обновили движок — с ним могли измениться parser, чат-шаблон или зависимости;
поменяли параметры запуска, точность вычислений или работу с контекстом;
поправили политику семантического роутера — запрос стал попадать в другую ветку;
изменили выбор реплик — поменялись локальность кэша и задержки.
Проверка устроена как сравнение двух прогонов. В baseline идёт текущая конфигурация, в candidate — изменение, которое хотим применить. Датасеты у них одинаковые, свои сценарии добавляют продуктовые команды.
Часть ответа можно проверить кодом: валидный ли JSON, допустимые ли значения, корректны ли вызовы инструментов. Для содержательных ответов нужны другие проверки: сравнение с эталоном и оценка по критериям продукта, в том числе с помощью LLM-судьи. Результат собираем в общий дифф по качеству, задержкам и стоимости выполнения.
По этому диффу решаем, выкатывать изменение, проверять на ограниченном трафике или блокировать. В примере с точностью KV-cache короткий и длинный контекст нельзя было свести к одной средней оценке.

Если в наборе проверок только короткие запросы, о длинном контексте мы ничего не узнаем.
Как теперь выглядит путь запроса
Вернёмся к трём блокам из начала статьи. Модель всё ещё отвечает на запросы, но перед ней теперь довольно много работы: определить потребителя и его лимиты, выбрать сценарий, дождаться свободных ресурсов, найти подходящую реплику. А изменения всей этой системы нужно проверять на задачах продукта.
Так выглядит собранная схема:

Quality Gate здесь вынесен в сторону: он проверяет изменения перед выкаткой, а не каждый пользовательский запрос. Хранилище отложенных задач тоже нужно не всем — обычный чат через него не проходит.
Готовы ли вы к self-hosted
Перед переходом ответьте на несколько вопросов:
Какие требования к данным ограничивают выбор места обработки? Они же определяют, какие резервные пути допустимы.
Можете ли вы запустить на доступных мощностях модель с нужным для ваших задач качеством? Установка модели — только часть работы.
Чем вы займёте мощности между пиками? Если вся нагрузка короткая и редкая, стоит посчитать, сколько времени они будут простаивать.
Кто будет поддерживать модели и движки, проверять API-контракты, настраивать пулы и разбирать регрессии?
Как вы заметите ухудшение ответов после обновления? Инфраструктурных метрик для этого мало, нужны прогоны на сценариях продукта.
Начали с модели за endpoint. Добавили разные сценарии — пришлось разделить нагрузку и решить, что можно отложить. Для общей платформы понадобились права, квоты и учёт. Затем дошли до выбора пути запроса и реплики с подходящим кэшем. Из этих решений и выросла схема.
Менять её всё равно придётся: появятся другие модели, параметры движка и правила роутинга. Поэтому в конце доклада я возвращался к Quality Gate. После каждой такой правки нас интересует не только то, как работает инфраструктура, но и то, как модель решает задачи продукта.
Если материал показался полезным — в своем тг-канале я делюсь опытом построения AI-платформы: метрики, guardrails, семантические роутеры, движки для сервинга LLM и другие аспекты.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.