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

Запустіть граф.

Graph – це план. Run – це засіб для запуску та виконання.

Використовуйте цю сторінку після створення графа. Якщо вам все ще потрібно визначити, які вузли або фрагменти мають бути в графі, почніть з Граф. Якщо граф вже виглядає правильно, на цій сторінці ви можете запустити його, перевірити, виміряти та забезпечити його стабільну роботу з реальними даними.

Оберіть одноразове або багаторазове виконання.

Використовуйте найкоротший можливий шлях у середовищі виконання, який відповідає вимогам завдання:

ПотрібноВикористовуйтеЧому?
Введіть один набір даних і отримайте один результат.Graph.run(...)Найкоротший маршрут, який можна пройти за один раз.
Послідовно передавайте багато вхідних даних протягом певного часу.Graph.build(...) та Run.Повторно використовує середовище виконання та надає можливість керування за допомогою механізмів «push» і «pull».
Використовуйте іменовані вхідні або вихідні параметри.Graph.build(...) та названо run.push(...) / run.pull(...).Забезпечує чітке визначення застосунків із кількома вхідними та вихідними даними.
Нехай вихідні вузли визначають структуру графа.Graph.build() або Graph.run() без введення даних від програми.Використовуйте, коли граф містить камеру, файл, джерело RTSP або інший вузол-джерело.
Вимірюйте, експортуйте, зливайте або зупиняйте навмисно.RunНадає можливість контролювати життєвий цикл і проводити діагностику.

Без жодних чарів. Створіть граф, запустіть його, перевірте результат.

Оберіть, яким чином дані надходитимуть у граф.

Перш ніж налаштовувати черги, визначте, кому належать вхідні дані.

Стиль графа.Як здійснюється введення даних.Як ви це робите.
Граф, що відображається за допомогою застосунку.Ваша програма викликає Graph.run(input, ...), run.run(input, ...), run.push(...) або run.try_push(...).Створіть або запустіть із вхідними даними. Перевірте назви кінцевих точок перед додаванням до графів із кількома вхідними даними.
Граф, що належить джерелу.У графі міститься вихідний вузол або фрагмент, наприклад, файл, камера, RTSP або вхідний потік.Створіть або запустіть без введення даних із програми: graph.build() або graph.run(). Отримуйте результати, використовуйте вузли виводу або використовуйте функції зворотного виклику, залежно від графа.

Якщо граф є власником джерела, не намагайтеся в нього щось додати. Натомість перевірте, що воно генерує.

Запустіть граф, яким володіє джерело.

Якщо граф містить власну початкову вузлу, створіть або запустіть його без введення даних від програми. Не додавайте дані до графа, який вже містить початкову вузлу. Отримуйте іменовані вихідні дані, коли граф їх надає; дозвольте кінцевим вузлам обробляти вихідні дані, коли граф закінчується кінцевою вузлою.

Використовуйте graph.run() для завдання, яке передбачає передачу даних від початкової вузли до кінцевої, де граф одночасно є джерелом і приймачем даних. Використовуйте graph.build(), коли вашій програмі потрібно отримати результати, виміряти час виконання або навмисно зупинити процес.

auto run = graph.build();

while (running && run.can_pull()) {
auto sample = run.pull("detections", /*timeout_ms=*/1000);
if (!sample) {
continue;
}
handle(*sample);
}

run.close();

Для джерел, які працюють тривалий час, налаштуйте свою програму таким чином, щоб вона визначала, коли потрібно вийти з циклу, і викликала close(). Тайм-аут означає, що протягом цього проміжку часу не було отримано жодних даних; це не завжди означає, що джерело завершило роботу.

Запустити один раз.

Використовуйте Graph.run(...), коли вам потрібна одна синхронна операція надсилання/отримання даних у графі.

simaai::neat::Graph graph("classifier");
graph.add(simaai::neat::nodes::Input("image"));
graph.add(model);
graph.add(simaai::neat::nodes::Output("classes"));

simaai::neat::TensorList outputs = graph.run(std::vector<cv::Mat>{frame});

У Python передавайте список або кортеж. graph.run([tensor]) означає «один вхідний граф», а не «додайте розмірність пакета».

Створіть багаторазовий сценарій виконання.

Використовуйте Graph.build(...), коли ваш застосунок керує циклом.

auto run = graph.build();

run.push("image", std::vector<cv::Mat>{frame});
simaai::neat::TensorList outputs = run.pull_tensors("classes", /*timeout_ms=*/2000);

run.close_input();
while (auto sample = run.pull(/*timeout_ms=*/100)) {
// Drain remaining output after end-of-input.
}
run.close();

Використовуйте close_input(), коли завершите передачу даних і захочете, щоб поточна робота завершилася. Використовуйте close(), коли захочете зупинити виконання; у C++ також передбачено stop() як команду для негайної зупинки.

Використовуйте повторно використовуваний блок для обробки запитів/відповідей.

Graph.run(...) — це найкоротший одноразовий шлях. Якщо вам потрібна однакова структура запиту/відповіді без щоразу перебудови графа, створіть один Run, який можна повторно використовувати, і викликайте run.run(...).

Використовуйте це, коли:

  • граф залишається активним протягом багатьох запитів;
  • кожний запит все одно має чекати на отримання власного результату;
  • наразі вам не потрібні окремі потоки для виробника та споживача.
auto run = graph.build();

