vLLM Inference on KLC Reserve GPUs#

Serve an open-source model with vLLM inside a Singularity container on a KLC Reserve GPU node, then query the server from a login node with the OpenAI Python client. The job exposes an OpenAI-compatible HTTP API for chat, streaming, and batch inference.

Note

See Open Source LLMs for an overview of every open-source LLM workflow on KLC — interactive, GPU, and tutorial.

Prerequisites#

  • KLC access and permission to submit to the kellogg partition

  • Familiarity with GPU Jobs — GPUs are available only through KLC Reserve, not on login nodes

  • Write access to a project or scratch directory for the Singularity image (.sif) and HuggingFace model cache

Overview#

  1. Configure and submit a SLURM batch script that starts vllm serve in a container.

  2. Tail the job log until the server reports ready; note the node hostname and port.

  3. From a login node, install the openai client and run a test query against the server.

Configure the Job#

Save the script below as run_vllm.slurm. Edit the #SBATCH directives and user configuration variables before submitting. The highlighted section handles image pull, container launch, and health checks — you do not need to change it.

Variable

Purpose

MODEL

HuggingFace model ID or local path inside the container

SIF_IMAGE

Path to the Singularity image (.sif); must be in a directory you can write to

HF_HOME

HuggingFace cache directory for downloaded weights

HF_TOKEN

Required for gated models; export before submit or leave empty for public models

#SBATCH --gres

Number of GPUs; tensor parallel size follows automatically via $SLURM_GPUS_ON_NODE

Set SIF_IMAGE to a path under your project or scratch space — for example /kellogg/proj/<your-netid>/containers/vllm-openai.sif or /scratch/$USER/containers/vllm-openai.sif. If the file does not exist, the script pulls docker://vllm/vllm-openai:v0.27.1 on first run. The initial pull can take several minutes.

  1#!/bin/bash
  2# =============================================================================
  3# Slurm submission script for running vLLM via Singularity
  4# Serves an OpenAI-compatible HTTP API on the allocated node(s).
  5#
  6# Usage:
  7#   sbatch run_vllm.slurm
  8#
  9# After the job starts, query the server from a login node or another job:
 10#   curl http://<NODE_HOSTNAME>:${PORT}/v1/models
 11# =============================================================================
 12
 13#SBATCH --job-name=vllm-serve
 14#SBATCH --output=logs/vllm-%j.out
 15#SBATCH --partition=kellogg
 16#SBATCH --account=kellogg
 17#SBATCH --nodes=1
 18#SBATCH --ntasks-per-node=1
 19#SBATCH --cpus-per-task=1
 20#SBATCH --gres=gpu:1
 21#SBATCH --mem=32G
 22#SBATCH --time=02:00:00
 23#SBATCH --signal=B:SIGTERM@60
 24
 25module purge
 26module load singularityce/4.3.1-gcc-8.5.0
 27
 28# =============================================================================
 29# User configuration — set these before submitting
 30# =============================================================================
 31
 32MODEL="microsoft/Phi-4-mini-instruct"
 33TENSOR_PARALLEL_SIZE=$SLURM_GPUS_ON_NODE
 34PORT=8001
 35SIF_IMAGE="/kellogg/proj/<your-netid>/containers/vllm-openai.sif"
 36DOCKER_IMAGE="docker://vllm/vllm-openai:v0.27.1"
 37HF_HOME="/scratch/$USER/hf_home"
 38HF_TOKEN="$HF_TOKEN"
 39LOCAL_MODELS_DIR=""
 40EXTRA_VLLM_ARGS=""
 41
 42# =============================================================================
 43# Do not edit below — environment setup and server launch
 44# =============================================================================
 45
 46set -euo pipefail
 47
 48mkdir -p "$(dirname "${SLURM_SUBMIT_DIR:-$(pwd)}/logs")" logs
 49
 50echo "=========================================="
 51echo "Job ID      : ${SLURM_JOB_ID}"
 52echo "Node        : ${SLURMD_NODENAME}"
 53echo "GPUs        : ${SLURM_GPUS_ON_NODE:-${SLURM_JOB_GPUS:-N/A}}"
 54echo "Model       : ${MODEL}"
 55echo "TP size     : ${TENSOR_PARALLEL_SIZE}"
 56echo "Port        : ${PORT}"
 57echo "SIF image   : ${SIF_IMAGE}"
 58echo "Start time  : $(date)"
 59echo "=========================================="
 60
 61if [[ ! -f "${SIF_IMAGE}" ]]; then
 62    echo "[INFO] SIF image not found. Pulling ${DOCKER_IMAGE} ..."
 63    mkdir -p "$(dirname "${SIF_IMAGE}")"
 64    singularity pull --disable-cache "${SIF_IMAGE}" "${DOCKER_IMAGE}"
 65    echo "[INFO] Pull complete: ${SIF_IMAGE}"
 66else
 67    echo "[INFO] Using cached SIF image: ${SIF_IMAGE}"
 68fi
 69
 70mkdir -p "${HF_HOME}"
 71BINDS="${HF_HOME}:/hf_home"
 72
 73if [[ -n "${LOCAL_MODELS_DIR}" && -d "${LOCAL_MODELS_DIR}" ]]; then
 74    BINDS="${BINDS},${LOCAL_MODELS_DIR}:/models:ro"
 75fi
 76
 77SINGULARITY_ENV_ARGS=""
 78
 79if [[ -n "${HF_TOKEN}" ]]; then
 80    SINGULARITY_ENV_ARGS="${SINGULARITY_ENV_ARGS} --env HF_TOKEN=${HF_TOKEN}"
 81fi
 82
 83SINGULARITY_ENV_ARGS="${SINGULARITY_ENV_ARGS} --env VLLM_PORT=${PORT}"
 84SINGULARITY_ENV_ARGS="${SINGULARITY_ENV_ARGS} --env NCCL_SOCKET_IFNAME=^lo,docker0"
 85SINGULARITY_ENV_ARGS="${SINGULARITY_ENV_ARGS} --env HF_HOME=/hf_home"
 86
 87cleanup() {
 88    echo "[INFO] Caught shutdown signal — stopping vLLM server (PID ${VLLM_PID:-unknown})"
 89    [[ -n "${VLLM_PID:-}" ]] && kill -SIGTERM "${VLLM_PID}" 2>/dev/null || true
 90    wait "${VLLM_PID:-}" 2>/dev/null || true
 91    echo "[INFO] vLLM server stopped."
 92}
 93trap cleanup SIGTERM SIGINT
 94
 95echo "[INFO] Starting vLLM server..."
 96
 97singularity exec \
 98    --nv \
 99    --bind "${BINDS}" \
