# 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。 ## 启动 ```bash 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` ```json {"status":"ok","model":"ready","gpu":"1","queued":0,"running":0} ``` `status` 在模型加载完成前是 `loading`。**别在 `loading` 时提交任务**——会排队但看不出进度。 ### `POST /v1/videos` → `202` ```bash 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 }' ``` ```json {"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 校验) - `400` `turbo=true` 且 `steps>8`(Turbo LoRA 是 4 步蒸馏的,多步没意义) ### `GET /v1/videos/{id}` ```json {"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) ```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 装卸开销**,批量跑同一种模式更快。