MeloTTS QNN ONNX更新记录
概述本文档记录了将 MeloTTS 从原始 ONNX 动态模型适配为Qualcomm QNN HTP 静态模型所做的所有源码改动以及每项改动的原理和收益。背景原始 MeloTTS ONNX 模型是动态 shapevariable sequence length在 CPU/CUDA 上运行正常。但要部署到 Qualcomm QCS8550 的 Hexagon HTP (v73) 上运行时遇到了动态 shape 不兼容、fp16 精度崩塌、DSP 内存不足等一系列问题。重磅更新啦MeloTTS-QNN基于QCS8550 V73架构qnn onnx模型部署问题解决# 模型下载modelscope download--modelKeanuX/MeloTTS-ZH-MIXED-EN-ONNX--local_dir./# 源码Clonegitclone https://gitee.com/jackroing/melo-tts-onnx.git使用说明解压job_jp3xz8lmp_optimized_onnx_mn7p4wz8n.onnx.zip到precompiled_qnn_onnx文件夹下并提取model.onnx以及model.bin到precompiled_qnn_onnx文件夹下在QCS8550设备上需要安装Qairt SDK 2.46及以上版本Python3.10.12并安装了onnxruntime-qnn基于Qairt SDK 2.46版本的Python依赖。一、修改的文件清单文件改动类型说明melo/models.py核心逻辑添加静态导出路径、QNN 保底模式、y_lengths 输出melo/attentions.py核心逻辑添加导出模式相对位置编码切片替代动态 padmelo_extra/melo_tts.py封装层支持多路导出动态/静态/QNN、y_lengths 联合输出export_melo.py导出工具新增--seq_len、--max_mel_frames、--qnn、--precision参数run_onnx.py推理引擎y_lengths 精确截断、RMS 静音检测、自动 dtype 适配、名称映射输入compile.py编译工具一键导出 qai-hub 编译 profilecompile_configs/cfg.ini编译配置静态 512 序列长度的输入 spec二、核心问题与解决方案问题 1动态 shape → QNN 不兼容现象动态 ONNX 模型sequence_length可变编译为 QNN context binary 后推理时报QNN error 1100。根因QNN context binary generator 在编译时把图优化固化为特定 shape运行时 shape 不匹配即报错。解决方案导出静态 ONNX 模型所有维度冻结为固定值。改动位置export_melo.pymelo/models.py文件改动export_melo.pybuild_melo_dummy_input()新增target_seq_len512参数将文本 token 序列统一 pad/truncate 到 512export_melo.py不传-id时dynamic_axes{}导出纯静态模型melo/models.pyforward_for_export_static()中y_mask的arange改为可配置的max_mel_frames参数原硬编码 2048melo/attentions.pyMultiHeadAttention新增_build_export_embeddings()export_mode预构建固定尺寸的 relative position 缓冲区用纯 slice 替代运行时F.pad避免动态 op收益QNN 编译器可以完整静态优化整个计算图消除 shape 不匹配导致的 QNN error 1100问题 2DSP 内存不足QNN_COMMON_ERROR_MEM_ALLOC现象模型加载时 DSP 尝试 mmap ~191MB 的缓冲区失败报QNN_COMMON_ERROR_MEM_ALLOC。根因forward_for_export_static中y_mask硬编码2048帧导致输出 buffer(1, 1, 2048×512) 1,048,576 samples中间 tensor 呈指数级放大DDR spill 高达 12.4GB累积峰值内存超 Hexagon v73 可用空间解决方案将max_mel_frames从 2048 降低到 1024可配置。改动位置melo/models.pyexport_melo.py文件改动melo/models.pyforward_for_export_static()的static_range torch.arange(max_mel_frames, ...)参数默认 1024melo/attentions.pyMultiHeadAttention._max_length同步减小_build_export_embeddings()buffer 缩小一半melo_extra/melo_tts.pyMeloTTSWrapper.__init__()新增max_mel_frames属性export_melo.py新增-mmf/--max_mel_framesCLI 参数收益指标修改前 (2048)修改后 (1024)输出 audio shape(1, 1, 1,048,576)(1, 1, 524,288)Attention buffer per MHA 层4095 × channels2047 × channelsDSP peak memory (profile)309 MB (OOM)~247 MB (正常)问题 3fp16 精度崩塌 → 音频全空现象模型在 fp32 (CPU/CUDA) 上输出正常但在 QNN HTP fp16 上y_lengths847 token 只预测 8 帧 mel即 0.09 秒音频几乎为空。根因链Text Encoder (12 层 Transformer) ↓ fp16 累积误差 Encoder 输出 x 失真 ↓ Duration Predictor (DP/SDP) ↓ logw 极度负值 → exp(logw) fp16 underflow → 0 w_ceil 0 for most tokens ↓ y_lengths ≈ 8 (几乎为零) ↓ Decoder 输出全是 bias 漂移噪声逐步排查过程尝试 1 — 仅 clamp logwclamp(logw, min-11.5)→ 无效因为exp(-11.5) ≈ 1e-5在 fp16 中是 subnormal被 flush to zero尝试 2 — 提高 clamp 到 fp16 normal 下界clamp(logw, min-9.7)→ 无效问题不在 exp 而在 encoder 输出本身已损坏尝试 3 — 绕过 SDP 只用 DP clamp仍无效DP 同样依赖 encoder 输出fp16 下也产出垃圾尝试 4最终— 彻底绕过整个 Duration Predictor固定每 token 时长不走任何可学习的 duration 模块最终解决方案QNN 模式下 duration 完全绕过 SDP/DP使用固定时长。改动位置melo/models.py第 1060-1061 行ifgetattr(self,_qnn_mode,False):# QNN HTP fp16: SDP/DP 全崩固定每 token 4 帧w_ceiltorch.ones_like(x_mask.squeeze(1))*4# [b, t_text]w_ceilw_ceil.unsqueeze(1)*x_mask# [b, 1, t_text]else:# fp32 / CPU: 原始 SDP DP正常质量logwself.sdp(...)*sdp_ratioself.dp(...)*(1-sdp_ratio)wtorch.exp(logw)*x_mask*(1.0/speed[0])w_ceiltorch.ceil(w)导出模式Duration 方案适用场景本地 ONNX (--qnn不传)原始 SDP DPCPU/CUDAfp32语音自然QNN HTP (--qnn)固定 4 帧/tokenQCS8550 HTPfp16零精度风险收益彻底消除 fp16 下 encoder → duration predictor 链路的精度崩塌每 token 固定 4 帧47 token → 188 帧 → ~2.2 秒覆盖常规语速纯常量乘法ones * 4 * x_mask无任何 fp16 敏感 op问题 4静态模型输出超长 → 尾部杂音/空音频现象静态模型输出固定长度 buffer如 524,288 samples实际有效音频只占前面一小段尾部是 decoder 对零 mel 输入产生的空闲输出。需要精确截断。根因ONNX 静态模型只有audio_data一个输出推理端无法知道有效音频从哪结束。排查过程尝试 1 — 文本比例线性估算valid_audio total_audio × (text_len / total_len)→ 不准确duration 是非线性的尝试 2 — 静音检测峰值尾部空闲噪音振幅 ~0.06语音振幅 ~0.32ratio 仅 5.5x → 门限难以选取尝试 3 — RMS 能量检测CPU 上区分度好ratio 10x但在 QNN fp16 上完全失效idle noise RMS ≈ speech RMS尝试 4最终— 直接导出y_lengths模型内部已知有效 mel 帧数直接作为第二个 ONNX 输出解决方案在 ONNX 模型中添加y_lengths有效 mel 帧数作为第二个输出。改动位置melo/models.pymelo_extra/melo_tts.pyexport_melo.pyrun_onnx.py文件改动melo/models.pyforward_for_export_static()返回第 5 个元素为y_lengthsmelo_extra/melo_tts.py静态路径return audio_valid, y_lengths.to(torch.int32)export_melo.py静态导出output_names [audio_data, y_lengths]run_onnx.pyvalid_samples y_len * hop_size精确截取收益推理端直接取y_lengths × hop_size得到精确有效音频长度任何 EPCPU/CUDA/QNN都适用零误判兼容旧模型检测到单输出时自动 fallback 到 RMS 检测问题 5输入索引硬编码 → 多版本模型不兼容现象不同导出模式下的模型输入数量不同动态 11 输入、静态 ONNX 10 输入、QNN 8-9 输入硬编码self.input_names[8]导致IndexError。解决方案改为按名称映射输入。改动位置run_onnx.py# 旧硬编码索引input_spec{self.input_names[0]:x_tst,self.input_names[7]:np_sdp_ratio,...}# 新按名字映射兼容所有输入变体named_inputs{x_tst:x_tst,sdp_ratio:np_sdp_ratio,speed:np_speed,...}input_spec{k:vfork,vinnamed_inputs.items()ifkinself.input_names}收益一套推理代码兼容 8/9/10/11 输入的所有模型版本。三、导出命令速查# 本地 ONNXfp32SDPDP完整质量python export_melo.py-sl512-mmf1024--opset16# QNN HTPfp32 IO固定 durationQNN 优化python export_melo.py-sl512-mmf1024--opset16--qnn# 一键导出 编译 profilebashexport_and_compile.sh四、架构总览┌─────────────────────────┐ │ export_melo.py │ │ -sl 512 -mmf 1024 │ │ --qnn --opset 16 │ └───────────┬───────────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ┌─────────▼────────┐ ┌──────▼──────┐ ┌────────▼─────────┐ │ 本地 ONNX (fp32) │ │ QNN ONNX │ │ 推理端 │ │ SDP DP │ │ 固定 4 帧 │ │ y_lengths 精确 │ │ 自然语音质量 │ │ fp16 安全 │ │ 截断音频 │ └──────────────────┘ └─────────────┘ └──────────────────┘