文章转载自Apollo开发者社区
Apollo 3.5 总体架构
最新发布的 Apollo 3.5 总体架构从上到下仍分为四层,最底层为车辆平台,自动驾驶汽车需要对车辆进行线控改造,使得车载大脑可以通过电信号来控制车辆的执行器;
往上一层是硬件平台,包括计算单元、传感器以及 V2X 相关接收设备等。
再上一层是软件平台,主要包括操作系统、中间件、算法模块等。
最顶层的是云端服务,主要包括地图、OTA 服务升级、数据平台、语音交互等方面。
此次发布的Apollo 3.5,解锁了Apollo 在复杂城市道路中的自动驾驶能力,开源Cyber RT 计算框架、V2X 车路协同方案以及硬件平台的一些能力。
Apollo所使用的 ROS 系统
Apollo最初使用的中间件是 ROS(机器人操作系统)。概括来说,ROS系统主要包含三方面内容:
- 第一,通信系统。
ROS是个分布式的松耦合系统,算法模块以独立进程形式存在,也就是我们常说的Node。
ROS基于Socket实现了Pub/Sub的通信方式,不同的算法节点(Node)之间通过Pub/Sub发送/接收消息。
- 第二,Framework&Tools (框架和工具)。
开发者可基于ROS提供的Client Library和通信层,方便地收发消息。
开发者只需要关注消息处理相关的算法,而至于算法何时被调用,全部由框架来处理。
- 第三,生态系统。
从社区内,开发者可以很方便地寻找到很多现成的「传感器驱动」和「算法实现」等进行参考。
ROS在自动驾驶实践中遇到很多挑战
随着自动驾驶的发展,不少开发者,包括Apollo平台,把ROS应用于自动驾驶系统,毕竟自动驾驶汽车也相当于一个大的机器人。但是我们在实践中仍然遇到很多挑战。
- 首先,ROS 中的算法模块是以独立的进程形式存在的,进程之间该以什么样的顺序去执行?
实际上,Linux 本身是一个通用系统,内核中的调度器对上面的算法业务逻辑并不清楚。它只是在尽量满足公平的情况下让大家都得到调度。所以,ROS Node运行顺序并无任何逻辑。但本质上自动驾驶是一个专用系统,任务应按照一定的业务逻辑执行。
那么是在ROS层加一个Node,由其来同步各个算法任务的运行,还是在Linux内核中实现新的调度策略,使其结合算法业务逻辑进行调度?前者的开销,后者的迁移性,都是需要思考的问题。
- 其次,ROS是一个分布式的系统。
既然是分布式系统,就要有通信的开销。即使在同一个物理节点上,依然存在着通信的开销。所以Apollo前期曾经使用共享内存去降低ROS原生的基于Socket通信的开销。ROS2 也在使用 DDS 解决通信方面的实时性。ROS也支持Nodelet模式,这可以去掉进程间通信的开销,但是调度的挑战依然存在。
- 第三,除了调度的不确定性,ROS系统中还存在其他很多不确定的地方,比如内存的动态申请。
Cyber RT的运行流程
算法模块通过有向无环图(DAG),配置任务间的逻辑关系。对于每个算法,也有其优先级、运行时间、使用资源等方面的配置。系统启动时,结合DAG、调度配置等,创建相应的任务,从框架内部来讲,就是协程,调度器把任务放到各个Processor的队列中。然后,由Sensor输入的数据,驱动整个系统运转。
Cyber RT的架构及包含地软件模块
基本上,Cyber RT包括如下软件模块:
- 最下面一层是基础库,为了高效,Cyber RT实现了自己的基础库。
比如我们实现了Lock-Free的对象池,实现了Lock-Free的队列,随着成熟,会陆续开放更多。除了框架自身外,将来也会逐渐应用于算法模块。除了效率原因以外,也希望Cyber RT减少依赖。
- 再往上是通信相关的。包括服务发现,还有Publish-Subscribe通信机制。
Cyber RT也支持跨进程、跨机通信,上层业务逻辑无需关心,通信层会根据算法模块的部署,自动选择相应通信机制。
通信层之上是数据缓存/融合层。
- 多路传感器之间数据需要融合,而且算法可能需要缓存一定的数据。
比如典型的仿真应用,不同算法模块之间需要有一个数据桥梁,数据层起到了这个模块间通信的桥梁的作用。
- 再往上是计算模型,计算模型包括刚才前面提到的调度和任务,后面我们会详细讨论。
- 计算模型之上是为开发者提供的接口。
Cyber RT为开发者提供了Component类,开发者的算法业务模块只需要继承该类,实现其中的Proc接口即可。
该接口类似于ROS中的Callback,消息通过参数的方式传递,用户只要在Proc中实现算法、消息处理相关的逻辑。
- Cyber RT也基于协程,为开发者提供了并行计算相关的接口。
- Cyber RT也为开发者提供了开发调试、录制回放等工具,未来还会开放性能调试工具。
将调度、任务从内核空间搬到用户空间
ROS的主要挑战之一是没有调度。为了解决ROS遇到的问题,Cyber RT的核心设计将调度、任务从内核空间搬到了用户空间。
调度可以和算法业务逻辑紧密结合。
从Cyber RT角度,OS的Native thread相当于物理CPU。
在OS中,是内核中的调度器负责调度任务(进程、线程……)到物理CPU上运行。
而在Cyber RT中,是Cyber RT中的调度器调度协程(Coroutine)在Native Thread上有序运行。
任务编排策略
每个Processor (Native Thread) 一个任务队列,由调度器编排队列中的任务。
任务在哪个CPU上运行?任务之间是否需要相邻运行?哪些先运行?哪些后运行?
这些都由调度器统一调度,任务基于协程实现。在任务阻塞时,快速让出CPU。
每个物理CPU上除运行1个Normal级别的Thread 外,运行着另外1+个高优先级的Thread。
基于此,实现用户空间的高优先级的任务抢占运行。
比如,之前去GPU运行的算法,在GPU上完成运行返回后,应该尽快的得到运行。
这种调度策略,很好的结合了业务逻辑、数据共享和算力的平衡。并且任务不会在不同CPU上随机的调度来调度去,具有非常好的Cache友好性。
任务编排策略,不仅需要对业务逻辑的深度理解,也需要结合计算机的算力等综合考虑。
因此,Cyber RT也提供类似经典线程池模式的调度算法,这种模式几乎不存在配置的代价。
对此,Cyber RT也做了一些改进,比如为了减小锁的瓶颈,任务是多队列的。
任务队列也支持优先级,后续还会支持分组,通过组控制算法对资源的使用。
协程
Cyber RT使用了协程作为算法任务的载体。
协程之于线程,就类似于线程之于物理CPU,由Cyber RT中的调度器负责在各个线程之上周而复始,切换调度协程。
为了算法模块在其他协处理器执行计算时,可以让出Processor(Native Thread),并在完成之后,回来时可以再次运行,Cyber RT采用了有状态的协程。
那么Cyber RT为什么采用协程呢?
除了协程的切换非常快之外,调度的确定性是另外一个重要的原因。
举个典型的例子,假设用Native Thread去执行一个任务。
当任务因为去GPU等加速器运算时,或者因为资源原因被Block时。在Thread就绪时,什么时候调度上来,其实是一个非确定的过程,完全依赖于操作系统以及其上任务的情况。
Cyber RT也支持跨进程、跨机通信。
实际部署中,也存在着比如某个工具需要运行在独立的进程,安全系统部署在另外的节点。
因此,Cyber RT也支持跨进程、跨机通信。上层业务逻辑无需关心,通信层会根据算法模块的部署,自动选择相应通信机制。