Статья

Что кэш-роутер знает о кэше

KV на worker, индекс размещения и сведения об активной работе живут и восстанавливаются по-разному. Сравнение роутеров начинается с происхождения этих данных.

Автор — Сергей Нотевский

Опубликовано

Обновлено

Роутер отправил запрос на тёплую реплику. Откуда он знает, что она тёплая? Возможно, раньше он отправлял туда похожий текст. Возможно, worker сообщил, какие блоки KV сохранил и какие удалил. Для выбора маршрута полезны оба источника, но подтверждают они разное.

Здесь я разбираю standalone vllm-router на commit 1d10e71 и документацию NVIDIA Dynamo v1.4.0. vLLM Semantic Router — другой проект; выводы об этой cache_aware policy к нему не относятся. Это проверка механизмов по коду и документации на 5 сентября 2026 года. Сравнительных результатов на GPU в статье нет.

При отказе теряются разные состояния

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

СостояниеЧто в нём хранитсяВопрос после перезапуска
KV-данныеПромежуточные состояния моделиОстались ли блоки и совместимы ли они с запросом?
Индекс размещенияСвязь префикса или блоков с workerКто знает об этих блоках и насколько свежи сведения?
Активная работаНазначенные запросы и оценка незавершённой обработкиВидит ли новый роутер уже исполняющиеся запросы?

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

vllm-router строит прогноз по истории назначений

В проверенной политике дерево хранит переданную ей строку и назначения workers. Совпадение оценивается по символам этой строки. Запись о назначении появляется до подтверждения чтения KV движком. Код cache_awareВнешняя ссылка, откроется в новой вкладке.

Какую строку получает политика

До обсуждения качества индекса нужно проверить его вход. В обычном HTTP-пути закреплённого коммита 1d10e71 роутер вызывает extract_text_for_routing(). Результат зависит от типа запроса:

  • /v1/chat/completions: непустой строковый session_params.session_id. Если такого значения нет или в нём только пробелы — пустая строка.
  • /v1/completions: значение, извлечённое из prompt.

Содержимое messages первый метод не извлекает. Поэтому два длинных chat-запроса с одинаковой инструкцией без session_id дадут политике две пустые строки. Это следует из реализаций извлечения входаВнешняя ссылка, откроется в новой вкладке и вызова в HTTP-роутереВнешняя ссылка, откроется в новой вкладке.

Название cache_aware здесь ещё не обещает сравнения начала chat-промпта. Сначала фиксируем установленную версию, endpoint и фактическую строку для политики. Совпадение идентификаторов сессии нельзя выдавать за совпадение токенов или чтение KV. Вывод ограничен этим HTTP-путём и коммитом; другие версии и режимы требуют своей проверки.

Что подтверждает запись в дереве

Между двумя обращениями worker может вытеснить блоки, о которых дерево ещё помнит. Возможна и обратная ситуация: запись из индекса уже удалена, а KV ещё доступен. evict_tenant_by_size очищает сведения в дереве роутера; команды удалить KV на сервере модели в этом методе нет. Реализация дереваВнешняя ссылка, откроется в новой вкладке.

Истории назначений может быть достаточно на предсказуемом потоке. Но процент совпавших символов нельзя переименовать в долю прочитанных токенов. Это разные единицы и разные наблюдения: в одном случае сопоставлен текст, в другом движок действительно использовал вычисленное состояние.

Load виден из процесса роутера

load() у worker читает локальный атомарный счётчик. Этот сигнал участвует в выборе: при дисбалансе или недостаточном совпадении политика может предпочесть worker с меньшим значением. Счётчик workerВнешняя ссылка, откроется в новой вкладке.

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

Лучшее совпадение может относиться к другой реплике

Дерево нашло лучший префикс у A, но совпадение оказалось ниже порога. Политика выбрала менее загруженную B. Если записать рядом только «совпадение» и «выбранный worker», легко приписать прогноз для A серверу B. В коде это разные шаги: результат поиска содержит владельца, а ветвь выбора может вернуть другого кандидата. Ветви выбораВнешняя ссылка, откроется в новой вкладке.

В данных аудита нужны оба идентификатора: worker, к которому относился прогноз, и выбранный worker. Если первого нет, прогноз для выбранной реплики остаётся неизвестным.

Dynamo разделяет события кэша и активную работу

Dynamo v1.4.0 описывает оценку, которая объединяет пересечение с кэшем, prefill-нагрузку, потенциальные decode-блоки и необязательную добавку за число активных запросов. У коэффициентов есть настройки. Более подробная модель стоимости даёт дополнительные возможности выбора, но сама по себе не доказывает лучший p99. Router DesignВнешняя ссылка, откроется в новой вкладке.

В режиме KV events индекс получает от workers события Stored и Removed. После запуска роутер восстанавливает сведения через локальные индексаторы workers; недоступный worker не может ответить на такой запрос. Есть и режим без событий: прогноз по собственным назначениям с истечением срока действия, TTL.

