TerslerThe little junk rig that could

Source files / install · raw

install.sh

#!/usr/bin/env bash
# tersler all-in-one installer for Ubuntu 24.04 + 2x Tesla P100.
#
#   sudo ./install/install.sh            # run all stages, resumes after reboots
#   sudo ./install/install.sh <stage>... # run selected stages
#   sudo ./install/install.sh --list     # show stages and their state
#
# Stages are idempotent: re-running one is safe. Configuration:
# install/tersler.conf (+ install/tersler.local.conf).
set -euo pipefail

REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
JR_CONF=${JR_CONF:-}
# shellcheck source=../scripts/lib.sh
source "$REPO/scripts/lib.sh"

[[ ${1:-} == --list || ${1:-} == -h || ${1:-} == --help ]] && LIST_ONLY=1 || LIST_ONLY=0

# Config can be loaded before root check (for --list); repo copy wins during install.
if [[ -z $JR_CONF ]]; then
    JR_CONF="$REPO/install/tersler.conf"
fi
# LLAMA_COMMIT from the environment beats the config: build a candidate with
#   LLAMA_COMMIT=<commit> sudo -E ./install/install.sh build
env_commit=${LLAMA_COMMIT:-}
# shellcheck source=tersler.conf
source "$JR_CONF"
# shellcheck source=/dev/null
[[ -f $REPO/install/tersler.local.conf ]] && source "$REPO/install/tersler.local.conf"
LLAMA_COMMIT=${env_commit:-$LLAMA_COMMIT}
: "${MODEL_FILE:=${MODEL_URL##*/}}"; MODEL_FILE=${MODEL_FILE%%\?*}

STAGES=(preflight config base driver cuda kernel build gpu model services verify)
declare -A STAGE_HELP=(
    [preflight]="check OS, GPUs, BAR1 sizes, RAM, disk"
    [config]="copy config to /etc/tersler, scripts to \$JR_PREFIX/bin"
    [base]="build tools and utilities"
    [driver]="NVIDIA $DRIVER_PKG (proprietary, Pascal-capable)  [reboot]"
    [cuda]="CUDA toolkit only, NVIDIA-repo drivers pinned out"
    [kernel]="kernel parameters (iommu=pt)  [reboot]"
    [build]="clone + build the P100 llama.cpp fork into a versioned dir"
    [gpu]="persistence mode + power limit service"
    [model]="download the GGUF (if MODEL_URL is set)"
    [services]="llama-server (+ optional fan control) systemd units"
    [verify]="driver, BAR1, P2P, build and server sanity checks"
)

STATE_DIR=$JR_PREFIX/state
REBOOT_FLAG=$STATE_DIR/reboot-required

stage_done() { [[ -f $STATE_DIR/stage.$1 ]]; }
mark_done()  { mkdir -p "$STATE_DIR"; date -Is > "$STATE_DIR/stage.$1"; }
need_reboot(){ mkdir -p "$STATE_DIR"; echo "$1" >> "$REBOOT_FLAG"; }

if (( LIST_ONLY )); then
    printf 'Stages (in order):\n'
    for s in "${STAGES[@]}"; do
        st="pending"; stage_done "$s" && st="done $(cat "$STATE_DIR/stage.$s")"
        printf '  %-10s %-55s %s\n' "$s" "${STAGE_HELP[$s]}" "$st"
    done
    exit 0
fi

[[ $EUID -eq 0 ]] || jr_die "run with sudo"
TARGET_USER=${JR_USER:-${SUDO_USER:-root}}
TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
export DEBIAN_FRONTEND=noninteractive

# ---------------------------------------------------------------- stages

