執筆者 : 小田 逸郎
1. はじめに
前回 は、OpenCLを使用したホスト側プログラムから、カーネルを動かせるようになりましたが、ホスト側で何をやっているのか、特にカーネルを実行するためにどんなことが行われているのか気になります。
そこで、今回は、OpenCLよりも低レイヤのライブラリを使用してホスト側のプログラムを作成しながら、何をやっているのか、見ていきます。
2. XRT
前回は、パッケージをインストールしただけで、あまり説明しませんでしたが、このXRTというのが、ホスト側で動作するコンポーネントのすべてになります。
ソースコードは、以下のgithubにあります。
補足
今回の環境でインストールしたパッケージは、tag 202010.2.6.655 でビルドされたものなので、それを参照します。
この中には、OpenCLライブラリはもちろんのこと、それよりも低レイヤなライブラリや、Linuxカーネルで動くドライバ、さらには、FPGA上のmicro blazeという小さなCPUコアで動作するプログラムのソースコードまで含まれています。
今回は、XRTで提供されているライブラリの中で、XRTコアライブラリと呼ばれるものを使用してカーネルを動かしてみます。
参照
ソースコードの他、以下のドキュメントを参照しています。(以下、XRTドキュメントと呼びます。)
Xilinx Runtime (XRT) Architecture https://xilinx.github.io/XRT/2020.1/html/index.html
(表題から分かるとおり、XRTというのは、Xilinx Runtimeの略なのですね。)
3. xclbinの内容
カーネルを実行するためには、xclbinの内容を理解する必要があります。前回作成した、vadd.xclbin の内容を参照してみましょう。
補足
コマンドやサンプルプログラムを実行する際には、環境変数の設定が必要です。以降、いちいち触れませんが、注意願います。
$ source /opt/xilinx/xrt/setup.sh
ざっと概要をつかむためには、以下のコマンドを実行します。
$ xclbinutil --info -i vadd.xclbin
出力結果は、ここ に置いてあるので参照してください。
参考
xclbinの形式については、ソースコード中の以下のヘッダファイルを参照してください。
src/runtime_src/core/include/xclbin.h
以下、カーネルの実行に必要なポイントに絞って、説明します。
xclbin Information ------------------ ... Sections: DEBUG_IP_LAYOUT, BITSTREAM, MEM_TOPOLOGY, IP_LAYOUT, CONNECTIVITY, CLOCK_FREQ_TOPOLOGY, BUILD_METADATA, EMBEDDED_METADATA, SYSTEM_METADATA
上記の部分を見ると、xclbinに含まれるセクションの種別が分かります。BITSTREAM というのが多分、カーネルを含んだ回路の本体でしょうね。それは必須として、それ以外にカーネルを実行するためには、MEM_TOPOLOGY、IP_LAYOUT、CONNECTIVITY、EMBEDDED_METADATA の4つのセクションが必要です。
それぞれの内容を見てみましょう。
$ xclbinutil --dump-section IP_LAYOUT:json:ip_layout.json -i vadd.xclbin $ xclbinutil --dump-section MEM_TOPOLOGY:json:mem_topology.json -i vadd.xclbin $ xclbinutil --dump-section CONNECTIVITY:json:connectivity.json -i vadd.xclbin $ xclbinutil --dump-section EMBEDDED_METADATA:raw:embedded_metadata.xml -i vadd.xclbin
dump-section
オプションでセクションの内容を取り出すことができます。IP_LAYOUT、MEM_TOPOLOGY、CONNECTIVITYは、実体はバイナリなので、見やすさを考えて、json形式に変換して出しています。
EMBEDDED_METADATAは、実体がテキスト (xml) なので、そのまま (raw) 出しています。
出力結果は、ここ の下に置いてあるので、参照してください。
IP_LAYOUTより、一部抜粋。
"m_ip_data": [ { "m_type": "IP_KERNEL", "m_int_enable": "1", "m_interrupt_id": "0", "m_ip_control": "AP_CTRL_CHAIN", "m_base_address": "0x1800000", "m_name": "vadd:vadd_1" }, { "m_type": "IP_KERNEL", "m_int_enable": "1", "m_interrupt_id": "0", "m_ip_control": "AP_CTRL_CHAIN", "m_base_address": "0x1810000", "m_name": "vadd:vadd_2" }, { "m_type": "IP_KERNEL", "m_int_enable": "1", "m_interrupt_id": "0", "m_ip_control": "AP_CTRL_CHAIN", "m_base_address": "0x1820000", "m_name": "vadd:vadd_3" },
これを見ると、カーネルインスタンス (m_name) とベースアドレス (m_base_address) の対応が分かります。
MEMTOPOLOGYより、一部抜粋。
"m_mem_data": [ { "m_type": "MEM_DDR4", "m_used": "0", "m_sizeKB": "0x1000000", "m_tag": "bank0", "m_base_address": "0x4000000000" }, { "m_type": "MEM_DDR4", "m_used": "1", "m_sizeKB": "0x1000000", "m_tag": "bank1", "m_base_address": "0x5000000000" },
これを見ると、メモリバンクの情報が分かります。
CONNECTIVITYより、一部抜粋。
"m_connection": [ { "arg_index": "0", ===> カーネル引数のindex 0(in1) "m_ip_layout_index": "0", ===> IP_LAYOUT m_ip_dataのindex 0(vadd:vadd_1) "mem_data_index": "1" ===> MEMTOPOLOGY m_mem_dataのindex 1(bank1) },
これと、IP_LAYOUT、MEMTOPOLOGYを合わせて見ると、カーネルのポインタ引数で使用するFPGAのメモリバンクが分かります。
上記の例では、カーネルインスタンス vadd_1 の in1 で使うメモリバンクは、bank1 である、ということが特定できました。
EMBEDDED_METADATAより、一部抜粋。
<kernel name="vadd" language="c" vlnv="xilinx.com:hls:vadd:1.0" preferredWorkGroupSizeMultiple="0" workGroupSize="1" debug="false" interrupt="true" hwControlProtocol="ap_ctrl_chain"> <module name="vadd"/> <port name="M_AXI_GMEM" mode="master" range="0xFFFFFFFF" dataWidth="32" portType="addressable" base="0x0"/> <port name="S_AXI_CONTROL" mode="slave" range="0x1000" dataWidth="32" portType="addressable" base="0x0"/> <arg name="in1" addressQualifier="1" id="0" port="M_AXI_GMEM" size="0x8" offset="0x10" hostOffset="0x0" hostSize="0x8" type="void*"/> <arg name="in2" addressQualifier="1" id="1" port="M_AXI_GMEM" size="0x8" offset="0x1C" hostOffset="0x0" hostSize="0x8" type="void*"/> <arg name="out_r" addressQualifier="1" id="2" port="M_AXI_GMEM" size="0x8" offset="0x28" hostOffset="0x0" hostSize="0x8" type="void*"/> <arg name="size" addressQualifier="0" id="3" port="S_AXI_CONTROL" size="0x4" offset="0x34" hostOffset="0x0" hostSize="0x4" type="unsigned int"/>
EMBEDDED_METADATAからは、カーネルの引数に関する情報を得ます。上記の size=
と offset=
を後で参照することになります。
4. XRTコアライブラリによるプログラミング
それでは、XRTコアライブラリを使用して、カーネルを動作させるホスト側のプログラムを作成してみましょう。
以下に処理の流れと使用するライブラリ関数を示します。
- デバイスハンドルの取得
xclOpen - xclbinのロード (省略可)
xclLoadXclBin - CUコンテキストのオープン
xclOpenContext - 入出力バッファの準備
xclAllocBO + xclMapBO
xclAllocUserPtrBO
xclGetBOProperties - 入力データのメモリ転送
xclSyncBO - カーネル実行
xclAllocBO + xclMapBO
xclExecBuf - 完了待ち
xclExecWait - 出力データのメモリ転送
xclSyncBO - 資源解放
関数割愛
流れとしては、OpenCLの場合とよく似ていますね。
サンプルコードの全体は、ここ にあるので、参照してください。
以下では、コード片を参照しながら、裏で行われている処理について説明していきます。
デバイスハンドルの取得
64 dev_handle = xclOpen(0, NULL, 0);
第1引数には、デバイスインデックスを指定します。デバイスインデックスは、0から始まる数字でシステムに装備されているデバイスの数(-1)までとなります。
システムに装備されているデバイスの数は、xclProbe関数で取得することができます。
ここでは、デバイスの数がひとつと仮定して、0を指定しています。
デバイスが複数ある場合は、xbutil scan
を実行すれば、以下のような出力が出てくるので、判別することができます。(下記は、2台付いていたときの例で、今回の実行環境とは異なります。)
$ xbutil scan INFO: Found total 2 card(s), 2 are usable ... [0] 0000:86:00.1 xilinx_u50lv_gen3x4_xdma_base_2 user(inst=129) [1] 0000:3b:00.1 xilinx_u200_xdma_201830_2(ID=0x5d1211e8) user(inst=128)
xclOpenでは、/dev/dri/renderD128
というファイルがオープンされ、以降、ドライバとのやりとりは、このファイル (をオープンしたファイルディスクリプタ) に対する、ioctl で行われることになります。(128の部分は、上記のinst=
に対応しています。)
補足
XilinxのFPGAドライバは、LinuxカーネルのDRI (Direct Rendering Interface) という、主にGPUを対象としたデバイスドライバ用のフレームワークを利用しています。/dev/dri/renderD...
ファイルは、そのフレームワークの使用時にできるファイルとなります。
xclOpenで返しているdev_handleの実体は、XRTコアライブラリの内部構造体 (あるクラスのインスタンス) へのポインタです。
補足
xclOpenの第2引数、第3引数は、XRTドキュメントには説明が書いてありますが、実際には使用されていません。
xclbinのロード
71 fd = open(XCLBIN, O_RDONLY); 82 xclbin = (struct axlf *)mmap(NULL, xclbin_size, PROT_READ, 83 MAP_PRIVATE, fd, 0); 91 ret = xclLoadXclBin(dev_handle, (const struct axlf *)xclbin);
xclbinファイルを読み込んで、xclLoadXclBinを実行します。
xclLoadXclBinでは、xclbinをロードするioctlを実行しています。ドライバでは、xclbinのBITSTREAMをFPGAに書き込む処理などを実行することになります。
ドライバでは、xclbinのuuidを見ており、既に同じxclbinがロードされていた場合は、何もしません。別のxclbinがロードされていた場合は、デバイスが使用中でなければ、置き換えられます。
xclbinのロードは、コマンドでも実行可能です。
$ xbutil program -p ./vadd.xclbin
あらかじめ、このコマンドの実行を行っておくことを前提とすれば、本節のコードは不要となります。敢えて、例題のため、入れておきました。
なお、このコマンドは、実は、xclLoadXclBinの実行を行っているだけのものです。
さて、xclLoadXclBinの中では、xclbinのロード要求のioctlの後に、ドライバにコマンド実行のためのioctlを実行しています。コマンドの種類は、いくつかありますが、ここでは、ERTコンフィグというコマンドを実行しています。詳細は割愛しますが、ここで説明するポイントとしては、このコマンドで、CUインデックスとカーネルインスタンスの対応をドライバに指示しているということです。
CUというのは、ドライバが扱うカーネルの実行単位で、0からのインデックスで識別します。
CUインデックスは、後で、カーネル実行の際、どのカーネルインスタンスを実行するのか指定するために使用します。CUインデックスとカーネルインスタンスの対応は、xclbinのロード後に以下のファイルを見ると分かります。
$ cat /sys/devices/pci0000:3a/0000:3a:00.0/0000:3b:00.1/mb_scheduler.u.5242880/kds_custat CU[@0x1800000] : 0 status : 0 CU[@0x1810000] : 0 status : 0 CU[@0x1820000] : 0 status : 0 CU[@0x250000] : 0 status : 0 <== これが何かは説明割愛 KDS number of pending commands: 0 KDS number of running commands: 0
上から、CU 0、1、2 と並んでおり、それぞれ、ベースアドレス(m_base_address)が書いてあるので、それからカーネルインスタンスとの対応が分かります。
メモ
デバイスは、2つのファンクションが提供されていて、.0がカードの管理用で、xclmgmtドライバが担当、.1がカーネルの実行用で、xoclドライバが担当しています。それぞれ、/sys/devicesの下に様々な情報を出しています。上記のパス名ですが、環境により、PCI番号が異なるのはもちろんですが、パスの構成も少し変わるのでご注意ください。本例の場合、PCIブリッジ (0000:3a:00.0) の下にカードが付いているので、少しパスが長くなっています。
注意
CUインデックスとカーネルインスタンスの対応は、XRTライブラリの内部ロジックにより、決まるので、一旦ロードして、上記ファイルで確かめるのが確実です。一応、XRTライブラリのコードを見ると、ベースアドレス順にしているようです (正確には、その他の条件もあります)。
ここで、注意しておきたいのは、CUインデックスの順と、IP_LAYOUTの順とは、必ずしも一致しないということです。(本例は、たまたま一致しているにすぎないです。)
CUコンテキストのオープン
103 for (i = 0; i < 3; i++) { 105 ret = xclOpenContext(dev_handle, xclbin_uuid, (unsigned int)i, 1); 110 }
使用する可能性のある、CUに対し、コンテキストをオープンします。コンテキストがオープンされているかどうかは、ドライバが管理する情報となります。カーネルの実行時にコンテキストがオープンされていないとエラーになります。
ここでは、CU 0、1、2を使用する可能性があるので、それぞれオープンしています。
コンテキストのオープンには、デバイスを使用中にするという意味もあります。コンテキストがオープンされているデバイスに対しては、xclbinの置き換えができなくなります。カーネルの実行に関係なく、デバイスを使用中にするために、CUインデックス(xclOpenContextの第3引数)に0xffffffffを指定してxclOpenContextを実行することもできます。
第4引数は、shared(1)か、exclusive(0)の指定ですが、exclusiveでコンテキストをオープンすると、他のプロセスが同時に同じCUをオープンすることができなくなります。OpenCLライブラリではデフォルトでは、sharedでオープンしています。CUコンテキストは複数のプロセスでオープン可能であり、同時にカーネル実行することができます。カーネルの実行はドライバでスケジューリングされます。
メモ
OpenCLライブラリやXRTライブラリの動作を、コンフィグレーションファイル (xrt.ini) で変更可能です。詳細は、XRTドキュメントをご参照ください。OpenCLで、CUコンテキストをsharedでオープンするかexclusiveでオープンするかを変更可能です。
入出力バッファの準備
20 #define ARG_BANK 1 /* bank 1 */ 113 bo_in1 = xclAllocBO(dev_handle, data_len, 0, ARG_BANK); 132 if (xclGetBOProperties(dev_handle, bo_in1, &bo_prop) != 0) { 136 p_in1 = bo_prop.paddr; 153 data_in1 = (int *)xclMapBO(dev_handle, bo_in1, true);
(in1のみ示しましたが、in2、outに関しても同様です。)
xclAllocBOで、入出力用のバッファオブジェクト(BO)の獲得を行います。内部では、BO獲得のためのioctlが発行されています。BOというのは、ドライバで、デバイス側メモリを管理するためのデータ構造です。xclAllocBOの中で、BO獲得のためのioctlが発行され、ドライバで デバイス側のメモリの確保が行われます。
第4引数でメモリバンクの番号を指定する必要があります。したがって、実行しようとしているカーネルインスタンスがどのメモリバンクを使用しているのか、知っている必要があります。 前章で見たように、xclbinを解析すると分かります。
xclMapBOで、獲得したBOに関連付けて、プロセス空間の領域を確保します。内部的には、mmapシステムコールが発行されています。ここで注意しておきたいのは、このプロセス空間にアクセスしても、ホスト側のメモリが割り当てられるだけで、デバイスのメモリにアクセスできるわけではないということです。
第3引数は、プロセス空間が、read only (false) か、read/write (true) かを指定しています。(プロセスから見て、ホストメモリに対するアクセスに関するものです。)
補足
出力用 (bo_out) に関しては、本来readするだけなので、falseでも良かったのですが、サンプルプログラムでは、念のため、最初に0クリア (すなわち、write) しているので、trueになっています。
xclGetBOPropertiesは、BOの情報を取得するための関数ですが、ここでは、デバイス側メモリのアドレス(デバイス側でのアドレス)を取得するために使用しています。カーネル実行時に必要となります。
本例では、xclAllocBOとxclMapBOの組み合わせで、入出力用バッファの準備をしましたが、既存のプロセス空間領域がある場合は、xclAllocUserPtrBOを使用することもできます。 以下の要領となります。
data_in1 = (cl_int *)aligned_alloc(ALIGNMENT, data_len); bo_in1 = xclAllocUserPtrBO(dev_handle, data_in, data_len, ARG_BANK);
OpenCLでは、こちらのパターンが使用されています。
入力データのメモリ転送
180 if (xclSyncBO(dev_handle, bo_in1, XCL_BO_SYNC_BO_TO_DEVICE, data_len, 0) != 0) {
(in1のみ示しましたが、in2も同様です。)
xclSyncBOでホスト側メモリのデータをデバイス側メモリに転送します。カーネルの実行前に済ましておく必要があります。
カーネル実行
カーネル実行の前に、まずはコマンド実行用バッファの準備をする必要があります。
193 bo_cmd = xclAllocBO(dev_handle, (size_t)CMD_SIZE, 0, XCL_BO_FLAGS_EXECBUF); 200 start_cmd = (struct ert_start_kernel_cmd *)xclMapBO(dev_handle, bo_cmd, true);
要領は、入出力バッファと同様ですが、BOの属性が少し違っています。xclAllocBOの第4引数でXCL_BO_FLAGS_EXECBUF を指定していますが、これは、コマンド実行用であることを示しています。
(第4引数は、上位がフラグで、下位がバンク番号の構成となります。この場合、バンク番号は関係ないので、0になっています。前出の入出力用BOのケースでは、フラグが0となっています。)
コマンド実行用BOは、デバイス側のメモリとは関係ありません。プロセスとドライバ間のデータのやりとりに使用されるもので、仮想的なバッファとなります。xclMapBOでマップした領域は、プロセスとドライバの間の共有メモリと捉えることができます。
23 #define ARG_IN1_OFFSET 0x10 24 #define ARG_IN2_OFFSET 0x1c 25 #define ARG_OUT_OFFSET 0x28 26 #define ARG_SIZE_OFFSET 0x34 206 memset(start_cmd, 0, CMD_SIZE); 207 start_cmd->state = ERT_CMD_STATE_NEW; 208 start_cmd->opcode = ERT_START_CU; 209 start_cmd->stat_enabled = 1; 210 start_cmd->count = 1 + (ARG_SIZE_OFFSET/4 + 1); 211 start_cmd->cu_mask = 0x7; /* CU 0, 1, 2 */ 212 start_cmd->data[ARG_IN1_OFFSET/4] = p_in1 & 0xffffffff; 213 start_cmd->data[ARG_IN1_OFFSET/4 + 1] = p_in1 >> 32; 214 start_cmd->data[ARG_IN2_OFFSET/4] = p_in2 & 0xffffffff; 215 start_cmd->data[ARG_IN2_OFFSET/4 + 1] = p_in2 >> 32; 216 start_cmd->data[ARG_OUT_OFFSET/4] = p_out & 0xffffffff; 217 start_cmd->data[ARG_OUT_OFFSET/4 + 1] = p_out >> 32; 218 start_cmd->data[ARG_SIZE_OFFSET/4] = DATA_SIZE; 219 220 LOG("xclExecBuf\n"); 221 if (xclExecBuf(dev_handle, bo_cmd) != 0) {
コマンド実行用バッファに内容を埋めて、xclEexecBufを実行します。
xclEexecBufでは、コマンド実行用のioctlが発行されています。
バッファの内容の形式(この場合、struct ert_start_kernel_cmd)については、ヘッダ src/runtime_src/core/include/ert.hをご参照ください。
補足
xclLoadXclBinの中で実行している、ERTコンフィグコマンドの実行も、同様の方法で実行されています。実行しているコマンド種別が異なり、バッファの内容が異なります。
ここでは、opcodeに ERT_START_CU を指定していますが、このコマンドがカーネル実行であることを示しています。
cu_maskに実行するCUを指定しますが、ビットマスクの形で複数のCUを指定することができます。
本例では、7すなわち、CU 0、1、2 を指定していますが、これは、CU 0、1、2 のどれかで実行してください、と指示することになります。
本例の場合、どのカーネルインスタンスも同等で、同じメモリバンクを使用しているので、このような指定ができます。xclbinのビルド時にそれぞれのカーネルインスタンスの使用するメモリバンクを別々のものに指定することもできます。そうした場合は、このようにまとめて指定することはできません。
前回のOpenCLの例では、カーネルインスタンスの区別はせず、単にvadd
カーネルを指定して実行しましたが、本例と同様、cu_mask = 7
で実行されています。
どのCUを使用するかは、ドライバが選択します。もし、本サンプルプログラムを複数同時に実行した場合、3つまでは、同時に実行されます。3つ以上、同時に実行しても問題はなく、ドライバでキューイングされ、CUが空き次第実行されることになります。
dataの部分がPCIのメモリ空間に書き込む内容となります。(本例の場合は、dataのoffset 0の位置から。)
0 4 8 C +--------------------+--------------------+--------------------+--------------------+ 0x00 |[0] |[1] |[2] |[3] | +--------------------+--------------------+--------------------+--------------------+ 0x10 |[4] in1 下位32bit |[5] in1 上位32bit |[6] |[7] in2 下位32bit | +--------------------+--------------------+--------------------+--------------------+ 0x20 |[8] in2 上位32bit |[9] |[10] out 下位32bit |[11] out 上位32bit | +--------------------+--------------------+--------------------+--------------------+ 0x30 |[12] |[13] size | (litle endian) +--------------------+--------------------+
xclbinのEMBEDDED_METADATAセクションの説明で出てきた、offset
というのが、カーネル引数を配置するオフセットになります。本サンプルプログラムでは、予め、xclbinから読み取った内容をdefineしています。
さて、ここまで来て、カーネルのポインタ引数のところは、結局のところ、確保したBOのデバイス側メモリアドレスを指定しているというが分かりました。
メモ
最初の16バイトにも意味があって、ここには、カーネルの実行ステータスがデバイスによって示されるので、ドライバはそれを読み取って、カーネルの実行終了を検知することができます。
ドライバは、そのステータスをコマンドバッファのstate
に反映するので、プロセスからは、それを参照して、カーネルの実行終了を検知することができます。
なお、カーネルの実行や終了検知については、FPGA内のmicro blazeも関係していて、いろいろあるのですが、詳細は割愛します。
補足
コマンド形式については、XRTドキュメントに記述されているわけではなく、XRTライブラリのソースコードを解析して、プログラムを作成しました。
おまけ
stat_enabled
は、OpenCL 使用時には、0で、xrt.iniでも変更することはできません。1を指定すると、カーネルのstate変更時の時刻を記録することができます。サンプルプログラムでは、カーネルが実際に
デバイス上で実行した時間を求めています。(詳細は割愛しますが、コードの以下の部分です。)
234 ts = ert_start_kernel_timestamps(start_cmd); 235 mes_time = ts->skc_timestamps[ERT_CMD_STATE_COMPLETED] \ 236 - ts->skc_timestamps[ERT_CMD_STATE_RUNNING]; 237 printf("FPGA kernel execution time: %ld(ns)\n", mes_time);
参考
デバイスが使用しているPCIのメモリ空間について、少し見てみましょう。
$ lspci -vv -s 0000:3b:00.1 3b:00.1 Processing accelerators: Xilinx Corporation Device 5001 Subsystem: Xilinx Corporation Device 000e Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx- Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx- Latency: 0, Cache Line Size: 32 bytes Interrupt: pin A routed to IRQ 271 NUMA node: 0 Region 0: Memory at 382ff0000000 (64-bit, prefetchable) [size=32M] Region 2: Memory at 382ff4020000 (64-bit, prefetchable) [size=64K] Region 4: Memory at 382fe0000000 (64-bit, prefetchable) [size=256M] Capabilities: <access denied> (補足: rootで実行すると、この部分、詳細に出ます) Kernel driver in use: xocl Kernel modules: xocl
ここで、Region 0 がカーネルの実行に関係していることろです。 詳しく見てみると、xclbinのロード前は、以下のようになっていますが、
$ sudo cat /proc/iomem (一部抜粋) 382000000000-382fffffffff : PCI Bus 0000:3a 382fe0000000-382ff40fffff : PCI Bus 0000:3b 382fe0000000-382fefffffff : 0000:3b:00.1 382ff0000000-382ff1ffffff : 0000:3b:00.1 382ff00b0000-382ff00b0fff : rom.u.0 382ff00c0000-382ff00cffff : xvc_pub.u.6291456 382ff0180000-382ff0180fff : mb_scheduler.u.5242880 382ff0190000-382ff019ffff : mb_scheduler.u.5242880 382ff0200000-382ff020002f : mailbox.u.15728640
xclbinのロード後には以下のようになります。
$ sudo cat /proc/iomem (一部抜粋) 382000000000-382fffffffff : PCI Bus 0000:3a 382fe0000000-382ff40fffff : PCI Bus 0000:3b 382fe0000000-382fefffffff : 0000:3b:00.1 382ff0000000-382ff1ffffff : 0000:3b:00.1 382ff00b0000-382ff00b0fff : rom.u.0 382ff00c0000-382ff00cffff : xvc_pub.u.6291456 382ff0180000-382ff0180fff : mb_scheduler.u.5242880 382ff0190000-382ff019ffff : mb_scheduler.u.5242880 382ff0200000-382ff020002f : mailbox.u.15728640 382ff0270000-382ff0270fff : aximm_mon.u.24117249 382ff0280000-382ff0280fff : aximm_mon.u.24117250 382ff0290000-382ff0290fff : aximm_mon.u.24117248 382ff1800000-382ff180ffff : cu.u.36700160 382ff1810000-382ff181ffff : cu.u.36700161 382ff1820000-382ff182ffff : cu.u.36700162
cu...
の部分のアドレスに見覚えがありませんか。Region 0 の開始アドレスにカーネルインスタンスのベースアドレス (IP_LAYOUTのm_base_address) を足したものですね。カーネル実行時には、コマンドバッファのdata部分をここに書き込むことにより、カーネル実行がスタートし、実行完了もこのアドレスにアクセスすることにより分かるということですね。
そのように回路が組まれているということでしょうね、きっと。(aximm_mon... は、メモリ転送に関係してそうです。想像ですが。)
完了待ち
227 ret = xclExecWait(dev_handle, -1);
カーネルの実行完了の待ち合わせは、xclExecWaitで行います。xclExecWaitの中では、/dev/dri/renderD128
をオープンしたファイルディスクリプタにpollシステムコールを実行しているだけです。
なお、pollシステムコールは何かイベントが起きたら返るだけなので、もし、ひとつのプロセスで複数のカーネル実行を行う (可能です) と、どのカーネルが終了したのかを、コマンドバッファのstateを参照して識別する必要が出てきます。
出力データのメモリ転送
240 if (xclSyncBO(dev_handle, bo_out, XCL_BO_SYNC_BO_FROM_DEVICE, data_len, 0) != 0) {
カーネルの実行完了後、結果にアクセスするために、デバイス側メモリからホスト側メモリにデータ転送します。入力データのときと同様、xclSyncBOを使用しますが、方向 (第3引数) が異なります。
ビルドと実行
サンプルコードは、以下のようにビルドして、実行できます。
$ gcc -o vadd_xrt -I/opt/xilinx/xrt/include vadd_xrt.c -L/opt/xilinx/xrt/lib -lxrt_core -lxrt_coreutil $ ./vadd_xrt
OpenCL との対応
OpenCLの流れの中で、XRTコアライブラリの実行がどうなっているか観察してみたところ、以下のようになっていました。
- clGetPlatformIDs
- 情報取得のため、xclOpen、xclGetDeviceInfo2、xclClose 実行
- clGetDeviceIDs
- clCreateContext
- xclOpen
- clCreateCommandQueue
- clCreateProgramWithBinary
- xclLoadXclBin
- clCreateKernel
- clCreateBuffer
- xclAllocUserPtrBO (※1)
- clSetKernelArg
- xclAllocUserPtrBO (※1)
- clEnqueueMigrateMemObjects
- clEnqueueTask
- xclOpenContext
- clEnqueueMigrateMemObjects
- clFinish
- xclSyncBO(in側)
- カーネル実行
- xclAllocBO(cmd用)
- xclMapBO(cmd用)
- xclExecBuf
- xclExecWait
- xclSyncBO(out側)
※1: カーネルにより、実行タイミングが異なります。clSetKernelArgを行わないと、メモリバンクが分からないケースがあるためです。
clCreateProgramWithBinary では、xclbinのセクションの内容も解析して、カーネル実行に必要な情報を保持するようになっています。xmlの解析などご苦労なことです。
clEnqueue... は、OpenCLのコマンドキューにキューイングするだけで、実際の実行は行われません。
キューイングしたコマンドは、clFlushを行うと実行されます。サンプルプログラムでは、clFlushは実行していませんが、clFinishを実行すると暗黙にclFlushが実行されるので、その延長で実際のコマンド実行が行われます。
5. まとめ
XRTコアライブラリを使用して、カーネルを実行するプログラムを作成してみました。
結局のところ、プログラムの作成には、XRTライブラリのソースコードの解析が必要で、ドキュメントの情報だけでは出来ませんでした。実は、ドキュメントには書いてないけど、カーネル実行のためのライブラリ関数が存在してはいるのですが、見たところ、あまり使いやすそうではありませんでした。
xclbinの形式にしても、ホスト側の実行ライブラリにしても、どうもまだあまり整理できていない印象ですね。
まあ、OpenCLの裏側でどんなことが行われているかは分かったし、結局のところ、OpenCLを使っておくのが一番簡単そうなので、今後は、OpenCLを使うことにします。
一番疑問だったカーネルの実行方法ですが、PCIのメモリ空間のカーネルインスタンス用領域にアクセスしているという身も蓋もない結果となりました。xclbinビルド時にそうなるような回路を生成しているということですね。
6. おわりに
ホスト側で行われていることは分かった気がしますし、少なくとも調べれば分かる自信はついたので、いよいよ、カーネル側に挑戦したいと思います。とは言っても、現状まだネタはありません。次回はいつになるか分かりませんが、お楽しみに。