diff --git a/cmake/onnxruntime_providers_webgpu.cmake b/cmake/onnxruntime_providers_webgpu.cmake index cd29e4dad0a17..ff586bef9470e 100644 --- a/cmake/onnxruntime_providers_webgpu.cmake +++ b/cmake/onnxruntime_providers_webgpu.cmake @@ -84,6 +84,14 @@ add_definitions("-DONNX_NAMESPACE=onnx") add_definitions("-DONNX_USE_LITE_PROTO=1") + # Default plugin EP version to ORT_VERSION with "-dev" suffix if not explicitly provided. + if(NOT DEFINED onnxruntime_PLUGIN_EP_VERSION) + set(onnxruntime_PLUGIN_EP_VERSION "${ORT_VERSION}-dev") + endif() + + # Set preprocessor definition for plugin EP version + target_compile_definitions(onnxruntime_providers_webgpu PRIVATE ORT_PLUGIN_EP_VERSION="${onnxruntime_PLUGIN_EP_VERSION}") + # Set preprocessor definitions used in onnxruntime_providers_webgpu.rc if(WIN32) set(WEBGPU_DLL_FILE_DESCRIPTION "ONNX Runtime WebGPU Provider") diff --git a/include/onnxruntime/ep/adapter/op_kernel.h b/include/onnxruntime/ep/adapter/op_kernel.h index 273461b36e75f..06ffdca16af6d 100644 --- a/include/onnxruntime/ep/adapter/op_kernel.h +++ b/include/onnxruntime/ep/adapter/op_kernel.h @@ -155,7 +155,11 @@ struct OpKernelContext { } bool GetUseDeterministicCompute() const { // TODO(fs-eire): Implement GetUseDeterministicCompute(). + // if (CurrentOrtApiVersion() >= 25) { + // return /* TBD: wait for GetUseDeterministicCompute to be added in ORT API v25 */; + // } else { return false; + // } } void* GetGPUComputeStream() const { return context_.GetGPUComputeStream(); diff --git a/include/onnxruntime/ep/api.h b/include/onnxruntime/ep/api.h index c22e52ed8aaa5..60f6b8613bb49 100644 --- a/include/onnxruntime/ep/api.h +++ b/include/onnxruntime/ep/api.h @@ -3,6 +3,8 @@ #pragma once +#include +#include #include #include #include @@ -26,8 +28,30 @@ struct ApiPtrs { namespace detail { inline std::optional g_api_ptrs; + +inline bool TryGetAPIVersionFromVersionString(const char* version_str, uint32_t& api_version) { + // A valid version string should always be in the format of "1.{API_VERSION}.*". + if (!version_str || version_str[0] != '1' || version_str[1] != '.') { + return false; + } + const char* begin = version_str + 2; + const char* end = std::strchr(begin, '.'); + if (!end) { + return false; + } + uint32_t version = 0; + auto [ptr, ec] = std::from_chars(begin, end, version); + if (ec != std::errc{} || ptr != end) { + return false; + } + api_version = version; + return true; } +inline uint32_t g_current_ort_api_version{}; + +} // namespace detail + /// /// Get the global instance of ApiPtrs. /// @@ -45,10 +69,41 @@ inline const ApiPtrs& Api() { inline void ApiInit(const OrtApiBase* ort_api_base) { static std::once_flag init_flag; std::call_once(init_flag, [&]() { - // Manual init for the C++ API - const OrtApi* ort_api = ort_api_base->GetApi(ORT_API_VERSION); + // The following initialization process is composed of 3 steps: + // 1) Get the ORT API version string + // 2) Try to parse the ORT API version from the version string. If parsing fails, we assume the version is 24. + // 3) Get the ORT API for the parsed version and initialize the global API instance with it. + constexpr uint32_t ORT_BASE_API_VERSION = 24; + const char* version_str = ort_api_base->GetVersionString(); + if (!version_str) { + version_str = "unknown"; + } + uint32_t current_ort_version = 0; + if (!detail::TryGetAPIVersionFromVersionString(version_str, current_ort_version)) { + // If we fail to parse the version string, we can still try to get the API for the base version and hope it works. + current_ort_version = ORT_BASE_API_VERSION; + } + if (current_ort_version < ORT_BASE_API_VERSION) { + throw std::runtime_error("Failed to initialize EP API: the minimum required ORT API version is " + std::to_string(ORT_BASE_API_VERSION) + + ", but the current version is \"" + version_str + + "\" (parsed API version: " + std::to_string(current_ort_version) + ")."); + } + + const OrtApi* ort_api = ort_api_base->GetApi(current_ort_version); + if (!ort_api) { + throw std::runtime_error("Failed to initialize EP API: the current ORT version is \"" + std::string(version_str) + + "\" but it does not support the parsed API version " + std::to_string(current_ort_version) + "."); + } + + detail::g_current_ort_api_version = current_ort_version; + const OrtEpApi* ep_api = ort_api->GetEpApi(); const OrtModelEditorApi* model_editor_api = ort_api->GetModelEditorApi(); + if (!ep_api || !model_editor_api) { + throw std::runtime_error("Failed to initialize EP API: GetEpApi or GetModelEditorApi returned null."); + } + + // Manual init for the C++ API Ort::InitApi(ort_api); // Initialize the global API instance @@ -56,5 +111,17 @@ inline void ApiInit(const OrtApiBase* ort_api_base) { }); } +/// +/// Get the current ORT API version that the EP API has been initialized with. +/// +/// This function should be called after ApiInit() to get the actual API version. +/// +inline uint32_t CurrentOrtApiVersion() { + if (!detail::g_api_ptrs.has_value()) { + throw std::logic_error("onnxruntime::ep::CurrentOrtApiVersion() called before ApiInit()."); + } + return detail::g_current_ort_api_version; +} + } // namespace ep } // namespace onnxruntime diff --git a/onnxruntime/core/providers/webgpu/ep/factory.cc b/onnxruntime/core/providers/webgpu/ep/factory.cc index 99dd0c68f6954..6d8e8724f72d9 100644 --- a/onnxruntime/core/providers/webgpu/ep/factory.cc +++ b/onnxruntime/core/providers/webgpu/ep/factory.cc @@ -66,7 +66,7 @@ uint32_t ORT_API_CALL Factory::GetVendorIdImpl(const OrtEpFactory* /*this_ptr*/) } const char* ORT_API_CALL Factory::GetVersionImpl(const OrtEpFactory* /*this_ptr*/) noexcept { - return "0.1.0"; + return ORT_PLUGIN_EP_VERSION; } OrtStatus* ORT_API_CALL Factory::GetSupportedDevicesImpl( diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index f28132d23aa5b..1ff20fc9dac73 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -55,6 +55,7 @@ #include "core/session/onnxruntime_session_options_config_keys.h" #include "core/session/ort_apis.h" #include "core/session/ort_env.h" +#include "core/session/ort_version_check.h" #include "core/session/utils.h" #if defined(USE_CUDA) || defined(USE_CUDA_PROVIDER_INTERFACE) @@ -4830,6 +4831,9 @@ ORT_API(const OrtApi*, OrtApis::GetApi, uint32_t version) { } ORT_API(const char*, OrtApis::GetVersionString) { + static_assert(onnxruntime::version_check::IsOrtVersionValid(ORT_VERSION), + "ORT_VERSION must be in the format '1.Y.Z' where Y and Z are non-negative integers without leading " + "zeros, and Y must equal ORT_API_VERSION"); return ORT_VERSION; } diff --git a/onnxruntime/core/session/ort_version_check.h b/onnxruntime/core/session/ort_version_check.h new file mode 100644 index 0000000000000..82fd757e3ce9f --- /dev/null +++ b/onnxruntime/core/session/ort_version_check.h @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +#include "core/session/onnxruntime_c_api.h" + +namespace onnxruntime::version_check { + +// A simple consteval-friendly result type for ParseUint. +// std::optional triggers an internal compiler error in MSVC 14.44 when used with consteval. +struct ParseUintResult { + uint32_t value; + bool has_value; + + consteval bool operator==(uint32_t other) const { return has_value && value == other; } + consteval bool operator!=(uint32_t other) const { return !(*this == other); } +}; + +inline consteval ParseUintResult ParseUintNone() { return {0, false}; } + +// Parse a non-negative integer from a string_view without leading zeros. +// Returns a result with has_value == false on failure (empty, leading zero, non-digit, or overflow). +consteval ParseUintResult ParseUint(std::string_view str) { + if (str.empty()) return ParseUintNone(); + // Leading zeros are not allowed (except "0" itself). + if (str.size() > 1 && str[0] == '0') return ParseUintNone(); + uint64_t result = 0; + for (char c : str) { + if (c < '0' || c > '9') return ParseUintNone(); + result = result * 10 + static_cast(c - '0'); + if (result > UINT32_MAX) return ParseUintNone(); + } + return {static_cast(result), true}; +} + +// Validates a version string at compile time. +// It must be in the format "1.Y.Z" where: +// - Major version is 1 +// - Y and Z are non-negative integers without leading zeros +// - Y (minor version) must equal expected_api_version (defaults to ORT_API_VERSION) +consteval bool IsOrtVersionValid(std::string_view version, uint32_t expected_api_version = ORT_API_VERSION) { + size_t first_dot = version.find('.'); + if (first_dot == std::string_view::npos) return false; + size_t second_dot = version.find('.', first_dot + 1); + if (second_dot == std::string_view::npos) return false; + if (version.find('.', second_dot + 1) != std::string_view::npos) return false; // Exactly two dots + std::string_view major = version.substr(0, first_dot); + std::string_view minor = version.substr(first_dot + 1, second_dot - first_dot - 1); + std::string_view patch = version.substr(second_dot + 1); + if (major != "1") { + return false; + } + auto minor_val = ParseUint(minor); + auto patch_val = ParseUint(patch); + if (!minor_val.has_value || !patch_val.has_value) { + return false; + } + if (minor_val.value != expected_api_version) { + return false; + } + return true; +} + +} // namespace onnxruntime::version_check diff --git a/onnxruntime/test/shared_lib/test_version.cc b/onnxruntime/test/shared_lib/test_version.cc index 32a0f41ca99f1..8011132dd2b0c 100644 --- a/onnxruntime/test/shared_lib/test_version.cc +++ b/onnxruntime/test/shared_lib/test_version.cc @@ -1,31 +1,64 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include "onnxruntime_config.h" #include "core/session/onnxruntime_cxx_api.h" +#include "core/session/ort_version_check.h" -#include -#include -#include -#include -#include - -#include "absl/strings/str_split.h" #include "gtest/gtest.h" -TEST(CApiTest, VersionConsistencyWithApiVersion) { - const auto version_string = Ort::GetVersionString(); - const std::vector version_string_components = absl::StrSplit(version_string, '.'); - ASSERT_EQ(version_string_components.size(), size_t{3}); - - auto to_uint32_t = [](const std::string& s) -> std::optional { - uint32_t result{}; - if (std::from_chars(s.data(), s.data() + s.size(), result).ec == std::errc{}) { - return result; - } - return std::nullopt; - }; - - ASSERT_NE(to_uint32_t(version_string_components[0]), std::nullopt); - ASSERT_EQ(to_uint32_t(version_string_components[1]), uint32_t{ORT_API_VERSION}); - ASSERT_NE(to_uint32_t(version_string_components[2]), std::nullopt); +using onnxruntime::version_check::IsOrtVersionValid; +using onnxruntime::version_check::ParseUint; + +// Compile-time tests for ParseUint +static_assert(ParseUint("0") == 0u); +static_assert(ParseUint("1") == 1u); +static_assert(ParseUint("25") == 25u); +static_assert(ParseUint("123") == 123u); +static_assert(ParseUint("4294967295") == 4294967295u); // UINT32_MAX +static_assert(!(ParseUint("4294967296").has_value)); // UINT32_MAX + 1 overflows +static_assert(!(ParseUint("").has_value)); // empty +static_assert(!(ParseUint("01").has_value)); // leading zero +static_assert(!(ParseUint("00").has_value)); // leading zero +static_assert(!(ParseUint("abc").has_value)); // non-digit +static_assert(!(ParseUint("1a").has_value)); // trailing non-digit +static_assert(!(ParseUint("-1").has_value)); // negative sign +static_assert(!(ParseUint("1.0").has_value)); // contains dot +static_assert(ParseUint("0").has_value); +static_assert(!ParseUint("").has_value); + +// Compile-time tests for IsOrtVersionValid (default expected_api_version = ORT_API_VERSION) +static_assert(IsOrtVersionValid(ORT_VERSION)); // current version must be valid + +// Invalid formats +static_assert(!IsOrtVersionValid("")); +static_assert(!IsOrtVersionValid("1")); +static_assert(!IsOrtVersionValid("1.0")); +static_assert(!IsOrtVersionValid("1.0.0.0")); // too many dots +static_assert(!IsOrtVersionValid("2.0.0")); // major != 1 +static_assert(!IsOrtVersionValid("1.02.0")); // leading zero in minor +static_assert(!IsOrtVersionValid("1.0.01")); // leading zero in patch +static_assert(!IsOrtVersionValid("1..0")); // empty minor +static_assert(!IsOrtVersionValid("1.0.")); // empty patch +static_assert(!IsOrtVersionValid(".1.0")); // empty major +static_assert(!IsOrtVersionValid("abc")); // non-numeric +static_assert(!IsOrtVersionValid("1.abc.0")); // non-numeric minor +static_assert(!IsOrtVersionValid("1.0.abc")); // non-numeric patch + +// Compile-time tests for IsOrtVersionValid with explicit expected_api_version +static_assert(IsOrtVersionValid("1.0.0", 0)); +static_assert(IsOrtVersionValid("1.1.0", 1)); +static_assert(IsOrtVersionValid("1.25.0", 25)); +static_assert(IsOrtVersionValid("1.25.3", 25)); +static_assert(IsOrtVersionValid("1.100.0", 100)); +static_assert(!IsOrtVersionValid("1.25.0", 24)); // minor doesn't match expected +static_assert(!IsOrtVersionValid("1.25.0", 26)); // minor doesn't match expected +static_assert(!IsOrtVersionValid("1.0.0", 1)); // minor 0 != expected 1 +static_assert(!IsOrtVersionValid("2.0.0", 0)); // major != 1 +static_assert(!IsOrtVersionValid("1.02.0", 2)); // leading zero in minor +static_assert(!IsOrtVersionValid("1.0.01", 0)); // leading zero in patch + +TEST(CApiTest, VersionIsValid) { + // Runtime sanity check — the version string returned by the API is the expected one. + EXPECT_STREQ(Ort::GetVersionString().c_str(), ORT_VERSION); } diff --git a/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml b/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml new file mode 100644 index 0000000000000..a9cfc2139fb95 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml @@ -0,0 +1,117 @@ +trigger: none + +resources: + repositories: + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +parameters: +- name: build_windows_x64 + displayName: 'Build Windows x64' + type: boolean + default: true + +- name: build_windows_arm64 + displayName: 'Build Windows ARM64' + type: boolean + default: false + +- name: build_linux_x64 + displayName: 'Build Linux x64' + type: boolean + default: false + +- name: build_macos_arm64 + displayName: 'Build macOS ARM64' + type: boolean + default: false + +- name: package_version + displayName: 'Package Version' + type: string + values: + - release + - RC + - dev + default: dev + +- name: cmake_build_type + type: string + default: 'Release' + values: + - Debug + - Release + - RelWithDebInfo + - MinSizeRel + +variables: + # Windows ARM64 build requires Windows x64 build to be enabled (ARM64 cross-compilation depends on x64 build artifacts) + - name: invalidARM64Config + value: ${{ and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, false)) }} + # Non-dev package versions (release, RC) must use Release build type + - name: invalidBuildTypeConfig + value: ${{ and(ne(parameters.package_version, 'dev'), ne(parameters.cmake_build_type, 'Release')) }} + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + settings: + networkIsolationPolicy: Permissive + sdl: + componentgovernance: + ignoreDirectories: '$(Build.Repository.LocalPath)/cmake/external/emsdk/upstream/emscripten/tests,$(Build.Repository.LocalPath)/cmake/external/onnx/third_party/benchmark,$(Build.Repository.LocalPath)/cmake/external/onnx/third_party/pybind11,$(Build.Repository.LocalPath)/cmake/external/onnx/third_party/pybind11/tests,$(Build.Repository.LocalPath)/cmake/external/onnxruntime-extensions,$(Build.Repository.LocalPath)/js/react_native/e2e/node_modules,$(Build.Repository.LocalPath)/js/node_modules,$(Build.Repository.LocalPath)/onnxruntime-inference-examples,$(Build.SourcesDirectory)/cmake/external/emsdk/upstream/emscripten/tests,$(Build.SourcesDirectory)/cmake/external/onnx/third_party/benchmark,$(Build.SourcesDirectory)/cmake/external/onnx/third_party/pybind11,$(Build.SourcesDirectory)/cmake/external/onnx/third_party/pybind11/tests,$(Build.SourcesDirectory)/cmake/external/onnxruntime-extensions,$(Build.SourcesDirectory)/js/react_native/e2e/node_modules,$(Build.SourcesDirectory)/js/node_modules,$(Build.SourcesDirectory)/onnxruntime-inference-examples,$(Build.BinariesDirectory)' + alertWarningLevel: High + failOnAlert: false + verbosity: Normal + timeout: 3600 + tsa: + enabled: true + codeSignValidation: + enabled: true + break: true + policheck: + enabled: true + exclusionsFile: '$(Build.SourcesDirectory)\tools\ci_build\policheck_exclusions.xml' + codeql: + compiled: + enabled: false + justificationForDisabling: 'CodeQL is taking nearly 6 hours resulting in timeouts in our production pipelines' + pool: + name: 'onnxruntime-Win-CPU-VS2022-Latest' + os: windows + + stages: + # Validate parameter combinations + - ${{ if or(eq(variables['invalidARM64Config'], 'True'), eq(variables['invalidBuildTypeConfig'], 'True')) }}: + - stage: Validate_Parameters + displayName: 'Validate Parameters' + dependsOn: [] + jobs: + - job: Validate + displayName: 'Validate parameter combinations' + pool: + name: 'onnxruntime-Win-CPU-VS2022-Latest' + os: windows + steps: + - checkout: none + - ${{ if eq(variables['invalidARM64Config'], 'True') }}: + - script: | + echo "##vso[task.logissue type=error]Windows ARM64 build requires Windows x64 build to be enabled." + exit 1 + displayName: 'ERROR: Windows ARM64 requires Windows x64' + - ${{ if eq(variables['invalidBuildTypeConfig'], 'True') }}: + - script: | + echo "##vso[task.logissue type=error]Non-dev package version requires Release build type." + exit 1 + displayName: 'ERROR: Non-dev package version requires Release build type' + - ${{ else }}: + - template: stages/plugin-webgpu-packaging-stage.yml + parameters: + build_windows_x64: ${{ parameters.build_windows_x64 }} + build_windows_arm64: ${{ parameters.build_windows_arm64 }} + build_linux_x64: ${{ parameters.build_linux_x64 }} + build_macos_arm64: ${{ parameters.build_macos_arm64 }} + package_version: ${{ parameters.package_version }} + cmake_build_type: ${{ parameters.cmake_build_type }} diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml new file mode 100644 index 0000000000000..00e716ff3af26 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml @@ -0,0 +1,96 @@ +parameters: +- name: machine_pool + type: string + default: 'onnxruntime-Ubuntu2404-AMD-CPU' + +- name: package_version + type: string + default: dev + +- name: cmake_build_type + type: string + default: 'Release' + values: + - Debug + - Release + - RelWithDebInfo + - MinSizeRel + +- name: docker_base_image + type: string + default: 'onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cuda12_x64_almalinux8_gcc14:20251017.1' + +stages: +- stage: Linux_plugin_webgpu_x64_Build + dependsOn: [] + jobs: + - job: Linux_plugin_webgpu_x64_Build + timeoutInMinutes: 240 + workspace: + clean: all + pool: + name: ${{ parameters.machine_pool }} + os: linux + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory) + artifactName: webgpu_plugin_linux_x64 + variables: + - template: ../templates/common-variables.yml + steps: + - checkout: self + clean: true + submodules: recursive + + - template: ../templates/set-nightly-build-option-variable-step.yml + + - template: ../templates/set-plugin-build-variables-step.yml + parameters: + package_version: ${{ parameters.package_version }} + + - template: ../templates/get-docker-image-steps.yml + parameters: + Dockerfile: tools/ci_build/github/linux/docker/inference/x86_64/python/cuda/Dockerfile + Context: tools/ci_build/github/linux/docker/inference/x86_64/python/cuda + DockerBuildArgs: "--build-arg BASEIMAGE=${{ parameters.docker_base_image }} --build-arg BUILD_UID=$( id -u )" + Repository: onnxruntimewebgpuplugin + + - script: $(Build.SourcesDirectory)/tools/ci_build/github/linux/build_webgpu_plugin_package.sh -i onnxruntimewebgpuplugin -c ${{ parameters.cmake_build_type }} + workingDirectory: $(Build.SourcesDirectory) + displayName: 'Build WebGPU Plugin' + env: + EXTRA_CMAKE_DEFINES: $(PluginEpVersionDefine) + + - script: | + set -e -x + mkdir -p $(Build.ArtifactStagingDirectory)/bin + plugin_path="$(Build.BinariesDirectory)/${{ parameters.cmake_build_type }}/libonnxruntime_providers_webgpu.so" + if [ ! -f "$plugin_path" ]; then + echo "Error: Expected plugin binary not found at '$plugin_path'. Failing build to avoid publishing an invalid package." + exit 1 + fi + cp "$plugin_path" "$(Build.ArtifactStagingDirectory)/bin/" + displayName: 'Copy plugin binaries' + + - script: | + set -e -x + mkdir -p "$(Build.ArtifactStagingDirectory)/version" + touch "$(Build.ArtifactStagingDirectory)/version/$(PluginPackageVersion)" + displayName: 'Create version marker file' + + - script: | + set -e -x + mkdir -p "$(Build.BinariesDirectory)/universal_package" + cp -R "$(Build.ArtifactStagingDirectory)/bin/"* "$(Build.BinariesDirectory)/universal_package/" + displayName: 'Stage binaries for universal package' + + - task: UniversalPackages@0 + displayName: 'Publish universal package' + inputs: + command: publish + publishDirectory: '$(Build.BinariesDirectory)/universal_package' + vstsFeedPublish: 'PublicPackages/ORT-Nightly' + vstsFeedPackagePublish: 'onnxruntime-plugin-ep-webgpu-linux-x64' + versionOption: custom + versionPublish: '$(PluginUniversalPackageVersion)' diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml new file mode 100644 index 0000000000000..16e16e54fd236 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml @@ -0,0 +1,111 @@ +parameters: +- name: package_version + type: string + default: dev + +- name: cmake_build_type + type: string + default: 'Release' + values: + - Debug + - Release + - RelWithDebInfo + - MinSizeRel + +stages: +- stage: MacOS_plugin_webgpu_arm64_Build + dependsOn: [] + jobs: + - job: MacOS_plugin_webgpu_arm64_Build + timeoutInMinutes: 240 + workspace: + clean: all + pool: + name: AcesShared + os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory) + artifactName: webgpu_plugin_macos_arm64 + variables: + - name: MACOSX_DEPLOYMENT_TARGET + value: '14.0' + - template: ../templates/common-variables.yml + steps: + - checkout: self + clean: true + submodules: none + + - template: ../templates/use-xcode-version.yml + parameters: + xcodeVersion: '16.4' + + - template: ../templates/setup-build-tools.yml + parameters: + host_cpu_arch: 'arm64' + + - template: ../templates/set-nightly-build-option-variable-step.yml + + - template: ../templates/set-plugin-build-variables-step.yml + parameters: + package_version: ${{ parameters.package_version }} + + - script: | + set -e -x + python3 -m pip install -r '$(Build.SourcesDirectory)/tools/ci_build/github/linux/docker/scripts/requirements.txt' + python3 $(Build.SourcesDirectory)/tools/ci_build/build.py \ + --build_dir $(Build.SourcesDirectory)/build \ + --use_vcpkg --use_vcpkg_ms_internal_asset_cache \ + --use_binskim_compliant_compile_flags \ + --config ${{ parameters.cmake_build_type }} \ + --enable_onnx_tests \ + --use_webgpu shared_lib \ + --wgsl_template static \ + --disable_rtti \ + --enable_lto \ + --cmake_extra_defines CMAKE_OSX_ARCHITECTURES=arm64 onnxruntime_BUILD_UNIT_TESTS=ON $(PluginEpVersionDefine) \ + --update --skip_submodule_sync --build --parallel + displayName: 'Build WebGPU Plugin' + + - script: | + set -e + plugin_path="$(Build.SourcesDirectory)/build/${{ parameters.cmake_build_type }}/libonnxruntime_providers_webgpu.dylib" + if [ ! -f "$plugin_path" ]; then + echo "Error: Expected plugin binary not found at '$plugin_path'. Failing build to avoid publishing an invalid package." + exit 1 + fi + echo "Verified plugin binary exists at: $plugin_path" + displayName: 'Verify plugin binary exists' + + - task: CopyFiles@2 + displayName: 'Copy plugin binaries to staging directory' + inputs: + SourceFolder: '$(Build.SourcesDirectory)/build/${{ parameters.cmake_build_type }}' + Contents: | + libonnxruntime_providers_webgpu.dylib + TargetFolder: '$(Build.ArtifactStagingDirectory)/bin' + + - script: | + set -e -x + mkdir -p "$(Build.ArtifactStagingDirectory)/version" + touch "$(Build.ArtifactStagingDirectory)/version/$(PluginPackageVersion)" + displayName: 'Create version marker file' + + - script: | + set -e -x + mkdir -p "$(Build.BinariesDirectory)/universal_package" + cp -R "$(Build.ArtifactStagingDirectory)/bin/"* "$(Build.BinariesDirectory)/universal_package/" + displayName: 'Stage binaries for universal package' + + - task: UniversalPackages@0 + displayName: 'Publish universal package' + inputs: + command: publish + publishDirectory: '$(Build.BinariesDirectory)/universal_package' + vstsFeedPublish: 'PublicPackages/ORT-Nightly' + vstsFeedPackagePublish: 'onnxruntime-plugin-ep-webgpu-macos-arm64' + versionOption: custom + versionPublish: '$(PluginUniversalPackageVersion)' diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml new file mode 100644 index 0000000000000..1864bb4016bb4 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml @@ -0,0 +1,72 @@ +parameters: +- name: build_windows_x64 + displayName: 'Build Windows x64' + type: boolean + default: true + +- name: build_windows_arm64 + displayName: 'Build Windows ARM64' + type: boolean + default: false + +- name: build_linux_x64 + displayName: 'Build Linux x64' + type: boolean + default: false + +- name: build_macos_arm64 + displayName: 'Build macOS ARM64' + type: boolean + default: false + +- name: package_version + displayName: 'Package Version' + type: string + default: dev + values: + - dev + - release + - RC + +- name: cmake_build_type + type: string + displayName: 'CMake build type' + default: 'Release' + values: + - Debug + - Release + - RelWithDebInfo + - MinSizeRel + +stages: + # Windows x64 + - ${{ if eq(parameters.build_windows_x64, true) }}: + - template: plugin-win-webgpu-stage.yml + parameters: + arch: 'x64' + package_version: ${{ parameters.package_version }} + cmake_build_type: ${{ parameters.cmake_build_type }} + + # Windows ARM64 + # ARM64 build requires the x64 tblgen.exe (used during the build), which is not correctly + # generated in a cross build. So we require x64 to be built first and download tblgen.exe from it. + - ${{ if and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, true)) }}: + - template: plugin-win-webgpu-stage.yml + parameters: + arch: 'arm64' + package_version: ${{ parameters.package_version }} + cmake_build_type: ${{ parameters.cmake_build_type }} + + # Linux x64 + - ${{ if eq(parameters.build_linux_x64, true) }}: + - template: plugin-linux-webgpu-stage.yml + parameters: + package_version: ${{ parameters.package_version }} + cmake_build_type: ${{ parameters.cmake_build_type }} + + # macOS ARM64 + - ${{ if eq(parameters.build_macos_arm64, true) }}: + - template: plugin-mac-webgpu-stage.yml + parameters: + package_version: ${{ parameters.package_version }} + cmake_build_type: ${{ parameters.cmake_build_type }} diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml new file mode 100644 index 0000000000000..352ae77544e93 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml @@ -0,0 +1,257 @@ +parameters: +- name: arch + type: string + values: + - x64 + - arm64 + +- name: package_version + type: string + default: dev + +- name: cmake_build_type + type: string + default: 'Release' + values: + - Debug + - Release + - RelWithDebInfo + - MinSizeRel + +- name: npm_registry_url + type: string + default: 'https://pkgs.dev.azure.com/aiinfra/_packaging/ONNXRuntime_WebGPU_BuildDependencies/npm/registry/' + +stages: + - stage: Win_plugin_webgpu_${{ parameters.arch }}_Build + ${{ if eq(parameters.arch, 'arm64') }}: + dependsOn: Win_plugin_webgpu_x64_Build + ${{ else }}: + dependsOn: [] + jobs: + - job: Win_plugin_webgpu_${{ parameters.arch }}_Build + timeoutInMinutes: 360 + workspace: + clean: all + pool: + name: onnxruntime-Win-CPU-VS2022-Latest + os: windows + templateContext: + sdl: + codeSignValidation: + enabled: true + break: true + additionalTargetsGlobPattern: '-|**\clang-tblgen.exe;-|**\llvm-tblgen.exe' + psscriptanalyzer: + enabled: true + binskim: + enabled: true + scanOutputDirectoryOnly: true + outputs: + - ${{ if eq(parameters.arch, 'x64') }}: + - output: pipelineArtifact + targetPath: '$(Build.ArtifactStagingDirectory)\WebGPU_BuildTools' + artifactName: WebGPU_BuildTools_x64 + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory) + artifactName: webgpu_plugin_win_${{ parameters.arch }} + variables: + - template: ../templates/common-variables.yml + - name: GRADLE_OPTS + value: '-Dorg.gradle.daemon=false' + - name: VSGenerator + value: 'Visual Studio 17 2022' + - name: CrossCompileDefines + value: '' + - name: ArchFlag + ${{ if eq(parameters.arch, 'arm64') }}: + value: '--arm64' + ${{ else }}: + value: '' + steps: + - checkout: self + clean: true + submodules: none + + - template: ../templates/setup-build-tools.yml + parameters: + host_cpu_arch: 'x64' + + - template: ../templates/set-nightly-build-option-variable-step.yml + + - template: ../templates/set-plugin-build-variables-step.yml + parameters: + package_version: ${{ parameters.package_version }} + + - script: | + python -m pip install -r "$(Build.SourcesDirectory)\tools\ci_build\github\windows\python\requirements.txt" + displayName: 'Install Python build dependencies' + env: + TMPDIR: "$(Agent.TempDirectory)" + + - task: PowerShell@2 + displayName: '[WebGPU] Create .npmrc for WebGPU build' + inputs: + targetType: 'inline' + pwsh: true + script: | + $npmrcPath = Join-Path "$(Build.SourcesDirectory)" "onnxruntime/core/providers/webgpu/wgsl_templates/.npmrc" + $npmrcDir = Split-Path -Parent $npmrcPath + $packageJsonPath = Join-Path $npmrcDir "package.json" + + # Check if package.json exists + if (-not (Test-Path $packageJsonPath)) { + Write-Error "package.json not found at: $packageJsonPath" + throw "package.json does not exist in $npmrcDir" + } + + # Create .npmrc file with required content + $npmrcContent = "registry=${{ parameters.npm_registry_url }}`n`nalways-auth=true" + + Set-Content -Path $npmrcPath -Value $npmrcContent -Encoding UTF8 + Write-Host "Created .npmrc at: $npmrcPath" + + - task: npmAuthenticate@0 + displayName: '[WebGPU] Authenticate npm for WebGPU build' + inputs: + workingFile: '$(Build.SourcesDirectory)/onnxruntime/core/providers/webgpu/wgsl_templates/.npmrc' + + - ${{ if eq(parameters.arch, 'arm64') }}: + - task: DownloadPipelineArtifact@2 + displayName: 'Download WebGPU build tools from x64 build' + inputs: + artifactName: 'WebGPU_BuildTools_x64' + targetPath: '$(Build.BinariesDirectory)\WebGPU_BuildTools' + - script: | + @echo ##vso[task.setvariable variable=LLVM_TABLEGEN_PATH]$(Build.BinariesDirectory)\WebGPU_BuildTools\llvm-tblgen.exe + @echo ##vso[task.setvariable variable=CLANG_TABLEGEN_PATH]$(Build.BinariesDirectory)\WebGPU_BuildTools\clang-tblgen.exe + displayName: 'Set tablegen paths' + - powershell: | + Write-Host "Using LLVM_TABLEGEN_PATH: $(LLVM_TABLEGEN_PATH)" + Write-Host "Using CLANG_TABLEGEN_PATH: $(CLANG_TABLEGEN_PATH)" + Write-Host "##vso[task.setvariable variable=CrossCompileDefines]LLVM_TABLEGEN=$(LLVM_TABLEGEN_PATH) CLANG_TABLEGEN=$(CLANG_TABLEGEN_PATH)" + displayName: 'Set build flags for WebGPU cross-compilation' + + - task: PythonScript@0 + displayName: 'Build' + inputs: + scriptPath: '$(Build.SourcesDirectory)\tools\ci_build\build.py' + arguments: >- + $(ArchFlag) + --config ${{ parameters.cmake_build_type }} + --build_dir $(Build.BinariesDirectory) + --skip_submodule_sync + --cmake_generator "$(VSGenerator)" + --parallel + --use_vcpkg + --use_vcpkg_ms_internal_asset_cache + --use_binskim_compliant_compile_flags + --update + --build + --enable_onnx_tests + --use_webgpu shared_lib + --wgsl_template static + --disable_rtti + --enable_lto + --cmake_extra_defines onnxruntime_BUILD_UNIT_TESTS=ON onnxruntime_ENABLE_DAWN_BACKEND_D3D12=1 onnxruntime_ENABLE_DAWN_BACKEND_VULKAN=1 $(PluginEpVersionDefine) $(CrossCompileDefines) + $(TelemetryOption) + workingDirectory: '$(Build.BinariesDirectory)' + + - ${{ if eq(parameters.arch, 'x64') }}: + - script: | + mkdir $(Build.ArtifactStagingDirectory)\WebGPU_BuildTools + copy $(Build.BinariesDirectory)\${{ parameters.cmake_build_type }}\_deps\dawn-build\third_party\dxc\${{ parameters.cmake_build_type }}\bin\llvm-tblgen.exe $(Build.ArtifactStagingDirectory)\WebGPU_BuildTools + copy $(Build.BinariesDirectory)\${{ parameters.cmake_build_type }}\_deps\dawn-build\third_party\dxc\${{ parameters.cmake_build_type }}\bin\clang-tblgen.exe $(Build.ArtifactStagingDirectory)\WebGPU_BuildTools + displayName: 'Copy WebGPU build tools' + + - powershell: | + $dxcZipUrl = "https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.8.2502/dxc_2025_02_20.zip" + $dxcZipPath = "$(Build.BinariesDirectory)\dxc.zip" + $dxcExtractPath = "$(Build.BinariesDirectory)\dxc_extracted" + $targetArch = "${{ parameters.arch }}" + $expectedHash = "70B1913A1BFCE4A3E1A5311D16246F4ECDF3A3E613ABEC8AA529E57668426F85" + + # Download the DXC package + Write-Host "Downloading DXC release from $dxcZipUrl" + Invoke-WebRequest -Uri $dxcZipUrl -OutFile $dxcZipPath + + # Verify integrity of downloaded file + $actualHash = (Get-FileHash -Path $dxcZipPath -Algorithm SHA256).Hash + if ($actualHash -ne $expectedHash) { + throw "DXC zip hash mismatch! Expected: $expectedHash, Got: $actualHash. The download may be corrupted or tampered with." + } + Write-Host "DXC zip hash verified: $actualHash" + + # Create extraction directory + if (-not (Test-Path $dxcExtractPath)) { + New-Item -Path $dxcExtractPath -ItemType Directory -Force + } + + # Extract the zip file + Write-Host "Extracting DXC package to $dxcExtractPath" + Expand-Archive -Path $dxcZipPath -DestinationPath $dxcExtractPath -Force + + # Copy the necessary DLLs to the build output directory so they get ESRP-signed + $sourcePath = Join-Path $dxcExtractPath "bin\$targetArch" + $targetPath = "$(Build.BinariesDirectory)\${{ parameters.cmake_build_type }}\${{ parameters.cmake_build_type }}" + + if (-not (Test-Path $targetPath)) { + New-Item -Path $targetPath -ItemType Directory -Force + } + + Write-Host "Copying dxil.dll and dxcompiler.dll from $sourcePath to $targetPath" + Copy-Item -Path "$sourcePath\dxil.dll" -Destination $targetPath -Force + Copy-Item -Path "$sourcePath\dxcompiler.dll" -Destination $targetPath -Force + + Write-Host "DXC DLLs successfully copied to the target directory" + displayName: 'Download and Copy DXC Binaries' + + # Esrp signing + - template: ../templates/win-esrp-dll.yml + parameters: + FolderPath: '$(Build.BinariesDirectory)\${{ parameters.cmake_build_type }}\${{ parameters.cmake_build_type }}' + DisplayName: 'ESRP - Sign Native dlls' + DoEsrp: true + Pattern: '*.dll' + + - powershell: | + $pluginPath = "$(Build.BinariesDirectory)\${{ parameters.cmake_build_type }}\${{ parameters.cmake_build_type }}\onnxruntime_providers_webgpu.dll" + if (-not (Test-Path $pluginPath)) { + Write-Error "Expected plugin binary not found at '$pluginPath'. Failing build to avoid publishing an invalid package." + exit 1 + } + Write-Host "Verified plugin binary exists at: $pluginPath" + displayName: 'Verify plugin binary exists' + + - task: CopyFiles@2 + displayName: 'Copy plugin binaries to staging directory' + inputs: + SourceFolder: '$(Build.BinariesDirectory)\${{ parameters.cmake_build_type }}\${{ parameters.cmake_build_type }}' + Contents: | + onnxruntime_providers_webgpu.dll + onnxruntime_providers_webgpu.pdb + dxil.dll + dxcompiler.dll + TargetFolder: '$(Build.ArtifactStagingDirectory)\bin' + + - script: | + mkdir "$(Build.ArtifactStagingDirectory)\version" + type nul > "$(Build.ArtifactStagingDirectory)\version\$(PluginPackageVersion)" + displayName: 'Create version marker file' + + - task: CopyFiles@2 + displayName: 'Stage binaries for universal package' + inputs: + SourceFolder: '$(Build.ArtifactStagingDirectory)\bin' + Contents: '**' + TargetFolder: '$(Build.BinariesDirectory)\universal_package' + + - task: UniversalPackages@0 + displayName: 'Publish universal package' + inputs: + command: publish + publishDirectory: '$(Build.BinariesDirectory)\universal_package' + vstsFeedPublish: 'PublicPackages/ORT-Nightly' + vstsFeedPackagePublish: 'onnxruntime-plugin-ep-webgpu-win-${{ parameters.arch }}' + versionOption: custom + versionPublish: '$(PluginUniversalPackageVersion)' diff --git a/tools/ci_build/github/azure-pipelines/templates/set-plugin-build-variables-step.yml b/tools/ci_build/github/azure-pipelines/templates/set-plugin-build-variables-step.yml new file mode 100644 index 0000000000000..212eca44ae3ec --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/templates/set-plugin-build-variables-step.yml @@ -0,0 +1,85 @@ +# This file is used to set variables for plugin build stages. It sets the package version +# variable based on the build type (nightly, official, or dev). + +parameters: +- name: package_version + type: string + +steps: +# Set package version string +- task: PythonScript@0 + displayName: 'Set plugin package version string' + inputs: + scriptSource: inline + script: | + import os + import re + import subprocess + import sys + + package_version = "${{ parameters.package_version }}" + + src_root = os.environ.get("BUILD_SOURCESDIRECTORY", "") + version_file = os.path.join(src_root, "VERSION_NUMBER") + if not os.path.isfile(version_file): + print("##vso[task.logissue type=error]Cannot find VERSION_NUMBER at: {}".format(version_file)) + sys.exit(1) + + with open(version_file, "r") as f: + original_ver = f.read().strip() + + if not original_ver: + print("##vso[task.logissue type=error]VERSION_NUMBER is empty.") + sys.exit(1) + + print("Original version: {}".format(original_ver)) + print("Package version type: {}".format(package_version)) + + if package_version == "release": + version_string = original_ver + universal_version = original_ver + + elif package_version == "RC": + # RC versioning is not yet implemented. Fail the build to prevent publishing + # an ambiguous version without an RC number. + print("##vso[task.logissue type=error]RC versioning is not yet implemented. Use 'dev' or 'release' instead.") + sys.exit(1) + + elif package_version == "dev": + try: + commit_sha = subprocess.check_output( + ["git", "rev-parse", "--short=8", "HEAD"], + cwd=src_root + ).decode("utf-8").strip() + date_str = subprocess.check_output( + ["git", "show", "-s", "--format=%cd", "--date=format:%Y%m%d", "HEAD"], + cwd=src_root + ).decode("utf-8").strip() + except Exception as e: + print("##vso[task.logissue type=error]Failed to get git info: {}".format(e)) + sys.exit(1) + version_string = "{}-dev.{}+{}".format(original_ver, date_str, commit_sha) + universal_version = "{}-dev.{}.{}".format(original_ver, date_str, commit_sha) + + else: + print("##vso[task.logissue type=error]Unknown package_version '{}'. Must be 'release', 'RC', or 'dev'.".format(package_version)) + sys.exit(1) + + print("Plugin package version string: {}".format(version_string)) + print("Plugin universal package version string: {}".format(universal_version)) + + # Validate semver 2.0.0 format + semver_pattern = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" + if not re.match(semver_pattern, version_string): + print("##vso[task.logissue type=error]Version string '{}' is not valid semver 2.0.0.".format(version_string)) + sys.exit(1) + + # Validate universal version (SemVer 1.0.0 - no build metadata) + universal_semver_pattern = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$" + if not re.match(universal_semver_pattern, universal_version): + print("##vso[task.logissue type=error]Universal version string '{}' is not valid semver 1.0.0.".format(universal_version)) + sys.exit(1) + + print("##vso[task.setvariable variable=PluginPackageVersion]{}".format(version_string)) + print("##vso[task.setvariable variable=PluginUniversalPackageVersion]{}".format(universal_version)) + print("##vso[task.setvariable variable=PluginEpVersionDefine]onnxruntime_PLUGIN_EP_VERSION={}".format(version_string)) diff --git a/tools/ci_build/github/linux/build_webgpu_plugin_package.sh b/tools/ci_build/github/linux/build_webgpu_plugin_package.sh new file mode 100755 index 0000000000000..a3583bf9d17a5 --- /dev/null +++ b/tools/ci_build/github/linux/build_webgpu_plugin_package.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -e -x + +# Build WebGPU plugin shared library for Linux inside Docker. +# This script follows the same pattern as build_nodejs_package.sh. + +BUILD_CONFIG="Release" +DOCKER_IMAGE="onnxruntimewebgpuplugin" + +while getopts "i:c:" parameter_Option +do case "${parameter_Option}" +in +i) DOCKER_IMAGE=${OPTARG};; +c) BUILD_CONFIG=${OPTARG};; +*) echo "Usage: $0 -i [-c ]" + exit 1;; +esac +done + +mkdir -p "${HOME}/.onnx" + +docker run --rm \ + --volume /data/onnx:/data/onnx:ro \ + --volume "${BUILD_SOURCESDIRECTORY}:/onnxruntime_src" \ + --volume "${BUILD_BINARIESDIRECTORY}:/build" \ + --volume /data/models:/build/models:ro \ + --volume "${HOME}/.onnx:/home/onnxruntimedev/.onnx" \ + -e NIGHTLY_BUILD \ + -e BUILD_BUILDNUMBER \ + -e SYSTEM_COLLECTIONURI \ + "$DOCKER_IMAGE" \ + /bin/bash -c "/usr/bin/python3 /onnxruntime_src/tools/ci_build/build.py \ + --build_dir /build \ + --config ${BUILD_CONFIG} \ + --skip_submodule_sync \ + --parallel \ + --use_binskim_compliant_compile_flags \ + --use_webgpu shared_lib \ + --wgsl_template static \ + --disable_rtti \ + --enable_lto \ + --enable_onnx_tests \ + --use_vcpkg \ + --use_vcpkg_ms_internal_asset_cache \ + --update \ + --build \ + --cmake_extra_defines onnxruntime_BUILD_UNIT_TESTS=ON ${EXTRA_CMAKE_DEFINES}"