MiniMax-H3 架构拆解 一条 5 秒带声音的视频,背后是哪些模型在干活
T AGENT 开发纪实 · EP30 · MiniMax-H3 六期原理课 ①
"134GB 权重" 不是单个模型,是一组模型的合体:text encoder 63G + 33B 参数的 DiT 62G + 双 VAE 10G。本期把它们一个个拆开看。
权重总账
134GB
DiT 参数
33B
层 DiT block
50
端到端耗时
5s→20min
💡 本节要点:MiniMax-H3 六期原理课 ①:一条 5 秒带声音的视频 = text encoder(63G)+ 双 VAE(10G)+ 33B DiT(62G)——134GB 是一组模型的合体
起点问题
四个问题:134GB 到底是什么?
- Q1 · "134GB" 是什么? · 不是一个模型,是 text encoder + 双 VAE + DiT 的合体——本期拆开看每一块在干什么。
- Q2 · 声音从哪来? · t2va 任务同时出画面 + 音频:一个 DiT 带 video / audio 两个输出头,两个 VAE 分别解码。
- Q3 · 5 秒为什么要 20 分钟? · 扩散 20 步 × 每步全序列注意力,耗时随帧数超线性增长——这是架构性成本,不是机器慢。
- Q4 · FL2VA 是什么? · Latent 空间的 Flow-based 视频 + 音频生成:先在压缩空间去噪,再解码回像素和波形。
核心矛盾: 一个"文生视频"的名字里,装着一整条多模态流水线 ——不理解它,就看不懂后面所有的坑
💡 本节要点:起点问题:'134GB' 是什么?声音从哪来?为什么 5 秒要 20 分钟?FL2VA 是什么?——四个问题指向同一个答案:一组模型的合体
设计决策
三段式全链路:编码 → 去噪 → 解码
text encoder 63G 提示词 → 文本条件 text_dim 5120 → 双 VAE 10G 视频/音频 → latent 压缩 4×(patch 1,2,2) → 33B DiT 62G 50 层,latent 空间 去噪 20 步 → 双输出头 video 96 + audio 32 去噪完的 latent 一分为二 → VAE 解码 latent → 像素 + 波形 768×448@24fps
- 🎯 · latent 空间扩散 · 不在像素空间干活:视频 latent 24 通道、音频 32 通道,patch (1,2,2) 空间压缩 4 倍——算力和内存省一个量级。
- 🧩 · 一次前向管多模态 · 视频/音频/文本 token 打包成一个 packed sequence,DiT 一次前向同时生成画面和声音的条件。
- ⚡ · CFG-distilled · 每去噪步只做一次前向(对照扩散需要两次),省一半算力——速度换的是蒸馏过的质量。
💡 本节要点:设计决策:三段式全链路——text encoder(条件)→ 双 VAE(压缩)→ DiT(latent 去噪)→ 双输出头 → VAE 解码;扩散在 latent 空间做,不在像素空间
核心代码 1 · 模型配置
从配置看规模:一个 33B 参数的宽模型
@dataclass
class MiniMaxH3DiTArchConfig:
num_layers: int = 50 # 50 层 DiT block
token_refiner_num_layers: int = 2 # 文本精炼 2 层
hidden_size: int = 5376 # 每层宽度
num_attention_heads: int = 56
attention_head_dim: int = 128 # 56 × 128 = 7168 投影
ffn_hidden_size: int = 14336 # FFN ≈ 2.7× 宽度
latents_dim: int = 24 # 视频 latent 通道
audio_latents_dim: int = 32 # 音频 latent 通道
patch_size: tuple[int,int,int] = (1, 2, 2) # 空间压缩 2×2
text_dim: int = 5120 # 文本条件维度
time_embed_dim: int = 2688 # 时间步嵌入
规模推演: hidden 5376 是"宽"模型风格(对比常见 4096),FFN 14336 接近 3 倍宽度。50 层 × 每层 qkv+fc1+fc2 三个大矩阵 → 权重约 33.1B 参数,BF16 下恰好 ~62GB,和磁盘实测一致。
💡 本节要点:核心代码 1:MiniMaxH3DiTArchConfig——50 层 / hidden 5376 / 56 heads×128 / ffn 14336 / latents 24+32 / patch (1,2,2) / text_dim 5120
核心代码 2 · 多模态打包
packed sequence:三种 token 拼成一次前向
# _embed:把三种模态拼成一个 packed sequence
视频 token: x [T_v, 24] ← video VAE latent + patch 投影
音频 token: audio_x [T_a, 32] ← audio VAE latent
文本 token: prompt [T_t, 5120] ← text encoder 条件
↓ 按顺序拼接
packed: [T_v + T_a + T_t, 5376] ← 一次 DiT 前向
cu_seqlens = [0, T_v, T_v+T_a, T_v+T_a+T_t]
# 注意力按 cu_seqlens 分段,视频/音频/文本互不串扰
为什么这样设计: 多模态一次前向 → 去噪时画面和声音共享同一套条件(文本、时间步),一个 DiT 同时生成 96 维视频 latent 和 32 维音频 latent——这是"5 秒视频自带声音"的来源。
注:代价:变长序列需要变长注意力内核 → 埋下 FA4 编译失败的坑(Step 8 / EP32 展开)。
💡 本节要点:核心代码 2:packed sequence——视频 latent + 音频 latent + 文本 embedding 拼成一个序列,cu_seqlens 分段,注意力不跨文档
核心代码 3 · DiT block 内部
每个 block:AdaLN 调制 + 3D RoPE + 门控残差
class MiniMaxH3DiTBlock(nn.Module):
# AdaLN:每 block 出 6 个调制向量(3 模态 × 6 组)
shift_msa, scale_msa, gate_msa, \
shift_mlp, scale_mlp, gate_mlp = self.adaln_proj(t_emb)
residual = x
h = self.norm1(x)
h = _modulate_scale_shift(h, shift_msa, scale_msa, idx) # x*(1+s)+shift
h = self.attn(h, rope_freqs=rope_freqs, ...) # 3D RoPE 96/128 维
x = _modulate_gate(residual, gate_msa, h, idx) # x + gate*h
residual = x
h = self.norm2(x) # 同样走一遍 MLP
h = self.mlp(h)
return _modulate_gate(residual, gate_mlp, h, idx)
理解: 时间步 + 文本条件通过 AdaLN 变成每个 block 的 shift/scale/gate——这就是"条件怎么控制生成"。3D RoPE 把 (t, h, w) 位置编码进注意力,所以视频知道每一帧该长什么样。
💡 本节要点:核心代码 3:MiniMaxH3DiTBlock——AdaLN 出 6 个调制向量(shift/scale/gate × attn/mlp)+ 3D RoPE(t,h,w 旋转 96/128 维)+ gated residual
理解型踩坑 ① · 账单之谜
text encoder 比主模型还大——文本条件是"大模型级"的
| 组件 | 大小 | 参数/形态 | 角色 |
|---|---|---|---|
| text_encoder/ | 63 GiB | 30B+ 级文本模型 | 提示词 → 5120 维文本条件 |
| transformer/ | 62 GiB | 33.1B 参数(BF16) | 50 层 DiT 去噪主体 |
| video_vae/ | 9.8 GiB | 视频压缩器 | 像素 ↔ 24 通道 latent |
| audio_vae/ | 578 MiB | 音频压缩器 | 波形 ↔ 32 通道 latent |
| ▌ text encoder · 63 GiB | |||
| ▌ DiT · 62 GiB | |||
| ▌ 双 VAE · 10.4 GiB | |||
| 认知反转: "文生视频"的大头不在"视频模型",而在"文本模型"。134GB 里文本条件占了近一半——这也是 128GB 设备装 134GB 权重时最头痛的部分(EP31 的 FP8 量化正是从这 63GB 开始省的)。 | |||
| > 💡 本节要点:踩坑 1:134GB 账单之谜——text encoder(63G)比主 DiT(62G)还大;video VAE 9.8G + audio VAE 578M;文本条件是'大模型级'的 |
理解型踩坑 ② · 变长序列的代价
打包的代价:变长注意力内核编不出来
视频/音频/文本 打包成变长序列 → FlashAttention-4 CuTe 变长内核 → SM121 编译失败 → 换 PyTorch SDPA 按 cu_seqlens 分段
架构性坑: packed 设计(Step 5)换来一次前向多模态,代价是注意力必须支持"一条序列里多段文档"。FA4 的 CuTe 变长内核在 GB10 上编不出来——这不是配置问题,是架构选择撞上了硬件限制。
修复思路: SDPA 的 _sdpa_varlen_attention 按 cu_seqlens 逐段做注意力,语义完全等价、可编译。性能略降,但正确性不妥协。完整排障链放 EP32/33。
注:这个坑的教训:多模态打包是有代价的——架构每省一次前向,推理后端就多一个必须支持的特性。
💡 本节要点:踩坑 2:packed 变长序列的代价——三种 token 打包 → 变长注意力 → FlashAttention-4 的 CuTe 变长内核在 SM121 编译失败 → 必须换 SDPA
理解型踩坑 ③ · 扩散的"慢"
5 秒视频等 20 分钟:注意力是 O(T²) 的
| 请求 | 帧数 | 相对帧数 | 端到端耗时 | 相对耗时 |
|---|---|---|---|---|
| 2s demo | 56 | 1× | 298s | 1× |
| 5s demo | 124 | 2.2× | 1235s | 4.0× |
| - 20 步扩散 · 每步一次完整前向(CFG-distilled 已省一半)——20 步 × 全序列。 | ||||
| - O(T²) 注意力 · 帧数翻倍 → 注意力矩阵 4 倍。视频扩散的序列比语言模型长一个量级。 | ||||
| - 不是机器慢 · GPU 满载 92-95%、温度余量充足——慢是数学决定的,换更好的硬件只缩常数。 | ||||
| 推论: 2s→5s 帧数 2.2×、耗时 4.0× 的超线性增长是架构性成本——10s 外推约 55-70 分钟,且内存是硬约束(EP35 基准展开)。 | ||||
| > 💡 本节要点:踩坑 3:扩散的'慢'是架构性的——CFG-distilled 已省一半,但 attention O(T²):帧数 2.2× → 耗时 4.0×(2s→298s、5s→1235s) |
横向话题 · 开源组件
架构代码从哪来:vLLM-Omni 的开源栈
- 📦 · vllm-project/vllm-omni · MiniMax H3 transformer 模块的上游来源;Apache-2.0;架构代码在此,SM121 兼容补丁是二次修改。
- 🧬 · SGLang 参考 + vLLM 并行 · 架构数值契约来自随 checkpoint 发布的 SGLang 扩散参考;vLLM 的 TP 线性层 + Ulysses/Ring 序列并行预留。
- 🛠️ · CacheDiT + PR 5691 · CacheDiT 缓存块复用(block_forward_patterns);H3 支持落地于 vllm-omni PR 5691 + 官方 recipe。
vllm-omni 镜像 → H3 架构代码(开源) → SM121 兼容补丁(本项目) → 在线 FP8 + SDPA 跑通
启示: 架构本身是开源的——33B DiT 的每一行都能在 GitHub 上读到。补丁只做兼容层,不改架构、不改 checkpoint 布局——这是"最小改动"原则(EP33 展开)。💡 本节要点:横向话题:架构代码来自 vllm-project/vllm-omni(Apache-2.0,SGLang 参考 + vLLM TP 并行)+ upstream H3 recipe + PR 5691;CacheDiT 缓存 / SP/TP 并行预留
验证数据 · 架构实测
拆解账本 + 生成基准:数字都对得上
33.1B
DiT 参数(BF16 = 62GB)
89.17 GB
模型加载内存(532s)
7.75s
平均每去噪步耗时
4.0×
帧数 2.2× → 耗时 4.0×
| 验证项 | 结果 |
|---|---|
| 模型加载 | 89.1659 GiB / 532.257s(shard 加载 154.75s,13/13 shards) |
| 2s T2VA 请求 | 154.956s 端到端(mean denoising step 7.748s) |
| 5s T2VA 请求 | ~1235s 端到端;124 帧 H.264 软编 55s(无 NVENC) |
| 产物校验 | 768×448@24fps,H.264 + AAC 32kHz stereo,full decode 通过 |
注:口径:加载 89.17 GiB 是模型参数内存;请求后 worker 分配 92,957 MiB——134GB 原始权重靠 FP8 量化压下来(EP31 讲实现)。
💡 本节要点:验证:134GB 拆解(text 63G / DiT 62G / 双 VAE 10G);模型加载 89.17 GiB / 532s;2s→298s、5s→1235s;124 帧软编 55s
总结
"文生视频"不是一个模型,是一整条流水线
—— text encoder 63G + 33B DiT 62G + 双 VAE 10G,文本条件是大头
134GB 拆解
—— 编码(VAE 压缩 4×)→ 去噪(50 层 DiT in latent)→ 解码(双输出头)
三段式架构
—— 视频/音频/文本一次前向,cu_seqlens 分段,声音画面同源
packed 多模态
—— 20 步 × O(T²) 注意力:5s 要 20 分钟,不是机器不行
慢是架构性的
理解了流水线,下一个问题就来了: 128GB 到底怎么装下 134GB?
- 📐 · EP30 · 模型原理 · 本期:流水线拆解 ✅
- 🧮 · EP31 · 量化原理 · 下期:在线动态 FP8——从 63GB 文本条件开始省
- 🖥️ · EP32 · 硬件适配 · 预告:GB10 统一内存与 SM121 的四个"不支持"
本期证据:patches/minimax_h3_transformer.py(架构代码)· 本地权重目录实测(134GB 拆解)· docs/RESULTS.md · generation-benchmark.md
💡 本节要点:总结:134GB 拆解 → 三段式架构 → packed 多模态 → 扩散超线性慢;下期 EP31:128GB 怎么装下 134GB——在线动态 FP8 量化原理