586 lines
23 KiB
ReStructuredText
586 lines
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`.
|
||
|