animede/Diffusers_minimax-h3

MiniMax-H3 (video + synchronized audio, 33B) on diffusers — 9.7s per still, 44.2s per 5s clip on 48GB, with every speedup verified byte-identical

19

stars

116

commits

Python

primary language

Aug 14, 2026

updated

README

minimax-h3

日本語 | 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 本体には一切手を入れていない)。

GUI メイン画面

ブラウザから使う単一ページUI。5つのモードをタブで切替(動画: T2VA=テキスト / FL2VA=フレーム / Ref2VA=参照、静止画: T2I / Ref2I)。下段は生成物ギャラリーで、 チェックした動画を連結して書き出せる。プロンプトはローカルLLMで H3 公式ガイドの形式へ強化でき、 FirstBlockCache・Sage・Turbo・量子化・低VRAMモードなどは再起動なしでこのパネルから切替。 日英切替あり。

VRAM×機能マトリクス(2026-08-11 実測)

○ = 実測で完走 / △ = 導出見込み(未実測)/ × = 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²ref2ii2va(画像参照)音声参照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
  • 12GB 単体は未実測。t2va 実ピーク 11.4GB / i2va 9.41GB(実11.2GB)が 12GiB に収まる計算だが、 音声参照(実14.7GB)・768×1344(実15.2GB)は超える。
  • 16GB / 8GB 構成の鍵は 投影TE(Qwen3-VL-4B + 学習済み線形写像 ClipProj、NF4 で 3.11GB。 32B の text_encoder の代替)+ H3_LOWVRAM=group(transformer int8 をブロック単位で ストリーム、GPU常駐は ~1.4GB)+ video VAE の fp16 デコード。詳細は日付節 2026-08-10〜11。
  • 測定環境は2台のマシンで共有: 16GB / 8GB 列は 96GB機(RTX PRO 6000 + 増設 4060 Ti 16GB)、 48GB+20GB 列は 48GB機(RTX PRO 5000 48GB + RTX 4000 SFF Ada 20GB) での実測。旧版の実測 (96GB単騎時代)は各日付節にそのまま残してある。

VRAM級別クイックスタート

セットアップは「インストール」「モデルの取得」を先に済ませること。 起動後、ブラウザで 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 なし)157s351.4s
+ lightx2v turbo LoRA(4steps)157s143s
+ H3_TE_PREQUANT(量子化TEのディスクキャッシュ)83.2s
+ H3_TE_DEVICE(TE を2枚目GPUへ)約35s60.5s
+ H3_KEEP_TRANSFORMER(transformer 常駐)9.7s44.2s
16倍8.0倍

デノイズ自体を速くする turbo だけでは 2.6倍で頭打ち。残りの3倍強は すべて固定費の撤廃から来ている(turbo なし 30steps のままでも、固定費撤廃だけで t2i は 157s → 51.1s と3倍速い)。

モード別の到達点(全モードを同一構成で実測、2026-08-12)

構成: 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.40s7.9s(0.94倍 = 使う意味なし)2.40s45.0GBVAE の CPU↔GPU 往復 ~3.3s
t2va 5秒 768²28.13s(バッチAPIなし)14.94s45.6GBデコード 7.05s
ref2i 768²(参照付き静止画)79.3s47.0s(1.69倍)7.8s45.4GB参照ビジョンエンコード ~47s
i2va 5秒 768²(画像参照→動画)103.1s75.0s(1.37倍)22.0s45.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。

連番生成(バッチ)について分かったこと

  • t2i のバッチはもう使う意味がない(0.94倍 = わずかに遅い)。バッチは元々 「モデルのロード固定費をバッチ1回に償却する」機能で、常駐化で償却すべき固定費が 消えたため(常駐化前は 157s → 67.5s の 2.3倍だった)。
  • 参照系だけは今も効く。共有されるのがロードではなく参照ビジョンエンコード ~47s だから。バッチのステップ時間は単発と完全一致しており(ref2i 2.598s vs 単発 2.599s、 i2va 7.321s vs 7.323s)、バッチ経路自体のオーバーヘッドはゼロ — 差は純粋に 「47s のエンコードを何回払うか」だけ。
  • 場面数が増えるほど効く: 節約は 47×(場面数-1)/場面数。i2va なら 2場面 1.37倍 → 3場面 1.44倍 → 5場面 1.57倍。同じキャラクターで長い物語を作る用途ほど有利。
  • 「共有すれば縮む」モデルは静止画・動画とも成立(実測削減 vs 予測: ref2i 3場面が 32.3 vs 31.3s/枚、i2va 2場面が 28.1 vs 23.5s/本)。→ 参照エンコードをリクエスト跨ぎで キャッシュすれば、単発の繰り返しでも同じ短縮が得られる見込み(未実装、日付節 2026-08-12)。
  • 制約2つ: バッチ経路は 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 では判定できないので目視で見ること。

最近の更新(2026-08-11〜12)

  • 最終ゴール2つを達成: 実 RTX 4060 Ti 16GB 単体でフル機能(t2va 768² ピーク 11.4GB、 最大は 768×1344 の実 15.2GB)/ 8GB×2 で t2va 5秒 768² がフル解像度のまま完走(ピーク 7.23GB)。
  • デコード末尾の逆正規化を CPU 化(ビット同一、全構成でデコードピーク減)。
  • H3_VIDEO_VAE_FP16 × 参照ありの dtype 即死を修正(エンコード側にも fp16 autocast)。
  • group モードの pinned RAM 残留を解放し、t2va↔ref2va のモード切替が可能に。

現在地と再開の入口(2026-08-12 時点)

作業を再開するとき、まずここを読む。

知りたいこと見る場所
仕様・性能・設計(何をどう実現しているか)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×機能マトリクス」参照
ホストRAM64GB 以上を推奨(H3_LOWVRAM=group は int8 重み ~34GB をRAMに常駐させるため 48GB 以上の空きが必要)
ディスク約 145GB(T2VA/FL2VA のみ)/ 約 207GB(Ref2VA も使う場合)
CUDA12.8 系(torch 2.9.0+cu128 に合わせる)。SageAttention をビルドするなら nvcc も同系統
Python3.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 回帰)を踏むこと。

SageAttention(任意、既定で有効)

既定の 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超で警告)。

VRAM対応表と主な環境変数(早見表)

用途に応じた起動例。上段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=int8peak 59.7GB / 約160秒
48GB級H3_LOWVRAM=1peak 38.9GB / 約215秒
32GB級H3_LOWVRAM=grouppeak 28.7GB / 約280秒
18GB級H3_LOWVRAM=group H3_TE_PRUNE=1peak 17.7GB / 約280〜320秒
16GB 単体(実 4060 Ti)H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1peak 11.4GB / 約25分※
12GB 単体(見込み・未実測)16GB 単体と同じ実ピーク11.4GBが12GiBに入る計算。参照系・768×1344は不可
8GB×2(バラスト実測)16GB 単体の指定 + H3_TE_DEVICE=cuda:1peak 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_QUANTbnb-4bittext_encoder の量子化(none は bf16 で 66.7GB)
H3_TE_PRUNE0TE の未使用上位レイヤー削除(出力は不変、-3.6GB)
H3_TE_PROJ(無効)投影TE: 32B TE を Qwen3-VL-4B+線形写像で代替(repo id か .safetensors パス。品質は近似、日付節 2026-08-10 参照)
H3_TE_PROJ_QUANTbnb-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_QUANTnoneint8 で transformer を 66.3→34GB
H3_LOWVRAM01=48GB級のフェーズ循環 / group=32GB級以下の block offload
H3_KEEP_TRANSFORMER0transformer 常駐で再ロード固定費を撤廃(成立条件は該当節参照)
H3_CACHE / H3_CACHE_THRESHOLDfbc / 0.05FirstBlockCache(デノイズ -25%。int8+SDPA 軌道では不発の実測あり)
H3_ATTN_BACKENDsagesage は sm_120 専用ビルド。それ以外のGPUは default(SDPA)必須
H3_TURBO_LORA04/8ステップ蒸留LoRA(初回に約780MBをDL。group とは併用不可)
H3_VIDEO_VAE_FP160video VAE を fp16 化(デコードのピークを削減。16GB 以下では必須)
H3_LLM_URLhttp://127.0.0.1:64650プロンプト強化に使うローカルLLM(任意機能)

VRAM/RAM 設計 (重要、実測に基づく)

このマシンは 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 入れ替えが消滅する。

  • 定常常駐: transformer + TE-nf4 = ~87.5GB。これに VAE 11GB を足すと~98.5GBで 96GBを超えるため、このモードでは VAE ペアは常駐しない(CPUに置き、キーフレーム エンコード/デコードの当該フェーズだけGPUへ往復する。fp32 11GBのPCIe往復のみで ディスクI/Oなし)。
  • デコード窓(~9s)だけは transformer を解放してから実行し、直後にリロードする (transformer+TE+VAE+デコードバッファは物理的に収まらないため。実測でOOM確認済み)。 毎stepのスワップではなく単発の片道×2なので、diffusers-server CLAUDE.md 33番の 禁止パターンには当たらない。
  • 品質A/B(同一seed 12345): フレーム比較で構図・被写体・シャープさは同等 (条件付け数値の変化による木立の配置等の微差のみ)、音声も rms 0.0080→0.0061 と 同水準で -20dB 型の崩壊なし。劣化なしと判定して既定化した

H3_TE_QUANT=none (bf16 TE、旧方式)

2つの66GBモデルをリクエストごとにGPU上で入れ替える:

  • vae + audio_vae(fp32 計11GB)は常時GPU常駐。
  • エンコード段階: VAE 11GB + text_encoder 66GB
  • デノイズ/デコード段階: VAE 11GB + transformer 66GB
  • 解放は CUDAモデルの参照を直接落とす(.to("cpu") でRAMへ退避しない。66GBの RAM経由はスワップ突入の実測原因になった)。リロードはページキャッシュ/ディスクから 11〜40秒/モデル。リクエスト間の定常状態は transformer+VAE 常駐(77.5GB)。

実測のオーバーヘッド: 1リクエストあたり TEロード ~37s + transformerリロード ~26s。 この入れ替えコストを解消したのが上記の bnb-4bit(既定)で、リクエスト合計は 245s → 185s に短縮された(デノイズ157sは共通のモデル律速で不変)。

2つの実装上の罠(実機で踏んで修正済み):

  1. MiniMaxH3TextEncoderStep.encode_prompt は素の staticmethod で、@torch.no_grad() はブロックの __call__ 側にしか付いていない。直接呼ぶ場合は必ず torch.no_grad() で包むこと。忘れると autograd グラフが TE の重み約50GB分をGPU上にピン留めし、 モデルを解放してもVRAMが返ってこない(diffusers-server CLAUDE.md 39番と同型)。
  2. ブロックの出力(num_frameskeyframes・latent形状等)は PipelineState に入る。 get_block_state() は宣言された入力しかマップしないので、出力は state.get(名前) で読むこと。

video VAE の decode は diffusers 側の MiniMaxH3VideoDecodeStep が内部で torch.autocast(dtype=torch.float16) を使うため、重み自体は fp32 のままでよい。 audio VAE は fp32 のまま一切キャストしないこと(bf16化すると生成音声の音量が 約20dB小さくなる既知の問題があるため、runner.pyvae/audio_vae のロードに 明示的に dtype=torch.float32 を渡している)。

実測値 (RTX PRO 6000 Blackwell 96GB, 768×768, 124フレーム=5.17秒, 30steps)

項目実測
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/リクエスト)に戻せる。

FirstBlockCache によるデノイズ高速化 (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 でほぼ同一(目視でも区別困難)。
  • threshold 0.1 は 1.92x(デノイズ81.5s、14スキップ)だが構図が目視で分かるレベルで ドリフトするため既定にしていない(速度最優先の場合のみ)。
  • H3_CACHE=none でキャッシュ無しの従来挙動に完全に戻る(バイト一致を回帰確認済み)。
  • ピークVRAMは残差キャッシュ分 +0.7GB(91.4→92.1GB)。
  • 実装メモ: MiniMaxH3TransformerBlock はPRブランチの TransformerBlockRegistry に 未登録のため、runner側で TransformerBlockMetadata を登録してから enable_cache() を 呼ぶ(venvのdiffusers本体は無改変)。リクエストごとに _reset_stateful_cache() + cache_context("h3") で包む(リセット漏れは前リクエストの残差による誤スキップを招く)。 同一seed連続2本のmp4バイト完全一致でリセットの正しさを検証済み。

2段生成による2xアップスケール (/api/t2vaupscale=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=0181s125s6.5s92.1GB768²
upscale=1645s533s (パス1 78s + パス2 455s)24.7s88.0GB1536²
  • 構図・被写体は upscale=0 と一致し、毛並み・芝などの実ディテールが乗る。背景の 細部(フェンス等)はパス2の再デノイズで軽微にドリフトする(hires-fixの性質)。
  • 音声: latentテンソル自体はアップスケール処理で無変更だが、映像と音声は1つの パックドシーケンスで自己注意を共有するため、パス2以降の音声出力は upscale=0 と bit一致しない(相関0.89、非無音・品質同等。アーキテクチャ上の制約で仕様)。
  • VRAM: パス2はシーケンス長~4倍のため、upscale=1 のリクエストではエンコード直後に TE-nf4 を解放してからデノイズする(次リクエストのエンコード時に遅延再ロード)。
  • 実装の要点(実機でバグを踏んで確定): 補間対象はノイズ付きlatentではなくx0推定値 であること(ノイズ付きを補間すると市松状ノイズが増幅されて全面ノイズ化する。ComfyUI 参考実装も denoised_output を使っている)。解像度変更時は build_packed_sequence() で position_ids/token_tags/各indicesを再構築し、row_timestep_plan も残ステップ分を 作り直す。ModularPipeline の _execution_device はコンポーネント登録順の先頭モジュール で決まるため、TE解放後は components.transformer.device を明示的に使う (diffusers-server CLAUDE.md 23番・47番と同型の罠)。

Sage Attention (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)。

  • 実測: デノイズ 118s→104s(-12%)。完全決定論(同一seed 2本バイト一致)。品質は 目視同等(PSNR 21dBは int8-QK 近似による軌道ドリフトで劣化ではない)
  • H3_ATTN_BACKEND=default で従来のSDPAに戻る
  • FBCと独立に併用可: sage + H3_CACHE_THRESHOLD=0.1 でデノイズ67s(-43%、リクエスト ~125s。FBC 0.1の構図ドリフト特性は既知どおり)
  • hub系backend(flash_hub/sage_hub)は torch 2.9 向けビルドがHub側に存在せず不成立 (2026-08-05時点。環境の問題ではない)

ステップ数の指針(蒸留モデル、実測 2026-08-05)

num_inference_steps はAPI/UIパラメータ。30(検証既定)に対し 20で-15%、16で-31% の デノイズ短縮。16/20とも単フレーム品質・時間方向の安定性に破綻なし(ただし構図は ステップ数で変わる)。ドラフト用途は16-20、本番は30が目安。ステップを減らすと FBCのスキップ機会も減る(30stepsで7スキップ→16stepsで0)ため、効果は単純比例しない。

transformer int8量子化 (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両常駐
t2va175-185s / peak 92.1GB177-196s / peak 59.7-91.1GB
ref2va(2回目以降)523s / peak 87.6GB463-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 は従来とバイト一致(回帰確認済み)。

48GB級VRAM対応 (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_QUANTbnb-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_devicecpuに解決されてしまいデノイズの最初の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_encoderNone が同期され、 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.68GBTEロード52s + transformerロード36s + デノイズ108s + デコード6.6s
    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)では採用自体が非現実的なため。

任意サイズ・秒数の丸め (2026-08-06)

UIの解像度セレクトに 「任意 (32の倍数へ丸め)」 を追加し、幅/高さを自由に入力できる。 入力値は エラーにせず H3 の規則へ丸める:

  • キャンバス: 32の倍数へ四捨五入し、256〜2048にクランプ(app.pyround_canvas_value)。H3のブロックは32の倍数でないと ValueError を出す仕様 (MINIMAX_H3_CANVAS_MULTIPLE)なので、以前は端数を送ると400になっていた。 ネイティブ範囲(短辺768・最大768×1344)を超える指定はUIが警告を出す(VRAM・品質は未検証)
  • 秒数→フレーム数: 5〜15秒にクランプ後、17n+5 へ切り上げ(align_num_frames)。 例: 6.3秒 → 158フレーム(6.58秒)。UIは送信前に実フレーム数と実尺をプレビューする
  • API: /api/t2va/api/fl2vaheight/width(任意、指定時は resolution プリセットより優先)を追加。/api/ref2va は元から受け付けるが、こちらも丸めるように変更
  • /api/statusconstraints で丸め規則(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は 何も起きない、という分かりにくい症状になる)。丸める前提の欄には制約属性を付けないこと。

Turbo LoRA (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_offloadcpu_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自動無効化。

turbo × 他機能の組み合わせ検証(2026-08-06)

デフォルトのtransformer経路(transformer_quant=none, lowvram=0)以外との組み合わせ は当初「未検証のため予防的に拒否」だったが、実測でA/B検証した。

  • turbo × upscale(2xアップスケール hires-fix): 動作確認済み、解禁。 768²→1536²・seed12345で8/16stepsとも成功。8steps: 総所要210.3s(denoise 82.6s+ decode 24.4s)、pass1=5/pass2=2steps(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呼び出し」は実害なしと確認)。
  • turbo × transformer int8(H3_TRANSFORMER_QUANT=int8transformer_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生成は正常動作を確認)。
  • turbo × lowvram=1: 実測で不可と確定、拒否のまま維持。 lowvram=1transformer_quant=int8 を強制するため、上記と全く同じ Int8Tensor/aten.cat エラーで失敗(同一エラーメッセージを実機確認)。
  • turbo × lowvram=group: 実測で不可と確定、拒否のまま維持。 同じ理由 (lowvram=grouptransformer_quant=int8 前提)で同一エラー。この失敗は group offloadフックの適用順序とは無関係(sibling project の 「LoRAをenable_group_offload()より前に適用する」という順序修正パターンは ここでは効かない——fuse_projections()自体がgroup offloadフックを一切介さず torch.catだけで失敗するため、順序を入れ替えても直らないと判断し深追いしなかった)。
  • ref2va: 本タスクの検証対象外のまま(元々タスクブリーフのスコープ外)。

16GB級の検証結果: 非対応(床は~18GB、2026-08-06確定)

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未満の量子化が必要(未着手の別課題)。

video VAE の fp16 化 (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の既知問題)。

  • 品質: 全124フレームの平均PSNR 39.97dB(min 39.08)で目視区別不能。デコード計算は 元々autocast fp16のため重みfp16化の影響が小さい
  • 実装の罠: AutoencoderKLMiniMaxH3._keep_in_fp32_modules が encoder/decoder等を 強制fp32に戻すため、from_pretrained(dtype=fp16) は効かない(実機確認)。ロード後に .to(torch.float16) を明示的に呼ぶ必要がある
  • 既定OFFでは既存基準とMD5一致(回帰ゼロ)

24〜32GB級VRAM対応 (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 のような毎リクエスト 再ロードは発生しない)。

PR側の「streamed offload時のload-time量子化」調査結果

タスク時点で読んだ 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())。
  • TEは 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にフォールバックする、上記の壊れる組み合わせを 避けるため)を明示指定すればよい。

choreography最終形(フェーズ×常駐物×ピーク)

フェーズ常駐する大きいもの備考
起動時preloadtransformer(int8, CPU常駐+groupoffloadフック)約90秒(CPUロード70s+pin化22s)
t2va encodeTE-nf4(GPU,21GB) + transformer(CPU)
t2va denoiseTE-nf4(GPU,21GB) + transformerの1-2ブロック(GPU,~1.4GB)
t2va decodevaeペア(GPU,~11GB) + transformerの1-2ブロックTEはこの窓だけ強制解放(下記参照)、decode後に再ロード
ref2va参照エンコードvaeペア(GPU,11GB)TEはこの窓だけ強制解放(下記参照)
ref2va denoiseTE-nf4(GPU,21GB) + transformer_refの1-2ブロック
リクエスト間定常transformer(CPU) + TE-nf4(GPU)ref2va後はtransformer_refが未ロードに戻る(t2va↔ref2va切替のたび再ロード)

【実装中に発見・修正した2つ目のバグ】decode窓・参照エンコード窓でのTE強制解放が必要だった

当初「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へ、の順で安全性を確保)。

MD5一致チェックの結果

同一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制限・24GB制限プローブ

32GB制限(ダミーVRAM確保で空きを~30GBへ制限、H3_CACHE=fbc有効のまま):

完走ピークVRAM所要時間
t2va 1本目(768²・5秒・30steps)28.67GBdenoise 220.79s / decode 6.31s / 総計337.19s(TE初回ロード込み)
t2va 2本目(連続)28.23GBdenoise 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へ制限):

  • 768²: denoise中(transformerブロックのonload)でOOM。実消費21.85GB、うち TE-nf4だけで21GB。TE-nf4自体の固定サイズ(21GB)が22GB予算の大半を占め、 transformerブロック1個分(~148MB)の追加onloadすら入らない
  • 544×960(RESOLUTION_PRESETSへ一時追加して検証、検証後に削除済み): 同一箇所・ 同一21.85GBでOOM。解像度を下げても失敗点も消費量も変わらず、VAEタイル 縮小と同じく「解像度に依存しない固定コストが律速」であることを確認 (下記VAEタイル調査参照)。
  • 結論: 現行アーキテクチャ(TEをbnb-4bit・常時ほぼ常駐という設計)では、 TE-nf4単体の21GBが24GB級カードの実効予算(~22GB)の大半を占めてしまい、 どんな解像度でも成立しない。24GB級に対応するには、TEをdenoise中は解放する (H3_LOWVRAM=1的な設計に戻す)か、TE自体をさらに軽量化する(GGUF等、ただし transformers系モデルへのGGUF適用は構造的に困難)必要がある。本タスクの スコープ外(48GB→24-32GB対応が目的で24GBは探索的プローブ)のため、 現時点では未対応と結論する。

