Vitis Accel Examples を試す(3/3)
2021-9-17 22:41 JST

Vitis_Accel_Examplesを試すうちに内部構造を知る必要が出てきました。ここでは U50 の内部構造を見ていきます。

3部作になっています。

Vitis_Accel_Examplesの本家はこちら

HBM2(High Bandwidth Memory 2)

HBM2 は高帯域幅メモリでデバイスを作る上での技術の名称でもあります。

私にとって身近な DDR などは、DOS/V ショップで売っているモジュールで実際に手元にある Xilinx のボード(例えば ZCU-102 とか)にも搭載されていいます。

一方 HBM2 は Si Interposer の上のに DRAM を3次元的に!載せています。基板じゃなくてシリコン。日経 XTECH の Siインターポーザの説明によると「Siインターポーザは配線のみを作り込んだSiチップ」とのこと。

参考資料:広帯域と大容量にフォーカスした“第2世代”のHBM2メモリ

U50 は HBM2 を使っているということが1つ。そして、HBM はチャネルをもっているということが重要です。

SLR(Super Logic Region)

U50 はSSI(スタックド シリコン インターコネクト デバイス) デバイスです。「SSI デバイスでは、シリコン インターコネクトを介して複数のシリコン ダイが一緒に接続され、1 つのデバイスにパッケージされます。」(新規ターゲット プラットフォームへの移行から引用)とのこと。この SSI を構成するのが SLR で U50 は2つの SLR を持ちます。

どこに影響してくるのか?

Vitis では cfg ファイルを使って、カーネルの配置であったり、メモリのアクセスをする為のインタフェースの指定をします。

例えばこんな感じ

[connectivity]
sp=krnl_vadd_1.in1:HBM[0:31]
sp=krnl_vadd_1.in2:HBM[0:31]
sp=krnl_vadd_1.out_r:HBM[0:31]

DDR を積んでいる他の Alveo は HSB は指定せずに DDR を指定します。

通常はこのように 0:31 のチャネルどれ使ってもいいよと v++ に教えてやりv++ が都合よく解釈して最適化をこころみるでしょう。U50 の場合HBM がついているのは SLR0 だけなので、あまり選択の余地はないでしょう。

カーネルをどの SLR に配置するかも指定可能です。

[connectivity]
slr=vmult_1:SLR1
slr=vadd_1:SLR1

SFP-SQFP を使用するのであれば SLR1 の指定をする必要があるでしょう。特に複数のカーネルを配置する場合は明示的にすることで効率をあげることが出来そうです。

複数のカーネル

複数のカーネルと言っても FPGA なので1つのビットストリームです。ap_start で起動するタイミングをもつ複数のモジュールが1つのビットストリームの中にあるという事のようです。ですから、複数のビットストリームに分散しているからと言って特段デメリットがあるわけではなく、むしろ、モジュールを分離できるのでうまく活用すべきです。

host/streaming_free_running_k2k

このサンプルは3つのカーネル(mem_read/increment/mem_write)があり非常に面白いサンプルになっています。ここではソースを掲げるだけで詳しく解説しませんが、中間の increment がAXI Stream になっているのが特徴です。

次のように cfg で各カーネルを接続することを明示します。

[connectivity]
stream_connect=mem_read_1.stream:increment_1.input
stream_connect=increment_1.output:mem_write_1.stream

increment のインタフェースは次のように至って簡単です。ap_ctrl_none なのでだれもキックしません(ap_start がない)。したがって、HOST 側へのインタフェースも持ちません。

