Skip to content

Commit 98aa4d2

Browse files
committed
AI: Add install-llamacpp.sh for llama.cpp Linux binaries
Installs prebuilt llama.cpp binaries from GitHub releases on AMD64/ARM64, defaulting to the latest release or an explicit tag. Auto-detects the GPU (sysfs PCI vendor IDs, lspci/nvidia-smi fallback) and picks ROCm, Vulkan, or a plain CPU build, with a graceful fallback chain; NVIDIA GPUs get the Vulkan build since upstream ships no CUDA build for Linux. Extracts into <destdir>/lib/llama.cpp and symlinks the executables into <destdir>/bin (the binaries carry RUNPATH "$ORIGIN"). Accelerator overridable via flags or PHOTOPRISM_LLAMA_ACCEL; optional LLAMA_SHA256 verification.
1 parent c9362a6 commit 98aa4d2

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

scripts/dist/install-llamacpp.sh

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#!/usr/bin/env bash
2+
3+
# Installs llama.cpp prebuilt Linux binaries from GitHub on AMD64 and ARM64.
4+
# bash <(curl -s https://raw.githubusercontent.com/photoprism/photoprism/develop/scripts/dist/install-llamacpp.sh)
5+
# bash <(curl -s https://raw.githubusercontent.com/photoprism/photoprism/develop/scripts/dist/install-llamacpp.sh) -- --rocm /usr/local b9948
6+
#
7+
# The accelerator is auto-detected by default (ROCm or Vulkan for a supported GPU,
8+
# otherwise a plain CPU build). Upstream does not publish CUDA builds for Linux, so
9+
# NVIDIA GPUs are served the Vulkan build, which their driver supports.
10+
11+
set -euo pipefail
12+
13+
if ! command -v jq >/dev/null 2>&1; then
14+
echo "Error: jq is required but not installed." 1>&2
15+
exit 1
16+
fi
17+
18+
REPO="ggml-org/llama.cpp"
19+
20+
# usage prints the command-line synopsis and supported environment variables.
21+
usage() {
22+
echo "Usage: ${0##*/} [--auto|--cpu|--vulkan|--rocm|--sycl] [destdir] [version]" 1>&2
23+
echo " ${0##*/} [--accel auto|cpu|vulkan|rocm|sycl] [destdir] [version]" 1>&2
24+
echo "" 1>&2
25+
echo "Arguments:" 1>&2
26+
echo " destdir Install prefix (default: /usr/local); binaries land in <destdir>/bin." 1>&2
27+
echo " version Release tag to install (e.g. b9948); defaults to the latest release." 1>&2
28+
echo "" 1>&2
29+
echo "Environment:" 1>&2
30+
echo " PHOTOPRISM_LLAMA_ACCEL=auto|cpu|vulkan|rocm|sycl Accelerator to install." 1>&2
31+
echo " PHOTOPRISM_ARCH / BUILD_ARCH Override target architecture." 1>&2
32+
echo " LLAMA_SHA256 Verify the download against this checksum." 1>&2
33+
}
34+
35+
# ACCEL selects the hardware backend; "auto" resolves it from the detected GPU below.
36+
ACCEL=${PHOTOPRISM_LLAMA_ACCEL:-auto}
37+
38+
if [[ ${1:-} == "--help" || ${1:-} == "-h" ]]; then
39+
usage
40+
exit 0
41+
fi
42+
43+
while [[ $# -gt 0 ]]; do
44+
case $1 in
45+
--auto) ACCEL=auto; shift ;;
46+
--cpu) ACCEL=cpu; shift ;;
47+
--vulkan) ACCEL=vulkan; shift ;;
48+
--rocm) ACCEL=rocm; shift ;;
49+
--sycl) ACCEL=sycl; shift ;;
50+
--accel)
51+
ACCEL=${2:-}
52+
if [[ -z $ACCEL ]]; then
53+
echo "Error: --accel requires a value (auto, cpu, vulkan, rocm, or sycl)." 1>&2
54+
exit 1
55+
fi
56+
shift 2
57+
;;
58+
--accel=*) ACCEL=${1#--accel=}; shift ;;
59+
-h | --help) usage; exit 0 ;;
60+
--) shift; break ;;
61+
-*) echo "Error: Unknown option: $1" 1>&2; exit 1 ;;
62+
*) break ;;
63+
esac
64+
done
65+
66+
# Normalize the accelerator selection to a known keyword.
67+
ACCEL=${ACCEL,,}
68+
case $ACCEL in
69+
auto | cpu | vulkan | rocm | sycl) ;;
70+
gpu) ACCEL=auto ;;
71+
*) echo "Error: Unsupported accelerator \"$ACCEL\" (use auto, cpu, vulkan, rocm, or sycl)." 1>&2; exit 1 ;;
72+
esac
73+
74+
# You can provide a custom installation directory as the first positional argument.
75+
DESTDIR=$(realpath -m "${1:-/usr/local}")
76+
77+
# Determine target architecture.
78+
if [[ -n ${PHOTOPRISM_ARCH:-} ]]; then
79+
SYSTEM_ARCH=$PHOTOPRISM_ARCH
80+
else
81+
SYSTEM_ARCH=$(uname -m)
82+
fi
83+
84+
DESTARCH=${BUILD_ARCH:-$SYSTEM_ARCH}
85+
86+
# llama.cpp names its Linux assets "...-ubuntu-<accel>-x64" / "...-arm64".
87+
case $DESTARCH in
88+
amd64 | AMD64 | x86_64 | x86-64) DESTARCH=x64 ;;
89+
arm64 | ARM64 | aarch64) DESTARCH=arm64 ;;
90+
*)
91+
echo "Error: Unsupported Machine Architecture: \"$DESTARCH\"" 1>&2
92+
exit 1
93+
;;
94+
esac
95+
96+
# Abort if not executed as root when installing into a system directory.
97+
if [[ $(id -u) != "0" ]] && [[ $DESTDIR == "/usr" || $DESTDIR == "/usr/local" ]]; then
98+
echo "Error: Run ${0##*/} as root to install in a system directory!" 1>&2
99+
exit 1
100+
fi
101+
102+
# detect_gpu_vendors populates GPU_VENDORS with the GPU makers found on this host.
103+
# It reads PCI vendor IDs from sysfs first (no dependencies) and falls back to lspci.
104+
GPU_VENDORS=""
105+
detect_gpu_vendors() {
106+
local f id out
107+
for f in /sys/class/drm/card[0-9]*/device/vendor; do
108+
[[ -r $f ]] || continue
109+
id=$(cat "$f" 2>/dev/null || true)
110+
case $id in
111+
0x10de) GPU_VENDORS+=" nvidia" ;;
112+
0x1002) GPU_VENDORS+=" amd" ;;
113+
0x8086) GPU_VENDORS+=" intel" ;;
114+
esac
115+
done
116+
if [[ -z ${GPU_VENDORS// /} ]] && command -v lspci >/dev/null 2>&1; then
117+
out=$(lspci 2>/dev/null | grep -iE 'vga|3d controller|display' || true)
118+
grep -qi 'nvidia' <<<"$out" && GPU_VENDORS+=" nvidia"
119+
grep -qiE 'amd|advanced micro|ati' <<<"$out" && GPU_VENDORS+=" amd"
120+
grep -qi 'intel' <<<"$out" && GPU_VENDORS+=" intel"
121+
fi
122+
command -v nvidia-smi >/dev/null 2>&1 && GPU_VENDORS+=" nvidia"
123+
# Deduplicate and trim.
124+
GPU_VENDORS=$(tr ' ' '\n' <<<"$GPU_VENDORS" | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')
125+
}
126+
127+
# has_gpu returns success if the given GPU vendor was detected.
128+
has_gpu() { [[ " $GPU_VENDORS " == *" $1 "* ]]; }
129+
130+
# rocm_runtime_available returns success if a ROCm/HIP runtime is present on the host.
131+
rocm_runtime_available() {
132+
command -v rocminfo >/dev/null 2>&1 && return 0
133+
[[ -d /opt/rocm ]] && return 0
134+
ldconfig -p 2>/dev/null | grep -q 'libamdhip64\.so' && return 0
135+
return 1
136+
}
137+
138+
# vulkan_runtime_available returns success if the Vulkan loader is present on the host.
139+
vulkan_runtime_available() {
140+
command -v vulkaninfo >/dev/null 2>&1 && return 0
141+
ldconfig -p 2>/dev/null | grep -q 'libvulkan\.so\.1' && return 0
142+
return 1
143+
}
144+
145+
# Resolve "auto" to a concrete accelerator based on the detected GPU: prefer ROCm for
146+
# AMD GPUs with a ROCm runtime, otherwise Vulkan for any GPU (NVIDIA included), else CPU.
147+
if [[ $ACCEL == auto ]]; then
148+
detect_gpu_vendors
149+
echo "GPU detected: ${GPU_VENDORS:-none}"
150+
if [[ $DESTARCH == x64 ]] && has_gpu amd && rocm_runtime_available; then
151+
ACCEL=rocm
152+
elif [[ -n $GPU_VENDORS ]] && { has_gpu nvidia || has_gpu amd || has_gpu intel; }; then
153+
ACCEL=vulkan
154+
else
155+
ACCEL=cpu
156+
fi
157+
echo "Selected accelerator: ${ACCEL} (override with --cpu/--vulkan/--rocm/--sycl)."
158+
fi
159+
160+
echo "Installing llama.cpp (${ACCEL}) for ${DESTARCH^^}..."
161+
162+
# Fetch release metadata: a specific tag when requested, otherwise the latest release.
163+
if [[ -n ${2:-} ]]; then
164+
VERSION=${2}
165+
RELEASE_JSON=$(curl --fail --silent --show-error "https://api.github.com/repos/${REPO}/releases/tags/${VERSION}" || true)
166+
if [[ -z $RELEASE_JSON ]]; then
167+
echo "Error: Unable to fetch release metadata for tag ${VERSION}." 1>&2
168+
exit 1
169+
fi
170+
else
171+
RELEASE_JSON=$(curl --fail --silent --show-error "https://api.github.com/repos/${REPO}/releases/latest" || true)
172+
if [[ -z $RELEASE_JSON ]]; then
173+
echo "Error: Unable to fetch release metadata from GitHub." 1>&2
174+
exit 1
175+
fi
176+
fi
177+
178+
TAG_NAME=$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")
179+
if [[ -z $TAG_NAME ]]; then
180+
echo "Error: Release metadata did not include a tag name." 1>&2
181+
exit 1
182+
fi
183+
184+
# asset_pattern returns the regex matching the ubuntu asset for the given accelerator.
185+
# ROCm and SYCL embed a runtime version in the file name, so match it with a wildcard.
186+
asset_pattern() {
187+
case $1 in
188+
cpu) echo "bin-ubuntu-${DESTARCH}\\.tar\\.gz$" ;;
189+
vulkan) echo "bin-ubuntu-vulkan-${DESTARCH}\\.tar\\.gz$" ;;
190+
rocm) echo "bin-ubuntu-rocm-.*-${DESTARCH}\\.tar\\.gz$" ;;
191+
sycl) echo "bin-ubuntu-sycl-fp16-${DESTARCH}\\.tar\\.gz$" ;;
192+
esac
193+
}
194+
195+
# asset_url prints the download URL of the first asset matching the given accelerator.
196+
asset_url() {
197+
local re
198+
re=$(asset_pattern "$1")
199+
[[ -z $re ]] && return 0
200+
jq -r --arg re "$re" '.assets[]? | select(.name | test($re)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1
201+
}
202+
203+
# Build a fallback chain so an unavailable accelerator degrades gracefully (e.g. ROCm
204+
# has no ARM64 build, so it falls back to Vulkan and then to the plain CPU build).
205+
case $ACCEL in
206+
rocm) CANDIDATES=(rocm vulkan cpu) ;;
207+
vulkan) CANDIDATES=(vulkan cpu) ;;
208+
sycl) CANDIDATES=(sycl cpu) ;;
209+
cpu) CANDIDATES=(cpu) ;;
210+
esac
211+
212+
RESOLVED_ACCEL=""
213+
ASSET_URL=""
214+
for candidate in "${CANDIDATES[@]}"; do
215+
url=$(asset_url "$candidate")
216+
if [[ -n $url && $url != "null" ]]; then
217+
RESOLVED_ACCEL=$candidate
218+
ASSET_URL=$url
219+
break
220+
fi
221+
done
222+
223+
if [[ -z $ASSET_URL ]]; then
224+
echo "Error: No llama.cpp Linux asset found for ${ACCEL}/${DESTARCH} in release ${TAG_NAME}." 1>&2
225+
exit 1
226+
fi
227+
228+
if [[ $RESOLVED_ACCEL != "$ACCEL" ]]; then
229+
echo "Warning: No ${ACCEL} build for ${DESTARCH} in ${TAG_NAME}; falling back to ${RESOLVED_ACCEL}." 1>&2
230+
fi
231+
232+
ASSET_NAME=${ASSET_URL##*/}
233+
LLAMA_DIR="${DESTDIR}/lib/llama.cpp"
234+
235+
echo "--------------------------------------------------------------------------------"
236+
echo "VERSION : ${TAG_NAME}"
237+
echo "ACCEL : ${RESOLVED_ACCEL}"
238+
echo "ARCH : ${DESTARCH}"
239+
echo "ASSET : ${ASSET_NAME}"
240+
echo "DOWNLOAD: ${ASSET_URL}"
241+
echo "DESTDIR : ${DESTDIR}"
242+
echo "LIBDIR : ${LLAMA_DIR}"
243+
echo "--------------------------------------------------------------------------------"
244+
245+
tmp_tar=$(mktemp)
246+
workdir=$(mktemp -d)
247+
trap 'rm -rf "${tmp_tar}" "${workdir}"' EXIT
248+
249+
echo "Downloading ${ASSET_NAME}..."
250+
curl --fail --silent --show-error --location --retry 3 --retry-delay 2 "${ASSET_URL}" -o "${tmp_tar}"
251+
252+
# Verify the download when a checksum was provided; upstream ships no checksum manifest.
253+
if [[ -n ${LLAMA_SHA256:-} ]]; then
254+
if command -v sha256sum >/dev/null 2>&1; then
255+
actual=$(sha256sum "${tmp_tar}" | awk '{print $1}')
256+
else
257+
actual=$(shasum -a 256 "${tmp_tar}" | awk '{print $1}')
258+
fi
259+
if [[ ${LLAMA_SHA256} != "${actual}" ]]; then
260+
echo "Error: SHA-256 mismatch for ${ASSET_NAME}." 1>&2
261+
echo " expected: ${LLAMA_SHA256}" 1>&2
262+
echo " actual: ${actual}" 1>&2
263+
exit 1
264+
fi
265+
echo "Checksum OK (${actual})."
266+
fi
267+
268+
echo "Extracting ${ASSET_NAME}..."
269+
tar xzf "${tmp_tar}" -C "${workdir}"
270+
271+
# The archive extracts to a single top-level "llama-<tag>" directory.
272+
srcdir=$(find "${workdir}" -mindepth 1 -maxdepth 1 -type d | head -n1)
273+
if [[ -z ${srcdir} ]]; then
274+
echo "Error: Unexpected archive layout in ${ASSET_NAME}." 1>&2
275+
exit 1
276+
fi
277+
278+
# Replace any previous install so stale libraries don't linger next to the binaries.
279+
echo "Installing binaries and libraries to ${LLAMA_DIR}..."
280+
rm -rf "${LLAMA_DIR}"
281+
mkdir -p "${LLAMA_DIR}"
282+
cp -a "${srcdir}/." "${LLAMA_DIR}/"
283+
284+
# Symlink the executables into <destdir>/bin. The binaries carry RUNPATH "$ORIGIN",
285+
# so they resolve their co-located shared libraries through the symlink.
286+
mkdir -p "${DESTDIR}/bin"
287+
linked=0
288+
for f in "${LLAMA_DIR}"/llama* "${LLAMA_DIR}"/ggml-rpc-server; do
289+
[[ -f $f ]] || continue
290+
case $f in *.so | *.so.*) continue ;; esac
291+
[[ -x $f ]] || continue
292+
ln -sf "$f" "${DESTDIR}/bin/$(basename "$f")"
293+
linked=$((linked + 1))
294+
done
295+
296+
if [[ $linked -eq 0 ]]; then
297+
echo "Error: No llama.cpp executables found in ${LLAMA_DIR}." 1>&2
298+
exit 1
299+
fi
300+
301+
echo "Linked ${linked} executables into ${DESTDIR}/bin."
302+
303+
# Warn when the selected backend needs a runtime that is not present on this host.
304+
case $RESOLVED_ACCEL in
305+
rocm)
306+
if ! rocm_runtime_available; then
307+
echo "Note: The ROCm build needs the ROCm/HIP runtime (e.g. libamdhip64.so) at runtime." 1>&2
308+
fi
309+
;;
310+
vulkan)
311+
if ! vulkan_runtime_available; then
312+
echo "Note: The Vulkan build needs the Vulkan loader (libvulkan.so.1) and a GPU driver ICD at runtime." 1>&2
313+
echo " On Debian/Ubuntu: apt-get install libvulkan1 mesa-vulkan-drivers (plus the NVIDIA driver for NVIDIA GPUs)." 1>&2
314+
fi
315+
;;
316+
esac
317+
318+
echo "Try: ${DESTDIR}/bin/llama-cli --version"
319+
echo "Done."

0 commit comments

Comments
 (0)