В предыдущей статье я описал процесс компиляции модели yolo8n в HEF-файл для нейрочипа HAILO-8L в модуле HAT+. В этой работе я оцениваю быстродействие инференса нескольких моделей YOLO для этого же чипа.
В предыдущей статье я описал процесс компиляции модели yolo8n в HEF-файл для нейрочипа HAILO-8L в модуле HAT+. В этой работе я оцениваю быстродействие инференса нескольких моделей YOLO для этого же чипа.
ЗадумкаРасскажу немного о поставленной задаче, для которой все это и задумывалось. Я решил сделать систему видеонаблюдения с сервером на базе малинки с использованием своей нейросети. Да, производители уже давно встраивают в свои видеокамеры наблюдения алгоритмы, распознающие людей и движения в кадре. Но после некоторого опыта использования устройств Xiaomi я сделал вывод, что мне не нравится работа встроенных нейронок: людей иногда не детектирует, а срабатывания на движения слишком частые (ветер двигает ветки, траву, насекомые летают и тд.). К тому же все видеоданные идут через китайские сервера, из-за этого возникают большие задержки записи в NAS. Появилась идея сделать стандартную систему наблюдения со своим сервером, но внести немного своей лепты в виде личной нейронки и обработки детекций с записью и доп сигналами. Если это будет интересно, то может еще что-то напишу о развитии этой задумки. А пока выбираем нейронку для детекции.
ЗадачаБерем несколько моделей YOLO и пробуем скомпилировать для них соответствующие бинарники для имеющегося нейрочипа малинки для оценки быстродействия их инференса, чтобы:
Выяснить соответствие закладываемых возможностей, а именно: сколько потоков видео сможет обрабатывать нейрочип в режиме реального времени, и сможет ли вообще тянуть больше одного.
Понимать возможности нейрочипа для нескольких разных моделей.
Я решил взять следующий пул моделей:
YOLO8n
YOLO8s
YOLO10n
YOLO10s
YOLO12n
YOLO12s
YOLO26n
YOLO26s
В момент проведения работ я решил взять модели, экспортированные в 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, который был под рукой, а конкретно из тестового набора. Название картинок обозначают количество детектируемых объектов на изображении, чтобы иметь дополнительное понимание зависимости времени инференса от количества объектов и есть ли вообще подобная корреляция. Результат вот:
РезультатыИнференс через встроенный бенчмарк:
8n: 7.85 ms; 8s: 17.00 ms
10n: 10.95 ms; 10s: 22.23 ms
26n: 13.97 ms; 26s: 29.39 ms
Инференс через ручной запуск (среднее по шести фрэймам):
8n: 10.82 ms; 8s: 19.57 ms
10n: 14.10 ms; 10s: 23.51 ms
26n: 16.89 ms; 26s: 30.09 ms
Расхождения есть. В ручном режиме ко времени инференса прибавляется еще время доставки данных к чипу. При встроенном бенчмарке это не входит в рассчет, там только чистый пробег сквозь чип. Стоит еще добавить, что модели 8 семейства требуют в нагрузку еще процесс NMS для рамок, что отражено в alls конфиге (присутствует в предыдущей статье), следовательно время инференса дополняется еще и этим процессом, поэтому для 8 семейства прибавка ко времени самая ощутимая.
Для моей задачи нужно попробовать параллельно обрабатывать 2 или 3 потока. Несомненно, узким бутылочным горлышком станет предобработка и постобработка каждого обрабатываемого кадра. И, судя по времени инференса, работать дальше предстоит с nano версиями представленных моделей.
PS.Провел еще один опыт добавочный. Так как не могу запустить инференс на старших чипах семейства Hailo, таких как 8 и 10 из-за отсутствия оных, я попробовал схитрить и допустил, что возможно benchmark на движке hailort смоделирует это без железа. Скомпилировал HEF-файл для yolo8n под чип Hailo-8 (а напомню, что у меня самый младший Hailo-8L) и попробовал запустить benchmark:

Обмануть систему не удалось, всем хорошего дня =)
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Компиляция yolov8n в формат HEF для Hailo-8L на Raspberry Pi 5 | 0 | 9.95 | 21-06-2026 |
| 2 | fast-volleyball-tracking-inference — детектор волейбольного мяча на скорости 80 fps (CPU). Или «не YOLO единым» | 0 | 11.65 | 19-05-2026 |
| 3 | YOLOv11 и RF-DETR для object detection в видео. | 0 | 11.7 | 03-04-2026 |
| 4 | Хакатон: как мы выжали максимум из YOLO и заняли 2 место | 0 | 12.78 | 17-01-2026 |
| 5 | Масштабируемость ML-алгоритмов при увеличении вычислительных ресурсов | 0 | 17.69 | 04-06-2026 |
| 6 | ESP32 + MicroPython + трубка Гейгера: как за вечер собрать детектор радиации | 0 | 10.84 | 10-08-2026 |
| 7 | Nano Banana Pro. Реальное применение, а не мемные картинки | 0 | 7.9 | 10-02-2026 |
| 8 | Как я свертки ускорял | 0 | 7 | 28-06-2026 |
| 9 | Снимаем показания счётчика электроэнергии МИР С-05.10 c помощью Raspberry по Bluetooth | 0 | 8.98 | 30-03-2026 |
| 10 | Scaling NumPy on Free-Threaded Python | 0 | 14.69 | 09-08-2026 |