Preskočiť na obsah

Pripojenie Qwen Code k vLLM na clustri Perun

Toto riešenie umožňuje programu Qwen Code spustenému v lokálnom kontajneri Singularity pripojiť sa k inštancii vLLM bežiacej na výpočtovom GPU uzle HPC clustra Perun.

Komunikácia medzi pracovnou stanicou a clustrom prebieha prostredníctvom SSH tunela. API vLLM nie je vystavené do externej siete a na pridelenom výpočtovom uzle počúva výhradne na adrese 127.0.0.1.

Architektúra

Pripojenie pozostáva z troch komponentov:

  • Pracovná stanica: Spúšťa qwen_remote.sh a Qwen Code v lokálnom kontajneri Singularity.
  • Perun login node: Provides SSH access to the cluster, SLURM job discovery and an intermediate SSH tunnel.
  • Výpočtový uzol: Spúšťa server vLLM v rámci SLURM alokácie.

Schéma pripojenia:

Workstation                    Perun login node       Compute node
------------------------       ----------------       ------------
qwen_remote.sh                 SSH                    vLLM
Singularity container   --->   tunnel          --->   127.0.0.1:<port>
127.0.0.1:<local_port>

SSH tunel pozostáva z dvoch stupňov presmerovania:

  1. Pracovná stanica presmeruje svoj lokálny port na port prihlasovacieho uzla Perun.
  2. Prihlasovací uzol presmeruje tento port na server vLLM na pridelenom výpočtovom uzle.

Všetky parametre pripojenia sa zisťujú počas behu. Klient automaticky identifikuje:

  • bežiacu SLURM úlohu vLLM;
  • pridelený výpočtový uzol;
  • port servera vLLM;
  • názov sprístupneného modelu;
  • API kľúč.

Vďaka tomu nie je pri zmene modelu, opätovnom spustení úlohy ani pridelení iného výpočtového uzla potrebné meniť konfiguráciu pracovnej stanice.

1. Požiadavky

Pracovná stanica

Pracovná stanica potrebuje:

  • SSH klienta;
  • Singularity/Apptainer;
  • nástroj curl;
  • Bash;
  • lokálny obraz Singularity s Qwen Code.

Perun

Cluster vyžaduje:

  • SSH prístup k prihlasovaciemu uzlu Perun;
  • SLURM;
  • bežiacu úlohu vLLM;
  • Python 3 na prihlasovacom uzle (používa sa na výber voľného sprostredkujúceho portu);
  • SSH pripojenie bez zadávania hesla z prihlasovacieho uzla na výpočtové uzly.

Štruktúra súborov

Súbory na pracovnej stanici majú nasledujúcu štruktúru:

~/llm/
├── config.env
├── qwen_remote.sh
└── Containers/
    └── Qwen_code_x86_64_Nov2026.sif

Skripty možno uložiť aj na iné miesto. Skript qwen_remote.sh predvolene hľadá súbor config.env v rovnakom adresári.

Cestu ku konfiguračnému súboru možno zmeniť pomocou:

export QWEN_REMOTE_CONFIG=/path/to/config.env

Obraz Singularity a integrácia so SLURM

Dodávaný obraz Singularity je určený na spúšťanie Qwen Code na pracovnej stanici a na pripojenie k vzdialenej službe vLLM na clustri Perun.

Nemusí však obsahovať klientske nástroje SLURM ani konfiguráciu potrebnú na komunikáciu s lokálnym HPC clusterom.

Ak má Qwen Code komunikovať s lokálnym prostredím SLURM, je potrebná ďalšia konfigurácia.

Existujú dva možné postupy:

  • Pripojenie existujúcej inštalácie SLURM: Do kontajnera pripojte klientske binárne súbory SLURM, knižnice a konfiguračné súbory z hostiteľa. Kontajner musí mať kompatibilné závislosti a prístup k príslušným autentifikačným mechanizmom.

  • Vytvorenie samostatného obrazu: Vytvorte vlastný obraz Singularity, ktorý bude obsahovať potrebné klientske nástroje SLURM, knižnice a konfiguráciu.

Potrebné nastavenie závisí od lokálnej inštalácie SLURM, autentifikačného mechanizmu a konfigurácie clustra.

