Привет! Меня зовут Андрей Пахомов, я разработчик в AI Platform Битрикс24. Сегодня расскажу, почему self-hosted LLM ломается в проде, где на самом деле возникают проблемы и какие метрики помогают вовремя это увидеть.
Когда прототип превращается в реальный сервис с пользователями и пиками нагрузки, становится понятно: устойчивость системы зависит не от «правильной» модели, а от наблюдаемости. Важно видеть, где вы упёрлись в пропускную способность пула, где падают воркеры, а где проблема вообще не в GPU, а в размере контекста или предобработке входа.
Хороший пример — увеличение контекстного окна модели. Мы расширили его с 64k до 85k и сняли 73.5% ошибок прежней конфигурации. Но заметный хвост запросов продолжил падать. Это наглядно показывает: точечные улучшения не решают проблему целиком, если вы не понимаете, где именно система деградирует и почему.
TL;DR — краткое саммари по статье
-
Для стабильной эксплуатации self-hosted LLM достаточно базового набора из пяти метрик:
queue depth;
active workers;
processing latency;
TTFB;
потребление ОЗУ контейнером воркера.
Очередь между API и инференсом полезна не только для backpressure, но и потому, что делает деградацию наблюдаемой.
Увеличение контекстного окна помогает, но само по себе не решает класс проблем: по нашим логам переход с 64k на 85k закрыл 73.5% ошибок, а верхний хвост остался.
Предобработка во воркере может привести к тем же сбоям, что и перегруженный GPU-пул, поэтому за памятью воркеров нужно следить так же внимательно, как за latency.
MaaS vs Self-hosted: когда свой стек действительно оправдан
Когда команда обсуждает запуск LLM у себя, разговор обычно начинается с качества ответов и заканчивается бюджетом на GPU. Но на практике сначала нужно решить более приземлённый вопрос: где физически и юридически живут данные. Если в запросах есть ПДн, коммерчески чувствительная информация или внутренние документы, внешний MaaS означает не только технический, но и комплаенс-риск.
MaaS выигрывает скоростью запуска: зарегистрировались, получили API-ключ, интегрировали за пару дней. Для MVP это отличный путь.
Если ваши запросы нерегулярные, в них мало чувствительных данных и нет жёстких требований к периметру, MaaS почти всегда дешевле по времени команды.
Self-hosted выигрывает в других сценариях:
Чувствительные данные в промптах. Если в запросах есть клиентские документы, внутренние отчёты, кейсы поддержки или персональные данные, контроль периметра становится критичным.
Предсказуемость нагрузки. При больших и стабильных нагрузках self-hosted может быть дешевле и предсказуемее по затратам, чем MaaS.
Управляемость поведения. Можно более гибко управлять маршрутизацией, приоритетами, очередями и деградацией сервиса.
Ограничения по инфраструктуре/договору. Иногда внешний провайдер просто не проходит по требованиям.
Если нагрузка системная и контроль данных важен, self-hosted быстро становится не экзотикой, а необходимостью. Следующий вопрос уже не «можно ли поднять модель у себя», а как хорошо вы подготовитесь к реальным пикам, ошибкам и деградации.
Очередь как инструмент наблюдаемости
Работа с OpenAI или другим крупным провайдером часто ощущается как почти бесконечный ресурс. В пробном режиме синхронная работа действительно часто не вызывает проблем. Но то, что выглядит устойчиво на тестовом стенде, в проде быстро ломается из\-за пиков нагрузки и смешанных типов задач. В этот момент первое очевидное решение — добавить очередь, чтобы балансировать нагрузку и не терять клиентские запросы из\-за таймаутов.
Вместо конкретного брокера важнее понимание, какие эффекты даёт очередь и почему после её появления без метрик уже не обойтись.
Коротко: очередь даёт три практических эффекта.
Приоритеты и отложенная обработка
Срочные задачи должны проходить быстрее фоновых. Например, пользовательский чат нужно обработать быстрее массовой разметки или бэкфилл. Без очереди и приоритизации «тяжёлые» задания быстро забивают GPU и увеличивают latency для всего потока.
Роутинг по пулам моделей
Аппетит приходит во время еды. Впервые внедрив ИИ для решения каких-то задач, скорее всего, вы найдёте новые сценарии его применения. Поэтому виды задач и количество разных моделей под разные нужны начнет только расти: транскрибация, генерация текста, изображений.
Разные по нагрузке, скорости и требованиям задачи нельзя держать в одном пуле, иначе они начинают мешать друг другу. Очередь позволяет направлять разные типы задач в отдельные воркеры и модельные пулы под нужный SLA.
Гарантия ответа с допустимой задержкой
При перегрузке очередь дает backpressure: задача не теряется, а ожидает своей очереди. Для пользователя это может быть «чуть позже», но зато он не получит ошибку 502 сразу в ответ на запрос.

