Skip to content

[SPARK-26572][SQL] fix aggregate codegen result evaluation - #23731

Closed
peter-toth wants to merge 5 commits into
apache:masterfrom
peter-toth:SPARK-26572
Closed

[SPARK-26572][SQL] fix aggregate codegen result evaluation#23731
peter-toth wants to merge 5 commits into
apache:masterfrom
peter-toth:SPARK-26572

Conversation

@peter-toth

@peter-tothpeter-toth commented Feb 3, 2019

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This PR is a correctness fix in HashAggregateExec code generation. It forces evaluation of result expressions before calling consume() to avoid multiple executions.

This PR fixes a use case where an aggregate is nested into a broadcast join and appears on the "stream" side. The issue is that Broadcast join generates it's own loop. And without forcing evaluation of resultExpressions of HashAggregateExec before the join's loop these expressions can be executed multiple times giving incorrect results.

How was this patch tested?

New UT was added.

@maropu

Copy link
Copy Markdown
Member

I think we should handle this case in a planner?
For example, if we turn off broadcast join, the behaviour changes;

scala> val baseTable = Seq((1), (1)).toDF("idx")
scala> val distinctWithId = baseTable.distinct.withColumn("id", functions.monotonically_increasing_id())
scala> baseTable.join(distinctWithId, "idx").show
+---+------------+
|idx| id|
+---+------------+
| 1|369367187456|
| 1|369367187457|
+---+------------+
sql("SET spark.sql.autoBroadcastJoinThreshold=-1")
scala> baseTable.join(distinctWithId, "idx").show
+---+------------+
|idx| id|
+---+------------+
| 1|369367187456|
| 1|369367187456|
+---+------------+

Could you check again?

@maropu

Copy link
Copy Markdown
Member

btw, could you describe more in the PR description? what's the root cause of this issue? How did this pr fix the issue? brabrabra....

@peter-toth

peter-toth commented Feb 4, 2019

Copy link
Copy Markdown
ContributorAuthor

The reason why I think this is a code generation issue is that if you disable spark.sql.codegen.wholeStage then the result is correct.

This is the physical plan of the example in the ticket:

== Physical Plan ==
*(3) Project [idx#4, id#6L]
+- *(3) BroadcastHashJoin [idx#4], [idx#9], Inner, BuildLeft
:- BroadcastExchange HashedRelationBroadcastMode(List(cast(input[0, int, false] as bigint)))
: +- *(1) Project [value#1 AS idx#4]
: +- LocalTableScan [value#1]
+- *(3) HashAggregate(keys=[idx#9], functions=[], output=[idx#9, id#6L])
+- Exchange hashpartitioning(idx#9, 5)
+- *(2) HashAggregate(keys=[idx#9], functions=[], output=[idx#9])
+- *(2) Project [value#1 AS idx#9]
+- LocalTableScan [value#1]

and if you take a look the code of stage 3 (left some comments in it regarding what my PR does):

 ...
// this method is called for every aggregation key
private void agg_doAggregateWithKeysOutput_0(UnsafeRow agg_keyTerm_0, UnsafeRow agg_bufferTerm_0)
throws java.io.IOException {
((org.apache.spark.sql.execution.metric.SQLMetric) references[4] /* numOutputRows */).add(1);
int agg_value_4 = agg_keyTerm_0.getInt(0);
// this PR moves agg_value_5 calculation and agg_count_0 increment from boradcast join loop to here
// generate join key for stream side
boolean bhj_isNull_0 = false;
long bhj_value_0 = -1L;
if (!false) {
bhj_value_0 = (long) agg_value_4;
}
// find matches from HashRelation
scala.collection.Iterator bhj_matches_0 = bhj_isNull_0 ? null
: (scala.collection.Iterator) bhj_relation_0.get(bhj_value_0);
if (bhj_matches_0 != null) {
while (bhj_matches_0.hasNext()) {
UnsafeRow bhj_matched_0 = (UnsafeRow) bhj_matches_0.next();
{
((org.apache.spark.sql.execution.metric.SQLMetric) references[6] /* numOutputRows */).add(1);
int bhj_value_2 = bhj_matched_0.getInt(0);
boolean project_isNull_0 = false;
UTF8String project_value_0 = null;
if (!false) {
project_value_0 = UTF8String.fromString(String.valueOf(bhj_value_2));
}
final long agg_value_5 = partitionMask + agg_count_0;
agg_count_0++;
boolean project_isNull_2 = false;
UTF8String project_value_2 = null;
if (!false) {
project_value_2 = UTF8String.fromString(String.valueOf(agg_value_5));
}
...

So both hash aggregate and broadcast join are required in one codegen stage to experience this issue and also important that aggregate has to be on the "stream" side. This might be a rare case and explains why this issue hasn't come up earlier.
(I also think that there might be other operators than broadcast join that generate loop and so are affected, but I didn't look into that.)
But I think this is an issue with the generated code of HashAggregateExec and it seems to me that we can force evaluation of resultExpressions before generating broadcast join code (ie. calling consume()) without any drawback.

@mgaido91

Copy link
Copy Markdown
Contributor

The changes makes sense to me, but I think this problem was introduced in SPARK-13404, which claimed to have a significant perf gain (about 30% on TPCDS Q55), so it would be great if we can fix this without introducing perf regression. @peter-toth may you please run (and post the results) the benchmarks in order to ensure we are not introducing a perf regression with this PR?

@davies you are the author of that PR, do you have time to check this?

@maropu

Copy link
Copy Markdown
Member

ok to test

@maropu

Copy link
Copy Markdown
Member

cc: @cloud-fan@hvanhovell

@maropu

Copy link
Copy Markdown
Member

This issue happens in case of stateful exprs only? If so, could you modify the code to apply the current fix only if HashAggregateExec has stateful exprs? I worry about the performance regression @mgaido91 pointed out, too. It seems the current fix affect the other queries, its a corner case though....

$evaluateKeyVars
$evaluateBufferVars
$evaluateAggResults
$evaluateResultVars

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this change?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. If you replace .distinct() to .groupBy("idx").max() in the example then this code path runs and the change fixes the same issue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so, could you please add test cases to cover all the code paths you added in this pr.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I've added that path to the test.

@SparkQA

Copy link
Copy Markdown

Test build #102034 has finished for PR 23731 at commit b5d079c.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@peter-toth

Copy link
Copy Markdown
ContributorAuthor

Here are my benchmark results of q55. I run 3 times on master and 3 times on this PR branch against scale=5 generated data.
Master:

master:
Stopped after 5 iterations, 29324 ms
Java HotSpot(TM) 64-Bit Server VM 1.8.0_162-b12 on Mac OS X 10.14.2
Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
TPCDS Snappy: Best/Avg Time(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------
q55 5683 / 5865 2.6 391.2 1.0X
Stopped after 5 iterations, 28914 ms
Java HotSpot(TM) 64-Bit Server VM 1.8.0_162-b12 on Mac OS X 10.14.2
Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
TPCDS Snappy: Best/Avg Time(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------
q55 5584 / 5783 2.6 384.3 1.0X
Stopped after 5 iterations, 29905 ms
Java HotSpot(TM) 64-Bit Server VM 1.8.0_162-b12 on Mac OS X 10.14.2
Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
TPCDS Snappy: Best/Avg Time(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------
q55 5873 / 5981 2.5 404.3 1.0X

This PR:

this PR:
Stopped after 5 iterations, 32577 ms
Java HotSpot(TM) 64-Bit Server VM 1.8.0_162-b12 on Mac OS X 10.14.2
Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
TPCDS Snappy: Best/Avg Time(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------
q55 6226 / 6515 2.3 428.5 1.0X
Stopped after 5 iterations, 30612 ms
Java HotSpot(TM) 64-Bit Server VM 1.8.0_162-b12 on Mac OS X 10.14.2
Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
TPCDS Snappy: Best/Avg Time(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------
q55 5792 / 6122 2.5 398.6 1.0X
Stopped after 5 iterations, 32918 ms
Java HotSpot(TM) 64-Bit Server VM 1.8.0_162-b12 on Mac OS X 10.14.2
Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
TPCDS Snappy: Best/Avg Time(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------
q55 6415 / 6584 2.3 441.5 1.0X

Although the results are a bit varying, it seems this patch would introduce some performance degradation.
I will try to modify the patch to evaluate only Stateful expressions as @maropu suggested and run the benchmark again.

@mgaido91

Copy link
Copy Markdown
Contributor

@peter-toth did you run the benchmark also on the other queries? My guess is that it may also happen that q55 gets some perf degradation, but others improve. In that case we should kind of average over all the queries whether the impact is positive or not.

In case we decide to limit this to be done only for some expressions, we should do it for those which aer non-deterministic rather than only for the Stateful ones.

@peter-toth

Copy link
Copy Markdown
ContributorAuthor

Thanks @mgaido91, then I will run a full benchmark first.

@mgaido91

Copy link
Copy Markdown
Contributor

Thanks @peter-toth!

@dongjoon-hyun

Copy link
Copy Markdown
Member

Retest this please.

@SparkQA

Copy link
Copy Markdown

Test build #102104 has finished for PR 23731 at commit b5d079c.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@cloud-fan

Copy link
Copy Markdown
Contributor

The issue is that Broadcast join generates it's own loop. And without forcing evaluation of resultExpressions of HashAggregateExec before the join's loop these expressions can be executed multiple times giving incorrect results.

Shouldn't we fix join instead of aggregate?

consume(ctx, eval)
val evaluateResultVars = evaluateVariables(resultVars)
s"""
$evaluateResultVars

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For non broadcast join cases, the change will force evaluation unnecessarily too. We should move evaluation out of the loop in broadcast join, if possible.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I a bit concern about is; is it semantically ok to defer the evaluation of nondeterministic exprs if HashAggregateExec has these exprs?

I think, to fix this issue, its ok to modify code in the join side if we could find a simpler solution there with no performance regression. But, I have just a question about the design regardless of this issue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh.. Kris answered my question.. in #23731 (review)

@peter-toth

Copy link
Copy Markdown
ContributorAuthor

@mgaido91@maropu@cloud-fan@viirya I've just collected the results of full TPCDSQueryBenchmark runs on master vs. this PR and overall it doesn't seem to have that big impact if we force the evaluation in aggregate. But I will try to change the PR and fix broadcast join to force evaluation of non-deterministic expressions out of the loop.

Here are the benchmark results if you are interested:

test case (values in ms)mastermaster 2nd runthis PRthis PR 2nd run
total27023411266504362707638627024466
q176017756797898881349
q1090319880329311297605
q10a-v2.793878932209511394199
q11187918187985198156201343
q11-v2.7186384183901191372195608
q1235709347683710939020
q12-v2.733617346293566637382
q1376088754207922479844
q14-v2.7647991593543611725638555
q14a793687779841786366780845
q14a-v2.7835538849404845488824285
q14b632994659007647556628163
q1550407487125303952104
q16173310176263181887185434
q17358234349108384112380049
q1867506676117054771098
q18a-v2.7260611263082268121260403
q1945170455664827048136
q262042632196408065965
q2037064367263892238857
q20-v2.735919351093788237615
q2130900308863139432062
q22134810140961139015146047
q22-v2.7881799666436736824786129
q22a-v2.777062766217641277984
q23a551085562690570236554450
q23b509188529924514952504751
q24-v2.7232464223424235800242875
q24a235519239793240399239449
q24b242000235056240476237701
q25365661364580375756388874
q2650035497115283253557
q2754088531215534457900
q27a-v2.7156000153863156385149929
q28253946253869264420266887
q29364358368899371732377960
q332569328633446035264
q3086921871799032293566
q31195233195026204116208575
q3270098703557200073724
q33113700114023117777121381
q3448608488595038251431
q34-v2.747455463664891749559
q3596340933869737599735
q35-v2.792379930769575997744
q35a-v2.798323971029899095888
q3652161523145387355724
q36a-v2.756251553595568855844
q3771609727887400775574
q38113581111851115416117293
q39a67328702856955367597
q39b67457697477005967390
q4645699632785630790662483
q4092189903629477497688
q413931390641574170
q4232727338283336835238
q4341522409114327944498
q44118338118557122399126860
q4542166427294408643529
q4657798570735981360527
q4792922928239455199835
q47-v2.790674901669169696707
q4864766653246601769578
q49290310306006294090303586
q49-v2.7288623320468294292290968
q5242664244383258006263323
q50177935187770193884188241
q51180839180103184773189253
q51a-v2.71261774116628511735361156590
q5232827325613362233896
q5342069422034338744729
q54178694180335185816193587
q5532863320913304933913
q56113489110395116137116632
q5777085781208031380765
q57-v2.775062751847897178424
q58102682104439106576109776
q5958396574085931462043
q5a-v2.7264955257989264370266317
q6109696107457107418109251
q6-v2.7114195109684113283114997
q60115152111163116920118019
q6188653878158890592676
q6245958450024584249475
q6342271414994255344231
q64375608366708371139389530
q64-v2.7398085387919378419381406
q65120201121013121007124101
q66107631110160110194116045
q67491712496057502941498816
q67a-v2.7582984582585588929561230
q6857644573965857462110
q6992364864738780492744
q753092541075605056440
q7076027762367824980208
q70a-v2.781529812888019379803
q71111032118959114687115729
q721263412121525812047831194142
q72-v2.71274719129723012243491188723
q7350254478804853950459
q74169414163799163079169219
q74-v2.7162417163068161341164335
q75365739366476372510377480
q75-v2.7352554372481374001359940
q76114328110486114099114004
q77181185174504177397179485
q77a-v2.7191487188109196948188439
q78381549366170382482377300
q78-v2.7373872381872400429363220
q7955042536355558855914
q840407399124174542687
q80515092512636542615522297
q80a-v2.7527537532262546034529508
q8185570858088857986749
q82990389638599921100342
q8310624999379104401105534
q8439353376834016539717
q85167298167352165008169000
q8637422372463843239093
q86a-v2.740440401654169741213
q87120091117061125508123227
q88283596293759296313300676
q8944557448824623646012
q9499645497991514793525809
q9070330749057299874123
q9145903467324884348705
q9266107663816838867379
q93281893293320292280295871
q94117957117824126470124528
q95632772591228607336608948
q9637997382763983239946
q97105541104452113225108664
q9838516384364104941108
q98-v2.737586369393978639280
q9950378514915360852571

@mgaido91

Copy link
Copy Markdown
Contributor

@cloud-fan@viirya I am not sure about fixing this in the join is a good idea. First of all we have many kind of joins, so likely we would need to impact all of them and there may be other operators which use loops other than joins. I don't think it is correct to delegate to the consumer the responsibility of computing variables if needed. It seems more reasonable to me to fix it in the aggregate honestly.

@cloud-fan

Copy link
Copy Markdown
Contributor

@mgaido91 are you sure aggregate is the only one that produces unevaluated result expressions? IIRC this is a long-standing optimization in the whole stage codegen framework, and there is no such a rule that operators must evaluate the result expressions before calling parent.consume.

also cc @rednaxelafx@kiszk

@dongjoon-hyun

Copy link
Copy Markdown
Member

cc @dbtsai since he is the release manager for 2.4.1.

@rednaxelafxrednaxelafx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bug and fix touches a basic design area of Spark SQL's whole-stage codegen:

  • Deterministic expressions can be evaluated anywhere as long as the inputs (data dependencies) are available, and are allowed to be evaluated multiple times (although from a performance point of view it's not preferred to evaluate them repeatedly); non-deterministic expressions has to be only evaluated once, and the order of evaluation should respect the order in the original query.

Two rules of thumb are:

  1. In the whole-stage codegen framework, the evaluation of a deterministic expression can be deferred to just before its result is used. To improve performance and reduce code size, we only expect output expressions that are used more than once to be eagerly evaluated. This "used more than once" is expressed by CodegenSupport.usedInputs, and CodegenSupport.consume() handles the eager evaluation of such expressions automatically. That's #11274 already mentioned in one of the comments above.
  2. Any physical plan operator that carries an output projection list, such as ProjectExec and in this case HashAggregateExec has to perform special treatment of forcing evaluation of non-deterministic expressions before passing the outputVars to consume(), to make sure the side effects are emitted in the correct order and not evaluated repeatedly in the parents' doConsume(). See ProjectExec.doConsume() for an example of what this special treatment should look like.

Note that Stateful expressions are Nondeterministic by design; the latter covers more expressions than the former.

The reason why this special treatment isn't done in the CodegenSupport.consume() framework function is because: consume() only gets to see the outputVars from the child as a list of ExprCodes but not the list of Expressions that produced the code. The former has lost the notion of whether the generated code is deterministic or not, which can only be found on the latter.
consume() also gets to see the child.outputs but that's a list of Attributes, which doesn't have the knowledge of whether or not the original expression was deterministic. So that doesn't help.
With that, we'd have to perform the special treatment before calling consume().

This brings us to another related note: in the whole-stage codegen world, it really is preferred to host non-trivial expressions in ProjectExec as much as possible, so that we'd only have to non-trivial expression handling in one place. Fusing the output projection list in a fat operator is a design from the past -- it would have helped reduce the operator boundaries and thus reduce materialization/operator dispatch overhead in the Volcano model, but in the whole-stage codegen world that doesn't matter at all.

Here's my suggested fix for HashAggregateExec:

diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala
index 19a47ffc6d..be457b435b 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala@@ -154,6 +154,14 @@ case class HashAggregateExec(
child.asInstanceOf[CodegenSupport].inputRDDs()
}
+ // Extract the code to evaluate non-deterministic expressions in the resultExpressions.+ // NOTE: this function will mutate the state of the `ExprCode`s in `resultVars`: the `code` of+ // non-deterministic expressions will be cleared.+ private def evaluateNondeterministicResults(resultVars: Seq[ExprCode]): String = {+ val nondeterministicAttrs = resultExpressions.filterNot(_.deterministic).map(_.toAttribute)+ evaluateRequiredVariables(output, resultVars, AttributeSet(nondeterministicAttrs))+ }+
protected override def doProduce(ctx: CodegenContext): String = {
if (groupingExpressions.isEmpty) {
doProduceWithoutKeys(ctx)
@@ -208,8 +216,10 @@ case class HashAggregateExec(
// evaluate result expressions
ctx.currentVars = aggResults
val resultVars = bindReferences(resultExpressions, aggregateAttributes).map(_.genCode(ctx))
+ val evaluateNondeterministicAggResults = evaluateNondeterministicResults(resultVars)
(resultVars, s"""
|$evaluateAggResults
+ |$evaluateNondeterministicAggResults
|${evaluateVariables(resultVars)}
""".stripMargin)
} else if (modes.contains(Partial) || modes.contains(PartialMerge)) {
@@ -466,10 +476,12 @@ case class HashAggregateExec(
val resultVars = bindReferences[Expression](
resultExpressions,
inputAttrs).map(_.genCode(ctx))
+ val evaluateNondeterministicAggResults = evaluateNondeterministicResults(resultVars)
s"""
$evaluateKeyVars
$evaluateBufferVars
$evaluateAggResults
+ $evaluateNondeterministicAggResults
${consume(ctx, resultVars)}
"""
} else if (modes.contains(Partial) || modes.contains(PartialMerge)) {
@@ -506,10 +518,14 @@ case class HashAggregateExec(
// generate result based on grouping key
ctx.INPUT_ROW = keyTerm
ctx.currentVars = null
- val eval = bindReferences[Expression](+ val resultVars = bindReferences[Expression](
resultExpressions,
groupingAttributes).map(_.genCode(ctx))
- consume(ctx, eval)+ val evaluateNondeterministicResults = evaluateNondeterministicResults(resultVars)+ s"""+ |$evaluateNondeterministicAggResults+ |${consume(ctx, resultVars)}+ """.stripMargin
}
ctx.addNewFunction(funcName,
s"""

@peter-toth

peter-toth commented Feb 12, 2019

Copy link
Copy Markdown
ContributorAuthor

I was thinking of why this following simple code snippet doesn't have the same issue:

 val baseTable = Seq((1), (1)).toDF("idx")
val distinctWithId = baseTable.withColumn("id", monotonically_increasing_id())
val x = baseTable.join(distinctWithId, "idx")
x.show()

because it produces the expected

+---+----------+
|idx| id|
+---+----------+
| 1| 0|
| 1| 0|
| 1|8589934592|
| 1|8589934592|
+---+----------+

and it seems because doConsume in ProjectExec evaluates non deterministic result vars before passing to Join. So I think it would be analogous to handle non-determinism in aggregate.

Oops, meanwhile we got the same answer. Thanks @rednaxelafx.

@maropu

Copy link
Copy Markdown
Member

Thanks, Kris, I'm just curious that the @rednaxelafx approach has no performance regression..

@peter-toth

peter-toth commented Feb 12, 2019

Copy link
Copy Markdown
ContributorAuthor

So, shall I adjust the fix as @rednaxelafx suggested and maybe run another benchmark? Any objections?

@rednaxelafx

Copy link
Copy Markdown
Contributor

@maropu : my proposed change won't introduce any performance regressions because what used to be both (1) correct and (2) fast will stay the same, no changes whatsoever; whereas what used to be incorrect will be fixed.
You won't see any statistically significant differences in TPC-DS perf numbers because that benchmark doesn't really use a lot of non-deterministic expressions. Such expressions are rare in the SQL world. There isn't even a rand() call in TPC-DS...
We should expect the TPC-DS queries to generate identical whole-stage codegen code before and after my proposed fix.

@mgaido91

Copy link
Copy Markdown
Contributor

Thanks for your comment @rednaxelafx , huge +1 on everything you just said.

@mgaido91 are you sure aggregate is the only one that produces unevaluated result expressions?

@cloud-fan if it is not the only one, I think we have to fix the others too, but I don't think there are. ProjectExec is fine as mentioned by @rednaxelafx and I can't think of other plans which can generate non-deterministic expressions (there may be, but in this moment none comes to my mind).

@maropu

maropu commented Feb 12, 2019

Copy link
Copy Markdown
Member

@rednaxelafx I just worried about performance numbers other than TPCDS though, that's certainly true. Thanks, Kris.

nit: btw, could we move evaluateNondeterministicResults into CodegenSupport, and then ProjectExec reuse it?

@rednaxelafxrednaxelafx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly LGTM, with a comment in the test case.

* Returns source code to evaluate the variables for non-deterministic expressions, and clear the
* code of evaluated variables, to prevent them to be evaluated twice.
*/
protected def evaluateNondeterministicVariables(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick on naming: "variables" are never non-deterministic, only expressions can have the property of being deterministic or not. Two options:

  • I'd prefer naming this utility function evaluateNondeterministicResults to emphasis this should (mostly) be used on the results of an output projection list.
  • But the existing utility function evaluateRequiredVariables uses the "variable" notion, so keeping consistency there is fine too.

I'm fine either way.

Also, historically Spark SQL's WSCG would use variable names like eval for the ExprCode type, e.g. evals: Seq[ExprCode]. Not sure why it started that way but you can see that naming pattern throughout the WSCG code base.
Again, your new utility function follows the same names used in evaluateRequiredVariables so that's fine. Local consistency is good enough.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep the consistent naming, +1 for evaluateNondeterministicVariables .

val baseTable = Seq((1), (1)).toDF("idx")

// BroadcastHashJoinExec with a HashAggregateExec child containing no aggregate expressions
val distinctWithId = baseTable.distinct().withColumn("id", monotonically_increasing_id())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how stable the results are going to be if you use monotonically_increasing_id here with an unspecified number of shuffle partitions. Since you're checking the exact value of the resulting id, if the number of shuffle partitions changes (let's say if someone decides to change the default shuffle partitions setting in all tests), this test can become fragile and fail unnecessarily.

It might be worth setting the shuffle partition to 1 explicitly inside this test case. Or go back to grouping by id instead of checking the exact value of id, or just assert the ids are equal.

@maropumaropuFeb 13, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, how about wrapping with withSQLConf(SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> Long.MaxValue.toString) for safeguard.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Fixed both.

@SparkQA

Copy link
Copy Markdown

Test build #102262 has finished for PR 23731 at commit 567f8f6.

  • This patch fails Spark unit tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

Comment threadsql/core/src/test/scala/org/apache/spark/sql/DataFrameSuite.scala Outdated
val distinctWithId = baseTable.distinct().withColumn("id", monotonically_increasing_id())
.join(baseTable, "idx")
assert(distinctWithId.queryExecution.executedPlan.collectFirst {
case BroadcastHashJoinExec(_, _, _, _, _, HashAggregateExec(_, _, Seq(), _, _, _, _), _) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this?

 assert(distinctWithId.queryExecution.executedPlan.collectFirst {
case j: BroadcastHashJoinExec if j.left.asInstanceOf[HashAggregateExec] => true
}.isDefined)

We need to strictly check agregate exprs? It seems baseTable.distinct() obviously has no aggregate expr?

@peter-tothpeter-tothFeb 13, 2019

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer avoiding isInstanceOf if possible, but changed it a bit.

Comment threadsql/core/src/test/scala/org/apache/spark/sql/DataFrameSuite.scala Outdated
Comment threadsql/core/src/test/scala/org/apache/spark/sql/DataFrameSuite.scala Outdated

@mgaido91mgaido91 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fix itself looks fine to me. Just some comments on the test, may you please also re-run the benchmark for the query having a considerable perf issue earlier i order to confirm now we have no regression? Thanks.

}
}

test("SPARK-26572: fix aggregate codegen result evaluation") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a problem with whole stage codegen, waht about moving this test to WholeStageCodegenSuite? And adding an assert that whole stage codegen is actually used, ie. the HashAggregate is a child of WholeStageCodegenExec?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with moving it to WholeStageCodegenSuite but the plan looks like:

*(3) Project [idx#4, id#6L]
+- *(3) BroadcastHashJoin [idx#4], [idx#9], Inner, BuildRight
:- *(3) HashAggregate(keys=[idx#4], functions=[], output=[idx#4, id#6L])
: +- Exchange hashpartitioning(idx#4, 1)
: +- *(1) HashAggregate(keys=[idx#4], functions=[], output=[idx#4])
: +- *(1) Project [value#1 AS idx#4]
: +- LocalTableScan [value#1]
+- BroadcastExchange HashedRelationBroadcastMode(List(cast(input[0, int, false] as bigint)))
+- *(2) Project [value#1 AS idx#9]
+- LocalTableScan [value#1]

so I guess you mean checking WholeStageCodegenExec has a ProjectExec child that has a BroadcastHashJoinExec child?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved and added WholeStageCodegenExec check.

@SparkQA

Copy link
Copy Markdown

Test build #102288 has finished for PR 23731 at commit 5ae9add.

  • This patch fails Spark unit tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@peter-toth

Copy link
Copy Markdown
ContributorAuthor

Hmm, the failing UT doesn't seem to be related to the changes in this PR.

@mgaido91

Copy link
Copy Markdown
Contributor

retest this please

@SparkQA

Copy link
Copy Markdown

Test build #102292 has finished for PR 23731 at commit 5ae9add.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@peter-toth

Copy link
Copy Markdown
ContributorAuthor

the fix itself looks fine to me. Just some comments on the test, may you please also re-run the benchmark for the query having a considerable perf issue earlier i order to confirm now we have no regression? Thanks.

@mgaido91, I checked that the PR now doesn't add pref regression.

@mgaido91

Copy link
Copy Markdown
Contributor

LGTM

@viirya

viirya commented Feb 14, 2019

Copy link
Copy Markdown
Member

Looks good and a minor comment about variable naming.

Change-Id: I1a2c52e7ba30a186517d91568093da813f201d1f
@SparkQA

Copy link
Copy Markdown

Test build #102342 has finished for PR 23731 at commit af861d5.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

cloud-fan pushed a commit that referenced this pull request Feb 14, 2019
This PR is a correctness fix in `HashAggregateExec` code generation. It forces evaluation of result expressions before calling `consume()` to avoid multiple executions.
This PR fixes a use case where an aggregate is nested into a broadcast join and appears on the "stream" side. The issue is that Broadcast join generates it's own loop. And without forcing evaluation of `resultExpressions` of `HashAggregateExec` before the join's loop these expressions can be executed multiple times giving incorrect results.
New UT was added.
Closes#23731 from peter-toth/SPARK-26572.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 2228ee5)
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
cloud-fan pushed a commit that referenced this pull request Feb 14, 2019
This PR is a correctness fix in `HashAggregateExec` code generation. It forces evaluation of result expressions before calling `consume()` to avoid multiple executions.
This PR fixes a use case where an aggregate is nested into a broadcast join and appears on the "stream" side. The issue is that Broadcast join generates it's own loop. And without forcing evaluation of `resultExpressions` of `HashAggregateExec` before the join's loop these expressions can be executed multiple times giving incorrect results.
New UT was added.
Closes#23731 from peter-toth/SPARK-26572.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 2228ee5)
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
@cloud-fan

Copy link
Copy Markdown
Contributor

thanks, merging to master/2.4/2.3!

@peter-toth

Copy link
Copy Markdown
ContributorAuthor

Thanks @cloud-fan@maropu@mgaido91@rednaxelafx and @viirya for your review and help.

jackylee-ch pushed a commit to jackylee-ch/spark that referenced this pull request Feb 18, 2019
## What changes were proposed in this pull request?
This PR is a correctness fix in `HashAggregateExec` code generation. It forces evaluation of result expressions before calling `consume()` to avoid multiple executions.
This PR fixes a use case where an aggregate is nested into a broadcast join and appears on the "stream" side. The issue is that Broadcast join generates it's own loop. And without forcing evaluation of `resultExpressions` of `HashAggregateExec` before the join's loop these expressions can be executed multiple times giving incorrect results.
## How was this patch tested?
New UT was added.
Closesapache#23731 from peter-toth/SPARK-26572.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
kai-chi pushed a commit to kai-chi/spark that referenced this pull request Jul 23, 2019
This PR is a correctness fix in `HashAggregateExec` code generation. It forces evaluation of result expressions before calling `consume()` to avoid multiple executions.
This PR fixes a use case where an aggregate is nested into a broadcast join and appears on the "stream" side. The issue is that Broadcast join generates it's own loop. And without forcing evaluation of `resultExpressions` of `HashAggregateExec` before the join's loop these expressions can be executed multiple times giving incorrect results.
New UT was added.
Closesapache#23731 from peter-toth/SPARK-26572.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 2228ee5)
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
kai-chi pushed a commit to kai-chi/spark that referenced this pull request Jul 25, 2019
This PR is a correctness fix in `HashAggregateExec` code generation. It forces evaluation of result expressions before calling `consume()` to avoid multiple executions.
This PR fixes a use case where an aggregate is nested into a broadcast join and appears on the "stream" side. The issue is that Broadcast join generates it's own loop. And without forcing evaluation of `resultExpressions` of `HashAggregateExec` before the join's loop these expressions can be executed multiple times giving incorrect results.
New UT was added.
Closesapache#23731 from peter-toth/SPARK-26572.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 2228ee5)
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
kai-chi pushed a commit to kai-chi/spark that referenced this pull request Aug 1, 2019
This PR is a correctness fix in `HashAggregateExec` code generation. It forces evaluation of result expressions before calling `consume()` to avoid multiple executions.
This PR fixes a use case where an aggregate is nested into a broadcast join and appears on the "stream" side. The issue is that Broadcast join generates it's own loop. And without forcing evaluation of `resultExpressions` of `HashAggregateExec` before the join's loop these expressions can be executed multiple times giving incorrect results.
New UT was added.
Closesapache#23731 from peter-toth/SPARK-26572.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 2228ee5)
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
otterc pushed a commit to linkedin/spark that referenced this pull request Mar 22, 2023
This PR is a correctness fix in `HashAggregateExec` code generation. It forces evaluation of result expressions before calling `consume()` to avoid multiple executions.
This PR fixes a use case where an aggregate is nested into a broadcast join and appears on the "stream" side. The issue is that Broadcast join generates it's own loop. And without forcing evaluation of `resultExpressions` of `HashAggregateExec` before the join's loop these expressions can be executed multiple times giving incorrect results.
New UT was added.
Closesapache#23731 from peter-toth/SPARK-26572.
Authored-by: Peter Toth <peter.toth@gmail.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>
(cherry picked from commit 2228ee5)
RB=1571578
G=superfriends-reviewers
R=fli,yezhou,edlu,mshen
A=yezhou
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@peter-toth@maropu@mgaido91@SparkQA@dongjoon-hyun@cloud-fan@rednaxelafx@viirya@kiszk