MiniMax Music 3 的三档文本编码器:代价不在音质,在时长控制
把 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 形状、文件大小都只是线索,用来决定先试哪个,不能用来跳过实测。
对照跑一轮的成本是五分钟。我差点为了省这五分钟,把一个错误的推荐写进文档里长期用下去。