AI模型推理延迟监控,听起来像是个不需要太花心思的事,毕竟模型能跑起来不就行了吗?直到你遇到线上的推理服务突然从200ms变成5s,用户开始刷屏投诉,你手里却没有任何指标能解释这段时间到底发生了什么,才会后悔当时没有搭一套完整的延迟监控方案。我自己在运维AI推理服务时就踩过几次这样的坑,后来整理出一套从指标定义、采集到告警排查的完整方案,今天分享出来,希望能帮正在做模型部署、或者打算把AI能力接入自己个人系统的朋友少走点弯路。
这套方案的核心思路其实不复杂:先把延迟拆开看,再把指标接进来,最后让监控在出问题时能直接告诉你是哪一环变慢了。我会用实际部署中的例子来说明,包括vLLM这种主流推理框架的指标怎么采集、Prometheus监控怎么部署并集成到个人系统、以及遇到“模型偶尔卡一下”这种问题该怎么一步步查。全文不会停留在概念层面,每个环节都给出能直接用的配置或步骤。
1. 为什么推理延迟监控这么重要
1.1 一次生产事故引发的思考
当时我负责一个AI Agent服务,底层接的是外部大模型API,前端用户反馈“回答越来越慢”,一开始我们所有人都怀疑是上游API变慢了,于是反复看接口文档、查官方状态页,折腾了大半天毫无进展。后来我手动压测了一下服务入口,发现本地到上游API的网络往返确实有几百毫秒,但这个延迟一直是稳定的,根本不足以致命。
真正的问题出现在服务内部:由于用户请求量上涨,连接池不够用,新请求在网关层排队;同时请求里包含了大段历史上下文,Agent框架每次都要把完整消息序列重新组装、传给模型,这部分序列化时间被我们也几乎忽略。因为没有监控,这一切都是靠猜。从那次之后,我给自己定了一条规矩:任何模型推理服务,上线之前必须先有延迟监控,否则不叫发布,叫碰运气。
这个案例很典型地说明了为什么不能只盯着“模型快不快”。大模型服务是一个完整的链路,从客户端发起请求、负载均衡转发、网关鉴权、上下文组装、模型调度、显存分配、token流式生成到网络回包,任何一环都可能成为延迟瓶颈。而监控的意义,就是让链路里的每一段都能被看见、被测量、被对比。
1.2 推理延迟的构成拆解:模型快慢只是其中一环
很多人一说“延迟高”第一反应就是“换更快的模型”,这是最常见的误区。理解延迟构成,是设计监控方案的前提。按我自己的习惯,会把一次推理请求拆成五段:
- 请求排队时间:从请求到达服务到真正开始处理的时间。伴随并发升高、线程池或GPU队列被打满,这段会急剧上升。
- 输入处理时间:包括tokenize、上下文拼接、图像预处理等。prompt越长,这部分越不可忽略。
- 模型调度与推理时间:模型从接收输入到生成完整回复的时间。可以细分为首token延迟(TTFT)和生成输出token的吞吐速度。
- 输出后处理时间:包括decode、采样约束、过滤、格式化输出等。
- 网络传输时间:客户端到服务端的往返延迟,如果是跨区域调用还要考虑公网链路。
用餐厅点餐来类比最好理解:排队等位是请求排队,服务员记录菜单是输入处理,后厨炒菜是模型推理,上菜是网络传输。你不能只盯着后厨的锅快不快,因为可能菜全都堆在传菜口没人端。
所以监控方案里的第一个原则是:不要只监控一个总延迟数字,要拆到段落级别。这样当总延迟升高时,你能一眼看出是哪一段“排长队”了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 延迟监控指标怎么设计才不白搭
2.1 核心指标定义与统计口径
有了“拆段”的思路,接下来要定义指标。我通常会暴露三组延迟相关的指标:端到端请求延迟、首token延迟、以及每token生成时间。它们分别对应不同的用户体验问题:端到端延迟影响整体等待感受,首token延迟影响用户觉得“有没有反应”,每token生成时间决定回复的整体流畅度。
这里特别提醒一个坑:延迟指标不能只看平均值。平均值最容易被长尾请求掩盖,比如一批请求里的P99已经是3秒了,平均值可能还是500ms,监控一整天都太平无事,实际用户早就卡疯了。所以我的做法是至少同时关注三个分位数:
- P50:代表典型用户体验。
- P95:代表大多数用户遇到的最差情况。
- P99:代表极端情况,是告警触发的主要依据。
除了分位数,还要记录延迟的计数和总和,这样通过PromQL就能实时计算出任意时间窗口内的平均延迟和分位数,而不是等埋点端替你算死。这个习惯很重要,否则你后期想调整口径就得重新改代码。
我常用Prometheus的Histogram类型来存延迟,它天然支持分位数计算。但也要注意bucket边界需要合理设置,如果你的服务延迟通常在10ms到50ms之间,那bucket就不要从1秒开始分,那样所有请求都堆在同一个桶里,算出来的分位数完全失真。
2.2 滑动窗口滤波器:去掉毛刺看趋势
原始延迟指标天然带刺。某一次GC暂停、某一次磁盘IO抖动,都可能导致单点延迟飙高,但紧接着又恢复正常。如果直接用原始值做告警,你的手机一晚上都会被误报刷屏。
这时候就需要滑动窗口滤波器。思想很简单:维护一个固定大小的最近时间窗口,窗口内的采样数据通过平均值或中位数来反映趋势,而不是被单个异常点带偏。我自己写过多语言版本的实现,核心逻辑大概长这样:
python复制import time
import statistics
class SlidingWindowFilter:
def __init__(self, window_size=60, max_age_seconds=60):
self.window_size = window_size
self.max_age_seconds = max_age_seconds
self.values = []
self.timestamps = []
def add(self, value):
now = time.time()
self.values.append(value)
self.timestamps.append(now)
# 丢弃过期数据和超出窗口数量的旧数据
while self.timestamps and (len(self.values) > self.window_size or
now - self.timestamps[0] > self.max_age_seconds):
self.values.pop(0)
self.timestamps.pop(0)
def median(self):
if not self.values:
return 0.0
return statistics.median(self.values)
def mean(self):
if not self.values:
return 0.0
return sum(self.values) / len(self.values)
滑动窗口的窗口大小选择有讲究。窗口太短,滤波效果差;窗口太长,真正的延迟恶化也会被抹平,导致告警变迟钝。我常用的组合是:采样间隔5秒,窗口取60个点,也就是5分钟的中位数窗口。这个参数可以在实时变化面前保持一定的稳定性,又不会掩盖持续半分钟以上的性能劣化。
在Prometheus体系里,其实有天然对应的函数,比如avg_over_time、median_over_time(部分版本新推出),或者你可以直接用Histogram的分位数加rate函数,底层就是对一段时间窗口做滑动的。理解滑动窗口原理,能帮你在调PromQL时少犯糊涂。
2.3 采集频率与存储策略的取舍
定义好指标之后,采集频率也得想清楚。Pull模型下,Prometheus每隔一定时间拉一次指标,这个间隔直接决定了你能发现多快的问题。对于绝大多数AI推理服务,我建议抓取间隔设置为10到15秒。太短会增加目标服务端的CPU开销,尤其是vLLM这样本身就在高负载下跑的服务,每次抓取都要序列化大量指标,频率太高确实会有额外损耗;太长又会让告警响应变得迟钝。
在存储上,Prometheus本地磁盘适合保留7到15天的数据,默认的15天保留期对个人系统基本够用。如果你想把监控数据对接到个人系统长期保存,建议用远程存储或对象存储做归档。另外别把所有高基数指标一股脑全接进来,比如把每个用户的请求ID都作为label,那存储会直接爆炸。label尽量保留服务名、模型名、版本、路由等稳定维度,凡是取值经常变化的标签都要警惕。
3. Prometheus监控部署与个人系统集成
3.1 从零部署一套可用的监控环境
Prometheus监控部署是整个方案的地基。我选择Prometheus的核心原因有三个:一是生态成熟,vLLM、TensorRT-LLM这类推理框架都原生暴露了/metrics端口;二是它采用pull模型,服务端不需要主动推送,天然避免了对推理进程的侵入;三是存储和查询一体化,个人系统里跑一个容器就能用,不需要搭一堆中间件。
最省事的部署方式是用docker-compose把Prometheus和Grafana一起拉起来。下面这个配置是我常用的最小可用环境:
yaml复制version: '3'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=15d'
- '--web.enable-lifecycle'
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=your_strong_password
volumes:
- grafana-data:/var/lib/grafana
depends_on:
- prometheus
restart: unless-stopped
volumes:
prometheus-data:
grafana-data:
对应的Prometheus配置文件,我一般把目标服务的地址都写在scrape_configs里。如果你的推理框架跑在同一台机器,直接用容器IP或宿主机IP都行;如果是Docker容器,推荐用host.docker.internal这类地址指向宿主机,避免每次容器重建都要改IP。
yaml复制global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'vllm-inference'
static_configs:
- targets: ['10.0.0.10:8000']
metrics_path: /metrics
把这套环境跑起来之后,在Grafana里添加Prometheus数据源,你的个人系统就具备了一套完整的监控底座。后面新增任何服务,只需要往scrape_configs里加一个target,再重载配置即可。
3.2 采集vLLM等推理框架的延迟指标
如果你用的是vLLM部署模型,那监控接入比想象中简单。vLLM从很早的版本就内置了Prometheus指标,默认在8000端口/metrics路径下暴露。你不需要在代码里做任何埋点,常用指标包括:
vllm:time_to_first_token_seconds_bucket/sum/count:首token延迟直方图。vllm:e2e_request_latency_seconds_bucket/sum/count:端到端请求延迟直方图。vllm:generation_tokens_total:生成的token总数。vllm:num_requests_running、vllm:num_requests_waiting:当前运行和排队的请求数。
我在生产环境踩过一个小坑:有段时间服务卡顿,但Grafana里的延迟分位数曲线没有任何异常,后来才发现新版本的vLLM把指标名改了,旧面板查询的指标已经不存在,空数据看起来当然“健康”。所以升级vLLM之后,第一件事就是去/metrics页面确认指标名有没有变化,别偷懒。
有了这些指标,你在Grafana里就能画出关键图表和设置告警。例如端到端P99延迟的PromQL可以这样写:
promql复制histogram_quantile(0.99,
sum by (le) (rate(vllm:e2e_request_latency_seconds_bucket[5m]))
)
首token延迟P95则是:
promql复制histogram_quantile(0.95,
sum by (le) (rate(vllm:time_to_first_token_seconds_bucket[5m]))
)
这两条查询是我最常用的“体检项”。每次模型上线或调整参数后,我都会盯一段时间这两个曲线,确认没有劣化再放量。
3.3 自研服务如何暴露延迟指标
如果你的推理服务不是直接用vLLM这类框架,而是自己用Python封装了一层,那也可以很方便地用prometheus_client库暴露指标。我写过不少这类代码,基础写法是定义一个Histogram,然后把一次推理的耗时塞进去:
python复制from prometheus_client import Histogram, start_http_server
import time
INFERENCE_LATENCY = Histogram(
'inference_request_seconds',
'Inference request latency in seconds',
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10),
labelnames=['model_name', 'route']
)
def predict(model_name: str, input_text: str):
start = time.perf_counter()
try:
# 你的推理逻辑
result = do_inference(model_name, input_text)
return result
finally:
INFERENCE_LATENCY.labels(model_name=model_name, route='/predict').observe(
time.perf_counter() - start
)
这里有几个细节值得注意。observe方法才是记录观测值的核心,装饰器time()虽然能用,但只能装饰同步函数,异步接口里很容易漏掉挂起时间。对于异步推理,我会在请求开始时记录start_time,等真正拿到结果后再计算耗时并observe,这样统计的是端到端用户感知时间,而不是函数内某个微小的局部时间。另外bucket列表要贴合自己的延迟分布,如果你的服务普遍在几十毫秒量级,那就把1秒以上的桶去掉一半,让精度集中在常用区间。
最后在主进程里再起一个HTTP服务暴露metrics:
python复制start_http_server(8001)
这样Prometheus只需要把target指向服务IP:8001就能稳定抓取。个人系统接入时,我会给每个服务单独分配一个端口,避免端口冲突,也用job_name区分。
3.4 Grafana面板和告警规则配置
Grafana面板本身是可视化,真正让监控“活”起来的是告警规则。告警配置我是在Prometheus里做的,而不是全部扔给Grafana,原因是Prometheus的告警规则可以和指标查询放在同一个版本管理目录下,变更可追溯。
下面这个告警规则会检测P99延迟是否超过2秒并持续10分钟,一旦触发就告警。为什么持续10分钟?因为延迟偶发抖动很常见,持续10分钟才说明是真实劣化,误报率会大幅降低。
yaml复制groups:
- name: inference-latency-alerts
rules:
- alert: InferenceP99LatencyHigh
expr: |
histogram_quantile(0.99,
sum by (le) (rate(vllm:e2e_request_latency_seconds_bucket[5m]))
) > 2
for: 10m
labels:
severity: warning
annotations:
summary: "推理服务P99延迟超过2秒"
description: "服务当前P99延迟已超过2秒并持续10分钟,请检查GPU利用率和请求排队数。"
- alert: InferenceQueueTooLong
expr: |
vllm:num_requests_waiting > 20
for: 5m
labels:
severity: critical
annotations:
summary: "推理请求排队堆积"
description: "等待处理的请求数量超过20,持续5分钟,很可能发生了资源瓶颈或死锁。"
告警通知我习惯直接发到Webhook,个人系统里可以接飞书、钉钉、企业微信机器人,也可以转发到邮件。关键是要给每条告警配上排查建议,描述里写清“下一步看什么”,这样半夜收到告警时你不会慌。
4. 推理服务卡顿与延迟高的排查实操
4.1 从现象到根因:一次线上LLM推理变慢的完整排查过程
有了监控之后,排查延迟问题就从“瞎猜”变成了“按图索骥”。我复盘一个典型的定位过程。
现象是模型回复明显卡顿,用户反馈“半天才蹦两个字”。我打开Grafana面板,先看端到端P99,已经冲到4秒,正常只有1秒。接着看首token延迟,发现它从500ms涨到了2秒,这说明问题主要出现在推理链路前半段,而不是token生成阶段。
再往下看vllm:num_requests_running和vllm:num_requests_waiting,发现waiting数量持续在高位,说明请求在排队。为什么排队?两种可能:要么请求量涨了,要么单请求处理时间变长,系统吞吐下降。查询请求QPS,并没有明显上涨,所以问题变成了“为什么每个请求变慢了”。
这一步就要看GPU层指标了。用nvidia-smi看了GPU利用率,发现利用率一直在100%,但显存剩余不多。再回看监控里的KV cache使用率,发现已经非常高,接近上限。真相浮出水面:上下文很长,每个请求占用的KV cache都很大,在有限的显存内并发数量被压低,新来的请求只能排队。再加上某些请求触发了显存碎片整理,导致偶发超时。
最终解决方式是调低--max-model-len、降低单请求的最大token限制,同时限制服务最大并发,避免无限排队。这些都是靠监控数据才敢做的决策,没有指标前根本不知道KV cache才是瓶颈。
4.2 Qwen3 8B FP8 vLLM部署延迟高怎么办
很多人喜欢用FP8量化模型降低显存需求,但部署后经常反馈“感觉有延迟,会慢或者会卡顿”。我自己在vLLM上部署Qwen3 8B FP8也遇到过类似情况,总结下来有几个高频原因。
第一是vLLM版本太旧,对FP8的支持不够成熟。某些老版本会把FP8的算子回退到更慢的兼容实现,性能反而不如同尺寸BF16。我的建议是优先使用官方发布的新版本,并在模型加载日志里确认量化类型是FP8还是FP8_E4M3,如果看到任何fallback或not supported字样,基本就可以怀疑是算子回退。
第二是显存参数配置不合理。很多人只想让模型能跑起来,于是把--gpu-memory-utilization设得很保守,结果留给KV cache的空间太少,并发稍微上去就开始排队。我通常先设为0.85起步,然后根据显存余量微调。对8B模型,如果显存有24GB,FP8模型权重占8GB左右,剩下的空间基本都能留给KV cache。
第三是--max-num-seqs默认值可能过小,导致量化模型的自注意力并发上不去。这个参数影响单次Batch内最多同时处理的序列数,我一般需要结合显存和请求并发来调整,太小会浪费算力,太大又容易爆显存。建议从32开始压测,观察vllm:num_requests_waiting和延迟曲线,找到最佳值。
第四是前缀缓存没有开启。如果很多请求都有共同的前缀,比如Agent任务里固定的系统提示词,打开--enable-prefix-caching后可以大幅减少重复预计算,首token延迟会有肉眼可见的下降。这个开关在vLLM新版本里默认启用,但老版本可能还是默认关闭,务必检查启动参数。
4.3 别忽略网络延迟和外部依赖
推理服务变慢,还真不一定都在推理本身。我有一次排查半天,最后发现是服务里的一个工具函数会去调用外部向量数据库做召回,而那次外部服务刚好在GC,导致每次查询增加了800ms。
如果你在Agent或RAG应用中接入多个外部依赖,建议在监控里单独记录每个下游调用的延迟。方法上可以给每个外部调用定义一个Histogram标签,比如dependency="embedding"、dependency="rerank"。这样当端到端延迟飙升时,你首先去对比不同依赖的P99,如果是某个依赖明显增加,问题就不在模型推理。
网络传输层面的延迟怎么查?我的习惯是用ping和traceroute看基础连通性,再用curl -w直接测量HTTP请求各阶段耗时,包括DNS解析、TCP握手、TLS握手、首字节时间。其实很多“AI服务卡”的案例,最后都落在跨区域网络往返或本地TCP窗口调优这种问题上。推理服务器最好选择离模型服务最近的区域,能同机房就不要跨可用区调通,能内网就不要走公网。
4.4 常见问题速查表
这里整理了一张我日常排查用的速查表,直接照着处理能省不少时间:
| 现象 | 优先查看的指标 | 常见根因 | 处理方向 |
|---|---|---|---|
| 首token延迟高 | vllm:time_to_first_token_seconds |
前缀缓存未开启、prompt过长 | 开启前缀缓存,压缩上下文 |
| 整体响应卡顿 | vllm:num_requests_waiting |
并发超过服务上限 | 调整max-num-seqs或扩容 |
| GPU利用率低但延迟高 | KV cache使用率 | KV cache不足导致排队 | 提高gpu-memory-utilization |
| 延迟偶发尖刺 | GC耗时、网络重传 | 外部依赖抖动 | 对下游依赖加熔断和超时 |
| 模型吞吐上不去 | 生成token速率 | FP8算子回退或模型量化异常 | 升级vLLM版本,检查日志 |
| 客户端等待但服务端空闲 | 网络往返时间 | 跨区域链路问题 | 迁移服务区域或走内网 |
这张表不是万能的,但它能帮你把排查路径收敛到一两个方向。监控的价值就是让你不要再从零开始猜。
5. 个人系统监控方案的进阶思路
5.1 让监控数据提供更大价值
监控搭好只是开始,数据用起来才算闭环。我后来在个人系统里加了一个简单的“周报”脚本,每天凌晨把过去24小时的P50/P95/P99延迟、请求量、GPU利用率统计出来,发到自己的邮箱。这样即使没有告警,我也能发现某些规律,比如周五下午请求量大导致P99偏高,或者某次模型版本发布之后延迟悄悄升高了2%。
如果你愿意更进一步,可以把监控数据接入AI Agent。Prometheus提供了HTTP API,你可以让Agent定时拉取指标,出现异常时自己先做一轮初步分析,再把结论和可能的修复建议发给你。这其实才是“AI辅助运维”比较务实的落地方式,不是让AI直接改配置,而是让它帮你缩小排查范围。
5.2 保持方案可持续的小习惯
最后分享几个我踩坑之后养成的习惯。监控配置一定要用版本管理,prometheus.yml和告警规则文件都放进Git,每次改动都留记录。服务升级后一定要重新检查指标名和类型,别拿旧面板硬套新版本。告警规则要定期做“断奶测试”,故意把某个服务停掉看告警会不会触发,不触发的规则等于没有规则。
我个人在实际操作中的体会是,延迟监控方案最难的从来不是装一个Prometheus或画一张Grafana面板,而是你愿不愿意把“延迟”这件事拆得足够细,并持续根据反馈调整指标和告警阈值。这套方案我用了很长时间,每次遇到推理变慢,都能在十几分钟内定位到大概方向。你完全可以先按本文的步骤在个人系统里搭一套最小版本,跑起来之后再一点点加料。
