指标#
指标通过 OpenTelemetry 收集,并根据是否设置了 --otlp-endpoint 以两种方式之一提供给 Prometheus:
拉取模式(默认,无收集器)。 当
--otlp-endpoint未 设置时,服务器会发布一个 Prometheus/metrics端点,Prometheus 直接抓取该端点。重要
对于
lmcache server,/metrics由 HTTP 前端 在--http-port``(默认 **8080**)上提供,例如 ``http://<host>:8080/metrics— 不 在--prometheus-port上。--prometheus-port被lmcache server忽略:独立的 Prometheus HTTP 服务器被禁用,因为 HTTP 前端已经提供了/metrics。没有前端的入口点(python -m lmcache.v1.multiprocess.server和lmcache trace replay)没有 HTTP 前端,因此 它们 在--prometheus-port``(默认 9090)上提供 ``/metrics。有关详细信息,请参见 /metrics 的位置。无论如何,/metrics在第一次存储/检索之前是空的 — 在您查找之前先驱动一些流量。The MP Coordinator follows the same embedded-HTTP pattern. In pull mode,
lmcache coordinatorexposes/metricson--port(default 9300) and never starts a separate Prometheus server. Use the coordinator's--disable-metricsflag to disable metrics.推送模式 (OTLP)。 当设置
--otlp-endpoint时,指标会被推送到 OpenTelemetry Collector,后者会重新暴露这些指标供 Prometheus 抓取。有关捆绑的 Collector + Prometheus + Grafana 堆栈,请参见 可观察性。The Coordinator also accepts
--otlp-endpoint. Its local/metricsroute 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)正交。
属性 |
命令行标志 / 配置 |
未设置时的默认值 |
|---|---|---|
|
|
在启动时生成的随机 UUID v4。 |
资源属性附加到 MeterProvider / TracerProvider 并通过 OTLP 传播到每个导出的数据点。在 Prometheus 中,SDK 资源属性出现在 target_info 系列上,而不是每个时间序列上——这是标准的 OTel 行为。
L1 指标#
指标 |
类型 |
描述 |
|---|---|---|
|
计数器(属性: |
按租户分组的从 L1 读取的块数。 |
|
计数器(属性: |
按租户分组写入 L1 的块数。 |
|
计数器(属性: |
按租户分组的被 EvictionController 逐出的块数。 |
|
计数器 |
L1 逐出循环迭代(每个周期,无论水位线是否被跨越)。由 |
|
计数器 |
L1 逐出循环迭代,其中 |
L1 块生命周期直方图#
通过 L1LifecycleSubscriber 进行采样的(默认 1%)块级生命周期跟踪。只有采样的块会对直方图产生贡献;上述计数器始终计算所有事件。采样是确定性的(基于哈希),因此相同的键总是会得到相同的决策,且没有内存开销。
指标 |
类型 |
描述 |
|---|---|---|
|
直方图 |
每个采样块从分配到逐出的时间。 |
|
直方图 |
每个采样块从最后访问到逐出的时间。 |
|
直方图 |
同一块的连续访问(读取或写入)之间的时间间隔。 |
|
直方图 |
逐出到下次重用的时间(上限为 300 秒)。 |
存储管理器真实重用指标#
由 SMLifecycleSubscriber 发出的工作负载级重用直方图,由面向调用者的 StorageManager 事件(SM_READ_PREFETCHED_FINISHED,SM_WRITE_FINISHED)驱动。存储/预取控制器的内部读取锁释放被排除在外,因此信号仅反映用户驱动的访问。
这两个直方图都带有 cache_salt 标签,以实现租户隔离。每个块的每次读取和写入都会使对应 cache_salt 的访问计数器递增(无论是否采样),因此块间隔反映了真实的存储量;直方图本身仅记录通过(确定性、基于哈希的)采样门控的块的间隔。
指标 |
类型 |
描述 |
|---|---|---|
|
直方图(标签: |
块的最后一次访问(读取或写入)与下一次读取之间的时间间隔。捕获存储成本——一个存储块在访问之间静止了多久。仅在读取事件中发出。 |
|
直方图(标签: |
同一块两次读取之间每 |
L2 指标#
指标 |
类型 |
描述 |
|---|---|---|
|
计数器 |
提交的 L2 存储请求数量。 |
|
计数器(属性: |
按租户分组的提交到 L2 存储的块数量。 |
|
计数器(属性: |
按适配器类型标记的完成的 L2 存储请求数量。 |
|
计数器(属性: |
按租户分组成功存储到 L2 的块数。 |
|
计数器 |
L2 预取查找请求的数量。 |
|
计数器(属性: |
按租户分组的提交用于 L2 预取查找的块数量。 |
|
计数器 |
在 L2 查找中找到的前缀块数量。 |
|
计数器 |
提交的 L2 预取加载请求数量。 |
|
计数器(属性: |
按租户分组的提交用于 L2 加载的块数量。 |
|
计数器(属性: |
按租户分组成功从 L2 加载的块数。 |
|
计数器(属性: |
按适配器类型标记的每个适配器完成的 L2 加载请求数量。 |
|
计数器(属性: |
按租户分组的从 L2 逐出的块数量。 |
l2_name 标签的计数器 (l2_store_completed 和 l2_load_completed) 存在的目的是为了让仪表板能够通过 rate(lmcache_mp_l2_store_completed_requests_total{l2_name=\"...\"}[1m]) 按需计算每个后端的 IOPS(加载的等效项也是如此)。没有单独导出 *_iops 指标;保留原始计数器让仪表板用户可以选择自己的时间窗口。
失败与健康计数器#
在专用的 lmcache_mp.health OTel 计量器上发出的健康监测计数器。由 L1FailureMetricsSubscriber 和 L2FailureMetricsSubscriber 驱动,这两个订阅者在启用指标时会自动注册。所有三个计数器都携带 model_name(从每个 ObjectKey 中提取),以便操作员可以在 Prometheus /metrics 端点上按模型进行切片。
指标 |
类型 |
描述 |
|---|---|---|
|
计数器 |
在 |
|
计数器 |
L1 |
|
计数器 |
L2 报告在查找时存在但未能进入 L1 的块。标记为 |
一旦 L2 适配器区分反序列化错误和缺失对象,将会将 reason=serde_failure 值作为附加的、非破坏性的扩展添加到 l2_prefetch_failure 中——当这项功能上线时,无需进行仪表板迁移。
有关完整的设计原理(包括哪些事件类型驱动每个计数器以及为何推迟 lmcache_instance_id),请参阅源树中的 docs/design/v1/mp_observability/METRICS.md。
查找命中率指标#
令牌级计数器,其比值表示通过查找请求的令牌中有多少比例是从 L1 或 L2 提供的。L0(GPU 前缀缓存)被有意排除在外——它由 vLLM 管理,无法从 LMCache 中观察到。
指标 |
类型 |
描述 |
|---|---|---|
|
计数器(属性: |
提交查找的总令牌数(L1+L2 令牌级命中率的分母)。仅计算与块对齐的令牌。 |
|
计数器(属性: |
在查找过程中在 L1 或 L2 中找到的总令牌数(L1+L2 令牌级命中率的分子)。仅计算连续前缀命中。 |
这两个计数器由同一事件(MP_LOOKUP_PREFETCH_END)驱动,因此它们在每次完成查找时总是一起增加。提前退出的查找对两者都贡献 0,而放弃的查找则不对两者贡献。
model_name 和 cache_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_id 和 model_name OTel 属性,从而在 Prometheus 中实现按实例和按模型的指标切片(例如 lmcache_mp_l0_block_lifetime_seconds{instance_id=\"12345\",model_name=\"llama-7b\"})。
指标 |
类型 |
描述 |
|---|---|---|
|
直方图 |
每个采样的 GPU 块从分配到逐出的时间。 |
|
直方图 |
每个采样的 GPU 块从最后访问到逐出的时间。 |
|
直方图 |
同一 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"})。
指标 |
类型 |
描述 |
|---|---|---|
|
直方图 |
每个请求的 GPU→CPU (L0→L1) 存储吞吐量(单位:GB/s)。 |
|
直方图 |
每个请求的 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.
指标 |
类型 |
描述 |
|---|---|---|
|
Counter (attr: |
GPU→CPU store transfers enqueued on the device stream, +1 per
|
|
Counter (attr: |
GPU→CPU store transfers completed on the device stream, +1 per completion. |
|
Counter (attr: |
CPU→GPU retrieve transfers enqueued on the device stream, +1 per
|
|
Counter (attr: |
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_SUBMITTED → L2_STORE_COMPLETED。加载路径通过 (request_id, adapter_index) 关联每个适配器的 L2_LOAD_TASK_SUBMITTED → L2_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\"})。
指标 |
类型 |
描述 |
|---|---|---|
|
直方图 |
每个请求的 L1→L2 存储吞吐量(GB/s)。 |
|
直方图 |
每对(请求,适配器)的 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 块总数,汇总自所有 |
可观察仪表#
通过 register_gauge 注册的即时状态快照(基于拉取的 OTel 可观察仪表)。
这三个正在进行的指标携带两个属性,即使在与相同后端类型注册多个适配器时也能区分它们——与 lmcache_mp.l2_store_completed 具有相同形状:
l2_name— 注册的适配器类型(例如"fs","nixl_store","mooncake_store")。adapter_index— 控制器适配器列表中的位置。
没有正在进行的工作的适配器不会为该抓取发出数据点。
指标 |
类型 |
描述 |
|---|---|---|
|
可观察仪表 |
当前正在进行的预取作业数量。持续的高值可能表示 L2 后端缓慢或轮询延迟。 |
|
可观察仪表 |
当前在 L1 中占用的字节数。持续上升而没有平台期通常表示存在泄漏;在配置的 |
|
可观察仪表 |
L1 使用/总比率 ( |
|
可观察仪表 (属性: |
每个 L2 适配器当前持有的字节数,在抓取时从 |
|
ObservableGauge (attrs: |
每个适配器当前正在执行的 L2 存储任务。 持续的非零值表明适配器无法跟上 L1 → L2 写入速率。 |
|
ObservableGauge (attrs: |
每个适配器当前正在执行的 L2 → L1 预取加载任务。与 |
|
ObservableGauge (attrs: |
每个适配器由正在进行的 L2 → L1 预取加载所预留的 L1 字节数。若正在进行的字节数与 |
事件总线自我监控#
EventBus 本身的健康指标,由 EventBusSelfMetricsSubscriber 在 lmcache.event_bus OTel 计量器上注册。这些指标通过 EventBus 访问器直接观察总线状态,并在每次 OTel 抓取时报告——它们不是由事件驱动的,因此丢弃或失败的订阅者无法使其静默。
使用它们来回答:EventBus 是否能跟上发布者的速度,是否有事件被丢弃,是否有订阅者回调抛出异常?非零的 dropped_events_total 或持续非零的 drain_lag_seconds 表明总线队列已达 --event-bus-queue-size 上限并正在进行尾部丢弃;请调大该参数值或排查慢速订阅者。
指标 |
类型 |
描述 |
|---|---|---|
|
可观察仪表 |
事件总线中当前排队的事件(在抓取时的 |
|
可观察仪表 |
自最旧的排队事件发布以来的秒数;队列为空时为 |
|
可观察计数器 |
因 EventBus 队列达到 |
|
可观察计数器 (属性: |
由事件总线分发期间由订阅者回调引发的累计异常,按 |
有关完整的设计原理以及支持每个指标的进程内访问器,请参阅源代码树中的 docs/design/v1/mp_observability/METRICS.md 和 docs/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/ 中捆绑的堆栈为您配置了这一点 — 请参阅 可观察性。