本文来源:瓦砾村夫
/ 导读 /
Bazel是谷歌开源的分布式软件构建系统 。BazelCon是Bazel软件社区的首要年度分享活动。今年的BazelCon在纽约的57号码头举行。
【资料图】
特斯拉的本次分享长约19分钟,本文字数约4千。
源视频:Google Open Source /翻译及字幕:瓦砾村夫
使用Bazel构建autopilot软件
演讲全文
Gabriel:大家好,我叫Gabriel。
Romu:我叫Romi。
Gabriel:我们是特斯拉的软件工程师,从事Autopilot的工作。我们很高兴今天能在这里, 讨论我们如何使用Bazel开发汽车和机器人的软件 。
但在我们开始之前,我想播放一个简短的视频,向你们展示我们的成果。
那是我们的 完全自动驾驶,或FSD beta项目的运行视频 。现在,让我们来看看,它是如何运作的。
这个系统的运作机制是,接收来自环绕汽车的八个摄像头的信号,将其输入神经网络,以三维的方式重建汽车周围的场景。我们称之为 三维向量空间 。
然后,我们 混合使用C++和神经网络,运行规划和控制模块 。有大量的数据需要在低延迟的条件下进行处理,而且都是保证安全的关键要素。
我们不得不搭建自己的FSD计算机,更快更有效地进行神经网络各种操作的计算。我们正在为这款FSD计算机开发软件。
为了快速取得进展,我们需要两个要素:能够快速迭代我们的技术栈,以及用以支持这一目标的合适的基础设施。
这是我们工作流程的一个简化图,我们从顶部开始。
我们使用从我们的车队收集得到的视频片段,训练我们的神经网络。我们在我们的AI集群(我们称之为Dojo)和GPU集群上进行训练。训练完神经网络后,我们将其整合到构建版本中,对它进行评估,然后部署到我们的工程车队,以获得实测里程数据。最后,我们将分批部署到主车队。
在这次演讲中,我们想 把重点放在构建,评估,以及FSD计算机集群上 。从构建开始。
我简单提到过,我们的技术栈,是 C++和神经网络的混合 。我们有许多工程师在C++代码库上开展工作,而他们需要能够快速迭代。
为了演示我们如何做到这一点,我们将创建一个新的C++库。
为了创建一个新的库,我们需要创建一个新的头文件和源文件。然后,我们增加include语句,添加依赖。最后,我们需要在构建文件中用正确的依赖关系创建一个cc库目标。
从另一个角度看,我们似乎把依赖关系图更新了两次,一次在C++中,另一次在Bazel中。
我们的目标,是让工程师们专注于C++代码,而Bazel的目标应该能从源代码中推断出来。
为了做到这一点,我们使用了一个叫做 gazelle 的工具。gaze lle读取源代码,并更新或生成构建文件,而且它可以被扩展为支持任何语言。我们增加了对C++的支持。
为了充分利用该插件,我们建立了一些最佳实践方式。
要让插件管理一个目录,这个目录中的构建文件必须包括我们的自定义指令。我们在构建文件的顶部添加我们的指令。我们运行工具,gazelle将给我们提供右边的构建文件。每个生成的cc库只有一个头文件和一个源文件,这让我们能够更细粒度的构建版本。
但它也有另一个属性,那就是,我们必须让 include语句使用 从根开始的完整路径。这让我们可以通过阅读include语句,推断出Bazel的依赖关系,而且也很容易就能弄清楚所有的依赖关系。
我们从头文件中提取include,这些成为了deps。而我们从源文件中获取include,这些成为了实现的deps。
我们已经看了C++的情况,现在让我们来看看神经网络。
就像我们把C++代码编译成可以在机器上运行的二进制文件一样,我们必须 将神经网络编译成汽车或机器人能够理解的二进制文件 。
我们正在为这台FSD计算机进行开发,它有CPU,GPU和一个称为Trip的AI加速器。每种形式的计算,都针对不同的工作负载进行了优化。例如,我们很多的神经网络矩阵操作在Trip上运行得更好。
让我们看一个例子,这是占用网络。顾名思义,它用来确定向量空间中的任意给定坐标是否被占用。
而这是它的架构。每个节点都是一个神经网络的操作,流向是从上到下。
这个网络非常庞大,其中一些操作在CPU或GPU上运行得更好,而其他操作在Trip上运行得更好。我们要做的是,把网络分割成子图,为合适的计算进行优化。
进行分割的代码,是Basel资源库的一个角色,我们称之为 分割器 。
我们一开始用一个手写的构建文件来管理这些模型,但它变得非常非常庞大。而最初的分割器是一套自定义规则,但要声明每个子图的依赖关系以及不同子图间的连接方式,很快就变得让人沮丧了。
最终,我们 把分割器变成了一个资源库的规则 。
而现在,它加载了仓库中的所有模型,对它们进行分割,并针对合适的Trip创建每个子图。在将模型分割成子图之后,每个子图都使用合适的编译器进行编译。
当对一个神经网络进行修改时,主要的工作流程是更新末端节点,在这里被展示为左边的两个头部节点。
如果我们改变这两个头部节点,中间的头部子图将必须要重新编译,但其他的子图都会被高速缓存击中,这让我们能够非常快速地迭代神经网络的改动。
我们已经将 占用网络分割成子图 ,这些子图可以进行并行编译,但我们也需要考虑到关键路径,我们需要让每个子图都快速编译。
因为我们开发了编译器,我们能把它分解成三个步骤,每个步骤都在Bazel中使用自定义规则进行建模。
而整条流水线则以宏的形式实现,工程师可以快速尝试不同的策略。
而且 如果临时构建产物是相同的,那么它们就可以受益于缓存命中 ,而且它们就不需要重新运行。这可以让从事编译器工作的工程师能够非常快速地进行迭代
虽然在这个演讲中,我们只涉及了神经网络和C++的编译工具链,我们正在处理软件2.0技术栈的所有部分。但我们会把那些部分留给下一次演讲。
现在,Romy将谈谈我们如何评估改动。
评估autopilot的性能,仿真及FSD计算机集群
Romy:我们已经讨论了我们 如何构建autopilot ,现在,我们会谈谈如何评估autopilot的性能。
我们有两种形式的评估,第一种形式叫做 开环评估 。
开环评估,我们从我们的特斯拉车队中收集数据,获取这些预先记录的传感器数据,将其输入autopilot。并从那里开始记录控制模块的输出,验证autopilot是否按预期执行。
第二种评估形式,被称为 闭环评估 。这里,我们通过仿真,产生人工合成的传感器数据。我们将其送入autopilot,并和之前一样,记录控制模块的输出。然而,由于这些数据完全是人工合成的,我们可以获取控制模块的输出,并在仿真中使用它来产生新的人工合成数据,从而形成闭环。
现在,我们了解了这两种评估方式,让我们看看如何利用它们来解决autopilot的问题。
在接下来的这个视频中,你会看到一个骑自行车的人迅速进入autopilot的车道。
为了避开这个骑自行车的人,autopilot向左偏了一下。你可以看到,在这里的左边,汽车非常接近左边车道上的汽车。
从我们的数据可以看出,这是一个非常不舒服的体验,需要我们加以解决。autopilot工程师们能够 在控制和视觉模块上做出改变,并重新运行仿真 。在重新运行的过程中,我们可以看到,一切都按预期进行。
仿真给了我们无限的可能性,因为我们可以改变景观,角色,光照和地形,这为我们所有的测试案例提供了可观的能力。
现在,我们了解了开环和闭环仿真是如何帮助我们的autopilot的,让我们深入了解Bazel如何与评估互动,以及两者如何都在我们的服务器上运行。
我们的数据中心有三个组成部分:存储,评估服务器和FSD计算机 。让我们深入了解每一部分。
我们的数据是由存储在Bazel缓存中的构建产物,我们的docker镜像,以及来自我们客户车队的数据组成的。这些数据被送入我们的评估服务器。
这里,你可以看到整个 评估循环 。评估世界的状态被送入渲染器,这些渲染得到的图像和仿真的传感器,被送入autopilot计算机。这会产生控制输出,用来更新评估世界的状态。从那里,我们可以为下一个循环生成新的渲染图像。
我们的数据中心在同一批机器上运行构建和评估。现在,我们已经探索了高层次的软件架构,让我们来看看,使这一切成为可能的硬件。
构建和评估是要求极高的任务,需要大量的带宽和计算 。每次构建,我们从我们的Bazel缓存中取出53G数据,而每次仿真需要45G。
这些带宽要求带来了各种挑战,我们将在本次演讲的后面部分谈及。
最终的仿真图像是以每台FSD计算机10G链路进行流式传输的。
我们介绍了单次评估和单次构建的硬件和软件,接下来,我们将介绍,如何通过我们的FSD计算机群来扩展这两者。
这是一台 FSD计算机 。在我们添加了散热器,冷却器,高速网络和外壳之后,为了让这台计算机适合于数据中心,我们把它们和风扇,继电器,电源和热传感器组合起来一起放入托盘。
这些托盘垂直堆放在机架上。我们的数据中心有很多排这样的机架。我们在全球有多个数据中心,并拥有超过5000台计算机,FSD计算机,而且我们还在不断扩大这个规模。
在2018年,我们让FSD计算机能在桌面上运行,而且我们的评估量相对较低。
我们在2019年搭建了我们的第一个完整的机架,并在2020年底,以年仿真量3000万次,让我们的第一个数据中心达到了饱和。
在2022年,我们每周运行200万次仿真。
而这个规模给我们带来了很多独特的软件上的挑战,包括网络,分布式计算和存储。鉴于我们现在是在BazelCon会议上,让我们谈谈,这样的规模如何影响我们的Bazel缓存。
在开始的时候,我们有几台机器, 击中单个Bazel缓存,这个缓存是作为nginx服务器实现的 。这个简单的配置应该很熟悉,因为它是BazelCon网站的建议之一。
随着规模的扩大,我们碰到了磁盘IO的限制,以及单一缓存服务器的空间问题。
为了解决这个问题,我们决定进行横向扩展。我们添加了一个负载均衡服务,这解决了我们的磁盘IO问题,以及磁盘空间问题。但是,随着每次构建的8G峰值带宽,进入负载均衡服务的带宽成了问题的关键所在。
我们添加了客户端的负载均衡服务,并特意考虑了我们数据中心的网络拓扑结构,以消除这个瓶颈。
我们在多个节点上 采用一致的hash算法 ,以确保我们在单个数据中心内,能实现缓存命中。
如果我们把前面的图放大,我们就可以完整地看到单个数据中心的情况。随着时间的推移,我们把数据中心扩展到了几个,我们希望避免数据中心之间的高速缓存不命中,并理解数据中心之间的入口和出口是一种宝贵的资源。
为了解决这个问题,我们实现了 分层Bazel缓存 。每个数据中心都实现了写直通和读直通缓存。每一个新的构建产物,Bazel都会写直通到数据中心的本地缓存上,并把它保存在位于独立数据中心的黄色主缓存中。数据中心内的缓存不命中,会读直通到主缓存上,并把构建产物拉取到数据中心的本地缓存。
通过以上方式,我们就能保证很高的缓存命中率。
Bazel的读负载是非常重的,而且大部分的读取都是在数据中心内完成的 。正因为如此,主缓存的负载仍然相对较低,主要是写入负载较重。
目前为止,这个方案对我们来说扩展性不错。
在我们的技术栈中,我们用BuildBuddy和Grafana记录指标 。我们自己fork了BuildBuddy的代码,并利用它来收集额外的指标。
我们使用Bazel事件服务来记录每个构建目标所花费的时间,并检测回退问题。通过Grafana,我们为我们的负载和聚合记录了高层面的基础设施指标。
基于所有这些测量数据,我们记录了我们的负载是200万次评估,90000次构建。每周生成1PB的构建产物,每周进行1000万次测试。
为了支持这个级别的负载,我们必须搭建最先进的计算机,跨越五个数据中心,达到1exa-ops算力 。超过5000台FSD计算机,使用了超过90000个内核。
这些数字每几个月就会增加。
我们不仅仅是在扩展我们的基础设施,我们也在扩大我们的团队,我们正在进行招聘。如果你有兴趣加入我们的团队,请查看tesla.com/ai。
谢谢大家,我们的演讲到此结束。
- End -