Repository navigation
[VL] Use a dedicated CPU thread pool for parallel broadcast hash-table build - #13115
Conversation
1a20b07 to
9416aa4
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Two critical executor initialization issues and a moderate fallback-sizing issue remain unresolved; documentation also needs updating.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 2
Open (2)
What changed in this PR
This PR adds a dedicated, configurable CPU executor for parallel broadcast hash-table construction, separating it from Velox’s I/O pool.
Changes:
- Routes parallel build and merge work through the new executor.
- Adds native thread-count configuration.
- Adds executor lifecycle management.
| File | Summary |
|---|---|
cpp/velox/jni/VeloxJniWrapper.cc |
Uses the dedicated executor for hash-table build and merge operations. |
cpp/velox/config/VeloxConfig.h |
Defines the new pool-size configuration key. |
cpp/velox/compute/VeloxBackend.h |
Declares executor access and ownership. |
cpp/velox/compute/VeloxBackend.cc |
Creates and tears down the executor; lifecycle, zero-thread fallback, and compatibility sizing require changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@zhztheplayer Could you please take a look if you find a time, thanks. |
5ae1189 to
b1c95b6
Compare
|
Run Gluten Clickhouse CI on x86 |
zhztheplayer
left a comment
There was a problem hiding this comment.
Thanks for the improvement.
cc @JkSelf
| folly::Executor* VeloxBackend::hashTableBuildExecutor() { | ||
| std::call_once(hashTableBuildExecutorInit_, [this] { | ||
| auto numThreads = backendConf_->get<int32_t>(kHashTableBuildThreads, kHashTableBuildThreadsDefault); | ||
| if (numThreads <= 0) { | ||
| // Parallel builds still need a worker when there are no task slots. | ||
| numThreads = std::max<int32_t>(1, backendConf_->get<int32_t>(kNumTaskSlotsPerExecutor, 1)); | ||
| } | ||
| hashTableBuildExecutor_ = std::make_unique<folly::CPUThreadPoolExecutor>(numThreads); | ||
| }); | ||
| return hashTableBuildExecutor_.get(); | ||
| } |
There was a problem hiding this comment.
Minor questions:
- Can we initialize
hashTableBuildExecutor_in VeloxBackend::init()? - Should we set the default value as
numTaskSlotsPerExecutor?
There was a problem hiding this comment.
@zhztheplayer Thanks for your suggestion!
-
hashTableBuildExecutor_is only used during native build, so lazy initialization is a better choice here, what do you think? -
Sounds good! Updated accordingly.
There was a problem hiding this comment.
hashTableBuildExecutor_ is only used during native build, so lazy initialization is a better choice here, what do you think?
The thread creation should already be lazy in folly::CPUThreadPoolExecutor, so I assume std::make_unique<folly::CPUThreadPoolExecutor>(numThreads) is a cheap operation?
I asked the question mainly because it's not aligned with initialization code of other executors.
There was a problem hiding this comment.
Should we set the default value as numTaskSlotsPerExecutor?
Since the driver and executor build hash table share same code path, we need to distinguish the numTaskSlotsPerExecutor and numTaskSlotPerDriver.
There was a problem hiding this comment.
the default value has been updated, and we now distinguish between numTaskSlotsPerExecutor and numTaskSlotPerDriver.
b1c95b6 to
7dd4248
Compare
|
Run Gluten Clickhouse CI on x86 |
7dd4248 to
bcbe53a
Compare
ae64f89 to
8d70bdf
Compare
|
Run Gluten Clickhouse CI on x86 |
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Copilot review overview
15 open findings
The test config forcesbroadcastHashTableBuildThreadsto"1"but later asserts `buildThreads >… · New The test config forcesbroadcastHashTableBuildThreadsto"1"but later asserts `buildThreads >… · New Defaulting the dedicated CPU pool size tonumTaskSlotsPerExecutorcan create a large number of… · New The dedicatedCPUThreadPoolExecutoris created unconditionally duringVeloxBackend::init(),… The test only setskNumTaskSlotsPerExecutor=0but then assertsioExecutor()isnullptr. That… The default column shows<undefined>, but the description states a concrete default behavior… This creates an additional CPU thread pool sized (by default) tonumTaskSlotsPerExecutor, which… This assertion can be brittle because the suite never pins the broadcast hash-table build… The PR description references a different config key name (`spark.gluten.sql.columnar.backend.velox.… Use task slots for driver-side default Fall back to task slots for non-positive thread settings Create the broadcast build thread pool lazily The PR description refers to aVeloxBackend::hashTableBuildExecutor()API, but the actual API… The documented default here is incomplete relative to the behavior introduced in Scala… This introduces a second localconvertToIntthat duplicates the one already embedded in…
3 resolved since last review
🧠 Review effort: Lite
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Copilot review overview
19 open findings
The test forcesbroadcastHashTableBuildThreadsto1but then assertsbuildThreads > 1. With a… · New The test forcesbroadcastHashTableBuildThreadsto1but then assertsbuildThreads > 1. With a… · New The test config forcesbroadcastHashTableBuildThreadsto"1"but later asserts `buildThreads >… The test config forcesbroadcastHashTableBuildThreadsto"1"but later asserts `buildThreads >… This creates an additionalCPUThreadPoolExecutorunconditionally during backend init. With the… · New The default column is set to<undefined>, but the description specifies a concrete defaulting… · New Defaulting the dedicated CPU pool size tonumTaskSlotsPerExecutorcan create a large number of… The dedicatedCPUThreadPoolExecutoris created unconditionally duringVeloxBackend::init(),… The test only setskNumTaskSlotsPerExecutor=0but then assertsioExecutor()isnullptr. That… The default column shows<undefined>, but the description states a concrete default behavior… This creates an additional CPU thread pool sized (by default) tonumTaskSlotsPerExecutor, which… This assertion can be brittle because the suite never pins the broadcast hash-table build… The PR description references a different config key name (`spark.gluten.sql.columnar.backend.velox.… Use task slots for driver-side default Fall back to task slots for non-positive thread settings Create the broadcast build thread pool lazily The PR description refers to aVeloxBackend::hashTableBuildExecutor()API, but the actual API… The documented default here is incomplete relative to the behavior introduced in Scala… This introduces a second localconvertToIntthat duplicates the one already embedded in…
🧠 Review effort: Lite
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
16 open findings
The test config forcesbroadcastHashTableBuildThreadsto"1"but later asserts `buildThreads >… This creates an additional CPU thread pool unconditionally during backend init. Even if parallel… · New This test relies ondynamic_casttofolly::CPUThreadPoolExecutor, which depends on RTTI being… · New The test only setskNumTaskSlotsPerExecutor=0but then assertsioExecutor()isnullptr. That… The default column shows<undefined>, but the description states a concrete default behavior… This creates an additional CPU thread pool sized (by default) tonumTaskSlotsPerExecutor, which… This assertion can be brittle because the suite never pins the broadcast hash-table build… The PR description references a different config key name (`spark.gluten.sql.columnar.backend.velox.… Use task slots for driver-side default Fall back to task slots for non-positive thread settings Create the broadcast build thread pool lazily The PR description says this follows the existingstd::call_oncepattern (like… · New The 'Default' column says<undefined>, but the description states a concrete default behavior… · New The PR description refers to aVeloxBackend::hashTableBuildExecutor()API, but the actual API… The documented default here is incomplete relative to the behavior introduced in Scala… This introduces a second localconvertToIntthat duplicates the one already embedded in…
7 resolved since last review
The test forcesbroadcastHashTableBuildThreadsto1but then assertsbuildThreads > 1. With a… The test forcesbroadcastHashTableBuildThreadsto1but then assertsbuildThreads > 1. With a… The test config forcesbroadcastHashTableBuildThreadsto"1"but later asserts `buildThreads >… The default column is set to<undefined>, but the description specifies a concrete defaulting… This creates an additionalCPUThreadPoolExecutorunconditionally during backend init. With the… Defaulting the dedicated CPU pool size tonumTaskSlotsPerExecutorcan create a large number of… The dedicatedCPUThreadPoolExecutoris created unconditionally duringVeloxBackend::init(),…
🧠 Review effort: Lite
|
Run Gluten Clickhouse CI on x86 |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
18 open findings
The PR description says this follows the existinggetReaderThreadPool()std::call_oncepattern,… · New In this PR,broadcastHashTableBuildThreadsis registered viabuildStaticConf(...)(static).… · New This test relies ondynamic_casttofolly::CPUThreadPoolExecutor, which depends on RTTI being… This creates an additional CPU thread pool unconditionally during backend init. Even if parallel… The test only setskNumTaskSlotsPerExecutor=0but then assertsioExecutor()isnullptr. That… The default column shows<undefined>, but the description states a concrete default behavior… This creates an additional CPU thread pool sized (by default) tonumTaskSlotsPerExecutor, which… This assertion can be brittle because the suite never pins the broadcast hash-table build… The PR description references a different config key name (`spark.gluten.sql.columnar.backend.velox.… Use task slots for driver-side default Fall back to task slots for non-positive thread settings Create the broadcast build thread pool lazily This call fetchesbroadcastHashTableBuildExecutor()again even though anexecutorvariable is… · New The 'Default' column says<undefined>, but the description states a concrete default behavior… The PR description says this follows the existingstd::call_oncepattern (like… The PR description refers to aVeloxBackend::hashTableBuildExecutor()API, but the actual API… The documented default here is incomplete relative to the behavior introduced in Scala… This introduces a second localconvertToIntthat duplicates the one already embedded in…
1 resolved since last review
🧠 Review effort: Lite
|
Run Gluten Clickhouse CI on x86 |
88bb5a8 to
bd472a6
Compare
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Copilot review overview
13 open findings
This creates a dedicated thread pool unconditionally (minimum 1 thread) for every backend instance,… · New The test only setskNumTaskSlotsPerExecutor=0but then assertsioExecutor()isnullptr. That… The default column shows<undefined>, but the description states a concrete default behavior… This creates an additional CPU thread pool sized (by default) tonumTaskSlotsPerExecutor, which… This assertion can be brittle because the suite never pins the broadcast hash-table build… The PR description references a different config key name (`spark.gluten.sql.columnar.backend.velox.… Use task slots for driver-side default Fall back to task slots for non-positive thread settings Create the broadcast build thread pool lazily Other executors in this backend appear to use an explicit queue configuration (e.g.,… · New The PR description refers to aVeloxBackend::hashTableBuildExecutor()API, but the actual API… The documented default here is incomplete relative to the behavior introduced in Scala… This introduces a second localconvertToIntthat duplicates the one already embedded in…
7 resolved since last review
In this PR,broadcastHashTableBuildThreadsis registered viabuildStaticConf(...)(static).… The PR description says this follows the existinggetReaderThreadPool()std::call_oncepattern,… This test relies ondynamic_casttofolly::CPUThreadPoolExecutor, which depends on RTTI being… This creates an additional CPU thread pool unconditionally during backend init. Even if parallel… This call fetchesbroadcastHashTableBuildExecutor()again even though anexecutorvariable is… The 'Default' column says<undefined>, but the description states a concrete default behavior… The PR description says this follows the existingstd::call_oncepattern (like…
🧠 Review effort: Lite
| std::max<int32_t>(1, backendConf_->get<int32_t>(kBroadcastHashTableBuildThreads, numTaskSlotsPerExecutor)); | ||
| broadcastHashTableBuildExecutor_ = std::make_unique<folly::CPUThreadPoolExecutor>(broadcastHashTableBuildThreads); |
|
|
||
| const auto broadcastHashTableBuildThreads = | ||
| std::max<int32_t>(1, backendConf_->get<int32_t>(kBroadcastHashTableBuildThreads, numTaskSlotsPerExecutor)); | ||
| broadcastHashTableBuildExecutor_ = std::make_unique<folly::CPUThreadPoolExecutor>(broadcastHashTableBuildThreads); |
There was a problem hiding this comment.
Note
Copilot was unable to run its full agentic suite in this review.
Copilot review overview
16 open findings
Using a literal value like\"placeholder\"forresourcePathis unclear and can make failures… · New This unconditionally creates an additional CPU thread pool sized (by default) to the executor task… · New The production API exposesfolly::Executor*, but the test relies on… · New This creates a dedicated thread pool unconditionally (minimum 1 thread) for every backend instance,… The test only setskNumTaskSlotsPerExecutor=0but then assertsioExecutor()isnullptr. That… The default column shows<undefined>, but the description states a concrete default behavior… This creates an additional CPU thread pool sized (by default) tonumTaskSlotsPerExecutor, which… This assertion can be brittle because the suite never pins the broadcast hash-table build… The PR description references a different config key name (`spark.gluten.sql.columnar.backend.velox.… Use task slots for driver-side default Fall back to task slots for non-positive thread settings Create the broadcast build thread pool lazily Other executors in this backend appear to use an explicit queue configuration (e.g.,… The PR description refers to aVeloxBackend::hashTableBuildExecutor()API, but the actual API… The documented default here is incomplete relative to the behavior introduced in Scala… This introduces a second localconvertToIntthat duplicates the one already embedded in…
🧠 Review effort: Lite
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
21 open findings
The PR description states 'default behavior is unchanged — this is a pool-isolation change, not a… · New This eagerly creates an additional thread pool sized (by default) similarly to the existing… · New The accessor returns a generic folly::Executor*, but the implementation always constructs a… · New This test relies on dynamic_cast (RTTI) to validate the executor type and inspect numThreads(). If… · New The production API exposesfolly::Executor*, but the test relies on… This unconditionally creates an additional CPU thread pool sized (by default) to the executor task… Using a literal value like\"placeholder\"forresourcePathis unclear and can make failures… This creates a dedicated thread pool unconditionally (minimum 1 thread) for every backend instance,… The test only setskNumTaskSlotsPerExecutor=0but then assertsioExecutor()isnullptr. That… The default column shows<undefined>, but the description states a concrete default behavior… This creates an additional CPU thread pool sized (by default) tonumTaskSlotsPerExecutor, which… This assertion can be brittle because the suite never pins the broadcast hash-table build… The PR description references a different config key name (`spark.gluten.sql.columnar.backend.velox.… Use task slots for driver-side default Fall back to task slots for non-positive thread settings Create the broadcast build thread pool lazily Using a literal value like \"placeholder\" for resourcePath makes the intent unclear and can be… · New Other executors in this backend appear to use an explicit queue configuration (e.g.,… The PR description refers to aVeloxBackend::hashTableBuildExecutor()API, but the actual API… The documented default here is incomplete relative to the behavior introduced in Scala…
And 1 more that still need to be addressed.
🧠 Review effort: Lite
| folly::Executor* broadcastHashTableBuildExecutor() const { | ||
| return broadcastHashTableBuildExecutor_.get(); |
JkSelf
left a comment
There was a problem hiding this comment.
LGTM. Thanks for your fix.



What changes are proposed in this pull request?
The parallel broadcast hash-table build path (numThreads > 1 in HashJoinBuilder.nativeBuild) dispatched its CPU-bound build tasks onto VeloxBackend::ioExecutor(). That executor is reserved for async IO tasks (split preloading, DirectBufferedInput).
This PR introduces a dedicated CPU thread pool (VeloxBackend:: broadcastHashTableBuildExecutor()), following the existing getReaderThreadPool() std::call_once pattern, and switches both places that previously reused the io executor:
The pool size is controlled by a new native config key spark.gluten.sql.columnar.backend.velox.broadcastHashTableBuildThreads. if miss this config falls back to kNumTaskSlotsPerExecutor, i.e. the same sizing this path previously inherited from the io executor. So default behavior is unchanged — this is a pool-isolation change, not a concurrency change. The key is native-only and passed through via the spark.gluten.sql.columnar.backend.velox. prefix, mirroring how spillThreadNum is handled.
How was this patch tested?
GA.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude claude-opus-4-8