This runbook starts Qwen/Qwen3.8-27B on a Slurm GPU node and then starts the
AORTA Chat UI on port 8080. It is written for the Ruby cluster and the checkout
at /apps/avsharma/aorta-cia-qwen38.
The two ports have different jobs:
The commands below never probe or connect to localhost:8000. The Slurm job
starts its search at 8001, selects the first free port on the allocated GPU
node, starts vLLM on that port, and records the exact remote URL in a shared
environment file. The UI refuses to start unless that file exists, so omitting
the model-server steps cannot silently restore AORTA’s built-in
http://localhost:8000/v1 default.
The vLLM job reserves one whole GPU node for three hours. Cancel it when you finish.
From ruby-slurmlogin02, start or reuse a healthy Qwen service:
cd /apps/avsharma/aorta-cia-qwen38
scripts/chat/start_qwen_vllm.sh \
--login-host ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
--runtime-dir /apps/avsharma/aorta-chat-runtime \
--hf-cache /apps/avsharma/hf-cache
The command returns after /health passes. It enables Qwen’s reasoning and XML
tool parsers, prints the selected compute-node URL, and never inspects port
aorta chat doctor—including its native
function-call probe—before starting the UI.
Each user must complete Prepare the AORTA checkout
once first; that step fetches the per-user Chat index required by the full
Doctor report.
Startup output is limited to Slurm state changes and one progress message every
30 seconds; add --verbose to print every five-second poll.
Shutdown follows the same policy instead of printing every COMPLETING poll.Start the CIA-enabled UI:
scripts/chat/start_aorta_chat.sh \
--login-host ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
--runtime-dir /apps/avsharma/aorta-chat-runtime
The launcher explicitly selects the vllm provider and native tool protocol,
overriding any different provider saved in the user’s chat profile.
It prints a startup message immediately and uses a per-port lock, so retrying
the command cannot silently launch a second Chainlit server on the same port.
Open http://127.0.0.1:8080. Keep the second command running. Ctrl-C stops
the remote UI and SSH tunnel without leaving either process behind.
If the terminal is closed or killed before its cleanup trap runs, recover with:
scripts/chat/start_aorta_chat.sh --stop \
--login-host ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
--runtime-dir /apps/avsharma/aorta-chat-runtime
When finished, release the Qwen GPU node:
scripts/chat/start_qwen_vllm.sh --stop \
--login-host ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
--runtime-dir /apps/avsharma/aorta-chat-runtime \
--hf-cache /apps/avsharma/hf-cache
The remainder of this guide expands those two scripts into individual commands
for troubleshooting or changing their defaults. Run either launcher with
--help to see its supported overrides.
Run these commands from ruby-slurmlogin02:
cd /apps/avsharma/aorta-cia-qwen38
# Create the environment only once.
if [ ! -x .venv-ui/bin/aorta ]; then
python3.13 -m venv .venv-ui
.venv-ui/bin/python -m pip install --upgrade pip
.venv-ui/bin/python -m pip install -e ".[chat-ui,cia]"
fi
source .venv-ui/bin/activate
aorta chat index fetch # doctor FAILs without one, and the UI launcher gates on doctor
aorta chat --help
Create shared directories for the Hugging Face cache and runtime files:
mkdir -p /apps/avsharma/hf-cache
mkdir -p /apps/avsharma/aorta-chat-runtime
If you will enable CIA cluster submissions, install the current RocJITsu
sanitizer bundle. The downloader verifies the artifact’s SHA-256 manifest and
uses gh auth token; run gh auth login first if it reports that no token is
available.
ROCJITSU_PREBUILT=/apps/avsharma/aorta-chat-runtime/rocjitsu-prebuilt
ROCJITSU_BUILD=/apps/avsharma/aorta-chat-runtime/rocjitsu-build
python scripts/sanitizers/download_sanitizer_artifacts.py \
--run latest \
--dest "${ROCJITSU_PREBUILT}" \
--force
# Chat currently passes the raw-build layout to submitted CIA jobs. Present
# the verified prebuilt files under that layout without copying them.
mkdir -p "${ROCJITSU_BUILD}/lib/rocjitsu/src/rocjitsu/hooks"
mkdir -p "${ROCJITSU_BUILD}/tools"
ln -sfn \
"${ROCJITSU_PREBUILT}/lib/librocjitsu_dbi_hooks.so" \
"${ROCJITSU_BUILD}/lib/rocjitsu/src/rocjitsu/hooks/librocjitsu_dbi_hooks.so"
ln -sfn \
"${ROCJITSU_PREBUILT}/bin/rj_waitcheck" \
"${ROCJITSU_BUILD}/tools/rj_waitcheck"
test -f \
"${ROCJITSU_BUILD}/lib/rocjitsu/src/rocjitsu/hooks/librocjitsu_dbi_hooks.so"
test -x "${ROCJITSU_BUILD}/tools/rj_waitcheck"
"${ROCJITSU_BUILD}/tools/rj_waitcheck" --help >/dev/null
ldd -r \
"${ROCJITSU_BUILD}/lib/rocjitsu/src/rocjitsu/hooks/librocjitsu_dbi_hooks.so"
Still on ruby-slurmlogin02, create the job script on the shared filesystem:
cat > /apps/avsharma/aorta-chat-runtime/qwen38-vllm.sbatch <<'SBATCH'
#!/usr/bin/env bash
#SBATCH --job-name=aorta-qwen38
#SBATCH --partition=interactive
#SBATCH --nodes=1
#SBATCH --exclusive
#SBATCH --time=03:00:00
#SBATCH --output=/apps/avsharma/aorta-chat-runtime/qwen38-vllm-%j.log
set -euo pipefail
RUNTIME_DIR=/apps/avsharma/aorta-chat-runtime
ENDPOINT_FILE="${RUNTIME_DIR}/qwen38-endpoint.env"
HF_CACHE=/apps/avsharma/hf-cache
mkdir -p "${RUNTIME_DIR}" "${HF_CACHE}"
# Check ports on the allocated GPU node without connecting to anything.
# Port 8000 is intentionally excluded because it belongs to another service.
PORT="$(
python3 - <<'PY'
import socket
for port in range(8001, 8101):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.bind(("0.0.0.0", port))
except OSError:
sock.close()
continue
sock.close()
print(port)
break
else:
raise SystemExit("no free model port in the range 8001-8100")
PY
)"
HOST="$(hostname -f)"
TMP_ENDPOINT="${ENDPOINT_FILE}.${SLURM_JOB_ID}.tmp"
umask 077
cat > "${TMP_ENDPOINT}" <<EOF
export QWEN_VLLM_JOB_ID="${SLURM_JOB_ID}"
export AORTA_CHAT_VLLM_BASE_URL="http://${HOST}:${PORT}/v1"
export AORTA_CHAT_VLLM_MODEL="Qwen/Qwen3.8-27B"
EOF
mv "${TMP_ENDPOINT}" "${ENDPOINT_FILE}"
cleanup() {
if [ -f "${ENDPOINT_FILE}" ] &&
grep -Fq "QWEN_VLLM_JOB_ID=\"${SLURM_JOB_ID}\"" "${ENDPOINT_FILE}"; then
rm -f "${ENDPOINT_FILE}"
fi
}
trap cleanup EXIT
echo "vLLM endpoint: http://${HOST}:${PORT}/v1"
docker pull rocm/vllm-dev:nightly
docker run --rm \
--name "aorta-qwen38-${SLURM_JOB_ID}" \
--network host \
--device=/dev/kfd \
--device=/dev/dri \
--group-add video \
--ipc=host \
--shm-size 32g \
-e HIP_VISIBLE_DEVICES=0 \
-e HF_HOME=/root/.cache/huggingface \
-v "${HF_CACHE}:/root/.cache/huggingface" \
rocm/vllm-dev:nightly \
vllm serve Qwen/Qwen3.8-27B \
--served-model-name Qwen/Qwen3.8-27B \
--host 0.0.0.0 \
--port "${PORT}" \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.85 \
--max-model-len 32768 \
--trust-remote-code \
--enable-auto-tool-choice \
--tool-call-parser qwen3_xml \
--reasoning-parser qwen3
SBATCH
chmod 700 /apps/avsharma/aorta-chat-runtime/qwen38-vllm.sbatch
First stop a model job left by an earlier run of this guide. This only cancels the job ID recorded in AORTA’s own endpoint file; it does not inspect or contact port 8000.
ENDPOINT_FILE=/apps/avsharma/aorta-chat-runtime/qwen38-endpoint.env
if [ -s "${ENDPOINT_FILE}" ]; then
source "${ENDPOINT_FILE}"
if ! status="$(
ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
"squeue -h -j ${QWEN_VLLM_JOB_ID} -o '%i'"
)"; then
echo "error: could not query Slurm job ${QWEN_VLLM_JOB_ID}; preserving ${ENDPOINT_FILE}" >&2
exit 1
fi
if [ -n "${status}" ]; then
echo "Cancelling previous vLLM job ${QWEN_VLLM_JOB_ID}"
if ! ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
"scancel ${QWEN_VLLM_JOB_ID}"; then
echo "error: could not cancel Slurm job ${QWEN_VLLM_JOB_ID}; preserving ${ENDPOINT_FILE}" >&2
exit 1
fi
fi
rm -f "${ENDPOINT_FILE}"
fi
Submit through the login node that can reach the Slurm controller:
QWEN_JOB_ID="$(
ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
"sbatch --parsable /apps/avsharma/aorta-chat-runtime/qwen38-vllm.sbatch"
)"
QWEN_JOB_ID="${QWEN_JOB_ID%%;*}"
echo "Submitted vLLM job ${QWEN_JOB_ID}"
Wait for Slurm to allocate a node and for the job to publish its endpoint:
ENDPOINT_FILE=/apps/avsharma/aorta-chat-runtime/qwen38-endpoint.env
until [ -s "${ENDPOINT_FILE}" ]; do
JOB_STATUS="$(
ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
"squeue -h -j ${QWEN_JOB_ID} -o '%.18i %.10T %.10M %.30R'"
)"
if [ -z "${JOB_STATUS}" ]; then
echo "vLLM job ${QWEN_JOB_ID} exited before publishing an endpoint."
ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
"sacct -j ${QWEN_JOB_ID} -o JobID,State,ExitCode"
break
fi
echo "${JOB_STATUS}"
sleep 5
done
if [ -s "${ENDPOINT_FILE}" ]; then
source "${ENDPOINT_FILE}"
printf 'AORTA model endpoint: %s\n' "${AORTA_CHAT_VLLM_BASE_URL}"
else
echo "No endpoint was created. Do not continue to the UI steps."
false
fi
The printed URL must name a compute node and port 8001 or higher. It must not
say localhost:8000.
Model loading can take a few minutes. Poll the endpoint recorded by the job:
HEALTH_URL="${AORTA_CHAT_VLLM_BASE_URL%/v1}/health"
until curl --fail --silent --show-error "${HEALTH_URL}" >/dev/null; do
JOB_STATUS="$(
ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
"squeue -h -j ${QWEN_VLLM_JOB_ID} -o '%i %T %R'"
)"
if [ -z "${JOB_STATUS}" ]; then
echo "vLLM job ${QWEN_VLLM_JOB_ID} exited before becoming ready."
cat "/apps/avsharma/aorta-chat-runtime/qwen38-vllm-${QWEN_VLLM_JOB_ID}.log"
break
fi
echo "Waiting for Qwen at ${HEALTH_URL} ..."
sleep 5
done
if curl --fail --silent --show-error "${HEALTH_URL}" >/dev/null; then
echo "Qwen is ready."
curl --fail --silent --show-error \
"${AORTA_CHAT_VLLM_BASE_URL}/models" | python3 -m json.tool
else
echo "Qwen did not become ready. Do not continue to the UI steps."
false
fi
If the Slurm job exits before the health check succeeds, inspect its log:
cat "/apps/avsharma/aorta-chat-runtime/qwen38-vllm-${QWEN_JOB_ID}.log"
Verify an actual model response, not only the health route:
curl --fail --silent --show-error \
"${AORTA_CHAT_VLLM_BASE_URL}/chat/completions" \
-H 'Content-Type: application/json' \
-d '{
"model": "Qwen/Qwen3.8-27B",
"messages": [{"role": "user", "content": "Reply with READY only."}],
"chat_template_kwargs": {"enable_thinking": false},
"temperature": 0,
"max_tokens": 16
}' | python3 -m json.tool
The response should contain "content": "READY" and no reasoning trace.
Reproduce aorta chat doctor’s native function-call check. Build the request
with the model recorded by the running worker:
TOOL_REQUEST="$(
AORTA_EXPECTED_MODEL="${AORTA_CHAT_VLLM_MODEL}" python3 -c '
import json
import os
name = "aorta_tool_protocol_probe"
print(json.dumps({
"model": os.environ["AORTA_EXPECTED_MODEL"],
"messages": [{
"role": "user",
"content": "Call the provided tool with source set to READY. Do not answer in text.",
}],
"tools": [{
"type": "function",
"function": {
"name": name,
"description": "Verify native function calling.",
"parameters": {
"type": "object",
"properties": {"source": {"type": "string"}},
"required": ["source"],
},
},
}],
"tool_choice": {"type": "function", "function": {"name": name}},
"temperature": 0,
"max_tokens": 128,
}))
'
)"
TOOL_RESPONSE="$(
curl --fail --silent --show-error \
"${AORTA_CHAT_VLLM_BASE_URL}/chat/completions" \
-H 'Content-Type: application/json' \
-d "${TOOL_REQUEST}"
)"
Validate the response exactly as the vLLM backend does:
python3 -c '
import json
import sys
message = json.load(sys.stdin)["choices"][0]["message"]
calls = message.get("tool_calls") or []
if len(calls) != 1:
raise SystemExit(f"native tool-call probe produced {len(calls)} calls: {message!r}")
function = calls[0].get("function") or {}
if function.get("name") != "aorta_tool_protocol_probe":
raise SystemExit(f"native tool-call probe used the wrong function: {message!r}")
arguments = function.get("arguments") or {}
if isinstance(arguments, str):
arguments = json.loads(arguments)
if not isinstance(arguments, dict) or not isinstance(arguments.get("source"), str):
raise SystemExit(f"native tool-call probe used the wrong arguments: {message!r}")
' <<<"${TOOL_RESPONSE}"
Success produces no output. A response containing XML or ordinary text instead
of one native tool_calls entry fails validation. The source value itself is
not compared with READY; the check verifies the server’s structured
function-call capability rather than the model’s casing or punctuation.
Start the UI from the same shell that sourced qwen38-endpoint.env. The
explicit guards are important: they prevent a missing endpoint file from
falling through to localhost:8000.
For ordinary chat:
cd /apps/avsharma/aorta-cia-qwen38
source .venv-ui/bin/activate
source /apps/avsharma/aorta-chat-runtime/qwen38-endpoint.env
: "${AORTA_CHAT_VLLM_BASE_URL:?Start the vLLM job and source its endpoint file first}"
: "${AORTA_CHAT_VLLM_MODEL:?The endpoint file did not specify a model}"
export AORTA_CHAT_LLM_PROVIDER=vllm
export AORTA_CHAT_LLM_TOOL_MODE=native
aorta chat ui --host 127.0.0.1 --port 8080
Open http://127.0.0.1:8080.
ruby-slurmlogin02 cannot always reach the Slurm controller. To use
triage_kernel_source, triage_assembly_source, and triage_workload, run the
UI on ruby-slurmlogin01 instead.
Check for a UI left by an earlier session before starting another one:
ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com '
if [ -n "$(ss -H -ltn "sport = :8080")" ]; then
echo "Port 8080 is already in use on ruby-slurmlogin01:"
ss -ltnp "sport = :8080"
pgrep -af "aorta chat ui|chainlit run" || true
exit 1
fi
'
If this reports an old AORTA UI, stop the terminal that owns it with Ctrl-C
before continuing. Do not kill an unfamiliar process merely to free the port.
Start the CIA-enabled UI:
ssh -t ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com '
cd /apps/avsharma/aorta-cia-qwen38
source .venv-ui/bin/activate
source /apps/avsharma/aorta-chat-runtime/qwen38-endpoint.env
: "${AORTA_CHAT_VLLM_BASE_URL:?Start the vLLM job first}"
: "${AORTA_CHAT_VLLM_MODEL:?The endpoint file did not specify a model}"
export AORTA_CHAT_LLM_PROVIDER=vllm
export AORTA_CHAT_LLM_TOOL_MODE=native
export AORTA_CHAT_ALLOW_CLUSTER_JOBS=true
export AORTA_CHAT_JOBS_PATH=/apps/avsharma/cia-chat-jobs
export AORTA_CHAT_GPU_ARCH=gfx950
export AORTA_CHAT_ROCJITSU_BUILD=/apps/avsharma/aorta-chat-runtime/rocjitsu-build
export CIA_PARTITION=interactive
export CIA_TIME_LIMIT=00:12:00
export CIA_SEARCH_ROOTS=/apps/avsharma
exec aorta chat ui --host 127.0.0.1 --port 8080
'
In a second terminal, forward that UI back to ruby-slurmlogin02:
ssh -N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-L 8080:127.0.0.1:8080 \
ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com
If this says port 8080 is already in use, inspect the existing local listener
with ss -ltnp 'sport = :8080' and stop the terminal that owns the old tunnel.
Then open http://127.0.0.1:8080.
In a separate terminal:
cd /apps/avsharma/aorta-cia-qwen38
source .venv-ui/bin/activate
source /apps/avsharma/aorta-chat-runtime/qwen38-endpoint.env
echo "${AORTA_CHAT_VLLM_BASE_URL}"
aorta chat config show
The environment variable overrides the profile. The UI log should report the
compute-node URL from qwen38-endpoint.env; it must not report
http://localhost:8000.
Stop the UI with Ctrl-C. If an SSH tunnel is running, stop that terminal with
Ctrl-C too.
If either process survives an interrupted terminal, stop only the UI and tunnel managed by the launcher:
scripts/chat/start_aorta_chat.sh --stop \
--login-host ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
--runtime-dir /apps/avsharma/aorta-chat-runtime
Cancel the model server while the endpoint file still exists:
source /apps/avsharma/aorta-chat-runtime/qwen38-endpoint.env
if ! ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
"scancel ${QWEN_VLLM_JOB_ID}"; then
echo "error: could not cancel Slurm job ${QWEN_VLLM_JOB_ID}" >&2
exit 1
fi
Cancellation can leave the job in COMPLETING briefly while Docker exits.
Wait until Slurm removes it:
while true; do
if ! JOB_STATUS="$(
ssh -o BatchMode=yes ruby-slurmlogin01.rckg.g03.cpe.ice.amd.com \
"squeue -h -j ${QWEN_VLLM_JOB_ID} -o '%i %T %M %R'"
)"; then
echo "error: could not query Slurm job ${QWEN_VLLM_JOB_ID}; shutdown is unconfirmed" >&2
exit 1
fi
[ -n "${JOB_STATUS}" ] || break
echo "${JOB_STATUS}"
sleep 2
done
echo "vLLM job ${QWEN_VLLM_JOB_ID} has stopped."
The Slurm script removes qwen38-endpoint.env when the job exits, preventing a
later UI session from reusing a stale endpoint.