사용자 지정 데이터 그래프 만들기
| 필드 | 값 |
|---|---|
| 범주 | 그래프 및 파이프라인 |
| 난이도 | 중급 |
| 예상 소요 시간 | 15-20 minutes |
| 레이블 | graph, traversal, metadata |
3장에서는 익명의 입력 → 출력 그래프를 구축하고 위치 정보를 사용하여 실행했습니다. run() 호출. 실제 오케스트레이션 — 팬아웃, 팬인, 스트림별 라우팅 — 은 위치가 아닌 이름으로 엔드포인트를 처리해야 합니다. 이 장에서는 가능한 가장 작은 그래프에서 이름이 지정된 엔드포인트 표면을 소개하므로, 멀티스트림 및 임베디드 모델 장에서 이를 기반으로 구축하기 전에 이름 지정 및 연결 메커니즘을 독립적으로 확인할 수 있습니다.
공개 Graph 애플리케이션 구성 표면은 다음과 같습니다. add(...) 노드, connect(...) 이름이 지정된 엔드포인트, build() 한 번 재사용 가능한 형태로 Run그런 다음 push("image", ...) 그리고 pull("out", ...) 이름으로. 결국에는 하나의 텐서를 푸시하게 될 것입니다. Sample 이름이 지정된 그래프를 통해 확인한 결과 stream_id, frame_id그리고 pts_ns 변경되지 않은 채로 출력됨 — 런 타임이 시작부터 끝까지 메타데이터를 보존한다는 증거입니다.
둘러보기
그래프 구성
노드를 두 개 추가합니다. Input("image") 이름이 지정된 푸시 엔드포인트를 선언합니다. image; Output("out") 이름이 지정된 풀 엔드포인트를 선언합니다. out이름은 계약 조건입니다. 이는 실제로 전달할 문자열입니다. push(...) 그리고 pull(...) 나중에. 엔드포인트에 이름을 지정하는 방식(추가 순서에 의존하는 대신)은 여러 입력 또는 출력을 가진 더 큰 그래프를 명확하게 제어할 수 있도록 합니다.
노드는 다음에서 생성됩니다. simaai::neat::nodes::Input("image") 그리고 nodes::Output("out").
// `Graph` is the public composition type. Input("image") declares the name
// used by Run::push("image", ...). Output("out") declares the name used by
// Run::pull("out", ...).
simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::Input("image"));
graph.add(simaai::neat::nodes::Output("out"));
엔드포인트를 연결합니다.
connect("image", "out") 경계를 선언합니다: 프레임이 밀려난 곳은 image 흐름이 향하는 곳 out두 개의 노드만으로 전체 토폴로지를 구성할 수 있지만 connect(...) 더 큰 그래프에서 분기와 병합을 생성하는 데 사용하는 것과 동일한 함수입니다. 그런 다음 다음을 출력합니다. graph.describe() 구성된 토폴로지를 내보내고, 그래프가 의도한 대로 연결되었는지 빠르게 확인합니다. 그런 다음 빌드합니다.
graph.connect("image", "out");
std::cout << graph.describe() << "\n";
샘플 빌드 및 푸시
build() (여기서는 초기 샘플이 필요하지 않으므로) 설명을 실행 가능한 형태로 구현합니다. Run. 그런 다음 하나의 결정론적 텐서를 구성합니다. Sample — 알려진 값을 담고 있는 8×8×3 RGB 이미지 stream_id, frame_id그리고 pts_ns — 그리고 push(...) 해당 항목을 image 이름으로 엔드포인트를 지정합니다. 샘플의 메타데이터는 반대쪽에서 확인할 내용입니다.
push(...) bool 값을 반환합니다. 실패 시 오류를 표시합니다. run.last_error()샘플은 다음과 같이 구성됩니다. make_sample().
simaai::neat::Run run = graph.build();
if (!run.push("image", make_sample())) {
throw std::runtime_error("push failed: " + run.last_error());
}
결과를 가져와서 메타데이터를 확인합니다.
pull("out", ...) 지정된 출력 엔드포인트에서 결과를 가져오고, 지정된 시간 제한이 지나면 close() 실행 결과입니다. 입력과 출력 사이에 변환 과정이 없으므로 올바른 파이프라인은 동일한 논리적 샘플을 반환합니다. 따라서 다시 읽어보면 stream_id, frame_id그리고 pts_ns 그리고 우리가 전송한 값을 확인하면 런타임이 반복 작업을 통해 샘플별 메타데이터를 보존한다는 것을 알 수 있습니다. 이러한 보장은 후속 단계에서 프레임 식별 정보와 타임스탬프를 신뢰할 수 있도록 합니다.
auto out = run.pull("out", /*timeout_ms=*/2000);
run.close();
실행
실행하면 그래프 설명과 그 뒤에 왕복 전송된 메타데이터가 표시됩니다. Python 및 C++(미리 빌드된) 명령을 실행합니다.Neat root 디렉터리(해당 디렉터리에 포함된)를 설치합니다. share/ 그리고 lib/); 소스 코드를 기반으로 빌드하는 명령어를 저장소의 루트 디렉터리에서 실행합니다. 이 장에서는 모델 아카이브가 필요하지 않습니다.
C++ (prebuilt):
./lib/sima-neat/tutorials/tutorial_013_build_a_custom_data_graph
C++ (build from source):
./build.sh --target tutorial_013_build_a_custom_data_graph
./build/tutorials-standalone/tutorial_013_build_a_custom_data_graph
예상 출력 (다음과 같음) graph.describe() 덤프:
stream=graph frame=42 pts_ns=123456789
[OK] 013_build_a_custom_data_graph
(Python 빌드에서는 stream_id=graph frame_id=42 pts_ns=123456789가 출력됩니다.) 이 장의 C++ 소스 코드를 사용자 지정 CMakeLists.txt를 사용하여 자신의 프로젝트에 통합하려면(추가 폴더는 필요하지 않음), 랜딩 페이지의 튜토리얼 실행 방법을 참조하십시오.
전체 소스
전체 소스 프로그램 표시
// Compose a minimal public Neat Graph: named Input -> named Output.
//
// Usage:
// tutorial_013_build_a_custom_data_graph
#include "neat.h"
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <utility>
#include <vector>
namespace {
std::vector<int64_t> contiguous_strides_bytes(const std::vector<int64_t>& shape,
int64_t elem_bytes) {
std::vector<int64_t> strides(shape.size(), 0);
int64_t stride = elem_bytes;
for (int i = static_cast<int>(shape.size()) - 1; i >= 0; --i) {
strides[static_cast<size_t>(i)] = stride;
stride *= shape[static_cast<size_t>(i)];
}
return strides;
}
simaai::neat::Sample make_sample() {
const int w = 8;
const int h = 8;
const int c = 3;
const std::size_t bytes = static_cast<std::size_t>(w) * h * c;
simaai::neat::Tensor t;
t.device = {simaai::neat::DeviceType::CPU, 0};
t.dtype = simaai::neat::TensorDType::UInt8;
t.layout = simaai::neat::TensorLayout::HWC;
t.shape = {h, w, c};
t.semantic.image = simaai::neat::ImageSpec{simaai::neat::ImageSpec::PixelFormat::RGB, ""};
t.storage = simaai::neat::make_cpu_owned_storage(bytes);
t.strides_bytes = contiguous_strides_bytes(t.shape, 1);
t.read_only = false;
{
auto map = t.map(simaai::neat::MapMode::Write);
auto* p = static_cast<std::uint8_t*>(map.data);
for (std::size_t i = 0; i < bytes; ++i)
p[i] = static_cast<std::uint8_t>(i % 255);
}
t.read_only = true;
simaai::neat::Sample s;
s.kind = simaai::neat::SampleKind::Tensor;
s.tensor = std::move(t);
s.stream_id = "graph";
s.frame_id = 42;
s.pts_ns = 123456789;
return s;
}
} // namespace
int main() {
try {
// CORE LOGIC
// `Graph` is the public composition type. Input("image") declares the name
// used by Run::push("image", ...). Output("out") declares the name used by
// Run::pull("out", ...).
simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::Input("image"));
graph.add(simaai::neat::nodes::Output("out"));
graph.connect("image", "out");
std::cout << graph.describe() << "\n";
simaai::neat::Run run = graph.build();
if (!run.push("image", make_sample())) {
throw std::runtime_error("push failed: " + run.last_error());
}
auto out = run.pull("out", /*timeout_ms=*/2000);
run.close();
if (!out.has_value())
throw std::runtime_error("graph produced no output");
std::cout << "stream=" << out->stream_id << " frame=" << out->frame_id
<< " pts_ns=" << out->pts_ns << "\n";
std::cout << "[OK] 013_build_a_custom_data_graph\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "[FAIL] " << e.what() << "\n";
return 1;
}
}