VAE tiling調査結果

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級対応において空間タイルサイズの調整は無意味(既定のままでよい)。

FBC/sage共存の確認結果

全ての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本)が完走した ことも確認済み。

Ref2VAのRAM制約(既知の制限、未解決)

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 -gusedベースで見ても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 搭載機での追試は未実施)。

text_encoder の未使用上位レイヤー削除 (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 は完全無変更(この分岐自体が 一切呼ばれない)。

なぜ「50層」ちょうどではなく「51層」なのか(このタスクで発見・検証したtransformers側の罠)

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_pretrainedconfig が既に 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)のどの組み合わせとも合成可能。

削除後TEの実測サイズ

精度削除前削除後(51層)削減
bnb-4bit nf421.02GB17.45GB-3.57GB (-17%)
bf1666.71GB53.06GB-13.65GB (-20%)

nf4は量子化で1層あたりのサイズがbf16の約1/4に圧縮されるため、削減の絶対量もbf16より 小さい(相対削減率はほぼ同じ)。

MD5一致チェックの結果

同一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=1H3_LOWVRAM=groupの各モードでも、削除の有無で出力が完全一致する ことを確認済み(後述)。

24GB級対応: 削除だけでは不十分だった(実機で発見・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.pygroup_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.72GB321.7s(TE初回ロード込み)
24GB(修正後、2本目・連続)18.68GB277.7s(TE常駐のため短縮)
20GB(修正後)17.72GB320.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.3s21.01GB
削除あり35.0s(-17%)17.44GB

出力mp4は削除の有無でバイト完全一致(md5一致)。

回帰確認

H3_TE_PRUNE未指定(既定0)の状態で同一条件のt2vaを実行し、この機能追加前に 取得していた基準mp4とバイト完全一致(md5一致)することを確認した。既定動作は 完全無変更。

Ref2VA (オムニ参照生成、/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/statusactive_variant)。TE-nf4・VAE類・processorは両変種で共有。
  • VRAM対策(実機OOM 3件を踏んで確定): 参照VAEエンコード完了後に transformer_ref を ロード(逆順は98.5GBでOOM)、デノイズ前にTE-nf4を強制解放(参照行でシーケンスが 伸びるため。hires-fixと同じパターン)。共有text_encoderの解放は両パイプライン シェルの参照を両方消すこと(片方だけではrefcountが残りVRAMが返らない)。
  • 実測(768x768指定→1344×768出力・30steps・seed=12345): 画像1枚参照 523s/87.6GB、 画像+音声(尺は音声由来7.3s) 753s/88.1GB、画像2枚 635s/88.1GB。参照人物の同一性・ 複数参照の合成(人物が参照シーンのカフェに座る)を目視確認済み。ref2va⇔t2vaの 往復切替も正常(切替込みt2va 188s)。
  • UIは「Ref2VA (参照→動画)」タブ(複数ファイル選択、選択順=参照順)。

起動

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 用途は未検証・ 非推奨)

回帰確認プローブ (UIより先に動作確認する場合)

venv/bin/python scripts/probe_t2va.py

outputs/probe_t2va.mp4outputs/probe_report.json (ロード時間・生成時間・ ピークVRAM等)を出力する。

API

  • POST /api/t2va (multipart/form-data): prompt, resolution (768x768|768x1344|1344x768), seconds(5〜15), num_inference_steps, seed
  • POST /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/RAM
  • GET /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 にまとめてある (取り込んだもの / 調査の結果取り込まなかったもの / 着想を得て自前実装したもの、 それぞれの出典・実測値・判定・踏んだ罠)。

UIからの設定切替 (2026-08-06)

環境変数でしか変えられなかったオプトイン設定を、性質で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/applycore/settings.pyapply_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と整合)。

生成済み動画ギャラリー (2026-08-06)

結果表示の下に 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 取得時のみ)。

公式スキル(h3-prompt-writing)モードの実機検証 (2026-08-07)

ローカルLLM(gemma4-31B)接続下で h3-official モードを実測した結果。

構造適合: 生成されたプロンプトは公式仕様に完全準拠していた。

  • T2VA: 3フィールド(integrated_multimodal_description / overall_soundscape / non_diegetic_music)、[Shot 1] は時刻なし、[Shot 2] At 00:05.000 の3桁記法、 台詞は <d>[Japanese] おかえり</d> と原語で保持、話者ID (S1) も付与。応答6.2秒
  • Ref2VA: 6フィールドすべて出力、<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.0005.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> タグ内の台詞は英語/原語のまま維持される。

h3-official の品質保証: 検証 + 修復ループ (2026-08-08)

「LLMの性能限界なのか、プロンプトで直るのか」を推測で議論しないために、まず故障率を 実測し(scripts/probe_h3official_compliance.py、5入力×3回)、その結果に基づいて 対策した。LLM は gemma4-31B Q4_K_M / llama.cpp、n_ctx=7680

ベースライン実測: 失敗は1クラスに集中していた

故障クラス発生率
構造・記法(フィールド名・[Shot n]・タイムコード・<d>タグ・話者ID)0/15 (0%)
時間配分6/15 (40%)
文脈溢れ0/15(余裕 2,900+ トークン)

「入力が変われば別の問題が出るのでは」という懸念は良い方に外れた — 違反は散らばらず 時間配分の1クラスに集中していた。しかもその6件は性質が2つに分かれる:

  • 入力が物理的に不可能 (3/6): 台詞の推定発話時間が尺を超えている。実例は38文字の 独白(推定9.5秒)を9秒尺で要求したケースで、LLMは1ショットに9秒全部を割く最善の 配分をしていた。公式仕様が台詞の逐語保持を要求する以上、短縮という逃げ道も無い → どのモデルでも解決不能。ユーザーに返すべき情報
  • LLMの配分ミス (3/6): 尺には収まるのに配置が悪い → 指摘すれば直せる部類

文脈: t2va は system 4,397 tok / 7,680 で余裕あり。ref2va は 6,191 tok (81%) で余裕 約1,100 トークンしかない(実測では6フィールド揃って正常終了したが、入力が長い場合は要注意)。

対策(3層)

  1. バリデータ (core/prompt_check.py): 決定的に判定できる規則を実装 (F1 3フィールド / F2 先頭ショットに時刻なし / F3 カット時刻の厳密増加 / F4 尺内 / F5 ショット尺の下限 / F6 <d>タグと言語タグ / F7 台詞がショットに収まる / F8 話者ID)。F5・F7 は公式仕様に無い本アプリの実用規則 — 公式は「カット時刻が 尺内」としか言わないため、5秒尺で4.5秒にカットして最終ショット0.5秒、という 字義どおりだが使えない出力を許してしまう(実際に発生)。意味的整合性(1ショット内で 画角が混在する等)は近似ルールで警告にとどめる。手書きプロンプトにも効く
  2. システムプロンプト改善: 英語版wrapperにはガイド本文の後ろに何も無かった (日本語版だけ lang=ja 修正時に後方ブロックを持っていた)。既定は lang=en なので、 尺の制約を含む全指示が15.8KBのガイドに埋もれていた。両版の末尾に時間配分6ルール (ショット尺の下限・台詞の発話時間見積り・ショット数の目安・1ショット1構図・ 画面外音声の定型句・話者ID)を追加
  3. 修復ループ (core/llm.pyenhance_prompt_checked): 違反を検出したら その内容を日本語で突きつけて再生成(最大2回、H3_OFFICIAL_MAX_REPAIRS)。 違反が増える修復案は破棄する(修復が別の箇所を壊す事故の防止)。 入力の実現不可能性はLLMに投げる前に判定し InfeasibleInputError → 400 + 助言

改善後の実測(同一条件)

clean入力不可を検出違反が残存
ベースライン9/1506/15
改善後12/153/15(正しい挙動)0/15

未解決の違反がゼロになった。 全ケースが「妥当なプロンプトを返す」か「実行不能な理由を 助言付きで返す」かのいずれかになる。所要時間の中央値は 8.5s → 9.2s(+8%)で、修復ループは 必要なときだけ発動する(発動時は約2倍の21秒)。

API は /api/prompt/enhance の h3-official 応答に violations / warnings / check_report / attempts / repaired を追加。UI は残った指摘をステータス欄に表示する (生成はブロックしない — プロンプトは編集可能なので人間が最終ゲート)。

量子化済み text_encoder のディスクキャッシュ (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 を変えると重みの中身が 別物になるため、同じ場所を使い回すと設定切替時に古い重みを読む事故が起きる
  • 消費ディスク: 17.44GB(H3_TE_PRUNE=1)/ 未削除なら ~21GB
  • H3_TE_PREQUANT=0 で完全に無効化(導入前と同一挙動)。空きディスクが H3_TE_PREQUANT_MIN_FREE_GB(既定 25GB)を下回る場合は保存をスキップして生成は続行する (キャッシュは高速化であって機能ではない)。保存は一時ディレクトリへ書いてから rename するので、途中で落ちても中途半端なキャッシュが「有効」に見えることはない

測定上の注意(この検証で踏んだ罠): 保存直後に測ると 17.44GB がページキャッシュに 載っているため 2.6秒という非現実的な値が出る。実運用ではリクエスト間に transformer (34GB)をディスクから読むため TE のページキャッシュが押し出され、実測は 21〜34秒に 落ち着く(リクエストを重ねるほど遅くなり 34秒前後で頭打ち)。それでも従来経路の 46〜56秒より速いが、プローブの数値をそのまま性能として報告してはいけない。

text_encoder を別GPUへ常駐 (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.3s67.6〜86.2s(平均 78.4s)
TE を cuda:1 へ88.2s(TE初回ロード込み)33.6〜36.7s(約35s、-55%)

TE のロードはプロセス起動後1回だけ。GPU1 に 17GB 常駐したまま、GPU0 はアイドル時 2.3GB まで下がる。

  • ref2va は自動的に拒否される(20GB級の場合): 2048px 短辺の参照を vision tower に 通す活性化が入らず OOM することを実測済み(19.25GB 使用中に 204MB 不足)。 TE用GPUが 24GB 未満なら理由を添えて 400 を返す — 「動くはず」で走らせて OOM させない。 24GB 以上なら自動的に許可される。t2va/fl2va/t2i とその各バッチは併用可能
  • PCIe 幅は問題にならない: GPU1 は Gen4 x4(x16対応カードだが4レーン接続)だが、 TE は起動時に一度載せるだけで、毎リクエストの転送は prompt_embeds の約42MB のみ(約6ms)

出力は従来構成とビット一致しない(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_meanExpected 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 が最初に見つかるようにする)。

静止画モード (T2I、/api/t2i、2026-08-07 本実装)

H3を「超短尺動画→静止画取り出し」で画像生成の代用にするモード。前段の超短尺プローブ (下記)の結論をそのまま本実装した。価値は「H3と画風が完全に一致する静止画」を FL2VA の先頭フレームや Ref2VA の参照画像として作れること(速度は固定費支配のため 専用T2Iモデルには勝てない)。

  • API: POST /api/t2iprompt / frames(22既定 or 5)/ resolution or height+width / num_inference_steps / seed / 即反映パラメータ (cache/cache_threshold/attn/turbo)。レスポンスは超短尺 mp4 と中央フレームの PNG (image_urlt2i_<ts>.png)の両方
  • UI: 静止画段の「T2I」タブ(秒数・upscale欄は非表示、フレーム数選択のみ)。 ギャラリーにも PNG がタイル表示される(削除可・連結不可)
  • フレーム数: 22 (0.917s) が既定。5 (0.208s) は実験的だが下記の VAE 修正で動く
  • 実装の3点セット(core/runner.py):
    1. generate(still=True, still_frames=...) — diffusers 側の最小尺5秒バリデーション (MINIMAX_H3_MIN_DURATION)は MiniMaxH3SetupStep の呼び出し1回分だけ _relaxed_min_duration() で緩和(生成は generation_lock で直列なので漏れない)
    2. VAE 小クリップデコード修正 (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 本体は無変更)
    3. decode 例外時の steady state 復元: プローブで観測した「decode 例外 → transformer drop 済みのまま復元コード未実行 → ~98.5GB 残留で後続リクエストが 連鎖 OOM」への対策として、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.8s35.0GB157s破綻なし・高品質
5 (0.208s)9.1s0.67s35.2GB125s破綻なし(史上初の実デコード成功)

5フレームの音声 (0.208s) も非無音 (rms 0.055)。

連続生成 (物語の場面画像などプロンプト違いの連番) の実測: group モードは静止画に不利

「常駐モード (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=1157s~157sTE+transformerロード ~120s + デノイズ 29s + デコード 2s
H3_LOWVRAM=group310s (TEコールド込み)~240sデノイズ 130s + デコード 2s + デコード後のTE再ロード 75-97s
  • デノイズ 29s → 130s の悪化が本質: group offload は1ステップごとに int8 transformer 全 ~34GB をブロック単位で PCIe 転送する。転送コストはシーケンス長に 無関係な固定費なので、計算が軽い超短尺 (22フレーム) では転送が完全に支配する (124フレームの通常動画では計算時間に隠れて相対的に軽い、という設計前提が 静止画では崩れる)
  • group モードはデコード窓で TE を force-free して後で再ロードする(24-32GB級の ヘッドルーム設計)。この箱では TE NF4 ロードが実測 75-97s かかる(96GB時代の 15-40s より大幅に遅い)ため、この再ロードも定常コストに乗る
  • 品質は lowvram=1 と同様に良好(目視)。ピークVRAMは 25.1GB
  • 結論: この箱での静止画の連続生成は 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.pygenerate_still_batch())。generate() の lowvram choreography を 位相順に並べ替える:

entry   : [nothing big resident]
encode  : [TE-nf4]         全場面の setup/エンコード/layout/latents/timesteps
denoise : [transformer]    全場面を順にデノイズ
decode  : [vae pair]       全場面を順にデコード → PNG/mp4 保存(場面ごとに保存
                           しながら進むので途中失敗でも完了分は残る)
  • API: POST /api/t2i_batch — multipart で prompts を場面数ぶん繰り返し送る (最大24場面)。frames/resolution/num_inference_steps/seed/即反映パラメータは 全場面共通(変えられるのはプロンプトのみ)。UI は T2I タブの 「バッチ連続生成(1行=1場面)」チェックで、プロンプト欄の1行を1場面として送る
  • 場面間の共有状態リセット(実装の要): スケジューラは sigmas/timesteps の値が 全場面同一(同じ幾何・ステップ数)なので、デノイズ直前に _step_index = None に 戻すだけでよい(MiniMaxH3Scheduler.step() は index を timestep 値から再導出する)。 FirstBlockCache は場面ごとに _reset_stateful_cache() + cache_context (generate() の per-request リセットの per-scene 版)
  • 等価性の実証: 同一プロンプト・seed の逐次 /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~471s157s毎回 ロード~120s + デノイズ29s + デコード2s
/api/t2i_batch (3場面)202.6s67.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のみ)。

参照付き静止画 (ref2i、/api/ref2vastill=1/api/ref2i_batch、2026-08-07 本実装)

下記スパイクの成立を受けて本実装した。キャラクター参照から場面ごとの一貫した 静止画を作るモード。作った静止画は FL2VA の先頭フレームや次の Ref2VA の参照に回せる (物語の複数動画生成の素材作り)。

  • 単発: POST /api/ref2vastill=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 一致まで実証済みの同じ手法
  • UI: 静止画段の「Ref2I」タブ(2026-08-09 に Ref2VA タブ内の「静止画」チェックから 独立タブへ移動。下記「タブの2段組み」参照)+「バッチ連続生成(1行=1場面)」チェック

実測 (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場面とも赤マントを保持し 各プロンプトに追従)。

  • 既知の改善余地: 参照ビジョンエンコードが場面ごとに走る下記 「参照プレフィックスの KV キャッシュ共有」で解決済み (2026-08-08)

参照プレフィックスの KV キャッシュ共有 (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 を読んだ上で実機実測):

  • プレフィックス部分の hidden_states[50] はフル計算とビット一致 (torch.equal)
  • プロンプト末尾部分は相対RMS ~1.5% の丸め差が残る。原因はカーネル/GEMM のタイル経路が 系列長で変わること — eager 固定でも同水準 (sdpa 固有ではない)。ロジックバグでない ことはネガティブコントロールで確定: わざと位置オフセットを壊した継続は相対RMS 27-30% (20倍) に跳ねる。1.5% は「正しい計算の丸めノイズ」の水準
  • 継続呼び出しのレシピ (罠3点): 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.5s494.7s164.9s
共有あり (既定)83.1s350.1s116.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場面とも赤マントを保持しプロンプトに追従)。

スパイク: Ref2VA×超短尺 (参照付き静止画、2026-08-07、実測済み → 上記の通り本実装済み)

「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

条件デノイズデコードピークVRAMwall (逐次・ロード込み)品質(中央フレーム目視)
short22 (0.917s)58.1s1.7s36.7GB265s破綻なし・高品質
baseline (5.0s)243.1s10.6s40.7GB485s破綻なし(アンカー)
  • 判定: 成立 (GO)。分布外の重なり (超短尺×参照パッキング) でも品質は崩れず、 参照の衣装・小物 (赤マント・ランタン・白いドレス)・画風は short22 でも保持された。 構図もプロンプト (苔むした岩に座る) に正しく追従
  • デノイズは t2va の22フレーム (29s) の約2倍 (58s) — 参照行のぶんパック列が伸びる ため。それでも 5 秒基準の 1/4
  • 顔の同一性の厳密さは参照画像の写り (この実験では後ろ姿気味) に依存する。これは 参照の性質であって超短尺の欠陥ではない
  • 本実装 (API/UI 化、generate_still_batch の ref2va 版) は未着手 — t2i と同じ 3点セット (尺ゲート緩和・VAE 小クリップ修正・例外クリーンアップ) は流用可能で、 バッチ位相並べ替えも t2i_batch と同じ手法 (スケジューラ _step_index リセット + per-scene FBC リセット、参照が全場面共通なら参照エンコード1回共有) が使える見込み

前段の超短尺プローブ (2026-08-07、実測記録)

フレーム数は 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.2s6.3s成功
  • 22フレームは学習分布外(公式下限5秒の1/5)でも品質が崩れず、音声も非無音
  • 教訓: プローブ中、5フレームの例外がデコード後のクリーンアップ(transformer再ロード) 前に発生し、~98.5GB残留で後続リクエストがOOMする連鎖を観測 → 上記3のクリーンアップ 経路として本実装に反映済み

2026-08-09: H3_KEEP_TRANSFORMER — transformer 常駐で毎リクエスト再ロード固定費を撤廃

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 収支の理由:

  1. H3_LOWVRAM=1(raw "1" のみ。group は transformer をホストRAM常駐+ブロック offload する別設計のため対象外)
  2. H3_TE_DEVICE 設定済み(TE を別GPUに常駐)— これが無いとエンコード位相が先に 破綻する: TE-nf4 17.45GB + 常駐transformer-int8 34.3GB = 51.75GB が実効予算を 超える
  3. 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、上記起動構成):

  • transformer(int8) は初回リクエストで1回だけロード(32.0s)。以降のリクエストで 再ロードなし(サーバーログで確認)
  • t2i turbo 4steps 定常: 9.7s/枚(denoise 4.32s、decode 1.5s)。peak VRAM 41.97GB (デノイズ時)
  • t2i steps=30 定常: 51.1s(denoise 45.7s)。peak 41.97GB
  • t2va 5秒 turbo 4steps: 44.2s(denoise 26.05s、decode 10.81s)。peak VRAM 44.15GB = デコード位相(transformer 34.03GB 常駐 + fp16 デコード)。導出予測 45.7GB に対し実測 44.15GB、カタログ 48.9GB に対し余裕 ~4.8GB
  • nvidia-smi 実測ピーク 42,620 MiB(1秒サンプリング。瞬間ピークは torch 計測の 44.15GB が正)
  • 同一seed 出力等価性: フラグOFFのベースライン(他は同条件)と PNG MD5 完全一致 (seed=11、md5 665eadddea8f34298a1b5b89e69d4bd0)。ベースライン側は total 63.27s (transformer ロード込み)/ peak 36.4GB

高速化の系譜(48GB GPU0・H3_LOWVRAM=1 系、t2va は 768²・5秒):

t2it2va 5秒
turboなし 30steps(GPU交換直後の素の構成、08-07)157s351.4s(デノイズ197.7s)
lightx2v turbo 4steps 導入(08-07朝)157s143s
+ H3_TE_PREQUANT83.2s
+ H3_TE_DEVICE~35s60.5s
+ H3_KEEP_TRANSFORMER(本節)9.7s44.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 を参照。

2026-08-09: PR #14355 マージ版 (f37ab93) への追従 第1段 — t2i/t2va/バッチ、同一seed MD5 完全一致

ブランチ 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=129.24sPNG 完全一致
t2i 30steps seed=149.65sPNG 完全一致
t2va 5秒 turbo seed=2141.54s、peak 44.15GB(同値)MP4 完全一致
t2i_batch 2場面 seed=1116.47s(8.2s/枚)場面1 = 単発 seed=11 完全一致

