米开朗基罗优步的机器学习(ML)平台支持公司内数千辆生产中的车型的培训和服务。该系统旨在覆盖端到端ML工作流,目前支持经典的机器学习、时间序列预测和深度学习模型,这些模型跨越了从生成到生成的无数用例市场预测,回应客户支持票,计算准确的预计到达时间为我们的一键聊天功能在驱动程序上使用自然语言处理(NLP)模型。
米开朗基罗的大部分模型都是基于Apache Spark MLlib,一个可扩展的机器学习库Apache火花.为了处理高qps在线服务,米开朗基罗最初只支持Spark MLlib模型的一个子集,使用内部定制的模型序列化和表示,这阻止了客户灵活地试验任意复杂的模型管道,抑制了米开朗基罗的扩展速度。为了解决这些问题,我们发展了Michelangelo对Spark MLlib的使用,特别是在模型表示、持久性和在线服务领域。
米开朗基罗Spark MLlib进化背后的动机
我们最初开发米开朗基罗是为了为生产提供可扩展的机器学习模型。它对基于spark的预定数据摄取、模型培训和评估的端到端支持,以及批量部署和在线模型服务,已经在Uber中获得了广泛的接受。
最近,米开朗基罗已经发展到可以处理更多的用例,包括服务在米开朗基罗核心之外训练的模型。例如,深度学习模型的扩展和加速端到端训练需要在不同的环境中执行操作步骤,以便利用Spark转换的分布式计算和Spark pipeline在cpu上的低延迟服务,以及在使用的GPU集群上的分布式深度学习训练Horovod,优步的分布式深度学习框架。为了促进这些需求并保证培训和服务的一致性,有一个一致的体系结构和模型表示是至关重要的,它可以利用我们现有的低延迟的基于jvm的模型服务基础设施,同时提供正确的抽象来封装这些需求。
另一个动机是授权数据科学家在熟悉的Jupyter Notebook环境中构建和实验任意复杂的模型(数据科学工作台),同时仍然能够利用米开朗基罗生态系统来可靠地进行分布式培训、部署和服务。这也为所需要的更复杂的模型结构提供了可能性整体学习或多任务学习技术,同时允许用户动态地进行数据操作和自定义计算。
因此,我们重新审视了米开朗基罗使用Spark MLlib和Spark ML管道推广它的模型持久性和在线服务机制,以在不影响可伸缩性的情况下实现可扩展性和互操作性。
米开朗基罗最初推出的是一个单片架构,管理紧密耦合的工作流程和用于培训和服务的Spark工作。米开朗基罗对每种支持的模型类型都有特定的管道模型定义,并有内部定制protobuf代表训练有素的服务模范。离线服务通过Spark处理;在线服务使用添加到Spark内部版本的定制api进行处理,以实现高效的单行预测。
最初的体系结构支持通用机器学习模型(如广义线性模型(GLM)和梯度提升决策树(GBDT)模型)的训练和服务,但自定义的protobuf表示使得为新的Spark变压器添加支持变得困难,并排除了在米开朗基罗之外训练的模型的服务。当Spark的新版本可用时,自定义的内部版本也使每次升级的迭代变得复杂。为了提高对新变压器的支持速度,并允许客户将他们自己的模型带到Michelangelo中,我们考虑如何发展模型表示,并更无缝地添加在线服务接口。
米开朗基罗建筑与模型表征的演变
Uber的机器学习工作流程通常很复杂,跨越了不同的团队、库、数据格式和语言;为了正确地发展模型表示和在线服务接口,我们需要考虑所有这些维度。
要部署用于服务的机器学习模型,需要部署整个管道,包括通向模型的转换工作流。通常还需要对数据转换、特征提取、预处理甚至预测后转换进行打包。原始预测通常需要解释或转换回标签,或者在某些情况下转换到可被下游服务使用的不同维度空间,如日志空间。用额外的数据来增强原始预测也很有价值,例如它们的置信区间和校准的概率概率校准.我们想要一个模型表示,它能反映Spark MLlib模型固有的管道步骤,并允许与米开朗基罗外部的工具进行无缝交换。
选择一个更新的模型表示
在评估替代模型表示时,我们评估了各种需求,包括:
-
- 表达广义转换序列的能力(必需的)
- 为在线用例处理轻量级服务的可扩展性(必需的)
- 支持将存储在米开朗基罗中的模型与非米开朗基罗本地Spark工具交换(必需)
- 列车与服务时间之间模型解释偏差风险低(需求高)
- Spark更新速度快,易于编写新的估计器/转换器(高需求)
我们考虑的一种方法是使用MLeap,一个提供管道和模型序列化的独立库包。毫升)和反序列化支持,使用专门的运行时来执行管道。MLeap具有理想的表达能力和对轻量级在线服务的支持。然而,它有自己专有的持久性格式,这限制了与序列化和反序列化普通Spark MLlib模型的工具集的互操作性。
MLeap还引入了一些服务时间行为偏离训练时间评估的风险,因为在技术上,服务时间的模型是从与训练时在内存中不同的格式加载的。MLeap还为Spark更新速度引入了摩擦,因为除了Spark MLlib本身使用的方法外,必须为每个变压器和估计器添加单独的MLeap保存/加载方法。砖的毫升导出dbml-local提供了类似的方法。
我们考虑的另一种方法是将经过训练的模型导出为标准格式,如预测模型标记语言(PMML)或用于分析的便携格式(PFA),两者都具有我们想要的表达能力和与Spark的交换,其中PMML在Spark和aardpfark提供Spark输出到PFA。然而,这些表示形式在服务时间行为偏差方面再次出现了风险,我们预计这比MLeap要高,因为通用标准在特定的实现中经常有不同的解释。标准在Spark更新速度方面也存在较大的阻力,因为标准本身可能需要根据Spark更改的性质进行更新。
我们发现最直接的方法是使用标准的Spark ML管道序列化来表示模型。Spark ML管道展示了我们想要的表达能力,允许与Spark工具集进行交换,演示了模型解释偏差的低风险,以及对Spark更新速度的低摩擦。它也很适合编写定制的转换器和估计器。
我们看到的使用Spark管道序列化开箱即用的主要挑战是它与在线服务需求的不兼容(Nick Pentreath也在他的Spark AI峰会2018演讲).这种启动本地Spark会话并使用它加载经过训练的Spark MLlib模型的方法相当于在一台主机上运行一个具有显著内存开销和延迟的小型集群,这使得它不适合许多需要毫秒级p99延迟的在线服务场景。虽然现有的用于服务的Spark api的性能不足以满足Uber的用例,但我们发现我们可以在这个开箱即开的体验中做一些直接的改变,以满足我们的需求。
为在线服务提供轻量级接口,我们添加一个OnlineTransformer可以在线服务的变压器的特性,包括利用低级Spark预测方法的单一和小列表方法。我们还调整了模型加载的性能,以达到我们的目标开销。
使用增强型变压器和估计器的管道
执行变压器或估计量可以由米开朗基罗在线训练和服务,我们建造了一个OnlineTransformer接口扩展了开箱即用的Spark变压器接口并强制两种方法:变换(实例:数据集[所有])和2)ScoreInstance(实例:Map[String, Any]).
变换(实例:数据集(对象))作为开箱即用的分布式批处理服务的入口点数据集基于执行模型。scoreInstance(instance: Map[String, Object]): Map[String, Object]作为较轻量级的API,用于在低延迟、实时服务场景中出现的单个特征值映射集的单行预测请求。背后的动机scoreInstance是提供一个轻量级的API,绕过了数据集s依赖于Spark SQL Engine的催化剂优化器对每个请求进行查询计划和优化。如上所述,这对于实时服务场景(如营销和欺诈检测,因此p99延迟的SLA通常为毫秒量级。
当火花迸发PipelineModel已加载,任何变压器它有一个类似的类,其中包括OnlineTransformerTrait被映射到那个类。这使得现有的经过训练的Spark模型(由受支持的变压器组成)可以获得在线服务的能力,而无需进行任何额外的工作。请注意,OnlineTransformer也实现了Spark的MLWritable而且MLReadable接口,它免费提供了Spark对序列化和反序列化的本机支持。
维护线上和线下服务一致性
朝着一个标准前进PipelineModel驱动体系结构进一步加强了在线和离线服务准确性之间的一致性,消除了任何自定义的得分前和得分后的实现PipelineModel.在每一个管道阶段,在实现自定义评分方法时的标准实践是首先实现一个公共评分函数。在线下变换,它可以作为一套Spark运行用户定义函数(UDF)的输入DataFrame同样的评分函数也可以应用到网上scoreInstance而且scoreInstances方法。在线和离线评分的一致性将通过单元测试和端到端集成测试进一步加强。
性能调优
我们最初的测量表明,原生Spark管道的加载延迟相对于我们自定义protobuf表示的加载延迟非常高,如下表所示:
这种序列化模型加载时间上的性能差异对于在线服务场景是不可接受的。模型在每个在线预测服务实例中虚拟分片,并在每个服务实例启动时、在新模型部署期间或在接收针对特定模型的预测请求时加载。在我们的多租户模型服务设置中,过多的加载时间会影响服务器资源的敏捷性和运行状况监视。我们分析了负载延迟的来源,并进行了一些调优更改。
影响所有变压器加载时间的一个开销来源是Spark的原生使用sc.textFile读取变压器元数据;为一个小的单行文件形成字符串的RDD非常慢。用Java I/O替换本地文件用例代码明显更快:
[loadMetadata在src / main / scala / org/apache/spark/ml/util/ReadWrite.scala]
影响我们用例中许多感兴趣的变压器的另一个开销来源(例如,LogisticRegression,StringIndexer,LinearRegression)正在使用Spark分布式读/选择命令来处理与变形金刚.对于这些情况,我们进行了替换sparkSession.read.parquet与ParquetUtil.read;直接拼花读/ getRecord大大提高了这些变压器的负载时间。
树集成变压器有一些特殊的相关调谐机会。加载树集成模型需要读取序列化到要调用的磁盘上的模型元数据文件groupByKey,sortByKey, Spark分布式读/选择/排序/收集小文件的操作非常慢。我们用镶木地板代替了这些读/ getRecord这要快得多。在树集成模型保存端,我们合并了树集成节点和元数据权值DataFrames避免写入大量读起来很慢的小文件。
由于这些调优工作,我们能够将基准测试示例的原生Spark模型加载时间从8 -44倍减少到仅比自定义原型buf加载慢2 -3倍,相当于比原生Spark模型加快了4 -15倍。考虑到使用标准表示的好处,这个级别的开销是可以接受的。
需要注意的是,米开朗基罗在线服务创造了一个本地SparkContext处理任何未优化变压器的负载,以便SparkContext不需要在线服务。我们发现留下一个SparkContext在没有激活模型负载的情况下运行可能会对性能和服务延迟产生负面影响,例如,通过SparkContext清洁剂。为了避免这种影响,我们停止SparkContext当没有负载运行时。
可服务管道的灵活施工
使用管道模型作为米开朗基罗的模型表示,可以作为用户灵活组合和扩展可服务组件单元的契约,保证在线和离线服务时保持一致。然而,这并没有完全封装关于如何在机器学习工作流的各个阶段利用管道模型的操作需求的差异。有些操作步骤或概念与机器学习工作流的特定阶段本身相关,但与其他阶段完全无关。例如,当用户在模型上评估和迭代时,通常会有超参数优化、交叉验证和生成模型解释和评估所需的特殊元数据等操作。这些步骤允许用户帮助生成、与管道模型交互,并对其进行评估,但是一旦它准备好进行生产,这些步骤就不应该合并到模型服务中。
机器学习工作流不同阶段的需求差异,促使并行地在常见的编排引擎上开发工作流和操作员框架。除了组合自定义服务管道模型的灵活性之外,这进一步允许用户组合和编排自定义操作的执行,以有向图或工作流的形式实现最终的服务管道模型,如下面的图6所示:
前进
在这一点上,Spark原生模型表示已经在Michelangelo的生产环境中运行了一年多,作为一种健壮的、可扩展的方法,在我们公司范围内大规模地服务ML模型。
多亏了这一进化和对米开朗基罗平台的其他更新,Uber的ML堆栈可以支持新的用例,如灵活的实验和在Uber的训练模型数据科学工作台,一个可以在Michelangelo中服务的Jupyter笔记本环境,以及端到端深度学习使用TFTransformers.为了突出我们的经验并帮助其他人扩展他们自己的ML建模解决方案,我们在2019年4月Spark AI峰会并提交了SPIP和JIRA开放我们对Spark MLlib的更改。
我们期待听到您在类似问题上的经验,我们非常感谢Spark代码库及其社区的价值。
Apache Spark是Apache软件基金会在美国和/或其他国家的注册商标。使用此标志并不意味着Apache软件基金会的认可。Docker和Docker标志是Docker, Inc.在美国和/或其他国家的商标或注册商标。Docker, Inc.和其他各方在本协议中使用的其他术语中也可能拥有商标权。使用此标志并不意味着Docker的背书。TensorFlow、TensorFlow标志和任何相关标志均为谷歌Inc.的商标。
参考文献
-
- Spark AI峰会2019,旧金山在生产和服务平台中使用Spark MLlib模型体验和扩展
- D.斯卡利,G.霍尔特,D.戈洛文,E.达维多夫,T .菲利普斯,D.埃布纳,V.乔杜里,M.杨,J.克雷斯波,D.丹尼森,机器学习系统中隐藏的技术债务第28届神经信息处理系统国际会议论文集,p.2503-2511







