# MiniMax-H3 视频生成部署(4 × RTX 2080 Ti) 在 `192.168.110.67` 上用 DiffSynth-Studio 跑 MiniMax-H3 FL2VA(文生视频 + 原生立体声)。 > **状态(2026-08-18)**:环境已装好并通过全部兼容性实测;模型下载中(33 GB / 6 MB/s); > 尚未完成首次出片。验收结果见本文末「验收记录」,未填即表示还没跑通。 服务器侧根目录 **`/data/models/minimax-h3/`**,本目录是其脚本副本 + 决策记录。 --- ## 一、为什么不能照抄官方示例 官方 [MiniMax-H3.md](https://github.com/modelscope/DiffSynth-Studio/blob/main/docs/en/Model_Details/MiniMax-H3.md) 的示例在这台机器上**跑不起来**,有三处必须改。 ### 1. `torch.bfloat16` → `torch.float16`(致命) 官方示例的 `vram_config` 与 `torch_dtype` 全用 bf16。sm_75 上实测: ``` [ERR] SDPA bfloat16 EFFICIENT_ATTENTION → RuntimeError: No available kernel [ERR] SDPA bfloat16 FLASH_ATTENTION → RuntimeError: No available kernel [OK ] SDPA bfloat16 MATH → 只剩朴素实现 [OK ] SDPA float16 EFFICIENT_ATTENTION → 可用 ``` bf16 不是"慢一点",是**没有任何高效 attention kernel**。PyTorch 会静默掉到 MATH 后端, 在 H3 的 ~14,100 token 序列上要物化 `14100 × 14100 × 56` 的注意力矩阵,必然 OOM。 FP16 路径实测(真实 H3 形状 S=14100 H=56 D=128): ``` SDPA EFFICIENT fp16 → OK, 0.21s fp16 GEMM 有效算力 → 54.3 TFLOPS (2080Ti fp16 张量核满血) fp16 深链数值稳定性 → finite ``` **注意**:`llm-setup.md` 说 "Turing…不支持 bf16,跑 GGUF 走 llama.cpp 是唯一实际路径" —— 前半句对,后半句对扩散模型不成立。PyTorch + bitsandbytes NF4 在 sm_75 上是通的,见下。 ### 2. `vram_limit` 取错了下标(会 OOM) ```python vram_limit=torch.cuda.mem_get_info("cuda")[1] / (1024 ** 3) - 2 # 官方:[1] = 总显存 vram_limit=torch.cuda.mem_get_info("cuda")[0] / (1024 ** 3) - 2 # 本机:[0] = 空闲显存 ``` `mem_get_info()` 返回 `(free, total)`。这台机器四张卡都有常驻服务(llm38 / embedding / SD / whisper), 按总显存 21.5G 要预算,实际只有 12–15G 可用,必 OOM。 ### 3. `offload_device="disk"` 不能用 `model_inference_low_vram/` 那组示例把权重 offload 到磁盘。本机 `/data` 是 **7200rpm 机械盘**(HGST HUS726020ALA610,实测顺序读 184 MB/s)。 NF4 DiT 16 GB,每个去噪步都要过一遍,光 I/O 就 **87 秒/步**。 改用 CPU 内存 offload:物理内存 128 GB,可用 123 GB,单 worker 常驻约 32 GB,宽裕。 --- ## 二、attention backend:只用 PyTorch SDPA | Backend | sm_75 | 处置 | |---|---|---| | FlashAttention 2 / 3 / 4 | ❌ 仅 sm_80–sm_121 | **不装** | | SageAttention | ❌ 需 sm_80+ | **不装** | | xFormers | ⚠️ 系统已装 0.0.33,fp16 可用 | 不用(见下) | | PyTorch SDPA | ✅ EFFICIENT(cutlass)后端 | **采用** | DiffSynth 的 `diffsynth/core/attention/attention.py` 里有自动优先级, **xformers 排在 torch 前面会被自动选中**。实测: ``` 不设环境变量 → selected = xformers DIFFSYNTH_ATTENTION_IMPLEMENTATION=torch → selected = torch ``` 两者 fp16 下都能跑,但锁死 `torch` 保证可复现。`run.sh` 已导出该变量。 H3 的 DiT 只用最干净的 attention 调用(无 mask / 无 causal / 无 sliding window, 见 `minimax_h3_dit.py:79`),不会踩 flex_attention 需要 torch.compile 的坑。 --- ## 三、NF4 量化在 Turing 上成立 NF4 走 bitsandbytes 的 `Linear4bit`。bitsandbytes 的 4-bit 最低要求正好是 **sm_75**,2080 Ti 卡在支持线上;其 CMakeLists 的架构列表明确含 `75`。 **服务器上原有的 bitsandbytes 0.45.1 是坏的**(在 `~/.local`): ``` AttributeError: 'NoneType' object has no attribute 'cquantize_blockwise_fp16_nf4' ``` CUDA 动态库没加载上。已在 venv 内装 0.50.1 覆盖。实测通过: ``` [OK ] NF4 fp16 forward out=(1024,14336) finite=True rel_err=0.0913 [OK ] NF4 prequantized reload match=True ← 这正是加载 .safetensors 时走的分支 ``` --- ## 四、多卡:H3 不支持模型并行 DiffSynth 的序列并行(`diffsynth/utils/xfuser/`)**只被 `wan_video.py` 和 `mova_audio_video.py` 引用**;`minimax_h3_audio_video.py` 里零引用 xfuser / torch.distributed。 叠加硬件事实(`llm-setup.md` 第 2.2 节):四卡无 P2P(`topo -p2p r` 全 CNS), GPU0/1 与 GPU2/3 跨 NUMA。 > **结论:不要试图把 4×22G 当成一张 88G 卡。** > 多卡只能做**独立 worker 并发**(每卡一个进程),不能拆单个任务。 并发数上限由**内存**而非显存决定:每 worker 常驻约 32 GB(DiT NF4 16 GB + 文本编码器 NF4 14.3 GB + VAE 1.8 GB),128 GB 内存 → **最多 2–3 个 worker**。 --- ## 五、模型与算力 DiT 是**稠密 30.9B**(50 层 / hidden 5376 / 56 heads × 128 / ffn 14336,无 MoE)。 480×832×124 帧 → 视频 latent 35×30×52,patch(1,2,2) → **≈14,100 token**。 每步约 830 TFLOP。 | 文件 | 大小 | |---|---| | `minimax-h3-fl2va-nf4.safetensors` | 15.98 GB | | `minimax-h3-text-encoder-nf4.safetensors`(Qwen3-VL 32B) | 14.27 GB | | `video_vae_nf4.safetensors` | 1.50 GB | | `audio_vae_nf4.safetensors` | 0.26 GB | | `minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors`(LoRA) | 1.29 GB | | **合计** | **33.3 GB** | Turbo LoRA 能叠在 NF4 上——`core/vram/layers.py:445` 的 `AutoWrappedQuantizedModule(AutoTorchModule, LoRAHotLoadMixin)` 在量化输出**之后**做 `out += x@A@B`,不需要反量化基座权重。这是最大的性能杠杆: | 方案 | 步数 | 单条 5 秒视频(估算) | |---|---|---| | NF4 全量 | 50 | 20–35 分钟 | | NF4 + Turbo LoRA | 4 | **2–4 分钟** | > 文件名里的 `bf16` 只是 LoRA 权重的存储精度,加载时会按 `pipe.torch_dtype` 转成 fp16,不影响。 --- ## 六、安装了什么(以及没动什么) **全部落在 `/data/models/minimax-h3/`。系统级零改动。** - 没装 CUDA、没动驱动(570.153.02 保持)、没碰 `/usr`、没改 `~/.local` - 系统 `nvcc` 是 11.5,很老,但**推理用不到**——pip 的 torch wheel 自带 CUDA runtime - venv 用 `--system-site-packages` 复用已验证的 `torch 2.9.0+cu128` (其 `arch_list` 含 `sm_75`),省 3 GB 下载 - **只装了 bitsandbytes 0.50.1 + diffsynth(`-e . --no-deps`)**,其余依赖系统已齐 - **不要设 `PYTHONNOUSERSITE=1`**:`torch` / `transformers` / `av` 都住在 `~/.local`, 屏蔽掉会连 torch 一起丢。venv 的 site-packages 本来就排在 `~/.local` 前面, 我们的 bnb 0.50.1 自然覆盖坏掉的 0.45.1 ### 容器变更(用户已授权,可逆) | 容器 | 动作 | 恢复 | |---|---|---| | `llm`(旧 llama.cpp,unhealthy 5 天,已被 llm38 取代) | **已停** | `docker start llm` | | `whisper` | 停后**已恢复** | — | | `llm38`(Qwen3.8-27B,经 FRP 对外供 `qwen.wudaima.com`) | **全程未碰** | — | 启动参数完整备份在 `/data/models/minimax-h3/backup/`(`llm.inspect.json` / `whisper.inspect.json` / `restore-notes.txt`)。两者都是 `restart=unless-stopped`, 手动 stop 不会被自动拉起。 --- ## 七、目录与路径 | 用途 | 路径 | |---|---| | 根目录 | `/data/models/minimax-h3/` | | venv | `/data/models/minimax-h3/venv/` | | DiffSynth 源码 | `/data/models/minimax-h3/DiffSynth-Studio/`(HEAD `0361581`) | | **模型权重** | `/data/models/minimax-h3/models/` | | **日志** | `/data/models/minimax-h3/logs/` | | **输出视频** | `/data/models/minimax-h3/outputs/` | | 脚本 | `/data/models/minimax-h3/scripts/` | | 容器备份 | `/data/models/minimax-h3/backup/` | ## 八、启动命令 ```bash cd /data/models/minimax-h3 ./run.sh 1 # GPU1,Turbo 4-step,480x832x124f(默认) ./run.sh 1 --no-turbo --steps 50 # 全质量 50 步 ./run.sh 2 --prompt "..." --seed 7 ./run.sh 1 --fp32-accum # 若出现 NaN/伪影:关 fp16 累加(约慢一倍) ``` 多 worker(每卡一个独立进程,**内存上限 2–3 个**): ```bash ./run.sh 0 --prompt "..." & # GPU0(NUMA0) ./run.sh 1 --prompt "..." & # GPU1(NUMA0) ./run.sh 2 --prompt "..." & # GPU2(NUMA1) ./run.sh 3 --prompt "..." & # GPU3(NUMA1) ``` `run.sh` 会按 GPU 编号选 NUMA 节点绑核(GPU0/1→node0,GPU2/3→node1), 但服务器上**没装 `numactl`**,目前这段会自动跳过。装了会更快。 ## 九、脚本 | 脚本 | 作用 | |---|---| | `run.sh` | 启动器:设环境变量 + 选 GPU + 落日志 | | `t2va.py` | 推理主体(fp16 / SDPA / CPU offload) | | `download.py` | 用 DiffSynth 自己的 `ModelConfig` 下载,保证落盘布局与加载时解析一致 | | `verify.py` | 逐文件比对字节数 vs ModelScope + 解析 safetensors 头 | | `probe-turing-caps.py` | sm_75 能力探测(bf16/fp16 × 各 attention 后端) | | `probe-h3-shapes.py` | 真实 H3 形状下的 attention + GEMM 吞吐 | | `probe-nf4-fp16.py` | NF4 fp16 前向 + prequantized 重载 | | `probe-attention-backend.py` | 验证 DiffSynth 实际选中的后端 | 换硬件或升级 torch/bnb 后,**先跑 probe-* 四个再动别的**。 ## 十、下载注意 出口带宽 **5.9 MB/s 封顶**(`llm-setup.md` 第三节),33 GB 约 **95 分钟**。 - 必须 `setsid`,**只用 `nohup` 不够**——SSH 通道关闭时进程仍会被带走(本次踩过) - **不要开两个下载进程**:并行不会更快(出口带宽是瓶颈),只会争抢同一个 `._____temp` 分片互相破坏(本次踩过,事后用 `verify.py` 校验) - ModelScope 可达;HuggingFace / ghcr 不通 ## 十一、失败时的降级顺序 1. fp16 出 NaN → `--fp32-accum`(关 fp16 累加,约慢一倍) 2. 仍不行 → 把 norm / adaln 提到 fp32 3. 显存不够 → 调小 `--vram-reserve`,或换 pruned 权重 (`minimax-h3-fl2va-pruned-nf4.safetensors`,9.76 GB / 18.7B,但 **Turbo LoRA 与之不兼容**,层数不同) --- ## ★ 2026-08-18 实测推翻本文档第一节:fp16 是故障本身,不是修复 ★ 本 README 第一节写「`torch.bfloat16` → `torch.float16`(致命)」,把改用 fp16 当作**必须的修正**。 **实测结论相反:改成 fp16 才是这条线出不了片的根本原因。** ### 对照实验(唯一变量是 dtype) 同分辨率 448×256×28f、同 prompt、同权重、同 Turbo LoRA,只改 `torch_dtype`: | | fp32 | fp16 | |---|---|---| | 视频像素 | mean=140.9 **std=50.6** max=255 | mean=0 **std=0** max=0 | | 音频 | **finite=True** | finite=False | | 生成耗时 | 62s | 40s | | 峰值显存 | 16.3G | 16.3G | | 结果 | **正常出片 ✅** | **124 帧纯黑 ❌** | 模型、权重、管线、Turbo LoRA、VAE 全部正常——**唯一的问题是 fp16 的动态范围装不下这个模型**。 ### 为什么之前的 probe 没测出来 `probe-nf4-fp16.py` / `probe-h3-shapes.py` 用的是**合成小张量**,测的是「NF4 fp16 前向不报错、数值有限」。 这类测试测不出真实模型在 **50 层残差累积**后的量级。实测 `dit.blocks.0` 输入 absmax 89 → 输出 38176, 逼近 fp16 上限 65504;再往后必然溢出。**「单层不溢出」推不出「50 层不溢出」。** ### 溢出机理:`.to(x.dtype)` 这个模式在库里反复出现 ```python # minimax_h3_dit.py:61-67 def _modulate_scale_shift(x, shift, scale, indices): return (x * (1.0 + scale.index_select(0, indices)) + shift.index_select(0, indices)).to(x.dtype) def _modulate_gate(x, gate, other, indices): return (x + gate.index_select(0, indices) * other).to(x.dtype) ``` 库作者自己的注释说「index_select on the AdaLN params can promote the expression」—— 表达式确实被提升到 fp32 算,但**结果又降回 fp16**,恰好丢掉了提升带来的动态范围。 DiT 有 50 个 block × 每个 2 次 = **100 个这样的降回点**。`condition_proj` 那里是同一个病灶的另一个实例 (fp32 真值 absmax 79554 > 65504)。 ### 逐点修 fp16 是行不通的(四条弯路,都实测过) 1. `--fp32-accum` —— 开关生效(`fp16_reduced_accum` True→False)但**输出逐字节相同**。 它管的是 reduced-precision reduction,与这里的溢出无关。README 第十一节把它列为降级第一档,**对此故障不对症**。 2. 替换 `AutoWrappedLinear.forward` —— 会绕过 `preparing()`/onload 换入机制。 3. 只替换 `linear_forward`、最后仍 `.to(fp16)` —— **无效**:fp32 算得对,溢出发生在那次类型转换上。 (自检若只查转换前的张量,会永远显示"正常"——踩过。) 4. 钳位到 65504 再降回 —— **反而更糟**:`blocks.0` 是 `h = x + attn(norm1(x))`, x 恰好等于 fp16 最大可表示值时再加任何正数立刻变 inf(实测 3.45% 非有限)。钳位不留余量等于自造故障。 每修一处,NaN 就移到下一处——因为问题是全局的动态范围,不是某几个点。 ### 另一个方法教训:诊断配置必须与故障配置逐项一致 前几轮诊断用的是中文 prompt「一只橘猫…」,而实际失败的是 `t2va.py` 的**英文默认 prompt**。 不同文本 → 不同嵌入量级 → 溢出点不同。定位出的路径在真实故障里根本不触发,白跑了几轮。 ### 退出码不可信 原 `t2va.py` 只检查 `audio` 是否非有限,**从不检查 video**,全黑视频照样打印 `[done]` 并返回 **0**。 已补上 video 的有限性 + 纯色检查(std < 1.0 判失败),坏产物以**退出码 3** 退出。 --- ## 冒烟测试前置检查(2026-08-18 17:59,llm38 切 256K 之后) 环境侧已全部验证,剩下的唯一变量是权重本身。 | 检查项 | 结果 | |---|---| | attention 后端 @ 真实 DiT 形状 (1,56,14100,128) fp16 | ✅ SDPA 选中,`finite=True`(xformers 可用但被 env 钉死为 torch) | | bitsandbytes 版本 | ✅ **0.50.1**(成功压住 `~/.local` 里坏的 0.45.1) | | NF4 fp16 前向 + 预量化重载 | ✅ `rel_err=0.0913`,reload `match=True` | | 已落盘 3 文件字节 + safetensors 头 | ✅ video_vae / audio_vae / turbo LoRA 全对 | ### ⚠️ `t2va.py:47` 的地板值现在是个雷 ```python vram_limit = max(6.0, free_gb - args.vram_reserve) ``` 这个 `6.0` 地板是在**每卡还有 12–15 GB 空闲**时写的。2026-08-18 llm38 上下文提到 256K 后(KV 从 8 GiB 涨到 32 GiB),空闲显存腰斩,地板值开始**反过来盖掉安全余量**: | | 空闲 | 实得 `vram_limit` | 余量 | | |---|---|---|---|---| | GPU0 | 6.58G | 6.00G | +0.58G | 勉强,无缓冲 | | **GPU1** | **7.51G** | 6.00G | **+1.51G** | 勉强,最宽裕 → **冒烟测试用这张** | | GPU2 | 5.25G | 6.00G | **−0.75G** | ❌ **必 OOM**:地板要的比卡上有的还多 | | GPU3 | 6.59G | 6.00G | +0.59G | 勉强,无缓冲 | `--vram-reserve` **调不动这个地板**——`max()` 在外层。 ### ✅ 正解:跑 H3 之前先停 llm38 用户 2026-08-18 明确:**这是测试机,不是生产环境,停 llm38 让路是预期做法**(见工作区 `CLAUDE.md`)。所以上面整张表都不必绕,串行跑就行: ```bash docker stop llm38 # 让出约 57 GB ./run.sh 1 ... # 四卡各约 21 GB 空闲 → vram_limit 约 19 G,地板值失效 docker start llm38 # 跑完起回来,约 15 秒 ``` 这样测出来的还是**干净数字**(无抢占),比在争抢状态下测更有参考价值。 **地板值仍值得修**(低优先级):`max(6.0, ...)` 在放不下时会去要一个不存在的额度而不是报错。真要多负载共存时才会再咬人;目前串行跑绕开了。 ### 脚本漂移(已修) 本 README 第九节列了 8 个脚本、并说「换硬件后先跑 probe-* 四个」,但**服务器上原本只有 3 个**(`download.py` / `t2va.py` / `verify.py`),4 个 probe 从没上过服务器,那条指示在服务器上无法执行。已补齐到 `/data/models/minimax-h3/scripts/`。 > 另注:`run.sh` 在服务器上位于 `/data/models/minimax-h3/run.sh`(顶层),本地副本放在 `scripts/` 下,两边路径不一致。README 第八节的 `cd /data/models/minimax-h3 && ./run.sh 1` 以服务器为准。 --- ## 验收记录 **2026-08-18 22:31 首次成功出片。** 配置:`dtype=fp32` · Turbo 4-step · 832×480×124f · GPU1 · `vram_limit=7.3G` ### 产物验证(ffprobe + 逐像素 + 音频样本) | 项目 | 实测 | |---|---| | 文件 | `outputs/t2va_20260818-223136.mp4` **7,859,869 字节 (7.5 MB)** | | 视频流 | h264 **832×480** · 24 fps · **124 帧** · 5.167 s | | 实际解码帧数 | **124**(与声明一致,无截断) | | 像素 | mean 69.33 · **std 57.85** · 全域 0–255 | | **帧间平均差异** | **16.99** → 真实动态视频,非静止图 | | 音频流 | aac · **32000 Hz · 双声道** · 163 帧 · 5.175 s | | 音频电平 | mean **−34.0 dB** · max **−14.9 dB**(静音应为 −91 dB) | | 音频样本 | absmax 0.1799 · rms 0.0199 · **非零样本 100%** | > 验证方法上的教训:`ffmpeg -af volumedetect -f null -` 的输出容易被 grep 漏掉, > 误报成"无音频流"。**直接用 PyAV 解码样本算 rms** 更可靠。 ### 性能与显存(fp32,全部实测) | 分辨率 | 步数 | 生成耗时 | 每步 | 峰值显存 | 结果 | |---|---|---|---|---|---| | 448×256×28f | 4 (turbo) | 62 s | 15.5 s | 16.3 G | ✅ std 50.6 | | 640×384×61f | 4 (turbo) | 171 s | 42.8 s | 11.4 G | ✅ std 51.7 | | **832×480×124f** | **4 (turbo)** | **553 s** | **107 s** | **12.2 G** | ✅ std 66.8 | | 832×480×124f | 4 (turbo),真实 prompt | 556 s | 138.9 s | 12.3 G | ✅ **本次验收** | > 同配置两次每步耗时 107 s vs 138.9 s,差异来自并发:验收那次有另一条 50 步任务 > 在别的卡上同时跑,抢 CPU 与内存带宽(权重 offload 在 CPU 侧)。**单任务跑约 107 s/步。** **加载耗时**:首次从机械盘读 fp32 权重 **238 s**;page cache 命中后 fp16 只要 **12–15 s**。 反复调试时第二次之后极快,不必每次预留 6 分钟。 ### 与 fp16 的对照(唯一变量是 dtype) | | fp32 | fp16 | |---|---|---| | 每步耗时 | 107 s | 36.8 s(快 2.9×) | | 峰值显存 | 12.2 G | 15.9 G | | **产物** | **正常出片** | **124 帧纯黑 / 音频 NaN** | fp16 更快更省显存,但**根本出不了片**。这不是速度取舍,是能用与不能用的区别。详见上一节。 ### 启动命令(已验证) ```bash cd /data/models/minimax-h3 echo "你的提示词" > prompt.txt # 从文件读,避开多层 shell 引号 ./realrun.sh turbo # Turbo 4-step,全分辨率,约 10 分钟 ./realrun.sh full50 # 50 步全质量,约 90 分钟 ``` 直接调用也可(`--vram-reserve` 现按 dtype 自适应,fp32 默认 14): ```bash ./run.sh 1 --prompt "$(cat prompt.txt)" --seed 42 ``` > **prompt 必须从文件读。** 直接在 `ssh … bash -c "…"` 里传含引号的 prompt 会被 > 三层 shell 吃掉,传进去是空串,报错是 > `RuntimeError: cannot reshape tensor of 0 elements into shape [1, 0, -1, 128]` > ——看着像 transformers 的 bug,实际是**输入为空**。0 元素张量崩在 reshape 是这个的典型指纹。 ### 50 步全质量(2026-08-18 23:51 完成,已验收) 同 prompt、同 seed,`--no-turbo --steps 50`: | 项目 | 实测 | |---|---| | 文件 | `outputs/t2va_20260818-235141.mp4` **6,726,194 字节 (6.4 MB)** | | 生成耗时 | **5364 s(89 分钟)· 107.3 s/步** | | 峰值显存 | **9.1 G**(比 turbo 的 12.3 G 更低——无 LoRA 层) | | 视频 | h264 832×480 · 24 fps · **124 帧** · 5.167 s | | 像素 | mean 71.63 · **std 59.68** · 全域 0–255 | | 帧间平均差异 | **7.366** | | 音频 | mean **−27.2 dB** · max **−9.2 dB** · 331776 样本 | **与 Turbo 4-step 的对比**: | | Turbo 4-step | 50 步全质量 | |---|---|---| | 耗时 | 556 s | **5364 s(9.6×)** | | 峰值显存 | 12.3 G | 9.1 G | | 像素 std | 57.85 | 59.68 | | 帧间差异 | **16.99** | **7.366** | | 音频电平 | −34.0 / −14.9 dB | **−27.2 / −9.2 dB** | 50 步的帧间差异明显更小、音频更响。**帧间差异小不等于质量差**——更多去噪步数带来更平滑的时序连贯性; Turbo 的 16.99 里有一部分是 4 步蒸馏残留的帧间抖动。哪个观感更好需要人眼判断, 这两条客观指标只能说明**两者都是真实动态视频,且 50 步的时序更稳**。 **每步耗时 107.3 s 与单任务 turbo 的 107 s 完全一致**——证明这个数字是这台机器上 fp32 全分辨率的稳定值,不随步数或 LoRA 变化。