Commit d2a4537da212fc7db121eea3cb43043f877e2c06
1 parent
ccf5db19
删除本地 minimax-h3/ 旧代码,已打包备份
生产已全面切到 raylight(2026-08-22起),minimax-h3 的路由代码(gateway.py/api.py等) 今天已经从服务器上删除(保留console/models/venv/outputs/logs)。本地这份是同样的代码, 删前打包备份到 ~/Documents/lemonAI/2080tiserver-backups/ minimax-h3-local-repo-backup-20260824.zip,另外服务器上实际删除的文件也单独打包 下载在同目录 minimax-h3-code-backup-20260824.tar.gz(包含console/models/venv/outputs/ logs之外的全部内容,比本地这份更全,含DISABLED-README.md等本地没有的文件)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
37 changed files
with
0 additions
and
2404 deletions
minimax-h3/API.md deleted
| 1 | -# MiniMax-H3 视频生成 API | ||
| 2 | - | ||
| 3 | -服务在服务器上:`/data/models/minimax-h3/api.py` + `api-run.sh`(本地是副本)。 | ||
| 4 | - | ||
| 5 | -## 为什么是异步任务式,不是同步接口 | ||
| 6 | - | ||
| 7 | -| 事实 | 后果 | | ||
| 8 | -|---|---| | ||
| 9 | -| 模型加载 240 s(首次,fp32 权重从机械盘读;page cache 命中 20 s) | 不能每请求加载 | | ||
| 10 | -| 单条视频 Turbo 4 步约 **7 分钟**,50 步约 **90 分钟** | 同步 HTTP 必然超时 | | ||
| 11 | -| 一条生成占 **12.3 G 显存 + 约 32 G 内存** | 22 G 卡上同时只能跑一条 | | ||
| 12 | - | ||
| 13 | -所以接口是:**提交任务拿 id → 轮询状态 → 下载文件**。并发提交会排队,不会 OOM。 | ||
| 14 | - | ||
| 15 | -## 启动 | ||
| 16 | - | ||
| 17 | -```bash | ||
| 18 | -cd /data/models/minimax-h3 | ||
| 19 | -./api-run.sh 1 8010 # GPU_ID=1, PORT=8010 | ||
| 20 | -# 后台跑: | ||
| 21 | -setsid nohup ./api-run.sh 1 8010 > logs/api.log 2>&1 < /dev/null & | ||
| 22 | -``` | ||
| 23 | - | ||
| 24 | -⚠️ **显存前提**:这条服务常驻约 12.3 G。llm38 跑 256K 上下文时占 57.3 G(四卡各约 14.3 G), | ||
| 25 | -两者在同一张卡上放不下。跑视频前先 `docker stop llm38`(这是测试机,见工作区 `CLAUDE.md`), | ||
| 26 | -或把 H3 放到 llm38 没用满的卡上并自行核算。恢复用 `qwen38-deploy/RESTORE-GPU.md`。 | ||
| 27 | - | ||
| 28 | -## 接口 | ||
| 29 | - | ||
| 30 | -### `GET /health` | ||
| 31 | -```json | ||
| 32 | -{"status":"ok","model":"ready","gpu":"1","queued":0,"running":0} | ||
| 33 | -``` | ||
| 34 | -`status` 在模型加载完成前是 `loading`。**别在 `loading` 时提交任务**——会排队但看不出进度。 | ||
| 35 | - | ||
| 36 | -### `POST /v1/videos` → `202` | ||
| 37 | - | ||
| 38 | -```bash | ||
| 39 | -curl -X POST http://192.168.110.67:8010/v1/videos \ | ||
| 40 | - -H "Content-Type: application/json" \ | ||
| 41 | - -d '{ | ||
| 42 | - "prompt": "一只橘猫趴在洒满阳光的窗台上,慢慢眨眼,尾巴轻轻摆动", | ||
| 43 | - "width": 832, "height": 480, "frames": 124, | ||
| 44 | - "steps": 4, "turbo": true, "seed": 7 | ||
| 45 | - }' | ||
| 46 | -``` | ||
| 47 | - | ||
| 48 | -```json | ||
| 49 | -{"id":"ca0f2f095ddd","status":"queued","queue_position":1, | ||
| 50 | - "eta_seconds":428,"poll":"/v1/videos/ca0f2f095ddd"} | ||
| 51 | -``` | ||
| 52 | - | ||
| 53 | -| 字段 | 默认 | 范围 | 说明 | | ||
| 54 | -|---|---|---|---| | ||
| 55 | -| `prompt` | 必填 | 1–2000 字符 | 中英文都行;含人物台词时用英文引号包住那句 | | ||
| 56 | -| `width` | 832 | 64–**832** | | | ||
| 57 | -| `height` | 480 | 64–**480** | | | ||
| 58 | -| `frames` | 124 | 8–**124** | 24 fps,124 帧 ≈ 5.17 秒 | | ||
| 59 | -| `steps` | 4 | 1–50 | turbo 时应 ≤8 | | ||
| 60 | -| `turbo` | true | | true=4 步蒸馏 LoRA;false=全质量多步 | | ||
| 61 | -| `seed` | 0 | 0–2³¹−1 | 同 seed 同参数结果可复现 | | ||
| 62 | -| `flow_shift` | 自动 | | 不传则 turbo=6.0 / 非 turbo=12.0 | | ||
| 63 | - | ||
| 64 | -**上界不是随手写的**:832×480×124f 是实测跑通的最高档(峰值 12.2 G)。再大在 22 G 卡上 OOM, | ||
| 65 | -所以在入口就挡住,而不是让它跑 7 分钟后崩掉。 | ||
| 66 | - | ||
| 67 | -错误码: | ||
| 68 | -- `422` 参数越界或缺失(pydantic 校验) | ||
| 69 | -- `400` `turbo=true` 且 `steps>8`(Turbo LoRA 是 4 步蒸馏的,多步没意义) | ||
| 70 | - | ||
| 71 | -### `GET /v1/videos/{id}` | ||
| 72 | - | ||
| 73 | -```json | ||
| 74 | -{"id":"ca0f2f095ddd","status":"running","step":0,"total_steps":4, | ||
| 75 | - "elapsed_seconds":132, ...} | ||
| 76 | -``` | ||
| 77 | - | ||
| 78 | -`status`: `queued` → `running` → `done` / `failed`。 | ||
| 79 | -完成后多出 `file`、`size_bytes`、`peak_vram_gb`、`download`。 | ||
| 80 | - | ||
| 81 | -### `GET /v1/videos/{id}/file` | ||
| 82 | -返回 mp4。任务未完成时返回 `409`。 | ||
| 83 | - | ||
| 84 | -### `GET /v1/videos?limit=50` | ||
| 85 | -列出最近任务。 | ||
| 86 | - | ||
| 87 | -## 完整调用示例(Python) | ||
| 88 | - | ||
| 89 | -```python | ||
| 90 | -import time, requests | ||
| 91 | -API = "http://192.168.110.67:8010" | ||
| 92 | - | ||
| 93 | -r = requests.post(f"{API}/v1/videos", json={ | ||
| 94 | - "prompt": "清晨的中式庭院,一位女子坐在长廊上喝茶,阳光透过竹叶", | ||
| 95 | - "steps": 4, "turbo": True, "seed": 42, | ||
| 96 | -}, timeout=30) | ||
| 97 | -r.raise_for_status() | ||
| 98 | -jid = r.json()["id"] | ||
| 99 | -print("任务", jid, "预计", r.json()["eta_seconds"], "秒") | ||
| 100 | - | ||
| 101 | -while True: | ||
| 102 | - j = requests.get(f"{API}/v1/videos/{jid}", timeout=30).json() | ||
| 103 | - if j["status"] in ("done", "failed"): | ||
| 104 | - break | ||
| 105 | - print(j["status"], j.get("elapsed_seconds", 0), "s") | ||
| 106 | - time.sleep(20) | ||
| 107 | - | ||
| 108 | -if j["status"] == "failed": | ||
| 109 | - raise SystemExit("失败: " + j["error"]) | ||
| 110 | - | ||
| 111 | -open(f"{jid}.mp4", "wb").write( | ||
| 112 | - requests.get(f"{API}/v1/videos/{jid}/file", timeout=300).content) | ||
| 113 | -print("已保存", j["size_bytes"], "字节,峰值显存", j["peak_vram_gb"], "G") | ||
| 114 | -``` | ||
| 115 | - | ||
| 116 | -## 服务端会自己验产物 | ||
| 117 | - | ||
| 118 | -**写出文件不等于成功**——fp16 时代踩过:全黑视频照样写出 24 KB 的 mp4 并返回退出码 0。 | ||
| 119 | -所以 worker 在写文件前会检查: | ||
| 120 | - | ||
| 121 | -- 抽 8 帧算像素 `std`,`< 1.0` 判为纯色 → 任务标 `failed` | ||
| 122 | -- 音频张量含非有限值 → 任务标 `failed` | ||
| 123 | - | ||
| 124 | -这两条不通过时**不会**给你一个"成功但没用"的文件。 | ||
| 125 | - | ||
| 126 | -## 任务记录会落盘(2026-08-20 起) | ||
| 127 | - | ||
| 128 | -`JOBS` 曾经只在内存里,进程重启(比如 `h3-console` 弹性调度器切 fl2va/ref2va 分区时的 | ||
| 129 | -kill+restart)就把这张表清空——视频文件在 `outputs/` 没事,但 id→文件路径这层映射没了, | ||
| 130 | -`GET /v1/videos/{id}`、下载都会 404。现在改成每次状态变化都往 sqlite 写一份: | ||
| 131 | - | ||
| 132 | -- 落盘文件是 **`jobs_gpu{GPU_ID}.sqlite3`**(跟 `api.py` 同目录),四张卡各一份,不共享—— | ||
| 133 | - 一个进程只会在同一张 GPU 上起来,per-GPU 分文件天然不会有另一个进程来抢,不用处理 | ||
| 134 | - 跨进程写锁竞争。 | ||
| 135 | -- 进程启动时会把这个文件读回内存,日志能看到 `[api] 从磁盘恢复了 N 条历史任务记录`。 | ||
| 136 | -- **in-flight(`queued`/`running`)的记录没法真正恢复**——GPU 上下文和排队队列本来就不落盘, | ||
| 137 | - 重启后没有任何东西能"接着跑"。这类记录读回来会被**改判为 `failed`**,`error` 字段写明 | ||
| 138 | - "进程重启导致这条任务的执行状态丢失"。只有 `done`/`failed` 这两个终态是完整保留的。 | ||
| 139 | -- 只影响**直接调用 `api.py`/`gateway.py` 的场景**——如果你是通过 `h3-console`(端口 8080) | ||
| 140 | - 提交的任务,即使碰到上面这种真正没法恢复的 in-flight 丢失,console 自己也存了一份 | ||
| 141 | - 影子记录,历史和已完成任务的文件不受影响。 | ||
| 142 | - | ||
| 143 | -## 已知限制 | ||
| 144 | - | ||
| 145 | -- **无鉴权**,和这台机器上其他服务一致。放到公网前必须加。 | ||
| 146 | -- **单 worker 串行**。要并发就每张空闲卡起一个实例(不同 PORT),自己在上层做负载。 | ||
| 147 | -- `turbo=false` 时会 `clear_lora()`,任务结束后再挂回来。**混合提交 turbo/非 turbo 时 | ||
| 148 | - 每次切换都有 LoRA 装卸开销**,批量跑同一种模式更快。 |
minimax-h3/README.md deleted
| 1 | -# MiniMax-H3 视频生成部署(4 × RTX 2080 Ti) | ||
| 2 | - | ||
| 3 | -在 `192.168.110.67` 上用 DiffSynth-Studio 跑 MiniMax-H3 FL2VA(文生视频 + 原生立体声)。 | ||
| 4 | - | ||
| 5 | -> **状态(2026-08-18)**:环境已装好并通过全部兼容性实测;模型下载中(33 GB / 6 MB/s); | ||
| 6 | -> 尚未完成首次出片。验收结果见本文末「验收记录」,未填即表示还没跑通。 | ||
| 7 | - | ||
| 8 | -服务器侧根目录 **`/data/models/minimax-h3/`**,本目录是其脚本副本 + 决策记录。 | ||
| 9 | - | ||
| 10 | ---- | ||
| 11 | - | ||
| 12 | -## 一、为什么不能照抄官方示例 | ||
| 13 | - | ||
| 14 | -官方 [MiniMax-H3.md](https://github.com/modelscope/DiffSynth-Studio/blob/main/docs/en/Model_Details/MiniMax-H3.md) | ||
| 15 | -的示例在这台机器上**跑不起来**,有三处必须改。 | ||
| 16 | - | ||
| 17 | -### 1. `torch.bfloat16` → `torch.float16`(致命) | ||
| 18 | - | ||
| 19 | -官方示例的 `vram_config` 与 `torch_dtype` 全用 bf16。sm_75 上实测: | ||
| 20 | - | ||
| 21 | -``` | ||
| 22 | -[ERR] SDPA bfloat16 EFFICIENT_ATTENTION → RuntimeError: No available kernel | ||
| 23 | -[ERR] SDPA bfloat16 FLASH_ATTENTION → RuntimeError: No available kernel | ||
| 24 | -[OK ] SDPA bfloat16 MATH → 只剩朴素实现 | ||
| 25 | -[OK ] SDPA float16 EFFICIENT_ATTENTION → 可用 | ||
| 26 | -``` | ||
| 27 | - | ||
| 28 | -bf16 不是"慢一点",是**没有任何高效 attention kernel**。PyTorch 会静默掉到 MATH 后端, | ||
| 29 | -在 H3 的 ~14,100 token 序列上要物化 `14100 × 14100 × 56` 的注意力矩阵,必然 OOM。 | ||
| 30 | - | ||
| 31 | -FP16 路径实测(真实 H3 形状 S=14100 H=56 D=128): | ||
| 32 | - | ||
| 33 | -``` | ||
| 34 | -SDPA EFFICIENT fp16 → OK, 0.21s | ||
| 35 | -fp16 GEMM 有效算力 → 54.3 TFLOPS (2080Ti fp16 张量核满血) | ||
| 36 | -fp16 深链数值稳定性 → finite | ||
| 37 | -``` | ||
| 38 | - | ||
| 39 | -**注意**:`llm-setup.md` 说 "Turing…不支持 bf16,跑 GGUF 走 llama.cpp 是唯一实际路径" —— | ||
| 40 | -前半句对,后半句对扩散模型不成立。PyTorch + bitsandbytes NF4 在 sm_75 上是通的,见下。 | ||
| 41 | - | ||
| 42 | -### 2. `vram_limit` 取错了下标(会 OOM) | ||
| 43 | - | ||
| 44 | -```python | ||
| 45 | -vram_limit=torch.cuda.mem_get_info("cuda")[1] / (1024 ** 3) - 2 # 官方:[1] = 总显存 | ||
| 46 | -vram_limit=torch.cuda.mem_get_info("cuda")[0] / (1024 ** 3) - 2 # 本机:[0] = 空闲显存 | ||
| 47 | -``` | ||
| 48 | - | ||
| 49 | -`mem_get_info()` 返回 `(free, total)`。这台机器四张卡都有常驻服务(llm38 / embedding / SD / whisper), | ||
| 50 | -按总显存 21.5G 要预算,实际只有 12–15G 可用,必 OOM。 | ||
| 51 | - | ||
| 52 | -### 3. `offload_device="disk"` 不能用 | ||
| 53 | - | ||
| 54 | -`model_inference_low_vram/` 那组示例把权重 offload 到磁盘。本机 `/data` 是 | ||
| 55 | -**7200rpm 机械盘**(HGST HUS726020ALA610,实测顺序读 184 MB/s)。 | ||
| 56 | -NF4 DiT 16 GB,每个去噪步都要过一遍,光 I/O 就 **87 秒/步**。 | ||
| 57 | - | ||
| 58 | -改用 CPU 内存 offload:物理内存 128 GB,可用 123 GB,单 worker 常驻约 32 GB,宽裕。 | ||
| 59 | - | ||
| 60 | ---- | ||
| 61 | - | ||
| 62 | -## 二、attention backend:只用 PyTorch SDPA | ||
| 63 | - | ||
| 64 | -| Backend | sm_75 | 处置 | | ||
| 65 | -|---|---|---| | ||
| 66 | -| FlashAttention 2 / 3 / 4 | ❌ 仅 sm_80–sm_121 | **不装** | | ||
| 67 | -| SageAttention | ❌ 需 sm_80+ | **不装** | | ||
| 68 | -| xFormers | ⚠️ 系统已装 0.0.33,fp16 可用 | 不用(见下) | | ||
| 69 | -| PyTorch SDPA | ✅ EFFICIENT(cutlass)后端 | **采用** | | ||
| 70 | - | ||
| 71 | -DiffSynth 的 `diffsynth/core/attention/attention.py` 里有自动优先级, | ||
| 72 | -**xformers 排在 torch 前面会被自动选中**。实测: | ||
| 73 | - | ||
| 74 | -``` | ||
| 75 | -不设环境变量 → selected = xformers | ||
| 76 | -DIFFSYNTH_ATTENTION_IMPLEMENTATION=torch → selected = torch | ||
| 77 | -``` | ||
| 78 | - | ||
| 79 | -两者 fp16 下都能跑,但锁死 `torch` 保证可复现。`run.sh` 已导出该变量。 | ||
| 80 | - | ||
| 81 | -H3 的 DiT 只用最干净的 attention 调用(无 mask / 无 causal / 无 sliding window, | ||
| 82 | -见 `minimax_h3_dit.py:79`),不会踩 flex_attention 需要 torch.compile 的坑。 | ||
| 83 | - | ||
| 84 | ---- | ||
| 85 | - | ||
| 86 | -## 三、NF4 量化在 Turing 上成立 | ||
| 87 | - | ||
| 88 | -NF4 走 bitsandbytes 的 `Linear4bit`。bitsandbytes 的 4-bit 最低要求正好是 | ||
| 89 | -**sm_75**,2080 Ti 卡在支持线上;其 CMakeLists 的架构列表明确含 `75`。 | ||
| 90 | - | ||
| 91 | -**服务器上原有的 bitsandbytes 0.45.1 是坏的**(在 `~/.local`): | ||
| 92 | - | ||
| 93 | -``` | ||
| 94 | -AttributeError: 'NoneType' object has no attribute 'cquantize_blockwise_fp16_nf4' | ||
| 95 | -``` | ||
| 96 | - | ||
| 97 | -CUDA 动态库没加载上。已在 venv 内装 0.50.1 覆盖。实测通过: | ||
| 98 | - | ||
| 99 | -``` | ||
| 100 | -[OK ] NF4 fp16 forward out=(1024,14336) finite=True rel_err=0.0913 | ||
| 101 | -[OK ] NF4 prequantized reload match=True ← 这正是加载 .safetensors 时走的分支 | ||
| 102 | -``` | ||
| 103 | - | ||
| 104 | ---- | ||
| 105 | - | ||
| 106 | -## 四、多卡:H3 不支持模型并行 | ||
| 107 | - | ||
| 108 | -DiffSynth 的序列并行(`diffsynth/utils/xfuser/`)**只被 `wan_video.py` 和 | ||
| 109 | -`mova_audio_video.py` 引用**;`minimax_h3_audio_video.py` 里零引用 xfuser / torch.distributed。 | ||
| 110 | - | ||
| 111 | -叠加硬件事实(`llm-setup.md` 第 2.2 节):四卡无 P2P(`topo -p2p r` 全 CNS), | ||
| 112 | -GPU0/1 与 GPU2/3 跨 NUMA。 | ||
| 113 | - | ||
| 114 | -> **结论:不要试图把 4×22G 当成一张 88G 卡。** | ||
| 115 | -> 多卡只能做**独立 worker 并发**(每卡一个进程),不能拆单个任务。 | ||
| 116 | - | ||
| 117 | -并发数上限由**内存**而非显存决定:每 worker 常驻约 32 GB(DiT NF4 16 GB + | ||
| 118 | -文本编码器 NF4 14.3 GB + VAE 1.8 GB),128 GB 内存 → **最多 2–3 个 worker**。 | ||
| 119 | - | ||
| 120 | ---- | ||
| 121 | - | ||
| 122 | -## 五、模型与算力 | ||
| 123 | - | ||
| 124 | -DiT 是**稠密 30.9B**(50 层 / hidden 5376 / 56 heads × 128 / ffn 14336,无 MoE)。 | ||
| 125 | -480×832×124 帧 → 视频 latent 35×30×52,patch(1,2,2) → **≈14,100 token**。 | ||
| 126 | -每步约 830 TFLOP。 | ||
| 127 | - | ||
| 128 | -| 文件 | 大小 | | ||
| 129 | -|---|---| | ||
| 130 | -| `minimax-h3-fl2va-nf4.safetensors` | 15.98 GB | | ||
| 131 | -| `minimax-h3-text-encoder-nf4.safetensors`(Qwen3-VL 32B) | 14.27 GB | | ||
| 132 | -| `video_vae_nf4.safetensors` | 1.50 GB | | ||
| 133 | -| `audio_vae_nf4.safetensors` | 0.26 GB | | ||
| 134 | -| `minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors`(LoRA) | 1.29 GB | | ||
| 135 | -| **合计** | **33.3 GB** | | ||
| 136 | - | ||
| 137 | -Turbo LoRA 能叠在 NF4 上——`core/vram/layers.py:445` 的 | ||
| 138 | -`AutoWrappedQuantizedModule(AutoTorchModule, LoRAHotLoadMixin)` 在量化输出**之后**做 | ||
| 139 | -`out += x@A@B`,不需要反量化基座权重。这是最大的性能杠杆: | ||
| 140 | - | ||
| 141 | -| 方案 | 步数 | 单条 5 秒视频(估算) | | ||
| 142 | -|---|---|---| | ||
| 143 | -| NF4 全量 | 50 | 20–35 分钟 | | ||
| 144 | -| NF4 + Turbo LoRA | 4 | **2–4 分钟** | | ||
| 145 | - | ||
| 146 | -> 文件名里的 `bf16` 只是 LoRA 权重的存储精度,加载时会按 `pipe.torch_dtype` 转成 fp16,不影响。 | ||
| 147 | - | ||
| 148 | ---- | ||
| 149 | - | ||
| 150 | -## 六、安装了什么(以及没动什么) | ||
| 151 | - | ||
| 152 | -**全部落在 `/data/models/minimax-h3/`。系统级零改动。** | ||
| 153 | - | ||
| 154 | -- 没装 CUDA、没动驱动(570.153.02 保持)、没碰 `/usr`、没改 `~/.local` | ||
| 155 | -- 系统 `nvcc` 是 11.5,很老,但**推理用不到**——pip 的 torch wheel 自带 CUDA runtime | ||
| 156 | -- venv 用 `--system-site-packages` 复用已验证的 `torch 2.9.0+cu128` | ||
| 157 | - (其 `arch_list` 含 `sm_75`),省 3 GB 下载 | ||
| 158 | -- **只装了 bitsandbytes 0.50.1 + diffsynth(`-e . --no-deps`)**,其余依赖系统已齐 | ||
| 159 | -- **不要设 `PYTHONNOUSERSITE=1`**:`torch` / `transformers` / `av` 都住在 `~/.local`, | ||
| 160 | - 屏蔽掉会连 torch 一起丢。venv 的 site-packages 本来就排在 `~/.local` 前面, | ||
| 161 | - 我们的 bnb 0.50.1 自然覆盖坏掉的 0.45.1 | ||
| 162 | - | ||
| 163 | -### 容器变更(用户已授权,可逆) | ||
| 164 | - | ||
| 165 | -| 容器 | 动作 | 恢复 | | ||
| 166 | -|---|---|---| | ||
| 167 | -| `llm`(旧 llama.cpp,unhealthy 5 天,已被 llm38 取代) | **已停** | `docker start llm` | | ||
| 168 | -| `whisper` | 停后**已恢复** | — | | ||
| 169 | -| `llm38`(Qwen3.8-27B,经 FRP 对外供 `qwen.wudaima.com`) | **全程未碰** | — | | ||
| 170 | - | ||
| 171 | -启动参数完整备份在 `/data/models/minimax-h3/backup/`(`llm.inspect.json` / | ||
| 172 | -`whisper.inspect.json` / `restore-notes.txt`)。两者都是 `restart=unless-stopped`, | ||
| 173 | -手动 stop 不会被自动拉起。 | ||
| 174 | - | ||
| 175 | ---- | ||
| 176 | - | ||
| 177 | -## 七、目录与路径 | ||
| 178 | - | ||
| 179 | -| 用途 | 路径 | | ||
| 180 | -|---|---| | ||
| 181 | -| 根目录 | `/data/models/minimax-h3/` | | ||
| 182 | -| venv | `/data/models/minimax-h3/venv/` | | ||
| 183 | -| DiffSynth 源码 | `/data/models/minimax-h3/DiffSynth-Studio/`(HEAD `0361581`) | | ||
| 184 | -| **模型权重** | `/data/models/minimax-h3/models/` | | ||
| 185 | -| **日志** | `/data/models/minimax-h3/logs/` | | ||
| 186 | -| **输出视频** | `/data/models/minimax-h3/outputs/` | | ||
| 187 | -| 脚本 | `/data/models/minimax-h3/scripts/` | | ||
| 188 | -| 容器备份 | `/data/models/minimax-h3/backup/` | | ||
| 189 | - | ||
| 190 | -## 八、启动命令 | ||
| 191 | - | ||
| 192 | -```bash | ||
| 193 | -cd /data/models/minimax-h3 | ||
| 194 | - | ||
| 195 | -./run.sh 1 # GPU1,Turbo 4-step,480x832x124f(默认) | ||
| 196 | -./run.sh 1 --no-turbo --steps 50 # 全质量 50 步 | ||
| 197 | -./run.sh 2 --prompt "..." --seed 7 | ||
| 198 | -./run.sh 1 --fp32-accum # 若出现 NaN/伪影:关 fp16 累加(约慢一倍) | ||
| 199 | -``` | ||
| 200 | - | ||
| 201 | -多 worker(每卡一个独立进程,**内存上限 2–3 个**): | ||
| 202 | - | ||
| 203 | -```bash | ||
| 204 | -./run.sh 0 --prompt "..." & # GPU0(NUMA0) | ||
| 205 | -./run.sh 1 --prompt "..." & # GPU1(NUMA0) | ||
| 206 | -./run.sh 2 --prompt "..." & # GPU2(NUMA1) | ||
| 207 | -./run.sh 3 --prompt "..." & # GPU3(NUMA1) | ||
| 208 | -``` | ||
| 209 | - | ||
| 210 | -`run.sh` 会按 GPU 编号选 NUMA 节点绑核(GPU0/1→node0,GPU2/3→node1), | ||
| 211 | -但服务器上**没装 `numactl`**,目前这段会自动跳过。装了会更快。 | ||
| 212 | - | ||
| 213 | -## 九、脚本 | ||
| 214 | - | ||
| 215 | -| 脚本 | 作用 | | ||
| 216 | -|---|---| | ||
| 217 | -| `run.sh` | 启动器:设环境变量 + 选 GPU + 落日志 | | ||
| 218 | -| `t2va.py` | 推理主体(fp16 / SDPA / CPU offload) | | ||
| 219 | -| `download.py` | 用 DiffSynth 自己的 `ModelConfig` 下载,保证落盘布局与加载时解析一致 | | ||
| 220 | -| `verify.py` | 逐文件比对字节数 vs ModelScope + 解析 safetensors 头 | | ||
| 221 | -| `probe-turing-caps.py` | sm_75 能力探测(bf16/fp16 × 各 attention 后端) | | ||
| 222 | -| `probe-h3-shapes.py` | 真实 H3 形状下的 attention + GEMM 吞吐 | | ||
| 223 | -| `probe-nf4-fp16.py` | NF4 fp16 前向 + prequantized 重载 | | ||
| 224 | -| `probe-attention-backend.py` | 验证 DiffSynth 实际选中的后端 | | ||
| 225 | - | ||
| 226 | -换硬件或升级 torch/bnb 后,**先跑 probe-* 四个再动别的**。 | ||
| 227 | - | ||
| 228 | -## 十、下载注意 | ||
| 229 | - | ||
| 230 | -出口带宽 **5.9 MB/s 封顶**(`llm-setup.md` 第三节),33 GB 约 **95 分钟**。 | ||
| 231 | - | ||
| 232 | -- 必须 `setsid`,**只用 `nohup` 不够**——SSH 通道关闭时进程仍会被带走(本次踩过) | ||
| 233 | -- **不要开两个下载进程**:并行不会更快(出口带宽是瓶颈),只会争抢同一个 | ||
| 234 | - `._____temp` 分片互相破坏(本次踩过,事后用 `verify.py` 校验) | ||
| 235 | -- ModelScope 可达;HuggingFace / ghcr 不通 | ||
| 236 | - | ||
| 237 | -## 十一、失败时的降级顺序 | ||
| 238 | - | ||
| 239 | -1. fp16 出 NaN → `--fp32-accum`(关 fp16 累加,约慢一倍) | ||
| 240 | -2. 仍不行 → 把 norm / adaln 提到 fp32 | ||
| 241 | -3. 显存不够 → 调小 `--vram-reserve`,或换 pruned 权重 | ||
| 242 | - (`minimax-h3-fl2va-pruned-nf4.safetensors`,9.76 GB / 18.7B,但 **Turbo LoRA 与之不兼容**,层数不同) | ||
| 243 | - | ||
| 244 | ---- | ||
| 245 | - | ||
| 246 | -## ★ 2026-08-18 实测推翻本文档第一节:fp16 是故障本身,不是修复 ★ | ||
| 247 | - | ||
| 248 | -本 README 第一节写「`torch.bfloat16` → `torch.float16`(致命)」,把改用 fp16 当作**必须的修正**。 | ||
| 249 | -**实测结论相反:改成 fp16 才是这条线出不了片的根本原因。** | ||
| 250 | - | ||
| 251 | -### 对照实验(唯一变量是 dtype) | ||
| 252 | - | ||
| 253 | -同分辨率 448×256×28f、同 prompt、同权重、同 Turbo LoRA,只改 `torch_dtype`: | ||
| 254 | - | ||
| 255 | -| | fp32 | fp16 | | ||
| 256 | -|---|---|---| | ||
| 257 | -| 视频像素 | mean=140.9 **std=50.6** max=255 | mean=0 **std=0** max=0 | | ||
| 258 | -| 音频 | **finite=True** | finite=False | | ||
| 259 | -| 生成耗时 | 62s | 40s | | ||
| 260 | -| 峰值显存 | 16.3G | 16.3G | | ||
| 261 | -| 结果 | **正常出片 ✅** | **124 帧纯黑 ❌** | | ||
| 262 | - | ||
| 263 | -模型、权重、管线、Turbo LoRA、VAE 全部正常——**唯一的问题是 fp16 的动态范围装不下这个模型**。 | ||
| 264 | - | ||
| 265 | -### 为什么之前的 probe 没测出来 | ||
| 266 | - | ||
| 267 | -`probe-nf4-fp16.py` / `probe-h3-shapes.py` 用的是**合成小张量**,测的是「NF4 fp16 前向不报错、数值有限」。 | ||
| 268 | -这类测试测不出真实模型在 **50 层残差累积**后的量级。实测 `dit.blocks.0` 输入 absmax 89 → 输出 38176, | ||
| 269 | -逼近 fp16 上限 65504;再往后必然溢出。**「单层不溢出」推不出「50 层不溢出」。** | ||
| 270 | - | ||
| 271 | -### 溢出机理:`.to(x.dtype)` 这个模式在库里反复出现 | ||
| 272 | - | ||
| 273 | -```python | ||
| 274 | -# minimax_h3_dit.py:61-67 | ||
| 275 | -def _modulate_scale_shift(x, shift, scale, indices): | ||
| 276 | - return (x * (1.0 + scale.index_select(0, indices)) + shift.index_select(0, indices)).to(x.dtype) | ||
| 277 | -def _modulate_gate(x, gate, other, indices): | ||
| 278 | - return (x + gate.index_select(0, indices) * other).to(x.dtype) | ||
| 279 | -``` | ||
| 280 | - | ||
| 281 | -库作者自己的注释说「index_select on the AdaLN params can promote the expression」—— | ||
| 282 | -表达式确实被提升到 fp32 算,但**结果又降回 fp16**,恰好丢掉了提升带来的动态范围。 | ||
| 283 | -DiT 有 50 个 block × 每个 2 次 = **100 个这样的降回点**。`condition_proj` 那里是同一个病灶的另一个实例 | ||
| 284 | -(fp32 真值 absmax 79554 > 65504)。 | ||
| 285 | - | ||
| 286 | -### 逐点修 fp16 是行不通的(四条弯路,都实测过) | ||
| 287 | - | ||
| 288 | -1. `--fp32-accum` —— 开关生效(`fp16_reduced_accum` True→False)但**输出逐字节相同**。 | ||
| 289 | - 它管的是 reduced-precision reduction,与这里的溢出无关。README 第十一节把它列为降级第一档,**对此故障不对症**。 | ||
| 290 | -2. 替换 `AutoWrappedLinear.forward` —— 会绕过 `preparing()`/onload 换入机制。 | ||
| 291 | -3. 只替换 `linear_forward`、最后仍 `.to(fp16)` —— **无效**:fp32 算得对,溢出发生在那次类型转换上。 | ||
| 292 | - (自检若只查转换前的张量,会永远显示"正常"——踩过。) | ||
| 293 | -4. 钳位到 65504 再降回 —— **反而更糟**:`blocks.0` 是 `h = x + attn(norm1(x))`, | ||
| 294 | - x 恰好等于 fp16 最大可表示值时再加任何正数立刻变 inf(实测 3.45% 非有限)。钳位不留余量等于自造故障。 | ||
| 295 | - | ||
| 296 | -每修一处,NaN 就移到下一处——因为问题是全局的动态范围,不是某几个点。 | ||
| 297 | - | ||
| 298 | -### 另一个方法教训:诊断配置必须与故障配置逐项一致 | ||
| 299 | - | ||
| 300 | -前几轮诊断用的是中文 prompt「一只橘猫…」,而实际失败的是 `t2va.py` 的**英文默认 prompt**。 | ||
| 301 | -不同文本 → 不同嵌入量级 → 溢出点不同。定位出的路径在真实故障里根本不触发,白跑了几轮。 | ||
| 302 | - | ||
| 303 | -### 退出码不可信 | ||
| 304 | - | ||
| 305 | -原 `t2va.py` 只检查 `audio` 是否非有限,**从不检查 video**,全黑视频照样打印 `[done]` 并返回 **0**。 | ||
| 306 | -已补上 video 的有限性 + 纯色检查(std < 1.0 判失败),坏产物以**退出码 3** 退出。 | ||
| 307 | - | ||
| 308 | ---- | ||
| 309 | - | ||
| 310 | -## 冒烟测试前置检查(2026-08-18 17:59,llm38 切 256K 之后) | ||
| 311 | - | ||
| 312 | -环境侧已全部验证,剩下的唯一变量是权重本身。 | ||
| 313 | - | ||
| 314 | -| 检查项 | 结果 | | ||
| 315 | -|---|---| | ||
| 316 | -| attention 后端 @ 真实 DiT 形状 (1,56,14100,128) fp16 | ✅ SDPA 选中,`finite=True`(xformers 可用但被 env 钉死为 torch) | | ||
| 317 | -| bitsandbytes 版本 | ✅ **0.50.1**(成功压住 `~/.local` 里坏的 0.45.1) | | ||
| 318 | -| NF4 fp16 前向 + 预量化重载 | ✅ `rel_err=0.0913`,reload `match=True` | | ||
| 319 | -| 已落盘 3 文件字节 + safetensors 头 | ✅ video_vae / audio_vae / turbo LoRA 全对 | | ||
| 320 | - | ||
| 321 | -### ⚠️ `t2va.py:47` 的地板值现在是个雷 | ||
| 322 | - | ||
| 323 | -```python | ||
| 324 | -vram_limit = max(6.0, free_gb - args.vram_reserve) | ||
| 325 | -``` | ||
| 326 | - | ||
| 327 | -这个 `6.0` 地板是在**每卡还有 12–15 GB 空闲**时写的。2026-08-18 llm38 上下文提到 256K 后(KV 从 8 GiB 涨到 32 GiB),空闲显存腰斩,地板值开始**反过来盖掉安全余量**: | ||
| 328 | - | ||
| 329 | -| | 空闲 | 实得 `vram_limit` | 余量 | | | ||
| 330 | -|---|---|---|---|---| | ||
| 331 | -| GPU0 | 6.58G | 6.00G | +0.58G | 勉强,无缓冲 | | ||
| 332 | -| **GPU1** | **7.51G** | 6.00G | **+1.51G** | 勉强,最宽裕 → **冒烟测试用这张** | | ||
| 333 | -| GPU2 | 5.25G | 6.00G | **−0.75G** | ❌ **必 OOM**:地板要的比卡上有的还多 | | ||
| 334 | -| GPU3 | 6.59G | 6.00G | +0.59G | 勉强,无缓冲 | | ||
| 335 | - | ||
| 336 | -`--vram-reserve` **调不动这个地板**——`max()` 在外层。 | ||
| 337 | - | ||
| 338 | -### ✅ 正解:跑 H3 之前先停 llm38 | ||
| 339 | - | ||
| 340 | -用户 2026-08-18 明确:**这是测试机,不是生产环境,停 llm38 让路是预期做法**(见工作区 `CLAUDE.md`)。所以上面整张表都不必绕,串行跑就行: | ||
| 341 | - | ||
| 342 | -```bash | ||
| 343 | -docker stop llm38 # 让出约 57 GB | ||
| 344 | -./run.sh 1 ... # 四卡各约 21 GB 空闲 → vram_limit 约 19 G,地板值失效 | ||
| 345 | -docker start llm38 # 跑完起回来,约 15 秒 | ||
| 346 | -``` | ||
| 347 | - | ||
| 348 | -这样测出来的还是**干净数字**(无抢占),比在争抢状态下测更有参考价值。 | ||
| 349 | - | ||
| 350 | -**地板值仍值得修**(低优先级):`max(6.0, ...)` 在放不下时会去要一个不存在的额度而不是报错。真要多负载共存时才会再咬人;目前串行跑绕开了。 | ||
| 351 | - | ||
| 352 | -### 脚本漂移(已修) | ||
| 353 | - | ||
| 354 | -本 README 第九节列了 8 个脚本、并说「换硬件后先跑 probe-* 四个」,但**服务器上原本只有 3 个**(`download.py` / `t2va.py` / `verify.py`),4 个 probe 从没上过服务器,那条指示在服务器上无法执行。已补齐到 `/data/models/minimax-h3/scripts/`。 | ||
| 355 | - | ||
| 356 | -> 另注:`run.sh` 在服务器上位于 `/data/models/minimax-h3/run.sh`(顶层),本地副本放在 `scripts/` 下,两边路径不一致。README 第八节的 `cd /data/models/minimax-h3 && ./run.sh 1` 以服务器为准。 | ||
| 357 | - | ||
| 358 | ---- | ||
| 359 | - | ||
| 360 | -## 验收记录 | ||
| 361 | - | ||
| 362 | -**2026-08-18 22:31 首次成功出片。** 配置:`dtype=fp32` · Turbo 4-step · 832×480×124f · GPU1 · `vram_limit=7.3G` | ||
| 363 | - | ||
| 364 | -### 产物验证(ffprobe + 逐像素 + 音频样本) | ||
| 365 | - | ||
| 366 | -| 项目 | 实测 | | ||
| 367 | -|---|---| | ||
| 368 | -| 文件 | `outputs/t2va_20260818-223136.mp4` **7,859,869 字节 (7.5 MB)** | | ||
| 369 | -| 视频流 | h264 **832×480** · 24 fps · **124 帧** · 5.167 s | | ||
| 370 | -| 实际解码帧数 | **124**(与声明一致,无截断) | | ||
| 371 | -| 像素 | mean 69.33 · **std 57.85** · 全域 0–255 | | ||
| 372 | -| **帧间平均差异** | **16.99** → 真实动态视频,非静止图 | | ||
| 373 | -| 音频流 | aac · **32000 Hz · 双声道** · 163 帧 · 5.175 s | | ||
| 374 | -| 音频电平 | mean **−34.0 dB** · max **−14.9 dB**(静音应为 −91 dB) | | ||
| 375 | -| 音频样本 | absmax 0.1799 · rms 0.0199 · **非零样本 100%** | | ||
| 376 | - | ||
| 377 | -> 验证方法上的教训:`ffmpeg -af volumedetect -f null -` 的输出容易被 grep 漏掉, | ||
| 378 | -> 误报成"无音频流"。**直接用 PyAV 解码样本算 rms** 更可靠。 | ||
| 379 | - | ||
| 380 | -### 性能与显存(fp32,全部实测) | ||
| 381 | - | ||
| 382 | -| 分辨率 | 步数 | 生成耗时 | 每步 | 峰值显存 | 结果 | | ||
| 383 | -|---|---|---|---|---|---| | ||
| 384 | -| 448×256×28f | 4 (turbo) | 62 s | 15.5 s | 16.3 G | ✅ std 50.6 | | ||
| 385 | -| 640×384×61f | 4 (turbo) | 171 s | 42.8 s | 11.4 G | ✅ std 51.7 | | ||
| 386 | -| **832×480×124f** | **4 (turbo)** | **553 s** | **107 s** | **12.2 G** | ✅ std 66.8 | | ||
| 387 | -| 832×480×124f | 4 (turbo),真实 prompt | 556 s | 138.9 s | 12.3 G | ✅ **本次验收** | | ||
| 388 | - | ||
| 389 | -> 同配置两次每步耗时 107 s vs 138.9 s,差异来自并发:验收那次有另一条 50 步任务 | ||
| 390 | -> 在别的卡上同时跑,抢 CPU 与内存带宽(权重 offload 在 CPU 侧)。**单任务跑约 107 s/步。** | ||
| 391 | - | ||
| 392 | -**加载耗时**:首次从机械盘读 fp32 权重 **238 s**;page cache 命中后 fp16 只要 **12–15 s**。 | ||
| 393 | -反复调试时第二次之后极快,不必每次预留 6 分钟。 | ||
| 394 | - | ||
| 395 | -### 与 fp16 的对照(唯一变量是 dtype) | ||
| 396 | - | ||
| 397 | -| | fp32 | fp16 | | ||
| 398 | -|---|---|---| | ||
| 399 | -| 每步耗时 | 107 s | 36.8 s(快 2.9×) | | ||
| 400 | -| 峰值显存 | 12.2 G | 15.9 G | | ||
| 401 | -| **产物** | **正常出片** | **124 帧纯黑 / 音频 NaN** | | ||
| 402 | - | ||
| 403 | -fp16 更快更省显存,但**根本出不了片**。这不是速度取舍,是能用与不能用的区别。详见上一节。 | ||
| 404 | - | ||
| 405 | -### 启动命令(已验证) | ||
| 406 | - | ||
| 407 | -```bash | ||
| 408 | -cd /data/models/minimax-h3 | ||
| 409 | -echo "你的提示词" > prompt.txt # 从文件读,避开多层 shell 引号 | ||
| 410 | -./realrun.sh turbo # Turbo 4-step,全分辨率,约 10 分钟 | ||
| 411 | -./realrun.sh full50 # 50 步全质量,约 90 分钟 | ||
| 412 | -``` | ||
| 413 | - | ||
| 414 | -直接调用也可(`--vram-reserve` 现按 dtype 自适应,fp32 默认 14): | ||
| 415 | - | ||
| 416 | -```bash | ||
| 417 | -./run.sh 1 --prompt "$(cat prompt.txt)" --seed 42 | ||
| 418 | -``` | ||
| 419 | - | ||
| 420 | -> **prompt 必须从文件读。** 直接在 `ssh … bash -c "…"` 里传含引号的 prompt 会被 | ||
| 421 | -> 三层 shell 吃掉,传进去是空串,报错是 | ||
| 422 | -> `RuntimeError: cannot reshape tensor of 0 elements into shape [1, 0, -1, 128]` | ||
| 423 | -> ——看着像 transformers 的 bug,实际是**输入为空**。0 元素张量崩在 reshape 是这个的典型指纹。 | ||
| 424 | - | ||
| 425 | -### 50 步全质量(2026-08-18 23:51 完成,已验收) | ||
| 426 | - | ||
| 427 | -同 prompt、同 seed,`--no-turbo --steps 50`: | ||
| 428 | - | ||
| 429 | -| 项目 | 实测 | | ||
| 430 | -|---|---| | ||
| 431 | -| 文件 | `outputs/t2va_20260818-235141.mp4` **6,726,194 字节 (6.4 MB)** | | ||
| 432 | -| 生成耗时 | **5364 s(89 分钟)· 107.3 s/步** | | ||
| 433 | -| 峰值显存 | **9.1 G**(比 turbo 的 12.3 G 更低——无 LoRA 层) | | ||
| 434 | -| 视频 | h264 832×480 · 24 fps · **124 帧** · 5.167 s | | ||
| 435 | -| 像素 | mean 71.63 · **std 59.68** · 全域 0–255 | | ||
| 436 | -| 帧间平均差异 | **7.366** | | ||
| 437 | -| 音频 | mean **−27.2 dB** · max **−9.2 dB** · 331776 样本 | | ||
| 438 | - | ||
| 439 | -**与 Turbo 4-step 的对比**: | ||
| 440 | - | ||
| 441 | -| | Turbo 4-step | 50 步全质量 | | ||
| 442 | -|---|---|---| | ||
| 443 | -| 耗时 | 556 s | **5364 s(9.6×)** | | ||
| 444 | -| 峰值显存 | 12.3 G | 9.1 G | | ||
| 445 | -| 像素 std | 57.85 | 59.68 | | ||
| 446 | -| 帧间差异 | **16.99** | **7.366** | | ||
| 447 | -| 音频电平 | −34.0 / −14.9 dB | **−27.2 / −9.2 dB** | | ||
| 448 | - | ||
| 449 | -50 步的帧间差异明显更小、音频更响。**帧间差异小不等于质量差**——更多去噪步数带来更平滑的时序连贯性; | ||
| 450 | -Turbo 的 16.99 里有一部分是 4 步蒸馏残留的帧间抖动。哪个观感更好需要人眼判断, | ||
| 451 | -这两条客观指标只能说明**两者都是真实动态视频,且 50 步的时序更稳**。 | ||
| 452 | - | ||
| 453 | -**每步耗时 107.3 s 与单任务 turbo 的 107 s 完全一致**——证明这个数字是这台机器上 | ||
| 454 | -fp32 全分辨率的稳定值,不随步数或 LoRA 变化。 | ||
| 455 | - | ||
| 456 | - | ||
| 457 | - |
minimax-h3/api-run.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 启动 MiniMax-H3 视频 API。用法: ./api-run.sh [GPU_ID] [PORT] [PARTITION] | ||
| 3 | -# PARTITION=fl2va 收首末帧;ref2va 收任意多张参考图。一个进程一个分区,各占一张卡。 | ||
| 4 | -set -uo pipefail | ||
| 5 | -B=/data/models/minimax-h3 | ||
| 6 | -GPU="${1:-1}"; PORT="${2:-8010}"; export H3_PARTITION="${3:-fl2va}" | ||
| 7 | -export CUDA_VISIBLE_DEVICES="$GPU" | ||
| 8 | -export DIFFSYNTH_ATTENTION_IMPLEMENTATION=torch | ||
| 9 | -export DIFFSYNTH_MODEL_BASE_PATH="$B/models" | ||
| 10 | -export MODELSCOPE_CACHE="$B/.mscache" | ||
| 11 | -export PYTORCH_ALLOC_CONF=expandable_segments:True | ||
| 12 | -export PYTHONUNBUFFERED=1 | ||
| 13 | -cd "$B" | ||
| 14 | -echo "[api] GPU=$GPU PORT=$PORT PARTITION=$H3_PARTITION 首次加载约 240 秒(fp32 权重从机械盘读)" | ||
| 15 | -exec "$B/venv/bin/python" -m uvicorn api:app --host 0.0.0.0 --port "$PORT" --workers 1 |
minimax-h3/api.py deleted
| 1 | -"""MiniMax-H3 文生视频 HTTP API(异步任务式)。 | ||
| 2 | - | ||
| 3 | -为什么不是同步接口:模型加载 238s、单条视频 7 分钟(50 步 90 分钟), | ||
| 4 | -同步 HTTP 必然超时。所以是「提交任务拿 id → 轮询状态 → 下载文件」。 | ||
| 5 | - | ||
| 6 | -为什么是串行单 worker:一条生成占 ~12.3G 显存 + ~32G CPU 内存, | ||
| 7 | -一张 22G 卡同时只能跑一条。并发提交会排队,不会 OOM。 | ||
| 8 | -""" | ||
| 9 | -import os, sys, time, uuid, threading, queue, traceback, io, sqlite3, urllib.request | ||
| 10 | -from typing import Optional, Literal | ||
| 11 | -from pathlib import Path | ||
| 12 | - | ||
| 13 | -BASE = Path("/data/models/minimax-h3") | ||
| 14 | -OUT = BASE / "outputs"; OUT.mkdir(exist_ok=True) | ||
| 15 | -# 一个进程只跑一个分区。两套权重的 DiT、processor、Turbo LoRA 三样都不同, | ||
| 16 | -# 同进程装两套要么重建 pipe(每次重读 16G 权重),要么常驻 77G 内存。 | ||
| 17 | -# 每卡一个进程本来就是这台机器唯一的并发方式(H3 不支持模型并行),顺手就把分区分开了。 | ||
| 18 | -PARTITION = os.environ.get("H3_PARTITION", "fl2va") | ||
| 19 | -# 没上 GPU 的那部分权重存哪。cpu 是每进程一份 40G 匿名内存,并发被 125G 内存卡在 3 个; | ||
| 20 | -# disk 让权重留在 safetensors 文件里(layers.py 里 offload() 就是 .to("meta")), | ||
| 21 | -# 每次前向从文件读进显存,走 OS 页缓存——多个进程读同一个文件,物理内存里只有一份。 | ||
| 22 | -OFFLOAD = os.environ.get("H3_OFFLOAD", "cpu") | ||
| 23 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION", "torch") | ||
| 24 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", str(BASE / "models")) | ||
| 25 | -os.environ.setdefault("MODELSCOPE_CACHE", str(BASE / ".mscache")) | ||
| 26 | -os.environ.setdefault("PYTORCH_ALLOC_CONF", "expandable_segments:True") | ||
| 27 | - | ||
| 28 | -import torch | ||
| 29 | -from fastapi import FastAPI, HTTPException | ||
| 30 | -from fastapi.responses import FileResponse | ||
| 31 | -from pydantic import BaseModel, Field, model_validator | ||
| 32 | - | ||
| 33 | -def _rows(width: int, height: int, frames: int) -> float: | ||
| 34 | - """序列行数:潜帧数 x 宽 x 高 / 1024。真正决定显存/算力的量,帧数和分辨率是同一笔预算。""" | ||
| 35 | - latent_frames = (max(frames, 5) - 5) // 17 * 5 + 2 | ||
| 36 | - return latent_frames * width * height / 1024 | ||
| 37 | - | ||
| 38 | -# 22G 卡的显存墙按 rows 算,不是单独卡 frames——实测边界(2026-08-20 long15s.sh): | ||
| 39 | -# 832x480x124f rows=14430 稳定基准;448x256x362f rows=11984、576x320x362f rows=19260 都能跑 | ||
| 40 | -# (后者峰值 14.5G,换入换出吃紧但没崩);640x384x362f rows=25680 OOM(撞到 20.6G)。 | ||
| 41 | -# 20000 卡在 19260(能跑)和 25680(OOM)之间,留安全余量。 | ||
| 42 | -# 支持环境变量覆盖:只用于临时拉高某一个 worker 做边界探测实验,默认值(生产用的这道 | ||
| 43 | -# 安全线)不变——测完记得把起这个 worker 的进程重启回不带这个环境变量的状态。 | ||
| 44 | -ROWS_LIMIT = int(os.environ.get("H3_ROWS_LIMIT", 20000)) | ||
| 45 | - | ||
| 46 | -# ---------------- 任务模型 ---------------- | ||
| 47 | -class GenReq(BaseModel): | ||
| 48 | - prompt: str = Field(min_length=1, max_length=2000) | ||
| 49 | - # 上界取模型自己的默认档 1344x768。pipeline 本身不限分辨率,只把长宽向上取整到 32 的倍数, | ||
| 50 | - # 真正的墙是这张 22G 卡:序列行数按 h*w/1024 每潜帧算,832x480 是 390 行/帧 x 37 帧 = 14430 行, | ||
| 51 | - # 1344x768 是 1008 行/帧 = 37296 行,注意力是平方级,涨 2.6 倍行数就是涨约 6.7 倍算力。 | ||
| 52 | - # 832x480 带三张参考图实测峰值 13.7G,往上哪一档撞墙没测过,超了就是这条任务 failed,不会拖死服务。 | ||
| 53 | - width: int = Field(832, ge=64, le=1344) | ||
| 54 | - height: int = Field(480, ge=64, le=768) | ||
| 55 | - # 上界 362=15s(协议规格上限),是否真能生成看下面 _check_rows——832x480 撞了还是会被拒。 | ||
| 56 | - frames: int = Field(124, ge=8, le=362) | ||
| 57 | - steps: int = Field(4, ge=1, le=50) | ||
| 58 | - turbo: bool = True | ||
| 59 | - seed: int = Field(0, ge=0, le=2**31 - 1) | ||
| 60 | - flow_shift: Optional[float] = None | ||
| 61 | - # 首末帧条件(FL2VA)。给了首帧,这一段就从那张画面长出去——同一间屋子、同一个人。 | ||
| 62 | - # 不给就是纯文生视频,屋子和人由模型自己编,跨条对不上。 | ||
| 63 | - first_frame_url: Optional[str] = None | ||
| 64 | - last_frame_url: Optional[str] = None | ||
| 65 | - # 参考图(Ref2VA)。首帧只管这一段从哪开始,参考图全程参与注意力,管的是「中间别乱改」。 | ||
| 66 | - # 张数没有硬上限,代价是序列变长:一张按 ref_image_short_edge 缩放后折成 token, | ||
| 67 | - # 短边 480 时约 390 行,而目标视频本身才 14430 行。 | ||
| 68 | - reference_image_urls: list[str] = [] | ||
| 69 | - ref_image_short_edge: int = Field(480, ge=64, le=2048) | ||
| 70 | - # CFG 引导强度 / 负面提示词。pipeline 自己默认 cfg_scale=1.0(约等于不引导)、 | ||
| 71 | - # negative_prompt="",我们的代码之前从没传过,一直裸跑默认值。Optional,留空=不覆盖, | ||
| 72 | - # 跟以前行为一样;蒸馏 LoRA(turbo)模型引导太强有时反而出伪影,调高前留意这点。 | ||
| 73 | - cfg_scale: Optional[float] = Field(None, ge=0) | ||
| 74 | - negative_prompt: Optional[str] = Field(None, max_length=2000) | ||
| 75 | - | ||
| 76 | - @model_validator(mode="after") | ||
| 77 | - def _check_rows(self): | ||
| 78 | - rows = _rows(self.width, self.height, self.frames) | ||
| 79 | - if rows > ROWS_LIMIT: | ||
| 80 | - raise ValueError(f"分辨率×帧数超出显存安全上限(rows={rows:.0f} > {ROWS_LIMIT});" | ||
| 81 | - f"降低 frames 或分辨率,例如 448x256/576x320 可到 362 帧,832x480 建议 ≤124 帧") | ||
| 82 | - return self | ||
| 83 | - | ||
| 84 | -class Job(BaseModel): | ||
| 85 | - id: str | ||
| 86 | - status: Literal["queued", "loading", "running", "done", "failed"] | ||
| 87 | - req: GenReq | ||
| 88 | - created_at: float | ||
| 89 | - started_at: Optional[float] = None | ||
| 90 | - finished_at: Optional[float] = None | ||
| 91 | - step: int = 0 | ||
| 92 | - total_steps: int = 0 | ||
| 93 | - file: Optional[str] = None | ||
| 94 | - size_bytes: Optional[int] = None | ||
| 95 | - error: Optional[str] = None | ||
| 96 | - peak_vram_gb: Optional[float] = None | ||
| 97 | - | ||
| 98 | -JOBS: dict[str, Job] = {} | ||
| 99 | -Q: "queue.Queue[str]" = queue.Queue() | ||
| 100 | -LOCK = threading.Lock() | ||
| 101 | -STATE = {"model": "loading", "loaded_at": None, "gpu": os.environ.get("CUDA_VISIBLE_DEVICES", "?")} | ||
| 102 | - | ||
| 103 | -# ---------------- 任务记录落盘 ---------------- | ||
| 104 | -# 之前 JOBS 只在内存里,进程重启(比如 h3-console 的弹性调度器切分区)就把这张表清空—— | ||
| 105 | -# 视频文件在 outputs/ 没事,但 id→文件路径这层映射没了,前端/API 都查不到。改成每次状态变化 | ||
| 106 | -# 都往 sqlite 写一份,进程起来时先读回内存。四个 GPU 各一个进程,按 GPU id 分开的文件, | ||
| 107 | -# 不共享一份——共享一份要处理多进程并发写 sqlite 的锁竞争,没必要:反正一个进程只会在同一张 | ||
| 108 | -# GPU 上起来,per-GPU 的文件天然不会有另一个进程来抢。 | ||
| 109 | -JOBS_DB = BASE / f"jobs_gpu{os.environ.get('CUDA_VISIBLE_DEVICES', 'x')}.sqlite3" | ||
| 110 | -_JOBS_CON = sqlite3.connect(JOBS_DB, check_same_thread=False) | ||
| 111 | -_JOBS_CON.execute( | ||
| 112 | - "CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, updated_at REAL NOT NULL, data TEXT NOT NULL)" | ||
| 113 | -) | ||
| 114 | -_JOBS_CON.commit() | ||
| 115 | - | ||
| 116 | -def _persist(job: Job): | ||
| 117 | - with LOCK: | ||
| 118 | - _JOBS_CON.execute( | ||
| 119 | - "INSERT INTO jobs (id, updated_at, data) VALUES (?,?,?) " | ||
| 120 | - "ON CONFLICT(id) DO UPDATE SET updated_at=excluded.updated_at, data=excluded.data", | ||
| 121 | - (job.id, time.time(), job.model_dump_json()), | ||
| 122 | - ) | ||
| 123 | - _JOBS_CON.commit() | ||
| 124 | - | ||
| 125 | -def _load_jobs(): | ||
| 126 | - """进程刚起来时把上次落盘的任务记录读回内存。in-flight 的状态没法恢复——Q 是空的, | ||
| 127 | - GPU 上下文也是全新的,没有任何东西能"接着跑"。落盘时正好是 queued/running 的一律 | ||
| 128 | - 改判为 failed 并写清原因,不然它会卡成一条永远显示"运行中"但没有 worker 会再碰的 | ||
| 129 | - 死记录,比直接查不到这个 id 更容易让人误判成"卡住了,再等等"。""" | ||
| 130 | - n = 0 | ||
| 131 | - for (data,) in _JOBS_CON.execute("SELECT data FROM jobs"): | ||
| 132 | - try: | ||
| 133 | - j = Job.model_validate_json(data) | ||
| 134 | - except Exception: | ||
| 135 | - continue # 单条记录损坏不该挡住整个服务启动 | ||
| 136 | - if j.status in ("queued", "running"): | ||
| 137 | - j.status = "failed" | ||
| 138 | - j.error = "进程重启导致这条任务的执行状态丢失(GPU 上下文和排队状态不落盘,无法恢复)" | ||
| 139 | - j.finished_at = j.finished_at or time.time() | ||
| 140 | - JOBS[j.id] = j | ||
| 141 | - n += 1 | ||
| 142 | - if n: | ||
| 143 | - print(f"[api] 从磁盘恢复了 {n} 条历史任务记录({JOBS_DB.name})", flush=True) | ||
| 144 | - | ||
| 145 | -_load_jobs() | ||
| 146 | - | ||
| 147 | -# ---------------- 推理 worker ---------------- | ||
| 148 | -# DiT、processor、Turbo LoRA 三样成套,跨分区混用会对不上层。 | ||
| 149 | -# ref2v 的 Turbo LoRA 只有 v0.1,fl2v 那个是 v1.0 768p——这一项两边成色不同。 | ||
| 150 | -PARTITIONS = { | ||
| 151 | - "fl2va": {"dit": "minimax-h3-fl2va-nf4.safetensors", | ||
| 152 | - "processor": "FL2VA/processor/", | ||
| 153 | - "lora": "minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors"}, | ||
| 154 | - # ref2v 的 bf16 版实测出 124 帧噪点:同尺寸同 seed,挂上它是噪点、摘掉就出画面, | ||
| 155 | - # 换 flow_shift 无效(见 docs/H3 的二分表)。改用 comfyui 版试另一套键名。 | ||
| 156 | - "ref2va": {"dit": "minimax-h3-ref2va-nf4.safetensors", | ||
| 157 | - "processor": "Ref2VA/processor/", | ||
| 158 | - "lora": "minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors"}, | ||
| 159 | -} | ||
| 160 | - | ||
| 161 | -def _turbo_lora_config(): | ||
| 162 | - from diffsynth.pipelines.minimax_h3_audio_video import ModelConfig | ||
| 163 | - return ModelConfig(model_id="lightx2v/Minimax-h3-Turbo", | ||
| 164 | - origin_file_pattern=PARTITIONS[PARTITION]["lora"]) | ||
| 165 | - | ||
| 166 | -def _build_pipe(): | ||
| 167 | - from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 168 | - part = PARTITIONS[PARTITION] | ||
| 169 | - DT = torch.float32 # fp16 在 sm_75 上出全黑视频,见 README | ||
| 170 | - # 常驻显存里留给权重多少 GB,剩下的全是激活。默认公式是按 832x480 调的(留 14G 给激活,实测峰值 13.7G)。 | ||
| 171 | - # 分辨率往上走就得调低它:1280x720 在默认值下要 22.3G、卡只有 21.49G,差约 1G 撞 OOM; | ||
| 172 | - # 调低后权重多从内存流几趟,慢一点,换来激活空间。用 H3_VRAM_LIMIT 覆盖。 | ||
| 173 | - free = torch.cuda.mem_get_info("cuda")[0] / 1024**3 | ||
| 174 | - vram_limit = float(os.environ.get("H3_VRAM_LIMIT", max(4.0, free - 14.0))) | ||
| 175 | - offload_dt = "disk" if OFFLOAD == "disk" else DT | ||
| 176 | - vc = dict(offload_dtype=offload_dt, offload_device=OFFLOAD, | ||
| 177 | - onload_dtype=offload_dt, onload_device=OFFLOAD, | ||
| 178 | - preparing_dtype=DT, preparing_device="cuda", | ||
| 179 | - computation_dtype=DT, computation_device="cuda") | ||
| 180 | - NF4 = "DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 181 | - pipe = MiniMaxH3Pipeline.from_pretrained( | ||
| 182 | - torch_dtype=DT, device="cuda", | ||
| 183 | - model_configs=[ModelConfig(model_id=NF4, origin_file_pattern=f, **vc) for f in | ||
| 184 | - [part["dit"], "minimax-h3-text-encoder-nf4.safetensors", | ||
| 185 | - "video_vae_nf4.safetensors", "audio_vae_nf4.safetensors"]], | ||
| 186 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3", | ||
| 187 | - origin_file_pattern=part["processor"]), | ||
| 188 | - vram_limit=vram_limit) | ||
| 189 | - pipe.load_lora(pipe.dit, _turbo_lora_config()) | ||
| 190 | - return pipe | ||
| 191 | - | ||
| 192 | -def _fetch_image(url): | ||
| 193 | - from PIL import Image | ||
| 194 | - with urllib.request.urlopen(url, timeout=60) as resp: | ||
| 195 | - return Image.open(io.BytesIO(resp.read())).convert("RGB") | ||
| 196 | - | ||
| 197 | -def _keyframes(req): | ||
| 198 | - """取首末帧图,缩到出片尺寸。都没给就返回 (None, None),pipeline 退回纯文生视频。""" | ||
| 199 | - from PIL import Image | ||
| 200 | - images, indices = [], [] | ||
| 201 | - for index, url in ((0, req.first_frame_url), (-1, req.last_frame_url)): | ||
| 202 | - if url is None: | ||
| 203 | - continue | ||
| 204 | - images.append(_fetch_image(url).resize((req.width, req.height), Image.LANCZOS)) | ||
| 205 | - indices.append(index) | ||
| 206 | - return (images, indices) if images else (None, None) | ||
| 207 | - | ||
| 208 | -def _references(req): | ||
| 209 | - """参考图按请求顺序取下来。尺寸交给 pipeline 按 ref_image_short_edge 归一,这里不动。""" | ||
| 210 | - if not req.reference_image_urls: | ||
| 211 | - return None | ||
| 212 | - return [{"type": "image", "image": _fetch_image(url)} for url in req.reference_image_urls] | ||
| 213 | - | ||
| 214 | -def worker(): | ||
| 215 | - from diffsynth.utils.data.audio_video import write_video_audio | ||
| 216 | - import numpy as np | ||
| 217 | - t0 = time.time() | ||
| 218 | - pipe = _build_pipe() | ||
| 219 | - STATE["model"] = "ready"; STATE["loaded_at"] = time.time() | ||
| 220 | - print(f"[api] 模型就绪,耗时 {time.time()-t0:.0f}s", flush=True) | ||
| 221 | - | ||
| 222 | - while True: | ||
| 223 | - jid = Q.get() | ||
| 224 | - j = JOBS[jid] | ||
| 225 | - try: | ||
| 226 | - j.status = "running"; j.started_at = time.time() | ||
| 227 | - j.total_steps = j.req.steps | ||
| 228 | - _persist(j) | ||
| 229 | - torch.cuda.reset_peak_memory_stats() | ||
| 230 | - fs = j.req.flow_shift if j.req.flow_shift is not None else (6.0 if j.req.turbo else 12.0) | ||
| 231 | - if not j.req.turbo: | ||
| 232 | - pipe.clear_lora() | ||
| 233 | - keyframes, keyframe_indices = _keyframes(j.req) | ||
| 234 | - cfg_scale = j.req.cfg_scale if j.req.cfg_scale is not None else 1.0 | ||
| 235 | - negative_prompt = j.req.negative_prompt if j.req.negative_prompt is not None else "" | ||
| 236 | - video, audio = pipe(prompt=j.req.prompt, negative_prompt=negative_prompt, | ||
| 237 | - height=j.req.height, width=j.req.width, | ||
| 238 | - num_frames=j.req.frames, num_inference_steps=j.req.steps, | ||
| 239 | - seed=j.req.seed, flow_shift=fs, cfg_scale=cfg_scale, | ||
| 240 | - keyframes=keyframes, keyframe_indices=keyframe_indices, | ||
| 241 | - references=_references(j.req), | ||
| 242 | - ref_image_short_edge=j.req.ref_image_short_edge) | ||
| 243 | - # 产物必须验过才算成功——全黑视频也能写出文件(踩过) | ||
| 244 | - a = np.stack([np.asarray(f.convert("RGB"), dtype=np.float32) | ||
| 245 | - for f in video[:: max(1, len(video)//8)]]) | ||
| 246 | - if a.std() < 1.0: | ||
| 247 | - raise RuntimeError(f"生成结果近乎纯色 (std={a.std():.3f}),判为失败") | ||
| 248 | - if torch.is_tensor(audio) and not torch.isfinite(audio).all(): | ||
| 249 | - raise RuntimeError("音频含非有限值,判为失败") | ||
| 250 | - out = OUT / f"api_{jid}.mp4" | ||
| 251 | - write_video_audio(video=video, audio=audio, output_path=str(out), | ||
| 252 | - fps=24, audio_sample_rate=32000) | ||
| 253 | - j.file = str(out); j.size_bytes = out.stat().st_size | ||
| 254 | - j.peak_vram_gb = round(torch.cuda.max_memory_allocated()/1024**3, 2) | ||
| 255 | - j.status = "done" | ||
| 256 | - except Exception as e: | ||
| 257 | - j.status = "failed"; j.error = f"{type(e).__name__}: {e}" | ||
| 258 | - traceback.print_exc() | ||
| 259 | - finally: | ||
| 260 | - j.finished_at = time.time() | ||
| 261 | - _persist(j) | ||
| 262 | - if not j.req.turbo: # 下一个任务可能要 turbo,把 LoRA 挂回去 | ||
| 263 | - try: | ||
| 264 | - pipe.load_lora(pipe.dit, _turbo_lora_config()) | ||
| 265 | - except Exception: | ||
| 266 | - traceback.print_exc() | ||
| 267 | - Q.task_done() | ||
| 268 | - | ||
| 269 | -# ---------------- HTTP ---------------- | ||
| 270 | -app = FastAPI(title="MiniMax-H3 Video API", version="1.0") | ||
| 271 | - | ||
| 272 | -@app.on_event("startup") | ||
| 273 | -def _start(): | ||
| 274 | - threading.Thread(target=worker, daemon=True).start() | ||
| 275 | - | ||
| 276 | -@app.get("/health") | ||
| 277 | -def health(): | ||
| 278 | - return {"status": "ok" if STATE["model"] == "ready" else "loading", | ||
| 279 | - "model": STATE["model"], "gpu": STATE["gpu"], "partition": PARTITION, | ||
| 280 | - "offload": OFFLOAD, "queued": Q.qsize(), | ||
| 281 | - "running": sum(1 for j in JOBS.values() if j.status == "running")} | ||
| 282 | - | ||
| 283 | -@app.post("/v1/videos", status_code=202) | ||
| 284 | -def create(req: GenReq): | ||
| 285 | - if req.turbo and req.steps > 8: | ||
| 286 | - raise HTTPException(400, "turbo 模式步数应 ≤8(LoRA 是 4 步蒸馏的);要多步请传 turbo=false") | ||
| 287 | - jid = uuid.uuid4().hex[:12] | ||
| 288 | - with LOCK: | ||
| 289 | - JOBS[jid] = Job(id=jid, status="queued", req=req, created_at=time.time()) | ||
| 290 | - _persist(JOBS[jid]) # LOCK 外面调用 —— _persist 自己会拿 LOCK,Lock 不可重入,嵌套会死锁 | ||
| 291 | - Q.put(jid) | ||
| 292 | - # 耗时的自变量是序列行数(潜帧数 x 宽 x 高 / 1024),不是分辨率——分辨率和帧数是同一笔预算。 | ||
| 293 | - # 基准点 832x480x124f = 14430 行,4 步实测 692 秒 = 4 x 132 + 163 | ||
| 294 | - #(163 是文本编码 + 参考图编码 + VAE 解码,它也跟帧数走,所以一起按行数缩放)。 | ||
| 295 | - # 回代:1280x736x56f(15640 行)估 749 / 实测 736;832x480x22f(2730 行)估 131 / 实测 96。 | ||
| 296 | - # 注意力是平方级,行数越大这个线性估计偏得越小。 | ||
| 297 | - rows = _rows(req.width, req.height, req.frames) | ||
| 298 | - est = round((req.steps * 132 + 163) * rows / 14430) | ||
| 299 | - return {"id": jid, "status": "queued", "queue_position": Q.qsize(), | ||
| 300 | - "eta_seconds": est, "poll": f"/v1/videos/{jid}"} | ||
| 301 | - | ||
| 302 | -@app.get("/v1/videos/{jid}") | ||
| 303 | -def get(jid: str): | ||
| 304 | - j = JOBS.get(jid) | ||
| 305 | - if not j: raise HTTPException(404, "无此任务") | ||
| 306 | - d = j.model_dump() | ||
| 307 | - if j.status == "running" and j.started_at: | ||
| 308 | - d["elapsed_seconds"] = round(time.time() - j.started_at) | ||
| 309 | - if j.status == "done": | ||
| 310 | - d["download"] = f"/v1/videos/{jid}/file" | ||
| 311 | - return d | ||
| 312 | - | ||
| 313 | -@app.get("/v1/videos/{jid}/file") | ||
| 314 | -def download(jid: str): | ||
| 315 | - j = JOBS.get(jid) | ||
| 316 | - if not j: raise HTTPException(404, "无此任务") | ||
| 317 | - if j.status != "done": raise HTTPException(409, f"任务尚未完成(当前 {j.status})") | ||
| 318 | - return FileResponse(j.file, media_type="video/mp4", filename=f"{jid}.mp4") | ||
| 319 | - | ||
| 320 | -@app.get("/v1/videos") | ||
| 321 | -def listjobs(limit: int = 50): | ||
| 322 | - js = sorted(JOBS.values(), key=lambda x: x.created_at, reverse=True)[:limit] | ||
| 323 | - return {"jobs": [{"id": j.id, "status": j.status, "created_at": j.created_at, | ||
| 324 | - "size_bytes": j.size_bytes, "error": j.error} for j in js]} |
minimax-h3/burst-test.py deleted
| 1 | -"""压并发:12 条同时投给调度层,看三件事。 | ||
| 2 | - | ||
| 3 | -1. 派发会不会打偏——调度层是「读 health 挑最闲的」,同时提交时几个请求可能读到同一份快照。 | ||
| 4 | - 顺序提交测不出这个,必须真并发。 | ||
| 5 | -2. queue_position 和 eta_seconds 在排队时说了什么。 | ||
| 6 | -3. 队列有没有上限、超了会怎样。 | ||
| 7 | -""" | ||
| 8 | -import json, time, urllib.request | ||
| 9 | -from collections import Counter | ||
| 10 | -from concurrent.futures import ThreadPoolExecutor | ||
| 11 | - | ||
| 12 | -GW = "http://127.0.0.1:8000" | ||
| 13 | -N = 12 | ||
| 14 | -SPEC = {"prompt": "一只橘猫趴在洒满阳光的窗台上,慢慢眨眼", "frames": 22, "steps": 4, "turbo": True} | ||
| 15 | - | ||
| 16 | - | ||
| 17 | -def call(path, data=None, timeout=60): | ||
| 18 | - req = urllib.request.Request(GW + path, data=data, | ||
| 19 | - headers={"Content-Type": "application/json"} if data else {}) | ||
| 20 | - with urllib.request.urlopen(req, timeout=timeout) as r: | ||
| 21 | - return json.load(r) | ||
| 22 | - | ||
| 23 | - | ||
| 24 | -def submit(i): | ||
| 25 | - t = time.time() | ||
| 26 | - job = call("/v1/videos", json.dumps(dict(SPEC, seed=100 + i)).encode()) | ||
| 27 | - return i, job, time.time() - t | ||
| 28 | - | ||
| 29 | - | ||
| 30 | -t0 = time.time() | ||
| 31 | -with ThreadPoolExecutor(max_workers=N) as pool: | ||
| 32 | - results = sorted(pool.map(submit, range(N))) | ||
| 33 | -print(f"12 条并发提交耗时 {time.time() - t0:.2f}s\n") | ||
| 34 | - | ||
| 35 | -for i, job, dt in results: | ||
| 36 | - print(f" #{i:<2} {job['backend'][-4:]} queue_position={job['queue_position']} " | ||
| 37 | - f"eta={job['eta_seconds']}s ({dt:.2f}s)") | ||
| 38 | - | ||
| 39 | -spread = Counter(job["backend"][-4:] for _, job, _ in results) | ||
| 40 | -print(f"\n派发分布: {dict(sorted(spread.items()))} 理想是每个 {N // 4} 条") | ||
| 41 | - | ||
| 42 | -ids = [job["id"] for _, job, _ in results] | ||
| 43 | -print("\n--- 轮询") | ||
| 44 | -while True: | ||
| 45 | - time.sleep(30) | ||
| 46 | - states = [call(f"/v1/videos/{jid}") for jid in ids] | ||
| 47 | - c = Counter(s["status"] for s in states) | ||
| 48 | - print(f" {dict(c)}", flush=True) | ||
| 49 | - if all(s["status"] in ("done", "failed") for s in states): | ||
| 50 | - break | ||
| 51 | - | ||
| 52 | -total = time.time() - t0 | ||
| 53 | -print(f"\n12 条全部完成,墙钟 {total:.0f}s") | ||
| 54 | -for s in sorted(states, key=lambda s: s["created_at"]): | ||
| 55 | - took = s["finished_at"] - s["started_at"] | ||
| 56 | - wait = s["started_at"] - s["created_at"] | ||
| 57 | - print(f" {s['id'][:6]} {s['status']} 排队{wait:5.0f}s 执行{took:4.0f}s {s['backend'][-4:]}") |
minimax-h3/concurrency-test.py deleted
| 1 | -"""四张卡同时跑同一条任务,量并发缩水。 | ||
| 2 | - | ||
| 3 | -各端口和自己的单跑基线比:cpu 模式 692s,disk 模式 767s。 | ||
| 4 | -任务规格从 8011 上那条已完成的 826db7b3f08e 原样取,保证四条完全一致。 | ||
| 5 | -""" | ||
| 6 | -import json, time, urllib.request | ||
| 7 | - | ||
| 8 | -# 8010/8011 是三小时前起的,跑的还是加 H3_OFFLOAD 之前的 api.py,/health 里没有 offload 字段, | ||
| 9 | -# 所以模式和基线都写在这里。 | ||
| 10 | -MODE = {8010: "cpu", 8011: "cpu", 8012: "disk", 8013: "disk"} | ||
| 11 | -BASELINE = {"cpu": 692, "disk": 767} | ||
| 12 | - | ||
| 13 | - | ||
| 14 | -def call(port, path, body=None): | ||
| 15 | - url = f"http://127.0.0.1:{port}{path}" | ||
| 16 | - req = urllib.request.Request(url, data=body, | ||
| 17 | - headers={"Content-Type": "application/json"} if body else {}) | ||
| 18 | - with urllib.request.urlopen(req, timeout=60) as r: | ||
| 19 | - return json.load(r) | ||
| 20 | - | ||
| 21 | - | ||
| 22 | -spec = json.dumps(call(8011, "/v1/videos/826db7b3f08e")["req"]).encode() | ||
| 23 | - | ||
| 24 | -jobs = {} | ||
| 25 | -for port, mode in MODE.items(): | ||
| 26 | - jobs[port] = (call(port, "/v1/videos", spec)["id"], mode) | ||
| 27 | - print(f"{port} ({mode}) -> {jobs[port][0]}", flush=True) | ||
| 28 | - | ||
| 29 | -print("--- 四条已同时投出,开始轮询", flush=True) | ||
| 30 | -pending = dict(jobs) | ||
| 31 | -while pending: | ||
| 32 | - time.sleep(60) | ||
| 33 | - for port in list(pending): | ||
| 34 | - jid, mode = pending[port] | ||
| 35 | - j = call(port, f"/v1/videos/{jid}") | ||
| 36 | - if j["status"] in ("done", "failed"): | ||
| 37 | - took = j["finished_at"] - j["started_at"] | ||
| 38 | - base = BASELINE[mode] | ||
| 39 | - print(f"{port} ({mode}) {j['status']} {took:.0f}s 基线{base}s " | ||
| 40 | - f"缩水{(took/base-1)*100:+.1f}% 峰值显存{j.get('peak_vram_gb')}G " | ||
| 41 | - f"{j.get('size_bytes')}字节", flush=True) | ||
| 42 | - del pending[port] | ||
| 43 | - else: | ||
| 44 | - print(f" {port} {j['status']} {j.get('elapsed_seconds', 0)}s", flush=True) |
minimax-h3/direct-backend-test.py deleted
| 1 | -"""验直连后端端口这条老路还通不通——调度层是加在旁边的,不该动到它。 | ||
| 2 | - | ||
| 3 | -走的是 API.md 里那段完整调用示例的形状:POST 拿 id → 轮询 → 下载。 | ||
| 4 | -""" | ||
| 5 | -import json, time, urllib.request | ||
| 6 | - | ||
| 7 | -API = "http://127.0.0.1:8010" | ||
| 8 | -SPEC = {"prompt": "一只橘猫趴在洒满阳光的窗台上,慢慢眨眼", "frames": 22, "steps": 4, | ||
| 9 | - "turbo": True, "seed": 99} | ||
| 10 | - | ||
| 11 | - | ||
| 12 | -def call(path, data=None, timeout=60): | ||
| 13 | - req = urllib.request.Request(API + path, data=data, | ||
| 14 | - headers={"Content-Type": "application/json"} if data else {}) | ||
| 15 | - with urllib.request.urlopen(req, timeout=timeout) as r: | ||
| 16 | - return json.load(r) | ||
| 17 | - | ||
| 18 | - | ||
| 19 | -print("health:", call("/health")) | ||
| 20 | -job = call("/v1/videos", json.dumps(SPEC).encode()) | ||
| 21 | -print("提交:", job) | ||
| 22 | - | ||
| 23 | -while True: | ||
| 24 | - time.sleep(20) | ||
| 25 | - j = call(f"/v1/videos/{job['id']}") | ||
| 26 | - print(" ", j["status"], j.get("elapsed_seconds", 0), "s", flush=True) | ||
| 27 | - if j["status"] in ("done", "failed"): | ||
| 28 | - break | ||
| 29 | - | ||
| 30 | -if j["status"] == "failed": | ||
| 31 | - raise SystemExit("失败: " + j["error"]) | ||
| 32 | - | ||
| 33 | -with urllib.request.urlopen(f"{API}/v1/videos/{job['id']}/file", timeout=300) as r: | ||
| 34 | - data = r.read() | ||
| 35 | -print(f"下载 {len(data)} 字节,后端记录 {j['size_bytes']},一致={len(data) == j['size_bytes']}") |
minimax-h3/gateway-run.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 起调度层。用法: ./gateway-run.sh [PORT] | ||
| 3 | -# 后端列表用 H3_BACKENDS 覆盖,默认 8010,8011,8012,8013。 | ||
| 4 | -# 这一层不碰模型也不占显存,秒起秒停,随便重启。 | ||
| 5 | -set -uo pipefail | ||
| 6 | -B=/data/models/minimax-h3 | ||
| 7 | -PORT="${1:-8000}" | ||
| 8 | -export PYTHONUNBUFFERED=1 | ||
| 9 | -cd "$B" | ||
| 10 | -echo "[gateway] PORT=$PORT 后端=${H3_BACKENDS:-8010,8011,8012,8013}" | ||
| 11 | -exec "$B/venv/bin/python" -m uvicorn gateway:app --host 0.0.0.0 --port "$PORT" --workers 1 |
minimax-h3/gateway-test.py deleted
| 1 | -"""验调度层:往 :8000 投 5 条(比后端多一条),看是不是每卡一条、多出来那条排队。 | ||
| 2 | - | ||
| 3 | -用短任务(22 帧)只是为了快,路由逻辑和长任务完全一样。 | ||
| 4 | -""" | ||
| 5 | -import json, time, urllib.request | ||
| 6 | - | ||
| 7 | -GW = "http://127.0.0.1:8000" | ||
| 8 | -SPEC = {"prompt": "一只橘猫趴在洒满阳光的窗台上,慢慢眨眼,尾巴轻轻摆动", | ||
| 9 | - "width": 832, "height": 480, "frames": 22, "steps": 4, "turbo": True, "seed": 7} | ||
| 10 | - | ||
| 11 | - | ||
| 12 | -def call(path, data=None, timeout=60): | ||
| 13 | - req = urllib.request.Request(GW + path, data=data, | ||
| 14 | - headers={"Content-Type": "application/json"} if data else {}) | ||
| 15 | - with urllib.request.urlopen(req, timeout=timeout) as r: | ||
| 16 | - return json.load(r) | ||
| 17 | - | ||
| 18 | - | ||
| 19 | -ids = [] | ||
| 20 | -for i in range(5): | ||
| 21 | - job = call("/v1/videos", json.dumps(dict(SPEC, seed=7 + i)).encode()) | ||
| 22 | - ids.append(job["id"]) | ||
| 23 | - print(f"第{i+1}条 -> {job['backend']} id={job['id']} 队列位置={job['queue_position']}", flush=True) | ||
| 24 | - | ||
| 25 | -print("\n--- 派发结果") | ||
| 26 | -used = {} | ||
| 27 | -for jid in ids: | ||
| 28 | - b = call(f"/v1/videos/{jid}")["backend"] | ||
| 29 | - used.setdefault(b, []).append(jid) | ||
| 30 | -for b, js in sorted(used.items()): | ||
| 31 | - print(f"{b}: {len(js)} 条 {js}") | ||
| 32 | -print(f"用上了 {len(used)}/4 个后端") | ||
| 33 | - | ||
| 34 | -print("\n--- 轮询到全部完成") | ||
| 35 | -while True: | ||
| 36 | - time.sleep(30) | ||
| 37 | - states = [call(f"/v1/videos/{jid}") for jid in ids] | ||
| 38 | - print(" " + " ".join(f"{s['id'][:6]}:{s['status']}" for s in states), flush=True) | ||
| 39 | - if all(s["status"] in ("done", "failed") for s in states): | ||
| 40 | - break | ||
| 41 | - | ||
| 42 | -print("\n--- 结果") | ||
| 43 | -for s in states: | ||
| 44 | - took = (s["finished_at"] - s["started_at"]) if s.get("started_at") else 0 | ||
| 45 | - print(f"{s['id']} {s['status']} {took:.0f}s {s.get('size_bytes')}字节 {s['backend']}") | ||
| 46 | - | ||
| 47 | -ok = [s for s in states if s["status"] == "done"] | ||
| 48 | -if ok: | ||
| 49 | - with urllib.request.urlopen(f"{GW}/v1/videos/{ok[0]['id']}/file", timeout=300) as r: | ||
| 50 | - data = r.read() | ||
| 51 | - print(f"\n经调度层下载 {ok[0]['id']}: {len(data)} 字节 " | ||
| 52 | - f"(后端记录 {ok[0]['size_bytes']}) 一致={len(data) == ok[0]['size_bytes']}") |
minimax-h3/gateway.py deleted
| 1 | -"""四个 H3 实例前面的调度层。 | ||
| 2 | - | ||
| 3 | -api.py 是单 worker 串行,一个进程独占一张卡,所以并发的唯一办法是往不同端口投。 | ||
| 4 | -这一层把「投哪个端口」收掉:对外只有一个地址,接口和 api.py 逐字相同, | ||
| 5 | -调用方原来怎么写现在还怎么写,只把端口换成 8000。 | ||
| 6 | - | ||
| 7 | -自己不存任何状态。任务 id 归哪个后端,问一遍就知道——四个后端一次扫描的代价可以忽略, | ||
| 8 | -换来的是这个进程随时能重启,不丢任何东西(api.py 的 JOBS 本来也在内存里, | ||
| 9 | -调度层再存一份 id→后端 的映射,就是第二个会丢的地方)。 | ||
| 10 | -""" | ||
| 11 | -import os, json, threading, urllib.request, urllib.error | ||
| 12 | -from concurrent.futures import ThreadPoolExecutor | ||
| 13 | - | ||
| 14 | -from fastapi import FastAPI, HTTPException | ||
| 15 | -from fastapi.responses import StreamingResponse | ||
| 16 | - | ||
| 17 | -BACKENDS = [f"http://127.0.0.1:{port.strip()}" for port in | ||
| 18 | - os.environ.get("H3_BACKENDS", "8010,8011,8012,8013").split(",")] | ||
| 19 | -POOL = ThreadPoolExecutor(max_workers=16) | ||
| 20 | -# 最闲的那个后端已经压了这么多条时,再来的提交直接 429 顶回去。 | ||
| 21 | -# 没有这个上限的话队列是无界的:投 1000 条全部返回 202,最后一条两天后才开始跑, | ||
| 22 | -# 调用方却收到「已接受」。宁可当场说满了。默认 4 条 ≈ 45 分钟积压。 | ||
| 23 | -MAX_QUEUE = int(os.environ.get("H3_MAX_QUEUE", "4")) | ||
| 24 | -# 选后端是「读一遍各家队列深度,再往最闲的那个投」。这中间不能插进另一个提交, | ||
| 25 | -# 否则同时来的请求会读到同一份快照、一起挑中同一个后端,全堆在一张卡上。 | ||
| 26 | -# 锁只圈住选 + 投这一段(约 30 毫秒),任务本身要跑十几分钟,序列化的代价可以忽略。 | ||
| 27 | -PICK = threading.Lock() | ||
| 28 | - | ||
| 29 | - | ||
| 30 | -def fetch(url, data=None, timeout=30): | ||
| 31 | - req = urllib.request.Request(url, data=data, | ||
| 32 | - headers={"Content-Type": "application/json"} if data else {}) | ||
| 33 | - with urllib.request.urlopen(req, timeout=timeout) as resp: | ||
| 34 | - return json.load(resp) | ||
| 35 | - | ||
| 36 | - | ||
| 37 | -def health_all(): | ||
| 38 | - def one(backend): | ||
| 39 | - try: | ||
| 40 | - return dict(fetch(backend + "/health", timeout=5), backend=backend) | ||
| 41 | - except Exception as e: | ||
| 42 | - return {"backend": backend, "status": "down", "error": f"{type(e).__name__}: {e}"} | ||
| 43 | - return list(POOL.map(one, BACKENDS)) | ||
| 44 | - | ||
| 45 | - | ||
| 46 | -def find_owner(jid): | ||
| 47 | - """任务归谁,挨个问。后端对陌生 id 返回 404,urlopen 抛异常,接住当没有。""" | ||
| 48 | - def one(backend): | ||
| 49 | - try: | ||
| 50 | - return backend, fetch(f"{backend}/v1/videos/{jid}", timeout=5) | ||
| 51 | - except Exception: | ||
| 52 | - return backend, None | ||
| 53 | - for backend, job in POOL.map(one, BACKENDS): | ||
| 54 | - if job is not None: | ||
| 55 | - return backend, job | ||
| 56 | - raise HTTPException(404, "无此任务") | ||
| 57 | - | ||
| 58 | - | ||
| 59 | -app = FastAPI(title="MiniMax-H3 Gateway", version="1.0") | ||
| 60 | - | ||
| 61 | - | ||
| 62 | -@app.get("/health") | ||
| 63 | -def health(): | ||
| 64 | - backends = health_all() | ||
| 65 | - ready = [b for b in backends if b.get("status") == "ok"] | ||
| 66 | - return {"status": "ok" if ready else "unavailable", | ||
| 67 | - "ready": len(ready), "total": len(backends), | ||
| 68 | - "queued": sum(b.get("queued", 0) for b in ready), | ||
| 69 | - "running": sum(b.get("running", 0) for b in ready), | ||
| 70 | - "backends": backends} | ||
| 71 | - | ||
| 72 | - | ||
| 73 | -# 同步 def,让 FastAPI 把它丢进线程池。写成 async def 会让下面这些阻塞的 HTTP 调用 | ||
| 74 | -# 卡住事件循环——那样确实也能歪打正着地把提交排成队,但任何一个后端卡住, | ||
| 75 | -# 5 秒超时就会连轮询一起堵死。要串行就用锁明说。 | ||
| 76 | -@app.post("/v1/videos", status_code=202) | ||
| 77 | -def create(req: dict): | ||
| 78 | - # 给了首末帧就只能走 fl2va;其余都走 ref2va——ref2va 不给参考图就退回纯文生视频, | ||
| 79 | - # 所以「没有首末帧」这一类不需要再细分。 | ||
| 80 | - want = "fl2va" if (req.get("first_frame_url") or req.get("last_frame_url")) else "ref2va" | ||
| 81 | - with PICK: | ||
| 82 | - candidates = [b for b in health_all() | ||
| 83 | - if b.get("status") == "ok" and b.get("partition") == want] | ||
| 84 | - if not candidates: | ||
| 85 | - raise HTTPException(503, f"没有就绪的 {want} 实例") | ||
| 86 | - # 排队最短的那个。queued 是还在队列里的,running 是正在跑的那条,两个都要算。 | ||
| 87 | - pick = min(candidates, key=lambda b: b["queued"] + b["running"]) | ||
| 88 | - ahead = pick["queued"] + pick["running"] | ||
| 89 | - if ahead >= MAX_QUEUE: | ||
| 90 | - raise HTTPException(429, f"队列满:{len(candidates)} 个 {want} 实例每个都压着 " | ||
| 91 | - f"{ahead} 条(上限 {MAX_QUEUE})。等一会儿再投," | ||
| 92 | - f"或调大 H3_MAX_QUEUE") | ||
| 93 | - try: | ||
| 94 | - job = fetch(pick["backend"] + "/v1/videos", data=json.dumps(req).encode()) | ||
| 95 | - except urllib.error.HTTPError as e: | ||
| 96 | - raise HTTPException(e.code, e.read().decode()) | ||
| 97 | - # eta_seconds 是这一条自己的执行时间,不含排队。前面还压着 ahead 条, | ||
| 98 | - # 真正的等待还要加上它们——它们规格可能不同,这里不替调用方瞎乘。 | ||
| 99 | - return dict(job, backend=pick["backend"], queue_ahead=ahead, poll=f"/v1/videos/{job['id']}") | ||
| 100 | - | ||
| 101 | - | ||
| 102 | -@app.get("/v1/videos/{jid}") | ||
| 103 | -def get(jid: str): | ||
| 104 | - backend, job = find_owner(jid) | ||
| 105 | - return dict(job, backend=backend) | ||
| 106 | - | ||
| 107 | - | ||
| 108 | -@app.get("/v1/videos/{jid}/file") | ||
| 109 | -def download(jid: str): | ||
| 110 | - backend, job = find_owner(jid) | ||
| 111 | - if job["status"] != "done": | ||
| 112 | - raise HTTPException(409, f"任务尚未完成(当前 {job['status']})") | ||
| 113 | - resp = urllib.request.urlopen(f"{backend}/v1/videos/{jid}/file", timeout=300) | ||
| 114 | - return StreamingResponse(resp, media_type="video/mp4", | ||
| 115 | - headers={"Content-Disposition": f'attachment; filename="{jid}.mp4"'}) | ||
| 116 | - | ||
| 117 | - | ||
| 118 | -@app.get("/v1/videos") | ||
| 119 | -def listjobs(limit: int = 50): | ||
| 120 | - def one(backend): | ||
| 121 | - try: | ||
| 122 | - return [dict(j, backend=backend) | ||
| 123 | - for j in fetch(f"{backend}/v1/videos?limit={limit}", timeout=5)["jobs"]] | ||
| 124 | - except Exception: | ||
| 125 | - return [] | ||
| 126 | - jobs = [job for part in POOL.map(one, BACKENDS) for job in part] | ||
| 127 | - jobs.sort(key=lambda j: j["created_at"], reverse=True) | ||
| 128 | - return {"jobs": jobs[:limit]} |
minimax-h3/long15s.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 15 秒(362 帧 = 17*21+5)可行性验证。低分辨率先验证长时长通路,再考虑全分辨率。 | ||
| 3 | -# 用法: ./long15s.sh <GPU> <FRAMES> <W> <H> [额外参数...] | ||
| 4 | -set -uo pipefail | ||
| 5 | -cd /data/models/minimax-h3 | ||
| 6 | -P="$(cat prompt.txt)" | ||
| 7 | -GPU="$1"; F="$2"; W="$3"; H="$4"; shift 4 | ||
| 8 | -./run.sh "$GPU" --frames "$F" --width "$W" --height "$H" --prompt "$P" --seed 42 "$@" |
minimax-h3/pin-cache.sh deleted
| 1 | -#!/bin/bash | ||
| 2 | -# 把 H3 权重的页缓存钉住:告诉内核 user.slice 下的 40 G 不许回收。 | ||
| 3 | -# 不钉的话,别的程序一申请大块内存,权重缓存就被丢掉, | ||
| 4 | -# 之后每次前向都要回机械盘 /dev/sdb 读 34.4 G,单条耗时会成倍涨。 | ||
| 5 | -# | ||
| 6 | -# 重跑安全。撤销:sudo rm /etc/systemd/system/user-.slice.d/h3-cache.conf && sudo systemctl daemon-reload | ||
| 7 | -set -e | ||
| 8 | - | ||
| 9 | -echo "需要 admin 密码(sudo 用的是你自己的密码,不是 root 的)" | ||
| 10 | - | ||
| 11 | -sudo mkdir -p /etc/systemd/system/user-.slice.d | ||
| 12 | -printf '[Slice]\nMemoryMin=40G\n' | sudo tee /etc/systemd/system/user-.slice.d/h3-cache.conf > /dev/null | ||
| 13 | -sudo systemctl daemon-reload | ||
| 14 | -sudo systemctl set-property user.slice MemoryMin=40G | ||
| 15 | -sudo systemctl set-property "user-$(id -u).slice" MemoryMin=40G | ||
| 16 | - | ||
| 17 | -echo | ||
| 18 | -echo "=== 生效情况(两行都应该是 42949672960,即 40 G)" | ||
| 19 | -grep -H . /sys/fs/cgroup/user.slice/memory.min | ||
| 20 | -grep -H . "/sys/fs/cgroup/user.slice/user-$(id -u).slice/memory.min" | ||
| 21 | - | ||
| 22 | -echo | ||
| 23 | -echo "=== 当前被保护的量" | ||
| 24 | -grep -H . /sys/fs/cgroup/user.slice/memory.current |
minimax-h3/prompt.txt deleted
| 1 | -清晨的中式庭院,一位穿浅蓝色棉麻衬衫的年轻女子坐在木质长廊上,手里捧着一杯冒着热气的茶。她微笑着说 "Good morning, the tea smells wonderful today." 阳光透过竹叶洒在她脸上,微风吹动她的头发,远处有清脆的鸟鸣声。 |
minimax-h3/realrun.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 真实测试:从文件读 prompt,避开多层 shell 引号 | ||
| 3 | -set -uo pipefail | ||
| 4 | -cd /data/models/minimax-h3 | ||
| 5 | -P="$(cat prompt.txt)" | ||
| 6 | -echo "[prompt] ${#P} 字符: ${P:0:40}..." | ||
| 7 | -case "$1" in | ||
| 8 | - turbo) ./run.sh 1 --prompt "$P" --seed 42 ;; | ||
| 9 | - full50) ./run.sh 2 --no-turbo --steps 50 --prompt "$P" --seed 42 ;; | ||
| 10 | -esac |
minimax-h3/restart-8010.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 重启 8010 的 ref2va 实例,把常驻权重压到 4G 换激活空间,验 1280x720 能不能跑。 | ||
| 3 | -set -uo pipefail | ||
| 4 | -cd /data/models/minimax-h3 | ||
| 5 | -PID=$(ss -lptn "sport = :8010" | grep -o "pid=[0-9]*" | cut -d= -f2 | sort -u) | ||
| 6 | -[ -n "$PID" ] && kill "$PID" | ||
| 7 | -sleep 8 | ||
| 8 | -export H3_VRAM_LIMIT=4 | ||
| 9 | -setsid nohup ./api-run.sh 1 8010 ref2va > logs/api-8010.log 2>&1 < /dev/null & | ||
| 10 | -sleep 12 | ||
| 11 | -pgrep -af uvicorn |
minimax-h3/run-verify.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 把调度层临时换成 H3_MAX_QUEUE=1 跑一遍验证,跑完恢复默认。 | ||
| 3 | -set -uo pipefail | ||
| 4 | -cd /data/models/minimax-h3 | ||
| 5 | - | ||
| 6 | -restart_gateway() { | ||
| 7 | - PID=$(ss -lptn "sport = :8000" | grep -o "pid=[0-9]*" | cut -d= -f2 | sort -u) | ||
| 8 | - [ -n "$PID" ] && { kill $PID; sleep 3; } | ||
| 9 | - setsid nohup ./gateway-run.sh 8000 > logs/gateway.log 2>&1 < /dev/null & | ||
| 10 | - sleep 5 | ||
| 11 | -} | ||
| 12 | - | ||
| 13 | -export H3_MAX_QUEUE=1 | ||
| 14 | -restart_gateway | ||
| 15 | -echo "调度层已起,H3_MAX_QUEUE=1" | ||
| 16 | -./venv/bin/python verify-fixes.py | ||
| 17 | - | ||
| 18 | -echo | ||
| 19 | -echo "=== 恢复默认 H3_MAX_QUEUE" | ||
| 20 | -unset H3_MAX_QUEUE | ||
| 21 | -restart_gateway | ||
| 22 | -curl -s http://127.0.0.1:8000/health | grep -o '"ready":[0-9]*' |
minimax-h3/run.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# MiniMax-H3 FL2VA NF4 launcher. usage: ./run.sh <GPU_ID> [args for t2va.py ...] | ||
| 3 | -# ./run.sh 1 # turbo 4-step, 480x832x124f | ||
| 4 | -# ./run.sh 1 --no-turbo --steps 50 # full quality | ||
| 5 | -# ./run.sh 2 --prompt "..." --seed 7 | ||
| 6 | -set -euo pipefail | ||
| 7 | -B=/data/models/minimax-h3 | ||
| 8 | -GPU="${1:-1}"; shift || true | ||
| 9 | - | ||
| 10 | -export CUDA_VISIBLE_DEVICES="$GPU" | ||
| 11 | -export DIFFSYNTH_ATTENTION_IMPLEMENTATION=torch # sm_75: no flash/sage; pin instead of auto-picking xformers | ||
| 12 | -export DIFFSYNTH_MODEL_BASE_PATH="$B/models" | ||
| 13 | -export MODELSCOPE_CACHE="$B/.mscache" | ||
| 14 | -export PYTHONUNBUFFERED=1 | ||
| 15 | -# 22GB 卡上激活与权重贴着上限,碎片会直接压垮它(实测 reserved-but-unallocated 达 2.49GB) | ||
| 16 | -export PYTORCH_ALLOC_CONF=expandable_segments:True | ||
| 17 | -# NOTE: deliberately NOT setting PYTHONNOUSERSITE -- the venv's site-packages already | ||
| 18 | -# precedes ~/.local in sys.path, so our bitsandbytes 0.50.1 shadows the broken 0.45.1 there. | ||
| 19 | - | ||
| 20 | -# GPU0/1 sit on NUMA0, GPU2/3 on NUMA1 -- keep the CPU-offloaded weights on the near node. | ||
| 21 | -case "$GPU" in 0|1) NODE=0 ;; 2|3) NODE=1 ;; *) NODE="" ;; esac | ||
| 22 | -BIND=(); if [ -n "$NODE" ] && command -v numactl >/dev/null 2>&1; then BIND=(numactl --cpunodebind="$NODE" --membind="$NODE"); fi | ||
| 23 | - | ||
| 24 | -LOG="$B/logs/gpu${GPU}_$(date +%Y%m%d-%H%M%S).log" | ||
| 25 | -echo "[run] GPU=$GPU numa=${NODE:-none} log=$LOG" | ||
| 26 | -"${BIND[@]}" "$B/venv/bin/python" "$B/scripts/t2va.py" "$@" 2>&1 | tee "$LOG" |
minimax-h3/scripts/diag-nan.py deleted
| 1 | -"""定位 NaN 的产生位置:对 dit / text_encoder 挂 forward hook, | ||
| 2 | -找出第一个『输入有限、输出非有限』的模块。""" | ||
| 3 | -import os, sys, time, torch | ||
| 4 | -sys.argv = [sys.argv[0]] | ||
| 5 | -BASE = "/data/models/minimax-h3" | ||
| 6 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION", "torch") | ||
| 7 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", f"{BASE}/models") | ||
| 8 | -os.environ.setdefault("MODELSCOPE_CACHE", f"{BASE}/.mscache") | ||
| 9 | - | ||
| 10 | -from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 11 | - | ||
| 12 | -DTYPE = torch.float16 | ||
| 13 | -free_gb = torch.cuda.mem_get_info("cuda")[0] / 1024**3 | ||
| 14 | -vram_limit = max(6.0, free_gb - 2.0) | ||
| 15 | -print(f"[env] free={free_gb:.1f}G vram_limit={vram_limit:.1f}G", flush=True) | ||
| 16 | - | ||
| 17 | -vc = dict(offload_dtype=DTYPE, offload_device="cpu", onload_dtype=DTYPE, onload_device="cpu", | ||
| 18 | - preparing_dtype=DTYPE, preparing_device="cuda", | ||
| 19 | - computation_dtype=DTYPE, computation_device="cuda") | ||
| 20 | -NF4 = "DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 21 | -t0 = time.time() | ||
| 22 | -pipe = MiniMaxH3Pipeline.from_pretrained( | ||
| 23 | - torch_dtype=DTYPE, device="cuda", | ||
| 24 | - model_configs=[ | ||
| 25 | - ModelConfig(model_id=NF4, origin_file_pattern="minimax-h3-fl2va-nf4.safetensors", **vc), | ||
| 26 | - ModelConfig(model_id=NF4, origin_file_pattern="minimax-h3-text-encoder-nf4.safetensors", **vc), | ||
| 27 | - ModelConfig(model_id=NF4, origin_file_pattern="video_vae_nf4.safetensors", **vc), | ||
| 28 | - ModelConfig(model_id=NF4, origin_file_pattern="audio_vae_nf4.safetensors", **vc), | ||
| 29 | - ], | ||
| 30 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3", origin_file_pattern="FL2VA/processor/"), | ||
| 31 | - vram_limit=vram_limit) | ||
| 32 | -print(f"[load] {time.time()-t0:.0f}s", flush=True) | ||
| 33 | - | ||
| 34 | -pipe.load_lora(pipe.dit, ModelConfig(model_id="lightx2v/Minimax-h3-Turbo", | ||
| 35 | - origin_file_pattern="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors")) | ||
| 36 | - | ||
| 37 | -def finite(x): | ||
| 38 | - if torch.is_tensor(x): | ||
| 39 | - return bool(torch.isfinite(x).all()), (float(x.abs().max()) if x.numel() and x.is_floating_point() else None) | ||
| 40 | - return True, None | ||
| 41 | - | ||
| 42 | -hits = [] # 第一个坏掉的模块 | ||
| 43 | -seen = set() | ||
| 44 | -def mk(name): | ||
| 45 | - def hook(mod, inp, out): | ||
| 46 | - if name in seen: return | ||
| 47 | - ok_in = all(finite(t)[0] for t in inp if torch.is_tensor(t)) | ||
| 48 | - outs = out if isinstance(out, (tuple, list)) else (out,) | ||
| 49 | - for o in outs: | ||
| 50 | - ok, mx = finite(o) | ||
| 51 | - if not ok: | ||
| 52 | - seen.add(name) | ||
| 53 | - hits.append((name, type(mod).__name__, ok_in, mx)) | ||
| 54 | - return | ||
| 55 | - return hook | ||
| 56 | - | ||
| 57 | -handles = [] | ||
| 58 | -for tag, mod in (("dit", pipe.dit), ("txt", pipe.text_encoder)): | ||
| 59 | - for n, m in mod.named_modules(): | ||
| 60 | - if n: # 跳过根 | ||
| 61 | - handles.append(m.register_forward_hook(mk(f"{tag}.{n}"))) | ||
| 62 | -print(f"[hook] {len(handles)} 个模块已挂钩", flush=True) | ||
| 63 | - | ||
| 64 | -video, audio = pipe(prompt="一只橘猫在窗台上打哈欠,阳光洒进来", | ||
| 65 | - height=480, width=832, num_frames=124, | ||
| 66 | - num_inference_steps=4, seed=42, flow_shift=6.0) | ||
| 67 | -for h in handles: h.remove() | ||
| 68 | - | ||
| 69 | -print("\n===== 结果 =====", flush=True) | ||
| 70 | -for t, nm in (("video", video), ("audio", audio)): | ||
| 71 | - if torch.is_tensor(nm): | ||
| 72 | - print(f"{t}: shape={tuple(nm.shape)} dtype={nm.dtype} finite={bool(torch.isfinite(nm).all())} " | ||
| 73 | - f"absmax={float(nm.abs().max()) if nm.is_floating_point() else 'n/a'}") | ||
| 74 | - else: | ||
| 75 | - print(f"{t}: type={type(nm)}") | ||
| 76 | -print(f"\n第一批非有限输出的模块(共 {len(hits)},只列前 15):") | ||
| 77 | -for n, cls, ok_in, mx in hits[:15]: | ||
| 78 | - print(f" {n} [{cls}] 输入有限={ok_in} 输出absmax={mx}") | ||
| 79 | -if not hits: | ||
| 80 | - print(" 无 —— dit/text_encoder 内部全程有限,问题在 VAE 解码或后处理") |
minimax-h3/scripts/diag3.py deleted
| 1 | -"""定位 NaN 的产生位置:对 dit / text_encoder 挂 forward hook, | ||
| 2 | -找出第一个『输入有限、输出非有限』的模块。""" | ||
| 3 | -import os, sys, time, torch | ||
| 4 | -sys.argv = [sys.argv[0]] | ||
| 5 | -BASE = "/data/models/minimax-h3" | ||
| 6 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION", "torch") | ||
| 7 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", f"{BASE}/models") | ||
| 8 | -os.environ.setdefault("MODELSCOPE_CACHE", f"{BASE}/.mscache") | ||
| 9 | - | ||
| 10 | -from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 11 | - | ||
| 12 | -DTYPE = torch.float16 | ||
| 13 | -free_gb = torch.cuda.mem_get_info("cuda")[0] / 1024**3 | ||
| 14 | -vram_limit = max(6.0, free_gb - 2.0) | ||
| 15 | -print(f"[env] free={free_gb:.1f}G vram_limit={vram_limit:.1f}G", flush=True) | ||
| 16 | - | ||
| 17 | -vc = dict(offload_dtype=DTYPE, offload_device="cpu", onload_dtype=DTYPE, onload_device="cpu", | ||
| 18 | - preparing_dtype=DTYPE, preparing_device="cuda", | ||
| 19 | - computation_dtype=DTYPE, computation_device="cuda") | ||
| 20 | -NF4 = "DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 21 | -t0 = time.time() | ||
| 22 | -pipe = MiniMaxH3Pipeline.from_pretrained( | ||
| 23 | - torch_dtype=DTYPE, device="cuda", | ||
| 24 | - model_configs=[ | ||
| 25 | - ModelConfig(model_id=NF4, origin_file_pattern="minimax-h3-fl2va-nf4.safetensors", **vc), | ||
| 26 | - ModelConfig(model_id=NF4, origin_file_pattern="minimax-h3-text-encoder-nf4.safetensors", **vc), | ||
| 27 | - ModelConfig(model_id=NF4, origin_file_pattern="video_vae_nf4.safetensors", **vc), | ||
| 28 | - ModelConfig(model_id=NF4, origin_file_pattern="audio_vae_nf4.safetensors", **vc), | ||
| 29 | - ], | ||
| 30 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3", origin_file_pattern="FL2VA/processor/"), | ||
| 31 | - vram_limit=vram_limit) | ||
| 32 | -print(f"[load] {time.time()-t0:.0f}s", flush=True) | ||
| 33 | - | ||
| 34 | -pipe.load_lora(pipe.dit, ModelConfig(model_id="lightx2v/Minimax-h3-Turbo", | ||
| 35 | - origin_file_pattern="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors")) | ||
| 36 | - | ||
| 37 | -# 与 t2va.py 一致的修复:整条路径(含 LoRA)抬到 fp32 | ||
| 38 | -def _fp32_all(mod): | ||
| 39 | - mod.computation_dtype = torch.float32 | ||
| 40 | - orig = mod.forward | ||
| 41 | - def fwd(x, *a, **kw): | ||
| 42 | - return orig(x.float(), *a, **kw).to(x.dtype) | ||
| 43 | - mod.forward = fwd | ||
| 44 | -_fp32_all(pipe.dit.condition_proj) | ||
| 45 | -print("[fix ] condition_proj 全路径 -> fp32", flush=True) | ||
| 46 | - | ||
| 47 | -def finite(x): | ||
| 48 | - if torch.is_tensor(x): | ||
| 49 | - return bool(torch.isfinite(x).all()), (float(x.abs().max()) if x.numel() and x.is_floating_point() else None) | ||
| 50 | - return True, None | ||
| 51 | - | ||
| 52 | -hits = [] # 第一个坏掉的模块 | ||
| 53 | -seen = set() | ||
| 54 | -def mk(name): | ||
| 55 | - def hook(mod, inp, out): | ||
| 56 | - if name in seen: return | ||
| 57 | - ok_in = all(finite(t)[0] for t in inp if torch.is_tensor(t)) | ||
| 58 | - outs = out if isinstance(out, (tuple, list)) else (out,) | ||
| 59 | - for o in outs: | ||
| 60 | - ok, mx = finite(o) | ||
| 61 | - if not ok: | ||
| 62 | - seen.add(name) | ||
| 63 | - ins=[float(t.abs().max()) for t in inp if torch.is_tensor(t) and t.is_floating_point() and t.numel()] | ||
| 64 | - hits.append((name, type(mod).__name__, ok_in, mx, max(ins) if ins else None)) | ||
| 65 | - return | ||
| 66 | - return hook | ||
| 67 | - | ||
| 68 | -handles = [] | ||
| 69 | -for tag, mod in (("dit", pipe.dit), ("txt", pipe.text_encoder)): | ||
| 70 | - for n, m in mod.named_modules(): | ||
| 71 | - if n: # 跳过根 | ||
| 72 | - handles.append(m.register_forward_hook(mk(f"{tag}.{n}"))) | ||
| 73 | -print(f"[hook] {len(handles)} 个模块已挂钩", flush=True) | ||
| 74 | - | ||
| 75 | -video, audio = pipe(prompt="一只橘猫在窗台上打哈欠,阳光洒进来", | ||
| 76 | - height=480, width=832, num_frames=124, | ||
| 77 | - num_inference_steps=1, seed=42, flow_shift=6.0) | ||
| 78 | -for h in handles: h.remove() | ||
| 79 | - | ||
| 80 | -print("\n===== 结果 =====", flush=True) | ||
| 81 | -for t, nm in (("video", video), ("audio", audio)): | ||
| 82 | - if torch.is_tensor(nm): | ||
| 83 | - print(f"{t}: shape={tuple(nm.shape)} dtype={nm.dtype} finite={bool(torch.isfinite(nm).all())} " | ||
| 84 | - f"absmax={float(nm.abs().max()) if nm.is_floating_point() else 'n/a'}") | ||
| 85 | - else: | ||
| 86 | - print(f"{t}: type={type(nm)}") | ||
| 87 | -print(f"\n第一批非有限输出的模块(共 {len(hits)},只列前 15):") | ||
| 88 | -print(">>> 真源头(输入有限=True):") | ||
| 89 | -for h in hits: | ||
| 90 | - if h[2]: print(f" ★ {h[0]} [{h[1]}] 输入absmax={h[4]} 输出absmax={h[3]}") | ||
| 91 | -print(">>> 下游受害者(前 5):") | ||
| 92 | -for h in [x for x in hits if not x[2]][:5]: | ||
| 93 | - print(f" {h[0]} [{h[1]}]") | ||
| 94 | -if not hits: | ||
| 95 | - print(" 无 —— dit/text_encoder 内部全程有限,问题在 VAE 解码或后处理") |
minimax-h3/scripts/diag4.py deleted
| 1 | -"""用与实际失败运行完全一致的配置定位 NaN:同 prompt、同修复、同显存参数。""" | ||
| 2 | -import os, time, torch | ||
| 3 | -BASE="/data/models/minimax-h3" | ||
| 4 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION","torch") | ||
| 5 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", f"{BASE}/models") | ||
| 6 | -os.environ.setdefault("MODELSCOPE_CACHE", f"{BASE}/.mscache") | ||
| 7 | -os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF","expandable_segments:True") | ||
| 8 | -from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 9 | - | ||
| 10 | -PROMPT = ("A girl is very happy, she is speaking in english: " | ||
| 11 | - "“I enjoy working with Diffsynth-Studio, it's a perfect framework.”") | ||
| 12 | -DT=torch.float16 | ||
| 13 | -free=torch.cuda.mem_get_info("cuda")[0]/1024**3 | ||
| 14 | -vram_limit=max(6.0, free-5.0) # 与 --vram-reserve 5 一致 | ||
| 15 | -print(f"[env] free={free:.1f}G vram_limit={vram_limit:.1f}G", flush=True) | ||
| 16 | -vc=dict(offload_dtype=DT,offload_device="cpu",onload_dtype=DT,onload_device="cpu", | ||
| 17 | - preparing_dtype=DT,preparing_device="cuda",computation_dtype=DT,computation_device="cuda") | ||
| 18 | -NF4="DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 19 | -pipe=MiniMaxH3Pipeline.from_pretrained(torch_dtype=DT,device="cuda", | ||
| 20 | - model_configs=[ModelConfig(model_id=NF4,origin_file_pattern=p,**vc) for p in | ||
| 21 | - ["minimax-h3-fl2va-nf4.safetensors","minimax-h3-text-encoder-nf4.safetensors", | ||
| 22 | - "video_vae_nf4.safetensors","audio_vae_nf4.safetensors"]], | ||
| 23 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3",origin_file_pattern="FL2VA/processor/"), | ||
| 24 | - vram_limit=vram_limit) | ||
| 25 | -pipe.load_lora(pipe.dit, ModelConfig(model_id="lightx2v/Minimax-h3-Turbo", | ||
| 26 | - origin_file_pattern="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors")) | ||
| 27 | - | ||
| 28 | -FP16_MAX=65504.0 | ||
| 29 | -def _fix(mod, tag): | ||
| 30 | - mod.computation_dtype = torch.float32 | ||
| 31 | - orig = mod.forward | ||
| 32 | - def fwd(x,*a,**kw): | ||
| 33 | - out = orig(x.float(),*a,**kw) | ||
| 34 | - print(f'[cp] fp32 out absmax={float(out.abs().max()):.1f} finite={bool(torch.isfinite(out).all())} dtype={out.dtype}', flush=True) | ||
| 35 | - return out | ||
| 36 | - mod.forward=fwd | ||
| 37 | -_fix(pipe.dit.condition_proj, "condition_proj") | ||
| 38 | -print("[fix ] condition_proj fp32+clamp 已装", flush=True) | ||
| 39 | - | ||
| 40 | -# 先单独查文本编码器输出——它是整条条件链的源头 | ||
| 41 | -te_out={} | ||
| 42 | -def te_hook(m,i,o): | ||
| 43 | - t = o[0] if isinstance(o,(tuple,list)) else o | ||
| 44 | - if torch.is_tensor(t) and t.is_floating_point(): | ||
| 45 | - te_out["absmax"]=float(t.abs().max()); te_out["finite"]=bool(torch.isfinite(t).all()) | ||
| 46 | - te_out["shape"]=tuple(t.shape); te_out["dtype"]=str(t.dtype) | ||
| 47 | -h_te = pipe.text_encoder.register_forward_hook(te_hook) | ||
| 48 | - | ||
| 49 | -hits=[]; seen=set() | ||
| 50 | -def mk(name): | ||
| 51 | - def hook(mod, inp, out): | ||
| 52 | - if name in seen: return | ||
| 53 | - ok_in = all(bool(torch.isfinite(t).all()) for t in inp if torch.is_tensor(t) and t.is_floating_point()) | ||
| 54 | - outs = out if isinstance(out,(tuple,list)) else (out,) | ||
| 55 | - for o in outs: | ||
| 56 | - if torch.is_tensor(o) and o.is_floating_point() and not torch.isfinite(o).all(): | ||
| 57 | - seen.add(name) | ||
| 58 | - ins=[float(t.abs().max()) for t in inp if torch.is_tensor(t) and t.is_floating_point() and t.numel()] | ||
| 59 | - fin=torch.isfinite(o) | ||
| 60 | - hits.append((name, type(mod).__name__, ok_in, | ||
| 61 | - float(o[fin].abs().max()) if fin.any() else float("nan"), | ||
| 62 | - max(ins) if ins else None, | ||
| 63 | - float((~fin).float().mean())*100)) | ||
| 64 | - return | ||
| 65 | - return hook | ||
| 66 | -handles=[] | ||
| 67 | -for tag,mod in (("dit",pipe.dit),("txt",pipe.text_encoder), | ||
| 68 | - ("vvae",pipe.video_vae),("avae",pipe.audio_vae)): | ||
| 69 | - for n,m in mod.named_modules(): | ||
| 70 | - if n: handles.append(m.register_forward_hook(mk(f"{tag}.{n}"))) | ||
| 71 | -print(f"[hook] {len(handles)} 个模块", flush=True) | ||
| 72 | - | ||
| 73 | -video, audio = pipe(prompt=PROMPT, height=480,width=832,num_frames=124, | ||
| 74 | - num_inference_steps=1, seed=0, flow_shift=6.0) | ||
| 75 | -for h in handles: h.remove() | ||
| 76 | -h_te.remove() | ||
| 77 | - | ||
| 78 | -print("\n===== 结果 =====") | ||
| 79 | -print("[文本编码器输出]", te_out) | ||
| 80 | -import numpy as np | ||
| 81 | -try: | ||
| 82 | - a=np.stack([np.asarray(f.convert("RGB"),dtype=np.float32) for f in video[::20]]) | ||
| 83 | - print(f"[video] frames={len(video)} mean={a.mean():.4f} std={a.std():.4f}") | ||
| 84 | -except Exception as e: | ||
| 85 | - print("[video]", type(video), e) | ||
| 86 | -print(f"[audio] finite={bool(torch.isfinite(audio).all())}") | ||
| 87 | -print(f"\n>>> 真源头(输入有限=True),共 {len([h for h in hits if h[2]])} 个:") | ||
| 88 | -for h in hits: | ||
| 89 | - if h[2]: print(f" ★ {h[0]} [{h[1]}] 输入absmax={h[4]} 有限部分absmax={h[3]} 非有限占比={h[5]:.4f}%") | ||
| 90 | -print(f">>> 受害者总数 {len([h for h in hits if not h[2]])},前 5:") | ||
| 91 | -for h in [x for x in hits if not x[2]][:5]: print(f" {h[0]} [{h[1]}]") |
minimax-h3/scripts/download.py deleted
| 1 | -"""Pre-download MiniMax-H3 NF4 weights via DiffSynth's own ModelConfig, | ||
| 2 | -so the on-disk layout matches exactly what the pipeline later resolves. | ||
| 3 | - | ||
| 4 | -已下过的会被 download_if_necessary() 跳过,重跑是幂等的。 | ||
| 5 | -两个分区:FL2VA 收首末帧,Ref2VA 收任意多张参考图/参考视频, | ||
| 6 | -DiT 与 processor 各一套,VAE 和文本编码器共用。""" | ||
| 7 | -import os, time, traceback | ||
| 8 | -from diffsynth.core.loader import ModelConfig | ||
| 9 | - | ||
| 10 | -JOBS = [ # small first, so a load smoke-test can start early | ||
| 11 | - ("MiniMax/MiniMax-H3", "FL2VA/processor/"), | ||
| 12 | - ("DiffSynth-Studio/MiniMax-H3-NF4", "audio_vae_nf4.safetensors"), | ||
| 13 | - ("DiffSynth-Studio/MiniMax-H3-NF4", "video_vae_nf4.safetensors"), | ||
| 14 | - ("lightx2v/Minimax-h3-Turbo", "minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors"), | ||
| 15 | - ("DiffSynth-Studio/MiniMax-H3-NF4", "minimax-h3-text-encoder-nf4.safetensors"), | ||
| 16 | - ("DiffSynth-Studio/MiniMax-H3-NF4", "minimax-h3-fl2va-nf4.safetensors"), | ||
| 17 | - # Ref2VA:参考图模式。Turbo LoRA 只有 v0.1,FL2V 那个是 v1.0 768p。 | ||
| 18 | - ("MiniMax/MiniMax-H3", "Ref2VA/processor/"), | ||
| 19 | - ("lightx2v/Minimax-h3-Turbo", "minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors"), | ||
| 20 | - ("DiffSynth-Studio/MiniMax-H3-NF4", "minimax-h3-ref2va-nf4.safetensors"), | ||
| 21 | - # 上面那个 bf16 版实测出噪点(二分见 docs/H3)。comfyui 版是另一套键名,试它能不能被认。 | ||
| 22 | - ("lightx2v/Minimax-h3-Turbo", "minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors"), | ||
| 23 | -] | ||
| 24 | - | ||
| 25 | -for i, (mid, pat) in enumerate(JOBS, 1): | ||
| 26 | - t0 = time.time() | ||
| 27 | - print(f"[{i}/{len(JOBS)}] {mid} :: {pat}", flush=True) | ||
| 28 | - for attempt in range(1, 6): | ||
| 29 | - try: | ||
| 30 | - ModelConfig(model_id=mid, origin_file_pattern=pat).download_if_necessary() | ||
| 31 | - print(f" done in {time.time()-t0:.0f}s", flush=True) | ||
| 32 | - break | ||
| 33 | - except Exception: | ||
| 34 | - print(f" attempt {attempt} failed:\n{traceback.format_exc()}", flush=True) | ||
| 35 | - time.sleep(15) | ||
| 36 | - else: | ||
| 37 | - print(f" GIVING UP on {mid}::{pat}", flush=True) | ||
| 38 | -print("ALL DOWNLOADS FINISHED", flush=True) |
minimax-h3/scripts/probe-attention-backend.py deleted
| 1 | -import os, sys, torch | ||
| 2 | -from diffsynth.core.attention import attention_forward | ||
| 3 | -A = sys.modules[attention_forward.__module__] | ||
| 4 | -print("DIFFSYNTH_ATTENTION_IMPLEMENTATION env =", os.environ.get("DIFFSYNTH_ATTENTION_IMPLEMENTATION")) | ||
| 5 | -print("selected =", A.ATTENTION_IMPLEMENTATION, | ||
| 6 | - "| xformers=", A.XFORMERS_AVAILABLE, "flash2=", A.FLASH_ATTN_2_AVAILABLE, | ||
| 7 | - "flash3=", A.FLASH_ATTN_3_AVAILABLE, "flash4=", A.FLASH_ATTN_4_AVAILABLE, "sage=", A.SAGE_ATTN_AVAILABLE) | ||
| 8 | -dev="cuda:1" | ||
| 9 | -q=torch.randn(1,56,14100,128,device=dev,dtype=torch.float16) | ||
| 10 | -o=A.attention_forward(q,q,q) | ||
| 11 | -print("[OK] attention_forward fp16 S=14100:", tuple(o.shape), "finite=", bool(torch.isfinite(o).all())) |
minimax-h3/scripts/probe-cp.py deleted
| 1 | -"""摸清 dit.condition_proj 的溢出幅度:权重/输入/fp32 参考输出的量级。""" | ||
| 2 | -import os, time, torch | ||
| 3 | -BASE="/data/models/minimax-h3" | ||
| 4 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION","torch") | ||
| 5 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", f"{BASE}/models") | ||
| 6 | -os.environ.setdefault("MODELSCOPE_CACHE", f"{BASE}/.mscache") | ||
| 7 | -from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 8 | -DT=torch.float16 | ||
| 9 | -free=torch.cuda.mem_get_info("cuda")[0]/1024**3 | ||
| 10 | -vc=dict(offload_dtype=DT,offload_device="cpu",onload_dtype=DT,onload_device="cpu", | ||
| 11 | - preparing_dtype=DT,preparing_device="cuda",computation_dtype=DT,computation_device="cuda") | ||
| 12 | -NF4="DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 13 | -pipe=MiniMaxH3Pipeline.from_pretrained(torch_dtype=DT,device="cuda", | ||
| 14 | - model_configs=[ModelConfig(model_id=NF4,origin_file_pattern=p,**vc) for p in | ||
| 15 | - ["minimax-h3-fl2va-nf4.safetensors","minimax-h3-text-encoder-nf4.safetensors", | ||
| 16 | - "video_vae_nf4.safetensors","audio_vae_nf4.safetensors"]], | ||
| 17 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3",origin_file_pattern="FL2VA/processor/"), | ||
| 18 | - vram_limit=max(6.0,free-2.0)) | ||
| 19 | -pipe.load_lora(pipe.dit, ModelConfig(model_id="lightx2v/Minimax-h3-Turbo", | ||
| 20 | - origin_file_pattern="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors")) | ||
| 21 | - | ||
| 22 | -cp = pipe.dit.condition_proj | ||
| 23 | -print("[cp] type:", type(cp).__name__) | ||
| 24 | -for n,p_ in cp.named_parameters(recurse=True): | ||
| 25 | - print(f" param {n}: shape={tuple(p_.shape)} dtype={p_.dtype} absmax={float(p_.abs().max()):.4g}") | ||
| 26 | -for n,b in cp.named_buffers(recurse=True): | ||
| 27 | - print(f" buf {n}: {tuple(b.shape)} {b.dtype}") | ||
| 28 | - | ||
| 29 | -cap={} | ||
| 30 | -def hook(m,i,o): | ||
| 31 | - x=i[0] | ||
| 32 | - cap["x"]=x.detach() | ||
| 33 | - cap["o"]=o.detach() if torch.is_tensor(o) else None | ||
| 34 | -h=cp.register_forward_hook(hook) | ||
| 35 | -try: | ||
| 36 | - pipe(prompt="测试", height=480,width=832,num_frames=124, | ||
| 37 | - num_inference_steps=1, seed=42, flow_shift=6.0) | ||
| 38 | -except Exception as e: | ||
| 39 | - print("[gen] 提前中断(预期):", type(e).__name__, str(e)[:120]) | ||
| 40 | -h.remove() | ||
| 41 | - | ||
| 42 | -x=cap.get("x") | ||
| 43 | -if x is None: | ||
| 44 | - print("!! 没抓到输入"); raise SystemExit(1) | ||
| 45 | -print(f"\n[输入] shape={tuple(x.shape)} dtype={x.dtype} absmax={float(x.abs().max()):.6g} " | ||
| 46 | - f"mean={float(x.float().mean()):.4g} std={float(x.float().std()):.4g} finite={bool(torch.isfinite(x).all())}") | ||
| 47 | -o=cap.get("o") | ||
| 48 | -if o is not None: | ||
| 49 | - fin=torch.isfinite(o) | ||
| 50 | - print(f"[fp16 输出] absmax={float(o[fin].abs().max()) if fin.any() else float('nan'):.6g} " | ||
| 51 | - f"非有限占比={100*(~fin).float().mean():.2f}%") | ||
| 52 | - | ||
| 53 | -# fp32 参考 | ||
| 54 | -W=None;B=None | ||
| 55 | -for n,p_ in cp.named_parameters(recurse=True): | ||
| 56 | - if n.endswith("weight"): W=p_ | ||
| 57 | - if n.endswith("bias"): B=p_ | ||
| 58 | -if W is not None: | ||
| 59 | - ref=torch.nn.functional.linear(x.float(), W.float(), B.float() if B is not None else None) | ||
| 60 | - print(f"[fp32 参考] absmax={float(ref.abs().max()):.6g} (fp16 上限 65504)") | ||
| 61 | - print(f" → 超出 fp16 上限的元素占比 {100*(ref.abs()>65504).float().mean():.4f}%") |
minimax-h3/scripts/probe-h3-shapes.py deleted
| 1 | -import torch, time | ||
| 2 | -dev="cuda:1" | ||
| 3 | -torch.cuda.set_device(dev) | ||
| 4 | -from torch.nn.attention import SDPBackend, sdpa_kernel | ||
| 5 | -S, H, D = 14100, 56, 128 # real H3 shape @ 480x832x124f | ||
| 6 | -for dt in (torch.float16,): | ||
| 7 | - q=k=v=torch.randn(1,H,S,D,device=dev,dtype=dt) | ||
| 8 | - for be,nm in ((SDPBackend.EFFICIENT_ATTENTION,"EFFICIENT"),): | ||
| 9 | - try: | ||
| 10 | - with sdpa_kernel(be): | ||
| 11 | - torch.cuda.synchronize(); t0=time.time() | ||
| 12 | - o=torch.nn.functional.scaled_dot_product_attention(q,k,v) | ||
| 13 | - torch.cuda.synchronize() | ||
| 14 | - print(f"[OK ] SDPA {nm} fp16 S={S} H={H} D={D}: {o.shape} {time.time()-t0:.2f}s peak={torch.cuda.max_memory_allocated(dev)/2**30:.2f}G") | ||
| 15 | - except Exception as e: | ||
| 16 | - print(f"[ERR] SDPA {nm} fp16 D={D}: {type(e).__name__}: {str(e)[:200]}") | ||
| 17 | - del q,k,v | ||
| 18 | - torch.cuda.empty_cache(); torch.cuda.reset_peak_memory_stats(dev) | ||
| 19 | - | ||
| 20 | -# fp16 GEMM throughput on this card (H3 block-ish shape) | ||
| 21 | -a=torch.randn(14100,5376,device=dev,dtype=torch.float16); b=torch.randn(5376,28672,device=dev,dtype=torch.float16) | ||
| 22 | -for _ in range(2): c=a@b | ||
| 23 | -torch.cuda.synchronize(); t0=time.time() | ||
| 24 | -for _ in range(5): c=a@b | ||
| 25 | -torch.cuda.synchronize() | ||
| 26 | -fl=2*14100*5376*28672*5 | ||
| 27 | -print(f"fp16 GEMM effective: {fl/(time.time()-t0)/1e12:.1f} TFLOPS") | ||
| 28 | -# fp16 overflow sanity on a deep-ish accumulation | ||
| 29 | -x=torch.randn(4096,4096,device=dev,dtype=torch.float16)*4 | ||
| 30 | -y=x | ||
| 31 | -for i in range(20): y=torch.nn.functional.layer_norm(y@x/64,(4096,)) | ||
| 32 | -print("fp16 deep-chain finite:", bool(torch.isfinite(y).all())) |
minimax-h3/scripts/probe-nf4-fp16.py deleted
| 1 | -import os, torch | ||
| 2 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION","torch") | ||
| 3 | -import bitsandbytes as bnb | ||
| 4 | -print("bnb", bnb.__version__) | ||
| 5 | -dev="cuda:1" | ||
| 6 | -# --- NF4 quantize + forward in FP16 on sm_75 --- | ||
| 7 | -lin = torch.nn.Linear(5376, 14336, bias=False, dtype=torch.float16) | ||
| 8 | -q = bnb.nn.Linear4bit(5376, 14336, bias=False, compute_dtype=torch.float16, quant_type="nf4", compress_statistics=True) | ||
| 9 | -q.weight = bnb.nn.Params4bit(lin.weight.data, requires_grad=False, quant_type="nf4", compress_statistics=True) | ||
| 10 | -q = q.to(dev) | ||
| 11 | -x = torch.randn(1024, 5376, device=dev, dtype=torch.float16) | ||
| 12 | -y = q(x) | ||
| 13 | -ref = torch.nn.functional.linear(x, lin.weight.to(dev)) | ||
| 14 | -err = (y.float()-ref.float()).abs().mean().item()/ref.float().abs().mean().item() | ||
| 15 | -print(f"[OK ] NF4 fp16 forward: out={tuple(y.shape)} dtype={y.dtype} finite={bool(torch.isfinite(y).all())} rel_err={err:.4f}") | ||
| 16 | -# --- prequantized round-trip (what DiffSynth actually does when loading .safetensors) --- | ||
| 17 | -qs = q.weight.quant_state.as_dict(packed=True) | ||
| 18 | -w2 = bnb.nn.Params4bit.from_prequantized(data=q.weight.data, quantized_stats={k:(v.to(dev) if torch.is_tensor(v) else v) for k,v in qs.items()}, requires_grad=False, device=dev) | ||
| 19 | -q2 = bnb.nn.Linear4bit(5376,14336,bias=False,compute_dtype=torch.float16,quant_type="nf4") | ||
| 20 | -q2.weight = w2; q2 = q2.to(dev) | ||
| 21 | -y2 = q2(x) | ||
| 22 | -print(f"[OK ] NF4 prequantized reload: match={torch.allclose(y,y2)} dtype={y2.dtype}") | ||
| 23 | -# --- DiffSynth attention backend selection --- | ||
| 24 | -import sys | ||
| 25 | -from diffsynth.core.attention import attention_forward | ||
| 26 | -A = sys.modules[attention_forward.__module__] | ||
| 27 | -print("ATTENTION_IMPLEMENTATION =", A.ATTENTION_IMPLEMENTATION, | ||
| 28 | - "| xformers_avail =", A.XFORMERS_AVAILABLE, "| flash2 =", A.FLASH_ATTN_2_AVAILABLE, "| sage =", A.SAGE_ATTN_AVAILABLE) | ||
| 29 | -o = A.attention_forward(*[torch.randn(1,56,4096,128,device=dev,dtype=torch.float16)]*3) | ||
| 30 | -print("[OK ] diffsynth attention_forward fp16:", tuple(o.shape), "finite=", bool(torch.isfinite(o).all())) |
minimax-h3/scripts/probe-turing-caps.py deleted
| 1 | -import torch, subprocess, sys, os | ||
| 2 | -print("torch", torch.__version__, "cuda", torch.version.cuda) | ||
| 3 | -print("arch_list", torch.cuda.get_arch_list()) | ||
| 4 | -print("n_gpu", torch.cuda.device_count()) | ||
| 5 | -for i in range(torch.cuda.device_count()): | ||
| 6 | - free, total = torch.cuda.mem_get_info(i) | ||
| 7 | - p = torch.cuda.get_device_properties(i) | ||
| 8 | - print(f" gpu{i} {p.name} sm_{p.major}{p.minor} free={free/2**30:.1f}G total={total/2**30:.1f}G") | ||
| 9 | - | ||
| 10 | -dev = "cuda:0" | ||
| 11 | -def t(name, fn): | ||
| 12 | - try: | ||
| 13 | - r = fn(); print(f"[OK ] {name}: {r}") | ||
| 14 | - except Exception as e: | ||
| 15 | - print(f"[ERR] {name}: {type(e).__name__}: {str(e)[:180]}") | ||
| 16 | - | ||
| 17 | -t("fp16 matmul", lambda: (torch.randn(512,512,device=dev,dtype=torch.float16) @ torch.randn(512,512,device=dev,dtype=torch.float16)).float().abs().mean().item()) | ||
| 18 | -t("bf16 matmul", lambda: (torch.randn(512,512,device=dev,dtype=torch.bfloat16) @ torch.randn(512,512,device=dev,dtype=torch.bfloat16)).float().abs().mean().item()) | ||
| 19 | -t("bf16 layernorm", lambda: torch.nn.functional.layer_norm(torch.randn(8,512,device=dev,dtype=torch.bfloat16),(512,)).float().abs().mean().item()) | ||
| 20 | - | ||
| 21 | -from torch.nn.attention import SDPBackend, sdpa_kernel | ||
| 22 | -q=lambda dt: torch.randn(1,8,1024,64,device=dev,dtype=dt) | ||
| 23 | -for dt in (torch.float16, torch.bfloat16): | ||
| 24 | - for be in (SDPBackend.FLASH_ATTENTION, SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH): | ||
| 25 | - def f(dt=dt,be=be): | ||
| 26 | - with sdpa_kernel(be): | ||
| 27 | - return tuple(torch.nn.functional.scaled_dot_product_attention(q(dt),q(dt),q(dt)).shape) | ||
| 28 | - t(f"SDPA {str(dt).split('.')[-1]:9s} {be.name}", f) | ||
| 29 | -def auto(): | ||
| 30 | - return tuple(torch.nn.functional.scaled_dot_product_attention(q(torch.float16),q(torch.float16),q(torch.float16)).shape) | ||
| 31 | -t("SDPA fp16 AUTO", auto) | ||
| 32 | - | ||
| 33 | -print("--- installed pkgs of interest ---") | ||
| 34 | -import importlib.metadata as md | ||
| 35 | -for name in ["transformers","accelerate","diffusers","bitsandbytes","torchao","safetensors","einops","modelscope","av","torchaudio","torchcodec","librosa","peft","xformers","flash_attn","sageattention","imageio","imageio-ffmpeg","numpy"]: | ||
| 36 | - try: print(f" {name}=={md.version(name)}") | ||
| 37 | - except Exception: print(f" {name}: NOT INSTALLED") | ||
| 38 | -print("--- ffmpeg ---") | ||
| 39 | -print(subprocess.run(["bash","-lc","ffmpeg -version 2>&1|head -1; which ffmpeg"],capture_output=True,text=True).stdout) |
minimax-h3/scripts/probe2.py deleted
| 1 | -"""量清楚 condition_proj 的 fp32 真值幅度:决定是局部修还是整条路径抬 fp32。""" | ||
| 2 | -import os, torch, torch.nn.functional as F | ||
| 3 | -BASE="/data/models/minimax-h3" | ||
| 4 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION","torch") | ||
| 5 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", f"{BASE}/models") | ||
| 6 | -os.environ.setdefault("MODELSCOPE_CACHE", f"{BASE}/.mscache") | ||
| 7 | -from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 8 | -DT=torch.float16 | ||
| 9 | -free=torch.cuda.mem_get_info("cuda")[0]/1024**3 | ||
| 10 | -vc=dict(offload_dtype=DT,offload_device="cpu",onload_dtype=DT,onload_device="cpu", | ||
| 11 | - preparing_dtype=DT,preparing_device="cuda",computation_dtype=DT,computation_device="cuda") | ||
| 12 | -NF4="DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 13 | -pipe=MiniMaxH3Pipeline.from_pretrained(torch_dtype=DT,device="cuda", | ||
| 14 | - model_configs=[ModelConfig(model_id=NF4,origin_file_pattern=p,**vc) for p in | ||
| 15 | - ["minimax-h3-fl2va-nf4.safetensors","minimax-h3-text-encoder-nf4.safetensors", | ||
| 16 | - "video_vae_nf4.safetensors","audio_vae_nf4.safetensors"]], | ||
| 17 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3",origin_file_pattern="FL2VA/processor/"), | ||
| 18 | - vram_limit=max(6.0,free-2.0)) | ||
| 19 | -pipe.load_lora(pipe.dit, ModelConfig(model_id="lightx2v/Minimax-h3-Turbo", | ||
| 20 | - origin_file_pattern="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors")) | ||
| 21 | - | ||
| 22 | -cp = pipe.dit.condition_proj | ||
| 23 | -class Stop(Exception): pass | ||
| 24 | -res={} | ||
| 25 | -orig_lf = cp.linear_forward | ||
| 26 | -orig_lora = cp.lora_forward | ||
| 27 | -def lf(x, weight, bias): | ||
| 28 | - o32 = F.linear(x.float(), weight.float(), bias.float() if bias is not None else None) | ||
| 29 | - res["x_absmax"]=float(x.abs().max()); res["x_dtype"]=str(x.dtype) | ||
| 30 | - res["lin32_absmax"]=float(o32.abs().max()) | ||
| 31 | - res["lin32_over_fp16"]=float((o32.abs()>65504).float().mean())*100 | ||
| 32 | - res["lin32"]=o32 | ||
| 33 | - return o32 | ||
| 34 | -def lora(x, out): | ||
| 35 | - o = out | ||
| 36 | - for A,B in zip(cp.lora_A_weights, cp.lora_B_weights): | ||
| 37 | - o = o + x.float() @ A.T.to(device=x.device,dtype=torch.float32) @ B.T.to(device=x.device,dtype=torch.float32) | ||
| 38 | - res["n_lora"]=len(cp.lora_A_weights) | ||
| 39 | - res["final32_absmax"]=float(o.abs().max()) | ||
| 40 | - res["final32_over_fp16"]=float((o.abs()>65504).float().mean())*100 | ||
| 41 | - res["p999"]=float(torch.quantile(o.abs().flatten().float(), 0.999)) | ||
| 42 | - raise Stop() | ||
| 43 | -cp.linear_forward=lf; cp.lora_forward=lora | ||
| 44 | -try: | ||
| 45 | - pipe(prompt="一只橘猫在窗台上打哈欠", height=480,width=832,num_frames=124, | ||
| 46 | - num_inference_steps=1, seed=42, flow_shift=6.0) | ||
| 47 | -except Stop: | ||
| 48 | - pass | ||
| 49 | -print("\n===== condition_proj fp32 真值 =====") | ||
| 50 | -for k in ["x_dtype","x_absmax","lin32_absmax","lin32_over_fp16","n_lora","final32_absmax","final32_over_fp16","p999"]: | ||
| 51 | - if k in res: print(f" {k:20s} = {res[k]}") | ||
| 52 | -print(f" fp16 上限 = 65504") |
minimax-h3/scripts/run.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# MiniMax-H3 FL2VA NF4 launcher. usage: ./run.sh <GPU_ID> [args for t2va.py ...] | ||
| 3 | -# ./run.sh 1 # turbo 4-step, 480x832x124f | ||
| 4 | -# ./run.sh 1 --no-turbo --steps 50 # full quality | ||
| 5 | -# ./run.sh 2 --prompt "..." --seed 7 | ||
| 6 | -set -euo pipefail | ||
| 7 | -B=/data/models/minimax-h3 | ||
| 8 | -GPU="${1:-1}"; shift || true | ||
| 9 | - | ||
| 10 | -export CUDA_VISIBLE_DEVICES="$GPU" | ||
| 11 | -export DIFFSYNTH_ATTENTION_IMPLEMENTATION=torch # sm_75: no flash/sage; pin instead of auto-picking xformers | ||
| 12 | -export DIFFSYNTH_MODEL_BASE_PATH="$B/models" | ||
| 13 | -export MODELSCOPE_CACHE="$B/.mscache" | ||
| 14 | -export PYTHONUNBUFFERED=1 | ||
| 15 | -# NOTE: deliberately NOT setting PYTHONNOUSERSITE -- the venv's site-packages already | ||
| 16 | -# precedes ~/.local in sys.path, so our bitsandbytes 0.50.1 shadows the broken 0.45.1 there. | ||
| 17 | - | ||
| 18 | -# GPU0/1 sit on NUMA0, GPU2/3 on NUMA1 -- keep the CPU-offloaded weights on the near node. | ||
| 19 | -case "$GPU" in 0|1) NODE=0 ;; 2|3) NODE=1 ;; *) NODE="" ;; esac | ||
| 20 | -BIND=(); if [ -n "$NODE" ] && command -v numactl >/dev/null 2>&1; then BIND=(numactl --cpunodebind="$NODE" --membind="$NODE"); fi | ||
| 21 | - | ||
| 22 | -LOG="$B/logs/gpu${GPU}_$(date +%Y%m%d-%H%M%S).log" | ||
| 23 | -echo "[run] GPU=$GPU numa=${NODE:-none} log=$LOG" | ||
| 24 | -"${BIND[@]}" "$B/venv/bin/python" "$B/scripts/t2va.py" "$@" 2>&1 | tee "$LOG" |
minimax-h3/scripts/t2va.py deleted
| 1 | -#!/usr/bin/env python | ||
| 2 | -"""MiniMax-H3 FL2VA (NF4) Text -> Video+Audio on RTX 2080 Ti (Turing / sm_75). | ||
| 3 | - | ||
| 4 | -Turing deltas vs the official example (all deliberate): | ||
| 5 | - * FP16 everywhere, never BF16 -- sm_75 has no bf16 SDPA kernel at all | ||
| 6 | - (EFFICIENT and FLASH both report "No available kernel"), so bf16 silently | ||
| 7 | - falls back to the MATH backend and OOMs on a ~14k-token sequence. | ||
| 8 | - * DIFFSYNTH_ATTENTION_IMPLEMENTATION=torch -- FlashAttention 2/3/4 and | ||
| 9 | - SageAttention are sm_80+. xformers works here but would be auto-selected | ||
| 10 | - ahead of torch; pin the backend so runs are reproducible. | ||
| 11 | - * vram_limit from mem_get_info()[0] (FREE) not [1] (TOTAL) -- other processes | ||
| 12 | - share these GPUs. | ||
| 13 | - * offload to "cpu", never "disk" -- /data is a 7200rpm HDD (184 MB/s). | ||
| 14 | -""" | ||
| 15 | -import os, sys, time, argparse | ||
| 16 | - | ||
| 17 | -BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
| 18 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION", "torch") | ||
| 19 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", os.path.join(BASE, "models")) | ||
| 20 | - | ||
| 21 | -p = argparse.ArgumentParser() | ||
| 22 | -p.add_argument("--prompt", default="A girl is very happy, she is speaking in english: " | ||
| 23 | - "“I enjoy working with Diffsynth-Studio, it's a perfect framework.”") | ||
| 24 | -p.add_argument("--height", type=int, default=480) | ||
| 25 | -p.add_argument("--width", type=int, default=832) | ||
| 26 | -p.add_argument("--frames", type=int, default=124) | ||
| 27 | -p.add_argument("--steps", type=int, default=4) | ||
| 28 | -p.add_argument("--flow-shift", type=float, default=None, help="default 6.0 with turbo, 12.0 without") | ||
| 29 | -p.add_argument("--seed", type=int, default=0) | ||
| 30 | -p.add_argument("--turbo", action="store_true", default=True) | ||
| 31 | -p.add_argument("--no-turbo", dest="turbo", action="store_false") | ||
| 32 | -p.add_argument("--dtype", choices=["fp32","fp16"], default="fp32", | ||
| 33 | - help="fp32=唯一能在 sm_75 上出片的选择(默认);fp16 只用于复现全黑故障") | ||
| 34 | -p.add_argument("--no-fp32-residual", dest="fp32_residual", action="store_false", | ||
| 35 | - help="关闭 DiT 残差流的 fp32 兜底(默认开启;关掉在 sm_75 上会出全黑视频)") | ||
| 36 | -p.add_argument("--no-fp32-cond", dest="fp32_cond", action="store_false", | ||
| 37 | - help="关闭 condition_proj 的 fp32 兜底(默认开启;关掉在 sm_75 上会出全黑视频)") | ||
| 38 | -p.add_argument("--fp32-accum", action="store_true", | ||
| 39 | - help="disable fp16 reduced-precision reduction (~2x slower, use if output is NaN/artifacted)") | ||
| 40 | -p.add_argument("--vram-reserve", type=float, default=None, | ||
| 41 | - help="留给激活的显存(GB)。默认按 dtype 自适应:fp32=14(实测全分辨率峰值 12.2G),fp16=5") | ||
| 42 | -p.add_argument("--out", default=None) | ||
| 43 | -args = p.parse_args() | ||
| 44 | - | ||
| 45 | -import torch | ||
| 46 | -if args.fp32_accum: | ||
| 47 | - torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction = False | ||
| 48 | - | ||
| 49 | -from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 50 | -from diffsynth.utils.data.audio_video import write_video_audio | ||
| 51 | - | ||
| 52 | -# dtype 决定这条线能不能出片。实测(2026-08-18, sm_75, 448x256x28f, 唯一变量是 dtype): | ||
| 53 | -# fp32 -> 视频 std=50.6、音频 finite ✅ fp16 -> 124 帧纯黑、音频 NaN ❌ | ||
| 54 | -# 原因是 fp16 的动态范围装不下这个 bf16 训练的模型:dit.blocks.0 输入 absmax 89 | ||
| 55 | -# 已放大到 38176,50 层残差累积后必然突破 fp16 上限 65504。 | ||
| 56 | -# bf16 在 sm_75 上不可用(无硬件支持),所以 fp32 是这张卡上唯一能出片的选择。 | ||
| 57 | -# 保留 --dtype fp16 只为复现/对照,不要用它出片。 | ||
| 58 | -DTYPE = {"fp32": torch.float32, "fp16": torch.float16}[args.dtype] | ||
| 59 | -free_gb = torch.cuda.mem_get_info("cuda")[0] / 1024**3 # [0]=free, official example wrongly uses [1]=total | ||
| 60 | -if args.vram_reserve is None: | ||
| 61 | - # fp32 的激活是 fp16 的两倍,权重要多让位。实测 832x480x124f fp32: | ||
| 62 | - # vram_limit 7.3G 时峰值 12.2G,跑通;vram_limit 19.3G 时 OOM。 | ||
| 63 | - args.vram_reserve = 14.0 if DTYPE is torch.float32 else 5.0 | ||
| 64 | -vram_limit = max(4.0, free_gb - args.vram_reserve) | ||
| 65 | -if vram_limit + 1.0 > free_gb: | ||
| 66 | - print(f"[warn] 空闲显存仅 {free_gb:.1f}G,vram_limit={vram_limit:.1f}G 几乎没有余量," | ||
| 67 | - f"很可能 OOM。先停掉占显存的服务(见 qwen38-deploy/RESTORE-GPU.md)", flush=True) | ||
| 68 | - | ||
| 69 | -prop = torch.cuda.get_device_properties(0) | ||
| 70 | -print(f"[env] GPU={prop.name} sm_{prop.major}{prop.minor} free={free_gb:.1f}G vram_limit={vram_limit:.1f}G", flush=True) | ||
| 71 | -print(f"[env] dtype={DTYPE} attention={os.environ['DIFFSYNTH_ATTENTION_IMPLEMENTATION']} " | ||
| 72 | - f"fp16_reduced_accum={torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction}", flush=True) | ||
| 73 | -assert prop.major == 7, "this script is tuned for Turing; re-check dtype choices on other archs" | ||
| 74 | - | ||
| 75 | -vram_config = dict( | ||
| 76 | - offload_dtype=DTYPE, offload_device="cpu", # not "disk": /data is a spinning HDD | ||
| 77 | - onload_dtype=DTYPE, onload_device="cpu", | ||
| 78 | - preparing_dtype=DTYPE, preparing_device="cuda", | ||
| 79 | - computation_dtype=DTYPE, computation_device="cuda", | ||
| 80 | -) | ||
| 81 | -NF4 = "DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 82 | - | ||
| 83 | -t0 = time.time() | ||
| 84 | -pipe = MiniMaxH3Pipeline.from_pretrained( | ||
| 85 | - torch_dtype=DTYPE, device="cuda", | ||
| 86 | - model_configs=[ | ||
| 87 | - ModelConfig(model_id=NF4, origin_file_pattern="minimax-h3-fl2va-nf4.safetensors", **vram_config), | ||
| 88 | - ModelConfig(model_id=NF4, origin_file_pattern="minimax-h3-text-encoder-nf4.safetensors", **vram_config), | ||
| 89 | - ModelConfig(model_id=NF4, origin_file_pattern="video_vae_nf4.safetensors", **vram_config), | ||
| 90 | - ModelConfig(model_id=NF4, origin_file_pattern="audio_vae_nf4.safetensors", **vram_config), | ||
| 91 | - ], | ||
| 92 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3", origin_file_pattern="FL2VA/processor/"), | ||
| 93 | - vram_limit=vram_limit, | ||
| 94 | -) | ||
| 95 | -print(f"[load] pipeline ready in {time.time()-t0:.0f}s", flush=True) | ||
| 96 | - | ||
| 97 | -if args.turbo: | ||
| 98 | - # LoRA rides on the NF4 layers: AutoWrappedQuantizedModule inherits LoRAHotLoadMixin, | ||
| 99 | - # so lora_forward() adds x@A@B to the quantized output -- no base dequantisation needed. | ||
| 100 | - pipe.load_lora(pipe.dit, ModelConfig( | ||
| 101 | - model_id="lightx2v/Minimax-h3-Turbo", | ||
| 102 | - origin_file_pattern="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors")) | ||
| 103 | -# --- sm_75 必需:condition_proj 在 fp16 下累加溢出 -> inf -> 全管线 NaN -> 全黑视频 --- | ||
| 104 | -# 实测(2026-08-18):该层输入 absmax≈17808、权重 absmax≈4.66、K=5120, | ||
| 105 | -# fp16 累加器(上限 65504)有 0.04% 的输出溢出成 inf,随后污染下游 999 个模块。 | ||
| 106 | -# 它只是 (1,5120)x(5376,5120) 的一次投影,改走 fp32 的算力开销可忽略。 | ||
| 107 | -# 注意:--fp32-accum 对此无效——溢出发生在累加器量级,不是 reduced-precision reduction。 | ||
| 108 | -def _force_fp32_linear(mod, tag): | ||
| 109 | - """该层在 fp32 里算,且**输出保持 fp32**流入 token_refiner。 | ||
| 110 | - | ||
| 111 | - 实测(2026-08-18, sm_75): | ||
| 112 | - 文本编码器输出 absmax 15328(有限、健康,源头不在它); | ||
| 113 | - condition_proj 的 fp32 真值 absmax 可达 79554,而 fp16 上限 65504。 | ||
| 114 | - 产生的 inf 会污染下游 2985 个模块,最终 124 帧纯黑(且原脚本退出码为 0)。 | ||
| 115 | - 根因是 Turing 无 bf16:模型按 bf16 训练(bf16 有 fp32 的动态范围), | ||
| 116 | - fp16 装不下这条条件路径。 | ||
| 117 | - | ||
| 118 | - 走过的四条弯路,都记下来免得后人重走: | ||
| 119 | - 1) 替换整个 forward —— 绕过 preparing/onload 换入机制。 | ||
| 120 | - 2) 只换 linear_forward、最后仍 `.to(fp16)` —— 无效:fp32 算得对, | ||
| 121 | - 溢出发生在那次类型转换上。自检若只查转换前的张量会永远显示"正常"。 | ||
| 122 | - 3) 钳位到 65504 再降回 fp16 —— **反而更糟**:token_refiner.blocks.0 是 | ||
| 123 | - `h = x + attn(norm1(x))`,x 恰好等于 fp16 最大可表示值时, | ||
| 124 | - 再加任何正数立刻变 inf(实测 3.45% 非有限)。钳位不留余量等于自造故障。 | ||
| 125 | - 4) 输出保持 fp32 但 vram_limit=19.3G —— 数值对了但 CUDA OOM。 | ||
| 126 | - 配 `--vram-reserve 5`(vram_limit 16.3G) + PYTORCH_ALLOC_CONF=expandable_segments:True | ||
| 127 | - 后峰值 17.2G,有余量。 | ||
| 128 | - | ||
| 129 | - 库里 _embed() 在 token_refiner 之后本来就有 `text_embed.to(dtype)`, | ||
| 130 | - 那时已归一化、量级回落,那次转换是安全的。 | ||
| 131 | - 下游 RMSNorm 会因 dtype 不一致发性能警告(非错误),可忽略。 | ||
| 132 | - """ | ||
| 133 | - mod.computation_dtype = torch.float32 | ||
| 134 | - orig = mod.forward | ||
| 135 | - def fwd(x, *a, **kw): | ||
| 136 | - out = orig(x.float(), *a, **kw) | ||
| 137 | - if not torch.isfinite(out).all(): | ||
| 138 | - print(f"[warn] {tag} 即使 fp32 仍非有限", flush=True) | ||
| 139 | - return out # 保持 fp32:既不降回也不钳位 | ||
| 140 | - mod.forward = fwd | ||
| 141 | - return mod | ||
| 142 | - | ||
| 143 | -# --- sm_75 必需 #2:DiT 残差流保持 fp32 --- | ||
| 144 | -# 实测(2026-08-18):dit.blocks.0 输入 absmax 89.25 → 输出 38176,且有极少量元素溢出。 | ||
| 145 | -# 50 个 block × 每个 2 次残差相加 = 100 个溢出点,累积后必然爆 fp16 的 65504。 | ||
| 146 | -# | ||
| 147 | -# 病灶与 condition_proj 同构,见 minimax_h3_dit.py:61-67: | ||
| 148 | -# def _modulate_gate(x, gate, other, indices): | ||
| 149 | -# return (x + gate.index_select(0, indices) * other).to(x.dtype) | ||
| 150 | -# ^^^^^^^^^^ 提升到 fp32 算完又降回 fp16 | ||
| 151 | -# 库作者自己的注释也写了 "index_select on the AdaLN params can promote the expression", | ||
| 152 | -# 但把结果降回 fp16 恰恰丢掉了提升带来的动态范围。 | ||
| 153 | -# | ||
| 154 | -# 做法是标准混合精度:**累加器(残差流)fp32,喂给子模块的张量仍 fp16**。 | ||
| 155 | -# _modulate_gate -> 返回 fp32(它的输出就是残差流,下游是 RMSNorm,容忍混合 dtype) | ||
| 156 | -# _modulate_scale_shift -> 显式返回 fp16(它的输出喂给 attn/mlp 的 Linear4bit,必须是 fp16) | ||
| 157 | -# 额外显存约 300 MB(残差流 (seq,5376) 一份 fp32),实测峰值仍在 16 GB 以内。 | ||
| 158 | -def _patch_dit_residual_fp32(): | ||
| 159 | - from diffsynth.models import minimax_h3_dit as _m | ||
| 160 | - | ||
| 161 | - def _mss(x, shift, scale, indices): | ||
| 162 | - return (x.float() * (1.0 + scale.index_select(0, indices).float()) | ||
| 163 | - + shift.index_select(0, indices).float()).to(torch.float16) | ||
| 164 | - | ||
| 165 | - def _mg(x, gate, other, indices): | ||
| 166 | - # 不降回 fp16:这一份就是残差流 | ||
| 167 | - return x.float() + gate.index_select(0, indices).float() * other.float() | ||
| 168 | - | ||
| 169 | - _m._modulate_scale_shift = _mss | ||
| 170 | - _m._modulate_gate = _mg | ||
| 171 | - print("[fix ] DiT 残差流已切到 fp32(_modulate_gate 不再降回 fp16)", flush=True) | ||
| 172 | - | ||
| 173 | -# 这两个补丁是 fp16 专用的挽救措施(最终证明救不回来,见 README)。 | ||
| 174 | -# fp32 下必须失效,否则强制 .to(float16) 会与 fp32 权重冲突: | ||
| 175 | -# RuntimeError: mat1 and mat2 must have the same dtype, but got Half and Float | ||
| 176 | -if args.fp32_residual and DTYPE is torch.float16: | ||
| 177 | - _patch_dit_residual_fp32() | ||
| 178 | - | ||
| 179 | -if args.fp32_cond and DTYPE is torch.float16: | ||
| 180 | - _force_fp32_linear(pipe.dit.condition_proj, "dit.condition_proj") | ||
| 181 | - print("[fix ] dit.condition_proj 已切到 fp32 计算(规避 sm_75 fp16 累加溢出)", flush=True) | ||
| 182 | - | ||
| 183 | -flow_shift = args.flow_shift if args.flow_shift is not None else (6.0 if args.turbo else 12.0) | ||
| 184 | - | ||
| 185 | -print(f"[gen ] {args.width}x{args.height} {args.frames}f steps={args.steps} " | ||
| 186 | - f"turbo={args.turbo} flow_shift={flow_shift} seed={args.seed}", flush=True) | ||
| 187 | -t1 = time.time() | ||
| 188 | -video, audio = pipe( | ||
| 189 | - prompt=args.prompt, | ||
| 190 | - height=args.height, width=args.width, num_frames=args.frames, | ||
| 191 | - num_inference_steps=args.steps, seed=args.seed, flow_shift=flow_shift, | ||
| 192 | -) | ||
| 193 | -gen_s = time.time() - t1 | ||
| 194 | - | ||
| 195 | -import torch as _t | ||
| 196 | -_bad = [] | ||
| 197 | -if not _t.isfinite(audio if _t.is_tensor(audio) else _t.tensor(0.)).all(): | ||
| 198 | - _bad.append("audio 含非有限值") | ||
| 199 | -# video 是 PIL 帧列表;原脚本从不检查它,导致全黑视频也返回 0 —— 实测踩过 | ||
| 200 | -def _video_stats(v): | ||
| 201 | - import numpy as _np | ||
| 202 | - if _t.is_tensor(v): | ||
| 203 | - a = v.float() | ||
| 204 | - return float(a.mean()), float(a.std()), bool(_t.isfinite(a).all()) | ||
| 205 | - try: | ||
| 206 | - arrs = [_np.asarray(f.convert("RGB"), dtype=_np.float32) for f in v[:: max(1, len(v)//8)]] | ||
| 207 | - a = _np.stack(arrs) | ||
| 208 | - return float(a.mean()), float(a.std()), True | ||
| 209 | - except Exception: | ||
| 210 | - return None, None, True | ||
| 211 | -_mean, _std, _fin = _video_stats(video) | ||
| 212 | -if not _fin: | ||
| 213 | - _bad.append("video 含非有限值") | ||
| 214 | -if _std is not None and _std < 1.0: | ||
| 215 | - _bad.append(f"video 近乎纯色 (std={_std:.3f}, mean={_mean:.3f})") | ||
| 216 | -if _bad: | ||
| 217 | - print("[FAIL] " + ";".join(_bad), flush=True) | ||
| 218 | - | ||
| 219 | -out = args.out or os.path.join(BASE, "outputs", f"t2va_{time.strftime('%Y%m%d-%H%M%S')}.mp4") | ||
| 220 | -os.makedirs(os.path.dirname(out), exist_ok=True) | ||
| 221 | -write_video_audio(video=video, audio=audio, output_path=out, fps=24, audio_sample_rate=32000) | ||
| 222 | - | ||
| 223 | -print(f"[done] generation {gen_s:.0f}s ({gen_s/max(args.steps,1):.1f}s/step), " | ||
| 224 | - f"peak VRAM {torch.cuda.max_memory_allocated()/1024**3:.1f}G", flush=True) | ||
| 225 | -print(f"[out ] {out} ({os.path.getsize(out)/1024**2:.1f} MB)", flush=True) | ||
| 226 | -if _bad: | ||
| 227 | - sys.exit(3) # 不要用退出码 0 报告一个坏产物 |
minimax-h3/scripts/test-fp32.py deleted
| 1 | -"""关键对照实验:小分辨率下 fp32 端到端能否出片。 | ||
| 2 | -若 fp32 出片而 fp16 全黑 → 确认是动态范围问题,而非模型/权重/流程问题。 | ||
| 3 | -小分辨率是为了让 fp32 装得下(seq_len 从 ~14100 降到 ~800)。""" | ||
| 4 | -import os, sys, time, torch, numpy as np | ||
| 5 | -BASE="/data/models/minimax-h3" | ||
| 6 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION","torch") | ||
| 7 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", f"{BASE}/models") | ||
| 8 | -os.environ.setdefault("MODELSCOPE_CACHE", f"{BASE}/.mscache") | ||
| 9 | -os.environ.setdefault("PYTORCH_ALLOC_CONF","expandable_segments:True") | ||
| 10 | -from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 11 | - | ||
| 12 | -DT = torch.float32 if os.environ.get("TEST_DTYPE","fp32")=="fp32" else torch.float16 | ||
| 13 | -H = int(os.environ.get("TEST_H", 256)); W = int(os.environ.get("TEST_W", 448)); F_ = int(os.environ.get("TEST_F", 28)) | ||
| 14 | -print(f"[cfg] dtype={DT} {W}x{H} {F_}f", flush=True) | ||
| 15 | -free=torch.cuda.mem_get_info("cuda")[0]/1024**3 | ||
| 16 | -vc=dict(offload_dtype=DT,offload_device="cpu",onload_dtype=DT,onload_device="cpu", | ||
| 17 | - preparing_dtype=DT,preparing_device="cuda",computation_dtype=DT,computation_device="cuda") | ||
| 18 | -NF4="DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 19 | -t0=time.time() | ||
| 20 | -pipe=MiniMaxH3Pipeline.from_pretrained(torch_dtype=DT,device="cuda", | ||
| 21 | - model_configs=[ModelConfig(model_id=NF4,origin_file_pattern=p,**vc) for p in | ||
| 22 | - ["minimax-h3-fl2va-nf4.safetensors","minimax-h3-text-encoder-nf4.safetensors", | ||
| 23 | - "video_vae_nf4.safetensors","audio_vae_nf4.safetensors"]], | ||
| 24 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3",origin_file_pattern="FL2VA/processor/"), | ||
| 25 | - vram_limit=max(4.0,free-float(os.environ.get("TEST_RESERVE",5)))) | ||
| 26 | -print(f"[load] {time.time()-t0:.0f}s", flush=True) | ||
| 27 | -pipe.load_lora(pipe.dit, ModelConfig(model_id="lightx2v/Minimax-h3-Turbo", | ||
| 28 | - origin_file_pattern="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors")) | ||
| 29 | - | ||
| 30 | -PROMPT=("A girl is very happy, she is speaking in english: " | ||
| 31 | - "“I enjoy working with Diffsynth-Studio, it's a perfect framework.”") | ||
| 32 | -t1=time.time() | ||
| 33 | -video,audio = pipe(prompt=PROMPT,height=H,width=W,num_frames=F_, | ||
| 34 | - num_inference_steps=4,seed=0,flow_shift=6.0) | ||
| 35 | -print(f"[gen ] {time.time()-t1:.0f}s peak={torch.cuda.max_memory_allocated()/1024**3:.1f}G", flush=True) | ||
| 36 | -a=np.stack([np.asarray(f.convert("RGB"),dtype=np.float32) for f in video[::5]]) | ||
| 37 | -print(f"[video] frames={len(video)} mean={a.mean():.4f} std={a.std():.4f} max={a.max():.1f}", flush=True) | ||
| 38 | -print(f"[audio] finite={bool(torch.isfinite(audio).all())}", flush=True) | ||
| 39 | -print("[VERDICT] " + ("出片正常 ✅" if a.std()>1.0 else "仍为纯色 ❌"), flush=True) |
minimax-h3/scripts/test-patch.py deleted
| 1 | -import os, torch, torch.nn.functional as F | ||
| 2 | -BASE="/data/models/minimax-h3" | ||
| 3 | -os.environ.setdefault("DIFFSYNTH_ATTENTION_IMPLEMENTATION","torch") | ||
| 4 | -os.environ.setdefault("DIFFSYNTH_MODEL_BASE_PATH", f"{BASE}/models") | ||
| 5 | -os.environ.setdefault("MODELSCOPE_CACHE", f"{BASE}/.mscache") | ||
| 6 | -from diffsynth.pipelines.minimax_h3_audio_video import MiniMaxH3Pipeline, ModelConfig | ||
| 7 | -DT=torch.float16 | ||
| 8 | -free=torch.cuda.mem_get_info("cuda")[0]/1024**3 | ||
| 9 | -vc=dict(offload_dtype=DT,offload_device="cpu",onload_dtype=DT,onload_device="cpu", | ||
| 10 | - preparing_dtype=DT,preparing_device="cuda",computation_dtype=DT,computation_device="cuda") | ||
| 11 | -NF4="DiffSynth-Studio/MiniMax-H3-NF4" | ||
| 12 | -pipe=MiniMaxH3Pipeline.from_pretrained(torch_dtype=DT,device="cuda", | ||
| 13 | - model_configs=[ModelConfig(model_id=NF4,origin_file_pattern=p,**vc) for p in | ||
| 14 | - ["minimax-h3-fl2va-nf4.safetensors","minimax-h3-text-encoder-nf4.safetensors", | ||
| 15 | - "video_vae_nf4.safetensors","audio_vae_nf4.safetensors"]], | ||
| 16 | - processor_config=ModelConfig(model_id="MiniMax/MiniMax-H3",origin_file_pattern="FL2VA/processor/"), | ||
| 17 | - vram_limit=max(6.0,free-2.0)) | ||
| 18 | -pipe.load_lora(pipe.dit, ModelConfig(model_id="lightx2v/Minimax-h3-Turbo", | ||
| 19 | - origin_file_pattern="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors")) | ||
| 20 | - | ||
| 21 | -cp = pipe.dit.condition_proj | ||
| 22 | -stats = {"calls":0, "inf_out":0} | ||
| 23 | -orig_lf = cp.linear_forward # 改 linear_forward 而不是 forward: | ||
| 24 | - # 这样 onload/preparing/LoRA 全套机制保持不变 | ||
| 25 | -def lf(x, weight, bias): | ||
| 26 | - stats["calls"] += 1 | ||
| 27 | - out = F.linear(x.float(), weight.float(), bias.float() if bias is not None else None) | ||
| 28 | - if not torch.isfinite(out).all(): stats["inf_out"] += 1 | ||
| 29 | - return out.to(x.dtype) | ||
| 30 | -cp.linear_forward = lf | ||
| 31 | -print("[patch] linear_forward 已替换", flush=True) | ||
| 32 | - | ||
| 33 | -video, audio = pipe(prompt="一只橘猫在窗台上打哈欠", height=480, width=832, | ||
| 34 | - num_frames=124, num_inference_steps=1, seed=42, flow_shift=6.0) | ||
| 35 | -print(f"[stat] condition_proj 被调用 {stats['calls']} 次,其中 fp32 仍非有限 {stats['inf_out']} 次", flush=True) | ||
| 36 | -import numpy as np | ||
| 37 | -a=np.stack([np.asarray(f.convert("RGB"),dtype=np.float32) for f in video[::20]]) | ||
| 38 | -print(f"[video] frames={len(video)} mean={a.mean():.4f} std={a.std():.4f} max={a.max():.1f}", flush=True) | ||
| 39 | -print(f"[audio] finite={bool(torch.isfinite(audio).all())} absmax={float(audio.abs().max()):.4g}", flush=True) |
minimax-h3/scripts/verify.py deleted
| 1 | -"""Verify every downloaded weight: exact byte size vs ModelScope, and a safetensors header parse.""" | ||
| 2 | -import os, sys, json, struct, urllib.request | ||
| 3 | - | ||
| 4 | -BASE = os.environ.get("DIFFSYNTH_MODEL_BASE_PATH", "/data/models/minimax-h3/models") | ||
| 5 | -WANT = { | ||
| 6 | - "DiffSynth-Studio/MiniMax-H3-NF4": ["minimax-h3-fl2va-nf4.safetensors", | ||
| 7 | - "minimax-h3-ref2va-nf4.safetensors", | ||
| 8 | - "minimax-h3-text-encoder-nf4.safetensors", | ||
| 9 | - "video_vae_nf4.safetensors", "audio_vae_nf4.safetensors"], | ||
| 10 | - "lightx2v/Minimax-h3-Turbo": ["minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors", | ||
| 11 | - "minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors"], | ||
| 12 | -} | ||
| 13 | -PROCESSORS = ["MiniMax/MiniMax-H3/FL2VA/processor", "MiniMax/MiniMax-H3/Ref2VA/processor"] | ||
| 14 | - | ||
| 15 | -def remote_sizes(mid): | ||
| 16 | - u = f"https://www.modelscope.cn/api/v1/models/{mid}/repo/files?Recursive=true" | ||
| 17 | - d = json.load(urllib.request.urlopen(u, timeout=60)) | ||
| 18 | - return {f["Path"]: f.get("Size", 0) for f in d.get("Data", {}).get("Files", []) if f.get("Type") == "blob"} | ||
| 19 | - | ||
| 20 | -def header_ok(path): | ||
| 21 | - with open(path, "rb") as f: | ||
| 22 | - n = struct.unpack("<Q", f.read(8))[0] | ||
| 23 | - if not (0 < n < 200_000_000): return False, f"bad header len {n}" | ||
| 24 | - hdr = json.loads(f.read(n)) | ||
| 25 | - end = max(v["data_offsets"][1] for k, v in hdr.items() if k != "__metadata__") | ||
| 26 | - expect = 8 + n + end | ||
| 27 | - actual = os.path.getsize(path) | ||
| 28 | - return expect == actual, f"tensors={len(hdr)} expect={expect} actual={actual}" | ||
| 29 | - | ||
| 30 | -bad = 0 | ||
| 31 | -for mid, files in WANT.items(): | ||
| 32 | - rs = remote_sizes(mid) | ||
| 33 | - for fn in files: | ||
| 34 | - p = os.path.join(BASE, mid, fn) | ||
| 35 | - if not os.path.exists(p): | ||
| 36 | - print(f"MISSING {mid}/{fn}"); bad += 1; continue | ||
| 37 | - got, want = os.path.getsize(p), rs.get(fn, -1) | ||
| 38 | - size_ok = (got == want) | ||
| 39 | - ok, detail = header_ok(p) | ||
| 40 | - flag = "OK " if (size_ok and ok) else "BAD " | ||
| 41 | - if not (size_ok and ok): bad += 1 | ||
| 42 | - print(f"{flag} {fn}\n size {got} vs remote {want} {'==' if size_ok else '!= MISMATCH'} | header {'ok' if ok else 'BROKEN'} ({detail})") | ||
| 43 | - | ||
| 44 | -need = ["tokenizer.json", "tokenizer_config.json", "preprocessor_config.json", "chat_template.json", "vocab.json", "merges.txt"] | ||
| 45 | -for processor in PROCESSORS: | ||
| 46 | - pdir = os.path.join(BASE, processor) | ||
| 47 | - missing = [f for f in need if not os.path.exists(os.path.join(pdir, f))] | ||
| 48 | - print((f"OK {processor}: all present" if not missing else f"BAD {processor} missing: {missing}")) | ||
| 49 | - bad += len(missing) | ||
| 50 | -print("\nRESULT:", "ALL GOOD" if bad == 0 else f"{bad} PROBLEM(S)") | ||
| 51 | -sys.exit(1 if bad else 0) |
minimax-h3/start-all.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 起全套:四张卡按默认分区起(GPU0/1 ref2va,GPU2/3 fl2va)+ 一个调度层。已经在跑的会先停掉。 | ||
| 3 | -# 用法: ./start-all.sh | ||
| 4 | -# | ||
| 5 | -# 停某个端口要按端口找 PID,不要用 pkill -f "port 8010"——bash -c 的命令行本身含这个模式, | ||
| 6 | -# 会把自己的 ssh 会话一起杀掉(踩过,退出码 255)。 | ||
| 7 | -set -uo pipefail | ||
| 8 | -cd /data/models/minimax-h3 | ||
| 9 | - | ||
| 10 | -for PORT in 8000 8010 8011 8012 8013; do | ||
| 11 | - PID=$(ss -lptn "sport = :$PORT" | grep -o "pid=[0-9]*" | cut -d= -f2 | sort -u) | ||
| 12 | - [ -n "$PID" ] && { echo "停 $PORT (PID $PID)"; kill $PID; } | ||
| 13 | -done | ||
| 14 | -sleep 8 | ||
| 15 | - | ||
| 16 | -# GPU 与端口的对应写死在这里,改了要同步改 README。 | ||
| 17 | -# H3_OFFLOAD=disk 是关键:不设就是 cpu,每进程多占 40G 内存,四张卡只能起 3 个。 | ||
| 18 | -export H3_OFFLOAD=disk | ||
| 19 | -# 2026-08-20:默认分区从「4 张全 ref2va」改成 2/2 固定分裂——GPU0/1 ref2va,GPU2/3 fl2va。 | ||
| 20 | -# console 的弹性调度器(h3-console/server.py 的 supervisor)仍然在跑,需求超出这个 2/2 | ||
| 21 | -# 基线时照样会临时 flip 别的卡;这里只是把冷启动默认值往实际更常见的 fl2va 需求上偏, | ||
| 22 | -# 减少「提交 fl2va 任务却撞上 4 张全 ref2va」时那 13-20 秒 flip 冷启动 + 最长 30 秒探测延迟。 | ||
| 23 | -setsid nohup ./api-run.sh 1 8010 ref2va > logs/api-8010.log 2>&1 < /dev/null & | ||
| 24 | -setsid nohup ./api-run.sh 2 8011 fl2va > logs/api-8011.log 2>&1 < /dev/null & | ||
| 25 | -setsid nohup ./api-run.sh 0 8012 ref2va > logs/api-8012.log 2>&1 < /dev/null & | ||
| 26 | -setsid nohup ./api-run.sh 3 8013 fl2va > logs/api-8013.log 2>&1 < /dev/null & | ||
| 27 | -setsid nohup ./gateway-run.sh 8000 > logs/gateway.log 2>&1 < /dev/null & | ||
| 28 | - | ||
| 29 | -echo "等后端加载(disk 模式约 20 秒)" | ||
| 30 | -for i in $(seq 1 30); do | ||
| 31 | - sleep 5 | ||
| 32 | - READY=$(curl -s --max-time 5 http://127.0.0.1:8000/health | grep -o '"ready":[0-9]*' | cut -d: -f2) | ||
| 33 | - echo " ready=${READY:-?}/4" | ||
| 34 | - [ "${READY:-0}" = "4" ] && break | ||
| 35 | -done | ||
| 36 | -curl -s http://127.0.0.1:8000/health | ||
| 37 | -echo |
minimax-h3/start-gateway.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -set -uo pipefail | ||
| 3 | -cd /data/models/minimax-h3 | ||
| 4 | -PID=$(ss -lptn "sport = :8000" | grep -o "pid=[0-9]*" | cut -d= -f2 | sort -u) | ||
| 5 | -[ -n "$PID" ] && { echo "停旧的 (PID $PID)"; kill "$PID"; sleep 3; } | ||
| 6 | -setsid nohup ./gateway-run.sh 8000 > logs/gateway.log 2>&1 < /dev/null & | ||
| 7 | -sleep 5 | ||
| 8 | -pgrep -af "gateway:app" |
minimax-h3/submit-same.py deleted
| 1 | -"""把 8011 上某条已完成任务的请求原样投给另一个端口,用来做单变量对照。""" | ||
| 2 | -import json, sys, urllib.request | ||
| 3 | - | ||
| 4 | -src_port, dst_port, job_id = sys.argv[1], sys.argv[2], sys.argv[3] | ||
| 5 | - | ||
| 6 | -with urllib.request.urlopen(f"http://127.0.0.1:{src_port}/v1/videos/{job_id}", timeout=30) as r: | ||
| 7 | - req = json.load(r)["req"] | ||
| 8 | - | ||
| 9 | -body = json.dumps(req).encode() | ||
| 10 | -post = urllib.request.Request(f"http://127.0.0.1:{dst_port}/v1/videos", data=body, | ||
| 11 | - headers={"Content-Type": "application/json"}) | ||
| 12 | -with urllib.request.urlopen(post, timeout=30) as r: | ||
| 13 | - print(json.load(r)) |
minimax-h3/verify-fixes.py deleted
| 1 | -"""验三处修复:eta 跟帧数走、队列满返回 429、派发仍然分散。 | ||
| 2 | - | ||
| 3 | -跑之前要把调度层的 H3_MAX_QUEUE 调成 1,否则填满默认上限 4 要投 16 条、跑 6 分半。 | ||
| 4 | -用 22 帧的短任务,路由和限流逻辑与长任务完全一样。 | ||
| 5 | -""" | ||
| 6 | -import json, urllib.request, urllib.error | ||
| 7 | -from collections import Counter | ||
| 8 | -from concurrent.futures import ThreadPoolExecutor | ||
| 9 | - | ||
| 10 | -GW = "http://127.0.0.1:8000" | ||
| 11 | -CASES = [ | ||
| 12 | - ("832x480x124f 4步(基准,实测 767s)", {"width": 832, "height": 480, "frames": 124, "steps": 4}), | ||
| 13 | - ("1280x736x56f 4步(实测 736s)", {"width": 1280, "height": 736, "frames": 56, "steps": 4}), | ||
| 14 | - ("832x480x22f 4步(实测 96s)", {"width": 832, "height": 480, "frames": 22, "steps": 4}), | ||
| 15 | -] | ||
| 16 | -SPEC = {"prompt": "一只橘猫趴在洒满阳光的窗台上,慢慢眨眼", "turbo": True} | ||
| 17 | - | ||
| 18 | - | ||
| 19 | -def post(body): | ||
| 20 | - req = urllib.request.Request(GW + "/v1/videos", data=json.dumps(body).encode(), | ||
| 21 | - headers={"Content-Type": "application/json"}) | ||
| 22 | - try: | ||
| 23 | - with urllib.request.urlopen(req, timeout=60) as r: | ||
| 24 | - return r.status, json.load(r) | ||
| 25 | - except urllib.error.HTTPError as e: | ||
| 26 | - return e.code, e.read().decode() | ||
| 27 | - | ||
| 28 | - | ||
| 29 | -print("=== 1. eta 是否跟帧数走(改之前三档全报 691)") | ||
| 30 | -results = [post(dict(SPEC, seed=200 + i, **spec)) for i, (_, spec) in enumerate(CASES)] | ||
| 31 | -for (name, _), (code, body) in zip(CASES, results): | ||
| 32 | - print(f" {name:38} -> eta={body['eta_seconds']}s {body['backend'][-4:]}") | ||
| 33 | - | ||
| 34 | -print("\n=== 2. 队列满是否 429(H3_MAX_QUEUE=1,前 3 条已占 3 张卡)") | ||
| 35 | -with ThreadPoolExecutor(max_workers=4) as pool: | ||
| 36 | - burst = list(pool.map(lambda i: post(dict(SPEC, seed=300 + i, frames=22, steps=4)), range(4))) | ||
| 37 | -for i, (code, body) in enumerate(burst): | ||
| 38 | - detail = body if isinstance(body, str) else f"{body['backend'][-4:]} queue_ahead={body['queue_ahead']}" | ||
| 39 | - print(f" #{i} HTTP {code} {detail}") | ||
| 40 | - | ||
| 41 | -accepted = [b for c, b in results + burst if c == 202] | ||
| 42 | -print(f"\n=== 3. 派发分布: {dict(Counter(b['backend'][-4:] for b in accepted))}") | ||
| 43 | -print(f"接受 {len(accepted)} 条,拒绝 {len(results) + len(burst) - len(accepted)} 条") |
minimax-h3/watch-8012.sh deleted
| 1 | -#!/usr/bin/env bash | ||
| 2 | -# 盯 disk offload 那条任务:耗时、内存、以及 rchar/read_bytes—— | ||
| 3 | -# rchar 是所有读(含页缓存命中),read_bytes 是真正落到存储层的。 | ||
| 4 | -# 两者差得越大说明页缓存命中越好;read_bytes 跟着涨就是在读机械盘,方案不成立。 | ||
| 5 | -set -uo pipefail | ||
| 6 | -PID=$(pgrep -f "port 8012") | ||
| 7 | -JOB="$1" | ||
| 8 | -while true; do | ||
| 9 | - S=$(curl -s --max-time 5 "http://127.0.0.1:8012/v1/videos/$JOB") | ||
| 10 | - ST=$(echo "$S" | grep -o '"status":"[a-z]*"' | cut -d'"' -f4) | ||
| 11 | - EL=$(echo "$S" | grep -o '"elapsed_seconds":[0-9]*' | cut -d: -f2) | ||
| 12 | - RC=$(grep "^rchar:" /proc/$PID/io | tr -dc 0-9) | ||
| 13 | - RB=$(grep "^read_bytes:" /proc/$PID/io | tr -dc 0-9) | ||
| 14 | - PD=$(grep "^Private_Dirty:" /proc/$PID/smaps_rollup | tr -dc 0-9) | ||
| 15 | - printf "%s %-8s 已跑%4ss rchar=%6.1fG read_bytes=%6.2fG 私有内存=%5.2fG\n" \ | ||
| 16 | - "$(date +%H:%M:%S)" "$ST" "${EL:-0}" \ | ||
| 17 | - "$(echo "$RC/1073741824" | bc -l)" "$(echo "$RB/1073741824" | bc -l)" \ | ||
| 18 | - "$(echo "$PD/1048576" | bc -l)" | ||
| 19 | - case "$ST" in done|failed) echo "$S"; break;; esac | ||
| 20 | - sleep 60 | ||
| 21 | -done |