系统架构综述
Apollo9.0 的发布,为开发者提供了“灵活易上手,通用易扩展”的自动驾驶框架(以下简称 core 层)。为了加快企业开发者场景落地,基于 core 层,apollo 打造了面向园区低速场景的通用能力(以下简称通用层),加快企业开发者场景落地,实现价值共创。
在通用层,我们对感知、预测、定位、规划、控制、功能安全等核心模块都进行了能力增强。
感知
在感知模块,相较于 core 层,通用层提供了 lidar_segmentation、pointcloud_semantics 两个全新的包。并对 traffic_light_detection、traffic_light_recognition、camera_detection_multi_stage 等 core 层的包做了效果或性能的增强。
另外,对感知的核心模型 centerpoint 也进行了效果的提升。
Camera
traffic_light感知
traffic_light_detection 提供了红绿灯灯框检测的能力,traffic_light_recognition 提供了红绿灯灯色识别的能力。相较于 core 层的同名模块,通用层的模块有以下几点提升:
-
使用了较新的 yoloX/efficient_net 模型进行了重新训练,替代了以前的老旧的 caffe 模型。通过千万级的红绿灯灯框/灯色标注数据的训练,模型拥有较高的泛化性,支持路口常见的红绿灯。端到端识别准确率达到 99% 以上。
-
通过多 batch 优化、模型量化加速等方法,推理时延下降了 58.4% ,GPU 占有率下降了 83.8% ,显存占用率下降 18.5% 。
PointCloud
lidar_segmentation
lidar_segmentation 为激光雷达检测提供“兜底”的检测能力,通过图聚类算法,可以召回那些深度学习模型漏检或不在模型检测范围内的障碍物,最终使障碍物检测的召回率接近 100% ,为自动驾驶的安全性提供重要的保障。
pointcloud_semantics
pointcloud_semantics 引入了激光雷达语义分隔的能力,即通过深度学习模型逐点为点云进行语义分类,包括:地面、绿植、栅栏、障碍物、路沿等语义类别。 丰富的语义信息,为感知能力的提升,例如绿植过滤、扬尘雨雾过滤、地面点识别、路沿识别等提供了准确的基础信息。
在通用层,我们同时也提供了绿植过滤等能力。在环卫版本,我们提供了路沿识别的能力。
lidar_detection
相较于 core 层的 centerpoint 模型,我们使用了数倍的激光雷达 3d 标注数据(百万级)进行了模型效果的增强。整体感知准召率提升 20% 以上。
pointcloud_ground_detection
对地面检测算法进行优化,提升了对高度 >20cm 的低矮障碍物的检测能力。
定位
相较于 core 层,通用层的定位模块提供了激光雷达 slam 定位能力,可以解决高楼、茂密绿植等RTK失锁场景下的定位无解、漂移问题。相较于业界开源的众多 slam 算法,通用层的激光雷达 slam 算法在效果、性能等方面进行了长期的打磨:CPU 平均利用率在 45% (单核、nvidia orin 环境)左右,定位误差<10cm。
我们即将推出一款融合激光SLAM、RTK、轮速计和IMU等多源信息的定位框架,该框架基于卡尔曼滤波技术实现更加精准定位。 视觉定位的能力也在规划中。
预测
障碍物轨迹预测模块(prediction),负责预测感知输出的障碍物未来的运动轨迹。相较于 core 层的轨迹预测模块,通用层做了以下几点能力的提升:
-
性能优化:使用多目标 vectornet+tnt 预测模型进行行人轨迹预测和机动车轨迹预测,替换了 core 层的语义地图+LSTM单目标轨迹预测模型,模型只需要一次推理即可输出多个障碍物的预测轨迹,解决了障碍物较多情况下因线程抢占导致的各模块帧率下降的问题。在模型部署上,通过 tensorRT、fp16 量化等优化手段,替代了 libtorch 低效的推理框架,在园区上下班高峰情况下,优化后的预测模块帧率稳定在 10hz。
-
效果提升:通过深度学习网络 encoder/decoder 结构的优化,基于大量城市场景数据预训练,使用园区场景数据进行效果 finetune,机动车和行人的轨迹预测误差(minADE)降低 15% 左右,自动驾驶因预测导致的急刹次数降低 30% 以上。
决策规划控制
决策规划模块(planning),负责决策车辆的行为和规划车辆的行驶轨迹。相较于 core 层的决策规划模块,通用层中新增了多项能力,用于解决客户实际业务场景中遇到的问题,并在车辆的控制精度及流畅度方面也有了大幅提升:
-
新增了在限定区域调整位恣的能力,用于在限定区域掉头,灵活调整自车的位恣;
-
新增了在限定区域通过倒车来绕行障碍物的能力,提升了自车的通过性;
-
新增了车辆行车场景下的自主脱困能力,提升了自车在复杂行车场景下的通行能力;
-
新增了在U-turn场景的掉头能力,提升了车辆在大曲率场景的通过能力;
-
新增了在坡道处起步防溜车的能力,提升了自车乘坐的舒适性和安全感;
-
通过引入多场景控制参数动态适配,自车巡航行驶的横向控制平均误差小于 0.2m,速度跟随平均误差小于 1km/h;泊车终点的横向位置误差小于 0.3m,纵向位置误差小于 0.2m,航向误差小于 5 度。
-
决策规划:
-
控制模块
在细分测试场景方面,通用层也有了大幅提升,在遇行人急刹场景、绕行障碍物场景、掉头场景、靠边停车场景、泊车场景整体通过率均达到 90% 以上。
planning 模块架构图
功能安全
我们提供一套完善的安全机制,涵盖了信息采集、异常监测、故障报警、故障处理以及结果展示等全流程功能,覆盖硬件、软件、车体、网络多种故障维度,同时支持企业开发者根据自身需要定制故障类型。
硬件选型
为了满足企业开发者对硬件的成本及稳定性的高要求,Apollo 开放平台 9.0 实现了 ARM 架构 Orin 设备的良好适配,且可以支持多个 Lidar 和 Camera 的高性能运行,帧率在 10HZ 以上,CPU/GPU 利用率均在 60% 以下。同支持时间同步等功能,相比于 X86 工控机成本可降低 30% 以上。在感知传感器方面,Apollo 也与硬件厂商和合作伙伴构建了共建共享的协作机制,丰富了大量的设备选型。如相机支持更多的主流厂商、相机接口也从 USB3.0 升级为 GMSL,新增多品牌多型号激光雷达,定位设备也新增多家厂商适配。详细内容请参阅 Apollo硬件开发平台。
场景验证
-
通过功能增强、安全及稳定性提升,结合核心层的工程结构、算法能力和文档升级,整体适配环节减少 40% ,代码阅读量减少 90% ,代码调试量减少 80% 。
-
Apollo 联合企业合作伙伴经过长期的持续测试,并在教育、矿卡、物流、环卫、巡检等超 5 个场景落地。
-
在长期的运营过程中,发现并解决了很多 bug,同时也对性能、效果进行了提升,整体达到了一个成熟、稳定的状态。
-
经过 10 余家合作伙伴验证,自动驾驶任务成功率稳定在 98% 以上。
-
目前已有部分企业开发者基于 Apollo 开放平台提供的通用能力,在一个月内完成了零售、环卫场景的业务系统闭环,并圆满完成 ITS 大会展示。
-
Apollo 技术优化成果显著
-
Apollo 自动驾驶技术落地情况
cyberRT介绍
Cyber RT 组件机制
概述
Cyber RT是一个高性能、高吞吐、低延时的计算运行框架,其中,动态加载技术和有向无环图(DAG)是其实现高性能重要途径之一。
Cyber RT采用了基于Component模块和有向无环图(DAG)的动态加载配置的工程框架。即将相关算法模块通过Component创建,并通过DAG拓扑定义对各Component依赖关系进行动态加载和配置,从而实现对算法进行统一调度,对资源进行统一分配。采用这个工程框架可以使算法与工程解耦,达到工程更专注工程,算法更专注算法的目的。
什么是 Component
Component 是 Cyber RT提供的用来构建功能模块的基础类,可以理解为Cyber RT对算法功能模块的封装,配合Component对应的DAG文件,Cyber RT可实现对该功能模块的动态加载。以Apollo为例, Apollo 中所有的模块都由 Component 构建的。
被 Cyber RT加载的 Component 构成了一张去中心化的网络。每一个Component是一个算法功能模块,其依赖关系由对应的配置文件定义,而一个系统是由多个component组成,各个component由其相互间的依赖关系连接在一起,构成一张计算图。
下图是具有3个component,2个channel的简单网络:
Component 的类型
Component 有两种类型,分别为 apollo::cyber::Component 和 apollo::cyber::TimerComponent 。
-
Component 提供消息融合机制,最多可以支持 4 路消息融合,当 从多个 Channel 读取数据的时候,以第一个 Channel 为主 Channel。当主 Channel 有消息到达,Cyber RT会调用 Component 的 apollo::cyber::Component::Proc 进行一次数据处理。
-
TimerComponent 不提供消息融合,与 Component 不同的是 TimerComponent 的 apollo::cyber::TimerComponent::Proc 函数不是基于主 channel 触发执行,而是由系统定时调用,开发者可以在配置文件中确定调用的时间间隔。
Component 的创建及如何工作
1、包含头文件;
2、定义一个类,并继承Component或者time Component;根据Component功能需要,选择继承Component或者继承TimeComponent。
3、重写Init()和Proc()函数;Init()函数在 Component 被加载的时候执行,用来对Component进行初始化,如Node创建,Node Reader创建,Node Writer创建等等;Proc()函数是实现该Component功能的核心函数,其中实现了该Component的核心逻辑功能。
4、在Cyber RT中注册该Component,只有在Cyber RT中注册了该Component,Cyber RT才能对其进行动态的加载,否则,cyber RT动态加载时报错。
Component 的通信是基于 Channel 通信实现的,使用 reader 和 writer 对 channel 读写实现数据读取与写出。
Component 如何被加载
在 Cyber RT中,所有的 Comopnent 都会被编译成独立的.so文件,Cyber RT 会根据开发者提供的配置文件,按需加载对应的 Component。所以,开发者需要为.so文件编写好配置文.dag文件和.launch文件,以供 Cyber RT正确的加载执行Component。
Cyber RT提供两种加载启动Component的方式,分别是使用cyber_launch工具启动component对应的launch文件,和使用mainboard启动component对应的dag文件。
cyber_launch工具可以启动dag文件和二进制文件,而mainboard执行启动dag文件。
Component 的优点
相较于在 main() 函数中写通信逻辑并编译为单独的可执行文件的方法,Component 有以下优点:
-
可以通过配置 launch 文件加载到不同进程中,可以弹性部署。
-
可以通过配置 DAG 文件来修改其中的参数配置,调度策略,Channel 名称。
-
可以接收多个种类的消息,并有多种消息融合策略。
-
接口简单,并且可以被 Cyber 框架动态地加载,更加灵活易用。
Cyber RT 插件机制
概述
首先,什么是插件?其实就是遵循一定规范的应用程序接口编写出来的程序,可以为我们的应用或者系统扩展出原来不存在的特性或功能;一个应用软件拥有了插件机制之后,不管是应用软件维护方,还是第三方开发者可以以更加简单的方式来扩展功能以应对更多更复杂的场景,同时插件机制可以让应用程序的核心逻辑变得更加精简,更容易让人理解和使用
而在 Cyber 里,其实我们熟知的组件(Component)也是一种插件,我们只需要重写 apollo::cyber::Component::Init 和 apollo::cyber::Component::Proc 函数,就可以轻松写出能与感知,规划等组件通信的程序,从而扩展新的自动驾驶能力
插件机制设计
整体上来看,插件机制有几个重要的概念,插件管理类,插件基类与插件类,以及插件描述文件
插件管理类
插件管理类的代码实现是在 cyber/plugin_manager目录下,是一个单例实现,其中包含两核心方法,和一个插件注册宏
-
apollo::cyber::plugin_manager::PluginManager::LoadInstalledPlugins: 是用于扫描和加载插件,加载过程中同时也会建立插件基类与插件类的索引,便于后续实例化时使用;我们在mainboard的启动流程已经默认执行了此方法,用户如果是想在组件(Component)中使用的话,无需自行调用;而如果是想在其它自定义的二进制程序中使用的话,则需要用户主动调用一下;
-
apollo::cyber::plugin_manager::PluginManager::CreateInstance: 是用来创建插件实例的,此方法是一个模板方法,接收一个string类型的参数,传入插件类名,即返回一个插件基类的指针,因此,对于核心流程的逻辑来说,只需知道基类即可,而不需要知晓插件类的定义;将插件功能与应用程序解耦隔离开来;
-
CYBER_PLUGIN_MANAGER_REGISTER_PLUGIN: 是用将插件类与插件基类绑定的,其逻辑与组件的注册类似,在动态库文件加载时,会自动执行并调用class_loader里的注册方法,只有注册好的插件才能被CreateInstance方法创建
插件基类与插件类
插件基类,是插件设计者对于业务逻辑的一个抽象
比如规划模块中的 apollo:planning::TrafficRule ,两个核心的接口方法就是 apollo::planning::TrafficRule::Init 和 apollo::planning::TrafficRule::ApplyRule ,每一个规则插件初始化时都会调用 Init 方法,而在应用规则时则调用 ApplyRule方法;
再比如 apollo::planning::Task ,核心接口是 apollo::planning::Task::Init 和 apollo::planning::Task::Execute ,同样地,初始化时会调用 Init 方法,而在插件调用(即执行规划任务时)则调用 Execute 方法;
可以看到,其实插件基类是插件与应用程序之间的接口规范,它定义了插件的基本我以及我们应该如何来编写插件,而插件类就是具体的插件实现,只要继承基类,以及调用插件注册的
插件描述文件
即描述插件类与基类以及所在类库之间关系的文件,在加载时,插件描述文件会被解析成 apollo::cyber::plugin_manager::PluginDescription 结构体,并以插件类名建立索引,即通过插件类名可以快速找到其所属基类以及所在的库文件
例: modules/planning/scenarios/lane_follow/plugins.xml
插件工作原理
插件的工作流程基本分这么3个阶段,加载阶段,实例化阶段以及调用阶段
-
首先是加载阶段,通过插件管理类的加载方法,可以扫描插件的安装目录,并读取插件描述文件,在这一阶段,会根据插件类所在动态库地址,类名,基类名等信息构建好插件的索引
在 mainboard 中的代码实现中( cyber/mainboard/mainboard.cc ),mainboard 加载组件模块时( apollo::cyber::mainboard::ModuleController::LoadAll ),会根据命令行参数,调用 apollo::cyber::plangin_manager::PluginManager::LoadPlugin 单独加载指定的插件,或者调用 apollo::cyber::plugin_manager::PluginManager::LoadInstalledPlugins 扫描插件安装目录
-
然后是实例化,或者说初始化阶段,这一阶段,插件设计者可以根据配置文件,flag等方式来判断启用了哪些插件,然后通过插件管理类的创建实例的方法来将插件实例化以供使用
在规划模块中 Scenario 的初始化过程( apollo::planning::ScenarioManager::Init ) ,ScenarioManager 从配置中读取了 Scenario 插件列表,然后通过 CreateInstance 一一初始化它们并存储到 scenario_list_ 中以待使用
-
最后是调用阶段,在这一阶段,如果判断条件满足了插件的使用,刚通过已经实例化的插件指针,直接调用插件的方法
还是以规划模块的 Scenario 为例,在 apolo::planning::ScenarioManager::Update 方法中,遍历了 scenario_list_ 中的所有插件,对于满足条件 scenario->IsTransferable(current_scenario_.get(), frame) 的插件执行 Scenario 切换逻辑,即调用 apollo::planning::Scenario::Exit 方法退出当前 Scenario ,然后调用满足条件的 scenario 的 Enter 方法进入新的 Scenario
插件有什么应用
其实插件的应用非常广,像对于Linux内核来说,各种硬件驱动等模块就是内核的插件;或者说像游戏中DLC和MOD也是一种插件
回到我们 Apollo 里,我们对规划模块做了插件化改造,抽象了 Scenario ,Task ,以及 TrafficRule 等基类,开发者可以只需要按需重写这些基类方法,就可以轻松实现不同场景的规划能力;
Cyber RT 调度机制
Cyber RT调度整体介绍
随着人工智能技术的不断发展,自动驾驶汽车已经开始变为可能。自动驾驶汽车需要同时完成多项任务,包括定位、感知、规划、控制等。如何合理调度编排这些任务,让它们运行得稳定、高效,是我们今天介绍的主要内容。
操作系统调度策略
操作系统进行调度的目的是为了最大化利用系统资源(特别是CPU资源)。调度往往是对有限资源的妥协,本质是为了:效率最大化,兼顾公平。
调度改变了什么?
先看以下的例子,有3个任务,当一个任务执行完成之后,再开始执行另一个任务,3个任务的执行时间分别为10s,6s和4s,我们应该如何编排任务呢?
通过上面的例子可以看出,策略1和策略2的CPU总执行时间不变,但是等待时间变少了。也就是说调度系统帮我们减少了任务的总等待时间。 超市里经常遇到排队的场景,当只买了少量的东西,和买大量东西的人排一队的时候,可能就会遇到等待时间过长的问题。
最短时间优先?
回到上面的例子,最优的调度策略诞生了,它就是最短时间优先,每次最短时间的任务优先执行,这样可以保证任务总等待时间最短。
如何保证公平?
可能马上就有人想到了,每次只创建小任务,这样可以保证自己的任务每次都可以插队,被优先执行。但问题来了,这会导致大任务总是得不到执行,一直处于饥饿状态。oh!!! 效率最大化会导致CPU利用的不公平,而我们要兼顾公平。
程序时间可以预知吗?
最短时间优先的策略很棒,但我们忘记了一个重要问题,在任务执行完之前,我们并不知道任务会执行多久!!!策略失效了!!!
别灰心,有新的方法:时间片轮转。交替执行各个任务,每个任务分配一小段CPU时间,时间用尽则退出,让给其它任务使用。
Tips
要支持时间片轮转,操作系统和硬件配合,实现任务抢占。
Cyber RT的改进
实时操作系统
-
实时操作系统,通过给linux打实时补丁,支持抢占。
-
中断绑定,摄像头,激光雷达,串口等外设需要不停的处理中断,因此可以绑定中断处理程序在一个核上。
Tips
实时操作系统不是万能的。很多人可能会问,假如我有一个实时系统,那么是否意味着任务总是可以按时完成,想法很美好,也符合实时操作系统的定义,但资源是有限的,例如你不可能同时吃饭还喝水。 只凭借有限的资源,却能保证完成无限的事情,如果有这样的系统我想它的成就不亚于永动机。因此当程序处理不过来的时候,我们要检查资源是否足够,特别是忙碌的时候。
资源限制&优先级
Cgroup是 Linux 内核的一个特性,用于限制、记录和隔离一组进程的资源使用(CPU、内存、磁盘 I/O、网络等)。Cgroup 具有以下特性:
-
资源限制 —— 您可以配置 cgroup,从而限制进程可以对特定资源(例如内存或 CPU)的使用量。
-
优先级 —— 当资源发生冲突时,您可以控制一个进程相比另一个 cgroup 中的进程可以使用的资源量(CPU、磁盘或网络)。
-
记录 —— 在 cgroup 级别监控和报告资源限制。
-
控制 —— 您可以使用单个命令更改 cgroup 中所有进程的状态(冻结、停止或重新启动)。
协程
协程。用户态的线程,由用户控制切换。协程的定义可以参考go语言中的GMP 模型
-
M,Machine,表示系统级线程,goroutine 是跑在 M 上的。线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
-
P,processor,是 goroutine 执行所必须的上下文环境,可以理解为协程处理器,是用来执行 goroutine 的。processor 维护着可运行的 goroutine 队列,里面存储着所有需要它来执行的 goroutine。
-
G,goroutine,协程。
协程相对线程的优势
-
系统消耗少。线程的切换用户态->中断->内核态,协程的切换只在用户态完成,减少了系统开销。
-
轻量。协程占用资源比线程少,一个线程往往消耗几兆的内存。
Tips
只能自己yeild,最好不要使用sleep,或者io,不然就会主动休眠
为什么引入协程?因为自动驾驶系统涉及到很多消息传输,因此会导致大量的IO消耗,那么如何减少任务切换带来的开销就显得尤为重要。
引入协程可以减少任务切换带来的开销,从而提高程序的效率。在自动驾驶系统中,大量的IO消耗会使得系统性能下降。通过使用协程,可以将系统的IO操作和非IO操作分别处理,减少任务切换的次数,从而提高系统的响应速度和效率。
此外,使用协程还可以简化代码结构,提高代码的可读性和可维护性。在自动驾驶系统中,涉及到大量的数据的处理和传输,如果使用传统的多线程编程方式,将会使得代码结构复杂,难以维护。而使用协程可以将代码结构简化,使得代码更加清晰易懂,从而提高代码的可读性和可维护性。
综上所述,引入协程可以减少任务切换带来的开销,提高系统的响应速度和效率,同时也可以简化代码结构,提高代码的可读性和可维护性。
Cyber RT调度策略
多优先级队列
对应到GMP模型,Cyber的每个任务都被视为一个协程,协程在线程上运行,并且可以设置协程的优先级。协程通过一个多优先级队列来管理,每次从优先级最高的协程开始执行。
任务窃取
有意思的是有些Processor上的任务分配的比较少,有些Processor上的任务可能分配的比较多,这样会导致一些Processor闲置,而另一些则处于饱和状态。为了提高效率,空闲的Processor会从饱和的Processor上偷取一半的任务过来执行,从而提高CPU利用率。
二种任务类型
组件
组件是Apollo中的最小执行单元,每个组件对应某一项特定的任务,例如定位、控制。多个组件联合起来可以实现更加复杂的功能,例如感知。Apollo一共有2种类型的组件:消息触发型和定时触发型。
自定义任务
如果在程序中想启动新的任务并发处理,可以使用cyber::Async接口,创建的任务会在协程池中由cyber统一调度管理。
Tips
手动创建的线程并没有设置cgroup,因此最好不要手动创建线程,如果有需要可以通过cyber配置来设置cgroup。
Cyber RT 性能分析工具
性能分析能力介绍
随着自动驾驶系统复杂度的增加,越来越多性能问题出现在实际开发、路测的流程中,性能优化已成为整个过程中不可或缺的一环。但是如何发现、分析、定位和解决性能问题相对有一定门槛,并且需要付出额外的工作量/为了解决此类痛点,新版本cyber对框架进行了改造,开发了性能可视化工具,建立了性能分析的能力。cyber的性能分析能力有以下特点:
多维数据采集,全面监控,无所遁形:
cyber性能分析工具能够监测自动驾驶系统关键的性能指标,包括:
通过对这些关键指标的实时跟踪,您可以快速识别出自动驾驶系统中影响性能的瓶颈进程所在。
直观的性能可视化界面,洞察一目了然:
cyber的性能分析能力提供了用户友好的可视化界面,为您展示了所有性能数据的直观视图。借助清晰的图表和报告,您可以轻松解读性能分析结果,指明优化方向。
进程级别分析,宏观洞察,精准定位:
可视化界面提供实时数据允许您观察到每一个进程的性能表现。这种宏观的视角让您可以确定哪一个或哪些进程可能会拖慢整个应用的运行速度,从而作出相应的调整。
函数级别分析,微观解析,深度调优:
更进一步,cyber的性能分析能力中,提供了对自动驾驶系统各模块的进程级别分析,通过这一个功能,能够对每个进程生成cpu、内存以及gpu各项指标(依赖于nvidia nsight系列工具)相应的火焰图、有向图等,您可以识别出代码中最需要优化的部分。这种粒度的分析帮助开发者进行深度优化。
功能使用
前序工作:部署Apollo
要使用新版本cyber的性能分析功能,只需部署最新版本的Apollo即可,整理流程与之前的Apollo部署流程一致
源码
clone Apollo最新代码
使用脚本启动并进入容器
编译Apollo
包管理
clone 最新Apollo工程
安装aem,启动并进入容器
安装Apollo软件包
自动驾驶系统中模块级别的性能分析
cyber会记录所有基于cyber的component、可执行的二进制程序的各项性能指标,并且可以实时查看。通过cyber的可视化工具,可以很容易找到定位到自动驾驶系统中对硬件资源占用高的模块,启动可视化界面也很简单:
启动完毕后,浏览器输入 localhost:5000,即可看见类似下面的网页:
其中左上角的按钮可以用来选择显示的进程,通常是以下的的格式:mainboard.xxx.dag,其中xxx.dag为模块启动的dag名称,例如control模块叫做mainboard.control.dag。另外有两个特例,system表示系统的性能指标,而bvar.python3表示的是cyber_performance这个软件的性能指标。
选择进程后,会有多个图表分别以xx - yy - zz 或者 xx - yy 命名,其中xx代表的是指标的类型,目前有:
-
BASIC:记录基础的性能指标,包括cpu占用、gpu占用、内存占用、显存占用
-
BLOCK_DEVICE_IO:记录块设备相关IO的指标
-
ETHERNET_DEVICE_IO:记录网卡设备IO的指标
-
E2E_LATENCY:模块Proc函数的时延
yy指的是指标名字,而zz只会存在BLOCK_DEVICE_IO和ETHERNET_DEVICE_IO这两个类型中,代表的是实际是设备的名称。
通过上述图表,可以迅速判断自动驾驶系统中,不同进程对cpu、gpu、内存,显存和IO分别占用了多少,帮助定位自动驾驶中的进程瓶颈。
可视化界面显示的是实时值,cyber会将性能数据dump到data目录下。如果需要计算均值,可以使用/apollo/scripts中的performance_parse.py脚本解析:
python3/apollo/scripts/performance_parse.py-f data/performance_dumps.07-29-2024.json
其中:
-f 指定的是落盘的性能监控文件,目前会生成在工程目录的data目录下
自动驾驶系统中函数级别的性能分析
cpu及内存
cyber的性能分析功能可以对cpu和内存使用情况进行采样,最终生成热力图或者有向图进行分析。
使用方式
mainboard 新增了以下参数:
其中 -c 声明开启 cpu 采样功能,-H 开启内存采样功能,可以通过以下命令启动模块:
默认会在当前路径下生成 ${process_group}_cpu.prof 和 ${process_group}_mem.prof 两个采样文件。
采样功能也可以在launch文件中添加,例如planning的launch
添加 cpuprofile 与 memprofile 标签,运行该launch后会在/apollo路径下生成planning_cpu.prof、planning_mem.prof的采样文件。
cpu采样时只会生成一个prof文件,而内存采样时会生成多个prof文件,每个prof文件代表生成时候进程内存使用的情况:
由于内存和cpu采样会对性能造成影响,不太建议同时开启两种采样,建议一次启动只开启一种
生成可视化结果
CPU:使用以下命令对采样结果可视化,生成火焰图:
源码:
包管理:
内存:根据需求生成内存采样的有向无环图或者火焰图
分析内存热点:找出哪些代码路径或函数导致了大量的内存分配
有向图会带有内存信息,更加明确哪个函数分配了多少内存,而火焰图更加直观,开发者根据习惯选择生成何种类型的图即可
分析两个时刻之间哪个函数分配了多少内存,通常用于内存泄漏问题排查
分析
cpu
上述为cyber示例通信程序的火焰图,从下到上代表函数调用栈,长度代表函数在整个进程中占用的CPU资源百分比,例如上图中的ReadMessage,占比81.82,可以看出这部分占用了绝大部分的内存,因此通过火焰图可以得知需要对图中的顶部、较长的函数(占比高)进行优化,从而减少进程对cpu的占用
内存
内存热点分析
通过热力图或有向图找到内存热点分配
热力图和有向图均可找到内存热点分配,同样,热力图从下到上代表函数调用栈,长度代表函数在整个进程中分配的内存百分比;
而有向图部分:
-
第一个数字代表当前函数分配了多少内存
-
第二个数字代表调用的函数分别了多少内存
-
线代表调用链
-
有向图的左上角说明了进程在当前快照时间点中一共分配了多少内存
-
框代表实际的函数,每个框有两个数字:
通过上述方式可以迅速定位到内存热点分配所在的函数以及相应的调用链。
内存泄漏分析:
内存泄漏通常基于以下命令比较不同时间点的内存分配diff情况:
上述命令会生成两个时间点内存diff的有向图,假如发现某个函数在不同时间点的分配的大小一直增加,说明可能存在内存泄露:
gpu及显存
GPU的分析需要基于NVIDIA Nsight Systems工具,分别为:
-
nsys system:用于查找性能瓶颈、理解应用程序结构、识别同步和并发问题以及优化整体性能
-
nsys compute:关注于单个 CUDA 核函数的性能分析,用于调整 CUDA 核函数参数、分析寄存器使用、共享内存使用、执行路径和内存访问模式以提高核函数性能
类似gperftools,nsys会对进程性能产生严重影响,应仅在研发、分析阶段使用,日常路测时不应该使用
使用方式
启动工具对进程采样(以感知为例):
-
profile 指明了要进行采样
-
–gpu-metrics-device all 指明了需要采集所有gpu相关的指标
-
–cuda-memory-usage true 指明了对cuda中分配的显存进行追踪
-
–delay=15 –duration=30 指明了程序启动后15s后才开始采样,采集30秒后结束
-
-f true 指明生成的报告文件覆盖现有报告
-
-o preception 指明生成报告的名称,采集结束后会在当前路径生成perception.nsys-rep文件
-
–trace=cuda,cudnn,cublas,osrt,nvtx 指明采集的接口
-
mainboard ... 是正常apollo启动感知的命令
注意事项:采集需要用sudo权限
打开nsys图形化界面
在界面上打开采集生成的文件
打开后会有以下几栏:
其中:
-
cpu栏包括进程运行过程中瞬时的cpu使用率
-
iGPU栏包括orin上gpu各项性能指标
-
CUDA HW包含各个cuda流中运行的核函数
-
Threads包含进程中运行的各个函数,包括cpu系统库以及cuda、cublas、cudnn等接口,这些函数会将对应的核函数放到流的等待队列中,等待被CUDA HW运行
简单介绍下执行cuda核函数的逻辑,程序在应用层调用某个核函数时,硬件并不是立刻执行,而是将其发射到等待执行的队列中,等gpu能够处理时再进行执行
横轴为进程运行的时间线
展开后:
时间轴上可以分为两部分,第二部分(绿色框)可以看出gpu的行为不断重复,可以看出属于模型推理;第一部分(蓝色框)则是模型在进行初始化,性能分析主要关注的是模型推理部分
放大某一次推理:
刚才提到,iGPU栏包含多个gpu指标,我们比较关注的是SM占用率(对应红框);另外,CUDA HW栏则记录着时间线上在硬件执行的核函数;而绿框则是对应用于发射这个核函数的的cuda API
通过SM Warp Occupancy栏,当SM占用率高(如上图所示,占用率达到95)时,可以迅速定位并获取对应执行的核函数的详细信息:
以及发射该核函数处的调用链:
通过以上信息,对于我们自己写的核函数,可以迅速定位到瓶颈;而对于模型推理,情况就相对复杂,因为里面每个核函数相当于是一个算子,没办法精确匹配到模型的哪一层
针对模型推理问题,如果是使用
-
可以通过tensorRT的API给层命名的方式确定核函数对应的层,nvinfer1::INetworkDefinition::setLayerName
-
或者NVTX创建相应的时间点:
-
tensorRT推理:
-
就可以在nsight system上相应的时间线看到对应核函数的统计
-
paddle推理:paddle内置profiler,也可以查看各层各个算子的运行情况,详细可以参考:https://www.paddlepaddle.org.cn/documentation/docs/zh/2.3/guides/performance_improving/profiling_model.html
内存分析方面,nsight system也有类似的输出:
同样,通过以上信息,对于我们自己写的cudaMalloc等分配函数,可以迅速定位到瓶颈;而对于模型推理,也是需要类似NVTX的方式确定属于哪一层分配的内存
优化建议
cpu
主要的优化思路是对火焰图中的顶部、较长的函数(占比高)进行优化,一般来说可以考虑以下几点:
-
例如上图中tzset这个函数占用了约20的cpu时间片,在未设置环境变量TZ时内核会通过复杂计算求解时区,而设置TZ环境变量后则不存在这种计算
-
使用cuda加速运算,并释放占用的cpu时间,降低cpu占用率
-
使用SIMD指令集并行处理运算:SIMD是单指令多操作的缩写,这类指令集可以同时处理多个数据的数学运算,例如下列代码使用arm架构上的neon指令集并行处理16个像素,将yuyv的图片转换成rgb的图片,降低函数在cpu上占用时间:
-
考虑能否使用更低时间复杂度的算法
-
考虑运算是否可以并行化:
```cpp for (int j = 0; j < height_; j += 2) { for (int i = 0; i < width_; i += 16) { // 读取16个像素点到双通道寄存器中,其中通道1存储y分量,通道2存储uv分量 (32 bytes) uint8x16x2_t row0 = vld2q_u8(usrc + j * width_ * 2 + i * 2); uint8x16x2_t row1 = vld2q_u8(usrc + (j + 1) * width_ * 2 + i * 2);
// 提取y分量 uint8x16_t y0 = row0.val[0]; uint8x16_t y1 = row1.val[0];
// 保存y分量到数组中 vst1q_u8(yPlane + yIndex, y0); vst1q_u8(yPlane + yIndex + width_, y1); yIndex += 16;
// 提取uv分量 uint8x16_t u0 = row0.val[1]; uint8x16_t v0 = row1.val[1];
// 求uv分量均值(减轻下采样丢失数据的影响) uint16x8_t u16_low = vaddl_u8(vget_low_u8(u0), vget_low_u8(v0)); uint16x8_t u16_high = vaddl_u8(vget_high_u8(u0), vget_high_u8(v0)); uint8x8_t u8_avg_low = vshrn_n_u16(u16_low, 1); uint8x8_t u8_avg_high = vshrn_n_u16(u16_high, 1);
uint16x8_t v16_low = vaddl_u8(vget_low_u8(row0.val[1]), vget_low_u8(row1.val[1])); uint16x8_t v16_high = vaddl_u8(vget_high_u8(row0.val[1]), vget_high_u8(row1.val[1])); uint8x8_t v8_avg_low = vshrn_n_u16(v16_low, 1); uint8x8_t v8_avg_high = vshrn_n_u16(v16_high, 1);
uint8x16_t u_avg = vcombine_u8(u8_avg_low, u8_avg_high); uint8x16_t v_avg = vcombine_u8(v8_avg_low, v8_avg_high);
// 保存uv分量到数组中 vst1_u8(uPlane + uIndex, u8_avg_low); vst1_u8(vPlane + vIndex, v8_avg_low); uIndex += 8; vIndex += 8; } yIndex += width_; // Skip to the next line } ```
-
考虑运算能否放在非cpu硬件上执行:例如orin上nvjpeg芯片,可以专门用于处理图片压缩等任务
-
这类函数是否可以去掉:
-
这类函数涉及到复杂运算:
内存
与cpu思路相同,我们需要看有什么函数是我们能够优化的,热力图和有向图能够查看各函数分配内存大概占比以及精确数值,例如预测进程:
一般来说可以考虑以下几点:
-
进程是否引入了cuda与tensorrt等库:cuda与tensorrt等库会在每个进程中申请内存,而这部分内存类似于c++中的“单例”,换句话说这部分内存是可以复用的。假如分别启动感知与预测进程,这部分内存会申请两份;而将感知和预测在launch文件中合并为启动一个进程时,这部分使用的内存只会申请一份。如果组建的自动驾驶系统有大量使用gpu的进程时,可以合并成一份进一步减少内存
-
是否有申请了的内存但是没被使用的情况,这个可以通过采样结果进行详细分析
gpu
gpu的优化分为时延优化和使用率优化,这两个对应着不同的目标:
-
时延优化:降低gpu一个或多个核函数的处理时延,这类优化可能会提高gpu使用率
-
使用率优化:降低gpu一个或多个核函数的使用率
算法优化:
采用更高效的模型/时间复杂度更低的算法
工程优化:
cpu限制
表现:timeline上的空白:
优化方式:确定这段空白时间cpu的行为,并降低空白时间(可参考cpu优化的方式)
对时延有优化效果,对使用率影响不大
timeline上的Memcpy操作:
优化方式:
-
减少冗余的传输
-
使用大块传输,尽可能利用带宽
-
如果硬件支持统一内存(orin),使用统一内存减少memcpy:
统一内存使用示例:
对时延和使用率均有优化效果
小kernel数量多,并且kernel发射时间高于执行时间:
优化方式:
kernel融合:融合小kernel成大kernel,减少发射次数
应用cudaGraph技术:将多次kernel发射合并成1次cudaGraph发射:
cudaGraph使用示例:
对时延有优化效果,对使用率影响不大
当SM占用率未达100时,可并行执行无依赖关系的kernel函数,通过多个流的方式实现(即将串行改为并行)
单个stream:
多个stream:
使用示例:
对时延有优化效果,而使用率也会明显提升
减少kernel的时延:
要减少一个kernel的时延,需要了解了解这个kernel的SOL(Speed Of Light)指标,这个指标可以使用nsight compute采样得到:
-
Memory SOL: 这个指标表示应用程序在访问全局内存时的效率。它是通过将应用程序实际利用的内存带宽与GPU的最大内存带宽相比较得出的。如果Memory SOL很低,意味着可能存在内存访问瓶颈。
-
SM SOL: 这个指标表示应用程序在执行计算任务时的效率。它是通过将应用程序的计算性能与GPU的最大计算性能(浮点运算峰值)相比较得出的。如果SM SOL很低,意味着计算资源可能没有得到充分利用。
一般可能会出现三种情况:
-
访存受限:Memory SOL低
-
计算受限:SM SOL低
优化方式:
-
访存受限:减少使用全局内存,使用block内的共享内存
计算受限:
-
使用高吞吐指令:降低精度(fp64->fp32, fp32->fp16/int8);使用快速数学函数(__func())
-
减少warp内分支:一个warp是由一定数量的线程组成的执行单元,warp是GPU调度和执行指令的基本单位,其中的所有线程都执行相同的指令序列,假设一个warp有32个线程,对于一个存在分支的核函数:
warp的前半部分(线程0-15)将执行操作A,后半部分(线程16-31)将执行操作B。GPU将首先为执行操作A的线程然后为执行操作B的线程。这种情况下,warp的效率只有最大的一半,导致计算受限。
因此,尽可能地确保一个warp内的所有线程执行相同的指令,减少wrap内分支能提升SM的SOL
包管理
软件包管理使用教程 - 基础概念与编译自定义组件和插件
综述
本教程涵盖使用软件包管理下载,安装与构建 Apollo 各模块的基础知识。您将设置工作空间并构建一个简单的 C++ 项目,该项目将说明 Apollo 中软件包管理的关键概念,如 .workspace.json 和 .env 文件。完成本教程后,请参阅 Apollo 研发工具 - buildtool,了解软件包管理中关键工具 buildtool 的高级用法。
在本教程中,您将学到:
-
Apollo 软件包管理模式下的新增的配置信息
-
创建一个简单的示例组件,并且编译运行
-
安装现有软件包的源码到工作空间中,修改并进行编译
-
如何引入第三方库到工作空间中并且依赖该第三方库
-
将二次开发的模块打包,并部署到另一台主机上
开始之前
Apollo 在包管理模式下,以工程来组织和管理软件包,Apollo 官方提供了多个工程示例,这些工程可以在 Apollo 中的 github 主页中找到。在本教程中,我们使用 application-pnc 工程。您可以从 Apollo 的 github 库来获取示例工程:
该工程声明依赖了和 pnc 相关的软件包,可以通过部署该工程来迅速调试、仿真 Apollo 的 pnc 软件包,该工程的目录结构如下:
如您所见,这个工程里面并无任何 Apollo 源码,仅有一些配置文件(.workspace.json和.env)与 core 模块,下面将通过部署该工程,逐步介绍上述提到的概念
部署 application-pnc
在部署整个工程之前,您需要先对工作空间的各个文件有一定了解。工作空间,即整个工程目录,存放着模块源码。它还包含一些供 Apollo 编译工具链识别的特殊文件:
WORKSPACE: 也即是 bazel 中的 WORKSPACE 文件,该文件会将该目录及其内容标识为 Bazel 工作区,并位于项目目录结构的根目录下。除非您需要额外引入可供bazel识别的第三方库,否则一般该文件不需要修改。
.workspace.json: 该文件同样位于项目的根目录下,标明了该工程从何处下载的软件包,以及软件包的版本,该文件内容如下:
其中repositories字段中包含软件包仓库信息,name为软件包仓库名称,当前, Apollo 有两个仓库可供用户选择:
-
apollo-core: Apollo 开源代码库的所有软件包仓库(X86架构)
-
apollo-core-arm: Apollo 开源代码库的所有软件包仓库(ARM架构)
而version则是为该工程下载软件包版本。
当您想把整个工程从9.0.0-alpha2-r28版本升级到9.0.0-alpha2-r29版本时,简单将该文件修改至如下内容即可:
然后重新部署工程,工程依赖的软件包就会自动升级至9.0.0-alpha2-r29版本。
注意:由于不同版本下 Apollo 各模块的对外接口可能会存在变更,因此升级版本后工程的源码可能需要做额外的适配工作
.env: Apollo 当前还需要运行在 docker 容器中,该文件指定了启动容器时使用的镜像的仓库以及tag等信息,该文件的内容如下:
其中:
-
APOLLO_ENV_NAME 指定了容器的名称。实际开发中,经常会在一个开发环境中启动多个 Apollo 工程。通过配置该变量,解决了不同容器名称冲突的问题。
-
APOLLO_ENV_CONTAINER_REPO 指定了镜像仓库
-
APOLLO_ENV_CONTAINER_TAG 指定了镜像的tag
除上述提到的文件以外,该工程在 core 路径下还包括 core 这一模块的"源码",尽管该模块不包括任何 c++ 传统意义上的源码。
在 Apollo 软件包管理模式中,一个模块的源码包括以下内容:
-
cyberfile.xml(必须): 模块的描述文件,描述了该模块的名称、所在工作空间路径、依赖等版本信息
-
BUILD(必须): 即 bazel 的 BUILD 文件,描述了模块中 c++ 源码, python 源码,配置文件,数据文件等该组织成什么形式进行编译
-
一系列的源码,数据文件,配置文件等(可选)
接下来,将以 core 模块为例,简单对模块涉及的源码文件进行介绍
理解 cyberfile
cyberfile.xml 位于工作区模块的根目录下。包含 cyberfile.xml 文件的目录该模块的源码路径,其子目录不允许有 cyberfile.xml 文件,即软件包不支持嵌套。
core 的 cyberfile.xml 内容如下:
其中,一些比较重要的属性:
-
repo_name: 与依赖的模块名相同。
-
type: 该依赖是何种形式被引入的,分别有 src 与 binary 可选。src 为以源码形式引入该依赖,即编译工具链会将该依赖的源码拷贝到工作空间下,一并进行编译;binary 为以二进制形式引入该依赖,即编译工具链在编译当前模块时,会导入依赖模块的动态库和头文件进行编译。
-
name: 模块的名称。
-
version: 模块版本号,由于是本地源码,所以版本为local。
-
type: 模块的类型,必须为module。
-
src_path: 模块在工作空间下源码的路径。
-
depend: 该模块的依赖,depend标签中有一些属性:
-
builder: 底层编译工具,当前仅支持 bazel。
理解 BUILD
BUILD 文件即 bazel 的 BUILD 文件,它告诉 Bazel 如何编译所需的输出,例如可执行的二进制文件或库。
在软件包管理模式中,Apollo 在 bazel 的基础上,提供了一些宏,规则,例如 core 的 BUILD 文件
其中 apollo_package 是软件包管理模式新增的宏,在软件包管理模式下,所有模块源码的BUILD文件中都需要添加该宏,保证模块源码可以正常的编译,部署。
除此之外,Apollo为c++源码、python源码、proto源码提供了相应的规则,例如在cyber源码的 BUILD 文件中:
在软件包管理模式下,请使用 Apollo 提供的规则,这些规则分别有:
-
apollo_cc_library:声明该 target 生成动态库,替代 bazel 原生 cc_library
-
apollo_cc_binary:声明该 target 生成可执行文件,替代 bazel 原生 cc_binary
-
apollo_cc_test:声明该 target 生成测试文件,替代 bazel 原生 cc_test
-
apollo_component:声明该 target 生成一个 Apollo 组件
-
apollo_plugin:声明该 target 生成一个 Apollo 插件
apollo_cc_library, apollo_cc_binary, apollo_cc_test规则的使用方式与 bazel 原生规则一致。
apollo_component, apollo_plugin将在下文中介绍如何使用这两个新增规则。
启动 Apollo 容器
启动 Apollo 容器需要使用 Apollo 环境管理工具 aem,该工具的安装可以查阅包管理安装方式 文档。
启动容器
启动容器后 aem 会产生类似以下的输出:
进入容器环境
当进入容器后,可以看见 in-dev-docker 字样。
部署工程
上文提过,application-pnc 工程内部没有任何 c++ 源码,只有一个 core 模块的,并声明了该模块的依赖,通过编译该模块,就可以在当前环境下迅速下载需要的软件包,达到部署工程的目的。
通过以下命令来编译 core 模块:
-p 参数指定了要编译模块的路径,假如不指定该参数,buildtool将会编译工作空间下的所有模块
输入命令后,buildtool 将会自动下载依赖,这个过程根据网络状况,可能会持续 30 - 60 分钟,请耐心等待
buildtool 部署工程完毕后,当前环境已经安装了 pnc 仿真需要的软件和工具,可以启动 dreamview 来对 planning 进行调试
通过以下命令启动 dreamview
然后从浏览器访问 localhost:8888 就可以访问 dreamview ,启动仿真和 pnc 进行调试。
或者手动启动 planning component:
安装源码到工作空间
除部署以外,开发者可能想要查看或修改某个模块,插件的源码,可以通过以下命令安装某个模块:
该命令安装了 planning-task-lane-follow-path 的源码到工作空间下,源码可以在 modules/planning/planning_base/tasks/lane_follow_path 下找到。
您可以修改上述的代码,改变task的行为,然后编译使其生效。
上述命令会编译modules/planning路径下的所有模块。
然后再通过dreamview重启 planning ,或者命令行重新运行 planning 来进一步调试。
当您想舍弃本地编译的结果,恢复planning-task-lane-follow-path最原始的效果,可以通过以下命令来重装模块:
然后重启 planning 即可。
创建一个 Apollo 组件
除了修改 Apollo 源码以外,您可能会想自己创建一个 Apollo 组件,也即是 component。
buildtool 提供了一个便捷的命令创建组建模版:
–template 参数声明了创建的模版是 component,sample_component 声明了该 component 的源码路径
执行完毕后,会有类似以下输出:
工作空间下会多出了 sample_component 目录:
其中:
-
BUILD:告诉 bazel 如何编译这个组件
-
conf:保存该 component 的配置文件,其中 sample_component.pb.txt 的内容会被解析成 sample_component.proto 定义的 SampleComponentConfig 类型;sample_component.conf 声明了一些 gflags
-
dag, launch:保存该组件的启动文件
-
proto:保存该组件配置,消息的proto message定义
-
c++源码:描述该组件的初始化以及接收到相应消息的行为
该示例 component 会监听 /apollo/sample_component channel,接收到消息后在终端打出相应消息。
由于该示例是一个 component,因此在 BUILD 文件中使用了apollo_component规则:
apollo_component规则声明了该 target 是一个组件,其中:
-
name:该组件产出的动态库名称,与 dag 文件中读取的动态库名称一致
-
srcs:该组件的源文件
-
hdrs:该组件的头文件
-
deps:该组件的依赖,在这里依赖了 cyber 的接口以及组件本身 proto 下的声明 target
该示例可以直接编译:
编译完成后直接运行即可:
您也可以修改该 component 初始化和接收到消息的行为,修改代码完毕后直接编译即可
创建一个 Apollo 插件
与组件一致,可以使用 buildtool 的 create 命令来创建一个插件:
插件需要继承一个插件基类,上述命令的 –base_class_name 参数声明继承 planning 中的 Task 类,sample_plugin 声明了该插件的源码路径,–dependencies 参数在 cyberfile.xml 中添加了对 planning 的依赖
工作空间会多出 sample_plugin 目录:
相较于组件,插件增加了一个描述文件 plugin_sample_plugin_description.xml:
该文件描述插件动态库的名称,以及插件的类名和集成的基类名,以便cyber能够正确加载该插件。
与组件不同,由于插件基类的定义不同,上述命令创建的插件模版仅是一个最基础的模版,无法直接编译,需要开发者补齐相应信息才能够编译,接下来将介绍开发者需要补齐的信息。
引入的头文件
在 create 命令中,指定了继承的基类是 apollo::planning::Task,而实际上,sample_plugin.h 只是单纯继承了,apollo::planning::Task,并未引入相应的头文件:
开发者可以参考planning的接口文档来添加apollo::planning::Task对应的头文件,并根据基类定义修改函数签名:
修改源码实现文件
由于函数签名被修改了,相应的实现也需要对照修改,以下是 create 命令初始化的 cc 文件:
修改后的 cc 文件:
修改BUILD文件,添加依赖的target
同样,插件的BUILD文件本身也没引入基类对应的target,开发者可以参考依赖模块的BUILD文件,来添加头文件对应的target
以下是 create 命令初始化的 BUILD 文件:
apollo::planning::Task对应的target是"//modules/planning/planning_base:apollo_planning_planning_base",因此修改 BUILD 文件后:
这个 BUILD 文件中使用了 apollo_plugin 规则来声明该 target 生成一个插件,其中:
-
name:该组件产出的动态库名称,与插件描述文件中读取的动态库名称一致
-
description:插件描述文件的名称
-
srcs:该组件的源文件
-
hdrs:该组件的头文件
-
deps:该组件的依赖,在这里依赖了 cyber 的接口以及组件本身 proto 下的声明 target
修改完毕后,就可以通过以下命令进行编译:
编译完毕后,该插件就可被 planning component 加载使用。按照上述步骤编译的插件只是一个空插件,插件本身没有任何的行为,要插件来规划车辆轨迹,还需要进一步编写相应的业务代码。另外,插件被 planning 加载并调用需要修改 planning 的配置文件,详细可参阅 planning 的入门教程,此处不再过多叙述。
引入第三方库到工作空间中
目前为止,apollo支持引入以下类型的第三方库:
-
可通过apt下载的第三方库
-
以源码形式存在的第三方库
-
以头文件与动态库形式存在的第三方库
对于第一类第三方库,简单在需要依赖该第三方库模块的cyberfile.xml中添加 depend 的标签即可,假设您的模块依赖curl库,在cyberfile.xml中添加以下依赖信息:
然后在该模块的 BUILD 文件中添加需要链接的动态库:
在源文件中添加相应"#include"即可:
对于第二类第三方库,本质上和用户在工作空间开发的三方库无异,可当作普通模块处理。即用户为其编写cyberfile.xml与 BUILD文件即可正常编译与使用。
对于第三类第三方库,处理方式类似第二类,也可当作普通模块处理,但是在编写BUILD文件时,对于动态库需要额外的声明,例如以下的cancard驱动:
cyberfile.xml中,需要依赖bazel-extend-tools, 3rd-bazel-skylib, 3rd-gpus这几个软件包:
BUILD中,对于动态库,需要使用原生的cc_library声明:
对于需要依赖该第三方库的模块,cyberfile.xml需添加对于该第三方库的依赖:
BUILD文件中直接依赖该第三方库声明的apollo_cc_library即可:
然后正常编译即可。
将本地开发模块打包并部署到其他主机上
开发者在本地开发完毕后,可能需要将本机开发的模块进行打包,并部署到另一台主机的需求,这里以上述编译的插件为例,介绍软件包管理模式下如何打包与部署。
打包
buildtool通过以下命令对模块进行打包:
-p 参数指明了要打包模块的源码路径,当不添加该参数时,buildtool 会尝试将工作空间下所有模块进行打包
打包完成后,buildtool 会有类似输出:
工作空间下会产生一个 release.tar.gz 文件,该文件可用于后续到其他主机上部署。
部署
在部署之前,确保另外一台主机下载好了 aem 工具来启动 Apollo 容器
首先,创建一个用于部署用的工作空间:
然后将上一步的 release.tar.gz 与 .env 文件拷贝到新的主机中
接下来同样通过 aem 启动容器
启动完毕后,在容器内调用 buildtool 部署工程: