上一篇用三张地图梳理了全双工语音对话系统的设计空间,Moshi 在每张地图上都是绕不开的坐标:token 级双工(L2)的开创者、「并行多流」流派的代表、五状态全可达的少数系统之一。那篇是横向比较,这篇换成纵向解剖——把 Moshi(Kyutai,2024 年 9 月,arXiv:2410.00037)从 codec 到训练配方一层层拆开,必要处直接看开源实现的代码。

先把结论放在前面:Moshi 真正的贡献不是「延迟低」,而是把口语对话整个变成了一个语言建模问题。没有 VAD、没有轮次切分、没有「用户说完了吗」的判断模块——用户在说、自己在说、双方沉默、互相重叠,全都只是同一个序列模型笔下不同的 token 组合。

一、级联管线到底卡在哪

传统语音助手是一条流水线:VAD 检测你说完了 → ASR 转文字 → LLM 生成回答 → TTS 合成语音。论文开篇点了它的三宗罪:

  1. 时延叠加。 每个组件都要等上游出完整结果,全程好几秒;而真人对话的响应间隔,跨十种语言测量的平均值是 230 毫秒。
  2. 文本瓶颈。 对话的中间表示是文字,情绪、口音、笑声、迟疑、环境音——一切写不成字的信息都在 ASR 那一步被扔掉了。
  3. 轮次假设。 流水线预设「一人说完另一人再说」,但真实对话里 10–20% 的时间是重叠语音,还有大量「嗯」「对」这类不抢话的附和(backchannel)。级联系统在结构上就表达不了这些。

这三个问题同根同源:对话被表示成了文字轮次的序列,而不是两条同时流动的声音。Moshi 的解法是釜底抽薪——直接建模声音本身。

二、总览:三个组件,一张总谱

Moshi 由三个部件组成,各自对应论文的一章:

  • Helium:7B 参数的文本 LLM 底座,从零预训练 2.1T token,提供知识和推理能力;
  • Mimi:流式神经音频 codec,把 24 kHz 波形压成每秒 12.5 帧、每帧 8 个离散 token,再无损地还原回声音;
  • RQ-Transformer:生成架构。大号时间 Transformer(Helium 初始化)沿时间轴走,小号深度 Transformer 在每个时间步内部把当步的所有 token 一口气出齐。
用户 24 kHzMimi编码器每帧8 tokenMoshi 语言模型时间 Transformer7B · Helium 初始化z_s深度 Transformer6 层 · 步内出 17 token每 80 ms 前进一步(12.5 Hz)文字流(内心独白)×8Mimi解码器Moshi 的声音自己的 1 + 8 个 token,下一步回喂输入每步输入 = 17 个 token 的嵌入相加(自己 9 条 + 用户 8 条)
Moshi 总览:用户与 Moshi 各占一条音频 token 流,加上 Moshi 的文字流;时间 Transformer 沿时间轴自回归,深度 Transformer 在每步内部展开

推理主循环出乎意料地朴素(摘自 moshi/run_inference.py,略有删减):

frame_size = int(mimi.sample_rate / mimi.frame_rate)   # 24000 / 12.5 = 1920 采样点 = 80ms

for chunk in audio.split(frame_size):     # 每 80ms 一帧
    codes = mimi.encode(chunk)            # [B, 8, 1] 用户这帧的 8 个 token
    tokens = lm_gen.step(codes)           # 喂进去,采样出 Moshi 这帧的 1+8 个 token
    if tokens is not None:
        out_pcm = mimi.decode(tokens[:, 1:])   # 后 8 个还原成 80ms 声音
        text_id = tokens[0, 0]                 # 第 0 个是文字 token

每 80 毫秒转一圈:收一帧用户的声音,吐一帧自己的声音,外加一个文字 token。没有状态机、没有打断检测——「让不让用户说话」这件事,全部溶解在 lm_gen.step 的概率分布里。

下面按数据流动的顺序,把三个部件依次拆开。

三、Mimi:把 80 毫秒的声音压成 8 个整数

语音 tokenizer 的通用背景(语义 vs 声学的矛盾、RVQ 原理、各家路线)在语音 tokenizer 一篇里讲过,这里聚焦 Mimi 自己的取舍。

