RX 9060 XT ×2 + ROCm 10 + llama.cpp + Qwen3.8 27Bで40 tok/s超を出すまで
はじめに
RX 9060 XT 16GBを2枚使って、Qwen3.8 27Bをできるだけ高速に動かしたくなりました。
最終的には、
- RX 9060 XT 16GB ×2
- ROCm 10
- llama.cpp
- Qwen3.8 27B Q4_K_M
- tensor split
- MTP
- PCIe P2P
という構成で、
平均生成速度: 42.20 tok/s
瞬間速度: 46.71 tok/s
まで到達しました。
最初の素のベンチは、
13.69 tok/s
だったので、最終的には約3.08倍です。
ただし、ここまで来るまでにかなり苦労しました。
この記事では、成功した手順だけではなく、
- WindowsネイティブROCm 10でクラッシュ
- WSLでは2GPU認識するのに2枚目が落ちる
- Ubuntuへ移行
- Secure Bootでamdgpuがロード不能
- P2Pが無効
- MTPやGPU分割比率の調整
といった失敗も含めて順番にまとめます。
環境
今回の構成です。
| 項目 | 構成 |
|---|---|
| CPU | Ryzen 7 3700X |
| Motherboard | MSI MEG X570 UNIFY |
| GPU | Radeon RX 9060 XT 16GB ×2 |
| GPU arch | gfx1200 |
| OS | Ubuntu 24.04 |
| Kernel | 7.0.0-30-generic |
| ROCm | ROCm Core 10.0 |
| amdgpu DKMS | 7.1.3-2390945.24.04 |
| llama.cpp | 0.3.0-dev |
| llama.cpp build | bebc9350e / build 10700 |
| Model | Qwen3.8 27B |
| Quant | Q4_K_M |
| GGUF size | 15.65 GiB |
使用したGGUFはこちらです。
OBLITERATUS/Qwen3.8-27B-OBLITERATED
Qwen3.8-27B-OBLITERATED-Q4_K_M.gguf
Step 1. 最初はWindowsネイティブROCm 10を試した
ROCm 10が出たので、まずWindowsネイティブでTheRock版ROCm 10を導入しました。
ROCm自体は正常にRX 9060 XTを2枚認識しました。
Device 0: AMD Radeon RX 9060 XT, gfx1200
Device 1: AMD Radeon RX 9060 XT, gfx1200
llama.cppもROCm 10向けにビルドできました。
しかしモデルをロードすると、
cudaMemGetInfo failed (invalid argument)
が発生し、最終的に、
-1073741819
つまり、
0xC0000005
Access Violation
でクラッシュ。
1GPUに限定しても同じでした。
Flash Attentionを無効にしても、
-fa off
warmupを切っても、
--no-warmup
改善なし。
AMDドライバもROCm 10検証系の26.6.4へ合わせましたが結果は変わりませんでした。
Windowsネイティブは一旦断念
この時点で、
ROCm 10 + Windows + llama.cpp + gfx1200
の組み合わせ自体がまだかなり不安定だと判断しました。
Step 2. WSL2を試す
次に、
Windows 11
↓
WSL2
↓
Ubuntu 24.04
↓
ROCm 10
を試しました。
WSLではROCDXGを利用します。
ROCm 10を導入すると、
Agent 2
Name: gfx1200
Marketing Name: AMD Radeon RX 9060 XT
Agent 3
Name: gfx1200
Marketing Name: AMD Radeon RX 9060 XT
と、2枚とも認識。
さらにllama.cppでも、
ggml_cuda_init: found 2 ROCm devices
ROCm0: AMD Radeon RX 9060 XT
ROCm1: AMD Radeon RX 9060 XT
まで通りました。
これは期待できそうでした。
Step 3. WSLでモデルをロードすると2枚目のGPUが落ちる
Qwen3.8 27Bをロードすると、
load_tensors: offloaded 66/66 layers to GPU
ROCm0 model buffer size = 7898.69 MiB
ROCm1 model buffer size = 7189.63 MiB
と、66/66レイヤーを完全GPUオフロードできました。
しかし、
load_all_data: using async uploads for device ROCm0
load_all_data: using async uploads for device ROCm1
あたりでAMDドライバエラー。
GPU0には約9GB残っているのに、
GPU1 VRAM = 0GB
となりました。
WSLでは2GPUを列挙できても、実際のmGPU負荷で問題が発生しました。
ここでWSLで粘るより、
ネイティブLinuxへ移行することにしました。
Step 4. Windowsとのデュアルブート構成へ
幸いPCにはSSDが2台ありました。
Disk 0: 1TB
└ Windows 11
Disk 1: 512GB
└ Ubuntu
という構成にしました。
Windows SSDを触らずにUbuntuを別SSDへ入れられるので、かなり安全です。
最終的には、
Windows 11
Ubuntu 24.04
のデュアルブート環境になりました。
Step 5. ネイティブUbuntuへROCm 10を導入
まずAMD公式のamdgpu-installを利用。
wget https://repo.radeon.com/amdgpu-install/31.50/ubuntu/noble/amdgpu-install_31.50.315000-1_all.deb
sudo apt install -y \
./amdgpu-install_31.50.315000-1_all.deb
ROCm + graphicsを導入します。
sudo amdgpu-install \
--usecase=rocm,graphics \
--gfxversion=auto \
-y
ユーザーをGPUグループへ追加。
sudo usermod -a -G render,video "$USER"
再起動。
Step 6. llama.cppビルド用のROCm 10開発環境
最初はここでもハマりました。
CMakeすると、
The ROCm root directory:
/opt/rocm
does not contain the HIP runtime CMake package
そして、
hip-lang-config.cmake
が見つからない。
ROCm runtimeだけではなく、開発パッケージが必要でした。
gfx1200用ROCm 10開発環境を追加。
sudo apt install -y amdrocm-core-dev10.0-gfx1200
確認。
find /opt/rocm \
-name 'hip-lang-config.cmake' \
-o -name 'hip-config.cmake'
結果:
/opt/rocm/core-10.0/lib/cmake/hip/hip-config.cmake
/opt/rocm/core-10.0/lib/cmake/hip-lang/hip-lang-config.cmake
Step 7. llama.cppをROCm 10 + gfx1200向けにビルド
環境変数。
export ROCM_PATH=/opt/rocm/core-10.0
export HIP_PATH=/opt/rocm/core-10.0
export PATH="$ROCM_PATH/bin:$ROCM_PATH/lib/llvm/bin:$PATH"
export LD_LIBRARY_PATH="$ROCM_PATH/lib:$ROCM_PATH/lib64:${LD_LIBRARY_PATH:-}"
llama.cpp取得。
cd ~
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
ビルド。
HIP_LLVM="$ROCM_PATH/lib/llvm/bin"
cmake -S . -B build-rocm10 -G Ninja \
-DGGML_HIP=ON \
-DGPU_TARGETS=gfx1200 \
-DCMAKE_HIP_COMPILER="$HIP_LLVM/clang" \
-DCMAKE_PREFIX_PATH="$ROCM_PATH" \
-DCMAKE_BUILD_TYPE=Release \
-DLLAMA_BUILD_TESTS=OFF
cmake --build build-rocm10 -j "$(nproc)"
GPU確認。
./build-rocm10/bin/llama-bench --list-devices
結果:
ggml_cuda_init: found 2 ROCm devices
Device 0: AMD Radeon RX 9060 XT
Device 1: AMD Radeon RX 9060 XT
ROCm0: AMD Radeon RX 9060 XT
ROCm1: AMD Radeon RX 9060 XT
ネイティブLinuxではきれいに2枚とも認識しました。
Step 8. まず素のllama-bench
モデルを、
MODEL="$HOME/models/Qwen3.8-27B-OBLITERATED-Q4_K_M.gguf"
としてベンチ。
./build-rocm10/bin/llama-bench \
-m "$MODEL" \
-ngl 99 \
-p 512 \
-n 256 \
-r 5 \
-sm layer
結果:
pp512 = 499.15 ± 35.46 t/s
tg256 = 13.69 ± 0.68 t/s
正直、
「思ったより遅い……」
という結果でした。
しかし、これはMTPを使わない通常decodeの速度です。
Step 9. Qwen3.8のMTPを有効化
今回のGGUFには、
qwen35.nextn_predict_layers = 1
があり、MTPを利用できます。
llama-serverを、
./build-rocm10/bin/llama-server \
-m "$MODEL" \
-ngl 99 \
-sm layer \
-ts 0.4,0.6 \
-c 8192 \
-fa on \
--spec-type draft-mtp \
--spec-draft-n-max 3 \
--spec-draft-p-min 0 \
--parallel 1 \
--jinja \
--host 127.0.0.1 \
--port 8080
で起動。
結果:
predicted_per_second = 21.33 t/s
draft_n = 252
draft_n_accepted = 170
Acceptanceは約67.5%。
MTPだけで、
13.69
↓
21.33 t/s
約1.56倍になりました。
Step 10. layer splitからtensor splitへ
ここが大きな転換点でした。
最初は、
-sm layer
でした。
これを、
-sm tensor
へ変更。
./build-rocm10/bin/llama-server \
-m "$MODEL" \
-ngl 99 \
-sm tensor \
-ts 0.4,0.6 \
-c 8192 \
-fa on \
--spec-type draft-mtp \
--spec-draft-n-max 3 \
--spec-draft-p-min 0 \
--parallel 1 \
--jinja \
--host 127.0.0.1 \
--port 8080
すると、
30.47 t/s
まで上昇。
21.33
↓
30.47 t/s
**+43%**です。
2枚GPUではtensor splitがかなり効きました。
Step 11. MTPのn-maxを調整
次に、
--spec-draft-n-max
を比較。
n-max = 4
29.29 t/s
Acceptance 55.70%
mean len 3.23
n-max = 3
30.47 t/s
Acceptance 62.64%
mean len 2.87
n-max = 2
31.54 t/s
Acceptance 72.60%
mean len 2.45
今回の構成では、
n-max = 2
が明確に最速でした。
長くdraftするほど良いわけではなく、外れたdraftのコストが効きます。
Step 12. GPU分割比率を最適化
次に、
-ts
を調整。
40:60
-ts 0.4,0.6
結果:
31.54 t/s
Acceptance 72.60%
45:55
-ts 0.45,0.55
結果:
32.81 t/s
Acceptance 75.37%
tg_3s 36.91 t/s
50:50
-ts 0.5,0.5
結果:
30.18 t/s
Acceptance 67.28%
意外にも50:50ではなく、
45:55
が最速でした。
Ubuntuの画面表示をGPU0側が担当していることも影響している可能性があります。
Step 13. P2Pを調査
起動ログには毎回、
internal AllReduce init failed
falling back to meta-backend butterfly
という警告が出ていました。
そこでGPU間P2Pを調査。
最初のamd-smi topologyは、
ACCESS TABLE:
GPU0 -> GPU1 DISABLED
GPU1 -> GPU0 DISABLED
でした。
PCIe構成自体は、
GPU0: CPU Root Port 03.1
GPU1: CPU Root Port 03.2
で、両方CPU直結。
カーネル側も、
CONFIG_PCI_P2PDMA=y
CONFIG_HSA_AMD_P2P=y
CONFIG_DMABUF_MOVE_NOTIFY=y
さらに、
cat /sys/module/amdgpu/parameters/pcie_p2p
は、
Y
でした。
Step 14. Resizable BARを確認
BIOSで、
Above 4G Decoding
Resizable BAR
IOMMU
を有効化。
確認すると、
BAR 0: current size: 16GB
両方とも16GB BARになりました。
GPU0 BAR0 = 16GB
GPU1 BAR0 = 16GB
ここは正常。
Step 15. BIOS変更後、突然GPUドライバが消える
ここでまたハマりました。
Ubuntuを起動すると、
/dev/kfd
が消え、
/dev/dri/renderD*
も存在しない。
HIPテストも、
devices = 0
になりました。
Windows側ではGPUは正常。
つまりハード故障ではありません。
確認すると、
sudo modprobe amdgpu
で、
Key was rejected by service
原因はSecure Bootでした。
DKMS自体は、
amdgpu/7.1.3-2390945.24.04,
7.0.0-30-generic,
x86_64:
installed
なのに、Secure Bootがamdgpuモジュールを拒否していました。
BIOS側でSecure Bootを無効化。
その結果GPUは復活しました。
Step 16. HIPでP2Pを直接確認
簡単なHIPプログラムを作りました。
#include <hip/hip_runtime.h>
#include <cstdio>
int main() {
int n = 0;
hipError_t err = hipGetDeviceCount(&n);
printf("devices = %d, %s\n",
n,
hipGetErrorString(err));
for (int i = 0; i < n; i++) {
hipDeviceProp_t p{};
hipGetDeviceProperties(&p, i);
printf("GPU%d: %s\n", i, p.name);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j) continue;
int can = -1;
err = hipDeviceCanAccessPeer(
&can,
i,
j
);
printf(
"GPU%d -> GPU%d: canAccessPeer=%d (%s)\n",
i,
j,
can,
hipGetErrorString(err)
);
if (can) {
hipSetDevice(i);
err = hipDeviceEnablePeerAccess(
j,
0
);
printf(
" enablePeerAccess: %s\n",
hipGetErrorString(err)
);
}
}
}
}
コンパイル。
hipcc /tmp/hip-p2p.cpp \
-o /tmp/hip-p2p
結果:
devices = 2, no error
GPU0: AMD Radeon RX 9060 XT
GPU1: AMD Radeon RX 9060 XT
GPU0 -> GPU1:
canAccessPeer=1
enablePeerAccess: no error
GPU1 -> GPU0:
canAccessPeer=1
enablePeerAccess: no error
ついに、
GPU0 ⇔ GPU1の双方向P2Pが有効
になりました。
Step 17. P2Pをllama.cppで利用
環境変数を追加。
export HSA_FORCE_FINE_GRAIN_PCIE=1
export GGML_CUDA_P2P=1
そして現時点の最適設定。
./build-rocm10/bin/llama-server \
-m "$MODEL" \
-ngl 99 \
-sm tensor \
-ts 0.45,0.55 \
-c 8192 \
-fa on \
--spec-type draft-mtp \
--spec-draft-n-max 2 \
--spec-draft-p-min 0 \
--parallel 1 \
--jinja \
--host 127.0.0.1 \
--port 8080
Step 18. ついに40 tok/sへ
256-token生成では、
eval time = 6487.26 ms / 256 tokens
39.31 tokens per second
途中値は、
tg = 39.51 t/s
tg_3s = 43.21 t/s
MTP acceptanceは、
153 accepted / 203 generated
75.37%
ここでほぼ40 t/sへ到達。
Step 19. 長めの生成では42 tok/s超
さらに長めに生成すると、
n_gen = 115
tg = 37.29 t/s
n_gen = 242
tg = 39.42 t/s
n_gen = 384
tg = 41.84 t/s
n_gen = 518
tg = 42.53 t/s
n_gen = 654
tg = 42.95 t/s
最終結果:
eval time =
17606.96 ms / 744 tokens
23.70 ms per token
42.20 tokens per second
瞬間速度:
tg_3s = 46.71 t/s
MTP:
draft acceptance =
0.80810
459 accepted / 568 generated
mean len = 2.62
最終的に、
42.20 tok/s
まで到達しました。
最終結果まとめ
最初から比較するとこうなります。
| 設定 | 生成速度 |
|---|---|
| llama-bench / MTPなし | 13.69 t/s |
| layer + MTP | 21.33 t/s |
| tensor + MTP3 | 30.47 t/s |
| tensor + MTP2 / 40:60 | 31.54 t/s |
| tensor + MTP2 / 45:55 | 32.81 t/s |
| P2P有効化後 | 39.31 t/s |
| 長めの実生成 | 42.20 t/s |
| 瞬間最大 | 46.71 t/s |
最初の、
13.69 t/s
から、
42.20 t/s
なので、
約3.08倍
になりました。
最終設定
現在使っている設定はこちらです。
export ROCM_PATH=/opt/rocm/core-10.0
export HIP_PATH=/opt/rocm/core-10.0
export PATH="$ROCM_PATH/bin:$PATH"
export LD_LIBRARY_PATH="$ROCM_PATH/lib:$ROCM_PATH/lib64:${LD_LIBRARY_PATH:-}"
export HSA_FORCE_FINE_GRAIN_PCIE=1
export GGML_CUDA_P2P=1
MODEL="$HOME/models/Qwen3.8-27B-OBLITERATED-Q4_K_M.gguf"
cd ~/llama.cpp
./build-rocm10/bin/llama-server \
-m "$MODEL" \
-ngl 99 \
-sm tensor \
-ts 0.45,0.55 \
-c 8192 \
-fa on \
--spec-type draft-mtp \
--spec-draft-n-max 2 \
--spec-draft-p-min 0 \
--parallel 1 \
--jinja \
--host 127.0.0.1 \
--port 8080
起動スクリプト化
毎回入力するのは面倒なので、例えば、
nano ~/start-qwen38.sh
として、
#!/usr/bin/env bash
export ROCM_PATH=/opt/rocm/core-10.0
export HIP_PATH=/opt/rocm/core-10.0
export PATH="$ROCM_PATH/bin:$PATH"
export LD_LIBRARY_PATH="$ROCM_PATH/lib:$ROCM_PATH/lib64:${LD_LIBRARY_PATH:-}"
export HSA_FORCE_FINE_GRAIN_PCIE=1
export GGML_CUDA_P2P=1
MODEL="$HOME/models/Qwen3.8-27B-OBLITERATED-Q4_K_M.gguf"
cd "$HOME/llama.cpp"
exec ./build-rocm10/bin/llama-server \
-m "$MODEL" \
-ngl 99 \
-sm tensor \
-ts 0.45,0.55 \
-c 8192 \
-fa on \
--spec-type draft-mtp \
--spec-draft-n-max 2 \
--spec-draft-p-min 0 \
--parallel 1 \
--jinja \
--host 127.0.0.1 \
--port 8080
保存。
chmod +x ~/start-qwen38.sh
以後は、
~/start-qwen38.sh
だけです。
APIから動作確認
curl -s \
http://127.0.0.1:8080/completion \
-H 'Content-Type: application/json' \
-d '{
"prompt":
"Write a Python program that implements a fast LRU cache.",
"n_predict": 512,
"temperature": 0
}' \
> /tmp/qwen38.json
速度確認。
python3 - <<'PY'
import json
d = json.load(
open("/tmp/qwen38.json")
)
t = d["timings"]
print(
"speed:",
t["predicted_per_second"],
"tok/s"
)
print(
"draft:",
t.get("draft_n")
)
print(
"accepted:",
t.get("draft_n_accepted")
)
if t.get("draft_n"):
print(
"acceptance:",
t["draft_n_accepted"]
/ t["draft_n"] * 100,
"%"
)
PY
生成された文章を見るなら、
python3 - <<'PY'
import json
d = json.load(
open("/tmp/qwen38.json")
)
print(d["content"])
PY
今回分かったこと
1. MTPは非常に重要
Qwen3.8ではMTPの有無で、
13.69
↓
21~40+ tok/s
と、大きな差が出ました。
ただし、
n-maxを大きくすれば速い
という単純な話ではありません。
今回の場合、
n-max=2
が最速でした。
2. multi-GPUではsplit modeが非常に重要
layer
より、
tensor
の方がかなり高速でした。
今回、
21.33
↓
30.47 tok/s
まで改善。
GPUを2枚挿しただけでは性能は出ません。
3. 50:50が最速とは限らない
同じ9060 XTを2枚使っていても、
50:50
より、
45:55
の方が高速でした。
ディスプレイ出力やVRAM使用量なども影響している可能性があります。
4. P2Pはかなり重要
P2P有効化後、
32.81
↓
39~42 t/s
まで伸びました。
少なくとも今回の構成では、ここが最後の大きなブレイクスルーでした。
5. Secure Bootには注意
今回一番焦ったのがこれ。
突然、
devices = 0
になり、
/dev/kfd
も消滅。
原因は、
modprobe: ERROR:
could not insert 'amdgpu':
Key was rejected by service
でした。
つまりSecure BootによるDKMS署名拒否。
ROCmを再インストールする前に、
sudo modprobe amdgpu
を試すのがおすすめです。
最後に
最初は、
ROCm 10にすれば少し速くなるかな?
くらいの気持ちでした。
しかし実際には、
Windowsネイティブでクラッシュ
↓
WSLへ
↓
2GPU目が落ちる
↓
Ubuntuデュアルブート
↓
ROCm開発環境不足
↓
llama.cppビルド
↓
13.69 t/s
↓
MTP
↓
tensor split
↓
split比率調整
↓
P2P調査
↓
Secure Bootでドライバ死亡
↓
P2P復旧
↓
42.20 t/s
という、かなり長い道のりになりました。
それでも最終的には、
RX 9060 XT 16GB ×2
Qwen3.8 27B Q4_K_M
ROCm 10
llama.cpp
という比較的コンシューマ向けの構成で、
平均42 tok/s超
まで出せました。
特にQwen3.8のMTPとllama.cppのtensor splitは効果が非常に大きく、設定次第で同じハードウェアでも3倍近く速度が変わるというのが今回一番面白かったところです。
現在の設定で十分速いため、ひとまずここで完成とします。