Pri vzdialenej práci s clustrom Perun cez uvedený SSH tunel nemusí byť SLURM nainštalovaný v kontajneri na pracovnej stanici. Príkazy SLURM sa vykonávajú vzdialene prostredníctvom SSH.

2. Konfigurácia SSH

Vyžaduje sa SSH prístup bez zadávania hesla, pretože spúšťací skript pri automatizovaných operáciách SSH používa parameter BatchMode=yes.

2.1. Vytvorenie SSH kľúča

Na pracovnej stanici vytvorte samostatný SSH kľúč:

ssh-keygen -t ed25519 -C "user@workstation -> perun" \
    -f ~/.ssh/id_ed25519_perun

Kľúč môže byť chránený heslovou frázou. V takom prípade musí pred spustením Qwen Code bežať SSH agent a kľúč musí byť doň načítaný.

Príkaz vytvorí tieto súbory:

  • ~/.ssh/id_ed25519_perun – súkromný kľúč;
  • ~/.ssh/id_ed25519_perun.pub – verejný kľúč.

Súkromný kľúč musí zostať na pracovnej stanici.

2.2. Inštalácia verejného kľúča

Skopírujte verejný kľúč na prihlasovací uzol Perun:

ssh-copy-id -i ~/.ssh/id_ed25519_perun.pub \
    user@<perun-login-hostname>

Prípadne ho môžete nainštalovať manuálne:

cat ~/.ssh/id_ed25519_perun.pub | \
    ssh user@<perun-login-hostname> \
    'mkdir -p ~/.ssh && chmod 700 ~/.ssh &&
     cat >> ~/.ssh/authorized_keys &&
     chmod 600 ~/.ssh/authorized_keys'

2.3. Konfigurácia SSH

Na pracovnej stanici pridajte do súboru ~/.ssh/config nasledujúcu položku:

Host perun
    HostName <perun-login-hostname>
    User <username>
    IdentityFile ~/.ssh/id_ed25519_perun
    IdentitiesOnly yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

    # Reuse an existing SSH connection.
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m

Nastavte príslušné oprávnenia:

chmod 600 ~/.ssh/config

Otestujte pripojenie:

ssh perun true && echo "login ok"

Príkaz sa musí dokončiť bez požiadavky na zadanie hesla.

Alias SSH (v tomto príklade perun) sa používa v súbore config.env na pracovnej stanici.

2.4. Konfigurácia SSH z prihlasovacieho uzla na výpočtové uzly

Na rozdiel od priameho pripojenia SSH ProxyJump používa aktuálna implementácia prihlasovací uzol Perun na vytvorenie druhého SSH spojenia.

Prihlasovací uzol preto musí byť schopný pripojiť sa k prideleným výpočtovým uzlom bez požiadavky na zadanie hesla.

Otestujte pripojenie:

ssh perun "ssh -o BatchMode=yes <compute-node> hostname"

Ak sa príkaz úspešne dokončí, ďalšia konfigurácia nie je potrebná.

Ak sa vyžaduje heslo, nakonfigurujte na clustri Perun samostatný interný SSH kľúč.

Prihláste sa na cluster:

ssh perun

Vytvorte interný kľúč:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_internal -N ""

Nainštalujte verejný kľúč:

cat ~/.ssh/id_ed25519_internal.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Nakonfigurujte SSH tak, aby používalo interný kľúč:

cat >> ~/.ssh/config <<'EOF'
Host *
    IdentityFile ~/.ssh/id_ed25519_internal
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
EOF

chmod 600 ~/.ssh/config

Z pracovnej stanice znovu otestujte pripojenie:

ssh perun "ssh -o BatchMode=yes <compute-node> hostname"

Príkaz sa musí dokončiť bez požiadavky na zadanie hesla.

Výpočtové uzly môžu opätovne generovať svoje SSH hostiteľské kľúče. Spúšťací skript preto pri internom spojení vypína overovanie hostiteľského kľúča.

3. Konfigurácia API kľúča

API kľúč je spoločný pre server vLLM a klienta Qwen Code.

Na clustri Perun je uložený v súbore:

~/.llm_api_key

Kľúč vytvorte jednorazovo:

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

Serverový skript run_vllm.sh načíta tento súbor pri spustení vLLM.

Skript qwen_remote.sh na pracovnej stanici načíta ten istý kľúč cez SSH pri každom spustení.

