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 844241f500 [VL] Use a dedicated CPU thread pool for parallel broadcast 
hash-table build (#13115)
844241f500 is described below

commit 844241f500d65b31b41e7d9f9616324e2f48be9c
Author: Kaifei Yi <[email protected]>
AuthorDate: Sat Oct 17 05:09:06 2026 +0800

    [VL] Use a dedicated CPU thread pool for parallel broadcast hash-table 
build (#13115)
---
 .../org/apache/gluten/config/VeloxConfig.scala     | 10 ++++
 .../joins/GlutenParallelBroadcastJoinSuite.scala   | 67 ++++++++++++++++++++++
 cpp/velox/compute/VeloxBackend.cc                  |  6 ++
 cpp/velox/compute/VeloxBackend.h                   |  8 +++
 cpp/velox/config/VeloxConfig.h                     |  7 +++
 cpp/velox/jni/VeloxJniWrapper.cc                   |  8 +--
 cpp/velox/tests/CMakeLists.txt                     |  1 +
 cpp/velox/tests/VeloxBackendTest.cc                | 48 ++++++++++++++++
 docs/velox-configuration.md                        |  1 +
 9 files changed, 152 insertions(+), 4 deletions(-)

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 6d66080a23..f2d188e818 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
@@ -249,6 +249,16 @@ object VeloxConfig extends ConfigRegistry {
       .intConf
       .createOptional
 
+  val COLUMNAR_VELOX_BROADCAST_HASH_TABLE_BUILD_THREADS =
+    
buildStaticConf("spark.gluten.sql.columnar.backend.velox.broadcastHashTableBuildThreads")
+      .doc(
+        "Size of the dedicated CPU thread pool for the parallel broadcast 
hash-table build. " +
+          "By default, the thread pool is sized to the maximum task slots per 
Spark executor " +
+          "on executors, or to spark.driver.cores on the driver (used when " +
+          "driver-side broadcast hash-table build is enabled), with a minimum 
of one thread.")
+      .intConf
+      .createOptional
+
   val COLUMNAR_VELOX_BROADCAST_HASH_TABLE_BUILD_TARGET_BYTES =
     buildStaticConf("spark.gluten.velox.broadcast.build.targetBytesPerThread")
       .doc(
diff --git 
a/backends-velox/src/test/scala/org/apache/spark/sql/execution/joins/GlutenParallelBroadcastJoinSuite.scala
 
b/backends-velox/src/test/scala/org/apache/spark/sql/execution/joins/GlutenParallelBroadcastJoinSuite.scala
new file mode 100644
index 0000000000..ce15734871
--- /dev/null
+++ 
b/backends-velox/src/test/scala/org/apache/spark/sql/execution/joins/GlutenParallelBroadcastJoinSuite.scala
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.execution.joins
+
+import org.apache.gluten.execution.{BroadcastHashJoinExecTransformer, 
VeloxWholeStageTransformerSuite}
+
+import org.apache.spark.SparkConf
+import org.apache.spark.sql.execution.ColumnarBroadcastExchangeExec
+
+class GlutenParallelBroadcastJoinSuite extends VeloxWholeStageTransformerSuite 
{
+
+  override protected val resourcePath: String = "placeholder"
+  override protected val fileFormat: String = "parquet"
+
+  override def sparkConf: SparkConf = super.sparkConf
+    .set("spark.sql.adaptive.enabled", "false")
+    .set("spark.gluten.sql.columnar.backend.velox.IOThreads", "0")
+    .set("spark.gluten.velox.broadcast.build.targetBytesPerThread", "8KB")
+    
.set("spark.gluten.sql.columnar.backend.velox.broadcastHashTableBuildThreads", 
"4")
+
+  for (buildOnDriver <- Seq(false, true)) {
+    test(s"parallel broadcast hash table build with IO disabled, 
driver=$buildOnDriver") {
+      withSQLConf(
+        
"spark.gluten.sql.columnar.backend.velox.driverSideBroadcastHashTableBuild" ->
+          buildOnDriver.toString,
+        "spark.gluten.velox.minTableRowsForParallelJoinBuild" -> "1"
+      ) {
+        withTable("parallel_broadcast_build") {
+          spark.range(4096)
+            .selectExpr("id", "sha2(cast(id as string), 256) AS value")
+            .write.saveAsTable("parallel_broadcast_build")
+
+          val result = spark.sql("""
+                                   |SELECT /*+ BROADCAST(b) */ p.id, b.value
+                                   |FROM parallel_broadcast_build p JOIN 
parallel_broadcast_build b
+                                   |ON p.id = b.id
+                                   |""".stripMargin)
+          val expected = 
spark.table("parallel_broadcast_build").collect().toSeq
+          checkAnswer(result, expected)
+
+          val plan = result.queryExecution.executedPlan
+          assert(plan.collect { case j: BroadcastHashJoinExecTransformer => j 
}.nonEmpty)
+          val exchanges = plan.collect { case e: ColumnarBroadcastExchangeExec 
=> e }
+          assert(exchanges.size == 1)
+          assert(exchanges.head.metrics("buildThreads").value > 1)
+          if (buildOnDriver) {
+            assert(exchanges.head.metrics("serializedHashTableSize").value > 0)
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/cpp/velox/compute/VeloxBackend.cc 
b/cpp/velox/compute/VeloxBackend.cc
index a3e1dbd9cc..c131c008f4 100644
--- a/cpp/velox/compute/VeloxBackend.cc
+++ b/cpp/velox/compute/VeloxBackend.cc
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#include <algorithm>
 #include <filesystem>
 
 #include "VeloxBackend.h"
@@ -254,6 +255,10 @@ void VeloxBackend::init(
         std::make_unique<folly::CPUThreadPoolExecutor>(ioThreads, 
folly::CPUThreadPoolExecutor::makeLifoSemQueue());
   }
 
+  const auto broadcastHashTableBuildThreads =
+      std::max<int32_t>(1, 
backendConf_->get<int32_t>(kBroadcastHashTableBuildThreads, 
numTaskSlotsPerExecutor));
+  broadcastHashTableBuildExecutor_ = 
std::make_unique<folly::CPUThreadPoolExecutor>(broadcastHashTableBuildThreads);
+
   initJolFilesystem();
 
   velox::dwio::common::registerFileSinks();
@@ -472,6 +477,7 @@ void VeloxBackend::tearDown() {
   spillExecutor_.reset();
   ioExecutor_.reset();
   ssdCacheExecutor_.reset();
+  broadcastHashTableBuildExecutor_.reset();
   globalMemoryManager_.reset();
 
   // dump cache stats on exit if enabled
diff --git a/cpp/velox/compute/VeloxBackend.h b/cpp/velox/compute/VeloxBackend.h
index 09fad04dc4..5e129b9059 100644
--- a/cpp/velox/compute/VeloxBackend.h
+++ b/cpp/velox/compute/VeloxBackend.h
@@ -23,6 +23,7 @@
 #include <boost/uuid/uuid_io.hpp>
 #include <folly/executors/ThreadPoolExecutor.h>
 #include <filesystem>
+#include <mutex>
 
 #include "velox/common/caching/AsyncDataCache.h"
 #include "velox/common/config/Config.h"
@@ -53,6 +54,12 @@ class VeloxBackend {
 
   ReaderThreadPool* getReaderThreadPool();
 
+  /// A dedicated CPU thread pool for the parallel broadcast hash-table build. 
The build is
+  /// CPU-bound, so it must not share ioExecutor() which is reserved for async 
IO tasks.
+  folly::Executor* broadcastHashTableBuildExecutor() const {
+    return broadcastHashTableBuildExecutor_.get();
+  }
+
   std::shared_ptr<facebook::velox::config::ConfigBase> getBackendConf() const {
     return backendConf_;
   }
@@ -135,6 +142,7 @@ class VeloxBackend {
   std::shared_ptr<facebook::velox::config::ConfigBase> backendConf_;
 
   std::unique_ptr<ReaderThreadPool> readerThreadPool_;
+  std::unique_ptr<folly::Executor> broadcastHashTableBuildExecutor_;
 };
 
 } // namespace gluten
diff --git a/cpp/velox/config/VeloxConfig.h b/cpp/velox/config/VeloxConfig.h
index dcb806274b..91069e86a2 100644
--- a/cpp/velox/config/VeloxConfig.h
+++ b/cpp/velox/config/VeloxConfig.h
@@ -169,6 +169,13 @@ const std::string kVeloxAsyncTimeoutOnTaskStopping =
     "spark.gluten.sql.columnar.backend.velox.asyncTimeoutOnTaskStopping";
 const int32_t kVeloxAsyncTimeoutOnTaskStoppingDefault = 30000; // 30s
 
+// By default, executors use the number of task slots per executor, with a
+// minimum of one thread. When driver-side broadcast hash-table build is 
enabled
+// and Spark is not running in local mode, Scala-side logic in VeloxListenerApi
+// overrides the driver default to use spark.driver.cores instead.
+const std::string kBroadcastHashTableBuildThreads =
+    "spark.gluten.sql.columnar.backend.velox.broadcastHashTableBuildThreads";
+
 // udf
 const std::string kVeloxUdfLibraryPaths = 
"spark.gluten.sql.columnar.backend.velox.internal.udfLibraryPaths";
 
diff --git a/cpp/velox/jni/VeloxJniWrapper.cc b/cpp/velox/jni/VeloxJniWrapper.cc
index 1c91c11c18..12e1872376 100644
--- a/cpp/velox/jni/VeloxJniWrapper.cc
+++ b/cpp/velox/jni/VeloxJniWrapper.cc
@@ -1065,9 +1065,9 @@ JNIEXPORT jlong JNICALL 
Java_org_apache_gluten_vectorized_HashJoinBuilder_native
     return gluten::getHashTableObjStore()->save(builder);
   }
 
-  // Use thread pool (executor) instead of creating threads directly.
-  // FIXME: This reuses the io executor which is supposed to only serve async 
IO tasks.
-  auto executor = VeloxBackend::get()->ioExecutor();
+  // Use a dedicated CPU thread pool for the parallel hash-table build. The 
build is CPU-bound,
+  // so it must not run on the io executor which is reserved for async IO 
tasks.
+  auto executor = VeloxBackend::get()->broadcastHashTableBuildExecutor();
 
   std::vector<std::shared_ptr<gluten::HashTableBuilder>> 
hashTableBuilders(numThreads);
   std::vector<std::unique_ptr<facebook::velox::exec::BaseHashTable>> 
otherTables(numThreads);
@@ -1138,7 +1138,7 @@ JNIEXPORT jlong JNICALL 
Java_org_apache_gluten_vectorized_HashJoinBuilder_native
       facebook::velox::exec::BaseHashTable::kNoSpillInputStartPartitionBit,
       hashTableBuilders[0]->joinBuildVectorHasherMaxNumDistinct(),
       hashTableBuilders[0]->dropDuplicates(),
-      allowParallelJoinBuild ? VeloxBackend::get()->ioExecutor() : nullptr);
+      allowParallelJoinBuild ? 
VeloxBackend::get()->broadcastHashTableBuildExecutor() : nullptr);
 
   for (int i = 1; i < numThreads; ++i) {
     if (hashTableBuilders[i]->joinHasNullKeys()) {
diff --git a/cpp/velox/tests/CMakeLists.txt b/cpp/velox/tests/CMakeLists.txt
index 6a10641fbf..369c92f5d4 100644
--- a/cpp/velox/tests/CMakeLists.txt
+++ b/cpp/velox/tests/CMakeLists.txt
@@ -140,6 +140,7 @@ add_velox_test(
 add_velox_test(spark_functions_test SOURCES SparkFunctionTest.cc
                FunctionTest.cc)
 add_velox_test(runtime_test SOURCES RuntimeTest.cc)
+add_velox_test(velox_backend_test SOURCES VeloxBackendTest.cc)
 add_velox_test(velox_memory_test SOURCES MemoryManagerTest.cc)
 add_velox_test(buffer_outputstream_test SOURCES BufferOutputStreamTest.cc)
 add_velox_test(velox_whole_stage_dumper_test SOURCES
diff --git a/cpp/velox/tests/VeloxBackendTest.cc 
b/cpp/velox/tests/VeloxBackendTest.cc
new file mode 100644
index 0000000000..65bacf6f1f
--- /dev/null
+++ b/cpp/velox/tests/VeloxBackendTest.cc
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <folly/executors/CPUThreadPoolExecutor.h>
+#include <folly/futures/Future.h>
+#include <gtest/gtest.h>
+#include <thread>
+
+#include "compute/VeloxBackend.h"
+#include "config/VeloxConfig.h"
+
+namespace gluten {
+
+class VeloxBackendTest : public ::testing::Test {
+ protected:
+  static void SetUpTestSuite() {
+    VeloxBackend::create(AllocationListener::noop(), 
{{kNumTaskSlotsPerExecutor, "0"}});
+  }
+
+  static void TearDownTestSuite() {
+    VeloxBackend::get()->tearDown();
+  }
+};
+
+TEST_F(VeloxBackendTest, broadcastHashTableBuildExecutorWithZeroTaskSlots) {
+  auto* backend = VeloxBackend::get();
+  auto* executor = 
dynamic_cast<folly::CPUThreadPoolExecutor*>(backend->broadcastHashTableBuildExecutor());
+  ASSERT_NE(executor, nullptr);
+  ASSERT_EQ(executor->numThreads(), 1);
+  const auto callerThread = std::this_thread::get_id();
+  EXPECT_NE(folly::via(executor, [] { return std::this_thread::get_id(); 
}).get(), callerThread);
+}
+
+} // namespace gluten
diff --git a/docs/velox-configuration.md b/docs/velox-configuration.md
index 6cc3995645..1966c511fb 100644
--- a/docs/velox-configuration.md
+++ b/docs/velox-configuration.md
@@ -16,6 +16,7 @@ nav_order: 16
 | spark.gluten.sql.columnar.backend.velox.abandonPartialAggregationMinPct      
    | 🔄 Dynamic    | 90                | If partial aggregation aggregationPct 
greater than this value, partial aggregation may be early abandoned. Note: this 
option only works when flushable partial aggregation is enabled. Ignored when 
spark.gluten.sql.columnar.backend.velox.flushablePartialAggregation=false.      
                                                                                
                 [...]
 | spark.gluten.sql.columnar.backend.velox.abandonPartialAggregationMinRows     
    | 🔄 Dynamic    | 100000            | If partial aggregation input rows 
number greater than this value,  partial aggregation may be early abandoned. 
Note: this option only works when flushable partial aggregation is enabled. 
Ignored when 
spark.gluten.sql.columnar.backend.velox.flushablePartialAggregation=false.      
                                                                                
             [...]
 | spark.gluten.sql.columnar.backend.velox.asyncTimeoutOnTaskStopping           
    | âš“ Static      | 30000ms           | Timeout in milliseconds when waiting 
for runtime-scoped async work to finish during teardown.                        
                                                                                
                                                                                
                                                                                
               [...]
+| spark.gluten.sql.columnar.backend.velox.broadcastHashTableBuildThreads       
    | âš“ Static      | &lt;undefined&gt; | Size of the dedicated CPU thread pool 
for the parallel broadcast hash-table build. By default, the thread pool is 
sized to the maximum task slots per Spark executor on executors, or to 
spark.driver.cores on the driver (used when driver-side broadcast hash-table 
build is enabled), with a minimum of one thread.                                
                              [...]
 | 
spark.gluten.sql.columnar.backend.velox.broadcastNLJ.fullOuterRewriteThreshold  
 | 🔄 Dynamic    | 10MB              | Maximum per-side plan size in bytes for 
rewriting a full outer broadcast nested loop join into a left outer join and an 
existence join followed by union. The rewrite is applied only when both sides 
have known statistics and each side is at or below this threshold. Set to -1 to 
disable the rewrite.                                                            
               [...]
 | spark.gluten.sql.columnar.backend.velox.cacheEnabled                         
    | âš“ Static      | false             | Enable Velox cache, default off. It's 
recommended to enablesoft-affinity as well when enable velox cache.             
                                                                                
                                                                                
                                                                                
              [...]
 | spark.gluten.sql.columnar.backend.velox.cachePrefetchMinPct                  
    | âš“ Static      | 0                 | Set prefetch cache min pct for velox 
file scan                                                                       
                                                                                
                                                                                
                                                                                
               [...]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to