This is an automated email from the ASF dual-hosted git repository.
zhouyuan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new 54b1439d2f [GLUTEN-12504][VL] Support dumping and replaying BHJ stages
in the micro benchmark (#12592)
54b1439d2f is described below
commit 54b1439d2f39ccce2d512fb0e140a83b9d13479a
Author: Mohammad Linjawi <[email protected]>
AuthorDate: Fri Oct 23 11:13:42 2026 +0300
[GLUTEN-12504][VL] Support dumping and replaying BHJ stages in the micro
benchmark (#12592)
---
.../backendsapi/velox/VeloxSparkPlanExecApi.scala | 19 ++++++++
.../org/apache/gluten/config/VeloxConfig.scala | 5 +-
.../apache/gluten/config/VeloxConfigSuite.scala | 57 ++++++++++++++++++++++
.../gluten/execution/VeloxHashJoinSuite.scala | 43 +++++++++++++++-
cpp/velox/benchmarks/GenericBenchmark.cc | 10 ++--
cpp/velox/jni/JniHashTable.cc | 20 +++++++-
cpp/velox/jni/JniHashTable.h | 9 +++-
cpp/velox/operators/reader/FileReaderIterator.cc | 3 ++
cpp/velox/operators/reader/FileReaderIterator.h | 16 ++++++
.../operators/writer/VeloxColumnarBatchWriter.cc | 6 ++-
cpp/velox/tests/CMakeLists.txt | 1 +
cpp/velox/tests/JniHashTableTest.cc | 50 +++++++++++++++++++
cpp/velox/tests/VeloxWholeStageDumperTest.cc | 32 ++++++++++++
cpp/velox/utils/VeloxWholeStageDumper.cc | 17 +++++++
docs/backends/velox/developers/MicroBenchmarks.md | 15 +++++-
.../org/apache/gluten/config/GlutenConfig.scala | 4 ++
16 files changed, 296 insertions(+), 11 deletions(-)
diff --git
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala
index eae0ff97f6..387215ca66 100644
---
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala
+++
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala
@@ -68,11 +68,13 @@ import org.apache.commons.lang3.ClassUtils
import java.net.URI
import java.util.{ArrayList => JArrayList, List => JList}
import java.util.Locale
+import java.util.concurrent.atomic.AtomicBoolean
import scala.collection.JavaConverters._
import scala.collection.mutable.ArrayBuffer
class VeloxSparkPlanExecApi extends SparkPlanExecApi with Logging {
+ import VeloxSparkPlanExecApi._
/** Transform GetArrayItem to Substrait. */
override def genGetArrayItemTransformer(
@@ -768,6 +770,17 @@ class VeloxSparkPlanExecApi extends SparkPlanExecApi with
Logging {
// IdentityBroadcastMode
Seq.empty
}
+ if (
+ mode.isInstanceOf[HashedRelationBroadcastMode] &&
+ VeloxConfig.get.benchmarkDumpRequested &&
+
VeloxConfig.get.getConf(VeloxConfig.VELOX_BROADCAST_BUILD_HASHTABLE_ONCE_PER_EXECUTOR)
&&
+ warnedBroadcastBuildOncePerExecutorIgnored.compareAndSet(false, true)
+ ) {
+ logWarning(
+ s"${VeloxConfig.VELOX_BROADCAST_BUILD_HASHTABLE_ONCE_PER_EXECUTOR.key}
is ignored " +
+ "while whole stage dumping is requested: the broadcast hash table is
built per task " +
+ "so that the micro benchmark can replay the dumped build side.")
+ }
var offload = true
val (newChild, newOutput, newBuildKeys) =
if (VeloxConfig.get.enableBroadcastBuildOncePerExecutor) {
@@ -1749,3 +1762,9 @@ class VeloxSparkPlanExecApi extends SparkPlanExecApi with
Logging {
}
}
}
+
+object VeloxSparkPlanExecApi {
+ // A new VeloxSparkPlanExecApi is created per call to
Backend.sparkPlanExecApi(), so state that
+ // must be shared across calls lives here.
+ private val warnedBroadcastBuildOncePerExecutorIgnored = new
AtomicBoolean(false)
+}
diff --git
a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
index f2d188e818..3345d615d0 100644
--- a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
+++ b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
@@ -70,8 +70,11 @@ class VeloxConfig(conf: SQLConf) extends GlutenConfig(conf) {
def broadcastBuildMergeBatches: Boolean =
getConf(VELOX_BROADCAST_BUILD_MERGE_BATCHES)
+ // A hash table built once per executor reaches the native join through a
process local cache
+ // rather than through the build side input iterator, so a dumped stage
could not be replayed by
+ // the micro benchmark. Dumping therefore always builds the hash table per
task.
def enableBroadcastBuildOncePerExecutor: Boolean =
- getConf(VELOX_BROADCAST_BUILD_HASHTABLE_ONCE_PER_EXECUTOR)
+ getConf(VELOX_BROADCAST_BUILD_HASHTABLE_ONCE_PER_EXECUTOR) &&
!benchmarkDumpRequested
def broadcastNestedLoopJoinFullOuterRewriteThreshold: Long =
getConf(VELOX_BROADCAST_NESTED_LOOP_JOIN_FULL_OUTER_REWRITE_THRESHOLD)
diff --git
a/backends-velox/src/test/scala/org/apache/gluten/config/VeloxConfigSuite.scala
b/backends-velox/src/test/scala/org/apache/gluten/config/VeloxConfigSuite.scala
new file mode 100644
index 0000000000..f91724dbcc
--- /dev/null
+++
b/backends-velox/src/test/scala/org/apache/gluten/config/VeloxConfigSuite.scala
@@ -0,0 +1,57 @@
+/*
+ * 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.config
+
+import org.apache.spark.sql.internal.SQLConf
+
+import org.scalatest.funsuite.AnyFunSuite
+
+class VeloxConfigSuite extends AnyFunSuite {
+
+ private def veloxConfig(entries: (String, String)*): VeloxConfig = {
+ val conf = new SQLConf
+ entries.foreach { case (k, v) => conf.setConfString(k, v) }
+ new VeloxConfig(conf)
+ }
+
+ private val buildOncePerExecutor =
+ VeloxConfig.VELOX_BROADCAST_BUILD_HASHTABLE_ONCE_PER_EXECUTOR.key
+
+ test("build hash table once per executor is enabled by default") {
+ val conf = veloxConfig()
+ assert(!conf.benchmarkDumpRequested)
+ assert(conf.enableBroadcastBuildOncePerExecutor)
+ }
+
+ test("whole stage dumping builds the broadcast hash table per task") {
+ // GLUTEN-12504: the micro benchmark can only replay a broadcast hash join
whose build side
+ // was streamed through the input iterator, and so dumped.
+ Seq(
+ GlutenConfig.BENCHMARK_TASK_STAGEID.key -> "1",
+ GlutenConfig.BENCHMARK_TASK_TASK_ID.key -> "3"
+ ).foreach {
+ dumpTrigger =>
+ val conf = veloxConfig(dumpTrigger, buildOncePerExecutor -> "true")
+ assert(conf.benchmarkDumpRequested, dumpTrigger)
+ assert(!conf.enableBroadcastBuildOncePerExecutor, dumpTrigger)
+ }
+ }
+
+ test("build hash table once per executor can still be disabled explicitly") {
+ assert(!veloxConfig(buildOncePerExecutor ->
"false").enableBroadcastBuildOncePerExecutor)
+ }
+}
diff --git
a/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxHashJoinSuite.scala
b/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxHashJoinSuite.scala
index 25e5df6e98..8f837cc506 100644
---
a/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxHashJoinSuite.scala
+++
b/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxHashJoinSuite.scala
@@ -22,10 +22,11 @@ import org.apache.gluten.sql.shims.SparkShimLoader
import org.apache.spark.{SparkConf, SparkException}
import org.apache.spark.sql.Row
import org.apache.spark.sql.catalyst.expressions.AttributeReference
-import org.apache.spark.sql.execution.{ColumnarBroadcastExchangeExec,
ColumnarSubqueryBroadcastExec, InputIteratorTransformer,
SerializedHashTableBroadcastRelation}
+import org.apache.spark.sql.execution.{ColumnarBroadcastExchangeExec,
ColumnarBuildSideRelation, ColumnarSubqueryBroadcastExec,
InputIteratorTransformer, SerializedHashTableBroadcastRelation}
import org.apache.spark.sql.execution.exchange.ReusedExchangeExec
import org.apache.spark.sql.execution.joins.BuildSideRelation
import org.apache.spark.sql.execution.joins.HashedRelationBroadcastMode
+import org.apache.spark.sql.execution.unsafe.UnsafeColumnarBuildSideRelation
import org.apache.spark.sql.functions.broadcast
class VeloxHashJoinSuite extends VeloxWholeStageTransformerSuite {
@@ -487,6 +488,46 @@ class VeloxHashJoinSuite extends
VeloxWholeStageTransformerSuite {
}
}
+ test("GLUTEN-12504: whole stage dumping streams the broadcast build side") {
+ // The micro benchmark can only replay a broadcast hash join whose build
side reaches the native
+ // join through its input iterator. A task id that matches no task
requests dumping without
+ // dumping anything.
+ withSQLConf(
+ ("spark.sql.autoBroadcastJoinThreshold", "10MB"),
+ ("spark.sql.adaptive.enabled", "false"),
+ (VeloxConfig.VELOX_BROADCAST_BUILD_HASHTABLE_ONCE_PER_EXECUTOR.key,
"true"),
+ (VeloxConfig.VELOX_DRIVER_SIDE_BROADCAST_HASH_TABLE_BUILD.key, "true"),
+ (GlutenConfig.BENCHMARK_TASK_TASK_ID.key, "-1")
+ ) {
+ withTable("dump_fact", "dump_dim") {
+ spark.range(0, 400).selectExpr("id as id", "id % 40 as
k").write.saveAsTable("dump_fact")
+ spark.range(0, 40).selectExpr("id as k", "id * 3 as
v").write.saveAsTable("dump_dim")
+
+ val query =
+ """
+ |SELECT /*+ BROADCAST(d) */ f.id, d.v
+ |FROM dump_fact f
+ |JOIN dump_dim d
+ | ON f.k = d.k
+ |""".stripMargin
+
+ runQueryAndCompare(query) {
+ df =>
+ val plan = df.queryExecution.executedPlan
+ assert(plan.collect { case bhj: BroadcastHashJoinExecTransformer
=> bhj }.size == 1)
+
+ val relations = collectBroadcastRelations(df)
+ assert(relations.size == 1)
+ relations.head match {
+ case columnar: ColumnarBuildSideRelation =>
assert(!columnar.offload)
+ case unsafe: UnsafeColumnarBuildSideRelation =>
assert(!unsafe.isOffload)
+ case other => fail(s"Expected the build side to be streamed, got
${other.getClass}")
+ }
+ }
+ }
+ }
+ }
+
test("Broadcast join with multiple cast expressions in join keys") {
withSQLConf(
("spark.sql.autoBroadcastJoinThreshold", "10MB"),
diff --git a/cpp/velox/benchmarks/GenericBenchmark.cc
b/cpp/velox/benchmarks/GenericBenchmark.cc
index feaf1b551f..e71870f122 100644
--- a/cpp/velox/benchmarks/GenericBenchmark.cc
+++ b/cpp/velox/benchmarks/GenericBenchmark.cc
@@ -443,7 +443,8 @@ auto BM_Generic = [](::benchmark::State& state,
inputIters.end(),
std::back_inserter(inputItersRaw),
[](std::shared_ptr<gluten::ResultIterator> iter) {
- return static_cast<FileReaderIterator*>(iter->getInputIter());
+ // Null for an empty input marker, which reads no file.
+ return dynamic_cast<FileReaderIterator*>(iter->getInputIter());
});
}
*Runtime::localWriteFilesTempPath() = FLAGS_write_path;
@@ -495,7 +496,7 @@ auto BM_Generic = [](::benchmark::State& state,
readInputTime +=
std::accumulate(inputItersRaw.begin(), inputItersRaw.end(), 0,
[](int64_t sum, FileReaderIterator* iter) {
- return sum + iter->getCollectBatchTime();
+ return iter == nullptr ? sum : sum + iter->getCollectBatchTime();
});
auto* rawIter =
static_cast<gluten::WholeStageResultIterator*>(resultIter->getInputIter());
@@ -551,8 +552,9 @@ auto BM_ShuffleWriteRead = [](::benchmark::State& state,
localDirs,
dataFileDir);
- auto reader =
static_cast<FileReaderIterator*>(resultIter->getInputIter());
- readInputTime += reader->getCollectBatchTime();
+ if (auto* reader =
dynamic_cast<FileReaderIterator*>(resultIter->getInputIter())) {
+ readInputTime += reader->getCollectBatchTime();
+ }
}
}
diff --git a/cpp/velox/jni/JniHashTable.cc b/cpp/velox/jni/JniHashTable.cc
index 7db693e3d0..0e5481360a 100644
--- a/cpp/velox/jni/JniHashTable.cc
+++ b/cpp/velox/jni/JniHashTable.cc
@@ -45,14 +45,21 @@ void JniHashTableContext::finalize(JNIEnv* env) {
}
}
-jlong JniHashTableContext::callJavaGet(const std::string& id) const {
+std::optional<jlong> JniHashTableContext::callJavaGet(const std::string& id)
const {
+ if (vm_ == nullptr) {
+ return std::nullopt;
+ }
+
JNIEnv* env;
if (vm_->GetEnv(reinterpret_cast<void**>(&env), jniVersion) != JNI_OK) {
throw gluten::GlutenException("JNIEnv was not attached to current thread");
}
const jstring s = env->NewStringUTF(id.c_str());
+ checkException(env);
auto result = env->CallStaticLongMethod(jniVeloxBroadcastBuildSideCache_,
jniGet_, s);
+ env->DeleteLocalRef(s);
+ checkException(env);
return result;
}
@@ -161,7 +168,16 @@ std::shared_ptr<HashTableBuilder> nativeHashTableBuild(
}
long getJoin(const std::string& hashTableId) {
- return JniHashTableContext::getInstance().callJavaGet(hashTableId);
+ const auto handle =
JniHashTableContext::getInstance().callJavaGet(hashTableId);
+ if (!handle.has_value()) {
+ // The JVM side VeloxBroadcastBuildSideCache holding the pre-built hash
tables is unreachable,
+ // e.g. in the standalone micro benchmark. Report a miss, the same way
that cache reports one,
+ // and let the caller build the hash table from the join's build side
input instead.
+ LOG(WARNING) << "No JVM is attached to this process, cannot look up the
pre-built hash table for cache key: "
+ << hashTableId << ". A new hash table will be built from the
build side input.";
+ return 0;
+ }
+ return handle.value();
}
size_t serializedHashTableSize(std::shared_ptr<HashTableBuilder> builder) {
diff --git a/cpp/velox/jni/JniHashTable.h b/cpp/velox/jni/JniHashTable.h
index 48bed2b4f0..531b8c864a 100644
--- a/cpp/velox/jni/JniHashTable.h
+++ b/cpp/velox/jni/JniHashTable.h
@@ -18,6 +18,7 @@
#pragma once
#include <jni.h>
+#include <optional>
#include "memory/ColumnarBatch.h"
#include "memory/VeloxMemoryManager.h"
#include "operators/hashjoin/HashTableBuilder.h"
@@ -52,7 +53,11 @@ class JniHashTableContext {
return hashTableObjStore_.get();
}
- jlong callJavaGet(const std::string& id) const;
+ // Returns the handle registered under the given id by the JVM side
+ // VeloxBroadcastBuildSideCache, or std::nullopt when that cache cannot be
reached at all because
+ // no JVM is attached to this process. The latter is the case for the
standalone micro benchmark
+ // and the native unit tests, where JNI_OnLoad never runs.
+ std::optional<jlong> callJavaGet(const std::string& id) const;
private:
JniHashTableContext() : hashTableObjStore_(ObjectStore::create()) {}
@@ -89,6 +94,8 @@ std::shared_ptr<HashTableBuilder> nativeHashTableBuild(
std::vector<std::shared_ptr<ColumnarBatch>>& batches,
std::shared_ptr<facebook::velox::memory::MemoryPool> memoryPool);
+// Returns the handle of the pre-built hash table registered for the given id,
or 0 if there is
+// none. Safe to call from a process with no JVM attached, in which case it
always returns 0.
long getJoin(const std::string& hashTableId);
// Return the exact serialized hash table size for direct buffer allocation.
diff --git a/cpp/velox/operators/reader/FileReaderIterator.cc
b/cpp/velox/operators/reader/FileReaderIterator.cc
index 9528763fbc..10da04159f 100644
--- a/cpp/velox/operators/reader/FileReaderIterator.cc
+++ b/cpp/velox/operators/reader/FileReaderIterator.cc
@@ -31,6 +31,9 @@ std::shared_ptr<gluten::ResultIterator>
FileReaderIterator::getInputIteratorFrom
std::shared_ptr<facebook::velox::memory::MemoryPool> pool) {
std::filesystem::path input{path};
auto suffix = input.extension().string();
+ if (suffix == kEmptyInputMarkerSuffix) {
+ return
std::make_shared<gluten::ResultIterator>(std::make_unique<EmptyInputMarkerIterator>());
+ }
if (suffix == kParquetSuffix) {
if (readerType == FileReaderType::kStream) {
return std::make_shared<gluten::ResultIterator>(
diff --git a/cpp/velox/operators/reader/FileReaderIterator.h
b/cpp/velox/operators/reader/FileReaderIterator.h
index 910746d6a4..474746f018 100644
--- a/cpp/velox/operators/reader/FileReaderIterator.h
+++ b/cpp/velox/operators/reader/FileReaderIterator.h
@@ -17,6 +17,8 @@
#pragma once
+#include <string_view>
+
#include "compute/ResultIterator.h"
#include "memory/ColumnarBatchIterator.h"
#include "velox/common/memory/MemoryPool.h"
@@ -25,6 +27,11 @@ namespace gluten {
enum FileReaderType { kBuffered, kStream, kNone };
+// Suffix of the marker file that the whole stage dumper leaves in place of a
data file when an input
+// iterator produced no batches, e.g. `data_1_2_4_0.parquet.empty`. No parquet
file can be written
+// without a schema, which the writer takes from the first batch.
+inline constexpr std::string_view kEmptyInputMarkerSuffix = ".empty";
+
class FileReaderIterator : public ColumnarBatchIterator {
public:
static std::shared_ptr<ResultIterator> getInputIteratorFromFileReader(
@@ -46,4 +53,13 @@ class FileReaderIterator : public ColumnarBatchIterator {
std::string path_;
};
+// Replays an empty input marker as an input iterator that yields no batches.
It reads no file, so it
+// is not a FileReaderIterator.
+class EmptyInputMarkerIterator final : public ColumnarBatchIterator {
+ public:
+ std::shared_ptr<ColumnarBatch> next() override {
+ return nullptr;
+ }
+};
+
} // namespace gluten
diff --git a/cpp/velox/operators/writer/VeloxColumnarBatchWriter.cc
b/cpp/velox/operators/writer/VeloxColumnarBatchWriter.cc
index 598199f19b..5b94eb2b94 100644
--- a/cpp/velox/operators/writer/VeloxColumnarBatchWriter.cc
+++ b/cpp/velox/operators/writer/VeloxColumnarBatchWriter.cc
@@ -57,7 +57,11 @@ arrow::Status VeloxColumnarBatchWriter::write(const
std::shared_ptr<ColumnarBatc
}
arrow::Status VeloxColumnarBatchWriter::close() {
- writer_->close();
+ // The writer takes its schema from the first batch, so it is only created
once something has been
+ // written. Closing a writer that never received a batch is a no-op rather
than a null dereference.
+ if (writer_) {
+ writer_->close();
+ }
return arrow::Status::OK();
}
} // namespace gluten
diff --git a/cpp/velox/tests/CMakeLists.txt b/cpp/velox/tests/CMakeLists.txt
index 9abc4b6aa5..df00a8db15 100644
--- a/cpp/velox/tests/CMakeLists.txt
+++ b/cpp/velox/tests/CMakeLists.txt
@@ -163,6 +163,7 @@ if(ENABLE_S3)
endif()
add_velox_test(scoped_timer_test SOURCES ScopedTimerTest.cc)
add_velox_test(row_based_checksum_test SOURCES RowBasedChecksumTest.cc)
+add_velox_test(jni_hash_table_test SOURCES JniHashTableTest.cc)
if(BUILD_EXAMPLES)
add_velox_test(my_udf_test SOURCES MyUdfTest.cc)
# UDF loading exercises the production shared library, so use the same
diff --git a/cpp/velox/tests/JniHashTableTest.cc
b/cpp/velox/tests/JniHashTableTest.cc
new file mode 100644
index 0000000000..aa57339de5
--- /dev/null
+++ b/cpp/velox/tests/JniHashTableTest.cc
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+#include "jni/JniHashTable.h"
+
+#include <gtest/gtest.h>
+
+namespace gluten {
+
+// This test binary, like the micro benchmark, runs without a JVM. JNI_OnLoad
is therefore never
+// invoked and JniHashTableContext::vm_ stays null.
+//
+// Looking up the build side of a broadcast hash join used to dereference that
null JavaVM
+// unconditionally, so converting any substrait plan carrying a non-empty
`hashTableId` crashed the
+// process. See https://github.com/apache/gluten/issues/12504.
+class JniHashTableTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ ASSERT_EQ(JniHashTableContext::getInstance().getJavaVM(), nullptr)
+ << "This test asserts behaviour of a process with no JVM attached";
+ }
+};
+
+TEST_F(JniHashTableTest, getJoinReportsMissWithoutJvm) {
+ // Must report a miss rather than segfault. SubstraitToVeloxPlanConverter
treats 0 as "no
+ // pre-built table" and falls back to building one from the build side input.
+ EXPECT_EQ(getJoin("no-such-broadcast-hash-table-id"), 0);
+}
+
+TEST_F(JniHashTableTest, callJavaGetReportsUnreachableCacheWithoutJvm) {
+ // Keeps "the cache says there is no such table" distinguishable from "the
cache cannot be
+ // reached at all", rather than collapsing both into a handle of 0 at this
level.
+
EXPECT_FALSE(JniHashTableContext::getInstance().callJavaGet("any-id").has_value());
+}
+
+} // namespace gluten
diff --git a/cpp/velox/tests/VeloxWholeStageDumperTest.cc
b/cpp/velox/tests/VeloxWholeStageDumperTest.cc
index 1ecb5f376a..7ff2bd4d38 100644
--- a/cpp/velox/tests/VeloxWholeStageDumperTest.cc
+++ b/cpp/velox/tests/VeloxWholeStageDumperTest.cc
@@ -19,11 +19,14 @@
#include "compute/VeloxBackend.h"
#include "memory/VeloxColumnarBatch.h"
#include "memory/VeloxMemoryManager.h"
+#include "operators/reader/FileReaderIterator.h"
#include "velox/vector/tests/utils/VectorTestBase.h"
+#include <fmt/format.h>
#include <gtest/gtest.h>
#include <filesystem>
+#include <vector>
using namespace facebook::velox;
@@ -133,4 +136,33 @@ TEST_F(VeloxWholeStageDumperTest,
dumpedBatchesOutliveReaderIterator) {
ASSERT_EQ(leafPool->usedBytes(), baselineBytes);
}
+// An input iterator that yields nothing used to leave the parquet writer
uninitialized, since the
+// writer takes its schema from the first batch, and closing it dereferenced a
null writer.
+// See https://github.com/apache/gluten/issues/12504.
+TEST_F(VeloxWholeStageDumperTest, dumpEmptyInputIterator) {
+ const SparkTaskInfo taskInfo{/*stageId=*/1, /*partitionId=*/2, /*taskId=*/3,
/*vId=*/4};
+ VeloxWholeStageDumper dumper(taskInfo, saveDir_.string(), 4096, vmm_.get());
+ auto input =
std::make_shared<RowVectorBatchIterator>(std::vector<RowVectorPtr>{});
+
+ std::shared_ptr<ColumnarBatchIterator> dumped;
+ ASSERT_NO_THROW(dumped = dumper.dumpInputIterator(0, input));
+
+ // The task keeps running against what the original iterator produced:
nothing.
+ ASSERT_NE(dumped, nullptr);
+ ASSERT_EQ(dumped->next(), nullptr);
+
+ // No parquet file can be written without a schema, so a marker takes its
place.
+ const auto marker = saveDir_ / "data_1_2_4_0.parquet.empty";
+ ASSERT_FALSE(std::filesystem::exists(saveDir_ / "data_1_2_4_0.parquet"));
+ ASSERT_TRUE(std::filesystem::exists(marker));
+
+ // The benchmark replays the marker passed to --data as an empty input
iterator.
+ for (const auto readerType : {FileReaderType::kStream,
FileReaderType::kBuffered}) {
+ auto replayed = FileReaderIterator::getInputIteratorFromFileReader(
+ readerType, marker.string(), 4096, vmm_->getLeafMemoryPool());
+ ASSERT_NE(replayed, nullptr);
+ ASSERT_FALSE(replayed->hasNext());
+ }
+}
+
} // namespace gluten
diff --git a/cpp/velox/utils/VeloxWholeStageDumper.cc
b/cpp/velox/utils/VeloxWholeStageDumper.cc
index 3f0bb556f8..f1129b82ae 100644
--- a/cpp/velox/utils/VeloxWholeStageDumper.cc
+++ b/cpp/velox/utils/VeloxWholeStageDumper.cc
@@ -119,11 +119,28 @@ std::shared_ptr<ColumnarBatchIterator>
VeloxWholeStageDumper::dumpInputIterator(
batchSize_,
memoryManager_->getAggregateMemoryPool()->addAggregateChild(fmt::format("dump_iterator.{}",
iteratorIndex)));
+ bool wroteBatch = false;
while (auto cb = inputIterator->next()) {
GLUTEN_THROW_NOT_OK(writer->write(cb));
+ wroteBatch = true;
}
GLUTEN_THROW_NOT_OK(writer->close());
+ if (!wroteBatch) {
+ // The writer takes its schema from the first batch, so an iterator that
yielded nothing leaves
+ // no parquet file behind. Leave a marker in its place instead: the
benchmark binds the files
+ // passed to --data to iterator indexes by position, and replays the
marker as an empty input
+ // iterator, so the positions of later iterators still line up.
+ const auto markerName = fileName + std::string(kEmptyInputMarkerSuffix);
+ dumpToStorage(
+ saveDir_,
+ markerName,
+ fmt::format("Input iterator {} of {} produced no batches.\n",
iteratorIndex, taskInfo_.toString()));
+ LOG(INFO) << "Input iterator " << iteratorIndex << " of " << taskInfo_ <<
" produced no batches, dumped as "
+ << markerName << ".";
+ return std::make_shared<EmptyInputMarkerIterator>();
+ }
+
// Velox parquet reader requires leaf memory pool. Do not create a dedicated
child pool here: the reader
// iterator is destroyed as soon as the input stream is exhausted, while
vectors it produced may still be
// referenced by downstream operators or by batches handed to Java. Freeing
them against a destroyed pool
diff --git a/docs/backends/velox/developers/MicroBenchmarks.md
b/docs/backends/velox/developers/MicroBenchmarks.md
index dc395eda0c..c7ff2c1772 100644
--- a/docs/backends/velox/developers/MicroBenchmarks.md
+++ b/docs/backends/velox/developers/MicroBenchmarks.md
@@ -139,7 +139,11 @@ or 4 types of dumped file:
file splits.
- Data file(optional): Parquet formatted, file
name `data_{stageId}_{partitionId}_{vId}_{iteratorIdx}.parquet`. If the
first stage contains one or
- more BHJ operators, there can be one or more input data files. The input
data files of a first
+ more BHJ operators, there can be one or more input data files, see
+ [Broadcast hash join](#broadcast-hash-join). An input iterator that produced
no batches is dumped
+ as an empty marker file
`data_{stageId}_{partitionId}_{vId}_{iteratorIdx}.parquet.empty` instead,
+ since no parquet file can be written without a schema. Pass it to `--data`
like any other data
+ file: the benchmark replays it as an empty input iterator. The input data
files of a first
stage will be loaded as iterators to serve as the inputs for the pipeline:
```
@@ -152,6 +156,15 @@ or 4 types of dumped file:
}
```
+### Broadcast hash join
+
+By default the hash table of a BHJ is built once per executor and handed to
the native join through
+a process-local cache, so its build side never reaches the native operator as
input data. While
+`spark.gluten.sql.benchmark_task.stageId` or
`spark.gluten.sql.benchmark_task.taskId` is set,
+Gluten therefore ignores
`spark.gluten.velox.buildHashTableOncePerExecutor.enabled` for every query
+and builds the hash table in each task from the streamed build side, which is
dumped as a regular
+data file. Note that the replayed stage includes building the hash table in
its measurement.
+
Run benchmark. By default, the result will be printed to stdout. You can use
`--noprint-result` to
suppress this output.
diff --git
a/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
b/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
index 1459e7fed0..70501f3256 100644
---
a/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
+++
b/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
@@ -358,6 +358,10 @@ class GlutenConfig(conf: SQLConf) extends
GlutenCoreConfig(conf) {
def benchmarkSaveDir: String = getConf(BENCHMARK_SAVE_DIR)
+ // Whether whole stage dumping for the micro benchmark is requested for some
task. Which tasks
+ // are actually dumped is decided per task, see
DebugUtil.isDumpingEnabledForTask.
+ def benchmarkDumpRequested: Boolean = benchmarkStageId >= 0 ||
benchmarkTaskId.nonEmpty
+
def textInputMaxBlockSize: Long = getConf(TEXT_INPUT_ROW_MAX_BLOCK_SIZE)
def textIputEmptyAsDefault: Boolean = getConf(TEXT_INPUT_EMPTY_AS_DEFAULT)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]