数値経路(スケジューラ/transformer/VAE)が無変更という影響調査の結論どおり、 移行はビット等価。性能・VRAM ピークも移行前と同値。

移植で踏んだ新しい罠2つ(いずれも _execution_device の変種):

  1. components 順で audio_vaetransformer より前に移動した(旧: 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 も窓の間外す
  2. バッチの位相並べ替えではレイアウト段の _execution_device が解決不能 (TE 外部常駐だと transformer 未ロード+TE デタッチ済み)→ テンソルが CPU に 生まれる。値は正しいので、デノイズ直前に _scene_state_to_compute() で明示的に 計算GPUへ運ぶ(既に GPU なら no-op)

2026-08-09: マージ版追従 第2段 — ref2va 系も同一seed MD5 完全一致で完了

第1段(上節)の続き。ref2va / ref2i / refバッチを f37ab93 の新契約へ移植し、 旧ピン abc5e9b で採ったベースライン(一時的に main + abc5e9b へ戻して記録)と 同一 seed・同一参照画像・同一構成(H3_LOWVRAM=1 H3_TE_PRUNE=1、TE_DEVICE なし)で比較:

経路 (steps=8)ベースライン移行後MD5
ref2i seed=101206.1s201.4sPNG 完全一致
ref2va 5秒 seed=102332.5s322.5s(定常。初回496.8sはディスクキャッシュ冷え)MP4 完全一致
ref2i_batch 2場面 seed=101(KVプレフィックスキャッシュ経路)257.9s246.7s両場面 PNG 完全一致

設計判断2つ:

  1. pipe シェルは1つに統合。マージ版の MiniMaxH3Blocks は3ワークフロー (t2va/fl2va/ref2va)のサブブロックを合併した component specs を持ち、 transformer_reftransformer の両スロットが同一シェルに存在する (MiniMaxH3AutoDenoiseStep呼び出しごとに state から分岐)。旧2シェル設計の _ensure_pipe_ref_shell / _sync_shared_components_to_refself._pipe_ref = self._pipe のエイリアスだけ残して no-op 化 -- 既存の VRAM 振り付け(_pipe_ref.transformer_ref を 触る多数の箇所)は無改修で生きる
  2. KVプレフィックスキャッシュの分割点は不変。新 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 へ戻す理由は無くなった。

2026-08-09: タブの2段組み(上段=動画・下段=静止画)と Ref2I の独立タブ化

タブが一列に増え続けるのを避けるため、出力の種別で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 に任せる。

UI の日英切替(i18n)

画面右上のトグル(日本語 / English)で再読込なしに切り替わる。選択は localStorage['h3_lang'] に保存し、初回は navigator.language で判定する。

  • 辞書は1つ(I18N = { ja, en }、165キー)。参照は t('key', {vars}) で、 未訳キーは日本語へフォールバックする
  • 静的な文言は data-i18n / data-i18n-html(<b> を含む文言)/ data-i18n-placeholder / data-i18n-title で印を付け、applyI18n() が一括で書き換える
  • JS が生成する文言は属性置換では追従しないので、切替時に rerenderDynamicI18n() が ギャラリー・結果パネル・参照タイル・各種ヒント行を描画し直す(直近の結果は lastResultData に保持して同じ描画関数へ渡す)
  • HTML を日英2ファイルに分ける案は採らなかった。UI は今後も変わるので、 二重編集は必ず片方が腐る

翻訳しないもの: 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)が切替後も不変であること、 コンソールエラーが無いことを確認済み。

2026-08-10: 音声参照でリップシンク(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。

【バグ修正】音声参照つき ref2va が sage attention で必ず落ちていた

上記の検証で発火した。音声を含む参照を渡すと確実にクラッシュする:

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)

原因: MiniMaxH3AudioAttnProcessorbackend=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)の両方で行った。

2026-08-10: 投影TE(Qwen3-VL-4B + 学習済み線形写像)— 実装と実測、未検証項目

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 はアテンションシンク

実測(96GB機、t2i 768²・30steps・seed 4242・同一プロンプト)

32B TE(現行)投影4B TE
PSNR22.64 dB
鮮鋭度(ラプラシアン分散)4837(-23%)
生成時間65.7s43.7s
ピークVRAM88.73GB76.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)で 入れられることを同日に実証済み(上記セクション参照)なので、運用上の回避策がある。

2枚目GPUの要件が下がる

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.7GBt2va のみ成立・ref2va は OOM成立(余裕 ~10.5GB)

12GB は実機確認なしに断言しない。 以前 20GB で「導出上は入るはずが実測 OOM」を 経験しており(単位の罠、§5.1)、余裕1.3GB は薄い。

追記(同日): NF4 量子化オプションと動画での品質確認

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 TE21.02GB65.7s162.1s91.9GB
投影4B bf168.88GB43.7s
投影4B NF43.11GB33.5s143.5s74.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 を追加)。

追記(同日その2): NF4 を既定化、UI からの切り替えに対応

  • H3_TE_PROJ_QUANT の既定を bnb-4bit に変更(品質実測に基づく。none/bnb-8bit は 明示指定で選択可)。既定変更に伴い「量子化指定あり+投影OFF」ガードは明示指定 (in os.environ)のときだけ発火するよう修正 -- 既定値のままの通常ユーザーを 誤って落とさないため
  • UI の再ロード設定パネルから投影TEを切替可能に(/api/settings/applyte_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。

未検証(重要)

  • 48GB機での TE+transformer 同時常駐本命。8.88 + 34(int8) = 42.9GB で載る見込みだが 未実測。これが成立して初めて導入の意味がある(96GB機では元々同時に載るため差が出ない)
  • 参照経路(ref2va) — 投影行列はテキストのみで校正されており、vision 特徴が正しく 写るかは未知。実装は動くようにしてあるが、一度だけ logger.warning を出す → 2026-08-11 に目視検証済み: 参照は明確に効く(顔・髪・服・小物まで反映、32B 正解と 同水準。「2026-08-11: ref2va/i2va はどこまで動くか」の節参照)。warning は校正の事実の 記録として残置
  • 音声参照 + fully_copy との組み合わせ(台詞を音声から入れる運用が投影TEでも成立するか)
  • 4B の TE ロード 80.5s(初回DL込み)。定常のロード時間は未測定

2026-08-10: デコード位相のピークVRAMを分解した — 内訳と、そこから取れた15%

ComfyUI の PR #15446(H3 の VAE を チャンク・ストリーミング化してデコードのVRAMを尺非依存にする)を移植すべきか判断するため、 まず自分たちのデコード位相のピーク 16.29GB が何でできているかを分解した

内訳(768×1344・107フレーム、fp32、実測)

内訳実測比率
VAE 2本の重み(常駐)11.02 GB66%
video decode の活性化3.08 GB19%
postprocess_video0.00 GB0%
audio decode0.00 GB0%
uint8変換 + CPU転送2.49 GB15%
合計16.59 GB(README の実測 16.29GB とほぼ一致)

分かったこと: ピークの2/3は重みであり、チャンク化では1バイトも減らない。 PR #15446 が対象にするのは活性化 3.08GB の部分で、移植しても削減は最大19%

デコードのピークが尺に比例することは別途確認した(潜在32→48フレームで 3.08→4.80GB、 約30MB/フレームの線形)。つまり PR の指摘自体は正しく、長尺ほど効く。

先に取った15% — uint8変換の中間テンソル

分解して初めて気づいたが、私たち自身のコードが 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)することを確認済み。

PR #15446 の移植は保留

活性化 3.08GB(19%)に対し、上流の _decode を丸ごと置き換える monkeypatch が必要で、 追従コストが生じる。より安い 15% を先に取ったので、残りは audio_vae の fp16 化(重み11GBの一部) を先に検討する方が期待値が高い (現在 fp32 固定の根拠は「bf16 で音量が約20dB落ちる」という実測だが、fp16 は未検証。 bf16 は仮数部7ビット、fp16 は10ビットで、小振幅の音声では挙動が異なりうる)。

2026-08-11: 96GB機に RTX 4060 Ti 16GB を増設 — 低VRAMゴールへの第1段(TEを2枚目GPUへ)

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 MD53cd088df882e37547219b9816a217b91新行列+sm_89 なので旧アンカーとは不一致(想定どおり)

見つかって直したバグ2件(Sonnet エージェントの検証で発覚)

  1. plain モード(lowvram なし)+ 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同居の通常経路はバイト単位で不変)。
  2. 投影行列の既定ファイル名が 404。配布元が 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 に絶対パスを渡せば再現可能。

観察: 定常 t2i が単騎比 +10s

デノイズ+デコードは 14.4s で変わらず、リクエスト毎の固定費が約29s(単騎時は約19s)。 差分の主因候補はエンコードが sm_89 の 4060 Ti 上で走ること(NF4 の dequant が遅い)+ cond の PCIe 転送。一方 t2va は 134.6s と悪化しておらず、尺が長いほど固定費は薄まる。 96GB機では TE は元々 GPU0 に載るのでこの箱で2枚目GPUを使う実益はない(本命は 16GB級単体・8GB×2 の構成検証)。

次段(未実測)

  • 第2段 = ゴールA: GPU0 をバラストで 16GB 相当に制限し、H3_LOWVRAM=group + 投影TE 同居(TE_DEVICE なし)。導出 3.11+1.4+6.6 = 11.1GB で載る見込みだが、 te_proj × group の組合せは未走行
  • 第3段 = ゴールB: 8GB×2。GPU1 に TE 3.41GB は載るが、GPU0 の blocks+活性化 8.0GB が 実効予算 7.1GB を 0.9GB 超過 — 解像度/尺で活性化を削る検討が必要

2026-08-11: 第2段 = ゴールA成立 — 実 RTX 4060 Ti 16GB 単体で t2i / t2va が完走

バラスト模擬ではなく、増設した実カードで 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・エラーは一切なし。

時間が重い理由は主に「この箱の2番スロットが PCIe x4」

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 と数値上は壊滅だが、目視では別の話だった:

  • 32B 基準・前段(bf16+sage)の出力: seed 4242 ではアニメ調の夕景の湖 (レターボックス付き)に収束していた。プロンプトの "photorealistic" や "snow-capped peaks" は反映されていない
  • 今回(int8+group+SDPA)の出力: 写実的な雪山と朝霧の湖 — プロンプトに忠実で 画質も高い

つまり int8+SDPA の組合せで生成の軌道が別のアトラクタに移り、結果的に プロンプト適合はむしろ良くなった。構成をまたぐ PSNR/MD5 比較はもともと成立しない (既知の int8 軌道分岐 ~19dB のさらに先)ので、構成間の品質は目視で判断すること。

2026-08-11: 第3段 = ゴールB成立 — 8GB×2 で t2va 5秒 768² がフル解像度のまま完走

両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つ直した: デコード末尾の 838MiB 一括 fp32 化で OOM → 逆正規化を CPU へ

初回試行はデノイズ完走後、デコード末尾の1行で OOM した。上流 decoders.pyMiniMaxH3VideoDecodeStep.__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つとも達成

  • ゴールA(単体16GB): 実 RTX 4060 Ti 16GB でフル機能、ピーク 11.4GB(余裕 ~4.7GB)
  • ゴールB(8GB×2): 計算 8GiB + TE 8GiB で t2i / t2va 5秒 768² 完走(ピーク 7.23GB)

残る注意は速度のみ(この箱は2番スロットが Gen3 x4 のため 16.5〜51s/step。 Gen4 x16 なら転送 ~8分の1)と、FBC が int8+SDPA 軌道で効いていない件(閾値調整は未検証)。

2026-08-11: ref2va/i2va はどこまで動くか — 潜伏バグ2件の発見・修正と、低VRAM境界の確定

ゴールA/B の構成で参照系(ref2i / i2va=画像参照 ref2va / 音声参照 / 768×1344)を検証した。 初回は全滅だったが、原因は VRAM ではなく今日まで一度も併用されなかった組合せで 発火する潜伏バグ2件だった。修正後は境界が VRAM 量で素直に決まるようになった。

バグ1: 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 に丸めてから返す)。

バグ2: group モードでは t2va→ref2va のモード切替が恒久的に不可能だった(pinned RAM 残留)

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×216GB単体(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分
  • 8GiB×2 は ref2i まで。動画は参照トークンの分だけ t2va(7.23GB)より系列が長く、 最短の 768²/5秒ですらデノイズ活性化が入らない(必要量は実測 9.41GB)。
  • 16GB 単体は参照系も全部動く。768×1344/5秒(実ピーク15.2GB)が実質上限。
  • 音声参照の注意: 供給音声が 5 秒未満だと最低尺チェックで 400(仕様どおり)。

品質(投影TEビジョン経路の初の目視検証): 参照は明確に効く

「投影行列はテキストのみで校正」のため vision 品質は未知だったが、目視では劣化を検出できず: 参照人物の顔・前髪・髪の長さ・カーディガンの色・白リボン・ネックレスまで一貫して反映され、 i2va の絵作り(横顔構図)は 32B 正解 test1_walk_park.mp4 とほぼ同じ。音声参照では リップ動作も明確。唯一 768×1344 の終盤に軽い構図ドリフト(破綻ではない)。 生成物: outputs/ref2i_1786447720.png / ref2va_1786449645.mp4 / ref2va_1786452054.mp4 / ref2va_1786455323.mp4

2026-08-12: 96GB機で「全部盛り」を実測 — t2i 7.65s / t2va 28.56s、そこで見つけた2つの無駄

低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.65s28.56s42.5GB
30steps + FBC(t2i 8 / t2va 7 ステップskip)21.41s121.5s41.8GB
30steps・FBC なし27.42s155.0s42.5GB
(参考)96GB 既定・30steps・FBC あり(従来の基準)約160s92GB

内訳(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 級にも載る)。

無駄1: 投影TEを GPU0 に同居させると毎リクエスト解放される

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.23s35.69s
GPU1 常駐(H3_TE_DEVICE=cuda:1)7.65s28.56s

同じ「全部盛り」でも TE の置き場所だけで t2i は2倍違う。なお 96GB機の2枚目は 16GB なので 32B TE(nf4 21GB / 削除版 17.45GB)は載らず、この常駐は投影TEだからこそ成立する

無駄2: bf16 transformer が毎リクエスト 12s かけて解放・再ロードされる

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%速い)。ところが合計では負ける:

transformert2i 定常t2va 5秒ピーク
int8 常駐(H3_KEEP_TRANSFORMER=1)7.65s28.56s42.5GB
bf16 plain モード19.9s40.0s68.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 モード側にも 同種の「解放しない」判断を入れるのが筋。未実装

2026-08-12(その2): デコード窓の解放を止めた — 単騎 45.6GB で t2i 7.4s、48GB級の本命が成立

上の「無駄2」をその場で潰した。変更は import 時ガードの条件1だけ: H3_KEEP_TRANSFORMERH3_LOWVRAM=1 を要求していたが、これを「group でないこと」に 緩め、plain モード(H3_LOWVRAM=0)でもデコード窓の解放をスキップできるようにした。 解放をスキップする分岐(if H3_KEEP_TRANSFORMER: pass)は元から共通で、復元側の _ensure_transformer は冪等なので 追加の実装は不要だった。残り2条件 (TE が GPU0 に居ない = H3_TE_DEVICEH3_TE_PROJ / H3_VIDEO_VAE_FP16=1)は そのまま plain モードの成立条件でもある(66.3 + fp16 デコード 11.4 = 77.7GB)。

全構成の比較(96GB機、turbo 4steps、768²)

構成t2i 定常t2va 5秒ピークGPU
bf16 + TE@GPU1 + 解放停止(案B)6.89s26.8s74.2GB + 3.2GB2枚
bf16 単騎(TE も GPU0)+ 解放停止(案A)7.08s27.04s77.3GB1枚
int8 単騎 + 解放停止(案C)7.40s28.13s45.6GB1枚
int8 + H3_LOWVRAM=1 + KEEP + TE@GPU1(前節の最速)7.65s28.56s42.5GB2枚
int8 単騎・解放あり(案C の KEEP=0)19.58s39.8GB1枚
bf16 + TE@GPU1・解放あり(前節)19.9s40.0s68.9GB2枚

等価性: 案C と「同条件で H3_KEEP_TRANSFORMER=0」の PNG が MD5 完全一致 (596a718e4b5cf9a0b907d2ec479225d2)。解放の停止は数学的に無影響で、同じ絵が **19.58s → 7.40s(2.6倍)**で出る。

分かったこと

  • TE の置き場所はもう効かない。単騎(TE も GPU0)7.08s vs 2枚(TE@GPU1)6.89s の 差は 2.7% しかない。GPU1 の価値は「解放を止めるまで」の話で、止めた後は 1枚で足りる(前節の「TE の置き場所で2倍違う」は解放が残っていたからこその現象)。
  • 効いていたのは解放停止そのもの(同条件で 2.6倍)。
  • 実用最適は案C: int8 単騎・ピーク 45.6GB。48GB 級の実効予算 ~49.8GB に対して 余裕 ~4.2GB で、1枚の 48GB カードで最速級(最速比 7% 落ち)が成立する。 積み残しだった「48GB機での TE+transformer 同時常駐」は、投影TE を 使う形でこれで満たされた(48GB 実機での確認は未実施 — 数値は 96GB 機で測った ピークが 48GB 予算に収まることの確認まで)。
  • bf16 のデノイズは int8 より速い(t2i 2.05-2.07s vs 2.39-2.40s)が、77GB 級の カードが要る。45.6GB で 7% 落ちの int8 とどちらを取るかは持っているカード次第。

2026-08-12(その3): 参照系(ref2i / i2va)の速度 — turbo が効いていなかったバグと、残る固定費

上の最速構成(int8 単騎・解放停止・ピーク45.6GB)で参照系も測った。参照は transformer_ref(別モデル)を使うので収支も律速も t2va とは別物になる。

まず見つかったバグ: 参照系エンドポイントだけ turbo のステップ既定を拾わない

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 に揃えて解消。

実測(96GB機・int8 単騎・turbo・768²)

モードturbo 4steps30steps(turboなし)デノイズ(4/30)ピーク
ref2i(参照付き静止画・22フレーム)79.3s148.4s7.8s / 72.1s45.4GB
i2va(画像参照→動画5秒・124フレーム)103.1s290.3s22.0s / 209.0s45.9GB

ピークは 45.9GB で t2va 系(45.6GB)とほぼ同じ — 参照系も 48GB 級1枚に収まる。 turbo の効きが t2va(5.5倍)より小さい(i2va で 2.8倍)のは、下記のとおり デノイズ以外の固定費が支配的だから。

積み残しだった「ref2va × turbo」は成立(品質を目視確認)

turbo LoRA は transformer でしか実測しておらず transformer_ref は未検証だったが、 4ステップでも参照人物の忠実度は保たれた: 前髪・髪型・薄緑カーディガン・白リボン・ ネックレスまで一致し、動画も先頭〜中央〜末尾で人物が一貫、カメラ追従・公園の情景とも プロンプトどおりで破綻なし(outputs/ref2i_1786509275.png / outputs/ref2va_1786509457.mp4)。strength 0.094 のまま参照条件付きの軌道でも成立する

残っている固定費(i2va 103.1s の内訳、ログのタイムスタンプから)

位相時間備考
参照のビジョンエンコード約47s最大の律速。4B 投影TE でも縮まない(32B 時代の ~65s/場面から改善はしている)
デノイズ(4steps)22.0s
デコード + VAE 往復約10s
参照のVAEエンコード約6s
末尾で t2va 用 transformer を再ロード約13s連続 ref2va では純粋な無駄
  • 47s のビジョンエンコードが参照系の本丸。バッチ経路には既に場面間共有 (H3_REF_PREFIX_CACHE)があるが、単発リクエストの繰り返しでは共有されない (同じ参照画像を使い回す運用ではプロセス跨ぎのキャッシュが効くはず — 未実装)。
  • 13s の再ロードは、ref2va 完了時に「t2va の定常状態」へ戻す設計のため。 次も ref2va なら不要で、H3_KEEP_TRANSFORMER と同種の「戻さない」判断を 入れられる余地がある(未実装)。

見込みの検証(同日、バッチ実測で確認)

「この2つを潰せば i2va 103s → 45s 程度」という見積もりは、バッチ実測で支持された。 バッチ経路は「参照エンコードを共有したらどうなるか」を実際にやっている実験そのもので、 768² に揃えた計測では:

単発バッチ1件あたり実測削減共有モデルの予測
ref2i(3場面)79.3s47.0s32.3s/枚31.3s/枚+1.0s
i2va(2場面)103.1s75.0s28.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倍のピクセル)で生成していたため「動画では共有の利得が出ない」と 誤読して見積もりを撤回した。解像度を揃えたら一致した。経路間の比較では解像度を明示する — 前付けの「バッチ」節の囲みも参照。

2026-08-12(その4): lightx2v turbo LoRA v1.0 スパイク — 768p 版で品質が別次元に

上流 lightx2v/Minimax-h3-Turbo が 本日 v1.0 を公開した(v0.1 に加えて2種)。上流 ModelTC/Minimax-H3-Turbo の Model specs:

モデル学習解像度shift(video/audio)推奨NFEmetadata alpha
4-step v0.1(現既定)544p 混合12 / 34なし(実測 scale 0.094)
8-step v1.0544p 混合12 / 38(4も可)8 → scale 8/128 = 0.0625
4-step v1.0 768p768p(1344×768)6 / 34128 → 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() で上書きできるようにした。

実測(96GB機・int8 単騎・解放停止・投影TE、いずれも定常)

