D言語で始めるOpenCL(5) AWS FPGA編


はじめに

これまでの記事で、GPUをOpenCLで動かして速度を出す方法を探ってきました。
しかしながら、結局NVIDIAのGPUを使っている限り、諸々の制約でCUDA(やPTXなど)を使用するのが最速となってしまいそうです。

今回は、CUDAではなくOpenCLにしかできないこと(多分)に挑戦します。すなわち、FPGAでのOpenCLカーネルの実行です。

今回のソースコード

いままでのdcltkはサブモジュールとして参照しています。

FPGAとは

FPGAとは、詳細はリンク先参照ですが、つまり動作を動的に書き換えられる大規模な電子回路です。

CPUもGPUも、基本的に電子回路の機能が書き換わるようなことはありません。固定された機能の上でソフトウェアが走って動作します。
しかしFPGAでは、ルックアップテーブル(LUT)などの汎用的な電子回路セルを設定(コンフィギュレーション)することで、まったく異なる機能に変化させることができます。

たとえば、CPUやGPUの演算器は固定された数しか用意されていませんが、FPGAでは入力データに合わせて必要なだけ演算器を増やすことができます。
(もちろん上限はあり、リアルタイムに増やすのも難しいですが……)

CPUやGPUは、どうしてもソフトウェアで動くので、乗算の命令を読み取って乗算・次に加算の命令を読み取って加算といった、命令を読む・実行するという手順で処理を行うことになります。
しかしFPGAでは、乗算と加算の処理が必要な場合、乗算して加算する回路を作り込み、渡されたデータをそこに流すという手順で処理を行うことになります。

FPGAの利点

上記の特徴から、大規模なデータ処理をCPUの代替として実行させる場合、FPGAには下記の利点があるようです。

  • 処理に応じた専用回路を作れるので、電力効率が良い。
  • 演算器を好きなだけ増やすことでCPUやGPU以上の並列計算を行える。
  • if文のような分岐を多く含む処理にも比較的強い。
    • パイプライン(のストール)とかキャッシュ(のミス)が無いので……。それらを作れば弱くなります。
  • CPUを介さずに、RAMやIOからのデータを直接処理できる。
    • CPUを介した場合のレイテンシがない。
    • ソフトウェアを呼び出さずに、FPGA自体で処理を行なって結果を返せる。

FPGAの欠点

そんな面白いFPGAですが、以下のような悲しい欠点があります。

  • 動作周波数が低い。
    • せいぜい100MHz〜250MHzくらい? 回路による。
    • 得意の並列計算や低遅延で頑張る必要がある。
  • 高い。
    • PCに繋げて計算に使えるFPGAは気軽に買えない。ゲームに使えるわけでもないし……。
  • RAMとの通信はCPUやGPUより遅い。
    • システムのRAMとはPCIeを介してアクセスする。
    • FPGAにRAMが乗っている場合もあるが、帯域幅がCPU・GPUほど無い?
  • 電子回路を作るのに、基本的にハードウェア定義言語(HDL)を使う必要がある。
    • C言語などからHDLを生成する高位合成という技術もあるが、あまり一般的ではない。
  • HDLから電子回路を作る論理合成に時間が掛かる。
    • 大規模になるとメモリ32GB・Xeon 4GHzマシンで一晩以上……。(経験者談)

AWS FPGAについて

そういった癖の強いFPGAが、2016年からAWSで使えるようになり、注目を集めました。

AWSのFPGAは、下記の問題を解決しています。

  • 高い。
    • AWSであれば、時間単位の課金(2018年現在 1.65USD/時間〜)で必要量だけFPGAを使用できる。
    • 高価(100万以上……)なボードを購入する必要がない。
    • 使えるボードはXilinx UltraScale Plus FPGAで、250万ロジックセル・約6800個のDSPスライス・64 GiB DDR4 ローカルメモリを備える。
    • しかも最大8枚まで使える!
  • 電子回路を作るのに、基本的にハードウェア定義言語(HDL)を使う必要がある。
    • XilinxのSDAccelという高位合成技術が使えて、OpenCLで書いたカーネルを電子回路にできる!

今まで敷居が高かったFPGAとその高位合成技術が、一気に使いやすくなりました。(それでも色々大変だけど……)

今回せっかくOpenCLを覚えたので、FPGAで果たして使えるのか、一体どの程度の速度が出せるのか、試してみようと思います。

しかもホスト言語はD言語で!

AWSにFPGAインスタンスを立てる

書いてみたらD言語と全然関係なかったので別記事にまとめました。

AWS FPGAで行列積を計算してみた

以下はD言語特有の問題になります。

SDAccelをD言語から使う

今回使用するOpenCLは、FPGA高位合成向けの特別なものになります。
それで、色々配慮が必要かと思ったのですが、clEnqueueCopyBufferなどが使えないだけでほとんどGPUと同じコードで動いてしまいました。
さすがOpenCL……。

