fix(void): report pywhispercpp 1.5.0 and load CPU variants at runtime (#49)
Fixes #43 Fixes #44
This commit was merged in pull request #49.
This commit is contained in:
+27
-10
@@ -88,19 +88,35 @@ for the sdist; `tests/test_void_pywhispercpp_template.py` fails otherwise.
|
||||
There is no CUDA build: Void ships no CUDA toolkit package. Rebuilding
|
||||
locally with a hand-installed toolkit (`GGML_CUDA=1`) is a documented,
|
||||
unsupported escape hatch.
|
||||
- **Portable CPU flags**: every ggml SIMD option is pinned off
|
||||
(`-DGGML_NATIVE=OFF -DGGML_SSE42=OFF ...`), the plain x86-64 baseline
|
||||
already used by the Flatpak (`#938`, `#957`). The install smoke scans the
|
||||
shipped `libggml-cpu` (`.GCC.command.line`) for `-march=native`,
|
||||
`-mcpu=native` and `-mtune=native` and fails on a match. The scan is a
|
||||
tripwire, not a proof: the proof is the pinned configuration.
|
||||
- **CPU variants (`#44`)**: `GGML_BACKEND_DL=ON` and
|
||||
`GGML_CPU_ALL_VARIANTS=ON` build one CPU library per instruction-set
|
||||
level (`libggml-cpu-x64.so` for the plain x86-64 baseline, then
|
||||
`haswell`, `skylakex`, `zen4`, ...). ggml loads the best one the host
|
||||
supports, so a host with AVX2 runs AVX2 code, and the baseline variant
|
||||
ships everywhere. `GGML_NATIVE` stays `OFF`: ggml refuses it with
|
||||
`GGML_BACKEND_DL`, and it keeps `-march=native` out of every variant
|
||||
(`#938`). `patches/load-ggml-backends-beside-module.patch` makes the
|
||||
extension load the variants from its own directory: ggml's default search
|
||||
covers the interpreter's directory and the working directory, and neither
|
||||
holds them, so without the patch no CPU backend is registered. The install
|
||||
smoke checks that `libggml-cpu-x64.so` holds no AVX (`ymm`/`zmm`) code,
|
||||
that `libggml-cpu-haswell.so` does, that no variant carries host-native
|
||||
tuning (`.GCC.command.line`), and that the CPU and Vulkan backends load
|
||||
from the installed package.
|
||||
- **Version (`#43`)**: `setup.py` passes no version to `setup()`. The
|
||||
version comes from setuptools-scm, which the sdist declares as a build
|
||||
requirement, so `hostmakedepends` provides `python3-setuptools_scm` and
|
||||
`SETUPTOOLS_SCM_PRETEND_VERSION` pins `1.5.0` without git metadata. Without
|
||||
setuptools-scm, setuptools writes `0.0.0`, which the app reads as an
|
||||
unknown binding version and then skips device selection. The install smoke
|
||||
asserts the installed package reports `1.5.0`.
|
||||
- **Shared libraries**: `BUILD_SHARED_LIBS=ON` with an `$ORIGIN` rpath
|
||||
(as `install.sh` does), so the extension resolves the whisper.cpp
|
||||
libraries installed beside it with no `LD_LIBRARY_PATH`.
|
||||
- **Offline build**: the sdist vendors whisper.cpp, pybind11, and the
|
||||
ggml sources; the build fetches nothing beyond the declared distfiles.
|
||||
`PYWHISPERCPP_VERSION` is injected because the sdist carries no
|
||||
`version.txt` (its version otherwise comes from git metadata).
|
||||
The sdist carries no git metadata, which is why the version is pinned
|
||||
through setuptools-scm (see Version above).
|
||||
|
||||
## voculinux
|
||||
|
||||
@@ -202,8 +218,9 @@ host Void install and nothing is published. Stages, in order, each fails the run
|
||||
`python3-pysocks` are installed, then `voculinux` installs and upgrades both
|
||||
to the carried versions (see [Version floors](#version-floors)),
|
||||
`voculinux --version` shows the version, the payload checks pass (desktop
|
||||
entry, icon, marker, no models, no pre-rename names, the Vulkan backend, no
|
||||
host-native CPU tuning), and the imports load (`pynput` through its dummy
|
||||
entry, icon, marker, no models, no pre-rename names, the Vulkan backend, the
|
||||
CPU variants with an AVX-free x86-64 baseline, no host-native CPU tuning),
|
||||
the installed pywhispercpp reports `1.5.0`, and the imports load (`pynput` through its dummy
|
||||
backend, `evdev`, `gi` with Gtk 3, `pywhispercpp.model`, `pyaudio`, `socks`,
|
||||
and the app's own modules, which keep their retained internal name).
|
||||
|
||||
|
||||
@@ -106,19 +106,46 @@ payload_voculinux() {
|
||||
echo "payload ok: desktop entry, icon and marker present; no models, wheels or pre-rename names"
|
||||
}
|
||||
|
||||
# whisper.cpp must ship its Vulkan backend and no host-native CPU tuning.
|
||||
# whisper.cpp ships its Vulkan backend and its CPU backend as runtime-loaded
|
||||
# variants beside the extension (GGML_BACKEND_DL, #44). Every CPU variant
|
||||
# carries its own instruction set, but the x86-64 baseline variant must hold no
|
||||
# AVX code (ymm/zmm), the AVX2 variant must hold some (so the variants are real,
|
||||
# not one baseline build), and every other shipped native library (the ggml
|
||||
# base, the registry, whisper, the extension, Vulkan) must stay AVX-free. No
|
||||
# library may carry host-native tuning.
|
||||
payload_pywhispercpp() {
|
||||
PKGFILE=$1
|
||||
tar -tf "$PKGFILE" | grep -q "libggml-vulkan" || fail "the Vulkan backend is missing from $PKGFILE"
|
||||
LIBMEMBER=$(tar -tf "$PKGFILE" | grep -E "libggml-cpu\.so" | head -n1)
|
||||
[ -n "$LIBMEMBER" ] || fail "libggml-cpu is missing from $PKGFILE"
|
||||
mkdir -p /flagscan
|
||||
tar -xf "$PKGFILE" -C /flagscan "$LIBMEMBER"
|
||||
LIBSCAN="/flagscan/${LIBMEMBER#./}"
|
||||
if readelf -p .GCC.command.line "$LIBSCAN" 2>/dev/null | grep -E "march=native|mcpu=native|mtune=native"; then
|
||||
fail "host-native CPU tuning in the shipped libggml-cpu"
|
||||
for VARIANT in x64 haswell; do
|
||||
tar -tf "$PKGFILE" | grep -q "libggml-cpu-$VARIANT\.so" ||
|
||||
fail "CPU variant libggml-cpu-$VARIANT.so is missing from $PKGFILE"
|
||||
done
|
||||
rm -rf /flagscan && mkdir -p /flagscan
|
||||
tar -xf "$PKGFILE" -C /flagscan --wildcards '*.so*'
|
||||
while IFS= read -r LIBSCAN; do
|
||||
NAME=${LIBSCAN##*/}
|
||||
if readelf -p .GCC.command.line "$LIBSCAN" 2>/dev/null | grep -E "march=native|mcpu=native|mtune=native"; then
|
||||
fail "host-native CPU tuning in $NAME"
|
||||
fi
|
||||
case "$NAME" in
|
||||
libggml-cpu-*) ;; # each variant is checked below
|
||||
*)
|
||||
if objdump -d "$LIBSCAN" | grep -Eq '%[yz]mm[0-9]'; then
|
||||
fail "$NAME contains AVX code; only the CPU variants may"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done <<LIBS
|
||||
$(find /flagscan -type f \( -name '*.so' -o -name '*.so.*' \))
|
||||
LIBS
|
||||
BASELINE=$(find /flagscan -type f -name 'libggml-cpu-x64.so')
|
||||
AVX2=$(find /flagscan -type f -name 'libggml-cpu-haswell.so')
|
||||
[ -n "$BASELINE" ] && [ -n "$AVX2" ] || fail "the CPU variants were not extracted from $PKGFILE"
|
||||
if objdump -d "$BASELINE" | grep -Eq '%[yz]mm[0-9]'; then
|
||||
fail "the x86-64 baseline CPU variant contains AVX code; it would not run on every x86-64 CPU"
|
||||
fi
|
||||
echo "payload ok: Vulkan backend present; no host-native tuning in libggml-cpu"
|
||||
objdump -d "$AVX2" | grep -Eq '%ymm[0-9]' || fail "the AVX2 CPU variant has no AVX2 code"
|
||||
echo "payload ok: Vulkan backend; CPU variants present; baseline and other libraries free of AVX; no host-native tuning"
|
||||
}
|
||||
|
||||
phase_install() {
|
||||
@@ -179,6 +206,46 @@ phase_install() {
|
||||
python3 -c "import evdev; print('evdev ok')"
|
||||
python3 -c "import gi; gi.require_version('Gtk', '3.0'); from gi.repository import Gtk; print('gi ok: Gtk', Gtk.get_major_version(), Gtk.get_minor_version())"
|
||||
python3 -c "from pywhispercpp.model import Model; print('pywhispercpp.model ok')"
|
||||
echo "== pywhispercpp reports the version it is pinned to (#43)"
|
||||
PWC_VERSION=$(python3 -c "import importlib.metadata as m; print(m.version('pywhispercpp'))") ||
|
||||
fail "pywhispercpp has no installed version"
|
||||
[ "$PWC_VERSION" = "1.5.0" ] || fail "pywhispercpp reports $PWC_VERSION, not 1.5.0 (#43)"
|
||||
echo "pywhispercpp version: $PWC_VERSION"
|
||||
echo "== the extension loads its CPU and Vulkan backends from the package (#44)"
|
||||
python3 - <<'PY' || fail "the dynamic ggml backends did not load from the package"
|
||||
import ctypes
|
||||
import os
|
||||
|
||||
import _pywhispercpp
|
||||
import pywhispercpp.model # noqa: F401 (importing the extension loads the backends)
|
||||
|
||||
libdir = os.path.dirname(_pywhispercpp.__file__)
|
||||
base = ctypes.CDLL(os.path.join(libdir, "libggml.so.0"))
|
||||
base.ggml_backend_reg_count.restype = ctypes.c_size_t
|
||||
base.ggml_backend_reg_get.argtypes = [ctypes.c_size_t]
|
||||
base.ggml_backend_reg_get.restype = ctypes.c_void_p
|
||||
base.ggml_backend_reg_name.argtypes = [ctypes.c_void_p]
|
||||
base.ggml_backend_reg_name.restype = ctypes.c_char_p
|
||||
names = [
|
||||
base.ggml_backend_reg_name(base.ggml_backend_reg_get(i)).decode()
|
||||
for i in range(base.ggml_backend_reg_count())
|
||||
]
|
||||
print("ggml backends registered:", ", ".join(names))
|
||||
assert "CPU" in names, names
|
||||
loaded = set()
|
||||
for line in open("/proc/self/maps"):
|
||||
parts = line.split()
|
||||
if len(parts) >= 6 and "libggml-" in parts[-1]:
|
||||
loaded.add(os.path.basename(parts[-1]))
|
||||
cpu = sorted(name for name in loaded if name.startswith("libggml-cpu-"))
|
||||
assert cpu, "no CPU variant was loaded"
|
||||
print("CPU variant loaded:", ", ".join(cpu))
|
||||
# The Vulkan module registers only when a GPU driver answers, and this image
|
||||
# has none, so prove what it needs instead: its NEEDED libraries (libvulkan,
|
||||
# the ggml base) resolve from the installed layout.
|
||||
ctypes.CDLL(os.path.join(libdir, "libggml-vulkan.so"))
|
||||
print("Vulkan backend library loads from the package")
|
||||
PY
|
||||
python3 -c "import pyaudio; print('pyaudio ok')"
|
||||
python3 -c "import socks; print('pysocks ok')"
|
||||
python3 -c "import vocalinux.main, vocalinux.speech_recognition.recognition_manager; print('vocalinux imports ok')"
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
--- a/src/main.cpp
|
||||
+++ b/src/main.cpp
|
||||
@@ -16,6 +16,10 @@
|
||||
#include <pybind11/numpy.h>
|
||||
|
||||
#include "whisper.h"
|
||||
+#include "ggml-backend.h"
|
||||
+
|
||||
+#include <dlfcn.h>
|
||||
+#include <string>
|
||||
|
||||
|
||||
#define STRINGIFY(x) #x
|
||||
@@ -850,7 +854,25 @@
|
||||
|
||||
////////////
|
||||
|
||||
+// With GGML_BACKEND_DL the CPU and GPU backends are libraries next to this
|
||||
+// extension, and whisper.cpp cannot create a context until a CPU device is
|
||||
+// registered. ggml searches the executable's directory (the interpreter's, for
|
||||
+// Python) and the working directory, so load them from this module's directory.
|
||||
+static void load_ggml_backends_beside_module() {
|
||||
+ Dl_info info;
|
||||
+ if (dladdr(reinterpret_cast<void *>(&load_ggml_backends_beside_module), &info) == 0 ||
|
||||
+ info.dli_fname == nullptr) {
|
||||
+ ggml_backend_load_all();
|
||||
+ return;
|
||||
+ }
|
||||
+ const std::string path(info.dli_fname);
|
||||
+ const std::size_t slash = path.find_last_of('/');
|
||||
+ const std::string dir = slash == std::string::npos ? std::string(".") : path.substr(0, slash);
|
||||
+ ggml_backend_load_all_from_path(dir.c_str());
|
||||
+}
|
||||
+
|
||||
PYBIND11_MODULE(_pywhispercpp, m) {
|
||||
+ load_ggml_backends_beside_module();
|
||||
m.doc() = R"pbdoc(
|
||||
Pywhispercpp: Python binding to whisper.cpp
|
||||
-----------------------
|
||||
@@ -3,7 +3,7 @@ pkgname=python3-pywhispercpp
|
||||
version=1.5.0
|
||||
revision=1
|
||||
build_style=python3-module
|
||||
hostmakedepends="cmake pkg-config python3-setuptools python3-wheel $(vopt_if vulkan shaderc)"
|
||||
hostmakedepends="cmake pkg-config python3-setuptools python3-setuptools_scm python3-wheel $(vopt_if vulkan shaderc)"
|
||||
makedepends="python3-devel $(vopt_if vulkan vulkan-loader-devel)"
|
||||
depends="python3-numpy python3-requests python3-tqdm python3-platformdirs $(vopt_if vulkan vulkan-loader)"
|
||||
short_desc="Python bindings for whisper.cpp speech recognition"
|
||||
@@ -20,21 +20,23 @@ build_options="vulkan"
|
||||
build_options_default="vulkan"
|
||||
|
||||
pre_build() {
|
||||
# The sdist derives its version from git metadata, which is absent in
|
||||
# the xbps build root, so inject the pinned version explicitly. This is
|
||||
# the same version install.sh (PYWHISPERCPP_VERSION) and uv.lock pin.
|
||||
export PYWHISPERCPP_VERSION=${version}
|
||||
# Portable x86-64 baseline: GGML_NATIVE is ggml's default and compiles
|
||||
# libggml-cpu with -march=native, so the package would inherit the
|
||||
# builder's instruction set and SIGILL on older CPUs (#938).
|
||||
# GGML_NATIVE=OFF alone is not enough: while the native default is on,
|
||||
# ggml turns every GGML_* SIMD option on and still compiles -mavx2
|
||||
# -mfma (#957). Each option is pinned off, mirroring the Flatpak build.
|
||||
# Shared libraries with an $ORIGIN rpath let the extension resolve the
|
||||
# whisper.cpp libraries installed beside it (as install.sh does), with
|
||||
# no LD_LIBRARY_PATH at runtime. (setup.py install never builds a
|
||||
# wheel, so NO_REPAIR is meaningless here and deliberately unset.)
|
||||
export CMAKE_ARGS='-DCMAKE_INSTALL_RPATH=$ORIGIN -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DBUILD_SHARED_LIBS=ON -DGGML_NATIVE=OFF -DGGML_SSE42=OFF -DGGML_AVX=OFF -DGGML_AVX2=OFF -DGGML_BMI2=OFF -DGGML_FMA=OFF -DGGML_F16C=OFF'
|
||||
# Version (#43). setup.py passes no version to setup(); the version comes
|
||||
# from setuptools-scm, which the sdist declares as a build requirement and
|
||||
# which hostmakedepends provides. The xbps build root has no git metadata,
|
||||
# so pin the version through setuptools-scm. Without it setuptools falls
|
||||
# back to 0.0.0. The sdist's PYWHISPERCPP_VERSION hook is never called.
|
||||
export SETUPTOOLS_SCM_PRETEND_VERSION=${version}
|
||||
# CPU (#44). GGML_BACKEND_DL builds each CPU variant (x64 baseline,
|
||||
# sandybridge, haswell, icelake, zen4, ...) as its own library. The
|
||||
# extension, through patches/, loads the variants that sit beside it, and
|
||||
# ggml picks the best one the host CPU supports. The x64 baseline always
|
||||
# ships, so the package still runs on any x86-64 CPU, and hosts with AVX2
|
||||
# use it. GGML_NATIVE must stay OFF: ggml refuses it with GGML_BACKEND_DL,
|
||||
# and it keeps -march=native out of every variant (#938).
|
||||
# Shared libraries with an $ORIGIN rpath let each variant resolve
|
||||
# libggml-base beside it, with no LD_LIBRARY_PATH at runtime. (setup.py
|
||||
# install never builds a wheel, so NO_REPAIR is meaningless here.)
|
||||
export CMAKE_ARGS='-DCMAKE_INSTALL_RPATH=$ORIGIN -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DBUILD_SHARED_LIBS=ON -DGGML_NATIVE=OFF -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON'
|
||||
if [ -n "$build_option_vulkan" ]; then
|
||||
# Vulkan acceleration for every GPU vendor. There is no CUDA
|
||||
# build: Void ships no CUDA toolkit package.
|
||||
@@ -42,6 +44,20 @@ pre_build() {
|
||||
fi
|
||||
}
|
||||
|
||||
pre_install() {
|
||||
# The backend modules (libggml-cpu-*.so, libggml-vulkan.so) are written to
|
||||
# build/bin, a CMake directory whose whisper.cpp setup fixes, and setup.py
|
||||
# installs only the build/lib tree that holds the extension. Copy them
|
||||
# beside the extension, where patches/ loads them from.
|
||||
local backend found=""
|
||||
for backend in build/temp.*/_pywhispercpp/bin/libggml-*.so; do
|
||||
[ -f "$backend" ] || continue
|
||||
cp "$backend" build/lib.*/
|
||||
found=1
|
||||
done
|
||||
[ -n "$found" ] || msg_error "no ggml backend modules were built; the CPU variants are missing\n"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user