numpyでGPUを使用する方法と、CPUとの実行時間比較について

## 実行環境
OS: Windows 11
Python: 3.10.2 (インストール済みであることを前提します)

## 実験条件
スクリプト、使用法等はこれ以外の環境でも同様のはずです。ただしCUDAはGeForceのGPUでないと使えません。
CPU: intel corei7 11700F (ハイエンド(？))
GPU: GeForce GTX1650(4GB) (ローエンド(？))



## 注意
筆者は一介の学部生です。GPU等について認識が誤っている部分があるかもしれません。
誤り、誤植等ありましたら、ご指摘よろしくお願いいたします。

## GPUの利用方法
##### 1.CUDA(GeForce Experience)のインストール
[公式サイト](https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64&target_version=11&target_type=exe_local)からCUDA11.6のインストーラをダウンロード。Installer TypeはLocalで大丈夫です。インストーラをダウンロード後、exeファイルを実行して指示に従ってインストールを進めます。基本的には推奨設定のまま進めて大丈夫だと思います。

##### 2.GPUに適したドライバをインストール
[このページ](https://www.nvidia.co.jp/Download/index.aspx?lang=jp)からお使いのGPUに適したドライバをインストールしましょう。
ダウンロードタイプについてですが、大雑把にGameReadyはゲーム用（速度優先）、studioはグラフィック処理の精度優先だそうです。筆者もあまり詳しくありませんので、詳細は他の方の記事にお任せします。筆者はとりあえずGameReadyの方をインストールしました。
インストーラをダウンロード後、exeファイルを実行して同様に指示に従って進めます。GeForce Experienceはインストール済みのはずですので、ドライバーのみインストールの方で大丈夫です。

##### 3.numpyとcupyのpipインストール（ローカル環境に入れたくない場合はvenv等の仮想環境を利用できます）
```
pip install cupy-cuda116
pip install numpy
```

pip install cupyと書いてある記事が複数ありますが、筆者の環境では失敗しました。
CUDAのバージョンによって適宜116の部分を書き換える（例えば10.1の場合cupy-cuda101）必要があるようです。（すべてのバージョンについて確認したわけではありません。）

cupyはnumpyのほとんどの関数に対応しているようですので、以下のように変更することで簡単にGPUを用いた実行が可能です。

```python:CPU
import numpy as np
arr = np.random.random_sample((10, 10)) 
```

```python:GPU
import cupy as cp
arr = cp.random.random_sample((10, 10)) 
```
では最後に、GPUとCPUに速度勝負をさせてみましょう。お題は逆行列計算です。

```python:test.py

import numpy as np
import cupy as cp
import time

def test(size,num=1):
    start=time.time()#時間計測開始
    print(f"size:{size},num:{num}")
    for i in range(num):
        arr = np.random.random_sample((size, size)) #size次の乱数行列生成
        rev = np.linalg.inv(arr) #逆行列計算
    end=time.time()#時間計測終了
    print(f"cpu:{round(end-start,8)}sec")

    start=time.time()#時間計測開始
    for i in range(num):
        arr = cp.random.random_sample((size, size)) #size次の乱数行列生成
        rev = cp.linalg.inv(arr)#逆行列計算
    end=time.time()#時間計測終了
    print(f"gpu:{round(end-start,8)}sec")

Size=[10,10,100,1000,2000,5000,10000]
Num=[5000,5000,500,50,25,10,5]

for s,n in zip(Size,Num):
    test(s,n)
```
上のプログラムを用いて、行列のサイズと計算回数を適宜変化させて実験しました。
以下の動画は、プログラムをコマンドプロンプトから実行した際のタスクマネージャの様子です。
CPUとGPUが交代で動いている様子がわかっていただけると思います。

![ezgif.com-gif-maker (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/881013/eb55a605-607e-1088-2e91-3514eb83cb03.gif)


以下の表に実験結果を示します。各値は、各サイズの乱数行列についての逆行列の平均計算時間です。
比の値が1以上であれば、GPUの方が高速に計算を実行できたことを示します。


| 行列サイズ | CPU：Corei7 11700 (sec)   | GPU：GTX1650 (sec)  | 比（CPU/GPU） | 
| ---------- | ----------- | ---------- | ------------- | 
| 10         | 0.00018624  | 0.00029849 | 0.624         | 
| 10         | 0.00018572  | 0.00016565 | 1.121         | 
| 100        | 0.00522239  | 0.00090294 | 5.784         | 
| 1000       | 0.08481761  | 0.05751644 | 1.475         | 
| 2000       | 0.26992218  | 0.30537095 | 0.884         | 
| 5000       | 1.76327453  | 3.504635   | 0.503         | 
| 10000      | 10.22772169 | 7.51856899 | 1.360         | 

最初に行列サイズを10として実行したときのGPUの実行時間がやや長くなっていますが、これはGPUの起動にかかる時間の影響を含んでいると考えられます。全体としてややGPUの方が速いという印象を受けましたが、行列サイズによっては必ずしもそうではないようです。

何度か実験を行いましたが、サイズが100の時にはGPUが圧倒的に優位であり、サイズが2000の時には拮抗、5000のときにはCPUの方が優位となり、サイズ10000で再びGPUの方が速くなる、という傾向は変化しませんでした。なお、サイズ20000の行列をそのまま計算させようとすると、筆者の環境のGPUではメモリ不足でエラーとなってしまいました。

今回はPython、中でもNumpyで書かれたプログラムをGPUで実行する方法と、CPUとGPUの逆行列計算の実行時間の比較を行いました。Python環境とGPUを積んだPCがあれば簡単に実行できますので、ぜひ試してみてください。