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


##########
backends-velox/src/main/scala/org/apache/gluten/execution/VeloxBroadcastBuildSideCache.scala:
##########
@@ -212,6 +250,50 @@ object VeloxBroadcastBuildSideCache
     )
   }
 
+  /**
+   * Returns a handle to the native hash table for `serialized`, deserializing 
it at most once per
+   * driver-side broadcast id. The returned handle is owned by 
[[sharedDeserializedCache]]; callers
+   * must clone it rather than releasing it.
+   */
+  private def getOrDeserializeShared(
+      serialized: SerializedBroadcastHashTable,
+      broadcastHashTableId: String,
+      deserializeHashTableTimeMetric: 
Option[org.apache.spark.sql.execution.metric.SQLMetric])
+      : SharedDeserializedHashTable = {
+    // Older payloads, and any path that did not go through the driver-side 
build, carry no
+    // broadcast id. Fall back to keying on the join id, which is what the 
previous behavior was.
+    val sharedKey =
+      if (serialized.broadcastId != null) serialized.broadcastId else 
broadcastHashTableId
+
+    sharedDeserializedCache.get(
+      sharedKey,
+      (key: String) => {
+        logInfo(s"Deserializing hash table on executor for broadcast ID: $key")
+        val startTime = System.currentTimeMillis()
+        val hashTableHandle = serialized.deserialize(key)
+        val timeMs = System.currentTimeMillis() - startTime
+        deserializeHashTableTimeMetric.foreach(_ += timeMs)
+
+        // The serialized bytes have been fully consumed into the native 
table. On an executor
+        // nothing else refers to them, so free the off-heap copy instead of 
waiting for the
+        // broadcast object to be collected. On the driver the same object is 
still owned by the
+        // broadcast variable and by driverSerializedCache, so it must be left 
alone.
+        if (!isDriver) {
+          serialized.releaseSerializedData()
+        }

Review Comment:
   Releasing `serialized`’s off-heap bytes mutates the broadcast value on 
executors and can make later re-materialization impossible if the shared native 
table is evicted (e.g., `expireAfterAccess`) or if Spark reuses the same cached 
broadcast value for a later stage without forcing a re-deserialize from blocks. 
This can surface as runtime failures in 
`SerializedBroadcastHashTable.deserialize` (“already been released”). Prefer 
keeping `serializedData` intact (remove `releaseSerializedData()`), or only 
releasing bytes when you can guarantee the native table will remain available 
for the broadcast’s full usable lifetime (e.g., tie it to broadcast 
lifecycle/unpersist rather than cache eviction).



##########
gluten-arrow/src/main/java/org/apache/spark/sql/execution/unsafe/UnsafeByteArray.java:
##########
@@ -102,12 +120,12 @@ public void read(Kryo kryo, Input input) {
     this.buffer = ArrowBufferAllocators.globalInstance().buffer((int) size);

Review Comment:
   Casting `size` (a `long`) to `int` can truncate for payloads > 2GB, leading 
to an under-allocated `ArrowBuf` and subsequent out-of-bounds reads/writes 
during streaming. Since the serialized format persists `size` as a `long`, add 
an explicit bound check (e.g., fail fast with an exception) before allocating 
and before using the `int index` loop variable. The same issue applies to the 
identical allocation in `readExternal`.



##########
gluten-arrow/src/main/java/org/apache/spark/sql/execution/unsafe/UnsafeByteArray.java:
##########
@@ -45,9 +49,14 @@ public class UnsafeByteArray implements Externalizable, 
KryoSerializable {
 
   public UnsafeByteArray() {}
 
-  private byte[] chunkBuf() {
-    if (chunkBuf == null) {
-      chunkBuf = new byte[CHUNK_SIZE];
+  /**
+   * Returns a scratch buffer of at most {@link #CHUNK_SIZE} bytes, never 
larger than the payload
+   * itself so that relations holding many small batches do not each allocate 
a full-sized chunk.
+   */
+  private byte[] chunkBuf(long dataSize) {
+    final int wanted = (int) Math.max(1, Math.min(CHUNK_SIZE, dataSize));
+    if (chunkBuf == null || chunkBuf.length < wanted) {
+      chunkBuf = new byte[wanted];

Review Comment:
   The doc comment says the scratch buffer is “never larger than the payload 
itself”, but the implementation only grows the buffer and never shrinks it; 
after handling a large payload once, later smaller payloads can still reuse a 
larger-than-payload `chunkBuf`. Either adjust the wording to reflect the 
non-shrinking behavior (recommended to avoid realloc churn), or implement 
shrinking with care (tradeoff: more allocations/GC).



##########
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:
   After `releaseSerializedData()` runs, `serializedData` can be non-null but 
released; `sizeInBytes` will still report the original size, which can be 
misleading for memory accounting/debugging. Consider returning 0 when 
`serializedData.isReleased` is true (or documenting that `sizeInBytes` reflects 
the original payload size, not current resident bytes).



-- 
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