この備忘録の概要

Zynq MPSoCの中でもハードウェア容量の大きなFPGAでは、DPU (Deep-Learning Processor Unit)の最大規模のアーキテクチャであるDPUCZDX8GB4096を複数実装できる。今回は、DPUCZDX8G (B4096)を2コア用いて、画像認識処理を2並列で処理することを目指す。本備忘録では、事前学習済みのDenseBox(顔検出)とResNet-50(クラス分類)モデルを2並列で処理してみることにした。Vitis AI Libraryからの使い方はそれぞれ、UG1354 vitis-ai-FaceDetectUG1354 vitis-ai-Classificationを参照すること。

動作確認済み環境

Zynq UltraScale+ MPSoC カスタムボードでの動作を確認した。詳細は以下の通りである。

  • Device Part: xczu19eg-ffvc1760-2-i
  • OS: Petalinux 2022.2
  • CPU: Cortex-A53
  • Petalinux SDK: 2022.2
  • Vitis-AI: 3.0
  • DPU: DPUCZDX8G v4.1 (B4096) x 2コア

カスタムボード向けへのDPU構築にはPG338 DPU-TRDのVitisフローを用いた。構築の解説は割愛するが、Vitisフローが完了すると、以下の画像のようにVivadoのブロックデザインや使用したハードウェア量を見ることができる。具体的な構築方法は、Zynq UltraScale+ MPSoC DPU TRD V4.1 Vitis 2022.2を追っていくと良い。また、Vitisフローで登場するDPUの構成を変更できる設定ファイルdpu_conf.vhは、デフォルトのまま変更せずに構築した。
Block_Design utilize_dpu 一番使用率の高いDSPでも27%の空きがあるので、B1024のようなハードウェア使用率を抑えたアーキテクチャであれば、実装可能かもしれない。 と思ったのだが、PG338 featuresには、

Software and IP core support for up to a maximum of four homogeneous DPU instances in a single AMD Xilinx® SoC.

と書かれているので、そもそもヘテロなDPU環境は使えなさそうだ。

utilize

DPU環境の確認

Zynq MPSoCデバイス上でxdputil queryコマンドを用いて、DPUコアの確認する。DPUCZDX8Gが2つと、Softmaxコアsfm_xrt_top:sfm_xrt_top_1が1つある。しかし、cu_idx: 2であるsfm_xrt_top:sfm_xrt_top_1はBitstreamに含まれているもののサポートされていない、というエラーが出ている。今回は2つのDPUCZDX8Gを並列動作で推論処理することが目的なのでSoftmaxコアは無視した。

root@zynqmp-generic:~# xdputil query
WARNING: Logging before InitGoogleLogging() is written to STDERR
E0906 07:22:55.206533   952 xdputil_query.cpp:196] Unsupported platform fingerprint: 0, cu_idx: 2
{
    "DPU IP Spec":{
        "DPU Core Count":3,
        "IP version":"v4.1.0",
        "generation timestamp":"2023-02-21 21-30-00",
        "git commit id":"7d32c41",
        "git commit time":2023022121,
        "regmap":"1to1 version"
    },
    "VAI Version":{
        "libvart-runner.so":"Xilinx vart-runner Version: 3.0.0-331ba47f80502ef2a1f37b3f7ce616b31c22e577 86 2023-01-02-21:50:29 ",
        "libvitis_ai_library-dpu_task.so":"Xilinx vitis_ai_library dpu_task Version: 3.0.0-1cccff04dc341c4a6287226828f90aed56005f4f 86 2023-01-02 14:31:50 [UTC] ",
        "libxir.so":"Xilinx xir Version: xir-9204ac72103092a7b253a0c23ec7471481656940 2023-01-02-21:49:01",
        "target_factory":"target-factory.3.0.0 860ed0499ab009084e2df3004eeb9ae710c26351"
    },
    "kernels":[
        {
            "DPU Arch":"DPUCZDX8G_ISA1_B4096",
            "DPU Frequency (MHz)":300,
            "IP Type":"DPU",
            "Load Parallel":2,
            "Load augmentation":"enable",
            "Load minus mean":"disable",
            "Save Parallel":2,
            "XRT Frequency (MHz)":300,
            "cu_addr":"0x80010000",
            "cu_handle":"0xaaaafbaf0930",
            "cu_idx":0,
            "cu_mask":2,
            "cu_name":"DPUCZDX8G:DPUCZDX8G_1",
            "device_id":0,
            "fingerprint":"0x101000056010407",
            "name":"DPU Core 0"
        },
        {
            "DPU Arch":"DPUCZDX8G_ISA1_B4096",
            "DPU Frequency (MHz)":300,
            "IP Type":"DPU",
            "Load Parallel":2,
            "Load augmentation":"enable",
            "Load minus mean":"disable",
            "Save Parallel":2,
            "XRT Frequency (MHz)":300,
            "cu_addr":"0x80011000",
            "cu_handle":"0xaaaafbb0b0b0",
            "cu_idx":1,
            "cu_mask":1,
            "cu_name":"DPUCZDX8G:DPUCZDX8G_2",
            "device_id":0,
            "fingerprint":"0x101000056010407",
            "name":"DPU Core 1"
        },
        {
            "DPU Arch":"",
            "cu_addr":"0x0",
            "cu_handle":"0x0",
            "cu_idx":2,
            "cu_mask":0,
            "cu_name":"sfm_xrt_top:sfm_xrt_top_1",
            "device_id":0,
            "fingerprint":"0x0",
            "name":"DPU Core 2"
        }
    ]
}

ホスト(CPU)側のプログラム

DenseBoxResNet-50xczu19eg上に実装したDPU向けにコンパイルした。入力画像サイズは640x360と224x224である。詳細はworkflow-model-zooCompiling the Modelを参照すると良い。
以下のコードブロックがDPUを扱うC++プログラムである。

#include <condition_variable>
#include <filesystem>
#include <iostream>
#include <mutex>
#include <opencv2/opencv.hpp>
#include <queue>
#include <thread>
#include <vector>
#include <vitis/ai/classification.hpp>
#include <vitis/ai/facedetect.hpp>

struct FrameInfo {
    FrameInfo(std::vector<std::string> file_names)
        : file_names(file_names), frame_count(0), stop(false) {
        file_count = file_names.size();
    }
    // ディレクトリに含まれる画像ファイル
    std::vector<std::string> file_names;
    // ディレクトリに含まれている画像数
    unsigned long file_count;
    // 何枚処理したか
    unsigned long frame_count;
    // スレッド停止フラグ
    bool stop;
    // 推論処理される画像キュー
    std::queue<cv::Mat> image_in;
    // image_in用のmutex
    std::mutex mtx_in;
    // image_in用の条件変数
    std::condition_variable cv_in;
};

void face_detect(vitis::ai::FaceDetect *model, FrameInfo *data) {
    while (!data->stop) {
        std::unique_lock<std::mutex> lock_in(data->mtx_in);
        // image_inがemptyでなくなるまで最長1秒待機
        data->cv_in.wait_for(lock_in, std::chrono::milliseconds(1000),
                             [&data] { return !data->image_in.empty(); });
        // 1秒待機してimage_inがemptyでstopでないならリトライ
        if (data->image_in.empty() && !data->stop) {
            continue;
        }

        cv::Mat image = data->image_in.front();
        data->image_in.pop();
        data->stop = (++(data->frame_count) == data->file_count);
        lock_in.unlock();
        // DPUによって推論処理する
        vitis::ai::FaceDetectResult result = model->run(image);
        // 顔検出処理した結果を標準出力する
        for (const auto &r : result.rects) {
            int x = r.x * result.width;
            int y = r.y * result.height;
            int size_col = r.width * result.width;
            int size_row = r.height * result.height;
            std::cout << "x = " << x << ", y = " << y
                      << ", size_col = " << size_col
                      << ", size_row = " << size_row;
            std::cout << std::endl;
        }
    }
}

void classify(vitis::ai::Classification *model, FrameInfo *data) {
    while (!data->stop) {
        std::unique_lock<std::mutex> lock_in(data->mtx_in);
        // image_inがemptyでなくなるまで最長1秒待機
        data->cv_in.wait_for(lock_in, std::chrono::milliseconds(1000),
                             [&data] { return !data->image_in.empty(); });
        // 1秒待機してimage_inがemptyでstopでないならリトライ
        if (data->image_in.empty() && !data->stop) {
            continue;
        }

        cv::Mat image = data->image_in.front();
        data->image_in.pop();
        data->stop = (++(data->frame_count) == data->file_count);
        lock_in.unlock();
        // DPUによって推論処理する
        vitis::ai::ClassificationResult result = model->run(image);
        // クラス分類した結果を標準出力する
        for (const auto &r : result.scores) {
            auto score = r.score;
            auto index = result.lookup(r.index);
            std::cout << index << ": " << score << std::endl;
        }
    }
}

void read_image(FrameInfo *data) {
    while (!data->file_names.empty()) {
        std::string path = data->file_names.front();
        data->file_names.erase(data->file_names.begin());
        cv::Mat image = cv::imread(path);
        std::unique_lock<std::mutex> lock_in(data->mtx_in);
        data->image_in.push(image);
        lock_in.unlock();
        // cv_inで待機しているスレッドを起床
        data->cv_in.notify_one();
    }
}

