google-apputilsって何者?

Operations Research関連のオープンソースソフトウェアを探していたら,2010年9月から始まってgooglerが中心となって開発しているようなor-toolsというプロジェクトを見つけました.or-toolsはswigを使ったPythonのライブラリになっていてサンプルコードも多数含まれているのですが,サンプルコード一部(例えばknapsack.py)の中に下のような見慣れないコードがありました.

from google.apputils import app
# 省略
def main(unused_argv):
    # 省略

if __name__ == '__main__':
    app.run()

google-apputils-pythonとはGoogle内製のアプリでも使用されてる便利ツールのようです.残念ながらまだドキュメントが充実していないようなのですが,どんなことができるのか簡単に見ていきましょう.まずは次のようなシンプルなPythonファイルを用意します.

#!/usr/bin/env python

'''How to use this module'''
from google.apputils import app

def main(argv):
    if len(argv) < 2:
        raise app.UsageError('usage error')
    print argv

if __name__ == '__main__':
    app.run()

sample.pyとして実行します.

$ sample.py --help
How to use this module
flags:

google.apputils.app:
  -?,--[no]help: show this help
  --[no]helpshort: show usage only for this module
  --[no]helpxml: like --help, but generates XML output
  --[no]run_with_pdb: Set to true for PDB debug mode
    (default: 'false')
  --[no]run_with_profiling: Set to true for profiling the script. Execution will
    be slower, and the output format might change over time.
    (default: 'false')
  --[no]show_build_data: show build data and exit

gflags:
  --flagfile: Insert flag definitions from the given file into the command line.
    (default: '')
  --undefok: comma-separated list of flag names that it is okay to specify on
    the command line even if the program does not define a flag with that name.
    IMPORTANT: flags in this list that have arguments MUST use the --flag=value
    format.
    (default: '')
$ sample.py --helpshort
How to use this module
$ sample.py
How to use this module

usage error
$ sample.py arg
['./sample.py', 'arg']
$ sample.py --run_with_profiling arg
['./sample.py', 'arg']
         4 function calls in 0.002 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 :0(len)
        1    0.002    0.002    0.002    0.002 :0(setprofile)
        1    0.000    0.000    0.002    0.002 profile:0(<function main at 0x1010679b0>)
        0    0.000             0.000          profile:0(profiler)
        1    0.000    0.000    0.000    0.000 sample.py:6(main)


$ sample.py --run_with_pdb arg
> /Users/likr/python/test/sample.py(7)main()
-> if len(argv) < 2:
(Pdb) 

このようにapputilsを使えば,モジュールのdocstringをusageとして表示させたり,コマンドライン引数を渡すだけでプロファイラやデバッガの起動ができるようになります.apputilsでハンドルされるフラグはmainには渡ってきません.また,以下のようにフラグを自分で追加することもできます.

#!/usr/bin/env python

'''How to use this module'''
from google.apputils import app
import gflags

class MyFlag(gflags.BooleanFlag):

    def __init__(self):
        gflags.BooleanFlag.__init__(self, 'myflag', 0, 'my flag',
                                    short_name='m', allow_override=1)

    def Parse(self, arg):
        if arg:
            print 'myflag is used'

def main(argv):
    if len(argv) < 2:
        raise app.UsageError('usage error')
    print argv

if __name__ == '__main__':
    gflags.DEFINE_flag(MyFlag())
    app.run()
$ sample.py -?
How to use this module
flags:

./sample.py:
  -m,--[no]myflag: my flag

google.apputils.app:
  -?,--[no]help: show this help
  --[no]helpshort: show usage only for this module
  --[no]helpxml: like --help, but generates XML output
  --[no]run_with_pdb: Set to true for PDB debug mode
    (default: 'false')
  --[no]run_with_profiling: Set to true for profiling the script. Execution will
    be slower, and the output format might change over time.
    (default: 'false')
  --[no]show_build_data: show build data and exit

gflags:
  --flagfile: Insert flag definitions from the given file into the command line.
    (default: '')
  --undefok: comma-separated list of flag names that it is okay to specify on
    the command line even if the program does not define a flag with that name.
    IMPORTANT: flags in this list that have arguments MUST use the --flag=value
    format.
    (default: '')
$ sample.py --myflag aaa
myflag is used
['./sample.py', 'aaa']
$ sample.py -m aaa
myflag is used
['./sample.py', 'aaa']

gflags.BooleanFlag.__init__の第2引数が--xxxのxxxに,shortname引数が-yのyに対応します.
普通のmainの書き方と大きく変わらない書き方で,コマンドライン引数によるプロファイラの起動ができるのは便利そうだと思いました.google-apputilsはPyPIにも登録されているのでeasy_installやpipでもインストールできます.google-apputilsには,今回紹介した以外にもアプリケーションのサブコマンド,デバッグ,プロファイリング,日付などを便利に扱う機能があるようです.他の機能の紹介は機会があれば後日.

Python Hack-a-thonで発表してきました + 発表の補足

Python Hack-a-thon 2011.02で「PyCUDAの紹介」という内容で発表をしました.発表資料はSlideShareにアップしています.

発表時間が短くなったこともあり,説明が雑だった部分も多かったと思うので,喋り足りなかった部分をここで改めて補足します.

発表動機

ここ数年CPU単一コアのクロック数が伸び悩み,「タダ飯が食える時代は終わった」とまで言われる中,GPGPUはマルチスレッド・マルチプロセスプログラミングと同じぐらい重要な技術になっていくかもしれません.しかしながらGPUコンピューティングに関する知識が十分に広まっているとはまだ言えない現状だと思います.マルチコア環境における並行コンピューティングと同様で,GPGPUも「いいプロセッサを使ってるんだから速くなって当たり前」というわけではなく,GPUアーキテクチャに適したアルゴリズムを考えることが重要です.自分自身がここ1年でGPGPUに触れてそういったことを強く感じたので,今回はより多くの人にGPUコンピューティングを広められたらと思い発表の機会をいただきました.

PyCUDAの利点

PyCUDAではとてもPythonらしい高度な抽象化が実現されているので,煩雑なメモリ操作などを簡略化し,全体として簡潔なコードでGPUコンピューティングを行うことができます.そしてPyCUDAのSourceModule用に書いたカーネル関数はそのままC言語の環境に持っていくことが可能です.GPUコンピューティングで性能を出すために重要なのはもちろん,GPU上で実行されるカーネル関数のチューニングです.CPUで行っていたある処理をGPU上で処理させてどの程度高速化できるかは,試してみないとわからないという部分があります.そこで,まずPyCUDAを使ってカーネル関数のチューニングを行うという使い方が考えられます.PyCUDAで十分な性能が出ないのなら,CやFortranで書き換えても十分な性能は得られないかと思います.まずはPyCUDAでGPUコンピューティングを体験してみるのはいかがでしょうか.
また,複数GPUを使った処理や複数マシンを使った処理をするときにもPythonを使えば比較的簡単に書けるかもしれません.

GPUArray

GPUArrayを使ったプログラミングをコメントを入れつつ少し解説します.

>>> import pycuda.autoinit        # デバイスの初期化などをしてくれる
>>> from pycuda import gpuarray
>>> import numpy
>>> a = numpy.random.rand(1000000).astype(numpy.float32) # ホスト側データ
>>> gpu_a = gpuarray.to_gpu(a)    # ホストからデバイスへ転送
>>> n = numpy.random.rand(100, 100).astype(numpy.float32)
>>> gpu_b = gpuarray.to_gpu(b)    # 多次元でもOK
>>> gpu_a *= 2                    # numpyのarrayのように演算するとGPU上で並列実行される
>>> gpu_c = gpu_a + 2             # gpuarrayを直接生成できる
>>> from pycuda import cumath     # GPU上で並列実行される数学ライブラリ
>>> gpu_c = cumath.sin(gpu_c)
>>> c = gpu_c.get()               # デバイスからホストへ転送
>>> print type(c)                 # numpyのarrayとして返ってくる
<type 'numpy.ndarray'>

どうしてもデバイスホスト間のデータ転送はボトルネックになるので,シンプルな処理だとデータが十分に大きくなければnumpyの方が速くなってしまいます.データ転送は最小限に抑えましょう.

Cluster GPU Instance

Cluster GPU Instanceを使えば,高価なマシンを初期コストなしに時間借りできます.なおかつ消費電力やハードウェアのメンテナンスを気にする必要がないのも大きな利点です.Cluster InstanceではHPC(High Performance Computing)向けに高速なネットワークに接続されるようなので,Cluster GPU Instanceを複数台用意してHadoopのMapper,Reducer内でGPU処理をするといった使い方もありそうです(検証はしてない).

MacBookGPU

現行のMacBook,MacBookPro,MacBookAirにはNVIDIAGPUが搭載されているので,CUDAのToolkitをインストールすればCUDA,PyCUDAを使うことができます.十分な性能は出せないかもしれませんが,動作確認レベルでは十分に行えます.発表中に簡単に触れましたがPyCUDAのインストールはpipが一番楽だと思います.

Amazon EC2 Cluster InstanceをAMI化

