Ultralytics YOLO27: Встать в лист ожидания.
Get Started
Ultralytics YOLO в продакшене·Масштаб и наблюдение·Урок 7/9
Уроксредний

Инференс по нескольким потокам

Обрабатывай видео с нескольких камер одной моделью с помощью пакетного инференса, ограниченных очередей и измерения потерь кадров.

Система, которая справляется с одной камерой, при подключении четырёх может отставать на несколько минут — и при этом ничего не падает: результаты просто приходят всё позже, пока не закончится память. На ноутбуке с CPU, использованном здесь, система справлялась с одним потоком 30 FPS, при четырёх потоках теряла половину кадров, а при восьми — больше двух третей. В этом уроке мы построим конвейер для измерения этого показателя: он поможет подобрать оборудование.

Результат

Обрабатывай два или более потоков одной моделью, сообщай число обработанных и потерянных кадров для каждого потока и найди количество потоков, с которым твоё оборудование перестаёт справляться.

Ускоренное прохождение
Если ты уже освоился, вот краткая версия.
  1. Встроенный вариант: перечисли источники в файле .streams и вызови model.track("cams.streams", stream=True).

  2. stream_buffer=False (по умолчанию) оставляет только самый новый кадр для каждого потока; True обрабатывает каждый кадр записанных видео.

  3. Собственный вариант: потоки декодирования, одна ограниченная очередь и один цикл-потребитель, которому принадлежат модель и пакетная обработка.

  4. Считай потерянные кадры для каждого потока; пропускная способность — это наибольшее количество потоков без потерь при заданном FPS.

Практические занятия

Куда уходит время#

NVIDIA DeepStream запускает Ultralytics YOLO на четырёх видеопотоках с камер на Jetson примерно при 12 FPS на поток; рядом с сеткой детекций 2×2 показаны логи PERF для каждого потока

Каждый кадр проходит четыре этапа:

ЭтапТипичные затраты времениГде выполняется
Декодирование (RTSP или файл → массив)5–20 мс на кадр 1080pCPU
Предобработка (letterbox, нормализация)около 1 мсCPU
Инференсот нескольких мс (GPU) до десятков мс (CPU)Ускоритель
Постобработка, отслеживание, приёмники результатовнесколько мс, больше при медленной работе баз данныхCPU

Запусти эти этапы последовательно, обрабатывая по одной камере в одном потоке, — и ускоритель будет простаивать, пока декодируются кадры. Решение — разделить этапы, как сделано в конвейере DeepStream выше: четыре потока используют одно устройство, а для каждого показывается собственный FPS (здесь около 12). Аналогичная структура на Python:

Встроенный вариант: файл .streams#

Ultralytics пакетно обрабатывает несколько источников за тебя. Добавь по одному источнику в строку файла .streams и передай его в режим Predict или Track. Для каждого потока создаются собственные поток чтения и трекер, а на каждой итерации выполняется один прямой проход с пакетной обработкой всех потоков:

from collections import Counter
from pathlib import Path

from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download

base = "https://github.com/ultralytics/assets/releases/download/v0.0.0/"
clips = [safe_download(base + name) for name in ("solutions_ci_demo.mp4", "ground-vehicles-airport.mp4")]
Path("cams.streams").write_text("\n".join(str(c) for c in clips))  # продакшен: один URL rtsp:// в каждой строке

model = YOLO("yolo26n.pt")  # используй в продакшене оптимизированный экспорт модели
frames = Counter()
for r in model.track("cams.streams", stream=True, stream_buffer=True, tracker="bytetrack.yaml", verbose=False):
    frames[Path(r.path).name] += 1  # r.path указывает, из какого потока пришёл этот результат
print(frames)
Counter({'solutions_ci_demo.mp4': 62, 'ground-vehicles-airport.mp4': 62})

Оба потока выдали по 62 результата, потому что итерация останавливается, когда завершается самый короткий поток (в ролике из аэропорта 150 кадров). stream_buffer=True обработал каждый кадр — это подходит для записанных видео; для камер в реальном времени оставь значение False по умолчанию, чтобы медленная модель пропускала устаревшие кадры, а не отставала. Для максимальной пропускной способности каждого потока используй экспорт в TensorRT или OpenVINO.

Собственный вариант: производитель-потребитель со счётчиками потерь#

Три диаграммы времени: однопоточный процесс последовательно ждёт завершения каждого запроса ввода-вывода; многопоточный процесс перекрывает ожидание ввода-вывода, но в каждый момент обрабатывает данные только поток, удерживающий GIL; asyncio перекрывает ожидание в одном потоке

Диаграмма показывает, почему в этом конвейере работают потоки. Декодирование в основном состоит из ожидания (розовые полосы ввода-вывода), а OpenCV и PyTorch освобождают GIL Python во время работы, поэтому потоки декодирования хорошо перекрываются (панель b). Интенсивная постобработка на Python не перекрывается; перенеси её в отдельные процессы.

Если тебе нужны собственные приёмники результатов, логика для каждой камеры или явные метрики потерь, собери конвейер самостоятельно. Потоки декодирования не обращаются к модели; модель принадлежит одному циклу-потребителю, что также позволяет соблюдать правила потокобезопасного инференса:

import queue
import threading
import time

import cv2

from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download

base = "https://github.com/ultralytics/assets/releases/download/v0.0.0/"
SOURCES = [str(safe_download(base + n)) for n in ("solutions_ci_demo.mp4", "ground-vehicles-airport.mp4")]
frames = queue.Queue(maxsize=16)  # bounded: memory and latency stay flat under overload
dropped = [0] * len(SOURCES)

