大模型压测总踩坑:显存 OOM 与 TTFT 解决指南(附代码与避坑清单)
电商平台上线 AI 客服大模型,仅用 10 条短 prompt 简单地测了个 QPS=50 便直接投入运行。
结果在大促当天,用户发来的长 prompt 占比陡然增至 40%,而并发量刚达到 30 时,显存便立刻爆了 OOM(Out Of Memory),客服系统直接宕机长达两小时,所造成的损失超过百万元——
大模型压测,绝不是仅仅跑出个 QPS 就能敷衍了事的。
1. 大模型压测的"反传统"特性
传统软件压测仅仅关注"请求成不成",不过大模型的"思考生成"双阶段架构(Prefill-Decode),Prefill 阶段通常是计算密集型(Compute-Bound),而 Decode 阶段则是内存密集型(Memory-Bound),将压测逻辑进行了彻底的重构:
1.1. 核心差异:PD 分离架构的"两面性"
👉 举个实例:用 GPT-4 写 500 字报告,Prefill 只占 30% 耗时,却决定用户"会不会不等就关掉";
Decode 占 70% 耗时决定"读的时候会不会觉得卡"。
2. 必避的 3 个"反常识"坑
- 不是"并发越高越好":企业对 Llama3 进行测试,并发从 20 开始逐渐提升到 30,此时 GPU 的利用率从 80% 上升到 95%,不过吐字率却从 50 Tokens 急剧下降到 15 Tokens,用户直接给出反馈"像卡壳了";
- 长 prompt 可谓是"隐形杀手":64k Token 的 prompt,它会使 KV Cache 占用翻倍,相较于短 prompt 而言,也更易于触发 OOM;
- 缓存会"骗结果":相同 prompt,需加 UUID(例如
f"({uuid.uuid4()}) 你的 prompt"),如此便可避免测试的是"缓存性能"而不是"真实性能"。
3. 压测核心指标:5 个"必看" + 行业基准
新手总是紧紧地盯着 QPS,但是实际上真正有作用的却是这 5 个指标——不同的场景,其合格线存在着很大的差异:
| 指标 | 计算公式 | 电商客服(短 prompt) | 金融报告(长 prompt) | 测试技巧 |
|---|---|---|---|---|
| TTFT | T_first - T_request |
≤1.5s(P99) | ≤2.5s(P99) | 每次请求加 UUID 防缓存 |
| 吐字率 | Token 总数 / (T_end - T_first) |
≥60 Tokens | ≥30 Tokens | 排除前 3 个 Token 的波动值 |
| QPM | 成功请求数 / (时长 / 60) |
≥120 | ≥40 | 阶梯加压找"拐点"(如并发 25 后 QPM 不升反降) |
| TPOT(单 Token 耗时) | 单 Token 生成时间 | ≤30ms | ≤80ms | 取稳定阶段平均值 |
| GPU 利用率 | 计算核心占用率 | 75%-90% | 65%-85% | 避免长期≥95%(容易崩) |
👉 关键结论:用户感知优先级,TTFT>"吐字率">"QPM"。
例如在金融场景中,即便 QPM 较低,但一定要保证 TTFT≤2.5s,因为分析师等不起。
4. 实战:从 0 到 1 做压测(300 行可跑代码)
以"Locust+OpenAI 接口"为例,适配自建模型、阿里云 PAI/AWS SageMaker,3 步搞定:
4.1. 环境搭建(5 分钟完成)
# 1. 安装依赖(Python 3.9+,指定 aiohttp 版本防兼容坑)
pip install locust openai python-dotenv aiohttp==3.9.1
# 2. 新建 .env 文件(存密钥,别硬编码!)
echo "OPENAI_API_KEY=sk-你的密钥
LLM_BASE_URL=https://api.你的模型地址/v1" > .env
4.2. 核心脚本:Locust 压测代码(支持长短 prompt 混合)
# locust_llm.py
import os
import uuid
import time
import json
import tiktoken # 精准计算 Token 的库
from dotenv import load_dotenv
from locust import HttpUser, task
# 加载环境变量
load_dotenv()
API_KEY = os.getenv("OPENAI_API_KEY")
BASE_URL = os.getenv("LLM_BASE_URL")
# 【新增】精准计算 Token 数(替代原"//2 估算",避免误差)
def count_tokens(prompt, model="gpt-4-turbo"):
encoder = tiktoken.encoding_for_model(model)
return len(encoder.encode(prompt))
# 1. 生成测试用例(短/长 prompt 覆盖)
def generate_test_cases():
# 场景1:电商客服(100 Token,高频)
short_prompt = f"({uuid.uuid4()}) 我的订单12345没发货,帮我查物流"
# 场景2:金融报告(16k Token,高资源消耗)
long_prompt = f"({uuid.uuid4()}) " + "某公司2024财报:营收10亿,净利润2.3亿,毛利率35%..." * 80
# 验证 Token 数(避免实际长度不符)
print(f"短prompt Token数:{count_tokens(short_prompt)}")
print(f"长prompt Token数:{count_tokens(long_prompt)}")
return [
{"name": "short_prompt", "content": short_prompt},
{"name": "long_prompt", "content": long_prompt}
]
TEST_CASES = generate_test_cases()
# 2. 自定义指标容器(TTFT、吐字率,按 prompt 类型分)
CUSTOM_STATS = {
"first_token_latency_short": [],
"first_token_latency_long": [],
"token_rate_short": [],
"token_rate_long": []
}
# 3. 定义压测用户行为
class LLMTestUser(HttpUser):
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# 权重 3:1,模拟电商场景短 prompt 更多
@task(3)
def test_short_prompt(self):
self.run_test(TEST_CASES[0])
@task(1)
def test_long_prompt(self):
self.run_test(TEST_CASES[1])
def run_test(self, test_case):
prompt_name = test_case["name"]
prompt_content = test_case["content"]
start_time = time.time()
first_token_time = None
total_tokens = 0
# 流式请求(模拟用户"边看边等"的真实场景)
with self.client.post(
url=f"{BASE_URL}/chat/completions",
json={
"model": "gpt-4-turbo", # 替换成你的模型(如 qwen-plus/vllm-7b)
"messages": [{"role": "user", "content": prompt_content}],
"stream": True,
"max_tokens": 1000 # 按业务需求改
},
headers=self.headers,
stream=True,
name=prompt_name # Locust UI 里按 prompt 类型分组
) as response:
for line in response.iter_lines():
if not line:
continue
line_str = line.decode("utf-8").strip()
if not line_str.startswith("data:"):
continue
line_str = line_str[5:].strip()
if line_str == "[DONE]":
break
# 解析 Token(按你模型的响应格式调整,此处适配 OpenAI)
try:
data = json.loads(line_str)
token = data["choices"][0]["delta"].get("content", "")
if token and not first_token_time:
# 记录首 Token 到达时间
first_token_time = time.time()
if token:
# 用 tiktoken 精准算 Token 数(替代原估算)
total_tokens += count_tokens(token)
except Exception as e:
print(f"解析响应出错:{e}")
# 计算并记录指标
if first_token_time and total_tokens > 0:
first_token_latency = first_token_time - start_time
token_rate = total_tokens / (time.time() - first_token_time)
# 按 prompt 类型存指标
if prompt_name == "short_prompt":
CUSTOM_STATS["first_token_latency_short"].append(first_token_latency)
CUSTOM_STATS["token_rate_short"].append(token_rate)
else:
CUSTOM_STATS["first_token_latency_long"].append(first_token_latency)
CUSTOM_STATS["token_rate_long"].append(token_rate)
# 启动命令(复制到终端执行)
# locust -f locust_llm.py --host=https://api.你的模型地址 --web-port=8089
4.3. 压测执行:阶梯加压 + 结果分析
新建 locust_stages.yaml 配置阶梯压力(模拟真实流量增长):
stages:
- duration: 3m # 预热期:1 并发(让模型加载缓存)
target: 1
- duration: 10m # 基准期:10 并发(日常流量)
target: 10
- duration: 10m # 压力期:25 并发(峰值流量)
target: 25
- duration: 5m # 极限期:40 并发(故障演练)
target: 40
👉 执行命令:
locust -f locust_llm.py --host=https://api.你的模型地址 --web-port=8089阶梯加压可用 Web UI 的 Step Load 模式(
--step-load --step-users 40 --step-time 10m),或按locust_stages.yaml的节奏手动调节并发。
打开 http://localhost:8089 看结果:
- 若 TTFT 在并发为 25 之后超 2.5s(金融线),就先扩容 Prefill 节点;
- 若"吐字率"骤降但 GPU 利用率反而更低,就先排查 Decode 显存调度(如 KV Cache 碎片化,可开启 PagedAttention)。
5. 个高频瓶颈:秒定位 + 解决方案
| 异常现象 | 根因分析 | 解决方案(附工具) |
|---|---|---|
| TTFT>3s,GPU 利用率低 | Prefill 阶段 CPU 瓶颈 | 用 htop 查 CPU 占用,关无关进程;Prefill 节点 CPU:GPU 按 1:4 配(如 16G 显存 GPU 配 64 核 CPU) |
| 吐字率波动大,显存占用高 | KV Cache 碎片化 | 用 nvidia-smi -l 1 看显存碎片;开启 PagedAttention(vLLM 默认已启用,可用 --gpu-memory-utilization 调节 KV Cache 显存占比) |
| 多模态请求超时(图文混合) | 图像预处理占 GPU | 单独部署 TensorRT 加速的图像处理服务;分队列测"文本"和"图文"请求 |
6. 年 3 个关键演进方向
- 智能化压测工具:DeepMind 的 JEST 算法,能够自动模拟"随机长短 prompt,以及间歇请求",相比人工用例,其精准度高出 30%;
- 边缘端压测:车载 IoT 设备资源较为有限,会新增"内存占用"以及"功耗"这两个指标(例如测试 Llama3-8B 在车载芯片上所带来的续航方面的影响);
- 跨云协同测试:企业混用多平台模型时,EvalScope 等工具支持"一键对比 AWS SageMaker 和阿里云 PAI 的 QPM"。
7. 总结:压测落地 3 个"黄金法则"
- 先定场景,接着再选指标:电商要看"TTFT,加上 QPM",金融要看"吐字率以及显存",千万不要盲目地去追高并发;
- 工具组合用:Locust 测 HTTP 接口,SGLangBench 测推理引擎(vLLM/TensorRT-LLM),EvalScope 测多模态;
- 压测并非一次性:需建立"日常基准(每日更新),发布前极限(迭代测试),线上监控(实时监测)"三级体系,以避免在生产过程中遭遇问题。
8. 互动提问
你在大模型压测中踩过哪些坑?
是显存 OOM还是吐字率崩了?
评论区分享你的经验和想法!
最后一句经验:大模型压测的终极目标,并非"测到最高并发",而是"寻找到业务需求与资源成本的均衡点"——
比如说电商客服,当并发达到 30 时,TTFT≤1.5s 与并发为 50 然而延迟却为 5s 的方案相比,更具有价值。