構成t2i 768²t2va 5秒 768²品質(目視、seed 4242 の雪山湖)
v0.1 4step(scale 0.094、現既定)7.40s28.13s岩肌・反射に市松状アーティファクト顕著
8step v1.0 @8steps(scale 0.0625)10.94s49.61s非常にクリーン(光条まで出る)
8step v1.0 @4steps7.47sv0.1 同等〜やや良
768p v1.0 @4steps(scale 1.0 + H3_VIDEO_SHIFT=6)7.59s28.32s8steps 級にクリーン、v0.1 のアーティファクトほぼ消滅
同・1344×768(学習解像度)10.53s特に良好(雲・霧・樹木の質感)

結論: 768p v1.0 が本命。 速度は v0.1 と同じ 4steps のまま、品質だけが 8steps 級に上がる。 アニメ調の t2va(seed 555)も目視で高品質・破綻なし。

既定化はまだしていない(shift の設計判断が要るため)

現状の 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_vaesH3_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 自動切替なしに戻る)。

設計からの逸脱は無し(タスク仕様どおりの実装)。

実機検証(96GB機、`CUDA_VISIBLE_DEVICES=0 H3_TRANSFORMER_QUANT=int8

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 42428.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ビットも変わらない」ことがコード変更をまたいで実証された。

2026-08-12(その5): 「無音で出力」トグル(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 を返す。
  • 速度は変わらない(実測 t2i 8.84s vs 7.71s は通常のばらつき範囲)。音声は生成された上で 捨てられるだけ。速度目的の機能ではない。

実測(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 を使うこと

2026-08-12(その6): 「<d> を使っていないのに声が出る」の原因調査

ユーザー報告「<d> を使わないのに声が生成される」を調べた。原因は2つあり、片方は こちらの不具合、もう片方はモデルの性質だった。あわせて「以前の説明(<d> を使わなければ 声は入らない)は不正確だった」ことを記録しておく — <d>台詞を指定するためのタグで、 声が出るかどうかを制御するものではない

測り方: 声らしさの客観指標

聴かずに判定するため、音声帯域(300-3400Hz)のエンベロープが音節レート(4-8Hz)で どれだけ変調されているかを測る指標を用意した(speech_score = 4-8Hz の変調エネルギー / 0.5-20Hz 全体)。人の声はこの帯域に強い変調を持ち、風・水・環境音は持たない。

生成rmsspeech_score
女性が公園を散歩(LLM強化が**「子供たちの笑い声」を挿入**)0.2070.307
通学路の少女(声の指示は一切なし)0.0330.298
同・プロンプトで明示的に声を禁止0.00710.272
同・LLM修正後の強化文で生成0.01040.269
雪山の湖(人が映っていない)0.3640.125

原因1(こちらの不具合・修正済み): LLM強化が人の声を書き込んでいた

core/llm.py のストーリーボード用 few-shot 例が「商店街の雑踏と呼び込みの声」で、 モデルがこれを真似て「遠くで聞こえる子供たちの笑い声」等を勝手に足していた。H3 は音の記述に 忠実なので、そのとおり声が生成される。共通ルールに「ユーザーが声を求めていない限り、 音の記述に人の声(台詞・話し声・笑い声・歓声・呼び込み・歌声)を入れない」を追加した。 修正後、同じ入力「公園を散歩する女性」の強化結果は風・枯れ葉・鳥の声のみになり、 人の声は消えた。

原因2(モデルの性質・回避不能): 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(無音で出力)を使う — 音声トラックごと落とすので確実

2026-08-12(その7): 台詞(<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> タグを外し、地の文で「〜と挨拶する」のように書く
    → 発話らしい音は出ますが、台詞の内容は保証されません。

さらに: 生成を投げる前に UI で警告する

エラーは生成リクエストを消費してから返るので、プロンプト欄に <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 は時々破るので、指示と後処理の二段で対処した:

  1. 共通ルールに「書き直しは出力前に済ませ、訂正・注釈・言い訳を本文に残さない。 『(100mmに修正)』『※』『←』のようなメタ記述を混ぜてはならない」を追加。
  2. 後処理サニタイザ _sanitize_enhanced() を全4モード(brief/storyboard/translate/ h3-official)の戻り値に適用。(a)「NNmm(MMmmに修正)」を訂正後の値へ畳む、 (b) 単独で残った訂正注釈の括弧を落とす、(c) 許可外の焦点距離を最も近い許可値へ丸める (24mm → 35mm 等)。発火したら logger.info を出す(黙って書き換えない)。

ユニット検証(全ケース期待どおり): 85mm(100mmに修正)100mm(全角・半角括弧とも)、 24mm35mm50mm/35mm/100mm は不変。実機でも同じ入力を3回投げ、焦点距離は 全て許可値・メタ記述の混入ゼロを確認。

2026-08-12(その8): ref2va 後の t2va transformer 復元をやめた(既定を遅延へ)

「結果表示のピーク 49.47GB と、生成ボタン下の GPU alloc 74.21GB がなぜ違うのか」という 疑問から、そもそもその復元は必要なのかを検討した。

まず表示が違う理由(どちらも正しい)

peak_vram_gb(結果表示)はリクエスト先頭の reset_peak_memory_stats() からデコード 直後までの高水位で、ステータス行はリクエスト完了後の現在値。ref2va は入口で t2va 用 transformer を解放し、終了時に積み直していたため、両者は別の瞬間を見ていた:

時刻(実測ログ)イベントGPU alloc
入口transformer freed3.11GB(投影TEのみ)
transformer_ref loaded in 34.8s37.2GB
デノイズ+デコード ← ここが結果表示の 49.47GB
終了直前transformer loaded in 36.9s(復元)74.29GB ← ステータス行はこれ

復元は「無駄 + 遅い + VRAM高水位を上げる」だった

復元はそのリクエストの所要時間に含まれるので、ユーザーは自分の動画と無関係な ロードを待たされる。しかも次も 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 で戻せる。

A/B 実測(96GB機・768²・同一 seed・turbo・連続 ref2va 3本)

eager(旧既定)lazy(新既定)短縮
初回(transformer_ref コールドロード込み)185.92s131.13s-54.8s
定常(2〜3本目の平均)108.4s90.8s-17.6s
生成中ピーク45.67〜45.92GB45.67〜45.92GB同じ
t2va transformer のロード回数 / ref2va 3本4回0回(t2i を投げた時に1回)
リクエスト間の常駐74.28GB3.11GB

連続 ref2va ではリクエスト間の常駐が 74.28GB → 3.11GB になり、48GB 級での運用に 効く。コストが移るだけの ref2va→t2va 切替では、切替後の最初の t2i が 57.17s (2本目以降は 16.18s)と、移った分を素直に払う。

注意: 上の「生成中ピーク」は両者同じで、混在運用の高水位(74.3GB)が消えるわけでは ない。t2va と参照系を同じプロセスで混ぜれば、遅延復元でも t2va 側で両 transformer が 載る。48GB 1枚ならプロセスをモード別に分けるという結論は変わらない。

2026-08-12(その9): 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 として弾く = 同居相手のメモリを奪わない。 指定がカード総容量以上なら警告だけ出して素通しする。

実測(96GB機)

検証結果
上限8GBで 6GB 確保OK
同・20GB 確保OOM で正しく拒否
H3_VRAM_LIMIT_GB=48 + 通常構成(int8単騎・turbo・投影TE)で t2i7.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/statusrunner.vram_limit_gb で確認する。

2026-08-13: 48GB で ref2va 1344×768 を通すには — TE を2枚目GPUへ(実測で確定)

前日の最終テストで踏んだ「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 を逃がすほうが良い。

そのために必要だった修正2件(どちらも「塞がれていて誰も通っていなかった」経路)

  1. _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が不当に弾かれなくなった。
  2. 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 不一致 → 修正、という順で顕在化した)。

2026-08-13: 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.0s11.1s / 28.3s55.83GB
672×384 → 1344×76834.0s4.5s / 8.6s44.67GB

旧実測(96GB機・bf16 + 32B TE・30steps)の 645s / 88.0GB から、時間で7.5倍・VRAM で 32GB 以上の改善。

品質問題の切り分け(同日、ステップ掃引で解決)

ユーザー指摘「動く腕に二重の線」を受けて調査。当初「ステップを増やしても消えない」と 判断したがこれは誤りだった(30steps の対照は turbo なしで、別種の破綻を見ていた)。 同条件でパス2のステップ数だけを振ると:

turbo・総ステップパス2品質(目視・動く部位)時間
4steps(既定)1動く輪郭が激しく二重化・手が崩れる86s
8steps3半透明のゴーストが残る(体越しに背景が透ける)135s
12steps4クリーン(モザイクも二重輪郭もなし)219s
  • sage は無関係: SDPA(attn=default)でも 4steps の二重輪郭は同一。
  • パッキング往復(patchify_unpatchify)は純テンソルプローブでビット一致、 x0 復元・再ノイズ式もマージ版スケジューラの実装と一致 — 移行漏れではなく、 純粋にパス2のステップ不足が原因だった。
  • turbo なし 30steps(パス2=10)の格子モザイクは別問題として残る(bf16+32B 時代の 検証では出ていなかったので int8/投影TE との相互作用を疑うが、未切り分け)。

推奨レシピ: turbo=1 + num_inference_steps=12 + upscale=148GB 級で使える ワイド版も同条件で確認済み:

生成 → 仕上げstepsパス2合計ピーク品質
768² → 1536²124219s55.8GBクリーン
672×384 → 1344×76812496s44.43GBクリーン(4steps 版で出ていた輪郭の甘さも解消)

ただし写実系では upscale は使わないほうがよい(2026-08-13、ユーザー指摘を受けて実測):

1344×768・同一プロンプト(写実)・同一seed時間ピーク髪・建物の高周波成分
直接生成(turbo 4steps)52.4s47.79GB0.2675(基準)
672×384 → upscale(turbo 12steps)89.1s44.69GB0.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.1s59.21GB0.2596
upscale(turbo 12steps)218.9s55.83GB0.2750

目視でもどちらも良好で、直接生成のほうがむしろ構図が素直。つまり upscale の利点は 「ピークが 3〜3.4GB 低い」ことだけで、代わりに1.5〜1.7倍遅く写実系では細部が 25% 落ちる

使いどころは「直接生成では VRAM が足りず、upscale なら収まる」という幅 3GB の隙間だけ (例: 1536² を 56〜58GB 級の予算で出したい)。それ以外は直接生成が上位互換。 UI に出しておく価値はあるが、既定で勧める機能ではない

残っている懸念(この機能を今後さわる人へ)

  1. turbo なし + upscale の格子モザイクが未解決。30steps(パス2=10)で空・電線に市松状の 破綻が出る。bf16 + 32B TE 時代の実測では出ていなかったので int8 量子化または投影TE との 相互作用を疑っているが、切り分けていない。再現は turbo=0 num_inference_steps=30 upscale=1。切り分けるなら「turbo なし・30steps・bf16 transformer」を1本(77GB級が要る)。
  2. turbo とパス2ステップ数が構造的に噛み合わない。turbo は少ステップが前提、upscale の パス2は多ステップを要求する。既定配分(H3_HIRES_DENOISE=0.35)では turbo 既定4ステップで パス2が1ステップしか取れない。12ステップまで上げれば直るが、そのぶん turbo の速度利点が 消える。配分そのものを見直す(パス2に最低ステップ数を保証する)余地はある。
  3. 回帰セットに入っていなかった。この不具合は PR #14355 移行(2026-08-09、第1段=t2i/t2va/ バッチ・第2段=ref2va 系)で upscale が対象外だったために4日間気づかれなかった。 同種の取り残しが他の経路にもある可能性があり、次に diffusers を上げるときは upscale=1 を回帰対象に含めること(docs/internal/regression_baselines.json は upscale ケースを持っていない)。
  4. 品質判定がスタイル依存。アニメ調(平坦色)では細部の欠落がほぼ見えず、写実系で 初めて露見した。この機能の検証は必ず写実プロンプトを含めること。
  5. 等倍で見ないと分からない。縮小表示やサムネイルでは差が出ない。判定は 1:1 クロップ + 高周波成分の比較で行うこと(本節の数値はその方法で取った)。
  6. 最も VRAM が厳しい構成でこそ使えないという逆説。upscale=1 は t2va 専用(fl2va は 明示拒否・ref2va は経路なし)で、低VRAMモード(H3_LOWVRAM=1/group)では 400 拒否。 つまり「VRAM を節約したい」動機が最も強い場面では、そもそも選べない。
  7. 警告がサーバログにしか出ていなかった問題は解消済み(2026-08-13): UI のチェックを入れて いるあいだ、注意書きを画面にも表示するようにした。ラベルの「約3.6倍時間」も bf16/30steps 時代の値だったため削除(現構成では 1.5〜1.7倍)。

UI からは非表示にした(2026-08-13)

上記のとおり直接生成の下位互換なので、チェックボックスを UI から隠した。機能自体は 残してあるupscale=1 を API に直接渡せば従来どおり動くので、「直接生成では VRAM が 足りず upscale なら収まる」幅3GBの隙間には引き続き使える。

表示の可否は static/index.htmlUPSCALE_UI_VISIBLE(既定 false)一箇所で決まる。 markup 側の style="display:none;" だけ外しても updateStillControls() がモード切替のたびに display を上書きするので戻らない — 戻すときはこのフラグを true にすること。 警告は実測ベースの条件付きに変更: パス2 が 4 未満になるステップ数なら具体的な推奨値 (12steps)を示し、turbo なしならモザイクの既知問題を知らせる。

記録(検証の教訓2つ): (1) 修正直後に 1536² の静止した中央フレームだけを見て 「破綻なし」と誤報した — 動画の破綻は動いている部位に出るので、連続フレームを 並べて動く部位を見ること。(2) 「ステップを増やしても消えない」も誤りだった — 30steps の対照は turbo の有無という別の変数も同時に変えていたため、別種の破綻を 同一視した。1変数ずつ振ること(B11 の解像度と同型の教訓)。

48GB 級との関係: 1344×768 なら upscale 経由が最も軽い

1344×768 を得る方法ピーク48GB 級予算 ~49.8GB写実系の細部
t2va を直接 1344×768 で生成47.79GB入る(余裕 ~2GB)良い(基準)
672×384 → upscale で 1344×76844.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)が答え。

なお、今回の用途(48GB で 1344×768 の ref2va)にはそもそも使えない

upscale=1t2va 専用で、fl2va は明示的に ValueErrorref2va にはそもそも 経路が無い(still=1 との併用も拒否)。さらに 低VRAMモード(H3_LOWVRAM=1/group)では 400 で拒否される(パス2の系列長が約4倍になるため未検証扱い)。したがって 「小さく生成して upscale」は参照系の VRAM 対策にはならず、TE を2枚目GPUへ逃がすのが 引き続き正解(前節の実測: 49.73GB → 46.36GB)。

2026-08-13: 全機能の回帰監査 — 21項目、実バグ2件を発見・修正

upscale が「回帰セット外だったため4日間壊れたまま」だった事故を受けて、全エンドポイント・ 全経路の監査を実施した(等価性アンカー2件・実行テスト16件・静的精査3件)。

結果: 19/21 健全。発見した実バグは2件(いずれも設定リロード系、修正済み)

バグ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> の運用がこれで実際に機能する。

環境起因1件(コードのバグではない)

ref2va_batch + TE@GPU1 が OOM — ただし GPU1 には別サービス(ace-server、約10GB)が常駐して おり、純粋な容量不足。_te_external_usable_for() の閾値判定は「TE用GPUの総容量」だけを 見ており、他プロセスの使用量を見ないため、同居環境では判定を通過してから実行時に OOM する。 実測空きを見る判定への変更は設計判断が要るため未実施(参照枚数で必要量が変わり、固定の 床値では1枚参照の成功ケースを誤って弾く)。

監査で確認できた健全性(抜粋)

  • 等価性アンカー2件とも一致: turbo=0 基底経路(f3622b6d)・旧 v0.1 LoRA 後方互換(596a718e)
  • 未検証だった経路が全て PASS: fl2va の last_image のみ/両端指定、複数参照、音声参照+尺自動 導出、mute×バッチ、バッチ×turbo(steps 既定4)、バッチ×外部TE(ガード緩和で初開通の 経路 — ref2i_batch は device 不一致なく完走)、LLM brief/translate、concat
  • 静的精査: hires と同型の「denoiser_input_fields 更新漏れ」は他に無し。死んだ旧 API 参照も無し

回帰基準(regression_baselines.json)の再設計が必要

現行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。基準セットの作り直しは別途(方針判断待ち)。

2026-08-14: この箱向けの回帰基準セット(dev96)と実行ランナー

前日の監査で「既存の回帰基準 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。
  • 採取後にもう一度全実行して 16/16 PASS = 決定論を実証。非決定的ケースはゼロ。 既知アンカー2件(f3622b6d 基底経路 / 596a718e 旧LoRA互換)とも一致し、新旧基準の 連続性を確認。
  • 注意: MD5 は sm_120 + sage + v1.0 768p LoRA に依存(_meta に明記)。runtime は 実行順に依存する参考値で、閾値チェックには使っていない。設定リロード往復は基準対象外 (機能検証は 08-13 の監査で済み)。

使い方: venv/bin/python scripts/run_regression.py(全グループ実行)。

今後の外部イベント待ち(積み残し、2026-08-06時点)

1. diffusers PR #14355 — マージ済み(2026-08-05)、追従も完了(2026-08-09)

この節は追従前に書いた影響調査の記録。実際の追従結果は上の「マージ版 (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 に再編)、 MiniMaxH3AutoKeyframeVaeEncoderStepMiniMaxH3KeyframeVaeEncoderStep / 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 エージェントによる全行監査):

  • 数値経路の土台は無変更: scheduling_minimax_h3.py の差分(36+/41-)は docstring 整形のみでコード変更ゼロ(diff で確認)。transformer は padding-row 処理の削除のみ (QKV 構造・forward シグネチャ無変更)、video VAE の _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 は 全面書き直し
  • ref2va は 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 として実装済み)。

2. Turbo LoRA 完成版のリリース待ち → lightx2v 版で解決(2026-08-08 本実装済み、「Turbo LoRA」節参照)

H3_TURBO_LORA=1 の配線は完成済み(上記セクション参照)。現行LoRA(Ostris氏)は 「デモ/プレビュー、学習途上」と明記されているため既定OFF、かつint8/低VRAMでは 使用不可(融合QKV前提 → Int8Tensoraten.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.projlora_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 が対応値、という仮説が 実測と一致した。

strength4steps の結果音声 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 への適用可否のみ未検証のまま。

3. 未着手の改善候補(優先度順、いずれも急ぎではない)

  • 量子化済みチェックポイントの事前保存TE は実装済み(H3_TE_PREQUANT、 2026-08-08。TEロード 53.0s→29.5s、リクエスト合計 -35%)。残るのは transformer int8 で、保存に約34GB を要しこの箱のディスク空き(43GB)では非現実的。ディスクに余裕の ある機なら、同じ手法で transformer のロード(約32.5s)も削れる見込み
  • TE を2枚目GPUに常駐させる2026-08-09 実装済み(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 での 実測値)。参照付き生成を高速化したくなったら、まずここをスパイクすること
  • 投影TE (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_PROJH3_TE_QUANT/H3_TE_PRUNE/H3_TE_PREQUANT を import 時ガードで排他にしている。これは「32B TE 向けの設定を4Bへ流用させない」 ための措置であって、4B を量子化してはいけないという意味ではない。実装するなら 4B専用の別フラグ(例 H3_TE_PROJ_QUANT)として足し、既存ガードの意図を壊さないこと。 投影行列は fp32 のまま適用する必要がある点にも注意(W は 2560×5120 の fp32)
  • 16GB級対応達成(2026-08-11): 投影TE NF4(3.11GB)+ 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 要件の再計算を参照)
  • torch.compile: 未検証。FBC/group offload の hook との相性(graph break)確認が要る
  • torchao の C++ カーネル: int8モードの dequant コスト(+5s)を解消しうるが torch>=2.11 が必要で、venv全体のリグレッションリスクが大きい(非推奨)

ライセンス

このリポジトリのコードは 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プロンプト強化(2026-08-04追加)

ローカル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}
  • モード(UIの「LLM強化」ボタン+モード選択):
    • 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.pyskills_cache/(.gitignore 済み)へ事前取得しておく必要があり、未取得時は 400+取得コマンド案内のエラーになる。システムプロンプトはSKILL.md+参照ガイド全文を 要約せず投入(約18.8KB=t2va/fl2va、約26.6KB=ref2va)。
    • translate: 過剰創作なしの英訳(CUT構造は保持)
  • 強化結果はプロンプト欄を置き換え(編集可)、「元に戻す」で1世代復元。生成結果には 使用プロンプト全文を折りたたみ表示(強化あり/なし・モード間の比較評価用)
  • UIに手書き用「プロンプトガイド」チートシート(公式ブリーフ構造・CUT記法・実測カット精度±1秒)を同梱
  • 実LLM検証済み(gemma4-31B Q4_K_M): 3モード(brief/storyboard/translate)とも形式に従う ことを確認(storyboardはタイムコード合計・焦点距離制限も遵守、応答11〜17秒)。 h3-official は2026-08-07追加時点でLLMサーバ未接続のため生成A/Bのみ実施(下記参照)、 LLM経由での実応答確認は別途必要

Contributors

yuzuki4251-afk

87 commits

animede

29 commits

animede/Diffusers_minimax-h3

MiniMax-H3 (video + synchronized audio, 33B) on diffusers — 9.7s per still, 44.2s per 5s clip on 48GB, with every speedup verified byte-identical

19

stars

116

commits

Python

primary language

Aug 14, 2026

updated

README

minimax-h3

日本語 | 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 本体には一切手を入れていない)。

GUI メイン画面

ブラウザから使う単一ページUI。5つのモードをタブで切替(動画: T2VA=テキスト / FL2VA=フレーム / Ref2VA=参照、静止画: T2I / Ref2I)。下段は生成物ギャラリーで、 チェックした動画を連結して書き出せる。プロンプトはローカルLLMで H3 公式ガイドの形式へ強化でき、 FirstBlockCache・Sage・Turbo・量子化・低VRAMモードなどは再起動なしでこのパネルから切替。 日英切替あり。

VRAM×機能マトリクス(2026-08-11 実測)

○ = 実測で完走 / △ = 導出見込み(未実測)/ × = 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²ref2ii2va(画像参照)音声参照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
  • 12GB 単体は未実測。t2va 実ピーク 11.4GB / i2va 9.41GB(実11.2GB)が 12GiB に収まる計算だが、 音声参照(実14.7GB)・768×1344(実15.2GB)は超える。
  • 16GB / 8GB 構成の鍵は 投影TE(Qwen3-VL-4B + 学習済み線形写像 ClipProj、NF4 で 3.11GB。 32B の text_encoder の代替)+ H3_LOWVRAM=group(transformer int8 をブロック単位で ストリーム、GPU常駐は ~1.4GB)+ video VAE の fp16 デコード。詳細は日付節 2026-08-10〜11。
  • 測定環境は2台のマシンで共有: 16GB / 8GB 列は 96GB機(RTX PRO 6000 + 増設 4060 Ti 16GB)、 48GB+20GB 列は 48GB機(RTX PRO 5000 48GB + RTX 4000 SFF Ada 20GB) での実測。旧版の実測 (96GB単騎時代)は各日付節にそのまま残してある。

VRAM級別クイックスタート

セットアップは「インストール」「モデルの取得」を先に済ませること。 起動後、ブラウザで 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 なし)157s351.4s
+ lightx2v turbo LoRA(4steps)157s143s
+ H3_TE_PREQUANT(量子化TEのディスクキャッシュ)83.2s
+ H3_TE_DEVICE(TE を2枚目GPUへ)約35s60.5s
+ H3_KEEP_TRANSFORMER(transformer 常駐)9.7s44.2s
16倍8.0倍

デノイズ自体を速くする turbo だけでは 2.6倍で頭打ち。残りの3倍強は すべて固定費の撤廃から来ている(turbo なし 30steps のままでも、固定費撤廃だけで t2i は 157s → 51.1s と3倍速い)。

モード別の到達点(全モードを同一構成で実測、2026-08-12)

構成: 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.40s7.9s(0.94倍 = 使う意味なし)2.40s45.0GBVAE の CPU↔GPU 往復 ~3.3s
t2va 5秒 768²28.13s(バッチAPIなし)14.94s45.6GBデコード 7.05s
ref2i 768²(参照付き静止画)79.3s47.0s(1.69倍)7.8s45.4GB参照ビジョンエンコード ~47s
i2va 5秒 768²(画像参照→動画)103.1s75.0s(1.37倍)22.0s45.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。

連番生成(バッチ)について分かったこと

  • t2i のバッチはもう使う意味がない(0.94倍 = わずかに遅い)。バッチは元々 「モデルのロード固定費をバッチ1回に償却する」機能で、常駐化で償却すべき固定費が 消えたため(常駐化前は 157s → 67.5s の 2.3倍だった)。
  • 参照系だけは今も効く。共有されるのがロードではなく参照ビジョンエンコード ~47s だから。バッチのステップ時間は単発と完全一致しており(ref2i 2.598s vs 単発 2.599s、 i2va 7.321s vs 7.323s)、バッチ経路自体のオーバーヘッドはゼロ — 差は純粋に 「47s のエンコードを何回払うか」だけ。
  • 場面数が増えるほど効く: 節約は 47×(場面数-1)/場面数。i2va なら 2場面 1.37倍 → 3場面 1.44倍 → 5場面 1.57倍。同じキャラクターで長い物語を作る用途ほど有利。
  • 「共有すれば縮む」モデルは静止画・動画とも成立(実測削減 vs 予測: ref2i 3場面が 32.3 vs 31.3s/枚、i2va 2場面が 28.1 vs 23.5s/本)。→ 参照エンコードをリクエスト跨ぎで キャッシュすれば、単発の繰り返しでも同じ短縮が得られる見込み(未実装、日付節 2026-08-12)。
  • 制約2つ: バッチ経路は 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 では判定できないので目視で見ること。

最近の更新(2026-08-11〜12)

  • 最終ゴール2つを達成: 実 RTX 4060 Ti 16GB 単体でフル機能(t2va 768² ピーク 11.4GB、 最大は 768×1344 の実 15.2GB)/ 8GB×2 で t2va 5秒 768² がフル解像度のまま完走(ピーク 7.23GB)。
  • デコード末尾の逆正規化を CPU 化(ビット同一、全構成でデコードピーク減)。
  • H3_VIDEO_VAE_FP16 × 参照ありの dtype 即死を修正(エンコード側にも fp16 autocast)。
  • group モードの pinned RAM 残留を解放し、t2va↔ref2va のモード切替が可能に。

現在地と再開の入口(2026-08-12 時点)

作業を再開するとき、まずここを読む。

知りたいこと見る場所
仕様・性能・設計(何をどう実現しているか)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×機能マトリクス」参照
ホストRAM64GB 以上を推奨(H3_LOWVRAM=group は int8 重み ~34GB をRAMに常駐させるため 48GB 以上の空きが必要)
ディスク約 145GB(T2VA/FL2VA のみ)/ 約 207GB(Ref2VA も使う場合)
CUDA12.8 系(torch 2.9.0+cu128 に合わせる)。SageAttention をビルドするなら nvcc も同系統
Python3.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 回帰)を踏むこと。

SageAttention(任意、既定で有効)

既定の 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超で警告)。

VRAM対応表と主な環境変数(早見表)

用途に応じた起動例。上段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=int8peak 59.7GB / 約160秒
48GB級H3_LOWVRAM=1peak 38.9GB / 約215秒
32GB級H3_LOWVRAM=grouppeak 28.7GB / 約280秒
18GB級H3_LOWVRAM=group H3_TE_PRUNE=1peak 17.7GB / 約280〜320秒
16GB 単体(実 4060 Ti)H3_LOWVRAM=group H3_TE_PROJ=NicoLab28/ClipProj-MiniMax-H3 H3_VIDEO_VAE_FP16=1peak 11.4GB / 約25分※
12GB 単体(見込み・未実測)16GB 単体と同じ実ピーク11.4GBが12GiBに入る計算。参照系・768×1344は不可
8GB×2(バラスト実測)16GB 単体の指定 + H3_TE_DEVICE=cuda:1peak 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_QUANTbnb-4bittext_encoder の量子化(none は bf16 で 66.7GB)
H3_TE_PRUNE0TE の未使用上位レイヤー削除(出力は不変、-3.6GB)
H3_TE_PROJ(無効)投影TE: 32B TE を Qwen3-VL-4B+線形写像で代替(repo id か .safetensors パス。品質は近似、日付節 2026-08-10 参照)
H3_TE_PROJ_QUANTbnb-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_QUANTnoneint8 で transformer を 66.3→34GB
H3_LOWVRAM01=48GB級のフェーズ循環 / group=32GB級以下の block offload
H3_KEEP_TRANSFORMER0transformer 常駐で再ロード固定費を撤廃(成立条件は該当節参照)
H3_CACHE / H3_CACHE_THRESHOLDfbc / 0.05FirstBlockCache(デノイズ -25%。int8+SDPA 軌道では不発の実測あり)
H3_ATTN_BACKENDsagesage は sm_120 専用ビルド。それ以外のGPUは default(SDPA)必須
H3_TURBO_LORA04/8ステップ蒸留LoRA(初回に約780MBをDL。group とは併用不可)
H3_VIDEO_VAE_FP160video VAE を fp16 化(デコードのピークを削減。16GB 以下では必須)
H3_LLM_URLhttp://127.0.0.1:64650プロンプト強化に使うローカルLLM(任意機能)

VRAM/RAM 設計 (重要、実測に基づく)

このマシンは 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 入れ替えが消滅する。

  • 定常常駐: transformer + TE-nf4 = ~87.5GB。これに VAE 11GB を足すと~98.5GBで 96GBを超えるため、このモードでは VAE ペアは常駐しない(CPUに置き、キーフレーム エンコード/デコードの当該フェーズだけGPUへ往復する。fp32 11GBのPCIe往復のみで ディスクI/Oなし)。
  • デコード窓(~9s)だけは transformer を解放してから実行し、直後にリロードする (transformer+TE+VAE+デコードバッファは物理的に収まらないため。実測でOOM確認済み)。 毎stepのスワップではなく単発の片道×2なので、diffusers-server CLAUDE.md 33番の 禁止パターンには当たらない。
  • 品質A/B(同一seed 12345): フレーム比較で構図・被写体・シャープさは同等 (条件付け数値の変化による木立の配置等の微差のみ)、音声も rms 0.0080→0.0061 と 同水準で -20dB 型の崩壊なし。劣化なしと判定して既定化した

H3_TE_QUANT=none (bf16 TE、旧方式)

2つの66GBモデルをリクエストごとにGPU上で入れ替える:

  • vae + audio_vae(fp32 計11GB)は常時GPU常駐。
  • エンコード段階: VAE 11GB + text_encoder 66GB
  • デノイズ/デコード段階: VAE 11GB + transformer 66GB
  • 解放は CUDAモデルの参照を直接落とす(.to("cpu") でRAMへ退避しない。66GBの RAM経由はスワップ突入の実測原因になった)。リロードはページキャッシュ/ディスクから 11〜40秒/モデル。リクエスト間の定常状態は transformer+VAE 常駐(77.5GB)。

実測のオーバーヘッド: 1リクエストあたり TEロード ~37s + transformerリロード ~26s。 この入れ替えコストを解消したのが上記の bnb-4bit(既定)で、リクエスト合計は 245s → 185s に短縮された(デノイズ157sは共通のモデル律速で不変)。

2つの実装上の罠(実機で踏んで修正済み):

  1. MiniMaxH3TextEncoderStep.encode_prompt は素の staticmethod で、@torch.no_grad() はブロックの __call__ 側にしか付いていない。直接呼ぶ場合は必ず torch.no_grad() で包むこと。忘れると autograd グラフが TE の重み約50GB分をGPU上にピン留めし、 モデルを解放してもVRAMが返ってこない(diffusers-server CLAUDE.md 39番と同型)。
  2. ブロックの出力(num_frameskeyframes・latent形状等)は PipelineState に入る。 get_block_state() は宣言された入力しかマップしないので、出力は state.get(名前) で読むこと。

video VAE の decode は diffusers 側の MiniMaxH3VideoDecodeStep が内部で torch.autocast(dtype=torch.float16) を使うため、重み自体は fp32 のままでよい。 audio VAE は fp32 のまま一切キャストしないこと(bf16化すると生成音声の音量が 約20dB小さくなる既知の問題があるため、runner.pyvae/audio_vae のロードに 明示的に dtype=torch.float32 を渡している)。

実測値 (RTX PRO 6000 Blackwell 96GB, 768×768, 124フレーム=5.17秒, 30steps)

項目実測
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/リクエスト)に戻せる。

FirstBlockCache によるデノイズ高速化 (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 でほぼ同一(目視でも区別困難)。
  • threshold 0.1 は 1.92x(デノイズ81.5s、14スキップ)だが構図が目視で分かるレベルで ドリフトするため既定にしていない(速度最優先の場合のみ)。
  • H3_CACHE=none でキャッシュ無しの従来挙動に完全に戻る(バイト一致を回帰確認済み)。
  • ピークVRAMは残差キャッシュ分 +0.7GB(91.4→92.1GB)。
  • 実装メモ: MiniMaxH3TransformerBlock はPRブランチの TransformerBlockRegistry に 未登録のため、runner側で TransformerBlockMetadata を登録してから enable_cache() を 呼ぶ(venvのdiffusers本体は無改変)。リクエストごとに _reset_stateful_cache() + cache_context("h3") で包む(リセット漏れは前リクエストの残差による誤スキップを招く)。 同一seed連続2本のmp4バイト完全一致でリセットの正しさを検証済み。

2段生成による2xアップスケール (/api/t2vaupscale=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=0181s125s6.5s92.1GB768²
upscale=1645s533s (パス1 78s + パス2 455s)24.7s88.0GB1536²
  • 構図・被写体は upscale=0 と一致し、毛並み・芝などの実ディテールが乗る。背景の 細部(フェンス等)はパス2の再デノイズで軽微にドリフトする(hires-fixの性質)。
  • 音声: latentテンソル自体はアップスケール処理で無変更だが、映像と音声は1つの パックドシーケンスで自己注意を共有するため、パス2以降の音声出力は upscale=0 と bit一致しない(相関0.89、非無音・品質同等。アーキテクチャ上の制約で仕様)。
  • VRAM: パス2はシーケンス長~4倍のため、upscale=1 のリクエストではエンコード直後に TE-nf4 を解放してからデノイズする(次リクエストのエンコード時に遅延再ロード)。
  • 実装の要点(実機でバグを踏んで確定): 補間対象はノイズ付きlatentではなくx0推定値 であること(ノイズ付きを補間すると市松状ノイズが増幅されて全面ノイズ化する。ComfyUI 参考実装も denoised_output を使っている)。解像度変更時は build_packed_sequence() で position_ids/token_tags/各indicesを再構築し、row_timestep_plan も残ステップ分を 作り直す。ModularPipeline の _execution_device はコンポーネント登録順の先頭モジュール で決まるため、TE解放後は components.transformer.device を明示的に使う (diffusers-server CLAUDE.md 23番・47番と同型の罠)。

Sage Attention (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)。

  • 実測: デノイズ 118s→104s(-12%)。完全決定論(同一seed 2本バイト一致)。品質は 目視同等(PSNR 21dBは int8-QK 近似による軌道ドリフトで劣化ではない)
  • H3_ATTN_BACKEND=default で従来のSDPAに戻る
  • FBCと独立に併用可: sage + H3_CACHE_THRESHOLD=0.1 でデノイズ67s(-43%、リクエスト ~125s。FBC 0.1の構図ドリフト特性は既知どおり)
  • hub系backend(flash_hub/sage_hub)は torch 2.9 向けビルドがHub側に存在せず不成立 (2026-08-05時点。環境の問題ではない)

ステップ数の指針(蒸留モデル、実測 2026-08-05)

num_inference_steps はAPI/UIパラメータ。30(検証既定)に対し 20で-15%、16で-31% の デノイズ短縮。16/20とも単フレーム品質・時間方向の安定性に破綻なし(ただし構図は ステップ数で変わる)。ドラフト用途は16-20、本番は30が目安。ステップを減らすと FBCのスキップ機会も減る(30stepsで7スキップ→16stepsで0)ため、効果は単純比例しない。

transformer int8量子化 (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両常駐
t2va175-185s / peak 92.1GB177-196s / peak 59.7-91.1GB
ref2va(2回目以降)523s / peak 87.6GB463-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 は従来とバイト一致(回帰確認済み)。

48GB級VRAM対応 (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_QUANTbnb-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_devicecpuに解決されてしまいデノイズの最初の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_encoderNone が同期され、 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.68GBTEロード52s + transformerロード36s + デノイズ108s + デコード6.6s
    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)では採用自体が非現実的なため。

任意サイズ・秒数の丸め (2026-08-06)

UIの解像度セレクトに 「任意 (32の倍数へ丸め)」 を追加し、幅/高さを自由に入力できる。 入力値は エラーにせず H3 の規則へ丸める:

  • キャンバス: 32の倍数へ四捨五入し、256〜2048にクランプ(app.pyround_canvas_value)。H3のブロックは32の倍数でないと ValueError を出す仕様 (MINIMAX_H3_CANVAS_MULTIPLE)なので、以前は端数を送ると400になっていた。 ネイティブ範囲(短辺768・最大768×1344)を超える指定はUIが警告を出す(VRAM・品質は未検証)
  • 秒数→フレーム数: 5〜15秒にクランプ後、17n+5 へ切り上げ(align_num_frames)。 例: 6.3秒 → 158フレーム(6.58秒)。UIは送信前に実フレーム数と実尺をプレビューする
  • API: /api/t2va/api/fl2vaheight/width(任意、指定時は resolution プリセットより優先)を追加。/api/ref2va は元から受け付けるが、こちらも丸めるように変更
  • /api/statusconstraints で丸め規則(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は 何も起きない、という分かりにくい症状になる)。丸める前提の欄には制約属性を付けないこと。

Turbo LoRA (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_offloadcpu_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自動無効化。

turbo × 他機能の組み合わせ検証(2026-08-06)

デフォルトのtransformer経路(transformer_quant=none, lowvram=0)以外との組み合わせ は当初「未検証のため予防的に拒否」だったが、実測でA/B検証した。

  • turbo × upscale(2xアップスケール hires-fix): 動作確認済み、解禁。 768²→1536²・seed12345で8/16stepsとも成功。8steps: 総所要210.3s(denoise 82.6s+ decode 24.4s)、pass1=5/pass2=2steps(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呼び出し」は実害なしと確認)。
  • turbo × transformer int8(H3_TRANSFORMER_QUANT=int8transformer_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生成は正常動作を確認)。
  • turbo × lowvram=1: 実測で不可と確定、拒否のまま維持。 lowvram=1transformer_quant=int8 を強制するため、上記と全く同じ Int8Tensor/aten.cat エラーで失敗(同一エラーメッセージを実機確認)。
  • turbo × lowvram=group: 実測で不可と確定、拒否のまま維持。 同じ理由 (lowvram=grouptransformer_quant=int8 前提)で同一エラー。この失敗は group offloadフックの適用順序とは無関係(sibling project の 「LoRAをenable_group_offload()より前に適用する」という順序修正パターンは ここでは効かない——fuse_projections()自体がgroup offloadフックを一切介さず torch.catだけで失敗するため、順序を入れ替えても直らないと判断し深追いしなかった)。
  • ref2va: 本タスクの検証対象外のまま(元々タスクブリーフのスコープ外)。

16GB級の検証結果: 非対応(床は~18GB、2026-08-06確定)

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未満の量子化が必要(未着手の別課題)。

video VAE の fp16 化 (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の既知問題)。

  • 品質: 全124フレームの平均PSNR 39.97dB(min 39.08)で目視区別不能。デコード計算は 元々autocast fp16のため重みfp16化の影響が小さい
  • 実装の罠: AutoencoderKLMiniMaxH3._keep_in_fp32_modules が encoder/decoder等を 強制fp32に戻すため、from_pretrained(dtype=fp16) は効かない(実機確認)。ロード後に .to(torch.float16) を明示的に呼ぶ必要がある
  • 既定OFFでは既存基準とMD5一致(回帰ゼロ)

24〜32GB級VRAM対応 (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 のような毎リクエスト 再ロードは発生しない)。

PR側の「streamed offload時のload-time量子化」調査結果

タスク時点で読んだ 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())。
  • TEは 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にフォールバックする、上記の壊れる組み合わせを 避けるため)を明示指定すればよい。

choreography最終形(フェーズ×常駐物×ピーク)

フェーズ常駐する大きいもの備考
起動時preloadtransformer(int8, CPU常駐+groupoffloadフック)約90秒(CPUロード70s+pin化22s)
t2va encodeTE-nf4(GPU,21GB) + transformer(CPU)
t2va denoiseTE-nf4(GPU,21GB) + transformerの1-2ブロック(GPU,~1.4GB)
t2va decodevaeペア(GPU,~11GB) + transformerの1-2ブロックTEはこの窓だけ強制解放(下記参照)、decode後に再ロード
ref2va参照エンコードvaeペア(GPU,11GB)TEはこの窓だけ強制解放(下記参照)
ref2va denoiseTE-nf4(GPU,21GB) + transformer_refの1-2ブロック
リクエスト間定常transformer(CPU) + TE-nf4(GPU)ref2va後はtransformer_refが未ロードに戻る(t2va↔ref2va切替のたび再ロード)

【実装中に発見・修正した2つ目のバグ】decode窓・参照エンコード窓でのTE強制解放が必要だった

当初「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へ、の順で安全性を確保)。

MD5一致チェックの結果

同一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制限・24GB制限プローブ

32GB制限(ダミーVRAM確保で空きを~30GBへ制限、H3_CACHE=fbc有効のまま):

完走ピークVRAM所要時間
t2va 1本目(768²・5秒・30steps)28.67GBdenoise 220.79s / decode 6.31s / 総計337.19s(TE初回ロード込み)
t2va 2本目(連続)28.23GBdenoise 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へ制限):

  • 768²: denoise中(transformerブロックのonload)でOOM。実消費21.85GB、うち TE-nf4だけで21GB。TE-nf4自体の固定サイズ(21GB)が22GB予算の大半を占め、 transformerブロック1個分(~148MB)の追加onloadすら入らない
  • 544×960(RESOLUTION_PRESETSへ一時追加して検証、検証後に削除済み): 同一箇所・ 同一21.85GBでOOM。解像度を下げても失敗点も消費量も変わらず、VAEタイル 縮小と同じく「解像度に依存しない固定コストが律速」であることを確認 (下記VAEタイル調査参照)。
  • 結論: 現行アーキテクチャ(TEをbnb-4bit・常時ほぼ常駐という設計)では、 TE-nf4単体の21GBが24GB級カードの実効予算(~22GB)の大半を占めてしまい、 どんな解像度でも成立しない。24GB級に対応するには、TEをdenoise中は解放する (H3_LOWVRAM=1的な設計に戻す)か、TE自体をさらに軽量化する(GGUF等、ただし transformers系モデルへのGGUF適用は構造的に困難)必要がある。本タスクの スコープ外(48GB→24-32GB対応が目的で24GBは探索的プローブ)のため、 現時点では未対応と結論する。

VAE tiling調査結果

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級対応において空間タイルサイズの調整は無意味(既定のままでよい)。

FBC/sage共存の確認結果

全ての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本)が完走した ことも確認済み。

Ref2VAのRAM制約(既知の制限、未解決)

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 -gusedベースで見ても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 搭載機での追試は未実施)。

text_encoder の未使用上位レイヤー削除 (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 は完全無変更(この分岐自体が 一切呼ばれない)。

なぜ「50層」ちょうどではなく「51層」なのか(このタスクで発見・検証したtransformers側の罠)

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_pretrainedconfig が既に 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)のどの組み合わせとも合成可能。

削除後TEの実測サイズ

精度削除前削除後(51層)削減
bnb-4bit nf421.02GB17.45GB-3.57GB (-17%)
bf1666.71GB53.06GB-13.65GB (-20%)

nf4は量子化で1層あたりのサイズがbf16の約1/4に圧縮されるため、削減の絶対量もbf16より 小さい(相対削減率はほぼ同じ)。

MD5一致チェックの結果

同一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=1H3_LOWVRAM=groupの各モードでも、削除の有無で出力が完全一致する ことを確認済み(後述)。

24GB級対応: 削除だけでは不十分だった(実機で発見・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.pygroup_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.72GB321.7s(TE初回ロード込み)
24GB(修正後、2本目・連続)18.68GB277.7s(TE常駐のため短縮)
20GB(修正後)17.72GB320.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.3s21.01GB
削除あり35.0s(-17%)17.44GB

出力mp4は削除の有無でバイト完全一致(md5一致)。

回帰確認

H3_TE_PRUNE未指定(既定0)の状態で同一条件のt2vaを実行し、この機能追加前に 取得していた基準mp4とバイト完全一致(md5一致)することを確認した。既定動作は 完全無変更。

Ref2VA (オムニ参照生成、/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/statusactive_variant)。TE-nf4・VAE類・processorは両変種で共有。
  • VRAM対策(実機OOM 3件を踏んで確定): 参照VAEエンコード完了後に transformer_ref を ロード(逆順は98.5GBでOOM)、デノイズ前にTE-nf4を強制解放(参照行でシーケンスが 伸びるため。hires-fixと同じパターン)。共有text_encoderの解放は両パイプライン シェルの参照を両方消すこと(片方だけではrefcountが残りVRAMが返らない)。
  • 実測(768x768指定→1344×768出力・30steps・seed=12345): 画像1枚参照 523s/87.6GB、 画像+音声(尺は音声由来7.3s) 753s/88.1GB、画像2枚 635s/88.1GB。参照人物の同一性・ 複数参照の合成(人物が参照シーンのカフェに座る)を目視確認済み。ref2va⇔t2vaの 往復切替も正常(切替込みt2va 188s)。
  • UIは「Ref2VA (参照→動画)」タブ(複数ファイル選択、選択順=参照順)。

起動

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 用途は未検証・ 非推奨)

回帰確認プローブ (UIより先に動作確認する場合)

venv/bin/python scripts/probe_t2va.py

outputs/probe_t2va.mp4outputs/probe_report.json (ロード時間・生成時間・ ピークVRAM等)を出力する。

API

  • POST /api/t2va (multipart/form-data): prompt, resolution (768x768|768x1344|1344x768), seconds(5〜15), num_inference_steps, seed
  • POST /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/RAM
  • GET /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 にまとめてある (取り込んだもの / 調査の結果取り込まなかったもの / 着想を得て自前実装したもの、 それぞれの出典・実測値・判定・踏んだ罠)。

UIからの設定切替 (2026-08-06)

環境変数でしか変えられなかったオプトイン設定を、性質で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/applycore/settings.pyapply_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と整合)。

生成済み動画ギャラリー (2026-08-06)

結果表示の下に 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 取得時のみ)。

公式スキル(h3-prompt-writing)モードの実機検証 (2026-08-07)

ローカルLLM(gemma4-31B)接続下で h3-official モードを実測した結果。

構造適合: 生成されたプロンプトは公式仕様に完全準拠していた。

  • T2VA: 3フィールド(integrated_multimodal_description / overall_soundscape / non_diegetic_music)、[Shot 1] は時刻なし、[Shot 2] At 00:05.000 の3桁記法、 台詞は <d>[Japanese] おかえり</d> と原語で保持、話者ID (S1) も付与。応答6.2秒
  • Ref2VA: 6フィールドすべて出力、<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.0005.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> タグ内の台詞は英語/原語のまま維持される。

h3-official の品質保証: 検証 + 修復ループ (2026-08-08)

「LLMの性能限界なのか、プロンプトで直るのか」を推測で議論しないために、まず故障率を 実測し(scripts/probe_h3official_compliance.py、5入力×3回)、その結果に基づいて 対策した。LLM は gemma4-31B Q4_K_M / llama.cpp、n_ctx=7680

ベースライン実測: 失敗は1クラスに集中していた

故障クラス発生率
構造・記法(フィールド名・[Shot n]・タイムコード・<d>タグ・話者ID)0/15 (0%)
時間配分6/15 (40%)
文脈溢れ0/15(余裕 2,900+ トークン)

「入力が変われば別の問題が出るのでは」という懸念は良い方に外れた — 違反は散らばらず 時間配分の1クラスに集中していた。しかもその6件は性質が2つに分かれる:

  • 入力が物理的に不可能 (3/6): 台詞の推定発話時間が尺を超えている。実例は38文字の 独白(推定9.5秒)を9秒尺で要求したケースで、LLMは1ショットに9秒全部を割く最善の 配分をしていた。公式仕様が台詞の逐語保持を要求する以上、短縮という逃げ道も無い → どのモデルでも解決不能。ユーザーに返すべき情報
  • LLMの配分ミス (3/6): 尺には収まるのに配置が悪い → 指摘すれば直せる部類

文脈: t2va は system 4,397 tok / 7,680 で余裕あり。ref2va は 6,191 tok (81%) で余裕 約1,100 トークンしかない(実測では6フィールド揃って正常終了したが、入力が長い場合は要注意)。

対策(3層)

  1. バリデータ (core/prompt_check.py): 決定的に判定できる規則を実装 (F1 3フィールド / F2 先頭ショットに時刻なし / F3 カット時刻の厳密増加 / F4 尺内 / F5 ショット尺の下限 / F6 <d>タグと言語タグ / F7 台詞がショットに収まる / F8 話者ID)。F5・F7 は公式仕様に無い本アプリの実用規則 — 公式は「カット時刻が 尺内」としか言わないため、5秒尺で4.5秒にカットして最終ショット0.5秒、という 字義どおりだが使えない出力を許してしまう(実際に発生)。意味的整合性(1ショット内で 画角が混在する等)は近似ルールで警告にとどめる。手書きプロンプトにも効く
  2. システムプロンプト改善: 英語版wrapperにはガイド本文の後ろに何も無かった (日本語版だけ lang=ja 修正時に後方ブロックを持っていた)。既定は lang=en なので、 尺の制約を含む全指示が15.8KBのガイドに埋もれていた。両版の末尾に時間配分6ルール (ショット尺の下限・台詞の発話時間見積り・ショット数の目安・1ショット1構図・ 画面外音声の定型句・話者ID)を追加
  3. 修復ループ (core/llm.pyenhance_prompt_checked): 違反を検出したら その内容を日本語で突きつけて再生成(最大2回、H3_OFFICIAL_MAX_REPAIRS)。 違反が増える修復案は破棄する(修復が別の箇所を壊す事故の防止)。 入力の実現不可能性はLLMに投げる前に判定し InfeasibleInputError → 400 + 助言

改善後の実測(同一条件)

clean入力不可を検出違反が残存
ベースライン9/1506/15
改善後12/153/15(正しい挙動)0/15

未解決の違反がゼロになった。 全ケースが「妥当なプロンプトを返す」か「実行不能な理由を 助言付きで返す」かのいずれかになる。所要時間の中央値は 8.5s → 9.2s(+8%)で、修復ループは 必要なときだけ発動する(発動時は約2倍の21秒)。

API は /api/prompt/enhance の h3-official 応答に violations / warnings / check_report / attempts / repaired を追加。UI は残った指摘をステータス欄に表示する (生成はブロックしない — プロンプトは編集可能なので人間が最終ゲート)。

量子化済み text_encoder のディスクキャッシュ (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 を変えると重みの中身が 別物になるため、同じ場所を使い回すと設定切替時に古い重みを読む事故が起きる
  • 消費ディスク: 17.44GB(H3_TE_PRUNE=1)/ 未削除なら ~21GB
  • H3_TE_PREQUANT=0 で完全に無効化(導入前と同一挙動)。空きディスクが H3_TE_PREQUANT_MIN_FREE_GB(既定 25GB)を下回る場合は保存をスキップして生成は続行する (キャッシュは高速化であって機能ではない)。保存は一時ディレクトリへ書いてから rename するので、途中で落ちても中途半端なキャッシュが「有効」に見えることはない

測定上の注意(この検証で踏んだ罠): 保存直後に測ると 17.44GB がページキャッシュに 載っているため 2.6秒という非現実的な値が出る。実運用ではリクエスト間に transformer (34GB)をディスクから読むため TE のページキャッシュが押し出され、実測は 21〜34秒に 落ち着く(リクエストを重ねるほど遅くなり 34秒前後で頭打ち)。それでも従来経路の 46〜56秒より速いが、プローブの数値をそのまま性能として報告してはいけない。

text_encoder を別GPUへ常駐 (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.3s67.6〜86.2s(平均 78.4s)
TE を cuda:1 へ88.2s(TE初回ロード込み)33.6〜36.7s(約35s、-55%)

TE のロードはプロセス起動後1回だけ。GPU1 に 17GB 常駐したまま、GPU0 はアイドル時 2.3GB まで下がる。

  • ref2va は自動的に拒否される(20GB級の場合): 2048px 短辺の参照を vision tower に 通す活性化が入らず OOM することを実測済み(19.25GB 使用中に 204MB 不足)。 TE用GPUが 24GB 未満なら理由を添えて 400 を返す — 「動くはず」で走らせて OOM させない。 24GB 以上なら自動的に許可される。t2va/fl2va/t2i とその各バッチは併用可能
  • PCIe 幅は問題にならない: GPU1 は Gen4 x4(x16対応カードだが4レーン接続)だが、 TE は起動時に一度載せるだけで、毎リクエストの転送は prompt_embeds の約42MB のみ(約6ms)

出力は従来構成とビット一致しない(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_meanExpected 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 が最初に見つかるようにする)。

静止画モード (T2I、/api/t2i、2026-08-07 本実装)

H3を「超短尺動画→静止画取り出し」で画像生成の代用にするモード。前段の超短尺プローブ (下記)の結論をそのまま本実装した。価値は「H3と画風が完全に一致する静止画」を FL2VA の先頭フレームや Ref2VA の参照画像として作れること(速度は固定費支配のため 専用T2Iモデルには勝てない)。

  • API: POST /api/t2iprompt / frames(22既定 or 5)/ resolution or height+width / num_inference_steps / seed / 即反映パラメータ (cache/cache_threshold/attn/turbo)。レスポンスは超短尺 mp4 と中央フレームの PNG (image_urlt2i_<ts>.png)の両方
  • UI: 静止画段の「T2I」タブ(秒数・upscale欄は非表示、フレーム数選択のみ)。 ギャラリーにも PNG がタイル表示される(削除可・連結不可)
  • フレーム数: 22 (0.917s) が既定。5 (0.208s) は実験的だが下記の VAE 修正で動く
  • 実装の3点セット(core/runner.py):
    1. generate(still=True, still_frames=...) — diffusers 側の最小尺5秒バリデーション (MINIMAX_H3_MIN_DURATION)は MiniMaxH3SetupStep の呼び出し1回分だけ _relaxed_min_duration() で緩和(生成は generation_lock で直列なので漏れない)
    2. VAE 小クリップデコード修正 (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 本体は無変更)
    3. decode 例外時の steady state 復元: プローブで観測した「decode 例外 → transformer drop 済みのまま復元コード未実行 → ~98.5GB 残留で後続リクエストが 連鎖 OOM」への対策として、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.8s35.0GB157s破綻なし・高品質
5 (0.208s)9.1s0.67s35.2GB125s破綻なし(史上初の実デコード成功)

5フレームの音声 (0.208s) も非無音 (rms 0.055)。

連続生成 (物語の場面画像などプロンプト違いの連番) の実測: group モードは静止画に不利

「常駐モード (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=1157s~157sTE+transformerロード ~120s + デノイズ 29s + デコード 2s
H3_LOWVRAM=group310s (TEコールド込み)~240sデノイズ 130s + デコード 2s + デコード後のTE再ロード 75-97s
  • デノイズ 29s → 130s の悪化が本質: group offload は1ステップごとに int8 transformer 全 ~34GB をブロック単位で PCIe 転送する。転送コストはシーケンス長に 無関係な固定費なので、計算が軽い超短尺 (22フレーム) では転送が完全に支配する (124フレームの通常動画では計算時間に隠れて相対的に軽い、という設計前提が 静止画では崩れる)
  • group モードはデコード窓で TE を force-free して後で再ロードする(24-32GB級の ヘッドルーム設計)。この箱では TE NF4 ロードが実測 75-97s かかる(96GB時代の 15-40s より大幅に遅い)ため、この再ロードも定常コストに乗る
  • 品質は lowvram=1 と同様に良好(目視)。ピークVRAMは 25.1GB
  • 結論: この箱での静止画の連続生成は 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.pygenerate_still_batch())。generate() の lowvram choreography を 位相順に並べ替える:

entry   : [nothing big resident]
encode  : [TE-nf4]         全場面の setup/エンコード/layout/latents/timesteps
denoise : [transformer]    全場面を順にデノイズ
decode  : [vae pair]       全場面を順にデコード → PNG/mp4 保存(場面ごとに保存
                           しながら進むので途中失敗でも完了分は残る)
  • API: POST /api/t2i_batch — multipart で prompts を場面数ぶん繰り返し送る (最大24場面)。frames/resolution/num_inference_steps/seed/即反映パラメータは 全場面共通(変えられるのはプロンプトのみ)。UI は T2I タブの 「バッチ連続生成(1行=1場面)」チェックで、プロンプト欄の1行を1場面として送る
  • 場面間の共有状態リセット(実装の要): スケジューラは sigmas/timesteps の値が 全場面同一(同じ幾何・ステップ数)なので、デノイズ直前に _step_index = None に 戻すだけでよい(MiniMaxH3Scheduler.step() は index を timestep 値から再導出する)。 FirstBlockCache は場面ごとに _reset_stateful_cache() + cache_context (generate() の per-request リセットの per-scene 版)
  • 等価性の実証: 同一プロンプト・seed の逐次 /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~471s157s毎回 ロード~120s + デノイズ29s + デコード2s
/api/t2i_batch (3場面)202.6s67.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のみ)。

参照付き静止画 (ref2i、/api/ref2vastill=1/api/ref2i_batch、2026-08-07 本実装)

下記スパイクの成立を受けて本実装した。キャラクター参照から場面ごとの一貫した 静止画を作るモード。作った静止画は FL2VA の先頭フレームや次の Ref2VA の参照に回せる (物語の複数動画生成の素材作り)。

  • 単発: POST /api/ref2vastill=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 一致まで実証済みの同じ手法
  • UI: 静止画段の「Ref2I」タブ(2026-08-09 に Ref2VA タブ内の「静止画」チェックから 独立タブへ移動。下記「タブの2段組み」参照)+「バッチ連続生成(1行=1場面)」チェック

実測 (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場面とも赤マントを保持し 各プロンプトに追従)。

  • 既知の改善余地: 参照ビジョンエンコードが場面ごとに走る下記 「参照プレフィックスの KV キャッシュ共有」で解決済み (2026-08-08)

参照プレフィックスの KV キャッシュ共有 (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 を読んだ上で実機実測):

  • プレフィックス部分の hidden_states[50] はフル計算とビット一致 (torch.equal)
  • プロンプト末尾部分は相対RMS ~1.5% の丸め差が残る。原因はカーネル/GEMM のタイル経路が 系列長で変わること — eager 固定でも同水準 (sdpa 固有ではない)。ロジックバグでない ことはネガティブコントロールで確定: わざと位置オフセットを壊した継続は相対RMS 27-30% (20倍) に跳ねる。1.5% は「正しい計算の丸めノイズ」の水準
  • 継続呼び出しのレシピ (罠3点): 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.5s494.7s164.9s
共有あり (既定)83.1s350.1s116.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場面とも赤マントを保持しプロンプトに追従)。

スパイク: Ref2VA×超短尺 (参照付き静止画、2026-08-07、実測済み → 上記の通り本実装済み)

「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

条件デノイズデコードピークVRAMwall (逐次・ロード込み)品質(中央フレーム目視)
short22 (0.917s)58.1s1.7s36.7GB265s破綻なし・高品質
baseline (5.0s)243.1s10.6s40.7GB485s破綻なし(アンカー)
  • 判定: 成立 (GO)。分布外の重なり (超短尺×参照パッキング) でも品質は崩れず、 参照の衣装・小物 (赤マント・ランタン・白いドレス)・画風は short22 でも保持された。 構図もプロンプト (苔むした岩に座る) に正しく追従
  • デノイズは t2va の22フレーム (29s) の約2倍 (58s) — 参照行のぶんパック列が伸びる ため。それでも 5 秒基準の 1/4
  • 顔の同一性の厳密さは参照画像の写り (この実験では後ろ姿気味) に依存する。これは 参照の性質であって超短尺の欠陥ではない
  • 本実装 (API/UI 化、generate_still_batch の ref2va 版) は未着手 — t2i と同じ 3点セット (尺ゲート緩和・VAE 小クリップ修正・例外クリーンアップ) は流用可能で、 バッチ位相並べ替えも t2i_batch と同じ手法 (スケジューラ _step_index リセット + per-scene FBC リセット、参照が全場面共通なら参照エンコード1回共有) が使える見込み

前段の超短尺プローブ (2026-08-07、実測記録)

フレーム数は 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.2s6.3s成功
  • 22フレームは学習分布外(公式下限5秒の1/5)でも品質が崩れず、音声も非無音
  • 教訓: プローブ中、5フレームの例外がデコード後のクリーンアップ(transformer再ロード) 前に発生し、~98.5GB残留で後続リクエストがOOMする連鎖を観測 → 上記3のクリーンアップ 経路として本実装に反映済み

2026-08-09: H3_KEEP_TRANSFORMER — transformer 常駐で毎リクエスト再ロード固定費を撤廃

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 収支の理由:

  1. H3_LOWVRAM=1(raw "1" のみ。group は transformer をホストRAM常駐+ブロック offload する別設計のため対象外)
  2. H3_TE_DEVICE 設定済み(TE を別GPUに常駐)— これが無いとエンコード位相が先に 破綻する: TE-nf4 17.45GB + 常駐transformer-int8 34.3GB = 51.75GB が実効予算を 超える
  3. 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、上記起動構成):

  • transformer(int8) は初回リクエストで1回だけロード(32.0s)。以降のリクエストで 再ロードなし(サーバーログで確認)
  • t2i turbo 4steps 定常: 9.7s/枚(denoise 4.32s、decode 1.5s)。peak VRAM 41.97GB (デノイズ時)
  • t2i steps=30 定常: 51.1s(denoise 45.7s)。peak 41.97GB
  • t2va 5秒 turbo 4steps: 44.2s(denoise 26.05s、decode 10.81s)。peak VRAM 44.15GB = デコード位相(transformer 34.03GB 常駐 + fp16 デコード)。導出予測 45.7GB に対し実測 44.15GB、カタログ 48.9GB に対し余裕 ~4.8GB
  • nvidia-smi 実測ピーク 42,620 MiB(1秒サンプリング。瞬間ピークは torch 計測の 44.15GB が正)
  • 同一seed 出力等価性: フラグOFFのベースライン(他は同条件)と PNG MD5 完全一致 (seed=11、md5 665eadddea8f34298a1b5b89e69d4bd0)。ベースライン側は total 63.27s (transformer ロード込み)/ peak 36.4GB

高速化の系譜(48GB GPU0・H3_LOWVRAM=1 系、t2va は 768²・5秒):

t2it2va 5秒
turboなし 30steps(GPU交換直後の素の構成、08-07)157s351.4s(デノイズ197.7s)
lightx2v turbo 4steps 導入(08-07朝)157s143s
+ H3_TE_PREQUANT83.2s
+ H3_TE_DEVICE~35s60.5s
+ H3_KEEP_TRANSFORMER(本節)9.7s44.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 を参照。

2026-08-09: PR #14355 マージ版 (f37ab93) への追従 第1段 — t2i/t2va/バッチ、同一seed MD5 完全一致

ブランチ 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=129.24sPNG 完全一致
t2i 30steps seed=149.65sPNG 完全一致
t2va 5秒 turbo seed=2141.54s、peak 44.15GB(同値)MP4 完全一致
t2i_batch 2場面 seed=1116.47s(8.2s/枚)場面1 = 単発 seed=11 完全一致

数値経路(スケジューラ/transformer/VAE)が無変更という影響調査の結論どおり、 移行はビット等価。性能・VRAM ピークも移行前と同値。

移植で踏んだ新しい罠2つ(いずれも _execution_device の変種):

  1. components 順で audio_vaetransformer より前に移動した(旧: 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 も窓の間外す
  2. バッチの位相並べ替えではレイアウト段の _execution_device が解決不能 (TE 外部常駐だと transformer 未ロード+TE デタッチ済み)→ テンソルが CPU に 生まれる。値は正しいので、デノイズ直前に _scene_state_to_compute() で明示的に 計算GPUへ運ぶ(既に GPU なら no-op)

2026-08-09: マージ版追従 第2段 — ref2va 系も同一seed MD5 完全一致で完了

第1段(上節)の続き。ref2va / ref2i / refバッチを f37ab93 の新契約へ移植し、 旧ピン abc5e9b で採ったベースライン(一時的に main + abc5e9b へ戻して記録)と 同一 seed・同一参照画像・同一構成(H3_LOWVRAM=1 H3_TE_PRUNE=1、TE_DEVICE なし)で比較:

経路 (steps=8)ベースライン移行後MD5
ref2i seed=101206.1s201.4sPNG 完全一致
ref2va 5秒 seed=102332.5s322.5s(定常。初回496.8sはディスクキャッシュ冷え)MP4 完全一致
ref2i_batch 2場面 seed=101(KVプレフィックスキャッシュ経路)257.9s246.7s両場面 PNG 完全一致

設計判断2つ:

  1. pipe シェルは1つに統合。マージ版の MiniMaxH3Blocks は3ワークフロー (t2va/fl2va/ref2va)のサブブロックを合併した component specs を持ち、 transformer_reftransformer の両スロットが同一シェルに存在する (MiniMaxH3AutoDenoiseStep呼び出しごとに state から分岐)。旧2シェル設計の _ensure_pipe_ref_shell / _sync_shared_components_to_refself._pipe_ref = self._pipe のエイリアスだけ残して no-op 化 -- 既存の VRAM 振り付け(_pipe_ref.transformer_ref を 触る多数の箇所)は無改修で生きる
  2. KVプレフィックスキャッシュの分割点は不変。新 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 へ戻す理由は無くなった。

2026-08-09: タブの2段組み(上段=動画・下段=静止画)と Ref2I の独立タブ化

タブが一列に増え続けるのを避けるため、出力の種別で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 に任せる。

UI の日英切替(i18n)

画面右上のトグル(日本語 / English)で再読込なしに切り替わる。選択は localStorage['h3_lang'] に保存し、初回は navigator.language で判定する。

  • 辞書は1つ(I18N = { ja, en }、165キー)。参照は t('key', {vars}) で、 未訳キーは日本語へフォールバックする
  • 静的な文言は data-i18n / data-i18n-html(<b> を含む文言)/ data-i18n-placeholder / data-i18n-title で印を付け、applyI18n() が一括で書き換える
  • JS が生成する文言は属性置換では追従しないので、切替時に rerenderDynamicI18n() が ギャラリー・結果パネル・参照タイル・各種ヒント行を描画し直す(直近の結果は lastResultData に保持して同じ描画関数へ渡す)
  • HTML を日英2ファイルに分ける案は採らなかった。UI は今後も変わるので、 二重編集は必ず片方が腐る

翻訳しないもの: 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)が切替後も不変であること、 コンソールエラーが無いことを確認済み。

2026-08-10: 音声参照でリップシンク(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。

【バグ修正】音声参照つき ref2va が sage attention で必ず落ちていた

上記の検証で発火した。音声を含む参照を渡すと確実にクラッシュする:

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)

原因: MiniMaxH3AudioAttnProcessorbackend=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)の両方で行った。

2026-08-10: 投影TE(Qwen3-VL-4B + 学習済み線形写像)— 実装と実測、未検証項目

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 はアテンションシンク

実測(96GB機、t2i 768²・30steps・seed 4242・同一プロンプト)

32B TE(現行)投影4B TE
PSNR22.64 dB
鮮鋭度(ラプラシアン分散)4837(-23%)
生成時間65.7s43.7s
ピークVRAM88.73GB76.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)で 入れられることを同日に実証済み(上記セクション参照)なので、運用上の回避策がある。

2枚目GPUの要件が下がる

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.7GBt2va のみ成立・ref2va は OOM成立(余裕 ~10.5GB)

12GB は実機確認なしに断言しない。 以前 20GB で「導出上は入るはずが実測 OOM」を 経験しており(単位の罠、§5.1)、余裕1.3GB は薄い。

追記(同日): NF4 量子化オプションと動画での品質確認

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 TE21.02GB65.7s162.1s91.9GB
投影4B bf168.88GB43.7s
投影4B NF43.11GB33.5s143.5s74.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 を追加)。

追記(同日その2): NF4 を既定化、UI からの切り替えに対応

  • H3_TE_PROJ_QUANT の既定を bnb-4bit に変更(品質実測に基づく。none/bnb-8bit は 明示指定で選択可)。既定変更に伴い「量子化指定あり+投影OFF」ガードは明示指定 (in os.environ)のときだけ発火するよう修正 -- 既定値のままの通常ユーザーを 誤って落とさないため
  • UI の再ロード設定パネルから投影TEを切替可能に(/api/settings/applyte_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。

未検証(重要)

  • 48GB機での TE+transformer 同時常駐本命。8.88 + 34(int8) = 42.9GB で載る見込みだが 未実測。これが成立して初めて導入の意味がある(96GB機では元々同時に載るため差が出ない)
  • 参照経路(ref2va) — 投影行列はテキストのみで校正されており、vision 特徴が正しく 写るかは未知。実装は動くようにしてあるが、一度だけ logger.warning を出す → 2026-08-11 に目視検証済み: 参照は明確に効く(顔・髪・服・小物まで反映、32B 正解と 同水準。「2026-08-11: ref2va/i2va はどこまで動くか」の節参照)。warning は校正の事実の 記録として残置
  • 音声参照 + fully_copy との組み合わせ(台詞を音声から入れる運用が投影TEでも成立するか)
  • 4B の TE ロード 80.5s(初回DL込み)。定常のロード時間は未測定

2026-08-10: デコード位相のピークVRAMを分解した — 内訳と、そこから取れた15%

ComfyUI の PR #15446(H3 の VAE を チャンク・ストリーミング化してデコードのVRAMを尺非依存にする)を移植すべきか判断するため、 まず自分たちのデコード位相のピーク 16.29GB が何でできているかを分解した

内訳(768×1344・107フレーム、fp32、実測)

内訳実測比率
VAE 2本の重み(常駐)11.02 GB66%
video decode の活性化3.08 GB19%
postprocess_video0.00 GB0%
audio decode0.00 GB0%
uint8変換 + CPU転送2.49 GB15%
合計16.59 GB(README の実測 16.29GB とほぼ一致)

分かったこと: ピークの2/3は重みであり、チャンク化では1バイトも減らない。 PR #15446 が対象にするのは活性化 3.08GB の部分で、移植しても削減は最大19%

デコードのピークが尺に比例することは別途確認した(潜在32→48フレームで 3.08→4.80GB、 約30MB/フレームの線形)。つまり PR の指摘自体は正しく、長尺ほど効く。

先に取った15% — uint8変換の中間テンソル

分解して初めて気づいたが、私たち自身のコードが 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)することを確認済み。

PR #15446 の移植は保留

活性化 3.08GB(19%)に対し、上流の _decode を丸ごと置き換える monkeypatch が必要で、 追従コストが生じる。より安い 15% を先に取ったので、残りは audio_vae の fp16 化(重み11GBの一部) を先に検討する方が期待値が高い (現在 fp32 固定の根拠は「bf16 で音量が約20dB落ちる」という実測だが、fp16 は未検証。 bf16 は仮数部7ビット、fp16 は10ビットで、小振幅の音声では挙動が異なりうる)。

2026-08-11: 96GB機に RTX 4060 Ti 16GB を増設 — 低VRAMゴールへの第1段(TEを2枚目GPUへ)

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 MD53cd088df882e37547219b9816a217b91新行列+sm_89 なので旧アンカーとは不一致(想定どおり)

見つかって直したバグ2件(Sonnet エージェントの検証で発覚)

  1. plain モード(lowvram なし)+ 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同居の通常経路はバイト単位で不変)。
  2. 投影行列の既定ファイル名が 404。配布元が 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 に絶対パスを渡せば再現可能。

観察: 定常 t2i が単騎比 +10s

デノイズ+デコードは 14.4s で変わらず、リクエスト毎の固定費が約29s(単騎時は約19s)。 差分の主因候補はエンコードが sm_89 の 4060 Ti 上で走ること(NF4 の dequant が遅い)+ cond の PCIe 転送。一方 t2va は 134.6s と悪化しておらず、尺が長いほど固定費は薄まる。 96GB機では TE は元々 GPU0 に載るのでこの箱で2枚目GPUを使う実益はない(本命は 16GB級単体・8GB×2 の構成検証)。

次段(未実測)

  • 第2段 = ゴールA: GPU0 をバラストで 16GB 相当に制限し、H3_LOWVRAM=group + 投影TE 同居(TE_DEVICE なし)。導出 3.11+1.4+6.6 = 11.1GB で載る見込みだが、 te_proj × group の組合せは未走行
  • 第3段 = ゴールB: 8GB×2。GPU1 に TE 3.41GB は載るが、GPU0 の blocks+活性化 8.0GB が 実効予算 7.1GB を 0.9GB 超過 — 解像度/尺で活性化を削る検討が必要

2026-08-11: 第2段 = ゴールA成立 — 実 RTX 4060 Ti 16GB 単体で t2i / t2va が完走

バラスト模擬ではなく、増設した実カードで 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・エラーは一切なし。

時間が重い理由は主に「この箱の2番スロットが PCIe x4」

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 と数値上は壊滅だが、目視では別の話だった:

  • 32B 基準・前段(bf16+sage)の出力: seed 4242 ではアニメ調の夕景の湖 (レターボックス付き)に収束していた。プロンプトの "photorealistic" や "snow-capped peaks" は反映されていない
  • 今回(int8+group+SDPA)の出力: 写実的な雪山と朝霧の湖 — プロンプトに忠実で 画質も高い

つまり int8+SDPA の組合せで生成の軌道が別のアトラクタに移り、結果的に プロンプト適合はむしろ良くなった。構成をまたぐ PSNR/MD5 比較はもともと成立しない (既知の int8 軌道分岐 ~19dB のさらに先)ので、構成間の品質は目視で判断すること。

2026-08-11: 第3段 = ゴールB成立 — 8GB×2 で t2va 5秒 768² がフル解像度のまま完走

両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つ直した: デコード末尾の 838MiB 一括 fp32 化で OOM → 逆正規化を CPU へ

初回試行はデノイズ完走後、デコード末尾の1行で OOM した。上流 decoders.pyMiniMaxH3VideoDecodeStep.__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つとも達成

  • ゴールA(単体16GB): 実 RTX 4060 Ti 16GB でフル機能、ピーク 11.4GB(余裕 ~4.7GB)
  • ゴールB(8GB×2): 計算 8GiB + TE 8GiB で t2i / t2va 5秒 768² 完走(ピーク 7.23GB)

残る注意は速度のみ(この箱は2番スロットが Gen3 x4 のため 16.5〜51s/step。 Gen4 x16 なら転送 ~8分の1)と、FBC が int8+SDPA 軌道で効いていない件(閾値調整は未検証)。

2026-08-11: ref2va/i2va はどこまで動くか — 潜伏バグ2件の発見・修正と、低VRAM境界の確定

ゴールA/B の構成で参照系(ref2i / i2va=画像参照 ref2va / 音声参照 / 768×1344)を検証した。 初回は全滅だったが、原因は VRAM ではなく今日まで一度も併用されなかった組合せで 発火する潜伏バグ2件だった。修正後は境界が VRAM 量で素直に決まるようになった。

バグ1: 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 に丸めてから返す)。

バグ2: group モードでは t2va→ref2va のモード切替が恒久的に不可能だった(pinned RAM 残留)

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×216GB単体(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分
  • 8GiB×2 は ref2i まで。動画は参照トークンの分だけ t2va(7.23GB)より系列が長く、 最短の 768²/5秒ですらデノイズ活性化が入らない(必要量は実測 9.41GB)。
  • 16GB 単体は参照系も全部動く。768×1344/5秒(実ピーク15.2GB)が実質上限。
  • 音声参照の注意: 供給音声が 5 秒未満だと最低尺チェックで 400(仕様どおり)。

品質(投影TEビジョン経路の初の目視検証): 参照は明確に効く

「投影行列はテキストのみで校正」のため vision 品質は未知だったが、目視では劣化を検出できず: 参照人物の顔・前髪・髪の長さ・カーディガンの色・白リボン・ネックレスまで一貫して反映され、 i2va の絵作り(横顔構図)は 32B 正解 test1_walk_park.mp4 とほぼ同じ。音声参照では リップ動作も明確。唯一 768×1344 の終盤に軽い構図ドリフト(破綻ではない)。 生成物: outputs/ref2i_1786447720.png / ref2va_1786449645.mp4 / ref2va_1786452054.mp4 / ref2va_1786455323.mp4

2026-08-12: 96GB機で「全部盛り」を実測 — t2i 7.65s / t2va 28.56s、そこで見つけた2つの無駄

低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.65s28.56s42.5GB
30steps + FBC(t2i 8 / t2va 7 ステップskip)21.41s121.5s41.8GB
30steps・FBC なし27.42s155.0s42.5GB
(参考)96GB 既定・30steps・FBC あり(従来の基準)約160s92GB

内訳(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 級にも載る)。

無駄1: 投影TEを GPU0 に同居させると毎リクエスト解放される

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.23s35.69s
GPU1 常駐(H3_TE_DEVICE=cuda:1)7.65s28.56s

同じ「全部盛り」でも TE の置き場所だけで t2i は2倍違う。なお 96GB機の2枚目は 16GB なので 32B TE(nf4 21GB / 削除版 17.45GB)は載らず、この常駐は投影TEだからこそ成立する

無駄2: bf16 transformer が毎リクエスト 12s かけて解放・再ロードされる

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%速い)。ところが合計では負ける:

transformert2i 定常t2va 5秒ピーク
int8 常駐(H3_KEEP_TRANSFORMER=1)7.65s28.56s42.5GB
bf16 plain モード19.9s40.0s68.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 モード側にも 同種の「解放しない」判断を入れるのが筋。未実装

2026-08-12(その2): デコード窓の解放を止めた — 単騎 45.6GB で t2i 7.4s、48GB級の本命が成立

上の「無駄2」をその場で潰した。変更は import 時ガードの条件1だけ: H3_KEEP_TRANSFORMERH3_LOWVRAM=1 を要求していたが、これを「group でないこと」に 緩め、plain モード(H3_LOWVRAM=0)でもデコード窓の解放をスキップできるようにした。 解放をスキップする分岐(if H3_KEEP_TRANSFORMER: pass)は元から共通で、復元側の _ensure_transformer は冪等なので 追加の実装は不要だった。残り2条件 (TE が GPU0 に居ない = H3_TE_DEVICEH3_TE_PROJ / H3_VIDEO_VAE_FP16=1)は そのまま plain モードの成立条件でもある(66.3 + fp16 デコード 11.4 = 77.7GB)。

全構成の比較(96GB機、turbo 4steps、768²)

構成t2i 定常t2va 5秒ピークGPU
bf16 + TE@GPU1 + 解放停止(案B)6.89s26.8s74.2GB + 3.2GB2枚
bf16 単騎(TE も GPU0)+ 解放停止(案A)7.08s27.04s77.3GB1枚
int8 単騎 + 解放停止(案C)7.40s28.13s45.6GB1枚
int8 + H3_LOWVRAM=1 + KEEP + TE@GPU1(前節の最速)7.65s28.56s42.5GB2枚
int8 単騎・解放あり(案C の KEEP=0)19.58s39.8GB1枚
bf16 + TE@GPU1・解放あり(前節)19.9s40.0s68.9GB2枚

等価性: 案C と「同条件で H3_KEEP_TRANSFORMER=0」の PNG が MD5 完全一致 (596a718e4b5cf9a0b907d2ec479225d2)。解放の停止は数学的に無影響で、同じ絵が **19.58s → 7.40s(2.6倍)**で出る。

分かったこと

  • TE の置き場所はもう効かない。単騎(TE も GPU0)7.08s vs 2枚(TE@GPU1)6.89s の 差は 2.7% しかない。GPU1 の価値は「解放を止めるまで」の話で、止めた後は 1枚で足りる(前節の「TE の置き場所で2倍違う」は解放が残っていたからこその現象)。
  • 効いていたのは解放停止そのもの(同条件で 2.6倍)。
  • 実用最適は案C: int8 単騎・ピーク 45.6GB。48GB 級の実効予算 ~49.8GB に対して 余裕 ~4.2GB で、1枚の 48GB カードで最速級(最速比 7% 落ち)が成立する。 積み残しだった「48GB機での TE+transformer 同時常駐」は、投影TE を 使う形でこれで満たされた(48GB 実機での確認は未実施 — 数値は 96GB 機で測った ピークが 48GB 予算に収まることの確認まで)。
  • bf16 のデノイズは int8 より速い(t2i 2.05-2.07s vs 2.39-2.40s)が、77GB 級の カードが要る。45.6GB で 7% 落ちの int8 とどちらを取るかは持っているカード次第。

2026-08-12(その3): 参照系(ref2i / i2va)の速度 — turbo が効いていなかったバグと、残る固定費

上の最速構成(int8 単騎・解放停止・ピーク45.6GB)で参照系も測った。参照は transformer_ref(別モデル)を使うので収支も律速も t2va とは別物になる。

まず見つかったバグ: 参照系エンドポイントだけ turbo のステップ既定を拾わない

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 に揃えて解消。

実測(96GB機・int8 単騎・turbo・768²)

モードturbo 4steps30steps(turboなし)デノイズ(4/30)ピーク
ref2i(参照付き静止画・22フレーム)79.3s148.4s7.8s / 72.1s45.4GB
i2va(画像参照→動画5秒・124フレーム)103.1s290.3s22.0s / 209.0s45.9GB

ピークは 45.9GB で t2va 系(45.6GB)とほぼ同じ — 参照系も 48GB 級1枚に収まる。 turbo の効きが t2va(5.5倍)より小さい(i2va で 2.8倍)のは、下記のとおり デノイズ以外の固定費が支配的だから。

積み残しだった「ref2va × turbo」は成立(品質を目視確認)

turbo LoRA は transformer でしか実測しておらず transformer_ref は未検証だったが、 4ステップでも参照人物の忠実度は保たれた: 前髪・髪型・薄緑カーディガン・白リボン・ ネックレスまで一致し、動画も先頭〜中央〜末尾で人物が一貫、カメラ追従・公園の情景とも プロンプトどおりで破綻なし(outputs/ref2i_1786509275.png / outputs/ref2va_1786509457.mp4)。strength 0.094 のまま参照条件付きの軌道でも成立する

残っている固定費(i2va 103.1s の内訳、ログのタイムスタンプから)

位相時間備考
参照のビジョンエンコード約47s最大の律速。4B 投影TE でも縮まない(32B 時代の ~65s/場面から改善はしている)
デノイズ(4steps)22.0s
デコード + VAE 往復約10s
参照のVAEエンコード約6s
末尾で t2va 用 transformer を再ロード約13s連続 ref2va では純粋な無駄
  • 47s のビジョンエンコードが参照系の本丸。バッチ経路には既に場面間共有 (H3_REF_PREFIX_CACHE)があるが、単発リクエストの繰り返しでは共有されない (同じ参照画像を使い回す運用ではプロセス跨ぎのキャッシュが効くはず — 未実装)。
  • 13s の再ロードは、ref2va 完了時に「t2va の定常状態」へ戻す設計のため。 次も ref2va なら不要で、H3_KEEP_TRANSFORMER と同種の「戻さない」判断を 入れられる余地がある(未実装)。

見込みの検証(同日、バッチ実測で確認)

「この2つを潰せば i2va 103s → 45s 程度」という見積もりは、バッチ実測で支持された。 バッチ経路は「参照エンコードを共有したらどうなるか」を実際にやっている実験そのもので、 768² に揃えた計測では:

単発バッチ1件あたり実測削減共有モデルの予測
ref2i(3場面)79.3s47.0s32.3s/枚31.3s/枚+1.0s
i2va(2場面)103.1s75.0s28.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倍のピクセル)で生成していたため「動画では共有の利得が出ない」と 誤読して見積もりを撤回した。解像度を揃えたら一致した。経路間の比較では解像度を明示する — 前付けの「バッチ」節の囲みも参照。

2026-08-12(その4): lightx2v turbo LoRA v1.0 スパイク — 768p 版で品質が別次元に

上流 lightx2v/Minimax-h3-Turbo が 本日 v1.0 を公開した(v0.1 に加えて2種)。上流 ModelTC/Minimax-H3-Turbo の Model specs:

モデル学習解像度shift(video/audio)推奨NFEmetadata alpha
4-step v0.1(現既定)544p 混合12 / 34なし(実測 scale 0.094)
8-step v1.0544p 混合12 / 38(4も可)8 → scale 8/128 = 0.0625
4-step v1.0 768p768p(1344×768)6 / 34128 → 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() で上書きできるようにした。

実測(96GB機・int8 単騎・解放停止・投影TE、いずれも定常)

構成t2i 768²t2va 5秒 768²品質(目視、seed 4242 の雪山湖)
v0.1 4step(scale 0.094、現既定)7.40s28.13s岩肌・反射に市松状アーティファクト顕著
8step v1.0 @8steps(scale 0.0625)10.94s49.61s非常にクリーン(光条まで出る)
8step v1.0 @4steps7.47sv0.1 同等〜やや良
768p v1.0 @4steps(scale 1.0 + H3_VIDEO_SHIFT=6)7.59s28.32s8steps 級にクリーン、v0.1 のアーティファクトほぼ消滅
同・1344×768(学習解像度)10.53s特に良好(雲・霧・樹木の質感)

結論: 768p v1.0 が本命。 速度は v0.1 と同じ 4steps のまま、品質だけが 8steps 級に上がる。 アニメ調の t2va(seed 555)も目視で高品質・破綻なし。

既定化はまだしていない(shift の設計判断が要るため)

現状の 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_vaesH3_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 自動切替なしに戻る)。

設計からの逸脱は無し(タスク仕様どおりの実装)。

実機検証(96GB機、`CUDA_VISIBLE_DEVICES=0 H3_TRANSFORMER_QUANT=int8

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 42428.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ビットも変わらない」ことがコード変更をまたいで実証された。

2026-08-12(その5): 「無音で出力」トグル(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 を返す。
  • 速度は変わらない(実測 t2i 8.84s vs 7.71s は通常のばらつき範囲)。音声は生成された上で 捨てられるだけ。速度目的の機能ではない。

実測(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 を使うこと

2026-08-12(その6): 「<d> を使っていないのに声が出る」の原因調査

ユーザー報告「<d> を使わないのに声が生成される」を調べた。原因は2つあり、片方は こちらの不具合、もう片方はモデルの性質だった。あわせて「以前の説明(<d> を使わなければ 声は入らない)は不正確だった」ことを記録しておく — <d>台詞を指定するためのタグで、 声が出るかどうかを制御するものではない

測り方: 声らしさの客観指標

聴かずに判定するため、音声帯域(300-3400Hz)のエンベロープが音節レート(4-8Hz)で どれだけ変調されているかを測る指標を用意した(speech_score = 4-8Hz の変調エネルギー / 0.5-20Hz 全体)。人の声はこの帯域に強い変調を持ち、風・水・環境音は持たない。

生成rmsspeech_score
女性が公園を散歩(LLM強化が**「子供たちの笑い声」を挿入**)0.2070.307
通学路の少女(声の指示は一切なし)0.0330.298
同・プロンプトで明示的に声を禁止0.00710.272
同・LLM修正後の強化文で生成0.01040.269
雪山の湖(人が映っていない)0.3640.125

原因1(こちらの不具合・修正済み): LLM強化が人の声を書き込んでいた

core/llm.py のストーリーボード用 few-shot 例が「商店街の雑踏と呼び込みの声」で、 モデルがこれを真似て「遠くで聞こえる子供たちの笑い声」等を勝手に足していた。H3 は音の記述に 忠実なので、そのとおり声が生成される。共通ルールに「ユーザーが声を求めていない限り、 音の記述に人の声(台詞・話し声・笑い声・歓声・呼び込み・歌声)を入れない」を追加した。 修正後、同じ入力「公園を散歩する女性」の強化結果は風・枯れ葉・鳥の声のみになり、 人の声は消えた。

原因2(モデルの性質・回避不能): 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(無音で出力)を使う — 音声トラックごと落とすので確実

2026-08-12(その7): 台詞(<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> タグを外し、地の文で「〜と挨拶する」のように書く
    → 発話らしい音は出ますが、台詞の内容は保証されません。

さらに: 生成を投げる前に UI で警告する

エラーは生成リクエストを消費してから返るので、プロンプト欄に <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 は時々破るので、指示と後処理の二段で対処した:

  1. 共通ルールに「書き直しは出力前に済ませ、訂正・注釈・言い訳を本文に残さない。 『(100mmに修正)』『※』『←』のようなメタ記述を混ぜてはならない」を追加。
  2. 後処理サニタイザ _sanitize_enhanced() を全4モード(brief/storyboard/translate/ h3-official)の戻り値に適用。(a)「NNmm(MMmmに修正)」を訂正後の値へ畳む、 (b) 単独で残った訂正注釈の括弧を落とす、(c) 許可外の焦点距離を最も近い許可値へ丸める (24mm → 35mm 等)。発火したら logger.info を出す(黙って書き換えない)。

ユニット検証(全ケース期待どおり): 85mm(100mmに修正)100mm(全角・半角括弧とも)、 24mm35mm50mm/35mm/100mm は不変。実機でも同じ入力を3回投げ、焦点距離は 全て許可値・メタ記述の混入ゼロを確認。

2026-08-12(その8): ref2va 後の t2va transformer 復元をやめた(既定を遅延へ)

「結果表示のピーク 49.47GB と、生成ボタン下の GPU alloc 74.21GB がなぜ違うのか」という 疑問から、そもそもその復元は必要なのかを検討した。

まず表示が違う理由(どちらも正しい)

peak_vram_gb(結果表示)はリクエスト先頭の reset_peak_memory_stats() からデコード 直後までの高水位で、ステータス行はリクエスト完了後の現在値。ref2va は入口で t2va 用 transformer を解放し、終了時に積み直していたため、両者は別の瞬間を見ていた:

時刻(実測ログ)イベントGPU alloc
入口transformer freed3.11GB(投影TEのみ)
transformer_ref loaded in 34.8s37.2GB
デノイズ+デコード ← ここが結果表示の 49.47GB
終了直前transformer loaded in 36.9s(復元)74.29GB ← ステータス行はこれ

復元は「無駄 + 遅い + VRAM高水位を上げる」だった

復元はそのリクエストの所要時間に含まれるので、ユーザーは自分の動画と無関係な ロードを待たされる。しかも次も 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 で戻せる。

A/B 実測(96GB機・768²・同一 seed・turbo・連続 ref2va 3本)

eager(旧既定)lazy(新既定)短縮
初回(transformer_ref コールドロード込み)185.92s131.13s-54.8s
定常(2〜3本目の平均)108.4s90.8s-17.6s
生成中ピーク45.67〜45.92GB45.67〜45.92GB同じ
t2va transformer のロード回数 / ref2va 3本4回0回(t2i を投げた時に1回)
リクエスト間の常駐74.28GB3.11GB

連続 ref2va ではリクエスト間の常駐が 74.28GB → 3.11GB になり、48GB 級での運用に 効く。コストが移るだけの ref2va→t2va 切替では、切替後の最初の t2i が 57.17s (2本目以降は 16.18s)と、移った分を素直に払う。

注意: 上の「生成中ピーク」は両者同じで、混在運用の高水位(74.3GB)が消えるわけでは ない。t2va と参照系を同じプロセスで混ぜれば、遅延復元でも t2va 側で両 transformer が 載る。48GB 1枚ならプロセスをモード別に分けるという結論は変わらない。

2026-08-12(その9): 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 として弾く = 同居相手のメモリを奪わない。 指定がカード総容量以上なら警告だけ出して素通しする。

実測(96GB機)

検証結果
上限8GBで 6GB 確保OK
同・20GB 確保OOM で正しく拒否
H3_VRAM_LIMIT_GB=48 + 通常構成(int8単騎・turbo・投影TE)で t2i7.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/statusrunner.vram_limit_gb で確認する。

2026-08-13: 48GB で ref2va 1344×768 を通すには — TE を2枚目GPUへ(実測で確定)

前日の最終テストで踏んだ「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 を逃がすほうが良い。

そのために必要だった修正2件(どちらも「塞がれていて誰も通っていなかった」経路)

  1. _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が不当に弾かれなくなった。
  2. 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 不一致 → 修正、という順で顕在化した)。

2026-08-13: 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.0s11.1s / 28.3s55.83GB
672×384 → 1344×76834.0s4.5s / 8.6s44.67GB

旧実測(96GB機・bf16 + 32B TE・30steps)の 645s / 88.0GB から、時間で7.5倍・VRAM で 32GB 以上の改善。

品質問題の切り分け(同日、ステップ掃引で解決)

ユーザー指摘「動く腕に二重の線」を受けて調査。当初「ステップを増やしても消えない」と 判断したがこれは誤りだった(30steps の対照は turbo なしで、別種の破綻を見ていた)。 同条件でパス2のステップ数だけを振ると:

turbo・総ステップパス2品質(目視・動く部位)時間
4steps(既定)1動く輪郭が激しく二重化・手が崩れる86s
8steps3半透明のゴーストが残る(体越しに背景が透ける)135s
12steps4クリーン(モザイクも二重輪郭もなし)219s
  • sage は無関係: SDPA(attn=default)でも 4steps の二重輪郭は同一。
  • パッキング往復(patchify_unpatchify)は純テンソルプローブでビット一致、 x0 復元・再ノイズ式もマージ版スケジューラの実装と一致 — 移行漏れではなく、 純粋にパス2のステップ不足が原因だった。
  • turbo なし 30steps(パス2=10)の格子モザイクは別問題として残る(bf16+32B 時代の 検証では出ていなかったので int8/投影TE との相互作用を疑うが、未切り分け)。

推奨レシピ: turbo=1 + num_inference_steps=12 + upscale=148GB 級で使える ワイド版も同条件で確認済み:

生成 → 仕上げstepsパス2合計ピーク品質
768² → 1536²124219s55.8GBクリーン
672×384 → 1344×76812496s44.43GBクリーン(4steps 版で出ていた輪郭の甘さも解消)

ただし写実系では upscale は使わないほうがよい(2026-08-13、ユーザー指摘を受けて実測):

1344×768・同一プロンプト(写実)・同一seed時間ピーク髪・建物の高周波成分
直接生成(turbo 4steps)52.4s47.79GB0.2675(基準)
672×384 → upscale(turbo 12steps)89.1s44.69GB0.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.1s59.21GB0.2596
upscale(turbo 12steps)218.9s55.83GB0.2750

目視でもどちらも良好で、直接生成のほうがむしろ構図が素直。つまり upscale の利点は 「ピークが 3〜3.4GB 低い」ことだけで、代わりに1.5〜1.7倍遅く写実系では細部が 25% 落ちる

使いどころは「直接生成では VRAM が足りず、upscale なら収まる」という幅 3GB の隙間だけ (例: 1536² を 56〜58GB 級の予算で出したい)。それ以外は直接生成が上位互換。 UI に出しておく価値はあるが、既定で勧める機能ではない

残っている懸念(この機能を今後さわる人へ)

  1. turbo なし + upscale の格子モザイクが未解決。30steps(パス2=10)で空・電線に市松状の 破綻が出る。bf16 + 32B TE 時代の実測では出ていなかったので int8 量子化または投影TE との 相互作用を疑っているが、切り分けていない。再現は turbo=0 num_inference_steps=30 upscale=1。切り分けるなら「turbo なし・30steps・bf16 transformer」を1本(77GB級が要る)。
  2. turbo とパス2ステップ数が構造的に噛み合わない。turbo は少ステップが前提、upscale の パス2は多ステップを要求する。既定配分(H3_HIRES_DENOISE=0.35)では turbo 既定4ステップで パス2が1ステップしか取れない。12ステップまで上げれば直るが、そのぶん turbo の速度利点が 消える。配分そのものを見直す(パス2に最低ステップ数を保証する)余地はある。
  3. 回帰セットに入っていなかった。この不具合は PR #14355 移行(2026-08-09、第1段=t2i/t2va/ バッチ・第2段=ref2va 系)で upscale が対象外だったために4日間気づかれなかった。 同種の取り残しが他の経路にもある可能性があり、次に diffusers を上げるときは upscale=1 を回帰対象に含めること(docs/internal/regression_baselines.json は upscale ケースを持っていない)。
  4. 品質判定がスタイル依存。アニメ調(平坦色)では細部の欠落がほぼ見えず、写実系で 初めて露見した。この機能の検証は必ず写実プロンプトを含めること。
  5. 等倍で見ないと分からない。縮小表示やサムネイルでは差が出ない。判定は 1:1 クロップ + 高周波成分の比較で行うこと(本節の数値はその方法で取った)。
  6. 最も VRAM が厳しい構成でこそ使えないという逆説。upscale=1 は t2va 専用(fl2va は 明示拒否・ref2va は経路なし)で、低VRAMモード(H3_LOWVRAM=1/group)では 400 拒否。 つまり「VRAM を節約したい」動機が最も強い場面では、そもそも選べない。
  7. 警告がサーバログにしか出ていなかった問題は解消済み(2026-08-13): UI のチェックを入れて いるあいだ、注意書きを画面にも表示するようにした。ラベルの「約3.6倍時間」も bf16/30steps 時代の値だったため削除(現構成では 1.5〜1.7倍)。

UI からは非表示にした(2026-08-13)

上記のとおり直接生成の下位互換なので、チェックボックスを UI から隠した。機能自体は 残してあるupscale=1 を API に直接渡せば従来どおり動くので、「直接生成では VRAM が 足りず upscale なら収まる」幅3GBの隙間には引き続き使える。

表示の可否は static/index.htmlUPSCALE_UI_VISIBLE(既定 false)一箇所で決まる。 markup 側の style="display:none;" だけ外しても updateStillControls() がモード切替のたびに display を上書きするので戻らない — 戻すときはこのフラグを true にすること。 警告は実測ベースの条件付きに変更: パス2 が 4 未満になるステップ数なら具体的な推奨値 (12steps)を示し、turbo なしならモザイクの既知問題を知らせる。

記録(検証の教訓2つ): (1) 修正直後に 1536² の静止した中央フレームだけを見て 「破綻なし」と誤報した — 動画の破綻は動いている部位に出るので、連続フレームを 並べて動く部位を見ること。(2) 「ステップを増やしても消えない」も誤りだった — 30steps の対照は turbo の有無という別の変数も同時に変えていたため、別種の破綻を 同一視した。1変数ずつ振ること(B11 の解像度と同型の教訓)。

48GB 級との関係: 1344×768 なら upscale 経由が最も軽い

1344×768 を得る方法ピーク48GB 級予算 ~49.8GB写実系の細部
t2va を直接 1344×768 で生成47.79GB入る(余裕 ~2GB)良い(基準)
672×384 → upscale で 1344×76844.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)が答え。

なお、今回の用途(48GB で 1344×768 の ref2va)にはそもそも使えない

upscale=1t2va 専用で、fl2va は明示的に ValueErrorref2va にはそもそも 経路が無い(still=1 との併用も拒否)。さらに 低VRAMモード(H3_LOWVRAM=1/group)では 400 で拒否される(パス2の系列長が約4倍になるため未検証扱い)。したがって 「小さく生成して upscale」は参照系の VRAM 対策にはならず、TE を2枚目GPUへ逃がすのが 引き続き正解(前節の実測: 49.73GB → 46.36GB)。

2026-08-13: 全機能の回帰監査 — 21項目、実バグ2件を発見・修正

upscale が「回帰セット外だったため4日間壊れたまま」だった事故を受けて、全エンドポイント・ 全経路の監査を実施した(等価性アンカー2件・実行テスト16件・静的精査3件)。

結果: 19/21 健全。発見した実バグは2件(いずれも設定リロード系、修正済み)

バグ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> の運用がこれで実際に機能する。

環境起因1件(コードのバグではない)

ref2va_batch + TE@GPU1 が OOM — ただし GPU1 には別サービス(ace-server、約10GB)が常駐して おり、純粋な容量不足。_te_external_usable_for() の閾値判定は「TE用GPUの総容量」だけを 見ており、他プロセスの使用量を見ないため、同居環境では判定を通過してから実行時に OOM する。 実測空きを見る判定への変更は設計判断が要るため未実施(参照枚数で必要量が変わり、固定の 床値では1枚参照の成功ケースを誤って弾く)。

監査で確認できた健全性(抜粋)

  • 等価性アンカー2件とも一致: turbo=0 基底経路(f3622b6d)・旧 v0.1 LoRA 後方互換(596a718e)
  • 未検証だった経路が全て PASS: fl2va の last_image のみ/両端指定、複数参照、音声参照+尺自動 導出、mute×バッチ、バッチ×turbo(steps 既定4)、バッチ×外部TE(ガード緩和で初開通の 経路 — ref2i_batch は device 不一致なく完走)、LLM brief/translate、concat
  • 静的精査: hires と同型の「denoiser_input_fields 更新漏れ」は他に無し。死んだ旧 API 参照も無し

回帰基準(regression_baselines.json)の再設計が必要

現行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。基準セットの作り直しは別途(方針判断待ち)。

2026-08-14: この箱向けの回帰基準セット(dev96)と実行ランナー

前日の監査で「既存の回帰基準 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。
  • 採取後にもう一度全実行して 16/16 PASS = 決定論を実証。非決定的ケースはゼロ。 既知アンカー2件(f3622b6d 基底経路 / 596a718e 旧LoRA互換)とも一致し、新旧基準の 連続性を確認。
  • 注意: MD5 は sm_120 + sage + v1.0 768p LoRA に依存(_meta に明記)。runtime は 実行順に依存する参考値で、閾値チェックには使っていない。設定リロード往復は基準対象外 (機能検証は 08-13 の監査で済み)。

使い方: venv/bin/python scripts/run_regression.py(全グループ実行)。

今後の外部イベント待ち(積み残し、2026-08-06時点)

1. diffusers PR #14355 — マージ済み(2026-08-05)、追従も完了(2026-08-09)

この節は追従前に書いた影響調査の記録。実際の追従結果は上の「マージ版 (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 に再編)、 MiniMaxH3AutoKeyframeVaeEncoderStepMiniMaxH3KeyframeVaeEncoderStep / 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 エージェントによる全行監査):

  • 数値経路の土台は無変更: scheduling_minimax_h3.py の差分(36+/41-)は docstring 整形のみでコード変更ゼロ(diff で確認)。transformer は padding-row 処理の削除のみ (QKV 構造・forward シグネチャ無変更)、video VAE の _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 は 全面書き直し
  • ref2va は 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 として実装済み)。

