Skip to content

Deploying GLM-5.2 on Amazon EKS using vLLM, Deep Learning Containers (DLCs), and FSx for Lustre

12 minute read
Content level: Advanced
0

Example on how to deploy GLM-5.2 on Amazon EKS using vLLM, DLCs, P5en instances and FSx for Lustre

GLM-5.2 is one of the strongest open-weight large language models available today - a ~753B-parameter Mixture-of-Experts (MoE) model with DeepSeek-style sparse attention. Its FP8 checkpoint (zai-org/GLM-5.2-FP8) is roughly 705 GB across 141 safetensors shards, and it supports a context window of up to 1 million tokens.

In this post we deploy GLM-5.2-FP8 on Amazon EKS across 2x p5en.48xlarge instances (16× NVIDIA H200 GPUs total, ~2.26 TB of HBM3e). We’ll leverage the AWS Deep Learning Container (DLCs) that are pre-configured with necessary libraries and dependencies, including Elastic Fabric Adapter (EFA) drivers optimized for high-throughput, low-latency inter-node communications and GPUDirect RDMA support for running distributed inferencing.

We combine several building blocks that each solve a specific problem:

  • vLLM on the AWS Deep Learning Container (DLC) as the inference engine - the stock vLLM 0.23.0 DLC already supports GLM-5.2. We'll just be adding a thin layer (Run:AI streamer + Ray) on top of it.
  • Tensor parallelism (TP=8) inside each node over NVLink, and pipeline parallelism (PP=2) across the two nodes over Elastic Fabric Adapter (EFA) with GPUDirect RDMA.
  • LeaderWorkerSet (LWS) and Ray to model and coordinate the two-node serving group on Kubernetes.
  • Amazon FSx for Lustre as a shared, high-throughput file system for the model weights, paired with the Run:AI Model Streamer for fast loading.
  • An EC2 Capacity Block for ML for 2x p5en.48xlarge instances

The model is sharded two ways at once: PP=2 splits the transformer layers across the two nodes, and within each node TP=8 shards every layer across its 8 GPUs. A LeaderWorkerSet describes the group (1 leader + 1 worker); Ray forms the two-node cluster, and vLLM runs on top with --tensor-parallel-size 8 --pipeline-parallel-size 2.

This guide assumes you have intermediate Kubernetes experiences and are familiar with Amazon EKS and AWS CLI.

All artefacts used by this post are available here.

 

Prerequisites

  • An EC2 Capacity Block for ML reservation for 2× p5en.48xlarge. Note its reservation ID and Availability Zone - the FSx file system and the GPU node group both pin to that single AZ.
  • eksctl ≥ v0.205.0 (native Capacity-Block support), plus kubectl, helm, and the AWS CLI.
  • A Hugging Face token with access to zai-org/GLM-5.2-FP8.
  • An x86 build host with Docker, and a private Amazon ECR repository in your region.
  • Familiarity with EKS, and IAM permissions to create clusters, node groups, and FSx file systems.
  • The following tools:

We use region ap-northeast-2 (Seoul) and Kubernetes 1.36 throughout this post, adjust these to your own environment.

Set the below in your shell:

export AWS_REGION="ap-northeast-2"; export REGION="$AWS_REGION"
export ACCOUNT_ID="<ACCOUNT_ID>"
export CLUSTER_NAME="glm52-poc-cluster"
export K8S_VERSION="1.36"
export ECR_REPO="glm52-vllm"; export IMAGE_TAG="glm52-fp8"
export IMAGE_URI="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ECR_REPO}:${IMAGE_TAG}"
export DLC_BASE="763104351884.dkr.ecr.${REGION}.amazonaws.com/vllm:0.23.0-gpu-py312-cu130-ubuntu22.04-ec2"
export HF_MODEL="zai-org/GLM-5.2-FP8"
export MODEL_DIR="/mnt/fsx/glm-5.2-fp8"
export HF_TOKEN="<hf_token>"     # rotate after use

 

Step 1 - Create an EKS cluster with CPU nodes only

We create the EKS control plane and a small CPU-based system node group first, with no GPU nodes. The GPU node group comes later, once the ML Capacity Block is active - this lets us build everything else (FSx, the customized vLLM image, the weights) ahead of the reservation window.

envsubst < 01-cluster/cluster.yaml | eksctl create cluster -f -

