Deploying a Model from the OpenShift AI Dashboard

This document walks through serving a large language model with vLLM on OpenShift AI, using the OpenShift AI dashboard to create the deployment. The model weights are read from a PersistentVolumeClaim (PVC) in your project rather than from object storage.

The worked example deploys Qwen2.5-3B-Instruct, but the same steps apply to any model vLLM supports, to do this substitute your own repository name and directory.

If you have not logged in to the cluster yet, start with the user onboarding documentation.

Before you begin

You will need:

The dashboard can select an existing PVC as a model source, but it cannot populate one. Everything up to Load the weights onto the PVC prepares the storage from the command line; from Start the deployment in the dashboard onward, the work happens in the dashboard.

Project Checks

First make sure you are on your project and it is labeled correctly.

oc project <your-project-name-here>

Check if your namespace is labeled correctly:

oc get namespace <your-project-name-here> -o jsonpath='{.metadata.labels.modelmesh-enabled}{"\n"}'
false

You should see false.

Also check the dashboard label:

oc get namespace <your-project-name-here> -o jsonpath='{.metadata.labels.opendatahub\.io/dashboard}{"\n"}' 
true

You should see true.

If either label has the wrong value (or is empty), run:

oc label namespace <your-project-name-here> \
  opendatahub.io/dashboard=true \
  modelmesh-enabled=false \
  --overwrite

Create the model storage PVC

The weights have to live somewhere before either the loader job or the model server can start, so create the PVC now. The PVC must be ReadWriteMany so that both can mount it. On this cluster, pure-fb-nfsv4 is the RWX-capable storage class.

oc apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: qwen-model-pvc
  labels:
    opendatahub.io/dashboard: "true"
  annotations:
    openshift.io/display-name: qwen-model-pvc
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: pure-fb-nfsv4
  resources:
    requests:
      storage: 20Gi
EOF

Size the PVC for the model you are downloading, with room to spare. Qwen2.5-3B-Instruct is roughly 5.8 GB across 12 files.

Confirm the PVC actually exists before going any further:

oc get pvc qwen-model-pvc
NAME             STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS    AGE
qwen-model-pvc   Bound    pvc-...    20Gi       RWX            pure-fb-nfsv4   10s

The status may briefly be Pending while the volume is provisioned. It must reach Bound, with RWX under ACCESS MODES. If oc get pvc returns nothing, the PVC was not created and the loader job in the next-but-one step will never be scheduled.

Store your HuggingFace token

Skip this step if the model repository is not gated.

oc create secret generic hf-token --from-literal=token=<your-hf-token>

Do not put the token directly into a YAML file you intend to commit.

Load the weights onto the PVC

This job mounts the PVC created in Create the model storage PVC at /mnt/models and downloads the model into a subdirectory. The PVC has to exist first — the job's pod cannot be scheduled otherwise, and the job will sit at 0/1 completions indefinitely rather than failing.

If you are using a different model than Qwen2.5-3B-Instruct, update the MODEL_REPO and MODEL_DIR variables. If you gave the PVC a name other than qwen-model-pvc, update claimName to match.

oc apply -f - <<'EOF'
apiVersion: batch/v1
kind: Job
metadata:
  name: model-loader
spec:
  backoffLimit: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: loader
          image: registry.access.redhat.com/ubi9/python-311:latest
          env:
            - name: MODEL_REPO
              value: Qwen/Qwen2.5-3B-Instruct
            - name: MODEL_DIR
              value: Qwen2.5-3B-Instruct
            - name: HF_HOME
              value: /tmp/hf
            - name: PYTHONPATH
              value: /tmp/pylibs
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-token
                  key: token
                  optional: true
          command:
            - /bin/bash
            - -c
            - |
              set -euo pipefail
              pip install --quiet --target "$PYTHONPATH" "huggingface_hub[hf_xet]"
              export PATH="$PYTHONPATH/bin:$PATH"
              hf download "$MODEL_REPO" --local-dir "/mnt/models/$MODEL_DIR"
              ls -la "/mnt/models/$MODEL_DIR"
          volumeMounts:
            - name: model-store
              mountPath: /mnt/models
          resources:
            requests:
              cpu: "1"
              memory: 4Gi
            limits:
              cpu: "2"
              memory: 8Gi
      volumes:
        - name: model-store
          persistentVolumeClaim:
            claimName: qwen-model-pvc
