生成 AI のサービスには画像や効果音、音楽を作る物がありますが、同じことが手元の GPU でもできるかを、余っていた Radeon RX 7900 XT で技術検証しました。その中で、生成の最初の1回だけが数分止まったように見えることがあり、私たちも一度この遅さをハングと誤診しました。生成のジョブに時間の上限を置くときも、どの値を基準にするかを誤ると、正常なジョブを止めてしまいます。

結論

  • 最初の1回が遅いのは、hipBLASLt と MIOpen がカーネルを自動調整しているためです。GPU が動いていればハングではありません。
  • 調整の結果は利用者ごとにキャッシュされ、解像度を変えるとやり直しになります。使う解像度は固定します。
  • MOSS-SoundEffect v2.0 は、キャッシュがあってもプロセスごとに約 90 秒かかります。一組の効果音は1つのプロセスでまとめて作ります。
  • ジョブの時間の上限は、冷たいキャッシュ(初めての利用者・初めての解像度)の値から作ります。温かい値から作った 300 秒の上限に、初回の 283.1 秒が迫りました。

試した環境

  • GPU: AMD Radeon RX 7900 XT(gfx1100、VRAM 20 GiB)
  • OS と ROCm: Ubuntu、ROCm 7.2.4
  • ライブラリ: torch 2.13.0+rocm7.2
  • モデル: FLUX.2-klein-4B(画像)、MOSS-SoundEffect v2.0(効果音)、ACE-Step 1.5 turbo(音楽)、Qwen-Image-Edit-2511(画像編集)、Qwen3-VL-8B(画像を見る)
  • 生成は、検証の中で組んだ、ジョブを並べて GPU で1本ずつ回す仕組みから、サービス用の利用者で走らせました

初回と2回目以降はこれだけ違う

FLUX.2-klein-4B で画像を作ったときの所要です。「新しい解像度の初回」のほかは 1024² です。

FLUX.2-klein-4B の所要。キャッシュが空の初回 274.7 秒、新しい解像度の初回 220.5 秒、後のプロセスの初回 29.3 秒、2回目以降 10.6〜16.7 秒

キャッシュが空の最初の1回は 274.7 秒、2回目以降は 10.6〜16.7 秒でした。いちど調整が済めば、後で別のプロセスを起こしても、同じ解像度なら初回は 29.3 秒です。ところが解像度を変えると、その解像度で調整がやり直しになり、220.5 秒かかりました。

ほかのモデルでも、初回だけが長くなります。

モデル初回2回目以降
MOSS-SoundEffect v2.0125.7 秒(プロセスの中の初回)35.2 秒
ACE-Step 1.5 turbo123.9〜143.9 秒45 秒の曲を 37.9 秒
Qwen-Image-Edit-25111枚目 197 秒(8 手)/133 秒(4 手)Lightning 8 手で 140〜145 秒/枚

GPU が動いていれば、ハングではありません。ただ GPU の使用率だけでは見分けにくいので、後の節の3つの指標を合わせて見ます。1 step に何十秒もかかり、GPU の使用率が 0% のまま時間だけ経つなら、自動調整ではなく別の原因を疑います(MOSS-SoundEffect では torch.compile が重く、1 step に 70 秒かかりました)。

ふたの開いた機械が静かに動き、そばで人がお茶を飲みながら待っている絵

キャッシュは利用者ごとに溜まる

私たちの環境で見た、キャッシュの置き場です。どれも利用者の HOME の下にあります。

中身置き場
自動調整の結果(find-db)~/.config/miopen
コンパイル済みのカーネル~/.cache/miopen
comgr のキャッシュ~/.cache/comgr

そのため、生成を走らせる利用者を新しく作ると、また最初から払います。次のグラフは、サービス用の利用者を新しく作ったあと、各モデルを1回ずつ流したときの値です(ジョブの開始から終了まで)。

冷たいキャッシュでの所要。画像 1024² が 283.1 秒、続けて 512² が 50.2 秒、効果音 2 秒が 340.6 秒、音楽 turbo 10 秒が 159.3 秒、VL 1 枚が 44.1 秒

