diff --git a/.ci/scripts/build-qnn-windows-msvc.ps1 b/.ci/scripts/build-qnn-windows-msvc.ps1 new file mode 100644 index 00000000000..dd17c5da297 --- /dev/null +++ b/.ci/scripts/build-qnn-windows-msvc.ps1 @@ -0,0 +1,120 @@ +# Copyright (c) Qualcomm Innovation Center, Inc. +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# Build-validation for the Qualcomm (QNN) backend under MSVC on Windows. Mirrors +# setup-windows-msvc.ps1 but provisions the QNN SDK and delegates the actual +# build to backends/qualcomm/scripts/build.ps1, which builds both the x86-64 +# Windows host and the ARM64 Windows cross-compiled targets. This is a build-only +# bring-up job: it exists to surface MSVC portability issues in the QNN backend +# sources. The QNN backend loads the SDK's runtime libraries via the OS loader at +# execution time, so only the SDK headers (shipped cross-platform in the SDK zip) +# are needed to build. +# +# backends/qualcomm/scripts/download_qnn_sdk.py only auto-downloads on Linux +# x86, so this script fetches and extracts the SDK itself. The pinned version +# and URL are read from that same module (see "Provision the QNN SDK" below), +# which resolves them from backends/qualcomm/scripts/qnn_config.sh -- the single +# source of truth shared with the Linux build scripts. + +$ErrorActionPreference = "Stop" + +conda create --yes --quiet -n et python=3.12 +conda activate et + +# Install cmake +conda install -y cmake + +# Install CI requirements +pip install -r .ci/docker/requirements-ci.txt + +# --- Provision the QNN SDK ------------------------------------------------- +# Reuse the QNN_VERSION / QNN_ZIP_URL that download_qnn_sdk.py already resolves +# from qnn_config.sh (the single source of truth) instead of re-parsing it here. +$qnnInfo = python -c "import sys; sys.path.insert(0, r'backends\qualcomm\scripts'); import download_qnn_sdk as d; print(d.QNN_VERSION); print(d.QNN_ZIP_URL)" +if ($LASTEXITCODE -ne 0 -or $qnnInfo.Count -lt 2) { + Write-Error "Failed to read QNN_VERSION / QNN_ZIP_URL from download_qnn_sdk.py." + exit 1 +} +$qnnVersion = $qnnInfo[0].Trim() +$qnnZipUrl = $qnnInfo[1].Trim() + +$qnnInstallDir = Join-Path $env:TEMP "qnn" +$qnnSdkRoot = Join-Path $qnnInstallDir "qairt\$qnnVersion" + +if (Test-Path -Path $qnnSdkRoot) { + Write-Host "QNN SDK already present at $qnnSdkRoot" +} else { + New-Item -Path $qnnInstallDir -ItemType Directory -Force | Out-Null + $qnnZip = Join-Path $env:TEMP "qnn_sdk.zip" + Write-Host "Downloading QNN SDK v$qnnVersion ..." + $ProgressPreference = "SilentlyContinue" + Invoke-WebRequest -Uri $qnnZipUrl -OutFile $qnnZip + Write-Host "Extracting QNN SDK ..." + Expand-Archive -Path $qnnZip -DestinationPath $qnnInstallDir -Force + Remove-Item -Path $qnnZip -Force +} + +if (-not (Test-Path -Path (Join-Path $qnnSdkRoot "include\QNN"))) { + Write-Error "QNN SDK layout unexpected: missing include\QNN under $qnnSdkRoot" + exit 1 +} + +$env:QNN_SDK_ROOT = $qnnSdkRoot +Write-Host "Set QNN_SDK_ROOT=$env:QNN_SDK_ROOT" + +# --- Build ------------------------------------------------------------------ +# Delegate to backends/qualcomm/scripts/build.ps1, the canonical Windows build +# entry point, rather than duplicating cmake invocations here. It reads +# $env:QNN_SDK_ROOT (set above), activates the matching VS dev shell for each +# pass itself, and has its own $ErrorActionPreference = 'Stop' plus per-step +# $LASTEXITCODE checks that throw on failure -- so a failed configure or compile +# aborts this script (and the CI job) without extra handling here. +# +# Two passes, one per target architecture: +# +# Pass 1 -- x86-64 Windows host build. -SkipArm64Windows limits build.ps1 to +# its x86-64 block, which also builds the AOT pybind module and example +# runners. This validates the native host build. +.\backends\qualcomm\scripts\build.ps1 -SkipArm64Windows -Release + +# Verify the x86-64 pass produced its key artifacts. build.ps1 throws on a +# failed configure/compile, but a silently-empty or relocated output would still +# pass; assert the backend DLL and the QNN example runner exist so a packaging or +# output-path regression fails the job here rather than downstream. +$x86Artifacts = @( + "build-x86_64-windows\backends\qualcomm\Release\qnn_executorch_backend.dll", + "build-x86_64-windows\examples\qualcomm\executor_runner\Release\qnn_executor_runner.exe" +) +foreach ($artifact in $x86Artifacts) { + if (-not (Test-Path -Path $artifact)) { + Write-Error "Expected x86-64 build artifact not found: $artifact" + exit 1 + } +} + +# Pass 2 -- ARM64 Windows cross-compile (for Windows-on-Snapdragon devices). +# Still runs on the same x86 CI runner: build.ps1 activates the arm64 dev shell +# itself and sets CMAKE_SYSTEM_PROCESSOR=ARM64 to cross-compile, so no arm64 +# hardware is required. -SkipX86Windows limits build.ps1 to its ARM64 block. +# This validates the cross-build, which is the configuration that ships to +# devices. +.\backends\qualcomm\scripts\build.ps1 -SkipX86Windows -Release + +# Verify the ARM64 cross-compile produced its key artifacts, mirroring the +# x86-64 check above. This is the configuration that ships to devices, so a +# missing backend DLL or example runner here should fail the job. +$arm64Artifacts = @( + "build-arm64-windows\backends\qualcomm\Release\qnn_executorch_backend.dll", + "build-arm64-windows\examples\qualcomm\executor_runner\Release\qnn_executor_runner.exe" +) +foreach ($artifact in $arm64Artifacts) { + if (-not (Test-Path -Path $artifact)) { + Write-Error "Expected ARM64 build artifact not found: $artifact" + exit 1 + } +} + +Write-Host "QNN backend MSVC build completed successfully!" diff --git a/.github/workflows/qnn-windows-msvc.yml b/.github/workflows/qnn-windows-msvc.yml new file mode 100644 index 00000000000..8e8399f8ebe --- /dev/null +++ b/.github/workflows/qnn-windows-msvc.yml @@ -0,0 +1,80 @@ +name: Test QNN Backend (Windows MSVC build) + +# Windows MSVC build-validation for the Qualcomm (QNN) backend. Separate from +# test-backend-qnn.yml (which runs the Linux delegate tests) so this scarce +# Windows runner is only used when files that can affect the build change. +# The path filter is intentionally broader than backends/qualcomm/**: core +# build inputs (top-level CMake, tools/cmake, extension/, runtime/) can also +# break the QNN Windows build even when no QNN file is touched. +# +# Kept standalone rather than added to pull.yml: pull.yml runs every job on +# every PR, and Windows runners are expensive. Path-filtering here avoids +# burning one on unrelated PRs, matching cuda.yml / vulkan.yml / windows-msvc.yml. + +on: + push: + branches: + - main + - release/* + tags: + - ciflow/trunk/* + pull_request: + paths: + - .github/workflows/qnn-windows-msvc.yml + - .ci/scripts/build-qnn-windows-msvc.ps1 + - backends/qualcomm/** + - CMakeLists.txt + - tools/cmake/** + - extension/** + - runtime/** + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.event_name == 'workflow_dispatch' }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + changed-files: + name: Get changed files + uses: ./.github/workflows/_get-changed-files.yml + with: + include-push-diff: true + + run-decision: + name: CI run decision + uses: ./.github/workflows/_ci-run-decision.yml + + build-qnn-windows-msvc: + name: build-qnn-windows-msvc + needs: [changed-files, run-decision] + # Path-filtered: mirrors the workflow-level pull_request `paths:` filter, so + # push commits that don't touch these paths skip this job on non-sampled + # commits. The paths are broader than backends/qualcomm/** because core + # build inputs can also break the QNN Windows build. See + # _ci-run-decision.yml for the sampling policy. + if: | + contains(needs.changed-files.outputs.changed-files, 'backends/qualcomm/') || + contains(needs.changed-files.outputs.changed-files, '.ci/scripts/build-qnn-windows-msvc.ps1') || + contains(needs.changed-files.outputs.changed-files, '.github/workflows/qnn-windows-msvc.yml') || + contains(needs.changed-files.outputs.changed-files, 'CMakeLists.txt') || + contains(needs.changed-files.outputs.changed-files, 'tools/cmake/') || + contains(needs.changed-files.outputs.changed-files, 'extension/') || + contains(needs.changed-files.outputs.changed-files, 'runtime/') || + needs.run-decision.outputs.is-full-run == 'true' + uses: pytorch/test-infra/.github/workflows/windows_job.yml@main + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + timeout: 90 + script: | + git config --global http.sslBackend openssl + git submodule update --init --recursive + conda init powershell + powershell -Command "& { + Set-PSDebug -Trace 1 + \$ErrorActionPreference = 'Stop' + \$PSNativeCommandUseErrorActionPreference = \$true + .ci/scripts/build-qnn-windows-msvc.ps1 + }" diff --git a/backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp b/backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp index 7570bdc9ca2..acff97478d7 100644 --- a/backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp +++ b/backends/qualcomm/runtime/backends/QnnBackendUnifiedRegistry.cpp @@ -16,6 +16,8 @@ #include #include +#include + #include namespace executorch { @@ -154,7 +156,7 @@ Error QnnBackendUnifiedRegistry::GetOrCreateBackendBundle( } // 5. Create QnnSystemImplementation and load qnn library std::unique_ptr system_implementation = - std::make_unique("libQnnSystem.so"); + std::make_unique(pal::path::GetLibraryName("QnnSystem")); ret = system_implementation->Load(); ET_CHECK_OR_RETURN_ERROR( ret == Error::Ok, Internal, "Fail to load Qnn system library"); diff --git a/backends/qualcomm/scripts/build.ps1 b/backends/qualcomm/scripts/build.ps1 index c64d49ce8d0..757945eeb84 100644 --- a/backends/qualcomm/scripts/build.ps1 +++ b/backends/qualcomm/scripts/build.ps1 @@ -115,6 +115,27 @@ function Run-CMake([string[]]$ConfigArgs, [string]$BuildDir, [string]$Target = ' if ($LASTEXITCODE -ne 0) { throw "cmake build failed (exit $LASTEXITCODE)" } } +function Get-InstalledVsGenerator() { + $ProgramRoots = @(${env:ProgramFiles(x86)}, $env:ProgramFiles) | Where-Object { $_ } + $VsInstalls = @( + @{ Dir = "18"; Generator = "Visual Studio 18 2026" }, + @{ Dir = "2022"; Generator = "Visual Studio 17 2022" } + ) + $Editions = @("Enterprise", "Professional", "Community", "Preview", "BuildTools") + foreach ($root in $ProgramRoots) { + foreach ($vs in $VsInstalls) { + foreach ($edition in $Editions) { + $DevShell = "$root\Microsoft Visual Studio\$($vs.Dir)\$edition\Common7\Tools\Launch-VsDevShell.ps1" + if (Test-Path $DevShell) { + Write-Host "Using VS dev shell: $DevShell" + return [PSCustomObject]@{ Generator = $vs.Generator; DevShell = $DevShell } + } + } + } + } + throw "No supported Visual Studio installation found (2026 or 2022)." +} + # --------------------------------------------------------------------------- # Block 1: ARM64 Windows cross-compiled build (build-arm64-windows/) # @@ -131,15 +152,25 @@ function Run-CMake([string[]]$ConfigArgs, [string]$BuildDir, [string]$Target = ' # --------------------------------------------------------------------------- if (-not $SkipArm64Windows) { $BuildRoot = Join-Path $PrjRoot $CmakeArm64 + + # Activate the VS environment for arm64 so the cross-compile toolchain is on + # PATH, and pin the matching CMake generator (the target arch is selected by + # -A ARM64 below). + $VsGenerator = Get-InstalledVsGenerator + & $VsGenerator.DevShell -Arch arm64 + Prepare-BuildDir $BuildRoot $ConfigArgs = @( $PrjRoot, - "-DCMAKE_INSTALL_PREFIX=$BuildRoot", - "-DCMAKE_BUILD_TYPE=$BuildType", + "-G", $VsGenerator.Generator, + "-A", "ARM64", "-DCMAKE_SYSTEM_NAME=Windows", "-DCMAKE_SYSTEM_PROCESSOR=ARM64", - "-A", "ARM64", + "-DCMAKE_BUILD_TYPE=$BuildType", + "-DCMAKE_INSTALL_PREFIX=$BuildRoot", + "-DPYTHON_EXECUTABLE=$PythonExe", + "-DQNN_SDK_ROOT=$env:QNN_SDK_ROOT", "-DEXECUTORCH_BUILD_QNN=ON", "-DEXECUTORCH_BUILD_DEVTOOLS=ON", "-DEXECUTORCH_BUILD_EXTENSION_LLM=ON", @@ -149,11 +180,9 @@ if (-not $SkipArm64Windows) { "-DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON", "-DEXECUTORCH_BUILD_EXTENSION_NAMED_DATA_MAP=ON", "-DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON", + "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON", "-DEXECUTORCH_ENABLE_EVENT_TRACER=ON", "-DEXECUTORCH_ENABLE_LOGGING=ON", - "-DQNN_SDK_ROOT=$env:QNN_SDK_ROOT", - "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON", - "-DPYTHON_EXECUTABLE=$PythonExe", "-B$BuildRoot" ) Run-CMake -ConfigArgs $ConfigArgs -BuildDir $BuildRoot @@ -167,17 +196,18 @@ if (-not $SkipArm64Windows) { $ExampleArgs = @( $ExampleRoot, + "-G", $VsGenerator.Generator, + "-A", "ARM64", "-DCMAKE_SYSTEM_NAME=Windows", "-DCMAKE_SYSTEM_PROCESSOR=ARM64", - "-A", "ARM64", "-DCMAKE_BUILD_TYPE=$BuildType", "-DCMAKE_PREFIX_PATH=$CmakePrefixPath", - "-DSUPPORT_REGEX_LOOKAHEAD=ON", - "-DBUILD_TESTING=OFF", - "-DEXECUTORCH_ENABLE_LOGGING=ON", - "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON", "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", "-DPYTHON_EXECUTABLE=$PythonExe", + "-DEXECUTORCH_ENABLE_LOGGING=ON", + "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON", + "-DSUPPORT_REGEX_LOOKAHEAD=ON", + "-DBUILD_TESTING=OFF", "-DDSP_TYPE=$DspType", $DirectModeFlag, "-B$ExampleBuild" @@ -194,15 +224,16 @@ if (-not $SkipArm64Windows) { $LlamaArgs = @( $LlamaRoot, - "-DBUILD_TESTING=OFF", + "-G", $VsGenerator.Generator, + "-A", "ARM64", "-DCMAKE_SYSTEM_NAME=Windows", "-DCMAKE_SYSTEM_PROCESSOR=ARM64", - "-A", "ARM64", "-DCMAKE_BUILD_TYPE=$BuildType", "-DCMAKE_PREFIX_PATH=$CmakePrefixPath", - "-DEXECUTORCH_ENABLE_LOGGING=ON", "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", "-DPYTHON_EXECUTABLE=$PythonExe", + "-DEXECUTORCH_ENABLE_LOGGING=ON", + "-DBUILD_TESTING=OFF", "-B$LlamaBuild" ) Write-Host "`n=== cmake configure (examples/models/llama arm64-windows) ===" -ForegroundColor Cyan @@ -226,8 +257,15 @@ if ($EnableHexagon) { $ConfigArgs = @( $PrjRoot, - "-DCMAKE_INSTALL_PREFIX=$BuildRoot", + "-DCMAKE_TOOLCHAIN_FILE=$env:HEXAGON_SDK_ROOT\build\cmake\hexagon_toolchain.cmake", "-DCMAKE_BUILD_TYPE=$BuildType", + "-DCMAKE_INSTALL_PREFIX=$BuildRoot", + "-DPYTHON_EXECUTABLE=$PythonExe", + "-DQNN_SDK_ROOT=$env:QNN_SDK_ROOT", + "-DHEXAGON_SDK_ROOT=$env:HEXAGON_SDK_ROOT", + "-DHEXAGON_TOOLS_ROOT=$env:HEXAGON_TOOLS_ROOT", + "-DDSP_VERSION=$env:DSP_VERSION", + "-DDSP_TYPE=$DspType", "-DEXECUTORCH_BUILD_QNN=ON", "-DEXECUTORCH_BUILD_XNNPACK=OFF", "-DEXECUTORCH_BUILD_DEVTOOLS=ON", @@ -236,19 +274,12 @@ if ($EnableHexagon) { "-DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON", "-DEXECUTORCH_BUILD_EXTENSION_NAMED_DATA_MAP=ON", "-DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON", - "-DEXECUTORCH_ENABLE_EVENT_TRACER=ON", - "-DEXECUTORCH_ENABLE_LOGGING=ON", + "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON", "-DEXECUTORCH_BUILD_PTHREADPOOL=OFF", "-DEXECUTORCH_BUILD_EXECUTOR_RUNNER=OFF", + "-DEXECUTORCH_ENABLE_EVENT_TRACER=ON", + "-DEXECUTORCH_ENABLE_LOGGING=ON", "-DFLATCC_ALLOW_WERROR=OFF", - "-DQNN_SDK_ROOT=$env:QNN_SDK_ROOT", - "-DHEXAGON_SDK_ROOT=$env:HEXAGON_SDK_ROOT", - "-DHEXAGON_TOOLS_ROOT=$env:HEXAGON_TOOLS_ROOT", - "-DDSP_VERSION=$env:DSP_VERSION", - "-DCMAKE_TOOLCHAIN_FILE=$env:HEXAGON_SDK_ROOT\build\cmake\hexagon_toolchain.cmake", - "-DDSP_TYPE=$DspType", - "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON", - "-DPYTHON_EXECUTABLE=$PythonExe", "-B$BuildRoot" ) Run-CMake -ConfigArgs $ConfigArgs -BuildDir $BuildRoot @@ -270,11 +301,19 @@ if ($EnableHexagon) { if (-not $SkipX86Windows) { $BuildRoot = Join-Path $PrjRoot $CmakeX86 + # Activate the VS environment for amd64 so the MSVC host toolchain is on PATH, + # and pin the matching CMake generator for the native x86-64 build. + $VsGenerator = Get-InstalledVsGenerator + & $VsGenerator.DevShell -Arch amd64 + Prepare-BuildDir $BuildRoot $ConfigArgs = @( + $PrjRoot, + "-G", $VsGenerator.Generator, "-DCMAKE_BUILD_TYPE=$BuildType", "-DCMAKE_INSTALL_PREFIX=$BuildRoot", + "-DPYTHON_EXECUTABLE=$PythonExe", "-DQNN_SDK_ROOT=$env:QNN_SDK_ROOT", "-DEXECUTORCH_BUILD_QNN=ON", "-DEXECUTORCH_BUILD_DEVTOOLS=ON", @@ -284,12 +323,10 @@ if (-not $SkipX86Windows) { "-DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON", "-DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON", "-DEXECUTORCH_BUILD_EXTENSION_NAMED_DATA_MAP=ON", - "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON", "-DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON", + "-DEXECUTORCH_BUILD_KERNELS_QUANTIZED=ON", "-DEXECUTORCH_ENABLE_EVENT_TRACER=ON", "-DEXECUTORCH_ENABLE_LOGGING=ON", - "-DPYTHON_EXECUTABLE=$PythonExe", - "-S$PrjRoot", "-B$BuildRoot" ) Run-CMake -ConfigArgs $ConfigArgs -BuildDir $BuildRoot @@ -342,13 +379,14 @@ if (-not $SkipX86Windows) { $ExampleArgs = @( $ExampleRoot, + "-G", $VsGenerator.Generator, "-DCMAKE_BUILD_TYPE=$BuildType", "-DCMAKE_PREFIX_PATH=$CmakePrefixPath", "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", "-DPYTHON_EXECUTABLE=$PythonExe", + "-DEXECUTORCH_ENABLE_LOGGING=ON", "-DSUPPORT_REGEX_LOOKAHEAD=ON", "-DBUILD_TESTING=OFF", - "-DEXECUTORCH_ENABLE_LOGGING=ON", "-B$ExampleBuild" ) Write-Host "`n=== cmake configure (examples/qualcomm x86-64 Windows) ===" -ForegroundColor Cyan @@ -366,12 +404,13 @@ if (-not $SkipX86Windows) { $LlamaArgs = @( $LlamaRoot, - "-DBUILD_TESTING=OFF", + "-G", $VsGenerator.Generator, "-DCMAKE_BUILD_TYPE=$BuildType", "-DCMAKE_PREFIX_PATH=$CmakePrefixPath", - "-DEXECUTORCH_ENABLE_LOGGING=ON", "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", "-DPYTHON_EXECUTABLE=$PythonExe", + "-DEXECUTORCH_ENABLE_LOGGING=ON", + "-DBUILD_TESTING=OFF", "-B$LlamaBuild" ) Write-Host "`n=== cmake configure (examples/models/llama x86-64 Windows) ===" -ForegroundColor Cyan