FPGAカーネル超入門 (2)

執筆者 : 小田 逸郎


1. はじめに

前回 は、OpenCLを使用したホスト側プログラムから、カーネルを動かせるようになりましたが、ホスト側で何をやっているのか、特にカーネルを実行するためにどんなことが行われているのか気になります。

そこで、今回は、OpenCLよりも低レイヤのライブラリを使用してホスト側のプログラムを作成しながら、何をやっているのか、見ていきます。

2. XRT

前回は、パッケージをインストールしただけで、あまり説明しませんでしたが、このXRTというのが、ホスト側で動作するコンポーネントのすべてになります。

ソースコードは、以下のgithubにあります。

https://github.com/Xilinx/XRT

補足
今回の環境でインストールしたパッケージは、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コアライブラリを使用して、カーネルを動作させるホスト側のプログラムを作成してみましょう。

以下に処理の流れと使用するライブラリ関数を示します。

  1. デバイスハンドルの取得
    xclOpen
  2. xclbinのロード (省略可)
    xclLoadXclBin
  3. CUコンテキストのオープン
    xclOpenContext
  4. 入出力バッファの準備
    xclAllocBO + xclMapBO
    xclAllocUserPtrBO
    xclGetBOProperties
  5. 入力データのメモリ転送
    xclSyncBO
  6. カーネル実行
    xclAllocBO + xclMapBO
    xclExecBuf
  7. 完了待ち
    xclExecWait
  8. 出力データのメモリ転送
    xclSyncBO
  9. 資源解放
    関数割愛

流れとしては、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. おわりに

ホスト側で行われていることは分かった気がしますし、少なくとも調べれば分かる自信はついたので、いよいよ、カーネル側に挑戦したいと思います。とは言っても、現状まだネタはありません。次回はいつになるか分かりませんが、お楽しみに。