Skip to content

[VL] Disable useHashTableCache if the hash table is not pre-built during HashJoinNode creation#12487

Merged
JkSelf merged 3 commits into
apache:mainfrom
JkSelf:fix-memory-leak
Jul 10, 2026
Merged

[VL] Disable useHashTableCache if the hash table is not pre-built during HashJoinNode creation#12487
JkSelf merged 3 commits into
apache:mainfrom
JkSelf:fix-memory-leak

Conversation

@JkSelf

@JkSelf JkSelf commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What changes are proposed in this pull request?

Because Velox does not release the hash tables in HashTableCache upon query completion (they are only released when the executor terminates), the CI encounters the following flaky error:

2026-07-09T05:01:36.4980422Z E20260709 05:01:36.497267 1021214 VeloxMemoryManager.cc:446] Failed to release Velox memory manager as there are still outstanding memory resources. 
2026-07-09T05:01:36.4983663Z E20260709 05:01:36.497301 1021214 MemoryPool.cpp:481] [MEM] Memory leak (Used memory): Memory Pool[cached_table_15560717 LEAF root[root] parent[root] MALLOC track-usage thread-safe]<unlimited max capacity capacity 8.00MB used 68.00KB available 956.00KB reservation [used 68.00KB, reserved 1.00MB, min 0B] counters [allocs 3, frees 1, reserves 0, releases 0, collisions 0, external-allocs 0, external-frees 0, cumulative-external 0B])>
2026-07-09T05:01:36.4987303Z E20260709 05:01:36.497359 1021214 Exceptions.h:87] Line: /work/cpp/velox/memory/VeloxMemoryManager.cc:108, Function:removePool, Expression: pool->reservedBytes() == 0 (1048576 vs. 0), Source: RUNTIME, ErrorCode: INVALID_STATE
2026-07-09T05:01:36.4989039Z terminate called after throwing an instance of 'facebook::velox::VeloxRuntimeError'
2026-07-09T05:01:36.4992594Z   what():  Exception: VeloxRuntimeError
2026-07-09T05:01:36.4993041Z Error Source: RUNTIME
2026-07-09T05:01:36.4993272Z Error Code: INVALID_STATE
2026-07-09T05:01:36.4993498Z Reason: (1048576 vs. 0)
2026-07-09T05:01:36.4993711Z Retriable: False
2026-07-09T05:01:36.4994093Z Expression: pool->reservedBytes() == 0
2026-07-09T05:01:36.4994432Z Function: removePool
2026-07-09T05:01:36.4994702Z File: /work/cpp/velox/memory/VeloxMemoryManager.cc
2026-07-09T05:01:36.4995003Z Line: 108
2026-07-09T05:01:36.4995183Z Stack trace:
2026-07-09T05:01:36.4995357Z # 0  
2026-07-09T05:01:36.4995516Z # 1  
2026-07-09T05:01:36.4995671Z # 2  
2026-07-09T05:01:36.4995823Z # 3  
2026-07-09T05:01:36.4995978Z # 4  
2026-07-09T05:01:36.4996133Z # 5  
2026-07-09T05:01:36.4996290Z # 6  
2026-07-09T05:01:36.4996446Z # 7  
2026-07-09T05:01:36.4996597Z # 8  
2026-07-09T05:01:36.4996749Z # 9  
2026-07-09T05:01:36.4996901Z # 10 
2026-07-09T05:01:36.4997059Z # 11 
2026-07-09T05:01:36.4997218Z # 12 

This PR adds a check to determine whether the hash table is pre-built. If it is not, useHashTableCache will be disabled automatically.

How was this patch tested?

Existing tests

Was this patch authored or co-authored using generative AI tooling?

No

Copilot AI review requested due to automatic review settings July 10, 2026 03:53
@github-actions github-actions Bot added the VELOX label Jul 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets an unstable Velox memory leak at query end by triggering cleanup of broadcast hash-join hash tables when Spark fires SQLExecutionEnd, and by aligning logging to the new cleanup path.

Changes:

  • On SparkListenerSQLExecutionEnd, traverse the executed plan to collect BroadcastHashJoinExecTransformer build-table IDs and initiate cleanup.
  • Update the warning message in BroadcastHashJoinExecTransformer for the “execution id is null” scenario to reference SQLExecutionEnd cleanup.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
backends-velox/src/main/scala/org/apache/spark/listener/VeloxGlutenSQLAppStatusListener.scala Adds SQLExecutionEnd-based discovery of BHJ broadcast table IDs and triggers per-executor cleanup.
backends-velox/src/main/scala/org/apache/gluten/execution/HashJoinExecTransformer.scala Updates the warning message to reflect the new cleanup trigger at SQL execution end.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +85 to +90
val idSet = new util.HashSet[String]()
bhjIds.foreach(idSet.add)
GlutenDriverEndpoint.executorDataMap.forEach(
(_, executor) => executor.executorEndpointRef.send(GlutenCleanExecutionResource(executionId, idSet)))
}
}
Comment on lines +78 to +82
val qe = event.qe
if (qe != null) {
val bhjIds = qe.executedPlan.collect { case bhj: BroadcastHashJoinExecTransformer =>
bhj.buildBroadcastTableId
}.toSet
Comment on lines +142 to +145
logWarning(
s"Cannot trace broadcast table data $buildBroadcastTableId" +
s" because execution id is null." +
s" Will clean up until expire time.")
s" Will clean up on SQLExecutionEnd.")
@JkSelf
JkSelf force-pushed the fix-memory-leak branch from 48ee3d1 to c26e85f Compare July 10, 2026 04:26
Copilot AI review requested due to automatic review settings July 10, 2026 04:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

