免费模型往往使用共享服务器,邻居的批量任务会拖慢你的请求延迟。文章提供可复现的基准测试方法,测量模型+服务器这对组合的实际性能,而非仅评估模型输出质量。
你选了一个免费模型,因为答案看起来不错。好答案不是终点。终点是模型加上服务器再加上网络。演示能过。流水线却卡住了。模型很少是问题所在。
那为什么我们一直在只基准测试模型?因为它太简单了。你粘贴一条 prompt,读取输出,宣布一个赢家。服务器从来没有任何发言权。
这篇文章是一个可复现的基准测试。它测量的是这对组合,而不是模型。在你把任何免费端点接入 CI 之前,先跑一下它。
大多数评估比较的是答案。你粘贴一条 prompt,判断输出,选一个赢家。那测的是模型,忽略了服务器。
免费模型访问通常意味着共享端点。免费服务器选项意味着共享租户。其他用户共享 CPU、内存和网络。你的延迟就是他们的延迟。你的超时就是他们的超时。
这是我一直在看到的场景。一个团队在周五评估了一个免费模型。答案看起来很棒。周一他们把它接入了 CI。到周三,流水线就红了。模型没有变,服务器变了。一个邻居启动了一个批处理任务。现在每个请求都排在它后面。
我把同样的测试工具应用到了 MonkeyCode 的免费模型访问和他们的免费服务器选项上。
Disclosure:本文是作为 MonkeyCode 产品推广的一部分准备的。
我不信任演示。我建了一个测试工具。
一个基准测试需要三样东西:一组固定的 prompt、并发阶梯和通过/失败表。这是我使用的工具。
#!/usr/bin/env python3
"""Benchmark a model endpoint as a pair: model + server."""
import argparse
import asyncio
import json
import statistics
import time
import httpx
PROMPTS = [
"Say OK.",
"Classify this log line: ERROR disk full",
"Return one word: is 429 a retryable status?",
]
async def fire(client, url, payload, sem, timeout=30):
async with sem:
start = time.perf_counter()
try:
r = await client.post(url, json=payload, timeout=timeout)
return r.status_code, time.perf_counter() - start
except Exception as exc:
return type(exc).__name__, time.perf_counter() - start
async def run_level(client, url, payload, concurrency, n):
sem = asyncio.Semaphore(concurrency)
t0 = time.perf_counter()
results = await asyncio.gather(
*[fire(client, url, payload, sem) for _ in range(n)]
)
wall = time.perf_counter() - t0
ok = [lat for code, lat in results if code == 200]
errors = [r for r in results if r[0] != 200]
p95 = None
if len(ok) >= 20:
ordered = sorted(ok)
p95 = ordered[int(len(ok) * 0.95) - 1]
return {
"concurrency": concurrency,
"requests": n,
"ok": len(ok),
"errors": len(errors),
"p50": round(statistics.median(ok), 3) if ok else None,
"p95": round(p95, 3) if p95 is not None else None,
"throughput": round(len(ok) / wall, 2),
}
async def main():
ap = argparse.ArgumentParser()
ap.add_argument("--url", required=True)
ap.add_argument("--payload", default='{"prompt": "Say OK", "max_tokens": 16}')
ap.add_argument("--header", action="append", default=[])
args = ap.parse_args()
headers = {}
for h in args.header:
key, _, value = h.partition(":")
headers[key.strip()] = value.strip()
payload = json.loads(args.payload)
async with httpx.AsyncClient(headers=headers) as client:
t0 = time.perf_counter()
try:
await client.post(args.url, json=payload, timeout=60)
print(f"cold_start: {time.perf_counter() - t0:.2f}s")
except Exception as exc:
print(f"cold_start: FAILED ({exc})")
for c in (1, 2, 4, 8):
report = await run_level(client, args.url, payload, c, 20)
print(json.dumps(report))
if __name__ == "__main__":
asyncio.run(main())
这个脚本做四件事:
用一次请求探测冷启动。
在并发 1、2、4、8 下各跑 20 个请求。
记录 p50、p95、错误数和吞吐量。
打印 JSON 以便存储和对比报告。
为什么是 20 个请求?足够得到一个稳定的 p95。不会触发真正的速率限制。并发阶梯才是关键。单个请求会隐藏排队问题。八个并发请求会暴露它。
python3 bench_endpoint.py \
--url "https://your-endpoint.example/v1/complete" \
--payload '{"prompt": "Say OK", "max_tokens": 16}' \
--header "Authorization: Bearer $TOKEN"
payload 是你的,端点 schema 是你的。传递你自己的 header 和 body。工具保持不变。
示例输出(说明性,非实测):
cold_start: 4.12s
{"concurrency": 1, "requests": 20, "ok": 20, "errors": 0, "p50": 0.84, "p95": 1.21, "throughput": 1.12}
{"concurrency": 2, "requests": 20, "ok": 20, "errors": 0, "p50": 1.10, "p95": 1.98, "throughput": 1.74}
{"concurrency": 4, "requests": 20, "ok": 17, "errors": 3, "p50": 2.31, "p95": 6.44, "throughput": 2.02}
{"concurrency": 8, "requests": 20, "ok": 9, "errors": 11, "p50": 4.87, "p95": 12.30, "throughput": 1.13}
看到规律了吗?并发 1 看起来健康。并发 4 开始丢请求。并发 8 崩溃了。单个 prompt 测试永远不会发现这个问题。
用这张表作为起点。根据你的工作负载调整阈值。
绿色意味着这对组合可以处理同步 CI gate。黄色意味着只适合批处理或异步。红色意味着不要把它接入。
错误率比延迟更重要。慢答案可以重试。丢掉的请求无法重试。先看错误列。
冷启动是一个特殊情况。调度的流水线会唤醒服务器。如果第一次调用要 20 秒,你的任务在模型说话之前就超时了。探测它。了解空闲的代价。
免费服务器在可预测的地方失败。以下是我最常看到的四种。
空闲后的冷启动。第一次调用付出代价。调度的流水线唤醒服务器。你的 p95 变成了你的 p100。
共享租户噪音。一个邻居的批处理任务改变了你的延迟。你的数字每小时都不一样。跑两次阶梯。对比分布。
没有 header 的速率限制。429 响应不带 Retry-After。你的重试逻辑在猜。猜会让问题更糟。指数退避是唯一安全的做法。
长 prompt 超时。模型能回答。服务器先放弃。你的 30 秒超时杀掉了有效工作。用真实 payload 测量,不要用玩具 prompt。
这些都不会在 prompt 比较中出现。都会在并发阶梯中出现。
一次性基准测试只是一个快照。定时基准测试是一个趋势。每周跑一次工具。保留 JSON。对比数字。
benchmark:
stage: test
image: python:3.12-slim
script:
- pip install httpx
- python3 bench_endpoint.py --url "$ENDPOINT_URL" --payload '{"prompt": "Say OK", "max_tokens": 16}' --header "Authorization: Bearer $TOKEN" > report.json
artifacts:
paths:
- report.json
expire_in: 30 days
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
这个 job 需要两个 CI 变量。ENDPOINT_URL 和 TOKEN。在 GitLab 中添加一个 schedule。在你信任这个趋势之前先审查 artifact。
当错误率从绿色变为黄色时设置警报。这是你的早期预警。免费服务器在告诉你什么。在流水线崩溃之前听它说。
如果你需要亚秒级 p95 来做用户面向的功能,跳过这种方法。如果你需要在高峰时段保证吞吐量,跳过它。免费服务器是共享的。共享意味着变化。变化意味着你需要重试、一个队列或付费层。
基准测试也有局限性。它测量一个端点、一个 payload、一天。它不测量答案质量。为此要配合 golden tests 一起用。
如果你的团队无法根据数字采取行动,也跳过它。没有决策表的基准测试只是一个图表。在你跑之前先决定阈值。这样输出是一个 verdict,而不是一个好奇的观察。
模型回答。服务器交付。两者都必须经受住 CI。在你信任端点之前先基准测试这对组合。这周跑一下工具。数字会告诉你比演示更多的东西。