The cluster.yaml spans 2x Availability Zones (ap-northeast-2a + 2b), with the ML Capacity Block (for the 2x p5en.48xlarge) available in ap-northeast-2a. The system node group uses privateNetworking: true so all nodes (CPU + GPU) and FSx stay in private subnets.

Cluster creation takes about 15 minutes.

 

Step 2 - Create an FSx for Lustre file system

FSx for Lustre gives all serving pods a shared, high-throughput ReadWriteMany volume for the weights.

On FSx for Lustre, capacity determines the number of Object Storage Targets (OSTs) - at the 1000 MB/s/TiB tier, 1 OST = 4.8 TiB. Weight-load time scales almost linearly with OST count (more on this in Results). We use 19.2 TiB = 4 OSTs as a balanced baseline.

First, create a dedicated security group. FSx validates its network settings at creation, and an EFA-enabled file system needs a self-referencing rule in both directions plus the Lustre ports:

export VPC_ID=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$REGION" \
  --query 'cluster.resourcesVpcConfig.vpcId' --output text)
export NODE_SG_ID=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$REGION" \
  --query 'cluster.resourcesVpcConfig.clusterSecurityGroupId' --output text)

export FSX_SG_ID=$(aws ec2 create-security-group --region "$REGION" \
  --group-name glm52-fsx-lustre-sg --description 'GLM-5.2 FSx Lustre' --vpc-id "$VPC_ID" \
  --query 'GroupId' --output text)

aws ec2 authorize-security-group-ingress --group-id "$FSX_SG_ID" --protocol -1 --source-group "$FSX_SG_ID" --region "$REGION"
aws ec2 authorize-security-group-egress  --group-id "$FSX_SG_ID" --protocol -1 --source-group "$FSX_SG_ID" --region "$REGION"
aws ec2 authorize-security-group-ingress --group-id "$FSX_SG_ID" --protocol tcp --port 988       --source-group "$NODE_SG_ID" --region "$REGION"
aws ec2 authorize-security-group-ingress --group-id "$FSX_SG_ID" --protocol tcp --port 1018-1023 --source-group "$NODE_SG_ID" --region "$REGION"

Then create the file system in the Capacity Block's AZ (the FSx API takes a subnet ID, so we look up the private subnet in that AZ):

export FSX_CAPACITY_GIB="19200" 
export FSX_THROUGHPUT_TIER="1000"
export CB_AZ="ap-northeast-2a"
export CB_SUBNET_ID=$(aws ec2 describe-subnets --region "$REGION" \
  --filters "Name=vpc-id,Values=$VPC_ID" "Name=availability-zone,Values=$CB_AZ" \
            "Name=tag:kubernetes.io/role/internal-elb,Values=1" \
  --query 'Subnets[0].SubnetId' --output text)
echo "CB_SUBNET_ID=$CB_SUBNET_ID"  

aws fsx create-file-system --region "$REGION" \
  --file-system-type LUSTRE --storage-type SSD \
  --storage-capacity "$FSX_CAPACITY_GIB" \
  --subnet-ids "$CB_SUBNET_ID" --security-group-ids "$FSX_SG_ID" \
  --lustre-configuration '{
      "DeploymentType":"PERSISTENT_2",
      "PerUnitStorageThroughput":'"$FSX_THROUGHPUT_TIER"',
      "EfaEnabled":true,
      "MetadataConfiguration":{"Mode":"AUTOMATIC"},
      "DataCompressionType":"NONE"
    }'

Once it reaches AVAILABLE, install the FSx CSI driver and bind a static PV/PVC:

helm repo add aws-fsx-csi-driver https://kubernetes-sigs.github.io/aws-fsx-csi-driver/ && helm repo update
helm upgrade --install aws-fsx-csi-driver aws-fsx-csi-driver/aws-fsx-csi-driver -n kube-system

aws fsx describe-file-systems --region "$REGION" \
  --query 'FileSystems[-1].[FileSystemId,LustreConfiguration.MountName,Lifecycle]' --output table

export FSX_ID=<from the output above>; export FSX_MOUNT_NAME=<from the output above>
export FSX_DNS="${FSX_ID}.fsx.${REGION}.amazonaws.com"
envsubst < 02-fsx/fsx-pv.yaml  | kubectl apply -f -
envsubst < 02-fsx/fsx-pvc.yaml | kubectl apply -f -

$ kubectl get pv
NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
fsx-glm52-pv   19200Gi    RWX            Retain           Bound    default/fsx-glm52-pvc                  <unset>                          11s
$ kubectl get pvc
NAME            STATUS   VOLUME         CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
fsx-glm52-pvc   Bound    fsx-glm52-pv   19200Gi    RWX                           <unset>                 11s

Use a helper pod to confirm the mount and verify the four OSTs:

kubectl apply -f 02-fsx/fsx-test-pod.yaml
kubectl wait --for=condition=Ready pod/fsx-test --timeout=180s 

$ kubectl exec -it fsx-test -- lfs df -h /mnt/fsx
UUID                       bytes        Used   Available Use% Mounted on
bok2lbev-MDT0000_UUID      549.9G        9.5M      549.9G   1% /mnt/fsx[MDT:0]
bok2lbev-OST0000_UUID        4.5T        7.8M        4.5T   1% /mnt/fsx[OST:0]
bok2lbev-OST0001_UUID        4.5T        7.5M        4.5T   1% /mnt/fsx[OST:1]
bok2lbev-OST0002_UUID        4.5T        7.5M        4.5T   1% /mnt/fsx[OST:2]
bok2lbev-OST0003_UUID        4.5T        7.5M        4.5T   1% /mnt/fsx[OST:3]

filesystem_summary:        18.0T       30.2M       18.0T   1% /mnt/fsx

 

Step 3 - Add the Capacity-Block GPU node group

A single eksctl config consumes the Capacity Block and builds the launch template.

export CB_RESERVATION_ID="<CB_RESERVATION_ID>"
export CB_AZ="ap-northeast-2a"
eksctl create nodegroup -f <(envsubst < 03-nodegroup/nodegroup.yaml)

# When the CB is active, scale the node group from 0 to 2:
eksctl scale nodegroup --cluster "$CLUSTER_NAME" --name p5en-cb --nodes 2 --nodes-max 2 --region "$REGION"

Once the nodes join, install the NVIDIA device plugin and verify the GPUs are allocatable resources on each node:

kubectl apply -f 03-nodegroup/nvidia-device-plugin.yaml
for NODE in $(kubectl get nodes -l role=gpu -o name); do
  echo -n "$NODE gpu="; kubectl get "$NODE" -o jsonpath='{.status.allocatable.nvidia\.com/gpu}'
  echo -n " efa=";      kubectl get "$NODE" -o jsonpath='{.status.allocatable.vpc\.amazonaws\.com/efa}{"\n"}'
done

node/ip-192-168-72-123.ap-northeast-2.compute.internal  gpu=8  efa=16
node/ip-192-168-88-156.ap-northeast-2.compute.internal  gpu=8  efa=16

 

Step 4 - Validate the network fabric with a NCCL test

To prove the inter-node high-bandwidth, low-latency fabric works, we run the standard NCCL all_reduce_perf test across both p5en nodes using the public nccl-tests image.

# Install the Kubeflow MPI Operator once (provides the MPIJob CRD):
kubectl apply --server-side -f https://raw.githubusercontent.com/kubeflow/mpi-operator/v0.8.0/deploy/v2beta1/mpi-operator.yaml
kubectl get crd mpijobs.kubeflow.org   

kubectl apply -f 04-efa-validation/nccl-test-mpijob.yaml
kubectl logs -f <nccl-tests-launcher-pod>

Look for NCCL INFO NET/OFI Selected Provider is efa (confirming EFA transport, not a TCP fallback):

[1,6]<stdout>:nccl-tests-worker-0:770:770 [6] NCCL INFO NET/OFI Plugin selected platform: AWS
[1,6]<stdout>:nccl-tests-worker-0:770:770 [6] NCCL INFO NET/OFI Configuring AWS-specific options
[1,6]<stdout>:nccl-tests-worker-0:770:770 [6] NCCL INFO NET/OFI Internode latency set at 35.0 us
[1,6]<stdout>:nccl-tests-worker-0:770:770 [6] NCCL INFO NET/OFI Using transport protocol RDMA (platform set)
[1,12]<stdout>:nccl-tests-worker-1:764:764 [4] NCCL INFO NET/OFI Selected provider is efa, fabric is efa-direct (found 16 nics)

