Розробка застосунків за допомогою графа.
Використовуйте Model, коли вам потрібно лише завантажити та запустити один скомпільований архів моделі. Використовуйте Graph, коли ви хочете створити застосунок на основі моделей і вузлів: додавайте загальнодоступні вхідні та вихідні дані, з’єднуйте повторно використовувані фрагменти, розгалужуйте потоки, об’єднуйте потоки, перевіряйте застосунок і зберігайте або візуалізуйте те, що фактично було запущено.
Концептуальна модель навмисно проста:
| Концепція | Значення |
|---|---|
Model | Завантажений з диска скомпільований архів моделі, наприклад resnet50.tar.gz або yolov8.tar.gz. |
Node | Один етап обробки: вхідний, вихідний, трансформаційний, вихідний, кінцевий, модельний або допоміжний етап. |
Graph | Схема взаємозв’язків компонентів застосунку: які вузли/фрагменти існують і як дані передаються між ними. |
Run | Обробник, який повертає ться функцією Graph::build() під час виконання: передавання вхідних даних, отримання вихідних даних, збір метрик, зупинка. |
Коротко кажучи:
Graph = what to run
Run = the running instance
Почніть з перегляду сторінок із завданнями, коли вам потрібен коротший шлях:
- Граф навчає створенню контенту.
- Запустіть граф. навчає основам життєвого циклу середовища виконання, роботі з чергами, методам вимірювання та обчисленню пропускної здатності.
- Вузол відображає загальні вузли та групи.
- Тензор і зразок. пояснює структуру корисного навантаження та метаданих.
У більшості випадків код застосунків має використовувати загальнодоступні simaai::neat::Graph та simaai::neat::Run. Не створюйте застосунки з використанням низькорівневих просторів імен реалізації, оскільки вони не призначені для використання клієнтами як API.
Коли мені знадобиться граф?
| Мета | Рекомендований API |
|---|---|
| Запустіть одну модель для одного набору вхідних даних. | Model::run(...) або Model::build(...) |
| Визначте межі вхідних і вихідних даних для моделі застосунку. | Graph |
| Створіть модель із використанням спеціальних вузлів обробки. | Graph::add(...) |
| Використовуйте фрагмент графа в кількох застосунках. | Повернути/передати фрагмент Graph. |
| Налаштуйте декілька входів або виходів. | Назва: nodes::Input(...) / nodes::Output(...) плюс connect(...). |
| Розподіліть один потік даних між кількома споживачами. | graphs::Branch(...) |
| Об’єднайте кілька потоків в один логічний вихід. | graphs::Combine(...) з CombinePolicy |
| Збережіть або візуалізуйте виконану топологію та показники. | save_run_json(run, ...) |
Перший граф: один вхід, одна модель, один вихід.
Це найпростіший повноцінний граф, що має вигляд застосунку:
#include <neat.h>
#include <iostream>
namespace neat = simaai::neat;
int main() {
neat::Model model("resnet50.tar.gz");
neat::Graph app;
app.add(neat::nodes::Input("image"));
app.add(model);
app.add(neat::nodes::Output("classes"));
neat::Run run = app.build();
neat::Tensor image = /* create or load an image tensor */;
run.push("image", neat::TensorList{image});
std::optional<neat::Sample> result = run.pull("classes", /*timeout_ms=*/1000);
if (result) {
// Consume result->tensors, result->detections, or other Sample metadata.
}
run.stop();
}
Рядок за рядком:
nodes::Input("image")оголошує загальнодоступний вхідний порт під назвоюimage.app.add(model)додає вибраний маршрут моделі до графа.nodes::Output("classes")оголошує загальнодоступний вихідний порт під назвоюclasses.app.build()перевіряє та компілює весь граф і повертаєRun.run.push("image", ...)надсилає дані до вказаного вхідного каналу.run.pull("classes", ...)отримує дані з вказаного вихідного каналу.
Така сама форма в Python:
import pyneat
model = pyneat.Model("resnet50.tar.gz")
app = pyneat.Graph()
app.add(pyneat.nodes.input("image"))
app.add(model)
app.add(pyneat.nodes.output("classes"))
run = app.build()
image = ... # Create or load a tensor-compatible object.
run.push("image", [image])
result = run.pull("classes", timeout_ms=1000)
run.stop()
У Python функція Run.push(...) очікує послідовність, що нагадує пакет даних. Передайте [tensor] або [sample], а не окремий об’єкт тензора/зразка.
Запуск графа
Вбудований модуль Run приймає ті самі типи загальнодоступних даних, які використовуються в інших част инах Neat:
| Корисне навантаження | Використовуйте, коли |
|---|---|
TensorList | Ви передаєте тензори, тому додаткові метадані зразків не потрібні. |
Sample | Вам потрібні мітки часу, frame_id, stream_id, метадані тексту/аудіо/відео, результати виявлення або сигнал кінця потоку (EOS). |
std::vector<cv::Mat> | Вам потрібен зручний спосіб введення зображень за допомогою OpenCV. |
Найпоширеніші виклики функцій у C++:
run.push(neat::TensorList{image});
run.push("image", neat::TensorList{image});
run.push(sample);
run.push("image", sample);
auto out = run.pull(/*timeout_ms=*/1000);
auto named = run.pull("classes", /*timeout_ms=*/1000);
neat::TensorList tensors = run.pull_tensors("classes", 1000);
neat::Sample sample_out = run.pull_samples("classes", 1000);
Використовуйте pull(...), коли час очікування вичерпано або з’єднання закрито, і потрібно повернути порожній std::optional. Використовуйте pull_tensors(...) або pull_samples(...), коли вам потрібна зручна допоміжна функція з типізацією, яка генерує виняток у разі перевищення часу очікування або виникнення помилки.
Для скінченних потоків, що надходять від програми, закрийте вхідний потік і очистіть його, перш ніж збирати остаточні показники:
run.close_input();
while (auto out = run.pull("classes", 1000)) {
// Drain remaining output.
}
run.stop();
Для отримання інформації про орієнтований на виконання завдань набір інструкцій для середовища виконання, зокрема про політику черги, визначення власника вихідних даних, відключення телеметрії та багатопотокове вимірювання, див. Запустіть граф..
build() проти build(first_input)
Більшість графів можна створити без використання вхідних даних:
neat::Run run = app.build();
Використовуйте це, коли граф уже містить достатньо інформації про форму/обмеження, або коли граф володіє своїми вихідними вузлами, наприклад, вхідними даними RTSP/файлу/статичного зображення.
Під час створення, початкова збірка надає Neat перші вхідні дані:
neat::Run run = app.build(neat::TensorList{first_image});
Використовуйте це, коли перше введення має ініціювати адаптацію форми/формату перед початком потокової передачі. За замовчуванням увімкнено попередню перевірку зі збереженням початкових даних, тому Neat може один раз передати/отримати початкові дані, щоб виявити помилки на першому етапі під час збірки, замість того, щоб повернути Run, який одразу ж зазнає невдачі пізніше.
Для отримання даних про пропускну здатність, затримку та енергоспоживання зберігайте показники після фактичного виконання навантаження, а не одразу після збірки.
Назви графів не є назвами кінцевих точок.
Graph("name") — це позначка для діагностики, збережених файлів графа та візуалізації. Вона не визначає загальнодоступний вхід або вихід під назвою name.
Неправильна ментальна модель:
neat::Graph camera("image");
// This does not make run.push("image", ...) valid by itself.
Правильне оголошення кінцевої точки:
neat::Graph camera("camera_route");
camera.add(neat::nodes::Input("image"));
І для отримання результату:
neat::Graph classifier("classifier");
classifier.add(neat::nodes::Output("classes"));
Уявіть собі, що Input("image") та Output("classes") є своєрідними вхідними дверима фрагмента графа. Назва графа – це просто виві ска на будівлі.
Перевіряйте назви кінцевих точок, а не намагайтеся вгадати.
Перед збіркою перевірте логічні загальнодоступні кінцеві точки, визначені графом:
for (const auto& name : app.inputs()) {
std::cout << "graph input: " << name << "\n";
}
for (const auto& name : app.outputs()) {
std::cout << "graph output: " << name << "\n";
}
Після збірки перевірте, які саме значення приймає Run:
for (const auto& name : run.input_names()) {
std::cout << "run input: " << name << "\n";
}
for (const auto& name : run.output_names()) {
std::cout << "run output: " << name << "\n";
}
Використовуйте це для визначення маршрутів моделі та будь-яких програм із кількома вхідними та вихідними даними. Відповідність кінцевим точкам відбувається точно:
Input("image_l") може бути пов’язаний із вхідним параметром моделі під назвою image_l; Input("my_random_name") – ні.
Неназвані зручні API
Для графів з одним входом і одним виходом можна не вказувати назви кінцевих точок:
neat::Graph app;
app.add(neat::nodes::Input());
app.add(model);
app.add(neat::nodes::Output());
neat::Run run = app.build();
run.push(neat::TensorList{image});
auto result = run.pull(1000);
Це зручно для швидких скриптів і тестів. Для більш складних застосувань краще використовувати іменовані вхідні та вихідні дані.
Якщо граф має кілька можливих входів або виходів, то неіменовані операції push(...) або pull() завершуються з помилкою та
повідомляють про доступні імена. Ця помилка є навмисною: Neat не повинен намагатися вгадати, яку саме камеру, тензор або вихідний блок ви мали на увазі.
Моделі є фрагментами графа.
Модель Model можна безпосер едньо додати до графа:
neat::Model yolo("yolov8.tar.gz");
neat::Graph app;
app.add(neat::nodes::Input("image"));
app.add(yolo);
app.add(neat::nodes::Output("detections"));
Graph::add(model) додає обраний із архіву та параметрів моделі маршрут моделі. Цей маршрут може містити етапи попередньої обробки, виведення за допомогою MLA, подальшої обробки, перетворення тензорів і декодування для виявлення.
Вам не потрібно вручну викликати model.graph() для загального лінійного випадку.
Для більш складних композицій перегляньте або повторно використайте маршрут як фрагмент Graph:
neat::Graph route = yolo.graph();
auto model_inputs = route.inputs();
auto model_outputs = route.outputs();
Моделі з кількома входами
Для моделей із кількома вхідними даними не намагайтеся вгадати назви. Запитайте про маршрут:
neat::Graph route = model.graph();
for (const auto& name : route.inputs()) {
std::cout << "model expects input: " << name << "\n";
}
Потім дайте назви фрагментам, що передаються на вхід, щоб вони відповідали назвам вхідних даних моделі:
neat::Graph left_camera;
left_camera.add(neat::nodes::Input("image_l"));
neat::Graph uv_camera;
uv_camera.add(neat::nodes::Input("image_uv"));
neat::Graph app;
app.connect(left_camera, route); // Binds image_l -> model image_l.
app.connect(uv_camera, route); // Binds image_uv -> model image_uv.
Якщо left_camera оголошено як Input("a_new_name_image_l"), воно не буде пов’язане з image_l. Замість того, щоб покладатися на неявне перейменування, додайте невеликий адаптерний граф із правильною назвою кінцевої точки.
Окремі моделі графів
За замовчуванням, model.graph() повертає фрагмент моделі, який можна повторно використовувати, з відкритими іменованими кінцевими точками. Якщо ви хочете, щоб повернутий граф можна було запускати самостійно, запросіть явні загальнодоступні вхідні/вихідні вузли:
neat::Model::RouteOptions route_opt;
route_opt.include_input = true;
route_opt.include_output = true;
neat::Graph standalone = model.graph(route_opt);
neat::Run run = standalone.build();
Для розширеного використання або налагодження модель маршруту може надавати доступ до окремих фізичних вихідних даних:
route_opt.expose_all_outputs = true;
Залиште цю функцію вимкненою, якщо вам не потрібні окремі фізичні буфери виводу. За замовчуванням модель
показує логічний вивід моделі, який очікується відповідно до контракту маршруту. Якщо модель має лише
один фізичний вихід, expose_all_outputs = true все одно показує лише один вихід.
add() проти connect()
Існує два інструменти для створення композицій:
| API | Значення | Використовуйте, коли |
|---|---|---|
add(x) | Додайте або вставте в поточну лінійну послідовність. | Ви маєте на увазі «наступний етап у тому ж конвеєрі». |
connect(a, b) | З’єднайте два фрагменти графа за допомогою іменованих кінцевих точок. | Ви створюєте фрагменти, які можна повторно використовувати, або розробляєте топологію. |
connect("a", "b") | З’єднайте двома проводами дві кінцеві точк и, які вже були визначені всередині одного й того ж графа. | Ви створюєте невеликий допоміжний фрагмент. |
Лінійна композиція:
neat::Graph app;
app.add(neat::nodes::Input("image"));
app.add(model);
app.add(neat::nodes::Output("classes"));
Склад фрагментів:
neat::Graph app;
app.connect(camera, model_route);
app.connect(model_route, output_sink);
Внутрішнє підключення кінцевих точок у допоміжному фрагменті:
neat::Graph pass_through("pass_through");
pass_through.add(neat::nodes::Input("in"));
pass_through.add(neat::nodes::Output("out"));
pass_through.connect("in", "out");
Основне правило: add() означає лінійний ланцюг. connect() означає топологію графа.
Повторно використовувані фрагменти графа.
Функції можуть повертати фрагменти графа, які можна повторно використовувати:
neat::Graph make_classifier(neat::Model& model) {
neat::Graph g("classifier");
g.add(neat::nodes::Input("image"));
g.add(model);
g.add(neat::nodes::Output("classes"));
return g;
}
Використовуйте багаторазовий фрагмент лінійно:
neat::Graph classifier = make_classifier(model);
neat::Graph app;
app.add(classifier);
Або явно вкажіть фрагменти дроту:
neat::Graph app;
app.connect(camera, classifier);
app.connect(classifier, class_sink);
Якщо add() після того, як гілка стане неоднозначною, Neat не вдається і пропонує вам використовувати connect(...) натомість.
Це краще, ніж мовчки додавати зміни до неправильної гілки.
Розгалуження одного потоку
Використовуйте graphs::Branch, коли один вхідний потік має надходити до кількох іменованих вихідних потоків:
neat::Graph branch = neat::graphs::Branch("image", {"preview", "model_input"});
Значення:
image -> preview
-> model_input
Приклад:
neat::Graph camera;
camera.add(neat::nodes::Input("image"));
neat::Graph preview;
preview.add(neat::nodes::Output("preview"));
neat::Graph branch = neat::graphs::Branch("image", {"preview", "model_input"});
neat::Graph app;
app.connect(camera, branch);
app.connect(branch, preview);
Під час підключення гілки до моделі виберіть назву вихідних даних гілки, щоб вона збігалася з назвою вхідних даних моделі:
neat::Graph route = model.graph();
for (const auto& name : route.inputs()) {
std::cout << "choose a branch output matching: " << name << "\n";
}
Розгалуження є явним, оскільки воно впливає на черги та механізми регулювання потоку даних. Якщо одна з гілок працює повільно, це може призвести до уповільнення або припинення обробки даних у порівнянні з іншою гілкою, залежно від параметрів виводу та структури графа, що обробляє дані.
Python:
branch = pyneat.graphs.branch("image", ["preview", "model_input"])
Об’єднання кількох потоків.
Використовуйте graphs::Combine, коли кілька вхідних потоків мають об’єднатися в один логічний вихід:
neat::Graph pair = neat::graphs::Combine({"left", "right"},
"stereo",
neat::CombinePolicy::ByFrame);
Значення:
left --\
+--> stereo
right --/
Правила:
| Політика | Значення |
|---|---|
CombinePolicy::None | Не об’єднуйте автоматично. Якщо до одного вихідного каналу підключено кілька джерел, система має перемикатися у закритий режим у разі їх відмови. |
CombinePolicy::ByFrame | Зіставте зразки, які мають абсолютно однаковий Sample::frame_id. Якщо ідентифікатор кадру відсутній, зіставлення не відбудеться; механізм резервного копіювання PTS не передбачено. |
CombinePolicy::ByPts | Зіставте зразки, щоб час їхньої презентації Sample::pts_ns був абсолютно однаковим. Відсутність PTS призводить до помилки; механізм резервного копіювання ідентифікатора кадру не передбачено. |
Проста мова:
ByFrameозначає: «надайте мені зразки для лівого та правого каналів з однаковим номером кадру».ByPtsозначає: «надайте мені зразки з однаковим часовим штампом медіафайлу».
Приклад:
neat::Graph left;
left.add(neat::nodes::Input("left"));
neat::Graph right;
right.add(neat::nodes::Input("right"));
neat::Graph pair = neat::graphs::Combine({"left", "right"},
"stereo",
neat::CombinePolicy::ByFrame);
neat::Graph app;
app.connect(left, pair);
app.connect(right, pair);
neat::Run run = app.build();
run.push("left", left_sample_with_frame_id_42);
run.push("right", right_sample_with_frame_id_42);
auto stereo = run.pull("stereo", 1000);
Python:
pair = pyneat.graphs.combine(["left", "right"], "stereo", pyneat.CombinePolicy.ByFrame)
Якщо зразки не містять необхідного ключа, етап об’єднання завершується невдало, і замість того, щоб намагатися вгадати, виводиться діагностичне повідомлення.