This is an automated email from the ASF dual-hosted git repository.
zhztheplayer 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 5d014b4517 [VL] Use Velox TaskCursor infrastructure (#12302)
5d014b4517 is described below
commit 5d014b45179e55dada42ed84632a5624501fb16b
Author: Hongze Zhang <[email protected]>
AuthorDate: Wed Jun 17 16:18:05 2026 +0100
[VL] Use Velox TaskCursor infrastructure (#12302)
---
.../org/apache/gluten/metrics/MetricsUtil.scala | 2 +-
.../org/apache/gluten/test/MockVeloxBackend.java | 6 +-
cpp-ch/local-engine/Parser/CHColumnToSparkRow.h | 2 +-
.../local-engine/Parser/SerializedPlanParser.cpp | 2 +-
cpp/core/CMakeLists.txt | 2 +
cpp/core/compute/Runtime.cc | 3 +-
cpp/core/compute/Runtime.h | 11 +-
cpp/core/jni/JniCommon.cc | 2 +-
cpp/core/jni/JniCommon.h | 84 +++++++++++++++
cpp/core/jni/JniWrapper.cc | 59 ++++++++++-
.../core/threads/ThreadInitializer.cc | 21 +++-
cpp/core/threads/ThreadInitializer.h | 56 ++++++++++
cpp/core/threads/ThreadManager.cc | 53 ++++++++++
cpp/core/threads/ThreadManager.h | 73 +++++++++++++
cpp/velox/benchmarks/GenericBenchmark.cc | 15 ++-
cpp/velox/benchmarks/ParquetWriteBenchmark.cc | 5 +-
cpp/velox/compute/VeloxBackend.cc | 40 ++++++-
cpp/velox/compute/VeloxRuntime.cc | 33 ++++--
cpp/velox/compute/VeloxRuntime.h | 1 +
cpp/velox/compute/WholeStageResultIterator.cc | 115 +++++++++------------
cpp/velox/compute/WholeStageResultIterator.h | 11 +-
cpp/velox/config/VeloxConfig.h | 2 +-
cpp/velox/jni/VeloxJniWrapper.cc | 2 +-
cpp/velox/operators/plannodes/CudfVectorStream.h | 5 +
cpp/velox/tests/RuntimeTest.cc | 30 +++++-
.../tests/Substrait2VeloxPlanConversionTest.cc | 1 +
.../apache/gluten/runtime/RuntimeJniWrapper.java | 3 +-
.../gluten/threads/NativeThreadInitializer.java | 44 ++++++++
.../threads/NativeThreadManagerJniWrapper.java | 44 ++++++++
.../gluten/threads/TaskChildThreadInitializer.java | 67 ++++++++++++
.../scala/org/apache/gluten/runtime/Runtime.scala | 10 +-
.../gluten/threads/NativeThreadManager.scala | 75 ++++++++++++++
.../org/apache/spark/task/TaskResources.scala | 2 +-
.../org/apache/gluten/config/GlutenConfig.scala | 6 +-
34 files changed, 771 insertions(+), 116 deletions(-)
diff --git
a/backends-velox/src/main/scala/org/apache/gluten/metrics/MetricsUtil.scala
b/backends-velox/src/main/scala/org/apache/gluten/metrics/MetricsUtil.scala
index 24930ea153..878eb2dcf1 100644
--- a/backends-velox/src/main/scala/org/apache/gluten/metrics/MetricsUtil.scala
+++ b/backends-velox/src/main/scala/org/apache/gluten/metrics/MetricsUtil.scala
@@ -277,7 +277,7 @@ object MetricsUtil extends Logging {
}
ju.updateJoinMetrics(operatorMetrics, metrics.getSingleMetrics,
joinParams)
case u: UnionMetricsUpdater =>
- // JoinRel outputs two suites of metrics respectively for hash build
and hash probe.
+ // Union outputs two suites of metrics respectively.
// Therefore, fetch one more suite of metrics here.
operatorMetrics.add(metrics.getOperatorMetrics(curMetricsIdx))
curMetricsIdx -= 1
diff --git
a/backends-velox/src/test/java/org/apache/gluten/test/MockVeloxBackend.java
b/backends-velox/src/test/java/org/apache/gluten/test/MockVeloxBackend.java
index 2c4b813f30..a02bfa46e0 100644
--- a/backends-velox/src/test/java/org/apache/gluten/test/MockVeloxBackend.java
+++ b/backends-velox/src/test/java/org/apache/gluten/test/MockVeloxBackend.java
@@ -17,7 +17,8 @@
package org.apache.gluten.test;
import org.apache.gluten.config.GlutenConfig;
-import org.apache.gluten.config.VeloxConfig$;
+import org.apache.gluten.config.GlutenCoreConfig;
+import org.apache.gluten.config.VeloxConfig;
import com.codahale.metrics.MetricRegistry;
import org.apache.spark.SparkConf;
@@ -72,7 +73,8 @@ public final class MockVeloxBackend {
private static SparkConf newSparkConf() {
final SparkConf conf = new SparkConf();
conf.set(GlutenConfig.SPARK_OFFHEAP_SIZE_KEY(), "1g");
- conf.set(VeloxConfig$.MODULE$.COLUMNAR_VELOX_CONNECTOR_IO_THREADS().key(),
"0");
+ conf.set(GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR().key(), "1");
+ conf.set(VeloxConfig.COLUMNAR_VELOX_CONNECTOR_IO_THREADS().key(), "0");
return conf;
}
}
diff --git a/cpp-ch/local-engine/Parser/CHColumnToSparkRow.h
b/cpp-ch/local-engine/Parser/CHColumnToSparkRow.h
index d30a5d767b..7c9fbf140c 100644
--- a/cpp-ch/local-engine/Parser/CHColumnToSparkRow.h
+++ b/cpp-ch/local-engine/Parser/CHColumnToSparkRow.h
@@ -102,7 +102,7 @@ public:
explicit BackingDataLengthCalculator(const DB::DataTypePtr & type_);
virtual ~BackingDataLengthCalculator() = default;
- /// Return length is guranteed to round up to 8
+ /// Return length is guaranteed to round up to 8
virtual int64_t calculate(const DB::Field & field) const;
static int64_t getArrayElementSize(const DB::DataTypePtr & nested_type);
diff --git a/cpp-ch/local-engine/Parser/SerializedPlanParser.cpp
b/cpp-ch/local-engine/Parser/SerializedPlanParser.cpp
index 8c702e2092..5ca985575d 100644
--- a/cpp-ch/local-engine/Parser/SerializedPlanParser.cpp
+++ b/cpp-ch/local-engine/Parser/SerializedPlanParser.cpp
@@ -246,7 +246,7 @@ QueryPlanPtr SerializedPlanParser::parseOp(const
substrait::Rel & rel, std::list
if (read_rel_parser->isReadRelFromJavaIter(read))
{
- /// If read from java iter, local_files is guranteed to be set in
read rel.
+ /// If read from java iter, local_files is guaranteed to be set in
read rel.
auto iter = read.local_files().items().at(0).uri_file();
auto pos = iter.find(':');
auto iter_index = std::stoi(iter.substr(pos + 1, iter.size()));
diff --git a/cpp/core/CMakeLists.txt b/cpp/core/CMakeLists.txt
index e16464b811..9530d603f5 100644
--- a/cpp/core/CMakeLists.txt
+++ b/cpp/core/CMakeLists.txt
@@ -128,6 +128,8 @@ set(SPARK_COLUMNAR_PLUGIN_SRCS
memory/MemoryManager.cc
memory/ArrowMemoryPool.cc
memory/ColumnarBatch.cc
+ threads/ThreadInitializer.cc
+ threads/ThreadManager.cc
shuffle/Dictionary.cc
shuffle/FallbackRangePartitioner.cc
shuffle/HashPartitioner.cc
diff --git a/cpp/core/compute/Runtime.cc b/cpp/core/compute/Runtime.cc
index f5b4ed0f33..f283bc7283 100644
--- a/cpp/core/compute/Runtime.cc
+++ b/cpp/core/compute/Runtime.cc
@@ -40,9 +40,10 @@ void Runtime::registerFactory(const std::string& kind,
Runtime::Factory factory,
Runtime* Runtime::create(
const std::string& kind,
MemoryManager* memoryManager,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& sessionConf) {
auto& factory = runtimeFactories().get(kind);
- return factory(kind, std::move(memoryManager), sessionConf);
+ return factory(kind, memoryManager, threadManager, sessionConf);
}
void Runtime::release(Runtime* runtime) {
diff --git a/cpp/core/compute/Runtime.h b/cpp/core/compute/Runtime.h
index 4ab944898b..07120684d3 100644
--- a/cpp/core/compute/Runtime.h
+++ b/cpp/core/compute/Runtime.h
@@ -30,6 +30,7 @@
#include "shuffle/ShuffleReader.h"
#include "shuffle/ShuffleWriter.h"
#include "substrait/plan.pb.h"
+#include "threads/ThreadManager.h"
#include "utils/ObjectStore.h"
#include "utils/WholeStageDumper.h"
@@ -66,12 +67,14 @@ class Runtime : public
std::enable_shared_from_this<Runtime> {
using Factory = std::function<Runtime*(
const std::string& kind,
MemoryManager* memoryManager,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& sessionConf)>;
using Releaser = std::function<void(Runtime*)>;
static void registerFactory(const std::string& kind, Factory factory,
Releaser releaser);
static Runtime* create(
const std::string& kind,
MemoryManager* memoryManager,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& sessionConf = {});
static void release(Runtime*);
static std::optional<std::string>* localWriteFilesTempPath();
@@ -80,8 +83,9 @@ class Runtime : public std::enable_shared_from_this<Runtime> {
Runtime(
const std::string& kind,
MemoryManager* memoryManager,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& confMap)
- : kind_(kind), memoryManager_(memoryManager), confMap_(confMap) {}
+ : kind_(kind), memoryManager_(memoryManager),
threadManager_(threadManager), confMap_(confMap) {}
virtual ~Runtime() = default;
@@ -131,6 +135,10 @@ class Runtime : public
std::enable_shared_from_this<Runtime> {
return memoryManager_;
};
+ virtual ThreadManager* threadManager() {
+ return threadManager_;
+ };
+
/// This function is used to create certain converter from the format used by
/// the backend to Spark unsafe row.
virtual std::shared_ptr<ColumnarToRowConverter>
createColumnar2RowConverter(int64_t column2RowMemThreshold) {
@@ -189,6 +197,7 @@ class Runtime : public
std::enable_shared_from_this<Runtime> {
protected:
std::string kind_;
MemoryManager* memoryManager_;
+ ThreadManager* threadManager_;
std::unique_ptr<ObjectStore> objStore_ = ObjectStore::create();
std::unordered_map<std::string, std::string> confMap_; // Session conf map
diff --git a/cpp/core/jni/JniCommon.cc b/cpp/core/jni/JniCommon.cc
index 3c17e1085b..76a07aa2d8 100644
--- a/cpp/core/jni/JniCommon.cc
+++ b/cpp/core/jni/JniCommon.cc
@@ -16,6 +16,7 @@
*/
#include "JniCommon.h"
+#include <folly/system/ThreadName.h>
void gluten::JniCommonState::ensureInitialized(JNIEnv* env) {
std::lock_guard<std::mutex> lockGuard(mtx_);
@@ -120,7 +121,6 @@ std::shared_ptr<gluten::ColumnarBatch>
gluten::JniColumnarBatchIterator::next()
std::shared_ptr<gluten::ColumnarBatch>
gluten::JniColumnarBatchIterator::nextInternal() const {
JNIEnv* env = nullptr;
attachCurrentThreadAsDaemonOrThrow(vm_, &env);
-
if (!env->CallBooleanMethod(jColumnarBatchItr_,
serializedColumnarBatchIteratorHasNext_)) {
checkException(env);
return nullptr; // stream ended
diff --git a/cpp/core/jni/JniCommon.h b/cpp/core/jni/JniCommon.h
index a4edd2c57e..43ddb2226a 100644
--- a/cpp/core/jni/JniCommon.h
+++ b/cpp/core/jni/JniCommon.h
@@ -26,6 +26,7 @@
#include "compute/Runtime.h"
#include "memory/AllocationListener.h"
#include "shuffle/rss/RssClient.h"
+#include "threads/ThreadInitializer.h"
#include "utils/Compression.h"
#include "utils/Exception.h"
#include "utils/ResourceMap.h"
@@ -549,3 +550,86 @@ class JavaRssClient : public RssClient {
jmethodID javaPushPartitionData_;
jbyteArray array_;
};
+
+/// Bridges gluten::ThreadInitializer callbacks to a Java-side
+/// NativeThreadInitializer instance via JNI.
+///
+/// On initialize(), attaches the current native thread to the JVM and calls
+/// into the Java initializer so it can install Spark TaskContext or other
+/// task-local state. On destroy(), calls the Java destroy() method but
+/// does NOT detach the thread — the underlying JVM thread may be reused by
+/// the pool, and detaching prematurely could allow the Java Thread object
+/// to be garbage-collected.
+class SparkThreadInitializer final : public gluten::ThreadInitializer {
+ public:
+ /// @param vm The JavaVM pointer from JNI_OnLoad.
+ /// @param jInitializerLocalRef A local reference to a Java object
+ /// implementing org.apache.gluten.threads.NativeThreadInitializer.
+ /// A global reference is created internally.
+ SparkThreadInitializer(JavaVM* vm, jobject jInitializerLocalRef) : vm_(vm) {
+ JNIEnv* env;
+ attachCurrentThreadAsDaemonOrThrow(vm_, &env);
+ jInitializerGlobalRef_ = env->NewGlobalRef(jInitializerLocalRef);
+ GLUTEN_CHECK(jInitializerGlobalRef_ != nullptr, "Failed to create global
reference for native thread initializer.");
+ (void)initializeMethod(env);
+ }
+
+ SparkThreadInitializer(const SparkThreadInitializer&) = delete;
+ SparkThreadInitializer(SparkThreadInitializer&&) = delete;
+ SparkThreadInitializer& operator=(const SparkThreadInitializer&) = delete;
+ SparkThreadInitializer& operator=(SparkThreadInitializer&&) = delete;
+
+ ~SparkThreadInitializer() override {
+ JNIEnv* env;
+ if (vm_->GetEnv(reinterpret_cast<void**>(&env), jniVersion) != JNI_OK) {
+ LOG(WARNING) << "SparkThreadInitializer#~SparkThreadInitializer(): "
+ << "JNIEnv was not attached to current thread";
+ return;
+ }
+ env->DeleteGlobalRef(jInitializerGlobalRef_);
+ }
+
+ void initialize(const std::string& threadName) override {
+ JNIEnv* env;
+ attachCurrentThreadAsDaemonOrThrow(vm_, &env);
+ jstring jThreadName = env->NewStringUTF(threadName.c_str());
+ env->CallVoidMethod(jInitializerGlobalRef_, initializeMethod(env),
jThreadName);
+ env->DeleteLocalRef(jThreadName);
+ checkException(env);
+ }
+
+ void destroy(const std::string& threadName) override {
+ // IMPORTANT: Do not call vm_.DetachCurrentThread here, otherwise Java
side thread
+ // object might be dereferenced and garbage-collected, to break the reuse
of thread
+ // resources.
+ JNIEnv* env;
+ attachCurrentThreadAsDaemonOrThrow(vm_, &env);
+ jstring jThreadName = env->NewStringUTF(threadName.c_str());
+ env->CallVoidMethod(jInitializerGlobalRef_, destroyMethod(env),
jThreadName);
+ env->DeleteLocalRef(jThreadName);
+ checkException(env);
+ }
+
+ private:
+ jmethodID initializeMethod(JNIEnv* env) {
+ static jmethodID initializeMethod =
+ getMethodIdOrError(env, nativeThreadInitializerClass(env),
"initialize", "(Ljava/lang/String;)V");
+ return initializeMethod;
+ }
+
+ jmethodID destroyMethod(JNIEnv* env) {
+ static jmethodID destroyMethod =
+ getMethodIdOrError(env, nativeThreadInitializerClass(env), "destroy",
"(Ljava/lang/String;)V");
+ return destroyMethod;
+ }
+
+ jclass nativeThreadInitializerClass(JNIEnv* env) {
+ static jclass javaInitializerClass =
+ createGlobalClassReferenceOrError(env,
"Lorg/apache/gluten/threads/NativeThreadInitializer;");
+ return javaInitializerClass;
+ }
+
+ private:
+ JavaVM* vm_;
+ jobject jInitializerGlobalRef_;
+};
diff --git a/cpp/core/jni/JniWrapper.cc b/cpp/core/jni/JniWrapper.cc
index 86da59d2ef..efdfd36cba 100644
--- a/cpp/core/jni/JniWrapper.cc
+++ b/cpp/core/jni/JniWrapper.cc
@@ -182,8 +182,9 @@ class InternalRuntime : public Runtime {
InternalRuntime(
const std::string& kind,
MemoryManager* memoryManager,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& confMap)
- : Runtime(kind, memoryManager, confMap) {}
+ : Runtime(kind, memoryManager, threadManager, confMap) {}
};
MemoryManager* internalMemoryManagerFactory(const std::string& kind,
std::unique_ptr<AllocationListener> listener) {
@@ -194,11 +195,33 @@ void internalMemoryManagerReleaser(MemoryManager*
memoryManager) {
delete memoryManager;
}
+class InternalThreadManager : public ThreadManager {
+ public:
+ InternalThreadManager(const std::string& kind,
std::unique_ptr<ThreadInitializer> initializer)
+ : ThreadManager(kind),
initializer_(std::shared_ptr<ThreadInitializer>(std::move(initializer))) {}
+
+ ThreadInitializer* getThreadInitializer() override {
+ return initializer_.get();
+ }
+
+ private:
+ std::shared_ptr<ThreadInitializer> initializer_;
+};
+
+ThreadManager* internalThreadManagerFactory(const std::string& kind,
std::unique_ptr<ThreadInitializer> initializer) {
+ return new InternalThreadManager(kind, std::move(initializer));
+}
+
+void internalThreadManagerReleaser(ThreadManager* threadManager) {
+ delete threadManager;
+}
+
Runtime* internalRuntimeFactory(
const std::string& kind,
MemoryManager* memoryManager,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& sessionConf) {
- return new InternalRuntime(kind, memoryManager, sessionConf);
+ return new InternalRuntime(kind, memoryManager, threadManager, sessionConf);
}
void internalRuntimeReleaser(Runtime* runtime) {
@@ -253,6 +276,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
getJniErrorState()->ensureInitialized(env);
MemoryManager::registerFactory(kInternalBackendKind,
internalMemoryManagerFactory, internalMemoryManagerReleaser);
+ ThreadManager::registerFactory(kInternalBackendKind,
internalThreadManagerFactory, internalThreadManagerReleaser);
Runtime::registerFactory(kInternalBackendKind, internalRuntimeFactory,
internalRuntimeReleaser);
byteArrayClass = createGlobalClassReferenceOrError(env, "[B");
@@ -320,14 +344,16 @@ JNIEXPORT jlong JNICALL
Java_org_apache_gluten_runtime_RuntimeJniWrapper_createR
jclass,
jstring jBackendType,
jlong nmmHandle,
+ jlong ntmHandle,
jbyteArray sessionConf) {
JNI_METHOD_START
MemoryManager* memoryManager = jniCastOrThrow<MemoryManager>(nmmHandle);
+ ThreadManager* threadManager = jniCastOrThrow<ThreadManager>(ntmHandle);
auto safeArray = getByteArrayElementsSafe(env, sessionConf);
auto sparkConf = parseConfMap(env, safeArray.elems(), safeArray.length());
auto backendType = jStringToCString(env, jBackendType);
- auto runtime = Runtime::create(backendType, memoryManager, sparkConf);
+ auto runtime = Runtime::create(backendType, memoryManager, threadManager,
sparkConf);
return reinterpret_cast<jlong>(runtime);
JNI_METHOD_END(kInvalidObjectHandle)
}
@@ -371,6 +397,33 @@ JNIEXPORT jlong JNICALL
Java_org_apache_gluten_memory_NativeMemoryManagerJniWrap
JNI_METHOD_END(-1L)
}
+JNIEXPORT jlong JNICALL
Java_org_apache_gluten_threads_NativeThreadManagerJniWrapper_create( // NOLINT
+ JNIEnv* env,
+ jclass,
+ jstring jBackendType,
+ jobject jInitializer) {
+ JNI_METHOD_START
+ JavaVM* vm;
+ if (env->GetJavaVM(&vm) != JNI_OK) {
+ throw GlutenException("Unable to get JavaVM instance");
+ }
+ auto backendType = jStringToCString(env, jBackendType);
+ std::unique_ptr<ThreadInitializer> initializer =
std::make_unique<SparkThreadInitializer>(vm, jInitializer);
+ ThreadManager* tm = ThreadManager::create(backendType,
std::move(initializer));
+ return reinterpret_cast<jlong>(tm);
+ JNI_METHOD_END(-1L)
+}
+
+JNIEXPORT void JNICALL
Java_org_apache_gluten_threads_NativeThreadManagerJniWrapper_release( // NOLINT
+ JNIEnv* env,
+ jclass,
+ jlong ntmHandle) {
+ JNI_METHOD_START
+ auto* threadManager = jniCastOrThrow<ThreadManager>(ntmHandle);
+ ThreadManager::release(threadManager);
+ JNI_METHOD_END()
+}
+
JNIEXPORT jbyteArray JNICALL
Java_org_apache_gluten_memory_NativeMemoryManagerJniWrapper_collectUsage( //
NOLINT
JNIEnv* env,
jclass,
diff --git
a/gluten-arrow/src/main/java/org/apache/gluten/runtime/RuntimeJniWrapper.java
b/cpp/core/threads/ThreadInitializer.cc
similarity index 59%
copy from
gluten-arrow/src/main/java/org/apache/gluten/runtime/RuntimeJniWrapper.java
copy to cpp/core/threads/ThreadInitializer.cc
index 8c4280a3b5..62ee76def0 100644
---
a/gluten-arrow/src/main/java/org/apache/gluten/runtime/RuntimeJniWrapper.java
+++ b/cpp/core/threads/ThreadInitializer.cc
@@ -14,13 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.gluten.runtime;
-public class RuntimeJniWrapper {
+#include "ThreadInitializer.h"
- private RuntimeJniWrapper() {}
+namespace gluten {
+namespace {
- public static native long createRuntime(String backendType, long nmm, byte[]
sessionConf);
+/// A ThreadInitializer whose initialize() and destroy() are no-ops.
+/// Used when no JVM or Spark task context is available (e.g., benchmarks).
+class NoopThreadInitializer final : public ThreadInitializer {
+ public:
+ void initialize(const std::string& threadName) override {}
+ void destroy(const std::string& threadName) override{};
+};
- public static native void releaseRuntime(long handle);
+} // namespace
+
+std::unique_ptr<ThreadInitializer> ThreadInitializer::noop() {
+ return std::make_unique<NoopThreadInitializer>();
}
+
+} // namespace gluten
diff --git a/cpp/core/threads/ThreadInitializer.h
b/cpp/core/threads/ThreadInitializer.h
new file mode 100644
index 0000000000..0907cd03c2
--- /dev/null
+++ b/cpp/core/threads/ThreadInitializer.h
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <memory>
+
+namespace gluten {
+
+/// Per-task hook invoked around each submitted task on executors wrapped
+/// by a ThreadManager.
+///
+/// When a task is submitted to an executor (e.g., via HookedExecutor::wrap),
+/// the ThreadInitializer gives the application a chance to attach per-task
+/// context — such as JNI thread attachment or Spark TaskContext propagation —
+/// before the task runs native work and to clean up after.
+///
+/// Implementations must be thread-safe; initialize() and destroy() can be
+/// called concurrently from different threads.
+class ThreadInitializer {
+ public:
+ /// Returns an initializer that does nothing (noop). Useful in benchmarks
+ /// and tests where no JVM/Spark context is available.
+ static std::unique_ptr<ThreadInitializer> noop();
+
+ virtual ~ThreadInitializer() = default;
+
+ /// Called before each submitted task executes. Attach per-task context
+ /// such as a JNI env or Spark TaskContext.
+ /// @param taskName A human-readable name identifying the task (not the
thread).
+ virtual void initialize(const std::string& taskName) = 0;
+
+ /// Called after each submitted task completes. Must not detach the JNI
+ /// thread — the thread may be reused for subsequent tasks.
+ /// @param taskName The same name passed to initialize().
+ virtual void destroy(const std::string& taskName) = 0;
+
+ protected:
+ ThreadInitializer() = default;
+};
+
+} // namespace gluten
diff --git a/cpp/core/threads/ThreadManager.cc
b/cpp/core/threads/ThreadManager.cc
new file mode 100644
index 0000000000..20a377b9a0
--- /dev/null
+++ b/cpp/core/threads/ThreadManager.cc
@@ -0,0 +1,53 @@
+/*
+ * 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 "ThreadManager.h"
+
+#include "utils/Registry.h"
+
+namespace gluten {
+namespace {
+
+Registry<ThreadManager::Factory>& threadManagerFactories() {
+ static Registry<ThreadManager::Factory> registry;
+ return registry;
+}
+
+Registry<ThreadManager::Releaser>& threadManagerReleasers() {
+ static Registry<ThreadManager::Releaser> registry;
+ return registry;
+}
+
+} // namespace
+
+void ThreadManager::registerFactory(const std::string& kind, Factory factory,
Releaser releaser) {
+ threadManagerFactories().registerObj(kind, std::move(factory));
+ threadManagerReleasers().registerObj(kind, std::move(releaser));
+}
+
+ThreadManager* ThreadManager::create(const std::string& kind,
std::unique_ptr<ThreadInitializer> initializer) {
+ auto& factory = threadManagerFactories().get(kind);
+ return factory(kind, std::move(initializer));
+}
+
+void ThreadManager::release(ThreadManager* threadManager) {
+ const std::string kind = threadManager->kind();
+ auto& releaser = threadManagerReleasers().get(kind);
+ releaser(threadManager);
+}
+
+} // namespace gluten
diff --git a/cpp/core/threads/ThreadManager.h b/cpp/core/threads/ThreadManager.h
new file mode 100644
index 0000000000..52bf662ad2
--- /dev/null
+++ b/cpp/core/threads/ThreadManager.h
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <string>
+
+#include "threads/ThreadInitializer.h"
+
+namespace gluten {
+
+/// Per-backend registry of ThreadInitializer instances.
+///
+/// ThreadManager follows the same Factory / Releaser registry pattern as
+/// MemoryManager and Runtime: each backend registers its own implementation
+/// at init time, and the factory is keyed by backend kind (e.g., "velox").
+///
+/// The ThreadManager owns a ThreadInitializer that is used to propagate
+/// per-task context (JNI env, Spark TaskContext) to executor tasks
+/// submitted via HookedExecutor. It is created once per Spark task and
released
+/// when the task finishes.
+class ThreadManager {
+ public:
+ using Factory =
+ std::function<ThreadManager*(const std::string& kind,
std::unique_ptr<ThreadInitializer> initializer)>;
+ using Releaser = std::function<void(ThreadManager*)>;
+
+ /// Register a backend-specific factory and releaser for the given kind.
+ static void registerFactory(const std::string& kind, Factory factory,
Releaser releaser);
+
+ /// Create a ThreadManager for the given backend kind with the supplied
+ /// thread initializer. The returned pointer is owned by the caller.
+ static ThreadManager* create(const std::string& kind,
std::unique_ptr<ThreadInitializer> initializer);
+
+ /// Release a ThreadManager previously created with create().
+ static void release(ThreadManager* threadManager);
+
+ explicit ThreadManager(const std::string& kind) : kind_(kind) {}
+
+ virtual ~ThreadManager() = default;
+
+ /// Return the backend kind this manager was created for.
+ virtual std::string kind() {
+ return kind_;
+ }
+
+ /// Return the ThreadInitializer that should be called around each
+ /// submitted task (before and after execution) to attach per-task
+ /// context. The returned pointer is owned by ThreadManager; callers
+ /// must not delete it.
+ virtual ThreadInitializer* getThreadInitializer() = 0;
+
+ private:
+ std::string kind_;
+};
+
+} // namespace gluten
diff --git a/cpp/velox/benchmarks/GenericBenchmark.cc
b/cpp/velox/benchmarks/GenericBenchmark.cc
index 616ba9bcfb..1e00eadd84 100644
--- a/cpp/velox/benchmarks/GenericBenchmark.cc
+++ b/cpp/velox/benchmarks/GenericBenchmark.cc
@@ -38,6 +38,7 @@
#include "tests/utils/LocalRssClient.h"
#include "tests/utils/TestAllocationListener.h"
#include "tests/utils/TestStreamReader.h"
+#include "threads/ThreadInitializer.h"
#include "utils/Exception.h"
#include "utils/StringUtil.h"
#include "utils/Timer.h"
@@ -382,7 +383,7 @@ void setQueryTraceConfig(std::unordered_map<std::string,
std::string>& configs)
}
} // namespace
-using RuntimeFactory = std::function<VeloxRuntime*(MemoryManager*
memoryManager)>;
+using RuntimeFactory = std::function<VeloxRuntime*(MemoryManager*
memoryManager, ThreadManager* threadManager)>;
auto BM_Generic = [](::benchmark::State& state,
const std::string& planFile,
@@ -398,7 +399,8 @@ auto BM_Generic = [](::benchmark::State& state,
auto* listenerPtr = listener.get();
auto* memoryManager = MemoryManager::create(kVeloxBackendKind,
std::move(listener));
- auto runtime = runtimeFactory(memoryManager);
+ auto* threadManager = ThreadManager::create(kVeloxBackendKind,
ThreadInitializer::noop());
+ auto runtime = runtimeFactory(memoryManager, threadManager);
auto plan = getPlanFromFile("Plan", planFile);
std::vector<std::string> splits{};
@@ -507,6 +509,7 @@ auto BM_Generic = [](::benchmark::State& state,
updateBenchmarkMetrics(state, elapsedTime, readInputTime, writerMetrics,
readerMetrics);
Runtime::release(runtime);
+ ThreadManager::release(threadManager);
MemoryManager::release(memoryManager);
};
@@ -522,7 +525,8 @@ auto BM_ShuffleWriteRead = [](::benchmark::State& state,
auto* listenerPtr = listener.get();
auto* memoryManager = MemoryManager::create(kVeloxBackendKind,
std::move(listener));
- auto runtime = runtimeFactory(memoryManager);
+ auto* threadManager = ThreadManager::create(kVeloxBackendKind,
ThreadInitializer::noop());
+ auto runtime = runtimeFactory(memoryManager, threadManager);
const size_t dirIndex =
std::hash<std::thread::id>{}(std::this_thread::get_id()) % localDirs.size();
const auto dataFileDir =
@@ -554,6 +558,7 @@ auto BM_ShuffleWriteRead = [](::benchmark::State& state,
updateBenchmarkMetrics(state, elapsedTime, readInputTime, writerMetrics,
readerMetrics);
Runtime::release(runtime);
+ ThreadManager::release(threadManager);
MemoryManager::release(memoryManager);
};
@@ -709,8 +714,8 @@ int main(int argc, char** argv) {
}
}
- RuntimeFactory runtimeFactory = [=](MemoryManager* memoryManager) {
- return dynamic_cast<VeloxRuntime*>(Runtime::create(kVeloxBackendKind,
memoryManager, sessionConf));
+ RuntimeFactory runtimeFactory = [=](MemoryManager* memoryManager,
ThreadManager* threadManager) {
+ return dynamic_cast<VeloxRuntime*>(Runtime::create(kVeloxBackendKind,
memoryManager, threadManager, sessionConf));
};
const auto localDirs = createLocalDirs();
diff --git a/cpp/velox/benchmarks/ParquetWriteBenchmark.cc
b/cpp/velox/benchmarks/ParquetWriteBenchmark.cc
index c66f6a3a4f..a534e1f6d4 100644
--- a/cpp/velox/benchmarks/ParquetWriteBenchmark.cc
+++ b/cpp/velox/benchmarks/ParquetWriteBenchmark.cc
@@ -23,6 +23,7 @@
#include "memory/VeloxMemoryManager.h"
#include "operators/reader/ParquetReaderIterator.h"
#include "operators/writer/VeloxParquetDataSource.h"
+#include "threads/ThreadInitializer.h"
#include "utils/VeloxArrowUtils.h"
namespace gluten {
@@ -52,7 +53,8 @@ class GoogleBenchmarkVeloxParquetWriteCacheScanBenchmark {
// reuse the ParquetWriteConverter for batches caused system % increase a
lot
auto memoryManager = getDefaultMemoryManager();
- auto runtime = Runtime::create(kVeloxBackendKind, memoryManager);
+ auto* threadManager = ThreadManager::create(kVeloxBackendKind,
ThreadInitializer::noop());
+ auto runtime = Runtime::create(kVeloxBackendKind, memoryManager,
threadManager);
auto veloxPool = memoryManager->getAggregateMemoryPool();
for (auto _ : state) {
@@ -98,6 +100,7 @@ class GoogleBenchmarkVeloxParquetWriteCacheScanBenchmark {
state.counters["write_time"] =
benchmark::Counter(writeTime, benchmark::Counter::kAvgThreads,
benchmark::Counter::OneK::kIs1000);
Runtime::release(runtime);
+ ThreadManager::release(threadManager);
}
private:
diff --git a/cpp/velox/compute/VeloxBackend.cc
b/cpp/velox/compute/VeloxBackend.cc
index c95cbfc5b7..902674b23b 100644
--- a/cpp/velox/compute/VeloxBackend.cc
+++ b/cpp/velox/compute/VeloxBackend.cc
@@ -87,15 +87,37 @@ void veloxMemoryManagerReleaser(MemoryManager*
memoryManager) {
Runtime* veloxRuntimeFactory(
const std::string& kind,
MemoryManager* memoryManager,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& sessionConf) {
auto* vmm = dynamic_cast<VeloxMemoryManager*>(memoryManager);
GLUTEN_CHECK(vmm != nullptr, "Not a Velox memory manager");
- return new VeloxRuntime(kind, vmm, sessionConf);
+ return new VeloxRuntime(kind, vmm, threadManager, sessionConf);
}
void veloxRuntimeReleaser(Runtime* runtime) {
delete runtime;
}
+
+class VeloxThreadManager : public ThreadManager {
+ public:
+ VeloxThreadManager(const std::string& kind,
std::unique_ptr<ThreadInitializer> initializer)
+ : ThreadManager(kind),
initializer_(std::shared_ptr<ThreadInitializer>(std::move(initializer))) {}
+
+ ThreadInitializer* getThreadInitializer() override {
+ return initializer_.get();
+ }
+
+ private:
+ std::shared_ptr<ThreadInitializer> initializer_;
+};
+
+ThreadManager* veloxThreadManagerFactory(const std::string& kind,
std::unique_ptr<ThreadInitializer> initializer) {
+ return new VeloxThreadManager(kind, std::move(initializer));
+}
+
+void veloxThreadManagerReleaser(ThreadManager* threadManager) {
+ delete threadManager;
+}
} // namespace
void VeloxBackend::init(
@@ -122,6 +144,7 @@ void VeloxBackend::init(
// Register factories.
MemoryManager::registerFactory(kVeloxBackendKind, veloxMemoryManagerFactory,
veloxMemoryManagerReleaser);
+ ThreadManager::registerFactory(kVeloxBackendKind, veloxThreadManagerFactory,
veloxThreadManagerReleaser);
Runtime::registerFactory(kVeloxBackendKind, veloxRuntimeFactory,
veloxRuntimeReleaser);
if (backendConf_->get<bool>(kDebugModeEnabled, false)) {
@@ -186,11 +209,24 @@ void VeloxBackend::init(
}
#endif
+ const int32_t numTaskSlotsPerExecutor = [&]() {
+ if (!backendConf_->valueExists(kNumTaskSlotsPerExecutor)) {
+ LOG(WARNING) << kNumTaskSlotsPerExecutor << " is not set. Falling back
to 1.";
+ return 1;
+ }
+ return backendConf_->get<int32_t>(kNumTaskSlotsPerExecutor).value();
+ }();
+ GLUTEN_CHECK(
+ numTaskSlotsPerExecutor >= 0,
+ kNumTaskSlotsPerExecutor + " was set to negative number " +
std::to_string(numTaskSlotsPerExecutor) +
+ ", this should not happen.");
+
const auto spillThreadNum = backendConf_->get<uint32_t>(kSpillThreadNum,
kSpillThreadNumDefaultValue);
if (spillThreadNum > 0) {
spillExecutor_ =
std::make_unique<folly::CPUThreadPoolExecutor>(spillThreadNum);
}
- auto ioThreads = backendConf_->get<int32_t>(kVeloxIOThreads,
kVeloxIOThreadsDefault);
+
+ const auto ioThreads = backendConf_->get<int32_t>(kVeloxIOThreads,
numTaskSlotsPerExecutor);
GLUTEN_CHECK(
ioThreads >= 0,
kVeloxIOThreads + " was set to negative number " +
std::to_string(ioThreads) + ", this should not happen.");
diff --git a/cpp/velox/compute/VeloxRuntime.cc
b/cpp/velox/compute/VeloxRuntime.cc
index 62e6820e9c..f13430bd0c 100644
--- a/cpp/velox/compute/VeloxRuntime.cc
+++ b/cpp/velox/compute/VeloxRuntime.cc
@@ -74,9 +74,15 @@ namespace {
class HookedExecutor final : public folly::Executor {
public:
- HookedExecutor(folly::Executor* parent, std::string name, bool debug,
std::chrono::milliseconds joinTimeout)
+ HookedExecutor(
+ folly::Executor* parent,
+ std::string name,
+ ThreadInitializer* initializer,
+ bool debug,
+ std::chrono::milliseconds joinTimeout)
: parent_(parent),
name_(std::move(name)),
+ initializer_(initializer),
debug_(debug),
joinTimeout_(joinTimeout),
state_(std::make_shared<State>()) {}
@@ -155,8 +161,10 @@ class HookedExecutor final : public folly::Executor {
}
folly::Func wrap(folly::Func func, int8_t priority) {
+ auto initializer = initializer_;
auto state = state_;
const auto taskId = state->nextTaskId.fetch_add(1,
std::memory_order_relaxed);
+ const auto taskName = name_ + std::to_string(taskId);
if (debug_) {
TaskInfo info{
.enqueueTime = std::chrono::steady_clock::now(),
@@ -166,7 +174,7 @@ class HookedExecutor final : public folly::Executor {
state->inFlightTasks[taskId] = std::move(info);
}
const auto debug = debug_;
- return [func = std::move(func), state, debug, taskId]() mutable {
+ return [func = std::move(func), initializer, state, debug, taskId,
taskName]() mutable {
auto markDone = folly::makeGuard([&] {
if (debug) {
std::lock_guard<std::mutex> lock(state->taskMutex);
@@ -177,6 +185,9 @@ class HookedExecutor final : public folly::Executor {
state->cv.notify_all();
}
});
+ GLUTEN_CHECK(initializer != nullptr, "ThreadInitializer is null.");
+ initializer->initialize(taskName);
+ auto invokeDestroyer = folly::makeGuard([&] {
initializer->destroy(taskName); });
// Destroy the submitted callable and all of its captures before
// decrementing inFlight_. Some async tasks capture AsyncLoadHolder,
// which keeps a MemoryPool alive until the callable itself is
@@ -191,6 +202,7 @@ class HookedExecutor final : public folly::Executor {
folly::Executor* parent_;
std::string name_;
+ ThreadInitializer* initializer_;
bool debug_;
std::chrono::milliseconds joinTimeout_;
std::shared_ptr<State> state_;
@@ -199,12 +211,13 @@ class HookedExecutor final : public folly::Executor {
std::unique_ptr<folly::Executor> makeHookedExecutor(
folly::Executor* parent,
const std::string& name,
+ ThreadInitializer* initializer,
bool debug,
std::chrono::milliseconds joinTimeout) {
if (parent == nullptr) {
return nullptr;
}
- return std::make_unique<HookedExecutor>(parent, name, debug, joinTimeout);
+ return std::make_unique<HookedExecutor>(parent, name, initializer, debug,
joinTimeout);
}
std::string makeScopedConnectorId(const std::string& base, uint64_t runtimeId)
{
@@ -224,8 +237,9 @@ VeloxConnectorIds makeScopedConnectorIds(uint64_t
runtimeId) {
VeloxRuntime::VeloxRuntime(
const std::string& kind,
VeloxMemoryManager* vmm,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& confMap)
- : Runtime(kind, vmm, confMap) {
+ : Runtime(kind, vmm, threadManager, confMap) {
// Refresh session config.
veloxCfg_ =
std::make_shared<facebook::velox::config::ConfigBase>(std::unordered_map<std::string,
std::string>(confMap_));
@@ -258,10 +272,13 @@ void VeloxRuntime::initializeExecutors() {
const auto timeoutMs =
veloxCfg_->get<int32_t>(kVeloxAsyncTimeoutOnTaskStopping,
kVeloxAsyncTimeoutOnTaskStoppingDefault);
const auto timeout = std::chrono::milliseconds(timeoutMs);
- executor_ = makeHookedExecutor(VeloxBackend::get()->executor(), kind_ +
".executor", debugModeEnabled_, timeout);
- spillExecutor_ =
- makeHookedExecutor(VeloxBackend::get()->spillExecutor(), kind_ +
".spill", debugModeEnabled_, timeout);
- ioExecutor_ = makeHookedExecutor(VeloxBackend::get()->ioExecutor(), kind_ +
".io", debugModeEnabled_, timeout);
+ ThreadInitializer* const initializer =
threadManager_->getThreadInitializer();
+ executor_ =
+ makeHookedExecutor(VeloxBackend::get()->executor(), kind_ + ".executor",
initializer, debugModeEnabled_, timeout);
+ spillExecutor_ = makeHookedExecutor(
+ VeloxBackend::get()->spillExecutor(), kind_ + ".spill", initializer,
debugModeEnabled_, timeout);
+ ioExecutor_ =
+ makeHookedExecutor(VeloxBackend::get()->ioExecutor(), kind_ + ".io",
initializer, debugModeEnabled_, timeout);
}
void VeloxRuntime::registerConnectors() {
diff --git a/cpp/velox/compute/VeloxRuntime.h b/cpp/velox/compute/VeloxRuntime.h
index 37f4da3343..c6ee1c462c 100644
--- a/cpp/velox/compute/VeloxRuntime.h
+++ b/cpp/velox/compute/VeloxRuntime.h
@@ -42,6 +42,7 @@ class VeloxRuntime final : public Runtime {
explicit VeloxRuntime(
const std::string& kind,
VeloxMemoryManager* vmm,
+ ThreadManager* threadManager,
const std::unordered_map<std::string, std::string>& confMap);
~VeloxRuntime() override;
diff --git a/cpp/velox/compute/WholeStageResultIterator.cc
b/cpp/velox/compute/WholeStageResultIterator.cc
index 753ccb2a68..b3d9f480c1 100644
--- a/cpp/velox/compute/WholeStageResultIterator.cc
+++ b/cpp/velox/compute/WholeStageResultIterator.cc
@@ -15,6 +15,9 @@
* limitations under the License.
*/
#include "WholeStageResultIterator.h"
+#include <folly/executors/CPUThreadPoolExecutor.h>
+#include <folly/executors/thread_factory/NamedThreadFactory.h>
+#include <folly/system/ThreadName.h>
#include <optional>
#include "VeloxBackend.h"
#include "VeloxPlanConverter.h"
@@ -137,27 +140,24 @@ WholeStageResultIterator::WholeStageResultIterator(
auto fileSystem = velox::filesystems::getFileSystem(spillDir, nullptr);
GLUTEN_CHECK(fileSystem != nullptr, "File System for spilling is null!");
fileSystem->mkdir(spillDir);
- velox::common::SpillDiskOptions spillOpts{
- .spillDirPath = spillDir, .spillDirCreated = true, .spillDirCreateCb =
nullptr};
- // Create task instance.
std::unordered_set<velox::core::PlanNodeId> emptySet;
- velox::core::PlanFragment planFragment{planNode,
velox::core::ExecutionStrategy::kUngrouped, 1, emptySet};
- std::shared_ptr<velox::core::QueryCtx> queryCtx = createNewVeloxQueryCtx();
- task_ = velox::exec::Task::create(
- fmt::format(
- "Gluten_Stage_{}_TID_{}_VTID_{}",
- std::to_string(taskInfo_.stageId),
- std::to_string(taskInfo_.taskId),
- std::to_string(taskInfo.vId)),
- std::move(planFragment),
- 0,
- std::move(queryCtx),
- velox::exec::Task::ExecutionMode::kSerial,
- /*consumer=*/velox::exec::Consumer{},
- /*memoryArbitrationPriority=*/0,
- /*spillDiskOpts=*/spillOpts,
- /*onError=*/nullptr);
+ const bool serialExecution = true;
+
+ facebook::velox::exec::CursorParameters params;
+ params.planNode = planNode;
+ params.destination = 0;
+ params.maxDrivers = 1;
+ params.queryCtx = createNewVeloxQueryCtx();
+ params.executionStrategy = velox::core::ExecutionStrategy::kUngrouped;
+ params.groupedExecutionLeafNodeIds = std::move(emptySet);
+ params.numSplitGroups = 1;
+ params.spillDirectory = spillDir;
+ params.serialExecution = serialExecution;
+ params.copyResult = false;
+ params.outputPool = memoryManager_->getLeafMemoryPool();
+ cursor_ = velox::exec::TaskCursor::create(params);
+ task_ = cursor_->task().get();
if (!task_->supportSerialExecutionMode()) {
throw std::runtime_error("Task doesn't support single threaded execution:
" + planNode->toString());
}
@@ -307,41 +307,24 @@ std::shared_ptr<velox::core::QueryCtx>
WholeStageResultIterator::createNewVeloxQ
}
std::shared_ptr<ColumnarBatch> WholeStageResultIterator::next() {
- if (task_->isFinished()) {
- return nullptr;
- }
- velox::RowVectorPtr vector;
while (true) {
- auto future = velox::ContinueFuture::makeEmpty();
- auto out = task_->next(&future);
- if (!future.valid()) {
- // Not need to wait. Break.
- vector = std::move(out);
- break;
+ if (!cursor_->moveNext()) {
+ return nullptr;
}
- // Velox suggested to wait. This might be because another thread (e.g.,
background io thread) is spilling the task.
- GLUTEN_CHECK(out == nullptr, "Expected to wait but still got non-null
output from Velox task");
- VLOG(2) << "Velox task " << task_->taskId()
- << " is busy when ::next() is called. Will wait and try again.
Task state: "
- << taskStateString(task_->state());
- future.wait();
- }
- if (vector == nullptr) {
- return nullptr;
- }
- uint64_t numRows = vector->size();
- if (numRows == 0) {
- return nullptr;
- }
-
- {
- ScopedTimer timer(&loadLazyVectorTime_);
- for (auto& child : vector->children()) {
- child->loadedVector();
+ RowVectorPtr vector = cursor_->current();
+ GLUTEN_CHECK(vector != nullptr, "Cursor returned null vector.");
+ uint64_t numRows = vector->size();
+ if (numRows == 0) {
+ continue;
+ }
+ {
+ ScopedTimer timer(&loadLazyVectorTime_);
+ for (auto& child : vector->children()) {
+ child->loadedVector();
+ }
}
+ return std::make_shared<VeloxColumnarBatch>(vector);
}
-
- return std::make_shared<VeloxColumnarBatch>(vector);
}
int64_t WholeStageResultIterator::spillFixedSize(int64_t size) {
@@ -373,9 +356,6 @@ void WholeStageResultIterator::getOrderedNodeIds(
std::vector<velox::core::PlanNodeId>& nodeIds) {
bool isProjectNode = (std::dynamic_pointer_cast<const
velox::core::ProjectNode>(planNode) != nullptr);
bool isLocalExchangeNode = (std::dynamic_pointer_cast<const
velox::core::LocalPartitionNode>(planNode) != nullptr);
- bool isUnionNode = isLocalExchangeNode &&
- std::dynamic_pointer_cast<const
velox::core::LocalPartitionNode>(planNode)->type() ==
- velox::core::LocalPartitionNode::Type::kGather;
const auto& sourceNodes = planNode->sources();
if (isProjectNode) {
GLUTEN_CHECK(sourceNodes.size() == 1, "Illegal state");
@@ -391,22 +371,24 @@ void WholeStageResultIterator::getOrderedNodeIds(
return;
}
- if (isUnionNode) {
- // FIXME: The whole metrics system in gluten-substrait is magic. Passing
metrics trees through JNI with a trivial
- // array is possible but requires for a solid design. Apparently we
haven't had it. All the code requires complete
- // rework.
- // Union was interpreted as LocalPartition + LocalExchange + 2 fake
projects as children in Velox. So we only fetch
- // metrics from the root node.
- std::vector<std::shared_ptr<const velox::core::PlanNode>> unionChildren{};
+ if (isLocalExchangeNode) {
+ // LocalPartition was interpreted as LocalPartition + LocalExchange + 2
fake projects (optional) as children
+ // in SubstraitToVeloxPlan. So we only fetch metrics from the root node.
for (const auto& source : planNode->sources()) {
const auto projectedChild = std::dynamic_pointer_cast<const
velox::core::ProjectNode>(source);
- GLUTEN_CHECK(projectedChild != nullptr, "Illegal state");
- const auto projectSources = projectedChild->sources();
- GLUTEN_CHECK(projectSources.size() == 1, "Illegal state");
- const auto projectSource = projectSources.at(0);
- getOrderedNodeIds(projectSource, nodeIds);
+ if (projectedChild != nullptr) {
+ const auto projectSources = projectedChild->sources();
+ GLUTEN_CHECK(projectSources.size() == 1, "Illegal state");
+ const auto projectSource = projectSources.at(0);
+ getOrderedNodeIds(projectSource, nodeIds);
+ } else {
+ getOrderedNodeIds(source, nodeIds);
+ }
+ }
+ if (planNode->sources().size() == 2) {
+ // The LocalPartition maps to a concrete Spark native union transformer
operator.
+ nodeIds.emplace_back(planNode->id());
}
- nodeIds.emplace_back(planNode->id());
return;
}
@@ -468,6 +450,7 @@ void WholeStageResultIterator::noMoreSplits() {
for (const auto& streamId : streamIds_) {
task_->noMoreSplits(streamId);
}
+ cursor_->setNoMoreSplits();
allSplitsAdded_ = true;
}
diff --git a/cpp/velox/compute/WholeStageResultIterator.h
b/cpp/velox/compute/WholeStageResultIterator.h
index 4fcc002ffd..ebb13d342f 100644
--- a/cpp/velox/compute/WholeStageResultIterator.h
+++ b/cpp/velox/compute/WholeStageResultIterator.h
@@ -28,6 +28,7 @@
#include "velox/common/config/Config.h"
#include "velox/connectors/hive/iceberg/IcebergSplit.h"
#include "velox/core/PlanNode.h"
+#include "velox/exec/Cursor.h"
#include "velox/exec/Task.h"
#ifdef GLUTEN_ENABLE_GPU
#include "cudf/GpuLock.h"
@@ -57,7 +58,8 @@ class WholeStageResultIterator : public
SplitAwareColumnarBatchIterator {
task_->requestCancel().wait();
}
auto deletionFuture = task_->taskDeletionFuture();
- task_.reset();
+ cursor_.reset();
+ task_ = nullptr;
deletionFuture.wait();
}
#ifdef GLUTEN_ENABLE_GPU
@@ -85,7 +87,7 @@ class WholeStageResultIterator : public
SplitAwareColumnarBatchIterator {
/// Get the underlying Velox task for direct manipulation
facebook::velox::exec::Task* task() {
- return task_.get();
+ return task_;
}
/// Add iterator-based splits from input iterators
@@ -137,12 +139,13 @@ class WholeStageResultIterator : public
SplitAwareColumnarBatchIterator {
#endif
const SparkTaskInfo taskInfo_;
folly::Executor* executor_;
- std::shared_ptr<facebook::velox::exec::Task> task_;
+ std::unique_ptr<facebook::velox::exec::TaskCursor> cursor_;
+ facebook::velox::exec::Task* task_ = nullptr;
std::shared_ptr<const facebook::velox::core::PlanNode> veloxPlan_;
/// Spill.
std::string spillStrategy_;
- folly::Executor* spillExecutor_ = nullptr;
+ folly::Executor* spillExecutor_;
VeloxConnectorIds connectorIds_;
/// Metrics
diff --git a/cpp/velox/config/VeloxConfig.h b/cpp/velox/config/VeloxConfig.h
index 89a9c4e05a..5d8c3bd4d9 100644
--- a/cpp/velox/config/VeloxConfig.h
+++ b/cpp/velox/config/VeloxConfig.h
@@ -142,8 +142,8 @@ const std::string kVeloxSsdCheckSumReadVerificationEnabled =
"spark.gluten.sql.columnar.backend.velox.ssdChecksumReadVerificationEnabled";
// async
+const std::string kNumTaskSlotsPerExecutor =
"spark.gluten.numTaskSlotsPerExecutor";
const std::string kVeloxIOThreads =
"spark.gluten.sql.columnar.backend.velox.IOThreads";
-const uint32_t kVeloxIOThreadsDefault = 0;
const std::string kVeloxAsyncTimeoutOnTaskStopping =
"spark.gluten.sql.columnar.backend.velox.asyncTimeoutOnTaskStopping";
const int32_t kVeloxAsyncTimeoutOnTaskStoppingDefault = 30000; // 30s
diff --git a/cpp/velox/jni/VeloxJniWrapper.cc b/cpp/velox/jni/VeloxJniWrapper.cc
index aa4d959943..317b9e56b1 100644
--- a/cpp/velox/jni/VeloxJniWrapper.cc
+++ b/cpp/velox/jni/VeloxJniWrapper.cc
@@ -1128,7 +1128,7 @@ JNIEXPORT jlong JNICALL
Java_org_apache_gluten_vectorized_HashJoinBuilder_native
facebook::velox::exec::BaseHashTable::kNoSpillInputStartPartitionBit,
hashTableBuilders[0]->joinBuildVectorHasherMaxNumDistinct(),
hashTableBuilders[0]->dropDuplicates(),
- allowParallelJoinBuild ? VeloxBackend::get()->executor() : nullptr);
+ allowParallelJoinBuild ? VeloxBackend::get()->ioExecutor() : nullptr);
for (int i = 1; i < numThreads; ++i) {
if (hashTableBuilders[i]->joinHasNullKeys()) {
diff --git a/cpp/velox/operators/plannodes/CudfVectorStream.h
b/cpp/velox/operators/plannodes/CudfVectorStream.h
index 7a663d252e..492b8556be 100644
--- a/cpp/velox/operators/plannodes/CudfVectorStream.h
+++ b/cpp/velox/operators/plannodes/CudfVectorStream.h
@@ -134,6 +134,11 @@ class CudfValueStreamNode final : public
facebook::velox::core::PlanNode {
std::shared_ptr<ResultIterator> iterator)
: facebook::velox::core::PlanNode(id), outputType_(outputType),
iterator_(std::move(iterator)) {}
+ // Only supports single thread because iterator_ is not guaranteed
thread-safe.
+ bool requiresSingleThread() const override {
+ return true;
+ }
+
const facebook::velox::RowTypePtr& outputType() const override {
return outputType_;
}
diff --git a/cpp/velox/tests/RuntimeTest.cc b/cpp/velox/tests/RuntimeTest.cc
index 9772d752b8..d01f52ae6c 100644
--- a/cpp/velox/tests/RuntimeTest.cc
+++ b/cpp/velox/tests/RuntimeTest.cc
@@ -20,6 +20,7 @@
#include <gtest/gtest.h>
#include "compute/VeloxBackend.h"
#include "memory.pb.h"
+#include "threads/ThreadInitializer.h"
namespace gluten {
@@ -46,13 +47,26 @@ class DummyMemoryManager final : public MemoryManager {
inline static const std::string kDummyBackendKind{"dummy"};
+class DummyThreadManager final : public ThreadManager {
+ public:
+ explicit DummyThreadManager(const std::string& kind) : ThreadManager(kind),
initializer_(ThreadInitializer::noop()) {}
+
+ ThreadInitializer* getThreadInitializer() override {
+ return initializer_.get();
+ }
+
+ private:
+ std::shared_ptr<ThreadInitializer> initializer_;
+};
+
class DummyRuntime final : public Runtime {
public:
DummyRuntime(
const std::string& kind,
DummyMemoryManager* mm,
+ ThreadManager* tm,
const std::unordered_map<std::string, std::string>& conf)
- : Runtime(kind, mm, conf) {}
+ : Runtime(kind, mm, tm, conf) {}
void parsePlan(const uint8_t* data, int32_t size) override {}
@@ -127,8 +141,9 @@ class DummyRuntime final : public Runtime {
static Runtime* dummyRuntimeFactory(
const std::string& kind,
MemoryManager* mm,
+ ThreadManager* tm,
const std::unordered_map<std::string, std::string> conf) {
- return new DummyRuntime(kind, dynamic_cast<DummyMemoryManager*>(mm), conf);
+ return new DummyRuntime(kind, dynamic_cast<DummyMemoryManager*>(mm), tm,
conf);
}
static void dummyRuntimeReleaser(Runtime* runtime) {
@@ -138,7 +153,8 @@ static void dummyRuntimeReleaser(Runtime* runtime) {
TEST(TestRuntime, CreateRuntime) {
Runtime::registerFactory(kDummyBackendKind, dummyRuntimeFactory,
dummyRuntimeReleaser);
DummyMemoryManager mm(kDummyBackendKind);
- auto runtime = Runtime::create(kDummyBackendKind, &mm);
+ DummyThreadManager tm(kDummyBackendKind);
+ auto runtime = Runtime::create(kDummyBackendKind, &mm, &tm);
ASSERT_EQ(typeid(*runtime), typeid(DummyRuntime));
Runtime::release(runtime);
}
@@ -146,14 +162,18 @@ TEST(TestRuntime, CreateRuntime) {
TEST(TestRuntime, CreateVeloxRuntime) {
VeloxBackend::create(AllocationListener::noop(), {});
auto mm = MemoryManager::create(kVeloxBackendKind,
AllocationListener::noop());
- auto runtime = Runtime::create(kVeloxBackendKind, mm);
+ auto tm = ThreadManager::create(kVeloxBackendKind,
ThreadInitializer::noop());
+ auto runtime = Runtime::create(kVeloxBackendKind, mm, tm);
ASSERT_EQ(typeid(*runtime), typeid(VeloxRuntime));
Runtime::release(runtime);
+ ThreadManager::release(tm);
}
TEST(TestRuntime, GetResultIterator) {
DummyMemoryManager mm(kDummyBackendKind);
- auto runtime = std::make_shared<DummyRuntime>(kDummyBackendKind, &mm,
std::unordered_map<std::string, std::string>());
+ DummyThreadManager tm(kDummyBackendKind);
+ auto runtime =
+ std::make_shared<DummyRuntime>(kDummyBackendKind, &mm, &tm,
std::unordered_map<std::string, std::string>());
auto iter = runtime->createResultIterator("/tmp/test-spill", {});
runtime->noMoreSplits(iter.get());
ASSERT_TRUE(iter->hasNext());
diff --git a/cpp/velox/tests/Substrait2VeloxPlanConversionTest.cc
b/cpp/velox/tests/Substrait2VeloxPlanConversionTest.cc
index a1d1cea02b..0081bf3004 100644
--- a/cpp/velox/tests/Substrait2VeloxPlanConversionTest.cc
+++ b/cpp/velox/tests/Substrait2VeloxPlanConversionTest.cc
@@ -28,6 +28,7 @@
#include "velox/type/Type.h"
#include "FilePathGenerator.h"
+#include "compute/VeloxBackend.h"
using namespace facebook::velox;
using namespace facebook::velox::test;
diff --git
a/gluten-arrow/src/main/java/org/apache/gluten/runtime/RuntimeJniWrapper.java
b/gluten-arrow/src/main/java/org/apache/gluten/runtime/RuntimeJniWrapper.java
index 8c4280a3b5..ed22759672 100644
---
a/gluten-arrow/src/main/java/org/apache/gluten/runtime/RuntimeJniWrapper.java
+++
b/gluten-arrow/src/main/java/org/apache/gluten/runtime/RuntimeJniWrapper.java
@@ -20,7 +20,8 @@ public class RuntimeJniWrapper {
private RuntimeJniWrapper() {}
- public static native long createRuntime(String backendType, long nmm, byte[]
sessionConf);
+ public static native long createRuntime(
+ String backendType, long nmm, long ntm, byte[] sessionConf);
public static native void releaseRuntime(long handle);
}
diff --git
a/gluten-arrow/src/main/java/org/apache/gluten/threads/NativeThreadInitializer.java
b/gluten-arrow/src/main/java/org/apache/gluten/threads/NativeThreadInitializer.java
new file mode 100644
index 0000000000..efa35d010e
--- /dev/null
+++
b/gluten-arrow/src/main/java/org/apache/gluten/threads/NativeThreadInitializer.java
@@ -0,0 +1,44 @@
+/*
+ * 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.threads;
+
+/**
+ * Java-side callback invoked by native code when a managed worker thread is
created or destroyed.
+ *
+ * <p>Implementations are responsible for installing per-thread context — such
as {@link
+ * org.apache.spark.TaskContext} — so that native worker threads behave as
proper child threads of
+ * the Spark task.
+ *
+ * <p>Implementations must be thread-safe; {@link #initialize} and {@link
#destroy} may be called
+ * concurrently from different native threads.
+ */
+public interface NativeThreadInitializer {
+ /**
+ * Called when a native worker thread is about to start executing tasks.
+ *
+ * @param threadName a human-readable name identifying the thread.
+ */
+ void initialize(String threadName);
+
+ /**
+ * Called when a native worker thread is about to be returned to the pool or
destroyed. Must not
+ * detach the JNI thread — the thread may be reused.
+ *
+ * @param threadName the same name passed to {@link #initialize}.
+ */
+ void destroy(String threadName);
+}
diff --git
a/gluten-arrow/src/main/java/org/apache/gluten/threads/NativeThreadManagerJniWrapper.java
b/gluten-arrow/src/main/java/org/apache/gluten/threads/NativeThreadManagerJniWrapper.java
new file mode 100644
index 0000000000..ec5ad963cd
--- /dev/null
+++
b/gluten-arrow/src/main/java/org/apache/gluten/threads/NativeThreadManagerJniWrapper.java
@@ -0,0 +1,44 @@
+/*
+ * 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.threads;
+
+/**
+ * JNI bridge for native ThreadManager creation and release.
+ *
+ * <p>Each Spark task creates one native ThreadManager (via {@link #create})
that wraps a {@link
+ * NativeThreadInitializer}. The returned handle is passed to the native
Runtime factory so it can
+ * install thread lifecycle callbacks on its worker executors.
+ */
+public class NativeThreadManagerJniWrapper {
+ private NativeThreadManagerJniWrapper() {}
+
+ /**
+ * Create a native ThreadManager for the given backend type.
+ *
+ * @param backendType the backend kind string (e.g., "velox").
+ * @param initializer the Java-side callback invoked on worker thread
create/destroy.
+ * @return opaque native handle.
+ */
+ public static native long create(String backendType, NativeThreadInitializer
initializer);
+
+ /**
+ * Release a native ThreadManager previously created with {@link #create}.
+ *
+ * @param handle the native handle returned by {@link #create}.
+ */
+ public static native void release(long handle);
+}
diff --git
a/gluten-arrow/src/main/java/org/apache/gluten/threads/TaskChildThreadInitializer.java
b/gluten-arrow/src/main/java/org/apache/gluten/threads/TaskChildThreadInitializer.java
new file mode 100644
index 0000000000..3a7d15be6e
--- /dev/null
+++
b/gluten-arrow/src/main/java/org/apache/gluten/threads/TaskChildThreadInitializer.java
@@ -0,0 +1,67 @@
+/*
+ * 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.threads;
+
+import com.google.common.base.Preconditions;
+import org.apache.spark.TaskContext;
+import org.apache.spark.util.SparkTaskUtil;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Propagates the parent Spark task's {@link TaskContext} to native worker
threads so they inherit
+ * the task's thread-local properties (e.g., local properties, job group, task
attempt ID).
+ *
+ * <p>Each native thread that enters {@link #initialize} gets the parent
{@code TaskContext}
+ * installed via {@link SparkTaskUtil#setTaskContext}. When the thread
finishes its work, {@link
+ * #destroy} clears the context so the thread can be safely returned to the
pool.
+ *
+ * <p>The internal map tracks which native thread names are currently active;
initializing an
+ * already-active thread or destroying an unknown thread is treated as a
programming error and
+ * throws {@link IllegalStateException}.
+ */
+public class TaskChildThreadInitializer implements NativeThreadInitializer {
+ private final TaskContext parentTaskContext;
+ private final Map<String, String> childThreads = new ConcurrentHashMap<>();
+
+ public TaskChildThreadInitializer(TaskContext parentTaskContext) {
+ Preconditions.checkNotNull(parentTaskContext);
+ this.parentTaskContext = parentTaskContext;
+ }
+
+ @Override
+ public void initialize(String threadName) {
+ final String javaThreadName = Thread.currentThread().getName();
+ if (childThreads.put(threadName, javaThreadName) != null) {
+ throw new IllegalStateException(
+ String.format(
+ "Task native child thread %s (Java name: %s) is already
initialized",
+ threadName, javaThreadName));
+ }
+ SparkTaskUtil.setTaskContext(parentTaskContext);
+ }
+
+ @Override
+ public void destroy(String threadName) {
+ if (childThreads.remove(threadName) == null) {
+ throw new IllegalStateException(
+ String.format("Task native thread %s is not initialized",
threadName));
+ }
+ SparkTaskUtil.unsetTaskContext();
+ }
+}
diff --git
a/gluten-arrow/src/main/scala/org/apache/gluten/runtime/Runtime.scala
b/gluten-arrow/src/main/scala/org/apache/gluten/runtime/Runtime.scala
index e57bec619d..2e3c468140 100644
--- a/gluten-arrow/src/main/scala/org/apache/gluten/runtime/Runtime.scala
+++ b/gluten-arrow/src/main/scala/org/apache/gluten/runtime/Runtime.scala
@@ -19,10 +19,11 @@ package org.apache.gluten.runtime
import org.apache.gluten.config.GlutenConfig
import org.apache.gluten.exception.GlutenException
import org.apache.gluten.memory.NativeMemoryManager
+import org.apache.gluten.threads.{NativeThreadManager,
TaskChildThreadInitializer}
import org.apache.gluten.utils.ConfigUtil
import org.apache.spark.sql.internal.{GlutenConfigUtil, SQLConf}
-import org.apache.spark.task.TaskResource
+import org.apache.spark.task.{TaskResource, TaskResources}
import java.util
import java.util.concurrent.atomic.AtomicBoolean
@@ -31,6 +32,7 @@ import scala.collection.JavaConverters._ // for 2.12
trait Runtime {
def memoryManager(): NativeMemoryManager
+ def threadManager(): NativeThreadManager
def getHandle(): Long
}
@@ -51,9 +53,13 @@ object Runtime {
with TaskResource {
private val nmm: NativeMemoryManager = NativeMemoryManager(backendName,
name)
+ private val ntm: NativeThreadManager = NativeThreadManager(
+ backendName,
+ new TaskChildThreadInitializer(TaskResources.getLocalTaskContext()))
private val handle = RuntimeJniWrapper.createRuntime(
backendName,
nmm.getHandle(),
+ ntm.getHandle(),
ConfigUtil.serialize(
(GlutenConfig
.getNativeSessionConf(
@@ -67,6 +73,8 @@ object Runtime {
override def memoryManager(): NativeMemoryManager = nmm
+ override def threadManager(): NativeThreadManager = ntm
+
override def release(): Unit = {
if (!released.compareAndSet(false, true)) {
throw new GlutenException(
diff --git
a/gluten-arrow/src/main/scala/org/apache/gluten/threads/NativeThreadManager.scala
b/gluten-arrow/src/main/scala/org/apache/gluten/threads/NativeThreadManager.scala
new file mode 100644
index 0000000000..8abe6d9c9b
--- /dev/null
+++
b/gluten-arrow/src/main/scala/org/apache/gluten/threads/NativeThreadManager.scala
@@ -0,0 +1,75 @@
+/*
+ * 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.threads
+
+import org.apache.gluten.exception.GlutenException
+
+import org.apache.spark.task.{TaskResource, TaskResources}
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Scala wrapper around a native ThreadManager handle.
+ *
+ * Created once per Spark task and registered as a [[TaskResource]] so it is
automatically released
+ * when the task completes. The ThreadManager wraps a
[[NativeThreadInitializer]] that propagates
+ * task context to native worker threads spawned by folly executors.
+ */
+trait NativeThreadManager {
+
+ /** @return opaque native handle passed to RuntimeJniWrapper#createRuntime.
*/
+ def getHandle(): Long
+}
+
+object NativeThreadManager {
+ private class Impl(
+ private val backendName: String,
+ private val initializer: NativeThreadInitializer)
+ extends NativeThreadManager
+ with TaskResource {
+ private val handle = NativeThreadManagerJniWrapper.create(backendName,
initializer)
+ private val released = new AtomicBoolean(false)
+
+ override def getHandle(): Long = handle
+
+ override def release(): Unit = {
+ if (!released.compareAndSet(false, true)) {
+ throw new GlutenException(
+ s"Thread manager instance already released: $handle,
${resourceName()}, ${priority()}")
+ }
+ NativeThreadManagerJniWrapper.release(handle)
+ }
+
+ // Release before MemoryManager (10) but after most other resources.
+ override def priority(): Int = 20
+
+ override def resourceName(): String = "ntm"
+ }
+
+ /**
+ * Create a new NativeThreadManager and register it with the current Spark
task's
+ * [[TaskResources]] so it is automatically released when the task finishes.
+ *
+ * @param backendName
+ * the backend kind string (e.g., "velox").
+ * @param initializer
+ * callback invoked when native worker threads are created / destroyed.
+ */
+ def apply(backendName: String, initializer: NativeThreadInitializer):
NativeThreadManager = {
+ TaskResources.addAnonymousResource(new Impl(backendName, initializer))
+ }
+}
diff --git
a/gluten-core/src/main/scala/org/apache/spark/task/TaskResources.scala
b/gluten-core/src/main/scala/org/apache/spark/task/TaskResources.scala
index 2c386e52ff..1b148d9b2a 100644
--- a/gluten-core/src/main/scala/org/apache/spark/task/TaskResources.scala
+++ b/gluten-core/src/main/scala/org/apache/spark/task/TaskResources.scala
@@ -140,7 +140,7 @@ object TaskResources extends TaskListener with Logging {
if (!inSparkTask()) {
throw new UnsupportedOperationException(
"Not in a Spark task. If the code is running on driver or for testing
purpose, " +
- "try using TaskResources#runUnsafe")
+ "try using TaskResources#runUnsafe. Current thread: " +
Thread.currentThread().getName)
}
val tc = getLocalTaskContext()
RESOURCE_REGISTRIES.synchronized {
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 fad32a0489..14610868eb 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
@@ -630,10 +630,8 @@ object GlutenConfig extends ConfigRegistry {
(SPARK_S3_CONNECTION_MAXIMUM, "15"),
("spark.gluten.velox.fs.s3a.retry.mode", "legacy"),
(
- "spark.gluten.sql.columnar.backend.velox.IOThreads",
- conf.getOrElse(
- GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR.key,
- GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR.defaultValueString)),
+ GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR.key,
+ GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR.defaultValueString),
(COLUMNAR_SHUFFLE_CODEC.key, ""),
(COLUMNAR_SHUFFLE_CODEC_BACKEND.key, ""),
(DEBUG_CUDF.key, DEBUG_CUDF.defaultValueString),
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]