diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index a6c7f2f..0fc0f4d 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -58,6 +58,33 @@ jobs: name: wheel-${{ matrix.wheel_tag }} path: wheelhouse/pygpubench*.whl + gpu-test: + name: GPU tests (Modal L4) + needs: wheel + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Download wheel + uses: actions/download-artifact@v4 + with: + pattern: wheel-* + path: dist/ + merge-multiple: true + + - name: Install Modal + run: pip install modal + + - name: Run GPU tests + env: + MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} + MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} + run: modal run ci/modal_gpu_test.py --wheel dist/pygpubench*.whl --test-dir test + release: name: Publish to GitHub Releases needs: wheel diff --git a/ci/modal_gpu_test.py b/ci/modal_gpu_test.py new file mode 100644 index 0000000..0e30399 --- /dev/null +++ b/ci/modal_gpu_test.py @@ -0,0 +1,69 @@ +"""Run pygpubench GPU tests on a Modal L4 GPU. + +Usage: modal run ci/modal_gpu_test.py +""" + +import modal +from pathlib import Path + +image = ( + modal.Image.from_registry( + "nvidia/cuda:13.0.1-cudnn-devel-ubuntu24.04", add_python="3.12" + ) + .entrypoint([]) + .uv_pip_install("torch", index_url="https://download.pytorch.org/whl/cu130") +) + +app = modal.App("pygpubench-ci", image=image) + + +@app.function(gpu="L4", timeout=600) +def run_tests(whl_bytes: bytes, whl_name: str, test_files: dict[str, bytes]): + import subprocess + import sys + import os + + # Write wheel and install it + whl_path = f"/tmp/{whl_name}" + with open(whl_path, "wb") as f: + f.write(whl_bytes) + subprocess.run([sys.executable, "-m", "pip", "install", whl_path], check=True) + + # Write test files + test_dir = "/tmp/tests" + os.makedirs(test_dir, exist_ok=True) + for name, content in test_files.items(): + with open(os.path.join(test_dir, name), "wb") as f: + f.write(content) + + # Run all test scripts + os.chdir(test_dir) + failed = [] + for test_file in sorted(test_files): + if test_file == "submission.py": + continue + print(f"\n=== {test_file} ===") + result = subprocess.run([sys.executable, test_file], text=True) + if result.returncode != 0: + failed.append(test_file) + + if failed: + print(f"\nFailed: {', '.join(failed)}") + raise SystemExit(1) + + +@app.local_entrypoint() +def main(wheel: str, test_dir: str = "test"): + import glob + + # Read the wheel + whl_path = Path(wheel) + whl_bytes = whl_path.read_bytes() + + # Read all test files + test_path = Path(test_dir) + test_files = {} + for f in test_path.glob("*.py"): + test_files[f.name] = f.read_bytes() + + run_tests.remote(whl_bytes, whl_path.name, test_files)