AIの片隅で(19) CuPy、NumPy、単精度、倍精度

JosephHalfmoon

前回の投稿でChainer開発終わって寂しい件を書かせていただきました。しかし、CuPyの方は開発を益々?継続のようであります。そういえば、Chainerの裏でCuPyが動いているのは知っていましたが、直接使ってみたことってなかったかも。それにちょっと気になるのがNumPyと凄く互換性が高く見えるけれど、「所詮」CPUとGPUの違いあり、精度の違いなどどう処理されているのでしょうか。とりあえずやってみました。

「やってみる」のは、またもやJetson Nano(Development Kit)上であります。ChainerもCuPyもインストール済ですが、今回、両方ともバージョンアップされたということでまずバージョンアップから入りたいと思います。Chainerのメジャーバージョンはこれで仕舞かと、よよと泣き崩れはしませんが。

まずは、インストール済バージョンを確認しておきます。

$ pip3 list
~途中略~
chainer (6.2.0)
~途中略~
cupy (6.2.0)

なお、pipが出力フォーマットの煩い警告メッセージを出していないのは、pipが言う通りに

.config/pip/pip.conf

の中に以下書き込んでおるためであります。

[list]
format = legacy

Chainer, CuPyとも 6.2 であったので、アップデートをかけます。

$ pip3 install -U chainer
Collecting chainer
  Downloading https://files.pythonhosted.org/packages/a8/ba/32b704e077cb24b4d85260512a5af903e772f06fb58e716301dd51758869/chainer-7.0.0.tar.gz (1.0MB)
~以下略~

ダウンロードはすぐ終わりますが、Jetson Nano上ではChainerから呼ばれるNumPyのビルドにかなりかかります。さらに言えば、Chainerをアップデートすると、CPUを使うNumPyの方は漏れなくアップデートしてくれますが、CuPyの方はそのままではしてくれません。別にアップデートします。

$ pip3 install -U cupy
Collecting cupy
  Downloading https://files.pythonhosted.org/packages/dc/89/99f980706c61e6b96a579a81dea3eb68c22df1b526bf357673be5e18fe31/cupy-7.0.0.tar.gz (3.7MB)
~以下略~

これまたCuPyのビルドに時間がかかります。Jetson Nano上ではちょっとハングしたようにも見えるほどだったので、別な仮想端末から中を確認しました。

$ ps ax
~途中略~
9402 pts/0 S+ 0:00 /usr/local/cuda/bin/nvcc -D_FORCE_INLINES=1 -I/usr/local/cuda/include -I/usr/include/python3.
~途中略~
9472 pts/0 S+ 0:00 sh -c cicc --c++11 --gnu_version=70400 --allow_managed --unsigned_chars -arch compute_30 -m
9473 pts/0 R+ 1:48 cicc --c++11 --gnu_version=70400 --allow_managed --unsigned_chars -arch compute_30 -m64 -ftz=
~以下略~

nvcc (NvidiaのCudaコンパイラ・ドライバ)が働いて、しっかりビルドをしているのが見えました。多分 cicc こそがコンパイラの本体部分でしょうが、ちゃんと動いているので、気長に待ちました。

さてようやくアップデートが終わったので、本題である、CuPyの精度の確認をしてみます。単なるそれも短いベクトルの内積計算(それも最適化などしていない)が題材です。テスト用のコードはこんな感じ。

のんべんだらりと乱数を詰め込んで要素毎の掛け算をし、その後全要素のSUMを取るだけのもの。そしてNumPyの結果と、CuPyの結果の差分をとって表示しています。結果3連発!はこちら、

$ python3 mul_sd.py
z.dtype=
 float64
zC.dtype=
 float64
zCs.dtype=
 float32
Diff zSum - zCsum =
 0.0
Diff zSum - zCsSum =
 0.0
$ python3 mul_sd.py
z.dtype=
 float64
zC.dtype=
 float64
zCs.dtype=
 float32
Diff zSum - zCsum =
 -7.105427357601002e-15
Diff zSum - zCsSum =
 0.0
$ python3 mul_sd.py
z.dtype=
 float64
zC.dtype=
 float64
zCs.dtype=
 float32
Diff zSum - zCsum =
 -7.105427357601002e-15
Diff zSum - zCsSum =
 -1.9073486e-06
  1. NumPyの浮動小数点型は何も言わなければ float64 になる
  2. CuPyはNumPyと互換性をとるためか、やはりデフォは float64 のようだ
  3. NumPyもCuPyもdtypeで精度は制御できる

極めて実装の互換性が高いので、npをcpに換えるだけで、上のように簡単なプログラムは動作してしまいます。しかし、GPUを使う、という場合、float64型では地力が出ないであろうでしょうし、GPUに合わせた型をちゃんと選択しなければならないと思いました。なお、3連発の結果をみると

  • 1回目は、CPUfloat64 – GPU float64でも CPUfloat64 – GPU float32でも差が見えない
  • 2回目は、CPUfloat64 – GPU float64 で僅差が生じているが、CPUfloat64 – GPU float32の方は差が見えない
  • 3回目は、CPUfloat64 – GPU float64 で僅差が生じ、CPUfloat64 – GPU float32の方でも差が見える(当然float64の時よりは差がデカい)

浮動小数点のビット表現の奥底の方の微妙な差なので、与えている乱数列により幽霊のような見えたり見えなかったりの挙動をするようです。ちゃんとした数値計算にCuPy使うときは心に留めておかないと。

AIの片隅で(18) Chainer、開発の終わり へ戻る

AIの片隅で(20) Kendryte K210がくる へ進む