大模型压测总踩坑:显存 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 的方案相比,更具有价值。

9. 参考链接

  1. https://blog.csdn.net/keeppractice/article/details/147012874