diff --git a/Directory.Build.targets b/Directory.Build.targets index e086787dc5..f24ee72ef3 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -30,13 +30,21 @@ + - + + + + + diff --git a/src/Microsoft.ML.Mkl.Components/SymSgdClassificationTrainer.cs b/src/Microsoft.ML.Mkl.Components/SymSgdClassificationTrainer.cs index a56ecda0ba..d83f09e776 100644 --- a/src/Microsoft.ML.Mkl.Components/SymSgdClassificationTrainer.cs +++ b/src/Microsoft.ML.Mkl.Components/SymSgdClassificationTrainer.cs @@ -825,7 +825,16 @@ private void CheckLabel(RoleMappedData examples, out int weightSetCount) private static unsafe class Native { //To triger the loading of MKL library since SymSGD native library depends on it. - static Native() => ErrorMessage(0); + //On ARM there is no MKL: SymSgdNative bundles the small CBLAS shim it needs and no + //libMklImports is shipped, so skip this call (it would fail to load MklImports). + static Native() + { + if (RuntimeInformation.ProcessArchitecture != Architecture.Arm64 && + RuntimeInformation.ProcessArchitecture != Architecture.Arm) + { + ErrorMessage(0); + } + } internal const string NativePath = "SymSgdNative"; internal const string MklPath = "MklImports"; @@ -834,8 +843,8 @@ private static unsafe class Native [DllImport(NativePath), SuppressUnmanagedCodeSecurity] private static extern void LearnAll(int totalNumInstances, int* instSizes, int** instIndices, - float** instValues, float* labels, bool tuneLR, ref float lr, float l2Const, float piw, float* weightVector, ref float bias, - int numFeatres, int numPasses, int numThreads, bool tuneNumLocIter, ref int numLocIter, float tolerance, bool needShuffle, bool shouldInitialize, + float** instValues, float* labels, [MarshalAs(UnmanagedType.I1)] bool tuneLR, ref float lr, float l2Const, float piw, float* weightVector, ref float bias, + int numFeatres, int numPasses, int numThreads, [MarshalAs(UnmanagedType.I1)] bool tuneNumLocIter, ref int numLocIter, float tolerance, [MarshalAs(UnmanagedType.I1)] bool needShuffle, [MarshalAs(UnmanagedType.I1)] bool shouldInitialize, State* state, ChannelCallBack info); /// diff --git a/src/Native/CMakeLists.txt b/src/Native/CMakeLists.txt index 9e3647ede1..8602e9dde6 100644 --- a/src/Native/CMakeLists.txt +++ b/src/Native/CMakeLists.txt @@ -265,9 +265,16 @@ if(NOT ${ARCHITECTURE} MATCHES "arm.*") add_subdirectory(CpuMathNative) add_subdirectory(FastTreeNative) add_subdirectory(MklProxyNative) - # TODO: once we fix the 4 intel MKL methods, SymSgdNative will need to go back in. add_subdirectory(SymSgdNative) - endif() +else() + # On ARM, SymSgdNative compiles the small MklImportsArm CBLAS shim directly + # (see SymSgdNative/CMakeLists.txt), so we do not build a separate libMklImports here. + # SymSGD needs OpenMP, which is unavailable for arm64 on the macOS cross-compilation + # runner (it only ships an x86_64 libomp), so SymSgdNative is built on Windows/Linux arm only. + if(NOT APPLE) + add_subdirectory(SymSgdNative) + endif() +endif() if(${ARCHITECTURE} MATCHES "[xX].*64") add_subdirectory(OneDalNative) diff --git a/src/Native/MklImportsArm/MklImportsArm.c b/src/Native/MklImportsArm/MklImportsArm.c new file mode 100644 index 0000000000..b0db577a39 --- /dev/null +++ b/src/Native/MklImportsArm/MklImportsArm.c @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// ARM replacement for Intel MKL (libMklImports.so). +// +// This provides a small, self-contained libMklImports for arm/arm64 that +// covers exactly the symbols SymSGD needs, with no external BLAS dependency. +// That is important because the cross-compilation sysroots used in CI do not +// ship OpenBLAS (or any system BLAS), so linking against one is not an option. +// +// SymSGD uses only four CBLAS routines: +// * cblas_sdot / cblas_saxpy - dense single-precision dot and AXPY, +// * cblas_sdoti / cblas_saxpyi - their sparse counterparts (MKL extensions). +// All four are implemented below as plain C loops. With -O3 the compiler +// autovectorizes the dense paths to NEON, matching hand-written BLAS closely. +// +// MKL DFTI (FFT) functions are stubbed — they are referenced by the managed +// MKL Components initializer but not used by SymSGD. The stubs return error +// codes so any actual FFT call fails cleanly rather than crashing. + +// The native build is compiled with -fvisibility=hidden, so every symbol that +// must be visible to SymSgdNative (the CBLAS routines) or to the managed +// P/Invoke layer (DftiErrorMessage) has to be exported explicitly. +#if defined(_WIN32) +#define MKLIMPORTS_EXPORT __declspec(dllexport) +#else +#define MKLIMPORTS_EXPORT __attribute__((visibility("default"))) +#endif + +// --- Dense BLAS (CBLAS, level 1) --- + +MKLIMPORTS_EXPORT float cblas_sdot(const int n, const float *x, const int incx, + const float *y, const int incy) +{ + float result = 0.0f; + if (incx == 1 && incy == 1) + { + for (int i = 0; i < n; i++) + result += x[i] * y[i]; + } + else + { + int ix = incx < 0 ? (1 - n) * incx : 0; + int iy = incy < 0 ? (1 - n) * incy : 0; + for (int i = 0; i < n; i++, ix += incx, iy += incy) + result += x[ix] * y[iy]; + } + return result; +} + +MKLIMPORTS_EXPORT void cblas_saxpy(const int n, const float a, const float *x, const int incx, + float *y, const int incy) +{ + if (a == 0.0f) + return; + if (incx == 1 && incy == 1) + { + for (int i = 0; i < n; i++) + y[i] += a * x[i]; + } + else + { + int ix = incx < 0 ? (1 - n) * incx : 0; + int iy = incy < 0 ? (1 - n) * incy : 0; + for (int i = 0; i < n; i++, ix += incx, iy += incy) + y[iy] += a * x[ix]; + } +} + +// --- Sparse BLAS (MKL extensions, not in standard BLAS) --- + +MKLIMPORTS_EXPORT void cblas_saxpyi(const int nz, const float a, + const float *x, const int *indx, float *y) +{ + for (int i = 0; i < nz; i++) + y[indx[i]] += a * x[i]; +} + +MKLIMPORTS_EXPORT float cblas_sdoti(const int nz, const float *x, + const int *indx, const float *y) +{ + float result = 0.0f; + for (int i = 0; i < nz; i++) + result += x[i] * y[indx[i]]; + return result; +} + +// --- DFTI (FFT) stubs --- + +MKLIMPORTS_EXPORT const char* DftiErrorMessage(long status) +{ + return "DFTI not available (arm64 MKL shim build)"; +} + +MKLIMPORTS_EXPORT long DftiCreateDescriptor(void **h, int precision, int domain, int dim, ...) +{ + *h = (void*)0; + return -1; +} + +MKLIMPORTS_EXPORT long DftiSetValue(void *h, int param, ...) +{ + return -1; +} + +MKLIMPORTS_EXPORT long DftiCommitDescriptor(void *h) { return -1; } +MKLIMPORTS_EXPORT long DftiComputeForward(void *h, ...) { return -1; } +MKLIMPORTS_EXPORT long DftiComputeBackward(void *h, ...) { return -1; } +MKLIMPORTS_EXPORT long DftiFreeDescriptor(void **h) +{ + // Match MKL's contract: clear the caller's handle after freeing so callers + // that rely on the descriptor being nulled out (e.g. the managed + // FreeDescriptor(ref IntPtr) P/Invoke) behave correctly. + if (h != (void*)0) + *h = (void*)0; + return 0; +} diff --git a/src/Native/Native.proj b/src/Native/Native.proj index 02a95495bf..22a36bd566 100644 --- a/src/Native/Native.proj +++ b/src/Native/Native.proj @@ -189,8 +189,10 @@ RelativePath="Microsoft.ML.CpuMath\runtimes\$(PackageRid)\nativeassets\net8.0" /> - - + - [NativeDependencyFact("MklImports")] + [NativeDependencyFact("SymSgdNative")] public void SimpleTrainAndPredictSymSGD() { var ml = new MLContext(seed: 1); diff --git a/test/Microsoft.ML.Tests/TrainerEstimators/SymSgdClassificationTests.cs b/test/Microsoft.ML.Tests/TrainerEstimators/SymSgdClassificationTests.cs index 9a2d0aaf13..b1597469d6 100644 --- a/test/Microsoft.ML.Tests/TrainerEstimators/SymSgdClassificationTests.cs +++ b/test/Microsoft.ML.Tests/TrainerEstimators/SymSgdClassificationTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.ML.Tests.TrainerEstimators { public partial class TrainerEstimators { - [NativeDependencyFact("MklImports")] + [NativeDependencyFact("SymSgdNative")] public void TestEstimatorSymSgdClassificationTrainer() { (var pipe, var dataView) = GetBinaryClassificationPipeline(); @@ -27,7 +27,7 @@ public void TestEstimatorSymSgdClassificationTrainer() Done(); } - [NativeDependencyFact("MklImports")] + [NativeDependencyFact("SymSgdNative")] public void TestEstimatorSymSgdInitPredictor() { (var pipe, var dataView) = GetBinaryClassificationPipeline();