GMKTec EVO-X2(Ryzen AI Max+ 395)で Qwen-Image-2.1 を約2倍速化した記録【ComfyUI+ROCm FP8常駐】
背景
- Qwen-Image-2.1:AlibabaのQwenチームが2026年9月に公開した7Bのオープンウェイト画像生成モデル。テキストからの生成と参照画像を使った編集を1モデルで扱える。VRAM 13〜15GB程度で動くため、ミドルレンジGPUでもローカル実行できる
- GMKTec EVO-X2:Ryzen AI Max+ 395(Strix Halo)搭載のミニPC。CPUとGPU(Radeon 8060S)が128GBのLPDDR5x統合メモリを共有しており、大容量モデルを丸ごと載せられるのが強み。一方、デディケイティブGPUに比べて演算性能は控えめで、ROCmのソフトウェア環境もまだまだ整いきっていない
- 用語:DiT=画像生成の本体(Diffusion Transformer)、VAE=生成後の潜在表現を画像に戻す部品、AOTriton=AMD GPU向けの高速attention実装、GGUF=llama.cpp系の量子化ファイル形式
環境
| 項目 | 内容 |
|---|---|
| 機種 | GMKTec EVO-X2 |
| CPU | Ryzen AI Max+ 395(16C32T) |
| GPU | Radeon 8060S(gfx1151・RDNA3.5 iGPU) |
| Memory | 128GB LPDDR5x統合メモリ(CPUとGPUが共有) |
| OS | Ubuntu 26.04.1 LTS |
| 計測条件 | 1280x720・20steps |
課題:1枚5分は遅いのか?
当初の構成は stable-diffusion.cpp(Vulkanバックエンド・GGUF Q8量子化重み)+HTTP互換レイヤーで、1280x720・20stepsで1枚あたり約5分かかった。
なぜVulkan構成だったのか
結論から言うと、Vulkanを選んだのは好みではなく消去法だった。当時のStrix Halo+ROCmはGPUのハングアップ、VAE decode中の停止、96GB割当時のオーバーフローなど不安定な点が多く、安定して使い続けられる状態ではなかった(同機種での苦労記録)。一方、stable-diffusion.cppはROCmを経由せずMesaのRADVドライバからGPUを直接使うため、遅い代わりに安定して完走できた。
diffusers+bf16フル精度の常駐(約31GB)も試したが、メモリ不足と速度の両面で実用にならず、CPUオフロード前提のVulkan+GGUFに落ち着いた。つまり5分という時間は、性能ではなく安定性を優先した結果だった。
5分の内訳(概算)
| 工程 | 目安 |
|---|---|
| text encoder(Qwen3-VL 8B・Q4)の読み込み+推論 | CPU寄りで数十秒 |
| DiT 7B・20steps(fused attentionなし) | 約15秒/step × 20 ≒ 5分の大半 |
| VAE decode(tilingなし) | 単発で数十秒 |
支配的なのはdiffusionのstep単価で、1 step約15秒は「DiT 7Bを最適化されていないカーネルで回した」値そのものだ。NVIDIA勢の秒単位と比べるとずいぶん遅く感じるが、CPUオフロード+最適化なしカーネル+VAE tilingなしの条件なら、Strix Halo・ROCm界隈の「分単位が普通」という報告とも矛盾しない。
ネットの目安を調べると、NVIDIA勢はまったく別世界に見えた。
- RTX 4090:1MP画像で約5秒(CellCogの起動日レポート内のHacker News報告)
- RTX 4060 Ti 16GB(INT8):生成約20秒・編集約60秒(Hugging Face Discussion #35)
- RTX 4070:合計20〜30秒(Spectrum高速化の実測)
- DGX Spark(GB10):Qwen旧20B系(2512/2509)の20stepsで約172秒(DGX Spark実測)、4090の約3分の1の速度(diffusionベンチマーク)。現行2.1(7B)とは別モデルのため直接比較はできないが、統合メモリ機どうしの目安としては使える
一方、Strix Halo側の報告は「分単位が普通」というものだった。ただし高速化に成功した事例もあった:
- kyuz0氏のツールボックス:VAE tilingで3分の1、AOTritonでQwenが約3倍速に(Frameworkスレ・ベンチマーク集)
- AMD公式ブログ(ROCm 7.2.1・Windowsネイティブ):SDXL 1024pxが18.4秒、Flux.1-dev(完全精度)が77.6秒(ROCm Blog)
- Qwen 2512を自作アプリ+Q8量子化で34〜35秒/枚まで縮めた報告(r/StrixHalo)
まとめると、5分は故障ではない(この構成では当たり前)が、工夫すれば縮む。方針はこれに決めた。
方針:ROCm FP8 ComfyUI常駐
ここまでで原因は特定済みで、直す点は次の3つだった。
- Vulkan+GGUFはGPUメモリを使わず(実測VRAM 0MB)、演算のたびに量子化解除とCPU側へのフォールバックが走る
- 高速なattention(AOTriton)が使われない
- VAE decodeにtiling(分割処理)が効いていない
そこでPyTorch(ROCm版)上でComfyUIを常駐させ、FP8量子化した重みをGPUメモリに載せたままにしておく方針にした。
実施内容
1. ComfyUI用venv(torchは再利用)
別途、torch(ROCm版)を入れ直すと数GBのダウンロードに加えてバージョンの組み合わせが不安定になる。そこで動作実績のある qwen21-server/.venv(torch 2.12+ROCm 7.13、gfx1151で動作確認済み)を .pth ファイル経由で再利用した。
uv venv -p ~/qwen21-server/.venv/bin/python --seed ~/ComfyUI/.venv
SITE_Q=$(~/qwen21-server/.venv/bin/python -c 'import site; print(site.getsitepackages()[0])')
SITE_C=$(~/ComfyUI/.venv/bin/python -c 'import site; print(site.getsitepackages()[0])')
echo "$SITE_Q" > "$SITE_C/qwen-rocm-torch.pth"
~/ComfyUI/.venv/bin/python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
# reuse torch 2.12.0a0+rocm7.13 True
2. ComfyUI依存の導入(CUDA版torch混入防止)
ComfyUIの requirements.txt をそのまま入れると、PyPIのCUDA版torchに上書きされてしまう。torch / torchvision / torchaudio の3つを除外し、torchsde は依存解決を止める --no-deps で入れる。
grep -v -E "^(torch|torchvision|torchaudio|torchsde)[>=< ]" ~/ComfyUI/requirements.txt > /tmp/comfy-req-no-torch.txt
~/ComfyUI/.venv/bin/pip install -r /tmp/comfy-req-no-torch.txt
~/ComfyUI/.venv/bin/pip install --no-deps torchsde
3. モデル3点(DiT / text encoder / VAE)の配置
使うファイルは DiTにComfy-Org公式配布の int8_convrot 版(後述の「つまずき」2件から得た教訓)、text encoderとVAEはダウンロード済みのキャッシュをsymlinkした。配布元は Comfy-Org/Qwen-Image-2.1 と unsloth/Qwen-Image-2.1-FP8。
ln -sfn <hub>/models--Comfy-Org--Qwen-Image-2.1/.../diffusion_models/qwen_image_2.1_int8_convrot.safetensors \
~/ComfyUI/models/diffusion_models/
ln -sfn <hub>/models--unsloth--Qwen-Image-2.1-FP8/.../Qwen-Image-2.1-text_encoder-FP8.safetensors \
~/ComfyUI/models/text_encoders/
ln -sfn <hub>/models--Comfy-Org--Qwen-Image-2.1/.../vae/qwen_image_2.1_vae_bf16.safetensors \
~/ComfyUI/models/vae/
text encoderは16.7GBあり、通常なら分割ロードを検討する規模だが、統合メモリのおかげで full load: True のまま常駐できた。
4. 起動オプション
env PYTORCH_HIP_ALLOC_CONF=expandable_segments:True \
TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 \
MIOPEN_FIND_MODE=FAST MIOPEN_FIND_ENABLE_DYNAMIC=0 \
~/ComfyUI/.venv/bin/python ~/ComfyUI/main.py \
--listen 127.0.0.1 --port 8188 \
--disable-mmap --gpu-only --disable-smart-memory --cache-none --bf16-vae
5. ComfyUI連携(/prompt API)
自作スクリプトからComfyUIの /prompt API へworkflow JSONを投げる。構成は以下の通りで、新規生成と参照編集ではtext encodeノードだけが変わる:
-
UNETLoader(weight_dtype=default)→CLIPLoader(type=qwen_image)→VAELoader - 新規生成:
CLIPTextEncode/参照編集:TextEncodeQwenImageEdit(+LoadImage) -
EmptyLatentImage→KSampler(euler・simple・cfg 4.0・20steps)→VAEDecodeTiled→SaveImage
参照編集では参照画像を /upload/image でComfyUIへ送り、LoadImage 経由で TextEncodeQwenImageEdit に渡す。参照画像の構図・画風を引き継いだ生成になる。
つまずき3件
1. VAEDecodeTiledにtemporal系が必須
このComfyUI版では temporal_size・temporal_overlap が必須入力。未指定だと400が返る。
{"class_type": "VAEDecodeTiled",
"inputs": {"samples": ["8", 0], "vae": ["3", 0],
"tile_size": 512, "overlap": 64,
"temporal_size": 64, "temporal_overlap": 8}}
2. unsloth単体FP8はUNETLoaderで検出不可
Could not detect model type で失敗。unslothの単体ファイルはdiffusers形式のままで、ComfyUIが読める形式(ComfyUI向けに量子化・変換されたconvrot形式)ではない。Comfy-Orgのsplit重み qwen_image_2.1_int8_convrot.safetensors を取得して解決した。
3. TextEncodeQwenImageEditの入力名はprompt
text ではなく prompt が正しい(新schema)。required_input_missing: prompt が出たらここを疑う。
結果
| 条件 | 時間 | 1 stepあたり |
|---|---|---|
| 従来Vulkan(GGUF Q8) | 約300秒/枚 | 約15秒 |
| ComfyUI FP8・新規 | 124〜129秒/枚 | 約6秒 |
| ComfyUI FP8・参照edit | 約158秒/枚 | 約8秒 |
約2倍から2.4倍に短縮された。テスト用プロンプトで描画品質を確認し問題はなく、参照編集でも参照画像の構図・画風を引き継げることを目視で確認した。
考察
- メモリ帯域はDGX Spark(273GB/s)とStrix Halo(理論値256GB/s)でほぼ互角。実際に差が出るのは演算ユニットとCUDA環境の成熟度で、同じ条件ならDGX Sparkの方がさらに2〜5倍速い。Strix Haloの強みは速度ではなく容量(128GBの統合メモリに大きいモデルを載せられること)にある
- 今回の約2倍は「最適化の効いた上限」に達したわけではなく、誤った経路を正しく直した分の伸びにすぎない。残る余地としてはsteps削減(Lightning系4-step LoRAの併用)が最大
- ROCm環境の再現性が最大の課題だった。動くtorch一式を
.pthで使い回す方式が最も安定した
再現手順まとめ
手順スクリプトは本作業用に自作したもので、内容は次の4つに分けられる:
- ComfyUI用venvの作成(依存+モデルリンク)を
setup_comfyui_qwen.shにまとめる - 起動オプション付きでComfyUIを常駐させる(
start-comfyui-qwen.sh) -
/object_info/UNETLoaderにDiT名が出ることを確認する - 上記workflow JSONを
/promptに投げ、/history/{prompt_id}の完了を待って/viewで取得する(完了待ちポーリングと取得は定型処理なので、共通化しておくと楽)
以上です。EVO-X2でQwen-Image-2.1を回している方の参考になれば幸いです。