Agentic inference: почему сессии ломают старый сервинг LLM

Agentic inference: почему сессии ломают старый сервинг LLM

Один запрос к кодинг-агенту превращается в десятки вызовов модели. Сессия открывается большим префиллом на десятки тысяч токенов, это системный промпт, описания инструментов и инструкции пользователя, а каждый следующий ход дописывается к контексту, который агент отправляет целиком. Для сервера LLM это выглядит как поток независимых запросов: он маршрутизирует, кэширует и пропускает их по одному, не зная, что все они принадлежат одной траектории. Именно поэтому agentic inference требует пересборки сервинга, и команда NVIDIA Dynamo показала, как это делается: с единым session ID пропускная способность агентских нагрузок вырастает на 12-16%.

Речь о свежем инженерном разборе Dynamo. Авторы объединили свои оптимизации вокруг одного примитива, общего идентификатора сессии, который превращает request-aware инфраструктуру в program-aware. Дальше разберём, как сервер узнаёт сессии популярных агентов, почему request-level маршрутизация ломается на агентских нагрузках и как паузы на границах инструментов экономят повторные префиллы.

Почему агентский трафик не похож на чат

Чатбот чередует реплики: запрос, ответ, пауза. Агентская сессия устроена иначе. Она начинается с большого префилла, а дальше одна задача разрастается в десятки вызовов, и рядом параллельно работают короткоживущие и долгоживущие субагенты. Большую часть времени сессии модель ничего не генерирует: контекст лежит в KV-кэше, пока выполняются инструменты.

Отсюда неочевидный вывод. Пропускная способность такого сервинга зависит не столько от вычислений, сколько от двух вещей: сколько контекста система удерживает в кэше и переиспользует, и сколько сессий держит одновременно, не выходя за рамки latency-целей. Вокруг этих двух вопросов и построена вся работа Dynamo.

Что такое session-aware inference

Session-aware inference это подход, при котором сервер рассматривает запросы не по отдельности, а как части одной сессии агента. Маршрутизацией, допуском и кэшем управляет единый идентификатор, связывающий все вызовы одной траектории: где чей контекст лежит, что можно переиспользовать и чем можно пожертвовать под давлением.

В Dynamo это session_id, уникальный стабильный идентификатор цепочки рассуждений и вызовов инструментов. Все запросы одной траектории несут один и тот же session_id, а дочерние сессии добавляют parent_session_id, так система видит связи между агентом и запущенными им субагентами. Каждая оптимизация потребляет эту информацию опционально: не отправляете session ID, сервинг работает как раньше.

Как сервер узнаёт, где чья сессия

Популярные кодинг-агенты уже отправляют идентификаторы сессий, и Dynamo распознаёт их из коробки. Claude Code передаёт x-claude-code-session-id, а для дочерних агентов x-claude-code-agent-id и x-claude-code-parent-agent-id. Codex использует thread-id и x-codex-parent-thread-id, OpenCode передаёт x-opencode-session-id и x-opencode-parent-session-id. Настройка не нужна: сервер сам приводит заголовки к внутреннему формату agent_context.

Для харнессов, обвязок, в которых запускаются агенты, без нативной поддержки есть плагины. Интеграции для Pi, Hermes и OpenClaw транслируют родной идентификатор в x-dynamo-session-id, а собственный харнесс подключается одним заголовком X-Dynamo-Session-ID, для субагентов добавляется X-Dynamo-Parent-Session-ID. Дальше остальному стеку не важно, откуда пришёл запрос.

Почему request-level маршрутизация ломается на агентах

Стандартный роутер решает задачу размещения по одному запросу: смотрит на промпт и выбирает воркер с наибольшим пересечением кэша. Для отдельного хода это верно, для агента нет. Первый провал: раздувание занятости кэша. Между ходами кэш агента не освобождается, блоки продолжают лежать в HBM, пока агент выполняет инструмент вне GPU. Суммарный рабочий набор при этом растёт как произведение числа сессий на длину их контекста, и request-level роутер не видит этот агрегат, он всегда смотрит на контекст одного хода.

Как только набор превышает HBM, а затем и DRAM, движок начинает вытеснять кэш живых сессий, и каждый следующий ход платит полный повторный префилл вместо попадания в кэш. Второй провал: нет backpressure на границах инструментов. Когда воркер перегружен, у request-level роутера только два рычага, отменить запросы в полёте или поставить их в очередь. Оба хуже третьего: отложить сессию в точке паузы прямо перед следующим ходом, когда она всё равно простаивает.

