3456 lines
142 KiB
C++
3456 lines
142 KiB
C++
#include <yams/common/test_utils.h>
|
|
#include <yams/daemon/components/TuneAdvisor.h>
|
|
#include <yams/daemon/resource/onnx_model_pool.h>
|
|
#include <yams/vector/tokenizer.h>
|
|
|
|
#include "onnx_gpu_provider.h"
|
|
#include <yams/daemon/resource/gpu_info.h>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <spdlog/spdlog.h>
|
|
#include <filesystem>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <unordered_set>
|
|
#include <boost/asio/post.hpp>
|
|
#include <boost/asio/thread_pool.hpp>
|
|
|
|
#include "ort_cxx_api_wrapper.h"
|
|
#include "ort_runtime_loader.h"
|
|
#ifdef _WIN32
|
|
#include <fcntl.h>
|
|
#include <io.h>
|
|
#include <windows.h>
|
|
#include <sys/stat.h>
|
|
#else
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <sys/file.h>
|
|
#endif
|
|
|
|
// GPU execution provider headers - conditionally included based on Conan build options
|
|
// Note: Prebuilt ONNX Runtime binaries from GitHub releases use the C API for providers.
|
|
// The provider-specific headers (cuda_provider_factory.h, etc.) are only available when
|
|
// building ONNX Runtime from source. For prebuilt binaries, we use
|
|
// OrtSessionOptionsAppendExecutionProvider_* functions from the C API which are declared in
|
|
// onnxruntime_c_api.h (included via onnxruntime_cxx_api.h).
|
|
|
|
// Global ONNX Runtime environment - single instance per process, intentionally leaked.
|
|
// Using leaking singleton pattern because ONNX Runtime's LoggingManager destructor
|
|
// tries to lock a mutex that may be destroyed during static destruction, causing
|
|
// "mutex lock failed: Invalid argument" crashes.
|
|
static std::recursive_mutex* g_onnx_init_mutex = new std::recursive_mutex();
|
|
static Ort::Env* g_onnx_env = nullptr; // Intentionally leaked to avoid static destruction crash
|
|
static bool g_onnx_env_initialized = false;
|
|
|
|
static Ort::Env& get_global_ort_env() {
|
|
std::lock_guard<std::recursive_mutex> lock(*g_onnx_init_mutex);
|
|
if (!g_onnx_env_initialized) {
|
|
spdlog::info("[ONNX] Initializing global Ort::Env (thread-safe, single instance)");
|
|
|
|
OrtThreadingOptions* threading_options = nullptr;
|
|
(void)Ort::GetApi().CreateThreadingOptions(&threading_options);
|
|
if (threading_options) {
|
|
unsigned hw_threads = std::thread::hardware_concurrency();
|
|
if (hw_threads == 0)
|
|
hw_threads = 4;
|
|
|
|
#ifdef _WIN32
|
|
int intra_threads = 1;
|
|
int inter_threads = 1;
|
|
spdlog::info(
|
|
"[ONNX] Windows: using single-threaded globalOrt::Env (EDEADLK workaround)");
|
|
#else
|
|
int intra_threads = static_cast<int>(std::max(1u, hw_threads / 2));
|
|
int inter_threads = 1;
|
|
spdlog::info("[ONNX] Using multi-threaded global Ort::Env (intra={}, inter={})",
|
|
intra_threads, inter_threads);
|
|
#endif
|
|
(void)Ort::GetApi().SetGlobalIntraOpNumThreads(threading_options, intra_threads);
|
|
(void)Ort::GetApi().SetGlobalInterOpNumThreads(threading_options, inter_threads);
|
|
(void)Ort::GetApi().SetGlobalSpinControl(threading_options, 0);
|
|
|
|
g_onnx_env = new Ort::Env(threading_options, ORT_LOGGING_LEVEL_WARNING, "YamsDaemon");
|
|
Ort::GetApi().ReleaseThreadingOptions(threading_options);
|
|
spdlog::info("[ONNX] Global Ort::Env initialized with custom threading options");
|
|
} else {
|
|
g_onnx_env = new Ort::Env(ORT_LOGGING_LEVEL_WARNING, "YamsDaemon");
|
|
spdlog::info("[ONNX] Global Ort::Env initialized (default threading)");
|
|
}
|
|
g_onnx_env_initialized = true;
|
|
}
|
|
return *g_onnx_env;
|
|
}
|
|
|
|
// Check if we should skip model loading for tests
|
|
#ifdef GTEST_API_
|
|
#include <yams/common/test_utils.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <future>
|
|
#include <limits>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
|
|
namespace yams::daemon {
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
bool envTruthy(const char* value) {
|
|
if (!value || !*value) {
|
|
return false;
|
|
}
|
|
std::string v(value);
|
|
std::transform(v.begin(), v.end(), v.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return !(v == "0" || v == "false" || v == "off" || v == "no");
|
|
}
|
|
|
|
bool allowGpuAutoUnload() {
|
|
return envTruthy(std::getenv("YAMS_ONNX_GPU_AUTO_UNLOAD"));
|
|
}
|
|
|
|
int envIntOr(const char* name, int fallback) {
|
|
if (const char* value = std::getenv(name)) {
|
|
try {
|
|
return std::stoi(value);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
std::string lowerCopy(std::string value) {
|
|
std::transform(value.begin(), value.end(), value.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return value;
|
|
}
|
|
|
|
bool supportsCoreMLCpuFallbackModel(const std::string& modelName) {
|
|
const std::string lowerName = lowerCopy(modelName);
|
|
return lowerName.find("embeddinggemma") != std::string::npos ||
|
|
lowerName.find("gemma") != std::string::npos;
|
|
}
|
|
|
|
bool isGpuExecutionProviderName(std::string provider) {
|
|
provider = lowerCopy(std::move(provider));
|
|
return provider.find("cuda") != std::string::npos ||
|
|
provider.find("migraphx") != std::string::npos ||
|
|
provider.find("coreml") != std::string::npos ||
|
|
provider.find("dml") != std::string::npos ||
|
|
provider.find("directml") != std::string::npos;
|
|
}
|
|
|
|
GraphOptimizationLevel resolveGraphOptimizationLevel(bool cpuFallback) {
|
|
GraphOptimizationLevel optLevel = cpuFallback ? GraphOptimizationLevel::ORT_ENABLE_BASIC
|
|
: GraphOptimizationLevel::ORT_ENABLE_EXTENDED;
|
|
if (const char* opt = std::getenv("YAMS_ONNX_OPT_LEVEL")) {
|
|
std::string level = lowerCopy(opt);
|
|
if (level == "all") {
|
|
optLevel = GraphOptimizationLevel::ORT_ENABLE_ALL;
|
|
if (!cpuFallback) {
|
|
spdlog::info("[ONNX] Using ORT_ENABLE_ALL optimization (slow first load)");
|
|
}
|
|
} else if (level == "extended") {
|
|
optLevel = GraphOptimizationLevel::ORT_ENABLE_EXTENDED;
|
|
} else if (level == "basic") {
|
|
optLevel = GraphOptimizationLevel::ORT_ENABLE_BASIC;
|
|
}
|
|
}
|
|
return optLevel;
|
|
}
|
|
|
|
int resolveCpuFallbackIntraThreads(int configuredIntraThreads) {
|
|
if (const char* value = std::getenv("YAMS_ONNX_INTRA_OP_THREADS")) {
|
|
try {
|
|
const int parsed = std::stoi(value);
|
|
if (parsed >= 1 && parsed <= 64) {
|
|
return parsed;
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
const int fallback = configuredIntraThreads > 0 ? configuredIntraThreads : 2;
|
|
return std::clamp(fallback, 1, 2);
|
|
}
|
|
|
|
int resolveCpuFallbackInterThreads(int configuredInterThreads) {
|
|
if (const char* value = std::getenv("YAMS_ONNX_INTER_OP_THREADS")) {
|
|
try {
|
|
const int parsed = std::stoi(value);
|
|
if (parsed >= 1 && parsed <= 64) {
|
|
return parsed;
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
(void)configuredInterThreads;
|
|
return 1;
|
|
}
|
|
|
|
bool resolveCpuFallbackAllowSpinning() {
|
|
if (const char* value = std::getenv("YAMS_ONNX_ALLOW_SPINNING")) {
|
|
return envTruthy(value);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Resolve path for caching ONNX Runtime optimized graphs on disk.
|
|
// Precedence: YAMS_DATA_DIR > XDG_DATA_HOME > ~/.local/share > model directory.
|
|
fs::path resolveOptimizedModelPath(const std::string& modelPath, GraphOptimizationLevel optLevel) {
|
|
std::string filename = fs::path(modelPath).stem().string() + "." +
|
|
std::to_string(static_cast<int>(optLevel)) + ".ort";
|
|
if (const char* dir = std::getenv("YAMS_DATA_DIR"); dir && *dir) {
|
|
return fs::path(dir) / "cache" / "onnx-opt" / filename;
|
|
}
|
|
if (const char* xdg = std::getenv("XDG_DATA_HOME"); xdg && *xdg) {
|
|
return fs::path(xdg) / "yams" / "cache" / "onnx-opt" / filename;
|
|
}
|
|
if (const char* home = std::getenv("HOME"); home && *home) {
|
|
return fs::path(home) / ".local" / "share" / "yams" / "cache" / "onnx-opt" / filename;
|
|
}
|
|
return fs::path(modelPath).parent_path() / filename;
|
|
}
|
|
|
|
bool gpuProcessLockEnabled() {
|
|
if (const char* v = std::getenv("YAMS_GPU_PROCESS_LOCK")) {
|
|
return envTruthy(v);
|
|
}
|
|
if (const char* v = std::getenv("YAMS_MIGRAPHX_PROCESS_LOCK")) {
|
|
return envTruthy(v);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::filesystem::path resolveGpuProcessLockPath() {
|
|
if (const char* configured = std::getenv("YAMS_GPU_LOCK_PATH"); configured && *configured) {
|
|
std::filesystem::path path(configured);
|
|
if (path.extension() == ".lock") {
|
|
return path;
|
|
}
|
|
auto devEnv = std::getenv("YAMS_GPU_DEVICE_ID");
|
|
int deviceId = devEnv ? std::stoi(devEnv) : 0;
|
|
return path / ("yams-gpu-device-" + std::to_string(deviceId) + ".lock");
|
|
}
|
|
|
|
if (const char* configured = std::getenv("YAMS_MIGRAPHX_LOCK_PATH");
|
|
configured && *configured) {
|
|
std::filesystem::path path(configured);
|
|
if (path.extension() == ".lock") {
|
|
return path;
|
|
}
|
|
return path / "migraphx.lock"; // Fallback legacy behavior if needed, or just standard
|
|
}
|
|
|
|
int deviceId = envIntOr("YAMS_GPU_DEVICE_ID", -1);
|
|
if (deviceId < 0) {
|
|
deviceId = envIntOr("YAMS_MIGRAPHX_DEVICE_ID", 0);
|
|
}
|
|
return std::filesystem::temp_directory_path() /
|
|
("yams-gpu-device-" + std::to_string(deviceId) + ".lock");
|
|
}
|
|
|
|
class ScopedProcessFileLock {
|
|
public:
|
|
explicit ScopedProcessFileLock(const std::filesystem::path& path) : path_(path) {
|
|
std::error_code ec;
|
|
auto parent = path_.parent_path();
|
|
if (!parent.empty()) {
|
|
std::filesystem::create_directories(parent, ec);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
fd_ = _open(path_.string().c_str(), _O_CREAT | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
|
|
if (fd_ < 0) {
|
|
return;
|
|
}
|
|
hFile_ = reinterpret_cast<HANDLE>(_get_osfhandle(fd_));
|
|
if (hFile_ == INVALID_HANDLE_VALUE) {
|
|
_close(fd_);
|
|
fd_ = -1;
|
|
return;
|
|
}
|
|
OVERLAPPED overlapped = {0};
|
|
if (!LockFileEx(hFile_, LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &overlapped)) {
|
|
_close(fd_);
|
|
fd_ = -1;
|
|
hFile_ = INVALID_HANDLE_VALUE;
|
|
return;
|
|
}
|
|
#else
|
|
fd_ = ::open(path_.c_str(), O_CREAT | O_RDWR, 0644);
|
|
if (fd_ < 0) {
|
|
return;
|
|
}
|
|
if (::flock(fd_, LOCK_EX) != 0) {
|
|
::close(fd_);
|
|
fd_ = -1;
|
|
return;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
ScopedProcessFileLock(const ScopedProcessFileLock&) = delete;
|
|
ScopedProcessFileLock& operator=(const ScopedProcessFileLock&) = delete;
|
|
|
|
bool locked() const { return fd_ >= 0; }
|
|
|
|
~ScopedProcessFileLock() {
|
|
if (fd_ < 0) {
|
|
return;
|
|
}
|
|
#ifdef _WIN32
|
|
if (hFile_ != INVALID_HANDLE_VALUE) {
|
|
OVERLAPPED overlapped = {0};
|
|
(void)UnlockFileEx(hFile_, 0, 1, 0, &overlapped);
|
|
}
|
|
_close(fd_);
|
|
#else
|
|
(void)::flock(fd_, LOCK_UN);
|
|
::close(fd_);
|
|
#endif
|
|
}
|
|
|
|
private:
|
|
std::filesystem::path path_;
|
|
int fd_{-1};
|
|
#ifdef _WIN32
|
|
HANDLE hFile_{INVALID_HANDLE_VALUE};
|
|
#endif
|
|
};
|
|
|
|
size_t& gpuProcessLockDepth() {
|
|
static thread_local size_t depth = 0;
|
|
return depth;
|
|
}
|
|
|
|
// Thread-local reusable buffers for ONNX tensor inputs.
|
|
// Avoids per-inference heap allocation for tokens, masks, and token_type_ids.
|
|
struct TensorBufferPool {
|
|
std::vector<int64_t> tokens;
|
|
std::vector<int64_t> masks;
|
|
std::vector<int64_t> token_type_ids;
|
|
};
|
|
|
|
TensorBufferPool& getThreadLocalBuffers() {
|
|
static thread_local TensorBufferPool pool;
|
|
return pool;
|
|
}
|
|
|
|
std::string providerBatchLimitKey(const std::string& modelName, const std::string& provider) {
|
|
std::string providerLower = provider;
|
|
std::transform(providerLower.begin(), providerLower.end(), providerLower.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return modelName + "|" + providerLower;
|
|
}
|
|
|
|
std::recursive_mutex& sharedBatchLimitMutex() {
|
|
static auto* mu = new std::recursive_mutex();
|
|
return *mu;
|
|
}
|
|
|
|
std::unordered_map<std::string, size_t>& sharedBatchLimitMap() {
|
|
static auto* caps = new std::unordered_map<std::string, size_t>();
|
|
return *caps;
|
|
}
|
|
|
|
size_t loadSharedBatchLimit(const std::string& modelName, const std::string& provider) {
|
|
const std::string key = providerBatchLimitKey(modelName, provider);
|
|
std::lock_guard<std::recursive_mutex> lock(sharedBatchLimitMutex());
|
|
const auto& caps = sharedBatchLimitMap();
|
|
const auto it = caps.find(key);
|
|
return (it == caps.end()) ? 0 : it->second;
|
|
}
|
|
|
|
size_t mergeSharedBatchLimit(const std::string& modelName, const std::string& provider,
|
|
size_t observedCap) {
|
|
if (observedCap == 0) {
|
|
return 0;
|
|
}
|
|
const std::string key = providerBatchLimitKey(modelName, provider);
|
|
std::lock_guard<std::recursive_mutex> lock(sharedBatchLimitMutex());
|
|
auto& caps = sharedBatchLimitMap();
|
|
const auto it = caps.find(key);
|
|
const size_t merged = (it == caps.end()) ? observedCap : std::min(it->second, observedCap);
|
|
caps[key] = merged;
|
|
return merged;
|
|
}
|
|
|
|
size_t resolveCpuFallbackBatchCap() {
|
|
constexpr size_t kSafeCpuFallbackBatchCap = 4;
|
|
const size_t configuredCap = TuneAdvisor::getEmbedDocCap();
|
|
if (configuredCap == 0) {
|
|
return kSafeCpuFallbackBatchCap;
|
|
}
|
|
return std::max<size_t>(1u, std::min(configuredCap, kSafeCpuFallbackBatchCap));
|
|
}
|
|
|
|
size_t applyCpuFallbackSafetyCap(const std::string& modelName, const std::string& provider,
|
|
std::atomic<size_t>& providerBatchLimit) {
|
|
const size_t safeCap = resolveCpuFallbackBatchCap();
|
|
const size_t mergedCap = mergeSharedBatchLimit(modelName, provider, safeCap);
|
|
providerBatchLimit.store(mergedCap, std::memory_order_relaxed);
|
|
|
|
const size_t currentCap = TuneAdvisor::getEmbedDocCap();
|
|
if (currentCap == 0 || currentCap > mergedCap) {
|
|
TuneAdvisor::setEmbedDocCap(mergedCap);
|
|
}
|
|
|
|
return mergedCap;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ============================================================================
|
|
// Cross-instance CoreML denylist
|
|
// ============================================================================
|
|
// When a model fails CoreML inference (e.g., mask feature partition bug),
|
|
// record it here so other pool instances skip CoreML and go straight to CPU.
|
|
// This avoids wasting ~20s per instance on CoreML compilation + failed retries.
|
|
|
|
static std::mutex g_coreml_denylist_mu;
|
|
static std::unordered_set<std::string> g_coreml_denylist;
|
|
|
|
bool OnnxModelSession::isCoreMLDenylisted(const std::string& modelName) {
|
|
std::lock_guard<std::mutex> lk(g_coreml_denylist_mu);
|
|
return g_coreml_denylist.count(modelName) > 0;
|
|
}
|
|
|
|
void OnnxModelSession::denylistCoreML(const std::string& modelName) {
|
|
std::lock_guard<std::mutex> lk(g_coreml_denylist_mu);
|
|
g_coreml_denylist.insert(modelName);
|
|
}
|
|
|
|
void OnnxModelSession::clearCoreMLDenylist() {
|
|
std::lock_guard<std::mutex> lk(g_coreml_denylist_mu);
|
|
g_coreml_denylist.clear();
|
|
}
|
|
|
|
// ============================================================================
|
|
// OnnxModelSession Implementation
|
|
// ============================================================================
|
|
|
|
class OnnxModelSession::Impl {
|
|
public:
|
|
Impl(const std::string& modelPath, const std::string& modelName, const OnnxTextConfig& config)
|
|
: modelPath_(modelPath), modelName_(modelName), config_(config), preprocessor_(config) {
|
|
// Check if dynamic padding is disabled via environment variable
|
|
if (const char* dp = std::getenv("YAMS_ONNX_DYNAMIC_PADDING")) {
|
|
std::string val(dp);
|
|
std::transform(val.begin(), val.end(), val.begin(), ::tolower);
|
|
if (val == "0" || val == "false" || val == "no" || val == "off") {
|
|
dynamicPaddingEnabled_ = false;
|
|
}
|
|
}
|
|
#ifdef GTEST_API_
|
|
// Check if we should skip model loading in test mode
|
|
if (yams::test::shouldSkipModelLoading()) {
|
|
test_mode_ = true;
|
|
spdlog::debug("[ONNX] Test mode enabled - skipping actual model loading");
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// Lightweight, environment-driven mock mode for CI and constrained hosts
|
|
// (works regardless of gtest being present in this translation unit).
|
|
if (std::getenv("YAMS_USE_MOCK_PROVIDER") || std::getenv("YAMS_SKIP_MODEL_LOADING") ||
|
|
std::getenv("YAMS_TEST_MODE")) {
|
|
test_mode_ = true;
|
|
spdlog::warn("[ONNX] Mock provider mode enabled via env; skipping ONNX init");
|
|
return;
|
|
}
|
|
|
|
const auto& runtimeInfo = yams::onnx_util::OrtRuntimeLoader::instance().ensureLoaded();
|
|
if (!runtimeInfo.available) {
|
|
throw std::runtime_error(runtimeInfo.errorMessage.empty()
|
|
? std::string("ONNX Runtime unavailable")
|
|
: runtimeInfo.errorMessage);
|
|
}
|
|
|
|
memoryInfo_ = std::make_unique<Ort::MemoryInfo>(
|
|
Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault));
|
|
|
|
// Initialize ONNX Runtime environment
|
|
// Use a single global Ort::Env per process (best practice)
|
|
// The global env is protected by g_onnx_init_mutex during initialization.
|
|
spdlog::debug("[ONNX] Impl constructor: getting global Ort::Env for '{}'", modelName);
|
|
env_ = &get_global_ort_env();
|
|
spdlog::debug("[ONNX] Impl constructor: global Ort::Env obtained for '{}'", modelName);
|
|
|
|
// Configure session options
|
|
sessionOptions_ = std::make_unique<Ort::SessionOptions>();
|
|
// Allow env overrides for thread tuning
|
|
auto detect_threads = [](const char* name, int fallback) {
|
|
if (const char* s = std::getenv(name)) {
|
|
try {
|
|
int v = std::stoi(s);
|
|
if (v > 0 && v <= 64)
|
|
return v;
|
|
} catch (...) {
|
|
}
|
|
}
|
|
return fallback;
|
|
};
|
|
auto detect_bool = [](const char* name, bool fallback) {
|
|
if (const char* s = std::getenv(name)) {
|
|
std::string val(s);
|
|
std::transform(val.begin(), val.end(), val.begin(), ::tolower);
|
|
if (val == "0" || val == "false" || val == "no")
|
|
return false;
|
|
if (val == "1" || val == "true" || val == "yes")
|
|
return true;
|
|
}
|
|
return fallback;
|
|
};
|
|
|
|
#ifdef _WIN32
|
|
// Windows-specific: ALWAYS use minimal thread counts to avoid thread pool
|
|
// exhaustion and "resource deadlock would occur" errors. The Windows thread
|
|
// scheduler behaves differently than Linux. Do NOT use config.num_threads
|
|
// here as it defaults to hardware_concurrency() which causes issues.
|
|
int intra_default = 1; // Single thread for intra-op
|
|
int inter_default = 1; // Single thread for inter-op
|
|
bool allow_spinning_default = false; // No spinning on Windows
|
|
#else
|
|
int intra_default = 4;
|
|
int inter_default = 1;
|
|
bool allow_spinning_default = true;
|
|
#endif
|
|
|
|
// On Windows, ignore config.num_threads to enforce safe defaults
|
|
// On other platforms, respect the config
|
|
#ifdef _WIN32
|
|
int intra = detect_threads("YAMS_ONNX_INTRA_OP_THREADS", intra_default);
|
|
#else
|
|
int intra = config.num_threads > 0 ? config.num_threads : intra_default;
|
|
intra = detect_threads("YAMS_ONNX_INTRA_OP_THREADS", intra);
|
|
#endif
|
|
int inter = config.inter_op_threads > 0 ? config.inter_op_threads : inter_default;
|
|
inter = detect_threads("YAMS_ONNX_INTER_OP_THREADS", inter);
|
|
configuredIntraThreads_ = intra;
|
|
configuredInterThreads_ = inter;
|
|
sessionOptions_->SetIntraOpNumThreads(intra);
|
|
sessionOptions_->SetInterOpNumThreads(inter);
|
|
|
|
// Thread spinning control - spinning threads consume CPU cycles waiting for work
|
|
// which can cause contention issues on Windows with concurrent embedding requests
|
|
bool allow_spinning = detect_bool("YAMS_ONNX_ALLOW_SPINNING", allow_spinning_default);
|
|
sessionOptions_->AddConfigEntry("session.intra_op.allow_spinning",
|
|
allow_spinning ? "1" : "0");
|
|
sessionOptions_->AddConfigEntry("session.inter_op.allow_spinning",
|
|
allow_spinning ? "1" : "0");
|
|
|
|
spdlog::debug("[ONNX] SessionOptions threads: intra-op={} inter-op={} spinning={}", intra,
|
|
inter, allow_spinning);
|
|
|
|
#ifdef _WIN32
|
|
// On Windows, use sequential execution mode to avoid thread pool issues
|
|
// This ensures ONNX Runtime doesn't spawn additional threads internally
|
|
sessionOptions_->SetExecutionMode(ExecutionMode::ORT_SEQUENTIAL);
|
|
spdlog::info("[ONNX] Windows: using sequential execution mode");
|
|
#endif
|
|
|
|
// Graph optimization level: EXTENDED enables operator fusion (MatMul+Add, GELU, etc.)
|
|
// which significantly benefits transformer embedding models.
|
|
// ORT_ENABLE_ALL performs additional expensive transformations (minutes for large models).
|
|
// Override with YAMS_ONNX_OPT_LEVEL=basic|extended|all
|
|
GraphOptimizationLevel optLevel = resolveGraphOptimizationLevel(false);
|
|
sessionOptions_->SetGraphOptimizationLevel(optLevel);
|
|
|
|
// Cache the optimized graph to disk so subsequent loads skip optimization passes.
|
|
if (detect_bool("YAMS_ONNX_GRAPH_CACHE", false)) {
|
|
auto optPath = resolveOptimizedModelPath(modelPath, optLevel);
|
|
std::error_code ec;
|
|
fs::create_directories(optPath.parent_path(), ec);
|
|
if (!ec) {
|
|
sessionOptions_->SetOptimizedModelFilePath(optPath.c_str());
|
|
spdlog::info("[ONNX] Optimized graph cache: {}", optPath.string());
|
|
} else {
|
|
spdlog::warn("[ONNX] Could not create graph cache dir {}: {}",
|
|
optPath.parent_path().string(), ec.message());
|
|
}
|
|
}
|
|
|
|
// Enable memory pattern optimization for faster inference
|
|
sessionOptions_->EnableMemPattern();
|
|
// Enable CPU memory arena for better memory reuse
|
|
sessionOptions_->EnableCpuMemArena();
|
|
|
|
if (config.enable_gpu) {
|
|
if (OnnxModelSession::isCoreMLDenylisted(modelName)) {
|
|
const int fallbackIntra = resolveCpuFallbackIntraThreads(configuredIntraThreads_);
|
|
const int fallbackInter = resolveCpuFallbackInterThreads(configuredInterThreads_);
|
|
const bool allowSpinning = resolveCpuFallbackAllowSpinning();
|
|
sessionOptions_->SetIntraOpNumThreads(fallbackIntra);
|
|
sessionOptions_->SetInterOpNumThreads(fallbackInter);
|
|
sessionOptions_->AddConfigEntry("session.intra_op.allow_spinning",
|
|
allowSpinning ? "1" : "0");
|
|
sessionOptions_->AddConfigEntry("session.inter_op.allow_spinning",
|
|
allowSpinning ? "1" : "0");
|
|
configuredIntraThreads_ = fallbackIntra;
|
|
configuredInterThreads_ = fallbackInter;
|
|
const size_t fallbackBatchCap =
|
|
applyCpuFallbackSafetyCap(modelName_, "cpu", providerBatchLimit_);
|
|
spdlog::info("[ONNX] Skipping CoreML for '{}' (previously failed; "
|
|
"CPU with intra_threads={} inter_threads={} batch_cap={} "
|
|
"spinning={})",
|
|
modelName, fallbackIntra, fallbackInter, fallbackBatchCap,
|
|
allowSpinning ? 1 : 0);
|
|
actualExecutionProvider_ = "cpu";
|
|
} else {
|
|
appendGpuExecutionProvider();
|
|
}
|
|
}
|
|
}
|
|
|
|
Result<void> loadModel() {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
if (test_mode_) {
|
|
// In test mode, pretend loading succeeded
|
|
// Derive embeddingDim_/maxSequenceLength_/pooling/normalize from nearby configs when
|
|
// available to better match real model characteristics.
|
|
try {
|
|
parseModelConfigHints();
|
|
} catch (...) {
|
|
}
|
|
// Heuristics based on common model families when config hints are absent
|
|
auto lname = modelName_;
|
|
std::transform(lname.begin(), lname.end(), lname.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
// Dimension defaults
|
|
if (embeddingDim_ == 0) {
|
|
auto has = [&](const char* s) { return lname.find(s) != std::string::npos; };
|
|
// OpenAI TE3 sizes (mock support for external references)
|
|
if (has("text-embedding-3-large")) {
|
|
embeddingDim_ = 3072;
|
|
} else if (has("text-embedding-3-small")) {
|
|
embeddingDim_ = 1536;
|
|
} else if (has("e5")) {
|
|
if (has("large"))
|
|
embeddingDim_ = 1024;
|
|
else if (has("small"))
|
|
embeddingDim_ = 384;
|
|
else
|
|
embeddingDim_ = 768; // base/default
|
|
} else if (has("gte")) {
|
|
if (has("large"))
|
|
embeddingDim_ = 1024;
|
|
else if (has("small"))
|
|
embeddingDim_ = 384;
|
|
else
|
|
embeddingDim_ = 768; // base/default
|
|
} else if (has("bge")) {
|
|
if (has("-m3") || has("m3") || has("large"))
|
|
embeddingDim_ = 1024;
|
|
else if (has("small"))
|
|
embeddingDim_ = 384;
|
|
else
|
|
embeddingDim_ = 768; // base/default
|
|
} else if (has("nomic") || has("mpnet") || has("jina")) {
|
|
embeddingDim_ = 768;
|
|
} else if (has("minilm")) {
|
|
embeddingDim_ = 384;
|
|
} else if (has("colbert")) {
|
|
embeddingDim_ = 48;
|
|
}
|
|
}
|
|
// Sequence length defaults
|
|
if (maxSequenceLength_ == 0 || maxSequenceLength_ == 512) {
|
|
auto has = [&](const char* s) { return lname.find(s) != std::string::npos; };
|
|
if (has("nomic") || has("bge-m3") || has("text-embedding-3-")) {
|
|
maxSequenceLength_ = 8192;
|
|
} else if (has("minilm")) {
|
|
maxSequenceLength_ = 256;
|
|
} else if (has("mpnet") || has("bge") || has("e5") || has("gte") ||
|
|
has("colbert")) {
|
|
maxSequenceLength_ = 512;
|
|
}
|
|
}
|
|
// Respect explicit config cap
|
|
if (config_.max_sequence_length > 0 &&
|
|
maxSequenceLength_ > config_.max_sequence_length) {
|
|
maxSequenceLength_ = config_.max_sequence_length;
|
|
}
|
|
isLoaded_ = true;
|
|
return Result<void>();
|
|
}
|
|
|
|
auto providerLower = actualExecutionProvider_;
|
|
std::transform(providerLower.begin(), providerLower.end(), providerLower.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
std::unique_lock<std::recursive_mutex> gpuLoadLock;
|
|
std::unique_ptr<ScopedProcessFileLock> gpuProcessLoadLock;
|
|
|
|
bool usesGpuProvider = false;
|
|
if (providerLower.find("migraphx") != std::string::npos ||
|
|
providerLower.find("cuda") != std::string::npos ||
|
|
providerLower.find("dml") != std::string::npos ||
|
|
providerLower.find("directml") != std::string::npos) {
|
|
usesGpuProvider = true;
|
|
}
|
|
|
|
if (usesGpuProvider) {
|
|
// Serialize GPU load paths to avoid non-deterministic GPU faults during startup.
|
|
static std::recursive_mutex* g_gpu_load_mutex = new std::recursive_mutex();
|
|
gpuLoadLock = std::unique_lock<std::recursive_mutex>(*g_gpu_load_mutex);
|
|
spdlog::debug("[ONNX] GPU load gate acquired for '{}'", modelName_);
|
|
|
|
if (gpuProcessLockEnabled()) {
|
|
const auto processLockPath = resolveGpuProcessLockPath();
|
|
gpuProcessLoadLock = std::make_unique<ScopedProcessFileLock>(processLockPath);
|
|
if (gpuProcessLoadLock->locked()) {
|
|
spdlog::debug("[ONNX] GPU process load lock acquired: {}",
|
|
processLockPath.string());
|
|
} else {
|
|
spdlog::warn("[ONNX] Failed to acquire GPU process load lock: {}",
|
|
processLockPath.string());
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
spdlog::debug("[ONNX] Creating Ort::Session for model '{}' at {}", modelName_.c_str(),
|
|
modelPath_.c_str());
|
|
|
|
int retries = 3;
|
|
while (retries-- > 0) {
|
|
try {
|
|
// NOTE: Removed g_onnx_init_mutex lock here - it may have been conflicting
|
|
// with ONNX Runtime's internal locking. We rely on:
|
|
// 1. Single-flight pattern in loadModel() to prevent concurrent model loads
|
|
// 2. ONNX Runtime configured for single-threaded operation on Windows
|
|
spdlog::debug("[ONNX] Configuring session options for '{}'", modelName_);
|
|
|
|
// Clone session options; on Windows force single-thread to avoid deadlocks
|
|
auto options = sessionOptions_->Clone();
|
|
#ifdef _WIN32
|
|
// Windows: force single-threaded to avoid "resource deadlock would occur"
|
|
options.SetIntraOpNumThreads(1);
|
|
options.SetInterOpNumThreads(1);
|
|
#endif
|
|
// Non-Windows: use configured threads from sessionOptions_ (default intra=4)
|
|
applyCoreMLFreeDimensionOverrides(options);
|
|
|
|
// Create session directly - no async wrapper needed for local file operations
|
|
spdlog::debug("[ONNX] Creating Ort::Session for '{}' at '{}'", modelName_,
|
|
modelPath_);
|
|
session_ = std::make_unique<Ort::Session>(
|
|
*env_, std::filesystem::path(modelPath_).c_str(), options);
|
|
spdlog::info("[ONNX] Ort::Session created successfully for '{}'", modelName_);
|
|
|
|
// Success!
|
|
break;
|
|
} catch (const std::system_error& e) {
|
|
spdlog::warn("[ONNX] loadModel: system_error '{}' (code={}). Retries left: {}",
|
|
e.what(), e.code().value(), retries);
|
|
if (retries == 0)
|
|
throw;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
} catch (const Ort::Exception& e) {
|
|
spdlog::warn("[ONNX] loadModel: Ort::Exception '{}'. Retries left: {}",
|
|
e.what(), retries);
|
|
if (retries == 0)
|
|
throw;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
}
|
|
}
|
|
|
|
// Get input/output information
|
|
size_t numInputs = session_->GetInputCount();
|
|
size_t numOutputs = session_->GetOutputCount();
|
|
|
|
if (numInputs < 2) {
|
|
return Error{ErrorCode::InvalidData,
|
|
"Model must have at least 2 inputs (input_ids, attention_mask)"};
|
|
}
|
|
|
|
// Get input/output names
|
|
Ort::AllocatorWithDefaultOptions allocator;
|
|
for (size_t i = 0; i < numInputs; ++i) {
|
|
auto inputName = session_->GetInputNameAllocated(i, allocator);
|
|
inputNames_.push_back(inputName.get());
|
|
// Inspect first input shape to detect model sequence length when available
|
|
if (i == 0) {
|
|
try {
|
|
auto inInfo = session_->GetInputTypeInfo(i);
|
|
auto inTensor = inInfo.GetTensorTypeAndShapeInfo();
|
|
auto inShape = inTensor.GetShape();
|
|
// Expect shape like [batch, seq_len]; seq_len > 0 means fixed
|
|
if (inShape.size() >= 2) {
|
|
auto seq = inShape[1];
|
|
if (seq > 0) {
|
|
maxSequenceLength_ = static_cast<size_t>(seq);
|
|
hasFixedInputShape_ = true;
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
spdlog::debug("[ONNX] Failed to read input shape: {}", e.what());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < numOutputs; ++i) {
|
|
auto outputName = session_->GetOutputNameAllocated(i, allocator);
|
|
outputNames_.push_back(outputName.get());
|
|
}
|
|
|
|
// Get output shape to determine embedding dimension
|
|
if (numOutputs > 0) {
|
|
auto outputInfo = session_->GetOutputTypeInfo(0);
|
|
auto tensorInfo = outputInfo.GetTensorTypeAndShapeInfo();
|
|
auto shape = tensorInfo.GetShape();
|
|
if (shape.size() >= 2 && shape.back() > 0) {
|
|
embeddingDim_ = static_cast<size_t>(shape.back());
|
|
}
|
|
}
|
|
|
|
// Try to parse nearby config (Sentence-Transformers) for pooling/max
|
|
// length/normalization
|
|
parseModelConfigHints();
|
|
|
|
// Load tokenizer.json from model directory (same search dirs as config.json)
|
|
try {
|
|
namespace fs = std::filesystem;
|
|
fs::path mp(modelPath_);
|
|
for (const auto& dir : {mp.parent_path(), mp.parent_path().has_parent_path()
|
|
? mp.parent_path().parent_path()
|
|
: fs::path{}}) {
|
|
if (dir.empty())
|
|
continue;
|
|
fs::path tok = dir / "tokenizer.json";
|
|
if (fs::exists(tok) && tokenizer_.load(tok.string())) {
|
|
spdlog::info("[ONNX] Loaded tokenizer from {} (vocab={})", tok.string(),
|
|
tokenizer_.vocabSize());
|
|
break;
|
|
}
|
|
}
|
|
if (!tokenizer_.isLoaded()) {
|
|
spdlog::warn(
|
|
"[ONNX] tokenizer.json not found; using TextPreprocessor fallback");
|
|
}
|
|
} catch (...) {
|
|
}
|
|
|
|
// If input shape and config did not give a positive seq_len, fall back to a heuristic.
|
|
if (maxSequenceLength_ == 0) {
|
|
if (modelName_.find("MiniLM") != std::string::npos) {
|
|
maxSequenceLength_ = 256;
|
|
} else if (modelName_.find("mpnet") != std::string::npos) {
|
|
maxSequenceLength_ = 512;
|
|
} else if (modelName_.find("nomic") != std::string::npos) {
|
|
// Nomic BERT supports very long contexts (e.g., 8192). Default to 2048 unless
|
|
// user config overrides to avoid excessive memory on defaults.
|
|
maxSequenceLength_ = 2048;
|
|
} else if (modelName_.find("embeddinggemma") != std::string::npos ||
|
|
modelName_.find("gemma") != std::string::npos) {
|
|
maxSequenceLength_ = 2048;
|
|
} else {
|
|
maxSequenceLength_ = 512;
|
|
}
|
|
}
|
|
// Respect configured max from EmbeddingConfig if provided
|
|
if (config_.max_sequence_length > 0) {
|
|
maxSequenceLength_ = std::min(maxSequenceLength_, config_.max_sequence_length);
|
|
}
|
|
|
|
if (gemmaCoreMLStaticBatchOneEnabled()) {
|
|
providerBatchLimit_.store(1, std::memory_order_relaxed);
|
|
spdlog::info("[ONNX] Enabled static batch_size=1 CoreML path for '{}'; larger "
|
|
"embedding batches will be chunked per item",
|
|
modelName_);
|
|
}
|
|
|
|
isLoaded_ = true;
|
|
// IoBinding benefits real GPU providers (pre-resolved memory placement);
|
|
// on CPU/CoreML-to-CPU it adds overhead vs direct Session::Run().
|
|
if (actualExecutionProvider_ == "cuda" || actualExecutionProvider_ == "migraphx" ||
|
|
actualExecutionProvider_ == "directml") {
|
|
initIoBinding();
|
|
}
|
|
spdlog::info(
|
|
"[ONNX] Session ready for '{}' (inputs={}, outputs={}, dim={}, max_seq_len={})",
|
|
modelName_.c_str(), numInputs, numOutputs, embeddingDim_, maxSequenceLength_);
|
|
return Result<void>();
|
|
|
|
} catch (const Ort::Exception& e) {
|
|
spdlog::error("[ONNX] Failed to load '{}' (Ort::Exception): {}", modelName_.c_str(),
|
|
e.what());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("Failed to load ONNX model: ") + e.what()};
|
|
} catch (const std::system_error& e) {
|
|
spdlog::error("[ONNX] Failed to load '{}' (std::system_error): {}", modelName_.c_str(),
|
|
e.what());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("Failed to load ONNX model (system error): ") + e.what()};
|
|
}
|
|
}
|
|
|
|
Result<std::vector<float>> generateEmbedding(const std::string& text) {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
if (test_mode_) {
|
|
// Representative mock: deterministic per-text embedding derived from tokens.
|
|
// - Uses same preprocessing (tokenize/truncate/pad + attention mask)
|
|
// - Pseudo-random per-token vector seeded by token id and position
|
|
// - Pooling respects configured pooling_mode_
|
|
// - Optional L2 normalization
|
|
const size_t seq_len = maxSequenceLength_ > 0 ? maxSequenceLength_ : 512;
|
|
auto toks = preprocessor_.tokenize(text);
|
|
toks = preprocessor_.truncateTokens(toks, seq_len);
|
|
toks = preprocessor_.padTokens(toks, seq_len);
|
|
auto mask = preprocessor_.generateAttentionMask(toks);
|
|
|
|
const size_t D = embeddingDim_ > 0 ? embeddingDim_ : 0;
|
|
auto prng_val = [](uint32_t seed) -> float {
|
|
// xorshift32 → map to [-1, 1]
|
|
uint32_t x = seed ? seed : 0x9e3779b9u;
|
|
x ^= x << 13;
|
|
x ^= x >> 17;
|
|
x ^= x << 5;
|
|
// Scale to [0,1)
|
|
float u = (x & 0x00FFFFFFu) / static_cast<float>(0x01000000u);
|
|
return 2.0f * u - 1.0f;
|
|
};
|
|
|
|
std::vector<float> emb(D, 0.0f);
|
|
if (pooling_mode_ == Pooling::CLS) {
|
|
const int32_t t = toks.empty() ? 0 : toks[0];
|
|
for (size_t d = 0; d < D; ++d) {
|
|
emb[d] = prng_val(static_cast<uint32_t>(t * 1315423911u + d * 2654435761u));
|
|
}
|
|
} else if (pooling_mode_ == Pooling::MAX) {
|
|
for (size_t i = 0; i < std::min(seq_len, toks.size()); ++i) {
|
|
if (i >= mask.size() || mask[i] <= 0)
|
|
continue;
|
|
const int32_t t = toks[i];
|
|
for (size_t d = 0; d < D; ++d) {
|
|
float v = prng_val(static_cast<uint32_t>(t * 2246822519u + i * 3266489917u +
|
|
d * 668265263u));
|
|
emb[d] = (i == 0) ? v : std::max(emb[d], v);
|
|
}
|
|
}
|
|
} else { // MEAN pooling
|
|
double denom = 0.0;
|
|
for (size_t i = 0; i < std::min(seq_len, toks.size()); ++i) {
|
|
float w = (i < mask.size() && mask[i] > 0) ? 1.0f : 0.0f;
|
|
if (w <= 0.0f)
|
|
continue;
|
|
denom += w;
|
|
const int32_t t = toks[i];
|
|
for (size_t d = 0; d < D; ++d) {
|
|
float v = prng_val(
|
|
static_cast<uint32_t>(t * 374761393u + i * 1664525u + d * 1013904223u));
|
|
emb[d] += v * w;
|
|
}
|
|
}
|
|
if (denom > 0.0) {
|
|
for (size_t d = 0; d < D; ++d)
|
|
emb[d] = static_cast<float>(emb[d] / denom);
|
|
}
|
|
}
|
|
|
|
if (normalize_) {
|
|
double n2 = 0.0;
|
|
for (auto v : emb) {
|
|
double dv = static_cast<double>(v);
|
|
n2 += dv * dv;
|
|
}
|
|
n2 = std::sqrt(n2);
|
|
if (n2 > 1e-8) {
|
|
for (auto& v : emb)
|
|
v = static_cast<float>(v / n2);
|
|
} else {
|
|
std::fill(emb.begin(), emb.end(), 0.0f);
|
|
}
|
|
}
|
|
return emb;
|
|
}
|
|
|
|
if (!isLoaded_) {
|
|
if (auto result = loadModel(); !result) {
|
|
return result.error();
|
|
}
|
|
}
|
|
|
|
// Try real ONNX path first (preferred)
|
|
if (auto r = withCoreMLFallback([&]() { return runOnnx(text); }); r) {
|
|
return r.value();
|
|
}
|
|
|
|
return Error{ErrorCode::InternalError,
|
|
"ONNX embedding inference failed and no fallback backend is available"};
|
|
}
|
|
|
|
// Public batch API that uses the optimized batched ONNX path
|
|
Result<std::vector<std::vector<float>>>
|
|
generateBatchEmbeddings(const std::vector<std::string>& texts) {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
if (test_mode_) {
|
|
std::vector<std::vector<float>> res;
|
|
res.reserve(texts.size());
|
|
for (const auto& t : texts) {
|
|
auto one = generateEmbedding(t);
|
|
if (!one)
|
|
return one.error();
|
|
res.emplace_back(std::move(one.value()));
|
|
}
|
|
return res;
|
|
}
|
|
if (!isLoaded_) {
|
|
if (auto result = loadModel(); !result) {
|
|
return result.error();
|
|
}
|
|
}
|
|
return withCoreMLFallback([&]() { return runOnnxBatch(texts); });
|
|
}
|
|
|
|
bool isValid() const {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
return test_mode_ ? isLoaded_ : (isLoaded_ && session_ != nullptr);
|
|
}
|
|
|
|
std::pair<int, int> getThreading() const {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
return {configuredIntraThreads_, configuredInterThreads_};
|
|
}
|
|
|
|
size_t getLearnedBatchLimit() const {
|
|
return providerBatchLimit_.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
bool prefersExplicitSentenceEmbeddingOutput() const {
|
|
return modelName_.find("embeddinggemma") != std::string::npos ||
|
|
modelName_.find("gemma") != std::string::npos;
|
|
}
|
|
|
|
bool prefersStaticShapesOnCoreML() const {
|
|
return actualExecutionProvider_ == "coreml" &&
|
|
(modelName_.find("embeddinggemma") != std::string::npos ||
|
|
modelName_.find("gemma") != std::string::npos);
|
|
}
|
|
|
|
bool gemmaCoreMLStaticBatchOneEnabled() const { return prefersStaticShapesOnCoreML(); }
|
|
|
|
bool shouldApplyCoreMLFreeDimensionOverrides() const {
|
|
return gemmaCoreMLStaticBatchOneEnabled();
|
|
}
|
|
|
|
int64_t resolveCoreMLStaticSequenceLength() const {
|
|
if (config_.max_sequence_length > 0) {
|
|
return std::max<int64_t>(1, std::min<int64_t>(config_.max_sequence_length, 2048));
|
|
}
|
|
if (maxSequenceLength_ > 512) {
|
|
return static_cast<int64_t>(maxSequenceLength_);
|
|
}
|
|
return int64_t{2048};
|
|
}
|
|
|
|
void applyCoreMLFreeDimensionOverrides(Ort::SessionOptions& options) {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
if (!shouldApplyCoreMLFreeDimensionOverrides()) {
|
|
return;
|
|
}
|
|
|
|
const int64_t effectiveBatchSize = 1;
|
|
const int64_t sequenceLength = resolveCoreMLStaticSequenceLength();
|
|
const int64_t tripleSequenceLength =
|
|
sequenceLength > (std::numeric_limits<int64_t>::max() / 3) ? sequenceLength
|
|
: (sequenceLength * 3);
|
|
|
|
Ort::ThrowOnError(Ort::GetApi().AddFreeDimensionOverrideByName(options, "batch_size",
|
|
effectiveBatchSize));
|
|
Ort::ThrowOnError(Ort::GetApi().AddFreeDimensionOverrideByName(
|
|
options, "total_sequence_length", sequenceLength));
|
|
Ort::ThrowOnError(Ort::GetApi().AddFreeDimensionOverrideByName(
|
|
options, "3*total_sequence_length", tripleSequenceLength));
|
|
spdlog::info("[ONNX] Applied Gemma CoreML static-shape overrides for '{}': batch_size={} "
|
|
"total_sequence_length={} triple_total_sequence_length={}",
|
|
modelName_, effectiveBatchSize, sequenceLength, tripleSequenceLength);
|
|
}
|
|
|
|
bool supportsCpuFallbackForCurrentModel() const {
|
|
return supportsCoreMLCpuFallbackModel(modelName_);
|
|
}
|
|
|
|
bool isCoreMLMaskFeatureFailure(const std::string& message) const {
|
|
return message.find("_model_st_pool_0_mask_unsqueeze_output_0") != std::string::npos ||
|
|
message.find("required but not specified") != std::string::npos;
|
|
}
|
|
|
|
Result<void> reinitializeSessionOnCpu() {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
if (!supportsCpuFallbackForCurrentModel()) {
|
|
return Error{ErrorCode::InternalError, "CPU fallback is not enabled for this model"};
|
|
}
|
|
|
|
if (actualExecutionProvider_ == "cpu") {
|
|
return Result<void>();
|
|
}
|
|
|
|
try {
|
|
session_.reset();
|
|
sessionOptions_ = std::make_unique<Ort::SessionOptions>();
|
|
actualExecutionProvider_ = "cpu";
|
|
providerBatchLimit_.store(0, std::memory_order_relaxed);
|
|
|
|
const int hwThreads = static_cast<int>(std::thread::hardware_concurrency());
|
|
const int fallbackIntra = resolveCpuFallbackIntraThreads(configuredIntraThreads_);
|
|
const int fallbackInter = resolveCpuFallbackInterThreads(configuredInterThreads_);
|
|
sessionOptions_->SetIntraOpNumThreads(fallbackIntra);
|
|
sessionOptions_->SetInterOpNumThreads(fallbackInter);
|
|
configuredIntraThreads_ = fallbackIntra;
|
|
configuredInterThreads_ = fallbackInter;
|
|
|
|
const bool allowSpinning = resolveCpuFallbackAllowSpinning();
|
|
sessionOptions_->AddConfigEntry("session.intra_op.allow_spinning",
|
|
allowSpinning ? "1" : "0");
|
|
sessionOptions_->AddConfigEntry("session.inter_op.allow_spinning",
|
|
allowSpinning ? "1" : "0");
|
|
|
|
#ifdef _WIN32
|
|
sessionOptions_->SetExecutionMode(ExecutionMode::ORT_SEQUENTIAL);
|
|
#endif
|
|
|
|
GraphOptimizationLevel optLevel = resolveGraphOptimizationLevel(true);
|
|
sessionOptions_->SetGraphOptimizationLevel(optLevel);
|
|
|
|
// Graph cache is on by default; disable with YAMS_ONNX_GRAPH_CACHE=0
|
|
if (!std::getenv("YAMS_ONNX_GRAPH_CACHE") ||
|
|
envTruthy(std::getenv("YAMS_ONNX_GRAPH_CACHE"))) {
|
|
auto optPath = resolveOptimizedModelPath(modelPath_, optLevel);
|
|
std::error_code ec;
|
|
fs::create_directories(optPath.parent_path(), ec);
|
|
if (!ec) {
|
|
sessionOptions_->SetOptimizedModelFilePath(optPath.c_str());
|
|
}
|
|
}
|
|
|
|
sessionOptions_->EnableMemPattern();
|
|
sessionOptions_->EnableCpuMemArena();
|
|
|
|
auto options = sessionOptions_->Clone();
|
|
#ifdef _WIN32
|
|
options.SetIntraOpNumThreads(1);
|
|
options.SetInterOpNumThreads(1);
|
|
#endif
|
|
session_ = std::make_unique<Ort::Session>(
|
|
*env_, std::filesystem::path(modelPath_).c_str(), options);
|
|
// CPU fallback — no IoBinding (adds overhead on CPU)
|
|
useIoBinding_ = false;
|
|
ioBinding_.reset();
|
|
OnnxModelSession::denylistCoreML(modelName_);
|
|
const size_t fallbackBatchCap = applyCpuFallbackSafetyCap(
|
|
modelName_, actualExecutionProvider_, providerBatchLimit_);
|
|
spdlog::warn("[ONNX] Reinitialized '{}' on CPU after CoreML inference failure "
|
|
"(denylisted for future instances, intra_threads={}, inter_threads={}, "
|
|
"batch_cap={}, spinning={}, hw={})",
|
|
modelName_, fallbackIntra, fallbackInter, fallbackBatchCap,
|
|
allowSpinning ? 1 : 0, hwThreads);
|
|
return Result<void>();
|
|
} catch (const Ort::Exception& e) {
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("CPU fallback session init failed: ") + e.what()};
|
|
} catch (const std::exception& e) {
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("CPU fallback session init failed: ") + e.what()};
|
|
}
|
|
}
|
|
|
|
template <typename Fn> auto withCoreMLFallback(Fn&& fn) -> decltype(fn()) {
|
|
auto result = fn();
|
|
if (result || actualExecutionProvider_ != "coreml" ||
|
|
!supportsCpuFallbackForCurrentModel()) {
|
|
return result;
|
|
}
|
|
|
|
const auto& message = result.error().message;
|
|
if (!isCoreMLMaskFeatureFailure(message)) {
|
|
return result;
|
|
}
|
|
|
|
auto fallback = reinitializeSessionOnCpu();
|
|
if (!fallback) {
|
|
return decltype(fn())(fallback.error());
|
|
}
|
|
return fn();
|
|
}
|
|
|
|
std::vector<const char*> resolveOutputNames() const {
|
|
if (outputNames_.empty()) {
|
|
static const char* default_out = "sentence_embedding";
|
|
return {default_out};
|
|
}
|
|
|
|
if (prefersExplicitSentenceEmbeddingOutput()) {
|
|
for (const auto& name : outputNames_) {
|
|
if (name == "sentence_embedding") {
|
|
return {name.c_str()};
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<const char*> output_names_c;
|
|
output_names_c.reserve(outputNames_.size());
|
|
for (const auto& n : outputNames_) {
|
|
output_names_c.push_back(n.c_str());
|
|
}
|
|
return output_names_c;
|
|
}
|
|
|
|
Result<void> setThreading(int intraThreads, int interThreads) {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
auto valid = [](int value) { return value == -1 || (value >= 1 && value <= 64); };
|
|
if (!valid(intraThreads) || !valid(interThreads)) {
|
|
return Error{ErrorCode::InvalidArgument,
|
|
"Threading values must be -1 or in range [1, 64]"};
|
|
}
|
|
|
|
if (intraThreads > 0) {
|
|
config_.num_threads = intraThreads;
|
|
}
|
|
if (interThreads > 0) {
|
|
config_.inter_op_threads = interThreads;
|
|
}
|
|
|
|
configuredIntraThreads_ =
|
|
(config_.num_threads > 0) ? config_.num_threads : configuredIntraThreads_;
|
|
configuredInterThreads_ =
|
|
(config_.inter_op_threads > 0) ? config_.inter_op_threads : configuredInterThreads_;
|
|
if (sessionOptions_) {
|
|
sessionOptions_->SetIntraOpNumThreads(configuredIntraThreads_);
|
|
sessionOptions_->SetInterOpNumThreads(configuredInterThreads_);
|
|
}
|
|
|
|
// Force lazy reload on next inference with updated threading.
|
|
session_.reset();
|
|
isLoaded_ = false;
|
|
|
|
return Result<void>();
|
|
}
|
|
|
|
size_t getEmbeddingDim() const {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
return embeddingDim_;
|
|
}
|
|
size_t getMaxSequenceLength() const {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
return maxSequenceLength_;
|
|
}
|
|
std::string getExecutionProvider() const {
|
|
std::lock_guard<std::recursive_mutex> lock(stateMutex_);
|
|
return actualExecutionProvider_;
|
|
}
|
|
|
|
private:
|
|
void appendGpuExecutionProvider() {
|
|
std::string cacheDir;
|
|
if (!modelPath_.empty()) {
|
|
auto parentDir = fs::path(modelPath_).parent_path();
|
|
if (fs::exists(parentDir)) {
|
|
cacheDir = parentDir.string();
|
|
}
|
|
}
|
|
actualExecutionProvider_ = onnx_util::appendGpuProvider(*sessionOptions_, cacheDir);
|
|
}
|
|
|
|
struct GpuInferenceGuard {
|
|
std::unique_lock<std::recursive_mutex> threadLock;
|
|
std::unique_ptr<ScopedProcessFileLock> processLock;
|
|
bool depthIncremented{false};
|
|
|
|
GpuInferenceGuard() = default;
|
|
GpuInferenceGuard(const GpuInferenceGuard&) = delete;
|
|
GpuInferenceGuard& operator=(const GpuInferenceGuard&) = delete;
|
|
GpuInferenceGuard(GpuInferenceGuard&&) noexcept = default;
|
|
GpuInferenceGuard& operator=(GpuInferenceGuard&&) noexcept = default;
|
|
|
|
~GpuInferenceGuard() {
|
|
if (depthIncremented) {
|
|
auto& depth = gpuProcessLockDepth();
|
|
if (depth > 0) {
|
|
--depth;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
GpuInferenceGuard acquireGpuInferenceLock() const {
|
|
GpuInferenceGuard guard;
|
|
std::string providerLower = actualExecutionProvider_;
|
|
std::transform(providerLower.begin(), providerLower.end(), providerLower.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
bool usesGpuProvider = false;
|
|
if (providerLower.find("migraphx") != std::string::npos ||
|
|
providerLower.find("cuda") != std::string::npos ||
|
|
providerLower.find("dml") != std::string::npos ||
|
|
providerLower.find("directml") != std::string::npos) {
|
|
usesGpuProvider = true;
|
|
}
|
|
if (!usesGpuProvider) {
|
|
return guard;
|
|
}
|
|
|
|
static std::recursive_mutex* g_gpu_infer_mutex = new std::recursive_mutex();
|
|
guard.threadLock = std::unique_lock<std::recursive_mutex>(*g_gpu_infer_mutex);
|
|
|
|
if (!gpuProcessLockEnabled()) {
|
|
return guard;
|
|
}
|
|
|
|
auto& depth = gpuProcessLockDepth();
|
|
++depth;
|
|
guard.depthIncremented = true;
|
|
if (depth > 1) {
|
|
return guard;
|
|
}
|
|
|
|
const auto processLockPath = resolveGpuProcessLockPath();
|
|
guard.processLock = std::make_unique<ScopedProcessFileLock>(processLockPath);
|
|
if (!guard.processLock->locked()) {
|
|
spdlog::warn("[ONNX] Failed to acquire GPU process inference lock: {}",
|
|
processLockPath.string());
|
|
guard.processLock.reset();
|
|
}
|
|
return guard;
|
|
}
|
|
|
|
// Align a sequence length up to a SIMD-friendly multiple (AVX: 8 x int32, NEON: 4).
|
|
static size_t alignSeqLen(size_t len, size_t alignment = 8) {
|
|
return ((len + alignment - 1) / alignment) * alignment;
|
|
}
|
|
|
|
void initIoBinding() {
|
|
if (!session_)
|
|
return;
|
|
try {
|
|
ioBinding_ = std::make_unique<Ort::IoBinding>(*session_);
|
|
useIoBinding_ = true;
|
|
spdlog::debug("[ONNX] IoBinding created for '{}'", modelName_);
|
|
} catch (const std::exception& e) {
|
|
spdlog::warn("[ONNX] IoBinding creation failed for '{}': {}; using Run()", modelName_,
|
|
e.what());
|
|
useIoBinding_ = false;
|
|
}
|
|
}
|
|
|
|
// Run inference using IoBinding if available, otherwise fall back to Session::Run().
|
|
std::vector<Ort::Value> runSession(std::vector<Ort::Value>& inputs) {
|
|
if (useIoBinding_ && ioBinding_) {
|
|
ioBinding_->ClearBoundInputs();
|
|
ioBinding_->ClearBoundOutputs();
|
|
for (size_t i = 0; i < inputs.size() && i < inputNames_.size(); ++i) {
|
|
ioBinding_->BindInput(inputNames_[i].c_str(), inputs[i]);
|
|
}
|
|
for (const auto& name : resolveOutputNames()) {
|
|
ioBinding_->BindOutput(name, *memoryInfo_);
|
|
}
|
|
session_->Run(Ort::RunOptions{nullptr}, *ioBinding_);
|
|
return ioBinding_->GetOutputValues();
|
|
}
|
|
// Fallback: direct Run with name arrays
|
|
std::vector<const char*> in_names;
|
|
for (auto& n : inputNames_)
|
|
in_names.push_back(n.c_str());
|
|
auto out_names = resolveOutputNames();
|
|
return session_->Run(Ort::RunOptions{nullptr}, in_names.data(), inputs.data(),
|
|
inputs.size(), out_names.data(), out_names.size());
|
|
}
|
|
|
|
Result<std::vector<float>> runOnnx(const std::string& t) {
|
|
auto gpuInferenceLock = acquireGpuInferenceLock();
|
|
|
|
const size_t seq_len = maxSequenceLength_ > 0 ? maxSequenceLength_ : 512;
|
|
auto tokens = tokenizer_.isLoaded() ? tokenizer_.encode(t) : preprocessor_.tokenize(t);
|
|
tokens = preprocessor_.truncateTokens(tokens, seq_len);
|
|
|
|
// Dynamic padding: pad to aligned actual length instead of model max
|
|
size_t effective_seq_len = seq_len;
|
|
if (dynamicPaddingEnabled_ && !hasFixedInputShape_ && !prefersStaticShapesOnCoreML()) {
|
|
effective_seq_len = std::min(alignSeqLen(tokens.size()), seq_len);
|
|
if (effective_seq_len == 0)
|
|
effective_seq_len = 1;
|
|
spdlog::debug("[ONNX] Dynamic padding (single): actual={} effective={} (model_max={})",
|
|
tokens.size(), effective_seq_len, seq_len);
|
|
} else if (prefersStaticShapesOnCoreML()) {
|
|
spdlog::debug("[ONNX] Static padding forced for '{}' on CoreML: seq_len={}", modelName_,
|
|
effective_seq_len);
|
|
}
|
|
|
|
tokens = preprocessor_.padTokens(tokens, effective_seq_len);
|
|
auto attention_mask = preprocessor_.generateAttentionMask(tokens);
|
|
|
|
std::vector<int64_t> input_shape = {1, static_cast<int64_t>(effective_seq_len)};
|
|
const size_t input_tensor_size = effective_seq_len;
|
|
|
|
// Reuse thread-local buffers to avoid per-call heap allocations.
|
|
auto& bufs = getThreadLocalBuffers();
|
|
bufs.tokens.resize(effective_seq_len);
|
|
bufs.masks.resize(effective_seq_len);
|
|
|
|
if (tokenizer_.isLoaded()) {
|
|
for (size_t i = 0; i < effective_seq_len && i < tokens.size(); ++i)
|
|
bufs.tokens[i] = static_cast<int64_t>(tokens[i]);
|
|
for (size_t i = tokens.size(); i < effective_seq_len; ++i)
|
|
bufs.tokens[i] = 0;
|
|
} else {
|
|
const int64_t MAX_TOKEN_ID = 30527;
|
|
const int64_t UNK_TOKEN_ID = 100;
|
|
for (size_t i = 0; i < effective_seq_len && i < tokens.size(); ++i) {
|
|
auto v = tokens[i];
|
|
bufs.tokens[i] =
|
|
(v < 0 || v > MAX_TOKEN_ID) ? UNK_TOKEN_ID : static_cast<int64_t>(v);
|
|
}
|
|
for (size_t i = tokens.size(); i < effective_seq_len; ++i)
|
|
bufs.tokens[i] = UNK_TOKEN_ID;
|
|
}
|
|
for (size_t i = 0; i < effective_seq_len && i < attention_mask.size(); ++i)
|
|
bufs.masks[i] = static_cast<int64_t>(attention_mask[i]);
|
|
for (size_t i = attention_mask.size(); i < effective_seq_len; ++i)
|
|
bufs.masks[i] = 0;
|
|
|
|
std::vector<Ort::Value> input_tensors;
|
|
input_tensors.push_back(Ort::Value::CreateTensor<int64_t>(
|
|
*memoryInfo_, bufs.tokens.data(), input_tensor_size, input_shape.data(), 2));
|
|
input_tensors.push_back(Ort::Value::CreateTensor<int64_t>(
|
|
*memoryInfo_, bufs.masks.data(), input_tensor_size, input_shape.data(), 2));
|
|
if (inputNames_.size() >= 3) {
|
|
bufs.token_type_ids.assign(effective_seq_len, 0);
|
|
input_tensors.push_back(
|
|
Ort::Value::CreateTensor<int64_t>(*memoryInfo_, bufs.token_type_ids.data(),
|
|
input_tensor_size, input_shape.data(), 2));
|
|
}
|
|
|
|
auto outputs = runSession(input_tensors);
|
|
if (outputs.empty()) {
|
|
return Error{ErrorCode::InternalError, "ONNX session returned no outputs"};
|
|
}
|
|
|
|
auto& out = outputs[0];
|
|
auto out_shape = out.GetTensorTypeAndShapeInfo().GetShape();
|
|
const size_t hidden_dim = embeddingDim_;
|
|
|
|
if (out_shape.size() == 3) {
|
|
const size_t seq = static_cast<size_t>(out_shape[1]);
|
|
const size_t dim = static_cast<size_t>(out_shape[2]);
|
|
if (dim != hidden_dim) {
|
|
return Error{ErrorCode::InvalidData, "Output dim mismatch"};
|
|
}
|
|
const float* data = out.GetTensorData<float>();
|
|
std::vector<float> embedding(hidden_dim, 0.0f);
|
|
if (pooling_mode_ == Pooling::CLS) {
|
|
// CLS token (first token)
|
|
const size_t off = 0;
|
|
for (size_t d = 0; d < dim; ++d)
|
|
embedding[d] = data[off + d];
|
|
} else if (pooling_mode_ == Pooling::MAX) {
|
|
for (size_t i = 0; i < std::min(seq, attention_mask.size()); ++i) {
|
|
if (attention_mask[i] <= 0)
|
|
continue;
|
|
const size_t off = i * dim;
|
|
for (size_t d = 0; d < dim; ++d)
|
|
embedding[d] = std::max(embedding[d], data[off + d]);
|
|
}
|
|
} else { // MEAN
|
|
float total_attn = 0.0f;
|
|
for (size_t i = 0; i < std::min(seq, attention_mask.size()); ++i) {
|
|
const float w = static_cast<float>(attention_mask[i]);
|
|
if (w <= 0.0f)
|
|
continue;
|
|
total_attn += w;
|
|
const size_t off = i * dim;
|
|
for (size_t d = 0; d < dim; ++d)
|
|
embedding[d] += data[off + d] * w;
|
|
}
|
|
if (total_attn > 0.0f) {
|
|
for (auto& v : embedding)
|
|
v = static_cast<float>(v / total_attn);
|
|
}
|
|
}
|
|
if (normalize_) {
|
|
double norm = 0.0;
|
|
for (auto v : embedding) {
|
|
double dv = static_cast<double>(v);
|
|
norm += dv * dv;
|
|
}
|
|
norm = std::sqrt(norm);
|
|
if (norm > 1e-8) {
|
|
for (auto& v : embedding)
|
|
v = static_cast<float>(v / norm);
|
|
} else {
|
|
std::fill(embedding.begin(), embedding.end(), 0.0f);
|
|
}
|
|
}
|
|
return embedding;
|
|
}
|
|
|
|
if (out_shape.size() == 2) {
|
|
const size_t dim = static_cast<size_t>(out_shape[1]);
|
|
if (dim != hidden_dim) {
|
|
return Error{ErrorCode::InvalidData, "Output dim mismatch"};
|
|
}
|
|
const float* data = out.GetTensorData<float>();
|
|
std::vector<float> embedding(data, data + dim);
|
|
if (normalize_) {
|
|
double norm = 0.0;
|
|
for (auto v : embedding) {
|
|
double dv = static_cast<double>(v);
|
|
norm += dv * dv;
|
|
}
|
|
norm = std::sqrt(norm);
|
|
if (norm > 1e-8) {
|
|
for (auto& v : embedding)
|
|
v = static_cast<float>(v / norm);
|
|
} else {
|
|
std::fill(embedding.begin(), embedding.end(), 0.0f);
|
|
}
|
|
}
|
|
return embedding;
|
|
}
|
|
|
|
return Error{ErrorCode::InvalidData, "Unexpected output tensor rank"};
|
|
}
|
|
|
|
Result<std::vector<std::vector<float>>> runOnnxBatch(const std::vector<std::string>& texts) {
|
|
auto gpuInferenceLock = acquireGpuInferenceLock();
|
|
|
|
if (texts.empty())
|
|
return std::vector<std::vector<float>>{};
|
|
|
|
if (providerBatchLimit_.load(std::memory_order_relaxed) == 0) {
|
|
const size_t sharedCap = loadSharedBatchLimit(modelName_, actualExecutionProvider_);
|
|
if (sharedCap > 0) {
|
|
providerBatchLimit_.store(sharedCap, std::memory_order_relaxed);
|
|
const auto currentCap = TuneAdvisor::getEmbedDocCap();
|
|
if (currentCap == 0 || currentCap > sharedCap) {
|
|
TuneAdvisor::setEmbedDocCap(sharedCap);
|
|
}
|
|
}
|
|
}
|
|
|
|
const size_t requestedBatchSize = texts.size();
|
|
size_t learnedBatchCap = providerBatchLimit_.load(std::memory_order_relaxed);
|
|
if (gemmaCoreMLStaticBatchOneEnabled() && requestedBatchSize > 1 && learnedBatchCap == 0) {
|
|
providerBatchLimit_.store(1, std::memory_order_relaxed);
|
|
learnedBatchCap = 1;
|
|
}
|
|
if (learnedBatchCap > 0 && requestedBatchSize > learnedBatchCap) {
|
|
std::vector<std::vector<float>> merged;
|
|
merged.reserve(requestedBatchSize);
|
|
for (size_t offset = 0; offset < requestedBatchSize; offset += learnedBatchCap) {
|
|
const size_t count =
|
|
std::min(learnedBatchCap, static_cast<size_t>(requestedBatchSize - offset));
|
|
std::vector<std::string> chunk;
|
|
chunk.reserve(count);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
chunk.push_back(texts[offset + i]);
|
|
}
|
|
auto chunkResult = runOnnxBatch(chunk);
|
|
if (!chunkResult) {
|
|
return chunkResult.error();
|
|
}
|
|
auto& vectors = chunkResult.value();
|
|
merged.insert(merged.end(), std::make_move_iterator(vectors.begin()),
|
|
std::make_move_iterator(vectors.end()));
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
const size_t seq_len = maxSequenceLength_ > 0 ? maxSequenceLength_ : 512;
|
|
|
|
// Pass 1: tokenize + truncate (no padding yet)
|
|
std::vector<std::vector<int32_t>> token_seqs;
|
|
token_seqs.reserve(texts.size());
|
|
for (const auto& t : texts) {
|
|
auto tokens = tokenizer_.isLoaded() ? tokenizer_.encode(t) : preprocessor_.tokenize(t);
|
|
tokens = preprocessor_.truncateTokens(tokens, seq_len);
|
|
token_seqs.push_back(std::move(tokens));
|
|
}
|
|
|
|
// Determine effective padding length
|
|
size_t effective_seq_len = seq_len;
|
|
if (dynamicPaddingEnabled_ && !hasFixedInputShape_ && !prefersStaticShapesOnCoreML()) {
|
|
size_t max_actual = 0;
|
|
for (const auto& toks : token_seqs)
|
|
max_actual = std::max(max_actual, toks.size());
|
|
effective_seq_len = std::min(alignSeqLen(max_actual), seq_len);
|
|
if (effective_seq_len == 0)
|
|
effective_seq_len = 1;
|
|
spdlog::debug("[ONNX] Dynamic padding: B={} max_actual={} effective={} (model_max={})",
|
|
texts.size(), max_actual, effective_seq_len, seq_len);
|
|
} else if (prefersStaticShapesOnCoreML()) {
|
|
spdlog::debug("[ONNX] Static padding forced for '{}' batch on CoreML: B={} seq_len={}",
|
|
modelName_, texts.size(), effective_seq_len);
|
|
}
|
|
|
|
// Pass 2: pad to effective_seq_len + generate masks
|
|
std::vector<std::vector<int32_t>> masks;
|
|
masks.reserve(texts.size());
|
|
for (auto& toks : token_seqs) {
|
|
toks = preprocessor_.padTokens(toks, effective_seq_len);
|
|
masks.push_back(preprocessor_.generateAttentionMask(toks));
|
|
}
|
|
|
|
// Prepare batched tensors [B, S]
|
|
const size_t B = texts.size();
|
|
std::vector<int64_t> input_shape = {static_cast<int64_t>(B),
|
|
static_cast<int64_t>(effective_seq_len)};
|
|
const size_t tensor_size = B * effective_seq_len;
|
|
|
|
// Reuse thread-local buffers to avoid per-call heap allocations.
|
|
auto& bufs = getThreadLocalBuffers();
|
|
bufs.tokens.resize(tensor_size);
|
|
bufs.masks.resize(tensor_size);
|
|
|
|
const bool useRealTokenizer = tokenizer_.isLoaded();
|
|
const int64_t MAX_TOKEN_ID = 30527;
|
|
const int64_t UNK_TOKEN_ID = 100;
|
|
|
|
size_t offset = 0;
|
|
for (size_t i = 0; i < B; ++i) {
|
|
const auto& toks = token_seqs[i];
|
|
const auto& m = masks[i];
|
|
for (size_t j = 0; j < effective_seq_len && j < toks.size(); ++j) {
|
|
int64_t v = toks[j];
|
|
if (useRealTokenizer) {
|
|
bufs.tokens[offset] = v;
|
|
} else {
|
|
bufs.tokens[offset] = (v < 0 || v > MAX_TOKEN_ID) ? UNK_TOKEN_ID : v;
|
|
}
|
|
bufs.masks[offset] = static_cast<int64_t>(m[j]);
|
|
++offset;
|
|
}
|
|
for (size_t j = toks.size(); j < effective_seq_len; ++j) {
|
|
bufs.tokens[offset] = useRealTokenizer ? 0 : UNK_TOKEN_ID;
|
|
bufs.masks[offset] = 0;
|
|
++offset;
|
|
}
|
|
}
|
|
|
|
std::vector<Ort::Value> inputs;
|
|
inputs.push_back(Ort::Value::CreateTensor<int64_t>(*memoryInfo_, bufs.tokens.data(),
|
|
tensor_size, input_shape.data(), 2));
|
|
inputs.push_back(Ort::Value::CreateTensor<int64_t>(*memoryInfo_, bufs.masks.data(),
|
|
tensor_size, input_shape.data(), 2));
|
|
if (inputNames_.size() >= 3) {
|
|
bufs.token_type_ids.assign(tensor_size, 0);
|
|
inputs.push_back(Ort::Value::CreateTensor<int64_t>(
|
|
*memoryInfo_, bufs.token_type_ids.data(), tensor_size, input_shape.data(), 2));
|
|
}
|
|
|
|
// Wrap ONNX Run in try/catch to prevent exceptions from bubbling through ABI boundary
|
|
// On Windows, ORT can throw std::system_error("resource deadlock would occur") when
|
|
// thread pool resources are exhausted (PBI-1c1)
|
|
std::vector<Ort::Value> outputs;
|
|
int retries = 3;
|
|
while (retries-- > 0) {
|
|
try {
|
|
outputs = runSession(inputs);
|
|
break; // synchronous success
|
|
} catch (const Ort::Exception& e) {
|
|
if (retries == 0) {
|
|
fprintf(stderr, "[ONNX Plugin] Ort::Exception in Run: %s\n", e.what());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("ONNX runtime error: ") + e.what()};
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
} catch (const std::system_error& e) {
|
|
if (std::string(e.what()).find("resource deadlock") != std::string::npos ||
|
|
retries > 0) {
|
|
spdlog::warn("[ONNX Plugin] Deadlock/system error caught, retrying... ({})",
|
|
e.what());
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
if (retries == 0) {
|
|
fprintf(stderr, "[ONNX Plugin] std::system_error in Run (exhausted): %s\n",
|
|
e.what());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("System error during ONNX inference: ") +
|
|
e.what()};
|
|
}
|
|
continue;
|
|
}
|
|
fprintf(stderr, "[ONNX Plugin] std::system_error in Run: %s\n", e.what());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("System error during ONNX inference: ") + e.what()};
|
|
} catch (const std::exception& e) {
|
|
fprintf(stderr, "[ONNX Plugin] std::exception in Run: %s\n", e.what());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("Exception during ONNX inference: ") + e.what()};
|
|
}
|
|
}
|
|
if (outputs.empty())
|
|
return Error{ErrorCode::InternalError, "ONNX session returned no outputs"};
|
|
|
|
auto& out = outputs[0];
|
|
auto shape = out.GetTensorTypeAndShapeInfo().GetShape();
|
|
// Some execution providers can leave output dims unknown until the first Run.
|
|
// If we don't know the embedding dim yet, infer it from the first output tensor.
|
|
size_t hidden_dim = embeddingDim_;
|
|
std::vector<std::vector<float>> result;
|
|
if (shape.size() == 3) {
|
|
// [B, S, D]
|
|
const int64_t b0 = shape[0];
|
|
const int64_t s1 = shape[1];
|
|
const int64_t d2 = shape[2];
|
|
const size_t SS = (s1 > 0) ? static_cast<size_t>(s1) : maxSequenceLength_;
|
|
const size_t DD = (d2 > 0) ? static_cast<size_t>(d2) : 0;
|
|
if (hidden_dim == 0 && DD > 0) {
|
|
hidden_dim = DD;
|
|
embeddingDim_ = hidden_dim;
|
|
}
|
|
|
|
if (b0 > 0 && static_cast<size_t>(b0) < B && (hidden_dim == 0 || DD == hidden_dim)) {
|
|
const size_t observedCap = static_cast<size_t>(b0);
|
|
const size_t mergedCap =
|
|
mergeSharedBatchLimit(modelName_, actualExecutionProvider_, observedCap);
|
|
if (mergedCap > 0) {
|
|
providerBatchLimit_.store(mergedCap, std::memory_order_relaxed);
|
|
const auto currentCap = TuneAdvisor::getEmbedDocCap();
|
|
if (currentCap == 0 || currentCap > mergedCap) {
|
|
TuneAdvisor::setEmbedDocCap(mergedCap);
|
|
}
|
|
}
|
|
spdlog::warn(
|
|
"[ONNX] Provider returned smaller batch than requested: requested_B={} "
|
|
"output_B={} (learned_cap={}), processing partial result + chunking remainder",
|
|
B, observedCap, mergedCap);
|
|
|
|
std::vector<std::vector<float>> partial(observedCap,
|
|
std::vector<float>(hidden_dim, 0.0f));
|
|
const float* data = out.GetTensorData<float>();
|
|
for (size_t b = 0; b < observedCap; ++b) {
|
|
auto& emb = partial[b];
|
|
if (pooling_mode_ == Pooling::CLS) {
|
|
const size_t off = b * SS * DD;
|
|
for (size_t d = 0; d < DD; ++d)
|
|
emb[d] = data[off + d];
|
|
} else if (pooling_mode_ == Pooling::MAX) {
|
|
for (size_t i = 0; i < std::min(SS, masks[b].size()); ++i) {
|
|
if (masks[b][i] <= 0)
|
|
continue;
|
|
const size_t off = b * SS * DD + i * DD;
|
|
for (size_t d = 0; d < DD; ++d)
|
|
emb[d] = std::max(emb[d], data[off + d]);
|
|
}
|
|
} else {
|
|
float total = 0.0f;
|
|
for (size_t i = 0; i < std::min(SS, masks[b].size()); ++i) {
|
|
float w = static_cast<float>(masks[b][i]);
|
|
if (w <= 0.0f) {
|
|
continue;
|
|
}
|
|
total += w;
|
|
const size_t off = b * SS * DD + i * DD;
|
|
for (size_t d = 0; d < DD; ++d)
|
|
emb[d] += data[off + d] * w;
|
|
}
|
|
if (total > 0.0f) {
|
|
for (auto& v : emb)
|
|
v = static_cast<float>(v / total);
|
|
}
|
|
}
|
|
if (normalize_) {
|
|
double norm = 0.0;
|
|
for (auto v : emb) {
|
|
const double dv = static_cast<double>(v);
|
|
norm += dv * dv;
|
|
}
|
|
norm = std::sqrt(norm);
|
|
if (norm > 1e-8) {
|
|
for (auto& v : emb)
|
|
v = static_cast<float>(v / norm);
|
|
} else {
|
|
std::fill(emb.begin(), emb.end(), 0.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> remainder;
|
|
remainder.reserve(B - observedCap);
|
|
for (size_t idx = observedCap; idx < B; ++idx) {
|
|
remainder.push_back(texts[idx]);
|
|
}
|
|
if (!remainder.empty()) {
|
|
auto rem = runOnnxBatch(remainder);
|
|
if (!rem) {
|
|
return rem.error();
|
|
}
|
|
auto& remVals = rem.value();
|
|
partial.insert(partial.end(), std::make_move_iterator(remVals.begin()),
|
|
std::make_move_iterator(remVals.end()));
|
|
}
|
|
return partial;
|
|
}
|
|
|
|
if (hidden_dim > 0 && d2 > 0 && DD != hidden_dim) {
|
|
std::ostringstream oss;
|
|
oss << "Output shape mismatch: expected [B,S,D] with B=" << B;
|
|
if (hidden_dim > 0) {
|
|
oss << " D=" << hidden_dim;
|
|
}
|
|
oss << ", got [" << b0 << "," << s1 << "," << d2 << "]";
|
|
return Error{ErrorCode::InvalidData, oss.str()};
|
|
}
|
|
if (b0 > 0 && static_cast<size_t>(b0) > B) {
|
|
spdlog::debug("[ONNX] Provider returned padded batch output: requested_B={} "
|
|
"output_B={} (using first B)",
|
|
B, b0);
|
|
}
|
|
const float* data = out.GetTensorData<float>();
|
|
result.resize(B, std::vector<float>(hidden_dim, 0.0f));
|
|
for (size_t b = 0; b < B; ++b) {
|
|
auto& emb = result[b];
|
|
if (pooling_mode_ == Pooling::CLS) {
|
|
const size_t off = b * SS * DD;
|
|
for (size_t d = 0; d < DD; ++d)
|
|
emb[d] = data[off + d];
|
|
} else if (pooling_mode_ == Pooling::MAX) {
|
|
for (size_t i = 0; i < std::min(SS, masks[b].size()); ++i) {
|
|
if (masks[b][i] <= 0)
|
|
continue;
|
|
const size_t off = b * SS * DD + i * DD;
|
|
for (size_t d = 0; d < DD; ++d)
|
|
emb[d] = std::max(emb[d], data[off + d]);
|
|
}
|
|
} else {
|
|
float total = 0.0f;
|
|
for (size_t i = 0; i < std::min(SS, masks[b].size()); ++i) {
|
|
float w = static_cast<float>(masks[b][i]);
|
|
if (w <= 0.0f) {
|
|
continue;
|
|
}
|
|
total += w;
|
|
const size_t off = b * SS * DD + i * DD;
|
|
for (size_t d = 0; d < DD; ++d)
|
|
emb[d] += data[off + d] * w;
|
|
}
|
|
if (total > 0.0f) {
|
|
for (auto& v : emb)
|
|
v = static_cast<float>(v / total);
|
|
}
|
|
}
|
|
if (normalize_) {
|
|
double norm = 0.0;
|
|
for (auto v : emb) {
|
|
double dv = static_cast<double>(v);
|
|
norm += dv * dv;
|
|
}
|
|
norm = std::sqrt(norm);
|
|
if (norm > 1e-8) {
|
|
for (auto& v : emb)
|
|
v = static_cast<float>(v / norm);
|
|
} else {
|
|
std::fill(emb.begin(), emb.end(), 0.0f);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
if (shape.size() == 2) {
|
|
// [B, D]
|
|
const int64_t b0 = shape[0];
|
|
const int64_t d1 = shape[1];
|
|
const size_t DD = (d1 > 0) ? static_cast<size_t>(d1) : 0;
|
|
if (hidden_dim == 0 && DD > 0) {
|
|
hidden_dim = DD;
|
|
embeddingDim_ = hidden_dim;
|
|
}
|
|
|
|
if (b0 > 0 && static_cast<size_t>(b0) < B && (hidden_dim == 0 || DD == hidden_dim)) {
|
|
const size_t observedCap = static_cast<size_t>(b0);
|
|
const size_t mergedCap =
|
|
mergeSharedBatchLimit(modelName_, actualExecutionProvider_, observedCap);
|
|
if (mergedCap > 0) {
|
|
providerBatchLimit_.store(mergedCap, std::memory_order_relaxed);
|
|
const auto currentCap = TuneAdvisor::getEmbedDocCap();
|
|
if (currentCap == 0 || currentCap > mergedCap) {
|
|
TuneAdvisor::setEmbedDocCap(mergedCap);
|
|
}
|
|
}
|
|
spdlog::warn(
|
|
"[ONNX] Provider returned smaller batch than requested: requested_B={} "
|
|
"output_B={} (learned_cap={}), processing partial result + chunking remainder",
|
|
B, observedCap, mergedCap);
|
|
|
|
std::vector<std::vector<float>> partial;
|
|
partial.reserve(B);
|
|
const float* data = out.GetTensorData<float>();
|
|
for (size_t b = 0; b < observedCap; ++b) {
|
|
partial.emplace_back(data + b * DD, data + (b + 1) * DD);
|
|
if (normalize_) {
|
|
double norm = 0.0;
|
|
for (auto v : partial.back()) {
|
|
const double dv = static_cast<double>(v);
|
|
norm += dv * dv;
|
|
}
|
|
norm = std::sqrt(norm);
|
|
if (norm > 1e-8) {
|
|
for (auto& v : partial.back())
|
|
v = static_cast<float>(v / norm);
|
|
} else {
|
|
std::fill(partial.back().begin(), partial.back().end(), 0.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> remainder;
|
|
remainder.reserve(B - observedCap);
|
|
for (size_t idx = observedCap; idx < B; ++idx) {
|
|
remainder.push_back(texts[idx]);
|
|
}
|
|
if (!remainder.empty()) {
|
|
auto rem = runOnnxBatch(remainder);
|
|
if (!rem) {
|
|
return rem.error();
|
|
}
|
|
auto& remVals = rem.value();
|
|
partial.insert(partial.end(), std::make_move_iterator(remVals.begin()),
|
|
std::make_move_iterator(remVals.end()));
|
|
}
|
|
return partial;
|
|
}
|
|
|
|
if (hidden_dim > 0 && d1 > 0 && DD != hidden_dim) {
|
|
std::ostringstream oss;
|
|
oss << "Output shape mismatch: expected [B,D] with B=" << B;
|
|
if (hidden_dim > 0) {
|
|
oss << " D=" << hidden_dim;
|
|
}
|
|
oss << ", got [" << b0 << "," << d1 << "]";
|
|
return Error{ErrorCode::InvalidData, oss.str()};
|
|
}
|
|
if (b0 > 0 && static_cast<size_t>(b0) > B) {
|
|
spdlog::debug("[ONNX] Provider returned padded batch output: requested_B={} "
|
|
"output_B={} (using first B)",
|
|
B, b0);
|
|
}
|
|
const float* data = out.GetTensorData<float>();
|
|
result.resize(B);
|
|
for (size_t b = 0; b < B; ++b) {
|
|
result[b].assign(data + b * DD, data + (b + 1) * DD);
|
|
if (normalize_) {
|
|
double norm = 0.0;
|
|
for (auto v : result[b]) {
|
|
double dv = static_cast<double>(v);
|
|
norm += dv * dv;
|
|
}
|
|
norm = std::sqrt(norm);
|
|
if (norm > 1e-8) {
|
|
for (auto& v : result[b])
|
|
v = static_cast<float>(v / norm);
|
|
} else {
|
|
std::fill(result[b].begin(), result[b].end(), 0.0f);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
return Error{ErrorCode::InvalidData, "Unexpected output tensor rank (batch)"};
|
|
}
|
|
|
|
std::string modelPath_;
|
|
std::string modelName_;
|
|
OnnxTextConfig config_;
|
|
OnnxTextPreprocessor preprocessor_;
|
|
yams::vector::HuggingFaceTokenizer tokenizer_;
|
|
|
|
Ort::Env* env_ = nullptr; // shared global env
|
|
std::unique_ptr<Ort::SessionOptions> sessionOptions_;
|
|
std::unique_ptr<Ort::Session> session_;
|
|
|
|
std::string actualExecutionProvider_{"cpu"};
|
|
|
|
std::vector<std::string> inputNames_;
|
|
std::vector<std::string> outputNames_;
|
|
|
|
size_t embeddingDim_ = 0;
|
|
size_t maxSequenceLength_ = 512;
|
|
bool hasFixedInputShape_ = false; // true when ONNX model input dim[1] > 0
|
|
bool isLoaded_ = false;
|
|
bool test_mode_ = false;
|
|
bool dynamicPaddingEnabled_ = true; // controlled by YAMS_ONNX_DYNAMIC_PADDING
|
|
std::atomic<size_t> providerBatchLimit_{0};
|
|
int configuredIntraThreads_{1};
|
|
int configuredInterThreads_{1};
|
|
|
|
enum class Pooling { MEAN, CLS, MAX };
|
|
Pooling pooling_mode_ = Pooling::MEAN;
|
|
bool normalize_ = true;
|
|
|
|
// Cached MemoryInfo to avoid re-creating per inference call.
|
|
std::unique_ptr<Ort::MemoryInfo> memoryInfo_;
|
|
|
|
// I/O Binding for optimized inference dispatch (pre-resolved name lookups,
|
|
// better GPU memory placement). Falls back to Session::Run() if unavailable.
|
|
std::unique_ptr<Ort::IoBinding> ioBinding_;
|
|
bool useIoBinding_ = false;
|
|
mutable std::recursive_mutex stateMutex_;
|
|
|
|
void parseModelConfigHints() {
|
|
try {
|
|
namespace fs = std::filesystem;
|
|
fs::path mp(modelPath_);
|
|
std::vector<fs::path> candidates;
|
|
if (mp.has_parent_path()) {
|
|
candidates.push_back(mp.parent_path() / "sentence_bert_config.json");
|
|
candidates.push_back(mp.parent_path() / "config.json");
|
|
if (mp.parent_path().has_parent_path()) {
|
|
auto up = mp.parent_path().parent_path();
|
|
candidates.push_back(up / "sentence_bert_config.json");
|
|
candidates.push_back(up / "config.json");
|
|
}
|
|
}
|
|
for (const auto& c : candidates) {
|
|
if (!fs::exists(c))
|
|
continue;
|
|
std::ifstream in(c);
|
|
if (!in.good())
|
|
continue;
|
|
nlohmann::json j;
|
|
in >> j;
|
|
if (j.contains("max_seq_length") && j["max_seq_length"].is_number_integer()) {
|
|
maxSequenceLength_ = static_cast<size_t>(j["max_seq_length"].get<int>());
|
|
} else if (j.contains("model_max_length") &&
|
|
j["model_max_length"].is_number_integer()) {
|
|
maxSequenceLength_ = static_cast<size_t>(j["model_max_length"].get<int>());
|
|
}
|
|
// Common HF key for context length
|
|
if (j.contains("max_position_embeddings") &&
|
|
j["max_position_embeddings"].is_number_integer()) {
|
|
maxSequenceLength_ =
|
|
static_cast<size_t>(j["max_position_embeddings"].get<int>());
|
|
}
|
|
if (j.contains("pooling_mode_cls_token") &&
|
|
j["pooling_mode_cls_token"].is_boolean() &&
|
|
j["pooling_mode_cls_token"].get<bool>()) {
|
|
pooling_mode_ = Pooling::CLS;
|
|
}
|
|
if (j.contains("pooling_mode_max_tokens") &&
|
|
j["pooling_mode_max_tokens"].is_boolean() &&
|
|
j["pooling_mode_max_tokens"].get<bool>()) {
|
|
pooling_mode_ = Pooling::MAX;
|
|
}
|
|
if (j.contains("pooling_mode_mean_tokens") &&
|
|
j["pooling_mode_mean_tokens"].is_boolean()) {
|
|
if (j["pooling_mode_mean_tokens"].get<bool>())
|
|
pooling_mode_ = Pooling::MEAN;
|
|
}
|
|
if (j.contains("normalize_embeddings") && j["normalize_embeddings"].is_boolean()) {
|
|
normalize_ = j["normalize_embeddings"].get<bool>();
|
|
}
|
|
// Only use config hints for embedding dimension when we could not infer it from
|
|
// the ONNX graph output shape. The ONNX output tensor is authoritative.
|
|
if (embeddingDim_ == 0) {
|
|
if (j.contains("embedding_dimension") &&
|
|
j["embedding_dimension"].is_number_integer()) {
|
|
embeddingDim_ = static_cast<size_t>(j["embedding_dimension"].get<int>());
|
|
}
|
|
// Hidden size often present in HF config (standard models use hidden_size,
|
|
// Nomic models use n_embd)
|
|
if (j.contains("hidden_size") && j["hidden_size"].is_number_integer()) {
|
|
embeddingDim_ = static_cast<size_t>(j["hidden_size"].get<int>());
|
|
} else if (j.contains("n_embd") && j["n_embd"].is_number_integer()) {
|
|
embeddingDim_ = static_cast<size_t>(j["n_embd"].get<int>());
|
|
}
|
|
}
|
|
// Nomic-specific hints via architectures
|
|
try {
|
|
if (j.contains("architectures") && j["architectures"].is_array()) {
|
|
for (const auto& a : j["architectures"]) {
|
|
if (a.is_string()) {
|
|
auto s = a.get<std::string>();
|
|
if (s.find("Nomic") != std::string::npos) {
|
|
pooling_mode_ = Pooling::MEAN;
|
|
normalize_ = true;
|
|
// If very long context supported and not set, prefer 8192
|
|
if (maxSequenceLength_ == 0 || maxSequenceLength_ < 2048) {
|
|
maxSequenceLength_ = 8192;
|
|
}
|
|
break;
|
|
}
|
|
// EmbeddingGemma: mean pool + L2 normalize, 2048 context
|
|
if (s.find("Gemma") != std::string::npos) {
|
|
pooling_mode_ = Pooling::MEAN;
|
|
normalize_ = true;
|
|
if (maxSequenceLength_ == 0) {
|
|
maxSequenceLength_ = 2048;
|
|
}
|
|
spdlog::info("[ONNX] Detected Gemma architecture: "
|
|
"mean pooling, L2 normalize, max_seq={}",
|
|
maxSequenceLength_);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
break; // stop after first readable config
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// OnnxModelSession Public Interface
|
|
// ============================================================================
|
|
|
|
OnnxModelSession::OnnxModelSession(const std::string& modelPath, const std::string& modelName,
|
|
const OnnxTextConfig& config)
|
|
: pImpl(std::make_unique<Impl>(modelPath, modelName, config)) {
|
|
info_.name = modelName;
|
|
info_.path = modelPath;
|
|
info_.loadTime = std::chrono::system_clock::now();
|
|
|
|
// Get file size for memory estimation
|
|
if (fs::exists(modelPath)) {
|
|
info_.memoryUsageBytes = fs::file_size(modelPath);
|
|
}
|
|
|
|
// Lazy loading: model will be loaded on first inference request
|
|
// This prevents slow session creation during pool initialization
|
|
spdlog::debug("[ONNX] Created session for '{}' - lazy loading (load on first inference)",
|
|
modelName);
|
|
|
|
// Dimension info will be populated on first inference or explicit warm-up
|
|
info_.embeddingDim = 0;
|
|
info_.maxSequenceLength = 0;
|
|
}
|
|
|
|
OnnxModelSession::~OnnxModelSession() = default;
|
|
|
|
Result<std::vector<float>> OnnxModelSession::generateEmbedding(const std::string& text) {
|
|
info_.requestCount++;
|
|
|
|
auto result = pImpl->generateEmbedding(text);
|
|
if (!result) {
|
|
info_.errorCount++;
|
|
return result;
|
|
}
|
|
|
|
// Lazy load dimension info on first successful inference
|
|
if (info_.embeddingDim == 0 && pImpl->isValid()) {
|
|
info_.embeddingDim = pImpl->getEmbeddingDim();
|
|
info_.maxSequenceLength = pImpl->getMaxSequenceLength();
|
|
spdlog::debug("[ONNX] Lazy loaded dimensions for '{}': dim={}, max_seq_len={}", info_.name,
|
|
info_.embeddingDim, info_.maxSequenceLength);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Result<std::vector<std::vector<float>>>
|
|
OnnxModelSession::generateBatchEmbeddings(const std::vector<std::string>& texts) {
|
|
info_.requestCount++;
|
|
|
|
auto result = pImpl->generateBatchEmbeddings(texts);
|
|
|
|
if (!result) {
|
|
info_.errorCount++;
|
|
return result;
|
|
}
|
|
|
|
// Lazy load dimension info on first successful inference
|
|
if (info_.embeddingDim == 0 && pImpl->isValid()) {
|
|
info_.embeddingDim = pImpl->getEmbeddingDim();
|
|
info_.maxSequenceLength = pImpl->getMaxSequenceLength();
|
|
spdlog::debug("[ONNX] Lazy loaded dimensions for '{}' (batch): dim={}, max_seq_len={}",
|
|
info_.name, info_.embeddingDim, info_.maxSequenceLength);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool OnnxModelSession::isValid() const {
|
|
return pImpl->isValid();
|
|
}
|
|
|
|
std::string OnnxModelSession::getExecutionProvider() const {
|
|
return pImpl->getExecutionProvider();
|
|
}
|
|
|
|
size_t OnnxModelSession::getLearnedBatchLimit() const {
|
|
return pImpl->getLearnedBatchLimit();
|
|
}
|
|
|
|
Result<void> OnnxModelSession::setThreading(int intraThreads, int interThreads) {
|
|
return pImpl->setThreading(intraThreads, interThreads);
|
|
}
|
|
|
|
std::pair<int, int> OnnxModelSession::getThreading() const {
|
|
return pImpl->getThreading();
|
|
}
|
|
|
|
// ============================================================================
|
|
// OnnxModelPool Implementation
|
|
// ============================================================================
|
|
|
|
OnnxModelPool::OnnxModelPool(const ModelPoolConfig& config) : config_(config) {
|
|
// In mock mode, avoid initializing any ONNX Runtime state to prevent
|
|
// platform-specific crashes on hosts without compatible runtimes.
|
|
if (std::getenv("YAMS_USE_MOCK_PROVIDER") || std::getenv("YAMS_SKIP_MODEL_LOADING") ||
|
|
std::getenv("YAMS_TEST_MODE")) {
|
|
spdlog::warn("[ONNX] Mock provider mode enabled; skipping global ONNX environment init");
|
|
return;
|
|
}
|
|
|
|
if (config_.asyncLoading) {
|
|
size_t threads = config_.loadWorkerThreads > 0 ? config_.loadWorkerThreads : 1;
|
|
loadPool_ = std::make_unique<boost::asio::thread_pool>(threads);
|
|
spdlog::info("[ONNX] Async model loading enabled (threads={})", threads);
|
|
}
|
|
|
|
// NOTE: We do NOT create a separate Ort::Env here.
|
|
// Instead, OnnxModelSession::Impl uses the global get_global_ort_env() function.
|
|
// Creating multiple Ort::Env instances causes thread pool conflicts that lead to
|
|
// "resource deadlock would occur" errors (EDEADLK, code 36).
|
|
// The ortEnv_ and sessionOptions_ members are kept for API compatibility but not used.
|
|
|
|
try {
|
|
// Log that we're using the shared global environment
|
|
// NOTE: We do NOT create a separate Ort::Env here - OnnxModelSession::Impl
|
|
// uses the global get_global_ort_env() function to avoid thread pool conflicts.
|
|
spdlog::info(
|
|
"[ONNX] Pool: Using shared global Ort::Env (sessions use single-threaded execution)");
|
|
|
|
runtimeGpuEnabled_ = false;
|
|
if (config.enableGPU) {
|
|
const auto& gpu = yams::daemon::resource::detectGpu();
|
|
if (gpu.detected) {
|
|
runtimeGpuEnabled_ = true;
|
|
spdlog::info("[ONNX] GPU enabled: {} ({:.1f} GB VRAM, provider={})", gpu.name,
|
|
static_cast<double>(gpu.vramBytes) / (1024.0 * 1024.0 * 1024.0),
|
|
gpu.provider);
|
|
} else {
|
|
spdlog::info("[ONNX] GPU requested but no GPU detected (will use CPU)");
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
// Do not crash the daemon if runtime init fails; log and continue.
|
|
spdlog::warn("[ONNX] Runtime environment init failed: {} — continuing (mock may be used)",
|
|
e.what());
|
|
} catch (...) {
|
|
spdlog::warn("[ONNX] Runtime environment init failed with unknown error — continuing");
|
|
}
|
|
}
|
|
|
|
OnnxModelPool::~OnnxModelPool() {
|
|
try {
|
|
shutdown();
|
|
} catch (const std::exception& e) {
|
|
try {
|
|
spdlog::warn("[OnnxModelPool] destructor exception: {}", e.what());
|
|
} catch (...) {
|
|
}
|
|
} catch (...) {
|
|
try {
|
|
spdlog::warn("[OnnxModelPool] destructor unknown exception");
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
|
|
Result<void> OnnxModelPool::initialize() {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
if (initialized_.load(std::memory_order_acquire)) {
|
|
return Result<void>();
|
|
}
|
|
|
|
// Mark as startup mode - reduces pool sizes during initial loading
|
|
TuneAdvisor::setOnnxStartupMode(true);
|
|
|
|
spdlog::info("Initializing ONNX model pool with max {} models", config_.maxLoadedModels);
|
|
|
|
const auto& runtimeInfo = yams::onnx_util::OrtRuntimeLoader::instance().ensureLoaded();
|
|
if (!runtimeInfo.available) {
|
|
return Error{ErrorCode::InternalError, runtimeInfo.errorMessage.empty()
|
|
? std::string("ONNX Runtime unavailable")
|
|
: runtimeInfo.errorMessage};
|
|
}
|
|
|
|
// Eagerly initialize the global Ort::Env NOW, before any concurrent access.
|
|
// This prevents "resource deadlock would occur" errors that can happen when
|
|
// multiple threads try to initialize ONNX Runtime simultaneously.
|
|
try {
|
|
spdlog::info("[ONNX Pool] Eagerly initializing global Ort::Env...");
|
|
(void)get_global_ort_env();
|
|
spdlog::info("[ONNX Pool] Global Ort::Env ready");
|
|
} catch (const std::exception& e) {
|
|
spdlog::error("[ONNX Pool] Failed to initialize Ort::Env: {}", e.what());
|
|
return Error{ErrorCode::InternalError, std::string("ONNX Env init failed: ") + e.what()};
|
|
}
|
|
|
|
// Start background preloading if configured (non-blocking)
|
|
if (!config_.lazyLoading && !config_.preloadModels.empty()) {
|
|
// Launch preloading in background thread to avoid blocking
|
|
// Store the thread so we can join it during shutdown
|
|
preloadThread_ = std::thread([this]() {
|
|
try {
|
|
spdlog::info("Starting background model preloading");
|
|
// Check shutdown flag periodically during preload
|
|
for (const auto& modelName : config_.preloadModels) {
|
|
if (shutdown_.load(std::memory_order_acquire)) {
|
|
spdlog::info("Background preloading interrupted by shutdown");
|
|
return;
|
|
}
|
|
auto result = loadModel(modelName);
|
|
if (!result) {
|
|
spdlog::warn("Failed to preload model {}: {}", modelName,
|
|
result.error().message);
|
|
} else {
|
|
// Warm up the model to trigger lazy loading
|
|
// This ensures the first real inference request is fast
|
|
spdlog::info("Warming up preloaded model: {}", modelName);
|
|
auto warmupResult = warmupModel(modelName);
|
|
if (!warmupResult) {
|
|
spdlog::warn("Failed to warm up model {}: {}", modelName,
|
|
warmupResult.error().message);
|
|
}
|
|
}
|
|
}
|
|
// Clear startup mode - normal pool sizing now applies to new models
|
|
TuneAdvisor::setOnnxStartupMode(false);
|
|
spdlog::info("Background model preloading completed");
|
|
} catch (const std::exception& e) {
|
|
try {
|
|
spdlog::error("[ONNX] Background preload crashed: {}", e.what());
|
|
} catch (...) {
|
|
}
|
|
try {
|
|
TuneAdvisor::setOnnxStartupMode(false);
|
|
} catch (...) {
|
|
}
|
|
} catch (...) {
|
|
try {
|
|
spdlog::error("[ONNX] Background preload crashed with unknown exception");
|
|
} catch (...) {
|
|
}
|
|
try {
|
|
TuneAdvisor::setOnnxStartupMode(false);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
// No preload configured - clear startup mode immediately
|
|
TuneAdvisor::setOnnxStartupMode(false);
|
|
}
|
|
|
|
initialized_.store(true, std::memory_order_release);
|
|
return Result<void>();
|
|
}
|
|
|
|
void OnnxModelPool::shutdown() {
|
|
if (shutdown_.exchange(true, std::memory_order_acq_rel)) {
|
|
return;
|
|
}
|
|
|
|
if (preloadThread_.joinable()) {
|
|
try {
|
|
spdlog::info("Waiting for background preload thread to finish...");
|
|
} catch (...) {
|
|
}
|
|
try {
|
|
preloadThread_.join();
|
|
try {
|
|
spdlog::info("Background preload thread joined");
|
|
} catch (...) {
|
|
}
|
|
} catch (const std::exception& e) {
|
|
try {
|
|
spdlog::warn("Failed to join preload thread: {}", e.what());
|
|
} catch (...) {
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
if (loadPool_) {
|
|
loadPool_->join();
|
|
loadPool_.reset();
|
|
}
|
|
|
|
try {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
try {
|
|
spdlog::info("Shutting down ONNX model pool");
|
|
} catch (...) {
|
|
}
|
|
|
|
loadingFutures_.clear();
|
|
|
|
for (auto& [name, entry] : models_) {
|
|
if (entry.pool) {
|
|
try {
|
|
entry.pool->shutdown();
|
|
} catch (const std::exception& e) {
|
|
try {
|
|
spdlog::warn("Failed to shutdown model pool '{}': {}", name, e.what());
|
|
} catch (...) {
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
|
|
models_.clear();
|
|
initialized_.store(false, std::memory_order_release);
|
|
} catch (const std::exception& e) {
|
|
try {
|
|
spdlog::warn("ONNX model pool shutdown exception: {}", e.what());
|
|
} catch (...) {
|
|
}
|
|
} catch (...) {
|
|
try {
|
|
spdlog::warn("ONNX model pool shutdown unknown exception");
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
|
|
Result<OnnxModelPool::ModelHandle> OnnxModelPool::acquireModel(const std::string& modelName,
|
|
std::chrono::milliseconds timeout) {
|
|
if (!initialized_.load(std::memory_order_acquire)) {
|
|
if (auto result = initialize(); !result) {
|
|
return result.error();
|
|
}
|
|
}
|
|
|
|
totalRequests_++;
|
|
|
|
// Check if model is already loaded
|
|
// IMPORTANT: Get pool pointer under lock, then release lock before calling acquire()
|
|
// to avoid deadlock with ResourcePool's internal locking (PBI-1c1 fix)
|
|
std::shared_ptr<ResourcePool<OnnxModelSession>> poolPtr;
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
auto it = models_.find(modelName);
|
|
if (it != models_.end() && it->second.pool) {
|
|
updateAccessStats(modelName);
|
|
cacheHits_++;
|
|
poolPtr = it->second.pool;
|
|
}
|
|
}
|
|
|
|
// Acquire outside of lock to avoid deadlock
|
|
if (poolPtr) {
|
|
return poolPtr->acquire(timeout);
|
|
}
|
|
|
|
cacheMisses_++;
|
|
|
|
// Model not loaded, need to load it
|
|
if (config_.asyncLoading) {
|
|
if (auto result = loadModel(modelName, /*wait=*/false); !result) {
|
|
return result.error();
|
|
}
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
auto it = loadingFutures_.find(modelName);
|
|
if (it != loadingFutures_.end()) {
|
|
if (it->second.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
|
|
return Error{ErrorCode::OperationInProgress, "Model loading in background"};
|
|
}
|
|
const auto loadOk = static_cast<bool>(it->second.get());
|
|
std::optional<Error> loadError;
|
|
if (!loadOk) {
|
|
loadError = it->second.get().error();
|
|
}
|
|
loadingFutures_.erase(it);
|
|
if (loadError) {
|
|
return std::move(*loadError);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (auto result = loadModel(modelName); !result) {
|
|
return result.error();
|
|
}
|
|
}
|
|
|
|
// Try again after loading - get pool pointer while holding lock
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
auto it = models_.find(modelName);
|
|
if (it != models_.end() && it->second.pool) {
|
|
updateAccessStats(modelName);
|
|
poolPtr = it->second.pool;
|
|
}
|
|
}
|
|
|
|
// Acquire outside of lock
|
|
if (poolPtr) {
|
|
return poolPtr->acquire(timeout);
|
|
}
|
|
|
|
return Error{ErrorCode::NotFound, "Failed to load model: " + modelName};
|
|
}
|
|
|
|
Result<void> OnnxModelPool::loadModel(const std::string& modelName, bool wait) {
|
|
if (!config_.asyncLoading || wait) {
|
|
return loadModelSync(modelName);
|
|
}
|
|
return scheduleLoad(modelName);
|
|
}
|
|
|
|
Result<void> OnnxModelPool::scheduleLoad(const std::string& modelName) {
|
|
if (!loadPool_) {
|
|
return loadModelSync(modelName);
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
auto it = models_.find(modelName);
|
|
if (it != models_.end() && it->second.pool) {
|
|
return Result<void>();
|
|
}
|
|
if (loadingFutures_.count(modelName) > 0) {
|
|
return Result<void>();
|
|
}
|
|
|
|
auto prom = std::make_shared<std::promise<Result<void>>>();
|
|
auto fut = prom->get_future().share();
|
|
loadingFutures_[modelName] = fut;
|
|
|
|
boost::asio::post(*loadPool_, [this, modelName, prom]() {
|
|
auto res = loadModelSync(modelName);
|
|
prom->set_value(res);
|
|
});
|
|
}
|
|
|
|
return Result<void>();
|
|
}
|
|
|
|
bool OnnxModelPool::isModelLoaded(const std::string& modelName) const {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
auto it = models_.find(modelName);
|
|
return it != models_.end() && it->second.pool != nullptr;
|
|
}
|
|
|
|
std::vector<std::string> OnnxModelPool::getLoadedModels() const {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
std::vector<std::string> loaded;
|
|
for (const auto& [name, entry] : models_) {
|
|
if (entry.pool) {
|
|
loaded.push_back(name);
|
|
}
|
|
}
|
|
|
|
return loaded;
|
|
}
|
|
|
|
Result<void> OnnxModelPool::loadModelSync(const std::string& modelName) {
|
|
spdlog::info("[ONNX Plugin] loadModel() called for: {}", modelName);
|
|
|
|
// In test environments, avoid heavy model loading entirely
|
|
if (yams::test::shouldSkipModelLoading()) {
|
|
return Error{ErrorCode::NotFound, "Model loading skipped in test mode"};
|
|
}
|
|
|
|
// Single-flight pattern: only one thread loads a given model at a time.
|
|
// Other threads wait for the loading thread to finish.
|
|
{
|
|
std::unique_lock<std::recursive_mutex> lock(mutex_);
|
|
|
|
// Check if already loaded
|
|
auto it = models_.find(modelName);
|
|
if (it != models_.end() && it->second.pool) {
|
|
spdlog::info("[ONNX Plugin] Model already loaded: {}", modelName);
|
|
return Result<void>();
|
|
}
|
|
|
|
// Check if another thread is currently loading this model
|
|
if (loadingModels_.count(modelName) > 0) {
|
|
spdlog::info("[ONNX Plugin] Model '{}' is being loaded by another thread, waiting...",
|
|
modelName);
|
|
// Wait for the other thread to finish loading
|
|
loadingCv_.wait(lock, [this, &modelName]() {
|
|
// Wake up when either: model is loaded, or no longer in loading set
|
|
auto it = models_.find(modelName);
|
|
bool loaded = (it != models_.end() && it->second.pool);
|
|
bool notLoading = (loadingModels_.count(modelName) == 0);
|
|
return loaded || notLoading;
|
|
});
|
|
|
|
// Check if model was loaded successfully by the other thread
|
|
it = models_.find(modelName);
|
|
if (it != models_.end() && it->second.pool) {
|
|
spdlog::info("[ONNX Plugin] Model '{}' loaded by another thread", modelName);
|
|
return Result<void>();
|
|
}
|
|
// If not loaded and not loading, we'll try to load it ourselves
|
|
spdlog::warn("[ONNX Plugin] Model '{}' load failed by other thread, retrying",
|
|
modelName);
|
|
}
|
|
|
|
// Mark this model as being loaded by us
|
|
loadingModels_.insert(modelName);
|
|
}
|
|
|
|
// RAII guard to remove from loadingModels_ and notify waiters
|
|
struct LoadingGuard {
|
|
OnnxModelPool* pool;
|
|
std::string name;
|
|
~LoadingGuard() {
|
|
std::lock_guard<std::recursive_mutex> lock(pool->mutex_);
|
|
pool->loadingModels_.erase(name);
|
|
pool->loadingCv_.notify_all();
|
|
}
|
|
} guard{this, modelName};
|
|
|
|
// Resolve model path WITHOUT holding the lock
|
|
std::string modelPath = resolveModelPath(modelName);
|
|
spdlog::info("[ONNX Plugin] Resolved path: {}", modelPath);
|
|
|
|
// Direct filesystem check
|
|
spdlog::info("[ONNX Plugin] Checking file existence: {}", modelPath);
|
|
bool fileExists = false;
|
|
try {
|
|
fileExists = fs::exists(modelPath) && fs::is_regular_file(modelPath);
|
|
} catch (const std::exception& e) {
|
|
spdlog::warn("Error checking model file existence: {}", e.what());
|
|
}
|
|
|
|
if (!fileExists) {
|
|
spdlog::info("Model file not found: {} (searched for: {})", modelName, modelPath);
|
|
const bool offline = []() {
|
|
if (const char* s = std::getenv("YAMS_ONNX_OFFLINE")) {
|
|
std::string v(s);
|
|
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
|
|
return v == "1" || v == "true" || v == "yes" || v == "on";
|
|
}
|
|
return false;
|
|
}();
|
|
if (offline) {
|
|
return Error{ErrorCode::NotFound,
|
|
"Model file not found in offline mode. Pre-place artifacts in cache or "
|
|
"~/.yams/models, or unset YAMS_ONNX_OFFLINE"};
|
|
}
|
|
spdlog::info("Download the model with: yams model --download {}", modelName);
|
|
return Error{ErrorCode::NotFound,
|
|
"Model file not found: " + modelPath +
|
|
". Download with: yams model --download " + modelName +
|
|
" or set YAMS_ALLOW_MODEL_DOWNLOAD=1 for automatic downloads"};
|
|
}
|
|
|
|
// Check file size
|
|
size_t modelSize = 0;
|
|
try {
|
|
modelSize = fs::file_size(modelPath);
|
|
} catch (const std::exception& e) {
|
|
spdlog::warn("Error getting model file size: {}", e.what());
|
|
modelSize = 100 * 1024 * 1024; // Assume 100MB if can't determine
|
|
}
|
|
|
|
// Configure pool for this model (no lock needed - local variables)
|
|
// Use TuneAdvisor for GPU-aware pool sizing:
|
|
// GPU mode: larger pool (GPU handles inference, more sessions for throughput)
|
|
// CPU mode: smaller pool (avoid CPU saturation from parallel inference)
|
|
const bool gpuEnabled = runtimeGpuEnabled_ && !OnnxModelSession::isCoreMLDenylisted(modelName);
|
|
const size_t embedConcurrencyCap =
|
|
std::max<size_t>(1u, std::min<size_t>(TuneAdvisor::postEmbedConcurrent(),
|
|
TuneAdvisor::getEmbedMaxConcurrency()));
|
|
const size_t dynamic_max = std::max<size_t>(
|
|
1u, std::min<size_t>(TuneAdvisor::onnxSessionsPerModel(gpuEnabled), embedConcurrencyCap));
|
|
const size_t dynamic_min = std::min(size_t{2}, dynamic_max);
|
|
|
|
spdlog::info("[ONNX Plugin] Configuring resource pool (gpu={}, pool_max={}, embed_cap={})",
|
|
gpuEnabled ? "enabled" : "disabled", dynamic_max, embedConcurrencyCap);
|
|
PoolConfig<OnnxModelSession> poolConfig;
|
|
poolConfig.minSize = dynamic_min;
|
|
poolConfig.maxSize = dynamic_max;
|
|
// Keep maxIdle = maxSize to prevent session churn. With maxIdle < maxSize,
|
|
// excess sessions would be destroyed on return and recreated on next acquire.
|
|
poolConfig.maxIdle = dynamic_max;
|
|
poolConfig.idleTimeout = config_.modelIdleTimeout;
|
|
// CRITICAL: Disable preCreateResources to avoid creating ONNX session during
|
|
// ResourcePool construction. Sessions will be created lazily on first acquire().
|
|
// This prevents EDEADLK errors from ONNX Runtime's internal thread pool.
|
|
poolConfig.preCreateResources = false;
|
|
|
|
// Create embedding config
|
|
OnnxTextConfig embConfig;
|
|
embConfig.model_path = modelPath;
|
|
embConfig.model_name = modelName;
|
|
embConfig.enable_gpu = gpuEnabled;
|
|
embConfig.num_threads = config_.numThreads;
|
|
// Leave max_sequence_length unset so session/model metadata can determine the native limit.
|
|
// The OnnxTextConfig default of 512 would otherwise silently clamp models like Gemma.
|
|
embConfig.max_sequence_length = 0;
|
|
|
|
// Create ResourcePool WITHOUT holding mutex_ - the factory lambda will be called
|
|
// later during acquire(), also without holding mutex_.
|
|
spdlog::debug("[ONNX Plugin] Creating ResourcePool for '{}'", modelName);
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
|
|
std::shared_ptr<ResourcePool<OnnxModelSession>> pool;
|
|
try {
|
|
spdlog::debug("[ONNX Plugin] About to construct ResourcePool...");
|
|
pool = std::make_shared<ResourcePool<OnnxModelSession>>(
|
|
poolConfig,
|
|
[modelPath, modelName, embConfig](const std::string&) -> Result<ModelSessionPtr> {
|
|
try {
|
|
spdlog::debug("[ONNX Plugin] Factory: creating OnnxModelSession for '{}'",
|
|
modelName);
|
|
return std::make_shared<OnnxModelSession>(modelPath, modelName, embConfig);
|
|
} catch (const std::exception& e) {
|
|
spdlog::error("[ONNX Plugin] Factory: failed to create session: {}", e.what());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("Failed to create model session: ") + e.what()};
|
|
} catch (...) {
|
|
spdlog::error("[ONNX Plugin] Factory: unknown error creating session");
|
|
return Error{ErrorCode::InternalError, "Unknown error creating ONNX session"};
|
|
}
|
|
},
|
|
[](const OnnxModelSession& session) { return session.isValid(); });
|
|
spdlog::debug("[ONNX Plugin] ResourcePool constructed successfully");
|
|
} catch (const std::system_error& e) {
|
|
spdlog::error("[ONNX Plugin] ResourcePool construction threw system_error: {} (code={})",
|
|
e.what(), e.code().value());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("ResourcePool construction failed: ") + e.what()};
|
|
} catch (const std::exception& e) {
|
|
spdlog::error("[ONNX Plugin] ResourcePool construction threw exception: {}", e.what());
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("ResourcePool construction failed: ") + e.what()};
|
|
}
|
|
|
|
// Now acquire lock briefly to update shared state
|
|
spdlog::debug("[ONNX Plugin] Acquiring mutex for model registration...");
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
// Check if we need to evict models
|
|
size_t loadedCount = 0;
|
|
for (const auto& [name, entry] : models_) {
|
|
if (entry.pool)
|
|
loadedCount++;
|
|
}
|
|
|
|
if (loadedCount >= config_.maxLoadedModels) {
|
|
evictLRU(1);
|
|
}
|
|
|
|
// Check memory constraints
|
|
if (!canLoadModel(modelSize)) {
|
|
return Error{ErrorCode::ResourceExhausted, "Insufficient memory to load model"};
|
|
}
|
|
|
|
// Create/update model entry
|
|
spdlog::info("[ONNX Plugin] Registering model entry for: {}", modelName);
|
|
ModelEntry& entry = models_[modelName];
|
|
entry.name = modelName;
|
|
entry.path = modelPath;
|
|
entry.lastAccess = std::chrono::steady_clock::now();
|
|
entry.pool = std::move(pool);
|
|
|
|
// Update cached model count for non-blocking status queries
|
|
loadedModelCount_.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
auto t1 = std::chrono::steady_clock::now();
|
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
|
|
spdlog::info("Registered ONNX model: {} ({}MB) load_ms={} path={}", modelName,
|
|
modelSize / (1024 * 1024), ms, modelPath);
|
|
|
|
spdlog::info("[ONNX Plugin] Model '{}' registered successfully (session created on first use)",
|
|
modelName);
|
|
return Result<void>();
|
|
}
|
|
|
|
Result<void> OnnxModelPool::unloadModel(const std::string& modelName) {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
auto it = models_.find(modelName);
|
|
if (it == models_.end()) {
|
|
return Error{ErrorCode::NotFound, "Model not found: " + modelName};
|
|
}
|
|
|
|
if (it->second.isHot) {
|
|
return Error{ErrorCode::InvalidOperation, "Cannot unload hot model: " + modelName};
|
|
}
|
|
|
|
if (it->second.pool) {
|
|
auto stats = it->second.pool->getStats();
|
|
if (stats.inUseResources > 0) {
|
|
return Error{ErrorCode::OperationInProgress,
|
|
"Cannot unload model with in-use sessions: " + modelName};
|
|
}
|
|
it->second.pool->shutdown();
|
|
it->second.pool.reset();
|
|
|
|
// Update cached model count for non-blocking status queries
|
|
loadedModelCount_.fetch_sub(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
spdlog::info("Unloaded ONNX model: {}", modelName);
|
|
|
|
return Result<void>();
|
|
}
|
|
|
|
Result<void> OnnxModelPool::preloadModels() {
|
|
for (const auto& modelName : config_.preloadModels) {
|
|
spdlog::info("Preloading model: {}", modelName);
|
|
|
|
// Load model directly - no async wrapper needed
|
|
auto result = loadModel(modelName);
|
|
if (!result) {
|
|
spdlog::warn("Failed to preload model {}: {}", modelName, result.error().message);
|
|
} else {
|
|
// Warm up the model to trigger lazy loading
|
|
spdlog::info("Warming up preloaded model: {}", modelName);
|
|
auto warmupResult = warmupModel(modelName);
|
|
if (!warmupResult) {
|
|
spdlog::warn("Failed to warm up model {}: {}", modelName,
|
|
warmupResult.error().message);
|
|
}
|
|
|
|
// Mark as hot model
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
auto it = models_.find(modelName);
|
|
if (it != models_.end()) {
|
|
it->second.isHot = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result<void>();
|
|
}
|
|
|
|
Result<void> OnnxModelPool::warmupModel(const std::string& modelName) {
|
|
spdlog::info("[ONNX Plugin] Warming up model: {}", modelName);
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
|
|
// Acquire a session from the pool
|
|
auto handleResult = acquireModel(modelName, std::chrono::seconds(30));
|
|
if (!handleResult) {
|
|
spdlog::error("[ONNX Plugin] Failed to acquire session for warmup: {}",
|
|
handleResult.error().message);
|
|
return handleResult.error();
|
|
}
|
|
|
|
auto handle = std::move(handleResult.value());
|
|
|
|
// Do a batch inference to trigger lazy loading and warm the batch code path
|
|
std::vector<std::string> warmupTexts = {"warmup text one", "warmup text two",
|
|
"warmup text three", "warmup text four"};
|
|
auto embedResult = handle->generateBatchEmbeddings(warmupTexts);
|
|
if (!embedResult) {
|
|
spdlog::error("[ONNX Plugin] Warmup inference failed for {}: {}", modelName,
|
|
embedResult.error().message);
|
|
return embedResult.error();
|
|
}
|
|
|
|
auto t1 = std::chrono::steady_clock::now();
|
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
|
|
spdlog::info("[ONNX Plugin] Model '{}' warmed up in {}ms (batch={}, embedding dim={})",
|
|
modelName, ms, warmupTexts.size(),
|
|
embedResult.value().empty() ? 0 : embedResult.value()[0].size());
|
|
|
|
// Auto-tune batch size if not explicitly set and auto-tuning not disabled
|
|
auto autotuneDisabled = []() {
|
|
if (const char* s = std::getenv("YAMS_EMBED_AUTOTUNE")) {
|
|
std::string v(s);
|
|
std::transform(v.begin(), v.end(), v.begin(), ::tolower);
|
|
return v == "0" || v == "false" || v == "no" || v == "off";
|
|
}
|
|
return false;
|
|
};
|
|
if (TuneAdvisor::getEmbedDocCap() == 0 && !autotuneDisabled()) {
|
|
spdlog::info("[ONNX Plugin] Auto-tuning batch size for '{}'...", modelName);
|
|
|
|
std::vector<std::string> probeTexts;
|
|
probeTexts.reserve(64);
|
|
for (uint32_t i = 0; i < 64; ++i)
|
|
probeTexts.push_back("probe text number " + std::to_string(i) + " for auto-tuning");
|
|
|
|
std::vector<uint32_t> candidates = {4, 8, 16, 32, 64};
|
|
double bestTps = 0.0;
|
|
uint32_t bestBatch = 16;
|
|
|
|
for (uint32_t bs : candidates) {
|
|
std::vector<std::string> batch(probeTexts.begin(),
|
|
probeTexts.begin() + static_cast<ptrdiff_t>(bs));
|
|
auto pt0 = std::chrono::steady_clock::now();
|
|
auto r = handle->generateBatchEmbeddings(batch);
|
|
auto elapsed = std::chrono::steady_clock::now() - pt0;
|
|
if (!r)
|
|
break;
|
|
double probeMs =
|
|
std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count() / 1000.0;
|
|
double tps = static_cast<double>(bs) / (probeMs / 1000.0);
|
|
spdlog::debug("[ONNX Plugin] Autotune probe: batch={} {:.1f}ms ({:.1f} texts/sec)", bs,
|
|
probeMs, tps);
|
|
if (tps > bestTps) {
|
|
bestTps = tps;
|
|
bestBatch = bs;
|
|
}
|
|
}
|
|
|
|
TuneAdvisor::setEmbedDocCap(bestBatch);
|
|
spdlog::info("[ONNX Plugin] Auto-tuned batch size: {} ({:.1f} texts/sec)", bestBatch,
|
|
bestTps);
|
|
}
|
|
|
|
// Pre-warm additional sessions to avoid cold-start penalty on concurrent requests.
|
|
// GPU mode: up to 3 sessions (GPU provider init is expensive).
|
|
// CPU mode: up to 2 sessions (eliminates ~1788ms cold start for second request).
|
|
// The initial handle is still held; additional sessions are acquired and immediately
|
|
// released back to the pool so they are pre-initialized for future use.
|
|
{
|
|
const std::string activeProvider = handle->getExecutionProvider();
|
|
const bool providerGpuEnabled = isGpuExecutionProviderName(activeProvider);
|
|
const bool cpuFallbackPinned = !providerGpuEnabled && runtimeGpuEnabled_ &&
|
|
OnnxModelSession::isCoreMLDenylisted(modelName);
|
|
|
|
size_t targetWarm =
|
|
std::min(static_cast<size_t>(TuneAdvisor::onnxSessionsPerModel(providerGpuEnabled)),
|
|
providerGpuEnabled ? size_t{3} : size_t{2});
|
|
if (cpuFallbackPinned) {
|
|
targetWarm = std::min<size_t>(targetWarm, 1u);
|
|
}
|
|
|
|
// Keep warm session count aligned with current embedding throughput budget
|
|
// to avoid retaining idle ONNX sessions under low-concurrency settings.
|
|
const size_t embedConcurrencyCap =
|
|
std::max<size_t>(1u, std::min<size_t>(TuneAdvisor::postEmbedConcurrent(),
|
|
TuneAdvisor::getEmbedMaxConcurrency()));
|
|
targetWarm = std::min(targetWarm, embedConcurrencyCap);
|
|
|
|
// Optional explicit cap for test/benchmark scenarios.
|
|
if (const char* s = std::getenv("YAMS_ONNX_PREWARM_SESSIONS")) {
|
|
try {
|
|
size_t forced = static_cast<size_t>(std::stoull(s));
|
|
if (forced >= 1 && forced <= 8) {
|
|
targetWarm = std::min(targetWarm, forced);
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
for (size_t i = 1; i < targetWarm; ++i) {
|
|
auto extra = acquireModel(modelName, std::chrono::seconds(60));
|
|
if (!extra) {
|
|
spdlog::debug("[ONNX Plugin] Pre-warm: could not acquire session {} for {}", i + 1,
|
|
modelName);
|
|
break;
|
|
}
|
|
// Session returned to pool when extra goes out of scope
|
|
}
|
|
if (targetWarm > 1) {
|
|
spdlog::info("[ONNX Plugin] Pre-warmed {} sessions for '{}' (provider={} gpu={})",
|
|
targetWarm, modelName, activeProvider, providerGpuEnabled ? "yes" : "no");
|
|
}
|
|
}
|
|
|
|
return Result<void>();
|
|
}
|
|
|
|
Result<void> OnnxModelPool::setModelHot(const std::string& modelName, bool hot) {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
auto it = models_.find(modelName);
|
|
if (it == models_.end()) {
|
|
return Error{ErrorCode::NotFound, "Model not found: " + modelName};
|
|
}
|
|
it->second.isHot = hot;
|
|
spdlog::info("[ONNX Plugin] Model '{}' hot={} ", modelName, hot ? "true" : "false");
|
|
return Result<void>();
|
|
}
|
|
|
|
Result<void> OnnxModelPool::setModelThreading(const std::string& modelName, int intraThreads,
|
|
int interThreads, bool applyNow) {
|
|
auto valid = [](int value) { return value == -1 || (value >= 1 && value <= 64); };
|
|
if (modelName.empty() || !valid(intraThreads) || !valid(interThreads)) {
|
|
return Error{ErrorCode::InvalidArgument,
|
|
"setModelThreading requires model name and valid thread values"};
|
|
}
|
|
|
|
std::shared_ptr<ResourcePool<OnnxModelSession>> pool;
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
threadingOverrides_[modelName] = {intraThreads, interThreads};
|
|
auto it = models_.find(modelName);
|
|
if (it != models_.end()) {
|
|
pool = it->second.pool;
|
|
}
|
|
}
|
|
|
|
if (!applyNow || !pool) {
|
|
return Result<void>();
|
|
}
|
|
|
|
if (pool->getStats().inUseResources > 0) {
|
|
return Error{ErrorCode::ResourceBusy,
|
|
"Model is currently in use; threading override will apply on next reload"};
|
|
}
|
|
|
|
auto handle = pool->acquire(std::chrono::milliseconds(500));
|
|
if (!handle) {
|
|
return Error{ErrorCode::Timeout,
|
|
"Timed out acquiring model session to apply threading override"};
|
|
}
|
|
|
|
return handle.value()->setThreading(intraThreads, interThreads);
|
|
}
|
|
|
|
void OnnxModelPool::evictLRU(size_t numToEvict) {
|
|
if (runtimeGpuEnabled_ && !allowGpuAutoUnload()) {
|
|
spdlog::debug(
|
|
"Skipping LRU model eviction on GPU path (set YAMS_ONNX_GPU_AUTO_UNLOAD=1 to enable)");
|
|
return;
|
|
}
|
|
|
|
// Find least recently used models that are not hot
|
|
std::vector<std::pair<std::string, std::chrono::steady_clock::time_point>> candidates;
|
|
|
|
for (const auto& [name, entry] : models_) {
|
|
if (entry.pool && !entry.isHot) {
|
|
candidates.emplace_back(name, entry.lastAccess);
|
|
}
|
|
}
|
|
|
|
// Sort by last access time
|
|
std::sort(candidates.begin(), candidates.end(),
|
|
[](const auto& a, const auto& b) { return a.second < b.second; });
|
|
|
|
// Evict the oldest models
|
|
for (size_t i = 0; i < std::min(numToEvict, candidates.size()); ++i) {
|
|
const auto& modelName = candidates[i].first;
|
|
auto it = models_.find(modelName);
|
|
if (it != models_.end() && it->second.pool) {
|
|
auto stats = it->second.pool->getStats();
|
|
if (stats.inUseResources > 0) {
|
|
spdlog::debug("Skipping LRU eviction for model '{}' (in_use={})", modelName,
|
|
stats.inUseResources);
|
|
continue;
|
|
}
|
|
spdlog::info("Evicting model due to LRU: {}", modelName);
|
|
it->second.pool->shutdown();
|
|
it->second.pool.reset();
|
|
const size_t count = loadedModelCount_.load(std::memory_order_relaxed);
|
|
if (count > 0) {
|
|
loadedModelCount_.fetch_sub(1, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
OnnxModelPool::PoolStats OnnxModelPool::getStats() const {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
PoolStats stats;
|
|
stats.totalRequests = totalRequests_.load();
|
|
stats.cacheHits = cacheHits_.load();
|
|
stats.cacheMisses = cacheMisses_.load();
|
|
stats.hitRate =
|
|
stats.totalRequests > 0 ? static_cast<double>(stats.cacheHits) / stats.totalRequests : 0.0;
|
|
|
|
stats.loadedModels = 0;
|
|
stats.totalMemoryBytes = 0;
|
|
|
|
for (const auto& [name, entry] : models_) {
|
|
if (entry.pool) {
|
|
stats.loadedModels++;
|
|
|
|
// Get model info (approximate from first session in pool)
|
|
// Note: This is simplified - in production, track actual memory usage
|
|
if (fs::exists(entry.path)) {
|
|
size_t modelSize = fs::file_size(entry.path);
|
|
stats.totalMemoryBytes += modelSize;
|
|
}
|
|
|
|
// Populate per-model stats for status reporting (copyable ModelInfo)
|
|
ModelInfo mi;
|
|
mi.name = name;
|
|
mi.path = entry.path;
|
|
mi.embeddingDim = 384; // default when unknown
|
|
mi.maxSequenceLength = 512; // default when unknown
|
|
try {
|
|
if (fs::exists(entry.path)) {
|
|
mi.memoryUsageBytes = fs::file_size(entry.path);
|
|
}
|
|
} catch (...) {
|
|
mi.memoryUsageBytes = 0;
|
|
}
|
|
mi.loadTime = std::chrono::system_clock::now();
|
|
mi.requestCount = 0;
|
|
mi.errorCount = 0;
|
|
stats.modelStats.emplace(name, std::move(mi));
|
|
}
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
size_t OnnxModelPool::getMemoryUsage() const {
|
|
return getStats().totalMemoryBytes;
|
|
}
|
|
|
|
void OnnxModelPool::performMaintenance() {
|
|
if (runtimeGpuEnabled_ && !allowGpuAutoUnload()) {
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
for (auto& [name, entry] : models_) {
|
|
if (!entry.pool) {
|
|
continue;
|
|
}
|
|
|
|
auto stats = entry.pool->getStats();
|
|
if (stats.inUseResources > 0) {
|
|
continue;
|
|
}
|
|
|
|
if (entry.isHot) {
|
|
// Keep the model loaded, but still evict expired idle sessions from its pool.
|
|
entry.pool->evictExpired();
|
|
continue;
|
|
}
|
|
|
|
// Check if model has been idle too long
|
|
auto idleTime = now - entry.lastAccess;
|
|
if (idleTime > config_.modelIdleTimeout) {
|
|
spdlog::info("Unloading idle model: {}", name);
|
|
entry.pool->shutdown();
|
|
entry.pool.reset();
|
|
const size_t count = loadedModelCount_.load(std::memory_order_relaxed);
|
|
if (count > 0) {
|
|
loadedModelCount_.fetch_sub(1, std::memory_order_relaxed);
|
|
}
|
|
} else if (entry.pool) {
|
|
// Clean up expired resources in the pool
|
|
entry.pool->evictExpired();
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef YAMS_TESTING
|
|
std::optional<OnnxModelPool::TestingModelPoolStats>
|
|
OnnxModelPool::testingModelPoolStats(const std::string& modelName) const {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
|
|
auto it = models_.find(modelName);
|
|
if (it == models_.end()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
TestingModelPoolStats out;
|
|
out.loaded = (it->second.pool != nullptr);
|
|
out.isHot = it->second.isHot;
|
|
|
|
if (it->second.pool) {
|
|
auto stats = it->second.pool->getStats();
|
|
out.totalResources = stats.totalResources;
|
|
out.availableResources = stats.availableResources;
|
|
out.inUseResources = stats.inUseResources;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
#endif
|
|
|
|
std::string OnnxModelPool::resolveModelPath(const std::string& modelName) const {
|
|
auto is_hf_like = [](const std::string& s) {
|
|
if (s.rfind("hf://", 0) == 0)
|
|
return true;
|
|
if (s.rfind("https://huggingface.co/", 0) == 0)
|
|
return true;
|
|
return (s.find('/') != std::string::npos) && (s.find('/') == s.rfind('/')) &&
|
|
s.find(".onnx") == std::string::npos && s.find('/') > 0;
|
|
};
|
|
auto to_repo_id = [](const std::string& s) {
|
|
if (s.rfind("hf://", 0) == 0)
|
|
return s.substr(5);
|
|
const std::string prefix = "https://huggingface.co/";
|
|
if (s.rfind(prefix, 0) == 0) {
|
|
auto rest = s.substr(prefix.size());
|
|
auto slash = rest.find('/');
|
|
if (slash != std::string::npos) {
|
|
auto slash2 = rest.find('/', slash + 1);
|
|
if (slash2 != std::string::npos)
|
|
return rest.substr(0, slash2);
|
|
}
|
|
return rest;
|
|
}
|
|
return s;
|
|
};
|
|
// Get home directory (cross-platform)
|
|
std::string homeDir;
|
|
if (const char* home = std::getenv("HOME")) {
|
|
homeDir = home;
|
|
} else if (const char* userprofile = std::getenv("USERPROFILE")) {
|
|
// Windows: USERPROFILE is the user's home directory
|
|
homeDir = userprofile;
|
|
}
|
|
|
|
// Windows-specific paths
|
|
std::string localAppData;
|
|
std::string appData;
|
|
#ifdef _WIN32
|
|
if (const char* lad = std::getenv("LOCALAPPDATA")) {
|
|
localAppData = lad;
|
|
}
|
|
if (const char* ad = std::getenv("APPDATA")) {
|
|
appData = ad;
|
|
}
|
|
#endif
|
|
|
|
// Build search paths with priority:
|
|
// 1) Treat modelName as a full path
|
|
// 2) Configured modelsRoot (if set), with common filename layouts
|
|
// 3) User models (~/.yams/models), local models/, system share models
|
|
std::vector<std::string> searchPaths;
|
|
searchPaths.push_back(modelName);
|
|
|
|
if (is_hf_like(modelName)) {
|
|
std::string repo = to_repo_id(modelName);
|
|
// Check for revision hint
|
|
std::string preferredRev;
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lk(mutex_);
|
|
auto it = modelHints_.find(modelName);
|
|
if (it != modelHints_.end())
|
|
preferredRev = it->second.hfRevision;
|
|
}
|
|
auto cache_roots = [&]() {
|
|
std::vector<std::string> roots;
|
|
if (const char* env = std::getenv("YAMS_ONNX_HF_CACHE"))
|
|
roots.push_back(env);
|
|
if (const char* hf = std::getenv("HF_HOME")) {
|
|
roots.push_back(std::string(hf));
|
|
roots.push_back(std::string(hf) + "/hub");
|
|
}
|
|
if (!homeDir.empty()) {
|
|
roots.push_back(homeDir + "/.cache/huggingface");
|
|
roots.push_back(homeDir + "/.cache/huggingface/hub");
|
|
}
|
|
return roots;
|
|
}();
|
|
std::string repo_dir = "models--";
|
|
for (auto c : repo)
|
|
repo_dir += (c == '/' ? std::string("--") : std::string(1, c));
|
|
for (const auto& root : cache_roots) {
|
|
try {
|
|
fs::path base = fs::path(root) / repo_dir;
|
|
if (!fs::exists(base))
|
|
continue;
|
|
fs::path found;
|
|
if (!preferredRev.empty()) {
|
|
fs::path snapdir;
|
|
fs::path ref = base / "refs" / preferredRev;
|
|
if (fs::exists(ref)) {
|
|
try {
|
|
std::ifstream in(ref);
|
|
std::string s;
|
|
std::getline(in, s);
|
|
if (!s.empty())
|
|
snapdir = base / "snapshots" / s;
|
|
} catch (...) {
|
|
}
|
|
}
|
|
if (snapdir.empty())
|
|
snapdir = base / "snapshots" / preferredRev;
|
|
if (fs::exists(snapdir)) {
|
|
for (auto& p : fs::recursive_directory_iterator(snapdir)) {
|
|
if (!p.is_regular_file())
|
|
continue;
|
|
if (p.path().filename() == "model.onnx") {
|
|
found = p.path();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (found.empty()) {
|
|
for (auto& p : fs::recursive_directory_iterator(base)) {
|
|
if (!p.is_regular_file())
|
|
continue;
|
|
if (p.path().filename() == "model.onnx") {
|
|
found = p.path();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!found.empty()) {
|
|
spdlog::debug("[ONNX] Resolved HF {} to {}", repo, found.string());
|
|
return found.string();
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
spdlog::debug("[ONNX] HF repo '{}' not found in cache; falling back to filesystem search",
|
|
repo);
|
|
}
|
|
|
|
// Expand configured modelsRoot if present (supports '~')
|
|
if (!config_.modelsRoot.empty()) {
|
|
std::string root = config_.modelsRoot;
|
|
if (!root.empty() && root[0] == '~') {
|
|
if (!homeDir.empty()) {
|
|
root = homeDir + root.substr(1);
|
|
}
|
|
}
|
|
// Common layouts under configured root
|
|
searchPaths.push_back(root + "/" + modelName + "/model.onnx");
|
|
searchPaths.push_back(root + "/" + modelName + "/" + modelName + ".onnx");
|
|
searchPaths.push_back(root + "/" + modelName + ".onnx");
|
|
}
|
|
|
|
// Default locations (XDG standard first, then fallbacks)
|
|
// Priority: XDG_DATA_HOME > ~/.local/share/yams > ~/.yams > relative > system
|
|
std::string xdg_data_home;
|
|
if (const char* xdg = std::getenv("XDG_DATA_HOME")) {
|
|
xdg_data_home = xdg;
|
|
} else if (!homeDir.empty()) {
|
|
xdg_data_home = homeDir + "/.local/share";
|
|
}
|
|
|
|
// XDG paths (highest priority)
|
|
if (!xdg_data_home.empty()) {
|
|
searchPaths.push_back(xdg_data_home + "/yams/models/" + modelName + "/model.onnx");
|
|
searchPaths.push_back(xdg_data_home + "/yams/models/" + modelName + "/" + modelName +
|
|
".onnx");
|
|
searchPaths.push_back(xdg_data_home + "/yams/models/" + modelName + ".onnx");
|
|
}
|
|
|
|
// Legacy ~/.yams/models (for backward compatibility)
|
|
if (!homeDir.empty()) {
|
|
searchPaths.push_back(homeDir + "/.yams/models/" + modelName + "/model.onnx");
|
|
searchPaths.push_back(homeDir + "/.yams/models/" + modelName + "/" + modelName + ".onnx");
|
|
searchPaths.push_back(homeDir + "/.yams/models/" + modelName + ".onnx");
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
// Windows: Check LOCALAPPDATA/yams/models (primary Windows location)
|
|
if (!localAppData.empty()) {
|
|
searchPaths.push_back(localAppData + "\\yams\\models\\" + modelName + "\\model.onnx");
|
|
searchPaths.push_back(localAppData + "\\yams\\models\\" + modelName + "\\" + modelName +
|
|
".onnx");
|
|
searchPaths.push_back(localAppData + "\\yams\\models\\" + modelName + ".onnx");
|
|
}
|
|
// Windows: Check APPDATA/yams/models (roaming profile)
|
|
if (!appData.empty()) {
|
|
searchPaths.push_back(appData + "\\yams\\models\\" + modelName + "\\model.onnx");
|
|
searchPaths.push_back(appData + "\\yams\\models\\" + modelName + "\\" + modelName +
|
|
".onnx");
|
|
searchPaths.push_back(appData + "\\yams\\models\\" + modelName + ".onnx");
|
|
}
|
|
#endif
|
|
|
|
// Relative paths
|
|
searchPaths.push_back("models/" + modelName + "/model.onnx");
|
|
searchPaths.push_back("models/" + modelName + "/" + modelName + ".onnx");
|
|
searchPaths.push_back("models/" + modelName + ".onnx");
|
|
|
|
#ifdef _WIN32
|
|
// Windows: System paths
|
|
searchPaths.push_back("C:\\ProgramData\\yams\\models\\" + modelName + "\\model.onnx");
|
|
searchPaths.push_back("C:\\ProgramData\\yams\\models\\" + modelName + "\\" + modelName +
|
|
".onnx");
|
|
searchPaths.push_back("C:\\ProgramData\\yams\\models\\" + modelName + ".onnx");
|
|
#else
|
|
// Unix: System paths
|
|
searchPaths.push_back("/usr/local/share/yams/models/" + modelName + "/model.onnx");
|
|
searchPaths.push_back("/usr/local/share/yams/models/" + modelName + "/" + modelName + ".onnx");
|
|
searchPaths.push_back("/usr/local/share/yams/models/" + modelName + ".onnx");
|
|
#endif
|
|
|
|
// Log search paths on first attempt for debugging
|
|
spdlog::info("[ONNX Plugin] Resolving model path for '{}', searching {} locations", modelName,
|
|
searchPaths.size());
|
|
#ifdef _WIN32
|
|
spdlog::debug("[ONNX Plugin] Windows paths: LOCALAPPDATA={}, APPDATA={}, HOME={}", localAppData,
|
|
appData, homeDir);
|
|
#endif
|
|
|
|
// Direct filesystem checks - no async, no timeouts needed for local files
|
|
for (const auto& path : searchPaths) {
|
|
if (path.empty())
|
|
continue;
|
|
|
|
try {
|
|
if (fs::exists(path)) {
|
|
spdlog::info("[ONNX Plugin] Found model at: {}", path);
|
|
return path;
|
|
}
|
|
} catch (const std::exception& e) {
|
|
spdlog::debug("[ONNX Plugin] Error checking path {}: {}", path, e.what());
|
|
}
|
|
}
|
|
|
|
spdlog::warn("[ONNX Plugin] Model '{}' not found in any search path. Searched:", modelName);
|
|
for (const auto& path : searchPaths) {
|
|
if (!path.empty()) {
|
|
spdlog::warn("[ONNX Plugin] - {}", path);
|
|
}
|
|
}
|
|
// Default to models directory
|
|
return "models/" + modelName + ".onnx";
|
|
}
|
|
|
|
Result<OnnxModelPool::ModelSessionPtr>
|
|
OnnxModelPool::createModelSession(const std::string& modelName) {
|
|
std::string modelPath = resolveModelPath(modelName);
|
|
|
|
OnnxTextConfig config;
|
|
config.model_path = modelPath;
|
|
config.model_name = modelName;
|
|
config.enable_gpu = config_.enableGPU;
|
|
config.num_threads = config_.numThreads;
|
|
config.max_sequence_length = 0;
|
|
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
auto it = threadingOverrides_.find(modelName);
|
|
if (it != threadingOverrides_.end()) {
|
|
config.num_threads = it->second.first;
|
|
config.inter_op_threads = it->second.second;
|
|
}
|
|
}
|
|
|
|
try {
|
|
auto session = std::make_shared<OnnxModelSession>(modelPath, modelName, config);
|
|
return session;
|
|
} catch (const std::exception& e) {
|
|
return Error{ErrorCode::InternalError,
|
|
std::string("Failed to create model session: ") + e.what()};
|
|
}
|
|
}
|
|
|
|
void OnnxModelPool::updateAccessStats(const std::string& modelName) {
|
|
auto it = models_.find(modelName);
|
|
if (it != models_.end()) {
|
|
it->second.lastAccess = std::chrono::steady_clock::now();
|
|
it->second.accessCount++;
|
|
}
|
|
}
|
|
|
|
bool OnnxModelPool::canLoadModel(size_t estimatedSize) const {
|
|
size_t currentUsage = getMemoryUsage();
|
|
size_t maxBytes = config_.maxMemoryGB * 1024 * 1024 * 1024;
|
|
|
|
return (currentUsage + estimatedSize) <= maxBytes;
|
|
}
|
|
|
|
void OnnxModelPool::setResolutionHints(const std::string& modelName, const ResolutionHints& hints) {
|
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
modelHints_[modelName] = hints;
|
|
}
|
|
|
|
} // namespace yams::daemon
|