andygrove commented on code in PR #5538:
URL: https://github.com/apache/datafusion-comet/pull/5538#discussion_r3886893616


##########
spark/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala:
##########
@@ -49,6 +50,8 @@ import org.apache.comet.shims.CometTypeShim
 import org.apache.comet.vector.CometVector
 
 object Utils extends CometTypeShim with Logging {
+  private val NATIVE_IPC_LZ4_PREFIX = Array[Byte](0x4c, 0x5a, 0x34, 0x5f)

Review Comment:
   Should the direct-read format follow the configured codec rather than always 
writing LZ4? Native `read_ipc_compressed` handles `SNAP`, `ZSTD` and `NONE` as 
well as `LZ4_`, and `spark.comet.exec.shuffle.compression.codec` already exists 
with a `zstd`/`lz4`/`snappy` check. As it stands someone who configured zstd 
silently gets LZ4 for broadcast.



##########
spark/src/main/scala/org/apache/comet/CometConf.scala:
##########
@@ -229,6 +229,16 @@ object CometConf extends ShimCometConf {
     createExecEnabledConfig("broadcastHashJoin", defaultValue = true)
   val COMET_EXEC_BROADCAST_EXCHANGE_ENABLED: ConfigEntry[Boolean] =
     createExecEnabledConfig("broadcastExchange", defaultValue = true)
+  val COMET_EXEC_BROADCAST_DIRECT_READ_ENABLED: ConfigEntry[Boolean] =
+    conf(s"$COMET_EXEC_CONFIG_PREFIX.broadcast.directRead.enabled")

Review Comment:
   The shuffle equivalent is `spark.comet.shuffle.directRead.enabled`. #4986 
deliberately moved these out of `spark.comet.exec.*` and kept the old name only 
as an alternative, so this lands back under the prefix we are moving away from. 
Should it be `spark.comet.broadcast.directRead.enabled`? Easier to settle now 
than to add a deprecation alias once it has shipped.



##########
spark/src/main/java/org/apache/comet/CometBroadcastBlockIterator.java:
##########
@@ -0,0 +1,109 @@
+/*
+ * 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.comet;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+
+/** Provides codec-prefixed Arrow IPC broadcast blocks to native code via JNI. 
*/
+public final class CometBroadcastBlockIterator extends 
CometShuffleBlockIterator {

Review Comment:
   Extending `CometShuffleBlockIterator` and handing it a throwaway 
`ByteArrayInputStream` means the superclass constructor still allocates a 128KB 
direct `ByteBuffer` and a 16-byte header buffer per instance, neither of which 
this class ever touches, and there is one iterator per consumer partition.
   
   Since every protocol method is overridden anyway, would it be cleaner to 
extract a small interface for the JNI block protocol and have both classes 
implement it? The native `comet_shuffle_block_iterator` bridge would keep 
working, and the inheritance would stop implying shared state that is not there.



##########
spark/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala:
##########
@@ -332,12 +348,38 @@ object Utils extends CometTypeShim with Logging {
       return Iterator.empty
     }
 
-    // use Spark's compression codec (LZ4 by default) and not Comet's 
compression
-    val codec = CompressionCodec.createCodec(SparkEnv.get.conf)
-    val cbbis = bytes.toInputStream()
-    val ins = new DataInputStream(codec.compressedInputStream(cbbis))
     // batches are in Arrow IPC format
-    new ArrowReaderIterator(Channels.newChannel(ins), source)
+    new 
ArrowReaderIterator(Channels.newChannel(compressedIpcInputStream(bytes)), 
source)
+  }
+
+  private def compressedIpcOutputStream(
+      output: ChunkedByteBufferOutputStream,
+      nativeIpc: Boolean): DataOutputStream = {
+    if (nativeIpc) {
+      output.write(NATIVE_IPC_LZ4_PREFIX)
+      new DataOutputStream(new net.jpountz.lz4.LZ4FrameOutputStream(output))
+    } else {
+      val codec = CompressionCodec.createCodec(SparkEnv.get.conf)
+      new DataOutputStream(codec.compressedOutputStream(output))
+    }
+  }
+
+  /**
+   * Opens either the existing Spark-codec stream or the codec-prefixed 
direct-read stream. This
+   * keeps non-native broadcast consumers compatible when direct read is 
enabled.
+   */
+  private def compressedIpcInputStream(bytes: ChunkedByteBuffer): InputStream 
= {
+    val input = new PushbackInputStream(bytes.toInputStream(), 
NATIVE_IPC_LZ4_PREFIX.length)
+    val prefix = new Array[Byte](NATIVE_IPC_LZ4_PREFIX.length)
+    val read = input.read(prefix)

Review Comment:
   `input.read(prefix)` can come back with fewer than 4 bytes, and a short read 
here falls through to the Spark codec branch on what is actually a native-IPC 
stream. I do not think it can happen in practice because the chunks are 1MB, 
but `input.readNBytes(4)` would take the question off the table.



##########
native/core/src/execution/operators/shuffle_scan.rs:
##########
@@ -219,6 +275,14 @@ impl ShuffleScanExec {
                 .map(|col| unpack_dictionary(col))
                 .collect();
 
+            debug_assert_eq!(

Review Comment:
   This looks redundant. `check_column_count` already returns a real error on 
the non-validating path, `validate_remote_schema` covers the validating one, 
and `cast_and_stamp_schema` checks again in `poll_next`. In release builds the 
assert compiles away entirely, and it is the only reason `kind` has to be 
threaded down into `get_next`. Can it come out?



##########
spark/src/main/scala/org/apache/spark/sql/comet/CometBroadcastExchangeExec.scala:
##########
@@ -64,7 +65,8 @@ case class CometBroadcastExchangeExec(
     originalPlan: SparkPlan,
     override val output: Seq[Attribute],
     mode: BroadcastMode,
-    override val child: SparkPlan)
+    override val child: SparkPlan,
+    directRead: Boolean = 
CometConf.COMET_EXEC_BROADCAST_DIRECT_READ_ENABLED.get())

Review Comment:
   Two questions about the conf-reading default here.
   
   There are two existing construction sites that do not pass this parameter 
and will silently inherit whatever the conf says at that moment: 
`CometExecRule.scala:527`, which builds the exchange for 
`CometSubqueryBroadcastExec`, and 
`CometPlanAdaptiveDynamicPruningFilters.scala:279`, whose comment says the 
fresh exchange has to have the same canonical form as the join's exchange so 
AQE's stage cache produces a `ReusedExchangeExec`. Now that `doCanonicalize` 
includes `directRead`, that reuse depends on a session conf resolving 
identically at two different points in planning, one of which is on an AQE 
thread. Dropping the default so the compiler forces each site to state its 
value would make the coupling explicit. My DPP test passes today so this is 
latent rather than live, but it is cheap to close off.
   
   Separately, what does including `directRead` in `doCanonicalize` protect 
against? The serialized format is self-describing and `decodeBatches` sniffs 
the prefix, so two exchanges with different modes look reusable to me anyway.



##########
spark/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala:
##########
@@ -332,12 +348,38 @@ object Utils extends CometTypeShim with Logging {
       return Iterator.empty
     }
 
-    // use Spark's compression codec (LZ4 by default) and not Comet's 
compression
-    val codec = CompressionCodec.createCodec(SparkEnv.get.conf)
-    val cbbis = bytes.toInputStream()
-    val ins = new DataInputStream(codec.compressedInputStream(cbbis))
     // batches are in Arrow IPC format
-    new ArrowReaderIterator(Channels.newChannel(ins), source)
+    new 
ArrowReaderIterator(Channels.newChannel(compressedIpcInputStream(bytes)), 
source)
+  }
+
+  private def compressedIpcOutputStream(
+      output: ChunkedByteBufferOutputStream,
+      nativeIpc: Boolean): DataOutputStream = {
+    if (nativeIpc) {
+      output.write(NATIVE_IPC_LZ4_PREFIX)
+      new DataOutputStream(new net.jpountz.lz4.LZ4FrameOutputStream(output))

Review Comment:
   `LZ4FrameOutputStream(output)` defaults to `BLOCKSIZE.SIZE_4MB`. I measured 
allocation against lz4-java 1.8.0 and it comes to about 12.6MB of heap per 
instance, versus about 135KB with `SIZE_64KB`. `serializeBatches` builds one of 
these per `ColumnarBatch`, and Spark's `LZ4CompressionCodec` on the path this 
replaces uses a 32KB block by default, so this is roughly a 90x jump in 
per-batch allocation.
   
   The scaladoc on `coalesceBroadcastBatches` just below describes 200K tiny 
batches arriving from a 400-task, 500-partition shuffle, which is exactly the 
shape that would hurt. Could we pass `LZ4FrameOutputStream.BLOCKSIZE.SIZE_64KB` 
here? That fixes the read side at the same time, since `LZ4FrameInputStream` 
sizes its buffers from the frame's `BD` byte.



##########
spark/src/main/scala/org/apache/spark/sql/comet/CometBroadcastExchangeExec.scala:
##########
@@ -111,7 +113,11 @@ case class CometBroadcastExchangeExec(
 
   private def getByteArrayRdd(plan: SparkPlan): RDD[(Long, ChunkedByteBuffer)] 
= {
     plan.executeColumnar().mapPartitionsInternal { iter =>
-      Utils.serializeBatches(iter)
+      if (directRead) {
+        Utils.serializeBroadcastBatches(iter)

Review Comment:
   Reading `directRead` inside the lambda makes it capture the plan node. 
`mapPartitionsInternal` does not run `sc.clean`, and I confirmed against the 
compiled class that the closure is now 
`$anonfun$getByteArrayRdd$1(CometBroadcastExchangeExec, Iterator)` where before 
it captured nothing. That ships the whole exchange, including `originalPlan` 
and the child subtree, with every build-side task.
   
   Could you hoist `val useNativeIpc = directRead` above the closure and branch 
on that instead?



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