Unverified Commit 8ce27782 authored by Xuehai Pan's avatar Xuehai Pan Committed by GitHub
Browse files

[CI][Refactor] Merge test CI workflow files into one (#973)



* refactor: merge test CI workflow files into one

* chore: set `UV_INDEX_STRATEGY=unsafe-best-match`

* feat: add AST test with Python 3.8

* feat: implement manual caching mechanism for self-hosted runners

* refactor: simplify cache logic for self-hosted runners

* chore: clear uv cache on failure

* chore: print format.sh output to logs

* chore: improve uv caching

* chore: disable parallel test

* chore: use `PYTHONDEVMODE=1` in CI

* feat: enable coredump generation

* fix: fix perfbench condition

* Revert "feat: enable coredump generation"

This reverts commit c52da65cb572932e09905d08c43a39ec3cf47c54.

* chore: move example CI down

* Revert "chore: move example CI down"

This reverts commit 9d8e65055e01d955c5268a9a6705d270c2de0d57.

* chore: skip example `test_example_mha_sink_bwd_bhsd`

* chore: skip example `test_example_gqa_sink_bwd_bhsd`

* fix: fix example argument passing

* fix: loosen test criteria

* chore: rename `CMAKE_CONFIGURE_OPTIONS` -> `CLANG_TIDY_CMAKE_OPTIONS` for clarity

* feat: enable parallel testings

* chore: update pytest options

* remove skipped test as now been resolved

* chore: empty commit to re-trigger ci

* test for n 1

* chore: remove ` --numprocesses=1` option in example

* chore: disable failfast

* chore: update cibw selection

* fix: fix git submodule clone

* chore: update cibw commands

* fix: fix yapf multiprocessing

* chore: setup ccache for CIBW on macOS only

* chore: update comments

* chore: update artifact listing

* fix: do not fail if not found nvcc in PATH

* fix: fix flash-attn installation

* chore: update dist workflow trigger

* chore: remove outdated comments

* chore(workflows/dist): simplify build matrix strategy

* fix: fix CUDA path finding

* fix: fix CUDA path finding

* chore: imcrease CI timeout

* ci: disable failfast

* fix: hide path prefix

* chore: more verbose

* chore: disable PR trigger for dist workflow

* fix: seed for tests

* fix: use nightly torch for ROCm tests

* chore: enable PR trigger for dist workflow

* chore: stop uploading debug wheels as artifacts in PR

* chore: do not run workflows in forks

* chore: housekeep requirements

* chore: use Nightly-ROCm-6.3 for CI

* chore: use Nightly-ROCm-6.4 for CI

* Update ROCm toolkit version to 7.0

* chore: restore previous rocm-ci.yml for test

* fix: cleanup PYTHONPATH

* chore: remove previous rocm-ci.yml

* ci fix

* chore: remove previous rocm-ci.yml

* chore: enable parallel example run

---------
Co-authored-by: default avatarLeiWang1999 <leiwang1999@outlook.com>
Co-authored-by: default avataralex_xiao <xinyuxiao2024@gmail.com>
parent 80665cd1
name: CI
on:
pull_request:
types:
- labeled
- unlabeled
- opened
- synchronize
- reopened
# Allow to trigger the workflow manually
workflow_dispatch:
permissions:
contents: read
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CLANG_TIDY_CMAKE_OPTIONS: "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" # to be updated
PYTHONDEVMODE: "1"
PYTHONUNBUFFERED: "1"
PYTHONPATH: "" # explicit cleanup
FORCE_COLOR: "1"
CLICOLOR_FORCE: "1"
UV_INDEX_STRATEGY: "unsafe-best-match"
XDG_CACHE_HOME: "${{ github.workspace }}/.cache" # to be updated
PIP_CACHE_DIR: "${{ github.workspace }}/.cache/pip" # to be updated
UV_CACHE_DIR: "${{ github.workspace }}/.cache/uv" # to be updated
PRE_COMMIT_HOME: "${{ github.workspace }}/.cache/pip/.pre-commit" # to be updated
jobs:
lint:
name: Quick Lint
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Setup Python 3.8
id: setup-py38
uses: actions/setup-python@v6
with:
python-version: "3.8" # use lowest supported version for linting
update-environment: false
- name: Check AST with Python 3.8
run: |
"${{ steps.setup-py38.outputs.python-path }}" -m compileall -q -f tilelang
- name: Setup Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
update-environment: true
cache: pip
cache-dependency-path: |
pyproject.toml
requirements*.txt
.pre-commit-config.yaml
- name: Pre-commit Lint
run: |
if ! pipx run pre-commit run --all-files --color=always --show-diff-on-failure; then
echo "::error::Pre-commit checks failed. Please run 'pre-commit install' and 'pre-commit run --all-files' locally to see the issues."
exit 1
fi
tests:
name: Test for Python ${{ matrix.python-version }} with ${{ matrix.runner.toolkit }} (on ${{ matrix.runner.name }})
if: |
github.repository_owner == 'tile-ai' &&
(github.event_name != 'pull_request' || !github.event.pull_request.draft)
needs: [lint]
runs-on: ${{ matrix.runner.tags }}
strategy:
matrix:
runner:
- tags: [self-hosted, nvidia]
name: self-hosted-nvidia
# Format: [Nightly-]CUDA-<major>.<minor>[.<patch>]. E.g., "CUDA-12.8" or "Nightly-CUDA-13.0".
# Use "Nightly-" prefix to use torch nightly builds.
toolkit: CUDA-12.8
- tags: [self-hosted, amd, gpu]
name: self-hosted-amd
# Format: [Nightly-]ROCm-<major>.<minor>[.<patch>]. E.g., "ROCm-6.4" or "Nightly-ROCm-7.0".
# Use "Nightly-" prefix to use torch nightly builds.
toolkit: Nightly-ROCm-7.0
- tags: [macos-latest]
name: macos-latest
toolkit: Metal # or Nightly-Metal
python-version:
- "3.12"
fail-fast: false
timeout-minutes: 120
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Set environment (self-hosted runners)
if: startsWith(matrix.runner.name, 'self-hosted')
run: |
# Hide sensitive data in logs for self-hosted runners
if [[ -n "${{ secrets.SECRET_PATH_PREFIXES }}" ]]; then
echo "::add-mask::${{ secrets.SECRET_PATH_PREFIXES }}"
# Colon separated list of secrets to mask
for secret in $(echo "${{ secrets.SECRET_PATH_PREFIXES }}" | tr ':' '\n'); do
echo "::add-mask::${secret}"
done
fi
# Use runner tool_cache as cache root for self-hosted runners to avoid internet connection
# issues and to share cache between jobs.
export XDG_CACHE_HOME="${{ runner.tool_cache }}/.ci-cache-${{ github.workflow }}"
echo "XDG_CACHE_HOME=${XDG_CACHE_HOME}" | tee -a "${GITHUB_ENV}"
echo "PIP_CACHE_DIR=${XDG_CACHE_HOME}/pip" | tee -a "${GITHUB_ENV}"
echo "UV_CACHE_DIR=${XDG_CACHE_HOME}/uv" | tee -a "${GITHUB_ENV}"
echo "PRE_COMMIT_HOME=${XDG_CACHE_HOME}/pip/.pre-commit" | tee -a "${GITHUB_ENV}"
- name: Set environment (GitHub-hosted runners)
if: ${{ !startsWith(matrix.runner.name, 'self-hosted') }}
run: |
# Enable ccache on GitHub-hosted runners to speed up builds
echo "CMAKE_C_COMPILER_LAUNCHER=ccache" | tee -a "${GITHUB_ENV}"
echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" | tee -a "${GITHUB_ENV}"
# Do not use ccache on self-hosted runners, as it will download/upload caches which is slow.
# Self-hosted runners usually have more CPU power to compile without ccache.
- name: Setup ccache (GitHub-hosted runners)
id: setup-ccache
if: ${{ !startsWith(matrix.runner.name, 'self-hosted') }}
uses: hendrikmuhs/ccache-action@v1
with:
create-symlink: true
key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.python-version }}-${{ matrix.runner.name }}-${{ matrix.runner.toolkit }}
evict-old-files: "7d"
- name: Set environment (CUDA)
if: contains(matrix.runner.toolkit, 'CUDA')
run: |
TOOLKIT="${{ matrix.runner.toolkit }}"
CUDA_VERSION="${TOOLKIT##*-}"
CUDA_VERSION_MAJMIN="$(echo ${CUDA_VERSION} | cut -d '.' -f-2)"
CUDA_VERSION_MAJMIN_NODOT="${CUDA_VERSION_MAJMIN//./}"
if [[ "${TOOLKIT}" == "Nightly-"* ]]; then
# Use torch nightly builds
export PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/nightly/cu${CUDA_VERSION_MAJMIN_NODOT}"
else
export PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/cu${CUDA_VERSION_MAJMIN_NODOT}"
fi
export UV_INDEX="${PIP_EXTRA_INDEX_URL}"
export CLANG_TIDY_CMAKE_OPTIONS="${CLANG_TIDY_CMAKE_OPTIONS} -DUSE_CUDA=ON"
echo "USE_CUDA=ON" | tee -a "${GITHUB_ENV}"
echo "CUDA_VERSION=${CUDA_VERSION}" | tee -a "${GITHUB_ENV}"
echo "CUDA_VERSION_MAJMIN=${CUDA_VERSION_MAJMIN}" | tee -a "${GITHUB_ENV}"
echo "CUDA_VERSION_MAJMIN_NODOT=${CUDA_VERSION_MAJMIN_NODOT}" | tee -a "${GITHUB_ENV}"
echo "PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL}" | tee -a "${GITHUB_ENV}"
echo "UV_INDEX=${UV_INDEX}" | tee -a "${GITHUB_ENV}"
echo "CLANG_TIDY_CMAKE_OPTIONS=${CLANG_TIDY_CMAKE_OPTIONS}" | tee -a "${GITHUB_ENV}"
if [[ ! -x "$(command -v nvcc)" ]]; then
export PATH="/usr/local/cuda/bin:${PATH}"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
echo "PATH=${PATH}" | tee -a "${GITHUB_ENV}"
echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" | tee -a "${GITHUB_ENV}"
fi
if [[ -x "$(command -v nvcc)" ]]; then
echo "\$ $(command -v nvcc) --version" && nvcc --version
else
echo "::warning::nvcc not found in PATH!"
fi
- name: Set environment (ROCm)
if: contains(matrix.runner.toolkit, 'ROCm')
run: |
TOOLKIT="${{ matrix.runner.toolkit }}"
ROCM_VERSION="${TOOLKIT##*-}"
ROCM_VERSION_MAJMIN="$(echo ${ROCM_VERSION} | cut -d '.' -f-2)"
ROCM_VERSION_MAJMIN_NODOT="${ROCM_VERSION_MAJMIN//./}"
if [[ "${TOOLKIT}" == "Nightly-"* ]]; then
# Use torch nightly builds
export PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/nightly/rocm${ROCM_VERSION_MAJMIN}"
else
export PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/rocm${ROCM_VERSION_MAJMIN}"
fi
export UV_INDEX="${PIP_EXTRA_INDEX_URL}"
export CLANG_TIDY_CMAKE_OPTIONS="${CLANG_TIDY_CMAKE_OPTIONS} -DUSE_ROCM=ON"
echo "USE_ROCM=ON" | tee -a "${GITHUB_ENV}"
echo "ROCM_VERSION=${ROCM_VERSION}" | tee -a "${GITHUB_ENV}"
echo "ROCM_VERSION_MAJMIN=${ROCM_VERSION_MAJMIN}" | tee -a "${GITHUB_ENV}"
echo "ROCM_VERSION_MAJMIN_NODOT=${ROCM_VERSION_MAJMIN_NODOT}" | tee -a "${GITHUB_ENV}"
echo "PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL}" | tee -a "${GITHUB_ENV}"
echo "UV_INDEX=${UV_INDEX}" | tee -a "${GITHUB_ENV}"
echo "CLANG_TIDY_CMAKE_OPTIONS=${CLANG_TIDY_CMAKE_OPTIONS}" | tee -a "${GITHUB_ENV}"
if [[ ! -x "$(command -v hipcc)" ]]; then
export PATH="/opt/rocm/bin:${PATH}"
export LD_LIBRARY_PATH="/opt/rocm/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
echo "PATH=${PATH}" | tee -a "${GITHUB_ENV}"
echo "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" | tee -a "${GITHUB_ENV}"
fi
if [[ -x "$(command -v hipcc)" ]]; then
echo "\$ $(command -v hipcc) --version" && hipcc --version
else
echo "::warning::hipcc not found in PATH!"
fi
- name: Set environment (Metal)
if: contains(matrix.runner.toolkit, 'Metal')
run: |
if [[ "${{ matrix.runner.toolkit }}" == "Nightly-"* ]]; then
# Use torch nightly builds
export PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/nightly/cpu"
export UV_INDEX="${PIP_EXTRA_INDEX_URL}"
echo "PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL}" | tee -a "${GITHUB_ENV}"
echo "UV_INDEX=${UV_INDEX}" | tee -a "${GITHUB_ENV}"
fi
export CLANG_TIDY_CMAKE_OPTIONS="${CLANG_TIDY_CMAKE_OPTIONS} -DUSE_METAL=ON"
echo "USE_METAL=ON" | tee -a "${GITHUB_ENV}"
echo "CLANG_TIDY_CMAKE_OPTIONS=${CLANG_TIDY_CMAKE_OPTIONS}" | tee -a "${GITHUB_ENV}"
- name: Setup Python and uv with caching
id: setup-uv
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
# Do not use cache for self-hosted runners, as it will download/upload caches which is slow.
enable-cache: ${{ !startsWith(matrix.runner.name, 'self-hosted') }}
prune-cache: ${{ !startsWith(matrix.runner.name, 'self-hosted') }}
# Use runner tool_cache for self-hosted runners
cache-local-path: ${{ env.UV_CACHE_DIR }}
ignore-nothing-to-cache: true
# Extra cache key to upload/download caches on GitHub-hosted runners
cache-suffix: uv-${{ runner.os }}-${{ runner.arch }}-${{ matrix.python-version }}-${{ matrix.runner.name }}-${{ matrix.runner.toolkit }}
cache-dependency-glob: |
pyproject.toml
requirements*.txt
.pre-commit-config.yaml
- name: Setup venv
id: setup-venv
run: |
set -o pipefail
uv pip install --upgrade pip setuptools wheel
if [[ "${UV_INDEX}" == *"/nightly/"* ]]; then
uv pip install --prerelease=allow -v torch
fi
uv pip install -v -r requirements-test.txt
echo "import torch; print(f'torch: {torch.__version__}')" | uv run --no-project --script -
if [[ "${{ matrix.runner.toolkit }}" == *"CUDA"* ]]; then
uv pip install --no-build-isolation-package=flash-attn -v -r requirements-test-cuda.txt
echo "import flash_attn; print(f'flash_attn: {flash_attn.__version__}')" | uv run --no-project --script -
elif [[ "${{ matrix.runner.toolkit }}" == *"ROCm"* ]]; then
uv pip install -v -r requirements-test-rocm.txt
elif [[ "${{ matrix.runner.toolkit }}" == *"Metal"* ]]; then
uv pip install -v -r requirements-test-metal.txt
else
echo "::error::Unknown toolkit: ${{ matrix.runner.toolkit }}"
exit 1
fi
echo "::group::torch.utils.collect_env"
uv run --no-project -m -- torch.utils.collect_env
echo "::endgroup::"
- name: Clear uv cache for self-hosted runners (if setup failed)
if: >-
${{
failure() &&
startsWith(matrix.runner.name, 'self-hosted') &&
(steps.setup-uv.conclusion == 'failure' || steps.setup-venv.conclusion == 'failure')
}}
run: |
echo "Clearing uv cache at ${UV_CACHE_DIR} due to failure."
uv cache clean
- name: Run format check
id: format-check
run: |
mkdir -p build
# Run cmake to create the build directory with compile_commands.json
(
cd build
cmake .. ${CLANG_TIDY_CMAKE_OPTIONS} # no quotes here
)
rc=0
bash format.sh || rc="$?"
rm -rf build
if [[ "${rc}" -ne 0 ]]; then
echo "::error::Format check failed. Please run 'bash format.sh' locally to fix the issues."
exit 1
fi
- name: Enable core dump generation (Linux / GitHub-hosted runners)
if: ${{ runner.os == 'Linux' && !startsWith(matrix.runner.name, 'self-hosted') }}
run: |
sudo sysctl -w kernel.core_pattern="core.${{ matrix.python-version }}.${{ matrix.runner.toolkit }}.%P"
sudo sysctl -w kernel.core_uses_pid=0
sudo sysctl -w fs.suid_dumpable=1
sysctl kernel.core_pattern kernel.core_uses_pid fs.suid_dumpable
- name: Enable core dump generation (macOS / GitHub-hosted runners)
if: ${{ runner.os == 'macOS' && !startsWith(matrix.runner.name, 'self-hosted') }}
run: |
sudo sysctl -w kern.corefile="core.${{ matrix.python-version }}.${{ matrix.runner.toolkit }}.%P"
sudo sysctl -w kern.coredump=1
sudo sysctl -w kern.sugid_coredump=1
sysctl kern.corefile kern.coredump kern.sugid_coredump
- name: Install project (wheel form)
run: |
uv pip install -v .
- name: Run examples with Python ${{ matrix.python-version }} (${{ matrix.runner.toolkit }})
if: contains(matrix.runner.toolkit, 'CUDA')
run: |
cd testing
PYTEST=(
uv run --no-project -m --
pytest --verbose --color=yes --durations=0 --showlocals --cache-clear
)
"${PYTEST[@]}" --maxfail=3 --numprocesses=2 \
../examples
# NVIDIA CUDA tests
- name: Run CUDA tests with Python ${{ matrix.python-version }} (${{ matrix.runner.toolkit }})
id: cuda-tests
if: contains(matrix.runner.toolkit, 'CUDA')
run: |
cd testing
PYTEST=(
uv run --no-project -m --
pytest --verbose --color=yes --durations=0 --showlocals --cache-clear
)
"${PYTEST[@]}" --maxfail=3 --numprocesses=4 \
./python
# AMD ROCm tests
- name: Run ROCm tests with Python ${{ matrix.python-version }} (${{ matrix.runner.toolkit }})
id: rocm-tests
if: contains(matrix.runner.toolkit, 'ROCm')
# FIXME: ROCm test incorrectly skips tests
continue-on-error: true
run: |
cd testing
PYTEST=(
uv run --no-project -m --
pytest --verbose --color=yes --durations=0 --showlocals --cache-clear
)
"${PYTEST[@]}" --maxfail=3 --numprocesses=4 \
./python/amd/test_tilelang_test_amd.py
echo "::error::ROCm tests are known to be skipped incorrectly due to ROCm TVM build issues." >&2
# Apple Metal tests
- name: Run Metal tests with Python ${{ matrix.python-version }} (${{ matrix.runner.toolkit }})
id: metal-tests
if: contains(matrix.runner.toolkit, 'Metal')
run: |
cd testing
PYTEST=(
uv run --no-project -m --
pytest --verbose --color=yes --durations=0 --showlocals --cache-clear
)
"${PYTEST[@]}" --maxfail=3 --numprocesses=4 \
-k metal \
./python
- name: List generated files
if: ${{ !cancelled() }}
run: |
find . -type f -name '*.py[co]' -delete
find . -depth -type d -name "__pycache__" -exec rm -r "{}" +
if git status --ignored --porcelain | grep -qvE '/$'; then
ls -alh $(git status --ignored --porcelain | grep -vE '/$' | grep -oE '\S+$')
fi
name: CI
on: [pull_request]
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
PYTHON_VERSION: '3.12'
VENV_DIR: tilelang_ci
jobs:
format-check:
runs-on: [self-hosted, nvidia]
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
- name: Install python via uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
cache-local-path: ${{ runner.tool_cache }}/uv
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
uv pip install flash_attn==2.5.8 --no-build-isolation
- name: Run format check
run: |
set -ex
mkdir -p build
# run cmake to create the build directory with compile_commands.json
uv pip install cmake
cd build; USE_CUDA=1 cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON; cd ..
if ! output=$(./format.sh 2>&1); then
echo "------------------------------------"
echo "message:"
echo "$output"
printf '%s\n' "$output" | grep "Please review and stage the changes."
echo "------------------------------------"
exit 1
fi
rm -rf build
build-test-nvidia:
runs-on: [self-hosted, nvidia]
needs: format-check
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
- name: Install python via uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
cache-local-path: ${{ runner.tool_cache }}/uv
activate-environment: true
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup venv
run: |
[[ -f requirements-test.txt ]] && \
uv pip install -r requirements-test.txt --no-build-isolation
uv pip install flash_attn==2.5.8 --no-build-isolation
- name: Install project (wheel form)
run: |
uv pip install .
- name: Run examples
run: |
cd examples
python -m pytest -n 4 **/test*.py -v -r fE --durations=0 --cache-clear
- name: Run tests
run: |
cd testing/python
python -m pytest -n 4 -v -r fE --durations=0 --cache-clear --timeout=3600
...@@ -2,60 +2,125 @@ name: Dist ...@@ -2,60 +2,125 @@ name: Dist
on: on:
schedule: schedule:
# gemini said this is 6:00 china time # gemini said this is 6:00 china time
- cron: '0 22 * * *' - cron: "0 22 * * *"
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- setup.py
- setup.cfg
- pyproject.toml
- MANIFEST.in
- CMakeLists.txt
- version_provider.py
- .github/workflows/dist.yml
release: release:
types: [ published ] types:
- published
env: permissions:
PYTHON_VERSION: '3.12' contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
build-wheels: build-wheels:
name: Build wheels for Python ${{ matrix.python-version }} on ${{ matrix.target.runner }} with ${{ matrix.target.toolkit }}
if: |
github.repository_owner == 'tile-ai' &&
(github.event_name != 'pull_request' || !github.event.pull_request.draft)
strategy: strategy:
matrix: matrix:
os: [ubuntu-22.04, ubuntu-22.04-arm, macos-16] target:
include: - { runner: ubuntu-latest, toolkit: "CUDA-12.1" }
- os: ubuntu-22.04 - { runner: ubuntu-24.04-arm, toolkit: "CUDA-12.8" }
cuda_version: "12.1" - { runner: macos-latest, toolkit: "Metal" }
- os: ubuntu-22.04-arm python-version:
cuda_version: "12.8" - "3.8"
fail-fast: true # TVM is built with Python 3.8 Limited API, it should work with all Python >= 3.8.
runs-on: ${{ matrix.os }} # - "3.9"
# - "3.10"
# - "3.11"
# - "3.12"
# - "3.13"
# - "3.14"
fail-fast: false
timeout-minutes: 120
runs-on: ${{ matrix.target.runner }}
env: env:
CUDA_VERSION: ${{ matrix.cuda_version }} NO_VERSION_LABEL: ${{ github.event_name == 'release' && 'OFF' || 'ON' }}
NO_VERSION_LABEL: ${{ github.event_name != 'release' }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
submodules: recursive submodules: recursive
- name: ccache # NB: CIBW builds wheels in containers on Linux
uses: hendrikmuhs/ccache-action@v1.2 - name: Setup ccache (macOS only)
if: startsWith(matrix.os, 'macos') if: runner.os == 'macOS'
with: uses: hendrikmuhs/ccache-action@v1
create-symlink: true with:
key: ${{ github.job }}-${{ matrix.os }} create-symlink: true
key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.python-version }}-${{ matrix.target.toolkit }}
- name: Build wheels evict-old-files: "7d"
uses: pypa/cibuildwheel@v3.2
with: - name: Set CIBW_BUILD
output-dir: wheelhouse run: |
config-file: "{package}/pyproject.toml" PYTHON_VERSION="${{ matrix.python-version }}"
PYTHON_VERSION_MAJMIN="$(echo "${PYTHON_VERSION}" | cut -d '.' -f-2)"
# just for now to list all files PYTHON_VERSION_MAJMIN_NODOT="${PYTHON_VERSION_MAJMIN//./}"
- name: List wheels echo "CIBW_BUILD=cp${PYTHON_VERSION_MAJMIN_NODOT}-*" | tee -a "${GITHUB_ENV}"
id: ls-whl
run: echo "whl_name=$(ls wheelhouse | head -n1)" >> $GITHUB_OUTPUT if [[ "${{ matrix.target.toolkit }}" == *"CUDA"* ]]; then
CUDA_VERSION="${{ matrix.target.toolkit }}"
- uses: actions/upload-artifact@v4 CUDA_VERSION="${CUDA_VERSION#CUDA-}"
with: echo "CUDA_VERSION=${CUDA_VERSION}" | tee -a "${GITHUB_ENV}"
name: ${{ steps.ls-whl.outputs.whl_name }}.zip fi
path: wheelhouse/${{ steps.ls-whl.outputs.whl_name }}
compression-level: 0 - name: Build wheels
uses: pypa/cibuildwheel@v3.2
with:
package-dir: .
output-dir: wheelhouse
config-file: "{package}/pyproject.toml"
- name: Upload wheels
# Not PR to save artifact storage, as wheels are only needed for releases.
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.python-version }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.target.toolkit }}
path: wheelhouse/*.whl
if-no-files-found: error
list-artifacts:
name: List artifacts
# Not PR to save artifact storage, as wheels are only needed for releases.
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: [build-wheels]
timeout-minutes: 15
steps:
- name: Download built wheels
uses: actions/download-artifact@v5
with:
pattern: wheels-*
path: dist
merge-multiple: true
- name: List distributions
run: ls -lh dist/*
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: artifacts
path: dist/*
if-no-files-found: error
name: CI Test on Metal
on: [pull_request]
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: ${{ github.event_name == '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@v5
with:
fetch-depth: 0
submodules: recursive
- name: Install python via uv
uses: astral-sh/setup-uv@v7
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@v5
with:
fetch-depth: 1
submodules: recursive
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
create-symlink: true
key: ${{ github.job }}-${{ matrix.os }}
- name: Install python via uv
uses: astral-sh/setup-uv@v7
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
- name: Build wheel
run: |
source .venv/bin/activate
uv pip install -v .
- name: Run metal test
run: |
cd testing/python
unset PYTHONPATH
python -m pytest -k metal -v -r fE --durations=0 --cache-clear --timeout=3600
...@@ -16,7 +16,8 @@ jobs: ...@@ -16,7 +16,8 @@ jobs:
perfbench: perfbench:
name: Benchmark between PR and main name: Benchmark between PR and main
if: | if: |
github.event_name == 'pull_request' && github.repository_owner == 'tile-ai' &&
github.event.issue.pull_request &&
(contains(github.event.comment.body, '/performance-report') || contains(github.event.comment.body, '/perf')) (contains(github.event.comment.body, '/performance-report') || contains(github.event.comment.body, '/perf'))
runs-on: [self-hosted, nvidia] runs-on: [self-hosted, nvidia]
steps: steps:
...@@ -27,7 +28,7 @@ jobs: ...@@ -27,7 +28,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
- name: Set up Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.9" python-version: "3.9"
......
...@@ -8,6 +8,7 @@ on: ...@@ -8,6 +8,7 @@ on:
jobs: jobs:
remind: remind:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository_owner == 'tile-ai'
steps: steps:
- name: Remind - name: Remind
uses: actions/github-script@v8 uses: actions/github-script@v8
......
...@@ -13,8 +13,15 @@ jobs: ...@@ -13,8 +13,15 @@ jobs:
docs: docs:
name: Build and Publish Docs name: Build and Publish Docs
if: | if: |
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main') || github.repository_owner == 'tile-ai' &&
github.event_name == 'workflow_dispatch' (
(
github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main'
) ||
github.event_name == 'workflow_dispatch'
)
runs-on: [self-hosted, nvidia] runs-on: [self-hosted, nvidia]
steps: steps:
- name: Checkout repository - name: Checkout repository
...@@ -23,7 +30,7 @@ jobs: ...@@ -23,7 +30,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
- name: Set up Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.10" python-version: "3.10"
......
name: CI Test on AMD
on: [pull_request]
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
PYTHON_VERSION: '3.12'
VENV_DIR: tilelang_ci
PYTORCH_INDEX_URL: https://download.pytorch.org/whl/nightly/rocm6.3/
jobs:
format-check:
runs-on: [self-hosted, amd, gpu]
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Ensure venv (local & persistent)
run: |
set -e
REQS_HASH=$(sha256sum requirements-rocm.txt 2>/dev/null | awk '{print $1}' || echo "no_requirements")
MARKER="${{ runner.tool_cache }}/.venv_marker_${{ env.PYTHON_VERSION }}_${REQS_HASH:0:8}"
if [[ -f "$MARKER" ]] && [[ -f "${{ runner.tool_cache }}/${{ env.VENV_DIR }}/bin/activate" ]]; then
echo "venv exists and hash matches – reuse it"
else
echo "venv stale or missing – recreating"
rm -rf "${{ runner.tool_cache }}/${{ env.VENV_DIR }}" "$MARKER"
python -m venv "${{ runner.tool_cache }}/${{ env.VENV_DIR }}"
# shellcheck source=/dev/null
source "${{ runner.tool_cache }}/${{ env.VENV_DIR }}/bin/activate"
python -m pip install --upgrade pip --no-user
[[ -f requirements-rocm.txt ]] && \
PIP_NO_BUILD_ISOLATION=1 pip install -r requirements-rocm.txt --no-user
touch "$MARKER"
fi
- name: Run format check
run: |
source "${{ runner.tool_cache }}/${{ env.VENV_DIR }}/bin/activate"
git submodule update --init --recursive --checkout
mkdir -p build
cd build; USE_ROCM=1 cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON; cd ..
if ! output=$(./format.sh 2>&1); then
echo "------------------------------------"
echo "message:"
echo "$output"
printf '%s\n' "$output" | grep "Please review and stage the changes."
echo "------------------------------------"
exit 1
fi
rm -rf build
build-test-amd:
runs-on: [self-hosted, amd, gpu]
needs: format-check
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Ensure venv (local & persistent)
run: |
set -e
REQS_HASH=$(sha256sum requirements-rocm.txt 2>/dev/null | awk '{print $1}' || echo "no_requirements")
MARKER="${{ runner.tool_cache }}/.venv_marker_${{ env.PYTHON_VERSION }}_${REQS_HASH:0:8}"
if [[ -f "$MARKER" ]] && [[ -f "${{ runner.tool_cache }}/${{ env.VENV_DIR }}/bin/activate" ]]; then
echo "venv exists and hash matches – reuse it"
else
echo "venv stale or missing – recreating"
rm -rf "${{ runner.tool_cache }}/${{ env.VENV_DIR }}" "$MARKER"
python -m venv "${{ runner.tool_cache }}/${{ env.VENV_DIR }}"
# shellcheck source=/dev/null
source "${{ runner.tool_cache }}/${{ env.VENV_DIR }}/bin/activate"
python -m pip install --upgrade pip --no-user
[[ -f requirements-rocm.txt ]] && \
PIP_NO_BUILD_ISOLATION=1 pip install -r requirements-rocm.txt --no-user
touch "$MARKER"
fi
- name: Install project (wheel form)
run: |
echo "Installing project (wheel form)"
source "${{ runner.tool_cache }}/${{ env.VENV_DIR }}/bin/activate"
git submodule update --init --recursive --checkout --recommend-shallow
USE_ROCM=True pip install . --no-user
- name: Run tests
run: |
echo "Running tests"
source "${{ runner.tool_cache }}/${{ env.VENV_DIR }}/bin/activate"
cd testing/python/amd
unset PYTHONPATH
python -m pytest -v --cache-clear test_tilelang_test_amd.py
...@@ -48,6 +48,12 @@ repos: ...@@ -48,6 +48,12 @@ repos:
- repo: https://github.com/google/yapf - repo: https://github.com/google/yapf
rev: v0.43.0 # sync with requirements-lint.txt rev: v0.43.0 # sync with requirements-lint.txt
hooks: hooks:
- id: yapf
name: yapf-multiproc-bugfix
# yapf is not multiprocess safe, so we run a dummy yapf first.
args: [--in-place, docs/conf.py]
always_run: true
pass_filenames: false
- id: yapf - id: yapf
args: [--recursive, --in-place] args: [--recursive, --in-place]
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
......
import os
import random
os.environ["PYTHONHASHSEED"] = "0"
random.seed(0)
try:
import torch
except ImportError:
pass
else:
torch.manual_seed(0)
try:
import numpy as np
except ImportError:
pass
else:
np.random.seed(0)
...@@ -527,7 +527,7 @@ def main(m=256, ...@@ -527,7 +527,7 @@ def main(m=256,
print(f"max abs diff: {max_val} at index: {max_idx}") print(f"max abs diff: {max_val} at index: {max_idx}")
assert_similar( assert_similar(
output, ref_output, name="output", output, ref_output, name="output",
eps=1e-5) # We care about the similarity rather than abs. difference eps=2e-5) # We care about the similarity rather than abs. difference
print("All checks pass. ✅") print("All checks pass. ✅")
......
...@@ -5,7 +5,7 @@ import example_vertical_slash_sparse_attn ...@@ -5,7 +5,7 @@ import example_vertical_slash_sparse_attn
@tilelang.testing.requires_cuda @tilelang.testing.requires_cuda
def test_vs_sparse_attn(): def test_vs_sparse_attn():
example_vertical_slash_sparse_attn.main() example_vertical_slash_sparse_attn.main(argv=[])
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -67,13 +67,13 @@ def ref_program(logits, top_k): ...@@ -67,13 +67,13 @@ def ref_program(logits, top_k):
return top_k_gates, top_k_indices.to(torch.int32) return top_k_gates, top_k_indices.to(torch.int32)
def main(): def main(argv=None):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--M", type=int, default=320, help="num_tokens") parser.add_argument("--M", type=int, default=320, help="num_tokens")
parser.add_argument("--N", type=int, default=128, help="num_experts") parser.add_argument("--N", type=int, default=128, help="num_experts")
parser.add_argument("--topk", type=int, default=6, help="topk") parser.add_argument("--topk", type=int, default=6, help="topk")
parser.add_argument("--blk_m", type=int, default=64, help="blk_m") parser.add_argument("--blk_m", type=int, default=64, help="blk_m")
args = parser.parse_args() args = parser.parse_args(argv)
M, N, topk, blk_m = args.M, args.N, args.topk, args.blk_m M, N, topk, blk_m = args.M, args.N, args.topk, args.blk_m
logits = torch.rand((M, N), device="cuda", dtype=torch.float32) logits = torch.rand((M, N), device="cuda", dtype=torch.float32)
......
...@@ -4,8 +4,8 @@ import example_topk ...@@ -4,8 +4,8 @@ import example_topk
@tilelang.testing.requires_cuda @tilelang.testing.requires_cuda
def test_topk_tilelang(): def test_topk_tilelang():
example_topk.main() example_topk.main(argv=[])
if __name__ == "__main__": if __name__ == "__main__":
test_topk_tilelang() tilelang.testing.main()
[project] [project]
name = "tilelang" name = "tilelang"
authors = [{name = "Tile-AI"}]
maintainers = [{name = "Lei Wang", email = "leiwang1999@outlook.com"}]
description = "A tile level programming language to generate high performance code." description = "A tile level programming language to generate high performance code."
readme.file = "README.md" readme = "README.md"
requires-python = ">=3.8"
authors = [{name = "TileLang Contributors"}, {name = "Tile-AI"}]
maintainers = [{name = "Lei Wang", email = "leiwang1999@outlook.com"}]
license = "MIT" license = "MIT"
keywords = ["BLAS", "CUDA", "HIP", "Code Generation", "TVM"] keywords = ["BLAS", "CUDA", "HIP", "Code Generation", "TVM"]
classifiers = [ classifiers = [
...@@ -20,37 +21,28 @@ classifiers = [ ...@@ -20,37 +21,28 @@ classifiers = [
"Intended Audience :: Science/Research", "Intended Audience :: Science/Research",
"Scientific/Engineering :: Artificial Intelligence", "Scientific/Engineering :: Artificial Intelligence",
] ]
readme.content-type = "text/markdown"
requires-python = ">=3.8"
dynamic = ["version"] dynamic = ["version"]
# Somehow this does not work, hard-code for now
# dynamic = ["version", "dependencies"]
# [tool.setuptools.dynamic]
# dependencies = {file = ["requirements.txt"]}
dependencies = [ dependencies = [
"numpy>=1.23.5",
"tqdm>=4.62.3",
"typing_extensions>=4.10.0",
"cloudpickle", "cloudpickle",
"ml_dtypes", "ml-dtypes",
"numpy>=1.23.5",
"psutil", "psutil",
"torch", "torch",
"torch>=2.7; platform_system == 'Darwin'" "torch>=2.7; platform_system == 'Darwin'",
"tqdm>=4.62.3",
"typing-extensions>=4.10.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
# mldtypes should be greater than 0.5.1 # mldtypes should be greater than 0.5.1
# if you want to enable fp4 # if you want to enable fp4
fp4 = ["ml_dtypes>=0.5.1"] fp4 = ["ml-dtypes>=0.5.1"]
[build-system] [build-system]
requires = [ requires = [
"setuptools>=63", "cython>=3.0.0",
"Cython>=3.0.0",
"scikit-build-core", "scikit-build-core",
"setuptools>=63",
] ]
build-backend = "scikit_build_core.build" build-backend = "scikit_build_core.build"
...@@ -135,49 +127,42 @@ ignore = [ ...@@ -135,49 +127,42 @@ ignore = [
"3rdparty/**/*" = ["ALL"] "3rdparty/**/*" = ["ALL"]
"examples/deepseek_v32/inference/**/*" = ["ALL"] "examples/deepseek_v32/inference/**/*" = ["ALL"]
[tool.pytest.ini_options]
verbosity_assertions = 3
filterwarnings = ["always"]
[tool.cibuildwheel] [tool.cibuildwheel]
archs = ["auto64"] archs = ["auto64"]
# wait for tvm fix
build = "cp38-*"
[tool.cibuildwheel.macos]
archs = ["arm64"]
[tool.cibuildwheel.linux]
# Pin to glibc 2.17 for x86 and 2.28 for aarch64 for now # Pin to glibc 2.17 for x86 and 2.28 for aarch64 for now
manylinux-x86_64-image = "manylinux2014" manylinux-x86_64-image = "manylinux2014"
manylinux-aarch64-image = "manylinux_2_28" manylinux-aarch64-image = "manylinux_2_28"
skip = "*-musllinux*" skip = "*musllinux*"
environment-pass = ["CUDA_VERSION"] environment-pass = ["CUDA_VERSION"]
[tool.cibuildwheel.linux]
repair-wheel-command = [ repair-wheel-command = [
"auditwheel repair --exclude libcuda.so.1 --exclude /usr/local/cuda\\* -w {dest_dir} {wheel}", "auditwheel repair --exclude libcuda.so.1 --exclude '/usr/local/cuda*' -w {dest_dir} {wheel}",
"pipx run abi3audit --strict --report {wheel}", "pipx run abi3audit --strict --report {wheel}",
] ]
environment.PATH = "/usr/local/cuda/bin:$PATH"
# Install CUDA runtime and stub driver library # Install CUDA runtime and stub driver library
# manylinux_2_28 uses gcc 14, which needs CUDA 12.8 # manylinux_2_28 uses gcc 14, which needs CUDA 12.8
before-all = """ before-all = """
set -eux set -eux
case "$(uname -m)" in case "$(uname -m)" in
"x86_64") "x86_64")
yum-config-manager --add-repo https://developer.download.nvidia.cn/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo yum-config-manager --add-repo https://developer.download.nvidia.cn/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo
;; ;;
"aarch64") "aarch64")
dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/sbsa/cuda-rhel8.repo dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/sbsa/cuda-rhel8.repo
;; ;;
*) *)
exit 1 exit 1
;; ;;
esac esac
# Assume CUDA_VERSION=xx.y cudaver="$(echo "${CUDA_VERSION:-"12.4"}" | cut -d '.' -f-2)"
v=${CUDA_VERSION:-12.4} v="${cudaver//./-}"
v=${v:0:4} yum install -y "cuda-minimal-build-${v}" "cuda-driver-devel-${v}" "cuda-nvrtc-devel-${v}"
v=${v/./-}
yum install -y cuda-minimal-build-${v} cuda-driver-devel-${v} cuda-nvrtc-devel-${v}
""" """
[tool.cibuildwheel.linux.environment]
# Equlivant to `source /opt/rh/gcc-toolset-12/enable`, safe when gcc-toolset-12 is not installed
PATH = "/usr/local/cuda/bin:$PATH"
# Requirements to run local build with `--no-build-isolation` or other developments # Requirements to run local build with `--no-build-isolation` or other developments
Cython>=3.0.0
build build
cmake>=3.26 cmake>=3.26
cython>=3.0.0
ninja
packaging packaging
setuptools>=61
scikit-build-core scikit-build-core
setuptools>=61
torch torch
wheel wheel
ninja
auditwheel; platform_system == 'Linux' auditwheel; platform_system == 'Linux'
patchelf; platform_system == 'Linux' patchelf; platform_system == 'Linux'
......
# formatting # Format and lint requirements
pre-commit pre-commit
yapf==0.43.0
ruff==0.14.0
codespell[toml]==2.4.1
clang-format==15.0.7 clang-format==15.0.7
clang-tidy==18.1.8 clang-tidy==18.1.8
codespell[toml]==2.4.1
ruff==0.14.0
yapf==0.43.0
# lint requirements
-r requirements-lint.txt
# build requirements
Cython
cmake>=3.26
# runtime requirements
cffi
cpplint
Cython
docutils
dtlib
numpy>=1.23.5
pytest>=6.2.4
pytest_xdist>=2.2.1
pytest-durations
pytest-timeout
packaging>=21.0
PyYAML
tqdm>=4.62.3
typing_extensions>=4.10.0
requests
cloudpickle
ml_dtypes
psutil
tabulate
wheel
setuptools
einops
scipy
tornado
# Lint requirements
--requirement requirements-lint.txt
# Common test requirements
--requirement requirements-test.txt
# CUDA specific requirements
flash-attn==2.5.8
# Lint requirements
--requirement requirements-lint.txt
# Common test requirements
--requirement requirements-test.txt
# Metal specific requirements
# Currently: none
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment