Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Section 3 - My First Program

このセクションでは、AIE配列用の最初の完全なプログラムを作成します。ここでは、デバイスバイナリとホストバイナリの2つの異なるバイナリアーティファクトをコンパイルします。デバイスバイナリは、AIE配列の構成を含むXCLBINファイル(コンピュートコアのプログラムメモリ、データムーバーのバッファディスクリプタ、スイッチボックス設定など)と、外部メモリとの間のデータ移動を実行するシーケンス命令を含むinsts.binで構成されます。ホストバイナリは、デバイスバイナリをロードし、insts.binシーケンスをトリガーし、何らかのテストベンチチェックを実行して結果を検証する実行可能プログラムです。

セクション1セクション2で学んだ構造設計とデータ移動の概念を組み合わせて、ホストバイナリは主にC++またはPythonで作成され、Xilinx RunTime (XRT)およびAMD XDNA Driverを使用してデバイスと通信します。セクション4では、ホストコードを使用した性能測定とトレースについて詳しく説明します。

このセクションで使用する例は、ベクトルスカラー乗算c = a * factor)です。これは、basic programming_examplesディレクトリにあります。このガイドセクションのコードスニペットの完全版は、programming_examplesディレクトリで見つけることができます。入力ベクトルaは合計4096個のint32要素で構成され、1024個の要素を持つ4つのチャンクに分割されて処理されます。

AIE配列の構造記述

設計の記述はaie2.pyファイルにあります。その構造はセクション1で紹介したhigh-level IRONスタイルを使用しており、Workerタスクを配置し、ホストからAIE配列へのシーケンスを定義します。この設計では、乗算を実行するコンピュートコアとともにshimDMAユニットを配置し、入力と出力データを外部メモリとの間で移動させます。簡単にするために、この設計例では外部関数として定義されたバイナリカーネル(つまり、構造記述の外部でコンパイルされたカーネル)を使用します。

tensor_size = 4096
tile_size = tensor_size // 4

# テンソル型の定義
tensor_ty = np.ndarray[(tensor_size,), np.dtype[np.int32]]
tile_ty = np.ndarray[(tile_size,), np.dtype[np.int32]]
scalar_ty = np.ndarray[(1,), np.dtype[np.int32]]

# 外部バイナリカーネルの定義
scale_fn = Kernel(
    "vector_scalar_mul_aie_scalar",
    "scale.o",
    [tile_ty, tile_ty, scalar_ty, np.int32],
)

データ移動

次に、データ移動を設定する必要があります。これはセクション2で詳しく説明されているObject FIFOを使用します。この例では、3つのObject FIFOがあります:2つの入力FIFO(入力ベクトルa用に1つとスカラー係数用に1つ)と1つの出力FIFO(出力ベクトルc用)です。各Object FIFOの深さは2です。これにより、ShimのDMAとCompute TileのDMAが並行して実行でき、一方がバッファへの書き込みを行っている間に、もう一方がバッファからの読み取りを行うことができます。

# 入力データ移動
of_in = ObjectFifo(tile_ty, name="in")
of_factor = ObjectFifo(scalar_ty, name="infactor")

# 出力データ移動
of_out = ObjectFifo(tile_ty, name="out")

Object FIFOは、shimDMAと外部メモリ間のデータ転送を実行する際に使用されます。このデータ転送の実装は、ランタイムシーケンスで定義されます。ランタイムシーケンスについては、セクション2dで詳しく説明されています。現在の例では、Runtime()クラスを使用して、入力データ(rt.fill())と出力データ(rt.drain())のshimDMA操作を設定します。また、Workerタスクを開始します(rt.start())。

# AIE配列との間でデータを移動するランタイム操作
rt = Runtime()
with rt.sequence(tensor_ty, scalar_ty, tensor_ty) as (a_in, f_in, c_out):
    rt.start(my_worker)
    rt.fill(of_in.prod(), a_in)
    rt.fill(of_factor.prod(), f_in)
    rt.drain(of_out.cons(), c_out, wait=True)

コンピュートコアのアクセスパターン

最後に、各Object FIFOのオブジェクトにアクセスするパターンを定義します。Object FIFOオブジェクトへのアクセスは、プロデューサとコンシューマのハンドルを介して実行されます。コンピュートコアは、Object FIFOオブジェクトのacquire(取得)とrelease(解放)を行います。