Tým sa zabezpečí jediný zdroj pravdy a API kľúče sa nemusia ukladať do konfigurácie pracovnej stanice.

Voliteľne možno v config.env nastaviť lokálny súbor s kľúčom pomocou premennej LOCAL_KEY_FILE. Ak je nastavený a čitateľný, má prednosť pred kľúčom načítaným zo vzdialeného systému.

Dôležité: API kľúč použitý klientom sa musí zhodovať s kľúčom nastaveným pre bežiaci server vLLM. V opačnom prípade vLLM odmietne požiadavky s chybou HTTP 401.

4. Konfigurácia pracovnej stanice

Konfigurácia pracovnej stanice je uložená v súbore config.env.

Súbor definuje:

  • parametre SSH pripojenia;
  • názov SLURM úlohy;
  • umiestnenie vzdialených runtime metadát;
  • umiestnenie API kľúča;
  • lokálny port;
  • lokálny obraz Singularity;
  • ďalšie pripojenia adresárov do kontajnera;
  • nastavenia terminálu.

Konfigurácia nezávisí od sprístupneného modelu. Názov modelu sa načíta z metadát vytvorených skriptom run_vllm.sh.

config.env

# Workstation-side configuration for connecting Qwen Code to a vLLM job
# running on Perun. Sourced by qwen_remote.sh.

# Nothing here is a secret: the API key is read from the cluster at runtime.

# ---- Cluster access ----------------------------------------------------

# LOGIN_ALIAS is the Host entry in ~/.ssh/config.
export LOGIN_ALIAS="perun"
export LOGIN_USER="username"

# Name of the SLURM job that runs vLLM.
export VLLM_JOB_NAME="vllm"

# Remote runtime metadata directory.
# Must match LLM_RUNTIME in the Perun-side config.env.
export REMOTE_RUNTIME="\$HOME/llm/run"

# Remote API key file.
export REMOTE_KEY_FILE="\$HOME/.llm_api_key"

# Optional local API key file.
# When readable, this takes precedence over the remote key.
# export LOCAL_KEY_FILE="$HOME/.llm_api_key"

# ---- Tunnel ------------------------------------------------------------

# Local port Qwen Code talks to.
# Set to 0 to let the script pick a free one.
export LOCAL_PORT="44044"

# SSH options used for the compute node.
# Compute nodes often regenerate their host keys.
export NODE_SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"

# Maximum time to wait for the vLLM API endpoint.
export WAIT_TIMEOUT="60"

# ---- Model -------------------------------------------------------------

# The served model is retrieved from the job metadata.
# Set this variable only when an explicit override is required.
# export SERVED_MODEL="qwen3.8"

# ---- Local Qwen Code container -----------------------------------------

# Local Singularity image containing Qwen Code.
export QWEN_SIF_LOCAL="$HOME/llm/Containers/Qwen_code_x86_64_Nov2026.sif"

# Additional bind mounts (space-separated paths).
export QWEN_BINDS="/apps"

# ---- Terminal ----------------------------------------------------------

# Flickering bug fix.
export QWEN_CODE_FORCE_SYNCHRONIZED_OUTPUT=1
export TERM=xterm-256color

5. Spustenie servera vLLM

Server vLLM sa spúšťa prostredníctvom SLURM pomocou odosielacích skriptov umiestnených na clustri.

Napríklad:

ssh perun 'cd ~/llm && sbatch submit_llm.sh'

Skontrolujte stav úlohy:

ssh perun 'squeue -u username -n vllm'

Počkajte, kým úloha dosiahne stav R (RUNNING).

Prvé spustenie môže trvať dlhšie, pretože váhy modelu sa musia stiahnuť alebo načítať do pamäte GPU.

Sledujte log úlohy:

ssh perun 'tail -f ~/llm/jobs/vllm-<jobid>.log'

Runtime metadáta

Server zverejňuje informácie o svojom behu v súbore:

~/llm/run/<jobid>.job

Metadátový súbor vytvorí skript run_vllm.sh pri spustení servera a odstráni ho pri ukončení úlohy.

It contains the following information:

Premenná Opis
JOB_ID ID SLURM úlohy.
NODE Pridelený výpočtový uzol.
SERVER_PORT Port používaný vLLM.
SERVED_MODEL Identifikátor modelu sprístupnený prostredníctvom API.
MODEL_NAME Názov modelu používaný v logoch a pri zobrazovaní.
STARTED Časová značka spustenia servera.

