diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbc2c6a..a54eb97 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,7 +41,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-13, macos-latest, windows-latest] + os: [ubuntu-latest, macos-15-intel, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index d14823d..a107e45 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: - os: [ubuntu-latest, macos-13, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/.gitignore b/.gitignore index a54a539..6e08b37 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ build/ .vscode/ __pycache__/ -qoco_custom*/ \ No newline at end of file +qoco_custom*/ +src/bindings.cpp +dist/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d5d3ff..6258701 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,13 +23,22 @@ include(FetchContent) FetchContent_Declare( qoco GIT_REPOSITORY https://github.com/qoco-org/qoco.git - GIT_TAG bbab0db3899f19331c5c8e9d31d68bbbb68704ae + GIT_TAG d40cb8170b0e967be38ad0e1b134d9c51f5d636e ) list(POP_BACK CMAKE_MESSAGE_INDENT) FetchContent_MakeAvailable(qoco) -pybind11_add_module(qoco_ext src/bindings.cpp) -target_include_directories(qoco_ext INTERFACE ${qoco_SOURCE_DIR}/include) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/bindings.cpp.in + ${CMAKE_CURRENT_SOURCE_DIR}/src/bindings.cpp) +pybind11_add_module(${QOCO_EXT_MODULE_NAME} src/bindings.cpp) +target_include_directories(${QOCO_EXT_MODULE_NAME} INTERFACE ${qoco_SOURCE_DIR}/include) +install(TARGETS ${QOCO_EXT_MODULE_NAME} DESTINATION . COMPONENT python) + +if(${QOCO_ALGEBRA_BACKEND} STREQUAL "builtin") target_link_libraries(qoco_ext PUBLIC pybind11::module qocostatic) -install(TARGETS qoco_ext DESTINATION . COMPONENT python) +elseif(${QOCO_ALGEBRA_BACKEND} STREQUAL "cuda") + enable_language(CUDA) + find_package(CUDA) + target_link_libraries(qoco_cuda PUBLIC pybind11::module qocostatic) +endif() \ No newline at end of file diff --git a/Dockerfile.cuda-manylinux b/Dockerfile.cuda-manylinux new file mode 100644 index 0000000..a7b2eb7 --- /dev/null +++ b/Dockerfile.cuda-manylinux @@ -0,0 +1,19 @@ +# Base manylinux image with Python interpreters +FROM quay.io/pypa/manylinux_2_28_x86_64:2025.11.09-2 + +# Install prerequisites +RUN yum install -y wget tar bzip2 xz gzip make gcc gcc-c++ git + +# Install CUDA Toolkit 13.1 and cuDSS 0.7.1 +RUN wget https://developer.download.nvidia.com/compute/cuda/13.0.0/local_installers/cuda_13.0.0_580.65.06_linux.run && \ + sh cuda_13.0.0_580.65.06_linux.run --silent --toolkit && \ + rm cuda_13.0.0_580.65.06_linux.run && \ + curl -O https://developer.download.nvidia.com/compute/cudss/0.7.1/local_installers/cudss-local-repo-rhel10-0.7.1-0.7.1-1.x86_64.rpm && \ + rpm -i cudss-local-repo-rhel10-0.7.1-0.7.1-1.x86_64.rpm && \ + dnf clean all && \ + dnf -y install cudss + +# Set CUDA environment variables +ENV PATH=/usr/local/cuda/bin:$PATH +ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH +ENV CUDAToolkit_ROOT=/usr/local/cuda/bin diff --git a/backend/cuda/cibuildwheel.toml b/backend/cuda/cibuildwheel.toml new file mode 100644 index 0000000..7702380 --- /dev/null +++ b/backend/cuda/cibuildwheel.toml @@ -0,0 +1,4 @@ +[tool.cibuildwheel.linux] +skip = ["*-musllinux*"] +manylinux-x86_64-image = "mycuda-manylinux:latest" +environment = { CUDAToolkit_ROOT = "/usr/local/cuda/bin" } diff --git a/backend/cuda/pyproject.toml b/backend/cuda/pyproject.toml new file mode 100644 index 0000000..e984347 --- /dev/null +++ b/backend/cuda/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["scikit-build-core", "pybind11"] +build-backend = "scikit_build_core.build" + +[project] +name = "qoco-cuda" +version = "0.1.0" +description = "QOCO: Quadratic Objective Conic Optimizer" +requires-python = ">=3.8" +authors = [{ name = "Govind M. Chari", email = "govindchari1@gmail.com" }] +dependencies = ["numpy>=1.7", "scipy>=0.13.2", "setuptools", "qoco>=0.2.0"] + +[tool.scikit-build] +install.components = ["python"] + +[tool.scikit-build.cmake.define] +QOCO_ALGEBRA_BACKEND = "cuda" +QOCO_EXT_MODULE_NAME = "qoco_cuda" + +[project.urls] +Homepage = "https://github.com/qoco-org/qoco" +Issues = "https://github.com/qoco-org/qoco/issues" \ No newline at end of file diff --git a/build_cuda_wheels.sh b/build_cuda_wheels.sh new file mode 100755 index 0000000..776c9de --- /dev/null +++ b/build_cuda_wheels.sh @@ -0,0 +1,3 @@ +cp backend/cuda/pyproject.toml . +docker build -f Dockerfile.cuda-manylinux -t mycuda-manylinux:latest . +cibuildwheel --platform linux --output-dir dist --config-file backend/cuda/cibuildwheel.toml \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 35752b9..ddbe79e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,20 @@ description = "QOCO: Quadratic Objective Conic Optimizer" readme = "README.md" requires-python = ">=3.8" authors = [{ name = "Govind M. Chari", email = "govindchari1@gmail.com" }] -dependencies = ["jinja2", "numpy>=1.7", "qdldl", "scipy>=0.13.2", "setuptools"] +dependencies = ["numpy>=1.7", "scipy>=0.13.2", "setuptools"] +[project.optional-dependencies] +cuda = [ + "qoco-cuda", +] [tool.scikit-build] install.components = ["python"] wheel.install-dir = "qoco" +[tool.scikit-build.cmake.define] +QOCO_ALGEBRA_BACKEND = "builtin" +QOCO_EXT_MODULE_NAME = "qoco_ext" + [project.urls] Homepage = "https://github.com/qoco-org/qoco" Issues = "https://github.com/qoco-org/qoco/issues" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3b81b01..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -pybind11 -numpy>=1.7 -# Exclude scipy 1.12 because the random sparse array function started returning -# the transpose of the original, breaking the unit tests. This was fixed in 1.13.0. -# ref: https://github.com/scipy/scipy/issues/20027 -scipy>=0.13.2,!=1.12.0 -qdldl \ No newline at end of file diff --git a/src/bindings.cpp b/src/bindings.cpp.in similarity index 99% rename from src/bindings.cpp rename to src/bindings.cpp.in index 50a2af2..7e88d7b 100644 --- a/src/bindings.cpp +++ b/src/bindings.cpp.in @@ -241,7 +241,7 @@ QOCOInt PyQOCOSolver::update_settings(const QOCOSettings &new_settings) return qoco_update_settings(this->_solver, &new_settings); } -PYBIND11_MODULE(qoco_ext, m) +PYBIND11_MODULE(@QOCO_EXT_MODULE_NAME@, m) { // Enums. py::enum_(m, "qoco_solve_status", py::module_local()) diff --git a/src/qoco/interface.py b/src/qoco/interface.py index ac9607c..e55ced5 100644 --- a/src/qoco/interface.py +++ b/src/qoco/interface.py @@ -5,7 +5,32 @@ import numpy as np from scipy import sparse from types import SimpleNamespace -import time + +ALGEBRAS = ( + "cuda", + "builtin", +) + +ALGEBRA_MODULES = { + "cuda": "qoco_cuda", + "builtin": "qoco.qoco_ext", +} + + +def algebra_available(algebra): + assert algebra in ALGEBRAS, f"Unknown algebra {algebra}" + module = ALGEBRA_MODULES[algebra] + + try: + importlib.import_module(module) + except ImportError: + return False + else: + return True + + +def algebras_available(): + return [algebra for algebra in ALGEBRAS if algebra_available(algebra)] class QOCO: @@ -41,7 +66,10 @@ def __init__(self, *args, **kwargs): "QOCO_MAX_ITER", ] - self.ext = importlib.import_module("qoco.qoco_ext") + self.algebra = kwargs.pop("algebra") if "algebra" in kwargs else "builtin" + if not algebra_available(self.algebra): + raise RuntimeError(f"Algebra {self.algebra} not available") + self.ext = importlib.import_module(ALGEBRA_MODULES[self.algebra]) self._solver = None def update_settings(self, **kwargs):