Vitis Accel Examples を試す(2/3)
Vitis_Accel_Examples 内の cpp_kernels/burst_rw のソースを読みます。
ソースを眺める
ソースは src のしたにあります
- host.cpp :ホスト側のソース(OpenCL で結構難しい。コピペして使うことになりそう)
- vadd.cpp :FPGA 側のソース さて、ポイントは HLS のソースでもある vadd.cpp です。冒頭部分を引用します。
extern "C" {
void vadd(int* a, int size, int inc_value) {
// Map pointer a to AXI4-master interface for global memory access
#pragma HLS INTERFACE m_axi port = a offset = slave bundle = gmem max_read_burst_length = 256 max_write_burst_length = \
256
// We also need to map a and return to a bundled axilite slave interface
#pragma HLS INTERFACE s_axilite port = a
#pragma HLS INTERFACE s_axilite port = size
#pragma HLS INTERFACE s_axilite port = inc_value
#pragma HLS INTERFACE s_axilite port = return
ここでのポイントは2つ。1つは gmem で bundle された HLS INTERFACE m_axi の 変数 a。もう1つは HLS INTERFACE s_axilite 。 Vivado HLS(Vitis HLS) に慣れた人なら見覚えがある記述でしょう。 OpenCL であるため extern “C” というのがちょっとした違いかもしれません。
通常の HLS とは違い、 XRT でつかえるカーネルはある一定の命名規則とインタフェースを持っている 必要があります。どうやら ap_fifo などは使えないようです。
XRT(Xilinx Runtime library)
突然出てきたキーワード XRT。ソースも github で公開されています。 FPGA とユーザの間の API は SDSoC では UIO とか使ってました(Deprecated!!)が、XRT の導入によって 整理されました。OpenCL をつかっているので、メモリの管理も 整理されたと言ってよいでしょう。
XRT Controlled Kernel Execution Models では幾つかの Model のケースを掲げています。 図中の CU は Compute Unit の略。(Host Programingを参照)
XRT のモデルは Vitis HLS で生成される信号線 (ap_start, ap_ready, ap_done, ap_continue) で見覚えがあるものばかりです。 それが U50 のような PCIe の場合、レジスタとして CPU 側から見えます。 RTL Kernels参照
オフセット | 名称 | 説明 |
---|---|---|
0x0 | 制御 | カーネル ステータスを制御および示します。 |
0x4 | グローバル割り込みイネーブル | ホストへの割り込みをイネーブルにします。 |
0x8 | IP 割り込みイネーブル | 割り込みの生成に使用する IP で生成された信号を制御します。 |
0xC | IP 割り込みステータス | 割り込みステータスを示します。 |
0x10 | カーネル引数 (アドレス 0x10 で開始) | スカラーおよびグローバル メモリ引数を含みます。 |
このように規定されており、0x10 以降が HLS の axilite と対応します。 制御の為の信号線を on/off しないといけないか等は気にしなくてよく、 OpenCL の仕組み(API)で吸収されます。
ホスト側の呼び出し
OCL_CHECK(err, err = krnl_add.setArg(0, buffer_rw));
OCL_CHECK(err, err = krnl_add.setArg(1, size));
OCL_CHECK(err, err = krnl_add.setArg(2, inc_value));
確認はしていませんが、、、buffer_rw はアドレスで AXIM は 64bit の アドレスを用意しているので、64bit になるはずです。 また buffer はユーザ側が用意しなければならず、このソースでは 次のようにしています。
OCL_CHECK(err, cl::Buffer buffer_rw(context, CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR, vector_size_bytes, source_inout.data(), &err));
<中略>
OCL_CHECK(err, err = q.enqueueMigrateMemObjects({buffer_rw}, 0 /* 0 means from host*/));
結果を得る
次のようにして結果を得ています。 enqueueTask がカーネルの実行。つまり、 なかで ap_start とかの信号線をアサートしているのでしょう。 enqueueMigrateMemObjects で buffer_rw から情報を書き戻してます。 つまり FPGA 側にあるメモリから HOST(すなわち Intel CPU) へ 転送しています。
// Launch the Kernel
OCL_CHECK(err, err = q.enqueueTask(krnl_add));
// Copy Result from Device Global Memory to Host Local Memory
OCL_CHECK(err, err = q.enqueueMigrateMemObjects({buffer_rw}, CL_MIGRATE_MEM_OBJECT_HOST));
OCL_CHECK(err, err = q.finish());
vadd 側のソース
これが内部バッファを使っていてバースト転送するソースになっていて ちょっと難しいですが、やっていることは a というバッファのアドレスから 値を読んで値を足して書き戻しています。
<前略>
burstbuffer[j] = a[i + j];
<中略>
burstbuffer[j] = burstbuffer[j] + inc_value;
a[i + j] = burstbuffer[j];
<後略>
burstbuffer をつかって内部の SRAM を使うようにしています。 a のバッファは FPGA 内のバッファです。あれ?いつ 誰が FPGA 内のバッファへ情報を転送したのだ?
どうやってメモリを転送しているのだ?
OpenCL として host.cpp から転送命令はあるものの、 vadd.cpp のソースに現れない誰かが HOST(CPU) から DEVICE(FPGA) へ そしてその逆の転送をしています。
これは恐らくブラックボックスの XDMA がやっているのでしょう。 (たぶん XRT のソースを読めばわかるはず。が読んでません)
おまけ Linux Kernel 5.11 以降
Linux Kernel 5.11 以降 DRM(Direct Rendering Manager) の API が変更になり、現行の XRT では対応できなくなりました。なので、Linux のカーネルは 5.11 より前にしましょう。 私は戻すのに苦労しました。
XRT Second Generation Linux Kernel Driver というのが開発されているみたいです。(2021/09/17 時点で work-in-progress)