diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index c203deec..e9b046e7 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -155,6 +155,7 @@ set(FOUNDRY_LOCAL_SOURCES src/download/model_registry_client.cc src/ep_detection/cuda_ep_bootstrapper.cc src/ep_detection/ep_detector.cc + src/ep_detection/ep_utils.cc src/ep_detection/runtime_version_info.cc src/ep_detection/webgpu_ep_bootstrapper.cc src/exception.cc diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index 55ac8cd2..6f50de93 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -2,10 +2,10 @@ // Licensed under the MIT License. #include "ep_detection/cuda_ep_bootstrapper.h" +#include "ep_detection/ep_utils.h" #include "logger.h" #include "util/file_lock.h" #include "http/http_download.h" -#include "util/sha256.h" #include "util/zip_extract.h" #include @@ -61,31 +61,6 @@ constexpr ExpectedBinary kExpectedBinaries[] = { constexpr const char* kRegistrationName = "Foundry.CUDA"; constexpr const char* kCudaProviderDll = "onnxruntime_providers_cuda.dll"; -/// Verify all expected binaries exist and have correct SHA256 hashes. -bool VerifyPackage(const std::filesystem::path& dir, fl::ILogger& logger) { - for (const auto& expected : kExpectedBinaries) { - auto file_path = dir / expected.filename; - - if (!std::filesystem::exists(file_path)) { - return false; - } - - auto hash = fl::Sha256File(file_path); - - // Case-insensitive comparison - std::string expected_hash(expected.sha256); - if (!std::equal(hash.begin(), hash.end(), expected_hash.begin(), expected_hash.end(), - [](char a, char b) { return std::toupper(a) == std::toupper(b); })) { - logger.Log(fl::LogLevel::Warning, - fmt::format("CUDA EP: hash mismatch for {}: got {}, expected {}", - expected.filename, hash, expected.sha256)); - return false; - } - } - - return true; -} - } // anonymous namespace namespace fl { @@ -127,7 +102,10 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, FileLock lock(lock_path); // Check if package already exists and is valid - if (VerifyPackage(ep_dir, logger)) { + if (fl::VerifyEpPackage(ep_dir, + {{kExpectedBinaries[0].filename, kExpectedBinaries[0].sha256}, + {kExpectedBinaries[1].filename, kExpectedBinaries[1].sha256}}, + "CUDA EP", logger)) { logger.Log(LogLevel::Information, "CUDA EP: package already valid, skipping download"); } else { // Clean up any partial install @@ -170,7 +148,10 @@ bool CudaEpBootstrapper::DownloadAndRegister(bool force, std::filesystem::remove(zip_path); // Verify - if (!VerifyPackage(ep_dir, logger)) { + if (!fl::VerifyEpPackage(ep_dir, + {{kExpectedBinaries[0].filename, kExpectedBinaries[0].sha256}, + {kExpectedBinaries[1].filename, kExpectedBinaries[1].sha256}}, + "CUDA EP", logger)) { logger.Log(LogLevel::Warning, "CUDA EP: verification failed after download"); return false; } diff --git a/sdk_v2/cpp/src/ep_detection/ep_detector.cc b/sdk_v2/cpp/src/ep_detection/ep_detector.cc index 710ff9d3..4881cab6 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_detector.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_detector.cc @@ -163,7 +163,10 @@ EpDownloadResult EpDetector::DownloadAndRegisterEps(const std::vectorName()); - if (bs->DownloadAndRegister(/*force=*/true, wrapped_cb, logger_)) { + // Reuse previously downloaded EP packages unless the caller explicitly asks + // for a forced refresh. Downloading every time made the bootstrapper + // re-fetch and re-register EPs on every invocation. + if (bs->DownloadAndRegister(/*force=*/false, wrapped_cb, logger_)) { result.registered_eps.push_back(bs->Name()); // Update cached registration state in place under the cache lock so diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.cc b/sdk_v2/cpp/src/ep_detection/ep_utils.cc new file mode 100644 index 00000000..7fa6524f --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.cc @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#include "ep_detection/ep_utils.h" + +#include "logger.h" +#include "util/sha256.h" + +#include + +#include +#include +#include + +namespace fl { + +bool VerifyEpPackage( + const std::filesystem::path& dir, + std::initializer_list> expected, + std::string_view ep_name, + ILogger& logger) { + for (const auto& [filename, expected_hash] : expected) { + auto file_path = dir / filename; + + if (!std::filesystem::exists(file_path)) { + return false; + } + + auto hash = Sha256File(file_path); + + // Case-insensitive hex comparison + if (!std::equal(hash.begin(), hash.end(), expected_hash.begin(), expected_hash.end(), + [](char a, char b) { return std::toupper(a) == std::toupper(b); })) { + logger.Log(LogLevel::Warning, + fmt::format("{}: hash mismatch for {}: got {}, expected {}", + ep_name, filename, hash, expected_hash)); + return false; + } + } + + return true; +} + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.h b/sdk_v2/cpp/src/ep_detection/ep_utils.h new file mode 100644 index 00000000..634bb517 --- /dev/null +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +#pragma once + +#include +#include +#include +#include + +namespace fl { + +class ILogger; + +/// Verify a set of binaries in @p dir all exist and match their expected SHA-256 hashes. +/// +/// @param dir Directory containing the extracted EP binaries. +/// @param expected List of (filename, expected_sha256_hex) pairs. +/// @param ep_name EP name used in warning log messages (e.g. "CUDA EP"). +/// @param logger Logger for diagnostic output. +/// @return true if every file exists and its hash matches; false otherwise. +bool VerifyEpPackage( + const std::filesystem::path& dir, + std::initializer_list> expected, + std::string_view ep_name, + ILogger& logger); + +} // namespace fl diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index 76b00821..afdb7da9 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -2,6 +2,8 @@ // Licensed under the MIT License. #include "ep_detection/webgpu_ep_bootstrapper.h" +#include "ep_detection/ep_utils.h" +#include "http/http_client.h" #include "http/http_download.h" #include "logger.h" #include "util/file_lock.h" @@ -9,6 +11,7 @@ #include "util/zip_extract.h" #include +#include #include #include @@ -25,82 +28,77 @@ namespace { constexpr const char* kPackageFileName = "webgpu-ep.zip"; constexpr const char* kLockFileName = "webgpu-ep.lock"; +constexpr const char* kStagingDirName = "webgpu-ep-staging"; constexpr const char* kUserAgent = "FoundryLocal"; constexpr int kMaxInstallAttempts = 5; -// Platform-specific download URL suffix. +// Manifest URL — always uses prod. +constexpr const char* kManifestUrl = + "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_prod.json"; + +// Platform key used to look up this platform's package in the manifest. #if defined(_WIN32) && defined(_M_ARM64) -constexpr const char* kPlatformSuffix = "win-arm64"; +constexpr const char* kPlatformKey = "win-arm64"; #elif defined(_WIN32) -constexpr const char* kPlatformSuffix = "win-x64"; +constexpr const char* kPlatformKey = "win-x64"; #elif defined(__APPLE__) -constexpr const char* kPlatformSuffix = "macos-arm64"; +constexpr const char* kPlatformKey = "macos-arm64"; #else -constexpr const char* kPlatformSuffix = "linux-x64"; +constexpr const char* kPlatformKey = "linux-x64"; #endif -// Platform-specific EP library filename and expected SHA-256 hash. -// -- Update these hashes when uploading new WebGPU EP binaries -- +// Platform-specific EP library filename. #if defined(_WIN32) constexpr const char* kWebGpuProviderLib = "onnxruntime_providers_webgpu.dll"; -#if defined(_M_ARM64) -constexpr const char* kWebGpuProviderHash = - "3AE46E25A2DF149A890A78A09B466189070456EC79AC206E87E09F1840704597"; -#else -constexpr const char* kWebGpuProviderHash = - "8E074DB27BE59203A8F58E15E8700058D1F76DF7A4295EA3361FC46331BB985E"; -#endif #elif defined(__APPLE__) constexpr const char* kWebGpuProviderLib = "libonnxruntime_providers_webgpu.dylib"; -constexpr const char* kWebGpuProviderHash = - "12D9E105FCAC11B50685DB64462D7490C7AEEB5219530387464A7CF6D9F323E7"; #else constexpr const char* kWebGpuProviderLib = "libonnxruntime_providers_webgpu.so"; -constexpr const char* kWebGpuProviderHash = - "64211A7844B243DB78E0ECA0FAB7DF0EE5B4F7D131886A00C09CF105BF7D94CE"; #endif -struct ExpectedBinary { - const char* filename; - const char* sha256; -}; - -constexpr ExpectedBinary kExpectedBinaries[] = { - {kWebGpuProviderLib, kWebGpuProviderHash}, -}; - constexpr const char* kRegistrationName = "Foundry.WebGPU"; -/// Build the full CDN download URL for the current platform. -std::string GetDownloadUrl() { - return fmt::format( - "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_20260504-224804_{}.zip", - kPlatformSuffix); -} +/// Parsed manifest entry for a single platform. +struct ManifestPackageInfo { + std::string url; + std::string sha256; // expected SHA256 hash of kWebGpuProviderLib +}; -/// Verify all expected binaries exist and have correct SHA256 hashes. -bool VerifyPackage(const std::filesystem::path& dir, fl::ILogger& logger) { - for (const auto& expected : kExpectedBinaries) { - auto file_path = dir / expected.filename; +/// Fetch the manifest JSON from CDN and extract the package info for this platform. +ManifestPackageInfo FetchManifest(fl::ILogger& logger) { + logger.Log(fl::LogLevel::Debug, fmt::format("WebGPU EP: fetching manifest from {}", kManifestUrl)); - if (!std::filesystem::exists(file_path)) { - return false; - } + auto body = fl::http::HttpGetWithRetry(kManifestUrl, kUserAgent, logger); + auto manifest = nlohmann::json::parse(body); - auto hash = fl::Sha256File(file_path); + if (!manifest.contains("packages") || !manifest["packages"].is_object()) { + throw std::runtime_error( + fmt::format("WebGPU EP: manifest is invalid — missing 'packages' field. " + "Raw content (first 200 chars): {}", + body.substr(0, 200))); + } - // Case-insensitive comparison - std::string expected_hash(expected.sha256); - if (!std::equal(hash.begin(), hash.end(), expected_hash.begin(), expected_hash.end(), - [](char a, char b) { return std::toupper(a) == std::toupper(b); })) { - logger.Log(fl::LogLevel::Warning, - fmt::format("WebGPU EP: hash mismatch for {}: got {}, expected {}", - expected.filename, hash, expected.sha256)); - return false; + const auto& packages = manifest["packages"]; + if (!packages.contains(kPlatformKey)) { + std::string available; + for (auto it = packages.begin(); it != packages.end(); ++it) { + if (!available.empty()) available += ", "; + available += it.key(); } + throw std::runtime_error( + fmt::format("WebGPU EP: manifest does not contain a package for platform '{}'. " + "Available platforms: {}", + kPlatformKey, available)); } - return true; + const auto& pkg = packages[kPlatformKey]; + ManifestPackageInfo info; + info.url = pkg.at("url").get(); + info.sha256 = pkg.at("sha256").at(kWebGpuProviderLib).get(); + + logger.Log(fl::LogLevel::Information, + fmt::format("WebGPU EP: manifest fetched for platform '{}'", kPlatformKey)); + return info; } } // anonymous namespace @@ -136,61 +134,91 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, attempts_++; auto ep_dir = std::filesystem::path(ep_dir_); - auto lock_path = ep_dir.parent_path() / kLockFileName; - auto zip_path = ep_dir.parent_path() / kPackageFileName; + auto parent_dir = ep_dir.parent_path(); try { - // Cross-process lock to prevent concurrent installs - FileLock lock(lock_path); + // Fetch manifest before acquiring lock (avoid holding lock during network I/O) + auto manifest = FetchManifest(logger); // Check if package already exists and is valid - if (VerifyPackage(ep_dir, logger)) { - logger.Log(LogLevel::Information, "WebGPU EP: package already valid, skipping download"); + if (!force && VerifyEpPackage(ep_dir, {{kWebGpuProviderLib, manifest.sha256}}, "WebGPU EP", logger)) { + logger.Log(LogLevel::Debug, "WebGPU EP: local binaries match manifest, skipping download"); } else { - // Clean up any partial install - if (std::filesystem::exists(ep_dir)) { - std::filesystem::remove_all(ep_dir); - } - - std::filesystem::create_directories(ep_dir); + // Ensure parent directory exists for the lock file + std::filesystem::create_directories(parent_dir); + auto lock_path = parent_dir / kLockFileName; + + // Cross-process lock to prevent concurrent installs + FileLock lock(lock_path); + + // Re-check after acquiring lock (another process may have completed the update) + if (!force && VerifyEpPackage(ep_dir, {{kWebGpuProviderLib, manifest.sha256}}, "WebGPU EP", logger)) { + logger.Log(LogLevel::Debug, "WebGPU EP: another process already completed the update"); + } else { + // Download and extract to staging directory for atomic swap + auto staging_dir = parent_dir / kStagingDirName; + if (std::filesystem::exists(staging_dir)) { + std::filesystem::remove_all(staging_dir); + } + std::filesystem::create_directories(staging_dir); + + auto zip_path = staging_dir / kPackageFileName; + + // Download + logger.Log(LogLevel::Information, + fmt::format("WebGPU EP: downloading for {} from CDN", kPlatformKey)); + logger.Log(LogLevel::Debug, + fmt::format("WebGPU EP: download URL is {}", manifest.url)); + + std::atomic cancel_flag{false}; + auto download_progress = [&](float pct) { + if (progress_cb) { + // 0–80% for download phase + if (!progress_cb(name_, pct * 0.8f)) { + cancel_flag.store(true); + } + } + }; - // Download - auto url = GetDownloadUrl(); - logger.Log(LogLevel::Information, fmt::format("WebGPU EP: downloading from CDN ({})", url)); + if (!HttpDownloadFile(manifest.url, zip_path, kUserAgent, + &cancel_flag, download_progress, logger)) { + logger.Log(LogLevel::Warning, "WebGPU EP: download failed (see prior log for details)"); + return false; + } - // Bridge callback-based cancellation to the atomic flag HttpDownloadFile expects - std::atomic cancel_flag{false}; + // Extract + logger.Log(LogLevel::Information, + fmt::format("WebGPU EP: extracting package to {}", staging_dir.string())); - auto download_progress = [&](float pct) { - if (progress_cb) { - // 0-80% for download phase - if (!progress_cb(name_, pct * 0.8f)) { - cancel_flag.store(true); - } + if (!ExtractZip(zip_path, staging_dir, logger)) { + logger.Log(LogLevel::Warning, "WebGPU EP: extraction failed"); + std::filesystem::remove_all(staging_dir); + return false; } - }; - if (!HttpDownloadFile(url, zip_path, kUserAgent, - &cancel_flag, download_progress, logger)) { - logger.Log(LogLevel::Warning, "WebGPU EP: download failed (see prior log for details)"); - return false; - } + // Clean up zip + std::filesystem::remove(zip_path); - // Extract - logger.Log(LogLevel::Information, "WebGPU EP: extracting..."); + // Verify staging + if (!VerifyEpPackage(staging_dir, {{kWebGpuProviderLib, manifest.sha256}}, "WebGPU EP", logger)) { + logger.Log(LogLevel::Warning, + fmt::format("WebGPU EP: verification failed after extraction (attempt {})", + attempts_)); + std::filesystem::remove_all(staging_dir); + return false; + } - if (!ExtractZip(zip_path, ep_dir, logger)) { - logger.Log(LogLevel::Warning, "WebGPU EP: extraction failed"); - return false; - } + logger.Log(LogLevel::Debug, + fmt::format("WebGPU EP: staging verification succeeded, promoting to {}", + ep_dir.string())); - // Clean up zip - std::filesystem::remove(zip_path); + // Atomic swap: delete old, rename staging to target + if (std::filesystem::exists(ep_dir)) { + std::filesystem::remove_all(ep_dir); + } + std::filesystem::rename(staging_dir, ep_dir); - // Verify - if (!VerifyPackage(ep_dir, logger)) { - logger.Log(LogLevel::Warning, "WebGPU EP: verification failed after download"); - return false; + logger.Log(LogLevel::Information, "WebGPU EP: successfully installed"); } } @@ -229,8 +257,6 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, progress_cb(name_, 100.0f); } - // Bootstrapper-side log — captures the install dir, which the central - // register_ep callback (logs library + version) doesn't have. logger.Log(LogLevel::Information, fmt::format("WebGPU EP: ready (install_path={})", ep_dir.string())); return true; diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h index f01f0430..2cc8e52e 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h @@ -13,9 +13,10 @@ class ILogger; /// Bootstrapper for the WebGPU execution provider. /// -/// Downloads WebGPU EP binaries from Azure CDN, extracts, verifies SHA256, -/// then registers with ORT. Unlike CUDA, no GPU detection is needed — -/// WebGPU is always attempted when the bootstrapper is present. +/// Fetches a manifest from Azure CDN to discover the current WebGPU EP +/// package URL and expected SHA-256 hash, downloads the binary, verifies +/// integrity, then registers with ORT. The manifest-driven approach allows +/// updating WebGPU EP binaries without shipping a new Foundry Local release. /// /// Supports Windows x64/ARM64, Linux x64, and macOS ARM64. class WebGpuEpBootstrapper : public IEpBootstrapper { diff --git a/sdk_v2/cpp/src/manager.cc b/sdk_v2/cpp/src/manager.cc index f7c1e4bb..6ec8dbed 100644 --- a/sdk_v2/cpp/src/manager.cc +++ b/sdk_v2/cpp/src/manager.cc @@ -247,19 +247,19 @@ Manager::Manager(const Configuration& config) #endif if (config_.model_cache_dir.has_value()) { + const auto cache_dir = std::filesystem::path(*config_.model_cache_dir).parent_path(); + // CUDA EP — only if an NVIDIA GPU is detected if (CudaEpBootstrapper::HasNvidiaGpu()) { - auto cuda_ep_dir = *config_.model_cache_dir + "/cuda-ep"; - bootstrappers.push_back(std::make_unique(std::move(cuda_ep_dir), register_ep)); + const auto cuda_ep_dir = cache_dir / "cuda-ep"; + bootstrappers.push_back(std::make_unique(cuda_ep_dir.string(), register_ep)); } // WebGPU EP — always available (no hardware detection needed). - // Skipped in WinML builds because the WinML-aligned ORT (1.23.2) is older - // than the ORT API version required by the WebGPU EP plugin (>= 24). -#if !(defined(FOUNDRY_LOCAL_USE_WINML) && FOUNDRY_LOCAL_USE_WINML) - auto webgpu_ep_dir = *config_.model_cache_dir + "/webgpu-ep"; - bootstrappers.push_back(std::make_unique(std::move(webgpu_ep_dir), register_ep)); -#endif + // TODO(@bmehta001): When WinML 2.0 adds WebGPU support, add a WinML-aware + // WebGPU path here that can coexist with the WinML EPs discovered above. + const auto webgpu_ep_dir = cache_dir / "webgpu-ep"; + bootstrappers.push_back(std::make_unique(webgpu_ep_dir.string(), register_ep)); } ep_detector_ = std::make_unique(*ort_api_, *ort_env_, std::move(bootstrappers), *logger_); diff --git a/sdk_v2/python/pyproject.toml b/sdk_v2/python/pyproject.toml index cb8cf3d3..d1bc1d71 100644 --- a/sdk_v2/python/pyproject.toml +++ b/sdk_v2/python/pyproject.toml @@ -8,7 +8,7 @@ backend-path = ["."] [project] name = "foundry-local-sdk" -version = "0.1.0" +version = "2.0.0.dev0" description = "Foundry Local Python SDK (v2): in-process Python bindings for the Foundry Local native runtime." readme = "README.md" requires-python = ">=3.11,<3.15"