Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/gpu_ci_trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Sync to GitLab and Run GPU CI

on:
push:
branches: [main, devel]
pull_request:
branches: [main, devel]
workflow_dispatch:

jobs:
sync-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install SSH Key
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}

- name: Push to GitLab via SSH & Provide Link
run: |
# 1. Setup SSH known hosts
mkdir -p ~/.ssh
ssh-keyscan gitlab.mpcdf.mpg.de >> ~/.ssh/known_hosts

# 2. Determine target branch
if [ "${{ github.event_name }}" == "pull_request" ]; then
TARGET_BRANCH="pr-${{ github.event.number }}"
else
TARGET_BRANCH="${{ github.ref_name }}"
fi

# 3. Add GitLab SSH remote
git remote add gitlab git@gitlab.mpcdf.mpg.de:maxlin/cunumpy.git

# 4. Force push (This automatically starts the GitLab Pipeline)
git push -f gitlab HEAD:refs/heads/$TARGET_BRANCH

# 5. Provide the direct link
# We construct the URL manually since the push triggers the pipeline automatically
PIPELINE_URL="https://gitlab.mpcdf.mpg.de/maxlin/cunumpy/-/pipelines?ref=$TARGET_BRANCH"

echo "::notice::GitLab GPU CI Pipeline started automatically via Push!"
echo "::notice::View Pipeline: $PIPELINE_URL"

93 changes: 38 additions & 55 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,57 +1,40 @@
# This file is a template, and might need editing before it works on your project.
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml

# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/python/tags/
image: python:latest

# Change pip's cache directory to be inside the project directory since we can
# only cache local items.
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

# https://pip.pypa.io/en/stable/topics/caching/
cache:
paths:
- .cache/pip

before_script:
- python --version ; pip --version # For debugging
- pip install virtualenv
- virtualenv venv
- source venv/bin/activate

test:
# Use the specialized MPCDF HPC image
CUDA_IMAGE: "gitlab-registry.mpcdf.mpg.de/mpcdf/ci-module-image/nvhpcsdk_26:2026"
PIP_DISABLE_PIP_VERSION_CHECK: "1"

stages:
- test

gpu_tests:
stage: test
image: ${CUDA_IMAGE}
tags:
- gpu-nvidia
- gpu-nvidia-cc80
before_script:
- module load python-waterboa/2025.06
- module load nvhpcsdk/26
script:
- pip install ruff tox # you can also use tox
- pip install --editable ".[test]"
- tox -e py,ruff

run:
script:
- pip install .
# run the command here
artifacts:
paths:
- build/*

pages:
script:
- pip install sphinx sphinx-rtd-theme
- cd doc
- make html
- mv build/html/ ../public/
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

deploy:
stage: deploy
script: echo "Define your deployment script!"
environment: production

- echo "--- CUDA Sanity Check ---"
- nvidia-smi

- echo "--- Detect Compute Capability ---"
- nvidia-smi --query-gpu=compute_cap --format=csv,noheader

- echo "--- Tool Versions ---"
- cmake --version
- python3 --version
- git --version

- echo "--- Pytest Execution ---"
# The MPCDF image likely has a specific python environment.
# We install our dependencies into the user directory or a virtualenv.
- python3 -m pip install --user cupy-cuda12x
- python3 -m pip install --user -e .

# Add the user bin to PATH for pytest
- export PATH="$HOME/.local/bin:$PATH"
- export ARRAY_BACKEND=cupy

- python3 -m pytest tests/unit/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `xp.synchronize()`: Blocks until GPU operations are complete (no-op on CPU). Essential for accurate benchmarking.
- **Developer Experience**:
- Added `isort` configuration to `pyproject.toml` with `black` profile compatibility.
- Reorganized test suite into specialized files: `test_numpy.py`, `test_cupy.py`, and `test_cunumpy.py`.

### Changed
- **Dynamic Dispatch Architecture**: Refactored `src/cunumpy/xp.py` to use module-level `__getattr__`. This ensures that `cunumpy.<op>` calls always resolve to the currently active backend module, enabling seamless runtime switching via `set_backend`.
Expand Down
131 changes: 0 additions & 131 deletions tests/unit/test_app.py

This file was deleted.

58 changes: 58 additions & 0 deletions tests/unit/test_cunumpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import numpy as np
import pytest

import cunumpy as xp


def test_to_numpy():
arr = xp.array([1, 2, 3])
# Even if it's already numpy, to_numpy should work
arr_np = xp.to_numpy(arr)
assert isinstance(arr_np, np.ndarray)
assert np.array_equal(arr_np, [1, 2, 3])


def test_to_cunumpy():
arr = np.array([1, 2, 3])
arr_xp = xp.to_cunumpy(arr)
# Backend is numpy in tests usually
assert isinstance(arr_xp, (np.ndarray, xp.ndarray))


def test_get_backend_and_is_gpu_cpu():
arr = np.array([1, 2, 3])
assert xp.get_backend(arr) == "numpy"
assert xp.is_gpu(arr) is False
assert xp.is_cpu(arr) is True


def test_use_backend():
# Initial backend should be numpy (default) in this test environment
assert "numpy" in xp.xp.__name__

with xp.use_backend("numpy"):
assert "numpy" in xp.xp.__name__
arr = xp.zeros(10)
assert isinstance(arr, np.ndarray)

assert "numpy" in xp.xp.__name__


def test_set_backend():
# Set to numpy
xp.set_backend("numpy")
assert "numpy" in xp.xp.__name__
arr = xp.array([1])
assert isinstance(arr, np.ndarray)

# Set to cupy (falls back to numpy if not available)
xp.set_backend("cupy")
# If cupy is not installed, xp.xp will be numpy module
arr2 = xp.array([2])
assert arr2 is not None


def test_backend_bools():
with xp.use_backend("numpy"):
assert xp.numpy_backend is True
assert xp.cupy_backend is False
54 changes: 54 additions & 0 deletions tests/unit/test_cupy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import numpy as np
import pytest

import cunumpy as xp


def test_to_cupy_available():
try:
import cupy as cp
except ImportError:
pytest.skip("CuPy not installed")

with xp.use_backend("cupy"):
arr = np.array([1, 2, 3])
arr_cp = xp.to_cupy(arr)
assert isinstance(arr_cp, cp.ndarray)


def test_to_cupy_not_available():
try:
import cupy

pytest.skip("CuPy is installed, cannot test missing cupy error")
except ImportError:
pass

with xp.use_backend("cupy"):
arr = np.array([1, 2, 3])
with pytest.raises(ImportError):
xp.to_cupy(arr)


def test_synchronize():
# Should not crash on any backend
xp.synchronize()

with xp.use_backend("numpy"):
xp.synchronize()

with xp.use_backend("cupy"):
xp.synchronize()


def test_xp_array_cupy():
try:
import cupy as cp
except ImportError:
pytest.skip("CuPy not installed")

with xp.use_backend("cupy"):
arr = xp.array([1, 2])
arr *= 2
assert isinstance(arr, cp.ndarray)
assert cp.asnumpy(arr).tolist() == [2, 4]
Loading
Loading