본문으로 건너뛰기

첫 번째 그래프 만들기

필드
범주그래프 및 파이프라인
난이도초급
예상 소요 시간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/가 포함된 디렉터리)에서 PythonC++(미리 빌드된 버전) 명령을 실행하고, 저장소 루트에서 소스 코드를 빌드하는 명령을 실행합니다. 이 장에서는 모델 아카이브가 필요하지 않습니다.

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;
}
}

소스