当前位置: 首页 > news >正文

LLM 推理延迟监控:从 Token 级指标到全链路可观测性方案

LLM 推理延迟监控:从 Token 级指标到全链路可观测性方案

一、大模型推理的延迟盲区:为什么传统 APM 不够用

大模型推理的延迟分布与传统 HTTP 请求有本质区别。一个 Chat Completion 请求的端到端延迟包含多个阶段:请求排队等待、Prompt 编码(Prefill)、Token 逐个生成(Decode)、网络传输。其中 Decode 阶段的耗时与输出 Token 数成正比,可能占总延迟的 80% 以上。传统 APM 只能看到请求的总耗时,无法区分各阶段耗时,更无法回答"延迟升高是因为排队还是因为 Decode 变慢"。

更棘手的是流式响应场景。SSE 推送模式下,首 Token 延迟(Time To First Token, TTFT)和 Token 间延迟(Inter-Token Latency, ITL)是用户体验的核心指标。TTFT 决定用户等待多久看到第一个字,ITL 决定"打字"速度是否流畅。传统 APM 的请求级指标无法捕获这些 Token 级别的延迟特征。

二、LLM 推理延迟指标体系:从请求级到 Token 级

完整的 LLM 推理监控需要三层指标:请求级指标(端到端延迟、成功率)、Token 级指标(TTFT、ITL、吞吐量)和资源级指标(GPU 利用率、显存占用、KV Cache 命中率)。三层指标之间有因果关系——资源级指标异常会导致 Token 级指标劣化,Token 级指标劣化最终反映在请求级指标上。

flowchart TB subgraph 请求级指标 A1[端到端延迟 P50/P99] A2[请求成功率] A3[并发连接数] end subgraph Token级指标 B1[TTFT 首 Token 延迟] B2[ITL Token 间延迟] B3[Token 吞吐量 tokens/s] end subgraph 资源级指标 C1[GPU 利用率] C2[显存占用率] C3[KV Cache 命中率] end C1 --> B2 C2 --> B1 C3 --> B1 B1 --> A1 B2 --> A1 B3 --> A2

TTFT 升高通常指向 Prefill 阶段的瓶颈——Prompt 过长或 GPU 资源竞争。ITL 升高则指向 Decode 阶段的问题——Batch Size 过大导致单步推理变慢,或 KV Cache 溢出导致频繁换页。资源级指标帮助定位根因,Token 级指标量化用户感知,请求级指标评估整体健康度。

三、生产级代码实现:Token 级指标采集与告警

3.1 流式响应的 Token 级指标采集

