Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .vsts-dotnet-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ jobs:
- template: /build/ci/job-template.yml
parameters:
architecture: arm64
name: MacOS_cross_arm64
name: MacOS_arm64
buildScript: ./build.sh
customMatrixes:
Debug_Build:
Expand All @@ -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
Expand Down
12 changes: 9 additions & 3 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,23 @@
<OutputPath Condition="'$(OutputPath)'==''">$(BinDir)$(MSBuildProjectName)\Debug</OutputPath>
</PropertyGroup>

<ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('osx')) And '$(TargetArchitecture)' == 'arm64' And @(NativeAssemblyReference->AnyHaveMetadataValue('Identity', 'SymSgdNative'))">
<NativeAssemblyReference Include="omp"/>
</ItemGroup>

<ItemGroup>
<NativeAssemblyReference>
<FullAssemblyPath>$(NativeOutputPath)$(LibPrefix)%(NativeAssemblyReference.Identity)$(LibExtension)</FullAssemblyPath>
</NativeAssemblyReference>
</ItemGroup>
</ItemGroup>

<ItemGroup Condition="'$(TargetArchitecture)' == 'arm64' Or '$(TargetArchitecture)' == 'arm'">
<!-- SymSgdNative is built on arm/arm64 (with its CBLAS shim compiled in), so keep it
here so it is copied next to the managed assemblies. There is no separate MklImports
on arm/arm64, so it stays removed like the other MKL-based natives. -->
<NativeAssemblyReference Remove="MklImports"/>
<NativeAssemblyReference Remove="CpuMathNative"/>
<NativeAssemblyReference Remove="FastTreeNative"/>
<NativeAssemblyReference Remove="SymSgdNative"/>
<NativeAssemblyReference Remove="MklProxyNative"/>
<NativeAssemblyReference Remove="libiomp5md"/>
</ItemGroup>
Expand Down
35 changes: 35 additions & 0 deletions THIRD-PARTY-NOTICES.TXT
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,41 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

License notice for LLVM OpenMP Runtime
---------------------------------------

https://github.com/llvm/llvm-project/blob/main/openmp/LICENSE.TXT

The LLVM Project is under the Apache License v2.0 with LLVM Exceptions.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

LLVM Exceptions to the Apache 2.0 License:

As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.

In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.


License notice for SentencePiece
---------------------------------
Expand Down
2 changes: 1 addition & 1 deletion build/vsts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ extends:
################################################################################
pool:
name: Azure Pipelines
vmImage: $(MacImage)
vmImage: macOS-15-arm64
os: macOS
templateContext:
outputs:
Expand Down
4 changes: 1 addition & 3 deletions eng/helix.proj
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
<RunTestScript Condition="'$(OS)' == 'Windows_NT'">.\runTests.cmd</RunTestScript>

<MacFilesToCopy>/usr/local/opt/libomp/lib/libomp.dylib;</MacFilesToCopy>
<MacFilesToCopy Condition="'$(BuildArchitecture)' == 'arm64'">$(BUILD_SOURCESDIRECTORY)\artifacts\bin\Native\arm64.$(BuildConfig)\libomp.dylib;</MacFilesToCopy>
</PropertyGroup>

<!-- Copy OpenMP runtime(s) to the publish folders for OSX -->
Expand All @@ -135,9 +136,6 @@
</Copy>

<!-- Fix up install names where we know about libomp.dylib usage -->
<Exec Condition="Exists('$(BUILD_SOURCESDIRECTORY)\artifacts\bin\%(ProjectsWithTargetFramework.Filename)\$(BuildConfig)\%(ProjectsWithTargetFramework.TargetFrameworks)$(PublishFolder)\libSymSgdNative.dylib') AND $(HelixTargetQueues.ToLowerInvariant().Contains('osx'))"
Command="install_name_tool -change &quot;/usr/local/opt/libomp/lib/libomp.dylib&quot; &quot;@loader_path/libomp.dylib&quot; $(BUILD_SOURCESDIRECTORY)/artifacts/bin/%(ProjectsWithTargetFramework.Filename)/$(BuildConfig)/%(ProjectsWithTargetFramework.TargetFrameworks)$(PublishFolder)/libSymSgdNative.dylib" />

