DESIGN.md 12.4 KB

H3 Console 需求 / 设计文档(草案,待确认)

给 MiniMax-H3 视频生成加一个 Web 管理台:上传 → 生成 → 预览,外加模型/资产/任务/调度的统一视图。 本文档先定范围和方案,代码还没写,确认后再动手。


一、现状(已有什么,不用重建)

服务器 /data/models/minimax-h3/ 已经有一套两层服务,本目录 console 是加在它前面的第三层,不是替换:

浏览器
  │
  ▼
[新增] H3 Console(Web UI + 上传/资产/任务持久化)   ← 本文档要建的东西
  │  内部调用
  ▼
gateway.py(已有,端口 8000)  ← 四后端调度:选最闲的、按 fl2va/ref2va 分流、429 顶回去
  │
  ├─ api.py @ 8010 (GPU1, ref2va)
  ├─ api.py @ 8011 (GPU2, ref2va)
  ├─ api.py @ 8012 (GPU0, ref2va)
  └─ api.py @ 8013 (GPU3, ref2va)

gateway.py 已经做了:调度(选队列最短的健康后端)、分区路由(有首末帧→fl2va,否则→ref2va)、 限流(H3_MAX_QUEUE,满了直接 429)、健康聚合、任务查询/下载代理。这部分不重做。

gateway.py 没有:Web 界面、文件上传(现在参考图/首末帧只接受 URL,_fetch_image 内部用 urllib.request.urlopen 拉取)、任务持久化(JOBS 是内存 dict,进程重启就丢,API.md 里写明了)、 模型/资产的统一视图。这些是 console 要补的。

一个关键约束,直接决定架构:四个 api.py 后端现在全部锁死在 ref2va 分区 (start-all.sh 写死 H3_PARTITION=ref2va ×4),没有一个在跑 fl2va。 所以你要的三个界面里,"上传首尾帧生成视频"这个功能现在提交上去会 503 (gateway.py: 没有就绪的 fl2va 实例)。这个必须先决定怎么办,见下面「需要你拍板」。


二、范围

三个生成界面

  1. 文生视频:输入 prompt + 参数(分辨率/帧数/步数/turbo/seed)→ 提交 → 走 ref2va(不给参考图退回纯文生)。
  2. 首尾帧生视频(FL2VA):上传首帧图(+可选尾帧)→ 提交 → 走 fl2va。
  3. 多参考图生视频(Ref2VA):上传任意多张参考图 → 提交 → 走 ref2va。

统一管理视图

  • 任务列表:状态(排队/运行/完成/失败)、进度、耗时、峰值显存、缩略图,点开内联播放 <video>。
  • 资产库:上传过的参考图/首尾帧 + 生成出的视频,统一存一份、可复用(同一张图不用每次重传)。
  • 模型信息:当前有哪些 partition(fl2va/ref2va)、Turbo LoRA 版本、各 GPU 上跑的是哪个 partition——只读展示,不是"切换模型"(原因见下)。
  • 调度视图:四个后端的实时健康/队列深度(gateway.py /health 已经给了,UI 只是可视化)。
  • 参数预设:把 API.md 里那几档实测能跑的参数(832×480×124f turbo、448×256×362f turbo…)存成可选预设,别让用户瞎填撞 OOM。

明确不做(YAGNI)

  • 不做用户账号体系,暂不加鉴权(问题 3 已拍板,跑通了再加)。
  • 不改 api.py 本身(不做"进程内热切 partition"——DiT+LoRA 配套,热切等于重新构建 pipeline, 和"杀进程重启"成本几乎一样,没必要在 api.py 里加这个复杂度,console 用进程级重启就够)。
  • 不做视频编辑/剪辑,只做生成+预览+下载。

四点五、弹性 partition 调度器(新增,替换原先"静态锁一张卡"的方案)

为什么不能复用 gateway.py 现成的调度逻辑

gateway.py 只会在已经 ready 的后端里按 partition 挑最闲的(health_all() 读每个后端 /health 上报的 partition 字段)。它不会主动让某个后端换 partition——这件事没人做。console 要做的就是这层。

谁来当"总闸"

console 新增一个后台循环(asyncio 任务,仅在部署到服务器、H3_ENABLE_SUPERVISOR=1 时启用; 本地 Mac 开发时关掉,因为这一层要直接管服务器上的进程,本地没有意义模拟):