def decode(stream_id, source):
    """Producer: read frames at the camera's pace and never block on a full queue."""
    cap = cv2.VideoCapture(source)
    interval = 1 / (cap.get(cv2.CAP_PROP_FPS) or 30)
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        try:
            frames.put_nowait((stream_id, frame))
        except queue.Full:
            dropped[stream_id] += 1  # consumer is behind: drop rather than serve stale frames
        time.sleep(interval)  # pace files like a live camera; remove for RTSP sources
    cap.release()

model = YOLO("yolo26n.pt")  # one model, used only by the consumer loop below
model.predict("https://ultralytics.com/images/bus.jpg", verbose=False)  # warm up before cameras start

decoders = [threading.Thread(target=decode, args=(i, s), daemon=True) for i, s in enumerate(SOURCES)]
for t in decoders:
    t.start()

processed = [0] * len(SOURCES)
while any(t.is_alive() for t in decoders) or not frames.empty():
    batch = []
    while len(batch) < 8:  # gather up to 8 frames from any camera
        try:
            batch.append(frames.get(timeout=0.05))
        except queue.Empty:
            break
    if not batch:
        continue
    results = model([frame for _, frame in batch], verbose=False)  # one batched forward pass
    for (stream_id, _), r in zip(batch, results):
        processed[stream_id] += 1  # per-stream postprocessing and sinks go here

print(f"processed={processed} dropped={dropped}")
processed=[56, 131] dropped=[6, 19]

В двух роликах 62 и 150 кадров, и каждый кадр либо обработан, либо потерян (56 + 6 = 62, 131 + 19 = 150), так что счётчики учитывают все кадры. Потеряно 25 из 212 кадров: CPU этого ноутбука не справляется с yolo26n в PyTorch при обработке двух потоков 30 FPS. Вот один прогон с повторением роликов для имитации работы большего числа камер:

Потоки (по 30 FPS каждый)Потерянные кадры
10%
212%
451%
869%

Подбирай пропускную способность по результатам измерений#

Потоки на устройство ≈ устойчивая пропускная способность (число кадров в секунду, обрабатываемых моделью при заданном размере пакета) ÷ требуемый FPS для одного потока. Измерь пропускную способность на реальном оборудовании с оптимизированным экспортом из предыдущих уроков, оставь запас 20–30% на пиковую нагрузку и настрой оповещение, если появляются потери. Затраты на более высокое разрешение примерно пропорциональны числу пикселей: обработка 1280×1280 требует примерно в 4 раза больше вычислений, чем 640×640. Для больших парков устройств естественными решениями для продакшена будут Triton Inference Server и DeepStream на Jetson.

Подводные камни#

Передача списка видео на Python

Симптом: model.track(["cam1.mp4", "cam2.mp4"]) воспринимает список как пакет изображений, а не как потоки. Решение: запиши источники в файл .streams, по одному на строку.

Параметр stream_buffer по умолчанию для записанных файлов

Симптом: из каждого файла получается всего один-два кадра. Решение: stream_buffer=False отбрасывает устаревшие кадры, а чтение файлов опережает модель; для файлов задай stream_buffer=True, а для потоковых камер оставь значение по умолчанию.

Совместное использование одной модели в потоках инференса

Симптом: под нагрузкой иногда появляются неверные или перепутанные результаты. Решение: никогда не вызывай один экземпляр YOLO одновременно из нескольких потоков. Назначь его одному обработчику, как описано выше, или создай модель внутри каждого потока инференса.

Постановка каждого кадра в очередь

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

Попробовать

Запусти producer_consumer.py со своими источниками (или двумя демонстрационными роликами), затем с помощью скрипта решения увеличивай число потоков, пока не появятся потери. Представь таблицу с числом потоков и процентом потерянных кадров на своём оборудовании, а также наибольшее число потоков без потерь при целевом FPS.

Критерии готовности
Урок считается завершённым, если выполнены все условия ниже.
  • Два или более потока обрабатываются одной моделью, и для каждого результата указан исходный поток.

  • В конвейере используется ограниченная очередь, а число обработанных и потерянных кадров для каждого потока в сумме равно числу прочитанных кадров.

  • Ты можешь назвать измеренное число потоков, при котором оборудование начинает терять кадры.

Показать решение

Сделай число потоков аргументом командной строки: добавь эти строки в producer_consumer.py сразу после строки SOURCES:

import sys

N = int(sys.argv[1]) if len(sys.argv) > 1 else 2  # число имитируемых камер
SOURCES = [SOURCES[i % len(SOURCES)] for i in range(N)]

Теперь проверь разные значения:

for n in 1 2 4 8; do python producer_consumer.py $n; done
processed=[62] dropped=[0]
processed=[56, 131] dropped=[6, 19]
processed=[23, 82, 23, 81] dropped=[39, 68, 39, 69]
processed=[14, 53, 13, 53, 13, 52, 13, 53] dropped=[48, 97, 49, 97, 49, 98, 49, 97]

Для этой машины ответ — один поток: потери начинаются при двух. Чтобы обслуживать четыре камеры, экспортируй модель в более быстрый runtime (уроки 3 и 4), уменьши imgsz или используй ускоритель, а затем повтори проверку.

Что дальше

Теперь мы можем масштабировать систему. Дальше — научимся достаточно хорошо следить за ней, чтобы замечать незаметное ухудшение работы.

Войди, чтобы сохранять прогресс