MacでPyCUDA環境構築からGPGPUプログラミング (MacOS 10.12 Sierra)

#はじめに
##対象者
PyCUDAをMacにインストールする手順を説明します。
以下に該当する方が対象となりえます。
- Python3.xを使用
- Chainer, Tensorflow, KerasなどのDeep Learningライブラリを使用しないでGPUで計算したい
- Mac上でPythonを使ってGPGPUプログラミングをしたい
- NVIDIA製のGPUを搭載したMac/Macbook Proを所有している

##開発環境
使用したMacのスペックは以下の通り。
- Macbook Pro, 2014 Mid
- MacOS Sierra 10.12.6
- Core i7 2.8GHz/16GBメモリ
- NVIDIA GeForce GT 750M 2GB GDDR5
- Xcode 7.3.1 & Command Line Tools (Xcode 8.xはCUDA8が対応していません)
最後のXcodeについて補足ですが、Xcode8をインストール済みでもXcode7と共存させる方法がありますので後述します。

#PyCUDA環境構築
##Xcode7 Installation
記事執筆時ではXcode 7.3.1がXcode7では最新でした。<a href="https://developer.apple.com/download">Downloads for Apple Developer</a>から**See more downloads**を選んで、検索ボックスに「Xcode 7」と入力します。**Xcode 7.3.1**と**Command Line Tools for Xcode 7.3.1**をダウンロードします。

ダウンロードしたdmgファイルを開いてXcode 7.3.1をインストールします。続いてCommand Line Tools for Xcode7.3.1もインストールします。

続いて、Command Line Toolsのdmgを開いてをインストールします。

##Xcode8がインストール済みの場合
Xcode8を既にインストールしているときは、Xcode7をXcode7.appとして/Applicationsにコピーします。Xcode7をアクティブにするには
`$ sudo mv /Applications/Xcode.app /Applications/Xcode8.app`
`$ sudo mv /Applications/Xcode7.app /Applications/Xcode.app`
`$ sudo xcode-select -s /Applications/Xcode.app`

逆にXcode8をアクティブにするには
`$ sudo mv /Applications/Xcode.app /Applications/Xcode7.app`
`$ sudo mv /Applications/Xcode8.app /Applications/Xcode.app`
`$ sudo xcode-select -s /Applications/Xcode.app`

***CUDA/PyCUDAを使用するにはXcode7をアクティブにしてください。CUDA8はXcode8非対応です。***

##CUDA8 Installation
CUDAはNVIDIAが提供するGPGPUのためのライブラリ、コンパイルなどの統合開発環境です。要はNVIDIA製のGPUを使って並列計算するためのツール群です。PyCUDAがCUDAを必要とするためCUDA8をインストールします。
###CUDA8 Installation
<a href="https://developer.nvidia.com/cuda-downloads">CUDA Toolkit Download | NVIDIA Developer</a>からOS、バージョンを選んでパッケージをダウンロードします。インストーラーの指示通りインストールします。

インストール後、MacOSの**システム環境設定**から**CUDA**を選んで**Install CUDA Update**をクリックしてCUDAをアップデートします。

###cuDNN Installation
<a href="https://developer.nvidia.com/cudnn">NVIDIA cuDNN | NVIDIA Developer</a>からcudnnをダウンロードします。メンバー登録が必要な場合があります。

ダウンロードしたcudnn-8.0-osx-x64-v6.0.tgzを解凍します。
```$ tar zxvf cudnn-8.0-osx-x64-v6.0.tgz```

ファイルをシステムにデプロイします。
```$ sudo mv <解凍フォルダ>/lib/libcudnn* /usr/local/cuda/lib```
```$ sudo mv <解凍フォルダ>/include/cudnn.h /usr/local/cuda/include```


**DYLD_LIBRARY_PATH**と**PATH**を設定
.bashrcに追加します。
```export DYLD_LIBRARY_PATH=/usr/local/cuda/lib:/Developer/NVIDIA/CUDA-8.0/lib:$DYLD_LIBRARY_PATH```
```export PATH=/Developer/NVIDIA/CUDA-8.0/bin:$PATH```

変更内容を反映。
```$ source .bashrc```

###CUDAサンプルプログラム

サンプルプログラムでインストールを確認できます。ただしコンパイルに時間が結構かかりますのでスキップしても結構です。

```$ /Developer/NVIDIA/CUDA-8.0/bin/cuda-install-samples-8.0.sh ~/Downloads```
```$ cd ~/Downloads```
```$ make -C 1_Utilities/deviceQuery```
```$ 1_Utilities/deviceQuery/deviceQuery```
```$ make -C 5_Simulations/smokeParticles```
```$ 5_Simulations/smokeParticles/smokeParticles```

コンパイルエラーが出る場合は、Xcodeの切り替え、DYLD_LIBRARY_PATHとPATHの設定、**システム環境設定**の**省エネルギーの設定**（**グラフィックスの自動切り替え**をオフ）をチェックします。

##Homebrew Installation
Homebrewのインストールがまだなら、
`$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”`

Python3のインストールがまだなら、
`$ brew install python3`

Python3確認
`$ python3 --version`

##pip Installation
pipのインストールがまだなら、
`$ curl -kL https://raw.github.com/pypa/pip/master/contrib/get-pip.p`

##Numpy Installation
numpyのインストールがまだなら、
`$ pip3 install numpy`
`$ pip3 install scipy`

***注意) Python2.xにNumpyがインストールされていても、Python3.x用にインストールする必要があります。***

numpy確認
`$ python3`
`>>> import numpy as np`

##PyCUDA Installation
いよいよPyCUDAのインストールです。
`$ pip3 install pycuda`

PyCUDA確認
`$ python3 -c "import pycuda.autoinit`

エラーが出なければとりあえずOKです。エラーが出る場合は、Xcode7への切り替え、DYLD_LIBRARY_PATHとPATHの設定、**システム環境設定**の**省エネルギーの設定**（**グラフィックスの自動切り替え**をオフ）を疑ってみてください。

続いて、scikit-cuda (skcuda)をインストールします。
`$ pip3 install scikit-cuda`

#PyCUDAでGPGPUプログラミング
##テストスクリプト test_gpu.py
```python
import pycuda.autoinit
import pycuda.driver as cuda
from pycuda import gpuarray
from skcuda import linalg
import numpy as np

linalg.init()

a = np.random.rand(2, 1).astype(np.float32)
b = np.ones(2).astype(np.float32).reshape(1, 2)

a_gpu = gpuarray.to_gpu(a)
b_gpu = gpuarray.to_gpu(b)

c_gpu = linalg.dot(a_gpu, b_gpu)
c = c_gpu.get()

print(a)
print(b)
# 内積をCPUで計算した結果
print(np.dot(a, b))
# 内積をGPUで計算した結果
print(c)
```

上のスクリプトをtest_gpu.pyとして保存して実行します。

`$ python3 test_gpu.py`

```python
[[ 0.85600704]
 [ 0.02441464]]
[[ 1.  1.]]
[[ 0.85600704  0.85600704]
 [ 0.02441464  0.02441464]]
[[ 0.85600704  0.85600704]
 [ 0.02441464  0.02441464]]
```

テストスクリプトtest_gpu.pyを説明します。

まず、PythonでGPUを利用するための初期化処理等の準備をします。

```python
import pycuda.autoinit
...
linalg.init()
```

GPUで処理する前に一旦Numpyのndarrayを作ります。データタイプはfloat32を使用します。

```python
a = random.rand(2, 1).astype(np.float32)
b = np.ones(2).astype(np.float32).reshape(1, 2)
```

次にndarrayをgpuarrayに変換します。

```python
a_gpu = gpuarray.to_gpu(a)
b_gpu = gpuarray.to_gpu(b)
```

この処理でCPU用のメモリからGPU用のメモリに配列データが変換・転送されます(CPU→GPU)。

Numpyのnp.dotに相当するGPU内積演算です。

```python
c_gpu = linalg.dot(ga, gb)
```

結果を確認するにはgpuarray (GPU)からndarray (CPU)に戻す必要があります。

```python
c = c_gpu.get()
print(c)
```

今度はGPU→CPUの流れです。

##PyCUDAプログラミングの注意点

PyCUDAを利用したプログラミングでは、CPUとGPUを行き来するため、変数がndarrayかgpuarrayかを常に意識することが重要です。また、CPU/GPU間のデータ変換はパフォーマンスに大きな影響を与えるため、最小限に止めることが望ましいです。

GPUメモリが少ないMacでgpuarrayサイズを大きくすると、システムが極端にもっさりして、簡単に操作不能に陥ります。GPUメモリの少ないマシンでスクリプトを実行する際は、ウィンドウを閉じてアプリケーションを完全に終了した方が無難です。特にSafariは結構メモリを消費するので、ご注意ください。


##便利な関数
gpuarrayとlinalgには下記のような便利な関数があります。ただし、x, y, zをgpuarrayとします。
- linalg.dot(x, y): 内積
- linalg.transpose(x): 転置行列
- gpuarray.max(x): 最大値
- abs(x): 絶対値
- x.shape: ndarrayのshapeと同じ
- gpuarray.if_positive(x > z, x, z): zが**0**ならrelu
- linalg.mdot(x, y): reshapeすることで、外積として利用できる

#API Docs
- <a href="https://documen.tician.de/pycuda/array.html">GPU Arrays — PyCUDA 2017.1.1 documentation</a>
- <a href="http://scikit-cuda.readthedocs.io/en/latest/generated/skcuda.linalg.dot.html">skcuda.linalg.dot — scikit-cuda 0.5.2 documentation</a>