第12章 推理系统可观测性

导入:你看不见的东西,就无法优化

“推理服务上线了,线上一切正常。”——这句话有多大的可信度?

如果你没有监控 TTFT,你怎么知道用户等了多久才看到第一个字?如果你没有追踪 token 吞吐,你怎么知道 GPU 是满载还是空转?如果你没有统计单位 token 成本,你怎么知道这个月的推理账单是否合理?

推理系统的可观测性不是为了”好看”——它是为了回答三个问题:用户满意吗?系统健康吗?成本可控吗?

本章覆盖推理系统可观测性的核心维度:性能指标、全链路追踪、质量监控和成本归因。


12.1 关键指标

推理系统有一组区别于传统 Web 服务的特有指标。不理解这些指标,就无法判断系统是否健康。

核心指标定义

graph LR
    subgraph "一次推理请求的时间线"
        A[t=0\n请求发出] --> B[t₁\n到达服务器]
        B --> C[t₂\n开始前向计算\n首token生成]
        C --> D[t₃\n最后一个token]
        D --> E[t₄\n客户端收到完整响应]
    end
    
    B -.->|网络延迟| A
    C -.->|"TTFT (Time To First Token)"| A
    D -.->|"E2E Latency"| A
    D -.->|"TPOT × output_tokens"| C

指标 全称 含义 目标值
TTFT Time To First Token 从请求到首 token 的时间 < 500ms
TPOT Time Per Output Token 生成每个 output token 的平均时间 < 50ms
E2E Latency End-to-End Latency 从请求到完整响应的总时间 取决于输出长度
Throughput Tokens/sec 每秒生成的 token 数(系统级) 越高越好
Goodput Tokens/sec(满足 SLA 的) 满足延迟 SLA 的有效吞吐 接近 Throughput

TTFT:用户感知最强的指标

TTFT 决定了用户是否觉得”快”。它包括: - 网络延迟 - 请求排队等待时间 - Prefill 时间(处理输入 prompt 的时间)

TTFT 与输入长度正相关——输入越长,prefill 计算越久,首 token 就越慢。

# vLLM 已经内置了 Prometheus 指标导出
# 启动时添加 --disable-log-stats 关闭控制台统计,用 Prometheus 替代

# 主要指标(vLLM 自动导出到 /metrics):
# vllm:time_to_first_token_seconds (Histogram)
# vllm:time_per_output_token_seconds (Histogram)  
# vllm:request_latency_seconds (Histogram)
# vllm:num_requests_running (Gauge)
# vllm:num_requests_waiting (Gauge)
# vllm:gpu_cache_usage_perc (Gauge)
# vllm:gpu_prefix_cache_queries_total (Counter)
# vllm:gpu_prefix_cache_hits_total (Counter)

TPOT:生成速度

TPOT 是用户感受到的”打字速度”。一个 token 大约 0.6 个英文单词,所以 50ms/token ≈ 12 words/s。人类阅读速度约 200-300 wpm ≈ 3-5 words/s,所以只要 TPOT 在 50ms 以内,用户会觉得”流畅”。

Goodput:真正重要的指标

Throughput 看总量,Goodput 只算满足 SLA 的请求。一个系统可能 throughput 很高(大量 batch 同时跑),但如果 P99 延迟超标,Goodput 可能很低。

# Goodput 计算
# 定义:在 SLA(如 P99 < 2s)内完成的请求数 / 总时间

from prometheus_client import Counter, Histogram

# 自定义 Goodput 指标
sla_compliant_requests = Counter(
    'inference_sla_compliant_requests_total',
    'Requests completed within SLA',
    ['model', 'slo']
)

sla_violations = Counter(
    'inference_sla_violations_total', 
    'Requests that violated SLA',
    ['model', 'reason']  # reason: timeout, error, high_latency
)

# 在请求处理中
async def handle_request(request):
    start_time = time.time()
    try:
        result = await model.generate(request)
        elapsed = time.time() - start_time
        
        if elapsed < request.sla_threshold:
            sla_compliant_requests.labels(
                model=request.model, 
                slo=f"{request.sla_threshold}s"
            ).inc()
        else:
            sla_violations.labels(
                model=request.model, 
                reason="high_latency"
            ).inc()
        
        return result
    except Exception:
        sla_violations.labels(model=request.model, reason="error").inc()
        raise
Tip