for (const auto& frame : frames) {
simaai::neat::TensorList outputs = run.run(
std::vector<cv::Mat>{frame},
/*timeout_ms=*/2000);
handle(outputs);
}

run.close();

Перейдіть від run.run(...) до явного використання push(...) / pull(...), коли вам потрібна обробка даних у процесі, потоки «виробник/споживач», неблокуючий механізм передачі даних, іменоване опитування вихідних даних або керування вивантаженням.

Перевірте кінцеві точки середовища виконання.

Перш ніж передавати дані в багатовхідний граф, запитайте у Run, які імена він приймає.

auto run = graph.build();

for (const auto& name : run.input_names()) {
std::cout << "input: " << name << "\n";
}
for (const auto& name : run.output_names()) {
std::cout << "output: " << name << "\n";
}

Якщо в графі є більше одного загальнодоступного входу або виходу, використовуйте іменовані push(...) та pull(...). Не потрібно, щоб Neat намагався вгадати, який саме провід ви мали на увазі.

Запустіть графи з кількома входами та кількома виходами.

Для графів з кількома вхідними точками, додавайте по черзі іменовані кінцеві точки, або додавайте неіменований список лише тоді, коли в графі є один чітко визначений вхідний маршрут.

run.push("left", simaai::neat::TensorList{left_tensor});
run.push("right", simaai::neat::TensorList{right_tensor});

auto boxes = run.pull_tensors("detections", /*timeout_ms=*/2000);
auto preview = run.pull("preview", /*timeout_ms=*/2000);

Під час об’єднання потоків зберігайте відповідний ключ, який очікує граф. Для CombinePolicy::ByFrame потрібен frame_id; для CombinePolicy::ByPts потрібен pts_ns. Відсутність ключа має призводити до чіткої помилки. Безшумне об’єднання – це спосіб, за допомогою якого помилки перетворюються на проблеми архітектури.

Оберіть параметри запуску.

RunOptions визначає поведінку під час виконання. Почніть із параметрів за замовчуванням. Змініть параметри, коли вихідний код, тривалість роботи або цільові показники продуктивності вимагають іншої політики.

Обсяг роботиПочніть зЧому?
Перша робоча версія застосунку.стандартні RunOptionsПеревірте правильність налаштувань перед їх оптимізацією.
Відео з камери в реальному часі або вхідний сигнал RTSP.RunPreset::Realtime; OutputOptions::Latest(), де важлива актуальність результатів.Нові кадри перевершують повну історію. Налаштування в реальному часі вирішує проблему переповнення останнього кадру, якщо ви не зміните його.
Обробка окремих файлів або пакетів файлів.RunPreset::Reliable; OutputOptions::EveryFrame(...)Зберігайте всі вхідні дані та забезпечуйте механізм зворотного зв’язку. Надійне попередньо встановлене значення запобігає переповненню, якщо ви не зміните його.
Звичайне обслуговування застосунку.RunPreset::BalancedЦе хороші налаштування за замовчуванням, коли граф працює належним чином.
Нестабільному джерелу потрібен буфер обмеженого розміру.queue_depthЗбільшуйте лише до необхідного рівня, щоб компенсувати коливання. Велика черга може приховати застарілі кадри та затримки зворотного зв’язку.
Магазини застосунків відображають результати після оновлення.OutputMemory::OwnedЗабезпечує незалежність часу існування вихідних даних від буферів середовища виконання.
Програма одразу використовує отримані дані.OutputMemory::AutoДозвольте Neat спочатку визначити оптимальний спосіб передачі прав власності.
Час очікування за замовчуванням має бути чітко визначений.input_timeout_msВстановлює значення тайм-ауту за замовчуванням для шляхів, що використовуються в режимі введення даних під час збирання/виконання. Індивідуальні значення тайм-ауту для кожного виклику мають пріоритет.
Під час початкового збирання проєкту слід виявляти перші помилки на ранніх етапах.startup_preflight = trueЗабезпечує коректність збірки на основі початкових даних. Відключайте лише тоді, коли первинні зразки можуть виявити помилки пізніше за допомогою pull(...) або last_error().
Термін дії вихідного буфера обмежений.advanced.copy_input = trueЗахищає вхідні дані в пам’яті, які можуть бути втрачені після виконання операції push(...).
Необхідно встановити обмеження для вхідних даних.advanced.max_input_bytesВідхиляє занадто великі вхідні дані, перш ніж вони будуть оброблені графом.
Вам потрібно відключити збір телеметричних даних.on_input_dropПідраховує кількість перевищень ліміту та випадків спрацювання захисту від надмірної кількості даних для кожного потоку та з урахуванням причини.
Вам потрібні докази, зібрані під час процесу розробки.run_exportСтворює знімок поточної сесії під час її виконання.
simaai::neat::RunOptions options;
options.preset = simaai::neat::RunPreset::Realtime;
options.on_input_drop = [](const simaai::neat::InputDropInfo& drop) {
std::cerr << "dropped input from stream " << drop.stream_id
<< ": " << drop.reason << "\n";
};

auto run = graph.build(options);

Не потрібно регулювати кожну ручку лише тому, що вона є. Найшвидший спосіб заблукати – це налаштовувати параметри до того, як ви визначите базові значення.

Рекомендації щодо налаштування параметрів середовища виконання.

