数据科学

客户流失建模与预测的端到端蓝图第 3 部分

这是描述预测客户流失的端到端蓝图的系列文章的第三部分。在前几期文章中,我们已经讨论了机器学习系统的一些挑战,这些挑战直到您投入生产时才会出现:在 第一期付款 中,我们介绍了我们的用例,并描述了一个加速的数据联合管道;在 第二期 中,我们展示了高级分析如何适应机器学习生命周期的其余部分。

在第三期文章中,我们将介绍应用程序的分析和联合组件,并解释如何充分利用 Apache Spark 和 RAPIDS Apache 加速器 Spark 的一些最佳实践。

架构( Architecture )评审

An architecture diagram showing a federation and analytics application that takes five database tables and produces one table and a set of reports, a model training application that takes the federated table and the reports and produces a model, and a production inference application that serves the model.
图 1 :我们的蓝图架构的高级概述。

回想一下,我们的 blueprint 应用程序(图 1 )包括一个联邦工作负载和一对分析工作负载。

  • 联合工作负载 生成了一个关于每个客户的非规范化宽数据表,这些数据来自于五个与客户账户不同方面相关的规范化观察表的数据汇总。
  • 第一次分析工作量 为每个特性生成一个机器可读的值分布和域的摘要报告。
  • 第二次分析工作量 生成一系列关于客户结果的说明性业务报告 我们的第一期 包含有关联合工作负载的其他详细信息, 我们的第二期 包含有关分析工作负载的其他详细信息。

我们将这三个工作负载作为一个具有多个阶段的 Spark 应用程序来实现:

  • 应用程序将 HDFS 中多个表(存储为拼花文件)的原始数据联合到一个宽表中。
  • 因为宽表比原始数据小得多,所以应用程序然后通过合并到较少的分区并将数值转换为适合 ML 模型训练的类型来重新格式化宽输出。此阶段的输出是 ML 模型训练的源数据。
  • 然后,应用程序针对合并和转换的宽表运行分析工作负载,首先生成机器可读的摘要报告,然后生成汇总和数据立方体报告的集合。

性能注意事项

并行执行

50 多年来,提高并行执行的适用性一直是计算机系统高性能最重要的考虑因素之一 ( 我们有点武断地选择在 1967 年确定 托马苏洛算法 的开发,它为无处不在的超标量处理奠定了基础,因为在这一点上,对并行性的关注变得实用而不仅仅是理论上的。)在分析员、数据科学家、数据和 ML 工程师以及应用程序开发人员的日常工作中,对并行性的关注通常表现为以下几种方式之一;我们现在来看看。

向外扩展时,在集群上执行工作

如果您使用的是横向扩展框架,请尽可能在集群上而不是在单个节点上执行工作。在 Spark 的情况下,这意味着在执行器上执行 Spark 作业中的代码,而不是在驱动程序上执行串行代码。 一般来说,在驱动程序中使用 Spark 的 API 而不是宿主语言代码将使您获得大部分的成功,但是您需要确保所使用的 Spark API 实际上是在执行器上并行执行的。

操作集合,而不是元素;在列上,而不是行上

开发并行性和提高性能的一般最佳实践是使用一次对集合执行操作的专用库,而不是一次对元素执行操作。在 Spark 的情况下,这意味着使用数据帧和列操作,而不是迭代 rdd 分区中的记录;在 Python 数据生态系统和 RAPIDS . ai 中,这意味着使用在单个库调用中对整个数组和矩阵进行操作的 矢量化操作 ,而不是在 Python 中使用显式循环。最关键的是,这两种方法也适用于 GPU 加速。

分摊 I / O 和数据加载的成本

I / O 和数据加载成本很高,因此在尽可能多的并行操作中分摊它们的成本是有意义的。 我们可以通过直接降低数据传输成本和在数据加载后尽可能多地处理数据来提高性能。在 Spark 中,这意味着使用列格式,在从稳定存储导入时只过滤一次关系,并在 I / O 或无序操作之间执行尽可能多的工作。

通过抽象提高性能

一般来说,提高分析师和开发人员在应用程序、查询和报表中使用的抽象级别,可以让运行时和框架找到开发人员没有(或无法)预料到的并行执行机会。

