【2026腾讯犀牛鸟】Qwen3-TTS的ncnn移植 #6877
AI1379
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Qwen3-TTS 到 ncnn 的完整移植:从模型拆分到 C++ 端到端语音生成
摘要
本项目将
Qwen/Qwen3-TTS-12Hz-0.6B-Base从 PyTorch 迁移到 ncnn,并用C++ 实现完整的文本到语音推理路径。最终运行时覆盖 Qwen Tokenizer、Talker、
Code Predictor、Speaker Encoder、Speech Tokenizer Codec Encoder、Codec
Decoder、采样、KV cache、参考音频处理和 WAV 输出,不依赖运行时 Python 或
PyTorch。
项目的主要难点并不是把某一个 Transformer 转成 ncnn,而是正确复现三个神经
网络之间的动态生成协议:
最终实现已在 Windows/MSVC 和 Linux/GCC 的 CPU 路径编译运行。短文本和148帧
长文本的 C++ codec ID 与官方 PyTorch 逐位一致;最终波形误差保持在
1e-4量级。模型结构拆解
Qwen3-TTS不是单一网络,而是三个子系统和一套生成协议。
1. Talker
Talker是28层Qwen3主干,hidden size为1024,16个query head、8个KV head,
head dim为128,SwiGLU中间维度为3072。它接收文本hidden、控制token、
speaker embedding和历史codec embedding,输出3072维第一个codec码本的
logits。
2. Code Predictor
Code Predictor是5层小型Transformer。每个音频帧中,Talker只决定第一个
码本,Code Predictor随后执行15次自回归步骤,生成剩余15个码本。完成一帧
后,16个token的embedding相加,成为Talker下一帧的codec输入。
3. Speech Tokenizer
Speech Tokenizer包含:
24 kHz音频每个codec帧对应1920个采样点,因此帧率为12.5 Hz。
Codec Decoder的切分
最初尝试直接导出“整数codes到wav”的完整图,但ncnn在16路
unbind/embedding和整数输入路径上执行失败。最终在量化器后切分:
[1, 512, T]浮点hidden送入ncnn Codec body。NumPy量化器复现与PyTorch的最大误差低于
1e-4,从而把问题缩小为纯浮点Transformer和卷积图。
单token SDPA问题与host attention
Talker和Code Predictor的decode阶段query length为1。独立测试发现ncnn SDPA
在该输入形状下与PyTorch不一致,无法通过简单参数调整恢复。
每层因此被拆成:
其中:
这种方式绕开了有问题的算子,同时仍由ncnn承担绝大多数矩阵计算。
mRoPE结论
配置中存在
mrope_section=[24,20,20]和interleaved=true,初看需要实现视觉模型式mRoPE。但检查官方
get_rope_index后发现,TTS路径中三个位置维度始终满足
t == h == w。因此mRoPE在当前模型中退化为标准一维half-rotate RoPE。直接使用
head_dim=128、rope_theta=1e6的一维实现与官方结果一致,避免了不必要的复杂度。
文本前端与生成循环
C++ Tokenizer从Qwen Byte-Level BPE适配而来,支持:
生成循环维护:
trailing_text_hidden到tts_pad的切换;min_frames、max_frames和codec EOS;greedy模式用于严格数值对齐;sampling模式验证同seed复现和不同seed分叉。
参考音频前端
x-vector路径在C++中完成:
完整ICL路径还会通过Speech Tokenizer Codec Encoder得到101×16参考codes,
并与参考文本一起构造官方prompt。
当前Speaker Encoder图固定使用8秒输入,Codec Encoder使用8.08秒输入;不足
时补零,超出时裁剪。这是参考前端的已知限制,不影响目标语音的任意长度输出。
从101帧到任意长度Codec
早期Codec图根据golden固定为101帧,即约8.08秒。101只是当时参考音频的
长度,不是模型架构限制。
官方
chunked_decode每次处理300帧新内容,并在后续块携带25帧左上下文。如果继续使用101帧图,每块只能容纳76帧新内容。实测这种高频分块在边界处
最大误差达到
0.2586,不可接受。最终将pnnx图固定为:
C++对任意输入长度执行相同的300+25调度。最后一个不足325帧的窗口重复尾部
token进行padding;由于网络是因果结构,只保留有效前缀。377帧跨块测试相对
PyTorch的最大误差为
2.26e-4。代价是短音频也要运行325帧图,CPU计算量较大。未来可以增加多个固定shape
图做长度分流,但单图325帧首先保证了官方分块语义和任意输出长度。
一致性验证方法
项目没有只比较“听起来像不像”,而是建立逐级golden:
关键结果:
9.299e-51.423e-42.294e-42.265e-4Windows本地具备全部golden时注册34项CTest,覆盖单元、CLI错误处理、子模型
数值、持续生成、greedy/sampling、x-vector、ICL和长序列Codec。
跨平台与依赖
运行时构建采用CMake和C++17,依赖:
Python、PyTorch、transformers、qwen-tts、ModelScope和pnnx只参与模型转换和
golden生成,不参与最终运行。
已验证:
已知限制
这些限制不改变本轮核心结论:Qwen3-TTS的文本、音色条件、双Transformer生成
和Codec解码已经能够在纯C++/ncnn CPU运行时中完成,并在已覆盖的短文本、
长文本和ICL样例上与官方PyTorch保持严格一致。
复现入口
All reactions