This is an automated email from the ASF dual-hosted git repository.
philo-he pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new 3b08a23683 [GLUTEN-12251][VL] Support merging broadcast batches for
BHJ performance (#12259)
3b08a23683 is described below
commit 3b08a23683cf18fe34c04a99a0609c315c4d8c08
Author: Shilong Duan <[email protected]>
AuthorDate: Wed Jun 17 13:16:18 2026 +0800
[GLUTEN-12251][VL] Support merging broadcast batches for BHJ performance
(#12259)
---
.../org/apache/gluten/config/VeloxConfig.scala | 14 ++++++
.../spark/sql/execution/BroadcastUtils.scala | 53 ++++++++++++++++++++++
.../gluten/execution/VeloxHashJoinSuite.scala | 32 +++++++++++++
cpp/core/jni/JniWrapper.cc | 28 ++++++++++++
docs/velox-configuration.md | 1 +
.../ColumnarBatchSerializerJniWrapper.java | 2 +
6 files changed, 130 insertions(+)
diff --git
a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
index 951eb6b444..13ec2dfebe 100644
--- a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
+++ b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
@@ -65,6 +65,9 @@ class VeloxConfig(conf: SQLConf) extends GlutenConfig(conf) {
def enableBroadcastBuildRelationInOffheap: Boolean =
getConf(VELOX_BROADCAST_BUILD_RELATION_USE_OFFHEAP)
+ def broadcastBuildMergeBatches: Boolean =
+ getConf(VELOX_BROADCAST_BUILD_MERGE_BATCHES)
+
def enableBroadcastBuildOncePerExecutor: Boolean =
getConf(VELOX_BROADCAST_BUILD_HASHTABLE_ONCE_PER_EXECUTOR)
@@ -617,6 +620,17 @@ object VeloxConfig extends ConfigRegistry {
.booleanConf
.createWithDefault(false)
+ val VELOX_BROADCAST_BUILD_MERGE_BATCHES =
+ buildConf("spark.gluten.velox.broadcastBuild.mergeBatches")
+ .doc(
+ "If enabled, all columnar batches in a broadcast build relation will
be " +
+ "serialized into a single buffer to reduce the number of addInput
calls in " +
+ "HashBuild operator. This can significantly improve BHJ performance
when " +
+ "the broadcast table has many small batches, but may increase
driver-side " +
+ "peak memory and is not suitable for very large broadcasts.")
+ .booleanConf
+ .createWithDefault(false)
+
val VELOX_HASHMAP_ABANDON_BUILD_DUPHASH_MIN_ROWS =
buildConf("spark.gluten.velox.abandonDedupHashMap.minRows")
.experimental()
diff --git
a/backends-velox/src/main/scala/org/apache/spark/sql/execution/BroadcastUtils.scala
b/backends-velox/src/main/scala/org/apache/spark/sql/execution/BroadcastUtils.scala
index cf3f9ccca4..9c092e1070 100644
---
a/backends-velox/src/main/scala/org/apache/spark/sql/execution/BroadcastUtils.scala
+++
b/backends-velox/src/main/scala/org/apache/spark/sql/execution/BroadcastUtils.scala
@@ -150,6 +150,7 @@ object BroadcastUtils {
}
def serializeStream(batches: Iterator[ColumnarBatch]):
ColumnarBatchSerializeResult = {
+ val mergeBatches = VeloxConfig.get.broadcastBuildMergeBatches
val filtered = batches
.filter(_.numRows() != 0)
.map(
@@ -157,6 +158,58 @@ object BroadcastUtils {
ColumnarBatches.retain(b)
b
})
+
+ if (mergeBatches) {
+ serializeStreamMerged(filtered)
+ } else {
+ serializeStreamPerBatch(filtered)
+ }
+ }
+
+ private def serializeStreamMerged(
+ filtered: Iterator[ColumnarBatch]): ColumnarBatchSerializeResult = {
+ var numRows: Long = 0
+ val handles = new ArrayBuffer[Long]()
+ val retainedBatches = new ArrayBuffer[ColumnarBatch]()
+ filtered.foreach {
+ b =>
+ numRows += b.numRows()
+ handles +=
ColumnarBatches.getNativeHandle(BackendsApiManager.getBackendName, b)
+ retainedBatches += b
+ }
+ if (handles.nonEmpty) {
+ try {
+ val jniWrapper = ColumnarBatchSerializerJniWrapper
+ .create(
+ Runtimes
+ .contextInstance(BackendsApiManager.getBackendName,
"BroadcastUtils#serializeStream"))
+ val merged = jniWrapper.serializeAll(handles.toArray)
+
+ // Merging produces a single payload. Spark/Gluten broadcast
serialization cannot handle
+ // a single buffer larger than 2GB, so fall back to per-batch
serialization in that case.
+ if (merged.size() > Integer.MAX_VALUE) {
+ merged.toUnsafeByteArray.release()
+ val batchesToSerialize = retainedBatches.toArray
+ retainedBatches.clear()
+ return serializeStreamPerBatch(batchesToSerialize.iterator)
+ }
+
+ val useOffheapBroadcastBuildRelation =
+ VeloxConfig.get.enableBroadcastBuildRelationInOffheap
+ new ColumnarBatchSerializeResult(
+ useOffheapBroadcastBuildRelation,
+ numRows,
+ java.util.Collections.singletonList(merged))
+ } finally {
+ retainedBatches.foreach(ColumnarBatches.release)
+ }
+ } else {
+ ColumnarBatchSerializeResult.EMPTY
+ }
+ }
+
+ private def serializeStreamPerBatch(
+ filtered: Iterator[ColumnarBatch]): ColumnarBatchSerializeResult = {
var numRows: Long = 0
val values = filtered
.map(
diff --git
a/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxHashJoinSuite.scala
b/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxHashJoinSuite.scala
index 86565aa42b..9c3f4f6e4e 100644
---
a/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxHashJoinSuite.scala
+++
b/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxHashJoinSuite.scala
@@ -279,6 +279,38 @@ class VeloxHashJoinSuite extends
VeloxWholeStageTransformerSuite {
}
}
+ test("Broadcast build mergeBatches: merged vs per-batch produce equivalent
results") {
+ Seq("true", "false").foreach {
+ mergeBatches =>
+ withSQLConf(
+ VeloxConfig.VELOX_BROADCAST_BUILD_MERGE_BATCHES.key -> mergeBatches,
+ "spark.sql.autoBroadcastJoinThreshold" -> "10MB",
+ "spark.sql.adaptive.enabled" -> "false",
+ // Force small batches so the build side has multiple batches to
merge.
+ GlutenConfig.COLUMNAR_MAX_BATCH_SIZE.key -> "16"
+ ) {
+ withTable("t_probe", "t_build") {
+ spark.range(200).selectExpr("id as key", "id * 2 as
v").write.saveAsTable("t_probe")
+ spark.range(50).selectExpr("id as key", "id + 1 as
v").write.saveAsTable("t_build")
+
+ val query =
+ """
+ |SELECT p.key, p.v, b.v AS bv
+ |FROM t_probe p JOIN t_build b ON p.key = b.key
+ |ORDER BY p.key
+ |""".stripMargin
+
+ runQueryAndCompare(query) {
+ df =>
+ val plan = df.queryExecution.executedPlan
+ val bhj = plan.collect { case j:
BroadcastHashJoinExecTransformer => j }
+ assert(bhj.nonEmpty, s"Should use BHJ when
mergeBatches=$mergeBatches")
+ }
+ }
+ }
+ }
+ }
+
test("Broadcast join with multiple cast expressions in join keys") {
withSQLConf(
("spark.sql.autoBroadcastJoinThreshold", "10MB"),
diff --git a/cpp/core/jni/JniWrapper.cc b/cpp/core/jni/JniWrapper.cc
index 827c5ad8bd..86da59d2ef 100644
--- a/cpp/core/jni/JniWrapper.cc
+++ b/cpp/core/jni/JniWrapper.cc
@@ -1310,6 +1310,34 @@ JNIEXPORT jobject JNICALL
Java_org_apache_gluten_vectorized_ColumnarBatchSeriali
JNI_METHOD_END(nullptr)
}
+JNIEXPORT jobject JNICALL
Java_org_apache_gluten_vectorized_ColumnarBatchSerializerJniWrapper_serializeAll(
// NOLINT
+ JNIEnv* env,
+ jobject wrapper,
+ jlongArray handles) {
+ JNI_METHOD_START
+ auto ctx = getRuntime(env, wrapper);
+ GLUTEN_CHECK(handles != nullptr, "serializeAll requires non-null handles
array");
+ const jsize numBatches = env->GetArrayLength(handles);
+ GLUTEN_CHECK(numBatches > 0, "serializeAll requires at least one batch");
+
+ auto safeArray = getLongArrayElementsSafe(env, handles);
+ auto serializer = ctx->createColumnarBatchSerializer(nullptr);
+ for (int32_t i = 0; i < numBatches; i++) {
+ auto batch = ObjectStore::retrieve<ColumnarBatch>(safeArray.elems()[i]);
+ GLUTEN_DCHECK(
+ batch != nullptr, "Cannot find the ColumnarBatch with handle " +
std::to_string(safeArray.elems()[i]));
+ serializer->append(batch);
+ }
+ auto serializedSize = serializer->maxSerializedSize();
+ auto byteBuffer = env->CallStaticObjectMethod(jniUnsafeByteBufferClass,
jniUnsafeByteBufferAllocate, serializedSize);
+ auto byteBufferAddress = env->CallLongMethod(byteBuffer,
jniUnsafeByteBufferAddress);
+ auto byteBufferSize = env->CallLongMethod(byteBuffer,
jniUnsafeByteBufferSize);
+ serializer->serializeTo(reinterpret_cast<uint8_t*>(byteBufferAddress),
byteBufferSize);
+
+ return byteBuffer;
+ JNI_METHOD_END(nullptr)
+}
+
// Framed [magic | statsLen | statsBlob | bytesLen | bytesBlob] entry point.
Uses the
// ColumnarBatchSerializer::framedSerializeWithStats virtual hook; non-Velox
backends inherit
// the default empty-vector return so callers fall back to the legacy
serialize() path.
diff --git a/docs/velox-configuration.md b/docs/velox-configuration.md
index 952a6c7f9c..5b67ef4afe 100644
--- a/docs/velox-configuration.md
+++ b/docs/velox-configuration.md
@@ -81,6 +81,7 @@ nav_order: 16
| spark.gluten.sql.enable.enhancedFeatures
| 🔄 Dynamic | true | Enable some features including iceberg
native write and other features.
[...]
| spark.gluten.sql.rewrite.castArrayToString
| 🔄 Dynamic | true | When true, rewrite `cast(array as
String)` to `concat('[', array_join(array, ', ', null), ']')` to allow
offloading to Velox.
[...]
| spark.gluten.velox.broadcast.build.targetBytesPerThread
| âš“ Static | 32MB | It is used to calculate the number of
hash table build threads. Based on our testing across various thresholds (1MB
to 128MB), we recommend a value of 32MB or 64MB, as these consistently provided
the most significant performance gains.
[...]
+| spark.gluten.velox.broadcastBuild.mergeBatches
| 🔄 Dynamic | false | If enabled, all columnar batches in a
broadcast build relation will be serialized into a single buffer to reduce the
number of addInput calls in HashBuild operator. This can significantly improve
BHJ performance when the broadcast table has many small batches, but may
increase driver-side peak memory and is not suitable for very large broadcasts.
|
| spark.gluten.velox.castFromVarcharAddTrimNode
| 🔄 Dynamic | false | If true, will add a trim node which
has the same semantic as vanilla Spark to CAST-from-varchar.Otherwise, do
nothing.
[...]
## Gluten Velox backend *experimental* configurations
diff --git
a/gluten-arrow/src/main/java/org/apache/gluten/vectorized/ColumnarBatchSerializerJniWrapper.java
b/gluten-arrow/src/main/java/org/apache/gluten/vectorized/ColumnarBatchSerializerJniWrapper.java
index 9a5247c823..513eb02ecf 100644
---
a/gluten-arrow/src/main/java/org/apache/gluten/vectorized/ColumnarBatchSerializerJniWrapper.java
+++
b/gluten-arrow/src/main/java/org/apache/gluten/vectorized/ColumnarBatchSerializerJniWrapper.java
@@ -39,6 +39,8 @@ public class ColumnarBatchSerializerJniWrapper implements
RuntimeAware {
public native JniUnsafeByteBuffer serialize(long handle);
+ public native JniUnsafeByteBuffer serializeAll(long[] handles);
+
// Framed [magic | statsLen | statsBlob | bytesLen | bytesBlob] payload
produced by
// VeloxColumnarBatchSerializer::framedSerializeWithStats. Returns byte[]
(not
// JniUnsafeByteBuffer) because the framed wire is small enough that the
simpler return type
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]