backends-velox/src/main/scala/org/apache/gluten/execution/VeloxBroadcastBuildSideCache.scala:98

  • registerLocalCleanupIfNeeded is invoked after buildSideRelationCache.get(...) returns. That leaves a window where another task can decrement the refcount to zero and invalidate the cache entry while this task already obtained the BroadcastHashTable but hasn’t registered its TaskResource yet (potential use-after-free of the native hash table). Registering the task-local cleanup should happen before exposing the hash table to the caller.
    val hashTable = buildSideRelationCache
      .get(
        broadcastContext.buildHashTableId,
        (_: String) => {
          val (pointer, relation, droppedDuplicates) = broadcast.value match {

backends-velox/src/main/scala/org/apache/gluten/execution/VeloxBroadcastBuildSideCache.scala:209

  • deserializeOnExecutor has the same race as getOrBuildBroadcastHashTable: task-local cleanup is registered after the cache entry is returned. Another task can invalidate the broadcast hash table before this task registers its TaskResource, which can lead to clearing the native hash table while it is still in use. Register the cleanup before calling buildSideRelationCache.get(...) so the refcount is bumped before the table is observable.
    val hashTable = buildSideRelationCache.get(
      broadcastHashTableId,
      (_: String) => {
        logInfo(s"Deserializing hash table on executor for broadcast ID: $broadcastHashTableId")
        val startTime = System.currentTimeMillis()

Comment on lines +266 to +290
private def registerLocalCleanupIfNeeded(
broadcastHashTableId: String,
enableLocalHashTableCleanup: Boolean): Unit = {
if (!enableLocalHashTableCleanup || !TaskResources.inSparkTask()) {
return
}

val resourceId = s"broadcast-hash-table-local-cleanup:$broadcastHashTableId"
TaskResources.addResourceIfNotRegistered(
resourceId,
() => {
localReferenceCounts
.computeIfAbsent(broadcastHashTableId, _ => new AtomicInteger(0))
.incrementAndGet()

new TaskResource {
override def release(): Unit = releaseLocalReference(broadcastHashTableId)

override def priority(): Int = 200

override def resourceName(): String = resourceId
}
}
)
}
Comment on lines +292 to +303
private def releaseLocalReference(broadcastHashTableId: String): Unit = {
val refCount = localReferenceCounts.get(broadcastHashTableId)
if (refCount == null) {
return
}

val remaining = refCount.decrementAndGet()
if (remaining <= 0 && localReferenceCounts.remove(broadcastHashTableId, refCount)) {
logInfo(s"Cleaning up broadcast hash table with local fallback: $broadcastHashTableId")
invalidateBroadcastHashtable(broadcastHashTableId)
}
}
Comment on lines +266 to +269
private def registerLocalCleanupIfNeeded(
broadcastHashTableId: String,
enableLocalHashTableCleanup: Boolean): Unit = {
if (!enableLocalHashTableCleanup || !TaskResources.inSparkTask()) {
Copilot AI review requested due to automatic review settings July 10, 2026 07:08
@JkSelf
JkSelf force-pushed the fix-memory-leak branch from c26e85f to 9263d72 Compare July 10, 2026 07:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment on lines 461 to +491
@@ -469,11 +484,11 @@ core::PlanNodePtr SubstraitToVeloxPlanConverter::toVeloxPlan(const ::substrait::
leftNode,
rightNode,
getJoinOutputType(leftNode, rightNode, joinType),
true,
useHashTableCache,
false,
false,
nullptr,
sJoin.hashtableid());
hashTableId);
Comment on lines 461 to +491
@@ -469,11 +484,11 @@ core::PlanNodePtr SubstraitToVeloxPlanConverter::toVeloxPlan(const ::substrait::
leftNode,
rightNode,
getJoinOutputType(leftNode, rightNode, joinType),
true,
useHashTableCache,
false,
false,
nullptr,
sJoin.hashtableid());
hashTableId);
Copilot AI review requested due to automatic review settings July 10, 2026 07:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment on lines +464 to +468
try {
const auto handle = getJoin(hashTableId);
if (handle != 0) {
auto hashTableBuilder = ObjectStore::retrieve<gluten::HashTableBuilder>(handle);
useHashTableCache = (hashTableBuilder != nullptr);
Comment on lines +461 to +465
const std::string hashTableId = sJoin.hashtableid();
bool useHashTableCache = false;
if (!hashTableId.empty()) {
try {
const auto handle = getJoin(hashTableId);
Copilot AI review requested due to automatic review settings July 10, 2026 08:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment on lines +461 to +474
const std::string hashTableId = sJoin.hashtableid();
bool useHashTableCache = false;
if (!hashTableId.empty()) {
try {
const auto handle = getJoin(hashTableId);
if (handle != 0) {
auto hashTableBuilder = ObjectStore::retrieve<gluten::HashTableBuilder>(handle);
useHashTableCache = (hashTableBuilder != nullptr);
}
} catch (const std::exception& e) {
LOG(WARNING) << "Failed to retrieve pre-built HashTableBuilder for cache key: " << hashTableId
<< ", error: " << e.what() << ". Disable hash table cache and build a new table.";
}
}
@JkSelf JkSelf changed the title [VL] Release all the hash table when SQLExecutionEnd end [VL] Disable useHashTableCache if the hash table is not pre-built during HashJoinNode creation Jul 10, 2026
@JkSelf
JkSelf merged commit 7f5ff0d into apache:main Jul 10, 2026
102 of 103 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants