Files
LocalAI/scripts/build/package-gpu-libs-intel_test.sh
Dimitris Karakasilis c089caf320 feat(sycl): make the intel llama.cpp backend self-contained on any host (#10991)
* feat(sycl): make the intel llama.cpp backend self-contained on any host

The SYCL backend shipped an incomplete oneAPI runtime AND relied on a
host-provided GPU driver, so it only ran inside the build container. On a
bare host it died with "libze_loader.so.1 / libdnnl.so.3: cannot open
shared object file", and even with the host's Intel driver installed it
SIGSEGV'd during SYCL init when the host driver was built against a newer
glibc than the backend's bundled loader (rolling-release distros).

package_intel_libs now bundles the complete, coherent oneAPI runtime
(the missing MKL ILP64 / sycl_blas / tbb_thread + oneDNN + the dlopen'd
UR adapters, plus a sweep of the backend binaries' own direct deps) and
the Intel GPU userspace driver (libze_intel_gpu + libigdrcl + IGC + gmm)
with its OpenCL ICD manifest, mirroring how package_vulkan_libs bundles
Mesa. run.sh points the Level Zero and OpenCL loaders at the bundled
driver, and install-base-deps.sh installs it in the SYCL build image.
Bundling the driver is safe across kernels because it talks to the host
i915/xe via the stable DRM UAPI (unlike NVIDIA's kernel-locked
userspace).

Validated on Arch (glibc 2.43, i915): the backend loads and runs on an
Iris Xe with no host Intel packages installed.

Assisted-by: Claude:claude-opus-4-8

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* fix(sycl): install a driver that exists, and let the user choose their own

The driver install added earlier in this branch asked apt for
intel-level-zero-gpu, which is not a package in Ubuntu 24.04. apt fails
outright on an unknown name, so neither driver was installed, nothing was there
to copy, and the images carried no driver at all.

It now comes from Intel's own repository, which has 25.18 for this Ubuntu
release, against 23.43 from late 2023 in the Ubuntu archive. The archive driver
does not know any card released since, so a machine with a recent Intel GPU
would end up carrying a driver that cannot drive it. Anything that goes wrong
during that install fails the build on purpose: an unreachable repository is a
passing problem that a retry fixes, while quietly carrying a different driver,
or none, is a difference nobody would notice until a user reports an idle GPU.

run.sh used to overwrite whatever driver the user had chosen. Level Zero uses
only the driver it is given, so on a machine with a card too new for the
carried driver, the GPU would go unused with no way back. Both that setting and
the OpenCL one are now left alone when already set, and the docs say how to
point a backend at the machine's own driver.

The OpenCL setting also used to be applied whenever the backend held a driver
list, even when the driver it named had not been copied, which leaves OpenCL
with nothing instead of falling back to the machine's own driver. It now
requires the copied driver to be present, and the packaging leaves out the list
entry of any driver it did not copy. The oneAPI images list a processor-only
OpenCL library, which was being carried with nothing behind it.

Two more corrections in the packaging. The scan for libraries a program is
linked against only looked at files named llama-cpp-*, so turboquant and bonsai,
which are also built for Intel GPUs, were left with the incomplete set of
libraries this branch set out to fix; it now looks at every program in the
directory. And a build that should carry a driver but ends up without one now
says so, which is what a stale prebuilt base image looks like: such a backend
still runs on a machine that has its own driver, so nothing fails and the only
other symptom is a user reporting an idle GPU.

Backends now also ask the driver to report how much graphics memory is free,
without which llama.cpp reads zero on an integrated GPU, since such a chip
shares the system memory instead of having its own. turboquant and bonsai get
the same run.sh handling as llama.cpp.

The driver is only carried by the builds that start through run.sh, because
run.sh is what points Level Zero and OpenCL at it. The Python backends for
Intel GPUs start differently and would never load it, so they keep using the
machine's own driver rather than carrying several hundred megabytes they cannot
use.

Checked in a container on Ubuntu 24.04: the install brings driver 25.18 with
the files where the packaging expects them, an unreachable repository fails the
build, and the copied set resolves on its own once the machine's Intel packages
are moved away.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* fix(ci): rebuild every Linux backend when the GPU packaging script changes

scripts/build/package-gpu-libs.sh decides which GPU libraries end up inside an
image. The filter that builds the backend matrix listed it as an input of the
Python images only, so changing it rebuilt no Go and no C++ backend, even
though those run it from their own package.sh. A packaging fix aimed at the
Intel llama.cpp backend could merge and reach no image, which is the same
failure this rule was written to prevent.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

* fix(sycl): carry only the driver Level Zero uses, not the OpenCL one

llama.cpp reaches an Intel GPU through Level Zero, which hands the driver
programs that are already compiled and so needs only the back end of the
graphics compiler. The OpenCL driver can be handed source code instead, so it
needs the compiler's front end as well, and that arrives with its own copy of
clang. Carrying it cost about 139 MB in every backend built for Intel GPUs, and
took the carried set from 123 MB to 261 MB.

Nothing here takes that path. No LocalAI code selects an OpenCL device, each
backend image holds one backend, and the documentation never described OpenCL
as a way to run models: the only mentions are a stale clblas row in the
BUILD_TYPE table, for a llama.cpp backend that no longer exists and that no
build matrix entry uses, and the sycl-ls troubleshooting hint. Before this
branch the packaging carried the OpenCL loader and adapter but no driver, so
the path could not work in a released image either. There is nobody to keep
working.

The driver list that OpenCL reads is no longer carried, and run.sh no longer
sets OCL_ICD_VENDORS, so OpenCL inside a container keeps using whatever the
image provides rather than being pointed at a directory with no driver in it.

Checked in a container against the real 25.18 driver: the carried set is 123 MB
with nothing unresolved, and Level Zero still reports the GPU with the
machine's own Intel packages moved out of the way. Neither the Level Zero
driver nor the compiler back end names the front end or clang among the
libraries it opens by name, so the leaner set is complete for this path.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>

---------

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
Co-authored-by: localai-org-maint-bot <bot-opensource@localaisrl.com>
2026-07-31 23:39:53 +02:00

172 lines
6.8 KiB
Bash
Executable File

#!/bin/bash
# Checks what package_intel_libs puts in a backend built for Intel GPUs.
#
# The packager copies the libraries a backend needs next to the backend itself,
# so it can run on a machine that has none of them installed. Four things have
# to happen, and each one has been missing at some point:
#
# 1. Copy the libraries the backend program is linked against. Some of them
# are only reachable from the program, not from any other copied library,
# so looking at the copied libraries alone is not enough.
# 2. Copy the libraries that are opened by name while the program runs. Those
# are invisible to any tool that reads the list of libraries a file is
# linked against, so they have to be named one by one.
# 3. Copy the Intel graphics driver, which is also opened by name at run
# time.
# 4. Copy only the driver Level Zero talks to, and leave the OpenCL one out.
# llama.cpp reaches an Intel GPU through Level Zero; the OpenCL driver
# brings a second copy of the graphics compiler with it, which is about
# 139 MB for a path nothing here uses.
#
# The test builds a stand-in for an oneAPI installation, a stand-in for a
# driver installation and two fake backend programs, runs the real packager and
# checks the result.
set -euo pipefail
CURDIR=$(dirname "$(realpath "$0")")
SCRIPT="$CURDIR/package-gpu-libs.sh"
if ! command -v gcc >/dev/null 2>&1 || ! command -v ldd >/dev/null 2>&1; then
echo "SKIP: gcc/ldd not available"
exit 0
fi
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
# Stand-in for /opt/intel/oneapi/*/lib.
ONEAPI="$WORK/oneapi/lib"
mkdir -p "$ONEAPI"
# Two libraries the backend programs are linked against, one each. Nothing else
# refers to them, so they can only be found by looking at the programs.
echo 'int first_fn(void){return 1;}' > "$WORK/first.c"
gcc -shared -fPIC -o "$ONEAPI/libfakeoneapifirst.so.2" "$WORK/first.c"
echo 'int second_fn(void){return 2;}' > "$WORK/second.c"
gcc -shared -fPIC -o "$ONEAPI/libfakeoneapisecond.so.2" "$WORK/second.c"
# A library that is opened by name while the program runs. Nothing is linked
# against it, so only the list of names in the packager can find it.
echo 'int adapter_fn(void){return 3;}' > "$WORK/adapter.c"
gcc -shared -fPIC -o "$ONEAPI/libur_adapter_level_zero.so.0" "$WORK/adapter.c"
# Two fake backend programs, in the directory the real packaging script uses:
# package/, one level above package/lib. One is named after llama.cpp, the
# other is not, because the same packager serves several backends.
PKG="$WORK/package"
TARGET="$PKG/lib"
mkdir -p "$TARGET"
echo 'int first_fn(void); int main(void){return first_fn();}' > "$WORK/main1.c"
gcc -o "$PKG/llama-cpp-grpc" "$WORK/main1.c" \
-L"$ONEAPI" -l:libfakeoneapifirst.so.2 -Wl,-rpath,"$ONEAPI"
echo 'int second_fn(void); int main(void){return second_fn();}' > "$WORK/main2.c"
gcc -o "$PKG/bonsai-grpc" "$WORK/main2.c" \
-L"$ONEAPI" -l:libfakeoneapisecond.so.2 -Wl,-rpath,"$ONEAPI"
# The real directory also holds the script that starts the backend. Looking at a
# shell script for libraries has to be harmless.
printf '#!/bin/bash\necho started\n' > "$PKG/run.sh"
chmod +x "$PKG/run.sh"
# Stand-in for the Intel graphics driver installation. These files are opened by
# name at run time rather than linked, so the packager has to name the ones it
# wants.
DRV="$WORK/driver"
mkdir -p "$DRV/intel-opencl"
echo 'int ze_drv(void){return 4;}' > "$WORK/zedrv.c"
gcc -shared -fPIC -o "$DRV/libze_intel_gpu.so.1" "$WORK/zedrv.c"
echo 'int cl_drv(void){return 5;}' > "$WORK/cldrv.c"
gcc -shared -fPIC -o "$DRV/intel-opencl/libigdrcl.so" "$WORK/cldrv.c"
# The compiler front end the OpenCL driver needs, and the large library it is
# linked against. The link is what makes the big one arrive on its own if the
# front end is ever copied again, so the fake mirrors it.
echo 'int clang_fn(void){return 6;}' > "$WORK/clang.c"
gcc -shared -fPIC -o "$DRV/libopencl-clang.so.15" "$WORK/clang.c"
echo 'int clang_fn(void); int fcl_fn(void){return clang_fn();}' > "$WORK/fcl.c"
gcc -shared -fPIC -o "$DRV/libigdfcl.so.2" "$WORK/fcl.c" \
-L"$DRV" -l:libopencl-clang.so.15 -Wl,-rpath,"$DRV"
# Let the fake oneAPI libraries be found the way the real ones are on the build
# machine.
export LD_LIBRARY_PATH="$ONEAPI:${LD_LIBRARY_PATH:-}"
# shellcheck source=/dev/null
source "$SCRIPT" "$TARGET"
export BUILD_TYPE=sycl_f16
export INTEL_ONEAPI_LIB_DIRS="$ONEAPI"
export INTEL_DRIVER_LIB_DIRS="$DRV $DRV/intel-opencl"
package_intel_libs
fail=false
for lib in libfakeoneapifirst.so.2 libfakeoneapisecond.so.2; do
if [ ! -e "$TARGET/$lib" ]; then
echo "FAIL: $lib is missing; the backend programs' own libraries were not copied"
fail=true
fi
done
if [ ! -e "$TARGET/libur_adapter_level_zero.so.0" ]; then
echo "FAIL: the Level Zero adapter, which is opened by name, was not copied"
fail=true
fi
if [ ! -e "$TARGET/libze_intel_gpu.so.1" ]; then
echo "FAIL: the Level Zero graphics driver was not copied"
fail=true
fi
# The OpenCL driver and the compiler front end that hangs off it are left out,
# and so is the driver list that would name them.
for lib in libigdrcl.so libigdfcl.so.2 libopencl-clang.so.15; do
if [ -e "$TARGET/$lib" ]; then
echo "FAIL: $lib was copied, but nothing here uses the OpenCL path"
fail=true
fi
done
if [ -e "$TARGET/../etc/OpenCL" ]; then
echo "FAIL: an OpenCL driver list was created for a path nothing uses"
fail=true
fi
# The Python backends for Intel GPUs, built as BUILD_TYPE=intel, start without
# run.sh and so never load a copied driver. Copying one for them would add
# several hundred megabytes that nothing reads.
PYTHON_STYLE="$WORK/python-backend/lib"
mkdir -p "$PYTHON_STYLE"
(
BUILD_TYPE=intel \
INTEL_ONEAPI_LIB_DIRS="$ONEAPI" \
INTEL_DRIVER_LIB_DIRS="$DRV $DRV/intel-opencl" \
bash -c 'source "$0" "$1"; package_intel_libs' "$SCRIPT" "$PYTHON_STYLE"
) >/dev/null 2>&1
if [ -e "$PYTHON_STYLE/libze_intel_gpu.so.1" ]; then
echo "FAIL: the graphics driver was copied into a backend that cannot load it"
fail=true
fi
# A build for Intel GPUs that ends up with no driver still works, but only on a
# machine that has its own. That is easy to cause by accident and impossible to
# see afterwards, so the packager has to say so.
warning=$(
BUILD_TYPE=sycl_f16 \
INTEL_ONEAPI_LIB_DIRS="$ONEAPI" \
INTEL_DRIVER_LIB_DIRS="$WORK/empty" \
bash -c 'source "$0" "$1"; package_intel_libs' \
"$SCRIPT" "$WORK/nodriver/lib" 2>&1 >/dev/null || true
)
if ! grep -qi "no intel graphics driver" <<< "$warning"; then
echo "FAIL: no warning when the graphics driver could not be copied"
fail=true
fi
if [ "$fail" = true ]; then
ls -la "$TARGET" || true
exit 1
fi
echo "PASS: the oneAPI libraries, the adapter and the Level Zero graphics driver were all handled"
exit 0