Raspberry PiでGPGPU

Raspberry PiでGPGPUを行う為の**[PyVideoCore](https://github.com/nineties/py-videocore)**というPythonライブラリを作りましたので紹介します。

# Raspberry PiのGPU

Raspberry PiシリーズはBroadcomの**VideoCore IV**というモバイル向けGPUを搭載しています。このGPUは公式に[リファレンスガイド](https://docs.broadcom.com/docs/12358545)が公開されています。これは2014年2月に[Raspberry Pi財団への誕生日プレゼントとしてBroadcomが公開した](https://www.raspberrypi.org/blog/a-birthday-present-from-broadcom/)ものです。この文書のおかげで、VideoCoreをハックする事が可能になりました。

VideoCore IVは12個のQuad Processing Unit (**QPU**)を搭載しています。各QPUは16 wayのSIMDプロセッサであり、1命令で4ワード×4サイクル(つまり長さ16)のベクトル計算を行います。各QPUは加算系と積算系の2つの演算を同時に実行する事が出来ます。つまり、GPU全体で最大12×4×2=96の演算を同時に実行する事が出来ます。クロックは250MHzなので、理論性能は96×0.25 = 24GFLOPSとなります。単精度のみです。Raspberry Pi2では500MHzくらいまでオーバークロックする事が出来るようです。

他にALUとは独立したSpecial Function Unit(SFU)が3つあってRECIP, RECIPSQRT, EXP2, LOG2が計算できます。ただ4命令分(16サイクル)使い、パイプライン化は出来ず、精度が悪い(ちゃんと実験してませんがLOG2以外は4桁くらいLOG2は2桁くらいのよう)のでSFUの計算能力としての寄与は微小です。各QPUは最大2つのハードウェアスレッドを実行できます。つまり全体で最大24スレッドを同時に走らせることができます。スレッドのQPUへのアサインはVideoCoreのスケジューラによって動的に行われます。スレッドを同期させる為のmutexが1つとsemaphoreが16個あります。あとはメモリが使い方に応じて数種類ありますが、長くなるのでまたそのうち説明をします。

大体こんな感じのものです。モバイル用ですから性能は高くないですが、完全なドキュメントが手に入り、数千円で買えて、あとNVIDIA以外でのGPGPUというのも珍しいですから遊んでみると面白いんじゃないかと思います。もちろん、Raspberry PiやVideoCore IVを搭載したスマホ等で何らかのプロジェクトを行うならばこの計算能力は有り難いと思います。

# PyVideoCore

残念ながらVideoCoreIVにはCUDAやOpenCLのようなGPGPU開発環境は(多分)ありませんので、QPU用のアセンブリ言語で開発を行う必要があります。というかそもそも言語もアセンブラも存在しません。過去には以下のようなプロジェクトが行われていますが、それぞれ自前でアセンブラを開発してやっているようです 。

- [FFTの実装](https://github.com/raspberrypi/userland/tree/master/host_applications/linux/apps/hello_pi/hello_fft)
- [SHA256の実装](https://github.com/elorimer/rpi-playground)
- [Deep Belief Image recognition SDKの移植](https://github.com/jetpacapp/DeepBeliefSDK)(行列乗算[(GEMM)](https://github.com/jetpacapp/pi-gemm)をGPUで高速化)

**PyVideoCore**ではもうちょっと手軽に書けるようにしようとPythonの内部DSLとしてアセンブリ言語を実装してみました。以下は長さ16のfloatのベクトルをただ足すだけのサンプルですが、このようにホスト側のコードとGPU側のコードを一つのファイルに書いて、コンパイル無しで普通のPythonスクリプトとして実行できます。


```py
import numpy as np

from videocore.assembler import qpu
from videocore.driver import Driver

@qpu
def hello_world(asm):
    # Load two vectors of length 16 from the host memory (address=uniforms[0]) to VPM
    setup_dma_load(nrows=2)
    start_dma_load(uniform)
    wait_dma_load()

    # Setup VPM read/write operaitons
    setup_vpm_read(nrows=2)
    setup_vpm_write()

    # Compute a + b
    mov(r0, vpm)
    mov(r1, vpm)
    fadd(vpm, r0, r1)

    # Store the result vector from VPM to the host memory (address=uniforms[1])
    setup_dma_store(nrows=1)
    start_dma_store(uniform)
    wait_dma_store()

    # Finish the thread
    exit()

with Driver() as drv:
    # Input vectors
    a = np.random.random(16).astype('float32')
    b = np.random.random(16).astype('float32')

    # Copy vectors to shared memory for DMA transfer
    inp = drv.copy(np.r_[a, b])
    out = drv.alloc(16, 'float32')

    # Run the program
    drv.execute(
            n_threads=1,
            program=drv.program(hello_world),
            uniforms=[inp.address, out.address]
            )

    print ' a '.center(80, '=')
    print(a)
    print ' b '.center(80, '=')
    print(b)
    print ' a+b '.center(80, '=')
    print(out)
    print ' error '.center(80, '=')
    print(np.abs(a+b-out))

```

`@qpu`というデコレータがついているのがアセンブリのコードです。現状は生のアセンブリを書く必要がありますが、GPUのコード自体はふつうの関数ですし、個々の命令も普通の関数なのでPythonの機能を使ってよく使うパターンをライブラリ化する等の工夫はできると思います。

以下がリポジトリです。試してみてください。

- https://github.com/nineties/py-videocore

これからベンチマーク取ったりソフトウェアを組んだりいろいろしてみようと思っているので、そしたらまた何か書こうと思います。