3.1 帧率账:为什么必须是 12.5 Hz

Mimi 的编码器是 SeaNet 因果卷积栈:四级降采样(步幅 4、5、6、8)把 24 kHz 压到 25 Hz,瓶颈处再加一个步幅 2 的卷积到 12.5 Hz,每帧对应 80 毫秒、一个 512 维向量。瓶颈两侧各放一个 8 层因果 Transformer(上下文约 10 秒),量化用 8 本码书、每本 2048 个条目——总码率 1.1 kbps。

12.5 Hz 不是随手选的。时间 Transformer 是个 7B 模型,每生成一帧音频就要做一次完整前向,帧率直接决定了它每秒要跑几次。50 Hz 的 codec(SpeechTokenizer、大多数 HuBERT 系)意味着每秒 50 次 7B 前向,实时无从谈起;12.5 Hz 把预算压到每秒 12.5 次,才给实时留出了空间。反过来,帧率越低每帧要塞的信息越多,重建质量越难保——Mimi 用瓶颈 Transformer 和下面两个训练技巧把质量拉了回来。

3.2 split RVQ:语义和声学分家

和 SpeechTokenizer 一样,Mimi 把自监督语音模型(WavLM)的表示蒸馏进第一本码书,让它专管「说了什么」;但结构上有个关键改动。SpeechTokenizer 是串行残差:第一层管语义,后面的层量化第一层的残差——于是声学信息被迫住在「语义的余数」里,两边互相拖累。Mimi 改成并联:一个单独的语义 VQ 和一条 7 层的声学 RVQ 各自量化同一个输入向量,输出相加。

# moshi/quantization/vq.py — SplitResidualVectorQuantizer.forward(简化)
semantic = self.rvq_first(x, frame_rate)    # 1 层 VQ,蒸馏 WavLM,管内容
acoustic = self.rvq_rest(x, frame_rate)     # 7 层 RVQ,管音色与细节
quantized = semantic.x + acoustic.x         # 并联相加,而非串行残差
codes = torch.cat([semantic.codes, acoustic.codes], dim=1)   # [B, 8, T]
编码(全因果,可流式)24 kHzSeaNet 卷积栈步幅 4·5·6·8→ 25 HzTransformer ×8因果 · 上下文 10 s卷积 ↓212.5 Hz每 80 ms 一个512 维向量split RVQ:并联,不是串行残差语义 VQ · 1 本码书声学 RVQ · 7 本码书WavLM仅训练时余弦距离蒸馏量化输出:每帧 8 个 token(1 语义 + 7 声学)8 × 11 bit × 12.5 Hz ≈ 1.1 kbps解码Transformer ×8因果SeaNet 反卷积↑2 · 8·6·5·424 kHz训练:只用对抗 + 特征匹配损失(无重建损失);50% 概率跳过量化直通解码器
Mimi 架构:因果 SeaNet 编码器 + 瓶颈 Transformer;量化改为并联的 split RVQ——语义 VQ(蒸馏 WavLM)与 7 层声学 RVQ 各自量化同一向量后相加

消融数据很直观:不蒸馏时语义码书的音素可分性(ABX 错误率,越低越好)只有 23.3%,蒸馏进单条 RVQ 第一层降到 6.5%,但重建质量(MUSHRA 主观分)从 65.9 掉到 57.8;改成 split 并联后,ABX 8.1%、MUSHRA 64.0——两头都保住了大半。

3.3 反直觉的发现:只用对抗损失,音质更好

神经 codec 的标配是「重建损失 + 对抗损失」。Mimi 试了把多尺度梅尔谱重建损失整个扔掉、只留判别器的对抗损失和特征匹配损失,结果客观指标暴跌(VisQOL 从 2.82 掉到 1.84),人耳评测却大涨——MUSHRA 从 58.8 升到 81.0,反超码率更高、帧率 6 倍于它的 RVQGAN(31.3)。论文顺带发出感慨:音频客观指标和人耳感受的相关性差到值得警惕。

另外两个细节:训练时 50% 的概率完全跳过量化、把未量化的连续向量直接喂给解码器(提升低码率下的质量);开源发布的 Mimi 权重其实带 32 层码本(训练用了量化器 dropout,码率可伸缩),Moshi 只取前 8 层。

