大模型上线推理后,运维团队最怕的不是"模型不跑",而是"模型跑着但悄悄变差"。传统监控只盯着 GPU 利用率、请求延迟这些基础设施指标,对 LLM 输出质量——延迟首字时间(TTFT)、吞吐量(Tokens/s)、输出截断率——几乎一无所知。Amazon SageMaker AI 的 Inference Component 架构配合 CloudWatch 与 Managed Grafana,可以把这两层指标缝合进同一张仪表盘,让基础设施指标和模型质量信号同时可见。
两层指标,一个盲区
SageMaker AI 的推理端点使用 Inference Component 来承载模型副本。每个 Component 会向 CloudWatch 推送两类指标:
| 层级 | 代表指标 | 信号含义 |
|---|---|---|
| 基础设施 | GPUUtilization、MemoryUtilization、CPUUtilization |
算力是否跑满、是否需要扩缩副本 |
| 模型质量 | ModelLatency、TTFT、TokensPerSecond、OutputTruncationRate |
用户体感是否下降、模型是否在"糊弄" |
常见做法是只看第一层。GPU 利用率 80% 就觉得"还行",但 TTFT 从 200ms 涨到 1.5s 的信号完全被淹没了。反过来,TTFT 假如稳定但 GPU 长期 30%,说明副本数过多、成本浪费。两层必须同屏。
指标怎么进 CloudWatch
SageMaker AI 端点在创建时,Inference Component 会自动注册一批 CloudWatch 指标。你不需要手动埋点,但需要确认端点配置中启用了数据捕获(Data Capture)和指标上报。
下面是一个完整的端点配置示例,包含 Inference Component 和指标捕获:
# sagemaker-inference-config.yaml
EndpointConfigName: llm-observability-endpoint-config
ProductionVariants:
- VariantName: AllTraffic
ModelName: llm-inference-component
InitialInstanceCount: 2
InstanceType: ml.g5.12xlarge
InitialVariantWeight: 1
# 启用数据捕获——把请求/响应存到 S3,后续可做质量回溯
DataCaptureConfig:
EnableCapture: true
InitialSamplingPercentage: 100 # 生产环境可降到 10-20
DestinationS3Uri: s3://my-llm-observability-bucket/data-capture/
CaptureOptions:
- CaptureMode: Input
- CaptureMode: Output
CaptureContentTypeHeader:
CsvContentType: 0
JsonContentType: 1
用 AWS CLI 创建端点:
# 1. 创建端点配置
aws sagemaker create-endpoint-config \
--cli-input-json file://sagemaker-inference-config.yaml
# 2. 创建端点(替换 <endpoint-config-name> 为上面返回的名字)
aws sagemaker create-endpoint \
--endpoint-name llm-observability-endpoint \
--endpoint-config-name llm-observability-endpoint-config
# 3. 等待端点 InService
aws sagemaker describe-endpoint \
--endpoint-name llm-observability-endpoint \
--query 'EndpointStatus' --output text
端点就绪后,CloudWatch 中会自动出现以下命名空间的指标:
AWS/SageMaker:GPUUtilization、MemoryUtilization、CPUUtilization、ModelLatency、Invocations等- Inference Component 级别指标会以
InferenceComponent维度区分
把 CloudWatch 指标拉进 Grafana
Amazon Managed Grafana 原生支持 CloudWatch 数据源。关键步骤是授权 Grafana 工作空间读取 SageMaker 的 CloudWatch 命名空间。
# 创建 Managed Grafana 工作空间(如果还没有)
aws grafana create-workspace \
--workspace-name llm-observability-grafana \
--account-access-type CURRENT_ACCOUNT \
--authentication-providers AWS_SSO \
--permission-type SERVICE_MANAGED
# 工作空间就绪后,在 AWS IAM 控制台找到 Grafana 自动创建的角色,
# 添加如下内联策略,允许读取 SageMaker 相关的 CloudWatch 指标
IAM 策略片段(附加到 Grafana 工作空间角色):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudwatch:GetMetricData",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"cloudwatch:namespace": [
"AWS/SageMaker"
]
}
}
},
{
"Effect": "Allow",
"Action": [
"logs:GetQueryResults",
"logs:StartQuery"
],
"Resource": "*"
}
]
}
在 Grafana UI 中:Settings → Data Sources → Add CloudWatch,选择命名空间 AWS/SageMaker,即可在面板中查询指标。
仪表盘面板设计要点
原文的核心贡献是一套预置 Grafana Dashboard JSON。这里提炼其设计思路,给出你可以直接在 Grafana 中创建的面板配置。
面板 1:GPU 与内存利用率(面积图)
指标: AWS/SageMaker → GPUUtilization
维度: InferenceComponent = <你的组件名>
统计: Average
周期: 1min
叠加 MemoryUtilization 同维度。面积图比折线图更容易看出"跑满"的时间段。
面板 2:TTFT 与 Tokens/s(双轴折线图)
TTFT 用左轴(ms),Tokens/s 用右轴。两者反向相关——TTFT 涨的时候 Tokens/s 通常跌,同屏能一眼看出质量拐点。
指标 A: AWS/SageMaker → ModelLatency (维度: InferenceComponent)
指标 B: 自定义指标 → TTFT / TokensPerSecond
(如果 SageMaker 未自动上报 TTFT,需要从 Data Capture 日志中提取,
详见下一节)
面板 3:输出截断率(单值 + 阈值告警)
截断率 = OutputTruncationCount / TotalInvocations。超过 5% 就该检查 max_tokens 配置或扩容。
面板 4:副本数与请求并发(堆叠柱状图)
指标: Invocations (Sum, 1min)
叠加: CurrentInstanceCount (从 SageMaker DescribeEndpoint 定期拉取)
这组面板的布局原则:上层放质量指标,下层放资源指标。运维扫一眼就知道"慢了是因为 GPU 满了,还是模型本身退化"。
从 Data Capture 日志提取 TTFT
SageMaker 自动上报的指标中不一定包含 TTFT 和 Tokens/s。如果你的模型框架没有内置上报,可以从 Data Capture 写入 S3 的 JSON 日志中提取。
下面是一个轻量 Python 脚本,读取 S3 上的捕获数据,计算 TTFT 和 Tokens/s,再写回 CloudWatch Custom Metrics:
#!/usr/bin/env python3
"""
从 SageMaker Data Capture S3 日志提取 LLM 质量指标,
推送到 CloudWatch Custom Namespace。
运行前设置环境变量:
S3_BUCKET=my-llm-observability-bucket
ENDPOINT_NAME=llm-observability-endpoint
CW_NAMESPACE=LLM-Quality
"""
import os, json, boto3, time
from datetime import datetime, timezone
s3 = boto3.client("s3")
cw = boto3.client("cloudwatch")
BUCKET = os.environ["S3_BUCKET"]
ENDPOINT = os.environ["ENDPOINT_NAME"]
NAMESPACE = os.environ.get("CW_NAMESPACE", "LLM-Quality")
def parse_capture_file(key: str):
"""解析单条 Data Capture JSON,返回 TTFT(ms) 和 Tokens/s"""
obj = s3.get_object(Bucket=BUCKET, Key=key)
data = json.loads(obj["Body"].read())
# Data Capture 结构:captureData 包含 input / output
# 假设 output 中有模型返回的时间戳和 token 数
# 实际字段取决于你的模型容器格式,需按实际情况调整
output_payload = json.loads(data["captureData"]["output"]["data"])
ttft_ms = output_payload.get("time_to_first_token_ms", None)
total_tokens = output_payload.get("completion_tokens", 0)
generation_time_s = output_payload.get("generation_time_s", None)
tokens_per_sec = None
if generation_time_s and generation_time_s > 0 and total_tokens:
tokens_per_sec = total_tokens / generation_time_s
is_truncated = output_payload.get("finish_reason") == "length"
return {
"ttft_ms": ttft_ms,
"tokens_per_sec": tokens_per_sec,
"is_truncated": is_truncated,
"timestamp": data["eventMetadata"]["inferenceTimestamp"],
}
def push_metrics(records: list[dict]):
"""批量推送到 CloudWatch"""
metric_data = []
for r in records:
ts = datetime.fromisoformat(r["timestamp"].replace("Z", "+00:00"))
if r["ttft_ms"] is not None:
metric_data.append({
"MetricName": "TTFT",
"Value": r["ttft_ms"],
"Unit": "Milliseconds",
"Timestamp": ts,
"Dimensions": [{"Name": "EndpointName", "Value": ENDPOINT}],
})
if r["tokens_per_sec"] is not None:
metric_data.append({
"MetricName": "TokensPerSecond",
"Value": r["tokens_per_sec"],
"Unit": "Count/Second",
"Timestamp": ts,
"Dimensions": [{"Name": "EndpointName", "Value": ENDPOINT}],
})
metric_data.append({
"MetricName": "OutputTruncated",
"Value": 1 if r["is_truncated"] else 0,
"Unit": "Count",
"Timestamp": ts,
"Dimensions": [{"Name": "EndpointName", "Value": ENDPOINT}],
})
# CloudWatch PutMetricData 单次最多 20 条
for batch in [metric_data[i:i+20] for i in range(0, len(metric_data), 20)]:
cw.put_metric_data(Namespace=NAMESPACE, MetricData=batch)
# --- 主流程:扫描最近 5 分钟的捕获文件 ---
prefix = f"data-capture/{ENDPOINT}/AllTraffic/"
now = time.time()
files = s3.list_objects_v2(Bucket=BUCKET, Prefix=prefix)
if "Contents" in files:
recent = [
o["Key"] for o in files["Contents"]
if now - o["LastModified"].timestamp() < 300 # 5 分钟内
]
records = [parse_capture_file(k) for k in recent]
push_metrics(records)
print(f"Pushed {len(records)} records to CW namespace {NAMESPACE}")
else:
print("No capture files found in the time window.")
部署建议:把这段脚本打包成 Lambda,用 EventBridge 每 1-5 分钟触发一次。这样 TTFT 和 Tokens/s 就会持续写入 LLM-Quality 命名空间,Grafana 面板可以实时刷新。
告警:让指标自己喊疼
光看仪表盘不够,关键指标必须配告警。以下是两个最值得设置的 CloudWatch Alarm:
# 告警 1:TTFT 超过 2 秒
aws cloudwatch put-metric-alarm \
--alarm-name llm-ttft-high \
--namespace LLM-Quality \
--metric-name TTFT \
--dimensions Name=EndpointName,Value=llm-observability-endpoint \
--statistic Average \
--period 60 \
--evaluation-periods 3 \
--threshold 2000 \
--comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:llm-alerts
# 告警 2:GPU 利用率持续低于 40%(副本浪费)
aws cloudwatch put-metric-alarm \
--alarm-name llm-gpu-underutilized \
--namespace AWS/SageMaker \
--metric-name GPUUtilization \
--dimensions Name=InferenceComponent,Value=<你的组件名> \
--statistic Average \
--period 300 \
--evaluation-periods 3 \
--threshold 40 \
--comparison-operator LessThanThreshold \
--alarm-actions arn:aws:sns:us-east-1:123456789012:llm-alerts
第一个告警抓"用户体感变差",第二个抓"钱花多了"。两者组合,基本覆盖了 LLM 推理运维的核心风险。
上线 Checklist
把这套可观测体系落地前,逐项确认:
- 端点配置:Data Capture 已启用,采样比例符合成本容忍度(生产建议 10-20%,调试阶段 100%)。
- CloudWatch 指标:确认
AWS/SageMaker命名空间下能看到 GPUUtilization 和 ModelLatency;如果缺少 TTFT/Tokens/s,部署 Lambda 补采。 - Grafana 数据源:CloudWatch 数据源已添加,IAM 角色有
cloudwatch:GetMetricData权限且命名空间限定为AWS/SageMaker和LLM-Quality。 - 面板布局:质量指标在上、资源指标在下;TTFT 和 Tokens/s 共用双轴图;截断率有独立面板和阈值线。
- 告警:TTFT > 2s 和 GPU < 40% 两个 Alarm 已创建,SNS 通知通道已验证。
- 成本意识:Data Capture 写 S3 会产生存储费用,Custom Metrics 按 API 调用计费;采样比例和推送频率需要和预算对齐。
这套方案的价值不在"多看几个数字",而在让基础设施指标和模型质量信号在同一时间轴上对齐——GPU 满载时 TTFT 是否同步飙升、扩副本后 Tokens/s 是否真的回升,这些因果关系只有同屏才能快速定位。