☰
大模型推理可观测性实战:Token级监控与延迟优化
2026/10/5 8:33:21 网站建设 项目流程

1. 大模型推理可观测性到底在解决什么问题

1.1 从一次线上告警说起

上个月凌晨两点,我被一条告警叫醒:某个对外提供的大模型推理接口 P99 延迟从 1.8 秒飙到了 11 秒,但 CPU、GPU、内存三项基础指标全都正常。运维同事第一反应是“网络抖动”,业务同事怀疑“上游流量突增”,而我盯着监控面板看了十分钟,发现真正的问题藏在一个没人关注的维度里——输出 Token 数。那段时间有一批用户开始用长文本摘要任务,单次请求的输出 Token 从平均 120 涨到了 900 多,而我们的推理引擎还在用固定 batch 策略,导致显存碎片化严重,调度队列越堆越长。

这件事让我彻底意识到:传统的那套 CPU/内存/网络监控,在大模型推理场景下几乎是失灵的。你看到的是资源利用率不高,但用户体感是“卡得没法用”;你看到的是 QPS 稳定,但成本账单在悄悄翻倍。原因很简单——大模型推理的核心成本单元和性能单元,不是“请求数”,而是Token;它的核心体验指标,不是“响应时间”,而是首 Token 延迟(TTFT)和单 Token 输出延迟(TPOT)。

这篇内容我想聊的就是:怎么给大模型推理做一套真正有用的可观测性体系,把每一次推理的 Token 消耗和延迟都追踪清楚。适合正在做大模型部署、推理引擎调优、AI 应用后端的工程师,也适合刚接触LLM 服务化、想搞清楚“钱花在哪、慢在哪”的同学。不管你是用 vLLM、LocalAI、Ollama 还是自研推理服务,这套思路都能直接套用。

1.2 为什么“请求级监控”不够用

先讲清楚一个认知差。传统 Web 服务的监控粒度是“请求”:一个请求进来,耗时 200ms,成功或失败,完事。但大模型推理完全不是这个模型。同一个接口,A 用户问“今天天气怎么样”,输出 15 个 Token,耗时 0.6 秒;B 用户让它写一篇 2000 字的产品文案,输出 1800 个 Token,耗时 40 秒。这两个请求在“请求级监控”里长得一模一样——都是 1 次调用、1 次成功。但它们的资源消耗差了上百倍,用户体验也天差地别。

所以可观测性必须下沉到Token 粒度。具体来说,一次推理要能拆出这几个关键量:

指标含义为什么重要
输入 Token 数(prompt tokens)用户提示词被分词后的长度决定 prefill 阶段的计算量,直接影响 TTFT
输出 Token 数(completion tokens)模型生成的 Token 总数决定 decode 阶段总耗时和计费成本
首 Token 延迟 TTFT从请求发出到第一个 Token 返回用户感知“快不快”的第一印象
单 Token 输出延迟 TPOT后续每个 Token 的平均生成间隔决定“打字机”是否流畅
总延迟整个请求端到端耗时计费、SLA、容量规划的基础
队列等待时间请求在调度队列里排队的时间区分“引擎慢”还是“排队慢”

把这六个量采集全,你才能回答那些真正要命的问题:为什么这个用户的请求特别慢?是 prompt 太长导致 prefill 卡住,还是输出太长导致 decode 拖尾?为什么这个月的成本涨了 30%?是调用量涨了,还是平均输出 Token 涨了?为什么高峰期延迟抖动?是 GPU 打满了,还是调度队列积压了?

1.3 可观测性的三层结构

我在实际项目里把大模型可观测性分成三层,从下往上依次是:

第一层是资源层,也就是 GPU 利用率、显存占用、KV Cache 使用率、CPU、网络。这一层解决“机器累不累”的问题,是基础但不是核心。

第二层是推理层,也就是上面那张表里的 Token 和延迟指标。这一层解决“引擎快不快、贵不贵”的问题,是整套体系的核心。

第三层是业务层,比如每个租户/用户的 Token 消耗、每个业务场景的平均输出长度、成本分摊。这一层解决“钱花在谁身上、哪个功能最烧钱”的问题。

很多团队只做了第一层,所以永远在“加机器”和“降延迟”之间反复横跳,却找不到根因。真正有效的做法是三层打通:资源层的异常能关联到推理层的具体请求,推理层的成本能归因到业务层的具体租户。

