2026年9月14日月曜日

michaelfeil/infinity を DGX Spark 向け ARM64 CUDA イメージとしてビルドする

michaelfeil/infinity の ARM64 CUDA イメージのビルド、動作確認を行ったのでその作業を記録する。

なお、この作業記録は Codex が自身の作業を記録したものである。

なお成果物は mikoto2000/infinity at for_dgx_spark に置いた。

目的

DGX Spark 上で、 Infinity を使いたかったので、 ARM 用の Docker イメージを作成したい。

実行環境

  • 作業日: 2026-09-14
  • リポジトリ: Windows F:\project\infinity / WSL /mnt/f/project/infinity
  • WSL2: Ubuntu-24.04、ユーザー mikoto、ホスト linux/amd64
  • ホスト Docker Engine: 29.6.1
  • DinD 内 Docker Engine: 29.8.0
  • 専用 DinD コンテナ: infinity-spark-dind
  • Docker データボリューム: infinity-spark-dind-data
  • ソースは DinD の /workspace に読み取り専用でマウントする。
  • PowerShell のサンドボックスから通常権限では WSL の列挙が Wsl/EnumerateDistros/Service/E_ACCESSDENIED になったが、昇格実行で利用可能と確認した。 前の回答の「Docker がない」は Windows の PATH の確認に限られていた。

参照した仕様

DinD の準備 (WSL2 のシェルで実行)

cd /mnt/f/project/infinity
docker run -d --name infinity-spark-dind --privileged \
  --mount type=volume,source=infinity-spark-dind-data,target=/var/lib/docker \
  --mount type=bind,source=/mnt/f/project/infinity,target=/workspace,readonly \
  --workdir /workspace \
  docker:29-dind dockerd --host=unix:///var/run/docker.sock
docker exec infinity-spark-dind docker info
docker exec infinity-spark-dind docker run --privileged --rm tonistiigi/binfmt --install arm64
docker exec infinity-spark-dind docker buildx create \
  --name infinity-spark-builder --driver docker-container --use --bootstrap
docker exec infinity-spark-dind docker pull --platform linux/arm64 nvcr.io/nvidia/pytorch:25.11-py3

進行状況

  • DinD の作成と ARM64 エミュレーション確認: 成功。
  • default ビルダー (docker driver / BuildKit v0.33.0) も linux/arm64 に対応していた。 以降は取得済みイメージを共有できる --builder default を使用する。
  • Poetry エクスポートと infinity_emb-0.0.77-py3-none-any.whl 作成: 成功。
  • CPU 用の既存テスト4件、Spark 依存関係変換2件、環境分離2件: WSL2 上で合計8件成功。
  • ARM64 依存インストール: 成功 (309.9 秒)。
  • Infinity 本体 0.0.77 のインストール: 成功。
  • 環境分離後の python -m pip check: No broken requirements found. (終了コード0)。
  • ARM64 スモークテスト: 成功 (初回の読み込みを含め 824.1 秒)。 torch の CPU 演算、torchvision の NMS、小規模 BERT forward、Infinity サーバー構築を確認。
  • infinity_emb v2 --help: 成功 (終了コード0)。
  • ARM64 production イメージ: ビルド・DinD 内への保存に成功 (終了コード0)

追加した構成

  • libs/infinity_emb/Dockerfile.spark: Spark 専用。既存の Jinja 生成対象とは別に直接編集する。
  • libs/infinity_emb/docker_spark_prepare.py: GPU 関連パッケージを NVIDIA ベースのバージョンで制約し、その他は既存 Poetry ロックを使用する。
  • libs/infinity_emb/docker_spark_isolate.py: インストール後、アプリ依存と NVIDIA の torch/torchvision/Triton のみを仮想環境から参照できるようにする。
  • libs/infinity_emb/tests/docker/smoke_spark.py: GPU 不要の import、CPU テンソル演算、小規模 BERT forward、サーバー構築確認。
  • libs/infinity_emb/tests/docker/test_spark_prepare.py: NVIDIA パッケージの保持と不適切なベースイメージの拒否を検証。

依存エクスポート、ARM64 wheel のダウンロード、純 Python wheel 作成は BUILDPLATFORM (amd64) で実行し、production の依存インストールと 動作確認は ARM64/QEMU で実行する。 ダウンロードは Python 3.12 / CPython / ARM64 の manylinux タグを明示する。 GPUtil のみソース配布なので、ネイティブ側で py3-none-any wheel を作る。 wheelhouse はビルド時だけマウントし、最終イメージには含めない。 Python 仮想環境 /opt/infinity はインストール時のみ NVIDIA ベースの system site-packages を参照する。 venv --without-pip --system-site-packagespython -m pip を組み合わせ、 ベース付属の pip 25.3 を利用する。通常の venv は QEMU 上の ensurepip に 3分以上かかったため、初回ビルドを中断してこの方式に変更した。 変更後の仮想環境作成は 3.4 秒で完了した。初回ログは spark-build-initial.log。 次の試行では QEMU 内のオンライン pip 解決・インストールに時間がかかり、 1,328 秒時点でもインストール途中だったため中断した (spark-build-online-install.log)。 ARM64 wheel をネイティブ側で事前取得する処理は 23.2 秒で成功した。 ARM64 側では --no-index --find-links /wheelhouse --no-compile を使用する。 バイトコードの事前生成を省くだけで、ARM64 のネイティブ拡張インストールと実行確認は省略しない。 インストール後、必要なベース由来の Python パッケージと dist-info を仮想環境へ シンボリックリンクし、include-system-site-packages = false にする。 これにより NVIDIA の CUDA バイナリを再利用しつつ、NGC の学習・開発用ツールが アプリの依存解決に混ざることを防ぐ。ベースの実ファイルは削除・変更しない。 追加の x86_64 用 Flash Attention wheel はインストールしない。 既定エンジンは torch、BetterTransformer は無効とする。