四、RQ-Transformer:一个时间步里的两级自回归

4.1 先算一笔序列长度的账

一帧 8 个 token,摊平成一条序列就是每秒 100 个 token——5 分钟对话 30,000 步,一个 7B 模型每秒还得前向 100 次,长度和算力双重爆炸。对比之下英语语音的文字转写每秒才 3–4 个 token。多码本怎么在自回归模型里安放,是所有音频 LM 的必答题(语音 tokenizer 一篇列过四种方案)。

Moshi 的答案是 RQ-Transformer(源自 Lee et al. 2022 的图像生成工作,UniAudio 等也用过):把「序列 × 深度」这个二维网格拆给两个模型——

  • 时间 Transformer(7B,Helium 架构):每个时间步只跑一次,输入是上一步全部 17 个 token 的嵌入之和,输出一个上下文向量 z_s;
  • 深度 Transformer(6 层、1024 维,约 1 亿参数):以 z_s 为条件,在时间步内部沿深度方向自回归——先出文字 token,再出语义 token,再逐层出声学 token,每个新 token 喂回去预测下一个。
时间轴:每步一次 7B 前向步 s−2步 s−117 个 token嵌入相加步 s(待生成)时间 Transformer(7B)上下文 3000 步 ≈ 4 分钟z上下文向量 z_s帧率 12.5 Hz:7B 模型每秒只前向 12.5 次,而不是摊平后的每秒 100 次深度轴:步 s 内部逐个出 token深度 Transformer(6 层 · 每深度独立权重)① 文字 token② 语义 token③ 声学 token ×7采出一个,喂回去出下一个
RQ-Transformer:时间 Transformer 沿时间轴每步一次前向,产出上下文向量 z;小号深度 Transformer 以 z 为条件,在步内沿深度方向把 K 个 token 逐个采样出来

于是 7B 模型的步数回到 12.5 Hz,深度方向的 K 次预测交给小模型——它小到在整个推理预算里几乎可以忽略。流式推理里这个分工长这样(models/lm.pydepformer_step,简化):

prev_token = text_token                      # 深度方向从文字 token 起步
with lm_model.depformer.streaming(B):
    for cb_index in range(8):                # 语义 1 层 + 声学 7 层
        logits = lm_model.forward_depformer(cb_index, prev_token, transformer_out)
        prev_token = sample_token(logits, temp=0.8, top_k=250)
        depformer_tokens.append(prev_token)  # 采出一个,喂回去出下一个

一个容易忽略的细节:深度 Transformer 对每个深度位置用独立的一套权重(投影层、嵌入、前馈都不共享,depformer_weights_per_step)。文字→语义→声学各层的「翻译任务」本就不同,参数分家后各干各的;模型够小,多几套权重无关痛痒,消融里这一手实打实提升了质量。

4.2 声学延迟:让声学 token 晚一拍

把 8 个码本直接按同一时刻对齐建模,生成并不稳定。Moshi 引入声学延迟:声学 token 相对语义 token 整体后移 τ 帧(预训练 τ=2,微调后 τ=1)。直觉是:同一帧的语义和声学 token 相关性极强,硬要深度 Transformer 在一步内把它们全预测对,等于逼一个小模型拟合复杂的联合分布;错开一两帧后,「这一帧说什么」由大模型先定,声学细节参考着已经出炉的语义慢慢补,相关性被时间差解耦了。

消融给了很强的证据:延迟模式 [0,2,…,2] 下,不用 RQ-Transformer(8 个独立分类头并行预测)困惑度 135.4,用了降到 36.8;[0,0,…,0](零延迟,理论时延 80ms)比 [0,1,…,1](160ms)的生成质量差一截,而再放宽到 [0,2,…,2](240ms)收益就不大了。最终模型选 τ=1:理论时延 = 80ms(攒一帧)+ 80ms(声学延迟)= 160ms,实测约 200ms——低于真人对话的 230ms 平均间隔。

五、多流与内心独白:17 行 token 总谱

到这里还只是「单人音频的语言模型」。Moshi 的两个招牌设计——多流建模和 Inner Monologue——都发生在「每个时间步建模哪些子序列」这一层。

5.1 多流:对话就是两条并行的音频

