Skip to content
Open
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
484 changes: 484 additions & 0 deletions .github/workflows/python-release.yml

Large diffs are not rendered by default.

32 changes: 24 additions & 8 deletions python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,21 @@ foreach(source ggml/CMakeLists.txt thirdparty/json/CMakeLists.txt thirdparty/min
endif()
endforeach()

# Build a self-contained CPU extension, never CLI executables, GPU backends,
# external OpenMP runtimes, or shared libraries requiring a loader search path.
# Each wheel contains one statically linked runtime. GPU wheels install into
# separate packages so extras never overwrite the base package's CPU extension.
set(CLAP_PYTHON_BACKEND "CPU" CACHE STRING "Python wheel backend: CPU, CUDA, HIP or VULKAN")
set_property(CACHE CLAP_PYTHON_BACKEND PROPERTY STRINGS CPU CUDA HIP VULKAN)
if(NOT CLAP_PYTHON_BACKEND MATCHES "^(CPU|CUDA|HIP|VULKAN)$")
message(FATAL_ERROR "Unknown Python backend: ${CLAP_PYTHON_BACKEND}")
endif()
set(CLAP_PYTHON_PACKAGE "clap_cpp")
if(NOT CLAP_PYTHON_BACKEND STREQUAL "CPU")
string(TOLOWER "${CLAP_PYTHON_BACKEND}" backend_name)
if(backend_name STREQUAL "hip")
set(backend_name "rocm")
endif()
set(CLAP_PYTHON_PACKAGE "clap_cpp_${backend_name}")
endif()
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
Expand All @@ -22,6 +35,9 @@ foreach(backend METAL CUDA HIP VULKAN SYCL OPENCL OPENVINO RPC MUSA WEBGPU ZDNN
VIRTGPU VIRTGPU_BACKEND ET HEXAGON ZENDNN BLAS ACCELERATE)
set(GGML_${backend} OFF CACHE BOOL "" FORCE)
endforeach()
if(NOT CLAP_PYTHON_BACKEND STREQUAL "CPU")
set(GGML_${CLAP_PYTHON_BACKEND} ON CACHE BOOL "" FORCE)
endif()
foreach(feature BACKEND_DL OPENMP BUILD_TESTS BUILD_EXAMPLES)
set(GGML_${feature} OFF CACHE BOOL "" FORCE)
endforeach()
Expand All @@ -42,9 +58,9 @@ pybind11_add_module(_clap_cpp MODULE bindings.cpp)
target_link_libraries(_clap_cpp PRIVATE clap)
target_compile_features(_clap_cpp PRIVATE cxx_std_17)
set_target_properties(_clap_cpp PROPERTIES CXX_EXTENSIONS OFF)
install(TARGETS _clap_cpp LIBRARY DESTINATION clap_cpp COMPONENT python
RUNTIME DESTINATION clap_cpp COMPONENT python)
install(FILES "${CLAP_ROOT}/ggml/LICENSE" DESTINATION clap_cpp/licenses RENAME ggml.txt COMPONENT python)
install(FILES "${CLAP_ROOT}/thirdparty/miniaudio/LICENSE" DESTINATION clap_cpp/licenses RENAME miniaudio.txt COMPONENT python)
install(FILES "${CLAP_ROOT}/thirdparty/icu/LICENSE" DESTINATION clap_cpp/licenses RENAME icu.txt COMPONENT python)
install(FILES "${CLAP_ROOT}/thirdparty/json/LICENSE.MIT" DESTINATION clap_cpp/licenses RENAME json.txt COMPONENT python)
install(TARGETS _clap_cpp LIBRARY DESTINATION ${CLAP_PYTHON_PACKAGE} COMPONENT python
RUNTIME DESTINATION ${CLAP_PYTHON_PACKAGE} COMPONENT python)
install(FILES "${CLAP_ROOT}/ggml/LICENSE" DESTINATION ${CLAP_PYTHON_PACKAGE}/licenses RENAME ggml.txt COMPONENT python)
install(FILES "${CLAP_ROOT}/thirdparty/miniaudio/LICENSE" DESTINATION ${CLAP_PYTHON_PACKAGE}/licenses RENAME miniaudio.txt COMPONENT python)
install(FILES "${CLAP_ROOT}/thirdparty/icu/LICENSE" DESTINATION ${CLAP_PYTHON_PACKAGE}/licenses RENAME icu.txt COMPONENT python)
install(FILES "${CLAP_ROOT}/thirdparty/json/LICENSE.MIT" DESTINATION ${CLAP_PYTHON_PACKAGE}/licenses RENAME json.txt COMPONENT python)
97 changes: 89 additions & 8 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,51 @@
# clap.cpp Python bindings (CPU)
# clap.cpp Python bindings

## Installation

```sh
pip install clap-cpp # CPU
pip install "clap-cpp[cuda]" # NVIDIA CUDA
pip install "clap-cpp[rocm]" # AMD ROCm
pip install "clap-cpp[vulkan]" # Vulkan
```

The extras install version-matched `clap-cpp-cuda`, `clap-cpp-rocm` or
`clap-cpp-vulkan` wheels. They do not change compiler flags during pip install.
The public import remains `from clap_cpp import Model`. With one GPU backend
installed it is selected automatically; otherwise the CPU extension is used.
Set `CLAP_CPP_BACKEND=cpu|cuda|rocm|vulkan` before importing to select explicitly.
If multiple GPU packages are installed, explicit selection is required.
`clap_cpp.backend` reports the selected compiled backend, not device availability.
A GPU extension that cannot load raises an error rather than silently loading
CPU; `CLAP_CPP_BACKEND=cpu` explicitly uses the bundled CPU extension.

Release wheels cover CPython 3.9–3.14 (not free-threaded builds):

| Backend | Platforms | Required system runtime |
| --- | --- | --- |
| CPU | Linux x86_64/ARM64 (glibc 2.35+), macOS ARM64, Windows x86_64 | None beyond platform runtimes |
| CUDA | Linux x86_64 (glibc 2.35+) | CUDA 12.9-compatible NVIDIA driver; cudart/cuBLAS are statically linked |
| ROCm | Linux x86_64 (glibc 2.35+) | ROCm 6.4.3 HIP, hipBLAS, rocBLAS (including library data), compatible AMD driver |
| Vulkan | Linux x86_64 (glibc 2.39+) | System Vulkan loader and vendor ICD/driver |

GPU wheels do not bundle drivers. ROCm libraries must be discoverable through
`ldconfig` or `LD_LIBRARY_PATH=/opt/rocm/lib:/opt/rocm/lib64`; keep rocBLAS's data
installed in the matching ROCm prefix. No Windows GPU or macOS GPU wheels are
built by this workflow. On other targets pip can build from a source archive,
but this requires the corresponding GPU SDK and compiler.

GPU acceleration currently uses the native runtime's cached dense matrix
multiplication path for CLAP. Architectures requiring storage-only precision
(MERT, MuQ, MuQ-MuLan and CLaMP3) and other operations retain their CPU paths.
Device initialization failures and unsupported operations retain the native
CPU fallback; installing an extra does not guarantee every operation runs on GPU.

The commands above require these projects to have been published to PyPI.
Before the first publication, download matching wheels from a GitHub Release
into a local directory and use, for example,
`pip install --find-links ./wheels "clap-cpp[cuda]"`.

## Build from source

Build and install from a recursive checkout:

Expand All @@ -9,11 +56,26 @@ python -m pip install ./python
```

Requires Python 3.9+, a C++17 compiler and CMake 3.20+. The build backend installs
CMake/Ninja if necessary. NumPy is the only runtime Python dependency; PyTorch,
FFmpeg, a separate `clap` executable and GPU drivers are not required.
The extension statically links the CPU runtime and bundled ICU. GPU backends and
OpenMP are disabled. x86 wheels use baseline instructions rather than the build
machine's instruction set. No pretrained weights are downloaded automatically.
CMake/Ninja if necessary. For the default CPU package NumPy is the only runtime
Python dependency; PyTorch, FFmpeg, a separate `clap` executable and GPU drivers
are not required. The extension statically links the CPU runtime and bundled
ICU. The base package disables GPU backends and OpenMP. x86 wheels use baseline
instructions rather than the build machine's instruction set. No pretrained
weights are downloaded automatically.

Build an optional backend project separately, with its GPU SDK installed:

```sh
python python/prepare_backend.py cuda ./build-python-cuda
python -m pip install ./build-python-cuda
python -m pip install ./python
```

Replace `cuda` with `rocm` or `vulkan` as needed. The helper also works inside
the unpacked base source distribution. Extra CMake options can be passed with
pip's `-Ccmake.define.NAME=VALUE`, for example CUDA/HIP architecture lists when
building on a machine without a GPU. GPU backend selection and wheel package
names must match: do not enable a GPU backend directly on the base project.

## Usage

Expand Down Expand Up @@ -81,5 +143,24 @@ python -m build python
The source distribution includes the pinned native sources and third-party
licenses; a wheel can be rebuilt from that archive without a Git checkout or
CMake-time downloads. Build dependencies must still be available from the Python
package index or a local wheelhouse. Wheel publishing and GPU support are out of
scope for this initial binding.
package index or a local wheelhouse.

## Releases

`.github/workflows/python-release.yml` runs when a GitHub Release is published.
It builds CPU, CUDA, ROCm and Vulkan distributions without running tests, using
sccache for C, C++, CUDA and HIP compilation. Cache storage reuses the repository's
`SCCACHE_BUCKET`, `SCCACHE_ENDPOINT`, `SCCACHE_REGION` variables and
`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` secrets. Linux wheels are repaired
with auditwheel; system GPU libraries remain external as listed above. All
wheels and source distributions are attached to that release.

Stable releases also publish the four distributions to PyPI using OIDC. Before
releasing, configure a PyPI trusted publisher (owner `gorse-io`, repository
`clap.cpp`, workflow `python-release.yml`, environment `pypi`) for **each** of
`clap-cpp`, `clap-cpp-cuda`, `clap-cpp-rocm` and `clap-cpp-vulkan`, and configure
any required GitHub `pypi` environment approvals. Prereleases only attach assets.
The version in `python/pyproject.toml` and its three exact extra dependencies
must be updated together before publishing a new version. The backend helper
copies that version into each backend distribution; existing PyPI versions
cannot be overwritten.
2 changes: 1 addition & 1 deletion python/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class Model {
} // namespace

PYBIND11_MODULE(_clap_cpp, module) {
module.doc() = "Native CPU inference for clap.cpp";
module.doc() = "Native inference for clap.cpp";
py::class_<Model>(module, "Model")
.def(py::init<const std::filesystem::path &, int>(), py::arg("path"), py::arg("threads") = 0)
.def_property_readonly("projection_dimension", [](Model &self) {
Expand Down
44 changes: 41 additions & 3 deletions python/clap_cpp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
"""CPU audio/text embeddings backed by the native clap.cpp runtime."""
"""Audio/text embeddings backed by the selected native clap.cpp runtime."""

from ._clap_cpp import Model, decode_audio, resample_audio
from importlib import import_module
from importlib.metadata import version
from importlib.util import find_spec
import os

__all__ = ["Model", "decode_audio", "resample_audio"]

def _load_backend():
requested = os.environ.get("CLAP_CPP_BACKEND", "auto").lower()
choices = ("cuda", "rocm", "vulkan")
if requested not in ("auto", "cpu", *choices):
raise ImportError("CLAP_CPP_BACKEND must be auto, cpu, cuda, rocm or vulkan")
if requested == "auto":
installed = [name for name in choices if find_spec(f"clap_cpp_{name}") is not None]
if len(installed) > 1:
raise ImportError(
"Multiple clap-cpp GPU backends are installed; select one with "
"CLAP_CPP_BACKEND=cpu|cuda|rocm|vulkan"
)
requested = installed[0] if installed else "cpu"
if requested == "cpu":
return requested, import_module("._clap_cpp", __name__)
package = f"clap_cpp_{requested}"
if find_spec(package) is None:
raise ImportError(f'Install the backend with pip install "clap-cpp[{requested}]"')
if version(f"clap-cpp-{requested}") != version("clap-cpp"):
raise ImportError(f"clap-cpp and clap-cpp-{requested} must have the same version")
try:
return requested, import_module(f"{package}._clap_cpp")
except (ImportError, OSError) as error:
raise ImportError(
f"Cannot load clap-cpp {requested} backend. Install its GPU runtime/driver "
"or set CLAP_CPP_BACKEND=cpu."
) from error


backend, _native = _load_backend()
Model = _native.Model
decode_audio = _native.decode_audio
resample_audio = _native.resample_audio

__all__ = ["Model", "decode_audio", "resample_audio", "backend"]
73 changes: 73 additions & 0 deletions python/prepare_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Create a standalone GPU wheel project from a checkout or the base sdist."""

import argparse
from pathlib import Path
import re
import shutil


def prepare(backend, destination):
source = Path(__file__).resolve().parent
metadata = (source / "pyproject.toml").read_text(encoding="utf-8")
match = re.search(r'^version = "([^"]+)"$', metadata, re.MULTILINE)
if match is None:
raise ValueError("Missing package version")
version = match.group(1)
package = f"clap_cpp_{backend}"
cmake_backend = "HIP" if backend == "rocm" else backend.upper()
# Reuse the base sdist's exact native-source manifest, including licenses.
manifest = metadata.split("[tool.scikit-build.sdist.force-include]", 1)[1].split("\n[", 1)[0]
entries = re.findall(r'^"([^"]+)" = "([^"]+)"$', manifest, re.MULTILINE)
if not entries:
raise ValueError("Missing native source manifest")
destination.mkdir(parents=True, exist_ok=False)
ignore = shutil.ignore_patterns(".git", "__pycache__", "build", "dist")
for original, target in entries:
# Inside a base sdist the manifest's ../ paths have already been moved.
origin = source / target if (source / "native").is_dir() else source / original
output = destination / target
output.parent.mkdir(parents=True, exist_ok=True)
if origin.is_dir():
shutil.copytree(origin, output, ignore=ignore)
else:
shutil.copy2(origin, output)
for name in ("CMakeLists.txt", "bindings.cpp", "README.md"):
shutil.copy2(source / name, destination / name)
(destination / package).mkdir()
(destination / package / "__init__.py").write_text(
f'"""Native {backend} runtime; import clap_cpp for the public API."""\n', encoding="utf-8"
)
(destination / "pyproject.toml").write_text(
f'''[build-system]
requires = ["scikit-build-core>=1.0", "pybind11>=2.13"]
build-backend = "scikit_build_core.build"

[project]
name = "clap-cpp-{backend}"
version = "{version}"
description = "{backend.upper()} backend for clap-cpp"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
dependencies = ["numpy>=1.23"]

[tool.scikit-build]
minimum-version = "build-system.requires"
wheel.packages = ["{package}"]
install.components = ["python"]
build.targets = ["_clap_cpp"]
sdist.exclude = ["**/.git", "**/__pycache__/**", "dist/**", "build/**"]

[tool.scikit-build.cmake.define]
CLAP_PYTHON_BACKEND = "{cmake_backend}"
''', encoding="utf-8"
)
print(f"Prepared clap-cpp-{backend} {version} in {destination}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("backend", choices=("cuda", "rocm", "vulkan"))
parser.add_argument("destination", type=Path)
args = parser.parse_args()
prepare(args.backend, args.destination.resolve())
5 changes: 4 additions & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ build-backend = "scikit_build_core.build"
[project]
name = "clap-cpp"
version = "0.1.0"
description = "CPU Python bindings for clap.cpp audio and text embeddings"
description = "Python bindings for clap.cpp audio and text embeddings"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
dependencies = ["numpy>=1.23"]

[project.optional-dependencies]
test = ["pytest>=7"]
cuda = ["clap-cpp-cuda==0.1.0"]
rocm = ["clap-cpp-rocm==0.1.0"]
vulkan = ["clap-cpp-vulkan==0.1.0"]

[tool.scikit-build]
minimum-version = "build-system.requires"
Expand Down
10 changes: 10 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ target_include_directories(clap SYSTEM PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/miniaudio")
target_link_libraries(clap PUBLIC ggml
PRIVATE ggml-cpu nlohmann_json::nlohmann_json ICU::uc ICU::i18n)
if(GGML_CUDA OR GGML_HIP OR GGML_VULKAN)
target_compile_definitions(clap PRIVATE CLAP_USE_GPU)
endif()
if(GGML_CUDA)
target_link_libraries(clap PRIVATE ggml-cuda)
target_compile_definitions(clap PRIVATE CLAP_USE_CUDA)
elseif(GGML_HIP)
target_link_libraries(clap PRIVATE ggml-hip)
target_compile_definitions(clap PRIVATE CLAP_USE_HIP)
endif()
if(GGML_VULKAN)
target_link_libraries(clap PRIVATE ggml-vulkan)
target_compile_definitions(clap PRIVATE CLAP_USE_VULKAN)
Expand Down
Loading
Loading