日本語 | English
MiniMax H3 (Hailuo 3.0) — 動画+ステレオ音声を1回のデノイズで同時生成する 33B オムニ モーダルモデルを、普及帯GPUで動かす検証アプリ。 テキスト / 画像参照 / 音声参照を入力に 動画+ステレオ音声を生成し、静止画のみ(T2I / Ref2I)も出せる。基盤は diffusers の Modular Pipeline(PR #14355、コミット f37ab93 にピン留め)。8GB×2 の2枚構成でも、 実 RTX 4060 Ti 16GB カード単体でも動くことを実測済み(下の対応表)。将来 diffusers-server へ統合するための先行検証 ワークスペース(diffusers-server 本体には一切手を入れていない)。

ブラウザから使う単一ページUI。5つのモードをタブで切替(動画: T2VA=テキスト / FL2VA=フレーム / Ref2VA=参照、静止画: T2I / Ref2I)。下段は生成物ギャラリーで、 チェックした動画を連結して書き出せる。プロンプトはローカルLLMで H3 公式ガイドの形式へ強化でき、 FirstBlockCache・Sage・Turbo・量子化・低VRAMモードなどは再起動なしでこのパネルから切替。 日英切替あり。
○ = 実測で完走 / △ = 導出見込み(未実測)/ × = OOM。時間は定常値(初回のモデル ロードを含まない2回目以降)、ピークは torch 計測の割当ピーク。速度はいずれもこの箱の PCIe Gen3 x4 スロット + sm_89(4060 Ti)+ SDPA での値で、Gen4 x16 なら重み転送は 約1/8になる(下の「速度の正直な注記」参照)。16GB/8GiB 行は 30ステップ、 48GB+20GB 行は turbo 4ステップ。
| 構成 | t2i 768² | t2va 5秒 768² | ref2i | i2va(画像参照) | 音声参照 | 768×1344 5秒 |
|---|---|---|---|---|---|---|
| 実 RTX 4060 Ti 16GB 単体 | ○ 498s・ピーク7.4GB | ○ 25分・11.4GB | ○ | ○ 39分・9.41GB | ○ 54分・11.96GB | ○ 66分・13.37GB(実15.2GB=上限) |
| 8GiB×2(計算+TE、バラスト模擬) | ○ 512s・6.4GB | ○ 25.6分・7.23GB | ○ 17.7分・6.69GB | × OOM | × OOM | × OOM |
| 12GB 単体 | △ 見込み | △ | △ | △ | ×に近い(実14.7GB) | × |
| 48GB+20GB(2枚、turbo LoRA) | ○ 9.7s | ○ 44.2s | ○ | ○ | ○ | ○ |
H3_LOWVRAM=group(transformer int8 をブロック単位で
ストリーム、GPU常駐は ~1.4GB)+ video VAE の fp16 デコード。詳細は日付節 2026-08-10〜11。セットアップは「インストール」「モデルの取得」を先に済ませること。
起動後、ブラウザで http://<host>:8611/ を開く。
# 実16GB 単体(sm_89 の 4060 Ti など)
H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 \
H3_ATTN_BACKEND=default \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
# ※ sm_120 以外は H3_ATTN_BACKEND=default が必須(既定の sage は sm_120 専用ビルド)
# 8GB×2(TE を2枚目GPUへ)
H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 \
H3_ATTN_BACKEND=default H3_TE_DEVICE=cuda:1 \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
# 48GB級(推奨: 高速化フル)
H3_LOWVRAM=1 H3_TE_PRUNE=1 H3_TE_DEVICE=cuda:1 H3_VIDEO_VAE_FP16=1 H3_KEEP_TRANSFORMER=1 \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
# 96GB級(全モデル常駐、env なし)
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
より細かい VRAM 級別(80/48/32/18GB)の起動例と全環境変数は 「VRAM対応表と主な環境変数」を参照。
このリポジトリの中心的な発見: 律速はデノイズではなく「モデルの load/free という固定費」 だった。 以下は各節の実測値を1か所に集めたもの(再測定ではない)。
48GB機(PRO 5000 48GB + RTX 4000 20GB)・H3_LOWVRAM=1 系・768²・定常値
(初回のモデルロードを含まない2回目以降。t2va は 5秒 = 124フレーム)。
| 段階 | t2i 1枚 | t2va 5秒 |
|---|---|---|
| 素の構成(30steps、turbo なし) | 157s | 351.4s |
| + lightx2v turbo LoRA(4steps) | 157s | 143s |
+ H3_TE_PREQUANT(量子化TEのディスクキャッシュ) | 83.2s | — |
+ H3_TE_DEVICE(TE を2枚目GPUへ) | 約35s | 60.5s |
+ H3_KEEP_TRANSFORMER(transformer 常駐) | 9.7s | 44.2s |
| 16倍 | 8.0倍 |
デノイズ自体を速くする turbo だけでは 2.6倍で頭打ち。残りの3倍強は すべて固定費の撤廃から来ている(turbo なし 30steps のままでも、固定費撤廃だけで t2i は 157s → 51.1s と3倍速い)。
構成: int8 transformer 常駐 + 投影TE(NF4)+ デコード窓の解放停止 + turbo 4steps + sage +
fp16 デコード、GPU 1枚(96GB機で実測。H3_TRANSFORMER_QUANT=int8 H3_KEEP_TRANSFORMER=1 H3_VIDEO_VAE_FP16=1 H3_TE_PROJ=… H3_TURBO_LORA=1)。
| モード | 単発(最速) | 連番なら1件あたり | デノイズ | ピーク | 時間を食っている残りの固定費 |
|---|---|---|---|---|---|
| t2i 768² | 7.40s | 7.9s(0.94倍 = 使う意味なし) | 2.40s | 45.0GB | VAE の CPU↔GPU 往復 ~3.3s |
| t2va 5秒 768² | 28.13s | (バッチAPIなし) | 14.94s | 45.6GB | デコード 7.05s |
| ref2i 768²(参照付き静止画) | 79.3s | 47.0s(1.69倍) | 7.8s | 45.4GB | 参照ビジョンエンコード ~47s |
| i2va 5秒 768²(画像参照→動画) | 103.1s | 75.0s(1.37倍) | 22.0s | 45.9GB | 同 ~47s + t2va transformer の再ロード ~13s |
計測条件(この表の数値の読み方):
すべて定常値。サーバ起動時の初回ロードは含まない(2回目以降のリクエスト)。 起動時に transformer + VAE を常駐させるのに 約50秒かかるが、これはプロセスに1回だけ。
参照系(ref2i / i2va)は初回リクエストだけ別: transformer_ref は起動時ではなく
最初の参照リクエストでロードされるため、初回のみ +55秒(ref2i 実測: 初回 134.7s →
定常 79.3s)。以後は常駐するので定常値になる。
解像度 768×768 固定。動画は 5秒 = 124フレーム(24fps)、静止画は 22フレーム。
turbo LoRA の 4ステップ(H3_TURBO_LORA=1)。FirstBlockCache は turbo により自動 OFF。
attention は sage。seed とプロンプトは各モードで固定。
「連番なら1件あたり」= 同じ参照/設定で複数件まとめて作るときの1件あたり
(/api/t2i_batch, /api/ref2i_batch, /api/ref2va_batch)。実測は t2i/ref2i が3場面、
i2va が2場面。バッチ経路は H3_LOWVRAM=1 専用なので単発列とは構成が違う —
つまり「1件だけ作る最速手段」と「N件作る最速手段」を並べた表。
全モードが 45〜46GB に収まる = 48GB カード1枚で全機能が最速級で動く。 ただし t2va 系と参照系を同じプロセスで混ぜると両方の transformer が常駐して 74.3GB になる(実測。t2va に戻ったときのピークは 77.3GB)。48GB 1枚で運用するなら プロセスをモード別に分けること。
参照系の律速はデノイズではなく参照のビジョンエンコード ~47s。turbo の効きが t2va(5.5倍)より小さく i2va で 2.8倍に留まるのはこのため。
2枚構成 + bf16 transformer なら t2i 6.89s / t2va 26.8s がこれまでの最速だが、 77GB 級のカードが要る(差は 7% 程度)。詳細は日付節 2026-08-12。
47×(場面数-1)/場面数。i2va なら 2場面 1.37倍 →
3場面 1.44倍 → 5場面 1.57倍。同じキャラクターで長い物語を作る用途ほど有利。H3_LOWVRAM=1 専用で、それ以外では黙って逐次実行に落ちる。
参照バッチは H3_TE_DEVICE と併用不可(「TE用GPUに 24GB 以上」のガード。32B TE の
vision 活性化を前提にした閾値で 3.11GB の投影TEには過大 — 16GB の 4060 Ti が弾かれる)。
どちらも要見直し。計測の落とし穴(2026-08-12 に実際に踏んだ): バッチ側だけ
height/widthを渡さずに 単発と比べると、バッチはサーバ既定の 16:9 キャンバス(1344×768)で生成されるため 1.75倍のピクセル数になり、「バッチはステップが 1.75 倍遅い」という存在しない現象が 見える。ピクセル比 1344×768÷768² = 1.75 と実測のステップ時間比 1.753 が一致したことで 判明した。モード間・経路間の比較では解像度を必ず明示すること。
低VRAM構成は動くが遅い。 H3_LOWVRAM=group は毎ステップ int8 重み(~34GB)を CPU→GPU に
流すため、転送が律速になる。上の対応表の 16GB / 8GB の実測値は PCIe Gen3 x4 スロットでの
もので、16.5s/step(t2i)〜51s/step(t2va)のほとんどは転送時間。Gen4 x16 のまともな
スロットなら転送は約1/8になるので、これは「16GBカードの性能」ではなく「この箱のスロット」の値。
また int8+SDPA の軌道では FirstBlockCache が効かず(cache_skipped_steps: 0)、閾値調整で
最大2倍の短縮余地がある(未検証)。
品質については、投影TE(PSNR 22.4dB vs 32B)も int8+SDPA も、同一seedでも構図が変わる (軌道の分岐であって劣化ではない)。むしろ実測では int8+SDPA の方がプロンプト忠実度が 上がったケースもあり、参照(ビジョン)経路も目視で 32B 正解と同水準の忠実度を確認済み。 構成をまたぐ品質は PSNR/MD5 では判定できないので目視で見ること。
H3_VIDEO_VAE_FP16 × 参照ありの dtype 即死を修正(エンコード側にも fp16 autocast)。作業を再開するとき、まずここを読む。
| 知りたいこと | 見る場所 |
|---|---|
| 仕様・性能・設計(何をどう実現しているか) | docs/TECHNICAL_OVERVIEW.md |
| 踏んだ罠・失敗・運用の教訓(同じ穴を避けたい) | docs/internal/TECHNICAL_REPORT.md |
| コミュニティ改良の取り込み判断 | docs/COMMUNITY_IMPROVEMENTS.md |
| どのモードで何がいつロード/解放されるか | docs/RESIDENCY.md |
| 次に何をやるか(未着手・未検証) | 本 README の「今後の外部イベント待ち」§3 |
| diffusers を上げるときの回帰基準値 | 96GB機: regression_baselines_dev96.json(16ケース、scripts/run_regression.py で実行)/ 48GB機: regression_baselines.json |
現在の状態: diffusers はマージ版 f37ab93 にピン留め(PR #14355 の追従完了、全経路が
同一seed MD5 で等価)。低VRAMの最終ゴール2つ(実16GB単体 / 8GB×2)を 2026-08-11 に達成し、
訴求点は「高速化」だけでなく「普及帯GPUで動く」に広がった。48GB機での推奨起動は
H3_LOWVRAM=1 H3_TE_PRUNE=1 H3_TE_DEVICE=cuda:1 H3_VIDEO_VAE_FP16=1 H3_KEEP_TRANSFORMER=1
(t2i 定常 9.7s/枚、t2va 5秒 44.2s)。ref2va を使うときは H3_TE_DEVICE を外す
(TE用GPUが 20GB では容量不足で自動 400 拒否)。時系列の作業記録は本 README の日付付き
セクション群(下へ読み進む)。
測定環境について: この home は2台のマシンで共有されている。96GB機 = RTX PRO 6000 Blackwell 96GB + 増設した RTX 4060 Ti 16GB(低VRAMゴールの実測はこちら)、48GB機 = RTX PRO 5000 Blackwell 48GB + RTX 4000 SFF Ada 20GB(48GB級の推奨構成・turbo の実測はこちら)。 各日付節の数値がどちらの箱のものかは節内に明記してある。48GB 単騎では既定モード (bf16 transformer 66.3GB)は物理的にロードできないため、
H3_LOWVRAM=1(48GB級)かH3_LOWVRAM=group(24-32GB級)が必須。
minimax-h3/
├── app.py # FastAPI 本体 (port 8611)
├── core/
│ └── runner.py # ModularPipeline のロード/生成ロジック本体
├── static/
│ └── index.html # 単一ページUI (日本語)
├── scripts/
│ ├── download_t2va.py # T2VA検証に必要なサブフォルダのみをDLするスクリプト
│ └── probe_t2va.py # UIより先に動作確認する回帰スクリプト
├── outputs/ # 生成物 (.gitignore対象)
├── logs/ # ダウンロード監視ログ等 (.gitignore対象)
└── venv/ # 専用venv (.gitignore対象、下記参照)
| 要件 | |
|---|---|
| GPU | 最小: 16GB 単体(全機能・実測済み・遅い)または 8GB×2(t2i/t2va/ref2i まで)。快適動作は 48GB 級を推奨。構成別の実測は冒頭の「VRAM×機能マトリクス」参照 |
| ホストRAM | 64GB 以上を推奨(H3_LOWVRAM=group は int8 重み ~34GB をRAMに常駐させるため 48GB 以上の空きが必要) |
| ディスク | 約 145GB(T2VA/FL2VA のみ)/ 約 207GB(Ref2VA も使う場合) |
| CUDA | 12.8 系(torch 2.9.0+cu128 に合わせる)。SageAttention をビルドするなら nvcc も同系統 |
| Python | 3.12 |
| その他 | ffmpeg(ギャラリーの連結・情報取得に使用。無くても生成自体は動く) |
git clone https://github.com/animede/Diffusers_minimax-h3.git
cd Diffusers_minimax-h3
python3.12 -m venv venv
# PyTorch (cu128)
venv/bin/pip install torch==2.9.0 --index-url https://download.pytorch.org/whl/cu128
# diffusers は「検証済みコミットに固定」して入れる(下の注意を必ず読むこと)。
# マージ版 f37ab93(PR #14355 の最終形)が前提。全経路(t2i/t2va/バッチ/ref2va/refバッチ)
# を旧ピン abc5e9b と同一seed MD5 で回帰済み(末尾「追従 第1段/第2段」節)。
venv/bin/pip install "git+https://github.com/huggingface/diffusers.git@f37ab93e621d5ce206c9662e8291ca8b67d9c555"
# transformers は 5.14.1 以上が必須
# (5.1.0 には Qwen3VLProcessor.create_mm_token_type_ids が無く、PR #14355 のエンコーダが動かない)
venv/bin/pip install "transformers==5.14.1" accelerate==1.12.0 safetensors huggingface_hub
# 動画/音声の多重化と Web API
venv/bin/pip install av==16.0.1 fastapi==0.104.1 "uvicorn==0.24.0" python-multipart pillow numpy
# text_encoder の 4bit 量子化(既定の H3_TE_QUANT=bnb-4bit に必要)
venv/bin/pip install bitsandbytes==0.49.0
# transformer の int8 量子化を使う場合のみ(H3_TRANSFORMER_QUANT=int8 / H3_LOWVRAM)
# 0.18 以降は torch>=2.11 を要求するため 0.17.0 を固定する
venv/bin/pip install torchao==0.17.0
重要: diffusers はコミット固定のまま使うこと。 PR #14355 は 2026-08-05 にマージ済みで、本アプリはマージ最終形 f37ab93 へ 追従完了(第1段: t2i/t2va/バッチ、第2段: ref2va 系)。全経路が旧ピン abc5e9b の ベースラインと同一seed MD5 完全一致で回帰済み(末尾「追従 第1段/第2段」節)。 ここからさらに diffusers を上げる場合も、同じ手順(同一seed MD5 回帰)を踏むこと。
既定の H3_ATTN_BACKEND=sage を使うには sm_120 向けのビルドが要る(Linux 向けの
事前ビルド wheel は存在しない)。ビルドしない場合は H3_ATTN_BACKEND=default を
指定して起動すること(デノイズが約12%遅くなるだけで、機能に影響はない)。
CUDA_HOME=/usr/local/cuda-12.8 scripts/build_sageattention.sh
ビルドは 必ず並列数を制限すること(スクリプトは
MAX_JOBS=4 NVCC_THREADS=2+ systemd-run のメモリ上限付きで実行する)。無制限の並列 nvcc はホストRAMを食い潰し、 システム全体を巻き込んで OOM する。
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
ブラウザで http://<host>:8611/ を開く。初回起動時にモデルをロードするため数分かかる。
MiniMaxAI/MiniMax-H3 は498.6GB(FL2VA/Ref2VAの2チェックポイント + 両方のtransformer分)
だが、T2VA/FL2VA検証には transformer/(FL2VA用、66.3GB)+ text_encoder/(66.7GB)+
vae/(10.4GB)+ audio_vae/(0.6GB)+ 設定類の約144GBのみで足りる。
transformer_ref/(Ref2VA用、66.3GB)と Ref2VA/ FL2VA/ の別パッケージ(各144GB)は
今回不要なので 絶対に丸ごと snapshot_download しないこと。
venv/bin/python scripts/download_t2va.py
内部で allow_patterns を使い、必要なサブフォルダのみを取得する。ダウンロード中は
logs/du_monitor.log でキャッシュサイズを監視できる(170GB超で警告)。
用途に応じた起動例。上段5行は 96GB 機(PRO 6000)でのバラスト実測(32B TE 使用)、 下段3行は 投影TE を使う実カード実測(2026-08-11。詳細は冒頭の 「VRAM×機能マトリクス」と日付節 2026-08-11)。
| GPU | 起動時の指定 | t2va 実測(768²・5秒) |
|---|---|---|
| 96GB | (指定なし=既定) | peak 92GB / 約160秒 |
| 80GB級 | H3_TRANSFORMER_QUANT=int8 | peak 59.7GB / 約160秒 |
| 48GB級 | H3_LOWVRAM=1 | peak 38.9GB / 約215秒 |
| 32GB級 | H3_LOWVRAM=group | peak 28.7GB / 約280秒 |
| 18GB級 | H3_LOWVRAM=group H3_TE_PRUNE=1 | peak 17.7GB / 約280〜320秒 |
| 16GB 単体(実 4060 Ti) | H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 | peak 11.4GB / 約25分※ |
| 12GB 単体(見込み・未実測) | 16GB 単体と同じ | 実ピーク11.4GBが12GiBに入る計算。参照系・768×1344は不可 |
| 8GB×2(バラスト実測) | 16GB 単体の指定 + H3_TE_DEVICE=cuda:1 | peak 7.23GB / 約25.6分※ |
※ 低VRAM実カード列の時間は PCIe Gen3 x4 スロット + sm_89(SDPA)での実測。Gen4 x16 なら
重み転送は約1/8(「速度の正直な注記」参照)。sm_120 以外のカードは
H3_ATTN_BACKEND=default も必要。
# 例: 32GB級GPUで起動する
H3_LOWVRAM=group H3_TE_PRUNE=1 venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
# 例: 16GB カード単体で起動する(sm_89 なら H3_ATTN_BACKEND=default も付ける)
H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
主な環境変数(既定値。多くはUIからも切り替えられるので、恒久的に変えたい時だけ指定する):
| 変数 | 既定 | 意味 |
|---|---|---|
H3_TE_QUANT | bnb-4bit | text_encoder の量子化(none は bf16 で 66.7GB) |
H3_TE_PRUNE | 0 | TE の未使用上位レイヤー削除(出力は不変、-3.6GB) |
H3_TE_PROJ | (無効) | 投影TE: 32B TE を Qwen3-VL-4B+線形写像で代替(repo id か .safetensors パス。品質は近似、日付節 2026-08-10 参照) |
H3_TE_PROJ_QUANT | bnb-4bit | 投影TE 4B の量子化(NF4 で 3.11GB。none=bf16 8.88GB / bnb-8bit) |
H3_TE_DEVICE | (無効) | TE を2枚目GPUへ常駐(例 cuda:1。32B TE は 20GB 級、投影TE なら 8GB 級で可) |
H3_TRANSFORMER_QUANT | none | int8 で transformer を 66.3→34GB |
H3_LOWVRAM | 0 | 1=48GB級のフェーズ循環 / group=32GB級以下の block offload |
H3_KEEP_TRANSFORMER | 0 | transformer 常駐で再ロード固定費を撤廃(成立条件は該当節参照) |
H3_CACHE / H3_CACHE_THRESHOLD | fbc / 0.05 | FirstBlockCache(デノイズ -25%。int8+SDPA 軌道では不発の実測あり) |
H3_ATTN_BACKEND | sage | sage は sm_120 専用ビルド。それ以外のGPUは default(SDPA)必須 |
H3_TURBO_LORA | 0 | 4/8ステップ蒸留LoRA(初回に約780MBをDL。group とは併用不可) |
H3_VIDEO_VAE_FP16 | 0 | video VAE を fp16 化(デコードのピークを削減。16GB 以下では必須) |
H3_LLM_URL | http://127.0.0.1:64650 | プロンプト強化に使うローカルLLM(任意機能) |
このマシンは VRAM 96GB に対し RAM は 94GB。text_encoder は bf16ネイティブ配布で
実測66.73GB(fp32配布を仮定した「bf16化で半分の33GB」という当初推定は誤りだった)。
transformer bf16 66.3GB / vae+audio_vae fp32 計11GB と合わせると約144GBになり、
VRAMにもRAMにも同時に載らない。diffusers の
ComponentsManager.enable_auto_cpu_offload()(全コンポーネントを定常的にRAM常駐させ、
アクティブな1つだけをGPUへ出す方式)は RAM 94GB では成立しないため採用していない。
TEのロード方式は環境変数 H3_TE_QUANT で選択する(既定 bnb-4bit、2026-08-04 に
A/B検証して既定化)。
H3_TE_QUANT=bnb-4bit (既定)text_encoder を起動時に NF4(bitsandbytes、compute_dtype=bf16)へ量子化して GPU常駐のまま維持する(bnb 4bitモデルはデバイス間移動不可のため常駐一択)。 実測サイズ 21.0GB(当初推定の~17-18GBより大きい)。transformer(66.3GB)も常駐し、 リクエストごとの TE⇔transformer 入れ替えが消滅する。
H3_TE_QUANT=none (bf16 TE、旧方式)2つの66GBモデルをリクエストごとにGPU上で入れ替える:
vae + audio_vae(fp32 計11GB)は常時GPU常駐。.to("cpu") でRAMへ退避しない。66GBの
RAM経由はスワップ突入の実測原因になった)。リロードはページキャッシュ/ディスクから
11〜40秒/モデル。リクエスト間の定常状態は transformer+VAE 常駐(77.5GB)。実測のオーバーヘッド: 1リクエストあたり TEロード ~37s + transformerリロード ~26s。
この入れ替えコストを解消したのが上記の bnb-4bit(既定)で、リクエスト合計は
245s → 185s に短縮された(デノイズ157sは共通のモデル律速で不変)。
2つの実装上の罠(実機で踏んで修正済み):
MiniMaxH3TextEncoderStep.encode_prompt は素の staticmethod で、@torch.no_grad()
はブロックの __call__ 側にしか付いていない。直接呼ぶ場合は必ず torch.no_grad()
で包むこと。忘れると autograd グラフが TE の重み約50GB分をGPU上にピン留めし、
モデルを解放してもVRAMが返ってこない(diffusers-server CLAUDE.md 39番と同型)。num_frames・keyframes・latent形状等)は PipelineState に入る。
get_block_state() は宣言された入力しかマップしないので、出力は state.get(名前)
で読むこと。video VAE の decode は diffusers 側の MiniMaxH3VideoDecodeStep が内部で
torch.autocast(dtype=torch.float16) を使うため、重み自体は fp32 のままでよい。
audio VAE は fp32 のまま一切キャストしないこと(bf16化すると生成音声の音量が
約20dB小さくなる既知の問題があるため、runner.py は vae/audio_vae のロードに
明示的に dtype=torch.float32 を渡している)。
| 項目 | 実測 |
|---|---|
| DLサイズ(T2VA必要分のみ) | 135GiB (HFキャッシュ実測) |
| text_encoder ロード | 37.6s (コールド) / 15.9s (ページキャッシュ温) |
| transformer ロード | 37.7s (コールド) / 10〜26s (温) |
| vae+audio_vae ロード | 10.0s |
| プロンプトエンコード | 0.7s |
| デノイズ (30steps) | 157〜159s (約5.4s/step, GPU 100%/600W) |
| VAEデコード (video+audio) | 6.5〜9s |
| ピークVRAM (生成中) | none: 83.4GB (デコード時。デノイズ中70.4GB) / bnb-4bit: 91.7GB |
| リクエスト合計 (サーバAPI経由、ロード込み) | none: 245s / bnb-4bit(既定): 185s |
| RAM | 使用~6.5GBで安定、スワップ増ゼロ |
bnb-4bit のピーク91.7GBは96GBカードに収まるが余裕は約4GB。ヘッドルームを優先したい
場合は H3_TE_QUANT=none で旧方式(ピーク83.4GB、+60s/リクエスト)に戻せる。
H3_CACHE、既定 fbc)ComfyUIコミュニティのEasyCache高速化に相当する、diffusers公式の step間キャッシュ
(FirstBlockCache)を H3_CACHE=fbc(既定)で有効化している。ステップ間で最初の
transformerブロックの残差変化が小さいとき、残りの計算をスキップする。
H3_CACHE_THRESHOLD(既定 0.05): 実測でデノイズ 157s→118s(-25%、30step中7スキップ)、
出力はキャッシュ無しと PSNR 31.8〜34.3dB・音声相関0.979 でほぼ同一(目視でも区別困難)。H3_CACHE=none でキャッシュ無しの従来挙動に完全に戻る(バイト一致を回帰確認済み)。MiniMaxH3TransformerBlock はPRブランチの TransformerBlockRegistry に
未登録のため、runner側で TransformerBlockMetadata を登録してから enable_cache() を
呼ぶ(venvのdiffusers本体は無改変)。リクエストごとに _reset_stateful_cache() +
cache_context("h3") で包む(リセット漏れは前リクエストの残差による誤スキップを招く)。
同一seed連続2本のmp4バイト完全一致でリセットの正しさを検証済み。/api/t2va の upscale=1、既定OFF)ComfyUIコミュニティの MiniMaxH3_LatentUpscaler と同系の hires-fix。低解像度(768²)で
前半をデノイズ → x0推定値の映像latentだけを bilinear で空間2x → フレッシュノイズを
scheduler.scale_noise() で再注入 → 残りの低σステップを1536²で仕上げ → デコード。
H3_HIRES_DENOISE(既定0.35)がパス2の担当デノイズ強度。UIはT2VAタブのチェックボックス。
実測(768²→1536²・5秒・30steps・seed=12345、fbc+bnb-4bit):
| 合計 | デノイズ | デコード | ピークVRAM | 出力 | |
|---|---|---|---|---|---|
| upscale=0 | 181s | 125s | 6.5s | 92.1GB | 768² |
| upscale=1 | 645s | 533s (パス1 78s + パス2 455s) | 24.7s | 88.0GB | 1536² |
build_packed_sequence() で
position_ids/token_tags/各indicesを再構築し、row_timestep_plan も残ステップ分を
作り直す。ModularPipeline の _execution_device はコンポーネント登録順の先頭モジュール
で決まるため、TE解放後は components.transformer.device を明示的に使う
(diffusers-server CLAUDE.md 23番・47番と同型の罠)。H3_ATTN_BACKEND、既定 sage)sm_120(Blackwell)向けにソースビルドした SageAttention 2.2.0 を既定で使う
(ビルドは scripts/build_sageattention.sh、約2分。必ず MAX_JOBS=4 NVCC_THREADS=2 +
systemd-run のメモリ上限付きで実行——無制限並列nvccはホストRAM枯渇でシステム巻き添え
事故歴あり。CUDA_HOME=/usr/local/cuda-12.8 の明示が必要、既定のcuda-13.0はtorchの
cu128と不一致)。PyPI/コミュニティのLinux向けsm_120 wheelは存在しなかった(全てWindows)。
H3_ATTN_BACKEND=default で従来のSDPAに戻るH3_CACHE_THRESHOLD=0.1 でデノイズ67s(-43%、リクエスト
~125s。FBC 0.1の構図ドリフト特性は既知どおり)flash_hub/sage_hub)は torch 2.9 向けビルドがHub側に存在せず不成立
(2026-08-05時点。環境の問題ではない)num_inference_steps はAPI/UIパラメータ。30(検証既定)に対し 20で-15%、16で-31% の
デノイズ短縮。16/20とも単フレーム品質・時間方向の安定性に破綻なし(ただし構図は
ステップ数で変わる)。ドラフト用途は16-20、本番は30が目安。ステップを減らすと
FBCのスキップ機会も減る(30stepsで7スキップ→16stepsで0)ため、効果は単純比例しない。
H3_TRANSFORMER_QUANT、既定 none)H3_TRANSFORMER_QUANT=int8 で transformer / transformer_ref を torchao
(Int8WeightOnlyConfig(version=2)、PR #14355ドキュメントのレシピ、torchao 0.17.0)で
int8化する。66.3GB → 34.0GB。品質は目視同等(PSNR 19dBは軌道分岐であり劣化ではない。
int8同士は同一seedでmp4バイト一致の完全決定論)、デノイズは+5s程度(dequantコスト)。
int8時は両transformer同時常駐(34+34+TE-nf4 21=89GB)になり、ref2va⇔t2vaの
変種切替時の66GB級再ロードが消える(初回のref2vaのみ36sのコールドロード)。実測:
| bf16(既定) | int8両常駐 | |
|---|---|---|
| t2va | 175-185s / peak 92.1GB | 177-196s / peak 59.7-91.1GB |
| ref2va(2回目以降) | 523s / peak 87.6GB | 463-471s / peak 74.5GB |
| 変種切替の再ロード | 毎回~26-40s | なし |
フェーズ制御(int8両常駐時): t2vaデノイズ前は transformer_ref 常駐時のみTE強制解放
(89GB定常+activationsでOOMするため。実機確認)、ref2vaはTE強制解放不要になり
デコード窓も transformer_ref 常駐のまま通る。PYTORCH_CUDA_ALLOC_CONF= expandable_segments:True をrunnerが設定(int8ロード/解放サイクルの断片化で
「54GBしか使っていないのに15GB確保失敗」が実機再現したため。diffusers-serverでも
実績のある設定)。既定 none は従来とバイト一致(回帰確認済み)。
H3_LOWVRAM、既定 0)TE-nf4(21GB)+ transformer int8(34GB)= 55GB は48GB級カードでは同時常駐できない
(96GB機の既定・int8両常駐モードいずれも成立しない)。H3_LOWVRAM=1 は
「TEとtransformerを絶対に同時常駐させない」フェーズ循環方式で48GB級に対応する。
強制: H3_TRANSFORMER_QUANT 未指定なら自動で int8 に上書きする(bf16
66.3GBは48GBに単体でも収まらないため)。明示的に H3_TRANSFORMER_QUANT=none を
指定した場合は起動時に RuntimeError で拒否する。H3_TE_QUANT は bnb-4bit
(既定)以外を指定するとやはり起動時に拒否する。H3_TRANSFORMER_BOTH_RESIDENT
(int8両常駐)は無条件で無効化する。
定常状態: リクエスト間は「何も大きいものが常駐しない」(VAEペアのみCPU常駐、 他モードのような transformer/TE の常時居座りが無い)。
t2va/fl2vaのフェーズ: [エントリ: 常駐transformer/transformer_refがあれば解放]
→ TEロード → エンコード(+ fl2vaならkeyframeエンコード)→ layout/latents/
timestepsをTE常駐のまま先に実行(_execution_device解決の罠対策、下記参照)→
TE解放 → transformer(int8)ロード → デノイズ(34GB+活性化5GB≒39GB)→
transformer解放 → VAE→GPU → デコード(~11GB+バッファ)→ VAE→CPU
(transformerは次リクエストのために再ロードしない — 次は encode が先に必要)。
ref2vaのフェーズ: 同じ原則で、参照VAEエンコード → layout/latents/timesteps (ここもTE常駐のまま)→ TE解放 → transformer_ref(int8)ロード → デノイズ → 以下同様。
_execution_device 解決の罠(実装時に発見・修正): パイプラインのコンポーネント
順は text_encoder, tokenizer, processor, vae, scheduler, audio_scheduler, transformer, ...。TEを先に解放してからtransformerをロードする素朴な実装だと、
vae(CPUに退避されていても“存在する”nn.Module)がtext_encoderの次に解決され、
_execution_deviceがcpuに解決されてしまいデノイズの最初のtransformer forward
で RuntimeError: Expected all tensors to be on the same device になる
(実機で再現・特定)。対策として layout/latents/timesteps は TE がまだ
GPU常駐のうちに実行し、それらの出力テンソルが正しいデバイスに確定してから
TEを解放する 順序にした(他モードの force_free_te 遅延パターンと同じ発想)。
ref2vaでは reference_encoder_step も同様にTE常駐のうちに実行する必要がある
(layout_stepが参照の latent 形状に依存するため)。
upscale=1(hires-fix)は非対応: パス2は約4倍の系列長(~16倍のattention活性化
コスト)を要し、このモードの定常余裕(~9GB)では未検証のため、ValueError
(400)で拒否する。
副次的に見つけた既存バグの修正: generate_ref2va() の
_sync_shared_components_to_ref() はTEロードより前に呼ばれていたため、
TEが未ロードの状態(H3_LOWVRAM、またはnone系モードでref2vaが最初のリクエスト
になるケース)では self._pipe_ref.text_encoder に None が同期され、
AttributeError: 'NoneType' object has no attribute 'config' になっていた
(実機で再現・特定)。TEロードの後に呼ぶよう順序を修正した(全モード共通の
修正、H3_LOWVRAM専用ではない)。
正しさの検証: フェーズ循環は計算内容を変えないはずなので、同一seed
(768²・5秒・30steps・キツネのプロンプト、seed=12345)で H3_LOWVRAM=1 H3_TRANSFORMER_QUANT=int8 のt2va出力と通常int8モードのt2va出力を比較したところ
mp4がバイト完全一致(md5一致)することを確認済み。
48GB相当の実機検証(96GB機でダミーVRAM確保により空きを43.5GBへ制限、実48GB
カードの空き47GBより厳しい条件):
| 完走 | ピークVRAM | 内訳(概算) | |
|---|---|---|---|
| t2va 1本目 | ○ | 38.68GB | TEロード |
| t2va 2本目(連続) | ○ | 38.94GB | 同様の固定費が毎回発生(定常状態を持たない設計どおり) |
| ref2va(画像参照1枚) | ○ | 43.84GB | デノイズ283s(参照行分シーケンスが伸びるため39GB台では収まらずやや高め) |
| upscale=1 | - | - | 実装時点でOOMリスクを判断し明示的に400で拒否(未実行) |
いずれもホストRAMスワップの増加なし(free -g で作業前後とも Swap used ~6GB台で
安定)。作業後は H3_LOWVRAM 未指定(完全デフォルト設定、bf16 transformer)で
再起動して同一プロンプトのt2vaを1本実行し、peak_vram_gb: 91.94GB /
cache_skipped_steps: 6 など既存の実測値(本README上部の表・
int8量子化セクション)と同水準であることを確認し、回帰なしと判定した。
量子化済みチェックポイントの事前保存によるロード時間短縮: 未調査
→ TE は 2026-08-08 に実装済み(H3_TE_PREQUANT、既定ON。TEロード 53.0s→29.5s、
リクエスト合計 -35%、MD5一致で等価性確認済み。上記「量子化済み text_encoder の
ディスクキャッシュ」節を参照)。transformer int8 の直列化は未検証のまま —
保存に約34GBを要し、この箱のディスク空き(43GB)では採用自体が非現実的なため。
UIの解像度セレクトに 「任意 (32の倍数へ丸め)」 を追加し、幅/高さを自由に入力できる。 入力値は エラーにせず H3 の規則へ丸める:
app.py の
round_canvas_value)。H3のブロックは32の倍数でないと ValueError を出す仕様
(MINIMAX_H3_CANVAS_MULTIPLE)なので、以前は端数を送ると400になっていた。
ネイティブ範囲(短辺768・最大768×1344)を超える指定はUIが警告を出す(VRAM・品質は未検証)17n+5 へ切り上げ(align_num_frames)。
例: 6.3秒 → 158フレーム(6.58秒)。UIは送信前に実フレーム数と実尺をプレビューする/api/t2va・/api/fl2va に height/width(任意、指定時は resolution
プリセットより優先)を追加。/api/ref2va は元から受け付けるが、こちらも丸めるように変更/api/status の constraints で丸め規則(canvas_multiple/min/max、fps、frame_step/offset)
を公開し、UIは同じ規則でプレビューする(丸めの権威はサーバ側)実測確認: height=700 width=1000 seconds=6.3 で生成 → レスポンス 704×992 / 158フレーム、
出力mp4も ffprobe で 992×704・158フレームと一致。
UI実装の罠: 幅/高さの <input type=number> に min/max/step="32" を付けると、
HTML5の入力検証(stepMismatch/rangeOverflow)が丸める前の端数入力を不正扱いして
フォーム送信自体をブロックする(1024×576のような偶然valid な値だけ通り、1000×700は
何も起きない、という分かりにくい症状になる)。丸める前提の欄には制約属性を付けないこと。
H3_TURBO_LORA、既定 0、2026-08-06 / 2026-08-08 lightx2v 版へ切替)2026-08-08 更新: 既定 LoRA を lightx2v 版へ切り替え、48GB(int8/低VRAM)でも turbo が使えるようになった。
- 既定:
H3_TURBO_LORA_REPO=lightx2v/Minimax-h3-Turbo/H3_TURBO_LORA_FILE=minimax_h3_fl2v_turbo_4step_v0.1.safetensors(DMD蒸留、 Apache 2.0、rank128・312 Linear対象)。既定ステップ数は形式連動 (lightx2v=4 / Ostris に戻すと 8)- int8 対応の理由: キーが diffusers ネイティブ(to_q/to_k/to_v 分離)で
fuse_projections()不要 →torch.catを呼ばない →Int8Tensor非互換を 踏まない。適用関数は形式自動判別(detect_turbo_lora_format、comfy 署名qkv_projを先に見る — Ostris 版もtoken_refiner.キーを持つため順序が重要)- 適用係数(
H3_TURBO_LORA_SCALE、空=形式別の実測既定): lightx2v は 0.094 (Kijai 記載の 0.75 は ComfyUI の alpha 折り込み前提。生の B・A に 0.75 を掛けると 30steps でも完全ノイズ化 — 強度スイープはスパイク節参照)。Ostris は 1.0 のまま (scale=1.0 は恒等なので旧挙動と bit 一致)- E2E 実測(RTX PRO 5000 48GB +
H3_LOWVRAM=1、768²): t2va 4steps 総所要 143s / デノイズ 29s(非turbo 30steps は 351s)。t2i(静止画)×turbo は デノイズ 5.0s / 総所要 94s。本実装経路の出力はスパイク出力と mp4 md5 完全一致。 turbo 連続2本で lowvram の再ロード後の再適用も正常、turbo=0 で通常経路(FBC 有効) へ正しく復帰- 既知の性質: turbo 時は音声レベルが非turbo比で大きめ(rms 0.018-0.042 vs 0.007。白色雑音ではないが強度によっては peak が 1.0 に近づく)
- 併用制限:
H3_LOWVRAM=groupとは形式を問わず併用不可(enable_group_offloadのcpu_param_dictが有効化時点で固定され、後から追加する LoRA バッファが offload サイクルから欠落するリスク — 未検証のまま解禁しない)。comfy 形式 (Ostris)× int8 は従来どおり不可(リポジトリ名で予備判定+適用時に実ファイルの キーで最終判定)。ref2va への適用(transformer_ref)は未検証のまま- v0.1(2026-08-07 公開)のため引き続き既定OFF(UI/リクエストの turbo で opt-in)
以下は Ostris 版 (comfy 形式) 時代の記録(bf16 経路では現在も有効):
H3_TURBO_LORA_REPO=larryvrh/MiniMax-H3-Turbo-Lora で Ostris氏学習中の4/8ステップ
蒸留LoRA(Apache 2.0、rank64・259 Linear対象)を適用し、既定ステップ数を8にする。
プレビュー版LoRA(学習途上)のため既定OFF。完成版が出たら再評価する。
実測(768²/5秒/seed12345): 8steps 87.7s(-46%) で基準30steps(163.5s)に迫る品質、 16steps 98.4sで基準同等、4steps 39.6sは柔らかめだが破綻なし。 コミュニティの「4〜7stepはダメ」はComfyUI標準サンプラーがデュアルスケジュール (video shift12/audio shift3)を扱えないことが原因の可能性が高い——本実装(diffusers PRのscheduler/audio_scheduler分離+手動ループ)では4stepsでも音声破損は起きなかった。 シフト配線は改修不要(12/3はH3基準スケジューラの既定値で、sigma格子が作者リファレンス 実装とビット一致することを確認済み)。
実装メモ: LoRAキーはComfyUI命名のfused-QKV形式のため、attn.fuse_projections() +
ランタイムデルタ(W_eff=W+BA、fuseしない)で適用。罠: fuse_projections() は旧
to_q/k/vを削除せず+12.8GBリークする(明示deleteで対処)。AdaLNが linear.weight を
直接読むためラッパーに weight/bias 等のパススルーが必要。turbo時はFBC自動無効化。
デフォルトのtransformer経路(transformer_quant=none, lowvram=0)以外との組み合わせ
は当初「未検証のため予防的に拒否」だったが、実測でA/B検証した。
H3_HIRES_DENOISE=0.35既定分割)、
peak VRAM 88.09GB——非turbo30stepsの基準645sから大幅短縮。16steps: 総所要331.8s、
pass1=10/pass2=5steps、peak VRAM 88.35GB、8stepsより明確にシャープ。
全フレーム(先頭/中間/末尾)を目視確認し色化け・チェッカーボード崩壊なし、音声も
RMS/peakが正常値(無音・クリップなし)。turbo時はFBCが自動無効化されるため、
hires-fixのFBCブックキーピング(_fbc_last_step_was_skip())はtry/exceptで安全に
no-op化される(懸念だった「turbo未対応のFBC呼び出し」は実害なしと確認)。H3_TRANSFORMER_QUANT=int8、transformer_both_resident
含む): 実測で不可と確定、拒否のまま維持。 apply_turbo_lora() の
attn.fuse_projections() が torch.cat([to_q.weight, to_k.weight, to_v.weight])
を実行するが、int8量子化された to_q/to_k/to_v(H3_INT8_MODULES_TO_NOT_CONVERT
はこれらをスキップしない)は torchao の Int8Tensor であり、aten.cat カーネルが
未実装のため NotImplementedError: Int8Tensor dispatch: attempting to run unimplemented operator/function: func=<OpOverload(op='aten.cat', overload='default')>
で確実に失敗する(HTTP 500、リクエスト単位でクリーンに失敗しVRAMリークなし。
直後の非turbo生成は正常動作を確認)。lowvram=1 は
transformer_quant=int8 を強制するため、上記と全く同じ Int8Tensor/aten.cat
エラーで失敗(同一エラーメッセージを実機確認)。lowvram=group も transformer_quant=int8 前提)で同一エラー。この失敗は
group offloadフックの適用順序とは無関係(sibling project の
「LoRAをenable_group_offload()より前に適用する」という順序修正パターンは
ここでは効かない——fuse_projections()自体がgroup offloadフックを一切介さず
torch.catだけで失敗するため、順序を入れ替えても直らないと判断し深追いしなかった)。16GBバラスト(空き15.5GB)では TEロード(nf4量子化)の終盤でOOM(15.37GB使用時点で
+250MiB要求に失敗)。削除済みTE-nf4の常駐17.45GB自体が床であり、video VAE fp16は
デコード段階の対策のためこの床を動かせない。18GBバラストでは完走
(peak 17.72GB、total 302s)——つまり現アーキの実質下限は~18GB
(24GB級構成 H3_LOWVRAM=group H3_TE_PRUNE=1 がそのまま18GB級でも動く)。
16GB突破にはTEのストリーミング実行か4bit未満の量子化が必要(未着手の別課題)。
H3_VIDEO_VAE_FP16、既定 0)H3_VIDEO_VAE_FP16=1 で video VAE の重みだけを fp16 化する(9.70→4.85GB、デコード
ピーク 16.29→~11.4GB)。audio VAE は絶対に触らない(bf16化で-20dBの既知問題)。
AutoencoderKLMiniMaxH3._keep_in_fp32_modules が encoder/decoder等を
強制fp32に戻すため、from_pretrained(dtype=fp16) は効かない(実機確認)。ロード後に
.to(torch.float16) を明示的に呼ぶ必要があるH3_LOWVRAM=group、2026-08-05追加)H3_LOWVRAM=1(48GB級)は transformer(34GB)を毎リクエスト GPU に丸ごとロードするため、
24〜32GB級カードでは transformer 単体でも収まらない。H3_LOWVRAM=group は
diffusers-server(姉妹プロジェクト)の CLAUDE.md #33/#34/#37 で確立された
「block-level group offload」パターンをこのプロジェクトに移植したもので、transformer を
ホストRAMに常駐させたまま、denoise の各ステップで必要なブロック(50層中1〜2層、
~0.68GB×1〜2)だけを都度 GPU へ出し入れする。transformer はプロセス起動時に一度だけ
ロードされ、リクエストをまたいで常駐し続ける(H3_LOWVRAM=1 のような毎リクエスト
再ロードは発生しない)。
タスク時点で読んだ TorchAoHfQuantizer(quantizers/torchao/torchao_quantizer.py)の
validate_environment() は、device_map に(accelerateの自動割当のような)辞書
形式で "cpu" という文字列値が含まれる場合にのみ self.offload = True を立て、
check_if_quantized_param() はこのフラグが立っていると CPU 配置のパラメータの量子化を
スキップする(=CPUオフロードするパラメータは意図的に非量子化のまま残す設計)。
一方、本実装が使う device_map={"transformer": "cpu"} は load_components() を経由して
最終的に from_pretrained() にプレーン文字列 "cpu" として渡り、
modeling_utils.py の正規化コードにより {"": torch.device("cpu")} という
単一キーの辞書(値は torch.device オブジェクト、文字列ではない)に変換される。
torch.device("cpu") == "cpu" は Python 上で False になるため、
"cpu" in device_map.values() は False のまま保たれ、self.offload は立たない。
つまり CPU上へロードしても量子化はスキップされず、torchaoのInt8Tensorとして
正しく量子化される(scripts/probe_group_offload.py で370/370層がInt8Tensor化
されることを実機確認)。CPU上でのint8量子化は問題なく可能という結論。
_ensure_transformer_group()(core/runner.py)が
device_map={"transformer": "cpu"} + TorchAoConfig(Int8WeightOnlyConfig) で
CPU上に量子化ロードしてから enable_group_offload(offload_type="block_level", num_blocks_per_group=1, use_stream=..., low_cpu_mem_usage=...) を呼ぶ
(transformer_refも同型の _ensure_transformer_ref_group())。H3_LOWVRAM=1 と同じくbnb-4bit必須(起動時強制)。t2vaの定常状態では
TEはリクエストをまたいで常駐する(transformerがそもそも常駐するため、
TEも常駐させておいた方がリクエストごとの再ロードコストを避けられる)。use_stream=True + low_cpu_mem_usage=True の併用はtorchao Int8Tensorに対してバグがあり動かない
scripts/probe_group_offload_forward.py で実際にforwardを走らせたところ、
RuntimeError: cannot pin 'torch.cuda.CharTensor' only dense CPU tensors can be pinned で denoise の最初のブロックで必ず失敗することを実機確認した。
hooks/group_offloading.py の _pinned_memory_tensors()(use_stream=Trueなら
_onload_from_memory() から毎ステップ無条件で呼ばれる)が
low_cpu_mem_usageの値に関わらず .pin_memory() を試みるのに対し、
_init_cpu_param_dict()(enable_group_offload()呼び出し時点で1回だけ実行)は
low_cpu_mem_usage=True なら pin をスキップする、という非対称な実装になっており、
両者の想定が食い違っている。torchaoの Int8Tensor.qdata はこの食い違いが起きると
壊れた状態(内部的に torch.cuda.CharTensor として認識される)でpin_memory()が
呼ばれてクラッシュする。scripts/probe_group_offload_fix.py で対照実験した結果:
| 設定 | 結果 | 1ブロックあたりonload/offload |
|---|---|---|
use_stream=True, low_cpu_mem_usage=True(併用時) | クラッシュ | - |
(2026-08-10 訂正: 当初この組み合わせを「diffusers既定」と記載していたが誤り。apply_group_offloading の API 既定値は use_stream=False, low_cpu_mem_usage=False で、省メモリ目的で両方をオプトインしたときに踏む)
| use_stream=False, low_cpu_mem_usage=True | 動作OK | onload 0.1-0.26s / offload ~0.22s |
| use_stream=True, low_cpu_mem_usage=False | 動作OK | onload 0.04-0.07s / offload ~0s |
low_cpu_mem_usage=False(enable_group_offload()呼び出し時点で全パラメータを
eagerにpin)を新既定に採用した(H3_GROUP_OFFLOAD_LOW_CPU_MEM、既定0=False)。
理由: onloadが4-5倍速い(pinned memoryはページアウト不可でDMA転送が速いため)。
代償はロード時に追加で~14-16GBのホストRAMをpinする(page-lockedなのでスワップ
不可)ことと、enable_group_offload()自体が約22秒かかること(実機測定、
CPU上へのロード70秒 + pin化22秒 = 合計約90秒)。より少ないRAMを優先したい場合は
H3_GROUP_OFFLOAD_LOW_CPU_MEM=1(このときH3_GROUP_OFFLOAD_USE_STREAMも
明示指定しない限り自動で0にフォールバックする、上記の壊れる組み合わせを
避けるため)を明示指定すればよい。
| フェーズ | 常駐する大きいもの | 備考 |
|---|---|---|
| 起動時preload | transformer(int8, CPU常駐+groupoffloadフック) | 約90秒(CPUロード70s+pin化22s) |
| t2va encode | TE-nf4(GPU,21GB) + transformer(CPU) | |
| t2va denoise | TE-nf4(GPU,21GB) + transformerの1-2ブロック(GPU,~1.4GB) | |
| t2va decode | vaeペア(GPU,~11GB) + transformerの1-2ブロック | TEはこの窓だけ強制解放(下記参照)、decode後に再ロード |
| ref2va参照エンコード | vaeペア(GPU,11GB) | TEはこの窓だけ強制解放(下記参照) |
| ref2va denoise | TE-nf4(GPU,21GB) + transformer_refの1-2ブロック | |
| リクエスト間定常 | transformer(CPU) + TE-nf4(GPU) | ref2va後はtransformer_refが未ロードに戻る(t2va↔ref2va切替のたび再ロード) |
当初「group offloadされたtransformerのGPU実消費は極小(~1.4GB)だから、decode時に
transformerを解放する必要は無い」と設計したが、32GBダミーVRAM検証で
CUDA out of memory を実機再現し、_log_gpu_tensor_diag()
(H3_DEBUG_MEM_DIAG=1で有効化する一時診断関数、core/runner.pyに残置)で
実際に生存しているCUDAテンソルを列挙したところ、TE-nf4自身の埋め込みテーブル/
lm_head重み(shape (151936, 5120)、bf16、1.556GB×2 = 3.1GB強を含む合計22.25GB)が
デコード直前まで常駐したままだったことが判明した(empty_cache()だけでは
解放されない、実際に参照されている生きたテンソルだったため)。つまり
TE-nf4(21GB)+ decode専用バッファ(~16.3GB、下記VAEタイル調査参照)=37GBが
真の必要量で、transformerのフットプリントとは無関係にTEとVAEの競合だった。
対策: decode窓(と、ref2vaの参照VAEエンコード窓)でTEを強制解放し、窓を抜けたら
再ロードする(force_free_teとは別枠の専用ロジック、_execution_device解決順序
はTE解放→vaeをGPUへ、の順で安全性を確保)。
同一seed(768²・5秒・30steps・キツネのプロンプト、seed=12345)で、通常int8モード
(H3_LOWVRAM未指定、H3_TRANSFORMER_QUANT=int8、FBC H3_CACHE=fbc有効)の
出力と H3_LOWVRAM=group の出力を比較したところ、FBCのキャッシュスキップ判定が
実行経路の違いで異なった(cache_skipped_stepsが6→0)ため素朴な比較ではmp4が
不一致だった。FBCは前ステップとの残差の類似度という数値的に鋭敏な判定のため、
数学的に等価な演算でも経路が変わればスキップ判定が変わりうる(劣化ではない)。
両モードとも H3_CACHE=none に揃えて再比較したところ、**mp4がバイト完全一致
(md5一致)**することを確認した。group offloadの計算内容は既存経路と数学的に
同一であることの裏付け。
32GB制限(ダミーVRAM確保で空きを~30GBへ制限、H3_CACHE=fbc有効のまま):
| 完走 | ピークVRAM | 所要時間 | |
|---|---|---|---|
| t2va 1本目(768²・5秒・30steps) | ○ | 28.67GB | denoise 220.79s / decode 6.31s / 総計337.19s(TE初回ロード込み) |
| t2va 2本目(連続) | ○ | 28.23GB | denoise 220.85s / decode 6.01s / 総計278.83s(TE常駐のため短縮) |
| ref2va(画像参照1枚、768×1344) | RAM不足で拒否(下記参照) | - | - |
2本とも1本目と完全に同一mp4(md5一致、be3f32a84de074990208ad0d30f31a63)。
ホストRAM/スワップは各フェーズとも増加なし(free -gのSwap usedは作業前後とも
~7-8GB台で安定、この値は他プロセス由来の既存分)。
24GB制限プローブ(ダミーVRAM確保で空きを~22GBへ制限):
H3_LOWVRAM=1的な設計に戻す)か、TE自体をさらに軽量化する(GGUF等、ただし
transformers系モデルへのGGUF適用は構造的に困難)必要がある。本タスクの
スコープ外(48GB→24-32GB対応が目的で24GBは探索的プローブ)のため、
現時点では未対応と結論する。scripts/probe_vae_tile_size.py で768²・124フレームのVAE decodeを単体で
(denoiseを介さず合成潜在から)直接ベンチマークしたところ、
tile_sample_min_height/width を256(既定)→192→128→96まで縮小しても
ピークVRAM(16.29GB)が全く変化しなかった(所要時間はタイル数が増える分
5.9s→10.5sへ悪化)。この結果から、decodeのピークは空間タイルの合成バッファでは
なく、時間チャンク(tokens_chunk_size単位)の1チャンク分をまるごとデコード
するバッファか、固定的なVAEアーキテクチャのオーバーヘッドに支配されていると
推測される(VAEクラスは時間方向のチャンクサイズを公開パラメータとして持たない
ため、これ以上の調整はコード変更が必要で本タスクの範囲外)。
結論: 24-32GB級対応において空間タイルサイズの調整は無意味(既定のままでよい)。
全てのballast検証(32GB・24GB双方)を通じて H3_CACHE=fbc(既定)・
H3_ATTN_BACKEND=sage(既定)を有効にしたまま実行し、group offloadのフックと
競合するエラーは一切発生しなかった(FBCはブロック単位の計算スキップ判定、
group offloadはブロックのGPU常駐管理、sageは各ブロック内部のattention実装、と
三者は独立したレイヤーで動作するため)。MD5一致チェック(H3_CACHE=noneで
実施)とは別に、既定のFBC有効設定での通し実行(32GB制限のt2va 2本)が完走した
ことも確認済み。
H3_LOWVRAM=group でt2va実行後にref2vaを呼ぶと、VRAM予算に関わらず(96GB機で
ダミーVRAM確保無しでも再現)ホストRAMガードで拒否されることを実機確認した:
H3_LOWVRAM=group requires at least 40.0GB of available host RAM before loading
the (~34GB, permanently CPU-resident) int8 transformer, but only 33.0GB is
available right now.
原因の切り分け: transformer解放直後はavail_gbが正しく回復する(44.6GB前後)が、
その後のTE再ロード→参照VAEエンコード→レイアウト計算の過程でavail_gbが
33GB前後まで下がる(実機ログで確認)。swap_used_gbは一貫して増加しないため
実際のスワップ発生は無い ─ MemAvailable(Linuxのbuff/cache込みヒューリスティック
推定値)の変動が、真の空きRAMより保守的に振れている可能性が高い。ただし
free -gのusedベースで見ても94GB中62GB使用(32GB残)という状況で
追加34GBの確保は本質的にタイトであり、ガード自体が誤りとは断定できなかった
(96GB機でも「t2va transformerを常駐させたままref2va用transformer_refをさらに
CPUへpinしようとする」設計そのものが、94GB RAM機の物理容量に対してすでに
ギリギリ)。安全側に倒し、ガードを緩めることはせず既知の制限として記録する
(過去のスワップ暴走事故の教訓を優先)。将来の改善候補: preload_all()での
transformer即時ロードをやめてTEと同様に遅延ロード化する(初回リクエストの
レイテンシとのトレードオフ)、またはt2va⇔ref2va切替時によりRAMを消費しない
経路を設計する。RAM 48GB以上のマシンでは(未検証だがRAM予算に余裕があるため)
この問題は起きない可能性が高い(本タスクは94GB機でのみ検証、より多いRAM
搭載機での追試は未実施)。
H3_TE_PRUNE、既定 0、2026-08-06追加)MiniMax-H3 の text_encoder(Qwen3-VL-32B、64層)は
hidden_states[MINIMAX_H3_TEXT_ENCODER_LAYER](=50)しか読まない
(diffusers/modular_pipelines/minimax_h3/encoders.py)。H3_TE_PRUNE=1 は
text_encoder を 51層だけ(0〜50、MINIMAX_H3_TEXT_ENCODER_LAYER + 1)で構築し、
未使用の52〜64層目 + 最終norm + lm_head(重み換算で14層分、bnb-4bit実測 ~3.6GB /
bf16実測 ~13.6GB)を一度もロードしない。既定 0 は完全無変更(この分岐自体が
一切呼ばれない)。
hidden_states[k] の意味は transformers の _can_record_outputs = {"hidden_states": Qwen3VLTextDecoderLayer} フック機構(output_capturing.py)により決まる:
hidden_states[0] = 埋め込み出力(layer 0 の入力を捕捉)、hidden_states[k]
(k=1..num_hidden_layers) = layers[k-1] の出力。つまり hidden_states[50] =
layers[49] の出力で、layers0..49を実行すれば十分なはずだった。
しかし num_hidden_layers をちょうど50に切り詰めると、hidden_states[50] が
捕捉タプルの最後の要素になってしまい、Qwen3VLTextModel.forward を包む
@capture_outputs(tie_last_hidden_states=True)(既定)が「最後の要素を
outputs.last_hidden_state(=最終norm適用後の値)で強制的に上書きする」という
挙動を発動させる。実機検証(scripts/probe_te_prune*.py)で、50層ちょうどに
切り詰めた場合の hidden_states[50] は本来の(64層モデルの)値と桁違いに
異なる(max abs diff ~1.5e4、量子化誤差の水準ではなく完全に別の値)ことを確認した。
これはまさに encoders.py 自身のガード
(if num_layers <= MINIMAX_H3_TEXT_ENCODER_LAYER: raise ValueError(...))が
警告している「ちょうど50層に切り詰めた最終隠れ状態はpost-normであり、MiniMax-H3が
期待する値ではない」という事態そのもの(このガードのおかげで50層ちょうどの誤設定は
encode_prompt() 経由なら例外で弾かれる)。51層(layers[50]は実行されるが
その出力は読まれない、無駄な1層分の計算コストのみ)にすることで
hidden_states[50] が捕捉タプルの中間に位置するようになり、上書きを回避できる。
51層版で64層版の hidden_states[50] と完全一致(torch.equal、bf16・bnb-4bit
nf4とも)することを実機確認済み。
core/runner.py の _text_encoder_config_kwargs() が、text_encoder の
ComponentSpec(pretrained_model_name_or_path="MiniMaxAI/MiniMax-H3",
subfolder="text_encoder")と同じ場所から Qwen3VLConfig を個別ロードし、
text_config.num_hidden_layers = 51 に書き換えたオブジェクトを
load_components(..., config={"text_encoder": pruned_config}) として渡す。
PreTrainedModel.from_pretrained は config が既に PreTrainedConfig インスタンス
なら自前のconfig自動ロードをスキップしてそのまま使う(modeling_utils.pyで確認)。
チェックポイントの layers.51-63.* は from_pretrained のロードレポートに
UNEXPECTED として現れ、単純に無視される(構築されないため一切のVRAM/RAMを消費
しない)。vision tower(model.visual)は無変更(fl2va のキーフレーム/ref2va の
参照画像・動画のpixel_valuesがここを通るため、削除対象から明示的に除外)。
H3_TE_QUANT(bnb-4bit/none)・H3_LOWVRAM(0/1/group)のどの組み合わせとも合成可能。
| 精度 | 削除前 | 削除後(51層) | 削減 |
|---|---|---|---|
| bnb-4bit nf4 | 21.02GB | 17.45GB | -3.57GB (-17%) |
| bf16 | 66.71GB | 53.06GB | -13.65GB (-20%) |
nf4は量子化で1層あたりのサイズがbf16の約1/4に圧縮されるため、削減の絶対量もbf16より 小さい(相対削減率はほぼ同じ)。
同一seed(768²・5秒・30steps・キツネのプロンプト、seed=12345、H3_CACHE=noneで
FBCの経路依存を排除)で、H3_TE_PRUNE=0(削除なし)とH3_TE_PRUNE=1(削除あり)の
出力を比較したところ、**t2va・ref2va(画像参照1枚、vision tower経由)とも
mp4がバイト完全一致(md5一致)**した。削除が数学的に無影響であることの実証。
H3_LOWVRAM=1・H3_LOWVRAM=groupの各モードでも、削除の有無で出力が完全一致する
ことを確認済み(後述)。
H3_LOWVRAM_GROUP側に追加修正)24〜32GB級対応の既存機構(H3_LOWVRAM=group)は、TE-nf4(削除前21GB)を
denoise中も常駐させたままにする設計だった(group offloadされたtransformerの
実消費が~1.4GBと小さいため、32GB級では問題にならなかった)。削除後のTE(17.45GB)は
それでもまだ大きく、22GB制限で実機OOMを再現した(denoise開始直後、
21.73GB使用中に224MB要求で失敗)。24GB制限でも同様にOOM(23.12GB使用中に
1.16GB要求で失敗、step 1で発生)。
対策として H3_LOWVRAM_GROUP かつ H3_TE_PRUNE=1 の場合に限り、
H3_LOWVRAM=1と同じ「denoiseループの間だけTEを強制解放し、decode窓の前後で
リロードする」選択法を追加した(core/runner.pyのgroup_free_te_for_denoise
フラグ)。解放位置はlayout_step/latents_step/timesteps_stepの後(既存の
force_free_teと同じ理由: これらのステップの出力は既にテンソルとして
stateに載っているため、_execution_device解決には以後一切影響しない)。
H3_TE_PRUNE=0(既定)のH3_LOWVRAM_GROUPは完全無変更(このフラグは
H3_LOWVRAM_GROUP and H3_TE_PRUNEの両方が真のときのみ真になる)。
修正後、22GB/24GB/20GBいずれのVRAM制限でも実機で完走を確認した:
| VRAM制限 | 結果 | ピークVRAM(reset後の計測) | 総所要時間 |
|---|---|---|---|
| 22GB(修正前、削除のみ) | OOM(denoise開始直後、21.73GB使用中に224MB要求) | - | - |
| 24GB(修正前、削除のみ) | OOM(step 1、23.12GB使用中に1.16GB要求) | - | - |
| 24GB(修正後、1本目) | ○ | 17.72GB | 321.7s(TE初回ロード込み) |
| 24GB(修正後、2本目・連続) | ○ | 18.68GB | 277.7s(TE常駐のため短縮) |
| 20GB(修正後) | ○ | 17.72GB | 320.3s |
24GB×2本・20GB×1本の出力mp4はすべてバイト完全一致(md5一致、通常int8モード
(H3_LOWVRAM未指定)の出力とも一致)。group offloadの計算内容が
VRAM予算に関わらず数学的に同一であることの裏付け(既存の32GB/24GB検証結果と同じ
結論)。ホストRAM/スワップは各テストの前後で増加なし(free -hのSwap usedは
一貫して~7.9GB台、既存分のまま)。
H3_LOWVRAM=1(48GB級)でのTEロード時間短縮削除により、H3_LOWVRAM=1が毎リクエスト支払うTEロード固定費が短縮される
(実機、ダミーVRAM確保で空きを~43.5GBに制限):
| TEロード時間 | TEサイズ | |
|---|---|---|
| 削除なし | 42.3s | 21.01GB |
| 削除あり | 35.0s(-17%) | 17.44GB |
出力mp4は削除の有無でバイト完全一致(md5一致)。
H3_TE_PRUNE未指定(既定0)の状態で同一条件のt2vaを実行し、この機能追加前に
取得していた基準mp4とバイト完全一致(md5一致)することを確認した。既定動作は
完全無変更。
/api/ref2va)順序付きの参照素材(画像最大9・動画最大3・音声最大3、合計12。音声単独は不可)から
動画+音声を生成する。参照の順序はプロンプト内ラベル(<Picture i> 等)とrotary配置に
対応するため意味を持つ。動画参照はサウンドトラックも条件付けに使われる。
参照が音声ちょうど1本のときは秒数省略可(音声の長さが生成尺になる。APIでは
seconds=0)。出力キャンバスは参照に縛られず、未指定なら16:9(1344×768)。
transformer_ref/(61.7GB、クラス/configはtransformerと同一で
重みのみ別) を使う。t2va/fl2va用transformerとは同時常駐不可のため、runnerは
変種切替(アクティブな片方だけ常駐、解放→再ロード)で管理する(/api/status の
active_variant)。TE-nf4・VAE類・processorは両変種で共有。venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
起動時に transformer と TE(既定では NF4量子化してGPU常駐)をプリロードする
(H3_TE_QUANT=none の場合は旧方式: transformer/VAEを常駐させ、TEは各リクエストの
たびにロード/解放)。ブラウザで http://<host>:8611/ を開く。
現在の箱 (48GB + 20GB、2026-08-07 のGPU交換後) では既定モードはロードできない。 起動は低VRAMモード必須:
CUDA_VISIBLE_DEVICES=0 H3_LOWVRAM=1 venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611(
CUDA_VISIBLE_DEVICES=0は 48GB の RTX PRO 5000 を cuda:0 に固定するため。 20GB の RTX 4000 SFF Ada は、バラスト検証上の床 ~18GB に対して余裕が2GBしかなく、 かつ sm_89 なので sm_120 向けにビルドした SageAttention も使えない — H3 用途は未検証・ 非推奨)
venv/bin/python scripts/probe_t2va.py
outputs/probe_t2va.mp4 と outputs/probe_report.json (ロード時間・生成時間・
ピークVRAM等)を出力する。
POST /api/t2va (multipart/form-data): prompt, resolution
(768x768|768x1344|1344x768), seconds(5〜15), num_inference_steps, seedPOST /api/fl2va: 上記に加え image / last_image(どちらか一方以上)POST /api/t2i: 静止画モード。seconds の代わりに frames(22既定|5)。
超短尺mp4と中央フレームPNGの両方を保存(「静止画モード」セクション参照)POST /api/t2i_batch: 静止画のバッチ生成。prompts を場面数ぶん繰り返し送る
(最大24)。低VRAMモードのロード固定費をバッチ全体で1回に償却
(「静止画のバッチ生成」セクション参照)POST /api/ref2va + still=1 + frames: 参照付き静止画 (ref2i)。中央フレームPNGを保存POST /api/ref2i_batch: 参照付き静止画のバッチ生成。共通 references +
prompts(最大24)。キャラクター一貫の場面静止画の連番生成
(「参照付き静止画」セクション参照)POST /api/ref2va_batch: 参照付き動画のバッチ生成。共通 references +
prompts + seconds(全場面共通・必須)。物語の各場面動画の連番生成
(「参照付き動画のバッチ生成」セクション参照)GET /api/status: ロード状態・VRAM/RAMGET /api/progress: 生成中の進捗ポーリング用モード・量子化・turbo・TE配置の組み合わせが増えたため、**「この設定のこの局面で GPU に 何が載っているか」**を一枚で引ける表を docs/RESIDENCY.md に置いた。 ピークVRAMの内訳を誤って説明した実例(デノイズ時のピークをデコード時と取り違えた)も 「よくある誤解」として記録してある。
本 README は「どう使うか」の運用ドキュメント。技術面は目的別に2本ある。
| 文書 | 内容 | 読者 |
|---|---|---|
| docs/TECHNICAL_OVERVIEW.md | 技術概要: 機能、アーキテクチャ、各種方式の統合、VRAM容量別の扱い、性能、設定リファレンス | このアプリが何をどう実現しているかを知りたい人 |
| docs/internal/TECHNICAL_REPORT.md | 内部資料(作業記録): 2026-08-04〜08-08 の全作業。設計判断の背景、実機で踏んだ16件の罠とその解決、検証手法 | 同じ罠を踏みたくない開発者 |
技術レポートの方はバグ・失敗・回り道まで含む経過の記録なので、仕様や性能を知りたい だけなら技術概要を読めばよい。
ComfyUI コミュニティ等で出た改良を本アプリ(diffusers 経路)へ取り込んだ作業の記録は docs/COMMUNITY_IMPROVEMENTS.md にまとめてある (取り込んだもの / 調査の結果取り込まなかったもの / 着想を得て自前実装したもの、 それぞれの出典・実測値・判定・踏んだ罠)。
環境変数でしか変えられなかったオプトイン設定を、性質で2つに分けてUIから操作できる。
FirstBlockCache(+しきい値)・Sage Attention・Turbo LoRA。リクエストごとのパラメータ
(cache / cache_threshold / attn / turbo)として送り、MiniMaxH3Runner. apply_instant_settings() が生成ロック取得後・denoise前に常駐transformerへ適用する
(disable_cache()/enable_cache()、set_attention_backend()、_TurboLoRALinear.enabled)。
未指定なら従来どおりプロセス既定なので既存のcurl/スクリプトは無変更で動く。
turbo有効時はFBCを自動的に無効化する(元の安全規則を踏襲)。
実測(同一seed、再起動なしで切替): FBC on 100.8s(6スキップ)/ off 129.3s(0スキップ)、 Sage on 129.3s / off(native) 158.5s、Turbo on(8steps) 38.9s。
transformer int8・TE量子化・TEレイヤー削除・低VRAMモード・video VAE fp16。
POST /api/settings/apply が core/settings.py の apply_reload_settings() を呼び、
プロセスは再起動せず runner内で全モデルを解放→新設定でロードし直す
(自プロセスをkillすると誰も起動し直せずUIごと復帰不能になるため、
os.execv/self-kill の類は実装しない)。生成中は409、未検証の組み合わせは400。
実測: transformer_quant none→int8→none が 56.0s / 55.0s(GPU 87.5→55.0→87.3GB)、
lowvram 0→1→0 も往復動作。GET /api/settings が現在値と選択肢を返し、UIはこれで初期化する。
UI実装の注意: チェックボックスOFFは空文字ではなく明示的に turbo=0 を送ること
(空文字は「未指定=サーバ既定」と解釈されうるため、H3_TURBO_LORA=1 で起動した
サーバではチェックを外しても無効化されない恐れがある)。turboとupscaleは相互排他で、
片方を選ぶともう片方が自動的に解除・無効化される(サーバ側の400と整合)。
結果表示の下に outputs/ 直下の mp4 をタイル表示する。サムネイルはサーバで生成せず
<video preload="metadata" src="....mp4#t=0.1"> に先頭フレームを描かせる(依存追加なし。
Ref2VAの参照タイルと同じ手法)。
GET /api/outputs: **直下の .mp4 と .png(静止画モードの生成物。type フィールド
"video"/"image" で区別。outputs/ab_* 等の検証資料は対象外)。
尺/解像度は ffprobe で取得し mtime+size をキーにメモリキャッシュPOST /api/outputs/delete: パストラバーサル対策(/・\・.. を拒否し、
resolve 後に outputs/ 直下であることを検証=シンボリックリンク経由の脱出も遮断)。
UIは confirm() 必須。実機で ../app.py /etc/passwd ab_*/... 等が400になることを確認済みPOST /api/outputs/concat: 連結順は「チェックした順」(表示順は新しい順)。
全入力のパラメータが一致すれば concat demuxer + -c copy(再エンコードなし=劣化ゼロ)、
不一致なら filter_complex で先頭動画の解像度へ揃えて再エンコード(無音入力には
anullsrc を合成)。2本未満は400、連結同士の同時実行は409(GPUを使わないので
generation_lock は取らない)。PNG(静止画)は連結対象外(ffprobe が PNG も
video stream として読めてしまうため、拡張子で明示的に400にする。UI側も選択に
PNG が混ざると連結ボタンを無効化する)依存に関する注意: このアプリの生成物のmuxは PyAV(av.open() で libx264+aac を
直接書き出す、core/runner.py の _mux_mp4())で行っており、ffmpeg コマンドは使って
いなかった。ギャラリーの ffprobe/ffmpeg 呼び出しがこのアプリで初めての外部コマンド
依存になる(この環境には /usr/bin/ffmpeg があり、不在時は FileNotFoundError を
捕捉して明示的にエラーを返す)。外部コマンド依存を無くしたい場合は「同一パラメータ限定で
PyAVによるパケット再多重化(=-c copy 相当)にし、混在時はエラーにする」のが現実的な代替
(パラメータ混在の再エンコードまでPyAVで実装するのは実質ffmpegの再実装になるため非推奨)。
UI実装の注意: 選択のたびに全タイルを作り直すと <video> が全数再生成されて
ちらつく(95枚で顕著)。選択状態はバッジ/チェックの差分更新にすること
(updateGallerySelectionUI()。一覧の再構築は /api/outputs 取得時のみ)。
ローカルLLM(gemma4-31B)接続下で h3-official モードを実測した結果。
構造適合: 生成されたプロンプトは公式仕様に完全準拠していた。
integrated_multimodal_description / overall_soundscape /
non_diegetic_music)、[Shot 1] は時刻なし、[Shot 2] At 00:05.000 の3桁記法、
台詞は <d>[Japanese] おかえり</d> と原語で保持、話者ID (S1) も付与。応答6.2秒<Subject n>/<Picture n>/<Audio n> のラベルと
fully_preserved / fully_copy 等の関係マーカーも使用。応答12.8秒カット位置の実測(768²・10秒・turbo 8steps、LLM生成プロンプト、各2seed。
ffmpeg のシーン検出 gt(scene,0.15)):
| 記法 | 指示 | 検出(seed 12345 / 777) | ズレ |
|---|---|---|---|
公式 [Shot 2] At 00:05.000 | 5.000秒 | 4.875 / 4.875 | -0.125秒(両seedで同一) |
独自 CUT 2 [6-10秒] | 6.0秒 | 6.083 / 6.333 | +0.083 / +0.333秒 |
公式記法の方がばらつきが小さい(2seedで完全同一)。ただし独自CUT記法も0.1〜0.3秒の ズレに収まっており、当初の手書きプロンプトによる1試行(独自記法+1.0秒)ほどの差はない。 どちらも実用範囲で、公式記法は再現性が高い、というのが実測の結論。
実装上の罠(実機で再現・修正): lang=ja を指定しても英語で返ってくる。
言語指示をシステムプロンプトの冒頭に置くと、その後ろに続く15.8KBの英語リファレンス
(英語出力の実例が並ぶ)に上書きされるため。言語指示はリファレンス本文より後ろ、
最終指示として置くことで解消した(diffusers-server の Tポーズ実装で確立した
「プロンプトは末尾が最強」と同じ原理)。修正後は地の文だけが日本語になり、
フィールド名・[Shot n]・タイムコード・<d> タグ内の台詞は英語/原語のまま維持される。
「LLMの性能限界なのか、プロンプトで直るのか」を推測で議論しないために、まず故障率を
実測し(scripts/probe_h3official_compliance.py、5入力×3回)、その結果に基づいて
対策した。LLM は gemma4-31B Q4_K_M / llama.cpp、n_ctx=7680。
| 故障クラス | 発生率 |
|---|---|
構造・記法(フィールド名・[Shot n]・タイムコード・<d>タグ・話者ID) | 0/15 (0%) |
| 時間配分 | 6/15 (40%) |
| 文脈溢れ | 0/15(余裕 2,900+ トークン) |
「入力が変われば別の問題が出るのでは」という懸念は良い方に外れた — 違反は散らばらず 時間配分の1クラスに集中していた。しかもその6件は性質が2つに分かれる:
文脈: t2va は system 4,397 tok / 7,680 で余裕あり。ref2va は 6,191 tok (81%) で余裕 約1,100 トークンしかない(実測では6フィールド揃って正常終了したが、入力が長い場合は要注意)。
core/prompt_check.py): 決定的に判定できる規則を実装
(F1 3フィールド / F2 先頭ショットに時刻なし / F3 カット時刻の厳密増加 / F4 尺内 /
F5 ショット尺の下限 / F6 <d>タグと言語タグ / F7 台詞がショットに収まる /
F8 話者ID)。F5・F7 は公式仕様に無い本アプリの実用規則 — 公式は「カット時刻が
尺内」としか言わないため、5秒尺で4.5秒にカットして最終ショット0.5秒、という
字義どおりだが使えない出力を許してしまう(実際に発生)。意味的整合性(1ショット内で
画角が混在する等)は近似ルールで警告にとどめる。手書きプロンプトにも効くlang=ja 修正時に後方ブロックを持っていた)。既定は lang=en なので、
尺の制約を含む全指示が15.8KBのガイドに埋もれていた。両版の末尾に時間配分6ルール
(ショット尺の下限・台詞の発話時間見積り・ショット数の目安・1ショット1構図・
画面外音声の定型句・話者ID)を追加core/llm.py の enhance_prompt_checked): 違反を検出したら
その内容を日本語で突きつけて再生成(最大2回、H3_OFFICIAL_MAX_REPAIRS)。
違反が増える修復案は破棄する(修復が別の箇所を壊す事故の防止)。
入力の実現不可能性はLLMに投げる前に判定し InfeasibleInputError → 400 + 助言| clean | 入力不可を検出 | 違反が残存 | |
|---|---|---|---|
| ベースライン | 9/15 | 0 | 6/15 |
| 改善後 | 12/15 | 3/15(正しい挙動) | 0/15 |
未解決の違反がゼロになった。 全ケースが「妥当なプロンプトを返す」か「実行不能な理由を 助言付きで返す」かのいずれかになる。所要時間の中央値は 8.5s → 9.2s(+8%)で、修復ループは 必要なときだけ発動する(発動時は約2倍の21秒)。
API は /api/prompt/enhance の h3-official 応答に violations / warnings /
check_report / attempts / repaired を追加。UI は残った指摘をステータス欄に表示する
(生成はブロックしない — プロンプトは編集可能なので人間が最終ゲート)。
H3_TE_PREQUANT、既定 1、2026-08-08)H3_LOWVRAM=1 は毎リクエストで TE を再ロードするため、その時間がそのまま固定費になる。
この時間の大半は「元の bf16 重みを読む + その場で bnb-4bit へ量子化する」処理なので、
量子化後の重みを一度保存しておけば次回以降は読むだけで済む。初回ロード時に自動保存し、
以降はそこから読む。
実測(RTX PRO 5000 48GB + H3_LOWVRAM=1 H3_TE_PRUNE=1、t2i turbo 4steps を各4回):
| TE ロード | リクエスト合計 | |
|---|---|---|
| キャッシュ無効(従来) | 46.5〜55.8s(平均 53.0s) | 108.7〜150.0s(平均 128.6s) |
| キャッシュ有効(既定) | 21.1〜34.3s(平均 29.5s) | 75.3〜90.6s(平均 83.2s) |
| 差 | -23.5s(1.8倍) | -45.4s(-35%) |
等価性: 同一 seed の生成物が PNG で MD5 完全一致(seed 3・4 で確認)。加えて
プローブ(scripts/probe_prequant_equivalence.py)で hidden_states[50] が
torch.equal でビット一致(max_abs_diff 0.0)することも確認済み。bnb-4bit の量子化は
決定的なので当然の結果だが、速いだけで採用しないのが本プロジェクトの流儀。
models/prequant/te_<quant>_prune<0|1>/(.gitignore 済み)。
設定ごとに別ディレクトリにしてある — TE_QUANT / TE_PRUNE を変えると重みの中身が
別物になるため、同じ場所を使い回すと設定切替時に古い重みを読む事故が起きるH3_TE_PRUNE=1)/ 未削除なら ~21GBH3_TE_PREQUANT=0 で完全に無効化(導入前と同一挙動)。空きディスクが
H3_TE_PREQUANT_MIN_FREE_GB(既定 25GB)を下回る場合は保存をスキップして生成は続行する
(キャッシュは高速化であって機能ではない)。保存は一時ディレクトリへ書いてから rename
するので、途中で落ちても中途半端なキャッシュが「有効」に見えることはない測定上の注意(この検証で踏んだ罠): 保存直後に測ると 17.44GB がページキャッシュに 載っているため 2.6秒という非現実的な値が出る。実運用ではリクエスト間に transformer (34GB)をディスクから読むため TE のページキャッシュが押し出され、実測は 21〜34秒に 落ち着く(リクエストを重ねるほど遅くなり 34秒前後で頭打ち)。それでも従来経路の 46〜56秒より速いが、プローブの数値をそのまま性能として報告してはいけない。
H3_TE_DEVICE、既定 ``、2026-08-09)H3_LOWVRAM=1 が毎リクエストで TE を再ロードする根本原因は、デノイズ中に TE の
置き場所が無いことにある(48GB では transformer-int8 34GB + 活性化 5GB = 39GB で、
残り 9GB に TE の 17.45GB は入らない)。TE を2枚目のGPUへ逃がせばこの再ロードが消える。
# 例: TE を cuda:1 に常駐させる (CUDA_VISIBLE_DEVICES は設定しない = 両GPUを見せる)
H3_LOWVRAM=1 H3_TE_PRUNE=1 H3_TE_DEVICE=cuda:1 venv/bin/python -m uvicorn app:app --port 8611
実測(RTX PRO 5000 48GB + RTX 4000 SFF Ada 20GB、t2i turbo 4steps、768²):
| 1回目 | 定常(2回目以降) | |
|---|---|---|
| 従来(TE も cuda:0) | 81.3s | 67.6〜86.2s(平均 78.4s) |
| TE を cuda:1 へ | 88.2s(TE初回ロード込み) | 33.6〜36.7s(約35s、-55%) |
TE のロードはプロセス起動後1回だけ。GPU1 に 17GB 常駐したまま、GPU0 はアイドル時 2.3GB まで下がる。
出力は従来構成とビット一致しない(PNG の MD5 が異なる)。原因は sm_120(Blackwell)
と sm_89(Ada)のアーキテクチャ差による丸めで、実測した hidden_states[50] の
相対RMS差は 0.084%(max_abs_diff 3.5、値のRMS 57.4)。これは既に採用済みの
参照プレフィックス共有の差(1.5%)より20倍小さく、バグの水準(ネガティブ
コントロール 27-30%)とは桁が違う。Sage Attention や int8 と同種の軌道ドリフトであり、
目視でも品質は同等。ビット再現が要る対照実験では H3_TE_DEVICE を外すこと。
_execution_device の罠、再び)_execution_device は components 順(text_encoder, tokenizer, processor, vae, ...)で
最初の nn.Module のデバイスを返す(実装を読んで確認)。TE を cuda:1 に置くと
これが cuda:1 を返し、layout/latents/timesteps も decode も別GPUにテンソルを作る。
最初は「layout の窓だけ塞ぐ」実装にしたが、decode でも発火した
(latents = latents * latents_std + latents_mean が
Expected all tensors to be on the same device, cuda:0 and cuda:1 で失敗、実機で再現)。
窓を個別に塞ぐ方式は漏れるため、「既定では TE をパイプから外しておき、エンコード中
だけ繋ぐ」(_te_attached())という反転した設計に変更した。これなら新しい経路が
増えても安全側に倒れる。モジュール実体は self._te_module が保持し続けるので、
外しても解放されない。
layout/latents/timesteps の窓ではさらに _pin_execution_device_to_compute() で
vae も一時的に外す(TE を外した後の次の nn.Module が CPU 上の vae になり、今度は
_execution_device が cpu を返してしまうため。transformer が最初に見つかるようにする)。
/api/t2i、2026-08-07 本実装)H3を「超短尺動画→静止画取り出し」で画像生成の代用にするモード。前段の超短尺プローブ (下記)の結論をそのまま本実装した。価値は「H3と画風が完全に一致する静止画」を FL2VA の先頭フレームや Ref2VA の参照画像として作れること(速度は固定費支配のため 専用T2Iモデルには勝てない)。
POST /api/t2i — prompt / frames(22既定 or 5)/ resolution or
height+width / num_inference_steps / seed / 即反映パラメータ
(cache/cache_threshold/attn/turbo)。レスポンスは超短尺 mp4 と中央フレームの PNG
(image_url、t2i_<ts>.png)の両方core/runner.py):
generate(still=True, still_frames=...) — diffusers 側の最小尺5秒バリデーション
(MINIMAX_H3_MIN_DURATION)は MiniMaxH3SetupStep の呼び出し1回分だけ
_relaxed_min_duration() で緩和(生成は generation_lock で直列なので漏れない)H3_VAE_SMALLCLIP_FIX、既定1):
AutoencoderKLMiniMaxH3._decode() は潜在1-2フレームで num_chunks=0 →
torch.cat([]) で落ちる上流バグがある(プローブで実機再現)。runner 側の
monkeypatch で「1チャンク未満なら全トークンを単一 _decode_clip() で復号し、
frame_pre_padding(3) とパディング末尾を切り落とす」分岐を追加。潜在2フレーム
×4 − 3 = 5ピクセルフレームで幾何が一致する。通常動画 (num_chunks>=1) は元実装へ
そのまま委譲するため影響なし(venv の diffusers 本体は無変更)generate()/generate_ref2va() の decode 部を
try/except 化し、例外時も _restore_decode_steady_state()(VAE を CPU へ・
モードごとの transformer/TE 再ロード)を実行してから再送出する。
実機検証済み (2026-08-07): 一回限りの失敗注入フック H3_DEBUG_FAIL_DECODE=1
(os.environ.pop で読むため同一プロセスの次リクエストから通常動作)で decode を
故意に失敗させ、500 応答後に GPU が ~2GB まで解放され、同一プロセスの次の
リクエストが正常に 200 を返すことを確認した実測 (2026-08-07、RTX PRO 5000 48GB + H3_LOWVRAM=1、768²・30steps・fbc+sage・
seed 12345):
| frames | デノイズ | デコード | ピークVRAM | 合計(毎リクエストの再ロード込み) | 品質(目視) |
|---|---|---|---|---|---|
| 22 (0.917s) | 29.0s (1.0s/step) | 1.8s | 35.0GB | 157s | 破綻なし・高品質 |
| 5 (0.208s) | 9.1s | 0.67s | 35.2GB | 125s | 破綻なし(史上初の実デコード成功) |
5フレームの音声 (0.208s) も非無音 (rms 0.055)。
「常駐モード (H3_LOWVRAM=group) なら再ロード固定費が消えて連続生成が速いはず」という
仮説を、物語3場面のプロンプト (frames=22・768²・30steps・seed固定) の3連続実行で実測した
(2026-08-07、RTX PRO 5000 48GB)。結果は逆で、lowvram=1 より約1.5倍遅い:
| モード | 1枚目 | 定常 (2枚目以降) | 定常の内訳 |
|---|---|---|---|
H3_LOWVRAM=1 | 157s | ~157s | TE+transformerロード ~120s + デノイズ 29s + デコード 2s |
H3_LOWVRAM=group | 310s (TEコールド込み) | ~240s | デノイズ 130s + デコード 2s + デコード後のTE再ロード 75-97s |
H3_LOWVRAM=1 のまま回すのが最速
(~157s/枚)。さらに縮めるのは下の /api/t2i_batch(この実測を受けて実装済み)/api/t2i_batch、2026-08-07 本実装)上の実測を受けて、H3_LOWVRAM=1 の固定費 (~110s: TEロード75-97s + transformerロード
~20-35s) をバッチ全体で1回に償却するエンドポイントを実装した
(core/runner.py の generate_still_batch())。generate() の lowvram choreography を
位相順に並べ替える:
entry : [nothing big resident]
encode : [TE-nf4] 全場面の setup/エンコード/layout/latents/timesteps
denoise : [transformer] 全場面を順にデノイズ
decode : [vae pair] 全場面を順にデコード → PNG/mp4 保存(場面ごとに保存
しながら進むので途中失敗でも完了分は残る)
POST /api/t2i_batch — multipart で prompts を場面数ぶん繰り返し送る
(最大24場面)。frames/resolution/num_inference_steps/seed/即反映パラメータは
全場面共通(変えられるのはプロンプトのみ)。UI は T2I タブの
「バッチ連続生成(1行=1場面)」チェックで、プロンプト欄の1行を1場面として送る_step_index = None に
戻すだけでよい(MiniMaxH3Scheduler.step() は index を timestep 値から再導出する)。
FirstBlockCache は場面ごとに _reset_stateful_cache() + cache_context
(generate() の per-request リセットの per-scene 版)/api/t2i とバッチの場面1で
mp4 と PNG がバイト完全一致(md5一致)。位相並べ替えは数学的に無影響
(なお group モード出力とは一致しない — FBC の効き方が実行経路で変わる既知の現象で、
バッチ固有の差ではない)H3_LOWVRAM=1 以外のモード(大モデル常駐)では位相並べ替えの利得がないため、
同じ API のまま逐次 generate() にフォールバックする(レスポンス形式は同一)実測 (2026-08-07、RTX PRO 5000 48GB + H3_LOWVRAM=1、3場面・frames=22・768²・30steps):
| 方式 | 合計 | 1枚あたり | 内訳 |
|---|---|---|---|
逐次 /api/t2i ×3 | ~471s | 157s | 毎回 ロード~120s + デノイズ29s + デコード2s |
/api/t2i_batch (3場面) | 202.6s | 67.5s | ロード~110s(1回) + エンコード0.9s + デノイズ84.8s + デコード6.9s |
場面追加の限界コストは ~31s/枚(デノイズ28-29s + デコード2.3s)なので、場面数が 増えるほど1枚あたりは 31s に漸近する(10場面で ~42s/枚、24場面で ~36s/枚の計算)。 ピークVRAMは 35.0GB で1枚生成と同じ(場面ごとに増えるのは潜在+prompt_embedsの数十MBのみ)。
/api/ref2va の still=1 と /api/ref2i_batch、2026-08-07 本実装)下記スパイクの成立を受けて本実装した。キャラクター参照から場面ごとの一貫した 静止画を作るモード。作った静止画は FL2VA の先頭フレームや次の Ref2VA の参照に回せる (物語の複数動画生成の素材作り)。
POST /api/ref2va に still=1 + frames(22既定|5)を追加で渡す。
seconds は無視され、中央フレーム PNG (ref2i_<ts>.png) がレスポンスの
image_url に付く。実装は t2i と同じ3点セット (setup step の間だけの尺ゲート緩和・
VAE 小クリップ修正・既存の decode 例外クリーンアップ) の流用POST /api/ref2i_batch — 共通の references + prompts(最大24場面)。
H3_LOWVRAM=1 では generate_ref2i_batch()(t2i_batch と同じ位相並べ替えの
ref2va 版: TE常駐で全場面エンコード → VAE窓1回で全場面の参照VAEエンコード →
layout/timesteps → transformer_ref 1回ロードで全デノイズ → まとめてデコード)。
他モードは逐次フォールバック。スケジューラ/FBC の場面間リセットは t2i_batch で
md5 一致まで実証済みの同じ手法実測 (2026-08-07、RTX PRO 5000 48GB + H3_LOWVRAM=1、赤ずきん参照1枚・3場面・
frames=22・768²・30steps): 合計 494.7s = 164.9s/枚(逐次 ~265s/枚の1.6倍速)。
内訳: エンコード位相 212.5s + デノイズ 178.1s (57.7-60.2s/場面) + デコード 6.6s。
ピークVRAM 36.8GB。品質・キャラ一貫性とも良好(3場面とも赤マントを保持し
各プロンプトに追従)。
H3_REF_PREFIX_CACHE、既定1、2026-08-08 本実装)ref バッチ (generate_ref_batch = ref2i_batch/ref2va_batch) のエンコード位相で、
参照ラベル+ビジョン (~4104トークン、~65s/場面) の Qwen3-VL エンコードが場面ごとに
重複していた問題の解消。ref2va のトークン列は「参照が前置・プロンプトは末尾に verbatim
追記」(packing_ref2va の build_ref2va_presentation で確認) で、条件付け元の Qwen3-VL は
因果 LM なので、参照プレフィックスのテキスト表現はプロンプトに依存しない —
プレフィックスを1回だけ use_cache=True で通して DynamicCache に焼き、場面ごとには
プロンプト末尾 (14-33トークン、~0.2s) だけをキャッシュ継続する
(core/runner.py の _encode_ref_prompts_shared_prefix())。
検証 (scripts/probe_ref_prefix_cache.py、transformers 5.14.1 の
modeling_qwen3_vl.py を読んだ上で実機実測):
torch.equal)attention_mask=None
(compute_3d_position_ids の arange 分岐 = past長からの連番 + rope_deltas 加算に
乗せる)、mm_token_type_ids/pixel_values/grid_thw 系は全て None
(image_grid_thw を渡すと model.rope_deltas が再計算・上書きされる)、場面ごとに
DynamicCache.crop(prefix_len) で切り戻して直列再利用。rope_deltas は
Qwen3VLModel のインスタンス状態なので、プレフィックス→全継続の間に他の TE 呼び出しを
挟んではならない (ヘルパー1呼び出し内で完結させる設計)E2E 実測 (ref2i_batch、赤ずきん参照1枚・同一3場面・seed 12345・768²・30steps):
| エンコード位相 | 合計 | 1枚あたり | |
|---|---|---|---|
| 共有なし (H3_REF_PREFIX_CACHE=0 相当) | 212.5s | 494.7s | 164.9s |
| 共有あり (既定) | 83.1s | 350.1s | 116.7s (-29%) |
品質: 共有なしの同一 seed 出力との PSNR 21.9-27.4dB。バッチ出力は従来経路と
ビット一致しない (プロンプト末尾側の ~1.5% 丸め差が軌道をドリフトさせる —
Sage Attention 既定化時と同種・同水準の epsilon 級ドリフト) が、目視で構図・品質・
キャラ一貫性とも同等。ビット再現が要る対照実験は H3_REF_PREFIX_CACHE=0 で従来経路に
戻せる。ref2va_batch (動画) もエンコード位相は同一コードなので同じ削減 (~130s/3場面)
がそのまま効く。参照VAEエンコード (~数s/場面) は共有せず場面ごとのまま
(効果が小さく、状態の別名参照リスクを増やさないため)。
/api/ref2va_batch、2026-08-08 本実装)ref2i と同じ位相並べ替え(実装は同一メソッド generate_ref_batch(still=False))を
通常尺の動画に適用したもの。物語の各場面の動画を、共通参照+プロンプト違いで
連続生成する。尺 (seconds) は全場面共通で必須(スケジューラの sigmas/timesteps 値の
場面間同一性が位相並べ替えの前提のため。音声参照からの尺自動導出もバッチでは不可)。
UI は Ref2VA / Ref2I タブの「バッチ連続生成(1行=1場面)」チェック(タブと独立に
使える: Ref2I+バッチ = ref2i_batch、Ref2VA+バッチ = ref2va_batch)。
実測 (2026-08-08、RTX PRO 5000 48GB + H3_LOWVRAM=1、赤ずきん参照1枚・2場面・
5秒・768²・30steps): 合計 803.2s = 401.6s/本(逐次 ~485s/本の17%短縮@2場面)。
内訳: エンコード位相 151.3s + デノイズ 503.4s (247.9-255.5s/場面) + デコード 25.4s。
ピークVRAM 40.5GB。場面追加の限界コストは ~330s/本(ビジョンエンコード ~65s +
デノイズ ~250s + デコード ~13s)なので、場面数が増えると約32%短縮に漸近する。
動画はデノイズ支配のため静止画バッチほど劇的ではないが、品質・キャラ一貫性は
逐次と同様に良好(2場面とも赤マントを保持しプロンプトに追従)。
「t2va で検証済みの22フレーム超短尺が、参照パッキング (packing_ref2va: 参照行が
生成行より多くなる) と重なっても品質が崩れないか」を実測した
(scripts/probe_ref2va_short.py、本体コード無変更・プローブ内 monkeypatch のみ)。
成立すればキャラクター一貫の場面静止画が量産でき、物語の複数動画生成の素材作りが
速くなる。
条件: 参照 = 本リポジトリの t2i 生成物 (赤ずきんの少女 PNG) 1枚、同一プロンプト
(<Picture 1> 参照付き)・seed 12345・30steps・768²。RTX PRO 5000 48GB +
H3_LOWVRAM=1。
| 条件 | デノイズ | デコード | ピークVRAM | wall (逐次・ロード込み) | 品質(中央フレーム目視) |
|---|---|---|---|---|---|
| short22 (0.917s) | 58.1s | 1.7s | 36.7GB | 265s | 破綻なし・高品質 |
| baseline (5.0s) | 243.1s | 10.6s | 40.7GB | 485s | 破綻なし(アンカー) |
generate_still_batch の ref2va 版) は未着手 — t2i と同じ
3点セット (尺ゲート緩和・VAE 小クリップ修正・例外クリーンアップ) は流用可能で、
バッチ位相並べ替えも t2i_batch と同じ手法 (スケジューラ _step_index リセット +
per-scene FBC リセット、参照が全場面共通なら参照エンコード1回共有) が使える見込みフレーム数は 17n+5 刻み(最小5=0.208秒、次が22=0.917秒)。プローブは monkeypatch のみで
検証した(scripts/probe_short_frames.py + _one.py)。当時の環境は RTX PRO 6000 96GB・
既定モード。
| フレーム数 | デノイズ(30steps) | デコード | 結果 |
|---|---|---|---|
| 5 (0.208s) | 完走 | 失敗 | VAEチャンク分割デコードの境界バグ(潜在2フレームで num_chunks=0 → torch.cat([]))→ 上記2で修正済み |
| 22 (0.917s) | 13.5s(124fの1/7.6) | 1.2s | 成功・品質は5秒基準と遜色なし(目視確認済み) |
| 124 (5s、基準) | 102.2s | 6.3s | 成功 |
H3_LOWVRAM=1 はデコード直前に transformer を解放し、次リクエストで毎回再ロードする
(実測 14.8〜32.7s の固定費、docs/RESIDENCY.md §5.5)。H3_KEEP_TRANSFORMER=1 はこの
解放をスキップし、transformer を GPU に常駐させたまま次のリクエストに持ち越すことで
この固定費を初回ロードの1回だけに収束させる。
成立条件は3つとも必須(core/runner.py の import 時ガードで強制、欠けたら
RuntimeError)。VRAM 収支の理由:
H3_LOWVRAM=1(raw "1" のみ。group は transformer をホストRAM常駐+ブロック
offload する別設計のため対象外)H3_TE_DEVICE 設定済み(TE を別GPUに常駐)— これが無いとエンコード位相が先に
破綻する: TE-nf4 17.45GB + 常駐transformer-int8 34.3GB = 51.75GB が実効予算を
超えるH3_VIDEO_VAE_FP16=1 — encode 位相は TE を別GPUに逃がすことで回避できるが、
decode 位相は fp32 デコードピーク16.29GBのままだと transformer 34.3 + 16.29 =
50.6GB で実効予算(~49.8GB)を超える。fp16 化した VAE のデコードピーク ~11.4GB
なら 34.3 + 11.4 = 45.7GB で収まる(video VAE fp16 の品質影響は既知実測 PSNR
39.97dB・目視差なし、詳細は「video VAE の fp16 化」節)既定(H3_KEEP_TRANSFORMER=0)は挙動不変。ref2va(transformer_ref)は対象外 —
このフラグの範囲外で、従来どおり毎回解放される。
# 推奨起動コマンド (48GB GPU0 + 20GB GPU1)
H3_LOWVRAM=1 H3_TE_PRUNE=1 H3_TE_DEVICE=cuda:1 H3_VIDEO_VAE_FP16=1 H3_KEEP_TRANSFORMER=1 \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
実機 E2E 実測(2026-08-09、48GB GPU0 + 20GB GPU1、上記起動構成):
665eadddea8f34298a1b5b89e69d4bd0)。ベースライン側は total 63.27s
(transformer ロード込み)/ peak 36.4GB高速化の系譜(48GB GPU0・H3_LOWVRAM=1 系、t2va は 768²・5秒):
| t2i | t2va 5秒 | |
|---|---|---|
| turboなし 30steps(GPU交換直後の素の構成、08-07) | 157s | 351.4s(デノイズ197.7s) |
| lightx2v turbo 4steps 導入(08-07朝) | 157s | 143s |
+ H3_TE_PREQUANT | 83.2s | — |
+ H3_TE_DEVICE | ~35s | 60.5s |
+ H3_KEEP_TRANSFORMER(本節) | 9.7s | 44.2s |
t2va は素の構成比 8.0倍(351.4s → 44.2s)。turbo だけでは 2.6倍(デノイズは7.6倍
短縮されるがロード固定費 ~110s が残る)で、残りは固定費撤廃
(H3_TE_PREQUANT/H3_TE_DEVICE/H3_KEEP_TRANSFORMER)の寄与。なお本節構成での
turboなし 30steps は t2i で 51.1s(上記実測)— 素の 157s に対し、30steps のままでも
固定費撤廃だけで3倍速い。
詳細な VRAM 収支の導出・位相×常駐表は docs/RESIDENCY.md §5.5・§5.6 を参照。
ブランチ migrate-pr14355。runner.py の t2i/t2va/still_batch/hires/turbo/FBC 経路を
マージ版の新契約(§1 の影響調査参照)へ移植し、venv を f37ab93 へ差し替え
(--no-deps --force-reinstall、他の依存は不変)。ref2va 系は第2段送り
(app.py/runner.py 両方にガード、503 + 明確なメッセージ)。
回帰結果(全て abc5e9b で記録したベースラインと同一 seed で比較):
| 経路 | 結果 | MD5 |
|---|---|---|
| t2i turbo 4steps seed=11 | 定常 9.24s(移行前 9.7s) | PNG 完全一致 |
| t2i turbo 4steps seed=12 | 9.24s | PNG 完全一致 |
| t2i 30steps seed=1 | 49.65s | PNG 完全一致 |
| t2va 5秒 turbo seed=21 | 41.54s、peak 44.15GB(同値) | MP4 完全一致 |
| t2i_batch 2場面 seed=11 | 16.47s(8.2s/枚) | 場面1 = 単発 seed=11 完全一致 |
数値経路(スケジューラ/transformer/VAE)が無変更という影響調査の結論どおり、 移行はビット等価。性能・VRAM ピークも移行前と同値。
移植で踏んだ新しい罠2つ(いずれも _execution_device の変種):
audio_vae が transformer より前に移動した(旧:
text_encoder, tokenizer, processor, vae, scheduler, audio_scheduler, transformer, video_processor, audio_vae → 新: image_processor, text_encoder, tokenizer, processor, vae, audio_vae, scheduler, audio_scheduler, transformer_ref, transformer, video_processor)。_pin_execution_device_to_compute() が text_encoder と vae しか
外していなかったため、CPU 常駐の audio_vae が最初の nn.Module として拾われ、
新レイアウトステップ(出力を _execution_device に置く契約)が position_ids を
CPU に作り、rope() 内で device mismatch(実測再現)。→ audio_vae も窓の間外す_execution_device が解決不能
(TE 外部常駐だと transformer 未ロード+TE デタッチ済み)→ テンソルが CPU に
生まれる。値は正しいので、デノイズ直前に _scene_state_to_compute() で明示的に
計算GPUへ運ぶ(既に GPU なら no-op)第1段(上節)の続き。ref2va / ref2i / refバッチを f37ab93 の新契約へ移植し、
旧ピン abc5e9b で採ったベースライン(一時的に main + abc5e9b へ戻して記録)と
同一 seed・同一参照画像・同一構成(H3_LOWVRAM=1 H3_TE_PRUNE=1、TE_DEVICE なし)で比較:
| 経路 (steps=8) | ベースライン | 移行後 | MD5 |
|---|---|---|---|
| ref2i seed=101 | 206.1s | 201.4s | PNG 完全一致 |
| ref2va 5秒 seed=102 | 332.5s | 322.5s(定常。初回496.8sはディスクキャッシュ冷え) | MP4 完全一致 |
| ref2i_batch 2場面 seed=101(KVプレフィックスキャッシュ経路) | 257.9s | 246.7s | 両場面 PNG 完全一致 |
設計判断2つ:
MiniMaxH3Blocks は3ワークフロー
(t2va/fl2va/ref2va)のサブブロックを合併した component specs を持ち、
transformer_ref と transformer の両スロットが同一シェルに存在する
(MiniMaxH3AutoDenoiseStep が呼び出しごとに state から分岐)。旧2シェル設計の
_ensure_pipe_ref_shell / _sync_shared_components_to_ref は self._pipe_ref = self._pipe
のエイリアスだけ残して no-op 化 -- 既存の VRAM 振り付け(_pipe_ref.transformer_ref を
触る多数の箇所)は無改修で生きるMiniMaxH3Ref2VATextEncoderStep も
プロンプトを presentation 末尾の emit(text(prompt)) として組むため、
「参照プレフィックス共有+プロンプト末尾の継続エンコード」という旧設計がそのまま成立。
削除された packing_ref2va の関数群の代わりに、新ステップ自身のインスタンスメソッド
(_gather_vision_features / _build_presentation)の上に再構築した
(DynamicCache / rope_deltas / 継続呼び出しの引数規約は旧実装の罠メモをそのまま踏襲)他の移植内容は第1段と同型(AfterDenoiseStep 挿入、MiniMaxH3PrepareConditionLatentsStep /
MiniMaxH3Ref2VAPrepareLatentsStep の新ステップ挿入、reference_kind() → entry.kind /
entry.has_audio、app.py の参照構築を MiniMaxH3ImageReference.from_file() 系へ)。
seconds=None の「音声参照から尺を導出」は新 SetupStep が内部でやらなくなったため
_num_frames_from_audio_reference として runner 側に実装。
これで 全経路がマージ版 f37ab93 上でビット等価。旧ピン abc5e9b へ戻す理由は無くなった。
タブが一列に増え続けるのを避けるため、出力の種別で2段に分けた:
| 段 | タブ(括弧内=入力) |
|---|---|
| 動画 | T2VA (テキスト) / FL2VA (フレーム) / Ref2VA (参照) |
| 静止画 | T2I (テキスト) / Ref2I (参照) |
段見出しが「出力」を、タブ名が「入力」を表すので、段×タブで 「動画×参照 = Ref2VA」「静止画×参照 = Ref2I」と読める。
Ref2I を独立タブにした理由: 以前は Ref2VA タブ内の「静止画」チェックだった
(T2I だけがタブ)。この非対称は「T2VA タブに静止画チェックを置くと、同じタブの
キーフレーム入力欄と両立しない(still=True は image/last_image と併用不可、
runner.py で ValueError)」ことに由来していたが、2段組みにすると
『静止画の段に参照ありモードが無い』方が不自然になるため独立させた。参照
アップロード欄は Ref2VA と共有しており(isRefMode())、違いは尺の決め方
(秒数 or フレーム数)と still フラグだけ。
バッチはタブにしない: バッチは静止画/動画の軸と直交する(Ref2I+バッチ =
/api/ref2i_batch、Ref2VA+バッチ = /api/ref2va_batch、T2I+バッチ =
/api/t2i_batch)。タブ化すると段が3列×2段に膨れるので、各タブ内のチェックのまま。
タブボタンは2行構造(1行目=モデル名、2行目=入力種別、.tab-btn small)。
パネル幅 378px ではボタン内幅 77px に対し「T2VA (テキスト)」が 95px で折り返し、
ボタン高さが不揃いになったため、最初から2行にして幅に依存しないようにした
(ブラウザ実測で全ボタン 50px 揃い・2行目の位置も y=26px で一致)。
出力が溜まるとページ表示が重くなる問題(実測: outputs 165本の時点でブラウザの描画が
詰まり、スクリーンショットが30秒でタイムアウト)。原因は <video preload="metadata">
がタイル数ぶん、表示と同時に全数メタデータを取りに行くこと。
<img> は loading="lazy" が効くが <video> は同属性を解釈しないので、
src を data-src に退避し IntersectionObserver(rootMargin: 300px で1画面先読み)で
ビューポートに近づいたときだけ設定する。再描画のたびに disconnect() してから
observe し直す(observer は監視中の要素への参照を保持するため、外さないと古いタイルが
解放されない)。
実測(ブラウザのネットワークログ): ページ読み込み時の mp4 リクエストが 165本 → 0本。PNG(55枚)はブラウザ標準の lazy に任せる。
画面右上のトグル(日本語 / English)で再読込なしに切り替わる。選択は
localStorage['h3_lang'] に保存し、初回は navigator.language で判定する。
I18N = { ja, en }、165キー)。参照は t('key', {vars}) で、
未訳キーは日本語へフォールバックするdata-i18n / data-i18n-html(<b> を含む文言)/
data-i18n-placeholder / data-i18n-title で印を付け、applyI18n() が一括で書き換えるrerenderDynamicI18n() が
ギャラリー・結果パネル・参照タイル・各種ヒント行を描画し直す(直近の結果は
lastResultData に保持して同じ描画関数へ渡す)翻訳しないもの: JSコメント(103行、開発者向けの設計メモ)、サーバが返すエラー
メッセージ(data.detail、app.py 側は日本語のまま)、te_quant: などの技術識別子、
言語トグル自身のラベル。
実機確認(ブラウザ): 日↔英の往復でギャラリータイル(静止画 ↔ Still)・
結果プレースホルダ・ヒント行まで切り替わること、リロードをまたいで選択が残ること、
6経路の送信先(/api/t2i, /api/ref2va+still=1, /api/ref2i_batch,
/api/ref2va_batch, /api/ref2va, /api/t2va)が切替後も不変であること、
コンソールエラーが無いことを確認済み。
fully_copy)— 実測と、そこで見つかったバグ動機: テキストエンコーダを小型モデル+投影行列で置き換える手法 (ClipProj-MiniMax-H3)は TE を 15.7GB → 5.2GB にできるが、作者の実測では発話が劣化する(4B は不明瞭、8B は言語を 捨てて英語で喋る)。ならば台詞を音声参照から入れれば TE の発話能力に依存しない、 という仮説の検証。
結果: 仮説は成立した。
| 検証項目 | 結果 |
|---|---|
| 台詞の内容が音声参照から伝わるか | 成立 — 検出言語 ja (0.964)、認識「今日は良い天気だね」、文字一致 100% |
| リップシンク | 成立 — 口の開閉と音声包絡の相関 +0.745(ずれ 0ms) |
| キャラクター一貫性 | 成立(顔・髪型・制服が参照どおり) |
| 音声の 1:1 コピー | 不成立 — 波形相関 0.112、尺 5.22s → 5.88s |
決定的なのは、プロンプトに日本語の台詞を1文字も書いていないこと(<d> タグ不使用、
「<Audio 1> の台詞に口を合わせる」とだけ指示)。それで同じ日本語が出た =
台詞がテキストエンコーダを経由していない。
fully_copy は名前に反して信号のコピーではない。 公式ガイドは
"The complete source audio serves as the target video's complete final audio track" /
"reused 1:1" と書いているが、実際の挙動は「同じ内容を再生成する」。声質は変わり、
尺もモデル側の都合(141フレーム=5.88秒)で決まる。元音声をそのまま使いたいなら生成後に
差し替える必要がある(相関 +0.745 のシンク精度があるので実用的と思われる。未検証)。
なお口が開いている区間 4.1秒に対し音声のある区間は 1.6秒で、発話後も口が動く傾向がある。 アニメ表現としては許容範囲だが、音素レベルの厳密な一致ではない。
条件: 96GB機、TE nf4 + transformer bf16(非量子化)、768×1344・141フレーム、30steps、 seed 777、総所要 553.9s、ピーク 87.67GB。
上記の検証で発火した。音声を含む参照を渡すと確実にクラッシュする:
sageattention/core.py: assert dtype in [torch.float16, torch.bfloat16]
AssertionError: Input tensors must be in dtype of torch.float16 or torch.bfloat16
(発生源: autoencoder_kl_minimax_h3_audio.py -> dispatch_attention_fn)
原因: MiniMaxH3AudioAttnProcessor は backend=self._attention_backend(既定 None)で
dispatch_attention_fn を呼ぶため、バックエンドがグローバルに解決される。本アプリは
set_attention_backend() を transformer / transformer_ref にしか呼んでいないが、
H3_ATTN_BACKEND=sage(既定)だと audio_vae の attention まで sage に流れる。ところが
audio_vae は設計上 fp32 固定(bf16 にすると音量が約20dB落ちるため)で、sage は
fp16/bf16 しか受け付けない。リクエストの attn= 上書きでも回避できない
(transformer 系にしか効かないため)。
修正: audio_vae のロード直後に audio_vae.set_attention_backend("native") を呼び、
このモジュールだけ native に固定する。音声 VAE は計算量が小さく sage の利得もない。
テストの穴だった: この経路は「音声つき参照」でしか通らないため、既存の ref2va 回帰 (48GB機・int8・画像参照のみ)を全てすり抜けていた。96GB機の非量子化 ref2va も マージ版追従後は未検証だった。修正の確認は sage 既定のまま音声参照つき ref2va が通ること (186.3s、日本語 ja 0.976・文字一致100%)と、画像参照のみの ref2i が壊れていないこと (133.9s)の両方で行った。
H3_TE_PROJ として実装(既定OFF、既存挙動は1バイトも変わらない)。TE を Qwen3-VL-32B から
Qwen3-VL-4B + 学習済み投影行列(ClipProj-MiniMax-H3)へ
置き換える経路。狙いは48GB機で TE と transformer を同時常駐させ、載せ替えの固定費を消すこと。
cond = ((h - mean_in) / std_in) @ W * std_out + mean_out # h = 4B の hidden_states[24]
cond[:, 0] = sink_out # token 0 はアテンションシンク
| 32B TE(現行) | 投影4B TE | |
|---|---|---|
| PSNR | — | 22.64 dB |
| 鮮鋭度(ラプラシアン分散) | 48 | 37(-23%) |
| 生成時間 | 65.7s | 43.7s |
| ピークVRAM | 88.73GB | 76.1GB |
| TE の GPU 実占有 | 21.02GB (nf4) | 8.88GB |
品質は「同じ絵」ではなく「同等品質の別の絵」。 構図・色調・時間帯の解釈は一致するが、 細部の指定は落ちる(実例: プロンプトにない範囲だが、32B版にあった手前の睡蓮の葉と草が 投影版では水面に置き換わった)。破綻はなく実用水準。これは投影の性質(配布元の報告で test cosine 0.712)どおりで、プロンプトの大意は保持し細部は失うと理解するのが正確。
この変更はこれまでの全最適化と種類が違う。 量子化・常駐制御・turbo などは全て同一seed MD5一致で「数学的に無影響」を証明できたが、投影TEは原理的に近似なので MD5 は使えない。 判定は PSNR + 目視 + VRAM 実測による。
| 対象 | 結果 |
|---|---|
| 通常テキスト・日本語 | ID 完全一致(H3 側のトークナイザをそのまま使える) |
<d> / </d> | H3 は 151669/151670 の単一トークン、4B は語彙外で分解される |
<|cutoff|> <|lyrics_*|> <|caption_*|> | H3 固有(151671〜675)だが公式ガイドも本アプリも未使用 |
[Shot n] <cutoff> <scenetrans> | 通常テキスト(特殊トークンではない)。マルチショット構成は影響なし |
→ 実使用で影響を受けるのは台詞タグ <d> だけ。実装では id>=151669 を含むプロンプトを
ValueError で明示的に拒否する(黙って別物を送らない)。台詞は音声参照(fully_copy)で
入れられることを同日に実証済み(上記セクション参照)なので、運用上の回避策がある。
docs/RESIDENCY.md §5.4 の導出式に実測 8.88GB を入れ直すと、H3_TE_DEVICE 用のカード要件が
20GB → 16GB(おそらく12GB) に下がる。
| 2枚目GPU | 実効予算 | 32B TE (17.45GB) | 投影4B TE (8.88GB) |
|---|---|---|---|
| 12GB | ~10.5GB | 不可 | 成立見込み(必要 ~9.2GB、余裕 ~1.3GB) |
| 16GB | ~14.5GB | 不可 | 成立見込み(余裕 ~5.3GB) |
| 20GB | ~19.7GB | t2va のみ成立・ref2va は OOM | 成立(余裕 ~10.5GB) |
12GB は実機確認なしに断言しない。 以前 20GB で「導出上は入るはずが実測 OOM」を 経験しており(単位の罠、§5.1)、余裕1.3GB は薄い。
H3_TE_PROJ_QUANT(既定 none / bnb-4bit / bnb-8bit)を追加した。4B 自体を
量子化する 4B 専用フラグで、32B 用の H3_TE_QUANT とは別物(排他ガードの意図を参照)。
投影行列は bf16 用のものをそのまま使うのが正解(実測)。量子化による条件付けのズレは NF4 で相対RMS 0.61〜0.96%(cosine 1.0000)。配布元の int8_convrot 用行列を使うと かえってズレが増える(1.02〜2.97%)— あれは ComfyUI の量子化方式専用の校正なため。
| 構成 | TE 常駐 | t2i (30steps) | t2va 5秒 (30steps) | ピーク(t2va) |
|---|---|---|---|---|
| 32B TE | 21.02GB | 65.7s | 162.1s | 91.9GB |
| 投影4B bf16 | 8.88GB | 43.7s | — | — |
| 投影4B NF4 | 3.11GB | 33.5s | 143.5s | 74.3GB |
動画でも品質確認済み(768²・5秒・124frames・同一seed 555): 静止画で見えた鮮鋭度低下(-23%)は動画では出ず(187 vs 191)、ちらつき(2階差分)は むしろ 11% 少ない(7.51 → 6.67)。bf16 vs NF4 の直接比較は PSNR 34.45dB で量子化の 影響はほぼゼロ。32B との PSNR 14.98dB は「劣化」ではなく「同じ指示の別テイク」 (32B版には対向者が現れ、投影版は少女単独、など解釈の分岐)。
設定APIとの関係: 投影TEは現状 env 専用(H3_TE_PROJ/H3_TE_PROJ_QUANT)。
/api/settings/apply から te_quant/te_prune を変えようとすると、runner の import 時
ガードを素通りした上で適用されないのにスナップショットは変わったと報告する穴が
あったため、投影TE有効時はこれらのフィールドを 400 で拒否するようにした
(/api/settings のスナップショットに読み取り専用の te_proj/te_proj_quant を追加)。
H3_TE_PROJ_QUANT の既定を bnb-4bit に変更(品質実測に基づく。none/bnb-8bit は
明示指定で選択可)。既定変更に伴い「量子化指定あり+投影OFF」ガードは明示指定
(in os.environ)のときだけ発火するよう修正 -- 既定値のままの通常ユーザーを
誤って落とさないため/api/settings/apply の
te_proj / te_proj_quant)。ON のとき te_quant/te_prune のコントロールは無効化され、
API 側も 400 で拒否する(判定は「適用後の値」ベース: OFF に戻しながら te_quant を
変えるのは合法)往復リロード E2E(実測、96GB機): OFF→ON 47.9s / ON→OFF 29.4s / 再ON 27.3s。 各段で同一 seed 生成の PNG MD5 を照合し、UI 経路の ON = env 経路の NF4 と完全一致、 OFF = 32B ベースラインと完全一致、再 ON = 1回目の ON と完全一致(状態残留なし、 投影行列の再ロード正常)。ON 中の te_quant 変更は 400。
logger.warning を出す
→ 2026-08-11 に目視検証済み: 参照は明確に効く(顔・髪・服・小物まで反映、32B 正解と
同水準。「2026-08-11: ref2va/i2va はどこまで動くか」の節参照)。warning は校正の事実の
記録として残置fully_copy との組み合わせ(台詞を音声から入れる運用が投影TEでも成立するか)ComfyUI の PR #15446(H3 の VAE を チャンク・ストリーミング化してデコードのVRAMを尺非依存にする)を移植すべきか判断するため、 まず自分たちのデコード位相のピーク 16.29GB が何でできているかを分解した。
| 内訳 | 実測 | 比率 |
|---|---|---|
| VAE 2本の重み(常駐) | 11.02 GB | 66% |
| video decode の活性化 | 3.08 GB | 19% |
postprocess_video | 0.00 GB | 0% |
| audio decode | 0.00 GB | 0% |
| uint8変換 + CPU転送 | 2.49 GB | 15% |
| 合計 | 16.59 GB | (README の実測 16.29GB とほぼ一致) |
分かったこと: ピークの2/3は重みであり、チャンク化では1バイトも減らない。 PR #15446 が対象にするのは活性化 3.08GB の部分で、移植しても削減は最大19%。
デコードのピークが尺に比例することは別途確認した(潜在32→48フレームで 3.08→4.80GB、 約30MB/フレームの線形)。つまり PR の指摘自体は正しく、長尺ほど効く。
分解して初めて気づいたが、私たち自身のコードが 2.49GB を積んでいた:
frames_uint8 = (video_tensor.permute(0,2,3,1).float().clamp(0,1) * 255).round().to(torch.uint8).cpu().numpy()
float() / clamp / *255 / round() が各々全長ぶんの中間テンソルを返し、最後に
uint8 版も作られる。フレームを8枚ずつ変換して CPU の出力配列へ直接書き込む
frames_to_uint8() に集約した(4箇所で同一コードだったものを1つに)。
| ピーク | |
|---|---|
| 現行(一括) | +2.65 GB |
| 改良(逐次8フレーム) | +0.03 GB(-99%) |
演算順序は現行と同一なので丸めも変わらない。同一seedの本番生成で PNG の MD5 が完全一致
(66a59ff92d653f1284cabe76bdb6501c)することを確認済み。
活性化 3.08GB(19%)に対し、上流の _decode を丸ごと置き換える monkeypatch が必要で、
追従コストが生じる。より安い 15% を先に取ったので、残りは
audio_vae の fp16 化(重み11GBの一部) を先に検討する方が期待値が高い
(現在 fp32 固定の根拠は「bf16 で音量が約20dB落ちる」という実測だが、fp16 は未検証。
bf16 は仮数部7ビット、fp16 は10ビットで、小振幅の音声では挙動が異なりうる)。
96GB機(RTX PRO 6000)に RTX 4060 Ti 16GB(sm_89)を cuda:1 として増設した。 最終ゴールは (A) 単体 16GB で動くこと、(B) 8GB×2 の2枚構成で動くこと。 その第1段として「投影TE(4B NF4)を 4060 Ti に常駐」を実測した。
起動: H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_TE_DEVICE=cuda:1(lowvram なし、
transformer は bf16 で GPU0 常駐)。
| 計測 | 値 | 備考 |
|---|---|---|
| GPU1 常駐 | 3213 MiB(3.37GB) | 導出 3.11GB+ε と一致。生成後も増えない |
| t2i 768² 定常 | 43.94s(デノイズ 13.48s、デコード 0.88s) | 前日の単騎 33.5s より約10s 増(下記) |
| t2va 5秒 768² | 134.58s(デノイズ 102.57s)、ピーク 71.19GB | |
| PSNR vs 32B 基準 | 22.36 dB(鮮鋭度 37 vs 基準48) | 旧行列の 22.49dB と同水準 |
| PSNR vs 旧行列NF4(単騎) | 39.29 dB | 再校正+GPU違いでも実質同一の画 |
| t2i PNG MD5 | 3cd088df882e37547219b9816a217b91 | 新行列+sm_89 なので旧アンカーとは不一致(想定どおり) |
H3_TE_DEVICE の組合せで rope の device 不一致クラッシュ。
TE を切り離すと _execution_device が CPU の audio_vae に落ちる既知の罠だが、
この組合せは今回が初走行で、非 lowvram 2分岐(catch-all / bnb-4bit+fl2va)の
layout〜timesteps が _pin_execution_device_to_compute() の外にあった。
self._te_external のときだけピンで包む修正(TE同居の通常経路はバイト単位で不変)。h3_qwen3vl_4b_tap24.safetensors を
obsolete/ へ移し、再校正版 mmh3-4b-ClipProj.safetensors に置換していた
(学習 1,666→5,664 プロンプト、cos_test 0.711→0.717、W の cosine 旧比 0.9596 =
実質別の関数)。既定を新ファイル名へ更新。旧行列はローカル HF キャッシュ
(snapshot 3f762f19)に残存し、H3_TE_PROJ に絶対パスを渡せば再現可能。デノイズ+デコードは 14.4s で変わらず、リクエスト毎の固定費が約29s(単騎時は約19s)。 差分の主因候補はエンコードが sm_89 の 4060 Ti 上で走ること(NF4 の dequant が遅い)+ cond の PCIe 転送。一方 t2va は 134.6s と悪化しておらず、尺が長いほど固定費は薄まる。 96GB機では TE は元々 GPU0 に載るのでこの箱で2枚目GPUを使う実益はない(本命は 16GB級単体・8GB×2 の構成検証)。
H3_LOWVRAM=group +
投影TE 同居(TE_DEVICE なし)。導出 3.11+1.4+6.6 = 11.1GB で載る見込みだが、
te_proj × group の組合せは未走行バラスト模擬ではなく、増設した実カードで CUDA_VISIBLE_DEVICES=1 により単体検証した。
起動: H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 H3_ATTN_BACKEND=default(sage は sm_120 専用ビルドのため SDPA に戻す。TE_DEVICE なし =
投影TE同居。te_proj × group はこれが初走行 — 修正なしでそのまま動いた)。
| 計測 | 値 | 判定 |
|---|---|---|
| 起動直後の常駐 | 251 MiB(transformer は CPU、group offload) | |
| t2i 中ピーク | 7.4GB(nvidia-smi 7615MiB) | |
| t2va 5秒デノイズ中ピーク | 11.4GB(nvidia-smi 11681MiB) | 16GiB カードで残り約4.7GB |
| t2va デコード中 | ~7.0GB(fp16 デコード) | |
| t2i 定常 | 498.5s(デノイズ 479s、16.5s/step) | |
| t2va 5秒 | 1499s ≒ 25分(デノイズ 1445s、49.8s/step) |
VRAMの導出 11.1GB は実測 11.4GB でほぼ的中し、ゴールA(単体16GB)は残量たっぷりで成立。 OOM・エラーは一切なし。
group offload は毎ステップ int8 重み ~34GB を CPU→GPU に流すが、4060 Ti が刺さっている スロットは Gen3 x4(実効 ~3.5GB/s)。転送だけで ~10s/step になり、実測 16.5s/step と 整合する。Gen4 x16 のまともなスロットなら転送は ~8分の1になるので、この時間は 「16GB カードの性能」ではなく「この箱のスロット」の値。t2va の 49.8s/step は これに加えて 124 フレーム分の SDPA 計算(sm_89)が乗ったもの。
もう1つ: FBC キャッシュが1ステップも省けていない(cache_skipped_steps: 0、
閾値 0.05)。PRO 6000 + sage + bf16 の軌道では大半のステップが省けていたのとは対照的。
int8+SDPA の軌道では残差が閾値を下回らないらしい。閾値調整で最大2倍の短縮余地
(未検証、品質とのトレードオフ)。
PSNR は vs 32B 基準 7.40dB / vs 前段 7.43dB と数値上は壊滅だが、目視では別の話だった:
つまり int8+SDPA の組合せで生成の軌道が別のアトラクタに移り、結果的に プロンプト適合はむしろ良くなった。構成をまたぐ PSNR/MD5 比較はもともと成立しない (既知の int8 軌道分岐 ~19dB のさらに先)ので、構成間の品質は目視で判断すること。
両GPUを scripts/vram_ballast.py --target-free-gb 7.9(GiB)で headless 8GiB カード相当に
制限して模擬。計算側 = 4060 Ti、TE側 = PRO 6000(CUDA_VISIBLE_DEVICES=1,0 で
アプリの cuda:0/cuda:1 に割り当て)。起動はゴールAと同じ env + H3_TE_DEVICE=cuda:1
(te_proj × group × TE_DEVICE も初走行 — 無修正で動作)。
| 計測(8GiB×2) | 値 |
|---|---|
| t2i 768² | 成功。ピーク 6.4GB、total 512s |
| t2va 5秒 768² デノイズ | 29/29 完走(51.3s/step — 事前予測の「0.9GB超過」は外れ、収まった) |
| t2va 5秒 768² 全体 | 成功。total 1534s、decode 35.2s、APIピーク 7.23GB、実測ワークロード ~8069MiB |
解像度・尺の削減は不要だった(640²/3秒/512² の探索梯子は未使用)。
初回試行はデノイズ完走後、デコード末尾の1行で OOM した。上流 decoders.py の
MiniMaxH3VideoDecodeStep.__call__ 最終行 (video.float() * pixel_std + pixel_mean).clamp(0,1)
が fp16 の全長デコード結果を GPU 上で一括 fp32 化する — 768²・124フレームで
124×768×768×3×4B = 838MiB の一時確保(OOM メッセージの "Tried to allocate 838.00 MiB"
と完全一致)。frames_to_uint8 で潰した一括変換と同族の問題。
対処は venv 無改変ルールどおり runner 側サブクラス(_cpu_norm_video_decode_step()、
f37ab93 の __call__ を複製して1点だけ変更): fp16 のまま CPU へ移してから逆正規化。
要素毎の fp32 mul/add/clamp は CPU/GPU で IEEE754 の丸めが一致する(縮約も FMA 融合も
ない)ため出力はビット単位で同一 — 適用前後で同一 seed の PNG MD5 完全一致
(1a2a136b61234b4917465604ac35cca2)を実測確認。追加コストは fp16 全長 ~420MiB の
PCIe 転送1回のみ。全経路(t2va/t2i/ref2va/バッチ)に無条件適用で、どの構成でも
デコード位相のピークを全長 fp32 数本ぶん下げる(docs/RESIDENCY.md のデコード位相の
数値は次回更新時に再計測)。
残る注意は速度のみ(この箱は2番スロットが Gen3 x4 のため 16.5〜51s/step。 Gen4 x16 なら転送 ~8分の1)と、FBC が int8+SDPA 軌道で効いていない件(閾値調整は未検証)。
ゴールA/B の構成で参照系(ref2i / i2va=画像参照 ref2va / 音声参照 / 768×1344)を検証した。 初回は全滅だったが、原因は VRAM ではなく今日まで一度も併用されなかった組合せで 発火する潜伏バグ2件だった。修正後は境界が VRAM 量で素直に決まるようになった。
H3_VIDEO_VAE_FP16=1 × 参照ありは VRAM 量に関係なく必ず落ちる(dtype 不整合)H3_VIDEO_VAE_FP16=1 は VAE 重みを fp16 に恒久キャストする。デコードは上流ステップが
自前の fp16 autocast を張るので整合するが、エンコード側(encoders.py の
encode_vae_condition — ref2va の参照と fl2va のキーフレーム条件付けが使用)は autocast
なしで、内部で明示的に fp32 化した画素を vae.encode() に渡す →
Input type (float) and bias type (c10::Half) should be the same で即死。96GB でも落ちる。
これまでの ref2va 回帰がすべて fp32 VAE 構成だったため潜伏していた。
修正: _load_vae で fp16 化直後に vae.encode をデコード側と対称の fp16 autocast で
ラップ。精度は設計内(encode_vae_condition は元々、結果を自分で fp16 に丸めてから返す)。
group offload(use_stream=True)は int8 重み ~34GB を pinned memory に置く。
_free_transformer の del+gc ではページが torch のホスト側キャッシングアロケータに
保持されたまま OS に返らず(torch.cuda.empty_cache() はデバイス側のみ)、MemAvailable が
~34GB 不足したままになり、後続の transformer_ref ロードが RAM ガード(40GB 要求)で拒否される。
実測: 解放後も avail 38.6GB(RssShmem 45.6GB 残留)。
修正: _free_transformer / _free_transformer_ref に、group モード時のみ
torch._C._host_emptyCache()(私有APIのため getattr ガード付き)を追加。修正後は解放直後に
avail 85.8GB へ完全回復し、t2va→ref2va 切替が group モードで初めて成立した。
| テスト | 8GiB×2 | 16GB単体(TE同居) |
|---|---|---|
| ref2i(参照静止画 768²) | ○ ピーク6.69GB、1059s | (8GBで成立のため未実施) |
| i2va(768² 5秒) | × デノイズOOM | ○ ピーク9.41GB、~39分 |
| 音声参照(looped 6.9s→7.29s生成) | × デノイズOOM | ○ ピーク11.96GB、~54分 |
| 768×1344 5秒 | × デノイズOOM | ○ ピーク13.37GB(nvidia実測15.2GB)、~66分 |
「投影行列はテキストのみで校正」のため vision 品質は未知だったが、目視では劣化を検出できず:
参照人物の顔・前髪・髪の長さ・カーディガンの色・白リボン・ネックレスまで一貫して反映され、
i2va の絵作り(横顔構図)は 32B 正解 test1_walk_park.mp4 とほぼ同じ。音声参照では
リップ動作も明確。唯一 768×1344 の終盤に軽い構図ドリフト(破綻ではない)。
生成物: outputs/ref2i_1786447720.png / ref2va_1786449645.mp4 / ref2va_1786452054.mp4 /
ref2va_1786455323.mp4。
低VRAM検証で外していたバラストを撤去し、96GB機(RTX PRO 6000 + 2枚目の 4060 Ti 16GB)で 使える高速化を全部入れたときの上限を測った。
構成: H3_LOWVRAM=1 H3_KEEP_TRANSFORMER=1 H3_VIDEO_VAE_FP16=1 H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_TE_DEVICE=cuda:1 H3_TURBO_LORA=1
— GPU0 に int8 transformer 常駐(turbo LoRA 適用済み)、GPU1 に投影TE NF4(3.11GB)常駐、
sage attention(sm_120)、fp16 デコード。turbo は per-request で切替。
| 設定 | t2i 定常 | t2va 5秒 | ピーク |
|---|---|---|---|
| turbo 4steps(FBC は turbo により自動 OFF) | 7.65s | 28.56s | 42.5GB |
| 30steps + FBC(t2i 8 / t2va 7 ステップskip) | 21.41s | 121.5s | 41.8GB |
| 30steps・FBC なし | 27.42s | 155.0s | 42.5GB |
| (参考)96GB 既定・30steps・FBC あり(従来の基準) | — | 約160s | 92GB |
内訳(turbo): t2i = デノイズ 2.40s + デコード 1.07s + 残り約4.2s、t2va = デノイズ 14.81s + デコード 7.20s。48GB機の記録(t2i 9.7s / t2va 44.2s)を更新した。従来の 96GB 既定 (約160s)からは 5.6倍。
注目すべきはピーク 42.5GB — 最速構成は 96GB を使い切っておらず、速さの源は容量ではなく 「固定費ゼロ運用」の側にある(同じ構成は 48GB 級にも載る)。
H3_LOWVRAM=1 の振り付けは TE を毎リクエスト ロード→エンコード→解放する。32B TE(21GB)を
前提にした設計だが、投影TE は 3.11GB しかないのに同じ扱いを受け、7.6s のロードを毎回払う
(55GB 空いているのに、である)。H3_TE_DEVICE=cuda:1 で2枚目GPUに常駐させると:
| TE の置き場所 | t2i 定常 | t2va 5秒 |
|---|---|---|
| GPU0 同居(毎回ロード/解放) | 15.23s | 35.69s |
GPU1 常駐(H3_TE_DEVICE=cuda:1) | 7.65s | 28.56s |
同じ「全部盛り」でも TE の置き場所だけで t2i は2倍違う。なお 96GB機の2枚目は 16GB なので 32B TE(nf4 21GB / 削除版 17.45GB)は載らず、この常駐は投影TEだからこそ成立する。
96GB なら bf16 transformer(66.3GB)が載るので、int8 の逆量子化オーバーヘッドを避けられる
はず、と考えて plain モード(H3_LOWVRAM なし)でも測った。デノイズは確かに bf16 が速い
(t2i 2.07s vs int8 2.40s、t2va 14.05s vs 14.81s = 5〜14%速い)。ところが合計では負ける:
| transformer | t2i 定常 | t2va 5秒 | ピーク |
|---|---|---|---|
int8 常駐(H3_KEEP_TRANSFORMER=1) | 7.65s | 28.56s | 42.5GB |
| bf16 plain モード | 19.9s | 40.0s | 68.9〜73.1GB |
ログを見ると、plain モードはデコードのたびに transformer を解放し、直後に 11.9〜12.3s かけて
再ロードしている。この解放は「TE-nf4 21GB + transformer 66.3GB + VAE 11GB = 98.5GB > 96GB」
という前提で入ったものだが、TE が GPU0 に居ない今は 66.3 + 6.1(fp16 VAE)= 72.4GB で収まる
ので、この構成では不要な解放になっている。外せば bf16 が最速になる見込み(t2i ~8s / t2va ~28s
かつ全精度重み)。H3_KEEP_TRANSFORMER は現状 H3_LOWVRAM=1 を要求するので、plain モード側にも
同種の「解放しない」判断を入れるのが筋。未実装。
上の「無駄2」をその場で潰した。変更は import 時ガードの条件1だけ:
H3_KEEP_TRANSFORMER は H3_LOWVRAM=1 を要求していたが、これを「group でないこと」に
緩め、plain モード(H3_LOWVRAM=0)でもデコード窓の解放をスキップできるようにした。
解放をスキップする分岐(if H3_KEEP_TRANSFORMER: pass)は元から共通で、復元側の
_ensure_transformer は冪等なので 追加の実装は不要だった。残り2条件
(TE が GPU0 に居ない = H3_TE_DEVICE か H3_TE_PROJ / H3_VIDEO_VAE_FP16=1)は
そのまま plain モードの成立条件でもある(66.3 + fp16 デコード 11.4 = 77.7GB)。
| 構成 | t2i 定常 | t2va 5秒 | ピーク | GPU |
|---|---|---|---|---|
| bf16 + TE@GPU1 + 解放停止(案B) | 6.89s | 26.8s | 74.2GB + 3.2GB | 2枚 |
| bf16 単騎(TE も GPU0)+ 解放停止(案A) | 7.08s | 27.04s | 77.3GB | 1枚 |
| int8 単騎 + 解放停止(案C) | 7.40s | 28.13s | 45.6GB | 1枚 |
int8 + H3_LOWVRAM=1 + KEEP + TE@GPU1(前節の最速) | 7.65s | 28.56s | 42.5GB | 2枚 |
| int8 単騎・解放あり(案C の KEEP=0) | 19.58s | — | 39.8GB | 1枚 |
| bf16 + TE@GPU1・解放あり(前節) | 19.9s | 40.0s | 68.9GB | 2枚 |
等価性: 案C と「同条件で H3_KEEP_TRANSFORMER=0」の PNG が MD5 完全一致
(596a718e4b5cf9a0b907d2ec479225d2)。解放の停止は数学的に無影響で、同じ絵が
**19.58s → 7.40s(2.6倍)**で出る。
上の最速構成(int8 単騎・解放停止・ピーク45.6GB)で参照系も測った。参照は
transformer_ref(別モデル)を使うので収支も律速も t2va とは別物になる。
turbo=1 を渡しているのに steps=30 のまま走っていた。原因は app.py の
参照系3エンドポイント (/api/ref2va, /api/ref2i_batch, /api/ref2va_batch) だけが
num_inference_steps: int = Form(30) をハードコードしていたこと(t2va/t2i/t2i_batch/fl2va は
DEFAULT_NUM_INFERENCE_STEPS = turbo 時 4 を使う)。turbo LoRA 自体は transformer_ref へ
正しく適用されていた(ログに turbo LoRA lazily applied to transformer_ref (312 layers wrapped))
ので、「蒸留LoRA を付けたまま 30 ステップ回す」という噛み合わない組合せになっていた。
3箇所を DEFAULT_NUM_INFERENCE_STEPS に揃えて解消。
| モード | turbo 4steps | 30steps(turboなし) | デノイズ(4/30) | ピーク |
|---|---|---|---|---|
| ref2i(参照付き静止画・22フレーム) | 79.3s | 148.4s | 7.8s / 72.1s | 45.4GB |
| i2va(画像参照→動画5秒・124フレーム) | 103.1s | 290.3s | 22.0s / 209.0s | 45.9GB |
ピークは 45.9GB で t2va 系(45.6GB)とほぼ同じ — 参照系も 48GB 級1枚に収まる。 turbo の効きが t2va(5.5倍)より小さい(i2va で 2.8倍)のは、下記のとおり デノイズ以外の固定費が支配的だから。
turbo LoRA は transformer でしか実測しておらず transformer_ref は未検証だったが、
4ステップでも参照人物の忠実度は保たれた: 前髪・髪型・薄緑カーディガン・白リボン・
ネックレスまで一致し、動画も先頭〜中央〜末尾で人物が一貫、カメラ追従・公園の情景とも
プロンプトどおりで破綻なし(outputs/ref2i_1786509275.png /
outputs/ref2va_1786509457.mp4)。strength 0.094 のまま参照条件付きの軌道でも成立する。
| 位相 | 時間 | 備考 |
|---|---|---|
| 参照のビジョンエンコード | 約47s | 最大の律速。4B 投影TE でも縮まない(32B 時代の ~65s/場面から改善はしている) |
| デノイズ(4steps) | 22.0s | |
| デコード + VAE 往復 | 約10s | |
| 参照のVAEエンコード | 約6s | |
| 末尾で t2va 用 transformer を再ロード | 約13s | 連続 ref2va では純粋な無駄 |
H3_REF_PREFIX_CACHE)があるが、単発リクエストの繰り返しでは共有されない
(同じ参照画像を使い回す運用ではプロセス跨ぎのキャッシュが効くはず — 未実装)。H3_KEEP_TRANSFORMER と同種の「戻さない」判断を
入れられる余地がある(未実装)。「この2つを潰せば i2va 103s → 45s 程度」という見積もりは、バッチ実測で支持された。 バッチ経路は「参照エンコードを共有したらどうなるか」を実際にやっている実験そのもので、 768² に揃えた計測では:
| 単発 | バッチ1件あたり | 実測削減 | 共有モデルの予測 | 差 | |
|---|---|---|---|---|---|
| ref2i(3場面) | 79.3s | 47.0s | 32.3s/枚 | 31.3s/枚 | +1.0s |
| i2va(2場面) | 103.1s | 75.0s | 28.1s/本 | 23.5s/本 | +4.6s |
静止画・動画とも予測どおり(むしろ予測より少し良い)。バッチのステップ時間は単発と 一致している(i2va 7.321s vs 7.323s)ので、差は純粋に「47s のエンコードを何回払うか」だけ。 したがって リクエスト跨ぎのキャッシュ(47s)+ 再ロード撤廃(13s)→ 103.1 − 47 − 13 ≒ 43s という見積もりは、現時点の証拠と整合する(実装はまだ)。
一度は誤って取り下げた: 最初のバッチ計測でバッチ側にだけ
height/widthを渡し忘れ、 1344×768(=768² の 1.75倍のピクセル)で生成していたため「動画では共有の利得が出ない」と 誤読して見積もりを撤回した。解像度を揃えたら一致した。経路間の比較では解像度を明示する — 前付けの「バッチ」節の囲みも参照。
上流 lightx2v/Minimax-h3-Turbo が 本日 v1.0 を公開した(v0.1 に加えて2種)。上流 ModelTC/Minimax-H3-Turbo の Model specs:
| モデル | 学習解像度 | shift(video/audio) | 推奨NFE | metadata alpha |
|---|---|---|---|---|
| 4-step v0.1(現既定) | 544p 混合 | 12 / 3 | 4 | なし(実測 scale 0.094) |
| 8-step v1.0 | 544p 混合 | 12 / 3 | 8(4も可) | 8 → scale 8/128 = 0.0625 |
| 4-step v1.0 768p | 768p(1344×768) | 6 / 3 | 4 | 128 → scale 1.0 |
ファイル差し替えだけでは正しく動かないのが要点: 3ファイルは同一キー構造(rank 128)だが
metadata の alpha が全部違い、適用 scale は 0.094 / 0.0625 / 1.0 と三者三様。さらに
768p 版は video shift 6 で蒸留されており、H3 既定の 12 のままだとサンプリング格子が
蒸留時とずれる。このため H3_VIDEO_SHIFT / H3_AUDIO_SHIFT(既定は空 = 触らない)を
追加し、スケジューラロード直後に set_shift() で上書きできるようにした。
| 構成 | t2i 768² | t2va 5秒 768² | 品質(目視、seed 4242 の雪山湖) |
|---|---|---|---|
| v0.1 4step(scale 0.094、現既定) | 7.40s | 28.13s | 岩肌・反射に市松状アーティファクト顕著 |
| 8step v1.0 @8steps(scale 0.0625) | 10.94s | 49.61s | 非常にクリーン(光条まで出る) |
| 8step v1.0 @4steps | 7.47s | — | v0.1 同等〜やや良 |
768p v1.0 @4steps(scale 1.0 + H3_VIDEO_SHIFT=6) | 7.59s | 28.32s | 8steps 級にクリーン、v0.1 のアーティファクトほぼ消滅 |
| 同・1344×768(学習解像度) | 10.53s | — | 特に良好(雲・霧・樹木の質感) |
結論: 768p v1.0 が本命。 速度は v0.1 と同じ 4steps のまま、品質だけが 8steps 級に上がる。 アニメ調の t2va(seed 555)も目視で高品質・破綻なし。
現状の H3_VIDEO_SHIFT はプロセス全体に効く。turbo はリクエスト毎に on/off できるため、
768p 版を既定にして shift 6 を常時掛けると、turbo=0 の 30 ステップ生成まで shift 6 で
走ってしまう(ベースモデルは shift 12 前提)。既定化するなら「実効 turbo 状態に応じて
リクエスト毎に set_shift を切り替える」実装が必要。あわせて scale もファイル metadata の
alpha から自動導出(alpha/rank、無ければ従来の 0.094)にするのが筋。未実装。
それまでの 768p 版の使い方(env 3点セット):
H3_TURBO_LORA=1 H3_TURBO_LORA_FILE=minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors \
H3_TURBO_LORA_SCALE=1.0 H3_VIDEO_SHIFT=6 ...
→ 既定化後は H3_TURBO_LORA=1 だけでこの構成になる。
上の2点(scale のファイル metadata 自動導出・shift のリクエスト単位切替)を実装し、
core/runner.py の既定 LoRA ファイルを 768p v1.0 へ切り替えた。
resolve_turbo_lora_scale(lora_format, lora_path): H3_TURBO_LORA_SCALE 未指定時、
lora_path の safetensors metadata に alpha があれば scale = alpha / rank
(rank はソート済み最初の lora_A キーの shape[0]、3ファイルとも128) を使う。
alpha が無ければ従来どおり形式別既定(diffusers=0.094)にフォールバック。解決した
scale と出典を logger.info で1行出す。H3_TURBO_VIDEO_SHIFT(新 env、既定は空): 空のときは H3_TURBO_LORA_FILE に
_768p を含むかどうかで自動判定(768p系のみ 6.0、それ以外は「切替なし」= None)。
MiniMaxH3Runner._apply_turbo_video_shift(turbo_effective) が
generate()/generate_still_batch()/generate_ref2va()/generate_ref_batch()
それぞれのリクエスト先頭(MiniMaxH3SetTimestepsStep より前、バッチは場面ループの外)
で呼ばれ、instant["turbo"](リクエストの実効 turbo 状態)に応じて
scheduler.set_shift(turbo_shift if turbo_effective else base_video_shift) を行う
(base_video_shift は _ensure_vaes が H3_VIDEO_SHIFT 適用後に記録した値、
常に 12.0 とは限らない)。現在値と同じなら no-op。H3_TURBO_VIDEO_SHIFT is None
(768p 以外のファイル)のプロセスでは呼び出し自体が即 return する。H3_TURBO_LORA_FILE の既定を
minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors に変更。旧 v0.1 に戻すには
env で明示すればよい(scale はその場で 0.094 フォールバック、shift 自動切替なしに戻る)。設計からの逸脱は無し(タスク仕様どおりの実装)。
H3_KEEP_TRANSFORMER=1 H3_VIDEO_VAE_FP16=1 H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_TURBO_LORA=1`、FILE/SCALE/SHIFT は一切未指定 = 新既定のみ)
| 検証 | 結果 |
|---|---|
1. ユニット: resolve_turbo_lora_scale 3ファイル | v0.1→0.094 / 8step→0.0625 / 768p→1.0000、いずれも期待値と一致 |
| 2. 起動ログ | 768p ファイル選択・turbo LoRA scale resolved: 1.0000 (source=metadata alpha=128, rank=128, ...) を確認 |
| 3. turbo=1 (既定4steps) t2i seed 4242 | 8.95〜9.16s、ログに turbo video scheduler shift applied: 6.000 (turbo=True, ..., base=12.000)。PNG はクリーン、市松アーティファクトなし |
| 4. turbo=0 30steps 等価性 | 既存ベースライン outputs/t2i_1786507458.png(MD5 596a718e...)は実は「v0.1・turbo=1・4steps」の結果(README「その2」の H3_KEEP_TRANSFORMER 等価性検証の記録と一致)であって turbo=0/30steps ではなかったため、単純比較は無効と判明。turbo=0/30steps 自体は本タスクの往復検証(次項)で自己無矛盾を確認した |
| 5. 往復リーク検査(turbo1→turbo0(30)→turbo1→turbo0(30)) | turbo=1 側3回すべて MD5 d8bdffed4dce7694c2daf141f70f41a2 一致、turbo=0(30steps)側3回すべて MD5 f3622b6d0229feac608f2e4f666aa410 一致。shift の適用/復元ログもリクエストごとに1回ずつ、期待どおり増減。リークなし |
| 6. ref2va still=1 (ref2i) turbo=1 スモーク | 完走(147.0s、初回のみ transformer_ref ロード込み)。ログに shift 適用行あり。品質は参照人物の特徴(前髪・薄緑カーディガン・ネックレス)を保持、破綻なし |
7. 旧ファイル後方互換(H3_TURBO_LORA_FILE=...v0.1...) | 起動ログで scale=0.0940 (source=format-default)、shift ログは一切出ない(=切替なし)。t2i turbo=1 完走、PNG MD5 596a718e4b5cf9a0b907d2ec479225d2 が旧デフォルト構成の既知値と完全一致(岩肌の市松アーティファクトも再現、劣化ではなく忠実な再現) |
検証4で判明した重要事項: タスク前提の「outputs/t2i_1786507458.png = turbo=0/30steps ベースライン」は誤りだった
(実体は v0.1・turbo=1・4steps の結果)。shift 復元に実際のバグは無かった —
往復検証(5)の自己無矛盾で正しさを確認済み。今後 turbo=0/30steps の基準値が要るときは、
このタスクの検証5で得た f3622b6d0229feac608f2e4f666aa410(seed=4242, 768x768, 22frames,
int8単騎+解放停止+投影TE構成)を新しい基準として使える。
変更前後の等価性も後追いで成立(同日、親側で確認): 本来意図していた「旧コードの
turbo=0/30steps」の実物は outputs/t2i_1786510342.png(既定化前のコード・同構成で
同日実測した 30steps)で、その MD5 は f3622b6d0229feac608f2e4f666aa410 — 新コードの
turbo=0 出力と完全一致。つまり「既定 LoRA を 768p に替え、リクエスト毎 shift 切替を
入れても、turbo を使わない生成は1ビットも変わらない」ことがコード変更をまたいで実証された。
mute)H3 は動画と音声を同一の transformer forward で同時に生成するomni-modal モデルで、
音声の潜在はレイアウト段で位置ごと確保され、毎ステップ audio_hidden_states として
デノイズ本体に入る。つまり音声生成そのものは止められない(止めるには packed sequence の
再設計が要る = モデル構造いじり)。そこで出力段で音声トラックを付けない方式にした。
_mux_mp4(..., mute=False) に引数を追加し、mute=True のときは AAC ストリームを
そもそも add_stream しない。全4生成経路(generate / generate_still_batch /
generate_ref2va / generate_ref_batch)に mute を通し、API 7エンドポイント
(t2va/t2i/t2i_batch/fl2va/ref2va/ref2i_batch/ref2va_batch)の
mute フォーム項目、UI のチェックボックス「無音で出力(音声トラックを付けない)」
(日英 i18n)まで配線した。結果 JSON に mute を返す。実測(96GB機・新既定 turbo 768p・768²):
| 検証 | 結果 |
|---|---|
t2i mute=1 / mute=0 の mp4 | 音声stream 0本 / 1本(169KB vs 185KB) |
| 同 seed の映像側 | PNG MD5 完全一致(d8bdffed…)= 映像は1ビットも変わらない |
t2va 5秒 mute=1 | 音声stream 0本、768×768・124フレーム、27.51s |
ref2i mute=1 | 音声stream 0本、768×768・22フレーム、143.17s |
台詞は <d> タグで明示的に入れる仕様なので、使わなければ声は入らない
(環境音・BGM は残る)。overall_soundscape に環境音だけ書き non_diegetic_music を
空にすれば BGM も入らない。ただしプロンプトで「無音」と指示しても完全な無音にはならない
(実測の audio_rms は常に 0.065〜0.212 の範囲で、0 は観測していない)ので、
確実に無音が要るときは mute を使うこと。
<d> を使っていないのに声が出る」の原因調査ユーザー報告「<d> を使わないのに声が生成される」を調べた。原因は2つあり、片方は
こちらの不具合、もう片方はモデルの性質だった。あわせて「以前の説明(<d> を使わなければ
声は入らない)は不正確だった」ことを記録しておく — <d> は台詞を指定するためのタグで、
声が出るかどうかを制御するものではない。
聴かずに判定するため、音声帯域(300-3400Hz)のエンベロープが音節レート(4-8Hz)で
どれだけ変調されているかを測る指標を用意した(speech_score = 4-8Hz の変調エネルギー /
0.5-20Hz 全体)。人の声はこの帯域に強い変調を持ち、風・水・環境音は持たない。
| 生成 | rms | speech_score |
|---|---|---|
| 女性が公園を散歩(LLM強化が**「子供たちの笑い声」を挿入**) | 0.207 | 0.307 |
| 通学路の少女(声の指示は一切なし) | 0.033 | 0.298 |
| 同・プロンプトで明示的に声を禁止 | 0.0071 | 0.272 |
| 同・LLM修正後の強化文で生成 | 0.0104 | 0.269 |
| 雪山の湖(人が映っていない) | 0.364 | 0.125 |
core/llm.py のストーリーボード用 few-shot 例が「商店街の雑踏と呼び込みの声」で、
モデルがこれを真似て「遠くで聞こえる子供たちの笑い声」等を勝手に足していた。H3 は音の記述に
忠実なので、そのとおり声が生成される。共通ルールに「ユーザーが声を求めていない限り、
音の記述に人の声(台詞・話し声・笑い声・歓声・呼び込み・歌声)を入れない」を追加した。
修正後、同じ入力「公園を散歩する女性」の強化結果は風・枯れ葉・鳥の声のみになり、
人の声は消えた。
声の指示が一切なくても speech_score 0.298(人の映らない風景は 0.125)。プロンプトで 明示的に声を禁止しても 0.272 までしか下がらない。つまり H3 は人物が画面にいると 声らしい音を出す傾向があり、プロンプトでは消し切れない。
ただし音量は大きく下がる: 声禁止の指示で rms 0.033 → 0.0071(約1/4.6、実用上ほぼ 聞こえない水準)。LLM修正後の生成も rms 0.0104 と静か。
| やりたいこと | 手段 |
|---|---|
| 台詞(スクリプト)を入れない | <d> を使わない(これは従来どおり有効) |
| 声・生活音を減らしたい | 音の記述を環境音・自然音だけにする(LLM強化は修正済みで自動的にそうなる)+ 必要なら「人の声なし」を明記 → 音量は 1/4.6 まで下がるが完全には消えない |
| 確実に声・音をゼロにしたい | mute(無音で出力)を使う — 音声トラックごと落とすので確実 |
<d>)と投影TEの非互換を、ユーザーに伝える形にした「ユーザー入力に『おはよう』のように台詞を入れると声が出るのか」を実機で確認した結果、 出る仕様だが、経路によって扱いが分かれ、既定構成では正式な経路が塞がっていることが 分かったので、案内を整備した(TEの自動切替はしない — 32B TE は常駐 21GB で速度も 落ちるため、切り替えるかはオペレーターの判断に委ねる方針)。
| 強化モード | 出力 | 生成結果 |
|---|---|---|
| h3-official | 話者ID付きで <d>[Japanese] おはよう</d> に展開(H3 本来の形式) | 投影TE では 400 拒否 |
| storyboard | <d> を使わず地の文で「女性の『おはよう』という澄んだ声」 | 生成成功、speech_score 0.370(全測定で最高。声の指示なし 0.298 / 無人風景 0.125) |
つまり H3 は地の文の発話記述にも反応して声を出すが、<d> のように台詞内容を保証する
わけではない。なお「声を勝手に足さない」ルール(その6)は、ユーザーが挨拶を求めている
ケースを正しく通した(声が含意されているため)。
_reject_unsupported_proj_tokens() の文面はそのまま UI のエラー表示に出るので、
原因だけでなく選択肢を明示するようにした:
台詞タグ <d>…</d> は現在の設定では使えません。…
(1) 【推奨】UI の「再ロードが必要な設定」パネルで 投影TE を OFF にして適用する
→ 32B TE に切り替わり <d> が使えます(再起動不要。ただし TE 常駐が 3.11GB → 21GB に
増え、生成も遅くなります)。
(2) 台詞を音声参照 (fully_copy) で入れる → 投影TE のままで使えます。
(3) <d> タグを外し、地の文で「〜と挨拶する」のように書く
→ 発話らしい音は出ますが、台詞の内容は保証されません。
エラーは生成リクエストを消費してから返るので、プロンプト欄に <d> があり、かつ
投影TE が ON のあいだは常時ヒントを出すようにした(updateInstantHint() に
promptHasDialogueTag() を追加、プロンプト入力と投影TEトグルの両方で再評価。日英 i18n)。
⚠ 台詞タグ
<d>は投影TEでは使えません。「再ロードが必要な設定」パネルで投影TEを OFF(=32B TE)にして適用してください(再起動不要、常駐は3.11GB→21GBに増加)。 このまま生成するとエラーになります。
storyboard 強化の出力に「クローズアップ、85mm(100mmに修正)、固定」という文字列が 混入していた。焦点距離は 35/50/65/100mm のみ許可というルールに対し、LLM が誤った値を 書いたあと括弧で自己訂正し、そのメタ文が本文に残ってそのまま H3 へ渡る。指示だけでは LLM は時々破るので、指示と後処理の二段で対処した:
_sanitize_enhanced() を全4モード(brief/storyboard/translate/
h3-official)の戻り値に適用。(a)「NNmm(MMmmに修正)」を訂正後の値へ畳む、
(b) 単独で残った訂正注釈の括弧を落とす、(c) 許可外の焦点距離を最も近い許可値へ丸める
(24mm → 35mm 等)。発火したら logger.info を出す(黙って書き換えない)。ユニット検証(全ケース期待どおり): 85mm(100mmに修正) → 100mm(全角・半角括弧とも)、
24mm → 35mm、50mm/35mm/100mm は不変。実機でも同じ入力を3回投げ、焦点距離は
全て許可値・メタ記述の混入ゼロを確認。
「結果表示のピーク 49.47GB と、生成ボタン下の GPU alloc 74.21GB がなぜ違うのか」という 疑問から、そもそもその復元は必要なのかを検討した。
peak_vram_gb(結果表示)はリクエスト先頭の reset_peak_memory_stats() からデコード
直後までの高水位で、ステータス行はリクエスト完了後の現在値。ref2va は入口で t2va 用
transformer を解放し、終了時に積み直していたため、両者は別の瞬間を見ていた:
| 時刻(実測ログ) | イベント | GPU alloc |
|---|---|---|
| 入口 | transformer freed | 3.11GB(投影TEのみ) |
transformer_ref loaded in 34.8s | 37.2GB | |
| デノイズ+デコード ← ここが結果表示の 49.47GB | ||
| 終了直前 | transformer loaded in 36.9s(復元) | 74.29GB ← ステータス行はこれ |
復元はそのリクエストの所要時間に含まれるので、ユーザーは自分の動画と無関係な ロードを待たされる。しかも次も ref2va なら入口でまた解放するので完全な無駄で、復元した 瞬間に両 transformer が載って高水位が跳ね上がる。元のコード上の根拠は 32B TE(21GB)前提の収支で、投影TE(3.11GB)を使う現在の既定には当てはまらない。
そこで H3_EAGER_VARIANT_RESTORE(既定 0 = 復元しない) を追加。壊れない理由は、
t2va リクエストが入口で _switch_to_variant("t2va") → _ensure_transformer() を必ず通り、
未ロードならそこでロードするため — コストは消えるのではなく、それを必要とする
リクエスト側へ移る。旧挙動は H3_EAGER_VARIANT_RESTORE=1 で戻せる。
| eager(旧既定) | lazy(新既定) | 短縮 | |
|---|---|---|---|
| 初回(transformer_ref コールドロード込み) | 185.92s | 131.13s | -54.8s |
| 定常(2〜3本目の平均) | 108.4s | 90.8s | -17.6s |
| 生成中ピーク | 45.67〜45.92GB | 45.67〜45.92GB | 同じ |
| t2va transformer のロード回数 / ref2va 3本 | 4回 | 0回(t2i を投げた時に1回) | |
| リクエスト間の常駐 | 74.28GB | 3.11GB |
連続 ref2va ではリクエスト間の常駐が 74.28GB → 3.11GB になり、48GB 級での運用に 効く。コストが移るだけの ref2va→t2va 切替では、切替後の最初の t2i が 57.17s (2本目以降は 16.18s)と、移った分を素直に払う。
注意: 上の「生成中ピーク」は両者同じで、混在運用の高水位(74.3GB)が消えるわけでは ない。t2va と参照系を同じプロセスで混ぜれば、遅延復元でも t2va 側で両 transformer が 載る。48GB 1枚ならプロセスをモード別に分けるという結論は変わらない。
H3_VRAM_LIMIT_GB — 大きいカードでも使う量に上限を切る「96GB でも GPU を半分だけ使いたいケースはある」という要望への対応。同じカードで ComfyUI や diffusers-server を並走させるとき、H3 がカードを食い尽くさないよう プロセスの確保上限を切れるようにした(既定は空 = 無制限で挙動不変)。
H3_VRAM_LIMIT_GB=48 ... # このプロセスは 48GB までしか確保しない
実装は torch.cuda.set_per_process_memory_fraction()(GB 指定をカード総容量で割って
渡す)。予約ではなく上限なので、使わない限りメモリは消費せず、上限を超える確保は
PyTorch のキャッシングアロケータが OOM として弾く = 同居相手のメモリを奪わない。
指定がカード総容量以上なら警告だけ出して素通しする。
| 検証 | 結果 |
|---|---|
| 上限8GBで 6GB 確保 | OK |
| 同・20GB 確保 | OOM で正しく拒否 |
H3_VRAM_LIMIT_GB=48 + 通常構成(int8単騎・turbo・投影TE)で t2i | 7.8s / ピーク 45.03GB |
| 同・t2va 5秒 | 28.82s / ピーク 45.64GB(nvidia-smi 40.4GB) |
| ステータス表示 | vram_limit_gb: 48.0 |
今日確立した最速構成(ピーク 45.6GB)はそのまま 48GB 上限に収まるので、96GB カードの 半分だけを H3 に割り当てて残りを別ワークロードに回せる。
48GB 上限で落ちる既知のケース(同日、実運用テストで発生): ref2va の既定解像度 1344×768・5秒はピーク 49.73GB で、48GB 上限だと数百 MB 超過して OOM になる (
Tried to allocate 432.00 MiB ... 44.70 GiB allowed)。上限 48GB で参照系を使うならheight=768 width=768を明示する(768² ならピーク ~45.9GB で収まる)。 なお検証で付けたH3_VRAM_LIMIT_GBを外し忘れて運用を続けないこと — この事故の 直接原因は、上限付きで起動した検証サーバをそのまま使い続けたことだった。
scripts/vram_ballast.py はダミーテンソルを実際に確保して空き容量そのものを減らす。
一方 H3_VRAM_LIMIT_GB は確保上限を切るだけで、torch.cuda.mem_get_info() が返す空きや
カード総容量の見え方は変わらない。したがって:
H3_VRAM_LIMIT_GB(軽い、メモリを無駄にしない)_te_external_usable_for() の 24GB 判定など)
→ バラスト(見え方まで変わる必要があるため)注記: import 時の
logger.infoは uvicorn がロギングを設定する前に走るため、 起動ログには出ない。効いているかは/api/statusのrunner.vram_limit_gbで確認する。
前日の最終テストで踏んだ「ref2va 1344×768・5秒が 48GB 上限で OOM」(ピーク 49.73GB に対し 実効予算 ~49.8GB = 余裕 0.07GB)への答え。候補は「TE を2枚目GPUへ逃がす」か 「group offload」だったが、前者が正解と実測で確定した。
| 構成 | GPU0 ピーク | 48GB 級予算 ~49.8GB に対して |
|---|---|---|
| 投影TE を GPU0 に同居(前日の構成) | 49.73GB | 余裕 0.07GB = 実質入らない |
投影TE を GPU1 へ(H3_TE_DEVICE=cuda:1) | 46.36GB | 余裕 約3.4GB = 入る |
TE 側の実測は 3217MiB(約3.4GB)。2枚目は小さくてよい(16GB や 20GB で十分)。
group offload は turbo と併用不可(cpu_param_dict の制約)で 30 ステップに戻るため、
速度を捨てる分だけ不利 — 2枚目GPUがあるなら TE を逃がすほうが良い。
_te_external_usable_for() の 24GB 判定を TE 種別で分離。24GB は
32B TE の実測(TE-nf4 17.45GB + 参照エンコード 3.22GB 以上)から来た値で、
3.11GB の投影TE には過大だった。投影TE は 8GB を閾値に(2026-08-11 の 8GiB×2 検証で
TE 側ピーク約 5.7GB で ref2i/ref2va が完走している実測に基づく)。これで 16GB/20GB の
2枚目GPUが不当に弾かれなくなった。generate_ref2va() の bnb-4bit 分岐に _pin_execution_device_to_compute() を適用。
TE を切り離すと _execution_device が CPU 上の audio_vae に落ち、layout が CPU に
テンソルを作ってデノイズで cuda:0 and cpu の不一致になる(2026-08-12 に generate() の
非 lowvram 分岐で踏んだのと同型)。1 のガードがこの組合せを塞いでいたため、
これまで一度も走っていなかった — 緩和した瞬間に発火した。教訓: 「ガードで塞がれている経路」はテストされていない経路でもある。ガードを 緩めるときは、その先に未走行のコードがあると想定して検証すること(今回は緩和 → 即 device 不一致 → 修正、という順で顕在化した)。
upscale=1(2段アップスケール)が壊れていた — PR #14355 移行の取り残しを修正「小さく生成してアップスケール」で 48GB に収める案を検証しようとして発覚。現在の
コードでは upscale=1 が必ず失敗する(今日の変更とは無関係の既存不具合)。
`token_tags` and `timestep_indices` must both be `(seq_len,)` tensors matching
`position_ids`, got [21760] and [85696] for seq_len=21760
turbo 4steps でも 8steps でも同じで、ステップ数には依存しない。
hires-fix のパス2は、解像度が変わるのでレイアウト(position_ids/token_tags/各 indices)を
build_packed_sequence() で作り直す。この再構築は 2edc525(PR #14355 以前)に書かれ、
block_state.token_tags = ... のように block_state の属性へ直接代入している。
ところがマージ版(f37ab93)の MiniMaxH3DenoiseStep は、レイアウト値を
block_state.denoiser_input_fields(新設の kwargs_type グループ)経由で読むように
変わった。この dict は state 側の出力から組まれるため、属性代入だけでは反映されない。
結果、row_timestep_plan(こちらは state.set() で書いている)だけがパス2の長さ(85696)に
なり、token_tags/position_ids はパス1のまま(21760)で不整合になる。
移行時の回帰セットに upscale が入っていなかったのが根本原因(第1段は t2i/t2va/バッチ、
第2段は ref2va 系で、upscale=1 は対象外だった)。
再構築した5つのテンソル(position_ids/token_tags/video_indices/audio_indices/
text_indices)を block_state.denoiser_input_fields にも書き戻す。state.set() では
なくこの dict なのは、ループデノイザがここからしか読まないため(既にあるキーだけ差し替え、
新しいフィールドは生やさない)。
修正後の実測(96GB機・int8 + 投影TE(GPU1)+ turbo 4steps・5秒):
| 生成 → 仕上げ | 合計 | パス1 / パス2 | ピーク |
|---|---|---|---|
| 768² → 1536² | 86.0s | 11.1s / 28.3s | 55.83GB |
| 672×384 → 1344×768 | 34.0s | 4.5s / 8.6s | 44.67GB |
旧実測(96GB機・bf16 + 32B TE・30steps)の 645s / 88.0GB から、時間で7.5倍・VRAM で 32GB 以上の改善。
ユーザー指摘「動く腕に二重の線」を受けて調査。当初「ステップを増やしても消えない」と 判断したがこれは誤りだった(30steps の対照は turbo なしで、別種の破綻を見ていた)。 同条件でパス2のステップ数だけを振ると:
| turbo・総ステップ | パス2 | 品質(目視・動く部位) | 時間 |
|---|---|---|---|
| 4steps(既定) | 1 | 動く輪郭が激しく二重化・手が崩れる | 86s |
| 8steps | 3 | 半透明のゴーストが残る(体越しに背景が透ける) | 135s |
| 12steps | 4 | クリーン(モザイクも二重輪郭もなし) | 219s |
patchify⇄_unpatchify)は純テンソルプローブでビット一致、
x0 復元・再ノイズ式もマージ版スケジューラの実装と一致 — 移行漏れではなく、
純粋にパス2のステップ不足が原因だった。推奨レシピ: turbo=1 + num_inference_steps=12 + upscale=1。48GB 級で使える
ワイド版も同条件で確認済み:
| 生成 → 仕上げ | steps | パス2 | 合計 | ピーク | 品質 |
|---|---|---|---|---|---|
| 768² → 1536² | 12 | 4 | 219s | 55.8GB | クリーン |
| 672×384 → 1344×768 | 12 | 4 | 96s | 44.43GB | クリーン(4steps 版で出ていた輪郭の甘さも解消) |
ただし写実系では upscale は使わないほうがよい(2026-08-13、ユーザー指摘を受けて実測):
| 1344×768・同一プロンプト(写実)・同一seed | 時間 | ピーク | 髪・建物の高周波成分 |
|---|---|---|---|
| 直接生成(turbo 4steps) | 52.4s | 47.79GB | 0.2675(基準) |
| 672×384 → upscale(turbo 12steps) | 89.1s | 44.69GB | 0.1997(-25%) |
等倍で並べると、直接生成は毛束が1本ずつ分離し鞄の鎖まで解像するのに対し、upscale 側は 全体が溶ける。パス1を 672×384 で回す以上、そこに存在しない細部は 2x 補間でも パス2の4ステップでも作られない(既定 LoRA が 1344×768 学習の 768p 版であることも、 半分の解像度で回すパス1には不利に働く)。
upscale=1 は基本的に使わなくてよい(2026-08-13 の結論)「1536² は直接生成できないから upscale の出番がある」という想定も実測で否定された。
canvas_native_max は 1344 だが、直接 1536² を生成しても破綻しない:
| 1536²・同一プロンプト/seed(アニメ調) | 時間 | ピーク | 高周波成分 |
|---|---|---|---|
| 直接生成(turbo 4steps) | 144.1s | 59.21GB | 0.2596 |
| upscale(turbo 12steps) | 218.9s | 55.83GB | 0.2750 |
目視でもどちらも良好で、直接生成のほうがむしろ構図が素直。つまり upscale の利点は 「ピークが 3〜3.4GB 低い」ことだけで、代わりに1.5〜1.7倍遅く、写実系では細部が 25% 落ちる。
使いどころは「直接生成では VRAM が足りず、upscale なら収まる」という幅 3GB の隙間だけ (例: 1536² を 56〜58GB 級の予算で出したい)。それ以外は直接生成が上位互換。 UI に出しておく価値はあるが、既定で勧める機能ではない。
turbo=0 num_inference_steps=30 upscale=1。切り分けるなら「turbo なし・30steps・bf16 transformer」を1本(77GB級が要る)。H3_HIRES_DENOISE=0.35)では turbo 既定4ステップで
パス2が1ステップしか取れない。12ステップまで上げれば直るが、そのぶん turbo の速度利点が
消える。配分そのものを見直す(パス2に最低ステップ数を保証する)余地はある。upscale=1 を回帰対象に含めること(docs/internal/regression_baselines.json は
upscale ケースを持っていない)。upscale=1 は t2va 専用(fl2va は
明示拒否・ref2va は経路なし)で、低VRAMモード(H3_LOWVRAM=1/group)では 400 拒否。
つまり「VRAM を節約したい」動機が最も強い場面では、そもそも選べない。上記のとおり直接生成の下位互換なので、チェックボックスを UI から隠した。機能自体は
残してある — upscale=1 を API に直接渡せば従来どおり動くので、「直接生成では VRAM が
足りず upscale なら収まる」幅3GBの隙間には引き続き使える。
表示の可否は static/index.html の UPSCALE_UI_VISIBLE(既定 false)一箇所で決まる。
markup 側の style="display:none;" だけ外しても updateStillControls() がモード切替のたびに
display を上書きするので戻らない — 戻すときはこのフラグを true にすること。
警告は実測ベースの条件付きに変更: パス2 が 4 未満になるステップ数なら具体的な推奨値
(12steps)を示し、turbo なしならモザイクの既知問題を知らせる。
記録(検証の教訓2つ): (1) 修正直後に 1536² の静止した中央フレームだけを見て 「破綻なし」と誤報した — 動画の破綻は動いている部位に出るので、連続フレームを 並べて動く部位を見ること。(2) 「ステップを増やしても消えない」も誤りだった — 30steps の対照は turbo の有無という別の変数も同時に変えていたため、別種の破綻を 同一視した。1変数ずつ振ること(B11 の解像度と同型の教訓)。
| 1344×768 を得る方法 | ピーク | 48GB 級予算 ~49.8GB | 写実系の細部 |
|---|---|---|---|
| t2va を直接 1344×768 で生成 | 47.79GB | 入る(余裕 ~2GB) | 良い(基準) |
| 672×384 → upscale で 1344×768 | 44.67GB | 入る(余裕 ~5GB) | -25%(ぼやける) |
| 1536² を狙う場合 | 55.83GB | 入らない | — |
訂正(2026-08-13): この表は当初「t2va 直接 = 49.7GB 級・ほぼ余裕なし」と書いていたが 誤りだった。49.73GB は ref2va(参照トークンぶん重い)の実測値で、t2va 直接は 47.79GB で 48GB 級に収まる。参照系と非参照系の数値を取り違えていた。
ただし upscale=1 は t2va 専用(fl2va は明示拒否、ref2va は経路なし)で、低VRAM
モード(H3_LOWVRAM=1/group)では 400 拒否のまま。よって ref2va で 1344×768 を
出したいなら、引き続き TE を2枚目GPUへ(46.36GB)が答え。
upscale=1 は t2va 専用で、fl2va は明示的に ValueError、ref2va にはそもそも
経路が無い(still=1 との併用も拒否)。さらに 低VRAMモード(H3_LOWVRAM=1/group)では
400 で拒否される(パス2の系列長が約4倍になるため未検証扱い)。したがって
「小さく生成して upscale」は参照系の VRAM 対策にはならず、TE を2枚目GPUへ逃がすのが
引き続き正解(前節の実測: 49.73GB → 46.36GB)。
upscale が「回帰セット外だったため4日間壊れたまま」だった事故を受けて、全エンドポイント・ 全経路の監査を実施した(等価性アンカー2件・実行テスト16件・静的精査3件)。
バグ1: /api/settings/apply が turbo 既定 ON のプロセスで一律 400(core/settings.py)。
comfy 形式 LoRA の非互換ガード(2026-08-06)を、diffusers ネイティブ形式の解禁(08-08)の際に
validate_instant_settings と UI 用 constraints では形式条件付きに緩和したのに、
apply 側の1箇所だけ条件を付け忘れていた。int8 では transformer_both_resident が常に
真になるため、あらゆるフィールドの適用が拒否され、リロード設定 UI が実質使用不能だった
(<d> のために案内している「投影TEをOFF」もこの経路 — 案内が機能していなかった)。
他2箇所と同じ turbo_lora_expected_format() == "comfy" 条件を追加して修正。
バグ2: リロードパネルの bool 項目が UI から OFF にできない(static/index.html)。
バグ1の修正を検証中に発覚。UI は OFF を空文字で送るが、FastAPI の Optional[bool] は
空文字を None=「変更なし」に解釈するため、te_proj/te_prune/video_vae_fp16 の OFF は
黙って無視されていた。'0' を送るよう修正。
修正後の検証(往復 E2E): te_proj OFF 適用 → 32B TE で <d>[Japanese] こんにちは</d>
入り t2va が受理されて完走 → te_proj ON へ復帰 → 通常生成 8.03s。案内していた
<d> の運用がこれで実際に機能する。
ref2va_batch + TE@GPU1 が OOM — ただし GPU1 には別サービス(ace-server、約10GB)が常駐して
おり、純粋な容量不足。_te_external_usable_for() の閾値判定は「TE用GPUの総容量」だけを
見ており、他プロセスの使用量を見ないため、同居環境では判定を通過してから実行時に OOM する。
実測空きを見る判定への変更は設計判断が要るため未実施(参照枚数で必要量が変わり、固定の
床値では1枚参照の成功ケースを誤って弾く)。
f3622b6d)・旧 v0.1 LoRA 後方互換(596a718e)現行9ケースは 48GB機 + 32B TE(cuda:1)+ v0.1 LoRA 前提で、96GB機では 32B TE が 16GB の cuda:1 に載らず全件再現不能。さらに監査で判明した空白: fl2va・複数参照・ 音声参照・mute・upscale・投影TE経路(現在の実用構成なのに1件も無い)・both_resident 構成・ settings/apply 往復・旧LoRA後方互換・バッチ×turbo。基準セットの作り直しは別途(方針判断待ち)。
前日の監査で「既存の回帰基準 9ケースは 48GB機+32B TE+旧LoRA 前提でこの箱では全件再現 不能、しかも fl2va/投影TE/mute/upscale 等が空白」と判明したのを受けて、96GB機向けの 新基準セットを作成した(既存ファイルは 48GB機用としてそのまま温存)。
docs/internal/regression_baselines_dev96.json — 3グループ16ケース。
A: 実用最速構成(int8 単騎+投影TE+turbo v1.0 768p)で t2i/t2va/fl2va(片端・両端)/
ref2i/i2va/複数参照/音声参照(尺自動導出 175フレームのチェック付き)/mute(音声
ストリーム0本 + mute有無で PNG 一致)/upscale 12steps/<d> 拒否メッセージ。
B: 旧 v0.1 LoRA 後方互換。C: H3_LOWVRAM=1 のバッチ2種(場面ごと MD5)。scripts/run_regression.py — サーバのライフサイクルごと管理する汎用ランナー
(グループごとに pkill → 当該 env で起動 → HTTP 実行 → MD5 比較、--record で採取)。
ケース追加は JSON 編集だけで済む。全 PASS で exit 0。f3622b6d 基底経路 / 596a718e 旧LoRA互換)とも一致し、新旧基準の
連続性を確認。使い方: venv/bin/python scripts/run_regression.py(全グループ実行)。
この節は追従前に書いた影響調査の記録。実際の追従結果は上の「マージ版 (f37ab93) への追従 第1段/第2段」を参照(全経路が同一seed MD5 で等価、main へマージ済み)。 以後 diffusers を上げるときの手順の雛形として残してある。
PR #14355 は 2026-08-05 17:00Z にマージされた(merge commit f53d552、PR最終
head は f37ab93)。venv は f37ab93 に更新済み。H3 を含む安定版リリースは
まだ無い(最新 v0.39.0 は 7/3、H3 は次の v0.40.0 から)ので、引き続き SHA 直指定で
ピン留めする。
abc5e9b → f37ab93 の差分は 27コミット・70ファイルで、懸念どおり
8ab3662(review & refactor、#14371)による大規模リファクタを含む。2026-08-09 に
runner.py の全 diffusers 接点(import 15モジュール・シンボル約40個)をマージ版ソースと
突き合わせた影響調査の結果:
即死する箇所(ImportError、6 import サイト):
packing.py / packing_ref2va.py は削除された。runner が import している
MINIMAX_H3_TEXT_ENCODER_LAYER(→ components.text_encoder_layer プロパティ化、既定50)、
MINIMAX_H3_TEXT_TAG(→ modular_pipeline.py へ移動)、MINIMAX_H3_KEYFRAME_NOISE_AUG /
MINIMAX_H3_MIN_DURATION(→ components.keyframe_noise_aug / .min_duration
プロパティ化 — 「モジュール定数の monkeypatch」方式が通用しなくなる)、
build_packed_sequence / build_row_timesteps / patchify_video_latents
(→ before_denoise.py へ移動)、unpatchify_video_tokens(代替なし —
hires 経路が使うので自前実装の持ち込みが要る)、build_ref2va_presentation /
reference_kind / sample_reference_video_frames(代替なし — references.py の
クラス階層 MiniMaxH3ImageReference/VideoReference/AudioReference に再設計された)MiniMaxH3SetupStep → 消滅(MiniMaxH3ResizeStep に再編)、
MiniMaxH3AutoKeyframeVaeEncoderStep → MiniMaxH3KeyframeVaeEncoderStep /
MiniMaxH3AutoVaeEncoderStep に再編、
MiniMaxH3TextEncoderStep.encode_prompt(staticmethod)→ モジュール関数
get_qwen3vl_prompt_embeds に変更(シグネチャも変更)、
MiniMaxH3Ref2VABlocks → 公開APIから消滅(単一の MiniMaxH3Blocks +
MiniMaxH3AutoDenoiseStep の条件分岐に統合 — pipe/pipe_ref の2シェル設計に影響)生き残った箇所: 主要ステップ類(MiniMaxH3SetTimestepsStep / MiniMaxH3LoopDenoiser /
MiniMaxH3DenoiseStep / decode 2種 / Ref2VA 系 denoise・encoder)は同名で存続。
row_timestep_plan の state キーも存続。decoders.py の latents 正規化
(latents * latents_std + latents_mean、H3_TE_DEVICE 実装が依存)も同形で存続。
diff レベル監査での追加判明事項(2026-08-09、Sonnet エージェントによる全行監査):
_decode は byte-identical。
→ 同一seed MD5 一致の可能性は残っている。回帰はまず MD5、不一致なら PSNR+目視MiniMaxH3VideoDecodeStep は packed
シーケンス行を受けて内部で unpatchify していたが、新版は新設
MiniMaxH3AfterDenoiseStep が unpatchify を担い、decode ステップは unpatchify 済み
5Dテンソルを受ける契約に変更。generate / generate_still_batch / generate_ref2va /
generate_ref_batch 全4関数の decode 直前に AfterDenoiseStep 相当の挿入が必要build_packed_sequence / build_row_timesteps は before_denoise のステップクラスの
@staticmethod へ移動しただけでなくシグネチャも変更(audio_channels /
audio_tag / video_tag が必須引数化)— hires 経路 _upscale_block_state_2x は
全面書き直しModularPipeline.from_pretrained(MODEL_ID, workflow="ref2va") の
workflow 機構に統合(pipe/pipe_ref 2シェル設計の前提が変わる。transformer_ref
というコンポーネント名の存続も要確認)。_execution_device の解決アルゴリズム
(components 挿入順の最初の nn.Module)自体は維持encode_prompt は t2va 側・Ref2VA 側とも staticmethod ごと消滅(fl2va は
MiniMaxH3FL2VATextEncoderStep に分離)。runner が @torch.no_grad() を自前で
制御するために直接呼んでいた経路なので、新モジュール関数
get_qwen3vl_prompt_embeds ベースで同じ最適化を組み直す追従の作業量見積もり: 大(当初「中〜大」から上方修正)。改名追従だけでは済まず、 (a) import 参照先の総付け替え、(b) decode 直前への AfterDenoiseStep 挿入×4関数、 (c) encode_prompt 直接呼び出しの作り直し×2、(d) 定数 monkeypatch → プロパティ override 化、(e) hires 経路の全面書き直し、(f) ref2va 2シェル設計の workflow 機構への 再設計、(g) KVプレフィックスキャッシュの references.py ベース再実装、が必要。 土台の数値経路が無変更なので、移行後の per-機能 MD5/PSNR 回帰で等価性を証明できる 見込みは高いのが救い。
当面の方針: 現構成(abc5e9b、全機能A/B実測済み)を据え置き。追従するなら 最小差分の f37ab93 を対象に、(1) t2i/t2va 経路 → (2) ref2va 経路の2段階で。
なお int8 レシピ(TorchAoConfig + Int8WeightOnlyConfig)は abc5e9b に既に含まれて
おり、マージを待たずに使えている(H3_TRANSFORMER_QUANT=int8 として実装済み)。
H3_TURBO_LORA=1 の配線は完成済み(上記セクション参照)。現行LoRA(Ostris氏)は
「デモ/プレビュー、学習途上」と明記されているため既定OFF、かつint8/低VRAMでは
使用不可(融合QKV前提 → Int8Tensor に aten.cat が無い)だった。
2026-08-08、代替候補 lightx2v/Minimax-h3-Turbo
(minimax_h3_fl2v_turbo_4step_v0.1.safetensors、1.38GB、Apache 2.0、DMD蒸留。
Kijai/MiniMax-H3_comfy はこれのComfyUIリパック)をスパイクし、
48GB(int8)でも動くこと・4stepsで実用品質になることを実測で確認した
(scripts/probe_lightx2v_turbo.py、本体コードは無変更)。
決定的な違い: キーが diffusers ネイティブ。safetensors ヘッダを直接読んで確認した
ところ、キーは transformer_blocks.N.attn.to_q.lora_A.default.weight のように
to_q/to_k/to_v が分離しており(rank 128、312モジュール = 50ブロック×6 +
token_refiner 2ブロック×6、attn と ff のみで adaln/final_layer は含まない)、
fuse_projections() が不要。torch.cat を一切呼ばないので、Ostris版を阻んでいた
int8 非互換の主因を踏まない。実際 int8 の transformer に312モジュール全てを0.6秒で
適用でき、例外は出なかった。ff.net.0.proj の lora_B は (28672, 128) で
diffusers の SwiGLU(ゲート込み dim_out*2)と完全一致し、このLoRAが diffusers の
モジュール構成そのものに対して学習されたことを裏付ける。
罠(最重要): strength は Kijai 記載の 0.75 ではなく ~0.094。生の B·A に直接
掛ける本実装の経路では、0.75 は強すぎて 30 steps でも出力が完全にノイズ化する
(ステップ数の問題ではないことを 30steps で実証)。ComfyUI 側は alpha を折り込んで
適用するため、0.75 × (alpha/rank) = 0.75 × 16/128 ≈ 0.094 が対応値、という仮説が
実測と一致した。
| strength | 4steps の結果 | 音声 rms / peak |
|---|---|---|
| 0.75(Kijaiの記載値をそのまま) | 完全にノイズ(30stepsでも同じ) | 0.083 / 0.43 |
| 0.15 | 良好(背景がやや軟らかい) | 0.069 / 0.79 |
| 0.10 | 良好 | 0.065 / 1.05(クリップ) |
| 0.094(= 0.75 × 16/128) | 最良(毛並み・松葉までシャープ) | 0.039 / 0.70 |
速度(RTX PRO 5000 48GB + H3_LOWVRAM=1、768²・5秒・seed 12345・同一プロンプト):
デノイズ 197.7s → 26.1s(7.6倍)、総所要 351.4s → 135.2s(2.6倍)。総所要の
短縮率が小さいのは低VRAMモードのロード固定費(~110s)が残るため — バッチ経路と
組み合わせればここも償却できる。時間方向の整合性も4フレーム抜き出しで確認済み(破綻なし)。
残る懸念: 音声が基準比で約5倍大きい(基準 rms 0.0073 → 0.039)。スペクトル平坦度は
0.24 と基準(0.34)より低く白色雑音ではない(構造がある)が、強度によっては peak が
1.0 を超えてクリップする。実装するならレベル面の確認が要る。また token_refiner は
int8 の除外リストに入っているため、transformer_blocks は int8 ベース + bf16 デルタ、
token_refiner は bf16 ベース + bf16 デルタという混在状態になる(適用・生成とも成功
しているので実害は観測されていない)。
2026-08-08 本実装済み(「Turbo LoRA」節の更新参照): (1) diffusers ネイティブ用の
適用関数 + キー形式自動判別、(2) _TurboLoRALinear の scale 係数(形式別既定
1.0/0.094)、(3) 併用ガードの形式ベース化(comfy×int8 は拒否のまま、group は形式を
問わず拒否)。(4) ref2va への適用可否のみ未検証のまま。
H3_TE_PREQUANT、
2026-08-08。TEロード 53.0s→29.5s、リクエスト合計 -35%)。残るのは transformer int8
で、保存に約34GB を要しこの箱のディスク空き(43GB)では非現実的。ディスクに余裕の
ある機なら、同じ手法で transformer のロード(約32.5s)も削れる見込みH3_TE_DEVICE、
定常 78.4s→約35s = -55%。上記「text_encoder を別GPUへ常駐」節を参照)。以下は
スパイク時の記録: TE を別カードへ
逃がせば GPU0 に transformer を常駐させたままにでき、固定費が丸ごと消える。
実測では現行の RTX 4000 SFF Ada 20GB で t2va は成立(peak 17.76GB/余裕 3.23GB)
だが ref2va は OOM(2048px の参照を vision tower に通すため 1〜2GB 不足)。
ref2va まで含めるなら 24GB 級が要る。PCIe が Gen4 x4 でも問題にならない
(TE は起動時に一度載せるだけ、毎リクエストの転送は prompt_embeds の 42MB のみ)。
実装には _execution_device 周り(本プロジェクト最大の罠所)の慎重な改修が必要
(scripts/probe_te_on_second_gpu.py)ref2va × turbo は未検証: turbo LoRA は transformer(t2va系)にのみ適用を実測して
あり、transformer_ref への適用は一度も試していない。配線上は同じ経路を通るはずだが、
蒸留LoRAが参照条件付きの軌道でも成立するかは別問題(strength 0.094 の妥当性も t2va での
実測値)。参照付き生成を高速化したくなったら、まずここをスパイクすることH3_TE_PROJ) の4B専用量子化オプション → 実装済み(2026-08-10 同日):
H3_TE_PROJ_QUANT として追加し、実測(NF4 3.11GB・品質同水準)を経て既定を
bnb-4bit 化済み(日付節 2026-08-10 の「追記(同日その2)」参照)。以下は実装前の
検討記録: 当初の投影TEは Qwen3-VL-4B を
bf16 でロードしており、GPU実占有は 8.88GB(2026-08-10 実測。チェックポイント
8.88GB と一致)。投影行列の配布元は「15.7GB → 5.2GB」と書いているが、5.2GB は
bf16 では成立しない(量子化版を指していると思われる)。4B を int8/nf4 で量子化
できれば 4〜5GB 級になり、2枚目GPUの要件がさらに下がる。
注意: 現在の実装は H3_TE_PROJ と H3_TE_QUANT/H3_TE_PRUNE/H3_TE_PREQUANT を
import 時ガードで排他にしている。これは「32B TE 向けの設定を4Bへ流用させない」
ための措置であって、4B を量子化してはいけないという意味ではない。実装するなら
4B専用の別フラグ(例 H3_TE_PROJ_QUANT)として足し、既存ガードの意図を壊さないこと。
投影行列は fp32 のまま適用する必要がある点にも注意(W は 2560×5120 の fp32)H3_LOWVRAM=group の
同居で実 RTX 4060 Ti 16GB 単体での全機能動作を実測済み(TEのストリーミング実行は
不要だった)。8GiB×2 も t2va まで成立。日付節 2026-08-11 参照。以下は達成前の検討記録:
TEのストリーミング実行(ブロック単位でGPUへ流す)が必要。現状の床は
TE-nf4削除版の常駐17.45GB(上記セクション参照)。投影TEを使う場合はこの床が
8.88GB に下がるため、別ルートで 16GB 級に到達できる可能性がある(下記の2枚目GPU
要件の再計算を参照)このリポジトリのコードは Apache License 2.0(LICENSE)。 モデル重みは含まれない。
使用するモデル/重みのライセンスは別途それぞれに従うこと:
| 対象 | ライセンス |
|---|---|
| MiniMax-H3 本体の重み | MiniMax Community License(非商用無料、商用は年商$20M未満まで、要クレジット) |
Turbo LoRA(larryvrh/MiniMax-H3-Turbo-Lora) | Apache-2.0 |
| Qwen3-VL-32B(text_encoder) | Qwen 公式のライセンスに従う |
| diffusers / transformers / torchao / SageAttention 等 | 各パッケージのライセンス |
ローカルLLM(gemma4-31B、OpenAI互換 /v1/chat/completions)で、入力プロンプトを
H3公式ガイドの形式へ整形する。クラウド版Hailuo AIの内部プロンプト整形層のローカル再現。
H3_LLM_URL(既定 http://127.0.0.1:64650)。接続不可時は502
(生成機能には影響しない)POST /api/prompt/enhance {text, mode, seconds, task, lang}storyboard(既定): マルチショットのCUTタイムコード形式へ展開(総尺=seconds、2〜3カット、
ハードカット・被写体同一性維持・カット毎の音指示。焦点距離は35/50/65/100mmに制限)。
これはH3公式ドキュメントの記法ではなく本アプリの独自案(下記 h3-official 参照)brief: 公式ブリーフ形式(シーン→被写体→アクション→カメラ→音→終わり方)の単一ショット詳細化h3-official(2026-08-07追加): MiniMax公式スキル h3-prompt-writing
(MiniMax-AI/MiniMax-H3 リポジトリの skills/h3-prompt-writing/)のフィールド構造・
記法へ厳密準拠。task(UIの現在のタブ = t2va/fl2va/ref2va)で参照ガイドを切替える:
t2va/fl2va は references/base-en.txt の3フィールド形式
(integrated_multimodal_description / overall_soundscape / non_diegetic_music、
[Shot N] At MM:SS.SS のカット記法・<d>[言語] ...</d> の台詞逐語保持)、ref2va は
references/ref-en.txt の6フィールド形式(subject_definitions / summary /
retention_analysis / detailed_description / overall_soundscape /
non_diegetic_music)。出力は公式既定の英語(lang=en)、lang=ja で書き換え本文のみ
日本語化も可能(フィールド名・[Shot n]等のラベル・タイムコード記法・<d>内の台詞は
公式ルールどおり英語/原語のまま)。リファレンス本文は本リポジトリに同梱しない
(ライセンス表記が無いリポジトリのため)。venv/bin/python scripts/fetch_h3_skill.py
で skills_cache/(.gitignore 済み)へ事前取得しておく必要があり、未取得時は
400+取得コマンド案内のエラーになる。システムプロンプトはSKILL.md+参照ガイド全文を
要約せず投入(約18.8KB=t2va/fl2va、約26.6KB=ref2va)。translate: 過剰創作なしの英訳(CUT構造は保持)h3-official は2026-08-07追加時点でLLMサーバ未接続のため生成A/Bのみ実施(下記参照)、
LLM経由での実応答確認は別途必要87 commits
29 commits
Python
84.3%
HTML
15.5%
日本語 | English
MiniMax H3 (Hailuo 3.0) — 動画+ステレオ音声を1回のデノイズで同時生成する 33B オムニ モーダルモデルを、普及帯GPUで動かす検証アプリ。 テキスト / 画像参照 / 音声参照を入力に 動画+ステレオ音声を生成し、静止画のみ(T2I / Ref2I)も出せる。基盤は diffusers の Modular Pipeline(PR #14355、コミット f37ab93 にピン留め)。8GB×2 の2枚構成でも、 実 RTX 4060 Ti 16GB カード単体でも動くことを実測済み(下の対応表)。将来 diffusers-server へ統合するための先行検証 ワークスペース(diffusers-server 本体には一切手を入れていない)。

ブラウザから使う単一ページUI。5つのモードをタブで切替(動画: T2VA=テキスト / FL2VA=フレーム / Ref2VA=参照、静止画: T2I / Ref2I)。下段は生成物ギャラリーで、 チェックした動画を連結して書き出せる。プロンプトはローカルLLMで H3 公式ガイドの形式へ強化でき、 FirstBlockCache・Sage・Turbo・量子化・低VRAMモードなどは再起動なしでこのパネルから切替。 日英切替あり。
○ = 実測で完走 / △ = 導出見込み(未実測)/ × = OOM。時間は定常値(初回のモデル ロードを含まない2回目以降)、ピークは torch 計測の割当ピーク。速度はいずれもこの箱の PCIe Gen3 x4 スロット + sm_89(4060 Ti)+ SDPA での値で、Gen4 x16 なら重み転送は 約1/8になる(下の「速度の正直な注記」参照)。16GB/8GiB 行は 30ステップ、 48GB+20GB 行は turbo 4ステップ。
| 構成 | t2i 768² | t2va 5秒 768² | ref2i | i2va(画像参照) | 音声参照 | 768×1344 5秒 |
|---|---|---|---|---|---|---|
| 実 RTX 4060 Ti 16GB 単体 | ○ 498s・ピーク7.4GB | ○ 25分・11.4GB | ○ | ○ 39分・9.41GB | ○ 54分・11.96GB | ○ 66分・13.37GB(実15.2GB=上限) |
| 8GiB×2(計算+TE、バラスト模擬) | ○ 512s・6.4GB | ○ 25.6分・7.23GB | ○ 17.7分・6.69GB | × OOM | × OOM | × OOM |
| 12GB 単体 | △ 見込み | △ | △ | △ | ×に近い(実14.7GB) | × |
| 48GB+20GB(2枚、turbo LoRA) | ○ 9.7s | ○ 44.2s | ○ | ○ | ○ | ○ |
H3_LOWVRAM=group(transformer int8 をブロック単位で
ストリーム、GPU常駐は ~1.4GB)+ video VAE の fp16 デコード。詳細は日付節 2026-08-10〜11。セットアップは「インストール」「モデルの取得」を先に済ませること。
起動後、ブラウザで http://<host>:8611/ を開く。
# 実16GB 単体(sm_89 の 4060 Ti など)
H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 \
H3_ATTN_BACKEND=default \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
# ※ sm_120 以外は H3_ATTN_BACKEND=default が必須(既定の sage は sm_120 専用ビルド)
# 8GB×2(TE を2枚目GPUへ)
H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 \
H3_ATTN_BACKEND=default H3_TE_DEVICE=cuda:1 \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
# 48GB級(推奨: 高速化フル)
H3_LOWVRAM=1 H3_TE_PRUNE=1 H3_TE_DEVICE=cuda:1 H3_VIDEO_VAE_FP16=1 H3_KEEP_TRANSFORMER=1 \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
# 96GB級(全モデル常駐、env なし)
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
より細かい VRAM 級別(80/48/32/18GB)の起動例と全環境変数は 「VRAM対応表と主な環境変数」を参照。
このリポジトリの中心的な発見: 律速はデノイズではなく「モデルの load/free という固定費」 だった。 以下は各節の実測値を1か所に集めたもの(再測定ではない)。
48GB機(PRO 5000 48GB + RTX 4000 20GB)・H3_LOWVRAM=1 系・768²・定常値
(初回のモデルロードを含まない2回目以降。t2va は 5秒 = 124フレーム)。
| 段階 | t2i 1枚 | t2va 5秒 |
|---|---|---|
| 素の構成(30steps、turbo なし) | 157s | 351.4s |
| + lightx2v turbo LoRA(4steps) | 157s | 143s |
+ H3_TE_PREQUANT(量子化TEのディスクキャッシュ) | 83.2s | — |
+ H3_TE_DEVICE(TE を2枚目GPUへ) | 約35s | 60.5s |
+ H3_KEEP_TRANSFORMER(transformer 常駐) | 9.7s | 44.2s |
| 16倍 | 8.0倍 |
デノイズ自体を速くする turbo だけでは 2.6倍で頭打ち。残りの3倍強は すべて固定費の撤廃から来ている(turbo なし 30steps のままでも、固定費撤廃だけで t2i は 157s → 51.1s と3倍速い)。
構成: int8 transformer 常駐 + 投影TE(NF4)+ デコード窓の解放停止 + turbo 4steps + sage +
fp16 デコード、GPU 1枚(96GB機で実測。H3_TRANSFORMER_QUANT=int8 H3_KEEP_TRANSFORMER=1 H3_VIDEO_VAE_FP16=1 H3_TE_PROJ=… H3_TURBO_LORA=1)。
| モード | 単発(最速) | 連番なら1件あたり | デノイズ | ピーク | 時間を食っている残りの固定費 |
|---|---|---|---|---|---|
| t2i 768² | 7.40s | 7.9s(0.94倍 = 使う意味なし) | 2.40s | 45.0GB | VAE の CPU↔GPU 往復 ~3.3s |
| t2va 5秒 768² | 28.13s | (バッチAPIなし) | 14.94s | 45.6GB | デコード 7.05s |
| ref2i 768²(参照付き静止画) | 79.3s | 47.0s(1.69倍) | 7.8s | 45.4GB | 参照ビジョンエンコード ~47s |
| i2va 5秒 768²(画像参照→動画) | 103.1s | 75.0s(1.37倍) | 22.0s | 45.9GB | 同 ~47s + t2va transformer の再ロード ~13s |
計測条件(この表の数値の読み方):
すべて定常値。サーバ起動時の初回ロードは含まない(2回目以降のリクエスト)。 起動時に transformer + VAE を常駐させるのに 約50秒かかるが、これはプロセスに1回だけ。
参照系(ref2i / i2va)は初回リクエストだけ別: transformer_ref は起動時ではなく
最初の参照リクエストでロードされるため、初回のみ +55秒(ref2i 実測: 初回 134.7s →
定常 79.3s)。以後は常駐するので定常値になる。
解像度 768×768 固定。動画は 5秒 = 124フレーム(24fps)、静止画は 22フレーム。
turbo LoRA の 4ステップ(H3_TURBO_LORA=1)。FirstBlockCache は turbo により自動 OFF。
attention は sage。seed とプロンプトは各モードで固定。
「連番なら1件あたり」= 同じ参照/設定で複数件まとめて作るときの1件あたり
(/api/t2i_batch, /api/ref2i_batch, /api/ref2va_batch)。実測は t2i/ref2i が3場面、
i2va が2場面。バッチ経路は H3_LOWVRAM=1 専用なので単発列とは構成が違う —
つまり「1件だけ作る最速手段」と「N件作る最速手段」を並べた表。
全モードが 45〜46GB に収まる = 48GB カード1枚で全機能が最速級で動く。 ただし t2va 系と参照系を同じプロセスで混ぜると両方の transformer が常駐して 74.3GB になる(実測。t2va に戻ったときのピークは 77.3GB)。48GB 1枚で運用するなら プロセスをモード別に分けること。
参照系の律速はデノイズではなく参照のビジョンエンコード ~47s。turbo の効きが t2va(5.5倍)より小さく i2va で 2.8倍に留まるのはこのため。
2枚構成 + bf16 transformer なら t2i 6.89s / t2va 26.8s がこれまでの最速だが、 77GB 級のカードが要る(差は 7% 程度)。詳細は日付節 2026-08-12。
47×(場面数-1)/場面数。i2va なら 2場面 1.37倍 →
3場面 1.44倍 → 5場面 1.57倍。同じキャラクターで長い物語を作る用途ほど有利。H3_LOWVRAM=1 専用で、それ以外では黙って逐次実行に落ちる。
参照バッチは H3_TE_DEVICE と併用不可(「TE用GPUに 24GB 以上」のガード。32B TE の
vision 活性化を前提にした閾値で 3.11GB の投影TEには過大 — 16GB の 4060 Ti が弾かれる)。
どちらも要見直し。計測の落とし穴(2026-08-12 に実際に踏んだ): バッチ側だけ
height/widthを渡さずに 単発と比べると、バッチはサーバ既定の 16:9 キャンバス(1344×768)で生成されるため 1.75倍のピクセル数になり、「バッチはステップが 1.75 倍遅い」という存在しない現象が 見える。ピクセル比 1344×768÷768² = 1.75 と実測のステップ時間比 1.753 が一致したことで 判明した。モード間・経路間の比較では解像度を必ず明示すること。
低VRAM構成は動くが遅い。 H3_LOWVRAM=group は毎ステップ int8 重み(~34GB)を CPU→GPU に
流すため、転送が律速になる。上の対応表の 16GB / 8GB の実測値は PCIe Gen3 x4 スロットでの
もので、16.5s/step(t2i)〜51s/step(t2va)のほとんどは転送時間。Gen4 x16 のまともな
スロットなら転送は約1/8になるので、これは「16GBカードの性能」ではなく「この箱のスロット」の値。
また int8+SDPA の軌道では FirstBlockCache が効かず(cache_skipped_steps: 0)、閾値調整で
最大2倍の短縮余地がある(未検証)。
品質については、投影TE(PSNR 22.4dB vs 32B)も int8+SDPA も、同一seedでも構図が変わる (軌道の分岐であって劣化ではない)。むしろ実測では int8+SDPA の方がプロンプト忠実度が 上がったケースもあり、参照(ビジョン)経路も目視で 32B 正解と同水準の忠実度を確認済み。 構成をまたぐ品質は PSNR/MD5 では判定できないので目視で見ること。
H3_VIDEO_VAE_FP16 × 参照ありの dtype 即死を修正(エンコード側にも fp16 autocast)。作業を再開するとき、まずここを読む。
| 知りたいこと | 見る場所 |
|---|---|
| 仕様・性能・設計(何をどう実現しているか) | docs/TECHNICAL_OVERVIEW.md |
| 踏んだ罠・失敗・運用の教訓(同じ穴を避けたい) | docs/internal/TECHNICAL_REPORT.md |
| コミュニティ改良の取り込み判断 | docs/COMMUNITY_IMPROVEMENTS.md |
| どのモードで何がいつロード/解放されるか | docs/RESIDENCY.md |
| 次に何をやるか(未着手・未検証) | 本 README の「今後の外部イベント待ち」§3 |
| diffusers を上げるときの回帰基準値 | 96GB機: regression_baselines_dev96.json(16ケース、scripts/run_regression.py で実行)/ 48GB機: regression_baselines.json |
現在の状態: diffusers はマージ版 f37ab93 にピン留め(PR #14355 の追従完了、全経路が
同一seed MD5 で等価)。低VRAMの最終ゴール2つ(実16GB単体 / 8GB×2)を 2026-08-11 に達成し、
訴求点は「高速化」だけでなく「普及帯GPUで動く」に広がった。48GB機での推奨起動は
H3_LOWVRAM=1 H3_TE_PRUNE=1 H3_TE_DEVICE=cuda:1 H3_VIDEO_VAE_FP16=1 H3_KEEP_TRANSFORMER=1
(t2i 定常 9.7s/枚、t2va 5秒 44.2s)。ref2va を使うときは H3_TE_DEVICE を外す
(TE用GPUが 20GB では容量不足で自動 400 拒否)。時系列の作業記録は本 README の日付付き
セクション群(下へ読み進む)。
測定環境について: この home は2台のマシンで共有されている。96GB機 = RTX PRO 6000 Blackwell 96GB + 増設した RTX 4060 Ti 16GB(低VRAMゴールの実測はこちら)、48GB機 = RTX PRO 5000 Blackwell 48GB + RTX 4000 SFF Ada 20GB(48GB級の推奨構成・turbo の実測はこちら)。 各日付節の数値がどちらの箱のものかは節内に明記してある。48GB 単騎では既定モード (bf16 transformer 66.3GB)は物理的にロードできないため、
H3_LOWVRAM=1(48GB級)かH3_LOWVRAM=group(24-32GB級)が必須。
minimax-h3/
├── app.py # FastAPI 本体 (port 8611)
├── core/
│ └── runner.py # ModularPipeline のロード/生成ロジック本体
├── static/
│ └── index.html # 単一ページUI (日本語)
├── scripts/
│ ├── download_t2va.py # T2VA検証に必要なサブフォルダのみをDLするスクリプト
│ └── probe_t2va.py # UIより先に動作確認する回帰スクリプト
├── outputs/ # 生成物 (.gitignore対象)
├── logs/ # ダウンロード監視ログ等 (.gitignore対象)
└── venv/ # 専用venv (.gitignore対象、下記参照)
| 要件 | |
|---|---|
| GPU | 最小: 16GB 単体(全機能・実測済み・遅い)または 8GB×2(t2i/t2va/ref2i まで)。快適動作は 48GB 級を推奨。構成別の実測は冒頭の「VRAM×機能マトリクス」参照 |
| ホストRAM | 64GB 以上を推奨(H3_LOWVRAM=group は int8 重み ~34GB をRAMに常駐させるため 48GB 以上の空きが必要) |
| ディスク | 約 145GB(T2VA/FL2VA のみ)/ 約 207GB(Ref2VA も使う場合) |
| CUDA | 12.8 系(torch 2.9.0+cu128 に合わせる)。SageAttention をビルドするなら nvcc も同系統 |
| Python | 3.12 |
| その他 | ffmpeg(ギャラリーの連結・情報取得に使用。無くても生成自体は動く) |
git clone https://github.com/animede/Diffusers_minimax-h3.git
cd Diffusers_minimax-h3
python3.12 -m venv venv
# PyTorch (cu128)
venv/bin/pip install torch==2.9.0 --index-url https://download.pytorch.org/whl/cu128
# diffusers は「検証済みコミットに固定」して入れる(下の注意を必ず読むこと)。
# マージ版 f37ab93(PR #14355 の最終形)が前提。全経路(t2i/t2va/バッチ/ref2va/refバッチ)
# を旧ピン abc5e9b と同一seed MD5 で回帰済み(末尾「追従 第1段/第2段」節)。
venv/bin/pip install "git+https://github.com/huggingface/diffusers.git@f37ab93e621d5ce206c9662e8291ca8b67d9c555"
# transformers は 5.14.1 以上が必須
# (5.1.0 には Qwen3VLProcessor.create_mm_token_type_ids が無く、PR #14355 のエンコーダが動かない)
venv/bin/pip install "transformers==5.14.1" accelerate==1.12.0 safetensors huggingface_hub
# 動画/音声の多重化と Web API
venv/bin/pip install av==16.0.1 fastapi==0.104.1 "uvicorn==0.24.0" python-multipart pillow numpy
# text_encoder の 4bit 量子化(既定の H3_TE_QUANT=bnb-4bit に必要)
venv/bin/pip install bitsandbytes==0.49.0
# transformer の int8 量子化を使う場合のみ(H3_TRANSFORMER_QUANT=int8 / H3_LOWVRAM)
# 0.18 以降は torch>=2.11 を要求するため 0.17.0 を固定する
venv/bin/pip install torchao==0.17.0
重要: diffusers はコミット固定のまま使うこと。 PR #14355 は 2026-08-05 にマージ済みで、本アプリはマージ最終形 f37ab93 へ 追従完了(第1段: t2i/t2va/バッチ、第2段: ref2va 系)。全経路が旧ピン abc5e9b の ベースラインと同一seed MD5 完全一致で回帰済み(末尾「追従 第1段/第2段」節)。 ここからさらに diffusers を上げる場合も、同じ手順(同一seed MD5 回帰)を踏むこと。
既定の H3_ATTN_BACKEND=sage を使うには sm_120 向けのビルドが要る(Linux 向けの
事前ビルド wheel は存在しない)。ビルドしない場合は H3_ATTN_BACKEND=default を
指定して起動すること(デノイズが約12%遅くなるだけで、機能に影響はない)。
CUDA_HOME=/usr/local/cuda-12.8 scripts/build_sageattention.sh
ビルドは 必ず並列数を制限すること(スクリプトは
MAX_JOBS=4 NVCC_THREADS=2+ systemd-run のメモリ上限付きで実行する)。無制限の並列 nvcc はホストRAMを食い潰し、 システム全体を巻き込んで OOM する。
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
ブラウザで http://<host>:8611/ を開く。初回起動時にモデルをロードするため数分かかる。
MiniMaxAI/MiniMax-H3 は498.6GB(FL2VA/Ref2VAの2チェックポイント + 両方のtransformer分)
だが、T2VA/FL2VA検証には transformer/(FL2VA用、66.3GB)+ text_encoder/(66.7GB)+
vae/(10.4GB)+ audio_vae/(0.6GB)+ 設定類の約144GBのみで足りる。
transformer_ref/(Ref2VA用、66.3GB)と Ref2VA/ FL2VA/ の別パッケージ(各144GB)は
今回不要なので 絶対に丸ごと snapshot_download しないこと。
venv/bin/python scripts/download_t2va.py
内部で allow_patterns を使い、必要なサブフォルダのみを取得する。ダウンロード中は
logs/du_monitor.log でキャッシュサイズを監視できる(170GB超で警告)。
用途に応じた起動例。上段5行は 96GB 機(PRO 6000)でのバラスト実測(32B TE 使用)、 下段3行は 投影TE を使う実カード実測(2026-08-11。詳細は冒頭の 「VRAM×機能マトリクス」と日付節 2026-08-11)。
| GPU | 起動時の指定 | t2va 実測(768²・5秒) |
|---|---|---|
| 96GB | (指定なし=既定) | peak 92GB / 約160秒 |
| 80GB級 | H3_TRANSFORMER_QUANT=int8 | peak 59.7GB / 約160秒 |
| 48GB級 | H3_LOWVRAM=1 | peak 38.9GB / 約215秒 |
| 32GB級 | H3_LOWVRAM=group | peak 28.7GB / 約280秒 |
| 18GB級 | H3_LOWVRAM=group H3_TE_PRUNE=1 | peak 17.7GB / 約280〜320秒 |
| 16GB 単体(実 4060 Ti) | H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 | peak 11.4GB / 約25分※ |
| 12GB 単体(見込み・未実測) | 16GB 単体と同じ | 実ピーク11.4GBが12GiBに入る計算。参照系・768×1344は不可 |
| 8GB×2(バラスト実測) | 16GB 単体の指定 + H3_TE_DEVICE=cuda:1 | peak 7.23GB / 約25.6分※ |
※ 低VRAM実カード列の時間は PCIe Gen3 x4 スロット + sm_89(SDPA)での実測。Gen4 x16 なら
重み転送は約1/8(「速度の正直な注記」参照)。sm_120 以外のカードは
H3_ATTN_BACKEND=default も必要。
# 例: 32GB級GPUで起動する
H3_LOWVRAM=group H3_TE_PRUNE=1 venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
# 例: 16GB カード単体で起動する(sm_89 なら H3_ATTN_BACKEND=default も付ける)
H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
主な環境変数(既定値。多くはUIからも切り替えられるので、恒久的に変えたい時だけ指定する):
| 変数 | 既定 | 意味 |
|---|---|---|
H3_TE_QUANT | bnb-4bit | text_encoder の量子化(none は bf16 で 66.7GB) |
H3_TE_PRUNE | 0 | TE の未使用上位レイヤー削除(出力は不変、-3.6GB) |
H3_TE_PROJ | (無効) | 投影TE: 32B TE を Qwen3-VL-4B+線形写像で代替(repo id か .safetensors パス。品質は近似、日付節 2026-08-10 参照) |
H3_TE_PROJ_QUANT | bnb-4bit | 投影TE 4B の量子化(NF4 で 3.11GB。none=bf16 8.88GB / bnb-8bit) |
H3_TE_DEVICE | (無効) | TE を2枚目GPUへ常駐(例 cuda:1。32B TE は 20GB 級、投影TE なら 8GB 級で可) |
H3_TRANSFORMER_QUANT | none | int8 で transformer を 66.3→34GB |
H3_LOWVRAM | 0 | 1=48GB級のフェーズ循環 / group=32GB級以下の block offload |
H3_KEEP_TRANSFORMER | 0 | transformer 常駐で再ロード固定費を撤廃(成立条件は該当節参照) |
H3_CACHE / H3_CACHE_THRESHOLD | fbc / 0.05 | FirstBlockCache(デノイズ -25%。int8+SDPA 軌道では不発の実測あり) |
H3_ATTN_BACKEND | sage | sage は sm_120 専用ビルド。それ以外のGPUは default(SDPA)必須 |
H3_TURBO_LORA | 0 | 4/8ステップ蒸留LoRA(初回に約780MBをDL。group とは併用不可) |
H3_VIDEO_VAE_FP16 | 0 | video VAE を fp16 化(デコードのピークを削減。16GB 以下では必須) |
H3_LLM_URL | http://127.0.0.1:64650 | プロンプト強化に使うローカルLLM(任意機能) |
このマシンは VRAM 96GB に対し RAM は 94GB。text_encoder は bf16ネイティブ配布で
実測66.73GB(fp32配布を仮定した「bf16化で半分の33GB」という当初推定は誤りだった)。
transformer bf16 66.3GB / vae+audio_vae fp32 計11GB と合わせると約144GBになり、
VRAMにもRAMにも同時に載らない。diffusers の
ComponentsManager.enable_auto_cpu_offload()(全コンポーネントを定常的にRAM常駐させ、
アクティブな1つだけをGPUへ出す方式)は RAM 94GB では成立しないため採用していない。
TEのロード方式は環境変数 H3_TE_QUANT で選択する(既定 bnb-4bit、2026-08-04 に
A/B検証して既定化)。
H3_TE_QUANT=bnb-4bit (既定)text_encoder を起動時に NF4(bitsandbytes、compute_dtype=bf16)へ量子化して GPU常駐のまま維持する(bnb 4bitモデルはデバイス間移動不可のため常駐一択)。 実測サイズ 21.0GB(当初推定の~17-18GBより大きい)。transformer(66.3GB)も常駐し、 リクエストごとの TE⇔transformer 入れ替えが消滅する。
H3_TE_QUANT=none (bf16 TE、旧方式)2つの66GBモデルをリクエストごとにGPU上で入れ替える:
vae + audio_vae(fp32 計11GB)は常時GPU常駐。.to("cpu") でRAMへ退避しない。66GBの
RAM経由はスワップ突入の実測原因になった)。リロードはページキャッシュ/ディスクから
11〜40秒/モデル。リクエスト間の定常状態は transformer+VAE 常駐(77.5GB)。実測のオーバーヘッド: 1リクエストあたり TEロード ~37s + transformerリロード ~26s。
この入れ替えコストを解消したのが上記の bnb-4bit(既定)で、リクエスト合計は
245s → 185s に短縮された(デノイズ157sは共通のモデル律速で不変)。
2つの実装上の罠(実機で踏んで修正済み):
MiniMaxH3TextEncoderStep.encode_prompt は素の staticmethod で、@torch.no_grad()
はブロックの __call__ 側にしか付いていない。直接呼ぶ場合は必ず torch.no_grad()
で包むこと。忘れると autograd グラフが TE の重み約50GB分をGPU上にピン留めし、
モデルを解放してもVRAMが返ってこない(diffusers-server CLAUDE.md 39番と同型)。num_frames・keyframes・latent形状等)は PipelineState に入る。
get_block_state() は宣言された入力しかマップしないので、出力は state.get(名前)
で読むこと。video VAE の decode は diffusers 側の MiniMaxH3VideoDecodeStep が内部で
torch.autocast(dtype=torch.float16) を使うため、重み自体は fp32 のままでよい。
audio VAE は fp32 のまま一切キャストしないこと(bf16化すると生成音声の音量が
約20dB小さくなる既知の問題があるため、runner.py は vae/audio_vae のロードに
明示的に dtype=torch.float32 を渡している)。
| 項目 | 実測 |
|---|---|
| DLサイズ(T2VA必要分のみ) | 135GiB (HFキャッシュ実測) |
| text_encoder ロード | 37.6s (コールド) / 15.9s (ページキャッシュ温) |
| transformer ロード | 37.7s (コールド) / 10〜26s (温) |
| vae+audio_vae ロード | 10.0s |
| プロンプトエンコード | 0.7s |
| デノイズ (30steps) | 157〜159s (約5.4s/step, GPU 100%/600W) |
| VAEデコード (video+audio) | 6.5〜9s |
| ピークVRAM (生成中) | none: 83.4GB (デコード時。デノイズ中70.4GB) / bnb-4bit: 91.7GB |
| リクエスト合計 (サーバAPI経由、ロード込み) | none: 245s / bnb-4bit(既定): 185s |
| RAM | 使用~6.5GBで安定、スワップ増ゼロ |
bnb-4bit のピーク91.7GBは96GBカードに収まるが余裕は約4GB。ヘッドルームを優先したい
場合は H3_TE_QUANT=none で旧方式(ピーク83.4GB、+60s/リクエスト)に戻せる。
H3_CACHE、既定 fbc)ComfyUIコミュニティのEasyCache高速化に相当する、diffusers公式の step間キャッシュ
(FirstBlockCache)を H3_CACHE=fbc(既定)で有効化している。ステップ間で最初の
transformerブロックの残差変化が小さいとき、残りの計算をスキップする。
H3_CACHE_THRESHOLD(既定 0.05): 実測でデノイズ 157s→118s(-25%、30step中7スキップ)、
出力はキャッシュ無しと PSNR 31.8〜34.3dB・音声相関0.979 でほぼ同一(目視でも区別困難)。H3_CACHE=none でキャッシュ無しの従来挙動に完全に戻る(バイト一致を回帰確認済み)。MiniMaxH3TransformerBlock はPRブランチの TransformerBlockRegistry に
未登録のため、runner側で TransformerBlockMetadata を登録してから enable_cache() を
呼ぶ(venvのdiffusers本体は無改変)。リクエストごとに _reset_stateful_cache() +
cache_context("h3") で包む(リセット漏れは前リクエストの残差による誤スキップを招く)。
同一seed連続2本のmp4バイト完全一致でリセットの正しさを検証済み。/api/t2va の upscale=1、既定OFF)ComfyUIコミュニティの MiniMaxH3_LatentUpscaler と同系の hires-fix。低解像度(768²)で
前半をデノイズ → x0推定値の映像latentだけを bilinear で空間2x → フレッシュノイズを
scheduler.scale_noise() で再注入 → 残りの低σステップを1536²で仕上げ → デコード。
H3_HIRES_DENOISE(既定0.35)がパス2の担当デノイズ強度。UIはT2VAタブのチェックボックス。
実測(768²→1536²・5秒・30steps・seed=12345、fbc+bnb-4bit):
| 合計 | デノイズ | デコード | ピークVRAM | 出力 | |
|---|---|---|---|---|---|
| upscale=0 | 181s | 125s | 6.5s | 92.1GB | 768² |
| upscale=1 | 645s | 533s (パス1 78s + パス2 455s) | 24.7s | 88.0GB | 1536² |
build_packed_sequence() で
position_ids/token_tags/各indicesを再構築し、row_timestep_plan も残ステップ分を
作り直す。ModularPipeline の _execution_device はコンポーネント登録順の先頭モジュール
で決まるため、TE解放後は components.transformer.device を明示的に使う
(diffusers-server CLAUDE.md 23番・47番と同型の罠)。H3_ATTN_BACKEND、既定 sage)sm_120(Blackwell)向けにソースビルドした SageAttention 2.2.0 を既定で使う
(ビルドは scripts/build_sageattention.sh、約2分。必ず MAX_JOBS=4 NVCC_THREADS=2 +
systemd-run のメモリ上限付きで実行——無制限並列nvccはホストRAM枯渇でシステム巻き添え
事故歴あり。CUDA_HOME=/usr/local/cuda-12.8 の明示が必要、既定のcuda-13.0はtorchの
cu128と不一致)。PyPI/コミュニティのLinux向けsm_120 wheelは存在しなかった(全てWindows)。
H3_ATTN_BACKEND=default で従来のSDPAに戻るH3_CACHE_THRESHOLD=0.1 でデノイズ67s(-43%、リクエスト
~125s。FBC 0.1の構図ドリフト特性は既知どおり)flash_hub/sage_hub)は torch 2.9 向けビルドがHub側に存在せず不成立
(2026-08-05時点。環境の問題ではない)num_inference_steps はAPI/UIパラメータ。30(検証既定)に対し 20で-15%、16で-31% の
デノイズ短縮。16/20とも単フレーム品質・時間方向の安定性に破綻なし(ただし構図は
ステップ数で変わる)。ドラフト用途は16-20、本番は30が目安。ステップを減らすと
FBCのスキップ機会も減る(30stepsで7スキップ→16stepsで0)ため、効果は単純比例しない。
H3_TRANSFORMER_QUANT、既定 none)H3_TRANSFORMER_QUANT=int8 で transformer / transformer_ref を torchao
(Int8WeightOnlyConfig(version=2)、PR #14355ドキュメントのレシピ、torchao 0.17.0)で
int8化する。66.3GB → 34.0GB。品質は目視同等(PSNR 19dBは軌道分岐であり劣化ではない。
int8同士は同一seedでmp4バイト一致の完全決定論)、デノイズは+5s程度(dequantコスト)。
int8時は両transformer同時常駐(34+34+TE-nf4 21=89GB)になり、ref2va⇔t2vaの
変種切替時の66GB級再ロードが消える(初回のref2vaのみ36sのコールドロード)。実測:
| bf16(既定) | int8両常駐 | |
|---|---|---|
| t2va | 175-185s / peak 92.1GB | 177-196s / peak 59.7-91.1GB |
| ref2va(2回目以降) | 523s / peak 87.6GB | 463-471s / peak 74.5GB |
| 変種切替の再ロード | 毎回~26-40s | なし |
フェーズ制御(int8両常駐時): t2vaデノイズ前は transformer_ref 常駐時のみTE強制解放
(89GB定常+activationsでOOMするため。実機確認)、ref2vaはTE強制解放不要になり
デコード窓も transformer_ref 常駐のまま通る。PYTORCH_CUDA_ALLOC_CONF= expandable_segments:True をrunnerが設定(int8ロード/解放サイクルの断片化で
「54GBしか使っていないのに15GB確保失敗」が実機再現したため。diffusers-serverでも
実績のある設定)。既定 none は従来とバイト一致(回帰確認済み)。
H3_LOWVRAM、既定 0)TE-nf4(21GB)+ transformer int8(34GB)= 55GB は48GB級カードでは同時常駐できない
(96GB機の既定・int8両常駐モードいずれも成立しない)。H3_LOWVRAM=1 は
「TEとtransformerを絶対に同時常駐させない」フェーズ循環方式で48GB級に対応する。
強制: H3_TRANSFORMER_QUANT 未指定なら自動で int8 に上書きする(bf16
66.3GBは48GBに単体でも収まらないため)。明示的に H3_TRANSFORMER_QUANT=none を
指定した場合は起動時に RuntimeError で拒否する。H3_TE_QUANT は bnb-4bit
(既定)以外を指定するとやはり起動時に拒否する。H3_TRANSFORMER_BOTH_RESIDENT
(int8両常駐)は無条件で無効化する。
定常状態: リクエスト間は「何も大きいものが常駐しない」(VAEペアのみCPU常駐、 他モードのような transformer/TE の常時居座りが無い)。
t2va/fl2vaのフェーズ: [エントリ: 常駐transformer/transformer_refがあれば解放]
→ TEロード → エンコード(+ fl2vaならkeyframeエンコード)→ layout/latents/
timestepsをTE常駐のまま先に実行(_execution_device解決の罠対策、下記参照)→
TE解放 → transformer(int8)ロード → デノイズ(34GB+活性化5GB≒39GB)→
transformer解放 → VAE→GPU → デコード(~11GB+バッファ)→ VAE→CPU
(transformerは次リクエストのために再ロードしない — 次は encode が先に必要)。
ref2vaのフェーズ: 同じ原則で、参照VAEエンコード → layout/latents/timesteps (ここもTE常駐のまま)→ TE解放 → transformer_ref(int8)ロード → デノイズ → 以下同様。
_execution_device 解決の罠(実装時に発見・修正): パイプラインのコンポーネント
順は text_encoder, tokenizer, processor, vae, scheduler, audio_scheduler, transformer, ...。TEを先に解放してからtransformerをロードする素朴な実装だと、
vae(CPUに退避されていても“存在する”nn.Module)がtext_encoderの次に解決され、
_execution_deviceがcpuに解決されてしまいデノイズの最初のtransformer forward
で RuntimeError: Expected all tensors to be on the same device になる
(実機で再現・特定)。対策として layout/latents/timesteps は TE がまだ
GPU常駐のうちに実行し、それらの出力テンソルが正しいデバイスに確定してから
TEを解放する 順序にした(他モードの force_free_te 遅延パターンと同じ発想)。
ref2vaでは reference_encoder_step も同様にTE常駐のうちに実行する必要がある
(layout_stepが参照の latent 形状に依存するため)。
upscale=1(hires-fix)は非対応: パス2は約4倍の系列長(~16倍のattention活性化
コスト)を要し、このモードの定常余裕(~9GB)では未検証のため、ValueError
(400)で拒否する。
副次的に見つけた既存バグの修正: generate_ref2va() の
_sync_shared_components_to_ref() はTEロードより前に呼ばれていたため、
TEが未ロードの状態(H3_LOWVRAM、またはnone系モードでref2vaが最初のリクエスト
になるケース)では self._pipe_ref.text_encoder に None が同期され、
AttributeError: 'NoneType' object has no attribute 'config' になっていた
(実機で再現・特定)。TEロードの後に呼ぶよう順序を修正した(全モード共通の
修正、H3_LOWVRAM専用ではない)。
正しさの検証: フェーズ循環は計算内容を変えないはずなので、同一seed
(768²・5秒・30steps・キツネのプロンプト、seed=12345)で H3_LOWVRAM=1 H3_TRANSFORMER_QUANT=int8 のt2va出力と通常int8モードのt2va出力を比較したところ
mp4がバイト完全一致(md5一致)することを確認済み。
48GB相当の実機検証(96GB機でダミーVRAM確保により空きを43.5GBへ制限、実48GB
カードの空き47GBより厳しい条件):
| 完走 | ピークVRAM | 内訳(概算) | |
|---|---|---|---|
| t2va 1本目 | ○ | 38.68GB | TEロード |
| t2va 2本目(連続) | ○ | 38.94GB | 同様の固定費が毎回発生(定常状態を持たない設計どおり) |
| ref2va(画像参照1枚) | ○ | 43.84GB | デノイズ283s(参照行分シーケンスが伸びるため39GB台では収まらずやや高め) |
| upscale=1 | - | - | 実装時点でOOMリスクを判断し明示的に400で拒否(未実行) |
いずれもホストRAMスワップの増加なし(free -g で作業前後とも Swap used ~6GB台で
安定)。作業後は H3_LOWVRAM 未指定(完全デフォルト設定、bf16 transformer)で
再起動して同一プロンプトのt2vaを1本実行し、peak_vram_gb: 91.94GB /
cache_skipped_steps: 6 など既存の実測値(本README上部の表・
int8量子化セクション)と同水準であることを確認し、回帰なしと判定した。
量子化済みチェックポイントの事前保存によるロード時間短縮: 未調査
→ TE は 2026-08-08 に実装済み(H3_TE_PREQUANT、既定ON。TEロード 53.0s→29.5s、
リクエスト合計 -35%、MD5一致で等価性確認済み。上記「量子化済み text_encoder の
ディスクキャッシュ」節を参照)。transformer int8 の直列化は未検証のまま —
保存に約34GBを要し、この箱のディスク空き(43GB)では採用自体が非現実的なため。
UIの解像度セレクトに 「任意 (32の倍数へ丸め)」 を追加し、幅/高さを自由に入力できる。 入力値は エラーにせず H3 の規則へ丸める:
app.py の
round_canvas_value)。H3のブロックは32の倍数でないと ValueError を出す仕様
(MINIMAX_H3_CANVAS_MULTIPLE)なので、以前は端数を送ると400になっていた。
ネイティブ範囲(短辺768・最大768×1344)を超える指定はUIが警告を出す(VRAM・品質は未検証)17n+5 へ切り上げ(align_num_frames)。
例: 6.3秒 → 158フレーム(6.58秒)。UIは送信前に実フレーム数と実尺をプレビューする/api/t2va・/api/fl2va に height/width(任意、指定時は resolution
プリセットより優先)を追加。/api/ref2va は元から受け付けるが、こちらも丸めるように変更/api/status の constraints で丸め規則(canvas_multiple/min/max、fps、frame_step/offset)
を公開し、UIは同じ規則でプレビューする(丸めの権威はサーバ側)実測確認: height=700 width=1000 seconds=6.3 で生成 → レスポンス 704×992 / 158フレーム、
出力mp4も ffprobe で 992×704・158フレームと一致。
UI実装の罠: 幅/高さの <input type=number> に min/max/step="32" を付けると、
HTML5の入力検証(stepMismatch/rangeOverflow)が丸める前の端数入力を不正扱いして
フォーム送信自体をブロックする(1024×576のような偶然valid な値だけ通り、1000×700は
何も起きない、という分かりにくい症状になる)。丸める前提の欄には制約属性を付けないこと。
H3_TURBO_LORA、既定 0、2026-08-06 / 2026-08-08 lightx2v 版へ切替)2026-08-08 更新: 既定 LoRA を lightx2v 版へ切り替え、48GB(int8/低VRAM)でも turbo が使えるようになった。
- 既定:
H3_TURBO_LORA_REPO=lightx2v/Minimax-h3-Turbo/H3_TURBO_LORA_FILE=minimax_h3_fl2v_turbo_4step_v0.1.safetensors(DMD蒸留、 Apache 2.0、rank128・312 Linear対象)。既定ステップ数は形式連動 (lightx2v=4 / Ostris に戻すと 8)- int8 対応の理由: キーが diffusers ネイティブ(to_q/to_k/to_v 分離)で
fuse_projections()不要 →torch.catを呼ばない →Int8Tensor非互換を 踏まない。適用関数は形式自動判別(detect_turbo_lora_format、comfy 署名qkv_projを先に見る — Ostris 版もtoken_refiner.キーを持つため順序が重要)- 適用係数(
H3_TURBO_LORA_SCALE、空=形式別の実測既定): lightx2v は 0.094 (Kijai 記載の 0.75 は ComfyUI の alpha 折り込み前提。生の B・A に 0.75 を掛けると 30steps でも完全ノイズ化 — 強度スイープはスパイク節参照)。Ostris は 1.0 のまま (scale=1.0 は恒等なので旧挙動と bit 一致)- E2E 実測(RTX PRO 5000 48GB +
H3_LOWVRAM=1、768²): t2va 4steps 総所要 143s / デノイズ 29s(非turbo 30steps は 351s)。t2i(静止画)×turbo は デノイズ 5.0s / 総所要 94s。本実装経路の出力はスパイク出力と mp4 md5 完全一致。 turbo 連続2本で lowvram の再ロード後の再適用も正常、turbo=0 で通常経路(FBC 有効) へ正しく復帰- 既知の性質: turbo 時は音声レベルが非turbo比で大きめ(rms 0.018-0.042 vs 0.007。白色雑音ではないが強度によっては peak が 1.0 に近づく)
- 併用制限:
H3_LOWVRAM=groupとは形式を問わず併用不可(enable_group_offloadのcpu_param_dictが有効化時点で固定され、後から追加する LoRA バッファが offload サイクルから欠落するリスク — 未検証のまま解禁しない)。comfy 形式 (Ostris)× int8 は従来どおり不可(リポジトリ名で予備判定+適用時に実ファイルの キーで最終判定)。ref2va への適用(transformer_ref)は未検証のまま- v0.1(2026-08-07 公開)のため引き続き既定OFF(UI/リクエストの turbo で opt-in)
以下は Ostris 版 (comfy 形式) 時代の記録(bf16 経路では現在も有効):
H3_TURBO_LORA_REPO=larryvrh/MiniMax-H3-Turbo-Lora で Ostris氏学習中の4/8ステップ
蒸留LoRA(Apache 2.0、rank64・259 Linear対象)を適用し、既定ステップ数を8にする。
プレビュー版LoRA(学習途上)のため既定OFF。完成版が出たら再評価する。
実測(768²/5秒/seed12345): 8steps 87.7s(-46%) で基準30steps(163.5s)に迫る品質、 16steps 98.4sで基準同等、4steps 39.6sは柔らかめだが破綻なし。 コミュニティの「4〜7stepはダメ」はComfyUI標準サンプラーがデュアルスケジュール (video shift12/audio shift3)を扱えないことが原因の可能性が高い——本実装(diffusers PRのscheduler/audio_scheduler分離+手動ループ)では4stepsでも音声破損は起きなかった。 シフト配線は改修不要(12/3はH3基準スケジューラの既定値で、sigma格子が作者リファレンス 実装とビット一致することを確認済み)。
実装メモ: LoRAキーはComfyUI命名のfused-QKV形式のため、attn.fuse_projections() +
ランタイムデルタ(W_eff=W+BA、fuseしない)で適用。罠: fuse_projections() は旧
to_q/k/vを削除せず+12.8GBリークする(明示deleteで対処)。AdaLNが linear.weight を
直接読むためラッパーに weight/bias 等のパススルーが必要。turbo時はFBC自動無効化。
デフォルトのtransformer経路(transformer_quant=none, lowvram=0)以外との組み合わせ
は当初「未検証のため予防的に拒否」だったが、実測でA/B検証した。
H3_HIRES_DENOISE=0.35既定分割)、
peak VRAM 88.09GB——非turbo30stepsの基準645sから大幅短縮。16steps: 総所要331.8s、
pass1=10/pass2=5steps、peak VRAM 88.35GB、8stepsより明確にシャープ。
全フレーム(先頭/中間/末尾)を目視確認し色化け・チェッカーボード崩壊なし、音声も
RMS/peakが正常値(無音・クリップなし)。turbo時はFBCが自動無効化されるため、
hires-fixのFBCブックキーピング(_fbc_last_step_was_skip())はtry/exceptで安全に
no-op化される(懸念だった「turbo未対応のFBC呼び出し」は実害なしと確認)。H3_TRANSFORMER_QUANT=int8、transformer_both_resident
含む): 実測で不可と確定、拒否のまま維持。 apply_turbo_lora() の
attn.fuse_projections() が torch.cat([to_q.weight, to_k.weight, to_v.weight])
を実行するが、int8量子化された to_q/to_k/to_v(H3_INT8_MODULES_TO_NOT_CONVERT
はこれらをスキップしない)は torchao の Int8Tensor であり、aten.cat カーネルが
未実装のため NotImplementedError: Int8Tensor dispatch: attempting to run unimplemented operator/function: func=<OpOverload(op='aten.cat', overload='default')>
で確実に失敗する(HTTP 500、リクエスト単位でクリーンに失敗しVRAMリークなし。
直後の非turbo生成は正常動作を確認)。lowvram=1 は
transformer_quant=int8 を強制するため、上記と全く同じ Int8Tensor/aten.cat
エラーで失敗(同一エラーメッセージを実機確認)。lowvram=group も transformer_quant=int8 前提)で同一エラー。この失敗は
group offloadフックの適用順序とは無関係(sibling project の
「LoRAをenable_group_offload()より前に適用する」という順序修正パターンは
ここでは効かない——fuse_projections()自体がgroup offloadフックを一切介さず
torch.catだけで失敗するため、順序を入れ替えても直らないと判断し深追いしなかった)。16GBバラスト(空き15.5GB)では TEロード(nf4量子化)の終盤でOOM(15.37GB使用時点で
+250MiB要求に失敗)。削除済みTE-nf4の常駐17.45GB自体が床であり、video VAE fp16は
デコード段階の対策のためこの床を動かせない。18GBバラストでは完走
(peak 17.72GB、total 302s)——つまり現アーキの実質下限は~18GB
(24GB級構成 H3_LOWVRAM=group H3_TE_PRUNE=1 がそのまま18GB級でも動く)。
16GB突破にはTEのストリーミング実行か4bit未満の量子化が必要(未着手の別課題)。
H3_VIDEO_VAE_FP16、既定 0)H3_VIDEO_VAE_FP16=1 で video VAE の重みだけを fp16 化する(9.70→4.85GB、デコード
ピーク 16.29→~11.4GB)。audio VAE は絶対に触らない(bf16化で-20dBの既知問題)。
AutoencoderKLMiniMaxH3._keep_in_fp32_modules が encoder/decoder等を
強制fp32に戻すため、from_pretrained(dtype=fp16) は効かない(実機確認)。ロード後に
.to(torch.float16) を明示的に呼ぶ必要があるH3_LOWVRAM=group、2026-08-05追加)H3_LOWVRAM=1(48GB級)は transformer(34GB)を毎リクエスト GPU に丸ごとロードするため、
24〜32GB級カードでは transformer 単体でも収まらない。H3_LOWVRAM=group は
diffusers-server(姉妹プロジェクト)の CLAUDE.md #33/#34/#37 で確立された
「block-level group offload」パターンをこのプロジェクトに移植したもので、transformer を
ホストRAMに常駐させたまま、denoise の各ステップで必要なブロック(50層中1〜2層、
~0.68GB×1〜2)だけを都度 GPU へ出し入れする。transformer はプロセス起動時に一度だけ
ロードされ、リクエストをまたいで常駐し続ける(H3_LOWVRAM=1 のような毎リクエスト
再ロードは発生しない)。
タスク時点で読んだ TorchAoHfQuantizer(quantizers/torchao/torchao_quantizer.py)の
validate_environment() は、device_map に(accelerateの自動割当のような)辞書
形式で "cpu" という文字列値が含まれる場合にのみ self.offload = True を立て、
check_if_quantized_param() はこのフラグが立っていると CPU 配置のパラメータの量子化を
スキップする(=CPUオフロードするパラメータは意図的に非量子化のまま残す設計)。
一方、本実装が使う device_map={"transformer": "cpu"} は load_components() を経由して
最終的に from_pretrained() にプレーン文字列 "cpu" として渡り、
modeling_utils.py の正規化コードにより {"": torch.device("cpu")} という
単一キーの辞書(値は torch.device オブジェクト、文字列ではない)に変換される。
torch.device("cpu") == "cpu" は Python 上で False になるため、
"cpu" in device_map.values() は False のまま保たれ、self.offload は立たない。
つまり CPU上へロードしても量子化はスキップされず、torchaoのInt8Tensorとして
正しく量子化される(scripts/probe_group_offload.py で370/370層がInt8Tensor化
されることを実機確認)。CPU上でのint8量子化は問題なく可能という結論。
_ensure_transformer_group()(core/runner.py)が
device_map={"transformer": "cpu"} + TorchAoConfig(Int8WeightOnlyConfig) で
CPU上に量子化ロードしてから enable_group_offload(offload_type="block_level", num_blocks_per_group=1, use_stream=..., low_cpu_mem_usage=...) を呼ぶ
(transformer_refも同型の _ensure_transformer_ref_group())。H3_LOWVRAM=1 と同じくbnb-4bit必須(起動時強制)。t2vaの定常状態では
TEはリクエストをまたいで常駐する(transformerがそもそも常駐するため、
TEも常駐させておいた方がリクエストごとの再ロードコストを避けられる)。use_stream=True + low_cpu_mem_usage=True の併用はtorchao Int8Tensorに対してバグがあり動かない
scripts/probe_group_offload_forward.py で実際にforwardを走らせたところ、
RuntimeError: cannot pin 'torch.cuda.CharTensor' only dense CPU tensors can be pinned で denoise の最初のブロックで必ず失敗することを実機確認した。
hooks/group_offloading.py の _pinned_memory_tensors()(use_stream=Trueなら
_onload_from_memory() から毎ステップ無条件で呼ばれる)が
low_cpu_mem_usageの値に関わらず .pin_memory() を試みるのに対し、
_init_cpu_param_dict()(enable_group_offload()呼び出し時点で1回だけ実行)は
low_cpu_mem_usage=True なら pin をスキップする、という非対称な実装になっており、
両者の想定が食い違っている。torchaoの Int8Tensor.qdata はこの食い違いが起きると
壊れた状態(内部的に torch.cuda.CharTensor として認識される)でpin_memory()が
呼ばれてクラッシュする。scripts/probe_group_offload_fix.py で対照実験した結果:
| 設定 | 結果 | 1ブロックあたりonload/offload |
|---|---|---|
use_stream=True, low_cpu_mem_usage=True(併用時) | クラッシュ | - |
(2026-08-10 訂正: 当初この組み合わせを「diffusers既定」と記載していたが誤り。apply_group_offloading の API 既定値は use_stream=False, low_cpu_mem_usage=False で、省メモリ目的で両方をオプトインしたときに踏む)
| use_stream=False, low_cpu_mem_usage=True | 動作OK | onload 0.1-0.26s / offload ~0.22s |
| use_stream=True, low_cpu_mem_usage=False | 動作OK | onload 0.04-0.07s / offload ~0s |
low_cpu_mem_usage=False(enable_group_offload()呼び出し時点で全パラメータを
eagerにpin)を新既定に採用した(H3_GROUP_OFFLOAD_LOW_CPU_MEM、既定0=False)。
理由: onloadが4-5倍速い(pinned memoryはページアウト不可でDMA転送が速いため)。
代償はロード時に追加で~14-16GBのホストRAMをpinする(page-lockedなのでスワップ
不可)ことと、enable_group_offload()自体が約22秒かかること(実機測定、
CPU上へのロード70秒 + pin化22秒 = 合計約90秒)。より少ないRAMを優先したい場合は
H3_GROUP_OFFLOAD_LOW_CPU_MEM=1(このときH3_GROUP_OFFLOAD_USE_STREAMも
明示指定しない限り自動で0にフォールバックする、上記の壊れる組み合わせを
避けるため)を明示指定すればよい。
| フェーズ | 常駐する大きいもの | 備考 |
|---|---|---|
| 起動時preload | transformer(int8, CPU常駐+groupoffloadフック) | 約90秒(CPUロード70s+pin化22s) |
| t2va encode | TE-nf4(GPU,21GB) + transformer(CPU) | |
| t2va denoise | TE-nf4(GPU,21GB) + transformerの1-2ブロック(GPU,~1.4GB) | |
| t2va decode | vaeペア(GPU,~11GB) + transformerの1-2ブロック | TEはこの窓だけ強制解放(下記参照)、decode後に再ロード |
| ref2va参照エンコード | vaeペア(GPU,11GB) | TEはこの窓だけ強制解放(下記参照) |
| ref2va denoise | TE-nf4(GPU,21GB) + transformer_refの1-2ブロック | |
| リクエスト間定常 | transformer(CPU) + TE-nf4(GPU) | ref2va後はtransformer_refが未ロードに戻る(t2va↔ref2va切替のたび再ロード) |
当初「group offloadされたtransformerのGPU実消費は極小(~1.4GB)だから、decode時に
transformerを解放する必要は無い」と設計したが、32GBダミーVRAM検証で
CUDA out of memory を実機再現し、_log_gpu_tensor_diag()
(H3_DEBUG_MEM_DIAG=1で有効化する一時診断関数、core/runner.pyに残置)で
実際に生存しているCUDAテンソルを列挙したところ、TE-nf4自身の埋め込みテーブル/
lm_head重み(shape (151936, 5120)、bf16、1.556GB×2 = 3.1GB強を含む合計22.25GB)が
デコード直前まで常駐したままだったことが判明した(empty_cache()だけでは
解放されない、実際に参照されている生きたテンソルだったため)。つまり
TE-nf4(21GB)+ decode専用バッファ(~16.3GB、下記VAEタイル調査参照)=37GBが
真の必要量で、transformerのフットプリントとは無関係にTEとVAEの競合だった。
対策: decode窓(と、ref2vaの参照VAEエンコード窓)でTEを強制解放し、窓を抜けたら
再ロードする(force_free_teとは別枠の専用ロジック、_execution_device解決順序
はTE解放→vaeをGPUへ、の順で安全性を確保)。
同一seed(768²・5秒・30steps・キツネのプロンプト、seed=12345)で、通常int8モード
(H3_LOWVRAM未指定、H3_TRANSFORMER_QUANT=int8、FBC H3_CACHE=fbc有効)の
出力と H3_LOWVRAM=group の出力を比較したところ、FBCのキャッシュスキップ判定が
実行経路の違いで異なった(cache_skipped_stepsが6→0)ため素朴な比較ではmp4が
不一致だった。FBCは前ステップとの残差の類似度という数値的に鋭敏な判定のため、
数学的に等価な演算でも経路が変わればスキップ判定が変わりうる(劣化ではない)。
両モードとも H3_CACHE=none に揃えて再比較したところ、**mp4がバイト完全一致
(md5一致)**することを確認した。group offloadの計算内容は既存経路と数学的に
同一であることの裏付け。
32GB制限(ダミーVRAM確保で空きを~30GBへ制限、H3_CACHE=fbc有効のまま):
| 完走 | ピークVRAM | 所要時間 | |
|---|---|---|---|
| t2va 1本目(768²・5秒・30steps) | ○ | 28.67GB | denoise 220.79s / decode 6.31s / 総計337.19s(TE初回ロード込み) |
| t2va 2本目(連続) | ○ | 28.23GB | denoise 220.85s / decode 6.01s / 総計278.83s(TE常駐のため短縮) |
| ref2va(画像参照1枚、768×1344) | RAM不足で拒否(下記参照) | - | - |
2本とも1本目と完全に同一mp4(md5一致、be3f32a84de074990208ad0d30f31a63)。
ホストRAM/スワップは各フェーズとも増加なし(free -gのSwap usedは作業前後とも
~7-8GB台で安定、この値は他プロセス由来の既存分)。
24GB制限プローブ(ダミーVRAM確保で空きを~22GBへ制限):
H3_LOWVRAM=1的な設計に戻す)か、TE自体をさらに軽量化する(GGUF等、ただし
transformers系モデルへのGGUF適用は構造的に困難)必要がある。本タスクの
スコープ外(48GB→24-32GB対応が目的で24GBは探索的プローブ)のため、
現時点では未対応と結論する。scripts/probe_vae_tile_size.py で768²・124フレームのVAE decodeを単体で
(denoiseを介さず合成潜在から)直接ベンチマークしたところ、
tile_sample_min_height/width を256(既定)→192→128→96まで縮小しても
ピークVRAM(16.29GB)が全く変化しなかった(所要時間はタイル数が増える分
5.9s→10.5sへ悪化)。この結果から、decodeのピークは空間タイルの合成バッファでは
なく、時間チャンク(tokens_chunk_size単位)の1チャンク分をまるごとデコード
するバッファか、固定的なVAEアーキテクチャのオーバーヘッドに支配されていると
推測される(VAEクラスは時間方向のチャンクサイズを公開パラメータとして持たない
ため、これ以上の調整はコード変更が必要で本タスクの範囲外)。
結論: 24-32GB級対応において空間タイルサイズの調整は無意味(既定のままでよい)。
全てのballast検証(32GB・24GB双方)を通じて H3_CACHE=fbc(既定)・
H3_ATTN_BACKEND=sage(既定)を有効にしたまま実行し、group offloadのフックと
競合するエラーは一切発生しなかった(FBCはブロック単位の計算スキップ判定、
group offloadはブロックのGPU常駐管理、sageは各ブロック内部のattention実装、と
三者は独立したレイヤーで動作するため)。MD5一致チェック(H3_CACHE=noneで
実施)とは別に、既定のFBC有効設定での通し実行(32GB制限のt2va 2本)が完走した
ことも確認済み。
H3_LOWVRAM=group でt2va実行後にref2vaを呼ぶと、VRAM予算に関わらず(96GB機で
ダミーVRAM確保無しでも再現)ホストRAMガードで拒否されることを実機確認した:
H3_LOWVRAM=group requires at least 40.0GB of available host RAM before loading
the (~34GB, permanently CPU-resident) int8 transformer, but only 33.0GB is
available right now.
原因の切り分け: transformer解放直後はavail_gbが正しく回復する(44.6GB前後)が、
その後のTE再ロード→参照VAEエンコード→レイアウト計算の過程でavail_gbが
33GB前後まで下がる(実機ログで確認)。swap_used_gbは一貫して増加しないため
実際のスワップ発生は無い ─ MemAvailable(Linuxのbuff/cache込みヒューリスティック
推定値)の変動が、真の空きRAMより保守的に振れている可能性が高い。ただし
free -gのusedベースで見ても94GB中62GB使用(32GB残)という状況で
追加34GBの確保は本質的にタイトであり、ガード自体が誤りとは断定できなかった
(96GB機でも「t2va transformerを常駐させたままref2va用transformer_refをさらに
CPUへpinしようとする」設計そのものが、94GB RAM機の物理容量に対してすでに
ギリギリ)。安全側に倒し、ガードを緩めることはせず既知の制限として記録する
(過去のスワップ暴走事故の教訓を優先)。将来の改善候補: preload_all()での
transformer即時ロードをやめてTEと同様に遅延ロード化する(初回リクエストの
レイテンシとのトレードオフ)、またはt2va⇔ref2va切替時によりRAMを消費しない
経路を設計する。RAM 48GB以上のマシンでは(未検証だがRAM予算に余裕があるため)
この問題は起きない可能性が高い(本タスクは94GB機でのみ検証、より多いRAM
搭載機での追試は未実施)。
H3_TE_PRUNE、既定 0、2026-08-06追加)MiniMax-H3 の text_encoder(Qwen3-VL-32B、64層)は
hidden_states[MINIMAX_H3_TEXT_ENCODER_LAYER](=50)しか読まない
(diffusers/modular_pipelines/minimax_h3/encoders.py)。H3_TE_PRUNE=1 は
text_encoder を 51層だけ(0〜50、MINIMAX_H3_TEXT_ENCODER_LAYER + 1)で構築し、
未使用の52〜64層目 + 最終norm + lm_head(重み換算で14層分、bnb-4bit実測 ~3.6GB /
bf16実測 ~13.6GB)を一度もロードしない。既定 0 は完全無変更(この分岐自体が
一切呼ばれない)。
hidden_states[k] の意味は transformers の _can_record_outputs = {"hidden_states": Qwen3VLTextDecoderLayer} フック機構(output_capturing.py)により決まる:
hidden_states[0] = 埋め込み出力(layer 0 の入力を捕捉)、hidden_states[k]
(k=1..num_hidden_layers) = layers[k-1] の出力。つまり hidden_states[50] =
layers[49] の出力で、layers0..49を実行すれば十分なはずだった。
しかし num_hidden_layers をちょうど50に切り詰めると、hidden_states[50] が
捕捉タプルの最後の要素になってしまい、Qwen3VLTextModel.forward を包む
@capture_outputs(tie_last_hidden_states=True)(既定)が「最後の要素を
outputs.last_hidden_state(=最終norm適用後の値)で強制的に上書きする」という
挙動を発動させる。実機検証(scripts/probe_te_prune*.py)で、50層ちょうどに
切り詰めた場合の hidden_states[50] は本来の(64層モデルの)値と桁違いに
異なる(max abs diff ~1.5e4、量子化誤差の水準ではなく完全に別の値)ことを確認した。
これはまさに encoders.py 自身のガード
(if num_layers <= MINIMAX_H3_TEXT_ENCODER_LAYER: raise ValueError(...))が
警告している「ちょうど50層に切り詰めた最終隠れ状態はpost-normであり、MiniMax-H3が
期待する値ではない」という事態そのもの(このガードのおかげで50層ちょうどの誤設定は
encode_prompt() 経由なら例外で弾かれる)。51層(layers[50]は実行されるが
その出力は読まれない、無駄な1層分の計算コストのみ)にすることで
hidden_states[50] が捕捉タプルの中間に位置するようになり、上書きを回避できる。
51層版で64層版の hidden_states[50] と完全一致(torch.equal、bf16・bnb-4bit
nf4とも)することを実機確認済み。
core/runner.py の _text_encoder_config_kwargs() が、text_encoder の
ComponentSpec(pretrained_model_name_or_path="MiniMaxAI/MiniMax-H3",
subfolder="text_encoder")と同じ場所から Qwen3VLConfig を個別ロードし、
text_config.num_hidden_layers = 51 に書き換えたオブジェクトを
load_components(..., config={"text_encoder": pruned_config}) として渡す。
PreTrainedModel.from_pretrained は config が既に PreTrainedConfig インスタンス
なら自前のconfig自動ロードをスキップしてそのまま使う(modeling_utils.pyで確認)。
チェックポイントの layers.51-63.* は from_pretrained のロードレポートに
UNEXPECTED として現れ、単純に無視される(構築されないため一切のVRAM/RAMを消費
しない)。vision tower(model.visual)は無変更(fl2va のキーフレーム/ref2va の
参照画像・動画のpixel_valuesがここを通るため、削除対象から明示的に除外)。
H3_TE_QUANT(bnb-4bit/none)・H3_LOWVRAM(0/1/group)のどの組み合わせとも合成可能。
| 精度 | 削除前 | 削除後(51層) | 削減 |
|---|---|---|---|
| bnb-4bit nf4 | 21.02GB | 17.45GB | -3.57GB (-17%) |
| bf16 | 66.71GB | 53.06GB | -13.65GB (-20%) |
nf4は量子化で1層あたりのサイズがbf16の約1/4に圧縮されるため、削減の絶対量もbf16より 小さい(相対削減率はほぼ同じ)。
同一seed(768²・5秒・30steps・キツネのプロンプト、seed=12345、H3_CACHE=noneで
FBCの経路依存を排除)で、H3_TE_PRUNE=0(削除なし)とH3_TE_PRUNE=1(削除あり)の
出力を比較したところ、**t2va・ref2va(画像参照1枚、vision tower経由)とも
mp4がバイト完全一致(md5一致)**した。削除が数学的に無影響であることの実証。
H3_LOWVRAM=1・H3_LOWVRAM=groupの各モードでも、削除の有無で出力が完全一致する
ことを確認済み(後述)。
H3_LOWVRAM_GROUP側に追加修正)24〜32GB級対応の既存機構(H3_LOWVRAM=group)は、TE-nf4(削除前21GB)を
denoise中も常駐させたままにする設計だった(group offloadされたtransformerの
実消費が~1.4GBと小さいため、32GB級では問題にならなかった)。削除後のTE(17.45GB)は
それでもまだ大きく、22GB制限で実機OOMを再現した(denoise開始直後、
21.73GB使用中に224MB要求で失敗)。24GB制限でも同様にOOM(23.12GB使用中に
1.16GB要求で失敗、step 1で発生)。
対策として H3_LOWVRAM_GROUP かつ H3_TE_PRUNE=1 の場合に限り、
H3_LOWVRAM=1と同じ「denoiseループの間だけTEを強制解放し、decode窓の前後で
リロードする」選択法を追加した(core/runner.pyのgroup_free_te_for_denoise
フラグ)。解放位置はlayout_step/latents_step/timesteps_stepの後(既存の
force_free_teと同じ理由: これらのステップの出力は既にテンソルとして
stateに載っているため、_execution_device解決には以後一切影響しない)。
H3_TE_PRUNE=0(既定)のH3_LOWVRAM_GROUPは完全無変更(このフラグは
H3_LOWVRAM_GROUP and H3_TE_PRUNEの両方が真のときのみ真になる)。
修正後、22GB/24GB/20GBいずれのVRAM制限でも実機で完走を確認した:
| VRAM制限 | 結果 | ピークVRAM(reset後の計測) | 総所要時間 |
|---|---|---|---|
| 22GB(修正前、削除のみ) | OOM(denoise開始直後、21.73GB使用中に224MB要求) | - | - |
| 24GB(修正前、削除のみ) | OOM(step 1、23.12GB使用中に1.16GB要求) | - | - |
| 24GB(修正後、1本目) | ○ | 17.72GB | 321.7s(TE初回ロード込み) |
| 24GB(修正後、2本目・連続) | ○ | 18.68GB | 277.7s(TE常駐のため短縮) |
| 20GB(修正後) | ○ | 17.72GB | 320.3s |
24GB×2本・20GB×1本の出力mp4はすべてバイト完全一致(md5一致、通常int8モード
(H3_LOWVRAM未指定)の出力とも一致)。group offloadの計算内容が
VRAM予算に関わらず数学的に同一であることの裏付け(既存の32GB/24GB検証結果と同じ
結論)。ホストRAM/スワップは各テストの前後で増加なし(free -hのSwap usedは
一貫して~7.9GB台、既存分のまま)。
H3_LOWVRAM=1(48GB級)でのTEロード時間短縮削除により、H3_LOWVRAM=1が毎リクエスト支払うTEロード固定費が短縮される
(実機、ダミーVRAM確保で空きを~43.5GBに制限):
| TEロード時間 | TEサイズ | |
|---|---|---|
| 削除なし | 42.3s | 21.01GB |
| 削除あり | 35.0s(-17%) | 17.44GB |
出力mp4は削除の有無でバイト完全一致(md5一致)。
H3_TE_PRUNE未指定(既定0)の状態で同一条件のt2vaを実行し、この機能追加前に
取得していた基準mp4とバイト完全一致(md5一致)することを確認した。既定動作は
完全無変更。
/api/ref2va)順序付きの参照素材(画像最大9・動画最大3・音声最大3、合計12。音声単独は不可)から
動画+音声を生成する。参照の順序はプロンプト内ラベル(<Picture i> 等)とrotary配置に
対応するため意味を持つ。動画参照はサウンドトラックも条件付けに使われる。
参照が音声ちょうど1本のときは秒数省略可(音声の長さが生成尺になる。APIでは
seconds=0)。出力キャンバスは参照に縛られず、未指定なら16:9(1344×768)。
transformer_ref/(61.7GB、クラス/configはtransformerと同一で
重みのみ別) を使う。t2va/fl2va用transformerとは同時常駐不可のため、runnerは
変種切替(アクティブな片方だけ常駐、解放→再ロード)で管理する(/api/status の
active_variant)。TE-nf4・VAE類・processorは両変種で共有。venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
起動時に transformer と TE(既定では NF4量子化してGPU常駐)をプリロードする
(H3_TE_QUANT=none の場合は旧方式: transformer/VAEを常駐させ、TEは各リクエストの
たびにロード/解放)。ブラウザで http://<host>:8611/ を開く。
現在の箱 (48GB + 20GB、2026-08-07 のGPU交換後) では既定モードはロードできない。 起動は低VRAMモード必須:
CUDA_VISIBLE_DEVICES=0 H3_LOWVRAM=1 venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611(
CUDA_VISIBLE_DEVICES=0は 48GB の RTX PRO 5000 を cuda:0 に固定するため。 20GB の RTX 4000 SFF Ada は、バラスト検証上の床 ~18GB に対して余裕が2GBしかなく、 かつ sm_89 なので sm_120 向けにビルドした SageAttention も使えない — H3 用途は未検証・ 非推奨)
venv/bin/python scripts/probe_t2va.py
outputs/probe_t2va.mp4 と outputs/probe_report.json (ロード時間・生成時間・
ピークVRAM等)を出力する。
POST /api/t2va (multipart/form-data): prompt, resolution
(768x768|768x1344|1344x768), seconds(5〜15), num_inference_steps, seedPOST /api/fl2va: 上記に加え image / last_image(どちらか一方以上)POST /api/t2i: 静止画モード。seconds の代わりに frames(22既定|5)。
超短尺mp4と中央フレームPNGの両方を保存(「静止画モード」セクション参照)POST /api/t2i_batch: 静止画のバッチ生成。prompts を場面数ぶん繰り返し送る
(最大24)。低VRAMモードのロード固定費をバッチ全体で1回に償却
(「静止画のバッチ生成」セクション参照)POST /api/ref2va + still=1 + frames: 参照付き静止画 (ref2i)。中央フレームPNGを保存POST /api/ref2i_batch: 参照付き静止画のバッチ生成。共通 references +
prompts(最大24)。キャラクター一貫の場面静止画の連番生成
(「参照付き静止画」セクション参照)POST /api/ref2va_batch: 参照付き動画のバッチ生成。共通 references +
prompts + seconds(全場面共通・必須)。物語の各場面動画の連番生成
(「参照付き動画のバッチ生成」セクション参照)GET /api/status: ロード状態・VRAM/RAMGET /api/progress: 生成中の進捗ポーリング用モード・量子化・turbo・TE配置の組み合わせが増えたため、**「この設定のこの局面で GPU に 何が載っているか」**を一枚で引ける表を docs/RESIDENCY.md に置いた。 ピークVRAMの内訳を誤って説明した実例(デノイズ時のピークをデコード時と取り違えた)も 「よくある誤解」として記録してある。
本 README は「どう使うか」の運用ドキュメント。技術面は目的別に2本ある。
| 文書 | 内容 | 読者 |
|---|---|---|
| docs/TECHNICAL_OVERVIEW.md | 技術概要: 機能、アーキテクチャ、各種方式の統合、VRAM容量別の扱い、性能、設定リファレンス | このアプリが何をどう実現しているかを知りたい人 |
| docs/internal/TECHNICAL_REPORT.md | 内部資料(作業記録): 2026-08-04〜08-08 の全作業。設計判断の背景、実機で踏んだ16件の罠とその解決、検証手法 | 同じ罠を踏みたくない開発者 |
技術レポートの方はバグ・失敗・回り道まで含む経過の記録なので、仕様や性能を知りたい だけなら技術概要を読めばよい。
ComfyUI コミュニティ等で出た改良を本アプリ(diffusers 経路)へ取り込んだ作業の記録は docs/COMMUNITY_IMPROVEMENTS.md にまとめてある (取り込んだもの / 調査の結果取り込まなかったもの / 着想を得て自前実装したもの、 それぞれの出典・実測値・判定・踏んだ罠)。
環境変数でしか変えられなかったオプトイン設定を、性質で2つに分けてUIから操作できる。
FirstBlockCache(+しきい値)・Sage Attention・Turbo LoRA。リクエストごとのパラメータ
(cache / cache_threshold / attn / turbo)として送り、MiniMaxH3Runner. apply_instant_settings() が生成ロック取得後・denoise前に常駐transformerへ適用する
(disable_cache()/enable_cache()、set_attention_backend()、_TurboLoRALinear.enabled)。
未指定なら従来どおりプロセス既定なので既存のcurl/スクリプトは無変更で動く。
turbo有効時はFBCを自動的に無効化する(元の安全規則を踏襲)。
実測(同一seed、再起動なしで切替): FBC on 100.8s(6スキップ)/ off 129.3s(0スキップ)、 Sage on 129.3s / off(native) 158.5s、Turbo on(8steps) 38.9s。
transformer int8・TE量子化・TEレイヤー削除・低VRAMモード・video VAE fp16。
POST /api/settings/apply が core/settings.py の apply_reload_settings() を呼び、
プロセスは再起動せず runner内で全モデルを解放→新設定でロードし直す
(自プロセスをkillすると誰も起動し直せずUIごと復帰不能になるため、
os.execv/self-kill の類は実装しない)。生成中は409、未検証の組み合わせは400。
実測: transformer_quant none→int8→none が 56.0s / 55.0s(GPU 87.5→55.0→87.3GB)、
lowvram 0→1→0 も往復動作。GET /api/settings が現在値と選択肢を返し、UIはこれで初期化する。
UI実装の注意: チェックボックスOFFは空文字ではなく明示的に turbo=0 を送ること
(空文字は「未指定=サーバ既定」と解釈されうるため、H3_TURBO_LORA=1 で起動した
サーバではチェックを外しても無効化されない恐れがある)。turboとupscaleは相互排他で、
片方を選ぶともう片方が自動的に解除・無効化される(サーバ側の400と整合)。
結果表示の下に outputs/ 直下の mp4 をタイル表示する。サムネイルはサーバで生成せず
<video preload="metadata" src="....mp4#t=0.1"> に先頭フレームを描かせる(依存追加なし。
Ref2VAの参照タイルと同じ手法)。
GET /api/outputs: **直下の .mp4 と .png(静止画モードの生成物。type フィールド
"video"/"image" で区別。outputs/ab_* 等の検証資料は対象外)。
尺/解像度は ffprobe で取得し mtime+size をキーにメモリキャッシュPOST /api/outputs/delete: パストラバーサル対策(/・\・.. を拒否し、
resolve 後に outputs/ 直下であることを検証=シンボリックリンク経由の脱出も遮断)。
UIは confirm() 必須。実機で ../app.py /etc/passwd ab_*/... 等が400になることを確認済みPOST /api/outputs/concat: 連結順は「チェックした順」(表示順は新しい順)。
全入力のパラメータが一致すれば concat demuxer + -c copy(再エンコードなし=劣化ゼロ)、
不一致なら filter_complex で先頭動画の解像度へ揃えて再エンコード(無音入力には
anullsrc を合成)。2本未満は400、連結同士の同時実行は409(GPUを使わないので
generation_lock は取らない)。PNG(静止画)は連結対象外(ffprobe が PNG も
video stream として読めてしまうため、拡張子で明示的に400にする。UI側も選択に
PNG が混ざると連結ボタンを無効化する)依存に関する注意: このアプリの生成物のmuxは PyAV(av.open() で libx264+aac を
直接書き出す、core/runner.py の _mux_mp4())で行っており、ffmpeg コマンドは使って
いなかった。ギャラリーの ffprobe/ffmpeg 呼び出しがこのアプリで初めての外部コマンド
依存になる(この環境には /usr/bin/ffmpeg があり、不在時は FileNotFoundError を
捕捉して明示的にエラーを返す)。外部コマンド依存を無くしたい場合は「同一パラメータ限定で
PyAVによるパケット再多重化(=-c copy 相当)にし、混在時はエラーにする」のが現実的な代替
(パラメータ混在の再エンコードまでPyAVで実装するのは実質ffmpegの再実装になるため非推奨)。
UI実装の注意: 選択のたびに全タイルを作り直すと <video> が全数再生成されて
ちらつく(95枚で顕著)。選択状態はバッジ/チェックの差分更新にすること
(updateGallerySelectionUI()。一覧の再構築は /api/outputs 取得時のみ)。
ローカルLLM(gemma4-31B)接続下で h3-official モードを実測した結果。
構造適合: 生成されたプロンプトは公式仕様に完全準拠していた。
integrated_multimodal_description / overall_soundscape /
non_diegetic_music)、[Shot 1] は時刻なし、[Shot 2] At 00:05.000 の3桁記法、
台詞は <d>[Japanese] おかえり</d> と原語で保持、話者ID (S1) も付与。応答6.2秒<Subject n>/<Picture n>/<Audio n> のラベルと
fully_preserved / fully_copy 等の関係マーカーも使用。応答12.8秒カット位置の実測(768²・10秒・turbo 8steps、LLM生成プロンプト、各2seed。
ffmpeg のシーン検出 gt(scene,0.15)):
| 記法 | 指示 | 検出(seed 12345 / 777) | ズレ |
|---|---|---|---|
公式 [Shot 2] At 00:05.000 | 5.000秒 | 4.875 / 4.875 | -0.125秒(両seedで同一) |
独自 CUT 2 [6-10秒] | 6.0秒 | 6.083 / 6.333 | +0.083 / +0.333秒 |
公式記法の方がばらつきが小さい(2seedで完全同一)。ただし独自CUT記法も0.1〜0.3秒の ズレに収まっており、当初の手書きプロンプトによる1試行(独自記法+1.0秒)ほどの差はない。 どちらも実用範囲で、公式記法は再現性が高い、というのが実測の結論。
実装上の罠(実機で再現・修正): lang=ja を指定しても英語で返ってくる。
言語指示をシステムプロンプトの冒頭に置くと、その後ろに続く15.8KBの英語リファレンス
(英語出力の実例が並ぶ)に上書きされるため。言語指示はリファレンス本文より後ろ、
最終指示として置くことで解消した(diffusers-server の Tポーズ実装で確立した
「プロンプトは末尾が最強」と同じ原理)。修正後は地の文だけが日本語になり、
フィールド名・[Shot n]・タイムコード・<d> タグ内の台詞は英語/原語のまま維持される。
「LLMの性能限界なのか、プロンプトで直るのか」を推測で議論しないために、まず故障率を
実測し(scripts/probe_h3official_compliance.py、5入力×3回)、その結果に基づいて
対策した。LLM は gemma4-31B Q4_K_M / llama.cpp、n_ctx=7680。
| 故障クラス | 発生率 |
|---|---|
構造・記法(フィールド名・[Shot n]・タイムコード・<d>タグ・話者ID) | 0/15 (0%) |
| 時間配分 | 6/15 (40%) |
| 文脈溢れ | 0/15(余裕 2,900+ トークン) |
「入力が変われば別の問題が出るのでは」という懸念は良い方に外れた — 違反は散らばらず 時間配分の1クラスに集中していた。しかもその6件は性質が2つに分かれる:
文脈: t2va は system 4,397 tok / 7,680 で余裕あり。ref2va は 6,191 tok (81%) で余裕 約1,100 トークンしかない(実測では6フィールド揃って正常終了したが、入力が長い場合は要注意)。
core/prompt_check.py): 決定的に判定できる規則を実装
(F1 3フィールド / F2 先頭ショットに時刻なし / F3 カット時刻の厳密増加 / F4 尺内 /
F5 ショット尺の下限 / F6 <d>タグと言語タグ / F7 台詞がショットに収まる /
F8 話者ID)。F5・F7 は公式仕様に無い本アプリの実用規則 — 公式は「カット時刻が
尺内」としか言わないため、5秒尺で4.5秒にカットして最終ショット0.5秒、という
字義どおりだが使えない出力を許してしまう(実際に発生)。意味的整合性(1ショット内で
画角が混在する等)は近似ルールで警告にとどめる。手書きプロンプトにも効くlang=ja 修正時に後方ブロックを持っていた)。既定は lang=en なので、
尺の制約を含む全指示が15.8KBのガイドに埋もれていた。両版の末尾に時間配分6ルール
(ショット尺の下限・台詞の発話時間見積り・ショット数の目安・1ショット1構図・
画面外音声の定型句・話者ID)を追加core/llm.py の enhance_prompt_checked): 違反を検出したら
その内容を日本語で突きつけて再生成(最大2回、H3_OFFICIAL_MAX_REPAIRS)。
違反が増える修復案は破棄する(修復が別の箇所を壊す事故の防止)。
入力の実現不可能性はLLMに投げる前に判定し InfeasibleInputError → 400 + 助言| clean | 入力不可を検出 | 違反が残存 | |
|---|---|---|---|
| ベースライン | 9/15 | 0 | 6/15 |
| 改善後 | 12/15 | 3/15(正しい挙動) | 0/15 |
未解決の違反がゼロになった。 全ケースが「妥当なプロンプトを返す」か「実行不能な理由を 助言付きで返す」かのいずれかになる。所要時間の中央値は 8.5s → 9.2s(+8%)で、修復ループは 必要なときだけ発動する(発動時は約2倍の21秒)。
API は /api/prompt/enhance の h3-official 応答に violations / warnings /
check_report / attempts / repaired を追加。UI は残った指摘をステータス欄に表示する
(生成はブロックしない — プロンプトは編集可能なので人間が最終ゲート)。
H3_TE_PREQUANT、既定 1、2026-08-08)H3_LOWVRAM=1 は毎リクエストで TE を再ロードするため、その時間がそのまま固定費になる。
この時間の大半は「元の bf16 重みを読む + その場で bnb-4bit へ量子化する」処理なので、
量子化後の重みを一度保存しておけば次回以降は読むだけで済む。初回ロード時に自動保存し、
以降はそこから読む。
実測(RTX PRO 5000 48GB + H3_LOWVRAM=1 H3_TE_PRUNE=1、t2i turbo 4steps を各4回):
| TE ロード | リクエスト合計 | |
|---|---|---|
| キャッシュ無効(従来) | 46.5〜55.8s(平均 53.0s) | 108.7〜150.0s(平均 128.6s) |
| キャッシュ有効(既定) | 21.1〜34.3s(平均 29.5s) | 75.3〜90.6s(平均 83.2s) |
| 差 | -23.5s(1.8倍) | -45.4s(-35%) |
等価性: 同一 seed の生成物が PNG で MD5 完全一致(seed 3・4 で確認)。加えて
プローブ(scripts/probe_prequant_equivalence.py)で hidden_states[50] が
torch.equal でビット一致(max_abs_diff 0.0)することも確認済み。bnb-4bit の量子化は
決定的なので当然の結果だが、速いだけで採用しないのが本プロジェクトの流儀。
models/prequant/te_<quant>_prune<0|1>/(.gitignore 済み)。
設定ごとに別ディレクトリにしてある — TE_QUANT / TE_PRUNE を変えると重みの中身が
別物になるため、同じ場所を使い回すと設定切替時に古い重みを読む事故が起きるH3_TE_PRUNE=1)/ 未削除なら ~21GBH3_TE_PREQUANT=0 で完全に無効化(導入前と同一挙動)。空きディスクが
H3_TE_PREQUANT_MIN_FREE_GB(既定 25GB)を下回る場合は保存をスキップして生成は続行する
(キャッシュは高速化であって機能ではない)。保存は一時ディレクトリへ書いてから rename
するので、途中で落ちても中途半端なキャッシュが「有効」に見えることはない測定上の注意(この検証で踏んだ罠): 保存直後に測ると 17.44GB がページキャッシュに 載っているため 2.6秒という非現実的な値が出る。実運用ではリクエスト間に transformer (34GB)をディスクから読むため TE のページキャッシュが押し出され、実測は 21〜34秒に 落ち着く(リクエストを重ねるほど遅くなり 34秒前後で頭打ち)。それでも従来経路の 46〜56秒より速いが、プローブの数値をそのまま性能として報告してはいけない。
H3_TE_DEVICE、既定 ``、2026-08-09)H3_LOWVRAM=1 が毎リクエストで TE を再ロードする根本原因は、デノイズ中に TE の
置き場所が無いことにある(48GB では transformer-int8 34GB + 活性化 5GB = 39GB で、
残り 9GB に TE の 17.45GB は入らない)。TE を2枚目のGPUへ逃がせばこの再ロードが消える。
# 例: TE を cuda:1 に常駐させる (CUDA_VISIBLE_DEVICES は設定しない = 両GPUを見せる)
H3_LOWVRAM=1 H3_TE_PRUNE=1 H3_TE_DEVICE=cuda:1 venv/bin/python -m uvicorn app:app --port 8611
実測(RTX PRO 5000 48GB + RTX 4000 SFF Ada 20GB、t2i turbo 4steps、768²):
| 1回目 | 定常(2回目以降) | |
|---|---|---|
| 従来(TE も cuda:0) | 81.3s | 67.6〜86.2s(平均 78.4s) |
| TE を cuda:1 へ | 88.2s(TE初回ロード込み) | 33.6〜36.7s(約35s、-55%) |
TE のロードはプロセス起動後1回だけ。GPU1 に 17GB 常駐したまま、GPU0 はアイドル時 2.3GB まで下がる。
出力は従来構成とビット一致しない(PNG の MD5 が異なる)。原因は sm_120(Blackwell)
と sm_89(Ada)のアーキテクチャ差による丸めで、実測した hidden_states[50] の
相対RMS差は 0.084%(max_abs_diff 3.5、値のRMS 57.4)。これは既に採用済みの
参照プレフィックス共有の差(1.5%)より20倍小さく、バグの水準(ネガティブ
コントロール 27-30%)とは桁が違う。Sage Attention や int8 と同種の軌道ドリフトであり、
目視でも品質は同等。ビット再現が要る対照実験では H3_TE_DEVICE を外すこと。
_execution_device の罠、再び)_execution_device は components 順(text_encoder, tokenizer, processor, vae, ...)で
最初の nn.Module のデバイスを返す(実装を読んで確認)。TE を cuda:1 に置くと
これが cuda:1 を返し、layout/latents/timesteps も decode も別GPUにテンソルを作る。
最初は「layout の窓だけ塞ぐ」実装にしたが、decode でも発火した
(latents = latents * latents_std + latents_mean が
Expected all tensors to be on the same device, cuda:0 and cuda:1 で失敗、実機で再現)。
窓を個別に塞ぐ方式は漏れるため、「既定では TE をパイプから外しておき、エンコード中
だけ繋ぐ」(_te_attached())という反転した設計に変更した。これなら新しい経路が
増えても安全側に倒れる。モジュール実体は self._te_module が保持し続けるので、
外しても解放されない。
layout/latents/timesteps の窓ではさらに _pin_execution_device_to_compute() で
vae も一時的に外す(TE を外した後の次の nn.Module が CPU 上の vae になり、今度は
_execution_device が cpu を返してしまうため。transformer が最初に見つかるようにする)。
/api/t2i、2026-08-07 本実装)H3を「超短尺動画→静止画取り出し」で画像生成の代用にするモード。前段の超短尺プローブ (下記)の結論をそのまま本実装した。価値は「H3と画風が完全に一致する静止画」を FL2VA の先頭フレームや Ref2VA の参照画像として作れること(速度は固定費支配のため 専用T2Iモデルには勝てない)。
POST /api/t2i — prompt / frames(22既定 or 5)/ resolution or
height+width / num_inference_steps / seed / 即反映パラメータ
(cache/cache_threshold/attn/turbo)。レスポンスは超短尺 mp4 と中央フレームの PNG
(image_url、t2i_<ts>.png)の両方core/runner.py):
generate(still=True, still_frames=...) — diffusers 側の最小尺5秒バリデーション
(MINIMAX_H3_MIN_DURATION)は MiniMaxH3SetupStep の呼び出し1回分だけ
_relaxed_min_duration() で緩和(生成は generation_lock で直列なので漏れない)H3_VAE_SMALLCLIP_FIX、既定1):
AutoencoderKLMiniMaxH3._decode() は潜在1-2フレームで num_chunks=0 →
torch.cat([]) で落ちる上流バグがある(プローブで実機再現)。runner 側の
monkeypatch で「1チャンク未満なら全トークンを単一 _decode_clip() で復号し、
frame_pre_padding(3) とパディング末尾を切り落とす」分岐を追加。潜在2フレーム
×4 − 3 = 5ピクセルフレームで幾何が一致する。通常動画 (num_chunks>=1) は元実装へ
そのまま委譲するため影響なし(venv の diffusers 本体は無変更)generate()/generate_ref2va() の decode 部を
try/except 化し、例外時も _restore_decode_steady_state()(VAE を CPU へ・
モードごとの transformer/TE 再ロード)を実行してから再送出する。
実機検証済み (2026-08-07): 一回限りの失敗注入フック H3_DEBUG_FAIL_DECODE=1
(os.environ.pop で読むため同一プロセスの次リクエストから通常動作)で decode を
故意に失敗させ、500 応答後に GPU が ~2GB まで解放され、同一プロセスの次の
リクエストが正常に 200 を返すことを確認した実測 (2026-08-07、RTX PRO 5000 48GB + H3_LOWVRAM=1、768²・30steps・fbc+sage・
seed 12345):
| frames | デノイズ | デコード | ピークVRAM | 合計(毎リクエストの再ロード込み) | 品質(目視) |
|---|---|---|---|---|---|
| 22 (0.917s) | 29.0s (1.0s/step) | 1.8s | 35.0GB | 157s | 破綻なし・高品質 |
| 5 (0.208s) | 9.1s | 0.67s | 35.2GB | 125s | 破綻なし(史上初の実デコード成功) |
5フレームの音声 (0.208s) も非無音 (rms 0.055)。
「常駐モード (H3_LOWVRAM=group) なら再ロード固定費が消えて連続生成が速いはず」という
仮説を、物語3場面のプロンプト (frames=22・768²・30steps・seed固定) の3連続実行で実測した
(2026-08-07、RTX PRO 5000 48GB)。結果は逆で、lowvram=1 より約1.5倍遅い:
| モード | 1枚目 | 定常 (2枚目以降) | 定常の内訳 |
|---|---|---|---|
H3_LOWVRAM=1 | 157s | ~157s | TE+transformerロード ~120s + デノイズ 29s + デコード 2s |
H3_LOWVRAM=group | 310s (TEコールド込み) | ~240s | デノイズ 130s + デコード 2s + デコード後のTE再ロード 75-97s |
H3_LOWVRAM=1 のまま回すのが最速
(~157s/枚)。さらに縮めるのは下の /api/t2i_batch(この実測を受けて実装済み)/api/t2i_batch、2026-08-07 本実装)上の実測を受けて、H3_LOWVRAM=1 の固定費 (~110s: TEロード75-97s + transformerロード
~20-35s) をバッチ全体で1回に償却するエンドポイントを実装した
(core/runner.py の generate_still_batch())。generate() の lowvram choreography を
位相順に並べ替える:
entry : [nothing big resident]
encode : [TE-nf4] 全場面の setup/エンコード/layout/latents/timesteps
denoise : [transformer] 全場面を順にデノイズ
decode : [vae pair] 全場面を順にデコード → PNG/mp4 保存(場面ごとに保存
しながら進むので途中失敗でも完了分は残る)
POST /api/t2i_batch — multipart で prompts を場面数ぶん繰り返し送る
(最大24場面)。frames/resolution/num_inference_steps/seed/即反映パラメータは
全場面共通(変えられるのはプロンプトのみ)。UI は T2I タブの
「バッチ連続生成(1行=1場面)」チェックで、プロンプト欄の1行を1場面として送る_step_index = None に
戻すだけでよい(MiniMaxH3Scheduler.step() は index を timestep 値から再導出する)。
FirstBlockCache は場面ごとに _reset_stateful_cache() + cache_context
(generate() の per-request リセットの per-scene 版)/api/t2i とバッチの場面1で
mp4 と PNG がバイト完全一致(md5一致)。位相並べ替えは数学的に無影響
(なお group モード出力とは一致しない — FBC の効き方が実行経路で変わる既知の現象で、
バッチ固有の差ではない)H3_LOWVRAM=1 以外のモード(大モデル常駐)では位相並べ替えの利得がないため、
同じ API のまま逐次 generate() にフォールバックする(レスポンス形式は同一)実測 (2026-08-07、RTX PRO 5000 48GB + H3_LOWVRAM=1、3場面・frames=22・768²・30steps):
| 方式 | 合計 | 1枚あたり | 内訳 |
|---|---|---|---|
逐次 /api/t2i ×3 | ~471s | 157s | 毎回 ロード~120s + デノイズ29s + デコード2s |
/api/t2i_batch (3場面) | 202.6s | 67.5s | ロード~110s(1回) + エンコード0.9s + デノイズ84.8s + デコード6.9s |
場面追加の限界コストは ~31s/枚(デノイズ28-29s + デコード2.3s)なので、場面数が 増えるほど1枚あたりは 31s に漸近する(10場面で ~42s/枚、24場面で ~36s/枚の計算)。 ピークVRAMは 35.0GB で1枚生成と同じ(場面ごとに増えるのは潜在+prompt_embedsの数十MBのみ)。
/api/ref2va の still=1 と /api/ref2i_batch、2026-08-07 本実装)下記スパイクの成立を受けて本実装した。キャラクター参照から場面ごとの一貫した 静止画を作るモード。作った静止画は FL2VA の先頭フレームや次の Ref2VA の参照に回せる (物語の複数動画生成の素材作り)。
POST /api/ref2va に still=1 + frames(22既定|5)を追加で渡す。
seconds は無視され、中央フレーム PNG (ref2i_<ts>.png) がレスポンスの
image_url に付く。実装は t2i と同じ3点セット (setup step の間だけの尺ゲート緩和・
VAE 小クリップ修正・既存の decode 例外クリーンアップ) の流用POST /api/ref2i_batch — 共通の references + prompts(最大24場面)。
H3_LOWVRAM=1 では generate_ref2i_batch()(t2i_batch と同じ位相並べ替えの
ref2va 版: TE常駐で全場面エンコード → VAE窓1回で全場面の参照VAEエンコード →
layout/timesteps → transformer_ref 1回ロードで全デノイズ → まとめてデコード)。
他モードは逐次フォールバック。スケジューラ/FBC の場面間リセットは t2i_batch で
md5 一致まで実証済みの同じ手法実測 (2026-08-07、RTX PRO 5000 48GB + H3_LOWVRAM=1、赤ずきん参照1枚・3場面・
frames=22・768²・30steps): 合計 494.7s = 164.9s/枚(逐次 ~265s/枚の1.6倍速)。
内訳: エンコード位相 212.5s + デノイズ 178.1s (57.7-60.2s/場面) + デコード 6.6s。
ピークVRAM 36.8GB。品質・キャラ一貫性とも良好(3場面とも赤マントを保持し
各プロンプトに追従)。
H3_REF_PREFIX_CACHE、既定1、2026-08-08 本実装)ref バッチ (generate_ref_batch = ref2i_batch/ref2va_batch) のエンコード位相で、
参照ラベル+ビジョン (~4104トークン、~65s/場面) の Qwen3-VL エンコードが場面ごとに
重複していた問題の解消。ref2va のトークン列は「参照が前置・プロンプトは末尾に verbatim
追記」(packing_ref2va の build_ref2va_presentation で確認) で、条件付け元の Qwen3-VL は
因果 LM なので、参照プレフィックスのテキスト表現はプロンプトに依存しない —
プレフィックスを1回だけ use_cache=True で通して DynamicCache に焼き、場面ごとには
プロンプト末尾 (14-33トークン、~0.2s) だけをキャッシュ継続する
(core/runner.py の _encode_ref_prompts_shared_prefix())。
検証 (scripts/probe_ref_prefix_cache.py、transformers 5.14.1 の
modeling_qwen3_vl.py を読んだ上で実機実測):
torch.equal)attention_mask=None
(compute_3d_position_ids の arange 分岐 = past長からの連番 + rope_deltas 加算に
乗せる)、mm_token_type_ids/pixel_values/grid_thw 系は全て None
(image_grid_thw を渡すと model.rope_deltas が再計算・上書きされる)、場面ごとに
DynamicCache.crop(prefix_len) で切り戻して直列再利用。rope_deltas は
Qwen3VLModel のインスタンス状態なので、プレフィックス→全継続の間に他の TE 呼び出しを
挟んではならない (ヘルパー1呼び出し内で完結させる設計)E2E 実測 (ref2i_batch、赤ずきん参照1枚・同一3場面・seed 12345・768²・30steps):
| エンコード位相 | 合計 | 1枚あたり | |
|---|---|---|---|
| 共有なし (H3_REF_PREFIX_CACHE=0 相当) | 212.5s | 494.7s | 164.9s |
| 共有あり (既定) | 83.1s | 350.1s | 116.7s (-29%) |
品質: 共有なしの同一 seed 出力との PSNR 21.9-27.4dB。バッチ出力は従来経路と
ビット一致しない (プロンプト末尾側の ~1.5% 丸め差が軌道をドリフトさせる —
Sage Attention 既定化時と同種・同水準の epsilon 級ドリフト) が、目視で構図・品質・
キャラ一貫性とも同等。ビット再現が要る対照実験は H3_REF_PREFIX_CACHE=0 で従来経路に
戻せる。ref2va_batch (動画) もエンコード位相は同一コードなので同じ削減 (~130s/3場面)
がそのまま効く。参照VAEエンコード (~数s/場面) は共有せず場面ごとのまま
(効果が小さく、状態の別名参照リスクを増やさないため)。
/api/ref2va_batch、2026-08-08 本実装)ref2i と同じ位相並べ替え(実装は同一メソッド generate_ref_batch(still=False))を
通常尺の動画に適用したもの。物語の各場面の動画を、共通参照+プロンプト違いで
連続生成する。尺 (seconds) は全場面共通で必須(スケジューラの sigmas/timesteps 値の
場面間同一性が位相並べ替えの前提のため。音声参照からの尺自動導出もバッチでは不可)。
UI は Ref2VA / Ref2I タブの「バッチ連続生成(1行=1場面)」チェック(タブと独立に
使える: Ref2I+バッチ = ref2i_batch、Ref2VA+バッチ = ref2va_batch)。
実測 (2026-08-08、RTX PRO 5000 48GB + H3_LOWVRAM=1、赤ずきん参照1枚・2場面・
5秒・768²・30steps): 合計 803.2s = 401.6s/本(逐次 ~485s/本の17%短縮@2場面)。
内訳: エンコード位相 151.3s + デノイズ 503.4s (247.9-255.5s/場面) + デコード 25.4s。
ピークVRAM 40.5GB。場面追加の限界コストは ~330s/本(ビジョンエンコード ~65s +
デノイズ ~250s + デコード ~13s)なので、場面数が増えると約32%短縮に漸近する。
動画はデノイズ支配のため静止画バッチほど劇的ではないが、品質・キャラ一貫性は
逐次と同様に良好(2場面とも赤マントを保持しプロンプトに追従)。
「t2va で検証済みの22フレーム超短尺が、参照パッキング (packing_ref2va: 参照行が
生成行より多くなる) と重なっても品質が崩れないか」を実測した
(scripts/probe_ref2va_short.py、本体コード無変更・プローブ内 monkeypatch のみ)。
成立すればキャラクター一貫の場面静止画が量産でき、物語の複数動画生成の素材作りが
速くなる。
条件: 参照 = 本リポジトリの t2i 生成物 (赤ずきんの少女 PNG) 1枚、同一プロンプト
(<Picture 1> 参照付き)・seed 12345・30steps・768²。RTX PRO 5000 48GB +
H3_LOWVRAM=1。
| 条件 | デノイズ | デコード | ピークVRAM | wall (逐次・ロード込み) | 品質(中央フレーム目視) |
|---|---|---|---|---|---|
| short22 (0.917s) | 58.1s | 1.7s | 36.7GB | 265s | 破綻なし・高品質 |
| baseline (5.0s) | 243.1s | 10.6s | 40.7GB | 485s | 破綻なし(アンカー) |
generate_still_batch の ref2va 版) は未着手 — t2i と同じ
3点セット (尺ゲート緩和・VAE 小クリップ修正・例外クリーンアップ) は流用可能で、
バッチ位相並べ替えも t2i_batch と同じ手法 (スケジューラ _step_index リセット +
per-scene FBC リセット、参照が全場面共通なら参照エンコード1回共有) が使える見込みフレーム数は 17n+5 刻み(最小5=0.208秒、次が22=0.917秒)。プローブは monkeypatch のみで
検証した(scripts/probe_short_frames.py + _one.py)。当時の環境は RTX PRO 6000 96GB・
既定モード。
| フレーム数 | デノイズ(30steps) | デコード | 結果 |
|---|---|---|---|
| 5 (0.208s) | 完走 | 失敗 | VAEチャンク分割デコードの境界バグ(潜在2フレームで num_chunks=0 → torch.cat([]))→ 上記2で修正済み |
| 22 (0.917s) | 13.5s(124fの1/7.6) | 1.2s | 成功・品質は5秒基準と遜色なし(目視確認済み) |
| 124 (5s、基準) | 102.2s | 6.3s | 成功 |
H3_LOWVRAM=1 はデコード直前に transformer を解放し、次リクエストで毎回再ロードする
(実測 14.8〜32.7s の固定費、docs/RESIDENCY.md §5.5)。H3_KEEP_TRANSFORMER=1 はこの
解放をスキップし、transformer を GPU に常駐させたまま次のリクエストに持ち越すことで
この固定費を初回ロードの1回だけに収束させる。
成立条件は3つとも必須(core/runner.py の import 時ガードで強制、欠けたら
RuntimeError)。VRAM 収支の理由:
H3_LOWVRAM=1(raw "1" のみ。group は transformer をホストRAM常駐+ブロック
offload する別設計のため対象外)H3_TE_DEVICE 設定済み(TE を別GPUに常駐)— これが無いとエンコード位相が先に
破綻する: TE-nf4 17.45GB + 常駐transformer-int8 34.3GB = 51.75GB が実効予算を
超えるH3_VIDEO_VAE_FP16=1 — encode 位相は TE を別GPUに逃がすことで回避できるが、
decode 位相は fp32 デコードピーク16.29GBのままだと transformer 34.3 + 16.29 =
50.6GB で実効予算(~49.8GB)を超える。fp16 化した VAE のデコードピーク ~11.4GB
なら 34.3 + 11.4 = 45.7GB で収まる(video VAE fp16 の品質影響は既知実測 PSNR
39.97dB・目視差なし、詳細は「video VAE の fp16 化」節)既定(H3_KEEP_TRANSFORMER=0)は挙動不変。ref2va(transformer_ref)は対象外 —
このフラグの範囲外で、従来どおり毎回解放される。
# 推奨起動コマンド (48GB GPU0 + 20GB GPU1)
H3_LOWVRAM=1 H3_TE_PRUNE=1 H3_TE_DEVICE=cuda:1 H3_VIDEO_VAE_FP16=1 H3_KEEP_TRANSFORMER=1 \
venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8611
実機 E2E 実測(2026-08-09、48GB GPU0 + 20GB GPU1、上記起動構成):
665eadddea8f34298a1b5b89e69d4bd0)。ベースライン側は total 63.27s
(transformer ロード込み)/ peak 36.4GB高速化の系譜(48GB GPU0・H3_LOWVRAM=1 系、t2va は 768²・5秒):
| t2i | t2va 5秒 | |
|---|---|---|
| turboなし 30steps(GPU交換直後の素の構成、08-07) | 157s | 351.4s(デノイズ197.7s) |
| lightx2v turbo 4steps 導入(08-07朝) | 157s | 143s |
+ H3_TE_PREQUANT | 83.2s | — |
+ H3_TE_DEVICE | ~35s | 60.5s |
+ H3_KEEP_TRANSFORMER(本節) | 9.7s | 44.2s |
t2va は素の構成比 8.0倍(351.4s → 44.2s)。turbo だけでは 2.6倍(デノイズは7.6倍
短縮されるがロード固定費 ~110s が残る)で、残りは固定費撤廃
(H3_TE_PREQUANT/H3_TE_DEVICE/H3_KEEP_TRANSFORMER)の寄与。なお本節構成での
turboなし 30steps は t2i で 51.1s(上記実測)— 素の 157s に対し、30steps のままでも
固定費撤廃だけで3倍速い。
詳細な VRAM 収支の導出・位相×常駐表は docs/RESIDENCY.md §5.5・§5.6 を参照。
ブランチ migrate-pr14355。runner.py の t2i/t2va/still_batch/hires/turbo/FBC 経路を
マージ版の新契約(§1 の影響調査参照)へ移植し、venv を f37ab93 へ差し替え
(--no-deps --force-reinstall、他の依存は不変)。ref2va 系は第2段送り
(app.py/runner.py 両方にガード、503 + 明確なメッセージ)。
回帰結果(全て abc5e9b で記録したベースラインと同一 seed で比較):
| 経路 | 結果 | MD5 |
|---|---|---|
| t2i turbo 4steps seed=11 | 定常 9.24s(移行前 9.7s) | PNG 完全一致 |
| t2i turbo 4steps seed=12 | 9.24s | PNG 完全一致 |
| t2i 30steps seed=1 | 49.65s | PNG 完全一致 |
| t2va 5秒 turbo seed=21 | 41.54s、peak 44.15GB(同値) | MP4 完全一致 |
| t2i_batch 2場面 seed=11 | 16.47s(8.2s/枚) | 場面1 = 単発 seed=11 完全一致 |
数値経路(スケジューラ/transformer/VAE)が無変更という影響調査の結論どおり、 移行はビット等価。性能・VRAM ピークも移行前と同値。
移植で踏んだ新しい罠2つ(いずれも _execution_device の変種):
audio_vae が transformer より前に移動した(旧:
text_encoder, tokenizer, processor, vae, scheduler, audio_scheduler, transformer, video_processor, audio_vae → 新: image_processor, text_encoder, tokenizer, processor, vae, audio_vae, scheduler, audio_scheduler, transformer_ref, transformer, video_processor)。_pin_execution_device_to_compute() が text_encoder と vae しか
外していなかったため、CPU 常駐の audio_vae が最初の nn.Module として拾われ、
新レイアウトステップ(出力を _execution_device に置く契約)が position_ids を
CPU に作り、rope() 内で device mismatch(実測再現)。→ audio_vae も窓の間外す_execution_device が解決不能
(TE 外部常駐だと transformer 未ロード+TE デタッチ済み)→ テンソルが CPU に
生まれる。値は正しいので、デノイズ直前に _scene_state_to_compute() で明示的に
計算GPUへ運ぶ(既に GPU なら no-op)第1段(上節)の続き。ref2va / ref2i / refバッチを f37ab93 の新契約へ移植し、
旧ピン abc5e9b で採ったベースライン(一時的に main + abc5e9b へ戻して記録)と
同一 seed・同一参照画像・同一構成(H3_LOWVRAM=1 H3_TE_PRUNE=1、TE_DEVICE なし)で比較:
| 経路 (steps=8) | ベースライン | 移行後 | MD5 |
|---|---|---|---|
| ref2i seed=101 | 206.1s | 201.4s | PNG 完全一致 |
| ref2va 5秒 seed=102 | 332.5s | 322.5s(定常。初回496.8sはディスクキャッシュ冷え) | MP4 完全一致 |
| ref2i_batch 2場面 seed=101(KVプレフィックスキャッシュ経路) | 257.9s | 246.7s | 両場面 PNG 完全一致 |
設計判断2つ:
MiniMaxH3Blocks は3ワークフロー
(t2va/fl2va/ref2va)のサブブロックを合併した component specs を持ち、
transformer_ref と transformer の両スロットが同一シェルに存在する
(MiniMaxH3AutoDenoiseStep が呼び出しごとに state から分岐)。旧2シェル設計の
_ensure_pipe_ref_shell / _sync_shared_components_to_ref は self._pipe_ref = self._pipe
のエイリアスだけ残して no-op 化 -- 既存の VRAM 振り付け(_pipe_ref.transformer_ref を
触る多数の箇所)は無改修で生きるMiniMaxH3Ref2VATextEncoderStep も
プロンプトを presentation 末尾の emit(text(prompt)) として組むため、
「参照プレフィックス共有+プロンプト末尾の継続エンコード」という旧設計がそのまま成立。
削除された packing_ref2va の関数群の代わりに、新ステップ自身のインスタンスメソッド
(_gather_vision_features / _build_presentation)の上に再構築した
(DynamicCache / rope_deltas / 継続呼び出しの引数規約は旧実装の罠メモをそのまま踏襲)他の移植内容は第1段と同型(AfterDenoiseStep 挿入、MiniMaxH3PrepareConditionLatentsStep /
MiniMaxH3Ref2VAPrepareLatentsStep の新ステップ挿入、reference_kind() → entry.kind /
entry.has_audio、app.py の参照構築を MiniMaxH3ImageReference.from_file() 系へ)。
seconds=None の「音声参照から尺を導出」は新 SetupStep が内部でやらなくなったため
_num_frames_from_audio_reference として runner 側に実装。
これで 全経路がマージ版 f37ab93 上でビット等価。旧ピン abc5e9b へ戻す理由は無くなった。
タブが一列に増え続けるのを避けるため、出力の種別で2段に分けた:
| 段 | タブ(括弧内=入力) |
|---|---|
| 動画 | T2VA (テキスト) / FL2VA (フレーム) / Ref2VA (参照) |
| 静止画 | T2I (テキスト) / Ref2I (参照) |
段見出しが「出力」を、タブ名が「入力」を表すので、段×タブで 「動画×参照 = Ref2VA」「静止画×参照 = Ref2I」と読める。
Ref2I を独立タブにした理由: 以前は Ref2VA タブ内の「静止画」チェックだった
(T2I だけがタブ)。この非対称は「T2VA タブに静止画チェックを置くと、同じタブの
キーフレーム入力欄と両立しない(still=True は image/last_image と併用不可、
runner.py で ValueError)」ことに由来していたが、2段組みにすると
『静止画の段に参照ありモードが無い』方が不自然になるため独立させた。参照
アップロード欄は Ref2VA と共有しており(isRefMode())、違いは尺の決め方
(秒数 or フレーム数)と still フラグだけ。
バッチはタブにしない: バッチは静止画/動画の軸と直交する(Ref2I+バッチ =
/api/ref2i_batch、Ref2VA+バッチ = /api/ref2va_batch、T2I+バッチ =
/api/t2i_batch)。タブ化すると段が3列×2段に膨れるので、各タブ内のチェックのまま。
タブボタンは2行構造(1行目=モデル名、2行目=入力種別、.tab-btn small)。
パネル幅 378px ではボタン内幅 77px に対し「T2VA (テキスト)」が 95px で折り返し、
ボタン高さが不揃いになったため、最初から2行にして幅に依存しないようにした
(ブラウザ実測で全ボタン 50px 揃い・2行目の位置も y=26px で一致)。
出力が溜まるとページ表示が重くなる問題(実測: outputs 165本の時点でブラウザの描画が
詰まり、スクリーンショットが30秒でタイムアウト)。原因は <video preload="metadata">
がタイル数ぶん、表示と同時に全数メタデータを取りに行くこと。
<img> は loading="lazy" が効くが <video> は同属性を解釈しないので、
src を data-src に退避し IntersectionObserver(rootMargin: 300px で1画面先読み)で
ビューポートに近づいたときだけ設定する。再描画のたびに disconnect() してから
observe し直す(observer は監視中の要素への参照を保持するため、外さないと古いタイルが
解放されない)。
実測(ブラウザのネットワークログ): ページ読み込み時の mp4 リクエストが 165本 → 0本。PNG(55枚)はブラウザ標準の lazy に任せる。
画面右上のトグル(日本語 / English)で再読込なしに切り替わる。選択は
localStorage['h3_lang'] に保存し、初回は navigator.language で判定する。
I18N = { ja, en }、165キー)。参照は t('key', {vars}) で、
未訳キーは日本語へフォールバックするdata-i18n / data-i18n-html(<b> を含む文言)/
data-i18n-placeholder / data-i18n-title で印を付け、applyI18n() が一括で書き換えるrerenderDynamicI18n() が
ギャラリー・結果パネル・参照タイル・各種ヒント行を描画し直す(直近の結果は
lastResultData に保持して同じ描画関数へ渡す)翻訳しないもの: JSコメント(103行、開発者向けの設計メモ)、サーバが返すエラー
メッセージ(data.detail、app.py 側は日本語のまま)、te_quant: などの技術識別子、
言語トグル自身のラベル。
実機確認(ブラウザ): 日↔英の往復でギャラリータイル(静止画 ↔ Still)・
結果プレースホルダ・ヒント行まで切り替わること、リロードをまたいで選択が残ること、
6経路の送信先(/api/t2i, /api/ref2va+still=1, /api/ref2i_batch,
/api/ref2va_batch, /api/ref2va, /api/t2va)が切替後も不変であること、
コンソールエラーが無いことを確認済み。
fully_copy)— 実測と、そこで見つかったバグ動機: テキストエンコーダを小型モデル+投影行列で置き換える手法 (ClipProj-MiniMax-H3)は TE を 15.7GB → 5.2GB にできるが、作者の実測では発話が劣化する(4B は不明瞭、8B は言語を 捨てて英語で喋る)。ならば台詞を音声参照から入れれば TE の発話能力に依存しない、 という仮説の検証。
結果: 仮説は成立した。
| 検証項目 | 結果 |
|---|---|
| 台詞の内容が音声参照から伝わるか | 成立 — 検出言語 ja (0.964)、認識「今日は良い天気だね」、文字一致 100% |
| リップシンク | 成立 — 口の開閉と音声包絡の相関 +0.745(ずれ 0ms) |
| キャラクター一貫性 | 成立(顔・髪型・制服が参照どおり) |
| 音声の 1:1 コピー | 不成立 — 波形相関 0.112、尺 5.22s → 5.88s |
決定的なのは、プロンプトに日本語の台詞を1文字も書いていないこと(<d> タグ不使用、
「<Audio 1> の台詞に口を合わせる」とだけ指示)。それで同じ日本語が出た =
台詞がテキストエンコーダを経由していない。
fully_copy は名前に反して信号のコピーではない。 公式ガイドは
"The complete source audio serves as the target video's complete final audio track" /
"reused 1:1" と書いているが、実際の挙動は「同じ内容を再生成する」。声質は変わり、
尺もモデル側の都合(141フレーム=5.88秒)で決まる。元音声をそのまま使いたいなら生成後に
差し替える必要がある(相関 +0.745 のシンク精度があるので実用的と思われる。未検証)。
なお口が開いている区間 4.1秒に対し音声のある区間は 1.6秒で、発話後も口が動く傾向がある。 アニメ表現としては許容範囲だが、音素レベルの厳密な一致ではない。
条件: 96GB機、TE nf4 + transformer bf16(非量子化)、768×1344・141フレーム、30steps、 seed 777、総所要 553.9s、ピーク 87.67GB。
上記の検証で発火した。音声を含む参照を渡すと確実にクラッシュする:
sageattention/core.py: assert dtype in [torch.float16, torch.bfloat16]
AssertionError: Input tensors must be in dtype of torch.float16 or torch.bfloat16
(発生源: autoencoder_kl_minimax_h3_audio.py -> dispatch_attention_fn)
原因: MiniMaxH3AudioAttnProcessor は backend=self._attention_backend(既定 None)で
dispatch_attention_fn を呼ぶため、バックエンドがグローバルに解決される。本アプリは
set_attention_backend() を transformer / transformer_ref にしか呼んでいないが、
H3_ATTN_BACKEND=sage(既定)だと audio_vae の attention まで sage に流れる。ところが
audio_vae は設計上 fp32 固定(bf16 にすると音量が約20dB落ちるため)で、sage は
fp16/bf16 しか受け付けない。リクエストの attn= 上書きでも回避できない
(transformer 系にしか効かないため)。
修正: audio_vae のロード直後に audio_vae.set_attention_backend("native") を呼び、
このモジュールだけ native に固定する。音声 VAE は計算量が小さく sage の利得もない。
テストの穴だった: この経路は「音声つき参照」でしか通らないため、既存の ref2va 回帰 (48GB機・int8・画像参照のみ)を全てすり抜けていた。96GB機の非量子化 ref2va も マージ版追従後は未検証だった。修正の確認は sage 既定のまま音声参照つき ref2va が通ること (186.3s、日本語 ja 0.976・文字一致100%)と、画像参照のみの ref2i が壊れていないこと (133.9s)の両方で行った。
H3_TE_PROJ として実装(既定OFF、既存挙動は1バイトも変わらない)。TE を Qwen3-VL-32B から
Qwen3-VL-4B + 学習済み投影行列(ClipProj-MiniMax-H3)へ
置き換える経路。狙いは48GB機で TE と transformer を同時常駐させ、載せ替えの固定費を消すこと。
cond = ((h - mean_in) / std_in) @ W * std_out + mean_out # h = 4B の hidden_states[24]
cond[:, 0] = sink_out # token 0 はアテンションシンク
| 32B TE(現行) | 投影4B TE | |
|---|---|---|
| PSNR | — | 22.64 dB |
| 鮮鋭度(ラプラシアン分散) | 48 | 37(-23%) |
| 生成時間 | 65.7s | 43.7s |
| ピークVRAM | 88.73GB | 76.1GB |
| TE の GPU 実占有 | 21.02GB (nf4) | 8.88GB |
品質は「同じ絵」ではなく「同等品質の別の絵」。 構図・色調・時間帯の解釈は一致するが、 細部の指定は落ちる(実例: プロンプトにない範囲だが、32B版にあった手前の睡蓮の葉と草が 投影版では水面に置き換わった)。破綻はなく実用水準。これは投影の性質(配布元の報告で test cosine 0.712)どおりで、プロンプトの大意は保持し細部は失うと理解するのが正確。
この変更はこれまでの全最適化と種類が違う。 量子化・常駐制御・turbo などは全て同一seed MD5一致で「数学的に無影響」を証明できたが、投影TEは原理的に近似なので MD5 は使えない。 判定は PSNR + 目視 + VRAM 実測による。
| 対象 | 結果 |
|---|---|
| 通常テキスト・日本語 | ID 完全一致(H3 側のトークナイザをそのまま使える) |
<d> / </d> | H3 は 151669/151670 の単一トークン、4B は語彙外で分解される |
<|cutoff|> <|lyrics_*|> <|caption_*|> | H3 固有(151671〜675)だが公式ガイドも本アプリも未使用 |
[Shot n] <cutoff> <scenetrans> | 通常テキスト(特殊トークンではない)。マルチショット構成は影響なし |
→ 実使用で影響を受けるのは台詞タグ <d> だけ。実装では id>=151669 を含むプロンプトを
ValueError で明示的に拒否する(黙って別物を送らない)。台詞は音声参照(fully_copy)で
入れられることを同日に実証済み(上記セクション参照)なので、運用上の回避策がある。
docs/RESIDENCY.md §5.4 の導出式に実測 8.88GB を入れ直すと、H3_TE_DEVICE 用のカード要件が
20GB → 16GB(おそらく12GB) に下がる。
| 2枚目GPU | 実効予算 | 32B TE (17.45GB) | 投影4B TE (8.88GB) |
|---|---|---|---|
| 12GB | ~10.5GB | 不可 | 成立見込み(必要 ~9.2GB、余裕 ~1.3GB) |
| 16GB | ~14.5GB | 不可 | 成立見込み(余裕 ~5.3GB) |
| 20GB | ~19.7GB | t2va のみ成立・ref2va は OOM | 成立(余裕 ~10.5GB) |
12GB は実機確認なしに断言しない。 以前 20GB で「導出上は入るはずが実測 OOM」を 経験しており(単位の罠、§5.1)、余裕1.3GB は薄い。
H3_TE_PROJ_QUANT(既定 none / bnb-4bit / bnb-8bit)を追加した。4B 自体を
量子化する 4B 専用フラグで、32B 用の H3_TE_QUANT とは別物(排他ガードの意図を参照)。
投影行列は bf16 用のものをそのまま使うのが正解(実測)。量子化による条件付けのズレは NF4 で相対RMS 0.61〜0.96%(cosine 1.0000)。配布元の int8_convrot 用行列を使うと かえってズレが増える(1.02〜2.97%)— あれは ComfyUI の量子化方式専用の校正なため。
| 構成 | TE 常駐 | t2i (30steps) | t2va 5秒 (30steps) | ピーク(t2va) |
|---|---|---|---|---|
| 32B TE | 21.02GB | 65.7s | 162.1s | 91.9GB |
| 投影4B bf16 | 8.88GB | 43.7s | — | — |
| 投影4B NF4 | 3.11GB | 33.5s | 143.5s | 74.3GB |
動画でも品質確認済み(768²・5秒・124frames・同一seed 555): 静止画で見えた鮮鋭度低下(-23%)は動画では出ず(187 vs 191)、ちらつき(2階差分)は むしろ 11% 少ない(7.51 → 6.67)。bf16 vs NF4 の直接比較は PSNR 34.45dB で量子化の 影響はほぼゼロ。32B との PSNR 14.98dB は「劣化」ではなく「同じ指示の別テイク」 (32B版には対向者が現れ、投影版は少女単独、など解釈の分岐)。
設定APIとの関係: 投影TEは現状 env 専用(H3_TE_PROJ/H3_TE_PROJ_QUANT)。
/api/settings/apply から te_quant/te_prune を変えようとすると、runner の import 時
ガードを素通りした上で適用されないのにスナップショットは変わったと報告する穴が
あったため、投影TE有効時はこれらのフィールドを 400 で拒否するようにした
(/api/settings のスナップショットに読み取り専用の te_proj/te_proj_quant を追加)。
H3_TE_PROJ_QUANT の既定を bnb-4bit に変更(品質実測に基づく。none/bnb-8bit は
明示指定で選択可)。既定変更に伴い「量子化指定あり+投影OFF」ガードは明示指定
(in os.environ)のときだけ発火するよう修正 -- 既定値のままの通常ユーザーを
誤って落とさないため/api/settings/apply の
te_proj / te_proj_quant)。ON のとき te_quant/te_prune のコントロールは無効化され、
API 側も 400 で拒否する(判定は「適用後の値」ベース: OFF に戻しながら te_quant を
変えるのは合法)往復リロード E2E(実測、96GB機): OFF→ON 47.9s / ON→OFF 29.4s / 再ON 27.3s。 各段で同一 seed 生成の PNG MD5 を照合し、UI 経路の ON = env 経路の NF4 と完全一致、 OFF = 32B ベースラインと完全一致、再 ON = 1回目の ON と完全一致(状態残留なし、 投影行列の再ロード正常)。ON 中の te_quant 変更は 400。
logger.warning を出す
→ 2026-08-11 に目視検証済み: 参照は明確に効く(顔・髪・服・小物まで反映、32B 正解と
同水準。「2026-08-11: ref2va/i2va はどこまで動くか」の節参照)。warning は校正の事実の
記録として残置fully_copy との組み合わせ(台詞を音声から入れる運用が投影TEでも成立するか)ComfyUI の PR #15446(H3 の VAE を チャンク・ストリーミング化してデコードのVRAMを尺非依存にする)を移植すべきか判断するため、 まず自分たちのデコード位相のピーク 16.29GB が何でできているかを分解した。
| 内訳 | 実測 | 比率 |
|---|---|---|
| VAE 2本の重み(常駐) | 11.02 GB | 66% |
| video decode の活性化 | 3.08 GB | 19% |
postprocess_video | 0.00 GB | 0% |
| audio decode | 0.00 GB | 0% |
| uint8変換 + CPU転送 | 2.49 GB | 15% |
| 合計 | 16.59 GB | (README の実測 16.29GB とほぼ一致) |
分かったこと: ピークの2/3は重みであり、チャンク化では1バイトも減らない。 PR #15446 が対象にするのは活性化 3.08GB の部分で、移植しても削減は最大19%。
デコードのピークが尺に比例することは別途確認した(潜在32→48フレームで 3.08→4.80GB、 約30MB/フレームの線形)。つまり PR の指摘自体は正しく、長尺ほど効く。
分解して初めて気づいたが、私たち自身のコードが 2.49GB を積んでいた:
frames_uint8 = (video_tensor.permute(0,2,3,1).float().clamp(0,1) * 255).round().to(torch.uint8).cpu().numpy()
float() / clamp / *255 / round() が各々全長ぶんの中間テンソルを返し、最後に
uint8 版も作られる。フレームを8枚ずつ変換して CPU の出力配列へ直接書き込む
frames_to_uint8() に集約した(4箇所で同一コードだったものを1つに)。
| ピーク | |
|---|---|
| 現行(一括) | +2.65 GB |
| 改良(逐次8フレーム) | +0.03 GB(-99%) |
演算順序は現行と同一なので丸めも変わらない。同一seedの本番生成で PNG の MD5 が完全一致
(66a59ff92d653f1284cabe76bdb6501c)することを確認済み。
活性化 3.08GB(19%)に対し、上流の _decode を丸ごと置き換える monkeypatch が必要で、
追従コストが生じる。より安い 15% を先に取ったので、残りは
audio_vae の fp16 化(重み11GBの一部) を先に検討する方が期待値が高い
(現在 fp32 固定の根拠は「bf16 で音量が約20dB落ちる」という実測だが、fp16 は未検証。
bf16 は仮数部7ビット、fp16 は10ビットで、小振幅の音声では挙動が異なりうる)。
96GB機(RTX PRO 6000)に RTX 4060 Ti 16GB(sm_89)を cuda:1 として増設した。 最終ゴールは (A) 単体 16GB で動くこと、(B) 8GB×2 の2枚構成で動くこと。 その第1段として「投影TE(4B NF4)を 4060 Ti に常駐」を実測した。
起動: H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_TE_DEVICE=cuda:1(lowvram なし、
transformer は bf16 で GPU0 常駐)。
| 計測 | 値 | 備考 |
|---|---|---|
| GPU1 常駐 | 3213 MiB(3.37GB) | 導出 3.11GB+ε と一致。生成後も増えない |
| t2i 768² 定常 | 43.94s(デノイズ 13.48s、デコード 0.88s) | 前日の単騎 33.5s より約10s 増(下記) |
| t2va 5秒 768² | 134.58s(デノイズ 102.57s)、ピーク 71.19GB | |
| PSNR vs 32B 基準 | 22.36 dB(鮮鋭度 37 vs 基準48) | 旧行列の 22.49dB と同水準 |
| PSNR vs 旧行列NF4(単騎) | 39.29 dB | 再校正+GPU違いでも実質同一の画 |
| t2i PNG MD5 | 3cd088df882e37547219b9816a217b91 | 新行列+sm_89 なので旧アンカーとは不一致(想定どおり) |
H3_TE_DEVICE の組合せで rope の device 不一致クラッシュ。
TE を切り離すと _execution_device が CPU の audio_vae に落ちる既知の罠だが、
この組合せは今回が初走行で、非 lowvram 2分岐(catch-all / bnb-4bit+fl2va)の
layout〜timesteps が _pin_execution_device_to_compute() の外にあった。
self._te_external のときだけピンで包む修正(TE同居の通常経路はバイト単位で不変)。h3_qwen3vl_4b_tap24.safetensors を
obsolete/ へ移し、再校正版 mmh3-4b-ClipProj.safetensors に置換していた
(学習 1,666→5,664 プロンプト、cos_test 0.711→0.717、W の cosine 旧比 0.9596 =
実質別の関数)。既定を新ファイル名へ更新。旧行列はローカル HF キャッシュ
(snapshot 3f762f19)に残存し、H3_TE_PROJ に絶対パスを渡せば再現可能。デノイズ+デコードは 14.4s で変わらず、リクエスト毎の固定費が約29s(単騎時は約19s)。 差分の主因候補はエンコードが sm_89 の 4060 Ti 上で走ること(NF4 の dequant が遅い)+ cond の PCIe 転送。一方 t2va は 134.6s と悪化しておらず、尺が長いほど固定費は薄まる。 96GB機では TE は元々 GPU0 に載るのでこの箱で2枚目GPUを使う実益はない(本命は 16GB級単体・8GB×2 の構成検証)。
H3_LOWVRAM=group +
投影TE 同居(TE_DEVICE なし)。導出 3.11+1.4+6.6 = 11.1GB で載る見込みだが、
te_proj × group の組合せは未走行バラスト模擬ではなく、増設した実カードで CUDA_VISIBLE_DEVICES=1 により単体検証した。
起動: H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1 H3_ATTN_BACKEND=default(sage は sm_120 専用ビルドのため SDPA に戻す。TE_DEVICE なし =
投影TE同居。te_proj × group はこれが初走行 — 修正なしでそのまま動いた)。
| 計測 | 値 | 判定 |
|---|---|---|
| 起動直後の常駐 | 251 MiB(transformer は CPU、group offload) | |
| t2i 中ピーク | 7.4GB(nvidia-smi 7615MiB) | |
| t2va 5秒デノイズ中ピーク | 11.4GB(nvidia-smi 11681MiB) | 16GiB カードで残り約4.7GB |
| t2va デコード中 | ~7.0GB(fp16 デコード) | |
| t2i 定常 | 498.5s(デノイズ 479s、16.5s/step) | |
| t2va 5秒 | 1499s ≒ 25分(デノイズ 1445s、49.8s/step) |
VRAMの導出 11.1GB は実測 11.4GB でほぼ的中し、ゴールA(単体16GB)は残量たっぷりで成立。 OOM・エラーは一切なし。
group offload は毎ステップ int8 重み ~34GB を CPU→GPU に流すが、4060 Ti が刺さっている スロットは Gen3 x4(実効 ~3.5GB/s)。転送だけで ~10s/step になり、実測 16.5s/step と 整合する。Gen4 x16 のまともなスロットなら転送は ~8分の1になるので、この時間は 「16GB カードの性能」ではなく「この箱のスロット」の値。t2va の 49.8s/step は これに加えて 124 フレーム分の SDPA 計算(sm_89)が乗ったもの。
もう1つ: FBC キャッシュが1ステップも省けていない(cache_skipped_steps: 0、
閾値 0.05)。PRO 6000 + sage + bf16 の軌道では大半のステップが省けていたのとは対照的。
int8+SDPA の軌道では残差が閾値を下回らないらしい。閾値調整で最大2倍の短縮余地
(未検証、品質とのトレードオフ)。
PSNR は vs 32B 基準 7.40dB / vs 前段 7.43dB と数値上は壊滅だが、目視では別の話だった:
つまり int8+SDPA の組合せで生成の軌道が別のアトラクタに移り、結果的に プロンプト適合はむしろ良くなった。構成をまたぐ PSNR/MD5 比較はもともと成立しない (既知の int8 軌道分岐 ~19dB のさらに先)ので、構成間の品質は目視で判断すること。
両GPUを scripts/vram_ballast.py --target-free-gb 7.9(GiB)で headless 8GiB カード相当に
制限して模擬。計算側 = 4060 Ti、TE側 = PRO 6000(CUDA_VISIBLE_DEVICES=1,0 で
アプリの cuda:0/cuda:1 に割り当て)。起動はゴールAと同じ env + H3_TE_DEVICE=cuda:1
(te_proj × group × TE_DEVICE も初走行 — 無修正で動作)。
| 計測(8GiB×2) | 値 |
|---|---|
| t2i 768² | 成功。ピーク 6.4GB、total 512s |
| t2va 5秒 768² デノイズ | 29/29 完走(51.3s/step — 事前予測の「0.9GB超過」は外れ、収まった) |
| t2va 5秒 768² 全体 | 成功。total 1534s、decode 35.2s、APIピーク 7.23GB、実測ワークロード ~8069MiB |
解像度・尺の削減は不要だった(640²/3秒/512² の探索梯子は未使用)。
初回試行はデノイズ完走後、デコード末尾の1行で OOM した。上流 decoders.py の
MiniMaxH3VideoDecodeStep.__call__ 最終行 (video.float() * pixel_std + pixel_mean).clamp(0,1)
が fp16 の全長デコード結果を GPU 上で一括 fp32 化する — 768²・124フレームで
124×768×768×3×4B = 838MiB の一時確保(OOM メッセージの "Tried to allocate 838.00 MiB"
と完全一致)。frames_to_uint8 で潰した一括変換と同族の問題。
対処は venv 無改変ルールどおり runner 側サブクラス(_cpu_norm_video_decode_step()、
f37ab93 の __call__ を複製して1点だけ変更): fp16 のまま CPU へ移してから逆正規化。
要素毎の fp32 mul/add/clamp は CPU/GPU で IEEE754 の丸めが一致する(縮約も FMA 融合も
ない)ため出力はビット単位で同一 — 適用前後で同一 seed の PNG MD5 完全一致
(1a2a136b61234b4917465604ac35cca2)を実測確認。追加コストは fp16 全長 ~420MiB の
PCIe 転送1回のみ。全経路(t2va/t2i/ref2va/バッチ)に無条件適用で、どの構成でも
デコード位相のピークを全長 fp32 数本ぶん下げる(docs/RESIDENCY.md のデコード位相の
数値は次回更新時に再計測)。
残る注意は速度のみ(この箱は2番スロットが Gen3 x4 のため 16.5〜51s/step。 Gen4 x16 なら転送 ~8分の1)と、FBC が int8+SDPA 軌道で効いていない件(閾値調整は未検証)。
ゴールA/B の構成で参照系(ref2i / i2va=画像参照 ref2va / 音声参照 / 768×1344)を検証した。 初回は全滅だったが、原因は VRAM ではなく今日まで一度も併用されなかった組合せで 発火する潜伏バグ2件だった。修正後は境界が VRAM 量で素直に決まるようになった。
H3_VIDEO_VAE_FP16=1 × 参照ありは VRAM 量に関係なく必ず落ちる(dtype 不整合)H3_VIDEO_VAE_FP16=1 は VAE 重みを fp16 に恒久キャストする。デコードは上流ステップが
自前の fp16 autocast を張るので整合するが、エンコード側(encoders.py の
encode_vae_condition — ref2va の参照と fl2va のキーフレーム条件付けが使用)は autocast
なしで、内部で明示的に fp32 化した画素を vae.encode() に渡す →
Input type (float) and bias type (c10::Half) should be the same で即死。96GB でも落ちる。
これまでの ref2va 回帰がすべて fp32 VAE 構成だったため潜伏していた。
修正: _load_vae で fp16 化直後に vae.encode をデコード側と対称の fp16 autocast で
ラップ。精度は設計内(encode_vae_condition は元々、結果を自分で fp16 に丸めてから返す)。
group offload(use_stream=True)は int8 重み ~34GB を pinned memory に置く。
_free_transformer の del+gc ではページが torch のホスト側キャッシングアロケータに
保持されたまま OS に返らず(torch.cuda.empty_cache() はデバイス側のみ)、MemAvailable が
~34GB 不足したままになり、後続の transformer_ref ロードが RAM ガード(40GB 要求)で拒否される。
実測: 解放後も avail 38.6GB(RssShmem 45.6GB 残留)。
修正: _free_transformer / _free_transformer_ref に、group モード時のみ
torch._C._host_emptyCache()(私有APIのため getattr ガード付き)を追加。修正後は解放直後に
avail 85.8GB へ完全回復し、t2va→ref2va 切替が group モードで初めて成立した。
| テスト | 8GiB×2 | 16GB単体(TE同居) |
|---|---|---|
| ref2i(参照静止画 768²) | ○ ピーク6.69GB、1059s | (8GBで成立のため未実施) |
| i2va(768² 5秒) | × デノイズOOM | ○ ピーク9.41GB、~39分 |
| 音声参照(looped 6.9s→7.29s生成) | × デノイズOOM | ○ ピーク11.96GB、~54分 |
| 768×1344 5秒 | × デノイズOOM | ○ ピーク13.37GB(nvidia実測15.2GB)、~66分 |
「投影行列はテキストのみで校正」のため vision 品質は未知だったが、目視では劣化を検出できず:
参照人物の顔・前髪・髪の長さ・カーディガンの色・白リボン・ネックレスまで一貫して反映され、
i2va の絵作り(横顔構図)は 32B 正解 test1_walk_park.mp4 とほぼ同じ。音声参照では
リップ動作も明確。唯一 768×1344 の終盤に軽い構図ドリフト(破綻ではない)。
生成物: outputs/ref2i_1786447720.png / ref2va_1786449645.mp4 / ref2va_1786452054.mp4 /
ref2va_1786455323.mp4。
低VRAM検証で外していたバラストを撤去し、96GB機(RTX PRO 6000 + 2枚目の 4060 Ti 16GB)で 使える高速化を全部入れたときの上限を測った。
構成: H3_LOWVRAM=1 H3_KEEP_TRANSFORMER=1 H3_VIDEO_VAE_FP16=1 H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_TE_DEVICE=cuda:1 H3_TURBO_LORA=1
— GPU0 に int8 transformer 常駐(turbo LoRA 適用済み)、GPU1 に投影TE NF4(3.11GB)常駐、
sage attention(sm_120)、fp16 デコード。turbo は per-request で切替。
| 設定 | t2i 定常 | t2va 5秒 | ピーク |
|---|---|---|---|
| turbo 4steps(FBC は turbo により自動 OFF) | 7.65s | 28.56s | 42.5GB |
| 30steps + FBC(t2i 8 / t2va 7 ステップskip) | 21.41s | 121.5s | 41.8GB |
| 30steps・FBC なし | 27.42s | 155.0s | 42.5GB |
| (参考)96GB 既定・30steps・FBC あり(従来の基準) | — | 約160s | 92GB |
内訳(turbo): t2i = デノイズ 2.40s + デコード 1.07s + 残り約4.2s、t2va = デノイズ 14.81s + デコード 7.20s。48GB機の記録(t2i 9.7s / t2va 44.2s)を更新した。従来の 96GB 既定 (約160s)からは 5.6倍。
注目すべきはピーク 42.5GB — 最速構成は 96GB を使い切っておらず、速さの源は容量ではなく 「固定費ゼロ運用」の側にある(同じ構成は 48GB 級にも載る)。
H3_LOWVRAM=1 の振り付けは TE を毎リクエスト ロード→エンコード→解放する。32B TE(21GB)を
前提にした設計だが、投影TE は 3.11GB しかないのに同じ扱いを受け、7.6s のロードを毎回払う
(55GB 空いているのに、である)。H3_TE_DEVICE=cuda:1 で2枚目GPUに常駐させると:
| TE の置き場所 | t2i 定常 | t2va 5秒 |
|---|---|---|
| GPU0 同居(毎回ロード/解放) | 15.23s | 35.69s |
GPU1 常駐(H3_TE_DEVICE=cuda:1) | 7.65s | 28.56s |
同じ「全部盛り」でも TE の置き場所だけで t2i は2倍違う。なお 96GB機の2枚目は 16GB なので 32B TE(nf4 21GB / 削除版 17.45GB)は載らず、この常駐は投影TEだからこそ成立する。
96GB なら bf16 transformer(66.3GB)が載るので、int8 の逆量子化オーバーヘッドを避けられる
はず、と考えて plain モード(H3_LOWVRAM なし)でも測った。デノイズは確かに bf16 が速い
(t2i 2.07s vs int8 2.40s、t2va 14.05s vs 14.81s = 5〜14%速い)。ところが合計では負ける:
| transformer | t2i 定常 | t2va 5秒 | ピーク |
|---|---|---|---|
int8 常駐(H3_KEEP_TRANSFORMER=1) | 7.65s | 28.56s | 42.5GB |
| bf16 plain モード | 19.9s | 40.0s | 68.9〜73.1GB |
ログを見ると、plain モードはデコードのたびに transformer を解放し、直後に 11.9〜12.3s かけて
再ロードしている。この解放は「TE-nf4 21GB + transformer 66.3GB + VAE 11GB = 98.5GB > 96GB」
という前提で入ったものだが、TE が GPU0 に居ない今は 66.3 + 6.1(fp16 VAE)= 72.4GB で収まる
ので、この構成では不要な解放になっている。外せば bf16 が最速になる見込み(t2i ~8s / t2va ~28s
かつ全精度重み)。H3_KEEP_TRANSFORMER は現状 H3_LOWVRAM=1 を要求するので、plain モード側にも
同種の「解放しない」判断を入れるのが筋。未実装。
上の「無駄2」をその場で潰した。変更は import 時ガードの条件1だけ:
H3_KEEP_TRANSFORMER は H3_LOWVRAM=1 を要求していたが、これを「group でないこと」に
緩め、plain モード(H3_LOWVRAM=0)でもデコード窓の解放をスキップできるようにした。
解放をスキップする分岐(if H3_KEEP_TRANSFORMER: pass)は元から共通で、復元側の
_ensure_transformer は冪等なので 追加の実装は不要だった。残り2条件
(TE が GPU0 に居ない = H3_TE_DEVICE か H3_TE_PROJ / H3_VIDEO_VAE_FP16=1)は
そのまま plain モードの成立条件でもある(66.3 + fp16 デコード 11.4 = 77.7GB)。
| 構成 | t2i 定常 | t2va 5秒 | ピーク | GPU |
|---|---|---|---|---|
| bf16 + TE@GPU1 + 解放停止(案B) | 6.89s | 26.8s | 74.2GB + 3.2GB | 2枚 |
| bf16 単騎(TE も GPU0)+ 解放停止(案A) | 7.08s | 27.04s | 77.3GB | 1枚 |
| int8 単騎 + 解放停止(案C) | 7.40s | 28.13s | 45.6GB | 1枚 |
int8 + H3_LOWVRAM=1 + KEEP + TE@GPU1(前節の最速) | 7.65s | 28.56s | 42.5GB | 2枚 |
| int8 単騎・解放あり(案C の KEEP=0) | 19.58s | — | 39.8GB | 1枚 |
| bf16 + TE@GPU1・解放あり(前節) | 19.9s | 40.0s | 68.9GB | 2枚 |
等価性: 案C と「同条件で H3_KEEP_TRANSFORMER=0」の PNG が MD5 完全一致
(596a718e4b5cf9a0b907d2ec479225d2)。解放の停止は数学的に無影響で、同じ絵が
**19.58s → 7.40s(2.6倍)**で出る。
上の最速構成(int8 単騎・解放停止・ピーク45.6GB)で参照系も測った。参照は
transformer_ref(別モデル)を使うので収支も律速も t2va とは別物になる。
turbo=1 を渡しているのに steps=30 のまま走っていた。原因は app.py の
参照系3エンドポイント (/api/ref2va, /api/ref2i_batch, /api/ref2va_batch) だけが
num_inference_steps: int = Form(30) をハードコードしていたこと(t2va/t2i/t2i_batch/fl2va は
DEFAULT_NUM_INFERENCE_STEPS = turbo 時 4 を使う)。turbo LoRA 自体は transformer_ref へ
正しく適用されていた(ログに turbo LoRA lazily applied to transformer_ref (312 layers wrapped))
ので、「蒸留LoRA を付けたまま 30 ステップ回す」という噛み合わない組合せになっていた。
3箇所を DEFAULT_NUM_INFERENCE_STEPS に揃えて解消。
| モード | turbo 4steps | 30steps(turboなし) | デノイズ(4/30) | ピーク |
|---|---|---|---|---|
| ref2i(参照付き静止画・22フレーム) | 79.3s | 148.4s | 7.8s / 72.1s | 45.4GB |
| i2va(画像参照→動画5秒・124フレーム) | 103.1s | 290.3s | 22.0s / 209.0s | 45.9GB |
ピークは 45.9GB で t2va 系(45.6GB)とほぼ同じ — 参照系も 48GB 級1枚に収まる。 turbo の効きが t2va(5.5倍)より小さい(i2va で 2.8倍)のは、下記のとおり デノイズ以外の固定費が支配的だから。
turbo LoRA は transformer でしか実測しておらず transformer_ref は未検証だったが、
4ステップでも参照人物の忠実度は保たれた: 前髪・髪型・薄緑カーディガン・白リボン・
ネックレスまで一致し、動画も先頭〜中央〜末尾で人物が一貫、カメラ追従・公園の情景とも
プロンプトどおりで破綻なし(outputs/ref2i_1786509275.png /
outputs/ref2va_1786509457.mp4)。strength 0.094 のまま参照条件付きの軌道でも成立する。
| 位相 | 時間 | 備考 |
|---|---|---|
| 参照のビジョンエンコード | 約47s | 最大の律速。4B 投影TE でも縮まない(32B 時代の ~65s/場面から改善はしている) |
| デノイズ(4steps) | 22.0s | |
| デコード + VAE 往復 | 約10s | |
| 参照のVAEエンコード | 約6s | |
| 末尾で t2va 用 transformer を再ロード | 約13s | 連続 ref2va では純粋な無駄 |
H3_REF_PREFIX_CACHE)があるが、単発リクエストの繰り返しでは共有されない
(同じ参照画像を使い回す運用ではプロセス跨ぎのキャッシュが効くはず — 未実装)。H3_KEEP_TRANSFORMER と同種の「戻さない」判断を
入れられる余地がある(未実装)。「この2つを潰せば i2va 103s → 45s 程度」という見積もりは、バッチ実測で支持された。 バッチ経路は「参照エンコードを共有したらどうなるか」を実際にやっている実験そのもので、 768² に揃えた計測では:
| 単発 | バッチ1件あたり | 実測削減 | 共有モデルの予測 | 差 | |
|---|---|---|---|---|---|
| ref2i(3場面) | 79.3s | 47.0s | 32.3s/枚 | 31.3s/枚 | +1.0s |
| i2va(2場面) | 103.1s | 75.0s | 28.1s/本 | 23.5s/本 | +4.6s |
静止画・動画とも予測どおり(むしろ予測より少し良い)。バッチのステップ時間は単発と 一致している(i2va 7.321s vs 7.323s)ので、差は純粋に「47s のエンコードを何回払うか」だけ。 したがって リクエスト跨ぎのキャッシュ(47s)+ 再ロード撤廃(13s)→ 103.1 − 47 − 13 ≒ 43s という見積もりは、現時点の証拠と整合する(実装はまだ)。
一度は誤って取り下げた: 最初のバッチ計測でバッチ側にだけ
height/widthを渡し忘れ、 1344×768(=768² の 1.75倍のピクセル)で生成していたため「動画では共有の利得が出ない」と 誤読して見積もりを撤回した。解像度を揃えたら一致した。経路間の比較では解像度を明示する — 前付けの「バッチ」節の囲みも参照。
上流 lightx2v/Minimax-h3-Turbo が 本日 v1.0 を公開した(v0.1 に加えて2種)。上流 ModelTC/Minimax-H3-Turbo の Model specs:
| モデル | 学習解像度 | shift(video/audio) | 推奨NFE | metadata alpha |
|---|---|---|---|---|
| 4-step v0.1(現既定) | 544p 混合 | 12 / 3 | 4 | なし(実測 scale 0.094) |
| 8-step v1.0 | 544p 混合 | 12 / 3 | 8(4も可) | 8 → scale 8/128 = 0.0625 |
| 4-step v1.0 768p | 768p(1344×768) | 6 / 3 | 4 | 128 → scale 1.0 |
ファイル差し替えだけでは正しく動かないのが要点: 3ファイルは同一キー構造(rank 128)だが
metadata の alpha が全部違い、適用 scale は 0.094 / 0.0625 / 1.0 と三者三様。さらに
768p 版は video shift 6 で蒸留されており、H3 既定の 12 のままだとサンプリング格子が
蒸留時とずれる。このため H3_VIDEO_SHIFT / H3_AUDIO_SHIFT(既定は空 = 触らない)を
追加し、スケジューラロード直後に set_shift() で上書きできるようにした。
| 構成 | t2i 768² | t2va 5秒 768² | 品質(目視、seed 4242 の雪山湖) |
|---|---|---|---|
| v0.1 4step(scale 0.094、現既定) | 7.40s | 28.13s | 岩肌・反射に市松状アーティファクト顕著 |
| 8step v1.0 @8steps(scale 0.0625) | 10.94s | 49.61s | 非常にクリーン(光条まで出る) |
| 8step v1.0 @4steps | 7.47s | — | v0.1 同等〜やや良 |
768p v1.0 @4steps(scale 1.0 + H3_VIDEO_SHIFT=6) | 7.59s | 28.32s | 8steps 級にクリーン、v0.1 のアーティファクトほぼ消滅 |
| 同・1344×768(学習解像度) | 10.53s | — | 特に良好(雲・霧・樹木の質感) |
結論: 768p v1.0 が本命。 速度は v0.1 と同じ 4steps のまま、品質だけが 8steps 級に上がる。 アニメ調の t2va(seed 555)も目視で高品質・破綻なし。
現状の H3_VIDEO_SHIFT はプロセス全体に効く。turbo はリクエスト毎に on/off できるため、
768p 版を既定にして shift 6 を常時掛けると、turbo=0 の 30 ステップ生成まで shift 6 で
走ってしまう(ベースモデルは shift 12 前提)。既定化するなら「実効 turbo 状態に応じて
リクエスト毎に set_shift を切り替える」実装が必要。あわせて scale もファイル metadata の
alpha から自動導出(alpha/rank、無ければ従来の 0.094)にするのが筋。未実装。
それまでの 768p 版の使い方(env 3点セット):
H3_TURBO_LORA=1 H3_TURBO_LORA_FILE=minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors \
H3_TURBO_LORA_SCALE=1.0 H3_VIDEO_SHIFT=6 ...
→ 既定化後は H3_TURBO_LORA=1 だけでこの構成になる。
上の2点(scale のファイル metadata 自動導出・shift のリクエスト単位切替)を実装し、
core/runner.py の既定 LoRA ファイルを 768p v1.0 へ切り替えた。
resolve_turbo_lora_scale(lora_format, lora_path): H3_TURBO_LORA_SCALE 未指定時、
lora_path の safetensors metadata に alpha があれば scale = alpha / rank
(rank はソート済み最初の lora_A キーの shape[0]、3ファイルとも128) を使う。
alpha が無ければ従来どおり形式別既定(diffusers=0.094)にフォールバック。解決した
scale と出典を logger.info で1行出す。H3_TURBO_VIDEO_SHIFT(新 env、既定は空): 空のときは H3_TURBO_LORA_FILE に
_768p を含むかどうかで自動判定(768p系のみ 6.0、それ以外は「切替なし」= None)。
MiniMaxH3Runner._apply_turbo_video_shift(turbo_effective) が
generate()/generate_still_batch()/generate_ref2va()/generate_ref_batch()
それぞれのリクエスト先頭(MiniMaxH3SetTimestepsStep より前、バッチは場面ループの外)
で呼ばれ、instant["turbo"](リクエストの実効 turbo 状態)に応じて
scheduler.set_shift(turbo_shift if turbo_effective else base_video_shift) を行う
(base_video_shift は _ensure_vaes が H3_VIDEO_SHIFT 適用後に記録した値、
常に 12.0 とは限らない)。現在値と同じなら no-op。H3_TURBO_VIDEO_SHIFT is None
(768p 以外のファイル)のプロセスでは呼び出し自体が即 return する。H3_TURBO_LORA_FILE の既定を
minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors に変更。旧 v0.1 に戻すには
env で明示すればよい(scale はその場で 0.094 フォールバック、shift 自動切替なしに戻る)。設計からの逸脱は無し(タスク仕様どおりの実装)。
H3_KEEP_TRANSFORMER=1 H3_VIDEO_VAE_FP16=1 H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_TURBO_LORA=1`、FILE/SCALE/SHIFT は一切未指定 = 新既定のみ)
| 検証 | 結果 |
|---|---|
1. ユニット: resolve_turbo_lora_scale 3ファイル | v0.1→0.094 / 8step→0.0625 / 768p→1.0000、いずれも期待値と一致 |
| 2. 起動ログ | 768p ファイル選択・turbo LoRA scale resolved: 1.0000 (source=metadata alpha=128, rank=128, ...) を確認 |
| 3. turbo=1 (既定4steps) t2i seed 4242 | 8.95〜9.16s、ログに turbo video scheduler shift applied: 6.000 (turbo=True, ..., base=12.000)。PNG はクリーン、市松アーティファクトなし |
| 4. turbo=0 30steps 等価性 | 既存ベースライン outputs/t2i_1786507458.png(MD5 596a718e...)は実は「v0.1・turbo=1・4steps」の結果(README「その2」の H3_KEEP_TRANSFORMER 等価性検証の記録と一致)であって turbo=0/30steps ではなかったため、単純比較は無効と判明。turbo=0/30steps 自体は本タスクの往復検証(次項)で自己無矛盾を確認した |
| 5. 往復リーク検査(turbo1→turbo0(30)→turbo1→turbo0(30)) | turbo=1 側3回すべて MD5 d8bdffed4dce7694c2daf141f70f41a2 一致、turbo=0(30steps)側3回すべて MD5 f3622b6d0229feac608f2e4f666aa410 一致。shift の適用/復元ログもリクエストごとに1回ずつ、期待どおり増減。リークなし |
| 6. ref2va still=1 (ref2i) turbo=1 スモーク | 完走(147.0s、初回のみ transformer_ref ロード込み)。ログに shift 適用行あり。品質は参照人物の特徴(前髪・薄緑カーディガン・ネックレス)を保持、破綻なし |
7. 旧ファイル後方互換(H3_TURBO_LORA_FILE=...v0.1...) | 起動ログで scale=0.0940 (source=format-default)、shift ログは一切出ない(=切替なし)。t2i turbo=1 完走、PNG MD5 596a718e4b5cf9a0b907d2ec479225d2 が旧デフォルト構成の既知値と完全一致(岩肌の市松アーティファクトも再現、劣化ではなく忠実な再現) |
検証4で判明した重要事項: タスク前提の「outputs/t2i_1786507458.png = turbo=0/30steps ベースライン」は誤りだった
(実体は v0.1・turbo=1・4steps の結果)。shift 復元に実際のバグは無かった —
往復検証(5)の自己無矛盾で正しさを確認済み。今後 turbo=0/30steps の基準値が要るときは、
このタスクの検証5で得た f3622b6d0229feac608f2e4f666aa410(seed=4242, 768x768, 22frames,
int8単騎+解放停止+投影TE構成)を新しい基準として使える。
変更前後の等価性も後追いで成立(同日、親側で確認): 本来意図していた「旧コードの
turbo=0/30steps」の実物は outputs/t2i_1786510342.png(既定化前のコード・同構成で
同日実測した 30steps)で、その MD5 は f3622b6d0229feac608f2e4f666aa410 — 新コードの
turbo=0 出力と完全一致。つまり「既定 LoRA を 768p に替え、リクエスト毎 shift 切替を
入れても、turbo を使わない生成は1ビットも変わらない」ことがコード変更をまたいで実証された。
mute)H3 は動画と音声を同一の transformer forward で同時に生成するomni-modal モデルで、
音声の潜在はレイアウト段で位置ごと確保され、毎ステップ audio_hidden_states として
デノイズ本体に入る。つまり音声生成そのものは止められない(止めるには packed sequence の
再設計が要る = モデル構造いじり)。そこで出力段で音声トラックを付けない方式にした。
_mux_mp4(..., mute=False) に引数を追加し、mute=True のときは AAC ストリームを
そもそも add_stream しない。全4生成経路(generate / generate_still_batch /
generate_ref2va / generate_ref_batch)に mute を通し、API 7エンドポイント
(t2va/t2i/t2i_batch/fl2va/ref2va/ref2i_batch/ref2va_batch)の
mute フォーム項目、UI のチェックボックス「無音で出力(音声トラックを付けない)」
(日英 i18n)まで配線した。結果 JSON に mute を返す。実測(96GB機・新既定 turbo 768p・768²):
| 検証 | 結果 |
|---|---|
t2i mute=1 / mute=0 の mp4 | 音声stream 0本 / 1本(169KB vs 185KB) |
| 同 seed の映像側 | PNG MD5 完全一致(d8bdffed…)= 映像は1ビットも変わらない |
t2va 5秒 mute=1 | 音声stream 0本、768×768・124フレーム、27.51s |
ref2i mute=1 | 音声stream 0本、768×768・22フレーム、143.17s |
台詞は <d> タグで明示的に入れる仕様なので、使わなければ声は入らない
(環境音・BGM は残る)。overall_soundscape に環境音だけ書き non_diegetic_music を
空にすれば BGM も入らない。ただしプロンプトで「無音」と指示しても完全な無音にはならない
(実測の audio_rms は常に 0.065〜0.212 の範囲で、0 は観測していない)ので、
確実に無音が要るときは mute を使うこと。
<d> を使っていないのに声が出る」の原因調査ユーザー報告「<d> を使わないのに声が生成される」を調べた。原因は2つあり、片方は
こちらの不具合、もう片方はモデルの性質だった。あわせて「以前の説明(<d> を使わなければ
声は入らない)は不正確だった」ことを記録しておく — <d> は台詞を指定するためのタグで、
声が出るかどうかを制御するものではない。
聴かずに判定するため、音声帯域(300-3400Hz)のエンベロープが音節レート(4-8Hz)で
どれだけ変調されているかを測る指標を用意した(speech_score = 4-8Hz の変調エネルギー /
0.5-20Hz 全体)。人の声はこの帯域に強い変調を持ち、風・水・環境音は持たない。
| 生成 | rms | speech_score |
|---|---|---|
| 女性が公園を散歩(LLM強化が**「子供たちの笑い声」を挿入**) | 0.207 | 0.307 |
| 通学路の少女(声の指示は一切なし) | 0.033 | 0.298 |
| 同・プロンプトで明示的に声を禁止 | 0.0071 | 0.272 |
| 同・LLM修正後の強化文で生成 | 0.0104 | 0.269 |
| 雪山の湖(人が映っていない) | 0.364 | 0.125 |
core/llm.py のストーリーボード用 few-shot 例が「商店街の雑踏と呼び込みの声」で、
モデルがこれを真似て「遠くで聞こえる子供たちの笑い声」等を勝手に足していた。H3 は音の記述に
忠実なので、そのとおり声が生成される。共通ルールに「ユーザーが声を求めていない限り、
音の記述に人の声(台詞・話し声・笑い声・歓声・呼び込み・歌声)を入れない」を追加した。
修正後、同じ入力「公園を散歩する女性」の強化結果は風・枯れ葉・鳥の声のみになり、
人の声は消えた。
声の指示が一切なくても speech_score 0.298(人の映らない風景は 0.125)。プロンプトで 明示的に声を禁止しても 0.272 までしか下がらない。つまり H3 は人物が画面にいると 声らしい音を出す傾向があり、プロンプトでは消し切れない。
ただし音量は大きく下がる: 声禁止の指示で rms 0.033 → 0.0071(約1/4.6、実用上ほぼ 聞こえない水準)。LLM修正後の生成も rms 0.0104 と静か。
| やりたいこと | 手段 |
|---|---|
| 台詞(スクリプト)を入れない | <d> を使わない(これは従来どおり有効) |
| 声・生活音を減らしたい | 音の記述を環境音・自然音だけにする(LLM強化は修正済みで自動的にそうなる)+ 必要なら「人の声なし」を明記 → 音量は 1/4.6 まで下がるが完全には消えない |
| 確実に声・音をゼロにしたい | mute(無音で出力)を使う — 音声トラックごと落とすので確実 |
<d>)と投影TEの非互換を、ユーザーに伝える形にした「ユーザー入力に『おはよう』のように台詞を入れると声が出るのか」を実機で確認した結果、 出る仕様だが、経路によって扱いが分かれ、既定構成では正式な経路が塞がっていることが 分かったので、案内を整備した(TEの自動切替はしない — 32B TE は常駐 21GB で速度も 落ちるため、切り替えるかはオペレーターの判断に委ねる方針)。
| 強化モード | 出力 | 生成結果 |
|---|---|---|
| h3-official | 話者ID付きで <d>[Japanese] おはよう</d> に展開(H3 本来の形式) | 投影TE では 400 拒否 |
| storyboard | <d> を使わず地の文で「女性の『おはよう』という澄んだ声」 | 生成成功、speech_score 0.370(全測定で最高。声の指示なし 0.298 / 無人風景 0.125) |
つまり H3 は地の文の発話記述にも反応して声を出すが、<d> のように台詞内容を保証する
わけではない。なお「声を勝手に足さない」ルール(その6)は、ユーザーが挨拶を求めている
ケースを正しく通した(声が含意されているため)。
_reject_unsupported_proj_tokens() の文面はそのまま UI のエラー表示に出るので、
原因だけでなく選択肢を明示するようにした:
台詞タグ <d>…</d> は現在の設定では使えません。…
(1) 【推奨】UI の「再ロードが必要な設定」パネルで 投影TE を OFF にして適用する
→ 32B TE に切り替わり <d> が使えます(再起動不要。ただし TE 常駐が 3.11GB → 21GB に
増え、生成も遅くなります)。
(2) 台詞を音声参照 (fully_copy) で入れる → 投影TE のままで使えます。
(3) <d> タグを外し、地の文で「〜と挨拶する」のように書く
→ 発話らしい音は出ますが、台詞の内容は保証されません。
エラーは生成リクエストを消費してから返るので、プロンプト欄に <d> があり、かつ
投影TE が ON のあいだは常時ヒントを出すようにした(updateInstantHint() に
promptHasDialogueTag() を追加、プロンプト入力と投影TEトグルの両方で再評価。日英 i18n)。
⚠ 台詞タグ
<d>は投影TEでは使えません。「再ロードが必要な設定」パネルで投影TEを OFF(=32B TE)にして適用してください(再起動不要、常駐は3.11GB→21GBに増加)。 このまま生成するとエラーになります。
storyboard 強化の出力に「クローズアップ、85mm(100mmに修正)、固定」という文字列が 混入していた。焦点距離は 35/50/65/100mm のみ許可というルールに対し、LLM が誤った値を 書いたあと括弧で自己訂正し、そのメタ文が本文に残ってそのまま H3 へ渡る。指示だけでは LLM は時々破るので、指示と後処理の二段で対処した:
_sanitize_enhanced() を全4モード(brief/storyboard/translate/
h3-official)の戻り値に適用。(a)「NNmm(MMmmに修正)」を訂正後の値へ畳む、
(b) 単独で残った訂正注釈の括弧を落とす、(c) 許可外の焦点距離を最も近い許可値へ丸める
(24mm → 35mm 等)。発火したら logger.info を出す(黙って書き換えない)。ユニット検証(全ケース期待どおり): 85mm(100mmに修正) → 100mm(全角・半角括弧とも)、
24mm → 35mm、50mm/35mm/100mm は不変。実機でも同じ入力を3回投げ、焦点距離は
全て許可値・メタ記述の混入ゼロを確認。
「結果表示のピーク 49.47GB と、生成ボタン下の GPU alloc 74.21GB がなぜ違うのか」という 疑問から、そもそもその復元は必要なのかを検討した。
peak_vram_gb(結果表示)はリクエスト先頭の reset_peak_memory_stats() からデコード
直後までの高水位で、ステータス行はリクエスト完了後の現在値。ref2va は入口で t2va 用
transformer を解放し、終了時に積み直していたため、両者は別の瞬間を見ていた:
| 時刻(実測ログ) | イベント | GPU alloc |
|---|---|---|
| 入口 | transformer freed | 3.11GB(投影TEのみ) |
transformer_ref loaded in 34.8s | 37.2GB | |
| デノイズ+デコード ← ここが結果表示の 49.47GB | ||
| 終了直前 | transformer loaded in 36.9s(復元) | 74.29GB ← ステータス行はこれ |
復元はそのリクエストの所要時間に含まれるので、ユーザーは自分の動画と無関係な ロードを待たされる。しかも次も ref2va なら入口でまた解放するので完全な無駄で、復元した 瞬間に両 transformer が載って高水位が跳ね上がる。元のコード上の根拠は 32B TE(21GB)前提の収支で、投影TE(3.11GB)を使う現在の既定には当てはまらない。
そこで H3_EAGER_VARIANT_RESTORE(既定 0 = 復元しない) を追加。壊れない理由は、
t2va リクエストが入口で _switch_to_variant("t2va") → _ensure_transformer() を必ず通り、
未ロードならそこでロードするため — コストは消えるのではなく、それを必要とする
リクエスト側へ移る。旧挙動は H3_EAGER_VARIANT_RESTORE=1 で戻せる。
| eager(旧既定) | lazy(新既定) | 短縮 | |
|---|---|---|---|
| 初回(transformer_ref コールドロード込み) | 185.92s | 131.13s | -54.8s |
| 定常(2〜3本目の平均) | 108.4s | 90.8s | -17.6s |
| 生成中ピーク | 45.67〜45.92GB | 45.67〜45.92GB | 同じ |
| t2va transformer のロード回数 / ref2va 3本 | 4回 | 0回(t2i を投げた時に1回) | |
| リクエスト間の常駐 | 74.28GB | 3.11GB |
連続 ref2va ではリクエスト間の常駐が 74.28GB → 3.11GB になり、48GB 級での運用に 効く。コストが移るだけの ref2va→t2va 切替では、切替後の最初の t2i が 57.17s (2本目以降は 16.18s)と、移った分を素直に払う。
注意: 上の「生成中ピーク」は両者同じで、混在運用の高水位(74.3GB)が消えるわけでは ない。t2va と参照系を同じプロセスで混ぜれば、遅延復元でも t2va 側で両 transformer が 載る。48GB 1枚ならプロセスをモード別に分けるという結論は変わらない。
H3_VRAM_LIMIT_GB — 大きいカードでも使う量に上限を切る「96GB でも GPU を半分だけ使いたいケースはある」という要望への対応。同じカードで ComfyUI や diffusers-server を並走させるとき、H3 がカードを食い尽くさないよう プロセスの確保上限を切れるようにした(既定は空 = 無制限で挙動不変)。
H3_VRAM_LIMIT_GB=48 ... # このプロセスは 48GB までしか確保しない
実装は torch.cuda.set_per_process_memory_fraction()(GB 指定をカード総容量で割って
渡す)。予約ではなく上限なので、使わない限りメモリは消費せず、上限を超える確保は
PyTorch のキャッシングアロケータが OOM として弾く = 同居相手のメモリを奪わない。
指定がカード総容量以上なら警告だけ出して素通しする。
| 検証 | 結果 |
|---|---|
| 上限8GBで 6GB 確保 | OK |
| 同・20GB 確保 | OOM で正しく拒否 |
H3_VRAM_LIMIT_GB=48 + 通常構成(int8単騎・turbo・投影TE)で t2i | 7.8s / ピーク 45.03GB |
| 同・t2va 5秒 | 28.82s / ピーク 45.64GB(nvidia-smi 40.4GB) |
| ステータス表示 | vram_limit_gb: 48.0 |
今日確立した最速構成(ピーク 45.6GB)はそのまま 48GB 上限に収まるので、96GB カードの 半分だけを H3 に割り当てて残りを別ワークロードに回せる。
48GB 上限で落ちる既知のケース(同日、実運用テストで発生): ref2va の既定解像度 1344×768・5秒はピーク 49.73GB で、48GB 上限だと数百 MB 超過して OOM になる (
Tried to allocate 432.00 MiB ... 44.70 GiB allowed)。上限 48GB で参照系を使うならheight=768 width=768を明示する(768² ならピーク ~45.9GB で収まる)。 なお検証で付けたH3_VRAM_LIMIT_GBを外し忘れて運用を続けないこと — この事故の 直接原因は、上限付きで起動した検証サーバをそのまま使い続けたことだった。
scripts/vram_ballast.py はダミーテンソルを実際に確保して空き容量そのものを減らす。
一方 H3_VRAM_LIMIT_GB は確保上限を切るだけで、torch.cuda.mem_get_info() が返す空きや
カード総容量の見え方は変わらない。したがって:
H3_VRAM_LIMIT_GB(軽い、メモリを無駄にしない)_te_external_usable_for() の 24GB 判定など)
→ バラスト(見え方まで変わる必要があるため)注記: import 時の
logger.infoは uvicorn がロギングを設定する前に走るため、 起動ログには出ない。効いているかは/api/statusのrunner.vram_limit_gbで確認する。
前日の最終テストで踏んだ「ref2va 1344×768・5秒が 48GB 上限で OOM」(ピーク 49.73GB に対し 実効予算 ~49.8GB = 余裕 0.07GB)への答え。候補は「TE を2枚目GPUへ逃がす」か 「group offload」だったが、前者が正解と実測で確定した。
| 構成 | GPU0 ピーク | 48GB 級予算 ~49.8GB に対して |
|---|---|---|
| 投影TE を GPU0 に同居(前日の構成) | 49.73GB | 余裕 0.07GB = 実質入らない |
投影TE を GPU1 へ(H3_TE_DEVICE=cuda:1) | 46.36GB | 余裕 約3.4GB = 入る |
TE 側の実測は 3217MiB(約3.4GB)。2枚目は小さくてよい(16GB や 20GB で十分)。
group offload は turbo と併用不可(cpu_param_dict の制約)で 30 ステップに戻るため、
速度を捨てる分だけ不利 — 2枚目GPUがあるなら TE を逃がすほうが良い。
_te_external_usable_for() の 24GB 判定を TE 種別で分離。24GB は
32B TE の実測(TE-nf4 17.45GB + 参照エンコード 3.22GB 以上)から来た値で、
3.11GB の投影TE には過大だった。投影TE は 8GB を閾値に(2026-08-11 の 8GiB×2 検証で
TE 側ピーク約 5.7GB で ref2i/ref2va が完走している実測に基づく)。これで 16GB/20GB の
2枚目GPUが不当に弾かれなくなった。generate_ref2va() の bnb-4bit 分岐に _pin_execution_device_to_compute() を適用。
TE を切り離すと _execution_device が CPU 上の audio_vae に落ち、layout が CPU に
テンソルを作ってデノイズで cuda:0 and cpu の不一致になる(2026-08-12 に generate() の
非 lowvram 分岐で踏んだのと同型)。1 のガードがこの組合せを塞いでいたため、
これまで一度も走っていなかった — 緩和した瞬間に発火した。教訓: 「ガードで塞がれている経路」はテストされていない経路でもある。ガードを 緩めるときは、その先に未走行のコードがあると想定して検証すること(今回は緩和 → 即 device 不一致 → 修正、という順で顕在化した)。
upscale=1(2段アップスケール)が壊れていた — PR #14355 移行の取り残しを修正「小さく生成してアップスケール」で 48GB に収める案を検証しようとして発覚。現在の
コードでは upscale=1 が必ず失敗する(今日の変更とは無関係の既存不具合)。
`token_tags` and `timestep_indices` must both be `(seq_len,)` tensors matching
`position_ids`, got [21760] and [85696] for seq_len=21760
turbo 4steps でも 8steps でも同じで、ステップ数には依存しない。
hires-fix のパス2は、解像度が変わるのでレイアウト(position_ids/token_tags/各 indices)を
build_packed_sequence() で作り直す。この再構築は 2edc525(PR #14355 以前)に書かれ、
block_state.token_tags = ... のように block_state の属性へ直接代入している。
ところがマージ版(f37ab93)の MiniMaxH3DenoiseStep は、レイアウト値を
block_state.denoiser_input_fields(新設の kwargs_type グループ)経由で読むように
変わった。この dict は state 側の出力から組まれるため、属性代入だけでは反映されない。
結果、row_timestep_plan(こちらは state.set() で書いている)だけがパス2の長さ(85696)に
なり、token_tags/position_ids はパス1のまま(21760)で不整合になる。
移行時の回帰セットに upscale が入っていなかったのが根本原因(第1段は t2i/t2va/バッチ、
第2段は ref2va 系で、upscale=1 は対象外だった)。
再構築した5つのテンソル(position_ids/token_tags/video_indices/audio_indices/
text_indices)を block_state.denoiser_input_fields にも書き戻す。state.set() では
なくこの dict なのは、ループデノイザがここからしか読まないため(既にあるキーだけ差し替え、
新しいフィールドは生やさない)。
修正後の実測(96GB機・int8 + 投影TE(GPU1)+ turbo 4steps・5秒):
| 生成 → 仕上げ | 合計 | パス1 / パス2 | ピーク |
|---|---|---|---|
| 768² → 1536² | 86.0s | 11.1s / 28.3s | 55.83GB |
| 672×384 → 1344×768 | 34.0s | 4.5s / 8.6s | 44.67GB |
旧実測(96GB機・bf16 + 32B TE・30steps)の 645s / 88.0GB から、時間で7.5倍・VRAM で 32GB 以上の改善。
ユーザー指摘「動く腕に二重の線」を受けて調査。当初「ステップを増やしても消えない」と 判断したがこれは誤りだった(30steps の対照は turbo なしで、別種の破綻を見ていた)。 同条件でパス2のステップ数だけを振ると:
| turbo・総ステップ | パス2 | 品質(目視・動く部位) | 時間 |
|---|---|---|---|
| 4steps(既定) | 1 | 動く輪郭が激しく二重化・手が崩れる | 86s |
| 8steps | 3 | 半透明のゴーストが残る(体越しに背景が透ける) | 135s |
| 12steps | 4 | クリーン(モザイクも二重輪郭もなし) | 219s |
patchify⇄_unpatchify)は純テンソルプローブでビット一致、
x0 復元・再ノイズ式もマージ版スケジューラの実装と一致 — 移行漏れではなく、
純粋にパス2のステップ不足が原因だった。推奨レシピ: turbo=1 + num_inference_steps=12 + upscale=1。48GB 級で使える
ワイド版も同条件で確認済み:
| 生成 → 仕上げ | steps | パス2 | 合計 | ピーク | 品質 |
|---|---|---|---|---|---|
| 768² → 1536² | 12 | 4 | 219s | 55.8GB | クリーン |
| 672×384 → 1344×768 | 12 | 4 | 96s | 44.43GB | クリーン(4steps 版で出ていた輪郭の甘さも解消) |
ただし写実系では upscale は使わないほうがよい(2026-08-13、ユーザー指摘を受けて実測):
| 1344×768・同一プロンプト(写実)・同一seed | 時間 | ピーク | 髪・建物の高周波成分 |
|---|---|---|---|
| 直接生成(turbo 4steps) | 52.4s | 47.79GB | 0.2675(基準) |
| 672×384 → upscale(turbo 12steps) | 89.1s | 44.69GB | 0.1997(-25%) |
等倍で並べると、直接生成は毛束が1本ずつ分離し鞄の鎖まで解像するのに対し、upscale 側は 全体が溶ける。パス1を 672×384 で回す以上、そこに存在しない細部は 2x 補間でも パス2の4ステップでも作られない(既定 LoRA が 1344×768 学習の 768p 版であることも、 半分の解像度で回すパス1には不利に働く)。
upscale=1 は基本的に使わなくてよい(2026-08-13 の結論)「1536² は直接生成できないから upscale の出番がある」という想定も実測で否定された。
canvas_native_max は 1344 だが、直接 1536² を生成しても破綻しない:
| 1536²・同一プロンプト/seed(アニメ調) | 時間 | ピーク | 高周波成分 |
|---|---|---|---|
| 直接生成(turbo 4steps) | 144.1s | 59.21GB | 0.2596 |
| upscale(turbo 12steps) | 218.9s | 55.83GB | 0.2750 |
目視でもどちらも良好で、直接生成のほうがむしろ構図が素直。つまり upscale の利点は 「ピークが 3〜3.4GB 低い」ことだけで、代わりに1.5〜1.7倍遅く、写実系では細部が 25% 落ちる。
使いどころは「直接生成では VRAM が足りず、upscale なら収まる」という幅 3GB の隙間だけ (例: 1536² を 56〜58GB 級の予算で出したい)。それ以外は直接生成が上位互換。 UI に出しておく価値はあるが、既定で勧める機能ではない。
turbo=0 num_inference_steps=30 upscale=1。切り分けるなら「turbo なし・30steps・bf16 transformer」を1本(77GB級が要る)。H3_HIRES_DENOISE=0.35)では turbo 既定4ステップで
パス2が1ステップしか取れない。12ステップまで上げれば直るが、そのぶん turbo の速度利点が
消える。配分そのものを見直す(パス2に最低ステップ数を保証する)余地はある。upscale=1 を回帰対象に含めること(docs/internal/regression_baselines.json は
upscale ケースを持っていない)。upscale=1 は t2va 専用(fl2va は
明示拒否・ref2va は経路なし)で、低VRAMモード(H3_LOWVRAM=1/group)では 400 拒否。
つまり「VRAM を節約したい」動機が最も強い場面では、そもそも選べない。上記のとおり直接生成の下位互換なので、チェックボックスを UI から隠した。機能自体は
残してある — upscale=1 を API に直接渡せば従来どおり動くので、「直接生成では VRAM が
足りず upscale なら収まる」幅3GBの隙間には引き続き使える。
表示の可否は static/index.html の UPSCALE_UI_VISIBLE(既定 false)一箇所で決まる。
markup 側の style="display:none;" だけ外しても updateStillControls() がモード切替のたびに
display を上書きするので戻らない — 戻すときはこのフラグを true にすること。
警告は実測ベースの条件付きに変更: パス2 が 4 未満になるステップ数なら具体的な推奨値
(12steps)を示し、turbo なしならモザイクの既知問題を知らせる。
記録(検証の教訓2つ): (1) 修正直後に 1536² の静止した中央フレームだけを見て 「破綻なし」と誤報した — 動画の破綻は動いている部位に出るので、連続フレームを 並べて動く部位を見ること。(2) 「ステップを増やしても消えない」も誤りだった — 30steps の対照は turbo の有無という別の変数も同時に変えていたため、別種の破綻を 同一視した。1変数ずつ振ること(B11 の解像度と同型の教訓)。
| 1344×768 を得る方法 | ピーク | 48GB 級予算 ~49.8GB | 写実系の細部 |
|---|---|---|---|
| t2va を直接 1344×768 で生成 | 47.79GB | 入る(余裕 ~2GB) | 良い(基準) |
| 672×384 → upscale で 1344×768 | 44.67GB | 入る(余裕 ~5GB) | -25%(ぼやける) |
| 1536² を狙う場合 | 55.83GB | 入らない | — |
訂正(2026-08-13): この表は当初「t2va 直接 = 49.7GB 級・ほぼ余裕なし」と書いていたが 誤りだった。49.73GB は ref2va(参照トークンぶん重い)の実測値で、t2va 直接は 47.79GB で 48GB 級に収まる。参照系と非参照系の数値を取り違えていた。
ただし upscale=1 は t2va 専用(fl2va は明示拒否、ref2va は経路なし)で、低VRAM
モード(H3_LOWVRAM=1/group)では 400 拒否のまま。よって ref2va で 1344×768 を
出したいなら、引き続き TE を2枚目GPUへ(46.36GB)が答え。
upscale=1 は t2va 専用で、fl2va は明示的に ValueError、ref2va にはそもそも
経路が無い(still=1 との併用も拒否)。さらに 低VRAMモード(H3_LOWVRAM=1/group)では
400 で拒否される(パス2の系列長が約4倍になるため未検証扱い)。したがって
「小さく生成して upscale」は参照系の VRAM 対策にはならず、TE を2枚目GPUへ逃がすのが
引き続き正解(前節の実測: 49.73GB → 46.36GB)。
upscale が「回帰セット外だったため4日間壊れたまま」だった事故を受けて、全エンドポイント・ 全経路の監査を実施した(等価性アンカー2件・実行テスト16件・静的精査3件)。
バグ1: /api/settings/apply が turbo 既定 ON のプロセスで一律 400(core/settings.py)。
comfy 形式 LoRA の非互換ガード(2026-08-06)を、diffusers ネイティブ形式の解禁(08-08)の際に
validate_instant_settings と UI 用 constraints では形式条件付きに緩和したのに、
apply 側の1箇所だけ条件を付け忘れていた。int8 では transformer_both_resident が常に
真になるため、あらゆるフィールドの適用が拒否され、リロード設定 UI が実質使用不能だった
(<d> のために案内している「投影TEをOFF」もこの経路 — 案内が機能していなかった)。
他2箇所と同じ turbo_lora_expected_format() == "comfy" 条件を追加して修正。
バグ2: リロードパネルの bool 項目が UI から OFF にできない(static/index.html)。
バグ1の修正を検証中に発覚。UI は OFF を空文字で送るが、FastAPI の Optional[bool] は
空文字を None=「変更なし」に解釈するため、te_proj/te_prune/video_vae_fp16 の OFF は
黙って無視されていた。'0' を送るよう修正。
修正後の検証(往復 E2E): te_proj OFF 適用 → 32B TE で <d>[Japanese] こんにちは</d>
入り t2va が受理されて完走 → te_proj ON へ復帰 → 通常生成 8.03s。案内していた
<d> の運用がこれで実際に機能する。
ref2va_batch + TE@GPU1 が OOM — ただし GPU1 には別サービス(ace-server、約10GB)が常駐して
おり、純粋な容量不足。_te_external_usable_for() の閾値判定は「TE用GPUの総容量」だけを
見ており、他プロセスの使用量を見ないため、同居環境では判定を通過してから実行時に OOM する。
実測空きを見る判定への変更は設計判断が要るため未実施(参照枚数で必要量が変わり、固定の
床値では1枚参照の成功ケースを誤って弾く)。
f3622b6d)・旧 v0.1 LoRA 後方互換(596a718e)現行9ケースは 48GB機 + 32B TE(cuda:1)+ v0.1 LoRA 前提で、96GB機では 32B TE が 16GB の cuda:1 に載らず全件再現不能。さらに監査で判明した空白: fl2va・複数参照・ 音声参照・mute・upscale・投影TE経路(現在の実用構成なのに1件も無い)・both_resident 構成・ settings/apply 往復・旧LoRA後方互換・バッチ×turbo。基準セットの作り直しは別途(方針判断待ち)。
前日の監査で「既存の回帰基準 9ケースは 48GB機+32B TE+旧LoRA 前提でこの箱では全件再現 不能、しかも fl2va/投影TE/mute/upscale 等が空白」と判明したのを受けて、96GB機向けの 新基準セットを作成した(既存ファイルは 48GB機用としてそのまま温存)。
docs/internal/regression_baselines_dev96.json — 3グループ16ケース。
A: 実用最速構成(int8 単騎+投影TE+turbo v1.0 768p)で t2i/t2va/fl2va(片端・両端)/
ref2i/i2va/複数参照/音声参照(尺自動導出 175フレームのチェック付き)/mute(音声
ストリーム0本 + mute有無で PNG 一致)/upscale 12steps/<d> 拒否メッセージ。
B: 旧 v0.1 LoRA 後方互換。C: H3_LOWVRAM=1 のバッチ2種(場面ごと MD5)。scripts/run_regression.py — サーバのライフサイクルごと管理する汎用ランナー
(グループごとに pkill → 当該 env で起動 → HTTP 実行 → MD5 比較、--record で採取)。
ケース追加は JSON 編集だけで済む。全 PASS で exit 0。f3622b6d 基底経路 / 596a718e 旧LoRA互換)とも一致し、新旧基準の
連続性を確認。使い方: venv/bin/python scripts/run_regression.py(全グループ実行)。
この節は追従前に書いた影響調査の記録。実際の追従結果は上の「マージ版 (f37ab93) への追従 第1段/第2段」を参照(全経路が同一seed MD5 で等価、main へマージ済み)。 以後 diffusers を上げるときの手順の雛形として残してある。
PR #14355 は 2026-08-05 17:00Z にマージされた(merge commit f53d552、PR最終
head は f37ab93)。venv は f37ab93 に更新済み。H3 を含む安定版リリースは
まだ無い(最新 v0.39.0 は 7/3、H3 は次の v0.40.0 から)ので、引き続き SHA 直指定で
ピン留めする。
abc5e9b → f37ab93 の差分は 27コミット・70ファイルで、懸念どおり
8ab3662(review & refactor、#14371)による大規模リファクタを含む。2026-08-09 に
runner.py の全 diffusers 接点(import 15モジュール・シンボル約40個)をマージ版ソースと
突き合わせた影響調査の結果:
即死する箇所(ImportError、6 import サイト):
packing.py / packing_ref2va.py は削除された。runner が import している
MINIMAX_H3_TEXT_ENCODER_LAYER(→ components.text_encoder_layer プロパティ化、既定50)、
MINIMAX_H3_TEXT_TAG(→ modular_pipeline.py へ移動)、MINIMAX_H3_KEYFRAME_NOISE_AUG /
MINIMAX_H3_MIN_DURATION(→ components.keyframe_noise_aug / .min_duration
プロパティ化 — 「モジュール定数の monkeypatch」方式が通用しなくなる)、
build_packed_sequence / build_row_timesteps / patchify_video_latents
(→ before_denoise.py へ移動)、unpatchify_video_tokens(代替なし —
hires 経路が使うので自前実装の持ち込みが要る)、build_ref2va_presentation /
reference_kind / sample_reference_video_frames(代替なし — references.py の
クラス階層 MiniMaxH3ImageReference/VideoReference/AudioReference に再設計された)MiniMaxH3SetupStep → 消滅(MiniMaxH3ResizeStep に再編)、
MiniMaxH3AutoKeyframeVaeEncoderStep → MiniMaxH3KeyframeVaeEncoderStep /
MiniMaxH3AutoVaeEncoderStep に再編、
MiniMaxH3TextEncoderStep.encode_prompt(staticmethod)→ モジュール関数
get_qwen3vl_prompt_embeds に変更(シグネチャも変更)、
MiniMaxH3Ref2VABlocks → 公開APIから消滅(単一の MiniMaxH3Blocks +
MiniMaxH3AutoDenoiseStep の条件分岐に統合 — pipe/pipe_ref の2シェル設計に影響)生き残った箇所: 主要ステップ類(MiniMaxH3SetTimestepsStep / MiniMaxH3LoopDenoiser /
MiniMaxH3DenoiseStep / decode 2種 / Ref2VA 系 denoise・encoder)は同名で存続。
row_timestep_plan の state キーも存続。decoders.py の latents 正規化
(latents * latents_std + latents_mean、H3_TE_DEVICE 実装が依存)も同形で存続。
diff レベル監査での追加判明事項(2026-08-09、Sonnet エージェントによる全行監査):
_decode は byte-identical。
→ 同一seed MD5 一致の可能性は残っている。回帰はまず MD5、不一致なら PSNR+目視MiniMaxH3VideoDecodeStep は packed
シーケンス行を受けて内部で unpatchify していたが、新版は新設
MiniMaxH3AfterDenoiseStep が unpatchify を担い、decode ステップは unpatchify 済み
5Dテンソルを受ける契約に変更。generate / generate_still_batch / generate_ref2va /
generate_ref_batch 全4関数の decode 直前に AfterDenoiseStep 相当の挿入が必要build_packed_sequence / build_row_timesteps は before_denoise のステップクラスの
@staticmethod へ移動しただけでなくシグネチャも変更(audio_channels /
audio_tag / video_tag が必須引数化)— hires 経路 _upscale_block_state_2x は
全面書き直しModularPipeline.from_pretrained(MODEL_ID, workflow="ref2va") の
workflow 機構に統合(pipe/pipe_ref 2シェル設計の前提が変わる。transformer_ref
というコンポーネント名の存続も要確認)。_execution_device の解決アルゴリズム
(components 挿入順の最初の nn.Module)自体は維持encode_prompt は t2va 側・Ref2VA 側とも staticmethod ごと消滅(fl2va は
MiniMaxH3FL2VATextEncoderStep に分離)。runner が @torch.no_grad() を自前で
制御するために直接呼んでいた経路なので、新モジュール関数
get_qwen3vl_prompt_embeds ベースで同じ最適化を組み直す追従の作業量見積もり: 大(当初「中〜大」から上方修正)。改名追従だけでは済まず、 (a) import 参照先の総付け替え、(b) decode 直前への AfterDenoiseStep 挿入×4関数、 (c) encode_prompt 直接呼び出しの作り直し×2、(d) 定数 monkeypatch → プロパティ override 化、(e) hires 経路の全面書き直し、(f) ref2va 2シェル設計の workflow 機構への 再設計、(g) KVプレフィックスキャッシュの references.py ベース再実装、が必要。 土台の数値経路が無変更なので、移行後の per-機能 MD5/PSNR 回帰で等価性を証明できる 見込みは高いのが救い。
当面の方針: 現構成(abc5e9b、全機能A/B実測済み)を据え置き。追従するなら 最小差分の f37ab93 を対象に、(1) t2i/t2va 経路 → (2) ref2va 経路の2段階で。
なお int8 レシピ(TorchAoConfig + Int8WeightOnlyConfig)は abc5e9b に既に含まれて
おり、マージを待たずに使えている(H3_TRANSFORMER_QUANT=int8 として実装済み)。
H3_TURBO_LORA=1 の配線は完成済み(上記セクション参照)。現行LoRA(Ostris氏)は
「デモ/プレビュー、学習途上」と明記されているため既定OFF、かつint8/低VRAMでは
使用不可(融合QKV前提 → Int8Tensor に aten.cat が無い)だった。
2026-08-08、代替候補 lightx2v/Minimax-h3-Turbo
(minimax_h3_fl2v_turbo_4step_v0.1.safetensors、1.38GB、Apache 2.0、DMD蒸留。
Kijai/MiniMax-H3_comfy はこれのComfyUIリパック)をスパイクし、
48GB(int8)でも動くこと・4stepsで実用品質になることを実測で確認した
(scripts/probe_lightx2v_turbo.py、本体コードは無変更)。
決定的な違い: キーが diffusers ネイティブ。safetensors ヘッダを直接読んで確認した
ところ、キーは transformer_blocks.N.attn.to_q.lora_A.default.weight のように
to_q/to_k/to_v が分離しており(rank 128、312モジュール = 50ブロック×6 +
token_refiner 2ブロック×6、attn と ff のみで adaln/final_layer は含まない)、
fuse_projections() が不要。torch.cat を一切呼ばないので、Ostris版を阻んでいた
int8 非互換の主因を踏まない。実際 int8 の transformer に312モジュール全てを0.6秒で
適用でき、例外は出なかった。ff.net.0.proj の lora_B は (28672, 128) で
diffusers の SwiGLU(ゲート込み dim_out*2)と完全一致し、このLoRAが diffusers の
モジュール構成そのものに対して学習されたことを裏付ける。
罠(最重要): strength は Kijai 記載の 0.75 ではなく ~0.094。生の B·A に直接
掛ける本実装の経路では、0.75 は強すぎて 30 steps でも出力が完全にノイズ化する
(ステップ数の問題ではないことを 30steps で実証)。ComfyUI 側は alpha を折り込んで
適用するため、0.75 × (alpha/rank) = 0.75 × 16/128 ≈ 0.094 が対応値、という仮説が
実測と一致した。
| strength | 4steps の結果 | 音声 rms / peak |
|---|---|---|
| 0.75(Kijaiの記載値をそのまま) | 完全にノイズ(30stepsでも同じ) | 0.083 / 0.43 |
| 0.15 | 良好(背景がやや軟らかい) | 0.069 / 0.79 |
| 0.10 | 良好 | 0.065 / 1.05(クリップ) |
| 0.094(= 0.75 × 16/128) | 最良(毛並み・松葉までシャープ) | 0.039 / 0.70 |
速度(RTX PRO 5000 48GB + H3_LOWVRAM=1、768²・5秒・seed 12345・同一プロンプト):
デノイズ 197.7s → 26.1s(7.6倍)、総所要 351.4s → 135.2s(2.6倍)。総所要の
短縮率が小さいのは低VRAMモードのロード固定費(~110s)が残るため — バッチ経路と
組み合わせればここも償却できる。時間方向の整合性も4フレーム抜き出しで確認済み(破綻なし)。
残る懸念: 音声が基準比で約5倍大きい(基準 rms 0.0073 → 0.039)。スペクトル平坦度は
0.24 と基準(0.34)より低く白色雑音ではない(構造がある)が、強度によっては peak が
1.0 を超えてクリップする。実装するならレベル面の確認が要る。また token_refiner は
int8 の除外リストに入っているため、transformer_blocks は int8 ベース + bf16 デルタ、
token_refiner は bf16 ベース + bf16 デルタという混在状態になる(適用・生成とも成功
しているので実害は観測されていない)。
2026-08-08 本実装済み(「Turbo LoRA」節の更新参照): (1) diffusers ネイティブ用の
適用関数 + キー形式自動判別、(2) _TurboLoRALinear の scale 係数(形式別既定
1.0/0.094)、(3) 併用ガードの形式ベース化(comfy×int8 は拒否のまま、group は形式を
問わず拒否)。(4) ref2va への適用可否のみ未検証のまま。
H3_TE_PREQUANT、
2026-08-08。TEロード 53.0s→29.5s、リクエスト合計 -35%)。残るのは transformer int8
で、保存に約34GB を要しこの箱のディスク空き(43GB)では非現実的。ディスクに余裕の
ある機なら、同じ手法で transformer のロード(約32.5s)も削れる見込みH3_TE_DEVICE、
定常 78.4s→約35s = -55%。上記「text_encoder を別GPUへ常駐」節を参照)。以下は
スパイク時の記録: TE を別カードへ
逃がせば GPU0 に transformer を常駐させたままにでき、固定費が丸ごと消える。
実測では現行の RTX 4000 SFF Ada 20GB で t2va は成立(peak 17.76GB/余裕 3.23GB)
だが ref2va は OOM(2048px の参照を vision tower に通すため 1〜2GB 不足)。
ref2va まで含めるなら 24GB 級が要る。PCIe が Gen4 x4 でも問題にならない
(TE は起動時に一度載せるだけ、毎リクエストの転送は prompt_embeds の 42MB のみ)。
実装には _execution_device 周り(本プロジェクト最大の罠所)の慎重な改修が必要
(scripts/probe_te_on_second_gpu.py)ref2va × turbo は未検証: turbo LoRA は transformer(t2va系)にのみ適用を実測して
あり、transformer_ref への適用は一度も試していない。配線上は同じ経路を通るはずだが、
蒸留LoRAが参照条件付きの軌道でも成立するかは別問題(strength 0.094 の妥当性も t2va での
実測値)。参照付き生成を高速化したくなったら、まずここをスパイクすることH3_TE_PROJ) の4B専用量子化オプション → 実装済み(2026-08-10 同日):
H3_TE_PROJ_QUANT として追加し、実測(NF4 3.11GB・品質同水準)を経て既定を
bnb-4bit 化済み(日付節 2026-08-10 の「追記(同日その2)」参照)。以下は実装前の
検討記録: 当初の投影TEは Qwen3-VL-4B を
bf16 でロードしており、GPU実占有は 8.88GB(2026-08-10 実測。チェックポイント
8.88GB と一致)。投影行列の配布元は「15.7GB → 5.2GB」と書いているが、5.2GB は
bf16 では成立しない(量子化版を指していると思われる)。4B を int8/nf4 で量子化
できれば 4〜5GB 級になり、2枚目GPUの要件がさらに下がる。
注意: 現在の実装は H3_TE_PROJ と H3_TE_QUANT/H3_TE_PRUNE/H3_TE_PREQUANT を
import 時ガードで排他にしている。これは「32B TE 向けの設定を4Bへ流用させない」
ための措置であって、4B を量子化してはいけないという意味ではない。実装するなら
4B専用の別フラグ(例 H3_TE_PROJ_QUANT)として足し、既存ガードの意図を壊さないこと。
投影行列は fp32 のまま適用する必要がある点にも注意(W は 2560×5120 の fp32)H3_LOWVRAM=group の
同居で実 RTX 4060 Ti 16GB 単体での全機能動作を実測済み(TEのストリーミング実行は
不要だった)。8GiB×2 も t2va まで成立。日付節 2026-08-11 参照。以下は達成前の検討記録:
TEのストリーミング実行(ブロック単位でGPUへ流す)が必要。現状の床は
TE-nf4削除版の常駐17.45GB(上記セクション参照)。投影TEを使う場合はこの床が
8.88GB に下がるため、別ルートで 16GB 級に到達できる可能性がある(下記の2枚目GPU
要件の再計算を参照)このリポジトリのコードは Apache License 2.0(LICENSE)。 モデル重みは含まれない。
使用するモデル/重みのライセンスは別途それぞれに従うこと:
| 対象 | ライセンス |
|---|---|
| MiniMax-H3 本体の重み | MiniMax Community License(非商用無料、商用は年商$20M未満まで、要クレジット) |
Turbo LoRA(larryvrh/MiniMax-H3-Turbo-Lora) | Apache-2.0 |
| Qwen3-VL-32B(text_encoder) | Qwen 公式のライセンスに従う |
| diffusers / transformers / torchao / SageAttention 等 | 各パッケージのライセンス |
ローカルLLM(gemma4-31B、OpenAI互換 /v1/chat/completions)で、入力プロンプトを
H3公式ガイドの形式へ整形する。クラウド版Hailuo AIの内部プロンプト整形層のローカル再現。
H3_LLM_URL(既定 http://127.0.0.1:64650)。接続不可時は502
(生成機能には影響しない)POST /api/prompt/enhance {text, mode, seconds, task, lang}storyboard(既定): マルチショットのCUTタイムコード形式へ展開(総尺=seconds、2〜3カット、
ハードカット・被写体同一性維持・カット毎の音指示。焦点距離は35/50/65/100mmに制限)。
これはH3公式ドキュメントの記法ではなく本アプリの独自案(下記 h3-official 参照)brief: 公式ブリーフ形式(シーン→被写体→アクション→カメラ→音→終わり方)の単一ショット詳細化h3-official(2026-08-07追加): MiniMax公式スキル h3-prompt-writing
(MiniMax-AI/MiniMax-H3 リポジトリの skills/h3-prompt-writing/)のフィールド構造・
記法へ厳密準拠。task(UIの現在のタブ = t2va/fl2va/ref2va)で参照ガイドを切替える:
t2va/fl2va は references/base-en.txt の3フィールド形式
(integrated_multimodal_description / overall_soundscape / non_diegetic_music、
[Shot N] At MM:SS.SS のカット記法・<d>[言語] ...</d> の台詞逐語保持)、ref2va は
references/ref-en.txt の6フィールド形式(subject_definitions / summary /
retention_analysis / detailed_description / overall_soundscape /
non_diegetic_music)。出力は公式既定の英語(lang=en)、lang=ja で書き換え本文のみ
日本語化も可能(フィールド名・[Shot n]等のラベル・タイムコード記法・<d>内の台詞は
公式ルールどおり英語/原語のまま)。リファレンス本文は本リポジトリに同梱しない
(ライセンス表記が無いリポジトリのため)。venv/bin/python scripts/fetch_h3_skill.py
で skills_cache/(.gitignore 済み)へ事前取得しておく必要があり、未取得時は
400+取得コマンド案内のエラーになる。システムプロンプトはSKILL.md+参照ガイド全文を
要約せず投入(約18.8KB=t2va/fl2va、約26.6KB=ref2va)。translate: 過剰創作なしの英訳(CUT構造は保持)h3-official は2026-08-07追加時点でLLMサーバ未接続のため生成A/Bのみ実施(下記参照)、
LLM経由での実応答確認は別途必要87 commits
29 commits
Python
84.3%
HTML
15.5%