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

Створіть свій перший граф

ПолеЗначення
КатегоріяГрафи та конвеєри
СкладністьПочатковий
Орієнтовний час читання5 minutes
Міткиgraph, build, run, pipeline

Розділ 001 запустив модель у три рядки. Ця зручність приховує двокомпонентний життєвий цикл, який кожна нетривіальна програма 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);

Запуск

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

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, режими виконання, поверхня push/pull і RunOptions взаємодіють, коли ви переходите до кількох синхронних викликів.

Побудова проти виконання

  • Graph::build(...) створює конвеєр і повертає об’єкт Run для керування push/pull.
  • Graph::run(...) — це зручний синхронний метод: він створює (за потреби) граф, передає один вхідний сигнал і отримує один вихідний сигнал.

Синхронний проти асинхронного

  • Використовуйте Graph::run(...) для простого одноразового виклику.
  • Використовуйте Graph::build(...), коли вам потрібен повторно використовуваний засіб виконання та явне керування push(...) / pull(...) — див. Асинхронне виконання висновків.

API push/pull

Run надає доступ до:

  • push(...) / try_push(...) для вхідних даних (cv::Mat, Tensor або Sample).
  • pull(...), pull_tensor(...), pull_tensor_or_throw(...) для вихідних даних.

Якщо вам потрібні метадані вихідних даних (мітки часу, ідентифікатори потоків), використовуйте pull(), щоб отримати Sample. Якщо вам потрібен лише корисний вміст тензора, використовуйте pull_tensor().

Параметри виконання (простий API)

Загальні параметри:

  • preset: профіль затримки/безпеки (Realtime, Balanced, Reliable).
  • queue_depth: глибина черги під час виконання.
  • overflow_policy: поведінка черги у разі переповнення (Block, KeepLatest, DropIncoming).
  • output_memory: політика володіння вихідними даними (Auto, ZeroCopy, Owned).
  • on_input_drop: функція зворотного виклику для обробки подій відхилення вхідних даних.

Щоб дізнатися про глибину черги, переповнення та вимірювання під навантаженням, див. Налаштування пропускної здатності та глибини черги.

Розширені параметри запуску (для досвідчених користувачів 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;
}
}

Джерело