ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

大模型推理可观测性实战:Token级监控与延迟优化

2026/10/5 8:33:24 拓冰建站 浏览量
大模型推理可观测性实战:Token级监控与延迟优化 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 - t2prefill 耗时 t4 - t3decode 耗时 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_tokensFalse)) prompt_tokens count_tokens(prompt) completion_tokens count_tokens(generated_text)这里有个细节add_special_tokens参数要跟实际推理时保持一致。有些模型会在 prompt 前后自动加 BOS/EOS如果你统计时没加就会少算几个 Token长期累积下来对账会有偏差。2.3 流式场景下的延迟采集现在大部分大模型应用都是流式输出streamingToken 是一个一个吐出来的。这种场景下延迟采集要特别处理不能等整个响应结束才算总时间。我的做法是在流式回调里记录每个 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 个 TokenTPOT 会被拉低看起来“很快”但用户体感是“一顿一顿的”。所以除了平均 TPOT我还建议记录TPOT 的 P95 和最大值抖动比平均值更能反映体验问题。2.4 埋点不能拖慢推理这是很多人忽略的一点可观测性本身也是有开销的。如果你在每个 Token 生成时都同步写一次数据库那推理性能会被拖垮。我见过一个项目加了详细埋点后 QPS 直接掉了 30%就是因为埋点写库是同步阻塞的。正确姿势是异步 批量。埋点数据先写进内存队列由独立线程批量刷到后端import queue import threading metrics_queue queue.Queue(maxsize10000) def metrics_worker(): batch [] while True: try: item metrics_queue.get(timeout1) batch.append(item) if len(batch) 100: flush_to_backend(batch) batch [] except queue.Empty: if batch: flush_to_backend(batch) batch [] threading.Thread(targetmetrics_worker, daemonTrue).start()队列要设上限满了就丢弃并计数丢弃本身也是个指标说明埋点压力过大。刷盘批量大小 100 左右比较合适太小频繁 IO太大延迟高。3. 从零搭建一套可观测性链路3.1 技术选型Prometheus Grafana 是基本盘指标采集和展示我强烈建议用Prometheus Grafana这套组合。原因很实在生态成熟、社区案例多、和 Kubernetes 集成好、查询语言 PromQL 足够灵活。大模型推理的指标大多是时序数据随时间变化的延迟、Token 数正好是 Prometheus 的强项。具体分工是Prometheus负责拉取和存储指标做告警规则Grafana负责可视化做大盘和报表推理服务暴露一个/metrics接口用 Prometheus 客户端库埋点Python 服务用prometheus_clientfrom 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租户/业务方做成本分摊typeprompt 还是 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{typecompletion, tenanttenant_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 数”。这个值在正常情况下应该稳定在一个区间一旦明显下降说明引擎效率出了问题比单纯看延迟更敏感。这个指标我用了两年屡试不爽。