On our 2-node, 16-GPU setup, bus bandwidth ramped to ~488 GB/s at the large-message tail - combining intra-node NVSwitch and cross-node EFA GPUDirect RDMA. (look for a busbw column that climbs to a plateau at the large-message sizes, and 0 wrong at every size. )


[1,0]<stdout>:#                                                              out-of-place                       in-place          
[1,0]<stdout>:#       size         count      type   redop    root     time   algbw   busbw  #wrong     time   algbw   busbw  #wrong 
[1,0]<stdout>:#        (B)    (elements)                               (us)  (GB/s)  (GB/s)             (us)  (GB/s)  (GB/s)         

[truncated]

[1,0]<stdout>:   536870912     134217728     float     sum      -1  2466.66  217.65  408.10       0  2456.98  218.51  409.70       0
[1,0]<stdout>:  1073741824     268435456     float     sum      -1  4531.65  236.94  444.27       0  4537.00  236.66  443.74       0
[1,0]<stdout>:  2147483648     536870912     float     sum      -1  9000.57  238.59  447.36       0  9034.22  237.71  445.70       0
[1,0]<stdout>:  4294967296    1073741824     float     sum      -1  17349.7  247.55  464.16       0  17369.6  247.27  463.63       0
[1,0]<stdout>:  8589934592    2147483648     float     sum      -1  33684.6  255.01  478.15       0  33685.6  255.00  478.13       0
[1,0]<stdout>: 17179869184    4294967296     float     sum      -1  66054.7  260.09  487.66       0  66006.7  260.27  488.01       0

 

Step 5 - Build the custom vLLM image

The stock vLLM 0.23.0 DLC already supports GLM-5.2's GlmMoeDsaForCausalLM architecture, so we don't need to upgrade vLLM or torch. Our image is a thin layer that adds exactly two things:

  1. The Run:AI Model Streamer, which enables --load-format runai_streamer for concurrent weight loading.
  2. Ray, which is required for multi-node vLLM serving.
aws ecr get-login-password --region "$REGION" | docker login --username AWS --password-stdin 763104351884.dkr.ecr.${REGION}.amazonaws.com
aws ecr get-login-password --region "$REGION" | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com
aws ecr create-repository --repository-name "$ECR_REPO" --region "$REGION" 2>/dev/null || true

docker build --platform linux/amd64 --build-arg DLC_BASE="$DLC_BASE" -t "$IMAGE_URI" 05-image/
docker push "$IMAGE_URI"

 

Step 6 - Stage the weights onto FSx

A one-time Kubernetes Job downloads the ~705 GB model into the FSx volume using the Hugging Face Xet high-performance backend:

kubectl create secret generic hf-token --from-literal=token="$HF_TOKEN"
envsubst '${MODEL_DIR} ${HF_MODEL}' < 06-weights/download-weights-job.yaml | kubectl apply -f -

$ kubectl get job glm52-weight-download -w  
NAME                    STATUS    COMPLETIONS   DURATION   AGE
glm52-weight-download   Running   0/1           19s        19s
glm52-weight-download   Running   0/1           13m        13m
glm52-weight-download   SuccessCriteriaMet   0/1           14m        14m
glm52-weight-download   Complete             1/1           14m        14m

Once it completes, confirm the weights spread evenly across all four OSTs (approx. 176G per OST):

$ kubectl exec fsx-test -- lfs df -h /mnt/fsx
UUID                       bytes        Used   Available Use% Mounted on
bok2lbev-MDT0000_UUID      549.9G       12.8M      549.9G   1% /mnt/fsx[MDT:0]
bok2lbev-OST0000_UUID        4.5T      178.3G        4.3T   4% /mnt/fsx[OST:0]
bok2lbev-OST0001_UUID        4.5T      173.2G        4.3T   4% /mnt/fsx[OST:1]
bok2lbev-OST0002_UUID        4.5T      176.2G        4.3T   4% /mnt/fsx[OST:2]
bok2lbev-OST0003_UUID        4.5T      176.4G        4.3T   4% /mnt/fsx[OST:3]

filesystem_summary:        18.0T      704.1G       17.3T   4% /mnt/fsx

 

Step 7 - Deploy GLM-5.2 with LeaderWorkerSet and Ray