100    ${SINGULARITY_ENV_ARGS} \
101    "${SIF_IMAGE}" \
102    vllm serve "${MODEL}" \
103        --host 0.0.0.0 \
104        --port "${PORT}" \
105        --tensor-parallel-size "${TENSOR_PARALLEL_SIZE}" \
106        ${EXTRA_VLLM_ARGS} &
107
108VLLM_PID=$!
109echo "[INFO] vLLM PID: ${VLLM_PID}"
110
111echo "[INFO] Waiting for server to become ready on port ${PORT}..."
112MAX_WAIT=300
113ELAPSED=0
114until curl -sf "http://localhost:${PORT}/health" > /dev/null 2>&1; do
115    sleep 5
116    ELAPSED=$(( ELAPSED + 5 ))
117    if [[ ${ELAPSED} -ge ${MAX_WAIT} ]]; then
118        echo "[ERROR] Server did not become ready within ${MAX_WAIT}s — aborting."
119        kill -SIGTERM "${VLLM_PID}" 2>/dev/null || true
120        exit 1
121    fi
122done
123
124echo "[INFO] vLLM server is ready."
125echo "[INFO] OpenAI-compatible API: http://${SLURMD_NODENAME}:${PORT}/v1"
126echo "[INFO] Available models     : http://${SLURMD_NODENAME}:${PORT}/v1/models"
127
128wait "${VLLM_PID}"
129echo "[INFO] vLLM server exited. Job finishing."

Tip

To reduce GPU memory use, set EXTRA_VLLM_ARGS — for example --gpu-memory-utilization 0.85 --max-model-len 8192. For larger models on fewer GPUs, consider quantization flags such as --quantization awq or --quantization gptq.

For multi-GPU jobs, increase #SBATCH --gres (for example --gres=gpu:2 or --gres=gpu:h100:4). TENSOR_PARALLEL_SIZE tracks the allocated GPU count automatically.

Submit and Wait for Ready#

mkdir -p logs
sbatch run_vllm.slurm

Monitor the job and tail the log:

squeue -u $USER
tail -f logs/vllm-<job-id>.out

Wait until the log shows:

[INFO] vLLM server is ready.
[INFO] OpenAI-compatible API: http://qgpu0202:8001/v1
[INFO] Available models     : http://qgpu0202:8001/v1/models

Use the hostname and port from your log (here, qgpu0202 and 8001) when connecting from a login node.

Query From a Login Node#

Install the OpenAI Python client in a local environment:

module purge
module load mamba/24.3.0
mamba create --prefix=./env openai python=3.13
eval "$('/hpc/software/mamba/24.3.0/bin/conda' 'shell.bash' 'hook' 2> /dev/null)"
conda activate ./env

Save the client script below as query_vllm.py, then run a chat test using the hostname and port from the job log:

python query_vllm.py --host qgpu0202 --port 8001 --example chat
import argparse
import os

from openai import OpenAI


def make_client(host: str, port: int, api_key: str = "EMPTY") -> OpenAI:
    return OpenAI(
        base_url=f"http://{host}:{port}/v1",
        api_key=api_key,
    )


def get_model(client: OpenAI) -> str:
    models = client.models.list()
    return models.data[0].id


def chat_example(client: OpenAI, model: str) -> None:
    print("\n=== Chat Completion ===")

    messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Explain what vLLM is in two sentences."},
    ]

    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0.7,
        max_tokens=256,
    )

    print("Model :", response.model)
    print("Reply :", response.choices[0].message.content)
    print("Usage :", response.usage)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Query a vLLM OpenAI-compatible server")
    parser.add_argument(
        "--host",
        default=os.environ.get("VLLM_HOST", "localhost"),
        help="Hostname or IP of the vLLM server",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=int(os.environ.get("VLLM_PORT", 8000)),
        help="Port the vLLM server is listening on",
    )
    parser.add_argument(
        "--example",
        choices=["chat"],
        default="chat",
        help="Which example to run",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    client = make_client(args.host, args.port)
    model = get_model(client)
    print(f"[INFO] Connected to http://{args.host}:{args.port}/v1  |  model: {model}")
    chat_example(client, model)


if __name__ == "__main__":
    main()

The full client in the source example also supports streaming, text completion, and batch requests via --example stream, completion, or batch.

Gated Models#

For models that require HuggingFace authentication (for example Llama 3), export your token before submitting:

export HF_TOKEN=<your-huggingface-token>
sbatch run_vllm.slurm