執筆者 : 小田 逸郎
1. はじめに
本記事は、Linuxカーネルならぬ、FPGAカーネルに関するもの(予定)です。 筆者は、一応、Linuxカーネル技術者の端くれですが、カーネルつながり(?)とは言え、 FPGAカーネルの方は、全くの素人ですので、「超」入門としましたが、果たして、お役に立てる 記事になるかどうか。
対象とするFPGAは、サーバにPCI-eで接続して使用するもので、アクセラレータとも呼ばれるものです。 アクセラレータとしては、GPUが最も一般的かと思いますが、FPGAも最近何かと話題になっています。
筆者は、最近、Xilinx社のAlveo U200 というアクセラレータカードを使用する機会に恵まれました。 そのため、本記事では、この機種での使用例を元に説明しています。
今回の記事では、ともかくFPGAカーネルを動作させるところまでチャレンジします。
補足
同じカーネルで紛らわしいですが、本記事では、FPGAカーネルが支配的なので、単にカーネルと
言った場合は、FPGAカーネルのことを指します。Linuxカーネルについては、その都度、何か分かる
ように注記します。
2. 動作環境
ハードウェア(サーバ)については、Xilinx社が Alveo U200 のプラットフォームとして公式にサポート しているものであれば、問題ないはずです。普通のx64マシンを使用しています。
OSは、Ubuntu 18.04 を使用しています。
以下にインストールが必要なソフトウェアについて、示します。取得したURLも合わせて示します。
Vitis統合開発環境 2020.1
カーネルを開発するための開発環境です。
- Xilinx_Unified_2020.1_0602_1208_Lin64.bin (インストーラ)
- https://japan.xilinx.com/support/download/index.html/content/xilinx/ja/downloadNav/vitis.html
Xilinxのユーザ登録(無料)が必要です。
補足
最新は、2020.2 ですが、使用を始めた時点では、2020.1 が最新だったので、2020.1で
説明します。2020.1 では、Ubuntu 20.04 がサポートされていませんでしたが、2020.2 では
サポートされているようです。将来、何かあれば、2020.2 への更新記事を書くかもしれません。
以下のパッケージは、2020.1に整合するものとなります。
XRTパッケージ
ホスト側プログラムのためのライブラリやアクセラレータカードのドライバが含まれます。
- xrt_202010.2.6.655_18.04-amd64-xrt.deb
- https://japan.xilinx.com/products/boards-and-kits/alveo/u200.html#gettingStarted
適切なタブを選択していきます。
U200 プラットフォーム(運用用)
アクセラレータカードを動作させるために必要なものです。(機種依存)
- xilinx-u200-xdma-201830.2-2580015_18.04.deb
URLは、XRTパッケージと同じです。
補足
カーネルの実行に必要です。カーネルの開発を行うだけ、すなわち、
カーネルを実機で動かさない(アクセラレータが装備されていない)場合は、不要です。
U200 プラットフォーム(開発用)
カーネル開発時に必要な機種依存の情報が含まれます。
- xilinx-u200-xdma-201830.2-dev-2580015_18.04.deb
- https://www.xilinx.com/products/boards-and-kits/alveo/u200.html#gettingStarted
Xilinxのユーザ登録(無料)が必要です。
URLは、XRTパッケージと同じはずですが、なぜか、日本語のサイトではうまく取れなかったので、 英語のサイトから取得しました。
補足
カーネルの開発を行うのに必要です。既存のカーネルを実行するだけであれば、不要です。
インストール
インストールについて、以下に簡単に説明します。
Vitis
インストールには、GUI環境が必要です。インストーラを実行するとGUIが開くので、それに従って、 インストールすればよいです。
$ ./Xilinx_Unified_2020.1_0602_1208_Lin64.bin ...
デフォルトの指定で十分です。ディスク容量が結構必要で、150GBくらい用意しておいた方がよいです。 デフォルトでは、/tools/Xilinx ディレクトリ配下にインストールされます。/tools ディレクトリは あらかじめ存在している必要があります。
注意
公式には、インストーラが、Ubuntu 18.04.4 までしか対応していないようです。18.04.5 以降では、
インストーラのバグのため、インストーラがハングしてしまいます。回避するには、/etc/os-release
ファイル中の「18.04.X」の文字列を「18.04.4」に変更すればよいです。インストールが終わったら、
戻しておきましょう。筆者は、18.04.5 で問題なく使用しています。保証はしませんが、ご参考まで。
XRT
$ sudo apt install ./xrt_202010.2.6.655_18.04-amd64-xrt.deb
ドライバ(カーネルモジュール)のビルドも行われます。
U200 プラットフォーム(運用用)
$ sudo apt install ./xilinx-u200-xdma-201830.2-2580015_18.04.deb ...
最後に、以下のコマンドを実行せよ、とメッセージが出るので、コピペして実行します。
sudo /opt/xilinx/xrt/bin/xbutil flash -a xilinx_u200_xdma_201830_2 -t 1561465320
$ sudo /opt/xilinx/xrt/bin/xbutil flash -a xilinx_u200_xdma_201830_2 -t 1561465320 ...
最後にコールドリブートせよ、とメッセージが出るのでコールドリブートします。
U200 プラットフォーム(開発用)
$ sudo apt install ./xilinx-u200-xdma-201830.2-dev-2580015_18.04.deb
3. カーネルの作成
カーネルのソースコードの記述方法、ビルド方法は、いくつかあるのですが、ここでは 最も簡単なC言語で記述する方法を選択しています。
参照
Vitis統合ソフトウェアプラットフォームの資料
(以下、Vitisマニュアル)を参照しています。
以下に今回作成するカーネルのソースコードを示します。(本章で参照するソースコードは、 ここにもあります。)
void vadd(const unsigned int *in1, const unsigned int *in2, unsigned int *out, int size) { #pragma HLS INTERFACE m_axi port=in1 offset=slave bundle=gmem #pragma HLS INTERFACE m_axi port=in2 offset=slave bundle=gmem #pragma HLS INTERFACE m_axi port=out offset=slave bundle=gmem int i; for (i = 0; i < size; i++) { out[i] = in1[i] + in2[i]; } }
vadd関数の処理内容については、説明するまでもないですね。
補足
今回は、ともかくカーネルを作成して動かしてみるのが目的ですので、カーネルの内容
については、不問としてください。
少なくとも、ベクトルを一気に加算できるようになっていないと、アクセラレータで
実行する意味がなさそうですが、こんなコードでそうなるのか疑問ですよね。
アクセラレータでこのvadd関数が実行されるというところまでは想像が付きますが、引数の 取り扱いが気になるところです。それは、おいおい明らかになっていく予定ですが、 ここで説明するポイントは、ポインタ引数(in1、in2、out)に関してです。
アクセラレータからは、ホストのメモリを直接アクセスすることができません。ホストの メモリを読みたい場合は、一旦、アクセラレータのメモリに転送する必要があります。 また、アクセラレータで計算した結果は、アクセラレータのメモリに格納し、それを ホストのメモリに転送することにより、ホストからアクセスできるようになります。
ソースコード中のpragma文は、アクセラレータのメモリを使用すること、ホストとアクセラ
レータ間で転送が必要なことを示します。ポインタ引数については、このおまじないが
必要です。port=
に引数名を指定し、bundle=
に適当な文字列(後で参照します)を
指定すれば、後はこのままでOKです。
補足
アクセラレータからホストのメモリを直接アクセス(DMA)できる機種もあります。少なくとも
現時点でのU200では、できません。
DMAがサポートされた暁には、また記事を書くかもしれません。
カーネルのビルドは、以下のようにします。2段階で行うことになります。
まずは、ビルドに先立ち、ビルドツールを使用するのに必要な環境変数を設定します。
$ source tools/Xilinx/Vitis/2020.1/settings64.sh
カーネル単独のバイナリファイル(.xo)を作成します。
$ v++ -t hw --platform xilinx_u200_xdma_201830_2 -c -k vadd -o vadd.xo vadd.c
-k
でカーネル名を指定しています。
アクセラレータにロードするためのバイナリファイル(xclbin)を作成します。
$ v++ -t hw --platform xilinx_u200_xdma_201830_2 --config design.cfg -l -o vadd.xclbin vadd.xo
補足
xclbinには、複数のカーネルを含めることができます。今回は、ひとつだけです。
指定したコンフィグファイル design.cfg の内容を以下に示します。
[connectivity] nk=vadd:3 sp=vadd_1.m_axi_gmem:DDR[1] sp=vadd_2.m_axi_gmem:DDR[1] sp=vadd_3.m_axi_gmem:DDR[1]
nk=
は、vaddカーネルのインスタンスを3つ作成することを指定しています。個々のインスタンス名
を指定することもできますが、指定しなければ、vadd_1
~vadd_3
のようにカーネル名
_1からの
数字
になります。
sp=
は、アクセラレータで使用するメモリバンクを指定しています。m_aci_
の後のgmem
が
ソースコードのpragma文のbundle=
で指定した文字列に対応しています。
補足
u200は、メモリバンクが4つ(0~3)あり、ここでは、メモリバンク1を使用することを
指定しています。
コンフィグファイルの仕様は多岐に渡りますが、最小では、nk=
文だけあればOKです。
今回は、後々(次回)の説明の都合上、sp=
も使ってみました。
注意
xclbinの作成には、無茶苦茶時間がかかります。今回のような単純なカーネルでさえ、1時間半
ほど掛かっています。
4. ホスト側プログラムの作成
カーネルを実行するホスト側のプログラムを作成します。ホスト側のプログラムは、OpenCLを使用します。
参照
前章でも参照した、Vitisマニュアル
を参照しています。また、OpenCL 1.2の仕様書を
参照しています。今回インストールしたXRTパッケージでサポートしているOpenCLのバージョンは、1.2です。
ここでは、OpenCLの解説をするつもりはなくて、カーネルを動かすプログラムをなるべく機械的に作れる ようなひな形を示すことを目的として説明します。
以下に処理の流れと実行するOpenCLの関数を示します。
- プラットフォーム(cl_platform_id)の取得
clGetPlatformIDs - デバイス(cl_device_id)の取得
clGetDeviceIDs - コンテキスト(cl_context)の作成
clCreateContext - コマンドキュー(cl_command_queue)の作成
clCreateCommandQueue - プラグラム(cl_program)のロード
clCreateProgramWithBinary - カーネル(cl_kernel)の取得
clCreateKernel - メモリの準備とバッファ(cl_mem)の作成
clCreateBuffer - カーネル引数の設定
clSetKernelArg - 入力データの転送要求の投入
clEnqueueMigrateMemObjects - カーネルの実行要求の投入
clEnqueueTask - 出力データの転送要求の投入
clEnqueueMigrateMemObjects - 完了待ち合わせ
clFinish - リソース解放
関数割愛
この順序のとおりに実行していけばOKです。
サンプルプログラムの全体は、以下にありますので参照してください。
https://github.com/oda-g/FPGA/tree/main/xilinx/vadd/src/host/vadd_cl.c
ご覧いただくと分かるように、まさにこの順で実行しているだけです。
以下にサンプルプログラムのコード片を示しながら、ポイントを説明します。 (対照しやすいように行番号を付記します。)
プラットフォーム取得
61 err = clGetPlatformIDs(1, &platform_id, NULL);
サンプルプログラムは、プラットフォームは1種類と仮定しています。プラットフォームがある場合 (例えば、FPGAとGPU)は、複数取得した上で選択する必要があります。 プラットフォーム選択の際は、clGetPlatformInfoにて、各プラットフォームの情報を取得して選択します。 (サンプルプログラムに使用例があります。)
デバイス取得
82 err = clGetDeviceIDs(platform_id, CL_DEVICE_TYPE_ACCELERATOR, 83 1, &device_id, NULL);
サンプルプログラムは、デバイスが1台と仮定しています。デバイスが 複数ある場合は、複数取得した上で選択する必要があります。デバイスの選択の際は、clGetDeviceInfo にて各デバイスの情報を取得して選択します。
コンテキスト作成
90 context = clCreateContext(NULL, 1, &device_id, NULL, NULL, &err);
補足
OpenCLの各リソース(クラス)の関係は結構複雑ですが、本プログラムは最も単純なパターンを使用していると言えます。
コマンドキュー作成
97 cmd_queue = clCreateCommandQueue(context, device_id, 98 CL_QUEUE_PROFILING_ENABLE, &err);
プログラムのロード
22 #define XCLBIN "../../bin/vadd.xclbin" 105 fd = open(XCLBIN, O_RDONLY); 110 if (fstat(fd, &st) == -1) { 114 xclbin_size = st.st_size; 116 xclbin = (unsigned char *)mmap(NULL, xclbin_size, PROT_READ, 117 MAP_PRIVATE, fd, 0); 124 program = clCreateProgramWithBinary(context, 1, &device_id, 125 &xclbin_size, (const unsigned char **)&xclbin, 126 NULL, &err);
プログラム(cl_program)の取得は、clCreateProgramWithBinaryを使用する必要があります。 clCreateProgramWithBinaryには、xclbinファイルの内容を渡す必要がありますので、xclbinファイルのパス名が 分かっている必要があります。サンプルプログラム では、ファイルの内容の取得にmmapを使用しています。
補足
clCreateProgramWithBinaryの延長で、(必要であれば)ドライバにxclbinの内容がロードされます。
カーネルの取得
139 kernel = clCreateKernel(program, "vadd", &err);
Vitisマニュアルによると、「"vadd:{vadd_1}"」のように特定のカーネルインスタンスを指定することも できます(複数指定可)。
補足
"vadd"
指定(あるいは、カーネルインスタンスを複数指定)の場合、どのカーネルインスタンスを実行
するかの選択は、ドライバが行っています。
メモリの準備とバッファの作成
146 data_in1 = (cl_int *)aligned_alloc(ALIGNMENT, sizeof(cl_int) * DATA_SIZE); 151 data_in2 = (cl_int *)aligned_alloc(ALIGNMENT, sizeof(cl_int) * DATA_SIZE); 156 data_out = (cl_int *)aligned_alloc(ALIGNMENT, sizeof(cl_int) * DATA_SIZE); 171 bo_in1 = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR, 172 sizeof(cl_int) * DATA_SIZE, data_in1, &err); 178 bo_in2 = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR, 179 sizeof(cl_int) * DATA_SIZE, data_in2, &err); 185 bo_out = clCreateBuffer(context, CL_MEM_WRITE_ONLY | CL_MEM_USE_HOST_PTR, 186 sizeof(cl_int) * DATA_SIZE, data_out, &err);
プロセス側のメモリは、ページ境界に合っている必要があるので、aligned_allocを使用して います。
clCreateBufferでプロセスメモリと関連付けたバッファの作成を行っています。入力用と 出力用で一部引数が異なります。
バッファは、デバイス側のメモリを管理するためのリソースですが、ここでは、プロセスメモリ と紐づけているところがポイントです。
サンプルプログラムでは、入力用のデータを用意している箇所があります。
162 srand((unsigned int)time(NULL)); 163 for (i = 0; i < DATA_SIZE; i++) { 164 data_in1[i] = rand(); 165 data_in2[i] = rand(); 166 cpu_out[i] = data_in1[i] + data_in2[i]; 167 data_out[i] = 0; 168 }
cpu_outは、後で、FPGAの計算結果が正しいかチェックするためのものです。実運用のプログラムで こんなことをしていては、アクセラレータを使用する意味がありません。テスト用プログラム ならではの部分だと言えます。
カーネル引数の設定
193 err = clSetKernelArg(kernel, 0, sizeof(cl_mem), &bo_in1); 199 err = clSetKernelArg(kernel, 1, sizeof(cl_mem), &bo_in2); 205 err = clSetKernelArg(kernel, 2, sizeof(cl_mem), &bo_out); 211 err = clSetKernelArg(kernel, 3, sizeof(cl_int), &size);
このタイミング、すなわち、データ転送要求の前にカーネル引数の設定を行います。 当然のことですが、カーネルのインタフェースは理解している必要があります。
sizeに関しては、直感的に分かり易いと思いますが、カーネル引数と同じ型を用意して、渡し ます。ポインタ引数に関しては、バッファのアドレスを指定します。
補足
バッファは、OpenCLのリソースですから、OpenCLライブラリの延長で、よろしく何らかの変換が行われて
いることが想像できます。(次回解説予定)
データ転送、カーネル実行要求
218 err = clEnqueueMigrateMemObjects(cmd_queue, 2, (cl_mem []){bo_in1, bo_in2}, 219 0, 0, NULL, NULL); 226 err = clEnqueueTask(cmd_queue, kernel, 0, NULL, NULL); 233 err = clEnqueueMigrateMemObjects(cmd_queue, 1, &bo_out, 234 CL_MIGRATE_MEM_OBJECT_HOST, 0, NULL, NULL);
入力データの転送要求、カーネルの実行要求、出力データの転送要求をこの順で、コマンドキューに 投入します。データの転送要求には、clEnqueueMigrateMemObjectsを使用します。入力用と出力用で 一部引数が異なります。
実行完了待ち合わせ
241 err = clFinish(cmd_queue);
コマンドキューに投入した要求の完了を待ち合わせします。
補足
本プログラムのパターンでは、実際のコマンド実行の契機にもなっているので、実は、この延長で
実際のデータ転送やカーネル実行が行われます。
ホスト側プログラムのビルドは以下のように行います。
$ gcc -o vadd_cl -I/opt/xilinx/xrt/include vadd_cl.c -L/opt/xilinx/xrt/lib -lxrt_core -lxrt_coreutil -lOpenCL -lpthread
実行は、以下のとおりです。(実行に先立ち、環境変数の設定が必要です。)
$ source /opt/xilinx/xrt/setup.sh $ ./vadd_cl ... OK. ...
問題なければ、「OK.」の出力があるはずです。
5. ドライバの解析
XRTパッケージのインストール時にドライバのインストールも行われますが、dkmsという仕組みを使っており、 インストール時にドライバのビルドが行われます。以下のコマンド実行で、dkmsの仕組みを 使っているものを把握することができます。
$ dkms status xrt, 2.6.655, 4.15.0-60-generic, x86_64: installed
ソースコードは、/usr/src/xrt-2.6.655/ 配下にインストールされています。 ソースコードが参照できるのはありがたいですね。
補足
ソースコードは、元々、githubで公開されています。ドライバだけではなく、
XRTパッケージの元となったソースコード一式となります。今回使用したパッケージのソースコードを
参照するには、tag 202010.2.6.655 をチェックアウトすれば良いです。
ドライバのビルドがされたということは、ドライバのビルドに必要なパッケージは既にインストールされて おり、ビルドのためのお膳立てはできているということを意味します。
これは、手間が掛からず、ドライバのビルドができそうですが、実際、簡単にできます。
一応、その場でビルドするのは避けて、ソースコードを別の場所にコピーして行います。
$ cp -r /usr/src/xrt-2.6.655/ ~ $ cd ~/xrt-2.6.655/driver/xocl $ sudo make KERNELDIR=/lib/modules/$(uname -r)/build SYMBOL=1
userpf/xocl.ko
と mgmtpf/xclmgmt.ko
がビルドされたドライバになります。上記では、SYMBOL=1
を付けましたが、これは、デバッグ情報付きでビルドする指定になります。
補足
何か修正して、ドライバを置き換えたい場合は、既存のドライバをrmmod(ex. 「sudo rmmod xocl」)し、
作成したドライバをinsmod(ex.「sudo insmod userpf/xocl.ko」)すればよいです。上記は、デバッグ情報
を付けただけなので、置き換えは意味ありません。
デバッグ情報付きドライバ(Linuxカーネルモジュール)ができたので、これで、crashコマンドによる解析もできますね。
補足
crashを使用するためには、linuxのdebug symbolパッケージが必要です。crash、linuxのdebug symbol
パッケージのインストールは、以下のページを参照してください。
https://github.com/oda-g/eBPF/blob/master/doc/kernel_analysis.rst
$ sudo crash /usr/lib/debug/boot/vmlinux-$(uname -r) ... crash> mod |grep xocl ffffffffc0778400 xocl 786432 (not loaded) [CONFIG_KALLSYMS] crash>
先ほど作成した、デバッグ情報付きモジュールを使って見ましょう。(mod -s
で、明示的に
モジュールのパスを指定します。)
crash> mod -s xocl /home/hoge/xrt-2.6.655/driver/xocl/userpf/xocl.ko MODULE NAME SIZE OBJECT FILE ffffffffc0778400 xocl 786432 /home/hoge/xrt-2.6.655/driver/xocl/userpf/xocl.ko crash> struct -o exec_core struct exec_core { [0] struct platform_device *pdev; [8] struct mutex exec_lock; [40] void *base; [48] void *csr_base; [56] void *cq_base; [64] unsigned int cq_size; [68] u32 intr_base; [72] u32 intr_num; [76] struct xocl_ert_sched_privdata ert_cfg_priv; [84] bool needs_reset; [88] wait_queue_head_t poll_wait_queue; ... [4896] unsigned int uid; [4900] unsigned int ip_reference[128]; } SIZE: 5416 crash>
ばっちりですね、ドライバ内に定義された構造体の情報も見れました。
これで、ドライバに何か不具合があっても、調べたり、直したりできることが分かったので一安心です。
6. 参考サイト
アダプティブコンピューティング研究推進体(ACRi)という団体があって、そのWebサイトが参考
になりました(特にブログ記事)。
https://www.acri.c.titech.ac.jp/wp/
今回使用した、Xilinx Alveo U200が装備されたマシンを無料で借りることもできます。と言っても、 ドライバを置き換えるような乱暴はできませんのでご注意(そもそも、sudoが使えない)。 作成したカーネルの動作確認等には有用かと思います。時間貸しなので、カーネルのビルドまでは、 手元のマシンで行えるようにしておくと良いかと思います。
7. おわりに
カーネルをビルドして動かせるようにはなりました。それだけでは、少し寂しいので、 筆者の守備範囲の話も足しておきました。ともかく、ホスト側については、いろいろと調べられる 環境は整えられたと思います。
次回は、OpenCLの裏(OpenCLよりも低レイヤ)でどんなことが行われているのか、見ていきます。 なかなかカーネル(FPGA)の話に行けませんが、中身(LinuxカーネルないしはLinuxカーネルに近いところで 何やっているか)が気になってしまうのは、カーネル(Linux)屋の性(さが)でしょうね。