-
Notifications
You must be signed in to change notification settings - Fork 4.9k
[Apple Silicon Support Phase 1] Add Metal FusedAdam kernel and CPU Adam build for Apple Silicon #8300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Apple Silicon Support Phase 1] Add Metal FusedAdam kernel and CPU Adam build for Apple Silicon #8300
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| // DeepSpeed Team | ||
|
|
||
| // Fused Adam / AdamW step for one tensor. Mirrors csrc/adam/multi_tensor_adam.cu: all math is done in | ||
| // fp32 regardless of the storage dtype, so fp16/bf16 parameters see the same numerics as on CUDA. | ||
|
|
||
| #include <metal_stdlib> | ||
| using namespace metal; | ||
|
|
||
| template <typename T> | ||
| kernel void fused_adam(device T* param [[buffer(0)]], | ||
| device const T* grad [[buffer(1)]], | ||
| device T* exp_avg [[buffer(2)]], | ||
| device T* exp_avg_sq [[buffer(3)]], | ||
| constant float& lr [[buffer(4)]], | ||
| constant float& beta1 [[buffer(5)]], | ||
| constant float& beta2 [[buffer(6)]], | ||
| constant float& epsilon [[buffer(7)]], | ||
| constant float& weight_decay [[buffer(8)]], | ||
| constant float& bias_correction1 [[buffer(9)]], | ||
| constant float& bias_correction2 [[buffer(10)]], | ||
| constant uint& adam_w_mode [[buffer(11)]], | ||
| uint i [[thread_position_in_grid]]) | ||
| { | ||
| float g = float(grad[i]); | ||
| float p = float(param[i]); | ||
| float m = float(exp_avg[i]); | ||
| float v = float(exp_avg_sq[i]); | ||
|
|
||
| // L2 mode folds weight decay into the gradient; AdamW mode applies it to the parameter. | ||
| if (adam_w_mode == 0) { g += weight_decay * p; } | ||
| m = beta1 * m + (1.0f - beta1) * g; | ||
| v = beta2 * v + (1.0f - beta2) * g * g; | ||
| float denom = sqrt(v / bias_correction2) + epsilon; | ||
| float update = (m / bias_correction1) / denom; | ||
| if (adam_w_mode != 0) { update += weight_decay * p; } | ||
|
|
||
| param[i] = T(p - lr * update); | ||
| exp_avg[i] = T(m); | ||
| exp_avg_sq[i] = T(v); | ||
| } | ||
|
|
||
| template [[host_name("fused_adam_float")]] kernel void fused_adam<float>( | ||
| device float*, device const float*, device float*, device float*, | ||
| constant float&, constant float&, constant float&, constant float&, constant float&, | ||
| constant float&, constant float&, constant uint&, uint); | ||
| template [[host_name("fused_adam_half")]] kernel void fused_adam<half>( | ||
| device half*, device const half*, device half*, device half*, | ||
| constant float&, constant float&, constant float&, constant float&, constant float&, | ||
| constant float&, constant float&, constant uint&, uint); | ||
| // bfloat is a Metal 3.1 type (macOS 14); older toolchains still get the float and half kernels. | ||
| #if __METAL_VERSION__ >= 310 | ||
| template [[host_name("fused_adam_bfloat")]] kernel void fused_adam<bfloat>( | ||
| device bfloat*, device const bfloat*, device bfloat*, device bfloat*, | ||
| constant float&, constant float&, constant float&, constant float&, constant float&, | ||
| constant float&, constant float&, constant uint&, uint); | ||
| #endif | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |
|
|
||
| # DeepSpeed Team | ||
|
|
||
| import os | ||
|
|
||
| try: | ||
| # is op_builder from deepspeed or a 3p version? this should only succeed if it's deepspeed | ||
| # if successful this also means we're doing a local install and not JIT compile path | ||
|
|
@@ -12,11 +14,7 @@ | |
|
|
||
|
|
||
| class MPSOpBuilder(OpBuilder): | ||
| """Base class for ops on Apple Silicon. | ||
|
|
||
| Ops here are currently pure PyTorch (torch.mps) implementations, so there is nothing to compile. | ||
| Metal kernels will plug into the same builder classes later. | ||
| """ | ||
| """Base class for ops on Apple Silicon that need no C++ compilation (pure torch.mps implementations).""" | ||
|
|
||
| def sources(self): | ||
| return [] | ||
|
|
@@ -27,3 +25,33 @@ def include_paths(self): | |
| def is_compatible(self, verbose=False): | ||
| import torch | ||
| return hasattr(torch.backends, "mps") and torch.backends.mps.is_available() | ||
|
|
||
|
|
||
| class MetalOpBuilder(MPSOpBuilder): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is that true that ops on Metal are not compiled and saved on disk cache? Is it temporary or its nature of Metal?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the nature of the Metal workflow rather than a temporary gap: torch.mps.compile_shader compiles these small kernels in milliseconds at first load, and macOS's Metal framework maintains its own per-app on-disk cache of compiled pipelines, so a torch-extensions style build cache would add complexity without saving anything. Documented in the MetalOpBuilder docstring in 34f7cb7. |
||
| """Base class for ops implemented as Metal shaders. | ||
|
|
||
| Shaders are compiled at load time through torch.mps.compile_shader, which dispatches them on | ||
| PyTorch's own MPS command stream, so no Xcode toolchain or C++ extension build is needed. | ||
| There is deliberately no torch-extensions style disk cache: runtime compilation of these | ||
| kernels takes milliseconds, and macOS's Metal framework keeps its own per-app cache of | ||
| compiled pipelines. Subclasses list their .metal files in metal_sources() and get back the | ||
| compiled library. | ||
| """ | ||
| _libraries = {} | ||
|
|
||
| def metal_sources(self): | ||
| return [] | ||
|
|
||
| def is_compatible(self, verbose=False): | ||
| import torch | ||
| return super().is_compatible(verbose) and hasattr(torch.mps, "compile_shader") | ||
|
|
||
| def load_metal_library(self): | ||
| import torch | ||
| if self.name not in MetalOpBuilder._libraries: | ||
| source = "" | ||
| for path in self.metal_sources(): | ||
| with open(os.path.join(self.deepspeed_src_path(path))) as shader_file: | ||
| source += shader_file.read() + "\n" | ||
| MetalOpBuilder._libraries[self.name] = torch.mps.compile_shader(source) | ||
| return MetalOpBuilder._libraries[self.name] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| # DeepSpeed Team | ||
|
|
||
| import os | ||
| import subprocess | ||
|
|
||
| from .builder import MPSOpBuilder | ||
|
|
||
|
|
||
| class CPUAdamBuilder(MPSOpBuilder): | ||
| """Builds the C++ CPU Adam kernel on Apple Silicon for ZeRO-Offload. | ||
|
|
||
| Unified memory means the optimizer step can run on the CPU cores without copying parameters | ||
| off the GPU, so this is the offload path on Macs until a Metal Adam kernel exists. | ||
| """ | ||
| BUILD_VAR = "DS_BUILD_CPU_ADAM" | ||
| NAME = "cpu_adam" | ||
|
|
||
| def __init__(self): | ||
| super().__init__(name=self.NAME) | ||
|
|
||
| def absolute_name(self): | ||
| return f'deepspeed.ops.adam.{self.NAME}_op' | ||
|
|
||
| def sources(self): | ||
| return ['csrc/adam/cpu_adam.cpp', 'csrc/adam/cpu_adam_impl.cpp'] | ||
|
|
||
| def include_paths(self): | ||
| return ['csrc/includes'] | ||
|
|
||
| def builder(self): | ||
| from torch.utils.cpp_extension import CppExtension | ||
| include_dirs = [os.path.abspath(path) for path in self.strip_empty_entries(self.include_paths())] | ||
| return CppExtension(name=self.absolute_name(), | ||
| sources=self.strip_empty_entries(self.sources()), | ||
| include_dirs=include_dirs, | ||
| extra_compile_args={'cxx': self.strip_empty_entries(self.cxx_args())}, | ||
| extra_link_args=self.strip_empty_entries(self.extra_ldflags())) | ||
|
|
||
| def _libomp_prefix(self): | ||
| # Apple clang ships without OpenMP; Homebrew's libomp provides it when installed. | ||
| try: | ||
| return subprocess.check_output(['brew', '--prefix', 'libomp'], stderr=subprocess.DEVNULL).decode().strip() | ||
| except (OSError, subprocess.CalledProcessError): | ||
| return None | ||
|
|
||
| def cxx_args(self): | ||
| args = ['-O3', '-std=c++17', '-g', '-Wno-reorder', '-D__SCALAR__'] | ||
| libomp = self._libomp_prefix() | ||
| if libomp is not None: | ||
| args += ['-Xpreprocessor', '-fopenmp', f'-I{libomp}/include'] | ||
| return args | ||
|
|
||
| def extra_ldflags(self): | ||
| libomp = self._libomp_prefix() | ||
| if libomp is None: | ||
| return [] | ||
| return [f'-L{libomp}/lib', '-lomp'] |
Uh oh!
There was an error while loading. Please reload this page.