본문 바로가기
카테고리 없음

[AEWS 3기-11주차] Amazon EKS, Amazon FSx 및 AWS Inferentia를 사용하여 성능과 확장성을 위한 GenAI 및 ML 구축

by james_janghun 2025. 4. 19.

이번 내용은 워크샵에서 기회를 주셔서 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를 생성하는 2가지 방법이 있다.
종류 방법
정적 프로비저닝 관리자가 백엔드 스토리지 엔티티를 만들고 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 모델을 올려서 서비스까지 해볼 수 있는 좋은 경험을 제공해주신 최영락님께도 감사드린다.