作者:来自 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
但你可能会遇到一些具体问题,例如: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.xlargeGPU 节点池上。对于一张显卡上的一个模型而言,一个 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_running、vllm:num_requests_waiting 和 vllm: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
在 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
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 ------ 这是预期的,因为它请求了一个固定的上限。在截图中,在混合负载下,stop 和 length 并列出现,大约分别为每分钟 51 次和 32 次完成。这种比例是健康的:大多数请求自行完成,少数请求达到上限。
这个经验具有普遍意义。 error 和 abort 是你应该触发告警的情况。但 stop 与 length 的比例是你应该每周检查的指标 ,因为向 length 偏移意味着答案正在被截断,而任何延迟仪表板都无法告诉你这一点。
还有一个需要补上的盲点:vllm:* 指标只统计被引擎接受的请求 。格式错误的 JSON、4xx、身份验证失败以及连接被丢弃的请求都不会进入这些指标。它们会出现在带有 status 和 handler 标签的 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
→ 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_perc和vllm: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,因为真正的问题实际上出在那里。
接下来需要采取的具体措施,每一项都由指标提供依据,而不是凭感觉:
-
停止优化 prefill。 ------ 比较
vllm:request_decode_time_seconds和vllm:request_prefill_time_seconds。Decode 占 97% 的时间,而 prefill 只占 2%,这一点已经得到两次验证。对于这个工作负载,chunked prefill 和 prompt compression 都可以排除。 -
如果需要更高的吞吐量,先进行量化,再考虑购买硬件。 ------
DCGM_FI_PROF_PIPE_TENSOR_ACTIVE(16.6%)相比DCGM_FI_PROF_DRAM_ACTIVE(80.4%)。瓶颈是内存带宽,而不是计算能力,因此对相同模型采用 FP8 或 AWQ 版本是最值得优先尝试的单项改动:它每个 token 需要移动更少的数据,而这恰恰是当前受限制的资源。 -
提高客户端的
max_tokens上限。 ------ 查看vllm:request_success_total{finished_reason}。每一个以length而不是stop结束的请求,都意味着用户的回答在中途被截断。这是目前唯一一个直接影响用户体验的实际问题。你需要提高 prompt 的max_token限制。 -
标准化 prompt 前缀。 ------ 查看
vllm:prompt_tokens_cached_total与vllm:prompt_tokens_total的比例,目前为 32%。但这个比例应该更高(更接近 70%)。让更多共享的 boilerplate 以一致的前缀位置出现,可以提高这个比例,而且不需要额外成本。 -
现在就设置好自动扩缩容触发条件,在真正需要之前完成准备。 ------ 使用
vllm:kv_cache_usage_perc和vllm:num_requests_waiting。当前者超过约 60%,或者后者持续大于 0 时进行扩容。不要根据DCGM_FI_DEV_GPU_UTIL进行扩容------它无论如何都会保持在 100%。 -
针对 queue time 设置告警,而不是针对 VRAM。 ------
vllm:request_queue_time_seconds是最早出现的真正饱和信号;DCGM_FI_DEV_FB_USED则是一个看起来像紧急情况、实际上却可能一直保持不变的指标。 -
当工作负载形态发生变化时重新评估。 ------ 查看
vllm:request_generation_tokens与vllm: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_seconds 和 vllm: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 占比意味着回答在中途被截断。这一点在任何延迟指标中都看不出来,因此应该定期检查 stop 与 length 的比例;如果 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