Skip to content

Commit f7277c1

Browse files
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>
1 parent 71856e2 commit f7277c1

File tree

1 file changed

+60
-15
lines changed

1 file changed

+60
-15
lines changed

src/TorchSharp/Torch.cs

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -189,31 +189,76 @@ private static void LoadNativeBackend(bool useCudaBackend, out StringBuilder? tr
189189

190190
// See https://github.com/dotnet/TorchSharp/issues/169
191191
//
192-
// If we are loading in .NET Interactive or F# Interactive, these are in packages in separate
193-
// package directories. For managed DLLs this works OK, but native DLLs do not load transitive dependencies.
192+
// Native DLLs from NuGet packages may be in separate package directories.
193+
// For managed DLLs this works OK, but native DLLs do not load transitive dependencies
194+
// across different directories.
194195
//
195-
// So we shadow copy the DLLs into the TorchSharp package, make a copy of the native DLL and continue
196-
// with the dynamic load
196+
// Additionally, on netstandard2.0 builds the NativeLibrary polyfill uses LoadLibraryEx
197+
// directly and does not probe deps.json, so native DLLs in the NuGet cache are not found.
197198
//
198-
// Assumed to be in ...\packages\torchsharp\0.3.0-local-debug-20200918\lib\net6.0\TorchSharp.dll
199+
// We consolidate (shadow copy) all native DLLs into a single directory so that
200+
// inter-DLL dependencies can be resolved by the OS loader.
199201
//
200202
// TODO: on linux make these copies link not shadow-copy
201203
var torchsharpLoc = Path.GetDirectoryName(typeof(torch).Assembly.Location);
202-
var packagesDir = Path.GetFullPath(Path.Combine(torchsharpLoc!, "..", "..", "..", ".."));
204+
205+
// Try to find the NuGet global packages folder
206+
string? packagesDir = null;
207+
string? torchSharpVersion = null;
208+
209+
// Path 1: F# Interactive / .NET Interactive - TorchSharp.dll is inside the NuGet cache
210+
// e.g. .../packages/torchsharp/0.106.1/lib/net6.0/TorchSharp.dll
211+
var interactivePackagesDir = Path.GetFullPath(Path.Combine(torchsharpLoc!, "..", "..", "..", ".."));
203212
var torchsharpHome = Path.GetFullPath(Path.Combine(torchsharpLoc!, "..", ".."));
204213

205214
trace.AppendLine($" torchsharpLoc = {torchsharpLoc}");
206-
trace.AppendLine($" packagesDir = {packagesDir}");
207-
trace.AppendLine($" torchsharpHome = {torchsharpHome}");
208215

209-
if (torchsharpLoc!.Contains("torchsharp") && torchsharpLoc.Contains("lib") && Directory.Exists(packagesDir) && Directory.Exists(torchsharpHome)) {
216+
if (torchsharpLoc!.Contains("torchsharp") && torchsharpLoc.Contains("lib") && Directory.Exists(interactivePackagesDir) && Directory.Exists(torchsharpHome)) {
217+
packagesDir = interactivePackagesDir;
218+
torchSharpVersion = NormalizeNuGetVersion(Path.GetFileName(torchsharpHome));
219+
trace.AppendLine($" Detected interactive scenario, packagesDir = {packagesDir}, torchSharpVersion = {torchSharpVersion}");
220+
}
221+
222+
// Path 2: Regular project - TorchSharp.dll is in the output directory
223+
// Find the NuGet global packages folder independently
224+
if (packagesDir == null) {
225+
trace.AppendLine(" TorchSharp.dll not in NuGet cache, probing NuGet global packages folder...");
226+
227+
var nugetPackagesPath = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
228+
if (string.IsNullOrEmpty(nugetPackagesPath)) {
229+
nugetPackagesPath = Path.Combine(
230+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
231+
".nuget", "packages");
232+
}
233+
234+
trace.AppendLine($" nugetPackagesPath = {nugetPackagesPath}");
235+
236+
if (Directory.Exists(nugetPackagesPath)) {
237+
packagesDir = nugetPackagesPath;
238+
// Determine TorchSharp NuGet package version from the assembly metadata
239+
var asm = typeof(torch).Assembly;
240+
var infoVersionAttr = asm.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false);
241+
if (infoVersionAttr.Length > 0) {
242+
var rawVersion = ((System.Reflection.AssemblyInformationalVersionAttribute)infoVersionAttr[0]).InformationalVersion;
243+
// Strip source hash suffix (e.g. "0.106.1+abc123def")
244+
var versionPart = rawVersion.Split('+')[0];
245+
torchSharpVersion = NormalizeNuGetVersion(versionPart);
246+
} else {
247+
torchSharpVersion = NormalizeNuGetVersion(asm.GetName().Version!.ToString());
248+
}
249+
trace.AppendLine($" Resolved packagesDir = {packagesDir}, torchSharpVersion = {torchSharpVersion}");
250+
} else {
251+
trace.AppendLine($" NuGet packages folder not found at {nugetPackagesPath}");
252+
}
253+
}
254+
255+
if (packagesDir != null && torchSharpVersion != null && Directory.Exists(packagesDir)) {
210256

211-
var torchSharpVersion = NormalizeNuGetVersion(Path.GetFileName(torchsharpHome));
212257
var normalizedLibtorchPackageVersion = NormalizeNuGetVersion(libtorchPackageVersion);
213258
if (useCudaBackend) {
214-
var consolidatedDir = Path.Combine(torchsharpLoc, $"cuda-{cudaVersion}");
259+
var consolidatedDir = Path.Combine(torchsharpLoc!, $"cuda-{cudaVersion}");
215260

216-
trace.AppendLine($" Trying dynamic load for .NET/F# Interactive by consolidating native {cudaRootPackage}-* binaries to {consolidatedDir}...");
261+
trace.AppendLine($" Trying dynamic load by consolidating native {cudaRootPackage}-* binaries to {consolidatedDir}...");
217262

218263
var cudaOk = CopyNativeComponentsIntoSingleDirectory(packagesDir, $"{cudaRootPackage}-*", normalizedLibtorchPackageVersion, consolidatedDir, trace);
219264
if (cudaOk) {
@@ -229,9 +274,9 @@ private static void LoadNativeBackend(bool useCudaBackend, out StringBuilder? tr
229274
throw new NotSupportedException(message);
230275
}
231276
} else {
232-
var consolidatedDir = Path.Combine(torchsharpLoc, $"cpu");
277+
var consolidatedDir = Path.Combine(torchsharpLoc!, $"cpu");
233278

234-
trace.AppendLine($" Trying dynamic load for .NET/F# Interactive by consolidating native {cpuRootPackage}-* binaries to {consolidatedDir}...");
279+
trace.AppendLine($" Trying dynamic load by consolidating native {cpuRootPackage} binaries to {consolidatedDir}...");
235280

236281
var cpuOk = CopyNativeComponentsIntoSingleDirectory(packagesDir, cpuRootPackage, normalizedLibtorchPackageVersion, consolidatedDir, trace);
237282
if (cpuOk) {
@@ -249,7 +294,7 @@ private static void LoadNativeBackend(bool useCudaBackend, out StringBuilder? tr
249294
}
250295
}
251296
else {
252-
trace.AppendLine(" Giving up, TorchSharp.dll does not appear to have been loaded from package directories");
297+
trace.AppendLine(" Giving up, could not locate NuGet packages directory");
253298
}
254299
if (!ok) {
255300
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}";

0 commit comments

Comments
 (0)