PCIe非同期推論の実行
| 項目 | 値 |
|---|---|
| カテゴリ | PCIe コプロセッシング |
| 難易度 | 初級 |
| 推定所要時間 | 15 minutes |
| ラベル | PCIe, asynchronous, throughput, detection |
このチュートリアルでは、チュートリアル024で使用されたYOLOv8sの画像とボックスデコードの構成、および640x480のストリートシーンを再利用します。ストレージと画像デコードがPCIeの測定に影響を与えないように、同じ画像を繰り返し送信します。
ウォークスルー
1つの検出モデルを構成する
画像を1回ロードし、カード側のCOCO前処理とYOLOv8ボックスデコードを構成し、キュー0に1つのModelを構築します。ファイルが見つからない場合や、カードの起動に失敗した場合、測定が開始される前にプログラムが停止します。
const Args args = parse_args(argc, argv);
if (!std::filesystem::is_regular_file(kModelPath)) {
throw std::runtime_error(std::string("model does not exist: ") + kModelPath);
}
const cv::Mat image = cv::imread(kImagePath, cv::IMREAD_COLOR);
if (image.empty()) {
throw std::runtime_error(std::string("OpenCV could not decode: ") + kImagePath);
}
pcie::ConnectionOptions connection;
connection.card_id = args.card_id;
pcie::Model model(kModelPath, detection_options(), connection);
model.build(kBuildTimeoutMs);
パイプラインをウォームアップする
タイミングを計測せずに、いくつかの完全な検出を実行します。ウォームアップは、モデルの起動と最初のバッファーの効果を、報告されたワークロードから取り除きます。
for (int index = 0; index < kWarmupFrames; ++index) {
(void)detection_count(model.run(image, kPullTimeoutMs));
}
同時に送信および取得する
1つのスレッドがpush()を使用して画像を送信し、別のスレッドが有限のタイムアウトを持つpull()を使用してBBOX出力を取得します。小さなアプリケーション専用のFIFOは、各順序付けされた送信の開始時刻を保存します。拒否、タイムアウト、または不正な結果が発生した場合、モデルは閉じられ、別のスレッドが起動します。
この例では、通常のModelのフロー制御動作のみに依存しており、アプリケーションにはキューの深さの調整はありません。
const BenchmarkResult result = measure(model, image, kMeasuredFrames);
完了した作業を報告する
両方のスレッドが完了し、すべての受け入れられた画像が取得された後にのみ、タイミングの計測を停止します。1秒あたりのフレーム数は、完了した出力の数を使用します。平均レイテンシーは、各送信試行から、対応する順序付けされた結果が到着するまでの時間を測定します。
std::cout << "completed=" << result.completed << '\n';
std::cout << std::fixed << std::setprecision(2) << "elapsed_seconds=" << result.elapsed_seconds
<< '\n'
<< "throughput_fps=" << result.completed / result.elapsed_seconds << '\n'
<< "average_latency_ms=" << result.average_latency_ms << '\n'
<< "total_detections=" << result.total_detections << '\n';
実行
PCIeホストパッケージをインストールし、チュートリアルの設定で説明されているように、チュートリアルのバンドルをダウンロードします。抽出されたPCIeエクストラルのルートから、YOLOv8sがまだ存在しない場合は、ダウンロードします。
sima-cli modelzoo get yolo_v8s
プログラムには、このディレクトリ内の正確なパスyolo_v8s_mpk.tar.gzが必要です。Model Zooが別の名前または場所を使用した場合は、ダウンロードしたアーカイブを適切な場所にコピーします。
cp /absolute/path/to/downloaded-yolov8s-archive.tar.gz yolo_v8s_mpk.tar.gz
test -f yolo_v8s_mpk.tar.gz
Pythonを実行します。
source ~/pyneatpcie/bin/activate
python3 share/sima-pcie-host/tutorials/025_run_pcie_inference_async/run_pcie_inference_async.py
事前に構築されたC++チュートリアルを実行します。
./lib/sima-pcie-host/tutorials/tutorial_025_run_pcie_inference_async
または、再構築します。
./build.sh --target tutorial_025_run_pcie_inference_async
./build/tutorials-standalone/tutorial_025_run_pcie_inference_async
正確なタイミングは、ホストとカードによって異なりますが、両方のプログラムは同じ測定境界を使用し、以下を出力します。
completed=1000
elapsed_seconds=...
throughput_fps=...
average_latency_ms=...
total_detections=...
[OK] 025_run_pcie_inference_async
チュートリアルでは、常に5つのフレームでウォームアップし、その後、1,000の完了したフレームを測定します。別のカードを使用する場合は、--card Nを渡します。
実践
送信と取得のバランスを保ちます。アプリケーションが、取得せずに画像を無期限に送信し続けると、通常のバックプレッシャーによって送信が最終的に遅くなります。専用のコンシューマーを使用すると、障害も簡単に処理できます。有限のタイムアウトにより、停止した結果が特定され、モデルを閉じることで、プロデューサーが待機している場合でも、キュー0が解放されます。
代表的なベンチマークを作成するには、繰り返しのフレームを固定された画像セットに置き換え、ディスクからの読み取りをタイミング計測の対象外にします。複数のモデルを同時に実行に進み、2つの異なるモデルを 同時に実行します。
完全なソース
完全なソースプログラムを表示
// Measure completed YOLOv8s detections with asynchronous PCIe push/pull.
//
// Usage:
// tutorial_025_run_pcie_inference_async
#include <simaai/neat/pcie/Model.h>
#include <opencv2/imgcodecs.hpp>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <cstdlib>
#include <deque>
#include <exception>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <numeric>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace pcie = simaai::neat::pcie;
namespace {
using Clock = std::chrono::steady_clock;
constexpr int kBuildTimeoutMs = 180000;
constexpr int kPullTimeoutMs = 30000;
constexpr int kWarmupFrames = 5;
constexpr int kMeasuredFrames = 1000;
constexpr char kModelPath[] = "yolo_v8s_mpk.tar.gz";
constexpr char kImagePath[] = "share/sima-pcie-host/tutorials/assets/street-scene.png";
struct Args {
int card_id = 0;
};
std::string require_value(int argc, char** argv, int& index, const char* option) {
if (index + 1 >= argc) {
throw std::runtime_error(std::string("missing value for ") + option);
}
return argv[++index];
}
Args parse_args(int argc, char** argv) {
Args args;
for (int index = 1; index < argc; ++index) {
const std::string arg = argv[index];
if (arg == "--card") {
args.card_id = std::stoi(require_value(argc, argv, index, "--card"));
} else if (arg == "-h" || arg == "--help") {
std::cout << "Usage: " << argv[0] << " [--card 0]\n";
std::exit(0);
} else {
throw std::runtime_error("unknown argument: " + arg);
}
}
return args;
}
pcie::ModelOptions detection_options() {
pcie::ModelOptions options;
options.preprocess.kind = pcie::InputKind::Image;
options.preprocess.color_convert.input_format = pcie::ColorFormat::BGR;
options.preprocess.color_convert.output_format = pcie::ColorFormat::RGB;
options.preprocess.resize.enable = pcie::AutoFlag::On;
options.preprocess.resize.mode = pcie::ResizeMode::Letterbox;
options.preprocess.normalize.preset = pcie::NormalizePreset::COCO_YOLO;
options.decode_type = pcie::BoxDecodeType::YoloV8;
options.score_threshold = 0.25F;
options.nms_iou_threshold = 0.45F;
options.top_k = 100;
return options;
}
std::uint32_t detection_count(const pcie::TensorList& outputs) {
if (outputs.size() != 1 || outputs[0].data == nullptr || outputs[0].byte_offset < 0) {
throw std::runtime_error("boxdecode must return one populated BBOX tensor");
}
const auto& tensor = outputs[0];
const auto offset = static_cast<std::size_t>(tensor.byte_offset);
if (offset > tensor.size_bytes || tensor.size_bytes - offset < sizeof(std::uint32_t)) {
throw std::runtime_error("BBOX tensor is too small");
}
std::uint32_t count = 0;
std::memcpy(&count, static_cast<const std::uint8_t*>(tensor.data) + offset, sizeof(count));
constexpr std::size_t record_size = 24;
if (count > (tensor.size_bytes - offset - 4) / record_size) {
throw std::runtime_error("BBOX detection count exceeds its payload");
}
return count;
}
struct BenchmarkResult {
std::size_t completed = 0;
double elapsed_seconds = 0.0;
double average_latency_ms = 0.0;
std::uint64_t total_detections = 0;
};
BenchmarkResult measure(pcie::Model& model, const cv::Mat& image, const int frame_count) {
std::deque<Clock::time_point> submitted;
std::mutex submitted_mutex;
std::mutex failure_mutex;
std::exception_ptr first_failure;
std::atomic<bool> cancelled = false;
std::vector<double> latency_ms;
latency_ms.reserve(static_cast<std::size_t>(frame_count));
std::uint64_t total_detections = 0;
auto fail = [&](std::exception_ptr failure) {
{
std::lock_guard<std::mutex> lock(failure_mutex);
if (!first_failure) {
first_failure = std::move(failure);
}
}
cancelled = true;
model.close();
};
const auto benchmark_start = Clock::now();
std::thread producer([&] {
try {
for (int index = 0; index < frame_count && !cancelled; ++index) {
const auto started = Clock::now();
{
std::lock_guard<std::mutex> lock(submitted_mutex);
submitted.push_back(started);
}
if (!model.push(image)) {
throw std::runtime_error("push rejected frame " + std::to_string(index));
}
}
} catch (...) {
fail(std::current_exception());
}
});
std::thread consumer([&] {
try {
for (int index = 0; index < frame_count && !cancelled; ++index) {
auto outputs = model.pull(kPullTimeoutMs);
if (!outputs) {
throw std::runtime_error("pull timed out for frame " + std::to_string(index));
}
Clock::time_point started;
{
std::lock_guard<std::mutex> lock(submitted_mutex);
if (submitted.empty()) {
throw std::runtime_error("completion arrived without a submission record");
}
started = submitted.front();
submitted.pop_front();
}
total_detections += detection_count(*outputs);
latency_ms.push_back(
std::chrono::duration<double, std::milli>(Clock::now() - started).count());
}
} catch (...) {
fail(std::current_exception());
}
});
producer.join();
consumer.join();
const auto benchmark_end = Clock::now();
if (first_failure) {
std::rethrow_exception(first_failure);
}
if (latency_ms.size() != static_cast<std::size_t>(frame_count)) {
throw std::runtime_error("not every submitted frame completed");
}
BenchmarkResult result;
result.completed = latency_ms.size();
result.elapsed_seconds = std::chrono::duration<double>(benchmark_end - benchmark_start).count();
result.average_latency_ms =
std::accumulate(latency_ms.begin(), latency_ms.end(), 0.0) / latency_ms.size();
result.total_detections = total_detections;
return result;
}
} // namespace
int main(int argc, char** argv) {
try {
const Args args = parse_args(argc, argv);
if (!std::filesystem::is_regular_file(kModelPath)) {
throw std::runtime_error(std::string("model does not exist: ") + kModelPath);
}
const cv::Mat image = cv::imread(kImagePath, cv::IMREAD_COLOR);
if (image.empty()) {
throw std::runtime_error(std::string("OpenCV could not decode: ") + kImagePath);
}
pcie::ConnectionOptions connection;
connection.card_id = args.card_id;
pcie::Model model(kModelPath, detection_options(), connection);
model.build(kBuildTimeoutMs);
for (int index = 0; index < kWarmupFrames; ++index) {
(void)detection_count(model.run(image, kPullTimeoutMs));
}
// CORE LOGIC
const BenchmarkResult result = measure(model, image, kMeasuredFrames);
std::cout << "completed=" << result.completed << '\n';
std::cout << std::fixed << std::setprecision(2) << "elapsed_seconds=" << result.elapsed_seconds
<< '\n'
<< "throughput_fps=" << result.completed / result.elapsed_seconds << '\n'
<< "average_latency_ms=" << result.average_latency_ms << '\n'
<< "total_detections=" << result.total_detections << '\n';
model.close();
std::cout << "[OK] 025_run_pcie_inference_async\n";
return 0;
} catch (const std::exception& error) {
std::cerr << "[FAIL] " << error.what() << '\n';
return 1;
}
}