Схема: клиент → API Gateway → очередь → воркеры → vLLM → результат и метрики
Есть вещи, с которыми очередь не поможет:
не заменит идемпотентность задач;
не исправит плохую схему ретраев;
не снимет лимиты на стороне пула.
Зато она делает поведение системы заметно предсказуемее. Это позволяет наблюдать систему и отличать нехватку ресурсов от сетевых проблем, падений воркеров и других неприятных сценариев.
Что мониторить в первую очередь
Очередь ценна не только тем, что даёт backpressure — она делает поведение системы наблюдаемым. Когда перед моделью появляется слой очереди и воркеров, деградацию можно разложить по пунктам: где вы упёрлись в пропускную способность, где шумит сеть, а где ломается предобработка или сама конфигурация модели.
Для этого обычно хватает связки Prometheus + Grafana. Но как только в системе появляется очередь, возникает следующий логичный вопрос: она действительно помогает или просто аккуратно прячет проблему за растущим backlog?
Чтобы это понять, советую начать с пяти базовых метрик:
Количество сообщений в очереди (queue depth).
Количество активных обработчиков (active workers).
Гистограмма времени обработки сообщения (processing latency).
TTFB (time to first byte) со стороны клиентского API.
Потребление ОЗУ контейнером воркера.
Как читать метрики в связке
-
Queue depth растёт \+ active workers на максимуме.
Причина: не хватает мощности пула, обычно GPU. Этот сценарий особенно хорошо ловит периодические всплески нагрузки: пул уже разогнан до предела и новые задачи начинают копиться в очереди.
Пример: рост клиентских запросов по расписанию (начало рабочего дня, старт рекламной кампании, выход релиза). Очередь и глубина backlog показывают, что в эти окна спрос на вычисления стабильно опережает пропускную способность, даже если «в среднем за сутки» всё выглядит терпимо.
-
Queue depth растёт \+ active workers низкий или падает.
Причина: проблемы с воркерами, сетью между брокером и воркерами, сетью между воркерами и GPU серверами.
-
TTFB начинает выходить за p95/p99 при нормальном processing latency.
Частая причина: сетевые или прокси-проблемы между сервисами.
-
Стабильный рост processing latency — это сигнал деградации, даже если очередь не растёт.
Чтобы разобраться, смотрите не на длину очереди, а на гистограмму latency. В первую очередь меняются хвосты распределения: p95 и p99 начинают расти относительно прошлой недели или базового уровня. Всё больше задач перестают укладываться в те сроки, в которые укладывались раньше при сопоставимом трафике. При этом очередь может выглядеть нормально: backlog не растёт, воркеры постоянно заняты, но нагрузка уже даёт о себе знать.
Часто причина в том, что пул обрабатывает поток медленнее или сами задачи становятся тяжелее: длиннее вход, выше max\_tokens, больше reasoning.
Если у продукта есть требования к скорости ответа (SLO или latency budget), рост перцентилей — это повод наращивать ресурсы или пересматривать размер и класс задач. Ждать, пока очередь визуально «взорвётся», в этом случае уже поздно, потому что к этому моменту система уже давно деградирует.