Скопіюйте структуру цих рецептів, а не числові значення. Розміри черги та обмеження вихідних даних залежать від моделі, швидкості обробки даних і того, як швидко ваш застосунок отримує результати.

Трансляція в реальному часі з мінімальною затримкою.

Використовуйте це, коли наступний кадр важливіший, ніж повна історія кадрів. Встановіть політику вихідної черги під час додавання вихідного вузла; встановіть політику обробки вхідних даних/відхилення під час створення Run.

graph.add(simaai::neat::nodes::Output(
"detections",
simaai::neat::OutputOptions::Latest()));

simaai::neat::RunOptions options;
options.preset = simaai::neat::RunPreset::Realtime;

auto run = graph.build(options);

Цей рецепт зберігає найновіший корисний результат, замість того щоб створювати своєрідний «музей» застарілих кадрів. Постійно тягніть і підраховуйте кількість крапель у кожній серії.

Вивід пакету даних без втрат.

Використовуйте це, коли кожен вхідний сигнал має генерувати відповідний вихідний сигнал, і коли краще мати механізм регулювання навантаження, ніж втрачати дані.

graph.add(simaai::neat::nodes::Output(
"result",
simaai::neat::OutputOptions::EveryFrame(/*max_buffers=*/64)));

simaai::neat::RunOptions options;
options.preset = simaai::neat::RunPreset::Reliable;

auto run = graph.build(options);

Закривайте вхідні дані після завершення роботи генератора, а потім очищайте вихідні дані. Якщо кількість вхідних і вихідних даних не збігається, перевірте контракт моделі, перш ніж звинувачувати середовище виконання.

Термін служби об’єкта, яким володіє програма.

Використовуйте керований вихід, коли ваш застосунок зберігає тензори після того, як функція pull(...) завершить роботу або передасть їх іншому потоку. Залиште Auto для коду, який виконується вперше, і змінюйте це лише тоді, коли це вимагає час життя об’єкта.

simaai::neat::RunOptions options;
options.output_memory = simaai::neat::OutputMemory::Owned;

auto run = graph.build(options);

Початкова збірка потрібна, коли необхідно на ранніх етапах підтвердити структуру або формат.

Більшість повторно використовуваних завдань можна виконати без введення даних:

run = graph.build()

Використовуйте початкові дані build(input, ...), коли перші реальні дані повинні підтвердити структуру, формат, регістр або поведінку, пов’язану з обробкою байтів, перш ніж програма перейде до циклу обробки потоку даних.

auto run = graph.build(std::vector<cv::Mat>{frame});

За замовчуванням для збірок, що використовують початкові дані, увімкнено startup_preflight, тому початкові дані фіксують помилки на рівні корисного навантаження під час збирання. Якщо збирання завершується невдало, структурований звіт може містити build_adaptation: форму початкових даних, динамічні обмеження, захист байтів і дії адаптації, які виконував Neat. Використовуйте це для налагодження на основі фактів, а не припущень.

Обробляйте зворотний тиск.

Зворотний тиск означає, що граф не може приймати або передавати дані так швидко, як цього потребує застосунок.

Використовуйте ці елементи керування обдумано:

  • queue_depth визначає, який обсяг завдань може очікувати в чергах у середовищі виконання.
  • overflow_policy = Block створює зворотний тиск для виробника.
  • overflow_policy = KeepLatest видаляє старіші дані з черги вхідних даних, щоб забезпечити актуальність потокових трансляцій.
  • overflow_policy = DropIncoming відхиляє нові вхідні дані, коли черга заповнена.
  • try_push(...) повертає false, а не блокує виконання.
  • on_input_drop повідомляє про відхилені вхідні дані, надаючи інформацію InputDropInfo, зокрема поля stream_id, frame_id, port_name та reason.

Для організації багатопоточності використовуйте один потік для надсилання даних і один потік для отримання даних для Run. Не надсилайте дані до одного й того ж Run одночасно з кількох потоків, якщо ваша програма не забезпечує послідовне виконання цих операцій.

Використовуйте просту схему організації потоків.

Для графіків, які оновлюються в реальному часі або мають високу пропускну здатність, і дані для яких надсилаються застосунком, почніть із двох потоків застосунку:

  1. Потік-продюсер додає метадані та викликає push(...) або try_push(...).
  2. Споживчий потік безперервно зчитує дані та швидко передає або копіює результати.

Додайте більше потоків навколо власних черг, а не навколо однієї й тієї ж Run. Основний цикл має бути монотонним. Монотонність – це швидкодія.

auto run = graph.build(options);

std::thread producer([&] {
while (auto sample = next_sample()) {
sample->stream_id = current_stream_id();
sample->frame_id = next_frame_id();

if (!run.try_push("image", *sample)) {
count_local_drop(sample->stream_id);
}
}

run.close_input();
});

std::thread consumer([&] {
simaai::neat::Sample output;
simaai::neat::PullError error;

while (true) {
switch (run.pull("detections", /*timeout_ms=*/100, output, &error)) {
case simaai::neat::PullStatus::Ok:
handle_output(output);
break;
case simaai::neat::PullStatus::Timeout:
continue;
case simaai::neat::PullStatus::Closed:
return;
case simaai::neat::PullStatus::Error:
record_runtime_error(error);
return;
}
}
});

producer.join();
consumer.join();
run.close();