@Service public class LlmMetricsCollector { private final MeterRegistry meterRegistry; private final Timer ttftTimer; private final DistributionSummary itlSummary; private final Counter totalTokensCounter; public LlmMetricsCollector(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; // TTFT 使用 Timer 记录分布,关注 P99 this.ttftTimer = Timer.builder("llm.ttft") .description("首 Token 延迟") .publishPercentiles(0.5, 0.9, 0.95, 0.99) .publishPercentileHistogram() .register(meterRegistry); // ITL 使用 DistributionSummary,因为单次延迟极短 this.itlSummary = DistributionSummary.builder("llm.itl") .description("Token 间延迟(ms)") .publishPercentiles(0.5, 0.9, 0.99) .register(meterRegistry); this.totalTokensCounter = Counter.builder("llm.tokens.total") .description("Token 总消耗量") .register(meterRegistry); } public Flux<ServerSentEvent<ChatChunk>> streamWithMetrics( ChatRequest request, Flux<ServerSentEvent<ChatChunk>> stream) { long requestStart = System.nanoTime(); AtomicLong lastTokenTime = new AtomicLong(0); AtomicBoolean firstTokenReceived = new AtomicBoolean(false); return stream.doOnNext(event -> { long now = System.nanoTime(); if (!firstTokenReceived.getAndSet(true)) { // 记录 TTFT long ttftMs = (now - requestStart) / 1_000_000; ttftTimer.record(ttftMs, TimeUnit.MILLISECONDS); } else if (lastTokenTime.get() > 0) { // 记录 ITL long itlMs = (now - lastTokenTime.get()) / 1_000_000; itlSummary.record(itlMs); } lastTokenTime.set(now); // 统计 Token 消耗 if (event.data() != null && event.data().getTokens() != null) { totalTokensCounter.increment( event.data().getTokens()); } }); } }

3.2 GPU 资源指标采集(NVIDIA DCGM)

@Component public class GpuMetricsCollector { private final MeterRegistry meterRegistry; @Scheduled(fixedRate = 5000) public void collectGpuMetrics() { try { // 通过 nvidia-smi 采集 GPU 指标 // 为什么用 nvidia-smi 而非 DCGM Exporter: // 单机场景下 nvidia-smi 更简单,无需额外部署; // 集群场景建议用 DCGM Exporter + Prometheus ProcessBuilder pb = new ProcessBuilder( "nvidia-smi", "--query-gpu=utilization.gpu,memory.used,memory.total", "--format=csv,noheader,nounits"); Process process = pb.start(); String output = new String( process.getInputStream().readAllBytes()); String[] parts = output.trim().split(","); if (parts.length >= 3) { double gpuUtil = Double.parseDouble(parts[0].trim()); double memUsed = Double.parseDouble(parts[1].trim()); double memTotal = Double.parseDouble(parts[2].trim()); meterRegistry.gauge("llm.gpu.utilization", gpuUtil); meterRegistry.gauge("llm.gpu.memory.used", memUsed); meterRegistry.gauge("llm.gpu.memory.total", memTotal); meterRegistry.gauge("llm.gpu.memory.ratio", memUsed / memTotal * 100); } } catch (Exception e) { log.warn("GPU 指标采集失败: {}", e.getMessage()); } } }

3.3 延迟异常告警规则

@Configuration public class LlmAlertConfig { @Bean public MeterFilter llmAlertFilter() { return MeterFilter.accept(); } // TTFT P99 超过 5 秒告警 // 为什么用 P99 而非均值:均值会被大量正常请求稀释, // P99 能捕捉到尾部延迟异常,更符合用户体验的真实感知 @Bean public ttftAlertRule(MeterRegistry registry) { return AlertRule.builder() .name("llm-ttft-p99-high") .metric("llm.ttft") .statistic(Statistic.Percentile) .percentile(0.99) .threshold(5000.0) // 5 秒 .duration(Duration.ofMinutes(3)) .action(() -> log.error( "TTFT P99 超过 5 秒,可能存在 Prefill 瓶颈")) .build(); } }

3.4 全链路 Trace 集成

@Service public class TracedLlmService { private final LlmClient llmClient; private final Tracer tracer; public Flux<ChatChunk> chatWithTrace(ChatRequest request) { Span span = tracer.nextSpan() .name("llm.inference") .tag("model", request.getModel()) .tag("prompt.tokens", String.valueOf( estimateTokens(request.getMessage()))) .start(); return llmClient.streamChat(request) .doOnNext(chunk -> { // 在 Span 上记录关键事件 if (chunk.isFirstToken()) { span.event("first_token_received"); } }) .doOnError(span::error) .doFinally(signal -> { span.tag("stream.completed", signal == SignalType.ON_COMPLETE ? "true" : "false"); span.end(); }); } }

四、LLM 可观测性的架构权衡:采样率、指标基数与存储成本

Token 级指标的基数爆炸:每个请求的每个 Token 都产生一条 ITL 记录,高并发下指标量级远超传统 HTTP 请求。Prometheus 的指标存储按基数(Label 组合数)线性增长,如果 ITL 按 model + endpoint + status_code 分组,基数可能达到数万。建议 ITL 只按 model 分组,详细维度通过 Trace 追溯。

采样策略的精度损失:高并发场景下不可能对每个请求都记录完整的 Token 级指标。采样率设为 1% 时,P99 指标的置信区间会变宽,可能漏掉真实的尾部延迟。建议对 TTFT 全量采集(每个请求只记录一次),对 ITL 采样采集(每个请求只记录部分 Token 间延迟)。

Trace 的存储成本:流式请求的 Span 持续时间可能长达数十秒,远超普通 HTTP 请求。如果每个 Token 都产生一个 Span Event,单个请求的 Trace 数据量可能达到数百 KB。建议只在异常请求(TTFT > 阈值或 ITL > 阈值)上记录详细 Trace,正常请求只记录摘要。

GPU 指标的采集频率:nvidia-smi 的调用本身有约 100ms 的开销,5 秒采集一次对性能影响可忽略。但如果部署了多个推理实例,每个实例都独立采集会导致指标重复。建议在集群级别用 DCGM Exporter 统一采集,避免重复。

五、总结

LLM 推理延迟监控的核心是建立 Token 级指标体系,TTFT 和 ITL 是比端到端延迟更有价值的观测维度。TTFT 升高指向 Prefill 瓶颈,ITL 升高指向 Decode 瓶颈,GPU 资源指标帮助定位根因。落地时建议先实现 TTFT 和 ITL 两个核心指标的采集和告警,再逐步补充 GPU 资源指标和全链路 Trace。指标基数和存储成本是需要持续优化的维度,采样策略应根据业务规模动态调整。

http://www.jsqmd.com/news/1018165/

相关文章:

  • 中文NLP实战入门:从文本清洗到LightGBM分类的落地路径
  • 告别米家App!在HomeAssistant里原生显示小米温湿度计2代,我是这么做的
  • 告别Dev C++!VSCode配置C/C++环境保姆级教程(含MinGW-w64安装与两种调试方案)
  • 可视耳勺靠谱吗?西圣、蜂鸟最建议买哪一款?必备好物测评
  • 告别PDF乱码!Win10下CTeX 2.9.2与WinEdt 11完整配置与关联指南
  • 如何快速掌握猫抓浏览器扩展:新手完整实战指南
  • ZID应用配置文件深度解析:无线HID设备开发核心API与异步消息机制实践
  • 从脑机接口到情感计算:5个前沿HCI技术案例,看未来交互如何重塑我们的生活
  • Rust 1.75.0 新特性尝鲜前,你的 rustup 和 cargo 工具链管理真的做对了吗?
  • 深入解析MPC866 PowerQUICC:嵌入式通信处理器的架构、编程与实战
  • MonkeyCode vs Cursor vs Copilot:三大AI编程工具横评,谁更适合你?
  • 终极指南:如何让《模拟人生1》在现代显示器上完美运行 - 宽屏补丁完整教程
  • 终极指南:如何用GLTR快速检测AI生成文本
  • ESP32-S3 AI相机硬件组成与通信配置说明
  • 成都钻石回收价格测算 行情解读 + 门店对比避坑 - 开心测评
  • 涂料企业的下一个竞争力:用PLM把“配色“从手艺变成科学
  • 深入解析MCU时钟系统:从架构原理到低功耗调试实战
  • SSH连接卡顿变慢终极解决教程:吃透DNS反向解析与GSSAPI核心问题
  • 嵌入式系统单元测试实战:基于NXP i.MX平台的硬件驱动验证与故障排查
  • 2026年热转印膜厂家推荐排行榜,烫画热转印膜/刻字膜/数码喷墨热转印膜品牌推荐! - 品牌发掘
  • 重组CRM197载体蛋白详解:结合疫苗开发中的安全性、免疫增强机制与应用优势
  • 介绍生物素标记的各种氨基酸:生物素-甘氨酸Biotin-Glycin/生物素-L-缬氨酸Biotinoyl-L-Val/生物素-半胱氨酸Bio-L-Cys/生物素-组氨酸Bio-L-His
  • WinEdt 11不是唯一选择?聊聊Win10上CTeX 2.9.2的几种编辑器搭档(VSCode/TeXworks对比)
  • 如何用Kimi-Free-API快速构建智能对话系统:完整实践指南
  • 098、Prompt Caching 优化实战:在 API 调用中利用缓存降低延迟和成本的方案
  • 手把手教你用树莓派+HA抓取小米温湿度计2代数据(附密钥获取避坑指南)
  • 2026晋中装修设计落地能力排行榜——360㎡实景展厅保障“所见即所得” - 装企自媒体训练营辉哥
  • GPT-4稀疏化真相:MoE架构下的参数激活与工程落地瓶颈
  • 保姆级教程:用VSCode+MinGW搭建C语言环境,刷透西工大NOJ这82道题
  • 高效清理Windows 11系统垃圾:Win11Debloat一键优化工具完全指南