Ryzen NPU の利用方法

Ryzen NPU サーバー

Ryzen NPU サーバーでは Ryzen APU が搭載する NPU を使ったプログラミングを体験することができます。利用できるサーバーのスペックを次の表に示します。

ホスト名APUCPUiGPUNPUメモリ
ds001Ryzen AI 370Zen 5 : 4コア
Zen 5c : 8コア
RDNA 3.5 16CUXDNA 232GB

IRON による Ryzen NPU プログラミング

Ryzen AI 300 シリーズ APU に搭載される NPU は XDNA 2 と呼ばれ、ザイリンクス由来の AI Engine 技術が採用されています。AI Engine のアーキテクチャについて次のブログ記事で概要を解説しています(記事中で解説しているアーキテクチャは初代の AI Engine です。XDNA 2 は第3世代の AIE-MLv2 アーキテクチャのため詳細は異なります)。

AI を加速する AI Engine アーキテクチャ解説と入門チュートリアル

NPU 向けにプログラミングを行うためのツールチェーンとして IRON が提供されています。IRON では Python を使用して AI Engine アレイ上のカーネル配置やデータ移動の記述とコンパイルを行います。AI Engine コアの C++ コード(AIE カーネルコードと呼びます)のコンパイルにはオープンソースの Peano コンパイラを使用します。IRON を使用する利点は、Python によるメタプログラミングでデータフローグラフを記述できる点です。FPGA 向けのツールではデータフローグラフを記述すれば細かな制御(メモリリソースの取得・開放、カーネルの起動など)は自動で生成されますが、IRON ではこれらの低レベルな制御もユーザーが記述する必要があります。

IRON チュートリアル

NPU で float 型のベクトル加算を行うプログラムを IRON を使って作成しましょう。全体の流れは次の通りです。

  1. AIE カーネルコード(C++)を作成する
  2. AIE プログラムコード(Python)を作成する
  3. ホストコード(C++)を作成する

まずは IRON ツールチェーンを使えるようにするために環境設定を行います。ds001 にログインし、ターミナルで次のコマンドを実行してください。

source /tools/repo/Xilinx/mlir-aie/ironenv/bin/activate
source /tools/repo/Xilinx/mlir-aie/utils/env_setup.sh

ベクトル加算を行う以下の AIE カーネルコードを vadd_kernel.cppとして保存してください。

extern "C"
void vadd_kernel(float* in0, float* in1, float* out){
    for (int i = 0; i < 1024; i++) {
        out[i] = in0[i] + in1[i];
    }
}

このコードではふたつの float 型のベクトルを入力とし、要素ごとの足し算を行った結果を出力します。

AI Engine をターゲットにして Peano コンパイラでコンパイルします。

${PEANO_INSTALL_DIR}/bin/clang++ \
    -O2 -std=c++20 --target=aie2p-none-unknown-elf \
    -DNDEBUG -I${MLIR_AIE_INSTALL_DIR}/include \
    -c vadd_kernel.cpp -o vadd_kernel.o

vadd_kernel.cpp がどのようなアセンブリにコンパイルされたかは次のコマンドで確認できます。

${PEANO_INSTALL_DIR}/bin/llvm-objdump -dr vadd_kernel.o

次に AIE プログラムを作成します。AIE プログラムでは AIE アレイ上で実行される AIE カーネルの配置、AIE カーネルと外部メモリとのデータの受け渡し方法を Python コードで定義します。次のコードを vadd_aie.py として保存してください。

import numpy as np
import aie.iron as iron

# (1) 入出力データの型を定義
vec_type = np.ndarray[(1024,), np.dtype[np.float32]]

# (2) カーネル関数定義
kernel = iron.Kernel(
    "vadd_kernel",                  # カーネル関数名
    "vadd_kernel.o",                # コンパイル済みオブジェクト
    [vec_type, vec_type, vec_type], # 引数型
)

# (3) AIEコアで実行するメイン関数の定義
def core_fn(of_in0, of_in1, of_out, kernel):
    # メモリリソースを取得
    elemOut = of_out.acquire(1)
    elemIn0 = of_in0.acquire(1)
    elemIn1 = of_in1.acquire(1)
    # カーネルを実行
    kernel(elemIn0, elemIn1, elemOut)
    # メモリリソースを開放
    of_in0.release(1)
    of_in1.release(1)
    of_out.release(1)