Admission control на уровне сессии

Первая agent-aware стратегия Dynamo переносит планировщик из статьи ThunderAgent (Kang et al., 2026) на KV-aware роутер. Она написана на Rust и работает как нативный плагин, который сам владеет роутером, без лишнего прокси-хопа, и считает реальные prompt_tokens и completion_tokens из каждого ответа, а не оценивает токены по байтам.

Планировщик группирует запросы по program_id, то есть по session ID, и ведёт каждую программу через два независимых состояния: REASONING или ACTING, ACTIVE или PAUSED. В ACTING программа переходит на границе инструмента. Под давлением памяти на паузу ставятся именно ACTING-программы, при этом decode-преемпшена нет: текущий ход досчитывается до конца, а движок получает свободу вытеснить его KV-блоки. Если ACTING-программ на воркере не осталось, помечается самая маленькая REASONING-программа, и она встанет на паузе при следующей границе инструмента.

Возобновление устроено как bin-packing: приостановленные программы сортируются по числу токенов и возвращаются от самых маленьких, пока следующая не вытолкнет утилизацию обратно за порог. Возобновлённые запросы получают секундный приоритетный буст, чтобы не застрять за новыми поступлениями, а форсированный возврат через 30 минут гарантирует, что ни одна программа не будет голодать бесконечно.

Контур управления и пороги

Паузы и возобновления управляются одной величиной на воркер: утилизацией, долей KV-пула, занятой рабочими наборами назначенных программ. Порогов три. При утилизации от 0.95 воркер считается переподписанным: тик ставит на паузу самые маленькие ACTING-программы, пока показатель не опустится до 0.80. В полосе от 0.80 до 0.95 программы ещё не паузятся, но получают штраф приоритета -2.0 как ранний backpressure. Возобновление срабатывает только при утилизации не выше 0.85: это порог паузы минус гистерезис 0.10, буфер, который не даёт контуру дребезжать на самой границе.

Результаты

На SWE-bench, где крутились две реплики TP4 MiniMax-M2 на одной ноде с восемью H100, program-aware планирование дало примерно 12-16% прироста пропускной способности относительно одного KV-роутинга. Разрыв объясняется именно работой повторного префилла, которую request-level маршрутизация не может избежать.

Похожая картина на агентских RL-роллаутах. На Uni-Agent SWE-Bench с Qwen3-Coder-30B-A3B-Instruct против стандартного Global LB из VERL стратегия Dynamo шла наравне при низкой конкурентности (32-128), где паузы не требовались. На средней (192-256) вырвалась вперёд с приростом 11.0-14.6% по пропускной способности модельных токенов. На высокой (384-512) Global LB резко просел, а session-aware планировщик продолжал масштабироваться. По всему диапазону он удерживал долю попаданий в prefix-кэш выше 94.5%.

Трейсы и replay: как измерить агентскую нагрузку

Чтобы такое оптимизировать, нагрузку нужно измерять. Dynamo включает сбор трейсов переменной DYN_REQUEST_TRACE=1: после каждого ответа пишется запись request_end с полями сессии, числом выходных токенов, причиной остановки, метриками KV-кэша, именами инструментов и блоком replay с длиной входа и хешами последовательностей. Содержимое промптов и ответов по умолчанию не сохраняется. Трейсы дают единую картину: префилл, декод и выполнение инструментов на одной временной шкале.

Захваченный трейс превращается в многоразовый бенчмарк: одна запись воспроизводит расписание запросов сколько угодно раз без прогонов модели и инструментов. Офлайн-путь это AISimulate: симуляция планировщика, роутера и KV-кэша позволяет сравнить число воркеров, топологии, политики маршрутизации и ёмкость кэша, не тратя GPU-часы. Живой путь это AIPerf: тот же граф запросов прогоняется по реальному эндпоинту Dynamo на настоящих GPU ради финальных цифр по latency, throughput и кэшу.

KV-кэш вне GPU: shared-pool indexer

Когда рабочий набор перестаёт помещаться в HBM, кэш не исчезает: он уезжает в память CPU, а затем во внешние хранилища вроде Mooncake. Раньше роутер Dynamo учитывал только GPU и нативный CPU-оффлоад, а внешнее хранилище для него было чёрным ящиком. Теперь вместе с командой Mooncake сделано эмитирование KV-событий из хранилища и экспериментальный shared-pool indexer, работающий в паре с FlashIndexer.