Install the LeaderWorkerSet controller, then deploy the two-node serving group and a ClusterIP Service:

helm install lws oci://registry.k8s.io/lws/charts/lws --version 0.9.0 -n lws-system --create-namespace --wait
kubectl -n lws-system get pods    # wait for lws-controller-manager to be Running before applying below

envsubst '$IMAGE_URI $MODEL_DIR' < 07-serving/lws-serving.yaml | kubectl apply -f -
kubectl apply -f 07-serving/service.yaml
kubectl get pods -l app=glm52 -w      # glm52-0 (leader) + glm52-0-1 (worker); wait for glm52-0 = 1/1

The leader starts the Ray head, waits for the worker to register all 16 GPUs, then launches vLLM:

vllm serve /mnt/fsx/glm-5.2-fp8 \
  --served-model-name glm-5.2-fp8 \
  --tensor-parallel-size 8 --pipeline-parallel-size 2 \
  --distributed-executor-backend ray \
  --load-format runai_streamer \
  --model-loader-extra-config '{"concurrency":32,"memory_limit":10737418240}' \
  --kv-cache-dtype fp8 \
  --max-model-len 1048576 \
  --gpu-memory-utilization 0.90 \
  --trust-remote-code \
  --tool-call-parser glm47 --enable-auto-tool-choice \
  --host 0.0.0.0 --port 8000

A few things worth calling out in the serving manifest:

  • We set TP=8 for intra-node parallelism over NVLink, and PP=2 for inter-node pipeline parallelism using GPUDirect RDMA (over EFA)
  • We set --max-model-len 1048576 to provide GLM-5.2's full 1M-context window.
  • We point VLLM_CACHE_ROOT at the FSx volume. This persists vLLM's torch.compile and DeepGEMM kernel caches on shared storage for warm start.
  • We run the Run:AI streamer at concurrency 32

Test it

From any in-cluster pod (or use the fsx test pod), hit the OpenAI-compatible endpoint:

kubectl exec fsx-test -- curl -sS http://glm52-leader.default.svc.cluster.local:8000/v1/chat/completions \
  -H content-type:application/json \
  -d '{"model":"glm-5.2-fp8","messages":[{"role":"user","content":"What is Kubernetes? Answer in 3 sentences."}],"max_tokens":300}' \
  | python3 -m json.tool

GLM-5.2 is a reasoning model - it "thinks out loud" before giving its answer, so expect a verbose trace.

Benchmark

Throughput tests (2 nodes, vllm bench serve, 200 prompts, concurrency 64, 1024-in / 256-out, run from the LWS LEADER pod):

$ kubectl exec glm52-0 -- bash -lc 'vllm bench serve --backend openai-chat --model /mnt/fsx/glm-5.2-fp8 \
--served-model-name glm-5.2-fp8 --tokenizer /mnt/fsx/glm-5.2-fp8 \
--base-url http://localhost:8000 --endpoint /v1/chat/completions \
--dataset-name random --num-prompts 200 --max-concurrency 64 \
--random-input-len 1024 --random-output-len 256'
MetricValue
Total token throughput~7,900 tok/s
Output token throughput~1,570 tok/s (peak ~2,000)
Median time-to-first-token~274 ms
Median inter-token latency~35 ms

Storage and cold start. We measured the weight-load phase across different FSx OST counts (loading the same 705 GB, dropping the OS page cache before each run):

FSx OSTsCapacityAvg weight-loadFull cold start (warm caches)
29.6 TiB~676 s~840 s
419.2 TiB~382 s~540 s
628.8 TiB~274 s~400 s

 

Cleanup

To avoid incurring long-term charges, delete the AWS resources created as part of the demo walkthrough.

kubectl delete lws glm52 ; kubectl delete svc glm52-leader
kubectl delete job glm52-weight-download ; kubectl delete secret hf-token
kubectl delete pvc fsx-glm52-pvc ; kubectl delete pv fsx-glm52-pv
aws fsx delete-file-system --file-system-id "$FSX_ID" --region "$REGION" 
eksctl delete cluster --name "$CLUSTER_NAME" --region "$REGION"

 

Conclusion

In this post, we walked through a deployment example for running the GLM-5.2 model on Amazon EKS using vLLM, DLCs, served by EC2 p5en.48xlarge instances with FSx for Lustre integrations.

References