stage_preflight() {
    # shellcheck source=/dev/null
    source /etc/os-release
    [[ $ID == ubuntu && $VERSION_ID == 24.04 ]] || jr_warn "tested on Ubuntu 24.04, this is $PRETTY_NAME"
    [[ $(uname -m) == x86_64 ]] || jr_die "x86_64 required"

    local gpus
    gpus=$(lspci -Dnn | grep -i '\[10de:' | grep -Ei '3D controller|VGA' || true)
    if [[ -z $gpus ]]; then
        jr_warn "no NVIDIA GPU visible on PCI. Cards seated? Above 4G Decoding enabled?"
    else
        jr_log "NVIDIA devices:"; sed 's/^/    /' <<<"$gpus" >&2
        local n; n=$(grep -ci 'P100' <<<"$gpus" || true)
        (( n == 2 )) || jr_warn "expected 2x Tesla P100, found $n"
        # Pipes below read their input to the end: an early exit (awk exit,
        # head, grep -q) SIGPIPEs the writer, and pipefail + set -e then end
        # the script without a message.
        local addr
        while read -r addr _; do
            local bar1
            bar1=$(lspci -vv -s "$addr" 2>/dev/null | awk '/Region 1:/ && !f {print; f=1}')
            jr_log "  $addr BAR1: ${bar1:-<not assigned>}"
            [[ $bar1 == *'[disabled]'* || -z $bar1 ]] && \
                jr_warn "  $addr: BAR1 not mapped. Enable Above 4G Decoding (docs/hardware-notes.md)"
        done <<<"$gpus"
    fi

    local mem_gb disk_gb
    mem_gb=$(awk '/MemTotal/ {printf "%d", $2/1024/1024}' /proc/meminfo)
    mkdir -p "$JR_PREFIX"
    disk_gb=$(df -BG --output=avail "$JR_PREFIX" | tail -1 | tr -dc 0-9)
    jr_log "RAM: ${mem_gb} GB, free disk at $JR_PREFIX: ${disk_gb} GB"
    (( disk_gb >= 40 )) || jr_warn "less than 40 GB free: one build + one 27B model need ~30 GB"
    grep -qw iommu=pt /proc/cmdline && jr_log "iommu=pt active" || jr_log "iommu=pt not active yet (kernel stage)"
    if [[ -d /sys/module/nouveau ]]; then jr_warn "nouveau loaded; the driver stage blacklists it"; fi
}

stage_config() {
    install -d -m 0755 "$JR_ETC" "$JR_PREFIX/bin" "$JR_PREFIX/builds" "$JR_MODEL_DIR" "$STATE_DIR"
    install -m 0644 "$JR_CONF" "$JR_ETC/tersler.conf"
    if [[ -f $REPO/install/tersler.local.conf ]]; then
        install -m 0644 "$REPO/install/tersler.local.conf" "$JR_ETC/tersler.local.conf"
    fi
    install -m 0644 "$REPO/install/upstreams.list" "$JR_ETC/upstreams.list"
    install -m 0644 "$REPO/scripts/lib.sh" "$JR_PREFIX/bin/lib.sh"
    local s
    for s in gpu-setup server strata-server fan-control bench loadtest check-updates; do
        install -m 0755 "$REPO/scripts/$s.sh" "$JR_PREFIX/bin/tersler-$s"
    done
    install -d -m 0755 "$JR_PREFIX/bench"
    cp -r "$REPO/bench/prompts" "$JR_PREFIX/bench/"
    # long prompt for prompt-processing speed: this repo's own docs (~5k tokens)
    { echo "Summarize the following build notes as 10 concise bullet points, then list the three biggest risks."
      echo; cat "$REPO/docs/research.md" "$REPO/docs/hardware-notes.md"; } > "$JR_PREFIX/bench/prompts/document.txt"
    chown -R "$TARGET_USER": "$JR_MODEL_DIR" "$JR_PREFIX/builds" "$JR_PREFIX/bench"
    # put tools on PATH for everyone
    cat > /etc/profile.d/tersler.sh <<EOF
export PATH="$JR_PREFIX/bin:\$PATH"
EOF
    jr_log "config -> $JR_ETC, scripts -> $JR_PREFIX/bin"
}

stage_base() {
    apt-get update
    apt-get install -y --no-install-recommends \
        build-essential cmake ninja-build git curl ca-certificates jq pciutils \
        lm-sensors python3 ccache libcurl4-openssl-dev "linux-headers-$(uname -r)" \
        dkms ubuntu-drivers-common
}

