Перейти к основному содержимому

Чтобы получить чистую Markdown-версию этой страницы, добавьте .md к этому URL. Полный индекс документации см. в https://docs.nvidia.com/dynamo/llms.txt. Полное содержимое, включая справочник API и примеры SDK, см. в https://docs.nvidia.com/dynamo/llms-full.txt.

Автономный KV Indexer

Обзор

Standalone KV indexer (python -m dynamo.indexer) — это легковесный сервис, который поддерживает radix tree кэшированных blocks и предоставляет HTTP endpoints для запросов и управления workers.

  • Он подписывается на ZMQ KV event streams напрямую от workers.
  • Он предоставляет HTTP API для регистрации, просмотра и overlap queries.
  • Он сохраняет P2P recovery и detection/replay gaps для standalone ZMQ path.
  • Он индексирует blocks уровней device, host-pinned и disk и возвращает совпадения по уровням в ответах /query.

Это отличается от Standalone Router, который представляет собой полноценный routing service. Standalone indexer предоставляет только слой индексации и запросов без routing logic.

Для Dynamo-native remote indexing используйте --serve-indexer на dynamo.frontend или dynamo.router и --use-remote-indexer на consumers. Этот service request plane повторно использует уже существующие механизмы приема событий и recovery у router; он не реализован в dynamo.indexer.

HTTP API следует соглашениям Mooncake KV Indexer RFC.

DYN_ROUTER_MIN_INITIAL_WORKERS здесь тоже учитывается. Если установить положительное целое значение, standalone indexer будет ждать, пока зарегистрируется столько же workers, прежде чем открыть startup-ready gate, что соответствует поведению запуска frontend/router.

Поддержка нескольких моделей и tenants

Indexer поддерживает по одному radix tree для каждой пары (model_name, tenant_id). Workers, зарегистрированные с разными model name или tenant ID, изолируются в отдельные indexers — запросы к одной модели/tenant никогда не возвращают scores от другой.

  • model_name (обязательно для /register и /query): идентифицирует модель. Workers, обслуживающие разные модели, получают отдельные radix tree.
  • tenant_id (необязательно, по умолчанию "default"): включает изоляцию между tenants внутри одной модели. Не указывайте его для single-tenant deployments.
  • block_size задается per-indexer: первый вызов /register для данной пары (model_name, tenant_id) задает block size. Последующие регистрации для той же пары должны использовать тот же block size, иначе запрос завершится ошибкой.

Совместимость

Standalone indexer работает с любым engine, который публикует KV cache events по ZMQ в ожидаемом msgpack format. Это включает обычные engines vLLM и SGLang, которые нативно выпускают ZMQ KV events — никакой Dynamo-specific wrapper не требуется.

События с тегами non-device storage tiers (host-pinned, disk, external) направляются в слот lower-tier, а не отбрасываются, и отображаются в ответах /query как reach cpu / disk.

Сценарии использования

  • Debugging: изучать состояние radix tree, чтобы проверить, какие blocks кэшированы на каких workers.
  • State verification: подтверждать, что представление indexer о KV cache state совпадает с внутренним состоянием router (используется в integration tests).
  • Custom routing: строить внешнюю routing logic, которая запрашивает у indexer overlap scores и сама принимает решения о выборе worker.
  • Monitoring: наблюдать распределение KV cache по workers без запуска полноценного router.
  • Standalone microservice: запускать indexer независимо от router/frontend, когда нужен прямой HTTP inspection и ingestion через ZMQ.

P2P-восстановление

Несколько реплик indexer могут подписываться на одни и те же ZMQ endpoints workers для fault tolerance. Когда реплика запускается (или перезапускается после сбоя), она загружает состояние своего radix tree от исправного peer перед обработкой live events.

Как это работает

  1. Workers регистрируются через --workers или /register. Каждый ZMQ listener переходит в состояние pending и в фоне начинает первую попытку subscribe/connect.
  2. Задержка в 1 секунду смещает peer recovery за пределы окна slow-joiner, чтобы dump покрывал события, которые могли произойти до того, как новый listener безопасно начнет draining.
  3. Indexer получает /dump от первого доступного peer в --peers.
  4. События dump применяются для заполнения radix tree.
  5. После завершения recovery открывается ready gate. Любой listener, у которого начальное ZMQ connect уже успешно завершилось, переходит в active и начинает draining buffered events; listeners для workers, которые все еще недоступны, остаются в pending, пока не подключатся.

