본문으로 건너뛰기

첫 번째 그래프 만들기

필드값
범주그래프 및 파이프라인
난이도초급
예상 소요 시간5 minutes
레이블graph, build, run, pipeline

제1장에서는 세 줄의 코드로 모델을 실행합니다. 이러한 편리함은 모든 의미 있는 Neat 프로그램에서 직접 사용하는 두 부분으로 구성된 라이프사이클을 숨깁니다. 먼저 Graph로 파이프라인을 설명하고, 그런 다음 해당 설명을 실행 가능한 Run으로 구축합니다. 이 장에서는 가능한 가장 작은 파이프라인(하나의 입력 노드가 하나의 출력 노드에 연결되고, 그 사이에 모델이 없음)을 구성하고, 단일 프레임을 파이프라인에 통과시켜 해당 라이프사이클을 보여줍니다.

핵심은 개념적입니다. Graph는 한 번 구축하고 여러 번 실행하는 재사용 가능한 정의이며, 일회성 호출이 아닙니다. 이 장의 끝 부분에서는 그래프를 만들고, 실행 가능한 파이프라인으로 변환하고, 출력 텐서의 랭크를 읽어 프레임이 파이프라인을 통과했음을 증명합니다.

둘러보기​

입력 설명​

노드를 연결하기 전에 프레임이 어떻게 생겼는지 선언합니다. InputOptions는 이러한 계약입니다. 픽셀 format, width/height, 채널 depth 및 런타임이 각 버퍼에 타임스탬프를 찍는지 여부를 지정합니다. 이러한 옵션에서 구축된 입력 노드는 들어오는 프레임을 파이프라인에서 예상하는 모양과 비교하여 유효성을 검사합니다.

C++는 추가적으로 is_live = false를 설정하여 이를 비실시간(파일/텐서) 소스로 표시합니다.

tutorials/004_build_inference_pipeline/build_inference_pipeline.cpp
simaai::neat::InputOptions in;
in.format = "RGB";
in.width = width;
in.height = height;
in.depth = 3;
in.is_live = false;
in.do_timestamp = true;

그래프 구성​

이제 구조를 구축합니다. 새 Graph는 빈 구성 표면이며, add()는 노드를 순서대로 추가합니다. 정확히 두 개의 노드(위에서 구성한 입력 노드와 빈 출력 노드)를 추가합니다. 이것이 전체 토폴로지입니다. 프레임은 입력에서 들어오고 출력에서 나가며, 그 사이에 아무것도 없습니다. 이것이 나중에 모델 또는 전처리 단계가 삽입될 위치입니다.

노드는 simaai::neat::nodes::Input(...) 및 nodes::Output()에서 가져옵니다.

tutorials/004_build_inference_pipeline/build_inference_pipeline.cpp
simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::Input(in));
graph.add(simaai::neat::nodes::Output());

파이프라인 구축​

build()는 설명에서 실행 가능으로 전환하는 단계입니다. 추가된 노드를 구체적인 파이프라인으로 변환하고, 실제 샘플을 사용하여 입력/출력 계약의 유효성을 검사하고, 재사용 가능한 Run 핸들을 만듭니다. 대표 프레임을 전달하여 build()가 협상된 텐서 모양을 고정할 수 있도록 합니다. 다음 단계에서는 Run::run(...)을 사용하여 결정적인 방식으로 한 번에 하나의 호출을 수행합니다.

샘플 프레임은 cv::Mat이며, run_opt.output_memory = Owned는 런타임에 소유된 출력 버퍼를 반환하도록 요청합니다.

tutorials/004_build_inference_pipeline/build_inference_pipeline.cpp
auto run = graph.build(std::vector<cv::Mat>{input}, run_opt);

프레임을 실행하고 결과를 읽기​

Run을 확보한 후 run()은 하나의 프레임을 파이프라인에 통과시키고 하나의 결과를 동기적으로 가져옵니다. 모델이 없으므로 출력은 입력 계약을 반영합니다. 따라서 텐서의 랭크를 읽는 것만으로도 프레임이 전체 과정을 완료했는지 확인할 수 있습니다. 실제 파이프라인에서는 동일한 run()/push/pull 표면을 사용하여 추론을 수행합니다.

run()은 TensorList를 반환합니다. sample.front().shape.size()를 읽습니다.

tutorials/004_build_inference_pipeline/build_inference_pipeline.cpp
simaai::neat::TensorList sample = run.run(std::vector<cv::Mat>{input}, /*timeout_ms=*/1000);

실행​

실행하면 출력 텐서의 순위가 표준 출력에 표시됩니다. Neat 설치 디렉터리(share/ 및 lib/가 포함된 디렉터리)에서 Python 및 C++(미리 빌드된 버전) 명령을 실행하고, 저장소 루트에서 소스 코드를 빌드하는 명령을 실행합니다. 이 장에서는 모델 아카이브가 필요하지 않습니다.

C++ (prebuilt):

./lib/sima-neat/tutorials/tutorial_004_build_inference_pipeline \
--width 320 --height 240

C++ (build from source):

./build.sh --target tutorial_004_build_inference_pipeline
./build/tutorials-standalone/tutorial_004_build_inference_pipeline \
--width 320 --height 240

예상 결과:

tensor_rank=3
[OK] 004_build_inference_pipeline

(Python 빌드에서는 output_rank=...가 출력됩니다.) 이 장의 C++ 소스 코드를 사용자 지정 CMakeLists.txt를 사용하여 자신의 프로젝트에 통합하려면 (별도의 추가 폴더는 필요하지 않음) 랜딩 페이지의 튜토리얼 실행 방법을 참조하십시오.

실전 활용​

