用于自托管 LLM 调优的 vLLM Prometheus 指标:TTFT、KV Cache 和 GPU 利用率

作者:来自 Elastic Bahubali Shetti

使用 Elastic 可观测性中的 Prometheus 指标调优自托管的 vLLM 推理 ------ TTFT、KV Cache、前缀缓存和 DCGM GPU 计数器

你们公司某个地方一定有一个团队无法使用 Claude 、GPT 或 Gemini ------ 不是因为他们不想用,而是因为他们的数据不允许离开某个司法管辖区、网络边界,或者受到合同限制。理赔文件、患者记录、受出口管制制度约束的源代码,等等。

这个团队仍然需要一个模型。所以请求就落到了 SRE 的桌面上,听起来出奇地简单:"你能不能为理赔团队部署一个开放权重模型?60 个人使用。必须运行在我们的硬件上。" 他们甚至不允许使用新云服务商。这里面确实存在成本,但我们不会讨论这一部分。我们只关注运行模型以及观测配置的部分。

部署起来是比较容易的一半。四个清单文件,一个下午,你就可以让模型回答问题。困难的一周后才会出现:有人说"感觉很慢",而你意识到自己完全不知道这个部署的配置是好、是坏,还是糟糕到灾难性的程度------也没有明显的方法可以找出答案。

本指南将介绍如何使用 Elastic 可观测性帮助你分析配置产生的指标。我们将使用 vLLM 已经提供的指标,对一个真实的 vLLM 部署进行调优。vLLM 会在 /metrics 端点以 Prometheus 展示格式公开这些指标------无需插桩、无需 sidecar、无需修改代码------这也是为什么本指南中的每个查询都从 Prometheus 抓取开始。目标是:将"感觉很慢"转化为一个具体且有充分依据的决策。

测试环境:使用 NVIDIA A10G 在 Kubernetes 集群上运行 vLLM,并配合 dcgm-exporter 和 Prometheus 指标

本指南中的每一项数据都是在以下技术栈上测量的------一个副本、一块 GPU、没有自动扩缩容。

  • 工作负载------原生 Kubernetes 负载生成器,并发请求数从 8 增加到 32。

  • 模型 ------Qwen/Qwen2.5-3B-Instruct,bf16,--max-model-len 4096

  • 引擎 ------vLLM v0.23.0,兼容 OpenAI 的服务器,Prometheus /metrics 位于 :8000

  • GPU ------NVIDIA A10G,24 GB------一台 AWS g5.xlarge

  • 集群 ------Amazon EKS 1.30,带污点的 GPU 节点池,minSize: 0

  • 遥测 ------Prometheus 每 15 秒抓取一次,另外使用 dcgm-exporter:9400 上提供指标,并通过 remote_write 发送

  • 分析------Elastic 可观测性,使用 ES|QL 和 PromQL 进行查询

  • 测量时间------2026-07-27

为什么 SRE 很难自托管并调优开放权重 LLM?

困难不在于部署,而在于调优,因为调优没有反馈闭环。 无论配置得非常出色还是极其浪费,vLLM 都会启动、提供服务并报告成功。没有任何东西会告诉你到底是哪一种情况。

加载模型时,你的清单文件会包含以下配置:

yaml 复制代码
 `1.        containers:
2.          - name: vllm
3.            image: vllm/vllm-openai:v0.23.0  # pin an exact release --- metric names shift between versions
4.            args:
5.              - "--model=Qwen/Qwen2.5-3B-Instruct"
6.              - "--max-model-len=4096"         # cap context → predictable KV-cache size
7.              # A10G has native bf16 --- do NOT add --dtype=half (T4-only).
8.            ports:
9.              - name: http
10.                containerPort: 8000            # OpenAI API + /metrics`Lobster AI![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

但你可能会遇到一些具体问题,例如:Hugging Face 下载需要几分钟。如果你的集群要求服务器在 30 秒内启动,它会认为应用已经失效,并在下载过程中将其终止,让你陷入无限崩溃循环。

或者你可能遇到硬件不匹配的问题,由于硬件架构各不相同,这可能会降低模型的速度或精度。

或者还有大量其他问题。

模型终于运行起来之后,优化性能就变成了完全靠猜,因为默认指标无法告诉你是否正在高效运行。

你可以使用 nvidia-smi,但它只了解原始硬件状态,而不了解应用软件逻辑。

现在模型已经运行起来了,几个小时,甚至一天之后,团队说:"感觉很慢。"实际上,你是在盲目调优。

如何调优自托管的 vLLM 部署?

你不是推理工程师,除了这个服务之外,你还负责另外 40 个服务,而且没有模型供应商派驻的工程师随时待命。但事实证明,调优 LLM 服务器恰恰需要一个你已经具备的技能:读取遥测数据并分析饱和度。 唯一缺少的是有实际意义的遥测数据。

它确实存在。vLLM 开箱即用地提供了丰富的 Prometheus 端点------按照推理阶段分解的延迟、缓存命中率、批次占用率、令牌统计、完成结果。几乎没有人查看这些指标。本指南接下来的内容就是介绍如何读取这些指标

定义工作负载:60 个用户、短提示词、流式响应

检测到并报告响应缓慢之后,你收集用户的使用情况。其使用模式如下:

  • 约 60 个用户,但不是同时使用。 实际峰值为 8--12 个同时处于处理中的请求;持续并发量更低。

  • 短提示词、长回答。 用户粘贴一段文字,并要求生成结构化摘要。提示词约 50 个令牌;有用的回答约 500--1,000 个令牌。

  • 交互式流式 UI。 用户感知的速度主要由**首令牌时间(TTFT)**决定,而不是总时间------这与聊天界面的用户心理预期相同。

  • 大量提示词复用。 每个请求都包含相同的系统提示词以及相同的策略语言模板。

基于这些情况,你制定了实际的服务目标------这是大多数自托管 LLM 项目都会跳过的一步:

  • TTFT p95 < 300 ms

  • 令牌间延迟 < 50 ms(≥ 20 个令牌/秒,比阅读速度更快)

  • 12 个并发请求时零排队

  • 错误 + 中止率 < 0.5%

这四个数字是后续所有内容的核心。没有它们,"感觉很慢"就没有答案。有了它们,下面的每个指标都可以根据一个明确的标准判断通过或失败。

如何从 vLLM 和 GPU 获取 Prometheus 指标?

有两个来源,而且两者都需要。

  • vLLM 报告自身的运行情况------包括按阶段划分的延迟、缓存命中率、批次占用率、令牌数量------通过服务端口上的 /metrics 提供,无需适配器,也无需进行插桩工作。

  • GPU 则通过 NVIDIA 的 dcgm-exporter:9400 上单独报告指标(NVIDIA Data Center GPU Manager(DCGM)是一套工具和库,旨在全面管理、监控和诊断集群和数据中心中的企业级 NVIDIA GPU)。vLLM 告诉你_推理引擎_认为正在发生什么;DCGM 告诉你_显卡_实际上正在做什么。第 6 步完全建立在这两个答案之间的差异之上。

在这个 EKS 集群中,这意味着三个组件并行运行:

  • vLLM 作为一个普通的 Deployment,运行在带污点的 g5.xlarge GPU 节点池上。对于一张显卡上的一个模型而言,一个 Deployment 和一个 Service 就构成了完整架构。

  • dcgm-exporter 作为一个 DaemonSet,固定运行在相同的 GPU 节点上。

  • 一个 Prometheus 服务器运行在 CPU 节点上,每 15 秒抓取这两个端点。

这里没有任何 AWS 特有的内容。生产版本使用的是相同的清单文件,只不过运行在配备 L40S 或 H100 节点的本地部署集群上------这正是选择在 Kubernetes 上完成这项工作的意义所在,而不是使用供应商的平台。

KServe 和 llm-d 怎么样?

**本指南没有运行这两者,而且它们都不会改变指标的来源。**KServe 和 llm-d 位于 vLLM 之上 ,而不是取代 vLLM ------ vLLM 仍然是推理引擎,因此/metrics仍然是这里每个数据的来源。每一个都会在上面增加自己的层(KServe:自动扩缩容和版本指标;llm-d:路由器和缓存路由指标),但底层的推理遥测数据完全相同。

它们改变的是你什么时候需要它们 ------ 而且每次升级都是由你已经在收集的某个指标触发的:

如何将 vLLM 指标和 DCGM 指标发送到 Observability

**集群内的 Prometheus 抓取只能保存几个小时的数据 ------ 这些指标必须进入一个你下周仍然可以查询的数据存储。**有两种方式可以做到这一点,而且它们并不等价。

**路径 A ------ OpenTelemetry Collector。**将推理指标放入与你的 traces 和 logs 相同的流水线中。一个 Collector、一条认证路径、一套思维模型。代价是,通过 OTLP 发送的 Prometheus 指标会被标准化:最终存储的 schema 既不是纯粹的 Prometheus,也不是纯粹的 OTel,而且指标名称也会发生变化。

**路径 B ------ 原生 Prometheus remote_write。**部署一个小型 Prometheus,同时抓取两个端点的数据,然后将数据推送到支持 remote-write 协议的后端。名称和标签保持不变,_sum / _count / _bucket 直方图部分也会保持完整,现有查询可以继续使用。

**对于调优实验,请选择路径 B。**本指南中的每一个数据都是通过这种方式得到的。原因很简单,但非常关键:调优意味着需要与 vLLM 文档和 vLLM 社区进行对照,而两者使用的都是精确的指标名称。当你的图表显示vllm:kv_cache_usage_perc时,你可以直接搜索它。

部署需要两个 YAML 文件------一个包含两个抓取任务和一个remote_write代码块的 Prometheus Deployment,以及一个保存后端凭证的 Secret。在这个构建中,目标是一个 Elastic Serverless 项目,它提供 Prometheus remote-write 端点,并将数据写入一个时间序列数据流metrics-vllm.prometheus-inference

有两件事花费了我大量实际时间。如果你的后端为 OTLP 和主 API 分别提供了不同的 ingest 主机,那么 remote-write 通常位于主 API 主机 上,而不是 ingest 主机上------指向错误的主机会返回 404,看起来像是路径错误。而且凭证需要索引写入权限 ,而不仅仅是 ingest 认证权限;一个能够正常用于 OTLP 的密钥可能会成功通过认证,然后对每个样本都返回 403。在查看其他任何地方之前,先检查 Prometheus 自身的prometheus_remote_storage_samples_failed_total

如何确认 vLLM 指标已经写入 Elastic?

流水线启动后,查看字段列表。单个 vLLM Pod 加上 DCGM 大约会产生 127 个指标序列

这个页面比看起来更有用。扫描字段列表可以确认你的 vLLM 版本所使用的确切指标名称------不同主要版本的 vLLM 之间,指标名称确实会发生变化,而针对错误名称构建的仪表板不会报错,而是静默地返回空结果,因此会失效。

查询 vLLM 指标时的两个注意事项

这两种情况都会产生类似于"指标没有正常工作"的结果,而实际上整个数据流水线完全正常。

vLLM 指标名称包含冒号vllm:num_requests_running),因此在大多数查询语言中都需要进行转义。更隐蔽的是,如果你在多个指标之间按照指标_名称_进行过滤,然后只聚合其中一个指标,你仍然会得到结果行------其中充满 null,但不会报错。每个 Prometheus 指标都会写入自己的字段,因此字段名称本身就是过滤条件;你根本不需要指标名称谓词。