Cluster InstanceはHPC向けにサポートされた機能がある一方で通常のInstanceにはない制約もあります.Cluster InstanceはVirtualizationがHVMでなければならないようで,ec2-bundle-volを使ったAMI化の方法ではparavirtualとなるため,たとえそれをCluster Instance上で作ったとしても新しいCluster Instanceに戻すことができません.作成して環境構築したInstanceをしっかりstopしておけばその間は課金がされないのですが,新しくInstanceを作る毎に環境構築するのも時間の無駄なのでAMI化を行っておきたいところです.ここではHVMで使えるAMIの作り方を解説していきます.
と意気込んでいたのですが,意外と簡単でした.AWS Management Consoleから下図のようにCreate Image(EBS AMI)を選択します.

出てきたダイアログにイメージの名前と説明を入力して「Create This Image」としてやります.

そうすると2枚目の図のようにAMIが作成されます.イメージ作成処理には数分かかる場合があります.
というわけでCluster GPU InstanceにPyCUDA環境を構築Cluster GPU InstanceにPyOpenCL環境とかも構築で構築した環境をAMI化して公開しました.Community AMIsを「ami-f29f6c9b」か「501488653145/PythonGPU」で検索すれば引っかかると思います.使用時にはInstance TypeでCluster GPUを選択してください.

Cluster GPU InstanceにPyOpenCL環境とかも構築

Cluster GPU InstanceにPyCUDA環境を構築の続きで,PyOpenCLとGPU Computing SDK code samplesもインストールしましょう.まずはpipでPyOpenCLをインストールします.

CPLUS_INCLUDE_PATH=/usr/local/cuda/include PATH=/opt/local/bin:$PATH pip install pyopencl

コマンド一つでした.サンプルプログラムなどを実行して動作確認しましょう.
次にGPU Computing SDK code samplesです.現時点でバージョン3.1のToolkitがプリインストールされているのですが,これより新しい3.2のcode samplesなどを落としてきてもmakeが通らないのでご注意ください.

# wget http://developer.download.nvidia.com/compute/cuda/3_1/sdk/gpucomputingsdk_3.1_linux.run
# sh gpucomputingsdk_3.1_linux.run

ここまで私とまったく同じやり方であればインストール時の質問はそのままEnterで大丈夫です.コンパイルにいくつかのライブラリが必要なのでyumでインストールします.

# yum install libGLU-devel libXi-devel libXmu-devel freeglut-devel

あとはmakeでエラーが起きなければ大丈夫です.

# cd NVIDIA_GPU_COMPUTING_SDK/C
# make

deviceQueryを実行してみましょう.Cluster GPU Instanceに搭載されているTesla M2050の情報が2枚分見えるはずです.

# bin/linux/release/deviceQuery
bin/linux/release/deviceQuery Starting...

 CUDA Device Query (Runtime API) version (CUDART static linking)

There are 2 devices supporting CUDA

Device 0: "Tesla M2050"
  CUDA Driver Version:                           3.20
  CUDA Runtime Version:                          3.10
  CUDA Capability Major revision number:         2
  CUDA Capability Minor revision number:         0
  Total amount of global memory:                 2817982464 bytes
  Number of multiprocessors:                     14
  Number of cores:                               448
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total number of registers available per block: 32768
  Warp size:                                     32
  Maximum number of threads per block:           1024
  Maximum sizes of each dimension of a block:    1024 x 1024 x 64
  Maximum sizes of each dimension of a grid:     65535 x 65535 x 1
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Clock rate:                                    1.15 GHz
  Concurrent copy and execution:                 Yes
  Run time limit on kernels:                     No
  Integrated:                                    No
  Support host page-locked memory mapping:       Yes
  Compute mode:                                  Default (multiple host threads can use this device simultaneously)
  Concurrent kernel execution:                   Yes
  Device has ECC support enabled:                Yes

Device 1: "Tesla M2050"
  CUDA Driver Version:                           3.20
  CUDA Runtime Version:                          3.10
  CUDA Capability Major revision number:         2
  CUDA Capability Minor revision number:         0
  Total amount of global memory:                 2817982464 bytes
  Number of multiprocessors:                     14
  Number of cores:                               448
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total number of registers available per block: 32768
  Warp size:                                     32
  Maximum number of threads per block:           1024
  Maximum sizes of each dimension of a block:    1024 x 1024 x 64
  Maximum sizes of each dimension of a grid:     65535 x 65535 x 1
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Clock rate:                                    1.15 GHz
  Concurrent copy and execution:                 Yes
  Run time limit on kernels:                     No
  Integrated:                                    No
  Support host page-locked memory mapping:       Yes
  Compute mode:                                  Default (multiple host threads can use this device simultaneously)
  Concurrent kernel execution:                   Yes
  Device has ECC support enabled:                Yes

deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 3.20, CUDA Runtime Version = 3.10, NumDevs = 2, Device = Tesla M2050, Device = Tesla M2050


