PyCudaの使用例:CPUとGPUにおけるPyTorchの処理時間比較
はじめに
GPUとはGraphics Processing Unitの略で、描画処理用のCPUのことを指しています。元々はGraphic用途だったのを汎用的な計算に拡大することをGPGPUと呼ばれています。General Purpose Graphics Processing Unit(GPGPU)はGPUを画像処理ではなく、他の用途(ex. 暗号解読、音声処理)に使用するときに使用されています。今回Nvidia社が作ったCUDAを機械学習の裏でどのように実行されているかを紹介いたします。CUDAとはGPU並列を扱うためのプログラミング言語です。C言語を拡張したものはPyCUDAです(CUDAのPython Wrapper API)。Kaggleや研究などでは、CUDAをPythonから扱えば、メモリ管理が楽になります。
実行環境:conda_torch17_py36, PyCuda version : 2021.1

CPUとGPUにおけるPyTorchの処理時間
PyTorchでGPU情報を確認
PyTorchでGPUの情報を取得する関数はtorch.cuda以下に用意されています。GPUが使用可能かを確認するtorch.cuda.is_available()
を使って、自分の環境でGPUが使えているかどうかを確認します。
import torch
if torch.cuda.is_available():
device = torch.device("cuda")
else:
device = torch.device("cpu")
print("using", device, "device")
GPUが使えることを確認したら、簡単なCPUとGPUの処理時間を見比べましょう。GPUで計算を行う際に、GPUにデータをPyTorchの.to(device)
を使って転送する必要があります。 また、GPUの計算は裏で行われるため、計算の処理が終わるまで、torch.cuda.synchronize()
でプログラムの待機が必要です。初めてPyCUDAを実行するとき、GPUの初期化などの処理が必要になるため、処理時間が遅くなるけど、2回目以降にGPUの計算を行うと、早くなることがわかりました。そのため、今回3回ぐらいの計算を行って、その平均値を取って、CPUの処理時間と比べます。
import time
matrix_size = 30*512
x = torch.randn(matrix_size, matrix_size)
y = torch.randn(matrix_size, matrix_size)
print("************ CPU SPEED ***************")
start = time.time()
result = torch.matmul(x,y)
print(time.time() - start)
print("verify device:", result.device)
x_gpu = x.to(device)
y_gpu = y.to(device)
torch.cuda.synchronize()
for i in range(3):
print("************ GPU SPEED ***************")
start = time.time()
result_gpu = torch.matmul(x_gpu,y_gpu)
torch.cuda.synchronize()
print(time.time() - start)
print("verify device:", result_gpu.device)
PyTorchでGPUによる計算が早いことがわかりました。しかし、 CUDAでの計算流れがどのように実行されますでしょうか?
計算の流れ

## 必要なライブラリをImportする
import numpy as np
import math
import pycuda.gpuarray as gpuarray
from pycuda.elementwise import ElementwiseKernel
from pycuda.compiler import SourceModule
import pycuda.autoinit
PyCUDAのコードを書きます。今回ElementwiseKernel
を使って、CUDAカーネルを記述してPythonから実行します。PythonのForループになっていた遅い部分をGPUで実行したい時によく用いられます。他のユースケースは以下となります。
- 配列(行列・テンソル)の要素ごとに同じような処理したい
- バッチで入力した画像全部に同じような処理したい
ElementwiseKernelを使えば、CUDAカーネルを簡易に書くことができます。実行時にカーネルの実行内容はコンパイルされます。詳細はCuPyの参考資料をご覧ください。
plus_one_kernel = ElementwiseKernel(
"int *x, int *y",
"y[i] = x[i] + 1",
"plus_one")
np.arange
は、連番や等差数列を生成する関数です。np.arange
を使って、配列x
とy
を作成します。
num_components = 10
x = np.arange(num_components, dtype=np.int32)
CPUからGPUに配列を転送します。
x_gpu = gpuarray.to_gpu(x)
y_gpu = gpuarray.zeros(num_components, dtype=np.int32)
plus_one_kernel(y_gpu, x_gpu)
結果を取得します。
y_gpu.get()
x
2次元配列の四則演算
SourceModelはCUDA Cカーネルをコンパイルする関数SourceModelにより一般的なCUDAカーネルを実行することができます。詳細はCuPyの参考資料をご覧ください。
module = SourceModule("""
__global__ void add_two_array_2d(int nx, int ny, float *res, float *arr1, float *arr2){
int x = threadIdx.x + blockDim.x * blockIdx.x;
int y = threadIdx.y + blockDim.y * blockIdx.y;
int ij = nx * y + x;
if (x < nx && y < ny){
res[ij] = arr1[ij] + arr2[ij];
}
}
""")
add_two_array = module.get_function("add_two_array_2d")
num_x, num_y = np.int32(5), np.int32(2)
num_components = num_x * num_y
arr1 = np.arange(num_components, dtype=np.float32).reshape(num_y, num_x)
np.random.seed(123)
arr2 = 10 * np.random.rand(num_y, num_x)
arr2 = arr2.astype(np.float32)
res = np.zeros([num_y, num_x], dtype=np.float32)
arr1_gpu = gpuarray.to_gpu(arr1)
arr2_gpu = gpuarray.to_gpu(arr2)
res_gpu = gpuarray.to_gpu(res)
threads_per_block = (16, 16, 1)
block_x = math.ceil(num_x / threads_per_block[0])
block_y = math.ceil(num_y / threads_per_block[1])
blocks_per_grid = (block_x, block_y, 1)
add_two_array(num_x, num_y, res_gpu, arr1_gpu, arr2_gpu, block=threads_per_block, grid=blocks_per_grid)
結果を取得します。
res_gpu.get()
arr1 + arr2
いかがでしたでしょうか。
弊社クラウドGPUサーバのご利用をご検討中の方はhttps://soroban.highreso.jp/から
お気軽にお申込みやお問い合わせください。
初回インスタンス作成から
3日間は無料利用可能!