У C++ використовуйте перевантажену версію pull(...), яка враховує стан, коли час очікування, кінець потоку та помилки мають оброблятися по-різному. У Python функція pull(...) повертає None, якщо під час виклику не повертається жоден зразок, тому поєднуйте її зі своїм власним станом генератора/завершення.

Закрийте, злийте або зупиніть навмисно.

Оберіть спосіб завершення роботи, який відповідає вашим намірам. Не намагайтеся продовжувати роботу, якщо процес завершується.

НамірВикористовуйтеЩо робити далі?
Завершіть обробку завдань зі списку очікування після отримання останнього вхідного сигналу.close_input()Продовжуйте зчитувати дані, поки вихідний потік не буде повністю вичерпано. У C++, функція зчитування, яка враховує стан, повертає значення PullStatus::Closed після завершення зчитування даних.
Скасувати зараз.stop()Зупиніть процеси-виробники та дайте змогу процесам очікування завершити роботу, щоб уникнути блокувань. Використовуйте це для процедур аварійного завершення роботи або обробки помилок, а не для звичайного процесу обробки даних.
Звільніть ресурси середовища виконання.close()Зателефонуйте після завершення зливу або скасування, або дозвольте об’єкту Run вийти за межі області видимості.

Для пакетної обробки закрийте вхідні дані, очистіть вихідні дані, а потім завершіть процес. Для обробки в реальному часі спочатку зупиніть процеси-джерела, а потім зупиніть або закрийте процес. Не допускайте наявності неактивних процесів-джерел і «занедбаних» черг.

Оберіть, кому належатимуть права на результат.

OutputMemory визначає, як отримані тензори пов’язані з буферами в середовищі виконання:

  • Auto: дозвольте Neat зробити вибір. Спочатку використайте це.
  • Owned: скопіюйте вихідні дані в пам’ять, що належить фреймворку. Використовуйте це, коли інший потік або об’єкт зберігає тензори після отримання даних.
  • ZeroCopy: використовуйте спільне сховище в середовищі виконання. Застосовуйте це лише тоді, коли на сторінці або в прикладі пояснюються правила терміну дії.

Якщо пропускна здатність різко падає, перевірте, чи не зберігає застосунок зразки вихідних даних занадто довго. Механізм «нульового копіювання» може бути швидким, але зафіксовані буфери залишаються зафіксованими буферами.

Збережіть ідентифікатор потоку.

Багатопотокові графи потребують ідентифікації, перш ніж їх можна буде налаштувати. Збережіть stream_id та frame_id, щоб мати можливість підтвердити справедливість, виявити ситуації, коли певні потоки не отримують достатньо ресурсів, і підрахувати кількість втрачених даних.

auto sample = simaai::neat::Sample::from_image(
frame,
simaai::neat::ImageSpec::PixelFormat::BGR,
simaai::neat::TensorMemory::CPU);
sample.stream_id = camera_id;
sample.frame_id = frame_number++;

if (!run.try_push("image", sample)) {
// Count local backpressure here. Runtime drops also flow through on_input_drop.
}

Для графів, що належать джерелу, виберіть вузли джерела, які зберігають або додають метадані потоку. Для графів, що передаються застосунком, метадані належать вашому застосунку.

Збільшуйте обсяги від одного потоку даних до багатьох.

Почніть з одного потоку. Потім навмисно масштабуйте топологію та політику середовища виконання.

ЗразокВикористовуйте його, колиДивіться
Один потік даних -> одна модель -> один результат.Створення першого правильного шляху.Форма вихідних даних, тип даних і затримка.
Багато потоків -> одна модельна лінія.Сукупна швидкість введення даних відповідає одній моделі.Справедливий розподіл ресурсів між окремими потоками та обробка застарілих потоків.
Багато потоків -> декілька моделей/смугОднієї смуги недостатньо, щоб впоратися з навантаженням.Розподіл потоків, призначення імен маршрутам і ведення обліку вихідних даних.
Один потік даних -> декілька моделей.Для різних рішень потрібні однакові вхідні дані.Затримка на рівні гілки та нормалізована кількість кадрів за секунду (FPS) для цільової платформи.
Багато потоків -> модель + вихідні дані у вигляді метаданих/відео.Програма, що використовується для розробки, генерує кілька артефактів.Підраховуйте кількість цільових результатів окремо від результатів попереднього перегляду або телеметрії.

Під час з’єднання динамічних фрагментів графа, GraphLinkOptions дозволяє обрати режим оновлення даних у реальному часі, коли найновіші дані з кожного потоку мають пріоритет. Використовуйте цей режим, коли важливіше мати актуальні дані, ніж зберігати кожен кадр у динамічному з’єднанні.

Запустіть багатопотокові графи, що належать джерелу.

Для програм, які активно використовують камеру, зазвичай саме граф керує потоками даних. У такому випадку, групи джерел передають дані в модель, а ваша програма отримує результати. Вам, як і раніше, потрібно дотримуватися тієї ж дисципліни щодо обсягу оброблюваних даних:

  • присвойте кожному джерелу стабільний stream_id;
  • використовуйте поведінку «останні дані за потоком у реальному часі» для активних каналів передачі даних, коли важлива актуальність інформації;
  • постійно отримуйте дані;
  • підраховуйте кількість результатів для кожного окремого потоку, а не лише загальну кількість;
  • експортуйте дані після завершення вимірюваного періоду, якщо один із потоків даних припиняє надходження або втрачає кадри.