2. 核心指标定义与埋点设计

2.1 指标口径必须先对齐,否则全是废数据

我踩过最大的坑,就是指标口径不统一。前端统计的“响应时间”是从用户点击按钮开始算,后端统计的“推理延迟”是从收到 HTTP 请求开始算,中间差了网络传输、网关转发、鉴权校验一大截。结果两边对不上账,排查问题时互相甩锅。

所以第一步,必须把每个指标的时间戳打点位置定义死。我一般要求至少打这几个时间戳:

  • t0:客户端发起请求(前端埋点)
  • t1:网关收到请求
  • t2:推理服务收到请求,进入调度队列
  • t3:引擎开始 prefill(拿到 GPU 资源)
  • t4:第一个 Token 生成
  • t5:最后一个 Token 生成
  • t6:响应完整返回客户端

有了这七个点,就能算出:网络耗时 = t1 - t0,网关耗时 = t2 - t1,排队耗时 = t3 - t2,prefill 耗时 = t4 - t3,decode 耗时 = t5 - t4,回传耗时 = t6 - t5。哪个环节慢,一目了然。

注意:时间戳一定要用统一时钟源。分布式环境下不同机器的本地时钟可能有几十毫秒偏差,建议用 NTP 同步,或者干脆在网关层统一打点,避免跨机时钟误差污染数据。

2.2 Token 计数:别信估算,要信分词器

Token 数怎么来?很多人图省事,用“字符数除以 4”这种经验公式估算。这在英文场景下勉强能用,但中文、代码、特殊符号一多,误差能到 50% 以上。我见过一个团队按估算值做成本核算,月底对账发现实际账单比预估高了 40%,就是因为中文 Token 密度远高于英文。

正确做法是用模型自己的分词器(tokenizer)来数。主流推理引擎在返回结果时都会带上usage字段,比如:

{ "usage": { "prompt_tokens": 128, "completion_tokens": 512, "total_tokens": 640 } }

这个字段是引擎内部真实统计的,最准。如果引擎不返回(有些自研引擎会漏),那就得在服务层自己调 tokenizer 算一遍。以 HuggingFace 的 tokenizer 为例:

from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("your-model-path") def count_tokens(text: str) -> int: return len(tokenizer.encode(text, add_special_tokens=False)) prompt_tokens = count_tokens(prompt) completion_tokens = count_tokens(generated_text)

这里有个细节:add_special_tokens参数要跟实际推理时保持一致。有些模型会在 prompt 前后自动加 BOS/EOS,如果你统计时没加,就会少算几个 Token,长期累积下来对账会有偏差。

2.3 流式场景下的延迟采集

现在大部分大模型应用都是流式输出(streaming),Token 是一个一个吐出来的。这种场景下延迟采集要特别处理,不能等整个响应结束才算总时间。

我的做法是在流式回调里记录每个 chunk 的到达时间:

import time first_token_time = None token_times = [] def on_token(token: str): global first_token_time now = time.perf_counter() if first_token_time is None: first_token_time = now token_times.append(now) # 推理结束后计算 ttft = first_token_time - request_start_time if len(token_times) > 1: tpot = (token_times[-1] - token_times[0]) / (len(token_times) - 1) else: tpot = 0

这里用time.perf_counter()而不是time.time(),因为前者是单调时钟,不受系统时间调整影响,测间隔更准。TPOT 的计算要注意分母是len(token_times) - 1,因为第一个 Token 的时间已经算进 TTFT 了,不能重复计算。

实操心得:流式场景下,如果某个 chunk 特别大(比如一次返回 10 个 Token),TPOT 会被拉低,看起来“很快”,但用户体感是“一顿一顿的”。所以除了平均 TPOT,我还建议记录TPOT 的 P95 和最大值,抖动比平均值更能反映体验问题。

2.4 埋点不能拖慢推理

这是很多人忽略的一点:可观测性本身也是有开销的。如果你在每个 Token 生成时都同步写一次数据库,那推理性能会被拖垮。我见过一个项目,加了详细埋点后 QPS 直接掉了 30%,就是因为埋点写库是同步阻塞的。

正确姿势是异步 + 批量。埋点数据先写进内存队列,由独立线程批量刷到后端:

import queue import threading metrics_queue = queue.Queue(maxsize=10000) def metrics_worker(): batch = [] while True: try: item = metrics_queue.get(timeout=1) batch.append(item) if len(batch) >= 100: flush_to_backend(batch) batch = [] except queue.Empty: if batch: flush_to_backend(batch) batch = [] threading.Thread(target=metrics_worker, daemon=True).start()

队列要设上限,满了就丢弃并计数(丢弃本身也是个指标,说明埋点压力过大)。刷盘批量大小 100 左右比较合适,太小频繁 IO,太大延迟高。

3. 从零搭建一套可观测性链路

3.1 技术选型:Prometheus + Grafana 是基本盘

指标采集和展示,我强烈建议用Prometheus + Grafana这套组合。原因很实在:生态成熟、社区案例多、和 Kubernetes 集成好、查询语言 PromQL 足够灵活。大模型推理的指标大多是时序数据(随时间变化的延迟、Token 数),正好是 Prometheus 的强项。

具体分工是:

  • Prometheus负责拉取和存储指标,做告警规则
  • Grafana负责可视化,做大盘和报表
  • 推理服务暴露一个/metrics接口,用 Prometheus 客户端库埋点

Python 服务用prometheus_client:

from prometheus_client import Histogram, Counter, start_http_server TTFT = Histogram( 'llm_ttft_seconds', 'Time to first token', buckets=[0.1, 0.25, 0.5, 1, 2, 5, 10] ) TOKENS = Counter( 'llm_tokens_total', 'Total tokens processed', ['type', 'model', 'tenant'] ) start_http_server(8000)

Histogram的 buckets 要按实际延迟分布来设。大模型 TTFT 通常在 0.1 到几秒之间,所以我在 0.1、0.25、0.5、1、2、5、10 秒设了桶。桶设得太粗,P99 算不准;设得太细,存储成本高。经验是覆盖 P1 到 P99.9 的范围,中间均匀分布。

3.2 标签设计:决定你能查多细

Prometheus 的标签(label)设计是门学问。标签加得好,你能按任意维度下钻;加得烂,要么查不出来,要么基数爆炸把 Prometheus 撑死。

我的标签设计原则是:高频查询的维度做标签,低频的做日志。大模型场景下,这几个标签是必须的:

  • model:模型名称,区分不同模型的成本
  • tenant:租户/业务方,做成本分摊
  • type:prompt 还是 completion,区分输入输出
  • status:成功/失败/超时

但要注意标签基数(cardinality)。如果你把request_id做成标签,那每个请求都是一个独立时间序列,几万请求就能把 Prometheus 打爆。request_id这种唯一标识,应该放日志里,不放指标标签。

踩坑记录:我曾经把user_id做成标签,结果一个 C 端产品有几十万用户,Prometheus 内存直接爆了。后来改成只对 Top 100 的大客户做标签,其余归到other,问题才解决。

3.3 日志与 Trace 的配合

指标告诉你“哪里不对”,日志和 Trace 告诉你“为什么不对”。三者要能互相跳转。

我的做法是:每个请求生成一个trace_id,这个 ID 同时出现在指标标签(作为 exemplar)、结构化日志、以及分布式 Trace 里。当 Grafana 上看到某个时间点 TTFT 飙升,点开 exemplar 就能跳到对应的 Trace,看到这次请求的完整链路和日志。

结构化日志用 JSON 格式,方便检索:

import logging import json logger = logging.getLogger("llm") logger.info(json.dumps({ "trace_id": trace_id, "model": model_name, "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "ttft_ms": ttft_ms, "tpot_ms": tpot_ms, "total_ms": total_ms, "status": "success" }))

这样在日志系统里可以直接按completion_tokens > 1000这种条件筛选,快速定位长输出请求。

3.4 采样策略:全量还是抽样

高 QPS 场景下,全量采集日志和 Trace 成本很高。我的策略是指标全量、日志抽样、Trace 按需:

  • 指标(Prometheus)数据量小,全量采集,保证统计准确
  • 日志按比例抽样,比如 10%,但错误日志和慢请求(超过阈值)100% 采集
  • Trace 默认关闭,需要排查时对特定租户或特定时间段开启

这样既保证了统计指标的准确性,又控制了存储成本。慢请求全采是关键,因为排查问题时你最需要的就是那些“异常样本”。

4. 延迟与 Token 的联合分析方法

4.1 用 TTFT 和 TPOT 定位瓶颈

拿到 TTFT 和 TPOT 两个指标后,就能做二维定位了。我总结了一个判断表:

TTFTTPOT可能原因优化方向
高正常prompt 太长,prefill 慢压缩 prompt、开启 prefix caching
正常高decode 慢,显存带宽瓶颈量化、张量并行、换更快的 GPU
高高整体资源不足或排队严重扩容、优化调度、限流
正常正常网络或网关问题查网关、查回传链路

这个表在实际排查中非常管用。比如前面提到的凌晨告警,我一查发现 TTFT 正常但 TPOT 从 30ms 涨到了 90ms,立刻判断是 decode 阶段的问题,最后定位到是长输出请求把 KV Cache 撑满,导致显存换页。

4.2 排队时间:被忽视的隐形杀手

很多团队只看“推理耗时”,忽略了“排队耗时”。但在高并发场景下,排队时间往往才是延迟的大头。

举个例子:你的引擎单次推理只要 2 秒,但同时来了 50 个请求,GPU 一次只能处理 8 个,那剩下 42 个就得排队。第 50 个请求的排队时间可能长达 12 秒,端到端延迟 14 秒,但引擎自己“觉得”只花了 2 秒。

所以必须把排队时间单独采集。在调度层记录请求入队和出队的时间戳:

enqueue_time = time.perf_counter() # ... 等待调度 ... dequeue_time = time.perf_counter() queue_wait = dequeue_time - enqueue_time

排队时间的 P99 是容量规划的核心依据。如果排队时间经常超过 TTFT 本身,说明你的并发容量不够,该扩容了。

4.3 滑动窗口统计:别被瞬时值骗了

延迟指标波动很大,看瞬时值容易被误导。我一般用滑动窗口做平滑统计。Prometheus 的rate和histogram_quantile本身就是基于时间窗口的,比如:

# 过去 5 分钟的 P95 TTFT histogram_quantile(0.95, rate(llm_ttft_seconds_bucket[5m])) # 过去 5 分钟的平均 TPOT rate(llm_tpot_seconds_sum[5m]) / rate(llm_tpot_seconds_count[5m])

窗口大小的选择有讲究:窗口太小(如 1 分钟),曲线抖动大,看不出趋势;窗口太大(如 1 小时),反应迟钝,告警不及时。我的经验是告警用 5 分钟窗口,趋势分析用 1 小时窗口,两个都看。

注意:histogram_quantile算出来的是近似值,桶的边界越密越准。如果发现 P95 和 P99 差距异常大,可能是桶设置不合理,需要调整。

4.4 Token 消耗的成本归因

Token 不只是性能指标,更是成本指标。按 Token 计费的场景下,每个租户、每个功能消耗了多少 Token,直接对应真金白银。

我一般会做一个成本归因看板,按租户和模型维度聚合:

# 某租户过去 24 小时的 completion token 总量 sum(increase(llm_tokens_total{type="completion", tenant="tenant_a"}[24h]))

再乘以单价,就是成本。这样能快速发现“哪个租户最烧钱”“哪个功能 Token 消耗异常”。我遇到过一个小功能,因为 prompt 里塞了一大段没用的系统提示,导致每次请求的 prompt token 多了 800 个,一个月多花了好几万。这种问题,没有 Token 级别的归因根本发现不了。

5. 常见问题与排查技巧实录

5.1 指标对不上账怎么办

现象:前端统计的延迟和后端统计的对不上,差了几百毫秒甚至几秒。

排查思路:先确认时间戳打点位置是否一致。最常见的原因是前端从“用户点击”开始算,后端从“收到请求”开始算,中间的网络和网关耗时没算进去。解决办法是统一口径,或者干脆在网关层打一个“统一起点”,前后端都以这个为准。

另一个常见原因是时钟不同步。分布式环境下,如果各机器时钟有偏差,跨机计算的时间差就是错的。用 NTP 同步,或者所有时间戳都在同一台机器上打。

5.2 Token 数统计偏差

现象:自己统计的 Token 数和账单对不上。

排查思路:第一,确认是否用了模型自己的 tokenizer,而不是估算公式。第二,确认add_special_tokens参数是否和推理时一致。第三,如果是流式输出,确认是否把每个 chunk 的 Token 都算进去了,有没有漏算最后一个 chunk。第四,多模态场景下,图像 Token 的计算方式可能和文本不同,要单独处理。

5.3 埋点导致性能下降

现象:加了可观测性之后,推理 QPS 下降,延迟上升。

排查思路:检查埋点是否同步阻塞。所有埋点写入都应该是异步的,用内存队列缓冲,独立线程刷盘。另外检查指标标签基数是否过大,标签太多会导致 Prometheus 拉取和存储压力大。还有,Histogram 的桶不要设太多,几十个桶就够了。

5.4 延迟抖动大,找不到规律

现象:延迟时高时低,没有明显规律。

排查思路:先看是不是排队导致的。把排队时间和推理时间分开看,如果排队时间抖动大,说明并发调度有问题。再看是不是长输出请求拖尾,把 completion token 数和延迟做散点图,如果高 Token 请求延迟明显更高,说明需要针对长输出做优化(比如限制最大输出长度、开启 chunked prefill)。最后看 GPU 是否有其他任务抢占,显存是否频繁换页。

5.5 常见问题速查表

问题可能原因快速验证方法
TTFT 高prompt 太长 / prefix cache 未命中看 prompt token 分布
TPOT 高显存带宽瓶颈 / batch 太大看 GPU 显存带宽利用率
总延迟高但引擎快排队严重看队列等待时间
成本异常上涨输出 Token 变多看 completion token 趋势
指标缺失埋点被采样丢弃看队列丢弃计数
延迟抖动长输出请求拖尾做 Token-延迟散点图

5.6 几个我踩过的坑

第一个坑是只监控平均值。平均值会掩盖长尾问题,一个 P99 延迟 10 秒的系统,平均值可能只有 1 秒,看起来很美,但 1% 的用户在骂娘。一定要看 P95、P99。

第二个坑是告警阈值拍脑袋。延迟告警设多少合适?不能拍脑袋,要基于历史数据的 P99 来定,比如设成历史 P99 的 1.5 倍。而且要用滑动窗口,避免瞬时抖动误报。

第三个坑是忽略冷启动。模型刚加载完的第一批请求,延迟会明显偏高(显存预热、CUDA 编译等)。如果这部分数据混进统计,会污染指标。我的做法是启动后先跑一批预热请求,预热完成前的数据单独标记,不计入正式统计。

第四个坑是多模型共用一套指标。不同模型的延迟基线完全不同,混在一起看会互相干扰。一定要用model标签区分,分别设阈值。

6. 落地建议与扩展方向

6.1 从小处着手,别一上来就搞大而全

我见过太多团队一上来就想搞一套“全链路可观测平台”,结果做了三个月还没上线。我的建议是先做最小可用版本:先把 TTFT、TPOT、Token 数这三个核心指标采集起来,用 Grafana 出一个大盘,能看趋势、能下钻。这三样东西加起来,一天就能搞定,但已经能解决 80% 的问题。

等这套跑顺了,再逐步加排队时间、成本归因、Trace 关联。可观测性是迭代出来的,不是设计出来的。

6.2 把可观测性接进 CI/CD

更进一步,可以把延迟和 Token 指标接进 CI/CD 流程。每次模型更新或引擎升级,自动跑一批基准测试,对比新旧版本的 TTFT、TPOT、Token 效率。如果新版本延迟劣化超过 10%,直接卡住发布。这样能把性能回归挡在上线之前,而不是等用户投诉了才发现。

6.3 面向未来的扩展

随着推理引擎越来越复杂(多模态、MoE、投机解码),可观测性的维度也会越来越多。比如 MoE 模型要监控专家激活分布,投机解码要监控接受率。但核心思路不变:把每次推理拆成可度量的小单元,追踪每个单元的成本和延迟。这套方法论,从最早的 Transformer 到现在,一直适用。

我在实际项目里最大的体会是:可观测性不是“锦上添花”,而是“雪中送炭”。当你半夜被叫醒排查问题,当老板问你为什么成本涨了,当用户投诉“太慢了”,一套好的可观测性体系能让你在十分钟内给出答案,而不是花一整天去猜。Token 和延迟这两个维度,就是大模型推理的“任督二脉”,打通了,很多问题自然就清晰了。

最后分享一个小技巧:给每个请求的日志里加上completion_tokens / total_ms这个比值,也就是“每秒输出 Token 数”。这个值在正常情况下应该稳定在一个区间,一旦明显下降,说明引擎效率出了问题,比单纯看延迟更敏感。这个指标我用了两年,屡试不爽。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询