PASSED

Press <Enter> to Quit...
-----------------------------------------------------------

続いてOpenCLです.こちらもmakeして異常がなければoclDeviceQueryを実行してみましょう.

# cd ../OpenCL
# make
# bin/linux/release/oclDeviceQuery

Cluster GPU InstanceにPyCUDA環境を構築

Cluster GPU InstanceはOSがCent OS 5.5なのでPythonのバージョンが2.4です.このままではいろいろと不便なのでまずは最新のPython(2.7.1)をソースコードからインストールしましょう.URLやバージョンは執筆時点でのものですので,適当に読み替えてください.インストール先は/opt/localとします.

# wget http://www.python.org/ftp/python/2.7.1/Python-2.7.1.tar.bz2
# tar jxvf Python-2.7.1.tar.bz2
# cd Python-2.7.1
# ./configure --prefix=/opt/local
# make

makeすると,ライブラリが足りなくていくつかのモジュールがビルドされていないことがわかります.

Python build finished, but the necessary bits to build these modules were not found:
_bsddb             _curses            _curses_panel   
_sqlite3           _ssl               _tkinter        
bsddb185           bz2                dbm             
dl                 gdbm               imageop         
readline           sunaudiodev        zlib            

yumでインストールしていきましょう.

# yum install sqlite-devel readline-devel zlib-devel ncurses-devel tk-devel gdbm-devel openssl-devel bzip2-devel

再度makeします.

Python build finished, but the necessary bits to build these modules were not found:
_bsddb             bsddb185           dl              
imageop            sunaudiodev                        

まだいくつかのモジュールがビルドされていませんが,必要なライブラリがわかりませんでした.ご存知の方は教えていただけると助かります.

# make
# make install

Python本体のビルドは完了なはずです.PATHの設定をしておきます.

# echo 'export PATH=$PATH:/usr/local/cuda/bin:/opt/local/bin' >> /etc/profile
# echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/local/lib64' >> /etc/profile
# source /etc/profile

Pythonの動作確認.pythonだけだとデフォルトのpython2.4を見に行ってしまうので,python2.7と打ちます.

# python2.7
Python 2.7.1 (r271:86832, Feb 12 2011, 23:42:46) 
[GCC 4.1.2 20080704 (Red Hat 4.1.2-48)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 

pipを使えばPyCUDAを簡単にインストールできるので,まずpipをインストールしましょう.

# cd
# wget http://pypi.python.org/packages/source/s/setuptools/setuptools-0.6c11.tar.gz#md5=7df2a529a074f613b509fb44feefe74e
# tar xvzf setuptools-0.6c11
# cd setuptools-0.6c11
# python2.7 setup.py build
# python2.7 setup.py install
# cd
# wget http://pypi.python.org/packages/source/p/pip/pip-0.8.2.tar.gz#md5=df1eca0abe7643d92b5222240bed15f6
# tar xvzf pip-0.8.2.tar.gz
# cd pip-0.8.2
# python2.7 setup.py build
# python2.7 setup.py install

CentOSgccのバージョンも古く,PyCUDAのビルド時にエラーが発生します.gccソースコードからビルドしましょう.

# cd
# yum install gmp-devel glibc-devel
# wget ftp://ftp.dti.ad.jp/pub/lang/gcc/releases/gcc-4.3.5/gcc-4.3.5.tar.bz2
# tar jxvf gcc-4.3.5.tar.bz2
# cd gcc-4.3.5
# ./configure --prefix=/opt/local --enable-languages=c,c++
# make
# make install

いよいよPyCUDAのインストールです.pip実行時には,/opt/local/binにインストールしたgccを使ってもらうようにPATHを変更しています.

# pip install numpy
# yum install gcc-c++
# PATH=/opt/local/bin:$PATH pip install pycuda

これでインストールは完了です.pycuda.autoinit,pycuda.driverをimportして,エラーが発生しなければ問題ないでしょう.

# python2.7
Python 2.7.1 (r271:86832, Feb 13 2011, 02:53:05) 
[GCC 4.1.2 20080704 (Red Hat 4.1.2-48)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pycuda.autoinit
>>> import pycuda.driver
>>> 

回文の判定(シーケンスをコピーして反転する)

文字列をスマートに反転するにはスライスを使うと良いと思う.リストにはreverseメソッドがあるので,インプレースでいいならそっちを使う.リストだが,元のインスタンスとは別に反転したものが必要ならlist(reversed(seq))などとするよりはスライスでseq[::-1]としてやる.検証した環境ではスライスを用いる方が若干パフォーマンスが良かった.
応用例として,回文の判定.

def is_palindrome(s):
    return s == s[::-1]