Vitis Accel Examples を試す(3/3)
Vitis_Accel_Examples を試すうちに内部構造を知る必要が出てきました。 ここでは U50 の内部構造を見ていきます。
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