对话有两个声源,Moshi 就建两组音频流:自己的 8 个码本一组,用户的 8 个码本一组,同一时间步竖着摞在一起。轮次的概念从输入表示里彻底消失了——重叠就是两条流同时有语音,附和就是短促的插入,沉默就是双方都输出「自然静音」。训练时两条流都算损失(模型学的是整场对话的联合分布);推理时自己的流采样、用户的流用真实麦克风输入覆盖。这带来一个妙用:让模型把用户流也采样出来,就能凭空生成整场双人对话,论文用它做离线评测。

5.2 Inner Monologue:先想文字,再发声音

纯音频域建模已经能对话,但语言质量平平。Moshi 再往总谱顶上加一行文字流:模型自己说话内容的转写(Whisper 打词级时间戳),按 12.5 Hz 对齐铺到时间轴上——每个词的 token 放在词的起始帧,词与词之间填 PAD,下一个词开始前一帧放一个 EPAD 标记「padding 结束」。英语口语里 PAD 约占 65%。

每个时间步内,深度方向的预测顺序是文字 → 语义 → 声学:模型先用文字敲定「接下来说什么」,语义 token 在文字的条件下生成,声学 token 再落地成音色。这是 AudioLM「先语义后声学」层级的自然延伸——把文字当成了第零层、最粗粒度的语义 token。它和 SpeechGPT、Spectron 那类「先写完整段文字再朗读」(Chain-of-Modality)的本质区别在于:文字和音频逐帧交错,每一步都同时出文字和声音,所以流式毫不受损。

注意一个不对称:只有 Moshi 自己的流配文字,用户流没有——给用户的声音配文字等于要求内置一个实时 ASR,违背端到端的初衷。

两样拼起来,每步 K = 1 + 8 + 8 = 17 个 token,开源代码里那行配置就是整篇论文的浓缩(models/loaders.py):

"n_q": 16, "dep_q": 8,          # 16 条音频流;深度 Transformer 只采样自己那 8 条
"delays": [0,                    # 文字流
           0, 1, 1, 1, 1, 1, 1, 1,   # Moshi:语义 0 延迟,声学 7 层各延迟 1 帧
           0, 1, 1, 1, 1, 1, 1, 1],  # 用户:同样的排布
时间步(80 ms 一格)→12345678文字 WHelloPADEPADhowareyouPAD语义 A₁(自己)帧1帧2帧3帧4帧5帧6帧7帧8声学 A₂(自己)帧1帧2帧3帧4帧5帧6帧7← 晚 1 帧(τ=1)A₃ … A₈ 同上⋮(共 7 条声学流,排布相同)虚线上方:模型采样虚线下方:麦克风输入语义 A′₁(用户)帧1帧2帧3帧4帧5帧6帧7帧8声学 A′₂(用户)帧1帧2帧3帧4帧5帧6帧7A′₃ … A′₈ 同上同一步内,深度 Transformer自上而下依次生成 17 个文字流对齐:词的 token 放在词起始帧,词间填 PAD,新词前一帧放 EPAD;英语口语中 PAD 约占 65%理论时延 = 80 ms(攒一帧)+ 80 ms(声学延迟)= 160 msK = 1 文字 + 8 自己 + 8 用户 = 17 条子序列;没有轮次标记,重叠与打断天然可表示
Moshi 的联合序列(每列一个 80ms 时间步):文字流 + 自己的语义/声学流 + 用户的语义/声学流,声学整体右移 1 帧;虚线以下推理时由采样产生,以上来自用户麦克风

实现上,延迟不是真的把序列错开存储,而是一个环形缓存的读写偏移:用户 token 写进 (offset + delay) % CT 的位置,每步统一从 offset 读——写得越「远」,被读到的就越晚。想让 Moshi 立即开口也有了外科手术式的手段:在文字流上强制采一个 EPAD,下一帧它就得说话。

5.3 它为什么值得:消融数字

Inner Monologue 是全文杀伤力最大的设计。同等条件下加上它,生成语音转写的 NLL 从 3.65 降到 2.77,平均生成长度从 602 字符跳到 1920——不加的模型说着说着就沉默坍缩了。口语问答上更夸张:Web Questions 9.2 → 26.6,Llama Questions 21.0 → 62.3,TriviaQA 7.3 → 22.8,全线接近三倍。而代价只是每步 17 个而不是 16 个 token——推理开销几乎不变。