<Exec Condition="Exists('$(BUILD_SOURCESDIRECTORY)\artifacts\bin\%(ProjectsWithTargetFramework.Filename)\$(BuildConfig)\%(ProjectsWithTargetFramework.TargetFrameworks)$(PublishFolder)\runtimes\osx-x64\native\lib_lightgbm.dylib') AND $(HelixTargetQueues.ToLowerInvariant().Contains('osx'))"
Command="install_name_tool -change &quot;/usr/local/opt/libomp/lib/libomp.dylib&quot; &quot;@rpath/libomp.dylib&quot; $(BUILD_SOURCESDIRECTORY)/artifacts/bin/%(ProjectsWithTargetFramework.Filename)/$(BuildConfig)/%(ProjectsWithTargetFramework.TargetFrameworks)$(PublishFolder)/runtimes/osx-x64/native/lib_lightgbm.dylib" />

Expand Down
15 changes: 12 additions & 3 deletions src/Microsoft.ML.Mkl.Components/SymSgdClassificationTrainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);

/// <summary>
Expand Down
7 changes: 5 additions & 2 deletions src/Native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,12 @@ 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.
add_subdirectory(SymSgdNative)
endif()

if(${ARCHITECTURE} MATCHES "[xX].*64")
add_subdirectory(OneDalNative)
Expand Down
118 changes: 118 additions & 0 deletions src/Native/MklImportsArm/MklImportsArm.c
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +5 to +10
//
// 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.
Comment on lines +18 to +24
#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;
}
11 changes: 7 additions & 4 deletions src/Native/Native.proj
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,11 @@
RelativePath="Microsoft.ML.CpuMath\runtimes\$(PackageRid)\nativeassets\net8.0" />
<NativePackageAsset Include="$(NativeAssetsBuiltPath)\$(NativeLibPrefix)LdaNative$(NativeLibExtension)"
RelativePath="Microsoft.ML\runtimes\$(PackageRid)\native" />
<!-- TODO: once we fix the 4 intel MKL methods, SymSgdNative will need to go back in. -->
<NativePackageAsset Condition="'$(TargetArchitecture)' != 'arm64' And '$(TargetArchitecture)' != 'arm'"
Include="$(NativeAssetsBuiltPath)\$(NativeLibPrefix)SymSgdNative$(NativeLibExtension)"
<NativePackageAsset Include="$(NativeAssetsBuiltPath)\$(NativeLibPrefix)SymSgdNative$(NativeLibExtension)"
RelativePath="Microsoft.ML.Mkl.Components\runtimes\$(PackageRid)\native" />
<NativePackageAsset Condition="$([MSBuild]::IsOSPlatform('osx')) And '$(TargetArchitecture)' == 'arm64'"
Include="$(NativeAssetsBuiltPath)\libomp.dylib"
IncludeSymbols="false"
RelativePath="Microsoft.ML.Mkl.Components\runtimes\$(PackageRid)\native" />
<NativePackageAsset Condition="'$(TargetArchitecture)' != 'arm64' And '$(TargetArchitecture)' != 'arm'"
Include="$(NativeAssetsBuiltPath)\$(NativeLibPrefix)MklProxyNative$(NativeLibExtension)"
Expand Down Expand Up @@ -225,7 +227,8 @@

<ItemGroup>
<NativePackageAsset Condition="('$(OS)' == 'Windows_NT' OR '$(StripNativeSymbols)' == 'True')
AND '%(NativePackageAsset.Identity)' != '$(PlaceholderFile)'"
AND '%(NativePackageAsset.Identity)' != '$(PlaceholderFile)'
AND '%(NativePackageAsset.IncludeSymbols)' != 'false'"
Include="@(NativePackageAsset->'%(RootDir)%(Directory)%(Filename)$(NativeLibSymbolExtension)')" />
</ItemGroup>

Expand Down
36 changes: 32 additions & 4 deletions src/Native/SymSgdNative/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ if(APPLE)
# and the else condition can be used instead.
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Xpreprocessor -fopenmp")
SET(OPENMP_LIBRARY "omp")
include_directories("/usr/local/opt/libomp/include")
link_directories("/usr/local/opt/libomp/lib")
# Apple silicon and Intel macs store brew in different locations, this finds it no matter where it is.
execute_process(
COMMAND brew --prefix libomp
RESULT_VARIABLE BREW_LIBOMP
OUTPUT_VARIABLE BREW_LIBOMP_PREFIX
OUTPUT_STRIP_TRAILING_WHITESPACE
)
SET(OPENMP_LIBRARY "${BREW_LIBOMP_PREFIX}/lib/libomp.dylib")
include_directories("${BREW_LIBOMP_PREFIX}/include")
Comment on lines +17 to +24

list(APPEND SOURCES ${VERSION_FILE_PATH})
else()
Expand All @@ -33,7 +40,15 @@ else()
endif()
endif()

