Henry Jia

MiniMax Music 3 的三档文本编码器:代价不在音质,在时长控制

约 5 分钟显影AI

把 MiniMax Music 3 接进自己那套本地工具的时候,我卡在了一个很小的地方。ComfyUI 官方转换的权重仓里,文本编码器有三档:

minimax_music3_text_encoder_bf16.safetensors                 18.47 GB
minimax_music3_text_encoder_pruned_bf16.safetensors          16.71 GB
minimax_music3_text_encoder_pruned_int8_convrot.safetensors   9.20 GB

官方没说该选哪个。

这篇是我怎么选的,以及我第一次选错在哪。先说结论:用 bf16。但理由不是它音质更好——盲听分不出来。理由是它的时长控制明显更稳。这个区别很重要,因为它决定了 pruned 在什么场景下其实还能用。

(MiniMax Music 3 是 2026-08-13 开源的音乐生成模型,一次生成最长五分钟的完整歌,人声、编曲、混音一起出,ComfyUI 当天给了原生支持。)


一、先确认一件事:没有基线可抄

选型之前我查了一圈,官方和社区都没有给过说法。ComfyUI 官方教程(docs.comfy.org 的 Music 3 专页)只在下载清单里列了 pruned_int8_convrot,没有解释为什么是它,也没提另外两档。Comfy-Org 自己的量化文档(comfy-quants/docs/quantization/int8_tensorwise.md)只讲实现,INT8 怎么存、ConvRot 怎么旋转,明确没有质量损失对比,也没有选型建议。Hugging Face 上那个仓的 README 只有一份文件列表,零解释。社区搜不到任何三档对比的讨论。

顺带从那份量化文档里捡到一条有用的:ConvRot 的旋转只对 in_features % 256 == 0 的层生效,不满足的层退回朴素 INT8。也就是说 int8 那档里并不是所有层都受抑制离群值保护,它的质量下限比宣传听起来要低一些。

没有基线,那就只能自己查。我的第一反应是读文件。


二、读 safetensors 头:pruned 到底裁了什么

safetensors 的头是一段 JSON,用 HTTP Range 请求取前几十 KB 就能拿到,不用下整个文件。三档读下来:

tensors dtype lm_head 形状 参数量
bf16 447 全 BF16 [200000, 4096] 9.24 B
pruned_bf16 328 全 BF16 [16385, 4096] 8.36 B
pruned_int8_convrot 648 I8 160 + F32 scale 160 + BF16 167 [16385, 4096] —

328 比 447 少了 119 个 tensor,第一眼像是裁掉了整层。但把两边的 key 集合减一减,真相完全不同。消失的 202 个 key 全是这几类:

model.layers.N.self_attn.q_proj / k_proj / v_proj      × 36 层
model.layers.N.mlp.gate_proj / up_proj                 × 36 层
model.embed_tokens
model.lm_head

而 pruned 里多出来 83 个 bf16 没有的 key,长这样:

model.audio_decoder.layers.0.self_attn.qkv_proj.weight   [12288, 4096]
model.audio_decoder.layers.0.mlp.gate_up_proj.weight     [12288, 4096]

q/k/v 被融合成了一个 qkv_proj,gate/up 融合成 gate_up_proj,层数一个没少,audio_decoder 两边都是 4 层。这是推理侧的算子融合,数学上等价,而且融合 kernel 更快。

真正被裁掉的东西在另外两处:

输出层  lm_head:        [200000, 4096]  →  [16385, 4096]
输入层  embed_tokens:   [200000, 4096]  →  embed_tokens_prefill [151675, 4096]
                                        + embed_tokens_audio   [ 16384, 4096]

151675 正是 Qwen3 的词表大小。而 200000 − 151675 − 16384 = 31941,三万多个从来没被使用过的空槽位。

于是三件事各自的性质就很清楚了:

pruned 做了什么 对音乐生成有没有损失
删掉 31941 个从未使用的词表槽位 没有
输出层只留音频 codebook(16385 = 2¹⁴+1) 没有,这个编码器在管线里只吐音频 codes,不吐文本
q/k/v 与 gate/up 算子融合 没有,数学等价,而且更快
输入侧文本词表 151675 一个都没少,中文 caption 不受任何影响

推论看起来无懈可击:pruned_bf16 与 bf16 等价,省 1.76 GB,还更快,该用 pruned。

