Files
2026-07-13 12:24:33 +08:00

1677 lines
49 KiB
ReStructuredText

HTTP API
========
When the MP server is started via ``lmcache server`` (the recommended entry
point), a FastAPI-based HTTP frontend is exposed alongside the ZMQ socket
used by vLLM. This HTTP API is intended for operators, orchestrators
(e.g. Kubernetes), and debugging tools — it is **not** on the inference
data path.
Where the routes come from
--------------------------
Routes are assembled from three sources, all merged into one FastAPI app by
:class:`~lmcache.v1.multiprocess.http_api_registry.HTTPAPIRegistry` at startup:
- **MP-native routes** — any module named ``*_api.py`` under
``lmcache/v1/multiprocess/http_apis/`` that exposes a module-level
``router`` (a :class:`fastapi.APIRouter`) is auto-discovered. This covers
the operational surface: status, cache control, L2 management, quota, and
runtime reconfiguration.
- **Shared "common" routes**
``lmcache/v1/multiprocess/http_apis/common_api.py`` aggregates every
compatible router under ``lmcache/v1/internal_api_server/common/`` (skipping
any module listed in ``_MP_INCOMPATIBLE_MODULES``, currently empty) and
forwards them to the auto-discovery pipeline. These are the cross-server
diagnostics shared with the vLLM-embedded API server (``/env``,
``/loglevel``, ``/metrics``, ``/threads``, ``/periodic-threads*``,
``/run_script``). Adding a new compatible module under
``internal_api_server/common`` requires no wiring changes on the MP side.
- **Re-exported version routes** — the basic-info group
``lmcache/v1/multiprocess/http_apis/info_api.py`` includes the router
from ``lmcache/v1/internal_api_server/vllm/version_api.py``, exposing
``/version``, ``/lmc_version``, and ``/commit_id`` alongside ``/``,
``/healthcheck`` and ``/status``.
.. contents::
:local:
:depth: 2
Server Configuration
--------------------
.. list-table::
:header-rows: 1
:widths: 30 15 55
* - Argument
- Default
- Description
* - ``--http-host``
- ``0.0.0.0``
- Host to bind the HTTP server.
* - ``--http-port``
- ``8080``
- Port to bind the HTTP server.
Example:
.. code-block:: bash
lmcache server \
--l1-size-gb 100 --eviction-policy LRU \
--http-host 0.0.0.0 --http-port 8080
All examples below assume the server is reachable at
``http://localhost:8080``.
Endpoint Overview
-----------------
The routes are grouped by purpose below. The operational surface (health,
status, cache and storage control) lives at top-level paths; routes inherited
from the shared ``internal_api_server`` package keep their original paths for
compatibility with the vLLM-embedded API server.
.. note::
Several handlers report failure in the response **body** rather than via a
non-200 status code (e.g. ``DELETE /cache/objects`` returns ``200`` with ``ok=false``,
and ``/periodic-threads-health`` returns ``200`` with ``healthy=false``).
The error-field name is also not uniform: ``/healthcheck`` and
``/cache/clear`` use ``reason`` on failure, while ``/status``, ``/config``,
and ``/cache/checksums`` use ``error``. Per-endpoint details below are
authoritative.
**Liveness and health**
.. list-table::
:header-rows: 1
:widths: 10 35 55
* - Method
- Path
- Purpose
* - GET
- ``/``
- Static liveness ping (does not touch the engine).
* - GET
- ``/healthcheck``
- K8s liveness/readiness probe; ``503`` until the engine is initialized.
**Inspection and status**
.. list-table::
:header-rows: 1
:widths: 10 35 55
* - Method
- Path
- Purpose
* - GET
- ``/status``
- Detailed engine snapshot (L1, L2, registered contexts, sessions,
prefetch jobs) for inspection and debugging.
* - GET
- ``/config``
- Dump the merged server configuration objects (``mp``,
``storage_manager``, ``observability``).
* - GET
- ``/config/adapters``
- Enumerate the live cache adapters (``type_name``, ``tier``, ``primary``,
``reconfigurable``). Supersedes ``/reconfigure/backends``.
* - GET
- ``/version``
- Combined version string (``"<version>-<commit_id>"``).
* - GET
- ``/lmc_version``
- LMCache package version string.
* - GET
- ``/commit_id``
- Build commit id.
**Cache management**
.. list-table::
:header-rows: 1
:widths: 10 38 52
* - Method
- Path
- Purpose
* - GET
- ``/cache/objects``
- Paginate objects resident in a tier/adapter (query: ``tier``,
``adapter``, ``model_name``, ``page_size``, ``page_token``).
* - DELETE
- ``/cache/objects``
- Delete a caller-supplied list of object keys (body: ``tier``,
``adapter``, ``keys``).
* - POST
- ``/cache/prefetches``
- Warm a node's L1 by loading a token sequence from L2 ahead of traffic;
returns a ``request_id``.
* - GET
- ``/cache/prefetches/{request_id}``
- Poll a submitted warm prefetch (``pending`` / ``completed``).
* - POST
- ``/cache/clear``
- Force-clear a tier's resident cache (body: ``tier`` = ``l1``, ``force``).
* - POST
- ``/cache/checksums``
- Compute MD5 checksums over KV cache blocks (diagnostics / round-trip
integrity checks).
**Quota management**
.. list-table::
:header-rows: 1
:widths: 10 35 55
* - Method
- Path
- Purpose
* - GET
- ``/quota``
- List every registered ``cache_salt`` quota with live usage.
* - PUT
- ``/quota/{cache_salt}``
- Set or update the quota (in GB) for a ``cache_salt``.
* - GET
- ``/quota/{cache_salt}``
- Read the quota and live usage for a single ``cache_salt``.
* - DELETE
- ``/quota/{cache_salt}``
- Remove a ``cache_salt``'s quota entry (its data is evicted next cycle).
**Runtime L2 reconfiguration**
.. list-table::
:header-rows: 1
:widths: 10 35 55
* - Method
- Path
- Purpose
* - GET
- ``/reconfigure/{backend}/status``
- Report runtime-manageable L2 adapters for one backend type. (To discover
reconfigurable backends, use ``GET /config/adapters`` and read the
``reconfigurable`` flag.)
* - POST
- ``/reconfigure/{backend}/{operation}``
- Apply one runtime reconfiguration operation to a backend adapter.
**Observability**
.. list-table::
:header-rows: 1
:widths: 10 35 55
* - Method
- Path
- Purpose
* - GET
- ``/metrics``
- Prometheus exposition format.
* - POST
- ``/metrics/reset``
- Reset all observability metrics to their initial state.
**Diagnostics and debugging**
.. list-table::
:header-rows: 1
:widths: 10 35 55
* - Method
- Path
- Purpose
* - GET
- ``/loglevel``
- List or inspect logger levels; also accepts ``level`` to mutate one.
* - GET
- ``/threads``
- Enumerate active Python threads and their stack traces.
* - GET
- ``/periodic-threads``
- List registered periodic threads with summary counts.
* - GET
- ``/periodic-threads/{thread_name}``
- Detailed status for a single periodic thread.
* - GET
- ``/periodic-threads-health``
- Quick health check for critical/high-level periodic threads.
* - GET
- ``/env``
- Dump process environment variables (JSON body, ``text/plain``).
* - POST
- ``/run_script``
- Execute an uploaded Python script in a restricted sandbox.
Liveness and Health
-------------------
``GET /``
~~~~~~~~~
Basic liveness check. Returns a static payload indicating the HTTP server
is running; it does **not** touch the cache engine. Use ``/healthcheck``
instead for probes that also verify the engine is initialized.
**Response** (``200 OK``):
.. code-block:: json
{
"status": "ok",
"service": "LMCache HTTP API"
}
**HTTP status codes:**
- ``200``: server is alive (unconditional; does not touch the engine).
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/
``GET /healthcheck``
~~~~~~~~~~~~~~~~~~~~
Health check endpoint suitable for Kubernetes liveness and readiness
probes. A ``200`` response means the HTTP server is alive **and** the MP
cache engine object is wired onto ``app.state``. A ``503`` response
indicates the engine is not yet present (still initializing, or failed to
initialize). The check verifies that the engine attribute is set; it does
not call into the engine to assert deeper liveness.
**Response** (``200 OK``):
.. code-block:: json
{
"status": "healthy"
}
**Response** (``503 Service Unavailable``):
.. code-block:: json
{
"status": "unhealthy",
"reason": "engine not initialized"
}
**HTTP status codes:**
- ``200``: engine is wired onto ``app.state`` (healthy).
- ``503``: engine not initialized (still starting up, or failed to initialize).
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/healthcheck
**Kubernetes probe snippet:**
.. code-block:: yaml
livenessProbe:
httpGet:
path: /healthcheck
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthcheck
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Inspection and Status
---------------------
``GET /status``
~~~~~~~~~~~~~~~
Returns a detailed snapshot of the MP engine's internal state. The payload is
assembled by ``MPCacheServer.report_status()``: a fixed set of engine-level
fields, the full storage-manager status, plus whatever keys each loaded module
contributes (so the exact key set depends on which modules are active —
``registered_gpu_ids`` / ``cache_context_meta`` come from the transfer module,
``active_prefetch_jobs`` from the lookup module, and blend modes add their own
fields). Intended for operators and debugging, not for monitoring (use
Prometheus metrics for time-series data — see :doc:`observability/index`).
**Response** (``200 OK``):
.. code-block:: json
{
"is_healthy": true,
"engine_type": "MPCacheServer",
"chunk_size": 256,
"hash_algorithm": "builtin-hash",
"active_sessions": 2,
"registered_gpu_ids": [0, 1],
"cache_context_meta": {
"0": {
"model_name": "meta-llama/Llama-3.1-8B-Instruct",
"world_size": 1,
"kv_cache_layout": {
"num_layers": 32,
"num_blocks": 12345,
"cache_size_per_token": 131072,
"kernel_groups": [
{
"kernel_group_idx": 0,
"engine_group_idx": 0,
"object_group_idx": 0,
"num_layers": 32,
"layer_indices": [0, 1, "..."],
"tokens_per_block": 16,
"slots_per_block": 16,
"dtype": "torch.bfloat16",
"engine_kv_concrete_shape": "...",
"is_mla": false,
"engine_kv_format": "...",
"engine_kv_shape": "...",
"attention_backend": "..."
}
]
}
}
},
"active_prefetch_jobs": 0,
"storage_manager": {
"is_healthy": true,
"...": "backend-specific fields"
}
}
**Response** (``503 Service Unavailable``) when the engine has not yet
been initialized:
.. code-block:: json
{
"error": "engine not initialized"
}
**HTTP status codes:**
- ``200``: status snapshot returned.
- ``503``: engine not yet initialized on ``app.state``.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/status | jq
``GET /config``
~~~~~~~~~~~~~~~
Returns every server-side configuration object registered on
``app.state.configs`` (typically ``mp``, ``storage_manager`` and
``observability``) as a single indented JSON document. Dataclasses are
serialized via ``safe_asdict``; other values go through ``make_json_safe``.
Useful for confirming what the process actually loaded — including
environment overrides — without restarting.
**Response** (``200 OK``):
.. code-block:: json
{
"mp": {
"http_host": "0.0.0.0",
"http_port": 8080,
"...": "..."
},
"storage_manager": {
"...": "..."
},
"observability": {
"...": "..."
}
}
**Response** (``503 Service Unavailable``) when configs are not wired
onto ``app.state`` yet:
.. code-block:: json
{
"error": "configs not initialized"
}
**HTTP status codes:**
- ``200``: configuration document returned.
- ``503``: configs not yet wired onto ``app.state``.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/config | jq
``GET /config/adapters``
~~~~~~~~~~~~~~~~~~~~~~~~~
Enumerate every L2 adapter the engine has loaded, in configuration
order. Which storage backends are loaded is a configuration-inspection
concern, so this lives in the config group rather than under ``/cache``.
This is the single live adapter listing; it supersedes the old
``GET /reconfigure/backends`` (the reconfigurable backends are the
``type_name`` values whose ``reconfigurable`` flag is ``true``).
**Response** (``200 OK``):
.. code-block:: json
{
"adapters": [
{"index": 0, "type_name": "S3L2Adapter", "tier": "l2", "primary": true, "reconfigurable": false},
{"index": 1, "type_name": "dax", "tier": "l2", "primary": false, "reconfigurable": true}
]
}
``primary`` is ``true`` only on the first entry. ``reconfigurable`` is
``true`` for adapters that accept ``/reconfigure`` operations — pass that
adapter's ``type_name`` as the ``{backend}`` path parameter to
``GET /reconfigure/{backend}/status`` and the reconfigure operations. An
engine that has no L2 backends returns ``{"adapters": []}`` (still ``200``
the engine is initialized, it just has no L2 storage).
**HTTP status codes:**
- ``200``: success (including the no-adapters case).
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/config/adapters | jq
``GET /version``, ``GET /lmc_version``, ``GET /commit_id``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Version descriptors. Each returns a bare JSON **string** (not an object):
- ``GET /version`` — the combined descriptor from
``lmcache.utils.get_version()``, formatted ``"<version>-<commit_id>"``
(e.g. ``"0.3.1-ca79ea33"``). On a source checkout without build-time
version metadata, each missing component falls back to the literal
``"NA"`` (so a metadata-less build returns ``"NA-NA"``).
- ``GET /lmc_version`` — the raw package version string
(``lmcache.utils.VERSION``); empty string ``""`` when the generated
``lmcache._version`` module is absent.
- ``GET /commit_id`` — the git commit id baked into the build
(``lmcache.utils.COMMIT_ID``); empty string ``""`` when unavailable.
All three are unconditional ``200 OK``.
**Response** (``200 OK``):
.. code-block:: json
"0.3.1-ca79ea33"
**HTTP status codes:**
- ``200``: version string returned (unconditional for all three routes).
**Examples:**
.. code-block:: bash
curl -s http://localhost:8080/version
curl -s http://localhost:8080/lmc_version
curl -s http://localhost:8080/commit_id
Cache Management
----------------
``POST /cache/clear``
~~~~~~~~~~~~~~~~~~~~~~
Force-clears **all** KV cache data currently held in a tier (today ``l1``).
.. warning::
This endpoint is destructive and bypasses read/write locks. In-flight
store or prefetch operations may be corrupted. Use only when the
server is idle, or when recovering from a known-bad cache state.
**Request body:** optional -- an absent (or empty) body uses the defaults below.
.. list-table::
:header-rows: 1
:widths: 18 14 68
* - Field
- Type
- Description
* - ``tier``
- string
- Optional (default ``l1``). Tier to clear; today only ``l1`` is
supported. Any other value returns ``400``.
* - ``force``
- bool
- Optional (default ``true``). Currently accepted but **not honored** --
the clear always force-clears (active locks are ignored) regardless of
this value.
**Response** (``200 OK``):
.. code-block:: json
{"status": "ok", "cleared": {"tier": "l1"}}
**HTTP status codes:**
- ``200``: tier cleared.
- ``400``: unsupported ``tier`` (anything other than ``l1``).
- ``503``: server not initialized (``{"detail": "server not initialized"}``).
**Example:**
.. code-block:: bash
# the body is optional; this clears the default tier (l1)
curl -s -X POST http://localhost:8080/cache/clear
``POST /cache/checksums``
~~~~~~~~~~~~~~~~~~~~~~~~~~
Compute MD5 checksums over the engine KV cache, grouped ``chunk_size`` blocks
per hashed chunk. MP mode addresses KV storage by block IDs natively (the
same units used by ``STORE`` / ``RETRIEVE``), so the endpoint is fully
block-centric. Intended for diagnostics and round-trip integrity checks from
``lmcache bench server`` -- not for the inference data path.
**Request body:**
.. list-table::
:header-rows: 1
:widths: 25 15 60
* - Field
- Required
- Description
* - ``block_ids``
- yes
- Engine block IDs as a JSON list of ints, e.g. ``[0, 1, 2, 3]``.
* - ``chunk_size``
- yes
- Positive integer — number of blocks per hashed chunk.
* - ``instance_id``
- no (default ``0``)
- Registered KV context ID on the engine.
* - ``layerwise``
- no (default ``false``)
- If ``true``, return per-layer checksums keyed by ``"layer_<idx>"``;
otherwise a single aggregated digest per chunk over all layers.
**Response** (``200 OK``):
.. code-block:: json
{
"status": "success",
"chunk_size": 2,
"num_chunks": 2,
"chunk_checksums": ["<md5>", "<md5>"],
"layerwise": false,
"block_id_ranges": "0,[2,5],8"
}
When ``layerwise=true``, ``chunk_checksums`` is a dict keyed by
``"layer_<idx>"`` whose values are per-layer lists.
**HTTP status codes:**
- ``200``: success.
- ``400``: ``block_ids`` empty, or ``chunk_size`` missing or non-positive.
- ``404``: ``instance_id`` not registered, or the registered KV tensors
are empty.
- ``501``: engine has no ``cache_contexts``, or the KV format is not
supported by this endpoint (page-buffer-fused and cross-layer layouts
are declined until a real need appears).
- ``503``: engine not yet initialized on ``app.state``.
**Example:**
.. code-block:: bash
curl -s -X POST http://localhost:8080/cache/checksums \
-H 'Content-Type: application/json' \
-d '{"block_ids": [0, 1, 2, 3], "chunk_size": 2}'
.. _mp-http-l2-keys-api:
Cache Objects And Prefetch
--------------------------
Two endpoints — ``DELETE /cache/objects`` and ``GET /cache/objects`` — let
operators purge keys from a configured cache backend and enumerate what is
currently resident. (To enumerate the configured backends themselves, use
``GET /config/adapters`` in the config group.)
A further pair — ``POST /cache/prefetches`` and
``GET /cache/prefetches/{request_id}`` — lets an operator (or the
coordinator) **pre-warm** a node's L1 from L2 ahead of traffic and poll
the load to completion; they are documented at the end of this section.
The coordinator exposes an ``instance_id``-routed variant of both — see
:doc:`coordinator`.
Both object endpoints take an optional ``adapter`` selector — a query
parameter on ``GET /cache/objects`` (``?adapter=<type_name>``) and a body
field on ``DELETE /cache/objects``. Omit it to target the **primary**
(first-configured) adapter. When multiple adapters share a ``type_name``,
the first match wins. Use ``GET /config/adapters`` to learn the valid
selectors.
Both are intended for operator / admin workflows ("purge this
user's keys", "show me what's resident", "garbage-collect orphans
after a rename"). They are **not** on the inference data path.
L1 is intentionally not touched. Keys deleted from L2 may still return
from L1 until the L1 eviction controller expires them naturally;
callers that need an L1+L2 purge should layer their own L1
invalidation or wait for natural L1 eviction.
The coordinator's eviction loop uses ``DELETE /cache/objects`` automatically (see
:doc:`coordinator` — "L2 usage tracking and eviction"); the
``GET /cache/objects`` endpoint also powers the coordinator's startup
resync. Manual ``curl`` usage is reserved for ad-hoc operator
actions and debugging.
``DELETE /cache/objects``
~~~~~~~~~~~~~~~~~~~~~~~~~~
Delete a caller-supplied list of keys from one tier/adapter.
Idempotent: keys absent from the adapter are skipped silently; keys
currently locked by in-flight store/load tasks are skipped so the
delete never corrupts an active transfer. The blocking adapter call is
run off the event loop.
Per-key successful deletions fire ``on_l2_keys_deleted`` on the
adapter's listeners — when the coordinator is wired (see
``--coordinator-l2-event-reporting``), the deletions show up at the
coordinator's ``POST /quota/events`` as ``"type": "delete"`` events. The
coordinator's eviction + usage trackers learn about the deletion from
that event flow, not from the response of this call.
**Request body:**
.. list-table::
:header-rows: 1
:widths: 18 14 68
* - Field
- Type
- Description
* - ``keys``
- list[EncodedObjectKey]
- Required. The object keys to delete (schema below). The batch is
capped at ``10000`` keys per request.
* - ``tier``
- string
- Optional (default ``l2``). The only supported value.
* - ``adapter``
- string
- Optional (default: primary, first-configured adapter). The
``type_name`` of the target adapter (see ``GET /config/adapters``).
Each ``EncodedObjectKey`` is
.. code-block:: json
{
"chunk_hash_hex": "abc123...",
"model_name": "meta-llama/Llama-3-8B",
"kv_rank": 0,
"object_group_id": 0,
"cache_salt": "user-a"
}
``object_group_id`` (default ``0``) and ``cache_salt`` (default ``""``)
are optional for backward compatibility with older wire payloads.
**Response** (``200 OK``):
.. code-block:: json
{
"requested": 2,
"adapter": "S3L2Adapter",
"ok": true
}
On adapter-level failure the response is still ``200`` with
``ok=false`` and an ``error`` field carrying the reason.
**HTTP status codes:**
- ``200``: request reached the adapter (check ``ok`` for outcome).
- ``400``: batch exceeds the limit, or a key payload violates an
``ObjectKey`` invariant (bad hex, ``@`` in ``model_name``, forbidden
``cache_salt`` character).
- ``404``: ``adapter`` (body) does not match any configured adapter.
- ``422``: Pydantic-level body-shape failure (missing ``keys``,
wrong field types).
- ``503``: engine not initialized, or no L2 adapters configured.
**Example:**
.. code-block:: bash
curl -s -X DELETE http://localhost:8080/cache/objects \
-H 'Content-Type: application/json' \
-d '{
"keys": [
{"chunk_hash_hex": "aa", "model_name": "m",
"kv_rank": 0, "object_group_id": 0, "cache_salt": "user-a"}
]
}'
``GET /cache/objects``
~~~~~~~~~~~~~~~~~~~~~~
Paginate keys currently resident in one L2 adapter.
**Query parameters:**
.. list-table::
:header-rows: 1
:widths: 22 13 65
* - Name
- Default
- Description
* - ``adapter``
- primary
- ``type_name`` of the target adapter (see ``GET /config/adapters``).
Omit to target the primary (first-configured) adapter. First
match wins when multiple adapters share a ``type_name``.
* - ``model_name``
- none
- Restrict the result to keys whose ``model_name`` matches.
* - ``page_size``
- ``500``
- Max entries per page. Must be in ``[1, 5000]``; an out-of-range
value is rejected with ``422`` (it is not silently clamped).
* - ``page_token``
- none
- Opaque cursor from the previous page's ``next_page_token``.
Omit on the first call; pass back verbatim on subsequent calls.
The page token is private to the adapter; do not parse or modify it.
Adapters that support listing (currently only the S3 adapter via
``ListObjectsV2``) guarantee best-effort consistency, not snapshot
isolation — concurrent stores or deletes during a paginated walk may
cause keys to appear, disappear, or shift between pages.
**Response** (``200 OK``):
.. code-block:: json
{
"adapter": "S3L2Adapter",
"entries": [
{
"key": {
"chunk_hash_hex": "abc123",
"model_name": "meta-llama/Llama-3-8B",
"kv_rank": 0,
"object_group_id": 0,
"cache_salt": "user-a"
},
"size_bytes": 4194304
}
],
"next_page_token": "opaque-cursor-string"
}
``next_page_token`` is ``null`` when the listing is exhausted.
**HTTP status codes:**
- ``200``: success.
- ``400``: malformed ``page_token`` (adapter-level).
- ``404``: ``?adapter=<name>`` does not match any configured adapter.
- ``422``: ``page_size`` outside ``[1, 5000]``.
- ``501``: selected adapter does not implement listing. In v1 only
``S3L2Adapter`` does; adapters wrapped by ``SerdeL2AdapterWrapper``
inherit the wrapped adapter's behavior.
- ``503``: engine not initialized, or no L2 adapters configured.
**Example:** paginate every key for a model.
.. code-block:: bash
next=""
while :; do
page=$(curl -s "http://localhost:8080/cache/objects?model_name=meta-llama/Llama-3-8B&page_size=500&page_token=$next")
echo "$page" | jq '.entries[]'
next=$(echo "$page" | jq -r '.next_page_token // empty')
[ -z "$next" ] && break
done
``POST /cache/prefetches``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Warm one node's L1 by loading a token sequence's chunks from L2 **ahead** of
the requests that will use them, so the first request hits L1 instead of
paying the L2 fetch inline. Useful when a workload is about to be routed to a
node (a traffic shift, a hot shared system prompt).
The caller describes content by **token ids**, never by internal cache keys (a
key is a content hash plus a per-rank layout bitmap, which callers cannot
construct). The server hashes the tokens, expands each chunk across the node's
ranks, and submits a *warm* prefetch: loaded chunks are **retained**
(permanent) and left **unlocked** — there is no downstream reader to pin them
for, so a later real lookup takes its own lock. The call returns immediately;
the load runs in the storage manager's own thread. It coalesces across all
configured L2 adapters, so there is no ``?adapter=`` selector.
**Request body:**
.. list-table::
:header-rows: 1
:widths: 20 12 68
* - Field
- Type
- Description
* - ``model_name``
- string
- The served model id, exactly as registered (e.g. ``Qwen/Qwen3-8B``).
* - ``world_size``
- int
- The value vLLM registered for the node's KV layout and rank fan-out
(``1`` for a single-GPU, TP=1 deployment).
* - ``token_ids``
- list[int]
- The prompt token ids. Must use the same tokenizer / special-token
settings as the store, and contain at least one full ``chunk_size`` of
tokens — only complete chunks are warmed.
* - ``cache_salt``
- string
- Per-tenant key isolation; must match the store (default ``""``).
**Response** (``202 Accepted``):
.. code-block:: json
{"request_id": "abc123", "chunks": 12, "status": "submitted"}
When the sequence is shorter than one chunk, nothing is submitted and there is
no ``request_id`` to poll:
.. code-block:: json
{"chunks": 0, "status": "noop"}
**HTTP status codes:**
- ``202``: submitted (or a ``noop`` as above).
- ``400``: ``token_ids`` exceeds the per-request cap, or ``cache_salt``
violates its invariants.
- ``409``: no layout registered for ``(model_name, world_size)`` — the model
has not allocated KV cache on this node yet (start vLLM first).
- ``422``: request body fails field-level validation.
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s -X POST http://localhost:8080/cache/prefetches \
-H 'Content-Type: application/json' \
-d '{"model_name": "Qwen/Qwen3-8B", "world_size": 1,
"token_ids": [101, 102, 103], "cache_salt": "user-a"}'
# -> {"request_id": "abc123", "chunks": 1, "status": "submitted"}
``GET /cache/prefetches/{request_id}``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Poll a submitted warm prefetch. The warm holds no lock, so the poll only
reports progress; the first poll that observes completion drops the job
(exactly-once), so a later poll for the same id returns ``404``.
**Response** (``200 OK``) while the load runs:
.. code-block:: json
{"status": "pending"}
…and once complete:
.. code-block:: json
{"status": "completed", "found_keys": 12, "total_keys": 12}
``found_keys`` / ``total_keys`` count only chunks **loaded from L2** by this
request; chunks already resident in L1 are skipped at ``reserve_write`` and not
counted, so a partially-resident warm undercounts by the resident chunk count
(a cold request loads and counts everything). The warm uses the gap-tolerant
``SPARSE`` trim policy, so an already-resident chunk does not stop the rest from
loading — it loads every not-yet-resident chunk and reports that count. Not
counting the resident chunks is deliberate: an already-present entry may be a
transient temporary from another lookup, so claiming it as warmed could mislead.
**HTTP status codes:**
- ``200``: status reported (``pending`` or ``completed``).
- ``404``: unknown ``request_id`` — already completed-and-consumed, or never
submitted.
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/cache/prefetches/abc123
# -> {"status": "completed", "found_keys": 1, "total_keys": 1}
.. _mp-http-quota-api:
Quota Management
----------------
These endpoints manage the per-``cache_salt`` storage budgets consumed by
the ``IsolatedLRU`` eviction policy (selected via
``--eviction-policy IsolatedLRU``). Quotas are **soft**: setting a limit
does not reject writes — any over-budget ``cache_salt`` is evicted at
the next eviction cycle (~1 s).
A ``cache_salt`` with no registered quota has an effective limit of
``0`` bytes, so its data is cleared next cycle (allowlist semantics).
These endpoints are no-ops on engines that did not start with
``--eviction-policy IsolatedLRU``: the ``QuotaManager`` is still
present, but the LRU policy ignores the registered quotas.
**URL escaping for the empty salt.** ``cache_salt=""`` (un-salted /
anonymous traffic) cannot appear in a URL path parameter, so the API
accepts the sentinel ``_default`` in its place. ``PUT /quota/_default``
sets the quota for ``cache_salt=""``, and ``_default`` is echoed back in
responses for the empty salt. A user that legitimately stores data with
``cache_salt="_default"`` cannot be managed via this HTTP API distinctly
from anonymous traffic — both map to the same path parameter; pick any
other value (e.g. ``"default"``) to disambiguate.
``PUT /quota/{cache_salt}``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create or update a quota.
**Path parameters:** ``cache_salt`` — tenant identifier (use ``_default``
for the empty salt; see the section intro).
**Request body:**
.. list-table::
:header-rows: 1
:widths: 18 14 68
* - Field
- Type
- Description
* - ``limit_gb``
- float
- Required. The quota in GB. Must be finite and non-negative.
**Response** (``200 OK``):
.. code-block:: json
{"cache_salt": "alice", "limit_gb": 10.0, "status": "ok"}
**HTTP status codes:**
- ``200``: quota set or updated.
- ``400``: malformed JSON, missing ``limit_gb``, non-numeric ``limit_gb``,
``nan`` / ``inf``, or a negative value.
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s -X PUT http://localhost:8080/quota/alice \
-H 'Content-Type: application/json' \
-d '{"limit_gb": 10.0}'
``GET /quota/{cache_salt}``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Read the current quota and live usage for one ``cache_salt``.
**Path parameters:** ``cache_salt`` — tenant identifier (use ``_default``
for the empty salt).
**Response** (``200 OK``):
.. code-block:: json
{
"cache_salt": "alice",
"limit_gb": 10.0,
"current_usage_gb": 2.137,
"exists": true
}
``exists`` is ``false`` when no quota was ever registered for this
``cache_salt`` (``limit_gb`` is then ``0.0`` and ``current_usage_gb``
reflects whatever bytes are currently cached for that salt — those bytes
will evict next cycle under ``IsolatedLRU``). This endpoint never returns
``404`` for an unknown salt.
**HTTP status codes:**
- ``200``: quota and usage returned (also when the salt has no registered
quota — ``exists`` is then ``false``; never ``404``).
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/quota/alice | jq
``DELETE /quota/{cache_salt}``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Remove a ``cache_salt``'s quota entry. Any bytes still cached under this
``cache_salt`` become over-budget on the next eviction cycle (effective
limit drops to ``0``) and will be evicted.
**Path parameters:** ``cache_salt`` — tenant identifier (use ``_default``
for the empty salt).
**Response** (``200 OK``):
.. code-block:: json
{"cache_salt": "alice", "status": "removed"}
When no quota was registered for the given ``cache_salt``, the response
is ``{"cache_salt": "...", "status": "not_found"}`` (still ``200 OK``).
**HTTP status codes:**
- ``200``: quota entry removed (``"removed"``) or none existed
(``"not_found"``); never ``404``.
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s -X DELETE http://localhost:8080/quota/alice
``GET /quota``
~~~~~~~~~~~~~~~~~~
List every registered quota alongside its live usage.
**Response** (``200 OK``):
.. code-block:: json
{
"users": {
"alice": {"limit_gb": 10.0, "current_usage_gb": 2.137},
"bob": {"limit_gb": 4.0, "current_usage_gb": 0.812}
}
}
Only ``cache_salt`` values with a **registered** quota appear; the empty
salt is reported under the ``_default`` key.
**HTTP status codes:**
- ``200``: quota listing returned (``{"users": {}}`` when none registered).
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/quota | jq
.. _mp-http-dax-api:
Runtime L2 Reconfiguration
--------------------------
These endpoints are available when the server has a runtime-reconfigurable L2
adapter. They only change LMCache runtime mappings and metadata; backend
resources such as DAX device paths must already exist and be readable and
writable by the server. The endpoint routes ``backend``, ``operation``, and the
JSON request body into the generic L2 adapter reconfiguration API, while
backend-specific validation and migration semantics stay inside the adapter.
``backend`` and ``operation`` path segments are normalized (stripped and
lower-cased). Within a request body, ``adapter_index`` (default ``0``) is
**backend-local** — it indexes only the adapters of that backend, not the
engine-wide adapter list. If an L2 adapter is wrapped by serde, the backend
string is still the configured L2 adapter type, not the serde wrapper type.
.. note::
The backend strings accepted here are discovered via
``GET /config/adapters``: each adapter whose ``reconfigurable`` flag is
``true`` can be addressed by its ``type_name``.
``GET /reconfigure/{backend}/status``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Report the runtime-manageable adapters for one backend type. Each adapter
entry's ``adapter_index`` is rewritten to its **backend-local** 0-based index
(the value to pass back in operation request bodies).
**Path parameters:** ``backend`` — adapter ``type_name`` (normalized:
stripped and lower-cased; discover valid values via ``GET /config/adapters``).
**Response** (``200 OK``):
.. code-block:: json
{
"enabled": true,
"backend": "dax",
"num_adapters": 1,
"adapters": [
{"adapter_index": 0, "...": "backend-specific adapter fields"}
]
}
An unknown or empty backend returns ``enabled=false``, ``num_adapters=0``,
``adapters=[]`` (it is **not** a ``404``).
**HTTP status codes:**
- ``200``: status returned (including the unknown-backend case above).
- ``400``: ``backend`` is empty.
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/reconfigure/dax/status | jq
``POST /reconfigure/{backend}/{operation}``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Apply one reconfiguration operation to a backend adapter. The request body is
a JSON object whose accepted fields depend on the backend and operation. The
``200`` response is whatever the storage manager's
``reconfigure_l2_adapter`` returns (a backend-defined dict).
**Path parameters:** ``backend`` (adapter ``type_name``) and ``operation``
(e.g. ``add`` / ``remove`` / ``resize`` for ``dax``). Both are normalized
(stripped and lower-cased).
For the **generic** path (any backend other than ``dax``), the body carries
``adapter_index`` plus any backend-specific fields, which are forwarded
verbatim to the adapter.
For **Device-DAX** (``backend=dax``), JSON request bodies are used because DAX
paths contain slashes. The accepted operations and fields are:
.. list-table::
:header-rows: 1
:widths: 15 85
* - Operation
- Body fields
* - ``add``
- ``device_path`` (str, required), ``size`` (int byte count or string
such as ``"100GiB"``, required), ``adapter_index`` (default ``0``).
* - ``remove``
- ``device_path`` (str, required), ``mode`` (``migrate`` | ``evict`` |
``drain``, default ``migrate``), ``force`` (bool, default ``false``),
``adapter_index`` (default ``0``).
* - ``resize``
- ``device_path`` (str, required), ``size`` (int or string, required),
``mode`` (``migrate`` | ``evict``, default ``migrate``), ``force``
(bool, default ``false``), ``adapter_index`` (default ``0``).
``size`` accepts an integer byte count or a string with a base-1024 unit
suffix (``b``, ``kib``, ``mib``, ``gib``, ``tib`` and the ``k``/``m``/``g``/``t``
aliases), e.g. ``"100GiB"``; it must resolve to a positive value.
**Response** (``200 OK``):
The body is the backend's ``reconfigure_l2_adapter`` result (a backend-defined
dict). A successful DAX ``add`` looks like:
.. code-block:: json
{
"status": "ok",
"operation": "add",
"adapter_index": 0,
"device": {"device_path": "/dev/dax0.0", "state": "active", "size_bytes": 107374182400}
}
**HTTP status codes:**
- ``200``: success (body is the storage manager's reconfigure result).
- ``400``: empty ``backend``/``operation``, an unsupported DAX operation, or
an invalid ``size``.
- ``404``: ``adapter_index`` is out of range for the backend.
- ``422``: request body fails validation (e.g. a missing required field, or
an unknown field in a DAX body — DAX bodies reject extras).
- ``503``: engine not initialized.
**Example:**
.. code-block:: bash
curl -s -X POST http://localhost:8080/reconfigure/dax/add \
-H 'Content-Type: application/json' \
-d '{"device_path": "/dev/dax0.0", "size": "100GiB"}'
See :doc:`/kv_cache/storage_backends/dax` for detailed request examples,
mode semantics, and validation guidance.
Observability
-------------
``GET /metrics``
~~~~~~~~~~~~~~~~
Prometheus exposition format for every metric registered on the default
``prometheus_client`` registry (``Content-Type: text/plain``). Scrape this
directly from Prometheus. See :doc:`observability/index` for the list of
exported metrics.
**Response** (``200 OK``, ``text/plain``): the Prometheus exposition-format
metrics body.
**HTTP status codes:**
- ``200``: metrics returned.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/metrics
``POST /metrics/reset``
~~~~~~~~~~~~~~~~~~~~~~~
Resets all LMCache observability metrics to their initial state
(``reset_observability_metrics``). Intended for test harnesses and
benchmarks — not for production.
**Response** (``200 OK``, ``text/plain``):
.. code-block:: text
ok
**HTTP status codes:**
- ``200``: metrics reset.
**Example:**
.. code-block:: bash
curl -s -X POST http://localhost:8080/metrics/reset
Diagnostics and Debugging
-------------------------
``GET /loglevel``
~~~~~~~~~~~~~~~~~
Inspect or mutate Python logger levels at runtime. All responses are
``text/plain``. The endpoint has three modes driven by query parameters:
.. list-table::
:header-rows: 1
:widths: 30 70
* - Query
- Behavior
* - (no params)
- List every logger registered with :mod:`logging` and its level.
* - ``?logger_name=<name>``
- Return the effective level of the named logger.
* - ``?logger_name=<name>&level=<LEVEL>``
- Set the named logger (and its handlers) to ``LEVEL``
(``DEBUG``/``INFO``/``WARNING``/``ERROR``/``CRITICAL``;
case-insensitive). Returns ``400`` on an unknown level.
Passing ``level`` without ``logger_name`` matches none of the modes and
returns ``200`` with a ``null`` body.
**Response** (``200 OK``, ``text/plain``): one ``<logger>: <LEVEL>`` line per
registered logger (list mode), a single ``<logger>: <LEVEL>`` (read mode), or a
confirmation of the updated level (set mode).
**HTTP status codes:**
- ``200``: levels listed, read, or set (also the ``null``-body case above).
- ``400``: ``level`` is not a known logging level.
**Examples:**
.. code-block:: bash
# list everything
curl -s http://localhost:8080/loglevel
# read one
curl -s 'http://localhost:8080/loglevel?logger_name=lmcache'
# elevate to DEBUG
curl -s 'http://localhost:8080/loglevel?logger_name=lmcache&level=DEBUG'
``GET /threads``
~~~~~~~~~~~~~~~~
Enumerate active Python threads in the server process along with their
stack traces, plus a total-count summary (``Content-Type: text/plain``).
Useful for live debugging of hangs or runaway workers.
.. list-table::
:header-rows: 1
:widths: 30 70
* - Query
- Behavior
* - ``?name=<substr>``
- Keep only threads whose name contains ``<substr>``
(case-insensitive).
* - ``?thread_id=<int>``
- Keep only the thread with the matching ``ident``.
.. warning::
The response contains live stack traces and can disclose internal code
paths and state. Restrict network access to this endpoint in production.
**Response** (``200 OK``, ``text/plain``): a total-thread-count summary followed
by each thread's name, ``ident``, and current stack trace.
**HTTP status codes:**
- ``200``: thread listing returned.
**Example:**
.. code-block:: bash
curl -s 'http://localhost:8080/threads?name=periodic'
``GET /periodic-threads``
~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a JSON snapshot of the
:class:`~lmcache.v1.periodic_thread.PeriodicThreadRegistry`: counts by
level plus per-thread status (last run timestamp, latest summary, etc.).
.. list-table::
:header-rows: 1
:widths: 30 70
* - Query
- Behavior
* - ``?level=critical|high|medium|low``
- Only include threads at the given level. ``400`` on unknown.
* - ``?running_only=true``
- Only include threads currently running.
* - ``?active_only=true``
- Only include threads considered active (recent tick).
**Response** (``200 OK``):
.. code-block:: json
{
"summary": {
"total_count": 4,
"running_count": 4,
"active_count": 4,
"by_level": {
"critical": {"total": 1, "running": 1, "active": 1},
"high": {"total": 2, "running": 2, "active": 2},
"medium": {"total": 1, "running": 1, "active": 1},
"low": {"total": 0, "running": 0, "active": 0}
}
},
"threads": [
{
"name": "...",
"level": "high",
"interval": 5.0,
"is_running": true,
"is_active": true,
"last_run_ago": 1.2,
"total_runs": 120,
"failed_runs": 0,
"success_rate": 100.0,
"last_summary": {"...": "..."}
}
]
}
**HTTP status codes:**
- ``200``: snapshot returned.
- ``400``: unknown ``level`` filter.
**Example:**
.. code-block:: bash
curl -s 'http://localhost:8080/periodic-threads?level=critical' | jq
``GET /periodic-threads/{thread_name}``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Detailed status for a single periodic thread (the same per-thread object
shown in the ``threads`` list above).
**Path parameters:** ``thread_name`` — the registered periodic thread name.
**Response** (``200 OK``):
.. code-block:: json
{
"name": "storage-flush",
"level": "critical",
"interval": 5.0,
"is_running": true,
"is_active": true,
"last_run_ago": 1.2,
"total_runs": 120,
"failed_runs": 0,
"success_rate": 100.0,
"last_summary": {"...": "..."}
}
**Response** (``404 Not Found``) when the name is unknown:
.. code-block:: json
{"error": "Thread not found: <name>"}
**HTTP status codes:**
- ``200``: thread status returned.
- ``404``: no periodic thread with that name.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/periodic-threads/storage-flush | jq
``GET /periodic-threads-health``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Fast health check covering only ``critical`` and ``high`` level periodic
threads. A thread is flagged unhealthy when it is marked running but has
not ticked within its expected interval. Always returns ``200`` — health
is conveyed by the ``healthy`` boolean, not the HTTP status.
**Response** (``200 OK``):
.. code-block:: json
{
"healthy": true,
"unhealthy_count": 0,
"unhealthy_threads": []
}
When something is lagging:
.. code-block:: json
{
"healthy": false,
"unhealthy_count": 1,
"unhealthy_threads": [
{
"name": "storage-flush",
"level": "critical",
"last_run_ago": 42.5,
"interval": 5.0
}
]
}
**HTTP status codes:**
- ``200``: health reported — always ``200``, even when unhealthy
(the ``healthy`` boolean is ``false``).
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/periodic-threads-health
``GET /env``
~~~~~~~~~~~~
Dumps the process environment variables as a sorted, pretty-printed
JSON document. The response ``Content-Type`` is ``text/plain`` so it can be
piped directly to a terminal.
.. warning::
The payload contains **every** environment variable, including any
secrets injected via the environment. There is no redaction or auth —
restrict network access to this endpoint in production.
**Response** (``200 OK``, ``text/plain``):
.. code-block:: json
{
"HOME": "/root",
"LMCACHE_LOG_LEVEL": "INFO",
"PATH": "/usr/local/bin:/usr/bin"
}
**HTTP status codes:**
- ``200``: environment dump returned.
**Example:**
.. code-block:: bash
curl -s http://localhost:8080/env
``POST /run_script``
~~~~~~~~~~~~~~~~~~~~
Execute an uploaded Python script inside the server process. The script is
uploaded as multipart form data under the field name ``script`` and is
``exec``'d with a restricted ``__builtins__`` (only ``print``, ``str``,
``int``, ``float``, ``list``, ``dict``, ``tuple``, ``set``, and a guarded
``__import__``). Only modules listed in ``--script-allowed-imports`` can be
imported; the running FastAPI ``app`` is injected into the script globals.
If the script assigns a variable named ``result``, its stringified value is
returned; otherwise the body is ``"Script executed successfully"``
(``Content-Type: text/plain``).
**Request body:** multipart form data with a single ``script`` file field
(the Python source to execute). Not a JSON body.
.. danger::
This endpoint runs caller-supplied code in-process. The restricted
builtins are **not** a security sandbox — combined with the injected
``app`` object and any allowed imports, treat it as full remote code
execution. Never expose it on an untrusted network.
**Response** (``200 OK``, ``text/plain``): the stringified ``result`` variable if
the script assigns one, otherwise ``Script executed successfully``.
**HTTP status codes:**
- ``200``: script executed.
- ``400``: no ``script`` file provided.
- ``500``: an exception was raised during import setup or execution
(body: ``"Error executing script: <reason>"``).
**Example:**
.. code-block:: bash
curl -s -X POST http://localhost:8080/run_script \
-F 'script=@my_script.py'
Adding New Endpoints
--------------------
Endpoints are auto-discovered from
``lmcache/v1/multiprocess/http_apis/``. To add a new MP-only endpoint:
1. Create a new module in that directory named ``<name>_api.py``.
2. Define a module-level ``router = APIRouter()``.
3. Register handlers on ``router`` using FastAPI decorators.
4. Access the engine via ``request.app.state.engine`` and guard for the
``None`` case (engine not yet initialized).
The :class:`~lmcache.v1.multiprocess.http_api_registry.HTTPAPIRegistry`
will pick the module up automatically at startup — no central
registration list to edit.
If the route is generic enough to be shared with the vLLM-embedded API
server, add it under ``lmcache/v1/internal_api_server/common/`` instead.
It will be picked up on the MP side via ``common_api.py`` unless its
module name is listed in ``_MP_INCOMPATIBLE_MODULES`` there (reserved
for modules that require vLLM-specific ``app.state`` attributes; the
list is currently empty). A handler that lives under
``internal_api_server/vllm/`` can still be surfaced on the MP server by
including its router from a group module under ``http_apis/`` (as
``info_api.py`` does for the version endpoints).
When adding a new endpoint, please also add a matching section to this
page documenting the endpoint's purpose, request/response schema, and
an example ``curl`` invocation.