From b37a2cc0aa06a950c163435ea6a1668397c4a1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kopci=C5=84ski?= Date: Mon, 18 Aug 2025 11:03:52 +0200 Subject: [PATCH 1/7] Initial work for global thread pool --- apps/llm/app/llm/index.tsx | 10 +- .../common/rnexecutorch/GlobalThreadPool.h | 80 +++ .../rnexecutorch/HighPerformanceThreadPool.h | 454 ++++++++++++++++++ .../common/rnexecutorch/Log.h | 65 +-- .../rnexecutorch/RnExecutorchInstaller.cpp | 8 + .../common/rnexecutorch/ThreadPoolModule.h | 225 +++++++++ .../host_objects/ModelHostObject.h | 85 +++- .../common/rnexecutorch/models/llm/LLM.cpp | 12 +- .../react-native-executorch/src/ThreadPool.ts | 34 ++ 9 files changed, 932 insertions(+), 41 deletions(-) create mode 100644 packages/react-native-executorch/common/rnexecutorch/GlobalThreadPool.h create mode 100644 packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h create mode 100644 packages/react-native-executorch/common/rnexecutorch/ThreadPoolModule.h create mode 100644 packages/react-native-executorch/src/ThreadPool.ts diff --git a/apps/llm/app/llm/index.tsx b/apps/llm/app/llm/index.tsx index e6ee183ee..a1e8252e6 100644 --- a/apps/llm/app/llm/index.tsx +++ b/apps/llm/app/llm/index.tsx @@ -68,7 +68,7 @@ function LLMScreen() { keyboardVerticalOffset={Platform.OS === 'ios' ? 120 : 40} > - {llm.messageHistory.length ? ( + {/* {llm.messageHistory.length ? ( - )} + )} */} + + Hello! 👋 + + What can I help you with? + + +#include + +class GlobalThreadPool { +private: + inline static std::unique_ptr instance; + inline static std::once_flag initFlag; + + GlobalThreadPool() = delete; + +public: + // Initialize the global thread pool (call once at app startup) + static void initialize(size_t numThreads = 0, ThreadConfig config = {}) { + std::call_once(initFlag, [&numThreads, config]() { + // Auto-detect optimal thread count if not specified + if (numThreads == 0) { + numThreads = std::thread::hardware_concurrency(); + numThreads = std::min(numThreads, size_t(4)); // Cap at 4 for mobile + } + + LOGI("Initializing global thread pool with %zu threads", numThreads); + instance = + std::make_unique(numThreads, config); + }); + } + + // Get the global thread pool instance + static HighPerformanceThreadPool &get() { + if (!instance) { + // Auto-initialize with defaults if not already initialized + initialize(); + } + return *instance; + } + + // Convenience methods that mirror std::thread interface + template + static auto async(Func &&func, Args &&...args) { + return get().submit(std::forward(func), std::forward(args)...); + } + + template + static auto async_high_priority(Func &&func, Args &&...args) { + return get().submitWithPriority(Priority::HIGH, std::forward(func), + std::forward(args)...); + } + + // Fire and forget (like std::thread{}.detach()) + template + static void detach(Func &&func, Args &&...args) { + get().submitDetached(std::forward(func), std::forward(args)...); + } + + // Execute and wait (like std::thread{}.join()) + template + static auto execute(Func &&func, Args &&...args) { + return get().execute(std::forward(func), std::forward(args)...); + } + + static void shutdown() { + if (instance) { + instance->shutdown(); + instance.reset(); + } + } +}; + +// Static member definitions +// std::unique_ptr GlobalThreadPool::instance; +// std::once_flag GlobalThreadPool::initFlag; + +// Convenience macros for even simpler usage +#define ASYNC_TASK(...) GlobalThreadPool::async(__VA_ARGS__) +#define ASYNC_HIGH(...) GlobalThreadPool::async_high_priority(__VA_ARGS__) +#define ASYNC_DETACH(...) GlobalThreadPool::detach(__VA_ARGS__) +#define ASYNC_WAIT(...) GlobalThreadPool::execute(__VA_ARGS__) \ No newline at end of file diff --git a/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h new file mode 100644 index 000000000..39834aa14 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h @@ -0,0 +1,454 @@ +// HighPerformanceThreadPool.h +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_TAG "HPThreadPool" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +enum class Priority { LOW, NORMAL, HIGH, REALTIME }; + +struct ThreadConfig { + bool pinToPerformanceCores{true}; + Priority priority{Priority::HIGH}; + size_t stackSize{8 * 1024 * 1024}; // 8MB default + std::optional> specificCores; // Pin to specific cores + std::string namePrefix{"HPWorker"}; +}; + +class HighPerformanceThreadPool { +public: +private: + // Task wrapper that can hold any callable + class ITask { + public: + virtual ~ITask() = default; + virtual void execute() = 0; + }; + + template class Task : public ITask { + private: + Func func; + std::promise promise; + + public: + Task(Func &&f) : func(std::forward(f)) {} + + void execute() override { + try { + if constexpr (std::is_void_v) { + func(); + promise.set_value(); + } else { + promise.set_value(func()); + } + } catch (...) { + promise.set_exception(std::current_exception()); + } + } + + std::future getFuture() { return promise.get_future(); } + }; + + struct WorkItem { + std::unique_ptr task; + Priority priority; + std::chrono::steady_clock::time_point enqueueTime; + + bool operator<(const WorkItem &other) const { + // Higher priority first, then earlier enqueue time + if (priority != other.priority) { + return priority < other.priority; + } + return enqueueTime > other.enqueueTime; + } + }; + + // Thread pool state + std::vector workers; + std::priority_queue taskQueue; + std::mutex queueMutex; + std::condition_variable condition; + std::atomic running{true}; + std::atomic activeWorkers{0}; + std::atomic totalTasksProcessed{0}; + + // Performance cores + std::vector performanceCores; + std::vector efficiencyCores; + + // Configuration + ThreadConfig config; + + // Statistics + struct Stats { + std::atomic tasksCompleted{0}; + std::atomic tasksFailed{0}; + std::atomic totalWaitTimeMs{0}; + std::atomic totalExecutionTimeMs{0}; + } stats; + +private: + void detectCPUTopology() { + struct CoreInfo { + int id; + long maxFreq; + }; + + std::vector cores; + + for (int i = 0; i < 32; i++) { // Check up to 32 cores + std::string path = "/sys/devices/system/cpu/cpu" + std::to_string(i) + + "/cpufreq/cpuinfo_max_freq"; + std::ifstream file(path); + if (!file.good()) + break; + + CoreInfo info; + info.id = i; + file >> info.maxFreq; + cores.push_back(info); + } + + if (cores.empty()) { + log(rnexecutorch::LOG_LEVEL::Error, "Could not detect CPU topology"); + return; + } + + // Sort by frequency + std::sort(cores.begin(), cores.end(), + [](const CoreInfo &a, const CoreInfo &b) { + return a.maxFreq > b.maxFreq; + }); + + // Classify cores + long highestFreq = cores[0].maxFreq; + long lowestFreq = cores.back().maxFreq; + long threshold = lowestFreq + (highestFreq - lowestFreq) * 0.6; + + for (const auto &core : cores) { + if (core.maxFreq >= threshold) { + performanceCores.push_back(core.id); + log(rnexecutorch::LOG_LEVEL::Error, "Performance core: %d (%.2f GHz)", + core.id, core.maxFreq / 1000000.0); + } else { + efficiencyCores.push_back(core.id); + log(rnexecutorch::LOG_LEVEL::Error, "Efficiency core: %d (%.2f GHz)", + core.id, core.maxFreq / 1000000.0); + } + } + } + + void configureThread(int workerIndex) { + // Set thread name + std::string threadName = config.namePrefix + std::to_string(workerIndex); + pthread_setname_np(pthread_self(), threadName.c_str()); + + // Configure CPU affinity + if (config.specificCores.has_value()) { + // Pin to specific cores provided by user + setCPUAffinity(config.specificCores.value()); + } else if (config.pinToPerformanceCores && !performanceCores.empty()) { + // Pin to performance cores + setCPUAffinity(performanceCores); + } + + // Set thread priority + setThreadPriority(config.priority); + + log(rnexecutorch::LOG_LEVEL::Error, "Worker %d configured: %s", workerIndex, + threadName.c_str()); + } + + void setCPUAffinity(const std::vector &cores) { + if (cores.empty()) { + log(rnexecutorch::LOG_LEVEL::Error, + "No cores specified for affinity setting"); + return; + } + + // Validate core indices first + int maxCores = std::thread::hardware_concurrency(); + for (int core : cores) { + if (core < 0 || core >= maxCores) { + log(rnexecutorch::LOG_LEVEL::Error, "Invalid core index %d (max: %d)", + core, maxCores - 1); + return; + } + } + + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + + for (int core : cores) { + CPU_SET(core, &cpuset); + } + + // Use sched_setaffinity for Android compatibility + pid_t tid = gettid(); + log(rnexecutorch::LOG_LEVEL::Info, "Thread id ", tid); + if (sched_setaffinity(tid, sizeof(cpuset), &cpuset) == 0) { + std::string coreList; + for (size_t i = 0; i < cores.size(); ++i) { + coreList += std::to_string(cores[i]); + if (i < cores.size() - 1) + coreList += ","; + } + log(rnexecutorch::LOG_LEVEL::Info, "Thread pinned to cores: %s", + coreList.c_str()); + } else { + log(rnexecutorch::LOG_LEVEL::Error, + "Failed to set CPU affinity (error: %d). Continuing without " + "affinity.", + errno); + } + } + + void setThreadPriority(Priority priority) { + int nice_value = 0; + int sched_policy = SCHED_OTHER; + int sched_priority = 0; + + switch (priority) { + case Priority::LOW: + nice_value = 10; + break; + case Priority::NORMAL: + nice_value = 0; + break; + case Priority::HIGH: + nice_value = -10; + sched_policy = SCHED_FIFO; + sched_priority = sched_get_priority_min(SCHED_FIFO); + break; + case Priority::REALTIME: + nice_value = -20; + sched_policy = SCHED_FIFO; + sched_priority = sched_get_priority_max(SCHED_FIFO) - 1; + break; + } + + // Try to set real-time scheduling + if (sched_policy != SCHED_OTHER) { + struct sched_param param; + param.sched_priority = sched_priority; + if (pthread_setschedparam(pthread_self(), sched_policy, ¶m) != 0) { + log(rnexecutorch::LOG_LEVEL::Error, + "Failed to set real-time scheduling, falling back to nice value"); + } + return; + } + + // Set nice value as fallback or additional priority boost + if (setpriority(PRIO_PROCESS, 0, nice_value) != 0) { + log(rnexecutorch::LOG_LEVEL::Error, "Failed to set nice value"); + } else { + log(rnexecutorch::LOG_LEVEL::Error, "Set nice value", nice_value); + } + } + + void workerThread(int workerIndex) { + configureThread(workerIndex); + + while (running) { + WorkItem item; + + { + std::unique_lock lock(queueMutex); + condition.wait(lock, [this] { return !taskQueue.empty() || !running; }); + + if (!running && taskQueue.empty()) + break; + + if (!taskQueue.empty()) { + item = std::move(const_cast(taskQueue.top())); + taskQueue.pop(); + } else { + continue; + } + } + + // Process task + activeWorkers++; + + auto startTime = std::chrono::steady_clock::now(); + auto waitTime = std::chrono::duration_cast( + startTime - item.enqueueTime) + .count(); + stats.totalWaitTimeMs += waitTime; + + try { + item.task->execute(); + stats.tasksCompleted++; + } catch (const std::exception &e) { + LOGE("Task failed: %s", e.what()); + stats.tasksFailed++; + } + + auto endTime = std::chrono::steady_clock::now(); + auto executionTime = + std::chrono::duration_cast(endTime - + startTime) + .count(); + stats.totalExecutionTimeMs += executionTime; + + activeWorkers--; + totalTasksProcessed++; + } + + LOGI("Worker %d shutting down", workerIndex); + } + +public: + explicit HighPerformanceThreadPool(size_t numThreads = 1, + ThreadConfig cfg = ThreadConfig()) + : config(std::move(cfg)) { + + detectCPUTopology(); + + // Limit threads for CPU-bound tasks + numThreads = std::min( + numThreads, + size_t(performanceCores.empty() ? 4 : performanceCores.size())); + + // Create worker threads + for (size_t i = 0; i < numThreads; i++) { + workers.emplace_back(&HighPerformanceThreadPool::workerThread, this, i); + } + + log(rnexecutorch::LOG_LEVEL::Error, + "Thread pool initialized with %zu workers", numThreads); + } + + ~HighPerformanceThreadPool() { shutdown(); } + + // Submit a task and get a future for the result + template + auto submit(Func &&func, Args &&...args) + -> std::future { + return submitWithPriority(Priority::NORMAL, std::forward(func), + std::forward(args)...); + } + + // Submit a task with specific priority + template + auto submitWithPriority(Priority priority, Func &&func, Args &&...args) + -> std::future { + + using ReturnType = decltype(func(args...)); + + // Create a packaged task + auto boundFunc = + std::bind(std::forward(func), std::forward(args)...); + auto task = std::make_unique>( + std::move(boundFunc)); + auto future = task->getFuture(); + + // Add to queue + { + std::lock_guard lock(queueMutex); + + if (!running) { + throw std::runtime_error("Thread pool is shutting down"); + } + + WorkItem item; + item.task = std::move(task); + item.priority = priority; + item.enqueueTime = std::chrono::steady_clock::now(); + + taskQueue.push(std::move(item)); + } + + condition.notify_one(); + return future; + } + + // Execute a task and wait for result + template + auto execute(Func &&func, Args &&...args) -> decltype(func(args...)) { + auto future = submit(std::forward(func), std::forward(args)...); + return future.get(); + } + + // Fire and forget task + template + void submitDetached(Func &&func, Args &&...args) { + submit(std::forward(func), std::forward(args)...); + // Future is destroyed, task still runs + } + + void shutdown() { + if (!running.exchange(false)) + return; + + condition.notify_all(); + + for (auto &worker : workers) { + if (worker.joinable()) { + worker.join(); + } + } + + LOGI("Thread pool shut down. Stats: completed=%llu, failed=%llu, " + "avg_wait=%.1fms, avg_exec=%.1fms", + stats.tasksCompleted.load(), stats.tasksFailed.load(), + stats.tasksCompleted > 0 + ? (double)stats.totalWaitTimeMs / stats.tasksCompleted + : 0, + stats.tasksCompleted > 0 + ? (double)stats.totalExecutionTimeMs / stats.tasksCompleted + : 0); + } + + // Get pool statistics + struct PoolStats { + size_t queueSize; + size_t activeWorkers; + size_t totalWorkers; + uint64_t tasksCompleted; + uint64_t tasksFailed; + double avgWaitTimeMs; + double avgExecutionTimeMs; + }; + + PoolStats getStats() const { + std::lock_guard lock(const_cast(queueMutex)); + + PoolStats poolStats; + poolStats.queueSize = taskQueue.size(); + poolStats.activeWorkers = activeWorkers.load(); + poolStats.totalWorkers = workers.size(); + poolStats.tasksCompleted = stats.tasksCompleted.load(); + poolStats.tasksFailed = stats.tasksFailed.load(); + poolStats.avgWaitTimeMs = + poolStats.tasksCompleted > 0 + ? (double)stats.totalWaitTimeMs / poolStats.tasksCompleted + : 0; + poolStats.avgExecutionTimeMs = + poolStats.tasksCompleted > 0 + ? (double)stats.totalExecutionTimeMs / poolStats.tasksCompleted + : 0; + + return poolStats; + } +}; \ No newline at end of file diff --git a/packages/react-native-executorch/common/rnexecutorch/Log.h b/packages/react-native-executorch/common/rnexecutorch/Log.h index 9381324ab..716b9d477 100644 --- a/packages/react-native-executorch/common/rnexecutorch/Log.h +++ b/packages/react-native-executorch/common/rnexecutorch/Log.h @@ -103,66 +103,66 @@ concept Fallback = template requires concepts::Streamable && (!concepts::SmartPointer) -void printElement(std::ostream &os, const T &value); +inline void printElement(std::ostream &os, const T &value); template -void printElement(std::ostream &os, const std::pair &p); +inline void printElement(std::ostream &os, const std::pair &p); template -void printElement(std::ostream &os, const char (&array)[N]); +inline void printElement(std::ostream &os, const char (&array)[N]); template -void printElement(std::ostream &os, T (&array)[N]); +inline void printElement(std::ostream &os, T (&array)[N]); template requires concepts::Iterable && (!concepts::Streamable) -void printElement(std::ostream &os, const T &container); +inline void printElement(std::ostream &os, const T &container); template void printSequencable(std::ostream &os, T &&container); template requires concepts::ReadOnlySequencable -void printElement(std::ostream &os, const T &container); +inline void printElement(std::ostream &os, const T &container); template requires concepts::MutableSequencable -void printElement(std::ostream &os, T &&container); +inline void printElement(std::ostream &os, T &&container); template -void printElement(std::ostream &os, const std::tuple &tpl); +inline void printElement(std::ostream &os, const std::tuple &tpl); template -void printElement(std::ostream &os, const SP &ptr); +inline void printElement(std::ostream &os, const SP &ptr); template -void printElement(std::ostream &os, const WP &ptr); +inline void printElement(std::ostream &os, const WP &ptr); template -void printElement(std::ostream &os, const std::optional &opt); +inline void printElement(std::ostream &os, const std::optional &opt); template -void printElement(std::ostream &os, const std::variant &var); +inline void printElement(std::ostream &os, const std::variant &var); -void printElement(std::ostream &os, const std::exception_ptr &exPtr); +inline void printElement(std::ostream &os, const std::exception_ptr &exPtr); -void printElement(std::ostream &os, const std::filesystem::path &path); +inline void printElement(std::ostream &os, const std::filesystem::path &path); -void printElement(std::ostream &os, - const std::filesystem::directory_iterator &dir_it); +inline void printElement(std::ostream &os, + const std::filesystem::directory_iterator &dir_it); template -void printElement(std::ostream &os, const UnsupportedArg &value); +inline void printElement(std::ostream &os, const UnsupportedArg &value); -void printElement(std::ostream &os, const std::error_code &ec); +inline void printElement(std::ostream &os, const std::error_code &ec); template requires concepts::Streamable && (!concepts::SmartPointer) -void printElement(std::ostream &os, const T &value) { +inline void printElement(std::ostream &os, const T &value) { os << value; } template -void printElement(std::ostream &os, const std::pair &p) { +inline void printElement(std::ostream &os, const std::pair &p) { os << "("; printElement(os, p.first); os << ", "; @@ -171,7 +171,7 @@ void printElement(std::ostream &os, const std::pair &p) { } template -void printElement(std::ostream &os, const char (&array)[N]) { +inline void printElement(std::ostream &os, const char (&array)[N]) { // Treats the input as a string up to length N, drop null termination if (N > 1) { os << std::string_view(array, N - 1); @@ -180,7 +180,7 @@ void printElement(std::ostream &os, const char (&array)[N]) { // A special function for C-style arrays deducing size via template template -void printElement(std::ostream &os, T (&array)[N]) { +inline void printElement(std::ostream &os, T (&array)[N]) { os << "["; for (std::size_t i = 0; i < N; ++i) { if (i > 0) { @@ -193,7 +193,7 @@ void printElement(std::ostream &os, T (&array)[N]) { template requires concepts::Iterable && (!concepts::Streamable) -void printElement(std::ostream &os, const T &container) { +inline void printElement(std::ostream &os, const T &container) { os << "["; auto it = std::begin(container); if (it != std::end(container)) { @@ -206,7 +206,8 @@ void printElement(std::ostream &os, const T &container) { os << "]"; } -template void printSequencable(std::ostream &os, T &&container) { +template +inline void printSequencable(std::ostream &os, T &&container) { os << "["; bool isFirst = true; @@ -233,7 +234,7 @@ template void printSequencable(std::ostream &os, T &&container) { template requires concepts::ReadOnlySequencable -void printElement(std::ostream &os, const T &container) { +inline void printElement(std::ostream &os, const T &container) { T tempContainer = container; // Make a copy to preserve original container printSequencable( os, std::move(tempContainer)); // Use std::move since tempContainer won't @@ -242,12 +243,12 @@ void printElement(std::ostream &os, const T &container) { template requires concepts::MutableSequencable -void printElement(std::ostream &os, T &&container) { +inline void printElement(std::ostream &os, T &&container) { printSequencable(os, std::forward(container)); } template -void printElement(std::ostream &os, const std::tuple &tpl) { +inline void printElement(std::ostream &os, const std::tuple &tpl) { os << "<"; std::apply( [&os](const auto &...args) { @@ -269,7 +270,7 @@ void printElement(std::ostream &os, const std::tuple &tpl) { } template -void printElement(std::ostream &os, const SP &ptr) { +inline void printElement(std::ostream &os, const SP &ptr) { if (ptr) { printElement(os, *ptr); } else { @@ -278,7 +279,7 @@ void printElement(std::ostream &os, const SP &ptr) { } template -void printElement(std::ostream &os, const WP &ptr) { +inline void printElement(std::ostream &os, const WP &ptr) { auto sp = ptr.lock(); if (sp) { printElement(os, *sp); @@ -288,7 +289,7 @@ void printElement(std::ostream &os, const WP &ptr) { } template -void printElement(std::ostream &os, const std::optional &opt) { +inline void printElement(std::ostream &os, const std::optional &opt) { if (opt) { os << "Optional("; printElement(os, *opt); @@ -299,7 +300,7 @@ void printElement(std::ostream &os, const std::optional &opt) { } template -void printElement(std::ostream &os, const std::variant &var) { +inline void printElement(std::ostream &os, const std::variant &var) { std::visit( [&os](const auto &value) { os << "Variant("; @@ -348,7 +349,7 @@ printElement(std::ostream &os, // Fallback template -void printElement(std::ostream &os, const UnsupportedArg &value) { +inline void printElement(std::ostream &os, const UnsupportedArg &value) { const auto *typeName = typeid(UnsupportedArg).name(); throw std::runtime_error( "Type "s + std::string(typeName) + diff --git a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp index ed0d37f92..ee1cb2c97 100644 --- a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp @@ -1,5 +1,7 @@ #include "RnExecutorchInstaller.h" +#include +#include #include #include #include @@ -112,6 +114,12 @@ void RnExecutorchInstaller::injectJSIBindings( ->_unsafe_reset_threadpool(num_of_cores); log(LOG_LEVEL::Info, "Configuring xnnpack for ", num_of_cores, " threads"); #endif + + ThreadConfig config; + config.pinToPerformanceCores = true; + config.priority = Priority::HIGH; + config.namePrefix = "NativeWorker"; + GlobalThreadPool::initialize(2, config); } } // namespace rnexecutorch diff --git a/packages/react-native-executorch/common/rnexecutorch/ThreadPoolModule.h b/packages/react-native-executorch/common/rnexecutorch/ThreadPoolModule.h new file mode 100644 index 000000000..a29e03164 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/ThreadPoolModule.h @@ -0,0 +1,225 @@ +// ThreadPoolModule.cpp +#include "HighPerformanceThreadPool.h" +#include +#include + +using namespace facebook; + +class ThreadPoolModule : public jsi::HostObject { +private: + std::unique_ptr threadPool; + std::shared_ptr jsCallInvoker; + + // Store for managing async callbacks + struct PendingTask { + std::future future; + std::shared_ptr resolve; + std::shared_ptr reject; + }; + std::vector pendingTasks; + std::mutex pendingTasksMutex; + +public: + ThreadPoolModule(std::shared_ptr callInvoker, + size_t numThreads = 1, + HighPerformanceThreadPool::ThreadConfig config = {}) + : jsCallInvoker(callInvoker) { + threadPool = + std::make_unique(numThreads, config); + } + + jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override { + auto propName = name.utf8(runtime); + + if (propName == "execute") { + return jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forAscii(runtime, "execute"), 1, + [this](jsi::Runtime &runtime, const jsi::Value &thisValue, + const jsi::Value *arguments, size_t count) -> jsi::Value { + if (count < 1 || !arguments[0].isObject()) { + throw jsi::JSError(runtime, "execute requires a task function"); + } + + auto taskFunc = arguments[0].asObject(runtime).asFunction(runtime); + + // Create promise + auto promiseConstructor = + runtime.global().getPropertyAsFunction(runtime, "Promise"); + + return promiseConstructor.callAsConstructor( + runtime, + jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forAscii(runtime, "executor"), 2, + [this, taskFunc = std::make_shared(std::move( + taskFunc))](jsi::Runtime &runtime, + const jsi::Value &thisValue, + const jsi::Value *arguments, + size_t count) -> jsi::Value { + auto resolve = std::make_shared( + arguments[0].asObject(runtime).asFunction(runtime)); + auto reject = std::make_shared( + arguments[1].asObject(runtime).asFunction(runtime)); + + // Submit task to thread pool + auto future = threadPool->submit([this, taskFunc]() { + // This runs on the high-performance thread + // You can call native code here + + // For demonstration, return a value + return jsi::Value(42); + }); + + // Handle result asynchronously + std::thread([this, future = std::move(future), resolve, + reject]() mutable { + try { + auto result = future.get(); + + jsCallInvoker->invokeAsync([resolve, result]() { + // Call resolve with result + resolve->call(*resolve, std::move(result)); + }); + } catch (const std::exception &e) { + std::string error = e.what(); + jsCallInvoker->invokeAsync([reject, error]() { + reject->call(*reject, jsi::String::createFromUtf8( + *reject, error)); + }); + } + }).detach(); + + return jsi::Value::undefined(); + })); + }); + } + + if (propName == "executeNative") { + return jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forAscii(runtime, "executeNative"), 2, + [this](jsi::Runtime &runtime, const jsi::Value &thisValue, + const jsi::Value *arguments, size_t count) -> jsi::Value { + if (count < 2) { + throw jsi::JSError( + runtime, "executeNative requires functionName and args"); + } + + std::string funcName = arguments[0].asString(runtime).utf8(runtime); + + // Route to different native functions + if (funcName == "runInference") { + return executeInference(runtime, arguments[1]); + } else if (funcName == "processImage") { + return executeImageProcessing(runtime, arguments[1]); + } else if (funcName == "heavyComputation") { + return executeComputation(runtime, arguments[1]); + } + + throw jsi::JSError(runtime, "Unknown native function: " + funcName); + }); + } + + if (propName == "getStats") { + return jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forAscii(runtime, "getStats"), 0, + [this](jsi::Runtime &runtime, const jsi::Value &thisValue, + const jsi::Value *arguments, size_t count) -> jsi::Value { + auto stats = threadPool->getStats(); + + auto obj = jsi::Object(runtime); + obj.setProperty(runtime, "queueSize", (int)stats.queueSize); + obj.setProperty(runtime, "activeWorkers", (int)stats.activeWorkers); + obj.setProperty(runtime, "totalWorkers", (int)stats.totalWorkers); + obj.setProperty(runtime, "tasksCompleted", + (int)stats.tasksCompleted); + obj.setProperty(runtime, "tasksFailed", (int)stats.tasksFailed); + obj.setProperty(runtime, "avgWaitTimeMs", stats.avgWaitTimeMs); + obj.setProperty(runtime, "avgExecutionTimeMs", + stats.avgExecutionTimeMs); + + return obj; + }); + } + + return jsi::Value::undefined(); + } + +private: + jsi::Value executeInference(jsi::Runtime &runtime, const jsi::Value &args) { + // Create promise for async execution + auto promiseConstructor = + runtime.global().getPropertyAsFunction(runtime, "Promise"); + + return promiseConstructor.callAsConstructor( + runtime, + jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forAscii(runtime, "executor"), 2, + [this, argsStr = args.asString(runtime).utf8(runtime)]( + jsi::Runtime &runtime, const jsi::Value &thisValue, + const jsi::Value *arguments, size_t count) -> jsi::Value { + auto resolve = std::make_shared( + arguments[0].asObject(runtime).asFunction(runtime)); + auto reject = std::make_shared( + arguments[1].asObject(runtime).asFunction(runtime)); + + // Submit high-priority inference task + auto future = threadPool->submitWithPriority( + HighPerformanceThreadPool::Priority::HIGH, + [argsStr]() -> std::string { + // Your ExecutorTorch inference here + LOGI("Running inference with: %s", argsStr.c_str()); + + // Simulate inference + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + return "Inference result for: " + argsStr; + }); + + // Handle async result + std::thread([this, future = std::move(future), resolve, + reject]() mutable { + try { + std::string result = future.get(); + + jsCallInvoker->invokeAsync([resolve, result]() { + resolve->call(*resolve, jsi::String::createFromUtf8( + *resolve, result)); + }); + } catch (...) { + jsCallInvoker->invokeAsync([reject]() { + reject->call(*reject, jsi::String::createFromUtf8( + *reject, "Inference failed")); + }); + } + }).detach(); + + return jsi::Value::undefined(); + })); + } + + jsi::Value executeImageProcessing(jsi::Runtime &runtime, + const jsi::Value &args) { + // Similar pattern for image processing + // ... + } + + jsi::Value executeComputation(jsi::Runtime &runtime, const jsi::Value &args) { + // Similar pattern for heavy computation + // ... + } +}; + +// Installation +void installThreadPoolModule(jsi::Runtime &runtime, + std::shared_ptr callInvoker) { + // Configure thread pool for maximum performance + HighPerformanceThreadPool::ThreadConfig config; + config.pinToPerformanceCores = true; + config.priority = HighPerformanceThreadPool::Priority::HIGH; + config.namePrefix = "NativeWorker"; + + auto module = std::make_shared(callInvoker, 2, config); + + runtime.global().setProperty( + runtime, "NativeThreadPool", + jsi::Object::createFromHostObject(runtime, module)); +} \ No newline at end of file diff --git a/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h b/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h index f512dce9d..50eaf6d19 100644 --- a/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h +++ b/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h @@ -7,7 +7,11 @@ #include #include +#include #include +#include +#include +#include #include #include #include @@ -22,7 +26,70 @@ #include #include +#include +#include +#include // For pid_t +#include + namespace rnexecutorch { +inline void setThreadPriorityRealtime(int priority) { + pthread_t thread = pthread_self(); + + sched_param sch_params; + sch_params.sched_priority = priority; + + if (pthread_setschedparam(thread, SCHED_FIFO, &sch_params)) { + log(LOG_LEVEL::Error, "Failed to set thread priority: ", priority); + } else { + log(LOG_LEVEL::Error, "Thread priority set to: ", priority); + } +} +inline int getThreadPriority() { + pthread_t thread = pthread_self(); + int policy; + sched_param param; + + if (pthread_getschedparam(thread, &policy, ¶m) == 0) { + return param.sched_priority; + } else { + // handle error + return -1; + } +} +inline int getThreadAffinity() { + pthread_t thread = pthread_self(); + pid_t tid = syscall(SYS_gettid); + cpu_set_t mask; + CPU_ZERO(&mask); + sched_param param; + int err = sched_getaffinity(tid, sizeof(mask), &mask); + if (err == 0) { + log(LOG_LEVEL::Error, "Thread %d CPU affinity: ", tid); + for (int i = 0; i < CPU_SETSIZE; i++) { + if (CPU_ISSET(i, &mask)) { + log(LOG_LEVEL::Error, "CPU: ", i); + } + } + return param.sched_priority; + } else { + log(LOG_LEVEL::Error, "Thread CPU affinity ERROR: ", tid, err, errno); + + // handle error + return -1; + } +} +inline void setCurrentThreadAffinityToCPU(int cpu_id) { + pid_t tid = syscall(SYS_gettid); + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(cpu_id, &set); + + if (sched_setaffinity(tid, sizeof(set), &set) == 0) { + log(LOG_LEVEL::Error, "Thread %d bound to CPU %d\n", tid, cpu_id); + } else { + log(LOG_LEVEL::Error, "sched_setaffinity failed"); + } +} template class ModelHostObject : public JsiHostObject { public: @@ -201,8 +268,19 @@ template class ModelHostObject : public JsiHostObject { // We need to dispatch a thread if we want the function to be // asynchronous. In this thread all accesses to jsi::Runtime need to // be done via the callInvoker. - std::thread([this, promise, - argsConverted = std::move(argsConverted)]() { + // GlobalThreadPool::detach([]{ + // log(LOG_LEVEL::Error, "Calling from thread pool"); + // }); + GlobalThreadPool::detach([this, promise, + argsConverted = + std::move(argsConverted)]() { + // nice(-10); + // setThreadPriorityRealtime(-10); + // log(LOG_LEVEL::Error, "process id secondary", getpid()); + // log(LOG_LEVEL::Error, "prio2:", getThreadPriority()); + // getThreadAffinity(); + // setCurrentThreadAffinityToCPU(7); + // getThreadAffinity(); try { if constexpr (std::is_void_v class ModelHostObject : public JsiHostObject { [promise]() { promise->reject("Unknown error"); }); return; } - }).detach(); + }); + // }).detach(); } catch (...) { promise->reject("Couldn't parse JS arguments in a native function"); } diff --git a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp index 82a804852..2fa5c0f01 100644 --- a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace rnexecutorch::models::llm { using namespace facebook; @@ -30,12 +31,15 @@ void LLM::generate(std::string input, std::shared_ptr callback) { // Create a native callback that will invoke the JS callback on the JS thread auto nativeCallback = [this, callback](const std::string &token) { - callInvoker->invokeAsync([callback, token](jsi::Runtime &runtime) { - callback->call(runtime, jsi::String::createFromUtf8(runtime, token)); - }); + log(LOG_LEVEL::Error, token); + + // callInvoker->invokeAsync([callback, token](jsi::Runtime &runtime) { + // callback->call(runtime, jsi::String::createFromUtf8(runtime, + // token)); + // }); }; - auto error = runner->generate(input, nativeCallback, {}, false); + auto error = runner->generate(input, {}, {}, false); if (error != executorch::runtime::Error::Ok) { throw std::runtime_error("Failed to generate text, error code: " + std::to_string(static_cast(error))); diff --git a/packages/react-native-executorch/src/ThreadPool.ts b/packages/react-native-executorch/src/ThreadPool.ts new file mode 100644 index 000000000..af6590883 --- /dev/null +++ b/packages/react-native-executorch/src/ThreadPool.ts @@ -0,0 +1,34 @@ +// ThreadPool.js +class NativeThreadPool { + constructor() { + if (!global.NativeThreadPool) { + throw new Error('NativeThreadPool not installed'); + } + this.pool = global.NativeThreadPool; + } + + // Execute any native function + async executeNative(functionName, args) { + return await this.pool.executeNative(functionName, JSON.stringify(args)); + } + + // Specific methods for common tasks + async runInference(prompt, options = {}) { + return await this.executeNative('runInference', { prompt, ...options }); + } + + async processImage(imagePath, options = {}) { + return await this.executeNative('processImage', { imagePath, ...options }); + } + + async heavyComputation(data) { + return await this.executeNative('heavyComputation', data); + } + + // Get thread pool statistics + getStats() { + return this.pool.getStats(); + } +} + +export default new NativeThreadPool(); From aa40e5994c3472e2e0c3ceb9918db4a47329236a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kopci=C5=84ski?= Date: Wed, 20 Aug 2025 11:18:32 +0200 Subject: [PATCH 2/7] added threadpool extension, partially fixed perf problems --- .../android/.kotlin/errors/errors-1754380687676.log | 4 ++++ apps/llm/app/llm/index.tsx | 8 ++++---- .../android/src/main/cpp/CMakeLists.txt | 11 +++++++++++ .../common/rnexecutorch/models/llm/LLM.cpp | 13 ++++++------- .../src/constants/modelUrls.ts | 1 + .../executorch/extension/threadpool/cpuinfo_utils.h | 2 ++ .../executorch/extension/threadpool/threadpool.h | 2 ++ 7 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 apps/llm/android/.kotlin/errors/errors-1754380687676.log diff --git a/apps/llm/android/.kotlin/errors/errors-1754380687676.log b/apps/llm/android/.kotlin/errors/errors-1754380687676.log new file mode 100644 index 000000000..1219b509f --- /dev/null +++ b/apps/llm/android/.kotlin/errors/errors-1754380687676.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/apps/llm/app/llm/index.tsx b/apps/llm/app/llm/index.tsx index a1e8252e6..bbee331cf 100644 --- a/apps/llm/app/llm/index.tsx +++ b/apps/llm/app/llm/index.tsx @@ -68,7 +68,7 @@ function LLMScreen() { keyboardVerticalOffset={Platform.OS === 'ios' ? 120 : 40} > - {/* {llm.messageHistory.length ? ( + {llm.messageHistory.length ? ( - )} */} - + )} + {/* Hello! 👋 What can I help you with? - + */} +#include +#include #include #include @@ -31,15 +33,12 @@ void LLM::generate(std::string input, std::shared_ptr callback) { // Create a native callback that will invoke the JS callback on the JS thread auto nativeCallback = [this, callback](const std::string &token) { - log(LOG_LEVEL::Error, token); - - // callInvoker->invokeAsync([callback, token](jsi::Runtime &runtime) { - // callback->call(runtime, jsi::String::createFromUtf8(runtime, - // token)); - // }); + callInvoker->invokeAsync([callback, token](jsi::Runtime &runtime) { + callback->call(runtime, jsi::String::createFromUtf8(runtime, token)); + }); }; - auto error = runner->generate(input, {}, {}, false); + auto error = runner->generate(input, nativeCallback, {}, false); if (error != executorch::runtime::Error::Ok) { throw std::runtime_error("Failed to generate text, error code: " + std::to_string(static_cast(error))); diff --git a/packages/react-native-executorch/src/constants/modelUrls.ts b/packages/react-native-executorch/src/constants/modelUrls.ts index 34834733e..0c61c4de2 100644 --- a/packages/react-native-executorch/src/constants/modelUrls.ts +++ b/packages/react-native-executorch/src/constants/modelUrls.ts @@ -3,6 +3,7 @@ import { Platform } from 'react-native'; const URL_PREFIX = 'https://huggingface.co/software-mansion/react-native-executorch'; const VERSION_TAG = 'resolve/v0.5.0'; +const NEXT_VERSION_TAG = 'resolve/v0.5.0'; // LLMs diff --git a/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/cpuinfo_utils.h b/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/cpuinfo_utils.h index d559738b7..c00cc30a3 100644 --- a/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/cpuinfo_utils.h +++ b/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/cpuinfo_utils.h @@ -7,6 +7,7 @@ */ #pragma once +#if defined(__ANDROID__) && defined(__aarch64__) #include @@ -22,3 +23,4 @@ namespace torch::executorch::cpuinfo { // DEPRECATED // the namespace `torch::executorch` instead of `torch::executor`. using ::executorch::extension::cpuinfo::get_num_performant_cores; // DEPRECATED } // namespace torch::executorch::cpuinfo +#endif diff --git a/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/threadpool.h b/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/threadpool.h index 95678d2c6..9bbdabbd2 100644 --- a/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/threadpool.h +++ b/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/threadpool.h @@ -7,6 +7,7 @@ */ #pragma once +#if defined(__ANDROID__) && defined(__aarch64__) #include #include @@ -90,3 +91,4 @@ using ::executorch::extension::threadpool::get_pthreadpool; // DEPRECATED using ::executorch::extension::threadpool::get_threadpool; // DEPRECATED using ::executorch::extension::threadpool::ThreadPool; // DEPRECATED } // namespace torch::executorch::threadpool +#endif From c42df7034ecf21abee9ecc18fc81f60ce9d5687c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kopci=C5=84ski?= Date: Wed, 20 Aug 2025 14:58:34 +0200 Subject: [PATCH 3/7] Initial changes for iOS compliance --- apps/llm/app.json | 6 +- apps/llm/ios/llm.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../android/src/main/cpp/CMakeLists.txt | 13 -- .../common/rnexecutorch/GlobalThreadPool.h | 4 +- .../rnexecutorch/HighPerformanceThreadPool.h | 166 ++++++++--------- .../rnexecutorch/RnExecutorchInstaller.cpp | 3 - .../host_objects/ModelHostObject.h | 173 ++++++------------ .../react-native-executorch.podspec | 17 +- 9 files changed, 164 insertions(+), 237 deletions(-) create mode 100644 apps/llm/ios/llm.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/apps/llm/app.json b/apps/llm/app.json index 5eef49366..db61ee89d 100644 --- a/apps/llm/app.json +++ b/apps/llm/app.json @@ -58,7 +58,11 @@ "foregroundImage": "./assets/icons/adaptive-icon.png", "backgroundColor": "#ffffff" }, - "package": "com.anonymous.llm" + "package": "com.anonymous.llm", + "permissions": [ + "android.permission.READ_CALENDAR", + "android.permission.WRITE_CALENDAR" + ] }, "web": { "favicon": "./assets/icons/favicon.png" diff --git a/apps/llm/ios/llm.xcodeproj/project.pbxproj b/apps/llm/ios/llm.xcodeproj/project.pbxproj index 3bb7013fc..23c8e3f0e 100644 --- a/apps/llm/ios/llm.xcodeproj/project.pbxproj +++ b/apps/llm/ios/llm.xcodeproj/project.pbxproj @@ -180,6 +180,7 @@ TargetAttributes = { 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1250; + ProvisioningStyle = Automatic; }; }; }; @@ -358,7 +359,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = llm/llm.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = J5FM626PE2; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", @@ -377,7 +381,7 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.llm; + PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.llm2; PRODUCT_NAME = llm; SWIFT_OBJC_BRIDGING_HEADER = "llm/llm-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -394,7 +398,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = llm/llm.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = J5FM626PE2; INFOPLIST_FILE = llm/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -408,7 +415,7 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.llm; + PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.llm2; PRODUCT_NAME = llm; SWIFT_OBJC_BRIDGING_HEADER = "llm/llm-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/apps/llm/ios/llm.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/llm/ios/llm.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..08de0be8d --- /dev/null +++ b/apps/llm/ios/llm.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt b/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt index 46b4cf70b..0a3aa0cb6 100644 --- a/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt +++ b/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt @@ -32,19 +32,6 @@ set(RN_VERSION_LINK_LIBRARIES ReactAndroid::reactnative ) -# Dependencies: -# ------- pthreadpool ------- -add_library(pthreadpool SHARED IMPORTED) -set_target_properties(pthreadpool PROPERTIES - IMPORTED_LOCATION "${LIBS_DIR}/pthreadpool/${ANDROID_ABI}/libpthreadpool.so" - INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third-party/executorch/backends/xnnpack/third-party/pthreadpool/include") - -# ------- cpuinfo ------- -add_library(cpuinfo SHARED IMPORTED) -set_target_properties(cpuinfo PROPERTIES - IMPORTED_LOCATION "${LIBS_DIR}/cpuinfo/${ANDROID_ABI}/libcpuinfo.so" - INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third-party/executorch/backends/xnnpack/third-party/cpuinfo/include") - # ------- Executorch ------- add_library(executorch SHARED IMPORTED) diff --git a/packages/react-native-executorch/common/rnexecutorch/GlobalThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/GlobalThreadPool.h index a80e1345b..96b4fa2b7 100644 --- a/packages/react-native-executorch/common/rnexecutorch/GlobalThreadPool.h +++ b/packages/react-native-executorch/common/rnexecutorch/GlobalThreadPool.h @@ -4,6 +4,7 @@ #include "HighPerformanceThreadPool.h" #include #include +#include class GlobalThreadPool { private: @@ -22,7 +23,8 @@ class GlobalThreadPool { numThreads = std::min(numThreads, size_t(4)); // Cap at 4 for mobile } - LOGI("Initializing global thread pool with %zu threads", numThreads); + log(rnexecutorch::LOG_LEVEL::Info, + "Initializing global thread pool with ", numThreads, " threads"); instance = std::make_unique(numThreads, config); }); diff --git a/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h index 39834aa14..7ae4bc2cf 100644 --- a/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h +++ b/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h @@ -2,7 +2,6 @@ #pragma once #include -#include #include #include #include @@ -18,21 +17,25 @@ #include #include #include -#include #include -#define LOG_TAG "HPThreadPool" -#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) -#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) -#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#ifdef __APPLE__ +#include +#include +#include +#include +#endif + +#ifdef __ANDROID__ +#include +#include +#endif + enum class Priority { LOW, NORMAL, HIGH, REALTIME }; struct ThreadConfig { bool pinToPerformanceCores{true}; - Priority priority{Priority::HIGH}; - size_t stackSize{8 * 1024 * 1024}; // 8MB default - std::optional> specificCores; // Pin to specific cores - std::string namePrefix{"HPWorker"}; + std::string namePrefix{"RN_ET_Worker"}; }; class HighPerformanceThreadPool { @@ -75,11 +78,8 @@ class HighPerformanceThreadPool { std::chrono::steady_clock::time_point enqueueTime; bool operator<(const WorkItem &other) const { - // Higher priority first, then earlier enqueue time - if (priority != other.priority) { - return priority < other.priority; - } - return enqueueTime > other.enqueueTime; + return priority != other.priority ? priority < other.priority + : enqueueTime > other.enqueueTime; } }; @@ -148,38 +148,65 @@ class HighPerformanceThreadPool { for (const auto &core : cores) { if (core.maxFreq >= threshold) { performanceCores.push_back(core.id); - log(rnexecutorch::LOG_LEVEL::Error, "Performance core: %d (%.2f GHz)", - core.id, core.maxFreq / 1000000.0); + log(rnexecutorch::LOG_LEVEL::Error, "Performance core:", core.id, "(", + core.maxFreq / 1000000.0, " GHz)"); } else { efficiencyCores.push_back(core.id); - log(rnexecutorch::LOG_LEVEL::Error, "Efficiency core: %d (%.2f GHz)", - core.id, core.maxFreq / 1000000.0); + log(rnexecutorch::LOG_LEVEL::Error, "Efficiency core:", core.id, "(", + core.maxFreq / 1000000.0, " GHz)"); } } } + inline uint64_t getCurrentThreadId() { +#ifdef __ANDROID__ + return gettid(); +#elif defined(__APPLE__) + // Option 1: pthread_threadid_np (recommended) + uint64_t tid; + pthread_threadid_np(pthread_self(), &tid); + return tid; + + // Option 2: syscall (deprecated but works) + // return syscall(SYS_thread_selfid); + + // Option 3: Mach thread ID + // return mach_thread_self(); + + // Option 4: pthread_self as number (not unique across processes) + // return (uint64_t)pthread_self(); +#else + return -1; +#endif + } + + inline void setCurrentThreadName(const std::string &name) { +#ifdef __ANDROID__ + pthread_setname_np(pthread_self(), name.c_str()); +#elif defined(__APPLE__) + pthread_setname_np(name.c_str()); // Note: no thread parameter on iOS +#endif + } + void configureThread(int workerIndex) { // Set thread name std::string threadName = config.namePrefix + std::to_string(workerIndex); - pthread_setname_np(pthread_self(), threadName.c_str()); - - // Configure CPU affinity - if (config.specificCores.has_value()) { - // Pin to specific cores provided by user - setCPUAffinity(config.specificCores.value()); - } else if (config.pinToPerformanceCores && !performanceCores.empty()) { - // Pin to performance cores + setCurrentThreadName(threadName.c_str()); + + // Pin to performance cores + if (config.pinToPerformanceCores && !performanceCores.empty()) { setCPUAffinity(performanceCores); } // Set thread priority - setThreadPriority(config.priority); + setThreadPriority(); - log(rnexecutorch::LOG_LEVEL::Error, "Worker %d configured: %s", workerIndex, - threadName.c_str()); + log(rnexecutorch::LOG_LEVEL::Error, "Worker ", workerIndex, + " configured: ", threadName.c_str()); } void setCPUAffinity(const std::vector &cores) { +#ifdef __ANDROID__ if (cores.empty()) { log(rnexecutorch::LOG_LEVEL::Error, "No cores specified for affinity setting"); @@ -190,8 +217,8 @@ class HighPerformanceThreadPool { int maxCores = std::thread::hardware_concurrency(); for (int core : cores) { if (core < 0 || core >= maxCores) { - log(rnexecutorch::LOG_LEVEL::Error, "Invalid core index %d (max: %d)", - core, maxCores - 1); + log(rnexecutorch::LOG_LEVEL::Error, "Invalid core index ", core, + " (max: ", maxCores - 1, ")"); return; } } @@ -203,62 +230,24 @@ class HighPerformanceThreadPool { CPU_SET(core, &cpuset); } - // Use sched_setaffinity for Android compatibility - pid_t tid = gettid(); + pid_t tid = getCurrentThreadId(); log(rnexecutorch::LOG_LEVEL::Info, "Thread id ", tid); if (sched_setaffinity(tid, sizeof(cpuset), &cpuset) == 0) { - std::string coreList; - for (size_t i = 0; i < cores.size(); ++i) { - coreList += std::to_string(cores[i]); - if (i < cores.size() - 1) - coreList += ","; - } - log(rnexecutorch::LOG_LEVEL::Info, "Thread pinned to cores: %s", - coreList.c_str()); + log(rnexecutorch::LOG_LEVEL::Info, "Thread pinned to cores: ", cores); } else { log(rnexecutorch::LOG_LEVEL::Error, - "Failed to set CPU affinity (error: %d). Continuing without " - "affinity.", - errno); + "Failed to set CPU affinity (error: ", errno, + "). Continuing without affinity."); } +#endif } - void setThreadPriority(Priority priority) { - int nice_value = 0; - int sched_policy = SCHED_OTHER; - int sched_priority = 0; - - switch (priority) { - case Priority::LOW: - nice_value = 10; - break; - case Priority::NORMAL: - nice_value = 0; - break; - case Priority::HIGH: - nice_value = -10; - sched_policy = SCHED_FIFO; - sched_priority = sched_get_priority_min(SCHED_FIFO); - break; - case Priority::REALTIME: - nice_value = -20; - sched_policy = SCHED_FIFO; - sched_priority = sched_get_priority_max(SCHED_FIFO) - 1; - break; - } - - // Try to set real-time scheduling - if (sched_policy != SCHED_OTHER) { - struct sched_param param; - param.sched_priority = sched_priority; - if (pthread_setschedparam(pthread_self(), sched_policy, ¶m) != 0) { - log(rnexecutorch::LOG_LEVEL::Error, - "Failed to set real-time scheduling, falling back to nice value"); - } - return; - } + void setThreadPriority() { + // pthread_setschedparam doesn't work on android because permissions reasons + // and in general does not provide visible improvements on iOS // Set nice value as fallback or additional priority boost + const int nice_value = 0; if (setpriority(PRIO_PROCESS, 0, nice_value) != 0) { log(rnexecutorch::LOG_LEVEL::Error, "Failed to set nice value"); } else { @@ -300,7 +289,7 @@ class HighPerformanceThreadPool { item.task->execute(); stats.tasksCompleted++; } catch (const std::exception &e) { - LOGE("Task failed: %s", e.what()); + log(rnexecutorch::LOG_LEVEL::Error, "Task failed: ", e.what()); stats.tasksFailed++; } @@ -315,7 +304,8 @@ class HighPerformanceThreadPool { totalTasksProcessed++; } - LOGI("Worker %d shutting down", workerIndex); + log(rnexecutorch::LOG_LEVEL::Error, "Worker ", workerIndex, + " shutting down"); } public: @@ -335,8 +325,8 @@ class HighPerformanceThreadPool { workers.emplace_back(&HighPerformanceThreadPool::workerThread, this, i); } - log(rnexecutorch::LOG_LEVEL::Error, - "Thread pool initialized with %zu workers", numThreads); + log(rnexecutorch::LOG_LEVEL::Error, "Thread pool initialized with ", + numThreads, " workers ", numThreads); } ~HighPerformanceThreadPool() { shutdown(); } @@ -408,16 +398,6 @@ class HighPerformanceThreadPool { worker.join(); } } - - LOGI("Thread pool shut down. Stats: completed=%llu, failed=%llu, " - "avg_wait=%.1fms, avg_exec=%.1fms", - stats.tasksCompleted.load(), stats.tasksFailed.load(), - stats.tasksCompleted > 0 - ? (double)stats.totalWaitTimeMs / stats.tasksCompleted - : 0, - stats.tasksCompleted > 0 - ? (double)stats.totalExecutionTimeMs / stats.tasksCompleted - : 0); } // Get pool statistics diff --git a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp index ee1cb2c97..033a6503a 100644 --- a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp @@ -116,9 +116,6 @@ void RnExecutorchInstaller::injectJSIBindings( #endif ThreadConfig config; - config.pinToPerformanceCores = true; - config.priority = Priority::HIGH; - config.namePrefix = "NativeWorker"; GlobalThreadPool::initialize(2, config); } diff --git a/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h b/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h index 50eaf6d19..5733cae9d 100644 --- a/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h +++ b/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h @@ -32,64 +32,6 @@ #include namespace rnexecutorch { -inline void setThreadPriorityRealtime(int priority) { - pthread_t thread = pthread_self(); - - sched_param sch_params; - sch_params.sched_priority = priority; - - if (pthread_setschedparam(thread, SCHED_FIFO, &sch_params)) { - log(LOG_LEVEL::Error, "Failed to set thread priority: ", priority); - } else { - log(LOG_LEVEL::Error, "Thread priority set to: ", priority); - } -} -inline int getThreadPriority() { - pthread_t thread = pthread_self(); - int policy; - sched_param param; - - if (pthread_getschedparam(thread, &policy, ¶m) == 0) { - return param.sched_priority; - } else { - // handle error - return -1; - } -} -inline int getThreadAffinity() { - pthread_t thread = pthread_self(); - pid_t tid = syscall(SYS_gettid); - cpu_set_t mask; - CPU_ZERO(&mask); - sched_param param; - int err = sched_getaffinity(tid, sizeof(mask), &mask); - if (err == 0) { - log(LOG_LEVEL::Error, "Thread %d CPU affinity: ", tid); - for (int i = 0; i < CPU_SETSIZE; i++) { - if (CPU_ISSET(i, &mask)) { - log(LOG_LEVEL::Error, "CPU: ", i); - } - } - return param.sched_priority; - } else { - log(LOG_LEVEL::Error, "Thread CPU affinity ERROR: ", tid, err, errno); - - // handle error - return -1; - } -} -inline void setCurrentThreadAffinityToCPU(int cpu_id) { - pid_t tid = syscall(SYS_gettid); - cpu_set_t set; - CPU_ZERO(&set); - CPU_SET(cpu_id, &set); - - if (sched_setaffinity(tid, sizeof(set), &set) == 0) { - log(LOG_LEVEL::Error, "Thread %d bound to CPU %d\n", tid, cpu_id); - } else { - log(LOG_LEVEL::Error, "sched_setaffinity failed"); - } -} template class ModelHostObject : public JsiHostObject { public: @@ -268,69 +210,60 @@ template class ModelHostObject : public JsiHostObject { // We need to dispatch a thread if we want the function to be // asynchronous. In this thread all accesses to jsi::Runtime need to // be done via the callInvoker. - // GlobalThreadPool::detach([]{ - // log(LOG_LEVEL::Error, "Calling from thread pool"); - // }); - GlobalThreadPool::detach([this, promise, - argsConverted = - std::move(argsConverted)]() { - // nice(-10); - // setThreadPriorityRealtime(-10); - // log(LOG_LEVEL::Error, "process id secondary", getpid()); - // log(LOG_LEVEL::Error, "prio2:", getThreadPriority()); - // getThreadAffinity(); - // setCurrentThreadAffinityToCPU(7); - // getThreadAffinity(); - try { - if constexpr (std::is_void_v) { - // For void functions, just call the function and resolve with - // undefined - std::apply(std::bind_front(FnPtr, model), - std::move(argsConverted)); - callInvoker->invokeAsync([promise](jsi::Runtime &runtime) { - promise->resolve(jsi::Value::undefined()); - }); - } else { - // For non-void functions, capture the result and convert it - auto result = std::apply(std::bind_front(FnPtr, model), - std::move(argsConverted)); - // The result is copied. It should either be quickly copiable, - // or passed with a shared_ptr. - callInvoker->invokeAsync( - [promise, result](jsi::Runtime &runtime) { - promise->resolve(jsi_conversion::getJsiValue( - std::move(result), runtime)); - }); - } - } catch (const std::runtime_error &e) { - // This catch should be merged with the next two - // (std::runtime_error and jsi::JSError inherits from - // std::exception) HOWEVER react native has broken RTTI which - // breaks proper exception type checking. Remove when the - // following change is present in our version: - // https://github.com/facebook/react-native/commit/3132cc88dd46f95898a756456bebeeb6c248f20e - callInvoker->invokeAsync([e = std::move(e), promise]() { - promise->reject(e.what()); - }); - return; - } catch (const jsi::JSError &e) { - callInvoker->invokeAsync([e = std::move(e), promise]() { - promise->reject(e.what()); - }); - return; - } catch (const std::exception &e) { - callInvoker->invokeAsync([e = std::move(e), promise]() { - promise->reject(e.what()); + GlobalThreadPool::detach( + [this, promise, argsConverted = std::move(argsConverted)]() { + try { + if constexpr (std::is_void_v) { + // For void functions, just call the function and resolve + // with undefined + std::apply(std::bind_front(FnPtr, model), + std::move(argsConverted)); + callInvoker->invokeAsync( + [promise](jsi::Runtime &runtime) { + promise->resolve(jsi::Value::undefined()); + }); + } else { + // For non-void functions, capture the result and convert + // it + auto result = std::apply(std::bind_front(FnPtr, model), + std::move(argsConverted)); + // The result is copied. It should either be quickly + // copiable, or passed with a shared_ptr. + callInvoker->invokeAsync( + [promise, result](jsi::Runtime &runtime) { + promise->resolve(jsi_conversion::getJsiValue( + std::move(result), runtime)); + }); + } + } catch (const std::runtime_error &e) { + // This catch should be merged with the next two + // (std::runtime_error and jsi::JSError inherits from + // std::exception) HOWEVER react native has broken RTTI + // which breaks proper exception type checking. Remove when + // the following change is present in our version: + // https://github.com/facebook/react-native/commit/3132cc88dd46f95898a756456bebeeb6c248f20e + callInvoker->invokeAsync([e = std::move(e), promise]() { + promise->reject(e.what()); + }); + return; + } catch (const jsi::JSError &e) { + callInvoker->invokeAsync([e = std::move(e), promise]() { + promise->reject(e.what()); + }); + return; + } catch (const std::exception &e) { + callInvoker->invokeAsync([e = std::move(e), promise]() { + promise->reject(e.what()); + }); + return; + } catch (...) { + callInvoker->invokeAsync( + [promise]() { promise->reject("Unknown error"); }); + return; + } }); - return; - } catch (...) { - callInvoker->invokeAsync( - [promise]() { promise->reject("Unknown error"); }); - return; - } - }); // }).detach(); } catch (...) { promise->reject("Couldn't parse JS arguments in a native function"); diff --git a/packages/react-native-executorch/react-native-executorch.podspec b/packages/react-native-executorch/react-native-executorch.podspec index 381d3a99f..861827336 100644 --- a/packages/react-native-executorch/react-native-executorch.podspec +++ b/packages/react-native-executorch/react-native-executorch.podspec @@ -24,8 +24,11 @@ Pod::Spec.new do |s| 'MetalPerformanceShadersGraph' ] + pthreadpool_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/executorch/backends/xnnpack/third-party/pthreadpool', __dir__) + cpuinfo_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/executorch/backends/xnnpack/third-party/cpuinfo', __dir__) + s.user_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/third-party/include", + "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/third-party/include $(PODS_TARGET_SRCROOT)/../../third-party/executorch/backends/xnnpack/third-party/pthreadpool/include $(PODS_TARGET_SRCROOT)/../../third-party/executorch/backends/xnnpack/third-party/cpuinfo/include", "OTHER_LDFLAGS[sdk=iphoneos*]" => [ '$(inherited)', @@ -39,7 +42,9 @@ Pod::Spec.new do |s| "-force_load \"#{et_binaries_path}\"/libthreadpool_ios.a", "\"#{tokenizers_binaries_path}/physical-arm64-release/libtokenizers_cpp.a\"", "\"#{tokenizers_binaries_path}/physical-arm64-release/libsentencepiece.a\"", - "\"#{tokenizers_binaries_path}/physical-arm64-release/libtokenizers_c.a\"" + "\"#{tokenizers_binaries_path}/physical-arm64-release/libtokenizers_c.a\"", + "\"#{pthreadpool_binaries_path}/build-ios-arm64/libpthreadpool.a\"", + "\"#{cpuinfo_binaries_path}/build-ios-arm64/libcpuinfo.a\"" ].join(' '), "OTHER_LDFLAGS[sdk=iphonesimulator*]" => [ @@ -54,7 +59,9 @@ Pod::Spec.new do |s| "-force_load \"#{et_binaries_path}\"/libthreadpool_simulator.a", "\"#{tokenizers_binaries_path}/simulator-arm64-debug/libtokenizers_cpp.a\"", "\"#{tokenizers_binaries_path}/simulator-arm64-debug/libsentencepiece.a\"", - "\"#{tokenizers_binaries_path}/simulator-arm64-debug/libtokenizers_c.a\"" + "\"#{tokenizers_binaries_path}/simulator-arm64-debug/libtokenizers_c.a\"", + "\"#{pthreadpool_binaries_path}/build-ios-arm64/libpthreadpool.a\"", + "\"#{cpuinfo_binaries_path}/build-ios-arm64/libcpuinfo.a\"" ].join(' '), 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64', @@ -65,7 +72,9 @@ Pod::Spec.new do |s| "HEADER_SEARCH_PATHS" => '"$(PODS_TARGET_SRCROOT)/ios" '+ '"$(PODS_TARGET_SRCROOT)/third-party/include" '+ - '"$(PODS_TARGET_SRCROOT)/common" ', + '"$(PODS_TARGET_SRCROOT)/common" '+ + '"$(PODS_TARGET_SRCROOT)/third-party/executorch/backends/xnnpack/third-party/pthreadpool/include" '+ + '"$(PODS_TARGET_SRCROOT)/third-party/executorch/backends/xnnpack/third-party/cpuinfo/include" ', "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64', } From 1d89f5f0572ff632f245d05c84f599488ac54036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kopci=C5=84ski?= Date: Mon, 15 Sep 2025 12:47:47 +0200 Subject: [PATCH 4/7] Implemented GlobalThreadPool and added setting xnnpack threadpool for iOS --- .cspell-wordlist.txt | 8 +- .../.kotlin/errors/errors-1754380687676.log | 4 - apps/llm/app/llm/index.tsx | 6 - apps/llm/ios/llm.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/WorkspaceSettings.xcsettings | 8 - .../android/src/main/cpp/CMakeLists.txt | 2 + .../common/rnexecutorch/Log.h | 65 +++-- .../rnexecutorch/RnExecutorchInstaller.cpp | 29 +-- .../common/rnexecutorch/ThreadPoolModule.h | 225 ------------------ .../host_objects/ModelHostObject.h | 13 +- .../common/rnexecutorch/models/llm/LLM.cpp | 3 - .../{ => threads}/GlobalThreadPool.h | 29 +-- .../{ => threads}/HighPerformanceThreadPool.h | 209 ++++++---------- .../common/rnexecutorch/threads/ThreadUtils.h | 35 +++ .../ios/libs/cpuinfo/libcpuinfo.a | Bin 0 -> 42048 bytes .../physical-arm64-release/libpthreadpool.a | Bin 0 -> 53336 bytes .../simulator-arm64-debug/libpthreadpool.a | Bin 0 -> 183336 bytes .../react-native-executorch.podspec | 12 +- .../react-native-executorch/src/ThreadPool.ts | 34 --- .../src/constants/modelUrls.ts | 1 - .../extension/threadpool/cpuinfo_utils.h | 4 +- .../extension/threadpool/threadpool.h | 4 +- 22 files changed, 175 insertions(+), 527 deletions(-) delete mode 100644 apps/llm/android/.kotlin/errors/errors-1754380687676.log delete mode 100644 apps/llm/ios/llm.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings delete mode 100644 packages/react-native-executorch/common/rnexecutorch/ThreadPoolModule.h rename packages/react-native-executorch/common/rnexecutorch/{ => threads}/GlobalThreadPool.h (65%) rename packages/react-native-executorch/common/rnexecutorch/{ => threads}/HighPerformanceThreadPool.h (59%) create mode 100644 packages/react-native-executorch/common/rnexecutorch/threads/ThreadUtils.h create mode 100644 packages/react-native-executorch/ios/libs/cpuinfo/libcpuinfo.a create mode 100644 packages/react-native-executorch/ios/libs/pthreadpool/physical-arm64-release/libpthreadpool.a create mode 100644 packages/react-native-executorch/ios/libs/pthreadpool/simulator-arm64-debug/libpthreadpool.a delete mode 100644 packages/react-native-executorch/src/ThreadPool.ts diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt index f07211de5..9a2ffd573 100644 --- a/.cspell-wordlist.txt +++ b/.cspell-wordlist.txt @@ -73,4 +73,10 @@ timesteps Timesteps denoises denoise -denoising \ No newline at end of file +denoising +threadpool +chrono +setpriority +errno +ifdef +elif diff --git a/apps/llm/android/.kotlin/errors/errors-1754380687676.log b/apps/llm/android/.kotlin/errors/errors-1754380687676.log deleted file mode 100644 index 1219b509f..000000000 --- a/apps/llm/android/.kotlin/errors/errors-1754380687676.log +++ /dev/null @@ -1,4 +0,0 @@ -kotlin version: 2.0.21 -error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: - 1. Kotlin compile daemon is ready - diff --git a/apps/llm/app/llm/index.tsx b/apps/llm/app/llm/index.tsx index bbee331cf..e6ee183ee 100644 --- a/apps/llm/app/llm/index.tsx +++ b/apps/llm/app/llm/index.tsx @@ -85,12 +85,6 @@ function LLMScreen() { )} - {/* - Hello! 👋 - - What can I help you with? - - */} - - - - IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded - - - diff --git a/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt b/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt index 0a3aa0cb6..3a05f1533 100644 --- a/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt +++ b/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt @@ -32,6 +32,8 @@ set(RN_VERSION_LINK_LIBRARIES ReactAndroid::reactnative ) +# Dependencies: + # ------- Executorch ------- add_library(executorch SHARED IMPORTED) diff --git a/packages/react-native-executorch/common/rnexecutorch/Log.h b/packages/react-native-executorch/common/rnexecutorch/Log.h index 716b9d477..9381324ab 100644 --- a/packages/react-native-executorch/common/rnexecutorch/Log.h +++ b/packages/react-native-executorch/common/rnexecutorch/Log.h @@ -103,66 +103,66 @@ concept Fallback = template requires concepts::Streamable && (!concepts::SmartPointer) -inline void printElement(std::ostream &os, const T &value); +void printElement(std::ostream &os, const T &value); template -inline void printElement(std::ostream &os, const std::pair &p); +void printElement(std::ostream &os, const std::pair &p); template -inline void printElement(std::ostream &os, const char (&array)[N]); +void printElement(std::ostream &os, const char (&array)[N]); template -inline void printElement(std::ostream &os, T (&array)[N]); +void printElement(std::ostream &os, T (&array)[N]); template requires concepts::Iterable && (!concepts::Streamable) -inline void printElement(std::ostream &os, const T &container); +void printElement(std::ostream &os, const T &container); template void printSequencable(std::ostream &os, T &&container); template requires concepts::ReadOnlySequencable -inline void printElement(std::ostream &os, const T &container); +void printElement(std::ostream &os, const T &container); template requires concepts::MutableSequencable -inline void printElement(std::ostream &os, T &&container); +void printElement(std::ostream &os, T &&container); template -inline void printElement(std::ostream &os, const std::tuple &tpl); +void printElement(std::ostream &os, const std::tuple &tpl); template -inline void printElement(std::ostream &os, const SP &ptr); +void printElement(std::ostream &os, const SP &ptr); template -inline void printElement(std::ostream &os, const WP &ptr); +void printElement(std::ostream &os, const WP &ptr); template -inline void printElement(std::ostream &os, const std::optional &opt); +void printElement(std::ostream &os, const std::optional &opt); template -inline void printElement(std::ostream &os, const std::variant &var); +void printElement(std::ostream &os, const std::variant &var); -inline void printElement(std::ostream &os, const std::exception_ptr &exPtr); +void printElement(std::ostream &os, const std::exception_ptr &exPtr); -inline void printElement(std::ostream &os, const std::filesystem::path &path); +void printElement(std::ostream &os, const std::filesystem::path &path); -inline void printElement(std::ostream &os, - const std::filesystem::directory_iterator &dir_it); +void printElement(std::ostream &os, + const std::filesystem::directory_iterator &dir_it); template -inline void printElement(std::ostream &os, const UnsupportedArg &value); +void printElement(std::ostream &os, const UnsupportedArg &value); -inline void printElement(std::ostream &os, const std::error_code &ec); +void printElement(std::ostream &os, const std::error_code &ec); template requires concepts::Streamable && (!concepts::SmartPointer) -inline void printElement(std::ostream &os, const T &value) { +void printElement(std::ostream &os, const T &value) { os << value; } template -inline void printElement(std::ostream &os, const std::pair &p) { +void printElement(std::ostream &os, const std::pair &p) { os << "("; printElement(os, p.first); os << ", "; @@ -171,7 +171,7 @@ inline void printElement(std::ostream &os, const std::pair &p) { } template -inline void printElement(std::ostream &os, const char (&array)[N]) { +void printElement(std::ostream &os, const char (&array)[N]) { // Treats the input as a string up to length N, drop null termination if (N > 1) { os << std::string_view(array, N - 1); @@ -180,7 +180,7 @@ inline void printElement(std::ostream &os, const char (&array)[N]) { // A special function for C-style arrays deducing size via template template -inline void printElement(std::ostream &os, T (&array)[N]) { +void printElement(std::ostream &os, T (&array)[N]) { os << "["; for (std::size_t i = 0; i < N; ++i) { if (i > 0) { @@ -193,7 +193,7 @@ inline void printElement(std::ostream &os, T (&array)[N]) { template requires concepts::Iterable && (!concepts::Streamable) -inline void printElement(std::ostream &os, const T &container) { +void printElement(std::ostream &os, const T &container) { os << "["; auto it = std::begin(container); if (it != std::end(container)) { @@ -206,8 +206,7 @@ inline void printElement(std::ostream &os, const T &container) { os << "]"; } -template -inline void printSequencable(std::ostream &os, T &&container) { +template void printSequencable(std::ostream &os, T &&container) { os << "["; bool isFirst = true; @@ -234,7 +233,7 @@ inline void printSequencable(std::ostream &os, T &&container) { template requires concepts::ReadOnlySequencable -inline void printElement(std::ostream &os, const T &container) { +void printElement(std::ostream &os, const T &container) { T tempContainer = container; // Make a copy to preserve original container printSequencable( os, std::move(tempContainer)); // Use std::move since tempContainer won't @@ -243,12 +242,12 @@ inline void printElement(std::ostream &os, const T &container) { template requires concepts::MutableSequencable -inline void printElement(std::ostream &os, T &&container) { +void printElement(std::ostream &os, T &&container) { printSequencable(os, std::forward(container)); } template -inline void printElement(std::ostream &os, const std::tuple &tpl) { +void printElement(std::ostream &os, const std::tuple &tpl) { os << "<"; std::apply( [&os](const auto &...args) { @@ -270,7 +269,7 @@ inline void printElement(std::ostream &os, const std::tuple &tpl) { } template -inline void printElement(std::ostream &os, const SP &ptr) { +void printElement(std::ostream &os, const SP &ptr) { if (ptr) { printElement(os, *ptr); } else { @@ -279,7 +278,7 @@ inline void printElement(std::ostream &os, const SP &ptr) { } template -inline void printElement(std::ostream &os, const WP &ptr) { +void printElement(std::ostream &os, const WP &ptr) { auto sp = ptr.lock(); if (sp) { printElement(os, *sp); @@ -289,7 +288,7 @@ inline void printElement(std::ostream &os, const WP &ptr) { } template -inline void printElement(std::ostream &os, const std::optional &opt) { +void printElement(std::ostream &os, const std::optional &opt) { if (opt) { os << "Optional("; printElement(os, *opt); @@ -300,7 +299,7 @@ inline void printElement(std::ostream &os, const std::optional &opt) { } template -inline void printElement(std::ostream &os, const std::variant &var) { +void printElement(std::ostream &os, const std::variant &var) { std::visit( [&os](const auto &value) { os << "Variant("; @@ -349,7 +348,7 @@ printElement(std::ostream &os, // Fallback template -inline void printElement(std::ostream &os, const UnsupportedArg &value) { +void printElement(std::ostream &os, const UnsupportedArg &value) { const auto *typeName = typeid(UnsupportedArg).name(); throw std::runtime_error( "Type "s + std::string(typeName) + diff --git a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp index 033a6503a..278fc92fd 100644 --- a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp @@ -1,6 +1,5 @@ #include "RnExecutorchInstaller.h" -#include #include #include #include @@ -15,12 +14,8 @@ #include #include #include - -#if defined(__ANDROID__) && defined(__aarch64__) -#include -#include -#include -#endif +#include +#include namespace rnexecutorch { @@ -99,24 +94,8 @@ void RnExecutorchInstaller::injectJSIBindings( RnExecutorchInstaller::loadModel( jsiRuntime, jsCallInvoker, "loadSpeechToText")); -#if defined(__ANDROID__) && defined(__aarch64__) - auto num_of_perf_cores = - ::executorch::extension::cpuinfo::get_num_performant_cores(); - log(LOG_LEVEL::Info, "Detected ", num_of_perf_cores, " performant cores"); - // setting num_of_cores to floor(num_of_perf_cores / 2) + 1) because depending - // on cpu arch as when possible we want to leave at least 2 performant cores - // for other tasks (setting more actually results in drop of performance). For - // older devices (i.e. samsung s22) resolves to 3 cores, and for newer ones - // (like OnePlus 12) resolves to 4, which when benchamrked gives highest - // throughput. - auto num_of_cores = static_cast(num_of_perf_cores / 2) + 1; - ::executorch::extension::threadpool::get_threadpool() - ->_unsafe_reset_threadpool(num_of_cores); - log(LOG_LEVEL::Info, "Configuring xnnpack for ", num_of_cores, " threads"); -#endif - - ThreadConfig config; - GlobalThreadPool::initialize(2, config); + threads::ThreadUtils::unsafeSetupThreadPool(); + threads::GlobalThreadPool::initialize(); } } // namespace rnexecutorch diff --git a/packages/react-native-executorch/common/rnexecutorch/ThreadPoolModule.h b/packages/react-native-executorch/common/rnexecutorch/ThreadPoolModule.h deleted file mode 100644 index a29e03164..000000000 --- a/packages/react-native-executorch/common/rnexecutorch/ThreadPoolModule.h +++ /dev/null @@ -1,225 +0,0 @@ -// ThreadPoolModule.cpp -#include "HighPerformanceThreadPool.h" -#include -#include - -using namespace facebook; - -class ThreadPoolModule : public jsi::HostObject { -private: - std::unique_ptr threadPool; - std::shared_ptr jsCallInvoker; - - // Store for managing async callbacks - struct PendingTask { - std::future future; - std::shared_ptr resolve; - std::shared_ptr reject; - }; - std::vector pendingTasks; - std::mutex pendingTasksMutex; - -public: - ThreadPoolModule(std::shared_ptr callInvoker, - size_t numThreads = 1, - HighPerformanceThreadPool::ThreadConfig config = {}) - : jsCallInvoker(callInvoker) { - threadPool = - std::make_unique(numThreads, config); - } - - jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override { - auto propName = name.utf8(runtime); - - if (propName == "execute") { - return jsi::Function::createFromHostFunction( - runtime, jsi::PropNameID::forAscii(runtime, "execute"), 1, - [this](jsi::Runtime &runtime, const jsi::Value &thisValue, - const jsi::Value *arguments, size_t count) -> jsi::Value { - if (count < 1 || !arguments[0].isObject()) { - throw jsi::JSError(runtime, "execute requires a task function"); - } - - auto taskFunc = arguments[0].asObject(runtime).asFunction(runtime); - - // Create promise - auto promiseConstructor = - runtime.global().getPropertyAsFunction(runtime, "Promise"); - - return promiseConstructor.callAsConstructor( - runtime, - jsi::Function::createFromHostFunction( - runtime, jsi::PropNameID::forAscii(runtime, "executor"), 2, - [this, taskFunc = std::make_shared(std::move( - taskFunc))](jsi::Runtime &runtime, - const jsi::Value &thisValue, - const jsi::Value *arguments, - size_t count) -> jsi::Value { - auto resolve = std::make_shared( - arguments[0].asObject(runtime).asFunction(runtime)); - auto reject = std::make_shared( - arguments[1].asObject(runtime).asFunction(runtime)); - - // Submit task to thread pool - auto future = threadPool->submit([this, taskFunc]() { - // This runs on the high-performance thread - // You can call native code here - - // For demonstration, return a value - return jsi::Value(42); - }); - - // Handle result asynchronously - std::thread([this, future = std::move(future), resolve, - reject]() mutable { - try { - auto result = future.get(); - - jsCallInvoker->invokeAsync([resolve, result]() { - // Call resolve with result - resolve->call(*resolve, std::move(result)); - }); - } catch (const std::exception &e) { - std::string error = e.what(); - jsCallInvoker->invokeAsync([reject, error]() { - reject->call(*reject, jsi::String::createFromUtf8( - *reject, error)); - }); - } - }).detach(); - - return jsi::Value::undefined(); - })); - }); - } - - if (propName == "executeNative") { - return jsi::Function::createFromHostFunction( - runtime, jsi::PropNameID::forAscii(runtime, "executeNative"), 2, - [this](jsi::Runtime &runtime, const jsi::Value &thisValue, - const jsi::Value *arguments, size_t count) -> jsi::Value { - if (count < 2) { - throw jsi::JSError( - runtime, "executeNative requires functionName and args"); - } - - std::string funcName = arguments[0].asString(runtime).utf8(runtime); - - // Route to different native functions - if (funcName == "runInference") { - return executeInference(runtime, arguments[1]); - } else if (funcName == "processImage") { - return executeImageProcessing(runtime, arguments[1]); - } else if (funcName == "heavyComputation") { - return executeComputation(runtime, arguments[1]); - } - - throw jsi::JSError(runtime, "Unknown native function: " + funcName); - }); - } - - if (propName == "getStats") { - return jsi::Function::createFromHostFunction( - runtime, jsi::PropNameID::forAscii(runtime, "getStats"), 0, - [this](jsi::Runtime &runtime, const jsi::Value &thisValue, - const jsi::Value *arguments, size_t count) -> jsi::Value { - auto stats = threadPool->getStats(); - - auto obj = jsi::Object(runtime); - obj.setProperty(runtime, "queueSize", (int)stats.queueSize); - obj.setProperty(runtime, "activeWorkers", (int)stats.activeWorkers); - obj.setProperty(runtime, "totalWorkers", (int)stats.totalWorkers); - obj.setProperty(runtime, "tasksCompleted", - (int)stats.tasksCompleted); - obj.setProperty(runtime, "tasksFailed", (int)stats.tasksFailed); - obj.setProperty(runtime, "avgWaitTimeMs", stats.avgWaitTimeMs); - obj.setProperty(runtime, "avgExecutionTimeMs", - stats.avgExecutionTimeMs); - - return obj; - }); - } - - return jsi::Value::undefined(); - } - -private: - jsi::Value executeInference(jsi::Runtime &runtime, const jsi::Value &args) { - // Create promise for async execution - auto promiseConstructor = - runtime.global().getPropertyAsFunction(runtime, "Promise"); - - return promiseConstructor.callAsConstructor( - runtime, - jsi::Function::createFromHostFunction( - runtime, jsi::PropNameID::forAscii(runtime, "executor"), 2, - [this, argsStr = args.asString(runtime).utf8(runtime)]( - jsi::Runtime &runtime, const jsi::Value &thisValue, - const jsi::Value *arguments, size_t count) -> jsi::Value { - auto resolve = std::make_shared( - arguments[0].asObject(runtime).asFunction(runtime)); - auto reject = std::make_shared( - arguments[1].asObject(runtime).asFunction(runtime)); - - // Submit high-priority inference task - auto future = threadPool->submitWithPriority( - HighPerformanceThreadPool::Priority::HIGH, - [argsStr]() -> std::string { - // Your ExecutorTorch inference here - LOGI("Running inference with: %s", argsStr.c_str()); - - // Simulate inference - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - return "Inference result for: " + argsStr; - }); - - // Handle async result - std::thread([this, future = std::move(future), resolve, - reject]() mutable { - try { - std::string result = future.get(); - - jsCallInvoker->invokeAsync([resolve, result]() { - resolve->call(*resolve, jsi::String::createFromUtf8( - *resolve, result)); - }); - } catch (...) { - jsCallInvoker->invokeAsync([reject]() { - reject->call(*reject, jsi::String::createFromUtf8( - *reject, "Inference failed")); - }); - } - }).detach(); - - return jsi::Value::undefined(); - })); - } - - jsi::Value executeImageProcessing(jsi::Runtime &runtime, - const jsi::Value &args) { - // Similar pattern for image processing - // ... - } - - jsi::Value executeComputation(jsi::Runtime &runtime, const jsi::Value &args) { - // Similar pattern for heavy computation - // ... - } -}; - -// Installation -void installThreadPoolModule(jsi::Runtime &runtime, - std::shared_ptr callInvoker) { - // Configure thread pool for maximum performance - HighPerformanceThreadPool::ThreadConfig config; - config.pinToPerformanceCores = true; - config.priority = HighPerformanceThreadPool::Priority::HIGH; - config.namePrefix = "NativeWorker"; - - auto module = std::make_shared(callInvoker, 2, config); - - runtime.global().setProperty( - runtime, "NativeThreadPool", - jsi::Object::createFromHostObject(runtime, module)); -} \ No newline at end of file diff --git a/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h b/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h index 5733cae9d..97f769e3a 100644 --- a/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h +++ b/packages/react-native-executorch/common/rnexecutorch/host_objects/ModelHostObject.h @@ -7,11 +7,7 @@ #include #include -#include #include -#include -#include -#include #include #include #include @@ -25,11 +21,7 @@ #include #include #include - -#include -#include -#include // For pid_t -#include +#include namespace rnexecutorch { @@ -210,7 +202,7 @@ template class ModelHostObject : public JsiHostObject { // We need to dispatch a thread if we want the function to be // asynchronous. In this thread all accesses to jsi::Runtime need to // be done via the callInvoker. - GlobalThreadPool::detach( + threads::GlobalThreadPool::detach( [this, promise, argsConverted = std::move(argsConverted)]() { try { if constexpr (std::is_void_v class ModelHostObject : public JsiHostObject { return; } }); - // }).detach(); } catch (...) { promise->reject("Couldn't parse JS arguments in a native function"); } diff --git a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp index 3ad331def..82a804852 100644 --- a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp @@ -1,10 +1,7 @@ #include "LLM.h" #include -#include -#include #include -#include namespace rnexecutorch::models::llm { using namespace facebook; diff --git a/packages/react-native-executorch/common/rnexecutorch/GlobalThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h similarity index 65% rename from packages/react-native-executorch/common/rnexecutorch/GlobalThreadPool.h rename to packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h index 96b4fa2b7..ea1d139ae 100644 --- a/packages/react-native-executorch/common/rnexecutorch/GlobalThreadPool.h +++ b/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h @@ -1,10 +1,14 @@ // GlobalThreadPool.h #pragma once -#include "HighPerformanceThreadPool.h" +#include #include #include #include +#include + +namespace rnexecutorch { +namespace threads { class GlobalThreadPool { private: @@ -14,17 +18,16 @@ class GlobalThreadPool { GlobalThreadPool() = delete; public: - // Initialize the global thread pool (call once at app startup) - static void initialize(size_t numThreads = 0, ThreadConfig config = {}) { + static void initialize(uint32_t numThreads = 0, ThreadConfig config = {}) { std::call_once(initFlag, [&numThreads, config]() { // Auto-detect optimal thread count if not specified if (numThreads == 0) { - numThreads = std::thread::hardware_concurrency(); - numThreads = std::min(numThreads, size_t(4)); // Cap at 4 for mobile + numThreads = + ::executorch::extension::cpuinfo::get_num_performant_cores(); } - log(rnexecutorch::LOG_LEVEL::Info, - "Initializing global thread pool with ", numThreads, " threads"); + log(rnexecutorch::LOG_LEVEL::Info, "Initializing global thread pool with", + numThreads, "threads"); instance = std::make_unique(numThreads, config); }); @@ -33,7 +36,6 @@ class GlobalThreadPool { // Get the global thread pool instance static HighPerformanceThreadPool &get() { if (!instance) { - // Auto-initialize with defaults if not already initialized initialize(); } return *instance; @@ -71,12 +73,5 @@ class GlobalThreadPool { } }; -// Static member definitions -// std::unique_ptr GlobalThreadPool::instance; -// std::once_flag GlobalThreadPool::initFlag; - -// Convenience macros for even simpler usage -#define ASYNC_TASK(...) GlobalThreadPool::async(__VA_ARGS__) -#define ASYNC_HIGH(...) GlobalThreadPool::async_high_priority(__VA_ARGS__) -#define ASYNC_DETACH(...) GlobalThreadPool::detach(__VA_ARGS__) -#define ASYNC_WAIT(...) GlobalThreadPool::execute(__VA_ARGS__) \ No newline at end of file +} // namespace threads +} // namespace rnexecutorch diff --git a/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h similarity index 59% rename from packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h rename to packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h index 7ae4bc2cf..c4ffe2420 100644 --- a/packages/react-native-executorch/common/rnexecutorch/HighPerformanceThreadPool.h +++ b/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -17,20 +18,20 @@ #include #include #include +#include #include #ifdef __APPLE__ -#include -#include #include -#include #endif #ifdef __ANDROID__ #include -#include #endif +namespace rnexecutorch { +namespace threads { + enum class Priority { LOW, NORMAL, HIGH, REALTIME }; struct ThreadConfig { @@ -92,31 +93,27 @@ class HighPerformanceThreadPool { std::atomic activeWorkers{0}; std::atomic totalTasksProcessed{0}; +#ifdef __ANDROID__ // Performance cores std::vector performanceCores; std::vector efficiencyCores; +#endif // Configuration ThreadConfig config; - // Statistics - struct Stats { - std::atomic tasksCompleted{0}; - std::atomic tasksFailed{0}; - std::atomic totalWaitTimeMs{0}; - std::atomic totalExecutionTimeMs{0}; - } stats; - private: void detectCPUTopology() { +#ifdef __ANDROID__ struct CoreInfo { int id; long maxFreq; }; std::vector cores; + const auto numOfCores = std::thread::hardware_concurrency(); - for (int i = 0; i < 32; i++) { // Check up to 32 cores + for (int i = 0; i < numOfCores; i++) { std::string path = "/sys/devices/system/cpu/cpu" + std::to_string(i) + "/cpufreq/cpuinfo_max_freq"; std::ifstream file(path); @@ -130,7 +127,7 @@ class HighPerformanceThreadPool { } if (cores.empty()) { - log(rnexecutorch::LOG_LEVEL::Error, "Could not detect CPU topology"); + log(LOG_LEVEL::Debug, "Could not detect CPU topology"); return; } @@ -141,102 +138,72 @@ class HighPerformanceThreadPool { }); // Classify cores - long highestFreq = cores[0].maxFreq; - long lowestFreq = cores.back().maxFreq; - long threshold = lowestFreq + (highestFreq - lowestFreq) * 0.6; + const auto numOfPerfCores = + ::executorch::extension::cpuinfo::get_num_performant_cores(); - for (const auto &core : cores) { - if (core.maxFreq >= threshold) { + for (int i = 0; i < cores.size(); ++i) { + if (i < numOfPerfCores) { performanceCores.push_back(core.id); - log(rnexecutorch::LOG_LEVEL::Error, "Performance core:", core.id, "(", - core.maxFreq / 1000000.0, " GHz)"); + log(LOG_LEVEL::Debug, "Performance core:", core.id, "(", + core.maxFreq / 1000000.0, "GHz)"); } else { efficiencyCores.push_back(core.id); - log(rnexecutorch::LOG_LEVEL::Error, "Efficiency core:", core.id, "(", - core.maxFreq / 1000000.0, " GHz)"); + log(LOG_LEVEL::Debug, "Efficiency core:", core.id, "(", + core.maxFreq / 1000000.0, "GHz)"); } } +#endif } - inline uint64_t getCurrentThreadId() { #ifdef __ANDROID__ - return gettid(); -#elif defined(__APPLE__) - // Option 1: pthread_threadid_np (recommended) - uint64_t tid; - pthread_threadid_np(pthread_self(), &tid); - return tid; - - // Option 2: syscall (deprecated but works) - // return syscall(SYS_thread_selfid); - - // Option 3: Mach thread ID - // return mach_thread_self(); - - // Option 4: pthread_self as number (not unique across processes) - // return (uint64_t)pthread_self(); -#else - return -1; + inline uint64_t getCurrentThreadId() { return gettid(); } #endif - } inline void setCurrentThreadName(const std::string &name) { #ifdef __ANDROID__ pthread_setname_np(pthread_self(), name.c_str()); #elif defined(__APPLE__) - pthread_setname_np(name.c_str()); // Note: no thread parameter on iOS + pthread_setname_np(name.c_str()); #endif } void configureThread(int workerIndex) { - // Set thread name std::string threadName = config.namePrefix + std::to_string(workerIndex); setCurrentThreadName(threadName.c_str()); - // Pin to performance cores +#ifdef __ANDROID__ if (config.pinToPerformanceCores && !performanceCores.empty()) { - setCPUAffinity(performanceCores); + setCPUAffinity(); } +#endif - // Set thread priority setThreadPriority(); - log(rnexecutorch::LOG_LEVEL::Error, "Worker ", workerIndex, - " configured: ", threadName.c_str()); + log(LOG_LEVEL::Debug, "Worker", workerIndex, + "configured:", threadName.c_str()); } - void setCPUAffinity(const std::vector &cores) { + void setCPUAffinity() { + // AFAIK it is not possible on iOS #ifdef __ANDROID__ - if (cores.empty()) { - log(rnexecutorch::LOG_LEVEL::Error, - "No cores specified for affinity setting"); + if (performanceCores.empty()) { + log(LOG_LEVEL::Error, "No cores specified for affinity setting"); return; } - // Validate core indices first - int maxCores = std::thread::hardware_concurrency(); - for (int core : cores) { - if (core < 0 || core >= maxCores) { - log(rnexecutorch::LOG_LEVEL::Error, "Invalid core index ", core, - " (max: ", maxCores - 1, ")"); - return; - } - } - cpu_set_t cpuset; CPU_ZERO(&cpuset); - for (int core : cores) { + for (int core : performanceCores) { CPU_SET(core, &cpuset); } pid_t tid = getCurrentThreadId(); - log(rnexecutorch::LOG_LEVEL::Info, "Thread id ", tid); + log(LOG_LEVEL::Debug, "Thread id", tid); if (sched_setaffinity(tid, sizeof(cpuset), &cpuset) == 0) { - log(rnexecutorch::LOG_LEVEL::Info, "Thread pinned to cores: ", cores); + log(LOG_LEVEL::Debug, "Thread pinned to cores:", performanceCores); } else { - log(rnexecutorch::LOG_LEVEL::Error, - "Failed to set CPU affinity (error: ", errno, + log(LOG_LEVEL::Debug, "Failed to set CPU affinity (error:", errno, "). Continuing without affinity."); } #endif @@ -249,12 +216,36 @@ class HighPerformanceThreadPool { // Set nice value as fallback or additional priority boost const int nice_value = 0; if (setpriority(PRIO_PROCESS, 0, nice_value) != 0) { - log(rnexecutorch::LOG_LEVEL::Error, "Failed to set nice value"); + log(LOG_LEVEL::Debug, "Failed to set nice value"); } else { - log(rnexecutorch::LOG_LEVEL::Error, "Set nice value", nice_value); + log(LOG_LEVEL::Debug, "Set nice value", nice_value); } } + void processTask(const WorkItem &item) { + activeWorkers++; + + auto startTime = std::chrono::steady_clock::now(); + auto waitTime = std::chrono::duration_cast( + startTime - item.enqueueTime) + .count(); + + try { + item.task->execute(); + } catch (const std::exception &e) { + log(LOG_LEVEL::Error, "Task failed:", e.what()); + throw; + } + + auto endTime = std::chrono::steady_clock::now(); + auto executionTime = std::chrono::duration_cast( + endTime - startTime) + .count(); + + activeWorkers--; + totalTasksProcessed++; + } + void workerThread(int workerIndex) { configureThread(workerIndex); @@ -276,36 +267,10 @@ class HighPerformanceThreadPool { } } - // Process task - activeWorkers++; - - auto startTime = std::chrono::steady_clock::now(); - auto waitTime = std::chrono::duration_cast( - startTime - item.enqueueTime) - .count(); - stats.totalWaitTimeMs += waitTime; - - try { - item.task->execute(); - stats.tasksCompleted++; - } catch (const std::exception &e) { - log(rnexecutorch::LOG_LEVEL::Error, "Task failed: ", e.what()); - stats.tasksFailed++; - } - - auto endTime = std::chrono::steady_clock::now(); - auto executionTime = - std::chrono::duration_cast(endTime - - startTime) - .count(); - stats.totalExecutionTimeMs += executionTime; - - activeWorkers--; - totalTasksProcessed++; + processTask(item); } - log(rnexecutorch::LOG_LEVEL::Error, "Worker ", workerIndex, - " shutting down"); + log(LOG_LEVEL::Debug, "Worker", workerIndex, "shutting down"); } public: @@ -313,20 +278,17 @@ class HighPerformanceThreadPool { ThreadConfig cfg = ThreadConfig()) : config(std::move(cfg)) { +#ifdef __ANDROID__ detectCPUTopology(); + numThreads = std::min(numThreads, performanceCores.size()); +#endif - // Limit threads for CPU-bound tasks - numThreads = std::min( - numThreads, - size_t(performanceCores.empty() ? 4 : performanceCores.size())); - - // Create worker threads for (size_t i = 0; i < numThreads; i++) { workers.emplace_back(&HighPerformanceThreadPool::workerThread, this, i); } - log(rnexecutorch::LOG_LEVEL::Error, "Thread pool initialized with ", - numThreads, " workers ", numThreads); + log(LOG_LEVEL::Debug, "Thread pool initialized with", numThreads, + "workers."); } ~HighPerformanceThreadPool() { shutdown(); } @@ -399,36 +361,7 @@ class HighPerformanceThreadPool { } } } +}; - // Get pool statistics - struct PoolStats { - size_t queueSize; - size_t activeWorkers; - size_t totalWorkers; - uint64_t tasksCompleted; - uint64_t tasksFailed; - double avgWaitTimeMs; - double avgExecutionTimeMs; - }; - - PoolStats getStats() const { - std::lock_guard lock(const_cast(queueMutex)); - - PoolStats poolStats; - poolStats.queueSize = taskQueue.size(); - poolStats.activeWorkers = activeWorkers.load(); - poolStats.totalWorkers = workers.size(); - poolStats.tasksCompleted = stats.tasksCompleted.load(); - poolStats.tasksFailed = stats.tasksFailed.load(); - poolStats.avgWaitTimeMs = - poolStats.tasksCompleted > 0 - ? (double)stats.totalWaitTimeMs / poolStats.tasksCompleted - : 0; - poolStats.avgExecutionTimeMs = - poolStats.tasksCompleted > 0 - ? (double)stats.totalExecutionTimeMs / poolStats.tasksCompleted - : 0; - - return poolStats; - } -}; \ No newline at end of file +} // namespace threads +} // namespace rnexecutorch diff --git a/packages/react-native-executorch/common/rnexecutorch/threads/ThreadUtils.h b/packages/react-native-executorch/common/rnexecutorch/threads/ThreadUtils.h new file mode 100644 index 000000000..a46bbb405 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/threads/ThreadUtils.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace rnexecutorch { +namespace threads { + +class ThreadUtils { +public: + static void unsafeSetupThreadPool(uint32_t num_of_cores = 0) { + auto num_of_perf_cores = + ::executorch::extension::cpuinfo::get_num_performant_cores(); + log(LOG_LEVEL::Info, "Detected ", num_of_perf_cores, " performant cores"); + // setting num_of_cores to floor(num_of_perf_cores / 2) + 1) because + // depending on cpu arch as when possible we want to leave at least 2 + // performant cores for other tasks (setting more actually results in drop + // of performance). For older devices (i.e. samsung s22) resolves to 3 + // cores, and for newer ones (like OnePlus 12) resolves to 4, which when + // benchamrked gives highest throughput. For iPhones they usually have 2 + // performance cores + auto _num_of_cores = num_of_cores + ? num_of_cores + : static_cast(num_of_perf_cores / 2) + 1; + const auto threadpool = + ::executorch::extension::threadpool::get_threadpool(); + threadpool->_unsafe_reset_threadpool(_num_of_cores); + log(LOG_LEVEL::Info, "Configuring xnnpack for", + threadpool->get_thread_count(), "threads"); + } +}; + +} // namespace threads +} // namespace rnexecutorch diff --git a/packages/react-native-executorch/ios/libs/cpuinfo/libcpuinfo.a b/packages/react-native-executorch/ios/libs/cpuinfo/libcpuinfo.a new file mode 100644 index 0000000000000000000000000000000000000000..b5d3895694cb4044af408dc92b88f9ef70dbf2fd GIT binary patch literal 42048 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

hGzNV`tKY#4x{Mah>EbS$ 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 R7T^n<*Myw-0%x}8zX54(nwS6p literal 0 HcmV?d00001 diff --git a/packages/react-native-executorch/react-native-executorch.podspec b/packages/react-native-executorch/react-native-executorch.podspec index 861827336..20836d832 100644 --- a/packages/react-native-executorch/react-native-executorch.podspec +++ b/packages/react-native-executorch/react-native-executorch.podspec @@ -24,8 +24,8 @@ Pod::Spec.new do |s| 'MetalPerformanceShadersGraph' ] - pthreadpool_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/executorch/backends/xnnpack/third-party/pthreadpool', __dir__) - cpuinfo_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/executorch/backends/xnnpack/third-party/cpuinfo', __dir__) + pthreadpool_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/ios/libs/pthreadpool', __dir__) + cpuinfo_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/ios/libs/cpuinfo', __dir__) s.user_target_xcconfig = { "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/third-party/include $(PODS_TARGET_SRCROOT)/../../third-party/executorch/backends/xnnpack/third-party/pthreadpool/include $(PODS_TARGET_SRCROOT)/../../third-party/executorch/backends/xnnpack/third-party/cpuinfo/include", @@ -43,8 +43,8 @@ Pod::Spec.new do |s| "\"#{tokenizers_binaries_path}/physical-arm64-release/libtokenizers_cpp.a\"", "\"#{tokenizers_binaries_path}/physical-arm64-release/libsentencepiece.a\"", "\"#{tokenizers_binaries_path}/physical-arm64-release/libtokenizers_c.a\"", - "\"#{pthreadpool_binaries_path}/build-ios-arm64/libpthreadpool.a\"", - "\"#{cpuinfo_binaries_path}/build-ios-arm64/libcpuinfo.a\"" + "\"#{pthreadpool_binaries_path}/physical-arm64-release/libpthreadpool.a\"", + "\"#{cpuinfo_binaries_path}/libcpuinfo.a\"" ].join(' '), "OTHER_LDFLAGS[sdk=iphonesimulator*]" => [ @@ -60,8 +60,8 @@ Pod::Spec.new do |s| "\"#{tokenizers_binaries_path}/simulator-arm64-debug/libtokenizers_cpp.a\"", "\"#{tokenizers_binaries_path}/simulator-arm64-debug/libsentencepiece.a\"", "\"#{tokenizers_binaries_path}/simulator-arm64-debug/libtokenizers_c.a\"", - "\"#{pthreadpool_binaries_path}/build-ios-arm64/libpthreadpool.a\"", - "\"#{cpuinfo_binaries_path}/build-ios-arm64/libcpuinfo.a\"" + "\"#{pthreadpool_binaries_path}/simulator-arm64-debug/libpthreadpool.a\"", + "\"#{cpuinfo_binaries_path}/libcpuinfo.a\"" ].join(' '), 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64', diff --git a/packages/react-native-executorch/src/ThreadPool.ts b/packages/react-native-executorch/src/ThreadPool.ts deleted file mode 100644 index af6590883..000000000 --- a/packages/react-native-executorch/src/ThreadPool.ts +++ /dev/null @@ -1,34 +0,0 @@ -// ThreadPool.js -class NativeThreadPool { - constructor() { - if (!global.NativeThreadPool) { - throw new Error('NativeThreadPool not installed'); - } - this.pool = global.NativeThreadPool; - } - - // Execute any native function - async executeNative(functionName, args) { - return await this.pool.executeNative(functionName, JSON.stringify(args)); - } - - // Specific methods for common tasks - async runInference(prompt, options = {}) { - return await this.executeNative('runInference', { prompt, ...options }); - } - - async processImage(imagePath, options = {}) { - return await this.executeNative('processImage', { imagePath, ...options }); - } - - async heavyComputation(data) { - return await this.executeNative('heavyComputation', data); - } - - // Get thread pool statistics - getStats() { - return this.pool.getStats(); - } -} - -export default new NativeThreadPool(); diff --git a/packages/react-native-executorch/src/constants/modelUrls.ts b/packages/react-native-executorch/src/constants/modelUrls.ts index 0c61c4de2..34834733e 100644 --- a/packages/react-native-executorch/src/constants/modelUrls.ts +++ b/packages/react-native-executorch/src/constants/modelUrls.ts @@ -3,7 +3,6 @@ import { Platform } from 'react-native'; const URL_PREFIX = 'https://huggingface.co/software-mansion/react-native-executorch'; const VERSION_TAG = 'resolve/v0.5.0'; -const NEXT_VERSION_TAG = 'resolve/v0.5.0'; // LLMs diff --git a/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/cpuinfo_utils.h b/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/cpuinfo_utils.h index c00cc30a3..8f54889f7 100644 --- a/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/cpuinfo_utils.h +++ b/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/cpuinfo_utils.h @@ -7,9 +7,8 @@ */ #pragma once -#if defined(__ANDROID__) && defined(__aarch64__) -#include +#include namespace executorch::extension::cpuinfo { @@ -23,4 +22,3 @@ namespace torch::executorch::cpuinfo { // DEPRECATED // the namespace `torch::executorch` instead of `torch::executor`. using ::executorch::extension::cpuinfo::get_num_performant_cores; // DEPRECATED } // namespace torch::executorch::cpuinfo -#endif diff --git a/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/threadpool.h b/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/threadpool.h index 9bbdabbd2..edd3be02a 100644 --- a/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/threadpool.h +++ b/packages/react-native-executorch/third-party/include/executorch/extension/threadpool/threadpool.h @@ -7,13 +7,12 @@ */ #pragma once -#if defined(__ANDROID__) && defined(__aarch64__) #include #include #include -#include +#include namespace executorch::extension::threadpool { @@ -91,4 +90,3 @@ using ::executorch::extension::threadpool::get_pthreadpool; // DEPRECATED using ::executorch::extension::threadpool::get_threadpool; // DEPRECATED using ::executorch::extension::threadpool::ThreadPool; // DEPRECATED } // namespace torch::executorch::threadpool -#endif From c39e2a8c7e9cf30c633a7b7c4747a6e830239fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kopci=C5=84ski?= Date: Wed, 17 Sep 2025 10:39:35 +0200 Subject: [PATCH 5/7] adressed review comments --- .../rnexecutorch/RnExecutorchInstaller.cpp | 5 +- .../rnexecutorch/threads/GlobalThreadPool.h | 46 +-- .../threads/HighPerformanceThreadPool.h | 263 +++++++++--------- .../common/rnexecutorch/threads/ThreadUtils.h | 35 --- .../rnexecutorch/threads/utils/ThreadUtils.h | 29 ++ .../react-native-executorch.podspec | 2 +- 6 files changed, 186 insertions(+), 194 deletions(-) delete mode 100644 packages/react-native-executorch/common/rnexecutorch/threads/ThreadUtils.h create mode 100644 packages/react-native-executorch/common/rnexecutorch/threads/utils/ThreadUtils.h diff --git a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp index 278fc92fd..6d5e48902 100644 --- a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp @@ -1,6 +1,5 @@ #include "RnExecutorchInstaller.h" -#include #include #include #include @@ -15,7 +14,7 @@ #include #include #include -#include +#include namespace rnexecutorch { @@ -94,7 +93,7 @@ void RnExecutorchInstaller::injectJSIBindings( RnExecutorchInstaller::loadModel( jsiRuntime, jsCallInvoker, "loadSpeechToText")); - threads::ThreadUtils::unsafeSetupThreadPool(); + threads::utils::unsafeSetupThreadPool(); threads::GlobalThreadPool::initialize(); } diff --git a/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h index ea1d139ae..ec5ddeaa0 100644 --- a/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h +++ b/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h @@ -4,43 +4,42 @@ #include #include #include +#include #include #include -namespace rnexecutorch { -namespace threads { +namespace rnexecutorch::threads { class GlobalThreadPool { -private: - inline static std::unique_ptr instance; - inline static std::once_flag initFlag; - +public: GlobalThreadPool() = delete; + GlobalThreadPool(const GlobalThreadPool &) = delete; + GlobalThreadPool &operator=(const GlobalThreadPool &) = delete; + GlobalThreadPool(GlobalThreadPool &&) = delete; + GlobalThreadPool &operator=(GlobalThreadPool &&) = delete; -public: - static void initialize(uint32_t numThreads = 0, ThreadConfig config = {}) { + static HighPerformanceThreadPool &get() { + if (!instance) { + initialize(); + } + return *instance; + } + + static void initialize(std::optional numThreads = std::nullopt, + ThreadConfig config = {}) { std::call_once(initFlag, [&numThreads, config]() { - // Auto-detect optimal thread count if not specified - if (numThreads == 0) { + if (!numThreads) { numThreads = ::executorch::extension::cpuinfo::get_num_performant_cores(); } log(rnexecutorch::LOG_LEVEL::Info, "Initializing global thread pool with", numThreads, "threads"); - instance = - std::make_unique(numThreads, config); + instance = std::make_unique(numThreads.value(), + config); }); } - // Get the global thread pool instance - static HighPerformanceThreadPool &get() { - if (!instance) { - initialize(); - } - return *instance; - } - // Convenience methods that mirror std::thread interface template static auto async(Func &&func, Args &&...args) { @@ -71,7 +70,10 @@ class GlobalThreadPool { instance.reset(); } } + +private: + inline static std::unique_ptr instance; + inline static std::once_flag initFlag; }; -} // namespace threads -} // namespace rnexecutorch +} // namespace rnexecutorch::threads diff --git a/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h index c4ffe2420..b5810547b 100644 --- a/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h +++ b/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h @@ -5,22 +5,23 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include -#include +#include #include #include #include #include #include +#include +#include + #ifdef __APPLE__ #include #endif @@ -29,8 +30,7 @@ #include #endif -namespace rnexecutorch { -namespace threads { +namespace rnexecutorch::threads { enum class Priority { LOW, NORMAL, HIGH, REALTIME }; @@ -41,6 +41,93 @@ struct ThreadConfig { class HighPerformanceThreadPool { public: + explicit HighPerformanceThreadPool(size_t numThreads = 1, + ThreadConfig cfg = ThreadConfig()) + : config(std::move(cfg)) { + +#ifdef __ANDROID__ + detectCPUTopology(); + numThreads = std::min(numThreads, performanceCores.size()); +#endif + + for (size_t i = 0; i < numThreads; i++) { + workers.emplace_back(&HighPerformanceThreadPool::workerThread, this, i); + } + + log(LOG_LEVEL::Debug, "Thread pool initialized with", numThreads, + "workers."); + } + + ~HighPerformanceThreadPool() { shutdown(); } + + // Submit a task and get a future for the result + template + auto submit(Func &&func, Args &&...args) + -> std::future { + return submitWithPriority(Priority::NORMAL, std::forward(func), + std::forward(args)...); + } + + // Submit a task with specific priority + template + auto submitWithPriority(Priority priority, Func &&func, Args &&...args) + -> std::future { + + using ReturnType = decltype(func(args...)); + + // Create a packaged task + auto boundFunc = + std::bind(std::forward(func), std::forward(args)...); + auto task = std::make_unique>( + std::move(boundFunc)); + auto future = task->getFuture(); + + // Add to queue + { + std::scoped_lock lock(queueMutex); + + if (!running) { + throw std::runtime_error("Thread pool is shutting down"); + } + + WorkItem item(std::move(task), priority, + std::chrono::steady_clock::now()); + + taskQueue.push(std::move(item)); + } + + condition.notify_one(); + return future; + } + + // Execute a task and wait for result + template + auto execute(Func &&func, Args &&...args) -> decltype(func(args...)) { + auto future = submit(std::forward(func), std::forward(args)...); + return future.get(); + } + + // Fire and forget task + template + void submitDetached(Func &&func, Args &&...args) { + submit(std::forward(func), std::forward(args)...); + // Future is destroyed, task still runs + } + + void shutdown() { + if (!running.exchange(false)) { + return; + } + + condition.notify_all(); + + for (auto &worker : workers) { + if (worker.joinable()) { + worker.join(); + } + } + } + private: // Task wrapper that can hold any callable class ITask { @@ -50,10 +137,6 @@ class HighPerformanceThreadPool { }; template class Task : public ITask { - private: - Func func; - std::promise promise; - public: Task(Func &&f) : func(std::forward(f)) {} @@ -71,17 +154,29 @@ class HighPerformanceThreadPool { } std::future getFuture() { return promise.get_future(); } + + private: + Func func; + std::promise promise; }; - struct WorkItem { + class WorkItem { + public: + WorkItem() {} + WorkItem(std::unique_ptr task, Priority priority, + std::chrono::steady_clock::time_point enqueueTime) + : task(std::move(task)), priority(priority), enqueueTime(enqueueTime) {} + std::unique_ptr task; - Priority priority; - std::chrono::steady_clock::time_point enqueueTime; bool operator<(const WorkItem &other) const { return priority != other.priority ? priority < other.priority : enqueueTime > other.enqueueTime; } + + private: + Priority priority; + std::chrono::steady_clock::time_point enqueueTime; }; // Thread pool state @@ -95,30 +190,30 @@ class HighPerformanceThreadPool { #ifdef __ANDROID__ // Performance cores - std::vector performanceCores; - std::vector efficiencyCores; + std::vector performanceCores; + std::vector efficiencyCores; #endif // Configuration ThreadConfig config; -private: void detectCPUTopology() { #ifdef __ANDROID__ struct CoreInfo { - int id; - long maxFreq; + int32_t id; + int64_t maxFreq; }; std::vector cores; const auto numOfCores = std::thread::hardware_concurrency(); - for (int i = 0; i < numOfCores; i++) { + for (int i = 0; std::cmp_less(i, numOfCores); ++i) { std::string path = "/sys/devices/system/cpu/cpu" + std::to_string(i) + "/cpufreq/cpuinfo_max_freq"; std::ifstream file(path); - if (!file.good()) + if (!file.good()) { break; + } CoreInfo info; info.id = i; @@ -132,24 +227,24 @@ class HighPerformanceThreadPool { } // Sort by frequency - std::sort(cores.begin(), cores.end(), - [](const CoreInfo &a, const CoreInfo &b) { - return a.maxFreq > b.maxFreq; - }); + std::ranges::sort(cores, [](const CoreInfo &a, const CoreInfo &b) { + return a.maxFreq > b.maxFreq; + }); // Classify cores const auto numOfPerfCores = ::executorch::extension::cpuinfo::get_num_performant_cores(); + constexpr float kKiloToGigaRatio = 1e6; for (int i = 0; i < cores.size(); ++i) { if (i < numOfPerfCores) { - performanceCores.push_back(core.id); - log(LOG_LEVEL::Debug, "Performance core:", core.id, "(", - core.maxFreq / 1000000.0, "GHz)"); + performanceCores.push_back(cores[i].id); + log(LOG_LEVEL::Debug, "Performance core:", cores[i].id, "(", + cores[i].maxFreq / kKiloToGigaRatio, "GHz)"); } else { - efficiencyCores.push_back(core.id); - log(LOG_LEVEL::Debug, "Efficiency core:", core.id, "(", - core.maxFreq / 1000000.0, "GHz)"); + efficiencyCores.push_back(cores[i].id); + log(LOG_LEVEL::Debug, "Efficiency core:", cores[i].id, "(", + cores[i].maxFreq / kKiloToGigaRatio, "GHz)"); } } #endif @@ -167,7 +262,7 @@ class HighPerformanceThreadPool { #endif } - void configureThread(int workerIndex) { + void configureThread(uint32_t workerIndex) { std::string threadName = config.namePrefix + std::to_string(workerIndex); setCurrentThreadName(threadName.c_str()); @@ -214,7 +309,7 @@ class HighPerformanceThreadPool { // and in general does not provide visible improvements on iOS // Set nice value as fallback or additional priority boost - const int nice_value = 0; + constexpr int nice_value = 0; if (setpriority(PRIO_PROCESS, 0, nice_value) != 0) { log(LOG_LEVEL::Debug, "Failed to set nice value"); } else { @@ -225,23 +320,14 @@ class HighPerformanceThreadPool { void processTask(const WorkItem &item) { activeWorkers++; - auto startTime = std::chrono::steady_clock::now(); - auto waitTime = std::chrono::duration_cast( - startTime - item.enqueueTime) - .count(); - try { item.task->execute(); } catch (const std::exception &e) { log(LOG_LEVEL::Error, "Task failed:", e.what()); + activeWorkers--; throw; } - auto endTime = std::chrono::steady_clock::now(); - auto executionTime = std::chrono::duration_cast( - endTime - startTime) - .count(); - activeWorkers--; totalTasksProcessed++; } @@ -256,8 +342,9 @@ class HighPerformanceThreadPool { std::unique_lock lock(queueMutex); condition.wait(lock, [this] { return !taskQueue.empty() || !running; }); - if (!running && taskQueue.empty()) + if (!running && taskQueue.empty()) { break; + } if (!taskQueue.empty()) { item = std::move(const_cast(taskQueue.top())); @@ -272,96 +359,6 @@ class HighPerformanceThreadPool { log(LOG_LEVEL::Debug, "Worker", workerIndex, "shutting down"); } - -public: - explicit HighPerformanceThreadPool(size_t numThreads = 1, - ThreadConfig cfg = ThreadConfig()) - : config(std::move(cfg)) { - -#ifdef __ANDROID__ - detectCPUTopology(); - numThreads = std::min(numThreads, performanceCores.size()); -#endif - - for (size_t i = 0; i < numThreads; i++) { - workers.emplace_back(&HighPerformanceThreadPool::workerThread, this, i); - } - - log(LOG_LEVEL::Debug, "Thread pool initialized with", numThreads, - "workers."); - } - - ~HighPerformanceThreadPool() { shutdown(); } - - // Submit a task and get a future for the result - template - auto submit(Func &&func, Args &&...args) - -> std::future { - return submitWithPriority(Priority::NORMAL, std::forward(func), - std::forward(args)...); - } - - // Submit a task with specific priority - template - auto submitWithPriority(Priority priority, Func &&func, Args &&...args) - -> std::future { - - using ReturnType = decltype(func(args...)); - - // Create a packaged task - auto boundFunc = - std::bind(std::forward(func), std::forward(args)...); - auto task = std::make_unique>( - std::move(boundFunc)); - auto future = task->getFuture(); - - // Add to queue - { - std::lock_guard lock(queueMutex); - - if (!running) { - throw std::runtime_error("Thread pool is shutting down"); - } - - WorkItem item; - item.task = std::move(task); - item.priority = priority; - item.enqueueTime = std::chrono::steady_clock::now(); - - taskQueue.push(std::move(item)); - } - - condition.notify_one(); - return future; - } - - // Execute a task and wait for result - template - auto execute(Func &&func, Args &&...args) -> decltype(func(args...)) { - auto future = submit(std::forward(func), std::forward(args)...); - return future.get(); - } - - // Fire and forget task - template - void submitDetached(Func &&func, Args &&...args) { - submit(std::forward(func), std::forward(args)...); - // Future is destroyed, task still runs - } - - void shutdown() { - if (!running.exchange(false)) - return; - - condition.notify_all(); - - for (auto &worker : workers) { - if (worker.joinable()) { - worker.join(); - } - } - } }; -} // namespace threads -} // namespace rnexecutorch +} // namespace rnexecutorch::threads diff --git a/packages/react-native-executorch/common/rnexecutorch/threads/ThreadUtils.h b/packages/react-native-executorch/common/rnexecutorch/threads/ThreadUtils.h deleted file mode 100644 index a46bbb405..000000000 --- a/packages/react-native-executorch/common/rnexecutorch/threads/ThreadUtils.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace rnexecutorch { -namespace threads { - -class ThreadUtils { -public: - static void unsafeSetupThreadPool(uint32_t num_of_cores = 0) { - auto num_of_perf_cores = - ::executorch::extension::cpuinfo::get_num_performant_cores(); - log(LOG_LEVEL::Info, "Detected ", num_of_perf_cores, " performant cores"); - // setting num_of_cores to floor(num_of_perf_cores / 2) + 1) because - // depending on cpu arch as when possible we want to leave at least 2 - // performant cores for other tasks (setting more actually results in drop - // of performance). For older devices (i.e. samsung s22) resolves to 3 - // cores, and for newer ones (like OnePlus 12) resolves to 4, which when - // benchamrked gives highest throughput. For iPhones they usually have 2 - // performance cores - auto _num_of_cores = num_of_cores - ? num_of_cores - : static_cast(num_of_perf_cores / 2) + 1; - const auto threadpool = - ::executorch::extension::threadpool::get_threadpool(); - threadpool->_unsafe_reset_threadpool(_num_of_cores); - log(LOG_LEVEL::Info, "Configuring xnnpack for", - threadpool->get_thread_count(), "threads"); - } -}; - -} // namespace threads -} // namespace rnexecutorch diff --git a/packages/react-native-executorch/common/rnexecutorch/threads/utils/ThreadUtils.h b/packages/react-native-executorch/common/rnexecutorch/threads/utils/ThreadUtils.h new file mode 100644 index 000000000..664480d38 --- /dev/null +++ b/packages/react-native-executorch/common/rnexecutorch/threads/utils/ThreadUtils.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace rnexecutorch::threads::utils { + +void unsafeSetupThreadPool(uint32_t num_of_cores = 0) { + auto num_of_perf_cores = + ::executorch::extension::cpuinfo::get_num_performant_cores(); + log(LOG_LEVEL::Info, "Detected ", num_of_perf_cores, " performant cores"); + // setting num_of_cores to floor(num_of_perf_cores / 2) + 1) because + // depending on cpu arch as when possible we want to leave at least 2 + // performant cores for other tasks (setting more actually results in drop + // of performance). For older devices (i.e. samsung s22) resolves to 3 + // cores, and for newer ones (like OnePlus 12) resolves to 4, which when + // benchmarked gives highest throughput. For iPhones they usually have 2 + // performance cores + auto _num_of_cores = num_of_cores + ? num_of_cores + : static_cast(num_of_perf_cores / 2) + 1; + const auto threadpool = ::executorch::extension::threadpool::get_threadpool(); + threadpool->_unsafe_reset_threadpool(_num_of_cores); + log(LOG_LEVEL::Info, "Configuring xnnpack for", + threadpool->get_thread_count(), "threads"); +} + +} // namespace rnexecutorch::threads::utils diff --git a/packages/react-native-executorch/react-native-executorch.podspec b/packages/react-native-executorch/react-native-executorch.podspec index 20836d832..3d8acadc4 100644 --- a/packages/react-native-executorch/react-native-executorch.podspec +++ b/packages/react-native-executorch/react-native-executorch.podspec @@ -28,7 +28,7 @@ Pod::Spec.new do |s| cpuinfo_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/ios/libs/cpuinfo', __dir__) s.user_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/third-party/include $(PODS_TARGET_SRCROOT)/../../third-party/executorch/backends/xnnpack/third-party/pthreadpool/include $(PODS_TARGET_SRCROOT)/../../third-party/executorch/backends/xnnpack/third-party/cpuinfo/include", + "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/third-party/include", "OTHER_LDFLAGS[sdk=iphoneos*]" => [ '$(inherited)', From b01235df2371c61fa976be70ff484998e1700549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kopci=C5=84ski?= Date: Wed, 17 Sep 2025 10:58:11 +0200 Subject: [PATCH 6/7] small review changes --- .../rnexecutorch/threads/HighPerformanceThreadPool.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h index b5810547b..fd856f990 100644 --- a/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h +++ b/packages/react-native-executorch/common/rnexecutorch/threads/HighPerformanceThreadPool.h @@ -162,7 +162,7 @@ class HighPerformanceThreadPool { class WorkItem { public: - WorkItem() {} + WorkItem() = default; WorkItem(std::unique_ptr task, Priority priority, std::chrono::steady_clock::time_point enqueueTime) : task(std::move(task)), priority(priority), enqueueTime(enqueueTime) {} @@ -207,7 +207,7 @@ class HighPerformanceThreadPool { std::vector cores; const auto numOfCores = std::thread::hardware_concurrency(); - for (int i = 0; std::cmp_less(i, numOfCores); ++i) { + for (int32_t i = 0; std::cmp_less(i, numOfCores); ++i) { std::string path = "/sys/devices/system/cpu/cpu" + std::to_string(i) + "/cpufreq/cpuinfo_max_freq"; std::ifstream file(path); @@ -236,7 +236,7 @@ class HighPerformanceThreadPool { ::executorch::extension::cpuinfo::get_num_performant_cores(); constexpr float kKiloToGigaRatio = 1e6; - for (int i = 0; i < cores.size(); ++i) { + for (int32_t i = 0; i < cores.size(); ++i) { if (i < numOfPerfCores) { performanceCores.push_back(cores[i].id); log(LOG_LEVEL::Debug, "Performance core:", cores[i].id, "(", @@ -289,7 +289,7 @@ class HighPerformanceThreadPool { cpu_set_t cpuset; CPU_ZERO(&cpuset); - for (int core : performanceCores) { + for (int32_t core : performanceCores) { CPU_SET(core, &cpuset); } From 5d4e5887731dcd5c0111b7cf72fcdc87f8e25e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kopci=C5=84ski?= Date: Mon, 22 Sep 2025 09:24:47 +0200 Subject: [PATCH 7/7] removed unnecessary include directory --- .../react-native-executorch/react-native-executorch.podspec | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-native-executorch/react-native-executorch.podspec b/packages/react-native-executorch/react-native-executorch.podspec index 3d8acadc4..2c63c0923 100644 --- a/packages/react-native-executorch/react-native-executorch.podspec +++ b/packages/react-native-executorch/react-native-executorch.podspec @@ -72,9 +72,7 @@ Pod::Spec.new do |s| "HEADER_SEARCH_PATHS" => '"$(PODS_TARGET_SRCROOT)/ios" '+ '"$(PODS_TARGET_SRCROOT)/third-party/include" '+ - '"$(PODS_TARGET_SRCROOT)/common" '+ - '"$(PODS_TARGET_SRCROOT)/third-party/executorch/backends/xnnpack/third-party/pthreadpool/include" '+ - '"$(PODS_TARGET_SRCROOT)/third-party/executorch/backends/xnnpack/third-party/cpuinfo/include" ', + '"$(PODS_TARGET_SRCROOT)/common" ', "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64', }