Source files / scripts · raw
loadtest.sh
#!/usr/bin/env bash # Sustained-load thermal test: back-to-back long generations against the # running tersler-llama server, logging GPU temps/power/clocks and the fan # every few seconds. # # tersler-loadtest [minutes] default 20 # # Writes $JR_PREFIX/bench/loadtest-<date>.csv and prints a summary. set -euo pipefail # shellcheck source=lib.sh source "$(dirname "$(readlink -f "$0")")/lib.sh" jr_load_config minutes=${1:-20} url=http://127.0.0.1:$LLAMA_PORT out=$JR_PREFIX/bench/loadtest-$(date +%Y%m%d-%H%M).csv curl -sf "$url/health" >/dev/null || jr_die "no server on $url (sudo systemctl start tersler-llama)" fan="" if [[ -n ${FAN_HWMON_NAME:-} && -n ${FAN_PWM:-} ]]; then for d in /sys/class/hwmon/hwmon*; do [[ $(cat "$d/name" 2>/dev/null) == "$FAN_HWMON_NAME" ]] && fan=$d done fi fan_rpm() { # which fanN_input belongs to FAN_PWM is board-specific: FAN_INPUT in the config local f=${FAN_INPUT:-} [[ -n $fan && -n $f && -r $fan/${f}_input ]] && cat "$fan/${f}_input" || echo NA } echo "time_s,gpu,temp_c,power_w,sm_mhz,mem_mhz,util_pct,throttle,pwm,fan_rpm" > "$out" t0=$(date +%s) end=$(( t0 + minutes * 60 )) # load: long answers, thinking off, so every second is decode ( prompts=( "Write a detailed, well-commented Python implementation of a B-tree with insert, delete and range queries, followed by unit tests." "Write a long technical article explaining how TCP congestion control evolved from Tahoe to BBR, with examples." "Implement a small Lisp interpreter in C, with a REPL, garbage collection and error handling. Explain each part." ) i=0 while (( $(date +%s) < end )); do p=${prompts[i % ${#prompts[@]}]}; i=$((i + 1)) curl -s "$url/v1/chat/completions" -H 'Content-Type: application/json' \ -d "$(jq -n --arg p "$p" '{messages:[{role:"user",content:$p}], max_tokens:4096, chat_template_kwargs:{enable_thinking:false}}')" \ | jq -r '.timings | "req: \(.predicted_n) tok at \(.predicted_per_second|floor) tok/s"' || true done ) & load=$! trap 'kill $load 2>/dev/null || true' EXIT while (( $(date +%s) < end )); do now=$(( $(date +%s) - t0 )) pwm=NA; [[ -n $fan && -r $fan/$FAN_PWM ]] && pwm=$(cat "$fan/$FAN_PWM") rpm=$(fan_rpm) nvidia-smi --query-gpu=index,temperature.gpu,power.draw,clocks.sm,clocks.mem,utilization.gpu,clocks_throttle_reasons.active \ --format=csv,noheader,nounits | while IFS=', ' read -r g t p sm mem u thr; do echo "$now,$g,$t,$p,$sm,$mem,$u,$thr,$pwm,$rpm" >> "$out" done sleep 5 done wait "$load" 2>/dev/null || true jr_log "log: $out" python3 -I - "$out" <<'PY' import csv, sys rows = list(csv.DictReader(open(sys.argv[1]))) if not rows: sys.exit("no samples") t_end = max(int(r["time_s"]) for r in rows) # throttle reasons that mean heat: HW slowdown 0x8, SW thermal 0x20, HW thermal 0x40 # (0x4 = the power cap we set, 0x1 = idle: expected) HEAT = 0x8 | 0x20 | 0x40 for g in sorted({r["gpu"] for r in rows}): rs = [r for r in rows if r["gpu"] == g] temps = [int(r["temp_c"]) for r in rs] late = [int(r["temp_c"]) for r in rs if int(r["time_s"]) >= t_end - 120] power = [float(r["power_w"]) for r in rs] sm = [int(r["sm_mhz"]) for r in rs if int(r["util_pct"]) > 50] hot = sum(1 for r in rs if int(r["throttle"], 16) & HEAT) print(f"GPU {g}: max {max(temps)} C, last 2 min avg {sum(late)/len(late):.1f} C, " f"power avg {sum(power)/len(power):.0f} W / max {max(power):.0f} W, " f"SM clock under load {min(sm, default=0)}-{max(sm, default=0)} MHz, " f"thermal throttle in {hot} of {len(rs)} samples") pwm = [int(r["pwm"]) for r in rows if r["pwm"] != "NA"] rpm = [int(r["fan_rpm"]) for r in rows if r["fan_rpm"] != "NA"] if pwm: print(f"fan: pwm max {max(pwm)}/255" + (f", rpm max {max(rpm)}" if rpm else "")) PY