From b064181b6313f82f2555150504802b6291ce87ca Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Fri, 27 Feb 2026 14:41:04 +0200 Subject: [PATCH 1/6] Add Windows ARM64 CPU support for TorchSharp Add support for building, packaging, and distributing TorchSharp on Windows ARM64 (CPU-only, no CUDA). This enables .NET developers on Windows ARM64 devices to use TorchSharp with the stable LibTorch 2.10.0 release. Key changes: - MSBuild: Add win-arm64 RID mapping, archive name, and cmake path (ARM64 archive has different layout: lib/ instead of libtorch/lib/) - Native build: Enable ARM64 cross-compilation from x64 via MSVC amd64_arm64 toolchain and CMake -A ARM64 - Runtime: Detect win-arm64 in Torch.cs nativeRid for correct native library loading - NuGet: Add libtorch-cpu-win-arm64 package and update libtorch-cpu meta-package - CI: Add Windows ARM64 PR validation and native build pipeline jobs (cross-compiled on x64 agents) - LibTorch: ARM64 uses armpl_lp64.dll (ARM Performance Libraries) instead of Intel OpenMP (libiomp5md.dll) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Build.props | 7 +- Directory.Build.targets | 10 ++ azure-pipelines.yml | 118 ++++++++++++++++++ .../libtorch-cpu-win-arm64.nupkgproj | 16 +++ pkg/libtorch-cpu/libtorch-cpu.nupkgproj | 1 + pkg/pack.proj | 2 + src/Native/build.cmd | 30 ++--- src/Native/gen-buildsys-win.bat | 3 +- src/Redist/libtorch-cpu/libtorch-cpu.proj | 11 +- ...rm64-shared-with-deps-2.10.0%2Bcpu.zip.sha | 1 + src/TorchSharp/Torch.cs | 3 +- 11 files changed, 178 insertions(+), 24 deletions(-) create mode 100644 pkg/libtorch-cpu-win-arm64/libtorch-cpu-win-arm64.nupkgproj create mode 100644 src/Redist/libtorch-cpu/libtorch-win-arm64-shared-with-deps-2.10.0%2Bcpu.zip.sha diff --git a/Directory.Build.props b/Directory.Build.props index e8e44ee50..014fd9495 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -55,10 +55,12 @@ $(TargetOS)-$(TargetArchitecture) win-x64 + win-arm64 linux-x64 osx-arm64 win-x64 + win-arm64 linux-x64 osx-$(TargetArchitecture) @@ -141,6 +143,7 @@ cpu cu$(CudaVersionNoDot) libtorch-win-shared-with-deps$(LibTorchDebug) + libtorch-win-arm64-shared-with-deps$(LibTorchDebug) libtorch-shared-with-deps libtorch-macos-x86_64 libtorch-macos-arm64 @@ -148,7 +151,9 @@ $(LibTorchArchiveCoreName)-$(LibTorchVersion)$(LibTorchCudaArchiveNameSuffix) $(LibTorchArchiveCoreName)-$(LibTorchVersion)$(LibTorchCpuLocalNameSuffix) $(LibTorchArchiveCoreName)-$(LibTorchVersion)$(LibTorchCudaLocalNameSuffix) - $(IntermediateOutputRootPath)libtorch-cpu\$(LibTorchCpuLocalBase)\libtorch\share\cmake\Torch + $(IntermediateOutputRootPath)libtorch-cpu\$(LibTorchCpuLocalBase)\libtorch\share\cmake\Torch + + $(IntermediateOutputRootPath)libtorch-cpu\$(LibTorchCpuLocalBase)\share\cmake\Torch diff --git a/Directory.Build.targets b/Directory.Build.targets index 4ab3c814c..cc6bb3d4e 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -18,6 +18,16 @@ + + + + + + + + + + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9577abcf5..8beda00e8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -89,6 +89,15 @@ jobs: pool: vmImage: 'macos-latest' +- template: /build/ci/job-template.yml + parameters: + prepScript: echo "no prep needed" + name: Windows_arm64 + buildScript: dotnet build /p:SkipCuda=true /p:TargetArchitecture=arm64 /p:SkipNetFxBuild=true -c + testScript: echo "Cannot run ARM64 tests on x64 Azure Pipelines agent" + pool: + vmImage: 'windows-latest' + ################################################################################ # {Build} --> combine --> package to build native bits on multiple OS's ################################################################################ @@ -285,6 +294,64 @@ jobs: - publish: $(Build.SourcesDirectory)/bin/obj/packprep/$(BuildConfig) artifact: MacAssets_arm64 +################################################################################ +- job: Windows_arm64_Native_Build_For_Packages +################################################################################ + condition: and(ne(variables['system.pullrequest.isfork'], true), eq(variables['build.sourcebranchname'], '${{ parameters.SourceBranchName }}')) + variables: + BuildConfig: Release + OfficialBuildId: $(BUILD.BUILDNUMBER) + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_MULTILEVEL_LOOKUP: 0 + pool: + vmImage: 'windows-latest' + steps: + # Initial cleanup + - script: | + rmdir /s /q .git 2>nul + dotnet nuget locals all --clear + dir + displayName: Initial cleanup + continueOnError: true + + - task: UseDotNet@2 + displayName: 'Use .NET Core sdk' + inputs: + packageType: sdk + version: 8.0.x + installationPath: $(Agent.ToolsDirectory)/dotnet + + # Download ARM64 LibTorch and clean immediately + - script: | + dotnet build -c $(BuildConfig) src/Redist/libtorch-cpu/libtorch-cpu.proj /p:UpdateSHA=true /p:SkipTests=true /p:TargetOS=windows /p:TargetArchitecture=arm64 /t:Build /p:IncludeLibTorchCpuPackages=true + del /s /q *.zip 2>nul + del /s /q *.tar.gz 2>nul + displayName: Download ARM64 libtorch native binaries and cleanup + condition: eq('${{ parameters.BuildLibTorchPackages }}', true) + + # Cross-compile LibTorchSharp for ARM64 on x64 host + - script: dotnet build -c $(BuildConfig) src/TorchSharp/TorchSharp.csproj /p:SkipCuda=true /p:SkipTests=true /p:TargetArchitecture=arm64 + condition: eq('${{ parameters.BuildLibTorchPackages }}', true) + displayName: Build TorchSharp win-arm64 + + - script: dotnet build -c $(BuildConfig) src/TorchVision/TorchVision.csproj /p:SkipCuda=true /p:SkipTests=true /p:TargetArchitecture=arm64 + displayName: Build TorchVision + + - script: dotnet build -c $(BuildConfig) src/TorchAudio/TorchAudio.csproj /p:SkipCuda=true /p:SkipTests=true /p:TargetArchitecture=arm64 + displayName: Build TorchAudio + + # Clean up unnecessary files before publishing + - script: | + del /s /q $(Build.SourcesDirectory)\bin\*.pdb 2>nul + del /s /q $(Build.SourcesDirectory)\bin\*.xml 2>nul + del /s /q $(Build.SourcesDirectory)\bin\obj\packprep\$(BuildConfig)\*.lib 2>nul + displayName: Clean up unnecessary files + continueOnError: true + + - publish: $(Build.SourcesDirectory)/bin/obj/packprep/$(BuildConfig) + artifact: WindowsAssets_arm64 + ################################################################################ - job: Build_TorchSharp_And_libtorch_cpu_Packages ################################################################################ @@ -292,6 +359,7 @@ jobs: dependsOn: - Linux_Native_Build_For_Packages - Windows_Native_Build_For_Packages + - Windows_arm64_Native_Build_For_Packages - MacOS_arm64_Native_Build_For_Packages timeoutInMinutes: 90 variables: @@ -496,6 +564,56 @@ jobs: displayName: Clean WindowsAssets immediately continueOnError: true + # Process Windows ARM64 assets + - task: DownloadPipelineArtifact@2 + displayName: Download Windows ARM64 TorchSharp assets + inputs: + artifact: WindowsAssets_arm64 + patterns: | + TorchSharp/** + path: $(Pipeline.Workspace)/WindowsAssets_arm64 + retryCountOnTaskFailure: 3 + + - task: DownloadPipelineArtifact@2 + displayName: Download Windows ARM64 TorchAudio assets + inputs: + artifact: WindowsAssets_arm64 + patterns: | + TorchAudio/** + path: $(Pipeline.Workspace)/WindowsAssets_arm64 + retryCountOnTaskFailure: 3 + + - task: DownloadPipelineArtifact@2 + displayName: Download Windows ARM64 TorchVision assets + inputs: + artifact: WindowsAssets_arm64 + patterns: | + TorchVision/** + path: $(Pipeline.Workspace)/WindowsAssets_arm64 + retryCountOnTaskFailure: 3 + + - task: DownloadPipelineArtifact@2 + displayName: Download Windows ARM64 libtorch-cpu assets + condition: eq('${{ parameters.BuildLibTorchPackages }}', true) + inputs: + artifact: WindowsAssets_arm64 + patterns: | + libtorch-cpu-win-arm64/** + path: $(Pipeline.Workspace)/WindowsAssets_arm64 + retryCountOnTaskFailure: 3 + continueOnError: true + + - task: CopyFiles@2 + displayName: Copy Windows ARM64 native assets (batch) + inputs: + sourceFolder: $(Pipeline.Workspace)/WindowsAssets_arm64 + targetFolder: $(Build.SourcesDirectory)/bin/obj/packprep/$(BuildConfig) + cleanTargetFolder: false + + - script: rmdir /s /q $(Pipeline.Workspace)\WindowsAssets_arm64 + displayName: Clean WindowsAssets_arm64 immediately + continueOnError: true + # Restore and pack - script: dotnet restore pkg/pack.proj /p:Configuration=Release --nologo displayName: Restore package projects diff --git a/pkg/libtorch-cpu-win-arm64/libtorch-cpu-win-arm64.nupkgproj b/pkg/libtorch-cpu-win-arm64/libtorch-cpu-win-arm64.nupkgproj new file mode 100644 index 000000000..d8ce731a6 --- /dev/null +++ b/pkg/libtorch-cpu-win-arm64/libtorch-cpu-win-arm64.nupkgproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/pkg/libtorch-cpu/libtorch-cpu.nupkgproj b/pkg/libtorch-cpu/libtorch-cpu.nupkgproj index 97c3ffe67..c35a3d5fc 100644 --- a/pkg/libtorch-cpu/libtorch-cpu.nupkgproj +++ b/pkg/libtorch-cpu/libtorch-cpu.nupkgproj @@ -7,6 +7,7 @@ + diff --git a/pkg/pack.proj b/pkg/pack.proj index 3c9db2f98..55474fdc6 100644 --- a/pkg/pack.proj +++ b/pkg/pack.proj @@ -24,6 +24,8 @@ Condition="'$(IncludeTorchSharpPackage)' == 'true' AND !Exists('$(PackagePreparationPath)\TorchSharp\runtimes\linux-x64\native\libLibTorchSharp.so')" /> + diff --git a/src/Native/build.cmd b/src/Native/build.cmd index c0c26c600..ef2b1bd84 100644 --- a/src/Native/build.cmd +++ b/src/Native/build.cmd @@ -23,6 +23,7 @@ if /i [%1] == [Debug] ( set CMAKE_BUILD_TYPE=Debug&&shift&goto Arg_Loop) if /i [%1] == [x86] ( set __BuildArch=x86&&set __VCBuildArch=x86&&shift&goto Arg_Loop) if /i [%1] == [x64] ( set __BuildArch=x64&&set __VCBuildArch=x86_amd64&&shift&goto Arg_Loop) if /i [%1] == [amd64] ( set __BuildArch=x64&&set __VCBuildArch=x86_amd64&&shift&goto Arg_Loop) +if /i [%1] == [arm64] ( set __BuildArch=ARM64&&set __VCBuildArch=amd64_arm64&&shift&goto Arg_Loop) if /i [%1] == [--libtorchpath] ( set LIBTORCH_PATH=%2&&shift&goto Arg_Loop) @@ -66,50 +67,39 @@ exit /b 1 :: Setup vars for VS2026 set __PlatformToolset=v145 set __VSVersion=18 2026 -if NOT "%__BuildArch%" == "arm64" ( - :: Set the environment for the native build - call "%VS180COMNTOOLS%..\..\VC\Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% -) +:: Set the environment for the native build (including cross-compilation for ARM64) +call "%VS180COMNTOOLS%..\..\VC\Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% goto :SetupDirs :VS2022 :: Setup vars for VS2022 set __PlatformToolset=v143 set __VSVersion=17 2022 -if NOT "%__BuildArch%" == "arm64" ( - :: Set the environment for the native build - call "%VS170COMNTOOLS%..\..\VC\Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% -) +:: Set the environment for the native build (including cross-compilation for ARM64) +call "%VS170COMNTOOLS%..\..\VC\Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% goto :SetupDirs :VS2019 :: Setup vars for VS2019 set __PlatformToolset=v142 set __VSVersion=16 2019 -if NOT "%__BuildArch%" == "arm64" ( - :: Set the environment for the native build - call "%VS160COMNTOOLS%..\..\VC\Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% -) +:: Set the environment for the native build (including cross-compilation for ARM64) +call "%VS160COMNTOOLS%..\..\VC\Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% goto :SetupDirs :VS2017 :: Setup vars for VS2017 set __PlatformToolset=v141 set __VSVersion=15 2017 -if NOT "%__BuildArch%" == "arm64" ( - :: Set the environment for the native build - call "%VS150COMNTOOLS%..\..\VC\Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% -) +:: Set the environment for the native build (including cross-compilation for ARM64) +call "%VS150COMNTOOLS%..\..\VC\Auxiliary\Build\vcvarsall.bat" %__VCBuildArch% goto :SetupDirs :VS2015 :: Setup vars for VS2015build set __PlatformToolset=v140 set __VSVersion=14 2015 -if NOT "%__BuildArch%" == "arm64" ( - :: Set the environment for the native build - call "%VS140COMNTOOLS%..\..\VC\vcvarsall.bat" %__VCBuildArch% -) +call "%VS140COMNTOOLS%..\..\VC\vcvarsall.bat" %__VCBuildArch% :SetupDirs :: Setup to cmake the native components diff --git a/src/Native/gen-buildsys-win.bat b/src/Native/gen-buildsys-win.bat index b3870c171..c4a76bd4f 100644 --- a/src/Native/gen-buildsys-win.bat +++ b/src/Native/gen-buildsys-win.bat @@ -30,6 +30,7 @@ popd :: Set the target architecture to a format cmake understands. if /i "%3" == "x64" (set __ExtraCmakeParams=%__ExtraCmakeParams% -A x64) if /i "%3" == "x86" (set __ExtraCmakeParams=%__ExtraCmakeParams% -A Win32) +if /i "%3" == "ARM64" (set __ExtraCmakeParams=%__ExtraCmakeParams% -A ARM64) echo "%CMakePath%" "-DCMAKE_BUILD_TYPE=%CMAKE_BUILD_TYPE%" "-DCMAKE_INSTALL_PREFIX=%__CMakeBinDir%" "-DLIBTORCH_PATH=%LIBTORCH_PATH%" -G "Visual Studio %__VSString%" %__ExtraCmakeParams% -B. -H%1 "%CMakePath%" "-DCMAKE_BUILD_TYPE=%CMAKE_BUILD_TYPE%" "-DCMAKE_INSTALL_PREFIX=%__CMakeBinDir%" "-DLIBTORCH_PATH=%LIBTORCH_PATH%" -G "Visual Studio %__VSString%" %__ExtraCmakeParams% -B. -H%1 @@ -40,7 +41,7 @@ GOTO :DONE echo "Usage..." echo "gen-buildsys-win.bat " echo "Specify the VSVersion to be used - VS2015, VS2017 or VS2019" - echo "Specify the Target Architecture - x86, or x64." + echo "Specify the Target Architecture - x86, x64, or ARM64." EXIT /B 1 :DONE diff --git a/src/Redist/libtorch-cpu/libtorch-cpu.proj b/src/Redist/libtorch-cpu/libtorch-cpu.proj index adc8ba013..c082675ee 100644 --- a/src/Redist/libtorch-cpu/libtorch-cpu.proj +++ b/src/Redist/libtorch-cpu/libtorch-cpu.proj @@ -30,7 +30,7 @@ $(MainPackageFolder)\.copied.SkipTests.$(SkipTests).IncludeLibTorchCpuPackages.$(IncludeLibTorchCpuPackages) - + @@ -39,6 +39,15 @@ + + + + + + + + + diff --git a/src/Redist/libtorch-cpu/libtorch-win-arm64-shared-with-deps-2.10.0%2Bcpu.zip.sha b/src/Redist/libtorch-cpu/libtorch-win-arm64-shared-with-deps-2.10.0%2Bcpu.zip.sha new file mode 100644 index 000000000..f50bc08ff --- /dev/null +++ b/src/Redist/libtorch-cpu/libtorch-win-arm64-shared-with-deps-2.10.0%2Bcpu.zip.sha @@ -0,0 +1 @@ +38d666a9030ba098d1ac5dabfd995cf3d113a12d512252080978b0cc206af205 \ No newline at end of file diff --git a/src/TorchSharp/Torch.cs b/src/TorchSharp/Torch.cs index dd7a07689..c7e6dd608 100644 --- a/src/TorchSharp/Torch.cs +++ b/src/TorchSharp/Torch.cs @@ -38,7 +38,8 @@ public static partial class torch RuntimeInformation.OSArchitecture == Architecture.Arm64; static string nativeRid => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"win-x64" : + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? (RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "win-arm64" : "win-x64") : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? $"linux-x64" : isAppleSilicon ? "osx-arm64" : "any"; From 628989dacf28cd949fb6e75c39d45c936e462f40 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Wed, 11 Mar 2026 15:47:51 +0100 Subject: [PATCH 2/6] Address PR review feedback for ARM64 support - Use ProcessArchitecture instead of OSArchitecture for nativeRid to correctly handle x64 emulation on ARM64 Windows - Add missing '-c Release' configuration to Windows_arm64 CI job - Consolidate duplicate TargetRuntimeID blocks into single block with mutually exclusive conditions - Update VS version list in gen-buildsys-win.bat usage text to include VS2022 and VS2026 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Build.props | 7 +------ azure-pipelines.yml | 2 +- src/Native/gen-buildsys-win.bat | 2 +- src/TorchSharp/Torch.cs | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 014fd9495..b4cf70432 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -54,12 +54,7 @@ $(TargetOS)-$(TargetArchitecture) - win-x64 - win-arm64 - linux-x64 - osx-arm64 - - win-x64 + win-x64 win-arm64 linux-x64 osx-$(TargetArchitecture) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8beda00e8..2df815d25 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -93,7 +93,7 @@ jobs: parameters: prepScript: echo "no prep needed" name: Windows_arm64 - buildScript: dotnet build /p:SkipCuda=true /p:TargetArchitecture=arm64 /p:SkipNetFxBuild=true -c + buildScript: dotnet build /p:SkipCuda=true /p:TargetArchitecture=arm64 /p:SkipNetFxBuild=true -c Release testScript: echo "Cannot run ARM64 tests on x64 Azure Pipelines agent" pool: vmImage: 'windows-latest' diff --git a/src/Native/gen-buildsys-win.bat b/src/Native/gen-buildsys-win.bat index c4a76bd4f..f58139e92 100644 --- a/src/Native/gen-buildsys-win.bat +++ b/src/Native/gen-buildsys-win.bat @@ -40,7 +40,7 @@ GOTO :DONE :USAGE echo "Usage..." echo "gen-buildsys-win.bat " - echo "Specify the VSVersion to be used - VS2015, VS2017 or VS2019" + echo "Specify the VSVersion to be used - VS2015, VS2017, VS2019, VS2022, or VS2026" echo "Specify the Target Architecture - x86, x64, or ARM64." EXIT /B 1 diff --git a/src/TorchSharp/Torch.cs b/src/TorchSharp/Torch.cs index c7e6dd608..7dbc42ecd 100644 --- a/src/TorchSharp/Torch.cs +++ b/src/TorchSharp/Torch.cs @@ -39,7 +39,7 @@ public static partial class torch static string nativeRid => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? (RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "win-arm64" : "win-x64") : + ? (RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "win-arm64" : "win-x64") : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? $"linux-x64" : isAppleSilicon ? "osx-arm64" : "any"; From 4edf2e6c721567f658072fe4a09a4088896bfbd0 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Thu, 12 Mar 2026 10:11:32 +0100 Subject: [PATCH 3/6] Fix Windows ARM64 CI build: remove duplicate Release config The job template appends \ to buildScript, so including 'Release' after '-c' resulted in 'dotnet build ... -c Release Release', where the trailing 'Release' was interpreted as a project path (MSB1009). Remove the explicit 'Release' so the template provides it, matching the pattern used by the MacOS_arm64 job. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2df815d25..8beda00e8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -93,7 +93,7 @@ jobs: parameters: prepScript: echo "no prep needed" name: Windows_arm64 - buildScript: dotnet build /p:SkipCuda=true /p:TargetArchitecture=arm64 /p:SkipNetFxBuild=true -c Release + buildScript: dotnet build /p:SkipCuda=true /p:TargetArchitecture=arm64 /p:SkipNetFxBuild=true -c testScript: echo "Cannot run ARM64 tests on x64 Azure Pipelines agent" pool: vmImage: 'windows-latest' From e9759cfce2b94afddd107ff4e63f8346bea2689f Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Wed, 18 Mar 2026 14:09:32 +0100 Subject: [PATCH 4/6] Preload ARM64 native dependencies for NuGet package loading Add preloading of ARM Performance Libraries and other libtorch dependencies on Windows ARM64 before loading torch_cpu.dll and LibTorchSharp.dll. This fixes native library resolution when consuming TorchSharp from NuGet packages, where implicit DLL dependencies cannot be found in the NuGet cache directory. Follows the same pattern as the existing CUDA DLL preloading. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TorchSharp/Torch.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/TorchSharp/Torch.cs b/src/TorchSharp/Torch.cs index 7dbc42ecd..86ec78d7a 100644 --- a/src/TorchSharp/Torch.cs +++ b/src/TorchSharp/Torch.cs @@ -154,6 +154,19 @@ private static void LoadNativeBackend(bool useCudaBackend, out StringBuilder? tr ok = TryLoadNativeLibraryByName("torch_cuda", typeof(torch).Assembly, trace); ok = TryLoadNativeLibraryByName("LibTorchSharp", typeof(torch).Assembly, trace); } else { + var isWindowsArm64 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + RuntimeInformation.ProcessArchitecture == Architecture.Arm64; + if (isWindowsArm64) { + trace.AppendLine($" Try loading Windows ARM64 native components"); + // Preloading these DLLs on Windows ARM64 ensures dependencies are resolved + // when loading from NuGet package directories, similar to the CUDA preloading above. + // ARM64 libtorch uses ARM Performance Libraries instead of Intel OpenMP. + ok = TryLoadNativeLibraryByName("armpl_lp64", typeof(torch).Assembly, trace); + ok = TryLoadNativeLibraryByName("uv", typeof(torch).Assembly, trace); + ok = TryLoadNativeLibraryByName("c10", typeof(torch).Assembly, trace); + ok = TryLoadNativeLibraryByName("torch_global_deps", typeof(torch).Assembly, trace); + } + ok = TryLoadNativeLibraryByName("torch_cpu", typeof(torch).Assembly, trace); ok = TryLoadNativeLibraryByName("LibTorchSharp", typeof(torch).Assembly, trace); } From 71856e2b0c054ffe176f992de2ece5bf1be144c2 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Thu, 19 Mar 2026 14:51:23 +0100 Subject: [PATCH 5/6] Fix CI disk space exhaustion during CUDA libtorch download Add aggressive disk cleanup steps to prevent out-of-memory failures when downloading and extracting large PyTorch 2.10.0 CUDA binaries: - Linux: Remove .git dir and clear NuGet cache in initial cleanup (matching existing Windows behavior) - Both platforms: Delete CPU intermediate extraction dir and downloads before starting CUDA download - Both platforms: Delete CUDA intermediate extraction dir and downloads after CUDA build completes Only intermediate build directories are removed; the final packprep output used for publish artifacts is preserved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines.yml | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8beda00e8..2032427ca 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -117,8 +117,10 @@ jobs: vmImage: 'ubuntu-latest' container: UbuntuContainer steps: - # Initial cleanup + # Initial cleanup - free as much disk space as possible - script: | + rm -rf .git + dotnet nuget locals all --clear 2>/dev/null || true rm -rf bin/obj find . -name "*.pdb" -type f -delete find . -name "*.xml" -type f -delete @@ -145,6 +147,15 @@ jobs: condition: eq('${{ parameters.BuildLibTorchPackages }}', true) displayName: Download libtorch native binaries and cleanup archives + # Free intermediate space before CUDA download + - script: | + rm -rf bin/obj/AnyCPU.$(BuildConfig)/libtorch-cpu + rm -rf bin/downloads + df -h + condition: eq('${{ parameters.BuildLibTorchPackages }}', true) + displayName: Free disk space before CUDA download + continueOnError: true + # Build libtorch CUDA and clean immediately - script: | dotnet build -c $(BuildConfig) src/Redist/libtorch-cuda-12.8/libtorch-cuda-12.8.proj /p:UpdateSHA=true /p:SkipTests=true /p:TargetOS=linux /t:Build /p:IncludeLibTorchCudaPackages=true @@ -154,6 +165,15 @@ jobs: condition: eq('${{ parameters.BuildLibTorchPackages }}', true) displayName: Download libtorch CUDA binaries and cleanup archives + # Free intermediate space after CUDA build + - script: | + rm -rf bin/obj/AnyCPU.$(BuildConfig)/libtorch-cuda-12.8 + rm -rf bin/downloads + df -h + condition: eq('${{ parameters.BuildLibTorchPackages }}', true) + displayName: Free disk space after CUDA build + continueOnError: true + - script: dotnet build -c $(BuildConfig) src/TorchSharp/TorchSharp.csproj /p:SkipCuda=true /p:SkipTests=true displayName: Build TorchSharp @@ -211,6 +231,15 @@ jobs: displayName: Download libtorch native binaries and cleanup condition: eq('${{ parameters.BuildLibTorchPackages }}', true) + # Free intermediate space before CUDA download + - script: | + rmdir /s /q bin\obj\AnyCPU.$(BuildConfig)\libtorch-cpu 2>nul + rmdir /s /q bin\downloads 2>nul + dir + condition: eq('${{ parameters.BuildLibTorchPackages }}', true) + displayName: Free disk space before CUDA download + continueOnError: true + # Build libtorch CUDA and clean immediately - script: | dotnet build -c $(BuildConfig) src/Redist/libtorch-cuda-12.8/libtorch-cuda-12.8.proj /p:UpdateSHA=true /p:SkipTests=true /p:TargetOS=windows /t:Build /p:IncludeLibTorchCudaPackages=true @@ -219,6 +248,14 @@ jobs: condition: eq('${{ parameters.BuildLibTorchPackages }}', true) displayName: Download libtorch CUDA binaries and cleanup + # Free intermediate space after CUDA build + - script: | + rmdir /s /q bin\obj\AnyCPU.$(BuildConfig)\libtorch-cuda-12.8 2>nul + rmdir /s /q bin\downloads 2>nul + condition: eq('${{ parameters.BuildLibTorchPackages }}', true) + displayName: Free disk space after CUDA build + continueOnError: true + - script: dotnet build -c $(BuildConfig) src/TorchSharp/TorchSharp.csproj /p:SkipCuda=true /p:SkipTests=true condition: eq('${{ parameters.BuildLibTorchPackages }}', true) displayName: Build TorchSharp From f7277c1c1a7b03c4dbbd4f01ea3fb2f81e2fc12e Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Mon, 30 Mar 2026 16:59:45 +0200 Subject: [PATCH 6/6] Fix native library loading from NuGet packages on netstandard2.0 consumers The netstandard2.0 build of TorchSharp uses a custom NativeLibrary polyfill (in netstandard.cs) that calls LoadLibraryEx directly without probing deps.json. This means native DLLs in the NuGet package cache are invisible to Step 1 loading. Step 3's fallback only worked for F# Interactive scenarios where TorchSharp.dll was loaded from the NuGet cache path. This fix extends Step 3 with a second resolution path for regular projects: when TorchSharp.dll is in the app output directory (not the NuGet cache), it independently locates the NuGet global packages folder via the NUGET_PACKAGES environment variable or the default ~/.nuget/packages path, then consolidates native DLLs into a single directory for loading. This fixes the issue where consuming CI-produced NuGet packages on Windows ARM64 (targeting net6.0) would fail with: 'doesn't contain a reference to libtorch-cpu-win-arm64' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TorchSharp/Torch.cs | 75 ++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/src/TorchSharp/Torch.cs b/src/TorchSharp/Torch.cs index 86ec78d7a..a7137a88f 100644 --- a/src/TorchSharp/Torch.cs +++ b/src/TorchSharp/Torch.cs @@ -189,31 +189,76 @@ private static void LoadNativeBackend(bool useCudaBackend, out StringBuilder? tr // See https://github.com/dotnet/TorchSharp/issues/169 // - // If we are loading in .NET Interactive or F# Interactive, these are in packages in separate - // package directories. For managed DLLs this works OK, but native DLLs do not load transitive dependencies. + // Native DLLs from NuGet packages may be in separate package directories. + // For managed DLLs this works OK, but native DLLs do not load transitive dependencies + // across different directories. // - // So we shadow copy the DLLs into the TorchSharp package, make a copy of the native DLL and continue - // with the dynamic load + // Additionally, on netstandard2.0 builds the NativeLibrary polyfill uses LoadLibraryEx + // directly and does not probe deps.json, so native DLLs in the NuGet cache are not found. // - // Assumed to be in ...\packages\torchsharp\0.3.0-local-debug-20200918\lib\net6.0\TorchSharp.dll + // We consolidate (shadow copy) all native DLLs into a single directory so that + // inter-DLL dependencies can be resolved by the OS loader. // // TODO: on linux make these copies link not shadow-copy var torchsharpLoc = Path.GetDirectoryName(typeof(torch).Assembly.Location); - var packagesDir = Path.GetFullPath(Path.Combine(torchsharpLoc!, "..", "..", "..", "..")); + + // Try to find the NuGet global packages folder + string? packagesDir = null; + string? torchSharpVersion = null; + + // Path 1: F# Interactive / .NET Interactive - TorchSharp.dll is inside the NuGet cache + // e.g. .../packages/torchsharp/0.106.1/lib/net6.0/TorchSharp.dll + var interactivePackagesDir = Path.GetFullPath(Path.Combine(torchsharpLoc!, "..", "..", "..", "..")); var torchsharpHome = Path.GetFullPath(Path.Combine(torchsharpLoc!, "..", "..")); trace.AppendLine($" torchsharpLoc = {torchsharpLoc}"); - trace.AppendLine($" packagesDir = {packagesDir}"); - trace.AppendLine($" torchsharpHome = {torchsharpHome}"); - if (torchsharpLoc!.Contains("torchsharp") && torchsharpLoc.Contains("lib") && Directory.Exists(packagesDir) && Directory.Exists(torchsharpHome)) { + if (torchsharpLoc!.Contains("torchsharp") && torchsharpLoc.Contains("lib") && Directory.Exists(interactivePackagesDir) && Directory.Exists(torchsharpHome)) { + packagesDir = interactivePackagesDir; + torchSharpVersion = NormalizeNuGetVersion(Path.GetFileName(torchsharpHome)); + trace.AppendLine($" Detected interactive scenario, packagesDir = {packagesDir}, torchSharpVersion = {torchSharpVersion}"); + } + + // Path 2: Regular project - TorchSharp.dll is in the output directory + // Find the NuGet global packages folder independently + if (packagesDir == null) { + trace.AppendLine(" TorchSharp.dll not in NuGet cache, probing NuGet global packages folder..."); + + var nugetPackagesPath = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); + if (string.IsNullOrEmpty(nugetPackagesPath)) { + nugetPackagesPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".nuget", "packages"); + } + + trace.AppendLine($" nugetPackagesPath = {nugetPackagesPath}"); + + if (Directory.Exists(nugetPackagesPath)) { + packagesDir = nugetPackagesPath; + // Determine TorchSharp NuGet package version from the assembly metadata + var asm = typeof(torch).Assembly; + var infoVersionAttr = asm.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false); + if (infoVersionAttr.Length > 0) { + var rawVersion = ((System.Reflection.AssemblyInformationalVersionAttribute)infoVersionAttr[0]).InformationalVersion; + // Strip source hash suffix (e.g. "0.106.1+abc123def") + var versionPart = rawVersion.Split('+')[0]; + torchSharpVersion = NormalizeNuGetVersion(versionPart); + } else { + torchSharpVersion = NormalizeNuGetVersion(asm.GetName().Version!.ToString()); + } + trace.AppendLine($" Resolved packagesDir = {packagesDir}, torchSharpVersion = {torchSharpVersion}"); + } else { + trace.AppendLine($" NuGet packages folder not found at {nugetPackagesPath}"); + } + } + + if (packagesDir != null && torchSharpVersion != null && Directory.Exists(packagesDir)) { - var torchSharpVersion = NormalizeNuGetVersion(Path.GetFileName(torchsharpHome)); var normalizedLibtorchPackageVersion = NormalizeNuGetVersion(libtorchPackageVersion); if (useCudaBackend) { - var consolidatedDir = Path.Combine(torchsharpLoc, $"cuda-{cudaVersion}"); + var consolidatedDir = Path.Combine(torchsharpLoc!, $"cuda-{cudaVersion}"); - trace.AppendLine($" Trying dynamic load for .NET/F# Interactive by consolidating native {cudaRootPackage}-* binaries to {consolidatedDir}..."); + trace.AppendLine($" Trying dynamic load by consolidating native {cudaRootPackage}-* binaries to {consolidatedDir}..."); var cudaOk = CopyNativeComponentsIntoSingleDirectory(packagesDir, $"{cudaRootPackage}-*", normalizedLibtorchPackageVersion, consolidatedDir, trace); if (cudaOk) { @@ -229,9 +274,9 @@ private static void LoadNativeBackend(bool useCudaBackend, out StringBuilder? tr throw new NotSupportedException(message); } } else { - var consolidatedDir = Path.Combine(torchsharpLoc, $"cpu"); + var consolidatedDir = Path.Combine(torchsharpLoc!, $"cpu"); - trace.AppendLine($" Trying dynamic load for .NET/F# Interactive by consolidating native {cpuRootPackage}-* binaries to {consolidatedDir}..."); + trace.AppendLine($" Trying dynamic load by consolidating native {cpuRootPackage} binaries to {consolidatedDir}..."); var cpuOk = CopyNativeComponentsIntoSingleDirectory(packagesDir, cpuRootPackage, normalizedLibtorchPackageVersion, consolidatedDir, trace); if (cpuOk) { @@ -249,7 +294,7 @@ private static void LoadNativeBackend(bool useCudaBackend, out StringBuilder? tr } } else { - trace.AppendLine(" Giving up, TorchSharp.dll does not appear to have been loaded from package directories"); + trace.AppendLine(" Giving up, could not locate NuGet packages directory"); } if (!ok) { var message = $"This application or script uses TorchSharp but doesn't contain a reference to {(useCudaBackend ? cudaRootPackage : cpuRootPackage)}, Version={libtorchPackageVersion}.\n\nConsider referencing one of the combination packages TorchSharp-cpu, TorchSharp-cuda-linux, TorchSharp-cuda-windows or call System.Runtime.InteropServices.NativeLibrary.Load(path-to-{target}) explicitly for a Python install of pytorch. See https://github.com/dotnet/TorchSharp/issues/169.\".\n\nFor CUDA, you may need to call 'TorchSharp.torch.InitializeDeviceType(TorchSharp.DeviceType.CUDA)' before any use of TorchSharp CUDA packages from scripts or notebooks.\n\nTrace from LoadNativeBackend:\n{trace}";