extern "C" {
void increment(hls::stream<ap_axiu<32, 0, 0, 0> >& input, hls::stream<ap_axiu<32, 0, 0, 0> >& output) {
// For free running kernel, user needs to specify ap_ctrl_none for return port.
// This will create the kernel without AXI lite interface. Kernel will always be
// in running states.
#pragma HLS interface ap_ctrl_none port = return

ホスト側は次のように Kernel をロードするものの increment に対してはenqueueTask しません(ap_start がないから)。

    OCL_CHECK(err, krnl_increment = cl::Kernel(program, "increment", &err));
    OCL_CHECK(err, krnl_mem_read = cl::Kernel(program, "mem_read", &err));
    OCL_CHECK(err, krnl_mem_write = cl::Kernel(program, "mem_write", &err));
    <中略>
    // Launch the Kernel
    std::cout << "Launching Kernel..." << std::endl;
    OCL_CHECK(err, err = q.enqueueTask(krnl_mem_read));
    OCL_CHECK(err, err = q.enqueueTask(krnl_mem_write));

    // wait for all kernels to finish their operations
    OCL_CHECK(err, err = q.finish());

蛇足ながら、このソース cl:: という namespace でうまく OpenCL をラッピングしています。Khronos の提供する CL/cl2.h のようです。こちらの方がソースとしては見やすくなりますね。

RTL Kernel

RTL もインタフェースが合えば当然ながら組み込むことが出来ます。ap_clk や ap_rst_n や gmem や control といった名称がキーになります。生成される xo は実は zip なので、 I/F をチェックして xml をつけて zip にまとめただけということになります。

module krnl_vadd_rtl #(
  parameter integer  C_S_AXI_CONTROL_DATA_WIDTH = 32,
  parameter integer  C_S_AXI_CONTROL_ADDR_WIDTH = 6,
  parameter integer  C_M_AXI_GMEM_ID_WIDTH = 1,
  parameter integer  C_M_AXI_GMEM_ADDR_WIDTH = 64,
  parameter integer  C_M_AXI_GMEM_DATA_WIDTH = 32
)
(
  // System signals
  input  wire  ap_clk,
  input  wire  ap_rst_n,

  // AXI4 master interface
  output wire                                 m_axi_gmem_AWVALID,
  <AXI4 のI/F なので中略>
  input  wire [C_M_AXI_GMEM_ID_WIDTH - 1:0]   m_axi_gmem_BID,

  // AXI4-Lite slave interface
  input  wire                                 s_axi_control_AWVALID,
  <AXI4-Lite のI/F なので中略>
  output wire [1:0]                           s_axi_control_BRESP,

  output wire                                 interrupt
);

host/hbm_simple

HBM を使ったサンプル。カーネルのインタフェースは m_axi でgmem という bundle 名称。この gmem が HBM であると cfg で書くだけで後の転送は XDMA まかせ。簡単なものならこれで十分。バースト転送も struct をつかっているので出来るのでしょう(未確認)。

別のサンプルで hbm_large_buffers というのもあってこれは out_r がgmem2 になっている。確かにその方が速そう。

extern "C" {
void krnl_vadd(const v_dt* in1,        // Read-Only Vector 1
               const v_dt* in2,        // Read-Only Vector 2
               v_dt* out_r,            // Output Result for Addition
               const unsigned int size // Size in integer
               ) {
#pragma HLS INTERFACE m_axi port = in1 offset = slave bundle = gmem0
#pragma HLS INTERFACE m_axi port = in2 offset = slave bundle = gmem1
#pragma HLS INTERFACE m_axi port = out_r offset = slave bundle = gmem0

#pragma HLS INTERFACE s_axilite port = in1
#pragma HLS INTERFACE s_axilite port = in2
#pragma HLS INTERFACE s_axilite port = out_r
#pragma HLS INTERFACE s_axilite port = size
#pragma HLS INTERFACE s_axilite port = return

OpenCL Kernel

もうあまり使わないかもしれません。.cl の拡張子のついた由緒正しいOpenCL のプログラムです。

一部分だけソースを引用します。あまりきれいじゃないですね。

__kernel __attribute__((reqd_work_group_size(1, 1, 1))) void vadd(__global int* a, int size, int inc_value) {
    local int burstbuffer[BURSTBUFFERSIZE];
    // Per iteration of this loop perform BURSTBUFFERSIZE vector addition
    __attribute__((xcl_loop_tripcount(c_len, c_len))) for (int i = 0; i < size; i += BURSTBUFFERSIZE) {
        int chunk_size = BURSTBUFFERSIZE;
        // boundary checks