尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

大模型推理可观测性实战:Token统计、TTFT/TPOT延迟归因与成本核算

大模型推理可观测性实战:Token统计、TTFT/TPOT延迟归因与成本核算 1. 大模型推理可观测性到底在解决什么问题1.1 从一次线上告警说起去年下半年我负责维护的一个内部推理服务突然收到告警P99 延迟从 1.8 秒飙到 11 秒但 GPU 利用率、显存占用、请求 QPS 三个指标全都正常。运维同学第一反应是网络抖动查了半小时没结果。最后把单次请求的输入 Token 数和输出 Token 数打出来对比才发现问题某个上游业务把系统提示词从 200 Token 悄悄改到了 3000 Token导致 prefill 阶段计算量暴涨而 decode 阶段因为输出很短整体 QPS 没变化GPU 利用率自然看不出异常。这件事让我彻底意识到一个问题传统服务的可观测性三件套Metrics、Logging、Tracing直接套到大模型推理上是不够用的。CPU 密集型服务的瓶颈在指令数而大模型推理的瓶颈在 Token——输入多少 Token、输出多少 Token、每个 Token 花多少毫秒、KV Cache 命中多少这些才是真正决定成本和延迟的变量。所谓大模型日志与可观测性说白了就是给每一次推理请求建立一份体检报告这次请求吃了多少 Token、首 Token 延迟TTFT多少、每 Token 输出间隔TPOT多少、总耗时多少、命中了多少缓存、花了多少钱。没有这份报告你既不知道钱花在哪也不知道延迟卡在哪。1.2 谁需要这套东西这套东西不是只有大厂才需要。我梳理了一下下面几类角色都绕不开推理服务开发者要定位延迟毛刺、优化吞吐必须知道时间花在 prefill 还是 decode。平台/运维同学要按业务线分摊 GPU 成本必须按 Token 用量计费。算法/微调同学要评估微调后模型是否变啰嗦了必须对比微调前后的输出 Token 分布。业务方要知道一次对话大概烧多少钱才能决定要不要上大模型。我见过太多团队模型部署起来了接口也能调通但一问你们一次请求平均多少 Token、P99 延迟多少没人答得上来。这种状态下做容量规划和成本控制基本靠拍脑袋。1.3 核心指标先定义清楚在动手之前必须把指标口径统一否则后面数据全是乱的。我按重要性排了个序指标英文/缩写含义为什么重要首 Token 延迟TTFT从请求发出到第一个 Token 返回的时间直接决定用户卡不卡的体感单 Token 输出间隔TPOT / ITLdecode 阶段平均每个 Token 的生成时间决定输出流不流畅输入 Token 数prompt_tokens本次请求的输入长度决定 prefill 计算量和成本输出 Token 数completion_tokens本次生成的输出长度决定 decode 总时长和成本总 Token 数total_tokens输入输出计费基准端到端延迟E2E Latency整个请求的总耗时用户感知的总时间吞吐Throughput每秒处理的 Token 数/请求数决定要几张卡KV Cache 命中率cache hit rate前缀缓存复用比例直接影响 TTFT 和成本提示TTFT 和 TPOT 一定要分开统计。很多团队只统计端到端延迟结果优化时完全不知道该动 prefill 还是 decode这是最常见的坑。2. 日志埋点方案怎么设计才不返工2.1 埋点位置的选择逻辑埋点位置决定了你能拿到什么数据。我一般分三层埋第一层网关层Gateway。在请求进入推理服务之前记录请求 ID、用户 ID、业务线、时间戳、原始 prompt 长度。这一层的好处是能拿到用户视角的延迟包括排队时间。很多团队漏掉排队时间导致明明服务内部很快用户却觉得很慢。第二层推理引擎层。这是核心。以 vLLM 为例它本身提供了丰富的 metrics通过/metrics端点暴露 Prometheus 格式的数据。关键指标包括vllm:time_to_first_token_seconds、vllm:time_per_output_token_seconds、vllm:num_requests_running、vllm:gpu_cache_usage_perc等。如果你用的是别的推理引擎思路一样找它暴露的 metrics 端点。第三层应用层。在业务代码里记录这次请求的业务语义比如是哪个功能模块、用户是否点了重新生成、是否命中了缓存。这一层是纯业务数据推理引擎给不了。三层数据靠一个全局唯一的request_id串起来。这个 ID 必须在网关层生成然后一路透传到推理引擎和业务层。2.2 为什么不用纯日志而要用指标日志组合我踩过一个坑一开始所有数据都往日志里打结果一天几百万条日志ES 集群直接扛不住查询还慢。后来改成指标走 Prometheus明细走日志的组合高频、聚合类数据QPS、延迟分位数、Token 总量走 Prometheus采样成本低聚合查询快。低频、明细类数据单次请求的完整 Token 明细、异常请求的完整上下文走日志用于事后排查。这个分工的逻辑是你不可能对每一次请求都做全量明细存储成本扛不住但聚合指标又无法回答这次异常请求到底发生了什么。所以两者必须配合。2.3 采样策略全量还是抽样这里有个反直觉的结论Token 用量和延迟的聚合指标必须全量统计但明细日志可以抽样。原因很简单Token 用量是计费依据少统计一次就少收一次钱必须全量。而明细日志主要用于排查抽样 10% 已经足够发现规律性问题。我的做法是聚合指标100% 全量上报。明细日志正常请求采样 5%~10%异常请求延迟超阈值、报错100% 全量记录。这样既控制了存储成本又保证了排查时不会恰好没采到。3. 核心指标采集的实操细节3.1 TTFT 和 TPOT 到底怎么算这两个指标看着简单实际算起来有讲究。TTFT 是请求发出到第一个 Token 到达但请求发出这个时间点从哪算从网关收到请求算还是从推理引擎开始计算算我的建议是两个都算分别叫用户侧 TTFT和引擎侧 TTFT。两者之差就是排队和调度时间。如果用户侧 TTFT 远大于引擎侧说明瓶颈在排队该扩容了如果两者接近但都很大说明瓶颈在 prefill 计算该优化模型或加算力了。TPOT 的计算公式是TPOT (E2E Latency - TTFT) / (completion_tokens - 1)注意分母是completion_tokens - 1因为第一个 Token 的时间已经算在 TTFT 里了。这个细节很多人搞错导致 TPOT 偏小。3.2 用代码把埋点串起来下面是我实际项目里用的一段 Python 埋点代码基于 OpenAI 兼容接口思路通用import time import uuid from prometheus_client import Histogram, Counter TTFT_HIST Histogram(llm_ttft_seconds, Time to first token, buckets[0.1, 0.25, 0.5, 1, 2, 5, 10]) TPOT_HIST Histogram(llm_tpot_seconds, Time per output token, buckets[0.005, 0.01, 0.02, 0.05, 0.1, 0.5]) TOKEN_COUNTER Counter(llm_tokens_total, Total tokens, [type, biz]) def call_llm(prompt, biz_line): request_id str(uuid.uuid4()) start time.perf_counter() first_token_time None output_tokens 0 # 流式调用逐 Token 计时 for chunk in stream_chat(prompt): if first_token_time is None: first_token_time time.perf_counter() TTFT_HIST.observe(first_token_time - start) output_tokens 1 end time.perf_counter() if output_tokens 1: tpot (end - first_token_time) / (output_tokens - 1) TPOT_HIST.observe(tpot) TOKEN_COUNTER.labels(typecompletion, bizbiz_line).inc(output_tokens) return request_id这段代码的关键点用time.perf_counter()而不是time.time()因为前者是单调时钟不受系统时间调整影响流式调用才能精确拿到 TTFT非流式调用只能拿到总时间。3.3 输入 Token 数怎么准确获取输入 Token 数最好直接用推理引擎返回的usage.prompt_tokens不要自己用 tokenizer 估算。原因有两个一是不同模型的 tokenizer 不一样自己算容易错二是推理引擎返回的是实际参与计算的 Token 数包含了 chat template 拼接后的系统提示词这才是真实成本。如果你用的是 vLLM它会在响应的usage字段里返回准确的prompt_tokens和completion_tokens。如果引擎不返回那就只能自己用对应模型的 tokenizer 算但要记得把 chat template 也算进去。注意很多团队统计 Token 时只算用户输入的文本漏掉了系统提示词和对话历史。一个多轮对话场景系统提示词加历史可能有几千 Token漏算会导致成本统计严重偏低。4. 延迟归因把毛刺拆到具体环节4.1 延迟到底花在哪几个阶段一次推理请求的延迟可以拆成这几段网络传输请求从客户端到网关的时间。排队等待请求在推理引擎队列里等 GPU 的时间。Prefill 阶段处理输入 Token生成 KV Cache。Decode 阶段逐个生成输出 Token。回传Token 流式返回客户端的时间。大部分团队只统计了 34忽略了 1、2、5。但实际排查中排队时间第 2 段经常是延迟毛刺的元凶——当并发请求超过 GPU 处理能力时请求会在队列里堆积TTFT 暴涨。4.2 用滑动窗口做延迟平滑延迟数据天然抖动大直接看瞬时值会被噪声干扰。我一般用滑动窗口做平滑窗口大小取 1 分钟或 5 分钟。这里涉及一个滑动窗口滤波器的思路对窗口内的延迟数据取分位数P50、P95、P99而不是平均值。为什么不用平均值因为平均值会被极端值拉偏而且掩盖了长尾问题。用户体感差往往是因为 P99 高而不是平均值高。一个服务平均延迟 500ms 但 P99 是 10 秒用户体验就是时不时卡一下平均值完全反映不出来。4.3 一个真实的延迟归因案例回到开头那个案例我最后是怎么定位的把延迟按阶段拆开后发现阶段正常请求异常请求排队20ms25msPrefill180ms8500msDecode1200ms1300ms回传50ms55msPrefill 从 180ms 涨到 8500ms其他阶段几乎没变。这就锁定了问题在输入 Token 数。再对比输入 Token 分布发现异常请求的输入 Token 从平均 300 涨到了 3200。根因就是上游改了系统提示词。如果没有分阶段埋点只看端到端延迟你永远不知道是哪个环节出了问题。5. 成本核算与 Token 计费落地5.1 按 Token 计费的实现思路Token 计费的核心是谁用了多少 Token。实现上分两步第一步采集。每次请求记录user_id、biz_line、prompt_tokens、completion_tokens。这些数据在推理引擎层就能拿到。第二步聚合。按用户、按业务线、按天做聚合算出总 Token 数再乘以单价。这里有个细节输入和输出的单价通常不一样输出更贵因为 decode 更耗算力。所以计费时要分开算cost prompt_tokens * input_price completion_tokens * output_price5.2 缓存命中怎么算钱现在很多推理引擎支持前缀缓存Prefix Caching命中的部分不需要重新计算 prefill成本更低。但计费时要不要给用户打折我的做法是按实际计算量计费缓存命中的部分按折扣价。具体实现上vLLM 会返回num_cached_tokens之类的字段用prompt_tokens - cached_tokens作为实际计费的输入 Token 数。这样既公平也能激励业务方复用相同的前缀比如固定的系统提示词。5.3 成本异常告警光统计不够还要能告警。我设了几条规则单次请求 Token 数超过阈值比如 8000触发告警防止有人误传超长文本。单业务线日 Token 用量环比增长超过 50% 触发告警防止异常调用。单用户日 Token 用量超过配额触发限流。这几条规则帮我拦下过好几次事故最典型的一次是某个测试脚本忘了加循环退出条件一晚上烧了几百万 Token。6. 常见问题与排查技巧实录6.1 排查速查表现象可能原因排查方向TTFT 高但 TPOT 正常输入 Token 太多或排队严重看 prompt_tokens 分布和队列长度TTFT 正常但 TPOT 高decode 阶段算力不足或 batch 太大看 GPU 利用率和 batch size端到端延迟高但引擎指标正常网络回传慢或客户端处理慢看网关到客户端的耗时Token 统计和引擎对不上漏算系统提示词或 chat template对比 usage 字段和自算值延迟毛刺周期性出现定时任务抢占资源或缓存失效对齐毛刺时间和任务调度时间缓存命中率突然下降前缀被改动或缓存被清检查系统提示词是否变更6.2 几个我踩过的坑坑一用平均值看延迟。前面说过平均值会掩盖长尾。我现在的习惯是 P50、P95、P99 一起看任何一个异常都要查。坑二Token 统计口径不统一。网关层算一次、引擎层算一次、业务层又算一次三个数对不上。后来统一规定以推理引擎返回的 usage 为准其他层只做透传不做二次计算。坑三日志里打了完整 prompt。这既是隐私风险也是存储灾难。后来改成只打 prompt 的哈希值和长度需要排查时再根据 request_id 去受控的地方取原文。坑四忘了统计失败请求。失败的请求也消耗了资源比如 prefill 算完了但 decode 报错如果不统计成本会漏算。现在我把失败请求也纳入 Token 统计只是标记状态为 failed。6.3 一个容易被忽略的指标队列等待时间很多推理引擎的 metrics 里没有直接暴露排队时间但你可以用当前时间 - 请求入队时间来算。这个指标在扩容决策上非常关键如果排队时间持续大于 TTFT说明瓶颈在并发能力加卡比优化模型更有效。我一般会设一条规则排队时间 P95 超过 500ms 就触发扩容评估。这条规则比看 GPU 利用率靠谱得多因为 GPU 利用率高不代表排队严重可能 batch 打得好利用率低也不代表不排队可能调度有问题。7. 可视化看板怎么搭才有用7.1 看板分层原则看板不是指标越多越好我一般分三层总览层QPS、P99 延迟、Token 总量、成本。给管理层看一眼知道健康度。诊断层TTFT/TPOT 分位数、排队时间、缓存命中率、各业务线 Token 分布。给开发和运维看用于定位问题。明细层单请求的完整链路追踪。给排查具体问题时用。三层之间可以下钻总览发现异常点进诊断层看哪个环节再点进明细层看具体请求。7.2 告警阈值怎么定阈值不能拍脑袋要基于历史数据。我的做法是先跑两周收集基线然后按 P99 的 1.5 倍设告警阈值。比如历史 P99 延迟是 2 秒那告警阈值设 3 秒。这样既能捕捉异常又不会因为正常波动频繁误报。另外告警要分级P99 超过 1.5 倍发 warning超过 3 倍发 critical。不同级别走不同的通知渠道避免告警疲劳。8. 我个人的几点实操体会这套可观测性体系我从零搭到能用前后迭代了三四个月。最大的体会是不要一上来就追求大而全。我一开始想做一个覆盖所有指标的完美系统结果拖了很久没上线。后来改成先上 TTFT、TPOT、Token 数三个核心指标能跑起来再逐步加两周就上线了后面再慢慢补。第二个体会是指标口径一定要在团队内对齐。我们曾经因为延迟从哪算起这个问题争论了一周最后发现大家说的根本不是一回事。现在我们的做法是所有指标定义写进文档新人入职第一件事就是读这份文档。第三个体会是可观测性本身也有成本。埋点、存储、查询都要花钱。所以要有取舍高频聚合指标全量明细日志抽样异常请求全量。这个平衡点需要根据业务量慢慢调。最后一个建议如果你现在还没做任何 Token 和延迟统计别想着一步到位。先在最外层加一个简单的计时和 Token 计数把数据跑起来哪怕只是打到日志里。有了数据你才知道下一步该优化什么。没有数据的时候所有的优化都是猜。
返回列表