From bc63db3deb281eaa739726d18091f22b65e658dd Mon Sep 17 00:00:00 2001 From: Prathik Rao Date: Tue, 2 Jun 2026 13:32:21 -0700 Subject: [PATCH 1/3] update --- .../ep_detection/webgpu_ep_bootstrapper.cc | 237 +++++++++++------- .../src/ep_detection/webgpu_ep_bootstrapper.h | 13 +- 2 files changed, 157 insertions(+), 93 deletions(-) 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 76b008212..7fb1428bb 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,7 @@ // Licensed under the MIT License. #include "ep_detection/webgpu_ep_bootstrapper.h" +#include "http/http_client.h" #include "http/http_download.h" #include "logger.h" #include "util/file_lock.h" @@ -9,12 +10,14 @@ #include "util/zip_extract.h" #include +#include #include #include #include #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -25,82 +28,82 @@ 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 const char* kRegistrationName = "Foundry.WebGPU"; -constexpr ExpectedBinary kExpectedBinaries[] = { - {kWebGpuProviderLib, kWebGpuProviderHash}, +/// Parsed manifest entry for a single platform. +struct ManifestPackageInfo { + std::string url; + std::unordered_map sha256; // filename -> hash }; -constexpr const char* kRegistrationName = "Foundry.WebGPU"; +/// 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)); -/// 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); -} + auto body = fl::http::HttpGet(kManifestUrl, kUserAgent); + auto manifest = nlohmann::json::parse(body); -/// 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 (!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))); + } - if (!std::filesystem::exists(file_path)) { - 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)); + } - auto hash = fl::Sha256File(file_path); + const auto& pkg = packages[kPlatformKey]; + ManifestPackageInfo info; + info.url = pkg.at("url").get(); - // 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; + if (pkg.contains("sha256") && pkg["sha256"].is_object()) { + for (auto it = pkg["sha256"].begin(); it != pkg["sha256"].end(); ++it) { + info.sha256[it.key()] = it.value().get(); } } - return true; + logger.Log(fl::LogLevel::Information, + fmt::format("WebGPU EP: manifest fetched for platform '{}'", kPlatformKey)); + return info; } } // anonymous namespace @@ -118,6 +121,32 @@ bool WebGpuEpBootstrapper::IsRegistered() const { return registered_; } +bool WebGpuEpBootstrapper::VerifyPackage( + const std::filesystem::path& dir, + const std::unordered_map& expected_hashes, + ILogger& logger) { + for (const auto& [filename, expected_hash] : expected_hashes) { + auto file_path = dir / filename; + + if (!std::filesystem::exists(file_path)) { + return false; + } + + auto hash = Sha256File(file_path); + + // Case-insensitive 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("WebGPU EP: hash mismatch for {}: got {}, expected {}", + filename, hash, expected_hash)); + return false; + } + } + + return true; +} + bool WebGpuEpBootstrapper::DownloadAndRegister(bool force, const ProgressCallback& progress_cb, ILogger& logger) { @@ -136,61 +165,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 && VerifyPackage(ep_dir, manifest.sha256, 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 && VerifyPackage(ep_dir, manifest.sha256, 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 (!VerifyPackage(staging_dir, manifest.sha256, 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 +288,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 f01f0430f..3a98fc09d 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h @@ -6,6 +6,7 @@ #include "ep_detection/ep_types.h" #include +#include namespace fl { @@ -13,9 +14,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 hashes, downloads the binaries, 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 { @@ -37,6 +39,11 @@ class WebGpuEpBootstrapper : public IEpBootstrapper { ILogger& logger) override; private: + /// Verify all expected binaries exist in @p dir with correct SHA-256 hashes. + static bool VerifyPackage(const std::filesystem::path& dir, + const std::unordered_map& expected_hashes, + ILogger& logger); + std::string ep_dir_; std::string name_ = "WebGpuExecutionProvider"; bool registered_ = false; From af8fc51a8edc081368553bd689d03df02cc9ab43 Mon Sep 17 00:00:00 2001 From: Prathik Rao Date: Tue, 2 Jun 2026 14:29:02 -0700 Subject: [PATCH 2/3] copilot Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7fb1428bb..b78d16a47 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -68,7 +68,7 @@ struct ManifestPackageInfo { ManifestPackageInfo FetchManifest(fl::ILogger& logger) { logger.Log(fl::LogLevel::Debug, fmt::format("WebGPU EP: fetching manifest from {}", kManifestUrl)); - auto body = fl::http::HttpGet(kManifestUrl, kUserAgent); + auto body = fl::http::HttpGetWithRetry(kManifestUrl, kUserAgent, logger); auto manifest = nlohmann::json::parse(body); if (!manifest.contains("packages") || !manifest["packages"].is_object()) { From 21610bcd574ddc609e541812c0cbd775be6dc2a3 Mon Sep 17 00:00:00 2001 From: Prathik Rao Date: Wed, 3 Jun 2026 09:50:20 -0700 Subject: [PATCH 3/3] loc --- sdk_v2/cpp/src/manager.cc | 6 ++++-- sdk_v2/python/pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sdk_v2/cpp/src/manager.cc b/sdk_v2/cpp/src/manager.cc index f7c1e4bb1..64eb1a3b3 100644 --- a/sdk_v2/cpp/src/manager.cc +++ b/sdk_v2/cpp/src/manager.cc @@ -247,9 +247,11 @@ Manager::Manager(const Configuration& config) #endif if (config_.model_cache_dir.has_value()) { + auto cache_dir = std::filesystem::path(*config_.model_cache_dir).parent_path().string(); + // CUDA EP — only if an NVIDIA GPU is detected if (CudaEpBootstrapper::HasNvidiaGpu()) { - auto cuda_ep_dir = *config_.model_cache_dir + "/cuda-ep"; + auto cuda_ep_dir = cache_dir + "/cuda-ep"; bootstrappers.push_back(std::make_unique(std::move(cuda_ep_dir), register_ep)); } @@ -257,7 +259,7 @@ Manager::Manager(const Configuration& config) // 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"; + auto webgpu_ep_dir = cache_dir + "/webgpu-ep"; bootstrappers.push_back(std::make_unique(std::move(webgpu_ep_dir), register_ep)); #endif } diff --git a/sdk_v2/python/pyproject.toml b/sdk_v2/python/pyproject.toml index cb8cf3d3e..d1bc1d71c 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"