Goodput 是推理服务的北极星指标。Throughput 高但 Goodput 低意味着系统在”做无用功”——忙于计算但用户不满意。设定清晰的 SLA(如 TTFT P99 < 500ms,E2E P99 < 5s),然后用 Goodput 来衡量达标率。

Grafana 面板配置

# 推荐的 Grafana Dashboard 面板布局
# 基于 vLLM + Prometheus 的指标

panels:
  # Row 1: 实时延迟
  - title: "TTFT (P50/P90/P99)"
    query: |
      histogram_quantile(0.50, rate(vllm:time_to_first_token_seconds_bucket[5m]))
      histogram_quantile(0.90, rate(vllm:time_to_first_token_seconds_bucket[5m]))
      histogram_quantile(0.99, rate(vllm:time_to_first_token_seconds_bucket[5m]))
  
  - title: "TPOT (P50/P90/P99)"
    query: |
      histogram_quantile(0.50, rate(vllm:time_per_output_token_seconds_bucket[5m]))
      histogram_quantile(0.99, rate(vllm:time_per_output_token_seconds_bucket[5m]))
  
  # Row 2: 吞吐
  - title: "Output Throughput (tokens/s)"
    query: |
      sum(rate(vllm:request_success_total{additional_metrics=["output_tokens"]}[5m]))
  
  # Row 3: 队列状态
  - title: "Running / Waiting Requests"
    query: |
      vllm:num_requests_running
      vllm:num_requests_waiting
  
  # Row 4: GPU 内存
  - title: "KV Cache Usage %"
    query: |
      vllm:gpu_cache_usage_perc

12.2 全链路性能 Profiling

当延迟指标异常时,你需要知道慢在哪里。全链路 Profiling 能把一个推理请求的时间拆解到每个阶段。

请求时间拆解

gantt
    title 一次推理请求的时间构成
    dateFormat X
    axisFormat %s
    section 网络
    客户端到网关 :a, 0, 15
    section 网关
    鉴权 + 路由 :b, 15, 20
    section 排队
    等待 GPU 槽位 :crit, c, 20, 80
    section Prefill
    处理输入 prompt :d, 80, 200
    section Decode
    生成 tokens :e, 200, 500
    section 网络
    返回客户端 :f, 500, 510

上图中,排队等待(60ms)和 Prefill(120ms)往往是最大的优化空间。

使用 OpenTelemetry 追踪

# 推理服务的分布式追踪
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

tracer = trace.get_tracer(__name__)

app = FastAPI()
FastAPIInstrumentor.instrument_app(app)

@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
    body = await request.json()
    
    with tracer.start_as_current_span("inference_request") as span:
        span.set_attribute("model.name", body.get("model"))
        span.set_attribute("input.tokens", count_tokens(body.get("messages")))
        
        # 细分各阶段
        with tracer.start_as_current_span("queue_wait"):
            slot = await acquire_gpu_slot()
        
        with tracer.start_as_current_span("prefill"):
            pass  # vLLM 内部已处理
        
        with tracer.start_as_current_span("generate"):
            result = await model.generate(body)
        
        with tracer.start_as_current_span("response_serialize"):
            response = format_response(result)
        
        span.set_attribute("output.tokens", result.usage.completion_tokens)
        return response

GPU 级别 Profiling

有时瓶颈不在推理框架,而在 GPU 本身。NVIDIA Nsight Systems 可以做 kernel 级别的性能分析。

# 使用 Nsight Systems 分析推理过程
nsys profile \
    --trace=cuda,nvtx \
    --output=inference_profile \
    python inference_benchmark.py

# 生成报告
nsys stats inference_profile.nsys-rep
nsys-ui inference_profile.nsys-rep   # GUI 查看
Tip

常见瓶颈定位: 1. TTFT 高 → 检查 prefill 时间和队列深度 2. TPOT 高 → 检查 GPU 利用率和 batch 大小 3. 偶发延迟飙升 → 检查是否有 GC/内存整理/框架内部同步 4. 吞吐低于预期 → 检查 KV Cache 利用率和 batch 填充率


12.3 异常请求与模型幻觉监控

推理服务的”健康”不只是性能指标——输出质量也是可观测性的重要维度。

需要监控的异常输出