六、改一个延迟参数,就得到流式 ASR 和 TTS

文字流和音频流对齐铺在同一条时间轴上,还送了一个意外彩蛋。给文字流也加个延迟,控制「谁先落子」:

  • 文字滞后音频 2 秒 → 流式 ASR。音频 token 用真实输入 teacher-force,只采样文字流:模型「听」了两秒再落笔,写出的就是转写——还自带 80ms 精度的词级时间戳。
  • 文字超前音频 2 秒 → 流式 TTS。文字流用给定文本 teacher-force,采样音频流:声音跟着两秒前的文字走,就是 TTS。
对话模式文字延迟 0文字采样音频采样同一步内文字先出、声音后出:先想好词,再发出声ASR 模式文字滞后 2 s文字采样 → 转写音频外部输入(teacher-force)2 s听了两秒再落笔,附带 80 ms 精度词级时间戳TTS 模式文字超前 2 s(音频滞后)文字外部输入(待合成文本)音频采样 → 语音2 sPAD/EPAD 让模型自由采样,节奏自己定,词由外部喂外部输入(teacher-forced)模型采样同一套架构、损失与训练流程——只挪一个延迟参数
同一套架构、损失和数据,只挪文字流的延迟:对话模式(0)、ASR 模式(文字滞后)、TTS 模式(文字超前)

TTS 模式有个精巧的推理技巧:输入文本没有 PAD/EPAD 信息,怎么知道每个词该占几帧?答案是让模型在 PAD 和 EPAD 上自由采样——它想采别的 token 时,就强行替换成待合成文本的下一个词。节奏由模型自己掌握,词的内容由外部指定;还可以监控 PAD 占比、微调其 logits 来控制语速。

这不是纸面推演:Moshi 的指令微调数据——两万多小时合成对话——就是用这个 TTS 模式(音频延迟 2 秒的多流版本,在 170 小时真实双声道对话上微调)生成的。同一套架构闭环自举,是全文工程美感最强的一笔。

七、训练配方:从 700 万小时到一个会聊天的声音

Moshi 的能力是四个阶段垒起来的,每阶段解决一个明确的问题:

Helium文本预训练2.1T token50 万步① 音频预训练700 万小时 · 单流100 万步 · 5 分钟序列50% 步数穿插纯文本文字延迟 ±0.6 s 抖动学会「说话」② 多流后训练说话人分离拆双流主讲人 / 其余人10 万步模拟数据,无真实重叠学会「两条流」③ Fisher 微调2000 小时电话对话双声道分轨录音1 万步真实打断与附和学会「全双工」④ 指令微调2 万+ 小时合成对话3 万步噪声/回声增强定音色与人设多流流式 TTS音频延迟 2 s 的同款架构170 h 真实双声道对话微调Helium 写脚本 → TTS 配音Moshi 音色:一位演员 70+ 种语气声学延迟:预训练 τ=2,其后 τ=1;文字延迟固定为 0(对话模式)
四阶段训练:单流音频预训练学「说话」,模拟多流后训练学「两条流」,Fisher 微调学「真实对话动力学」,指令微调定人设——全程一半时间穿插文本批次防遗忘
  1. 音频预训练(100 万步)。700 万小时音频(Whisper large-v3 转写),单流、5 分钟长序列。关键的防遗忘设计:一半训练步仍是纯文本批次(用 Helium 的数据和独立的优化器状态)——消融显示去掉这一半,口语问答明显下滑。文字与音频的延迟在 ±0.6 秒间随机抖动,算是对齐的正则化。
  2. 多流后训练(10 万步)。用 PyAnnote 说话人分离把单声道音频拆成「主讲人 / 其余人」两条流,教会模型双流格式。但这种模拟数据没有真实重叠,不活跃一侧是纯零静音——离真对话还差关键一步。
  3. Fisher 微调(1 万步)。Fisher 语料:2000 小时、2003–2004 年采集的双声道电话对话(8 kHz,用 AudioSR 超分到 24 kHz),有真实的打断、抢话和附和。全双工的行为能力就是在这一步学到的。
  4. 指令微调(3 万步)。两万多小时合成对话(见上节):Helium 生成脚本、自家 TTS 配音。Moshi 的声音固定为一位配音演员(录了 70 多种语气风格的独白),用户侧则每条随机换声线。数据里埋了不少心思:误导性问题(「埃菲尔铁塔在北京吗?」)训练它说「不」;故意拼错的词训练它请用户重复;数学、语法、常识单轮问答补短板;安全对话教它拒答。用户流还叠加了随机增益、噪声、混响,甚至模拟 Moshi 自己的声音从用户麦克风漏回来的回声(延迟 100–500ms、幅度 0–0.2)——不做这一手,实际部署时它会被自己的回声打断。

