ローカルの 2048 を AI が自動プレイして VRM アバター「コテコ」が実況する完全オフラインデモ
AI2048 を、母艦の OS 更新(Ubuntu 24.04 → 26.04)と ROCm 7.2.x → 7.14.0 に
追随させました。
結論から書くと、
- Docker に閉じ込めていた部分(OpenClaw エージェント + play2048 スキル)は一切壊れなかった
- 壊れたのは全部ホスト側の起動スクリプト、しかも原因は GPU ではなく system python が 3.14 になったこと
- GPU 側は逆に設定が減った(
HSA_OVERRIDE_GFX_VERSIONを捨てられた)
という移行でした。前半にうまく行った話、後半に踏んだ地雷と解決策を書きます。
- AI2048 とは: 2048 を AI が自動プレイして VRM アバターが実況する「AI2048」を作った
- 母艦側の移行手順: Ubuntu 26.04 + ROCm 7.14 で gfx1151(Ryzen AI Max+ 395) の WhisperX / ローカル音声AIを動かす
リポジトリ: https://github.com/kotetsuy/AI2048
環境
| 項目 | 移行前 | 移行後 |
|---|---|---|
| マシン | NucBox EVO X2 / Ryzen AI MAX+ 395 (gfx1151 / 48GB unified memory) | 同じ |
| OS | Ubuntu 24.04 | Ubuntu 26.04 (resolute) |
| ROCm | 7.2.x(HSA_OVERRIDE_GFX_VERSION=11.5.1) |
7.14.0(/opt/rocm、gfx1151 ネイティブ。override はしない) |
| system python | 3.12 | 3.14 |
| 同梱 venv |
python3 -m venv(= system 3.12) |
uv 管理の 3.12(uv venv --python 3.12) |
| LLM | llama-server + Qwen3.6-35B-A3B | 同じ(gfx1151 ネイティブビルド) |
移行差分は 4 ファイル・24 行追加/10 行削除、うち実コードは起動スクリプト 2 本だけです。
前半: うまく行ったこと
① Docker に逃がした部分は OS 更新の影響をまったく受けなかった
このデモの中心にいる OpenClaw(エージェントランタイム + play2048 スキル)は Docker Compose で
動いています。ホストの OS を 26.04 に上げても、コンテナの中身は bookworm のままなので、
Python も playwright もバージョンが動きません。
ARG OPENCLAW_BASE=ghcr.io/openclaw/openclaw:latest
FROM ${OPENCLAW_BASE}
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
# bookworm は PEP 668 (externally-managed) のため --break-system-packages。
RUN python3 -m pip install --no-cache-dir --break-system-packages playwright
USER node
「完全オフラインデモなので依存は起動時インストールせず画像に焼き込む」という判断を
当時したのですが、それがそのままバージョン固定として効きました。実際、今回の移行差分に
Dockerfile と docker-compose.yml の変更は 1 行も入っていません(既存画像のまま動いた)。
network_mode: host を使っている都合上、ホストのポート構成が変わっていたら影響を受けますが、
2048 静的サーバ :8009 / three-vrm :8000 / llama :8080 / CDP :9222 / VOICEVOX :50021 は
全部そのままなので、ここも無風でした。
教訓: デモ用のオーケストレーション層をコンテナに置いておくと、母艦の OS 更新で
「アプリのロジックまで巻き込んで壊れる」ことがなくなります。今回ホスト側に残っていたのは
プロセスを起動して待ち合わせるシェルスクリプトだけで、壊れたのもそこだけでした。
② HSA_OVERRIDE_GFX_VERSION を「捨てる」方向の変更で済んだ
gfx1151(Ryzen AI Max+ 395)は長らく「ROCm が正式サポートしていないので gfx1150(11.5.1) を
名乗らせる」運用が必要で、起動スクリプトにも埋まっていました。
# Before
export HSA_OVERRIDE_GFX_VERSION="${HSA_OVERRIDE_GFX_VERSION:-11.5.1}"
ROCm 7.14 では前提が逆になります。repo.amd.com の gfx1151 専用ホイールも、自前ビルドの
llama.cpp も gfx1151 ネイティブなので、アーキテクチャを override すると壊れます。
# After
# gfx1151 (Ryzen AI Max+ 395) 向け ROCm env(llama 用)。
# HSA_OVERRIDE_GFX_VERSION は設定しない。
# repo.amd.com の gfx1151 wheel も llama.cpp も gfx1151 ネイティブビルドなので
# override すると壊れる。
unset HSA_OVERRIDE_GFX_VERSION
export ROCM_PATH="${ROCM_PATH:-/opt/rocm}"
export HIP_VISIBLE_DEVICES="${HIP_VISIBLE_DEVICES:-0}"
export AMDGPU_TARGETS="${AMDGPU_TARGETS:-gfx1151}"
export LD_LIBRARY_PATH="/usr/local/lib:/opt/rocm/lib:/opt/rocm/lib/llvm/lib:${LD_LIBRARY_PATH:-}"
ポイントは「書かない」ではなく unset することです。.bashrc / .profile に昔の
export HSA_OVERRIDE_GFX_VERSION=11.5.1 が残っている環境は普通にあるので、スクリプト側で
明示的に消しておかないと、起動したシェル次第で挙動が変わります。
llama-server を tmux ウィンドウで起動するときのコマンド文字列にも同じ変数を渡していたので、
そちらからも削除しました。
# Before
LLAMA_CMD="HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION} ROCM_PATH=${ROCM_PATH} \
HIP_VISIBLE_DEVICES=${HIP_VISIBLE_DEVICES} AMDGPU_TARGETS=${AMDGPU_TARGETS} \
...
# After
LLAMA_CMD="ROCM_PATH=${ROCM_PATH} \
HIP_VISIBLE_DEVICES=${HIP_VISIBLE_DEVICES} AMDGPU_TARGETS=${AMDGPU_TARGETS} \
...
env VAR=... command 形式で子プロセスに渡している箇所は、親シェルで unset しても
文字列として残り続けるので取りこぼしやすいところです。grep -rn HSA_OVERRIDE を
リポジトリ全体にかけて潰すのが確実です。
③ GPU 依存の重い部分は「他プロジェクトで解決済み」を流用できた
AI2048 の LLM は AIassistant / EarthTourGuide と同じ llama-server を共用する設計
(-c 65536 --parallel 2 で共有)なので、モデルまわりで AI2048 固有にやることはありませんでした。
llama.cpp を gfx1151 ネイティブで焼き直す作業も共用側で完了済みです。
cmake -DGGML_HIP=ON -DAMDGPU_TARGETS=gfx1151 ...
README の前提表もこれに合わせて更新しました。
| 必要物 | 既定パス | 入手方法 |
|---|---|---|
| llama.cpp(ROCm ビルド) | ~/llama.cpp/build/bin/llama-server |
gfx1151 ネイティブでビルド(cmake -DGGML_HIP=ON -DAMDGPU_TARGETS=gfx1151) |
「重い依存を共用リポジトリに寄せる」構成は、こういう基盤更新のときに移行コストが 1 回で済む
という形で効きます。
④ そもそも OS に依存しない部分が大きかった
AI2048 の「頭脳」側は、設計上ほぼ OS 非依存です。
- 手の決定は expectimax(純 Python・CPU) — 外部ライブラリなし。
-
盤面取得は localStorage の
gameStateを JSON で読む — 画像認識ではないので、
フォント・DPI・スケーリングが変わっても影響ゼロ。 -
ブラウザ操作は CDP(
connect_over_cdp) — ホストの headed Chrome に後から接続する形なので、
Playwright 側で Chromium バイナリを持たない(playwright install不要)。
26.04 で Chrome も GNOME も更新されましたが、画面を「見て」いないので実況の精度は
一切変わりませんでした。デモを画像認識で組んでいたら、ここで作り直しが発生していたはずです。
⑤ 起動確認
./start_all.sh を通しで実行して、7 サービスがヘルスチェックを通ることを確認しました。
[start] waiting for 2048 サーバ (http://localhost:8009/) ...
[start] 2048 サーバ is up
[start] VOICEVOX is up
[start] llama-server is up
[start] three-vrm is up
[start] 2048 Chrome(CDP) is up
[start] gateway healthy
そのうえで 1 ゲーム実況を回します。
cd openclaw-demo
docker compose run --rm -T openclaw-cli \
agent --agent main --session-key demo$(date +%s) \
--message "play2048 スキルで新規ゲームを始め、step→narrate で実況して報告して。"
盤面が動き、背景に 2048 のライブ映像が出て、その手前でコテコが喋れば移行完了です。
後半: 詰まったポイントと解決策
ここからが本題です。GPU まわりだと思って身構えていたら、全部 python 3.14 の話でした。
詰まり① ModuleNotFoundError: No module named 'aiohttp'
一括起動が three-vrm(VRM 表示 + /speak + 背景中継)で止まります。
[start] waiting for three-vrm (http://localhost:8000/status) ...
[start] three-vrm did not come up within 30s
tmux でウィンドウを覗くと本体はこれ。
Traceback (most recent call last):
File "$HOME/AI2048/three-vrm/server.py", line 28, in <module>
import aiohttp
ModuleNotFoundError: No module named 'aiohttp'
原因は単純で、起動スクリプトが素朴に python3 と書いていたことです。
new_window three-vrm "cd ${ROOT}/three-vrm && DISPLAY=${DISPLAY} python3 server.py"
24.04 では system python が 3.12 で aiohttp が入っていたのでこれで動いていました。
26.04 の system python は 3.14 で、当然入っていません。
解決策: sudo apt install ではなく、同梱 venv の python で起動する
この構成にはすでに aiohttp を持つ venv がある(背景配信 bgcast 用に playwright + aiohttp を
入れた AI2048/.venv)ので、system python を汚さずそれを使うようにしました。
# three-vrm(aiohttp) / bgcast(playwright) 用の同梱 venv。
# Ubuntu 26.04 のシステム python3 は 3.14 で aiohttp/playwright を持たないため、
# `uv venv --python 3.12 .venv && uv pip install aiohttp playwright` で作る。
VENV_PY="${ROOT}/.venv/bin/python"
...
new_window three-vrm "cd ${ROOT}/three-vrm && DISPLAY=${DISPLAY} ${VENV_PY} server.py"
system python に apt でライブラリを足す解決は、OS を上げるたびに同じ問題が再発します
(次に 3.16 が来たらまた消える)。アプリの依存はアプリ側の venv に持たせるのが、
ローリングで OS が上がる母艦では結局いちばん安いです。
詰まり② 旧 venv がインタプリタごと壊れる
では既存の .venv を使えばいいかというと、そうはいきませんでした。README にはこう書いてあります。
python3 -m venv .venv
./.venv/bin/pip install playwright aiohttp
これは 24.04 時代の手順なので、作られた venv は system python 3.12 を指すシンボリックリンクです。
OS を 26.04 に上げると 3.12 のインタプリタが消えるので、.venv/bin/python はリンク切れになって
何をしても動きません。しかも python3 -m venv を今実行すると、今度は 3.14 の venv ができてしまい、
playwright/aiohttp を入れ直すところからやり直しになります。
解決策: uv で 3.12 を持ってきて作り直す
cd ~/AI2048
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python aiohttp playwright
uv は指定バージョンの CPython を自前で取ってきて ~/.local/share/uv/python/ 以下に置くので、
system python のバージョンから独立します。
$ cat .venv/pyvenv.cfg
home = $HOME/.local/share/uv/python/cpython-3.12-linux-x86_64-gnu/bin
implementation = CPython
uv = 0.11.2
version_info = 3.12.13
include-system-site-packages = false
home が uv 側を向いていれば、次に OS を上げても壊れません。
なお connect_over_cdp でホストの Chrome に繋ぐ構成なので、playwright install(Chromium 本体の
ダウンロード)は不要です。オフラインデモ機で数百 MB を取りに行かずに済みます。
詰まり③ 起動経路が 2 つあり、片方だけ直すと再発する
three-vrm を起動しているコードは 1 箇所ではありませんでした。
-
start_all.sh— 一括起動の 4 番目として起動 -
start_phase2_display.sh— 表示だけ再起動したいとき用。:8000 が応答しなければ自分でも起動する
start_all.sh だけ直すと「一括起動は通るのに、表示だけ直そうと start_phase2_display.sh を
叩くと落ちる」という、再現条件がややこしいバグとして残ります。両方直しました。
# server.py は aiohttp が要るのでリポジトリ同梱 venv の python を使う
# (システム python3 は 3.14 で aiohttp 未導入)。
VRM_PY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.venv/bin/python"
[ -x "$VRM_PY" ] || VRM_PY=python3
...
( cd "$THREE_VRM_DIR" && DISPLAY="$DISPLAY" setsid nohup "$VRM_PY" server.py \
>/tmp/three-vrm.log 2>&1 </dev/null & )
冪等な起動スクリプト(既に上がっていればスキップ)を書くと、同じサービスの起動コードが
複数箇所に散りやすいという副作用です。移行のときは grep -rn "python3 " *.sh のような
雑な検索を一度かけて、起動経路を全部洗い出しておくと安全です。
詰まり④ 4 番目で落ちるので原因が埋もれる
start_all.sh は 7 サービスを順に立ち上げてヘルスチェックで待ち合わせます。three-vrm は
4 番目なので、そこで落ちると「2048 サーバ・VOICEVOX・llama-server(モデルロードに数十秒)を
立ち上げ切ってから 30 秒待ってタイムアウト」という、無駄に長い失敗になります。
解決策: preflight で fail-fast させる
サービスを 1 つも起動する前に venv の存在を確認し、直し方まで書いて落とすようにしました。
[[ -x "$VENV_PY" ]] || die "venv がありません: $VENV_PY
作成: cd ${ROOT} && uv venv --python 3.12 .venv && uv pip install --python .venv/bin/python aiohttp playwright"
デモ本番前の環境構築で効くのはこの手のエラーメッセージに復旧コマンドを埋めておく細工です。
トレードショー会場で ModuleNotFoundError のスタックトレースを読みたくはありません。
詰まり⑤(教訓)壊れなかったスクリプトから学ぶ
同じ venv に依存している背景配信 bgcast は、なぜか無傷で動きました。理由はコードを見て
すぐ分かりました。こちらは最初から「使える python を探す」書き方になっていたからです。
# playwright+aiohttp が使える python を選ぶ(自前 venv 優先、無ければ EarthTourGuide のを流用)。
BGCAST_PY=""
for cand in "$ROOT/.venv/bin/python" "$HOME/EarthTourGuide/earth-controller/.venv/bin/python"; do
if [ -x "$cand" ] && "$cand" -c "from playwright.async_api import async_playwright; import aiohttp" >/dev/null 2>&1; then
BGCAST_PY="$cand"; break
fi
done
パスをベタ書きするのではなく「必要な機能を import できるか」で選ぶと、環境差にそのまま強い。
three-vrm 側も同じ書き方にしておけば、今回そもそも踏まずに済んだ地雷でした。
詰まり⑥ ドキュメントの手順が古いまま残る
移行後の最大の落とし穴はこれかもしれません。README の
python3 -m venv .venv
は、26.04 で実行すると 3.14 の venv ができて詰まり②を再生産します。移行のときは
「動いた」で終わりにせず、セットアップ手順を新規クローンからなぞり直す必要があります。
README / READMEJ はこう差し替えました。
# 3) 同梱 venv を作成(three-vrm の aiohttp と 背景配信(bgcast) の playwright 用)
# Ubuntu 26.04 のシステム python3 は 3.14 で aiohttp/playwright を持たないため、
# uv で 3.12 を用意する(`python3 -m venv` だと system python に縛られ、
# OS 更新でインタプリタ参照が切れて壊れる)。
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python aiohttp playwright
# ※ connect_over_cdp はホストの Chrome を使うので `playwright install` は不要。
あわせて必須コマンドに uv を追加し、トラブルシュート節にも今回踏んだ症状(aiohttp 欠落 /
venv のリンク切れ / HSA_OVERRIDE_GFX_VERSION 残存)を追記しています。「venv を作る」という
1 行は、どの python で作るかまで書かないと再現しない手順でした。
トラブルシュート表
| 症状 | 原因 | 対処 |
|---|---|---|
three-vrm が ModuleNotFoundError: aiohttp
|
system python3 (3.14) で起動している | 同梱 venv の python(.venv/bin/python)で起動する |
.venv/bin/python が実行できない / リンク切れ |
OS 更新で system 3.12 が消えた |
uv venv --python 3.12 .venv で作り直す |
| 一括起動は通るが表示スクリプトだけ落ちる | 起動経路が 2 箇所ある |
start_phase2_display.sh 側も venv python に直す |
| 背景に 2048 が出ない | bgcast 未起動 |
/tmp/bgcast.log に /bg_ingest connected が出ているか確認。NO_BGCAST=1 で無効化も可 |
| GPU が使われない / HIP エラー |
HSA_OVERRIDE_GFX_VERSION が残っている |
gfx1151 + ROCm 7.14 では設定してはいけない。スクリプトで unset
|
| エージェントが context overflow | llama の起動オプション |
-c 65536 --parallel 2(per-slot 32768 が必要) |
| CDP に繋がらない | Chrome プロファイルのロック |
rm -f /tmp/chrome-cdp-2048/SingletonLock して headed 起動 |
まとめ
-
Docker に閉じ込めた部分は OS 更新で壊れない。 依存を画像に焼き込んでおくと、
それがそのままバージョン固定になります。今回 OpenClaw 側は再ビルドすら不要でした。 -
壊れるのはホストの起動スクリプト。 しかも GPU ではなく system python のバージョン。
Ubuntu 26.04 の system python は 3.14 なので、python3 script.pyと書いた箇所は全部候補です。 -
venv は uv で明示バージョンを持たせる。
python3 -m venvは system python に縛られるので、
OS 更新のたびに壊れます。 -
python の選び方は「パス」ではなく「import できるか」で。 実際、その書き方をしていた
bgcast だけ無傷でした。 -
gfx1151 + ROCm 7.14 では
HSA_OVERRIDE_GFX_VERSIONを設定しない。 もう偽装は不要で、
むしろ壊れます。シェル設定に残っている可能性があるのでunsetまで書く。 - 移行後は README をクローンからなぞり直す。 「動いた」と「手順が正しい」は別物です。