计数器需要使用速率函数,而仪表指标不需要。 vllm:generation_tokens_total 是累积且单调递增的------对它取最大值只能得到 Pod 的生命周期总量,而不是吞吐量。像 vllm:num_requests_runningvllm:num_requests_waitingvllm:kv_cache_usage_perc 这样的仪表指标是瞬时值,应该使用最大值或平均值。把这两类指标混淆,会产生错误但看起来合理的图表,这比得到空图表要糟糕得多。

哪些 vLLM Prometheus 指标真正重要?

下面是本指南中使用的指标参考,包括每个指标能告诉你什么,以及值得关注的情况。名称保持 vLLM 实际输出的形式;DCGM_FI_*系列来自dcgm-exporter

指标 类型 它告诉你什么 需要关注什么
vllm:time_to_first_token_seconds 直方图 TTFT------首个 token 开始流式传输前需要多长时间 p95 高于你的交互性能标准(这里为 300 ms)
vllm:inter_token_latency_seconds 直方图 首个 token 之后的流式传输速度 高于约 50 ms 意味着速度低于阅读速度
vllm:e2e_request_latency_seconds 直方图 请求总耗时 TTFT 保持稳定但该值上升 = 解码或工作负载发生变化
vllm:request_queue_time_seconds 直方图 等待接入所花费的时间 最早的饱和信号------任何持续上升都值得关注
vllm:request_prefill_time_seconds 直方图 处理提示词所花费的时间 占主要比例 = 预填充受限的工作负载
vllm:request_decode_time_seconds 直方图 生成 token 所花费的时间 占主要比例 = 内存带宽受限
vllm:num_requests_running 仪表 当前正在解码的请求数 批处理占用情况
vllm:num_requests_waiting 仪表 等待接入的排队请求数 持续非零 = 增加一个副本
vllm:kv_cache_usage_perc 仪表 KV block pool 的占用率------不是 VRAM 自动扩缩容触发条件(约 60%)
vllm:prompt_tokens_total + vllm:prompt_tokens_cached_total 计数器 前缀缓存命中率 下降意味着路由将前缀分散到了不同位置
vllm:generation_tokens_total 计数器 输出吞吐量,单位为 tokens/sec 核心吞吐量指标
vllm:request_prompt_tokens + vllm:request_generation_tokens 直方图 每个请求的 token 数量;两者的比值反映工作负载的形态 比值发生变化意味着工作负载的特征发生了变化
vllm:iteration_tokens_total 直方图 每次前向传播处理的 token 数量 并发情况下接近 1.0 = 批处理失效
vllm:request_success_total{finished_reason} 计数器 完成结果 error / abort = SLO;length占比 = 发生截断
http_requests_total{status} 计数器 服务端请求数量 捕获 4xx 和格式错误的请求,而这些请求不会被vllm:*指标看到
DCGM_FI_DEV_FB_USED / _FB_FREE 仪表 实际 VRAM 仅用于容量规划------绝不要基于它设置告警
DCGM_FI_DEV_GPU_UTIL 仪表 "有一个 kernel 正驻留" 不是有效工作量的衡量指标
DCGM_FI_PROF_PIPE_TENSOR_ACTIVE 仪表 Tensor Core 活动情况 这里较低 + DRAM 较高 = 受内存限制
DCGM_FI_PROF_DRAM_ACTIVE 仪表 内存带宽活动情况 较高 = 瓶颈在带宽
DCGM_FI_DEV_POWER_USAGE 仪表 功耗,单位为瓦特 与吞吐量结合,用于计算每瓦特 token 数

步骤 1:vLLM 的延迟都花在哪里?分解 TTFT、预填充和解码

**在优化任何东西之前,先将总延迟分解为排队、预填充和解码。**vLLM 会分别报告这三个阶段,而它们对应的解决方法完全不同。这是整个配置中最有价值的一张图表。

该查询会在同一个时间窗口内,以请求数量为基准,对每个阶段的累计耗时取平均值------用 Prometheus 的术语来说,就是rate(vllm:request_prefill_time_seconds_sum) / rate(vllm:e2e_request_latency_seconds_count),排队和解码也是如此:

less 复制代码
``