ビルドコマンド (WSL2)

set -o pipefail
docker exec infinity-spark-dind docker buildx build \
  --builder default --platform linux/arm64 --target production \
  --progress plain -f libs/infinity_emb/Dockerfile.spark \
  -t infinity:spark-arm64 --load libs/infinity_emb \
  2>&1 | tee /mnt/f/project/infinity/spark-build.log

ログは spark-build.logspark-requirements-build.log に保存する (*.log は Git 管理対象外)。 イメージは DinD 内 にロードされる。ホストの docker images には現れない。

取得した NVIDIA ベースのマニフェスト:

  • マルチアーキテクチャ: sha256:417cbf33f87b5378849df37983552cd1f8bc8b62fe1ceabe004de816a55dff21
  • ARM64: sha256:4a85d8cf6fb3a943280960b8948cf4e9b6eca77b4414c68c9b2c7bb863f79b70

厳密に同じベースを使用する場合は、ビルドに以下を追加する。

--build-arg SPARK_BASE_IMAGE=nvcr.io/nvidia/pytorch:25.11-py3@sha256:4a85d8cf6fb3a943280960b8948cf4e9b6eca77b4414c68c9b2c7bb863f79b70

ビルド後の確認コマンド (WSL2)

docker exec infinity-spark-dind docker image inspect infinity:spark-arm64 \
  --format '{{.Os}}/{{.Architecture}} {{.Id}}'
docker exec infinity-spark-dind docker run --rm --platform linux/arm64 \
  --entrypoint python infinity:spark-arm64 /app/smoke_spark.py
docker exec infinity-spark-dind docker run --rm --platform linux/arm64 \
  infinity:spark-arm64 v2 --help

DGX Spark への転送・GPU 確認の手順

WSL2 側でイメージをエクスポートし、gzip 圧縮した tar を Spark に転送する。 大きいイメージなので、出力先の空き容量を確保する。

set -o pipefail
docker exec infinity-spark-dind docker save infinity:spark-arm64 | gzip -1 > infinity.tar.gz

Spark 上で実行する。

docker load -i infinity.tar.gz
docker run --rm --gpus all --entrypoint python infinity:spark-arm64 \
  -c 'import torch; print(torch.__version__, torch.version.cuda); print(torch.cuda.get_device_name(0)); x = torch.ones(4, device="cuda"); print(x + 1)'
docker run --rm --gpus all -p 7997:7997 \
  -v infinity-spark-models:/app/.cache/huggingface \
  infinity:spark-arm64 v2 --model-id BAAI/bge-small-en-v1.5 --engine torch --device cuda --port 7997

最後のコマンドはモデルをダウンロードして API を起動する。 GPU 演算・実モデル推論・性能・Flash Attention・TensorRT・CTranslate2 の GPU 実行は今回の確認範囲に含まれない。

ベースイメージの依存チェック

docker exec infinity-spark-dind docker run --rm --platform linux/arm64 \
  --entrypoint python nvcr.io/nvidia/pytorch:25.11-py3 -m pip check

Infinity 追加前から以下の1件が存在した (終了コード1)。

nvidia-resiliency-ext 0.4.1+cuda13 requires pynvml, which is not installed.

ログ: spark-base-pip-check.log

さらに pip inspect のメタデータ (spark-base-inspect.log) とアプリのロックを比較し、 以下の衝突を確認した。これらの開発用パッケージをアプリ環境から分離する理由となった。

ベース内のパッケージ 要求 アプリのロック
tyro typing-extensions >= 4.13 4.12.2
typeguard typing-extensions >= 4.14 4.12.2
torchtitan datasets >= 2.21 2.14.4
lightning-thunder networkx >= 3.3 3.2.1
lightning-thunder dill >= 0.3.8 0.3.7

実インストール時には、NGC の httpcore 1.0.9 とアプリ側の h11 0.14.0 の 衝突も検出された。httpcore はアプリの実行依存ではないため、同じ分離処理で解消した。 一時的に system site-packages を共有しているインストール途中のログにはこれらの 依存警告が出るが、最終環境では pip check により不整合がないことを確認した。

GPU スタックを新しい PyPI torch で置き換えたり、アプリのロック全体を更新する処理は行わない。

ARM64 スモークテストの実測結果

architecture: aarch64
torch: 2.10.0a0+b558c986e8.nv25.11
torchvision: 0.25.0a0+7a13ad0f
cuda_build: 13.0
sentence_transformers: 3.3.1
onnxruntime: 1.19.2
gpu_execution_tested: False

torch.backends.cuda.is_built() も真であることを確認した。 GPU を必要とする演算は実行していない。

初回スモークテストは QEMU 上で多数のモジュールを読み込むため約14分かかった。 途中で生成される .pyc とロード済み ARM64 共有ライブラリを確認し、停止ではなく 初期化が進行していることを確認した。 既存コードの torch.backends.*.allow_tf32 に PyTorch 2.10 の非推奨警告が出たが、 演算とスモークテストは成功した。TF32 設定 API の更新は今回の変更に含めない。 スモークテストと別プロセスでの CLI ヘルプ確認を合わせたステージは 1,145.3 秒だった。 この実測は amd64 上の QEMU であり、DGX Spark ネイティブ実行の性能を表すものではない。

完成したイメージ

  • タグ: infinity:spark-arm64
  • 保存先: WSL2 の infinity-spark-dind コンテナ内の Docker
  • OS / Architecture: linux/arm64 (docker image inspect で確認)
  • Docker が報告したイメージ ID: sha256:578dd3cebf7b75a06cf017428ec55ecfdc1534b66dc2d153e66c57819c3cde1c
  • ARM64 イメージマニフェスト: sha256:94b2018bb094a357afe043e0ab415ba7f78b303ebb13d3c2232185af66ea0199
  • docker image inspect の Size: 9725644356 bytes
  • 作成時刻: 2026-09-14T08:42:26.723536977Z (日本時間 17:42)
  • ENTRYPOINT: ["infinity_emb"]
  • インストール一覧: イメージ内 /app/installed-packages.txt。 完成イメージから取り出した一覧を spark-installed-packages.log に保存した。 pip freeze 出力なので、NVIDIA パッケージにはビルド元のローカル wheel URL が含まれる。 再インストール用 requirements としてではなく構成記録として使用する。

ビルド中にスモークテストと CLI の成功を確認したため、完成後に同じ重いテストは 繰り返さず、イメージ属性と保存済みパッケージ一覧を確認した。

DinD コンテナと専用データボリュームは再利用できるよう残している。 WSL2 再起動などで停止した場合は、以下で再開できる。

docker start infinity-spark-dind
docker exec infinity-spark-dind docker image inspect infinity:spark-arm64

Docker Hub / NGC などへのイメージ push は行っていない。Spark への転送結果は末尾に記載。

gzip 圧縮したイメージの保存 (2026-09-14 実施済み)

ユーザーの指定により infinity:spark-arm64docker save で書き出した。 WSL2 の Bash 内で以下を実行し、Windows PowerShell のバイナリパイプを経由せずに保存した。

set -o pipefail
set -o noclobber
docker exec infinity-spark-dind docker save infinity:spark-arm64 \
  | gzip -1 > /mnt/f/project/infinity/infinity.tar.gz.partial
gzip -t /mnt/f/project/infinity/infinity.tar.gz.partial

両コマンドの終了コードが 0 であることを確認後、一時ファイルを同じディレクトリの infinity.tar.gz にリネームした。

  • 保存先: F:\project\infinity\infinity.tar.gz
  • ファイルサイズ: 9679762352 bytes (約 9.68 GB / 9.02 GiB)
  • 検証: gzip -t による圧縮ファイル全体の整合性チェック成功
  • docker save 単体の出力は .tar。今回は gzip 圧縮したため、指定の .tar.gz が正しい。
  • Spark での読み込み: docker load -i infinity.tar.gz

DGX Spark への SCP 転送 (2026-09-14 実施済み)

ユーザーの指定により Windows OpenSSH の scp で転送した。

scp -o BatchMode=yes -o ConnectTimeout=15 F:/project/infinity/infinity.tar.gz mikoto@gx10-707e.local:~/infinity.tar.gz
ssh -o BatchMode=yes -o ConnectTimeout=15 mikoto@gx10-707e.local 'stat -c "%s %n" ~/infinity.tar.gz'

scp は終了コード 0。転送先 /home/mikoto/infinity.tar.gz のサイズが 9679762352 bytes であり、転送元と一致することを確認した。

(筆者追記)DGX Spark での GPU 実行確認

Codex にイメージファイルの転送までしてもらったので、それ以降の動作確認は自分で行った。

DGX Spark 上で、gzip 圧縮したイメージをロードし、GPU を使用して Infinity サーバーを起動した。

docker load -i ./infinity.tar.gz
docker run -it --rm \
    --gpus all \
    -p 8081:8081 \
    -v models:/app/.cache 
    infinity:spark-arm64 v2 \
        --model-id BAAI/bge-m3 \
        --model-id BAAI/bge-reranker-v2-m3 \
        --engine torch \
        --device cuda \
        --port 8081

curl でモデル一覧取得 API を呼び出し、サーバーが正しく起動していることを確認した。

curl http://localhost:8081/models

以上。

参考資料

0 件のコメント:

コメントを投稿