1. はじめに

GPU はもともとグラフィックス処理に使われていましたが、その後、汎用 GPU 計算(General Purpose GPU, GPGPU)技術の登場により、一般計算にも広く使われるようになりました。GPU アーキテクチャは非常に多くのスレッドを持つため、プログラム実行時に大量の数値演算を GPU に任せ、ロジック部分を CPU に任せる、といった使い方が一般的です。NVIDIA は GPGPU の概念を最初に提唱した企業であり、CUDA 技術を提案しました。これにより開発者は、C に近い文法で GPU を駆動して計算を行えます。

2. CUDA のインストール

CUDA には多くのバージョンがあります。単に apt install cuda を実行しても、必ずしも必要なバージョンが入るとは限りません。

CUDA のプログラム開発だけでなく、PyTorch や TensorFlow のために CUDA を入れたい場合もあります。その場合は、NVIDIA の公式サイトで必要なバージョンを選ぶのがよいです。ただし CUDA Toolkit Archive のページからバージョンを選ぶことを忘れないでください。そうしないと、NVIDIA のサイトは最新バージョンへ誘導してきます。

以下の手順に従ってインストールできます。

2.1 CUDA のインストール

ここでは CUDA 11.0 を例にします。まず Archive ページに行き、11.0 を選択し、自分の環境に合わせてオプションを選びます。

CUDA 11.0 インストール例

たとえば上の例では x86 Ubuntu 20.04 を選択しており、runfile、deb local、deb network を選べます。

3 つの違いは、それぞれ「大きなインストーラ一式」「deb にまとめた大きなインストーラ」「ネットワークからダウンロードしてインストールする deb」です。deb かどうかの違いは、後からパッケージマネージャで管理できるかどうかに影響します。

私は通常 deb local を選びますが、好みに合わせて選んでください。

選択が終わると、親切にも長いコマンド列が提示されます。基本的にはその通りに実行すればインストールが完了します。

CUDA 11.0 Ubuntu 20.04 x86 のインストール手順:

wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-ubuntu2004.pin
sudo mv cuda-ubuntu2004.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/11.0.3/local_installers/cuda-repo-ubuntu2004-11-0-local_11.0.3-450.51.06-1_amd64.deb
sudo dpkg -i cuda-repo-ubuntu2004-11-0-local_11.0.3-450.51.06-1_amd64.deb
sudo apt-key add /var/cuda-repo-ubuntu2004-11-0-local/7fa2af80.pub
sudo apt-get update
sudo apt-get -y install cuda

dpkg -i で問題がある場合は、代わりに apt ./xxx.deb を使っても構いません。

2.2 cuDNN のインストール

ついでに cuDNN もインストールできます。cuDNN は CUDA の深層学習向けに最適化された関数ライブラリで、PyTorch を使う場合は必要になります。OS に合わせてリンクを調整してください。たとえば ubuntu18.04 は Ubuntu 18 を意味します。

$ sudo bash -c 'echo "deb http://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64 /" > /etc/apt/sources.list.d/cuda_learn.list'
$ sudo apt install libcudnn7

2.3 OpenCL のインストール

CUDA を入れたなら、基本的に OpenCL も一緒に入れておくとよいです。NVIDIA GPU の OpenCL は CUDA を利用するため、これで後から OpenCL を実行する際にもそのまま使えます。

$ sudo apt install -y nvidia-opencl-dev
$ sudo apt install opencl-headers

2.4 システム設定

まず 再起動 してください。これはとても重要です!!!!

インストールが完了したらパス設定も忘れずに行いましょう。~/.bashrc に次を追加します。

export PATH=$PATH:/usr/local/cuda/bin
export CUDADIR=/usr/local/cuda

2.5 インストール確認

インストールが正しくできたか確認します。

CUDA が正常に入っていれば、次のコマンドはいずれも反応するはずです。

$ nvidia-smi  # Driver 
$ nvcc --version # CUDA
$ /sbin/ldconfig -N -v $(sed 's/:/ /' <<< $LD_LIBRARY_PATH) 2>/dev/null | grep libcudnn # CuDNN

2.6 その他

上の手順でうまくいかなかった場合は、次の記事も参考になります:

3. CUDA プログラム例

CUDA を学ぶ最良の方法は、公式ドキュメントの CUDA C++ Programming Guide を読むことです。NVIDIA 自身の製品なので、やはり一次情報が一番です。また、Gerassimos による Multicore and GPU Programming: An Integrated Approach という書籍も良く、入門に向いています。

ここでは簡単な例を示します:

matadd.cu

#include <stdio.h>
#include <cuda.h>
#include <cuda_runtime.h>

#define N 512
#define BLOCK_SIZE 16

// GPU 的 Kernel
__global__ void MatAdd(float *A, float *B, float *C)
{
    // 根據 CUDA 模型,算出當下 thread 對應的 x 與 y
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    int j = blockIdx.y * blockDim.y + threadIdx.y;

    // 換算成線性的 index
    int idx = j * N + i;

    if (i < N && j < N)
    {
        C[idx] = A[idx] + B[idx];
    }
}

