Uh oh!
There was an error while loading. Please reload this page.
[SPARK-32945][SQL] Avoid collapsing projects if reaching max allowed common exprs - #29950
[SPARK-32945][SQL] Avoid collapsing projects if reaching max allowed common exprs#29950viirya wants to merge 12 commits into
Conversation
| @@ -766,6 +768,23 @@ object CollapseProject extends Rule[LogicalPlan] { | |||
| }) | |||
| } | |||
There was a problem hiding this comment.
We could extend to other cases like case p @ Project(_, agg: Aggregate), but leave it untouched for now.
SparkQA
commented
Oct 6, 2020
Kubernetes integration test starting |
SparkQA
commented
Oct 6, 2020
Kubernetes integration test status success |
maropu
commented
Oct 6, 2020
Related to #29094 ? |
viirya
commented
Oct 6, 2020
No, after did a quick scan of that PR. That PR targets driver OOM caused by too many leaf expressions in collapsed Project. Here this diff cares about duplicated common expressions in collapsed Project. Different problems, I think. |
Perhaps the max number of common expressions is not the best metric here? Lets compare two cases:
Adding more Although I must admit, that in that case we might cache more values for the number of extra computations we save. |
| val maxCommonExprs = SQLConf.get.maxCommonExprsInCollapseProject | ||
| if (haveCommonNonDeterministicOutput(p1.projectList, p2.projectList) || | ||
| getLargestNumOfCommonOutput(p1.projectList, p2.projectList) >= maxCommonExprs) { |
There was a problem hiding this comment.
Perhaps this comparison should be > instead of >=, because currently the actual max value is maxCommonExprs - 1.
SparkQA
commented
Oct 6, 2020
Test build #129432 has finished for PR 29950 at commit
|
Yes, in the case you add the number of redundant computations each time you add one more The number of redundant computations is misleading. If we have 100 |
viirya
commented
Oct 6, 2020
cc @cloud-fan@dongjoon-hyun too |
Uh oh!
There was an error while loading. Please reload this page.
SparkQA
commented
Oct 7, 2020
Kubernetes integration test starting |
SparkQA
commented
Oct 7, 2020
Kubernetes integration test starting |
This comment has been minimized.
This comment has been minimized.
SparkQA
commented
Oct 7, 2020
Kubernetes integration test status success |
SparkQA
commented
Oct 7, 2020
Kubernetes integration test status success |
This comment has been minimized.
This comment has been minimized.
| val query = relation.select( | ||
| JsonToStructs(schema, options, 'json).as("struct")) | ||
| .select( | ||
| GetStructField('struct, 0).as("a"), | ||
| GetStructField('struct, 1).as("b"), | ||
| GetStructField('struct, 2).as("c")).analyze |
There was a problem hiding this comment.
When using the dataset API, then it would be very common to chain withColumn calls:
dataset
.withColumn("json", ...)
.withColumn("a", col("json").getField("a"))
.withColumn("b", col("json").getField("b"))
.withColumn("c", col("json").getField("c"))
In that case the query should look more like this:
val query = relation
.select('json, JsonToStructs(schema, options, 'json).as("struct"))
.select('json, 'struct, GetStructField('struct, 0).as("a"))
.select('json, 'struct, 'a, GetStructField('struct, 1).as("b"))
.select('json, 'struct, 'a, 'b, GetStructField('struct, 2).as("c"))
.analyze
The CollapseProject rule uses transformUp. It seems that in that case we do not get the expected results from this optimization.
There was a problem hiding this comment.
This seems can be fixed by using transformDown instead? Seems to me CollapseProject is not necessarily to use transformUp if I don't miss anything. cc @cloud-fan@maropu
There was a problem hiding this comment.
If there is a chain of projects: P1(P2(P3(P4(...)))), then using transformDown will firstly merge P1 and P2 into P12 and then it will go to its child P3 and merge it with P4 into P34. Only on the second iteration it will merge all 4 of these.
In this case we want to merge P123 and then see, that we can't merge with P4 because we would exceed maxCommonExprsInCollapseProject.
There was a problem hiding this comment.
I think, that correct way would be using transformDown in a similar manner to recursiveRemoveSort in #21072.
So basically when you hit the first Project, then you collect all consecutive Projects until you hit the maxCommonExprsInCollapseProject limit and merge them.
There was a problem hiding this comment.
hm, it sounds fine, too. Rather, it seems a top-down transformation can collapse projects in one shot just like RemoveRedundantProjects?
There was a problem hiding this comment.
Seems like we need to change to transformDown and take a recursive approach like RemoveRedundantProjects and recursiveRemoveSort for collapsing Project.
| "if merging two Project, Spark SQL will skip the merging.") | ||
| .version("3.1.0") | ||
| .intConf | ||
| .createWithDefault(20) |
There was a problem hiding this comment.
Just a question. Is there a reason to choose 20?
There was a problem hiding this comment.
No, just decide a number that seems bad for repeating an expression.
Uh oh!
There was an error while loading. Please reload this page.
| val maxCommonExprs = SQLConf.get.maxCommonExprsInCollapseProject | ||
| if (haveCommonNonDeterministicOutput(p1.projectList, p2.projectList) || | ||
| getLargestNumOfCommonOutput(p1.projectList, p2.projectList) > maxCommonExprs) { |
| } | ||
| def moreThanMaxAllowedCommonOutput( | ||
| expr: Seq[NamedExpression], |
There was a problem hiding this comment.
indentation? It seems that there is one more space here.
| // do not have common non-deterministic expressions, or do not have equal to/more than | ||
| // maximum allowed common outputs. | ||
| if (!hasCommonNonDeterministic(fields, aliases) | ||
| || !moreThanMaxAllowedCommonOutput(fields, aliases)) { |
There was a problem hiding this comment.
nit, you may want to move || into line 157.
This comment has been minimized.
This comment has been minimized.
SparkQA
commented
Oct 9, 2020
Kubernetes integration test starting |
viirya
commented
Nov 1, 2020
gentle ping @dongjoon-hyun@cloud-fan |
dongjoon-hyun
commented
Nov 12, 2020
Oops. Sorry for being late, @viirya . |
dongjoon-hyun
commented
Nov 12, 2020
Retest this please. |
SparkQA
commented
Nov 12, 2020
Kubernetes integration test starting |
SparkQA
commented
Nov 12, 2020
Kubernetes integration test status success |
SparkQA
commented
Nov 12, 2020
Test build #130972 has finished for PR 29950 at commit
|
maropu
commented
Nov 12, 2020
retest this please |
SparkQA
commented
Nov 12, 2020
Kubernetes integration test starting |
SparkQA
commented
Nov 12, 2020
Kubernetes integration test status failure |
SparkQA
commented
Nov 12, 2020
Test build #130983 has finished for PR 29950 at commit
|
| .version("3.1.0") | ||
| .intConf | ||
| .checkValue(_ > 0, "The value of maxCommonExprsInCollapseProject must be larger than zero.") | ||
| .createWithDefault(20) |
There was a problem hiding this comment.
If possible, can we introduce this configuration with Int.MaxValue in 3.1.0 first? We can reduce it later.
SparkQA
commented
Nov 13, 2020
Kubernetes integration test starting |
SparkQA
commented
Nov 13, 2020
Kubernetes integration test status failure |
SparkQA
commented
Nov 13, 2020
Test build #131024 has finished for PR 29950 at commit
|
dongjoon-hyun
commented
Nov 13, 2020
The GitHub Action's flakiness at |
| } else { | ||
| p2.copy(projectList = buildCleanedProjectList(p1.projectList, p2.projectList)) | ||
| } | ||
| def apply(plan: LogicalPlan): LogicalPlan = plan transformDown { |
There was a problem hiding this comment.
Is there a reason to change from transformUp to transformDown? If the all test passed, it would be safe if we keep the original one.
There was a problem hiding this comment.
I found the previous comment about supporting withColumn. If this is designed for that, shall we add a test case for that?
| // If we collapse two Projects, `JsonToStructs` will be repeated three times. | ||
| val relation = LocalRelation('json.string) | ||
| val query1 = relation.select( | ||
| JsonToStructs(schema, options, 'json).as("struct")) |
There was a problem hiding this comment.
indentation? Maybe, the following is better?
-valquery1= relation.select(
-JsonToStructs(schema, options, 'json).as("struct"))
- .select(
+valquery1= relation.select(JsonToStructs(schema, options, 'json).as("struct"))
+ .select(| "the physical planning.") | ||
| .version("3.1.0") | ||
| .intConf | ||
| .checkValue(_ > 0, "The value of maxCommonExprsInCollapseProject must be larger than zero.") |
There was a problem hiding this comment.
larger than zero -> positive.
| }) | ||
| } | ||
| // Whether the largest times common outputs from lower operator used in upper operators is |
There was a problem hiding this comment.
upper operators -> upper operator?
| } | ||
| // Whether the largest times common outputs from lower operator used in upper operators is | ||
| // larger than allowed. |
There was a problem hiding this comment.
than allowed -> than the maximum?
dongjoon-hyun
commented
Nov 25, 2020
Retest this please. |
SparkQA
commented
Nov 25, 2020
Test build #131729 has finished for PR 29950 at commit
|
maropu
commented
Nov 25, 2020
retest this please |
SparkQA
commented
Nov 25, 2020
Test build #131780 has finished for PR 29950 at commit
|
viirya
commented
Dec 7, 2020
I recently generalized subexpression elimination feature to interpreted project and predicate. So now both whole-stage codegen and interpreted execution support subexpression elimination that could avoid the performance issue caused by embedding common expressions from collapsing projects. That's said, I think this patch is less useful for now. I'm closing it now. |
dongjoon-hyun
commented
Dec 8, 2020
Thank you for your decision, @viirya ! |
HyukjinKwon
commented
Dec 9, 2020
Thanks @viirya |
What changes were proposed in this pull request?
This patch proposes to avoid collapsing adjacent
Projectin query optimizer if the combinedProjectwill duplicate too many common expressions. One SQL configspark.sql.optimizer.maxCommonExprsInCollapseProjectis added to set up the maximum allowed number of common expressions.Why are the changes needed?
In some edge cases, collapsing adjacent
Projecthurts performance, instead of improving it. We observed such behavior in our customer Spark jobs where one expensive expression was repeatedly duplicated many times. It is hard to have a optimizer rule that could decide whether to collapse twoProjects because we don't know the cost of each expression. Currently we can provide a SQL config so users can set it up to change optimizer's behavior regarding collapsing adjacentProjects.Note that normally in whole-stage codegen Project operator will de-duplicate expressions internally, but in edge cases Spark cannot do whole-stage codegen and fallback to interpreted mode. In such cases, users can use this config to avoid duplicate expressions.
Does this PR introduce any user-facing change?
Yes. Users can change optimizer's behavior regarding collapsing
Projects by setting SQL config.How was this patch tested?
Unit test.