大模型推理服务性能测试实战:基于 vLLM + Locust 的压测指南与瓶颈分析

前言

最近在调研大模型(LLM)推理服务的性能表现,想实际测试一下 vLLM 框架在不同并发和任务长度下的吞吐量、首字延迟(TTFT)和显存占用情况。正好手里有 ModelScope 提供的免费 GPU 实例,于是从零开始搭建了一套基于 vLLM + Locust 的压测环境。本文将完整记录环境部署、压测脚本编写、测试过程以及最重要的——性能瓶颈数据的深度分析。

一、 环境准备与模型部署

1.1 基础环境

  • 硬件:ModelScope 免费实例(8核 32GB 显存24G)
  • 预装镜像ubuntu22.04-cuda13.0.3-py312-torch2.13.0-1.40.0
  • Python 版本:3.12(推荐使用独立虚拟环境)

1.2 安装 vLLM 并部署模型

由于 vLLM 对依赖要求较高,建议在独立的虚拟环境中安装。

bash

# 1. 创建并激活虚拟环境(假设使用 conda 或 venv)
conda create -n vllm_learn python=3.12 -y
conda activate vllm_learn

# 2. 安装 vLLM 及依赖
pip install vllm

# 3. 安装 ModelScope 库(vLLM 加载本地模型需要)
pip install modelscope>=1.18.1

# 4. 下载模型(以 Qwen2.5-7B-Instruct 为例)
# 建议提前手动下载,避免启动时网络不稳定导致下载中断
python -m modelscope.cli.cli download --model Qwen/Qwen2.5-7B-Instruct --local_dir ./Qwen2.5-7B-Instruct

1.3 启动 vLLM 推理服务

bash

# 启动 OpenAI 兼容的 API 服务
python -m vllm.entrypoints.openai.api_server \
    --model ./Qwen2.5-7B-Instruct \
    --host 0.0.0.0 --port 8000

注意:启动时可以在日志中看到 Maximum concurrency for 32,768 tokens per request: 1.96x,这预示着后续并发测试可能会遇到显存瓶颈。

二、 编写 Locust 压测脚本

2.1 安装 Locust

由于 Locust 脚本需要调用 OpenAI 接口,我们需要同时安装 Locust 和 OpenAI 的 SDK:

bash

pip install locust openai

2.2 编写 locustfile.py

这个脚本模拟用户进行流式对话,并精准计算首字延迟(TTFT)

python

# locustfile.py
from locust import HttpUser, task, between
from openai import OpenAI
import time

class LLMUser(HttpUser):
    # 设置你的vLLM服务地址
    host = "http://localhost:8000/v1"
    wait_time = between(0.5, 2)

    def on_start(self):
        # 初始化OpenAI客户端,因为vLLM兼容OpenAI接口
        self.client = OpenAI(
            base_url=self.host,
            api_key="sk-test" # 本地部署不需要真实key
        )

    @task
    def chat(self):
        # 1. 记录开始请求时间
        start_time = time.time()
        first_token_received = False

        try:
            # 2. 创建流式响应请求
            stream = self.client.chat.completions.create(
                model="./Qwen2.5-7B-Instruct",
                messages=[{"role": "user", "content": "请用一句话解释量子纠缠"}],
                stream=True,
                max_tokens=1000
            )

            # 3. 遍历流式响应,这是计算TTFT的关键
            for chunk in stream:
                if chunk.choices[0].delta.content:
                    # 4. 记录第一个有效token出现的时间,即TTFT
                    if not first_token_received:
                        ttft = (time.time() - start_time) * 1000
                        first_token_received = True
                        # 5. 向Locust报告自定义的TTFT指标
                        self.environment.events.request.fire(
                            request_type="LLM",
                            name="TTFT_ms",
                            response_time=ttft,
                            response_length=0
                        )

        except Exception as e:
            # 6. 处理异常,方便排查问题
            self.environment.events.request.fire(
                request_type="ERROR",
                name="error",
                response_time=0,
                exception=e,
                response_length=0
            )

三、 压测执行与核心指标解读

3.1 启动压测

在终端运行 Locust,并在浏览器打开 http://localhost:8089 设置参数:

bash

locust -f locustfile.py --host http://localhost:8000/v1
  • Number of users:并发用户数。
  • Ramp up:每秒启动用户数。
  • Run time:运行时间(如 5m)。

