diff --git a/.vsts-dotnet-ci.yml b/.vsts-dotnet-ci.yml
index afd48073a9..35a412e937 100644
--- a/.vsts-dotnet-ci.yml
+++ b/.vsts-dotnet-ci.yml
@@ -144,7 +144,7 @@ jobs:
_targetFramework: net8.0
innerLoop: true
pool:
- vmImage: macOS-15
+ vmImage: macOS-15-arm64
helixQueue: osx.15.arm64.open
- template: /build/ci/job-template.yml
diff --git a/Directory.Build.targets b/Directory.Build.targets
index e086787dc5..510eb2acfc 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -30,10 +30,12 @@
+
-
diff --git a/build/vsts-ci.yml b/build/vsts-ci.yml
index 11f916e718..bc86e5fac8 100644
--- a/build/vsts-ci.yml
+++ b/build/vsts-ci.yml
@@ -31,6 +31,8 @@ variables:
value: 1es-windows-2022
- name: MacImage
value: macOS-15
+- name: MacArm64Image
+ value: macOS-15-arm64
- ${{ if and(notin(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/main')) }}:
- name: enableSourceIndex
value: true
@@ -158,7 +160,7 @@ extends:
################################################################################
pool:
name: Azure Pipelines
- vmImage: $(MacImage)
+ vmImage: $(MacArm64Image)
os: macOS
templateContext:
outputs:
diff --git a/docs/building/unix-instructions.md b/docs/building/unix-instructions.md
index cb9c48a75e..58bf9c0b7e 100644
--- a/docs/building/unix-instructions.md
+++ b/docs/building/unix-instructions.md
@@ -64,15 +64,11 @@ macOS 10.13 (High Sierra) or higher is needed to build dotnet/machinelearning. W
On macOS a few components are needed which are not provided by a default developer setup:
* cmake 3.10.3
-* libomp 7
+* libomp
* gettext
* All the requirements necessary to run .NET Core 3.1 applications. To view macOS prerequisites click [here](https://docs.microsoft.com/en-us/dotnet/core/install/macos?tabs=netcore31#dependencies).
One way of obtaining CMake and other required libraries is via [Homebrew](https://brew.sh):
```sh
-$ brew update && brew install cmake https://raw.githubusercontent.com/dotnet/machinelearning/main/build/libomp.rb gettext && brew link gettext --force && brew link libomp --force
+$ brew update && brew install cmake libomp gettext && brew link gettext --force && brew link libomp --force
```
-
-Please note that newer versions of Homebrew [don't allow installing directly from a URL](https://github.com/Homebrew/brew/issues/8791). If you run into this issue, you may need to download libomp.rb first and install it with the local file instead.
-
-Also, libomp version 7.0.0 doesn't have a cask for Big Sur. You can work around this by downloading the libomp.rb file and then calling `brew install libomp.rb --build-from-source --formula`.
diff --git a/docs/project-docs/components-and-dependencies.md b/docs/project-docs/components-and-dependencies.md
index ce6b175010..031f891fa6 100644
--- a/docs/project-docs/components-and-dependencies.md
+++ b/docs/project-docs/components-and-dependencies.md
@@ -24,7 +24,7 @@ ML.NET redistributes Intel MKL as Microsoft.ML.MKL.Redist in which is a minimize
| `Microsoft.ML.FastTree` | `FastTreeRankingTrainer` | Optional native acceleration | Stable | Native library used on x86/x64; managed fallback |
| `Microsoft.ML.ImageAnalytics` | `MLImage` (image exchange type) | `libSkiaSharp` | Stable | Wrapper over SkiaSharp / Google Skia; supported where dependency is supported |
| `Microsoft.ML.LightGBM` | `LightGbm\*Trainer` | `LightGBM` | Stable | Wrapper over LightGBM; supported where dependency is supported |
-| `Microsoft.ML.MKL.Components` | `SymbolicSgdLogisticRegressionBinaryTrainer` | Intel MKL | Stable | Only works where Intel MKL works |
+| `Microsoft.ML.MKL.Components` | `SymbolicSgdLogisticRegressionBinaryTrainer` | Intel MKL (x86/x64); OpenMP runtime | Stable | Uses a built-in CBLAS implementation on ARM |
| `Microsoft.ML.MKL.Redist` | Internal native Intel MKL | `libomp` | Stable | Not for direct reference; win-x86/x64 only |
| `Microsoft.ML.OneDal` | Internal native Intel OneDal | Intel OneDAL | Preview | Not for direct reference; x64 only |
| `Microsoft.ML.OnnxConverter` | Adds ONNX export support | `Microsoft.ML.OnnxRuntime` | Stable | Wrapper over ONNX Runtime; supports "bring your own" runtime |
@@ -266,4 +266,3 @@ graph TD
class AutoML,CodeGen,FastTree,LightGBM,Recommender,TimeSeries,TorchSharp,ImageAnalytics,DnnFeaturizerAlexNet,DnnFeaturizerResNet18,DnnFeaturizerResNet50,DnnFeaturizerResNet101,DnnFeaturizerModelRedist,Vision,OnnxConverter,OnnxTransformer,TensorFlow,MKLComponents,Ensemble,EntryPoints,Experimental,FairLearn,Parquet,DataAnalysis,GenAICore,GenAILLaMA,GenAIMistral,GenAIPhi,Tokenizers,TokenizersGpt2,TokenizersR50k,TokenizersP50k,TokenizersO200k,TokenizersCl100k,SampleUtils algorithm
class CpuMath,MKLRedist,OneDal bundled
```
-
diff --git a/docs/project-docs/platform-limitations.md b/docs/project-docs/platform-limitations.md
index 28b9cc6e7d..b94dec95d7 100644
--- a/docs/project-docs/platform-limitations.md
+++ b/docs/project-docs/platform-limitations.md
@@ -8,9 +8,11 @@ While ML.NET is cross-platform, there are some limitations for specific platform
| **Windows** | Yes | Yes |
| **Linux** | Yes | Yes |
| **macOS** | Yes | Yes |
-| **ARM64** / **Apple M1** | Yes, with **limitations**.The following are *not supported*:
- Symbolic SGD
- TensorFlow
- OLS
- TimeSeries SSA
- TimeSeries SrCNN
- ONNX*
- Light GBM*
\**You can add support by compiling (no pre-compiled binaries provided).* | Yes, with **limitations**.The following are *not supported*: - Symbolic SGD
- TensorFlow
- OLS
- TimeSeries SSA
- TimeSeries SrCNN
- ONNX
|
+| **ARM64** / **Apple M1** | Yes, with **limitations**.The following are *not supported*:- TensorFlow
- OLS
- TimeSeries SSA
- TimeSeries SrCNN
- ONNX*
- Light GBM*
\**You can add support by compiling (no pre-compiled binaries provided).* | Yes, with **limitations**.The following are *not supported*: - TensorFlow
- OLS
- TimeSeries SSA
- TimeSeries SrCNN
- ONNX
|
| **Blazor WASM** | Yes, with **limitations**.The following are *not supported*:- Symbolic SGD
- TensorFlow
- OLS
- TimeSeries SSA
- TimeSeries SrCNN
- ONNX
- Light GBM
- LDA
- Matrix Factorization
*Note: You must currently set the EnableMLUnsupportedPlatformTargetCheck flag to false to use ML.NET in Blazor.* | Yes, with **limitations**.The following are *not supported*:- Symbolic SGD
- TensorFlow
- OLS
- TimeSeries SSA
- TimeSeries SrCNN
- ONNX
- Light GBM
- LDA
- Matrix Factorization
|
+> Symbolic SGD on macOS requires the Homebrew `libomp` package.
+>
> Note: All the limitations listed above will throw a DLL not found exception.
If you are blocked by any of these limitations or would like to see different behavior when hitting them, please let us know by [filing an issue](https://github.com/dotnet/machinelearning/issues/new?assignees=&labels=&template=suggest-a-feature.md&title=).
diff --git a/eng/helix.proj b/eng/helix.proj
index 71cd4fb2aa..cb50e914ff 100644
--- a/eng/helix.proj
+++ b/eng/helix.proj
@@ -111,8 +111,8 @@
$(HelixPreCommands);export LD_LIBRARY_PATH=/opt/homebrew/opt/mono-libgdiplus/lib;ls /usr/lib;ls $HELIX_WORKITEM_ROOT;export KMP_DUPLICATE_LIB_OK=TRUE;otool -L $HELIX_WORKITEM_ROOT/runtimes/osx-x64/native/lib_lightgbm.dylib
-
- $(HelixPreCommands);export DYLD_LIBRARY_PATH=$HELIX_WORKITEM_ROOT:$DYLD_LIBRARY_PATH;export DYLD_FALLBACK_LIBRARY_PATH=$HELIX_WORKITEM_ROOT:$DYLD_FALLBACK_LIBRARY_PATH
+
+ $(HelixPreCommands);export DYLD_LIBRARY_PATH=$HELIX_WORKITEM_ROOT:$DYLD_LIBRARY_PATH;export DYLD_FALLBACK_LIBRARY_PATH=$HELIX_WORKITEM_ROOT:$DYLD_FALLBACK_LIBRARY_PATH
$(HelixPreCommands);sudo apt update;sudo apt-get install libomp-dev libomp5 -y
@@ -121,10 +121,23 @@
runTests.sh
.\runTests.cmd
+
+
+
+
+
- /usr/local/opt/libomp/lib/libomp.dylib;
+
+ $(MacLibOmpPrefix)/lib/libomp.dylib
+ $(MacLibOmpPath);
+
+
+ Command="install_name_tool -change "$(MacLibOmpPath)" "@loader_path/libomp.dylib" $(BUILD_SOURCESDIRECTORY)/artifacts/bin/%(ProjectsWithTargetFramework.Filename)/$(BuildConfig)/%(ProjectsWithTargetFramework.TargetFrameworks)$(PublishFolder)/libSymSgdNative.dylib" />
-
-
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..c9322aa616 100644
--- a/src/Native/CMakeLists.txt
+++ b/src/Native/CMakeLists.txt
@@ -265,9 +265,11 @@ 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()
+endif()
+
+# On ARM, SymSgdNative compiles the small MklImportsArm CBLAS shim directly
+# (see SymSgdNative/CMakeLists.txt), so we do not build a separate libMklImports.
+add_subdirectory(SymSgdNative)
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..9e829fe250 100644
--- a/src/Native/Native.proj
+++ b/src/Native/Native.proj
@@ -189,9 +189,8 @@
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();