Чтобы получить чистую 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.
Как это работает
- Workers регистрируются через
--workersили/register. Каждый ZMQ listener переходит в состояниеpendingи в фоне начинает первую попытку subscribe/connect. - Задержка в 1 секунду смещает peer recovery за пределы окна slow-joiner, чтобы dump покрывал события, которые могли произойти до того, как новый listener безопасно начнет draining.
- Indexer получает
/dumpот первого доступного peer в--peers. - События dump применяются для заполнения radix tree.
- После завершения 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
Removeevents очистят их. - Missing blocks (partially added branches): live
Storedevents добавят их. - 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) |
--port | 8090 | Порт, на котором слушает HTTP server |
--threads | 4 | Число потоков indexer (1 = single-threaded, >1 = thread pool) |
--workers | (none) | Начальные workers в виде пар instance_id[:dp_rank]=zmq_address,... (по умолчанию dp_rank равен 0) |
--model-name | default | Имя модели для начальных --workers |
--tenant-id | default | ID 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_seconds | Histogram | endpoint | Время HTTP-запроса |
dynamo_kvindexer_requests_total | Counter | endpoint, method | Общее число HTTP-запросов |
dynamo_kvindexer_errors_total | Counter | endpoint, status_class | HTTP-ответы с ошибками (4xx/5xx) |
dynamo_kvindexer_models | Gauge | — | Число активных indexer по модели и tenant |
dynamo_kvindexer_workers | Gauge | — | Число зарегистрированных worker-экземпляров |
dynamo_kvindexer_listeners | Gauge | status | Число 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_id | Tenant, под которым зарегистрирован этот 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
См. также
- Mooncake KV Indexer RFC: Стандартизация community API для KV cache indexers
- Configuration and Tuning: Полная конфигурация и tuning KV router
- Router Design: Архитектура и режимы транспорта событий
- Standalone Router: Полноценный routing service (маршрутизирует запросы к workers)