Win10のAnaconda環境にclpyをmingwビルドで入れた話

記事タイトル間違ってません
clpy*1というcupyをベースに?バックエンドをOpenCLにしたライブラリが出ていたのでさっそくMinGWでビルドしてみた。

環境

前回と一緒

  • Windows10 pro 64bit
  • Anaconda3 5.1.0
  • pip 9.0.1
  • MinGW w64 7.3.0
  • clpy 2.1.0a0

clpyはGitHubからclone、もしくはzipダウンロードしておく。

前準備

  • g++にパスを通しておく
  • README.mdの通りに環境変数を設定する
    • C_INCLUDE_PATHとCPLUS_INCLUDE_PATHにOpenCLのインクルードパス
    • LIBRARY_PATHにOpenCLのライブラリパス*2
    • CLPY_GENERATE_CUPY_ALIAS=1

CLPY_GENERATE_CUPY_ALIASを設定しておくとclpyと書くところをcupyと書くことができてcupyを使っているような感じでコーディングできるっぽい。


以降は魔改造した内容

distutils

  • Anaconda3\Lib\distutils\ccompiler.py の改造

ccompilerを直接使っているせいなのか、distutils.cfgを見てくれない。ので、無理やりmingw32にする。

 _default_compilers = (

     # Platform string mappings

     # on a cygwin built python we can use gcc like an ordinary UNIXish
     # compiler
     ('cygwin.*', 'unix'),

     # OS name mappings
     ('posix', 'unix'),
+    ('nt', 'mingw32'),
-    ('nt', 'msvc'),

     )
  • Anaconda3\Lib\distutils\cygwinccompiler.py の改造

sys.versionを見てVisual C++ ライブラリを決める箇所をMinGW向けにバージョン変更する

 def get_msvcr():
     """Include the appropriate MSVC runtime library if Python was built
     with MSVC 7.0 or later.
     """
     msc_pos = sys.version.find('MSC v.')
     if msc_pos != -1:
         msc_ver = sys.version[msc_pos+6:msc_pos+10]
         if msc_ver == '1300':
             # MSVC 7.0
             return ['msvcr70']
         elif msc_ver == '1310':
             # MSVC 7.1
             return ['msvcr71']
         elif msc_ver == '1400':
             # VS2005 / MSVC 8.0
             return ['msvcr80']
         elif msc_ver == '1500':
             # VS2008 / MSVC 9.0
             return ['msvcr90']
         elif msc_ver == '1600':
             # VS2010 / MSVC 10.0
             return ['msvcr100']
         elif int(msc_ver) >= 1900:
             # VS2015 / MSVC 14.0
+            return ['msvcr100']
-            return ['msvcr140']
         else:
             raise ValueError("Unknown MS Compiler version %s " % msc_ver)

clpyのinstall\build.py変更

g++には「MANIFEST」というコマンドライン引数は無いので、build_shlib()のpostargsを空っぽの配列に変更する

         try:
+            postargs = []
-            postargs = ['/MANIFEST'] if sys.platform == 'win32' else []
             compiler.link_shared_lib(objects,
                                      os.path.join(temp_dir, 'a'),

いつものcmath改造

mingw\lib\gcc\x86_64-w64-mingw32\7.3.0\include\c++\cmathの「hypot」でコンパイルエラーになるのでコメントアウトする。

-  using ::hypot;
+//  using ::hypot;

インストール

README.mdに書かれている通り、以下で大丈夫と思う。自分はビルド確認をやっていたのでbuildしてからinstallした。

$ python setup.py install

動作確認

pyopenclのハンズオン*3を見ながら書いていたコードをさらに改造して、行列積の結果をclpyと比較してみる。CLPY_GENERATE_CUPY_ALIASを設定してビルドしたので以下の通り「cupy」と書いてclpyを使える。

import pyopencl as cl
from pyopencl import mem_flags
import cupy
import numpy as np
import time

input_size = 1024

context = cl.create_some_context()
queue = cl.CommandQueue(context)

# inputs
lhs_host = np.random.randn(input_size, input_size).astype(np.float32)
rhs_host = np.random.randn(input_size, input_size).astype(np.float32)
lhs_cl = cupy.asarray(lhs_host)
rhs_cl = cupy.asarray(rhs_host)
lhs_dev = cl.Buffer(context, mem_flags.READ_ONLY|mem_flags.COPY_HOST_PTR, hostbuf=lhs_host)
rhs_dev = cl.Buffer(context, mem_flags.READ_ONLY|mem_flags.COPY_HOST_PTR, hostbuf=rhs_host)

# outputs
result_host_pycl = np.empty((input_size, input_size), np.float32)
result_dev = cl.Buffer(context, mem_flags.WRITE_ONLY, result_host_pycl.nbytes)
result_cl = cupy.array(result_host_pycl) # 暫定コード。emptyなcupy.ndarrayを作る方法が不明。

# pyopenclの行列積kernel(最適化なし)
program = cl.Program(context, '''
__kernel void mul(
    __global const float *a,
    __global const float *b,
    __global float *c,
    const int n
)
{
    const int i = get_global_id(0);
    const int j = get_global_id(1);
    const int index = j*n + i;

    float accm = 0;
    for (int k=0;k<n;k++) {
        accm += a[j*n + k] * b[k*n + i];
    }
    c[index] = accm;
}
''').build()

n = np.int32(input_size)

start = time.time()

# cupy行列積
result_cl = cupy.dot(lhs_cl, rhs_cl)

# pyopencl行列積
ev = program.mul(queue, lhs_host.shape, None, lhs_dev, rhs_dev, result_dev, n)
ev.wait()

stop = time.time()

# result_host_pycl <- result_dev
cl.enqueue_copy(queue, result_host_pycl, result_dev)

result_host_cupy = cupy.asnumpy(result_cl)

# 差分の絶対値から最大(差分が最大)のものを表示
print('clpy vs pyopencl diff(max):', np.max(np.abs(result_host_cupy - result_host_pycl)))
print(np.all(result_host_cupy == result_host_pycl)) # 全部同値ならTrueのつもり

print('CPU vs clpy diff(max):', np.max(np.abs(np.dot(lhs_host, rhs_host) - result_host_cupy)))
print(np.all(np.dot(lhs_host, rhs_host) == result_host_cupy))

print('exec time:', stop - start, 'sec')

実行結果はこんな感じ。GPU側はfmaが使われてCPUと結果が一致しないと思われる。

>python clpy-test.py
Choose platform:
[0]
Choice [0]:0
Choose device(s):
[0]
[1]
Choice, comma-separated [0]:1
Set the environment variable PYOPENCL_CTX='0:1' to avoid being asked again.
clpy vs pyopencl diff(max): 0.0
True
CPU vs clpy diff(max): 0.00021362305
False
exec time: 0.37632179260253906 sec

*1:https://github.com/fixstars/clpy

*2:OpenCLSDKを入れてない場合は「C:\Windows\System32」を入れておけばOpenCL.dllとリンクしてくれる

*3:http://pykyoto201109-pyopencl.s3-website-ap-northeast-1.amazonaws.com/pyopencl.html