この備忘録の概要
Zynq MPSoCの中でもハードウェア容量の大きなFPGAでは、DPU (Deep-Learning Processor Unit)の最大規模のアーキテクチャであるDPUCZDX8G
のB4096
を複数実装できる。今回は、DPUCZDX8G (B4096)
を2コア用いて、画像認識処理を2並列で処理することを目指す。本備忘録では、事前学習済みのDenseBox
(顔検出)とResNet-50
(クラス分類)モデルを2並列で処理してみることにした。Vitis AI Libraryからの使い方はそれぞれ、UG1354 vitis-ai-FaceDetectとUG1354 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
は、デフォルトのまま変更せずに構築した。
一番使用率の高いDSPでも27%の空きがあるので、 と思ったのだが、PG338 featuresには、B1024
のようなハードウェア使用率を抑えたアーキテクチャであれば、実装可能かもしれない。
Software and IP core support for up to a maximum of four homogeneous DPU instances in a single AMD Xilinx® SoC.
と書かれているので、そもそもヘテロなDPU環境は使えなさそうだ。
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)側のプログラム
DenseBox
とResNet-50
をxczu19eg
上に実装したDPU向けにコンパイルした。入力画像サイズは640x360と224x224である。詳細はworkflow-model-zooやCompiling 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関数では、コマンドライン引数の受け取り→事前学習済みDenseBox
とResNet-50
の重みのロード(create)→2種類の入力画像デコード用に2スレッド起動→推論処理用に2スレッド起動→スレッド終了待機する。
以下がビルド時に使用したCMakeLists.txt
である。OpenCV
やpthread
の他にvitis_ai_library-facedetect
とvitis_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.csv
、summary.csv
、vart_trace.csv
、vitis_ai_profile.csv
、xrt.run_summary
の5つのファイルが生成された。これらをscpでvitis
のインストールされたマシンに転送し、xrt.run_summary
をvitis_analyzer
で開くと、実行時のサマリや、DPUコアに割り当てられたタスクや時間を見ることができ、顔検出は3ミリ秒程度、クラス分類は10ミリ秒程度で処理できたことがわかる(あくまでDPU側での処理時間であり、実際はこの他にCPU側で前処理・後処理を行っている)。また、DPUを2コア使い、並列で顔検出とクラス分類ができることが確認できた。