Если ни один peer недоступен, indexer стартует с пустым состоянием.

Пример: конфигурация с двумя репликами

# Replica A (first instance, no peers)
python -m dynamo.indexer --port 8090 --block-size 16 \
--workers "1=tcp://worker1:5557,2=tcp://worker2:5558"

# Replica B (recovers from A on startup)
python -m dynamo.indexer --port 8091 --block-size 16 \
--workers "1=tcp://worker1:5557,2=tcp://worker2:5558" \
--peers "http://localhost:8090"

Обе реплики подписываются на одних и тех же workers. Replica B восстанавливает состояние tree Replica A при запуске, а затем обе независимо обрабатывают live ZMQ events дальше.

Согласованность

Dump представляет собой weakly consistent BFS snapshot radix tree — concurrent writes могут конкурировать с обходом. Это допустимо, потому что:

  • Stale blocks (partially removed branches): live Remove events очистят их.
  • Missing blocks (partially added branches): live Stored events добавят их.
  • Tree сходится к корректному состоянию после того, как live events догонят.

Управление peers

Peers можно зарегистрировать при запуске через --peers или динамически через HTTP API. Список peers используется только для recovery — peers не синхронизируют состояние в реальном времени.

Сборка

Сервис предоставляется через пакет Python bindings и запускается командой python -m dynamo.indexer после сборки bindings с помощью maturin. Флаги возможностей определяют, какие компоненты будут скомпилированы:

ФичаОписание
kv-indexerОсновной путь сервиса standalone indexer (python -m dynamo.indexer: HTTP API, ZMQ listeners, P2P recovery)
kv-indexer-metricsНеобязательный endpoint /metrics

Автономная сборка

cd lib/bindings/python && VIRTUAL_ENV=../../.venv ../../.venv/bin/maturin develop --uv --features kv-indexer

После установки запускайте сервис командой python -m dynamo.indexer.

Автономная сборка с метриками

cd lib/bindings/python && VIRTUAL_ENV=../../.venv ../../.venv/bin/maturin develop --uv --features kv-indexer,kv-indexer-metrics

Это сохраняет базовую сборку kv-indexer легкой, но при необходимости позволяет включить метрики Prometheus.

CLI

python -m dynamo.indexer --port 8090 [--threads 4] [--block-size 16 --model-name my-model --tenant-id default --workers "1=tcp://host:5557,2:1=tcp://host:5558"] [--peers "http://peer1:8090,http://peer2:8091"]
ФлагЗначение по умолчаниюОписание
--block-size(none)Размер KV cache block для начальных --workers (обязательно, если задан --workers)
--port8090Порт, на котором слушает HTTP server
--threads4Число потоков indexer (1 = single-threaded, >1 = thread pool)
--workers(none)Начальные workers в виде пар instance_id[:dp_rank]=zmq_address,... (по умолчанию dp_rank равен 0)
--model-namedefaultИмя модели для начальных --workers
--tenant-iddefaultID tenant для начальных --workers
--peers(none)URL peer indexer через запятую для P2P recovery при старте

Общий gate запуска

Установите DYN_ROUTER_MIN_INITIAL_WORKERS=<n>, чтобы standalone indexer, frontend push-router path и KV router config-ready gate продолжали работу только после регистрации как минимум <n> workers. Оставьте значение пустым или задайте 0, чтобы отключить ожидание запуска.

HTTP API

GET /health — Проверка доступности

Всегда возвращает 200 OK.

curl http://localhost:8090/health

GET /metrics — Метрики Prometheus

Возвращает метрики в формате Prometheus text exposition. Доступно, когда Python bindings собраны с feature kv-indexer-metrics.

curl http://localhost:8090/metrics
МетрикаТипLabelsОписание
dynamo_kvindexer_request_duration_secondsHistogramendpointВремя HTTP-запроса
dynamo_kvindexer_requests_totalCounterendpoint, methodОбщее число HTTP-запросов
dynamo_kvindexer_errors_totalCounterendpoint, status_classHTTP-ответы с ошибками (4xx/5xx)
dynamo_kvindexer_modelsGaugeЧисло активных indexer по модели и tenant
dynamo_kvindexer_workersGaugeЧисло зарегистрированных worker-экземпляров
dynamo_kvindexer_listenersGaugestatusЧисло ZMQ listeners по статусам (pending, active, paused, failed)

POST /register — Регистрация endpoint

