朋友昨天跑来诉苦,说用 vLLM 部署的 Qwen2.5-27B-FP8 模型,请求稍微一多就感觉明显变慢,流式输出一卡一卡的,翻日志也只能看到零零散散的报错,根本说不清瓶颈到底在模型加载、显存占用、排队等待还是网络传输。他问我有没有一套能直接落地的推理延迟监控方案,让他不用靠“感觉”来判断服务是不是健康。
这类问题在 AI 推理服务上线后几乎是必踩的坑。模型能跑通是一回事,跑得稳不稳、延迟表现怎么样,完全是另一回事。今天这篇就围绕“AI 模型推理延迟监控”这个主题,把我自己搭过的监控方案、踩过的坑、排查过的典型案例一次讲清楚。方案以 Prometheus 为核心,覆盖指标埋点、数据采集、滑动窗口滤波、告警配置和实际问题定位,适合正在做模型服务化部署、推理接口维护的同学参考。
1. 推理延迟监控,到底在监控什么
1.1 延迟的三个量纲:TTFT、TPOT 和端到端延迟
很多刚接触推理监控的同学,习惯只看接口响应时间这一个数字。但这个数字在大模型场景下很容易“骗人”。比如你调一个 LLM 接口,问了一个长问题,要求模型生成 500 个 token,总耗时 20 秒,看起来“挺慢”。可实际上首字返回只用了 0.8 秒,用户感知并不差;真正耗时的是后续 499 个 token 逐个生成的过程。反过来,如果首字用了 8 秒,哪怕后面生成得再快,用户体验也是灾难性的。
所以监控大模型推理延迟,至少要拆成三个维度来看:
- TTFT(Time To First Token):从请求发出到收到第一个 token 的时间,直接决定用户“转圈”的时间长短。
- TPOT(Time Per Output Token):每个输出 token 的平均耗时,数值越小,流式输出越顺滑。
- 端到端延迟(E2E Latency):整个请求从进入到返回完毕的总耗时,运维排障时最常用的兜底指标。
如果服务用的是流式输出,还要关注 ITL(Inter-Token Latency),也就是相邻两个 token 之间的间隔。这个间隔如果忽大忽小,说明服务内部存在不稳定的等待,常见的例子就是排队、抢占或者显存不足导致的预emption。
监控时不要只盯一个指标。我见过不少团队只看“平均响应时间”,结果平均下来很好看,但 P95 延迟早就爆炸了。正确的做法是 TTFT、TPOT、E2E 三个指标都采集,并且全部按分位数(P50/P95/P99)统计,这样才能捕捉到长尾问题。
1.2 影响延迟的关键因素:量化、KV Cache 与并发
在搭监控之前,还得先理解延迟是从哪里来的。模型推理延迟不是单一因素决定的,至少有这么几块:
- 模型大小与量化方式。FP8、INT8、INT4 这些量化格式直接影响显存占用和算力需求。Qwen2.5-27B 如果是 FP8 部署,单位 token 的算力开销会比 FP16 低不少,但前提是硬件支持的加速内核足够成熟。监控时要关注是否发生了反量化开销、是否因为量化版本算子性能差反而更慢。
- KV Cache 占用。Concurrent requests 越多,KV Cache 消耗越快,一旦超过容量,vLLM 这种框架就会触发抢占或重新计算,表现为延迟突然飙升。所以 KV Cache 使用率要和延迟指标一起看。
- 并发量与排队。服务端通常有最大并发限制(比如 max-num-seqs),超过上限的请求会进入等待队列。队列越长,TTFT 越大。监控时一定要把“等待数量”这个指标跟延迟放在同一个面板里。
- 显存利用率。GPU 显存满了以后会发生 swap,极端情况会触发 OOM 或者频繁的显存拷贝,延迟直接上一个数量级。
理解了这些因素,后面选监控指标时就不会只盯着一个延迟数字了,而是把延迟、队列、显存、缓存使用率放在一起分析。这也是这套方案跟普通“接口响应时间监控”最本质的区别。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 监控方案选型与整体架构
2.1 为什么选 Prometheus 这套组合
市面上做监控的工具有很多,Zabbix、SkyWalking、Prometheus、Graphite、InfluxDB 都能用。但如果你要监控的是 AI 推理服务的延迟,我个人首选的组合还是 Prometheus + Grafana + Alertmanager。理由很直接:
- Prometheus 的拉模型非常适合微服务和容器化部署。推理服务通常以 API Server 或多副本 Pod 的形式跑,Prometheus 定期去抓取指标,不会对推理进程造成频繁的主动连接。
- 内置的分位数统计能力很省事。延迟这类指标天然适合用 Histogram 类型来聚合,PromQL 里直接 histogram_quantile(0.99, ...) 就能算 P99,不用自己在业务代码里维护分位数。
- 生态里现成的 exporter 非常多。除了推理服务自己的指标,GPU 显存、CPU 负载、Kafka 消费延迟、数据库连接池这些都能接入同一套体系。
- AI 框架对 Prometheus 支持度好。vLLM、Triton Inference Server 这些主流推理框架都自带 /metrics 端点,开箱即用。
Zabbix 那套面向传统 IT 基础设施的监控,不是不能用,但对自定义业务指标的支持比较笨重。SkyWalking 更偏分布式链路追踪,适合查单次请求的调用链,不适合做长期、聚合的延迟 SLO 监控。所以如果从零开始搭,我建议直接走 Prometheus 全家桶。
2.2 数据链路:埋点、采集、存储与展示
整个监控系统的数据链路可以分成四层:
- 埋点层:推理服务内部统计每次请求的 TTFT、TPOT、E2E 延迟数据,以 Histogram 或 Gauge 的形式暴露出来。vLLM 自带指标,自己写的 FastAPI/Flask 服务可以通过 prometheus-client 库手动埋点。
- 采集层:Prometheus Server 按固定间隔(通常是 10s 或 15s)去抓取各实例的 /metrics 端点,并把数据存储在自己的时序数据库里。
- 告警层:Alertmanager 根据 Prometheus 里的 PromQL 告警规则做判定,触发后把通知推到钉钉、飞书、Slack 或者邮件。
- 展示层:Grafana 从 Prometheus 查询数据,把各类延迟指标画成面板,方便日常盯盘和事后复盘。
这套链路里最容易被低估的是采集间隔的选择。间隔太短(比如 1s)会显著增加 Prometheus 和推理服务的资源开销;间隔太长(比如 60s)又无法捕捉到秒级的延迟抖动。我的经验是:采集间隔设 10s,评估间隔也设 10s,告警持续时间设 5 分钟起步。这样既不会漏掉大部分问题,也不会让告警变得碎片化。
2.3 延迟数据的滑动窗口滤波处理
热词里有人提到“滑动窗口滤波器延迟”,放在监控场景下其实非常实用。原始延迟数据抖得厉害,网络波动、GC 停顿、偶发调度延迟都会让单次指标出现尖刺。如果直接把原始曲线拿来做告警判断,误报率会高到让人想关掉告警系统。
所以我会在展示和告警前,对延迟数据做一层平滑处理。最常用的有两个办法:
- 滑动窗口平均(SMA):取最近 N 个数据点的算术平均,N 一般在 5-20 之间。优点是简单直观,缺点是窗口越大反应越迟钝。
- 指数移动平均(EMA):每个新数据点只以一定比例影响当前值。公式是 EMA_new = α * x + (1 - α) * EMA_old,其中 α = 2 / (N+1)。N 取 10 时,α 约等于 0.18,这样既保留了近期趋势,又不会因为一个异常点就剧烈跳变。
用 PromQL 来做这种滤波也很快,直接用 avg_over_time(metric[5m]) 就能得到 5 分钟滑动窗口的平均值。如果想让曲线更平滑,还可以用平滑函数配合子查询。但要注意,滤波只是让曲线好看、让告警稳定,它解决不了“真实延迟确实高”的问题。平滑处理后看到持续上升的均值,该排查还是要排查。
3. 实操落地:延迟监控从埋点到告警
3.1 vLLM 服务自带指标怎么接
如果你用的是 vLLM 部署推理服务,那是省事最多的场景,因为框架本身就暴露了标准 Prometheus 指标。以 Qwen2.5-27B-FP8 为例,启动服务的时候加上指标端口参数即可:
bash复制python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-27B \
--quantization fp8 \
--max-model-len 8192 \
--gpu-memory-utilization 0.9 \
--enable-prefix-caching \
--max-num-seqs 32 \
--port 8000 \
--served-model-name qwen-27b-fp8
启动后直接访问 http://localhost:8000/metrics,就能看到一堆指标。比较关键的几个是:
- vllm:num_requests_running:当前正在处理的请求数。
- vllm:num_requests_waiting:当前排队等待的请求数,这个数字一旦持续大于 0,TTFT 必然恶化。
- vllm:gpu_cache_usage_perc:KV Cache 使用百分比,接近 100% 时要留意抢占和预emption。
- vllm:request_time_to_first_token_seconds 系列:TTFT 的直方图指标。
- vllm:request_time_per_output_token_seconds 系列:TPOT 的直方图指标。
- vllm:e2e_request_latency_seconds 系列:端到端延迟的直方图指标。
不同版本的 vLLM 指标名可能会有一点差异,最靠谱的办法是启动后看一眼 /metrics 输出里实际叫什么名,再写 PromQL,别直接照抄网上的。我在 0.6.x 和 0.8.x 版本上都遇到过指标名变化的情况,这种事只能以现场为准。
3.2 自定义埋点:给推理接口加延迟中间件
vLLM 自带指标覆盖的是模型推理阶段,但实际业务请求往往还要经过 API 网关、鉴权、预检、后处理等多个环节。这些环节的延迟如果也想监控,就得自己做埋点。下面是一个基于 FastAPI 和 prometheus-client 的最小实现,统计接口层面的请求延迟和 QPS:
python复制import time
from fastapi import FastAPI, Request
from prometheus_client import Histogram, Counter, make_asgi_app
app = FastAPI()
# 延迟直方图,bucket 根据业务量级调整
LATENCY = Histogram(
"inference_request_latency_seconds",
"End-to-end inference request latency in seconds",
labelnames=["model", "endpoint"],
buckets=(0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0)
)
REQUEST_COUNT = Counter(
"inference_requests_total",
"Total number of inference requests",
labelnames=["model", "endpoint"]
)
@app.middleware("http")
async def latency_middleware(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
model = request.path_params.get("model", "unknown")
LATENCY.labels(model=model, endpoint=request.url.path).observe(duration)
REQUEST_COUNT.labels(model=model, endpoint=request.url.path).inc()
return response
# 挂载 Prometheus 指标端点
app.mount("/metrics", make_asgi_app())
代码不复杂,但有三个细节值得注意。
第一,延迟统计一定要用 time.perf_counter(),不要用 time.time()。time.time 的精度受系统时间调整影响,在统计毫秒级延迟时会有误差。
第二,Histogram 的 bucket 设计要贴合实际。如果大部分请求延迟都在 1-3 秒,bucket 就要在 0.5、1、2、3 这些位置多放几个桶,否则算出来的分位数精度不够。bucket 数也不宜太多,一个指标十几个桶已经很多了,再多会明显增加 Prometheus 的内存开销。
第三,label 不要乱加。model、endpoint、status 这种高基数场景很小的 label 没问题,但如果你把 user_id 这种每个用户都不同的 label 加进去,Prometheus 内存会被打爆,这个坑我亲眼见过别人踩过。
如果推理接口是流式输出,统计延迟就要分成两段,一个统计首 token 时间,一个统计总耗时。实现思路是在中间件里拿到流式响应后,监听第一个 chunk 发送的时间点,再去计时。
3.3 Prometheus 抓取配置与 Grafana 面板
埋点准备好之后,接下来是让 Prometheus 能够抓取这些指标。下面是 prometheus.yml 里最核心的配置片段:
yaml复制global:
scrape_interval: 10s
evaluation_interval: 10s
scrape_configs:
- job_name: "vllm-inference"
metrics_path: "/metrics"
static_configs:
- targets: ["127.0.0.1:8000"]
labels:
instance: "qwen-27b-fp8"
model: "qwen2.5-27b"
- job_name: "custom-inference-api"
metrics_path: "/metrics"
static_configs:
- targets: ["127.0.0.1:8080"]
labels:
instance: "inference-gateway"
- job_name: "node-exporter"
static_configs:
- targets: ["127.0.0.1:9100"]
node-exporter 是我每次都会加上的组件,GPU 服务器上还会再加一个 nvidia-dcgm-exporter 或者 nvidia-gpu-exporter,看显存、温度、功耗。很多延迟问题表面上是服务慢,本质上都是 GPU 被跑满了,没有硬件层指标的话排查会非常被动。
Grafana 面板方面,我一般会开两页。第一页是“实时总览”,放端到端延迟 P50/P95/P99、QPS、GPU 利用率、KV Cache 百分比、当前排队请求数这五个图,一屏看完定位问题在哪个环节。第二页是“TTFT/TPOT 专项”,专门放 TTFT 分位数、TPOT 分位数和 token 生成速率,排查模型性能问题用。
面板的 YAML 或者 JSON 是支持导出的,第一次调好之后记得导出留档,下次接新模型直接复用,能省不少时间。
3.4 告警规则:怎么设阈值才不误报
告警规则是很多人设计得最草率的部分,要么太敏感天天被打扰,要么阈值设得太宽,出了问题才发现根本没告警。下面这个规则是经过我迭代过很多次的版本,兼顾了敏感性和稳定性:
yaml复制groups:
- name: inference-latency-alerts
rules:
- alert: InferenceLatencyHighP95
expr: |
histogram_quantile(0.95,
sum by (le, instance) (rate(
inference_request_latency_seconds_bucket[5m]
))
) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "P95 latency above 5s"
description: "Instance {{ $labels.instance }} P95 latency is {{ $value }}s"
- alert: InferenceQueueGrowing
expr: |
sum by (instance) (vllm:num_requests_waiting) > 20
for: 3m
labels:
severity: critical
annotations:
summary: "Inference queue is growing"
description: "Instance {{ $labels.instance }} has {{ $value }} waiting requests"
设计告警规则时我总结了几条经验:
- “for: 5m”这个持续条件非常关键。瞬时突刺很多是网络抖动或 GC 引起的,过了就好,没必要告警;持续 5 分钟以上的异常才值得人工介入。
- 阈值不要拍脑袋。先接进监控跑两周,看看正常负载下的 P95 大概是多少,再在正常值之上留 50% 左右的余量去设阈值。直接用网上抄来的 5 秒阈值,很可能在你的场景下毫无意义。
- 告警规则要关联到“排队”指标。因为延迟高往往只是表象,队列堆积才是根因。单独监控延迟,等告警触发时其实已经堵了很久了。
- 分位数求值要在 rate 的基础上算,不要直接在原始 bucket 上算,否则得不到“每秒”的速率,曲线会非常奇怪。
4. 经典案例:Qwen 27B FP8 部署卡顿排查实录
4.1 现象与初步判断
说个真实排查过程。当时场景和他遇到的问题一模一样:vLLM 部署 Qwen2.5-27B-FP8,单张 A100-80G,开的是 OpenAI 兼容接口,内部对接了不少业务方。现象是用户反馈“生成速度时快时慢,有时请求直接超时”,而且主要集中在下午业务高峰期。
拿到这个反馈,我没有直接去看模型代码,而是先打开 Grafana 面板,按 1 小时间隔拉取数据。第一眼看的是端到端延迟的 P95 曲线,确实从早上的 3 秒左右爬升到了晚高峰的 12 秒以上。但单看这一条曲线还不够,因为延迟是结果指标,不是原因指标。我继续翻了三个相关面板:vllm:num_requests_waiting、GPU 利用率和 KV Cache 使用率。
这里有个排查技巧:看指标时要按“延迟 → 队列 → 资源”的顺序逐层往下钻。如果延迟高但队列为空,说明是单请求处理变慢,可能是模型计算、显存或算子问题;如果延迟高同时队列积压,说明是并发处理能力不足或流量超预期,优先考虑扩容和限流;如果队列正常但 GPU 利用率打满,说明单卡算力确实是瓶颈。
4.2 用监控数据定位瓶颈
那天的监控数据打开后,问题立刻就清晰了:晚高峰时 vllm:num_requests_waiting 一直徘徊在 15-30 之间,GPU 利用率接近 100%,KV Cache 使用率在 95% 左右反复震荡。也就是说,请求确实在排队,GPU 也确实没有空转,问题本质是单张 A100 处理 27B-FP8 模型时,出 token 的能力已经追不上业务请求量了。
进一步看 TPOT 指标,正常时每个输出 token 大约 35 毫秒,但排队严重时会被拉高到 80 毫秒以上。这其实不是模型算得慢了,而是等待时间被“摊”进了每个 token 的时间里。这一点是很多人会误解的地方:流式输出时,两个 token 之间如果隔了很久,不一定代表模型解码慢,可能是这个请求在等 GPU 资源被让出来。
定位到瓶颈后怎么解决就清楚了。我把 max-num-seqs 从 32 调低到 24,让每个并发请求分到的算力更稳定,避免过度争抢;同时给服务加了一层请求排队上限,超过了就快速返回 429,而不是让请求无限堆积在内部。这样做的结果是,高峰期的 P95 延迟从 12 秒降到了 6 秒左右,虽然单用户吞吐量没有提升,但至少不再出现大面积超时。另一个更彻底的方案是多卡张量并行或加副本,但这是成本和架构层面的取舍,不是一次监控调参能决定的。
这个案例有一个很重要的经验:监控不是用来“背锅”的,而是用来划清瓶颈边界。没有监控数据的时候,团队内部会互相怀疑,有人说是模型量化的问题,有人说是网络问题,扯皮半天。有了监控数据,问题明确摆在 GPU 利用率和排队指标上,所有人就都往同一个方向使劲了。
4.3 队列堆积问题:Kafka 消费侧的延迟监控
还有一类场景,推理服务前面挂了异步消息队列,比如 Kafka,请求先投递到 topic,再由消费服务调模型推理。这种架构下,热词里提到的“Kafka 消息延迟高”就得纳入监控范围,因为队列积压很可能是推理服务根本来不及消费。
具体监控思路是:在消费端记录每条消息的生产时间,和消费开始时间做差,得到一个 backlog 延迟,把它作为一个 Gauge 指标暴露给 Prometheus。如果这个值持续升高,说明消费能力跟不上生产速度,即使 CPU、GPU 看起来没满,也可能是消费线程数、批量拉取参数、网络带宽等环节有瓶颈。
Kafka 原生提供 consumer group 的 lag 指标,用 kafka_exporter 就能抓取,不用自己写代码。但我建议至少在消费端保留一层业务延迟指标,因为队列 lag 是 Kafka 维度的指标,跟推理服务实际处理请求的 TTFT 没有直接对应关系。两者结合起来看,才能判断延迟到底是发生在队列滞留阶段,还是发生在模型推理阶段。
5. 常见问题速查与避坑心得
5.1 监控系统踩坑速查表
搭得多了之后,我整理过一份踩坑记录,这里挑几条最常见的放出来,大家可以对着查。
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 延迟曲线忽高忽低,告警频繁 | 采集间隔太短或未做平滑处理 | 拉长采集间隔,用 avg_over_time 做滑动窗口滤波 |
| 指标在 /metrics 里有,Grafana 里查不到 | 指标名写错或用错 job 标签 | 到 Prometheus 页面执行查询确认指标名和标签 |
| P99 延迟远高于 P95 | 存在少量长尾请求占满了并发槽位 | 排查慢请求的业务逻辑,结合链路追踪定位 |
| TTFT 正常但 TPOT 很高 | 输出阶段 token 生成慢,可能是模型计算瓶颈 | 看 GPU 利用率、是否触发显存抢占 |
| GPU 利用率不高但排队严重 | 并发限制设置过小 | 上调 max-num-seqs,或检查是否被前处理环节卡住 |
| KV Cache 使用率长期 95% 以上 | 并发请求过多,缓存换入换出频繁 | 降低并发或启用 prefix caching 优化 |
| Prometheus 内存持续上涨 | Histogram 的 label 基数过高或 bucket 太多 | 检查是否有高基数 label,缩减 bucket 数量 |
| 消费侧 queue 延迟持续升高 | 消费能力不足或批量参数不合理 | 用 kafka_exporter 查看 lag,调整 consumer 参数 |
还有一个非常经典的坑:Prometheus 拉流式接口的指标时,如果服务端启用了 gzip 压缩,抓取配置里要显式允许压缩协商,否则会偶发抓取失败。这个问题不常见,但一旦发生,指标缺口会导致告警判定出问题,排查起来很隐蔽。
5.2 一套监控跑稳定后的几点心得
监控方案搭完之后,并不是就一劳永逸了,它会随着模型版本、流量模式的变化持续演化。这里分享几条我自己沉淀下来的经验,也算是对这套方案的一个收尾。
第一,延迟指标要按模型和版本分离。同一个服务可能同时加载多个模型,或者同一个模型升级过几版。label 里带上模型名和版本号,对比新旧版本的延迟曲线会非常有帮助,这在模型迭代时能直接量化“新版本是否变慢”。
第二,监控数据最好至少保留 30 天。推理服务的延迟问题往往不是当下爆发的,而是随着流量增长慢慢劣化的。只有拉到 30 天的趋势数据,才能看到周级波动和长期劣化趋势。Prometheus 默认的本地存储保留时间往往不够,建议搭配 Thanos 或者调整 retention 参数做长期存储。
第三,不要把所有指标都堆到一张面板上。Grafana 面板图一多,人眼就找不到重点了。我习惯分三层:第一层是总览,只看服务是否健康;第二层是分析,看延迟、队列、资源的具体关系;第三层是调试,用于出问题时下钻单个请求。这样的面板结构在紧急排障时能省很多时间。
最后再分享一个小技巧。监控跑通以后,记得把当时的告警截图、指标曲线和对应处理过程记录下来。几个月后再遇到类似问题,你不用重新从零分析,直接翻自己的历史记录就能快速定位。这一套“指标 -> 定位 -> 修复 -> 复盘”的循环走顺了,AI 推理服务的稳定性才真正有了保障。
