tkm2261's blog

研究員(OR屋) → データ分析官 → MLエンジニア → ニート → 米国CS PhDが諸々書いてます

Python高速化 Numba入門 その5

今回はGPUの数値実験についての記事です。

計算環境

OS: Windows 7 64bit
CPU: Intel Core i7-4771 CPU @ 3.50GHz
GPU: NVIDIA GeForce GTX 650
MEM: 32GB

我が家の計算マシンです。
これをWindowsで構成しているのは、もったいない気がしていますが、
LinuxGPU載せても用途が限られすぎるので妥協です。

環境構築

ここまで、特に環境構築について触れて来ませんでしたがGPUを使うと苦労したので少し書きます。

結論としては、『Anacondaを入れましょう!』が最善手のようです。

Download Anaconda Python Distribution

Win機64bitで環境を揃えるのはかなりめんどくさいです。というか頑張ったんですが
エラーが直らず断念しました

GPU使わないなら、GohlkeさんのページからNumbaとllvmliteを入れればお手軽に動かすことが出来ます。

Python Extension Packages for Windows - Christoph Gohlke

Win機のGPGPUは大体面倒ですが、世のGPUはゲーム用途でかなりWin機に搭載されているので、このミスマッチはいかんともしがたい感じです。

その他ライブラリはこんな感じ。記事を書いている間にnumbaの0.16が出たのでアップデートしています。

  • python=2.7.8
  • numba=0.16.0
  • numpy=1.9.1
  • llvmlite=0.2.0

実際に動かしてみた

その3まで同様に1,000地点間の3次元の距離行列を求めるコードで実験してみます。

# coding: utf-8
import numpy
from numba import double
from numba.decorators import jit
from numba import guvectorize
from numba import cuda
import math
import time

def pairwise_python(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)
    return D

@jit
def pairwise_numba(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)


@jit('f8[:, :](f8[:, :], f8[:, :])', nopython=True)
def pairwise_numba_jit(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)

    return D


@guvectorize(['void(f8[:, :], f8[:, :])'], '(x, y)->(x, x)')
def pairwise_numba_guvec(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)

@cuda.jit('void(f8[:, :], f8[:, :])')
def pairwise_numba_cuda1(X, D):
    M = X.shape[0]
    N = X.shape[1]
    
    i, j = cuda.grid(2)
    
    if i < M and j < M:
        d = 0.0
        for k in range(N):
            tmp = X[i, k] - X[j, k]
            d += tmp * tmp
            
        D[i, j] = math.sqrt(d)

if __name__ == '__main__':
    N = 1000
    dim = 3
    
    t = time.time()
    numpy.random.seed(0)
    X = numpy.random.random((N, dim))
    D = numpy.empty((N, N))
    pairwise_python(X, D)
    print "python           :", time.time() - t

    t = time.time()
    numpy.random.seed(0)
    X = numpy.random.random((N, dim))
    D = numpy.empty((N, N))
    pairwise_numba(X, D)
    print "numba_jit        :", time.time() - t

    t = time.time()
    numpy.random.seed(0)
    X = numpy.random.random((N, dim))
    D = numpy.empty((N, N))
    pairwise_numba_jit(X, D)
    print "numba_jit_type   :", time.time() - t

    t = time.time()
    numpy.random.seed(0)
    X = numpy.random.random((N, dim))
    D = numpy.empty((N, N))
    pairwise_numba_guvec(X)
    print "numba_guvectorize:", time.time() - t

    
    t = time.time()
    numpy.random.seed(0)
    X = numpy.random.random((N, dim))
    D = numpy.empty((N, N))
    griddim = 100, 100
    blockdim = 16, 16
    pairwise_numba_cuda1[griddim, blockdim](X, D)
    print "numba_cuda       :", time.time() - t

結果は以下、

blockdim: (16, 16)
griddim: (63, 63)
python           : 2.21000003815     #普通のPython
numba_jit        : 0.0870001316071   #型指定なしCPU
numba_jit_type   : 0.0090000629425   #型指定ありCPU
numba_guvectorize: 0.0090000629425   #numpyのGUFunc
numba_cuda       : 0.0139999389648   #GPU

あれ?GPU速くない・・・
問題サイズが小さいからかもとのことで、1,000地点間の距離から10,000地点間の距離に変更して実行してみる。
普通のPythonはまともに計算出来ないのでここで退場