stage_driver() {
    [[ $DRIVER_PKG != *-open* ]] || jr_die "DRIVER_PKG=$DRIVER_PKG: open kernel modules do not support Pascal"
    # keep nouveau away
    cat > /etc/modprobe.d/tersler-blacklist-nouveau.conf <<'EOF'
blacklist nouveau
options nouveau modeset=0
EOF
    local pkgs=("$DRIVER_PKG" "${DRIVER_EXTRA_PKGS[@]}") missing=() p
    for p in "${pkgs[@]}"; do dpkg -s "$p" >/dev/null 2>&1 || missing+=("$p"); done
    if (( ${#missing[@]} == 0 )); then
        jr_log "${pkgs[*]} already installed"
    else
        apt-get install -y --no-install-recommends "${missing[@]}"
        update-initramfs -u
        need_reboot driver
    fi
    dpkg -s xserver-xorg-core >/dev/null 2>&1 && \
        jr_warn "xserver-xorg-core is installed; the rig is meant to be headless (apt-cache rdepends --installed xserver-xorg-core)"
    # never let unattended upgrades or ubuntu-drivers swap the branch
    apt-mark hold "${pkgs[@]}" >/dev/null
    jr_log "held ${pkgs[*]} (apt-mark unhold to change on purpose)"
}

stage_cuda() {
    if ! dpkg -s cuda-keyring >/dev/null 2>&1; then
        local tmp; tmp=$(mktemp -d)
        curl -fsSL -o "$tmp/cuda-keyring.deb" "$CUDA_KEYRING_URL"
        dpkg -i "$tmp/cuda-keyring.deb"; rm -rf "$tmp"
    fi
    # NVIDIA's CUDA repo also carries drivers. Pin them out so they can never
    # replace the Ubuntu R580 packages; only the toolkit comes from there.
    cat > /etc/apt/preferences.d/tersler-no-nvidia-repo-drivers <<'EOF'
Package: nvidia-* libnvidia-* xserver-xorg-video-nvidia* cuda-drivers* nvidia-open* libxnvctrl*
Pin: origin developer.download.nvidia.com
Pin-Priority: -1
EOF
    apt-get update
    apt-get install -y --no-install-recommends "$CUDA_PKG"
    cat > /etc/profile.d/tersler-cuda.sh <<EOF
export CUDA_HOME=$CUDA_HOME
export PATH=$CUDA_HOME/bin:\$PATH
EOF
    "$CUDA_HOME/bin/nvcc" --version | tail -2
}

stage_kernel() {
    local want missing=() p
    for p in $KERNEL_PARAMS; do grep -qw -- "$p" /proc/cmdline || missing+=("$p"); done
    cat > /etc/default/grub.d/99-tersler.cfg <<EOF
# tersler: kernel parameters (P2P between the P100s needs iommu=pt)
GRUB_CMDLINE_LINUX_DEFAULT="\$GRUB_CMDLINE_LINUX_DEFAULT $KERNEL_PARAMS"
EOF
    update-grub
    want=${missing[*]:-}
    if [[ -n $want ]]; then need_reboot "kernel: $want"; fi
}

build_jobs() {
    if [[ $BUILD_JOBS != auto ]]; then echo "$BUILD_JOBS"; return; fi
    local mem_gb cpus jobs
    mem_gb=$(awk '/MemAvailable/ {printf "%d", $2/1024/1024}' /proc/meminfo)
    cpus=$(nproc)
    jobs=$(( mem_gb * 10 / 25 ))   # ~2.5 GB per nvcc job (FA_QUANTS=all is heavy)
    (( jobs < 1 )) && jobs=1
    (( jobs > cpus )) && jobs=$cpus
    echo "$jobs"
}

stage_build() {
    [[ -x $CUDA_HOME/bin/nvcc ]] || jr_die "nvcc not found at $CUDA_HOME (run the cuda stage)"
    local src=$JR_PREFIX/src/llama.cpp
    as_user() { sudo -u "$TARGET_USER" -H "$@"; }
    install -d -o "$TARGET_USER" "$JR_PREFIX/src"
    if [[ ! -d $src/.git ]]; then
        as_user git clone --branch "$LLAMA_BRANCH" "$LLAMA_REPO" "$src"
    else
        as_user git -C "$src" remote set-url origin "$LLAMA_REPO"
        as_user git -C "$src" fetch --tags origin "$LLAMA_BRANCH"
    fi
    local commit=${LLAMA_COMMIT:-origin/$LLAMA_BRANCH}
    as_user git -C "$src" checkout --detach "$commit"
    local full short dest jobs
    full=$(as_user git -C "$src" rev-parse HEAD); short=${full:0:9}
    dest=$JR_PREFIX/builds/$short
    if [[ -x $dest/bin/llama-server ]]; then
        jr_log "build $short already exists"
    else
        jobs=$(build_jobs)
        jr_log "building $short with $jobs parallel jobs (CUDA $CUDA_HOME, sm_60 only)"
        as_user env PATH="$CUDA_HOME/bin:$PATH" CUDACXX="$CUDA_HOME/bin/nvcc" \
            cmake -S "$src" -B "$src/build-$short" -G Ninja "${LLAMA_CMAKE_FLAGS[@]}" \
            -DCMAKE_INSTALL_PREFIX="$dest" -DCMAKE_INSTALL_RPATH='$ORIGIN/../lib' \
            -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache
        as_user env PATH="$CUDA_HOME/bin:$PATH" cmake --build "$src/build-$short" -j "$jobs"
        install -d -o "$TARGET_USER" "$dest"
        as_user cmake --install "$src/build-$short"
        jq -n --arg repo "$LLAMA_REPO" --arg branch "$LLAMA_BRANCH" --arg commit "$full" \
              --arg cuda "$("$CUDA_HOME/bin/nvcc" --version | sed -n 's/.*release \([0-9.]*\).*/\1/p')" \
              --arg date "$(date -Is)" --argjson flags "$(printf '%s\n' "${LLAMA_CMAKE_FLAGS[@]}" | jq -R . | jq -s .)" \
              '{repo:$repo, branch:$branch, commit:$commit, cuda:$cuda, built:$date, cmake_flags:$flags}' \
              > "$dest/build-info.json"
    fi
    # activate: $JR_PREFIX/current -> this build (previous one kept for rollback)
    if [[ -L $JR_PREFIX/current && $(readlink -f "$JR_PREFIX/current") != "$dest" ]]; then
        ln -sfn "$(readlink -f "$JR_PREFIX/current")" "$JR_PREFIX/previous"
    fi
    ln -sfn "$dest" "$JR_PREFIX/current"
    jr_log "active build: $short ($JR_PREFIX/current)"
}

# nvidia-smi -L exits 0 with "No devices found" when the driver is loaded
# but no GPU initialized (e.g. power cables missing), so look for a GPU line.
gpus_ready() { [[ $(nvidia-smi -L 2>/dev/null) == GPU* ]]; }

stage_gpu() {
    install -m 0644 "$REPO/install/systemd/tersler-gpu.service" /etc/systemd/system/
    sed -i "s|@PREFIX@|$JR_PREFIX|g" /etc/systemd/system/tersler-gpu.service
    systemctl daemon-reload
    systemctl enable tersler-gpu.service
    if gpus_ready; then
        systemctl restart tersler-gpu.service
    else
        jr_log "no GPU initialized yet (reboot pending?); tersler-gpu runs at next boot"
    fi
}

stage_model() {
    if [[ -z $MODEL_URL ]]; then
        jr_warn "MODEL_URL not set (install/tersler.local.conf); skipping download"
        return 0
    fi
    local dst; dst=$JR_MODEL_DIR/$MODEL_FILE
    install -d -o "$TARGET_USER" "$JR_MODEL_DIR"
    jr_log "downloading $MODEL_FILE (resumable)"
    sudo -u "$TARGET_USER" curl -fL -C - --retry 5 -o "$dst" "$MODEL_URL"
    if [[ -n $MODEL_SHA256 ]]; then
        echo "$MODEL_SHA256  $dst" | sha256sum -c - || jr_die "checksum mismatch for $dst"
    fi
    if [[ -n ${MMPROJ_URL:-} ]]; then
        dst=$JR_MODEL_DIR/$MMPROJ_FILE
        jr_log "downloading $MMPROJ_FILE (vision encoder)"
        sudo -u "$TARGET_USER" curl -fL -C - --retry 5 -o "$dst" "$MMPROJ_URL"
        if [[ -n ${MMPROJ_SHA256:-} ]]; then
            echo "$MMPROJ_SHA256  $dst" | sha256sum -c - || jr_die "checksum mismatch for $dst"
        fi
    fi
}

stage_services() {
    local u
    for u in tersler-llama.service tersler-strata.service tersler-fan.service; do
        install -m 0644 "$REPO/install/systemd/$u" /etc/systemd/system/
        sed -i -e "s|@PREFIX@|$JR_PREFIX|g" -e "s|@USER@|$TARGET_USER|g" /etc/systemd/system/$u
    done
    systemctl daemon-reload
    if [[ ${SERVE_ENGINE:-llama} == strata ]]; then
        [[ -x $STRATA_SRC/strata/build-p100/strata && -f $STRATA_WORK/flash-next-p100.json ]] ||
            jr_die "SERVE_ENGINE=strata, but Strata is not set up (run: scripts/strata-test.sh prep)"
        systemctl disable tersler-llama.service 2>/dev/null || true
        systemctl enable tersler-strata.service
        jr_log "tersler-strata enabled (start: systemctl start tersler-strata)"
    elif [[ -n $MODEL_FILE && -f $JR_MODEL_DIR/$MODEL_FILE ]]; then
        systemctl disable tersler-strata.service 2>/dev/null || true
        systemctl enable tersler-llama.service
        jr_log "tersler-llama enabled (start: systemctl start tersler-llama)"
    else
        jr_warn "model file missing; tersler-llama installed but not enabled"
    fi
    if [[ $FAN_CONTROL == 1 ]]; then
        systemctl enable --now tersler-fan.service
    else
        systemctl disable --now tersler-fan.service 2>/dev/null || true
        jr_log "fan control off (FAN_CONTROL=0); see tersler-fan --discover"
    fi
}

stage_verify() {
    local ok=1
    if ! gpus_ready; then jr_warn "no GPU initialized: reboot pending, or see 'sudo dmesg | grep NVRM'"; ok=0
    else
        nvidia-smi --query-gpu=index,name,driver_version,pcie.link.gen.max,pcie.link.width.current,power.limit,ecc.mode.current,temperature.gpu --format=csv
        local drv; drv=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | sed -n 1p)
        [[ $drv == "$DRIVER_BRANCH".* ]] || { jr_warn "driver $drv is not the $DRIVER_BRANCH branch"; ok=0; }
        nvidia-smi -q -d MEMORY | grep -A3 'BAR1 Memory Usage' | grep -E 'Total' | sed 's/^/    BAR1 /'
        jr_log "topology / P2P:"
        nvidia-smi topo -m || true
        nvidia-smi topo -p2p r 2>/dev/null || true
        local w; w=$(nvidia-smi --query-gpu=pcie.link.width.max --format=csv,noheader | sort -u | tr '\n' ' ')
        [[ $w == "16 " ]] || jr_warn "PCIe max link widths: $w (expected 16 on both; PCIE3/PCIE5 occupied?)"
    fi
    grep -qw iommu=pt /proc/cmdline || { jr_warn "iommu=pt not active (reboot after kernel stage)"; ok=0; }
    if [[ -x $JR_PREFIX/current/bin/llama-server ]]; then
        local missing; missing=$(ldd "$JR_PREFIX/current/bin/llama-server" | awk '/not found/ {print $1}')
        if [[ -n $missing ]]; then jr_warn "llama-server can't find: $(echo $missing)"; ok=0
        else "$JR_PREFIX/current/bin/llama-server" --version 2>&1 | grep -E '^(version|built with)' || true
        fi
        jq -r '"build: \(.commit[0:9]) (\(.branch)), CUDA \(.cuda)"' "$JR_PREFIX/current/build-info.json"
    else
        jr_warn "no active llama.cpp build"; ok=0
    fi
    [[ -s $REBOOT_FLAG ]] && { jr_warn "reboot required: $(sort -u "$REBOOT_FLAG" | tr '\n' ' ')"; ok=0; }
    (( ok )) && jr_log "verify: all good" || jr_warn "verify: see warnings above"
}

# ---------------------------------------------------------------- main

run_stage() {
    local s=$1
    declare -F "stage_$s" >/dev/null || jr_die "unknown stage: $s (try --list)"
    jr_log "=== stage: $s — ${STAGE_HELP[$s]}"
    "stage_$s"
    mark_done "$s"
}

# a reboot clears the pending-reboot flag
if [[ -f $REBOOT_FLAG && $REBOOT_FLAG -ot /proc/1 ]]; then rm -f "$REBOOT_FLAG"; fi

if (( $# )); then
    for s in "$@"; do run_stage "$s"; done
else
    for s in "${STAGES[@]}"; do
        # verify needs the rebooted system (driver loaded, iommu=pt active)
        if [[ -s $REBOOT_FLAG ]] && [[ $s == verify ]]; then
            jr_warn "reboot required ($(sort -u "$REBOOT_FLAG" | tr '\n' ' ')). Reboot, then run this script again; it continues from here."
            exit 0
        fi
        if stage_done "$s" && [[ $s != preflight && $s != config && $s != verify ]]; then
            jr_log "stage $s already done (re-run explicitly: install.sh $s)"
            continue
        fi
        run_stage "$s"
    done
fi