Skip to content
Merged
91 changes: 91 additions & 0 deletions .github/workflows/metal_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: CI Test on Metal
on: [pull_request]

env:
PYTHON_VERSION: '3.12'
VENV_DIR: tilelang_ci

jobs:
format-check:
runs-on: [macos-latest]

permissions:
contents: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

- name: Install python via uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
ignore-nothing-to-cache: true
activate-environment: true
python-version: ${{ env.PYTHON_VERSION }}

- name: Ensure venv (local & persistent)
run: |
[[ -f requirements-test.txt ]] && \
uv pip install -r requirements-test.txt --no-build-isolation

- name: Run format check
run: |
set -ex
mkdir -p build
# run cmake to create the build directory with compile_commands.json
cd build; cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DUSE_METAL=ON; cd ..
if ! output=$(./format.sh 2>&1); then
echo "------------------------------------"
echo "message:"
echo "$output"
printf '%s\n' "$output"
echo "------------------------------------"
exit 1
fi

build-test-metal:
runs-on: [macos-latest]
needs: format-check
permissions:
contents: read
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: recursive

- name: ccache
uses: hendrikmuhs/[email protected]
with:
create-symlink: true
key: ${{ github.job }}-${{ matrix.os }}

- name: Install python via uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
ignore-nothing-to-cache: true
activate-environment: true
python-version: ${{ env.PYTHON_VERSION }}

- name: Ensure venv (local & persistent)
run: uv pip install -r requirements-test.txt -r requirements-build.txt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard optional requirements to avoid failures if the file is missing.

-      run: uv pip install -r requirements-test.txt -r requirements-build.txt
+      run: |
+        set -e
+        [[ -f requirements-test.txt ]] && uv pip install -r requirements-test.txt
+        [[ -f requirements-build.txt ]] && uv pip install -r requirements-build.txt
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run: uv pip install -r requirements-test.txt -r requirements-build.txt
run: |
set -e
[[ -f requirements-test.txt ]] && uv pip install -r requirements-test.txt
[[ -f requirements-build.txt ]] && uv pip install -r requirements-build.txt
🤖 Prompt for AI Agents
In .github/workflows/metal_ci.yml around line 80, the step currently tries to
install optional requirement files unconditionally which will fail the job if
those files are missing; change the run command to detect each optional
requirements file (e.g., requirements-test.txt and requirements-build.txt) and
only include them in the pip install command if they exist (use a shell
conditional to build the -r arguments or test -f before calling pip), ensuring
that missing files are skipped but real install errors still fail the job.


- name: Build wheel
run: |
source .venv/bin/activate
uv pip install -v --no-build-isolation .

- name: Run metal test
run: |
cd testing/python
unset PYTHONPATH
python -m pytest -k metal -v -r fE --durations=0 --timeout=3600
22 changes: 20 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,21 @@ endif()
if(DEFINED TVM_PREBUILD_PATH)
message(STATUS "Using prebuilt TVM from ${TVM_PREBUILD_PATH}")
add_library(tvm SHARED IMPORTED)
find_library(TVM_LIBRARY_LOCATION
NAMES tvm
HINTS "${TVM_PREBUILD_PATH}"
)
set_target_properties(tvm PROPERTIES
IMPORTED_LOCATION "${TVM_PREBUILD_PATH}/libtvm.so"
IMPORTED_LOCATION "${TVM_LIBRARY_LOCATION}"
INTERFACE_INCLUDE_DIRECTORIES "${TVM_PREBUILD_PATH}/../include"
)
Comment on lines +111 to 118
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for missing TVM libraries

The find_library commands should check if the libraries were actually found before proceeding.

 find_library(TVM_LIBRARY_LOCATION
   NAMES tvm
   HINTS "${TVM_PREBUILD_PATH}"
 )
