NTT DATA TECH
🧸

Windows+GPUでAI環境をコンテナで作ろう!

に公開

生成AIモデルの推論や学習における環境は、Linuxが利用されていることが多いですが、最近はコンシューマで利用されるGeForceなども性能向上し、モデルの推論やファインチューニングなどを行えるようになってきています。

  • ただし、GeForceを利用してモデルの推論や学習を行おうとしたときには、OSがWindowsであることがほとんどだと思います。
  • Windows上でLinuxを動作させる仕組みであるWindows Subsystem for Linux(WSL)とコンテナの仕組みを利用すれば、コンテナを利用して簡単にAI環境を構築することができます。
  • 本稿では、WSLとContainerdを利用したWindows上にGPU利用環境を構築する方法を紹介します。

本記事で得られること:

  1. WSL2上で、containerdを用いてGPUコンテナを構築・実行する具体的な手順
  2. WSL2上のコンテナ内でGPUの有効化が、どのようなアーキテクチャで実現されているかの技術的理解

👉 WSL+kubernetesで環境構築する方法は、続編で説明予定です。


STEP1 : 環境構築

1.1 前提条件

以下、利用するPCの前提を確認してください。

  • Windows 側で NVIDIA Driver(WSL2 GPU サポート対応版)が導入されている必要があること
    • 基本的にドライバは下位互換があるため、最新のバージョンを導入しておくことを推奨します。
  • WSL2 側のカーネル・ディストリビューションが最新であること
    • wsl --update コマンドでカーネルを更新し、利用している Linux ディストリビューションも最新化しておくと安心です。

1.2 必須ツールのインストール

  • containerd & nerdctl
  • CNI (Container Network Interface) Plugin

1.2.1 containerd & nerdctl

nerdctlは、containerdをDockerライクなUIで操作できる便利なCLIツールです。Fullパッケージにはcontainerd本体も同梱されています。

  1. nerdctlの公式リリースページから、nerdctl-full-****-linux-amd64.tar.gzをダウンロードします。

  2. ダウンロードしたファイルをWSL2内でパスの通ったディレクトリ(例: /usr/local/bin)に展開します。

tar Cxzvvf /usr/local nerdctl-full-2.1.5-linux-amd64.tar.gz

1.2.2 CNI (Container Network Interface) Plugin

nerdctl runに必要なのでCNI Pluginもインストールします。

  1. バイナリを取得します。

CNI Pluginの公式リリースページから、cni-plugins-linux-amd64-****.tgzをダウンロードします。

# バージョンは適宜最新版を確認してください
CNI_VERSION="v1.8.0"
wget "https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-amd64-${CNI_VERSION}.tgz"
  1. 取得したファイルを所定のディレクトリに展開します。
sudo mkdir -p /opt/cni/bin
sudo tar -C /opt/cni/bin -xzvvf cni-plugins-linux-amd64-${CNI_VERSION}.tgz

1.2.3 NVIDIA Container Toolkit

ホストのGPUをコンテナに透過的に公開するためのキーコンポーネントです。

公式ガイドのインストール手順に従い、WSL2ディストリビューションに合わせたパッケージをインストールしてください。

  • WSLで実行しているディストリビューション(UbuntuならUbuntu用)に合わせてインストールをしてください。
  • 参考として2025/10/15時点の内容を転記します。(3の手順でのバージョンは適宜最新のバージョンを確認してください)
  1. リポジトリ情報を更新します。
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
  1. リポジトリをUpgradeします。
sudo apt-get update
  1. NVIDIA Container Toolkitのパッケージをインストールします。
export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.17.8-1
  sudo apt-get install -y \
      nvidia-container-toolkit=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      nvidia-container-toolkit-base=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      libnvidia-container-tools=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      libnvidia-container1=${NVIDIA_CONTAINER_TOOLKIT_VERSION}
  1. 以上で終了です。この後の手順ではnerdctlを使うので、設定作業不要です。

STEP 2 実践 :GPUコンテナの起動

nerdctl--gpus all が指定されるとコンテナ生成時にnvidia‑container‑cliが呼ばれ GPU を注入する流れになります。

2.1 GPUコンテナの起動

機械学習ライブラリTensorFlowの公式コンテナイメージを使用して実際に試してみましょう。

