peter-toth commented on code in PR #58942:
URL: https://github.com/apache/spark/pull/58942#discussion_r4062966334
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1457,6 +1457,30 @@ object PartitioningCollection {
case other => other.satisfies(required)
}
+ /**
+ * The specs `p` offers for `distribution`, one per member that may serve
it, built the way the
+ * planner builds them (`createShuffleSpec`): a keyed member that covers
part of the operation's
+ * keys contributes the projection onto the keys it covers, which is the
layout the alignment
+ * about to be planned will emit for it. A member that is not keyed has no
projection to make and
+ * is asked for its own. A keyed member is admitted on `keysMaySatisfy`, any
other member on
+ * `satisfies`, and a count the operation pinned is asked as it stands, the
way
+ * `maySatisfyAfterProjection` asks it. That is the planner's admission of a
member
+ * (`EnsureRequirements.createKeyedShuffleSpecs`) less the coverage of every
operation key it
+ * also requires there: that requirement is the skew heuristic of
+ * `spark.sql.requireAllClusterKeysForCoPartition`, while a member covering
a subset of the
+ * operation keys is a sound pairing.
+ */
+ private[sql] def specsForPairing(
Review Comment:
**Finding 2.** Three comments justify a choice by naming
`ValidateRequirements` as the caller that constrains them, and after this PR
none of them is reached from it:
- `partitioning.scala:1389-1395`, in
`PartitioningCollection.createShuffleSpec`: "The set matters because
`ValidateRequirements` builds a spec from a finished plan through here." It
does not any more, since `specsForPairing` flattens the collection and builds
the member specs itself.
- `partitioning.scala:1442-1450`, in `maySatisfyAfterProjection`: "the
caller feeds `ValidateRequirements` as well as the planner, so it does not
widen what a finished plan is checked against." The finished-plan check now
uses a wider admission, so that sentence no longer describes the code (finding
1).
- `partitioning.scala:1690-1696`, in
`SinglePartitionShuffleSpec.isCompatibleWith`: "The one production caller that
can put a collection on the `other` side is `ValidateRequirements`'
`specs.tail.forall(_.isCompatibleWith(specs.head))`, and there the stricter
answer is the safer one." That line is the one this PR deletes, and
`specsForPairing` returns leaf specs only, so the `case
ShuffleSpecCollection(specs) => specs.forall(isCompatibleWith)` arm is now
reachable only from `ShuffleSpecSuite`. The `forall`-versus-`exists` divergence
it documents has no production caller left to be safe for.
The description says the first two are left as they are. Each states an
invariant rather than a pointer, so all three are worth correcting here.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ValidateRequirements.scala:
##########
@@ -45,29 +45,60 @@ object ValidateRequirements extends Logging {
assert(requiredChildDistributions.length == children.length)
assert(requiredChildOrderings.length == children.length)
+ // A `ClusteredDistribution` is the one distribution an operator can owe
its children together
+ // rather than one by one, so an operator with two of them is judged by
their pairing below,
+ // and every other child, an operator with a single clustered child
included, answers for
+ // itself.
+ val coPartitioning = children.length > 1 &&
+ requiredChildDistributions.forall(_.isInstanceOf[ClusteredDistribution])
+
val satisfied =
children.zip(requiredChildDistributions.zip(requiredChildOrderings)).forall {
case (child, (distribution, ordering))
- if !child.outputPartitioning.satisfies(distribution)
+ if (!child.outputPartitioning.satisfies(distribution) &&
+ !(coPartitioning &&
+
PartitioningCollection.representativeOf(child.outputPartitioning).isDefined))
|| !SortOrder.orderingSatisfies(child.outputOrdering, ordering) =>
logDebug(s"ValidateRequirements failed: $distribution,
$ordering\n$plan")
false
case _ => true
}
- if (satisfied && children.length > 1 &&
-
requiredChildDistributions.forall(_.isInstanceOf[ClusteredDistribution])) {
- // Check the co-partitioning requirement.
- val specs =
children.map(_.outputPartitioning).zip(requiredChildDistributions).map {
- case (p, d) =>
p.createShuffleSpec(d.asInstanceOf[ClusteredDistribution])
- }
- if (specs.tail.forall(_.isCompatibleWith(specs.head))) {
- true
- } else {
+ // What a co-partitioning operator reads is the pairing: a pair aligned
without grouping, which
+ // partially clustered distribution builds on purpose, is one the sides
agree on while neither
+ // is grouped. The pairing cannot tell how the two sides hold a key's
rows, since a spread side
Review Comment:
**Finding 4.** Not a request to change this PR: the narrowing in finding 1
uses a configuration as a proxy for the property this comment names, and the
property itself could be carried instead.
What the pairing cannot tell apart is which role an ungrouped side plays,
and the node that made it ungrouped does know: `GroupPartitionsExec` with
`distributePartitions = true` spreads a key's splits, while the other side
repeats its whole group. If `KeyLayout` carried that role, the way it already
carries `mayContainUnknownPartitionKeys` for another per-row property, then
`isCompatibleWith` could require at most one spread side with a repeating
partner, and the validator would accept the sound pair and refuse the unsound
one on its own. No configuration would be read, and it would cover
SPARK-59436's skew-split side for free rather than needing another disjunct.
The counter-arguments are real, which is why this is not a request.
`KeyLayout` is master and `branch-4.x` only, so a fix that should reach
`branch-4.2` cannot be built on it. The marker has to be produced by
`GroupPartitionsExec` and kept across `copy`, and every consumer that compares
layouts has to agree what it means. And it is a larger change than the
validator question that prompted it. Worth its own ticket if you think the
direction is right; happy for it to sit on either of our lists.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1457,6 +1457,30 @@ object PartitioningCollection {
case other => other.satisfies(required)
}
+ /**
+ * The specs `p` offers for `distribution`, one per member that may serve
it, built the way the
+ * planner builds them (`createShuffleSpec`): a keyed member that covers
part of the operation's
+ * keys contributes the projection onto the keys it covers, which is the
layout the alignment
+ * about to be planned will emit for it. A member that is not keyed has no
projection to make and
+ * is asked for its own. A keyed member is admitted on `keysMaySatisfy`, any
other member on
+ * `satisfies`, and a count the operation pinned is asked as it stands, the
way
+ * `maySatisfyAfterProjection` asks it. That is the planner's admission of a
member
+ * (`EnsureRequirements.createKeyedShuffleSpecs`) less the coverage of every
operation key it
+ * also requires there: that requirement is the skew heuristic of
+ * `spark.sql.requireAllClusterKeysForCoPartition`, while a member covering
a subset of the
+ * operation keys is a sound pairing.
+ */
+ private[sql] def specsForPairing(
+ p: Partitioning,
+ distribution: ClusteredDistribution): Seq[ShuffleSpec] =
+ flatten(p).flatMap {
+ case k: KeyedPartitioning =>
+ Option.when(distribution.requiredNumPartitions.forall(_ ==
k.numPartitions) &&
+ k.keysMaySatisfy(distribution))(k.createShuffleSpec(distribution))
Review Comment:
**Finding 1.** `keysMaySatisfy` is `if (isGrouped) keysCanSatisfy else
mayGroupToSatisfy`, so for an ungrouped member it answers "yes, once something
groups it", and this then builds the spec from the ungrouped layout. The
planner may ask that question, because it is about to insert the node:
`createKeyedShuffleSpecs.tryCreate` builds the spec from
`partitioning.toGrouped` (`EnsureRequirements.scala:1135`). A finished plan has
nobody left to insert one.
That is the clause `maySatisfyAfterProjection` keeps, and it was a decision
rather than an omission. SPARK-59289 made `satisfies` strict about a projecting
node and had to widen this filter back for it; it widened the projection and
not the grouping, and wrote down why (`partitioning.scala:1442-1450`): "A
partitioning that is not grouped is not admitted, even though a node would also
group it. ... the caller feeds `ValidateRequirements` as well as the planner,
so it does not widen what a finished plan is checked against."
Your first unit test states the cost: two sides reporting `[1, 1, 2]`,
neither grouped, no partially-clustered conf set, and `validate` says yes. That
layout pair has the two readings your own comment in `validate` names. One side
spread and the other replicating the whole group, which is sound and is what
the rule builds. Or both sides splitting the key between two partitions, which
is not: rows of key `1` in left partition 0 never meet rows of key `1` in right
partition 1. The second is not reachable from `EnsureRequirements` today, which
is why this is Non-blocking, and it is also why the guarantee now rests
entirely on the producer with nothing enforcing it.
The projection half has the same shape: under
`allowKeysSubsetOfPartitionKeys` an ungrouped member contributes
`project(...).toGrouped`, so two sides whose key multiplicities differ can
project onto the same grouped key list and pair, while the plan holds neither
the projection nor the grouping.
Narrowing the widening to the shape that needs it keeps the rest as strict
as it was:
```scala
case k: KeyedPartitioning =>
// An ungrouped side is a plan only partially clustered distribution
builds.
val mayBeUngrouped =
SQLConf.get.v2BucketingPartiallyClusteredDistributionEnabled
Option.when((k.isGrouped || mayBeUngrouped) &&
distribution.requiredNumPartitions.forall(_ == k.numPartitions) &&
k.keysMaySatisfy(distribution))(k.createShuffleSpec(distribution))
```
The per-side exemption in `validateInternal` wants the same condition, and a
test for the refusal in the default configuration would pin it, which is what
your first test asserts the other way round today. One more reason to spell the
condition out rather than admit everything: SPARK-59436 (#58771) will want the
same relaxation for a skew-split side, and its own config then belongs in that
disjunction.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1457,6 +1457,30 @@ object PartitioningCollection {
case other => other.satisfies(required)
}
+ /**
+ * The specs `p` offers for `distribution`, one per member that may serve
it, built the way the
+ * planner builds them (`createShuffleSpec`): a keyed member that covers
part of the operation's
+ * keys contributes the projection onto the keys it covers, which is the
layout the alignment
+ * about to be planned will emit for it. A member that is not keyed has no
projection to make and
+ * is asked for its own. A keyed member is admitted on `keysMaySatisfy`, any
other member on
+ * `satisfies`, and a count the operation pinned is asked as it stands, the
way
+ * `maySatisfyAfterProjection` asks it. That is the planner's admission of a
member
Review Comment:
**Finding 5.** The doc, and the description, say these specs are "built the
way the planner builds them" and that the admission is the planner's "less the
coverage of every operation key it also requires there". There is a second
difference, and it is the one this change turns on:
`createKeyedShuffleSpecs.tryCreate` builds the spec from the *grouped* form of
an ungrouped member, `val grouped = if (partitioning.isGrouped) partitioning
else partitioning.toGrouped` at `EnsureRequirements.scala:1135`, because it is
about to insert the node that groups it. This helper deliberately does not,
which is the right call for a finished plan and worth saying outright instead
of implying an equivalence that does not hold.
Same for "the question `EnsureRequirements` commits a pair on (`committed`,
over the pair `agreeingPairs` picked)": on the push path `committed` is
`sidesDeclareSameKeys`, a `describesSameKeys` comparison of the two children's
declared layouts, and only the `compatibleAsIs` path asks `isCompatibleWith`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]