画像 1024² の 283.1 秒のうち、描画が 274.9 秒でした。別の利用者で初めて回したときも、FLUX.2-klein-4B の 512² が 52.5 秒、MOSS-SoundEffect の 2 秒・100 手が 316.7 秒かかっています。

サービス用の利用者の HOME はモデル用のディスクに作り、キャッシュがそこに溜まるようにしました。利用者のあいだでキャッシュを共有する案(MIOPEN_USER_DB_PATH・MIOPEN_CUSTOM_CACHE_DIR・AMD_COMGR_CACHE_DIR を共有の置き場へ向ける)も考えましたが、やめました。得をするのは人が手で生成を打つときだけで、acl を入れて既定 ACL の置き場を作る手間に見合わないと判断しました。

一組はまとめて作る

MOSS-SoundEffect は、2回目以降が 35.2 秒でも、プロセスを起こすたびに約 90 秒(MIOpen の自動調整)を払います。キャッシュがあっても消えないので、一組の効果音は1つのプロセスでまとめて作ります。

画像でも、1枚ずつプロセスを起こすのは損でした。画像のモデルを1枚ずつ呼んだ別の計測では、1枚 13〜24 秒のうち描画は 3〜4 秒で、残りは重みの積み直しでした。一組の画像は、重みを1回だけ読んでまとめて作ります。

時間の上限は冷たい値から作る

検証で組んだジョブの仕組みには、ハングしたジョブを止めるための時間の上限を持たせました。最初は、この上限を温かい値から作っていました。同じ解像度で後のプロセスの初回にかかる 29.3 秒をもとにした、300 秒です。

新しいサービス用の利用者で最初に画像 1024² を流すと 283.1 秒かかり、上限まで 17 秒しか残りませんでした。あと少し遅ければ、正常なジョブを止めるところでした。そこで初回の費用を、冷たいキャッシュ・新しい解像度で測った値に置き直し、下限も付けました。

上限 = 初回の費用 + 1件の目安 × 件数 × 3(ただし 15 分より短くしない)
初回の費用 = 冷たいキャッシュ・新しい解像度で測った値

上限を超えたら、取り消しと同じ手順(SIGTERM を送り、15 秒待って SIGKILL)で止めます。上限はハングに備えた安全弁なので、広めに取ります。一方、完了までの目安(ETA)は、温かい値のままにしました。

止まったかどうかは3つの指標で見る

初回は数分止まって見えるので、1つの指標だけでは見分けにくくなります。そこで3つを合わせて見ます。

止まったと見なす条件

GPU の使用率は amdgpu の gpu_busy_percent、CPU 時間は /proc/<pid>/stat の utime と stime の和で取り、3つがそろった状態が 5 分続いたら警告を出すようにしました。警告だけにして、止めはしませんでした。重みを読み込んでいるあいだは、窓を初回の費用の 1.5 倍に広げました。

進捗を標準出力から拾うときに、1つ引っかかりました。TQDM_DISABLE=1 を渡しても diffusers の進捗バーは止まらず、\r で同じ行を書き換え続けます。ログを読む側は、\r も行の区切りとして扱う必要があります。

確かめていないこと

  • キャッシュを利用者のあいだで共有したら、初回がどれだけ縮むか。hipBLASLt の自動調整は上の環境変数の対象外で、共有しても残る費用は切り分けていません。
  • MOSS-SoundEffect が、キャッシュがあってもプロセスごとに約 90 秒払う理由。
  • 新しいサービス用の利用者で、温まった後に2回目を測った値。温かい値は、以前に測った物を使っています。

おわりに

ROCm で最初の1回が止まって見えたら、GPU が動いているかを見てから待ってください。時間の上限を置くなら、初めての利用者・初めての解像度で一度測った値を基準にすると、正常なジョブを止めずに済みます。

※ ROCm・MIOpen・diffusers の振る舞いは執筆時点の版(ROCm 7.2.4、torch 2.13.0+rocm7.2)で見た物で、いまは変わっている可能性があります。