21年9月来自巴塞罗那超级计算中心的论文“Enabling Unit Testing of Already-Integrated AI Software Systems: The Case of Apollo for Autonomous Driving”。
用于自动驾驶的先进人工智能软件包含多个高度耦合的模块,这些模块依赖于数据和控制。部署这些已经集成的软件框架使得单元测试(关键软件验证过程中的基本步骤)在安全关键型系统中变得非常具有挑战性。为了解决这个问题,对工业级自动驾驶框架(Apollo)架构设计进行几处修改,开发模块独立版本。独立模块具有与其集成对应模块相同的功能行为。对其在异构片上系统(SoC)上运行的每个模块软件时序要求进行增量分析,举例说明独立模块的好处。这是整合和集成软件模块的强制性步骤,可保证时序约束(例如与不受干扰相关),同时最大限度地提高 SoC 利用率。
在汽车和航空电子等安全相关领域,每款嵌入式产品的软件数量和复杂性都在不断增加 [1]–[3]。软件已被公认为提高嵌入式产品竞争力的最重要因素之一,它越来越多地控制着更多的机载功能。为此,软件处理大量数据,并包含人工智能 (AI) 算法以实现多种功能。例如,汽车中的自动驾驶 (AD) 建立在深度学习的基础上,用于目标检测和跟踪、路径规划、驾驶员监控系统以及基于语音的指挥和控制 [4]。预计人工智能还将对航空电子和航天产生重大影响,使系统更加自主,可能不需要人工干预即可运行 [5]、[6]。
领先的人工智能公司提供了多种机器学习框架以及底层优化库,以利用可用平台的性能,这些平台用于从改进的内容发现到语音/图像识别等不同领域 [7]。然而,通用人工智能算法(软件)的设计并不遵循安全标准 [8]。这对汽车和航空电子领域的 OEM 和 TIER 公司来说是一个难题:虽然他们希望获得更高精度的 AI 算法(例如在目标检测中),但验证该软件是否符合适用的安全标准可能会花费不菲 [9]。这需要一个折衷的解决方案来兼顾两全其美。
嵌入式系统中的安全相关功能需要特定领域的设计、验证和确认 (V&V) 手段,这有助于系统地识别实施错误。在开发周期中遵守现有指南(例如道路车辆的 ISO 26262 和 ISO/PAS 21448)可使软件在设计上正确。也就是说,软件的架构是专门为以有效的方式满足所有安全要求而设计的,因此软件功能不当行为的剩余风险可以忽略不计。
此外,系统总体设计和实施,尤其是软件的设计和实施,都遵循特定的实践,例如评估输入值属性和其他条件、遵守 MISRA C 编码实践等。遵循这些实践有利于代码的简单性、模块化、清晰度和正确性,从而简化软件验证方法(例如功能的形式验证)以及验证过程(即测试)。
软件测试对于基于 AI 的框架至关重要,因为它们与通常的安全相关开发过程的性质不太匹配,需要特定的安全标准,如汽车 ISO/PAS 21448(SOTIF)。这是因为很难指定、设计和验证更依赖于测试过程的 AI 框架。挑战在于生成有限数量的测试场景,为软件的低级属性(例如代码覆盖率、边界值、及时性)和高级属性(例如 AD 框架的驾驶场景)提供足够的覆盖范围。测试过程从单元级别开始评估和量化低级属性,以便后续集成阶段可以专注于测试集成接口和更高级别的属性。
对于软件时序,验证每个软件单元可以缩小潜在的时序问题(例如,由于设计和验证过程中的错误可能会导致实时软件的时序问题),以及评估集成对时序的影响,这与所需的不受干扰有关(参见 ISO26262)。因此,在出现超限(overrun)的情况下,更容易找出超限的原因,无论是由于特定软件单元引起的,还是由于其中几个软件单元的集成影响引起的。
这些原则与 AD 软件相冲突,AD 软件是基于 AI 软件的代表,它包含不同的已经集成高度耦合的模块,每个模块都具有很高的循环复杂度 [9]。因此,关键问题是如何促进软件的单元测试,这些软件的单元(例如可运行程序、任务和软件组件)出于效率原因仅部署为紧密耦合的框架。
单元测试
单元测试是第一步,它建立在以下三个活动之上。
测试用例生成。其目的是以尽可能少的测试工作量实现对潜在软件故障的高覆盖率,这通常意味着最小化测试用例的数量。
覆盖率指标的定义。语句(statements)和 MC/DC 等覆盖率指标,定义了需要如何量化测试覆盖率。需要软件单元的可观察性来量化此类覆盖率指标(例如分析代码)。
测试执行和结果评估。通过将获得的结果与预期结果和/或结果的预期属性(例如特定范围内的值)进行比较来检查测试的正确性。还收集软件单元的覆盖率指标和资源使用信息(例如执行时间、使用的内存)。
集成测试
在单独测试软件单元之后,将它们逐步地集成到软件单元之间以及与传感器和执行器等硬件组件之间。
总体而言,单元测试和集成测试都在很大程度上依赖于测试执行粒度的可控性和可观察性。因此,如果需要以比整个框架更细的粒度进行测试,那么完全集成的框架将对单元测试和潜在的集成测试构成挑战。
如下图是百度 Apollo 的模块及其相关性:
Apollo (ver 3 为例)的主要模块包括:
(1)CANBus,是将控制命令传递给车辆硬件并将底盘信息传递给软件系统的接口。
(2)控制,通过生成加速、制动和转向等控制命令来执行规划的时空轨迹。
(3)Guardian ,是一个安全模块,它执行行动中心的功能,并在监控检测到故障时进行干预。
(4)高清地图(HD Map),提供道路的临时结构化信息。它用作库,作为感知、预测、规划和路线规划使用的查询引擎支持。
(5)Apollo 中的 HMI(人机界面)或 DreamView ,是一个用于查看车辆状态、测试其他模块和实时控制车辆运行的模块。
(6)定位,使用 GPS、LiDAR 和惯性测量单元 (IMU) 等各种信息源,以厘米级精度估计车辆所在的位置。定位为感知、预测、规划和控制模块提供数据。
(7) 监控,是车辆中所有模块(包括硬件)的监控系统。
(8) 感知,通过检测目标、障碍物和交通标志来识别车辆周围的区域。它被认为是 AD 系统中最关键和最复杂的模块。感知还融合了几种类型的传感器(如激光雷达、雷达和摄像头)的输出,以提高其准确性。
(9) 规划,为自动驾驶汽车规划时空轨迹。
(10) 预测,提前估计感知障碍物的未来运动轨迹。
(11) 路线规划,告诉自动驾驶汽车如何通过一系列车道或道路到达目的地。
下面介绍如何分解模块:
开发独立模块的方法包括两个主要阶段:(1)剥离数据以提供给独立模块;(2)使模块完全独立,这样,通过仅基于外部数据构建,它们既不会受到同时运行其他模块(包括它们自己的其他实例)行为的影响,也不会改变其他模块的行为。对于第一阶段,从全局数据集创建模块特定的数据集(包拆分),并使用集成版本中的数据(数据生成和时间戳调整)对其进行扩展,这对于模块的正确执行是必需的。对于第二阶段,强制所有模块处理所有传入数据(数据寿命调整),并禁用消息发布(禁用发布)以确保模块正在处理的唯一数据来自外部源,从而避免同时运行多个独立模块时产生干扰。
包拆分。当在非汽车内的机器上运行 AD 框架时,用户需要外部数据来模拟汽车在真实场景中接收的信息。一些 AD 系统(例如 Apollo)依赖于 ROS“bags”,这是基于 ROS 的程序使用的一种文件格式,用于存储可以包含任何类型信息的消息数据。在 Apollo 中,消息用于模块之间以及与各种传感器之间的通信。每条消息都附加到某个主题(例如,前置摄像头、LiDAR 传感器、感知输出等)。
运行 Apollo 模块意味着同时运行其所有模块。借助 ROS bag,如果播放 bag(即 bag 内的所有消息将按照记录时的顺序和时间间隔发送到系统),并且 bag 包含模块所需的所有数据,单个模块也可以独立运行。因此,模块处理在真实场景中记录的来自传感器或其他模块的消息。但是,对于某些模块,其所需的数据可能并非全部包含在 bag 中,而是仅在并发运行时由其他模块生成。
数据生成和时间戳调整。为了使 ROS 包完整,准确跟踪模块在正常执行过程中生成的数据,将其转储并集成到生成的拆分包文件中,以使每个模块独立运行,并具有作为集成框架的一部分运行时将使用的完整数据集。此外,调整包文件中每条消息的时间戳,以允许单独运行模块。这是必要的,以确保在与其他模块不断集成时,模块不会早于允许的时间触发。
寿命调整。比较原始版本和独立版本中处理的消息数量和启动的回调函数,Apollo 可以限制每个主题可以存储的消息数量(并非所有消息都可以在到达后立即处理)。这可能导致丢弃太旧的消息。这样做是因为每条消息都有一个生命周期,一旦在其生命周期内未使用,该消息就变得无用。但是,这里对 Apollo 进行修改,将这个限制移得足够远,因此让它处理包内的所有消息而不会丢弃任何消息,从而获得测试的确定性。
禁用发布。为了在所有执行中保持相同的行为,禁用消息发布。这样,确保所有模块只处理包内的消息,而不会生成冗余消息。否则,被分析的模块可能会生成并发布一个可能已经在包中的输出,即在记录包期间也会生成相同的消息,这样订阅的模块就会收到两个相同的消息,而这在真实情况下不会发生。
目标是采用配备 NVIDIA 1080 Ti GPU 的 x86 架构。它包含一个八核 AMD Ryzen 7 1800X 处理器,其中每个内核都是双线程的。每个内核都有私有的 32 KB 数据和 64 KB 指令缓存,以及私有的 512 KB 包容性 L2 缓存。2 x 8 MB 独占 L3 缓存在所有内核之间共享。独立 GPU 具有 3584 个 CUDA 内核和 11 GB GDDR5X 专用内存。汽车行业的一些产品依赖于使用具有独立内存的 GPU,例如 NVIDIA Drive Pegasus [15]。
在类似于真实车辆配置的 docker 镜像中运行 Apollo,整个框架在 Linux 操作系统上运行。高性能 GPU 采用高带宽独立内存来为内存密集型工作负载(例如目标检测或其他深度学习工作负载)提供所需的性能。使用 Apollo 提供的代表性数据集和输入视频来运行模块并进行实验 [12]。
开发几个脚本来执行 Apollo 模块,同时始终以相同的方式(即确定性地)运行 ROS 包,并解析 Apollo 的输出以收集需要的结果。为了收集 GPU 内核信息,使用 NVIDIA 分析器工具 nvprof [18]。专注于回调(处理、pfi)函数,因为它们是在 Apollo 稳定运行期间触发的函数。此外,这些函数通常负责模块的核心功能,使其成为实验的目标。专注于六个代表性模块(其余模块要么没有带来额外的见解,要么只能在真实的汽车硬件中实例化):感知、预测、规划、定位、守护和控制。由于空间限制,特别关注感知,因为它是 Apollo 中最耗时的模块。
步进集成
随着每个新产品的软件规模不断扩大,AD 系统的软件模块和驱动程序由不同的供应商设计、开发和实施。通常,在早期阶段,供应商可能并不完全了解模块将集成的系统的最终配置。例如,感知模块可以集成在不同的部署中,具有不同的传感器类型(摄像头、激光雷达和/或雷达),以及不同数量和类型的摄像头(例如,长距离、广角等)和使用的分辨率。因此,考虑多个软件集成是关键。
为了捕捉这种情况,在独立的 Apollo 模块上进行了一系列实验,在这些实验中,逐步集成它们,以评估每个集成模块对其他模块的影响。感知中主要 ImgCallback 函数(处理图像的函数)的执行时间变化:预测、定位、规划、控制、路由、CAN 总线、监控和守护。独立模块可以评估争用的影响很小且在标准偏差范围内,因此在统计上并不显着。发生这种情况的原因是感知执行时间由 GPU 主导,深度学习目标检测在 GPU 上运行,而其他模块不使用 GPU。此外,独立 GPU 使用自己的内存接口(即不与 CPU 共享)。在分析其他模块及其主要功能时,由于它们主要使用核心本地资源,因此对干扰非常不敏感,因此可变性在统计上并不显着。请注意,使用已经集成的框架无法进行这种分析。
不同部署场景
另一种集成场景,即使用 4 个摄像头而不是 1 个(默认情况)。这涵盖了自动驾驶汽车可以拥有多个摄像头以覆盖不同角度和/或范围的集成场景 [19]。在这种情况下,与处理摄像头输入相关的所有流程(例如用于感知的深度学习工作负载)都将增加四倍。将每个额外的摄像头流程作为新回调添加到系统来实现此配置。请注意,在 4-摄像头场景中,每个摄像头产生的输出由不同的回调函数处理。因此,每个回调函数进行的计算与 1-摄像头场景中的相同。
外部应用程序
汽车行业利润率的下降要求提高资源利用率以降低成本。电子产品也不例外,因此需要将更多应用程序安全地整合到同一个 SoC 上。但是,这可能会导致每个应用程序因访问共享资源的争用而遭受更大的减速。
为了捕捉这种情况,开发缓存激进的 CPU 基准测试,这些基准测试通过写入或读取/写入访问大型数据阵列,从而破坏 L3 并进入内存。它们分别称为 CPU_st 和 CPU_ldst。此外,由于某些模块使用 GPU 加速,还开发 GPU 基准测试,这些基准测试部署不同数量的线程,与 Apollo 模块的 GPU 内核同时运行。GPU 基准测试被开发为要添加到主内核的流,并使用分析可视化工具验证它们的并发执行。
每个模块部署不同数量的软件线程,这些线程映射到硬件线程和内核(回想一下,内核实现超线程)。在最坏的情况下,perception 使用 6 个线程,因此有 10 个硬件线程可用且未使用。为此,运行多达 10 个 CPU_dst 和 CPU_st 基准测试副本,以将资源利用率提高到 100%。
关于 GPU 基准测试,考虑了两种情况。当线程数低于 GPU 容量时,GPU_1kt(1kt = 1024 个线程)和 GPU_2kt,可以与 Perception 模块完全并行运行基准测试。已经使用 GPU 分析器和可视化工具证实了这一点,并得出结论,观察到的减速(1kt 和 2kt 的中位数分别为 2.56 倍和 2.65 倍)仅仅是由于争用造成的。但是,当线程数超过 GPU 容量时,即 GPU_4kt 和 GPU_8kt,CUDA 运行时会在 Perception 和基准测试之间分时使用 GPU,从而产生另一个延迟源,使 Perception ImgCallback 的速度降低高达 4.21 倍。
下表总结了对 CPU 和 GPU 基准测试中所有分析模块的回调函数的观察影响。不同模块之间争用的各种影响,这些模块的功能对添加额外的摄像头、CPU 负载和 GPU 负载的敏感度或高或低。Perception 模块中的 OnRadar 的情况很有趣,因为在 4 个摄像头设置中它会减少执行时间。初步分析表明,此功能对 CPU 争用特别敏感。因此,当添加更多摄像头或 GPU_8kt 时,由于 GPU 中的序列化,执行时间会增加,因此 OnRadar 在 CPU 中同时执行的竞争者更少。相反,当与 CPU_st 或 CPU_ldst 同时运行时,其执行时间会明显增加。再次注意,只有使用独立模块,才能以适当的可观察性和可控制性在每个模块的基础上执行此类分析。总体而言,独立模块允许轻松地对多个部署场景进行单元测试,从而对系统验证充满信心,否则将无法实现。