技術 約12分で読めます

Radeon 8060S(EVO-X2 / ROCm)でFastWanとWan 14Bを実走 ZLUDAは諦めてTheRockのgfx1151ホイールで動かした

いけさん目次

動画生成のローカル実行を、手元のマシンを変えながら何度か試している。M1 Max 64GBで Wan 2.2が2秒に82分、RTX 4060 Laptop 8GBでは FramePack F1が5秒に56分。どちらも「動くけど遅い」で、詰まる場所はGPUよりメモリ側だった。

今度はEVO-X2でやる。VRAM 48GBは、4060の8GBともM1 Maxの共有64GBとも違う。VRAMが壁になることはまずない。代わりに気になるのは、AMD GPUでPyTorchがそもそも動くのか、という一点だ。前回までの2台はCUDAとMetalで、いずれも「PyTorchが動く」のは前提だった。ROCmはそこから疑う。

テスト環境

項目
マシンGMKtec NucBox EVO-X2
CPUAMD Ryzen AI Max+ 395 (~3000 MHz)
GPUAMD Radeon 8060S (RDNA 3.5 / gfx1151)
メモリ構成64GB UMA(BIOSでVRAM 48GB / System RAM 16GBに分割)
OSWindows 11 Pro 26200
PyTorch2.11.0+rocm7.13.0
diffusers0.38.0
Python3.11.15 (Miniconda)

ここで先に「UMA」を説明しておく。UMA(Unified Memory Architecture)はCPUとGPUが同じ物理メモリを共有する方式で、Strix HaloではBIOSが64GBを切り分ける。今回はVRAMに48GB、システムRAMに16GB。GPU側は潤沢だが、CPU側のRAMは16GBしかない。この非対称が後半までずっと制約になる。

AMD GPUでPyTorchを動かすまで

ここが今回いちばん時間を食った。やったことを先に並べると、CUDA互換レイヤーは全滅で、AMD公式のWindows向けROCmホイールに行き着いた。順番に3回試している。

flowchart TD
    A[PyTorchをROCmで動かしたい] --> B[試行1: ZLUDA + CUDA版PyTorch]
    B --> B1[GPU検出はOK<br/>VRAM 53.9GBも見える]
    B1 --> B2[全演算が named symbol not found<br/>足し算すら通らない]
    B2 --> C[試行2: PyTorch公式ROCmホイール]
    C --> C1[Windowsには配布なし<br/>No matching distribution]
    C1 --> D[試行3: AMD TheRock gfx1151ホイール]
    D --> D1[全演算OK<br/>ZLUDA不要のネイティブROCm]

試行1 ZLUDAでは演算が通らない

最初にZLUDAを試した。ZLUDAはCUDA向けのバイナリをAMDのHIP(ROCmのCUDA相当API)に変換して動かす互換レイヤーで、「CUDA版PyTorchをそのままAMDで」を狙える、はずだった。

GPU検出までは通る。

# zluda_with.exe 経由で実行
CUDA available: True
Device: AMD Radeon(TM) 8060S Graphics [ZLUDA]
VRAM: 53.9 GB

ところが演算に入った瞬間に死ぬ。

RuntimeError: CUDA error: named symbol not found

テンソルの確保(メモリを取るだけ)は通るのに、a + b の足し算すら通らない。HSA_OVERRIDE_GFX_VERSION=11.0.0 でgfx1100にフォールバックさせても変わらなかった。

原因はGPUの世代ではなく、PyTorchの配布形態とZLUDAの相性だった。関わるのはPTXとSASSの違いだ。

表現何かZLUDAとの関係
PTXNVIDIA GPUの中間表現。仮想的な命令セット(バイトコード相当)で、実行時にドライバが各GPU向けに最終コンパイルするZLUDAはこのPTXを読んでHIPに変換する。PTXがあれば動く
SASS / ELF特定のGPUアーキ向けにコンパイル済みのネイティブ機械語ZLUDAは変換できない