Вибір, що здійснюється власником джерела.Почніть зЧому?
Одна камера на один граф.Одна група джерел, один шлях моделі, один результат.Найпростіший спосіб підтвердити характеристики камери, моделі та умови контракту щодо результату.
Багато камер в одному модельному ряду.Фрагменти вихідного коду, з’єднані з одним фрагментом моделі за допомогою GraphLinkOptions, для забезпечення динамічного об’єднання даних.Забезпечує завантаженість одного каналу моделі, одночасно зберігаючи ідентичність кожного потоку даних.
На багатьох камерах, розташованих вздовж смуг руху.Розподіліть вихідні фрагменти між кількома гілками графа.Використовуйте, коли одна модельна смуга досягає межі насичення. Вимірюйте кожну смугу та кожен потік.
Відеосигнал обробляється за допомогою графа.Групи каналів, такі як VideoSender(...) або групи вихідних даних H.264/UDP.Використовуйте, коли застосунок не повинен самостійно завантажувати та передавати кожен кадр.

Якщо граф є власником джерел, створіть його за допомогою graph.build() і зупиніть його навмисно. Не передавайте вхідні дані програми в граф, який уже має власні вузли-джерела.

Пропустіть багато потоків даних через один канал моделі.

Використовуйте одну загальнодоступну кінцеву точку для введення, коли кілька активних потоків використовують один і той самий канал моделі. Позначайте кожен зразок ідентифікаторами stream_id та frame_id, використовуйте попередньо встановлені параметри для обробки в реальному часі та здійснюйте безперервний потік даних. Ця стратегія може здаватися нудною, але вона ефективна: ніколи не дозволяйте черзі вихідних даних ставати вашим прихованим вузьким місцем.

simaai::neat::RunOptions options;
options.preset = simaai::neat::RunPreset::Realtime;

auto run = graph.build(options);

while (running) {
for (const auto& camera : cameras) {
auto sample = simaai::neat::Sample::from_image(
camera.frame(),
simaai::neat::ImageSpec::PixelFormat::BGR,
simaai::neat::TensorMemory::CPU);
sample.stream_id = camera.id();
sample.frame_id = camera.next_frame_id();

if (!run.try_push("image", sample)) {
++local_drop_count[camera.id()];
}
}

while (auto output = run.pull("detections", /*timeout_ms=*/0)) {
count_output_by_stream(output->stream_id);
}
}

run.close_input();
while (auto output = run.pull("detections", /*timeout_ms=*/1000)) {
count_output_by_stream(output->stream_id);
}
run.close();

Ця схема максимізує корисну пропускну здатність лише тоді, коли пропускна здатність каналу обробки даних відповідає прийнятій швидкості надходження вхідних даних. Якщо один канал перевантажений, розподіліть потоки даних між більшою кількістю каналів або зменште швидкість надходження даних. Не накопичуйте застарілі кадри у великій черзі.

Розділіть потоки даних між різними каналами моделі.

Коли один канал моделі досягає межі пропускної здатності, додайте канали замість того, щоб приховувати перевантаження за допомогою збільшення довжини черг. Зазвичай канал складається з одного Graph плюс один Run, з власними іменами маршрутів моделі та префіксом елементів графа. Розділіть потоки за допомогою стабільного ключа, а потім виміряйте кожен канал і кожен потік.

auto build_lane = [&](int lane_index) {
const std::string lane_name = "lane" + std::to_string(lane_index);

simaai::neat::Model::Options model_options;
model_options.name_suffix = "_" + lane_name;
simaai::neat::Model lane_model(model_path, model_options);

simaai::neat::GraphOptions graph_options;
graph_options.element_name_prefix = lane_name + "_";

simaai::neat::Graph graph("detector_" + lane_name, graph_options);
graph.add(simaai::neat::nodes::Input("image"));
graph.add(lane_model);
graph.add(simaai::neat::nodes::Output(
"detections",
simaai::neat::OutputOptions::Latest()));

simaai::neat::RunOptions run_options;
run_options.preset = simaai::neat::RunPreset::Realtime;
return graph.build(run_options);
};

std::vector<simaai::neat::Run> lanes;
lanes.emplace_back(build_lane(0));
lanes.emplace_back(build_lane(1));

while (running) {
for (const auto& camera : cameras) {
auto sample = make_sample_for_camera(camera);
const std::size_t lane_index = camera.id() % lanes.size();

if (!lanes[lane_index].try_push("image", sample)) {
++drop_count_by_lane[lane_index];
}
}

for (std::size_t lane_index = 0; lane_index < lanes.size(); ++lane_index) {
while (auto output = lanes[lane_index].pull("detections", /*timeout_ms=*/0)) {
count_output(lane_index, output->stream_id);
}
}
}

Забезпечте стабільність розділу, щоб ідентифікація потоку та поведінка кешу залишалися передбачуваними. Якщо потік 0 відчуває нестачу ресурсів, а потік 1 простоює, то проблема полягає в політиці розділення.

Ретельно налаштуйте модель траси.

Якщо граф є правильним, але не може забезпечити запропоновану швидкість потоку даних, спочатку визначте, де саме виникає вузьке місце. Не починайте з того, що збільшуєте розмір кожної черги. Це маскує перевантаження і створює умови, за яких застарілі кадри накопичуються.

Використовуйте таку стратегію:

СимптомСпочатку перевірте.Потім спробуйте.
Вхідна частота кадрів (FPS) є високою, але вихідна частота кадрів (FPS) є низькою.Модельний канал або канал постобробки перевантажений.Розділіть потоки даних між каналами, зменште запропоновану швидкість передачі або протестуйте advanced_execution.inference_async на моделі, використовуючи різні варіанти маршрутизації або параметри графа.
Функція try_push(...) часто повертає значення false.Черга вхідних даних заповнена.Постійно зменшуйте навантаження, знижуйте запропоновану швидкість або виберіть чітку політику обробки переповнення OverflowPolicy.
Один із показників зникає в сукупних метриках.Відсутність або нерівномірність обліку stream_id.Підраховуйте кількість вихідних даних і втрачених даних для кожного потоку; використовуйте найновіші дані для кожного потоку в режимі реального часу для об’єднання даних з кількох потоків.
Вивід припиняється, а вхідні дані продовжують надходити.Програма не достатньо швидко обробляє дані або зберігає результати, що залежать від середовища виконання.Використовуйте окремий цикл, а потім виводьте/копіюйте результати, перш ніж додавати нові дані.
Затримка збільшується з часом.Черги обробляють старі завдання.Використовуйте меншу чергу, RunPreset::Realtime, або OutputOptions::Latest(), де важлива актуальність даних.

Коли вам потрібно перевірити поведінку моделі під час виконання маршруту, встановлюйте по черзі кожне з розширених полів виконання та вимірюйте обсяг роботи до та після цього:

simaai::neat::GraphOptions graph_options;
graph_options.advanced_execution.inference_async = true;

simaai::neat::Graph graph("detector", graph_options);

Якщо внесені зміни не покращують показники, скасуйте їх. Елемент керування, який не може продемонструвати свою корисність, не повинен бути в програмі.

Оберіть оптимальний варіант налаштування пропускної здатності.

Починайте з урахуванням обсягу роботи, а не з довільного номера в черзі.

Обсяг роботиФорма під час виконання (у середовищі виконання).Почніть зДоведіть це за допомогою
Одна пряма трансляціяОдин пристрій для багаторазового використання Run, один пристрій для подачі, один пристрій для витягування.RunPreset::Realtime; OutputOptions::Latest() для виведення результатів у стилі попереднього перегляду.Прийнятий показник FPS, вихідний показник FPS, кількість втрачених кадрів і затримка.
Обробка окремих файлів або пакетів файлів.Один придатний для повторного використання Run; закрити вхід і очистити буферRunPreset::Reliable; OutputOptions::EveryFrame(...)Кількість вхідних даних дорівнює кількості вихідних даних, якщо інше не передбачено в модельному контракті.
Багато прямих трансляцій об’єднуються в одну модельну лінію.Вхідні дані, що надходять від програми, із зазначенням Sample, stream_id / frame_id, або фрагменти, що належать джерелу та містять ідентифікаційні дані.RunPreset::Realtime; GraphLinkPolicy::RealtimeLatestByStream через GraphLinkOptions для забезпечення об’єднання даних у реальному часі.Кількість кадрів в секунду (FPS) і кількість втрачених кадрів для кожного окремого потоку, а не загальна кількість кадрів в секунду.
Багато прямих трансляцій охоплюють різні моделі.Розподіляйте потоки даних між кількома екземплярами моделі або гілками графа.Те саме, що й у рецепті для онлайн-трансляції, на кожну смугу.Використання смуги пропускання на смугу, обмеження пропускної здатності для окремого потоку та цільовий показник FPS (кількість кадрів за секунду).
Один вхідний сигнал розгалужується на кілька моделей.Розгалужуйте один раз, а потім запускайте окремі шляхи моделювання.Розгалуження/розподіл у Graph; виберіть бажану поведінку для кожної гілки.Затримка переходів і частота кадрів, нормалізована за цільовими параметрами.

Якщо один із каналів моделювання перевантажений, не намагайтеся приховати проблему, збільшуючи довжину черги. Розподіліть навантаження між каналами, зменште швидкість надходження даних або виберіть чітку політику відхилення. Збільшення довжини черги підвищує стійкість до коливань; це не збільшує обчислювальні можливості.

Оптимізуйте пропускну здатність, не вдаючись до самообману.

Пропускна здатність – це циклічний процес, а не якась єдина чарівна опція.

  1. Побудуйте граф один раз.
  2. Проведіть попереднє нагрівання поза межами діапазону вимірювання.
  3. Підтримуйте обмежену кількість одночасних запитів.
  4. Постійно виконуйте операцію витягування даних, щоб черги вихідних даних не стали вузьким місцем.
  5. Звільніть або скопіюйте результати, перш ніж додавати нові, оскільки буфери вихідних даних можуть використовуватися спільно з середовищем виконання.
  6. Оберіть один із варіантів політики обробки перевантажень: блокування, збереження останньої версії або відхилення вхідних даних.
  7. Збережіть stream_id та frame_id.
  8. Перед зупинкою процесу закрийте вхідні дані та очистіть буфер.
  9. Виміряйте потрібні розміри.
  10. Збережіть результати тестування після виконання вимірювань при заданому навантаженні.

Виміряйте їх окремо:

