liuneng1994 commented on code in PR #13492:
URL: https://github.com/apache/gluten/pull/13492#discussion_r4409629555
##########
backends-bolt/src-delta/main/scala/org/apache/gluten/component/BoltDeltaComponent.scala:
##########
@@ -37,7 +37,10 @@ class BoltDeltaComponent extends Component {
val legacy = injector.gluten.legacy
legacy.injectTransform {
c =>
- val offload = Seq(OffloadDeltaScan(), OffloadDeltaProject(),
OffloadDeltaFilter())
+ val offload = Seq(
+ OffloadDeltaScan(enableNativeDmlRowIndexScan = true),
Review Comment:
[P1] Keep deletion-vector scans on Spark until Bolt supports their native
splits
On Spark 3.5+, a Delta scan selecting files with deletion vectors produces
`DeltaLocalFilesNode` splits. `BoltPlanConverter::parseLocalFiles` has neither
a `kDelta` case nor deletion-vector handling, so these splits become `UNKNOWN`
rather than Parquet with DV filtering. The shared
`DeltaPostTransformRules.nativeDeletionVectorRule` also removes Spark's
synthetic DV filter once the scan is offloaded.
Could we keep these scans on Spark until Bolt implements both the split
representation and DV filtering? Passing `false` here only guards DV-writing
DML row-index scans; ordinary reads of DV-bearing files need a backend
capability guard too.
##########
backends-bolt/src/main/scala/org/apache/spark/api/python/ColumnarArrowEvalPythonExec.scala:
##########
@@ -368,44 +373,29 @@ case class ColumnarArrowEvalPythonExec(
val joinedVectors = (0 until inputCb.numCols).toArray.map(
i => inputCb.column(i)) ++ (0 until
outputCb.numCols).toArray.map(
i => outputCb.column(i))
- (0 until joinedVectors.length).foreach(
- i => {
-
adjustRefCnt(joinedVectors(i).asInstanceOf[ArrowWritableColumnVector], 1)
- })
val numRows = inputCb.numRows
numOutputBatches += 1
numOutputRows += numRows
val batch = new ColumnarBatch(joinedVectors, numRows)
Review Comment:
[P1] Potential mixed reference counts in the joined Arrow batch
`inputBatchIter` retains the input columns for the cache, while the upstream
iterator can still own its reference. If a Python result arrives before the
writer advances far enough to release that upstream batch, the input columns
can have refcount 2 while the newly decoded output columns have refcount 1.
Joining those columns directly creates a batch that `ColumnarBatches.offload`
cannot accept: `getRefCntHeavy` requires identical reference counts and throws
`IllegalStateException` otherwise.
Could we give the joined result independent, consistently owned vectors
instead of relying on writer timing? A test with a delayed writer and a fast
Python result would help cover this interleaving. This is a static,
timing-dependent concern, not a reproduced failure.
##########
backends-bolt/src/main/scala/org/apache/gluten/execution/ColumnarPartialGenerateExec.scala:
##########
@@ -335,24 +256,11 @@ case class ColumnarPartialGenerateExec(generateExec:
GenerateExec, child: SparkP
return Iterator.empty
}
- val colSizes = Array.fill(generateExec.generatorOutput.length)(0L)
- rowResults.foreach {
- row =>
- for (i <- variableWidthFields) {
- colSizes(i) = colSizes(i) + fieldsSizeGetter(i)(row, i)
- }
- }
-
- val rightResultVectors: Array[ArrowWritableColumnVector] =
- ArrowWritableColumnVector.allocateColumns(rowResults.length, colSizes,
rightSchema)
- val rightTargetRow = new ArrowColumnarRow(rightResultVectors)
-
- rowResults.foreach(row => writeRowUnsafe(row, rightTargetRow))
- rightTargetRow.finishWriteRow()
+ val rightTargetBatch = ArrowColumnVectors.fromRows(rightSchema,
rowResults.iterator)
Review Comment:
[P1] Terminate the generator when the partition ends, including trailing
empty batches
There is an existing termination edge case in the function whose output
conversion is changed here: `generator.terminate()` runs only when the
`hasNext` argument is false, while `doExecuteColumnar` skips empty input
batches without calling this function. With a nonempty batch followed by a
final empty batch, the nonempty batch sees `hasNext = true`, and the empty one
is skipped, so termination never happens. A Hive UDTF that buffers rows until
`close()` loses its final output and close-time cleanup.
Could termination be driven by partition exhaustion independently of batch
contents? A nonempty batch followed by an empty batch should exercise this case.
##########
backends-bolt/src/main/scala/org/apache/gluten/execution/ColumnarRangeExec.scala:
##########
@@ -98,17 +100,17 @@ case class ColumnarRangeExec(range: LogicalRange) extends
ColumnarRangeBaseExec
batchSize
)
- val vectors =
ArrowWritableColumnVector.allocateColumns(numRows, schema)
-
+ val root = ArrowColumnVectors.allocateRoot(schema)
+ val vector = root.getVector(0).asInstanceOf[BigIntVector]
+ vector.allocateNew(numRows)
for (i <- 0 until numRows) {
val value = current + i * step
Review Comment:
[P1] Perform the range multiplications in BigInt before adding them
Although `current` is a `BigInt`, `i * step` is evaluated as `Long` before
the addition; the same applies to `numRows * step` when advancing `current`.
For `range(Long.MinValue, Long.MaxValue, Long.MaxValue, 1)`, the expected
values are `Long.MinValue`, `-1`, and `Long.MaxValue - 1`. Multiplication by 2
already overflows here, so the third value is wrong and advancing the batch can
move `current` backwards instead of exhausting the range.
Please use `BigInt(i) * step` and `BigInt(numRows) * step`. This is existing
arithmetic in the batch-generation function being migrated, and should be
covered with an extreme-step regression test.
##########
backends-bolt/src/main/scala/org/apache/spark/api/python/ColumnarArrowEvalPythonExec.scala:
##########
@@ -368,44 +373,29 @@ case class ColumnarArrowEvalPythonExec(
val joinedVectors = (0 until inputCb.numCols).toArray.map(
i => inputCb.column(i)) ++ (0 until
outputCb.numCols).toArray.map(
i => outputCb.column(i))
- (0 until joinedVectors.length).foreach(
- i => {
-
adjustRefCnt(joinedVectors(i).asInstanceOf[ArrowWritableColumnVector], 1)
- })
val numRows = inputCb.numRows
Review Comment:
[P1] Match Python output to input by rows, not by batch index
The surrounding `zipWithIndex` / `inputCbCache(batchId)` logic assumes one
Python output batch per input batch, but a scalar iterator pandas UDF only
needs to preserve the total number of rows. For example, it can concatenate two
2-row input batches and yield one 4-row output batch. Here that output is
joined with only the first input batch and the result is assigned 2 rows,
dropping the remaining output. Splitting an input batch into smaller outputs
can instead overrun an output vector or exhaust the input cache.
Could we align input/output slices by row count, or explicitly fall back for
iterator UDFs until that is supported? This assumption predates the changed
ownership code, but remains in the restored execution path.
##########
backends-bolt/src/test/scala/org/apache/gluten/execution/BoltTPCHSuite.scala:
##########
@@ -0,0 +1,59 @@
+/*
+ * 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.gluten.execution
+
+import org.apache.gluten.config.{BoltConfig, GlutenConfig}
+
+import org.apache.spark.SparkConf
+
+/** TPC-H fixture shared by Bolt's table-format suites, matching Velox's
default subtype. */
+abstract class BoltTPCHSuite extends BoltWholeStageTransformerSuite {
+ protected val rootPath: String = getClass.getResource("/").getPath
+ override protected val resourcePath: String = "/tpch-data-parquet"
+ override protected val fileFormat: String = "parquet"
+ protected val tpchQueries: String =
+ rootPath +
"../../../../tools/gluten-it/common/src/main/resources/tpch-queries"
+ protected val queriesResults: String = rootPath + "queries-output"
+
+ override protected def sparkConf: SparkConf = {
+ super.sparkConf
+ .set("spark.shuffle.manager",
"org.apache.spark.shuffle.sort.ColumnarShuffleManager")
+ .set("spark.sql.files.maxPartitionBytes", "1g")
+ .set("spark.sql.shuffle.partitions", "1")
+ .set(BoltConfig.COLUMNAR_BOLT_MEM_INIT_CAPACITY.key, "1m")
+ .set("spark.memory.offHeap.size", "2g")
+ .set("spark.unsafe.exceptionOnMemoryLeak", "true")
+ .set("spark.sql.autoBroadcastJoinThreshold", "-1")
+ .set(GlutenConfig.COLUMNAR_FORCE_SHUFFLED_HASH_JOIN_ENABLED.key, "true")
+ }
+
+ override def beforeAll(): Unit = {
+ super.beforeAll()
+ createTPCHNotNullTables()
Review Comment:
[P2] The new setup call exposes a double resource-path resolution in
BoltTPCHDeltaSuite
`BoltTPCHDeltaSuite` sets `resourcePath` to
`getClass.getResource("/tpch-data-parquet").getFile`, which is an absolute
filesystem path. Its override of `createTPCHNotNullTables()` then passes that
path back to `getClass.getResource(resourcePath).getFile`. `getResource`
interprets it as a classpath resource name, so the second lookup returns null
in a normal test checkout and this new `beforeAll` call fails with
`NullPointerException` before any inherited TPC-H query runs.
Please keep `resourcePath` as the classpath name, or use the already
resolved filesystem path directly in the Delta fixture.
##########
backends-bolt/src-uniffle/main/java/org/apache/spark/shuffle/writer/BoltUniffleColumnarShuffleWriter.java:
##########
@@ -224,29 +222,36 @@ public long spill(MemoryTarget self, Spiller.Phase phase,
long size) {
return;
}
long startTime = System.nanoTime();
- GlutenSplitResult splitResult;
+ BoltSplitResult splitResult;
try {
splitResult = shuffleWriterJniWrapper.stop(nativeShuffleWriter);
} catch (IOException e) {
throw new RssException(e);
}
columnarDep.metrics().get("shuffleWallTime").get().add(System.nanoTime() -
startTime);
+ // AQE needs the uncompressed size, including batches flushed by stop().
+ columnarDep
+ .metrics()
+ .get("dataSize")
+ .get()
+ .add(Arrays.stream(splitResult.getRawPartitionLengths()).sum());
+ // Bolt's evict time includes compression; the common writer reported push
time separately.
+ long pushTime =
+ Math.max(0L, splitResult.getTotalPushTime() -
splitResult.getTotalCompressTime());
columnarDep
.metrics()
.get("splitTime")
Review Comment:
[P1] Do not assume every shuffle dependency exposes splitTime
A dependency with at least the configured sort-partition threshold (4000 by
default) can be classified as `SortShuffleWriterType`.
`BoltMetricsApi.genShuffleExchangeMetrics` creates `sortTime` and `c2rTime` for
that type, but not `splitTime`. This unconditional `.get("splitTime").get()`
therefore throws after the native writer stops, before `sendRestBlockAndWait()`
and `sendCommit()` run.
Could we make metric selection consistent with the dependency's writer type,
or ensure this Bolt RSS path constructs a dependency with the matching metrics?
Please include a Uniffle test above the sort threshold.
##########
cpp/bolt/jni/BoltJniWrapper.cc:
##########
@@ -946,15 +946,14 @@ JNIEXPORT jlong JNICALL
Java_org_apache_gluten_shuffle_BoltShuffleWriterJniWrapp
std::shared_ptr<RssClient> rssClient = nullptr;
if (partitionPusher != nullptr) {
- jclass celebornPartitionPusherClass =
- createGlobalClassReferenceOrError(env,
"Lorg/apache/spark/shuffle/CelebornPartitionPusher;");
- jmethodID celebornPushPartitionDataMethod =
- getMethodIdOrError(env, celebornPartitionPusherClass,
"pushPartitionData", "(I[BI)I");
+ jclass partitionPusherClass = env->GetObjectClass(partitionPusher);
Review Comment:
[P2] Release the protobuf byte-array elements in the shared writer-creation
path
Just above this changed pusher lookup, `ParseFromArray` receives a pointer
returned by `GetByteArrayElements(shuffleWriterInfoProto, nullptr)`, but the
pointer is never passed to `ReleaseByteArrayElements`. Each writer creation can
therefore leave a JNI copy allocated or a Java array pinned, including
exceptional exits. Routing Uniffle through this function expands the use of the
existing leak.
Could we use `gluten::getByteArrayElementsSafe`, as the other
protobuf-reading methods in this file do, so the elements are released on every
exit?
##########
cpp/bolt/jni/BoltJniWrapper.cc:
##########
@@ -1006,8 +1005,10 @@ JNIEXPORT void JNICALL
Java_org_apache_gluten_shuffle_BoltShuffleWriterJniWrappe
// The column batch maybe BoltColumnBatch or
ArrowCStructColumnarBatch(FallbackRangeShuffleWriter)
auto batch = ObjectStore::retrieve<ColumnarBatch>(batchHandle);
+ const auto inputBytes = batch->numBytes();
Review Comment:
[P2] Avoid flattening every shuffle batch solely for unused byte accounting
`BoltColumnarBatch::numBytes()` calls `ensureFlattened()`, which
materializes dictionary/constant children and loads lazy vectors. The local and
Celeborn writers discard `write()`'s return value, and Uniffle now uses it only
in a debug log; its `dataSize` metric comes from the final raw partition
lengths instead. Nevertheless, this unconditional call forces all those shuffle
paths to allocate flattened vectors before splitting, destroying compact
encodings for accounting the callers do not use.
Could we remove this calculation, or make it opt-in and use a non-mutating
size estimate where genuinely needed?
##########
backends-bolt/src/test/scala/org/apache/gluten/execution/BoltTPCHSuite.scala:
##########
@@ -0,0 +1,59 @@
+/*
+ * 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.gluten.execution
+
+import org.apache.gluten.config.{BoltConfig, GlutenConfig}
+
+import org.apache.spark.SparkConf
+
+/** TPC-H fixture shared by Bolt's table-format suites, matching Velox's
default subtype. */
+abstract class BoltTPCHSuite extends BoltWholeStageTransformerSuite {
+ protected val rootPath: String = getClass.getResource("/").getPath
+ override protected val resourcePath: String = "/tpch-data-parquet"
+ override protected val fileFormat: String = "parquet"
+ protected val tpchQueries: String =
+ rootPath +
"../../../../tools/gluten-it/common/src/main/resources/tpch-queries"
+ protected val queriesResults: String = rootPath + "queries-output"
+
+ override protected def sparkConf: SparkConf = {
+ super.sparkConf
+ .set("spark.shuffle.manager",
"org.apache.spark.shuffle.sort.ColumnarShuffleManager")
+ .set("spark.sql.files.maxPartitionBytes", "1g")
+ .set("spark.sql.shuffle.partitions", "1")
+ .set(BoltConfig.COLUMNAR_BOLT_MEM_INIT_CAPACITY.key, "1m")
+ .set("spark.memory.offHeap.size", "2g")
+ .set("spark.unsafe.exceptionOnMemoryLeak", "true")
+ .set("spark.sql.autoBroadcastJoinThreshold", "-1")
+ .set(GlutenConfig.COLUMNAR_FORCE_SHUFFLED_HASH_JOIN_ENABLED.key, "true")
+ }
+
+ override def beforeAll(): Unit = {
+ super.beforeAll()
+ createTPCHNotNullTables()
+ }
+
+ (1 to 22).foreach {
+ query =>
+ test(s"TPC-H q$query") {
+ // Like Velox's default subtype, execute every query with
noFallBack=true
+ // without a backend-specific approved plan or a golden result file.
+ runTPCHQuery(query, tpchQueries, queriesResults, compareResult =
false, noFallBack = true) {
Review Comment:
[P2] Explicitly enforce the no-fallback assertion in the new TPC-H tests
This suite inherits `BoltWholeStageTransformerSuite`, whose constructor
calls `disableFallbackCheck`. `GlutenQueryComparisonTest.checkDataFrame`
therefore skips `checkFallBack` even when `runTPCHQuery` receives `noFallBack =
true`. The custom callback is empty as well, so all 22 queries can fall back
entirely to Spark and these tests still pass without demonstrating Bolt
execution compatibility.
Could this suite override `needCheckFallback` to return true, or explicitly
invoke `GlutenQueryComparisonTest.checkFallBack` in the callback?
##########
backends-bolt/src/main/scala/org/apache/gluten/execution/ColumnarRangeExec.scala:
##########
@@ -98,17 +100,17 @@ case class ColumnarRangeExec(range: LogicalRange) extends
ColumnarRangeBaseExec
batchSize
)
- val vectors =
ArrowWritableColumnVector.allocateColumns(numRows, schema)
-
+ val root = ArrowColumnVectors.allocateRoot(schema)
Review Comment:
[P2] Bound the remaining row count before converting it to Int
The `numRows` calculation immediately above this allocation calls `toInt` on
the remaining `BigInt` count before applying the 1000-row batch limit. For a
single-partition `range(0L, 4294967296L, 1L, 1)`, the first count wraps to 0,
and subsequent counts wrap negative; `.max(1)` turns these into one-row Arrow
batches. This makes a valid large range spend billions of iterations allocating
single-row batches.
Please apply the bound in `BigInt` first, e.g.
`remaining.min(BigInt(batchSize)).max(BigInt(1)).toInt`. This is existing
batch-sizing logic in the function being migrated.
##########
backends-bolt/src/main/scala/org/apache/spark/api/python/ColumnarArrowEvalPythonExec.scala:
##########
@@ -103,9 +101,20 @@ class ColumnarArrowPythonRunner(
if (reader != null && batchLoaded) {
batchLoaded = reader.loadNextBatch()
if (batchLoaded) {
- val batch = new ColumnarBatch(vectors)
- batch.setNumRows(root.getRowCount)
- batch
+ // The reader reuses root across batches. Give each result its
own vectors and
+ // retained buffer references so prefetch and reader.close
cannot invalidate it.
+ val recordBatch = new VectorUnloader(root).getRecordBatch
+ val outputRoot = VectorSchemaRoot.create(root.getSchema,
allocator)
+ try {
+ new VectorLoader(outputRoot).load(recordBatch)
+ ArrowColumnVectors.toBatch(outputRoot)
Review Comment:
[P2] Potential leak of independently allocated, prefetched Python output
Spark's `ReaderIterator.hasNext` can invoke `read()` and cache a result
before `next()` transfers it downstream. If a limit, cancellation, or
downstream exception ends iteration at that point, this new `outputRoot` never
reaches `recyclePayload(_.close())`. The task-completion listener closes only
`reader` and its original `root`, not the independently allocated output
vectors.
Could the reader track producer-owned output batches and close any
unconsumed batch during teardown, removing it from that tracking when ownership
is transferred? This is a static lifecycle concern, not a reproduced leak; an
early-stop test after prefetch would validate it.
--
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]