398 lines
12 KiB
Python
398 lines
12 KiB
Python
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
# The file has been adapted from DeepSeek DeepGEMM project
|
|
# Copyright (c) 2025 DeepSeek
|
|
# Licensed under the MIT License - https://github.com/deepseek-ai/DeepGEMM/blob/main/LICENSE
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import hashlib
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import time
|
|
import uuid
|
|
from typing import Any
|
|
|
|
import cuda.bindings
|
|
from cuda.bindings import nvrtc
|
|
|
|
from paddle.utils.cpp_extension.cpp_extension import CUDA_HOME
|
|
|
|
from . import interleave_ffma
|
|
from .runtime import Runtime, RuntimeCache
|
|
|
|
runtime_cache = RuntimeCache()
|
|
|
|
|
|
def hash_to_hex(s: str) -> str:
|
|
"""Transforms input into a hexadecimal hash string"""
|
|
if isinstance(s, int):
|
|
return f"{s & 0xFFFFFFFFFFFFFFFF:016x}"
|
|
elif isinstance(s, str):
|
|
md5 = hashlib.md5()
|
|
md5.update(s.encode("utf-8"))
|
|
return md5.hexdigest()[0:12]
|
|
else:
|
|
raise TypeError(f"Unsupported type for hashing: {type(s)}")
|
|
|
|
|
|
@functools.cache
|
|
def get_jit_include_dir() -> str:
|
|
return f"{os.path.dirname(os.path.abspath(__file__))}"
|
|
|
|
|
|
@functools.cache
|
|
def get_deep_gemm_version() -> str:
|
|
md5 = hashlib.md5()
|
|
|
|
# Update include directories
|
|
include_dir = f"{get_jit_include_dir()}/../../../../include/paddle/fluid/fp8/deep_gemm/include"
|
|
assert os.path.exists(include_dir), (
|
|
f"Cannot find GEMM include directory {include_dir}"
|
|
)
|
|
for filename in filter(
|
|
lambda x: x.endswith(".cuh"), sorted(os.listdir(include_dir))
|
|
):
|
|
with open(os.path.join(include_dir, filename), "rb") as f:
|
|
md5.update(f.read())
|
|
|
|
# Update `interleave_ffma.py`
|
|
with open(
|
|
os.path.join(
|
|
os.path.dirname(os.path.realpath(__file__)), "interleave_ffma.py"
|
|
),
|
|
"rb",
|
|
) as f:
|
|
md5.update(f.read())
|
|
return md5.hexdigest()[0:12]
|
|
|
|
|
|
@functools.cache
|
|
def get_nvcc_compiler() -> tuple[str, str]:
|
|
paths = []
|
|
if os.getenv("DG_JIT_NVCC_COMPILER"):
|
|
paths.append(os.getenv("DG_JIT_NVCC_COMPILER"))
|
|
paths.append(os.path.join(CUDA_HOME, "bin", "nvcc"))
|
|
|
|
# Try to find the first available NVCC compiler
|
|
least_version_required = "12.3"
|
|
version_pattern = re.compile(r"release (\d+\.\d+)")
|
|
for path in paths:
|
|
if os.path.exists(path):
|
|
command = [path, "--version"]
|
|
result = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
match = version_pattern.search(result.stdout)
|
|
version = match.group(1)
|
|
assert match, f"Cannot get the version of NVCC compiler {path}"
|
|
assert version >= least_version_required, (
|
|
f"NVCC {path} version {version} is lower than {least_version_required}"
|
|
)
|
|
return path, version
|
|
raise RuntimeError("Cannot find any available NVCC compiler")
|
|
|
|
|
|
@functools.cache
|
|
def get_default_user_dir():
|
|
if "DG_JIT_CACHE_DIR" in os.environ:
|
|
path = os.getenv("DG_JIT_CACHE_DIR")
|
|
os.makedirs(path, exist_ok=True)
|
|
return path
|
|
return os.path.join(os.path.expanduser("~"), ".deep_gemm")
|
|
|
|
|
|
@functools.cache
|
|
def get_tmp_dir():
|
|
return os.path.join(get_default_user_dir(), "tmp")
|
|
|
|
|
|
@functools.cache
|
|
def get_cache_dir():
|
|
return os.path.join(get_default_user_dir(), "cache")
|
|
|
|
|
|
def make_tmp_dir():
|
|
tmp_dir = get_tmp_dir()
|
|
os.makedirs(tmp_dir, exist_ok=True)
|
|
return tmp_dir
|
|
|
|
|
|
def put(path, data):
|
|
# Write and do POSIX atomic replace
|
|
tmp_file_path = os.path.join(
|
|
make_tmp_dir(), f"file.tmp.{uuid.uuid4()!s}.{hash_to_hex(path)}"
|
|
)
|
|
with open(tmp_file_path, "wb" if isinstance(data, bytes) else "w") as f:
|
|
f.write(data)
|
|
os.replace(tmp_file_path, path)
|
|
|
|
|
|
class Compiler:
|
|
@classmethod
|
|
def signature(cls) -> str:
|
|
pass
|
|
|
|
@staticmethod
|
|
def __version__() -> tuple[int, int]:
|
|
pass
|
|
|
|
@classmethod
|
|
def compile(cls, name: str, code: str, target_path: str) -> None:
|
|
pass
|
|
|
|
@staticmethod
|
|
def flags() -> list[str]:
|
|
cpp_standard = int(os.getenv("DG_JIT_OVERRIDE_CPP_STANDARD", 20))
|
|
return [
|
|
f"-std=c++{cpp_standard}",
|
|
"--ptxas-options=--register-usage-level=10"
|
|
+ (",--verbose" if "DG_JIT_PTXAS_VERBOSE" in os.environ else ""),
|
|
# Suppress some unnecessary warnings, such as unused variables for certain `constexpr` branch cases
|
|
"--diag-suppress=39,161,174,177,186,940",
|
|
]
|
|
|
|
@staticmethod
|
|
def include_dirs() -> list[str]:
|
|
return [
|
|
f"{get_jit_include_dir()}/../../../../include/paddle/fluid/fp8/deep_gemm/include"
|
|
]
|
|
|
|
@classmethod
|
|
def build(
|
|
cls,
|
|
name: str,
|
|
runtime_cls: type[Runtime],
|
|
kwargs: dict[str, Any] | None = None,
|
|
) -> Runtime:
|
|
code = runtime_cls.generate(kwargs)
|
|
# Compiler flags
|
|
flags = cls.flags()
|
|
|
|
# Build signature
|
|
enable_sass_opt = cls.__version__() <= (12, 8) and not int(
|
|
os.getenv('DG_JIT_DISABLE_FFMA_INTERLEAVE', 0)
|
|
)
|
|
signature = f'{name}$${get_deep_gemm_version()}$${cls.signature()}$${flags}$${enable_sass_opt}$${code}'
|
|
name = f"kernel.{name}.{hash_to_hex(signature)}"
|
|
path = os.path.join(get_cache_dir(), name)
|
|
|
|
# Check runtime cache or file system hit
|
|
global runtime_cache
|
|
cached_runtime = runtime_cache.get(path, runtime_cls, name, kwargs)
|
|
if cached_runtime is not None:
|
|
if int(os.getenv("DG_JIT_DEBUG", 0)):
|
|
print(f"Using cached JIT runtime {name} during build")
|
|
return cached_runtime
|
|
|
|
# Compile into a temporary CU file
|
|
os.makedirs(path, exist_ok=True)
|
|
cubin_path = os.path.join(path, "kernel.cubin")
|
|
tmp_cubin_path = os.path.join(
|
|
make_tmp_dir(),
|
|
f"nvcc.tmp.{uuid.uuid4()!s}.{hash_to_hex(cubin_path)}.cubin",
|
|
)
|
|
|
|
start_time = time.time()
|
|
cls.compile(name, code, tmp_cubin_path)
|
|
end_time = time.time()
|
|
elapsed_time = end_time - start_time
|
|
if int(os.getenv("DG_JIT_DEBUG", 0)):
|
|
print(
|
|
f"Compilation of JIT runtime {name} took {elapsed_time:.2f} seconds."
|
|
)
|
|
|
|
# Interleave FFMA reuse
|
|
if enable_sass_opt:
|
|
interleave_ffma.process(tmp_cubin_path)
|
|
|
|
# Atomic replace files
|
|
os.replace(tmp_cubin_path, cubin_path)
|
|
|
|
# Put cache and return
|
|
runtime = runtime_cache.get(
|
|
path, runtime_cls, name, kwargs, force_enable_cache=True
|
|
)
|
|
assert runtime is not None
|
|
return runtime
|
|
|
|
|
|
class NVCCCompiler(Compiler):
|
|
@staticmethod
|
|
def __version__() -> tuple[int, int]:
|
|
_, version = get_nvcc_compiler()
|
|
major, minor = map(int, version.split("."))
|
|
return major, minor
|
|
|
|
@classmethod
|
|
def signature(cls) -> str:
|
|
return f"{get_nvcc_compiler()[0]}+{cls.__version__()}"
|
|
|
|
@classmethod
|
|
def flags(cls) -> list[str]:
|
|
cxx_flags = [
|
|
"-fPIC",
|
|
"-O3",
|
|
"-fconcepts",
|
|
"-Wno-deprecated-declarations",
|
|
"-Wno-abi",
|
|
]
|
|
return [
|
|
*super().flags(),
|
|
*[f"-I{d}" for d in cls.include_dirs()],
|
|
"-gencode=arch=compute_90a,code=sm_90a",
|
|
"-cubin",
|
|
"-O3",
|
|
"--expt-relaxed-constexpr",
|
|
"--expt-extended-lambda",
|
|
f"--compiler-options={','.join(cxx_flags)}",
|
|
]
|
|
|
|
@classmethod
|
|
def compile(cls, name: str, code: str, target_path: str) -> None:
|
|
# Write the code
|
|
path = os.path.join(get_cache_dir(), name)
|
|
src_path = os.path.join(path, "kernel.cu")
|
|
put(src_path, code)
|
|
command = [
|
|
get_nvcc_compiler()[0],
|
|
src_path,
|
|
"-o",
|
|
target_path,
|
|
*cls.flags(),
|
|
]
|
|
if int(os.getenv("DG_JIT_DEBUG", 0)) or int(
|
|
os.getenv("DG_JIT_PRINT_COMPILER_COMMAND", 0)
|
|
):
|
|
print(f"Compiling JIT runtime {name} with command {command}")
|
|
|
|
result = subprocess.run(command, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
print(
|
|
f"NVCC compilation failed: stdout: {result.stdout}, stderr: {result.stderr}"
|
|
)
|
|
raise AssertionError(f"Failed to compile {src_path}")
|
|
|
|
|
|
class NVRTCCompiler(Compiler):
|
|
@staticmethod
|
|
def __version__() -> tuple[int, int]:
|
|
res, major, minor = nvrtc.nvrtcVersion()
|
|
if res != nvrtc.nvrtcResult.NVRTC_SUCCESS:
|
|
# Failed to get the actual NVRTC version, use cuda-bindings version instead
|
|
major, minor = map(int, cuda.bindings.__version__.split(".")[:2])
|
|
return major, minor
|
|
|
|
@classmethod
|
|
def signature(cls) -> str:
|
|
return f"nvrtc+{cls.__version__()}"
|
|
|
|
@staticmethod
|
|
def include_dirs() -> list[str]:
|
|
if CUDA_HOME is None:
|
|
raise RuntimeError("CUDA_HOME is required for NVRTC compilation")
|
|
return [get_jit_include_dir(), os.path.join(CUDA_HOME, "include")]
|
|
|
|
@classmethod
|
|
def flags(cls) -> list[str]:
|
|
flags = [
|
|
*super().flags(),
|
|
*[f"-I{d}" for d in cls.include_dirs()],
|
|
"--gpu-architecture=sm_90a",
|
|
"-default-device",
|
|
]
|
|
# NOTES: PCH is vital for compilation speed
|
|
if cls.__version__() >= (12, 8):
|
|
flags += ["--pch"]
|
|
if int(os.getenv("DG_JIT_DEBUG", 0)):
|
|
flags += ["--pch-verbose=true"]
|
|
return flags
|
|
|
|
@classmethod
|
|
def compile(cls, name: str, code: str, target_path: str) -> None:
|
|
# Create program
|
|
code_bytes = bytes(code, "utf-8")
|
|
result, program = nvrtc.nvrtcCreateProgram(
|
|
code_bytes, bytes(name, "utf-8"), 0, [], []
|
|
)
|
|
assert result == nvrtc.nvrtcResult.NVRTC_SUCCESS, (
|
|
f"Failed to create program: {result}"
|
|
)
|
|
|
|
# Compile
|
|
options = [bytes(flag, "utf-8") for flag in cls.flags()]
|
|
if int(os.getenv("DG_JIT_DEBUG", 0)) or int(
|
|
os.getenv("DG_JIT_PRINT_COMPILER_COMMAND", 0)
|
|
):
|
|
print(f"Compiling JIT runtime {name} with options: {options}")
|
|
compile_result = nvrtc.nvrtcCompileProgram(
|
|
program, len(options), options
|
|
)[0]
|
|
|
|
# Print compiler log
|
|
if (
|
|
int(os.getenv("DG_JIT_DEBUG", 0))
|
|
or compile_result != nvrtc.nvrtcResult.NVRTC_SUCCESS
|
|
):
|
|
result, log_size = nvrtc.nvrtcGetProgramLogSize(program)
|
|
assert result == nvrtc.nvrtcResult.NVRTC_SUCCESS, (
|
|
f"Failed to get program log size: {result}"
|
|
)
|
|
|
|
log_bytes = bytes(log_size)
|
|
result = nvrtc.nvrtcGetProgramLog(program, log_bytes)[0]
|
|
assert result == nvrtc.nvrtcResult.NVRTC_SUCCESS, (
|
|
f"Failed to get program log: {result}"
|
|
)
|
|
print(f"Compiler log: {log_bytes.decode('utf-8')}")
|
|
|
|
# Exit if failed
|
|
assert compile_result == nvrtc.nvrtcResult.NVRTC_SUCCESS, (
|
|
f"Failed to compile program: {compile_result}"
|
|
)
|
|
|
|
# Create CUBIN
|
|
result, cubin_size = nvrtc.nvrtcGetCUBINSize(program)
|
|
assert result == nvrtc.nvrtcResult.NVRTC_SUCCESS, (
|
|
f"Failed to get CUBIN size: {result}"
|
|
)
|
|
cubin_bytes = bytes(cubin_size)
|
|
result = nvrtc.nvrtcGetCUBIN(program, cubin_bytes)[0]
|
|
assert result == nvrtc.nvrtcResult.NVRTC_SUCCESS, (
|
|
f"Failed to get CUBIN: {result}"
|
|
)
|
|
|
|
# Write into the file system
|
|
put(target_path, cubin_bytes)
|
|
|
|
# Destroy handler
|
|
assert (
|
|
nvrtc.nvrtcDestroyProgram(program)[0]
|
|
== nvrtc.nvrtcResult.NVRTC_SUCCESS
|
|
), f"Failed to destroy program: {result}"
|
|
|
|
|
|
def build(
|
|
name: str, runtime_cls: type[Runtime], kwargs: dict[str, Any] | None = None
|
|
) -> Runtime:
|
|
compiler_cls = (
|
|
NVRTCCompiler if int(os.getenv("DG_JIT_USE_NVRTC", 0)) else NVCCCompiler
|
|
)
|
|
return compiler_cls.build(name, runtime_cls, kwargs)
|