Перейти до основного вмісту

Створення власного графа даних

ПолеЗначення
КатегоріяГрафи та конвеєри
СкладністьСередній
Орієнтовний час читання15-20 minutes
Міткиgraph, traversal, metadata

Розділ 003 створив анонімний граф «Вхід → Вихід» і запустив його за допомогою позиційних викликів run(). Справжня оркестрація — розгалуження, зведення, маршрутизація для кожного потоку — потребує звернення до кінцевих точок за назвою, а не за позицією. У цьому розділі представлено цю поверхню іменованих кінцевих точок на найменшому можливому графі, щоб ви могли побачити механізми іменування та з’єднання окремо, перш ніж розділи про багатопотоковість і вбудовані моделі будуть їх розвивати.

Публічний Graph є поверхнею композиції застосунку: ви add(...) вузли, connect(...) іменовані кінцеві точки, build() один раз, щоб створити багаторазовий Run, а потім push("image", ...) і pull("out", ...) за назвою. В результаті ви передасте один тензор Sample через іменований граф і підтвердите, що його stream_id, frame_id і pts_ns залишилися незмінними — це доказ того, що середовище виконання зберігає метадані від початку до кінця.

Покроковий огляд

Складіть граф

Додайте два вузли. Input("image") оголошує точку надсилання (push endpoint) під назвою image; Output("out") оголошує точку отримання (pull endpoint) під назвою out. Назви є частиною контракту — це саме ті рядки, які ви передаватимете до push(...) і pull(...) пізніше. Надання імен точкам (замість покладання на порядок додавання) дозволяє створювати більші графи з кількома входами або виходами, що робить їх однозначними для керування.

Вузли надходять з simaai::neat::nodes::Input("image") і nodes::Output("out").

tutorials/013_build_a_custom_data_graph/build_a_custom_data_graph.cpp
// `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(), щоб вивести створену топологію — це швидка перевірка, щоб переконатися, що граф з’єднано так, як ви й задумали, перш ніж його будувати.

tutorials/013_build_a_custom_data_graph/build_a_custom_data_graph.cpp
graph.connect("image", "out");

std::cout << graph.describe() << "\n";

Створіть і надішліть приклад

build() (у цьому випадку не потрібен початковий зразок) реалізує опис у вигляді виконуваного Run. Потім ми створюємо один детермінований тензор Sample — RGB-зображення розміром 8×8×3, що містить відомі stream_id, frame_id та pts_ns — і передаємо його до кінцевої точки image за назвою за допомогою push(...). Метадані зразка — це те, що ми перевіримо з іншого боку.

push(...) повертає булеве значення; у разі збою ми передаємо run.last_error(). Зразок створюється за допомогою make_sample().

tutorials/013_build_a_custom_data_graph/build_a_custom_data_graph.cpp
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 і перевірка відповідності значень тим, які ми передали, підтверджує, що середовище виконання зберегло метадані для кожного зразка під час обробки. Ця гарантія дозволяє наступним етапам довіряти ідентифікації кадрів і міткам часу.

tutorials/013_build_a_custom_data_graph/build_a_custom_data_graph.cpp
auto out = run.pull("out", /*timeout_ms=*/2000);
run.close();

Запуск

Запустіть його, і ви повинні побачити опис графа, за яким слідуватимуть метадані, отримані в результаті обміну даними. Запустіть команди Python і C++ (попередньо скомпільовані) з Neat встановити в кореневу директорію **(директорію, яка містить) share/ і lib/); виконайте команди build from source з кореневої директорії репозиторію. У цьому розділі не потрібен архів моделі.

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 (додаткова тека не потрібна), див. Як запускати навчальні матеріали на цільовій сторінці.

Повний початковий код

Показати повні програми
tutorials/013_build_a_custom_data_graph/build_a_custom_data_graph.cpp
// 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;
}
}

Джерело