Skip to content

Commit

Permalink
add
Browse files Browse the repository at this point in the history
  • Loading branch information
CjhHa1 committed Apr 23, 2024
1 parent f57b12d commit b9305fb
Show file tree
Hide file tree
Showing 69 changed files with 9,900 additions and 2 deletions.
1 change: 0 additions & 1 deletion colossalai/kernel/extensions

This file was deleted.

140 changes: 140 additions & 0 deletions colossalai/kernel/extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# 🔌 Extensions

## 📌 Table of Contents

- [🔌 Extensions](#-extensions)
- [📌 Table of Contents](#-table-of-contents)
- [📚 Introduction](#-introduction)
- [🪅 Design](#-design)
- [🛠 API Usage](#-api-usage)
- [🏗 Write a customized extension](#-write-a-customized-extension)
- [✏️ Acknowledgement](#️-acknowledgement)

## 📚 Introduction

This module is a designed to offer extensions to the existing ColossalAI framework. It is designed to be a collection of high-performance kernels to speed up the training and inference process. Different from writing an individual kernel, the `extensions` module offers a layer of abstraction to collate kernels written in different compiler backends and for different hardware backends in an organized way. Please see the design and usage in the sections below.

## 🪅 Design

The `extensions` module is a sub-module of the `colossalai.kernel` module. This module is put at the project root directory so that it can be imported for AOT (ahead-of-time) build. At the same time, it is symbolically linked at the `colossalai.kernel.extensions` path for runtime build.

As we want to support multi-backend kernels, we have to consider multiple compiler options such as `torch.jit`, `CUDA`, `triton` and multiple hardware backends such as `CPU`, `GPU` and `NPU`. To make it easy for the users, we have abstract away the kernels into extensions and expose a single loader to the user for each kind of kernel.

For example, if the user wants to use the CPU Adam kernel, he can just call `load()` on the kernel loader. The kernel loader will automatically select the correct extension based on the current hardware and compiler backend. The user does not need to worry about the details of the kernel implementation. For example, if the user is using ARM CPU, then Arm kernel will be built and loaded. If it is a X86 CPU, then it is the X86 kernel that will be loaded.

```python
from colossalai.kernel.kernel_loader import CPUAdamLoader

# load the kernel compatible with the current hardware
kernel = CPUAdamLoader().load()
```

![](https://github.com/hpcaitech/public_assets/blob/main/colossalai/img/extensions.png?raw=true)

## 🛠 API Usage

To make the `colossalai.kernel` easy to use, we expose some simple APIs and you can use them based on your scenario.

- Case 1: Simply load a kernel

```python
from colossalai.kernel.kernel_loader import CPUAdamLoader

# load the kernel compatible with the current hardware
kernel = CPUAdamLoader().load()
```

- Case 2: Load a specific kernel

This case applies if you are familiar with the extensions available.

```python
from colossalai.kernel.kernel_loader import CPUAdamLoader

# load the kernel by giving the kernel name
kernel = CPUAdamLoader().load(ext_name="cpu_adam_arm")
```

- Case 3: Register your own extension

This case applies if you know how to write an extension. If you do not know how, you can refer to the section below.

```python
from colossalai.kernel.kernel_loader import CPUAdamLoader
from colossalai.kernel.base_extension import _Extension

# create your own extension class
class MyExtension(_Extension):

def __init__(self):
self._name = "my_extension"
self._support_aot = True
self._support_jit = True
self.priority = 10

# implementation here
...

# register your extension
# you can use the priority value to make sure your kernel will be loaded by default
CPUAdamLoader.register_extension(MyExtension)

# load the kernel
kernel = CPUAdamLoader().load()
```

## 🏗 Write a customized extension

It is easy to write a customized extension. If you have experience writing CUDA/triton kernels, you should get familiar with the process quickly.

You just need to inherit the `_Extension` base class or other backend-specific classes such as `_CudaExtension` and implement the abstract methods. Then, you need to register your extension to the kernel loader based on the Case 3 above. The kernel loader will automatically select the correct extension based on the priority score, current hardware, compiler backend.

```python
from colossalai.kernel.base_extension import _Extension


class MyExtension(_Extension):

def __init__(self):
self._name = "my_extension"
self._support_aot = True
self._support_jit = True
self.priority = 10

def is_available(self) -> bool:
"""
Return if the required hardware can be found.
"""
...

def assert_compatible(self) -> None:
"""
Check if the hardware required by the kernel is compatible.
"""
...

def build_aot(self) -> Union["CppExtension", "CUDAExtension"]:
"""
If this kernel can be built AOT, it should return an extension object
to Python setuptools for compilation.
"""
...

def build_jit(self) -> Callable:
"""
Build extension kernel just in time.
"""
...

def load(self):
"""
The API called by the user to get the kernel.
"""
...

```

## ✏️ Acknowledgement

This module is written from scratch but we learnt a lot by looking into [DeepSpeed'
s op_builder](https://github.com/microsoft/DeepSpeed/tree/master/op_builder). We wish to acknowledge their great work and contributions to the open-source community.
35 changes: 35 additions & 0 deletions colossalai/kernel/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from .cpu_adam import CpuAdamArmExtension, CpuAdamX86Extension
from .flash_attention import FlashAttentionDaoCudaExtension, FlashAttentionNpuExtension, FlashAttentionSdpaCudaExtension
from .inference import InferenceOpsCudaExtension
from .layernorm import LayerNormCudaExtension
from .moe import MoeCudaExtension
from .optimizer import FusedOptimizerCudaExtension
from .softmax import ScaledMaskedSoftmaxCudaExtension, ScaledUpperTriangleMaskedSoftmaxCudaExtension

ALL_EXTENSIONS = [
CpuAdamArmExtension,
CpuAdamX86Extension,
LayerNormCudaExtension,
MoeCudaExtension,
FusedOptimizerCudaExtension,
InferenceOpsCudaExtension,
ScaledMaskedSoftmaxCudaExtension,
ScaledUpperTriangleMaskedSoftmaxCudaExtension,
FlashAttentionDaoCudaExtension,
FlashAttentionSdpaCudaExtension,
FlashAttentionNpuExtension,
]

__all__ = [
"CpuAdamArmExtension",
"CpuAdamX86Extension",
"LayerNormCudaExtension",
"MoeCudaExtension",
"FusedOptimizerCudaExtension",
"InferenceOpsCudaExtension",
"ScaledMaskedSoftmaxCudaExtension",
"ScaledUpperTriangleMaskedSoftmaxCudaExtension",
"FlashAttentionDaoCudaExtension",
"FlashAttentionSdpaCudaExtension",
"FlashAttentionNpuExtension",
]
82 changes: 82 additions & 0 deletions colossalai/kernel/extensions/base_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import hashlib
import os
from abc import ABC, abstractmethod
from typing import Callable, Union

__all__ = ["_Extension"]


class _Extension(ABC):
def __init__(self, name: str, support_aot: bool, support_jit: bool, priority: int = 1):
self._name = name
self._support_aot = support_aot
self._support_jit = support_jit
self.priority = priority

@property
def name(self):
return self._name

@property
def support_aot(self):
return self._support_aot

@property
def support_jit(self):
return self._support_jit

@staticmethod
def get_jit_extension_folder_path():
"""
Kernels which are compiled during runtime will be stored in the same cache folder for reuse.
The folder is in the path ~/.cache/colossalai/torch_extensions/<cache-folder>.
The name of the <cache-folder> follows a common format:
torch<torch_version_major>.<torch_version_minor>_<device_name><device_version>-<hash>
The <hash> suffix is the hash value of the path of the `colossalai` file.
"""
import torch

import colossalai
from colossalai.accelerator import get_accelerator

# get torch version
torch_version_major = torch.__version__.split(".")[0]
torch_version_minor = torch.__version__.split(".")[1]

# get device version
device_name = get_accelerator().name
device_version = get_accelerator().get_version()

# use colossalai's file path as hash
hash_suffix = hashlib.sha256(colossalai.__file__.encode()).hexdigest()

# concat
home_directory = os.path.expanduser("~")
extension_directory = f".cache/colossalai/torch_extensions/torch{torch_version_major}.{torch_version_minor}_{device_name}-{device_version}-{hash_suffix}"
cache_directory = os.path.join(home_directory, extension_directory)
return cache_directory

@abstractmethod
def is_available(self) -> bool:
"""
Check if the hardware required by the kernel is available.
"""

@abstractmethod
def assert_compatible(self) -> None:
"""
Check if the hardware required by the kernel is compatible.
"""

@abstractmethod
def build_aot(self) -> Union["CppExtension", "CUDAExtension"]:
pass

@abstractmethod
def build_jit(self) -> Callable:
pass

@abstractmethod
def load(self) -> Callable:
pass
134 changes: 134 additions & 0 deletions colossalai/kernel/extensions/cpp_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import importlib
import os
import time
from abc import abstractmethod
from pathlib import Path
from typing import List

from .base_extension import _Extension

__all__ = ["_CppExtension"]


class _CppExtension(_Extension):
def __init__(self, name: str, priority: int = 1):
super().__init__(name, support_aot=True, support_jit=True, priority=priority)

# we store the op as an attribute to avoid repeated building and loading
self.cached_op = None

# build-related variables
self.prebuilt_module_path = "colossalai._C"
self.prebuilt_import_path = f"{self.prebuilt_module_path}.{self.name}"
self.version_dependent_macros = ["-DVERSION_GE_1_1", "-DVERSION_GE_1_3", "-DVERSION_GE_1_5"]

def csrc_abs_path(self, path):
return os.path.join(self.relative_to_abs_path("csrc"), path)

def relative_to_abs_path(self, code_path: str) -> str:
"""
This function takes in a path relative to the colossalai root directory and return the absolute path.
"""

# get the current file path
# iteratively check the parent directory
# if the parent directory is "extensions", then the current file path is the root directory
# otherwise, the current file path is inside the root directory
current_file_path = Path(__file__)
while True:
if current_file_path.name == "extensions":
break
else:
current_file_path = current_file_path.parent
extension_module_path = current_file_path
code_abs_path = extension_module_path.joinpath(code_path)
return str(code_abs_path)

# functions must be overrided over
def strip_empty_entries(self, args):
"""
Drop any empty strings from the list of compile and link flags
"""
return [x for x in args if len(x) > 0]

def import_op(self):
"""
This function will import the op module by its string name.
"""
return importlib.import_module(self.prebuilt_import_path)

def build_aot(self) -> "CppExtension":
from torch.utils.cpp_extension import CppExtension

return CppExtension(
name=self.prebuilt_import_path,
sources=self.strip_empty_entries(self.sources_files()),
include_dirs=self.strip_empty_entries(self.include_dirs()),
extra_compile_args=self.strip_empty_entries(self.cxx_flags()),
)

def build_jit(self) -> None:
from torch.utils.cpp_extension import load

build_directory = _Extension.get_jit_extension_folder_path()
build_directory = Path(build_directory)
build_directory.mkdir(parents=True, exist_ok=True)

# check if the kernel has been built
compiled_before = False
kernel_file_path = build_directory.joinpath(f"{self.name}.o")
if kernel_file_path.exists():
compiled_before = True

# load the kernel
if compiled_before:
print(f"[extension] Loading the JIT-built {self.name} kernel during runtime now")
else:
print(f"[extension] Compiling the JIT {self.name} kernel during runtime now")

build_start = time.time()
op_kernel = load(
name=self.name,
sources=self.strip_empty_entries(self.sources_files()),
extra_include_paths=self.strip_empty_entries(self.include_dirs()),
extra_cflags=self.cxx_flags(),
extra_ldflags=[],
build_directory=str(build_directory),
)
build_duration = time.time() - build_start

if compiled_before:
print(f"[extension] Time taken to load {self.name} op: {build_duration} seconds")
else:
print(f"[extension] Time taken to compile {self.name} op: {build_duration} seconds")

return op_kernel

# functions must be overrided begin
@abstractmethod
def sources_files(self) -> List[str]:
"""
This function should return a list of source files for extensions.
"""

@abstractmethod
def include_dirs(self) -> List[str]:
"""
This function should return a list of include files for extensions.
"""

@abstractmethod
def cxx_flags(self) -> List[str]:
"""
This function should return a list of cxx compilation flags for extensions.
"""

def load(self):
try:
op_kernel = self.import_op()
except (ImportError, ModuleNotFoundError):
# if import error occurs, it means that the kernel is not pre-built
# so we build it jit
op_kernel = self.build_jit()

return op_kernel
Loading

0 comments on commit b9305fb

Please sign in to comment.