blockdim: (16, 16)
griddim: (626, 626)
numba_jit        : 0.734000205994  #型指定なしCPU
numba_jit_type   : 0.90399980545   #型指定ありCPU
numba_guvectorize: 0.888999938965  #numpyのGUFunc
numba_cuda       : 0.59299993515   #GPU

3割程度速くなった。GPUを導入したからといって劇的な改善があるわけではなさそう。

また、意図的にブロック数を増やしてみると

blockdim: (16, 16)
griddim: (5000, 5000)
numba_jit        : 0.733999967575
numba_jit_type   : 0.888999938965
numba_guvectorize: 0.858000040054
numba_cuda       : 2.18400001526

オーバーヘッドでかなり遅くなることがわかった。GPU君は高速化には寄与するものの、しっかりしたチューニングが必要そうな感じです。

Shared Memoryの効果検証

前回も触れた、行列をブロックに分けてそれをShared Memoryに格納して行列積を行う方法を検証してみる。
しかし、こいつはブロックの分けるのに端数が出ると、実装が面倒なので、

今回はブロックサイズ16、ブロック数50として行列サイズN=50×16=800の正方行列の積で検証する

実行したコードが以下

# coding: utf-8
import numpy
from numba import double
from numba.decorators import jit
from numba import guvectorize
from numba import cuda
import math
import time
from numba import float32, float64

bpg = 50
tpb = 16
n = bpg * tpb

#@cuda.jit('void(f4[:, :], f4[:, :], f4[:, :])')
@cuda.jit('void(f8[:, :], f8[:, :], f8[:, :])')
def cu_square_matrix_mul_shared(A, B, C):
    sA = cuda.shared.array(shape=(tpb, tpb), dtype=float64)
    sB = cuda.shared.array(shape=(tpb, tpb), dtype=float64)

    tx = cuda.threadIdx.x
    ty = cuda.threadIdx.y
    bx = cuda.blockIdx.x
    by = cuda.blockIdx.y
    bw = cuda.blockDim.x
    bh = cuda.blockDim.y

    x = tx + bx * bw
    y = ty + by * bh

    acc = 0.
    for i in range(bpg):
        if x < n and y < n:
            sA[ty, tx] = A[y, tx + i * tpb]
            sB[ty, tx] = B[ty + i * tpb, x]

        cuda.syncthreads()

        if x < n and y < n:
            for j in range(tpb):
                acc += sA[ty, j] * sB[j, tx]

        cuda.syncthreads()

    if x < n and y < n:
        C[y, x] = acc
        
#@cuda.jit('void(f4[:, :], f4[:, :], f4[:, :])')
@cuda.jit('void(f8[:, :], f8[:, :], f8[:, :])')
def cu_square_matrix_mul(A, B, C):
    M = A.shape[0]
    K = A.shape[1]
    N = B.shape[1]
    i, j = cuda.grid(2)
    
    if i < M and j < N:
        acc = 0.
        for k in range(K):
            acc += A[i, k] * B[k, j]
        C[i, j] = acc
        
if __name__ == '__main__':
    M = n
    K = n
    N = n
    blocknum = 16
    blockdim = blocknum, blocknum
    dtype = numpy.float64
    griddim = int(N / blocknum), int(N / blocknum)
    print 'blockdim:', blockdim
    print 'griddim:', griddim   
    
    
    t = time.time()
    numpy.random.seed(0)
    A = numpy.random.random((M, K)).astype(dtype)
    B = numpy.random.random((K, N)).astype(dtype)
    C = numpy.empty((M, N)).astype(dtype)
    cu_square_matrix_mul[griddim, blockdim](A, B, C)
    print "cu_square_matrix_mul        :", time.time() - t
    #print numpy.allclose(C, numpy.dot(A, B))
    
    t = time.time()
    numpy.random.seed(0)
    A = numpy.random.random((M, K)).astype(dtype)
    B = numpy.random.random((K, N)).astype(dtype)
    C = numpy.empty((M, N)).astype(dtype)
    cu_square_matrix_mul_shared[griddim, blockdim](A, B, C)
    print "cu_square_matrix_mul_shared :", time.time() - t
    #print numpy.allclose(C, numpy.dot(A, B))

結果は

blockdim: (16, 16)
griddim: (50, 50)
cu_square_matrix_mul        : 0.359000205994 #Shared Memory なし
cu_square_matrix_mul_shared : 0.077999830246 #Shared Memory あり

約5倍の速度向上となった。
この速度向上なら試す価値はありそうだが、メモリの使い方が職人芸過ぎて浸透しない気がする。。。

行列積をやりたいだけなら、 NVIDIAがcuBLASを提供しているのでそちらが良さそう。

Numbaの有償版のNumbaProにはcuBLASのAPIが提供されているので、お金を払うのも1手です。

CUDA Libraries Host API — Continuum documentation

単精度を試してみる

上のコードは倍精度の計算になっているが、昔はGPUは単精度計算のみだったので速度に寄与するか見てみる。

blockdim: (16, 16)
griddim: (50, 50)
cu_square_matrix_mul        : 0.34299993515   #Shared Memory なし
cu_square_matrix_mul_shared : 0.0940001010895 #Shared Memory あり

誤差レベルと言った感じ。
ただGPUのメモリは1GB程度なことが多いので、
メモリ使用量の面で単精度で良い場合は単精度が良いと思います

最後に

総じてNumbaはPythonの高速化には結構良さげです。

特にCPU利用の場合は、
Cythonほどコードをイジらずに数百倍の高速化が見込めるのは大きいと思います。

一方、GPU利用は、労力の割に益が少ない印象です。
コードもGPU専用に書くので当初の目的からは今ひとつかなと・・・

ただし、最大2兆並列は特定の処理では効果絶大だと思いますので、
知っていて良い知識であることは確かです。

さらにcuBLASのAPIを使うのは良さ気なので$130払って、numpyの高速化を目指すのは良いと思います。
numpyのMKLビルドも付いて来るので悪くないです。

これにてNumba入門は終了です。この後しばらくしたら応用編で、私が勉強するモチベとなった問題を話せればと思います。

Python高速化 Numba入門 その4

今回はNumbaのGPUコンピューティングについて読んでいきます。
最終回の予定でしたが、エントリが超長くなりそうなので今回はGPUの使用方法、次回に計算速度の検証をして終わりたいと思います。

Writing CUDA-Python — numba 0.15.1 documentation

exprimental扱いなので、商用で使われる方はNumbaProの方をオススメします。

NumbaのGPUコンピューティングはこれまでのデコレータだけれ使えるNumbaの状況とは異なり、かなりCUDAに関する知識を要求されます。

もし、少ない労力でPythonコードを高速化したい方は、『その3』までの内容で十分と思われます。

CUDAについて

とりあえず、Wikipediaから引用

CUDA(Compute Unified Device Architecture:クーダ)とは、NVIDIAが提供するGPU向けのC言語統合開発環境であり、コンパイラ (nvcc) やライブラリなどから構成されている。アプリケーションを実行する基盤となるプラットフォーム/アーキテクチャそのものをCUDAと呼ぶこともある。 CUDA - Wikipedia

つまりNVIDIAGPUGeForce等)でプログラミングするための環境になります。

詳しいことはここがわかりやすかったので紹介。後の議論を理解するには、このページをひと通り読まれることをオススメします。

第6回 CUDAプログラミングモデル① | G-DEP

簡単に説明すると、GPU

グリッド -> ブロック -> スレッド 

という階層構造になっており、1グリッドに65,535×65,535個のブロックを配置でき、1ブロックで512個のスレッドを実行することが出来ます。

つまり65,535×65,535×512並列の計算ができることになります。

計算すると大体2兆並列できるとのこと。

GPUすげー・・・

Numbaでの表記

基本的な表記は以下となっています。

# coding: utf-8

from numba import cuda

@cuda.jit('void(int32[:], int32[:])')
def foo(aryA, aryB):
    ...

if __name__ == '__main__':
    griddim = 1, 2
    blockdim = 3, 4
    foo[griddim, blockdim](aryA, aryB)

ポイントを挙げると

  1. デコレータの指定 ・・・ これまでと同様に型を指定 cuda.jitでなくjitにtarget='gpu'を指定しても良い
  2. griddim, blockdimの指定 ・・・ 今回使用するブロック数(griddim)と、ブロック内のスレッド数(blockdim)を指定する。

griddim, blockdimの数は、スレッドIDをインデックスに使用する場合にはインデックスより多くのスレッドを用意できる様に指定する。

ただしblockdimは32の倍数にしたほうが良いとのことでした。

また@cuda.autojitデコレータでは型を推測してくれますが、基本的に遅くなるので指定した方が無難です。

デコレータの引数

@cuda.jitデコレータには以下の引数を指定することができます。

  • device=True ・・・ GPU上でのみ使える関数
  • inline=True ・・・ 上のデバイス関数をインライン化するかどうか。このインラインのことを指していると思われる。インライン展開 - Wikipedia

スレッドの特定

上の第6回 CUDAプログラミングモデル① | G-DEPを読まれた方は見たと思いますが、 CUDAの計算では、どのスレッド何を計算させるかを指定する必要があります。

基本的には(ほぼ必ず)スレッドの番号を使用します。
配列同士の和は、スレッド番号0に1番目の要素の和を、スレッド番号1には2番目の要素の和といった形になります。
グリッドが2次元まで対応しているので2次元配列まではシンプルに計算することが出来ます。

そうでないと2兆個の命令を人間が出すのはほぼ不可能です。

並列計算全般がそうですが、同期の必要のない大量の単純作業をさせるとGPUの効果が最大限発揮されます。

スレッド番号の取得方法は2種類用意されてます。

1: 普通のCUDAと同じに愚直に計算

グリッドが1次元の場合

tx = cuda.threadIdx.x
bx = cuda.blockIdx.x
bw = cuda.blockDim.x
i = tx + bx * bw
array[i] = something(i)

グリッドが2次元の場合

tx = cuda.threadIdx.x
ty = cuda.threadIdx.y
bx = cuda.blockIdx.x
by = cuda.blockIdx.y
bw = cuda.blockDim.x
bh = cuda.blockDim.y
x = tx + bx * bw
y = ty + by * bh
array[x, y] = something(x, y)

行列計算を自分で書いたことある方には馴染み易い方法と思います。

2: Numbaの便利関数を使う

グリッドが1次元の場合

i = cuda.grid(1)
array[i] = something(i)

グリッドが2次元の場合

x, y = cuda.grid(2)
array[x, y] = something(x, y)

好きな方を使えば良いと思います。

メモリ転送

こっからかなりディープな世界に入って来ます。
ここまでの解説で普通の用途では十分な速度向上が見られると思います。

ただし、最大限に性能を引き出すには必須なので、メモリを制する者はGPUを制すといった感じでしょうか

詳細はコチラを見て頂きたいのですが、メモリも色々あります。

第5回 GPUの構造 | G-DEP

ここではメモリの使用を制御する以下の2つを紹介します。

  1. CUDA Streamの使用            
  2. Shared Memoryの使用

CUDA Streamの使用

通常のメモリの転送は以下になっています。

  • マシンメモリ→GPUメモリの転送・・・非同期
  • マシンメモリ←GPUメモリの転送・・・同期

CUDA Streamを使用すると『マシンメモリ←GPUメモリの転送』も非同期にすることが出来ます。
使用方法は以下。

stream = cuda.stream()
devary = cuda.to_device(an_array, stream=stream)
a_cuda_kernel[griddim, blockdim, stream](devary)
devary.copy_to_host(an_array, stream=stream)
# data may not be available in an_array
stream.synchronize()
# data available in an_array

with構文で以下のようにもかけます。

stream = cuda.stream()
with stream.auto_synchronize():
    devary = cuda.to_device(an_array, stream=stream)
    a_cuda_kernel[griddim, blockdim, stream](devary)
    devary.copy_to_host(an_array, stream=stream)
# data available in an_array

私自身もCUDA Streamの使用用途を見つけられていませんが、良かったら使って見て下さい。

Shared Memoryの使用

Shared Memoryが同一ブロックのスレッドが参照できるメモリで、かなり高速にアクセスすることが出来ます。Numbaマニュアルの例では行列積をブロック化して計算しています。

有名な例なので日本語でも資料があり、以下が非常にわかりやすい説明でした。

CUDAで行列演算:乗算(シェアードメモリ使用版) - PukiWiki for PBCG Lab

この例でもShared Memoryの使用で5~10倍高速化しており、効果が高いことがわかります。

NumbaマニュアルのShared Memoryを使った行列積コードは以下

bpg = 50
tpb = 32
n = bpg * tpb

@jit(argtypes=[float32[:,:], float32[:,:], float32[:,:]], target='gpu')
def cu_square_matrix_mul(A, B, C):
    sA = cuda.shared.array(shape=(tpb, tpb), dtype=float32)
    sB = cuda.shared.array(shape=(tpb, tpb), dtype=float32)

    tx = cuda.threadIdx.x
    ty = cuda.threadIdx.y
    bx = cuda.blockIdx.x
    by = cuda.blockIdx.y
    bw = cuda.blockDim.x
    bh = cuda.blockDim.y

    x = tx + bx * bw
    y = ty + by * bh

    acc = 0.
    for i in range(bpg):
        if x < n and y < n:
            sA[ty, tx] = A[y, tx + i * tpb]
            sB[ty, tx] = B[ty + i * tpb, x]

        cuda.syncthreads()

        if x < n and y < n:
            for j in range(tpb):
                acc += sA[ty, j] * sB[j, tx]

        cuda.syncthreads()

    if x < n and y < n:
        C[y, x] = acc

アトミック操作

ここで説明すると長くなるので詳細は以下を参照。簡単に言うとメモリアクセスのブロックが必要な操作のための関数です。

CUDAアトミック関数 - PukiWiki for PBCG Lab

Numbaでの実行方法は以下

@cuda.jit('void(uint32[:], uint32[:])')
def fill_bins(indices, bin_counts):
    offset = cuda.grid(1)
    stride = cuda.gridDim.x * cuda.blockDim.x

    for i in range(offset, indices.shape[0], stride):
        cuda.atomic.add(bin_counts, indices[i], 1) # (対象配列, インデックス, 足す値)

『実際にやってみた』は次回

長々話しましたが、話を見るだけでは正直わからないと思います。
早めに実際のコードも上げるのでそちらを見ながら、当記事を振り返って頂ければと思います。

ライトな用途ではメモリ転送以下の話は無視したほうが良いかもしれません。

Python高速化 Numba入門 その3

今回もNumbaのドキュメントを読んで行きます。

Numba — numba 0.15.1 documentation

と思ったんですが、読み進めて行くと以外に紹介する内容が少ないことに気づきました。

シンプルなのは良いことなので、今回はUFuncを紹介して、
次回にGPUについて紹介して入門編は一旦終わりたいと思います。

その後に、Numbaを勉強する動機となったものを中心に応用編を話す予定です。

UFunc

UFuncs — numba 0.15.1 documentation

UFunc(ユニバーサル関数)とは、要するにnumpy.ndarrayに対して要素ごとに演算してくれる関数です。

例えばnumpy.logに配列を食わせると、こんな感じです。

>>> import numpy
>>> hoge = numpy.arange(1, 5)
>>> hoge
array([1, 2, 3, 4])
>>> numpy.log(hoge)
array([ 0.        ,  0.69314718,  1.09861229,  1.38629436])

numpy自体にもUFuncの書き方はあるみたいですが、Numbaだとこれもデコレータ一発で行けるようなので見ていきます。

基本は以下の構文。@vectorizeデコレータにリストで型を指定する。

from numba import vectorize
import math
import numpy

@vectorize(['float64(float64, float64)'])
def my_ufunc(x, y):
    return x+y+math.sqrt(x*math.cos(y))

a = numpy.arange(1.0, 10.0)
b = numpy.arange(1.0, 10.0)
# Calls compiled version of my_ufunc for each element of a and b
print(my_ufunc(a, b))

結果は

[  2.73505259          nan          nan          nan  11.1909286
  14.40021285  16.29724091          nan          nan]

マイナスのルート取ろうてして・・・nanが発生。これが例でいいのか・・・

複数の型を受け取りたい時はデコレータの引数のリストにひたすら足していく。

from numba import vectorize
import math
import numpy

@vectorize(['int32(int32, int32)',
            'float64(float64, float64)'])
def my_ufunc(x, y):
    return x+y+math.sqrt(x*math.cos(y))

a = numpy.arange(1.0, 10.0, dtype='f8')
b = numpy.arange(1.0, 10.0, dtype='f8')
print(my_ufunc(a, b))

a = numpy.arange(1, 10, dtype='i4')
b = numpy.arange(1, 10, dtype='i4')
print(my_ufunc(a, b))
[  2.73505259          nan          nan          nan  11.1909286
  14.40021285  16.29724091          nan          nan]
C:\Python27\Scripts\ipython-script.py:16: RuntimeWarning: invalid value encountered in ufunc
[          2 -2147483648 -2147483648 -2147483648          11          14
          16 -2147483648 -2147483648]

@jitみたいに@vectorizeにも型推定ないかなと思い実行。

from numba import vectorize
import math
import numpy

@vectorize
def my_ufunc(x, y):
    return x+y+math.sqrt(x*math.cos(y))

a = numpy.arange(1.0, 10.0)
b = numpy.arange(1.0, 10.0)
# Calls compiled version of my_ufunc for each element of a and b
print(my_ufunc(a, b))
TypeError: wrap() takes exactly 1 argument (2 given)

@vectorizeはしっかり型を指定する必要があるらしい。

既存の関数を後から、UFuncBuilderでUFuncにする方法もある。

from numba.npyufunc.ufuncbuilder import UFuncBuilder
from numba import f8, i4

import math
import numpy

def my_ufunc(x, y):
    return x+y+math.sqrt(x*math.cos(y))

builder = UFuncBuilder(my_ufunc)
builder.add(restype=i4, argtypes=[i4, i4])
builder.add(restype=f8, argtypes=[f8, f8])

compiled_ufunc = builder.build_ufunc()

a = numpy.arange(1.0, 10.0, dtype='f8')
b = numpy.arange(1.0, 10.0, dtype='f8')
print(compiled_ufunc(a, b))

Generalized Ufuncs

こちらは私もよく分かっていませんが、わかっている範囲でご紹介します。

通常のUFuncは要素ごとの処理を行っていましたが、Generalized Ufuncsでは内部の配列ベースで処理を行ってくれるそうです。

C言語でも単に要素のforを回すよりも、配列としてBLASに投げた方が速いので動機としては理解できます。

import math
import numpy
from numba import guvectorize

@guvectorize(['void(int32[:,:], int32[:,:], int32[:,:])',
              'void(float64[:,:], float64[:,:], float64[:,:])'],
              '(x, y),(x, y)->(x, y)')
def my_gufunc(a, b, c):
    for i in range(c.shape[0]):
        for j in range(c.shape[1]):
            c[i, j] = a[i, j] + b[i, j]

a = numpy.arange(1.0, 10.0, dtype='f8').reshape(3,3)
b = numpy.arange(1.0, 10.0, dtype='f8').reshape(3,3)
# Calls compiled version of my_gufunc for each row of a and b
print(my_gufunc(a, b))

型を指定するところは@vectorizeと同じですが、デコレータの第2引数と、本体の関数に戻り値となる引数が増える様です。

デコレータの引数は内部の配列をどう扱うかを指定しています。詳しい話は以下を参考にして下さい。

Generalized Universal Function API — NumPy v1.9 Manual

ちょっと特殊ですが慣れれば行けそうです。

実際にやってみた

前回の@jitと@guvectorizeはどちらが速いか検証します。

# coding: utf-8
import numpy
from numba import double
from numba.decorators import jit
from numba import guvectorize
import time
import traceback

def pairwise_python(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)
    return D

@jit
def pairwise_numba(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)


@jit('f8[:, :](f8[:, :], f8[:, :])', nopython=True)
def pairwise_numba2(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)

    return D


@guvectorize(['void(f8[:, :], f8[:, :])'], '(x, y)->(x, x)')
def pairwise_numba3(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)



if __name__ == '__main__':

    t = time.time()
    X = numpy.random.random((1000, 3))
    D = numpy.empty((1000, 1000))
    pairwise_python(X, D)
    print "python           :", time.time() - t

    t = time.time()
    X = numpy.random.random((1000, 3))
    D = numpy.empty((1000, 1000))
    pairwise_numba(X, D)
    print "numba_jit        :", time.time() - t

    t = time.time()
    X = numpy.random.random((1000, 3))
    D = numpy.empty((1000, 1000))
    pairwise_numba2(X, D)
    print "numba_jit_type   :", time.time() - t

    t = time.time()
    X = numpy.random.random((1000, 3))
    D = numpy.empty((1000, 1000))
    pairwise_numba3(X)
    print "numba_guvectorize:", time.time() - t

結果は・・・

python           : 4.63400006294
numba_jit        : 0.131000041962
numba_jit_type   : 0.010999917984
numba_guvectorize: 0.0119998455048

特に@guvectorizeだからと言って速いわけではないようだ、
私の書き方が正しくない気もするが・・・

次回はGPUの話をする予定
GPU積んでるのがWindows機なので若干手間取るかもしれませんが、頑張ります。

Python高速化 Numba入門 その2

今回は、QuickStartを読んでいきます。

Quick Start — numba 0.15.1 documentation

とりあえず、前回の@jitデコレータだけで動くのは理解した。

from numba import jit

@jit
def sum(x, y):
    return x + y

引数と戻り値の型が陽にわかっている場合には、@jitの引数に『戻り値の型』(引数1の型, 引数2の型, ...)で指定できる。恐らく早くなるのだろう

@jit('f8(f8,f8)')
def sum(x, y):
    return x + y

numpyの配列もサポート。多次元配列はこんな感じf8[:, :, :]

@jit('f8(f8[:])')
def sum1d(array):
    sum = 0.0
    for i in range(array.shape[0]):
        sum += array[i]
    return sum

サポートされている型は以下。主要なものは大体ある。

Type Name Alias Result Type
boolean b1 uint8 (char)
bool_ b1 uint8 (char)
byte u1 unsigned char
uint8 u1 uint8 (char)
uint16 u2 uint16
uint32 u4 uint32
uint64 u8 uint64
char i1 signed char
int8 i1 int8 (char)
int16 i2 int16
int32 i4 int32
int64 i8 int64
float_ f4 float32
float32 f4 float32
double f8 float64
float64 f8 float64
complex64 c8 float complex
complex128 c16 double complex

型は文字列で指定しなくても、importして指定もできる。ハードコーディングにならないのでこっちのが嬉しい。

from numba import jit, f8

@jit(f8(f8[:]))
def sum1d(array):
    ...

型がわからない場合は、通常のPythonで処理するので異常に遅くなってしまう。それを避けるためにPythonでの処理を禁止することもできる。
どうしても型がわからない場合はエラー吐くのかな?

@jit(nopython=True)
def sum1d(array):
    ...

Numbaの型推論の結果を取得するには、inspect_typesメソッドが用意されている。これを参考にデコレータの引数を決めるのも良さそう

sum1d.inspect_types()

実際にやってみる

とりあえず、型がわからない時に、nopython=Trueするとどうなるか

from numba.decorators import jit
import numpy

class DummyClass(object):
    def __init__(self):
        hoge = 0
        huga = 2
        hogo = "hogehoge"

@jit('f8[:, :](f8[:, :], f8[:, :])', nopython=True)
def pairwise_numba3(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)

    return DummyClass()

結果は、やはり推論出来ない型でnopython=Trueはエラーらしい

TypingError: Failed at nopython frontend
Untyped global name 'DummyClass'

型指定したほうが速いのか?

検証のために以下を実行

# coding: utf-8
import numpy
from numba import double
from numba.decorators import jit
import time
import traceback

@jit
def pairwise_numba(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)

    return D

@jit('f8[:, :](f8[:, :], f8[:, :])', nopython=True)
def pairwise_numba2(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)

    return D

if __name__ == '__main__':

    t = time.time()
    X = numpy.random.random((1000, 3))
    D = numpy.empty((1000, 1000))
    pairwise_numba(X, D)
    print "numba:", time.time() - t

    t = time.time()
    X = numpy.random.random((1000, 3))
    D = numpy.empty((1000, 1000))
    pairwise_numba2(X, D)
    print "numba2:", time.time() - t

結果は・・・

numba: 0.134999990463
numba2: 0.0120000839233

10倍高速化!

前回、普通のPythonから33倍高速になったので、型指定まですると330倍高速化したことに。

Numba結構イケるやん!

次回は、ユーザーガイドの続きか、First Steps with numbaを読む予定

Python高速化 Numba入門 その1

みなさん、こんにちは
今日からPython高速化 Numbaに入門したいと思います。

入門資料を探しに来た皆様すみませんが、 本記事は私がこれから入門する内容になります。

結果として入門資料に慣れば幸いですが、過度な期待は御無用でお願いします。

基本的には以下を読み進めて行きます。

http://numba.pydata.org/

Numbaとは

JIT(just-in-time)コンパイラを使ってPythonを高速化しよう!』というPythonモジュールです。

LLVMコンパイラを使っており、これはJuliaが高速な理由でもあるので期待大です。

学生時代はCythonを使って高速化をよくしていましたが、以下の理由により今回はNumbaを学びます

候補 今回諦めた理由
Cython cdefとか結構手を入れるのでPythonに戻すのが面倒。pyximportも面倒
C拡張 C言語は極力触りたくない
PyPy numpyサポートが怪しい。いつか触るかも
Julia なるべくPythonでやりたい。これでダメなら入門するかも。

Numbaはどうやらデコレータ一発で一応動くらしい。Cythonよりは使いやすいことを期待したい。

とりあえず通常Pythonと比較

以下のサイトのコードを参考に速度を計測

Numba vs Cython

ちょっと手を加えて、実行したのが以下のコードで比較

# coding: utf-8
import numpy
from numba import double
from numba.decorators import jit
import time

@jit
def pairwise_numba(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)


def pairwise_python(X, D):
    M = X.shape[0]
    N = X.shape[1]
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)


if __name__ == '__main__':
    t = time.time()
    X = numpy.random.random((1000, 3))
    D = numpy.empty((1000, 1000))
    pairwise_python(X, D)
    print "python:", time.time() - t

    t = time.time()
    X = numpy.random.random((1000, 3))
    D = numpy.empty((1000, 1000))
    pairwise_numba(X, D)
    print "numba:", time.time() - t

結果は

python: 4.64800000191
numba: 0.137000083923

34倍の高速化!イケるやん!

次回はQuick Startを読む予定

最適化超入門

SlideShareだけでなく、ブログの記事にもすることに

先日、TokyoWebMning #40にて最適化について熱く語ってきました。

個人的にも結構やりきった感があり、網羅的に最適化手法を紹介出来たと思います。

その後飲んだ研究室の同期には『難しすぎる』と言われましたが、どうなんでしょう・・・

一応はてブが400を超えており、大丈夫だったと信じたい。

というか、最適化の話題ではてブこんなに頂けるのは予想外でした。ありがとうございます!

はてなブックマーク - 最適化超入門

PyData Tokyoの方にもお声掛け頂いたのでまたどこかでお話出来ればと思ってます。
次はセクシー女優みたいにしょーもない話にする予定ですw  
 
 

今回は修士卒の人間が最適化の入門資料を作る事のは、おこがましいと戦々恐々していたのですが、 Twitter上では概ね好評であり、安心しております。

NIIの前原先生からもお褒め頂いたので、最適化チートシート流行らそうと思いますw

f:id:tkm2261:20141206141012p:plain

あと、阪大の梅谷先生からご指摘があり、CPLEXは出来る子とのことです。

CPLEXはコア単位課金でお高いため、企業ではほぼGurobi一択な現状なので誇大に書きすぎたようです。
あとSOS制約の入った問題が高速だったため、うちの会社ではCPLEXからGurobiに移行しました。

研究で使われるかたはどちらもフリーなので、mpsファイルでも吐いて検証してみることをオススメします。

ちなみに、Gurobiは先日バージョン6.0がリリースされ、イベントに参加してきました。

Company - News - Highlights of Gurobi Optimizer 6.0

目玉としては、分散MIPと区分線形関数でした。
MIPも分散する時代になったか・・・といった感じです

区分線形関数もSOS制約でいちいち書かなくて済むので、応用事例が増えることが期待出来そうです。

合わせてBixby氏からNFLの試合日程最適化の話があり、非常に興味深い話でした。 2014年から木曜日に試合を行うことなり、モデルが異常に難しくなったようです。

新日鉄住金ソリューションズの山本さんからもJリーグの日程最適化の話があり、
うちの会社でもスポーツ系やりたい( ´Д`) 

長くなりましたが、益々最適化が流行ればいいな!と思う次第です。

Word2vecで大谷翔平の二刀流論争に終止符を打つ!

皆様、お久しぶりです。

VOYAGE GROUPさん主催の14' Data Scientist MeetUpでLTした時の資料 3時間で作った資料なので超絶適当です。

リファクタしてから上げようかと思いましたが、忙しくて断念

ネタ的にお蔵入りは勿体無かったので、そのまま上げています。

気が向いたらなんかやるかも