build/run 방식, 실행 모드, 푸시/풀 인터페이스, 그리고 RunOptions가 단일 동기 호출을 넘어서 어떻게 함께 작동하는지 설명합니다.

빌드 vs 실행​

  • Graph::build(...)는 파이프라인을 구성하고 푸시/풀 제어를 위한 Run 핸들을 반환합니다.
  • Graph::run(...)은 간편한 동기 방식입니다. 필요하면 그래프를 빌드하고, 하나의 입력을 푸시하고, 하나의 출력을 풀합니다.

동기 vs 비동기​

  • 간단한 단일 호출의 경우 Graph::run(...)을 사용합니다.
  • 재사용 가능한 실행기와 명시적인 push(...) / pull(...) 제어가 필요한 경우 Graph::build(...)를 사용합니다. 자세한 내용은 비동기 추론 실행을 참조하십시오.

푸시/풀 API​

Run은 다음을 제공합니다.

  • 입력용: push(...) / try_push(...) (cv::Mat, Tensor, 또는 Sample).
  • 출력용: pull(...), pull_tensor(...), pull_tensor_or_throw(...).

출력 메타데이터(타임스탬프, 스트림 ID)가 필요한 경우 pull()을 사용하여 Sample을 가져옵니다. 텐서 페이로드만 필요한 경우 pull_tensor()를 사용합니다.

RunOptions (간단한 API)​

일반적인 설정:

  • preset: 지연 시간/안전성 프로필(Realtime, Balanced, Reliable).
  • queue_depth: 런타임 큐 깊이.
  • overflow_policy: 큐 오버플로 동작(Block, KeepLatest, DropIncoming).
  • output_memory: 출력 소유권 정책(Auto, ZeroCopy, Owned).
  • on_input_drop: 삭제된 입력 이벤트에 대한 콜백 훅.

큐 깊이, 오버플로 및 부하 상태에서의 측정에 대해서는 처리량 및 큐 깊이 조정을 참조하십시오.

RunAdvancedOptions (고급 API)​

고급 설정은 RunOptions::advanced를 통해 선택적으로 사용할 수 있습니다.

  • advanced.max_input_bytes: 입력 버퍼 증가 제한.
  • advanced.copy_input: 방어적인 입력 복사 강제.

Run::start_measurement()를 사용하여 단일 측정 창에서 지연 시간, 처리량, 입력 카운터, 플러그인/에지 타이밍 및 선택적 보드 PMIC 전력 텔레메트리를 검사합니다.

보드 전력을 포함하려면 코드에서 활성화하고(환경 변수는 필요하지 않음) 측정 보고서에서 읽습니다.

simaai::neat::RunOptions run_opt;
run_opt.enable_board_power(); // default 100 ms sampling, auto-detects built-in profile
auto run = graph.build(inputs, run_opt);
auto scope = run.start_measurement();
run.push(inputs);
(void)run.pull_tensors(5000);
auto report = scope.stop();
run_opt = neat.RunOptions()
run_opt.enable_board_power() # default 100 ms sampling, auto-detects built-in profile
run = graph.build(tensor, run_opt)
scope = run.start_measurement()
run.push(tensor)
_ = run.pull_tensors(5000)
report = scope.stop()

Model::build(run_opt), Model::build(route_opt, run_opt) 및 Graph::build(run_opt)는 동일한 런타임 옵션을 기본 Run에 전달하므로, 파이프라인별로 중복되는 레일 샘플링 대신 그래프 수준의 보드 전력 모니터 하나를 사용합니다. 특정 기본 프로필을 강제로 적용해야 하는 경우, 보드별 헬퍼를 계속 사용할 수 있습니다: enable_modalix_som_power(), enable_modalix_dvt_power().

전체 소스​

전체 소스 프로그램 표시
tutorials/004_build_inference_pipeline/build_inference_pipeline.cpp
// Build a minimal Graph (Input -> Output), run a frame, read the tensor rank.
//
// Usage:
// tutorial_004_build_inference_pipeline [--width <w>] [--height <h>]

#include "neat.h"

#include <opencv2/core.hpp>

#include <iostream>
#include <stdexcept>
#include <string>

namespace {

bool get_arg(int argc, char** argv, const std::string& key, std::string& out) {
for (int i = 1; i + 1 < argc; ++i) {
if (key == argv[i]) {
out = argv[i + 1];
return true;
}
}
return false;
}

int parse_int_arg(int argc, char** argv, const std::string& key, int def) {
std::string value;
if (!get_arg(argc, argv, key, value))
return def;
return std::stoi(value);
}

} // namespace

int main(int argc, char** argv) {
try {
const int width = parse_int_arg(argc, argv, "--width", 320);
const int height = parse_int_arg(argc, argv, "--height", 240);

cv::Mat input(height, width, CV_8UC3, cv::Scalar(30, 60, 90));
if (!input.isContinuous())
input = input.clone();

simaai::neat::InputOptions in;
in.format = "RGB";
in.width = width;
in.height = height;
in.depth = 3;
in.is_live = false;
in.do_timestamp = true;

simaai::neat::RunOptions run_opt;
run_opt.output_memory = simaai::neat::OutputMemory::Owned;

// CORE LOGIC
// Compose a Graph from Input and Output nodes, then build+run one frame.
simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::Input(in));
graph.add(simaai::neat::nodes::Output());
auto run = graph.build(std::vector<cv::Mat>{input}, run_opt);
simaai::neat::TensorList sample = run.run(std::vector<cv::Mat>{input}, /*timeout_ms=*/1000);

if (sample.empty())
throw std::runtime_error("missing tensor output");
std::cout << "tensor_rank=" << sample.front().shape.size() << "\n";
std::cout << "[OK] 004_build_inference_pipeline\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "[FAIL] " << e.what() << "\n";
return 1;
}
}

소스​