From c67baa2a90237b77bd6586fbde0fb89b8574d968 Mon Sep 17 00:00:00 2001 From: abetlen Date: Sat, 20 Jun 2026 13:46:18 -0700 Subject: [PATCH] ci: add Pyodide wheel builds --- .github/workflows/wheels.yaml | 33 ++++++++++++++++++++++++++- CHANGELOG.md | 1 + CMakeLists.txt | 27 ++++++++++++++++++++++ README.md | 13 +++++++++++ ggml/ggml.py | 43 +++++++++++++++++++++++++++-------- 5 files changed, 106 insertions(+), 11 deletions(-) diff --git a/.github/workflows/wheels.yaml b/.github/workflows/wheels.yaml index c3adbd0..9ebf0e6 100644 --- a/.github/workflows/wheels.yaml +++ b/.github/workflows/wheels.yaml @@ -91,9 +91,40 @@ jobs: name: wheels_arm64 path: ./wheelhouse/*.whl + build_wheels_pyodide: + name: Build Pyodide wheel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: "recursive" + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Build wheel + uses: pypa/cibuildwheel@v4.1.0 + env: + CIBW_PLATFORM: "pyodide" + CIBW_BUILD: "cp314-pyodide_wasm32" + CIBW_BUILD_VERBOSITY: "1" + CIBW_REPAIR_WHEEL_COMMAND: "" + CIBW_TEST_COMMAND: > + python -c "import ggml; params = ggml.ggml_init_params(mem_size=16 * 1024 * 1024, mem_buffer=None); ctx = ggml.ggml_init(params); assert ctx is not None; x = ggml.ggml_new_tensor_1d(ctx, ggml.GGML_TYPE_F32, 1); a = ggml.ggml_new_tensor_1d(ctx, ggml.GGML_TYPE_F32, 1); b = ggml.ggml_new_tensor_1d(ctx, ggml.GGML_TYPE_F32, 1); x2 = ggml.ggml_mul(ctx, x, x); f = ggml.ggml_add(ctx, ggml.ggml_mul(ctx, a, x2), b); gf = ggml.ggml_new_graph(ctx); ggml.ggml_build_forward_expand(gf, f); ggml.ggml_set_f32(x, 2.0); ggml.ggml_set_f32(a, 3.0); ggml.ggml_set_f32(b, 4.0); ggml.ggml_graph_compute_with_ctx(ctx, gf, 1); assert ggml.ggml_get_f32_1d(f, 0) == 16.0; ggml.ggml_free(ctx); print('ggml pyodide ok', ggml.ggml_version().decode())" + CMAKE_ARGS: "-DEMSCRIPTEN_SYSTEM_PROCESSOR=wasm32 -DGGML_NATIVE=OFF -DGGML_OPENMP=OFF -DGGML_METAL=OFF -DGGML_BLAS=OFF -DGGML_CUDA=OFF -DGGML_HIP=OFF -DGGML_VULKAN=OFF -DGGML_OPENCL=OFF -DGGML_RPC=OFF -DGGML_SYCL=OFF -DGGML_CANN=OFF -DGGML_WEBGPU=OFF" + with: + output-dir: wheelhouse + + - name: Upload wheels as artifacts + uses: actions/upload-artifact@v7 + with: + name: wheels_pyodide + path: ./wheelhouse/*.whl + release: name: Release - needs: [build_wheels, build_wheels_arm64] + needs: [build_wheels, build_wheels_arm64, build_wheels_pyodide] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index f61342d..ffaa874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- ci: add Pyodide wheel builds by @abetlen in #171 - fix: bind ggml abort and backend guid API by @aisk in #169 - fix: sync updated gguf and backend binding signatures by @aisk in #170 diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e43ca3..6f000b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,27 @@ endif() set(BUILD_SHARED_LIBS "On") set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +if(EMSCRIPTEN) + if(DEFINED EMSCRIPTEN_SYSTEM_PROCESSOR) + set(CMAKE_SYSTEM_PROCESSOR ${EMSCRIPTEN_SYSTEM_PROCESSOR} CACHE STRING "Target processor" FORCE) + else() + set(CMAKE_SYSTEM_PROCESSOR wasm32 CACHE STRING "Target processor" FORCE) + endif() + + set(GGML_NATIVE "Off" CACHE BOOL "ggml: enable -march=native" FORCE) + set(GGML_OPENMP "Off" CACHE BOOL "ggml: use OpenMP" FORCE) + set(GGML_METAL "Off" CACHE BOOL "ggml: use Metal" FORCE) + set(GGML_BLAS "Off" CACHE BOOL "ggml: use BLAS" FORCE) + set(GGML_CUDA "Off" CACHE BOOL "ggml: use CUDA" FORCE) + set(GGML_HIP "Off" CACHE BOOL "ggml: use HIP" FORCE) + set(GGML_VULKAN "Off" CACHE BOOL "ggml: use Vulkan" FORCE) + set(GGML_OPENCL "Off" CACHE BOOL "ggml: use OpenCL" FORCE) + set(GGML_RPC "Off" CACHE BOOL "ggml: use RPC" FORCE) + + set(CMAKE_INSTALL_BINDIR ggml/lib CACHE PATH "Install binaries" FORCE) + set(CMAKE_INSTALL_INCLUDEDIR ggml/include CACHE PATH "Install headers" FORCE) + set(CMAKE_INSTALL_LIBDIR ggml/lib CACHE PATH "Install libraries" FORCE) +endif() if(APPLE) set(CMAKE_INSTALL_RPATH "@loader_path") else() @@ -64,6 +85,12 @@ set(GGML_PYTHON_TARGETS foreach(GGML_PYTHON_TARGET IN LISTS GGML_PYTHON_TARGETS) if(TARGET ${GGML_PYTHON_TARGET}) + if(EMSCRIPTEN) + set_target_properties(${GGML_PYTHON_TARGET} PROPERTIES + OUTPUT_NAME "${GGML_PYTHON_TARGET}.cpython-00-wasm32-emscripten" + ) + endif() + if(UNIX AND NOT APPLE) set_target_properties(${GGML_PYTHON_TARGET} PROPERTIES NO_SONAME TRUE diff --git a/README.md b/README.md index 8be1bac..67f4b23 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,19 @@ pip install ggml-python \ --extra-index-url https://abetlen.github.io/ggml-python/whl/hip-radeon ``` +Pre-built Pyodide wheels are available for browser runtimes: + +```python +import micropip + +await micropip.install(["numpy", "typing_extensions"]) +await micropip.install( + "ggml-python", + deps=False, + index_urls=["https://abetlen.github.io/ggml-python/whl/cpu"], +) +``` + When installing from source, pip compiles ggml with CMake and requires a C compiler installed on your system. To build ggml with specific features (ie. OpenBLAS, GPU Support, etc) you can pass specific cmake options through the `cmake.args` pip install configuration setting. For example to install ggml-python with cuBLAS support you can run: diff --git a/ggml/ggml.py b/ggml/ggml.py index 4ff3284..3ff0fc1 100644 --- a/ggml/ggml.py +++ b/ggml/ggml.py @@ -80,17 +80,25 @@ from typing_extensions import TypeAlias +_EMSCRIPTEN_SIDE_MODULE_SUFFIX = ".cpython-00-wasm32-emscripten.so" + + # Load the library def load_shared_library(module_name: str, lib_base_name: str): # Construct the paths to the possible shared library names base_path = pathlib.Path(__file__).parent.resolve() / "lib" # Searching for the library in the current directory under the name "libggml" (default name # for ggml) and "ggml" (default name for this repo) - lib_names: List[str] = [ - f"lib{lib_base_name}.so", - f"lib{lib_base_name}.dylib", - f"{lib_base_name}.dll", - ] + if sys.platform == "emscripten": + lib_names: List[str] = [ + f"lib{lib_base_name}{_EMSCRIPTEN_SIDE_MODULE_SUFFIX}", + ] + else: + lib_names = [ + f"lib{lib_base_name}.so", + f"lib{lib_base_name}.dylib", + f"{lib_base_name}.dll", + ] path: Optional[pathlib.Path] = None @@ -115,15 +123,30 @@ def load_shared_library(module_name: str, lib_base_name: str): os.environ["PATH"] = str(base_path) + os.pathsep + os.environ["PATH"] os.add_dll_directory(str(base_path)) cdll_args["winmode"] = 0 + elif sys.platform == "emscripten": + cdll_args["mode"] = ctypes.RTLD_GLOBAL + lib_dir = str(base_path) + ld_library_path = os.environ.get("LD_LIBRARY_PATH", "") + if lib_dir not in ld_library_path.split(os.pathsep): + os.environ["LD_LIBRARY_PATH"] = ( + lib_dir + if not ld_library_path + else f"{lib_dir}{os.pathsep}{ld_library_path}" + ) preloaded_libraries: List[ctypes.CDLL] = [] def preload_local_library(dep_base_name: str): - dep_names = [ - f"lib{dep_base_name}.so", - f"lib{dep_base_name}.dylib", - f"{dep_base_name}.dll", - ] + if sys.platform == "emscripten": + dep_names = [ + f"lib{dep_base_name}{_EMSCRIPTEN_SIDE_MODULE_SUFFIX}", + ] + else: + dep_names = [ + f"lib{dep_base_name}.so", + f"lib{dep_base_name}.dylib", + f"{dep_base_name}.dll", + ] cdll_dep_args = dict(cdll_args) if hasattr(ctypes, "RTLD_GLOBAL") and sys.platform != "win32": cdll_dep_args["mode"] = ctypes.RTLD_GLOBAL