Skip to content

Commit 0d4ef94

Browse files
committed
Fix for delta skip row exception
Signed-off-by: Niranjan Artal <nartal@nvidia.com>
1 parent 1ed2141 commit 0d4ef94

3 files changed

Lines changed: 172 additions & 24 deletions

File tree

delta-lake/delta-spark400db173/src/main/scala/com/nvidia/spark/rapids/delta/DeltaSpark400DB173Provider.scala

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,16 @@ import com.nvidia.spark.rapids.delta.shims.DeltaLogShim
4040
import com.nvidia.spark.rapids.shims.ShimPredicateHelper
4141

4242
import org.apache.spark.sql.{DataFrame, SaveMode, SparkSession}
43-
import org.apache.spark.sql.catalyst.expressions.{AttributeReference, EqualTo, Expression, If, IsNotNull, Literal, Not}
43+
import org.apache.spark.sql.catalyst.expressions.{
44+
And,
45+
AttributeReference,
46+
EqualTo,
47+
Expression,
48+
If,
49+
IsNotNull,
50+
Literal,
51+
Not
52+
}
4453
import org.apache.spark.sql.catalyst.plans.logical.TableSpec
4554
import org.apache.spark.sql.connector.write.V1Write
4655
import org.apache.spark.sql.execution.{FileSourceScanExec, FilterExec, ProjectExec, SparkPlan}
@@ -321,8 +330,20 @@ private object DB173DVPredicatePushdown extends ShimPredicateHelper {
321330

322331
def pruneDeletionVectorSkipRowColumn(plan: SparkPlan): SparkPlan = {
323332
plan.transformUp {
333+
case project @ ProjectExec(projectList, _) =>
334+
project.copy(projectList = projectList.filterNot(isDeletionVectorSkipRowColumnRef))
324335
case project @ GpuProjectExec(projectList, _, _) =>
325336
project.copy(projectList = projectList.filterNot(isDeletionVectorSkipRowColumnRef))
337+
case fsse: FileSourceScanExec =>
338+
fsse.copy(
339+
output = fsse.output.filterNot(attr => isDeletionVectorSkipRowColumn(attr.name)),
340+
requiredSchema = StructType(fsse.requiredSchema.filterNot(field =>
341+
isDeletionVectorSkipRowColumn(field.name))),
342+
// AQE expects expressions in dataFilters to exist in the output of the scan.
343+
// It will not reuse the stage of the scan otherwise. Since we are removing
344+
// the deletion-vector skip-row column from scan's output, remove the
345+
// corresponding filter from dataFilters as well.
346+
dataFilters = fsse.dataFilters.filterNot(isDVCondition))
326347
case fsse: GpuFileSourceScanExec =>
327348
fsse.copy(
328349
originalOutput = fsse.originalOutput.filterNot(attr =>
@@ -341,37 +362,53 @@ private object DB173DVPredicatePushdown extends ShimPredicateHelper {
341362
// Only native GPU DV scans can replace the skip-row filter. DB DML bitmap-writing
342363
// plans may still need that filter even when the plugin is enabled.
343364
plan.exists {
365+
case fsse: FileSourceScanExec =>
366+
fsse.relation.fileFormat.isInstanceOf[GpuDeltaParquetFileFormatNativeDV]
344367
case fsse: GpuFileSourceScanExec =>
345368
fsse.relation.fileFormat.isInstanceOf[GpuDeltaParquetFileFormatNativeDV]
346369
case _ => false
347370
}
348371
}
349372

373+
def rewriteFilter(
374+
condition: Expression,
375+
child: SparkPlan,
376+
combinePredicates: (Expression, Expression) => Expression,
377+
copyFilter: (Expression, SparkPlan) => SparkPlan): Option[SparkPlan] = {
378+
val conjuncts = splitConjunctivePredicates(condition)
379+
val (dvPredicates, otherPredicates) = conjuncts.partition { predicate =>
380+
predicate.references.size == 1 &&
381+
predicate.references.exists(ref => isDeletionVectorSkipRowColumn(ref.name)) &&
382+
isDVCondition(predicate)
383+
}
384+
val otherPredicatesReadingSkipRow = otherPredicates.exists { predicate =>
385+
predicate.references.exists(ref => isDeletionVectorSkipRowColumn(ref.name))
386+
}
387+
if (dvPredicates.nonEmpty &&
388+
!otherPredicatesReadingSkipRow &&
389+
hasNativeDeletionVectorGpuScan(child)) {
390+
val newChild = pruneDeletionVectorSkipRowColumn(child)
391+
Some(if (otherPredicates.isEmpty) {
392+
newChild
393+
} else {
394+
copyFilter(otherPredicates.reduce(combinePredicates), newChild)
395+
})
396+
} else {
397+
None
398+
}
399+
}
400+
350401
plan.transformUp {
351402
case filter @ GpuFilterExec(condition, child)
352403
if condition.references.exists(ref => isDeletionVectorSkipRowColumn(ref.name)) =>
353-
val conjuncts = splitConjunctivePredicates(condition)
354-
val (dvPredicates, otherPredicates) = conjuncts.partition { predicate =>
355-
predicate.references.size == 1 &&
356-
predicate.references.exists(ref => isDeletionVectorSkipRowColumn(ref.name)) &&
357-
isDVCondition(predicate)
358-
}
359-
val otherPredicatesReadingSkipRow = otherPredicates.exists { predicate =>
360-
predicate.references.exists(ref => isDeletionVectorSkipRowColumn(ref.name))
361-
}
362-
if (dvPredicates.nonEmpty &&
363-
!otherPredicatesReadingSkipRow &&
364-
hasNativeDeletionVectorGpuScan(child)) {
365-
val newChild = pruneDeletionVectorSkipRowColumn(child)
366-
if (otherPredicates.isEmpty) {
367-
newChild
368-
} else {
369-
filter.copy(condition = otherPredicates.reduce(GpuAnd),
370-
child = newChild)(filter.coalesceAfter)
371-
}
372-
} else {
373-
filter
374-
}
404+
rewriteFilter(condition, child, GpuAnd(_, _),
405+
(newCondition, newChild) => filter.copy(condition = newCondition,
406+
child = newChild)(filter.coalesceAfter)).getOrElse(filter)
407+
case filter @ FilterExec(condition, child)
408+
if condition.references.exists(ref => isDeletionVectorSkipRowColumn(ref.name)) =>
409+
rewriteFilter(condition, child, And(_, _),
410+
(newCondition, newChild) => filter.copy(condition = newCondition, child = newChild))
411+
.getOrElse(filter)
375412
}
376413
}
377414

delta-lake/delta-spark400db173/src/main/scala/com/nvidia/spark/rapids/delta/GpuDeltaParquetFileFormatNativeDV.scala

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.databricks.sql.io.{RowIndexFilterProvider, RowIndexFilterType}
2626
import com.databricks.sql.transaction.tahoe.{
2727
DeltaColumnMapping,
2828
DeltaColumnMappingMode,
29+
DeltaParquetFileFormat,
2930
IdMapping,
3031
NameMapping,
3132
NoMapping
@@ -1436,14 +1437,74 @@ case class DeltaParquetTableReader(
14361437
override protected lazy val resources: Seq[AutoCloseable] =
14371438
Seq(reader) ++ buffers ++ dvInfos.map(_.serializedBitmap)
14381439

1440+
private lazy val deletionVectorSkipRowIndexes =
1441+
MakeParquetTableWithDVProducer.deletionVectorSkipRowIndexes(readDataSchema)
1442+
14391443
override protected def postProcessChunk(chunk: Table): Table = {
14401444
// The cuDF reader prepends an extra index column in the output table.
14411445
// We need to drop it before returning as we don't use it.
14421446
RapidsDeletionVectors.dropFirstColumn(chunk)
14431447
}
1448+
1449+
override def next: Table = {
1450+
MakeParquetTableWithDVProducer.materializeDeletionVectorSkipRowColumnsAsFalseIfNeeded(
1451+
super.next, deletionVectorSkipRowIndexes)
1452+
}
14441453
}
14451454

14461455
object MakeParquetTableWithDVProducer extends Logging {
1456+
private def isDeletionVectorSkipRowColumn(name: String): Boolean =
1457+
name == DeltaParquetFileFormat.IS_ROW_DELETED_COLUMN_NAME ||
1458+
name == GpuDeltaParquetFileFormat.EDGE_COMPUTED_COLUMN_SKIP_ROW
1459+
1460+
private[delta] def deletionVectorSkipRowIndexes(readDataSchema: StructType): Array[Int] =
1461+
readDataSchema.fields.zipWithIndex.collect {
1462+
case (field, index) if isDeletionVectorSkipRowColumn(field.name) => index
1463+
}
1464+
1465+
// Returns the input table unchanged when the planner has pruned skip-row columns.
1466+
// If replacement is needed, this closes the input table and returns a new one.
1467+
private[delta] def materializeDeletionVectorSkipRowColumnsAsFalseIfNeeded(
1468+
table: Table,
1469+
skipRowIndexes: Array[Int]): Table = {
1470+
if (skipRowIndexes.nonEmpty) {
1471+
withResource(table) { tableToClose =>
1472+
materializeDeletionVectorSkipRowColumnsAsFalse(tableToClose, skipRowIndexes)
1473+
}
1474+
} else {
1475+
table
1476+
}
1477+
}
1478+
1479+
private[delta] def materializeDeletionVectorSkipRowColumnsAsFalse(
1480+
table: Table,
1481+
skipRowIndexes: Array[Int]): Table = {
1482+
require(skipRowIndexes.forall(_ < table.getNumberOfColumns),
1483+
s"Expected skip-row indexes ${skipRowIndexes.mkString(",")} within " +
1484+
s"${table.getNumberOfColumns} output columns")
1485+
val numRows = Math.toIntExact(table.getRowCount)
1486+
withResource(Scalar.fromBool(false)) { falseScalar =>
1487+
val columns = new Array[ColumnVector](table.getNumberOfColumns)
1488+
val replacementColumns = new ArrayBuffer[ColumnVector](skipRowIndexes.length)
1489+
try {
1490+
var i = 0
1491+
while (i < table.getNumberOfColumns) {
1492+
columns(i) = if (skipRowIndexes.contains(i)) {
1493+
val replacement = ColumnVector.fromScalar(falseScalar, numRows)
1494+
replacementColumns += replacement
1495+
replacement
1496+
} else {
1497+
table.getColumn(i)
1498+
}
1499+
i += 1
1500+
}
1501+
new Table(columns: _*)
1502+
} finally {
1503+
replacementColumns.safeClose()
1504+
}
1505+
}
1506+
}
1507+
14471508
def apply(
14481509
useChunkedReader: Boolean,
14491510
maxChunkedReaderMemoryUsageSizeBytes: Long,
@@ -1479,6 +1540,7 @@ object MakeParquetTableWithDVProducer extends Logging {
14791540
isSchemaCaseSensitive, useFieldId, readDataSchema, clippedParquetSchema,
14801541
splits, debugDumpPrefix, debugDumpAlways, deletionVectorInfos)
14811542
} else {
1543+
val skipRowIndexes = deletionVectorSkipRowIndexes(readDataSchema)
14821544
val table = withResource(buffers) { _ =>
14831545
withResource(deletionVectorInfos.map(_.serializedBitmap)) { _ =>
14841546
try {
@@ -1517,7 +1579,8 @@ object MakeParquetTableWithDVProducer extends Logging {
15171579
clippedParquetSchema, readDataSchema, isSchemaCaseSensitive, useFieldId)
15181580
val outputTable = GpuParquetScan.rebaseDateTime(evolvedSchemaTable, dateRebaseMode,
15191581
timestampRebaseMode)
1520-
new SingleGpuDataProducer(outputTable)
1582+
new SingleGpuDataProducer(
1583+
materializeDeletionVectorSkipRowColumnsAsFalseIfNeeded(outputTable, skipRowIndexes))
15211584
}
15221585
}
15231586
}

integration_tests/src/main/python/delta_lake_test.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,54 @@ def read_table(spark):
888888
assert_gpu_and_cpu_are_equal_collect(read_table, conf=conf)
889889

890890

891+
@allow_non_gpu("FilterExec", *delta_meta_allow)
892+
@delta_lake
893+
@ignore_order(local=True)
894+
@pytest.mark.skipif(not is_databricks173_or_later(),
895+
reason="This regression is specific to DBR 17.3 native DV reads")
896+
@pytest.mark.skipif(is_before_spark_353(),
897+
reason="Spark-RAPIDS supports scan with deletion vectors starting in Spark 3.5.3")
898+
def test_delta_dv_cpu_filter_after_native_scan(spark_tmp_path):
899+
data_path = spark_tmp_path + "/DELTA_DATA"
900+
conf = {
901+
"spark.rapids.sql.delta.deletionVectors.predicatePushdown.enabled": "true",
902+
"spark.databricks.delta.delete.deletionVectors.persistent": "true",
903+
"spark.databricks.delta.deletionVectors.useMetadataRowIndex": "true",
904+
"spark.rapids.sql.expression.In": "false",
905+
"spark.rapids.sql.expression.InSet": "false",
906+
"spark.rapids.sql.format.parquet.reader.type": "MULTITHREADED",
907+
"spark.rapids.sql.reader.chunked": "true"
908+
}
909+
910+
col_a_gen = IntegerGen(min_val=0, max_val=100, nullable=False, special_cases=[])
911+
col_b_gen = IntegerGen(min_val=0, max_val=5, nullable=False, special_cases=[0, 1, 2, 3])
912+
913+
def create_delta(spark):
914+
two_col_df(spark, col_a_gen, col_b_gen, length=4000).coalesce(1).write.format("delta") \
915+
.option("delta.enableDeletionVectors", "true") \
916+
.partitionBy("a").save(data_path)
917+
918+
count = spark.sql(f"DELETE FROM delta.`{data_path}` WHERE b = 0").collect()[0][0]
919+
assert count > 100, "Expected enough rows to be deleted to create deletion vectors"
920+
921+
def read_table(spark):
922+
df = spark.sql(f"SELECT a, b FROM delta.`{data_path}` WHERE b IN (1, 2, 3)")
923+
is_gpu = str(spark.conf.get("spark.rapids.sql.enabled", "false")).lower() == "true"
924+
if is_gpu:
925+
_assert_db173_gpu_delta_scan_if_enabled(spark, df)
926+
plan = df._jdf.queryExecution().executedPlan()
927+
explain_str = str(plan)
928+
callback = spark._sc._jvm.org.apache.spark.sql.rapids.ExecutionPlanCaptureCallback
929+
assert callback.contains(plan, "GpuFileGpuScan"), explain_str
930+
assert callback.contains(plan, "org.apache.spark.sql.execution.FilterExec"), \
931+
explain_str
932+
assert "_databricks_internal_edge_computed_column_skip_row" not in explain_str
933+
return df
934+
935+
with_cpu_session(create_delta, conf=conf)
936+
assert_gpu_and_cpu_are_equal_collect(read_table, conf=conf)
937+
938+
891939
@allow_non_gpu(*delta_meta_allow)
892940
@delta_lake
893941
@ignore_order(local=True)

0 commit comments

Comments
 (0)