МетрикаЗначення
Запропоновано значення FPS (кадрів за секунду).Кількість спроб обробки вхідних даних за секунду, зазвичай streams * source_fps.
Прийнята частота кадрів в секунду (FPS)Кількість вхідних даних, які приймаються функціями push(...) або try_push(...) за секунду.
Загальна кількість кадрів в секунду.Загальна кількість виведених даних за секунду для всіх вихідних каналів.
Кількість кадрів в секунду для кожного окремого потоку.Швидкість виведення даних для кожного stream_id.
Цільове значення FPS (кадрів за секунду) після нормалізації.Результати, які враховуються для досягнення цільового показника програми щодо кількості операцій за секунду. Корисно, коли один вхідний сигнал розгалужується на кілька вихідних.
Імовірність випаданняВідхилені або видалені вхідні дані за ідентифікатором потоку stream_id, джерелом і причиною.

Загальний показник FPS може виглядати чудово, навіть якщо один із потоків даних передається з перебоями. Метрики для кожного окремого потоку допомагають виявити проблему.

Форма циклу обробки даних.

Використовуйте цю форму для графа, який надсилається застосунком. Замініть next_inputs() на ваше джерело вхідних даних. Зробіть цикл монотонним: обмежте обсяг роботи, що виконується, забезпечте безперервне отримання даних і не передбачайте експорт звітів у межах основного циклу.

auto run = graph.build(options);

for (int i = 0; i < warmup_frames; ++i) {
run.push(next_inputs());
(void)run.pull(/*timeout_ms=*/5000);
}

auto measurement = run.start_measurement();

int in_flight = 0;
while (in_flight < max_in_flight && has_input()) {
if (run.push(next_inputs())) {
++inputs_sent;
++in_flight;
}
}

while (has_input() || in_flight > 0) {
auto output = run.pull(/*timeout_ms=*/1000);
if (output) {
++outputs_seen;
--in_flight;
output.reset(); // Do not pin runtime-backed buffers longer than needed.
}

while (has_input() && in_flight < max_in_flight) {
if (!run.try_push(next_inputs())) {
break;
}
++inputs_sent;
++in_flight;
}
}

run.close_input();
while (auto output = run.pull(/*timeout_ms=*/1000)) {
++outputs_seen;
}

simaai::neat::MeasureReport report = measurement.stop();
simaai::neat::save_run_json(run, report, "run_after_measurement.json");
run.close();

Залиште ведення журналу для кожного кадру, перевірку вихідних даних, завантаження файлів, налаштування джерела та експорт звіту поза межами вимірюваного циклу, якщо ви не вимірюєте явно загальну поведінку системи.

Вимірюйте та експортуйте дані.

Використовуйте start_measurement(...), щоб спостерігати за вікном надсилання/отримання даних, яким керує програма.

Використовуйте команду «run export» для отримання доказів:

  • RunOptions.run_export створює знімок стану під час збірки.
  • C++ run_to_json(...) та save_run_json(...) експортують результати виконання після завершення процесу.
  • Python run.json(...) та run.save_json(...) експортують один і той самий тип даних.

Увімкніть збір даних про енергоспоживання для RunOptions, які використовуються для запуску процесу:

simaai::neat::RunOptions options;
options.enable_board_power(/*sample_interval_ms=*/100);

auto run = graph.build(options);

simaai::neat::MeasureOptions measure_options;
measure_options.include_power = true;
auto scope = run.start_measurement(measure_options);

Дані про споживання енергії залежать від підтримки живлення на платі та налаштувань монітора. Задокументуйте налаштування вимірювання, вказавши відповідні значення; не слід подавати дані про споживання енергії таким чином, щоб вони виглядали як переносні, якщо живлення не забезпечується належним чином.

Експорт під час збірки відповідає на запитання: «Що створив Neat?». Експорт після завершення роботи відповідає на запитання: «Що відбулося під час роботи?».

Експортуйте під час збірки та після виконання.

Використовуйте експорт під час збірки для артефактів CI та налагодження під час запуску:

simaai::neat::RunOptions options;
options.run_export.path = "run-build.json";
options.run_export.label = "classifier-startup";

auto run = graph.build(options);

Використовуйте функцію експорту після завершення обробки, коли зразки пройшли через граф:

auto scope = run.start_measurement();
// Push and pull the workload.
simaai::neat::MeasureReport report = scope.stop();

simaai::neat::save_run_json(run, report, "run-after.json");

Не здійснюйте експорт даних у межах вимірюваного циклу нагрівання, якщо не передбачено, що тестування охоплює весь процес від початку до кінця.

Перегляньте експортовані дані запуску.

Експорт запуску корисний, оскільки він об’єднує топологію, параметри середовища виконання та результати вимірювань в одному артефакті. Відкриваючи файл JSON, почніть з перегляду даних, які будуть надані клієнту:

Розділ або полеНа які питання вона дає відповіді.
graph.named_inputs / graph.named_outputsЯкі загальнодоступні кінцеві точки були відкриті під час цього запуску?
graph.public_viewЯк виглядав граф застосунку до оптимізації для середовища виконання?
run.output_materializationЧи належали вихідні дані певній стороні, чи використовувався метод нульового копіювання, чи здійснювався їх автоматичний вибір?
run.statsЛічильники загальної кількості вхідних даних, вихідних даних, втрачених даних і затримок.
run.graph_metrics.countersВхідні дані, вихідні дані та відхилення для експортованого запуску або виміряного вікна.
run.graph_metrics.windowВиміряний часовий проміжок, протягом якого експорт містить MeasureReport.
run.node_metrics / run.plugin_metrics_unattributedЯкі етапи займали найбільше часу в середовищі виконання, коли було ввімкнено детальний збір даних про час виконання.
run.path_timingЧас спрацювання сигналу/переходу в момент збору даних про час проходження сигналу.
run.graph_metrics.powerЧи було зібрано, пропущено, вимкнено або недоступно живлення.