使用 Spark 的数据帧

例如,在 Spark 中使用数据帧并主要针对高级数据帧 API 进行开发有许多好处,包括执行速度更快、查询的语义保持优化、对存储和 I / O 的需求减少,以及相对于使用基于 RDD 的代码显著改善了内存占用。但除了这些好处之外,还有一个更深层次的优势:因为数据帧接口是高级的,而且 Spark 允许插件改变查询优化器的行为,所以 RAPIDS Apache 加速器 Spark 有可能用在 GPU 上运行的等效但实际上更快的操作替换某些数据帧操作。

透明加速 Spark 查询

用插件替换 Spark 的查询规划器的一些功能是抽象能力的一个特别引人注目的例子:在能够在 GPU 上运行 Spark 查询之前几年编写的应用程序仍然可以通过使用 Spark 3 . 1 和 RAPIDS 加速器来利用 GPU 加速。

保持清晰的抽象

尽管使用新的运行时加速未修改的应用程序的潜力是针对高级抽象进行开发的一个主要优势,但实际上,对于开发团队来说,维护清晰的抽象很少比按时交付工作项目更重要。由于多种原因,抽象背后的细节常常会泄漏到产品代码中;虽然这可能会引入技术债务并产生无数工程后果,但它也会限制高级运行时的适用性,以优化干净地使用抽象的程序。

考虑适合 GPU 加速的操作

为了从 Spark 中获得最大的收益,在围绕 Spark 的数据帧抽象的应用程序中偿还技术债务(例如,通过将部分查询实现为 RDD 操作)是有意义的。 不过,为了充分利用先进的基础设施,在不破坏抽象的情况下考虑执行环境的细节通常是有意义的。 为了从 NVIDIA GPU 和 RAPIDS Apache 加速器 Spark 获得尽可能好的性能,首先要确保您的代码不会围绕抽象工作,然后考虑或多或少适合 GPU 执行的类型和操作,这样您就可以确保尽可能多的应用程序在 GPU 上运行。下面我们将看到一些这样的例子。

类型和操作

并不是每一个操作都能被 GPU 加速。当有疑问时,运行作业时将 spark.rapids.sql.explain 设置为 NOT_ON_GPU 并检查记录到标准输出的解释总是有意义的。在本节中,我们将指出一些常见的陷阱,包括需要配置支持的十进制算法和操作。

小心十进制算术

十进制计算机算法支持高达给定精度限制的精确运算,可以避免和检测溢出,并像人类在执行铅笔和纸张计算时那样舍入数字。尽管十进制算法是许多数据处理系统(尤其是金融数据)的重要组成部分,但它对分析系统提出了特殊的挑战。为了避免溢出,十进制运算的结果必须扩大到包括所有可能的结果;在结果比系统特定限制更宽的情况下,系统必须检测溢出。在 cpu 上使用 Spark 的情况下,这涉及将操作委托给 Java 标准库中的 BigDecimal 类 ,并且精度限制为 38 位十进制数字或 128 位。 Apache 的 RAPIDS 加速器 Spark 目前可以加速计算多达 18 位或 64 位的十进制值。

我们已经评估了客户流失蓝图的两种配置:一种使用浮点值表示货币金额(如我们在 第一期 中所描述的那样),另一种使用十进制值表示货币金额(这是我们当前报告的性能数字所针对的配置)。由于其语义和健壮性,十进制算法比浮点算法成本更高,但只要所涉及的所有十进制类型都在 64 位以内,就可以通过 RAPIDS 加速器插件来加速。

配置 RAPIDS 加速器以启用更多操作

RAPIDS 加速器对于在 GPU 上执行 MIG ht 表现出较差性能或返回与基于 CPU 的加速器略有不同的结果的操作持保守态度。因此,一些可以加速的操作在默认情况下可能不会加速,许多实际应用程序需要使这些操作能够看到最佳性能。我们在 我们的第一期 中看到了这种现象的一个例子,其中我们必须通过将 true 设置为 true ,在 Spark 配置中显式启用浮点聚合操作。类似地,当我们将工作负载配置为使用十进制算法时,我们需要通过将 spark.rapids.sql.decimalType.enabled 设置为 true 来启用十进制加速。


插件文档 列出了配置支持或不支持的操作,以及在默认情况下启用或禁用某些操作的原因。除了浮点聚合和十进制支持之外,生产 Spark 工作负载极有可能受益于以下几类操作:

  • 铸造作业 ,特别是从字符串到日期或数字类型,或从浮点类型到十进制类型。
  • 某些 Unicode 字符不支持字符串大小写(例如“ SELECT UPPER(name) FROM EMPLOYEES ”),更改大小写也会更改字符宽度(以字节为单位),但许多应用程序不使用此类字符[或者通过将 Spark . RAPIDS . sql . compatibleops . enabled 设置为 true 来启用它们和其他几个。
  • 从 CSV 文件中读取特定类型;虽然插件( Spark . RAPIDS . sql . format . CSV . enabled )中当前默认启用了读取 CSV 文件,但读取某些类型的无效值(尤其是数字类型、日期和小数)在 GPU 和 CPU 上会有不同的行为,因此需要单独启用每个类型的读取。

加快从 CSV 文件接收数据

CSV 阅读需要额外的注意:它是昂贵的,加速它可以提高许多工作的性能。然而,由于在 RAPIDS 加速器下读取 CSV 的行为可能与在 cpu 上执行时的 Spark 行为不同,并且由于实际 CSV 文件质量的巨大动态范围,因此验证在 GPU 上读取 CSV 文件的结果尤为重要。一个快速但有价值的健全性检查是确保在 GPU 上读取 CSV 文件返回的空值数与在 CPU 上读取相同的文件返回的空值数相同。当然,如果可能的话,使用像 Parquet 或 ORC 这样的自文档结构化输入格式而不是 CSV 有很多好处。

避免查询优化的意外后果

RAPIDS 加速器将 物理查询计划 转换为将某些操作符委派给 GPU 。 但是,在 Spark 生成物理计划时,它已经对逻辑计划执行了几个转换,这可能涉及重新排序操作。 因此,开发人员或分析人员声明的接近查询或数据帧操作末尾的操作可能会从查询计划的叶移向根。

A diagram of a database query execution. The first step shows joining two input relations; the second step shows the output of joining these two relations; the third shows the result of filtering the join output, producing in relatively few records.
图 2 : 一种执行数据帧查询的描述,该查询连接两个数据帧,然后过滤结果。 如果谓词具有足够的选择性,则大多数输出元组将被丢弃。
A diagram of a database query execution. The first step shows filtering the first input relation; the second step shows filtering the second input relation; and the third shows joining the results of filtering the two input relations, resulting in relatively few records.
图 3 : 执行数据帧查询的描述,在连接结果之前过滤两个输入关系。 如果可以对每个输入关系独立地计算谓词,那么此查询执行将产生与图 2 中的查询执行相同的结果,效率将大大提高。

一般来说,这种转换可以提高性能。 例如,考虑一个查询,该查询连接两个数据帧,然后过滤结果: 如果可能的话,在执行连接之前执行过滤器通常会更有效。 这样做将减少连接的基数,消除最终不必要的比较,减少内存压力,甚至可能减少连接中需要考虑的数据帧分区的数量。 然而,这种优化可能会产生违反直觉的后果: 如果向查询计划的根移动的操作仅在 CPU 上受支持,或者如果它生成的值的类型在 GPU 上不受支持,则主动查询重新排序可能会对 GPU 的性能产生负面影响。 当这种情况发生时,在 CPU 上执行的查询计划的百分比可能比严格需要的要大。 您通常可以解决这个问题,并通过将查询划分为两个分别执行的部分来提高性能,从而强制在查询计划的叶子附近仅 CPU 的操作仅在原始查询的可加速部分在 GPU 上运行之后执行。

结论

在第三期文章中,我们详细介绍了如何充分利用 Apache Spark 和 Apache RAPIDS 加速器 Spark 。 大多数团队都会通过干净地使用 Spark 的数据帧抽象来实现最大的好处。 但是,一些应用程序可能会受益于细微的调整,特别是考虑 RAPIDS 加速器的执行模型并避免不受支持的操作的保留语义的代码更改。 未来几期文章将讨论数据科学发现工作流和机器学习生命周期的其余部分。

 

Tags