|
| 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