分享嘉宾:
孙涛 - 中汽创智智驾工具链数据平台开发专家
关于中汽创智:
中汽创智科技有限公司(以下简称“中汽创智”)由中国一汽、东风公司、南方工业集团、长安汽车和南京江宁经开科技共同出资设立。聚焦智能底盘、新能动力、智能网联三大业务领域,围绕“车端+云端+通信端”生态体系,开展前瞻、共性、平台、核心技术和产品研发及产业孵化。
分享提纲:
1. 自动驾驶数据闭环介绍;
2. Alluxio 在采集标注训练以及合规平台的一些应用场景;
3. 目前存在的问题以及未来规划。
自动驾驶业务介绍
首先介绍一下中汽创智的自动驾驶整个开发业务流程。我们现在围绕自动驾驶开发主要强调的是数据闭环:开始是数据采集,数据采集之后进行预处理/数据挖掘,包括一些大模型的预刷;接着进入数据标注环节,主要包括人工标注和一些自动化的标注;标注以后产生的带有增值的数据集,提交给算法进行开发训练,如模型训练、评测和推理;最后再去迭代整个自动驾驶模型,模型经过上线进行实车部署。
在数据采集环节,自动驾驶的采集车需要经过改装,进行传感器的标定,主要在标定厂里对车端的一些传感器进行刚性位置的关系调教,改装完的车去做实测采集,比如在城区的道路、高速公路进行采集。每天采集的数据量非常大,一天一辆车采集的数据大概在 1 TB 左右。而采集回来的数据量是没办法通过线上传输的方式上传到我们的私有云或者公有云里,一般是通过特殊的存储介质拷贝带回来。当然也有实时的采集,比如每隔多长时间需要去做一些采样,这种数据是可以通过实时的采集传输到我们的私有云里。
可以看到自动驾驶业务的每个流程里都有数据传输和拷贝的环节,这都是跟存储加速强相关的。
关于标注,公司内部研发了一个人工标注的数据平台,主要把采集回来的数据进行预处理,因为原始的数据可能是一个 bag 包,如同现在机器人开发里基于 ros,里边类似于 Java topic 机制, 可以去订阅包括点云、图片、轨迹数据等,而这些数据是存储在一个二进制的很大文件里。经过预处理要对应到每一个时间戳,比如它有一个频率,每秒大概两帧或者是几帧,然后把这些数据做参数时间戳上的对齐,包括做一些插值,去处理成算法或人工标注所能识别的数据集(可能包括一组文件,里面有 point cloud 这样的文件夹。文件夹里是一帧帧的点云。车上可能有一系列前后左右一组传感器camera,每个时间戳下面都有对应的一张2D图片和一些轨迹,包括内外参的一些参数)。在处理成这种格式的数据之后,然后会把数据集上传到数据标注平台里进行增值的标注。
标注包括多模态,如 3D 的点云, 2D 图片 /camera 。点云和图片标注的时候会做 3D 到 2D 的转换,这个过程从人工标注之后进入到人工审核验收环节,最后流转出来到导出层,就成为了算法所需要的数据集(主要是一些增值数据,比如标注的box、车道线、车道线障碍物、锥桶、行人以及路上的设施)。
在模型开发训练阶段,自动驾驶的主要算法模块包括感知、预测、定位、规控。传统是基于分层的架构开发,每个模块有上下层的逻辑关系,现在比较流行的是端到端的模型训练,类似于我们的人脑,依靠眼睛、耳朵来接收外界的输入信号,通过内容融合的大模型,最终输出自动控制车辆的行为。
对于模型训练,我们跟很多大型企业一样有自己的智算中心,会有很大的投入去建设 GPU 集群用于训练,然后数据集会源源不断的进行输入。最后是模型评测和模型上线。
数据存储管理的痛点:
在整个我们的数据闭环中,数据存储是影响整个开发的非常大的痛点。因为数据是自动驾驶的三要素之一,它像血液一样,能驱动算法,驱动整个业务流程。如果没有数据,算法仅仅是一堆代码,模型根本无法支撑自动驾驶。
√在自动驾驶场景,数据是海量的,包括有采集车数据,一年的规模在几个 PB,如果是长年累月的积攒,这个量是无法估量的,还有座舱数据、平台的业务数据和指标数据,这些都需要管理和治理。
√数据闭环过程中,往往存在重复性的拷贝,重复读写相同数据,造成效率低下。
√平台和算法都存在大量数据上传下载的业务逻辑,并且读写的效率低下。
数据平台架构以及存储选型
存储类型
方案1:JuiceFS
之前也调研过 JuiceFS,并且在数智化部门也有使用的案例。由于我们的数据湖底座是在 OBS 上,对象存储里的数据有多种使用方式,JuiceFS 是自己实现了分布式文件系统的元数据层,按照自己的协议把数据写入 UFS,虽然通过gateway 方式也支持多种协议的读写,但是我们算法还保留着对象存储 API 使用方式,所以我们出于数据可用性考虑放弃。
方案 2 :Alluxio(最终采用)
Alluxio 接触是在我们使用 JuiceFS 之后,在我们多个平台里面共享存储这样的需求,涉及到采集、数据筛选/数据挖掘、合规、标注、训练推理、仿真等相关业务,我们希望有统一底层 UFS 数据接入,读写加速、把读写 UFS 的能力下沉到中间件层,提升业务的效率;Alluxio 开源版本2.9.3的功能特性能够满足我们当前的需求。
Alluxio 介绍
Alluxio 作为机器学习生态系统大数据中的新增数据访问层,可位于任何持久化存储系统(如Amazon S3、Microsoft Azure 对象存储、Apache HDFS 或 OpenStack Swift)和计算框架(如 Pytorch、TensorFlow、Apache Spark、Presto或Hadoop MapReduce )之间,但是 Alluxio 本身并非持久化存储系统。使用 Alluxio 作为数据访问层可带来诸多优势:
对于用户应用和计算框架而言,Alluxio 提供的快速存储可以让任务(无论是否在同一计算引擎上运行)进行数据共享,并且在同时将数据缓存在本地计算集群。因此,当数据在本地时,Alluxio 可以提供内存级别的数据访问速度;当数据在Alluxio中时,Alluxio 将提供计算集群网络带宽级别的数据访问速度。数据只需在第一次被访问时从底层存储系统中读取一次即可。因此,即使底层存储的访问速度较慢,也可以通过 Alluxio 显著加速数据访问。为了获得最佳性能,建议将 Alluxio 与集群的计算框架部署在一起。
就底层存储系统而言,Alluxio 将 AI 应用和不同的存储系统连接起来,因此扩充了能够利用数据的可用工作负载集。由于Alluxio 和底层存储系统的集成对于应用程序是透明的,因此任何底层存储都可以通过 Alluxio支持数据访问的应用和框架。此外,当同时挂载多个底层存储系统时,Alluxio 可以作为任意数量的不同数据源的统一层。内部架构如下:
数据平台架构
我们的海量数据主要是非结构化数据,有别于传统的数仓是结构化的数据(语义直接在数据里),而自驾的数据都是非结构化的,如你光看这些文件(图片、视频或者是点云)并不清楚里面是什么,只有通过挖掘之后才知道它表达的是什么语义,蕴含什么信息。这些需要抽象出来在数据库和数仓里去表达。
架构中数据湖架构底层由对象存储和 HDFS 组成,上面是分布式缓存加速层,如 Alluxio;再往上,我们会将抽象出来的非结构化数据元数据,业务数据存储在 OLTP / OLAP 数据库中,通过流式 cdc 或者数据集成工具同步写入数据湖,采用Hudi / Iceberg / Delta Lake 这样的 tableformat 数据格式,也是为了解决早期 Hive 在 upsert 场景上的性能问题;往上是元数据层,接着再往上我们集成了如 Clickhouse 和 Doris等 OLAP 引擎对业务层进行查询,数据湖则通过 Trino 进行交互式分析。整个数据湖架构为上层业务平台以及算法、工具链提供 DataOps 支撑。
自动驾驶数据平台使用场景
当前方案
目前部署了 3 Master + 10 Worker,客户端随应用侧使用sidecar 方式部署,应用 pod 中业务容器和 sidecar 容器通过存储卷的共享传播机制实现挂载。
主要使用到的 Allluxio 3大特性:
√ 简化数据管理 :Alluxio 提供对多数据源的单点访问,能够对多个独立存储系统提供单点访问,无论这些存储系统的物理位置在何处。这提供了所有数据源的统一视图和应用程序的标准接口。
√ 内存速度 I/O :Alluxio 能够用作分布式共享缓存服务,这样与 Alluxio 通信的计算应用程序可以透明地缓存频繁访问的数据(尤其是从远程位置),以提供内存级 I/O 吞吐率。此外,Alluxio的层次化存储机制能够充分利用内存、固态硬盘或者磁盘,降低具有弹性扩张特性的数据驱动型应用的成本开销。
√ 应用侧实现数据本地挂载 Alluxio-fuse
应用案例一
在我们标注平台有个场景是上提数据集。数据集有大有小,一般上提数据集文件数处于较小规模时没有问题,但如果有一个很大 BEV 数据集,1个包大概5至6GB,我们需要在上提的时候,把原始目录下的文件做转换,一些文件夹的处理。然后,按照每一帧组装好元数据,因为如果是图片可能比较大(几 MB 至十几 MB ),特别是在标注平台里,由于加载原始图片比较大,会涉及到生成缩略图。比如1帧可能会有 7 个camera,每个图片会生成大、中、小 3 个缩略图,这样就放大 3 倍。假如我们现在有3, 000 帧,一个摄像头就可能变成 9, 000 张图片,然后再加上 6 个摄像头,那就再乘以 6 倍,这个数据量就蹭蹭上去了。
之前为使用 Alluxio 的时候,还是通过传统方式先在本地生成,接着通过对象存储的 API 去上传,但当上提较大数据集时(文件数在1w左右),则会出现 gc 问题,刚开始采用多线程并且加内存资源,也依然存在问题,出现 GCLocker 的告警,并且一直重试申请分配对象,最终导致 OOM。
当我们用了 Alluxio之后,把这么多文件通过 MQ 分发到缩略图服务里,然后把 OBS 或者 MinIO 通过 sidecar 方式挂载到我们服务的 pod 里,这个时候可以看到速度非常快,性能也好,没有再出现之前的问题。我们之前读写方面的瓶颈问题,也通过 Alluxio 解决了,非常能够体现 Alluxio 的价值。
应用案例二
第二个是合规平台场景。现在企业里很多都要讲究数据的合规,它是把我们采集回来的数据根据法律法规去做一些数据的糊化和转换,首先是对数据进行分类分级,之后识别出敏感的数据,再通过数据抽帧,对于敏感的帧要进行识别和删除。剩下的数据,比如点云或轨迹,里面会有一些地理性的坐标,这种精准的信息是不允许对外的,只有经过坐标偏转之后才能对外提供服务。最后是个性脱敏,比如拍到的图片里面有人脸、车牌这些属于个人隐私的内容,需要通过算法推理做糊化,输出的图片才能交付给下游使用。
这些服务是通过工作流串联起来的,比如通过消息队列,我们在每个服务侧挂载了 Alluxio,采用 Alluxio sidecar 方式将对象存储挂载到本地,所有数据流转都通过写入本地挂载路径,通过 Alluxio 同步到 OBS 。我们看到当 FUSE Client 开启 cache,Alluxio Fuse 的读取速度从 50MB/sec 提升到了 200MB/sec ;敏感区识别和个人脱敏(人脸和车牌识别糊化)这些推理服务加载预热后的 Alluxio 数据,推理性能提升了40%。
应用案例三
第三个场景是仿真测试平台案例。如图左边的集群是业务集群在 K8S 里,右边是由 C++ 和 C# 写的仿真程序,名字叫Smartview。原先在仿真测试平台我们有个播放 bag 包的case,平台需要获取 OBS 上采集数据目录,然后拉取 bag 包进行仿真程序的回放播包,给算法进行一些 corner case 的定位以及回放;仿真程序单独部署在一个工控机上,运行在 Docker 容器中,需要让 k8s 集群外的 fuse 客户端能访问集群中的 Alluxio;这个时候需要对 Alluxio集群做修改,比如 worker 的配置,将 worker 中 hostNetwork 置为 true。
现在仿真测试也是在用这个环境,1个 bag 包大小都不一样,最小的也超过1GB,属于大文件的读写,我们看到在仿真程序里播包没有任何的延迟,都很顺畅。
Sidecar 接入方法
业务要接入 Alluxio 实现挂载 obs 需要在 k8s 部署 yaml 文件增加如下配置:
比如 haohan-datainject 服务的 deploy.yaml 中增加:
1. pod 配置中增加用户权限配置:
securityContext:
runAsUser: 0
runAsGroup: 0
fsGroup: 0
2. pod 增加存储卷申明:
volumes:
- name: alluxio-fuse-volume
emptyDir: {}
3. 增加 sidecar 容器配置:(resources 部分可以自行定义)
4. 业务容器增加挂载配置:
知识点–挂载卷的传播:
挂载卷的传播能力允许将容器安装的卷共享到同一 Pod 中的其他容器,甚至共享到同一节点上的其他 Pod。卷的挂载传播特性由 Container.volumeMounts 中的 mountPropagation 字段控制。它的值包括:
√ None - 此卷挂载将不会感知到主机后续在此卷或其任何子目录上执行的挂载变化。类似的,容器所创建的卷挂载在主机上是不可见的。这是默认模式。该模式等同于 mount(8) 中描述的 rprivate 挂载传播选项。然而,当 rprivate 传播选项不适用时,CRI 运行时可以转为选择 rslave 挂载传播选项 (即 HostToContainer)。当挂载源包含 Docker 守护进程的根目录(/var/lib/docker)时, cri-dockerd ( Docker ) 已知可以选择 rslave 挂载传播选项。
√ HostToContainer - 此卷挂载将会感知到主机后续针对此卷或其任何子目录的挂载操作。换句话说,如果主机在此挂载卷中挂载任何内容,容器将能看到它被挂载在那里。类似的,配置了 Bidirectional 挂载传播选项的 Pod 如果在同一卷上挂载了内容,挂载传播设置为 HostToContainer 的容器都将能看到这一变化.该模式等同于 mount(8) 中描述的 rslave 挂载传播选项。
√ Bidirectional - 这种卷挂载和 HostToContainer 挂载表现相同。另外,容器创建的卷挂载将被传播回至主机和使用同一卷的所有 Pod 的所有容器。该模式等同于 mount(8) 中描述的 rshared 挂载传播选项。
AlluxioFuse 这里一共有三个挂载:
√ AlluxioFuse 把 Alluxio 挂载到容器内的 /mnt/alluxio-fuse 目录下;
√ k8s 把宿主机路径 /mnt 以 bi-direction 的方式挂载到 AlluxioFuse 容器内的 /mnt 路径;
√ k8s把宿主机路径 /mnt 挂载到 application container 内任意路径。
先说第三个挂载,application container,其实把宿主机的 /mnt 还是 /mnt/alluxio-fuse 挂进去都是可以的,只不过容器内部会是不同的路径访问数据,多了一层子目录 alluxio-fuse,前两个挂载是有一点 tricky 的。如果 k8s 把目录 /mnt/alluxio-fuse 挂进 fuse container 的 /mnt/alluxio-fuse,然后 alluxioFuse 又把 Alluxio 挂到 /mnt/alluxio-fuse,会把第一个挂载给挤掉。这个其实还算比较符合直觉,不可能把路径A和路径B都挂到路径C上。挤掉之后这层宿主机和alluxio-fuse 的 connection 就没有了,application 自然也就没办法读到数据。当然用户如果不想把一整个/mnt 都挂进fuse container 里面,也可以用 /mnt/alluxio/alluxio-fuse 取代 /mnt/alluxio-fuse,/mnt/alluxio 取代 /mnt。
JAVA API 访问 Alluxio
应用 pom 文件添加依赖:
<dependency>
<groupld>org.alluxio</groupld>
<artifactld>alluxio-core-client-fs<?artifactld>
<version>2.9.3</version>
</dependency>
数据预热:Alluxio master 默认是存储元数据的,挂载以后客户端侧默认能看到所有挂载路径下的文件系统系统其实是通过 master load 到元数据,但是业务访问文件系统实际上还要取load数据,数据块是存储在 worker 中的,如果 worker 没有则会去底层的 UFS 即 obs 加载数据,这个在业务运营过程中其实对性能是有损的,所以通常我们惯用的方法就是提前数据预热。Alluxio 的 Java api 中 FileSystem 接口提供大部分文件系统的操作 API,比如新建、删除、挂载、读取、加载元数据等等,当然也有通过 job service 方式提供了其他额外的接口,比如 LoadJob,即可实现 load 数据到 worker。代码如下:
try {
AlluxioProperties properties = new AlluxioProperties();
properties.set(PropertyKey.MASTER_HOSTNAME, "10.79.180.14");
properties.set(PropertyKey.MASTER_RPC_PORT, 30619);
AlluxioConfiguration conf = new InstancedConfiguration(properties);
FileSystem fs = FileSystem.Factory.create(conf);
AlluxioURI path = new AlluxioURI("/origin/101/1664083078340767745/CAIC_郁健峰_L42340_南京市_城区_晴天_白天_20221104_rosbag_202211041650_1667551809843");
LoadJobPOptions.Builder options = alluxio.grpc.LoadJobPOptions
.newBuilder().setPartialListing(true).setVerify(true);
LoadJobRequest job = new LoadJobRequest(path.getPath(), options.build());
Optional<String> jobId = fs.submitJob(job);
if (jobId.isPresent()) {
System.out.printf("Load '%s' is successfully submitted. JobId: %s%n", path, jobId.get());
} else {
System.out.printf("Load already running for path '%s', updated the job with "
+ "new bandwidth: %s, verify: %s%n",
path,
"unlimited",
"verify");
}
} catch (Exception e) {
System.out.println("Failed to submit load job " + ": " + e.getMessage());
}
遇到的问题
√ 我们的场景有读写,读其实没有太大的问题,但是写入目前2.9.3版本不支持追加写目前写的需求。目前我们是先写临时目录,然后复制到指定分发目录;
√ 元数据同步,目前 Alluxio 是通过设置 metadata sync interval 设置相关 UFS 的元数据同步,如果开启设置固定时间间隔,我们担心对 Alluxio 自身有性能影响,所以还是通过手动通过命令或者代码触发元数据同步。
未来规划
√ 从基础设施层去优化整个 Alluxio 集群的性能,赋能算法训练推理
- 将 Master 独立部署——稳定性保障
- 将 Worker 与 GPU 节点混布——降本增效
- 将 Worker 与 Fuse 混布——本地缓存
√ 参与社区研发,定制化开发一些场景需求