if(NOT ${ARCHITECTURE} MATCHES "arm.*")
if(${ARCHITECTURE} MATCHES "arm.*")
# On ARM, Intel MKL is unavailable. Compile the minimal, self-contained CBLAS shim
# (the four level-1 routines SymSGD needs, implemented as plain C loops with no external
# BLAS dependency) directly into SymSgdNative. We deliberately do NOT build or ship a
# separate libMklImports on ARM, so components that require the full MKL (LAPACK/DFTI)
# continue to correctly report it as unavailable there.
list(APPEND SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../MklImportsArm/MklImportsArm.c)
set(MKL_LIBRARY "")
else()
find_library(MKL_LIBRARY MklImports HINTS ${MKL_LIB_PATH})
endif()

Expand All @@ -46,7 +61,20 @@ endif()
target_link_libraries(SymSgdNative PUBLIC ${MKL_LIBRARY} PUBLIC ${OPENMP_LIBRARY})

if(APPLE)
set_target_properties(SymSgdNative PROPERTIES INSTALL_RPATH "@loader_path;@loader_path/${MKL_LIB_RPATH}}")
set_target_properties(SymSgdNative PROPERTIES INSTALL_RPATH "@loader_path")
if(MKL_LIB_RPATH)
set_property(TARGET SymSgdNative APPEND PROPERTY INSTALL_RPATH "@loader_path/${MKL_LIB_RPATH}")
endif()
if(${ARCHITECTURE} STREQUAL "arm64")
add_custom_command(
TARGET SymSgdNative
POST_BUILD
VERBATIM
COMMAND ${CMAKE_INSTALL_NAME_TOOL} -change "${BREW_LIBOMP_PREFIX}/lib/libomp.dylib" "@loader_path/libomp.dylib" $<TARGET_FILE:SymSgdNative>
COMMENT "Making SymSgdNative load libomp from its package directory"
)
install(FILES "${BREW_LIBOMP_PREFIX}/lib/libomp.dylib" DESTINATION .)
endif()
endif()

install_library_and_symbols (SymSgdNative)
18 changes: 12 additions & 6 deletions src/Native/SymSgdNative/SparseBLAS.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
#pragma once
#include "../Stdafx.h"

extern "C" float __cdecl cblas_sdot(const int vecSize, const float* denseVecX, const int incX, const float* denseVecY, const int incY);
extern "C" float __cdecl cblas_sdoti(const int sparseVecSize, const float* sparseVecValues, const int* sparseVecIndices, float* denseVec);
extern "C" void __cdecl cblas_saxpy(const int vecSize, const float coef, const float* denseVecX, const int incX, float* denseVecY, const int incY);
extern "C" void __cdecl cblas_saxpyi(const int sparseVecSize, const float coef, const float* sparseVecValues, const int* sparseVecIndices, float* denseVec);
#ifdef _WIN32
#define CBLAS_CALLING_CONV __cdecl
#else
#define CBLAS_CALLING_CONV
#endif

extern "C" float CBLAS_CALLING_CONV cblas_sdot(const int vecSize, const float* denseVecX, const int incX, const float* denseVecY, const int incY);
extern "C" float CBLAS_CALLING_CONV cblas_sdoti(const int sparseVecSize, const float* sparseVecValues, const int* sparseVecIndices, const float* denseVec);
extern "C" void CBLAS_CALLING_CONV cblas_saxpy(const int vecSize, const float coef, const float* denseVecX, const int incX, float* denseVecY, const int incY);
extern "C" void CBLAS_CALLING_CONV cblas_saxpyi(const int sparseVecSize, const float coef, const float* sparseVecValues, const int* sparseVecIndices, float* denseVec);

float SDOT(const int vecSize, const float* denseVecX, const float* denseVecY)
{
return cblas_sdot(vecSize, denseVecX, 1, denseVecY, 1);
}

float SDOTI(const int sparseVecSize, const int* sparseVecIndices, const float* sparseVecValues, float* denseVec)
float SDOTI(const int sparseVecSize, const int* sparseVecIndices, const float* sparseVecValues, const float* denseVec)
{
return cblas_sdoti(sparseVecSize, sparseVecValues, sparseVecIndices, denseVec);
}
Expand All @@ -28,4 +34,4 @@ void SAXPY(const int vecSize, const float* denseVecX, float* denseVecY, float co
void SAXPYI(const int sparseVecSize, const int* sparseVecIndices, const float* sparseVecValues, float* denseVec, float coef)
{
cblas_saxpyi(sparseVecSize, coef, sparseVecValues, sparseVecIndices, denseVec);
}
}
Loading
Loading