【FPAI开发】超详细!YOLO26适配FPAI芯片部署过程详解!
1. 背景知识
1.1 FPAI概述
FPAI是一款异构融合可重构智能芯片,是国内首个将FPGA和AI加速核心集成于单芯片内部的产品。FPAI单芯片集成高性能处理器系统(Processing System,PS)、神经网络处理器(Neural Processing Unit,NPU)和可编程逻辑(Programmable Logic,PL)三部分,实现了“通用计算+硬件可编程+ AI 加速”的深度融合与高效协同。
人工智能主要分为“训练”和“推理”两大场景,训练端通过海量数据和算法,让模型从无到有掌握规律、优化参数,最终实现“精准识别、精准决策”,核心目标是提升模型的准确率与泛化能力,推理端利用训练好的模型,对新的输入数据进行快速计算、输出结果,核心目标是在保证模型精度基本达标的前提下,实现高效部署、快速响应,满足实际应用的落地需求。
面向多元化人工智能推理应用场景,FPAI单芯片满足端侧智能应用全流程计算需求,涵盖数据采集与预处理、AI 推理计算、后处理及业务功能三大阶段。具备高集成度、小型化、高能效、高可靠、灵活可扩展等显著优势,搭配全自主研发的 ICraft 软件工具链,可快速赋能多场景智能应用落地。
1.2 FPAI开发流程
作为一款智能推理芯片,FPAI开发本质上是将上位机的智能推理应用(前后处理代码+AI模型推理)迁移部署至FPAI平台运行,开发过程中,要建立系统整体概念、明确数据流,以“数据”为核心,清晰把握“数据-预处理-AI推理-后处理-结果”每个阶段中数据格式、存储位置、计算后端、意义与去向。
1.3 FPAI支持哪些模型
FPAI 异构融合可编程智能芯片,采用算子级模块化支持机制,以算子(Op)为神经网络部署的最小执行单元。诸葛架构 NPU 原生支持 Conv2d、Linear、MaxPool、GRU、LSTM、LayerNorm、Softmax、multiply、sqrt、log、Sigmoid、SiLU 等百余种主流算子,完整覆盖 CV、NLP、时序预测等多类神经网络的核心计算需求。基于标准化算子集,只要神经网络结构所包含的算子均在官方支持清单(ICraft Docs算子支持清单)内,即可直接在FPAI平台完成编译部署与推理运行。同时,依托 FPAI 平台的 FPGA 可编程逻辑与 CPU 异构算力,可对 NPU 暂不支持的算子进行快速扩展与自定义实现,无需等待硬件迭代,即可适配神经网络的升级与新算子的快速落地,兼具部署兼容性与架构灵活性。
快速验证方式,将框架模型导出ONNX格式+ toml配置文件,通过ICraft-Compiler进行编译转换,转换成功,即说明支持该模型,此外,可参考ICraft-ModelZoo已适配模型清单(https://www.modelscope.cn/models/AIBS/modelzoo_index)。
1.4 Toml文件如何配置
- 教程:
https://www.modelscope.cn/models/AIBS/icraft_tutorial/file/view/v3.33.1/ZHUGE%2FPart%202_1%20compile.md?status=1
2. 开发环境
- 硬件:FMQL30TAI-悟净开发板(诸葛架构NPU)
- 软件:ICraft_V3.37.1
- ICraft-ModelZoo网址:
https://www.modelscope.cn/collections/icraft_modelzoo-18b52923d4854f
- 基础教程:https://blog.csdn.net/qq_36840004?spm=1000.2115.3001.5343
3. YOLO适配FPAI详解
3.1 YOLO26适配30TAI开发思路
本文目的是将YOLO26模型部署于30TAI平台运行,该过程是一个算法迁移的过程,将YOLO26从“上位机开发环境”迁移到“嵌入式运行环境”中,并利用30TAI的硬件实现算法运行,因此,第一步需要在上位机完成YOLO26的部署运行,并明确从图像输入到目标坐标结果的全部计算流程以及每一环节的数据格式。YOLO26在上位机运行的流程如下图所示。
部署过程是将上图所示流程迁移至30TAI平台实现,为了快速上手,首先对YOLOv5的运行流程进行梳理,其流程如下图所示。
结合上述两个流程图,可以看出,对于智能算法的部署,主要工作集中在前处理、AI推理和后处理三部分的迁移开发以及精度对齐,对于30TAI而言,前后处理可以基于CPU/NPU/FPGA来实现,AI推理基于NPU来实现,其中NPU只能运行ICraft编译生成的模型文件(json&raw/snapshot)。
YOLO26适配30TAI有以下几个问题需要明确:
- 前后处理模块是否能复用ICraft-ModelZoo已经支持的YOLO模型,避免重复开发:需要对YOLO系列模型结构进行分析。
- YOLO26主干网络部分是否有不支持算子:需要导出ONNX模型,通过ICraft进行编译验证。
- 如何适配DetPost模块:模型结构需要进行相应调整,DetPost参数需要正确配置。
3.2 DetPost 模块
DetPost是针对YOLO系列目标检测模型,基于硬件实现的网络后处理加速模块,读取 PLDDR 中的NPU计算结果,完成硬件筛选加速,然后将数据写回至PSDDR。在ICraft 中通过自定义硬算子的方式进行调用(ICraft 3.0 及以上版本)。
对于30TAI,诸葛架构NPU内集成了DetPost IP,但功能尚未正式开放;目前,30TAI中基于FPGA外挂实现了DetPost模块(仅支持INT8数据格式)。
- 工作原理:
对于YOLOv5输出的每个预选框向量,由85个数组成(x,y,w,h,s,类别数量80),第五个数据是score,score 值与DetPost设置的阈值进行比较,大于阈值的筛选出来,把整个向量回传到 PS。
- 调用方式:
如下图所示,编译时需要在相应的阶段打开 customop的pass。DetPost算子加入网络是在adapt阶段,所以在adapt阶段的pass_on参数中配置 customop.DetPostZGPass。custom_config参数用来指定custom_config.toml的路径。
- 配置 customop 的参数:
3.3 YOLO家族模型结构对比
YOLO26是Ultralytics在2026年发布的最新版本,专注边缘与低功耗设备优化设计,其主要特点有:
- 端到端无NMS推理,无需后处理中的非极大值抑制,简化部署流程。
- 去除分布焦点损失DFL模块,模型更易适配到各类终端设备。
- 支持多任务统一架构,覆盖检测、分割、分类、姿态估计、旋转框检测五大任务。
由于嵌入式部署,主要关注模型的算子、前后处理,因此对YOLO系列经典模型的结构进行分析,其特点如表格所示,其中attention/Softmax代表该模型的部署需要softmax算子,在100TAI平台softmax算子是通过FPGA扩展实现,30TAI的NPU支持softmax算子。
对于YOLO系列模型在30TAI平台部署,ICraft-ModelZoo已经提供了示例工程,建议先根据教程运行YOLOv5示例工程,其中对于YOLOv5源码模型导出ONNX或者Torchscript格式,ICraft要求对结构进行修改,只导出到卷积为止,目的是适配DetPost算子,实现后处理FPGA硬件加速。
- YOLOv5结构可视化分析
- YOLOv8结构可视化分析
- YOLOv10结构可视化分析
- YOLO26结构可视化分析
通过对经典YOLO系列模型的结构分析,可以看出YOLO26结构跟YOLOv10相比差异最小,因此,可以复用YOLOv10的部分代码。
4. YOLO26适配FPAI
4.1 验证YOLO26模型Pytorch框架下精度
- YOLO26源码下载地址:https://github.com/ultralytics/ultralytics
- YOLO26权重下载地址:
https://github.com/ultralytics/assets/releases/download/v8.4.0/yolo26n.pt
- 为了避免图像缩放和PAD操作影响对齐精度,所以准备一张640*640图片,用于推理验证
- 编写推理脚本infer.py并运行,得到以下结果,作为golden结果
- infer.py
from ultralytics import YOLO # Load a pretrained YOLO26n model model = YOLO("yolo26n.pt") # Perform object detection on an image results = model("./bus_640x640.png") # Predict on an image # 获取第一个图片的结果(最关键) result = results[0] # ====================== # 打印所有目标坐标 # ====================== print("==== 检测到的目标坐标信息 ====") for box in result.boxes: # 坐标 (x1, y1, x2, y2) 左上角 + 右下角 x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() # 置信度 conf = box.conf[0].cpu().numpy() # 类别编号 & 类别名 cls_id = int(box.cls[0].cpu().numpy()) cls_name = result.names[cls_id] # 打印! print(f"类别:{cls_name} \t置信度:{conf:.2f}") print(f"坐标 (x1,y1,x2,y2):({x1:.1f}, {y1:.1f}, {x2:.1f}, {y2:.1f})") print("-" * 50) results[0].show() # Display results4.2 保存ONNX模型并修改ONNX结构适配DetPost要求
- 编写导出ONNX脚本export.py并运行,得到yolo26n.onnx
from ultralytics import YOLO # Load a pretrained YOLO26n model model = YOLO("yolo26n.pt") # Export the model to ONNX format for deployment path = model.export(format="onnx") # Returns the path to the exported model- 通过netron查看yolo26.onnx输入输出维度以及结构
- 从结构图可以看出,其包含了后处理及TopK模块,由于DetPost加速模块要求接在conv2d算子之后,所以需要将图中红色箭头箭头之后的算子全部从ONNX中删掉,有两种方式,方式1:通过修改模型源码,类似于ICraft_ModelZoo中的1_save.py;方式2:通过脚本对ONNX进行算子的删除,该方法属于通用做法,注意输出的顺序要符合要求
- 编写get_onnx_graph.py,用于获取ONNX计算图中指定输入到输出节点的全部算子
- 输入节点名字:
- 第一个输出节点名字:
- get_onnx_graph.py
import onnx ## 注意为了适配DetPost ## 输出层顺序: 1x80x80x80,1x4x80x80;1x80x40x40,1x4x40x40;1x80x20x20,1x4x20x20; onnx.utils.extract_model("./yolo26n.onnx", # 输入ONNX模型路径 "yolo26n-icraft.onnx", # 输出ONNX模型路径 ["images"], # 模型输入节点名,对应1x3x640x640 ["/model.23/one2one_cv3.0/one2one_cv3.0.2/Conv_output_0", # 1x80x80x80 "/model.23/one2one_cv2.0/one2one_cv2.0.2/Conv_output_0", # 1x4x80x80 "/model.23/one2one_cv3.1/one2one_cv3.1.2/Conv_output_0", # 1x80x40x40 "/model.23/one2one_cv2.1/one2one_cv2.1.2/Conv_output_0", # 1x4x40x40 "/model.23/one2one_cv3.2/one2one_cv3.2.2/Conv_output_0", # 1x80x20x20 "/model.23/one2one_cv2.2/one2one_cv2.2.2/Conv_output_0" # 1x4x20x20 ] )- 运行get_onnx_graph.py脚本,得到yolo26n-icraft.onnx,其结构如下:
- 接下来的工作准备复用大部分yolov10的代码
4.3 下载ICraft_ModelZoo_yolov10
- 下载地址:https://www.modelscope.cn/models/AIBS/yolov10/files?version=v3.33.1
- 下载:
4.4 验证yolo26-icraft.onnx的正确性
- 该步骤目的有两点,1. 验证ONNX模型是正确的;2. 掌握ONNX 输入和输出数据的维度以及含义,对齐前后处理脚本,便于迁移至嵌入式平台。
- 从3.3章节可以看出,yolo26输出两部分cls部分维度1x80x8400,box部分1x4x8400;yolov10输出的1x64x8400经过DFL模块后变成1x4x8400。
- 对2_save_infer.py脚本进行修改,得到yolo26_save_infer.py
import argparse import os import cv2 import torch import numpy as np import sys sys.path.append(R'..') sys.path.insert(0,R'..') from ultralytics.nn.modules.block import DFL from ultralytics.utils.tal import dist2bbox,make_anchors from ultralytics.data.augment import LetterBox from ultralytics.utils import ops from visualize import vis,COCO_CLASSES import onnxruntime def pred_one_image(img_path,model_path,test_size): img_raw = cv2.imread(img_path) print('img_path=',img_path) # 前处理 letterbox = LetterBox(test_size, auto=False, stride=32) im = np.stack([letterbox(image=x) for x in [img_raw]]) print('******im =',im.shape) im = im[..., ::-1].transpose((0, 3, 1, 2)) im = np.ascontiguousarray(im) im = torch.from_numpy(im) im = im.float() im /= 255 # 加载 ONNX 模型(你之前导出的 yolo26n-icraft.onnx) onnx_model = onnxruntime.InferenceSession("../2_compile/fmodel/yolo26n-icraft.onnx") ## 前向推理计算 onnx_input = im.numpy() # 输入预处理后的图像数据(1x3x640x640) onnx_output = onnx_model.run(None, {onnx_model.get_inputs()[0].name: onnx_input}) # 1. 拼接 3 个尺度的类别输出(cls) # onnx_output[0]/[2]/[4] 分别对应 80x80、40x40、20x20 的类别特征图 cls = torch.cat([torch.tensor(onnx_output[0]).reshape(1,80,-1), torch.tensor(onnx_output[2]).reshape(1,80,-1), torch.tensor(onnx_output[4]).reshape(1,80,-1)], dim=2) # 2. 拼接 3 个尺度的坐标输出(box) # onnx_output[1]/[3]/[5] 分别对应 80x80、40x40、20x20 的坐标特征图 box = torch.cat([torch.tensor(onnx_output[1]).reshape(1,4,-1), torch.tensor(onnx_output[3]).reshape(1,4,-1), torch.tensor(onnx_output[5]).reshape(1,4,-1)], dim=2) print(cls.size()) # 输出应为 [1, 80, 80*80 + 40*40 + 20*20] = [1, 80, 8400] print(box.size()) # 输出应为 [1, 4, 8400] # 3. 生成锚点(anchors),并获取步长(strides) outputs = [torch.tensor(onnx_output[0]),torch.tensor(onnx_output[2]),torch.tensor(onnx_output[4])] anchors, strides = (x.transpose(0, 1) for x in make_anchors(outputs, torch.from_numpy(np.array([8, 16, 32],dtype=np.float32)), 0.5)) # 4. 将模型输出的分布值转换为实际框坐标(乘以步长还原到原图尺度) dbox = dist2bbox(box, anchors.unsqueeze(0), xywh=True, dim=1) * strides y = torch.cat((dbox, cls.sigmoid()), 1) #[1,84,8400] # yolov10 postprocess - NMS free preds = y.transpose(-1, -2) conf_thres = 0.25 max_det = 300 bboxes, scores, labels = ops.v10postprocess(preds,max_det, preds.shape[-1]-4)# bbox - [1,max_det,4] scores - [1,max_det] labels - [1,300] bboxes = ops.xywh2xyxy(bboxes) preds = torch.cat([bboxes, scores.unsqueeze(-1), labels.unsqueeze(-1)], dim=-1) #[1,max_det,6] = [1,max_det, bbox+scores+label] mask = preds[..., 4] > conf_thres b, _, c = preds.shape preds = preds.view(-1, preds.shape[-1])[mask.view(-1)]# 取mask = True的结果,即score>conf的结果 pred = preds.view(b, -1, c)#[1,res_num,6] _,res_num,_ = pred.shape pred = pred[0] # rescale coords to img_raw size print("im.shape[2:]: ",im.shape[2:],img_raw.shape) pred[:, :4] = ops.scale_boxes(im.shape[2:], pred[:, :4], img_raw.shape) # show results # 假设你的 pred 是推理后的结果 (N, 6) :[x1,y1,x2,y2, score, cls] print("==== 检测到的目标坐标 (x1y1x2y2 格式) ====") for i in range(len(pred)): # 取出每个目标 x1, y1, x2, y2, conf, cls_id = pred[i] # 转成普通数值方便打印 x1, y1, x2, y2 = float(x1), float(y1), float(x2), float(y2) conf = float(conf) cls_id = int(cls_id) cls_name = COCO_CLASSES[cls_id] # 类别名称 # 格式对齐打印(和你上面 YOLO 输出格式一模一样) print(f"类别:{cls_name:10s} \t置信度:{conf:.2f}") print(f"坐标 (x1,y1,x2,y2):({x1:6.1f}, {y1:6.1f}, {x2:6.1f}, {y2:6.1f})") print("-" * 60) result_image = vis(img_raw, boxes=pred[:,:4], scores=pred[:,4], cls_ids=pred[:,5], conf=conf_thres, class_names=COCO_CLASSES) cv2.imshow(" ", result_image) cv2.waitKey(0) cv2.imwrite('yolo26_infer_result.jpg',result_image) print('Detect ',res_num,' objects!') if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--model', type=str, default="../2_compile/fmodel/yolo26n-icraft.onnx", help='torchscript model path') parser.add_argument('--source', type=str, default="bus_640x640.png", help='image path') parser.add_argument('--imgsz', nargs='+', type=int, default=[640], help='image size') opt = parser.parse_args() opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 test_size = tuple(opt.imgsz) if os.path.isfile(opt.source): pred_one_image(opt.source, opt.model, test_size) elif os.path.isdir(opt.source): image_list = os.listdir(opt.source) for image_file in image_list: image_path = opt.source + "//" + image_file pred_one_image(image_path, opt.model, test_size)- 运行yolo26_save_infer.py脚本得到以下结果:
- 对比4.1章节的推理结果可以看到二者一致,说明ONNX正确以及前后处理脚本对齐,为下一步FPAI平台部署打下良好基础。注意:输入图像640x640和网络要求一致,未涉及缩放前处理对比。
4.5 ICraft编译yolo26模型toml文件配置(30TAI)
- Toml配置参考yolov10_int8即可,修改以下三处
- DetPost模块参数配置toml文件,将DetPost阈值设置为0.1
4.6 ICraft编译yolo26模型toml文件配置(100TAI)
- 将编译目标后端target改为buyi,增加imagemake模块配置
4.7 ICraft编译yolo26生成多阶段JSON&RAW
- 运行编译命令:icraft compile .\config\ZG\yolo26_int8.toml
- 通过ICraft-Show查看网络生成的模型结构是,选中JSON&RAW文件,右键选择ICraft-Show打开。
- JSON文件描述了AI模型的结构和算子维度等;RAW文件记录了AI模型的参数以及驱动NPU运行的指令。
- Parsed和Optimized阶段的JSON和RAW为FP32精度,理论上无精度损失,如果精度异常说明算子解析异常。
- Quantized阶段及之后的JSON&RAW文件,经过了量化适配,存在一定程度的精度损失。
- 所有阶段的JSON&RAW文件均支持在上位机WIN系统仿真,其中仅有BY/ZG/WL阶段的JSON&RAW支持在FPAI芯片上运行。
- BY/ZG/WL阶段的JSON&RAW,仿真结果与芯片推理结果完全一致。
- ICraft-Show展示ZG阶段JSON和RAW结构:
4.8 ICraft编译yolo26生成多阶段JSON&RAW(100TAI)
- 运行编译命令:icraft compile .\config\BY\yolo26_int8.toml
- ICraft-Show展示BY阶段JSON和RAW结构:
5. YOLO26-Runtime APP代码开发
5.1 C++运行时代码开发(支持WIN仿真/SOCKET模式/PSIN模式)
- 复用大部分yolov10.cpp代码,对前后处理部分进行修改。
- 修改bbox_info_channel = 4,yolo26直接输出xywh,yolov10该参数配置为64。
- 对于yolo系列模型ICraft_ModelZoo提供的示例runtime代码中,包含两套后处理参数,post_detpost_hard函数,仅支持带有DetPost算子模型的仿真与芯片端运行;post_detpost_soft函数支持不带DetPost算子模型的后处理,包括各阶段JSON&RAW的仿真与芯片端运行。
- 修改post_detpost_soft函数,适配yolo26后处理参数要求,3处修改。
- 修改post_detpost_hard进行参数修改,适配yolo26后处理参数要求,2处修改。
5.2 WIN系统编译C++工程
- 运行
- # cmake ..
- # cmake --build . --config Release
5.3 WIN系统运行C++仿真
- 仿真parsed阶段,将yaml参数修改为parsed阶段以及host仿真模式
- PARSED阶段仿真结果
- 从上述结果可以看出,PARSED结果完全正确,说明PARSED阶段JSON和RAW文件解析完全正确以及前后处理函数已实现精度对齐。
- 同理,可对optimized、quantized、adapted阶段进行仿真对比验证。
- 仿真ZG阶段模型(带DetPost)的结果,存在微小程度的精度误差属于正常现象:
5.4 WIN系统运行C++仿真(100TAI)
- 修改输入模型路径以及stage,PARSED阶段仿真结果:
- 修改输入模型路径以及stage,BY阶段仿真结果:
5.5 WIN系统运行Socket模式(30TAI)
- 开发板启动icraft-serve
- 修改yaml文件为socket模式
- 运行socket模式