3.2 核心指标解读

指标名称 含义 重要性
RPS 每秒完整处理的请求数 反映系统的业务吞吐能力
TTFT (首字延迟) 用户发出请求到收到第一个字的时间 直接影响用户体验
P95/P99 延迟 95%/99% 请求的响应时间上限 反映系统的长尾稳定性
服务端 Throughput 日志中的 Avg generation throughput (tokens/s) 反映 GPU 算力利用率

四、 性能测试数据与瓶颈深度剖析(重点)

经过阶梯加压测试,我们发现了大模型推理服务非常典型的“非线性性能规律”。

4.1 并发数与吞吐量的关系(固定 max_tokens=1000)

  • 2并发 -> 32并发:系统吞吐量呈线性增长,RPS 从 0.6 升至 8.3。
  • 64并发:RPS 达到 16.3,P95 延迟依然保持在 2.2 秒。此时服务端吞吐量达到 628 tokens/s。
  • 128并发(拐点):RPS 增至 36.6,但 P95 延迟飙升至 5.7秒,P99 达到 11 秒,且出现了 50 个请求失败
  • 结论:该环境在 64 并发下处于健康状态,128 并发已超出系统承载极限。

4.2 核心发现:任务长度对性能的反直觉影响(固定 64 并发)

我们在 64 并发下,尝试改变请求的 max_tokens 长度,得到了非常“反直觉”但极具价值的现象:

max_tokens RPS P95 首字延迟 (ms) 服务端吞吐 (tokens/s) 现象解读
1000 16.3 2200 628 正常状态,批处理未能完全填满
2000 8.3 3800 589 性能衰退:RPS腰斩,GPU算力遇瓶颈
4000 20.1 2200 719 性能回升:调度优化,消除气泡
8000 24.7 200 851 🎉 最佳性能点(黄金任务长度)
16384 11.3 1900 754 过载:最大延迟飙升至 63 秒

🔍 深度分析:

  1. 为什么 8000 tokens 时 P95 只有 200ms?
    在 8000 tokens 的任务长度下,单个请求的处理时间足够长(约10-20秒)。这给了 vLLM 的 Continuous Batching(连续批处理) 充足的时间去打包请求,彻底消除了批次间的“气泡”效应。GPU 流水线被完美填满,后来的请求几乎不需要排队就能被立即处理,因此延迟极低。
  2. 为什么 16384 tokens 时性能崩塌?
    当任务过长,单次 Attention 计算量呈平方级增长,GPU 的算力被耗尽。同时,长任务导致批处理碎片化,一个请求要等前面的几千个 token 全部生成完才能被调度,导致长尾延迟恶化至 63 秒。
  3. 关于 8000 tokens 的设定与模型的关系
    这个“黄金任务长度”并非绝对,它是由 Qwen2.5-7B 模型的架构(计算量)当前 GPU 的算力 共同博弈的平衡点。如果更换模型或增加显卡,这个数值将会改变。但该测试给出了“寻找最优任务长度”的通用方法论。

五、 总结与生产建议

通过这次从零搭建的压测环境,我们不仅跑通了 vLLM + Locust 的完整链路,还深入理解了 LLM 推理服务的性能特性:

  1. 并发建议:该实例建议生产环境并发数控制在 64 左右。
  2. 任务长度建议:建议单次请求的 max_tokens 设置在 8000 左右,此时可以达到最佳吞吐量(851 tokens/s)与最佳用户体验(P95 200ms)。
  3. 瓶颈定位:当前环境的瓶颈在于GPU 算力(Compute Bound),而非显存容量(KV Cache 始终低于 1.1%)。若需支撑更高并发或更长文本,需引入多卡张量并行(TP)或升级显卡。

六、 踩坑记录(附)

  • 问题1:vLLM 启动报错 ImportError: Please install modelscope
    • 解决:虚拟环境中缺少 modelscope 库,执行 pip install modelscope>=1.18.1 即可。
  • 问题2:Locust 压测时频繁出现 404 Not Found
    • 解决:检查脚本中的 host 路径是否包含 /v1,以及 model 参数名称是否严格与 vLLM 启动时设置的 served_model_name 一致。
posted @ 2026-09-17 15:09  xzy186  阅读(15)  评论(0)    收藏  举报