Docker コンテナで NVIDIA GPU によるハードウェアエンコードを動かす

## 目的

NVIDIA GPU による高速なハードウェアエンコードを Docker コンテナで動かしたい。

## 前提条件

- Docker コンテナで NVIDIA GPU を使用するには、ホストマシンに NVIDIA GPU が搭載されている必要がある。
- 今回はホストマシンとして [NVIDIA Tesla M60](https://www.nvidia.co.jp/object/tesla-m60-jp.html) を搭載した [Amazon EC2 G3 インスタンス](https://aws.amazon.com/jp/ec2/instance-types/g3/) を使用した。
- ホスト OS は [Amazon Linux 2](https://aws.amazon.com/jp/amazon-linux-2/) を選択、NVIDIA GPU のドライバは執筆当時の最新版を使用した。

## ホストマシンの準備

まずは Docker コンテナを動作させるホストマシンとなる Amazon EC2 インスタンスを用意する。

1. Amazon EC2 G3 インスタンスを起動
    - AMI (Amazon Machine Image) は **Amazon Linux 2 AMI** `ami-0a2de1c3b415889d2` を選択
    - インスタンスタイプは G3 インスタンスの中では比較的安価な `g3.4xlarge` や `g3s.xlarge` を推奨
    - EBS ルートボリュームは 8 GB だと空き容量が不足する可能性があるため注意
2. 起動した EC2 インスタンスに SSH 接続
3. 最初にタイムゾーンを設定

    ```bash
    sudo timedatectl set-timezone Asia/Tokyo
    ```
4. Docker のインストール
    Amazon Linux 2 では *Extras Library* を使用することで Docker を簡単にインストールすることができる。

    ```bash
    sudo amazon-linux-extras install docker
    ```
5. NVIDIA Graphics Driver のインストール
    最新のドライバは https://www.nvidia.com/Download/index.aspx で Tesla M60 を検索することで取得可能。
    なお、NVIDIA Graphics Driver の使用には [NVIDIA Software License Agreement](https://www.nvidia.com/content/DriverDownload-March2009/licence.php?lang=us) への同意が必要である。 

    ```bash
    sudo yum install gcc kernel-devel-$(uname -r)
    wget http://us.download.nvidia.com/tesla/410.72/NVIDIA-Linux-x86_64-410.72.run
    sudo bash NVIDIA-Linux-x86_64-410.72.run --ui=none --no-questions --accept-license
    ```
6. `nvidia-docker2` のインストール
    [nvidia-docker2](https://github.com/NVIDIA/nvidia-docker) は Docker コンテナで NVIDIA GPU を使用するために必要なランタイムである。

    ```bash
    distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
    curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.repo | sudo tee /etc/yum.repos.d/nvidia-docker.repo
    sudo yum install nvidia-docker2
    ```
7. Docker コンテナの起動確認
    先にインストールしたランタイムを指定して `nvidia/cuda` コンテナを起動する。
    コンテナ内部で実行する `nvidia-smi` は NVIDIA GPU の情報を取得するためのコマンドである。

    ```bash
    sudo systemctl start docker
    sudo docker run --runtime=nvidia nvidia/cuda nvidia-smi
    ```
    以下のように NVIDIA GPU の情報が表示されれば成功。

    ```
    +-----------------------------------------------------------------------------+
    | NVIDIA-SMI 410.72       Driver Version: 410.72       CUDA Version: 10.0     |
    |-------------------------------+----------------------+----------------------+
    | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
    | Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
    |===============================+======================+======================|
    |   0  Tesla M60           Off  | 00000000:00:1E.0 Off |                  N/A |
    | N/A   29C    P0    40W / 150W |      0MiB /  7618MiB |     97%      Default |
    +-------------------------------+----------------------+----------------------+
                                                                               
    +-----------------------------------------------------------------------------+
    | Processes:                                                       GPU Memory |
    |  GPU       PID   Type   Process name                             Usage      |
    |=============================================================================|
    |  No running processes found                                                 |
    +-----------------------------------------------------------------------------+
    ```

## FFmpeg のビルド

次に Docker コンテナで動かす [FFmpeg](https://www.ffmpeg.org/) のビルドを行う。ビルドも Docker コンテナ内部で行う。

1. 作業用コンテナの起動
    `nvidia/cuda:10.0-devel-centos7` コンテナを起動しシェルに接続する。

    ```bash
    sudo docker run --runtime=nvidia -it nvidia/cuda:10.0-devel-centos7 /bin/bash
    ```
2. 必要なパッケージのインストール

    ```bash
    yum install autoconf bzip2 git make
    ```
3. `nasm` のインストール

    ```bash
    curl -fsSLO https://www.nasm.us/pub/nasm/releasebuilds/2.14/nasm-2.14.tar.bz2
    tar xjvf nasm-2.14.tar.bz2
    cd nasm-2.14
    ./autogen.sh
    ./configure
    make -j$(nproc)
    make install
    ```
4. `ffnvcodec` のインストール
    FFmpeg 4.0 以降 [NVIDIA Video Codec SDK](https://developer.nvidia.com/nvidia-video-codec-sdk) のヘッダファイルが FFmpeg 本体に付属されなくなった。

    ```bash
    git clone https://git.videolan.org/git/ffmpeg/nv-codec-headers
    cd nv-codec-headers
    make install
    ```
5. FFmpeg 本体のビルド

    ```bash
    export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
    curl -fsSLO https://ffmpeg.org/releases/ffmpeg-4.1.tar.bz2
    tar xjvf ffmpeg-4.1.tar.bz2
    cd ffmpeg-4.1
    ./configure --enable-nvenc
    make -j$(nproc)
    make install
    ```

## ハードウェアエンコードの実行

ビルドした FFmpeg を使ってハードウェアエンコードを行う。エンコードはより軽量な新しいコンテナで行う。

1. エンコード実行用コンテナの起動
    環境変数 `NVIDIA_DRIVER_CAPABILITIES=video` を設定することで、ホストマシンにインストールされた NVIDIA GPU ドライバを構成するライブラリのうち NVENC に関連するものをコンテナ内部から使用できるようになる。
    `nvidia/cuda:10.0-runtime` イメージは `nvidia/cuda:10.0-devel` に比べて含まれるライブラリの種類が少ない代わりにイメージサイズが小さい。ビルド済みの CUDA アプリケーションを実行する場合はこちらを使用する。

    ```bash
    sudo docker run --runtime=nvidia -it -e 'NVIDIA_DRIVER_CAPABILITIES=video' nvidia/cuda:10.0-runtime-centos7 /bin/bash
    ```
2. 前節でビルドした FFmpeg 本体 `/usr/local/bin/ffmpeg` を新しいコンテナにもコピーする。
    いったんコンテナから `exit` してから `docker cp` コマンドによりコンテナ内部のファイルを操作できる。

    ```bash
    sudo docker cp <source-container-id>:/usr/local/bin/ffmpeg <target-container-id>:/usr/local/bin/ffmpeg
    ```
3. FFmpeg コマンドの実行
    再び新しいコンテナに `docker attach` してから、適当な動画ファイル input.mp4 を用意する。
    これに対し NVENC によるハードウェアエンコードを行うには、以下の `ffmpeg` コマンドを実行する。

    ```bash
    /usr/local/bin/ffmpeg -i input.mp4 -vcodec h264_nvenc -acodec aac output.mp4
    ```

## Docker コンテナイメージの作成

NVENC に対応した FFmpeg が最初から使用可能な新しい Docker コンテナイメージ `example/nvenc` を作成する。完成したコンテナイメージを [Docker Hub](https://hub.docker.com/) に公開すれば、誰でも `docker pull` できるようになる。

1. Dockerfile の作成
    Dockerfile はコンテナイメージのレシピを記述したものである。
    以下の内容を `Dockerfile` という名前で作成する。`Dockerfile` の記法についてはここでは割愛する。

    ```Dockerfile
    FROM nvidia/cuda:10.0-devel-centos7 as builder

    ENV NASM_VERSION 2.14
    ENV NVCODEC_VERSION 8.2.15.6
    ENV FFMPEG_VERSION 4.1

    RUN yum install -y autoconf bzip2 git make

    RUN curl -fsSLO https://www.nasm.us/pub/nasm/releasebuilds/$NASM_VERSION/nasm-$NASM_VERSION.tar.bz2 \
      && tar -xjf nasm-$NASM_VERSION.tar.bz2 \
      && cd nasm-$NASM_VERSION \
      && ./autogen.sh \
      && ./configure \
      && make -j$(nproc) \
      && make install

    RUN git clone -b n$NVCODEC_VERSION --depth 1 https://git.videolan.org/git/ffmpeg/nv-codec-headers \
      && cd nv-codec-headers \
      && make install

    ENV PKG_CONFIG_PATH /usr/local/lib/pkgconfig
    RUN curl -fsSLO https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VERSION.tar.bz2 \
      && tar -xjf ffmpeg-$FFMPEG_VERSION.tar.bz2 \
      && cd ffmpeg-$FFMPEG_VERSION \
      && ./configure --enable-nvenc \
      && make -j$(nproc) \
      && make install


    FROM nvidia/cuda:10.0-runtime-centos7

    ENV NVIDIA_VISIBLE_DEVICES all
    ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility

    COPY --from=builder /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
    COPY --from=builder /usr/local/bin/ffprobe /usr/local/bin/ffprobe
    ```
2. Docker コンテナイメージのビルド
    `Dockerfile` と同じディレクトリで以下のコマンドを実行するとコンテナイメージのビルドが開始される。

    ```bash
    sudo docker build -t example/nvenc .
    ```
3. Docker コンテナの起動確認
    作成した `example/nvenc` コンテナを起動し、`ffmpeg` コマンドが使用可能であることを確認する。

    ```bash
    sudo docker run --runtime=nvidia -it example/nvenc /bin/bash
    /usr/local/bin/ffmpeg
    ```
4. コンテナイメージを Docker Hub に公開
    あらかじめ Docker Hub のアカウントを作成し [Create Repository](https://hub.docker.com/add/repository/) からリポジトリを作成しておく必要がある。

    ```bash
    sudo docker login
    sudo docker push example/nvenc
    ```

なお、今回作成した Docker コンテナイメージは https://hub.docker.com/r/clerk67/nvenc/ にて公開している。