公式PyTorchのCUDAホイールには、PTXが入っていない。プリコンパイル済みのSASS/ELFカーネルだけが同梱されている。ZLUDAは変換元のPTXを見つけられないので、カーネルを呼んだ瞬間に「named symbol not found」になる(ZLUDA issue #626)。gfx1151固有の問題ではなく、この組み合わせでは原理的に動かない。

試行2 公式ROCmホイールはWindowsにない

ZLUDAを捨てて、PyTorch公式のROCmホイールを直接入れにいく。

pip install torch --index-url https://download.pytorch.org/whl/rocm6.2
pip install torch --index-url https://download.pytorch.org/whl/rocm6.3
pip install torch --index-url https://download.pytorch.org/whl/rocm6.4
# → 全滅: "No matching distribution found for torch"

PyTorch公式のROCmホイールはLinux専用で、Windows版は配布されていない。バージョンを変えても出てこない。

試行3 AMD TheRockのgfx1151ホイールで動いた

行き着いたのがAMDの TheRock。ROCmとPyTorchをまとめてビルドして配るプロジェクトで、ここがgfx1151向けのWindows PyTorchホイールを公式に出していた。gfx1151(RDNA 3.5 / Strix HaloのiGPUを指すLLVMのターゲット名)は、Windows上で唯一「Release Ready」扱いのコンシューマGPUになっている。

pip install --index-url https://repo.amd.com/rocm/whl/gfx1151/ torch torchvision torchaudio
# → torch-2.11.0+rocm7.13.0
# → rocm-sdk-core-7.13.0 / rocm-sdk-libraries-gfx1151-7.13.0
# hipcc, amdclang++ なども同梱

検証する。

import torch
print(torch.__version__)               # 2.11.0+rocm7.13.0
print(torch.cuda.is_available())       # True
print(torch.cuda.get_device_name(0))   # AMD Radeon(TM) 8060S Graphics
print(torch.cuda.get_device_capability(0))  # (11, 5)

d = torch.device('cuda')
a = torch.tensor([1.0, 2.0, 3.0], device=d)
b = torch.tensor([4.0, 5.0, 6.0], device=d)
print(a + b)  # tensor([5., 7., 9.], device='cuda:0')
# fp32 / fp16 / bf16 全テスト通過

足し算が通り、fp16もbf16も通る。ZLUDAなしのネイティブROCmで、torch.cuda のコードがそのまま動く。ここまで来てやっとスタートラインだ。

FastVideoは使えなかったのでdiffusersを直接叩く

当初の狙いはFastWanで、本来は FastVideo フレームワークを使う。が、これがWindows ROCmでは動かない。

障壁中身
Triton依存fastvideo-kerneltriton>=2.0.0 を要求。TritonはWindows非対応
torch.distributed欠落TheRock版PyTorchに torch.distributed が含まれず、importチェーンの早い段階で torch._C._distributed_c10d が無くて死ぬ

2つめは、1箇所コメントアウトしても次のdistributed importでまた死ぬ、というモグラ叩きになる。フレームワーク全体がWindows ROCmを想定していない。

そこで方針を変える。FastVideo「フレームワーク」を諦めて、diffusersから「モデル」だけを直接呼ぶ。

from diffusers import WanPipeline  # これは普通に動く

ここで前提を一つ補っておく。FastWanの速さの正体はVSA(Video Sparse Attention、全フレーム総当たりをやめて疎に注意を計算する手法)で、これが使えるのはH100 / A100 / 4090だけなのは 前回の記事 で確かめた通り。8060SではVSAは使えない。今回はFastWanの「速さ」ではなく、DMD蒸留(Distribution Matching Distillation。多ステップの拡散モデルを数ステップに蒸留する手法で、3ステップで生成できる)という「重み側の性質」だけを使う。だからパイプラインのクラスを標準のWanPipelineに差し替えても3ステップで回る。

FastWan 1.3B(T2V)を回す

モデルを落とす。

from huggingface_hub import snapshot_download
snapshot_download('FastVideo/FastWan2.1-T2V-1.3B-Diffusers')
# 29ファイル、約28GB(大半はテキストエンコーダUMT5)。ダウンロード約5分

model_index.json を見ると本来のクラスは WanDMDPipeline だが、これはdiffusers 0.38.0には入っていない。標準のWanPipelineで代用する。起動時に警告が出る。

Some weights of the model checkpoint were not used when initializing WanTransformer3DModel:
 ['blocks.*.to_gate_compress.bias', 'blocks.*.to_gate_compress.weight']

DMD固有のto_gate_compress層が標準のWanTransformer3DModelに無いので無視されている。品質への影響は今回は確認していない。

推論パラメータはこう。

パラメータ
モデルFastVideo/FastWan2.1-T2V-1.3B-Diffusers
解像度480x480
フレーム数25(≒1秒 @24fps)
ステップ数3(DMD蒸留)
guidance_scale1.0
精度fp16
AttentionSDPA(PyTorch標準のScaled Dot-Product Attention)

プロンプトは茶髪に赤ネクタイのアニメ調の女の子がピースしてウインク、というもの。出てきた結果。

1秒(25フレーム)が263秒で出た。問題はその内訳だ。

フェーズ時間
モデルロード(CPU)25.2秒
GPU転送19.2秒
DiT 3ステップ合計35.5秒(22.2 / 13.0 / 10.2秒、ウォームアップ後に短縮)
VAEデコード + 後処理~227.7秒
生成合計263.2秒(4分23秒)

拡散の本体であるDiTの3ステップは35.5秒で終わっている。生成時間263秒のうち、実に86%がVAEデコードに費やされている。25フレーム×480×480のラテント(潜在表現)をピクセルに戻すだけのVAE(1.3BのAutoencoderKLWan、Conv3Dベース)に227秒。ここが速度を決めていた。

メモリ側は余裕だった。

指標
VRAM合計53.9 GB
VRAM使用(モデルロード後)15.7 GB
VRAMピーク(生成中)20.9 GB
システムRAM使用15.0 / 15.6 GB(ほぼ限界)

VRAMはピーク20.9GBで33GB余る。一方でシステムRAMは15.0 / 15.6GBでほぼ天井。4060の記事で突き止めた「VRAMでなくRAMが壁」と同じ構図がここにもある。ただしUMAなので、モデルをVRAMに載せたあとはRAM側が解放され、ページファイル溢れには至らなかった。

なお生成中にこの警告が出る。

Flash Efficient attention on Current AMD GPU is still experimental.
Enable it with TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1.

AMD GPU向けのFlash Attention(AOTriton実装)はまだ実験的扱いで、デフォルトではSDPAにフォールバックしている。この環境変数を立てればFlash Attentionが使える。後半のI2Vでこれを使うことになる。

Wan 2.1 I2V 14B でかなちゃんを動かす

T2Vが動いたので、次は本命のI2V(image-to-video、静止画を起点に動かす)。前回FramePackで使った、かなちゃんの正面ピース画像を入力にする。

Wan I2V の入力にしたかなちゃんの正面ピース画像

モデルはWan-AI/Wan2.1-I2V-14B-720P-Diffusers、fp16で約28GB。VRAM 48GBには余裕で載る計算だが、ロードで3回壁にぶつかった。

ロードがSegfaultで落ちる

最初は普通にCPUへロードしようとした。low_cpu_mem_usage=Trueを付けても、トランスフォーマーのシャード3/14(6GBあたり)でSegfaultする。クラッシュ箇所は_local_scalar_dense_cpu。16GBのシステムRAMで28GBのモデルをCPU上に展開しきれず、途中で死ぬ。

次にdevice_map="balanced"でGPUへ直接ロードした。これはCPU RAMを経由せず、シャードを直接GPUに割り当てる指定だ。14シャード全部がGPUに載って46.6GB使用。載りはしたが、残りVRAMが7.3GBしかない。

そして832x480x33フレームで生成しようとすると、今度はattentionでOOM。dense attention(SDPA)が29.38GBのバッファを要求し、7.3GBに入らない。

通すための設定

最終的に480x480x33フレーム(約2秒)を通したときの設定はこう。

import os
os.environ["TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL"] = "1"  # Flash Attention有効化
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"

from diffusers import WanImageToVideoPipeline

pipe = WanImageToVideoPipeline.from_pretrained(
    model_id,
    torch_dtype=torch.float16,
    device_map="balanced",
    max_memory={0: "48GiB", "cpu": "12GiB"},  # 一部をCPU側へ振り分ける
)
pipe.vae.enable_tiling()    # VAEをタイル分割してメモリ削減
pipe.vae.enable_slicing()

これを通すための工夫は次のとおり。

対処効果
device_map="balanced"CPU RAMを経由せずGPU直接ロード。CPU展開時のSegfaultを回避
max_memory でCPUに12GiB確保テキストエンコーダの一部をCPU側へ分散。GPUロード処理のメモリ圧を下げてSegfaultを解消
Flash Attention(AOTriton)有効化attentionバッファが圧縮され、480x480x33fが7.3GBの空きに収まる

特に2つめが決定打だった。device_map="balanced"だけだと、トランスフォーマー(28GB)をGPUに載せた後、テキストエンコーダ(UMT5、~10GB)のsafetensorsロード中に重み127/242番で必ずSegfaultする。max_memoryでCPU側に12GiBの余地を作ると、GPUロード処理のメモリ圧が下がって通った。Flash AttentionをオフにするとSDPAに戻り、832x480x33fのattention行列29GBが空き7.3GBを超えて不可になる。

結果

パラメータ
モデルWan-AI/Wan2.1-I2V-14B-720P-Diffusers
入力画像frame_000.png (608x640)
出力解像度480x480
フレーム数33(≒2秒 @16fps)
ステップ数20
guidance_scale5.0
精度fp16

フェーズ時間
モデルロード(GPU直接)60.8秒
DiT 20ステップ~768秒(38.4秒/ステップ)
VAEデコード + 後処理~47秒
生成合計815.1秒(13.6分)
指標
モデルロード後 VRAM44.6 GB
ピーク VRAM45.4 GB
システムRAM使用6.2 / 15.6 GB

14Bは48GBのVRAMに載る。ただしモデルで44.6GB食うので、推論バッファの余地は9GB前後しかない。Flash Attentionでそこに収めて480x480x33fが通った。

ここでBIOSの固定分割が足かせになる。48GB/16GBに振っているせいでCPU側のRAMが16GBしかなく、enable_model_cpu_offload(モデルをCPUに置いて必要分だけGPUに送る省VRAM手法)が使えない。CPUオフロードはCPU側でモデルを展開する瞬間にSegfaultするからだ。32GB/32GBに振り直せばCPUオフロードでGPU全量を計算に回せるが、それはそれで別の検証になる。T2Vと違ってこちらはVRAMもRAMも両方ぎりぎりだった。

これまでとの比較

前回までの2台と今回を並べるとこうなる。

環境モデルモード解像度フレームステップ時間
EVO-X2 8060S(今回 T2V)FastWan 1.3BT2V480x480253263秒(4.4分)
EVO-X2 8060S(今回 I2V)Wan 14BI2V480x4803320815秒(13.6分)
M1 Max(前々回)Wan 14B GGUFI2V832x48033204965秒(82分)
4060 Laptop(前回)FramePack F1 13BI2V608x640145-3363秒(56分)

同じWan 14B I2Vの33フレームで、M1 Maxの4965秒に対しEVO-X2は815秒。解像度が832x480→480x480と下がっているので単純比較はできないが、桁が一つ違う。CPUオフロード前提でVRAMをやりくりしていたM1 Maxと、48GBにフルロードできるEVO-X2の差が出ている。

参考までに、FastWan公式のVSA有効ベンチはH200で81フレームを5秒、4090で21秒。EVO-X2のDiTのみ35.5秒(3ステップ)は4090の~21秒の1.7倍程度で、GPU性能差を考えれば悪くない。FastWanで遅いのはDiTではなくVAEデコードの方だった。

何が速度を決めているか

3台を通して、ボトルネックはモデルやGPUコアの速さではなく、その手前にあった。

環境ボトルネック
406026GBのモデルが32GB RAMに収まらずページファイルへ溢れ、DynamicSwapが毎ステップディスクから読み直す。GPU使用率が一桁のまま
M1 Max64GB共有メモリでCPUオフロードしながら回るので、転送のオーバーヘッドが加わる
EVO-X2VRAM 48GBにフルロードできてGPUは普通に回る。代わりにFastWanではVAEデコード(Conv3DがROCmで遅い)が時間の86%を占める

EVO-X2は「モデルがVRAMに丸ごと載る」という点で前の2台より明確に有利だった。VRAM 48GBの余裕は大きい。一方で、AMD GPUのVAEデコードとattentionまわりの最適化はまだ発展途上で、SDPAは実験的、Flash Attentionも実験フラグ前提。動くようになった、というのが今の段階だ。

そしてUMAの固定分割が両刃になる。VRAMに48GB振れば大きいモデルが載るが、CPU側が16GBまで減ってSegfaultとCPUオフロード不可を招く。結局、ワークロードごとにBIOSで割り当てを変えるしかない。

参考

  • AMD TheRock — ROCm + PyTorchのビルド・配布プロジェクト
  • gfx1151 PyTorchホイール: pip install --index-url https://repo.amd.com/rocm/whl/gfx1151/ torch
  • FastVideo / FastWan — FastWanの本家フレームワーク
  • ZLUDA issue #626 — 公式PyTorchホイールのPTX非互換