Summary
Ryzen AI MAX+ 395 (gfx1151) の ROCm を 7.14.0 → 10.0.0 に上げた記録。
手順書は「旧版を purge してから新版を入れる」と書いていたが、実際に調べると
ROCm 10 は 7.14 と共存インストールできる。/opt/rocmが update-alternatives
方式だからで、順序を逆にすると「ROCm 無しの時間帯」が消えて切り戻しも効く。
依存プロジェクト側は身構えていたが、CTranslate2 も whisperX も NeMo も再ビルド不要だった。
torch の wheel が ROCm ランタイムを丸ごと同梱しているため、システム ROCm を
入れ替えても影響を受けない。これを憶測でなく/proc/self/mapsで確定させた。
llama.cpp すら 7.14 でビルドしたバイナリが ROCm 10 でそのまま動く(soname が同じ)。
一方でhipconfig --versionが7.15を返す、apt autoremoveが NPU の予備カーネルを
消しにくる、ビルドディレクトリをリネームすると RUNPATH で壊れる、といった罠は踏んだ。
最後に音声対話AI(VOICEVOX / llama-server / NeMo STT / three-vrm)を丸ごと起動して、
発話 → 応答まで通ることを確認するところまで。
はじめに
ROCm 10.0.0 が出たので、音声対話AIを動かしている手元の mini PC を 7.14 から上げた。
この手の移行で怖いのは本体よりも周辺で、うちの場合は
- whisperX(faster-whisper 経由の CTranslate2 が ROCm にリンクしている)
- NeMo(nvidia の ASR。whisperX と同一プロセスに同居させている)
- llama.cpp の HIP ビルド
- 上記を束ねた音声対話AI一式
がぶら下がっている。事前に書いた手順書には「CTranslate2 は即死する。最優先で再ビルド」
と書いていた。これは結果的に間違いだった、というのがこの記事の一番の収穫になる。
憶測で「たぶん壊れる」と書いた箇所を一つずつ実測で潰していったら、
やることが半分以下に減った。その過程を、踏んだ罠込みで残しておく。
検証環境
| 機種 | GMKtec NucBox EVO X2 |
| CPU/GPU | AMD Ryzen AI MAX+ 395 w/ Radeon 8060S (gfx1151, RDNA 3.5) |
| メモリ | 96GB unified(BIOS で 48GB を VRAM に割当) |
| OS | Ubuntu 26.04 (resolute) |
| kernel | 7.0.0-30-generic(GA カーネル) |
| 移行前 ROCm | 7.14.0-3 (repo.amd.com/rocm/packages-multi-arch/ubuntu2604) |
| 移行後 ROCm |
10.0.0-4 (stable.repo.amd.com/rocm/core/packages/ubuntu2604) |
| PyTorch | 2.8.0+rocm7.12.0(gfx1151 専用 wheel) |
| NPU | XDNA2 / XRT 2.25.37(今回は無関係だが後述) |
ROCm 10.0.0 の compatibility matrix では Ubuntu 26.04 は GA kernel 7.0、
gfx1151 は正式サポート対象として載っている。手元の 7.0.0-30-generic は
そのまま要件を満たしていたので、HWE カーネルの hold といった小細工は不要だった。
$ uname -r
7.0.0-30-generic
$ apt-mark showhold
(空)
なお Ubuntu 26.04 は公式アーカイブにも ROCm を同梱していて apt install rocm が通るが、
中身は 7.1.0 相当で古い。最新を使いたいなら AMD のリポジトリを足す必要がある。
第1部 — ROCm 7.14 → 10.0.0
つまづき1: 「消してから入れる」は危険だった
AMD の公式ドキュメントには「ROCm 7.2.4 以前が入っているならアンインストールしてから進めろ」
と書いてある。自分の手順書もそれに倣って「7.14 を purge → ROCm 10 を install」の順にしていた。
だがこの順序には穴がある。先に消した後で ROCm 10 が入らなかったら、ROCm 無しの機械が残る。
移行作業でいちばん避けたい状態がこれだ。
そこで消す前に、共存できないかを調べた。結論として共存できる。根拠は3つ:
1. パッケージ側に競合宣言が無い
$ curl -s https://stable.repo.amd.com/rocm/core/packages/ubuntu2604/dists/stable/main/binary-amd64/Packages \
| awk '/^Package: amdrocm-base10\.0$/{f=1} f&&/^(Conflicts|Replaces|Breaks|Depends):/{print} f&&/^$/{exit}'
Depends: libgcc-s1, libstdc++6, python3, libc6, amdrocm-runtime10.0
Conflicts も Replaces も Breaks も無い。
2. apt のシミュレーションで削除が0件
$ apt-get install -s amdrocm-core-sdk10.0-gfx1151 | grep -c '^Inst'
52
$ apt-get install -s amdrocm-core-sdk10.0-gfx1151 | grep -c '^Remv'
0
3. /opt/rocm が update-alternatives 方式
これが決定的だった。TheRock 系のパッケージング(7.13 以降)では、/opt/rocm の中身は
実体ディレクトリではなく alternatives へのシンボリックリンクになっている。
$ ls -l /opt/rocm
lrwxrwxrwx amdgcn -> /etc/alternatives/rocm-amdgcn
lrwxrwxrwx bin -> /etc/alternatives/rocm-bin
lrwxrwxrwx core -> /etc/alternatives/core
lrwxrwxrwx core-7 -> /etc/alternatives/core-7
drwxr-xr-x core-7.14 ← 実体はここ
lrwxrwxrwx include -> /etc/alternatives/rocm-include
lrwxrwxrwx lib -> /etc/alternatives/rocm-lib
つまり core-7.14/ と core-10.0/ は別ディレクトリとして並べられる設計になっている。
ファイル衝突は起きない。
というわけで順序を逆にした。先に ROCm 10 を入れて検証し、それから 7.14 を消す。
これなら ROCm 無しの時間帯が発生しないし、問題があれば alternatives を戻すだけで済む。
つまづき2: GPG 鍵を上書きすると切り戻せなくなる
公式手順は鍵をこう置けと言う。
wget https://stable.repo.amd.com/rocm/gpg/packages.gpg -O - | \
gpg --dearmor | sudo tee /etc/apt/keyrings/amdrocm.gpg > /dev/null
ところが手元では同じパス /etc/apt/keyrings/amdrocm.gpg を旧リポジトリが既に使っていた。
このまま上書きすると、鍵が違った場合に旧リポジトリの検証が壊れる。
つまり「ROCm 10 がダメだったら 7.14 に戻す」という退路が消える。
比べてみたら、バイト単位で同一だった。
$ gpg --show-keys --with-fingerprint new-amdrocm.gpg
pub rsa4096 2026-01-26 [SCEAR] [有効期限: 2029-01-25]
D0F0 04A0 025A 1145 C780 7FCD 0701 EAC4 D5E0 2107
$ gpg --show-keys --with-fingerprint /etc/apt/keyrings/amdrocm.gpg
pub rsa4096 2026-01-26 [SCEAR] [有効期限: 2029-01-25]
D0F0 04A0 025A 1145 C780 7FCD 0701 EAC4 D5E0 2107
$ cmp new-amdrocm.gpg /etc/apt/keyrings/amdrocm.gpg && echo IDENTICAL
IDENTICAL
同一なので取得ごと省略した。クリーンな機械なら公式どおりでいい。
**要点は「既存の鍵と同じパスに書く前に中身を比べろ」**ということ。
リポジトリ登録
sudo tee /etc/apt/sources.list.d/amdrocm-stable.sources << 'EOF'
X-Repo-Id: amdrocm-stable
Types: deb
URIs: https://stable.repo.amd.com/rocm/core/packages/ubuntu2604/
Suites: stable
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/amdrocm.gpg
Enabled: yes
EOF
sudo apt update
旧リポジトリの rocm.list はこの時点では消さない。 切り戻し用に残しておく。
入手可能性をここで確認する。ここで候補が出なければ、まだ何も壊していないので引き返せる。
$ apt-cache policy amdrocm-core-sdk10.0-gfx1151
インストールされているバージョン: (なし)
候補: 10.0.0-4
ROCm 10 を 7.14 と共存インストール
sudo apt install amdrocm-core-sdk10.0-gfx1151
実測でダウンロード 0.9GB、展開後 8.0GB。7.14(8.0GB)と一時的に共存するので
16GB 使う。新規52パッケージ、7.14 の削除は0件。
alternatives は auto モードなので、優先度が高い方に自動で切り替わる。
$ update-alternatives --display core
リンクは現在 /opt/rocm/core-10.0 を指しています
/opt/rocm/core-10.0 - 優先度 1375134277
/opt/rocm/core-7.14 - 優先度 983081250
数字が大きい ROCm 10 が勝つ。/opt/rocm/bin も /opt/rocm/lib も、
これで ROCm 10 側を指すようになる。
この時点で再起動は不要。 amdgpu ドライバはカーネル in-tree で、今回は一切触っていない。
つまづき3: hipconfig --version は 10.0.0 を返さない
検証しようとして最初に面食らったのがこれ。
$ /opt/rocm/bin/hipconfig --version
7.15.26333-0000000
ROCm 10 を入れたのに 7.15。バージョンが戻ったように見えるが、これは正しい。
ROCm 10.0.0 が同梱する HIP のバージョンが 7.15 なだけで、製品バージョンとは別物。
実際 compatibility matrix でも HIP のバージョンは製品番号と一致していない。
製品バージョンを見たいならこっち。
$ cat /opt/rocm/core-10.0/.info/version
10.0.0
ROCm 10 の動作検証(7.14 を消す前に)
デバイス認識。
$ rocminfo | grep -i gfx1151
Name: gfx1151
Name: amdgcn-amd-amdhsa--gfx1151
$ rocm-smi
0 1 0x1586, 50234 34.0°C 12.029W ... auto 1% 2%
コンパイラも見ておく。後で llama.cpp を再ビルドするのに使うので、ここで壊れていたら困る。
// t.hip
#include <hip/hip_runtime.h>
#include <cstdio>
__global__ void k(float*a){a[threadIdx.x]=threadIdx.x*2.0f;}
int main(){float*d;hipMalloc(&d,64*4);hipLaunchKernelGGL(k,dim3(1),dim3(64),0,0,d);
float h[64];hipMemcpy(h,d,64*4,hipMemcpyDeviceToHost);
hipDeviceProp_t p;hipGetDeviceProperties(&p,0);
printf("h[63]=%.1f arch=%s\n",h[63],p.gcnArchName);return 0;}
$ /opt/rocm/bin/hipcc --offload-arch=gfx1151 t.hip -o t
$ LD_LIBRARY_PATH=/opt/rocm/lib:/opt/rocm/lib/llvm/lib ./t
h[63]=126.0 arch=gfx1151
コンパイルも実行も通った。ここまで来て初めて 7.14 を消しにかかる。
もしダメだったら sudo update-alternatives --set core /opt/rocm/core-7.14 で即座に戻せる。
7.14 はまだ入ったままなので、これが本当に効く。
つまづき4: グロブ 'amdrocm7.14*' はほとんど当たらない
7.14 を消す段になって気づいたのだが、パッケージ名がこうなっている。
amdrocm-base7.14
amdrocm-blas7.14-gfx1151
amdrocm-core-sdk7.14-gfx1151
amdrocm-llvm7.14
...
バージョン番号が名前の途中に埋まっている。だから 'amdrocm7.14*' という
グロブは amdrocm-blas7.14 にも amdrocm-base7.14 にも当たらない。
これで消したつもりになると、大半が残る。
dpkg の実リストから抜くのが確実。
# まずシミュレートして、ROCm 10 が巻き添えにならないことを確認する
apt-get -s remove --purge $(dpkg -l | awk '$1=="ii" && $2 ~ /^amdrocm/ && $2 ~ /7\.14/ {print $2}') \
| grep -E '^(Remv|Purg)' | grep -c '10\.0'
# => 0
sudo apt purge $(dpkg -l | awk '$1=="ii" && $2 ~ /^amdrocm/ && $2 ~ /7\.14/ {print $2}')
結果は52パッケージちょうど削除、ROCm 10 への波及ゼロ、残留設定(rc)もゼロ。
/opt/rocm からは core-7.14/ が消え、alternatives は core-10.0 を指したままになった。
シミュレーションで削除件数を数えるとき、apt-get -s remove --purge の出力は
Remv ではなく Purg で始まる行になる。grep '^Remv' だけ見ていると
「削除0件」と誤読するので注意(実際に一度やらかした)。
つまづき5: apt autoremove が NPU の予備カーネルを消しにくる
purge の後、反射的に apt autoremove を打ちたくなるが、シミュレートしたらこうなった。
$ apt-get -s autoremove | grep -E '^(Remv|Purg)' | awk '{print $2}'
linux-headers-7.0.0-28-generic
linux-headers-7.0.0-28
linux-main-modules-zfs-7.0.0-28-generic
linux-image-7.0.0-28-generic
linux-modules-7.0.0-28-generic
linux-tools-7.0.0-28-generic
linux-tools-7.0.0-28
ROCm と全く関係ない旧カーネル 7.0.0-28 一式が対象になっている。
この機械は NPU (XDNA2) を使っていて、xrt-amdxdna が DKMS で入っている。
$ dkms status
xrt-amdxdna/2.25.260102.56.release, 7.0.0-28-generic, x86_64: installed
xrt-amdxdna/2.25.260102.56.release, 7.0.0-29-generic, x86_64: installed
xrt-amdxdna/2.25.260102.56.release, 7.0.0-30-generic, x86_64: installed
3カーネル分ビルドされている。予備を消すと NPU 側の退避先が減るだけで、
ROCm のアップグレードには何の関係もない。実行しなかった。
ROCm 10 の検証が全部済んでから、旧リポジトリ定義だけ消して締める。
sudo rm -f /etc/apt/sources.list.d/rocm.list
sudo apt update
第2部 — whisperX / CTranslate2 は再ビルドが要らなかった
ここが今回いちばん予想と違ったところ。
「即死する」と書いていた
CTranslate2 は ROCm 対応フォークをソースからビルドして /usr/local/lib に入れてある。
ビルド時のリンク先はシステムの /opt/rocm なので、7.14 を消したら動かなくなる ——
と考えて、手順書には「即死。最優先で再ビルド」と書いていた。
実際、ldd はそう見える。
$ ldd /usr/local/lib/libctranslate2.so | grep -E 'hip|roc'
libhipblas.so.3 => /opt/rocm/lib/libhipblas.so.3
libamdhip64.so.7 => /opt/rocm/lib/libamdhip64.so.7
librocblas.so.5 => /opt/rocm/lib/librocblas.so.5
librocsolver.so.0 => /opt/rocm/lib/librocsolver.so.0
...
ところが動く
念のため、システム ROCm を見せない状態で動かしてみた。
$ env -u LD_LIBRARY_PATH python -c "
import torch, ctranslate2
x=torch.randn(512,512,device='cuda'); print('torch gpu:', float((x@x).sum())!=0)
print('ct2 devices:', ctranslate2.get_cuda_device_count())"
torch gpu: True
ct2 devices: 1
動く。/etc/ld.so.conf.d/ に ROCm のエントリは無いので、ld キャッシュ経由でもない。
/proc/self/maps で確定させる
「たぶん torch が持っているライブラリを掴んでいる」という推測で終わらせず、
プロセスが実際にロードしている .so を数えた。
import torch, ctranslate2, re
ctranslate2.get_cuda_device_count()
libs = {m.group(1) for l in open('/proc/self/maps')
if (m := re.search(r'(/\S+\.so[.0-9]*)$', l.strip()))
and re.search(r'hip|roc|amd', m.group(1), re.I)}
print('/opt/rocm:', sum(l.startswith('/opt/rocm') for l in libs))
print('venv :', sum('site-packages' in l for l in libs))
/opt/rocm: 3
venv : 70
70個が venv 内、/opt/rocm からは3個だけ。 しかもその3個は
libhsa-amd-aqlprofile64.so と OpenMP (libomp.so / libarcher.so) で、
HIP や BLAS の本体ではない。
なぜこうなるか
gfx1151 用の PyTorch wheel は ROCm ランタイムを丸ごと同梱している。
.venv/lib/python3.12/site-packages/
_rocm_sdk_core/lib/ libamdhip64.so.7, libamd_comgr.so.3, libhsa-runtime64.so.1 ...
_rocm_sdk_libraries_gfx1151/lib/ libhipblas.so.3, librocblas.so.5, libhiprand.so.1 ...
そして import torch はこれらを先に読み込む。後から import された CTranslate2 は、
既にプロセスに載っている torch 同梱の 7.12 に解決される。だから
システム側が 7.14 だろうが 10.0 だろうが影響を受けない。
以前から「import torch を import ctranslate2 より先に」というルールで
運用していたのだが、その理由がここで腑に落ちた。これは単なる相性の問題ではなく、
CTranslate2 がどのランタイムに解決されるかを決めている。
当然 ROCm 10 でもこの順序は必要なまま。
なお librocsolver.so.0 と librocm_kpack.so.0 は venv 側に存在しない。
これらは /opt/rocm の librocblas.so.5 が引く依存で、venv 版の librocblas は
引かない。ldd の出力だけ見ていると「venv に足りないライブラリがある」と
誤解するポイント。実際にロードされるものを見ないと分からない。
PyTorch は上げなかった
ROCm 10 世代の wheel も出ている。
python -m pip install --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" \
"torchaudio==2.11.0+rocm10.0.0"
が、上げなかった。torchaudio 2.9 で torchaudio.info と AudioMetaData が削除されていて、
whisperX の VAD/話者分離に使っている pyannote-audio がこれに依存している。
ROCm 10 世代の torchaudio は 2.11 なので、上げると音声側が壊れる。
システム ROCm と torch のバージョンは独立に選べる(それが第2部の結論そのもの)ので、
torch 2.8.0+rocm7.12.0 のまま ROCm 10 に載せている。実際それで動いている。
rocm[libraries,device-gfx1151] という wheel もあるが、これは ROCm SDK を入れるだけで
PyTorch は入らない。torch が欲しいなら torch[device-gfx1151] を指定する。
第3部 — llama.cpp
soname が同じなので、旧バイナリがそのまま動く
7.14 でビルドした llama-cli の依存を ROCm 10 環境で見たら、全部解決していた。
$ ldd build/bin/llama-cli | grep -c 'not found'
0
$ ldd build/bin/llama-cli | grep -E 'hip|roc' | head -3
libhipblas.so.3 => /opt/rocm/lib/libhipblas.so.3
libamdhip64.so.7 => /opt/rocm/lib/libamdhip64.so.7
librocblas.so.5 => /opt/rocm/lib/librocblas.so.5
ROCm 10 も soname は libamdhip64.so.7 のまま。メジャーバージョンが 7→10 に
飛んだのはあくまで製品名で、ABI の識別子は据え置きということらしい
(hipconfig が 7.15 を返すのと平仄が合っている)。
リンクが通るだけでカーネルが動くとは限らないので、実際に推論させた。
$ build/bin/llama-bench -m Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf -p 128 -n 32 -ngl 99 -r 3
| qwen35moe 35B.A3B Q4_K | 20.81 GiB | 34.66 B | ROCm | 99 | pp128 | 617.08 ± 18.97 |
| qwen35moe 35B.A3B Q4_K | 20.81 GiB | 34.66 B | ROCm | 99 | tg32 | 50.20 ± 0.62 |
7.14 でビルドしたバイナリが ROCm 10 上で普通に動く。 再ビルドは必須ではなかった。
とはいえ 7.14 のヘッダ由来の潜在的なズレを残したくないので、再ビルドはした。
旧ビルドは残してある。
つまづき6: ビルドディレクトリをリネームすると壊れる
ここで一番派手に転んだ。
動いている build/ を潰したくなかったので、build-rocm10/ に別途ビルドして、
検証後にリネームして入れ替えようとした。
cmake -B build-rocm10 -DGGML_HIP=ON -DAMDGPU_TARGETS=gfx1151 -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_HIP_COMPILER=/opt/rocm/lib/llvm/bin/clang++
cmake --build build-rocm10 -j$(nproc)
# ここまでは動作確認 OK
mv build build-rocm714-backup && mv build-rocm10 build
これで起動しなくなった。
$ build/bin/llama-cli --list-devices
build/bin/llama-cli: error while loading shared libraries:
libllama-cli-impl.so: cannot open shared object file: No such file or directory
libllama-cli-impl.so は build/bin/ にちゃんと存在する。原因は RUNPATH。
$ objdump -x build/bin/llama-cli | grep RUNPATH
RUNPATH /home/user/llama.cpp/build-rocm10/bin:/opt/rocm/core-10.0/lib:/opt/rocm/lib:
ビルドディレクトリの絶対パスが焼き込まれている。 リネームした瞬間に無効になる。
最近の llama.cpp は共有ライブラリ構成になっていて(以前のビルドは bin/ に .so が
0個だったが、今回は libllama-cli-impl.so などが並ぶ)、この差が効いた。
解決
最終的に置く場所で configure し直せば済む。オブジェクトファイルは再利用されるので、
再コンパイルは走らず再リンクだけで終わる(実測で1分未満)。
cd ~/llama.cpp
cmake -B build -DGGML_HIP=ON -DAMDGPU_TARGETS=gfx1151 -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
$ objdump -x build/bin/llama-cli | grep RUNPATH
RUNPATH /home/user/llama.cpp/build/bin:/opt/rocm/core-10.0/lib:/opt/rocm/lib:
このとき -DCMAKE_HIP_COMPILER=... を付けたまま再 configure すると
Configuring incomplete, errors occurred! で落ちた。既存キャッシュにコンパイラが
記録済みの状態で指定し直したのが原因らしい。フラグを外して実行したら通り、
CMAKE_CACHEFILE_DIR も新しいパスに更新された。
教訓: 最初から最終的な場所でビルドする。 退避したいなら古い方を rename する。
ベンチ比較
Qwen3.6-35B-A3B-UD-Q4_K_XL(20.81 GiB)、-ngl 99 -r 3。
| ビルド | pp128 (t/s) | tg32 (t/s) |
|---|---|---|
| 7.14 ビルド(ROCm 10 上で実行) | 617.08 ± 18.97 | 50.20 ± 0.62 |
| ROCm 10 ビルド | 592.63 ± 2.54 | 50.10 ± 0.58 |
生成速度(tg32)は誤差の範囲で同じ。pp は数%低い値が出ているが、
旧ビルドでも測定ごとに 604〜617 の幅があり、有意差とは言えない。
ROCm 10 にしても速くも遅くもならなかった、というのが正直な結果。
第4部 — NeMo と音声対話AI
最後に、これらを束ねている音声対話AI一式を丸ごと起動して確認する。
start_all.sh で4サービスが立つ構成になっている。
| サービス | ポート | 中身 |
|---|---|---|
| VOICEVOX | 50021 | 音声合成(docker / CPU) |
| llama-server | 9931 | Qwen3.6-35B-A3B |
| ttllm | 8001 | STT (NeMo / whisperX) + オーケストレーション |
| three-vrm | 8000 | ブラウザ側の VRM 表示 |
ROCm 依存は2箇所
移行の影響を受けうるのは次の2つだった。
1. llama.cpp/build/bin/llama-server
start_all.sh が絶対パスで直接叩いている。
LLAMA_BIN="/home/$USER/llama.cpp/build/bin/llama-server"
[[ -x "$LLAMA_BIN" ]] || die "llama-server が見つかりません: $LLAMA_BIN"
第3部で再ビルドしたものを同じ build/ に置いたので無変更で通った。
build-rocm10/ のままにしていたら、ここで落ちていた。
(第3部のリネーム事故を踏んだおかげで、結果的に正しい場所に落ち着いた)
2. ttllm/.venv — whisperX とは別の venv
これが見落としやすい。whisperX 用の venv とは別に、NeMo と whisperX を
同居させた共用 venv がある。
~/whisperx/whisperX-rocm/.venv ← whisperX 単体用
~/AIassistant/ttllm/.venv ← NeMo + whisperX 共用(Python 3.12)
こちらも torch 2.8.0+rocm7.12.0 を同梱しているので、第2部の結論がそのまま効く。
確認だけして無変更。
$ ttllm/.venv/bin/python -c "
import torch
print(torch.__version__, torch.cuda.get_device_name(0))
import ctranslate2
print(ctranslate2.__version__, ctranslate2.get_cuda_device_count())"
2.8.0+rocm7.12.0 Radeon 8060S Graphics
4.6.2 1
プロジェクト自身のゲートを使う
このプロジェクトには verify_coexist.py という検証スクリプトが置いてあった。
「NeMo と whisperX が1プロセスに同居できること」を確かめるために以前書いたもので、
今回の移行検証にそのまま使える。
$ ttllm/.venv/bin/python ttllm/verify_coexist.py
load: 25.3s (EncDecRNNTBPEModelWithPrompt)
--- whisperX ---
load: 1.9s
=== 転写結果の比較 ===
音源 NeMo whisperX
human_greeting こんにちは こんにちは。
human_mountain 日本で二番目に高い山は 日本で2番目に高い山は、
human_river 日本で一番長い川は 日本で一番長い川は、
zundamon_greeting こんにちは こんにちは
zundamon_mountain 日本で二番目に高い山は 日本で2番目に高い山は
zundamon_river 日本で一番長い川は 日本で一番長い川は
=== 判定 ===
OK: 同一プロセスで両バックエンドがロード・転写でき、NeMo は既存結果と一致
転写結果が移行前のリファレンスと一致している。ここまで出れば NeMo 側は問題ない。
移行のたびに手で確認するのは面倒なので、プロジェクト側にこういうゲートを
用意しておくと移行作業がだいぶ楽になる、というのは今回の副次的な収穫だった。
全体を起動して通す
$ ./start_all.sh
[launch] VOICEVOX is up
[launch] llama-server is up
[launch] ttllm is up
[launch] STT を warmup (モデルロード + 初回推論を先に済ませる) ...
[launch] warmup 完了
[launch] three-vrm is up
ttllm の /health で STT の状態を見る。
$ curl -s http://localhost:8001/health
{"ok":true,
"stt":{"backend":"nemo","requested":"auto","fallback":"whisperx","fallback_active":false,
"model":"nvidia/nemotron-3.5-asr-streaming-0.6b","device":"cuda","loaded":true},
"llama":{"url":"http://localhost:9931","reachable":true}}
backend: nemo / device: cuda / fallback_active: false。
NeMo が GPU で動いていて、whisperX へのフォールバックは発動していない。
実際に音声を投げる。
$ curl -s -X POST http://localhost:8001/transcribe -F "audio=@human_mountain.wav"
{"transcript":"日本で二番目に高い山は"}
音声 → STT → LLM の通し。
$ time curl -s -X POST http://localhost:8001/voice_chat -F "audio=@human_mountain.wav"
real 0m1.409s
transcript: 日本で二番目に高い山は
reply: それ、富士山だよ!
日本一高い山は富士山で、二番目は白山(はくさん)だね!...
1.4秒で応答。移行前と体感は変わらない。
(内容の正しさは別問題。二番目は北岳である。これはモデルの知識の話で ROCm とは関係ない)
音声合成も確認。
$ curl -s -X POST -G "http://localhost:50021/audio_query" \
--data-urlencode "text=こんにちは、テストです" --data-urlencode "speaker=3" -o q.json
$ curl -s -X POST "http://localhost:50021/synthesis?speaker=3" \
-H 'Content-Type: application/json' --data-binary @q.json -o out.wav
$ file out.wav
out.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 24000 Hz
つまづき7: VOICEVOX に日本語を URL エンコード無しで投げると死ぬ
最初こう書いて失敗した。
$ curl -s -X POST "http://localhost:50021/audio_query?text=テストです&speaker=3" -o q.json
$ cat q.json
Invalid HTTP request received.
生の日本語を URL に入れたのが原因。-G --data-urlencode を使えばいい。
ROCm とは無関係な curl の使い方の問題だが、移行検証中にこれを踏むと
「VOICEVOX が壊れた」と勘違いしかねないので書いておく。
つまづき8: reasoning モデルは content が空で返る
llama-server の疎通確認でこうなった。
$ curl -s http://localhost:9931/v1/chat/completions -d '{"messages":[...],"max_tokens":64}'
{"choices":[{"message":{"content":""}}],"usage":{"completion_tokens":64,...}}
content が空。壊れたかと思ったが、Qwen3.6 は reasoning モデルで、
思考トークンだけで max_tokens: 64 を使い切っていただけだった。
reasoning_content を見ると思考過程がびっしり入っている。
max_tokens を 200 にしたらちゃんと返ってきた。
content: 日本で一番高い山は富士山です。
疎通確認のつもりで小さい max_tokens を指定すると、正常なのに壊れて見える。
移行後の状態
/opt/rocm/core-10.0/.info/version 10.0.0
rocminfo gfx1151 / AMD Radeon 8060S Graphics
amdrocm*10.0 パッケージ 52
amdrocm*7.14 パッケージ 0(残留設定も0)
whisperX (torch 2.8.0 + ct2 4.6.2) 無変更で動作
NeMo (nemotron-3.5-asr 0.6b) 無変更で動作、転写結果は移行前と一致
llama.cpp tg32 50.10 t/s(移行前 50.20 と同等)
音声対話AI 一式 start_all.sh で全サービス起動、通しで応答 1.4s
NPU (XDNA2 / XRT) 無影響
HSA_OVERRIDE_GFX_VERSION はこの環境では元々設定していない。
gfx1151 ネイティブビルドで揃えているので、設定するとむしろ壊れる。ROCm 10 でも同じ。
まとめ — 踏んだ罠一覧
| # | 罠 | 対処 |
|---|---|---|
| 1 | 手順書どおり「purge → install」だと ROCm 無しの時間帯ができる | 共存できるので順序を逆に。alternatives で切り戻し可 |
| 2 | GPG 鍵を既存と同じパスに上書きすると旧リポジトリの検証が壊れる | 上書き前に cmp で比較。今回は同一だったので何もしない |
| 3 |
hipconfig --version が 7.15 を返す |
製品版は /opt/rocm/core-10.0/.info/version
|
| 4 | グロブ 'amdrocm7.14*' が大半のパッケージに当たらない |
dpkg -l + awk で実リストから抽出 |
| 5 |
apt autoremove が NPU の DKMS 用予備カーネルを消しにくる |
実行しない |
| 6 | ビルドディレクトリをリネームすると RUNPATH が壊れる | 最終的な場所で configure し直す(再リンクのみで済む) |
| 7 | VOICEVOX に日本語を URL エンコード無しで投げると Invalid HTTP request
|
curl -G --data-urlencode |
| 8 | reasoning モデルで content が空に見える |
max_tokens を増やすか reasoning_content を見る |
そして一番の収穫は、「たぶん壊れる」を実測で潰すと作業が激減するということ。
CTranslate2 は再ビルドすると30分近くかかる。「即死する」と思い込んだまま進めていたら、
不要なビルドを2回(CTranslate2 と Python バインディング)やった上で、
PyTorch まで巻き添えで上げようとして pyannote を壊していたはずだ。
ldd は「リンク時に何を要求しているか」しか教えてくれない。
実際に何がロードされるかは /proc/self/maps を見る。
この一手間で、移行計画の半分が消えた。
移行そのものは、共存インストールという逃げ道を先に確保してしまえば、
拍子抜けするほど素直だった。