入力Object FIFO of_inと出力Object FIFO of_outについては、各要素を取得し、外部カーネル関数scale_fnを介して処理し、それぞれのObject FIFOに解放します。これらの操作は、全4096要素を完全に処理するために、各反復で1024要素のチャンクを処理する4回のループで繰り返されます。スカラー係数Object FIFO of_factorについては、コアがループを開始する前に要素を取得し、処理が完了した後に解放します。

# コアが実行するタスク
def core_fn(of_in, of_factor, of_out, scale_scalar):
    elem_factor = of_factor.acquire(1)
    for _ in range_(4):
        elem_in = of_in.acquire(1)
        elem_out = of_out.acquire(1)
        scale_scalar(elem_in, elem_out, elem_factor, 1024)
        of_in.release(1)
        of_out.release(1)
    of_factor.release(1)


# タスクを実行するWorkerを作成
my_worker = Worker(core_fn, [of_in.cons(), of_factor.cons(), of_out.prod(), scale_fn])

カーネルコード

カーネルコードはvector_scalar_mul.ccファイルにあります。この例では、汎用C++コードを使用したスカラープロセッサバージョンを使用します。関数シグネチャは次のとおりです:

void vector_scalar_mul_aie_scalar(int32_t *a_in, int32_t *c_out,
                                  int32_t *factor, int32_t N) {
  for (int i = 0; i < N; i++) {
    c_out[i] = *factor * a_in[i];
  }
}

スカラー係数が配列ポインタとして渡されることに注意してください。これは、Object FIFO通信メカニズムが、スカラー係数をメモリ内の要素の配列として送信するためです。そのため、カーネルコード内で参照解除する必要があります。AIEコアのベクトルプロセッサ機能を活用する、より最適化されたベクトル化されたカーネル実装の例については、セクション4を参照してください。

ホストコード

概要

ホストコード設計は、C++(test.cpp)またはPython(test.py)で記述できます。詳細についてはセクション4bを参照してください。一般的に、C++ホストコードはC++ test utilitiesを、PythonホストコードはPython test utilitiesを利用して、コードを簡潔に保ちます。C++とPythonの両方のホストコードには、7つの主要なステップがあります。ここでは、C++バージョンに焦点を当てます。

1. プログラム引数の解析

C++ホストコードは、3つの必須引数を受け入れます:XCLBINファイルへのパス、カーネル名、シーケンス命令ファイルへのパス。オプションでverbosityフラグも受け入れます。この例では、test_utils.hを使用して引数を解析します。

// プログラム引数の解析
po::options_description desc("Allowed options");
po::variables_map vm;
test_utils::add_default_options(desc);

test_utils::parse_options(argc, argv, desc, vm);
int verbosity = vm["verbosity"].as<int>();

// 設計定数の宣言
constexpr bool VERIFY = true;
constexpr int IN_SIZE = 4096;
constexpr int OUT_SIZE = IN_SIZE;

2. 命令シーケンスの読み込み

次に、シーケンス命令ファイルを読み込みます。このファイルには、外部メモリとの間のデータ移動を実行する命令が含まれています。

// 命令シーケンスのロード
std::vector<uint32_t> instr_v =
    test_utils::load_instr_sequence(vm["instr"].as<std::string>());

3. XRT環境の作成

XRTランタイムを初期化し、デバイスとカーネルをロードします。

xrt::device device;
xrt::kernel kernel;

test_utils::init_xrt_load_kernel(device, kernel, verbosity,
                                vm["xclbin"].as<std::string>(),
                                vm["kernel"].as<std::string>());

4. XRTバッファオブジェクトの作成

XRTは最大5つのinoutバッファをサポートし、3から始まる連続したgroup_id値を使用してマッピングされます。この例では、命令シーケンス用に1つ、入力ベクトル用に1つ、スカラー係数用に1つ、出力ベクトル用に1つの、合計4つのバッファオブジェクトを作成します。番号付けは、シーケンス定義の順序に従います。最初の引数がgroup_id(3)を受け取り、2番目がgroup_id(4)を受け取る、というように続きます。この番号付けの詳細については、Python utils documentationを参照してください。

