이번 내용은 워크샵에서 기회를 주셔서 EKS를 통한 대화형 GenAI를 구축해보는 실습이다.
아키텍처는 다음과 같다.
EKS 클러스터에 FSx를 볼륨으로 붙여서 학습데이터를 보관 및 사용한다. 그리고 AWS Inferentia는 GPU가 탑재된 AWS EC2로 노드그룹으로 활용할 것이다.
실습
먼저 CSI 드라이버를 배포한다.
FSx for Lustre를 사용하므로 해당 CSI 드라이버를 배포한다.
account-id를 먼저 환경변수화하자
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
CSI 드라이버가 사용할 IAM 정책 및 ServiceAccount 생성
cat << EOF > fsx-csi-driver.json
{
"Version":"2012-10-17",
"Statement":[
{
"Effect":"Allow",
"Action":[
"iam:CreateServiceLinkedRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy"
],
"Resource":"arn:aws:iam::*:role/aws-service-role/s3.data-source.lustre.fsx.amazonaws.com/*"
},
{
"Action":"iam:CreateServiceLinkedRole",
"Effect":"Allow",
"Resource":"*",
"Condition":{
"StringLike":{
"iam:AWSServiceName":[
"fsx.amazonaws.com"
]
}
}
},
{
"Effect":"Allow",
"Action":[
"s3:ListBucket",
"fsx:CreateFileSystem",
"fsx:DeleteFileSystem",
"fsx:DescribeFileSystems",
"fsx:TagResource"
],
"Resource":[
"*"
]
}
]
}
EOF
위 파일을 통해 정책을 생성한다.
aws iam create-policy \
--policy-name Amazon_FSx_Lustre_CSI_Driver \
--policy-document file://fsx-csi-driver.json
해당 정책을 ServiceAccount에 연결
eksctl create iamserviceaccount \
--region $AWS_REGION \
--name fsx-csi-controller-sa \
--namespace kube-system \
--cluster $CLUSTER_NAME \
--attach-policy-arn arn:aws:iam::$ACCOUNT_ID:policy/Amazon_FSx_Lustre_CSI_Driver \
--approve
잠시 기다려주면 연결이 완료된다.
생성된 역할 ARN을 변수에 저장
export ROLE_ARN=$(aws cloudformation describe-stacks --stack-name "eksctl-${CLUSTER_NAME}-addon-iamserviceaccount-kube-system-fsx-csi-controller-sa" --query "Stacks[0].Outputs[0].OutputValue" --region $AWS_REGION --output text)
echo $ROLE_ARN
Lustre용 FSx의 csi 드라이버 배포
kubectl apply -k "github.com/kubernetes-sigs/aws-fsx-csi-driver/deploy/kubernetes/overlays/stable/?ref=release-1.2"
배포 확인
kubectl get pods -n kube-system -l app.kubernetes.io/name=aws-fsx-csi-driver
IRSA를 위한 ServiceAccount에 annotation 정보 생성
kubectl annotate serviceaccount -n kube-system fsx-csi-controller-sa \
eks.amazonaws.com/role-arn=$ROLE_ARN --overwrite=true
이를 통해서 IAM 역할과 ServiceAccount를 연결하는 IRSA를 설정하고, FSx for Lustre의 CSI 드라이버를 배포하였다.
Step2. EKS 클러스터에 영구 볼륨 생성
종류 | 방법 |
정적 프로비저닝 | 관리자가 백엔드 스토리지 엔티티를 만들고 PV를 생성하며, 사용자는 이 PV가 자신의 Pod에서 사용되도록 클레임(PVC)을 만든다. |
동적 프로비저닝 | 사용자가 PVC를 요청하면 CSI 드라이버가 사용자 요구 사항에 따라 PV(및 해당 백업 스토리지 엔티티)를 자동으로 생성한다. |
다만 이번 실습에서는 정적 프로비저닝의 PV를 사용한다. 실습간에는 인스턴스가 Mistral-7B 모델을 저장하는 S3 버킷에 연결되어있다. PVC와 PV를 이용하여 vLLM Pod에 이 스토리지 볼륨을 사용해 Mistral-7B 모델 데이터에 엑세스 할 수 있도록 하는게 목표이다.
환경변수 생성
cd /home/ec2-user/environment/eks/FSxL
FSXL_VOLUME_ID=$(aws fsx describe-file-systems --query 'FileSystems[].FileSystemId' --output text)
DNS_NAME=$(aws fsx describe-file-systems --query 'FileSystems[].DNSName' --output text)
MOUNT_NAME=$(aws fsx describe-file-systems --query 'FileSystems[].LustreConfiguration.MountName' --output text)
PV 생성
플레이스홀더 변수가 포함된 PV yaml의 정의는 다음과 같다. 이 PV의 정의에서는 1200GiB FSx for Lustre 인스턴스가 fsx-pv라는 이름을 사용해 EKS 클러스터 리소스로 등록되도록 구성하는게 목표이다.
# fsxL-persistent-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: fsx-pv
spec:
persistentVolumeReclaimPolicy: Retain
capacity:
storage: 1200Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
mountOptions:
- flock
csi:
driver: fsx.csi.aws.com
volumeHandle: FSXL_VOLUME_ID
volumeAttributes:
dnsname: DNS_NAME
mountname: MOUNT_NAME
sed를 통해서 아까 환경변수화된 내용을 치환시킨다.
sed -i'' -e "s/FSXL_VOLUME_ID/$FSXL_VOLUME_ID/g" fsxL-persistent-volume.yaml
sed -i'' -e "s/DNS_NAME/$DNS_NAME/g" fsxL-persistent-volume.yaml
sed -i'' -e "s/MOUNT_NAME/$MOUNT_NAME/g" fsxL-persistent-volume.yaml
# 적용여부 확인
cat fsxL-persistent-volume.yaml
해당 파일을 배포해 fsx-pv라는 PV를 생성해보자
kubectl apply -f fsxL-persistent-volume.yaml
kubectl get pv
PVC 생성
앞서 생성한 fsx-pv와 바인딩할 PVC를 생성한다. fsx-pv의 volumeName 값을 이용해 미리 프로비저닝된 PV를 직접 참고한다.
# fsxL-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fsx-lustre-claim
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
resources:
requests:
storage: 1200Gi
volumeName: fsx-pv
배포해보자. 잘 바인딩된 것을 확인할 수 있다.
kubectl apply -f fsxL-claim.yaml
kubectl get pv,pvc
Step3. 실제 추론모델 배포
추론 모델 배포를 위해서 노드그룹을 만들것이다. 노드 그룹은 AWS Inferentia Accelerators를 사용하고 Karpenter NodePool을 통해서 노드 관리를 자동화한다. 이를 위한 EC2 NodeClass도 생성할 것이다.
Karpenter란
Karpenter는 NodePool을 통해 클러스터 내 노드 생성을 관리한다.
NodePool이란
NodePool은 어떤 노드가 생성될 수 있고, 어떤 파드가 그 노드에서 실행될 수 있는지 제약조건을 설정한다. 다양한 파드 특성에 맞춰 노드를 유연하게 프로비저닝할 수 있으며, AWS 관련 설정은 NodeClass로 분리하여 여러 NodePool이 공유할 수 있다. 클러스터에는 여러 NodePool을 구성할 수 있어 다양한 워크로드 요구사항을 충족시킬 수 있다.
#워크샵 경로
cd /home/ec2-user/environment/eks/genai
cat inferentia_nodepool.yaml
먼저 배포할 Karpenter NodePool의 정의이다. 여기서는 AWS Inferentia INF2 Accelerated Compute 노드를 위한 새로운 노드 풀을 생성하고, 여기에 생성형 AI 애플리케이션인 vLLM Pod를 구동한다.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: inferentia
labels:
intent: genai-apps
NodeGroupType: inf2-neuron-karpenter
spec:
template:
spec:
taints:
- key: aws.amazon.com/neuron
value: "true"
effect: "NoSchedule"
requirements:
- key: "karpenter.k8s.aws/instance-family"
operator: In
values: ["inf2"]
- key: "karpenter.k8s.aws/instance-size"
operator: In
values: [ "xlarge", "2xlarge", "8xlarge", "24xlarge", "48xlarge"]
- key: "kubernetes.io/arch"
operator: In
values: ["amd64"]
- key: "karpenter.sh/capacity-type"
operator: In
values: ["spot", "on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: inferentia
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmpty
# expireAfter: 720h # 30 * 24h = 720h
consolidateAfter: 180s
weight: 100
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: inferentia
spec:
amiFamily: AL2
amiSelectorTerms:
- alias: al2@v20240917
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
deleteOnTermination: true
volumeSize: 100Gi
volumeType: gp3
role: "Karpenter-eksworkshop"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "eksworkshop"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "eksworkshop"
tags:
intent: apps
managed-by: karpenter
배포 및 확인
kubectl apply -f inferentia_nodepool.yaml
kubectl get nodepool,ec2nodeclass inferentia
Neuron 장치 플러그인 및 스케줄러 설치
Neuron 장치 플러그인은 Neuron 코어와 장치를 쿠버네티스에 리소스로 설치한다.
kubectl apply -f https://raw.githubusercontent.com/aws-neuron/aws-neuron-sdk/master/src/k8/k8s-neuron-device-plugin-rbac.yml
kubectl apply -f https://raw.githubusercontent.com/aws-neuron/aws-neuron-sdk/master/src/k8/k8s-neuron-device-plugin.yml
Neuron 스케줄러
스케줄러는 두 개 이상의 Neuron 코어 또는 디바이스 리소스가 필요한 Pod를 스케줄링하는데 필요하다. 이를 확장하면 코어/디바이스 ID가 연속적이지 않은 노드를 필터링하고, 이를 필요로하는 POD에 연속적인 코어/디바이스 ID를 할당하도록 강제하게 된다.
kubectl apply -f https://raw.githubusercontent.com/aws-neuron/aws-neuron-sdk/master/src/k8/k8s-neuron-scheduler-eks.yml
kubectl apply -f https://raw.githubusercontent.com/aws-neuron/aws-neuron-sdk/master/src/k8/my-scheduler.yml
vLLM 애플리케이션 배포
실제 모델을 vLLM으로 배포해보자. vLLM파드가 정상적으로 실행되면 FSx for Lustre PV에서 Mistral-7B 모델을 메모리에 업로드하여 사용할 수 있게된다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-mistral-inf2-deployment
spec:
replicas: 1
selector:
matchLabels:
app: vllm-mistral-inf2-server
template:
metadata:
labels:
app: vllm-mistral-inf2-server
spec:
tolerations:
- key: "aws.amazon.com/neuron"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: inference-server
image: public.ecr.aws/u3r1l1j7/eks-genai:neuronrayvllm-100G-root
resources:
requests:
aws.amazon.com/neuron: 1
limits:
aws.amazon.com/neuron: 1
args:
- --model=$(MODEL_ID)
- --enforce-eager
- --gpu-memory-utilization=0.96
- --device=neuron
- --max-num-seqs=4
- --tensor-parallel-size=2
- --max-model-len=10240
- --served-model-name=mistralai/Mistral-7B-Instruct-v0.2-neuron
env:
- name: MODEL_ID
value: /work-dir/Mistral-7B-Instruct-v0.2/
- name: NEURON_COMPILE_CACHE_URL
value: /work-dir/Mistral-7B-Instruct-v0.2/neuron-cache/
- name: PORT
value: "8000"
volumeMounts:
- name: persistent-storage
mountPath: "/work-dir"
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: fsx-lustre-claim
---
apiVersion: v1
kind: Service
metadata:
name: vllm-mistral7b-service
spec:
selector:
app: vllm-mistral-inf2-server
ports:
- protocol: TCP
port: 80
targetPort: 8000
kubectl apply -f mistral-fsxl.yaml
실제 나온 ingress url에 들어가보면 잘 나오는 것을 확인할 수 있다.
http://open-webui-ingress-1491715457.us-west-2.elb.amazonaws.com/
약 7-8분 정도 지나면 다음과 같이 모델이 들어오게 된다. 아무래도 FSx와 통신하고 메모리에 적재되는데 시간이 오래걸린 것으로 보인다.
프롬프트를 작성해봤는데 해당 모델 자체는 과거 데이터로 학습된 것 같고, 최육대가 왜 나온건지 모르겠는데 한국어 기능은 좀 떨어지는 것 같다.
step 5. 데이터 레이어 테스트를 위한 사용자 전용 환경 만들기
이전 섹션에서는 미리 생성한 FSx 스토리지 인스턴스르 사용했으나 이번에는 사용자 본인이 직접 테스트용 환경을 구축하여 사용하는 실습을 진행한다.
CSI 드라이버의 동적 프로비저닝의 기능을 사용하여 PV, PVC, FSx for Lustre 파일 시스템 인스턴스를 자동으로 생성하는 방법을 알아보자.
일단 먼저 필요한 부분을 환경변수화 한다.
VPC_ID=$(aws eks describe-cluster --name $CLUSTER_NAME --region $AWS_REGION --query "cluster.resourcesVpcConfig.vpcId" --output text)
SUBNET_ID=$(aws eks describe-cluster --name $CLUSTER_NAME --region $AWS_REGION --query "cluster.resourcesVpcConfig.subnetIds[0]" --output text)
SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --filters Name=vpc-id,Values=${VPC_ID} Name=group-name,Values="FSxLSecurityGroup01" --query "SecurityGroups[*].GroupId" --output text)
echo $SUBNET_ID
# subnet-0b1aed52050e12821
echo $SECURITY_GROUP_ID
# sg-059c97b11b1c0e227
StorageClass 생성
cd /home/ec2-user/environment/eks/FSxL
# fsxL-storage-class.yaml 파일
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: fsx-lustre-sc
provisioner: fsx.csi.aws.com
parameters:
subnetId: SUBNET_ID
securityGroupIds: SECURITY_GROUP_ID
deploymentType: SCRATCH_2
fileSystemTypeVersion: "2.15"
mountOptions:
- flock
변수값 치환
sed -i'' -e "s/SUBNET_ID/$SUBNET_ID/g" fsxL-storage-class.yaml
sed -i'' -e "s/SECURITY_GROUP_ID/$SECURITY_GROUP_ID/g" fsxL-storage-class.yaml
생성 및 조회
kubectl apply -f fsxL-storage-class.yaml
kubectl get sc
PVC 정의 및 생성
fsx-lustre-sc StorageClass를 참조해 1200GiB를 요청한다.
# fsxL-dynamic-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fsx-lustre-dynamic-claim
spec:
accessModes:
- ReadWriteMany
storageClassName: fsx-lustre-sc
resources:
requests:
storage: 1200Gi
kubectl apply -f fsxL-dynamic-claim.yaml
kubectl get pvc
동적 프로비저닝으로 스토리지 관리자 없이도 FSx Lustre 인스턴스, PV, PVC를 생성할 수 있다. 사용자는 필요한 만큼 PVC 요청으로 정의 할 수 있다.
Bound까지는 8분 정도는 소요되는 것 같다.
FSx for Lustre 성능 테스트
FIO라는 디스크 I/O 부하 테스트 도구와, IOping 디스크 지연을 실시간으로 측정하는 도구를 사용하여 성능테스트 한다.
작업 디렉토리로 이동
cd /home/ec2-user/environment/eks/FSxL
FSx Lustre 인스턴스의 가용영역 확인
aws ec2 describe-subnets --subnet-id $SUBNET_ID --region $AWS_REGION | jq .Subnets[0].AvailabilityZone
성능 측정 파드를 생성한다.
vi pod_performance.yaml
kind: Pod
apiVersion: v1
metadata:
name: fsxl-performance
spec:
containers:
- name: fsxl-performance
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: fsx-lustre-dynamic-claim
nodeSelector:
topology.kubernetes.io/zone: us-west-2c
성능 테스트 pod 접속
kubectl exec -it fsxl-performance -- bash
# 패키지 설치
apt-get update
apt-get install fio ioping -y
IOping 테스트 (지연 시간 측정)
ioping -c 20 .
FIO 테스트 (처리량 측정)
블록크기 (bs) : 1MB
작업 방식(readwrite) : 랜덤 읽기/쓰기 randrw, 50:50
동시작업(numjobs) : 8개
mkdir -p /data/performance
cd /data/performance
fio --randrepeat=1 --ioengine=libaio --direct=1 --gtod_reduce=1 \
--name=fiotest --filename=testfio8gb --bs=1MB --iodepth=64 \
--size=8G --readwrite=randrw --rwmixread=50 \
--numjobs=8 --group_reporting --runtime=10
이렇게 FSx를 활용하고 성능테스트까지 가능한 귀중한 실습이었다.
뿐만 아니라 쿠버네티스 환경에서 LLM 모델을 올려서 서비스까지 해볼 수 있는 좋은 경험을 제공해주신 최영락님께도 감사드린다.