-
Notifications
You must be signed in to change notification settings - Fork 28.9k
[SPARK-18186] Migrate HiveUDAFFunction to TypedImperativeAggregate for partial aggregation support #15703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SPARK-18186] Migrate HiveUDAFFunction to TypedImperativeAggregate for partial aggregation support #15703
Changes from all commits
c36b9dc
cf639c8
6b4908b
f6a080e
689684a
a6206af
0ab0a06
b418cd7
e88db5c
ca3978c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,16 +17,18 @@ | |
|
|
||
| package org.apache.spark.sql.hive | ||
|
|
||
| import java.nio.ByteBuffer | ||
|
|
||
| import scala.collection.JavaConverters._ | ||
| import scala.collection.mutable.ArrayBuffer | ||
|
|
||
| import org.apache.hadoop.hive.ql.exec._ | ||
| import org.apache.hadoop.hive.ql.udf.{UDFType => HiveUDFType} | ||
| import org.apache.hadoop.hive.ql.udf.generic._ | ||
| import org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator.AggregationBuffer | ||
| import org.apache.hadoop.hive.ql.udf.generic.GenericUDF._ | ||
| import org.apache.hadoop.hive.ql.udf.generic.GenericUDFUtils.ConversionHelper | ||
| import org.apache.hadoop.hive.serde2.objectinspector.{ConstantObjectInspector, ObjectInspector, | ||
| ObjectInspectorFactory} | ||
| import org.apache.hadoop.hive.serde2.objectinspector.{ConstantObjectInspector, ObjectInspector, ObjectInspectorFactory} | ||
| import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.ObjectInspectorOptions | ||
|
|
||
| import org.apache.spark.internal.Logging | ||
|
|
@@ -58,7 +60,7 @@ private[hive] case class HiveSimpleUDF( | |
|
|
||
| @transient | ||
| private lazy val isUDFDeterministic = { | ||
| val udfType = function.getClass().getAnnotation(classOf[HiveUDFType]) | ||
| val udfType = function.getClass.getAnnotation(classOf[HiveUDFType]) | ||
| udfType != null && udfType.deterministic() | ||
| } | ||
|
|
||
|
|
@@ -75,7 +77,7 @@ private[hive] case class HiveSimpleUDF( | |
|
|
||
| @transient | ||
| lazy val unwrapper = unwrapperFor(ObjectInspectorFactory.getReflectionObjectInspector( | ||
| method.getGenericReturnType(), ObjectInspectorOptions.JAVA)) | ||
| method.getGenericReturnType, ObjectInspectorOptions.JAVA)) | ||
|
|
||
| @transient | ||
| private lazy val cached: Array[AnyRef] = new Array[AnyRef](children.length) | ||
|
|
@@ -263,8 +265,35 @@ private[hive] case class HiveGenericUDTF( | |
| } | ||
|
|
||
| /** | ||
| * Currently we don't support partial aggregation for queries using Hive UDAF, which may hurt | ||
| * performance a lot. | ||
| * While being evaluated by Spark SQL, the aggregation state of a Hive UDAF may be in the following | ||
| * three formats: | ||
| * | ||
| * 1. An instance of some concrete `GenericUDAFEvaluator.AggregationBuffer` class | ||
| * | ||
| * This is the native Hive representation of an aggregation state. Hive `GenericUDAFEvaluator` | ||
| * methods like `iterate()`, `merge()`, `terminatePartial()`, and `terminate()` use this format. | ||
| * We call these methods to evaluate Hive UDAFs. | ||
| * | ||
| * 2. A Java object that can be inspected using the `ObjectInspector` returned by the | ||
| * `GenericUDAFEvaluator.init()` method. | ||
| * | ||
| * Hive uses this format to produce a serializable aggregation state so that it can shuffle | ||
| * partial aggregation results. Whenever we need to convert a Hive `AggregationBuffer` instance | ||
| * into a Spark SQL value, we have to convert it to this format first and then do the conversion | ||
| * with the help of `ObjectInspector`s. | ||
| * | ||
| * 3. A Spark SQL value | ||
| * | ||
| * We use this format for serializing Hive UDAF aggregation states on Spark side. To be more | ||
| * specific, we convert `AggregationBuffer`s into equivalent Spark SQL values, write them into | ||
| * `UnsafeRow`s, and then retrieve the byte array behind those `UnsafeRow`s as serialization | ||
| * results. | ||
| * | ||
| * We may use the following methods to convert the aggregation state back and forth: | ||
| * | ||
| * - `wrap()`/`wrapperFor()`: from 3 to 1 | ||
| * - `unwrap()`/`unwrapperFor()`: from 1 to 3 | ||
| * - `GenericUDAFEvaluator.terminatePartial()`: from 2 to 3 | ||
| */ | ||
| private[hive] case class HiveUDAFFunction( | ||
| name: String, | ||
|
|
@@ -273,89 +302,89 @@ private[hive] case class HiveUDAFFunction( | |
| isUDAFBridgeRequired: Boolean = false, | ||
| mutableAggBufferOffset: Int = 0, | ||
| inputAggBufferOffset: Int = 0) | ||
| extends ImperativeAggregate with HiveInspectors { | ||
| extends TypedImperativeAggregate[GenericUDAFEvaluator.AggregationBuffer] with HiveInspectors { | ||
|
|
||
| override def withNewMutableAggBufferOffset(newMutableAggBufferOffset: Int): ImperativeAggregate = | ||
| copy(mutableAggBufferOffset = newMutableAggBufferOffset) | ||
|
|
||
| override def withNewInputAggBufferOffset(newInputAggBufferOffset: Int): ImperativeAggregate = | ||
| copy(inputAggBufferOffset = newInputAggBufferOffset) | ||
|
|
||
| // Hive `ObjectInspector`s for all child expressions (input parameters of the function). | ||
| @transient | ||
| private lazy val resolver = | ||
| if (isUDAFBridgeRequired) { | ||
| private lazy val inputInspectors = children.map(toInspector).toArray | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add docs to explain when these internal vals are used (like which vals are needed for a given mode). |
||
|
|
||
| // Spark SQL data types of input parameters. | ||
| @transient | ||
| private lazy val inputDataTypes: Array[DataType] = children.map(_.dataType).toArray | ||
|
|
||
| private def newEvaluator(): GenericUDAFEvaluator = { | ||
| val resolver = if (isUDAFBridgeRequired) { | ||
| new GenericUDAFBridge(funcWrapper.createFunction[UDAF]()) | ||
| } else { | ||
| funcWrapper.createFunction[AbstractGenericUDAFResolver]() | ||
| } | ||
|
|
||
| @transient | ||
| private lazy val inspectors = children.map(toInspector).toArray | ||
|
|
||
| @transient | ||
| private lazy val functionAndInspector = { | ||
| val parameterInfo = new SimpleGenericUDAFParameterInfo(inspectors, false, false) | ||
| val f = resolver.getEvaluator(parameterInfo) | ||
| f -> f.init(GenericUDAFEvaluator.Mode.COMPLETE, inspectors) | ||
| val parameterInfo = new SimpleGenericUDAFParameterInfo(inputInspectors, false, false) | ||
| resolver.getEvaluator(parameterInfo) | ||
| } | ||
|
|
||
| // The UDAF evaluator used to consume raw input rows and produce partial aggregation results. | ||
| @transient | ||
| private lazy val function = functionAndInspector._1 | ||
| private lazy val partial1ModeEvaluator = newEvaluator() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to make it a lazy val since partialResultInspector is uses it right below?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. It has to be transient and lazy so that it's also available on executor side since Hive UDAF evaluators are not serializable.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok. I think in general we should avoid of using this pattern. If we have to use it now, let's explain it in the comment. |
||
|
|
||
| // Hive `ObjectInspector` used to inspect partial aggregation results. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Partial aggregation result is aggregation buffer, right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea. They are those objects returned by |
||
| @transient | ||
| private lazy val wrappers = children.map(x => wrapperFor(toInspector(x), x.dataType)).toArray | ||
| private val partialResultInspector = partial1ModeEvaluator.init( | ||
| GenericUDAFEvaluator.Mode.PARTIAL1, | ||
| inputInspectors | ||
| ) | ||
|
|
||
| // The UDAF evaluator used to merge partial aggregation results. | ||
| @transient | ||
| private lazy val returnInspector = functionAndInspector._2 | ||
| private lazy val partial2ModeEvaluator = { | ||
| val evaluator = newEvaluator() | ||
| evaluator.init(GenericUDAFEvaluator.Mode.PARTIAL2, Array(partialResultInspector)) | ||
| evaluator | ||
| } | ||
|
|
||
| // Spark SQL data type of partial aggregation results | ||
| @transient | ||
| private lazy val unwrapper = unwrapperFor(returnInspector) | ||
| private lazy val partialResultDataType = inspectorToDataType(partialResultInspector) | ||
|
|
||
| // The UDAF evaluator used to compute the final result from a partial aggregation result objects. | ||
| @transient | ||
| private[this] var buffer: GenericUDAFEvaluator.AggregationBuffer = _ | ||
|
|
||
| override def eval(input: InternalRow): Any = unwrapper(function.evaluate(buffer)) | ||
| private lazy val finalModeEvaluator = newEvaluator() | ||
|
|
||
| // Hive `ObjectInspector` used to inspect the final aggregation result object. | ||
| @transient | ||
| private lazy val inputProjection = new InterpretedProjection(children) | ||
| private val returnInspector = finalModeEvaluator.init( | ||
| GenericUDAFEvaluator.Mode.FINAL, | ||
| Array(partialResultInspector) | ||
| ) | ||
|
|
||
| // Wrapper functions used to wrap Spark SQL input arguments into Hive specific format. | ||
| @transient | ||
| private lazy val cached = new Array[AnyRef](children.length) | ||
| private lazy val inputWrappers = children.map(x => wrapperFor(toInspector(x), x.dataType)).toArray | ||
|
|
||
| // Unwrapper function used to unwrap final aggregation result objects returned by Hive UDAFs into | ||
| // Spark SQL specific format. | ||
| @transient | ||
| private lazy val inputDataTypes: Array[DataType] = children.map(_.dataType).toArray | ||
|
|
||
| // Hive UDAF has its own buffer, so we don't need to occupy a slot in the aggregation | ||
| // buffer for it. | ||
| override def aggBufferSchema: StructType = StructType(Nil) | ||
|
|
||
| override def update(_buffer: InternalRow, input: InternalRow): Unit = { | ||
| val inputs = inputProjection(input) | ||
| function.iterate(buffer, wrap(inputs, wrappers, cached, inputDataTypes)) | ||
| } | ||
|
|
||
| override def merge(buffer1: InternalRow, buffer2: InternalRow): Unit = { | ||
| throw new UnsupportedOperationException( | ||
| "Hive UDAF doesn't support partial aggregate") | ||
| } | ||
| private lazy val resultUnwrapper = unwrapperFor(returnInspector) | ||
|
|
||
| override def initialize(_buffer: InternalRow): Unit = { | ||
| buffer = function.getNewAggregationBuffer | ||
| } | ||
|
|
||
| override val aggBufferAttributes: Seq[AttributeReference] = Nil | ||
| @transient | ||
| private lazy val cached: Array[AnyRef] = new Array[AnyRef](children.length) | ||
|
|
||
| // Note: although this simply copies aggBufferAttributes, this common code can not be placed | ||
| // in the superclass because that will lead to initialization ordering issues. | ||
| override val inputAggBufferAttributes: Seq[AttributeReference] = Nil | ||
| @transient | ||
| private lazy val aggBufferSerDe: AggregationBufferSerDe = new AggregationBufferSerDe | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc |
||
|
|
||
| // We rely on Hive to check the input data types, so use `AnyDataType` here to bypass our | ||
| // catalyst type checking framework. | ||
| override def inputTypes: Seq[AbstractDataType] = children.map(_ => AnyDataType) | ||
|
|
||
| override def nullable: Boolean = true | ||
|
|
||
| override def supportsPartial: Boolean = false | ||
| override def supportsPartial: Boolean = true | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any Hive UDAF that does not support partial aggregation?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. Hive doesn't have an equivalent flag and all UDAFs inherently support partial aggregation since they have to implement callbacks of all phases. |
||
|
|
||
| override lazy val dataType: DataType = inspectorToDataType(returnInspector) | ||
|
|
||
|
|
@@ -365,4 +394,74 @@ private[hive] case class HiveUDAFFunction( | |
| val distinct = if (isDistinct) "DISTINCT " else " " | ||
| s"$name($distinct${children.map(_.sql).mkString(", ")})" | ||
| } | ||
|
|
||
| override def createAggregationBuffer(): AggregationBuffer = | ||
| partial1ModeEvaluator.getNewAggregationBuffer | ||
|
|
||
| @transient | ||
| private lazy val inputProjection = UnsafeProjection.create(children) | ||
|
|
||
| override def update(buffer: AggregationBuffer, input: InternalRow): Unit = { | ||
| partial1ModeEvaluator.iterate( | ||
| buffer, wrap(inputProjection(input), inputWrappers, cached, inputDataTypes)) | ||
| } | ||
|
|
||
| override def merge(buffer: AggregationBuffer, input: AggregationBuffer): Unit = { | ||
| // The 2nd argument of the Hive `GenericUDAFEvaluator.merge()` method is an input aggregation | ||
| // buffer in the 3rd format mentioned in the ScalaDoc of this class. Originally, Hive converts | ||
| // this `AggregationBuffer`s into this format before shuffling partial aggregation results, and | ||
| // calls `GenericUDAFEvaluator.terminatePartial()` to do the conversion. | ||
| partial2ModeEvaluator.merge(buffer, partial1ModeEvaluator.terminatePartial(input)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's explain what we are trying to do using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment added. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we follow the code flow from interfaces.scala, we see that the results of aggregation buffer mode in PARTIAL2 is merged with the aggregation buffer in PARTIAL1. I am new to Spark and Hive, so just wanted to know the reason behind the above behaviour. If there are any docs suggesting this, do let me know. Thank you. |
||
| } | ||
|
|
||
| override def eval(buffer: AggregationBuffer): Any = { | ||
| resultUnwrapper(finalModeEvaluator.terminate(buffer)) | ||
| } | ||
|
|
||
| override def serialize(buffer: AggregationBuffer): Array[Byte] = { | ||
| // Serializes an `AggregationBuffer` that holds partial aggregation results so that we can | ||
| // shuffle it for global aggregation later. | ||
| aggBufferSerDe.serialize(buffer) | ||
| } | ||
|
|
||
| override def deserialize(bytes: Array[Byte]): AggregationBuffer = { | ||
| // Deserializes an `AggregationBuffer` from the shuffled partial aggregation phase to prepare | ||
| // for global aggregation by merging multiple partial aggregation results within a single group. | ||
| aggBufferSerDe.deserialize(bytes) | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add docs to explain what these functions are doing.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
|
||
| // Helper class used to de/serialize Hive UDAF `AggregationBuffer` objects | ||
| private class AggregationBufferSerDe { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we take this class out from HiveUDAFFunction?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can, but it doesn't seem to be necessary. Make it a nested class also simplifies implementation since it has access to fields of the outer class. |
||
| private val partialResultUnwrapper = unwrapperFor(partialResultInspector) | ||
|
|
||
| private val partialResultWrapper = wrapperFor(partialResultInspector, partialResultDataType) | ||
|
|
||
| private val projection = UnsafeProjection.create(Array(partialResultDataType)) | ||
|
||
|
|
||
| private val mutableRow = new GenericInternalRow(1) | ||
|
|
||
| def serialize(buffer: AggregationBuffer): Array[Byte] = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| // `GenericUDAFEvaluator.terminatePartial()` converts an `AggregationBuffer` into an object | ||
| // that can be inspected by the `ObjectInspector` returned by `GenericUDAFEvaluator.init()`. | ||
| // Then we can unwrap it to a Spark SQL value. | ||
| mutableRow.update(0, partialResultUnwrapper(partial1ModeEvaluator.terminatePartial(buffer))) | ||
| val unsafeRow = projection(mutableRow) | ||
| val bytes = ByteBuffer.allocate(unsafeRow.getSizeInBytes) | ||
| unsafeRow.writeTo(bytes) | ||
| bytes.array() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we just use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aren't they equivalent in this case?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but you also create an unnecessary
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually they are different. If the buffer type is fixed length, then the |
||
| } | ||
|
|
||
| def deserialize(bytes: Array[Byte]): AggregationBuffer = { | ||
| // `GenericUDAFEvaluator` doesn't provide any method that is capable to convert an object | ||
| // returned by `GenericUDAFEvaluator.terminatePartial()` back to an `AggregationBuffer`. The | ||
| // workaround here is creating an initial `AggregationBuffer` first and then merge the | ||
| // deserialized object into the buffer. | ||
| val buffer = partial2ModeEvaluator.getNewAggregationBuffer | ||
| val unsafeRow = new UnsafeRow(1) | ||
| unsafeRow.pointTo(bytes, bytes.length) | ||
| val partialResult = unsafeRow.get(0, partialResultDataType) | ||
| partial2ModeEvaluator.merge(buffer, partialResultWrapper(partialResult)) | ||
| buffer | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Besides of explaining what are these three formats, let's also explain when we will use each of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(we can just put the pr description to here)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(is the doc below enough?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, added.