Describe the bug
When an orc file is written with a column of timestamps, it seems to return incorrect values when read back using the orc reader in cudf. It also fails when the file is read by the cpu orc reader.
Steps/Code to reproduce bug
Below is the test case I wrote using the cudf java api and spark api. Note the difference in input and output. If there are bits missing in how the timestamp was passed to the orc writer, I appreciate any guidance on that. I am also attaching the orc file generated by this test(rename to *.orc)
cudf-low-level-write-1752173860964.orc.txt
Output from GPU and input is :
+--------------------------+
|LONG |
+--------------------------+
|1780-02-20 03:02:11.654891|
|2017-11-22 01:48:37.52192 |
+--------------------------+
.............................
dataStrings: Seq[String] = List(2364-09-10 02:36:45.364442, 3187-01-01 00:57:44.941024)
import ai.rapids.cudf.{ColumnVector, DType, HostColumnVector, Table, ORCWriterOptions, TableWriter, CompressionType, ColumnWriterOptions}
import org.apache.spark.sql.{SparkSession, Row, SaveMode}
import org.apache.spark.sql.types.{StructType, StructField, StringType, TimestampType}
import org.apache.spark.sql.functions.col
import java.io.File
import java.time.{LocalDateTime, ZoneOffset}
import java.time.format.DateTimeFormatter
// This script combines low-level and high-level APIs to debug a timestamp write issue.
// 1. Low-Level: Dumps raw nanosecond values and writes an ORC file using a structured options builder.
// 2. High-Level: Reproduces the failing CPU read on the file created in Part 1.
object WriterOptionsHelper {
// Define an enumeration to replace the Java enum
object Columns extends Enumeration {
type Columns = Value
val BOOL, INT, LONG, FLOAT, DOUBLE, BYTE, STRING, DECIMAL64, DECIMAL128,
STRUCT, LIST, LIST_STRUCT, STRUCT_DEC128, LIST_DEC128 = Value
}
def structBuilder(name: String): ColumnWriterOptions.StructBuilder = ColumnWriterOptions.structBuilder(name)
def listBuilder(name: String): ColumnWriterOptions.ListBuilder = ColumnWriterOptions.listBuilder(name)
// FIX: Added wildcard type parameters `[_, _]` to NestedBuilder because it is a generic class.
def buildWriterOptions(builder: ColumnWriterOptions.NestedBuilder[_, _], colName: String): Unit = {
Columns.withName(colName) match {
case Columns.BOOL | Columns.INT | Columns.LONG | Columns.FLOAT |
Columns.DOUBLE | Columns.BYTE | Columns.STRING =>
builder.withColumns(false, colName)
case Columns.DECIMAL64 =>
builder.withDecimalColumn(colName, DType.DECIMAL64_MAX_PRECISION)
case Columns.DECIMAL128 =>
builder.withDecimalColumn(colName, DType.DECIMAL128_MAX_PRECISION)
case Columns.STRUCT =>
builder.withStructColumn(structBuilder(colName)
.withNullableColumns("ch_int")
.withNullableColumns("ch_str")
.withDecimalColumn("ch_dec64", DType.DECIMAL64_MAX_PRECISION, true)
.build())
case Columns.LIST =>
builder.withListColumn(listBuilder(colName)
.withNonNullableColumns("ch_int")
.build())
case Columns.LIST_STRUCT =>
builder.withListColumn(listBuilder(colName)
.withStructColumn(structBuilder(colName)
.withNullableColumns("ch_int")
.withNullableColumns("ch_str")
.withDecimalColumn("ch_dec64", DType.DECIMAL64_MAX_PRECISION, true)
.build())
.build())
case Columns.STRUCT_DEC128 =>
builder.withStructColumn(structBuilder(colName)
.withDecimalColumn("ch_dec128", DType.DECIMAL128_MAX_PRECISION, true)
.build())
case Columns.LIST_DEC128 =>
builder.withListColumn(listBuilder(colName)
.withStructColumn(structBuilder(colName)
.withDecimalColumn("ch_dec128", DType.DECIMAL128_MAX_PRECISION, true)
.build())
.build())
case _ =>
throw new IllegalArgumentException("should NOT reach here")
}
}
}
println("--- Part 1: Low-Level Data Inspection and ORC Write (CUDF JNI) ---")
val dataStrings = Seq(
// "1107-05-10 12:00:23.316538",
// "9999-12-30 23:59:59.999999",
// "1301-03-04 18:03:01.352251",
"2364-09-10 02:36:45.364442",
"3187-01-01 00:57:44.941024"
)
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
val tempFile = new File(System.getProperty("java.io.tmpdir"), "cudf-low-level-write-" + System.currentTimeMillis() + ".orc")
// tempFile.deleteOnExit()
val orcPath = tempFile.getAbsolutePath
var initialHostVector: HostColumnVector = null
var deviceVector: ColumnVector = null
var roundtripHostVector: HostColumnVector = null
var table: Table = null
var writer: TableWriter = null
try {
// Step 1a: Create the vector on the host (CPU).
println("\nStep 1a: Creating initial HostColumnVector (CPU)...")
val builder = HostColumnVector.builder(DType.TIMESTAMP_NANOSECONDS, dataStrings.length)
dataStrings.foreach { str =>
val ldt = LocalDateTime.parse(str, formatter)
val nanos = ldt.toEpochSecond(ZoneOffset.UTC) * 1000000000L + ldt.getNano()
builder.append(nanos)
}
initialHostVector = builder.build()
// Step 1b: DUMP BUFFER from the initial CPU vector.
println("\nStep 1b: Dumping buffer from initial CPU data...")
(0L until initialHostVector.getRowCount).foreach { i =>
println(f" - CPU Row $i: ${initialHostVector.getLong(i)}%20d L")
}
// Step 1c: Copy to GPU and back to CPU for verification.
println("\nStep 1c: Copying data to GPU and back to CPU...")
deviceVector = initialHostVector.copyToDevice()
roundtripHostVector = deviceVector.copyToHost()
println(" - Roundtrip complete.")
// Step 1d: A bit redudant DUMP BUFFER from the round-tripped CPU vector.
println("\nStep 1d: Dumping buffer from round-tripped CPU data...")
(0L until roundtripHostVector.getRowCount).foreach { i =>
val originalNanos = initialHostVector.getLong(i)
val roundtripNanos = roundtripHostVector.getLong(i)
val status = if (originalNanos == roundtripNanos) "OK" else "CORRUPTED"
println(f" - Roundtrip Row $i: ${roundtripNanos}%20d L (Status: $status)")
}
// Step 1e: Drive the deviceVector to a Table and write to ORC.
println(s"\nStep 1e: Writing low-level CUDF Table to ORC file at: $orcPath")
table = new Table(deviceVector)
deviceVector = null // The table now owns the vector.
// Use the helper function to configure the ORC writer options
val writerOptionsBuilder = ORCWriterOptions.builder()
WriterOptionsHelper.buildWriterOptions(writerOptionsBuilder, "LONG")
val writerOptions = writerOptionsBuilder.build()
// Create the writer targeting the file directly.
writer = Table.writeORCChunked(writerOptions, tempFile)
writer.write(table)
println(" - Successfully wrote table to file.")
} finally {
println("\nStep 1f: Cleaning up low-level resources...")
if (writer != null) writer.close()
if (table != null) table.close()
if (deviceVector != null) deviceVector.close()
if (initialHostVector != null) initialHostVector.close()
if (roundtripHostVector != null) roundtripHostVector.close()
}
// --- Part 2: High-Level Failure Reproduction (Spark RAPIDS) ---
println("\n\n--- Part 2: High-Level Failure Reproduction (Spark RAPIDS) ---")
// Use the same data to create a DataFrame
val dataRows = dataStrings.map(Row(_))
val schema = new StructType().add(StructField("timestamp_str", StringType, nullable = true))
val initialDf = spark.createDataFrame(spark.sparkContext.parallelize(dataRows), schema)
val df = initialDf.withColumn("timestamp_col", col("timestamp_str").cast("timestamp")).drop("timestamp_str")
println(s"\nStep 2a: Attempting to read ORC file from '$orcPath' with CPU Spark...")
println(" - This is where the 'nanos > 999999999' error is expected to occur.")
try {
spark.conf.set("spark.rapids.sql.enabled", "false")
val dfReadCpu = spark.read.orc(orcPath)
dfReadCpu.show(false)
} catch {
case e: Exception =>
println("\nERROR: As expected, the CPU read failed.")
e.printStackTrace()
} finally {
spark.conf.set("spark.rapids.sql.enabled", true)
val dfReadGpu = spark.read.orc(orcPath)
dfReadGpu.show(false)
}
Sample output that shows the mismatch:
Step 1e: Writing low-level CUDF Table to ORC file at: /tmp/cudf-low-level-write-1752173860964.orc
- Successfully wrote table to file.
Step 1f: Cleaning up low-level resources...
--- Part 2: High-Level Failure Reproduction (Spark RAPIDS) ---
Step 2a: Attempting to read ORC file from '/tmp/cudf-low-level-write-1752173860964.orc' with CPU Spark...
- This is where the 'nanos > 999999999' error is expected to occur.
25/07/10 18:57:41 ERROR Executor: Exception in task 0.0 in stage 20.0 (TID 20)
java.lang.IllegalArgumentException: nanos > 999999999 or < 0
at java.sql/java.sql.Timestamp.setNanos(Timestamp.java:336)
at org.apache.hadoop.hive.ql.exec.vector.TimestampColumnVector.asScratchTimestamp(TimestampColumnVector.java:139)
at org.apache.spark.sql.execution.datasources.orc.OrcAtomicColumnVector.getLong(OrcAtomicColumnVector.java:107)
at org.apache.spark.sql.catalyst.expressions.GeneratedClass$GeneratedIteratorForCodegenStage1.processNext(Unknown Source)
at org.apache.spark.sql.execution.BufferedRowIterator.hasNext(BufferedRowIterator.java:43)
at org.apache.spark.sql.execution.WholeStageCodegenEvaluatorFactory$WholeStageCodegenPartitionEvaluator$$anon$1.hasNext(WholeStageCodegenEvaluatorFactory.scala:43)
at org.apache.spark.sql.execution.SparkPlan.$anonfun$getByteArrayRdd$1(SparkPlan.scala:388)
at org.apache.spark.rdd.RDD.$anonfun$mapPartitionsInternal$2(RDD.scala:893)
at org.apache.spark.rdd.RDD.$anonfun$mapPartitionsInternal$2$adapted(RDD.scala:893)
at org.apache.spark.rdd.MapPartitionsRDD.compute(MapPartitionsRDD.scala:52)
at org.apache.spark.rdd.RDD.computeOrReadCheckpoint(RDD.scala:367)
at org.apache.spark.rdd.RDD.iterator(RDD.scala:331)
at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:93)
at org.apache.spark.TaskContext.runTaskWithListeners(TaskContext.scala:166)
at org.apache.spark.scheduler.Task.run(Task.scala:141)
at org.apache.spark.executor.Executor$TaskRunner.$anonfun$run$4(Executor.scala:620)
at org.apache.spark.util.SparkErrorUtils.tryWithSafeFinally(SparkErrorUtils.scala:64)
at org.apache.spark.util.SparkErrorUtils.tryWithSafeFinally$(SparkErrorUtils.scala:61)
at org.apache.spark.util.Utils$.tryWithSafeFinally(Utils.scala:94)
at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:623)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
.....................................................................................
.....................................................................................
25/07/10 18:57:41 WARN GpuOverrides:
!Exec <CollectLimitExec> cannot run on GPU because the Exec CollectLimitExec has been disabled, and is disabled by default because Collect Limit replacement can be slower on the GPU, if huge number of rows in a batch it could help by limiting the number of rows transferred from GPU to CPU. Set spark.rapids.sql.exec.CollectLimitExec to true if you wish to enable it
@Partitioning <SinglePartition$> could run on GPU
+--------------------------+
|LONG |
+--------------------------+
|1780-02-20 03:02:11.654891|
|2017-11-22 01:48:37.52192 |
+--------------------------+
.............................
dataStrings: Seq[String] = List(2364-09-10 02:36:45.364442, 3187-01-01 00:57:44.941024)
Expected behavior
GPU read result should match input. CPU side read should succeed past the assert failure.
Environment overview (please complete the following information)
- baremetal 25.08 release
Environment details
NA
Additional context
NA
Describe the bug
When an orc file is written with a column of timestamps, it seems to return incorrect values when read back using the orc reader in cudf. It also fails when the file is read by the cpu orc reader.
Steps/Code to reproduce bug
Below is the test case I wrote using the cudf java api and spark api. Note the difference in input and output. If there are bits missing in how the timestamp was passed to the orc writer, I appreciate any guidance on that. I am also attaching the orc file generated by this test(rename to *.orc)
cudf-low-level-write-1752173860964.orc.txt
Output from GPU and input is :
Sample output that shows the mismatch:
Expected behavior
GPU read result should match input. CPU side read should succeed past the assert failure.
Environment overview (please complete the following information)
Environment details
NA
Additional context
NA