指标#

指标通过 OpenTelemetry 收集,并根据是否设置了 --otlp-endpoint 以两种方式之一提供给 Prometheus:

  • 拉取模式(默认,无收集器)。--otlp-endpoint 设置时,服务器会发布一个 Prometheus /metrics 端点,Prometheus 直接抓取该端点。

    重要

    对于 lmcache server/metricsHTTP 前端--http-port``(默认 **8080**)上提供,例如 ``http://<host>:8080/metrics--prometheus-port 上。--prometheus-portlmcache server 忽略:独立的 Prometheus HTTP 服务器被禁用,因为 HTTP 前端已经提供了 /metrics。没有前端的入口点(python -m lmcache.v1.multiprocess.serverlmcache trace replay)没有 HTTP 前端,因此 它们--prometheus-port``(默认 9090)上提供 ``/metrics。有关详细信息,请参见 /metrics 的位置。无论如何,/metrics 在第一次存储/检索之前是空的 — 在您查找之前先驱动一些流量。

    The MP Coordinator follows the same embedded-HTTP pattern. In pull mode, lmcache coordinator exposes /metrics on --port (default 9300) and never starts a separate Prometheus server. Use the coordinator's --disable-metrics flag to disable metrics.

  • 推送模式 (OTLP)。 当设置 --otlp-endpoint 时,指标会被推送到 OpenTelemetry Collector,后者会重新暴露这些指标供 Prometheus 抓取。有关捆绑的 Collector + Prometheus + Grafana 堆栈,请参见 可观察性

    The Coordinator also accepts --otlp-endpoint. Its local /metrics route returns 404 in push mode, as it does when metrics are disabled.

所有指标使用 lmcache_mp. 前缀(多进程)。在 Prometheus 中,点被转换为下划线,计数器带有 _total 后缀(例如 lmcache_mp_l1_read_chunks_total);直方图增加单位后缀以及 _sum / _count / _bucket``(例如 ``lmcache_mp_l2_store_throughput_GB_per_second_sum)。

全局资源属性#

每个由 MP 服务器导出的指标和跨度都携带在启动时构建的资源级属性。这些属性标识生成遥测的进程,并与每个指标的属性(如 cache_salt)正交。

属性

命令行标志 / 配置

未设置时的默认值

service.instance.id

--instance-id / MPServerConfig.instance_id

在启动时生成的随机 UUID v4。

资源属性附加到 MeterProvider / TracerProvider 并通过 OTLP 传播到每个导出的数据点。在 Prometheus 中,SDK 资源属性出现在 target_info 系列上,而不是每个时间序列上——这是标准的 OTel 行为。

L1 指标#

指标

类型

描述

lmcache_mp.l1_read