2. Turbo LoRA 完成版のリリース待ち → lightx2v 版で解決(2026-08-08 本実装済み、「Turbo LoRA」節参照)

H3_TURBO_LORA=1 の配線は完成済み(上記セクション参照)。現行LoRA(Ostris氏)は 「デモ/プレビュー、学習途上」と明記されているため既定OFF、かつint8/低VRAMでは 使用不可(融合QKV前提 → Int8Tensoraten.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.projlora_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 が対応値、という仮説が 実測と一致した。

strength4steps の結果音声 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 への適用可否のみ未検証のまま。

3. 未着手の改善候補(優先度順、いずれも急ぎではない)

  • 量子化済みチェックポイントの事前保存TE は実装済み(H3_TE_PREQUANT、 2026-08-08。TEロード 53.0s→29.5s、リクエスト合計 -35%)。残るのは transformer int8 で、保存に約34GB を要しこの箱のディスク空き(43GB)では非現実的。ディスクに余裕の ある機なら、同じ手法で transformer のロード(約32.5s)も削れる見込み
  • TE を2枚目GPUに常駐させる2026-08-09 実装済み(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 での 実測値)。参照付き生成を高速化したくなったら、まずここをスパイクすること
  • 投影TE (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_PROJH3_TE_QUANT/H3_TE_PRUNE/H3_TE_PREQUANT を import 時ガードで排他にしている。これは「32B TE 向けの設定を4Bへ流用させない」 ための措置であって、4B を量子化してはいけないという意味ではない。実装するなら 4B専用の別フラグ(例 H3_TE_PROJ_QUANT)として足し、既存ガードの意図を壊さないこと。 投影行列は fp32 のまま適用する必要がある点にも注意(W は 2560×5120 の fp32)
  • 16GB級対応達成(2026-08-11): 投影TE NF4(3.11GB)+ 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 要件の再計算を参照)
  • torch.compile: 未検証。FBC/group offload の hook との相性(graph break)確認が要る
  • torchao の C++ カーネル: int8モードの dequant コスト(+5s)を解消しうるが torch>=2.11 が必要で、venv全体のリグレッションリスクが大きい(非推奨)

ライセンス

このリポジトリのコードは 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プロンプト強化(2026-08-04追加)

ローカル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}
  • モード(UIの「LLM強化」ボタン+モード選択):
    • 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.pyskills_cache/(.gitignore 済み)へ事前取得しておく必要があり、未取得時は 400+取得コマンド案内のエラーになる。システムプロンプトはSKILL.md+参照ガイド全文を 要約せず投入(約18.8KB=t2va/fl2va、約26.6KB=ref2va)。
    • translate: 過剰創作なしの英訳(CUT構造は保持)
  • 強化結果はプロンプト欄を置き換え(編集可)、「元に戻す」で1世代復元。生成結果には 使用プロンプト全文を折りたたみ表示(強化あり/なし・モード間の比較評価用)
  • UIに手書き用「プロンプトガイド」チートシート(公式ブリーフ構造・CUT記法・実測カット精度±1秒)を同梱
  • 実LLM検証済み(gemma4-31B Q4_K_M): 3モード(brief/storyboard/translate)とも形式に従う ことを確認(storyboardはタイムコード合計・焦点距離制限も遵守、応答11〜17秒)。 h3-official は2026-08-07追加時点でLLMサーバ未接続のため生成A/Bのみ実施(下記参照)、 LLM経由での実応答確認は別途必要

Contributors

yuzuki4251-afk

87 commits

animede

29 commits

Languages

Python

84.3%

HTML

15.5%