唯一、SDAccelの独自拡張部分だけ少し定義等を追加する必要があったので記載しておきます。

OpenCL SDAccelの独自拡張でメモリのバンクを割り当てる

カーネルが読み書きするOpenCLバッファーをグローバルメモリーの別々のバンクに割り当てることで、グローバルメモリーの帯域幅を広く使えるようになります。

バンクの指定には、SDAccelの独自拡張の機能をホストプログラムで使用する必要があります。
D言語にはSDAccelの独自拡張の定義が無いので、適当にヘッダファイルから拾ってきて専用の関数を作成しました。

source/app.d
// バンク指定用の定数
enum DdrBank {
    bank0 = 0b0001,
    bank1 = 0b0010,
    bank2 = 0b0100,
    bank3 = 0b1000,
}

/// ditto
enum CL_MEM_EXT_PTR_XILINX = cast(cl_mem_flags)(1 << 31);

/// バンク指定用の構造体。clCreateBufferでデータへのポインタの代わりにこの構造体のポインタを渡す。
struct cl_mem_ext_ptr_t {
  uint flags;
  const(void)* obj;
  void* param;
}

/// バンク指定でバッファを作成する。
private cl_mem createDeviceBuffer(cl_context context, size_t size, const(void)* data, cl_mem_flags flags, DdrBank bank) {
    cl_int errorCode;
    cl_mem_ext_ptr_t mem = {bank, data};
    auto buffer = clCreateBuffer(
            context,
            flags | CL_MEM_EXT_PTR_XILINX,
            size,
            &mem,
            &errorCode);
    cl.enforceCl(errorCode);
    return buffer;
}

private cl_mem createDeviceReadBuffer(cl_context context, const(void)[] data, DdrBank bank) {
    return createDeviceBuffer(
        context, data.length, data.ptr, CL_MEM_READ_ONLY | CL_MEM_HOST_WRITE_ONLY | CL_MEM_COPY_HOST_PTR, bank);
}

private cl_mem createDeviceWriteBuffer(cl_context context, size_t size, DdrBank bank) {
    return createDeviceBuffer(
        context, size, null, CL_MEM_WRITE_ONLY | CL_MEM_HOST_READ_ONLY, bank);
}

これらを使うとバンク指定でバッファを作れます。

source/app.d
    // バンク0で作成
    auto lhsBuffer = createDeviceReadBuffer(context, lhs, DdrBank.bank0);
    scope(exit) cl.releaseBuffer(lhsBuffer);

    // バンク1で作成
    auto rhsT = transpose(rhs, bufferCols, bufferResultCols);
    auto rhsBuffer = createDeviceReadBuffer(context, rhsT, DdrBank.bank1);
    scope(exit) cl.releaseBuffer(rhsBuffer);

    /// バンク2で作成
    auto resultBuffer = createDeviceWriteBuffer(context, resultSize * float.sizeof, DdrBank.bank2);
    scope(exit) cl.releaseBuffer(resultBuffer);

さらにxoccでのコンパイルオプションの追加が必要です。

xocc -c \
  -k product \
  --target hw \
  --platform ${AWS_PLATFORM} \
  --max_memory_ports product \ #productカーネルにメモリーポートを最大数割り振る。
  --save-temps \
  -o product.xo \
  product.cl

xocc -l \
  --target hw \
  --platform ${AWS_PLATFORM} \
  --max_memory_ports product \ #productカーネルにメモリーポートを最大数割り振る。
  --sp product_1.m_axi_gmem0:bank0 \ #各メモリーポートを何番のバンクに割り当てるか
  --sp product_1.m_axi_gmem1:bank1 \ #m_axi_gmem0〜2はカーネル引数の順序になっている。
  --sp product_1.m_axi_gmem2:bank2 \
  --nk product:1 \
  --save-temps \
  --profile_kernel data:all:all:all:all \
  --report_level estimate --report_dir ./report \
  -o product.xclbin \
  product.xo

結果

先ほどのソースの通りに頑張って最適化したものを動かしたのですが、まだ60GFLOPS程度しか出せていません……。ビルド時間の制約などでFPGAの能力を出し切っていないように思います。

本当にGPUと対抗するつもりであれば、やはりHDLでカーネルを書く必要があるのかもしれません。

なにはともあれ、FPGAとそのためのSDAccelというかなり特殊な環境であっても、D言語で困ることはなく、むしろプログラミングしやすくて便利ということがわかりました。

FPGAがひたすら扱いづらかった……。やはり本物のハードウェアを扱う技術は難しいですね。

ライセンス


この 作品 は クリエイティブ・コモンズ 表示 4.0 国際 ライセンスの下に提供されています。