TerslerThe little junk rig that could

Source files / scripts · raw

bench.sh

#!/usr/bin/env bash
# Benchmark a llama.cpp build with the configured model and flags, so builds,
# drivers and settings can be compared before switching.
#
#   tersler-bench                      # bench the active build
#   tersler-bench /opt/tersler/builds/<commit>   # bench a specific build
#   RUNS=5 MAX_TOKENS=1024 tersler-bench
#
# For the active build, uses the running tersler-llama server if it runs
# that build and model; otherwise starts a private llama-server on
# BENCH_PORT. Sends every prompt in bench/prompts/*.txt RUNS times (prompt
# cache off), and appends the medians to
# $JR_PREFIX/bench/results.csv (prompt + generation tok/s, as reported by
# llama-server's own timings).
set -euo pipefail
# shellcheck source=lib.sh
source "$(dirname "$(readlink -f "$0")")/lib.sh"
jr_load_config

build=$(readlink -f "${1:-$JR_PREFIX/current}")
RUNS=${RUNS:-3}
MAX_TOKENS=${MAX_TOKENS:-512}
BENCH_PORT=${BENCH_PORT:-18080}
PROMPTS=${PROMPTS:-$JR_PREFIX/bench/prompts}
[[ -d $PROMPTS ]] || PROMPTS=$(dirname "$(readlink -f "$0")")/../bench/prompts
RESULTS=${RESULTS:-$JR_PREFIX/bench/results.csv}
LOG=$(mktemp -t tersler-bench.XXXX.log)

[[ -x $build/bin/llama-server ]] || jr_die "no llama-server in $build"
command -v jq >/dev/null || jr_die "jq missing"

commit_full=$(jq -r '.commit // empty' "$build/build-info.json" 2>/dev/null || true)

# Reuse the running server if it is this build with this model: the build
# commit and model path come from its /props. (Flags changed in the config
# since it started are not detected; restart tersler-llama after edits.)
port=""
if props=$(curl -fs "http://127.0.0.1:$LLAMA_PORT/props" 2>/dev/null); then
    run_commit=$(jq -r '.build_info // "" | sub("^b[0-9]+-"; "")' <<<"$props")
    run_model=$(jq -r '.model_path // ""' <<<"$props")
    if [[ -n $commit_full && -n $run_commit && $commit_full == "$run_commit"* \
          && $run_model == "$(jr_model_path)" ]]; then
        port=$LLAMA_PORT
        jr_log "using the running server on :$port (build $run_commit, $(basename "$run_model"))"
    else
        jr_warn "the server on :$LLAMA_PORT runs build ${run_commit:-?} / $(basename "${run_model:-?}"), not $(basename "$build")."
        jr_warn "Two servers won't fit in VRAM:  sudo systemctl stop tersler-llama   and re-run"
        exit 1
    fi
elif ss -ltn | grep ":$LLAMA_PORT " >/dev/null; then
    jr_die "something listens on :$LLAMA_PORT but doesn't answer /props; stop it first"
fi

if [[ -z $port ]]; then
    port=$BENCH_PORT
    jr_log "starting $build on :$port (log: $LOG)"
    # installed as tersler-server next to us; server.sh in the repo
    here=$(dirname "$(readlink -f "$0")")
    server=$here/tersler-server; [[ -x $server ]] || server=$here/server.sh
    LLAMA_BIN=$build LLAMA_PORT=$port LLAMA_HOST=127.0.0.1 "$server" >"$LOG" 2>&1 &
    srv=$!
    trap 'kill $srv 2>/dev/null; wait $srv 2>/dev/null || true' EXIT

    for _ in $(seq 600); do
        curl -fs "http://127.0.0.1:$port/health" >/dev/null 2>&1 && break
        kill -0 $srv 2>/dev/null || { tail -20 "$LOG"; jr_die "server died during startup"; }
        sleep 1
    done
    curl -fs "http://127.0.0.1:$port/health" >/dev/null || jr_die "server not healthy after 10 min"
fi

median() { sort -n | awk '{a[NR]=$1} END {if (NR==0) print "NA"; else if (NR%2) print a[(NR+1)/2]; else printf "%.2f\n", (a[NR/2]+a[NR/2+1])/2}'; }

commit=$(jq -r '.commit[0:9]' "$build/build-info.json" 2>/dev/null || basename "$build")
driver=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | sed -n 1p)
plimit=$(nvidia-smi --query-gpu=power.limit --format=csv,noheader,nounits | sed -n 1p)
p2p=0; jr_p2p_enabled && p2p=1
[[ -f $RESULTS ]] || echo "date,commit,driver,power_limit_w,p2p,model,prompt,runs,max_tokens,pp_tok_s,tg_tok_s,draft_accept" > "$RESULTS"

for f in "$PROMPTS"/*.txt; do
    name=$(basename "$f" .txt)
    pp=(); tg=(); acc=()
    for r in $(seq "$RUNS"); do
        body=$(jq -n --rawfile p "$f" --argjson n "$MAX_TOKENS" \
            '{messages:[{role:"user",content:$p}], max_tokens:$n, stream:false, cache_prompt:false}')
        resp=$(curl -fs "http://127.0.0.1:$port/v1/chat/completions" \
               -H 'Content-Type: application/json' -d "$body") || { jr_warn "$name run $r failed"; continue; }
        pp+=("$(jq -r '.timings.prompt_per_second // empty' <<<"$resp")")
        tg+=("$(jq -r '.timings.predicted_per_second // empty' <<<"$resp")")
        acc+=("$(jq -r 'if .timings.draft_n then (.timings.draft_n_accepted / .timings.draft_n) else empty end' <<<"$resp")")
        printf '  %-12s run %d: pp %6.1f  tg %6.1f tok/s\n' "$name" "$r" "${pp[-1]:-0}" "${tg[-1]:-0}"
    done
    mp=$(printf '%s\n' "${pp[@]}" | grep . | median) || true
    mt=$(printf '%s\n' "${tg[@]}" | grep . | median) || true
    ma=$(printf '%s\n' "${acc[@]}" | grep . | median) || true
    echo "$(date -Is),$commit,$driver,$plimit,$p2p,$MODEL_FILE,$name,$RUNS,$MAX_TOKENS,$mp,$mt,${ma:-NA}" >> "$RESULTS"
    jr_log "$name: median pp $mp tok/s, tg $mt tok/s"
done
jr_log "results appended to $RESULTS"
column -s, -t < <(tail -n 8 "$RESULTS") || true