Мы уже перечислили много важных метрик: очередь, активные воркеры, latency, TTFB. В реальной эксплуатации быстро всплывает ещё один класс проблем, который снаружи выглядит почти так же, как нехватка GPU, хотя причина совсем в другом.
Неочевидный инцидент: ZIP-бомба и «токсичная» предобработка
Иногда модель ожидает уже подготовленные данные: картинку, которую нужно скачать и перевести в base64, аудио для транскрибации, zip-архив с файлами и так далее. Чтобы не усложнять логику на стороне модели, предобработку часто делают прямо внутри воркера, который забирает задачу из очереди.
Проблема в том, что файл может весить немного при загрузке в очередь, но после распаковки или преобразования занимать в разы больше памяти. Многие форматы хорошо сжимают содержимое. Лимиты на размер входного файла здесь не спасают, потому что данные могут упереться в лимит памяти контейнера воркера уже при переводе в «сырой» вид. Например, при распаковке zip или подготовке аудио для VAD.
В таких случаях возникает эффект «токсичной» задачи. Воркеры начинают падать по OOM (Out Of Memory), но сама задача остаётся в очереди и снова берётся в обработку. Контейнеры перезапускаются, но ситуация повторяется: как минимум один воркер постоянно занят попыткой обработать этот запрос.
Если «токсичных» задач несколько, система быстро деградирует. Воркеры тратят ресурсы на бесполезные попытки обработки, а нормальные задачи встают в ожидание. В худшем случае это превращается в простую атаку: достаточно отправить несколько «тяжёлых» архивов, чтобы заблокировать обработку запросов других пользователей.
Что нужно запомнить:
За памятью воркеров нужно следить так же внимательно, как за GPU.
Если вы видите, что потребление памяти регулярно упирается в лимит или растёт скачками, проблема, скорее всего, не в модели. Это сигнал смотреть на вход и предобработку: какие данные приходят, как они преобразуются и нужно ли фильтровать такие задачи заранее или выносить их в отдельный класс воркеров.
Минимальный набор алертов
queue_depth > X в течение N минут.
active_workers < порога для активной очереди.
ttfb_p99 > SLO и отдельно processing_latency_p95 \> SLO.
“heartbeat”/uptime воркеров (чтобы быстро видеть падения контейнеров).
worker_memory_usage близко к лимиту \+ всплески OOMKill/рестартов: ранний индикатор «токсичных» задач на предобработке (например, распаковка архивов с аномальным коэффициентом сжатия), которые могут циклически убивать воркеры и блокировать очередь.
Главный принцип: смотрите не на отдельную метрику, а на корреляцию.
Так вы реже ошибётесь в диагнозе. И это как раз тот момент, где observability выводит к следующему слою проблем: даже если очередь здорова, воркеры живы, а сеть не шумит, сама конфигурация инференса всё равно может ломать прод уже на уровне модели.
Где ломается инференс: контекстное окно и бюджет токенов
В нашей компании для разворачивания моделей мы используем фреймворк vLLM, поэтому то, о чем речь пойдет ниже — базируется на работе с ним. Но в целом эти наблюдения и рекомендации должны работать и на других фреймворках, потому что следуют из самой математики контекстного окна и бюджета токенов.
Почему vLLM часто берут в self-hosted:
Хорошая пропускная способность на инференсе.
Привычный OpenAI-совместимый API.
Зрелая экосистема вокруг деплоя и интеграций.
Причина основных проблем в проде
На практике инциденты чаще возникают не на уровне инференс-движка, а на уровне контекстного окна и бюджета токенов.
У облачных провайдеров инфраструктура обычно держит заявленный контекст модели, и в лимиты вы почти не упираетесь. Но на своих серверах большой контекст напрямую означает дорогую инфраструктуру. Держать окно в 256k токенов на multi-GPU под реальные задачи в 64k чаще всего просто невыгодно. При уменьшении контекста часть запросов перестаёт в него помещаться, и возникает новый класс падений.
Как это было видно на наших логах
Здесь важно смотреть не на все запросы, а только на те, которые уже упали из-за нехватки окна. В этой выборке мы сравнили два лимита — 64k и 85k.
Результат: увеличение окна сняло около 73.5% ошибок прежней конфигурации. Это заметный эффект — частота падений уменьшается примерно в 3 раза. Но это не решение проблемы, потому что остаётся верхний хвост запросов, которые всё равно не помещаются. Но самые тяжёлые случаи остаются:

Обратите внимание, что это не лабораторный эксперимент, а operational-анализ логов ошибок. Мы не проводили чистый A/B-тест, а сравнивали реальные ошибки на двух параллельно работающих конфигурациях. Для продовой практики такой анализ полезнее: он показывает, как система ведёт себя в реальных, а не идеальных условиях.
Такая статистика позволяет осознанно выбирать размер окна. Если построить зависимость «размер окна → доля устраняемых ошибок», получится кумулятивная кривая. Она показывает, какое окно нужно, чтобы убрать, например, 95% ошибок определённого класса, и сколько будет стоить оставшийся хвост.
Нужно ли увеличивать окно на всех серверах
Из анализа видно, что это не всегда правильная стратегия.
Если длинных запросов немного, дешевле вынести их в отдельный пул с большим max_model_len, а основной трафик оставить на более коротком и дешёвом окне.
Обратная стратегия тоже рабочая: если увеличить окно нельзя или это слишком дорого, часть проблемы можно решить до инференса через суммаризацию, обрезку истории, уменьшение RAG-вставок или более жёсткие лимиты на вход.
Сколько стоит длинный контекст
Теперь посмотрим на требования к железу для разных моделей при максимальном и минимальном размере окна.
Ниже приведены ориентиры для vLLM (BF16 или рекомендованные кванты; для gpt-oss — MXFP4), один поток запросов без агрессивного batching. Фактические цифры зависят от конкурентности и max_model_len в конфиге. Это не точные спецификации, а инженерная оценка по открытым источникам и практическим ограничениям по VRAM.
Под «минимально допустимым окном» я имею в виду не минимальный размер, при котором модель вообще запускается, а минимальный размер, при котором она остаётся полезной в продукте. Технически модель часто можно завести и с окном 2K–4K, но с длинным контекстом вы тогда почти не работаете.
Кривая ошибок от размера контекстного окна |
Макс. контекст (паспорт) |
Конфигурация GPU под макс. окно |
Минимально допустимое окно* |
Конфигурация GPU под мин. окно |
gpt-oss-120b |
128k |
1× H100 80GB / 1× A100 80GB (MXFP4, TP=1; на H100 нужно аккуратно тюнить memory utilization) |
16k-32k |
1× H100 80GB / 1× A100 80GB |
Qwen3.6-35B-A3B |
256k |
1× H100/H200 80GB с официальным FP8 checkpoint или 2× H100 80GB в BF16 |
128k |
1× H100/H200 80GB с FP8 или 2× H100 80GB BF16 |
Kimi-K2.6 |
256k |
8× H200 141GB (official verified, native INT4) или эквивалент ~640 GB aggregate VRAM |
128k |
8× H200 / ~640 GB VRAM |
Gemma 4 26B-A4B IT |
256k |
1× H100 80GB BF16 (safe); по vLLM docs — 1× NVIDIA GPU 80GB+ |
32k |
1× A100/H100 80GB BF16 |
Источники по паспортным окнам и требованиям к железу — анонс [gpt-oss](https://openai.com/blog/introducing-gpt-oss) (128K, MoE, размещение в 80GB/16GB для квантованных весов), а также репозитории [Qwen3](https://github.com/QwenLM/Qwen3) и [Kimi K2](https://github.com/moonshotai/kimi-k2).
Длинный контекст — это не просто характеристика модели, а реальная стоимость.
По таблице видно, что погоня за максимальным окном быстро становится дорогой. В этот момент “128K в model card” перестаёт быть красивой цифрой и превращается в эксплуатационную проблему. Нужно либо заранее ограничивать запросы, либо очень хорошо представлять сбои в проде.
Оценивать размер контекста «на глаз» нельзя, потому что токенизация сильно зависит от данных. Чтобы не гадать, я отдельно замерил токенизацию для gpt-oss и Qwen3 на разных типах входа:
Короткий чат.
Длинный текст.
JSON на русском и английском.
Для gpt-oss использовался tiktoken (o200k_harmony), результаты дополнительно проверены через Hugging Face tokenizer openai/gpt-oss-20b. На тестовых данных значения совпали. Для Qwen3 использовался актуальный tokenizer Qwen3-32B.
Сначала — результаты.
Кейс: короткий чат
Типичный вход: короткие реплики, история диалога, немного системного контекста
Безопасный ориентир для gpt-oss: ~3.5-4.0 символа/токен на русском, ~5.0 на английском
Безопасный ориентир для Qwen3: ~3.0-3.2 на русском, ~5.0 на английском
Что закладывать по окну: 8K-16K обычно хватает с запасом
Что делать с output/reasoning: Держать минимум 2K-4K output tokens
Кейс: Длинный текст / RAG-контекст
Типичный вход: большие фрагменты документов, переписка, выдержки из базы знаний
Безопасный ориентир для gpt-oss: ~4.0-4.2 на русском, ~4.7 на английском
Безопасный ориентир для Qwen3: ~3.2-3.3 на русском, ~4.7 на английском
Что закладывать по окну: 32K рабочий минимум, 64K комфортнее для нескольких документов
Что делать с output/reasoning: Оставлять 4K+ под ответ и не забивать окно «в ноль»
Кейс: Structured output / agent
Типичный вход: JSON, tool calling, schema-guided ответ, служебные поля
Безопасный ориентир для gpt-oss: ~3.5-3.9 символа/токен
Безопасный ориентир для Qwen3: ~3.1-3.8 символа/токен
Что закладывать по окну: 16K-32K обычно хватает, но зависит от числа инструментов и объема схемы
Что делать с output/reasoning: резервировать 4K-8K output tokens, иначе reasoning может сломать финальный JSON
Кейс: длинные reasoning-задачи
Типичный вход: многошаговый анализ, планирование, сложные агентские цепочки
Безопасный ориентир для gpt-oss: считать консервативно, как для structured input
Безопасный ориентир для Qwen3: считать консервативно, как для structured input
Что закладывать по окну: лучше планировать от 32K и выше
Что делать с output/reasoning: явно снижать reasoning, если нужен короткий финальный ответ
Кейс |
Типичный вход |
Безопасный ориентир |
Что закладывать по окну |
Что делать с output/reasoning |
Короткий чат |
Короткие реплики, история диалога, немного системного контекста |
~3.5-4.0 символа/токен на русском (gpt-oss) |
8K-16K обычно хватает с запасом |
Держать минимум 2K-4K output tokens |
Длинный текст / RAG-контекст |
Большие фрагменты документов, переписка, выдержки из базы знаний |
~4.0-4.2 на русском, ~4.7 на английском (gpt-oss) |
32K уже рабочий минимум, 64K комфортнее для нескольких документов |
Оставлять 4K+ под ответ и не забивать окно “в ноль” |
Structured output / agent |
JSON, tool calling, schema-guided ответ, служебные поля |
~3.5-3.9 символа/токен (gpt-oss) |
16K-32K обычно хватает, но зависит от числа инструментов и объема схемы |
Резервировать 4K-8K output tokens, иначе reasoning может сломать финальный JSON |
Длинные reasoning-задачи |
Многошаговый анализ, планирование, сложные агентские цепочки |
Считать консервативно, как для structured input |
Лучше планировать от 32K и выше |
Явно снижать reasoning, если нужен короткий финальный ответ |
Пояснения и наблюдения к результатам
Правило «1 токен = 4 символа» работает только как грубая оценка. На английской прозе оно ещё более-менее держится: около 4.7–4.8 символа на токен. Но на русском плотность хуже — примерно 4.2 у gpt-oss и около 3.2 у Qwen3. Для коротких реплик и особенно для JSON коэффициент падает ещё сильнее из\-за структуры, ключей и пунктуации.
Практически это означает, что окно заканчивается быстрее, чем кажется. Например, 32K токенов — это примерно 100–138 тысяч символов русского текста у gpt-oss и около 100–106 тысяч у Qwen3 на длинной прозе. Для 64K — примерно 200–276 тысяч.
Это всё ещё не «бесконечный контекст». Как только в промпте появляются JSON, служебные поля и описания инструментов, расчёт становится менее точным, и запас нужно закладывать больше.
Отдельная проблема — расход токенов на reasoning. У reasoning-моделей часть бюджета уходит не в финальный ответ, а во внутреннее рассуждение. В gpt-oss это регулируется через reasoning effort, в Qwen3 — режимами thinking и non-thinking.
Если вам нужен строгий JSON или короткий служебный ответ, эти настройки лучше снижать заранее. Иначе модель может «умно подумать», но не довести ответ до валидного состояния.
Три типа ошибок контекстного окна, которые реально встречаются
Сценарий |
Типичный симптом |
Что делать |
input_tokens больше окна модели |
Ошибка в API сразу при запросе |
Считать токены до отправки, делать суммаризацию/обрезку контекста |
input_tokens помещается, но input + max_completion_tokens > лимита |
Ошибка валидации или ранний отказ модели |
Пересмотреть max_completion_tokens, уменьшить вход, поднять лимит модели (если доступно) |
Reasoning “съедает” бюджет, финал обрезается |
Битый/неполный JSON, обрыв на полуслове |
Держать запас на финальный ответ, отделять reasoning от требуемого формата, валидировать output |

Во всех трёх случаях выше клиент либо не получает ответ, либо получает явно сломанный результат. Такие проблемы легко заметить: они сразу видны в логах и алертах.
Но есть ещё более неприятный сценарий «гниение контекста»: модель возвращает ответ на запрос, но его качество заметно низкое из-за слишком длинного или перегруженного контекста. С точки зрения пользователя сервис при этом всё равно не работает: ответ неточный, поверхностный или просто хуже ожидаемого.
Почему это проблема: такие случаи сложно отследить. В отличие от явных ошибок, деградация качества не проявляется в логах. Без продуктовых метрик и бенчмарков она может долго оставаться незаметной.
Практика, которая экономит дни
Самое дешёвое место, где можно остановить эту проблему, находится не в vLLM, а ещё до вызова модели в API-слое:
def preflight_context_limit(input_tokens: int, max_completion_tokens: int, model_window: int) -> None: total_budget = input_tokens + max_completion_tokens if input_tokens > model_window: raise ValueError("input exceeds model context window") if total_budget > model_window: raise ValueError("input + completion budget exceeds model context window")
Такой preflight не заменяет суммаризацию и нормальную сборку контекста, но позволяет отсеивать заведомо плохие запросы до того, как они дойдут до инференса и начнут ломать систему.
Несколько практических правил:
Считайте токены до запроса, не «примерно по символам», а токенизатором или совместимым методом оценки.
Оставляйте запас под ответ. Не ставьте max_completion_tokens вплотную к лимиту окна.
Валидируйте структурированный ответ. Если ожидаете JSON, проверка схемы обязательна — иначе обрезанный ответ попадёт дальше по пайплайну.
Проводите бенчмарк на разные длины входа. Поведение модели на 2K и 64K токенов может заметно отличаться.
Чеклист перед production
Мой рекомендованный итоговый список проверок перед продом:
Очереди и пулы архитектурно разделены по классам задач.
Есть чёткие SLO по TTFB и processing latency, под них настроены алерты.
У воркеров есть heartbeat/uptime-метрики и понятная retry/panic-стратегия.
На API-слое реализован preflight для проверки токенов.
Для структурированных ответов есть валидация и fallback-поведение.
Вместо вывода
Self-hosted даёт контроль, но приносит ответственность за эксплуатацию.
Вы больше не можете спрятать деградацию за чужим API. Если растёт очередь, ползут p95/p99, контекст выбивается за лимиты или воркер падает на предобработке — это уже ваша проблема. Решается она не только железом, но и тем, насколько быстро вы её замечаете и правильно диагностируете.
Если оставить одну мысль из всей статьи, она простая: сначала проектируйте наблюдаемость, потом масштабируйте инференс. Именно это отличает красивый пилот от системы, которая реально живёт в проде.
Если будет полезно, в следующем материале можно отдельно разобрать один из двух кейсов: либо как строить preflight и работать с длинным контекстом, либо как выстраивать алерты и диагностику вокруг очередей и воркеров.
KSupalo
Ваша статья тянет на создание Агента по инспектированию "своих" LLM - посмотрите пожалуйста в эту сторону. Вы создаете Агента который сразу проводит тестирование LLM Заказчика, показывает что необходимо перенастроить, в идеале - периодически запускается для понимания что изменилось и как под это подстроиться. Уверен, что за такого Агента будут Вам платить, сам бы точно заплатил, особенно если бы был референс или история использования этого Агента, статистика по % успешности решения им задач. Подумайте, почему нет?!
pandy Автор
Имеется ввиду что то аналогичное утилите mysqltuner, только для LLM ? Мысль интересная, но там скорее всего рекомендации будут вида: закупить 5 серверов с H200 )