std::vector<std::string> get_file_names(std::string images_directory) {
    std::set<std::filesystem::path> contents_of_dir;
    // ディレクトリからファイル(画像)をすべて取得する
    std::filesystem::directory_iterator iter(images_directory), end;
    for (const auto &file :
         std::filesystem::directory_iterator(images_directory)) {
        if (file.path().extension() == ".jpg" ||
            file.path().extension() == ".png") {
            // std::setで名前順にソートする
            contents_of_dir.insert(file.path());
        }
    }
    std::vector<std::string> file_names(contents_of_dir.begin(),
                                        contents_of_dir.end());
    return file_names;
}

int main(int argc, char *argv[]) {
    /*
     *  facedetect_clasify \
     *    <path_to_densebox.xmodel> <image_dir_for_face_detection> \
     *    <path_to_resnet50.xmodel> <image_dir_for_classification>
     */

    std::cout << "argv[1] = " << argv[1] << std::endl;
    std::cout << "argv[2] = " << argv[2] << std::endl;
    std::cout << "argv[3] = " << argv[3] << std::endl;
    std::cout << "argv[4] = " << argv[4] << std::endl;

    std::string face_model_path = argv[1];
    std::string face_images_directory = argv[2];
    std::string classification_model_path = argv[3];
    std::string classification_images_directory = argv[4];

    auto model_1 = vitis::ai::FaceDetect::create(face_model_path);
    auto model_2 = vitis::ai::Classification::create(classification_model_path);

    FrameInfo *data_1 = new FrameInfo(get_file_names(face_images_directory));
    FrameInfo *data_2 = new FrameInfo(get_file_names(classification_images_directory));

    std::thread read_face_image_thread(read_image, data_1);
    std::thread read_object_image_thread(read_image, data_2);

    std::thread face_detect_thread(face_detect, model_1.get(), data_1);
    std::thread classify_thread(classify, model_2.get(), data_2);

    read_face_image_thread.join();
    read_object_image_thread.join();
    face_detect_thread.join();
    classify_thread.join();
    std::cout << "All threads joined" << std::endl;
    return 0;
}

このプログラムでは、コマンドライン引数から

  • argv[1]: 顔検出モデルへのパス
  • argv[2]: 顔検出用の画像ディレクトリ
  • argv[3]: クラス分類モデルへのパス
  • argv[4]: クラス分類用の画像ディレクトリ
    を入力すると、argv[2]画像の顔検出結果と、argv[4]画像のクラス分類結果が標準出力される。

main関数では、コマンドライン引数の受け取り→事前学習済みDenseBoxResNet-50の重みのロード(create)→2種類の入力画像デコード用に2スレッド起動→推論処理用に2スレッド起動→スレッド終了待機する。

以下がビルド時に使用したCMakeLists.txtである。OpenCVpthreadの他にvitis_ai_library-facedetectvitis_ai_library-classificationがあればビルドできると思ったが、vitis_ai_library-xnnppが無いとリンクエラーが出るため付け足した。

cmake_minimum_required(VERSION 3.15)
project(multi-task)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "-O2 -Wall")

find_package(OpenCV REQUIRED)

add_executable(facedetect_clasify facedetect_clasify.cpp)

set(DEP_LIBS
    ${OpenCV_LIBRARIES}
    pthread
    vitis_ai_library-facedetect
    vitis_ai_library-classification
    vitis_ai_library-xnnpp
)

target_link_libraries(facedetect_clasify ${DEP_LIBS})

実際にプログラムを起動すると以下の画像のように顔検出処理の結果とクラス分類の結果が混じって出力される。
動作時

Vitis Analyzerで並列動作したか確認する

Vitis AnalyzerのDPUのタイムライントレース機能で、割り当てられたDPUコアや、DPU上での処理時間を確認できる。確認するには先にFPGAデバイス上でvaitraceコマンドを用いてプロファイルする必要がある。私の環境では以下のようにしてvaitraceを実行した。

vaitrace \
./facedetect_clasify \
densebox_640_360_xczu19eg_dpu/densebox_640_360_xczu19eg_dpu.xmodel face_frame \
resnet50_224_224_xczu19eg_dpu/resnet50_224_224_xczu19eg_dpu.xmodel animal/train_transformed

vaitraceが終了すると、profile_summary.csvsummary.csvvart_trace.csvvitis_ai_profile.csvxrt.run_summaryの5つのファイルが生成された。これらをscpでvitisのインストールされたマシンに転送し、xrt.run_summaryvitis_analyzerで開くと、実行時のサマリや、DPUコアに割り当てられたタスクや時間を見ることができ、顔検出は3ミリ秒程度、クラス分類は10ミリ秒程度で処理できたことがわかる(あくまでDPU側での処理時間であり、実際はこの他にCPU側で前処理・後処理を行っている)。また、DPUを2コア使い、並列で顔検出とクラス分類ができることが確認できた。

サマリ タイムライン