Активная работа учитывается отдельно. Новый процесс начинает без прежних сведений об активных блоках. Между несколькими роутерами можно включить replica sync, но это доставка без гарантии: ограниченная исходящая очередь может терять события. Рост выходных блоков остаётся локальным даже при синхронизации жизненного цикла запросов. Поэтому восстановленный индекс KV ещё не означает полную видимость нагрузки. Router Operations v1.4.0Внешняя ссылка, откроется в новой вкладке.

Поток событий нужно проверять на разрыве

Флага «события включены» мало. Подписчик должен замечать пропуски, понимать смену процесса-источника и показывать период восстановления. Иначе устаревший индекс выглядит просто как неудачный маршрут.

В ZMQ publisher vLLM v0.26.0 есть последовательные номера и ограниченный буфер replay. Он хранится в памяти; длинный разрыв может выйти за его пределы. Исходник этой версииВнешняя ссылка, откроется в новой вкладке. Наличие поля epoch здесь не предполагается: способ отличить новый процесс от старого потока нужно установить для конкретного producer и подписчика.

Я бы отдельно проверил короткий разрыв доставки, разрыв дольше доступной истории и перезапуск worker. Для каждого случая нужны источник, последовательность событий, локальное время получения и отметки начала и конца восстановления. Без согласованных часов между узлами нельзя вычислять точную задержку доставки простым вычитанием их времён.

Соединить решение с выполнением одного запроса

Для проверки полезна одна связанная запись, а не два похожих графика. Она должна показывать данные на момент выбора, сам выбор и результат исполнения.

Часть записиЧто сохранить
Запрос и попыткаRequest ID, attempt ID, модель, пул, версия роутера и endpoint
ПрогнозЕго источник, представление входа, единицы, время, worker-владелец совпадения
РешениеВыбранный worker, вид нагрузки, значение и область видимости
ИсполнениеРеальное переиспользование KV, ожидание, ошибки и повторы
КлиентПервый токен и успешное завершение ответа

Для streaming получение HTTP-ответа от upstream предшествует чтению всего потока. В проверенном HTTP-пути vllm-router тело передаётся дальше асинхронно. Поэтому тайминг получения ответа нельзя автоматически назвать клиентским TTFT, а успешный HTTP-статус — доказательством завершённой генерации. HTTP routerВнешняя ссылка, откроется в новой вкладке.

Сигнал для circuit breaker, механизма временного отключения проблемного worker, тоже вычисляется по HTTP-статусу. В этом пути record_outcome не проверяет, получил ли клиент завершающие события SSE. Поэтому success этого механизма нельзя считать числом завершённых генераций. Место регистрации результатаВнешняя ссылка, откроется в новой вкладке.

Для своего клиента я бы проверил два ответа: полный поток и поток с HTTP 200, который заканчивается без ожидаемых finish_reason и [DONE]. Нужно сохранить полученные события и причину окончания чтения, затем сопоставить их со счётчиком роутера. Конкретные признаки завершения задаёт контракт API; одна только закрывшаяся HTTP-связь его не подтверждает.

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

Что видно на трёх запросах

В сохранённом прогоне от 5 сентября 2026 годаВнешняя ссылка, откроется в новой вкладке неизменённый vllm-router работает с одним синтетическим HTTP-worker. Три одинаковых chat-запроса содержат непустые messages, но не содержат session_id. В журнале политики три раза записано matched_chars=0, input_chars=0.

Два потока содержат ожидаемые finish_reason и [DONE]. В третьем worker намеренно опускает оба завершающих события, после чего соединение закрывается. Все три ответа имеют HTTP 200, а счётчик circuit breaker показывает три success. Сырые события клиента, журнал роутера, метрики и описание сборки сохранены рядом с результатом.

У этого worker нет модели и KV-кэша. Вставленные задержки помогают различить получение заголовков, содержимого и конец потока; сравнивать по ним скорость роутеров нельзя. Пример также не восстанавливает связь между решением политики и конкретной попыткой запроса.

Практический следующий шаг

Можно начать с разбора на трёх запросах: он показывает, какие события получает клиент и что считает роутер. Описание стенда и сохранённые результаты доступны по ссылкам в разборе.

Для своего сценария сначала соберите связанные наблюдения. Анализатор маршрутизации помогает сопоставить решение роутера с результатом запроса. Понадобится экспорт в его формате JSONL: по одной записи на решение и результат попытки.

Затем сравните политики при одинаковой модели, нагрузке, workers и прогреве. Добавьте вытеснение KV, рестарт роутера и несколько путей входа по отдельности. Оценивать стоит и задержку восстановления, и результат для клиента. Достаточно точный простой индекс может оказаться разумным выбором; сложность оправдывается результатом в нужных условиях.

Если вопрос пока в выборе между сессией и общим началом, начните со sticky и prefix-aware routing, продолжения моего поста о локальности сессийВнешняя ссылка, откроется в новой вкладке. Механизм повторного использования собран в главе хэндбука. А когда найденные блоки лежат вне GPU, следующий вопрос — сколько стоит вернуть KV.

Связанные материалы