Lyft Level 5 无人驾驶团队分享 PyTorch 生产部署经验。
Lyft 的使命是用世界上最好的交通工具改善人们的生活。我们相信,在未来,无人驾驶汽车将使交通更加安全,人人都更加方便。这就是为什么 Lyft 的无人驾驶部门 Level 5正在为 Lyft 网络开发一个完整的无人驾驶自治系统,给乘客提供这种技术的便利。然而,这是一项极其复杂的任务。
在开发中,我们使用各种各样的机器学习算法来驱动我们的无人驾驶汽车,解决地图、感知、预测和规划方面的问题。为了开发这些模型,我们对数百万张图像和密集的 LiDAR/RADAR 点云以及很多其他类型的输入(如代理轨迹或视频序列)进行了训练和验证。然后这些模型被部署在 Lyft 的无人车(autonomous vehicle / AV)平台上,在上面他们需要以毫秒级的速度推理边界框,交通灯状态,以及车辆轨迹等。
我们早期构建的框架是为了在项目的第一年快速提高机器学习的效率,使我们的产品能够上路。但是,这些解决方案并没有提供我们所需的开发速度和规模来解决随着项目增长而出现的问题。我们的机器学习框架需要满足我们的团队在几个小时内(而不是几天)建立和训练复杂的模型,验证指标,并将深度学习模型部署到无人车队中。我们现在有了一个框架,它不仅满足了我们快速迭代和规模化的所有要求,而且统一了 Lyft Level 5所有工程师和研究人员的机器学习开发。请继续往下读,了解我们是如何实现这个目标的。
我们相信快速的迭代和适应是 Level 5 成功的关键。这一原则同样适用于我们的机器学习(ML)模型和 ML 工具。2017年创立 Lyft Level 5时,我们在桌面上训练了一些基本的计算机视觉模型。仅仅几个月后,我们建立了第一个内部训练框架,使我们能够扩大规模。有了这个框架,我们在无人车上部署了近12个模型,但是很快我们意识到我们需要一个范式的转变,重点是以下几个关键原则:
在考虑了这些原则之后,我们决定创建一个解决方案,将 PyTorch 纳入我们下一代机器学习框架的核心。在6个月的时间里,我们建立了一个原型系统,从上一个框架中移植了12个产品化的无人车模型,40多个机器学习工程师参与,并为 Level 5所有人建立了一个统一的机器学习框架。
在我们all in PyTorch 之前,我们想要验证它是否能够适应我们的场景用例。为了实现这一点,我们需要在 PyTorch 中实现我们的一个模型,对我们的数据进行训练,并将其部署到我们的 C++ 无人驾驶技术栈中。
图1—— LiDAR Segmenter模型结构图及Lidar semseg输出例子
我们选择实现的端到端的候选建模任务是 LiDAR 语义分割,这是一个接收 3D LiDAR 点云并将每个点分类的任务,类别比如地面、汽车、人等(参见图1)。
我们首先写了一个 PyTorch DataSet 类,它对带标注的点云的二值文件进行操作。然后,我们必须编写一个自定义的函数,将不同的数据项聚合到一个batch中,以便使用 PyTorch DataLoader。
鉴于我们现在有了一个可以工作的数据加载管道,我们开始实现我们的模型。我们当时的模型有以下结构:
其中一些阶段,如DeVoxelization在我们先前的实现中是用 CUDA 手写,花费数周的工程时间。我们发现,在原生 PyTorch 中使用诸如 scatter_add 和 index_select 这样的原语实现这些功能,可以让我们无需手写内核就能获得类似的性能,从而可以在几天内生成相同的模型。
对于模型的其余部分,我们能够利用 torch "nn"包中的模块,利用卷积和各种损失函数等算子。在我们有了一个模型实现之后,我们编写了一些用于训练的标准样板代码,我们的模型可以在数据集上收敛。
现在我们有了在数据集上产生训练好的模型的方法,剩下的唯一事情就是让它在我们的 C++ 无人驾驶技术栈中工作。在这个概念验证的时候(PyTorch 1.3) ,我们发现了两个可以导出并在 C++ runtime 环境中运行“冻结模型”的选项:
从我们之前的框架中学到的是: 尽管 ONNX 和 TensorRT 的历史更长,并且在某些情况下对推理速度进行了更优化,但我们重视快速部署模型的能力,以及更轻量级的管道(更少的外部库和依赖性) ,这使我们能够快速进行实验,而不必受制于 ONNX 的各种限制和编写自定义 TensorRT 插件。我们意识到我们总是需要编写自定义内核和操作,但我们宁愿编写 LibTorch 扩展(更多细节见下面的部分) ,而不是添加更多的黑盒外部层。由于这些原因,我们决定对 TorchScript 推理进行评估。
评估的最后一步是将我们的 TorchScript 模型集成到我们的无人驾驶技术栈中。首先,我们使用 PyTorch 提供的 LibTorch 共享库构建,并将其集成到构建中。然后我们能够利用 LibTorch C++ API 将模型集成到我们的 LiDAR 栈中。我们发现这个 API 虽然是C++,但对 python PyTorch API 用户很友好。
model = torch::jit::load(model_file, torch::kCUDA);
model.eval();
最后,我们必须验证模型是否在延迟预算之内,我们发现实际上实现了比当前生产模型更低的延迟和误差。
总的来说,我们认为我们的概念验证进行得非常好,我们决定继续在 Lyft Level 5 使用 PyTorch 进行生产训练和部署。
图2—— 基于 PyTorch 的完全分布式训练框架的高层草图,称为“ Jadoo”(来源于印地语中的术语“ Magic” ,是对我们以前的机器学习框架“ Magician”的致敬)。Level 5 机器学习的研究人员和工程师都在这个框架上开发。在本地运行作业和将作业分发到云上的许多节点之间没有任何额外的步骤。实际上,我们的计算基础设施团队已经无缝地集成了许多必要的资源,以便在云(比如 AWS SageMaker 执行引擎)上安排我们的分布式作业。
作为 PyTorch 生产化过程的一部分,为了增强我们的机器学习工程师的能力,我们创建了一个名为 Jadoo 的 Lyft 内部框架(参见图2)。与某些框架不同的是,我们的目标是提供一个强大的生态系统,能够简化 runtime 环境和计算基础设施,允许机器学习工程师和研究人员快速迭代和部署代码,而不是试图让机器学习对非专家来说“更容易” ,并抽象出 PyTorch 的所有优点。
Jadoo 的一些核心特性包括:
我们设计了分布式训练环境来模拟本地环境,这样用户可以在本地和分布式云训练之间无缝切换。实现这一目标的第一步是确保本地的开发环境得到良好的控制和容器化。然后,我们在环境中使用相同的容器,用于本地开发、分布式云训练和持续集成的 Jadoo/用户代码。对于分布式训练,我们可以非常依赖 PyTorch 中的分布式包。我们遵循了这样的模式,即使每个 GPU 都有自己的流程,并将我们的模型包装在 DistributedDataParallel 中。
def __init__(self) -> None:
super().__init__()
self.model = self._load_model(pretrained_backbone=True).cuda()
self.model = DistributedDataParallel(self.model, device_ids=[torch.cuda.current_device()],
output_device=torch.cuda.current_device())
Jadoo 负责在节点和工作者之间进行数据共享,因此用户只需以一种易于分区的方式创建数据集。我们还发现 Nvidia NCCL 后端对于训练和其他操作(如分布式 all-reduce、scatter 和gather)都有很高的性能。计算节点的协调和供应由我们的底层基础设施来处理。我们还控制模型状态的 checkpoint,以允许节点抢占和中断,以节省成本,如热点实例的训练。
使用 Jadoo,我们希望优先构建能够在 AV 中通过 C++ runtime 高效运行的模型。我们发现 LibTorch 允许我们通过TorchScript轻松地部署训练好的模型,而 C++ API 使其非常容易使用。当我们部署的模型需要进行预处理或后处理时,C++ API 特别有用,因为 API 遵循熟悉的 PyTorch。
# Python
output = torch.where(score_over_threshold, label, unknown_labels)
// C++
const auto output = torch::where(score_over_threshold, label, unknown_labels);
需要注意的一点是,尽管我们从 PyTorch 提供的LibTorch开始构建概念验证,但我们发现大型静态链接库很难管理。为了解决这个问题,我们使用自己的依赖项从源代码编译 LibTorch,通过共享库链接 LibTorch 的依赖项。这样我们将 LibTorch 的二进制文件大小减少一个数量级。
为了确保用户可以轻松地部署他们训练过的模型,Jadoo 检查模型是否可以在训练期间转换成 TorchScript,如果可以,定期保存包含 TorchScript 模型的 checkpoints 以及任何允许追溯模型起源的附加元数据。这些元数据包括训练运行、 GitSHA、用户名和用户选择跟踪的任何其他元数据。此外,Jadoo 自动分析这个 TorchScript 模型的延迟以及它的 MACs (乘-加)和参数计数。
当用户准备部署模型时,他们只需指向他们想要的从训练运行得到的模型,然后它就可以在我们的构建在 LibTorch 上的 C++ runtime 运行推理。
我们发现这是最佳实践,因为用户正在构建他们的模型时会记着 TorchScript 。这避免了复杂模型带来的麻烦,只需在尝试部署模型时发现,由于 TorchScript 不兼容的语法,需要更改大量模型 APIs。我们还发现,单元测试是确保模型和模块兼容并保持 TorchScript 兼容性的好方法。例如,通常一个开发人员可能会更改另一个开发人员使用的公共模块,并添加不受 TorchScript 支持的语法,这会在 CI (持续集成)的早期就被发现,并且永远不会进入生产环境。
在工程组织中,研究人员和工程师的工作方式往往有所不同; 研究人员希望自由地探索和快速迭代各种想法,而工程师则希望构建经过良好测试和可靠的产品。通过 Jadoo,我们建立了一个范式,在这个范式中,研究人员和工程师可以使用相同的框架进行共同开发,从而允许我们快速地创建一个从想法到生产的迭代循环。我们通过以下方式实现这一目标:
大量优化机器学习开发人员的迭代周期
使实验易于复现和可比较
保持高编码标准
图3 —— Dashboard 显示 Lyft Level 5过去6个月所有生产训练作业的每周平均时间(P50)的大约是几个小时(以前为几天)。为了达到这个目的,每个作业都要在云端的数十到数百个 GPU 上运行分布式训练,通过我们的内部模型框架 Jadoo 和 PyTorch 的帮助,对 worker 数量进行优化,以实现非常高效的扩展。
我们相信,通过采用 PyTorch 和构建 Jadoo,我们已经在机器学习方面取得了巨大的进步。在短短几个月的工作中,我们已经能够将十几个模型从旧框架移动到 PyTorch,并使用 TorchScript + LibTorch 部署。我们还能够将2D 和3D 检测器以及语义分割器等大型生产作业的中位数训练时间从几天缩短到大约1小时(见图3) ,允许用户指定他们需要多少计算资源和时间。这让我们的用户的模型开发每天进行很多次迭代,而这在之前是不可能的。我们相信我们也已经建立了一个独特的框架,真正使我们的机器学习工程师和研究人员能够在更短的时间内做更多的事情,使他们能够快速迭代各种想法,并将它们部署到交通工具平台上,而不必尝试任何机器学习简化。
展望未来,尽管我们已经训练了数百万的数据样本,但是我们的数据集规模正在呈指数增长。我们需要能够维持对越来越多的数据的训练,还需要单个作业能够扩展到数千个 GPU 且可容错。为此,我们正在研究 PyTorch Elastic 之类的容错技术。我们还计划围绕推理性能分析和优化,以及模型和数据自省和可视化来扩展工具库。
原文:https://medium.com/pytorch/how-lyft-uses-pytorch-to-power-machine-learning-for-their-self-driving-cars-80642bc2d0ae