+if(NOT TVM_LIBRARY_LOCATION)
+  message(FATAL_ERROR "Could not find TVM library in ${TVM_PREBUILD_PATH}")
+endif()
 set_target_properties(tvm PROPERTIES
   IMPORTED_LOCATION "${TVM_LIBRARY_LOCATION}"
   INTERFACE_INCLUDE_DIRECTORIES "${TVM_PREBUILD_PATH}/../include"
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
find_library(TVM_LIBRARY_LOCATION
NAMES tvm
HINTS "${TVM_PREBUILD_PATH}"
)
set_target_properties(tvm PROPERTIES
IMPORTED_LOCATION "${TVM_PREBUILD_PATH}/libtvm.so"
IMPORTED_LOCATION "${TVM_LIBRARY_LOCATION}"
INTERFACE_INCLUDE_DIRECTORIES "${TVM_PREBUILD_PATH}/../include"
)
find_library(TVM_LIBRARY_LOCATION
NAMES tvm
HINTS "${TVM_PREBUILD_PATH}"
)
if(NOT TVM_LIBRARY_LOCATION)
message(FATAL_ERROR "Could not find TVM library in ${TVM_PREBUILD_PATH}")
endif()
set_target_properties(tvm PROPERTIES
IMPORTED_LOCATION "${TVM_LIBRARY_LOCATION}"
INTERFACE_INCLUDE_DIRECTORIES "${TVM_PREBUILD_PATH}/../include"
)
🤖 Prompt for AI Agents
In CMakeLists.txt around lines 102 to 109, the find_library call for TVM does
not verify that TVM_LIBRARY_LOCATION was found; add a check after find_library
such as if(NOT TVM_LIBRARY_LOCATION) message(FATAL_ERROR "Could not find tvm
library in ${TVM_PREBUILD_PATH}; please set TVM_PREBUILD_PATH or install tvm")
to stop configuration when missing, and only call set_target_properties for the
imported tvm target if the library path exists (or create the imported target
conditionally); ensure the error message includes the searched path for easier
debugging.

add_library(tvm_runtime SHARED IMPORTED)
find_library(TVM_RUNTIME_LIBRARY_LOCATION
NAMES tvm_runtime
HINTS "${TVM_PREBUILD_PATH}"
)
set_target_properties(tvm_runtime PROPERTIES
IMPORTED_LOCATION "${TVM_PREBUILD_PATH}/libtvm_runtime.so"
IMPORTED_LOCATION "${TVM_RUNTIME_LIBRARY_LOCATION}"
INTERFACE_INCLUDE_DIRECTORIES "${TVM_PREBUILD_PATH}/../include"
)
Comment on lines +120 to 127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for missing TVM runtime library

Similar to the TVM library, the runtime library location should be validated.

 find_library(TVM_RUNTIME_LIBRARY_LOCATION
   NAMES tvm_runtime
   HINTS "${TVM_PREBUILD_PATH}"
 )
+if(NOT TVM_RUNTIME_LIBRARY_LOCATION)
+  message(FATAL_ERROR "Could not find TVM runtime library in ${TVM_PREBUILD_PATH}")
+endif()
 set_target_properties(tvm_runtime PROPERTIES
   IMPORTED_LOCATION "${TVM_RUNTIME_LIBRARY_LOCATION}"
   INTERFACE_INCLUDE_DIRECTORIES "${TVM_PREBUILD_PATH}/../include"
 )
🤖 Prompt for AI Agents
In CMakeLists.txt around lines 111 to 118, the find_library result for
TVM_RUNTIME_LIBRARY_LOCATION is not validated; add the same existence check and
failure handling used for the TVM library: after find_library, test if
TVM_RUNTIME_LIBRARY_LOCATION is NOTFOUND (or empty) and call message(FATAL_ERROR
"...unable to locate tvm_runtime library at ${TVM_PREBUILD_PATH}..." ) so
configuration stops with a clear error; only set_target_properties for
tvm_runtime and INTERFACE_INCLUDE_DIRECTORIES when the library was successfully
found (mirror the pattern used for the TVM library block).

else()
Expand Down Expand Up @@ -157,6 +165,13 @@ if(USE_ROCM)
list(APPEND TILE_LANG_SRCS ${TILE_LANG_HIP_SRCS})
endif()

if(USE_METAL)
tilelang_file_glob(GLOB TILE_LANG_METAL_SRCS
src/target/rt_mod_metal.cc
)
list(APPEND TILE_LANG_SRCS ${TILE_LANG_METAL_SRCS})
endif()

message(STATUS "Collected source files: ${TILE_LANG_SRCS}")

# Add TileLang object library
Expand Down Expand Up @@ -221,6 +236,9 @@ target_compile_definitions(tilelang_objs PRIVATE -DTILE_LANG_EXPORTS)
# Shared library
add_library(tilelang SHARED $<TARGET_OBJECTS:tilelang_objs>)
target_link_libraries(tilelang PUBLIC tvm_runtime)
if(USE_METAL)
target_link_libraries(tilelang PUBLIC tvm)
endif()

# Static library
add_library(tilelang_static STATIC $<TARGET_OBJECTS:tilelang_objs>)
Expand Down
19 changes: 19 additions & 0 deletions install_metal.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

set -eux

git submodule update --init --recursive

rm -rf build

mkdir build
cp 3rdparty/tvm/cmake/config.cmake build
cd build

echo "set(USE_METAL ON)" >> config.cmake

CMAKE_C_COMPILER_LAUNCHER=ccache CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake ..

CORES=$(sysctl -n hw.logicalcpu)
MAKE_JOBS=$(( CORES / 2 ))
make -j${MAKE_JOBS}
116 changes: 83 additions & 33 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,60 @@

logger = logging.getLogger(__name__)


def _read_bool_env(name: str, default: bool = False) -> bool:
if env := os.environ.get(name):
env = env.lower()
if env in ['on', '1', 'true']:
return True
elif env in ['', 'off', '0', 'false']:
return False
return default


# Environment variables False/True
PYPI_BUILD = os.environ.get("PYPI_BUILD", "False").lower() == "true"
PYPI_BUILD = _read_bool_env('PYPI_BUILD')
PACKAGE_NAME = "tilelang"
ROOT_DIR = os.path.dirname(__file__)

CYCACHE = Path(os.path.join(ROOT_DIR, "tilelang", "jit", "adapter", "cython", ".cycache"))
if not CYCACHE.exists():
# tvm may needs this, we won't always build cython backend so mkdir here.
CYCACHE.mkdir(exist_ok=True)

IS_LINUX = platform.system() == 'Linux'
MAYBE_METAL = platform.mac_ver()[2] == 'arm64'

# Add LLVM control environment variable
USE_LLVM = os.environ.get("USE_LLVM", "False").lower() == "true"
USE_LLVM = _read_bool_env('USE_LLVM')
# Add ROCM control environment variable
USE_ROCM = _read_bool_env("USE_ROCM")
# Add ROCM control environment variable
USE_ROCM = os.environ.get("USE_ROCM", "False").lower() == "true"
USE_METAL = _read_bool_env("USE_METAL", MAYBE_METAL)
# Add ROCM control environment variable
USE_CUDA = _read_bool_env("USE_CUDA", IS_LINUX and not USE_ROCM)
# Build with Debug mode
DEBUG_MODE = os.environ.get("DEBUG_MODE", "False").lower() == "true"
DEBUG_MODE = _read_bool_env('DEBUG_MODE')
# Include commit ID in wheel filename and package metadata
WITH_COMMITID = os.environ.get("WITH_COMMITID", "True").lower() == "true"
WITH_COMMITID = _read_bool_env("WITH_COMMITID")

TVM_PREBUILD_ITEMS = [
"libtvm_runtime.so",
"libtvm.so",
"libtilelang.so",
"libtilelang_module.so",
] if IS_LINUX else [
"libtvm_runtime.dylib",
"libtvm.dylib",
"libtilelang.dylib",
"libtilelang_module.dylib",
]

# from tvm's internal cython?
TVM_PREBUILD_ITEMS_TO_DELETE = [] if IS_LINUX else [
'libtvm_runtime.dylib.dSYM',
'libtvm.dylib.dSYM',
]


def load_module_from_path(module_name, path):
Expand All @@ -65,24 +106,17 @@ def load_module_from_path(module_name, path):
raise ValueError(
"ROCM support is enabled (USE_ROCM=True) but ROCM_HOME is not set or detected.")

if not USE_ROCM and not CUDA_HOME:
if USE_CUDA and not CUDA_HOME:
raise ValueError(
"CUDA support is enabled by default (USE_ROCM=False) but CUDA_HOME is not set or detected.")
"CUDA support is enabled by default on linux if `USE_ROCM=False`," \
" but CUDA_HOME is not set or detected.")

# Ensure one of CUDA or ROCM is available
if not (CUDA_HOME or ROCM_HOME):
if IS_LINUX and not (CUDA_HOME or ROCM_HOME):
raise ValueError(
"Failed to automatically detect CUDA or ROCM installation. Please set the CUDA_HOME or ROCM_HOME environment variable manually (e.g., export CUDA_HOME=/usr/local/cuda or export ROCM_HOME=/opt/rocm)."
)

# TileLang only supports Linux platform
assert sys.platform.startswith("linux"), "TileLang only supports Linux platform (including WSL)."


def _is_linux_like():
return (sys.platform == "darwin" or sys.platform.startswith("linux") or
sys.platform.startswith("freebsd"))


def get_path(*filepath) -> str:
return os.path.join(ROOT_DIR, *filepath)
Expand Down Expand Up @@ -144,7 +178,9 @@ def get_rocm_version():
return Version("5.0.0")


def get_tilelang_version(with_cuda=True, with_system_info=True, with_commit_id=False) -> str:
def get_tilelang_version(with_cuda=USE_CUDA,
with_system_info=not MAYBE_METAL,
with_commit_id=False) -> str:
version = find_version(get_path(".", "VERSION"))
local_version_parts = []
if with_system_info:
Expand Down Expand Up @@ -194,9 +230,6 @@ def get_cplus_compiler():
The path to the default C/C++ compiler, or None if none was found.
"""

if not _is_linux_like():
return None

env_cxx = os.environ.get("CXX") or os.environ.get("CC")
if env_cxx:
return env_cxx
Expand Down Expand Up @@ -371,6 +404,8 @@ def patch_libs(libpath):
and have a hard-coded rpath.
Set rpath to the directory of libs so auditwheel works well.
"""
if not IS_LINUX:
return
# check if patchelf is installed
# find patchelf in the system
patchelf_path = shutil.which("patchelf")
Expand Down Expand Up @@ -432,13 +467,6 @@ def run(self):
os.makedirs(target_dir)
shutil.copy2(source_dir, target_dir)

TVM_PREBUILD_ITEMS = [
"libtvm_runtime.so",
"libtvm.so",
"libtilelang.so",
"libtilelang_module.so",
]

potential_dirs = [
ext_output_dir,
self.build_lib,
Expand Down Expand Up @@ -468,6 +496,14 @@ def run(self):
else:
logger.info(f"WARNING: {item} not found in any expected directories!")

for item in TVM_PREBUILD_ITEMS_TO_DELETE:
source_lib_file = None
for dir in potential_dirs:
candidate = os.path.join(dir, item)
if os.path.exists(candidate):
shutil.rmtree(candidate)
break

TVM_CONFIG_ITEMS = [
f"{build_temp_dir}/config.cmake",
]
Expand Down Expand Up @@ -587,10 +623,10 @@ class CMakeExtension(Extension):
:param sourcedir: Directory containing the top-level CMakeLists.txt.
"""

def __init__(self, name, sourcedir=""):
def __init__(self, name, sourcedir="", **kwargs):
# We pass an empty 'sources' list because
# the actual build is handled by CMake, not setuptools.
super().__init__(name=name, sources=[])
super().__init__(name=name, sources=[], **kwargs)

# Convert the source directory to an absolute path
# so that CMake can correctly locate the CMakeLists.txt.
Expand Down Expand Up @@ -642,7 +678,7 @@ def run(self):
# To make it works with editable install,
# we need to copy the lib*.so files to the tilelang/lib directory
import glob
files = glob.glob("*.so")
files = glob.glob("*.so" if IS_LINUX else "*.dylib")
if os.path.exists(PACKAGE_NAME):
target_lib_dir = os.path.join(PACKAGE_NAME, "lib")
for file in files:
Expand Down Expand Up @@ -724,7 +760,10 @@ def build_cython(self, ext):
os.system(f"{cython} {cython_wrapper_path} --cplus -o {source_path}")
python_include_path = sysconfig.get_path("include")
cc = get_cplus_compiler()
if MAYBE_METAL:
cc += ' -Wl,-undefined,dynamic_lookup'
command = f"{cc} -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I{python_include_path} {source_path} -o {temp_path}"
logger.info(command)
os.system(command)

# rename the temp file to the library file
Expand Down Expand Up @@ -783,7 +822,7 @@ def build_cmake(self, ext):
"-G",
"Ninja",
]
if not USE_ROCM:
if USE_CUDA and not USE_ROCM:
cmake_args.append(f"-DCMAKE_CUDA_COMPILER={os.path.join(CUDA_HOME, 'bin', 'nvcc')}")

# Create the temporary build directory (if it doesn't exist).
Expand All @@ -804,12 +843,17 @@ def build_cmake(self, ext):
content_lines.append(f"set(USE_LLVM {llvm_config_path})")

# Append GPU backend configuration based on environment
if USE_ROCM:
if USE_METAL:
content_lines += [
"set(USE_METAL ON)",
"set(USE_ROCM OFF)",
]
elif USE_ROCM:
content_lines += [
f"set(USE_ROCM {ROCM_HOME})",
"set(USE_CUDA OFF)",
]
else:
elif CUDA_HOME:
content_lines += [
f"set(USE_CUDA {CUDA_HOME})",
"set(USE_ROCM OFF)",
Expand Down Expand Up @@ -846,6 +890,12 @@ def build_cmake(self, ext):
cwd=build_temp)


ext_modules = [
CMakeExtension("TileLangCXX", sourcedir="."),
]
if not MAYBE_METAL:
ext_modules.append(CythonExtension("TileLangCython", sourcedir="."))

Comment on lines +893 to +898
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: ext_modules gating ignored; CythonExtension builds unconditionally.

Pass the computed ext_modules into setup() so the MAYBE_METAL gate is effective.

-setup(
+setup(
@@
-    ext_modules=[
-        CMakeExtension("TileLangCXX", sourcedir="."),
-        CythonExtension("TileLangCython", sourcedir="."),
-    ],
+    ext_modules=ext_modules,

Also applies to: 920-923

🤖 Prompt for AI Agents
In setup.py around lines 886-891 (and similarly around 920-923), ext_modules is
built conditionally but never passed into the setup() call so the MAYBE_METAL
gate is ignored; update the setup() invocation(s) to include
ext_modules=ext_modules (and remove any hardcoded ext_modules lists) so the
computed list is used at build time, ensuring the CythonExtension is only added
when MAYBE_METAL is False.

setup(
name=PACKAGE_NAME,
version=(get_tilelang_version(with_cuda=False, with_system_info=False, with_commit_id=False)
Expand Down
3 changes: 3 additions & 0 deletions src/target/rt_mod_metal.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Currently mps backend use the codegen from tvm without modification.
// But in the future we're likely to add functions on top of that.
// Added an empty file for now.
Loading
Loading