RAPIDS Accelerator for Apache Spark 与 Apache Spark 的兼容性#
SQL 插件尝试生成与 Apache Spark 完全相同的结果。在某些情况下,存在一些差异。在大多数情况下,产生不同结果的运算符默认是关闭的,您可以查看 configs 以获取有关如何启用它们的更多信息。在某些情况下,我们认为默认启用不兼容性是值得性能提升的。如果出现问题,所有这些运算符都可以通过配置禁用。另请查看当前的 bugs 列表,这些通常是我们尚未解决的不兼容性问题。
输出排序#
在某些运算符中,Spark 不保证输出的顺序。这些通常是聚合和连接等操作,它们可能使用哈希来在下游任务之间分配工作负载。在这些情况下,插件不保证它将产生与 Spark 相同的输出顺序。在诸如 order by
操作之类的显式排序的情况下,插件将产生与 Spark 保证兼容的排序。如果排序是模糊的,则可能不是 100% 相同的。
在 Spark 3.1.0 之前的版本中,-0.0
始终 < 0.0
,但在 3.1.0 及更高版本中,排序并非如此。对于所有插件版本,排序时 -0.0
== 0.0
。
Spark 的排序通常是 stable 排序。在分布式工作负载中,不能保证排序稳定性,因为上游数据到达任务的顺序是不保证的。排序稳定性仅在一种情况下得到保证,即使用单个任务/分区从文件中读取和排序数据。RAPIDS Accelerator 默认执行不稳定的 out of core 排序。这仅仅意味着排序算法允许在数据大于 GPU 内存容量时溢出部分数据,但不保证在键的排序不明确时行的排序。如果您在处理中依赖稳定排序,则可以通过将 spark.rapids.sql.stableSort.enabled
设置为 true
来请求此功能,RAPIDS 将尝试在 GPU 上一次性对给定任务/分区的所有数据进行排序。将来可能会更改为允许可溢出的稳定排序。
浮点数#
对于大多数基本的浮点运算(如加法、减法、乘法和除法),插件将生成与 Spark 完全相同的结果。对于其他函数(如 sin
、cos
等),输出可能不同,但在浮点计算中固有的舍入误差范围内。用于计算值的操作顺序可能在 CPU 使用的底层 JVM 实现和 GPU 使用的 C++ 标准库实现之间有所不同。
在 round
和 bround
的情况下,结果可能相差更大,因为它们可能会放大差异。这种情况发生在二进制浮点表示无法精确捕获十进制值的情况下。例如,1.025
无法精确表示,最终更接近 1.02499
。Spark 的 round 实现首先将其转换为具有复杂逻辑的十进制值,使其为 1.025
,然后进行舍入。这导致纯 Spark 下的 round(1.025, 2)
得到值 1.03,但在 RAPIDS 加速器下,它产生 1.02
。作为旁注,Python 将产生 1.02
,Java 没有内置执行此类舍入的能力,但如果您执行 Math.round(1.025 * 100.0)/100.0
的简单操作,您也会得到 1.02
。
对于 degrees
函数,Spark 的实现依赖于 Java JDK 的内置函数 Math.toDegrees。在 Java 8 中,它是 angrad * 180.0 / PI
,而在 Java 9+ 中,它是 angrad * (180d / PI)
。因此,在考虑溢出时,它们的结果会因 JDK 运行时版本而异。RAPIDS Accelerator 遵循 Java 9+ 的行为。因此,在 JDK 8 或更低版本中,GPU 上的 degrees
不会在某些非常大的数字上溢出,而 CPU 版本会溢出。
对于聚合,底层实现正在并行执行聚合,并且由于计算本身中的竞争条件,每次运行查询时结果可能不相同。这是插件加速计算方式的固有特性,无法“修复”。如果查询连接浮点值(无论如何都不明智),并且该值是浮点聚合的结果,则连接可能无法与插件正常工作,但可以使用纯 Spark 工作。从 22.06 开始,此行为默认启用,但可以使用配置 spark.rapids.sql.variableFloatAgg.enabled
禁用。
0.0
与 -0.0
浮点数允许将零编码为 0.0
和 -0.0
,但 IEEE 标准规定应将它们解释为相同。大多数数据库将这些值规范化为始终为 0.0
。Spark 在某些情况下会这样做,但并非所有情况都如此,如 此处 所述。此插件的底层实现基本上将它们视为所有处理的相同值。这可能会导致与 Spark 在 Spark 3.1.0 之前的版本中进行排序和去重计数等操作时存在一些差异。即使在 Spark 3.1.0 之后,连接和比较 仍然存在差异。
我们不禁用由于数据中存在 -0.0
而产生不同结果的操作,因为这被认为是罕见的情况。
Unicode#
Spark 将 Unicode 操作委托给底层 JVM。每个 Java 版本都符合特定版本的 Unicode 标准。SQL 插件不使用 JVM 进行 Unicode 支持,并且与 Unicode 12.1 版兼容。因此,在某些极端情况下,Spark 将产生与插件不同的结果。
CSV 读取#
Spark 允许使用默认关闭的各种选项来去除前导和尾随空格。插件将去除除字符串之外的所有值的前导和尾随空格。
特定类型也存在差异/问题,详情如下。
CSV 字符串#
通常,除非您可以确保您的数据中没有任何行分隔符,否则在 Spark 中将字符串写入 CSV 文件可能会有问题。GPU 加速的 CSV 解析器处理带引号的行分隔符,类似于 multiLine
模式。但仍然存在许多与之相关的问题,应避免使用它们。
转义引号字符 '\"'
未得到良好支持,如 issue 所述。
CSV 日期#
解析日期时,仅支持有限的格式集。
"yyyy-MM-dd"
"yyyy/MM/dd"
"yyyy-MM"
"yyyy/MM"
"MM-yyyy"
"MM/yyyy"
"MM-dd-yyyy"
"MM/dd/yyyy"
"dd-MM-yyyy"
"dd/MM/yyyy"
CSV 时间戳#
CSV 解析器不支持时区。它将忽略任何尾随时区信息,尽管格式要求 XXX
或 [XXX]
。因此,它默认是关闭的,您可以通过将 spark.rapids.sql.csvTimestamps.enabled
设置为 true
来启用它。
时间戳支持的格式与日期类似受到限制。格式的第一部分必须是支持的日期格式。第二部分必须以 'T'
开头以分隔时间部分,后跟以下格式之一
HH:mm:ss.SSSXXX
HH:mm:ss[.SSS][XXX]
HH:mm:ss[.SSSXXX]
HH:mm
HH:mm:ss
HH:mm[:ss]
HH:mm:ss.SSS
HH:mm:ss[.SSS]
与日期一样,实际上同时支持所有时间戳格式。如果插件看到它不支持的格式,它将自行禁用。
Spark 中的无效时间戳(格式正确但数字产生无效日期或时间的那些)默认情况下可能会导致异常,并且它们的解析方式可以通过配置来控制。RAPIDS Accelerator 不支持任何这些,并且会产生不正确的日期。通常,是溢出的日期。
CSV 浮点数#
解析浮点值与 从字符串到浮点数的转换 具有相同的限制。
此外,某些值的解析不会产生与 CPU 完全相同的结果。它们都在舍入误差范围内,除非它们足够接近溢出到 Inf 或 -Inf,这会导致返回一个数字,而 CPU 会返回 null。
CSV ANSI 日时间间隔#
此类型作为 Spark 3.3.0 的一部分添加,并且在 Spark 3.3.0 之前的版本中不受支持。Apache Spark 在读取 ANSI 日时间间隔值时可能会溢出。RAPIDS Accelerator 不会溢出,因此在这种情况下与 Spark 不完全兼容。
csv 中的间隔字符串 |
Spark 读取为 |
RAPIDS Accelerator 读取为 |
评论 |
---|---|---|---|
interval ‘106751992’ day |
INTERVAL ‘-106751990’ DAY |
NULL |
Spark 问题 |
interval ‘2562047789’ hour |
INTERVAL ‘-2562047787’ HOUR |
NULL |
Spark 问题 |
CSV 中有两种有效的文本表示形式:ANSI 样式和 HIVE 样式,例如
SQL 类型 |
ANSI 样式的实例 |
HIVE 样式的实例 |
---|---|---|
INTERVAL DAY |
INTERVAL ‘100’ DAY TO SECOND |
100 |
INTERVAL DAY TO HOUR |
INTERVAL ‘100 10’ DAY TO HOUR |
100 10 |
INTERVAL DAY TO MINUTE |
INTERVAL ‘100 10:30’ DAY TO MINUTE |
100 10:30 |
INTERVAL DAY TO SECOND |
INTERVAL ‘100 10:30:40.999999’ DAY TO SECOND |
100 10:30:40.999999 |
INTERVAL HOUR |
INTERVAL ‘10’ HOUR |
10 |
INTERVAL HOUR TO MINUTE |
INTERVAL ‘10:30’ HOUR TO MINUTE |
10:30 |
INTERVAL HOUR TO SECOND |
INTERVAL ‘10:30:40.999999’ HOUR TO SECOND |
10:30:40.999999 |
INTERVAL MINUTE |
INTERVAL ‘30’ MINUTE |
30 |
INTERVAL MINUTE TO SECOND |
INTERVAL ‘30:40.999999’ MINUTE TO SECOND |
30:40.999999 |
INTERVAL SECOND |
INTERVAL ‘40.999999’ SECOND |
40.999999 |
目前,RAPIDS Accelerator 仅支持 ANSI 样式。
Hive 文本文件#
Hive 文本文件与 CSV 非常相似,但不完全相同。
Hive 文本文件浮点数#
解析浮点值与 从字符串到浮点数的转换 具有相同的限制。
此外,某些值的解析不会产生与 CPU 完全相同的结果。它们都在舍入误差范围内,除非它们足够接近溢出到 Inf 或 -Inf,这会导致返回一个数字,而 CPU 会返回 null。
Hive 文本文件十进制数#
Hive 在可以解析的十进制值方面有一些限制。我们用于解析十进制值的 GPU 内核没有相同的限制。这意味着有时 CPU 版本会为输入值返回 null,但 GPU 版本会返回值。这种情况通常发生在具有较大负指数的数字中,其中 GPU 将返回 0
,而 Hive 将返回 null
。请参阅 NVIDIA/spark-rapids#7246
ORC#
ORC 格式对读取和写入都具有相当完整的支持。只有少数已知问题。第一个问题是读取儒略历和格里高利历转换前后的时间戳和日期,如 此处 所述。写入日期也存在类似的问题,如 此处 所述。但是,写入时间戳似乎仅适用于 epoch 之后的日期,如 此处 所述
插件支持读取 uncompressed
、snappy
、zlib
和 zstd
ORC 文件,以及写入 uncompressed
和 snappy
ORC 文件。此时,插件无法在读取不受支持的压缩格式时回退到 CPU,并且在这种情况下会出错。
ORC 的下推聚合#
当用户配置 spark.sql.orc.aggregatePushdown
设置为 true 时,Spark-3.3.0+ 会将某些聚合 (MIN
/MAX
/COUNT
) 下推到 ORC 中。
通过启用此功能,聚合查询性能将得到提高,因为它利用了统计信息。
注意
Spark ORC 读取器/写入器假定所有 ORC 文件都必须具有有效的列统计信息。此假设偏离了 ORC 规范,该规范声明统计信息是可选的。
当 Spark-3.3.0+ 作业读取具有空文件统计信息的 ORC 文件时,它会失败并抛出以下运行时异常
org.apache.spark.SparkException: Cannot read columns statistics in file: /PATH_TO_ORC_FILE
E Caused by: java.util.NoSuchElementException
E at java.util.LinkedList.removeFirst(LinkedList.java:270)
E at java.util.LinkedList.remove(LinkedList.java:685)
E at org.apache.spark.sql.execution.datasources.orc.OrcFooterReader.convertStatistics(OrcFooterReader.java:54)
E at org.apache.spark.sql.execution.datasources.orc.OrcFooterReader.readStatistics(OrcFooterReader.java:45)
E at org.apache.spark.sql.execution.datasources.orc.OrcUtils$.createAggInternalRowFromFooter(OrcUtils.scala:428)
Spark 社区计划致力于在 ORC 文件统计信息丢失时运行时回退以从实际行读取(请参阅 SPARK-34960 讨论)。
RAPIDS 的限制#
在 22.06 之前的版本中,RAPIDS 不支持 ORC 文件中的完整文件统计信息。
写入 ORC 文件
如果您使用的是 22.06 之前的版本,其中 CUDF 不支持写入文件统计信息,则 GPU 写入的 ORC 文件与导致 ORC 读取作业失败的优化不兼容,如上所述。为了防止 22.06 之前版本中的作业失败,在读取 GPU 写入的 ORC 文件时,应禁用 spark.sql.orc.aggregatePushdown
。
读取 ORC 文件
为了利用聚合优化,插件回退到 CPU,因为它只是元数据查询。只要 ORC 文件具有有效的统计信息(由 CPU 写入),将聚合下推到 ORC 层应该是成功的。否则,读取 GPU 写入的 ORC 文件需要禁用 aggregatePushdown
。
Parquet#
Parquet 格式具有更多配置,因为存在多个版本,它们之间存在一些兼容性问题。日期和时间戳是已知问题的存在之处。对于读取,当 spark.sql.legacy.parquet.datetimeRebaseModeInWrite
设置为 CORRECTED
时,儒略历和格里高利历转换之前的 时间戳 是错误的,但日期是正常的。当 spark.sql.legacy.parquet.datetimeRebaseModeInWrite
设置为 LEGACY
时,对于儒略历和格里高利历转换之前的值,即:日期 <= 1582-10-04,读取可能会失败。
写入时,spark.sql.legacy.parquet.datetimeRebaseModeInWrite
当前被忽略,如 此处 所述
当 spark.sql.parquet.outputTimestampType
设置为 INT96
时,如果任何值在 1677 年 9 月 21 日 12:12:43 AM 之前或在 2262 年 4 月 11 日 11:47:17 PM 之后,时间戳将溢出并导致抛出 IllegalArgumentException
。要解决此问题,请通过将 spark.rapids.sql.format.parquet.writer.int96.enabled
设置为 false 或将 spark.sql.parquet.outputTimestampType
设置为 TIMESTAMP_MICROS
或 TIMESTAMP_MILLIS
来完全绕过此问题,从而关闭时间戳列的 ParquetWriter 加速。
插件支持读取 uncompressed
、snappy
、gzip
和 zstd
Parquet 文件,以及写入 uncompressed
和 snappy
Parquet 文件。此时,插件无法在读取不受支持的压缩格式时回退到 CPU,并且在这种情况下会出错。
JSON#
JSON 格式读取是一项非常实验性的功能,预计会存在一些问题,因此我们默认禁用它。如果您想对其进行测试,则需要启用 spark.rapids.sql.format.json.enabled
和 spark.rapids.sql.format.json.read.enabled
。
目前,GPU 加速的 JSON 读取器不支持列修剪,这可能会使其难以使用甚至测试。用户必须指定完整架构,或者只让 Spark 从 JSON 文件中推断架构。例如,
我们有一个包含以下内容的 people.json
文件
{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}
以下两种方式都有效
推断架构
val df = spark.read.json("people.json")
指定完整架构
val schema = StructType(Seq(StructField("name", StringType), StructField("age", IntegerType))) val df = spark.read.schema(schema).json("people.json")
虽然以下代码在当前版本中不起作用,
val schema = StructType(Seq(StructField("name", StringType)))
val df = spark.read.schema(schema).json("people.json")
JSON 支持的类型#
在当前版本中,嵌套类型(数组、结构和映射类型)在常规 JSON 解析中尚不受支持。
from_json
函数
此特定函数支持输出具有有限功能的映射类型。特别是,输出映射不是来自常规 JSON 解析,而是仅包含直接从输入 JSON 字符串中提取的键值对的纯文本。
由于这些限制,输入 JSON 架构必须是 MAP<STRING,STRING>
,仅此而已。此外,没有验证、没有错误容忍、没有数据转换以及执行字符串格式化。如果与 Spark CPU 的 from_json
结果相比,这可能会导致输出中的一些细微差异,例如
输入 JSON 字符串中的浮点数(例如
1.2000
)不会重新格式化为1.2
。相反,输出将与输入相同。如果输入 JSON 以多行形式给出,则任何包含无效 JSON 格式的行都将导致应用程序崩溃。另一方面,Spark CPU 版本只会为无效行生成 null,如下所示
scala> val df = Seq("{}", "BAD", "{\"A\": 100}").toDF df: org.apache.spark.sql.DataFrame = [value: string] scala> df.selectExpr("from_json(value, 'MAP<STRING,STRING>')").show() +----------+ | entries| +----------+ | {}| | null| |{A -> 100}| +----------+
JSON 浮点数#
解析浮点值与 从字符串到浮点数的转换 具有相同的限制。
在 Spark 3.3.0 之前,当指定数据类型为 FloatType
或 DoubleType
时,读取 JSON 字符串(例如 "+Infinity"
)会导致即使 allowNonNumericNumbers
设置为 false,这些值也会被解析。此外,Spark 3.3.0 之前的版本仅支持无穷大的 "Infinity"
和 "-Infinity"
表示形式,并且不支持 "+INF"
、"-INF"
或 "+Infinity"
,Spark 在未加引号时认为这些值有效。GPU JSON 读取器与 Spark 3.3.0 及更高版本中的行为一致。
GPU JSON 读取器的另一个限制是,它将解析包含非字符串布尔值或数值的字符串,而 Spark 会将它们视为无效输入,并且只会返回 null
。
JSON 时间戳#
目前不支持将数值读取为时间戳,而是返回 null 值 (#4940)。一种解决方法是读取为 longs,然后转换为时间戳。
JSON 架构发现#
如果未显式提供架构,Spark SQL 可以自动推断 JSON 数据集的架构。CPU 处理架构发现,并且没有 GPU 加速。默认情况下,Spark 将读取/解析整个数据集以确定架构。这意味着 GPU 忽略的某些选项/错误在使用架构发现时仍可能导致异常。
JSON 选项#
Spark 支持在读取数据集时将选项传递给 JSON 解析器。在大多数情况下,如果 RAPIDS Accelerator 看到它不支持的其中一个选项,它将回退到 CPU。在某些情况下,我们不会。以下选项在下面记录:
allowNumericLeadingZeros
- 允许数字中的前导零(例如 00012)。默认情况下,此项设置为 false。当它为 false 时,如果 Spark 遇到这种类型的数字,则会抛出异常。RAPIDS Accelerator 从所有数字中删除前导零,并且此配置对其没有影响。allowUnquotedControlChars
- 允许 JSON 字符串包含未加引号的控制字符(ASCII 值小于 32 的字符,包括制表符和换行符)或不允许。默认情况下,此项设置为 false。如果在读取 JSON 文件时提供了架构,则此标志对 RAPIDS Accelerator 没有影响,因为它始终允许未加引号的控制字符,但 Spark 会将这些条目错误地读取为 null。但是,如果未提供架构且该选项为 false,则 RAPIDS Accelerator 的行为与 Spark 相同,其中会抛出异常,如JSON Schema discovery
部分所述。allowNonNumericNumbers
- 允许解析NaN
和Infinity
值(请注意,这些在 JSON 规范 中不是有效的数值)。Spark 3.3.0 之前的版本具有不一致的行为,即使禁用此选项,也会解析NaN
和Infinity
的某些变体 (SPARK-38060)。RAPIDS Accelerator 行为与 Spark 3.3.0 及更高版本一致。
Avro#
Avro 格式读取是一项非常实验性的功能,预计会存在一些问题,因此我们默认禁用它。如果您想对其进行测试,则需要启用 spark.rapids.sql.format.avro.enabled
和 spark.rapids.sql.format.avro.read.enabled
。
目前,GPU 加速的 Avro 读取器不支持读取 Avro 1.2 版文件。
支持的类型#
当前版本支持 boolean、byte、short、int、long、float、double、string。
正则表达式#
GPU 上支持以下 Apache Spark 正则表达式函数和表达式
RLIKE
regexp
regexp_extract
regexp_extract_all
regexp_like
regexp_replace
string_split
str_to_map
当当前区域设置使用 UTF-8 字符集时,默认启用 GPU 上的正则表达式评估。对于 GPU 上尚不支持的正则表达式以及区域设置未使用 UTF-8 的环境,执行将回退到 CPU。但是,在某些极端情况下,仍然会在 GPU 上执行并产生与 CPU 不同的结果。要禁用 GPU 上的正则表达式,请设置 spark.rapids.sql.regexp.enabled=false
。
以下是在 GPU 上运行会产生与 CPU 不同结果的已知极端情况
以下正则表达式模式在 GPU 上尚不支持,并将回退到 CPU。
行锚点
^
在某些情况下不受支持,例如与选择 (^|a
) 结合使用时。行锚点
$
在某些罕见情况下不受支持。字符串锚点
\Z
不受regexp_replace
支持,并且在某些罕见情况下也不受支持。字符串锚点
\z
不受支持包含紧邻换行符或产生零个或多个结果的重复项的行尾或字符串锚点的模式
行锚点
$
和字符串锚点\Z
在包含\W
或\D
的模式中不受支持行锚点和字符串锚点不受
string_split
和str_to_map
支持惰性量词,例如
a*?
占有量词,例如
a*+
使用并集、交集或减法语义的字符类,例如
[a-d[m-p]]
、[a-z&&[def]]
或[a-z&&[^bc]]
空组:
()
regexp_replace
不支持反向引用
正在进行工作以增加可以在 GPU 上运行的正则表达式的范围。
时间戳#
Spark 在内部相对于 JVM 时区存储时间戳。目前 GPU 上不支持在时区之间转换任意时间戳。因此,仅当 JVM 使用的时区为 UTC 时,涉及时间戳的操作才会进行 GPU 加速。
窗口化#
窗口函数#
由于 CPU 和 GPU 窗口函数之间的排序差异,特别是基于行的窗口函数(如 row_number
、lead
和 lag
),如果排序同时包含 -0.0
和 0.0
,或者如果排序不明确,则可能会产生不同的结果。如果窗口函数上的排序不明确,Spark 也可能会在一次运行到另一次运行之间产生不同的结果。
范围窗口#
当基于范围的窗口的 order-by 列是数字类型(如 byte/short/int/long
)并且为某个值计算的范围边界发生溢出时,CPU 和 GPU 将获得不同的结果。
例如,考虑以下数据集
1+------+---------+
2| id | dollars |
3+------+---------+
4| 1 | NULL |
5| 1 | 13 |
6| 1 | 14 |
7| 1 | 15 |
8| 1 | 15 |
9| 1 | 17 |
10| 1 | 18 |
11| 1 | 52 |
12| 1 | 53 |
13| 1 | 61 |
14| 1 | 65 |
15| 1 | 72 |
16| 1 | 73 |
17| 1 | 75 |
18| 1 | 78 |
19| 1 | 84 |
20| 1 | 85 |
21| 1 | 86 |
22| 1 | 92 |
23| 1 | 98 |
24+------+---------+
执行 SQL 语句后
1SELECT
2COUNT(dollars) over
3 (PARTITION BY id
4 ORDER BY CAST (dollars AS Byte) ASC
5 RANGE BETWEEN 127 PRECEDING AND 127 FOLLOWING)
6FROM table
由于溢出处理,CPU 和 GPU 之间的结果将有所不同。
CPU: WrappedArray([0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0], [0]) GPU: WrappedArray([0], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19], [19] [19])
要在 GPU 上启用字节范围窗口化,请将 spark.rapids.sql.window.range.byte.enabled
设置为 true
。
我们还为其他整数范围类型提供配置
spark.rapids.sql.window.range.short.enabled
spark.rapids.sql.window.range.int.enabled
spark.rapids.sql.window.range.long.enabled
我们将 byte/short 的配置默认设置为 false,将 int/long 的配置默认设置为 true 的原因是,我们认为最真实的查询是基于 int 或 long 的。
将字符串解析为日期或时间戳#
当使用 to_date
和 unix_timestamp
等函数将字符串转换为日期或时间戳时,指定的格式字符串将分为以下三类之一
GPU 上支持且与 Spark 100% 兼容
GPU 上支持,但可能产生与 Spark 不同的结果
GPU 上不支持
GPU 上支持的格式因 timeParserPolicy
的设置而异。
CORRECTED 和 EXCEPTION timeParserPolicy#
将 timeParserPolicy 设置为 CORRECTED
或 EXCEPTION
(默认值)时,GPU 上支持以下格式,无需任何其他设置。
yyyy-MM-dd
yyyy/MM/dd
yyyy-MM
yyyy/MM
dd/MM/yyyy
yyyy-MM-dd HH:mm:ss
MM-dd
MM/dd
dd-MM
dd/MM
MM/yyyy
MM-yyyy
MM/dd/yyyy
MM-dd-yyyy
MMyyyy
列表中未出现的有效 Spark 日期/时间格式也可能受支持,但尚未经过广泛测试,并且可能产生与 CPU 相比不同的结果。已知问题包括
在 GPU 上,后跟尾随字符(包括空格)的有效日期和时间戳可能会解析为非空值,而 Spark 会将数据视为无效并返回 null
要尝试在 GPU 上使用其他格式,请将
spark.rapids.sql.incompatibleDateFormats.enabled
设置为true
。
包含以下任何字符的格式不受支持,并将回退到 CPU
'k', 'K','z', 'V', 'c', 'F', 'W', 'Q', 'q', 'G', 'A', 'n', 'N',
'O', 'X', 'p', '\'', '[', ']', '#', '{', '}', 'Z', 'w', 'e', 'E', 'x', 'Z', 'Y'
包含以下任何单词的格式不受支持,并将回退到 CPU
"u", "uu", "uuu", "uuuu", "uuuuu", "uuuuuu", "uuuuuuu", "uuuuuuuu", "uuuuuuuuu", "uuuuuuuuuu",
"y", "yy", yyy", "yyyyy", "yyyyyy", "yyyyyyy", "yyyyyyyy", "yyyyyyyyy", "yyyyyyyyyy",
"D", "DD", "DDD", "s", "m", "H", "h", "M", "MMM", "MMMM", "MMMMM", "L", "LLL", "LLLL", "LLLLL",
"d", "S", "SS", "SSS", "SSSS", "SSSSS", "SSSSSSSSS", "SSSSSSS", "SSSSSSSS"
LEGACY timeParserPolicy#
将 timeParserPolicy 设置为 LEGACY
,并将 spark.rapids.sql.incompatibleDateFormats.enabled
设置为 true
,并将 spark.sql.ansi.enabled
设置为 false
时,支持以下格式,但不保证产生与 CPU 相同的结果
dd-MM-yyyy
dd/MM/yyyy
yyyy/MM/dd
yyyy-MM-dd
yyyy/MM/dd HH:mm:ss
yyyy-MM-dd HH:mm:ss
在 GPU 上运行时,LEGACY timeParserPolicy 支持具有以下限制
仅支持 4 位年份
使用公历而不是 Spark 在旧版模式下使用的混合儒略历 + 格里高利历
将日期和时间戳格式化为字符串#
当使用 from_unixtime 等函数将日期和时间戳格式化为字符串时,GPU 上仅支持有效格式字符串的子集。
包含以下任何字符的格式不受支持,并将回退到 CPU
'k', 'K','z', 'V', 'c', 'F', 'W', 'Q', 'q', 'G', 'A', 'n', 'N',
'O', 'X', 'p', '\'', '[', ']', '#', '{', '}', 'Z', 'w', 'e', 'E', 'x', 'Z', 'Y'
包含以下任何单词的格式不受支持,并将回退到 CPU
"u", "uu", "uuu", "uuuu", "uuuuu", "uuuuuu", "uuuuuuu", "uuuuuuuu", "uuuuuuuuu", "uuuuuuuuuu",
"y", yyy", "yyyyy", "yyyyyy", "yyyyyyy", "yyyyyyyy", "yyyyyyyyy", "yyyyyyyyyy",
"D", "DD", "DDD", "s", "m", "H", "h", "M", "MMM", "MMMM", "MMMMM", "L", "LLL", "LLLL", "LLLLL",
"d", "S", "SS", "SSSS", "SSSSS", "SSSSSSSSS", "SSSSSSS", "SSSSSSSS"
请注意,此列表与上一节中给出的用于将字符串解析为日期的列表略有不同,因为两位数年份格式 "yy"
在将日期格式化为字符串时受支持,但在将字符串解析为日期时不受支持。
类型之间的转换#
通常,在 GPU 上执行 cast
和 ansi_cast
操作与在 CPU 上执行相同的操作兼容。但是,也存在一些例外情况。因此,默认情况下在 GPU 上禁用某些类型转换,并且需要指定配置选项才能启用它们。
浮点数到十进制数#
- GPU 将使用与 Java 的 BigDecimal 不同的策略来
处理/存储十进制值,这会导致限制
转换后,浮点值不能大于
1e18
或小于 ``-1e18``。GPU 产生的结果与 Spark 的默认结果略有不同。
从 22.06 开始,此配置已启用,要在使用 Spark 3.1.0 或更高版本时在 GPU 上禁用此操作,请将 spark.rapids.sql.castFloatToDecimal.enabled
设置为 false
浮点数到整数类型#
对于 cast
和 ansi_cast
,Spark 使用表达式 Math.floor(x) <= MAX && Math.ceil(x) >= MIN
来确定浮点值是否可以转换为整数类型。在 Spark 3.1.0 之前,MIN 和 MAX 值是浮点值,例如 Int.MaxValue.toFloat
,但从 3.1.0 开始,这些值现在是整数类型,例如 Int.MaxValue
,因此这稍微影响了值的有效范围,并且现在在某些情况下与 GPU 上的行为略有不同。
从 22.06 开始,此配置已启用,要在使用 Spark 3.1.0 或更高版本时在 GPU 上禁用此操作,请将 spark.rapids.sql.castFloatToIntegralTypes.enabled
设置为 false
。
当使用 3.1.0 之前的 Spark 版本时,将忽略此配置设置。
浮点数到字符串#
当将浮点数据类型转换为字符串时,GPU 将使用与 Java 的 toString 方法不同的精度。GPU 对指数使用小写 e
前缀,而 Spark 使用大写 E
。因此,计算出的字符串可能与 Spark 中的默认行为不同。
从 22.06 开始,默认情况下启用此配置,要在 GPU 上禁用此操作,请将 spark.rapids.sql.castFloatToString.enable``d to ``false
设置为 false。
字符串到浮点数#
当字符串表示以下范围内的任何数字时,在 GPU 上从字符串转换为浮点类型会返回不正确的结果。在这两种情况下,GPU 都返回 Double.MaxValue。Apache Spark 中的默认行为是分别返回 +Infinity
和 -Infinity
。
1.7976931348623158E308 <= x < 1.7976931348623159E308
1.7976931348623159E308 < x <= -1.7976931348623158E308
此外,GPU 不支持从包含十六进制值的字符串进行转换。
从 22.06 开始,默认情况下启用此配置,要在 GPU 上启用此操作,请将 spark.rapids.sql.castStringToFloat.enabled
设置为 false
。
字符串到日期#
GPU 上支持以下格式/模式。假定为 UTC 时区。
格式或模式 |
GPU 上支持? |
---|---|
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
字符串到时间戳#
要允许在 GPU 上从字符串转换为时间戳,请启用配置属性 spark.rapids.sql.castStringToTimestamp.enabled
。
当前从字符串到时间戳的转换具有以下限制。
格式或模式 |
GPU 上支持? |
---|---|
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
|
部分 [1] |
|
部分 [1] |
|
部分 [1] |
|
部分 [1] |
|
部分 [1] |
|
部分 [1] |
|
是 |
|
是 |
|
是 |
|
是 |
|
是 |
[1] 不支持闰秒。如果提供了 zone_id,则仅支持时区“Z”(UTC)。转换不受支持的格式将导致 null 值。
Spark 在从字符串转换为时间戳时非常宽松,因为所有日期和时间组件都是可选的,这意味着输入值(如 T
、T2
、:
、::
、1:
、:1
和 ::1
)被视为有效时间戳。GPU 会将这些值视为无效,并将它们转换为 null 值。
常量折叠#
ConstantFolding 是 Catalyst 中的运算符优化规则,它将可以静态评估的表达式替换为其等效的字面量值。RAPIDS Accelerator 依赖于常量折叠,如果 org.apache.spark.sql.catalyst.optimizer.ConstantFolding
被排除为规则,则查询的某些部分将不会加速。
long/double 到时间戳#
Spark 330+ 在将足够大的 long/double 转换为时间戳时存在问题,请参阅 https://issues.apache.org/jira/browse/SPARK-39209。当将足够大的 long/double 转换为时间戳时,Spark 330+ 会抛出错误,而 RAPIDS Accelerator 可以正确处理。
JSON 字符串处理#
0.5 版本引入了 get_json_object
操作。JSON 规范仅允许 JSON 数据中字符串周围使用双引号,而 Spark 允许 JSON 数据中字符串周围使用单引号。当尝试匹配用单引号括起来的字符串时,GPU 上的 RAPIDS Spark get_json_object
操作将在 PySpark 中返回 None
,在 Scala 中返回 Null
。此行为将在未来版本中更新,以更紧密地匹配 Spark。
- 如果 JSON 路径中有单引号
'
,则 GPU 查询 可能会因
ai.rapids.cudf.CudfException
而失败。更多示例请参见 issue-12483
近似百分位数#
approximate_percentile
的 GPU 实现使用 t-Digests,它具有高精度,尤其是在分布的尾部附近。结果与 Apache Spark 的 approximate_percentile
实现并非逐位相同。默认情况下启用此功能,可以通过设置 spark.rapids.sql.expression.ApproximatePercentile=false
来禁用它。
条件和具有副作用的操作(ANSI 模式)#
在 Apache Spark 中,条件操作(如 if
、coalesce
和 case/when
)会在逐行基础上延迟评估其参数。在 GPU 上,通常更有效的方法是评估参数,而不管条件如何,然后根据条件选择要返回的结果。只要评估参数不会引起副作用,这就可以。对于 Spark 中的大多数表达式来说,这都是正确的,但在 ANSI 模式下,许多表达式都可能抛出异常,例如,如果发生溢出,则对于 Add
表达式就是如此。对于 UDF 也是如此,因为从本质上讲,它们是用户定义的,并且可能具有副作用,例如抛出异常。
当前,RAPIDS Accelerator 假定没有副作用。这可能会导致某些情况,特别是在 ANSI 模式下,RAPIDS Accelerator 将始终抛出异常,但 CPU 上的 Spark 不会。例如
1spark.conf.set("spark.sql.ansi.enabled", "true")
2
3Seq(0L, Long.MaxValue).toDF("val")
4 .repartition(1) // The repartition makes Spark not optimize selectExpr away
5 .selectExpr("IF(val > 1000, null, val + 1) as ret")
6 .show()
如果在 CPU 上运行以上示例,您将获得如下结果。
1+----+
2| ret|
3+----+
4| 1|
5|null|
6+----+
但是,如果在 GPU 上运行,则会抛出溢出异常。如前所述,这是因为 RAPIDS Accelerator 将评估 val + 1
和 null
,而不管条件的结果如何。在某些情况下,您可以解决此问题。上面的示例可以重写,以便在 Add
操作之前发生 if
。
1Seq(0L, Long.MaxValue).toDF("val")
2 .repartition(1) // The repartition makes Spark not optimize selectExpr away
3 .selectExpr("IF(val > 1000, null, val) + 1 as ret")
4 .show()
但这并不是可以通用完成的事情,并且需要了解可能触发副作用的内部知识。