int main()
{

    float *h_A, *h_B, *h_C;
    float *d_A, *d_B, *d_C;

    int i;

    // 宣告 Host 記憶體 (線性)
    h_A = (float *)malloc(N * N * sizeof(float));
    h_B = (float *)malloc(N * N * sizeof(float));
    h_C = (float *)malloc(N * N * sizeof(float));

    // 初始化 Host 的數值
    for (i = 0; i < (N * N); i++)
    {
        h_A[i] = 1.0;
        h_B[i] = 2.0;
        h_C[i] = 0.0;
    }

    // 宣告 Device (GPU) 記憶體
    cudaMalloc((void **)&d_A, N * N * sizeof(float));
    cudaMalloc((void **)&d_B, N * N * sizeof(float));
    cudaMalloc((void **)&d_C, N * N * sizeof(float));

    // 將資料傳給 Device
    cudaMemcpy(d_A, h_A, N * N * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, N * N * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_C, h_C, N * N * sizeof(float), cudaMemcpyHostToDevice);

    dim3 blockSize(BLOCK_SIZE, BLOCK_SIZE);
    dim3 numBlock(N / BLOCK_SIZE, N / BLOCK_SIZE);

    // 執行 MatAdd kernel
    MatAdd<<<numBlock, blockSize>>>(d_A, d_B, d_C);

    // 等待 GPU 所有 thread 完成
    cudaDeviceSynchronize();

    // 將 Device 的資料傳回給 Host
    cudaMemcpy(h_C, d_C, N * N * sizeof(float), cudaMemcpyDeviceToHost);

    // 驗證正確性
    for (i = 0; i < (N * N); i++)
    {
        if (h_C[i] != 3.0)
        {
            printf("Error:%f, idx:%d\n", h_C[i], i);
            break;
        }
    }

    printf("PASS\n");

    // free memory

    free(h_A);
    free(h_B);
    free(h_C);

    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);

    return 0;
}

GPU 計算では、最小の実行単位を Kernel と呼び、GPU 上の各スレッドが実行する関数に相当します。そのため __global__ void MatAdd() という関数を宣言しています。__global__ は GPU 関数であることをコンパイラに伝える指定で、MatAdd() は A と B の行列を同じ index で足し合わせて C に書き込むだけです。

データは Host 側と Device 側に分かれます。Host は CPU、Device は GPU を指します。Host のメモリ確保は通常の malloc ですが、GPU のメモリ確保は cudaMalloc を使います。

計算を GPU に移したいので、Host 側のデータは cudaMemcpy で先に GPU に転送する必要があります。その後 MatAdd<<<numBlock, blockSize>>> を実行し、計算が終わったら再び cudaMemcpy で GPU のデータを CPU に戻して、Host 側で利用します。

Kernel 起動時の MatAdd<<<numBlock, blockSize>>> という記法は、GPU のスレッド階層に関係しています。

GPU thread grid

上図の通り、GPU には多くの Block があり、各 Block に多くの Thread があります。そのため Kernel を実行する際は、Block 数と Block 内の Thread 数を指定します。

大まかに理解できたら、コンパイルして実行します。

$ nvcc matadd.cu; ./a.out
PASS

実行中に別ウィンドウで nvidia-smi を叩けば、matadd の GPU 使用状況を確認できます。

また nvprof で CUDA の性能を見てみることもできます:

$ nvprof ./a.out
==27161== NVPROF is profiling process 27161, command: ./a.out
PASS
==27161== Profiling application: ./a.out
==27161== Profiling result:
            Type  Time(%)      Time     Calls       Avg       Min       Max  Name
 GPU activities:   70.06%  263.96us         3  87.986us  87.581us  88.285us  [CUDA memcpy HtoD]
                   21.55%  81.181us         1  81.181us  81.181us  81.181us  [CUDA memcpy DtoH]
                    8.40%  31.647us         1  31.647us  31.647us  31.647us  MatAdd(float*, float*, float*)
      API calls:   98.65%  133.57ms         3  44.524ms  3.6540us  133.50ms  cudaMalloc
                    0.78%  1.0564ms         4  264.10us  169.89us  383.89us  cudaMemcpy
                    0.19%  256.03us         3  85.342us  20.249us  148.41us  cudaFree
                    0.14%  187.38us         1  187.38us  187.38us  187.38us  cuDeviceTotalMem
                    0.13%  169.85us        97  1.7510us     193ns  69.776us  cuDeviceGetAttribute
                    0.07%  98.576us         1  98.576us  98.576us  98.576us  cudaDeviceSynchronize
                    0.02%  29.226us         1  29.226us  29.226us  29.226us  cudaLaunchKernel
                    0.02%  26.508us         1  26.508us  26.508us  26.508us  cuDeviceGetName
                    0.00%  4.0950us         1  4.0950us  4.0950us  4.0950us  cuDeviceGetPCIBusId
                    0.00%  1.3720us         3     457ns     266ns     811ns  cuDeviceGetCount
                    0.00%  1.0800us         2     540ns     192ns     888ns  cuDeviceGet
                    0.00%     365ns         1     365ns     365ns     365ns  cuDeviceGetUuid

上の結果を見ると、大半の時間がデータ転送に費やされていることが分かります。これは妥当で、計算自体がとても単純なため、オーバーヘッドはメモリ転送に支配されます。

4. 結論

本記事では CUDA の開発環境の設定を簡単に紹介し、シンプルな CUDA プログラム例を解説しました。GPGPU は大量計算において計算を効率的に高速化でき、数値計算ライブラリや機械学習・深層学習フレームワークの多くも、GPGPU を用いて計算を加速しています。