每 10 秒一轮:
  1. 读 console 自己 sqlite 里「排队中 + 运行中」的任务,按 partition 分两堆计数:
       fl2va_pending, ref2va_pending
     (partition 是提交时 console 自己按规则算好存下的:给了首尾帧 URL → fl2va,否则 → ref2va,
      跟 gateway.py 里 `want = "fl2va" if ... else "ref2va"` 完全一致的判断,不重新发明)
  2. 读 gateway.py /health,按 partition 分两组:
       fl2va_ready = [backend for backend in health if partition==fl2va]
       ref2va_ready = [backend for backend in health if partition==ref2va]
  3. 扩容判断(一轮最多做一次 flip,避免抖动):
       if fl2va_pending > len(fl2va_ready) 且 ref2va_ready 里存在 idle(queued==0 且 running==0)的后端:
         挑一个 idle 的 ref2va 后端,flip 成 fl2va
       elif ref2va_pending > len(ref2va_ready) 且 fl2va_ready 里存在 idle 的后端:
         挑一个 idle 的 fl2va 后端,flip 成 ref2va
  4. 缩容加一道保险:一个后端要「连续 idle ≥ 30 秒」才允许被 flip 走,
     防止一个请求刚处理完、下一个还没提交上来这种间隙里被误判成"没需求"来回抖。

flip 一个后端的具体动作(和我今天手动测试用的手法完全一样,复用现成脚本,不新写进程管理代码):

PID=$(ss -lptn "sport = :$PORT" | grep -o "pid=[0-9]*" | cut -d= -f2)
kill $PID                                    # 只杀这一个端口对应的进程,不动其余三个
setsid nohup ./api-run.sh $GPU $PORT $NEW_PARTITION > logs/api-$PORT.log 2>&1 < /dev/null &
# 等 /health 返回 model=ready 再纳入调度(disk offload 模式下实测约 13-20 秒,page cache 命中很快)

只允许 flip 一个 queued==0 且 running==0 的空闲后端——绝不杀正在生成的任务(会丢掉几分钟到十几分钟的进度)。

GPU/端口/初始 partition 映射(写死在 console 配置里,对应现有 start-all.sh)

GPU1 → 8010    GPU2 → 8011    GPU0 → 8012    GPU3 → 8013

初始状态(2026-08-20 起):GPU0/1 = ref2va、GPU2/3 = fl2va,2/2 固定分裂 (start-all.sh 已经这么起了,console 启动时只做"接管监控",不需要在 console 里重新实现 "冷启动 4 个后端"这部分,那是 start-all.sh 的职责,边界清楚)。之前是 4 张全 ref2va, 改成 2/2 是因为 fl2va 需求出现频率更高,全 ref2va 基线下每次 fl2va 任务几乎都要先等一次 13-20 秒的 flip 冷启动;调度器本身逻辑不变,超出 2/2 基线的需求仍然按上面的算法弹性 flip。

这层为什么只能在服务器上跑,不能本地模拟

kill+restart 操作的是服务器上真实的 GPU 进程。本地 Mac 没有这几张卡、没有这几个进程, 硬造一个"本地模拟版"没有意义(YAGNI)——三个生成界面、上传、任务列表这些跟 GPU 无关的部分 本地对着远程 gateway.py(问题 4:内网直连)就能测完;弹性调度这部分只能等部署到服务器后现场验证。


三、四个问题的决定(2026-08-20 已拍板)

  1. fl2va 怎么分卡:按需弹性,不是固定一张卡。 有几个 fl2va 任务排队,就用几张卡跑 fl2va;demand 降下去,卡要能flip 回 ref2va。 这比原先设计的方案 (c)(静态锁死一张卡)复杂得多,见下面「四点五」的具体设计。
  2. 端口:console 挂 8080,走现成的 gateway.wudaima.com 隧道,不用改 FRP。
  3. 鉴权:暂不加,整体跑通了再加。
  4. 本地开发连接方式:内网直连 http://192.168.110.67:8000(选项 B)。

四、技术方案