异常类型 表现 检测方法
幻觉 编造不存在的事实 与知识库比对 / LLM-as-Judge
重复 同一句话反复出现 n-gram 重复率
截断 输出在中间停止 检查 finish_reason
拒答 无正当理由拒绝回答 关键词 + 分类器
有害内容 歧视/暴力/违法 安全分类器
格式错误 JSON 解析失败 结构化校验
# 输出质量监控(简化实现)
import re
from collections import Counter

class OutputQualityMonitor:
    def __init__(self):
        self.stats = Counter()
    
    def check(self, output: str, finish_reason: str) -> dict:
        issues = []
        
        # 1. 检查是否被截断
        if finish_reason == "length":
            issues.append("truncated")
            self.stats["truncated"] += 1
        
        # 2. 检查重复(n-gram)
        words = output.split()
        if len(words) > 20:
            bigrams = [tuple(words[i:i+2]) for i in range(len(words)-1)]
            repeat_rate = sum(1 for b, c in Counter(bigrams).items() if c > 3) / len(bigrams)
            if repeat_rate > 0.1:    # 超过 10% 的 bigram 重复 3 次以上
                issues.append("repetition")
                self.stats["repetition"] += 1
        
        # 3. 检查 JSON 格式(如果期望 JSON 输出)
        if output.strip().startswith("{"):
            try:
                import json
                json.loads(output)
            except json.JSONDecodeError:
                issues.append("malformed_json")
                self.stats["malformed_json"] += 1
        
        # 4. 检查空输出
        if len(output.strip()) < 5:
            issues.append("empty_output")
            self.stats["empty_output"] += 1
        
        return {"issues": issues, "output_length": len(output)}

# 使用
monitor = OutputQualityMonitor()
result = monitor.check(generated_text, finish_reason="stop")
if result["issues"]:
    send_alert(f"Output quality issue: {result['issues']}")

幻觉检测:LLM-as-Judge

对于关键场景(如医疗、法律),可以用另一个 LLM 来评估输出的事实准确性:

# LLM-as-Judge 幻觉检测(成本较高,适合采样执行)
import random

JUDGE_PROMPT = """你是一个事实核查员。判断以下回答中是否存在事实错误。

问题:{question}
回答:{answer}
参考知识:{retrieved_context}

判断标准:
- 如果回答中的信息可以在参考知识中找到支持,则"正确"
- 如果回答包含参考知识中没有的具体声明,则"幻觉"
- 如果回答含糊或不够具体,则"模糊"

输出 JSON:{{"verdict": "correct|hallucination|vague", "issues": ["具体问题描述"]}}
"""

async def check_hallucination(question, answer, context, judge_model):
    # 只对 5% 的请求做幻觉检测(成本控制)
    if random.random() > 0.05:
        return None
    
    prompt = JUDGE_PROMPT.format(
        question=question, answer=answer, retrieved_context=context
    )
    
    result = await judge_model.generate(prompt, temperature=0.0)
    return parse_judge_result(result)
Warning

幻觉检测的成本:LLM-as-Judge 每次检测本身就是一次推理调用,成本不可忽略。建议只对采样数据(如 1-5% 的请求)执行,并使用较小的模型(如 8B)作为 judge,而不是用 70B+ 模型。


12.4 成本归因与单位 token 成本

推理是要花钱的。知道钱花在哪里,是成本优化的前提。

成本模型

# 推理服务的单位成本计算

class InferenceCostModel:
    """计算每个请求、每个 token 的实际成本"""
    
    def __init__(self, gpu_hourly_cost: float, num_gpus: int):
        self.gpu_hourly_cost = gpu_hourly_cost    # 每张 GPU 每小时成本
        self.num_gpus = num_gpus                  # 推理使用的 GPU 数量
        
        # 总硬件成本/小时
        self.hourly_cost = gpu_hourly_cost * num_gpus
    
    def cost_per_token(self, total_tokens: int, hours: float) -> float:
        """计算单位 token 成本"""
        total_cost = self.hourly_cost * hours
        return total_cost / total_tokens if total_tokens > 0 else float('inf')
    
    def cost_per_request(self, avg_input_tokens: int, avg_output_tokens: int, 
                         requests_per_hour: float) -> float:
        """计算每个请求的平均成本"""
        total_tokens_per_hour = (avg_input_tokens + avg_output_tokens) * requests_per_hour
        cost_per_token = self.hourly_cost / total_tokens_per_hour
        cost_per_request = cost_per_token * (avg_input_tokens + avg_output_tokens)
        return cost_per_request