Klient používa tento súbor ako jediný zdroj údajov o parametroch pripojenia.

Fronta SLURM sa používa iba na zistenie ID bežiacej úlohy.

6. Spúšťací skript na pracovnej stanici

Spúšťacím skriptom na pracovnej stanici je qwen_remote.sh.

Jeho hlavné úlohy sú:

  1. Načítať konfiguráciu pracovnej stanice.
  2. Identifikovať bežiacu SLURM úlohu vLLM.
  3. Načítať metadáta úlohy z clustra Perun.
  4. Načítať API kľúč.
  5. Vybrať voľný lokálny port.
  6. Prideliť sprostredkujúci port na clustri Perun.
  7. Vytvoriť dvojúrovňový SSH tunel.
  8. Počkať, kým bude API vLLM dostupné.
  9. Spustiť Qwen Code v lokálnom kontajneri Singularity.

Spúšťací skript nevyžaduje manuálne nastavenie výpočtového uzla, vzdialeného portu ani sprístupneného modelu.

qwen_remote.sh

#!/bin/bash
# Connect Qwen Code on this workstation to a vLLM job running on Perun.
#
# Usage:
#   qwen_remote.sh [workspace] [--continue|--new|--resume|--list]
#
# The script finds the running vLLM job, reads the node, port and served
# model from the job's metadata file, opens an SSH tunnel to it and starts
# Qwen Code inside the local Singularity container.
#
# The tunnel is two hops in one SSH process:
#
#   workstation:LOCAL_PORT -> perun:MID_PORT -> node:SERVER_PORT
#
# The second hop is made by Perun, so it uses the key stored on Perun.
# A direct 'ssh -J perun node' would instead make the node authenticate
# this workstation, which the compute nodes do not allow.

set -Eeuo pipefail

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
CONFIG="${QWEN_REMOTE_CONFIG:-$SCRIPT_DIR/config.env}"

if [[ ! -r "$CONFIG" ]]; then
    echo "ERROR: Cannot read configuration: $CONFIG" >&2
    exit 1
fi

# Load workstation configuration.
# shellcheck disable=SC1090
source "$CONFIG"

: "${LOGIN_ALIAS:?LOGIN_ALIAS not set in $CONFIG}"
: "${QWEN_SIF_LOCAL:?QWEN_SIF_LOCAL not set in $CONFIG}"

VLLM_JOB_NAME="${VLLM_JOB_NAME:-vllm}"
REMOTE_RUNTIME="${REMOTE_RUNTIME:-\$HOME/llm/run}"
REMOTE_KEY_FILE="${REMOTE_KEY_FILE:-\$HOME/.llm_api_key}"
LOCAL_PORT="${LOCAL_PORT:-0}"
WAIT_TIMEOUT="${WAIT_TIMEOUT:-60}"
QWEN_BINDS="${QWEN_BINDS:-}"

# Compute nodes regenerate their host keys, so the login node must not
# check them against its known_hosts.
NODE_SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"

usage() {
    cat <<USAGE
Usage:
  $0 [workspace]                 interactive session menu
  $0 [workspace] --continue      resume the most recent session
  $0 [workspace] --new           start a new session
  $0 [workspace] --resume        open the Qwen session selector
  $0 [workspace] --list          list sessions
USAGE
}

# ---------------------------------------------------------------------------
# Arguments: a workspace path and/or a mode flag, in any order.
# ---------------------------------------------------------------------------

WORKSPACE=""
MODE=""

for arg in "$@"; do
    case "$arg" in
        --continue|--new|--resume|--list)
            MODE="$arg"
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        -*)
            echo "ERROR: Unknown option: $arg" >&2
            usage >&2
            exit 1
            ;;
        *)
            WORKSPACE="$arg"
            ;;
    esac
done

WORKSPACE=${WORKSPACE:-$PWD}

if [[ ! -d "$WORKSPACE" ]]; then
    echo "ERROR: Workspace does not exist: $WORKSPACE" >&2
    exit 1
fi

WORKSPACE=$(cd "$WORKSPACE" && pwd)

case "$MODE" in
    --continue) QWEN_ARGS=(--continue) ;;
    --new)      QWEN_ARGS=() ;;
    --resume)   QWEN_ARGS=(--resume) ;;
    --list)     QWEN_ARGS=(sessions list) ;;
    "")
        echo
        echo "Qwen session:"
        echo
        echo "  1) Continue last session"
        echo "  2) Start new session"
        echo "  3) Select previous session"
        echo "  4) List sessions"
        echo "  5) Exit"
        echo
        read -r -p "Select [1]: " CHOICE
        CHOICE=${CHOICE:-1}
        case "$CHOICE" in
            1) QWEN_ARGS=(--continue) ;;
            2) QWEN_ARGS=() ;;
            3) QWEN_ARGS=(--resume) ;;
            4) QWEN_ARGS=(sessions list) ;;
            5) exit 0 ;;
            *) echo "ERROR: Invalid selection." >&2; exit 1 ;;
        esac
        ;;
esac

# ---------------------------------------------------------------------------
# Ask Perun about the running vLLM job.
#
# run_vllm.sh writes $LLM_RUNTIME/<jobid>.job with NODE, SERVER_PORT and
# SERVED_MODEL, and removes it when the job ends. That file is the source
# of truth; squeue is only used to find the job id.
# ---------------------------------------------------------------------------

echo "Looking for a running vLLM job on $LOGIN_ALIAS ..."

REMOTE_SCRIPT=$(cat <<REMOTE
set -eu
RUNTIME="$REMOTE_RUNTIME"
KEYFILE="$REMOTE_KEY_FILE"
JOB=\$(squeue -u "\$USER" -h -n "$VLLM_JOB_NAME" -t R -o '%i' | head -1)
if [ -z "\$JOB" ]; then
    echo "ERR=nojob"
    exit 0
fi
META="\$RUNTIME/\$JOB.job"
if [ ! -r "\$META" ]; then
    echo "ERR=nometa"
    echo "JOB_ID=\$JOB"
    echo "NODE=\$(squeue -j "\$JOB" -h -o '%N')"
    exit 0
fi
cat "\$META"
# A free port on the login node, for the middle of the tunnel.
echo "MID_PORT=\$(python3 -c "import socket; s=socket.socket(); s.bind(('127.0.0.1',0)); print(s.getsockname()[1]); s.close()")"
if [ -r "\$KEYFILE" ]; then
    echo "API_KEY=\$(cat "\$KEYFILE")"
else
    echo "ERR=nokey"
fi
REMOTE
)

REMOTE_INFO=$(ssh -o BatchMode=yes "$LOGIN_ALIAS" "bash -s" <<<"$REMOTE_SCRIPT") || {
    echo "ERROR: Cannot reach $LOGIN_ALIAS over SSH (passwordless login not working?)." >&2
    echo "       Try:  ssh -v $LOGIN_ALIAS true" >&2
    exit 1
}

JOB_ID=""; NODE=""; SERVER_PORT=""; MID_PORT=""; API_KEY=""; ERR=""
SERVED_MODEL=""; MODEL_NAME=""; STARTED=""

while IFS='=' read -r key value; do
    case "$key" in
        JOB_ID)       JOB_ID="$value" ;;
        NODE)         NODE="$value" ;;
        SERVER_PORT)  SERVER_PORT="$value" ;;
        MID_PORT)     MID_PORT="$value" ;;
        SERVED_MODEL) SERVED_MODEL="$value" ;;
        MODEL_NAME)   MODEL_NAME="$value" ;;
        STARTED)      STARTED="$value" ;;
        API_KEY)      API_KEY="$value" ;;
        ERR)          ERR="$value" ;;
    esac
done <<<"$REMOTE_INFO"

case "$ERR" in
    nojob)
        echo "ERROR: No running '$VLLM_JOB_NAME' job found on $LOGIN_ALIAS." >&2
        echo "       Start one with:  ssh $LOGIN_ALIAS 'cd ~/llm && sbatch submit_llm.sh'" >&2
        exit 1
        ;;
    nometa)
        echo "ERROR: Job $JOB_ID runs on ${NODE:-?} but its metadata file is missing." >&2
        echo "       vLLM may still be starting, or run_vllm.sh failed. Check:" >&2
        echo "       ssh $LOGIN_ALIAS 'tail -n 40 ~/llm/jobs/vllm-$JOB_ID.log'" >&2
        exit 1
        ;;
    nokey)
        echo "ERROR: The API key file does not exist on the cluster." >&2
        echo "       Create it with:" >&2
        echo "       ssh $LOGIN_ALIAS 'openssl rand -hex 24 > ~/.llm_api_key && chmod 600 ~/.llm_api_key'" >&2
        echo "       then restart the vLLM job so it picks the key up." >&2
        exit 1
        ;;
esac

: "${NODE:?Could not determine the compute node}"
: "${SERVER_PORT:?Could not determine the vLLM port}"
: "${MID_PORT:?Could not get a free port on $LOGIN_ALIAS}"
: "${API_KEY:?Could not determine the API key}"
: "${SERVED_MODEL:?Could not determine the served model name}"

echo "Job $JOB_ID on $NODE, port $SERVER_PORT, model $SERVED_MODEL (${MODEL_NAME:-?})"

# ---------------------------------------------------------------------------
# Pick a free local port.
# ---------------------------------------------------------------------------

port_free() {
    ! (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null
}

if [[ "$LOCAL_PORT" == "0" ]] || ! port_free "$LOCAL_PORT"; then
    if [[ "$LOCAL_PORT" != "0" ]]; then
        echo "Local port $LOCAL_PORT is busy, picking another one."
    fi
    for candidate in $(seq 44044 44144); do
        if port_free "$candidate"; then
            LOCAL_PORT="$candidate"
            break
        fi
    done
fi

echo "Local endpoint: http://127.0.0.1:${LOCAL_PORT}/v1"

# ---------------------------------------------------------------------------
# Open the tunnel.
#
# One local SSH process does both hops: it forwards
# 127.0.0.1:LOCAL_PORT -> perun:MID_PORT, and the command it runs on Perun
# forwards perun:MID_PORT -> node:SERVER_PORT. -tt gives that inner ssh a
# pty, so it dies with this process instead of lingering on the login node.
# ---------------------------------------------------------------------------

TUNNEL_PID=""
TUNNEL_LOG=$(mktemp "${TMPDIR:-/tmp}/qwen-tunnel.XXXXXX")

cleanup() {
    if [[ -n "$TUNNEL_PID" ]]; then
        kill "$TUNNEL_PID" 2>/dev/null || true
    fi
    # Make sure no forwarding process is left holding MID_PORT.
    ssh -o BatchMode=yes -o ConnectTimeout=5 "$LOGIN_ALIAS" \
        "pkill -f 'ssh -N .*-L 127.0.0.1:${MID_PORT}:' || true" \
        >/dev/null 2>&1 || true
    rm -f "$TUNNEL_LOG"
}
trap cleanup EXIT

echo "Opening ${LOGIN_ALIAS} -> ${NODE} tunnel ..."

ssh -tt \
    -o ExitOnForwardFailure=yes \
    -o ServerAliveInterval=30 \
    -L "127.0.0.1:${LOCAL_PORT}:127.0.0.1:${MID_PORT}" \
    "$LOGIN_ALIAS" \
    "ssh -N $NODE_SSH_OPTS -o ExitOnForwardFailure=yes -L 127.0.0.1:${MID_PORT}:127.0.0.1:${SERVER_PORT} ${NODE}" \
    >>"$TUNNEL_LOG" 2>&1 </dev/null &
TUNNEL_PID=$!

# ---------------------------------------------------------------------------
# Wait until vLLM answers through the tunnel.
# ---------------------------------------------------------------------------

printf 'Waiting for vLLM'
ready=0
for (( i = 0; i < WAIT_TIMEOUT; i++ )); do
    if ! kill -0 "$TUNNEL_PID" 2>/dev/null; then
        echo
        echo "ERROR: The SSH tunnel died:" >&2
        sed 's/^/       | /' "$TUNNEL_LOG" >&2
        echo "       Check that Perun can reach the node without a password:" >&2
        echo "       ssh $LOGIN_ALIAS \"ssh -o BatchMode=yes $NODE hostname\"" >&2
        exit 1
    fi
    if curl -fsS -m 3 -o /dev/null \
            -H "Authorization: Bearer ${API_KEY}" \
            "http://127.0.0.1:${LOCAL_PORT}/v1/models" 2>/dev/null; then
        ready=1
        break
    fi
    printf '.'
    sleep 1
done
echo

if (( ready == 0 )); then
    echo "ERROR: vLLM did not answer within ${WAIT_TIMEOUT}s." >&2
    echo "       The model may still be loading. Watch the job log:" >&2
    echo "       ssh $LOGIN_ALIAS 'tail -f ~/llm/jobs/vllm-${JOB_ID}.log'" >&2
    if [[ -s "$TUNNEL_LOG" ]]; then
        echo "       Tunnel output:" >&2
        sed 's/^/       | /' "$TUNNEL_LOG" >&2
    fi
    exit 1
fi

echo "vLLM is reachable."

# ---------------------------------------------------------------------------
# Launch Qwen Code in the local Singularity container.
# ---------------------------------------------------------------------------

BIND_ARGS=()
for b in $QWEN_BINDS; do
    BIND_ARGS+=(--bind "$b")
done

echo "Launching Qwen in $WORKSPACE ..."

singularity exec \
    "${BIND_ARGS[@]}" \
    --env OPENAI_BASE_URL="http://127.0.0.1:${LOCAL_PORT}/v1" \
    --env OPENAI_API_KEY="${API_KEY}" \
    --env OPENAI_MODEL="${SERVED_MODEL}" \
    --env QWEN_CODE_FORCE_SYNCHRONIZED_OUTPUT="${QWEN_CODE_FORCE_SYNCHRONIZED_OUTPUT:-1}" \
    --env TERM="${TERM:-xterm-256color}" \
    "${QWEN_SIF_LOCAL}" \
    bash -lc "cd '$WORKSPACE' && qwen ${QWEN_ARGS[*]}"

7. Spustenie Qwen Code

Po konfigurácii SSH a príprave servera vLLM nastavte spúšťaciemu skriptu právo na spustenie:

chmod +x ~/llm/qwen_remote.sh

Spustite interaktívnu reláciu:

~/llm/qwen_remote.sh

Spúšťací skript zobrazí menu relácií:

Qwen session:

  1) Continue last session
  2) Start new session
  3) Select previous session
  4) List sessions
  5) Exit

Režim relácie môžete zadať aj priamo.

Príkaz Opis
qwen_remote.sh Interaktívne menu relácií.
qwen_remote.sh . --continue Pokračovať v najnovšej relácii v aktuálnom adresári.
qwen_remote.sh . --new Spustiť novú reláciu.
qwen_remote.sh ~/projects/foo --new Spustiť novú reláciu v zadanom pracovnom adresári.
qwen_remote.sh . --resume Otvoriť výber relácie.
qwen_remote.sh . --list Zobraziť dostupné relácie.

Predvoleným pracovným adresárom je aktuálny adresár.

8. Správa pripojenia počas behu

Pripojenie využíva niekoľko mechanizmov, vďaka ktorým nezávisí od konkrétnej SLURM alokácie.

Dynamický vzdialený port

Server vLLM si pri spustení vyberie voľný port.

Port sa uloží do metadátového súboru úlohy a spúšťací skript na pracovnej stanici ho následne načíta.

Nie je potrebné konfigurovať pevný vzdialený port.

Dynamický sprostredkujúci port

Spúšťací skript požiada prihlasovací uzol Perun, aby vybral voľný port pre sprostredkujúcu fázu SSH presmerovania.

Port sa vyberá dynamicky pomocou Pythonu a neukladá sa do konfigurácie pracovnej stanice.

Dynamický lokálny port

Pracovná stanica používa premennú LOCAL_PORT zo súboru config.env.

Ak je nakonfigurovaný port obsadený, spúšťací skript hľadá iný voľný port v rozsahu:

44044-44144

Nastavením LOCAL_PORT="0" sa zapne automatický výber portu.

Kontrola dostupnosti API

Spúšťací skript nepredpokladá, že vLLM bude pripravené okamžite po vytvorení SSH tunela.

Namiesto toho v pravidelných intervaloch odosiela požiadavku na:

http://127.0.0.1:<local_port>/v1/models

Požiadavka obsahuje API kľúč.

Qwen Code sa spustí až po úspešnej odpovedi endpointu.

Maximálny čas čakania určuje premenná WAIT_TIMEOUT.

Ukončenie tunela

SSH tunel sa ukončí po skončení Qwen Code.

Spúšťací skript sa zároveň pokúsi ukončiť sprostredkujúci proces SSH na clustri Perun.

Tým sa zabráni tomu, aby opustené tunely zbytočne obsadzovali porty.