# (4) データ移動のためのObject FIFOを作成
of_in0 = iron.ObjectFifo(vec_type, name="in0")
of_in1 = iron.ObjectFifo(vec_type, name="in1")
of_out = iron.ObjectFifo(vec_type, name="out")

# (5) コアプログラムとObject FIFOを指定してWorkerを作成
worker = iron.Worker(
    core_fn, [of_in0.cons(), of_in1.cons(), of_out.prod(), kernel],
)

# (6) 実行時のAIEアレイとDRAMの間のデータ移動を定義
rt = iron.Runtime()
with rt.sequence(vec_type, vec_type, vec_type) as (in0, in1, out):
    rt.start(worker)
    rt.fill(of_in0.prod(), in0)
    rt.fill(of_in1.prod(), in1)
    rt.drain(of_out.cons(), out, wait=True)

# (7) リソースの配置を行いMLIRを生成
program = iron.Program(iron.device.NPU2(), rt)
module = program.resolve_program(iron.placers.SequentialPlacer())

# (8) MLIRを出力
print(module)

AIE プログラムコードの内容について解説します。

  1. AIE プログラム内で移動するデータのひとかたまりを定義しています。
  2. C++ で記述したカーネルコードを AIE プログラムから利用できるように定義します。
  3. AIE コア 単体で実行されるプログラムを定義します。この中では、AIE カーネルへ受け渡しするデータのメモリリソースの取得し、AIE カーネルを起動します。不要になったメモリリソースを解放します。
  4. データを受け渡しする通信路としての Object FIFO を作成します。Object FIFO は生産側から入力されたデータを消費側へ出力する FIFO です。タイル間や、外部メモリとのデータ移動を抽象化したオブジェクトで、実際には隣接するローカルメモリを介した直接転送や、インターコネクトを介したDMA転送として実装されます。
  5. Worker を作成します。Worker は、ひとつの AIE タイルに相当します。そのタイルのコアで実行するコアプログラムと、そのコアプログラムへの入出力を定義します。AIE タイルを複数使う場合は AIE アレイ上のどのタイルにどのコアプログラムを配置するかを指定します。
  6. ワーカーの起動と、外部メモリ(DRAM)と AIE アレイとの間のデータ受け渡しを定義します。rt.start(worker) で指定したワーカーを起動します。rt.fill で外部メモリから Object FIFO へデータを供給します。rt.drain で Object FIFO からデータを消費し外部メモリへ格納します。
  7. 対象とするデバイスを指定して、ワーカーや Object FIFO を配置し、MLIR を生成します。

Python プログラムとして実行することで MLIR が生成されます。次のコマンドで MLIR をファイルに保存します。

python vadd_aie.py > vadd_aie.mlir

MLIR から最終的な AIE バイナリ(XCLBIN)と命令シーケンスを生成します。

aiecc.py \
    --aie-generate-xclbin --aie-generate-npu-insts \
    --peano ${PEANO_INSTALL_DIR} \
    --no-compile-host --no-xchesscc --no-xbridge \
    --xclbin-name=vadd.xclbin \
    --npu-insts-name=vadd_inst.bin \
    vadd_aie.mlir

AIE バイナリには AIE アレイ上のプログラムや配線情報が含まれます。命令シーケンスはシムアレイで実行される DMA を制御するためのプログラムです。

ホストコードを実装します。vadd_host.cpp として保存してください。

#include <algorithm>
#include <fstream>
#include <iostream>
#include <random>
#include <string>
#include <vector>

#include "xrt/xrt_bo.h"
#include "xrt/xrt_device.h"
#include "xrt/xrt_kernel.h"

