AMD Ryzen AI MAX+ 395に内蔵された Radeon 8060S(gfx1151) で、Qwen-Image-2.1のローカル画像生成環境を構築しました。
Pythonの仮想環境にROCm版PyTorchとDiffusersを導入し、公式モデルをダウンロードして、512×512・20ステップの画像を約61秒で生成できました。今回は、作成したスクリプトと、UTF-8のテキストファイルからプロンプトを入力する方法をまとめます。
本記事は2026年9月26日の実機検証記録です。ROCmの導入とGPU認識が済んでいる環境から始めます。
今回作成したスクリプトと使い方は、以下のGitHubリポジトリで公開しています。
使用した環境
| 項目 | 構成 |
|---|---|
| CPU | AMD Ryzen AI MAX+ 395 |
| GPU | AMD Radeon 8060S Graphics / gfx1151 |
| GPU用メモリ | 48 GiB |
| OS | Ubuntu 26.04.1 LTS |
| カーネル | 7.0.0-34-generic |
| ROCm | 10.0(システムパッケージは10.0.0-4) |
| Python | 3.14.4 |
| PyTorch | 2.13.0+rocm10.0.0 |
| torchvision | 0.28.0+rocm10.0.0 |
| Diffusers | 0.41.0.dev0(GitHub版、コミット固定) |
| Transformers | 5.17.0 |
| Accelerate | 1.15.0 |
| Pillow | 12.3.0 |
8060SはAPUの内蔵GPUです。ここでいう48 GiBはGPU用として認識されたメモリで、独立したグラフィックスボードのVRAMではありません。OSから見えるメインメモリは約45 GiBでした。
システム側はrocminfo、Python側はtorch.version.hipと実際のGPU演算で確認しました。今回のtorch.version.hipは7.15.26333です。ROCm全体のリリース番号とHIPコンポーネントの番号は分けて記録しています。
作成したもの
作業ディレクトリは~/qwen-image-2.1です。
qwen-image-2.1/
├── .venv/ # Python実行環境
├── models/
│ └── Qwen-Image-2.1/ # 公式モデルの重みと設定
├── outputs/
│ ├── first-image.png # 初回生成画像
│ └── first-image.json # 設定・時間・メモリの記録
├── generate.py # 画像生成CLI
├── check_gpu.py # GPU演算とimportの簡易確認
├── requirements.lock.txt # インストール済みパッケージ一覧
├── README.md # 実行方法
└── PROGRESS.md # 導入・検証の記録
モデルの重みはPython環境とは別に置きます。今回の基本的な画像生成には、QwenのGitHubリポジトリのcloneやllama.cppのビルドは不要でした。
1. 仮想環境とROCm版PyTorchを用意する
以下ではuvとPython 3.14が導入済みであることを前提にします。
mkdir -p ~/qwen-image-2.1
cd ~/qwen-image-2.1
uv venv --python /usr/bin/python3.14 .venv
uv pip install --python .venv/bin/python \
--index-url https://stable.repo.amd.com/rocm/whl-next/ \
'torch[device-gfx1151]==2.13.0+rocm10.0.0' \
'torchvision[device-gfx1151]==0.28.0+rocm10.0.0'
device-gfx1151を指定して、対象GPU向けのパッケージを導入します。配布先とバージョンの組み合わせはAMD公式のPyTorch導入手順を確認しました。
続いて、画像生成に必要なライブラリを入れます。Diffusersは検証に使用したコミットに固定します。
uv pip install --python .venv/bin/python \
'torch==2.13.0+rocm10.0.0' \
'transformers==5.17.0' \
'accelerate==1.15.0' \
'pillow==12.3.0' \
'diffusers @ git+https://github.com/huggingface/diffusers.git@e0abab83b5df05de9e7abd788643c1a7c1e42e28'
uv pip check --python .venv/bin/python
uv pip freeze --python .venv/bin/python > requirements.lock.txt
実機では53パッケージの依存関係チェックに合格しました。requirements.lock.txtは導入済みバージョンの記録であり、AMDの配布先設定やハッシュまで含む完全なロックファイルではありません。再導入時もROCm関連パッケージにはAMDのindexが必要です。
torchvisionも必要だった
最初はQwenImage21Pipelineのimportがtorchvisionなしで成功しました。しかし、モデルの前処理器を読み込む段階で、Qwen3VLVideoProcessorがtorchvisionを要求しました。
画像生成だけでも前処理器の読み込みで必要になったため、上の手順では最初から対応するROCm版torchvisionを導入しています。torchaudioは使用していません。
2. モデル取得前にGPU演算を確認する
check_gpu.pyでは、次を確認します。
- ROCm版PyTorchがGPUを認識していること
- BF16行列積の結果がCPUのFP32計算と許容誤差内で一致すること
- BF16のSDPA(Scaled Dot Product Attention)も同様に計算できること
-
QwenImage21Pipelineをimportできること
check_gpu.py 全文
"""Small ROCm correctness checks; no model download required."""
import json
import torch
import torch.nn.functional as F
def main():
assert torch.version.hip, "ROCm-enabled PyTorch is required"
assert torch.cuda.is_available(), "GPU is unavailable"
torch.manual_seed(42)
props = torch.cuda.get_device_properties(0)
a = torch.randn(256, 256, dtype=torch.bfloat16)
b = torch.randn(256, 256, dtype=torch.bfloat16)
actual = (a.cuda() @ b.cuda()).float().cpu()
expected = a.float() @ b.float()
torch.testing.assert_close(actual, expected, rtol=0.02, atol=0.25)
q, k, v = [torch.randn(1, 4, 128, 64, dtype=torch.bfloat16) for _ in range(3)]
attention = F.scaled_dot_product_attention(q.cuda(), k.cuda(), v.cuda()).float().cpu()
reference = F.scaled_dot_product_attention(q.float(), k.float(), v.float())
torch.testing.assert_close(attention, reference, rtol=0.03, atol=0.02)
torch.cuda.synchronize()
from diffusers import QwenImage21Pipeline
import transformers
import diffusers
print(json.dumps({
"torch": torch.__version__, "hip": torch.version.hip,
"gpu": props.name, "architecture": props.gcnArchName,
"gpu_memory_gib": props.total_memory / 2**30,
"bf16_matmul": "PASS", "sdpa": "PASS",
"pipeline_import": QwenImage21Pipeline.__name__,
"transformers": transformers.__version__, "diffusers": diffusers.__version__,
}, indent=2))
if __name__ == "__main__":
main()
.venv/bin/python check_gpu.py
実機ではGPU名がAMD Radeon 8060S Graphics、アーキテクチャがgfx1151、メモリが48.0 GiBと表示され、BF16行列積とSDPAの両方がPASSになりました。
rocminfoでGPUが見えていても、PyTorchから計算できるかは別に確認します。今回も制限付き実行環境では/dev/kfdが見えず、ホスト側での実行が必要でした。
3. 公式モデルをダウンロードする
モデルはQwen/Qwen-Image-2.1から取得しました。検証時のrevisionに固定しています。
HF_HOME="$PWD/.cache/huggingface" .venv/bin/python - <<'PYTHON'
from huggingface_hub import snapshot_download
snapshot_download(
'Qwen/Qwen-Image-2.1',
revision='790c92633540aa0cb11d9abf19eb46d861714758',
local_dir='models/Qwen-Image-2.1',
ignore_patterns=['assets/*'],
max_workers=4,
)
PYTHON
紹介用のassetsを除く27ファイルを取得しました。モデル一式は約33.1 GB(約30.8 GiB)あります。これとは別にPython環境とダウンロード用キャッシュの容量も必要です。
4. 画像生成スクリプト
以下をgenerate.pyとして保存します。モデルはローカルファイルのみから読み込み、BF16、モデル単位のCPUオフロード、VAEタイリングを使います。
generate.py 全文
"""Generate an image with the local Qwen-Image-2.1 checkpoint."""
import argparse
import json
from pathlib import Path
import time
import torch
from diffusers import QwenImage21Pipeline
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--model', default='models/Qwen-Image-2.1')
prompt_group = parser.add_mutually_exclusive_group()
prompt_group.add_argument('--prompt', help='Prompt text')
prompt_group.add_argument('--prompt-file', help='UTF-8 text file containing the prompt')
parser.add_argument('--output', default='outputs/first-image.png')
parser.add_argument('--size', type=int, default=512)
parser.add_argument('--steps', type=int, default=20)
parser.add_argument('--seed', type=int, default=42)
args = parser.parse_args()
if args.prompt_file is not None:
try:
args.prompt = Path(args.prompt_file).read_text(encoding='utf-8-sig').strip()
except (OSError, UnicodeError) as exc:
parser.error(f'Cannot read prompt file {args.prompt_file!r}: {exc}')
if not args.prompt:
parser.error('Prompt file must not be empty or whitespace-only')
elif args.prompt is None:
args.prompt = 'A small red fox sitting in a peaceful green forest, soft morning sunlight, detailed natural photography.'
assert torch.cuda.is_available() and torch.version.hip, 'ROCm GPU required'
start = time.monotonic()
pipe = QwenImage21Pipeline.from_pretrained(
args.model, torch_dtype=torch.bfloat16, local_files_only=True,
)
pipe.enable_model_cpu_offload()
pipe.vae.enable_tiling()
loaded = time.monotonic()
torch.cuda.reset_peak_memory_stats()
image = pipe(
prompt=args.prompt, width=args.size, height=args.size,
num_inference_steps=args.steps,
generator=torch.Generator('cuda').manual_seed(args.seed),
).images[0]
torch.cuda.synchronize()
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
image.save(output)
report = dict(vars(args), load_seconds=loaded-start,
generation_seconds=time.monotonic()-loaded,
peak_gpu_allocated_gib=torch.cuda.max_memory_allocated()/2**30,
peak_gpu_reserved_gib=torch.cuda.max_memory_reserved()/2**30,
image_size=list(image.size), image_mode=image.mode,
torch=torch.__version__, hip=torch.version.hip)
output.with_suffix('.json').write_text(json.dumps(report, indent=2)+'\n')
print(json.dumps(report, indent=2), flush=True)
if __name__ == '__main__':
main()
コマンドからプロンプトを渡す
cd ~/qwen-image-2.1
.venv/bin/python generate.py \
--prompt 'A small red fox sitting in a peaceful green forest, soft morning sunlight, detailed natural photography.' \
--size 512 \
--steps 20 \
--seed 42 \
--output outputs/first-image.png
--outputで指定したPNGに加えて、同じ名前のJSONも保存します。上の例ではoutputs/first-image.jsonです。
テキストファイルからプロンプトを渡す
長いプロンプトはUTF-8のファイルに保存すると編集しやすくなります。例えば、prompt.txtを次の内容で作成します。
A small red fox sitting in a peaceful green forest.
Soft morning sunlight filters through the trees.
Detailed natural photography, gentle background blur.
実行時は--prompt-fileを指定します。
.venv/bin/python generate.py \
--prompt-file prompt.txt \
--size 512 --steps 20 --seed 42 \
--output outputs/from-file.png
ファイル入力の仕様は次のとおりです。
- UTF-8で読み込む。BOM付きUTF-8にも対応
- 日本語と複数行を入力できる
- ファイル全体を1つのプロンプトとして扱う。行ごとの連続生成ではない
- 先頭と末尾の空白・改行を取り除き、途中の改行は保持する
-
--promptと--prompt-fileの同時指定はエラーにする - 空ファイルや読み込みに失敗したファイルは、モデル読み込み前にエラーにする
- JSONにはファイル名と、実際に読み込んだプロンプトの両方を残す
パスに空白がある場合は--prompt-file "my prompts/fox.txt"のように引用符で囲みます。日本語の入力にも対応しますが、日本語プロンプトによる画質比較は今回行っていません。
オプション一覧
| オプション | 既定値 | 内容 |
|---|---|---|
--model |
models/Qwen-Image-2.1 |
ローカルモデルの保存先 |
--prompt |
キツネと森のサンプル文 | プロンプトを直接指定 |
--prompt-file |
なし | UTF-8ファイルから読み込む |
--output |
outputs/first-image.png |
画像の保存先 |
--size |
512 |
正方形画像の幅と高さ |
--steps |
20 |
推論ステップ数 |
--seed |
42 |
乱数シード |
同じ出力先を指定するとPNGとJSONを上書きします。結果を残す場合は--outputを変えてください。
実際の生成結果
上記の英語プロンプトで、森の中に座る赤いキツネの画像を生成できました。保存後に目視し、プロンプトに対応する内容であることを確認しています。
| 項目 | 結果 |
|---|---|
| 解像度 | 512×512 |
| 推論ステップ | 20 |
| seed | 42 |
| 保存形式 | PNG / RGBA |
| 生成所要時間 | 約60.9秒 |
| PyTorch GPUメモリピーク(allocated) | 約16.45 GiB |
| PyTorch GPUメモリピーク(reserved) | 約16.61 GiB |
生成所要時間にはプロンプト処理、GPUへの転送、最終画像への変換・保存を含みます。初回の1回の測定で、複数回平均のベンチマークではありません。
パイプライン読み込みの計測値は約0.53秒でしたが、重みの実体読み込みや転送が後段に遅延する可能性があります。この値だけをモデル全体のロード時間として比較しないようにしています。
メモリ値はPyTorchが計測したGPU上の確保量です。GPU全体の使用量やCPU側のメモリを合計した値ではありません。また、保存モードがRGBAであることと、背景が透明であることは別です。
今回の生成はHF_HUB_OFFLINE=1も指定して実行しました。モデル取得後は、ローカルの重みから外部通信なしで生成できています。
HF_HUB_OFFLINE=1 .venv/bin/python generate.py \
--prompt-file prompt.txt \
--output outputs/offline.png
今回確認した範囲
512×512・20ステップでの画像生成、出力保存、GPUメモリの計測まで確認できました。ファイル入力機能も作成済みで、上記スクリプトに含めています。計測結果は直接プロンプトを渡した初回生成のものです。
公式READMEには2K解像度、画像編集、透明画像生成などの例もありますが、今回の実機検証には含めていません。このgenerate.pyは正方形のテキスト画像生成用で、画像編集用の入力オプションは実装していません。
既存のROCm環境を使い、Python環境とモデルを分けて配置することで、テキストファイルを編集してローカル画像生成を繰り返せる状態になりました。