FunASR 中 Qwen3-ASR 流式 WebSocket 服务实战:VAD 职责辨析、vLLM 版本锁定与部署踩坑指南
【免费下载链接】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 仓库 examples/industrial_data_pretraining/qwen3_asr 下的serve_qwen3_asr_ws.py流式 WebSocket 服务及其配套说明文档展开,系统讲解 Qwen3-ASR 增量式流式 API 在使用中的核心难点:VAD 在 ASR 中的两种完全不同职责、vLLM 版本为何必须锁定 0.14、模型权重的离线下载链路,以及 tokenizer 与启动日志中几类"看起来吓人、实际无害"的警告。读完本文,你将能正确部署 Qwen3-ASR 流式 WebSocket 服务、规避版本与下载陷阱,并理解"服务端自动断句"在开源流式 API 中应由哪一层实现。
1. 服务定位:把 Qwen3-ASR 原生流式 API 包装成 WebSocket 服务
serve_qwen3_asr_ws.py的核心作用,是把官方example_qwen3_asr_vllm_streaming.py的原生流式 API(init_streaming_state/streaming_transcribe/finish_streaming_transcribe)包装成一个 WebSocket 服务,协议与 Fun-ASR-Nano 的serve_realtime_ws.py保持一致,因此可以直接复用同一个bench_streaming_ws.py压测脚本进行横向对比。
从 源码 可以看到其完整协议:
- 客户端连接
ws://host:port; - 客户端发送文本
"START",服务端回{"event": "started"}; - 客户端持续发送二进制 int16 PCM 块(16 kHz 单声道);
- 服务端随转写增长发送
{"partial": "<当前文本>"}; - 客户端发送文本
"STOP",服务端回{"is_final": true, "sentences": [{"text": "<最终文本>"}]},随后发送{"event": "stopped"}。
架构上它刻意对齐serve_realtime_ws.py:单 asyncio 事件循环,streaming_transcribe/finish_streaming_transcribe为同步调用、阻塞整个循环——这样压测出的并发特性才能与 Fun-ASR-Nano 那条路径同口径可比。生产扩容同样依赖"多进程 + CUDA MPS + nginx"方案(详见 vllm_guide.md §6.7 中关于单进程先压满再复制的扩容建议)。
启动方式(源码头注释明确给出):
pip install -U "qwen-asr[vllm]==0.0.6" "transformers==4.57.6" websockets numpy python serve_qwen3_asr_ws.py --port 10095 --gpu-memory-utilization 0.8其中--chunk-size-sec为可选参数,用于控制流式块大小(默认 2.0)。
几个关键实现细节值得注意:
- 全局只加载一次模型:
asr = None在模块级初始化,所有连接共用同一模型实例,每个连接各自持有独立的 streaming state(handle_client中每次收到START才调用init_streaming_state创建新 state)。 - int16 PCM 转 float32:
int16_pcm_to_float32将 bench 发来的 int16 小端 PCM 除以 32768 转为 Qwen3-ASR 的streaming_transcribe期望的[-1, 1)浮点波形。 - partial 去重:只在
state.text与上一次发送的文本不同时才下发{"partial": ...},避免刷屏,且不影响 bench 的首词延迟统计口径。 - max_new_tokens 固定 32:流式用小值,与官方 example 一致。
- websocket 日志降噪:
logging.getLogger("websockets").setLevel(logging.WARNING),压测时不再刷 "connection open/closed" INFO 日志。
配套的契约测试 tests/test_qwen3_asr_ws_example.py 还验证了两件事:说明文档不得残留"以下是我的个人理解"这类审阅占位语;handle_client必须捕获通用Exception并调用logging.exception记录,防止连接异常静默吞掉。
2. VAD 的两种用途:切段 VAD 与端点 VAD,切勿混为一谈
VAD(语音活动检测)在 ASR 中其实有两种完全不同的用途,容易混为一谈,这是理解 Qwen3-ASR 流式 API 设计的第一道门槛。
2.1 A 类:切段用的 VAD(给非流式 encoder 喂分段)
像 Fun-ASR-Nano 这类模型的 encoder 是非流式的,一次必须看到完整一段音频,因此必须靠 VAD 把连续音频切成一句句,每句整体编码、整体解码。这种 VAD 对这类模型是技术必需的——不切段就无法编码。
2.2 B 类:端点/轮次检测用的 VAD(判断"这一轮说完了没")
检测说话人停顿(例如静音 800ms),判定"一句话/一轮结束",从而触发"锁定文本 / 发 is_final / 该回应了"。这是产品行为层面的需求,与 encoder 是否流式无关。
2.3 Qwen3-ASR 开源流式 API 需要哪一种?
对本服务使用的qwen-asr[vllm]的init_streaming_state/streaming_transcribe而言:
- 不需要 A 类(切段)VAD。它是增量式流式——每次只吃新增的一小段音频,状态向前滚动、连续转写,不存在"先切句再解码"。"哪些字定了、哪些会变"由
unfixed_chunk_num/unfixed_token_num两个参数表达:尾部 N 个 chunk/token 算"未定"、可能被后续音频修正,其余视为已确认。这相当于内置的 partial/锁定机制,取代了 A 类 VAD 的切段职责。所以你在开源流式 API 的代码里搜不到vad——因为它这一层根本不切段。 - 仍然需要 B 类(端点)VAD,只是开源 API 自己不带。自动判断"用户停顿 = 这一轮结束"这件事,
streaming_transcribe本身不管。商用服务是在 ASR之外包一层server_vad来做(即turn_detection)。本服务目前用客户端显式发STOP来代替端点判断(bench 里音频放完即发 STOP)。若要在真实场景自动断句/断轮,需要自己在本服务之外接一个 VAD / 端点检测(角色等同商用的 server_vad),而不是去 Qwen3-ASR 内部找——它的开源流式 API 不含这一层。
一句话总结:Qwen3-ASR 增量流式省掉了"切段 VAD"(A),但**"端点/轮次 VAD"(B)这个职责依然存在**——商用版用server_vad实现、本服务用手动STOP代替。两者并不矛盾。
2.4 官方佐证:商用 Qwen-ASR-Realtime 的"VAD 模式 / Manual 模式"
阿里云百炼的实时语音识别(Qwen-ASR-Realtime)官方文档明确把"断句由谁做"分成两种模式,本质就是session.turn_detection开还是关:
VAD 模式(默认,
turn_detection配置为 server_vad):服务端自动检测语音起点/终点来断句,客户端只管持续发音频流,服务端在"检测到一句话结束"时自动返回最终结果。流程中服务端会发input_audio_buffer.speech_started/speech_stopped等事件——这正是 B 类端点 VAD,由服务端那一层(server_vad)实现,不是ASR 内核在切段。商用配置示例:"turn_detection": { "type": "server_vad", "threshold": 0.2, "silence_duration_ms": 800 }Manual 模式(
turn_detection设为 null):由客户端控制断句——发完一整句音频后,客户端发input_audio_buffer.commit通知服务端边界。适用于客户端能明确判断语句边界的场景(如"按住说话"、聊天发语音)。
对应关系:本服务serve_qwen3_asr_ws.py用客户端显式发STOP标记一轮结束,等价于商用的Manual 模式(turn_detection=null,客户端控制边界)。若要做成"服务端自动断句",就是去实现商用 VAD 模式的那一层端点检测(server_vad),加在本服务的增量转写之外,而不是在 Qwen3-ASR 转写内核里找。
2.5chunk-size-sec控制流式块大小
--chunk-size-sec控制流式块大小,默认 2.0,最终传入init_streaming_state的chunk_size_sec参数。值越小出字越快/越勤,但并发开销越大。配套说明给出了实测结论:在 L20 上,29 秒音频 48 路并发时,1.0全部失败、2.0全部通过。因此并发压力大的场景不宜盲目调小该值,需要在出字时延与并发容量之间取平衡。
3. 必须用 vllm 0.14,不要用 0.19(rope_scaling / thinker_config 警告)
本服务需要 vllm 加速,即安装qwen-asr[vllm],它锁定vllm==0.14.0(同目录下的 transcribe_vllm_offline_notes.md 也强调建议使用独立环境,因为该依赖被固定)。若换成更新的 vllm 版本(如 0.19.x),启动会出现:
Unrecognized keys in `rope_scaling` for 'rope_type'='default': {'mrope_section', 'mrope_interleaved', 'interleaved'} thinker_config is None. Initializing thinker model with default values3.1 根因:vllm 把rope_type从mrope改写为default
vllm 在 0.14 → 0.19 之间(注意 transformers 两版都是 4.57.6,可排除其影响),config 解析里的patch_rope_scaling_dict会把rope_type从'mrope'改写成'default'——它把 mrope 当作 legacy、假设由 vllm 内部消化mrope_section等字段:
elif rope_scaling["rope_type"] == "mrope": assert "mrope_section" in rope_scaling rope_scaling["rope_type"] = "default" # ← 改写但 Qwen3-ASR 自带的Qwen3ASRThinkerTextRotaryEmbedding期望从rope_scaling里读到"mrope"才走多模态 RoPE 分支:
self.rope_type = config.rope_scaling.get("rope_type", "default")被 vllm 改写成"default"后,它走了普通 RoPE,mrope_section/mrope_interleaved/interleaved这几个键没人认领 → 打印 "Unrecognized keys" 警告,且音频/文本的多模态位置编码退化。thinker_config is None那条同源:0.19 的加载路径没有正确解析 Qwen3-ASR 的 thinker 子配置,回退到默认参数。
3.2 影响与抉择
在 0.19 上服务"能起、也能出字"(两条是 WARNING/INFO 而非 ERROR),抽查几条转写也"看着正常";但位置编码退化对长音频/复杂内容可能有害,且未做 CER 量化对比,无法判定等价。保守起见,固定使用qwen-asr[vllm]自带的vllm==0.14.0。
3.3 vllm 加速必须用 Qwen3ASRModel,而非 AutoModelVLLM
funasr 的AutoModelVLLM不能加速 Qwen3-ASR,必须使用:
from qwen_asr import Qwen3ASRModel这一条解答了仓库 issue #3026 中提出的问题。离线示例 transcribe_vllm_offline.py 同样直接使用Qwen3ASRModel.LLM(...)作为后端,不经过AutoModelVLLM——后者服务于 FunASR 自有模型,当前不支持 Qwen3-ASR。
3.4 关于模型下载:仅设 VLLM_USE_MODELSCOPE 不够
官方推荐做法:如果运行环境不能在线下载、或无法访问 Hugging Face,先手动把权重下载到本地目录,再把本地路径传给--model:
# ModelScope(国内推荐) pip install -U modelscope modelscope download --model Qwen/Qwen3-ASR-1.7B --local_dir ./Qwen3-ASR-1.7B # 启动时指定本地模型目录 python serve_qwen3_asr_ws.py --model ./Qwen3-ASR-1.7B ...注意:仅设置VLLM_USE_MODELSCOPE=True并安装modelscope只能接管一部分下载流程。vLLM 会从 ModelScope 拉取 config、tokenizer、merges、vocab、model.safetensors.index.json等文件,但真正的权重model.safetensors仍可能回到huggingface.co拉取。这是 Qwen-ASR 当前下载链路的一个已知问题,实测日志形态如下:
Downloading Model from https://www.modelscope.cn to directory: /home/vllm/.cache/modelscope/hub/models/Qwen/Qwen3-ASR-1.7B 2026-06-28 10:15:56,301 - modelscope - INFO - Got 10 files, start to download ... ... INFO 06-28 10:15:59 [model.py:530] Resolved architecture: Qwen3ASRForConditionalGeneration '(MaxRetryError('HTTPSConnectionPool(host='huggingface.co', port=443): Max retries exceeded with url: /Qwen/Qwen3-ASR-1.7B/resolve/main/model.safetensors ...因此最稳妥的做法仍是提前用modelscope download将完整权重落盘到本地目录,再以本地路径启动,而不是依赖环境变量接管全部下载。
4. tokenizer 的fix_mistral_regex警告(无害,可忽略)
启动时可能出现:
The tokenizer you are loading from '.../Qwen3-ASR-1.7B' with an incorrect regex pattern ... This will lead to incorrect tokenization. You should set the `fix_mistral_regex=True` flag when loading this tokenizer to fix this issue.原因:Qwen3-ASR 的 tokenizer 沿用了一类带已知 regex 问题的分词器实现,底层库检测到该 regex 模式后给出提醒,建议加fix_mistral_regex=True修正切分。本服务通过 qwen-asr 的高层 API 加载模型、并不直接构造 tokenizer,没有暴露这个开关,所以这条提醒按原样打印。
影响:实测对中文 ASR 转写结果无可见影响(抽查多条转写正常,未做 CER 量化)。该 regex 修正主要影响某些特殊 token 的边界切分,对语音转写路径未观察到差异。属于"提醒级"噪音,可忽略。若要彻底消除,需在更底层自行加载 tokenizer 时传fix_mistral_regex=True,但 qwen-asr 高层 API 当前不直接支持,且无实测必要。
5. 顺带:另外两条启动日志(均无害)
Error retrieving safetensors: Repo id must be in the form ...:把本地模型路径当成 HF 仓库 id 去查线上元数据,失败后重试 2 次、回退本地加载,不影响功能。可设环境变量HF_HUB_OFFLINE=1消除。Downcasting torch.float32 to torch.bfloat16:权重以 fp32 存、按 bf16 加载,正常省显存/提速。bf16 与 fp32 指数位同宽,精度几乎无损。这是 INFO 不是错误。
6. 部署与压测实践:从单进程到多进程扩容
理解了上述踩坑点之后,部署路径就清晰了:
第一步,搭建独立环境并锁定版本(参考 transcribe_vllm_offline_notes.md 的做法):
python -m venv .venv-qwen3-vllm source .venv-qwen3-vllm/bin/activate pip install -U "qwen-asr[vllm]==0.0.6" "transformers==4.57.6" websockets numpy第二步,启动服务并验证协议:
python examples/industrial_data_pretraining/qwen3_asr/serve_qwen3_asr_ws.py \ --port 10095 --gpu-memory-utilization 0.8第三步,压测。服务协议与 Fun-ASR-Nano 的serve_realtime_ws.py一致,可复用 realtime_ws_benchmark.md 中描述的压测方法与指标口径(如first_update_ms、final_after_stop_ms、client_response_lag_ms等),输入须为 16 kHz 单声道 PCM16 WAV。注意:单循环同步转写的架构下,chunk-size-sec调小会显著放大并发开销(前述 L20 实测:29 秒音频 48 路并发,1.0 全失败、2.0 全通过),压测时应如实记录该参数。
第四步,扩容。按照 vllm_guide.md §6.7 的实践:一个进程是第一个扩容单元,先用内置批处理路径压满单进程(达到 GPU、CPU 或尾延迟上限)再增加副本;单卡上多个进程共享同一 GPU 时用 CUDA MPS(如多个进程都指向CUDA_VISIBLE_DEVICES=0),或按卡拆分进程;对外用 nginx 做负载均衡。每个额外进程都会重复占用模型显存并可能削弱批处理机会,因此"支持 N 路连接"没有普适数字,必须以自己的真实流量形态实测为准。
7. 总结:三类"坑"的本质与处理策略
| 现象 | 本质 | 处理方式 |
|---|---|---|
| 开源流式 API 里找不到 VAD | 增量式流式自带 partial/锁定机制,省掉了切段 VAD;端点 VAD 属于产品层职责 | 自动断句需在本服务之外接 server_vad 等效层;当前用手动STOP代替 |
| vllm 0.19 出现 rope_scaling / thinker_config 警告 | vllm 把mrope改写成default,多模态位置编码退化 | 固定vllm==0.14.0(qwen-asr[vllm]自带) |
VLLM_USE_MODELSCOPE=True仍连 huggingface.co | 下载链路只接管部分文件,权重文件仍回退线上 | 用modelscope download提前落盘,--model指向本地目录 |
fix_mistral_regex与两条启动日志 | 提醒级/INFO 噪音 | 可忽略;HF_HUB_OFFLINE=1可消除其中一条 |
核心结论:Qwen3-ASR 的增量流式 API 把"切段"职责交给了内置的unfixed_chunk_num/unfixed_token_num机制,把"端点检测"留给了上层;使用它的 WebSocket 服务时,版本锁定(vllm 0.14)与权重本地化是保证转写质量与离线可部署性的两个关键前提。
【免费下载链接】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),仅供参考