MiniMax-H3 视频生成 API
服务在服务器上:/data/models/minimax-h3/api.py + api-run.sh(本地是副本)。
为什么是异步任务式,不是同步接口
| 事实 | 后果 |
|---|---|
| 模型加载 240 s(首次,fp32 权重从机械盘读;page cache 命中 20 s) | 不能每请求加载 |
| 单条视频 Turbo 4 步约 7 分钟,50 步约 90 分钟 | 同步 HTTP 必然超时 |
| 一条生成占 12.3 G 显存 + 约 32 G 内存 | 22 G 卡上同时只能跑一条 |
所以接口是:提交任务拿 id → 轮询状态 → 下载文件。并发提交会排队,不会 OOM。
启动
cd /data/models/minimax-h3
./api-run.sh 1 8010 # GPU_ID=1, PORT=8010
# 后台跑:
setsid nohup ./api-run.sh 1 8010 > logs/api.log 2>&1 < /dev/null &
⚠️ 显存前提:这条服务常驻约 12.3 G。llm38 跑 256K 上下文时占 57.3 G(四卡各约 14.3 G),
两者在同一张卡上放不下。跑视频前先 docker stop llm38(这是测试机,见工作区 CLAUDE.md),
或把 H3 放到 llm38 没用满的卡上并自行核算。恢复用 qwen38-deploy/RESTORE-GPU.md。
接口
GET /health
{"status":"ok","model":"ready","gpu":"1","queued":0,"running":0}
status 在模型加载完成前是 loading。别在 loading 时提交任务——会排队但看不出进度。
POST /v1/videos → 202
curl -X POST http://192.168.110.67:8010/v1/videos \
-H "Content-Type: application/json" \
-d '{
"prompt": "一只橘猫趴在洒满阳光的窗台上,慢慢眨眼,尾巴轻轻摆动",
"width": 832, "height": 480, "frames": 124,
"steps": 4, "turbo": true, "seed": 7
}'
{"id":"ca0f2f095ddd","status":"queued","queue_position":1,
"eta_seconds":428,"poll":"/v1/videos/ca0f2f095ddd"}
| 字段 | 默认 | 范围 | 说明 |
|---|---|---|---|
prompt |
必填 | 1–2000 字符 | 中英文都行;含人物台词时用英文引号包住那句 |
width |
832 | 64–832 | |
height |
480 | 64–480 | |
frames |
124 | 8–124 | 24 fps,124 帧 ≈ 5.17 秒 |
steps |
4 | 1–50 | turbo 时应 ≤8 |
turbo |
true | true=4 步蒸馏 LoRA;false=全质量多步 | |
seed |
0 | 0–2³¹−1 | 同 seed 同参数结果可复现 |
flow_shift |
自动 | 不传则 turbo=6.0 / 非 turbo=12.0 |
上界不是随手写的:832×480×124f 是实测跑通的最高档(峰值 12.2 G)。再大在 22 G 卡上 OOM, 所以在入口就挡住,而不是让它跑 7 分钟后崩掉。
错误码:
422参数越界或缺失(pydantic 校验)400turbo=true且steps>8(Turbo LoRA 是 4 步蒸馏的,多步没意义)
GET /v1/videos/{id}
{"id":"ca0f2f095ddd","status":"running","step":0,"total_steps":4,
"elapsed_seconds":132, ...}
status: queued → running → done / failed。
完成后多出 file、size_bytes、peak_vram_gb、download。
GET /v1/videos/{id}/file
返回 mp4。任务未完成时返回 409。
GET /v1/videos?limit=50
列出最近任务。
完整调用示例(Python)
import time, requests
API = "http://192.168.110.67:8010"
r = requests.post(f"{API}/v1/videos", json={
"prompt": "清晨的中式庭院,一位女子坐在长廊上喝茶,阳光透过竹叶",
"steps": 4, "turbo": True, "seed": 42,
}, timeout=30)
r.raise_for_status()
jid = r.json()["id"]
print("任务", jid, "预计", r.json()["eta_seconds"], "秒")
while True:
j = requests.get(f"{API}/v1/videos/{jid}", timeout=30).json()
if j["status"] in ("done", "failed"):
break
print(j["status"], j.get("elapsed_seconds", 0), "s")
time.sleep(20)
if j["status"] == "failed":
raise SystemExit("失败: " + j["error"])
open(f"{jid}.mp4", "wb").write(
requests.get(f"{API}/v1/videos/{jid}/file", timeout=300).content)
print("已保存", j["size_bytes"], "字节,峰值显存", j["peak_vram_gb"], "G")
服务端会自己验产物
写出文件不等于成功——fp16 时代踩过:全黑视频照样写出 24 KB 的 mp4 并返回退出码 0。 所以 worker 在写文件前会检查:
- 抽 8 帧算像素
std,< 1.0判为纯色 → 任务标failed - 音频张量含非有限值 → 任务标
failed
这两条不通过时不会给你一个"成功但没用"的文件。
任务记录会落盘(2026-08-20 起)
JOBS 曾经只在内存里,进程重启(比如 h3-console 弹性调度器切 fl2va/ref2va 分区时的
kill+restart)就把这张表清空——视频文件在 outputs/ 没事,但 id→文件路径这层映射没了,
GET /v1/videos/{id}、下载都会 404。现在改成每次状态变化都往 sqlite 写一份:
- 落盘文件是
jobs_gpu{GPU_ID}.sqlite3(跟api.py同目录),四张卡各一份,不共享—— 一个进程只会在同一张 GPU 上起来,per-GPU 分文件天然不会有另一个进程来抢,不用处理 跨进程写锁竞争。 - 进程启动时会把这个文件读回内存,日志能看到
[api] 从磁盘恢复了 N 条历史任务记录。 - in-flight(
queued/running)的记录没法真正恢复——GPU 上下文和排队队列本来就不落盘, 重启后没有任何东西能"接着跑"。这类记录读回来会被改判为failed,error字段写明 "进程重启导致这条任务的执行状态丢失"。只有done/failed这两个终态是完整保留的。 - 只影响直接调用
api.py/gateway.py的场景——如果你是通过h3-console(端口 8080) 提交的任务,即使碰到上面这种真正没法恢复的 in-flight 丢失,console 自己也存了一份 影子记录,历史和已完成任务的文件不受影响。
已知限制
- 无鉴权,和这台机器上其他服务一致。放到公网前必须加。
- 单 worker 串行。要并发就每张空闲卡起一个实例(不同 PORT),自己在上层做负载。
turbo=false时会clear_lora(),任务结束后再挂回来。混合提交 turbo/非 turbo 时 每次切换都有 LoRA 装卸开销,批量跑同一种模式更快。