
机载AI部署这件事我在过去两年里前前后后折腾过五套不同算力的板子从最早拿树莓派4B硬扛YOLOv5到后来用Jetson Orin NX跑YOLOv8再到最近帮朋友在RK3588上做量化部署踩过的坑基本能写一本小册子。这一讲的核心就一件事把你在PC上训练好的模型真正塞进无人机那点可怜的算力和功耗预算里让它在天上飞的时候能实时出结果。很多人卡在这一步——训练脚本跑得飞起mAP看着也漂亮一到部署环节就发现帧率掉到个位数或者精度莫名其妙崩了。这篇内容适合已经跑通过YOLOv8训练、准备往机载端迁移的开发者也适合正在选型飞控 companion computer 的团队做参考。我会把从PyTorch到ONNX再到TensorRT/RKNN的完整链路拆开讲重点放在那些文档里不会写、但实际部署一定会遇到的细节上。1. 机载AI部署到底难在哪先搞清楚约束条件1.1 无人机平台的真实算力与功耗天花板很多人做机载AI部署的第一个误区是拿PC上的推理速度去推算机载表现。我在GTX 1660 Ti上跑YOLOv8n640×640输入能到120 FPS以上于是想当然觉得Jetson Orin NX怎么也能跑个60 FPS。实测下来Orin NX 16GB在15W功耗模式下YOLOv8n FP16大概在45到55 FPS之间一旦切到10W模式就掉到30 FPS出头。这还是有TensorRT加速的情况如果用ONNX Runtime的CPU执行提供器直接掉到5 FPS以下。这里面的核心矛盾是无人机对重量极其敏感每多100克载荷续航可能就少3到5分钟。一块Jetson Orin NX加上散热片和载板重量在150克左右功耗15W意味着你要从动力电池分出一路供电对续航的影响是实打实的。所以机载AI部署的第一原则不是能不能跑而是在给定功耗和重量预算下能不能跑得动。我一般会先算一笔账假设无人机总起飞重量2kg电池是6S 5000mAh标称续航25分钟。加装一个15W的companion computer后电池总输出功率增加约15W按6S电池22.2V标称电压算额外电流约0.68A。5000mAh电池在0.68A额外放电下续航大概减少3到4分钟。这个代价能不能接受取决于你的任务场景——巡检类任务可能无所谓但竞速或长航时任务就要慎重了。1.2 从训练精度到部署精度的隐形损耗训练时mAP 0.85部署后实际检测效果感觉只有0.7出头这种情况太常见了。损耗主要来自三个环节格式转换、量化、以及预处理/后处理的不一致。格式转换环节PyTorch到ONNX再到TensorRT每一步都可能引入数值差异。我遇到过最离谱的一次是PyTorch里NMS用的IoU阈值0.45导出ONNX时忘了把NMS算子一起导出结果部署端用了默认的0.5导致密集场景下漏检明显增多。后来我养成了一个习惯导出ONNX后先用ONNX Runtime在PC上跑一遍验证集和PyTorch结果对比确认mAP差异在0.5个百分点以内再往下走。量化环节的损耗更大。FP32到FP16通常没什么问题精度损失在0.1到0.3个百分点。但FP16到INT8就是另一回事了尤其是YOLOv8这种带大量小目标的检测任务INT8量化后小目标召回率可能掉5到10个百分点。我一般建议如果算力允许优先用FP16实在要上INT8必须做量化感知训练或者至少用校准集仔细调。预处理不一致是最隐蔽的坑。训练时用的是letterbox填充部署时如果直接resize长宽比变了检测框位置就会偏。这个后面会详细讲。1.3 机载部署的选型决策树面对一个具体的无人机项目怎么选部署方案我总结了一个简单的决策流程算力平台典型功耗推荐推理框架适用模型规模典型帧率YOLOv8n树莓派4B/55-8WONNX Runtime CPU / NCNNYOLOv8n以下3-8 FPSRK35885-15WRKNNYOLOv8n/s20-40 FPSJetson Orin Nano7-15WTensorRTYOLOv8n/s/m30-60 FPSJetson Orin NX10-25WTensorRTYOLOv8s/m45-80 FPSJetson AGX Orin15-40WTensorRTYOLOv8m/l60-120 FPS选型时还要考虑生态成熟度。TensorRT的文档和社区支持最完善但Jetson系列价格偏高RK3588性价比突出RKNN工具链这两年也成熟了很多但遇到问题排查起来资料相对少树莓派适合验证概念真上无人机基本不够用。注意上表帧率是典型值实际受散热、内存带宽、输入分辨率影响很大。散热做不好Jetson会降频帧率直接腰斩。2. 模型导出从PyTorch到ONNX的每一步都别想当然2.1 导出前的模型准备与动态轴设置YOLOv8用Ultralytics的库训练完之后导出ONNX其实一行命令就行yolo export modelbest.pt formatonnx imgsz640 opset12 simplifyTrue dynamicFalse但这行命令里有几个参数值得展开说。opset12是我比较推荐的版本兼容性好TensorRT 8.x和RKNN都支持。simplifyTrue会调用onnx-simplifier做图优化能去掉一些冗余算子对后续转换很有帮助。dynamicFalse表示固定输入尺寸机载部署我强烈建议固定尺寸动态轴会带来额外的内存分配开销而且TensorRT对动态shape的优化不如固定shape。如果你需要动态batch比如多路摄像头轮询可以设dynamicTrue但要在导出后检查ONNX的输入输出shape是否符合预期。我见过有人导出后输入变成了[1, 3, height, width]部署端没注意直接按固定尺寸喂数据结果推理结果全乱。导出完成后第一件事是用Netron打开ONNX文件确认输入输出节点。YOLOv8的ONNX输出通常是[1, 84, 8400]844个框坐标80个类别840080×8040×4020×20的anchor点总数。这个输出格式和YOLOv5不一样后处理代码不能直接复用。2.2 ONNX Runtime验证别跳过这一步导出ONNX后我习惯写一个简单的验证脚本用ONNX Runtime跑几张测试图和PyTorch的输出做对比import onnxruntime as ort import numpy as np import torch from ultralytics import YOLO # PyTorch推理 model YOLO(best.pt) pt_results model(test.jpg) # ONNX Runtime推理 session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name img preprocess(test.jpg) # 你的预处理函数 onnx_output session.run(None, {input_name: img}) # 对比输出 print(PyTorch output shape:, pt_results[0].boxes.data.shape) print(ONNX output shape:, onnx_output[0].shape)对比时重点看两件事一是输出shape是否一致二是相同输入下检测框的坐标差异。如果差异超过1个像素就要检查预处理是否一致。我遇到过PyTorch端用了BGRONNX端用了RGB导致颜色通道反了检测结果完全不对。2.3 预处理对齐letterbox的坑最深YOLOv8训练时的预处理是letterbox保持长宽比缩放然后填充到目标尺寸。部署时如果直接resize检测框会偏移。letterbox的实现要注意几个细节def letterbox(im, new_shape(640, 640), color(114, 114, 114)): shape im.shape[:2] # 当前尺寸 r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] dw / 2 dh / 2 im cv2.resize(im, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) im cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return im, r, (dw, dh)后处理时要把检测框坐标映射回原图映射公式是x_orig (x_letterbox - dw) / r。这个dw和dh的计算精度很关键用int(round())而不是直接int()否则会有1到2个像素的累积误差。提示如果你的部署端和训练端预处理不一致最直接的表现是检测框整体偏移而不是随机误差。遇到这种情况先查预处理。3. TensorRT部署Jetson平台上的性能榨取3.1 TensorRT版本选择与GTX 1070的兼容性TensorRT 10.x是否支持GTX 1070这个问题我被问过好几次。答案是TensorRT 10.x官方支持的计算能力最低是7.0Volta架构GTX 1070是Pascal架构计算能力6.1不在官方支持列表里。虽然有些情况下能编译通过但运行时会报错或者性能异常。如果你手头只有GTX 1070建议用TensorRT 8.6.x这是最后一个对Pascal架构有较好支持的版本。Jetson平台上的TensorRT版本和JetPack绑定不能单独升级。比如JetPack 5.1对应TensorRT 8.5JetPack 6.0对应TensorRT 8.6。选型时要先确定JetPack版本再查对应的TensorRT版本。3.2 ONNX到TensorRT的转换trtexec与Python API转换有两种方式命令行工具trtexec和Python API。trtexec适合快速验证trtexec --onnxbest.onnx --saveEnginebest.engine --fp16 --workspace4096--fp16开启FP16精度--workspace4096指定4GB工作空间。工作空间大小影响TensorRT的优化策略太小会导致某些优化无法进行太大浪费内存。对于YOLOv8n2GB工作空间通常够用YOLOv8m建议4GB。Python API适合集成到自动化流程里import tensorrt as trt logger trt.Logger(trt.Logger.WARNING) builder trt.Builder(logger) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, logger) with open(best.onnx, rb) as f: parser.parse(f.read()) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 4 30) config.set_flag(trt.BuilderFlag.FP16) engine builder.build_serialized_network(network, config) with open(best.engine, wb) as f: f.write(engine)转换过程中最常见的报错是Unsupported ONNX operator通常是某个算子TensorRT不支持。解决办法是用onnx-simplifier简化或者手动替换算子。YOLOv8的SiLU激活函数在TensorRT 8.x里是支持的但某些自定义算子可能需要插件。3.3 INT8量化校准集的选择比算法更重要INT8量化能把推理速度再提升30%到50%但精度损失也更大。TensorRT的INT8量化需要校准集校准集的选择直接决定量化质量。我的经验是校准集要覆盖实际部署场景的典型分布。比如做无人机巡检校准集里就要包含不同光照、不同角度、不同背景的图片数量在500到1000张之间。如果校准集全是晴天白天的图片部署到阴天场景下精度会崩。config.set_flag(trt.BuilderFlag.INT8) config.int8_calibrator MyCalibrator(calibration_data, cache_filecalib.cache)校准过程比较慢建议把校准缓存保存下来下次转换直接加载。另外INT8量化后一定要在验证集上重新评估mAP如果掉超过3个百分点就要考虑放弃INT8或者做量化感知训练。3.4 推理引擎的序列化与反序列化TensorRT引擎.engine文件是平台相关的在Jetson上生成的引擎不能拿到x86上跑甚至不同JetPack版本之间也不通用。所以部署时要在目标设备上生成引擎或者用相同环境生成。反序列化加载引擎的代码import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit with open(best.engine, rb) as f: engine_data f.read() runtime trt.Runtime(trt.Logger(trt.Logger.WARNING)) engine runtime.deserialize_cuda_engine(engine_data) context engine.create_execution_context()加载后要分配输入输出内存用CUDA的cudaMalloc或者PyCUDA的mem_alloc。推理时把数据拷贝到GPU执行context再把结果拷回来。这部分代码比较模板化但内存分配的大小和绑定索引容易出错建议用TensorRT的engine.get_binding_shape()动态获取。4. RK3588部署性价比路线的完整链路4.1 RKNN工具链的环境搭建RK3588的部署链路是ONNX转RKNN再在板端用RKNN Runtime推理。工具链在PC上安装pip install rknn-toolkit2注意版本要和板端RKNN Runtime匹配。我用的组合是rknn-toolkit2 1.6.0配板端librknnrt 1.6.0。版本不匹配会出现模型加载失败或者推理结果异常。转换脚本from rknn.api import RKNN rknn RKNN() rknn.config(mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588) rknn.load_onnx(modelbest.onnx) rknn.build(do_quantizationTrue, datasetcalibration.txt) rknn.export_rknn(best.rknn)mean_values和std_values要和训练时的归一化参数一致。YOLOv8训练时输入是0到1归一化所以这里std_values设255mean_values设0。4.2 量化数据集准备与精度调优RKNN的量化也需要校准集格式是一个txt文件每行是一张图片的路径。校准集数量建议200到500张同样要覆盖实际场景。RKNN量化后精度损失通常比TensorRT INT8小一些因为RKNN用的是混合量化策略。但如果发现精度掉得厉害可以尝试增加校准集数量调整quantized_dtype参数从w8a8改成w8a16权重量化8位激活16位对敏感层跳过量化RKNN还支持混合量化可以指定某些层不量化。这个功能在rknn-toolkit2的文档里有详细说明但实际调起来比较费时间建议先用默认配置跑一遍看效果。4.3 板端推理与零拷贝优化RK3588板端推理用RKNN Runtime的C API或者Python API。Python API适合快速验证from rknnlite.api import RKNNLite rknn RKNNLite() rknn.load_rknn(best.rknn) rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0_1_2) outputs rknn.inference(inputs[img])core_mask参数可以指定用哪些NPU核心。RK3588有3个NPU核心可以并行推理多路视频流。但要注意多核并行时内存带宽是瓶颈实际加速比不是线性的。零拷贝优化是RK3588部署的关键。默认情况下数据要从CPU内存拷贝到NPU内存这个拷贝开销在1080p输入下可能占到总延迟的20%到30%。用RKNN的零拷贝API可以直接在DMA缓冲区上操作省掉拷贝。具体做法是用rknn.inference(inputs[img], data_formatnhwc)配合预分配的DMA buffer。注意零拷贝对内存对齐有要求输入数据的地址必须是64字节对齐的。不对齐会导致推理结果错误或者程序崩溃。5. 机载部署的工程化细节那些文档不会告诉你的事5.1 散热设计对持续性能的影响Jetson Orin NX在25W模式下不加散热片跑YOLOv8前30秒能到60 FPS然后温度冲到80度以上开始降频帧率掉到35 FPS左右。加一个20克的铝散热片加小风扇能稳定在50 FPS以上。散热设计要考虑无人机飞行时的气流。固定翼无人机气流充足散热好做多旋翼无人机悬停时气流主要来自桨叶下洗如果companion computer安装在机身内部散热会很差。我一般建议把计算板安装在桨叶下洗气流能覆盖到的位置或者加装导流罩。5.2 供电与电磁兼容机载AI计算板的供电要从动力电池取但动力电池的电压随电量变化6S电池满电25.2V放到3.5V每片时只有21V。所以需要宽压输入的DC-DC模块输入范围至少覆盖20V到26V。电磁兼容是另一个容易被忽视的问题。无人机的电调、电机、图传都会产生电磁干扰计算板的USB3.0接口对干扰特别敏感。我遇到过图传开启后USB摄像头画面出现横纹导致检测精度下降。解决办法是用屏蔽线、加磁环、或者把摄像头接口换成MIPI CSI。5.3 推理管道的延迟优化端到端延迟不只是推理时间还包括采集、预处理、后处理、通信。我实测过一个典型管道环节耗时ms摄像头采集15-30预处理letterbox归一化5-10推理YOLOv8n FP1615-20后处理NMS坐标映射5-10结果发送到飞控5-10合计45-80优化方向用GStreamer管道做零拷贝采集预处理放到GPU上做后处理用CUDA加速NMS。这些优化能把总延迟压到30ms以内。5.4 模型更新与OTA无人机部署后模型可能需要更新。OTA更新模型文件要注意新模型必须和当前推理引擎兼容否则要连引擎一起更新。我一般把模型和引擎打包成一个版本包更新时先下载到临时目录校验MD5后再替换替换后重启推理服务。如果无人机在飞行中模型更新要等到降落后再做。飞行中更新模型风险太大万一更新失败AI功能就没了。6. 常见问题排查从现象到根因的完整链路6.1 推理结果全乱或者全是背景类这个现象通常是预处理不一致导致的。排查步骤用同一张图片在PC上用PyTorch推理保存检测框坐标在部署端推理保存检测框坐标对比两组坐标如果偏移是系统性的比如所有框都往一个方向偏检查letterbox参数如果坐标完全对不上检查输入数据的颜色通道顺序RGB vs BGR和归一化参数我遇到过最隐蔽的一次是部署端用了cv2.imread读图默认BGR但训练时用的是PIL读图RGB。颜色通道反了之后模型把蓝色天空识别成了绿色植被检测结果完全不对。6.2 帧率远低于预期排查顺序确认推理框架是否正确使用了硬件加速TensorRT vs ONNX Runtime CPU检查是否开启了FP16/INT8用tegrastatsJetson或top查看CPU/GPU/NPU占用率检查散热确认没有降频检查内存带宽是否成为瓶颈多路视频流时常见有一次我帮朋友排查RK3588帧率低的问题最后发现是Python的GIL锁导致多线程推理效率低改成多进程后帧率翻倍。6.3 量化后精度骤降先确认量化校准集是否覆盖了实际场景。如果校准集没问题尝试减少量化层数对敏感层保留FP16增加校准集数量检查量化参数mean/std是否和训练一致YOLOv8的检测头部分对量化比较敏感可以尝试只量化backbone检测头保留FP16。RKNN和TensorRT都支持这种混合精度配置。6.4 模型加载失败或推理崩溃常见原因引擎文件平台不匹配在x86上生成的TensorRT引擎拿到Jetson上用RKNN工具链版本和板端Runtime版本不匹配输入shape不匹配内存不足特别是INT8量化时workspace设置过大排查时先看日志TensorRT和RKNN都会输出详细的错误信息。如果日志不明确用最小复现拿一张固定图片写一个最简单的推理脚本逐步排除。7. 从部署到落地我的实操建议机载AI部署这件事我的核心体会是不要追求一步到位。先在PC上把ONNX导出和验证跑通再在目标板子上用FP16跑通最后才考虑INT8量化。每一步都做精度和性能的基准测试记录数据这样出问题的时候才能快速定位。另外部署代码要模块化。预处理、推理、后处理分开写每个模块单独测试。我见过太多人把整个管道写在一个函数里出问题根本没法排查。最后说一个容易被忽视的点机载AI的功耗预算要留余量。计算板标称15W实际峰值可能到20W以上供电模块要按1.5倍余量设计。散热也要留余量夏天户外飞行环境温度可能到40度散热设计要按这个工况来。我在实际项目里踩过最大的坑是忽略了摄像头和计算板之间的接口带宽。USB3.0理论带宽5Gbps但实际有效带宽可能只有2Gbps左右1080p 60fps的原始视频流就接近这个上限了。后来换成MIPI CSI接口带宽问题才解决。所以选型时一定要把接口带宽算进去别只看推理性能。