Skip to content

Commit 56b9f6c

Browse files
vinodkccloud-fan
authored andcommitted
[SPARK-44287][SQL] Use PartitionEvaluator API in RowToColumnarExec & ColumnarToRowExec SQL operators
### What changes were proposed in this pull request? SQL operators `RowToColumnarExec` & `ColumnarToRowExec` are updated to use the `PartitionEvaluator` API to do execution. ### Why are the changes needed? To avoid the use of lambda during distributed execution. Ref: SPARK-43061 for more details. ### Does this PR introduce _any_ user-facing change? No ### How was this patch tested? Updated 2 test cases, once all the SQL operators are migrated, the flag `spark.sql.execution.useTaskEvaluator` will be enabled by default to avoid running the tests with and without this TaskEvaluator Closes #41839 from vinodkc/br_refactorToEvaluatorFactory1. Authored-by: Vinod KC <vinod.kc.in@gmail.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
1 parent 7bfbeb6 commit 56b9f6c

File tree

4 files changed

+190
-95
lines changed

4 files changed

+190
-95
lines changed

sql/core/src/main/scala/org/apache/spark/sql/execution/Columnar.scala

Lines changed: 29 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@
1717

1818
package org.apache.spark.sql.execution
1919

20-
import scala.collection.JavaConverters._
21-
22-
import org.apache.spark.{broadcast, TaskContext}
20+
import org.apache.spark.broadcast
2321
import org.apache.spark.rdd.RDD
2422
import org.apache.spark.sql.catalyst.InternalRow
25-
import org.apache.spark.sql.catalyst.expressions.{Attribute, SortOrder, SpecializedGetters, UnsafeProjection}
23+
import org.apache.spark.sql.catalyst.expressions.{Attribute, SortOrder, SpecializedGetters}
2624
import org.apache.spark.sql.catalyst.expressions.codegen._
2725
import org.apache.spark.sql.catalyst.expressions.codegen.Block._
2826
import org.apache.spark.sql.catalyst.plans.physical.Partitioning
@@ -31,7 +29,7 @@ import org.apache.spark.sql.errors.QueryExecutionErrors
3129
import org.apache.spark.sql.execution.command.DataWritingCommandExec
3230
import org.apache.spark.sql.execution.datasources.V1WriteCommand
3331
import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
34-
import org.apache.spark.sql.execution.vectorized.{OffHeapColumnVector, OnHeapColumnVector, WritableColumnVector}
32+
import org.apache.spark.sql.execution.vectorized.WritableColumnVector
3533
import org.apache.spark.sql.types._
3634
import org.apache.spark.sql.vectorized.{ColumnarBatch, ColumnVector}
3735
import org.apache.spark.util.Utils
@@ -89,15 +87,18 @@ case class ColumnarToRowExec(child: SparkPlan) extends ColumnarToRowTransition w
8987
override def doExecute(): RDD[InternalRow] = {
9088
val numOutputRows = longMetric("numOutputRows")
9189
val numInputBatches = longMetric("numInputBatches")
92-
// This avoids calling `output` in the RDD closure, so that we don't need to include the entire
93-
// plan (this) in the closure.
94-
val localOutput = this.output
95-
child.executeColumnar().mapPartitionsInternal { batches =>
96-
val toUnsafe = UnsafeProjection.create(localOutput, localOutput)
97-
batches.flatMap { batch =>
98-
numInputBatches += 1
99-
numOutputRows += batch.numRows()
100-
batch.rowIterator().asScala.map(toUnsafe)
90+
val evaluatorFactory =
91+
new ColumnarToRowEvaluatorFactory(
92+
child.output,
93+
numOutputRows,
94+
numInputBatches)
95+
96+
if (conf.usePartitionEvaluator) {
97+
child.executeColumnar().mapPartitionsWithEvaluator(evaluatorFactory)
98+
} else {
99+
child.executeColumnar().mapPartitionsInternal { batches =>
100+
val evaluator = evaluatorFactory.createEvaluator()
101+
evaluator.eval(0, batches)
101102
}
102103
}
103104
}
@@ -453,51 +454,25 @@ case class RowToColumnarExec(child: SparkPlan) extends RowToColumnarTransition {
453454
)
454455

455456
override def doExecuteColumnar(): RDD[ColumnarBatch] = {
456-
val enableOffHeapColumnVector = conf.offHeapColumnVectorEnabled
457457
val numInputRows = longMetric("numInputRows")
458458
val numOutputBatches = longMetric("numOutputBatches")
459459
// Instead of creating a new config we are reusing columnBatchSize. In the future if we do
460460
// combine with some of the Arrow conversion tools we will need to unify some of the configs.
461461
val numRows = conf.columnBatchSize
462-
// This avoids calling `schema` in the RDD closure, so that we don't need to include the entire
463-
// plan (this) in the closure.
464-
val localSchema = this.schema
465-
child.execute().mapPartitionsInternal { rowIterator =>
466-
if (rowIterator.hasNext) {
467-
new Iterator[ColumnarBatch] {
468-
private val converters = new RowToColumnConverter(localSchema)
469-
private val vectors: Seq[WritableColumnVector] = if (enableOffHeapColumnVector) {
470-
OffHeapColumnVector.allocateColumns(numRows, localSchema)
471-
} else {
472-
OnHeapColumnVector.allocateColumns(numRows, localSchema)
473-
}
474-
private val cb: ColumnarBatch = new ColumnarBatch(vectors.toArray)
475-
476-
TaskContext.get().addTaskCompletionListener[Unit] { _ =>
477-
cb.close()
478-
}
479-
480-
override def hasNext: Boolean = {
481-
rowIterator.hasNext
482-
}
483-
484-
override def next(): ColumnarBatch = {
485-
cb.setNumRows(0)
486-
vectors.foreach(_.reset())
487-
var rowCount = 0
488-
while (rowCount < numRows && rowIterator.hasNext) {
489-
val row = rowIterator.next()
490-
converters.convert(row, vectors.toArray)
491-
rowCount += 1
492-
}
493-
cb.setNumRows(rowCount)
494-
numInputRows += rowCount
495-
numOutputBatches += 1
496-
cb
497-
}
498-
}
499-
} else {
500-
Iterator.empty
462+
val evaluatorFactory =
463+
new RowToColumnarEvaluatorFactory(
464+
conf.offHeapColumnVectorEnabled,
465+
numRows,
466+
schema,
467+
numInputRows,
468+
numOutputBatches)
469+
470+
if (conf.usePartitionEvaluator) {
471+
child.execute().mapPartitionsWithEvaluator(evaluatorFactory)
472+
} else {
473+
child.execute().mapPartitionsInternal { rowIterator =>
474+
val evaluator = evaluatorFactory.createEvaluator()
475+
evaluator.eval(0, rowIterator)
501476
}
502477
}
503478
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.spark.sql.execution
19+
20+
import scala.collection.JavaConverters._
21+
22+
import org.apache.spark.{PartitionEvaluator, PartitionEvaluatorFactory, TaskContext}
23+
import org.apache.spark.sql.catalyst.InternalRow
24+
import org.apache.spark.sql.catalyst.expressions.{Attribute, UnsafeProjection}
25+
import org.apache.spark.sql.execution.metric.SQLMetric
26+
import org.apache.spark.sql.execution.vectorized.{OffHeapColumnVector, OnHeapColumnVector, WritableColumnVector}
27+
import org.apache.spark.sql.types.StructType
28+
import org.apache.spark.sql.vectorized.ColumnarBatch
29+
30+
class ColumnarToRowEvaluatorFactory(
31+
childOutput: Seq[Attribute],
32+
numOutputRows: SQLMetric,
33+
numInputBatches: SQLMetric)
34+
extends PartitionEvaluatorFactory[ColumnarBatch, InternalRow] {
35+
36+
override def createEvaluator(): PartitionEvaluator[ColumnarBatch, InternalRow] = {
37+
new ColumnarToRowEvaluator
38+
}
39+
40+
private class ColumnarToRowEvaluator extends PartitionEvaluator[ColumnarBatch, InternalRow] {
41+
override def eval(
42+
partitionIndex: Int,
43+
inputs: Iterator[ColumnarBatch]*): Iterator[InternalRow] = {
44+
assert(inputs.length == 1)
45+
val toUnsafe = UnsafeProjection.create(childOutput, childOutput)
46+
inputs.head.flatMap { input =>
47+
numInputBatches += 1
48+
numOutputRows += input.numRows()
49+
input.rowIterator().asScala.map(toUnsafe)
50+
}
51+
}
52+
}
53+
}
54+
55+
class RowToColumnarEvaluatorFactory(
56+
enableOffHeapColumnVector: Boolean,
57+
numRows: Int,
58+
schema: StructType,
59+
numInputRows: SQLMetric,
60+
numOutputBatches: SQLMetric)
61+
extends PartitionEvaluatorFactory[InternalRow, ColumnarBatch] {
62+
63+
override def createEvaluator(): PartitionEvaluator[InternalRow, ColumnarBatch] = {
64+
new RowToColumnarEvaluator
65+
}
66+
67+
private class RowToColumnarEvaluator extends PartitionEvaluator[InternalRow, ColumnarBatch] {
68+
override def eval(
69+
partitionIndex: Int,
70+
inputs: Iterator[InternalRow]*): Iterator[ColumnarBatch] = {
71+
assert(inputs.length == 1)
72+
val rowIterator = inputs.head
73+
74+
if (rowIterator.hasNext) {
75+
new Iterator[ColumnarBatch] {
76+
private val converters = new RowToColumnConverter(schema)
77+
private val vectors: Seq[WritableColumnVector] = if (enableOffHeapColumnVector) {
78+
OffHeapColumnVector.allocateColumns(numRows, schema)
79+
} else {
80+
OnHeapColumnVector.allocateColumns(numRows, schema)
81+
}
82+
private val cb: ColumnarBatch = new ColumnarBatch(vectors.toArray)
83+
84+
TaskContext.get().addTaskCompletionListener[Unit] { _ =>
85+
cb.close()
86+
}
87+
88+
override def hasNext: Boolean = {
89+
rowIterator.hasNext
90+
}
91+
92+
override def next(): ColumnarBatch = {
93+
cb.setNumRows(0)
94+
vectors.foreach(_.reset())
95+
var rowCount = 0
96+
while (rowCount < numRows && rowIterator.hasNext) {
97+
val row = rowIterator.next()
98+
converters.convert(row, vectors.toArray)
99+
rowCount += 1
100+
}
101+
cb.setNumRows(rowCount)
102+
numInputRows += rowCount
103+
numOutputBatches += 1
104+
cb
105+
}
106+
}
107+
} else {
108+
Iterator.empty
109+
}
110+
}
111+
}
112+
}

sql/core/src/test/scala/org/apache/spark/sql/SparkSessionExtensionSuite.scala

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -279,36 +279,40 @@ class SparkSessionExtensionSuite extends SparkFunSuite with SQLHelper {
279279
}
280280
withSession(extensions) { session =>
281281
session.conf.set(SQLConf.ADAPTIVE_EXECUTION_ENABLED, enableAQE)
282-
assert(session.sessionState.columnarRules.contains(
283-
MyColumnarRule(PreRuleReplaceAddWithBrokenVersion(), MyPostRule())))
284-
import session.sqlContext.implicits._
285-
// perform a join to inject a broadcast exchange
286-
val left = Seq((1, 50L), (2, 100L), (3, 150L)).toDF("l1", "l2")
287-
val right = Seq((1, 50L), (2, 100L), (3, 150L)).toDF("r1", "r2")
288-
val data = left.join(right, $"l1" === $"r1")
289-
// repartitioning avoids having the add operation pushed up into the LocalTableScan
290-
.repartition(1)
291-
val df = data.selectExpr("l2 + r2")
292-
// execute the plan so that the final adaptive plan is available when AQE is on
293-
df.collect()
294-
val found = collectPlanSteps(df.queryExecution.executedPlan).sum
295-
// 1 MyBroadcastExchangeExec
296-
// 1 MyShuffleExchangeExec
297-
// 1 ColumnarToRowExec
298-
// 2 ColumnarProjectExec
299-
// 1 ReplacedRowToColumnarExec
300-
// so 11121 is expected.
301-
assert(found == 11121)
302-
303-
// Verify that we get back the expected, wrong, result
304-
val result = df.collect()
305-
assert(result(0).getLong(0) == 101L) // Check that broken columnar Add was used.
306-
assert(result(1).getLong(0) == 201L)
307-
assert(result(2).getLong(0) == 301L)
308-
309-
withTempPath { path =>
310-
val e = intercept[Exception](df.write.parquet(path.getCanonicalPath))
311-
assert(e.getMessage == "columnar write")
282+
Seq(true, false).foreach { enableEvaluator =>
283+
withSQLConf(SQLConf.USE_PARTITION_EVALUATOR.key -> enableEvaluator.toString) {
284+
assert(session.sessionState.columnarRules.contains(
285+
MyColumnarRule(PreRuleReplaceAddWithBrokenVersion(), MyPostRule())))
286+
import session.sqlContext.implicits._
287+
// perform a join to inject a broadcast exchange
288+
val left = Seq((1, 50L), (2, 100L), (3, 150L)).toDF("l1", "l2")
289+
val right = Seq((1, 50L), (2, 100L), (3, 150L)).toDF("r1", "r2")
290+
val data = left.join(right, $"l1" === $"r1")
291+
// repartitioning avoids having the add operation pushed up into the LocalTableScan
292+
.repartition(1)
293+
val df = data.selectExpr("l2 + r2")
294+
// execute the plan so that the final adaptive plan is available when AQE is on
295+
df.collect()
296+
val found = collectPlanSteps(df.queryExecution.executedPlan).sum
297+
// 1 MyBroadcastExchangeExec
298+
// 1 MyShuffleExchangeExec
299+
// 1 ColumnarToRowExec
300+
// 2 ColumnarProjectExec
301+
// 1 ReplacedRowToColumnarExec
302+
// so 11121 is expected.
303+
assert(found == 11121)
304+
305+
// Verify that we get back the expected, wrong, result
306+
val result = df.collect()
307+
assert(result(0).getLong(0) == 101L) // Check that broken columnar Add was used.
308+
assert(result(1).getLong(0) == 201L)
309+
assert(result(2).getLong(0) == 301L)
310+
311+
withTempPath { path =>
312+
val e = intercept[Exception](df.write.parquet(path.getCanonicalPath))
313+
assert(e.getMessage == "columnar write")
314+
}
315+
}
312316
}
313317
}
314318
}

sql/core/src/test/scala/org/apache/spark/sql/execution/SparkPlanSuite.scala

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,22 @@ class SparkPlanSuite extends QueryTest with SharedSparkSession {
127127

128128
test("SPARK-37779: ColumnarToRowExec should be canonicalizable after being (de)serialized") {
129129
withSQLConf(SQLConf.USE_V1_SOURCE_LIST.key -> "parquet") {
130-
withTempPath { path =>
131-
spark.range(1).write.parquet(path.getAbsolutePath)
132-
val df = spark.read.parquet(path.getAbsolutePath)
133-
val columnarToRowExec =
134-
df.queryExecution.executedPlan.collectFirst { case p: ColumnarToRowExec => p }.get
135-
try {
136-
spark.range(1).foreach { _ =>
137-
columnarToRowExec.canonicalized
138-
()
130+
Seq(true, false).foreach { enable =>
131+
withSQLConf(SQLConf.USE_PARTITION_EVALUATOR.key -> enable.toString) {
132+
withTempPath { path =>
133+
spark.range(1).write.parquet(path.getAbsolutePath)
134+
val df = spark.read.parquet(path.getAbsolutePath)
135+
val columnarToRowExec =
136+
df.queryExecution.executedPlan.collectFirst { case p: ColumnarToRowExec => p }.get
137+
try {
138+
spark.range(1).foreach { _ =>
139+
columnarToRowExec.canonicalized
140+
()
141+
}
142+
} catch {
143+
case e: Throwable => fail("ColumnarToRowExec was not canonicalizable", e)
144+
}
139145
}
140-
} catch {
141-
case e: Throwable => fail("ColumnarToRowExec was not canonicalizable", e)
142146
}
143147
}
144148
}

0 commit comments

Comments
 (0)