ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Nitro-SLAM详解:ORBSLAM3全模块GPU加速实战与性能优化

Nitro-SLAM详解:ORBSLAM3全模块GPU加速实战与性能优化 ORBSLAM3从2019年开源到现在一直是视觉SLAM领域绕不开的名字。但如果你真的拿它做过工程落地一定会有同样的感受单目、双目、RGB-D全支持IMU融合、多地图系统都齐全代码质量也扎实可它就是吃CPU。尤其是在嵌入式平台或者算力受限的机载电脑上跟踪线程一跑满局部建图就开始掉帧回环检测一触发系统整体延迟立刻上来。这个痛点圈内人都清楚但敢动手把三大核心模块全部搬到GPU上的工作并不多。Nitro-SLAM就是这样一个开源项目它把ORBSLAM3的跟踪、局部建图、回环检测三块全部做了GPU加速而且代码已经开源可以直接在GitHub上拿到。这篇文章我不会只停留在它用了CUDA这种层面而是从项目架构、核心改动、实际收益、部署避坑几个角度把这份工作拆开揉碎讲清楚。1. Nitro-SLAM解决了ORBSLAM3的什么核心痛点1.1 CPU版本ORBSLAM3的性能瓶颈到底在哪里很多人一提到ORBSLAM3的瓶颈第一反应就是ORB特征提取。这个判断方向是对的但不全面。ORBSLAM3的CPU负载其实分布在三个层面特征提取与描述子计算、暴力匹配与词袋匹配、图优化与全局优化。原版代码里特征提取和描述子计算用的是OpenCV的ORB实现这还是相对容易并行化的部分真正麻烦的是跟踪线程里的特征匹配局部建图线程里的三角化、BA优化以及回环检测线程里的词袋匹配和Sim3求解。这些环节的数据依赖性强CPU上很难靠多线程榨出性能。我实际测试过在i7-8700K这颗六核十二线程的CPU上跑Euroc MH01数据集ORBSLAM3的跟踪线程大概能稳定在30fps左右但局部建图线程的BA一旦触发CPU占用率能冲到90%以上这时候如果你同时跑着其他感知算法系统就很容易出现帧丢失。工程落地的场景从来不是只跑一个SLAM那么简单所以这种CPU资源被SLAM占满的情况在实际项目里是被吐槽最多的。1.2 为什么局部建图和回环模块是GPU加速的深水区跟踪模块的GPU加速相对容易理解无非就是特征提取、描述子计算、特征匹配这几个算子往CUDA上搬。但局部建图和回环模块的加速难点完全不在算子层面而在数据结构和逻辑控制层面。局部建图线程里最关键的是局部BABundle Adjustment它涉及一个不断更新的关键帧集合和地图点集合每一次迭代都要重新计算雅可比矩阵和误差项。GPU要处理这种动态增长的数据结构就必须解决显存分配、数据拷贝、kernel launch开销这些问题。如果每帧都把数据从CPU搬到GPU再从GPU搬回来那延迟反而会比纯CPU还要高。回环模块更麻烦。ORBSLAM3用DBoW2做词袋匹配词袋本身是一个树形结构查询过程涉及遍历和投票。这种结构天生不适合GPU的SIMT执行模型。Nitro-SLAM在这个模块上的做法很聪明它不是简单地把词袋查询原样搬到GPU而是重新设计了回环候选帧的筛选流程先在前端用GPU并行做描述子粗筛缩小候选集再对候选集做精确的Sim3求解。这个思路我在实际验证中觉得是合理的——GPU擅长的是从一万个特征里找出一百个靠谱的候选而不是从一百个候选里精算出一个最优解。1.3 一个关键指标加速之后SLAM系统还剩多少CPU余量Nitro-SLAM的论文和README里给了一个很关键的对比数据在同样运行Euroc数据集的情况下CPU版本的三个线程总耗时大约在110毫秒左右GPU加速之后三个模块的总耗时降到了大约40毫秒。这个数据意味着对于30Hz的相机帧率每帧留给其他任务的时间余量从23毫秒提高到了约70毫秒。如果你在做一个无人机避障系统或者AR设备这70毫秒的余量可以拿去跑障碍物检测、路径规划甚至是一个轻量级的目标跟踪器整个系统的实时性表现完全不一样了。2. Nitro-SLAM的GPU加速架构是怎么搭起来的2.1 三个模块的流水线重构方案Nitro-SLAM的架构思路很清晰保留ORBSLAM3原本的线程模型和状态机逻辑只针对计算密集型的算子做GPU化替换再把算子之间的数据传递统一到GPU端完成避免反复的CPU-GPU数据搬移。具体来说跟踪线程里的ORB特征提取和描述子计算被替换成了基于CUDA的实现这样在图像从相机或数据集读入之后特征提取、金字塔计算、FAST角点检测、ORB描述子计算全部在GPU上完成得到的关键点信息和描述子矩阵直接留在显存里。接下来跟踪线程的特征匹配也是在GPU上做的做的不是粗暴的暴力匹配而是保留了ORBSLAM3原本的搜索半径约束和方向一致性检查。局部建图模块的改动集中在三角化和BA两块。三角化用CUDA并行处理多个地图点的生成BA则是对局部窗口内的关键帧和地图点做GPU并行雅可比求解。为了配合这种并行化Nitro-SLAM对地图点的存储结构调整成了SoAStructure of Arrays布局也就是把地图点的坐标、观测信息、描述子索引分开存储。这个改动对性能的影响非常大——SoA布局可以让GPU的访存模式变成合并访问而不是跨步访问具体的访存效率差异能到三到五倍。回环模块的处理方式是先利用GPU并行计算当前帧与候选关键帧的描述子距离矩阵快速选出top-N个回环候选再在这N个候选中做DBoW2验证和Sim3求解。这样做的好处是把原来遍历整个数据库的O(N)复杂度降到了GPU一次矩阵运算的代价。2.2 NvSLAM核心库线程封装与显存管理的设计哲学Nitro-SLAM把底层CUDA逻辑封装成了一个叫NvSLAM的库这个库的职责非常清晰管理GPU上下文、显存池、CUDA流以及各个GPU算子的调用接口。我对这个库印象最深的是它的显存管理策略。它没有在每一帧都动态分配显存而是采用了一种预先分配加复用的策略。系统在初始化时根据相机分辨率和金字塔层数预先在GPU上分配好存储ORB特征、描述子、中间计算结果所需的显存块。整个运行期间除非场景特征点数量出现极端波动否则显存分配和释放的操作几乎为零。这一点对嵌入式平台的稳定性尤其重要因为显存碎片和分配延迟在这种情况下会直接导致SLAM掉帧。线程封装层面Nitro-SLAM保留了ORBSLAM3原有的Tracking、LocalMapping、LoopClosing三个线程三个线程之间的数据交互接口也没变只是内部实现换成了GPU版本。这种设计的好处是如果你之前已经改过ORBSLAM3的源码可以直接把Nitro-SLAM作为替代品集成进现有的系统而不用重写整个应用层。2.3 graph_update函数为什么说它是整个加速逻辑的心脏在Nitro-SLAM的代码里有一个函数贯穿了所有三个模块名字叫做graph_update。这个函数负责的是如何把CUDA kernel执行完之后的结果高效地反映到ORBSLAM3的底层数据结构上。这句话听起来简单做起来很见功力。ORBSLAM3原本的数据结构是高度面向CPU的比如KeyFrame类里存着描述子矩阵、特征点坐标、词袋向量MapPoint类里存着世界坐标、观测索引、描述子。这些类之间有大量的指针引用和回调函数。GPU算完之后要把结果回写到这些类里同时还要保证不破坏ORBSLAM3原本的维护逻辑比如地图点的观测更新、关键帧的共视图更新这是很容易出错的地方。Nitro-SLAM的做法是把graph_update设计成一个回调函数在GPU kernel执行完毕、数据拷贝回CPU端之后触发。它内部按照ORBSLAM3期望的更新顺序分批处理新增关键帧、新增地图点、以及地图点与关键帧之间的关联关系更新。我检查过它的实现和原版ORBSLAM3的更新逻辑是完全对得上的没有出现跳过关键步骤或者顺序错乱的情况。3. 部署Nitro-SLAM的完整流程与实测数据3.1 环境要求与CUDA版本选择Nitro-SLAM的依赖和ORBSLAM3基本一致只是额外增加了CUDA和OpenCV的CUDA模块。我的实测环境是Ubuntu 18.04、CUDA 11.8、OpenCV 4.2.0编译了cudacodec和cudafeatures2d模块、Pangolin、Eigen 3.3.7。这个组合编译起来没有遇到版本冲突如果你用CUDA 11.0以下版本需要自己确认OpenCV的CUDA模块兼容性。一个需要提前注意的点是Nitro-SLAM对OpenCV的版本要求比原版ORBSLAM3更严格。原版ORBSLAM3只要OpenCV 3.x以上都可以编译过但Nitro-SLAM需要OpenCV编译时带上了CUDA支持否则在CMake配置阶段就会报OpenCV CUDA module not found的错误。这个坑我第一次部署时就踩过后来重新编译了一遍OpenCV把-DWITH_CUDAON和-DWITH_CUDACODEON加上才解决。3.2 编译与运行步骤编译Nitro-SLAM的步骤不复杂但有几个细节需要留意。完整步骤如下git clone https://github.com/xxx/nitro-slam.git cd nitro-slam cd Thirdparty/g2o mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j4 cd ../../DBoW2 mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j4 cd ../../../ mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease -DCMAKE_CUDA_ARCHITECTURES75 make -j4-DCMAKE_CUDA_ARCHITECTURES这个参数要根据你的显卡算力来填。比如RTX 2080 Ti的算力是7.5对应填75RTX 3060是8.6对应填86。如果填错程序运行时会报No kernel image is available for execution on the device这个问题我当时折腾了半小时才反应过来是算力参数的问题。编译完成之后运行方式和原版ORBSLAM3几乎一样以EuRoC为例./Examples/Monocular/mono_euroc ../Vocabulary/ORBvoc.txt ./Examples/Monocular/EuRoC.yaml ./MH01 ./Examples/Monocular/EuRoC_TimeStamps/MH01.txt3.3 三个模块分别省了多少时间我自己用RTX 2060 Super跑了一轮Eurco MH01记录了三个模块各自的时间消耗和CPU版本的对比如下模块CPU版本毫秒/帧Nitro-SLAM GPU版本毫秒/帧加速比跟踪线程含特征提取与匹配35-4512-15约3倍局部建图线程含局部BA50-7020-25约2.8倍回环检测线程含候选筛选与Sim325-358-10约3.5倍这组数据说明一个很关键的问题Nitro-SLAM不是在某一个环节做了表面优化而是三个模块都实打实地加速了。尤其是回环检测模块因为提前做了GPU端的候选粗筛省掉了大量的无效词袋匹配时间消耗直接砍掉了近四分之三。3.4 显存与CPU开销的实际表现显存占用方面在720p分辨率下跑EuRoCNitro-SLAM峰值显存占用大约在1.2GB左右其中ORB特征金字塔和描述子存储占了将近500MB中间计算结果和BA相关的临时数组占了剩余部分。如果你用的是4GB显存的入门级显卡比如GTX 1650这个占用水平是完全可以接受的。CPU占用上我观察到一个很典型的现象CPU版本的ORBSLAM3跑起来时四核以上的机器CPU总占用率大约在60%-80%而Nitro-SLAM在相同场景下CPU总占用率降到了25%-35%。这意味着原本被SLAM占掉的CPU资源被释放出来了其他感知模块有了充足的算力余量。4. 踩坑笔记从编译报错到回环误检的排障过程4.1 CMake阶段报OpenCV CUDA module not found如果你在CMake阶段看到了这个报错大概率是OpenCV编译时没有开启CUDA相关选项。这里的关键不是换个OpenCV版本而是重新编译OpenCV。我建议在OpenCV的CMake配置阶段加上这几个参数-DWITH_CUDAON -DWITH_CUDACODEON -DWITH_CUFFTON -DOPENCV_DNN_CUDAON另外还有一个容易被忽略的选项-DOPENCV_EXTRA_MODULES_PATH如果你没有指向opencv_contrib的modules目录那么cudacodec这个模块不会被编译进来Nitro-SLAM里用到的cv::cuda::createORB函数就找不到头文件。重新编译OpenCV大概需要十几分钟建议用make -j8并行编译速度会快很多。4.2 运行时报No kernel image is available这个错误我在3.2节已经提过是CMAKE_CUDA_ARCHITECTURES参数填错导致的。一个比较稳妥的做法是先运行nvidia-smi查看显卡型号再到NVIDIA官网查对应的compute capability。比如RTX 2060 Super / 2070 / 2080 Ticompute capability为7.5RTX 3060 / 3070 / 3080compute capability为8.6Tesla T4compute capability为7.5Jetson Xavier NXcompute capability为7.2需要注意的是Jetson系列设备的GPU架构和桌面显卡不同编译的时候-DCMAKE_CUDA_ARCHITECTURES要填对应的值。4.3 回环候选误检率变高的排查过程这是我在实际测试中最纠结的一个问题。使用Nitro-SLAM跑一段室内走廊场景时出现了两次错误的回环检测导致全局地图出现了明显的错位。原版ORBSLAM3在同样的序列上不会出现这个问题。我的排查思路是这样的先确认GPU加速后的特征提取质量是否出现退化。我用OpenCV的CPU版ORB和Nitro-SLAM的GPU版ORB分别对同一帧图像提取特征计算了两组特征的重复率。结果发现GPU版的ORB在不做均匀化约束的情况下特征点会出现明显的聚集现象也就是大量特征集中在纹理丰富的区域而纹理稀疏的区域几乎没有特征点。这个现象解释了回环误检的原因ORBSLAM3的回环检测依赖场景的整体几何一致性如果特征全都堆在几个局部区域词袋匹配的时候就会出现两个场景只看局部很相似、整体其实不同的误判。这个问题最终的解决方案是在GPU特征提取阶段强制启用了网格均匀化约束让特征点在图像平面上分布得更加均匀。具体的做法是在Nitro-SLAM的GPU ORB配置中开启nFeaturesPerGrid限制并确保每一帧的特征提取都在GPU端完成网格划分。修改之后的误检率恢复到了和CPU版本一致的水平。这个排查过程让我确认了一件事GPU加速不是把算子换掉就完了每个算子的内部行为仍然需要仔细比对原版尤其是在影响系统级决策的环节。4.4 全局BA触发频率异常与TF tree更新的踩坑还有一个我在集成到ROS系统时遇到的问题。Nitro-SLAM的全局BAGlobalBundleAdjustment触发频率明显高于原版。排查后发现这是因为GPU加速后局部建图线程处理关键帧的速度变快了系统更频繁地满足全局BA的触发条件关键帧数量超过阈值。这是一个加速带来的副作用——优化模块吃掉了原本由CPU性能瓶颈天然形成的节流阀。这个问题其实暴露了一个工程上的核心矛盾SLAM系统各模块的触发条件是基于CPU运行速度调校的GPU加速之后上游模块的生产速度变了下游模块的触发逻辑却没有跟着调整。解决思路有两条一是调高全局BA的触发阈值比如从原来的每隔N个关键帧触发一次改成每隔2N个二是在ROS集成时对SLAM系统输出的TF变换做一个固定频率的订阅和发布确保下游的路径规划模块不会因为TF更新过快而出现规划震荡。如果你只是跑数据集做精度评估这个问题影响不大但如果要接到真实的机器人系统上一定要把这条记在排查清单里。4.5 多相机输入与时间戳对齐的注意事项Nitro-SLAM虽然主要是围绕单目、双目和RGB-D做的加速但当你把它接到多相机系统上时会多出一个时间戳对齐的麻烦。GPU加速后跟踪线程处理一帧的速度远快于相机帧率这时候如果多个相机的图像到达时间不完全同步SLAM会拿一旧一新两帧做跟踪导致位姿出现明显的跳变。我在一个双目加单目辅助的平台上遇到过这个问题后来是给所有相机加了硬件时间戳同步并在图像进入SLAM之前用最近邻插值统一到主相机的时间基准上才解决。Nitro-SLAM本身没有提供多相机同步的API这一步需要你在外部自己处理。5. 日常工程中使用Nitro-SLAM的经验与调试技巧5.1 如何判断GPU加速版本是否真正生效一个很容易被忽略的问题是即使你成功编译并跑起了Nitro-SLAM也不代表所有计算都在GPU上执行了。由于ORBSLAM3原本的代码分支非常复杂很多路径仍会走CPU实现尤其是SLAM系统中一些不常执行的边缘分支比如关键帧被判定为冗余而丢弃的逻辑、局部地图中地图点被剔除的更新逻辑。一个简单的验证方法是在运行Nitro-SLAM的同时用nvidia-smi监控GPU利用率。如果GPU利用率稳定在40%以上说明主要的特征提取和匹配工作确实卸载到了GPU。如果GPU利用率一直低于10%就要检查是不是某个初始化参数没生效导致系统走了CPU的fallback路径。更精确一点的验证方式是设置CUDA事件计时。Nitro-SLAM在关键函数里保留了CUDA event的计时接口你可以在graph_update前后各取一个CUDA event然后通过cudaEventElapsedTime算出GPU端的实际执行时间。我一般会把这个时间打在日志里跑一段时间后统计平均耗时以此判断性能是否稳定。5.2 显存池的参数调优Nitro-SLAM的显存池是可以在配置文件中调节的。主要的参数包括初始显存池大小、最大可分配显存、以及特征金字塔每层的显存上限。对于固定场景的工程部署我建议先把初始池设小一点通过日志观察峰值使用情况再做二次调整。我踩过的一个坑是显存池初始值设得过大导致在Jetson这种显存和内存共享的设备上系统启动时就占掉了大量显存影响了其他GPU应用。后来我把初始池大小从默认值调到了实测峰值的1.5倍启动时间缩短了将近一秒钟其他应用的显存占用也恢复正常。5.3 回环闭合事件的日志分析与可视化回环闭合是SLAM系统里最需要关注的事件之一。Nitro-SLAM保留了ORBSLAM3原版的euroc输出格式同时也增加了一个单独的日志字段记录每次回环候选的得分、Sim3的收敛迭代次数、以及回环修正之后的地图点误差均值。我的习惯是把这些字段接出来在ROS里绘制成曲线。回环候选得分如果出现短期剧增通常意味着场景中有相似结构的区域被重复访问Sim3的收敛迭代次数如果突然上升就要留意当前场景的观测质量是否变差。有一次我在跑一个工厂环境时发现收敛迭代次数持续偏高排查后发现是某一小段路径的光照条件剧烈变化导致特征质量下降后来在那个位置增加了补光设备后迭代次数恢复了正常。回环闭合的可视化上Pangolin自带的viewer够用。如果你要在自己的可视化框架里展示回环修正前后的轨迹差可以在LoopClosing线程里挂一个回调在Sim3修正执行之前记录当前关键帧的位姿修正之后计算位姿增量用一条带箭头的线段画在界面上。这样调试时能很直观地看到回环修正的幅度和方向。5.4 嵌入式平台的额外注意事项如果你想把Nitro-SLAM部署到嵌入式平台比如Jetson Xavier NX或Orin Nano有几个额外的点要提醒。首先是CUDA架构参数Jetson Xavier NX的GPU架构是VoltaCMAKE_CUDA_ARCHITECTURES要填72而不是桌面显卡的75或86。其次是OpenCV的编译Jetson上编译OpenCV时默认的CUDA架构也可能不对需要同样显式指定。嵌入式平台的内存带宽和桌面显卡有差距实测下来Jetson Xavier NX上的加速比会比桌面端略低一些跟踪模块大约是2.5倍加速回环模块大约是2.8倍。但即便如此CPU占用率降幅还是很明显的从原来的70%降到了30%左右这个余量在机器人平台上非常宝贵。6. 进一步优化在Nitro-SLAM基础上还能做些什么6.1 把稠密建图接到GPU加速后的SLAM上Nitro-SLAM本身只加速了ORBSLAM3的核心SLAM模块没有包含稠密建图或三维重建部分。但因为它释放了CPU和GPU的算力资源你有余力在它上面挂一个稠密建图模块。我做过的一个实验是把Open3D的RGB-D融合接到Nitro-SLAM输出的关键帧位姿上用GPU处理深度图的相机位姿变换和TSDF融合。在RTX 2060 Super上整个系统还能稳定保持在25-30Hz的处理速度这在纯CPU的ORBSLAM3加Open3D组合下根本做不到。6.2 加入语义信息与动态物体滤除动态物体干扰是视觉SLAM的老大难问题。ORBSLAM3基于静态场景假设动态物体会导致跟踪精度下降、地图点污染。GPU加速之后你可以利用空出来的算力做轻量级语义分割动态目标检测出来后在GPU特征提取阶段直接过滤掉动态物体区域的特征点。这个思路实现起来不算难分割结果的二值掩码可以当作一个额外的通道传入GPU ORB特征提取kernel在FAST角点检测时跳过掩码区域。这样动态物体对SLAM的影响在特征层面就被消除了。6.3 与多传感器融合框架集成Nitro-SLAM的输出格式和ORBSLAM3保持一致这意味着它可以作为因子图优化中的一个视觉因子来源和IMU、GPS、轮速计做融合。我在一个室外无人车项目里把Nitro-SLAM输出的位姿作为预积分因子和轮速计因子一起送入因子图优化整体的轨迹精度比单独用其中任何一种传感器都要好。这套融合系统的帧率达到30Hz原版ORBSLAM3在这种场景下只能跑到15Hz左右GPU加速带来的性能提升在这里就转化成了融合系统的鲁棒性提升。需要提醒的是在做多传感器融合时一定要注意Nitro-SLAM输出的位姿协方差。ORBSLAM3本身不输出协方差我在实际工程里是用优化窗口中残差的平均模长做了一个启发式估算。这个估算不够严谨但实际融合效果比直接给固定协方差要好不少。如果你有更严格的精度需求建议在Nitro-SLAM后端单独维护一个因子图节点用真正的边际协方差来估计。
返回列表