В предыдущей статье я описал процесс компиляции модели yolo8n в HEF-файл для нейрочипа HAILO-8L в модуле HAT+. В этой работе я оцениваю быстродействие инференса нескольких моделей YOLO для этого же чипа.

Задумка

Расскажу немного о поставленной задаче, для которой все это и задумывалось. Я решил сделать систему видеонаблюдения с сервером на базе малинки с использованием своей нейросети. Да, производители уже давно встраивают в свои видеокамеры наблюдения алгоритмы, распознающие людей и движения в кадре. Но после некоторого опыта использования устройств Xiaomi я сделал вывод, что мне не нравится работа встроенных нейронок: людей иногда не детектирует, а срабатывания на движения слишком частые (ветер двигает ветки, траву, насекомые летают и тд.). К тому же все видеоданные идут через китайские сервера, из-за этого возникают большие задержки записи в NAS. Появилась идея сделать стандартную систему наблюдения со своим сервером, но внести немного своей лепты в виде личной нейронки и обработки детекций с записью и доп сигналами. Если это будет интересно, то может еще что-то напишу о развитии этой задумки. А пока выбираем нейронку для детекции.

Задача

Берем несколько моделей YOLO и пробуем скомпилировать для них соответствующие бинарники для имеющегося нейрочипа малинки для оценки быстродействия их инференса, чтобы:

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

  2. Понимать возможности нейрочипа для нескольких разных моделей.

Я решил взять следующий пул моделей:

  • YOLO8n

  • YOLO8s

  • YOLO10n

  • YOLO10s

  • YOLO12n

  • YOLO12s

  • YOLO26n

  • YOLO26s

Компиляция HEF

В момент проведения работ я решил взять модели, экспортированные в onnx формат с разрешением входа (640, 640). В будущем для видеонаблюдения буду брать (640, 480). В данный момент это не критично. Действуя по алгоритму из предыдущей статьи с небольшими изменениями, готовим конфигурационные файлы, необходимые для компиляции.

  1. Сначала получаем первичные 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 серии пришлось отказаться и двигаться дальше с шестью оставшимися.

  1. После получения 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)  
  1. Собственно, сама компиляция в целом такая (пример для 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 без необходимости запускать каждую модель с ручным замером скорости. Я решил протестировать оба способа и заодно посмотреть разницу между ними.

  1. Первым опробуем метод с hailort. Для этого в консоли среды просто даем необходимую команду и прописываем наш скомпилированный HEF каждой модели. (На названия моделей не обращаем внимания, это моя личная индексация для распознавания версий). И вот какие результаты я получил:

  1. Затем ради интереса сделаем инференс самостоятельно и вручную измерим время. Заодно сравним расхождения с бенчмарком движка. В качестве разогрева чипа для инференса устроил ему цикл из 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:

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

Комментарии (0)