簡単なCUDAプログラミングを試してみます。
【入門】CUDAプログラミングを試してみる
CUDAプログラミングの入門として、簡単なプログラムの実装例と、その実行例をご紹介していきます。挙動を確認することで、CUDAプログラミングへの理解を深められたらと思います。
目次
0.環境構築
今回のプログラムを動かすためには、以下をインストールする必要があります。- gcc
- cmake
- nvidia-cuda-toolkit
- (NVIDIA Nsight Systems) ←プロファイルするためのツール
(Dockerを使う場合)
########## Pull ########## FROM nvidia/cuda:11.1.1-base-ubuntu20.04 ########## Non-interactive ########## ENV DEBIAN_FRONTEND=noninteractive ########## CUDA ########## RUN apt-get update && \ apt-get install -y \ build-essential \ cmake \ nvidia-cuda-toolkit && \ apt-get remove -y gcc && \ ln -s /usr/bin/gcc-8 /usr/bin/gcc && \ ln -s /usr/bin/g++-8 /usr/bin/g++ && \ ln -s /usr/bin/gcc-8 /usr/bin/cc && \ ln -s /usr/bin/g++-8 /usr/bin/c++ && \ cd ~/ && \ wget https://developer.download.nvidia.com/devtools/repos/ubuntu2004/amd64/nsight-systems-2022.2.1_2022.2.1.31-1_amd64.deb && \ apt-get install -y ./nsight-systems-2022.2.1_2022.2.1.31-1_amd64.deb
1.Hello World!をprintf
まずは、GPUを使って、並列で「Hello World!」と表示させてみます。実装
#include <stdio.h>
__global__ void helloWorld()
{
printf("Hello World!\n");
}
int main()
{
const size_t num_blocks = 1;
const size_t num_threads_per_block = 3;
helloWorld<<<num_blocks, num_threads_per_block>>>();
cudaDeviceSynchronize();
}
ビルド
$ nvcc ../src/hello_world.cu -o hello_worldなお、他のプログラムも同様にビルドできるため、これ以降ではビルド方法の記載を省略します。
実行
$ ./hello_worldなお、他のプログラムも同様に実行できるため、これ以降では実行方法の記載を省略します。
出力
Hello World! Hello World! Hello World!
NG例
以下のようにcudaDeviceSynchronize()を書かないと、同期される前にプログラムが終了してしまいます。#include <stdio.h>
__global__ void helloWorld()
{
printf("Hello World!\n");
}
int main()
{
const size_t num_blocks = 1;
const size_t num_threads_per_block = 3;
helloWorld<<<num_blocks, num_threads_per_block>>>();
// cudaDeviceSynchronize();
}
実行結果は、以下のように何も出力されません。
2.インデックスをprintf
並列プロセスの各インデックスを表示させてみます。実装
#include <stdio.h>
__global__ void printIndicies()
{
int index = threadIdx.x + blockIdx.x * blockDim.x;
printf("index = %d\n", index);
}
int main()
{
const size_t num_blocks = 2;
const size_t num_threads_per_block = 3;
printIndicies<<<num_blocks, num_threads_per_block>>>();
cudaDeviceSynchronize();
}
出力
index = 0 index = 1 index = 2 index = 3 index = 4 index = 5
3.配列の各要素をprintf
配列の各要素にアクセスして値を表示させてみます。実装
#include <stdio.h>
void printArrayCPU(int* arr, size_t num_elements)
{
for(size_t i = 0; i < num_elements; i++){
printf("arr[%d] = %d\n", i, arr[i]);
}
}
__global__ void printArray(int* arr, size_t num_elements)
{
int index = threadIdx.x + blockIdx.x * blockDim.x;
printf("arr[%d] = %d\n", index, arr[index]);
}
int main()
{
const size_t num_blocks = 2;
const size_t num_threads_per_block = 3;
const size_t num_elements = num_blocks * num_threads_per_block;
int* arr;
size_t bytes = num_elements * sizeof(int);
cudaMallocManaged(&arr, bytes);
for(size_t i = 0; i < num_elements; i++) arr[i] = i;
printf("----- CPU -----\n");
printArrayCPU(arr, num_elements);
printf("----- GPU -----\n");
printArray<<<num_blocks, num_threads_per_block>>>(arr, num_elements);
cudaDeviceSynchronize();
}
出力
----- CPU ----- arr[0] = 0 arr[1] = 1 arr[2] = 2 arr[3] = 3 arr[4] = 4 arr[5] = 5 ----- GPU ----- arr[0] = 0 arr[1] = 1 arr[2] = 2 arr[3] = 3 arr[4] = 4 arr[5] = 5
NG例
以下のように、cudaMallocManaged()でメモリを確保しておかないと、GPUでそのメモリにアクセスできません。#include <stdio.h>
void printArrayCPU(int* arr, size_t num_elements)
{
for(size_t i = 0; i < num_elements; i++){
printf("arr[%d] = %d\n", i, arr[i]);
}
}
__global__ void printArray(int* arr, size_t num_elements)
{
int index = threadIdx.x + blockIdx.x * blockDim.x;
printf("arr[%d] = %d\n", index, arr[index]);
}
int main()
{
const size_t num_blocks = 2;
const size_t num_threads_per_block = 3;
const size_t num_elements = num_blocks * num_threads_per_block;
int* arr;
size_t bytes = num_elements * sizeof(int);
arr = (int *)malloc(bytes);
for(size_t i = 0; i < num_elements; i++) arr[i] = i;
printf("----- CPU -----\n");
printArrayCPU(arr, num_elements);
printf("----- GPU -----\n");
printArray<<<num_blocks, num_threads_per_block>>>(arr, num_elements);
cudaDeviceSynchronize();
}
実行結果は、以下のように、GPUプロセスは出力されません。
----- CPU ----- arr[0] = 0 arr[1] = 1 arr[2] = 2 arr[3] = 3 arr[4] = 4 arr[5] = 5 ----- GPU -----
4.Grid-stride loopで配列の各要素をprintf
Grid-stride loopというテクニックで、配列の各要素にアクセスして値を表示させてみます。実装
#include <stdio.h>
__global__ void printArray(int* arr, size_t num_elements)
{
int index = threadIdx.x + blockIdx.x * blockDim.x;
int grid_stride = gridDim.x * blockDim.x;
for(int i = index; i < num_elements; i += grid_stride){
printf("arr[%d] = %d\n", i, arr[i]);
}
}
int main()
{
const size_t num_blocks = 2;
const size_t num_threads_per_block = 3;
const size_t num_elements = 10;
int* arr;
size_t bytes = num_elements * sizeof(int);
cudaMallocManaged(&arr, bytes);
for(size_t i = 0; i < num_elements; i++) arr[i] = i;
printArray<<<num_blocks, num_threads_per_block>>>(arr, num_elements);
cudaDeviceSynchronize();
}
出力
arr[0] = 0 arr[1] = 1 arr[2] = 2 arr[3] = 3 arr[4] = 4 arr[5] = 5 arr[6] = 6 arr[7] = 7 arr[8] = 8 arr[9] = 9
5.デバイス情報をprintf
デバイス情報を表示させてみます。実装
#include <stdio.h>
__global__ void helloWorld()
{
printf("Hello World!\n");
}
int main()
{
int device_id;
cudaGetDevice(&device_id);
cudaDeviceProp props;
cudaGetDeviceProperties(&props, device_id);
printf("Device ID: %d\n", device_id);
printf("Number of SMs: %d\n", props.multiProcessorCount);
printf("Compute Capability Major: %d\n", props.major);
printf("Compute Capability Minor: %d\n", props.minor);
printf("Warp Size: %d\n", props.warpSize);
}
出力
Device ID: 0 Number of SMs: 28 Compute Capability Major: 8 Compute Capability Minor: 6 Warp Size: 32
6.グリッドサイズの決め方
グリッドサイズは、SMの数の倍の数が良いとされています。実装
#include <stdio.h>
__global__ void hoge()
{
int hoge = 1 + 1;
}
int main()
{
/*device query*/
int device_id;
cudaGetDevice(&device_id);
int num_sm;
cudaDeviceGetAttribute(&num_sm, cudaDevAttrMultiProcessorCount, device_id);
printf("num_sm = %d\n", num_sm);
/*Grid sizes that are multiples of the number of available SMs can increase performance*/
const size_t num_blocks = 32 * num_sm;
const size_t num_threads_per_block = 256;
hoge<<<num_blocks, num_threads_per_block>>>();
cudaDeviceSynchronize();
}
出力
num_sm = 28
7.プリフェッチ
プリフェッチを記述しておくことで、CPU-GPU間のデータコピーを素早く実行できます。プリフェッチを記述しないと、データの格納先を探索する時間がかかってしまうのです。実装
#include <stdio.h>
void printArrayCPU(int* arr, size_t num_elements)
{
for(size_t i = 0; i < num_elements; i++){
printf("arr[%d] = %d\n", i, arr[i]);
}
}
__global__ void printArray(int* arr, size_t num_elements)
{
int index = threadIdx.x + blockIdx.x * blockDim.x;
printf("arr[%d] = %d\n", index, arr[index]);
}
int main()
{
const size_t num_blocks = 2;
const size_t num_threads_per_block = 3;
const size_t num_elements = num_blocks * num_threads_per_block;
int* arr;
size_t bytes = num_elements * sizeof(int);
cudaMallocManaged(&arr, bytes);
for(size_t i = 0; i < num_elements; i++) arr[i] = i;
int device_id;
cudaGetDevice(&device_id);
cudaMemPrefetchAsync(arr, bytes, device_id);
printf("----- GPU -----\n");
printArray<<<num_blocks, num_threads_per_block>>>(arr, num_elements);
cudaDeviceSynchronize();
printf("----- CPU -----\n");
cudaMemPrefetchAsync(arr, bytes, cudaCpuDeviceId);
printArrayCPU(arr, num_elements);
}
出力
----- GPU ----- arr[0] = 0 arr[1] = 1 arr[2] = 2 arr[3] = 3 arr[4] = 4 arr[5] = 5 ----- CPU ----- arr[0] = 0 arr[1] = 1 arr[2] = 2 arr[3] = 3 arr[4] = 4 arr[5] = 5
プロファイル
実行ファイルをプロファイルしてみると、各処理の時間を見ることができます。プリフェッチすることで、CPUとGPU間の転送を早くできます。$ nsys profile --stats=true ~/cuda_cpp_tutorial/build/print_array_with_prefetch
----- GPU -----
arr[0] = 0
arr[1] = 1
arr[2] = 2
arr[3] = 3
arr[4] = 4
arr[5] = 5
----- CPU -----
arr[0] = 0
arr[1] = 1
arr[2] = 2
arr[3] = 3
arr[4] = 4
arr[5] = 5
Generating '/tmp/nsys-report-d472.qdstrm'
[1/8] [========================100%] report1.nsys-rep
[2/8] [========================100%] report1.sqlite
[3/8] Executing 'nvtxsum' stats report
SKIPPED: /root/cuda_cpp_tutorial/build/report1.sqlite does not contain NV Tools Extension (NVTX) data.
[4/8] Executing 'osrtsum' stats report
Operating System Runtime API Statistics:
Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
-------- --------------- --------- ---------- ---------- -------- -------- ----------- --------------
70.2 231463209 17 13615482.9 10060535.0 3250 68769206 17963544.0 poll
14.2 46707197 454 102879.3 11345.0 1010 13064068 743131.1 ioctl
9.6 31819233 13 2447633.3 26640.0 10670 20429791 6081350.0 sem_timedwait
5.5 18114990 30 603833.0 3790.0 1210 17910430 3268710.9 fopen
0.2 752665 27 27876.5 3110.0 2150 466242 88468.7 mmap64
0.1 389781 44 8858.7 8125.5 3300 25340 4368.9 open64
0.1 176071 5 35214.2 42150.0 18800 45871 11728.4 pthread_create
0.0 111910 18 6217.2 5510.0 1270 28480 6326.0 mmap
0.0 41450 1 41450.0 41450.0 41450 41450 0.0 fgets
0.0 32010 6 5335.0 5040.0 2450 7900 2152.6 open
0.0 24480 7 3497.1 3190.0 2390 5690 1155.1 munmap
0.0 23310 11 2119.1 2250.0 1090 3960 893.8 write
0.0 20590 9 2287.8 1250.0 1070 6800 2079.4 fcntl
0.0 19100 9 2122.2 1710.0 1080 3980 1060.0 fclose
0.0 17040 8 2130.0 1935.0 1350 3320 663.7 read
0.0 15720 2 7860.0 7860.0 3500 12220 6166.0 socket
0.0 13250 2 6625.0 6625.0 5820 7430 1138.4 fread
0.0 11230 2 5615.0 5615.0 1420 9810 5932.6 fwrite
0.0 9390 1 9390.0 9390.0 9390 9390 0.0 connect
0.0 8610 2 4305.0 4305.0 1040 7570 4617.4 fflush
0.0 7480 1 7480.0 7480.0 7480 7480 0.0 pipe2
0.0 2380 1 2380.0 2380.0 2380 2380 0.0 bind
[5/8] Executing 'cudaapisum' stats report
CUDA API Statistics:
Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
-------- --------------- --------- ----------- ----------- --------- --------- ----------- ---------------------
99.8 190180906 1 190180906.0 190180906.0 190180906 190180906 0.0 cudaMallocManaged
0.1 216510 2 108255.0 108255.0 21790 194720 122280.0 cudaMemPrefetchAsync
0.0 56570 1 56570.0 56570.0 56570 56570 0.0 cudaDeviceSynchronize
0.0 46641 1 46641.0 46641.0 46641 46641 0.0 cudaLaunchKernel
[6/8] Executing 'gpukernsum' stats report
CUDA Kernel Statistics:
Time (%) Total Time (ns) Instances Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
-------- --------------- --------- -------- -------- -------- -------- ----------- --------------------------------
100.0 43168 1 43168.0 43168.0 43168 43168 0.0 printArray(int *, unsigned long)
[7/8] Executing 'gpumemtimesum' stats report
CUDA Memory Operation Statistics (by time):
Time (%) Total Time (ns) Count Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Operation
-------- --------------- ----- -------- -------- -------- -------- ----------- ---------------------------------
61.1 1856 1 1856.0 1856.0 1856 1856 0.0 [CUDA Unified Memory memcpy HtoD]
38.9 1184 1 1184.0 1184.0 1184 1184 0.0 [CUDA Unified Memory memcpy DtoH]
[8/8] Executing 'gpumemsizesum' stats report
CUDA Memory Operation Statistics (by size):
Total (MB) Count Avg (MB) Med (MB) Min (MB) Max (MB) StdDev (MB) Operation
---------- ----- -------- -------- -------- -------- ----------- ---------------------------------
0.004 1 0.004 0.004 0.004 0.004 0.000 [CUDA Unified Memory memcpy DtoH]
0.004 1 0.004 0.004 0.004 0.004 0.000 [CUDA Unified Memory memcpy HtoD]
Generated:
/root/cuda_cpp_tutorial/build/report1.nsys-rep
/root/cuda_cpp_tutorial/build/report1.sqlite
8.エラーチェック
CUDAプログラミングにおけるエラーチェックをご紹介します。実装
#include <stdio.h>
__global__ void printIndicies()
{
int index = threadIdx.x + blockIdx.x * blockDim.x;
printf("index = %d\n", index);
}
int main()
{
const size_t num_blocks = 1;
const size_t num_threads_per_block = 1025; // > 1024
printIndicies<<<num_blocks, num_threads_per_block>>>();
cudaDeviceSynchronize();
cudaError_t err = cudaGetLastError();
if(err != cudaSuccess) printf("Error: %s\n", cudaGetErrorString(err));
}
出力
Error: invalid configuration argument
NG例
以下のように、エラーチェックを実装しないと、実行エラーに気づくことができません。#include <stdio.h>
__global__ void printIndicies()
{
int index = threadIdx.x + blockIdx.x * blockDim.x;
printf("index = %d\n", index);
}
int main()
{
const size_t num_blocks = 1;
const size_t num_threads_per_block = 1025; // > 1024
printIndicies<<<num_blocks, num_threads_per_block>>>();
cudaDeviceSynchronize();
}
実行結果は、以下のように何も出力されません。
さいごに
CUDAプログラミングの基礎的な挙動を確認しました。 参考になれば幸いです。 Ad.
コメント