负载测试LLM-API 本节摘要:传统负载测试工具不是为流式响应、可变输出长度、token 级指标和 GPU 饱和设计的。两个陷阱咬住大多数团队。GIL 陷阱(GIL trap):Locust 的 token 级测量在 Python GIL 下跑分词,与请求生成在高并发下争用,分词积压把报告的 token 间延迟(ITL)撑大——瓶颈是你的客户端,不是服务器。提示一致性陷阱(prompt-uniformity trap):循环里发同一个提示只测了 token 分布上的一点;真实流量长度多变、前缀匹配多样,LLMPerf 用 + 修正。2026 工具映射:LLM 专用的(GenAI-Perf、LLMPerf、LLM-Locust、guidellm)拿 token 级精度;k6 v2026.1.
本节摘要:传统负载测试工具不是为流式响应、可变输出长度、token 级指标和 GPU 饱和设计的。两个陷阱咬住大多数团队。GIL 陷阱(GIL trap):Locust 的 token 级测量在 Python GIL 下跑分词,与请求生成在高并发下争用,分词积压把报告的 token 间延迟(ITL)撑大——瓶颈是你的客户端,不是服务器。提示一致性陷阱(prompt-uniformity trap):循环里发同一个提示只测了 token 分布上的一点;真实流量长度多变、前缀匹配多样,LLMPerf 用
--mean-input-tokens+--stddev-input-tokens修正。2026 工具映射:LLM 专用的(GenAI-Perf、LLMPerf、LLM-Locust、guidellm)拿 token 级精度;k6 v2026.1.0 + k6 Operator 1.0 GA(2025 年 9 月)——流式感知、通过 TestRun/PrivateLoadZone CRD 原生 K8s 分布式、最适合 CI/CD 门;Vegeta 做 Go 恒定速率饱和;Locust 2.43.3 仅在配 LLM-Locust 扩展时用于流式。负载模式:稳态、爬坡、尖峰(测自动伸缩)、浸泡(测内存泄漏)。
对应原课程:Phase 17 · Lesson 22 ·
22-load-testing-llm-apis(原英文phases/17-infrastructure-and-production/22-load-testing-llm-apis/docs/en.md)。
阅读完本节,你应当能够:
你用 k6 把 LLM 端点压到 500 并发用户。扛住了。你上线了。生产里只有 200 真实用户,服务却趴了——P99 TTFT 爆炸,GPU 钉死。
两件事发生了。第一,k6 发了 500 个一模一样的提示——请求合并与前缀缓存让你「看起来」在处理 500 路并发解码,实际上只处理了 1 路。第二,k6 不会像人眼体验那样追踪流式响应的 token 间延迟;它看到的是一条 HTTP 连接,不是 500 个 token 以不同间隔到达。
LLM 的负载测试是它自己的学科。
Locust 用 Python,客户端分词跑在 GIL 下。高并发时分词器排在请求生成后面。报告的 ITL 包含了客户端的分词积压。你以为服务器慢,其实是测试器慢。
修正:LLM-Locust 扩展把分词挪到独立进程,或用编译语言工具(k6、用 tokenizers.rs 的 LLMPerf)。
所有已知负载器都只让你配一个提示。循环 1 万次里每次发完全相同的提示——服务器每次看到同一个前缀,前缀缓存命中率逼近 100%,吞吐看起来很美。
修正:从提示分布里采样。LLMPerf 用 --mean-input-tokens 500 --stddev-input-tokens 150——长度多样、内容多样。
LLMPerf(Anyscale)—— Python 但 Rust 后端分词。均值/标准差提示。流式感知。性能跑的默认选择。
NVIDIA GenAI-Perf —— NVIDIA 参考实现。用 Triton 客户端;指标覆盖全面。注意它的 ITL 不含 TTFT,LLMPerf 的含——同一服务器两工具报出不同的 TPOT。
LLM-Locust(TrueFoundry)—— 修掉 GIL 陷阱的 Locust 扩展。熟悉的 Locust DSL + 流式指标。
guidellm —— 大规模合成基准。
k6 v2026.1.0 + k6 Operator 1.0 GA(2025 年 9 月):
Vegeta —— Go,比 k6 简单。恒定速率 HTTP 饱和。不 LLM 感知,但适合网关/限流测试。
Locust 2.43.3 原版 —— 对 LLM 有 GIL 陷阱。只在配 LLM-Locust 扩展时用。
在 PR 上跑 k6:
从真实流量样本(若有)或公开分布(如聊天用 ShareGPT、代码用 HumanEval)构造。把均值 + 标准差喂给 LLMPerf。不惜一切代价避免「单提示循环」。
原课程 code/main.py 模拟带真实提示分布的负载测试,测有效 TPOT,并演示一致性陷阱。下面给最小可读的提示采样与有效 TPOT 讋定骨架。
import random def sample_prompt_lengths(mean_tokens, stddev_tokens, n, seed=0): """从正态分布采样 n 个提示长度,模拟真实流量。 contrast: 一致性陷阱是循环里全发同一长度。 """ rng = random.Random(seed) lengths = [max(8, int(rng.gauss(mean_tokens, stddev_tokens))) for _ in range(n)] return lengths def effective_tpot(generate_times_ms, output_tokens): """有效 TPOT = 总生成时间 / 输出 token 数(含 TTFT)。 generate_times_ms: 每次请求首 token 到末 token 的墙钟 output_tokens: 每次请求的输出 token 数 """ total_time = sum(generate_times_ms) total_tokens = sum(output_tokens) return round(total_time / total_tokens, 3) # ms/token # 案例:对比一致性 vs 真实分布 uniform = [500] * 1000 # 一致性陷阱 real = sample_prompt_lengths(500, 150, 1000) # 真实分布 print(f"一致性长度方差={0}, 真实分布方差≈{(500, 150)}") # 真实分布会暴露前缀缓存失效后的真实解码压力,一致性分布会掩盖它
💡 一致性陷阱的修复成本几乎为零:把「单提示循环」换成「按均值 + 标准差采样」,你就能从「虚假的乐观」切到「诚实的容量数字」。
| 场景 | 首选工具 | 为什么 | 陷阱 |
|---|---|---|---|
| 基准性能跑 | LLMPerf | Rust 后端分词、均值/标准差提示、流式感知 | 无 |
| NVIDIA 硬件参考 | GenAI-Perf | 厂商参考、Triton 客户端、指标全 | ITL 不含 TTFT,与 LLMPerf 报的 TPOT 不同 |
| CI/CD 门 | k6 v2026.1 + k6 Operator | Go 编译无 GIL、流式感知、K8s 原生 CRD 分布式 | 需配真实提示分布,否则落入一致性陷阱 |
| 大规模合成压测 | guidellm | 合成流量、规模大 | 不 LLM 感知需校准 |
| 网关/限流饱和 | Vegeta | Go、恒定速率、简单 | 不 LLM 感知 |
| 熟悉 Locust DSL 的团队 | LLM-Locust | 修掉 GIL 陷阱、保留 Locust DSL | 原版 Locust 2.43.3 不可用 |
心法:工具选型先问「测什么」——基准跑选 LLMPerf,CI 门选 k6,大规模选 guidellm,厂商参考选 GenAI-Perf。无论哪个,真实提示分布(均值 + 标准差)是入场券。
本节产出 outputs/skill-load-test-plan.md(原课程目录)。给定工作负载与 SLA,它产出:
跑通模拟器:运行 code/main.py。对比一致性 vs 真实分布——差距在哪?
写 CI 门 k6 脚本:100 并发、运行 5 分钟、TTFT P95 < 800ms。写出脚本骨架。
浸泡诊断:浸泡测试显示内存每小时涨 50MB。列出三种可能原因,以及用以区分它们的探针。
尖峰恢复:从 10 RPS 尖峰到 100 RPS。若已部署 Karpenter + vLLM production-stack(第 03、18 节),预期恢复时间多少?
TPOT 不一致:同一服务器,GenAI-Perf 报 TPOT=6ms,LLMPerf 报 TPOT=11ms。解释为什么。
下一节,我们进入面向 AI 的 SRE——多智能体事故响应、运行手册、预测性检测如何把凌晨三点的排班从 30 分钟压到 5 分钟。