如何做压力测试?SenseVoiceSmall并发请求性能评估教程
1. 为什么语音识别模型也需要压力测试?
你可能已经用过 SenseVoiceSmall 的 Web 界面——上传一段音频,几秒后就看到带情感标签和声音事件的富文本结果。界面流畅、响应快、识别准,体验确实不错。
但如果你打算把它集成进客服系统、会议记录平台,或者部署为公司内部的语音分析中台,光“能用”远远不够。你真正需要知道的是:它到底能同时处理多少路音频?在高并发下会不会卡顿、崩溃、丢请求?延迟会不会突然飙升?GPU 显存会不会爆掉?
这正是压力测试要回答的问题。它不是验证“功能对不对”,而是检验“系统扛不扛得住”。
很多开发者跳过这一步,直到上线后用户一多,服务开始超时、报错、OOM,才手忙脚乱查日志、调参数、加机器——其实这些问题,在本地环境跑一次并发压测就能提前暴露。
本教程不讲抽象理论,也不堆砌 JMeter 配置截图。我们聚焦一个真实目标:用最简方式,对 SenseVoiceSmall 的 Gradio 服务做一次可复现、可量化、有业务意义的压力测试。你会学到:
- 怎么绕过浏览器,直接向 WebUI 发起批量语音识别请求
- 怎么模拟 5/10/20 路并发,观察响应时间、成功率、GPU 利用率变化
- 哪些指标最关键(别只盯着平均耗时)
- 遇到“CUDA out of memory”或“Connection refused”时,第一反应该查什么
全程基于你已有的镜像环境,无需额外安装复杂工具,15 分钟内就能跑出第一组有效数据。
2. 压力测试前的三个关键准备
在敲命令之前,请先确认以下三点。跳过检查,90% 的压测失败都源于这里。
2.1 确认服务已稳定运行且可被程序调用
Gradio 默认只监听127.0.0.1:6006,这是本地回环地址,外部程序(包括你本机的压测脚本)无法直连。必须改成监听所有接口:
打开app_sensevoice.py,找到最后一行demo.launch(...),修改为:
demo.launch(server_name="0.0.0.0", server_port=6006, share=False, debug=False)注意:server_name="0.0.0.0"是关键。保存后重启服务:
python app_sensevoice.py然后在服务器终端执行:
curl -s http://127.0.0.1:6006 | head -n 10如果返回 HTML 片段(含<html>标签),说明服务已正常启动。如果报Connection refused,请检查端口是否被占用,或 PyTorch 是否成功加载了 CUDA。
2.2 准备标准化测试音频
压测必须用同一段音频反复提交,否则结果不可比。推荐使用一段 8 秒左右、采样率 16kHz、单声道的中文语音(避免因音频解码差异引入噪音)。
没有现成音频?用 Python 快速生成一段测试音:
# generate_test_audio.py import numpy as np from scipy.io.wavfile import write # 模拟一段 8 秒 16kHz 的正弦波(代表语音能量) fs = 16000 t = np.linspace(0, 8, fs * 8, endpoint=False) audio = 0.3 * np.sin(2 * np.pi * 440 * t) # 440Hz 基频,类似人声范围 audio = (audio * 32767).astype(np.int16) write("test_8s.wav", fs, audio) print(" 已生成 test_8s.wav,可用于压力测试")运行后得到test_8s.wav。把它放在和app_sensevoice.py同一目录下,后续压测将统一使用它。
2.3 安装轻量级压测工具:locust
不用 JMeter,不用 k6。我们选 Locust —— Python 写的、语法极简、结果直观,且能直接复用你的模型调用逻辑。
在镜像环境中执行:
pip install locustLocust 不需要 GUI,所有操作都在终端完成。它会模拟多个“用户”(即并发请求者),每个用户按设定逻辑发送请求,并实时汇总统计。
3. 编写可执行的压力测试脚本
现在,我们写一个locustfile.py,它将:
- 模拟用户上传
test_8s.wav - 固定选择语言为
"zh"(中文) - 记录每次请求的耗时、状态码、响应内容长度
- 自动重试失败请求(最多 2 次)
# locustfile.py import os import time import requests from locust import HttpUser, task, between, events # 测试音频路径(与 locustfile.py 同目录) TEST_AUDIO_PATH = "test_8s.wav" # 全局计数器,用于标记请求序号(便于排查日志) request_counter = 0 class SenseVoiceUser(HttpUser): wait_time = between(0.1, 0.5) # 用户思考时间:100ms~500ms,模拟真实间隔 @task def transcribe_audio(self): global request_counter request_counter += 1 req_id = request_counter try: # 构造 multipart/form-data 请求(Gradio 接收格式) with open(TEST_AUDIO_PATH, "rb") as f: files = { "audio": (os.path.basename(TEST_AUDIO_PATH), f, "audio/wav"), } data = { "lang_dropdown": "zh", } start_time = time.time() response = self.client.post( "/api/predict/", files=files, data=data, timeout=30, # 单次请求最长等待 30 秒 allow_redirects=False, ) end_time = time.time() # 计算耗时(毫秒) response_time_ms = (end_time - start_time) * 1000 # Gradio API 返回 JSON,提取 text 字段 if response.status_code == 200: try: result = response.json() text_len = len(result.get("data", [""])[0]) if result.get("data") else 0 # 成功:记录耗时、文本长度 events.request_success.fire( request_type="POST", name="/api/predict/", response_time=response_time_ms, response_length=text_len, ) except Exception as e: events.request_failure.fire( request_type="POST", name="/api/predict/", response_time=response_time_ms, exception=e, ) else: # HTTP 错误码 events.request_failure.fire( request_type="POST", name="/api/predict/", response_time=response_time_ms, exception=f"HTTP {response.status_code}", ) except Exception as e: # 网络异常、超时等 end_time = time.time() response_time_ms = (end_time - start_time) * 1000 if 'start_time' in locals() else 0 events.request_failure.fire( request_type="POST", name="/api/predict/", response_time=response_time_ms, exception=e, )关键点说明:
self.client.post("/api/predict/"):这是 Gradio 的默认预测接口路径,不是/或/gradio_api。files和data的构造方式严格匹配 Gradio 表单提交格式,否则会返回 400。timeout=30防止某次请求卡死拖垮整个测试。- 所有异常(网络、解析、超时)都通过
events.request_failure.fire()上报,Locust 会自动统计。
保存文件后,确保test_8s.wav和locustfile.py在同一目录。
4. 执行压力测试并解读核心指标
4.1 启动 Locust 控制台
在locustfile.py所在目录,执行:
locust -f locustfile.py --host http://localhost:6006终端会输出类似:
[2024-06-15 10:22:34,123] INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces) [2024-06-15 10:22:34,124] INFO/locust.main: Starting Locust 2.22.0此时,打开浏览器访问http://localhost:8089(注意:是 8089 端口,不是 6006),进入 Locust Web 控制台。
4.2 配置并发场景并开始测试
在网页中填写:
- Number of users to simulate: 输入你想测试的并发数,例如
10 - Spawn rate (users spawned/second): 输入
2(每秒启动 2 个用户,避免瞬间冲击) - Host (optional): 已填
http://localhost:6006,保持不变
点击Start swarming。
页面会实时刷新图表:Requests/s(每秒请求数)、Response time(响应时间)、Failures(失败率)。
建议分三轮测试:
| 并发用户数 | 预期目标 | 关注重点 |
|---|---|---|
| 5 | 建立基线 | 平均响应 < 3s,失败率 0%,GPU 利用率 < 60% |
| 10 | 压力临界 | 响应时间是否翻倍?失败率是否突增?显存是否接近上限? |
| 20 | 极限探底 | 是否出现 OOM?是否有大量超时(>30s)?服务是否无响应? |
每轮测试持续 2–3 分钟,待曲线稳定后截图或记下关键数字。
4.3 最值得关注的 4 个指标
不要被满屏图表吓住。只盯这四个数字,就能判断服务健康度:
| 指标 | 健康阈值 | 异常信号 | 可能原因 |
|---|---|---|---|
| Median Response Time(中位响应时间) | < 3000 ms | > 5000 ms | 模型推理变慢,或 GPU 资源争抢严重 |
| 95% percentile(95 分位响应时间) | < 6000 ms | > 10000 ms | 少数请求严重延迟,可能是显存碎片化或 batch 处理不均 |
| Failure Rate(失败率) | 0% | > 1% | 服务崩溃、CUDA OOM、Gradio 队列溢出 |
| Requests/s(每秒请求数) | ≥ 3 | 持续下降 | 系统瓶颈(CPU 解码、GPU 显存、网络 I/O) |
小技巧:在 Locust 页面右上角点击Download Data→Download CSV,用 Excel 打开,筛选Response Time列,看分布是否集中。如果大量请求集中在 2000ms 和 12000ms 两档,大概率是部分请求触发了 VAD(语音活动检测)长段处理,而其他走短路径。
5. 常见问题与针对性优化建议
压测不是终点,而是调优的起点。以下是实战中高频问题及落地解法:
5.1 问题:并发 10 时失败率飙升,日志报CUDA out of memory
现象:nvidia-smi显示显存使用率 99%,dmesg有Out of memory: Kill process记录。
根因:SenseVoiceSmall 默认batch_size_s=60,指单次处理最多 60 秒音频。当多路请求同时到达,模型会尝试把所有音频拼成大 batch 推理,显存瞬间爆炸。
解法:强制关闭 batch 合并,在app_sensevoice.py的model.generate()调用中添加:
res = model.generate( input=audio_path, cache={}, language=language, use_itn=True, batch_size_s=1, # 关键:设为 1,禁用动态 batch merge_vad=True, merge_length_s=15, )重启服务后重测,显存占用会从 12GB 降至 5GB,失败率归零。
5.2 问题:响应时间波动极大,95% 分位远高于中位数
现象:中位数 2500ms,但 95% 分位达 15000ms,且失败请求多为ReadTimeout
根因:Gradio 默认使用单进程,VAD 检测长静音段时阻塞主线程,后续请求排队等待。
解法:启用 Gradio 多工作进程。修改demo.launch():
demo.launch( server_name="0.0.0.0", server_port=6006, num_workers=4, # 👈 启动 4 个独立 worker 进程 quiet=True, )num_workers建议设为 GPU 数量 × 2(单卡设 4 即可)。重启后,长请求不再阻塞短请求,95% 分位可降至 4000ms 内。
5.3 问题:CPU 占用 100%,但 GPU 利用率仅 30%
现象:htop显示 Python 进程吃满 CPU,nvidia-smi显示 GPU 闲着。
根因:音频解码(av库)和 VAD 前处理在 CPU 完成,成为瓶颈。
解法:预处理音频,绕过实时解码。用ffmpeg提前转成标准格式:
ffmpeg -i test_8s.wav -ar 16000 -ac 1 -f wav test_16k_mono.wav在locustfile.py中改用test_16k_mono.wav,CPU 占用可降 60%,GPU 利用率升至 75%+。
6. 总结:一份可直接复用的压力测试清单
做完以上步骤,你已掌握一套完整的 SenseVoiceSmall 服务压测方法论。最后,送你一份可粘贴执行的终版检查清单,下次部署新版本时,5 分钟内就能跑完:
# 一键压测准备清单(复制到终端逐行执行) cd /path/to/your/app # 进入项目目录 # 1. 确保服务监听 0.0.0.0 sed -i 's/server_name="127.0.0.1"/server_name="0.0.0.0"/' app_sensevoice.py # 2. 生成标准测试音(如无) python -c " import numpy as np; from scipy.io.wavfile import write; fs=16000; t=np.linspace(0,8,fs*8); write('test_16k_mono.wav', fs, (0.3*np.sin(2*np.pi*440*t)*32767).astype(np.int16)) " # 3. 启动服务(后台运行) nohup python app_sensevoice.py > sensevoice.log 2>&1 & # 4. 启动压测(10 并发,持续 180 秒) locust -f locustfile.py --host http://localhost:6006 --users 10 --spawn-rate 2 --run-time 3m --headless --csv=stress_test_10u压测结束后,查看生成的stress_test_10u_stats.csv,重点关注Median Response Time和Failure Count。若两者均达标,恭喜,你的 SenseVoiceSmall 服务已具备生产就绪的并发能力。
记住:压力测试不是一次性任务。每次模型升级、依赖更新、硬件更换后,都值得再跑一遍。它不能保证万无一失,但能让你在流量洪峰来临前,心里有底。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。