Додайте файл із результатами виконання програми, зразок контракту та найпростіший приклад, коли звертаєтеся за допомогою. Це, по суті, пристрій, що записує дані, але без елементу невідомості.

Відлагоджуйте виконання графа.

Якщо графік не відображається належним чином, перевірте, що ви створили, перш ніж змінювати параметри.

  1. Перевірте граф.
  2. Перевірте загальнодоступні кінцеві точки графа перед збіркою.
  3. Перевірте кінцеві точки середовища виконання після збірки.
  4. Використовуйте шлях, який враховує поточний статус, коли час очікування вичерпано, з’єднання закрито або виникла помилка, оскільки в кожному з цих випадків потрібно, щоб це означало різні речі.
  5. Експортуйте результати після завершення виконання навантаження.
simaai::neat::GraphReport report = graph.validate();
std::cout << report.to_json() << "\n";

auto run = graph.build();

simaai::neat::Sample sample;
simaai::neat::PullError error;

switch (run.pull("classes", /*timeout_ms=*/1000, sample, &error)) {
case simaai::neat::PullStatus::Ok:
// Use sample.
break;
case simaai::neat::PullStatus::Timeout:
// No output arrived before the timeout.
break;
case simaai::neat::PullStatus::Closed:
// End of stream. Stop draining.
break;
case simaai::neat::PullStatus::Error:
std::cerr << error.code << ": " << error.message << "\n";
if (error.report) {
std::cerr << error.report->repro_note << "\n";
}
break;
}

Зберіть докази на підтвердження.

Коли в застосунку виникає помилка в графі, збережіть найменший пакет даних, який пояснює зовнішню поведінку. Зробіть це перед зміною параметрів. Факти переконливіші за домисли.

Включіть:

  • назва моделі-артефакту та спосіб її створення;
  • Інформація про версію/збірку Neat;
  • форма вхідних даних, тип даних, розміщення, формат пікселів і сімейство корисних даних;
  • graph.validate().to_json() у разі невдалої збірки або перевірки;
  • run.input_names() та run.output_names() у разі збоїв кінцевих точок;
  • здійсніть експорт JSON після виконання, коли принаймні один зразок було оброблено, якщо проблема пов’язана з поведінкою в середовищі виконання;
  • файл MeasureReport у форматі JSON або текстовий файл, коли проблема пов’язана з пропускною здатністю, затримкою або енергоспоживанням;
  • найменший робочий фрагмент коду, який відтворює відповідну поведінку.

Python може збирати інформацію про версію та безпосередньо запускати тестові приклади:

print(pyneat.build_info())

report = graph.validate()
with open("graph-report.json", "w", encoding="utf-8") as f:
f.write(report.to_json())

# After samples have moved through the run:
run.save_json("run-after.json")

C++ може експортувати ті самі дані у форматі GraphReport::to_json() та save_run_json(...):

std::cout << "neat_version=" << sima_neat_version() << "\n";
std::cout << graph.validate().to_json() << "\n";

// After samples have moved through the run:
simaai::neat::save_run_json(run, "run-after.json");

Якщо помилка виникає лише під час навантаження, додайте експортовані дані вимірювання замість знімка, зробленого під час збірки. Експорт, зроблений під час збірки, показує, що створив Neat; експорт, зроблений після виконання, показує, що сталося, коли граф обробляв реальні вхідні дані.

У разі виникнення помилок, перехопіть NeatError і прочитайте структурований звіт:

try {
auto run = graph.build();
} catch (const simaai::neat::NeatError& error) {
const auto& report = error.report();
std::cerr << report.error_code << "\n";
std::cerr << report.repro_note << "\n";
}

Вирішення проблем, пов’язаних із низькою швидкістю або відсутністю вихідних даних.

Якщо пропускна здатність низька або вихідні дані зникають, спочатку перевірте наступне:

  1. Чи створюєте ви граф у межах вимірюваного циклу?
  2. Ви передаєте один вхідний сигнал, чекаєте, поки весь граф перейде в неактивний стан, а потім передаєте наступний?
  3. Чи постійно застосунок отримує нові дані?
  4. Чи блокує одна гілка вихідних даних увесь граф?
  5. Чи зберігаєте ви дані, отримані за допомогою механізму «нульового копіювання» або в середовищі виконання, надто довго?
  6. Чи недостатньо великі черги, щоб згладити коливання, чи надто великі, щоб спричинити зворотний тиск?
  7. Чи чітко визначено правила щодо перевищення допустимих навантажень?
  8. Чи підраховуються втрачені дані через on_input_drop чи через локальні збої try_push(...)?
  9. Чи генерує кожен очікуваний stream_id вихідні дані в межах визначеного часового інтервалу?
  10. Чи містяться в основному циклі операції запису в журнали, перевірки декодування, операції введення-виведення файлів або експорт звітів?

Спочатку усуньте неточності. Потім прискорте процес. А потім доведіть, що саме ви виміряли.

Див. також