Ultralytics Hailo 推理后端 HailoBackend:HEF 模型加载与七任务主机端解码实现
【免费下载链接】ultralyticsUltralytics YOLO26, YOLO11, YOLOv8 — object detection, instance segmentation, semantic segmentation, image classification, pose estimation, object tracking项目地址: https://gitcode.com/GitHub_Trending/ul/ultralytics
本篇技术指南以 ultralytics/nn/backends/hailo.py 中的HailoBackend类为核心,系统讲解 Ultralytics YOLO 在 Hailo AI 加速器上的推理链路:从 HailoRT 推理管线的构建、导出目录中metadata.yaml元数据的读取与应用,到 detect、segment、pose、obb、classify、semantic、depth 七类任务各自的主机端(host-side)输出解码逻辑。读完本文,你可以理解model = YOLO("yolo11n_hailo_model")背后发生了什么,并能判断自训练模型导出的 HEF 为何能(或不能)被predict与val直接运行。
HailoBackend 在推理架构中的位置
HailoBackend是 ultralytics/nn/backends/ 后端族的一员,继承自 BaseBackend 抽象基类。所有后端共享同一套接口契约:构造时调用子类load_model(weight)完成模型加载,forward(im)接收BCHW 布局、归一化到 [0, 1] 的输入张量,返回原始输出,由上层 Predictor 完成 NMS、可视化等后处理(见 base.py 中的抽象方法定义)。
后端的选择由 AutoBackend 按模型后缀自动完成:*_hailo_model/目录被映射到hailo格式,即HailoBackend(autobackend.py 的后端注册表)。因此在 Export mode 导出 HEF 之后,无需任何额外胶水代码,YOLO()构造函数即可透明地接管推理:
from ultralytics import YOLO model = YOLO("yolo11n_hailo_model") # 自动识别为 hailo 格式,实例化 HailoBackend results = model.predict("path/to/image.jpg")HailoBackend的设计要点是“硬件只做计算,解码放在主机”:HEF 中编译的是量化后的 YOLO 网络本体(部分任务附带片上后处理),而 box 解码、关键点坐标恢复、角度还原、深度标定等还原工作全部在 hailo.py 的主机端代码中完成,从而保证 HEF 结果与 PyTorch 模型在同一评估协议下对齐。
模型加载:load_model 构建 HailoRT 推理管线
load_model接收的参数是一个导出目录而非单文件,这是与多数单文件后端(.engine、.onnx)的显著差异(hailo.py#L20-L70)。其执行顺序如下:
- 导入 HailoRT。延迟导入
hailo_platform包中的HEF、VDevice、InferVStreams等符号;缺少时抛出ImportError并提示安装 HailoRT——注意这与导出侧依赖的 Hailo Dataflow Compiler(DFC)是两套独立组件,目标设备上只需要 HailoRT。 - 定位 HEF 文件。在目录内用
rglob("*.hef")递归查找第一个.hef文件,找不到即抛FileNotFoundError。 - 读取并应用元数据。
self.read_metadata(hef_file)实现在 BaseBackend.read_metadata:.hef不属于任何嵌入式元数据格式,于是走 sidecar 分支——读取与 HEF 同目录的metadata.yaml并通过apply_metadata逐字段写入实例属性(task、names、imgsz、stride、kpt_shape等)。这就是部署时必须把metadata.yaml与 HEF 放在一起的原因:缺少它,后端将无法选择正确的解码路径。 - 任务白名单校验。仅允许
detect、segment、pose、obb、classify、semantic、depth七类任务,其余任务直接ValueError(hailo.py#L46-L50)。 - 构建 HailoRT 推理管线。依次执行:
HEF(hef_file)打开模型 → 取第一个输入 vstream 信息与全部输出 vstream 信息 → 在ExitStack上下文中完成VDevice打开、ConfigureParams.create_from_hef(hef, interface=HailoStreamInterface.PCIe)、network_group.configure/activate,并以InputVStreamParams(FormatType.UINT8)、OutputVStreamParams(FormatType.FLOAT32)创建InferVStreams推理句柄。整个上下文栈被stack.pop_all()保存为self._stack,由析构函数__del__统一关闭,确保设备资源可靠释放。 - 任务相关初始化。segment/pose/obb 三类任务会实例化
DFL(Distribution Focal Loss 反卷积解码器,来自 ultralytics/nn/modules/)供 box 解码使用;同时设置self.end2end = task not in {"segment", "pose", "obb"}——注释说明 segmentation/pose/OBB 返回的是密集张量,需要交给 Predictor 的 NMS,而 detect 与 classify 返回已裁剪的结果。
两点值得注意的实现细节:
- 输入接口固定为PCIe 模式(
HailoStreamInterface.PCIe,hailo.py#L57)。从源码结构看,该后端面向通过 PCIe 总线连接 Hailo 加速卡的 ARM/x86 主机(如 Raspberry Pi AI HAT+ 系列),输入以 uint8 送入、输出以 float32 取回,归一化(/255)被固化进编译期指令,主机侧只需还原。 - 输入几何信息来自
self.input_info.shape(即 HEF 编译时的固定分辨率),后续所有 anchor 推导都以它为基准,这与 Hailo 不支持动态 shape 的约束一致。
推理入口:forward 的统一预处理与任务分派
forward(hailo.py#L77-L100)把基类约定的 BCHW 浮点输入转换为 HailoRT 期望的格式后一次性送卡:
im = np.ascontiguousarray(np.clip(im.permute(0, 2, 3, 1).cpu().numpy() * 255, 0, 255).astype(np.uint8)) results = self.model.infer({self.input_info.name: im}) outputs = [results[x.name] for x in self.output_infos]预处理做三件事:转置为NHWC、乘 255 并裁剪回 [0, 255]、转为连续的uint8数组(ascontiguousarray避免 vstream 拷贝失败)。infer以“输入 vstream 名 → 数组”的字典调用,返回按output_infos顺序排列的 NumPy 输出列表。随后按self.task分派到七个解码分支:segment/pose/obb走各自的_decode_*方法,classify直接透传片上 softmax 概率,semantic依据元数据semantic_baked二选一,depth走_decode_depth,其余(detect)按metadata.get("nms")区分 HailoRT NMS 输出与 YOLO26 one-to-one 原始输出。
detect 任务的两条解码路径
路径一:HailoRT 片上 NMS 输出解码(YOLOv8 / YOLO11)
YOLOv8/YOLO11 检测模型在编译期挂上了 HailoRT 的nms_postprocess(meta_arch=yolov8),HEF 直接返回按类别分组、归一化 yxyx 坐标的检测结果,此时metadata["nms"]为真,_decode_nms(hailo.py#L102-L121)负责还原:
- 以
input_info.shape的宽高构造[width, height, width, height]缩放向量,把每类检测结果的第 0/1 列(y、x)与第 2/3 列(y、x)取反并缩放为像素域xyxy; - 用
np.full补齐类别列,拼接分数列,得到(N, 6)的[x1, y1, x2, y2, score, cls]; - 按分数降序
argsort截取每帧最多300个目标,再零填充为(B, 300, 6)的统一张量返回。
路径二:YOLO26 无 NMS one-to-one 输出解码
YOLO26 采用免 NMS 的 one-to-one 检测头,其导出路径不挂 HailoRT NMS,HEF 输出为分支先行的回归图与分类图。_decode_raw(hailo.py#L174-L194)在主机端完成了原本由 NMS 承担的“选框”工作:
- 输出列表对半切分,前半为 box 图、后半为 cls 图(各尺度通道
permute(0, 3, 1, 2)转回 NCHW); - 首次调用时用
make_anchors(来自 ultralytics/utils/tal.py)以输入分辨率与特征图比例反推 strides,构建 anchors 并缓存到self._anchors; dist2bbox(boxes, anchors, xywh=False)得到像素 xyxy 框;- 分类图做 sigmoid 后,先按 anchor 维取每格 top-k(上限 300),再全局
topk(min(300, ...))二次筛选,用gather对齐框与分数; - 返回
(B, 300, 6)的 NumPy 数组,列序同为[x1, y1, x2, y2, score, cls],供 Predictor 的常规后处理消费。
segment / pose / obb:原始头张量的主机端还原
这三类任务编译进 HEF 的是未解码的原始头张量(每尺度一组三元组:reg、cls、extra,extra 分别是掩码系数、关键点或角度;segment 额外多一个 prototype 输出),解码逻辑与 PyTorch 头的decode_bboxes保持逐式对应。
共同基础_decode_boxes(hailo.py#L123-L134):把多尺度回归图拼接后过 DFL 得到距离分布,再借缓存的 anchors 调用dist2bbox(水平框,xywh=True)或dist2rbox(给定角度时输出旋转 xywh 框),最后乘 stride 张量还原到像素域。
Segment(_decode_segment):outputs[-1]是原型张量,其余按步长 3 拆出 reg/cls/cof 三组图;cls 图的 sigmoid 已在编译期烘入(注释标注 “sigmoid baked in at export”),最终torch.cat((boxes, cls, cof), 2)转置为密集检测张量,与 prototype 一起组成二元列表返回——这正是end2end=False的含义:由 Predictor 执行 NMS 与原型合成。
Pose(_decode_pose):三元组为 reg/cls/kpt。关键点还原严格镜像 PyTorch 头中的Pose.kpts_decode:xy = (raw * 2 + (anchor - 0.5)) * stride,且当关键点维度为 3 时,在主机端对可见度通道补做 sigmoid 再拼接。
OBB(_decode_obb):三元组为 reg/cls/angle,角度在主机端做 YOLO OBB 头的 squash 还原angle = (sigmoid(raw) - 0.25) * π,随后连同角度送入dist2rbox得到旋转框,输出(boxes, cls, angle)供旋转 NMS 消费。
三者共同点:anchors 延迟构建且只算一次;类别置信度的 sigmoid 依赖编译期指令change_output_activation(..., sigmoid)烘入 HEF,主机端不再重复计算。
classify / semantic / depth 的解码策略
Classify(hailo.py#L88-L89):HEF 在 Softmax 处截断输出,芯片内完成 softmax,后端只需reshape(B, -1)透传类别概率,无任何主机端还原。
Semantic(hailo.py#L90-L97):按元数据semantic_baked分两条路。多类别模型在 Hailo-10/15(DFC v5.x)上把双线性上采样与 ArgMax 编译进芯片,直接返回(B, H, W)类别图;Hailo-8/8L 与单类别头则返回 stride-8 的原始 logits(permute(0, 3, 1, 2)转回 NCHW),交给语义 Predictor 既有的上采样、letterbox 裁剪与类别归约流程,使结果与 PyTorch 模型完全一致。
Depth(_decode_depth):HEF 截断在深度头最后一个 logit 卷积处(编译期为该单输出施加a16_w16精度以保住动态范围),主机端镜像Depth.forward的收尾计算:
logit = torch.from_numpy(output).permute(0, 3, 1, 2) # (B, H/4, W/4, 1) -> (B, 1, H/4, W/4) depth = logit.clamp(-4.0, 5.0).exp() return depth.pow(self.metadata.get("cal_a", 1.0)) * math.exp(self.metadata.get("cal_b", 0.0))其中cal_a、cal_b是深度头的学习参数(log-affine 标定),由导出器写入metadata.yaml,缺失时安全回退为1.0/0.0。输出保持头分辨率(H/4, W/4),由DepthPredictor.postprocess经scale_masks放大到原图尺寸——与 PyTorch 模型推理走同一路径。
导出侧印证:HailoBackend 消费契约的来源
上述解码行为并非凭空设计,而是与 ultralytics/engine/exporter.py 中export_hailo的编译策略一一对应。理解导出侧有助于解释后端每个分支的成因:
- 端节点选择:classify 截断在
Softmax;semantic 依head.bake_argmax(多类别且目标为 hailo10h/15h/15l 时)截断在ArgMax或分类器卷积;depth 截断在head.3/Conv;YOLO26 one-to-one 检测取one2one_cv2/3各尺度卷积;seg/pose/obb 取cv2/cv3/cv4三组各尺度卷积,segment 额外追加proto/cv3/act/Mul(exporter.py#L1662-L1703)。 - model script 指令:
normalization([0,0,0],[255,255,255])对应后端 uint8 输入约定;model_optimization_flavor(optimization_level=2)与post_quantization_optimization(finetune, ...)强制 level-2 优化;one2one 检测与 depth 输出追加quantization_param(..., precision_mode=a16_w16);seg/pose/obb 在类别层烘 sigmoid;YOLOv8/11 检测则生成nms_config.json(含nms_scores_th、nms_iou_th、image_dims、regression_length: 16、按 stride 排列的bbox_decoders等)并追加nms_postprocess(..., meta_arch=yolov8, engine=cpu)(exporter.py#L1713-L1763)。 - metadata.yaml 写入:
hailo_arch、nms(detect 且非 one2one)、semantic_baked、depth 的cal_a/cal_b均在此写入(exporter.py#L1778-L1788),正是HailoBackend各分支读取的开关字段。 - 前置校验:
format="hailo"在 exporter.py#L624-L662 处强制 Linux x86_64、quantize仅接受 8(INT8 路线)、name缺省为hailo8l且必须属于("hailo8", "hailo8l", "hailo10h", "hailo15h", "hailo15l"),并显式拒绝end2end覆盖;YOLO26 仅放行 detect/classify/semantic/depth,YOLO26 的 seg/pose/obb 会被直接拒绝而非产出未验证的 HEF。
导出侧的完整操作指南(安装 DFC、name/imgsz/data/fraction/conf/iou参数表、校准精度建议、Raspberry Pi AI HAT+ 部署步骤与故障排查)见仓库中的配套文档 docs/en/integrations/hailo.md,其中“Export Arguments”小节列出的conf(默认 0.25)与iou(默认 0.7)正是被写入nms_config.json的两个阈值,与_decode_nms消费的片上 NMS 结果直接对应。
运行环境与使用方式
- 编译与部署分离:DFC 编译只能在 Linux x86_64 上进行,产物(
*_hailo_model/目录,含.hef、metadata.yaml,YOLOv8/11 检测另含nms_config.json)复制到任意装有 HailoRT 的目标设备即可推理;Raspberry Pi OS 上可经hailo-all(Hailo-8/8L)或hailo-h10-all(Hailo-10H)软件包安装 HailoRT,并用hailortcli fw-control identify与hailortcli parse-hef独立验证设备与 HEF。 - 直接推理:把导出目录交给
YOLO()即可完成predict与val,后端自动完成上文所有解码;val的意义在于把 INT8 HEF 与 PyTorch 基线在同一验证集上对比量化损失。 - 参数入口:通用推理/导出参数(如
fraction的取比值/计数/[train, val, test]语义、quantize精度约定)定义在 ultralytics/cfg/default.yaml,hailo已列入支持的format取值集合。 - 若应用需要相机采集、GStreamer 视频流水线或
hailonet集成,HEF 本身与本文后端解耦,可直接沿用 HailoRT 原生接口;但自研后处理必须与导出的模型族(是否带片上 NMS、是否烘焙 ArgMax)匹配,否则输出解析会错位。
约束与适用前提
- 固定输入分辨率:HEF 按导出时的
imgsz编译,_decode_boxes/_decode_raw的 stride 推导均依赖input_info.shape,多分辨率场景需在主机端缩放或分别编译多个 HEF。 - 仅支持七类任务:
load_model的任务白名单即硬边界;YOLO26 的 seg/pose/obb、以及 YOLOv10/YOLO-World/YOLOE/RT-DETR 等特殊检测族不在format="hailo"的支持范围内。 - INT8 路线 + 精度权衡:YOLO26 检测 logit 与深度 logit 在编译期用 a16 保范围;量化会平均下移 YOLO26 分数(建议设备端
conf略降)、并使 YOLO11 的注意力主干相对更敏感,具体数值以配套集成文档的测量表为准。 - 元数据随行:
metadata.yaml缺失时read_metadata回退为空字典,task/names/kpt_shape等将不可用,解码路径选择(NMS vs raw、baked vs logits、depth 标定)都会失效——部署时务必整目录拷贝。 - anchors 与 DFL 为惰性缓存:首次
forward才构建,同一后端实例反复推理时不重复计算;end2end标志仅对 segment/pose/obb 为 False,其余任务输出即最终检测结果。
综上,HailoBackend以约 200 行代码(ultralytics/nn/backends/hailo.py)实现了 Hailo 全任务族的主机端解码,其每个分支都能在上游export_hailo的编译指令中找到对应的“另一半”,二者共同保证了predict/val在 Hailo 设备上与 PyTorch 模型行为的一致性。
【免费下载链接】ultralyticsUltralytics YOLO26, YOLO11, YOLOv8 — object detection, instance segmentation, semantic segmentation, image classification, pose estimation, object tracking项目地址: https://gitcode.com/GitHub_Trending/ul/ultralytics
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考