FunASR CPU 性能基准实战:Libtorch 推理、RTF 测试与 CER 评估全解析
【免费下载链接】FunASROpen-source speech recognition toolkit for training, inference, streaming ASR, VAD, punctuation, speaker diarization pipelines, and OpenAI-compatible/MCP serving.项目地址: https://gitcode.com/GitHub_Trending/fun/FunASR
本文围绕 runtime/docs/benchmark_libtorch.md 这一官方基准文档展开,系统讲解如何基于 Libtorch(PyTorch TorchScript)推理后端对 FunASR 的 Paraformer 离线识别模型做 CPU 性能基准测试:包括基准环境搭建、模型导出、并发 RTF(实时率)测量与 CER(字错误率)评测的完整流程。读完后,你可以直接复现文档中的测试配方(recipe),理解 RTF 与加速比的统计口径,并结合 runtime/python/libtorch 运行时源码弄清 Libtorch 推理链路的底层实现。
基准测试的总体配置
基准文档给出的测试配置如下:
- 测试数据:Aishell1 test set,总语音时长36108.919 秒(约 10 小时)。
- 测试模型:Paraformer-large(即模型仓库中的
damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch),这是 16kHz 中文离线识别的旗舰模型。 - 测试硬件:Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz,16 core - 32 processor,支持avx512_vnni指令集(这是 int8 量化加速的前提之一)。
- 测试指标:
processing time(s):整份测试集(或单个并发任务)的处理耗时;RTF(Real-Time Factor):计算时间 / 语音时长,越小越快;Speedup Rate(加速比):1 / RTF,即单位墙钟时间内能处理多少倍语时的音频。
整个基准由两份 shell 配方驱动:test_rtf.sh测速度,test_cer.sh测精度,两者共享同一套模型导出逻辑与 scp 切分工具,全部位于 runtime/python/utils 目录下。
环境安装与依赖准备
安装 ModelScope 与 FunASR
基准的第一步是安装推理框架本身:
pip install -U modelscope funasr # 国内用户也可以使用镜像源: # pip install -U funasr -i https://mirror.sjtu.edu.cn/pypi/web/simple安装基准工具依赖
官方配方要求克隆仓库后,在基准工具目录安装额外依赖:
git clone <仓库地址> && cd FunASR cd funasr/runtime/python/utils pip install -r requirements.txt对应的 requirements.txt 内容很短,共 5 个包,每个都有明确用途:
| 依赖 | 作用 |
|---|---|
onnx | 模型导出为 ONNX 格式时使用(--type onnx) |
onnxruntime | ONNX 后端推理(本基准配方中backend="onnx"的默认路径) |
torch-quant >= 0.4.0 | TorchScript 模型量化,即 RTF 表格中 “torch int8” 一行的来源 |
funasr_torch | Libtorch 运行时包,封装 TorchScript 模型的加载与推理 |
funasr_onnx | ONNX 运行时包,封装 ONNX 模型的加载与推理 |
如果不想从 PyPI 安装funasr_torch,也可以从仓库源码安装,见 runtime/python/libtorch/README.md:
cd funasr/runtime/python/libtorch pip install -e ./需要注意两个前置条件,配方脚本对它们有隐含依赖:
- Perl:
split_scp.pl是 Perl 脚本,Linux 上一般默认自带; taskset:RTF 配方用taskset -c <core_id>把每个推理进程绑核,需要 Linux 环境。
模型导出(两条配方的共同 Stage 0)
两份配方在stage=0时都会先调用 FunASR 的导出模块把模型转换为运行时可用的形式:
python -m funasr.export.export_model \ --model-name ${model_name} \ --export-dir ${export_root} \ --type ${backend} \ --quantize ${quantize} \ --audio_in ${scp}关键参数说明(以 test_rtf.sh 为准):
--model-name:模型标识,默认值damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch,支持直接传 ModelScope 模型名,也支持本地路径;--export-dir:导出根目录,配方中为${export_root},导出产物位于${export_root}/${model_name};--type:torch(导出 TorchScript,即 Libtorch 路径)或onnx;--quantize:true时额外产出量化模型(Libtorch 路径下为model_quant.torchscript,由torch-quant完成 int8 量化);test_cer.sh还多一个--fallback-num参数(默认 20),用于 ONNX 导出时指定回退到 Torch 算子执行的算子数,这解释了配方中fallback_op_num_torch变量的作用。
RTF 测试配方:test_rtf.sh 全解
配方参数
test_rtf.sh 的头部定义了全部可调项:
nj=32 # 并发任务数 stage=0 # 从哪个 stage 开始执行 scp="/path/to/aishell-1/data/test/wav.scp" # 测试集 wav.scp export_root="/path/to/export" # 模型导出根目录 split_scps_tool=split_scp.pl # scp 切分工具 rtf_tool=test_rtf.py # 单任务 RTF 测量脚本 model_name="damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch" backend="onnx" # "torch" # torch 即 Libtorch 路径 quantize='true' # 'False' tag=${model_name}/${backend}_quantize_${quantize} logs_outputs_dir=${export_root}/logs/${tag}/split$nj使用时只需把scp、export_root、model_name、backend、quantize改成自己的值,然后按文档启动:
nohup bash test_rtf.sh &> log.txt &tag会拼出${backend}_quantize_${quantize},因此 torch/fp32、torch/int8、onnx/fp32、onnx/int8 四组实验的日志与导出目录天然隔离,互不覆盖。
Stage 1:切分、并发执行与结果汇总
stage=0完成导出后,stage=1的完整流程是:
切分:
perl split_scp.pl $scp wav.1.scp wav.2.scp ... wav.$nj.scp,把整份 wav.scp 均分为nj份。split_scp.pl 是经典的 Kaldi 风格脚本,无--utt2spk参数时按行数尽量均匀切分,并保证每个分片至少一行;绑核并发:对每个
JOB,执行core_id=$(expr $JOB - 1) taskset -c ${core_id} python test_rtf.py \ --backend ${backend} \ --model_dir ${model_dir} \ --wav_file ${logs_outputs_dir}/wav.${JOB}.scp \ --quantize ${quantize} &> ${logs_outputs_dir}/log.${JOB}.txt &每个进程独占一个核(
taskset -c绑定到JOB-1号核),wait等待全部完成。这一步模拟了“单机多路并发推理”的真实服务场景——每路独立加载一份模型、各自处理自己那份音频;汇总统计:脚本从每个
log.$JOB.txt中 grep 出三个量并归并:# 每路任务计算耗时(最长一路) total_time_comput = max(各路 total_time_comput) # 全部音频时长之和 total_time_wav = sum(各路 total_time_wav) rtf = total_time_comput / total_time_wav speed = 1 / rtf最终打印:
total_time_comput_ms: ... total_time_wav: ... total_rtf: ..., speech: ...这里有一个重要的统计口径:RTF 用的是“最慢一路的墙钟时间 / 全部音频总时长”,而不是各路 RTF 的简单平均。这正是并发场景下的正确口径——所有任务同时结束的那一刻才代表系统把整份数据“吐完”了。
单任务脚本 test_rtf.py 的实现细节
test_rtf.py 的逻辑与基准结果的可复现性直接相关,值得逐点说明:
model = Paraformer( args.model_dir, batch_size=1, quantize=args.quantize, intra_op_num_threads=args.intra_op_num_threads, )- 当
backend == "torch"时导入的是 funasr.runtime.python.libtorch.funasr_torch 中的Paraformer类(Libtorch 运行时);backend == "onnx"时导入 ONNX 运行时的同名类。两者接口一致,因此同一份脚本可横向对比两种后端; - 先做30 次 warm-up(反复推理第一条音频并打印平均耗时与 RTF),剔除冷启动对计时的污染;
- 随后对分配到的整个 scp 分片逐条推理,用
time.time()前后差值记录总计算时间(毫秒); - 再用
librosa逐条读取音频、按len(waveform)/16.0累加真实语音时长(注意这里按采样数直接计算,与数据集标注的 36108.919 秒口径一致); - 最终输出
total_time_comput_ms、total_time_wav_ms与total_rtf(5 位小数),这三个字段就是 shell 脚本 grep 的目标。
--intra_op_num_threads(默认 1)是 ONNX Runtime 的线程参数;对 torch 后端无实际作用。由于每个进程已被taskset限制在单核,这里默认 1 线程与绑核策略是自洽的。
CER 测试配方:test_cer.sh 全解
test_cer.sh 与 RTF 配方结构同源,但增加了精度评测链路,由stage/stop_stage控制执行区间:
| stage | 工作内容 |
|---|---|
| 0 | 模型导出(含--fallback-num参数,见上文) |
| 1 | scp 切分 +nj路绑核并发推理 + 结果合并为1best_recog/ |
| 2 | 文本规整 + WER/CER 计算,tail -n 3打印结果 |
Stage 1 中,每路任务运行:
taskset -c ${core_id} python test_cer.py \ --backend ${backend} \ --model_dir ${model_dir} \ --wav_file ${output_dir}/wav.${JOB}.scp \ --quantize ${quantize} \ --output_dir ${output_dir}/${JOB} &> ${output_dir}/log.${JOB}.txttest_cer.py 对分片内每条音频调用同一运行时(batch_size=1),把utt_id 识别文本与utt_id 词元序列分别写入text与token文件。全部任务完成后,脚本把nj份text/token按 utt_id 排序合并到1best_recog/,保证评测集完整。
Stage 2 的两步处理:
python proce_text.py ${output_dir}/1best_recog/text ${output_dir}/1best_recog/text.proc python proce_text.py ${label_text} ${output_dir}/1best_recog/text.ref python compute_wer.py ${output_dir}/1best_recog/text.ref ${output_dir}/1best_recog/text.proc ${output_dir}/1best_recog/text.cer tail -n 3 ${output_dir}/1best_recog/text.cerproce_text.py 做评测前规整:去除
<s>/</s>/@@等特殊符号、去空格、转小写,并把中文按字拆开([x for x in text]),使后续编辑距离以“字”为粒度——所以中文场景下算出的就是 CER;compute_wer.py 用动态规划编辑距离逐句统计
ins/del/sub,汇总输出标准 Kaldi 风格结果:%WER <Err> [ <wrong_words> / <Wrd>, <Ins> ins, <Del> del, <Sub> sub ] %SER <S.Err> [ <wrong_sentences> / <Snt> ] Scored <N> sentences, <M> not present in hyp.每句还会写入
text.cer明细文件(含 ref/hyp 对照),便于人工定位错例。Err = wrong_words * 100 / Wrd,即以参考字数为分母的字错误率。
这套流程的价值在于:换后端(torch vs onnx)、换精度(fp32 vs int8)后,CER 应当基本不变——它是量化与部署是否“精度无损”的验收标准,与 RTF 数据配合构成完整的部署前评估闭环。
官方基准结果与口径解读
基准文档在Intel(R) Xeon(R) Platinum 8269CY @ 2.50GHz(16core-32processor,avx512_vnni)上测得 Paraformer-large 的 RTF 结果如下(Aishell1 test set,36108.919 秒):
| 并发任务数 | 处理耗时 (s) | RTF | 加速比 |
|---|---|---|---|
| 1 (torch fp32) | 3522 | 0.0976 | 10.3 |
| 1 (torch int8) | 1746 | 0.0484 | 20.7 |
| 32 (torch fp32) | 236 | 0.0066 | 152.7 |
| 32 (torch int8) | 114 | 0.0032 | 317.4 |
| 64 (torch fp32) | 235 | 0.0065 | 153.7 |
| 64 (torch int8) | 113 | 0.0031 | 319.2 |
从这组数据可以读出三个结论:
- int8 量化收益接近 2 倍:单任务下 RTF 从 0.0976 降到 0.0484,几乎精确减半;并发场景下同样约 2 倍。这对应导出链路的
quantize=true→model_quant.torchscript,依赖torch-quant完成; - 并发扩展非常线性:1 路 3522s → 32 路 236s,单核 32 路并发约等效 150 倍吞吐(相对单路约 15 倍,考虑到 8269CY 有 32 逻辑核,接近理想扩展);
- 32 路与 64 路吞吐趋于饱和:64 路(235s/113s)并未比 32 路更快,说明在 32 逻辑核的机器上并发数超过核数后收益消失,
nj建议与可用核数对齐。
Libtorch 运行时的 README 还给出了一段单条 5.53s 音频、100 次平均的跨后端对比(Intel Xeon Platinum 8163 @ 2.50GHz):
| 后端 | RTF (FP32) |
|---|---|
| PyTorch(训练态框架) | 0.110 |
| Libtorch(TorchScript) | 0.048 |
| ONNX Runtime | 0.038 |
可以看出:同样的模型权重,从 eager PyTorch 换到 TorchScript 推理,RTF 降到约 0.44 倍;再换 ONNX 又略降。三者中 Libtorch 处于训练框架与 ONNX 之间,优势在于与 PyTorch 生态(包括torch-quant量化)无缝衔接,且量化后 RTF 可进一步减半,这也是基准文档标题直接以 Libtorch 命名的原因。
Libtorch 运行时的源码级实现
基准配方最终调用的是 runtime/python/libtorch/funasr_torch/paraformer_bin.py 中的Paraformer类,结合源码可以弄清几个关键行为:
1. 模型加载与自动导出。构造函数中(paraformer_bin.py#L53-L66):
model_file = os.path.join(model_dir, "model.torchscript") if quantize: model_file = os.path.join(model_dir, "model_quant.torchscript") if not os.path.exists(model_file): # 自动调用 AutoModel(...).export(type="torchscript", quantize=quantize)即:quantize=True时加载model_quant.torchscript,否则加载model.torchscript;若目录里还没有 TorchScript 产物,会自动用funasr.AutoModel完成导出。若model_dir不是一个存在的本地路径,还会先尝试modelscope.hub.snapshot_download自动下载(paraformer_bin.py#L37-L51)。所以test_rtf.py里直接传model_dir就能工作——目录里必须有model.torchscript(或model_quant.torchscript)、config.yaml、am.mvn、tokens.json四类文件,这与 README 对导出产物的描述一致。
2. 特征提取链路。extract_feat(paraformer_bin.py#L188-L200)走WavFrontend.fbank→lfr_cmvn:Fbank 特征加上 LFR(低帧率融合)与 CMVN(均值方差归一化,参数来自am.mvn),再 pad 成float32张量与int32长度张量,喂给torch.jit.load载入的脚本模型——输入输出就是feats, feats_len → am_scores, valid_token_lens。
3. 解码与时间戳。非自回归解码由decode(am_scores, valid_token_lens)一步完成(Paraformer 本身是平行解码结构,这是其快于自回归 Transformer 的根本原因);若导出模型额外返回us_alphas/us_peaks(BiCif 分支输出),运行时还会调用time_stamp_lfr6_onnx生成字级时间戳并做句级后处理,最终返回{"preds", "timestamp", "raw_tokens"}结构(paraformer_bin.py#L91-L143)。
4. 输入形态。__call__接受str路径、np.ndarray波形或List[str]三种输入,内部统一经librosa.load重采样到 16kHz 后按batch_size分批推理;device_id为-1(默认)时走 CPU,正数索引时把特征搬到 CUDA——同一份代码同时覆盖 CPU 基准与 GPU 部署。
复现基准的注意事项与前提
基于文档与源码,复现时需要注意以下前提与限制:
- 路径必须替换:
test_rtf.sh/test_cer.sh中scp、label_text、export_root指向作者内网的 NFS 路径(如/nfs/...),复现前必须替换为本地的 Aishell1 test set 路径(wav.scp 与 text 标签文件); - 并发数与核数匹配:
nj路推理通过taskset绑到0..nj-1号核,nj不要超过物理核数,否则 64 路数据所示的饱和现象会拖慢总耗时; - RTF 口径:总 RTF = max(各路耗时) / Σ(各路语音时长),与单条音频的 RTF 定义不同,横向对比时保持
nj一致; - 量化依赖 avx512_vnni:表格中 int8 收益依赖 CPU 支持 avx512_vnni;在不支持的 CPU 上量化未必有 2 倍收益,建议 fp32/int8 都测一轮;
- CER 与 RTF 是两条独立结论:RTF 回答“能跑多快”,CER(stage 2 的
%WER输出)回答“量化/换后端后精度是否保持”,部署决策需要两者同时满足。
小结
runtime/docs/benchmark_libtorch.md定义的这套基准,用 Aishell1 整段测试集、split_scp.pl切分 +taskset绑核并发的配方,把“模型导出 → 并发推理计时 → RTF/加速比统计 → 文本合并 → 字级编辑距离评测”串成了可直接执行的 shell 流水线,工具链集中在 runtime/python/utils(test_rtf.py、test_cer.py、compute_wer.py、proce_text.py、split_scp.pl),推理核心则位于 runtime/python/libtorch 的funasr_torch包。官方数据显示:在带 avx512_vnni 的 Xeon 8269CY 上,Paraformer-large 经 Libtorch + int8 量化后,32 路并发下整段 Aishell1 test set 的处理 RTF 达到 0.0032(加速比约 317 倍),且并发扩展到核数附近仍保持近似线性——这套配方与结论,正是 CPU 端 ASR 服务容量规划与后端选型(fp32/int8、torch/onnx)的直接依据。
【免费下载链接】FunASROpen-source speech recognition toolkit for training, inference, streaming ASR, VAD, punctuation, speaker diarization pipelines, and OpenAI-compatible/MCP serving.项目地址: https://gitcode.com/GitHub_Trending/fun/FunASR
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考