1.  TS metrics-vllm.prometheus-inference
2.  | WHERE @timestamp > NOW() - 30 minutes
3.  | STATS reqs = SUM(RATE(`metrics.vllm:e2e_request_latency_seconds_count`)),
4.          q_s  = SUM(RATE(`metrics.vllm:request_queue_time_seconds_sum`)),
5.          pf_s = SUM(RATE(`metrics.vllm:request_prefill_time_seconds_sum`)),
6.          dc_s = SUM(RATE(`metrics.vllm:request_decode_time_seconds_sum`))
7.      BY minute = BUCKET(@timestamp, 1 minute)
8.  | EVAL queue_ms   = ROUND(q_s  / reqs * 1000, 2),
9.         prefill_ms = ROUND(pf_s / reqs * 1000, 1),
10.         decode_ms  = ROUND(dc_s / reqs * 1000, 1)
11.  | KEEP minute, queue_ms, prefill_ms, decode_ms
12.  | SORT minute ASC

``Lobster AI![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

在 A10G 上运行 8 个并发请求时:

yaml 复制代码
`

1.  minute    | queue_ms | prefill_ms | decode_ms | e2e_ms  | ttft_ms | inter_token_ms
2.  22:55:00  | 0.01     | 37.98      | 1664.69   | 1715.18 | 50.81   | 16.23
3.  22:56:00  | 0.01     | 40.99      | 1670.44   | 1723.78 | 53.50   | 16.20

`Lobster AI

根据目标来解读:

  • TTFT 是 51 ms,而目标是 300 ms。 达标,并且还有接近 6 倍的余量。感知响应速度并不是问题,不管会议上当时怎么说。

  • Token 间延迟是 16 ms,即约 62 tokens/sec,目标是 50 ms / 20 tok/s。文本到达的速度大约是人类阅读速度的 3 倍。

  • 队列时间是 0.01 ms。 没有请求在等待准入;在这个并发量下,推理引擎还有充足的容量。

  • Decode 是 1,665 ms,而 Prefill 只有 38 ms------97% 的时间都花在 Decode 上。

最后这一点才是真正的发现。针对 Prefill 的任何优化对这个工作负载都没有价值。 Chunked Prefill、Prompt 压缩、针对 长上下文 更快的 Attention Kernel------这些都是实际存在的优化技术,但对于这个工作负载都无关紧要,因为 Prefill 只占 2% 的时间。Decode 受内存带宽限制,因此真正能够带来改善的手段是量化、跨两张卡进行 Tensor Parallelism,或者使用更小的模型。一张图表就排除了错误的优化方案。

重点关注 vllm:request_queue_time_seconds 它是整个 vLLM 指标集合中最早出现的饱和信号------队列时间会在 vllm:num_requests_waiting 明显出现非零之前就开始上升,因为请求可能只等待几毫秒就获得准入,在 Prometheus 抓取时刻甚至不会被记录为处于队列中。如果这一部分只设置一个告警,就应该设置为当队列时间超过一个较小的绝对阈值时触发。

第 2 步:vLLM 是否在高效地使用 GPU?KV Cache、Prefix Caching 和 Batch Occupancy

四个指标可以回答这个问题:Prefix Cache 命中率、每次迭代处理的 Token 数量、KV Cache 占用率,以及运行中的请求与等待中的请求数量。 延迟告诉你用户体验是否良好;这些指标则告诉你,为此付出的成本是否过高。

ini 复制代码
``

1.  TS metrics-vllm.prometheus-inference
2.  | WHERE @timestamp > NOW() - 30 minutes
3.  | STATS ptok    = SUM(RATE(`metrics.vllm:prompt_tokens_total`)),
4.          cached  = SUM(RATE(`metrics.vllm:prompt_tokens_cached_total`)),
5.          gen     = SUM(RATE(`metrics.vllm:generation_tokens_total`)),
6.          it_s    = SUM(RATE(`metrics.vllm:iteration_tokens_total_sum`)),
7.          it_c    = SUM(RATE(`metrics.vllm:iteration_tokens_total_count`)),
8.          running = MAX(`metrics.vllm:num_requests_running`),
9.          waiting = MAX(`metrics.vllm:num_requests_waiting`),
10.          kv      = MAX(`metrics.vllm:kv_cache_usage_perc`)
11.      BY minute = BUCKET(@timestamp, 1 minute)
12.  | EVAL prefix_cache_hit_pct = ROUND(cached / ptok * 100, 1),
13.         tokens_per_iteration = ROUND(it_s / it_c, 2),
14.         gen_tokens_per_sec   = ROUND(gen, 1),
15.         kv_cache_pct         = ROUND(kv * 100, 3)
16.  | KEEP minute, prefix_cache_hit_pct, tokens_per_iteration,
17.         gen_tokens_per_sec, running, waiting, kv_cache_pct
18.  | SORT minute ASC

``Lobster AI![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)
markdown 复制代码
`

1.  minute   | prefix_cache_hit_pct | tokens_per_iteration | gen_tok/s | running | waiting | kv_cache_pct
2.  22:55:00 | 32.5                 | 10.36                | 479.1     | 8.0     | 0.0     | 0.255
3.  22:59:00 | 32.3                 | 10.34                | 480.0     | 8.0     | 0.0     | 0.121

`Lobster AI

32% 的 Prefix Cache 命中率意味着,所有 Prefill 工作中有大约三分之一根本不需要执行。 Claims 团队的请求共享 System Prompt 和 Policy Boilerplate,而 vLLM 的 Automatic Prefix Caching 能够识别这一点。这直接说明应该进一步提高Prompt 标准化程度:应用在一致的前置位置放置更多共享上下文,命中率就会越高,每个请求的成本也就越低。如果在两个 Replica 前面放置一个简单的 Round-Robin Load Balancer,这个数字很可能会大幅下降------而这恰恰是需要 llm-d 的 Cache-Aware Routing 的典型场景。

tokens_per_iteration ≈ 10.3,在 8 个并发请求下说明 Continuous Batching 正常工作。 每次 Forward Pass 经过模型时,大约同时推进 10 个 Sequence。如果在有多个请求同时运行时,这个值接近 1.0,就说明 Batching 出了问题,你将不得不为每个用户的每个 Token 分别支付一次完整的 Model Forward 成本。这个指标证明你正在获得 vLLM 的核心价值。

vllm:kv_cache_usage_perc 实际测量的是什么?

vllm:kv_cache_usage_perc 报告的是 vLLM 预分配的 KV Block Pool 的占用率,而不是物理 GPU 内存的使用率。 vLLM 启动时,会预留一部分 VRAM(由 --gpu-memory-utilization 控制,默认值为 0.9),然后从这部分预留空间中划分出一个 KV Block Pool。这个 Gauge 报告的就是这个 Pool 有多满

这就是为什么这里的数值只有 0.25% 。8 个并发请求,每个请求大约持有 150 个 Token,只占用 A10G Block Budget 中非常小的一部分。这个 Pool 很大,而且这样设计是正确的。如果把同一个 Server 推到 32 个并发请求,同时每个请求生成 512--1,024 个 Token,这个数值就会上升到大约 2.8%。但仍然很低。

直觉上可能会认为这么低的数字意味着"Cache 出问题了",或者"我严重过度配置了"。这两种判断都是错误的。把这个 Gauge 当作 VRAM 使用率的代理指标,是我见过的 Self-Hosted vLLM 配置中最常见的错误之一。 第 6 步会准确展示这两者之间到底有多大的差距。

第 3 步:你的 vLLM 推理工作负载是什么形态?

**在进行任何调优之前,先确认实际工作负载是否与你的预期一致。**这个面板可以帮助你解释某些并非由你的配置导致的延迟"回归"。

go 复制代码
`

1.  minute   | requests_per_min | avg_prompt_tokens | avg_generated_tokens | avg_max_tokens | gen_to_prompt_ratio
2.  22:55:00 | 282.2            | 49.6              | 103.6                | 103.6          | 2.09
3.  23:01:00 | 276.0            | 50.6              | 103.0                | 103.0          | 2.04

`Lobster AI

生成 Token 与 Prompt Token 的比率是 2.04 ------ 这个工作负载产生的内容是读取内容的两倍这个单一比率 就是第 1 步中 97% 的时间花在 Decode 上这一发现的解释,而且对于任何 "总结并起草" 类工作负载都成立。如果团队之后增加一个长文档 RAG 功能,Prompt 会增加到数千个 Token,这个比率就会反转,工作负载会变成 Prefill-Bound,而正确的调优方式也会完全不同。关注这个比率,可以让你在有人提交工单之前,就发现工作负载的特征已经发生了变化。

现在来看一个应该让你警觉的列:**avg_generated_tokens 恰好等于 avg_max_tokens。**每个请求都是因为达到 Token 上限而停止的,而不是因为模型已经完成了它想表达的内容。截图显示,当生成长度增加到约 685 个 Token、而上限约为 777 个 Token 时,同样的模式依然存在。

在负载测试中,这是测试生成器造成的结果。但在生产环境中,这意味着用户会在句子中途被截断 ------ 而你现有的任何延迟指标都看不到这个问题。接下来就轮到能够捕获这个问题的指标了。

第 4 步:vLLM 请求是否真的成功?检查finished_reason

按照 finished_reason 标签拆分 vllm:request_success_total。这是 Self-Hosted Inference 最接近应用层 SLI 的指标,而且能够捕获任何延迟图表都无法发现的一种故障模式。

less 复制代码
``

1.  TS metrics-vllm.prometheus-inference
2.  | WHERE @timestamp > NOW() - 30 minutes
3.  | STATS completions_per_min = ROUND(SUM(RATE(`metrics.vllm:request_success_total`)) * 60, 2)
4.      BY minute = BUCKET(@timestamp, 1 minute), finish_reason = labels.finished_reason
5.  | SORT minute ASC, finish_reason

``Lobster AI

五种结果,每一种在运维层面都意味着不同的问题:

finished_reason 含义 应该怎么处理
stop 模型自然完成生成 这是你希望占比尽可能高的结果
length 达到 max_tokens 后被截断 占比较高意味着用户的回答被截断------提高上限,或者缩短请求
abort 客户端先断开连接 用户放弃等待,或者 Proxy 的超时时间短于生成所需时间
error 推理引擎发生故障 你的硬 SLO 信号。应该始终保持为 0
repetition 输出出现退化循环 这是 Sampling Parameter 问题,而不是基础设施问题

在小负载生成器下,结果是 100% length ------ 这是预期的,因为它请求了一个固定的上限。在截图中,在混合负载下,stoplength 并列出现,大约分别为每分钟 51 次和 32 次完成。这种比例是健康的:大多数请求自行完成,少数请求达到上限。

这个经验具有普遍意义。 errorabort 是你应该触发告警的情况。但 stoplength 的比例是你应该每周检查的指标 ,因为向 length 偏移意味着答案正在被截断,而任何延迟仪表板都无法告诉你这一点。

还有一个需要补上的盲点:vllm:* 指标只统计被引擎接受的请求 。格式错误的 JSON、4xx、身份验证失败以及连接被丢弃的请求都不会进入这些指标。它们会出现在带有 statushandler 标签的 http_requests_total 中 ------ 值得在这个面板旁边增加一个面板,因为 "模型坏了" 的报告,很多时候最终发现其实是模型前面的网关出了问题。

第 5 步:NVIDIA DCGM 指标显示 GPU 实际在做什么

**使用 NVIDIA DCGM 作为 vLLM 自身数据的独立验证依据。**到目前为止,所有指标都是引擎对自身运行情况的描述;而 DCGM 描述的是 GPU 硬件本身的实际运行状态。

less 复制代码
``

1.  FROM metrics-vllm.prometheus-inference
2.  | WHERE @timestamp > NOW() - 30 minutes
3.  | STATS gpu_util_pct    = MAX(`metrics.DCGM_FI_DEV_GPU_UTIL`),
4.          mem_bw_util_pct = MAX(`metrics.DCGM_FI_DEV_MEM_COPY_UTIL`),
5.          vram_used_mib   = MAX(`metrics.DCGM_FI_DEV_FB_USED`),
6.          vram_free_mib   = MIN(`metrics.DCGM_FI_DEV_FB_FREE`),
7.          power_w         = ROUND(MAX(`metrics.DCGM_FI_DEV_POWER_USAGE`), 1),
8.          temp_c          = MAX(`metrics.DCGM_FI_DEV_GPU_TEMP`)
9.      BY minute = BUCKET(@timestamp, 1 minute)
10.  | SORT minute ASC

``Lobster AI![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

100% GPU 利用率 · 已使用 21,483 MiB / 剩余 1,352 MiB · 240 W · 73 °C

100% 利用率和 94% VRAM 使用率。按照传统的解读方式,这张卡已经满负荷运行,是时候申请更多硬件了。但这种解读是错误的。

为什么 GPU 利用率是一个容易误导 LLM 推理的指标?

DCGM_FI_DEV_GPU_UTIL 表示的是 "GPU 上有一个 kernel 常驻运行",而不是"GPU 正在执行有用的工作"。一个调优良好的服务器和一个调优糟糕的服务器都可能显示 100%,因此这个指标无法区分两者。DCGM 的 profiling counters 则可以做到这一点:

go 复制代码
`gr_engine_active 99.8%  ·  tensor_active 16.6%  ·  dram_active 80.4%`Lobster AI

把这三个指标放在一起看。GPU 的计算引擎几乎一直处于忙碌状态 ------ 但真正执行矩阵计算的 tensor cores 仅有 16.6% 的时间处于活跃状态 ,而 DRAM 有 80.4% 的时间处于活跃状态。这张卡并不是在进行计算,而是在等待内存

这是对第 1 步仅通过延迟推断出的结论进行的独立、硬件层面的验证:decode 受内存带宽限制。两个完全不同的观测工具、来自技术栈的两个不同层次,却得出了同一个结论------这就是假设与发现之间的区别

这也意味着,对于 LLM 推理来说,GPU 利用率不再适合作为容量指标。如果你的 GPU 容量规划依赖 DCGM_FI_DEV_GPU_UTIL------而大多数容量规划确实如此 ------ 那么这种规划实际上没有可靠依据。

第 6 步:vLLM KV cache 与 GPU VRAM,以及为什么两者不一致

**将 vllm:kv_cache_usage_perc 和物理 VRAM 使用率放在同一个 0--100% 的坐标轴上。**它们描述的是同一块 GPU 内存,但处于图表的两个极端位置,而且两者都是正确的。

sql 复制代码
`

1.  minute   | running | gen_tok_s | kv_cache_pct | vram_used_pct | gpu_util | dram_active_pct | tensor_active_pct | tokens_per_watt
2.  00:05:00 | 31      | 1627.5    | 2.82         | 94.1          | 100.0    | 80.4            | 16.6              | 6.80

`Lobster AI

KV cache 为 2.8%。VRAM 为 94.1%。

vLLM 在启动时会预分配很大一部分 VRAM ------ 由 --gpu-memory-utilization 控制,默认值为 0.9 ------ 然后从这部分预留空间中划分出 KV block pool。vllm:kv_cache_usage_perc 报告的是 pool 的占用率 。DCGM 报告的是 驱动程序看到的情况,也就是整个预留空间,无论其中是否实际存放了数据。

这些指标带来的运维影响非常明确,这也是整个分析过程中最实际的价值所在:

  • **根据 vllm:kv_cache_usage_percvllm:num_requests_waiting 进行自动扩缩容。**这两个指标描述的是接纳容量------也就是引擎当前是否还能接收另一个请求。

  • 根据 VRAM 进行容量规划。它描述的是物理空间 ------ 也就是这张卡上是否还有可能再容纳第二个模型。(不可能。只剩 1.3 GB。)

  • 永远不要针对 VRAM 设置告警。否则即使服务器运行正常、基本处于空闲状态,它也会每天凌晨 3 点把你叫醒,而且会永远如此。

还有,tokens_per_watt ------ 生成的 token 数除以功耗,这里是 6.8------是真正的成本效率指标 。与延迟或利用率不同,它可以在不同 GPU 型号、batch 设置和量化级别之间进行比较。当你再次去找 Finance 申请第二张卡时,这就是最有说服力的数字:在 32 个并发请求下,我们以 240 瓦的功耗持续达到 1,627 tokens/sec,而如果换成 L40S,这个数字会变成......

vLLM 调优决策:SRE 如何利用这些 Prometheus 指标

六个步骤,30 分钟,一台服务器。根据之前设定的目标,最终结论如下:

目标 测量结果 结论
TTFT p95 < 300 ms 51 ms 通过,拥有 6× 余量
Inter-token < 50 ms 16 ms(≈62 tok/s) 通过
12 个并发请求下零排队 8 个并发时 queue_ms 0.01、waiting 0;32 个并发时仍为 0 通过,余量很大
Error + abort < 0.5% 0% 通过

对于这个部门而言,当前配置是正确的,而且该部门是过度配置,而不是配置不足。这就是对 "感觉很慢" 这一反馈一个有数据依据、站得住脚的回答 ------ 同时也会把调查方向转向应用、网关或者 prompt,因为真正的问题实际上出在那里。

接下来需要采取的具体措施,每一项都由指标提供依据,而不是凭感觉:

  1. 停止优化 prefill。 ------ 比较 vllm:request_decode_time_secondsvllm:request_prefill_time_seconds。Decode 占 97% 的时间,而 prefill 只占 2%,这一点已经得到两次验证。对于这个工作负载,chunked prefill 和 prompt compression 都可以排除。

  2. 如果需要更高的吞吐量,先进行量化,再考虑购买硬件。 ------ DCGM_FI_PROF_PIPE_TENSOR_ACTIVE(16.6%)相比 DCGM_FI_PROF_DRAM_ACTIVE(80.4%)。瓶颈是内存带宽,而不是计算能力,因此对相同模型采用 FP8 或 AWQ 版本是最值得优先尝试的单项改动:它每个 token 需要移动更少的数据,而这恰恰是当前受限制的资源。

  3. 提高客户端的 max_tokens 上限。 ------ 查看 vllm:request_success_total{finished_reason}。每一个以 length 而不是 stop 结束的请求,都意味着用户的回答在中途被截断。这是目前唯一一个直接影响用户体验的实际问题。你需要提高 prompt 的 max_token 限制。

  4. 标准化 prompt 前缀。 ------ 查看 vllm:prompt_tokens_cached_totalvllm:prompt_tokens_total 的比例,目前为 32%。但这个比例应该更高(更接近 70%)。让更多共享的 boilerplate 以一致的前缀位置出现,可以提高这个比例,而且不需要额外成本。

  5. 现在就设置好自动扩缩容触发条件,在真正需要之前完成准备。 ------ 使用 vllm:kv_cache_usage_percvllm:num_requests_waiting。当前者超过约 60%,或者后者持续大于 0 时进行扩容。不要根据 DCGM_FI_DEV_GPU_UTIL 进行扩容------它无论如何都会保持在 100%。

  6. 针对 queue time 设置告警,而不是针对 VRAM。 ------ vllm:request_queue_time_seconds 是最早出现的真正饱和信号;DCGM_FI_DEV_FB_USED 则是一个看起来像紧急情况、实际上却可能一直保持不变的指标。

  7. 当工作负载形态发生变化时重新评估。 ------ 查看 vllm:request_generation_tokensvllm:request_prompt_tokens 的比例,目前为 2.04。当 RAG 功能上线后,这个比例可能反转,工作负载变成 prefill-bound,那么这次分析的一半都需要重新进行。这个图表会告诉你这种变化发生的那一天。

值得注意的是,大多数用于优化的指标其实都来自 vLLM 指标,而不是 GPU 指标 。目前这些指标仍然处于单个服务的范围内,但当 vllm:num_requests_waiting 持续大于 0 时,就需要开始考虑使用 KServe,或者使用 KEDA 和 HPA 进行自动扩缩容。

而这些 vLLM 指标可以帮助你确定什么时候应该扩容,以及应该让 KServe 根据什么条件进行扩容。因此可以看到,理解这些指标对于调优 inference service 至关重要。

Elastic Observability 可以为你提供这些能力。

为什么自托管 LLM 调优是 SRE 的问题,而不是 ML 的问题

自托管一个开放权重模型,主要不是一个 ML 问题,而是一个容量和饱和度问题 ------ 这是 SRE 二十年来一直非常擅长的事情。真正的阻碍从来都不是技能,而是遥测数据一直躺在一个没人采集的 /metrics 端点上,并且使用了一种没有映射到实际问题的 schema。

一旦把这些数据采集起来,推理过程其实就是熟悉的工作,只不过换了一身不熟悉的外衣:

  • 在优化任何东西之前,先按照阶段分解延迟(队列 / 预填充 / 解码)。

  • 区分逻辑资源和物理资源(KV block pool ≠ VRAM),并明确每个指标描述的是哪一个。

  • 永远不要相信单一来源的利用率数据------用硬件层面的数据对引擎层面的数据进行交叉验证。

  • 将每一个配置项都与一个指标关联起来,再将每一个指标与明确的目标关联起来,这样调优才能逐步收敛,而不是不断试错。

对于一个不被允许将数据发送到任何外部服务的团队来说,这种差异------也就是从运行一个模型到真正运营一个模型之间的差异------就是全部的关键所在。这个部门获得了一项原本无法使用的能力,而 SRE 也终于可以用数字来回答有关这个模型的问题。

常见问题

vllm:kv_cache_usage_perc 衡量什么?

它衡量的是 vLLM 预分配的 KV block pool 的占用率,而不是物理 GPU 内存。vLLM 在启动时会预留一定比例的 VRAM(--gpu-memory-utilization,默认值为 0.9),然后从这部分预留内存中划分出 KV pool。在本次部署中,该指标为 2.8%,而同一时刻同一块 GPU 上 DCGM 报告的 VRAM 使用率为 94.1%。将它用作自动扩缩容信号;将 VRAM 用于容量规划。

为什么我的 vLLM 部署是解码受限(decode-bound)的?

因为工作负载生成的 token 比读取的 token 更多。比较 vllm:request_decode_time_secondsvllm:request_prefill_time_seconds,并检查生成 token 与提示词 token 的比例。在本次部署中,这个比例为 2.04------输出 token 是输入 token 的两倍------因此解码耗时为 1,665 ms,而预填充耗时仅为 38 ms。解码受内存带宽限制,因此量化、张量并行或使用更小的模型会有所帮助;预填充优化则没有帮助。

我应该根据 GPU 利用率对 vLLM 进行自动扩缩容吗?

不应该。DCGM_FI_DEV_GPU_UTIL 表示 GPU 上有 kernel 常驻,而不是 GPU 正在执行有用的工作------一个调优良好的服务器和一个调优糟糕的服务器都可能显示 100%。应该根据 vllm:kv_cache_usage_perc(大约 60%)或者持续高于零的 vllm:num_requests_waiting 进行自动扩缩容,因为这些指标描述的是引擎当前是否还能接纳新的请求。

为什么我的 GPU 显示 100% 利用率,但实际上并没有被充分使用?

因为 GPU 利用率只报告 kernel 的常驻情况。应该改为检查 DCGM 的 profiling counters:在本次部署中,DCGM_FI_PROF_GR_ENGINE_ACTIVE 为 99.8%,而 DCGM_FI_PROF_PIPE_TENSOR_ACTIVE 仅为 16.6%,DCGM_FI_PROF_DRAM_ACTIVE 为 80.4%。这种组合意味着 GPU 正在等待内存带宽,而不是进行计算。

如何将 vLLM 指标获取到 Prometheus?

vLLM 已经在其服务端口的 /metrics 上以 Prometheus exposition format 暴露指标------不需要 adapter 或额外的 instrumentation。让 Prometheus 的一个 scrape job 指向 vLLM Service,再添加第二个 job 抓取 :9400 上的 dcgm-exporter,然后使用 remote_write 将数据发送到长期存储。通过 OpenTelemetry Collector 发送也可以,但它会对指标名称进行标准化,这使得它们更难与 vLLM 文档中的名称对应。

对于交互式 LLM 应用,应该将 TTFT 设定在什么水平?

对于流式聊天界面,p95 time-to-first-token 低于 300 ms 会让用户感觉响应非常即时,而低于 50 ms 的 inter-token latency (约 20 tokens/sec)已经快于人类阅读速度。本次部署在单块 NVIDIA A10G 上运行 3B 模型时,测得 TTFT 为 51 ms,inter-token latency 为 16 ms,留下了大约 6 倍的余量。

为什么我的所有 vLLM 请求最终都是 length

因为它们达到了 max_tokens 上限,而不是模型自行决定停止。按照 finished_reason 标签拆分 vllm:request_success_total:较高的 length 占比意味着回答在中途被截断。这一点在任何延迟指标中都看不出来,因此应该定期检查 stoplength 的比例;如果 length 占比不断上升,就提高客户端的 token 上限。

什么时候应该从普通的 vLLM Deployment 迁移到 KServe 或 llm-d?

vllm:num_requests_waiting 在峰值负载期间持续不为零,并且你需要在没有人工干预的情况下自动增加副本时,可以迁移到 KServe。当跨副本的 prefix-cache 命中率下降时,可以考虑迁移到 llm-d------这通常意味着负载均衡将共享相同前缀的会话分散到了不同副本;或者当预填充耗时开始明显挤占解码耗时时,也可以考虑使用 llm-d。

原文:vLLM Prometheus metrics for self-hosted LLM tuning | Elastic Observability Labs

相关推荐
Sayai5 小时前
Elasticsearch 快照备份到 NAS(NFS)实战:SLM 自动化 + 365 天保留策略
elasticsearch·自动化·jenkins
JavaPub-rodert1 天前
写软著 - Copyright Forge Skill 完整闭环改造方案
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客1 天前
从建议到修复的 4 个阶段:使用 Elastic Workflows 实现人在回路中的自动化
运维·数据库·人工智能·后端·elasticsearch·ai·自动化
Elasticsearch1 天前
使用 OpenTelemetry 进行 Android 应用监控:从点击到后端的分布式追踪
elasticsearch
Elasticsearch1 天前
安心睡过凌晨 3 点的告警:在 Red Hat OpenShift 上使用 Elastic 实现自动化故障事件响应
elasticsearch
Elasticsearch2 天前
将 1,100 个文件迁移到 Redux Toolkit v2,同时避免冻结 Kibana 单体仓库
elasticsearch
Elasticsearch2 天前
从 582 毫秒的延迟峰值追踪到负责该服务的团队:使用 Kibana Discover
elasticsearch
Jinkxs2 天前
SkyWalking - 生产级部署:集成 Elasticsearch 作为后端存储
大数据·elasticsearch·skywalking
醉颜凉2 天前
Elasticsearch服务器部署:从零到一完整启动+配置教程
大数据·服务器·elasticsearch