Copilot commented on code in PR #12765:
URL: https://github.com/apache/gluten/pull/12765#discussion_r3925848320


##########
backends-velox/src/main/scala/org/apache/gluten/execution/SerializedBroadcastHashTable.scala:
##########
@@ -82,8 +105,20 @@ class SerializedBroadcastHashTable(
       joinHasNullKeys)
   }
 
+  /**
+   * Frees the off-heap buffer holding the serialized bytes. Only safe once 
the native hash table
+   * has been materialized from it, and only on an executor: on the driver the 
very same object is
+   * still owned by the broadcast variable and by
+   * 
[[VeloxBroadcastBuildSideCache.buildAndSerializeOnDriverInBroadcastExchange]]'s 
cache.
+   */
+  def releaseSerializedData(): Unit = {
+    if (serializedData != null) {
+      serializedData.release()
+    }
+  }
+
   /** Get the size of serialized data in bytes. */
-  def sizeInBytes: Long = serializedData.size()
+  def sizeInBytes: Long = if (serializedData == null) 0L else 
serializedData.size()

Review Comment:
   `releaseSerializedData()` frees the underlying off-heap buffer, but 
`sizeInBytes` will still report the pre-release size because 
`UnsafeByteArray.release()` nulls the buffer without changing the `size` field. 
This can lead to incorrect memory accounting/metrics (and can be misleading in 
logs/diagnostics). Consider returning 0 when `serializedData.isReleased` is 
true, or updating `UnsafeByteArray.release()` to also reset `size` when 
releasing.



##########
cpp/velox/jni/VeloxJniWrapper.cc:
##########
@@ -1208,7 +1220,11 @@ JNIEXPORT jlong JNICALL 
Java_org_apache_gluten_vectorized_HashJoinBuilder_deseri
       reinterpret_cast<const uint8_t*>(address),
       static_cast<size_t>(size),
       static_cast<bool>(ignoreNullKeys),
-      static_cast<bool>(joinHasNullKeys));
+      static_cast<bool>(joinHasNullKeys),
+      // Deserializing on one thread costs about as much as building the table 
from the raw build
+      // side does on all of them, which would leave the driver-side build 
with no upside at all.
+      // FIXME: This reuses the io executor which is supposed to only serve 
async IO tasks.
+      VeloxBackend::get()->ioExecutor());

Review Comment:
   This explicitly schedules CPU-heavy deserialization work onto the IO 
executor (and notes it as a FIXME). That can starve genuine async IO and cause 
query-wide latency regressions under load. Consider introducing (or using, if 
already available) a dedicated CPU executor/thread-pool for hash table 
build/deserialize work, or plumb an appropriate executor from the caller/config 
instead of reusing `ioExecutor()`.



##########
backends-velox/src/main/scala/org/apache/gluten/execution/VeloxBroadcastBuildSideCache.scala:
##########
@@ -199,11 +234,14 @@ object VeloxBroadcastBuildSideCache
     buildSideRelationCache.get(
       broadcastHashTableId,
       (_: String) => {
-        logInfo(s"Deserializing hash table on executor for broadcast ID: 
$broadcastHashTableId")
-        val startTime = System.currentTimeMillis()
-        val hashTableHandle = serialized.deserialize(broadcastHashTableId)
-        val timeMs = System.currentTimeMillis() - startTime
-        deserializeHashTableTimeMetric.foreach(_ += timeMs)
+        val shared = getOrDeserializeShared(
+          serialized,
+          broadcastHashTableId,
+          deserializeHashTableTimeMetric)
+        // Register the shared table under this join's id as well, so that the 
native probe side
+        // can resolve it. The clone holds its own reference to the same 
native table.
+        val hashTableHandle =
+          HashJoinBuilder.cloneHashTable(broadcastHashTableId, shared.pointer)

Review Comment:
   The new shared-deserialization + per-join clone behavior is 
performance-critical and subtly interacts with lifecycle (eviction + native 
ref-counting + `releaseSerializedData()`). Consider adding an integration test 
that forces a reused broadcast exchange with multiple BHJs on the same 
executor, and asserts that deserialization happens once per broadcast (e.g., 
via metrics/log counters) and that subsequent joins use clones successfully 
even after the serialized bytes are released.



##########
backends-velox/src/main/scala/org/apache/spark/sql/execution/SerializedHashTableBroadcastRelation.scala:
##########
@@ -83,9 +93,15 @@ case class SerializedHashTableBroadcastRelation(
   /**
    * Transform is used for DPP (Dynamic Partition Pruning) to extract keys. We 
delegate to the
    * underlying buildSideRelation in the serialized hash table.
+   *
+   * Driver-only, for the same reason as [[deserialized]].
    */
   override def transform(key: Expression): Array[InternalRow] = {
-    serializedHashTable.buildSideRelation.transform(key)
+    val relation = serializedHashTable.buildSideRelation
+    if (relation == null) {
+      throw new IllegalStateException(driverOnlyMessage("transform"))
+    }
+    relation.transform(key)

Review Comment:
   After making `buildSideRelation` transient/not serialized, this relation 
will have `buildSideRelation == null` on executors. Throwing from `transform` 
(and similarly from `deserialized`) is a behavioral break for any Spark paths 
that legitimately call these methods on executors (e.g., DPP key extraction in 
some runtimes). If executors may need these, consider preserving the minimal 
data required for `transform`/`deserialized` in the broadcast payload (instead 
of the full raw relation), or implement these methods using the serialized 
bytes (or explicitly ensure via call sites that they are never invoked 
off-driver).



##########
cpp/velox/jni/VeloxJniWrapper.cc:
##########
@@ -1297,9 +1313,10 @@ JNIEXPORT void JNICALL 
Java_org_apache_gluten_vectorized_HashJoinBuilder_seriali
   auto builder = 
ObjectStore::retrieve<gluten::HashTableBuilder>(hashTableHandle);
   VELOX_CHECK_GT(address, 0, "Serialized hash table buffer address must be 
positive");
   VELOX_CHECK_GE(size, 0, "Serialized hash table buffer size must be 
non-negative");
-  const auto serializedSize = gluten::serializedHashTableSize(builder);
-  VELOX_CHECK_EQ(static_cast<size_t>(size), serializedSize, "Hash table buffer 
size mismatch");
-  gluten::serializeHashTableTo(builder, reinterpret_cast<uint8_t*>(address), 
serializedSize);
+  // Do not recompute serializedHashTableSize() here just to validate 'size': 
that walks every
+  // build row again to re-measure the variable-width columns. serializeTo() 
bounds-checks each
+  // write and asserts that it filled the buffer exactly, which covers the 
same mistake.
+  gluten::serializeHashTableTo(builder, reinterpret_cast<uint8_t*>(address), 
static_cast<size_t>(size));

Review Comment:
   Removing the explicit size equality check changes error reporting and relies 
on internal checks that may be asserts (often compiled out in release builds), 
potentially turning a clear size-mismatch into a harder-to-diagnose failure. If 
recomputing the size is too expensive, consider caching the computed serialized 
size at allocation time (and passing it through), or ensure `serializeTo()` 
uses non-assert runtime checks for both overflow and exact-size agreement so 
mismatches fail deterministically with a clear message.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to