Preskočiť na obsah

Spúšťanie LLM pomocou vLLM

Tento dokument opisuje, ako spustiť jazykový model na GPU uzle a komunikovať s ním prostredníctvom API kompatibilného s OpenAI API. Model beží v SLURM úlohe, ktorú sami spúšťate a ukončujete. API je dostupné iba cez váš SSH tunel, takže komunikácia neprechádza mimo clustra.

Táto stránka opisuje server modelu, ktorý tvorí základ pre:

  • Qwen Code a ďalších AI programovacích agentov;
  • notebooky a skripty využívajúce OpenAI Python SDK;
  • dávkovú inferenciu a vyhodnocovacie úlohy;
  • všetkých ostatných klientov podporujúcich OpenAI API.

Ak chcete server používať z programovacieho agenta, pozrite dokument Qwen Code — jeho spúšťací skript vytvorí tunel automaticky. Pri použití iného klienta postupujte podľa časti Pripojenie klienta.

Rýchly štart

Po dokončení jednorazového nastavenia:

cd ~/llm
sbatch submit_llm.sh          # start the server (takes a few minutes)

Po skončení práce uvoľnite GPU:

scancel JOB_ID

Ak chcete server používať z programovacieho agenta, pozrite dokument Qwen Code — jeho spúšťací skript vytvorí tunel automaticky. Pri použití iného klienta postupujte podľa časti Pripojenie klienta.


Ako to funguje

Login node (x86_64)                    GPU compute node (aarch64)
+-----------------------------+        +------------------------------+
| your client                 |        | SLURM job: run_vllm.sh       |
|   agent / notebook / script |        |   +----------------------+   |
|                             |        |   | vLLM SIF             |   |
|                             |        |   |   vllm serve         |   |
|                             |        |   +----------+-----------+   |
|   127.0.0.1:LOCAL_PORT      |        |   127.0.0.1:SERVER_PORT      |
+--------------|--------------+        +--------------|---------------+
               |                                      |
               +---------- ssh -L tunnel -------------+
  • Server nie je verejne dostupný. vLLM počúva na adrese 127.0.0.1 na výpočtovom uzle, takže je dostupný iba cez váš SSH tunel. Nie je priamo vystavený do siete.
  • Životnosť modelu určuje úloha. Keď sa SLURM úloha ukončí z akéhokoľvek dôvodu vrátane dosiahnutia časového limitu, nasledujúca požiadavka klienta zlyhá.
  • Jeden server, viacero klientov. Ak má server dostatočnú kapacitu, môže ho súčasne používať viacero klientov. Každý klient nepotrebuje vlastnú GPU.
  • Uzly používajú odlišné architektúry. Prihlasovacie a CPU uzly používajú x86_64, GPU uzly aarch64. Obraz Singularity vytvorený pre jednu architektúru nebude fungovať na druhej.

Jednorazové nastavenie

1. Vytvorenie adresárovej štruktúry

mkdir -p ~/llm/jobs ~/llm/run
chmod 700 ~/llm/run
Cesta Obsah
~/llm/config.env Spoločná konfigurácia používaná všetkými skriptami
~/llm/submit_llm.sh Skript na odoslanie úlohy do SLURM
~/llm/run_vllm.sh Spúšťací skript servera, ktorý beží v rámci úlohy
~/llm/jobs/ Logy úloh, vllm-JOBID.log
~/llm/run/ Metadáta pripojenia pre jednotlivé úlohy, režim 0700

2. Vytvorenie API kľúča

vLLM vyžaduje API kľúč pri každej požiadavke. Tým sa zabráni ostatným používateľom výpočtového uzla používať vašu GPU prostredníctvom vášho portu.

openssl rand -hex 24 > ~/.llm_api_key
chmod 600 ~/.llm_api_key

Súbor config.env načítava kľúč z tohto súboru. Kľúč sa preto nenachádza priamo v konfigurácii, v obraze SIF ani v repozitári.

3. Inštalácia skriptov

Skopírujte súbory config.env, submit_llm.sh a run_vllm.sh z časti Skripty do adresára ~/llm a následne upravte:

  • config.env — Modify the LLM_PROJECT and VLLM_SIF variables.
chmod +x ~/llm/*.sh

4. Stiahnutie modelu vopred

Pred prvým spustením úlohy stiahnite model na prihlasovacom uzle, ktorý má prístup do externej siete:

source ~/llm/config.env
singularity exec "$VLLM_SIF" \
    hf download "$MODEL_NAME" --cache-dir "$HF_HUB_CACHE"

Sťahovanie v rámci úlohy zbytočne spotrebúva čas GPU a úplne zlyhá, ak výpočtové uzly nemajú prístup do externej siete. Modely s obmedzeným prístupom navyše vyžadujú huggingface-cli login alebo nastavenú premennú HF_TOKEN. Model s 27 miliardami parametrov vo formáte FP8 potrebuje približne 30 GB úložného priestoru a vyrovnávacia pamäť sa postupne zväčšuje s ďalšími modelmi.


Skripty

Riešenie pozostáva z troch súborov: config.env obsahuje nastavenia, submit_llm.sh požiada SLURM o GPU a run_vllm.sh spustí server modelu.

config.env — spoločná konfigurácia

Každý skript na tejto stránke aj na stránkach agentov načítava tento súbor, takže všetky používajú rovnaký model, vyrovnávaciu pamäť a kontajner. Súbor neobsahuje tajné údaje: API kľúč sa načítava na konci zo súboru ~/.llm_api_key, ktorý má oprávnenia 0600 a nachádza sa mimo adresára so skriptmi. Vďaka tomuto oddeleniu môžete config.env skopírovať kolegovi, uložiť do repozitára alebo vložiť do požiadavky bez rizika zverejnenia kľúča.

~/llm/config.env
# Shared configuration for vLLM and its clients on Perun.
# Sourced by run_vllm.sh and agent.sh:   source ~/llm/config.env
#
# This file holds no secrets. The API key lives in ~/.llm_api_key (0600).

# ---- Paths -------------------------------------------------------------
export LLM_HOME="${LLM_HOME:-$HOME/llm}"
export LLM_PROJECT=""                  # LLM storage - adviced to use `/project` or `/scratch` due to quota limit on `/home`
export LLM_RUNTIME="$LLM_HOME/run"     # per-job connection metadata
export LLM_LOGS="$LLM_HOME/jobs"       # SLURM job logs

# ---- Caches ------------------------------------------------------------
# Model weights land here. Keep it off $HOME.
export AI_CACHE="$LLM_PROJECT/llm/cache"
export XDG_CACHE_HOME="$AI_CACHE"
export HF_HOME="$AI_CACHE/huggingface"
export HF_HUB_CACHE="$AI_CACHE/huggingface"
export TORCH_HOME="$AI_CACHE/torch"
export TRITON_CACHE_DIR="$AI_CACHE/triton"
export CUDA_CACHE_PATH="$AI_CACHE/cuda"
export VLLM_CONFIG_ROOT="$AI_CACHE/vllm"
export FLASHINFER_CACHE_DIR="$AI_CACHE/flashinfer"

# ---- Model - Change these for other models -----------------------------
export MODEL_NAME="Qwen/Qwen3.8-27B-FP8"   # what vLLM loads - see `https://huggingface.co` for various models
export SERVED_MODEL="qwen3.8"              # what the API advertises
export MODEL_LENGTH="262144"               # Context window length
export GPU_MEMORY_UTILIZATION="0.92"       # Set hard limit to GPU memory to avoid OOM
export TENSOR_PARALLEL_SIZE="1"            # Number of used GPUs - must be same as number of GPUs requested in slurm

# Model-specific parsers - applies specifically to Qwen Code
export REASONING_PARSER="qwen3"
export TOOL_PARSER="qwen3_coder"

# ---- Server container - Change here for other images/clusters ----------
export VLLM_SIF="/apps/images/vllm/aarch64/vllm_26.04-py3-xxhash.sif"
export QWEN_SIF_X86="/apps/images/vllm/x86_64/Qwen_code_x86_64_Nov2026.sif"

# ---- API key -----------------------------------------------------------
# Create once:
#   openssl rand -hex 24 > ~/.llm_api_key && chmod 600 ~/.llm_api_key
if [[ -r "$HOME/.llm_api_key" ]]; then
    KEY=$(< "$HOME/.llm_api_key")
    export KEY
fi

# The server port is chosen by run_vllm.sh and published in $LLM_RUNTIME.
# The local tunnel port is chosen by the client. Neither is configured here.

Devana cluster

On Devana cluster singularity containers can be found at /

submit_llm.sh — požiadavka na výpočtové zdroje

Tento skript iba požiada SLURM o GPU a odovzdá riadenie skriptu run_vllm.sh. Vďaka jednoduchému obsahu môžete meniť spôsob spúšťania servera bez potreby upravovať odosielací skript. Skript run_vllm.sh môžete na účely ladenia spustiť aj pomocou srun.

~/llm/submit_llm.sh
#!/bin/bash
# Submit with:  sbatch submit_llm.sh
#
# #SBATCH lines are read by SLURM before any shell expansion, so $HOME and
# $USER do NOT work here. Edit the absolute paths below to your own login.

#SBATCH --account=                    # Slurm account
#SBATCH --job-name=vllm
#SBATCH --partition=gpu_short
#SBATCH --gres=gpu:1                  # Request one GPU
#SBATCH --ntasks=1                    
#SBATCH --cpus-per-task=72            # Corresponds to one GPU
#SBATCH --time=04:00:00
#SBATCH --output=jobs/%x-%j.log
#SBATCH --error=jobs/%x-%j.log

bash run_vllm.sh

run_vllm.sh — server modelu

Skript beží v rámci alokácie na GPU uzle. Vykonáva tieto kroky:

  1. Sources the configuration and refuses to continue without an API key or a SLURM_JOB_ID — the latter catches the common mistake of running it directly on a login node, where it would find no GPU.
  2. Nastaví automatické čistenie metadátového súboru, aby po zrušení alebo páde úlohy nezostal súbor nesprávne indikujúci, že server stále beží.
  3. Požiada jadro systému o voľný port, čím zabráni kolíziám medzi úlohami.
  4. Zverejní údaje o pripojení v súbore ~/llm/run/JOBID.job pri použití umask 077, takže informácie o serveri môžete čítať iba vy.
  5. Zostaví zoznam argumentov a pridá parser uvažovania a parser volania nástrojov iba vtedy, ak sú nakonfigurované.
~/llm/run_vllm.sh
#!/bin/bash
# Runs vLLM inside a detached tmux session within the SLURM allocation.
# SLURM job remains active while the tmux session is running.
set -euo pipefail

# Load configuration
source "${LLM_HOME:-$HOME/llm02}/config.env"

# Validate required variables
: "${KEY:?No API key. Create ~/.llm_api_key -- see config.env.}"
: "${SLURM_JOB_ID:?Run this through sbatch, not directly.}"

# Prepare runtime directories
mkdir -p "$LLM_RUNTIME" "$LLM_LOGS"
chmod 700 "$LLM_RUNTIME"

# Use a job-specific session name to allow multiple vLLM jobs per user
SESSION="vllm-${SLURM_JOB_ID}"

# Metadata file
META="$LLM_RUNTIME/$SLURM_JOB_ID.job"

# Remove metadata when the SLURM job exits
trap 'rm -f "$META"' EXIT

# Let the kernel pick a free port on this compute node
SERVER_PORT=$(python3 -c '
import socket
with socket.socket() as s:
    s.bind(("127.0.0.1", 0))
    print(s.getsockname()[1])
')

# Export the selected port for other scripts
export SERVER_PORT

# Write job metadata
umask 077
cat > "$META" <<EOF
JOB_ID=$SLURM_JOB_ID
NODE=$(hostname -s)
SERVER_PORT=$SERVER_PORT
SERVED_MODEL=$SERVED_MODEL
MODEL_NAME=$MODEL_NAME
STARTED=$(date -Is)
EOF

echo "Starting vLLM: $(hostname -s):$SERVER_PORT"
echo "Model: $MODEL_NAME"
echo "Session: $SESSION"

# Build vLLM arguments
args=(
    --served-model-name "$SERVED_MODEL"
    --host 127.0.0.1
    --port "$SERVER_PORT"
    --tensor-parallel-size "$TENSOR_PARALLEL_SIZE"
    --gpu-memory-utilization "$GPU_MEMORY_UTILIZATION"
    --max-model-len "$MODEL_LENGTH"
    --enable-prefix-caching
    --prefix-caching-hash-algo xxhash
)

# Optional reasoning parser
if [[ -n "${REASONING_PARSER:-}" ]]; then
    args+=(--reasoning-parser "$REASONING_PARSER")
fi

# Optional tool calling support
if [[ -n "${TOOL_PARSER:-}" ]]; then
    args+=(--enable-auto-tool-choice)
    args+=(--tool-call-parser "$TOOL_PARSER")
fi

# Optional chat template
if [[ -n "${CHAT_TEMPLATE:-}" ]]; then
    args+=(--chat-template "$CHAT_TEMPLATE")
fi

# Export API key through the tmux session environment
# This avoids exposing the key in the process command line
export VLLM_API_KEY="$KEY"

# Prepare command with proper shell escaping
printf -v MODEL_Q '%q' "$MODEL_NAME"
printf -v IMAGE_Q '%q' "$VLLM_SIF"
printf -v CACHE_Q '%q' "$AI_CACHE"

CMD="
module load singularity
unset LD_PRELOAD


singularity exec --nv \
    --env PYTHONNOUSERSITE=1 \
    --bind ${CACHE_Q}:${CACHE_Q} \
    ${IMAGE_Q} \
    vllm serve ${MODEL_Q}"

# Append arguments with shell-safe quoting
for arg in "${args[@]}"; do
    printf -v ARG_Q '%q' "$arg"
    CMD+=" $ARG_Q"
done

# Replace any existing session with the same job ID
tmux kill-session -t "$SESSION" 2>/dev/null || true

# Start detached tmux session with the API key in its environment
tmux new-session -d -s "$SESSION" -e "VLLM_API_KEY=$VLLM_API_KEY"

# Execute vLLM inside tmux
tmux send-keys -t "$SESSION" "$CMD" C-m

echo "vLLM running in tmux session: $SESSION"
echo "Node: $(hostname -s)"
echo "Port: $SERVER_PORT"
echo "Metadata: $META"

# Keep SLURM allocation alive while tmux session exists
while tmux has-session -t "$SESSION" 2>/dev/null; do
    sleep 30
done

echo "vLLM tmux session terminated."

Spustenie servera

Spustenie

cd ~/llm
sbatch submit_llm.sh
squeue -u "$USER"

Načítanie trvá niekoľko minút; pri prázdnej vyrovnávacej pamäti aj dlhšie. Priebeh sledujte pomocou:

tail -f ~/llm/jobs/vllm-JOBID.log

Server je pripravený, keď log zobrazí API routes a hlásenie Application startup complete.

Overenie funkčnosti

Pred ladením klienta overte, či model odpovedá. Takto jednoznačne odlíšite problém servera od problému klienta.

Health check from a login node
source ~/llm/config.env

# Read the node and port the job published.
NODE=$(sed -n 's/^NODE=//p'        ~/llm/run/JOBID.job)
PORT=$(sed -n 's/^SERVER_PORT=//p' ~/llm/run/JOBID.job)

ssh -N -L 9999:127.0.0.1:$PORT $NODE &
TUNNEL=$!

# 1. Is the model loaded and advertised?
curl -s -H "Authorization: Bearer $KEY" http://127.0.0.1:9999/v1/models

# 2. Does it generate?
curl -s http://127.0.0.1:9999/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"model\":\"$SERVED_MODEL\",
       \"messages\":[{\"role\":\"user\",\"content\":\"Say hello.\"}]}"

kill $TUNNEL

Ak /v1/models vypíše váš model a požiadavka na vytvorenie odpovede vráti text, server funguje správne.

Zastavenie

scancel JOB_ID

Porty

Oba porty sa prideľujú automaticky. Je vhodné rozumieť tomuto mechanizmu, pretože práve pri práci s portami vzniká väčšina problémov s pripojením.

Port servera: Skript run_vllm.sh požiada na výpočtovom uzle o port 0, čím nechá jadro systému vybrať voľný port. Výsledok zverejní v metadátovom súbore úlohy. Kolízie medzi úlohami sú týmto spôsobom vylúčené.

Lokálny port: Každý klient postupuje rovnako na prihlasovacom uzle, takže vedľa seba môže bežať viacero relácií.

Metadátový súbor

One file per job, written by the server and read by every client:

~/llm/run/123456.job
JOB_ID=123456
NODE=gn003
SERVER_PORT=18013
SERVED_MODEL=qwen3.8
MODEL_NAME=Qwen/Qwen3.8-27B-FP8
STARTED=2026-10-28T10:14:02+01:00

Súbor sa vytvorí po zistení portu a odstráni sa pri ukončení úlohy. Klienti ho majú parsovať, nie načítavať pomocou source, a pred použitím majú overiť, či úloha stále beží. Súbor v adresári s právom zápisu by nemal mať možnosť spúšťať kód a úloha môže skončiť bez odstránenia svojho metadátového súboru.


Pripojenie klienta

Funguje každý klient kompatibilný s OpenAI API. Postup pripojenia je vždy rovnaký:

  1. Načítajte hodnoty NODE a SERVER_PORT z metadátového súboru úlohy.
  2. Overte, či úloha stále beží (squeue -h -j JOBID).
  3. Vyberte voľný lokálny port.
  4. Otvorte SSH tunel pomocou ssh -N -L 127.0.0.1:LOCAL:127.0.0.1:SERVER_PORT NODE.
  5. Nastavte klienta tak, aby používal http://127.0.0.1:LOCAL/v1, hodnotu $KEY ako API kľúč a $SERVED_MODEL ako názov modelu.
  6. Po ukončení klienta zatvorte tunel.

Qwen Code obsahuje spúšťací skript, ktorý vykoná všetkých šesť krokov automaticky. Pri ostatných klientoch postupujte manuálne:

From a Python script or notebook
# Terminal 1 — open the tunnel.
source ~/llm/config.env
NODE=$(sed -n 's/^NODE=//p'        ~/llm/run/JOBID.job)
PORT=$(sed -n 's/^SERVER_PORT=//p' ~/llm/run/JOBID.job)
ssh -N -L 9999:127.0.0.1:$PORT $NODE
# Terminal 2 — talk to it.
import os
from openai import OpenAI

client = OpenAI(
    base_url="http://127.0.0.1:9999/v1",
    api_key=os.environ["KEY"],
)

reply = client.chat.completions.create(
    model=os.environ["SERVED_MODEL"],
    messages=[{"role": "user", "content": "Summarise this log file."}],
)
print(reply.choices[0].message.content)

Výber modelu

MODEL_NAME označuje model, ktorý vLLM načíta; SERVED_MODEL je názov, ktorý API sprístupňuje a ktorý musí klient požadovať. Tieto názvy sa nemusia zhodovať.

export MODEL_NAME="organization/model-repository"
export SERVED_MODEL="my-model"

Pred výberom modelu overte, či:

  • nainštalovaná verzia vLLM podporuje jeho architektúru;
  • máte prístup k váham modelu (pri niektorých modeloch je potrebné odsúhlasiť licenciu);
  • sa model spolu s KV cache zmestí do pamäte GPU pri požadovanej dĺžke kontextu;
  • podporuje volanie nástrojov, ak ho budú používať agenti;
  • má dostupnú chat šablónu alebo ju zadáte manuálne.

Spotrebu pamäte určujú najmä váhy modelu a KV cache, ktorá rastie s hodnotou MODEL_LENGTH. Kontext s dĺžkou 262144 tokenov je náročný na pamäť. Ak sa model nezmestí do GPU pamäte, najskôr znížte MODEL_LENGTH a až potom upravujte GPU_MEMORY_UTILIZATION.

Funkčnosť agentov závisí od podpory volania nástrojov

Bez tejto podpory môže model o kóde diskutovať, ale nedokáže čítať ani upravovať súbory. Volanie nástrojov musí podporovať samotný model, chat šablóna aj zodpovedajúci parameter --tool-call-parser vo vLLM. Ak model poskytujete pre agentov, túto podporu vždy overte.


Prispôsobenie obrazu vLLM

Vo väčšine prípadov to nie je potrebné. Na zmenu modelu stačí upraviť config.env a reštartovať server.

Vlastný obraz servera vytvorte vtedy, keď potrebujete inú verziu vLLM, ďalšie Python balíky alebo závislosti špecifické pre model. Začnite kompatibilným základným obrazom CUDA, nainštalujte verziu vLLM vhodnú pre architektúru GPU a obraz vytvorte pre aarch64 (GPU uzly Perun) alebo x86_64 (GPU uzly Devana). Váhy modelu ponechajte mimo obrazu — patria do vyrovnávacej pamäte Hugging Face, ktorá sa pripája pri spustení kontajnera. Obraz tak zostane čo najmenší.


Riešenie problémov

Úloha sa spustí a okamžite skončí

Podrobnosti o zlyhaní nájdete v logu:

tail -50 ~/llm/jobs/vllm-JOBID.log

Bežnými príčinami sú model nepodporovaný nainštalovanou verziou vLLM, nesprávna hodnota MODEL_NAME alebo chýbajúce váhy, ktoré nemožno stiahnuť pre nedostupné sieťové pripojenie.

Metadátový súbor sa nevytvorí

Úloha ešte nedospela do fázy zverejnenia portu alebo počas spúšťania skončila. Úloha v stave PD čaká vo fronte; nejde o chybu:

squeue -u "$USER"

Model sa nezmestí do pamäte GPU

Log uvádza alokáciu pamäte, ktorá zlyhala. Odporúčaný postup je: najskôr znížiť MODEL_LENGTH (zvyčajne dominuje spotreba KV cache), potom znížiť GPU_MEMORY_UTILIZATION a napokon zvoliť menší alebo viac kvantizovaný model. Zvýšenie GPU_MEMORY_UTILIZATION nad približne 0.95 často namiesto čistého zlyhania pri štarte spôsobí pád servera počas záťaže.

Načítanie trvá dlhšie, než sa očakávalo

Načítanie veľkého modelu z prázdnej vyrovnávacej pamäte môže trvať mnoho minút. Stiahnite váhy vopred na prihlasovacom uzle a až potom spustite úlohu. Ak sa log zastaví bez chybového hlásenia, príčinou je zvyčajne nedostatok pamäte — pozrite predchádzajúcu časť.

`401 Unauthorized`

Klient odosiela nesprávny kľúč alebo neposiela žiadny. Obe strany načítavajú ~/.llm_api_key prostredníctvom config.env. Overte, či súbor existuje, je čitateľný a či ste v testovacom shelli načítali config.env.

`Address already in use`

Pri portoch prideľovaných jadrom systému by k tejto chybe nemalo dochádzať. Ak sa objaví, iný proces pravdepodobne obsadil port v čase medzi jeho uvoľnením a použitím SSH alebo vLLM. Skúste to znova. Ak sa problém opakuje, skontrolujte, či nezostal otvorený starý tunel:

pgrep -af "ssh.*-L 127.0.0.1"

Klient prestane počas relácie fungovať

Úloha pravdepodobne dosiahla časový limit:

sacct -j JOBID --format=JobID,State,Elapsed,Timelimit

Submit a new job and reconnect.


Referenčná príručka

Konfiguračné premenné

Variable Purpose
MODEL_NAME Hugging Face repository vLLM loads
SERVED_MODEL Name the API advertises; clients must request it
MODEL_LENGTH Maximum context in tokens; drives KV cache size
GPU_MEMORY_UTILIZATION Fraction of GPU memory vLLM may use
TENSOR_PARALLEL_SIZE GPUs per server; must match --gres
REASONING_PARSER Model-specific; leave unset unless required
TOOL_PARSER Required for agent file editing; model-specific
AI_CACHE Root of all caches, including model weights
VLLM_SIF Server image, aarch64
QWEN_SIF_X86 Qwen Code client image, x86_64
KEY API key, read from ~/.llm_api_key

Príkazy

Task Command
Start a server sbatch submit_llm.sh
List your jobs squeue -u "$USER"
Follow a job log tail -f ~/llm/jobs/vllm-JOBID.log
See where a server landed cat ~/llm/run/JOBID.job
Stop a server scancel JOBID
Inspect a finished job sacct -j JOBID --format=JobID,State,Elapsed

Odkazy

Created by: Marek Štekláč