Add secure, transactional CUDA and WebGPU EP bootstrapping - #952
Add secure, transactional CUDA and WebGPU EP bootstrapping#952Baiju Meswani (baijumeswani) wants to merge 6 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a shared, transactional installer for CUDA and WebGPU execution-provider bundles.
Changes:
- Adds verified, reusable bundle installation with atomic activation.
- Adds CUDA platform manifests, NVML detection, and dependency ownership.
- Replaces external ZIP extraction and expands security-focused tests.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
sdk_v2/cpp/vcpkg.json |
Adds archive dependencies. |
sdk_v2/cpp/CMakeLists.txt |
Builds and links new components. |
sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc |
Installs and registers CUDA bundles. |
sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.h |
Updates CUDA bootstrapper ownership. |
sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.cc |
Defines platform CUDA bundles. |
sdk_v2/cpp/src/ep_detection/cuda_ep_manifest.h |
Declares CUDA manifest APIs. |
sdk_v2/cpp/src/ep_detection/ep_bundle_installer.cc |
Implements transactional installation. |
sdk_v2/cpp/src/ep_detection/ep_bundle_installer.h |
Declares installer transactions. |
sdk_v2/cpp/src/ep_detection/ep_bundle_manifest.h |
Defines bundle metadata. |
sdk_v2/cpp/src/ep_detection/ep_utils.cc |
Manages Windows dependencies. |
sdk_v2/cpp/src/ep_detection/ep_utils.h |
Exposes dependency helpers. |
sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.cc |
Implements NVML GPU detection. |
sdk_v2/cpp/src/ep_detection/nvml_gpu_detector.h |
Declares GPU detection APIs. |
sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc |
Migrates WebGPU to bundles. |
sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.h |
Updates WebGPU bootstrapper ownership. |
sdk_v2/cpp/src/http/http_download.cc |
Adds strict size handling. |
sdk_v2/cpp/src/http/http_download.h |
Extends downloader contract. |
sdk_v2/cpp/src/manager.cc |
Updates EP lifecycle and teardown. |
sdk_v2/cpp/src/manager.h |
Documents revised ownership order. |
sdk_v2/cpp/src/util/zip_extract.cc |
Adds in-process bounded extraction. |
sdk_v2/cpp/src/util/zip_extract.h |
Defines extraction limits. |
sdk_v2/cpp/test/CMakeLists.txt |
Registers new unit tests. |
sdk_v2/cpp/test/internal_api/c_api_test.cc |
Tests manager recreation. |
sdk_v2/cpp/test/internal_api/cuda_ep_bootstrapper_test.cc |
Tests CUDA manifests and behavior. |
sdk_v2/cpp/test/internal_api/ep_bundle_installer_test.cc |
Tests installation transactions. |
sdk_v2/cpp/test/internal_api/ep_utils_test.cc |
Tests dependency selection. |
sdk_v2/cpp/test/internal_api/http_download_test.cc |
Tests Content-Length parsing. |
sdk_v2/cpp/test/internal_api/nvml_gpu_detector_test.cc |
Tests capability filtering. |
sdk_v2/cpp/test/internal_api/webgpu_ep_bootstrapper_test.cc |
Tests WebGPU registration. |
sdk_v2/cpp/test/internal_api/zip_extract_test.cc |
Tests secure extraction. |
sdk_v2/cpp/test/utils/scoped_environment_variable.h |
Adds environment test helper. |
sdk_v2/cpp/test/utils/zip_builder.h |
Adds ZIP fixture builder. |
| try { | ||
| safe_log(LogLevel::Warning, std::string("EP unregister: UnregisterExecutionProviderLibrary failed for '") + | ||
| name + "': " + (msg ? msg : "unknown")); | ||
| } catch (...) { | ||
| } |
There was a problem hiding this comment.
nit: do you need another try/catch here if safe_log already has that? same for the "Exception while shutting down Manager subsystems during destruction" usage of safe_log.
| std::string archive_sha256; | ||
| std::vector<EpBundleFile> extracted_files; | ||
| std::vector<std::string> ignored_archive_paths; | ||
| uint64_t archive_max_bytes = 0; |
There was a problem hiding this comment.
I assume we'll manage the manifests using AI, so if we have the SHA for each file could we also have the exact bytes for individual files, and infer the max values from that?
There was a problem hiding this comment.
According to AI:
A ZIP can be larger than the sum of the declared file sizes because:
- Incompressible files may slightly expand when compressed.
- ZIP headers and the central directory add overhead.
- The archive contains ignored entries such as version.json .
we could derive the max from total file sizes plus some overhead.
| /// @param max_bytes Fail closed if a Content-Length header exceeds this, and abort mid-stream | ||
| /// if the body exceeds it regardless of what Content-Length promised (defends | ||
| /// against a missing/incorrect header on chunked transfers). -1 means no cap. |
There was a problem hiding this comment.
Is this a known issue? are we downloading from an endpoint we don't control and we need extra protection?
There was a problem hiding this comment.
The ep archives are hosted on endpoints controlled by us. The sha256 validation only happens post download. But if the url was wrong, or the endpoint compromised or misconfigured, this may protect users from downloading excessively large archives.
Will make clarifying code comment.
| bool CommitActive(ILogger& logger); | ||
|
|
||
| private: | ||
| friend class EpBundleInstaller; |
There was a problem hiding this comment.
nit: using friend means you give up control of all your internals. as this is an internal only class that seems like a high price just to protect the ctor.
| /// This selection logic is platform-independent (pure path/string manipulation) so it can be unit | ||
| /// tested on any platform, even though the actual preloading only happens on Windows. |
There was a problem hiding this comment.
what's the point of including/testing this on other platforms?
|
|
||
| #include <filesystem> | ||
|
|
||
| #if defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT) |
There was a problem hiding this comment.
Not clear how FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT relates to CUDA usage.
There was a problem hiding this comment.
from AI:
FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT means “this build uses WinHTTP for networking.” The NVML detector uses it as a proxy for “this is desktop Windows,” because CMake currently defines it only for desktop Windows and not Windows Store/UWP builds.
Windows Store/UWP applications cannot use the same native-loading approach as an unpackaged desktop application.
The detector currently:
- Loads nvml.dll dynamically from System32 or NVIDIA’s Program Files directory.
- Uses desktop APIs such as SHGetKnownFolderPath .
- Relies on access to the machine-wide NVIDIA driver installation.
A UWP process is sandboxed and generally must load packaged native libraries through LoadPackagedLibrary ; it cannot freely discover and load DLLs from arbitrary machine paths. The CUDA bootstrapper also downloads native DLLs at runtime, which does not fit the normal UWP package model.
So CUDA/NVML is intentionally disabled for WindowsStore builds today. Supporting it would require a separate packaged-library design, not simply changing the preprocessor condition
Will introduce FOUNDRY_LOCAL_DESKTOP_WINDOWS.
| if (!init_ || !shutdown_ || !get_count_ || !get_handle_ || !get_compute_cap_) { | ||
| UnloadLibrary(lib_); | ||
| lib_ = kNullLibrary; | ||
| return; | ||
| } |
There was a problem hiding this comment.
should there be something logged if we fail this way?
| constexpr const char* kBundleId = "webgpu-ep-0.2.1-win-arm64"; | ||
| constexpr const char* kDownloadUrl = | ||
| "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_win-arm64.zip"; | ||
| constexpr const char* kArchiveSha256 = "3674C8BD50F19AB84D3F738AC426DB37EB119BC1B790525B5A4F4139C253AF08"; | ||
| constexpr const char* kProviderSha256 = "63CFEF0E7FB8FDC2238F69CD8E804F50FDA393B2B60C448DAEC73E031DE75058"; | ||
| constexpr const char* kDxCompilerSha256 = "3895C1F437E8E91A771F562AD2E5EA9EF918365EA1D7D4216AF4C58BA87E9D7B"; | ||
| constexpr const char* kDxilSha256 = "9377B286B378AF2ACD7DA7686F25FB60C7D22DEC4BA384BAB0523494DE3E75D0"; | ||
| #elif defined(_WIN32) && defined(_M_X64) | ||
| constexpr const char* kBundleId = "webgpu-ep-0.2.1-win-x64"; | ||
| constexpr const char* kDownloadUrl = | ||
| "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_win-x64.zip"; | ||
| constexpr const char* kArchiveSha256 = "91A05B2C9EAF326011FE74604BBDF06E08C5B95A1F40425F4426EF0E90A9984D"; | ||
| constexpr const char* kProviderSha256 = "BE2EBCC0A96D1558D9123C04E75C2851260FE45C9DBC8959CB2CD8D11B83ABBE"; | ||
| constexpr const char* kDxCompilerSha256 = "174DBC3DF8F7AF5C32C0E39F43C0D5BC576395EDC3CCDD64119A1B63C081ED55"; | ||
| constexpr const char* kDxilSha256 = "080C02F62E90D0AB7ACC463BBC10280C37397DC6D036224D7C10F2ED9C20E13D"; | ||
| #elif defined(__APPLE__) && defined(__aarch64__) | ||
| constexpr const char* kBundleId = "webgpu-ep-0.2.1-macos-arm64"; | ||
| constexpr const char* kDownloadUrl = | ||
| "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.2.1_macos-arm64.zip"; | ||
| constexpr const char* kArchiveSha256 = "5F0F8378172F53EFA281328F33D1EBC793E44197FFB4CC605D02C593B891C0EA"; | ||
| constexpr const char* kProviderSha256 = "8FAC874A60F32F0127C74CB7DEF915807FCC8A6C30B77629E45F8CEE60272EAE"; | ||
| #endif |
There was a problem hiding this comment.
nit: do we need these vs. putting the values inline like we do in cuda_ep_manifest.cc? I found the latter a bit easier to read, but not sure if one way or the other is going to be easier to update.
| std::vector<std::filesystem::path> SelectEpBundleDependenciesToPreload(const std::filesystem::path& bin_dir, | ||
| const EpBundleManifest& manifest) { |
There was a problem hiding this comment.
Why do we need to do this? Is this potentially loading files that may not be needed for a specific model?
There was a problem hiding this comment.
We should not be preloading all binaries.
We need to preload libonnxrutime-genai-cuda.so so onnxruntime-genai can find it when it tries to load it.
On windows, we can do AddDllDirectory so cuda and cudnn and onnxruntime-genai-cuda.dll binaries be found.
Summary
This change adds first-class CUDA execution-provider bootstrapping for Windows x64, Windows ARM64, and Linux x64. It also moves WebGPU onto the same reusable bundle installer so both providers follow one secure, transactional installation flow.
CUDA is treated as one compatible bundle made from independently reusable artifacts. The large CUDA and cuDNN packages change less frequently than the execution-provider package, so Foundry Local validates each installed artifact against the new manifest before downloading anything. If the CUDA or cuDNN runtime-file hashes are unchanged, those files are reused and only the changed archive, such as the CUDA EP package, is downloaded. The reused and newly downloaded files are then assembled, verified, and atomically activated as one generation, avoiding unnecessary large downloads without exposing a partially updated or incompatible installation.
Why
The CUDA and WebGPU bootstrappers previously owned their package-management logic independently. Downloading, extracting, validating, caching, and activating provider binaries inside each bootstrapper made the behavior harder to reuse and maintain.
The new CUDA package layout also requires Foundry Local to manage several archives as one compatible installation:
The shared installer provides a single implementation for:
Code flow before this change
CUDA and WebGPU each managed most of the package lifecycle themselves.
Code flow after this change
The bootstrappers now describe their packages and handle provider-specific loading. The shared installer owns the package lifecycle.
Provider teardown after this change
The teardown sequence now keeps GenAI, ORT, provider libraries, and their dependencies alive in the required order.
CUDA platform support
WebGPU behavior
WebGPU continues to support its existing published platforms, but now uses the shared bundle installer. It gains the same:
Installation and repair behavior
Valid active bundle
Every declared runtime file is rehashed. If all hashes and the exact installed file set match, the existing generation is reused without downloading.
Partially damaged bundle
Each artifact is checked independently. Valid artifact files are copied into a new staging generation, while only invalid artifacts are downloaded again. The combined generation is then fully verified before activation.
Forced installation
A forced request downloads every artifact instead of selectively reusing files.
Failure or cancellation
Failed downloads, hash mismatches, malformed archives, cancellation, loading failures, and marker-publication failures return failure. Temporary staging data is removed, and a partially assembled generation is never made active.
Security and reliability improvements
NVIDIA GPU detection
CUDA eligibility is detected through NVML instead of starting a subprocess. Foundry Local requires at least one NVIDIA device with the supported compute capability. NVML is loaded dynamically and released after detection.