From d388f874de5e1e1cba3dd4932c6766bbe5895661 Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Fri, 26 Jun 2026 01:18:41 +0200 Subject: [PATCH] feat(backends): darwin/Metal build for the privacy-filter backend (#10513) * feat(backends): darwin/Metal build for the privacy-filter backend (timeboxed try) The privacy-filter.cpp engine is already Metal-capable on Apple Silicon: it pulls ggml and never forces GGML_METAL=OFF, and ggml defaults Metal ON on Apple, so a plain Darwin build is Metal-enabled. grpc++/protobuf resolve from Homebrew via find_package(... CONFIG). It just had no darwin build path - the existing package.sh and run.sh are Linux-only and there was no make target / workflow step. Adds the bespoke darwin path, modeled on the ds4 one: - scripts/build/privacy-filter-darwin.sh: native make grpc-server, otool -L dylib bundling, create-oci-image (no Linux package.sh). - Makefile: backends/privacy-filter-darwin target (+ .NOTPARALLEL). - .github/workflows/backend_build_darwin.yml: gated build step for privacy-filter. - scripts/changed-backends.js: inferBackendPathDarwin special-case -> backend/cpp. - .github/backend-matrix.yml: includeDarwin entry (lang go, like ds4/llama-cpp). - backend/index.yaml: metal: capability + metal-privacy-filter(-development) entries. - backend/cpp/privacy-filter/run.sh: DYLD_LIBRARY_PATH branch on Darwin. Signed-off-by: Ettore Di Giacinto Assisted-by: Claude:opus-4.8 [Claude Code] * fix(privacy-filter): macOS proto include + bundle ggml dylibs Validated natively on an M4 (the build/package/load chain now works with Metal): - CMakeLists.txt: hw_grpc_proto compiles the generated proto/grpc sources but only linked the binary dir, so on macOS it could not find protobuf's headers (runtime_version.h) - Homebrew puts them under /opt/homebrew, not /usr/include. Link protobuf::libprotobuf + gRPC::grpc++ so their include dirs propagate. No-op on Linux (apt headers are already on the default search path). - privacy-filter-darwin.sh: bundle the ggml shared libs the binary @rpath-links (libggml{,-base,-cpu,-blas,-metal}); the otool -L walk only catches on-disk absolute deps and missed them. Resolved at runtime by run.sh's DYLD_LIBRARY_PATH. M4 check: arm64 grpc-server links @rpath/libggml-metal.0.dylib; with the 15 ggml dylibs + grpc/protobuf bundled, it loads clean (no dyld errors) and prints usage. Signed-off-by: Ettore Di Giacinto Assisted-by: Claude:opus-4.8 [Claude Code] --------- Signed-off-by: Ettore Di Giacinto Co-authored-by: Ettore Di Giacinto --- .github/backend-matrix.yml | 6 ++ .github/workflows/backend_build_darwin.yml | 11 +++- Makefile | 6 +- backend/cpp/privacy-filter/CMakeLists.txt | 8 +++ backend/cpp/privacy-filter/run.sh | 8 ++- backend/index.yaml | 11 ++++ scripts/build/privacy-filter-darwin.sh | 66 ++++++++++++++++++++++ scripts/changed-backends.js | 5 ++ 8 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 scripts/build/privacy-filter-darwin.sh diff --git a/.github/backend-matrix.yml b/.github/backend-matrix.yml index b66f1bbf3..6147f1250 100644 --- a/.github/backend-matrix.yml +++ b/.github/backend-matrix.yml @@ -4963,6 +4963,12 @@ includeDarwin: tag-suffix: "-metal-darwin-arm64-sam3-cpp" build-type: "metal" lang: "go" + # privacy-filter (PII/NER) is a C++/ggml backend built by a bespoke darwin + # script (make backends/privacy-filter-darwin); ggml defaults Metal ON on Apple + # so the build is Metal-enabled. lang=go drives runner/toolchain selection only. + - backend: "privacy-filter" + tag-suffix: "-metal-darwin-arm64-privacy-filter" + lang: "go" # LocalVQE has no Metal path; on Apple Silicon it builds CPU-only (GGML_METAL # OFF) but is still a native arm64 image. Uses the darwin/metal build profile. - backend: "localvqe" diff --git a/.github/workflows/backend_build_darwin.yml b/.github/workflows/backend_build_darwin.yml index 749ffd4de..e0bbdb091 100644 --- a/.github/workflows/backend_build_darwin.yml +++ b/.github/workflows/backend_build_darwin.yml @@ -228,8 +228,17 @@ jobs: run: | make backends/ds4-darwin + # privacy-filter is a C++/ggml backend like ds4 - a single grpc-server with + # otool dylib bundling - so it gets its own bespoke darwin script rather than + # the generic build-darwin-go-backend path. + - name: Build privacy-filter backend (Darwin Metal) + if: inputs.backend == 'privacy-filter' + run: | + make protogen-go + make backends/privacy-filter-darwin + - name: Build ${{ inputs.backend }}-darwin - if: inputs.backend != 'llama-cpp' && inputs.backend != 'ds4' + if: inputs.backend != 'llama-cpp' && inputs.backend != 'ds4' && inputs.backend != 'privacy-filter' run: | make protogen-go BACKEND=${{ inputs.backend }} BUILD_TYPE=${{ inputs.build-type }} USE_PIP=${{ inputs.use-pip }} make build-darwin-${{ inputs.lang }}-backend diff --git a/Makefile b/Makefile index be0711b47..b3ab4fe57 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Disable parallel execution for backend builds -.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/turboquant backends/outetts backends/piper backends/stablediffusion-ggml backends/whisper backends/crispasr backends/parakeet-cpp backends/faster-whisper backends/silero-vad backends/local-store backends/huggingface backends/rfdetr backends/rfdetr-cpp backends/insightface backends/speaker-recognition backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/mlx-distributed backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/sglang backends/moonshine backends/pocket-tts backends/qwen-tts backends/faster-qwen3-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/acestep-cpp backends/fish-speech backends/voxtral backends/opus backends/trl backends/llama-cpp-quantization backends/kokoros backends/sam3-cpp backends/qwen3-tts-cpp backends/omnivoice-cpp backends/vibevoice-cpp backends/localvqe backends/tinygrad backends/sherpa-onnx backends/ds4 backends/ds4-darwin backends/liquid-audio backends/supertonic backends/depth-anything-cpp backends/privacy-filter +.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/turboquant backends/outetts backends/piper backends/stablediffusion-ggml backends/whisper backends/crispasr backends/parakeet-cpp backends/faster-whisper backends/silero-vad backends/local-store backends/huggingface backends/rfdetr backends/rfdetr-cpp backends/insightface backends/speaker-recognition backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/mlx-distributed backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/sglang backends/moonshine backends/pocket-tts backends/qwen-tts backends/faster-qwen3-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/acestep-cpp backends/fish-speech backends/voxtral backends/opus backends/trl backends/llama-cpp-quantization backends/kokoros backends/sam3-cpp backends/qwen3-tts-cpp backends/omnivoice-cpp backends/vibevoice-cpp backends/localvqe backends/tinygrad backends/sherpa-onnx backends/ds4 backends/ds4-darwin backends/liquid-audio backends/supertonic backends/depth-anything-cpp backends/privacy-filter backends/privacy-filter-darwin GOCMD=go GOTEST=$(GOCMD) test @@ -1129,6 +1129,10 @@ backends/ds4-darwin: build bash ./scripts/build/ds4-darwin.sh ./local-ai backends install "ocifile://$(abspath ./backend-images/ds4.tar)" +backends/privacy-filter-darwin: build + bash ./scripts/build/privacy-filter-darwin.sh + ./local-ai backends install "ocifile://$(abspath ./backend-images/privacy-filter.tar)" + build-darwin-python-backend: build bash ./scripts/build/python-darwin.sh diff --git a/backend/cpp/privacy-filter/CMakeLists.txt b/backend/cpp/privacy-filter/CMakeLists.txt index b8580d1d4..f9ee78328 100644 --- a/backend/cpp/privacy-filter/CMakeLists.txt +++ b/backend/cpp/privacy-filter/CMakeLists.txt @@ -51,6 +51,14 @@ add_library(hw_grpc_proto STATIC ${HW_GRPC_SRCS} ${HW_GRPC_HDRS} ${HW_PROTO_SRCS} ${HW_PROTO_HDRS}) target_include_directories(hw_grpc_proto PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) +# The generated proto/grpc sources include protobuf and grpc++ headers, so this +# library must see their include dirs. Linking the imported targets propagates +# them. On Linux the apt headers live in /usr/include (default search path) so +# this was a no-op; on macOS the Homebrew headers are under /opt/homebrew and +# would otherwise be missed (runtime_version.h not found). +target_link_libraries(hw_grpc_proto PUBLIC + protobuf::libprotobuf + gRPC::grpc++) # Build only the pf static lib (+ ggml) from the engine tree — no CLI/bench/tests. # PF_VULKAN is honored when passed on the cmake command line (it lands in the diff --git a/backend/cpp/privacy-filter/run.sh b/backend/cpp/privacy-filter/run.sh index 489b64580..0f4f22270 100755 --- a/backend/cpp/privacy-filter/run.sh +++ b/backend/cpp/privacy-filter/run.sh @@ -2,7 +2,13 @@ # Entry point for the privacy-filter backend image / BACKEND_BINARY mode. set -e CURDIR=$(dirname "$(realpath "$0")") -export LD_LIBRARY_PATH="$CURDIR/lib:$LD_LIBRARY_PATH" +# macOS has no bundled ld.so; the darwin package ships only dylibs under lib/, +# resolved via DYLD_LIBRARY_PATH (the ld.so branch below is skipped there). +if [ "$(uname)" = "Darwin" ]; then + export DYLD_LIBRARY_PATH="$CURDIR/lib:$DYLD_LIBRARY_PATH" +else + export LD_LIBRARY_PATH="$CURDIR/lib:$LD_LIBRARY_PATH" +fi if [ -f "$CURDIR/lib/ld.so" ]; then exec "$CURDIR/lib/ld.so" "$CURDIR/grpc-server" "$@" fi diff --git a/backend/index.yaml b/backend/index.yaml index a7399e20d..2841d0f79 100644 --- a/backend/index.yaml +++ b/backend/index.yaml @@ -1095,6 +1095,7 @@ amd: "vulkan-privacy-filter" intel: "vulkan-privacy-filter" vulkan: "vulkan-privacy-filter" + metal: "metal-privacy-filter" - &faster-whisper icon: https://avatars.githubusercontent.com/u/1520500?s=200&v=4 description: | @@ -2937,6 +2938,16 @@ uri: "quay.io/go-skynet/local-ai-backends:master-gpu-vulkan-privacy-filter" mirrors: - localai/localai-backends:master-gpu-vulkan-privacy-filter +- !!merge <<: *privacyfilter + name: "metal-privacy-filter" + uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-privacy-filter" + mirrors: + - localai/localai-backends:latest-metal-darwin-arm64-privacy-filter +- !!merge <<: *privacyfilter + name: "metal-privacy-filter-development" + uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-privacy-filter" + mirrors: + - localai/localai-backends:master-metal-darwin-arm64-privacy-filter - !!merge <<: *privacyfilter name: "cuda13-privacy-filter" uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-13-privacy-filter" diff --git a/scripts/build/privacy-filter-darwin.sh b/scripts/build/privacy-filter-darwin.sh new file mode 100644 index 000000000..aca901822 --- /dev/null +++ b/scripts/build/privacy-filter-darwin.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Darwin/Metal build for the privacy-filter backend. Mirrors ds4-darwin.sh: +# native make of the single grpc-server, otool -L dylib bundling, then assemble +# an OCI tar that `local-ai backends install` can consume. +# +# privacy-filter.cpp pulls ggml, which defaults GGML_METAL=ON on Apple - the +# engine's CMake never forces it off, so a plain Darwin build is Metal-enabled. +# grpc++/protobuf are resolved from Homebrew via find_package(... CONFIG). +set -ex + +IMAGE_NAME="${IMAGE_NAME:-localai/privacy-filter-darwin}" + +pushd backend/cpp/privacy-filter +make grpc-server +popd + +mkdir -p build/darwin +mkdir -p build/darwin/lib +mkdir -p backend-images + +cp -rf backend/cpp/privacy-filter/grpc-server build/darwin/ +cp -rf backend/cpp/privacy-filter/run.sh build/darwin/ + +# Apple Silicon: pick up Homebrew-installed protobuf utf8_validity if present +# (same as ds4-darwin.sh - it is a transitive dep otool may not surface). +if [[ "$(uname -s)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then + ADDITIONAL_LIBS=${ADDITIONAL_LIBS:-$(ls /opt/homebrew/Cellar/protobuf/**/lib/libutf8_validity*.dylib 2>/dev/null)} +else + ADDITIONAL_LIBS=${ADDITIONAL_LIBS:-""} +fi +for file in $ADDITIONAL_LIBS; do + cp -rfv "$file" build/darwin/lib +done + +# Bundle the ggml shared libs the binary @rpath-links (libggml, -cpu, -blas, +# -metal). The engine builds ggml shared, scattered under the build tree; flatten +# them (with their version symlinks) into lib/, resolved at runtime by leaf name +# via run.sh's DYLD_LIBRARY_PATH=lib. Without this the packaged binary can't find +# libggml*.dylib once the build dir is gone. +GGML_SRC="backend/cpp/privacy-filter/build/privacy-filter.cpp/ggml/src" +find "$GGML_SRC" -name 'libggml*.dylib' -exec cp -a {} build/darwin/lib/ \; + +# Walk dylibs via otool -L and bundle anything that isn't a system framework. +for file in build/darwin/grpc-server; do + LIBS="$(otool -L "$file" | awk 'NR > 1 { system("echo " $1) } ' | xargs echo)" + for lib in $LIBS; do + if [[ "$lib" == *.dylib ]] && [[ -e "$lib" ]]; then + cp -rvf "$lib" build/darwin/lib + fi + done +done + +echo "Bundled libraries:" +ls -la build/darwin/lib + +# Build an OCI image tar (with manifest.json) that `local-ai backends install` +# can consume - mirrors ds4-darwin.sh. +PLATFORMARCH="${PLATFORMARCH:-darwin/arm64}" + +./local-ai util create-oci-image \ + build/darwin/. \ + --output ./backend-images/privacy-filter.tar \ + --image-name "$IMAGE_NAME" \ + --platform "$PLATFORMARCH" + +rm -rf build/darwin diff --git a/scripts/changed-backends.js b/scripts/changed-backends.js index 5690e00f5..758cdb869 100644 --- a/scripts/changed-backends.js +++ b/scripts/changed-backends.js @@ -71,6 +71,11 @@ function inferBackendPathDarwin(item) { if (item.backend === "ds4") { return `backend/cpp/ds4/`; } + // privacy-filter is C++ too (built via `make backends/privacy-filter-darwin`); + // same lang=go-for-runner convention, source under backend/cpp. + if (item.backend === "privacy-filter") { + return `backend/cpp/privacy-filter/`; + } if (!item.lang) { return `backend/python/${item.backend}/`; }