为生成式 AI 端点选择实例数量,不能只看单次请求的延迟。真正决定容量的是:随着并发持续升高,吞吐量能否同步增长,P95/P99 延迟何时恶化,以及端点从哪个负载点开始出现排队、限流或错误。
Amazon SageMaker AI 的并发扫描方法,会在逐级增加负载的同时记录性能结果。结合 CreateAIBenchmarkJob API,可以把部署、压测和结果分析组织成可重复的容量评估流程,避免依靠一次手工测试或主观经验决定 fleet size。
并发扫描真正要找的是“拐点”
假设一个端点在不同并发下得到以下结果:
| 并发数 | 吞吐量(请求/秒) | P95 延迟 | 错误率 |
|---|---|---|---|
| 1 | 0.8 | 1.3 秒 | 0% |
| 2 | 1.5 | 1.5 秒 | 0% |
| 4 | 2.8 | 1.9 秒 | 0% |
| 8 | 4.1 | 3.8 秒 | 0% |
| 16 | 4.3 | 8.6 秒 | 2.5% |
并发从 1 增加到 8 时,吞吐量仍在增长;从 8 增加到 16 后,吞吐量几乎没有变化,但延迟和错误率快速上升。这里的并发 8 附近就是容量曲线的拐点,也可以理解为当前端点配置的饱和区域。
生产环境通常不应该运行在拐点上。更稳妥的做法是选择拐点之前、能够满足延迟目标的负载水平,并保留一定余量。例如,可以用下面的方式估算实例数:
实例数 = ceil(目标请求速率 / 单实例可持续请求速率 / 目标利用率)
如果单实例在满足 P95 延迟目标时可持续处理 3 RPS,业务峰值是 15 RPS,并希望利用率不超过 70%,则需要:
ceil(15 / 3 / 0.7) = 8 个实例
这里的“可持续请求速率”必须来自满足服务等级目标的测试点,而不是压到极限时出现的最高数字。
生成式 AI 压测不能只记录 RPS
传统 Web API 往往可以用请求数描述负载,但大模型请求的计算成本与输入、输出 token 数密切相关。两个同为 1 RPS 的流量模型,如果一个平均生成 32 token,另一个生成 512 token,对端点的压力完全不同。
设计 benchmark 时至少要固定或记录以下变量:
- 模型、模型版本和推理容器版本;
- 实例类型、实例数量以及是否启用了自动扩缩容;
- 输入 prompt 的长度分布;
max_new_tokens、采样参数和实际输出长度;- 并发级别、每级持续时间和预热请求数量;
- 平均延迟、P50、P95、P99、吞吐量和错误率;
- 若使用流式推理,还应关注首 token 延迟和 token 生成速率。
测试期间如果允许自动扩缩容介入,不同并发级别可能对应不同实例数,结果就难以直接比较。评估单实例能力时,可以暂时固定容量;评估真实生产弹性时,则应保留自动扩缩容,并同时记录扩容时间和实例数量变化。这是两类不同实验,不宜混在一组结果中。
用 CreateAIBenchmarkJob 建立托管测试流程
CreateAIBenchmarkJob API 可以用于创建 AI benchmark job,并系统化执行并发扫描。由于 AWS CLI 和 SDK 中的请求字段可能随版本演进,实践时可以先让当前 CLI 生成与本地版本匹配的输入骨架:
aws --version
aws sagemaker create-ai-benchmark-job \
--generate-cli-skeleton input \
> benchmark-job.json
编辑 benchmark-job.json,按生成的字段填写 job 名称、执行角色、待测试模型或端点、测试数据位置、输出位置以及并发配置。随后提交任务:
aws sagemaker create-ai-benchmark-job \
--cli-input-json file://benchmark-job.json
可以继续通过描述接口查看任务状态。具体返回字段以当前 AWS CLI 生成的模型为准:
aws sagemaker describe-ai-benchmark-job \
--job-name YOUR_BENCHMARK_JOB_NAME
运行前需要确认三类权限:调用 benchmark API 的身份权限、benchmark job 使用的执行角色,以及该角色访问模型制品、测试数据和结果存储位置的权限。若 CLI 报告未知命令,应先升级 AWS CLI,并确认目标区域支持相应 API。
托管任务适合形成统一、可审计的基准流程。不过,在正式提交大规模任务前,用一个轻量客户端验证请求体、模型响应格式和候选并发范围,通常能更快发现配置问题。
一个可直接改造的端点并发扫描脚本
下面的 Python 脚本会调用已经部署好的 SageMaker 实时端点,依次测试多个并发级别,并输出吞吐量、延迟分位数和错误率。示例假设端点接受常见的 JSON 文本生成请求;如果模型容器使用其他协议,只需修改 PAYLOAD_JSON。
保存为 sweep.py:
import json
import math
import os
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import boto3
from botocore.config import Config
ENDPOINT_NAME = os.environ["ENDPOINT_NAME"]
REGION = os.getenv("AWS_REGION", "us-east-1")
CONCURRENCY_LEVELS = [
int(value) for value in os.getenv("CONCURRENCY", "1,2,4,8,16").split(",")
]
REQUESTS_PER_LEVEL = int(os.getenv("REQUESTS_PER_LEVEL", "40"))
WARMUP_REQUESTS = int(os.getenv("WARMUP_REQUESTS", "5"))
payload = json.loads(
os.getenv(
"PAYLOAD_JSON",
json.dumps({
"inputs": "Explain why concurrency testing matters for model serving.",
"parameters": {"max_new_tokens": 128, "do_sample": False},
}),
)
)
body = json.dumps(payload).encode("utf-8")
runtime = boto3.client(
"sagemaker-runtime",
region_name=REGION,
config=Config(
max_pool_connections=max(CONCURRENCY_LEVELS) + 4,
retries={"max_attempts": 1, "mode": "standard"},
),
)
def invoke_once():
started = time.perf_counter()
try:
response = runtime.invoke_endpoint(
EndpointName=ENDPOINT_NAME,
ContentType="application/json",
Accept="application/json",
Body=body,
)
response["Body"].read()
return {"ok": True, "latency": time.perf_counter() - started}
except Exception as exc:
return {
"ok": False,
"latency": time.perf_counter() - started,
"error": type(exc).__name__,
}
def percentile(values, p):
if not values:
return None
ordered = sorted(values)
index = max(0, math.ceil((p / 100) * len(ordered)) - 1)
return ordered[index]
print(f"Warming up endpoint with {WARMUP_REQUESTS} requests...")
for _ in range(WARMUP_REQUESTS):
invoke_once()
for concurrency in CONCURRENCY_LEVELS:
started = time.perf_counter()
with ThreadPoolExecutor(max_workers=concurrency) as pool:
futures = [pool.submit(invoke_once) for _ in range(REQUESTS_PER_LEVEL)]
results = [future.result() for future in as_completed(futures)]
elapsed = time.perf_counter() - started
successful = [item["latency"] for item in results if item["ok"]]
errors = len(results) - len(successful)
summary = {
"concurrency": concurrency,
"requests": len(results),
"successful_requests": len(successful),
"elapsed_seconds": round(elapsed, 3),
"throughput_rps": round(len(successful) / elapsed, 3),
"mean_latency_seconds": round(sum(successful) / len(successful), 3)
if successful else None,
"p50_latency_seconds": round(percentile(successful, 50), 3)
if successful else None,
"p95_latency_seconds": round(percentile(successful, 95), 3)
if successful else None,
"p99_latency_seconds": round(percentile(successful, 99), 3)
if successful else None,
"error_rate": round(errors / len(results), 4),
}
print(json.dumps(summary, ensure_ascii=False))
安装依赖并运行:
python -m pip install --upgrade boto3
export AWS_REGION=us-east-1
export ENDPOINT_NAME=my-generative-ai-endpoint
export CONCURRENCY=1,2,4,8,16
export REQUESTS_PER_LEVEL=50
export WARMUP_REQUESTS=5
python sweep.py | tee sweep-results.jsonl
如果端点请求格式不同,可以直接覆盖负载:
export PAYLOAD_JSON='{"prompt":"Write a short capacity planning checklist.","max_tokens":128}'
python sweep.py
这个脚本适合冒烟测试和确定合理的并发区间,但不能完全替代托管 benchmark。客户端所在网络、Python 线程调度和单台压测机的连接数都可能成为瓶颈。正式容量决策应结合 CreateAIBenchmarkJob 的结果以及 SageMaker 和 CloudWatch 中的端点指标。
从测试结果落到 fleet size
分析结果时,可以按以下顺序做决定:
- 先排除有错误、限流或 P95/P99 超过目标的并发级别。
- 在剩余结果中找到吞吐量增长开始明显放缓的位置。
- 选择拐点之前的可持续吞吐量,而不是理论峰值。
- 根据峰值业务流量、突发系数和目标利用率计算实例数。
- 用接近真实比例的短 prompt、长 prompt 和长输出重新验证。
- 对计划中的实例数量再做一次端到端测试,确认扩展后吞吐量是否近似线性增长。
并发扫描给出的不是一个永远有效的实例数,而是一条在特定模型、硬件、容器和请求分布下测得的容量曲线。模型版本、量化方式、推理镜像或 prompt 分布发生变化后,都应该重新运行 benchmark。把测试配置和结果纳入发布流程,才能让端点定容从一次性估算变成可重复的工程决策。