Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 147 additions & 90 deletions sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@
// 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"
#include "util/sha256.h"
#include "util/zip_extract.h"

#include <fmt/format.h>
#include <nlohmann/json.hpp>

#include <algorithm>
#include <atomic>
#include <cctype>
#include <filesystem>
#include <string>
#include <unordered_map>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
Expand All @@ -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";
Comment thread
prathikr marked this conversation as resolved.

// 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<std::string, std::string> 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::HttpGetWithRetry(kManifestUrl, kUserAgent, logger);
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<std::string>();

// 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<std::string>();
}
}

return true;
logger.Log(fl::LogLevel::Information,
fmt::format("WebGPU EP: manifest fetched for platform '{}'", kPlatformKey));
return info;
}

} // anonymous namespace
Expand All @@ -118,6 +121,32 @@ bool WebGpuEpBootstrapper::IsRegistered() const {
return registered_;
}

bool WebGpuEpBootstrapper::VerifyPackage(
const std::filesystem::path& dir,
const std::unordered_map<std::string, std::string>& 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;
Comment thread
prathikr marked this conversation as resolved.
}

bool WebGpuEpBootstrapper::DownloadAndRegister(bool force,
const ProgressCallback& progress_cb,
ILogger& logger) {
Expand All @@ -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<bool> 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<bool> 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");
}
}

Expand Down Expand Up @@ -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;
Expand Down
13 changes: 10 additions & 3 deletions sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
#include "ep_detection/ep_types.h"

#include <string>
#include <unordered_map>

namespace fl {

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 {
Expand All @@ -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<std::string, std::string>& expected_hashes,
ILogger& logger);

std::string ep_dir_;
std::string name_ = "WebGpuExecutionProvider";
bool registered_ = false;
Expand Down
6 changes: 4 additions & 2 deletions sdk_v2/cpp/src/manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -247,17 +247,19 @@ 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<CudaEpBootstrapper>(std::move(cuda_ep_dir), 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";
auto webgpu_ep_dir = cache_dir + "/webgpu-ep";
bootstrappers.push_back(std::make_unique<WebGpuEpBootstrapper>(std::move(webgpu_ep_dir), register_ep));
#endif
}
Expand Down
2 changes: 1 addition & 1 deletion sdk_v2/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down