FunASR 流式 Paraformer Triton GPU 部署实战:从模型仓库到实时 ASR 服务的全链路解析
【免费下载链接】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
本篇基于 FunASR 仓库中 runtime/triton_gpu/README_paraformer_online.md 的部署指南,结合 model_repo_paraformer_large_online 中五个子模型的完整配置与 Python 后端实现,讲解如何把流式 Paraformer(Paraformer-Large Online)以 ONNX + Triton Inference Server 的形式部署为 GPU 实时中文 ASR 服务。读完后你将掌握:模型仓库的准备与目录结构、Docker 镜像构建与服务启动参数、feature_extractor → lfr_cmvn_pe → encoder → cif_search/decoder 四级 ensemble 流水线的每一级输入输出契约与状态管理机制,以及单卡 A10 上的吞吐/时延基准结果与调优依据。
一、部署总览:一个 Ensemble 编排四个子模型
流式 Paraformer 在 Triton 中不是一个单一模型,而是由 streaming_paraformer/config.pbtxt 定义的ensemble平台模型。它对外只暴露WAV(FP32,[-1])和WAV_LENS(INT32,[1])两个输入、TRANSCRIPTS(STRING)一个输出,内部按ensemble_scheduling定义的顺序串联四个子模型:
WAV → feature_extractor → lfr_cmvn_pe → encoder → cif_search → (异步子调用 decoder) → TRANSCRIPTS各子模型的设备与后端分配如下(均来自各子目录下的config.pbtxt):
| 子模型 | backend | 设备 | 实例数 | 核心职责 |
|---|---|---|---|---|
| feature_extractor | python | GPU | 1 | fbank 提帧 + LFR 左上下文补齐,按 0.6s chunk 切分 |
| lfr_cmvn_pe | onnxruntime | GPU | 1 | LFR(m=7, n=6) 帧叠叠、CMVN 归一化、位置编码,跨 chunk 缓存 |
| encoder | onnxruntime | GPU | 1 | 流式 SANM encoder,输出enc特征与 CIF 置信度alphas |
| cif_search | python | CPU | 6 | CIF 累积积分找 token 边界,异步调用 decoder 出文本 |
| decoder | onnxruntime | GPU | 1 | 自回归解码 16 层 KV cache,输出sample_ids |
这种编排的核心价值在于:把计算密集的 encoder/decoder 放在 GPU ONNX Runtime 上,把逐帧循环的 CIF 搜索放在 CPU(6 个实例并行),并让四级共享同一套 sequence batching 会话,从而支持多路并发流式会话。
二、步骤 1:准备模型仓库
按原文档步骤,准备工作分三步:
- 从 ModelScope 模型仓库克隆流式版模型(模型 ID:
damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx),获得encoder/1/model.onnx与decoder/1/decoder.onnx等 ONNX 权重。 - 转换 LFR 前端模块:在
lfr_cmvn_pe目录下运行python export_lfr_cmvn_pe_onnx.py生成lfr_cmvn_pe.onnx。 - 最终
${MODEL_DIR}下应得到如下目录树(与仓库中已提供的骨架一致):
├── README.md └── model_repo_paraformer_large_online ├── cif_search │ ├── 1 │ │ └── model.py │ └── config.pbtxt ├── decoder │ ├── 1 │ │ └── decoder.onnx │ └── config.pbtxt ├── encoder │ ├── 1 │ │ └── model.onnx │ └── config.pbtxt ├── feature_extractor │ ├── 1 │ │ └── model.py │ ├── config.pbtxt │ └── config.yaml ├── lfr_cmvn_pe │ ├── 1 │ │ └── lfr_cmvn_pe.onnx │ ├── am.mvn │ ├── config.pbtxt │ └── export_lfr_cmvn_pe_onnx.py └── streaming_paraformer ├── 1 └── config.pbtxt其中lfr_cmvn_pe/1/lfr_cmvn_pe.onnx与两个大模型 ONNX 需要你自己准备(仓库提供骨架与导出脚本),streaming_paraformer/1目录可留空(ensemble 模型无需模型文件)。
2.1 lfr_cmvn_pe ONNX 导出的实现细节
export_lfr_cmvn_pe_onnx.py 定义了LFR_CMVN_PE模块(L10-L71),它是 LFR + CMVN + 位置编码三合一的前端算子:
- LFR:
unfold(1, m=7, step=n=6)把相邻 7 帧 fbank 横向拼成80 × 7 = 560维特征,即encoder_input_size;subsample = (m-1)//2 = 3帧作为左上下文。 - CMVN:从 am.mvn 用
load_cmvn(L74-L99)解析出mean/istd两个 buffer,执行x = (x + mean) * istd。 - PE(位置编码):预计算
max_len=5000个位置的 sin/cos 编码,按offset + arange索引后加到特征上,offset随 chunk 累加,保证跨 chunk 的绝对位置正确。 - 状态输出:forward(L48-L71)返回
(r_x, r_x_len, r_cache, r_offset),其中r_cache是保留给下一 chunk 做左上下文的 10 帧特征([10, 560]),r_offset是累计帧偏移。
导出时以opset_version=11、dynamic_axes对 batch 维开放(L129-L140),输入输出签名["chunk_xs", "cache", "offset"] → ["chunk_xs_out", "chunk_xs_out_len", "r_cache", "r_offset"]与 lfr_cmvn_pe/config.pbtxt 中的 input/state 声明一一对应。
三、步骤 2:构建镜像并启动 Triton Server
3.1 构建与运行容器
原文档给出的完整命令:
# using docker image Dockerfile/Dockerfile.server docker build . -f Dockerfile/Dockerfile.server -t triton-paraformer:23.01 docker run -it --rm --name "paraformer_triton_server" --gpus all -v <path_host/model_repo_paraformer_large_online>:/workspace/ --shm-size 1g --net host triton-paraformer:23.01 # launch the service cd /workspace tritonserver --model-repository model_repo_paraformer_large_online \ --pinned-memory-pool-byte-size=512000000 \ --cuda-memory-pool-byte-size=0:1024000000关键点说明:
--model-repository指向宿主机挂载的model_repo_paraformer_large_online目录;--pinned-memory-pool-byte-size=512000000分配约 512MB 锁页内存池,用于 CPU↔GPU 的高速拷贝(Python 后端与 ONNX 后端之间传张量);--cuda-memory-pool-byte-size=0:1024000000为 0 号 GPU 预留 1GB 显存池供 ONNX Runtime 的 CUDA 工作区复用,避免反复分配;--shm-size 1g与--net host分别支撑容器共享内存通信与 gRPC 客户端直连宿主机网络(客户端默认连localhost:10086,见 client/client.py 的--url默认值)。
Dockerfile/Dockerfile.server 基于nvcr.io/nvidia/tritonserver:23.01-py3官方镜像,额外安装torch==2.4.1、pyyaml以及适配 CUDA 12.1 / torch 2.4.1 的kaldifeat预编译 wheel(供 feature_extractor 做 fbank),以及客户端依赖soundfile、grpcio-tools、tritonclient。Dockerfile 中还设置了zh_CN.UTF-8语言环境,避免中文文本在容器内出现编码问题。
四、逐级解析流水线实现(源码级证据)
4.1 feature_extractor:0.6s chunk 的会话式提帧
feature_extractor/config.pbtxt 声明了两个参数:chunk_size_s = "0.6"与config_path指向 feature_extractor/config.yaml(即训练用 paraformer_s2 LFR6 配置的快照,内含frontend_conf与token_list)。输出speech的形状固定为[61, 80]:80 是 fbank 维数,61 是每个 chunk 产出的解码窗口帧数。
Python 后端 feature_extractor/1/model.py 的实现逻辑:
initialize(L78-L149)从config.yaml读取frontend_conf的window/n_mels/frame_shift/frame_length/fs构造kaldifeat.FbankOptions;算出chunk_size = 0.6s × 16000采样、frame_stride = (0.6×1000)//10 = 60帧,offset_ms = 15ms(frame_length 25ms − frame_shift 10ms)。execute(L151-L213)处理每个请求:不足一个 chunk 的尾包会零填充到chunk_size;用kaldifeat批量提 fbank;Feat.add_frames(L58-L65)在序列首帧前重复垫入(lfr_m-1)//2 = 3帧左上下文;get_frames(61)取出 61 帧窗口并推进 60 帧——这正是流式场景下"60 帧新帧 + 1 帧重叠"的滑窗切分。- 会话状态用
LimitedDict(1024)按CORRID维护,最多缓存 1024 路并发会话,收到END即释放(L211-L212)。
四级模型全部启用 sequence batching,并通过START/READY/CORRID/END四个控制输入管理会话生命周期(见 feature_extractor/config.pbtxt L40-L77):max_sequence_idle_microseconds: 15000000(15s 无活动回收会话)、preferred_batch_size: [32, 64, 128](凑批策略)、max_queue_delay_microseconds: 300(最多等 300μs 凑批,把排队时延压到最低)。
4.2 lfr_cmvn_pe:带状态的 ONNX 前端
lfr_cmvn_pe/config.pbtxt 声明固定输入chunk_xs [61, 80]与两个序列状态:
cache → r_cache:FP32[10, 560],初值为零,保存上一 chunk 末尾 10 个 LFR 帧,作为本 chunk 的左上下文(对应导出脚本中r_x = cat((cache, r_cache), dim=1));offset → r_offset:INT32[1],累计帧偏移,供位置编码索引使用。
输出chunk_xs_out [-1, 560]与chunk_xs_out_len:61 帧 fbank 经 m=7/n=6 的 unfold 后产出约 10 帧 560 维特征,正好与 encoder 的 chunk 长度对齐。
4.3 encoder:流式 SANM + CIF 双输出
encoder/config.pbtxt 中 ONNX 图输入为speech [-1, 560]与speech_lengths,输出三个张量:
enc [-1, 512]:编码器隐状态(512 维);alphas [-1]:CIF(Continuous Integrate-and-Fire)的累积置信度,是后续 token 边界搜索的依据;enc_len:有效帧数。
配置里cudnn_conv_algo_search = "2"(EXHAUSTIVE)开启 cuDNN 卷积算子穷举搜索,用一次性构建开销换取推理期的算法最优。
4.4 cif_search:CPU 上的 token 搜索与 decoder 异步子调用
cif_search/1/model.py 是全仓库中最能体现流式 Paraformer 解码机制的部分:
CIFSearch.__init__(L24-L36)定义了三个核心超参:chunk_size = [5, 10, 5](左上下文 5 帧、本 chunk 10 帧、右上下文 5 帧,与 lfr_cmvn_pe 每 chunk 约 10 帧的输出对齐)、tail_threshold = 0.45(流结束时对残留积分的收尾阈值)、cif_threshold = 1.0(每累积满 1.0 置信度"发射"一个 token)。infer(L38-L104)先屏蔽首尾各 5 帧的alphas,拼接上一 chunk 缓存的cif_hidden/cif_alphas,逐帧累加alpha:积分达到 1.0 时切出一个 token 的加权隐帧,并把frames / integrate的残余隐状态缓存到下一 chunk,实现跨 chunk 的连续 CIF 积分。- 会话结束(
END控制输入)时,代码追加一帧tail_threshold的伪 alphas(L51-L56),把最后不足一个 token 的尾巴"冲"出来。 - 得到
acoustic_embeds后,execute(L153-L241)并不直接出文本,而是构造一个指向decoder的异步子推理请求(pb_utils.InferenceRequest+async_exec,L233-L241),并用 flags 标记会话阶段:1=首个 chunk(decoder 侧 sequence start)、2=末 chunk(sequence end)、3=首尾同帧、0=普通帧,最后asyncio.gather汇总各 decoder 返回的sample_ids,经config.yaml中的token_list转回中文文本(L253-L254)。 - 词表规模 8404,与模型名
vocab8404及 decoder/config.pbtxt 中logits [-1, 8404]输出维一致,可交叉印证三级配置的同源性。
4.5 decoder:16 层 KV cache 的自回归解码
decoder/config.pbtxt 声明了in_cache_0…in_cache_15共 16 个序列状态(每个[512, 10],初值全零),对应 decoder 16 层注意力对最近 10 帧编码帧的 KV 缓存——这是流式 Paraformer "编码器帧对齐解码"结构的直接体现:解码器每个时间步只看当前 chunk 的 acoustic embeds,跨 chunk 记忆完全由这 16 组 cache 承载。输入为enc、acoustic_embeds及各自长度,输出logits [-1, 8404]与采样后的sample_ids。其 sequence batching 采用preferred_batch_size: [16, 32, 64],与 feature_extractor 的[32, 64, 128]形成梯度凑批,降低解码器的单请求延迟。
4.6 端到端数据流小结
以一路 16kHz 流式音频为例:客户端每 0.6s 发送一块 PCM → feature_extractor 产出 61×80 fbank 窗口 → lfr_cmvn_pe 产出约 10×560 归一化特征(并滚动 10 帧左上下文 cache)→ encoder 产出 10×512 隐状态与 alphas → cif_search 积分出 0~2 个 token 的 acoustic embeds → decoder 采样出 token id → 拼成文本返回。四级共享CORRID会话,START/END控制输入保证各级的缓存(fbank 队列、LFR cache、CIF 积分、16 层 KV cache)同生共死。
五、单卡 A10 性能基准
原文档给出的实测基准(FP32、ONNX、Paraformer-Large Online;chunk 为 10 帧 × 960 采样 / 16000Hz =0.6s,因此实时性要求单次请求时延显著低于 600ms):
| Concurrency | Throughput | Latency_p50 (ms) | Latency_p90 (ms) | Latency_p95 (ms) | Latency_p99 (ms) |
|---|---|---|---|---|---|
| 20 | 309.252 | 56.913 | 76.267 | 85.598 | 138.462 |
| 40 | 391.058 | 97.911 | 145.509 | 150.545 | 185.399 |
| 60 | 426.269 | 138.244 | 185.855 | 201.016 | 236.528 |
| 80 | 431.781 | 170.991 | 227.983 | 252.453 | 412.273 |
| 100 | 473.351 | 206.205 | 262.612 | 288.964 | 463.337 |
从表格可以读出两点工程结论:
- 单卡 A10 在 20~60 并发路下即可稳定满足实时约束:p95 时延 85~200ms,仅为 600ms chunk 周期的 1/7 ~ 1/3,RTF 远小于 1;
- 吞吐拐点出现在约 80 并发:从 60→100 并发,吞吐仅从 426 提升到 473,而 p99 从 236ms 恶化到 463ms,说明此时 GPU encoder 与 CPU cif_search 均已接近饱和,盲目加并发只会推高尾延迟。若需更高并发,应横向扩卡或增加 encoder 实例数(
instance_group.count)而非堆叠单路请求。
5.1 基准的复现方式:manifest 并行压测客户端
仓库提供了两套客户端,可用于复现上述时延指标:
- client/decode_manifest_triton.py:基于 lhotse manifest 的并行解码脚本。
--streaming模式下,它把每条音频切成首块 + 若干等长 chunk 依次发送,通过 gRPC 的sequence_start/sequence_end标记会话边界(L344-L351),逐 chunk 记录latency_data,最后输出latency_50/90/99_percentile、方差与 RTF(L485-L493),并落盘recogs-*.txt与errs-*.txt(配合--compute-cer计算中文 CER);--simulate-streaming还会按真实语速 sleep,模拟真实说话场景。 - client/client.py:轻量 gRPC 客户端,支持
--audio_file单文件、--wavscp批处理(目录下附 client/aishell_test.txt 清单),--streaming时按--chunk_size/--sample_rate分块推送。
复现压测时的注意事项:sequence_id每任务独立(脚本以task_index + 10086生成),并发任务数即表中 Concurrency 列;客户端需与容器同网段(容器使用--net host,直接连localhost:10086)。
六、实操要点与排查清单
- 目录名必须与 config.pbtxt 中的相对参数一致:
feature_extractor/config.pbtxt的config_path写的是model_repo_paraformer_large_online/feature_extractor/config.yaml(相对--model-repository的上级路径),而cif_search/config.pbtxt的vocabulary参数同样指向该 yaml 以加载token_list。因此启动时必须cd到包含model_repo_paraformer_large_online的目录(即容器内/workspace),与原文档命令一致。 - ONNX 文件缺失会导致模型加载失败:
encoder/1/model.onnx、decoder/1/decoder.onnx、lfr_cmvn_pe/1/lfr_cmvn_pe.onnx三个文件必须就位;lfr_cmvn_pe.onnx可用仓库自带脚本重新导出(python export_lfr_cmvn_pe_onnx.py),导出前确保当前目录下有am.mvn。 - CUDA 驱动不匹配时降级 Triton 镜像:Dockerfile 顶部注释明确提示,若遇 CUDA driver mismatch,可改选更低版本的
tritonserver:xx.xx基础镜像。 - 会话回收与并发上限:各级
max_candidate_sequences: 1024与 Python 后端LimitedDict(1024)共同把单实例并发会话上限定在 1024 路;max_sequence_idle_microseconds为 15s,长空闲客户端需保证心跳 chunk 或及时发END。 - 量化与 TensorRT 不在本文档范围内:本部署全链路 FP32 ONNX;同目录的 README.md(SenseVoice 最佳实践)与 README_paraformer_offline.md 分别覆盖了其他模型的部署与离线版 paraformer 的模型仓库,可作横向参考。
七、小结
FunASR 的流式 Paraformer Triton 部署方案把"训练侧的流式建模"完整映射到了生产级推理框架上:sequence batching 的START/READY/CORRID/END控制输入承载多路会话生命周期,ONNX 侧的state声明承载 LFR 左上下文与 16 层 KV cache,Python 侧的LimitedDict会话字典承载 fbank 队列与 CIF 积分状态,而 cif_search 通过异步子调用把 CPU token 搜索与 GPU 解码解耦。配合 0.6s chunk 与 300μs 级凑批等待,单卡 A10 即可支撑数十路并发、p95 时延低于 200ms 的实时中文语音识别服务。完整参考实现与配置见 model_repo_paraformer_large_online 与 runtime/triton_gpu/client。
【免费下载链接】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),仅供参考