Страница считается попаданием только тогда, когда присутствуют все физические объекты под раскладку параллелизма воркера. Роутер хеширует промпт в KV-страницы SGLang и разворачивает каждую логическую страницу в физические объекты Mooncake с учётом TP/PP-раскладки, K/V-тензоров и опционального тега бэкенда. Итоговый счёт размещения складывается из пересечения на нативных уровнях, настраиваемого кредита за shared-пул и текущей загрузки воркера. Так воркер с коротким локальным префиксом получает кредит за суффикс, который уже лежит в Mooncake, и GPU-попадание при этом не считается дважды.

Programmatic KV cache: роутер решает, движок исполняет

Следующий шаг, сессионное управление KV-блоками, где роутер выступает мозгом, а движок руками. Роутер знает жизненный цикл сессий и паттерны переиспользования, движок знает точное состояние своего кэша. Поэтому Dynamo предлагает узкий интерфейс подсказок KvHint: Share переносит кэшированный префикс между воркерами через p2p, Prefetch подтягивает KV с более высокого уровня заранее, Demote вытесняет его при долгом вызове инструмента, Pin удерживает ценные блоки на уровне с ограниченным TTL, Retain смещает вероятность сохранения блоков под давлением.

Каждая подсказка несёт с собой session ID, чтобы холодные уровни группировали блоки по сессиям, а не хранили безымянные хеши. Движок вправе обрезать, отложить или проигнорировать любую подсказку, а нагрузки без подсказок ведут себя ровно как раньше. Чтобы такие политики стало возможно вычислять, в роутере появился Session-Prefix Indexer: радиксное дерево, где каждая ветка это уникальная сессия или субагентская сессия, ответвившаяся от другой. Он обновляется из KV-событий движков и попаданий FlashIndexer, давая роутеру совокупную картину: кому принадлежат блоки и к каким сессиям они относятся. Реализация Share уже встроена в HiCache, Prefetch и Demote спроектированы поверх неё.

Что это значит для разработчиков

Сервинг LLM перестаёт быть про чат и становится про агентов. Если вы запускаете агентов на своём стеке, есть три практических шага. Проверить, какие заголовки сессий отправляет ваш харнесс: многие популярные агенты делают это по умолчанию. Включить трассировку и посмотреть, где на самом деле уходит время между ходами. Считать не только latency отдельного запроса, но и стоимость переиспользования контекста на всей сессии: доля попаданий в prefix-кэш и динамика повторных префиллов расскажут о нагрузке больше, чем средний отклик.

Главный вывод статьи Dynamo прост: переиспользование кэша и конкурентность сессий нужно оптимизировать вместе. Держать полезный контекст между ходами и не платить за повторный префилл при росте числа сессий, вот на чём будет строиться экономика агентского инференса.

Часто задаваемые вопросы

Нужно ли переписывать агента, чтобы включить session-aware оптимизации?

Нет. Claude Code, Codex и OpenCode работают без настройки, потому что уже отправляют заголовки сессий. Для остальных харнессов есть готовые плагины, а кастомному достаточно одного заголовка X-Dynamo-Session-ID. Все оптимизации опциональны: без session ID сервинг ведёт себя как раньше.

Работает ли это с vLLM и SGLang?

Да. Общий индекс KV-кэша и программное перемещение кэша работают поверх vLLM и SGLang, а интерфейс KvHint спроектирован как мягкие подсказки, не требующие переписывать планировщики движков. Реализация Share уже встроена в HiCache от SGLang, остальные подсказки в работе.

Почему бы просто не увеличить KV-кэш?

Потому что рабочий набор растёт с числом параллельных сессий: чем их больше, тем больше суммарный контекст. HBM и DRAM всё равно закончатся, поэтому выигрыш даёт не только ёмкость, но и умное переиспользование: маршрутизация к нужным блокам, паузы на границах инструментов и перенос кэша между уровнями памяти.

Итог

Агентская нагрузка ломает привычную модель сервинга: запросы перестают быть независимыми, а кэш живых сессий превращается в главный дефицитный ресурс. Dynamo отвечает на это единым session ID, поверх которого собираются маршрутизация, admission control с паузами на границах инструментов, общий индекс KV-кэша и программные подсказки для движков. Прирост в 12-16% на SWE-bench показывает, что запас здесь реальный, и менять модели для этого не нужно. Начните с малого: проверьте, какой session-заголовок отправляет ваш харнесс, и включите трассировку. Первые цифры по повторным префиллам обычно удивляют.

← Все записи