我当时就是这么定的,还把理由写进了项目文档。


三、五发 A/B 把这个推论推翻了

好在动手换档之后我还是跑了一轮对照。同一个 caption、同样 60 秒、同样的段落策略,只换编码器,五个固定 seed 一一对应,两轮都先 POST /free 清干净显存。

要的是 60 秒,实际拿到的是:

seed bf16 pruned_bf16
1010101 59.99 34.63
2020202 35.19 59.84
3030303 59.99 56.39
4040404 59.99 54.51
5050505 59.99 56.99
4/5 精确到 −0.01 秒 0/5 精确,四发短 3~5.5 秒,一发短 25 秒

bf16 也不是完美的,2020202 那发飘到了 35.19 秒。但 pruned_bf16 是一发都没落到点上。

结构上无损的三项改动,行为上伤到了长度与终止控制。

机制我不知道。合理的猜测有两个:lm_head 从 200000 裁到 16385 时,连带影响了模型在生成过程中判断该停了的能力;或者 qkv/gate_up 融合改变了浮点累积路径。但这两个我都没有证据,而且不需要有——选型这件事上,实测已经把答案给了。


四、差的是哪一维,必须说清楚

这里有个很容易滑过去的坑:上面那张表只证明了时长控制这一维。

同一批产物我拿去做了盲听,判词是听不出来。

这句话你没理由信我,所以两条都放在这儿。同一个 seed(3030303)、同一段 caption,只换编码器:

bf16:59.99 秒

pruned_bf16:56.39 秒

挑这一对是有讲究的。seed 1010101 那对是 59.99 对 34.63,一听就知道谁是谁,那不叫盲听;这一对差 3.6 秒,才真的只能靠耳朵分。

两条都从原始 .flac 用完全相同的参数转成了 mp3(libmp3lame 320k CBR,时长核对过没变),不这么说明的话,你有理由怀疑差异是转码带来的。产物由 MiniMax-Music3 生成。

我的耳朵分不出来。所以:

pruned 伤的是长度与终止控制,没有任何证据说它伤了音乐质量。

如果我把结论写成“bf16 音质更好”,那是在编造证据。这个区别不是咬文嚼字,它直接改变结论的适用范围:

场景 该用哪档
音乐要卡进片子的固定时长 bf16,这正是 A/B 赢的那一维
只要一段氛围垫底,多长都行 pruned_bf16 完全可用,省 1.76 GB
显存不够 pruned_int8_convrot(9.20 GB),代价是再叠一层量化

我最后选 bf16,是因为我的用途属于第一类,而且 1.76 GB 在一张 32 GB 的卡上不值得省。这是个跟场景绑定的决定,不是给三档权重发的成绩单。


五、顺带的几个数字

既然跑了一轮标定,一并放在这儿。同一台 AutoDL 单张 RTX 5090,bf16 编码器,每发换 seed:

要的时长 实得 耗时
30 秒 29.99 42.1 秒(含加载 23.6 GB 权重)
60 秒 59.99 54.1 秒
90 秒 89.99 81.1 秒
120 秒 119.99 108.1 秒
150 秒 118.07(seed A)/ 108.75(seed B) —

天花板是 120 秒。 超过之后行为是不确定的,同样要 150 秒,换个 seed 给出两个不同的短值,不是简单截断到 120。

有意思的是这个上限藏在默认值里:ComfyUI 那个节点的输入定义写着 max=360,而它的 default 正好是 120。声明的范围不等于能力上限,而机器可读的声明看起来比文档权威得多,所以更容易让人不设防。

产物是 .flac、44100 Hz 立体声。官方文案说 32 kHz,实测不是。


六、我从这件事上学到的

读结构能提出假设,不能代替 A/B。

我做的那轮文件分析本身没错,每一条都是对的:空槽确实没用过、输出层确实只需要音频 token、算子融合确实数学等价、文本词表确实一个没少。四条全对,结论还是错的。

因为结构等价和行为等价之间隔着一整个训练过程,而那部分不在文件头里。

所以规矩很简单:凡是同一个模型的不同档,量化也好、裁剪、融合、蒸馏也好,选型的证据只能是产物对比。参数量、tensor 形状、文件大小都只是线索,用来决定先试哪个,不能用来跳过实测。

对照跑一轮的成本是五分钟。我差点为了省这五分钟,把一个错误的推荐写进文档里长期用下去。