计数器(属性:cache_salt

按租户分组的从 L1 读取的块数。

lmcache_mp.l1_write

计数器(属性:cache_salt

按租户分组写入 L1 的块数。

lmcache_mp.l1_evicted

计数器(属性:cache_salt

按租户分组的被 EvictionController 逐出的块数。

lmcache_mp.l1_eviction_loop_ticks

计数器

L1 逐出循环迭代(每个周期,无论水位线是否被跨越)。由 L1_EVICTION_LOOP_TICK 驱动。

lmcache_mp.l1_eviction_loop_triggered

计数器

L1 逐出循环迭代,其中 usage >= watermark 且逐出策略实际运行。这两个计数器区分“循环仍在运行”和“逐出已触发”——在调试完成速度快于 1 Hz 轮询周期的短期基准测试时,这一点非常重要。

L1 块生命周期直方图#

通过 L1LifecycleSubscriber 进行采样的(默认 1%)块级生命周期跟踪。只有采样的块会对直方图产生贡献;上述计数器始终计算所有事件。采样是确定性的(基于哈希),因此相同的键总是会得到相同的决策,且没有内存开销。

指标

类型

描述

lmcache_mp.l1_chunk_lifetime

直方图

每个采样块从分配到逐出的时间。

lmcache_mp.l1_chunk_idle_before_evict

直方图

每个采样块从最后访问到逐出的时间。

lmcache_mp.l1_chunk_reuse_gap

直方图

同一块的连续访问(读取或写入)之间的时间间隔。

lmcache_mp.l1_chunk_evict_reuse_gap

直方图

逐出到下次重用的时间(上限为 300 秒)。

存储管理器真实重用指标#

SMLifecycleSubscriber 发出的工作负载级重用直方图,由面向调用者的 StorageManager 事件(SM_READ_PREFETCHED_FINISHEDSM_WRITE_FINISHED)驱动。存储/预取控制器的内部读取锁释放被排除在外,因此信号仅反映用户驱动的访问。

这两个直方图都带有 cache_salt 标签,以实现租户隔离。每个块的每次读取和写入都会使对应 cache_salt 的访问计数器递增(无论是否采样),因此块间隔反映了真实的存储量;直方图本身仅记录通过(确定性、基于哈希的)采样门控的块的间隔。

指标

类型

描述

lmcache_mp.real_reuse_gap

直方图(标签:cache_salt

块的最后一次访问(读取或写入)与下一次读取之间的时间间隔。捕获存储成本——一个存储块在访问之间静止了多久。仅在读取事件中发出。

lmcache_mp.real_reuse_gap_objects

直方图(标签:cache_salt

同一块两次读取之间每 cache_salt 访问计数器的增量。反映存储量——在该块等待下次读取期间发生了多少次块访问。仅对采样块的读取事件发出。

L2 指标#

指标

类型

描述

lmcache_mp.l2_store_submitted

计数器

提交的 L2 存储请求数量。

lmcache_mp.l2_store_submitted_objects

计数器(属性:cache_salt

按租户分组的提交到 L2 存储的块数量。

lmcache_mp.l2_store_completed

计数器(属性:l2_name

按适配器类型标记的完成的 L2 存储请求数量。

lmcache_mp.l2_store_completed_objects

计数器(属性:cache_salt

按租户分组成功存储到 L2 的块数。

lmcache_mp.l2_prefetch_lookup

计数器

L2 预取查找请求的数量。

lmcache_mp.l2_prefetch_lookup_objects

计数器(属性:cache_salt

按租户分组的提交用于 L2 预取查找的块数量。

lmcache_mp.l2_prefetch_hit

计数器

在 L2 查找中找到的前缀块数量。

lmcache_mp.l2_prefetch_load_submitted

计数器

提交的 L2 预取加载请求数量。

lmcache_mp.l2_prefetch_load_submitted_objects

计数器(属性:cache_salt

按租户分组的提交用于 L2 加载的块数量。

lmcache_mp.l2_prefetch_load_completed

计数器(属性:cache_salt

按租户分组成功从 L2 加载的块数。

lmcache_mp.l2_load_completed

计数器(属性:l2_name

按适配器类型标记的每个适配器完成的 L2 加载请求数量。

lmcache_mp.l2_evicted_objects

计数器(属性:cache_salt

按租户分组的从 L2 逐出的块数量。

l2_name 标签的计数器 (l2_store_completedl2_load_completed) 存在的目的是为了让仪表板能够通过 rate(lmcache_mp_l2_store_completed_requests_total{l2_name=\"...\"}[1m]) 按需计算每个后端的 IOPS(加载的等效项也是如此)。没有单独导出 *_iops 指标;保留原始计数器让仪表板用户可以选择自己的时间窗口。

失败与健康计数器#

在专用的 lmcache_mp.health OTel 计量器上发出的健康监测计数器。由 L1FailureMetricsSubscriberL2FailureMetricsSubscriber 驱动,这两个订阅者在启用指标时会自动注册。所有三个计数器都携带 model_name(从每个 ObjectKey 中提取),以便操作员可以在 Prometheus /metrics 端点上按模型进行切片。

指标

类型

描述

lmcache_mp.l1_allocation_failure

计数器

reserve_write 期间的 L1 内存分配失败(OOM)。通过 during ∈ {l1_store, l2_prefetch} 标记,以区分用户发起的存储与预取触发的分配,以及 model_name

lmcache_mp.l1_read_failure

计数器

L1 reserve_read 失败。标记为 during ∈ {l2_store, l1_retrieve},reason ∈ {not_found, write_locked},加上 model_name后查找异常计数器,而非缓存未命中计数器——在 MP 模式下,reserve_read 仅在查找成功后调用,因此任何非零值均表明存在查找/预留竞争或意外逐出,健康运行时该值应接近零。

lmcache_mp.l2_prefetch_failure

计数器

L2 报告在查找时存在但未能进入 L1 的块。标记为 reason ∈ {l1_oom, not_found} 以及 model_namel1_oom 表示 L1 没有空间接收预取的对象;not_found 表示适配器在查找成功的情况下未返回数据(例如并发删除)。

一旦 L2 适配器区分反序列化错误和缺失对象,将会将 reason=serde_failure 值作为附加的、非破坏性的扩展添加到 l2_prefetch_failure 中——当这项功能上线时,无需进行仪表板迁移。

有关完整的设计原理(包括哪些事件类型驱动每个计数器以及为何推迟 lmcache_instance_id),请参阅源树中的 docs/design/v1/mp_observability/METRICS.md

查找命中率指标#

令牌级计数器,其比值表示通过查找请求的令牌中有多少比例是从 L1 或 L2 提供的。L0(GPU 前缀缓存)被有意排除在外——它由 vLLM 管理,无法从 LMCache 中观察到。

指标

类型

描述

lmcache_mp.lookup_requested

计数器(属性:model_name, cache_salt

提交查找的总令牌数(L1+L2 令牌级命中率的分母)。仅计算与块对齐的令牌。

lmcache_mp.lookup_hit

计数器(属性:model_name, cache_salt

在查找过程中在 L1 或 L2 中找到的总令牌数(L1+L2 令牌级命中率的分子)。仅计算连续前缀命中。

这两个计数器由同一事件(MP_LOOKUP_PREFETCH_END)驱动,因此它们在每次完成查找时总是一起增加。提前退出的查找对两者都贡献 0,而放弃的查找则不对两者贡献。

model_namecache_salt 属性在查找时从 IPCCacheServerKey 捕获,以便仪表板计算每个模型或每个租户的命中率。cache_salt 可能具有高基数(每个租户或隔离域一个条目);若存储成本敏感,请在抓取时通过 metric_relabel_configs 丢弃它。

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) 块生命周期直方图#

通过 L0LifecycleSubscriber 采样(默认 1%)GPU KV Cache 块生命周期跟踪。在重新分配时检测逐出(当块被分配不同的令牌时)。采样使用带有 _skipped 集的随机选择(受限于有限数量的物理 GPU 块)。

所有 L0 直方图都带有 instance_idmodel_name OTel 属性,从而在 Prometheus 中实现按实例和按模型的指标切片(例如 lmcache_mp_l0_block_lifetime_seconds{instance_id=\"12345\",model_name=\"llama-7b\"})。

指标

类型

描述

lmcache_mp.l0_block_lifetime

直方图

每个采样的 GPU 块从分配到逐出的时间。

lmcache_mp.l0_block_idle_before_evict

直方图

每个采样的 GPU 块从最后访问到逐出的时间。

lmcache_mp.l0_block_reuse_gap

直方图

同一 GPU 块连续访问之间的时间间隔。

L0 ↔ L1 吞吐量直方图#

每个请求的 GPU↔CPU 复制吞吐量通过 L0L1ThroughputSubscriber 进行统计。每个存储/检索请求为相应的直方图贡献一个样本:total_bytes / (end_ts - start_ts) 以 GB/s 为单位。时间戳来自在 GPU cupy 流上发布的 MP_{STORE,RETRIEVE}_{START,END} 事件,因此它们反映了真实的 GPU 流复制时间——而不是 Python/锁的开销。

所有吞吐量直方图都带有 engine_id(vLLM 工作实例 ID)、device(例如 "cuda:3")和 model_name OTel 属性,从而支持在 Prometheus 中按工作者、设备和模型进行切片(例如 lmcache_mp_l0_l1_store_throughput_GB_per_second{engine_id="0",device="cuda:3",model_name="meta-llama/Llama-3.1-8B"})。

指标

类型

描述

lmcache_mp.l0_l1_store_throughput

直方图

每个请求的 GPU→CPU (L0→L1) 存储吞吐量(单位:GB/s)。

lmcache_mp.l0_l1_load_throughput

直方图

每个请求的 CPU→GPU (L1→L0) 加载吞吐量(GB/s)。

MP Transfer Counters (in-flight GPU copies)#

Submitted / finished counter pairs for the LMCache-driven GPU transfers, via MPTransferCountersSubscriber. The MP_{STORE,RETRIEVE}_SUBMITTED events are published CPU-synchronously right before a copy is enqueued; the MP_{STORE,RETRIEVE}_END events are published from the GPU cupy stream once the copy has actually run. Subtracting the two gives the number of transfers currently in flight on each GPU.

All four counters carry exactly one attribute, device (e.g. "cuda:3"). This is deliberate: the SUBMITTED events carry no engine_id or model_name, so labeling the END side more richly would force a PromQL aggregation to line the two label sets back up before subtracting them. To slice GPU transfer activity by worker or model, use the L0 ↔ L1 throughput histograms or lmcache_mp.num_chunks_loaded instead.

指标

类型

描述

lmcache_mp.num_submitted_stores

Counter (attr: device)

GPU→CPU store transfers enqueued on the device stream, +1 per store() call that reaches the copy.

lmcache_mp.num_finished_stores

Counter (attr: device)

GPU→CPU store transfers completed on the device stream, +1 per completion.

lmcache_mp.num_submitted_retrieves

Counter (attr: device)

CPU→GPU retrieve transfers enqueued on the device stream, +1 per retrieve() call that reaches the copy.

lmcache_mp.num_finished_retrieves

Counter (attr: device)

CPU→GPU retrieve transfers completed on the device stream, +1 per completion.

PromQL for in-flight GPU copies:

# Stores currently on the device stream, per GPU:
lmcache_mp_num_submitted_stores_total
- lmcache_mp_num_finished_stores_total

# Retrieves currently on the device stream, per GPU:
lmcache_mp_num_submitted_retrieves_total
- lmcache_mp_num_finished_retrieves_total

# Store / retrieve rates per GPU (ops/sec):
rate(lmcache_mp_num_submitted_stores_total[1m])
rate(lmcache_mp_num_submitted_retrieves_total[1m])

What it answers: how many GPU KV copies is each device carrying right now? A submitted count that climbs while finished lags means the copy queue is backing up on that GPU; the two tracking together means the device is keeping pace.

备注

"Finished" counts a transfer leaving the device stream, not its success. A store that committed nothing (stored_count == 0) and a retrieve that missed both increment the finished counter. Counting only successes would strand a phantom in-flight transfer in the subtraction forever. Use lmcache_mp.num_chunks_loaded and the L0 ↔ L1 throughput histograms to measure what actually moved.

警告

The derived in-flight retrieve count can drift upward on one fail-closed path. retrieve() publishes MP_RETRIEVE_SUBMITTED before its block-id underflow check, and that check returns early without publishing MP_RETRIEVE_END, so each request hitting it permanently adds 1 to the difference. That path should never fire in healthy operation and logs at ERROR when it does — an in-flight floor that ratchets up alongside those log lines is the signature, not a stuck GPU. The store path publishes its sentinel after the equivalent check and is unaffected.

L1 ↔ L2 吞吐量直方图#

每个请求的 L1↔L2 传输吞吐量通过 L2ThroughputSubscriber 进行统计。存储路径通过 (adapter_index, task_id) 关联 L2_STORE_SUBMITTEDL2_STORE_COMPLETED。加载路径通过 (request_id, adapter_index) 关联每个适配器的 L2_LOAD_TASK_SUBMITTEDL2_LOAD_TASK_COMPLETED 事件;请求级别的 L2_PREFETCH_LOAD_* 事件用于块计数器在适配器之间进行聚合,无法归因于特定的 l2_name

时间戳跨度为 提交 → 完成,因此持续时间包括适配器队列、网络和磁盘 I/O — 该值为字节 / 端到端延迟,而非原始传输速率。使用这些直方图比较适配器类型并捕捉回归;当您需要纯粹的复制时间吞吐量时,请使用 L0↔L1 直方图。

所有 L1↔L2 吞吐量直方图都携带一个 l2_name OTel 属性——注册的适配器类型(例如 \"fs\", \"nixl_store\", \"mooncake_store\")——使得在 Prometheus 中能够按后端进行切片(例如 lmcache_mp_l2_store_throughput_GB_per_second{l2_name=\"nixl_store\"})。

指标

类型

描述

lmcache_mp.l2_store_throughput

直方图

每个请求的 L1→L2 存储吞吐量(GB/s)。

lmcache_mp.l2_load_throughput

直方图

每对(请求,适配器)的 L2→L1 加载吞吐量(单位:GB/s)。

每个后端的平均吞吐量(单位:GB/s)的 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]))

备注

l2_store_throughput 在写入块到 L2 时填充。 l2_load_throughput 仅在从 L2 读取块到 L1 时填充——即在条目从 L1 逐出后进行的预取加载。如果您的工作集完全适合 L1(在小模型或较大 --l1-size-gb 的情况下很常见),查找将从 L1 提供服务,尽管存储吞吐量非零,但加载直方图将保持为空。驱动足够不同的数据以强制 L1 逐出,或在存储和加载过程之间重启服务器,以测试 L2 加载路径。

引擎计数器#

与 MP 服务器通过 retrieve() 返回给每个 vLLM 工作线程相关的工作线程范围计数器。以 worker_id(vLLM 工作线程实例 ID)标记——与其他指标中可能出现的任何调度器范围 ID 不同。

指标

类型

描述

lmcache_mp.num_chunks_loaded

计数器(属性:worker_id, model_name, cache_salt

加载到引擎的 LMCache 块总数,汇总自所有 retrieve() 完成结果。可按工作者、模型及租户/隔离域(cache_salt)进行切片。cache_salt 可能具有高基数;若存储成本敏感,请在抓取时通过 metric_relabel_configs 丢弃它。

可观察仪表#

通过 register_gauge 注册的即时状态快照(基于拉取的 OTel 可观察仪表)。

这三个正在进行的指标携带两个属性,即使在与相同后端类型注册多个适配器时也能区分它们——与 lmcache_mp.l2_store_completed 具有相同形状:

  • l2_name — 注册的适配器类型(例如 "fs", "nixl_store", "mooncake_store")。

  • adapter_index — 控制器适配器列表中的位置。

没有正在进行的工作的适配器不会为该抓取发出数据点。

指标

类型

描述

lmcache_mp.active_prefetch_jobs

可观察仪表

当前正在进行的预取作业数量。持续的高值可能表示 L2 后端缓慢或轮询延迟。

lmcache_mp.l1_memory_usage_bytes

可观察仪表

当前在 L1 中占用的字节数。持续上升而没有平台期通常表示存在泄漏;在配置的 --l1-size-gb 达到饱和时表示工作集超过了容量。

lmcache_mp.l1_usage_ratio

可观察仪表

L1 使用/总比率 (0.01.0),在抓取时从 L1Manager.get_memory_usage() 采样。当仪表目标尚未连接或 total_bytes 为零时返回 0.0,因此在抓取期间回调不会触发。与逐出水位线 (默认 0.8) 进行比较,以判断逐出循环是否低于或高于触发阈值。

lmcache_mp.l2_usage_bytes

可观察仪表 (属性: l2_name)

每个 L2 适配器当前持有的字节数,在抓取时从 adapter.get_usage() 采样。每个已配置的适配器对应一个观测值,通过 l2_name 标记(适配器类型,例如 "fs""nixl_store""mooncake_store")。它是 L2 层中与 l1_memory_usage_bytes 对应的指标——用于查看每个 L2 后端当前持有多少数据。get_usage() 抛出异常的适配器会被静默跳过,而不会污染观测结果,因此某个 l2_name 缺失数据点可能意味着“未配置”或“该次抓取时适配器发生错误”——请与 L2 存储/加载计数器交叉核对。

lmcache_mp.num_inflight_l2_stores

ObservableGauge (attrs: l2_name, adapter_index)

每个适配器当前正在执行的 L2 存储任务。 持续的非零值表明适配器无法跟上 L1 → L2 写入速率。

lmcache_mp.num_inflight_l2_loads

ObservableGauge (attrs: l2_name, adapter_index)

每个适配器当前正在执行的 L2 → L1 预取加载任务。与 num_inflight_l2_stores 配对,以查看给定后端是读流量还是写流量占主导。

lmcache_mp.inflight_load_memory_usage_bytes

ObservableGauge (attrs: l2_name, adapter_index)

每个适配器由正在进行的 L2 → L1 预取加载所预留的 L1 字节数。若正在进行的字节数与 l1_memory_usage_bytes 同步上升,则表明预取预留正在挤占可缓存数据的空间。每个适配器的字节归属遵循每个请求的 load_plan 位图,因此跨适配器求和时不会重复计数。

事件总线自我监控#

EventBus 本身的健康指标,由 EventBusSelfMetricsSubscriberlmcache.event_bus OTel 计量器上注册。这些指标通过 EventBus 访问器直接观察总线状态,并在每次 OTel 抓取时报告——它们不是由事件驱动的,因此丢弃或失败的订阅者无法使其静默。

使用它们来回答:EventBus 是否能跟上发布者的速度,是否有事件被丢弃,是否有订阅者回调抛出异常?非零的 dropped_events_total 或持续非零的 drain_lag_seconds 表明总线队列已达 --event-bus-queue-size 上限并正在进行尾部丢弃;请调大该参数值或排查慢速订阅者。

指标

类型

描述

lmcache_mp.event_bus.queue_depth

可观察仪表

事件总线中当前排队的事件(在抓取时的 len(_queue))。

lmcache_mp.event_bus.drain_lag_seconds

可观察仪表

自最旧的排队事件发布以来的秒数;队列为空时为 0.0。值持续上升意味着排空线程处理速度跟不上。

lmcache_mp.event_bus.dropped_events_total

可观察计数器

因 EventBus 队列达到 --event-bus-queue-size 而累计丢弃的事件。

lmcache_mp.event_bus.subscriber_exceptions

可观察计数器 (属性: subscriber_name)

由事件总线分发期间由订阅者回调引发的累计异常,按 subscriber_name 标记(对于绑定方法为失败回调的拥有类,对于自由函数为 __qualname__)。

有关完整的设计原理以及支持每个指标的进程内访问器,请参阅源代码树中的 docs/design/v1/mp_observability/METRICS.mddocs/design/v1/mp_observability/event-bus.md

Prometheus 抓取配置#

拉取模式**(没有 ``--otlp-endpoint``)下,将 Prometheus 指向服务器的 HTTP 前端端口 — ``--http-port``,默认 **8080 — 而不是 --prometheus-port

scrape_configs:
  - job_name: "lmcache-mp"
    static_configs:
      - targets: ["<lmcache-host>:8080"]   # --http-port, NOT --prometheus-port

推送模式 (设置了 --otlp-endpoint) 下,服务器不会直接暴露 /metrics;请改为抓取 OpenTelemetry Collector 的 Prometheus 导出器。examples/observability/ 中捆绑的堆栈为您配置了这一点 — 请参阅 可观察性