diff --git a/include/onnxruntime/ep/adapter/op_kernel_info.h b/include/onnxruntime/ep/adapter/op_kernel_info.h index 7e61385f3686c..b2ebb37b617ff 100644 --- a/include/onnxruntime/ep/adapter/op_kernel_info.h +++ b/include/onnxruntime/ep/adapter/op_kernel_info.h @@ -12,9 +12,9 @@ #include "core/common/narrow.h" #include "core/common/status.h" #include "core/framework/config_options.h" -#include "core/framework/op_kernel_info.h" #include "core/framework/tensor_shape.h" #include "core/framework/tensor.h" +#include "core/session/allocator_adapters.h" #include "node.h" #include "kernel_def.h" @@ -43,12 +43,10 @@ struct OpKernelInfo { // to manage the lifetime of the cached data. struct KernelInfoCache { explicit KernelInfoCache(const OrtKernelInfo* kernel_info) : kernel_info_(kernel_info) { - const auto* core_kernel_info = reinterpret_cast(kernel_info); - execution_provider_ = core_kernel_info->GetExecutionProvider(); - ort_ep_ = execution_provider_ != nullptr ? execution_provider_->GetOrtEp() : nullptr; - ep_impl_ = ort_ep_ != nullptr ? (static_cast(ort_ep_))->EpImpl() : execution_provider_; - Ort::ConstKernelInfo info{kernel_info}; + ort_ep_ = info.GetEp(); + ep_impl_ = ort_ep_ != nullptr ? (static_cast(ort_ep_))->EpImpl() : nullptr; + const size_t input_count = info.GetInputCount(); constant_input_tensors.resize(input_count); for (size_t i = 0; i < input_count; ++i) { @@ -60,7 +58,6 @@ struct OpKernelInfo { } } const OrtKernelInfo* kernel_info_; - const ::onnxruntime::IExecutionProvider* execution_provider_{}; const OrtEp* ort_ep_{}; const ::onnxruntime::IExecutionProvider* ep_impl_{}; std::vector constant_input_tensors; @@ -74,11 +71,10 @@ struct OpKernelInfo { return (static_cast(cache_->ort_ep_))->GetDataTransferManager(); } - // Delegates to the core OpKernelInfo::GetAllocator so the adapter returns - // exactly the same allocator the framework would provide for each OrtMemType. AllocatorPtr GetAllocator(OrtMemType mem_type) const { - const auto* core_kernel_info = reinterpret_cast(cache_->kernel_info_); - return core_kernel_info->GetAllocator(mem_type); + OrtAllocator* ort_allocator = nullptr; + Ort::ThrowOnError(Ort::GetApi().KernelInfoGetAllocator(cache_->kernel_info_, mem_type, &ort_allocator)); + return std::make_shared(ort_allocator); } Node node() const noexcept { diff --git a/plugin-ep-webgpu/README.md b/plugin-ep-webgpu/README.md new file mode 100644 index 0000000000000..dd874f8af1c3b --- /dev/null +++ b/plugin-ep-webgpu/README.md @@ -0,0 +1,47 @@ +# WebGPU Plugin Execution Provider + +Packaging sources for the ONNX Runtime WebGPU plugin Execution Provider (EP), distributed as a standalone artifact +that plugs into an existing ONNX Runtime installation rather than being built into the main `onnxruntime` binary. + +For more information about plugin EPs, see the documentation [here](https://onnxruntime.ai/docs/execution-providers/plugin-ep-libraries/). + +## Contents + +- [`VERSION_NUMBER`](VERSION_NUMBER) — Base plugin EP version consumed by the CI pipeline. The pipeline derives the + final package version (release, dev) from this via + [`tools/ci_build/github/azure-pipelines/templates/set-plugin-build-variables-step.yml`](../tools/ci_build/github/azure-pipelines/templates/set-plugin-build-variables-step.yml). +- [`python/`](python/) — Sources and build script for the `onnxruntime-ep-webgpu` Python wheel. See + [`python/README.md`](python/README.md) for build and test instructions. + +## How it fits together + +The plugin EP is built as a shared library (`onnxruntime_providers_webgpu.{dll,so,dylib}`) by the main ONNX Runtime +build (`--use_webgpu shared_lib`). The resulting binaries are then packaged into: + +- A Python wheel (`onnxruntime-ep-webgpu`), built from [`python/`](python/). +- A universal package published to the internal ORT-Nightly feed for Windows (x64 / arm64), Linux x64, and macOS + arm64. + +Packaging is driven by the `WebGPU Plugin EP Packaging Pipeline` +([`tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml`](../tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml)), +and post-build smoke tests run in the companion `WebGPU Plugin EP Test Pipeline` +([`tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml`](../tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml)). + +## Usage + +Once installed, the plugin EP is registered at runtime: + +```python +import onnxruntime as ort +import onnxruntime_ep_webgpu as webgpu_ep + +ort.register_execution_provider_library("webgpu", webgpu_ep.get_library_path()) + +devices = [d for d in ort.get_ep_devices() if d.ep_name == webgpu_ep.get_ep_name()] +sess_options = ort.SessionOptions() +sess_options.add_provider_for_devices(devices, {}) +session = ort.InferenceSession("model.onnx", sess_options=sess_options) +``` + +See [`python/onnxruntime_ep_webgpu/README.md`](python/onnxruntime_ep_webgpu/README.md) for the user-facing package +documentation (this README is bundled into the wheel). diff --git a/plugin-ep-webgpu/VERSION_NUMBER b/plugin-ep-webgpu/VERSION_NUMBER new file mode 100644 index 0000000000000..5ff8c4f5d2ad2 --- /dev/null +++ b/plugin-ep-webgpu/VERSION_NUMBER @@ -0,0 +1 @@ +1.26.0 diff --git a/plugin-ep-webgpu/python/README.md b/plugin-ep-webgpu/python/README.md new file mode 100644 index 0000000000000..efca2f1ee7678 --- /dev/null +++ b/plugin-ep-webgpu/python/README.md @@ -0,0 +1,58 @@ +# WebGPU Plugin EP Python Package + +This directory contains the packaging source for the `onnxruntime-ep-webgpu` Python package. + +## Prerequisites + +- Python 3.11+ +- Pre-built WebGPU plugin EP binaries (from CI or a local build) + +Install build dependencies: + +```bash +pip install -r requirements-build-wheel.txt +``` + +## Building the wheel + +```bash +python build_wheel.py \ + --binary_dir \ + --version \ + --output_dir +``` + +Example: + +```bash +python build_wheel.py \ + --binary_dir ./build/Release \ + --version 1.26.0.dev20260410 \ + --output_dir ./dist +``` + +The script combines the pre-built plugin EP binaries with the package source to produce a platform-specific wheel. + +## Testing + +Install the wheel and dependencies in a clean environment, then run the smoke test: + +```bash +python -m venv test_venv +source test_venv/bin/activate # or test_venv\Scripts\Activate.ps1 on Windows +pip install onnx numpy +pip install dist/onnxruntime_ep_webgpu-*.whl # pulls in onnxruntime>=1.24.4 +python test/test_webgpu_plugin_ep.py +``` + +The wheel declares a runtime dependency on the minimum compatible `onnxruntime` package, so pip will install (or +verify) a compatible core runtime automatically. + +The test validates import, EP registration, device discovery, and inference (requires WebGPU-capable hardware for the +inference portion). Set the environment variable `ORT_TEST_VERBOSE=1` to print additional diagnostic information +(environment, available providers, discovered devices, etc.). + +## Versioning + +The package version is derived from `plugin-ep-webgpu/VERSION_NUMBER` by the packaging pipeline, which produces a +PEP 440 version string. diff --git a/plugin-ep-webgpu/python/build_wheel.py b/plugin-ep-webgpu/python/build_wheel.py new file mode 100644 index 0000000000000..9eb07aa8c2d69 --- /dev/null +++ b/plugin-ep-webgpu/python/build_wheel.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Build a wheel for the onnxruntime-ep-webgpu package. + +Combines pre-built plugin EP binaries with the Python package source to produce +a platform-specific wheel. + +Usage: + python build_wheel.py --binary_dir --version --output_dir +""" + +import argparse +import platform +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +SCRIPT_DIR = Path(__file__).parent + +# Patterns for binaries to include in the package +BINARY_PATTERNS = [ + "onnxruntime_providers_webgpu.dll", + "libonnxruntime_providers_webgpu.so", + "libonnxruntime_providers_webgpu.dylib", + # DXC dependencies (Windows) + "dxil.dll", + "dxcompiler.dll", + # Dawn shared library (if built as shared) + "webgpu_dawn.dll", + "libwebgpu_dawn.so", + "libwebgpu_dawn.dylib", +] + +# Libraries to exclude from auditwheel bundling (user-provided drivers) +AUDITWHEEL_EXCLUDE = [ + "libvulkan.so.1", +] + + +def prepare_staging_dir(staging_dir: Path, binary_dir: Path, version: str): + """Copy the package source tree into staging_dir, copy binaries, and stamp the version.""" + staging_dir.mkdir(parents=True, exist_ok=True) + + # Copy only the files needed to build the wheel + shutil.copy2(SCRIPT_DIR / "pyproject.toml", staging_dir / "pyproject.toml") + shutil.copy2(SCRIPT_DIR / "setup.py", staging_dir / "setup.py") + shutil.copytree(SCRIPT_DIR / "onnxruntime_ep_webgpu", staging_dir / "onnxruntime_ep_webgpu") + + # Copy plugin binaries into the package directory + package_dir = staging_dir / "onnxruntime_ep_webgpu" + copied = [] + for pattern in BINARY_PATTERNS: + for src in binary_dir.glob(pattern): + dst = package_dir / src.name + print(f"Copying {src} -> {dst}") + shutil.copy2(src, dst) + copied.append(dst) + if not copied: + print(f"ERROR: No plugin binaries found in {binary_dir}", file=sys.stderr) + print(f"Looked for: {BINARY_PATTERNS}", file=sys.stderr) + sys.exit(1) + + # Stamp the version in pyproject.toml + pyproject_path = staging_dir / "pyproject.toml" + content = pyproject_path.read_text(encoding="utf-8") + placeholder = 'version = "VERSION_PLACEHOLDER"' + if placeholder not in content: + print(f"ERROR: Version placeholder not found in pyproject.toml. Expected: {placeholder}", file=sys.stderr) + sys.exit(1) + updated = content.replace(placeholder, f'version = "{version}"') + pyproject_path.write_text(updated, encoding="utf-8") + + +def build_wheel(source_dir: Path, wheel_dir: Path): + """Build the wheel using pip.""" + wheel_dir.mkdir(parents=True, exist_ok=True) + cmd = [ + sys.executable, + "-m", + "pip", + "wheel", + str(source_dir), + "--wheel-dir", + str(wheel_dir), + "--no-deps", + "--no-build-isolation", + ] + print(f"Running: {' '.join(cmd)}") + subprocess.check_call(cmd) + + +def auditwheel_repair(wheel_dir: Path): + """Run auditwheel repair on Linux to produce a manylinux-compliant wheel.""" + if platform.system() != "Linux": + return + + raw_wheels = wheel_dir.glob("onnxruntime_ep_webgpu-*.whl") + if not raw_wheels: + return + + raw_wheel_list = list(raw_wheels) + if not raw_wheel_list: + return + + with tempfile.TemporaryDirectory() as repaired_dir_name: + repaired_dir = Path(repaired_dir_name) + + for wheel in raw_wheel_list: + cmd = [sys.executable, "-m", "auditwheel", "repair", str(wheel), "--wheel-dir", str(repaired_dir)] + for lib in AUDITWHEEL_EXCLUDE: + cmd.extend(["--exclude", lib]) + print(f"Running: {' '.join(cmd)}") + subprocess.check_call(cmd) + # Remove the raw wheel so only the repaired one remains + wheel.unlink() + + # Move repaired wheels into wheel_dir + for repaired_wheel in repaired_dir.glob("*.whl"): + repaired_wheel.replace(wheel_dir / repaired_wheel.name) + + +def collect_wheels(wheel_dir: Path, output_dir: Path): + """Copy built wheels to the output directory and verify at least one was produced.""" + wheels = wheel_dir.glob("onnxruntime_ep_webgpu-*.whl") + if not wheels: + print("ERROR: No wheel was produced", file=sys.stderr) + sys.exit(1) + + output_dir.mkdir(parents=True, exist_ok=True) + + for wheel in wheels: + dest = output_dir / wheel.name + shutil.copy2(wheel, dest) + print(f"Built wheel: {dest}") + + +def main(): + parser = argparse.ArgumentParser(description="Build onnxruntime-ep-webgpu wheel") + parser.add_argument( + "--binary_dir", required=True, type=Path, help="Directory containing the built plugin EP binaries" + ) + parser.add_argument("--version", required=True, help="Package version string (PEP 440 format)") + parser.add_argument("--output_dir", required=True, type=Path, help="Directory to place the built wheel") + args = parser.parse_args() + + if not args.binary_dir.is_dir(): + print(f"ERROR: Binary directory does not exist: {args.binary_dir}", file=sys.stderr) + sys.exit(1) + + with tempfile.TemporaryDirectory(prefix="ort_webgpu_wheel_") as tmp: + staging_dir = Path(tmp) / "package" + wheel_dir = Path(tmp) / "wheels" + + prepare_staging_dir(staging_dir, args.binary_dir, args.version) + build_wheel(staging_dir, wheel_dir) + auditwheel_repair(wheel_dir) + collect_wheels(wheel_dir, args.output_dir) + + +if __name__ == "__main__": + main() diff --git a/plugin-ep-webgpu/python/onnxruntime_ep_webgpu/README.md b/plugin-ep-webgpu/python/onnxruntime_ep_webgpu/README.md new file mode 100644 index 0000000000000..3200f0dd08ff0 --- /dev/null +++ b/plugin-ep-webgpu/python/onnxruntime_ep_webgpu/README.md @@ -0,0 +1,31 @@ +# ONNX Runtime WebGPU Plugin Execution Provider + +WebGPU Execution Provider plugin for ONNX Runtime. Install alongside `onnxruntime` to enable WebGPU acceleration. + +## Installation + +```bash +pip install onnxruntime-ep-webgpu +``` + +## Usage + +```python +import onnxruntime as ort +import onnxruntime_ep_webgpu as webgpu_ep + +# Register the plugin EP library with ONNX Runtime +ort.register_execution_provider_library("webgpu", webgpu_ep.get_library_path()) + +# Discover WebGPU devices +all_devices = ort.get_ep_devices() +webgpu_devices = [d for d in all_devices if d.ep_name == webgpu_ep.get_ep_name()] + +# Create a session using the WebGPU EP +sess_options = ort.SessionOptions() +sess_options.add_provider_for_devices(webgpu_devices, {}) +session = ort.InferenceSession("model.onnx", sess_options=sess_options) + +# Run inference +output = session.run(None, {"input": input_data}) +``` diff --git a/plugin-ep-webgpu/python/onnxruntime_ep_webgpu/__init__.py b/plugin-ep-webgpu/python/onnxruntime_ep_webgpu/__init__.py new file mode 100644 index 0000000000000..284269eb0356a --- /dev/null +++ b/plugin-ep-webgpu/python/onnxruntime_ep_webgpu/__init__.py @@ -0,0 +1,43 @@ +"""ONNX Runtime WebGPU Plugin Execution Provider Python Package. + +Provides helper functions to locate the plugin EP shared library and +retrieve the EP name for registration with ONNX Runtime. +""" + +from __future__ import annotations + +import pathlib + +__all__ = [ + "get_ep_name", + "get_ep_names", + "get_library_path", +] + +_module_dir = pathlib.Path(__file__).parent + + +def get_library_path() -> str: + """Return the path to the WebGPU plugin EP shared library.""" + candidate_paths = [ + _module_dir / "onnxruntime_providers_webgpu.dll", + _module_dir / "libonnxruntime_providers_webgpu.so", + _module_dir / "libonnxruntime_providers_webgpu.dylib", + ] + paths = [p for p in candidate_paths if p.is_file()] + if len(paths) != 1: + raise RuntimeError( + f"Expected exactly one WebGPU plugin EP library in {_module_dir}, " + f"found {len(paths)}: {[p.name for p in paths]}" + ) + return str(paths[0]) + + +def get_ep_name() -> str: + """Return the WebGPU Execution Provider name.""" + return "WebGpuExecutionProvider" + + +def get_ep_names() -> list[str]: + """Return a list of EP names provided by this plugin.""" + return [get_ep_name()] diff --git a/plugin-ep-webgpu/python/pyproject.toml b/plugin-ep-webgpu/python/pyproject.toml new file mode 100644 index 0000000000000..98fd472c1b76f --- /dev/null +++ b/plugin-ep-webgpu/python/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=68.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "onnxruntime-ep-webgpu" +version = "VERSION_PLACEHOLDER" # Replaced at build time by build_wheel.py +description = "ONNX Runtime WebGPU Plugin Execution Provider" +readme = "onnxruntime_ep_webgpu/README.md" +license = {text = "MIT"} +requires-python = ">=3.11" +dependencies = [ + "onnxruntime>=1.24.4", +] + +[tool.setuptools.packages.find] +include = ["onnxruntime_ep_webgpu*"] + +[tool.setuptools.package-data] +onnxruntime_ep_webgpu = ["*.dll", "*.so", "*.so.*", "*.dylib"] diff --git a/plugin-ep-webgpu/python/requirements-build-wheel.txt b/plugin-ep-webgpu/python/requirements-build-wheel.txt new file mode 100644 index 0000000000000..6421330bc6b8c --- /dev/null +++ b/plugin-ep-webgpu/python/requirements-build-wheel.txt @@ -0,0 +1,5 @@ +setuptools>=68.0 +wheel +# Linux-only (auditwheel + patchelf are needed for manylinux compliance) +auditwheel; sys_platform == "linux" +patchelf; sys_platform == "linux" diff --git a/plugin-ep-webgpu/python/setup.py b/plugin-ep-webgpu/python/setup.py new file mode 100644 index 0000000000000..1408047fcb887 --- /dev/null +++ b/plugin-ep-webgpu/python/setup.py @@ -0,0 +1,26 @@ +"""Minimal setup.py to produce a platform-specific wheel. + +The package contains pre-built native libraries (not CPython extension modules), +so the wheel tag should be py3-none-{platform} rather than cp3XX-cp3XX-{platform}. +This means a single wheel works across all supported Python versions. +""" + +from setuptools import setup +from setuptools.dist import Distribution +from wheel.bdist_wheel import bdist_wheel + + +class PlatformBdistWheel(bdist_wheel): + """Override wheel tags to py3-none-{platform}.""" + + def get_tag(self): + _, _, plat = super().get_tag() + return "py3", "none", plat + + +class BinaryDistribution(Distribution): + def has_ext_modules(self): + return True + + +setup(distclass=BinaryDistribution, cmdclass={"bdist_wheel": PlatformBdistWheel}) diff --git a/plugin-ep-webgpu/python/test/test_webgpu_plugin_ep.py b/plugin-ep-webgpu/python/test/test_webgpu_plugin_ep.py new file mode 100644 index 0000000000000..33d75c7510d46 --- /dev/null +++ b/plugin-ep-webgpu/python/test/test_webgpu_plugin_ep.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Smoke test for the onnxruntime-ep-webgpu Python package. + +Tests: +1. Package import and library path resolution +2. EP registration with ONNX Runtime +3. Device discovery +4. Inference with a simple Mul model (requires WebGPU-capable hardware) + +The inference test is skipped gracefully if no WebGPU device is available +(e.g., on CPU-only build agents). +""" + +import os +import platform +import sys +import tempfile +import traceback +from pathlib import Path + +import numpy as np +import onnx +from onnx import TensorProto, helper + +import onnxruntime as ort + +VERBOSE = os.environ.get("ORT_TEST_VERBOSE", "").strip().lower() in ("1", "true", "yes") + + +def debug_print(*args, **kwargs): + """Print only when ORT_TEST_VERBOSE is set to a truthy value.""" + if VERBOSE: + print(*args, **kwargs) + + +def create_mul_model() -> str: + """Create a simple Mul model and return the path to the saved .onnx file.""" + x = helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3]) + y = helper.make_tensor_value_info("y", TensorProto.FLOAT, [2, 3]) + z = helper.make_tensor_value_info("z", TensorProto.FLOAT, [2, 3]) + + mul_node = helper.make_node("Mul", inputs=["x", "y"], outputs=["z"]) + + graph = helper.make_graph([mul_node], "mul_graph", [x, y], [z]) + model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)]) + model.ir_version = 7 + + model_path = Path(tempfile.mkdtemp()) / "mul.onnx" + onnx.save(model, str(model_path)) + return str(model_path) + + +def print_environment_info(): + """Print diagnostic information about the runtime environment.""" + print(f" Python: {sys.version}") + print(f" Platform: {platform.platform()}") + print(f" Architecture: {platform.machine()}") + print(f" ONNX Runtime version: {ort.__version__}") + print(f" ONNX Runtime location: {ort.__file__}") + print(f" Available providers (built-in): {ort.get_available_providers()}") + # Print relevant environment variables + for var in sorted(os.environ): + lower = var.lower() + if any(kw in lower for kw in ["onnx", "ort", "gpu", "cuda", "vulkan", "webgpu", "dawn", "path", "ld_library"]): + print(f" ENV {var}={os.environ[var]}") + + +def test_import_and_library_path(): + """Test that the package imports and the library path is valid.""" + import onnxruntime_ep_webgpu as webgpu_ep # noqa: PLC0415 # `import` should be at the top-level of a file. + + debug_print(f" Package location: {webgpu_ep.__file__}") + pkg_dir = Path(webgpu_ep.__file__).parent + debug_print(f" Package directory contents: {sorted(p.name for p in pkg_dir.iterdir())}") + + lib_path = webgpu_ep.get_library_path() + assert Path(lib_path).is_file(), f"Library path does not exist: {lib_path}" + print(f"OK: Library path: {lib_path}") + + ep_name = webgpu_ep.get_ep_name() + assert ep_name == "WebGpuExecutionProvider", f"Unexpected EP name: {ep_name}" + print(f"OK: EP name: {ep_name}") + + ep_names = webgpu_ep.get_ep_names() + assert ep_names == ["WebGpuExecutionProvider"], f"Unexpected EP names: {ep_names}" + print(f"OK: EP names: {ep_names}") + + +def test_registration_and_inference(): + """Test EP registration, device discovery, and inference.""" + import onnxruntime_ep_webgpu as webgpu_ep # noqa: PLC0415 # `import` should be at the top-level of a file. + + lib_path = webgpu_ep.get_library_path() + ep_name = webgpu_ep.get_ep_name() + registration_name = "webgpu_plugin_test" + + # Register the plugin EP + debug_print(f" Registering library: {lib_path}") + debug_print(f" Library file size: {Path(lib_path).stat().st_size} bytes") + ort.register_execution_provider_library(registration_name, lib_path) + print(f"OK: Registered EP library as '{registration_name}'") + + try: + # Discover devices + all_devices = ort.get_ep_devices() + debug_print(f" All devices: {[(d.ep_name, getattr(d, 'device_id', 'N/A')) for d in all_devices]}") + webgpu_devices = [d for d in all_devices if d.ep_name == ep_name] + print(f"Found {len(webgpu_devices)} WebGPU device(s)") + + if not webgpu_devices: + print("SKIP: No WebGPU devices available — skipping inference test") + return + + # Create session with WebGPU EP + sess_options = ort.SessionOptions() + sess_options.add_session_config_entry("session.disable_cpu_ep_fallback", "1") + sess_options.add_provider_for_devices(webgpu_devices, {}) + assert sess_options.has_providers(), "SessionOptions should have providers after add_provider_for_devices" + print("OK: Session options configured with WebGPU EP") + + model_path = create_mul_model() + debug_print(f" Model path: {model_path}") + sess = ort.InferenceSession(model_path, sess_options=sess_options) + debug_print(f" Session providers: {sess.get_providers()}") + print("OK: InferenceSession created") + + # Run inference + x = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float32) + y = np.array([[2.0, 3.0, 4.0], [5.0, 6.0, 7.0]], dtype=np.float32) + expected = x * y + + outputs = sess.run(None, {"x": x, "y": y}) + result = outputs[0] + + np.testing.assert_allclose(result, expected, rtol=1e-5, atol=1e-5) + print("OK: Inference result matches expected output") + + del sess + print("OK: Session released") + + finally: + ort.unregister_execution_provider_library(registration_name) + print(f"OK: Unregistered EP library '{registration_name}'") + + +def main(): + print("=== WebGPU Plugin EP Python Package Test ===") + + if VERBOSE: + # Set verbose ORT logging so ORT internals are visible in CI logs + ort.set_default_logger_severity(0) + + print("\n--- Environment ---") + print_environment_info() + + print("\n--- Test 1: Import and library path ---") + test_import_and_library_path() + + print("\n--- Test 2: Registration and inference ---") + test_registration_and_inference() + + print("\n=== All tests passed ===") + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"\nFAILED: {e}", file=sys.stderr) + traceback.print_exc() + sys.exit(1) diff --git a/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml b/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml index 909010d5b2552..7d9f7c24b3360 100644 --- a/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml @@ -1,3 +1,8 @@ +# Packaging pipeline for the WebGPU EP plugin. This pipeline only builds +# and publishes artifacts. Tests that consume those artifacts live in +# plugin-webgpu-test-pipeline.yml, which is resource-triggered on +# successful runs of this pipeline. + trigger: none schedules: @@ -55,6 +60,9 @@ parameters: - MinSizeRel variables: + # Path (relative to the repository root) of the VERSION_NUMBER file used for plugin package versioning. + - name: epVersionFile + value: plugin-ep-webgpu/VERSION_NUMBER # 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)) }} @@ -122,4 +130,5 @@ extends: build_linux_x64: ${{ parameters.build_linux_x64 }} build_macos_arm64: ${{ parameters.build_macos_arm64 }} package_version: ${{ parameters.package_version }} + version_file: ${{ variables.epVersionFile }} cmake_build_type: ${{ parameters.cmake_build_type }} diff --git a/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml b/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml new file mode 100644 index 0000000000000..c322437bf7c9f --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml @@ -0,0 +1,97 @@ +# This pipeline runs tests against artifacts produced by the WebGPU +# plugin packaging pipeline. It is resource-triggered on successful +# packaging runs and can also be queued manually against any prior +# packaging run. +# +# Split from the packaging pipeline so the test side (Dockerfile, Vulkan +# configuration, test scripts) can be iterated on without rebuilding +# Dawn/WebGPU from source. + +trigger: none + +variables: +- name: DisableDockerDetector + value: true +- name: skipNugetSecurityAnalysis + value: true +- name: Codeql.SkipTaskAutoInjection + value: true + +resources: + pipelines: + - pipeline: build + source: 'WebGPU Plugin EP Packaging Pipeline' + trigger: true + repositories: + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +parameters: +- name: test_windows_x64 + displayName: 'Test Windows x64' + type: boolean + default: true + +# Note: Windows ARM64 is not tested here because the test runs on an x64 +# build agent, which cannot execute ARM64 binaries. + +- name: test_linux_x64 + displayName: 'Test Linux x64' + type: boolean + default: true + +- name: test_macos_arm64 + displayName: 'Test macOS ARM64' + type: boolean + default: true + +extends: + # The pipeline extends the 1ES PT which will inject SDL and compliance + # tasks. Uses "Official" to stay consistent with the companion + # WebGPU plugin packaging pipeline. + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + settings: + networkIsolationPolicy: Permissive + sdl: + # No top-level `pool:` is declared for this pipeline (each stage + # template pins its own pool), so source analysis needs an + # explicit pool. + sourceAnalysisPool: + name: onnxruntime-Win-CPU-VS2022-Latest + os: windows + 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 is intentionally omitted: this pipeline does + # not produce or publish binaries. The wheels it consumes were + # already signed-and-validated by the packaging pipeline. + 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' + + stages: + # Windows x64 + - ${{ if eq(parameters.test_windows_x64, true) }}: + - template: stages/plugin-win-webgpu-test-stage.yml + parameters: + arch: 'x64' + + # Linux x64 + - ${{ if eq(parameters.test_linux_x64, true) }}: + - template: stages/plugin-linux-webgpu-test-stage.yml + + # macOS ARM64 + - ${{ if eq(parameters.test_macos_arm64, true) }}: + - template: stages/plugin-mac-webgpu-test-stage.yml 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 index 357bd6027b854..8cc63aaa0fdec 100644 --- 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 @@ -3,10 +3,17 @@ parameters: type: string default: 'onnxruntime-Ubuntu2404-AMD-CPU' +- name: gpu_machine_pool + type: string + default: 'Onnxruntime-Linux-GPU-A10' + - name: package_version type: string default: dev +- name: version_file + type: string + - name: cmake_build_type type: string default: 'Release' @@ -18,7 +25,7 @@ parameters: - name: docker_base_image type: string - default: 'onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cuda12_x64_almalinux8_gcc14:20251017.1' + default: 'onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cpu_x64_almalinux8_gcc14:20251017.1' stages: - stage: Linux_plugin_webgpu_x64_Build @@ -48,13 +55,16 @@ stages: - template: ../templates/set-plugin-build-variables-step.yml parameters: package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} + + - template: ../templates/setup-feeds-and-python-steps.yml - template: ../templates/setup-feeds-and-python-steps.yml - 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 + Dockerfile: tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/Dockerfile + Context: tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu DockerBuildArgs: "--build-arg BASEIMAGE=${{ parameters.docker_base_image }} --build-arg BUILD_UID=$( id -u )" Repository: onnxruntimewebgpuplugin @@ -96,3 +106,53 @@ stages: vstsFeedPackagePublish: 'onnxruntime-plugin-ep-webgpu-linux-x64' versionOption: custom versionPublish: '$(PluginUniversalPackageVersion)' + + # Python package build job + - job: Linux_plugin_webgpu_x64_Python_Package + dependsOn: Linux_plugin_webgpu_x64_Build + timeoutInMinutes: 60 + workspace: + clean: all + pool: + name: ${{ parameters.machine_pool }} + os: linux + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/python + artifactName: webgpu_plugin_python_linux_x64 + variables: + - template: ../templates/common-variables.yml + steps: + - checkout: self + clean: true + submodules: none + + - template: ../templates/set-nightly-build-option-variable-step.yml + + - template: ../templates/set-plugin-build-variables-step.yml + parameters: + package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} + + - template: ../templates/setup-feeds-and-python-steps.yml + + - template: ../templates/get-docker-image-steps.yml + parameters: + Dockerfile: tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/Dockerfile + Context: tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu + DockerBuildArgs: "--build-arg BASEIMAGE=${{ parameters.docker_base_image }} --build-arg BUILD_UID=$( id -u )" + Repository: onnxruntimewebgpuplugin + + - task: DownloadPipelineArtifact@2 + displayName: 'Download plugin build artifacts' + inputs: + artifactName: webgpu_plugin_linux_x64 + targetPath: '$(Build.BinariesDirectory)/plugin_artifacts' + + - script: | + set -e -x + $(Build.SourcesDirectory)/tools/ci_build/github/linux/build_webgpu_plugin_python_package.sh \ + -i onnxruntimewebgpuplugin \ + -v "$(PluginPythonPackageVersion)" + displayName: 'Build Python wheel' diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml new file mode 100644 index 0000000000000..9ce494d4b3a36 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml @@ -0,0 +1,79 @@ +parameters: +- name: machine_pool + type: string + default: 'onnxruntime-Ubuntu2404-AMD-CPU' + +- name: docker_base_image + type: string + default: 'onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cpu_x64_almalinux8_gcc14:20251017.1' + +stages: +# Test stage. +# +# This stage runs against a software Vulkan implementation (SwiftShader, +# built from source in the Docker image). It does not require a GPU agent, +# so it uses the standard CPU pool. The ICD selection is pinned at +# `docker run` time via VK_ICD_FILENAMES / VK_DRIVER_FILES (see below) so +# the image remains reusable for a potential future real-GPU test job. +- stage: Linux_plugin_webgpu_x64_Test + dependsOn: [] + jobs: + - job: Linux_plugin_webgpu_x64_Python_Test + timeoutInMinutes: 60 + workspace: + clean: all + pool: + name: ${{ parameters.machine_pool }} + os: linux + steps: + - checkout: self + clean: true + submodules: none + + - template: ../templates/setup-feeds-and-python-steps.yml + + - template: ../templates/get-docker-image-steps.yml + parameters: + Dockerfile: tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/Dockerfile + Context: tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu + DockerBuildArgs: "--build-arg BASEIMAGE=${{ parameters.docker_base_image }} --build-arg BUILD_UID=$( id -u )" + Repository: onnxruntimewebgpuplugin + + # Download the Python wheel produced by the packaging pipeline run that + # triggered this pipeline (or that was selected at queue time). + - download: build + artifact: webgpu_plugin_python_linux_x64 + displayName: 'Download Python wheel' + + - script: | + set -e -x + mkdir -p "$(Build.BinariesDirectory)/python_wheel" + cp -R "$(Pipeline.Workspace)/build/webgpu_plugin_python_linux_x64/"* "$(Build.BinariesDirectory)/python_wheel/" + displayName: 'Stage Python wheel for test container' + + - script: | + set -e -x + # Pin Vulkan to SwiftShader (software Vulkan, built from source in + # the Docker image) so the test does not require a GPU agent. + # Keeping these env vars at `docker run` time (rather than baking + # them into the image) leaves the image reusable for a potential + # future real-GPU test job. + swiftshader_icd=/opt/swiftshader/vk_swiftshader_icd.json + docker run --rm \ + --volume "$(Build.SourcesDirectory):/onnxruntime_src" \ + --volume "$(Build.BinariesDirectory):/build" \ + --env "PIP_INDEX_URL=${PIP_INDEX_URL}" \ + --env "VK_ICD_FILENAMES=${swiftshader_icd}" \ + --env "VK_DRIVER_FILES=${swiftshader_icd}" \ + --env "ORT_TEST_VERBOSE=$(System.Debug)" \ + onnxruntimewebgpuplugin \ + /bin/bash -c " + set -e -x + python3 -m venv /build/test_venv + source /build/test_venv/bin/activate + python3 -m pip install onnxruntime onnx numpy + wheel=\$(find /build/python_wheel -name 'onnxruntime_ep_webgpu-*.whl' | head -1) + python3 -m pip install \"\$wheel\" + python3 -u /onnxruntime_src/plugin-ep-webgpu/python/test/test_webgpu_plugin_ep.py + " + displayName: 'Install and test Python package' 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 index 16e16e54fd236..eda45406f2480 100644 --- 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 @@ -3,6 +3,9 @@ parameters: type: string default: dev +- name: version_file + type: string + - name: cmake_build_type type: string default: 'Release' @@ -52,6 +55,7 @@ stages: - template: ../templates/set-plugin-build-variables-step.yml parameters: package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} - script: | set -e -x @@ -88,6 +92,25 @@ stages: libonnxruntime_providers_webgpu.dylib TargetFolder: '$(Build.ArtifactStagingDirectory)/bin' + - script: | + set -e -x + zip webgpu_plugin_ep_binaries.zip libonnxruntime_providers_webgpu.dylib + displayName: 'Zip plugin binary for signing' + workingDirectory: '$(Build.ArtifactStagingDirectory)/bin' + + - template: ../templates/mac-esrp-dylib.yml + parameters: + FolderPath: '$(Build.ArtifactStagingDirectory)/bin' + Pattern: 'webgpu_plugin_ep_binaries.zip' + + - script: | + set -e -x + unzip -o webgpu_plugin_ep_binaries.zip + rm -- webgpu_plugin_ep_binaries.zip + codesign --display --verbose=3 libonnxruntime_providers_webgpu.dylib + displayName: 'Unzip and verify signed binary' + workingDirectory: '$(Build.ArtifactStagingDirectory)/bin' + - script: | set -e -x mkdir -p "$(Build.ArtifactStagingDirectory)/version" @@ -109,3 +132,52 @@ stages: vstsFeedPackagePublish: 'onnxruntime-plugin-ep-webgpu-macos-arm64' versionOption: custom versionPublish: '$(PluginUniversalPackageVersion)' + + # Python package build job + - job: MacOS_plugin_webgpu_arm64_Python_Package + dependsOn: MacOS_plugin_webgpu_arm64_Build + timeoutInMinutes: 30 + workspace: + clean: all + pool: + name: AcesShared + os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/python + artifactName: webgpu_plugin_python_macos_arm64 + variables: + - template: ../templates/common-variables.yml + steps: + - checkout: self + clean: true + submodules: none + + - 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 }} + version_file: ${{ parameters.version_file }} + + - task: DownloadPipelineArtifact@2 + displayName: 'Download plugin build artifacts' + inputs: + artifactName: webgpu_plugin_macos_arm64 + targetPath: '$(Build.BinariesDirectory)/plugin_artifacts' + + - script: | + set -e -x + python3 -m pip install -r "$(Build.SourcesDirectory)/plugin-ep-webgpu/python/requirements-build-wheel.txt" + python3 "$(Build.SourcesDirectory)/plugin-ep-webgpu/python/build_wheel.py" \ + --binary_dir "$(Build.BinariesDirectory)/plugin_artifacts/bin" \ + --version "$(PluginPythonPackageVersion)" \ + --output_dir "$(Build.ArtifactStagingDirectory)/python" + displayName: 'Build Python wheel' diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml new file mode 100644 index 0000000000000..5ad4e170b2855 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml @@ -0,0 +1,39 @@ +stages: +- stage: MacOS_plugin_webgpu_arm64_Test + dependsOn: [] + jobs: + - job: MacOS_plugin_webgpu_arm64_Python_Test + timeoutInMinutes: 30 + workspace: + clean: all + pool: + name: AcesShared + os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia + steps: + - checkout: self + clean: true + submodules: none + + - template: ../templates/setup-build-tools.yml + parameters: + host_cpu_arch: 'arm64' + + # Download the Python wheel produced by the packaging pipeline run that + # triggered this pipeline (or that was selected at queue time). + - download: build + artifact: webgpu_plugin_python_macos_arm64 + displayName: 'Download Python wheel' + + - script: | + set -e -x + python3 -m venv "$(Build.BinariesDirectory)/test_venv" + source "$(Build.BinariesDirectory)/test_venv/bin/activate" + python3 -m pip install onnxruntime onnx numpy + wheel=$(find "$(Pipeline.Workspace)/build/webgpu_plugin_python_macos_arm64" -name "onnxruntime_ep_webgpu-*.whl" | head -1) + python3 -m pip install "$wheel" + python3 -u "$(Build.SourcesDirectory)/plugin-ep-webgpu/python/test/test_webgpu_plugin_ep.py" + displayName: 'Install and test Python package' + env: + ORT_TEST_VERBOSE: $(System.Debug) 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 index 1864bb4016bb4..9db25f5727cc2 100644 --- 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 @@ -28,6 +28,9 @@ parameters: - release - RC +- name: version_file + type: string + - name: cmake_build_type type: string displayName: 'CMake build type' @@ -45,6 +48,7 @@ stages: parameters: arch: 'x64' package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} cmake_build_type: ${{ parameters.cmake_build_type }} # Windows ARM64 @@ -55,6 +59,7 @@ stages: parameters: arch: 'arm64' package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} cmake_build_type: ${{ parameters.cmake_build_type }} # Linux x64 @@ -62,6 +67,7 @@ stages: - template: plugin-linux-webgpu-stage.yml parameters: package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} cmake_build_type: ${{ parameters.cmake_build_type }} # macOS ARM64 @@ -69,4 +75,5 @@ stages: - template: plugin-mac-webgpu-stage.yml parameters: package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} 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 index 5a1498c8841f2..acad674143961 100644 --- 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 @@ -9,6 +9,9 @@ parameters: type: string default: dev +- name: version_file + type: string + - name: cmake_build_type type: string default: 'Release' @@ -82,6 +85,7 @@ stages: - template: ../templates/set-plugin-build-variables-step.yml parameters: package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} - script: | python -m pip install -r "$(Build.SourcesDirectory)\tools\ci_build\github\windows\python\requirements.txt" @@ -228,3 +232,56 @@ stages: vstsFeedPackagePublish: 'onnxruntime-plugin-ep-webgpu-win-${{ parameters.arch }}' versionOption: custom versionPublish: '$(PluginUniversalPackageVersion)' + + # Python package jobs (x64 only — arm64 cross-compiled binaries can't be + # packaged or tested on x64 agents) + - ${{ if eq(parameters.arch, 'x64') }}: + # Python package build job + - job: Win_plugin_webgpu_${{ parameters.arch }}_Python_Package + dependsOn: Win_plugin_webgpu_${{ parameters.arch }}_Build + timeoutInMinutes: 30 + workspace: + clean: all + pool: + name: onnxruntime-Win-CPU-VS2022-Latest + os: windows + templateContext: + outputs: + - output: pipelineArtifact + targetPath: '$(Build.ArtifactStagingDirectory)\python' + artifactName: webgpu_plugin_python_win_${{ parameters.arch }} + variables: + - template: ../templates/common-variables.yml + 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 }} + version_file: ${{ parameters.version_file }} + + - task: DownloadPipelineArtifact@2 + displayName: 'Download plugin build artifacts' + inputs: + artifactName: webgpu_plugin_win_${{ parameters.arch }} + targetPath: '$(Build.BinariesDirectory)\plugin_artifacts' + + - task: PowerShell@2 + displayName: 'Build Python wheel' + inputs: + targetType: inline + pwsh: true + script: | + python -m pip install -r "$(Build.SourcesDirectory)\plugin-ep-webgpu\python\requirements-build-wheel.txt" + python "$(Build.SourcesDirectory)\plugin-ep-webgpu\python\build_wheel.py" ` + --binary_dir "$(Build.BinariesDirectory)\plugin_artifacts\bin" ` + --version "$(PluginPythonPackageVersion)" ` + --output_dir "$(Build.ArtifactStagingDirectory)\python" diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml new file mode 100644 index 0000000000000..6664f7716eefa --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml @@ -0,0 +1,62 @@ +parameters: +- name: arch + type: string + values: + - x64 + - arm64 + +stages: +- stage: Win_plugin_webgpu_${{ parameters.arch }}_Test + dependsOn: [] + jobs: + - job: Win_plugin_webgpu_${{ parameters.arch }}_Python_Test + timeoutInMinutes: 30 + workspace: + clean: all + pool: + name: onnxruntime-Win2022-VS2022-webgpu-A10 + os: windows + steps: + - checkout: self + clean: true + submodules: none + + - template: ../templates/setup-build-tools.yml + parameters: + host_cpu_arch: ${{ parameters.arch }} + + # Download the Python wheel produced by the packaging pipeline run that + # triggered this pipeline (or that was selected at queue time). + - download: build + artifact: webgpu_plugin_python_win_${{ parameters.arch }} + displayName: 'Download Python wheel' + + - task: PowerShell@2 + displayName: 'Install and test Python package' + env: + ORT_TEST_VERBOSE: $(System.Debug) + inputs: + targetType: inline + pwsh: true + script: | + $ErrorActionPreference = 'Stop' + + echo "creating test_venv" + python -m venv "$(Build.BinariesDirectory)\test_venv" + + echo "activating test_venv" + & "$(Build.BinariesDirectory)\test_venv\Scripts\Activate.ps1" + + echo "installing onnxruntime onnx numpy" + python -m pip install onnxruntime onnx numpy + if ($LASTEXITCODE -ne 0) { throw "pip install onnxruntime onnx numpy failed with exit code $LASTEXITCODE" } + + $wheelDir = "$(Pipeline.Workspace)\build\webgpu_plugin_python_win_${{ parameters.arch }}" + $wheel = (Get-ChildItem "$wheelDir\onnxruntime_ep_webgpu-*.whl")[0] + echo "installing ${wheel}" + python -m pip install $wheel.FullName + if ($LASTEXITCODE -ne 0) { throw "pip install wheel failed with exit code $LASTEXITCODE" } + + echo "running test_webgpu_plugin_ep.py" + python -u "$(Build.SourcesDirectory)\plugin-ep-webgpu\python\test\test_webgpu_plugin_ep.py" + if ($LASTEXITCODE -ne 0) { throw "test_webgpu_plugin_ep.py failed with exit code $LASTEXITCODE (0x$($LASTEXITCODE.ToString('X')))" } 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 index 212eca44ae3ec..00e341e81e531 100644 --- 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 @@ -2,8 +2,20 @@ # variable based on the build type (nightly, official, or dev). parameters: +# The package version type: 'release', 'RC', or 'dev'. Controls how the final version +# string is derived from the contents of the version_file. - name: package_version type: string + values: + - release + - RC + - dev + +# Path, relative to the repository root, of the file containing the base version number +# (e.g. "plugin-ep-webgpu/VERSION_NUMBER"). The file should contain a single semver-like +# version string (e.g. "1.2.3"). +- name: version_file + type: string steps: # Set package version string @@ -18,11 +30,12 @@ steps: import sys package_version = "${{ parameters.package_version }}" + version_file_rel = "${{ parameters.version_file }}" src_root = os.environ.get("BUILD_SOURCESDIRECTORY", "") - version_file = os.path.join(src_root, "VERSION_NUMBER") + version_file = os.path.join(src_root, version_file_rel) if not os.path.isfile(version_file): - print("##vso[task.logissue type=error]Cannot find VERSION_NUMBER at: {}".format(version_file)) + print("##vso[task.logissue type=error]Cannot find version number file at: {}".format(version_file)) sys.exit(1) with open(version_file, "r") as f: @@ -38,6 +51,7 @@ steps: if package_version == "release": version_string = original_ver universal_version = original_ver + python_version = original_ver elif package_version == "RC": # RC versioning is not yet implemented. Fail the build to prevent publishing @@ -60,6 +74,7 @@ steps: sys.exit(1) version_string = "{}-dev.{}+{}".format(original_ver, date_str, commit_sha) universal_version = "{}-dev.{}.{}".format(original_ver, date_str, commit_sha) + python_version = "{}.dev{}".format(original_ver, date_str) else: print("##vso[task.logissue type=error]Unknown package_version '{}'. Must be 'release', 'RC', or 'dev'.".format(package_version)) @@ -67,6 +82,7 @@ steps: print("Plugin package version string: {}".format(version_string)) print("Plugin universal package version string: {}".format(universal_version)) + print("Plugin Python package version string: {}".format(python_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-]+)*))?$" @@ -80,6 +96,13 @@ steps: print("##vso[task.logissue type=error]Universal version string '{}' is not valid semver 1.0.0.".format(universal_version)) sys.exit(1) + # Validate Python version (PEP 440) + pep440_pattern = r"^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$" + if not re.match(pep440_pattern, python_version): + print("##vso[task.logissue type=error]Python version string '{}' is not valid PEP 440.".format(python_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=PluginPythonPackageVersion]{}".format(python_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_python_package.sh b/tools/ci_build/github/linux/build_webgpu_plugin_python_package.sh new file mode 100755 index 0000000000000..317f537df020b --- /dev/null +++ b/tools/ci_build/github/linux/build_webgpu_plugin_python_package.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -e -x + +# Build the onnxruntime-ep-webgpu Python wheel inside Docker. +# The Docker container provides a manylinux-compatible environment +# with the correct Python version and auditwheel support. + +DOCKER_IMAGE="onnxruntimewebgpuplugin" +VERSION="" + +while getopts "i:v:" parameter_Option +do case "${parameter_Option}" +in +i) DOCKER_IMAGE=${OPTARG};; +v) VERSION=${OPTARG};; +*) echo "Usage: $0 -i -v " + exit 1;; +esac +done + +if [ -z "$VERSION" ]; then + echo "ERROR: Version is required. Use -v " + exit 1 +fi + +docker run --rm \ + --volume "${BUILD_SOURCESDIRECTORY}:/onnxruntime_src" \ + --volume "${BUILD_BINARIESDIRECTORY}:/build" \ + --volume "${BUILD_ARTIFACTSTAGINGDIRECTORY}:/staging" \ + --env "PIP_INDEX_URL=${PIP_INDEX_URL}" \ + "$DOCKER_IMAGE" \ + /bin/bash -c " + set -e -x + python3 -m ensurepip + python3 -m pip install -r /onnxruntime_src/plugin-ep-webgpu/python/requirements-build-wheel.txt + python3 /onnxruntime_src/plugin-ep-webgpu/python/build_wheel.py \ + --binary_dir /build/plugin_artifacts/bin \ + --version "${VERSION}" \ + --output_dir /staging/python + " diff --git a/tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/Dockerfile b/tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/Dockerfile new file mode 100644 index 0000000000000..526c129556395 --- /dev/null +++ b/tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/Dockerfile @@ -0,0 +1,71 @@ +ARG BASEIMAGE=onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cpu_x64_almalinux8_gcc14:20251017.1 + +# --------------------------------------------------------------------------- +# Builder stage: build SwiftShader (Google's software Vulkan ICD) from source. +# +# Why SwiftShader instead of Mesa lavapipe? +# The AlmaLinux 8 base ships an old Mesa lavapipe that rejects Dawn's +# requested Vulkan apiVersion with VK_ERROR_INCOMPATIBLE_DRIVER. SwiftShader +# is maintained alongside Dawn for headless CI and is self-contained (one +# .so + ICD JSON, no Mesa/DRM dependency). +# +# SwiftShader has no release tags, so pin to a commit SHA. The ICD must +# advertise at least the Vulkan apiVersion Dawn requests; picking a SHA from +# Dawn's DEPS is a convenient way to get one known to satisfy that. +# --------------------------------------------------------------------------- +FROM $BASEIMAGE AS swiftshader_builder + +ARG SWIFTSHADER_COMMIT=b7b7fd22e5f28079b92412f47f6da4df43e4cd37 + +RUN dnf install -y git ninja-build && dnf clean all + +RUN git -c advice.detachedHead=false init /tmp/swiftshader \ + && cd /tmp/swiftshader \ + && git remote add origin https://swiftshader.googlesource.com/SwiftShader \ + && git fetch --depth 1 origin "${SWIFTSHADER_COMMIT}" \ + && git checkout FETCH_HEAD \ + && git submodule update --init --recursive --depth 1 + +RUN cmake -S /tmp/swiftshader -B /tmp/swiftshader/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DSWIFTSHADER_BUILD_TESTS=OFF \ + -DSWIFTSHADER_BUILD_PVR=OFF \ + -DSWIFTSHADER_WARNINGS_AS_ERRORS=OFF \ + && cmake --build /tmp/swiftshader/build --target vk_swiftshader + +# Stage the artifacts + rewrite the ICD JSON's library_path to an absolute +# path so the Vulkan loader can find the .so from any working directory. +RUN mkdir -p /opt/swiftshader \ + && cp /tmp/swiftshader/build/Linux/libvk_swiftshader.so /opt/swiftshader/ \ + && python3 <<'EOF' +import json +src = '/tmp/swiftshader/build/Linux/vk_swiftshader_icd.json' +dst = '/opt/swiftshader/vk_swiftshader_icd.json' +with open(src) as f: + icd = json.load(f) +icd['ICD']['library_path'] = '/opt/swiftshader/libvk_swiftshader.so' +with open(dst, 'w') as f: + json.dump(icd, f, indent=2) +EOF + +# --------------------------------------------------------------------------- +# Runtime stage: final test image. +# --------------------------------------------------------------------------- +FROM $BASEIMAGE + +ADD scripts /tmp/scripts +RUN cd /tmp/scripts && /tmp/scripts/install_centos.sh && rm -rf /tmp/scripts + +# Vulkan loader. The SwiftShader ICD is copied from the builder stage +# below. Callers opt into SwiftShader at `docker run` time via +# VK_ICD_FILENAMES / VK_DRIVER_FILES (see plugin-linux-webgpu-test-stage.yml). +RUN dnf install -y vulkan-loader && dnf clean all + +COPY --from=swiftshader_builder /opt/swiftshader /opt/swiftshader + +ARG BUILD_UID=1001 +ARG BUILD_USER=onnxruntimedev +RUN adduser --uid $BUILD_UID $BUILD_USER +WORKDIR /home/$BUILD_USER +USER $BUILD_USER diff --git a/tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/scripts/install_centos.sh b/tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/scripts/install_centos.sh new file mode 100755 index 0000000000000..1ced7cd2f90c8 --- /dev/null +++ b/tools/ci_build/github/linux/docker/inference/x86_64/python/webgpu/scripts/install_centos.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +os_major_version=$(tr -dc '0-9.' < /etc/redhat-release |cut -d \. -f1) + +echo "installing for os major version : $os_major_version" +dnf install -y glibc-langpack-\* which redhat-lsb-core expat-devel tar unzip zlib-devel make bzip2 bzip2-devel perl-IPC-Cmd openssl-devel wget