diff --git a/plugin-ep-webgpu/README.md b/plugin-ep-webgpu/README.md index dd874f8af1c3b..889fef10ae5e1 100644 --- a/plugin-ep-webgpu/README.md +++ b/plugin-ep-webgpu/README.md @@ -10,8 +10,12 @@ For more information about plugin EPs, see the documentation [here](https://onnx - [`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). +- [`MIN_ONNXRUNTIME_VERSION`](MIN_ONNXRUNTIME_VERSION) — Minimum compatible core `onnxruntime` version. Single source + of truth shared by all packages built from this directory. - [`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. +- [`csharp/`](csharp/) — Sources and packaging script for the `Microsoft.ML.OnnxRuntime.EP.WebGpu` NuGet package. See + [`csharp/README.md`](csharp/README.md) for build and test instructions. ## How it fits together @@ -19,6 +23,7 @@ The plugin EP is built as a shared library (`onnxruntime_providers_webgpu.{dll,s build (`--use_webgpu shared_lib`). The resulting binaries are then packaged into: - A Python wheel (`onnxruntime-ep-webgpu`), built from [`python/`](python/). +- A NuGet package (`Microsoft.ML.OnnxRuntime.EP.WebGpu`), built from [`csharp/`](csharp/). - A universal package published to the internal ORT-Nightly feed for Windows (x64 / arm64), Linux x64, and macOS arm64. @@ -29,7 +34,7 @@ and post-build smoke tests run in the companion `WebGPU Plugin EP Test Pipeline` ## Usage -Once installed, the plugin EP is registered at runtime: +Once installed, the plugin EP is registered at runtime. Example in Python: ```python import onnxruntime as ort @@ -43,5 +48,7 @@ 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). +See the user-facing package READMEs (bundled into the published packages) for full per-language usage: + +- Python: [`python/onnxruntime_ep_webgpu/README.md`](python/onnxruntime_ep_webgpu/README.md) +- C# / .NET: [`csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md`](csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md) diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj new file mode 100644 index 0000000000000..94be6bec6ea46 --- /dev/null +++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj @@ -0,0 +1,91 @@ + + + + netstandard2.0 + latest + enable + + + Microsoft.ML.OnnxRuntime.EP.WebGpu + + 0.0.0-dev + Microsoft + Microsoft + ONNX Runtime WebGPU Plugin Execution Provider. + README.md + ONNX;ONNX Runtime;Machine Learning;AI;Deep Learning;WebGPU + + + MIT + https://github.com/microsoft/onnxruntime + git + © Microsoft Corporation. All rights reserved. + + + true + snupkg + + + + + $(MSBuildThisFileDirectory)..\..\MIN_ONNXRUNTIME_VERSION + $([System.IO.File]::ReadAllText('$(OnnxRuntimeMinVersionFile)').Trim()) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md new file mode 100644 index 0000000000000..f4a717b8836d5 --- /dev/null +++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md @@ -0,0 +1,42 @@ +## Microsoft.ML.OnnxRuntime.EP.WebGpu + +WebGPU plugin Execution Provider for [ONNX Runtime](https://github.com/microsoft/onnxruntime). + +### Usage + +```csharp +// Note: Error handling is omitted for brevity. + +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.EP.WebGpu; + +// Register the WebGPU EP plugin library +var env = OrtEnv.Instance(); +env.RegisterExecutionProviderLibrary("webgpu_ep", WebGpuEp.GetLibraryPath()); + +// Find the WebGPU EP device +OrtEpDevice? webGpuDevice = null; +foreach (var d in env.GetEpDevices()) +{ + if (d.EpName == WebGpuEp.GetEpName()) + { + webGpuDevice = d; + break; + } +} + +// Create a session with the WebGPU EP +using var sessionOptions = new SessionOptions(); +sessionOptions.AppendExecutionProvider(env, new[] { webGpuDevice }, new Dictionary()); + +using var session = new InferenceSession("model.onnx", sessionOptions); +``` + +### Supported Platforms + +| Runtime Identifier | Native Library | +|---|---| +| win-x64 | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` | +| win-arm64 | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` | +| linux-x64 | `libonnxruntime_providers_webgpu.so` | +| osx-arm64 | `libonnxruntime_providers_webgpu.dylib` | diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs new file mode 100644 index 0000000000000..2a5ec106aad0d --- /dev/null +++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs @@ -0,0 +1,112 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.ML.OnnxRuntime.EP.WebGpu +{ + /// + /// Provides helper methods to locate the WebGPU plugin EP native library + /// and retrieve the EP name for registration with ONNX Runtime. + /// + public static class WebGpuEp + { + /// + /// Returns the path to the WebGPU plugin EP native library contained by this package. + /// Can be passed to OrtEnv.RegisterExecutionProviderLibrary(). + /// + /// Full path to the EP native library. + /// If the native library file does not exist at the expected path. + public static string GetLibraryPath() + { + string rootDir = GetNativeDirectory(); + string rid = GetRuntimeIdentifier(); + string libraryName = GetLibraryName(); + + // Probe the standard NuGet runtimes//native/ layout first, then fall back + // to the base directory for single-file/published layouts where native assets + // can land directly next to the managed assembly. + string[] candidates = + { + Path.Combine(rootDir, "runtimes", rid, "native", libraryName), + Path.Combine(rootDir, libraryName), + }; + + foreach (var candidate in candidates) + { + if (File.Exists(candidate)) + return Path.GetFullPath(candidate); + } + + throw new FileNotFoundException( + $"Did not find WebGPU EP library file. Probed: {string.Join(", ", candidates)}"); + } + + /// + /// Returns the names of the EPs created by the WebGPU plugin EP library. + /// Can be used to select an OrtEpDevice from those returned by OrtEnv.GetEpDevices(). + /// + /// Array of EP names. + public static string[] GetEpNames() + { + return new[] { GetEpName() }; + } + + /// + /// Returns the name of the one EP supported by this plugin EP library. + /// Convenience method for plugin EP packages that expose a single EP. + /// + /// The EP name string. + public static string GetEpName() + { + return "WebGpuExecutionProvider"; + } + + private static string GetNativeDirectory() + { + var assemblyDir = Path.GetDirectoryName(typeof(WebGpuEp).Assembly.Location); + + if (!string.IsNullOrEmpty(assemblyDir) && Directory.Exists(assemblyDir)) + return assemblyDir; + + return AppContext.BaseDirectory; + } + + private static string GetRuntimeIdentifier() + { + return GetOSTag() + "-" + GetArchTag(); + } + + private static string GetLibraryName() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return "onnxruntime_providers_webgpu.dll"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return "libonnxruntime_providers_webgpu.so"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return "libonnxruntime_providers_webgpu.dylib"; + + throw new PlatformNotSupportedException( + $"WebGPU plugin EP does not support OS platform: {RuntimeInformation.OSDescription}"); + } + + private static string GetOSTag() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "win"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "osx"; + throw new PlatformNotSupportedException( + $"WebGPU plugin EP does not support OS platform: {RuntimeInformation.OSDescription}"); + } + + private static string GetArchTag() + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "x64", + Architecture.Arm64 => "arm64", + _ => throw new PlatformNotSupportedException( + $"WebGPU plugin EP does not support process architecture: {RuntimeInformation.ProcessArchitecture}"), + }; + } + } +} diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md new file mode 100644 index 0000000000000..7a2b2041e364f --- /dev/null +++ b/plugin-ep-webgpu/csharp/README.md @@ -0,0 +1,140 @@ +# WebGPU Plugin EP — NuGet Packaging + +This directory contains the C# NuGet package project and test app for the WebGPU plugin Execution Provider. + +## Directory Structure + +``` +csharp/ +├── pack_nuget.py # Helper script to build the NuGet package +├── Microsoft.ML.OnnxRuntime.EP.WebGpu/ +│ ├── Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj # NuGet package project (netstandard2.0) +│ ├── WebGpuEp.cs # Helper class for native library resolution +│ └── README.md # Package readme (shipped inside .nupkg) +└── test/ + └── WebGpuEpNuGetTest/ + ├── WebGpuEpNuGetTest.csproj # Test console app (net8.0) + ├── Program.cs # Registers EP, runs inference, validates output + ├── mul.onnx # Test model (element-wise multiply) + └── generate_mul_model.py # Script to regenerate mul.onnx +``` + +## Prerequisites + +- .NET SDK 8.0 or later +- A built WebGPU plugin EP shared library + +## Building the NuGet Package + +Use `pack_nuget.py` to stage native binaries and run `dotnet pack`. The script copies everything into a staging +directory before building — the source tree is never modified. By default, an auto-cleaned temporary directory is used; +pass `--staging-dir` to use an explicit one (required when running with `--build-only` or `--pack-only`). + +At least one binary directory (or `--artifacts-dir` with matching subdirectories) must be provided. Platforms without +a binary directory are skipped. Run `python pack_nuget.py --help` for the full list of options and their defaults. + +### Pack with a local build (single platform) + +```powershell +cd plugin-ep-webgpu/csharp + +python pack_nuget.py --version 0.1.0-dev ` + --binary-dir-win-x64 +``` + +### Pack multiple platforms + +Each `--binary-dir-*` points at the directory containing that platform's already-built native binaries. In practice +the four binaries are produced on different machines and combined in CI; locally you'd typically only set the one(s) +you have available. + +```powershell +python pack_nuget.py --version 0.1.0-dev ` + --binary-dir-win-x64 ` + --binary-dir-win-arm64 ` + --binary-dir-linux-x64 ` + --binary-dir-macos-arm64 +``` + +## Versioning + +The package version is supplied to `pack_nuget.py` via `--version`. In the packaging pipeline, the release or +pre-release version is derived from [`plugin-ep-webgpu/VERSION_NUMBER`](../VERSION_NUMBER). + +## Inspecting the Package + +The `.nupkg` is a ZIP file. To verify its contents: + +```powershell +Expand-Archive nuget_output/Microsoft.ML.OnnxRuntime.EP.WebGpu.0.1.0-dev.nupkg ` + -DestinationPath nuget_output/inspect -Force + +Get-ChildItem nuget_output/inspect -Recurse | Select-Object FullName +``` + +Expected layout inside the package: + +``` +lib/netstandard2.0/Microsoft.ML.OnnxRuntime.EP.WebGpu.dll +runtimes/win-x64/native/onnxruntime_providers_webgpu.dll +runtimes/win-x64/native/dxil.dll +runtimes/win-x64/native/dxcompiler.dll +runtimes/win-arm64/native/... +runtimes/linux-x64/native/libonnxruntime_providers_webgpu.so +runtimes/osx-arm64/native/libonnxruntime_providers_webgpu.dylib +``` + +## Testing the Package + +The test app registers the WebGPU EP, creates a session, runs a simple Mul model, and validates the output. + +```powershell +# Point the test project's nuget.config at the pack output +$localFeed = (Resolve-Path nuget_output).Path +@" + + + + + + + + +"@ | Set-Content test/WebGpuEpNuGetTest/nuget.config + +# Build and run +dotnet run --project test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj --configuration Release +``` + +A successful run prints `PASSED: All outputs match expected values.` and exits with code 0. + +## Regenerating the Test Model + +```bash +python test/WebGpuEpNuGetTest/generate_mul_model.py +``` + +Requires the `onnx` Python package. + +## CI Pipeline + +The NuGet packaging is integrated into the WebGPU plugin pipeline: + +- **Pipeline:** `tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml` +- **Packaging stage:** `tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml` + +The CI stage downloads build artifacts from all enabled platform stages, invokes `pack_nuget.py`, ESRP-signs the +package, and runs the test app on a GPU agent. + +## Native Binaries Per Platform + +| RID | Required Files | +|---|---| +| `win-x64` | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` | +| `win-arm64` | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` | +| `linux-x64` | `libonnxruntime_providers_webgpu.so` | +| `osx-arm64` | `libonnxruntime_providers_webgpu.dylib` | + +On Windows, `dxil.dll` and `dxcompiler.dll` are the DirectX Shader Compiler binaries downloaded from the +[DXC GitHub releases](https://github.com/microsoft/DirectXShaderCompiler/releases). The CI pipeline handles this +automatically. diff --git a/plugin-ep-webgpu/csharp/pack_nuget.py b/plugin-ep-webgpu/csharp/pack_nuget.py new file mode 100644 index 0000000000000..9a29d067a4034 --- /dev/null +++ b/plugin-ep-webgpu/csharp/pack_nuget.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Build the Microsoft.ML.OnnxRuntime.EP.WebGpu NuGet package. + +Stages native binaries from build artifacts into the runtimes/ layout expected +by the .csproj and runs `dotnet pack` to produce the .nupkg / .snupkg files. + +Can be invoked locally or from CI. In CI, pass --artifacts-dir to point at the +downloaded pipeline artifacts. Locally, pass individual --binary-dir-* options. + +Examples +-------- +Local: pack win-x64 only from a local build: + + python pack_nuget.py --version 0.1.0-dev \\ + --binary-dir-win-x64 ../../build/webgpu.plugin/Release/Release + +CI: pack all platforms from downloaded artifacts: + + python pack_nuget.py --version $(PluginPackageVersion) \\ + --artifacts-dir $(Build.BinariesDirectory)/artifacts \\ + --output-dir $(Build.ArtifactStagingDirectory)/nuget +""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +# Platform name -> (RID, list of native binary filenames expected in the source dir). +PLATFORMS: dict[str, tuple[str, tuple[str, ...]]] = { + "win_x64": ("win-x64", ("onnxruntime_providers_webgpu.dll", "dxil.dll", "dxcompiler.dll")), + "win_arm64": ("win-arm64", ("onnxruntime_providers_webgpu.dll", "dxil.dll", "dxcompiler.dll")), + "linux_x64": ("linux-x64", ("libonnxruntime_providers_webgpu.so",)), + "macos_arm64": ("osx-arm64", ("libonnxruntime_providers_webgpu.dylib",)), +} + +SCRIPT_DIR = Path(__file__).resolve().parent +PROJECT_DIR = SCRIPT_DIR / "Microsoft.ML.OnnxRuntime.EP.WebGpu" +CSPROJ = PROJECT_DIR / "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj" +MIN_ORT_VERSION_FILE = SCRIPT_DIR.parent / "MIN_ONNXRUNTIME_VERSION" + + +class PackError(RuntimeError): + """Raised for any user-actionable failure during packaging.""" + + +def parse_args() -> argparse.Namespace: + def _absolute_path(value: str) -> Path: + """argparse `type` converter: parse a string as an absolute Path.""" + return Path(value).resolve() + + p = argparse.ArgumentParser( + description="Build the Microsoft.ML.OnnxRuntime.EP.WebGpu NuGet package.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument("--version", required=True, help="Package version (e.g. 0.1.0-dev).") + p.add_argument( + "--output-dir", + type=_absolute_path, + default=(SCRIPT_DIR / "nuget_output").resolve(), + help="Directory for the .nupkg / .snupkg output (default: ./nuget_output).", + ) + p.add_argument("--configuration", default="Release", help="Build configuration (default: Release).") + + # CI mode: a single root containing per-platform subdirectories. + p.add_argument( + "--artifacts-dir", + type=_absolute_path, + help="CI mode: root containing /bin/ subdirectories for each platform.", + ) + + # Local mode: explicit per-platform binary directories. Each takes precedence over + # --artifacts-dir for that platform. + for name in PLATFORMS: + flag = f"--binary-dir-{name.replace('_', '-')}" + p.add_argument(flag, type=_absolute_path, dest=f"binary_dir_{name}", help=f"Path to {name} native binaries.") + + p.add_argument( + "--nuget-config", type=_absolute_path, help="Optional NuGet.config passed to dotnet via --configfile." + ) + p.add_argument( + "--staging-dir", + type=_absolute_path, + help=( + "Explicit staging directory. Required with --build-only / --pack-only " + "(caller owns its lifecycle). When omitted, an auto-cleaned temporary " + "directory is used for the full build+pack flow." + ), + ) + + phase = p.add_mutually_exclusive_group() + phase.add_argument( + "--build-only", + action="store_true", + help="Stage and build the managed DLL only; skip dotnet pack. Preserves the staging dir.", + ) + phase.add_argument( + "--pack-only", + action="store_true", + help="Skip staging/build and run dotnet pack against an existing staging directory.", + ) + + p.add_argument( + "--required-platforms", + default="", + help=( + "Comma-separated list of platforms that MUST be staged successfully. " + "When omitted, the script just requires at least one platform to be staged." + ), + ) + + return p.parse_args() + + +def parse_required_platforms(value: str) -> list[str]: + names = [tok.strip() for tok in value.split(",") if tok.strip()] + invalid = [n for n in names if n not in PLATFORMS] + if invalid: + raise PackError( + f"unknown platform(s) in --required-platforms: {', '.join(invalid)}. valid: {', '.join(PLATFORMS)}." + ) + return names + + +def stage_sources(staging_dir: Path) -> None: + """Copy project sources into staging, excluding bin/obj.""" + print(f"Staging project files to {staging_dir}") + if staging_dir.exists(): + shutil.rmtree(staging_dir) + shutil.copytree( + PROJECT_DIR, + staging_dir, + ignore=shutil.ignore_patterns("bin", "obj"), + ) + + +def resolve_platform_source( + name: str, + binary_dir_override: Path | None, + artifacts_dir: Path | None, + is_required: bool, +) -> Path | None: + """Return the source dir for a platform, or None to skip.""" + if binary_dir_override is not None: + return binary_dir_override + if artifacts_dir is not None: + candidate = artifacts_dir / name / "bin" + if candidate.is_dir(): + return candidate + if is_required: + raise PackError(f"required platform '{name}' artifact directory not found: {candidate}") + if is_required: + raise PackError( + f"required platform '{name}' has no binary directory " + f"(pass --binary-dir-{name.replace('_', '-')} or --artifacts-dir)." + ) + return None + + +def stage_binaries( + staging_dir: Path, + args: argparse.Namespace, + required_platforms: list[str], +) -> None: + staged: set[str] = set() + + for name, (rid, files) in PLATFORMS.items(): + binary_dir_override: Path | None = getattr(args, f"binary_dir_{name}") + is_required = name in required_platforms + source_dir = resolve_platform_source(name, binary_dir_override, args.artifacts_dir, is_required) + if source_dir is None: + print(f"Skipping {name} (no binary directory provided)") + continue + if not source_dir.is_dir(): + raise PackError(f"binary directory does not exist: {source_dir}") + + target_dir = staging_dir / "runtimes" / rid / "native" + target_dir.mkdir(parents=True, exist_ok=True) + + print(f"Staging {name} -> runtimes/{rid}/native/") + for filename in files: + src = source_dir / filename + if not src.is_file(): + raise PackError(f"expected binary not found: {src}") + shutil.copy2(src, target_dir / filename) + print(f" {filename}") + staged.add(name) + + if required_platforms: + missing = [n for n in required_platforms if n not in staged] + if missing: + raise PackError(f"required platforms not staged: {', '.join(missing)}") + elif not staged: + raise PackError("no platform binaries were staged. Provide at least one --binary-dir-* or --artifacts-dir.") + + print() + print("Runtimes layout:") + for path in sorted((staging_dir / "runtimes").rglob("*")): + print(f" {path}") + + +def dotnet_common_args( + staged_csproj: Path, + args: argparse.Namespace, + min_ort_version_file: Path, +) -> list[str]: + common = [ + str(staged_csproj), + "--configuration", + args.configuration, + f"-p:Version={args.version}", + f"-p:OnnxRuntimeMinVersionFile={min_ort_version_file}", + ] + if args.nuget_config: + common.extend(["--configfile", str(args.nuget_config)]) + print(f"Using NuGet.config: {args.nuget_config}") + return common + + +def do_build(staged_csproj: Path, staging_dir: Path, args: argparse.Namespace, min_ort_version_file: Path) -> None: + print() + print(f"Running dotnet build (Version={args.version}, Configuration={args.configuration})...") + cmd = ["dotnet", "build", *dotnet_common_args(staged_csproj, args, min_ort_version_file)] + print("+ " + " ".join(cmd)) + subprocess.run(cmd, check=True) + + # Note: "netstandard2.0" must match in Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj. + managed_dll = staging_dir / "bin" / args.configuration / "netstandard2.0" / "Microsoft.ML.OnnxRuntime.EP.WebGpu.dll" + if not managed_dll.is_file(): + raise PackError(f"managed DLL not found after build: {managed_dll}") + print() + print(f"Built managed DLL: {managed_dll}") + print("Staging directory preserved for subsequent --pack-only invocation.") + + +def do_pack( + staged_csproj: Path, + output_dir: Path, + args: argparse.Namespace, + min_ort_version_file: Path, +) -> None: + print() + print(f"Running dotnet pack (Version={args.version}, Configuration={args.configuration})...") + pack_args = [ + "dotnet", + "pack", + *dotnet_common_args(staged_csproj, args, min_ort_version_file), + "--output", + str(output_dir), + ] + if args.pack_only: + pack_args.append("--no-build") + print("+ " + " ".join(pack_args)) + subprocess.run(pack_args, check=True) + + print() + nupkgs = sorted(output_dir.glob("*.nupkg")) + if not nupkgs: + raise PackError(f"no .nupkg files found in {output_dir}") + for pkg in nupkgs: + print(f"Produced: {pkg.name} ({pkg.stat().st_size / (1024 * 1024):.2f} MB)") + for pkg in sorted(output_dir.glob("*.snupkg")): + print(f"Produced: {pkg.name} ({pkg.stat().st_size / (1024 * 1024):.2f} MB)") + + +def run_in_staging(args: argparse.Namespace, staging_dir: Path, min_ort_version_file: Path) -> None: + staged_csproj = staging_dir / "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj" + output_dir: Path = args.output_dir + output_dir.mkdir(parents=True, exist_ok=True) + required_platforms = parse_required_platforms(args.required_platforms) + + if args.pack_only: + if not staged_csproj.is_file(): + raise PackError(f"staged project not found at {staged_csproj}. Run with --build-only first.") + print(f"Reusing existing staging directory: {staging_dir}") + else: + stage_sources(staging_dir) + stage_binaries(staging_dir, args, required_platforms) + + if args.build_only: + do_build(staged_csproj, staging_dir, args, min_ort_version_file) + return + + do_pack(staged_csproj, output_dir, args, min_ort_version_file) + + print() + print(f"Done. Output: {output_dir}") + + +def run(args: argparse.Namespace) -> None: + if not CSPROJ.is_file(): + raise PackError(f"project file not found: {CSPROJ}") + if not MIN_ORT_VERSION_FILE.is_file(): + raise PackError(f"MIN_ONNXRUNTIME_VERSION file not found: {MIN_ORT_VERSION_FILE}") + if args.nuget_config and not args.nuget_config.is_file(): + raise PackError(f"NuGet.config not found: {args.nuget_config}") + + if (args.build_only or args.pack_only) and not args.staging_dir: + raise PackError("--staging-dir is required when using --build-only or --pack-only.") + + min_ort_version_file = MIN_ORT_VERSION_FILE.resolve() + + if args.staging_dir: + staging_dir: Path = args.staging_dir + staging_dir.mkdir(parents=True, exist_ok=True) + run_in_staging(args, staging_dir, min_ort_version_file) + return + + # Full build+pack flow with no caller-managed staging dir: use a temp dir that + # is cleaned up automatically (including on exception). + with tempfile.TemporaryDirectory(prefix="webgpu_pack_") as tmp: + run_in_staging(args, Path(tmp), min_ort_version_file) + + +def main() -> int: + args = parse_args() + try: + run(args) + except PackError as e: + print(f"error: {e}", file=sys.stderr) + return 1 + except subprocess.CalledProcessError as e: + cmd_name = e.cmd[0] if e.cmd else "subprocess" + print(f"error: {cmd_name} failed with exit code {e.returncode}", file=sys.stderr) + return e.returncode or 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs new file mode 100644 index 0000000000000..f5d1f0628c831 --- /dev/null +++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs @@ -0,0 +1,82 @@ +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.EP.WebGpu; + +class Program +{ + static int Main() + { + string epLibPath = WebGpuEp.GetLibraryPath(); + string epRegistrationName = "webgpu_ep_registration"; + string epName = WebGpuEp.GetEpName(); + + Console.WriteLine($"WebGPU EP library path: {epLibPath}"); + + var env = OrtEnv.Instance(); + env.RegisterExecutionProviderLibrary(epRegistrationName, epLibPath); + Console.WriteLine($"Registered EP library: {epLibPath}"); + + try + { + // Find the OrtEpDevice for the WebGPU EP + OrtEpDevice? epDevice = null; + foreach (var d in env.GetEpDevices()) + { + if (string.Equals(epName, d.EpName, StringComparison.Ordinal)) + { + epDevice = d; + break; + } + } + + if (epDevice == null) + { + Console.Error.WriteLine($"ERROR: Unable to find OrtEpDevice with name '{epName}'"); + return 1; + } + Console.WriteLine($"Found OrtEpDevice for EP: {epName}"); + + // Create session with WebGPU EP + using var sessionOptions = new SessionOptions(); + sessionOptions.AppendExecutionProvider(env, new[] { epDevice }, new Dictionary()); + sessionOptions.AddSessionConfigEntry("session.disable_cpu_ep_fallback", "1"); + + string inputModelPath = Path.Combine(AppContext.BaseDirectory, "mul.onnx"); + Console.WriteLine($"Loading model: {inputModelPath}"); + + using var session = new InferenceSession(inputModelPath, sessionOptions); + + // Run model: mul(x, y) = x * y + float[] inputData = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f }; + using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(inputData, new long[] { 2, 3 }); + var inputValues = new List { inputOrtValue, inputOrtValue }.AsReadOnly(); + var inputNames = new List { "x", "y" }.AsReadOnly(); + using var runOptions = new RunOptions(); + + using var outputs = session.Run(runOptions, inputNames, inputValues, session.OutputNames); + + float[] expected = { 1.0f, 4.0f, 9.0f, 16.0f, 25.0f, 36.0f }; + var actual = outputs[0].GetTensorDataAsSpan().ToArray(); + + Console.WriteLine($"Input: {string.Join(", ", inputData)}"); + Console.WriteLine($"Output: {string.Join(", ", actual)}"); + Console.WriteLine($"Expected: {string.Join(", ", expected)}"); + + // Validate output + for (int i = 0; i < expected.Length; i++) + { + if (Math.Abs(actual[i] - expected[i]) > 1e-5f) + { + Console.Error.WriteLine($"ERROR: Output mismatch at index {i}: expected {expected[i]}, got {actual[i]}"); + return 1; + } + } + + Console.WriteLine("PASSED: All outputs match expected values."); + return 0; + } + finally + { + env.UnregisterExecutionProviderLibrary(epRegistrationName); + } + } +} diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj new file mode 100644 index 0000000000000..9554161b1e978 --- /dev/null +++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj @@ -0,0 +1,34 @@ + + + + Exe + net8.0 + latest + enable + enable + + *-* + + + + + + + + + + PreserveNewest + + + + + + + diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py new file mode 100644 index 0000000000000..c64b4b7ec96bc --- /dev/null +++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py @@ -0,0 +1,25 @@ +"""Generate a simple Mul ONNX model for testing. + +Produces mul.onnx in the same directory as this script. +The model computes z = x * y (element-wise) for float32 tensors of shape [2, 3]. +""" + +import os + +from onnx import TensorProto, checker, helper, save + +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, producer_name="onnxruntime-webgpu-ep-test") +model.opset_import[0].version = 13 + +checker.check_model(model) + +output_path = os.path.join(os.path.dirname(__file__), "mul.onnx") +save(model, output_path) +print(f"Saved {output_path}") diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx new file mode 100644 index 0000000000000..6df01feb5cf58 --- /dev/null +++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx @@ -0,0 +1,16 @@ + onnxruntime-webgpu-ep-test:Z + +x +yz"Mul mul_graphZ +x +  + +Z +y +  + +b +z +  + +B \ No newline at end of file diff --git a/plugin-ep-webgpu/python/README.md b/plugin-ep-webgpu/python/README.md index ac14a84a70f48..849105a439396 100644 --- a/plugin-ep-webgpu/python/README.md +++ b/plugin-ep-webgpu/python/README.md @@ -19,19 +19,13 @@ Wheels are built via `build_wheel.py`. Running `pip install` or `pip wheel` dire supported — the source tree contains `pyproject.toml.in` (a template), not a real `pyproject.toml`. ```bash -python build_wheel.py \ - --binary_dir \ - --version \ - --output_dir +python build_wheel.py --binary_dir --version --output_dir ``` Example: ```bash -python build_wheel.py \ - --binary_dir ./build/Release \ - --version 0.1.0.dev20260429 \ - --output_dir ./dist +python build_wheel.py --binary_dir ./build/Release --version 0.1.0.devYYYYMMDD --output_dir ./dist ``` The script combines the pre-built plugin EP binaries with the package source to produce a platform-specific wheel. @@ -44,7 +38,7 @@ Install the wheel and dependencies in a clean environment, then run the smoke te 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 +pip install dist/onnxruntime_ep_webgpu-*.whl # pulls in the minimum compatible onnxruntime python test/test_webgpu_plugin_ep.py ``` diff --git a/plugin-ep-webgpu/python/build_wheel.py b/plugin-ep-webgpu/python/build_wheel.py index 8f855a5d2179b..b4357bcdfbe0f 100644 --- a/plugin-ep-webgpu/python/build_wheel.py +++ b/plugin-ep-webgpu/python/build_wheel.py @@ -86,6 +86,7 @@ def prepare_staging_dir(staging_dir: Path, binary_dir: Path, version: str): shutil.copytree(SCRIPT_DIR / "onnxruntime_ep_webgpu", staging_dir / "onnxruntime_ep_webgpu") # Copy plugin binaries into the package directory + # Note: The binaries are assumed to be directly under `binary_dir`. package_dir = staging_dir / "onnxruntime_ep_webgpu" copied = [] for pattern in BINARY_PATTERNS: 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 7d9f7c24b3360..673452d8b110a 100644 --- a/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml @@ -46,7 +46,7 @@ parameters: type: string values: - release - - RC + # - RC # not implemented yet - dev default: dev 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 index 9ce494d4b3a36..12ee9ca68bb4e 100644 --- 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 @@ -71,7 +71,7 @@ stages: set -e -x python3 -m venv /build/test_venv source /build/test_venv/bin/activate - python3 -m pip install onnxruntime onnx numpy + python3 -m pip install 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 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 index 5ad4e170b2855..6dca5dd450fd0 100644 --- 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 @@ -30,7 +30,7 @@ stages: set -e -x python3 -m venv "$(Build.BinariesDirectory)/test_venv" source "$(Build.BinariesDirectory)/test_venv/bin/activate" - python3 -m pip install onnxruntime onnx numpy + python3 -m pip install 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" diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml new file mode 100644 index 0000000000000..93210533d2dc0 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml @@ -0,0 +1,186 @@ +# NuGet packaging stage for WebGPU plugin EP. +# Downloads platform-specific build artifacts, packs them into a single multi-platform NuGet package, +# signs it, and runs a basic test. + +parameters: +- name: package_version + type: string + +- name: version_file + type: string + +- name: DoEsrp + type: boolean + default: true + +- name: platforms + type: object + default: + win_x64: false + win_arm64: false + linux_x64: false + macos_arm64: false + +stages: +- stage: NuGet_Packaging + displayName: 'NuGet Packaging' + dependsOn: + - ${{ if eq(parameters.platforms.win_x64, true) }}: + - Win_plugin_webgpu_x64_Build + - ${{ if eq(parameters.platforms.win_arm64, true) }}: + - Win_plugin_webgpu_arm64_Build + - ${{ if eq(parameters.platforms.linux_x64, true) }}: + - Linux_plugin_webgpu_x64_Build + - ${{ if eq(parameters.platforms.macos_arm64, true) }}: + - MacOS_plugin_webgpu_arm64_Build + jobs: + # ---------- Pack job ---------- + - job: NuGet_Pack + displayName: 'Pack NuGet' + timeoutInMinutes: 30 + workspace: + clean: all + pool: + name: onnxruntime-Win-CPU-VS2022-Latest + os: windows + templateContext: + outputs: + - output: pipelineArtifact + targetPath: '$(Build.ArtifactStagingDirectory)\nuget' + artifactName: webgpu_plugin_nuget + variables: + - template: ../templates/common-variables.yml + - name: WebGpuPackStagingDir + value: '$(Build.BinariesDirectory)\webgpu_pack_staging' + # Common arguments shared by the Build and Pack invocations of pack_nuget.py. + - name: WebGpuPackNuGetCommonArgs + value: >- + --version "$(PluginPackageVersion)" + --output-dir "$(Build.ArtifactStagingDirectory)\nuget" + --staging-dir "$(WebGpuPackStagingDir)" + --configuration Release + --nuget-config "$(Build.SourcesDirectory)\NuGet.config" + 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 }} + + # Download platform artifacts + - ${{ if eq(parameters.platforms.win_x64, true) }}: + - task: DownloadPipelineArtifact@2 + displayName: 'Download win-x64 artifacts' + inputs: + artifactName: webgpu_plugin_win_x64 + targetPath: '$(Build.BinariesDirectory)\artifacts\win_x64' + + - ${{ if eq(parameters.platforms.win_arm64, true) }}: + - task: DownloadPipelineArtifact@2 + displayName: 'Download win-arm64 artifacts' + inputs: + artifactName: webgpu_plugin_win_arm64 + targetPath: '$(Build.BinariesDirectory)\artifacts\win_arm64' + + - ${{ if eq(parameters.platforms.linux_x64, true) }}: + - task: DownloadPipelineArtifact@2 + displayName: 'Download linux-x64 artifacts' + inputs: + artifactName: webgpu_plugin_linux_x64 + targetPath: '$(Build.BinariesDirectory)\artifacts\linux_x64' + + - ${{ if eq(parameters.platforms.macos_arm64, true) }}: + - task: DownloadPipelineArtifact@2 + displayName: 'Download macos-arm64 artifacts' + inputs: + artifactName: webgpu_plugin_macos_arm64 + targetPath: '$(Build.BinariesDirectory)\artifacts\macos_arm64' + + # Compute the set of required platforms from the pipeline parameters and verify the + # corresponding artifact directories actually downloaded. This catches renamed/moved + # upstream artifacts loudly before any pack work, and feeds pack_nuget.py the same + # list so it fails fast if any required platform's binaries are missing. + - task: PythonScript@0 + displayName: 'Compute required platforms' + inputs: + scriptSource: inline + script: | + import os + import sys + + # The string literals below are filled in by ADO template expansion at queue + # time and resolve to a boolean value 'True' or 'False'. Compare case-insensitively. + platforms_enabled = { + "win_x64": "${{ parameters.platforms.win_x64 }}".lower() == "true", + "win_arm64": "${{ parameters.platforms.win_arm64 }}".lower() == "true", + "linux_x64": "${{ parameters.platforms.linux_x64 }}".lower() == "true", + "macos_arm64": "${{ parameters.platforms.macos_arm64 }}".lower() == "true", + } + expected = [name for name, enabled in platforms_enabled.items() if enabled] + + if not expected: + print("##vso[task.logissue type=error]No platforms enabled in 'platforms' parameter — nothing to pack.") + sys.exit(1) + + artifacts_dir = r"$(Build.BinariesDirectory)\artifacts" + missing = [ + f"{p} ({d})" + for p in expected + for d in [os.path.join(artifacts_dir, p, "bin")] + if not os.path.isdir(d) + ] + if missing: + print("##vso[task.logissue type=error]Expected artifact directories not found:") + for m in missing: + print(f"##vso[task.logissue type=error] {m}") + sys.exit(1) + + required = ",".join(expected) + print(f"Required platforms: {required}") + print(f"##vso[task.setvariable variable=WebGpuRequiredPlatforms]{required}") + + # Stage binaries and build the managed assembly (so it can be ESRP-signed before packing). + - task: PythonScript@0 + displayName: 'Build managed DLL' + inputs: + scriptSource: filePath + scriptPath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.py' + arguments: >- + $(WebGpuPackNuGetCommonArgs) + --artifacts-dir "$(Build.BinariesDirectory)\artifacts" + --required-platforms $(WebGpuRequiredPlatforms) + --build-only + + # ESRP-sign the managed DLL before it gets embedded in the .nupkg. + - template: ../templates/win-esrp-dll.yml + parameters: + FolderPath: '$(WebGpuPackStagingDir)' + Pattern: 'Microsoft.ML.OnnxRuntime.EP.WebGpu.dll' + DisplayName: 'ESRP - Sign managed DLL' + DoEsrp: ${{ parameters.DoEsrp }} + + # Pack the (now-signed) managed DLL plus native binaries into the .nupkg. + - task: PythonScript@0 + displayName: 'Pack NuGet package' + inputs: + scriptSource: filePath + scriptPath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.py' + arguments: >- + $(WebGpuPackNuGetCommonArgs) + --pack-only + + # ESRP sign + - template: ../templates/esrp_nuget.yml + parameters: + FolderPath: '$(Build.ArtifactStagingDirectory)\nuget' + DisplayName: 'ESRP - Sign NuGet package' + DoEsrp: ${{ parameters.DoEsrp }} 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 6777f207d67b9..996d6fa1af0a6 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 @@ -48,9 +48,7 @@ stages: 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)) }}: + - ${{ if eq(parameters.build_windows_arm64, true) }}: - template: plugin-win-webgpu-stage.yml parameters: arch: 'arm64' @@ -74,13 +72,25 @@ stages: version_file: ${{ parameters.version_file }} cmake_build_type: ${{ parameters.cmake_build_type }} + # NuGet packaging (runs after all platform builds) + - template: plugin-webgpu-nuget-packaging-stage.yml + parameters: + package_version: ${{ parameters.package_version }} + version_file: ${{ parameters.version_file }} + DoEsrp: true + platforms: + win_x64: ${{ parameters.build_windows_x64 }} + win_arm64: ${{ parameters.build_windows_arm64 }} + linux_x64: ${{ parameters.build_linux_x64 }} + macos_arm64: ${{ parameters.build_macos_arm64 }} + # Create zip packages for Foundry Local consumption - stage: Package_Foundry_Local_WebGPU_Zips displayName: 'Package Foundry Local WebGPU Plugin-EP Zips' dependsOn: - ${{ if eq(parameters.build_windows_x64, true) }}: - Win_plugin_webgpu_x64_Build - - ${{ if and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, true)) }}: + - ${{ if eq(parameters.build_windows_arm64, true) }}: - Win_plugin_webgpu_arm64_Build - ${{ if eq(parameters.build_linux_x64, true) }}: - Linux_plugin_webgpu_x64_Build @@ -111,10 +121,7 @@ stages: artifactName: webgpu_plugin_win_x64 targetPath: $(Build.SourcesDirectory)/webgpu-plugin-win-x64 - # 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)) }}: + - ${{ if eq(parameters.build_windows_arm64, true) }}: - task: DownloadPipelineArtifact@2 displayName: 'Download webgpu_plugin_win_arm64' inputs: 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 acad674143961..3d2ce8072845a 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 @@ -28,6 +28,7 @@ parameters: stages: - stage: Win_plugin_webgpu_${{ parameters.arch }}_Build ${{ if eq(parameters.arch, 'arm64') }}: + # The ARM64 build consumes the x64 tblgen.exe artifact published by the Windows x64 stage. dependsOn: Win_plugin_webgpu_x64_Build ${{ else }}: dependsOn: [] 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 index af29a62d69329..1494584ff98fd 100644 --- 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 @@ -31,29 +31,103 @@ stages: artifact: webgpu_plugin_python_win_${{ parameters.arch }} displayName: 'Download Python wheel' - - task: PowerShell@2 + - pwsh: | + $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 test dependencies" + python -m pip install onnx numpy + + $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 + + echo "running test_webgpu_plugin_ep.py" + python -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) - inputs: - targetType: inline - pwsh: true - script: | + + # NuGet package test (x64 only — the NuGet package is multi-platform but + # the test runs on a single Windows agent that exercises the WebGPU EP). + - ${{ if eq(parameters.arch, 'x64') }}: + - job: Win_plugin_webgpu_nuget_Test + timeoutInMinutes: 30 + workspace: + clean: all + pool: + name: onnxruntime-Win2022-VS2022-webgpu-A10 + os: windows + variables: + WebGpuTestProject: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj' + steps: + - checkout: self + submodules: none + + - template: ../templates/setup-feeds-and-python-steps.yml + + # Download the NuGet package produced by the packaging pipeline run that + # triggered this pipeline (or that was selected at queue time). + - download: build + artifact: webgpu_plugin_nuget + displayName: 'Download NuGet package' + + # Set up local NuGet feed and extract the package version from the .nupkg filename + # so the test project can pin to it (instead of resolving via a floating version). + - pwsh: | $ErrorActionPreference = 'Stop' + $localFeedDir = "$(Build.BinariesDirectory)\local_feed" + New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null - echo "creating test_venv" - python -m venv "$(Build.BinariesDirectory)\test_venv" + # Locate the .nupkg. + $nupkg = Get-ChildItem "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\Microsoft.ML.OnnxRuntime.EP.WebGpu.*.nupkg" | + Select-Object -First 1 + if (-not $nupkg) { + throw "No matching .nupkg found under $(Pipeline.Workspace)\build\webgpu_plugin_nuget" + } + Copy-Item $nupkg.FullName $localFeedDir -Force - echo "activating test_venv" - & "$(Build.BinariesDirectory)\test_venv\Scripts\Activate.ps1" + # Extract version from filename: Microsoft.ML.OnnxRuntime.EP.WebGpu..nupkg + # The version starts with a digit, which disambiguates from any future filename suffixes. + if ($nupkg.BaseName -notmatch '^Microsoft\.ML\.OnnxRuntime\.EP\.WebGpu\.(\d.*)$') { + throw "Could not extract version from .nupkg filename: $($nupkg.Name)" + } + $packageVersion = $Matches[1] + Write-Host "Detected package version: $packageVersion" + Write-Host "##vso[task.setvariable variable=OrtWebGpuPackageVersion]$packageVersion" - echo "installing onnxruntime onnx numpy" - python -m pip install onnxruntime onnx numpy + # Write a project-level nuget.config that adds ONLY the local feed. + # NuGet merges this with the repo-root NuGet.config. + $nugetConfig = "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\nuget.config" + Set-Content -Path $nugetConfig -Encoding UTF8 -Value @" + + + + + + + "@ + Write-Host "Wrote project-level nuget.config with local feed: $localFeedDir" + Write-Host "Local feed contents:" + Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" } + displayName: 'Set up local NuGet feed' - $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 + - pwsh: | + dotnet build ` + "$(WebGpuTestProject)" ` + --configuration Release ` + -p:OrtWebGpuPackageVersion=$(OrtWebGpuPackageVersion) + displayName: 'Build test project' - echo "running test_webgpu_plugin_ep.py" - python -u "$(Build.SourcesDirectory)\plugin-ep-webgpu\python\test\test_webgpu_plugin_ep.py" + - pwsh: | + dotnet run ` + --project "$(WebGpuTestProject)" ` + --configuration Release ` + --no-build + displayName: 'Run NuGet package test'