损失函数上,文字流独占一半权重,音频那半里语义 token 权重 100、声学 token 权重 1——层级越靠上越金贵。

八、效果与代价:几组关键数字

口语问答(音频进、音频出):Web Questions 26.6 / Llama Questions 62.3 / TriviaQA 22.8,大幅超过 SpeechGPT(6.5 / 21.6 / 14.8)和 Spectron(6.1 / 22.9 / –),而后两者还不支持流式。天花板是文本模式的 Helium(32.3 / 75.0 / 56.4)——差距就是「用声音思考」的税。TriviaQA 掉得最狠,论文归因于指令数据全是口语风格,书面长难句见得太少。

知识保持:MMLU 从 Helium 的 54.3 降到 Moshi 的 49.7。音频训练侵蚀文本知识是全行业通病,Moshi 靠「一半文本批次」把损失控制在 5 分以内(对比 Spirit-LM 的 36.9,高了近 13 分)。

对话动力学:让 Moshi 自己生成双方对话,统计轮换指标(温度 1.0):发声段均长 50.8s vs 真实数据 51.1s,停顿 7.0s vs 6.4s,轮间静默 4.5s vs 4.2s,重叠 4.1s vs 3.3s——四项全部贴近 Fisher 真实分布,而上一代全双工模型 dGSLM 的对话语义困惑度是它的 2.5 倍。轮换节奏不是规则写出来的,是从数据里学出来的

流式 ASR / TTS(2 秒延迟版):ASR 在 LibriSpeech test-clean 5.7% WER,TTS 4.7% WER——不是 SOTA(专职流式 ASR 约 3.6%),但它俩和对话模型是同一个架构、同一个损失函数,只差延迟参数。

量化部署:线上 demo 用 8-bit 权重(MMLU 掉约 2 分);压到 4-bit 后音质几乎无损,但 MMLU 掉 5–10 分——先崩的是语言能力,不是声音。2-bit 时则出现胡言乱语、复读、静音退化成背景噪声等典型伪影,论文附录还给了一套用 token 熵谱自动检测这些伪影的方法。

九、放回地图:Moshi 之后

回到全双工那篇的框架看,Moshi 的位置很清楚:双工决策在 token 流内部(L2),理论上五种对话状态全可达;打断用户不需要任何专门模块——它时刻在预测用户流,「用户开口了」这个事件被建模本身吸收,自己让位就是把输出流坍缩成自然静音。同时它的局限也在地图上:只会英语、知识弱于同尺寸文本模型、上下文 3000 步约合 4 分钟;而行为风格深度绑定训练数据——全双工能力主要来自二十年前的 Fisher 电话语料,这也是那篇综述「数据格子覆盖决定行为」论断的最佳注脚。

工程遗产同样可观:Mimi 成了低帧率流式 codec 的事实标杆(12.5 Hz 后来被 MOSS-Audio-Tokenizer 等一串工作沿用);「时间 + 深度」双 Transformer 和文字先行的思路在后续语音模型里随处可见;Kyutai 自己则把这套架构继续衍生——同声传译模型 Hibiki、生产级流式 STT/TTS,都是同一份总谱换不同的延迟与流配置。

回头看,Moshi 最漂亮的地方在于它的「减法」:没有为全双工添加任何专门机构——没有打断检测器、没有轮次管理器、没有状态机。它只是把对话如实地表示成几条并行的 token 流,然后相信一个足够好的序列模型能学会其中的一切。后来的事实证明,这个信任是值得的。

参考资料