sudo nerdctl run --gpus all --rm -it nvcr.io/nvidia/tensorflow:25.02-tf2-py3
  • 【コマンドオプション解説】
    • --gpus all: 利用可能な全てのGPUをコンテナにアタッチするための指示です。
    • 以下は通常のコンテナを利用する際のオプションと同じです。
      • --rm: 検証終了後に、コンテナを自動で削除するように指定しています。
      • -it: 検証作業で実際にコンテナ内でPythonのスクリプトを実行するために指定しています。

2.2 コンテナ内でのGPU認識確認

先ほど起動したコンテナで実際にGPUが検出されるか確認してみましょう。

  1. python3 を実行して対話モードに入ります。
  2. 以下のPythonコードを入力します。
import tensorflow as tf
print(tf.config.list_physical_devices('GPU'))

成功した場合、以下のような出力が得られます。
GPU:0というデバイスがリストにあれば、コンテナはホストのGPUを正しく認識しています。

[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
  • 補足
    • 以下のメッセージは無視して問題ありません。
      • import時 : Unable to register cuFFT/cuDNN/cuBLAS factory
      • could not open file to read NUMA node
        • WSL2 の Linux カーネルが NUMA を expose していないためです。

2.3 トラブルシュート

  • もし空のリスト [] が返された(=GPU が見えていない)場合、以下を確認してください。
    • ホスト(Windows)側: PowerShellで nvidia-smi を実行し、GPUが認識されているか確認します。
    • ゲスト(WSL2)側: WSL2のターミナルで nvidia-smi を実行し、同様にGPUが見えるか確認します。見えない場合は wsl --update を再試行してください。
    • バージョン不整合: エラーログに「driver/library version mismatch」といったメッセージがないか確認します。ホストドライバとコンテナ内のCUDAバージョンが非互換の場合、コンテナイメージのバージョン変更を検討してください。

STEP 3:アーキテクチャ解説 --gpus allはどのように機能するのか?

以降は、興味のある人向けです。

ここまで試してみて、なぜ、--gpus allだけでGPUが利用できるようになったか不思議に思いませんか?

この章では、gpusオプションを指定したときに、各コンポーネントがどのように協調して動作しているかを、次の流れで説明します。

  1. 全体の流れと、各コンポーネントの役割
  2. 各コンポーネントの詳細

3.1 主要なコンポーネントの役割と、全体の流れ

3.1.1 主要なコンポーネントの役割

  • nerdctl : nvidia-container-cliを呼び出す仕掛けを作る。
  • containerd : runcでコンテナのライフサイクル全体を管理します。
  • runc : 実際にコンテナ環境を作成するエンジンです
  • nvidia-container-cli : runcから呼び出されて、コンテナ上でデバイスを使えるようにします。

3.1.2 全体の流れ

  • 補足
    • containerdが受け取った情報をもとにconfig.jsonを作成し、runcを実行するのは、 containerdの通常の動作です。
    • nvidia-container-cliがコンテナのrootfs配下にデバイスファイルや、ライブラリを扱えるようにセットアップしています。

3.2 個別コンポーネント

3.2.1 nerdctlコマンドからcontainerdの呼び出し

  • nerdctlがgrpcクライアントとして、containerdのAPIを呼び出します。

  • その際のパラメータでOCI Specのhook定義に組み立て渡しています。

  • シーケンス図

    • nerdctl内の実装も気になる方は付録を参照ください。関連するコードへのリンクを張っておきます。

3.2.2 runcからnvidia-container-cliの呼び出し

  • runccontainerdが作成したconfig.jsonをもとにコンテナ環境を作成します。
  • その流れの中で呼び出されるnvidia-container-cliがホストにあるGPUや、ライブラリを埋め込みます。
    • OCI Specを書き換えてruncに任せるといった実装ではありません。
    • nvidia-container-cliが直接デバイスファイルの作成や、マウント処理を行っています。
  • oci specを実際に確認する方法
sudo nerdctl container inspect --mode=native tensorflow
  • config.json抜粋

    • この辺りの定義を作成している実装箇所に興味がある方は、最後に関連するコードの抜粋を載せておきますので、参照ください。
    • JSONでは厳密にはコメントはありませんが、説明用に//でコメントを記載しています。
    "hooks": {
      // 公式ドキュメントのアーキテクチャ概要ではpreStartと書かれていますが、
      // 実装はcreateRuntimeになっています。
      // `prestart`は`OCI Runtime Spec`で非推奨になったため実装変更されたようです。
      // hookの補足は付録を参照ください
      "createRuntime": [   
        {
          "path": "/usr/local/bin/containerd",
          "args": [
            "containerd",
            "oci-hook",
            "--",
            // Nvidia Container Toolkitで提供されるコマンドです
            "/usr/bin/nvidia-container-cli", 
            "--load-kmods",
            "configure",
            // nerdctl呼び出し時に設定したオプションに依存します
            "--device=all",
            // utility computeは追加オプションを指定しない場合のデフォルトオプションです
            "--utility",
            "--compute",
            "--pid={{pid}}",
            "{{rootfs}}"
          ],
    

付録

付録1. Hookに関する補足

  • OCI Runtime SpecificationのLifecycleで定義されています。

  • コンテナの作成中などに独自の処理を組み込んだりするのに使われます。

    フックステージ タイミング(OCIライフサイクル中) 実行名前空間 典型的なユースケース
    createRuntime create操作時:名前空間作成後、pivot_root前 ランタイム(ホスト) ホストの状態に基づいてコンテナ設定を変更。
    例:NVIDIA GPUの注入、デバイスのパススルー。
    createContainer create操作時:createRuntime後、pivot_root前 コンテナ ユーザープロセス開始前にコンテナの名前空間内でセットアップ。
    例:vethインターフェースの設定。
    startContainer start操作時:ユーザープロセスのexec直前 コンテナ 完全にマウントされたコンテナファイルシステム内での最終セットアップタスク。
    poststart start操作時:ユーザープロセス開始直後(非ブロッキング) ランタイム(ホスト) コンテナが実行中であることを外部システムに通知。
    poststop delete操作時:コンテナ破棄後 ランタイム(ホスト) コンテナに関連付けられた外部リソースのクリーンアップ。

付録2. nerdctlの実装

  • 関連するソースコード
    • nerdctl/cmd/nerdctl/container/container_run.go
    • processCreateCommandFlagsInRun
    • setPlatformOptions
    • parseGPUOpt
      • GPUオプションの保持する内部構造体を作成する
    • nvidia.WithGPUs
      • conatinerdに渡すためのOCISpecのオプションパラメータに変換する

nerdctl gpusフラグ定義の個所

// フラグの定義個所
// cmd/nerdctl/container/container_run.go`  
cmd.Flags().StringArray("gpus", nil, "GPU devices to add to the container ('all' to pass all GPUs)")
cmd.RegisterFlagCompletionFunc("gpus", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    return []string{"all"}, cobra.ShellCompDirectiveNoFileComp
})

// 取得した`--gpus`フラグの値を`ContainerCreateOptions.GPUs`に格納。
// cmd/nerdctl/container/container_create.go
opt.GPUs, err = cmd.Flags().GetStringArray("gpus")

nerdctl(gpusフラグのオプション解析)

// pkg/cmd/container/run_linux.go
gpuOpt, err := parseGPUOpts(options.GPUs) // GPUsフラグの値が設定されている
opts = append(opts, gpuOpt...)

//pkg/cmd/container/run_gpus.go` 
//`--gpus`オプションの構文(`count=all`, `device=GPU-xxxx`, `capabilities=compute,utility`など)を
//構造体に変換
func ParseGPUOptCSV(value string) (*GPUReq, error) { ... }

// nvidia.WithGPUを呼び出して、構造体に変換する
nvidia.WithGPUs(gpuOpts...), nil

nerdctl実装(構造体への変換)

  • 構造体への変換 nvidia.WithGPUs
  • gpusフラグに渡された文字列をもとにparseGPUOptメソッドでOCI Specのオプションに設定する構造体を生成します。
// github.com/containerd/containerd/v2/contrib/nvidia
// WithGPUs adds NVIDIA gpu support to a container
func WithGPUs(opts ...Opts) oci.SpecOpts {
    return func(_ context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error {
        //省略
        if c.OCIHookPath == "" {
            path, err := exec.LookPath("containerd")
            //省略
            c.OCIHookPath = path
        }
        // nvidia-container-cliのパスを取得する。
        nvidiaPath, err := exec.LookPath(NvidiaCLI)
        //省略
        s.Hooks.CreateRuntime = append(s.Hooks.CreateRuntime, specs.Hook{
            Path: c.OCIHookPath,
            Args: append([]string{
                "containerd",
                "oci-hook",
                "--",
                nvidiaPath,
                // ensures the required kernel modules are properly loaded
                "--load-kmods",
            }, c.args()...),
            Env: os.Environ(),
        })
        return nil
    }
}
NTT DATA TECH
NTT DATA TECH
設定によりコメント欄が無効化されています