项目文件夹

文件
2026-07-13 12:24:33 +08:00

586 行
23 KiB
ReStructuredText

Metrics
=======
Metrics are collected via OpenTelemetry and made available to Prometheus in
one of two ways, depending on whether ``--otlp-endpoint`` is set:
- **Pull mode (default, no collector).** When ``--otlp-endpoint`` is *not*
set, the server publishes a Prometheus ``/metrics`` endpoint that Prometheus
scrapes directly.
.. important::
For ``lmcache server``, ``/metrics`` is served by the **HTTP frontend**
on ``--http-port`` (default **8080**), e.g.
``http://<host>:8080/metrics``**not** on ``--prometheus-port``.
``--prometheus-port`` is *ignored* by ``lmcache server``: the standalone
Prometheus HTTP server is disabled because the HTTP frontend already
serves ``/metrics``. The frontend-less entrypoints
(``python -m lmcache.v1.multiprocess.server`` and ``lmcache trace
replay``) have no HTTP frontend, so *they* serve ``/metrics`` on
``--prometheus-port`` (default 9090). See
:ref:`mp-obs-metrics-endpoint` for the full breakdown. Either way,
``/metrics`` is empty until the first store/retrieve — drive some
traffic before you go looking.
- **Push mode (OTLP).** When ``--otlp-endpoint`` is set, metrics are pushed to
an OpenTelemetry Collector, which re-exposes them for Prometheus to scrape.
See :doc:`index` for the bundled Collector + Prometheus + Grafana stack.
All metrics use the ``lmcache_mp.`` prefix (multiprocess). On Prometheus,
dots are converted to underscores and counters get a ``_total`` suffix
(e.g. ``lmcache_mp_l1_read_chunks_total``); histograms gain a unit suffix
plus ``_sum`` / ``_count`` / ``_bucket`` (e.g.
``lmcache_mp_l2_store_throughput_GB_per_second_sum``).
.. _mp-observability-resource:
Global Resource Attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~
Every metric and span exported by an MP server carries Resource-level
attributes built at startup. These identify the process producing the
telemetry and are orthogonal to per-metric attributes such as
``cache_salt``.
.. list-table::
:header-rows: 1
:widths: 30 25 45
* - Attribute
- CLI flag / config
- Default when unset
* - ``service.instance.id``
- ``--instance-id`` / ``MPServerConfig.instance_id``
- Random UUID v4 minted at startup.
Resource attributes attach to the ``MeterProvider`` / ``TracerProvider``
and propagate to every exported datapoint via OTLP. On Prometheus, SDK
resource attributes surface on the ``target_info`` series rather than
on each time-series — this is standard OTel behavior.
L1 Metrics
~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.l1_read``
- Counter (attr: ``cache_salt``)
- Number of chunks read from L1, grouped by tenant.
* - ``lmcache_mp.l1_write``
- Counter (attr: ``cache_salt``)
- Number of chunks written to L1, grouped by tenant.
* - ``lmcache_mp.l1_evicted``
- Counter (attr: ``cache_salt``)
- Number of chunks evicted by the EvictionController, grouped by
tenant.
* - ``lmcache_mp.l1_eviction_loop_ticks``
- Counter
- L1 eviction-loop iterations (every cycle, regardless of whether
the watermark was crossed). Driven by ``L1_EVICTION_LOOP_TICK``.
* - ``lmcache_mp.l1_eviction_loop_triggered``
- Counter
- L1 eviction-loop iterations where ``usage >= watermark`` and the
eviction policy actually ran. The two counters distinguish "loop
is alive" from "eviction fired" — important when debugging
short-lived benchmarks that complete faster than the 1 Hz
polling cycle.
L1 Chunk Lifecycle Histograms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sampled (default 1%) chunk-level lifecycle tracking via
``L1LifecycleSubscriber``. Only sampled chunks contribute to histograms;
counters above always count all events. Sampling is deterministic
(hash-based), so the same key always gets the same decision with zero
memory overhead.
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.l1_chunk_lifetime``
- Histogram
- Time from allocation to eviction per sampled chunk.
* - ``lmcache_mp.l1_chunk_idle_before_evict``
- Histogram
- Time from last access to eviction per sampled chunk.
* - ``lmcache_mp.l1_chunk_reuse_gap``
- Histogram
- Time gap between consecutive touches (read or write) of the same chunk.
* - ``lmcache_mp.l1_chunk_evict_reuse_gap``
- Histogram
- Time from eviction to next reuse (capped at 300 s).
StorageManager Real-Reuse Metrics
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Workload-level reuse histograms emitted by ``SMLifecycleSubscriber``,
driven by caller-facing StorageManager events
(``SM_READ_PREFETCHED_FINISHED``, ``SM_WRITE_FINISHED``). Internal
read-lock releases by the store/prefetch controllers are excluded so
the signal reflects user-driven access only.
Both histograms are tagged with ``cache_salt`` for per-tenant
isolation. The per-salt access counter advances on every read and
write of every chunk (regardless of sampling) so the chunks-gap
reflects true storage volume; the histogram itself records gaps only
for chunks that pass the (deterministic, hash-based) sampling gate.
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.real_reuse_gap``
- Histogram (tag: ``cache_salt``)
- Time gap between a chunk's last access (read or write) and its
next read. Captures storage cost — how long a stored chunk sat
between accesses. Emitted only on read events.
* - ``lmcache_mp.real_reuse_gap_objects``
- Histogram (tag: ``cache_salt``)
- Per-``cache_salt`` access-counter gap between two reads of the
same chunk. Captures storage volume — how many chunk-accesses
occurred while this chunk waited for its next read. Emitted on
read events for sampled chunks.
L2 Metrics
~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.l2_store_submitted``
- Counter
- Number of L2 store requests submitted.
* - ``lmcache_mp.l2_store_submitted_objects``
- Counter (attr: ``cache_salt``)
- Number of chunks submitted for L2 store, grouped by tenant.
* - ``lmcache_mp.l2_store_completed``
- Counter (attr: ``l2_name``)
- Number of L2 store requests completed, labeled by adapter type.
* - ``lmcache_mp.l2_store_completed_objects``
- Counter (attr: ``cache_salt``)
- Number of chunks successfully stored to L2, grouped by tenant.
* - ``lmcache_mp.l2_prefetch_lookup``
- Counter
- Number of L2 prefetch lookup requests.
* - ``lmcache_mp.l2_prefetch_lookup_objects``
- Counter (attr: ``cache_salt``)
- Number of chunks submitted for L2 prefetch lookup, grouped by
tenant.
* - ``lmcache_mp.l2_prefetch_hit``
- Counter
- Number of prefix chunks found in L2 lookup.
* - ``lmcache_mp.l2_prefetch_load_submitted``
- Counter
- Number of L2 prefetch load requests submitted.
* - ``lmcache_mp.l2_prefetch_load_submitted_objects``
- Counter (attr: ``cache_salt``)
- Number of chunks submitted for L2 load, grouped by tenant.
* - ``lmcache_mp.l2_prefetch_load_completed``
- Counter (attr: ``cache_salt``)
- Number of chunks successfully loaded from L2, grouped by tenant.
* - ``lmcache_mp.l2_load_completed``
- Counter (attr: ``l2_name``)
- Number of per-adapter L2 load requests completed, labeled by adapter type.
* - ``lmcache_mp.l2_evicted_objects``
- Counter (attr: ``cache_salt``)
- Number of chunks evicted from L2, grouped by tenant.
The ``l2_name``-labeled counters (``l2_store_completed`` and
``l2_load_completed``) exist so dashboards can compute per-backend IOPS on
demand via ``rate(lmcache_mp_l2_store_completed_requests_total{l2_name="..."}[1m])``
(and the equivalent for loads). No separate ``*_iops`` metric is exported;
keeping the raw counter lets dashboard users pick their own window.
Failure & Health Counters
~~~~~~~~~~~~~~~~~~~~~~~~~
Health-monitoring counters emitted on the dedicated ``lmcache_mp.health``
OTel meter. Driven by the ``L1FailureMetricsSubscriber`` and
``L2FailureMetricsSubscriber``, which are registered automatically when
metrics are enabled. All three counters carry ``model_name`` (extracted
from each ``ObjectKey``) so operators can slice per-model on the
Prometheus ``/metrics`` endpoint.
.. list-table::
:header-rows: 1
:widths: 35 15 50
* - Metric
- Type
- Description
* - ``lmcache_mp.l1_allocation_failure``
- Counter
- L1 memory allocation failures (OOM) during ``reserve_write``.
Tagged by ``during`` ∈ {``l1_store``, ``l2_prefetch``} to
distinguish user-initiated stores from prefetch-triggered
allocations, plus ``model_name``.
* - ``lmcache_mp.l1_read_failure``
- Counter
- L1 ``reserve_read`` failures. Tagged by ``during``
{``l2_store``, ``l1_retrieve``}, ``reason`` ∈ {``not_found``,
``write_locked``}, plus ``model_name``. **Post-lookup anomaly
counter**, not a cache-miss counter — in MP mode ``reserve_read``
is only called after a successful lookup, so any non-zero value
indicates a lookup/reserve race or unexpected eviction and should
stay near zero in healthy operation.
* - ``lmcache_mp.l2_prefetch_failure``
- Counter
- Chunks that L2 reported present at lookup but failed to land in L1.
Tagged by ``reason`` ∈ {``l1_oom``, ``not_found``} plus
``model_name``. ``l1_oom`` means L1 had no room to receive the
prefetched object; ``not_found`` means the adapter returned no
data despite a positive lookup (e.g. concurrent delete).
A ``reason=serde_failure`` value will be added to ``l2_prefetch_failure``
as an additive, non-breaking extension once L2 adapters distinguish
deserialization errors from missing objects — no dashboard migration
needed when that lands.
For the full design rationale (including which event types drive each
counter and why ``lmcache_instance_id`` is deferred), see
``docs/design/v1/mp_observability/METRICS.md`` in the source tree.
Lookup Hit-Rate Metrics
~~~~~~~~~~~~~~~~~~~~~~~
Token-level counters whose ratio gives the fraction of tokens requested by
a lookup that were served from either L1 or L2. L0 (GPU prefix cache) is
intentionally excluded — it is vLLM-owned and not observable from LMCache.
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.lookup_requested``
- Counter (attrs: ``model_name``, ``cache_salt``)
- Total tokens submitted for lookup (denominator of the L1+L2
token-level hit rate). Only chunk-aligned tokens are counted.
* - ``lmcache_mp.lookup_hit``
- Counter (attrs: ``model_name``, ``cache_salt``)
- Total tokens found in L1 or L2 during lookup (numerator of the
L1+L2 token-level hit rate). Counts the contiguous prefix hit only.
Both counters are driven by the same event (``MP_LOOKUP_PREFETCH_END``),
so they always advance together per completed lookup. Early-exit lookups
contribute ``0`` to both, and abandoned lookups contribute to neither.
The ``model_name`` and ``cache_salt`` attributes are captured at lookup
time from ``IPCCacheServerKey`` so dashboards can compute per-model or
per-tenant hit rate. ``cache_salt`` can be high-cardinality (one entry
per tenant or isolation domain); drop it at scrape time with
``metric_relabel_configs`` if storage cost matters.
**PromQL for hit rate:**
.. code-block:: promql
# Aggregate (all models, all salts):
rate(lmcache_mp_lookup_hit_tokens_total[5m])
/ rate(lmcache_mp_lookup_requested_tokens_total[5m])
# Per-model:
sum(rate(lmcache_mp_lookup_hit_tokens_total[5m])) by (model_name)
/ sum(rate(lmcache_mp_lookup_requested_tokens_total[5m])) by (model_name)
L0 (GPU) Block Lifecycle Histograms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sampled (default 1%) GPU KV cache block lifecycle tracking via
``L0LifecycleSubscriber``. Eviction is detected at reallocation time
(when a block is assigned different tokens). Sampling uses random
selection with a ``_skipped`` set (bounded by the finite number of
physical GPU blocks).
All L0 histograms are emitted with ``instance_id`` and ``model_name``
OTel attributes, enabling per-instance and per-model metric slicing
in Prometheus (e.g.
``lmcache_mp_l0_block_lifetime_seconds{instance_id="12345",model_name="llama-7b"}``).
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.l0_block_lifetime``
- Histogram
- Time from allocation to eviction per sampled GPU block.
* - ``lmcache_mp.l0_block_idle_before_evict``
- Histogram
- Time from last access to eviction per sampled GPU block.
* - ``lmcache_mp.l0_block_reuse_gap``
- Histogram
- Time gaps between consecutive accesses of the same GPU block.
L0 ↔ L1 Throughput Histograms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Per-request throughput of GPU↔CPU copies via
``L0L1ThroughputSubscriber``. Every store/retrieve request contributes
one sample to the appropriate histogram:
``total_bytes / (end_ts - start_ts)`` in GB/s. Timestamps come from
``MP_{STORE,RETRIEVE}_{START,END}`` events published on the GPU cupy
stream, so they reflect true GPU-stream copy time — not Python/lock
overhead.
All throughput histograms are emitted with ``engine_id`` (vLLM worker
instance id), ``device`` (e.g. ``"cuda:3"``), and ``model_name`` OTel
attributes, enabling per-worker, per-device, and per-model slicing in
Prometheus (e.g.
``lmcache_mp_l0_l1_store_throughput_GB_per_second{engine_id="0",device="cuda:3",model_name="meta-llama/Llama-3.1-8B"}``).
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.l0_l1_store_throughput``
- Histogram
- GPU→CPU (L0→L1) store throughput in GB/s per request.
* - ``lmcache_mp.l0_l1_load_throughput``
- Histogram
- CPU→GPU (L1→L0) load throughput in GB/s per request.
L1 ↔ L2 Throughput Histograms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Per-request throughput of L1↔L2 transfers via
``L2ThroughputSubscriber``. The store path correlates
``L2_STORE_SUBMITTED````L2_STORE_COMPLETED`` by
``(adapter_index, task_id)``. The load path correlates the per-adapter
``L2_LOAD_TASK_SUBMITTED````L2_LOAD_TASK_COMPLETED`` events by
``(request_id, adapter_index)``; the request-level
``L2_PREFETCH_LOAD_*`` events used by the chunk-count counters aggregate
across adapters and cannot be attributed to a specific ``l2_name``.
Timestamps span **submit → complete**, so the duration includes adapter
queue, network, and disk I/O — the value is *bytes / end-to-end
latency*, not raw transfer rate. Use these histograms to compare
adapter types and catch regressions; use the L0↔L1 histograms when you
need pure copy-time throughput.
All L1↔L2 throughput histograms carry a single ``l2_name`` OTel
attribute — the registered adapter type (e.g. ``"fs"``, ``"nixl_store"``,
``"mooncake_store"``) — enabling per-backend slicing in Prometheus (e.g.
``lmcache_mp_l2_store_throughput_GB_per_second{l2_name="nixl_store"}``).
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.l2_store_throughput``
- Histogram
- L1→L2 store throughput in GB/s per request.
* - ``lmcache_mp.l2_load_throughput``
- Histogram
- L2→L1 load throughput in GB/s per (request, adapter) pair.
**PromQL for average throughput (GB/s), per backend:**
.. code-block:: promql
# L1 -> L2 store throughput, averaged over the last minute, per l2_name:
sum by (l2_name) (rate(lmcache_mp_l2_store_throughput_GB_per_second_sum[1m]))
/ sum by (l2_name) (rate(lmcache_mp_l2_store_throughput_GB_per_second_count[1m]))
# L2 -> L1 load throughput (same shape):
sum by (l2_name) (rate(lmcache_mp_l2_load_throughput_GB_per_second_sum[1m]))
/ sum by (l2_name) (rate(lmcache_mp_l2_load_throughput_GB_per_second_count[1m]))
.. note::
``l2_store_throughput`` populates whenever chunks are written to L2.
``l2_load_throughput`` only populates when chunks are read **from L2 into
L1** — i.e. on a prefetch load after the entry has aged out of L1. If your
working set fits entirely in L1 (common with small models or a large
``--l1-size-gb``), lookups are served from L1 and the load histogram stays
empty even though store throughput is non-zero. Drive enough distinct data
to force L1 eviction, or restart the server between store and load passes,
to exercise the L2 load path.
Engine Counters
~~~~~~~~~~~~~~~
Worker-scoped counters tied to what the MP server delivers back to each
vLLM worker via ``retrieve()``. Labeled by ``worker_id`` (the vLLM
worker instance id) — distinct from any scheduler-scoped id that may
appear on other metrics.
.. list-table::
:header-rows: 1
:widths: 40 25 35
* - Metric
- Type
- Description
* - ``lmcache_mp.num_chunks_loaded``
- Counter (attrs: ``worker_id``, ``model_name``, ``cache_salt``)
- Total number of LMCache chunks loaded into the engine, summed
over all ``retrieve()`` completions. Sliceable per worker, per
model, and per tenant / isolation domain (``cache_salt``).
``cache_salt`` may be high-cardinality; drop it at scrape time
with ``metric_relabel_configs`` if storage cost matters.
Observable Gauges
~~~~~~~~~~~~~~~~~
Point-in-time state snapshots registered via ``register_gauge``
(pull-based OTel observable gauges).
The three in-flight metrics carry two attributes that distinguish
adapters even when more than one is registered with the same backend
type — same shape as ``lmcache_mp.l2_store_completed``:
- ``l2_name`` — the registered adapter type (e.g. ``"fs"``,
``"nixl_store"``, ``"mooncake_store"``).
- ``adapter_index`` — position in the controller's adapter list.
Adapters with no in-flight work emit no datapoint for that scrape.
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.active_prefetch_jobs``
- ObservableGauge
- Number of prefetch jobs currently in-flight. A sustained high
value may indicate slow L2 backends or polling delays.
* - ``lmcache_mp.l1_memory_usage_bytes``
- ObservableGauge
- Bytes currently held in L1. Rising without plateauing typically
indicates a leak; saturating at the configured ``--l1-size-gb``
indicates working set exceeds capacity.
* - ``lmcache_mp.l1_usage_ratio``
- ObservableGauge
- L1 used/total ratio (``0.0````1.0``), sampled at scrape time
from ``L1Manager.get_memory_usage()``. Returns ``0.0`` when the
gauge target is not yet wired up or ``total_bytes`` is zero, so
the callback never raises during a scrape. Compare against the
eviction watermark (default ``0.8``) to read whether the
eviction loop is below or above its trigger threshold.
* - ``lmcache_mp.l2_usage_bytes``
- ObservableGauge (attr: ``l2_name``)
- Bytes currently held in each L2 adapter, sampled at scrape time
from ``adapter.get_usage()``. One observation per configured
adapter, tagged by ``l2_name`` (the adapter type, e.g. ``"fs"``,
``"nixl_store"``, ``"mooncake_store"``). Parallel to
``l1_memory_usage_bytes`` for the L2 tier — use it to see how
much each L2 backend currently holds. Adapters whose
``get_usage()`` raises are skipped silently rather than poisoning
the observation, so a missing datapoint for one ``l2_name`` can
mean either "not configured" or "adapter errored on this
scrape" — cross-check with the L2 store/load counters.
* - ``lmcache_mp.num_inflight_l2_stores``
- ObservableGauge (attrs: ``l2_name``, ``adapter_index``)
- L2 store tasks currently executing, per adapter. Sustained
non-zero values indicate the adapter cannot keep up with the
L1 → L2 write rate.
* - ``lmcache_mp.num_inflight_l2_loads``
- ObservableGauge (attrs: ``l2_name``, ``adapter_index``)
- L2 → L1 prefetch load tasks currently executing, per adapter.
Pair with ``num_inflight_l2_stores`` to see whether read or write
traffic dominates a given backend.
* - ``lmcache_mp.inflight_load_memory_usage_bytes``
- ObservableGauge (attrs: ``l2_name``, ``adapter_index``)
- L1 bytes reserved by in-flight L2 → L1 prefetch loads, per
adapter. Rising in-flight bytes alongside rising
``l1_memory_usage_bytes`` is a signal that prefetch reservations
are crowding out cacheable data. Per-adapter byte attribution
follows each request's ``load_plan`` bitmap, so summing across
adapters never double-counts.
EventBus Self-Monitoring
~~~~~~~~~~~~~~~~~~~~~~~~
Health metrics for the EventBus itself, registered by
``EventBusSelfMetricsSubscriber`` on the ``lmcache.event_bus`` OTel
meter. These metrics observe bus state directly via the ``EventBus``
accessors and report on every OTel scrape — they are not driven by
events, so dropping or failing subscribers cannot silence them.
Use them to answer: is the EventBus keeping up with publishers, is
anything being dropped, and are any subscriber callbacks raising?
A non-zero ``dropped_events_total`` or a sustained non-zero
``drain_lag_seconds`` indicates the bus is at ``--event-bus-queue-size``
and tail-dropping; raise that flag or investigate slow subscribers.
.. list-table::
:header-rows: 1
:widths: 40 15 45
* - Metric
- Type
- Description
* - ``lmcache_mp.event_bus.queue_depth``
- ObservableGauge
- Events currently queued in the EventBus (``len(_queue)`` at
scrape time).
* - ``lmcache_mp.event_bus.drain_lag_seconds``
- ObservableGauge
- Seconds since the oldest queued event was published; ``0.0``
when empty. Rising values mean the drain thread is falling
behind.
* - ``lmcache_mp.event_bus.dropped_events_total``
- ObservableCounter
- Cumulative events dropped because the EventBus queue was at
``--event-bus-queue-size``.
* - ``lmcache_mp.event_bus.subscriber_exceptions``
- ObservableCounter (attr: ``subscriber_name``)
- Cumulative exceptions raised by subscriber callbacks during
EventBus dispatch, tagged by ``subscriber_name`` (the failing
callback's owning class for bound methods, or ``__qualname__``
for free functions).
For the full design rationale and the in-process accessors that back
each metric see ``docs/design/v1/mp_observability/METRICS.md`` and
``docs/design/v1/mp_observability/event-bus.md`` in the source tree.
Prometheus Scrape Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In **pull mode** (no ``--otlp-endpoint``), point Prometheus at the server's
HTTP-frontend port — ``--http-port``, default **8080** — not at
``--prometheus-port``:
.. code-block:: yaml
scrape_configs:
- job_name: "lmcache-mp"
static_configs:
- targets: ["<lmcache-host>:8080"] # --http-port, NOT --prometheus-port
In **push mode** (``--otlp-endpoint`` set), the server does not expose
``/metrics`` itself; scrape the OpenTelemetry Collector's Prometheus exporter
instead. The bundled stack in ``examples/observability/`` wires this up for
you — see :doc:`index`.