본문으로 건너뛰기

PCIe 비동기 추론 실행

필드
범주PCIe 코프로세싱
난이도초급
예상 소요 시간15 minutes
레이블PCIe, asynchronous, throughput, detection

이 튜토리얼에서는 튜토리얼 024에서 사용한 YOLOv8s 이미지와 박스 디코딩 구성, 640x480 해상도의 거리 장면 이미지를 재사용합니다. 저장 및 이미지 디코딩이 PCIe 측정에 영향을 미치지 않도록 동일한 이미지를 반복적으로 제출합니다.

둘러보기

하나의 감지 모델 구성

이미지를 한 번 로드하고, 카드 측 COCO 전처리 및 YOLOv8 박스 디코딩을 구성한 다음, 큐 0에 하나의 Model을 빌드합니다. 누락된 파일이나 카드 시작 오류가 발생하면 측정 시작 전에 프로그램이 중지됩니다.

pcie_host/tutorials/025_run_pcie_inference_async/run_pcie_inference_async.cpp
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);

파이프라인 워밍업

시간을 측정하지 않고 몇 개의 전체 감지를 실행합니다. 워밍업은 모델 시작 및 첫 번째 버퍼 효과를 보고된 워크로드에서 제거합니다.

pcie_host/tutorials/025_run_pcie_inference_async/run_pcie_inference_async.cpp
for (int index = 0; index < kWarmupFrames; ++index) {
(void)detection_count(model.run(image, kPullTimeoutMs));
}

동시 제출 및 검색

하나의 스레드는 push()를 사용하여 이미지를 제출하고, 다른 스레드는 유한한 시간 제한이 있는 pull()을 사용하여 BBOX 출력을 검색합니다. 작은 애플리케이션 전용 FIFO는 각 순서대로 제출된 이미지의 시작 시간을 저장합니다. 거부, 시간 초과 또는 잘못된 결과가 발생하면 모델이 닫히고 다른 스레드가 깨어납니다.

이 예제는 일반적인 Model 흐름 제어 동작에만 의존하며, 애플리케이션에는 큐 깊이 조정이 없습니다.

pcie_host/tutorials/025_run_pcie_inference_async/run_pcie_inference_async.cpp
const BenchmarkResult result = measure(model, image, kMeasuredFrames);

완료된 작업 보고

두 스레드가 모두 완료되고 모든 수락된 이미지가 검색된 후에만 시간 측정을 중지합니다. 초당 프레임 수는 완료된 출력의 수를 사용합니다. 평균 대기 시간은 각 제출 시도부터 해당 순서대로 정렬된 결과가 도착할 때까지의 시간을 측정합니다.

pcie_host/tutorials/025_run_pcie_inference_async/run_pcie_inference_async.cpp
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

튜토리얼은 항상 다섯 개의 프레임으로 워밍업한 다음 1,000개의 완료된 프레임을 측정합니다. 다른 카드를 사용하는 경우에만 --card N을 전달합니다.

실전 활용

제출 및 검색을 균형 있게 유지합니다. 애플리케이션이 중단 없이 계속 이미지를 제출하는 경우, 일반적인 역압력이 결국 제출 속도를 늦춥니다. 전용 컨슈머를 사용하면 오류를 쉽게 처리할 수 있습니다. 유한한 시간 제한은 중단된 결과를 식별하고, 모델을 닫으면 프로듀서가 대기 중인 경우에도 큐 0이 해제됩니다.

대표적인 벤치마크를 위해 반복되는 프레임을 고정된 이미지 세트로 바꾸고 디스크 읽기를 시간 측정 영역 외부로 이동합니다. 여러 모델을 실행합니다.를 사용하여 두 개의 다른 모델을 동시에 실행합니다.

전체 소스

전체 소스 프로그램 표시
pcie_host/tutorials/025_run_pcie_inference_async/run_pcie_inference_async.cpp
// 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;
}
}

소스