В предыдущей статье я описал процесс компиляции модели yolo8n в HEF-файл для нейрочипа HAILO-8L в модуле HAT+. В этой работе я оцениваю быстродействие инференса нескольких моделей YOLO для этого же чипа.
Задумка
Расскажу немного о поставленной задаче, для которой все это и задумывалось. Я решил сделать систему видеонаблюдения с сервером на базе малинки с использованием своей нейросети. Да, производители уже давно встраивают в свои видеокамеры наблюдения алгоритмы, распознающие людей и движения в кадре. Но после некоторого опыта использования устройств Xiaomi я сделал вывод, что мне не нравится работа встроенных нейронок: людей иногда не детектирует, а срабатывания на движения слишком частые (ветер двигает ветки, траву, насекомые летают и тд.). К тому же все видеоданные идут через китайские сервера, из-за этого возникают большие задержки записи в NAS. Появилась идея сделать стандартную систему наблюдения со своим сервером, но внести немного своей лепты в виде личной нейронки и обработки детекций с записью и доп сигналами. Если это будет интересно, то может еще что-то напишу о развитии этой задумки. А пока выбираем нейронку для детекции.
Задача
Берем несколько моделей YOLO и пробуем скомпилировать для них соответствующие бинарники для имеющегося нейрочипа малинки для оценки быстродействия их инференса, чтобы:
Выяснить соответствие закладываемых возможностей, а именно: сколько потоков видео сможет обрабатывать нейрочип в режиме реального времени, и сможет ли вообще тянуть больше одного.
Понимать возможности нейрочипа для нескольких разных моделей.
Я решил взять следующий пул моделей:
YOLO8n
YOLO8s
YOLO10n
YOLO10s
YOLO12n
YOLO12s
YOLO26n
YOLO26s
Компиляция HEF
В момент проведения работ я решил взять модели, экспортированные в onnx формат с разрешением входа (640, 640). В будущем для видеонаблюдения буду брать (640, 480). В данный момент это не критично. Действуя по алгоритму из предыдущей статьи с небольшими изменениями, готовим конфигурационные файлы, необходимые для компиляции.
Сначала получаем первичные har файлы для каждой модели. Для этого каждую модель прогоняем через netron.app и берем оттуда названия конечных узлов. Образец ниже:
from hailo_sdk_client import ClientRunner TARGET_CHIP = "hailo8l" END_NODES = [ "/model.22/cv2.0/cv2.0.2/Conv", "/model.22/cv3.0/cv3.0.2/Conv", "/model.22/cv2.1/cv2.1.2/Conv", "/model.22/cv3.1/cv3.1.2/Conv", "/model.22/cv2.2/cv2.2.2/Conv", "/model.22/cv3.2/cv3.2.2/Conv", ] print("Формирование har") runner = ClientRunner(hw_arch=TARGET_CHIP) runner.translate_onnx_model( "yolo8n.onnx", end_node_names=END_NODES ) print("Сохранение har файла модели") runner.save_har("yolo8n.har")
Проделывая так для каждой модели по очереди я обнаружил, что у моделей семейства 12n и 12s узлы reshape и transpose пронизывают всю модель, встречаясь в архитектуре уже от самой середины. А надо помнить, что эти операции наш нейрочип не переваривает. Для этого мы в прошлой статье в модели 8 серии отсекали их, оставляя для нейрочипа все, что им предшествует. Почитав немного дополнительной информации, я пришел к выводу, что наш Hailo чип не сможет переварить именно это 12 семейство, так как разработчики этой серии сделали совсем иную архитектуру модели, взяв за основу блоки внимания в качестве подобия трансформеров, в результате чего и появилось много таких неперевариваемых чипом узлов. Поэтому от моделей 12 серии пришлось отказаться и двигаться дальше с шестью оставшимися.
После получения HAR файлов прогоняем их снова через netron.app и дополняем наши конфиги новыми именами конечных узлов, а также готовим конфиги alls. Для 8 серии делаем еще один конфиг json, его дублировать не буду, в предыдущей статье есть пример. Для 10 и 26 серии ниже приведу alls, в котором указываем имена конечных блоков классификации. А json для них не нужен.
nomalization1 = normalization([0.0, 0.0, 0.0], [255.0, 255.0, 255.0]) change_output_activation(conv48, sigmoid) change_output_activation(conv59, sigmoid) change_output_activation(conv69, sigmoid)
nomalization1 = normalization([0.0, 0.0, 0.0], [255.0, 255.0, 255.0]) change_output_activation(conv64, sigmoid) change_output_activation(conv80, sigmoid) change_output_activation(conv94, sigmoid)
Собственно, сама компиляция в целом такая (пример для 10n):
import numpy as np from PIL import Image from hailo_sdk_client import ClientRunner from ultralytics.utils import DATASETS_DIR from ultralytics.data.utils import check_det_dataset model = "yolo10n.onnx" MODEL = 'yolo10n' CALIB_IMAGES = 128 TARGET_CHIP = "hailo8l" END_NODES = [ "/model.23/one2one_cv3.0/one2one_cv3.0.2/Conv", "/model.23/one2one_cv3.1/one2one_cv3.1.2/Conv", "/model.23/one2one_cv3.2/one2one_cv3.2.2/Conv", "/model.23/one2one_cv2.0/one2one_cv2.0.2/Conv", "/model.23/one2one_cv2.1/one2one_cv2.1.2/Conv", "/model.23/one2one_cv2.2/one2one_cv2.2.2/Conv", ] print("Формирование har") runner = ClientRunner(hw_arch=TARGET_CHIP) runner.translate_onnx_model("yolo10n.onnx", end_node_names=END_NODES) print("Загружаем alls") runner.load_model_script("yolo10n.alls") print("Создаем датасет") check_det_dataset("coco128.yaml") calib_dir = DATASETS_DIR/"coco128"/"images"/"train2017" image_files = list(calib_dir.glob("*.jpg")) + list(calib_dir.glob("*.png")) if not image_files: raise FileNotFoundError(f"No calibration images found in {calib_dir}") calibset = np.zeros((CALIB_IMAGES, 640, 640, 3), dtype=np.float32) for i in range(CALIB_IMAGES): img = Image.open(np.random.choice(image_files)).convert("RGB").resize((640, 640)) calibset[i] = np.array(img, dtype=np.float32) print("Оптимизируем веса к датасету") runner.optimize(calibset) print("Сохранение оптимизированного har-файла") runner.save_har(f"{MODEL}.o.har") print("Компиляция hef-файла") hef = runner.compile() with open(f"{MODEL}.hef", "wb") as f: f.write(hef)
Как итог, получаем HEF-файлы для каждой модели и перекидываем их на Raspberry Pi 5.
Получаем время инференса
Есть такая интересная штука, как получение значений быстродействия инференса с помощью движка Hailort Cli без необходимости запускать каждую модель с ручным замером скорости. Я решил протестировать оба способа и заодно посмотреть разницу между ними.
Первым опробуем метод с hailort. Для этого в консоли среды просто даем необходимую команду и прописываем наш скомпилированный HEF каждой модели. (На названия моделей не обращаем внимания, это моя личная индексация для распознавания версий). И вот какие результаты я получил:


Затем ради интереса сделаем инференс самостоятельно и вручную измерим время. Заодно сравним расхождения с бенчмарком движка. В качестве разогрева чипа для инференса устроил ему цикл из 110 итераций на каждую картинку каждой модели. Так сказать, 100 разогревочных и 10 контрольных, по которым потом вычисляется среднее.
import os import numpy as np from hailo_platform import ( HEF, ConfigureParams, FormatType, HailoStreamInterface, InferVStreams, InputVStreamParams, OutputVStreamParams, VDevice, ) from time import perf_counter from PIL import Image, ImageDraw from collections import defaultdict, deque # Configuration MODELS_DIR = "./hef/" IMAGES_DIR = "./images/" models = sorted([m for m in os.listdir(MODELS_DIR) if m.endswith(".hef")]) imgs = sorted([i for i in os.listdir(IMAGES_DIR) if i.endswith(".jpg")]) IMGSZ = (640, 640) CONF = 0.25 params = VDevice.create_params() target = VDevice(params) for model in models: timings = defaultdict(lambda: deque(maxlen=10)) # Load HEF and connect to device hef = HEF(os.path.join(MODELS_DIR, model)) configure_params = ConfigureParams.create_from_hef(hef, interface=HailoStreamInterface.PCIe) network_groups = target.configure(hef, configure_params) network_group = network_groups[0] network_group_params = network_group.create_params() # Setup I/O virtual streams input_vstreams_params = InputVStreamParams.make(network_group, quantized=False, format_type=FormatType.FLOAT32) output_vstreams_params = OutputVStreamParams.make(network_group, quantized=False, format_type=FormatType.FLOAT32) input_name = hef.get_input_vstream_infos()[0].name # Inference with InferVStreams(network_group, input_vstreams_params, output_vstreams_params) as pipeline: with network_group.activate(network_group_params): for idx, img in enumerate(imgs): # Preprocess orig = Image.open(os.path.join(IMAGES_DIR, img)).convert("RGB") ow, oh = orig.size resized = orig.resize((IMGSZ[0],IMGSZ[1])) input_data = np.expand_dims(np.array(resized, dtype=np.float32), axis=0) # (1,640,640,3) for i in range(110): start_time = perf_counter() raw = pipeline.infer({input_name: input_data}) inf_time = (perf_counter() - start_time) * 1000 timings[img].append(inf_time) output_key = next(iter(raw.keys())) batch_dets = raw[output_key][0] # shape: (num_classes, max_dets, 5) print("*" * 150) print(f"Model: {model}") print("*" * 150) print(" ", end=" ") for img in imgs: print(f"{img}", end=" " * (20 - len(img))) print("") print("avg_inf_time(ms) = ", end=" ") for img in imgs: tm = sum(timings[img]) / 10 print(f"{tm:.2f}", end=" " * 15) print("\n\n\n\n")
В подопытные изображения взял несколько примеров из датасета VisDrone, который был под рукой, а конкретно из тестового набора. Название картинок обозначают количество детектируемых объектов на изображении, чтобы иметь дополнительное понимание зависимости времени инференса от количества объектов и есть ли вообще подобная корреляция. Результат вот:

Итог
Расхождения есть, где-то посильнее, где-то практически одинаково. Стоит добавить, что модели 8 семейства требуют в нагрузку еще NMS рамок, что отражено в alls конфиге (присутствует в предыдущей статье), поэтому время инференса дополняется этим процессом и в случае с моделью yolo8n увеличивается на 3мс.
Для моей задачи нужно попробовать параллельно обрабатывать 2 или 3 потока. Несомненно, узким бутылочным горлышком станет предобработка и постобработка каждого обрабатываемого кадра. И, судя по времени инференса, работать дальше предстоит с nano версиями представленных моделей.
PS.
Провел еще один опыт добавочный. Так как не могу запустить инференс на старших чипах семейства Hailo, таких как 8 и 10 из-за отсутствия оных, я попробовал схитрить и допустил, что возможно benchmark на движке hailort смоделирует это без железа. Скомпилировал HEF-файл для yolo8n под чип Hailo-8 (а напомню, что у меня самый младший Hailo-8L) и попробовал запустить benchmark:

Обмануть систему не удалось, всем хорошего дня =)