9. Riešenie problémov

Perun nie je dostupný cez SSH

Spúšťací skript používa BatchMode=yes, ktorý zabraňuje interaktívnym výzvam na zadanie hesla.

Skontrolujte SSH pripojenie:

ssh -v perun true

Overte SSH kľúč, oprávnenia a konfiguráciu opísanú v časti 2.

Nenašla sa žiadna bežiaca úloha vLLM

Skontrolujte frontu SLURM:

ssh perun 'squeue -u username'

Úloha môže stále čakať vo fronte (PD), mohla sa ukončiť alebo dosiahla časový limit.

Metadátový súbor úlohy chýba

Skontrolujte, či metadátový súbor existuje:

ssh perun 'ls -l ~/llm/run/'

Ak úloha beží, ale metadátový súbor chýba, vLLM sa ešte môže spúšťať alebo skript run_vllm.sh zlyhal.

Skontrolujte log úlohy:

ssh perun 'tail -n 50 ~/llm/jobs/vllm-<jobid>.log'

Výpočtový uzol požaduje heslo

Aktuálna implementácia vyžaduje SSH pripojenie bez hesla z prihlasovacieho uzla Perun na pridelený výpočtový uzol.

Otestujte pripojenie:

ssh perun "ssh -o BatchMode=yes <compute-node> hostname"

Ak príkaz zlyhá, nakonfigurujte interný SSH kľúč podľa časti 2.4.

SSH tunel sa neočakávane ukončil

Po ukončení spojenia spúšťací skript vypíše výstup SSH tunela.

Skontrolujte uvedenú chybu SSH.

Medzi bežné príčiny patria:

  • zlyhanie SSH autentifikácie bez hesla;
  • nedostupný výpočtový uzol;
  • nesprávne presmerovanie sprostredkujúceho portu;
  • ukončený server vLLM;
  • časový limit SSH spojenia.

vLLM neodpovedalo v nastavenom časovom limite

Model sa ešte môže načítavať do pamäte GPU.

Skontrolujte log úlohy:

ssh perun 'tail -f ~/llm/jobs/vllm-<jobid>.log'

V logu vyhľadajte hlásenie:

Application startup complete

Ak inicializácia modelu vyžaduje viac času, zvýšte hodnotu:

WAIT_TIMEOUT="120"

v súbore config.env.

Odpoveď HTTP 401 znamená, že sa API kľúče nezhodujú. Overte, či ~/.llm_api_key na clustri Perun obsahuje kľúč použitý bežiacim serverom vLLM.

Po zmene kľúča reštartujte úlohu vLLM.

Manuálne otestovanie API endpointu

Po vytvorení SSH tunela otestujte lokálny endpoint:

curl -s \
    -H "Authorization: Bearer $(ssh perun cat ~/.llm_api_key)" \
    http://127.0.0.1:44044/v1/models

Hodnotu 44044 nahraďte skutočným lokálnym portom vybraným spúšťacím skriptom.

10. Zhrnutie

Celý postup pozostáva z nasledujúcich krokov:

  1. Nakonfigurujte SSH prístup bez hesla z pracovnej stanice na Perun.
  2. Nakonfigurujte SSH prístup bez hesla z Perun na výpočtové uzly.
  3. Vytvorte API kľúč na clustri Perun.
  4. Nakonfigurujte súbor config.env na pracovnej stanici.
  5. Odošlite SLURM úlohu vLLM na clustri Perun.
  6. Počkajte, kým sa úloha spustí a vLLM zverejní svoje metadáta.
  7. Spustite qwen_remote.sh na pracovnej stanici.
  8. Spúšťací skript nájde bežiacu úlohu a načíta jej metadáta.
  9. Spúšťací skript načíta API kľúč a vytvorí SSH tunel.
  10. Lokálny API endpoint sa kontroluje, kým vLLM nezačne odpovedať.
  11. Qwen Code sa spustí v lokálnom kontajneri Singularity.
  12. Po ukončení Qwen Code sa SSH tunel ukončí.

Konfigurácia pracovnej stanice zostáva rovnaká pri zmene úloh, pridelených výpočtových uzlov aj modelov.

Pri zmene modelu alebo konfigurácie servera je potrebné upraviť iba konfiguráciu vLLM a parametre odosielania úlohy na clustri.

Created by: Marek Štekláč