后端:Python + FastAPI(复用 api.py/gateway.py 已经在用的框架,不引入新依赖)

  • console/server.py:
    • 反代 gateway.py 的 /v1/videos* 接口(生成/查询/下载),不重复实现调度逻辑。
    • 新增 /v1/assets:上传图片/视频,落盘到 console/assets/,返回一个可被 api.py 现有的 first_frame_url/reference_image_urls(URL 字段)使用的本地 URL——console 自己起一个静态文件服务, 上传后返回 http://<console>/assets/<id>.jpg,直接喂给 _fetch_image()(它本来就是通用 urlopen, 不关心 URL 来自哪)。这样不用改 api.py/gateway.py 一行代码。
    • 新增 SQLite(stdlib sqlite3,不引入 ORM)持久化任务记录——console 自己维护一份影子记录, 定期轮询 gateway.py /v1/videos 同步状态,解决"进程重启任务历史就丢"的问题。 这是新增持久层,不是替换 api.py 里的内存 JOBS(那个继续管真正的推理状态,改它风险更高)。
    • 只读接口:/v1/models(列 partition/LoRA 配置,读 PARTITIONS 那份静态信息或直接读 api.py 源码里的常量)、 /v1/backends(转发 gateway.py /health)。
  • 鉴权:Depends() 校验 Authorization: Bearer <token>,token 走环境变量,见上面问题 3。

前端:单页面,原生 HTML/JS(不引入 React/Vue 构建链——三个表单 + 一个任务列表,够不上框架)

  • console/static/index.html:顶部 tab 切三个生成界面,下面任务列表(轮询 /v1/videos 刷新, done 状态内联 <video src=".../file">)。
  • 上传用原生 <input type=file> + fetch(FormData),不需要额外库。
  • 这条完全符合"能一行搞定就别加依赖"——纯 fetch/DOM,没有构建步骤,python -m http.server 都能测。

目录结构(新增 h3-console/ 工作负载,符合仓库既有约定)

h3-console/
  DESIGN.md        # 本文档
  README.md        # 完成后补:跑在哪张卡/端口、怎么起停、怎么回滚(现在还没有,按约定完成后必须补)
  server.py         # FastAPI 主程序
  static/index.html # 单页前端
  assets/           # 上传的图 + 一份任务持久化 sqlite(.gitignore 掉,不进版本库)
  run.sh            # 本地/服务器通用启动脚本

部署路径

  1. 本地 Mac 先跑通:server.py 里 H3_GATEWAY_URL 指向 http://192.168.110.67:8000(问题 4 选 B), 本地 python server.py 起在比如 :8090,浏览器 localhost:8090 测三个界面 + 任务列表。 这一步验证 UI/上传/轮询逻辑,不需要动服务器任何东西。
  2. 确认没问题后部署到服务器:整个 h3-console/ 目录 scp 到 /data/models/minimax-h3/console/, H3_GATEWAY_URL 改成 http://127.0.0.1:8000(同机,不用走网络),起在 8080(问题 2)。 FRP 那条 gateway.wudaima.com 隧道不用改,直接生效。
  3. 按仓库约定:部署完在 h3-console/README.md 补验收记录、在顶层 CHANGELOG.md 加一节。

五、开放风险 / 后续会撞到的坑(先写下来,免得到时候当新发现浪费时间)

  • 上传接口 = 新的攻击面:任意文件上传要做基本校验(文件类型、大小上限、图片能否被 PIL 打开), 这属于"输入校验在信任边界",不是可以偷懒的部分。
  • console 自己的 SQLite 和 api.py 内存态可能短暂不一致(轮询间隔内)——UI 上任务状态允许有几秒延迟, 不追求强一致,够用。
  • 磁盘:/ 只剩 29G(CLAUDE.md 记录),/data 也 83% 满(300G 剩)。资产库和生成视频都落 /data, 上传接口要做磁盘余量检查/清理策略,不然堆满了会像 llm38_prev 那样变成"不敢删"的包袱。
  • 弹性调度的抖动风险:如果 fl2va/ref2va 需求犬牙交错地来(比如每隔 20 秒各来一个), 30 秒 idle 门槛可能不够、来回 flip,每次 flip 有 13-20 秒重载开销。先用这个简单阈值上线, 真出现抖动了再加更复杂的判断(比如「最近 N 次 flip 频率过高就临时锁定」),不预先过度设计。
  • flip 期间那个端口短暂离线:kill 到新进程 ready 之间的 13-20 秒,gateway.py /health 会看到 它 down,此时 gateway.py 自己的调度会跳过它选别的后端——这是既有行为,不用 console 额外处理。

六、下一步

四个问题已确认(见第三节),开始写代码:先本地跑通三个生成界面 + 上传 + 任务列表(对着远程 gateway.py 直连),你看过没问题后部署到服务器,弹性调度器在服务器上现场验证。