Зарегистрировать ZMQ endpoint для экземпляра. Каждый вызов создает или переиспользует indexer для заданной пары (model_name, tenant_id). Регистрация не блокирует выполнение: если worker еще не поднят, listener принимается в состоянии pending и переходит в active, как только начальное ZMQ-соединение успешно установлено.

# Single model, default tenant
curl -X POST http://localhost:8090/register \
-H 'Content-Type: application/json' \
-d '{
"instance_id": 1,
"endpoint": "tcp://127.0.0.1:5557",
"model_name": "llama-3-8b",
"block_size": 16
}'

# With tenant isolation
curl -X POST http://localhost:8090/register \
-H 'Content-Type: application/json' \
-d '{
"instance_id": 2,
"endpoint": "tcp://127.0.0.1:5558",
"model_name": "llama-3-8b",
"tenant_id": "customer-a",
"block_size": 16,
"dp_rank": 0
}'
ПолеОбязательноПо умолчаниюОписание
instance_idдаИдентификатор worker-экземпляра
endpointдаZMQ PUB address для подписки
model_nameдаИмя модели (используется для выбора indexer)
block_sizeдаРазмер KV cache block (должен совпадать с engine)
tenant_idнет"default"Идентификатор tenant для изоляции
dp_rankнет0Ранг data parallel
replay_endpointнетZMQ ROUTER address для replay gaps (например, tcp://host:5560)
additional_saltнетSalt для tenant (Mooncake RFC #1403 additionalsalt, alias accepted). Сейчас разбирается для forward compatibility — engines сегодня применяют собственное salting.

POST /unregister — Удаление регистрации экземпляра

Удалить экземпляр. Если не указывать tenant_id, экземпляр удаляется из всех tenants для данной модели; если указать его, удаление затронет только indexer этого tenant.

# Remove from all tenants
curl -X POST http://localhost:8090/unregister \
-H 'Content-Type: application/json' \
-d '{"instance_id": 1, "model_name": "llama-3-8b"}'

# Remove from a specific tenant
curl -X POST http://localhost:8090/unregister \
-H 'Content-Type: application/json' \
-d '{"instance_id": 1, "model_name": "llama-3-8b", "tenant_id": "customer-a"}'

# Remove a specific dp_rank
curl -X POST http://localhost:8090/unregister \
-H 'Content-Type: application/json' \
-d '{"instance_id": 1, "model_name": "llama-3-8b", "tenant_id": "default", "dp_rank": 0}'
ПолеОбязательноПо умолчаниюОписание
instance_idдаЭкземпляр worker, который нужно удалить
model_nameдаИмя модели (идентифицирует indexer)
tenant_idнетИдентификатор tenant (не указывайте, чтобы удалить из всех tenants)
dp_rankнетКонкретный dp_rank, который нужно удалить (не указывайте, чтобы удалить все)

GET /workers — Список зарегистрированных экземпляров

Возвращает всех зарегистрированных workers, при желании отфильтрованных по модели и/или tenant.

Параметр запросаОписание
model_nameВозвращает только workers, зарегистрированных для этой модели. Не указывайте параметр, чтобы вернуть все модели.
tenant_idВозвращает только workers, зарегистрированных для этого tenant. Не указывайте параметр, чтобы вернуть все tenants.
# All workers
curl http://localhost:8090/workers

# Workers for a specific model
curl "http://localhost:8090/workers?model_name=llama-3-8b"

# Workers for a specific model and tenant
curl "http://localhost:8090/workers?model_name=llama-3-8b&tenant_id=customer-a"

Возвращает:

[
{
"instance_id": 1,
"source": "zmq",
"status": "active",
"model_name": "llama-3-8b",
"tenant_id": "default",
"block_size": 16,
"endpoints": {
"0": "tcp://127.0.0.1:5557",
"1": "tcp://127.0.0.1:5558"
},
"listeners": {
"0": {
"endpoint": "tcp://127.0.0.1:5557",
"status": "active"
},
"1": {
"endpoint": "tcp://127.0.0.1:5558",
"status": "active"
}
}
}
]
Поле ответаОписание
instance_idИдентификатор worker-экземпляра
sourceВсегда "zmq" для workers, управляемых ZMQ
statusСводный статус listener: failed > pending > active > paused
model_nameМодель, под которой зарегистрирован этот worker
tenant_idTenant, под которым зарегистрирован этот worker
block_sizeРазмер KV cache block для indexer этого worker с (model_name, tenant_id)
endpointsОтображение dp_rank → zmq_address
listenersДетали listener по каждому dp_rank; каждый элемент может содержать поле last_error, если последняя попытка запуска или цикла приема завершилась ошибкой

Фильтры независимы — если указать и model_name, и tenant_id, будут возвращены только workers, соответствующие обоим условиям. Если ни один worker не подходит под фильтр, возвращается пустой массив, а не 404.

POST /query — Запрос overlap по token IDs

По исходным token IDs вычисляет block hashes и возвращает overlap scores по каждому экземпляру (в matched tokens):

curl -X POST http://localhost:8090/query \
-H 'Content-Type: application/json' \
-d '{"token_ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], "model_name": "llama-3-8b"}'

Возвращает:

{
"scores": {"1": {"0": 32}, "2": {"1": 0}},
"frequencies": [1, 1],
"instances": {
"1": {
"longest_matched": 48,
"gpu": 32,
"dp": {"0": 32},
"cpu": 48,
"disk": 48
},
"2": {
"longest_matched": 0,
"gpu": 0,
"dp": {"1": 0},
"cpu": 0,
"disk": 0
}
}
}

Все счетчики указаны в matched tokens (число overlap блоков × block size).

  • scores / frequencies: устаревший overlap для device-tier. scores вложен сначала по instance_id, затем по dp_rank. Сохранен для обратной совместимости — существующим callers не нужно ничего менять.
  • instances: разбивка по экземплярам и уровням, согласованная с Mooncake RFC #1403. См. ниже Разбивка по tiers для каждого экземпляра.
ПолеОбязательноПо умолчаниюОписание
token_idsдаПоследовательность token для запроса
model_nameдаИмя модели (выбирает indexer)
tenant_idнет"default"Идентификатор tenant
lora_nameнетLoRA adapter (переопределяет lora_name уровня indexer для этого запроса)
cache_saltнетPer-request cache salt (Mooncake RFC #1403). Сейчас разбирается для forward compatibility — engines сегодня применяют собственное salting.

POST /query_by_hash — Запрос overlap по заранее вычисленным hashes

curl -X POST http://localhost:8090/query_by_hash \
-H 'Content-Type: application/json' \
-d '{"block_hashes": [123456, 789012], "model_name": "llama-3-8b"}'

Формат ответа такой же, как у /query, включая карту instances по экземплярам. Scores указаны в matched tokens.

ПолеОбязательноПо умолчаниюОписание
block_hashesдаМассив заранее вычисленных block hash
model_nameдаИмя модели (выбирает indexer)
tenant_idнет"default"Идентификатор tenant
cache_saltнетPer-request cache salt (Mooncake RFC #1403). Сейчас разбирается для forward compatibility — engines сегодня применяют собственное salting.

Разбивка по tiers для каждого экземпляра

Каждая запись в instances индексируется по instance_id (в виде строки) и показывает reach prefix по уровням device, host-pinned и disk storage:

ПолеОписание
gpuТокены, совпавшие на device tier (самый длинный prefix уровня device для любого dp_rank этого экземпляра).
dpЧисло совпадений на device tier по каждому dp_rank в виде {rank: tokens}.
cpuТокены, совпавшие через host-pinned tier. Накопительно относительно device tier — включает все, что уже посчитано в gpu, плюс любую host-pinned extension.
diskТокены, совпавшие через disk (или external) tier. Накопительно относительно обхода device → host-pinned.
longest_matchedМаксимум из gpu, cpu и disk — единая "best prefix length", по которой gateway может сортировать результат.

Счетчики по tiers являются накопительными, потому что обход lower-tier сообщает для каждого уровня его extension поверх предыдущего. В естественном pipeline offload (device → host → disk) это гарантирует gpu ≤ cpu ≤ disk для каждого экземпляра — lower tiers расширяют prefix уровня device, а не сокращают его.

Старые callers, которые используют только scores, продолжают работать: эти значения равны gpu для каждого dp_rank экземпляра.

GET /dump — Выгрузка всех событий radix tree

Возвращает все состояние radix tree как JSON object, ключом которого является model_name:tenant_id:

curl http://localhost:8090/dump

Возвращает:

{
"llama-3-8b:default": {
"block_size": 16,
"events": [<RouterEvent>, ...]
},
"mistral-7b:customer-a": {
"block_size": 16,
"events": [<RouterEvent>, ...]
}
}

Каждый indexer выгружается одновременно. Поле block_size позволяет восстанавливающимся peers создавать indexer с корректным размером blocks без необходимости задавать --block-size на каждой реплике.

POST /register_peer — Регистрация peer indexer

curl -X POST http://localhost:8090/register_peer \
-H 'Content-Type: application/json' \
-d '{"url": "http://peer:8091"}'

POST /deregister_peer — Удаление peer indexer

curl -X POST http://localhost:8090/deregister_peer \
-H 'Content-Type: application/json' \
-d '{"url": "http://peer:8091"}'

GET /peers — Список зарегистрированных peers

curl http://localhost:8090/peers

Возвращает:

["http://peer:8091"]

Обработка DP Rank

Когда worker регистрируется в standalone KV indexer (/register), он передает instance_id, ZMQ endpoint и необязательный dp_rank (по умолчанию 0). Сервис создает по одному ZMQ listener на каждую регистрацию.

Каждый входящий KvEventBatch может содержать необязательное поле data_parallel_rank. Если оно присутствует, оно переопределяет статически зарегистрированный dp_rank для этого batch. Это позволяет одному ZMQ-порту мультиплексировать events от нескольких DP rank.

Caveat: registry отслеживает только dp_rank, зарегистрированные явными вызовами /register. Если engine динамически отправляет batches с dp_rank, который никогда не был зарегистрирован, indexer корректно сохранит эти blocks (под динамическим ключом WorkerWithDpRank), но удаление по конкретному dp_rank (/unregister с dp_rank) их не найдет. Полное удаление экземпляра (/unregister без dp_rank) по-прежнему очищает все dp_rank для заданного worker_id в tree через remove_worker.

Обнаружение и replay gaps

ZMQ PUB/SUB является lossy — сообщения могут теряться при backpressure или кратковременных disconnects. Indexer обнаруживает gaps, отслеживая sequence number каждого batch: если seq > last_seq + 1, gap считается обнаруженным.

Когда в /register указан replay_endpoint, indexer подключает DEALER socket к ROUTER socket engine и запрашивает отсутствующие batches по sequence number. Engine отправляет обратно buffered пары (seq, payload) из своего ring buffer, пока не встретится sentinel с пустым payload.

Если replay_endpoint не задан, gaps только записываются в warnings и не восстанавливаются.

Счетчик sequence (last_seq) сохраняется между циклами unregister/register, поэтому повторная регистрация worker после gap вызовет replay при первом batch, полученном новым listener.

Ограничения

  • Режим standalone только ZMQ: Workers должны публиковать KV events через ZMQ PUB sockets.
  • Без routing logic: Indexer только поддерживает radix tree и отвечает на запросы. Он не отслеживает active blocks, не управляет жизненным циклом request и не выполняет выбор worker.

Архитектура

Автономный режим

graph TD
subgraph Workers
W1[Worker 1<br/>ZMQ PUB]
W2[Worker 2<br/>ZMQ PUB]
end

subgraph "Standalone Indexer (HTTP)"
REG[Worker Registry]
ZMQ[ZMQ SUB Listeners]
IDX["Indexer Map<br/>(model, tenant) → Radix Tree"]
HTTP[HTTP API<br/>/query /dump /register /health]
end

CLIENT[External Client]

W1 -->|ZMQ events| ZMQ
W2 -->|ZMQ events| ZMQ
CLIENT -->|POST /register| REG
REG -->|spawn listeners| ZMQ
ZMQ -->|apply events| IDX
CLIENT -->|POST /query, GET /dump| HTTP
HTTP -->|query| IDX

style W1 fill:#f3e5f5,stroke:#333,color:#333
style W2 fill:#f3e5f5,stroke:#333,color:#333
style IDX fill:#2e8b57,stroke:#333,color:#fff
style ZMQ fill:#2e8b57,stroke:#333,color:#fff
style REG fill:#2e8b57,stroke:#333,color:#fff
style HTTP fill:#2e8b57,stroke:#333,color:#fff
style CLIENT fill:#fff3e0,stroke:#333,color:#333

Поток P2P-восстановления

sequenceDiagram
participant B as Replica B (new)
participant A as Replica A (healthy)
participant W as Workers (ZMQ PUB)

B->>W: Connect ZMQ SUB sockets
Note over B,W: 1s delay for peer tree to advance past connection point
B->>A: GET /dump
A-->>B: Radix tree snapshot + block sizes
Note over B: Apply dump events
Note over B: Unblock ZMQ listeners
B->>W: Start draining buffered events
Note over B: Ready to serve queries

См. также