// バッファオブジェクトの設定
auto bo_instr = xrt::bo(device, instr_v.size() * sizeof(int),
                        XCL_BO_FLAGS_CACHEABLE, kernel.group_id(1));
auto bo_inA = xrt::bo(device, IN_SIZE * sizeof(int32_t),
                        XRT_BO_FLAGS_HOST_ONLY, kernel.group_id(3));
auto bo_inFactor = xrt::bo(device, 1 * sizeof(int32_t),
                            XRT_BO_FLAGS_HOST_ONLY, kernel.group_id(4));
auto bo_outC = xrt::bo(device, OUT_SIZE * sizeof(int32_t),
                        XRT_BO_FLAGS_HOST_ONLY, kernel.group_id(5));

5. データの初期化と同期

バッファオブジェクトをデータで初期化し、ホストからデバイスのメモリに同期します。

// 命令ストリームをxrtバッファオブジェクトにコピー
void *bufInstr = bo_instr.map<void *>();
memcpy(bufInstr, instr_v.data(), instr_v.size() * sizeof(int));

// バッファbo_inAを初期化
int32_t *bufInA = bo_inA.map<int32_t *>();
for (int i = 0; i < IN_SIZE; i++)
    bufInA[i] = i + 1;

// バッファbo_inFactorを初期化
int32_t *bufInFactor = bo_inFactor.map<int32_t *>();
int32_t scaleFactor = 3;
*bufInFactor = scaleFactor;

// バッファbo_outCをゼロクリア
int32_t *bufOut = bo_outC.map<int32_t *>();
memset(bufOut, 0, OUT_SIZE * sizeof(int32_t));

// ホストからデバイスのメモリに同期
bo_instr.sync(XCL_BO_SYNC_BO_TO_DEVICE);
bo_inA.sync(XCL_BO_SYNC_BO_TO_DEVICE);
bo_inFactor.sync(XCL_BO_SYNC_BO_TO_DEVICE);
bo_outC.sync(XCL_BO_SYNC_BO_TO_DEVICE);

6. AIEで実行して同期

カーネルを実行し、完了を待ち、結果をデバイスからホストのメモリに同期します。

unsigned int opcode = 3;
auto run =
    kernel(opcode, bo_instr, instr_v.size(), bo_inA, bo_inFactor, bo_outC);
run.wait();

// デバイスからホストのメモリに同期
bo_outC.sync(XCL_BO_SYNC_BO_FROM_DEVICE);

7. テストベンチチェックの実行

最後に、出力をゴールデンリファレンスと比較して結果を検証します。

// 出力をゴールデンと比較
int errors = 0;
if (verbosity >= 1) {
    std::cout << "Verifying results ..." << std::endl;
}
for (uint32_t i = 0; i < IN_SIZE; i++) {
    int32_t ref = bufInA[i] * scaleFactor;
    int32_t test = bufOut[i];
    if (test != ref) {
    if (verbosity >= 1)
        std::cout << "Error in output " << test << " != " << ref << std::endl;
    errors++;
    } else {
    if (verbosity >= 1)
        std::cout << "Correct output " << test << " == " << ref << std::endl;
    }
}

プログラムの実行

設計をコンパイルして実行するには、次のコマンドを使用します:

make
make run

これにより、C++ホストコードを使用して設計がコンパイルおよび実行されます。Pythonテストベンチバリアントを実行するには、次のコマンドを使用します:

make
make run_py

C++とPythonの両方のテストベンチバリアントは、同じinsts.binファイルと同じXCLBINファイルを使用します。

ホストコードテンプレートと設計プラクティス

すべてのホストコード設計は、同じ7つのステップに従います。設計がコンパイルされてハングしないようにするために、設計パラメータ(IN_SIZEOUT_SIZEなど)が、構造記述ファイル、カーネルソースファイル、ホストコードファイル全体で一貫していることを確認することが重要です。これらのパラメータは通常、Makefileで定義され、必要に応じて他のファイルに渡されます。これらのパラメータが一貫していない場合、システムがハングしたり、デバッグが非常に困難な動作が発生する可能性があります。詳細については、セクション4bを参照してください。この設計例を参照として使用して、独自の設計を作成できます。


注意: より詳細な情報と完全なコード例については、公式ドキュメントを参照してください。