一、模型没有变,为什么还要更换推理路径?
在 Qwen3-ASR 接入会议系统的早期阶段,熙瑾会悟采用的是相对直接的 PyTorch 路径:通过 Qwen3ASRModel.from_pretrained() 加载模型,收到音频文件后调用 model.transcribe(),再把识别结果返回给上层业务。
这种方式代码直观、调试方便,也比较适合模型验证和单机功能测试。Qwen3-ASR 官方工具包同时提供 Transformers 和 vLLM 两种后端,其中 Transformers 路径可以直接通过 from_pretrained() 使用,vLLM 路径则通过 Qwen3ASRModel.LLM() 或独立服务启动。
但当 Qwen3-ASR 真正进入会悟的会议转写链路后,问题逐渐从"能不能跑"转向"能不能长时间稳定跑":
- 单路短音频识别正常,并发增加后吞吐提升有限;
- 音频长度变化较大时,设备利用率波动明显;
- Python、PyTorch、硬件插件和算子库之间的版本耦合较强;
- 在部分国产加速卡上,模型可以加载,但部分算子没有进入理想的优化路径;
- 流式转写还需要重新设计一套推理和状态管理逻辑;
- 设备厂商提供的生产镜像、模型案例和调优文档更多集中在 vLLM 路径。
最终,我们决定保留 Qwen3-ASR 模型和现有业务接口,只把底层推理方式从 PyTorch 路径迁移到 vLLM 路径。
这次改造的重点不是"换模型",而是给同一个模型更换一套更适合服务化运行的推理底座。
二、为什么信创环境更容易出现两条路径表现不一致
在标准 NVIDIA CUDA 环境中,PyTorch 生态成熟,很多模型使用原生 Transformers 路径就能获得不错的运行效果。
但在信创环境中,硬件通常还需要配套的软件栈:
text
国产加速卡
↓
设备驱动与运行时
↓
PyTorch适配插件
↓
算子库、图编译器与通信库
↓
Transformers或vLLM
↓
Qwen3-ASR
理论上,PyTorch 和 vLLM 最终都会调用底层加速卡完成计算;但工程上,两条路径的适配成熟度未必一致。
在我们接触的部分信创软硬件环境中,PyTorch 路径更偏向"模型能够运行",而 vLLM 路径已经开始围绕在线服务、动态批处理、图模式、内存管理和模型案例进行专项优化。
这种差异并不意味着国产加速卡不支持 PyTorch,也不能概括所有信创产品。更准确的说法是:
部分信创厂商将主要的模型服务化优化放在 vLLM 及其硬件插件上,PyTorch 路径则更适合作为基础兼容和功能验证方案。
昇腾的公开文档已经为 Qwen3-ASR 提供了独立的 vLLM-Ascend 部署教程、镜像要求、启动参数、性能评估方法和硬件相关配置,并要求 vLLM 与硬件插件版本保持匹配。这个现象说明,在部分国产算力平台上,vLLM 已经不只是通用开源框架,而是厂商重点维护的模型服务入口。
因此,迁移前我们先确定了一条原则:
text
PyTorch路径:保留,作为验证和回滚方案
vLLM路径:作为正式在线服务路径
三、改造前的 PyTorch 路径
原来的模型加载方式比较直接:
python
import os
import torch
from qwen_asr import Qwen3ASRModel
MODEL_PATH = os.getenv(
"ASR_MODEL_PATH",
"/data/models/Qwen3-ASR-0.6B",
)
model = Qwen3ASRModel.from_pretrained(
MODEL_PATH,
dtype=torch.bfloat16,
device_map="cuda:0",
max_inference_batch_size=1,
max_new_tokens=1024,
)
识别时直接调用:
python
def transcribe_with_pytorch(
audio_path: str,
language: str | None = "Chinese",
) -> dict:
results = model.transcribe(
audio=audio_path,
language=language,
)
if not results:
raise RuntimeError("ASR模型未返回结果")
result = results[0]
return {
"language": result.language,
"text": result.text,
}
再用 FastAPI 封装:
python
import asyncio
from fastapi import FastAPI
app = FastAPI()
inference_lock = asyncio.Lock()
@app.post("/asr")
async def transcribe(audio_path: str):
async with inference_lock:
result = await asyncio.to_thread(
transcribe_with_pytorch,
audio_path,
"Chinese",
)
return result
这条路径的优势很明显:
| 维度 | PyTorch 路径表现 |
|---|---|
| 代码理解 | 简单,调用关系清晰 |
| 单步调试 | 方便,可以直接断点 |
| 模型修改 | 容易修改前向过程 |
| 功能验证 | 适合快速验证模型 |
| 依赖数量 | 相对较少 |
| 服务化能力 | 需要自行补充 |
| 高并发调度 | 需要自行设计 |
| 流式识别 | 需要额外实现 |
对于单路文件转写,这种方式完全可以使用。问题主要出现在长期服务化运行之后。
四、PyTorch 路径在信创设备上的几个实际问题
1. 能运行,不代表所有算子都进入最佳路径
模型可以完成识别,只能证明功能链路打通。
如果部分算子发生回退、频繁执行设备与主机之间的数据交换,或者没有进入图编译和融合算子路径,最终表现可能是:
text
设备利用率时高时低
单路延迟尚可
并发增加后吞吐增长有限
长音频波动明显
CPU占用偏高
Python调用栈较重
这类问题通常不会直接报错,却会影响生产环境中的稳定性和容量规划。
2. 版本组合过多
PyTorch 路径通常同时依赖:
text
Python
PyTorch
Transformers
qwen-asr
硬件PyTorch插件
驱动
运行时
算子库
FlashAttention或替代实现
某个组件升级后,可能出现模型能导入但推理失败、符号找不到、算子不支持或显存占用变化。
3. 并发需要业务层自己管理
为了避免显存溢出,原服务使用了全局锁:
python
inference_lock = asyncio.Lock()
这可以保证稳定,但也意味着多个请求最终仍然排队串行执行。即使设备还有计算余量,业务层也难以安全地自行拼接动态批次。
4. 流式能力难以平滑接入
Qwen3-ASR 官方目前只在 vLLM 后端提供流式推理,流式模式不支持批量推理,也不返回时间戳。也就是说,继续停留在 PyTorch 路径,需要自行补齐流式状态、音频分块和增量结果管理。
五、迁移目标:业务接口保持不变
我们不希望上层会议业务感知底层推理框架变化。
改造前后的链路设计如下:
text
改造前:
会议音频
↓
ASR业务服务
↓
PyTorch模型进程
↓
转写结果
改造后:
会议音频
↓
ASR业务服务
↓
vLLM推理服务
↓
OpenAI兼容响应
↓
统一转写结果
会悟仍然调用原来的内部 ASR 接口:
text
POST /api/asr/transcribe
返回结构也保持不变:
json
{
"language": "Chinese",
"text": "本次会议主要讨论推理后端迁移方案。",
"duration_sec": 65.38,
"process_sec": 4.21,
"rtf": 0.0644
}
这样做有两个好处:
第一,会议录音、说话人处理、纪要生成和资料归档模块不需要同步修改。
第二,vLLM 出现兼容问题时,可以快速切回原来的 PyTorch 服务。
六、先不要删除旧环境,单独创建 vLLM 环境
原来的 PyTorch 环境继续保留:
text
/data/envs/qwen3-asr-pytorch
新建 vLLM 环境:
bash
conda create \
-n qwen3-asr-vllm \
python=3.12 \
-y
conda activate qwen3-asr-vllm
python -m pip install --upgrade pip
pip install -U "qwen-asr[vllm]"
Qwen3-ASR 官方明确将 vLLM 作为更快的推理后端,并提供 qwen-asr[vllm] 安装方式、Python 调用方式以及 qwen-asr-serve 服务化命令。
在信创环境中,不建议直接执行不带版本限制的安装命令。
更稳妥的做法是优先使用硬件厂商提供的镜像或依赖包:
text
硬件驱动版本
运行时版本
PyTorch插件版本
vLLM版本
vLLM硬件插件版本
qwen-asr版本
模型版本
这些版本必须作为一个整体保存。
检查环境:
bash
python -V
pip show qwen-asr
pip show vllm
python -c "
import vllm
print(vllm.__version__)
"
如果使用国产加速卡,还要额外检查对应插件:
bash
pip list | grep -Ei \
'ascend|musa|metax|vllm'
七、第一种改法:在 Python 中使用 vLLM 后端
Qwen3-ASR 官方提供了 Qwen3ASRModel.LLM() 初始化方式:
python
import os
from qwen_asr import Qwen3ASRModel
MODEL_PATH = os.getenv(
"ASR_MODEL_PATH",
"/data/models/Qwen3-ASR-0.6B",
)
def main() -> None:
model = Qwen3ASRModel.LLM(
model=MODEL_PATH,
gpu_memory_utilization=0.65,
max_inference_batch_size=8,
max_new_tokens=2048,
)
results = model.transcribe(
audio=[
"/data/test/meeting_01.wav",
"/data/test/meeting_02.wav",
],
language=[
"Chinese",
"Chinese",
],
)
for result in results:
print(result.language)
print(result.text)
if __name__ == "__main__":
main()
官方特别提醒,vLLM 初始化代码应放在:
python
if __name__ == "__main__":
下面,否则多进程启动方式可能引发 spawn 相关错误。
这条路径对原有代码改动较小,但模型生命周期仍然和业务 Python 进程绑定。
如果业务服务重启,模型也会重新加载;如果 ASR 进程异常,接口服务会一起退出。
因此,生产环境中我们最终选择了第二种方式:把 vLLM 做成独立推理服务。
八、第二种改法:独立启动 vLLM 服务
最简单的启动方式是:
bash
export CUDA_VISIBLE_DEVICES=0
qwen-asr-serve \
/data/models/Qwen3-ASR-0.6B \
--served-model-name qwen3-asr-0.6b \
--gpu-memory-utilization 0.65 \
--max-model-len 4096 \
--host 127.0.0.1 \
--port 18081
qwen-asr-serve 是对 vllm serve 的封装,可以继续传递 vLLM 支持的服务参数。Qwen3-ASR 也已经获得 vLLM 的模型支持,可以通过 OpenAI 风格接口调用。
服务启动后检查:
bash
curl http://127.0.0.1:18081/v1/models
预期返回:
json
{
"object": "list",
"data": [
{
"id": "qwen3-asr-0.6b",
"object": "model"
}
]
}
vLLM 可以作为 OpenAI API 兼容服务运行,因此上层系统只需要配置地址、模型名称和认证信息,不必直接依赖 vLLM 的 Python 内部结构。
信创环境下的参数不要直接照搬 CUDA 示例
不同硬件插件支持的参数可能不同。
部分信创环境需要增加:
text
--enforce-eager
--dtype float16
--max-model-len 4096
硬件专用additional-config
图编译配置
以 vLLM-Ascend 为例,官方 Qwen3-ASR 部署文档针对不同昇腾设备给出了不同启动参数,并明确要求使用匹配的 vLLM 与 vLLM-Ascend 版本。
因此,通用命令只能作为起点,不能直接当作所有国产加速卡的生产配置。
九、业务适配层如何调用 vLLM
可以通过 OpenAI SDK 发送音频请求:
python
import base64
import os
from pathlib import Path
from openai import OpenAI
from qwen_asr import parse_asr_output
VLLM_BASE_URL = os.getenv(
"VLLM_BASE_URL",
"http://127.0.0.1:18081/v1",
)
MODEL_NAME = os.getenv(
"VLLM_MODEL_NAME",
"qwen3-asr-0.6b",
)
client = OpenAI(
base_url=VLLM_BASE_URL,
api_key="EMPTY",
)
def audio_to_data_url(audio_path: str) -> str:
path = Path(audio_path)
suffix = path.suffix.lower().lstrip(".") or "wav"
encoded = base64.b64encode(
path.read_bytes()
).decode("ascii")
return (
f"data:audio/{suffix};base64,"
f"{encoded}"
)
def transcribe_with_vllm(
audio_path: str,
) -> dict:
response = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{
"role": "user",
"content": [
{
"type": "audio_url",
"audio_url": {
"url": audio_to_data_url(
audio_path
)
},
}
],
}
],
temperature=0,
)
raw_content = (
response.choices[0]
.message.content
)
language, text = parse_asr_output(
raw_content
)
return {
"language": language,
"text": text,
}
为了避免大文件经过 Base64 后明显膨胀,也可以由内部文件服务提供受控 URL,再让 vLLM 服务读取。
但在隔离网络或安全敏感环境中,需要特别防范服务端请求伪造问题。vLLM 推理服务不应被允许任意访问办公网、管理网或互联网地址。
更稳妥的方式是:
text
业务服务接收文件
↓
保存到受控音频目录
↓
内部文件服务生成一次性地址
↓
vLLM读取指定网段地址
↓
任务结束后删除文件
十、保留统一接口,避免业务层绑定 vLLM
业务接口继续使用原来的结构:
python
import os
import time
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
ASR_BACKEND = os.getenv(
"ASR_BACKEND",
"vllm",
)
class ASRResult(BaseModel):
backend: str
language: str
text: str
process_sec: float
@app.post("/api/asr/transcribe")
def transcribe(audio_path: str):
started_at = time.perf_counter()
if ASR_BACKEND == "pytorch":
result = transcribe_with_pytorch(
audio_path
)
elif ASR_BACKEND == "vllm":
result = transcribe_with_vllm(
audio_path
)
else:
raise ValueError(
f"不支持的ASR_BACKEND:"
f"{ASR_BACKEND}"
)
return {
"backend": ASR_BACKEND,
"language": result["language"],
"text": result["text"],
"process_sec": round(
time.perf_counter() - started_at,
3,
),
}
切换到 vLLM:
bash
export ASR_BACKEND=vllm
回滚到 PyTorch:
bash
export ASR_BACKEND=pytorch
这种结构让会悟只依赖统一的 ASR 业务接口,不直接绑定 PyTorch、vLLM 或特定硬件插件。
后续即使再次更换推理后端,也只需要修改适配层。
十一、前后性能测试怎么做
性能对比至少要保证以下条件一致:
text
同一台服务器
同一张加速卡
同一个Qwen3-ASR模型
同一批音频
相同采样率和声道
相同语言参数
相同max_new_tokens
相同是否启用时间戳
相同音频预处理流程
vLLM-Ascend 的 Qwen3-ASR 文档也建议至少记录音频时长、请求并发、端到端延迟、实时因子和吞吐量,并分别测试短音频、长音频及并发场景。
测试环境记录模板
| 项目 | 测试配置 |
|---|---|
| 模型 | Qwen3-ASR-0.6B |
| 音频格式 | 16kHz、单声道、PCM WAV |
| PyTorch 路径 | Transformers + 厂商 PyTorch 插件 |
| vLLM 路径 | vLLM + 厂商 vLLM 插件 |
| 推理精度 | BF16 或 FP16 |
| 时间戳 | 关闭 |
| 测试轮数 | 每组预热 3 次,正式测试 10 次 |
| RTF 口径 | 处理耗时 ÷ 音频时长 |
前后性能对比表
下表为脱敏后的演示数据,用于展示文章中的统计方式,不代表任何具体信创硬件的公开性能。
| 测试项目 | PyTorch 路径 | vLLM 路径 | 迁移后变化 |
|---|---|---|---|
| 模型冷启动时间 | 44.8 秒 | 68.5 秒 | 启动更慢 |
| 30 秒音频单路耗时 | 2.04 秒 | 1.62 秒 | 降低 20.6% |
| 5 分钟音频单路耗时 | 22.7 秒 | 16.9 秒 | 降低 25.6% |
| 5 分钟音频 RTF | 0.076 | 0.056 | 降低 26.3% |
| 4 路 60 秒音频总耗时 | 27.6 秒 | 10.8 秒 | 降低 60.9% |
| 4 路场景音频吞吐 | 8.7 倍实时 | 22.2 倍实时 | 提升约 2.55 倍 |
| 空载设备内存占用 | 6.8 GB | 9.6 GB | 增加 2.8 GB |
| 并发调度方式 | 业务锁与固定批次 | 引擎统一调度 | 逻辑简化 |
| 流式转写 | 需单独开发 | 原生支持 | 接入成本降低 |
这组结果反映出的趋势是:
vLLM 不一定在所有指标上都占优。
它的模型启动时间更长,空载内存占用也可能更高,因为服务启动时会预留部分设备内存。
真正明显的改善主要出现在:
text
中长音频
多个请求并发
任务长度不一致
持续在线服务
流式识别
如果业务始终只有单路短音频,PyTorch 和 vLLM 的差距可能并不明显,甚至 PyTorch 更轻量。
十二、写一个简单的双路径测试脚本
python
import csv
import statistics
import time
from pathlib import Path
from typing import Callable
def benchmark_backend(
name: str,
transcribe_func: Callable[[str], dict],
audio_files: list[Path],
repeat: int = 5,
) -> list[dict]:
rows = []
for audio_path in audio_files:
costs = []
# 预热一次
transcribe_func(str(audio_path))
for _ in range(repeat):
started_at = time.perf_counter()
result = transcribe_func(
str(audio_path)
)
cost = (
time.perf_counter()
- started_at
)
costs.append(cost)
rows.append(
{
"backend": name,
"filename": audio_path.name,
"language": result["language"],
"mean_sec": round(
statistics.mean(costs),
4,
),
"p50_sec": round(
statistics.median(costs),
4,
),
"min_sec": round(
min(costs),
4,
),
"max_sec": round(
max(costs),
4,
),
}
)
return rows
audio_files = sorted(
Path("/data/asr-benchmark")
.glob("*.wav")
)
rows = []
rows.extend(
benchmark_backend(
"pytorch",
transcribe_with_pytorch,
audio_files,
)
)
rows.extend(
benchmark_backend(
"vllm",
transcribe_with_vllm,
audio_files,
)
)
with open(
"/data/asr-benchmark/result.csv",
"w",
encoding="utf-8-sig",
newline="",
) as file_obj:
writer = csv.DictWriter(
file_obj,
fieldnames=rows[0].keys(),
)
writer.writeheader()
writer.writerows(rows)
测试时不要只执行一次。
模型第一次请求通常包含缓存初始化、图编译或运行时预热。如果把第一次请求直接作为正式结果,很容易误判两条路径的差异。
建议采用:
text
冷启动单独记录
预热请求不计入平均值
正式请求至少执行5至10次
同时记录平均值、P50和最大值
十三、迁移过程中最容易踩的几个坑
1. gpu_memory_utilization 不是越高越好
vLLM 会根据该参数规划可使用的设备内存。
bash
--gpu-memory-utilization 0.9
不代表一定能获得最高性能。如果同一张卡还运行声纹、VAD或其他模型,设置过高可能导致其他进程无法启动。
共享设备时可以从:
bash
--gpu-memory-utilization 0.55
或:
bash
--gpu-memory-utilization 0.65
开始测试。
2. vLLM 启动慢不代表推理慢
vLLM 启动阶段需要完成模型加载、内存规划和部分执行准备,因此冷启动通常比直接加载 PyTorch 模型更慢。
生产环境不应频繁启停模型容器,也不要为每个转写任务临时启动一次 vLLM。
3. 硬件插件版本必须和 vLLM 匹配
这是信创环境中最重要的一点。
不能只升级:
bash
pip install -U vllm
而不检查厂商插件是否支持这个版本。
昇腾官方文档明确要求使用与 vLLM 版本匹配的 vLLM-Ascend 镜像,并通过支持矩阵确认模型状态。其他国产加速卡环境同样应优先遵循厂商发布的版本组合,而不是只追求最新版。
4. 流式路径和离线文件路径仍需分别测试
Qwen3-ASR 的流式推理当前仅由 vLLM 后端提供,但流式模式不支持批量推理和时间戳。
因此不能因为离线文件识别正常,就认为流式会议也已经完成迁移。
至少需要额外验证:
text
首个增量结果延迟
音频分块大小
断流重连
最终文本稳定性
会话状态清理
并发会话数量
5. 时间戳仍可能需要独立服务
Qwen3-ASR 的时间戳由 Forced Aligner 提供。
在实际架构中,可以采用:
text
vLLM:负责ASR转写
PyTorch服务:负责Forced Aligner
这并不矛盾。
推理路径迁移不要求把所有相关模型全部塞进同一个 vLLM 服务。对于低频使用的时间戳功能,独立按需调用反而更节省设备资源。
十四、上线时采用双服务灰度,而不是原地覆盖
迁移期间同时保留两个端口:
text
PyTorch服务:127.0.0.1:18140
vLLM服务:127.0.0.1:18141
业务配置:
bash
ASR_BACKEND=vllm
PYTORCH_ASR_URL=http://127.0.0.1:18140
VLLM_ASR_URL=http://127.0.0.1:18141
灰度顺序:
text
第一阶段:同一音频双跑,只使用PyTorch结果
第二阶段:内部测试会议使用vLLM结果
第三阶段:部分普通会议切换到vLLM
第四阶段:扩大到长会议和并发任务
第五阶段:vLLM成为默认路径
每个阶段都记录:
text
识别耗时
RTF
失败率
超时次数
设备内存
设备利用率
文本差异
会议纪要生成是否正常
确认稳定后,再停止 PyTorch 常驻服务,但环境、模型和启动脚本仍应保留一段时间。
十五、从迁移结果看,vLLM真正解决了什么
迁移完成后,最明显的变化并不是单条音频快了多少,而是 ASR 模块从"嵌在 Python 业务中的模型"变成了"独立推理服务"。
改造前:
text
业务进程负责接口
业务进程负责模型
业务进程负责并发锁
业务进程负责显存控制
业务进程异常时模型一起退出
改造后:
text
业务进程负责文件和任务
vLLM负责模型生命周期
vLLM负责请求调度
vLLM负责设备内存规划
两层服务可以独立重启
会悟的上层会议流程没有因为推理框架变化而重新设计,仍然接收统一的转写文本,再继续完成说话人处理、纪要生成、待办提取和资料归档。
十六、总结:信创适配不能只看模型能否启动
Qwen3-ASR 从 PyTorch 路径迁移到 vLLM 路径,本质上是一次从"模型调用"到"推理服务"的改造。
PyTorch 路径并没有失去价值。它仍然适合:
text
模型功能验证
算法调试
小规模单路识别
Forced Aligner
故障回滚
vLLM 路径则更适合:
text
长期在线服务
多请求调度
中长音频
并发转写
流式识别
标准化API接入
在部分信创环境中,厂商对 vLLM 路径提供了更完整的镜像、插件、模型案例和调优参数,因此同一个 Qwen3-ASR 模型通过 vLLM 运行时,更容易进入经过专项优化的设备执行路径。
但这不是一个可以脱离硬件和版本讨论的绝对结论。真正决定迁移效果的,仍然是固定版本、统一输入、真实会议音频和完整的并发测试。
对熙瑾会悟而言,这次迁移最大的收益不是单项性能数字,而是让 ASR 模块拥有了更清晰的服务边界、更稳定的并发处理方式,以及后续适配不同信创算力平台的空间。