Граф
У SiMa.ai Neat Graph API використовується для створення застосунку. Graph описує потік даних від вхідних даних, через вузли обробки, до вихідних даних. Якщо ви маєте досвід у машинному навчанні, уявляйте Graph як невеликий граф моделі для застосунку, що використовує вашу модель.
- Кадри, тензори або зразки надходять через вхідні дані та виходять через вихідні дані. Обробка відбувається посередині за допомогою вузлів, таких як декодування, зміна розміру, попередня обробка, виконання, подальша обробка, розгалуження та власна логіка.
- Графи можуть виконуватися як окремі одиниці або використовуватися повторно всередині більшого графа.
Neat дозволяє описувати застосунок замість ручного налаштування середовища виконання. Використовуйте попередньо створені вузли та групи вузлів для типових завдань, таких як обробка вхідних даних, декодування, зміна розміру, попередня обробка, виведення результатів, подальша обробка та виведення. У Graph ви визначаєте вузли, встановлюєте їхні параметри та з’єднуєте їх у порядку, необхідному для вашого застосунку.
Під капотом Neat створює виконуваний граф середовища виконання на основі GStreamer. Neat абстрагує цю реалізацію, тому ви використовуєте загальнодоступний Graph API замість керування елементами GStreamer, appsrc, appsink, чергами або внутрішніми портами середовища виконання.
Вузли, групи та межі.
Graph – це межа збірки; Node – складова частина.
Це включає:
- атомні вузли, такі як етапи декодування, попередньої обробки, подальшої обробки, отримання даних і їх передачі;
- попередньо створені групи вузлів, які є наборами вузлів, що можна повторно використовувати;
- граничні вузли, такі як
Input("image")іOutput("classes").
Детальні правила щодо попередньо створених груп і граничних вузлів наведено в Вузол → Попередньо створені групи вузлів і Вузол → Межові вузли.
Коли ви викликаєте Graph::build(), Neat перетворює загальнодоступний граф на єдиний граф, придатний для виконання, зберігаючи імена кінцевих точок для діагностики та іменовані API Run.
Надання імен вхідним і вихідним даним
Імена, вказані у вузлах Input та Output, визначають кінцеві точки межі фрагмента графа. Межі, які залишаються за межами остаточно зібраного Graph, стають загальнодоступними кінцевими точками в середовищі виконання:
simaai::neat::Graph classifier("classifier");
classifier.add(simaai::neat::nodes::Input("image"));
classifier.add(model);
classifier.add(simaai::neat::nodes::Output("classes"));
Тут image є вхідною точкою, а classes – вихідною точкою. Назва графа, classifier, є лише позначкою для діагностики та візуалізації; вона не створює вхідні або вихідні дані.
Перевірте кінцеві точки перед збіркою.
Виведіть список публічних кінцевих точок перед тим, як створювати граф. Не повинно бути жодних неактивних кінцевих точок. Якщо назва відсутня в цьому списку, Run не прийме її пізніше.
for (const auto& name : classifier.inputs()) {
std::cout << "graph input: " << name << "\n";
}
for (const auto& name : classifier.outputs()) {
std::cout << "graph output: " << name << "\n";
}
Після створення graph.build(...), перевірте run.input_names() та run.output_names() у робочому режимі Run. Назви мають відповідати публічному інтерфейсу, який ви планували зробити доступним.
Оберіть топологію, яка визначається вихідним кодом або застосунком.
Кожен граф має відповідати на одне запитання: кому належать вхідні дані?
| Топологія | Використовуйте його, коли | Форма під час виконання (у середовищі виконання). |
|---|---|---|
| Надіслано через застосунок. | У вашому застосунку вже є кадри, тензори або зразки. | Додайте nodes.input("name"), потім запустіть за допомогою run.push("name", ...) або використайте Graph.run([...]). |
| Належить джерелу. | Граф має зчитувати дані з файлу, камери, протоколу RTSP або іншого джерела. | Додайте вихідний вузол або вихідну групу, а потім створіть/запустіть проєкт, не передаючи вхідні дані програми. |
Для графів, що надсилаються застосунком, назвіть вхідну кінцеву точку, орієнтуючись на концепцію застосунку: image, left_camera, metadata, prompt. Для графів, що належать джерелу, перевірте вихідний контракт і вилучіть те, що генерує шлях джерела.
Оберіть параметри графа.
Використовуйте GraphOptions для налаштування поведінки на рівні графа. Використовуйте RunOptions пізніше для налаштування поведінки під час виконання.
| Мета | Використовуйте | Примітки |
|---|---|---|
| Додайте мітку до графа в журналах і експортованих даних. | Graph("name") | Це мітка, а не кінцева точка. |
| Запустіть декілька графів в одному процесі. | element_name_prefix / element_name_suffix | Уникайте конфліктів імен згенерованих елементів і забезпечте читабельність діагностичних даних. |
| Керування діагностикою графа. | VerboseOptions | Почніть з аналізу результатів роботи. Увімкніть детальну інформацію для налагодження лише під час збору даних. |
| Оберіть бажану поведінку черги виводу. | OutputOptions::Latest(), EveryFrame(...) або Clocked(...). | Обирайте: свіжість, повноту або доставку в узгоджений час. |
| З’єднуйте динамічні фрагменти графа. | GraphLinkOptions з RealtimeLatestByStream. | Забезпечте безперервність кожного потоку даних, одночасно обмежуючи кількість декодованих кадрів, що надходять. |
| Зв’язана функція зворотного виклику. | callback_timeout_ms | Використовуйте разом із функціями зворотного виклику в стилі C++, щоб повільна функція зворотного виклику не спричиняла проблем із відображенням графа. |
| Налаштуйте розширені параметри виконання. | advanced_execution | Використовуйте лише після того, як для стандартного графа буде визначено базовий рівень. |
| Обмежити кількість потоків виконання ядер A65 | kernel_runner_a65_workers | Типово 2; допустимі значення 1–64. Стосується чисельних етапів A65 цього графа, а не EV74 чи MLA. |
simaai::neat::GraphOptions options;
options.element_name_prefix = "cam0_";
simaai::neat::Graph graph("cam0_detector", options);
graph.add(simaai::neat::nodes::Input("image"));
graph.add(model);
graph.add(simaai::neat::nodes::Output(
"detections",
simaai::neat::OutputOptions::Latest()));
Latest() зберігає найновіший результат, коли програма не може отримати всі вихідні дані. Використовуйте EveryFrame(...), коли важливі всі вихідні дані. Використовуйте Clocked(...), коли вихідні дані повинні відповідати тактовій частоті конвеєра.
RunOptions::queue_depth контролює вхідні дані графа та внутрішні черги середовища виконання. Він не замінює
умову черги загальнодоступного Output: OutputOptions::max_buffers та drop контрол юють
цю кінцеву чергу та мають пріоритет. Черги вихідних даних, створені фреймворком, повертаються до значень за замовчуванням RunOptions.
Перекривайте обробку кадрів без необмеженого зростання черг
Асинхронне подання дає змогу виконувати один кадр на пристрої, поки CPU готує або споживає інший. Побудуйте граф один раз, використовуйте його повторно й тримайте в обробці обмежену кількість незалежних входів. Якщо одразу після подання забирати його результат, доступним залишається лише один кадр, навіть коли граф підтримує асинхронне виконання.
Різні засоби керування мають різні завдання:
| Засіб керування | Що він обмежує |
|---|---|
| Вікно незавершених входів застосунку | Прийняті входи, результати яких застосунок ще не спожив. Керуйте цим вікном у циклі подання й отримання. |
advanced_execution.internal_queue_depth | Внутрішньо вставлені черги та місткість виконання чисельного Runner. Місткість Runner обмежується діапазоном 1–64; незадана або типова глибина вибирає 4 слоти для асинхронного виконання; синхронне виконання використовує 1 слот. |
advanced_execution.inference_output_buffers | Буферизація етапу MLA. Зміна глибини черги застосунку або середовища виконання не замінює цей параметр. |
RunOptions::queue_depth | Вхідні черги фреймворку та внутрішні черги середовища виконання, а не місткість прискорювача. |
OutputOptions | Буферизація публічного виходу та поведінка відкидання. Використовуйте EveryFrame(...), коли вимірюєте завершену роботу без втрат. |
Асинхронне виконання MLA типово ввімкнене. Залишайте його ввімкненим для вимірювань пропускної здатності; не додавайте потік синхронізації кешу й не вимикайте синхронізацію пам’яті заради перекриття. Завершення роботи пристрою та видимість для CPU — окремі вимоги. Середовище виконання зберігає обидві, дозвол яючи незалежним кадрам просуватися паралельно.
Налаштовуйте по одному навантаженню:
- Після побудови й прогрівання графа виміряйте один кадр в обробці як послідовну базову лінію.
- Збільшуйте вікно застосунку по одному кроку. Для одночасних кадрів надавайте окремі доступні для запису вхідні виділення пам’яті та своєчасно звільняйте виходи.
- Збільшуйте внутрішню буферизацію або буферизацію MLA лише тоді, коли її поточна межа заважає корисному перекриттю. Утримуваний вихід без копіювання все ще займає пул свого виробника; розмір пулу не гарантує, що кожний слот доступний застосунку.
- Припиніть збільшувати вікно, коли пропускна здатність перестане зростати. Додаткові кадри в черзі можуть збільшити затримку та обсяг утримуваної пам’яті без додаткової роботи.
Не перезаписуйте вхідне виділення пам’яті, поки прийнята робота ще може його читати. Повторне використання незмінного входу відрізняється від повторного використання його сховища для наступного кадру камери. В обох випадках дотримуйтеся наявного контракту володіння.
Порівнюйте час CPU на завершений кадр, а не лише відсоток використання CPU. Для того самого вікна вимірювання поділіть суму користувацького й системного часу CPU процесу на кількість завершених виходів; поділіть цей час CPU на минулий реальний час, щоб отримати середню кількість зайнятих ядер CPU. Вища пропускна здатність може використовувати більше ядер у часі без збільшення роботи CPU на кадр. Виключайте підготовку, прогрівання та завершення; у кожному порівнянні враховуйте однакову роботу з отримання й звільнення виходів. Затримку кадру звітуйте окремо: 1 / FPS — це інтервал пропускної здатності, а не затримка окремого кадру. Час CPU процесу не охоплює роботу, нараховану окремим робітникам ядра ОС, і не вимірює енергію.
Задайте бюджет робітників A65
Задайте GraphOptions.kernel_runner_a65_workers до побудови графа. Типове значення — 2; використовуйте 1, щоб обмежити чисельне виконання A65 одним робітником, або збільшуйте бюджет, коли незалежні етапи A65 можуть перекриватися. Недопустимі значення спричиняють помилку під час підготовки графа, а не мовчазне обмеження.
simaai::neat::GraphOptions options;
options.kernel_runner_a65_workers = 2;
simaai::neat::Graph graph("camera", options);
Пул робітників спільний для етапів ядер A65 цього графа й створюється лише за потреби. Неактивні робітники сплять без опитування; вони повторно використовуються між поданнями. Цей параметр не змінює глибину черги кадрів, не перевизначає обмеження ресурсів ядер і не налаштовує потоки EV74, MLA чи GStreamer. Більше робітників не обов’язково означає швидше виконання: перш ніж змінювати типове значення, порівняйте затримку, пропускну здатність і використання CPU для свого навантаження.
Перевірте, перш ніж починати будувати.
Використовуйте graph.validate(...), коли вам потрібні дані для підтвердження побудови графа, не запускаючи при цьому повноцінний процес.
simaai::neat::GraphReport report = graph.validate();
std::cout << report.to_json() << "\n";
ValidateOptions.parse_launch перевіряє згенерований рядок конвеєра. ValidateOptions.enforce_names виявляє елементи, які не мають назви або є несподіваними. Перегляньте звіт перед зміною параметрів графа; спочатку перевірте дані, а потім налаштовуйте параметри.
Створення графів
Найпростіша когнітивна модель.
Щоб створити застосунок, спочатку оголосіть Graph, потім використовуйте add() для лінійного ланцюга або connect() для визначення конкретної топології:
- Використовуйте
add()для типових сценаріїв обробки даних за одним шляхом, наприклад, для простого висновку на основі однієї моделі. - Використовуйте
connect(), коли вам потрібен чіткий контроль над тим, як з’єднуються вузли або фрагменти.
simaai::neat::Graph g;
g.add(...); // continue the same linear chain
g.connect(...); // add explicit graph topology
auto run = g.build();
Створіть, використовуючи add().
Наведений нижче приклад зчитує зображення, виконує обчислення та виводить передбачені класи, отримані з моделі.
image -> model inference -> classes
Ось як виглядатиме код:
simaai::neat::Model model("resnet50.tar.gz"); // Load a compiled model and prepare its Graph route.
simaai::neat::Graph g("classifier"); // instantiate the Graph that will describe the app
g.add(simaai::neat::nodes::Input("image")); // adds an input Node named "image"
g.add(model); // adds the model Nodes connected to the Input
g.add(simaai::neat::nodes::Output("classes")); // adds an output Node named "classes" connected to 'model'
auto run = g.build(); // build the Graph
Використовуйте add(), щоб додавати вузли в просту лінійну послідовність, слідуючи за попередньо доданим вузлом.
Створіть, використовуючи connect().
Використовуйте connect(), коли вам потрібен явний контроль над топологією. Кожна команда add() спочатку з’єднує новий вузол або фрагмент після попереднього. Коли ви використовуєте обидва методи, connect() замінює відповідні неявні з’єднання на конкретні з’єднання, які ви визначаєте. Ви також можете використовувати connect() безпосередньо між іменованими кінцевими точками, вузлами, моделями або фрагментами графа, які можна повторно використовувати.
Розгалуження за допомогою connect().
Функція розгалуження надсилає один вхідний сигнал на кілька вихідних. Спочатку додайте кінцеві точки, щоб вони існували в графі:
simaai::neat::Graph fan_out_graph("fan_out_graph");
fan_out_graph.add(simaai::neat::nodes::Input("image_input"));
fan_out_graph.add(simaai::neat::nodes::Output("original_image"));
fan_out_graph.add(simaai::neat::nodes::Output("model_image"));
У загальних рисах fan_out_graph виглядатиме так:
image_input --> original_image --> model_image
Потім використайте connect(), щоб замінити стандартну лінійну схему з’єднань на потрібну вам топологію:
fan_out_graph.connect("image_input", "original_image");
fan_out_graph.connect("image_input", "model_image");
У загальних рисах, fan_out_graph зараз матиме такий вигляд:
/--> original_image
image_input
\--> model_image
Використання Branch()
Оскільки розгалуження є поширеним явищем, Neat надає graphs::Branch() як допоміжний інструмент. Це створює вхідні дані, вихідні дані та внутрішні виклики connect():
auto fan_out_graph = simaai::neat::graphs::Branch(
"image_input",
{"original_image", "model_image"});
У Python використовуються ті самі назви кінце вих точок:
fan_out_graph = pyneat.graphs.branch(
"image_input",
["original_image", "model_image"],
)
fan_out_graph все ще є звичайним фрагментом Graph. Його можна під’єднати до більшої програми, як і будь-який інший Graph.
Розгалуження може спричинити зворотний тиск. Якщо одна з гілок припиняє обробку даних, це може уповільнити або заблокувати роботу виробника, залежно від обраної політики в середовищі виконання. Branch() робить процес розгалуження явним, замість того щоб приховувати його за допомогою випадкових дублікатів вихідних даних.
Підключення за допомогою connect().
Схема «зведення» (fan-in) передає кілька вхідних сигналів на один вихід. На відміну від простої схеми «розгалуження» (fan-out), схема «зведення» також повинна визначати, як узгоджуються зразки. Ця політика застосовується до вихідної кінцевої точки.
Спочатку налаштуйте вихід і додайте кінцеві точки, щоб вони існували в графі:
simaai::neat::OutputOptions render_options;
render_options.combine_policy = simaai::neat::CombinePolicy::ByFrame;
simaai::neat::Graph render_inputs_graph("render_inputs_graph");
render_inputs_graph.add(simaai::neat::nodes::Input("image"));
render_inputs_graph.add(simaai::neat::nodes::Input("bbox"));
render_inputs_graph.add(simaai::neat::nodes::Output("render_inputs", render_options));
У загальних рисах, render_inputs_graph виглядатиме так:
image -> bbox -> render_inputs
Потім використайте connect(), щоб замінити стандартну лінійну схему з’єднань на потрібну вам топологію:
render_inputs_graph.connect("image", "render_inputs");
render_inputs_graph.connect("bbox", "render_inputs");
В концептуальному плані render_inputs_graph зараз виглядатиме так:
image ----\
render_inputs
bbox -----/
Використовуючи Combine()
Оскільки широко використовується схема «багато до одного», Neat надає graphs::Combine() як допоміжний інструмент. Це створює вхідні дані, вихідні дані, політику об’єднання та внутрішні виклики connect():
auto render_inputs_graph = simaai::neat::graphs::Combine(
{"image", "bbox"},
"render_inputs",
simaai::neat::CombinePolicy::ByFrame);
У Python використовується None_ для політики, яка забороняє об’єднання, оскільки None є зарезервованим словом:
render_inputs_graph = pyneat.graphs.combine(
["image", "bbox"],
"render_inputs",
pyneat.CombinePolicy.ByFrame,
)
render_inputs_graph все ще є звичайним фрагментом Graph. Ви можете під’єднати його до більшої програми, як і будь-який інший Graph.
graphs::Combine() створює свій вихід, використовуючи стандартні OutputOptions: чотири збуферизовані зразки, блокування переповнення та відсутність синхронізації за часом. Якщо цей вихід залишається загальнодоступним терміналом, очищайте його під час генерації. Якщо перед отриманням даних необхідно передати скінченну партію, створіть явну структуру з’єднання, показану вище, і налаштуйте її вихід за допомогою OutputOptions::EveryFrame(...), розмір якої відповідає цій партії.
У Python використовуйте pyneat.OutputOptions.every_frame(...).
CombinePolicy визначає, як Neat має зіставляти вхідні зразки:
ByFrame: об’єднайте зразки з однаковимframe_id.ByPts: об’єднайте зразки, які мають однаковий час представлення (pts_ns).None: не об’єднуйте декілька джерел даних; у такому разі граф не працюватиме і вимагатиме чітко визначеної політики.- Стиль написання коду на Python:
pyneat.CombinePolicy.None_,ByFrameабоByPts.
Немає прихованого механізму резервного копіювання. У випадку використання ByFrame, відсутність ідентифікаторів кадрів вважається помилкою. У випадку використання ByPts, відсутність міток часу вважається помилкою. Це запобігає тому, щоб Neat непомітно об’єднував неправильні зразки.