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

# はじめに

以前からFPGAに興味があり、ボード(Altera MAX10など)も持っていたのですが、なかなか手がつけられない状態でした。
別の記事でGPUをOpenCLで触る機会があって、AWSのFPGAではOpenCLのコードを高位合成で動かせると聞いて、今回チャレンジしてみました。

## これまでの道のり

以下の記事で色々ためしてみた行列積の計算を、AWSのFPGAでもなるべく高速にやってみようと思いました。

* [D言語で始めるOpenCL(1) 導入編](https://qiita.com/outlandkarasu@github/items/f02ec0e8dfaa6fca7183)
* [D言語で始めるOpenCL(2) ローカルメモリー編](https://qiita.com/outlandkarasu@github/items/00b49cbe55b888702356)
* [D言語で始めるOpenCL(3) 下準備＆行列パディング編](https://qiita.com/outlandkarasu@github/items/0c207f61743433fc92d2)
* [D言語で始めるOpenCL(4) プライベートメモリー編](https://qiita.com/outlandkarasu@github/items/af4c130c25c74434012a)

なお、ホスト言語はD言語です。D言語はC言語のライブラリとのインターフェイスが充実しており、今回のSDAccel用のOpenCLライブラリも特別な考慮なしに使用できました。
独自拡張部分についても、関数宣言や構造体定義の追加のみで問題ありませんでした。
今回のソースコードのD言語側については、こちらの記事を参照してみてください。

[D言語で始めるOpenCL(5) AWS FPGA編](https://qiita.com/outlandkarasu@github/items/1fdc766249c235fbc0dd)

# 今回のソースコード

https://github.com/outlandkarasu-sandbox/dlang-aws-fpga

先述の通りD言語ですが、カーネルはOpenCL Cです。D言語はC言語と近いので特に読むのに支障はないと思います。多分……。

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

まずはAWSにf1インスタンスを立てます。
なお、リージョンはバージニア北部(`us-east-1`)とします。使えるリージョンが限られているので注意してください。

## インスタンス上限変更申請

FPGAが使えるf1インスタンスは最初の状態では上限が0になっていて使えません。サポートセンターで上限緩和を申請する必要があります。

私の場合、サポートセンターへの「ケースの作成」で

* サービス制限の緩和
* EC2インスタンス
* バージニア北部
* f1.2xlarge
* 新しい上限値 1

を選択し、日本語で「FPGA開発のため」などと書いて送信したところ、1営業日ほどで上限が緩和されました。

## インスタンス作成

次にFPGAインスタンス作成です。

実は**ソフトウェアエミュレーション・ハードウェアエミュレーションの間はFPGAインスタンスはまだ不要です。**なので、上限解除申請中にも行えます。

必要な手順は下記の通りです。

FPGAを使用するリージョン(バージニア北部)などで、

* AMI MarketplaceでAWSのFPGA Developer AMIを選択
    * 「FPGA」で検索すると出てきます。
* インスタンスタイプを選択
    * 最初はz1d.2xlargeという巨大インスタンスが選択されていますが、行列積をやる程度であれば、t3.xlargeなどで十分です。
    * というか、z1d.2xlargeでもそんなに速くならない……。
* `/dev/sdb`のボリュームサイズを30GB程度に増やす。
    * プロジェクトファイルが置かれる場所で、デフォルトの5GBだとすぐいっぱいになる。

あとは普通にSSHが行える設定をすれば大丈夫です。`/dev/sdb`のボリュームサイズの部分が割と地雷です。

## セットアップ

インスタンスが作成できたら、FPGAを使うためのツール群をインストールします。

今回はホスト側をD言語で書いてしまったので、D言語コンパイラのインストールが必要です。

```console
# 人生最良の行動
$ curl -fsS https://dlang.org/install.sh | bash -s dmd
```

次に、[aws-fpga](https://github.com/aws/aws-fpga)というAWSのFPGA開発キットをビルドします。
こちらはGithubで配布されているので、`git clone`します。

```console
$ cd /home/centos/src/project_data
$ git clone https://github.com/aws/aws-fpga.git
Cloning into 'aws-fpga'...
remote: Enumerating objects: 21, done.
remote: Counting objects: 100% (21/21), done.
remote: Compressing objects: 100% (18/18), done.
remote: Total 7272 (delta 3), reused 10 (delta 2), pack-reused 7251
Receiving objects: 100% (7272/7272), 299.16 MiB | 48.06 MiB/s, done.
Resolving deltas: 100% (3812/3812), done.
Checking out files: 100% (1830/1830), done.
$ cd aws-fpga/
$ source sdaccel_setup.sh
```

最後の`source sdaccel_setup.sh`は起動ごとに必要になるので、`~/.bashrc`などに書いておきます。
ついでに、ビルド時になぜかこれをしないとエラーになる`LC_ALL=C`の設定や、D言語の有効化も追加します。

```console
$ echo source /home/centos/src/project_data/aws-fpga/sdaccel_setup.sh >> ~/.bashrc
$ echo export LC_ALL=C >> ~/.bashrc
$ echo 'source ~/dlang/$(~/dlang/install.sh list)/activate' >> ~/.bashrc
```

以上が済んだら、一度ログアウトしてから再度ログインし、色々ログイン時に設定されることを確認します。

もしこの解説のソースコードを動かす場合、Githubからの`git clone`を行なってください。`submodule`の更新も必要です。

```console
$ cd /home/centos/src/project_data
$ git clone https://github.com/outlandkarasu-sandbox/dlang-aws-fpga
$ cd dlang-aws-fpga
$ git submodule update -i
```

ここまでで環境のセットアップは完了です。次にようやくOpenCLでFPGAを動かしていきます。

# OpenCLでFPGAを動かす

SDAccelを使用してOpenCLでFPGAを動かす場合、以下の手順が必要です。

詳しくは、[AWS FPGAのGithubリポジトリ](https://github.com/aws/aws-fpga/tree/master/SDAccel)にまとまっています。

1. OpenCL CでFPGA用カーネルのソースコードを作成する。
1. OpenCL Cのソースコードを`xocc`コマンドでビルドする。
1. 必要に応じてソフトウェアエミュレーション・ハードウェアエミュレーションでテストを行う。
1. AWS FPGA向けイメージ(AFI)を作成する。
    * `xclbin`ファイルから`awsxclbin`ファイルが作成される。
1. AFI登録完了まで待つ。
1. FPGA搭載インスタンスで、作成された`awsxclbin`ファイルをカーネルとして呼び出す。
    * `createProgramWithBinary`で`awsxclbin`を指定してプログラムを作成し、カーネルを呼び出す。

## OpenCL CでFPGA用カーネルのソースコードを作成する

詳しくは[SDAccel 環境プログラマ ガイド](https://www.xilinx.com/support/documentation/sw_manuals_j/xilinx2017_4/ug1277-sdaccel-programmers-guide.pdf
)などを参照すると良さそうです。

また、ワークグループ数の指定や各種最適化用の属性指定などに色々とGPUと異なる部分があります。(後述)

## OpenCL Cのソースコードを`xocc`コマンドでビルドする。

[Makefileを書きました。](https://github.com/outlandkarasu-sandbox/dlang-aws-fpga/blob/master/kernel/Makefile)それほど難しいオプションは無いと思いますが、メモリのバンク指定などがあまりドキュメントに載っていませんでした。

## エミュレーションでテストを行う。

ソフトウェアエミュレーション・ハードウェアエミュレーションは、`xclbin`ファイルをホストプログラムで`clCreateProgramWithBinary`で読み込んで実行することにより行えます。
ソフトウェアとハードウェアのどちらのエミュレーションになるかは、`xclbin`ビルド時の`TARGET`パラメーターに拠ります。

実行速度は ソフトウェアエミュレーション >>> ハードウェアエミュレーション なので、まずは`TARGET=sw_emu` でテストすることをお勧めします。

ハードウェアエミュレーションでは、カーネルのレイテンシや使用ロジックセル数などの細かいレポートが出るようです。ただし、**処理するデータ量を1KB以上にするとものすごく時間がかかりました。**

頻繁にビルドとテストを繰り返す場合、`float`であれば10〜100個程度のデータでのみ確認するとよいようです。

## AWS FPGA向けイメージ(AFI)の登録

この作業はAWS FPGA特有のものになります。（多分）
AWSのFPGAはAFIというものに本来のFPGA用ビットストリームを格納し、OpenCLでの実行時にそれをコンフィギュレーションして動くようです。
そのためのホストプログラム用の情報が`awsxclbin`であるようです。

なお、AFI登録コマンドを実行してから実際に登録されるまで30分程度掛かります。
`aws ec2 describe-fpga-images`で状態を確認でき、そちらが`available`になってから初めて使用可能になります。

## `awsxclbin`ファイルをカーネルとして呼び出す

先述の通り`awsxclbin`を`clCreateProgramWithBinary`で読み込んで実行することになります。

今までのOpenCLカーネルと同様に呼び出せますが、OpenCLのバージョンが`Embedded`のため、いくつかの関数(`clEnqueueCopyBuffer`など?)が使用できないのに注意が必要です。

また、実行時にはハードウェアアクセスする関係で`sudo`が必要になります。下記のような感じです。

```console
$ sudo sh
# source /opt/xilinx/xrt/setup.sh
# ./product #実行
```

# SDAccelでの最適化

実際に動かしたカーネルは[こちら。](https://github.com/outlandkarasu-sandbox/dlang-aws-fpga/blob/master/kernel/product.cl)

試行錯誤の末に、こんな形になりました……。
それぞれどんな最適化を考慮したか記載します。

## ワークグループ設定

`reqd_work_group_size`属性でワークグループサイズを指定するのが大事なようです。
**`reqd_work_group_size`属性をつけると、そのワークグループサイズでなければ実行できなくなります。**(異なるサイズを指定するとエラー)
この値を元に、後述のパイプラインの展開などを行なっているようです。

## パイプライン

FPGAの並列化は主にループのパイプライン化で行われるようです。
パイプライン化を行う指定は`xcl_pipeline_workitems`属性などで行えます。
`xcl_pipeline_workitems`属性では、ワークアイテム別にパイプラインの異なるステージを順次実行するようです。
パイプライン化が完全に行えた場合、`xcl_pipeline_workitems`を指定した内側は平均1クロックで処理できます。

大きなローカル変数などをループ間で共有するようなコーディングをしていると、パイプライン展開で整合性が崩れたりするのか、計算結果が謝ってしまう場合がありました。
OpenCL Cとはいえ、パイプライン化がされることを意識して、ループ間の依存や変数の共有が無くなるよう試行錯誤する必要がありそうです。

## 配列のパーティショニング

ローカルの配列変数は、FPGAのBlockRAMが割り当てられるようです。
このとき、BlockRAMに接続するためのポートが用意されますが、デフォルトでは1つの変数に1ポートしか用意されず、並列アクセスが行えなくなるようです。

`xcl_array_partition`属性を指定することでバンク分割などを行い、パイプラインステージなどからの同時アクセスが可能になります。

## コアを増やす

これはOpenCL Cのコードの外側の設定になりますが、`xocc`コマンドの`--nk`オプションで論理合成するカーネルの数を指定できます。

カーネルの数が増えると、同時実行できるワークグループの数が増え、高速化できるようです。
ワークグループの設定をちゃんとやっていれば恩恵が受けられます。

ただし、今はまだ最大で16までしか指定できないようです。

## メモリのバンクを割り当てる

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

```console
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 \ #カーネルインスタンス数を増やす場合、product_2について同様の指定を追加する。
  --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でカーネルを書く必要があるのかもしれません。
今後は、手元のボードでHDLをちゃんと勉強していきたいです。

# ライセンス

<a rel="license" href="http://creativecommons.org/licenses/by/4.0/"><img alt="クリエイティブ・コモンズ・ライセンス" style="border-width:0" src="https://i.creativecommons.org/l/by/4.0/88x31.png" /></a><br />この 作品 は <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">クリエイティブ・コモンズ 表示 4.0 国際 ライセンス</a>の下に提供されています。