EOF

Wait for it to finish:

oc wait --for=condition=complete job/model-loader --timeout=1800s

The transfer usually takes well under a minute for a model this size. If oc wait has not returned after a couple of minutes, the pod is probably not running at all — check with:

oc get jobs
oc events

A FailedScheduling event reading persistentvolumeclaim "qwen-model-pvc" not found means the PVC was never created. Go back to Create the model storage PVC, then delete and re-apply the job with oc delete job model-loader.

Confirm the files landed:

oc logs job/model-loader | tail -20

You should see the contents of /mnt/models/Qwen2.5-3B-Instruct — config.json, the *.safetensors shards, the tokenizer files, and so on. Note the directory name: you will type it into the dashboard under Point the deployment at the PVC.

Start the deployment in the dashboard

In the OpenShift AI dashboard, go to Data Science Projects → my-project → Models, then choose to deploy a model on the single-model serving platform.

It should look like this:

To begin deploying a model, click Deploy model and fill in the form:

  1. Model deployment name. We will use the name qwen-model. You can use whatever name you choose, just make sure you update the field when you inference with the model.

  2. Serving runtime. We will use vLLM NVIDIA GPU ServingRuntime for KServe. The Model framework field autopopulates with vLLM.

  3. Framework and deployment mode. You do not need a framework, and in this example we can pick Knative Serverless.

  4. Hardware profile. Pick NVIDIA A100 GPU or NVIDIA H100 GPU — we will use H100s in this example.

Point the deployment at the PVC

  1. Under the source model location, select the Existing cluster storage option.

It should look like:

Two fields appear:

The resulting URI is pvc://qwen-model-pvc/Qwen2.5-3B-Instruct. The field is validated against /^pvc:\/\/[a-zA-Z0-9-]+\/[^/\s][^\s]*$/, which in practice means:

If you see the warning "The access mode of the selected cluster storage is not ReadWriteMany", the PVC was not created as RWX. Recreate it with accessModes: [ReadWriteMany] as shown in Create the model storage PVC.

Deploy and verify

Click Deploy. The model appears in the project's model list and moves to a started state once vLLM has loaded the weights — expect a few minutes, most of it spent reading the model off the PVC and into GPU memory.

From the CLI you can watch the same thing:

oc get inferenceservice
oc get pods -l serving.kserve.io/inferenceservice=qwen-model

To follow the model server's startup:

oc logs -f -l serving.kserve.io/inferenceservice=qwen-model -c kserve-container

The dashboard shows the inference endpoint on the model's row once it is ready. It should look like:

Using the model

The deployment serves an OpenAI-compatible API. Using the endpoint from the dashboard:

curl -sk https://<endpoint>/v1/chat/completions \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen-model",
    "messages": [{"role": "user", "content": "What is OpenShift?"}],
    "max_tokens": 100
  }'

Drop the Authorization header if you deployed without token authentication. If you did not expose an external route, the endpoint is only reachable from inside the cluster — run the curl from a pod in your project, or use oc port-forward.

Other useful endpoints: GET /v1/models and POST /v1/completions.

Cleaning up

GPUs are a scarce shared resource. Delete the deployment when you are not using it — either from the dashboard, or:

oc delete inferenceservice qwen-model
oc delete job model-loader

Deleting the InferenceService releases the GPU. The PVC and its weights survive, so you can redeploy later without downloading the model again. To reclaim the storage as well:

oc delete pvc qwen-model-pvc

Troubleshooting

Your project does not appear in the dashboard

Only S3 / URI / OCI shown under source model location

"The access mode ... is not ReadWriteMany"

Model path rejected by the form

Loader job never completes, and oc logs job/model-loader prints nothing

Model server pod stuck in Pending

Deployment rejected, or pod never created

vLLM starts but cannot find the model

Loader job fails with a 401 or 403

If you are stuck, open a ticket at https://osticket.massopen.cloud.