# 示例:Llama-3-70B 推理成本
# 硬件:4 x A100 80GB,每张 $2.5/h(按需价格)
# 吞吐:约 5000 output tokens/s(经过优化后)
cost_model = InferenceCostModel(gpu_hourly_cost=2.5, num_gpus=4)

hourly_cost = cost_model.hourly_cost    # $10/h
tokens_per_hour = 5000 * 3600          # 18,000,000 tokens/h
cost_per_million_tokens = cost_model.hourly_cost / tokens_per_hour * 1e6  # $0.56/M tokens

print(f"每小时硬件成本: ${hourly_cost}")
print(f"每百万 token 成本: ${cost_per_million_tokens:.2f}")
print(f"作为对比,GPT-4o API: ~$5/M tokens (input)")

按团队/用户归因

# 基于请求标签的成本归因
from collections import defaultdict
from prometheus_client import Counter as PromCounter

# 带标签的 token 计数器
token_counter = PromCounter(
    'inference_tokens_total',
    'Total tokens processed',
    ['team', 'model', 'direction']  # direction: input/output
)

# 在请求处理中记录
async def handle_request(request):
    team = request.headers.get("X-Team", "unknown")
    model = request.model
    input_tokens = count_tokens(request.messages)
    
    token_counter.labels(team=team, model=model, direction="input").inc(input_tokens)
    
    result = await model.generate(request)
    output_tokens = result.usage.completion_tokens
    
    token_counter.labels(team=team, model=model, direction="output").inc(output_tokens)
    
    return result

# 每天生成本报告
# PromQL 查询:
# sum by (team) (rate(inference_tokens_total[24h]))
# → 每个团队的 token 使用量
-- 用 Grafana / Superset 展示成本看板
-- 每日成本明细

WITH daily_usage AS (
    SELECT 
        date(timestamp) as date,
        team,
        model,
        SUM(CASE WHEN direction = 'input' THEN tokens ELSE 0 END) as input_tokens,
        SUM(CASE WHEN direction = 'output' THEN tokens ELSE 0 END) as output_tokens,
        COUNT(DISTINCT request_id) as request_count
    FROM inference_logs
    WHERE date(timestamp) = CURRENT_DATE - 1
    GROUP BY 1, 2, 3
)
SELECT 
    date,
    team,
    model,
    input_tokens,
    output_tokens,
    request_count,
    (input_tokens + output_tokens) * 0.00000056 as cost_usd  -- 假设 $0.56/M tokens
FROM daily_usage
ORDER BY cost_usd DESC;
Tip

成本优化的杠杆: 1. 量化:INT8/FP8 直接减半计算成本(~50% 节省) 2. Prefix Caching:减少 prefill 计算(~20-30% 节省,取决于共享前缀比例) 3. 合理选模型:8B 能做好的事不要用 70B(~10x 成本差异) 4. 自动扩缩容:低谷时缩容到最小副本(~30-50% 节省,取决于流量波动) 5. Spot Instance:非实时场景使用竞价实例(~60-70% 节省)


小结

推理可观测性的四层模型:

层次 回答什么问题 核心指标
性能 用户满意吗? TTFT, TPOT, E2E, Goodput
系统 系统健康吗? GPU 利用率, KV Cache 使用率, 队列深度
质量 输出正确吗? 幻觉率, 重复率, 截断率, 格式错误率
成本 花了多少? 每 M token 成本, 每团队日成本, GPU 利用效率

可观测性不是”装个监控就完了”。它的价值在于形成闭环:测量 → 分析 → 优化 → 再测量。当你能清楚地看到 TTFT 80% 的时间花在 prefill 上,你就知道该启用 Prefix Caching;当你看到某个团队的 token 使用量占了 50% 但产出价值很低,你就知道该调整配额。

推理服务上线只是开始。持续的可观测和优化,才是 AI Infra 工程师的日常。


延伸阅读

  • vLLM Prometheus Metrics 文档:https://docs.vllm.ai/en/latest/serving/metrics.html
  • “Goodput: A Metric for Evaluating LLM Serving Performance” — Zhang et al., 2024
  • OpenTelemetry 推理追踪指南:https://opentelemetry.io/docs/demo/
  • NVIDIA Nsight Systems:https://docs.nvidia.com/nsight-systems/
  • “Prompt-Based Hallucination Detection in LLMs” — Liu et al., 2024
  • Chip Huyen, AI Engineering Chapter 11 — “Inference Optimization”