🧊

NumPyからCuPyへ:高速化の一例

2023/09/22に公開
2

はじめに

Pythonで数値計算を行う際によく使われるライブラリがNumPyです。しかし、GPUを活用することで計算速度を向上させたい場面も多いでしょう。そこでCuPyが登場します。この記事では、NumPyのコードをCuPyに置き換える一例とその性能比較について解説します。一方でCuPyの苦手な処理についても触れますので、使いどころを間違えないようにしましょう。

環境

$ pip install numpy cupy
Installing collected packages: fastrlock, numpy, cupy
Successfully installed cupy-12.2.0 fastrlock-0.8.2 numpy-1.24.4
(cupy)
$ pip list
Package       Version
------------- -------
cupy          12.2.0
fastrlock     0.8.2
numpy         1.24.4
pip           23.2.1
pkg_resources 0.0.0
setuptools    44.0.0

$ python -V
Python 3.8.10
$ uname -a
Linux user 5.15.0-83-generic #92~20.04.1-Ubuntu SMP Mon Aug 21 14:00:49 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

  • Ubuntu 20.04

インストール

バイナリパッケージ(ホイール形式)は、PyPI上でLinuxおよびWindows向けに利用可能。

Platform Architecture Command
CUDA 10.2 x86_64 / aarch64 pip install cupy-cuda102
CUDA 11.0 x86_64 pip install cupy-cuda110
CUDA 11.1 x86_64 pip install cupy-cuda111
CUDA 11.2 ~ 11.8 x86_64 / aarch64 pip install cupy-cuda11x
CUDA 12.x x86_64 / aarch64 pip install cupy-cuda12x
ROCm 4.3 (experimental) x86_64 pip install cupy-rocm-4-3
ROCm 5.0 (experimental) x86_64 pip install cupy-rocm-5-0

上記を参考に、pipでインストールします。

CuPyについて

CuPyはPythonプログラミング言語でGPUによる高速計算をサポートするオープンソースライブラリです。多次元配列、疎行列、およびそれらの上で実装された多様な数値アルゴリズムをサポートしています。CuPyはNumPy同じAPIセットを共有しており、NumPy/SciPyのコードをGPUで実行するためのドロップイン置換として機能します。CuPyはNVIDIAのCUDA GPUプラットフォームと、v9.0からはAMDのROCm GPUプラットフォームもサポートしています。

https://github.com/cupy/cupy

NumPyのサンプルコード

まずは、行列の積を計算するシンプルなNumPyのコードを見てみましょう。

https://github.com/yKesamaru/cupy/blob/041212dfe3101f4a3f8d7a8eb4755ccc899f528f/numpy_ex.py#L1-L18

CuPyによる高速化

次に、上記のコードをCuPyに置き換えてみます。

https://github.com/yKesamaru/cupy/blob/54be11bf6558c3a9c8d8d79844c04dfd3f4c379f/cupy_ex.py#L1-L18

性能比較

両者のコードを実行した結果、CuPyの方が計算速度が大幅に向上したことが確認できました。

  • NumPy Time: 3.5 seconds
  • CuPy Time: 1.0 seconds
    初回オーバーヘッド: CuPy(または他のGPUライブラリ)をはじめて使用する際には、GPUの初期化などに時間がかかる場合があります。このオーバーヘッドは一度だけ発生することが多いです。

CuPyの苦手な処理

CuPyを使用する際には、CPUからGPUへのデータ転送が必要なケースがあります。このデータ転送は、大量のデータを扱う場合には、パフォーマンスに悪影響となります。以下に、この「苦手な処理」を模倣する簡単なPythonコードを示します。

NumPyのサンプルコード

https://github.com/yKesamaru/cupy/blob/cb6a3786440e5ff6367c2271cec571737172a8d0/numpy_ex2.py#L1-L14

CuPyのサンプルコード

https://github.com/yKesamaru/cupy/blob/cb6a3786440e5ff6367c2271cec571737172a8d0/cupy_ex2.py#L1-L23

性能比較

NumPy Calculation time: 0.02361893653869629 seconds
CuPy Data transfer time: 0.2935044765472412 seconds
CuPy Calculation time: 0.34106016159057617 seconds

約17倍の差があります。このように、データ転送が必要なケースでは、CuPyのパフォーマンスが悪化することがあります。
使いどころを間違えないようにしましょう。

まとめ

CuPyはGPUを活用して高速な数値計算を可能にする強力なライブラリですが、その性能を最大限に引き出すためにはいくつかの注意点があります。とくに、CPUからGPUへのデータ転送が必要な場合、この転送時間が全体のパフォーマンスに影響を与える可能性があります。

一方で、大量のデータに対する複雑な計算を高速に行う必要がある場合、CuPyは非常に有用です。また、初回のオーバーヘッドを除けば、一般的にはNumPyよりも高速に動作することが多いです。

以上がCuPyとNumPyの比較、そしてCuPyの使いどころについての簡単なガイドでした。どちらのライブラリもそれぞれの用途で非常に優れていますので、自分のプロジェクトに最適な選択をしてください。

以上です。ありがとうございました。

Discussion

InriInri

すみません、実行時間に関して質問したいです。
数秒代だと誤差がどのくらいあるかが知りたいです

yKesamaruyKesamaru

コメントありがとうございます。
結論から先に申し上げると、ケースバイケースとなります。
以下に根拠をリストアップするので、それらが参考になれば幸いです。

  1. CuPyはGPUへのデータ転送に時間がかかる
    1. 処理データが大きさ:これが大きいほどGPUで処理をさせたい動機が高まりますが、それは転送時間とトレードオフです。小さい処理を複数回GPUで処理させようとすれば最終的な処理時間はかえって増加します。CuPyの性能を活かすには小さな前処理を挟む必要があるかも知れません。
    2. 処理データの内容:大規模な行列計算や並列処理が多いタスクでは、CuPyのメリットが活きてきます。そうでなければNumPyをCPUで処理させたほうが良いかも知れません。
  2. CPUやGPUの性能、マザーボードのPCIeの帯域幅が処理速度に影響します。(つまり各パーツの性能全てが変数となる。)

なので、結果的に処理させる端末で簡単な処理系を作って試してみないと、具体的なパフォーマンスをはかることはできません。

ということで、サンプルコードを書きました。

NumPy vs CuPy
import time
import numpy as np
import cupy as cp

# 変数(この値を任意に変更してください:100 ~ 10000)
x = 100
print(f"初期変数 x の値: {x}\n")

# NumPyでの処理
start = time.time()
a = np.random.rand(x, x)
b = np.random.rand(x, x)
end = time.time()
numpy_data_gen_time = round(end - start, 3)

start = time.time()
c = np.dot(a, b)
end = time.time()
numpy_comp_time = round(end - start, 3)

# CuPyでの処理
start = time.time()
a_gpu = cp.random.rand(x, x)  # データをGPUに転送
b_gpu = cp.random.rand(x, x)  # データをGPUに転送
cp.cuda.Stream.null.synchronize()  # データ転送の完了を待機
end = time.time()
cupy_data_gen_time = round(end - start, 3)

start = time.time()
c_gpu = cp.dot(a_gpu, b_gpu)
cp.cuda.Stream.null.synchronize()  # GPUの処理を同期
end = time.time()
cupy_comp_time = round(end - start, 3)

# 結果の表示
print("=== パフォーマンス結果 ===")
print("処理\t\t\tデータ生成時間\t\t計算時間")
print(f"NumPy\t\t{numpy_data_gen_time} seconds\t\t{numpy_comp_time} seconds")
print(f"CuPy\t\t{cupy_data_gen_time} seconds\t\t{cupy_comp_time} seconds")

上記コードに必要なライブラリ

$ pip freeze
cupy==13.1.0
fastrlock==0.8.2
numpy==1.26.4

コードは仮想環境をアクティベートして実行します。
その結果、以下の出力を得ました。

初期変数 x の値: 100

=== パフォーマンス結果 ===
処理			データ生成時間		計算時間
NumPy		0.0 seconds		0.008 seconds
CuPy		0.187 seconds		0.057 seconds



初期変数 x の値: 1000

=== パフォーマンス結果 ===
処理			データ生成時間		計算時間
NumPy		0.018 seconds		0.048 seconds
CuPy		0.181 seconds		0.072 seconds



初期変数 x の値: 10000

=== パフォーマンス結果 ===
処理			データ生成時間		計算時間
NumPy		1.399 seconds		23.127 seconds
CuPy		0.202 seconds		11.067 seconds

初期変数xの値を変化させたときの処理速度を示しています。
初期変数xが十分に大きくなった時、CuPyが活きているのがわかります。

参考までにわたしの実行環境を出力します。

$ inxi -SGC
System:
  Host: **user** Kernel: 6.5.0-35-generic x86_64 bits: 64 Desktop: Unity
    Distro: Ubuntu 22.04.4 LTS (Jammy Jellyfish)
CPU:
  Info: quad core model: AMD Ryzen 5 1400 bits: 64 type: MT MCP cache:
    L2: 2 MiB
  Speed (MHz): avg: 2069 min/max: 1550/3800 cores: 1: 1550 2: 3799 3: 1378
    4: 1550 5: 1550 6: 3799 7: 1550 8: 1378
Graphics:
  Device-1: NVIDIA TU116 [GeForce GTX 1660 Ti] driver: nvidia v: 545.23.08
  Display: x11 server: X.Org v: 1.21.1.4 driver: X: loaded: nvidia
    unloaded: fbdev,modesetting,nouveau,vesa gpu: nvidia
    resolution: 2560x1440~60Hz
  OpenGL: renderer: NVIDIA GeForce GTX 1660 Ti/PCIe/SSE2
    v: 4.6.0 NVIDIA 545.23.08

以上です。ご参考になれば幸いです。