ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型推理可观测性实战:Token消耗、延迟拆解与全链路追踪

大模型推理可观测性实战:Token消耗、延迟拆解与全链路追踪 1. 为什么大模型推理的黑盒感比传统服务更让人头疼做过传统后端服务监控的人第一次接手大模型推理服务时大概率会经历一段相当长的适应期。传统 HTTP 接口的监控逻辑很直接QPS、P99 延迟、错误码分布、CPU 和内存水位一套 Prometheus 加 Grafana 基本就能把服务状态看得七七八八。但换成大模型推理之后你会发现原来那套指标体系突然变得不够用了——一个请求进来出去的时候可能消耗了几百个 Token也可能消耗了几万个延迟可能是 800 毫秒也可能是 40 秒显存占用在请求前后剧烈波动但 CPU 曲线却平得像一条直线。这种黑盒感的根源在于大模型推理的成本和性能不是由请求数量决定的而是由Token 数量和推理阶段共同决定的。同样是一次/v1/chat/completions调用输入 200 Token、输出 50 Token 的短问答和输入 8000 Token、输出 4000 Token 的长文档摘要对 GPU 的占用完全不在一个量级。如果你只按请求数做限流和容量规划线上一定会出现请求数没涨但 GPU 打满、延迟飙升的诡异现象。我在早期做推理服务的时候踩过这个坑。当时用请求数做限流阈值结果某天接入了一个做代码补全的业务方单请求平均输出 3000 Token直接把整块卡的显存吃满其他业务的 P99 延迟从 1.2 秒涨到了 15 秒。事后复盘才发现问题不在于请求量而在于我们根本没有观测 Token 维度的消耗。所以这篇内容想聊的核心就是怎么给大模型推理服务建立一套真正有用的可观测性体系把每一次推理的 Token 消耗、延迟构成、显存波动都追踪清楚。这套东西不是给运维看的漂亮仪表盘而是能直接指导你做容量规划、成本核算、性能优化的工程基础设施。适合正在部署或已经上线大模型推理服务的后端工程师、算法工程师和 SRE 参考无论你用的是 vLLM、LocalAI、Ollama 还是自研推理引擎思路都是相通的。2. 先搞清楚要观测什么Token 与延迟的拆解逻辑2.1 Token 消耗的三个维度输入、输出与缓存命中很多人统计 Token 的时候只记一个总数这在成本核算上勉强够用但在性能优化上几乎没用。真正有价值的 Token 观测至少要拆成三个维度。第一个是prompt tokens输入 Token。这个数字直接决定了 prefill 阶段的耗时。Prefill 是模型对输入序列做一次性前向计算、生成 KV Cache 的过程它的计算量和输入长度基本呈线性甚至超线性关系。输入从 500 Token 涨到 5000 Tokenprefill 耗时可能涨 10 倍以上。所以当你发现某个请求特别慢时第一件事就是看它的输入长度是不是异常。第二个是completion tokens输出 Token。输出 Token 决定了 decode 阶段的迭代次数。Decode 是自回归逐 Token 生成的过程每生成一个 Token 就要做一次完整的前向计算。输出 100 Token 和输出 2000 Token在 decode 阶段的时间差距是数量级的。这也是为什么流式输出streaming对用户体验如此重要——首 Token 延迟TTFT和总生成时间Total Latency是两个完全不同的指标。第三个是cached tokens缓存命中 Token。现在主流推理引擎都支持前缀缓存Prefix Caching如果多个请求共享相同的前缀比如相同的 system prompt这部分 Token 可以复用 KV Cache跳过重复计算。命中缓存的 Token 在计费和耗时上都应该单独统计否则你会误判某些请求的成本。把这三个维度分开记录之后你会发现很多之前看不懂的现象突然有了合理解释。比如某个业务方抱怨同样的接口为什么我的请求比别人慢三倍一查日志发现他的平均输入长度是别人的 8 倍问题一目了然。2.2 延迟不是单一数字TTFT、TPOT 与端到端延迟延迟这块最常见的错误就是只记录一个端到端总耗时。对于流式输出的场景这个数字几乎没有诊断价值。你需要至少拆成三个指标。TTFTTime To First Token首 Token 延迟是从请求发出到第一个 Token 返回的时间它主要反映 prefill 阶段的性能以及请求在队列里的排队时间。TTFT 高通常意味着两件事要么输入太长导致 prefill 慢要么请求排队严重、GPU 被占满。区分这两者很简单看队列等待时间指标就行。TPOTTime Per Output Token单 Token 生成时间是 decode 阶段平均每个 Token 的耗时它反映的是 decode 阶段的效率。TPOT 稳定说明推理引擎运行正常TPOT 抖动大可能是 batch 内请求数量波动、显存交换swap或者被其他进程抢占。端到端延迟E2E Latency就是 TTFT 加上 TPOT 乘以输出 Token 数再加上网络传输和后处理的时间。这个数字对用户最直观但对工程师诊断问题帮助有限。我习惯在日志里把这三个指标都打出来配合输入输出 Token 数基本上任何一个慢请求都能在 30 秒内定位到瓶颈在哪。下面这张表是我实际用的字段设计可以直接参考指标字段含义典型用途prompt_tokens输入 Token 数定位 prefill 瓶颈、成本核算completion_tokens输出 Token 数定位 decode 瓶颈、成本核算cached_tokens命中缓存的 Token 数评估前缀缓存收益ttft_ms首 Token 延迟毫秒诊断排队与 prefill 性能tpot_ms单 Token 生成时间毫秒诊断 decode 效率e2e_ms端到端总延迟毫秒用户体验监控queue_wait_ms排队等待时间毫秒区分排队与计算瓶颈model_name模型标识多模型场景区分request_id请求唯一标识全链路追踪2.3 为什么这些指标必须绑定在一起看单独看任何一个指标都会误导你。举个真实例子某天监控告警说 P99 延迟从 2 秒涨到了 8 秒如果只看这个数字你可能会以为是 GPU 出问题了。但把指标绑在一起看发现 TTFT 没变、TPOT 没变涨的全是 e2e而 completion_tokens 的平均值从 300 涨到了 1200。结论很清楚不是性能退化了是业务方的使用模式变了输出变长了。这种情况你不需要优化推理引擎需要的是调整容量规划或者跟业务方沟通。反过来如果 TTFT 涨了但输入长度没变那大概率是排队问题要看并发数和 GPU 利用率。如果 TPOT 涨了那才是真正的推理效率问题可能要查显存、查 batch 策略、查是否有其他进程干扰。提示把 Token 指标和延迟指标放在同一张时序图上对比是排查大模型性能问题最快的方法。我一般会把 prompt_tokens、completion_tokens、ttft_ms、tpot_ms 四条曲线叠在一起看异常点几乎一眼就能识别。3. 埋点方案设计从推理引擎到应用层的全链路追踪3.1 推理引擎层拿到最原始的 Token 与耗时数据可观测性的第一手数据必须从推理引擎层拿因为只有这一层才知道真实的 Token 数和各阶段耗时。不同引擎的获取方式不太一样但思路是一致的。如果你用的是vLLM它本身提供了比较完善的 metrics 接口。启动时加上--disable-log-stats之外的相关参数它会通过 Prometheus 格式暴露指标包括vllm:prompt_tokens_total、vllm:generation_tokens_total、vllm:time_to_first_token_seconds、vllm:time_per_output_token_seconds等。这些是引擎级别的聚合指标适合做全局监控但拿不到单请求的明细。要拿单请求明细需要在调用层做文章。vLLM 的 OpenAI 兼容接口在响应里会返回usage字段包含prompt_tokens、completion_tokens、total_tokens。流式响应的话需要在最后一个 chunk 里取。我通常会在网关层拦截响应把 usage 和自测的耗时一起写进结构化日志。如果你用的是Ollama或LocalAI这类偏本地部署的引擎它们的 API 响应里同样有 usage 信息但字段命名可能略有差异。Ollama 的/api/generate返回里有prompt_eval_count和eval_count分别对应输入和输出 Token 数还有prompt_eval_duration和eval_duration两个纳秒级耗时字段这两个字段非常有用直接告诉了你 prefill 和 decode 各花了多少时间。下面是我在网关层做埋点的一个简化示例用 Python 写的思路是把引擎返回的 usage 和自测耗时合并成一条结构化日志import time import json import logging logger logging.getLogger(llm_observability) def call_llm_with_tracing(client, model, messages, request_id): start time.time() first_token_time None completion_text usage {} stream client.chat.completions.create( modelmodel, messagesmessages, streamTrue, stream_options{include_usage: True}, ) for chunk in stream: if first_token_time is None and chunk.choices: first_token_time time.time() if chunk.choices and chunk.choices[0].delta.content: completion_text chunk.choices[0].delta.content if chunk.usage: usage chunk.usage end time.time() ttft_ms (first_token_time - start) * 1000 if first_token_time else -1 e2e_ms (end - start) * 1000 completion_tokens usage.get(completion_tokens, 0) tpot_ms (e2e_ms - ttft_ms) / completion_tokens if completion_tokens 0 else -1 logger.info(json.dumps({ request_id: request_id, model: model, prompt_tokens: usage.get(prompt_tokens, 0), completion_tokens: completion_tokens, ttft_ms: round(ttft_ms, 2), tpot_ms: round(tpot_ms, 2), e2e_ms: round(e2e_ms, 2), })) return completion_text这段代码的关键点在于stream_options{include_usage: True}不加这个参数的话流式响应里是拿不到 usage 的这是很多人第一次做埋点时容易漏掉的细节。3.2 应用层补充业务语义与用户维度引擎层的数据是技术视角的应用层要补的是业务视角。同一个推理请求来自哪个用户、属于哪个业务线、对应什么功能场景这些信息只有应用层知道。没有这些维度你没法回答哪个业务最耗 Token哪个用户的请求最慢这类问题。我的做法是在请求进入网关时生成一个全局唯一的request_id然后把这个 ID 和业务标签tenant_id、feature_name、user_tier 等一起透传到推理调用最后在日志里合并。这样一条日志就同时包含了技术指标和业务标签后续做聚合分析非常方便。这里有个经验业务标签不要塞太多。我见过有人在日志里打了二十几个标签结果日志体积爆炸查询还慢。一般保留 3 到 5 个最关键的维度就够了比如租户、功能、模型、是否流式。其他的按需再加。3.3 日志格式与采样策略别让观测本身成为负担大模型推理的日志量是很大的尤其是高并发场景。如果每个请求都打全量日志磁盘和写入压力会非常可观。所以采样策略必须提前设计。我的建议是分层采样所有请求都记录基础指标Token 数、延迟、模型名这部分数据量小、价值高详细日志完整的 prompt、completion 内容只对慢请求、错误请求和按比例抽样的正常请求记录。具体来说可以设定几条规则所有请求记录基础指标100% 覆盖。e2e_ms 超过阈值比如 P95的请求记录完整上下文。报错的请求记录完整上下文。正常请求按 1% 到 5% 的比例抽样记录完整上下文。这样既保证了指标数据的完整性又控制了日志体积。实测下来这套策略能把日志量压到全量记录的十分之一左右但排查问题时该有的信息一个都不少。注意完整 prompt 和 completion 里可能包含用户敏感信息记录之前一定要做脱敏或者加密这是合规底线不能省。4. 指标聚合与可视化让数据真正能指导决策4.1 用 Prometheus 做指标聚合的正确姿势原始日志是明细数据要做趋势分析和告警还得靠时序数据库。Prometheus 是这套体系里最常用的选择但大模型场景下的指标设计和传统服务不太一样。传统服务用 Counter 和 Gauge 就够了大模型场景下Histogram才是主角。因为 Token 数和延迟都是高度偏态的分布平均值毫无意义你必须看分位数。比如 prompt_tokens 的 P50 可能是 800P99 可能是 12000这两个数字背后的优化策略完全不同。我一般会定义这几类 Histogramllm_prompt_tokens输入 Token 分布buckets 按 100、500、1000、2000、5000、10000、20000 划分。llm_completion_tokens输出 Token 分布buckets 按 50、100、200、500、1000、2000、4000 划分。llm_ttft_seconds首 Token 延迟分布buckets 按 0.1、0.3、0.5、1、2、5、10 秒划分。llm_tpot_seconds单 Token 生成时间分布buckets 按 0.01、0.02、0.05、0.1、0.2、0.5 秒划分。Bucket 的划分不是随便定的要结合你的实际业务分布来调。如果大部分请求的输入都在 1000 Token 以内那 buckets 就该在 1000 附近加密而不是把精度浪费在 20000 以上。4.2 关键看板设计三张图覆盖 90% 的排查场景看板不用做得多花哨能把关键信息一眼看清就行。我实际用下来三张图基本能覆盖绝大多数排查场景。第一张是Token 消耗趋势图按时间展示 prompt_tokens 和 completion_tokens 的 P50、P95、P99同时叠加请求数曲线。这张图能帮你快速判断是请求变多了还是单请求变重了。第二张是延迟分解图把 TTFT、TPOT、E2E 三条曲线放在一起再叠加 queue_wait。这张图是诊断性能问题的核心前面说的那些判断逻辑都靠它。第三张是成本与效率图展示单位时间内的总 Token 消耗、缓存命中率、GPU 利用率。这张图主要给容量规划和成本优化用。这三张图我建议放在同一个看板的首屏出问题时不用来回切换。下面是一个 Grafana 查询的示例展示怎么算 P95 的 TTFThistogram_quantile( 0.95, sum(rate(llm_ttft_seconds_bucket[5m])) by (le, model_name) )按 model_name 分组很重要多模型场景下不同模型的延迟差异很大混在一起看会互相干扰。4.3 告警规则哪些指标值得半夜叫醒你告警设计的原则是少而准大模型场景下尤其如此。指标太多、阈值太敏感最后就是告警疲劳真出事了反而没人看。我实际配置的告警规则不多主要这几条TTFT P99 超过阈值持续 5 分钟说明排队或 prefill 出了问题用户已经能感知到卡顿。错误率超过 1% 持续 3 分钟推理服务本身可能出问题了。GPU 显存利用率超过 90% 持续 10 分钟有 OOM 风险需要提前扩容或限流。单请求 completion_tokens 超过 8000可能是异常请求或者死循环生成需要人工介入。注意阈值一定要结合自己的业务基线来定别照搬别人的数字。我见过有人直接抄了个 TTFT 500ms 的阈值结果他们业务本身 P99 就是 800ms天天告警最后整个团队都麻木了。5. 从数据到优化几个真实场景的排查链路5.1 场景一延迟突然翻倍但请求量没变这是最典型的一类问题。某天下午开始P99 延迟从 2 秒涨到 5 秒但 QPS 曲线平稳没有任何异常。排查链路是这样的先看延迟分解图发现 TTFT 从 300ms 涨到了 2.5 秒TPOT 基本没变。TTFT 涨说明问题在 prefill 或排队阶段。接着看 queue_wait 指标发现排队时间从 50ms 涨到了 2 秒。到这里基本可以确定是排队问题不是计算问题。那为什么排队会变严重看并发请求数发现没涨。再看 prompt_tokens 的 P95发现从 1500 涨到了 6000。真相大白请求数没变但每个请求的输入变长了prefill 阶段占用 GPU 的时间变长导致后续请求排队。进一步查业务标签定位到是某个业务方改了 prompt 模板塞了一大段上下文进去。解决方案有两个方向一是跟业务方沟通精简 prompt二是给推理引擎加前缀缓存把重复的 system prompt 缓存起来。我们两个都做了TTFT 很快回到了正常水平。这个案例的教训是请求数不是大模型服务的负载指标Token 数才是。监控体系里如果没有 Token 维度这类问题你根本无从下手。5.2 场景二TPOT 抖动用户体验忽好忽坏另一个常见问题是 TPOT 不稳定有时候 20ms 一个 Token有时候 80ms用户感觉输出一顿一顿的。TPOT 抖动通常和 batch 策略有关。推理引擎为了提升吞吐会把多个请求打包成一个 batch 一起计算。batch 里的请求越多单个 Token 的计算时间越长但整体吞吐越高。如果 batch 大小动态变化TPOT 自然就抖。排查方法是看 TPOT 和 batch size 的相关性。如果两者高度相关那就是正常的吞吐-延迟权衡可以通过限制最大 batch size 来稳定 TPOT代价是吞吐下降。如果相关性不强那可能是显存交换swap导致的需要看显存利用率和 swap 次数指标。我们当时的处理是给不同优先级的业务设置了不同的 batch 策略高优先级的交互式请求限制 batch size 保证 TPOT 稳定低优先级的批处理请求放开 batch size 追求吞吐。这个策略落地后交互式业务的 TPOT 抖动从 ±60ms 降到了 ±15ms。5.3 场景三Token 用量对不上账成本核算场景下经常会出现监控统计的 Token 数和账单对不上的情况。这个问题排查起来比较琐碎但原因通常就那么几个。第一个原因是缓存命中没算清楚。如果推理引擎做了前缀缓存实际计费的 Token 数可能小于 usage 里返回的 prompt_tokens。这时候要以引擎的计费口径为准不能直接用 usage 数字。第二个原因是重试请求重复计数。如果网关层做了自动重试一次用户请求可能对应多次推理调用Token 会被重复统计。解决办法是在日志里标记is_retry聚合时去重。第三个原因是流式请求的 usage 丢失。前面提过流式响应必须加include_usage参数才能拿到 usage如果没加这部分请求的 Token 数就是 0统计自然对不上。这三个原因我都踩过尤其是第三个排查了大半天才发现是参数漏了。所以埋点做完之后一定要做一次对账验证拿一批已知 Token 数的请求跑一遍看统计结果是否吻合。6. 落地这套体系时最容易忽略的几个细节6.1 时钟同步分布式追踪的前提如果你的推理服务是多机部署那所有节点的时钟必须同步。否则你在做全链路追踪时会发现时间戳对不上TTFT 算出来是负数这种荒唐事都可能出现。这个坑我在早期项目中踩过。当时网关和推理节点的时间差了 3 秒导致所有跨节点的延迟计算全部错乱。后来统一上了 NTP 同步问题才解决。这件事的教训是可观测性体系的地基是准确的时间这个前提不成立上面所有指标都不可信。6.2 高基数问题别让标签把时序库撑爆Prometheus 这类时序数据库对标签基数cardinality非常敏感。如果你把 request_id、user_id 这种高基数标签直接打进指标时序库很快就会被撑爆查询也会变得极慢。正确的做法是高基数信息放日志低基数信息放指标。request_id、user_id 这些放结构化日志里用日志系统比如 Loki、Elasticsearch查询指标里只保留 model_name、tenant_id、feature_name 这类基数可控的标签。两者通过 request_id 关联需要明细时查日志需要趋势时查指标。6.3 观测本身的性能开销埋点是有成本的。日志写入、指标上报、序列化都会消耗 CPU 和 IO。如果埋点代码写得不好可能给推理服务增加 5% 到 10% 的开销。我的经验是埋点逻辑必须异步化。日志写入用异步队列指标上报用批量提交绝对不能在请求主链路上做同步 IO。另外序列化用轻量格式比如 JSON 的紧凑模式别用那些体积大、解析慢的格式。这些细节看起来不起眼但在高并发下差别很明显。6.4 数据保留策略热数据与冷数据分开大模型推理的日志量很大全量长期保留成本很高。合理的做法是分层保留最近 7 天的明细日志保留在热存储方便快速查询7 天到 90 天的数据降采样后存温存储90 天以上的只保留聚合指标明细直接归档或删除。这个策略要根据你的合规要求和排查需求来定。有些行业要求日志保留半年以上那就得提前规划存储容量别等到磁盘满了才手忙脚乱。7. 我在实际项目里沉淀下来的几条经验做这套可观测性体系的过程中有几个体会是文档里不会写、但实际非常管用的。第一先跑通最小闭环再追求完善。我见过太多团队一上来就想做全链路追踪、做实时大屏、做智能告警结果半年过去了连基础的 Token 统计都没落地。正确的顺序是先能记录单请求的 Token 和延迟再能聚合看趋势最后才是告警和自动化。每一步都能独立产生价值不要贪大求全。第二指标口径一定要和业务方对齐。Token 怎么算、延迟从哪个时间点开始算、缓存命中算不算消耗这些口径如果和业务方理解不一致后面扯皮会没完没了。我们当时的做法是写了一份指标定义文档所有相关方确认签字后面再也没为口径问题吵过架。第三定期做数据质量校验。埋点代码会随着业务迭代慢慢腐化某次重构可能就把某个字段弄丢了。我习惯每周跑一次数据质量检查看关键字段的空值率、异常值比例一旦发现异常就及时修。这个习惯帮我提前发现过好几次埋点失效的问题。第四别忽视冷启动和长尾请求。平均值好看不代表体验好大模型场景下长尾请求特别多。我一般会专门盯 P99.9 的延迟和最大 Token 数这些极端值往往才是用户投诉的来源。这套体系搭起来之后最大的变化不是监控好看了而是排查问题的速度。以前一个性能问题要查半天现在打开看板基本十分钟内能定位到根因。对于大模型这种成本和性能都高度敏感的服务来说这种可观测性带来的确定性比任何单点优化都值钱。
返回列表