如何做压力测试?DeepSeek-R1-Distill-Qwen-1.5B并发请求模拟实战
你刚把 DeepSeek-R1-Distill-Qwen-1.5B 模型搭好 Web 服务,界面跑起来了,单次提问也流畅——但心里总有点不踏实:如果同时来 20 个用户问数学题,30 个开发者调接口生成代码,服务会不会卡住?响应变慢?甚至直接崩掉?这不是杞人忧天,而是上线前必须直面的问题。
压力测试不是“等出问题再救火”,而是主动把服务推到临界点,看清它的真实承载力。本文不讲抽象理论,不堆参数公式,就用你手头正在跑的这个 1.5B 模型服务(基于 Gradio 的 Web 接口),从零开始实操一次完整的并发请求模拟:怎么装工具、怎么写脚本、怎么看指标、怎么定位瓶颈、怎么调参优化。所有步骤都可复制,所有命令都经过验证,连日志里报什么错、GPU 显存涨多少、响应时间跳到几秒,我都给你记下来了。
你不需要是性能专家,只要会运行 Python 脚本、能看懂终端输出,就能搞懂你的模型服务到底“扛不扛压”。
1. 为什么必须对 DeepSeek-R1-Distill-Qwen-1.5B 做压力测试?
1.1 这不是普通小模型,它的推理特性决定了压力表现很特别
DeepSeek-R1-Distill-Qwen-1.5B 看似只有 1.5B 参数,但它继承了 DeepSeek-R1 的强化学习蒸馏能力,专攻数学推理、代码生成、多步逻辑链。这意味着:
- 它的每次响应往往不是简单续写,而是要“想几步”:比如解方程要推导中间步骤,写 Python 要检查语法+逻辑+边界条件;
- Token 生成过程更耗时,尤其在
max_tokens=2048且temperature=0.6时,模型倾向于生成更长、更严谨的输出; - GPU 计算不是匀速流水线,而是“爆发式”:一次复杂推理可能瞬间吃满显存带宽,紧接着空闲几百毫秒。
所以,用传统“QPS=请求数/秒”的粗粒度指标去评估它,很容易误判。你得看到每一轮请求的真实延迟分布、显存峰值波动、失败请求的具体原因。
1.2 你的部署方式,天然存在几个隐性瓶颈点
你当前用的是 Gradio 启动的 Web 服务(端口 7860),这很便捷,但默认配置下有三处“温柔陷阱”:
- 单进程阻塞:Gradio 默认是单线程处理请求,第2个请求必须等第1个完全返回才能进队列;
- 无连接池管理:每个 HTTP 请求都新建 TCP 连接,高并发时大量 TIME_WAIT 状态会占满端口;
- 显存未预分配:模型权重和 KV Cache 是按需加载,首次并发请求可能触发多次 CUDA 内存重分配,造成抖动。
这些不会在单用户测试中暴露,但一旦并发量上到 8+,延迟就会明显拉长,甚至出现CUDA out of memory错误——而这恰恰是你最需要提前发现的。
1.3 压力测试的目标很实在:回答三个关键问题
我们不做花哨的全链路压测,就聚焦你最关心的三件事:
- 这台机器(你的 GPU 型号 + 显存大小)最多能稳定支撑多少并发用户?
- 在安全并发数下,95% 的请求响应时间能不能控制在 3 秒内?
- 如果响应变慢,到底是 CPU 卡住了、GPU 算不动了,还是网络或框架拖了后腿?
答案不在文档里,而在你敲下python stress_test.py后的那张实时图表里。
2. 准备工作:环境检查与轻量级压测工具选型
2.1 先确认你的服务真的在跑,且能被外部访问
别跳过这一步。很多压测失败,根源是服务根本没对外暴露。
打开终端,执行:
curl -s http://localhost:7860 | head -n 10如果返回一堆 HTML(含<title>Gradio</title>),说明服务正常;如果超时或报Connection refused,请先检查:
- 是否已执行
python3 /root/DeepSeek-R1-Distill-Qwen-1.5B/app.py? - 是否被防火墙拦截?临时放行:
sudo ufw allow 7860 - Docker 部署的话,确认
-p 7860:7860映射正确,且容器状态为Up
2.2 选择locust:轻量、Python 原生、结果直观
我们不用 JMeter(太重)、不用 k6(需学新 DSL),就用locust—— 它是 Python 写的,脚本就是纯 Python 类,你改提示词、调参数、加日志,就像写自己项目一样自然。
安装只需一行:
pip install locust它会自动启动一个 Web 控制台(默认http://localhost:8089),你点点鼠标就能发压测任务,还能实时看图表,比写命令行参数友好太多。
2.3 写一个最简可用的压测脚本:stress_test.py
创建文件stress_test.py,内容如下(已适配你的 DeepSeek-R1-Distill-Qwen-1.5B 接口):
# stress_test.py from locust import HttpUser, task, between import json import random class DeepSeekUser(HttpUser): wait_time = between(1, 3) # 每个用户随机等待1-3秒,模拟真实节奏 @task def generate_code(self): # 模拟开发者最常问的:写一个快速排序 prompt = "用 Python 写一个带详细注释的快速排序函数,要求处理空列表和重复元素。" payload = { "prompt": prompt, "temperature": 0.6, "max_new_tokens": 512, "top_p": 0.95 } # Gradio API 的标准路径是 /run/predict,注意 Content-Type with self.client.post( "/run/predict", json=payload, headers={"Content-Type": "application/json"}, catch_response=True # 允许手动标记成功/失败 ) as response: if response.status_code != 200: response.failure(f"HTTP {response.status_code}") else: try: result = response.json() # Gradio 返回结构:{"data": ["生成的文本"]} if not result.get("data") or not isinstance(result["data"], list): response.failure("Invalid response format: no 'data' list") elif len(result["data"]) == 0 or not result["data"][0].strip(): response.failure("Empty or whitespace-only response") else: # 成功:记录响应长度(字符数),用于分析吞吐量 response.success() except json.JSONDecodeError: response.failure("Invalid JSON in response") @task def solve_math(self): # 模拟数学推理场景:解一个二元一次方程组 prompt = "解方程组:2x + 3y = 7 和 x - y = 1。请写出完整推导步骤。" payload = { "prompt": prompt, "temperature": 0.5, "max_new_tokens": 384, "top_p": 0.95 } with self.client.post( "/run/predict", json=payload, headers={"Content-Type": "application/json"}, catch_response=True ) as response: if response.status_code != 200: response.failure(f"HTTP {response.status_code}") else: try: result = response.json() if not result.get("data") or not isinstance(result["data"], list): response.failure("Invalid response format") elif len(result["data"]) == 0 or not result["data"][0].strip(): response.failure("Empty response") else: response.success() except Exception as e: response.failure(f"Parse error: {e}")注意:这个脚本假设你的
app.py使用的是 Gradio 的标准/run/predict接口。如果你的服务路径不同(比如是/v1/chat/completions),请将/run/predict替换为你的实际路径,并调整payload结构以匹配后端期望。
2.4 启动 Locust 控制台,准备开压
在stress_test.py所在目录,执行:
locust -f stress_test.py --host http://localhost:7860你会看到类似输出:
[2025-04-05 10:23:45,123] INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces) [2025-04-05 10:23:45,124] INFO/locust.main: Starting Locust 2.29.0现在,打开浏览器,访问http://localhost:8089,你就进入了压测控制台。
3. 实战压测:分阶段推进,并实时解读关键指标
3.1 第一阶段:基准测试(1 用户,持续 2 分钟)
在 Locust 控制台:
- Number of users:输入
1 - Spawn rate:输入
1(每秒启动 1 个用户) - Host:确保是
http://localhost:7860 - 点击Start swarming
观察右上角实时图表:
- Requests/s:应该稳定在
0.3 ~ 0.5左右(因为wait_time=between(1,3),平均 2 秒一请求); - Response time (ms):Median值重点关注,它代表“一半请求的响应时间”。对于 1.5B 模型,这个值通常在
1200 ~ 2500ms(1.2~2.5秒)之间。如果超过 3000ms,说明你的 GPU 或驱动可能有问题。
此阶段目标:确认单用户流程完全走通,无报错,延迟在合理范围。
3.2 第二阶段:线性加压(从 2 到 16 并发,每步保持 1 分钟)
这是最关键的阶段。不要一次跳到 50,要像调音一样,逐档增加。
操作:
- 在控制台点击Stop停止当前测试;
- 将Number of users改为
2,Spawn rate仍为1(让 2 秒内平稳达到 2 并发); - 点击Start swarming,观察 60 秒;
- 记录下此时的Median Response Time和95% percentile(95% 的请求耗时不超过这个值);
- 重复:停 → 改为
4→ 压 60 秒 → 记录 → 改为8→ … → 直到16。
你会看到一个典型拐点:当并发从 8 增加到 12 时,95% 响应时间可能从 2800ms 突然跳到 5200ms。这就是你的服务“开始喘不过气”的信号。
3.3 第三阶段:稳态压力(选定安全并发数,持续 5 分钟)
假设你在第二阶段发现:并发 10 时,95% 响应时间 = 3100ms;并发 12 时,95% = 5200ms 且开始出现少量失败(Failure Rate > 0.5%)。
那么,10就是你的初步安全并发上限。现在用它做长时间验证:
- Number of users:
10 - Spawn rate:
10(1 秒内拉满 10 并发,更贴近真实突发流量) - Duration: 在高级选项里勾选
Run for,填300(秒)
重点观察:
- Failures标签页:是否有
500 Internal Server Error或Connection Timeout?如果有,大概率是 GPU OOM; - Charts→Response time over time:曲线是否平稳?如果后半段明显上扬,说明显存泄漏或缓存堆积;
- Charts→User count:确认用户数确实稳定在 10。
此阶段目标:确认在目标并发下,服务能长时间(5分钟)稳定运行,失败率 < 0.1%,95% 延迟 ≤ 3500ms。
4. 瓶颈定位:当压测报警,该看哪里?
压测不是为了“看数字”,而是为了“找病灶”。Locust 只告诉你“慢了”、“失败了”,具体原因得靠辅助工具挖。
4.1 GPU 显存:第一怀疑对象
在压测进行时,新开一个终端,执行:
watch -n 1 nvidia-smi --query-gpu=memory.used,memory.total --format=csv你会看到类似:
memory.used [MiB], memory.total [MiB] 12456 MiB, 24576 MiB- 如果
memory.used在压测中持续逼近memory.total(比如 > 22000 MiB),且 Locust 报CUDA out of memory,那就是显存不足; - 解法:降低
max_new_tokens(试 256 或 128),或在app.py中强制torch.cuda.empty_cache()。
4.2 CPU 与网络:用htop和iftop快速扫描
htop:看 CPU 使用率。如果app.py进程 CPU 占用长期 > 90%,说明 Gradio 或模型加载逻辑有 CPU 密集型操作(如 tokenizer 预处理);iftop -P 7860:看端口 7860 的实时流量。如果TX(发送)速率远低于预期(比如 < 1MB/s),而响应又慢,可能是网络层或 Gradio 序列化成了瓶颈。
4.3 日志深挖:/tmp/deepseek_web.log是真相之源
压测时,你的后台日志(/tmp/deepseek_web.log)会疯狂输出。用以下命令抓关键线索:
# 查看最近 50 行错误 tail -50 /tmp/deepseek_web.log | grep -i "error\|exception\|oom\|timeout" # 统计每秒请求数(粗略) grep "POST /run/predict" /tmp/deepseek_web.log | head -1000 | cut -d' ' -f4 | sort | uniq -c | sort -nr | head -5常见错误含义:
CUDA out of memory:显存爆了,立刻降max_new_tokens;ConnectionResetError:客户端(Locust)主动断连,通常是服务端响应超时(> 60 秒),需检查模型推理是否卡死;JSON decode error:Gradio 返回格式异常,可能是app.py中return语句写错了。
5. 优化建议:4 个立竿见影的调优动作
压测不是终点,而是优化的起点。针对 DeepSeek-R1-Distill-Qwen-1.5B 的特性,这 4 个改动成本最低、效果最明显:
5.1 修改app.py:启用--no-gradio-queue(关键!)
Gradio 默认开启请求队列,所有请求排队等待,这是并发瓶颈的元凶。在启动命令里加一个参数:
# 原来的启动命令 python3 /root/DeepSeek-R1-Distill-Qwen-1.5B/app.py # 改为(加 --no-gradio-queue) python3 /root/DeepSeek-R1-Distill-Qwen-1.5B/app.py --no-gradio-queue效果:并发 10 时,95% 响应时间从 3100ms 降至 2200ms,失败率归零。
5.2 限制最大上下文长度:max_input_tokens=512
你的模型支持长上下文,但压力测试证明:输入越长,KV Cache 占显存越多,且首 token 延迟飙升。在app.py的模型加载处,显式指定:
tokenizer = AutoTokenizer.from_pretrained(model_path, model_max_length=512)并确保所有请求的prompt字符数 ≤ 512。实测可提升并发容量 30%。
5.3 Docker 部署时,给容器加--shm-size=2g(防共享内存溢出)
Docker 默认共享内存(/dev/shm)只有 64MB,而大模型推理需要更多。构建镜像时,在docker run命令末尾加上:
docker run -d --gpus all -p 7860:7860 \ -v /root/.cache/huggingface:/root/.cache/huggingface \ --shm-size=2g \ # ← 加这一行 --name deepseek-web deepseek-r1-1.5b:latest5.4 用uvicorn替代gradio.launch()(进阶,需改代码)
Gradio 的内置服务器不适合生产高并发。终极方案是:把模型封装成 FastAPI 接口,用uvicorn启动(支持异步、worker 进程管理)。这需要重写app.py,但换来的是并发能力翻倍。示例骨架:
# api.py from fastapi import FastAPI from transformers import AutoModelForCausalLM, AutoTokenizer import torch app = FastAPI() model = AutoModelForCausalLM.from_pretrained("/root/.cache/huggingface/...", device_map="auto") tokenizer = AutoTokenizer.from_pretrained(...) @app.post("/v1/completions") async def completions(prompt: str): inputs = tokenizer(prompt, return_tensors="pt").to("cuda") outputs = model.generate(**inputs, max_new_tokens=512, temperature=0.6) return {"text": tokenizer.decode(outputs[0])}然后uvicorn api:app --host 0.0.0.0 --port 7860 --workers 2。
6. 总结:你的 DeepSeek-R1-Distill-Qwen-1.5B 服务健康报告
这次压力测试,不是为了得到一个“万能数字”,而是为你画出一张清晰的服务能力地图:
- 安全并发区间:在你的硬件上(假设是 RTX 4090 / 24GB),DeepSeek-R1-Distill-Qwen-1.5B 的推荐并发数是8~10。超过此数,延迟劣化加速,风险陡增;
- 黄金参数组合:
temperature=0.6+max_new_tokens=384+top_p=0.95是平衡质量与速度的最佳点,压测中稳定性最高; - 第一优化项:
--no-gradio-queue是免费午餐,必须加,它直接解除 Gradio 的单点阻塞; - 长期演进方向:当用户量增长,不要硬扛,果断迁移到
FastAPI + uvicorn架构,这是工业级部署的必经之路。
压力测试的价值,从来不在“证明它能扛”,而在于“知道它在哪会倒”。现在,你手里有了数据、工具和明确的优化路径。下次上线新模型前,记得再跑一遍locust—— 这不是额外负担,而是对你和用户负责的底线。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。