PCIe 공동 처리
Neat PCIe 호스트 API를 사용하면 호스트 시스템의 애플리케이션이 텐서 또는 이미지를 연결된 Modalix PCIe 카드에 전송하고 추론 결과를 받을 수 있습니다. 호스트 시스템이 애플리케이션 I/O 및 오케스트레이션을 담당하고 카드가 컴파일된 모델과 구성된 전처리 또는 후처리를 실행할 때 사용합니다.
이는 DevKit에서 직접 실행되는 Neat Library와는 별개의 API입니다. 공개 유형은 simaai::neat::pcie C++ 네임스페이스와 pyneatpcie Python 패키지에 있습니다.
호스트 시스템에 core/pciehost를 설치합니다. Neat SDK 컨테이너 내부나 Modalix PCIe 카드에 설치하지 마십시오. 이 페이지를 사용하기 전에 PCIe 호스트를 설치합니다. 지침을 따르십시오.
공동 처리 방식
하나의 pcie::Model은 하나의 물리적 PCIe 큐에서 실행되는 하나의 컴파일된 모델을 나타냅니다.
- 생성자는 로컬 모델 아카이브를 읽고 해당 입력 및 출력 계약을 노출합니다.
build()는 아카이브를 PCIe 가상 네트워크를 통해 카드에 업로드하고, 카드 측 파이프라인을 시작하 고, 준비될 때까지 기다립니다.run()또는push()는 PCIe를 통해 입력 페이로드를 보냅니다.- 카드는 사전 처리, 추론 및 구성된 후처리를 실행합니다.
run()또는pull()는 출력 텐서를 호스트로 반환합니다.close()는 카드 측 파이프라인을 중지하고 큐를 해제합니다.
모델 아카이브는 build() 중에 전송됩니다. 추론 페이로드 및 결과는 PCIe 데이터 전송을 사용합니다.
연결 구성
ConnectionOptions는 이 모델에서 사용하는 카드와 큐를 식별합니다.
| 필드 | 기본값 | 목적 |
|---|---|---|
card_host | 비어 있음 | 명시적 SSH/SCP 주소입니다. 비어 있는 경우 카드 N은 10.0.N.2를 사용합니다. |
card_id | 0 | 호스트 PCIe 플러그인에 전달되는 카드 번호입니다. |
user | sima | 카드 측 SSH 및 SCP에 대한 사용자입니다. |
queue | 0 | 0에서 3까지의 공동 처리 큐입니다. |
max_inflight | 10 | 결과를 기다리는 최대 허용 입력 수입니다. |
큐 0의 10.0.0.2에 있는 하나의 카드에 대해 기본값을 사용합니다. 카드가 다른 관리 주소를 사용하는 경우 card_host를 명시적 으로 설정합니다.
#include <simaai/neat/pcie/Model.h>
namespace pcie = simaai::neat::pcie;
pcie::ConnectionOptions connection;
connection.card_host = "10.0.0.2";
connection.card_id = 0;
connection.queue = 0;
connection.max_inflight = 10;
모델 검사 및 빌드
구성은 로컬에서 이루어지며, 카드를 시작하지 않습니다. 입력을 할당하기 전에 info()를 검사한 다음, 공동 처리 세션을 시작하기 위해 build()를 한 번 호출합니다.
pcie::Model model("model.tar.gz", {}, connection);
const pcie::ModelInfo info = model.info();
for (const auto& input : info.inputs) {
std::cout << input.name << " requires " << input.size_bytes << " bytes\n";
}
model.build(/*readiness_timeout_ms=*/180000);
input_specs()와 output_specs()는 각각 동일한 목록을 반환합니다.
running()은 성공적인 빌드 후에 true로 변경되고, close() 후에 false로 돌아갑니다.
이는 수명 주기 상태만 보고하며 호스트 전송 또는 원격 파이프라인 상태를 확인하지 않습니다.
동기 추론 실행
가장 간단한 요청/응답 흐름을 위해 run()을 사용합니다. 먼저 모델을 빌드하고, 애플리케이션 오류로 인해 무한정 대기하지 않도록 유한한 시간 제한을 설정합니다.
다음 예제는 보고된 입력 데이터 유형이 FP32인 모델에 대한 입력을 생성합니다.
const auto& input_spec = info.inputs.front();
if (input_spec.dtype != "FP32") {
throw std::runtime_error("this example requires an FP32 model input");
}
std::vector<float> values(input_spec.size_bytes / sizeof(float), 0.0f);
pcie::Tensor input = pcie::Tensor::from_vector(
std::move(values), input_spec.shape, input_spec.name);
pcie::TensorList outputs = model.run(input, /*timeout_ms=*/30000);
model.close();
다중 입력 모델의 경우, 논리적 입력마다 하나의 Tensor를 순서대로 전달하고, info().inputs에서 보고하는 경로 이름을 사용합니다.
run() 시간 초과 시 대기는 중단되지만, 이미 카드가 수락한 입력은 취소되지 않습니다. 시간 초과가 발생한 후에는 pull()을 사용하여 해당 대기 중인 결과를 처리하거나, 새 요청 시퀀스를 시작하기 전에 close()를 호출합니다.
푸시 및 풀을 사용하는 파이프라인 요청
입력 준비와 추론이 동시에 진행되어야 하는 경우 push() 및 pull()을 사용합니다. max_inflight는 아직 반환되지 않은 수락된 작업의 양을 제한합니다. 결과를 즉시 가져와서 프로듀서가 계속 작업을 진행할 수 있도록 합니다.
std::size_t pushed = 0;
std::size_t pulled = 0;
while (pulled < inputs.size()) {
while (pushed < inputs.size() && pushed - pulled < 10) {
model.push(inputs[pushed++]);
}
auto outputs = model.pull(/*timeout_ms=*/30000);
if (!outputs) {
throw std::runtime_error("PCIe inference timed out");
}
consume(*outputs);
++pulled;
}
push()는 max_inflight가 가득 찰 때까지 대기하므로, 결과를 가져오지 않고 구성된 윈도우보다 많은 데이터를 제출하지 마십시오. pull()은 이 모델에 사용할 수 있는 다음 결과를 반환합니다. run()을 호출하기 전에 push()로 제출된 모든 결과를 처리하십시오.
이미지 전송 및 전처리 구성
디코딩된 이미지 데이터를 전송할 때 preprocess.kind를 Image로 설정합니다. 카드 측의 Neat 파이프라인은 지원되는 객체 감지 출력의 크기를 조정하고, 색상을 변환하고, 정규화하고, 디코딩할 수 있습니다.
이 예제에서는 BGR 이미지를 전송하고, 모델 아카이브에서 추론된 모델 입력에 맞게 이미지의 비율을 조정한 다음, 디코딩된 YOLOv8 BBOX 페이로드를 포함하는 텐서를 반환합니다.
#include <opencv2/imgcodecs.hpp>
pcie::ModelOptions options;
options.preprocess.kind = pcie::InputKind::Image;
options.preprocess.color_convert.input_format = pcie::ColorFormat::BGR;
options.preprocess.resize.enable = pcie::AutoFlag::On;
options.preprocess.resize.mode = pcie::ResizeMode::Letterbox;
options.decode_type = pcie::BoxDecodeType::YoloV8;
options.score_threshold = 0.25f;
options.nms_iou_threshold = 0.45f;
options.top_k = 100;
pcie::Model detector("yolo_v8n_mpk.tar.gz", options, connection);
detector.build();
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_COLOR);
pcie::TensorList detections = detector.run(image, /*timeout_ms=*/30000);
detector.close();
input_max_width, input_max_height 또는 input_max_depth를 설정하지 마세요.
입력값이 없는 모델의 경우, 애플리케이션에서 명시적인 입력 제한이 필요한 경우가 아니라면 설정할 필요가 없습니다. Neat은 모델 아카이브에서 모델 측의 크기 조정 대상을 추론할 수 있습니다.
MLA만 실행하기
기본적으로 카드는 FP32 입력을 MLA의 dtype으로 변환하고, MLA를 실행한 뒤, 결과를 다시 변환하여 FP32로 반환합니다. mla_only는 그 dtype 변환 경계를 애플리케이션으로 옮깁니다. 그러면 카드는 MLA만 실행하고, 호스트는 info().inputs의 각 항목에 맞는 텐서를 제출하며, MLA 고유의 출력을 논리적 형상으로 받습니다.
info()는 그 계약을 설명합니다. INT8 텐서는 하나의 scale과 하나의 zero_point를 가진 quant를 가집니다:
x = (q - zero_point) * scale
q = clamp(round(x / scale) + zero_point, -128, 127)
BF16 텐서는 quant를 가지지 않습니다. 호스트는 입력 시 FP32를 최근접 짝수 반올림으로 BF16으로 반올림하고, 출력 시 BF16 헤드를 확장합니다. Python에서 BF16은 uint16 비트 패턴으로 Tensor.from_bytes(..., pcie.TensorDType.BFloat16, ...)와 to_numpy()를 거칩니다.
아래 예제는 INT8의 경우입니다.
pcie::ModelOptions options;
options.mla_only = true;
pcie::Model model("model_mlatess_int8.tar.gz", options, connection);
const auto& input_spec = model.info().inputs.front();
const pcie::QuantParams& quant = *input_spec.quant; // INT8 ingress
std::vector<std::int8_t> codes(input_spec.size_bytes);
for (std::size_t i = 0; i < codes.size(); ++i) {
const float x = /* value in the model's input domain, e.g. [0, 1] */;
const float q = std::nearbyint(x / quant.scale) + quant.zero_point;
codes[i] = static_cast<std::int8_t>(std::clamp(q, -128.0f, 127.0f));
}
model.build();
pcie::TensorList outputs = model.run(
pcie::Tensor::from_vector(std::move(codes), input_spec.shape, input_spec.name),
/*timeout_ms=*/30000);
const auto& head = model.info().outputs.front();
const std::int8_t* q = reinterpret_cast<const std::int8_t*>(
static_cast<const std::uint8_t*>(outputs[0].data) + outputs[0].byte_offset);
const float x0 = (q[0] - head.quant->zero_point) * head.quant->scale;
model.close();
이 경로는 전부 아니면 전무입니다. 제출하는 각 텐서는 해당 info().inputs 항목의 dtype, 형상, 바이트 크기와 그 순서대로 일치해야 합니다. 다른 dtype의 페이로드는 카드에서 변환되는 대신 거부됩니다. mla_only는 이미지 전처리나 박스 디코드와 결합할 수 없습니다. 생성자는 MLA 단계가 직접 MLA 입출력용으로 컴파일되지 않은 아카이브, 예를 들어 CVU에서 테셀레이션을 수행하는 Model Zoo의 yolo_v8s 빌드를 거부합니다(mla_only does not support stage '...' (tess)).
튜토리얼 INT8 텐서로 MLA만 실행하기는 INT8의 경우를 보여 줍니다. 호스트에서 이미지를 양자화하고 역양자화된 헤드를 기본 경로와 비교 검증합니다.
안정적으로 닫기
모델이 더 이상 필요하지 않을 때와 큐를 재사용하기 전에 close()를 호출하세요. 여러 번 호출해도 안전합니다.
model = pcie.Model("model.tar.gz", connection=connection)
model.build()
outputs = model.run([input_tensor], timeout_ms=30000)
model.close()
또는 컨텍스트 관리자를 사용하여 모델을 자동으로 닫을 수 있습니다.
with pcie.Model("model.tar.gz", connection=connection) as model:
model.build()
outputs = model.run([input_tensor], timeout_ms=30000)
컨텍스트 관리자는 블록이 종료될 때, 예외가 발생할 때를 포함하여 close()를 호출합니다. with 블록 내에 다른 명시적인 close()를 추가하지 마십시오.
C++ 호스트 애플리케이션 빌드
개발 패키지는 SimaPCIeHost CMake 패키지를 제공합니다.
cmake_minimum_required(VERSION 3.16)
project(pcie_model LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(SimaPCIeHost REQUIRED CONFIG)
add_executable(pcie_model main.cpp)
target_link_libraries(pcie_model PRIVATE SimaPCIeHost::sima_neat_pcie_host)
해당 호스트 머신에서 이 애플리케이션을 네이티브 방식으로 빌드합니다.
C++ 이미지 예제에서도 OpenCV를 사용합니다. 해당 애플리케이션 대상에 헤더 파일과 라이브러리를 추가합니다.
find_package(OpenCV REQUIRED COMPONENTS core imgcodecs)
target_include_directories(pcie_model PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(pcie_model PRIVATE ${OpenCV_LIBS})
현재 범위 및 제한 사항
- 하나의
pcie::Model은 하나의 PCIe 큐를 소유합니다. 큐 범위는0에서3까지입니다. - Modalix EV74는 최대 네 개의 동시 코프로세싱 파이프라인을 지원합니다.
- 동일한 큐에 두 개의 활성 모델을 할당하지 마십시오.
- 카드에 설치된 호스트 패키지와 Neat Library는 호환되는 버전이어야 합니다.
- 첫 번째 페이로드가 제출된 후 입력 미디어 유형과 형식을 안정적으로 유지하십시오. 활성 전송 용량보다 큰 후속 페이로드는 거부됩니다.
- PCIe 호스트 API는 Neat 모델의 전처리 및 객체 디코딩 옵션의 제한된 하위 집합을 지원합니다.
mla_only에는 직접 MLA 입출력용으로 컴파일된 아카이브가 필요합니다. 각 입력은 해당info().inputs항목의 dtype으로 제출되며, 헤드는 MLA의 dtype으로 연속되게 반환됩니다.- 이 코프로세싱 API는 호스트 측
Graph,Node또는Run구성을 노출하지 않습니다. 네이티브 애플리케이션 그래프를 위해 DevKit에서 일반 Neat Library를 사용하십시오.
설치 및 연결 확인을 위해 PCIe 호스트를 설치합니다.로 돌아가십시오.