int main(int argc, char** argv) {
    // (1) XRT環境の準備
    auto device = xrt::device(0);
    auto xclbin = xrt::xclbin(std::string("vadd.xclbin"));
    device.register_xclbin(xclbin);
    xrt::hw_context context(device, xclbin.get_uuid());
    auto kernel = xrt::kernel(context, "MLIR_AIE");

    // (2) 乱数生成の準備
    std::mt19937 rng(std::random_device{}());
    std::uniform_real_distribution<float> dist(-1.0f, 1.0f);

    // (3) 命令シーケンスの読み込み
    std::vector<uint32_t> inst;
    {
        std::ifstream ifs("vadd_inst.bin", std::ios::binary | std::ios::ate);
        inst.resize(ifs.tellg() / 4);
        ifs.seekg(0);
        ifs.read(reinterpret_cast<char*>(inst.data()), inst.size() * 4);
    }

    // (4) バッファオブジェクトの作成
    auto bo_inst = xrt::bo(device, inst.size() * sizeof(uint32_t), xrt::bo::flags::cacheable, kernel.group_id(1));
    auto bo_in0  = xrt::bo(device, 1024 * sizeof(float), xrt::bo::flags::host_only, kernel.group_id(3));
    auto bo_in1  = xrt::bo(device, 1024 * sizeof(float), xrt::bo::flags::host_only, kernel.group_id(4));
    auto bo_out  = xrt::bo(device, 1024 * sizeof(float), xrt::bo::flags::host_only, kernel.group_id(5));

    // (5) 命令シーケンスをコピー
    std::copy(inst.begin(), inst.end(), bo_inst.map<uint32_t*>());

    // (6) 入力ベクトルを乱数で初期化
    std::span in0 { bo_in0.map<float*>(), 1024 };
    for (auto& x : in0) x = static_cast<float>(dist(rng));

    std::span in1 { bo_in1.map<float*>(), 1024 };
    for (auto& x : in1) x = static_cast<float>(dist(rng));

    // (7) 出力ベクトルをゼロクリア
    std::span out { bo_out.map<float*>(), 1024 };
    std::fill(out.begin(), out.end(), 0);

    // (8) ホストからデバイスのメモリに同期
    bo_inst.sync(XCL_BO_SYNC_BO_TO_DEVICE);
    bo_in0 .sync(XCL_BO_SYNC_BO_TO_DEVICE);
    bo_in1 .sync(XCL_BO_SYNC_BO_TO_DEVICE);
    bo_out .sync(XCL_BO_SYNC_BO_TO_DEVICE);

    // (9) AIE実行
    unsigned int opcode = 3;
    auto run = kernel(opcode, bo_inst, inst.size(), bo_in0, bo_in1, bo_out);
    run.wait();

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

    // (11) 出力結果をチェック
    int mismatch = 0;
    for (int i = 0; i < 1024; i++) {
        if (i < 4) {
            std::cout << "In0[" << i << "] = " << in0[i] << std::endl;
            std::cout << "In1[" << i << "] = " << in1[i] << std::endl;
            std::cout << "Out[" << i << "] = " << out[i] << std::endl;
        }
        if (out[i] != in0[i] + in1[i]) mismatch++;
    }

    if (mismatch == 0) {
        std::cout << "PASS" << std::endl;
    } else {
        std::cout << "FAIL : mismatch=" << mismatch << std::endl;
    }
}

ホストコードの内容を解説します。

  1. XRT API を使用して、生成した XCLBIN ファイルを読み込み、AIE プログラムを実行する準備を行います。なお、ここで定義している kernel は AIE カーネルコードではなく、AIE プログラム全体を指します。
  2. 乱数生成を準備しています。
  3. 命令シーケンスをメモリ上に読み出しています。
  4. ホストコードと AIE プログラムとの間で受け渡しするバッファオブジェクトを作成します。
  5. バッファオブジェクトに命令シーケンスをコピーします。
  6. 入力ベクトルを乱数で初期化します。
  7. 出力ベクトルをゼロクリアします。
  8. ホストで作成したバッファオブジェクトに AIE アレイからアクセスできるように同期します。
  9. AIE プログラムを実行します。
  10. AIE アレイが出力を行ったバッファオブジェクトのデータにホストからアクセスできるように同期します。
  11. AIE で計算した結果が、ホスト上で計算した期待値と比較して不一致がないかチェックします。

ホストコードをビルドするには次のコマンドを実行してください。

g++ -std=c++23 -I${XILINX_XRT}/include -L${XILINX_XRT}/lib -c -o vadd_host.o vadd_host.cpp
g++ vadd_host.o -o vadd_host -L${XILINX_XRT}/lib -lxrt_coreutil

以下に実行例を示します。PASS と表示されれば成功です。

$ ./vadd_host
In0[0] = -0.183126
In1[0] = -0.671229
Out[0] = -0.854355
In0[1] = 0.221616
In1[1] = -0.79003
Out[1] = -0.568414
In0[2] = -0.222909
In1[2] = 0.372655
Out[2] = 0.149746
In0[3] = -0.608294
In1[3] = 0.941139
Out[3] = 0.332845
PASS

より詳しいプログラミング方法については IRON AIE プログラミングガイドをお読みください。非公式日本語訳は以下で読めます。

IRON AIEプログラミングガイド(非公式日本語訳)

タイトルとURLをコピーしました